13년차의 서버실

서버, 인프라, 홈랩 기술 블로그

[태그:] GitOps

  • [k8s] Helm Chart 베스트 프랙티스: 프로덕션 배포 및 관리 전략

    Helm Chart를 제대로 쓰고 있는 게 맞나요?

    솔직히 말씀드리면, 저도 처음 Helm을 쓰기 시작했을 때는 그냥 helm install 하나로 모든 게 해결된다고 생각했거든요. 차트 가져다 쓰고, values 파일 조금 수정하고, 배포하면 끝. 근데 이게 개발 환경에서는 통했는데, 프로덕션에 올리는 순간 문제가 터지기 시작하더라고요.

    롤백이 안 된다, 시크릿이 차트에 하드코딩돼 있다, 팀원이 values 파일을 잘못 수정해서 서비스가 내려갔다… 이런 일들을 겪으면서 “아, Helm Chart 베스트 프랙티스라는 게 괜히 있는 게 아니구나” 싶었습니다. 그래서 오늘은 13년 동안 쿠버네티스 인프라를 운영하면서 직접 삽질하며 정리한 Helm 배포 전략과 관리 노하우를 공유해드리려고 해요.

    쿠버네티스 Helm을 처음 시작하신 분들도, 이미 쓰고 계신데 뭔가 찜찜한 분들도 도움이 될 거예요.

    ▲ Helm Chart가 쿠버네티스 클러스터에 배포되는 전체 흐름 — Chart Repository부터 Release 관리까지 한눈에 볼 수 있습니다.

    Helm이 뭔지 다시 한번 짚고 가기

    아마 대부분 아시겠지만, 한 번 정리하고 넘어갈게요. Helm(헬름)은 쿠버네티스의 패키지 매니저입니다. 쉽게 말해, apt나 yum처럼 쿠버네티스 애플리케이션을 패키징하고 배포하는 도구예요.

    핵심 개념 세 가지만 기억하시면 됩니다.

    • Chart(차트): 쿠버네티스 리소스를 정의하는 파일 묶음. npm의 package.json 같은 개념이에요.
    • Release(릴리스): 클러스터에 설치된 Chart의 인스턴스. 같은 Chart를 여러 번 설치하면 각각 다른 Release가 됩니다.
    • Repository(레포지토리): Chart를 저장하고 공유하는 저장소. Docker Hub의 Chart 버전이라고 보시면 돼요.

    Helm 3 기준으로 설명드릴 거예요. Helm 2는 Tiller(틸러)라는 서버 컴포넌트가 있었는데, 보안 문제로 Helm 3에서 완전히 제거됐거든요. 혹시 아직 Helm 2를 쓰시는 분 계시면, 진짜 빨리 마이그레이션하세요.

    Chart 구조를 제대로 잡는 것부터 시작

    프로덕션 Helm 관리의 첫 번째 원칙은 Chart 디렉토리 구조를 일관성 있게 가져가는 것입니다. 처음부터 잘 잡아두지 않으면 나중에 수습하기가 정말 힘들어요.

    my-app/
    ├── Chart.yaml          # 차트 메타데이터 (이름, 버전, 의존성)
    ├── values.yaml         # 기본 설정값
    ├── values-dev.yaml     # 개발 환경 오버라이드
    ├── values-staging.yaml # 스테이징 환경 오버라이드
    ├── values-prod.yaml    # 프로덕션 환경 오버라이드
    ├── templates/
    │   ├── _helpers.tpl    # 재사용 가능한 템플릿 함수
    │   ├── deployment.yaml
    │   ├── service.yaml
    │   ├── ingress.yaml
    │   ├── configmap.yaml
    │   ├── hpa.yaml        # HorizontalPodAutoscaler
    │   ├── pdb.yaml        # PodDisruptionBudget
    │   └── NOTES.txt       # 설치 후 출력되는 안내 메시지
    └── charts/             # 의존 차트들 (서브차트)

    여기서 핵심은 환경별 values 파일을 분리하는 거예요. 하나의 values.yaml에 모든 환경 설정을 때려넣는 분들이 많은데, 그러면 관리가 안 됩니다. 제가 실제로 운영하는 방식은 기본값은 values.yaml에 두고, 환경별 차이점만 오버라이드 파일에 담아두는 거죠.

    # values.yaml (기본값)
    replicaCount: 1
    
    image:
      repository: my-registry/my-app
      tag: "latest"  # CI/CD에서 덮어씁니다
      pullPolicy: IfNotPresent
    
    resources:
      requests:
        cpu: 100m
        memory: 128Mi
      limits:
        cpu: 500m
        memory: 512Mi
    
    autoscaling:
      enabled: false
      minReplicas: 1
      maxReplicas: 10
      targetCPUUtilizationPercentage: 70
    
    podDisruptionBudget:
      enabled: false
      minAvailable: 1
    # values-prod.yaml (프로덕션 오버라이드)
    replicaCount: 3
    
    image:
      pullPolicy: Always
    
    resources:
      requests:
        cpu: 500m
        memory: 512Mi
      limits:
        cpu: 2000m
        memory: 2Gi
    
    autoscaling:
      enabled: true
      minReplicas: 3
      maxReplicas: 20
    
    podDisruptionBudget:
      enabled: true
      minAvailable: 2

    배포할 때는 이렇게 쓰면 됩니다.

    # 프로덕션 배포
    helm upgrade --install my-app ./my-app \
      -f values.yaml \
      -f values-prod.yaml \
      --namespace production \
      --create-namespace \
      --set image.tag=${IMAGE_TAG}

    💡 팁: --install 플래그를 함께 쓰면 없으면 설치하고, 있으면 업그레이드합니다. CI/CD 파이프라인에서 정말 유용하게 쓰이는 옵션이더라고요.

    실전 배포 전략: 이것만 지켜도 반은 성공

    ▲ GitOps 기반 Helm 배포 파이프라인 — Git push부터 프로덕션 릴리스까지의 자동화 흐름을 보여줍니다.

    1. Chart.yaml 버전 관리를 철저하게

    Chart.yaml에서 version과 appVersion을 구분하는 게 중요합니다. 처음엔 저도 이걸 같은 거라고 생각했는데, 아니더라고요.

    apiVersion: v2
    name: my-app
    description: My Application Helm Chart
    type: application
    version: 1.3.0      # Chart 자체의 버전 (Chart 구조가 바뀌면 올림)
    appVersion: "2.1.4" # 실제 애플리케이션 버전
    
    dependencies:
      - name: postgresql
        version: "12.x.x"
        repository: "https://charts.bitnami.com/bitnami"
        condition: postgresql.enabled

    SemVer(시맨틱 버저닝)를 반드시 지켜주세요. Chart 구조가 바뀌면 version을, 앱 소스만 바뀌면 appVersion만 올리는 습관을 들이면 나중에 롤백할 때 정말 편합니다.

    2. _helpers.tpl로 중복 제거하기

    템플릿 파일에서 같은 라벨, 같은 셀렉터를 매번 복붙하고 계신 분 계신가요? 저 예전에 그랬거든요. 나중에 앱 이름 하나 바꾸려고 파일을 10개 수정한 적이 있었는데, 그때 _helpers.tpl의 소중함을 알았습니다.

    # templates/_helpers.tpl
    {{/*
    공통 라벨 정의
    */}}
    {{- define "my-app.labels" -}}
    helm.sh/chart: {{ include "my-app.chart" . }}
    {{ include "my-app.selectorLabels" . }}
    {{- if .Chart.AppVersion }}
    app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
    {{- end }}
    app.kubernetes.io/managed-by: {{ .Release.Service }}
    {{- end }}
    
    {{/*
    셀렉터 라벨
    */}}
    {{- define "my-app.selectorLabels" -}}
    app.kubernetes.io/name: {{ include "my-app.name" . }}
    app.kubernetes.io/instance: {{ .Release.Name }}
    {{- end }}
    
    {{/*
    ServiceAccount 이름
    */}}
    {{- define "my-app.serviceAccountName" -}}
    {{- if .Values.serviceAccount.create }}
    {{- default (include "my-app.fullname" .) .Values.serviceAccount.name }}
    {{- else }}
    {{- default "default" .Values.serviceAccount.name }}
    {{- end }}
    {{- end }}

    3. 시크릿 관리 — 절대 Chart에 넣지 마세요

    ⚠️ 경고: 이게 진짜 중요합니다. DB 패스워드, API 키, 인증서 같은 민감한 정보를 values.yaml이나 Chart에 직접 넣으면 안 돼요. Git에 올라가는 순간 끝입니다.

    제가 추천하는 방법은 두 가지예요.

    방법 1: 외부 시크릿 참조

    # templates/deployment.yaml
    env:
      - name: DB_PASSWORD
        valueFrom:
          secretKeyRef:
            name: my-app-secrets  # 별도로 생성된 Secret
            key: db-password

    방법 2: Helm Secrets 플러그인 활용

    # helm-secrets 플러그인 설치
    helm plugin install https://github.com/jkroepke/helm-secrets
    
    # secrets.yaml을 암호화 (SOPS + AWS KMS 또는 GPG 활용)
    helm secrets encrypt secrets.yaml
    
    # 배포 시 복호화하여 사용
    helm secrets upgrade --install my-app ./my-app \
      -f values.yaml \
      -f secrets.yaml

    저는 현재 AWS Secrets Manager와 External Secrets Operator를 조합해서 쓰고 있는데, 이 조합이 가장 깔끔하더라고요. 나중에 이 주제로 별도 글 하나 써볼게요.

    4. PodDisruptionBudget과 HPA는 프로덕션 필수

    롤링 업데이트 중에 서비스가 잠깐 내려간 경험 있으신가요? 저 처음에 그거 때문에 새벽에 전화 받았거든요. PDB(PodDisruptionBudget)를 설정하면 노드 드레인이나 업그레이드 중에도 최소 파드 수를 보장해줍니다.

    # templates/pdb.yaml
    {{- if .Values.podDisruptionBudget.enabled }}
    apiVersion: policy/v1
    kind: PodDisruptionBudget
    metadata:
      name: {{ include "my-app.fullname" . }}
      labels:
        {{- include "my-app.labels" . | nindent 4 }}
    spec:
      minAvailable: {{ .Values.podDisruptionBudget.minAvailable }}
      selector:
        matchLabels:
          {{- include "my-app.selectorLabels" . | nindent 6 }}
    {{- end }}
    # templates/hpa.yaml
    {{- if .Values.autoscaling.enabled }}
    apiVersion: autoscaling/v2
    kind: HorizontalPodAutoscaler
    metadata:
      name: {{ include "my-app.fullname" . }}
      labels:
        {{- include "my-app.labels" . | nindent 4 }}
    spec:
      scaleTargetRef:
        apiVersion: apps/v1
        kind: Deployment
        name: {{ include "my-app.fullname" . }}
      minReplicas: {{ .Values.autoscaling.minReplicas }}
      maxReplicas: {{ .Values.autoscaling.maxReplicas }}
      metrics:
        - type: Resource
          resource:
            name: cpu
            target:
              type: Utilization
              averageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }}
    {{- end }}

    ⚠️ 실제로 겪었던 트러블슈팅 사례

    문제 1: helm upgrade 후 롤백이 안 되는 상황

    배포했는데 문제가 생겨서 롤백하려고 했더니 이전 릴리스 히스토리가 없는 거예요. 알고 보니 --history-max 옵션을 설정 안 해서 히스토리가 날아가 있었고, 심지어 ConfigMap이 Helm 외부에서 직접 수정돼 있어서 상태가 꼬여 있었습니다.

    # 릴리스 히스토리 최대 10개 유지 (기본값 10)
    helm upgrade --install my-app ./my-app \
      --history-max 10 \
      --atomic \
      --timeout 5m0s
    
    # 롤백 방법
    helm history my-app -n production  # 히스토리 확인
    helm rollback my-app 3 -n production  # 3번 리비전으로 롤백

    💡 팁: --atomic 플래그를 쓰면 배포 실패 시 자동으로 이전 상태로 롤백해줍니다. CI/CD에서 정말 유용하더라고요.

    문제 2: helm diff 없이 배포했다가 낭패

    values 파일을 수정하고 바로 배포했다가 예상치 못한 리소스가 변경된 적이 있었어요. 이후로는 반드시 helm-diff 플러그인을 써서 변경 사항을 먼저 확인합니다.

    # helm-diff 플러그인 설치
    helm plugin install https://github.com/databus23/helm-diff
    
    # 배포 전 변경 사항 미리 확인
    helm diff upgrade my-app ./my-app \
      -f values.yaml \
      -f values-prod.yaml \
      -n production

    문제 3: 네임스페이스 간 의존성 충돌

    서브차트를 쓰다 보면 의존성 버전이 충돌하는 경우가 있어요. helm dependency update를 주기적으로 실행하고, Chart.lock 파일을 Git에 함께 커밋하는 걸 습관화하세요.

    # 의존성 업데이트
    helm dependency update ./my-app
    
    # 의존성 목록 확인
    helm dependency list ./my-app

    배포 결과 검증하기

    ▲ Helm 릴리스 상태와 쿠버네티스 리소스 헬스를 한눈에 확인할 수 있는 모니터링 대시보드 예시입니다.

    배포가 끝났다고 끝이 아닙니다. 제대로 됐는지 확인하는 과정이 중요해요.

    # 릴리스 상태 확인
    helm status my-app -n production
    
    # 실제 렌더링된 매니페스트 확인
    helm get manifest my-app -n production
    
    # 적용된 values 확인
    helm get values my-app -n production
    
    # 전체 릴리스 목록
    helm list -A
    
    # 배포된 리소스 상태 확인
    kubectl get all -l app.kubernetes.io/instance=my-app -n production
    
    # 파드 로그 확인
    kubectl logs -l app.kubernetes.io/name=my-app -n production --tail=100

    저는 배포 후 항상 이 체크리스트를 확인합니다.

    1. 모든 파드가 Running 상태인지 확인
    2. Readiness Probe가 통과했는지 확인
    3. HPA가 올바르게 연결됐는지 확인
    4. Ingress가 정상 응답하는지 확인
    5. 에러 로그가 없는지 확인

    Helm Chart 관리 전략 정리

    ▲ 프로덕션 Helm Chart 관리를 위한 핵심 베스트 프랙티스를 한눈에 정리한 요약 가이드입니다.

    항목 나쁜 예 좋은 예
    시크릿 관리 values.yaml에 패스워드 직접 기입 External Secrets 또는 helm-secrets 활용
    환경 분리 단일 values.yaml에 모든 환경 설정 환경별 values 파일 분리 + 오버라이드
    배포 전 검증 바로 helm upgrade 실행 helm diff로 변경 사항 먼저 확인
    롤백 준비 히스토리 관리 없이 배포 –history-max 설정 + –atomic 플래그
    가용성 보장 PDB 없이 운영 PodDisruptionBudget + HPA 설정
    차트 버전 version과 appVersion 혼용 SemVer 기반으로 명확히 분리
    중복 코드 각 템플릿에 라벨 직접 기입 _helpers.tpl로 공통 템플릿 관리

    마무리: Helm은 도구, 전략은 여러분 몫

    Helm Chart 베스트 프랙티스를 정리하다 보니 꽤 길어졌네요. 핵심만 다시 짚어드리면 이렇습니다.

    • ✅ 환경별 values 파일 분리는 선택이 아닌 필수
    • ✅ 시크릿은 절대 Chart에 직접 넣지 말 것
    • ✅ --atomic + --history-max로 안전망 확보
    • ✅ helm-diff로 배포 전 반드시 변경 사항 확인
    • ✅ PDB와 HPA는 프로덕션 환경에서 필수 설정
    • ✅ _helpers.tpl로 중복 템플릿 코드 제거

    사실 이 모든 게 처음부터 완벽하게 되지는 않아요. 저도 수많은 삽질을 거쳐서 지금의 방식에 안착했거든요. 중요한 건 조금씩 개선해나가는 거예요.

    다음 글에서는 Helm과 ArgoCD를 연동한 GitOps 배포 전략을 다뤄볼 예정입니다. Helm만으로는 아쉬운 부분들을 GitOps가 어떻게 채워주는지 실제 경험 기반으로 써볼게요. 기대해 주세요!

    궁금한 점이나 다른 경험 있으시면 댓글로 편하게 남겨주세요. 같이 고민해봐요. 🎉

  • [k8s] ArgoCD 멀티 클러스터 GitOps 배포 및 관리 전략

    [k8s] ArgoCD 멀티 클러스터 GitOps 배포 및 관리 전략

    클러스터가 하나일 때는 괜찮았는데…

    처음 쿠버네티스를 도입했을 때 저도 클러스터 하나로 시작했어요. 개발(dev), 스테이징(staging), 프로덕션(production) 환경을 네임스페이스(Namespace)로 분리해서 쓰는 방식이었는데, 솔직히 그때는 그게 맞다고 생각했거든요. 근데 조직이 커지고 팀이 늘어나면서 문제가 하나씩 터지기 시작했습니다.

    “개발팀이 실수로 프로덕션 네임스페이스에 배포했어요.” 이 한 마디에 심장이 철렁 내려앉은 적 있으신가요? 저는 있습니다 ㅎㅎ. 결국 클러스터를 분리하기로 결정했고, 그때부터 ArgoCD 멀티 클러스터 전략을 본격적으로 파고들었습니다.

    이 글에서는 ArgoCD를 허브(Hub) 클러스터에 설치하고, 여러 개의 쿠버네티스 클러스터를 GitOps 방식으로 배포 및 관리하는 전략을 단계별로 정리해 드리겠습니다. 저처럼 삽질하지 않도록요.

    ArgoCD 멀티 클러스터 Hub-and-Spoke 아키텍처 다이어그램 - 허브 클러스터에서 dev, staging, prod 클러스터로 GitOps 배포 흐름

    ▲ ArgoCD 허브 클러스터가 여러 대상 클러스터(dev, staging, prod)를 관리하는 전체 아키텍처 구조. Hub-and-Spoke 패턴의 핵심입니다.

    ArgoCD 멀티 클러스터란? 개념부터 잡고 가요

    GitOps가 뭔지 먼저 짚고 가면

    GitOps는 쉽게 말해 “Git 저장소가 인프라와 애플리케이션의 단일 진실 공급원(Single Source of Truth)이 되는 운영 방식”이에요. 배포하고 싶으면 kubectl로 직접 명령을 날리는 게 아니라, Git에 커밋하면 자동으로 클러스터에 반영되는 방식이죠.

    ArgoCD는 이 GitOps를 쿠버네티스 위에서 구현해주는 대표적인 오픈소스 도구입니다. Git 저장소를 지속적으로 감시하다가 변경이 감지되면 클러스터에 자동으로 동기화(Sync)해줘요.

    멀티 클러스터 관리, 왜 필요한가요?

    네임스페이스 분리 방식의 가장 큰 문제는 폭발 반경(Blast Radius)이 너무 넓다는 거예요. 클러스터 레벨의 장애가 모든 환경에 영향을 주고, 보안 격리도 완벽하지 않습니다. 반면 클러스터를 분리하면:

    • 환경 간 완전한 격리 — 개발 배포 실수가 프로덕션에 영향 없음
    • 클러스터별 독립적인 리소스 할당 및 스케일링
    • 규정 준수(Compliance) 요구사항 충족 용이
    • 팀별 독립적인 업그레이드 주기 관리

    그런데 클러스터가 여러 개가 되면 관리 포인트도 여러 개가 되잖아요. 각 클러스터에 ArgoCD를 따로 설치하는 방법도 있지만, 저는 Hub-and-Spoke 패턴을 선호합니다. 허브 클러스터 하나에 ArgoCD를 설치하고, 나머지 클러스터들을 원격으로 관리하는 방식이에요.

    방식 장점 단점 추천 상황
    클러스터별 ArgoCD 설치 독립성 높음, 장애 격리 관리 포인트 분산, 일관성 유지 어려움 팀/조직이 완전히 분리된 경우
    Hub-and-Spoke (중앙화) 단일 관리 포인트, 일관된 정책 적용 허브 클러스터 장애 시 배포 불가 중앙 플랫폼팀이 관리하는 경우

    ArgoCD 설치 및 멀티 클러스터 등록 — 실전으로 들어갑니다

    1단계: 허브 클러스터에 ArgoCD 설치

    저는 허브 클러스터로 별도의 관리 전용 클러스터를 사용합니다. 프로덕션 워크로드가 없는 클러스터에 ArgoCD를 올리는 게 안전하더라고요.

    # ArgoCD 네임스페이스 생성
    kubectl create namespace argocd
    
    # ArgoCD 공식 매니페스트로 설치
    kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
    
    # 설치 확인
    kubectl get pods -n argocd
    

    설치가 완료되면 argocd-server 파드가 Running 상태가 되는데, 처음엔 이미지 풀(Image Pull) 때문에 좀 기다려야 할 수도 있어요. 저도 처음에 “왜 안 되지?” 하고 5분 기다렸더니 그냥 올라왔습니다 ㅎㅎ.

    # 초기 admin 비밀번호 확인
    kubectl -n argocd get secret argocd-initial-admin-secret \
      -o jsonpath="{.data.password}" | base64 -d
    
    # ArgoCD CLI 설치 (macOS 기준)
    brew install argocd
    
    # ArgoCD 서버 포트포워딩 (로컬 접근용)
    kubectl port-forward svc/argocd-server -n argocd 8080:443
    
    # CLI 로그인
    argocd login localhost:8080 --username admin --password <위에서_확인한_비밀번호> --insecure
    

    2단계: 대상 클러스터(Spoke) 등록

    이 부분이 멀티 클러스터 설정의 핵심이에요. ArgoCD CLI로 대상 클러스터를 등록하면, ArgoCD가 해당 클러스터에 argocd-manager라는 서비스 어카운트(Service Account)를 만들고 필요한 RBAC 권한을 자동으로 설정해줍니다.

    # 현재 kubeconfig에 등록된 컨텍스트 확인
    kubectl config get-contexts
    
    # 예시 출력:
    # CURRENT   NAME            CLUSTER         AUTHINFO
    # *         hub-cluster     hub-cluster     hub-admin
    #           dev-cluster     dev-cluster     dev-admin
    #           staging-cluster staging-cluster staging-admin
    #           prod-cluster    prod-cluster    prod-admin
    
    # 개발 클러스터 등록
    argocd cluster add dev-cluster --name dev
    
    # 스테이징 클러스터 등록
    argocd cluster add staging-cluster --name staging
    
    # 프로덕션 클러스터 등록
    argocd cluster add prod-cluster --name prod
    
    # 등록된 클러스터 목록 확인
    argocd cluster list
    

    💡 팁: 클러스터 등록 시 --name 옵션으로 별칭을 지정하면 나중에 Application 설정에서 훨씬 읽기 편합니다. URL 대신 이름으로 참조할 수 있거든요.

    ArgoCD Settings Clusters 화면에서 dev, staging, prod 멀티 클러스터가 등록된 관리 UI 화면

    ▲ ArgoCD 웹 UI의 Settings > Clusters 화면. dev, staging, prod 클러스터가 각각 등록되어 연결 상태를 실시간으로 확인할 수 있습니다.

    Git 저장소 구조 설계 — 이게 진짜 중요합니다

    멀티 클러스터 GitOps에서 Git 저장소 구조를 어떻게 잡느냐가 나중에 관리 편의성을 크게 좌우해요. 제가 여러 방식을 시도해보고 정착한 구조를 공유합니다.

    모노레포(Monorepo) 방식 — 제가 선호하는 방식

    gitops-repo/
    ├── apps/                          # 애플리케이션 정의
    │   ├── base/                      # 공통 기본 설정 (Kustomize base)
    │   │   ├── my-app/
    │   │   │   ├── deployment.yaml
    │   │   │   ├── service.yaml
    │   │   │   └── kustomization.yaml
    │   └── overlays/                  # 환경별 오버레이
    │       ├── dev/
    │       │   └── my-app/
    │       │       ├── kustomization.yaml
    │       │       └── patch-replicas.yaml
    │       ├── staging/
    │       │   └── my-app/
    │       └── prod/
    │           └── my-app/
    ├── argocd/                        # ArgoCD 설정 자체도 Git으로 관리
    │   ├── projects/                  # ArgoCD Project 정의
    │   │   ├── dev-project.yaml
    │   │   ├── staging-project.yaml
    │   │   └── prod-project.yaml
    │   └── applications/              # ArgoCD Application 정의
    │       ├── dev/
    │       ├── staging/
    │       └── prod/
    └── clusters/                      # 클러스터 레벨 설정
        ├── dev/
        ├── staging/
        └── prod/
    

    이 구조의 핵심은 Kustomize(커스터마이즈)를 활용해서 base 설정을 공유하면서 환경별로 다른 값(레플리카 수, 리소스 제한, 이미지 태그 등)만 오버레이로 덮어쓰는 방식이에요.

    Kustomize 오버레이 예시

    # apps/base/my-app/deployment.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: my-app
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: my-app
      template:
        metadata:
          labels:
            app: my-app
        spec:
          containers:
          - name: my-app
            image: my-registry/my-app:latest
            resources:
              requests:
                cpu: 100m
                memory: 128Mi
    
    # apps/overlays/prod/my-app/kustomization.yaml
    apiVersion: kustomize.config.k8s.io/v1beta1
    kind: Kustomization
    resources:
      - ../../../base/my-app
    patches:
      - path: patch-replicas.yaml
    images:
      - name: my-registry/my-app
        newTag: v1.2.3  # 프로덕션은 명시적 태그 사용
    
    # apps/overlays/prod/my-app/patch-replicas.yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: my-app
    spec:
      replicas: 3  # 프로덕션은 3개로
    

    ArgoCD Application 및 AppProject 설정

    AppProject(앱 프로젝트)로 클러스터 접근 제어

    ArgoCD의 AppProject는 애플리케이션들을 논리적으로 그룹화하고, 어떤 Git 저장소에서 어떤 클러스터로 배포할 수 있는지 제한하는 역할을 합니다. 저는 이걸 환경별로 나눠서 씁니다.

    # argocd/projects/prod-project.yaml
    apiVersion: argoproj.io/v1alpha1
    kind: AppProject
    metadata:
      name: production
      namespace: argocd
    spec:
      description: Production environment project
      # 허용된 소스 저장소
      sourceRepos:
        - 'https://github.com/myorg/gitops-repo.git'
      # 배포 가능한 대상 클러스터와 네임스페이스
      destinations:
        - server: https://prod-cluster-api.example.com
          namespace: '*'
      # 클러스터 범위 리소스 생성 제한 (선택적)
      clusterResourceWhitelist:
        - group: ''
          kind: Namespace
      # RBAC 역할 정의
      roles:
        - name: prod-deployer
          description: Production deployment role
          policies:
            - p, proj:production:prod-deployer, applications, sync, production/*, allow
          groups:
            - myorg:platform-team
    

    Application 정의 — 멀티 클러스터 배포의 핵심

    # argocd/applications/prod/my-app.yaml
    apiVersion: argoproj.io/v1alpha1
    kind: Application
    metadata:
      name: my-app-prod
      namespace: argocd
      # 앱 삭제 시 리소스도 함께 삭제되지 않도록 (중요!)
      finalizers:
        - resources-finalizer.argocd.argoproj.io
    spec:
      project: production
      source:
        repoURL: https://github.com/myorg/gitops-repo.git
        targetRevision: main
        path: apps/overlays/prod/my-app  # 프로덕션 오버레이 경로
      destination:
        # 등록한 클러스터 이름 또는 API 서버 URL
        server: https://prod-cluster-api.example.com
        namespace: my-app
      syncPolicy:
        automated:
          prune: true       # Git에서 삭제된 리소스는 클러스터에서도 삭제
          selfHeal: true    # 클러스터 상태가 Git과 다르면 자동 복구
        syncOptions:
          - CreateNamespace=true  # 네임스페이스 없으면 자동 생성
          - PrunePropagationPolicy=foreground
        retry:
          limit: 5
          backoff:
            duration: 5s
            factor: 2
            maxDuration: 3m
    

    ApplicationSet으로 여러 클러스터에 한 번에 배포

    근데 여기서 더 편한 방법이 있어요. ApplicationSet(애플리케이션셋)을 쓰면 여러 클러스터에 대한 Application을 템플릿 하나로 자동 생성할 수 있거든요. 처음 이걸 알았을 때 “이게 왜 이렇게 편하지?” 싶었습니다.

    # argocd/applicationsets/my-app-all-clusters.yaml
    apiVersion: argoproj.io/v1alpha1
    kind: ApplicationSet
    metadata:
      name: my-app-all-clusters
      namespace: argocd
    spec:
      generators:
        - list:
            elements:
              - cluster: dev
                url: https://dev-cluster-api.example.com
                env: dev
                revision: HEAD
              - cluster: staging
                url: https://staging-cluster-api.example.com
                env: staging
                revision: main
              - cluster: prod
                url: https://prod-cluster-api.example.com
                env: prod
                revision: main
      template:
        metadata:
          name: 'my-app-{{env}}'
        spec:
          project: '{{env}}'
          source:
            repoURL: https://github.com/myorg/gitops-repo.git
            targetRevision: '{{revision}}'
            path: 'apps/overlays/{{env}}/my-app'
          destination:
            server: '{{url}}'
            namespace: my-app
          syncPolicy:
            automated:
              prune: true
              selfHeal: true
            syncOptions:
              - CreateNamespace=true
    

    💡 팁: ApplicationSet의 generators에는 List 방식 외에도 클러스터 레이블 기반으로 자동 감지하는 Cluster Generator도 있어요. 클러스터가 많아질수록 이쪽이 훨씬 강력합니다.

    ⚠️ 실제로 겪은 트러블슈팅 — 이거 꼭 읽으세요

    문제 1: 클러스터 등록 후 연결 끊김 (Connection Refused)

    클러스터를 등록했는데 ArgoCD UI에서 계속 “Unknown” 상태가 뜨는 경우가 있었어요. 원인을 파고들어보니 허브 클러스터에서 대상 클러스터의 API 서버로 직접 네트워크 연결이 안 되는 거였습니다. VPC 피어링이나 방화벽 규칙을 확인해야 해요.

    # 허브 클러스터에서 대상 클러스터 API 서버 연결 테스트
    kubectl run curl-test --image=curlimages/curl --rm -it --restart=Never -- \
      curl -k https://prod-cluster-api.example.com/healthz
    

    문제 2: prune 옵션으로 인한 의도치 않은 리소스 삭제

    이건 진짜 아찔했던 경험인데요. automated.prune: true를 켜놨는데, 실수로 Git에서 파일을 지웠다가 프로덕션 Deployment가 통째로 삭제된 적이 있었어요. 다행히 빠르게 복구했지만…

    이후로 저는 프로덕션 환경에는 Sync Windows(동기화 창)를 설정해서 업무 시간 외에는 자동 동기화가 안 되도록 했습니다.

    # AppProject에 Sync Window 추가
    spec:
      syncWindows:
        - kind: allow
          schedule: '0 9 * * 1-5'  # 평일 오전 9시에만
          duration: 8h
          applications:
            - '*'
          manualSync: true  # 수동 동기화는 항상 허용
    

    문제 3: Helm 차트 버전 충돌

    여러 클러스터에 같은 Helm 차트를 다른 버전으로 배포하다 보면 values 파일 구조가 버전마다 달라서 오류가 나는 경우가 있어요. 저는 이걸 해결하기 위해 클러스터별 values 파일을 명확하게 분리해서 관리하고 있습니다.

    # Helm 소스 예시 - 클러스터별 values 파일 분리
    source:
      repoURL: https://charts.example.com
      chart: my-chart
      targetRevision: 1.2.3
      helm:
        valueFiles:
          - values.yaml
          - values-prod.yaml  # 환경별 values 파일
    
    ArgoCD Applications 대시보드에서 멀티 클러스터 배포된 애플리케이션들이 모두 Synced Healthy 상태로 표시된 결과 화면

    ▲ ArgoCD 웹 UI의 Applications 화면. dev, staging, prod 클러스터에 배포된 my-app들이 모두 Synced/Healthy 상태로 표시되는 모습입니다.

    ✅ 배포 검증 및 운영 팁

    헬스 체크와 동기화 상태 모니터링

    # 전체 Application 상태 확인
    argocd app list
    
    # 특정 앱 상세 상태 확인
    argocd app get my-app-prod
    
    # 수동으로 동기화 실행
    argocd app sync my-app-prod
    
    # 동기화 상태 및 히스토리 확인
    argocd app history my-app-prod
    
    # 롤백 (이전 버전으로)
    argocd app rollback my-app-prod 
    

    Notification(알림) 설정으로 배포 상태 파악

    ArgoCD에는 argocd-notifications라는 컴포넌트가 있어서 Slack, 이메일, PagerDuty 등으로 배포 상태를 알림 받을 수 있어요. 저는 Slack 채널에 연동해서 배포 성공/실패를 실시간으로 받고 있는데, 이거 없으면 이제 못 살 것 같습니다 ㅎㅎ.

    RBAC으로 팀별 접근 제어

    # argocd-rbac-cm ConfigMap 예시
    apiVersion: v1
    kind: ConfigMap
    metadata:
      name: argocd-rbac-cm
      namespace: argocd
    data:
      policy.csv: |
        # 개발팀: dev 프로젝트만 접근 가능
        p, role:dev-team, applications, *, dev/*, allow
        p, role:dev-team, applications, sync, dev/*, allow
        
        # 플랫폼팀: 전체 접근 가능
        p, role:platform-team, applications, *, */*, allow
        p, role:platform-team, clusters, *, *, allow
        
        g, myorg:dev-team, role:dev-team
        g, myorg:platform-team, role:platform-team
      policy.default: role:readonly
    

    전략 정리 — 이것만 기억하세요

    ArgoCD 멀티 클러스터 GitOps 전략 핵심 구성요소 요약 인포그래픽 - AppProject, ApplicationSet, Kustomize, RBAC 관계도

    ▲ ArgoCD 멀티 클러스터 GitOps 전략의 핵심 요소를 한눈에 정리한 요약 다이어그램. Git 저장소 구조, ArgoCD 컴포넌트, 클러스터 관계를 보여줍니다.

    구성 요소 역할 핵심 포인트
    AppProject 배포 범위 및 권한 제한 환경별로 분리, Sync Window 설정
    Application 개별 앱 배포 정의 destination.server로 대상 클러스터 지정
    ApplicationSet 다중 클러스터 배포 자동화 템플릿 하나로 여러 클러스터에 배포
    Kustomize Overlay 환경별 설정 분리 base 공유, 환경별 차이만 patch
    RBAC 팀별 접근 제어 AppProject role + argocd-rbac-cm

    마무리하며 — 그래서 뭐가 달라졌냐면

    ArgoCD 멀티 클러스터 구성을 제대로 잡고 나서 가장 크게 달라진 건, 배포에 대한 불안감이 사라졌다는 거예요. 예전엔 “내가 지금 어떤 클러스터에 붙어있지?”를 항상 확인해야 했는데, 이제는 Git에 PR 올리고 머지하면 끝이거든요.

    물론 처음 설계할 때 Git 저장소 구조를 잘 잡는 게 제일 중요합니다. 나중에 구조를 바꾸려면 진짜 손이 많이 가거든요. 저도 한 번 갈아엎었습니다… ㅎㅎ 이 글을 보시는 분들은 처음부터 overlays 구조로 시작하시길 강력 추천합니다.

    다음 글에서는 ArgoCD Image Updater를 활용해서 컨테이너 이미지 태그 업데이트까지 완전 자동화하는 방법을 다뤄볼 예정이에요. CI 파이프라인에서 이미지 빌드되면 ArgoCD가 자동으로 Git 커밋하고 배포까지 이어지는 풀 사이클인데, 이거 진짜 편합니다.

    혹시 멀티 클러스터 구성하면서 막히는 부분 있으시면 댓글로 남겨주세요. 제가 겪어본 삽질이라면 같이 해결해 드릴 수 있을 것 같습니다 🎉

    자주 묻는 질문 (FAQ)

    Q. ArgoCD 자체가 설치된 허브 클러스터가 다운되면 어떻게 되나요?

    A. 허브 클러스터가 다운되면 새로운 배포는 불가능하지만, 이미 배포된 애플리케이션들은 각 클러스터에서 계속 정상 동작합니다. 이 때문에 허브 클러스터는 고가용성(HA) 구성으로 운영하는 것을 권장합니다.

    Q. ApplicationSet과 Application을 언제 선택해야 하나요?

    A. 동일한 앱을 여러 클러스터에 배포해야 한다면 ApplicationSet이 훨씬 효율적입니다. 클러스터마다 배포 설정이 크게 다르거나 배포 시점을 완전히 독립적으로 관리해야 한다면 개별 Application을 사용하는 게 맞습니다.

    Q. Helm과 Kustomize 중 어떤 걸 쓰는 게 좋나요?

    A. 서드파티 차트를 그대로 쓸 때는 Helm, 자체 매니페스트를 환경별로 관리할 때는 Kustomize가 더 직관적입니다. 둘을 혼합해서 쓰는 것도 가능하고, 저는 실제로 두 방식을 같이 씁니다.