무색
기술블로그
에세이
연구
소개

무색

소프트웨어로 비즈니스의 가능성을 만듭니다. 웹·앱 개발, 음성 AI, 자동화 콘텐츠 제작까지 — 기술이 필요한 곳에 무색이 있습니다.

연락처

contact@museck.com

사업자 정보

상호: 무색

대표: 배성재

사업자등록번호: 577-58-00836

인천광역시 연수구 인천타워대로 323, 에이동 8층 801-802호 AB-132 (송도동, 송도 센트로드)

© 2026 무색. All rights reserved.
개인정보처리방침·이용약관·연락처
INCHEON, KR
GitOps 파이프라인 — Git에서 K8s까지 배포 흐름
홈랩 삽질기
2025. 12. 31.

홈랩에 GitOps 파이프라인 구축하기: Gitea + ArgoCD App of Apps 패턴

GitOpsArgoCDGiteaKubernetesKustomize

kubectl apply의 한계

홈랩에서 쿠버네티스를 돌리다 보면 처음에는 kubectl apply -f로 충분하다. 매니페스트 하나 만들고 적용하면 끝이니까. 그런데 서비스가 하나둘 늘어나면서 문제가 생긴다. Syncthing, Gitea, 회사 홈페이지, 모니터링 도구... 어느 순간 "지금 클러스터에 뭐가 돌아가고 있지?"를 터미널 없이는 답할 수 없게 된다.

설정을 바꾸고 apply를 했는데 이전 상태가 뭐였는지 기억이 안 나는 경우도 잦았다. 롤백하려면? 기억에 의존해야 하지. Windows 11 위에 Hyper-V, Proxmox, 그 위에 kubeadm으로 올린 클러스터라 환경 자체가 복잡한데, 배포 관리까지 머릿속에 두기엔 한계가 분명했다.

그래서 GitOps 체계를 갖추기로 했다. Git 저장소를 Single Source of Truth로 삼아서, 클러스터 상태를 항상 코드로 추적 가능하게 만드는 거다.

도구 선택: Gitea + ArgoCD

GitOps를 구현하려면 두 가지가 필요하다. Git 서버와 CD(Continuous Delivery) 컨트롤러.

Git 서버로는 Gitea를 골랐다. Go로 작성되어 리소스를 적게 먹고, 자체 OCI 컨테이너 레지스트리를 내장하고 있어 Docker Hub 없이도 이미지를 push/pull 할 수 있다. GitHub Actions 호환 CI(act_runner)까지 지원하니까 Git + Registry + CI를 단일 서비스로 해결할 수 있는 셈이지.

CD 컨트롤러는 ArgoCD이다. Git 저장소의 선언적 매니페스트와 클러스터 실제 상태를 비교해서 차이가 생기면 자동으로 동기화해준다. Flux도 고려했지만 ArgoCD의 웹 UI가 시각적으로 현재 상태를 파악하기 좋아서 선택했다.

Gitea 배포: Git 서버 + 컨테이너 레지스트리

Gitea는 SQLite + PVC 구성으로 단순하게 올렸다. 홈랩에서 별도 DB를 운영하는 건 오버헤드가 크니까. PVC가 worker1 노드에 바인딩되어 있어서 nodeSelector로 worker1에 고정시켰다.

컨테이너 레지스트리는 Gitea 설정에서 활성화만 하면 된다. 이후 docker build한 이미지를 gitea.xssh.org/admin/museck 같은 경로로 push하면 클러스터 안에서 바로 pull 가능하다. 외부 레지스트리 의존 없이 전부 자체 인프라로 돌아가는 구조이다.

ArgoCD 설치와 App of Apps 패턴

ArgoCD는 Helm으로 설치했다. 홈랩이니 non-HA 모드로 최소 리소스만 할당하고, 역시 nodeSelector로 worker1에 고정했다.

여기서 핵심은 App of Apps 패턴이다. ArgoCD에 앱을 하나하나 수동 등록하면 서비스가 늘어날 때마다 ArgoCD UI를 만져야 한다. App of Apps 패턴은 이 문제를 해결한다.

원리는 간단하다. root라는 Application 하나가 Git 저장소의 apps/ 디렉토리를 감시한다. 이 디렉토리에 Application YAML을 추가하면 ArgoCD가 자동으로 감지해서 새 앱으로 등록한다. 새 서비스를 배포하고 싶으면 YAML 파일 하나만 커밋하면 끝이야.

root Application의 설정은 이렇게 생겼다.

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: root
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://gitea.xssh.org/homelab/homelab.git
    targetRevision: master
    path: k8s-platform/services/argocd/apps
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

syncPolicy.automated를 보면 prune: true와 selfHeal: true가 있다. prune은 Git에서 삭제된 리소스를 클러스터에서도 지워주고, selfHeal은 누군가 kubectl로 직접 수정해도 Git 상태로 되돌려준다. 진정한 GitOps라면 이 두 옵션은 켜두는 게 맞다고 본다.

AppProject로 권한 분리하기

서비스가 늘어나면 권한 분리가 필요해진다. ArgoCD의 AppProject를 활용해서 세 개 프로젝트로 나눴다.

  • platform - ArgoCD, cert-manager 같은 인프라 서비스
  • company-internal - Syncthing 같은 사내 도구
  • company-public - 회사 홈페이지(museck) 같은 외부 공개 서비스

각 AppProject는 배포 가능한 네임스페이스를 제한한다. company-internal 프로젝트의 앱은 syncthing 네임스페이스에만 배포할 수 있고, 다른 네임스페이스는 건드릴 수 없다. 실수로 잘못된 네임스페이스에 배포하는 사고를 방지할 수 있지.

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: company-internal
  namespace: argocd
spec:
  destinations:
    - namespace: syncthing
      server: https://kubernetes.default.svc
  sourceRepos:
    - https://gitea.xssh.org/homelab/homelab.git

첫 번째 앱 배포: museck Kustomize 구조

파이프라인의 첫 대상 앱으로 회사 홈페이지(museck)를 선택했다. Kustomize의 base/overlay 패턴을 적용해서 staging과 production이 같은 base를 공유하되 환경별 차이만 overlay에서 덮어쓰는 구조이다.

base에 들어가는 Deployment는 이렇게 생겼다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: museck-app
spec:
  replicas: 1
  template:
    spec:
      nodeSelector:
        node-role: infra
      containers:
        - name: app
          image: gitea.xssh.org/admin/museck
          ports:
            - containerPort: 3000
          envFrom:
            - configMapRef:
                name: museck-config

이미지 경로가 gitea.xssh.org/admin/museck인 게 보이지. Gitea 내장 레지스트리에서 바로 가져온다. CI에서 docker build하고 이 레지스트리에 push하면 ArgoCD가 이미지 태그 변경을 감지하고 롤링 업데이트를 수행한다.

overlay에서는 도메인이나 이미지 태그, 리소스 제한 같은 환경별 설정만 패치한다. staging은 museck.xssh.org, production은 museck.com으로 도메인이 다르지만 애플리케이션 코드와 기본 구조는 동일하니까.

전체 흐름: push에서 배포까지

이 모든 조각이 맞춰지면 배포 흐름은 이렇게 된다.

  1. 개발자가 museck 코드를 수정하고 Gitea에 git push
  2. Gitea Actions(act_runner)가 트리거되어 Docker 이미지를 빌드하고 Gitea Registry에 push
  3. CI 워크플로우가 homelab 저장소의 overlay에서 이미지 태그를 새 SHA로 업데이트
  4. ArgoCD가 Git 변경을 감지하고 auto-sync로 K8s 클러스터에 롤링 업데이트 수행

개발자가 코드를 push하면 나머지는 전부 자동이다. 터미널에서 kubectl을 칠 일이 없어.

삽질 기록

PVC와 nodeSelector의 함정

Syncthing의 nodeSelector를 깜빡해서 worker2에 스케줄링된 적이 있다. 당연히 worker1에 있는 PVC에 접근이 안 되니까 Pod이 뜨질 않았지. 이 경험 이후로 PVC를 쓰는 Pod은 반드시 node-role: infra(worker1)에 고정한다는 규칙을 세웠다.

디렉토리 구조 리팩토링

처음에 Gitea와 ArgoCD를 k8s-company-internal 디렉토리에 넣었다가 k8s-platform으로 옮기는 작업을 했다. Git 서버나 CD 컨트롤러는 비즈니스 서비스가 아니라 플랫폼 인프라니까. 처음부터 인프라와 비즈니스 서비스의 경계를 명확하게 잡았어야 했는데, 뒤늦게 깨달은 셈이다.

progressDeadlineSeconds의 교훈

Syncthing Deployment에 progressDeadlineSeconds를 명시적으로 설정했다가 되돌렸다. 기본값인 600초가 대부분 상황에서 충분하고, 불필요한 설정은 오히려 매니페스트를 복잡하게 만들 뿐이라는 걸 배웠다. 쿠버네티스의 기본값은 대체로 잘 정해져 있어. 특별한 이유 없이 건드리면 나중에 "왜 이 값이 여기 있지?" 하고 헷갈리게 된다.

마무리하며

이제 홈랩의 모든 서비스 상태가 Git 저장소 하나에 담겨 있다. 새 서비스를 추가하려면 YAML 파일 하나를 apps/ 디렉토리에 커밋하면 되고, 기존 서비스 설정을 바꾸려면 해당 매니페스트를 수정하고 push하면 된다. 클러스터가 날아가도 git clone 한 번이면 전체 인프라를 복원할 수 있다는 안정감이 가장 크다.

GitOps가 거창한 인프라에서만 의미 있는 건 아니다. 오히려 홈랩처럼 한 사람이 여러 서비스를 운영하는 환경에서 더 빛난다고 느꼈다. "인프라를 코드로 관리한다"는 습관 자체가 이후 규모가 커지더라도 그대로 적용할 수 있는 기반이 되니까.

자주 묻는 질문

ArgoCD App of Apps 패턴이란 무엇인가요?
하나의 root Application이 apps/ 디렉토리를 감시하고, 그 안에 새 Application YAML을 커밋하면 자동으로 앱이 등록되는 패턴입니다. 서비스 추가 시 ArgoCD UI를 조작할 필요 없이 YAML 파일 하나로 배포가 완료됩니다.
홈랩에서 Gitea를 쓰면 GitHub 없이도 CI/CD가 가능한가요?
네. Gitea는 Git 서버, OCI 컨테이너 레지스트리, GitHub Actions 호환 CI(act_runner)를 모두 내장하고 있어서 외부 서비스 의존 없이 Git + Registry + CI를 단일 서비스로 운영할 수 있습니다.
ArgoCD의 selfHeal과 prune 옵션은 켜야 하나요?
GitOps 원칙을 지키려면 둘 다 켜는 것을 권장합니다. selfHeal은 kubectl로 직접 수정해도 Git 상태로 되돌려주고, prune은 Git에서 삭제된 리소스를 클러스터에서도 정리해줍니다.
홈랩 삽질기(2/19)
Prev

홈랩 TLS 자동화: mkcert 자체 서명에서 cert-manager + Let's Encrypt로

Next

containerd 기반 K8s에서 Docker 빌드하기: DinD 사이드카와 ArgoCD 크래시 수정기