Set up Github Action self-hosted runner on Kubernetes (AWS EKS)


Kubernetes (Amazon EKS)에 GitHub Actions self-hosted runner 실행하기

소개

CI/CD(지속적 통합 및 지속적 배포) 세계에서 GitHub Actions는 워크플로 자동화를 위한 강력한 도구로 등장했습니다. GitHub Actions의 주요 기능 중 하나는 빌드 및 배포 환경에 대한 더 많은 제어 및 사용자 지정을 제공하는 자체 호스팅 실행기를 사용하는 기능입니다. 하지만 Kubernetes 환경으로의 배포는 직접적으로 지원을 하지 않습니다. 커스텀 도커 이미지를 이용해서 빌드를 하려면 self-hosted runner를 띄워서 사용해야 합니다. GitOps 도구 중 하나인 ArgoCD를 활용하는 방법도 있습니다. 이 자습서에서는 Amazon EKS(Elastic Kubernetes Service)에서 self-hosted runner를 실행하는 방법을 안내합니다. 이 설정을 통해 서버 개발자와 DevOps 전문가는 Kubernetes의 확장성과 유연성을 통해 CI/CD 파이프라인을 향상할 수 있습니다.

ArgoCD vs Self-hosted runner

Argo CD는 Kubernetes 클러스터에서 애플리케이션을 더 쉽게 배포, 관리 및 유지 관리할 수 있도록 설계된 오픈 소스 선언형 GitOps 지속적 전달 도구입니다. GitOps의 원칙을 염두에 두고 구축되었으며, 애플리케이션과 해당 구성 요소의 원하는 상태가 버전 제어 Git 리포지토리에 저장됩니다. Argo CD는 Git 리포지토리를 정보 소스로 사용하여 애플리케이션을 관리하는 방법을 제공함으로써 개발과 운영 간의 격차를 해소하는 데 도움이 됩니다.

그런데 왜 ArgoCD를 도입하지 않고 self-hosted runner를 직접 구축했을까요? 그 이유는 Helm의 기능을 fully 활용하고 싶어서였습니다.

ArgoCD도 Helm Chart를 통해서 배포가 가능합니다. 하지만 helm install or update 방식이 아닌 helm template | kubectl apply -f . 의 방식입니다. 이러면 helm의 rollback 기능을 활용할 수 없습니다. 실제로 helm release로 배포된 것이 아니라 manifest를 그냥 배포한 것이기 때문입니다. ArgoCD도 자체적으로 history 관리 및 rollback을 지원합니다. ArgoCD는 GitOps 방식이므로 rollback 단위역시 git commit 단위입니다. ArgoCD의 rollback방식보다는 helm의 rollback 방식이 긴급한 상황에 대처하기 좀 더 유연하다고 판단했습니다. 별도의 툴없이 terminal에서 쉽게 가능하기 때문입니다. Lens 같은 GUI 툴에서도 쉽게 가능합니다. helm의 경우 application 단위로 release를 나눠놓는데, 이 경우 여러 application 중 일부만의 rollback도 가능합니다. ArgoCD의 경우에도 각각의 application을 별도로 관리가 가능하지만, 이것보다는 전체 서비스 단위로 관리를 하는 것에 좀 더 적절한 도구라고 판단했습니다. (이건 지극히 개인적인 의견입니다.)

아래에 소개하고 있는 모든 코드는 Github에서 확인이 가능합니다.

https://github.com/DevStarSJ/github-action-runner-docker-image

Build GithubAction self-hosted runner Docker Image

ARC (actions-runner-controller)를 이용하면 쉽게 runner를 띄울수 있습니다. 직접 도커 이미지를 구축한 이유는 각 회사마다 사용하는 툴 및 그 버전이 다를 수 있으므로 거기에 최적화된 이미지로 구축하고 싶었습니다.

Dockerfile

FROM --platform=linux/amd64 ubuntu:latest

ARG AWS_ACCESS_KEY_ID=""
ARG AWS_SECRET_ACCESS_KEY=""

# https://github.com/actions/runner/releases
ARG RUNNER_VERSION="2.308.0"

# 아래 값들은 docker runt 실행시 주입해줘야 함
ARG AUTH_PAT="your-github-personal-access-token"
ARG GITHUB_ORGANIZATION="your-github-organization-name"

# Github Action runner는 admin 권한으로는 실행이 안됨
RUN useradd -m docker
RUN apt-get -y update && apt-get -y install curl git openssl build-essential zip unzip sudo
RUN apt-get install -y --no-install-recommends libssl-dev libcurl4-openssl-dev

# kubectl
RUN curl -LO https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl 
RUN chmod a+x kubectl
RUN mv kubectl /usr/local/bin/kubectl

# helm
RUN curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
RUN chmod 700 get_helm.sh
RUN ./get_helm.sh
RUN rm get_helm.sh

RUN mkdir -p ~/.helm/plugins
RUN helm plugin install https://github.com/hypnoglow/helm-s3.git

# aws-cli
RUN curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
RUN unzip awscliv2.zip
RUN ./aws/install 
RUN rm awscliv2.zip

# aws credentials to update EKS kubeconfig
RUN mkdir /root/.aws
RUN echo "[default]\naws_access_key_id = $AWS_ACCESS_KEY_ID\naws_secret_access_key = $AWS_SECRET_ACCESS_KEY" > /root/.aws/credentials

# eksctl login
RUN aws eks --region ap-northeast-2 update-kubeconfig --name deepsearch-blue

# Github Actions Runner
RUN mkdir actions-runner && cd actions-runner && \
    curl -o actions-runner-linux-x64-$RUNNER_VERSION.tar.gz -L https://github.com/actions/runner/releases/download/v$RUNNER_VERSION/actions-runner-linux-x64-$RUNNER_VERSION.tar.gz && \
    tar xzf ./actions-runner-linux-x64-$RUNNER_VERSION.tar.gz && \
    chown -R docker:docker /actions-runner
RUN sudo /actions-runner/bin/installdependencies.sh

# runner can't run as root
USER docker

# GithubAction self-hosted runner 에서 등록시 사용가능한 token을 주지만, 1시간 정보밖에 유효하지 않아서, 어쩔수 없이 PAT를 사용함. 추후 더 안전한 방법으로 변경해야함.
ENTRYPOINT /actions-runner/config.sh --url $GITHUB_ORGANIZATION --pat $AUTH_PAT --name eks_runner --unattended --replace && \
            bash /actions-runner/run.sh

위 도커파일에서 하는 일에 대해서 설명드리겠습니다.

  1. linux/amd64 기반에서 동작하는 ubuntu OS를 사용합니다.
  2. action runner를 build하는데 필요한 라이브러리들을 설치합니다.
  3. 최신버전의 kubectl을 설치합니다.
  4. helm을 설치합니다.
  5. aws cli를 설치합니다.
  6. EKS에 접속하기 위하여 AWS credentials를 주입합니다.
  7. EKS kebeconfig를 적용합니다.
  8. GithubAction Runner를 다운로드 받아서 빌드합니다.
  9. runner는 root권한으로 실행이 되지 않으므로 docker라는 user로 owner를 변경합니다.
  10. runner config 설정 후 실행합니다.

게시글 작성당시 다른 블로그에 있는 도커 이미지들을 여러개 시도해 보았으나 모두 동작하지 않았습니다. 제대로 동작하는 명령어는 Github Setting에서 보여주는 명령어였습니다.

https://github.com/{$YOUR-ORGANIZATION}/deepsearch-hq/settings/actions/github-hosted-runners/new

메뉴상으로는 Github Organiation 페이지에서 Settings -> Actions -> Runners -> New runner -> new self-hosted runner -> Linux 로 접근하면 아래 명령어들을 볼 수 있습니다.

./config.sh --url https://github.com/deepsearch-hq --token REGISTRATION_TOKEN

그 명령어에서는 PAT(personal access token)을 활용하지 않고 Registration Token을 사용하고 있습니다. 이 토큰은 유효기간이 1시간 정도밖에 되지 않아서, 영구적으로 사용가능한 PAT를 활용하는 식으로 수정했습니다. 동적으로 token을 받아서 수행이 가능한 방법이 나온다면 그 방법으로 수정하는 것이 좀 더 안전한 방법일듯 합니다.

도커파일 빌드 및 ECR에 push 하겠습니다. (ECR이 아닌 다른 container repository에 할 경우 각자 방법대로 해주시면 됩니다.)

docker build -t github-runner . --platform linux/amd64 --build-arg AWS_ACCESS_KEY_ID=YOUR_KEY --build-arg AWS_SECRET_ACCESS_KEY=YOUR_SECRET

#  ECR LOGIN
aws ecr get-login-password --region ap-northeast-2 | docker login --username AWS --password-stdin $ACCOUNT.dkr.ecr.ap-northeast-2.amazonaws.com

docker tag github-runner:latest $ACCOUNT.dkr.ecr.ap-northeast-2.amazonaws.com/github-runner:latest
docker push $ACCOUNT.dkr.ecr.ap-northeast-2.amazonaws.com/github-runner:latest

Deploy on Kubernetes (AWS EKS)

manifest (runner.yaml)

apiVersion: v1
kind: Namespace
metadata:
  name: github
  labels:
    app.kubernetes.io/name: github
    app.kubernetes.io/instance: github
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name:  github-runner
  namespace: github
  labels:
    app.kubernetes.io/name:  github-runner
  replicas: 1
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name:  github-runner
  template:
    metadata:
      labels:
        app.kubernetes.io/name:  github-runner
    spec:
      containers:
        - name:  github-runner
          image: \{\{ .Values.image.url \}\}:\{\{ .Values.images.tag \}\}
          imagePullPolicy: Always
          ports:
            - name: http
              containerPort: 3000
              protocol: TCP
          resources:
            requests:
              cpu: 500m
              memory: 1024Mi
            limits:
              cpu: 1000m
              memory: 2048Mi
          env:
            - name: AUTH_PAT
              value: \{\{ .Values.github.pat \}\}
            - name: GITHUB_ORGANIZATION
              value: \{\{ .Values.github.organization \}\}

github이라는 namespace를 생성하여 1대의 runner를 deployment로 배포하였습니다. Stateful Set으로 배포하는 것이 더 적절할 수도 있겠으나, state를 가지지도 않고, 그럴 필요도 없어서 deployment로 배포하엿습니다.

GithubAction Workflow Example

Github Action Workflow에 아래와 같은 방법으로 self-hosted-runner를 활용할 수 있게 됩니다. 가장 심플한 배포의 예제입니다.

workflow.example.yaml

name: $NAME

on:
  push:
    branches:
      - release

jobs:
  docker-build:
    runs-on: ubuntu-latest
    timeout-minutes: 24
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: $\{\{ secrets.AWS_ACCESS_KEY_ID \}\}
          aws-secret-access-key: $\{\{ secrets.AWS_SECRET_ACCESS_KEY \}\}
          aws-region: ap-northeast-2
      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1
      - name: Build, tag, and push image to Amazon ECR
        id: build-image
        env:
          ECR_REGISTRY: $\{\{ secrets.AWS_ACCOUNT \}\}.dkr.ecr.ap-northeast-2.amazonaws.com
          ECR_REPOSITORY: $NAME
          IMAGE_TAG: $\{\{ github.sha \}\}
        run: |
          # Build a docker container and  push it to ECR.
          docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:latest . 
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:latest
          docker tag $ECR_REGISTRY/$ECR_REPOSITORY:latest $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
          docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG
    outputs:
      tag: $\{\{ github.sha \}\}
      result: "success"
    
  eks-deploy:
    runs-on: self-hosted
    needs:
      - docker-build
    if: needs.docker-build.outputs.result == 'success'
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        with:
          aws-access-key-id: $\{\{ secrets.AWS_ACCESS_KEY_ID \}\}
          aws-secret-access-key: $\{\{ secrets.AWS_SECRET_ACCESS_KEY \}\}
          aws-region: ap-northeast-2
      - name: EKS Login & helm deploy
        run: |
          aws eks --region ap-northeast-2 update-kubeconfig --name $CLUSTER_NAME
      - name: helm upgrade
        run: |
          cd ./helm
          helm upgrade -i $RELEASE . -n $NAMESPACE --set image.tag=$\{\{ github.sha \}\}
      - uses: skolobov/gh-action-slack@v1
        env:
          SLACK_WEBHOOK_URL: $\{\{ secrets.SLACK_WEBHOOK_URL \}\}
        with:
          status: $\{\{ job.status \}\}
          steps: $\{\{ toJson(steps) \}\}
          channel: '#SLACK_CHANNEL'

간단하게 설명해드리겠습니다.

  • release 브랜치에 push하면 동작합니다.
    1. ubuntu-latest라는 Github에서 제공하는 runner에서 실행합니다.
    • 코드 checkout
    • AWS credential 등록
    • ECR login
    • Dockerfile 빌드 및 ECR에 push
  1. self-histed runner에서 실행합니다.
    • 코드 checkout
    • AWS credential 등록
    • helm update 명령어도 코드상 /helm 안에 정의된 helm chart 배포
    • slack으로 내역 발송

뭔가 좀 이상한걸 못느꼈나요? 도커 이미지 속에 이미 AWS credential이 있는데, workflow에서도 동일한 과정을 실행하고 있습니다. 둘 중 하나를 제거해도 됩니다. 각자 회사 상황에 맞게 둘 중 한 곳만 남겨두고 다른 곳을 제거하시면 됩니다.

(참고: 이 블로그 게시물은 단순화된 가이드이며 특정 도구 버전 및 모범 사례에 대한 공식 문서로 보완되어야 합니다.)

Tags

#CI/CD #Github-Action #self-hosted-runner #EKS #Kubernetes #Workflow #Helm

이 글이 도움이 되셨다면 공감 및 광고 클릭을 부탁드립니다 :)