본문 바로가기

DevOps

[Github Action] Self hosted runner에서 Gradle, Docker image cache #1 - EBS, PVC로 캐시하기

반응형

Github hosted runner에서는 다양한 actions에서 caching 전략을 제공하기 때문에 단순 actions를 사용하는 것만으로 손쉽게 캐싱을 수행할 수 있다.

 

캐싱은 docker image caching도 아주 쉽게 수행할 수 있다.

 

이는 buildx에서 --cache-from과 --cache-to의 type 중 gha(github action)란 type을 제공하는데, 아주쉽게 구현 가능하다.

 

gradle cache 또한 제공하는 actions를 쓰면 된다.

 

 

하지만 github hosted runner를 쓰면 다양한 단점이 존재한다.

 

1. 개발한 코드가 클라우드 상에 배포된다.

2. runner가 argocd 등의 명령어를 수행하기 위해 argocd가 public 망에 존재해야 한다. 만약 방화벽을 제어하려면 수많은 github 네트워크 대역대를 열어줘야만 한다.

3. 1초 실행되는 job 조차 1분 사용한 것으로 간주된다.

4. 3000분 이상 사용할 경우 과금처리된다.

 

 

하여 직접 github actions runner를 관리하는 방식인 self hosted runner를 고려하게 되었다.

 

 

self hosted runner를 사용하는 방법도 정말 다양한데, 여기에서는 k8s에서 운영하는 방식인 actions runner controller 상에서 캐싱하는 방법을 소개한다.

 

Actions Runner Controller로 Self Hosted Runner를 동작시키면 기본적으로 Job을 수행시킨 뒤에 기존 Runner Pod는 status code가 0으로 변경되면서 중지되고 다시 시작된다.

아마 여러 Workflow가 다른 Repository 환경 간 Workflow Job을 공유하지 않도록 하지 않기 위함인 것으로 보인다.

이로 인해 당연히 Docker Cache나 Gradle Dependency Cache 또한 사라지는데, 이럴 때에는 어떻게 조치해야 할까?

방안은 여러가지가 있겠지만 한가지 방안으로는 EBS로 PV를 생성하고 CI Runner는 그 PV를 Mount 하여 캐시 데이터를 쓰도록 하는 것이다.

물론 CD Runner는 캐시가 필요하지 않기 때문에 상관없다.

 

 

참고로 Runner Pod가 Job이 끝난 뒤에 재시작하지 않도록 설정할 수 있다.
하지만 권장되는 방법은 아니기 때문에 최대한 Volume을 공유하여 사용하는 방법이 좋을 것으로 보인다.

 

1. 아이디어

[1] Docker는 /var/lib/docker 경로에 image나 network 설정, 볼륨 등의 정보를 저장한다.


[2] Gradle은 ${HOMEDIR}/.gradle , ${HOMEDIR}/.m2 에 Gradle에 의해 캐시 디렉터리가 생성된다.

 

 

이 2가지의 아이디어를 가지고 Self Hosted Runner에서도 캐시를 수행할 수 있을 것으로 생각한다.

 

위 내용 별개로 Docker Image를 캐시하거나 Gradle Dependency Cache 할 수 있는 방법은 정말 다양하다.

Docker는 Image Registry에도 캐시를 저장할 수 있고 특정 Local Path에도 캐시를 저장할 수 있고 s3에도 저장할 수 있다고 한다.


하지만 현재 ECR에 Docker Cache를 저장하는건 지원이 안된다.


Gradle 또한 build.gradle 파일 내에 어느 경로에 Build Cache를 저장할지 등을 정의할 수 있다.

 

2. 아키텍쳐

캐시 데이터는 CI를 수행하는 Job에서만 필요하다. ArgoCD에 배포하도록 하는 Job에는 굳이 캐시가 필요없다.

따라서 Runner Lable을 2개로 분리하고 CI Runner에만 PVC를 2개 연결해주면 될 것으로 보임

 

 

3. 설정

아래 내용을 참고하여 설정을 진행하였다.

https://github.com/actions/actions-runner-controller/blob/master/docs/detailed-docs.md#docker-image-layers-caching

 

GitHub - actions/actions-runner-controller: Kubernetes controller for GitHub Actions self-hosted runners

Kubernetes controller for GitHub Actions self-hosted runners - GitHub - actions/actions-runner-controller: Kubernetes controller for GitHub Actions self-hosted runners

github.com

https://github.com/actions/actions-runner-controller/issues/847

 

runner not able to connect to docker · Issue #847 · actions/actions-runner-controller

Describe the bug Time to time docker:dind container throws the following error: evel=warning msg="grpc: addrConn.createTransport failed to connect to {unix:///var/run/docker/containerd/contain...

github.com

 

 

 

3.1. EBS 2개 생성

EBS CSI Driver Controller을 설치해둬야 하고, StorageClass에 의해 dynamic하게 volume을 생성하는건 runner가 job을 수행하고 죽게 될 경우 새로운 volume을 생성하기 때문에 static하게 volume을 지정하는 과정이 필요했다.

 

 

docker-volume : /var/lib/docker 에 마운트시킬 EBS Volume
runner-volume : /home/runner/.gradle 에 마운트시킬 EBS Volume

주의 : Pod의 AZ와 EBS의 AZ는 C Zone으로 동일하게 맞춰주었다.

 

3.2. PV, PVC 생성

 

PV 2개 생성

 

 

3.3. RunnerSet 생성

 

Runner를 구동할 수 있는 기능이 2가지인데, RunnerDeployment와 RunnerSet이다.

 

RunnerSet은 StatefulSet과 유사하다고 생각하면 되고 RunnerDeployment는 Deployment와 유사하다고 보면 된다.

 

만약 RunnerDeployment에서 Volume을 세팅하고 싶다면 설정이 살짝 다르다.

nodeAffinity : c zone에만 배포되도록 설정

 

initContainer : /home/runner/.gradle 경로의 Owner가 기본적으로 Root인데, 이를 수정하기 위한 InitContainer이다.

 

runner에는 runner-volume을 마운트해주었고,

docker에는 docker-volume을 마운트해주었다.

 


runner는 Job을 실질적으로 수행시키는 컨테이너이고

docker는 docker daemon을 제공해주는 컨테이너이다.

 

 

 

4. Workflow 테스트

 

Workflow는 간단하게 아래대로 생성하였다.

 

  ci:
    name: Continuous Integration
    runs-on: mgmt-test
    steps:
      - name: Checkout
        uses: actions/checkout@v2

      - name: Configure AWS credentials
        uses: aws-actions/configure-aws-credentials@v1
        env:
          AWS_REGION: ap-northeast-2
        with:
          role-to-assume: ${{ env.ASSUME_ROLE }}
          aws-region: ${{ env.AWS_REGION }}

      - name: Login to Amazon ECR
        id: login-ecr
        uses: aws-actions/amazon-ecr-login@v1

      - name: Set up JDK 11
        uses: actions/setup-java@v3
        with:
          distribution: 'corretto'
          java-version: '11'

      - name: Build SpringBoot with Gradle
        run: |
          ./gradlew clean build
      
      - name: Build and Push image to ECR
        env:
          ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          ECR_REPOSITORY: "${{ env.SERVICE_NAME }}-${{ env.ENVIRONMENT }}"
          IMAGE_TAG: ${{ github.sha }}
        run: |
          docker build --build-arg ENVIRONMENT=${{ env.ENVIRONMENT }} \
          -t ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }} .
          docker push ${{ env.ECR_REGISTRY }}/${{ env.ECR_REPOSITORY }}:${{ env.IMAGE_TAG }}

 

 

4.1. 첫번째 빌드

Gradle 빌드 : 대략 2m 3s 소요
Docker Image 빌드 : 대략 1m 소요

 

 

4.2. /var/lib/docker 만 마운트했을 때

Gradle 빌드 : 그대로 대략 2m 14s 소요
Docker Image 빌드 : 3s 소요

→ Base Image랑 RUN yum update ~~ 하는 부분을 기 저장된 Docker Image Layer를 받아와서 Cache

 

4.3. /home/runner/.gradle 마운트 추가

Gradle 빌드 : 20s 소요
Docker Image 빌드 : 4s 소요

 

 

5. 장, 단점

장점

별도 기술 사용 없이 편하게 빌드, 도커 캐시만 볼륨 마운트하여 사용 가능

ephemeral 기능을 true로 하여 Job이 끝난 Runner는 중지된 뒤 깨끗한 Runner를 재시작하여 사용 가능

 

 

단점

1개의 Pod가 1개의 Volume에 마운트할 수 있기 때문에 PVC가 Available 될 때까지 기다려야 함

PVC가 특정 zone에 종속되다보니 pod 또한 affinity로 특정 zone에만 배포될 수 있도록 하거나 node를 특정 zone에 제한해야한다.

 

참고

https://github.com/actions/actions-runner-controller/blob/master/docs/detailed-docs.md#docker-image-layers-caching

 

 

 

이런 단점이 존재하지만 서비스가 별로 없을 경우 가장 간단하게 사용할 수 있다.

 

gradle을 캐시하는 방법은 build.gradle에서도 정의할 수 있고 efs를 통해서도 캐시데이터를 저장 및 복원할 수도 있다.

docker는 harbor 같은 registry에서도 가능하고 local type으로도 가능하고 s3에서도 가능하다. 대신 S3는 alpha 기능이라 선호되지는 않는다.

반응형