Dev Notes
Web Vitals
Web Vitals
Dev Notes
Docs홈 k3s 클러스터 구축기 (3) — ArgoCD와 GitOps 배포 파이프라인
Web Vitals
Web Vitals
GitHub
Infra79분

홈 k3s 클러스터 구축기 (3) — ArgoCD와 GitOps 배포 파이프라인

ArgoCD App-of-Apps, Helm Chart 분리 설계, GitHub Actions CI를 통한 GitOps 자동 배포 구축 과정

2026년 2월 5일
k3skubernetesargocdhelmgithub-actionsgitopsci-cd

환경: k3s v1.31+ (Traefik 내장), ArgoCD v3.3+, Helm v3, GitHub Actions, Docker Hub

들어가며

2편에서 포트포워딩과 TLS를 적용하여 외부에서 HTTPS로 클러스터에 접속할 수 있게 되었습니다. 하지만 아직 실제 애플리케이션을 배포하는 체계가 없는 상태입니다. 매번 서버에 SSH로 접속해서 kubectl apply를 실행하는 것은, 서비스가 하나일 때는 괜찮아도 배포 빈도가 올라가면 곧 한계에 부딪힙니다.

이 편에서는 GitOps 방식의 배포 파이프라인을 구축합니다. 코드를 GitHub에 푸시하면 Docker 이미지가 자동으로 빌드되고, ArgoCD가 변경을 감지하여 클러스터에 배포하는 흐름입니다.

도구역할
GitHub ActionsCI — Docker 이미지 빌드 및 Docker Hub 푸시
Docker Hub컨테이너 이미지 레지스트리
HelmKubernetes 매니페스트 템플릿화 및 패키징
ArgoCDCD — Git 저장소 감시 및 클러스터 자동 배포

저장소 구조 설계

GitOps에서 중요한 설계 결정 중 하나가 앱 코드와 클러스터 설정을 같은 저장소에 둘 것인가, 분리할 것인가입니다.

앱 코드와 Helm Chart를 한 저장소에 두면 CI가 단순해지는 대신, 앱을 추가할 때마다 각 저장소에 Helm Chart가 흩어지게 됩니다. 클러스터 전체의 배포 상태를 한눈에 파악하기 어려워지며, ArgoCD가 감시해야 할 저장소도 앱 수만큼 늘어납니다.

이 클러스터에서는 ArgoCD 공식 문서의 권장 사항에 따라 설정 저장소를 별도로 분리했습니다. 앱 코드는 앱 저장소에, 클러스터에 배포되는 모든 Helm Chart와 ArgoCD 설정은 설정 저장소에 모아두는 구조입니다.

helm-chart (설정 저장소)
cluster-issuer.yaml
mirunamu
docs
Chart.yaml
values.yaml
templates
deployment.yaml
service.yaml
configmap.yaml
ingress
Chart.yaml
values.yaml
templates
ingress.yaml
argocd
app-of-apps.yaml
docs-app.yaml
ingress-app.yaml
argocd-ingress.yaml
argocd-cmd-params-cm.yaml
repo-secret.yaml

mirunamu/ 디렉토리 아래에 네임스페이스 단위로 Helm Chart를 배치하고, argocd/ 디렉토리에는 ArgoCD 자체 설정과 Application 매니페스트를 모아두었습니다. 앱을 추가할 때는 mirunamu/새앱/ 디렉토리를 만들고, argocd/에 Application YAML을 추가하면 됩니다.

Ingress를 앱 차트에 포함시키지 않고 별도 차트로 분리한 것도 의도적인 설계입니다. 앱이 여러 개로 늘어났을 때 각 앱마다 Ingress를 만들면 도메인 라우팅 규칙이 흩어지는데, 하나의 Ingress 차트에서 모든 앱의 라우팅을 관리하면 도메인 추가나 변경이 values.yaml 한 곳에서 끝납니다.

ArgoCD 설치

ArgoCD는 Git 저장소에 정의된 매니페스트와 클러스터의 실제 상태를 지속적으로 비교하며, 차이가 발생하면 자동으로 동기화하는 GitOps CD 도구입니다.

Bash
# argocd 네임스페이스 생성 및 설치
kubectl create namespace argocd
kubectl apply -n argocd --server-side --force-conflicts \
  -f https://raw.githubusercontent.com/argoproj/argo-cd/v3.3.0/manifests/install.yaml

--server-side 플래그

ArgoCD의 CRD 중 일부는 kubectl apply의 클라이언트 사이드 어노테이션 크기 제한(262KB)을 초과합니다. --server-side --force-conflicts 플래그를 사용하면 서버 사이드에서 적용되어 이 제한을 우회할 수 있습니다.

Bash
# Pod 상태 확인 (7개 모두 Running이면 정상)
kubectl get pods -n argocd
 
# 초기 관리자 비밀번호 확인
kubectl -n argocd get secret argocd-initial-admin-secret \
  -o jsonpath="{.data.password}" | base64 -d; echo

출력된 문자열이 admin 계정의 초기 비밀번호입니다.

ArgoCD 외부 접속 설정

ArgoCD 대시보드를 argocd.mirunamu.info로 접속할 수 있도록 설정합니다. 2편에서 와일드카드 A 레코드(*)를 등록해두었으므로 서브도메인 DNS 설정은 추가로 필요 없습니다.

ArgoCD 서버는 기본적으로 자체 TLS를 사용하는데, Traefik이 앞단에서 TLS를 처리하도록 내부 TLS를 비활성화해야 합니다. 그렇지 않으면 Traefik과 ArgoCD 간에 TLS 이중 처리가 발생하여 502 에러가 나타날 수 있습니다.

이 설정을 kubectl patch로 직접 실행할 수도 있지만, 설정 저장소에 YAML 파일로 관리하면 ArgoCD가 자기 자신의 설정까지 Git으로 관리하게 됩니다.

YAML
# argocd/argocd-cmd-params-cm.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cmd-params-cm
  namespace: argocd
  labels:
    app.kubernetes.io/name: argocd-cmd-params-cm
    app.kubernetes.io/part-of: argocd
data:
  server.insecure: "true"

처음에는 ArgoCD가 아직 설정 저장소를 감시하고 있지 않으므로 수동으로 적용합니다.

Bash
kubectl apply -f argocd-cmd-params-cm.yaml
kubectl -n argocd rollout restart deployment argocd-server
kubectl apply -f argocd-ingress.yaml

1~2분 후 https://argocd.mirunamu.info에 접속하면 로그인 화면이 나타납니다. admin 계정으로 로그인한 뒤 반드시 비밀번호를 변경합니다.

앱 차트: mirunamu/docs

Next.js 앱을 배포하는 Helm Chart입니다. Deployment, Service, ConfigMap을 생성하며, 이미지 태그와 리소스 설정은 values.yaml로 관리합니다.

YAML
# mirunamu/docs/values.yaml (핵심 설정)
replicaCount: 1
 
image:
  repository: mirunamu00/mirunamu-docs
  tag: 610df67af85e18a2cdbca38f0e41d2afe1b64dfa
  pullPolicy: Always
 
service:
  port: 80
  targetPort: 3000
 
resources:
  requests:
    cpu: 250m
    memory: 256Mi
  limits:
    cpu: 500m
    memory: 512Mi
 
env:
  - name: GITHUB_OWNER
    value: "mirunamu00"
  - name: GITHUB_REPO
    value: "docs"
 
existingSecret: "docs-secret"
 
healthCheck:
  enabled: true
  path: /
 
strategy:
  type: RollingUpdate
  maxSurge: 1
  maxUnavailable: 0

image.tag에 커밋 해시가 들어가 있는 것이 핵심입니다. GitHub Actions가 이미지를 빌드한 후 이 값을 업데이트하면, ArgoCD가 변경을 감지하여 새 버전을 배포합니다.

existingSecret은 API 키나 OAuth 시크릿 같은 민감 정보를 Helm Chart에 포함시키지 않고, 클러스터에 직접 생성한 Secret을 참조하는 패턴입니다. 이렇게 하면 시크릿 값이 Git에 노출되지 않습니다.

Deployment 템플릿에서 주목할 부분은 프로덕션 환경을 위한 설정들입니다.

YAML
# mirunamu/docs/templates/deployment.yaml (핵심 부분)
spec:
  replicas: {{ .Values.replicaCount }}
  strategy:
    type: {{ .Values.strategy.type }}
    rollingUpdate:
      maxSurge: {{ .Values.strategy.maxSurge }}
      maxUnavailable: {{ .Values.strategy.maxUnavailable }}
  template:
    metadata:
      annotations:
        # ConfigMap 내용이 바뀌면 해시값이 달라져서 Pod가 자동 재시작
        checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }}
    spec:
      containers:
      - name: {{ .Chart.Name }}
        image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
        resources:
          {{- toYaml .Values.resources | nindent 10 }}
 
        # 기존 Secret에서 환경변수 일괄 주입
        envFrom:
          - secretRef:
              name: {{ .Values.existingSecret }}
 
        # readinessProbe: 트래픽 받을 준비 확인 (실패 시 Service에서 제외)
        readinessProbe:
          httpGet:
            path: {{ .Values.healthCheck.path }}
            port: {{ .Values.service.targetPort }}
          initialDelaySeconds: 5
          periodSeconds: 10
 
        # livenessProbe: 컨테이너 생존 확인 (실패 시 재시작)
        livenessProbe:
          httpGet:
            path: {{ .Values.healthCheck.path }}
            port: {{ .Values.service.targetPort }}
          initialDelaySeconds: 10
          periodSeconds: 30

RollingUpdate 전략에서 maxSurge: 1, maxUnavailable: 0은 새 Pod를 먼저 띄우고 정상 확인 후 기존 Pod를 내리는 무중단 배포를 의미합니다. checksum/config 어노테이션은 ConfigMap 내용이 바뀌면 해시가 달라지면서 Pod가 자동으로 재시작되는 트릭입니다. Kubernetes는 ConfigMap 변경만으로는 Pod를 재시작하지 않기 때문에, 이런 방식으로 변경을 감지합니다.

Next.js 캐시 저장소

Next.js는 런타임에 ISR 재생성, fetch 데이터 캐시, 이미지 최적화 결과 등 다양한 캐시를 생성합니다. 기본적으로 이 캐시들은 Pod의 로컬 파일시스템에 저장되기 때문에, Pod가 재시작되면 사라지고 여러 Pod 간에 공유도 되지 않습니다.

이 문제를 해결하기 위해 처음에는 PVC(Persistent Volume Claim)로 캐시 디렉토리를 영속화하는 방식을 사용했지만, 이후 Redis를 클러스터에 배포하고 Next.js의 cacheHandler를 통해 런타임 캐시를 Redis에 저장하는 방식으로 전환했습니다. Redis를 사용하면 Pod 간 캐시 공유가 자연스럽고, 레플리카 확장 시에도 별도의 StorageClass 고민이 필요 없습니다.

Redis 배포부터 Next.js 캐시 핸들러 연동까지의 전체 과정은 4편: Redis 배포와 5편: Next.js Redis 캐시 연동에서 다룹니다.

Ingress 차트: mirunamu/ingress

Ingress를 앱 차트에서 분리하여 독립 차트로 관리합니다. values.yaml의 apps 리스트에 도메인과 서비스 이름을 추가하는 것만으로 새 앱의 라우팅을 설정할 수 있습니다.

YAML
# mirunamu/ingress/values.yaml
ingressClassName: traefik
 
annotations:
  cert-manager.io/cluster-issuer: letsencrypt-prod
 
tls:
  enabled: true
  secretName: mirunamu-info-tls
 
apps:
  - host: docs.mirunamu.info
    serviceName: docs
    servicePort: 80

apps 리스트를 range로 순회하면서 각 도메인별 라우팅 규칙과 TLS 호스트를 동적으로 생성합니다. 새 앱을 추가할 때는 apps에 항목 하나를 추가하면 됩니다.

YAML
# 앱 추가 시 values.yaml에 항목 추가
apps:
  - host: docs.mirunamu.info
    serviceName: docs
    servicePort: 80
  - host: api.mirunamu.info      # 새 앱
    serviceName: api
    servicePort: 80

TLS Secret 하나(mirunamu-info-tls)에 모든 서브도메인의 인증서가 포함됩니다. cert-manager가 apps에 등록된 호스트들의 인증서를 자동으로 발급하고 갱신합니다.

App-of-Apps 패턴

여기까지 Helm Chart를 작성했지만, ArgoCD에게 "이 차트를 배포하라"고 지시하는 Application이 아직 없습니다. 앱마다 Application을 kubectl apply로 수동 생성할 수도 있지만, 앱이 늘어날수록 관리가 번거로워집니다.

App-of-Apps 패턴은 이 문제를 해결합니다. 루트 Application 하나가 argocd/ 디렉토리를 감시하고, 그 안에 있는 Application YAML들을 자동으로 생성하고 관리합니다. 새 앱을 추가할 때는 argocd/ 디렉토리에 YAML 파일 하나를 커밋하면 끝입니다.

YAML
# argocd/app-of-apps.yaml — 루트 Application
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: app-of-apps
  namespace: argocd
spec:
  project: default
  source:
    repoURL: https://github.com/mirunamu00/helm-chart.git
    targetRevision: master
    path: argocd
  destination:
    server: https://kubernetes.default.svc
    namespace: argocd
  syncPolicy:
    automated:
      prune: true
      selfHeal: true

app-of-apps.yaml이 argocd/ 경로를 감시하므로, 같은 디렉토리에 있는 docs-app.yaml, ingress-app.yaml, argocd-ingress.yaml, argocd-cmd-params-cm.yaml까지 전부 ArgoCD가 관리합니다. ArgoCD가 자기 자신의 Ingress와 설정까지 Git으로 관리하는 셈입니다.

설정 저장소가 프라이빗이라면 ArgoCD가 읽을 수 있도록 인증 정보를 등록해야 합니다.

YAML
# argocd/repo-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: helm-chart-repo
  namespace: argocd
  labels:
    argocd.argoproj.io/secret-type: repository
stringData:
  type: git
  url: https://github.com/mirunamu00/helm-chart.git
  username: mirunamu00
  password: "ghp_xxxxxxxxxxxxxxxxxxxx"  # GitHub PAT (repo 스코프)

Secret 관리

repo-secret.yaml에는 GitHub PAT가 포함되어 있으므로 .gitignore에 추가하거나, 최초 1회 kubectl apply로 직접 생성한 뒤 Git에는 커밋하지 않는 것이 안전합니다.

최초 부트스트랩

모든 파일이 준비되었으면 app-of-apps.yaml 한 번만 적용하면 됩니다.

Bash
# 저장소 인증 (프라이빗 저장소인 경우)
kubectl apply -f repo-secret.yaml
 
# 루트 Application 생성 — 이것 한 번이면 나머지는 전부 자동
kubectl apply -f app-of-apps.yaml

ArgoCD가 argocd/ 디렉토리를 읽어서 docs-app.yaml, ingress-app.yaml 등을 자동으로 생성하고, 각 Application이 mirunamu/docs, mirunamu/ingress 경로의 Helm Chart를 클러스터에 배포합니다. ArgoCD 대시보드에서 모든 앱의 동기화 상태를 확인할 수 있습니다.

GitHub Actions CI

앱 저장소(docs)에 코드를 푸시하면 Docker 이미지를 빌드하고, 설정 저장소의 values.yaml에 새 이미지 태그를 커밋하는 워크플로우입니다. 앱 저장소와 설정 저장소가 분리되어 있으므로, 설정 저장소에 CI 워크플로우가 없어 무한 루프 걱정이 없습니다.

YAML
# .github/workflows/deploy.yaml (앱 저장소에 위치)
name: Build and Deploy
on:
  push:
    branches: [master]
 
jobs:
  build-and-push:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKERHUB_USERNAME }}
          password: ${{ secrets.DOCKERHUB_TOKEN }}
 
      - uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: |
            mirunamu00/mirunamu-docs:${{ github.sha }}
            mirunamu00/mirunamu-docs:latest
 
      - name: Update Helm Chart
        run: |
          git clone https://x-access-token:${{ secrets.HELM_REPO_PAT }}@github.com/mirunamu00/helm-chart.git
          cd helm-chart
          sed -i "s|tag:.*|tag: ${{ github.sha }}|" mirunamu/docs/values.yaml
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add mirunamu/docs/values.yaml
          git commit -m "chore: update docs image tag to ${{ github.sha }}"
          git push

이 워크플로우가 동작하려면 앱 저장소의 Settings → Secrets에 세 개의 시크릿을 등록해야 합니다.

시크릿설명
DOCKERHUB_USERNAMEDocker Hub 사용자명
DOCKERHUB_TOKENDocker Hub Access Token
HELM_REPO_PATGitHub PAT — 설정 저장소에 push 권한(repo 스코프) 필요

GitHub Actions가 설정 저장소의 values.yaml을 업데이트하면, ArgoCD가 이 변경을 감지하여 새 이미지 태그로 배포를 시작합니다.

배포 흐름 확인

모든 설정이 끝났으면, 실제로 코드를 변경하여 자동 배포가 동작하는지 확인합니다.

코드 변경 및 푸시

앱 소스코드를 수정하고 master 브랜치에 푸시합니다.

Bash
git add .
git commit -m "feat: update homepage"
git push origin master

GitHub Actions 확인

앱 저장소의 Actions 탭에서 워크플로우 실행을 확인합니다. Docker 이미지가 빌드되어 Docker Hub에 푸시되고, 설정 저장소의 values.yaml이 새 커밋 해시로 업데이트됩니다.

ArgoCD 동기화 확인

ArgoCD가 설정 저장소의 변경을 감지하면 자동으로 동기화를 시작합니다. 기본 폴링 간격은 3분이므로, 즉시 확인하고 싶다면 대시보드에서 Sync 버튼을 클릭합니다.

배포 결과 확인

https://docs.mirunamu.info에 접속하여 변경 사항이 반영되었는지 확인합니다.

Bash
# Pod 상태 확인
kubectl get pods -n mirunamu
 
# 현재 배포된 이미지 태그 확인
kubectl get deployment docs -n mirunamu \
  -o jsonpath='{.spec.template.spec.containers[0].image}'

전체 흐름을 정리하면 다음과 같습니다.

앱 코드 푸시 → GitHub Actions (이미지 빌드 + Docker Hub 푸시)
  → 설정 저장소 values.yaml 업데이트
  → ArgoCD (변경 감지 + 클러스터 동기화) → 새 버전 배포 완료

트러블슈팅

정리

3편 요약

  1. ArgoCD 설치 및 외부 접속: GitOps CD 도구를 설치하고, Traefik Ingress로 대시보드를 외부에 노출합니다
  2. 설정 저장소 분리: 앱 코드와 Helm Chart를 별도 저장소로 분리하여 클러스터 전체 설정을 한 곳에서 관리합니다
  3. Ingress 차트 독립: 다중 앱 라우팅을 하나의 Ingress 차트에서 관리하여 도메인 추가를 values.yaml 수정으로 처리합니다
  4. App-of-Apps: 루트 Application 하나로 모든 하위 Application을 자동 관리합니다
  5. GitHub Actions CI: 앱 저장소에서 이미지를 빌드하고, 설정 저장소의 이미지 태그를 자동 업데이트합니다

이것으로 물리 장비 위에 올린 k3s 클러스터에 완전한 GitOps 배포 파이프라인이 갖춰졌습니다. USB에 Ubuntu를 구워 넣는 것에서 시작하여, 외부 접속과 TLS를 거쳐, 코드 푸시 한 번으로 자동 배포되는 환경까지 도달했습니다. 새로운 서비스를 추가할 때는 mirunamu/새앱/ 디렉토리에 Helm Chart를 만들고, argocd/에 Application YAML을 추가하면 됩니다.

참고 자료

  • ArgoCD 공식 문서
  • ArgoCD App of Apps 패턴
  • Helm Chart 개발 가이드
  • GitHub Actions - Docker Build and Push
Written by

Mirunamu (Park Geonwoo)

Software Developer

관련 글 더보기

Infra

홈 k3s 클러스터 구축기 (1) — Ubuntu 설치부터 워커 노드 조인까지

4코어 8GB 미니PC 2대로 시작하는 경량 Kubernetes 홈 클러스터 구축 과정

읽기
Infra

홈 k3s 클러스터 구축기 (2) — 외부 노출과 TLS 인증서

공유기 포트포워딩, 도메인 연결, cert-manager를 통한 HTTPS 적용 과정

읽기
Infra

홈 k3s 클러스터 구축기 (6) — Longhorn 분산 스토리지

CSI 기반 분산 스토리지 개념과 Longhorn ArgoCD 배포, 데이터베이스 워크로드 적용

읽기
Infra

홈 k3s 클러스터 구축기 (7) — Prometheus + Grafana 모니터링

kube-prometheus-stack 기반 클러스터 모니터링 환경의 ArgoCD 배포와 Grafana 대시보드 활용

읽기
이전 글홈 k3s 클러스터 구축기 (2) — 외부 노출과 TLS 인증서
다음 글홈 k3s 클러스터 구축기 (4) — Redis 배포와 RedisInsight
목차
  • 들어가며
  • 저장소 구조 설계
  • ArgoCD 설치
  • ArgoCD 외부 접속 설정
  • 앱 차트: mirunamu/docs
    • Next.js 캐시 저장소
  • Ingress 차트: mirunamu/ingress
  • App-of-Apps 패턴
    • 최초 부트스트랩
  • GitHub Actions CI
  • 배포 흐름 확인
  • 트러블슈팅
  • 정리
  • 참고 자료