본문 바로가기

DevOps

[Github Action] Self hosted runner에서 Gradle, Docker image cache #2 - EFS 활용

반응형

 

#1 편에서는 아주 간단하게 EBS, PVC를 활용하여 캐싱을 수행하였다.

 

https://nyyang.tistory.com/163

 

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

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

nyyang.tistory.com

1. 개요

#1 편에서는 아주 간단하게 EBS, PVC를 활용하여 캐싱을 수행하였다.

 

하지만 프로젝트가 많아질 경우 PVC가 Available일 경우에만 Mount하여 Runner가 Job을 수행할 수 있다보니 대기 시간이 발생할 수 밖에 없다.

 

PVC Mode를 RWO(ReadWriteOnly)로 설정하는 이유는 여러 SpringBoot 프로젝트가 동일한 Gradle Home에 Cache를 보관하고 이를 활용할 경우 Gradle Lock이 발생하기 때문이다.

 

그리고 EBS를 사용하게 되면 특정 Zone에만 종속되어 사용할 수 밖에 없으며 여러 Pod에서 동시에 Mount하여 사용할 수 없다.

 

이런 단점을 해결해주는게 EFS이다.

 

EFS는 여러 Zone에 분포되어 있는 Pod에서 Network를 활용하여 마운트를 할 수 있다보니 범용성이 있고, User나 Group owner를 세팅할 수 있다보니 #1편에서 다뤘던것처럼 Init Container를 통해 권한 세팅할 필요가 없다는 장점이 존재한다.

 

만약 비용이 걱정된다면 One zone에만 efs를 배포하도록 설정할 수도 있다.

 

하지만 EFS의 단점은 읽기, 쓰기 성능이 다소 좋지 않으며 비용이 비싸다는 점이 있는데 그래도 AWS에서 관리해주다보니 쓸만하지 않을까 생각한다.

 

 

2. 아키텍처

Runner Pod는 1개의 PVC에 volumeMount를 하고, EFS CSI Driver Controller를 활용하여 EFS에 마운트를 수행한다.

 

여기서 핵심은 2가지이다.

 

[1] Gradle User Home

Gradle dependency cache에 대해 Locking 메커니즘때문에 여러 Project가 동시에 Gradle build를 수행할 수 없다.

 

하여 서비스별로 gradle 명령어 옵션 중 -g /mnt/gradle/${{ env.SERVICE }} 을 활용하여 서비스별로 gradle user home directory를 변경해줘야 한다. 만약 하지 않으면 기본값으로 ${HOME}/.gradle 로 설정될 것이다.

 

Ref : https://stackoverflow.com/questions/74339765/how-can-i-make-one-single-gradle-cache-for-multiple-projects

 

 

[2] Docker image cache

Docker image cache는 buildx cache type 중 local 타입을 사용하여 캐싱을 수행한다.

 

이 또한 아래처럼 서비스별로 구분해줄 예정이다.

docker buildx build ~~~ \\
--cache-to type=local,dest=/mnt/docker/${{ env.SERVICE }} \\
--cache-from type=local,src=/mnt/docker/${{ env.SERVICE }} .

 

 

3. 구성

 

3.1. EFS 생성

EFS를 미리 생성해두고 Access Point를 만든다.

 

참고로 비용을 절감하기 위해 C zone에만 구성했다.

 

모드는 Bursting 모드이다.

 

 

추가로 eks에 efs csi driver controller를 구성해둬야 하는데 이 부분은 설명 생략한다.

 

 

 

3.2. StorageClass, PVC, PV 생성

[1] StorageClass

kind: StorageClass
apiVersion: storage.k8s.io/v1
metadata:
  name: efs-sc
provisioner: efs.csi.aws.com
parameters:
  provisioningMode: efs-ap
  fileSystemId: fs-XXXXXXXXXXX

 

 

[2] PV

volumeHandle에 왼쪽 부분은 efs id, 오른쪽 부분은 access point를 기입해준다.

apiVersion: v1
kind: PersistentVolume
metadata:
  name: efs-pv
spec:
  capacity:
    storage: 10Gi
  volumeMode: Filesystem
  accessModes:
    - ReadWriteMany
  persistentVolumeReclaimPolicy: Retain
  storageClassName: efs-sc
  csi:
    driver: efs.csi.aws.com
    volumeHandle: fs-XXXXXXXXXX::fsap-XXXXXXXXX

 

 

[3] PVC

apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: efs-claim
  namespace: self-hosted-runners
spec:
  storageClassName: efs-sc
  accessModes:
    - ReadWriteMany
  resources:
    requests:
      storage: 10Gi

 

 

3.3. Volume Mount

affinity 또한 c zone에만 배포될 수 있도록 설정해주고 /mnt 경로를 마운트해준다.

 

동시에 2개의 Runner Pod를 띄워보면 동시에 EFS에 마운트되는 것을 확인할 수 있을 것이다.

apiVersion: actions.summerwind.dev/v1alpha1
kind: RunnerDeployment
metadata:
  name: runner-deployment
  namespace: self-hosted-runners
spec:
  replicas: 2
  template:
    spec:
      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
              - matchExpressions:
                  - key: topology.kubernetes.io/zone
                    operator: In
                    values:
                      - ap-northeast-2c
      ephemeral: true
      organization: XXXXXXXXXXX
      volumeMounts:
        - name: persistent-runner-storage
          mountPath: /mnt
      volumes:
        - name: persistent-runner-storage
          persistentVolumeClaim:
            claimName: efs-claim
      labels:
        - runner-deployment

 

 

3.4. Workflow 파일 캐시 설정

아래와 같이 설정하면 다음의 구조가 될 것이다.

 

만약 A 서비스가 CI Job을 수행할 경우 아래의 디렉터리가 생성된다.

 

그럼 이제 각 서비스별로 gradle과 docker cache 디렉터리가 생성되는 것이다.

 

/mnt/gradle/{A서비스}/

/mnt/docker/{A서비스}/

 

 

4. 캐시 결과

4.1. 첫 번째 Job 수행 시
: Gradle 빌드 5분 41s

 

EFS를 연결하기 전에는 캐시 되기 전에 2m 가량 소요되었는데 확실히 EFS라서 읽기, 쓰기 성능이 좋지 않다.

 

Docker는 설정 오류가 나서 다음 Job 수행 시 정상 동작될 것이다.

 

 

 

4.2. 두 번째 Job 수행 시
: Gradle 빌드 28s (캐싱)
: Docker 빌드 1m 6s

Gradle cache는 EBS 연결 시 대략 19s 소요되었는데 28s 소요된 것으로 확인된다.

 

Docker는 초기 빌드 시 1m 소요되었는데 1m 6s가량 소요되었다.

 

 

 

4.3. 세 번째 Job 수행 시
: Gradle 빌드 28s (캐싱)
: Docker 빌드 27s (캐싱)

Gradle은 그대로 28s 가량 빌드가 수행되었고,

Docker image는 1m 10s → 27s 가량 줄었다.

 

 

 

 

4.4. 캐시 남아있는 모습

캐시는 EFS의 /mnt 디렉터리 하위에 서비스별로 캐시 데이터들이 남아있게 된다.

 

5. 요약

#1 편에서는 PVC를 이용해 캐시를 수행하였다.

 

다만 EBS 특성상 ReadWriteMany를 못하기 때문에 여러 Project가 동시에 Job 수행 시 PVC가 Available 될 때까지 다른 Job이 기다려야 하는 단점이 존재했다.

 

하지만 docker 컨테이너에서 /var/lib/docker를 직접 마운트하기 때문에 docker image 빌드 속도는 4s밖에 소요되지 않았다.

 

gradle cache 또한 19s 정도로 빠르게 진행되었다.

 

#2 에서는 Project가 수십개 있다는 가정하게 EFS를 이용하여 Build를 수행하였다.

 

장점으로는 여러 프로젝트에서 동시에 Build를 수행할 수 있지만 EFS 특성상 성능이 다소 좋지 않음을 확인할 수 있었다.

 

만약 EFS를 사용할거라면 IA나 Mode 등을 잘 숙지하고 사용해야 비용 폭탄을 맞지 않을 것이므로 유의할것

반응형