TL;DR
- Self-Hosted runner를 사용하더라도 worflow내 cache는 GitHub 의존적이다
- Cache할 내용을 Runner Image 또는 Runner Node에 저장하여 재사용할 수 있다.
- Runner Image는 모든 구성이 이미지에 포함되어 환경독립적이나 크기관리의 문제가,
Runner Node는 경량 이미지를 갖되 노드간 정보 상이, 노드 관리필요의 문제가 있다
1. Intro
- CI/CD 파이프라인을 태우다보면 환경설정을 위해 소요되는 시간이 아깝다
- 동작 검증을 위해 매번 초기화는 필요함
- 실제 테스트나 빌드를 위해 소요되는 시간은 인정
- 근데 필요한 도구구성/종속성을 셋팅하는 시간은 노인정
- 이를 줄이기 위해 기존에는 GitHub의 스토리지공간에 캐싱하는 방법이 있었음
- 근데 필요 종속성이 커지면 결국 내려받는 시간도 늘음
- 매번 같은 데이터 다운로드 받는것도 네트워크 공해아닐까
- 셀프호스트환경이면 러너에 대한 제어권을 가질 수 있음
- 필요한 도구를 처음부터 이미지에 추가해두면 어떨까?
- 러너자체는 미리 받아두고 구동시켜두면 되지만
- 이미지가 무겁고 보관비용이 커져서 부담스러워요
- 필요한 일부 환경은 로컬 물?리 볼륨에 저장해두고 러너에 마운트해서 쓰는건?
- k8s runner, pv/pvc!
- 필요한 도구를 처음부터 이미지에 추가해두면 어떨까?
2. 환경구성
기존의 k8s-runner와 동일한 환경구성에서 시작
기본적인 Runner로 구동시, 필요한 언어 등 환경을 가져오고 구동하는데 일정 시간이 소요된다. 목적은 이 시간을 줄이는 것이다
.github/workflows/setup-time-calcul.yaml
- setup node, java, python
name: setup-time-calcul
on:
workflow_dispatch:
inputs:
BUILD_MACHINE:
description: '빌드를 진행할 머신을 선택해주세요(택1)'
type: choice
default: k8s-runner
options:
- k8s-runner
- ubuntu-latest
DEBUG:
description: 'debug 세션 활성화'
type: boolean
required: false
jobs:
build_runner_image:
runs-on: ${{ inputs.BUILD_MACHINE }}
steps:
- name: checkout src
uses: actions/checkout@v4
- name: Login to DockerHub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Setup python 3.13
uses: actions/setup-python@v4
with:
python-version: '3.13'
- name: Setup Node 18
uses: actions/setup-node@v4
with:
node-version: 18.x
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '11'
- name: make debug-session
if: ${{ failure() || inputs.DEBUG == 'true' }}
run: |
screen -dmS debug bash
echo "Debug session started. To end normally, create /tmp/end_debug file."
echo "Runner name: ${{ runner.name }}"
echo "To connect: screen -r debug"
echo "To end : touch /tmp/end_debug"
while [ ! -f /tmp/end_debug ]; do sleep 10; done
echo "Debug session ended normally."
결과
- 기존에는 사용도구마다 일정의 소요시간 발생
- 사용환경에 따라 금방 설치되기도하고 다소 시간이 걸리기도함
- 또한 반복적인 환경구성의 경우, dockerhub pull limit에 걸리기도함(mysql, mongodb, etc..)

- Runner Shell 내부로 접근시 캐싱된 도구가 actions-tool-cache 디렉토리에 저장되어있음을 확인 가능

3. Tool caching to Runner Image
Building GitHub Actions Runner Images With A Tool Cache
Github Action Runner에서 빌드환경은 actions/setup-*
류의 marketplace action을 사용하여 구성되곤한다. 해당 액션 사용시 필요 도구는 ACTIONS_TOOL_CACHE
환경변수에 해당하는 디렉토리를 찾고, 해당 디렉토리에 환경이 구성되어있는지 살펴본다. 만약 대상이 비어있으면, 도구를 다운로드 한뒤 구성하고 필요한 환경변수를 업데이트하여 제공한다.
echo $ACTIONS_TOOL_CACHE
> /home/runner/_work/_tools
캐싱이 없는 일반적인 환경에서는 매 파이프 작동시마다 클린한 환경이 제공되기 때문에 매번 다운로드하여 환경을 구성한다. 10번 작동하면 10회, 1000번 작동하면 1000회의 다운로드 발생..
환경구성의 변경이 빈번하다면 그렇게 구성하는것이 옳을것이다. 그러나 setup류의 설정이 한번 구성된 이후 변경할일이 있을까? 프로젝트 시작 시 결정된 버전/환경구성이 변하지 않는다면 이는 낭비일 것이다.
해당 도구를 그럼 어느 Layer에 보존해둘것이냐는 고민 포인트, 처음 시도는 docker image 내부에 포함해보았다
Build Cache w/Dockerfile
환경 구성에 사용된 tool을 캐싱하여 GItHub 아티팩트에 저장하고, Runner 이미지 빌드시 해당 아티팩트를 가져와 재사용하는 구성
- Dockerfile
FROM ghcr.io/actions/actions-runner:latest
ENV ACTIONS_TOOL_CACHE=/home/runner/actions-tool-cache
COPY --link --chown=1001:123 tools $ACTIONS_TOOL_CACHE
- Building a tool cache
on:
# Your triggers here
jobs:
create-tool-cache:
runs-on: ubuntu-latest
steps:
## Remove any existing cached content
- name: Clear any existing tool cache
run: |
mv "${{ runner.tool_cache }}" "${{ runner.tool_cache }}.old"
mkdir -p "${{ runner.tool_cache }}"
## Run the setup tasks to download and cache the required tools
- name: Setup Node 16
uses: actions/setup-node@v4
with:
node-version: 16.x
- name: Setup Node 18
uses: actions/setup-node@v4
with:
node-version: 18.x
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '21'
## Compress the tool cache folder for faster upload
- name: Archive tool cache
working-directory: ${{ runner.tool_cache }}
run: |
tar -czf tool_cache.tar.gz *
## Upload the archive as an artifact
- name: Upload tool cache artifact
uses: actions/upload-artifact@v4
with:
name: tools
retention-days: 1
path: ${{runner.tool_cache}}/tool_cache.tar.gz
- Build the image
build-with-tool-cache:
runs-on: ubuntu-latest
## We need the tools archive to have been created
needs: create-tool-cache
env:
# Setup some variables for naming the image automatically
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
steps:
## Checkout the repo to get the Dockerfile
- name: Checkout repository
uses: actions/checkout@v4
## Download the tools artifact created in the last job
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: tools
path: ${{github.workspace}}/tools
## Expand the tools into the expected folder
- name: Unpack tools
run: |
tar -xzf ${{github.workspace}}/tools/tool_cache.tar.gz -C ${{github.workspace}}/tools/
rm ${{github.workspace}}/tools/tool_cache.tar.gz
## Build the image
## Set up BuildKit Docker container builder
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
## Automatically create metadata for the image
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
## Build the image
- name: Build and push Docker image
id: build
uses: docker/build-push-action@v5
with:
context: .
push: false
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
결과
- 이미 Runner Docker Image 내부에 지정 버전의 도구가 설정되어 체크 후 패스

캐싱을 수행해서 반복적인 환경구성에 소요되는 시간을 줄인다 외에..
- 장점
- 독립적 환경 구성이라는 컨테이너의 장점 유지 – 노드 종속성탈피
- 단점
- 비대해지는 Runner 컨테이너 사이즈
- 하나의 프로젝트의 환경 구성은 동일하더도, Runner가 여러 환경에 대응해야한다면?
- A repo는 Java 8, B repo는 Java 17, C repo는 Python 3.10, …
- 포함작업은 간단하나 결과 Runner Image 사이즈가 갈수록 비대해짐
4. Tool caching to Local Disk
또는 러너가 위치한 노드의 디스크를 사용하여, 로컬환경에 캐시를 남길 수 있다
로컬 영구저장소에 저장된 데이터는 러너의 생애주기와 별도의 생애주기를 가지며, CI/CD 작업 후 필요한 데이터를 갖고 분리되어, 재생성된 러너pod에 재부착되어 작동한다
이때 고려할 수 있는 local storage의 수준으로는 block storage(EBS)/network storage(EFS)가 있다.
- EBS
- 개별 노드별로 부착된 스토리지
- 상대적으로 속도 높고(3,000IOPS,125MB/s) 비용 저렴 (gp3, 월 0.1$/GB)
- 스토리지간 공유가 불가능, 작동환경의 캐시 유지의 어려움
- EFS
- 네트워크를 통해 공유되는 스토리지
- 상대적으로 낮은 속도, 높은 비용(OneZoneStandard, 월 0.176$/GB)
- 스토리지간 공유 가능, 러너의 작동간 동일한 환경 경험 가능
이중에서 EBS 유형을 택해서 예시를 진행하겠다.
- 작업환경의 동일성 보장에 대한 의문 > 그럴 필요가?
- 상대적으로 저렴한 가격
DinD(Docker in Docker) 환경의 Runner 에서, runner pod 내부에서 docker는 pod 내 컨테이너로, runner 컨테이너와 환경을 공유하며 docker socket 마운트를 통해 작동한다.
따라서 파이프 작동시 코드 실행에 필요한 환경(Programming Language, Database, etc..)는 Runner 컨테이너쪽 볼륨에 남겨두어야하고, docker 실행에 연관된 환경 (Docker Image, etc..)는 DinD 컨테이너쪽 볼륨에 남겨두어야한다
그러나 DinD container의 volumemount의 내용은 helm function으로 인해 고정되어있다.
설치 이후 autosclalingrunnerset에서 수정
pv-runner.yaml
- hostmachine과 /data 디렉토리를 공유하는 pv-runner 러너그룹 생성
# helm upgrade pv-runner oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set -f ./pv-runner.yaml --version 0.9.3 -n arc-systems -i
# runner group을 연결할 github repository/organization 등록
githubConfigUrl: https://github.com/nasir17git/logonme
githubConfigSecret: arc-ghp-token
#github_token: "ghp_aaa" //직접 토큰값을 넣을경우
# 유지할 Runner 수 설정
maxRunners: 3
minRunners: 3
controllerServiceAccount:
namespace: arc-system
name: arc-controller-gha-rs-controller
containerMode:
type: "dind"
template:
spec:
containers:
- name: runner
image: nasir17/gha-demo:latest
command: ["/home/runner/run.sh"]
# hostmachine의 directory
volumes:
- name: runner-pv
hostPath:
type: DirectoryOrCreate
path: /data/runner
- name: docker-pv
hostPath:
type: DirectoryOrCreate
path: /data/docker
topologySpreadConstraints:
- labelSelector:
matchLabels:
actions.github.com/scale-set-name: pv-runner
maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
- 생성될 볼륨을 부착하는 volumeMount는 values에서 제어불가
- 수동으로 pv-runner의 autoscalingrunnerset 에서 수정
...
image: nasir17/gha-demo:latest
name: runner
volumeMounts:
- mountPath: /home/runner/_work
name: work
- mountPath: /var/run
name: dind-sock
- mountPath: /home/runner/actions-tool-cache
name: runner-pv
...
image: docker:dind
name: dind
securityContext:
privileged: true
volumeMounts:
- mountPath: /home/runner/_work
name: work
- mountPath: /var/run
name: dind-sock
- mountPath: /home/runner/externals
name: dind-externals
- name: docker-pv
mountPath: /var/lib/docker
...
결과
- runner-pv > 호스트의 data파일만 남아있음
- 사전에 runner의 worker 위치 파악후 docker exec -it nasir-k8s-worker2 touch /data/runner/test.txt
- docker image에 저장된 tool 이 아닌 node의 /data/runner 와 연결
- docker image들 또한 저장되어있지 않음

- 테스트를 위해 러너 내부에서 docker image 저장 후, runner 환경구성(setup-time-calcul) 워크플로우 실행
- docker pull nginx
- 이후 확인
- 이전 작동 이후, 같은 노드에 재생성된 runner의 경우 이전에 구성했던 docker image와 tool이 해당 디렉토리에 남아있음
- 장점
- Runner의 이미지는 구동에 필요한 설정을 갖고있어 경량화
- 단점
- 캐싱의 필요 수준에 대한 적당한 고려
- 작업 branch간 차이가 크다면 캐싱의 의미가
- 로컬 디스크의 디스크 로테이션? 잔여 디스크 용량 비우기 등 관리를 어떻게 할지
- 노드의 볼륨을 직접마운트 하는 옵션을 왜 디폴트로 제공하지않을까.. 보안이슈가 생길수있나?
- 캐싱의 필요 수준에 대한 적당한 고려