
무색 홈페이지를 홈랩 Kubernetes에 배포하면서 CI/CD가 필요해졌다. Docker 이미지를 수동으로 빌드하고 푸시하는 건 딱 두 번까지만 참을 수 있는 일이니까.
이미 홈랩에 Gitea가 돌고 있었고 Gitea Actions라는 기능이 있다는 걸 알고 있었다. GitHub Actions와 문법이 거의 같다길래 워크플로우 파일 하나 만들면 끝이라 생각했다. 결론부터 말하면 7번의 커밋이 더 필요했다.
첫 번째 벽은 actions/checkout이었다. GitHub Actions에서 거의 모든 워크플로우의 첫 줄에 들어가는 그 액션이 Gitea의 act_runner 환경에서는 동작하지 않았다.
원인은 DinD(Docker-in-Docker) sidecar 구성에 있었다. act_runner가 컨테이너 안에서 실행되는데, 그 안에서 Docker 명령을 쓰려면 별도의 DinD 컨테이너가 sidecar로 붙는다. 문제는 이 구조에서 워크스페이스 마운트가 일반적인 방식으로 안 된다는 것.
해결은 단순했다. actions/checkout을 버리고 git clone을 직접 쓰는 거다.
- name: Checkout
run: git clone --branch master https://gitea.xssh.org/admin/museck-public.git .끝에 점(.) 하나가 핵심이다. 현재 디렉토리에 바로 clone해야 이후 docker build가 정상 동작한다. 서브디렉토리에 clone하면 Dockerfile 경로가 꼬인다.
두 번째 벽은 Docker CLI 버전이었다. act_runner 이미지에 기본 탑재된 docker-cli가 DinD sidecar의 Docker API(v1.44+)와 호환되지 않았다. apt-get install docker.io로 설치되는 버전이 너무 낡아서 API 핸드셰이크부터 실패하는 상황.
Docker 공식 레포지토리에서 최신 docker-ce-cli를 설치하는 방식으로 해결했다.
- name: Install dependencies
run: |
apt-get update && apt-get install -y ca-certificates curl git
install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/debian/gpg \
-o /etc/apt/keyrings/docker.asc
echo "deb [arch=amd64 signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/debian bookworm stable" \
> /etc/apt/sources.list.d/docker.list
apt-get update && apt-get install -y docker-ce-cli매번 빌드할 때마다 docker-cli를 새로 설치하는 게 좀 아깝긴 하다. 하지만 커스텀 러너 이미지를 만드는 건 나중으로 미뤘다. 지금은 돌아가는 게 우선이었으니까.
삽질 끝에 완성된 파이프라인은 이런 흐름이다.
master 브랜치에 push하면 Gitea가 webhook으로 act_runner를 깨운다. 러너가 코드를 clone하고 Docker 이미지를 빌드해서 Gitea 내장 레지스트리(gitea.xssh.org/admin/museck)에 push한다. 마지막으로 homelab 레포의 staging kustomization.yaml 이미지 태그를 새 커밋 SHA로 업데이트하면 ArgoCD가 이를 감지해서 K8s 클러스터에 자동 배포한다.
GitOps 패턴의 장점이 여기서 드러난다. 빌드 파이프라인은 이미지를 만들어서 레지스트리에 올리는 것까지만 책임지고, 실제 배포는 ArgoCD가 매니페스트 레포의 변경을 감지해서 처리한다. 빌드와 배포가 완전히 분리돼 있어서 문제가 생기면 매니페스트 레포의 태그만 되돌리면 된다.
커밋 로그를 돌아보면 삽질의 궤적이 선명하다.
actions/checkout 넣고 실행 -- 실패. DinD 환경에서 워크스페이스 마운트가 안 됨.git clone으로 교체 -- 코드는 받았는데 docker build 실패.GitHub Actions에서 당연하게 쓰던 기능이 셀프 호스팅에서는 전혀 당연하지 않았다. 관리형 서비스가 추상화해 주는 부분이 얼마나 많은지 직접 겪어봐야 체감된다. 러너가 어떤 환경에서 도는지, Docker 데몬이 어디 있는지, 워크스페이스가 어떻게 마운트되는지. 이런 걸 신경 써본 적이 없으니까.
DinD 구성은 보안 면에서는 낫다. 호스트의 Docker 소켓을 직접 마운트하는 것보다 격리가 잘 되니까. 대신 이렇게 표준 액션이 안 되는 트레이드오프가 있다. 홈랩이라 보안에 크게 민감하지 않지만 그래도 DinD 쪽이 마음이 편하다.
셀프 호스팅 CI/CD를 구축하면서 느낀 건 하나다. 관리형 서비스의 "당연한" 기능을 직접 만들다 보면 그 추상화 아래에 뭐가 있었는지 비로소 보인다는 것.
actions/checkout 한 줄이 내부에서 얼마나 많은 일을 하는지 이번에 알았다. 워크스페이스 준비, git 설정, 브랜치 체크아웃, shallow clone 최적화 같은 것들. 그걸 전부 git clone --branch master ... . 한 줄로 때울 수 있었던 건 우리 프로젝트가 단순했기 때문이지 일반적인 해법은 아니다.
다음은 이 파이프라인에 production 배포를 붙일 차례다. staging은 자동 배포지만 production은 수동 트리거로 가야 한다. 그건 다음 글에서.