1. Intro
이전글을 통해서 Self-hosted ARC 환경을 구성해보았다. 그러나 단순히 환경을 구성하고 동작을 테스트하는것과 실제로 사용가능하게 환경 구성을 설정하는 것은 다르다.
GitHub Action 문서에서 또한 기본 구성외에 추가적인 도구 구성은 사용자의 책임으로 정해두고 별다른 가이드라인을 제공하지않는다. Runner의 package는 어떻게 구성해야할까? 관련된 글도 많이 없었고 삽질했던 내용들을 정리해두고자 한다
2. Self-hosted Runner values 배포
이전글에서는 간단히 동작을 위해 –set override로 구성하였지만 실제로 구성하고 사용하기에는 부족하다. 또한 배포 이력을 관리하기 위해서라도 설정값을 별도 values.yaml로 분리한 뒤 관리하는것이 권장된다.
2.1. Actions Runner Controller
controller는 runner 그룹과 github과의 연결고리 역할을 한다. 실질적인 동작을 수행하는 부분은 아니며 따라서 필요시 HA등의 구성을 위해 replicas 나 pod scheduling(affinity,toleration, topologySpreadConstraints 등)을 설정할 수 있다.
# arc-controller-values.yaml
# https://github.com/actions/actions-runner-controller/blob/master/charts/gha-runner-scale-set-controller/values.yaml
# helm upgrade arc-controller oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set-controller -f ./arc-controller-values.yaml --version 0.9.3 -n arc-systems -i
# HA를 위해 3개의, 노드간 균형 배포 및 controlplane 우선 배포 예시
replicaCount: 3
# controlplane의 hostname을 value로 지정
# k get no --show-labels -o wide | grep control-plane | grep hostname
# affinity:
# nodeAffinity:
# preferredDuringSchedulingIgnoredDuringExecution:
# nodeSelectorTerms:
# - matchExpressions:
# - key: kubernetes.io/hostname
# operator: In
# values:
# - nasir-k8s-control-plane
# 일반적으로 control-plane node는 배치 금지를 위한 taints 설정, toleration으로 무시
tolerations:
- key: node-role.kubernetes.io/control-plane
operator: Exists
effect: NoSchedule
# 나머지 worker node에도 균일하게 배포하기 위해 topologySpreadConstraints 구성
topologySpreadConstraints:
- labelSelector:
matchLabels:
app.kubernetes.io/component: controller-manager
maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: DoNotSchedule
결과
controller 균등 배포
- 각 노드에서 1개씩, 총 3개의 controller pod가 균등 생성
- 3개 중 특정 1개의 pod가 leader가 되었음을 확인
ARC 관련 리소스 구독
- autoscalinglistener
- autoscalingrunnerset
- ephemeralrunnerset
- ephemeralrunner
2.2. Runner scale set
실질적으로 workflow 작업을 수행할 runner의 구성은 runner scaleset에서 설정한다.
# token 정보를 별도의 arc-ghp-token 이름의 k8s secret에 저장
kubectl create secret generic arc-ghp-token --namespace=arc-systems --from-literal=github_token='ghp_aaa'
# arc-runner-values.yaml
# https://github.com/actions/actions-runner-controller/blob/master/charts/gha-runner-scale-set/values.yaml
# helm upgrade k8s-runner oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set -f ./arc-runner-values.yaml --version 0.9.3 -n arc-systems -
# runner group을 연결할 github repository/organization 등록
githubConfigUrl: https://github.com/nasir17git/logonme
githubConfigSecret:arc-ghp-token
#github_token: "ghp_aaa" //직접 토큰값을 넣을경우
# 유지할 Runner 수 설정
maxRunners: 3
minRunners: 2
결과
- listener와 runner 들 생성
- github web에서도 생성된 runner들의 상태 정보 확인 가능
3. Self-hosted Runner Image 구성
3.1. Image
Github에서 기본적으로 제공하는 self-hosted runner 이미지는 github와의 연동설정만 되어있는 빈 ubuntu image이다.
github-hosted runner에 설치되어있는 추가적인 도구들은 포함되어있지 않아, 필요한 도구를 별도로 구성하여 사용하여야 한다
물론 workflow 단에서 setup류의 action을 통해 환경을 구성할 수 있으나 매번 실행간 도구설치의 과정이 필요하고, 퍼블릭 접속이 필요하며, 매번 같은 내용의 데이터를 다운로드 받는다는것이 아쉬웠다.
self-host runner를 사용하면 적절히 러너 환경을 커스텀 할 수 있기에, 빌드/배포단에서 필요한 도구를 사전에 러너에 추가할 수 있다
240820 fix: add jq
, git
, unzip
and curl
to default packages installed PR에 의해 일부 패키지는 추가되었음..
3.2. Image layer
dockerfile 빌드 최적화를 위해, 빌드결과물만 재사용하는 multi-stage build를 사용하거나, &&
와 /
같은 연산자를 사용해 이미지 레이어를 줄이는 multi-line build(?)가 권장되곤 한다.
그러나 self-hosted runner의 경우, 어플리케이션 이미지 빌드와는 다른 리눅스 패키지 설치가 목적이기에 때문에 위의 기법들이 유용하지 않았고, 설치한 도구 및 종속성마다 RUN 라인으로 분리하였다.
3.2.1 Multi-Stage Build
# Stage 1: Build the Java application
FROM openjdk:17-jdk-slim as builder
WORKDIR /app
COPY HelloWorld.java .
RUN javac HelloWorld.java
# Stage 2: Create a lightweight runtime image
FROM openjdk:17-jre-slim
WORKDIR /app
COPY --from=builder /app/HelloWorld.class .
CMD ["java", "HelloWorld"]
multi-stage build는 빌드와 구동환경을 분리하여, 빌드 도구와 소스 코드를 최종 이미지에서 제외하여 이미지 크기를 줄이는 기법이다. 중간 빌드 단계의 결과물만 최종 이미지로 복사되어 최종 docker image의 크기를 최적화 할 수 있다.
그러나 self-host runner 구성은 주로 패키지 설치와 환경 설정에 중점을 두고 있어, 빌드 과정에서 생성되는 중간 아티팩트나 컴파일된 바이너리가 없다. 최종 이미지에 모든 설치된 도구와 환경이 모두 필요하기에 multi-stage build의 효과를 보기 어렵다.
3.2.2. Multi-line build(?)
# https://github.com/actions/runner/pkgs/container/actions-runner
FROM ghcr.io/actions/actions-runner:2.318.0
# Set environment variables
ENV LC_ALL=ko_KR.UTF-8
# Install dependencies and clean up in a single RUN command
RUN sudo apt-get update && sudo apt-get install -y --no-install-recommends \
ca-certificates \
curl \
gnupg \
locales \
language-pack-ko \
screen \
mysql-server \
openjdk-21-jdk \
git \
openssh-client \
wget \
unzip \
jq \
build-essential \
procps \
file \
&& install -m 0755 -d /etc/apt/keyrings \
&& sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc \
&& sudo chmod a+r /etc/apt/keyrings/docker.asc \
&& echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo \"$VERSION_CODENAME\") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null \
&& sudo apt-get update \
&& sudo apt-get install -y docker-ce-cli docker-compose-plugin \
&& sudo locale-gen ko_KR.UTF-8 \
&& sudo dpkg-reconfigure locales \
&& sudo usermod -d /var/lib/mysql/ mysql \
&& sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 \
&& sudo chmod +x /usr/local/bin/yq \
&& sudo apt-get clean \
&& sudo rm -rf /var/lib/apt/lists/*
# Set up Linuxbrew
RUN echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> /home/runner/.bashrc \
&& . /home/runner/.bashrc \
&& /home/linuxbrew/.linuxbrew/bin/brew install gcc
공식문서의 best practice 중 RUN 파트에서 소개된 것과 같이, dockerfile 작성시 &&
와 /
와 같은 연산자를 활용해 여러 RUN 명령어를 통합해 레이어를 줄이는것이 권장된다.
그러나 이는 1. 가독성과 2. 유지보수성을 위한것이지 3. 용량 최적화를 위한것이 아니기에 설치한 패키지 도구별 RUN 라인을 사용하였다.
1. 가독성
- 설치하는 패키지가 많을수록 한줄쓰기 보다는 \ 로 구분하여 알파벳 순으로 정렬하는것이 권장됨
- 그러나 너무 많아지면 도구 제거시 의존성 파악이 어려움. procps는 어떤 패키지의 사전 구성일지?
- 또는 하나의 RUN 구문에서 에러 발생시 전체 RUN 구문이 반환되어 특정이 어려움
2. 유지보수성
- apt 명령어 사용에 있어, apt-get update 와 apt-get install이 별도의 라인에 존재한다면 update 라인은 캐싱되어 새로 업데이트 되지않아, install 구문 변경시 제대로 설치안될 가능성이 있어 한줄 사용 권장
- 빌드시 기존 이미지(또는 빌드캐시)를 사용하지 않고 새로 빌드하므로 영향 X
3. 용량 최적화
- 이유는 모르겠으나 원라인으로 작성시 결과 도커 이미지의 크기가 커짐 (?)
3.3. Image 구성 자동화
Runner Image를 Build하고 전달하는 또한 Github Action을 통해 자동화 할 수 있다. 다음과 같은 dockerfile 을 추가하고, workflow 파일을 추가해 시작할 수 있다
runner Image에 필요한 패키지를 구성한뒤 사용자의 Registry로 (아래는 dockerhub)로 전송한다. 그 뒤 필요한 runnerset에 적절하게 이미지 업데이트
Custom Image Create
- dockerfile 추가
# dockerfile
# https://github.com/actions/runner/pkgs/container/actions-runner // runner 버전확인
FROM ghcr.io/actions/actions-runner:__BASE_IMAGE_VERSION__
# Add Docker's official GPG key:
RUN sudo apt-get update && sudo apt-get install -y --no-install-recommends ca-certificates curl \
&& install -m 0755 -d /etc/apt/keyrings \
&& sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc \
&& sudo chmod a+r /etc/apt/keyrings/docker.asc
# Add the repository to Apt sources:
RUN echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null \
&& sudo apt-get update
# docker/compose cli only
RUN sudo apt-get install -y --no-install-recommends docker-ce-cli docker-compose-plugin
# set kr locale
RUN sudo apt-get install -y --no-install-recommends locales language-pack-ko && sudo locale-gen ko_KR.UTF-8 && sudo dpkg-reconfigure locales
ENV LC_ALL ko_KR.UTF-8
# install screen, vim
RUN sudo apt-get install -y --no-install-recommends screen vim
# install mysql
RUN sudo apt-get install -y --no-install-recommends mysql-server \
&& sudo usermod -d /var/lib/mysql/ mysql
# install java21
RUN sudo apt-get install -y --no-install-recommends openjdk-21-jdk
# install deploy-tools
RUN sudo apt update \
&& sudo apt-get install -y --no-install-recommends git openssh-client wget unzip jq less \
&& sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 && sudo chmod +x /usr/local/bin/yq
# cleanup cache
RUN sudo apt-get clean \
&& sudo rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
- image build & push workflow
name: action-runner-build
run-name: self-hosted runner image build
on:
workflow_dispatch:
inputs:
BUILD_MACHINE:
description: '빌드를 진행할 머신을 선택해주세요(택1)'
type: choice
default: k8s-runner
options:
- k8s-runner
- ubuntu-latest
BASE_IMAGE_VERSION:
description: ghcr.io/actions/actions-runner의 버전 입력. 기본값 latest
type: string
default: latest
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: build & push
run: |
sed -i 's/__BASE_IMAGE_VERSION__/${{ inputs.BASE_IMAGE_VERSION }}/g' dockerfile
docker build . -t nasir17/gha-demo:latest
- 이후 빌드된 이미지 정보를 runner-set의 values에 반영하여 helm upgrade
# arc-runner-values.yaml
# https://github.com/actions/actions-runner-controller/blob/master/charts/gha-runner-scale-set/values.yaml
# helm upgrade k8s-runner oci://ghcr.io/actions/actions-runner-controller-charts/gha-runner-scale-set -f ./arc-runner-values.yaml --version 0.9.3 -n arc-systems -
# runner group을 연결할 github repository/organization 등록
githubConfigUrl: https://github.com/nasir17git/logonme
githubConfigSecret:arc-ghp-token
#github_token: "ghp_aaa" //직접 토큰값을 넣을경우
# 유지할 Runner 수 설정
maxRunners: 3
minRunners: 2
#
template:
spec:
containers:
- name: runner
image: nasir17/gha-demo:latest
command: ["/home/runner/run.sh"]
3. Error 발생시 세션 연결하기
Github-Hosted Runner 사용시에는 에러가 발생해도 별도에 대응하는 job 을 만들어두지않았다면 그 원인과 결과/현황을 확인할 수 없었다. 그러나 Self-Hosted Runner의 사용 시 이점은 사용자의 환경에 Pod가 실행되고 접근할 수 있기 때문에 실행중이면 중단하고 그 세션에 접근하여 에러를 확인하고, 어디를 수정할지 파악할 수 있다.
에러 발생시 세션을 생성하고 유지하기위해, 에러를 발생하는 구문과 screen으로 별도의 세션을 생성하는 단계를 추가한다.
이제 파이프라인 단계에서 에러가 발생하거나, 실행시 DEBUG 항목을 체크를 하면 파이프가 정상적으로 작동하여도 작업을 수행하는 runner가 재생성되지 않고 남아있을 것이다.
name: action-runner-build
run-name: self-hosted runner image build
on:
workflow_dispatch:
inputs:
BUILD_MACHINE:
description: '빌드를 진행할 머신을 선택해주세요(택1)'
type: choice
default: k8s-runner
options:
- k8s-runner
- ubuntu-latest
BASE_IMAGE_VERSION:
description: ghcr.io/actions/actions-runner의 버전 입력. 기본값 latest
type: string
default: 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: trigger error
run: |
echo "ERROR!" > ./error.text && pwd
ERROR=true
false
- 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."
어떤 러너가 어떤 workflow를 수행하는지는 ephemeral runner 리소스를 조회하면 알 수 있다. 또는 위의 debug step에서 실행중인 파드명을 확인할 수 있다.
그렇지만 해당 pod에 단순히 shell로 접근한다고해서 진행중인 workflow에 접근할 수는 없다. 해당 작업은 별도 세션에서 돌고있고, 우리는 새로운 세션으로 접근했기 때문이다.
따라서 해당 step에서 debug 라는 이름으로 별도의 session을 만들어 놓았다. 해당 세션으로 재접속하면 현재 멈춘 workflow의 상태로 접근할 수 있다
- screen
- –help : 명령어 옵션보기
- -ls : 현재 만들어진 세션 확인
- -r : 기존 세션에 연결하기
workflow 내부에서의 작업은 _work 디렉토리 안에서 진행된다. 해당 경로안에 repository 명으로 디렉토리가 생기며 workflow가 생성되고, 액션 내부에 checkout을 통해 한번더 해당 레포지토리의 지정 시점(기본 default branch의 최신형상)을 fetch?clone해와 작업이 이루어진다.
환경변수의 단순선언은 연결되지않았다.. export를 했어야했나
현재 세션에 설정되어있는 환경변수들을 살펴보면, 기존외에 실제로 workflow 작동시 호출할 수 있는 Default environment variables 들이 설정되어있는것들 확인할 수 있다!
이를 통해 실제 파이프라인 작업중에, 에러로 중단된 부분이 있다면 실제 러너환경으로 들어와 수동으로 입력하며 어디가 에러였는지, 어떻게 수정하면되는지를 테스트 할 수 있다
Outro
위의 과정들을 통해 Self-host 사용시 베이스 이미지에 필요 도구를 추가하는 방법과, workflow 에러시 직접 runner의 세션에 접근하는 방법을 확인하였다.
그렇지만 지금처럼 base docker image에 직접 도구를 추가하는 경우, base image의 용량이 커지고 따라서 image pull/pod create 시간이 늘어나거나 image 저장소의 저장비용이 증가한다는 문제점이 있다. + 도구 버전관리는 어떻게?
runner 작동환경이 로컬환경이다보니 도구 캐싱을 이미지가 아닌 호스트에 직접하면 어떨까? 그 방법은 다음글에서 확인하겠다