GitHub Actions 도입기(1): ARC 구축 및 Container Jobs 지원

CircleCI에서 Github Actions로 전환하기

Jetty • Jaehong Jung, DevOps Engineer

  • 테크 인사이트

Intro

안녕하세요. 채널톡 DevOps 엔지니어 재티입니다.

이 글에서는 채널톡이 기존에 사용하던 CI/CD 플랫폼인 CircleCI에서 GitHub Actions(이하 GHA) 로 전환한 과정과 DevOps 팀에서 운영하는 ARC(Actions Runner Controller) 클러스터에 대한 이야기를 하려고 합니다.

CircleCI에서 GHA로, 왜 옮기게 되었을까?

기존에 채널톡은 CircleCI를 CI/CD 파이프라인의 핵심 도구로 활용하고 있었습니다. 하지만 작년 2분기부터 우리는 GHA로 옮기기 위한 PoC(Proof of Concept)를 진행했습니다.

GHA 도입을 검토하게 된 주요 계기

  1. GitHub Enterprise Plan 도입을 고민하면서

    • 당시 채널톡의 코드 저장소는 GitHub에서 관리하고 있었는데, GitHub Enterprise Plan을 고려하는 과정에서 CI/CD 또한 GitHub 생태계 내에서 운영할 수 있다면 어떨까? 하는 의문이 들었습니다.

    • GHA는 GitHub과의 호환성이 뛰어나기 때문에, 마치 GitHub의 확장 기능처럼 사용할 수 있는 장점이 있었습니다.

  2. CircleCI의 잦은 장애

    • CircleCI는 강력한 기능을 제공하지만, 간헐적으로 서비스 장애가 발생하곤 했습니다.

    • 문제는, 이 장애가 발생하면 전사의 CI/CD 파이프라인이 올스톱된다는 점이었습니다.

    • 물론 GHA 역시 장애가 없는 것은 아니지만, GitHub 자체의 문제라면 단순 CI/CD 문제에 국한되지 않을 가능성이 크고, 대응 우선순위가 더 높아질 것이라고 판단했습니다.

GHA 도입을 통해 기대한 점

CI/CD 플랫폼을 변경한다는 것은 단순한 툴 체인지 이상의 의미를 가지고 있습니다. 우리는 이번 전환을 통해 다음과 같은 점을 기대했습니다.

  1. 개발자 경험의 개선

    • GHA는 문서화도 잘 되어 있고, 전 세계적으로 많은 개발자들이 사용하는 만큼 제품팀 개발자들이 CI/CD 스크립트를 작성할 때 허들이 낮아질 것이라 기대했습니다.

    • 또한 각종 Marketplace의 Actions(GitHub Actions에서 제공하는 플러그인 같은 개념) 덕분에 기능을 직접 구현할 필요 없이 빠르게 활용할 수도 있습니다.

  2. 비용 절감 및 속도 향상

    • CI/CD 실행 비용도 중요한 요소였습니다.

    • Self-hosted runner를 운영하면 클라우드 환경에서 CI/CD를 실행하는 비용을 더욱 절감할 수 있습니다.

    • 우리의 AWS 인프라 및 쿠버네티스(Kubernetes) 환경 내에서 내부 네트워크(In-cluster 네트워크)를 활용한 이미지 저장소 및 캐시 접근이 가능해지면서, 네트워크 지연(latency)을 줄이고 캐시 및 빌드 속도를 최적화할 수 있을 것이라 기대했습니다.

  3. 안정성과 보안 강화

    • 기존 SaaS 기반 CI/CD 플랫폼에서는 장애가 발생하면 우리가 직접 대응할 수 있는 방법이 많지 않았습니다. 하지만 self-hosted runner를 직접 운영하면 CI/CD 인프라의 운영 주도권을 가질 수 있습니다.

    • 장애 발생 시 문제 해결 속도를 높일 수 있고, 필요한 경우 인프라를 즉각 확장하거나 조정할 수 있었습니다.

    • 보안적인 측면에서도 자체적인 Runner 인프라를 운영하면서 내부 보안 정책을 보다 구체적으로 적용할 수 있었습니다.

GHA 도입, 어떻게 진행했을까?

우리는 GHA를 도입하기로 하면서 단순히 기존의 CircleCI 스크립트를 GHA로 변환하는 것이 아니라, Self-hosted runner 클러스터를 운영하는 방식으로 CI/CD 인프라를 개선하고자 했습니다. 이 과정에서 마주했던 다양한 기술적인 고민들, 그리고 우리가 해결했던 문제들에 대해 다음 글에서 자세히 다루고자 합니다.

1부 – 환경 세팅: ARC 구축 및 Container Jobs 지원

1부에서는 self-hosted runner를 Kubernetes 클러스터 위에서 운영하는 환경을 세팅하는 과정에 대해 설명합니다. 우리가 결정한 GHA + Kubernetes 조합 이 어떻게 구축되었는지 단계별로 살펴보겠습니다.

구성 내용

ARC를 사용하여 self-hosted runner 클러스터를 운영

Kubernetes 위에서 실행되는 Runner의 동작 방식과 스케일링 로직

Container Jobs를 실행하기 위한 Docker-in-Docker(DinD), Kaniko 등의 고려 사항

2부 – 실제 운영 및 트러블슈팅

GHA를 인프라에 적용하면서 가장 중요한 부분은 안정적인 운영 이었습니다. 이를 위해 self-hosted runner가 지속적으로 운영되면서 발생한 이슈와 해결 과정을 기록했습니다.

주요 트러블슈팅 항목

Runner가 예고 없이 중단되는 "Shutdown signal received..." 문제 해결

적절한 nodepool EC2 인스턴스 타입 선택

docker pull rate limit 해결하기 위한 mirror registry 도입

캐시 효율을 높이기 위한 @actions/cache를 대체한 AWS S3 actions

1부에서는 CI/CD 인프라를 구축하고 최적화하는 과정,

2부에서는 운영 중 마주친 문제와 우리가 이를 어떻게 해결했는지에 대해 집중적으로 다룰 예정입니다.

1부 시작합니다!


1. ARC에 대해서

ARC란 무엇인가?

Actions Runner Controller(ARC)는 GitHub Actions 팀에서 공식적으로 제공하는 오픈소스 Kubernetes Operator입니다. ARC는 Kubernetes 환경에서 self-hosted runner를 자동으로 관리하고, 확장성을 갖춘 방식으로 운영할 수 있도록 지원합니다.

"Actions Runner Controller (ARC) is a Kubernetes operator that orchestrates and scales self-hosted runners for GitHub Actions."

현재 채널팀은 Kubernetes 기반으로 다양한 서비스를 운영하고 있으며, 수많은 Kubernetes 오퍼레이터(Operators)를 통해 인프라를 관리하고 있습니다. 이러한 환경 위에서 우리는 GitHub Actions의 self-hosted runner를 scalable하게 운영할 수 있는 방법을 고민해야 했고, ARC는 이러한 니즈를 충족하기에 가장 적합한 솔루션이었습니다.

ARC의 동작 방식

ARC의 동작 방식은 다음 공식문서에서 자세히 확인할 수 있지만, 전반적인 동작 방식을 살펴보기 위해서 크게 세 가지 주요 단계로 나눠볼 수 있습니다.

설치 (Installation)

  • Helm을 사용하여 ARC를 Kubernetes 클러스터에 배포합니다.

  • ARC는 실질적인 CI를 수행하는 RunnerSet과 이를 매니징하는 Controller의 역할과 책임이 분리되어있습니다.

  • ARC는 GitHub API와 상호작용하여 runner scale set을 조회하거나 생성하고, 이를 Kubernetes 내에서 관리하게 됩니다.

  • 사용자는 Kubernetes CRD(Custom Resource Definition)를 이용하여 runner들의 동작 방식을 정의할 수 있습니다.

Workflow 실행 시 (Workflow Trigger)

  • 두 번째 단계는 GitHub Actions Workflow가 실행될 때 일어나는 과정입니다.

  • GitHub Actions에서 workflow가 실행되면, ARC는 실행 환경을 확인합니다.

  • 이때 runs-on 조건이 self-hosted runner를 요구하면, ARC의 Runner ScaleSet Listener 가 GitHub Actions에서 실행 대기 중인 job을 감지합니다.

  • ARC는 현재 활성화된 runner 수와 job 수를 비교하여, 추가 runner가 필요한지 확인 합니다.

  • 필요한 경우 Ephemeral RunnerSet의 replicas가 패치(Patch) 되어 새로운 runner pod을 생성할 수 있도록 반영합니다.

Runner Pod 생성(Runner Pod Creation)

  • 세 번째 단계에서는 실제 runner pod이 생성되고, GitHub Actions에 등록되는 과정입니다.

  • ARC는 새로운 runner를 생성할 필요가 있다고 판단되면, Ephemeral RunnerSet 내부에 새로운 runner pod을 생성 합니다.

  • runner는 GitHub API를 통해 작업을 처리할 준비가 되었다는 신호를 보냅니다. - 생성된 runner는 GitHub Actions의 workflow job을 처리한 후, 사용이 종료되면 자동으로 삭제됩니다.

  • 작업 중인 runner는 GitHub Actions와 HTTPS long poll을 유지하면서, 로그와 상태를 지속적으로 전송합니다.

  • job이 끝난 후에는 cleanup이 자동으로 이루어집니다.

ARC 설치 및 Access Token 설정

ARC는 Helm Chart를 이용해 쉽게 설치할 수 있으며, 몇 가지 필수적인 GitHub Access Token 설정이 필요합니다.

  • Private Repository 사용 시: repo 스코프를 포함한 Access Token 필요

  • Public Repository 사용 시: public_repo 스코프 포함

  • Organization 레벨에서 Runner 사용할 경우: admin:org 스코프 포함

중요한 점:

ARC는 Access Token을 Kubernetes Secret으로 저장합니다. 해당 Secret에 대한 1) 접근제어 및 2) GitHub Token의 만료일 관리 및 Secret Rotation을 자동화할 수 있는 체계가 필요합니다.

기본적인 ARC 설치 및 동작 테스트

설치와 기본적인 동작 테스트는 큰 어려움 없이 원활하게 진행되었습니다.

Helm을 이용해 ARC를 배포하면 문제없이 runner가 등록되었고, GitHub Actions 워크플로가 실행될 때 자동으로 runner가 프로비저닝(provisioning)되는 것을 확인했습니다.

Scale-out 및 Scale-in 정책도 정상적으로 동작하였으며, minRunners, maxRunners 설정을 통해 필요한 만큼 Runner를 늘리거나 줄이는 것이 가능했습니다.

하지만...

Self-Hosted Runner에서 docker build와 같은 Container Jobs를 실행하려면 추가적인 고민이 필요했습니다. 즉, 기본적인 runner 확장은 잘 되었지만, 컨테이너 내부에서 다시 컨테이너를 실행하는 문제(docker-in-docker, DinD)와 같은 고려 사항 이 존재했습니다.

따라서, 다음 섹션에서는 Container Jobs를 Self-Hosted Runner에서 실행하기 위한 문제 해결 과정을 다뤄보겠습니다!


2. Self-Hosted Runner에서 Container Jobs 실행하기

GitHub Actions에서 사용하는 ARC는 Kubernetes Operator로서, 각 Runner가 Kubernetes 컨테이너(pod)로 실행되는 환경 입니다. 이 때문에 일반적인 Git 명령어나 Shell Command 실행에는 문제가 없지만, Docker를 활용한 Container Jobs를 실행할 때 몇 가지 제약이 있습니다.

그렇기 때문에 아래와 같이 Docker를 실행하는 스텝이 포함된 Job을 수행하려 하면,

YAML

docker: command not found 에러가 발생하게 됩니다.

🛠 해결책: Container 내부에서 다시 Container를 실행하기 위한 방법

컨테이너 내부에서 다시 컨테이너를 실행하는 방법은 여러 가지가 있지만, 대표적으로 두 가지 방식이 고려될 수 있습니다.

1. Docker-in-Docker를 활용한 방식

DinD(Docker-in-Docker)는 컨테이너 내부에서 다시 Docker Daemon을 실행하는 방식으로, GitHub Actions ARC에서도 containerMode: "dind" 옵션을 제공하여 손쉽게 설정할 수 있습니다.

DinD 사용 시 고려해야 할 점

  • DinD를 사용하면 Pod 내부에서 sidecar 형태로 Docker Daemon이 실행되므로 Docker 명령어를 바로 사용할 수 있습니다.

  • 하지만, DinD는 privileged 옵션은 device에 대한 root access를 필요로 하기 때문에, 보안상 권장되지 않는 방식입니다.

  • Docker 공식 문서에서도 privileged 모드를 사용하는 것에 대한 주의가 필요하다고 명시하고 있습니다.

2. Kaniko와 같은 대체 빌드 도구 사용

Kaniko는 Google에서 만든 툴로, Root(Privileged) 권한 없이도 Container Image를 빌드할 수 제공합니다. 특히, Container 내부 혹은 Kubernetes 환경에서도 잘 동작할 수 있도록 설계되었습니다. 그럼에도 불구하고, 채널팀에서는 Kaniko를 사용하지 않기로 결정했습니다.

Kaniko를 채택하지 않은 이유

  • 팀은 기존에 Docker 기반의 빌드 및 캐시 운영 경험이 많이 쌓여 있는 상태입니다.

  • Kaniko를 사용하기 위해 기존 Dockerfile 및 CI/CD 스크립트를 Kaniko 빌드 환경에 맞추어 모두 수정/검증해야하는 부담이 있습니다.

  • 보안적으로 Kubernetes 클러스터 자체에서 접근제어가 존재하기 때문에, Privileged 권한이 필요하더라도 아직까지는 큰 취약점은 아니라고 판단했습니다.

Kaniko 리서치도 진행되었지만, Kaniko로 얻을 수 있는 이익에 비해, 리소스가 너무 많이 소모될 것으로 생각되어 채택하지 않았습니다.

실제 적용: GitHub Actions Helm values.yaml 설정

ARC는 Helm 설치 시 values.yaml 설정을 통해 containerMode 를 지정할 수 있습니다.

YAML

위와 같이 containerMode: "dind"을 설정하면, 자동으로 privileged 모드가 적용된 Runner가 배포되며, Docker 명령어를 실행할 수 있는 환경이 갖춰집니다.

더 나아가 containerMode 값을 사용하지 않고 직접 templates를 지정하여 Runner 내부의 Docker Daemon 설정을 추가로 커스텀하는 방법도 존재하는데, 이 부분은 2부에서 더 상세히 다뤄보겠습니다!


1부를 마치며

여기까지, GitHub Actions의 self-hosted runner를 Kubernetes 위에서 운영하는 환경을 구축 하는 과정에 대해 살펴봤습니다.ARC(Actions Runner Controller)를 활용하여 CI/CD 인프라를 유연하고 확장 가능하게 구성했으며, Container Jobs를 실행하기 위한 다양한 고려 사항(Docker-in-Docker, Kaniko 등)에 대해서도 이야기했습니다.

이제 기본적인 인프라는 세팅되었지만, 실제 운영을 하면서 예상치 못한 다양한 문제들이 발생하게 됩니다. 2부에서는 운영 과정에서 마주친 트러블슈팅 사례들을 다루면서 더 안정적인 CI/CD 환경을 만들기 위해 어떤 해결책을 적용했는지 공유하겠습니다.

본격적인 운영과 최적화를 다룰 2부에서 계속됩니다!

We Make a Future Classic Product

채널팀과 함께 성장하고 싶은 분을 기다립니다

사이트에 무료로 채널톡을 붙이세요.

써보면서 이해하는게 가장 빠릅니다

회사 이메일을 입력해주세요