scalable self-hosted runner

uchan·2023년 4월 4일
1

배경

깃헙을 사용할 때 public 레포라면 상관 없으나 private 레포의 경우 깃헙 액션 워크플로 총합 시간이 일정 시간을 초과할 경우 비용을 지출하게 된다. 만약 GHA 로 CI CD 환경을 구성하고 비즈니스 조직에서 private 으로 코드를 관리한다면 많은 비용을 지출하게 될 것이다. 또한 깃헙에서 제공하는 러너는 사양이 제한되어 있다. 그렇다 보니 빌드 시 고사양 스펙을 요구할 경우 깃헙에서 제공하는 러너에서 빌드를 할 수가 없다(최근 larger runner 가 나왔다 한들 가격을 보니 비용이 만만치 않다...). 비용과 고사양 스펙을 만족시키기 위해 self-hosted runner 방식을 택했으며 특히 비용을 최대한 절감하기 위해 scalable 하게 러너를 관리하도록 자동화할 필요가 있다.

사전 지식

  • AWS EC2
  • AWS Lambda
  • AWS SQS
  • GITHUB Webhook
  • Terraform
  • packer

terraform-aws-github-runner

Philips-labs/aws-github-runner 를 사용하여 scalable runners 를 구축하였다. 해당 레포에 사용 방법과 custom options 에 대하여 구체적으로 설명되어 있으니 해당 글에서는 공부한 내용을 복기한다는 것에 중점을 둔다.


ref: https://github.com/philips-labs/terraform-aws-github-runner
위 이미지를 통해 어떻게 scalable 하게 만들었는지 한 눈에 파악할 수 있었다.
1. 깃헙에서 워크플로 실행 시 등록된 엔드포인트로 웹훅을 날린다.
2. 웹훅 요청을 담당하는 람다에서 SQS 에 넣는다.
3. SQS 에서는 FIFO 스케쥴로 scale-up lambda 를 호출한다.
4. scale-up lambda 에 구성된 환경 변수 등으로 세팅한 EC2 설정 값에 맞춰 EC2 instance 를 생성한다.
5. EC2(From pre-built AMI) 에서 설치된 러너는 깃헙에 등록되고, 호출한 워크플로(레이블이 일치)를 처리한다.
6. 시간이 지나 러너가 Idle 상태가 되면 Cloudwatch Event Scheduler 에 의하여 주기적으로 호출되는 scale-down lambda 에 의해 EC2 가 제거된다.

조금 더 들어가서

self-hosted runner 를 구축하면서 겪었던 문제들에 대해 어떻게 해결하였는지 기술하고자 한다.

EC2 가 띄워지는데(start-time) 너무 오래걸린다

AMI 가 무겁거나 OS 로 window 를 사용하면 EC2 를 띄워 러너가 등록되기까지 오래 걸린다. 이를 해결하기 위해 다음과 같은 작업을 하였다.

packer 를 통해 AMI 를 관리하자

  • 꼭 필요한 패키지와 러너만을 설치하고 그외에 필요없는 것들은 AMI 에서 삭제하였다.

스팟 요청을 최적화하자

  • 스팟 인스턴스가 부족하여 EC2 가 띄워지기까지 오래걸릴 수 있다. 이를 해결하기 위해 가용영역을 a, b, c, d 모두 사용하고 스팟플릿에서 사용할 인스턴스 타입을 2개 -> 5개로 늘렸다.

러너 업데이트를 조심하자

  • AWS 를 사용하여 EC2 를 띄울 때 해당 인스턴스가 위치할 VPC 를 지정한다. 이때 해당 VPC 영역과 깃헙 간 인터넷이 원활하지 않아 무한 큐상태에 빠지는 이슈가 있었다(우리의 경우 private 영역에 EC2 를 띄웠는데 특정 액션에서 패키지 설치가 안되는 이슈도 있었다). github runner 는 자동으로 러너 버전을 최신화한다. 만약 버전이 맞지 않는 경우 runner repo 로부터 latest 버전을 다운로드 받고 러너를 실행시키는데 이때 다운로드가 매우 오래 걸려 러너가 안띄워지고 결국 무한 큐상태에 빠졌던 것이다. 이를 해결하기 위해 disable_runner_autoupdate 옵션을 활성화하였다. 그리고 버전이 오래될 경우 해당 러너는 잡을 처리 못하기 때문에 GHA 를 통해 주기적으로 AMI 에 러너 버전을 업데이트하도록 하였다.

  • 알고리즘

  1. check runner version
    현재 우리가 사용하는 러너의 버전(S3 사용)과 runner 에 최신 릴리즈 버전과 일치하는지 확인한다.

  2. update AMI
    버전이 일치하지 않는다면 packer 를 통해 AMI 를 업데이트한다. 이때 AMI 에는 최신 버전의 러너로 설치한다.

  3. update Version
    AMI 가 사용가능상태가 되었다면 현재 버전을 저장하는 파일(S3 에 위치)을 수정한다

  • 소스코드
name: Update Runner AMI

on:
  schedule:
    - cron: '0 18 * * *'

jobs:
  check-runner-version:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.get-runner-version.outputs.version }}
      update: ${{ steps.compare-runner-version.outputs.update }}
    steps:
      - name: Get runner version # 최신 버전을 가지고 옵니다.
        id: get-runner-version
        run: |
          version=$(curl -s https://api.github.com/repos/actions/runner/releases/latest | jq -r '.tag_name')
          echo "version=${version:1}" >> $GITHUB_OUTPUT
      - name: 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: Compare runner version # 현재 버전과 비교합니다.
        id: compare-runner-version
        run: |
          version_exist=$(aws s3 ls s3://bucket/runner-version.txt || true)
          if [ -n "$version_exist" ]; then
            current_version=$(aws s3 cp s3://bucket/runner-version.txt -)
            latest_version=${{ steps.get-runner-version.outputs.version }}
            if [ "$current_version" == "$latest_version" ]; then
              echo "update=false" >> $GITHUB_OUTPUT
            else
              echo "update=true" >> $GITHUB_OUTPUT # 버전이 다르다면 업데이트를 해줘야됩니다
            fi
          else
            echo "update=true" >> $GITHUB_OUTPUT
          fi
      
      - name: Download runner version # 업데이트가 필요하다면 최신 러너를 다운로드합니다
        if: steps.compare-runner-version.outputs.update == 'true'
        run: |
          curl -O -L https://github.com/actions/runner/releases/download/v${{ steps.get-runner-version.outputs.version }}/actions-runner-linux-x64-${{ steps.get-runner-version.outputs.version }}.tar.gz
          aws s3 cp actions-runner-linux-x64-${{ steps.get-runner-version.outputs.version }}.tar.gz s3://bucket/actions-runner-linux-x64-${{ steps.get-runner-version.outputs.version }}.tar.gz
  
  update-ami:
    needs: check-runner-version
    if: needs.check-runner-version.outputs.update == 'true' # 업데이트가 필요할 때만 해당 잡을 실행시킵니다.
    runs-on: ami-builder
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: 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: Update AMI
        uses: ./.github/actions/update-ami # packer 를 사용하여 AMI 를 빌드하는 커스텀 액션입니다.
        

  update-runner-version: # 모든 잡이 끝났다면 현재 버전을 업데이트해줍니다.
    needs: [check-runner-version, update-ami]
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: 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: Update Runner
        uses: ./.github/actions/update-runner #  Terraform 으로 scale-up 람다를 업데이트 하는 커스텀 액션입니다. 이제 scale-up 람다는 새로운 AMI 로 EC2 를 띄웁니다.

      - name: Update AMI version to S3 # 바뀐 버전으로 바꿔줍니다.
        shell: bash
        run: |
          echo "${{ needs.check-runner-version.outputs.version }}" > runner-version.txt
          aws s3 cp runner-version.txt s3://bucket/runner-version.txt

reference

0개의 댓글