[K8s & Github Action] self-hosted-runner를 k8s로 구성해보자

[K8s & Github Action] self-hosted-runner를 k8s로 구성해보자
Photo by Yancy Min / Unsplash

[요약]

k8s 클러스터 구성

  1. k8s 클러스터 구성시 공식 문서에 나와있는 kubeadm 쓰지 말자. 훨씬 편리한 프로젝트들 많이 있다(k0s, k3s).

Github Action

  1. Github Action은 runner로 자신의 서버를 쓸 수 있는 self-hosted runner를 지원한다.
  2. ARC를 사용하면 k8s에 self-hosted runner를 비교적 쉽게 구성할 수 있다.
  3. 그러나 ARC는 self-hosted runner에서 사용할 수 있는 이미지가 제한되는 단점이 있다.
  4. 직접 pod에 self-hosted runner를 구성해서 github action을 사용할 수 있다. 하지만 현재 github API에 이슈가 있고 ARC를 사용할 때보다 귀찮다.

기타

쿠버네티스를 다룰 때 Lens IDE를 설치해서 사용하면 편리하다.


목차

  1. k0s로 쿠버네티스 클러스터 구성하기
  2. ARC: k8s 클러스터로 Github Action runner 구성
  3. ARC의 단점
  4. self-hosted runner를 직접 pod에 구성하기
  5. (참고) Lens IDE

Github Action 적용시 github에서 지원하는 runner를 사용하거나 혹은 자신의 서버(self-hosted runner)를 사용해야 한다.  

Self-hosted runner에 대한 가이드는 물리 서버를 등록하는 것 밖에 나와있지 않다. 만약 쿠버네티스 클러스터를 self-hosted runner로 사용하고 싶으면 어떻게 할까?

AWS나 GCP 등을 이용한다면 서비스에서 지원해주는 Github Action 연동을 사용하면 된다. 하지만 만약 자신이 직접 쿠버네티스 클러스터를 구축해서 사용하고 있다면 선택지는 1) ARC 사용, 2) runner를 설치한 이미지 띄우기 두 가지밖에 없다.

이번 글에서는 ARC와 runner를 설치한 이미지 띄우기 두 가지 모두 살펴볼 것이다. 그리고 추가적으로 쿠버네티스 클러스터를 편리하게 설치할 수 있는 k0s에 대해서도 간단하게 알아볼 것이다.

1. k0s로 쿠버네티스 클러스터 구성하기

서버에 쿠버네티스 클러스터를 쉽게 구성하려면 어떻게 해야할까?

쿠버네티스 공식 문서에서는 kubeadm으로 클러스터를 Bootstrapping하는 방법을 소개하고 있다. CKA(Certified Kubernetes Administrator)에서도 kubeadm을 다루고 있으니 정석적인 방법으로 쿠버네티스를 공부한 사람은 kubeadm으로 클러스터를 구성하는 방법을 가장 먼저 떠올릴 것이다.

kubeadm이 쿠버네티스 클러스터 구성을 도와주긴 하지만 여전히 수동으로 CRI, CNI 등을 설치 및 구성을 해야하는 등 귀찮은 부분이 많다.

밖으로 눈을 돌려보면 kubeadm말고도 쿠버네티스 클러스터를 bootstrapping 할 수 있는 솔루션들이 있다. 이번 글에서는 그 중 하나를 소개하려고 한다. 바로 k0s이다.

k0s를 사용하면 싱글 바이너리로 쿠버네티스 클러스터를 구성할 수 있다. 설치부터 구성까지 30분 이내로 완료할 수 있다. docs를 보면 사용방법이 4단계만으로 끝난다.

https://docs.k0sproject.io/v1.26.3+k0s.0/k0sctl-install/

configuration 파일에 워커 노드의 주소 및 ssh key만 담아놓으면 바이너리 실행 한번으로 모든 워커 노드 및 Controller에 배포가 가능하다.

클러스터를 구성했다면 Github Action과 연동하러 가보자.

2. ARC: k8s 클러스터로 Github Action runner 구성

ARC github

참고) ARC는 Runner가 실행되는 pod의 이미지가 제한된다는 단점이 있습니다. 글의 뒷 부분에 다른 방법도 소개했으니 끝까지 읽고 ARC를 사용할 지 고려하는 것을 추천합니다.

ARC란?

k8s 클러스터에서 Github Actions Runner를 쉽게 실행할 수 있도록 도와주는 툴이다. Github에서 직접 관리하고 있는 프로젝트이다.

https://github.com/actions/actions-runner-controller/blob/master/docs/about-arc.md

ARC에서 제공하는 crd(Custom Resource Definition)을 k8s 클러스터에 배포하면 runner가 담긴 pod이 배포가 된다.

Quick Start Guide

설치 방법은 문서에 잘 정리되어 있다.

(1) Cert-manager 설치

cert-manager 문서를 보고 설치를 하면 된다. 설치 이후 "kubectl get all -A"로 cert-manager 리소스가 존재하는지 확인해보면 좋다.

(2) 토큰 생성(Personal Access Token)

ARC를 사용하려면 github에서 PAT 토큰을 생성해야 한다(PAT 토큰을 생성하는 방법은 다른 글을 참고하자). 이 PAT 토큰은 (3)에서 사용할 것이다.

(3) Deploy and Configure ARC

공식 문서에서는 Helm과 kubectl로 ARC를 배포하는 방법 2가지를 소개하고 있다. 나는 두 가지 방법으로 모두 시도했으나 Kubectl로 배포하는 경우 에러가 발생해서 Helm을 사용하는 것을 추천한다.

만약 Github Enterprise를 사용하고 있다면 Helm 차트 설치시 문서에 나온 옵션 외에도 몇 가지 옵션을 더 추가해야 한다. 옵션 명세는 Artifact Hub에서 찾을 수 있다.

helm upgrade --install --namespace actions-runner-system --create-namespace\
--set=authSecret.create=true\
--set=authSecret.github_token={(2)에서 생성한 PAT 토큰}\
--set=githubEnterpriseServerURL={Enterpise Server URL}\
--set=githubURL={API call시 사용되는 URL}\
--set=runnerGithubURL={registration시 사용되는 URL}\
--wait actions-runner-controller actions-runner-controller/actions-runner-controller

githubEnterpriseServerURL, githubURL, runnerGithubURL 옵션을 추가해야 Github Enterprise에서도 ARC를 사용할 수 있다.

설치 이후 다음 커맨드로 actions-runner-system 네임스페이스의 리소스를 확인 가능하다

kubectl get all -n actions-runner-system

(4) ARC runner 추가

공식 문서

apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
  name: example-runnerdeploy
spec:
  replicas: 1
  template:
    spec:
      repository: {repository_name}
runnerdeployment.yaml
$ kubectl apply -f runnerdeployment.yaml
runnerdeployment.actions.summerwind.dev/example-runnerdeploy created

여기까지 따라왔다면 잘 됐는지 체크해보자.

# runner 리소스 확인
$ kubectl get runners
# Events 확인
$ kubectl describe runners {runner 이름}
# actions-runner-controller pod 로그 확인
$ kubectl get pods -n actions-runner-system
$ kubectl logs {actions-runner-controller pod 이름} -n actions-runner-system

이상이 없다면 Repository의 setting의 actions에서 등록이 된 것을 확인할 수 있을 것이다.

3. ARC의 단점

ARC를 이용하면 쉽게 쿠버네티스 클러스터 위에 Runner를 구성할 수 있다.

그러나 한 가지 치명적인 단점이 있다. Runner가 실행될 pod의 이미지는 ubuntu-18.04, ubuntu-20.04, ubuntu-22.04 총 3가지 밖에 지원이 안된다. Runner의 이미지는 summerwind/actions-runner이고 이 이미지의 tag는 위 3가지만 지원한다. 그리고 앞으로도 다른 OS 지원은 되지 않을 것 같다.

ubuntu가 아닌 centos나 debian 등 다른 OS를 사용하는 서버에서 서비스를 운영하고 싶다면 ARC에서 테스트하는 것은 좋지 않다.

4. self-hosted runner를 직접 pod에 구성하기

ARC를 사용하면 원하는 이미지에서 runner를 띄울 수 없다. 현재로서는 원하는 이미지를 사용하려면 직접 pod을 띄우고 그 안에서 직접 Runner를 실행하는 방법밖에 없다.

직접 Pod을 띄워 runner를 등록하는 것은 엄청 어렵지 않지만 인증 과정 등 신경써야할 부분들이 있다. 차근 차근 어떻게 runner를 구성하는지 알아보자.

1) Token 및 변수 관리

Runner를 등록하기 위해서 1) 등록될 레포지토리 정보와 2) 인증시 사용할 토큰 관리가 필요하다. 먼저 레포지토리의 정보를 ConfigMap으로 구성하자

apiVersion: v1
kind: ConfigMap
metadata:
  name: repo-config
  namespace: default
data:
  REPO_OWNER: {Repository Owner}
  REPO_NAME: {Repository Name}
ConfigMap

Runner를 등록하려면 레포지토리로부터 Registration Token을 발급받아야 한다. 이 토큰을 Secret 등으로 관리할 수 있지만 유효기간이 한 시간이기 때문에 직접 다루기 까다롭다.

그래서 우리는 Pod을 띄울때마다 PAT 토큰을 이용해 서버로부터 Registration Token을 발급받는 방법을 사용할 것이다. PAT 토큰은 유효기간을 길게 잡거나 아예 없앨 수 있으므로 관리하기 편하다. Secret으로 PAT 토큰을 관리하자.

apiVersion: v1
kind: Secret
metadata:
  name: github-runner-token
type: Opaque
stringData:
  pat-token: {PAT-Token}
secret

2) StatefulSet용 Service 구성하기

Runner를 띄울 Pod은 Deployment가 아닌 StatefulSet으로 구성한다. 등록한 Runner의 이름은 예상 가능해야한다. 만약 Pod이 재시작했을 때 다른 이름으로 Runner가 등록된다면 Runner를 관리하기 어렵다. StatefulSet은 Deployment와 달리 Pod의 이름이 {StatefulSet-name}-{index}로 정해진다. 그래서 Pod이 재시작해도 이름이 변하지 않아 Pod의 이름으로 Runner를 등록시 관리하기 편하다.

StatefulSet을 구성하려면 StatefulSet에서 사용하는 Service도 같이 구성해야 한다. Service에 큰 역할을 맡기지 않을 것이므로 Headless로 구성하자(ClusterIP: None)

apiVersion: v1
kind: Service
metadata:
  name: github-runner-service
spec:
  selector:
    app: github-runner
  clusterIP: None
  ports:
  - name: http
    port: 80
    protocol: TCP
service

3) Dockerfile 구성

Base image 예시로 centos를 사용할 것이다. 자신의 서비스에 맞는 base image를 선택하여 구성하면 된다.

아래 예시 Dockerfile은 linux amd64용 스크립트를 받아온다. 실행하는 이미지 및 호스트의 아키텍처에 맞게 스크립트를 설정해야한다.
FROM centos:7

ARG RUNNER_VERSION="2.304.0"

RUN yum install -y wget && \
    wget http://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm && \
    rpm -ivh epel-release-latest-7.noarch.rpm && \
    yum install -y curl sudo && \
    yum install -y libicu openssl-libs krb5-libs zlib libicu-devel && \
    curl -O -L https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-amd64-${RUNNER_VERSION}.tar.gz && \
    sudo mkdir /github-runner && \
    tar xzf actions-runner-linux-amd64-${RUNNER_VERSION}.tar.gz -C /github-runner && \
    rm -f actions-runner-linux-amd64-${RUNNER_VERSION}.tar.gz && \
    chown -R 1000:1000 /github-runner

USER 1000

ENV REPO_OWNER=""
ENV REPO_NAME=""
ENV GITHUB_PAT=""
ENV HOST_NAME="DEFAULT_HOST"

ENTRYPOINT /github-runner/config.sh --url https://github.com/${REPO_OWNER}/${REPO_NAME} --pat ${GITHUB_PAT} --name ${HOSTNAME} --unattended --replace && \
            bash /github-runner/run.sh
Github 이슈로 ENTRYPOINT에서 PAT 토큰으로 인증해야 한다.

💡
위 Dockerfile은 PAT 토큰을 이용해 인증을 합니다. 하지만 정석적인 방법은 PAT 토큰이 아니라 registration token으로 인증하는 것입니다. Registration token의 expiration time이 훨씬 짧기 때문에 안전하기 때문입니다.
그러나 현재 github의 API로 registration token을 받아올 시 registration token이 즉시 만료되는 이슈가 있습니다.
Github에서 버그가 고쳐지면 아래 코드를 이용해 registration token을 통해 인증하는 것을 추천합니다.
...

yarn install -y jq && \

...

ENTRYPOINT REG_TOKEN=$(curl -X POST -H "Accept: application/vnd.github+json" -H "Authorization: Bearer ${GITHUB_PAT}" "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/actions/runners/registration-token" | jq '.token') && \    
            /github-runner/config.sh --url https://github.com/${REPO_OWNER}/${REPO_NAME} --token ${REG_TOKEN} --name ${HOSTNAME} --unattended --replace && \
            bash /github-runner/run.sh
정석 방법

Registration Token을 받아오는 것은 API 명세서를 참고하였습니다.


Dockerfile은 먼저 runner 설치 스크립트를 받아온다. 이 과정은 특별한 것은 없지만 알아두어야 할 부분이 있다. chown -R 1000:1000 /github-runner 이다. Github은 root 권한으로 runner 실행을 보안상의 이유로 권장하지 않는다. 하지만 스크립트를 root로 설치했으므로 root가 아닌 유저로 변경 실행시 권한 문제가 발생한다. chown 명령으로 github-runner의 owner를 userID 1000과 groupID 1000으로 변경해 userID 1000으로 실행할 수 있도록 만든다.

이후 스크립트를 실행해 github에 인증하고 runner를 등록한다. 인증은 Registration Token을 이용해 하는 것이 정석이나 현재 github 이슈로 할 수 없어 PAT 토큰을 직접 이용해야 한다.

위 이미지를 빌드 후 실행시 환경 변수로 REPO_OWNER, REPO_NAME, GITHUB_PAT 등을 넣어줘야 한다. 이미지는 다음과 같이 테스트할 수 있다.

docker run -it --rm -e REPO_OWNER={REPO_OWNER} -e REPO_NAME={REPO_NAME} -e GITHUB_PAT={GITHUB_PAT} {IMAGE_NAME}

4) StatefulSet 구성

위 3)에서 만든 이미지로 StatefulSet을 구성해보자.

apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: github-runner-statefulset
spec:
  replicas: 5
  selector:
    matchLabels:
      app: github-runner
  serviceName: github-runner-service
  templage:
    metadata:
      labels:
        app: github-runner
    spec:
      containers:
        - name: github-runner
          image: { (3)에서 만든 이미지 }
          envFrom:
            - configMapRef:
              name: repo-config
          env:
            - name: GITHUB_PAT
              valueFrom:
                secretKeyRef:
                  name: github-runner-token
                  key: pat-token

앞에서 만든 configMap과 secret으로 필요한 환경 변수는 전부 구성이 되었다. ConfigMap과 Secret, Service를 먼저 생성한 이후 StatefulSet을 배포하면 레포지토리에 Runner가 잘 등록이 된 것을 확인할 수 있을 것이다.

5. (참고) Lens IDE

쿠버네티스 리소스를 다룰 때 Cli로 관리하면 너무 복잡하고 어렵다. Lens IDE를 사용하면 좀 더 쉽게 다룰 수 있다. 회사에서 사용 시 비용을 지불해야 하지만 개인 목적으로 사용한다면 무료로 쓸 수 있으니 꼭 사용하는 것을 추천한다.