Split View: ArgoCD GitOps 완벽 가이드: ApplicationSet·Sync Waves·Hook으로 구현하는 Kubernetes 선언적 배포
ArgoCD GitOps 완벽 가이드: ApplicationSet·Sync Waves·Hook으로 구현하는 Kubernetes 선언적 배포
- 들어가며
- ArgoCD 아키텍처 개요
- Application 리소스 기본 구성
- AppProject로 RBAC 구현
- ApplicationSet 제너레이터
- Sync Waves를 통한 배포 순서 제어
- Sync Hook 활용
- 시크릿 관리
- 멀티 클러스터 배포
- 롤백 전략
- 모니터링 구성
- 운영 시 주의사항
- 장애 사례 및 복구 절차
- 마치며
- 참고자료

들어가며
GitOps는 Git을 Single Source of Truth로 사용하여 인프라와 애플리케이션의 원하는 상태를 선언적으로 관리하는 운영 패러다임입니다. ArgoCD는 Kubernetes 환경에서 GitOps를 구현하는 가장 널리 사용되는 도구로, CNCF Graduated 프로젝트로서 프로덕션 검증이 완료되었습니다.
이 글에서는 ArgoCD의 핵심 기능인 ApplicationSet을 통한 멀티 클러스터/환경 배포, Sync Waves와 Hook을 통한 배포 순서 제어, RBAC과 시크릿 관리까지 프로덕션 환경에서 필요한 모든 설정을 실전 코드와 함께 다룹니다.
ArgoCD 아키텍처 개요
ArgoCD는 다음과 같은 핵심 컴포넌트로 구성됩니다.
| 컴포넌트 | 역할 | 주요 기능 |
|---|---|---|
| API Server | 웹 UI, CLI, CI/CD 연동 | gRPC/REST API 제공, RBAC 처리 |
| Repo Server | Git 저장소 관리 | 매니페스트 생성(Helm, Kustomize, Plain YAML) |
| Application Controller | 핵심 조정 루프 | 클러스터 상태 감시, 동기화 실행 |
| ApplicationSet Controller | 다중 Application 생성 | 템플릿 기반 자동 Application 생성 |
| Redis | 캐시 | 매니페스트 캐싱, 클러스터 상태 캐싱 |
| Dex | 인증 | SSO, OIDC, LDAP 등 외부 인증 연동 |
ArgoCD 설치
# Namespace 생성 및 ArgoCD 설치
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# 또는 Helm으로 설치 (프로덕션 권장)
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update
helm install argocd argo/argo-cd \
--namespace argocd \
--create-namespace \
--set server.service.type=LoadBalancer \
--set configs.params."server\.insecure"=true \
--set controller.replicas=2 \
--set repoServer.replicas=2 \
--set redis-ha.enabled=true
# 초기 admin 비밀번호 확인
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath='{.data.password}' | base64 -d
Application 리소스 기본 구성
Application 스펙 상세
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-application
namespace: argocd
# Finalizer: Application 삭제 시 클러스터 리소스도 함께 삭제
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: production
source:
repoURL: https://github.com/myorg/k8s-manifests.git
targetRevision: main
path: apps/my-app/overlays/production
# Kustomize 옵션
kustomize:
namePrefix: prod-
commonLabels:
env: production
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true # Git에서 삭제된 리소스를 클러스터에서도 삭제
selfHeal: true # 수동 변경 시 자동 복구
allowEmpty: false # 빈 매니페스트 배포 방지
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
- PruneLast=true
- ServerSideApply=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas # HPA 관리 대상이므로 무시
Helm 소스 설정
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: prometheus-stack
namespace: argocd
spec:
project: monitoring
source:
repoURL: https://prometheus-community.github.io/helm-charts
chart: kube-prometheus-stack
targetRevision: 65.1.0
helm:
releaseName: prometheus
valuesObject:
grafana:
enabled: true
adminPassword: vault:secret/grafana#password
prometheus:
prometheusSpec:
retention: 30d
storageSpec:
volumeClaimTemplate:
spec:
storageClassName: gp3
resources:
requests:
storage: 100Gi
destination:
server: https://kubernetes.default.svc
namespace: monitoring
AppProject로 RBAC 구현
프로젝트 정의
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: production
namespace: argocd
spec:
description: 'Production environment project'
# 허용된 소스 저장소
sourceRepos:
- 'https://github.com/myorg/k8s-manifests.git'
- 'https://github.com/myorg/helm-charts.git'
# 허용된 배포 대상
destinations:
- namespace: 'production'
server: 'https://kubernetes.default.svc'
- namespace: 'production-*'
server: 'https://kubernetes.default.svc'
# 허용된 클러스터 리소스
clusterResourceWhitelist:
- group: ''
kind: Namespace
- group: 'rbac.authorization.k8s.io'
kind: ClusterRole
- group: 'rbac.authorization.k8s.io'
kind: ClusterRoleBinding
# 차단된 네임스페이스 리소스
namespaceResourceBlacklist:
- group: ''
kind: ResourceQuota
- group: ''
kind: LimitRange
# RBAC 역할 정의
roles:
- name: deployer
description: 'Can sync and manage applications'
policies:
- p, proj:production:deployer, applications, get, production/*, allow
- p, proj:production:deployer, applications, sync, production/*, allow
- p, proj:production:deployer, applications, action/*, production/*, allow
groups:
- platform-team
- name: viewer
description: 'Read-only access'
policies:
- p, proj:production:viewer, applications, get, production/*, allow
groups:
- dev-team
ApplicationSet 제너레이터
Git Directory 제너레이터
Git 저장소의 디렉토리 구조를 기반으로 Application을 자동 생성합니다.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: app-of-apps
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ['missingkey=error']
generators:
- git:
repoURL: https://github.com/myorg/k8s-manifests.git
revision: main
directories:
- path: 'apps/*/overlays/production'
- path: 'apps/deprecated-*'
exclude: true
template:
metadata:
name: '{{.path.basename}}'
namespace: argocd
labels:
app.kubernetes.io/managed-by: applicationset
spec:
project: production
source:
repoURL: https://github.com/myorg/k8s-manifests.git
targetRevision: main
path: '{{.path.path}}'
destination:
server: https://kubernetes.default.svc
namespace: '{{.path.basename}}'
syncPolicy:
automated:
prune: true
selfHeal: true
Cluster 제너레이터
등록된 클러스터를 기반으로 멀티 클러스터 배포를 자동화합니다.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: multi-cluster-apps
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ['missingkey=error']
generators:
- clusters:
selector:
matchLabels:
env: production
matchExpressions:
- key: region
operator: In
values:
- ap-northeast-2
- us-west-2
- eu-west-1
template:
metadata:
name: 'app-{{.name}}'
namespace: argocd
spec:
project: production
source:
repoURL: https://github.com/myorg/k8s-manifests.git
targetRevision: main
path: 'apps/my-app/overlays/{{.metadata.labels.env}}'
kustomize:
commonLabels:
cluster: '{{.name}}'
region: '{{.metadata.labels.region}}'
destination:
server: '{{.server}}'
namespace: production
Matrix 제너레이터 (복합 조합)
두 제너레이터를 조합하여 모든 조합을 생성합니다.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: matrix-deployment
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ['missingkey=error']
generators:
- matrix:
generators:
# 제너레이터 1: 클러스터 목록
- clusters:
selector:
matchLabels:
env: production
# 제너레이터 2: Git 디렉토리에서 앱 목록
- git:
repoURL: https://github.com/myorg/k8s-manifests.git
revision: main
directories:
- path: 'apps/*'
template:
metadata:
name: '{{.path.basename}}-{{.name}}'
namespace: argocd
spec:
project: production
source:
repoURL: https://github.com/myorg/k8s-manifests.git
targetRevision: main
path: '{{.path.path}}/overlays/production'
destination:
server: '{{.server}}'
namespace: '{{.path.basename}}'
List 제너레이터
정적 값 목록을 기반으로 Application을 생성합니다.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: environment-apps
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ['missingkey=error']
generators:
- list:
elements:
- env: dev
cluster: https://dev-k8s.example.com
revision: develop
replicas: '1'
- env: staging
cluster: https://staging-k8s.example.com
revision: release/v2.5
replicas: '2'
- env: production
cluster: https://kubernetes.default.svc
revision: main
replicas: '3'
template:
metadata:
name: 'my-app-{{.env}}'
namespace: argocd
spec:
project: '{{.env}}'
source:
repoURL: https://github.com/myorg/k8s-manifests.git
targetRevision: '{{.revision}}'
path: 'apps/my-app/overlays/{{.env}}'
destination:
server: '{{.cluster}}'
namespace: 'my-app-{{.env}}'
Sync Waves를 통한 배포 순서 제어
Sync Wave 기본 개념
Sync Wave는 argocd.argoproj.io/sync-wave 어노테이션으로 정의하며, 정수 값을 사용하여 낮은 값부터 순차적으로 배포합니다. 기본값은 0입니다.
# Wave -2: 네임스페이스와 RBAC (전제 조건)
apiVersion: v1
kind: Namespace
metadata:
name: production
annotations:
argocd.argoproj.io/sync-wave: '-2'
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-service-account
namespace: production
annotations:
argocd.argoproj.io/sync-wave: '-2'
---
# Wave -1: ConfigMap과 Secret (설정)
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: production
annotations:
argocd.argoproj.io/sync-wave: '-1'
data:
APP_ENV: production
LOG_LEVEL: info
---
# Wave 0: 핵심 애플리케이션 (기본값)
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: production
annotations:
argocd.argoproj.io/sync-wave: '0'
spec:
replicas: 3
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
serviceAccountName: app-service-account
containers:
- name: app
image: myorg/my-app:v2.5.0
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: app-config
---
apiVersion: v1
kind: Service
metadata:
name: my-app
namespace: production
annotations:
argocd.argoproj.io/sync-wave: '0'
spec:
selector:
app: my-app
ports:
- port: 8080
targetPort: 8080
---
# Wave 1: 의존 리소스 (Ingress, HPA)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-app-hpa
namespace: production
annotations:
argocd.argoproj.io/sync-wave: '1'
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
---
# Wave 2: 모니터링
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: my-app-monitor
namespace: production
annotations:
argocd.argoproj.io/sync-wave: '2'
spec:
selector:
matchLabels:
app: my-app
endpoints:
- port: http
interval: 15s
Sync Hook 활용
PreSync Hook: 배포 전 데이터베이스 마이그레이션
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
namespace: production
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
argocd.argoproj.io/sync-wave: '-1'
spec:
backoffLimit: 3
template:
spec:
containers:
- name: migrate
image: myorg/db-migrator:v2.5.0
command: ['./migrate', 'up']
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url
restartPolicy: Never
PostSync Hook: 배포 후 검증
apiVersion: batch/v1
kind: Job
metadata:
name: smoke-test
namespace: production
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
backoffLimit: 1
template:
spec:
containers:
- name: smoke-test
image: myorg/smoke-tester:latest
command:
- /bin/sh
- -c
- |
echo "Running smoke tests..."
curl -sf http://my-app.production.svc:8080/healthz || exit 1
curl -sf http://my-app.production.svc:8080/api/v1/status || exit 1
echo "All smoke tests passed!"
restartPolicy: Never
SyncFail Hook: 동기화 실패 시 알림
apiVersion: batch/v1
kind: Job
metadata:
name: sync-fail-notification
namespace: production
annotations:
argocd.argoproj.io/hook: SyncFail
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
backoffLimit: 1
template:
spec:
containers:
- name: notify
image: curlimages/curl:latest
command:
- /bin/sh
- -c
- |
curl -X POST "$SLACK_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d '{"text":"ArgoCD Sync FAILED for my-app in production!"}'
env:
- name: SLACK_WEBHOOK_URL
valueFrom:
secretKeyRef:
name: slack-webhook
key: url
restartPolicy: Never
시크릿 관리
Sealed Secrets 연동
# Sealed Secrets 컨트롤러 설치
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm install sealed-secrets sealed-secrets/sealed-secrets \
--namespace kube-system
# kubeseal CLI로 시크릿 암호화
kubectl create secret generic db-credentials \
--namespace production \
--from-literal=url='postgresql://user:pass@db:5432/mydb' \
--dry-run=client -o yaml | \
kubeseal --format yaml > sealed-db-credentials.yaml
# Git에 커밋 가능한 SealedSecret
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: db-credentials
namespace: production
annotations:
argocd.argoproj.io/sync-wave: '-2'
spec:
encryptedData:
url: AgA2X5N0Q...encrypted...base64==
template:
metadata:
name: db-credentials
namespace: production
type: Opaque
External Secrets Operator 연동
# ExternalSecret: AWS Secrets Manager에서 시크릿 동기화
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: production
annotations:
argocd.argoproj.io/sync-wave: '-2'
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secretsmanager
kind: ClusterSecretStore
target:
name: db-credentials
creationPolicy: Owner
data:
- secretKey: url
remoteRef:
key: production/database
property: connection_url
- secretKey: password
remoteRef:
key: production/database
property: password
---
# ClusterSecretStore 정의
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: aws-secretsmanager
spec:
provider:
aws:
service: SecretsManager
region: ap-northeast-2
auth:
jwt:
serviceAccountRef:
name: external-secrets-sa
namespace: external-secrets
멀티 클러스터 배포
클러스터 등록
# 대상 클러스터 등록 (kubeconfig 컨텍스트 기반)
argocd cluster add staging-cluster \
--name staging \
--label env=staging \
--label region=ap-northeast-2
argocd cluster add production-cluster \
--name production \
--label env=production \
--label region=ap-northeast-2
# 등록된 클러스터 확인
argocd cluster list
클러스터별 설정 분리
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: platform-services
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ['missingkey=error']
generators:
- matrix:
generators:
- clusters:
selector:
matchLabels:
env: production
- list:
elements:
- app: cert-manager
namespace: cert-manager
chart: cert-manager
repoURL: https://charts.jetstack.io
targetRevision: v1.16.0
- app: external-secrets
namespace: external-secrets
chart: external-secrets
repoURL: https://charts.external-secrets.io
targetRevision: 0.12.0
- app: metrics-server
namespace: kube-system
chart: metrics-server
repoURL: https://kubernetes-sigs.github.io/metrics-server
targetRevision: 3.12.0
template:
metadata:
name: '{{.app}}-{{.name}}'
namespace: argocd
spec:
project: platform
source:
repoURL: '{{.repoURL}}'
chart: '{{.chart}}'
targetRevision: '{{.targetRevision}}'
helm:
releaseName: '{{.app}}'
destination:
server: '{{.server}}'
namespace: '{{.namespace}}'
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
롤백 전략
자동 롤백 설정
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argocd
spec:
project: production
source:
repoURL: https://github.com/myorg/k8s-manifests.git
targetRevision: main
path: apps/my-app/overlays/production
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
retry:
limit: 3
backoff:
duration: 10s
factor: 2
maxDuration: 5m
CLI를 통한 수동 롤백
# 배포 히스토리 확인
argocd app history my-app
# 특정 리비전으로 롤백
argocd app rollback my-app 5
# 또는 특정 Git 커밋으로 동기화
argocd app sync my-app --revision abc123def
# 동기화 상태 확인
argocd app get my-app
argocd app wait my-app --health
모니터링 구성
ArgoCD Prometheus 메트릭
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: argocd-metrics
namespace: argocd
spec:
selector:
matchLabels:
app.kubernetes.io/part-of: argocd
endpoints:
- port: metrics
interval: 30s
---
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: argocd-alerts
namespace: argocd
spec:
groups:
- name: argocd
rules:
- alert: ArgoCDAppOutOfSync
expr: |
argocd_app_info{sync_status="OutOfSync"} == 1
for: 10m
labels:
severity: warning
annotations:
summary: 'Application {{ "{{" }} $labels.name {{ "}}" }} is OutOfSync for more than 10 minutes'
- alert: ArgoCDAppDegraded
expr: |
argocd_app_info{health_status="Degraded"} == 1
for: 5m
labels:
severity: critical
annotations:
summary: 'Application {{ "{{" }} $labels.name {{ "}}" }} is Degraded'
- alert: ArgoCDSyncFailed
expr: |
increase(argocd_app_sync_total{phase="Failed"}[10m]) > 0
labels:
severity: critical
annotations:
summary: 'ArgoCD sync failed for {{ "{{" }} $labels.name {{ "}}" }}'
Slack 알림 설정
# argocd-notifications-cm ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-notifications-cm
namespace: argocd
data:
service.slack: |
token: $slack-token
trigger.on-sync-failed: |
- when: app.status.operationState.phase in ['Error', 'Failed']
send: [app-sync-failed]
trigger.on-health-degraded: |
- when: app.status.health.status == 'Degraded'
send: [app-health-degraded]
trigger.on-sync-succeeded: |
- when: app.status.operationState.phase in ['Succeeded']
oncePer: app.status.sync.revision
send: [app-sync-succeeded]
template.app-sync-failed: |
slack:
attachments: |
[{
"color": "#E96D76",
"title": "Sync Failed: {{.app.metadata.name}}",
"text": "Application {{.app.metadata.name}} sync failed.\nRevision: {{.app.status.sync.revision}}\nMessage: {{.app.status.operationState.message}}"
}]
template.app-health-degraded: |
slack:
attachments: |
[{
"color": "#f4c030",
"title": "Health Degraded: {{.app.metadata.name}}",
"text": "Application {{.app.metadata.name}} health is degraded."
}]
template.app-sync-succeeded: |
slack:
attachments: |
[{
"color": "#18BE52",
"title": "Sync Succeeded: {{.app.metadata.name}}",
"text": "Application {{.app.metadata.name}} synced successfully.\nRevision: {{.app.status.sync.revision}}"
}]
운영 시 주의사항
1. ApplicationSet 보안
ApplicationSet의 project 필드가 템플릿화된 경우, 개발자가 과도한 권한을 가진 프로젝트에 Application을 생성할 수 있습니다. 항상 관리자가 제어하는 소스에서만 프로젝트 필드를 참조하도록 설정해야 합니다.
2. 자동 동기화 주의점
automated.prune: true는 Git에서 제거된 리소스를 클러스터에서 삭제합니다. 실수로 매니페스트를 삭제하면 프로덕션 리소스가 즉시 삭제될 수 있으므로, 중요 리소스에는 argocd.argoproj.io/sync-options: Prune=false 어노테이션을 추가하는 것을 권장합니다.
3. Webhook 보안
ArgoCD가 공개적으로 접근 가능한 경우, 반드시 Webhook 시크릿을 설정하여 DDoS 공격을 방지해야 합니다.
4. Repo Server 리소스
대규모 저장소나 Helm 차트를 처리할 때 Repo Server의 메모리 사용량이 급증할 수 있습니다. 적절한 리소스 제한과 레플리카 수를 설정해야 합니다.
5. 추상화 계층 제한
Application of Applications 패턴 사용 시 3단계 이상의 추상화를 피해야 합니다. 4-5단계의 중첩은 디버깅을 극도로 어렵게 만듭니다.
장애 사례 및 복구 절차
사례 1: Sync 무한 루프
# 증상: Application이 계속 Syncing 상태를 반복
# 원인: ignoreDifferences 미설정으로 인한 drift 감지
# 진단
argocd app diff my-app
argocd app get my-app --show-operation
# 복구: ignoreDifferences 추가
kubectl patch application my-app -n argocd --type merge -p '{
"spec": {
"ignoreDifferences": [
{
"group": "apps",
"kind": "Deployment",
"jsonPointers": ["/spec/replicas"]
}
]
}
}'
사례 2: Repo Server OOM
# 증상: Application이 Unknown 상태, 매니페스트 생성 실패
# 진단
kubectl logs -n argocd deploy/argocd-repo-server --previous
kubectl top pods -n argocd
# 복구: 리소스 제한 증가
kubectl patch deployment argocd-repo-server -n argocd --type json -p '[
{
"op": "replace",
"path": "/spec/template/spec/containers/0/resources/limits/memory",
"value": "4Gi"
}
]'
# Repo Server 재시작
kubectl rollout restart deployment/argocd-repo-server -n argocd
사례 3: 시크릿 동기화 실패 (External Secrets)
# 증상: Application은 Healthy이지만 Pod가 CrashLoopBackOff
# 원인: ExternalSecret이 아직 동기화되지 않음
# 진단
kubectl get externalsecret -n production
kubectl describe externalsecret db-credentials -n production
# 복구: ExternalSecret 강제 동기화
kubectl annotate externalsecret db-credentials \
-n production \
force-sync=$(date +%s) --overwrite
# 또는 Sync Wave로 순서 보장
# ExternalSecret: sync-wave=-2, Deployment: sync-wave=0
사례 4: ApplicationSet 의도치 않은 삭제
# 증상: ApplicationSet 수정 후 기존 Application이 삭제됨
# 원인: 제너레이터 설정 오류로 매칭되는 항목 감소
# 예방: preserveResourcesOnDeletion 설정
kubectl patch applicationset my-appset -n argocd --type merge -p '{
"spec": {
"syncPolicy": {
"preserveResourcesOnDeletion": true
}
}
}'
# 복구: Git 히스토리에서 이전 ApplicationSet 설정 복원
git log --oneline -- applicationsets/my-appset.yaml
git checkout HEAD~1 -- applicationsets/my-appset.yaml
git commit -m "Revert ApplicationSet to restore applications"
git push
마치며
ArgoCD는 GitOps의 핵심 도구로서, ApplicationSet을 통한 멀티 클러스터 자동화, Sync Waves와 Hook을 통한 정밀한 배포 순서 제어, 그리고 RBAC과 시크릿 관리를 통한 보안 강화까지 프로덕션 환경에서 필요한 모든 기능을 제공합니다.
핵심은 Git을 Single Source of Truth로 유지하면서, 적절한 자동화 수준을 설정하는 것입니다. 모든 환경에 자동 동기화를 적용하기보다는, 개발/스테이징에는 자동 동기화를, 프로덕션에는 수동 동기화를 적용하는 점진적 접근을 권장합니다. 모니터링과 알림을 반드시 함께 구성하여, 동기화 실패나 헬스 체크 이상을 신속하게 감지하고 대응할 수 있도록 하시기 바랍니다.
참고자료
ArgoCD GitOps Complete Guide: Declarative Kubernetes Deployment with ApplicationSet, Sync Waves, and Hooks
- Introduction
- ArgoCD Architecture Overview
- Application Resource Basics
- RBAC with AppProject
- ApplicationSet Generators
- Deployment Order Control with Sync Waves
- Using Sync Hooks
- Secrets Management
- Multi-Cluster Deployment
- Rollback Strategies
- Monitoring Setup
- Operational Notes
- Failure Cases and Recovery Procedures
- Conclusion
- References

Introduction
GitOps is an operational paradigm that uses Git as the Single Source of Truth for declaratively managing the desired state of infrastructure and applications. ArgoCD is the most widely adopted tool for implementing GitOps in Kubernetes environments, and as a CNCF Graduated project, it is production-proven at scale.
This guide covers all production essentials with practical code: multi-cluster/environment deployment through ApplicationSet, deployment order control through Sync Waves and Hooks, RBAC, and secrets management.
ArgoCD Architecture Overview
ArgoCD consists of the following core components.
| Component | Role | Key Functions |
|---|---|---|
| API Server | Web UI, CLI, CI/CD integration | gRPC/REST API, RBAC processing |
| Repo Server | Git repository management | Manifest generation (Helm, Kustomize, Plain YAML) |
| Application Controller | Core reconciliation loop | Cluster state monitoring, sync execution |
| ApplicationSet Controller | Multi-Application generation | Template-based automatic Application creation |
| Redis | Cache | Manifest caching, cluster state caching |
| Dex | Authentication | SSO, OIDC, LDAP external auth integration |
ArgoCD Installation
# Create namespace and install ArgoCD
kubectl create namespace argocd
kubectl apply -n argocd -f https://raw.githubusercontent.com/argoproj/argo-cd/stable/manifests/install.yaml
# Or install via Helm (recommended for production)
helm repo add argo https://argoproj.github.io/argo-helm
helm repo update
helm install argocd argo/argo-cd \
--namespace argocd \
--create-namespace \
--set server.service.type=LoadBalancer \
--set configs.params."server\.insecure"=true \
--set controller.replicas=2 \
--set repoServer.replicas=2 \
--set redis-ha.enabled=true
# Retrieve initial admin password
kubectl -n argocd get secret argocd-initial-admin-secret \
-o jsonpath='{.data.password}' | base64 -d
Application Resource Basics
Application Spec Details
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-application
namespace: argocd
# Finalizer: delete cluster resources when Application is deleted
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: production
source:
repoURL: https://github.com/myorg/k8s-manifests.git
targetRevision: main
path: apps/my-app/overlays/production
# Kustomize options
kustomize:
namePrefix: prod-
commonLabels:
env: production
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true # Delete cluster resources removed from Git
selfHeal: true # Auto-revert manual changes
allowEmpty: false # Prevent deploying empty manifests
syncOptions:
- CreateNamespace=true
- PrunePropagationPolicy=foreground
- PruneLast=true
- ServerSideApply=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
ignoreDifferences:
- group: apps
kind: Deployment
jsonPointers:
- /spec/replicas # Ignore since HPA manages this
Helm Source Configuration
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: prometheus-stack
namespace: argocd
spec:
project: monitoring
source:
repoURL: https://prometheus-community.github.io/helm-charts
chart: kube-prometheus-stack
targetRevision: 65.1.0
helm:
releaseName: prometheus
valuesObject:
grafana:
enabled: true
adminPassword: vault:secret/grafana#password
prometheus:
prometheusSpec:
retention: 30d
storageSpec:
volumeClaimTemplate:
spec:
storageClassName: gp3
resources:
requests:
storage: 100Gi
destination:
server: https://kubernetes.default.svc
namespace: monitoring
RBAC with AppProject
Project Definition
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: production
namespace: argocd
spec:
description: 'Production environment project'
# Allowed source repositories
sourceRepos:
- 'https://github.com/myorg/k8s-manifests.git'
- 'https://github.com/myorg/helm-charts.git'
# Allowed deployment targets
destinations:
- namespace: 'production'
server: 'https://kubernetes.default.svc'
- namespace: 'production-*'
server: 'https://kubernetes.default.svc'
# Allowed cluster resources
clusterResourceWhitelist:
- group: ''
kind: Namespace
- group: 'rbac.authorization.k8s.io'
kind: ClusterRole
- group: 'rbac.authorization.k8s.io'
kind: ClusterRoleBinding
# Blocked namespace resources
namespaceResourceBlacklist:
- group: ''
kind: ResourceQuota
- group: ''
kind: LimitRange
# RBAC role definitions
roles:
- name: deployer
description: 'Can sync and manage applications'
policies:
- p, proj:production:deployer, applications, get, production/*, allow
- p, proj:production:deployer, applications, sync, production/*, allow
- p, proj:production:deployer, applications, action/*, production/*, allow
groups:
- platform-team
- name: viewer
description: 'Read-only access'
policies:
- p, proj:production:viewer, applications, get, production/*, allow
groups:
- dev-team
ApplicationSet Generators
Git Directory Generator
Automatically generates Applications based on the Git repository directory structure.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: app-of-apps
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ['missingkey=error']
generators:
- git:
repoURL: https://github.com/myorg/k8s-manifests.git
revision: main
directories:
- path: 'apps/*/overlays/production'
- path: 'apps/deprecated-*'
exclude: true
template:
metadata:
name: '{{.path.basename}}'
namespace: argocd
labels:
app.kubernetes.io/managed-by: applicationset
spec:
project: production
source:
repoURL: https://github.com/myorg/k8s-manifests.git
targetRevision: main
path: '{{.path.path}}'
destination:
server: https://kubernetes.default.svc
namespace: '{{.path.basename}}'
syncPolicy:
automated:
prune: true
selfHeal: true
Cluster Generator
Automates multi-cluster deployment based on registered clusters.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: multi-cluster-apps
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ['missingkey=error']
generators:
- clusters:
selector:
matchLabels:
env: production
matchExpressions:
- key: region
operator: In
values:
- ap-northeast-2
- us-west-2
- eu-west-1
template:
metadata:
name: 'app-{{.name}}'
namespace: argocd
spec:
project: production
source:
repoURL: https://github.com/myorg/k8s-manifests.git
targetRevision: main
path: 'apps/my-app/overlays/{{.metadata.labels.env}}'
kustomize:
commonLabels:
cluster: '{{.name}}'
region: '{{.metadata.labels.region}}'
destination:
server: '{{.server}}'
namespace: production
Matrix Generator (Composite Combinations)
Combines two generators to produce all possible combinations.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: matrix-deployment
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ['missingkey=error']
generators:
- matrix:
generators:
# Generator 1: Cluster list
- clusters:
selector:
matchLabels:
env: production
# Generator 2: App list from Git directories
- git:
repoURL: https://github.com/myorg/k8s-manifests.git
revision: main
directories:
- path: 'apps/*'
template:
metadata:
name: '{{.path.basename}}-{{.name}}'
namespace: argocd
spec:
project: production
source:
repoURL: https://github.com/myorg/k8s-manifests.git
targetRevision: main
path: '{{.path.path}}/overlays/production'
destination:
server: '{{.server}}'
namespace: '{{.path.basename}}'
List Generator
Generates Applications from a static list of values.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: environment-apps
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ['missingkey=error']
generators:
- list:
elements:
- env: dev
cluster: https://dev-k8s.example.com
revision: develop
replicas: '1'
- env: staging
cluster: https://staging-k8s.example.com
revision: release/v2.5
replicas: '2'
- env: production
cluster: https://kubernetes.default.svc
revision: main
replicas: '3'
template:
metadata:
name: 'my-app-{{.env}}'
namespace: argocd
spec:
project: '{{.env}}'
source:
repoURL: https://github.com/myorg/k8s-manifests.git
targetRevision: '{{.revision}}'
path: 'apps/my-app/overlays/{{.env}}'
destination:
server: '{{.cluster}}'
namespace: 'my-app-{{.env}}'
Deployment Order Control with Sync Waves
Sync Wave Fundamentals
Sync Waves are defined using the argocd.argoproj.io/sync-wave annotation with integer values, deploying sequentially from the lowest value. The default value is 0.
# Wave -2: Namespace and RBAC (prerequisites)
apiVersion: v1
kind: Namespace
metadata:
name: production
annotations:
argocd.argoproj.io/sync-wave: '-2'
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-service-account
namespace: production
annotations:
argocd.argoproj.io/sync-wave: '-2'
---
# Wave -1: ConfigMap and Secret (configuration)
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
namespace: production
annotations:
argocd.argoproj.io/sync-wave: '-1'
data:
APP_ENV: production
LOG_LEVEL: info
---
# Wave 0: Core application (default)
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
namespace: production
annotations:
argocd.argoproj.io/sync-wave: '0'
spec:
replicas: 3
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
serviceAccountName: app-service-account
containers:
- name: app
image: myorg/my-app:v2.5.0
ports:
- containerPort: 8080
envFrom:
- configMapRef:
name: app-config
---
apiVersion: v1
kind: Service
metadata:
name: my-app
namespace: production
annotations:
argocd.argoproj.io/sync-wave: '0'
spec:
selector:
app: my-app
ports:
- port: 8080
targetPort: 8080
---
# Wave 1: Dependent resources (Ingress, HPA)
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: my-app-hpa
namespace: production
annotations:
argocd.argoproj.io/sync-wave: '1'
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: my-app
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
---
# Wave 2: Monitoring
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: my-app-monitor
namespace: production
annotations:
argocd.argoproj.io/sync-wave: '2'
spec:
selector:
matchLabels:
app: my-app
endpoints:
- port: http
interval: 15s
Using Sync Hooks
PreSync Hook: Database Migration Before Deployment
apiVersion: batch/v1
kind: Job
metadata:
name: db-migration
namespace: production
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
argocd.argoproj.io/sync-wave: '-1'
spec:
backoffLimit: 3
template:
spec:
containers:
- name: migrate
image: myorg/db-migrator:v2.5.0
command: ['./migrate', 'up']
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: db-credentials
key: url
restartPolicy: Never
PostSync Hook: Post-Deployment Verification
apiVersion: batch/v1
kind: Job
metadata:
name: smoke-test
namespace: production
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
backoffLimit: 1
template:
spec:
containers:
- name: smoke-test
image: myorg/smoke-tester:latest
command:
- /bin/sh
- -c
- |
echo "Running smoke tests..."
curl -sf http://my-app.production.svc:8080/healthz || exit 1
curl -sf http://my-app.production.svc:8080/api/v1/status || exit 1
echo "All smoke tests passed!"
restartPolicy: Never
SyncFail Hook: Notification on Sync Failure
apiVersion: batch/v1
kind: Job
metadata:
name: sync-fail-notification
namespace: production
annotations:
argocd.argoproj.io/hook: SyncFail
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
backoffLimit: 1
template:
spec:
containers:
- name: notify
image: curlimages/curl:latest
command:
- /bin/sh
- -c
- |
curl -X POST "$SLACK_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d '{"text":"ArgoCD Sync FAILED for my-app in production!"}'
env:
- name: SLACK_WEBHOOK_URL
valueFrom:
secretKeyRef:
name: slack-webhook
key: url
restartPolicy: Never
Secrets Management
Sealed Secrets Integration
# Install Sealed Secrets controller
helm repo add sealed-secrets https://bitnami-labs.github.io/sealed-secrets
helm install sealed-secrets sealed-secrets/sealed-secrets \
--namespace kube-system
# Encrypt secret with kubeseal CLI
kubectl create secret generic db-credentials \
--namespace production \
--from-literal=url='postgresql://user:pass@db:5432/mydb' \
--dry-run=client -o yaml | \
kubeseal --format yaml > sealed-db-credentials.yaml
# SealedSecret safe to commit to Git
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: db-credentials
namespace: production
annotations:
argocd.argoproj.io/sync-wave: '-2'
spec:
encryptedData:
url: AgA2X5N0Q...encrypted...base64==
template:
metadata:
name: db-credentials
namespace: production
type: Opaque
External Secrets Operator Integration
# ExternalSecret: Sync secrets from AWS Secrets Manager
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: production
annotations:
argocd.argoproj.io/sync-wave: '-2'
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secretsmanager
kind: ClusterSecretStore
target:
name: db-credentials
creationPolicy: Owner
data:
- secretKey: url
remoteRef:
key: production/database
property: connection_url
- secretKey: password
remoteRef:
key: production/database
property: password
---
# ClusterSecretStore definition
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: aws-secretsmanager
spec:
provider:
aws:
service: SecretsManager
region: ap-northeast-2
auth:
jwt:
serviceAccountRef:
name: external-secrets-sa
namespace: external-secrets
Multi-Cluster Deployment
Cluster Registration
# Register target clusters (kubeconfig context-based)
argocd cluster add staging-cluster \
--name staging \
--label env=staging \
--label region=ap-northeast-2
argocd cluster add production-cluster \
--name production \
--label env=production \
--label region=ap-northeast-2
# Verify registered clusters
argocd cluster list
Per-Cluster Configuration Separation
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: platform-services
namespace: argocd
spec:
goTemplate: true
goTemplateOptions: ['missingkey=error']
generators:
- matrix:
generators:
- clusters:
selector:
matchLabels:
env: production
- list:
elements:
- app: cert-manager
namespace: cert-manager
chart: cert-manager
repoURL: https://charts.jetstack.io
targetRevision: v1.16.0
- app: external-secrets
namespace: external-secrets
chart: external-secrets
repoURL: https://charts.external-secrets.io
targetRevision: 0.12.0
- app: metrics-server
namespace: kube-system
chart: metrics-server
repoURL: https://kubernetes-sigs.github.io/metrics-server
targetRevision: 3.12.0
template:
metadata:
name: '{{.app}}-{{.name}}'
namespace: argocd
spec:
project: platform
source:
repoURL: '{{.repoURL}}'
chart: '{{.chart}}'
targetRevision: '{{.targetRevision}}'
helm:
releaseName: '{{.app}}'
destination:
server: '{{.server}}'
namespace: '{{.namespace}}'
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Rollback Strategies
Automatic Rollback Configuration
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: my-app
namespace: argocd
spec:
project: production
source:
repoURL: https://github.com/myorg/k8s-manifests.git
targetRevision: main
path: apps/my-app/overlays/production
destination:
server: https://kubernetes.default.svc
namespace: production
syncPolicy:
automated:
prune: true
selfHeal: true
retry:
limit: 3
backoff:
duration: 10s
factor: 2
maxDuration: 5m
Manual Rollback via CLI
# Check deployment history
argocd app history my-app
# Rollback to a specific revision
argocd app rollback my-app 5
# Or sync to a specific Git commit
argocd app sync my-app --revision abc123def
# Verify sync status
argocd app get my-app
argocd app wait my-app --health
Monitoring Setup
ArgoCD Prometheus Metrics
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: argocd-metrics
namespace: argocd
spec:
selector:
matchLabels:
app.kubernetes.io/part-of: argocd
endpoints:
- port: metrics
interval: 30s
---
apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
name: argocd-alerts
namespace: argocd
spec:
groups:
- name: argocd
rules:
- alert: ArgoCDAppOutOfSync
expr: |
argocd_app_info{sync_status="OutOfSync"} == 1
for: 10m
labels:
severity: warning
annotations:
summary: 'Application {{ "{{" }} $labels.name {{ "}}" }} is OutOfSync for more than 10 minutes'
- alert: ArgoCDAppDegraded
expr: |
argocd_app_info{health_status="Degraded"} == 1
for: 5m
labels:
severity: critical
annotations:
summary: 'Application {{ "{{" }} $labels.name {{ "}}" }} is Degraded'
- alert: ArgoCDSyncFailed
expr: |
increase(argocd_app_sync_total{phase="Failed"}[10m]) > 0
labels:
severity: critical
annotations:
summary: 'ArgoCD sync failed for {{ "{{" }} $labels.name {{ "}}" }}'
Slack Notification Configuration
# argocd-notifications-cm ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: argocd-notifications-cm
namespace: argocd
data:
service.slack: |
token: $slack-token
trigger.on-sync-failed: |
- when: app.status.operationState.phase in ['Error', 'Failed']
send: [app-sync-failed]
trigger.on-health-degraded: |
- when: app.status.health.status == 'Degraded'
send: [app-health-degraded]
trigger.on-sync-succeeded: |
- when: app.status.operationState.phase in ['Succeeded']
oncePer: app.status.sync.revision
send: [app-sync-succeeded]
template.app-sync-failed: |
slack:
attachments: |
[{
"color": "#E96D76",
"title": "Sync Failed: {{.app.metadata.name}}",
"text": "Application {{.app.metadata.name}} sync failed.\nRevision: {{.app.status.sync.revision}}\nMessage: {{.app.status.operationState.message}}"
}]
template.app-health-degraded: |
slack:
attachments: |
[{
"color": "#f4c030",
"title": "Health Degraded: {{.app.metadata.name}}",
"text": "Application {{.app.metadata.name}} health is degraded."
}]
template.app-sync-succeeded: |
slack:
attachments: |
[{
"color": "#18BE52",
"title": "Sync Succeeded: {{.app.metadata.name}}",
"text": "Application {{.app.metadata.name}} synced successfully.\nRevision: {{.app.status.sync.revision}}"
}]
Operational Notes
1. ApplicationSet Security
If the project field in your ApplicationSet is templated, developers may be able to create Applications under Projects with excessive permissions. Always ensure the project field references a source controlled by administrators only.
2. Auto-Sync Caution
automated.prune: true deletes cluster resources that have been removed from Git. If you accidentally delete a manifest, production resources will be immediately removed. For critical resources, add the argocd.argoproj.io/sync-options: Prune=false annotation.
3. Webhook Security
If ArgoCD is publicly accessible, you must configure webhook secrets to prevent DDoS attacks.
4. Repo Server Resources
When processing large repositories or Helm charts, Repo Server memory usage can spike significantly. Configure appropriate resource limits and replica counts.
5. Abstraction Layer Limits
When using the Application of Applications pattern, avoid more than 3 levels of abstraction. Nesting 4-5 levels makes debugging extremely difficult.
Failure Cases and Recovery Procedures
Case 1: Sync Infinite Loop
# Symptom: Application keeps cycling through Syncing state
# Cause: Drift detection due to missing ignoreDifferences
# Diagnosis
argocd app diff my-app
argocd app get my-app --show-operation
# Recovery: Add ignoreDifferences
kubectl patch application my-app -n argocd --type merge -p '{
"spec": {
"ignoreDifferences": [
{
"group": "apps",
"kind": "Deployment",
"jsonPointers": ["/spec/replicas"]
}
]
}
}'
Case 2: Repo Server OOM
# Symptom: Application in Unknown state, manifest generation fails
# Diagnosis
kubectl logs -n argocd deploy/argocd-repo-server --previous
kubectl top pods -n argocd
# Recovery: Increase resource limits
kubectl patch deployment argocd-repo-server -n argocd --type json -p '[
{
"op": "replace",
"path": "/spec/template/spec/containers/0/resources/limits/memory",
"value": "4Gi"
}
]'
# Restart Repo Server
kubectl rollout restart deployment/argocd-repo-server -n argocd
Case 3: Secret Sync Failure (External Secrets)
# Symptom: Application is Healthy but Pod is in CrashLoopBackOff
# Cause: ExternalSecret has not yet synchronized
# Diagnosis
kubectl get externalsecret -n production
kubectl describe externalsecret db-credentials -n production
# Recovery: Force ExternalSecret sync
kubectl annotate externalsecret db-credentials \
-n production \
force-sync=$(date +%s) --overwrite
# Or ensure ordering with Sync Waves
# ExternalSecret: sync-wave=-2, Deployment: sync-wave=0
Case 4: Unintended ApplicationSet Deletion
# Symptom: Existing Applications deleted after ApplicationSet modification
# Cause: Generator config error reducing matched items
# Prevention: Set preserveResourcesOnDeletion
kubectl patch applicationset my-appset -n argocd --type merge -p '{
"spec": {
"syncPolicy": {
"preserveResourcesOnDeletion": true
}
}
}'
# Recovery: Restore previous ApplicationSet config from Git history
git log --oneline -- applicationsets/my-appset.yaml
git checkout HEAD~1 -- applicationsets/my-appset.yaml
git commit -m "Revert ApplicationSet to restore applications"
git push
Conclusion
ArgoCD serves as the cornerstone of GitOps, providing everything needed for production environments: multi-cluster automation through ApplicationSet, precise deployment order control through Sync Waves and Hooks, and security hardening through RBAC and secrets management.
The key is maintaining Git as the Single Source of Truth while configuring the appropriate level of automation. Rather than applying auto-sync to all environments, a graduated approach is recommended: auto-sync for dev/staging and manual sync for production. Always configure monitoring and alerting together to quickly detect and respond to sync failures or health check anomalies.