Argo CD 멀티클러스터 시크릿 관리: Sealed Secrets와 External Secrets Operator 실전 패턴
Kubernetes를 단일 클러스터에서 운영해보셨고, Argo CD로 GitOps를 구성해보신 분이라면 바로 따라오실 수 있는 내용입니다. 멀티클러스터로 확장하는 순간 가장 먼저 맞닥뜨리는 문제 중 하나가 시크릿입니다. 저도 처음에 개발 클러스터 하나에서 잘 돌아가던 Argo CD 설정을 스테이징, 프로덕션으로 넓히면서 "일단 Base64로 인코딩하면 Git에 올려도 되지 않나?" 하고 넘어갔습니다. 물론 아닙니다. Base64는 암호화가 아닙니다. 그리고 그 사실을 깨달은 시점이 너무 늦었을 때, Spoke 클러스터 접근 토큰이 담긴 시크릿이 탈취되면 해당 클러스터 전체를 장악할 수 있다는 위험도 함께 실감했습니다. 팀 전체가 시크릿 관리 체계를 다시 세우는 데 이틀을 썼고, 그 이후로 이 부분은 처음부터 제대로 잡는 주제가 됐습니다.
이 글은 그 이틀을 여러분이 건너뛸 수 있도록 쓰는 글입니다. Argo CD 멀티클러스터 환경에서 ApplicationSet으로 배포를 자동화하고, Sealed Secrets와 External Secrets Operator(ESO) 중 상황에 맞는 방식을 골라 시크릿을 안전하게 관리하는 패턴을 실제 YAML과 함께 풀어드립니다. 개념 설명보다는 "이 상황에서 어떻게 쓰는가"에 집중했습니다.
핵심 개념
Hub-Spoke: Argo CD가 여러 클러스터를 바라보는 구조
멀티클러스터 Argo CD의 가장 일반적인 형태는 Hub-Spoke 모델입니다. 하나의 Hub 클러스터에 Argo CD를 설치하고, 여기서 여러 Spoke(원격) 클러스터를 관리합니다. Hub의 argocd 네임스페이스에 각 클러스터를 Secret 리소스로 등록하면, Argo CD가 해당 클러스터에 접근해 애플리케이션을 배포합니다.
# Hub 클러스터의 argocd 네임스페이스에 생성하는 Spoke 클러스터 시크릿
apiVersion: v1
kind: Secret
metadata:
name: prod-cluster
namespace: argocd
labels:
argocd.argoproj.io/secret-type: cluster
env: production
region: ap-northeast-2
type: Opaque
stringData:
name: prod-cluster
server: https://prod-cluster.example.com
config: |
{
"bearerToken": "<spoke-cluster-sa-token>",
"tlsClientConfig": {
"insecure": false,
"caData": "<base64-ca-cert>"
}
}이 시크릿에는 Spoke 클러스터의 ServiceAccount 토큰이 들어있습니다. 탈취당하면 해당 클러스터 전체를 장악할 수 있어서, 이 시크릿 자체도 반드시 안전하게 관리해야 합니다. ESO로 이 시크릿 자체를 Vault에서 주입하는 패턴이 가장 안전하며, 뒤에서 다시 다루겠습니다.
Hub-Spoke란? 자전거 바퀴처럼 중앙(Hub)에서 뻗어나가는 구조입니다. Hub Argo CD가 여러 Spoke 클러스터에 아웃바운드로 접근해 배포를 수행합니다. 방화벽으로 인바운드가 막힌 환경에서는
argocd-agent(역방향 연결)를 검토해볼 수 있습니다.
ApplicationSet: 클러스터가 늘어날수록 빛을 발하는 자동화
클러스터가 3개를 넘어가면 각 클러스터마다 Application 리소스를 손으로 만드는 게 금방 한계에 달합니다. ApplicationSet은 Generator를 통해 Application을 자동으로 생성해 줍니다. 특히 Cluster Generator + Git Generator를 Matrix로 조합하면, 클러스터 레이블과 Git 디렉토리 구조만으로 수백 개 클러스터 배포를 선언적으로 관리할 수 있습니다.
Git에는 시크릿 값이 아닌 '참조'만 남긴다
시크릿 관리의 핵심 원칙은 단순합니다. Git 저장소에는 절대 평문 시크릿을 커밋하지 않습니다. 여기서 두 갈래 길이 나뉩니다.
| 방식 | Git에 저장되는 것 | 복호화/동기화 주체 |
|---|---|---|
| Sealed Secrets | 암호화된 SealedSecret 리소스 |
클러스터 내 Sealed Secrets 컨트롤러 |
| External Secrets Operator | 시크릿 참조(ExternalSecret) |
외부 스토어(Vault, AWS SM 등) |
Sealed Secrets는 "암호화된 값 자체"를 Git에 저장하고, ESO는 "어디서 가져올지"만 Git에 저장합니다.
어느 쪽을 선택할까?
먼저 자신의 상황을 체크해보시면 방향이 잡힙니다.
| 항목 | Sealed Secrets | External Secrets Operator |
|---|---|---|
| 외부 의존성 | 없음 (클러스터 자체 완결) | 외부 스토어 필요 |
| 설치·설정 난이도 | 낮음 | 중간~높음 |
| 에어갭 환경 | 완벽 지원 | 불가 |
| 동적 로테이션 | 미지원 | 지원 (refreshInterval) |
| 멀티 프로바이더 | 해당 없음 | AWS/GCP/Azure/Vault 모두 지원 |
| GitOps 통합 | 자연스러움 (SealedSecret이 Git 리소스) | Git에는 참조만 남아 더 깔끔 |
| 멀티클러스터 확장성 | BYO Key 전략 필요 | 중앙 스토어 하나로 통합 관리 |
외부 의존성을 최소화하고 싶거나, 에어갭 환경이라면 Sealed Secrets. 엔터프라이즈 규모로 시크릿 로테이션·감사 로그가 필요하다면 ESO. 두 방식을 혼용하는 팀도 있습니다. 최근 트렌드는 ESO 쪽으로 기울고 있지만, 처음 GitOps를 도입하는 팀이라면 Sealed Secrets로 시작하는 게 현실적입니다.
실전 적용
예시 1: ApplicationSet Matrix Generator로 멀티클러스터 자동 배포
env: production 레이블이 붙은 모든 클러스터에, GitOps 저장소의 클러스터별 values.yaml을 적용해 동일한 앱을 배포하는 패턴입니다. 클러스터를 새로 등록하고 레이블만 달아주면 자동으로 Application이 생성됩니다.
한 가지 주의할 점이 있습니다. 앱 차트 저장소(my-app)와 values 파일이 있는 GitOps 저장소(gitops-repo)가 분리된 경우, source를 하나만 쓰면 helm.valueFiles에서 다른 저장소의 파일을 참조할 수 없습니다. Argo CD 2.6 이상이라면 sources(복수형)로 멀티소스를 지정해야 합니다.
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: my-app
namespace: argocd
spec:
generators:
- matrix:
generators:
- clusters:
selector:
matchLabels:
env: production
- git:
repoURL: https://github.com/org/gitops-repo
revision: HEAD
files:
- path: "clusters/*/values.yaml"
template:
metadata:
name: "my-app-{{name}}"
spec:
project: default
sources:
# 앱 차트 저장소
- repoURL: https://github.com/org/my-app
targetRevision: HEAD
helm:
valueFiles:
- $values/clusters/{{name}}/values.yaml
# values 파일이 있는 GitOps 저장소 (ref로 참조)
- repoURL: https://github.com/org/gitops-repo
targetRevision: HEAD
ref: values
destination:
server: "{{server}}"
namespace: my-app
syncPolicy:
automated:
prune: true
selfHeal: true| 필드 | 역할 |
|---|---|
clusters.selector.matchLabels |
env: production 레이블 클러스터만 대상으로 선택 |
git.files |
클러스터별 values 파일 경로 패턴 |
{{name}}, {{server}} |
Cluster Generator가 주입하는 클러스터 이름·주소 |
sources[1].ref: values |
두 번째 소스를 $values 변수로 참조 가능하게 설정 |
syncPolicy.automated |
변경 감지 시 자동 동기화·정리 |
예시 2: Sealed Secrets로 클러스터별 봉인
Sealed Secrets는 설정이 단순해서 팀 내 GitOps 도입 초기에 빠르게 시작하기 좋습니다. 저도 처음엔 이걸로 시작했고, 소규모 환경에서는 지금도 애용합니다.
공개키 추출 후 시크릿 암호화:
# 대상 클러스터의 공개키 추출 — 이 키는 클러스터마다 다릅니다
kubeseal --fetch-cert \
--controller-name=sealed-secrets-controller \
--controller-namespace=kube-system \
--kubeconfig ~/.kube/prod-cluster > pub-key-prod.pem
# 시크릿을 생성해 바로 봉인 (dry-run → kubeseal 파이프)
kubectl create secret generic db-password \
--from-literal=password=mysecret \
--dry-run=client -o yaml | \
kubeseal --cert pub-key-prod.pem \
--format yaml > db-password-sealed.yaml
# 봉인된 파일을 Git에 커밋
git add db-password-sealed.yaml && git commit -m "add sealed db-password for prod"--cert pub-key-prod.pem을 지정하면 kubeseal이 클러스터에 직접 연결하지 않고도 로컬에서 암호화합니다. 단, 이 공개키는 prod-cluster 전용이므로, 다른 클러스터에 배포할 시크릿은 해당 클러스터의 공개키로 따로 봉인해야 합니다.
멀티클러스터에서 Bring Your Own Key(BYO Key) 전략:
클러스터마다 별도 키를 쓰면 시크릿을 공유할 때 매번 다시 봉인해야 하는 번거로움이 생깁니다. 공통 키를 미리 만들어 여러 클러스터에 배포하는 BYO Key 방식으로 이 문제를 해결할 수 있습니다.
# 공통 RSA 키 생성
openssl req -x509 -nodes -newkey rsa:4096 \
-keyout sealed-secrets.key \
-out sealed-secrets.crt \
-subj "/CN=sealed-secret/O=sealed-secret" \
-days 3650
# 각 클러스터에 동일 키를 시크릿으로 등록 (apply 후 label 별도 적용)
kubectl create secret tls my-sealing-key \
--cert=sealed-secrets.crt \
--key=sealed-secrets.key \
--namespace=kube-system
kubectl label secret my-sealing-key \
--namespace=kube-system \
sealedsecrets.bitnami.com/sealed-secrets-key=active파이프로 한 줄에 처리하려는 시도를 종종 보는데, kubectl label --local과 --dry-run=client의 조합은 metadata.name이 누락되는 경우가 있어 실패하기 쉽습니다. apply 후 label을 별도로 붙이는 쪽이 훨씬 안전합니다.
키 백업은 선택이 아닌 필수입니다:
# 30일마다 자동 갱신되는 키를 정기 백업
kubectl get secret -n kube-system \
-l sealedsecrets.bitnami.com/sealed-secrets-key \
-o yaml > sealed-secrets-keys-backup.yaml
# 이 파일은 반드시 Vault나 AWS Secrets Manager에 보관 — Git에는 절대 올리지 않습니다왜 키 백업이 중요한가? Sealed Secrets 컨트롤러가 삭제되거나 키가 분실되면, Git에 저장된 SealedSecret을 복호화할 방법이 없습니다. 시크릿을 영구적으로 잃게 됩니다.
예시 3: External Secrets Operator + HashiCorp Vault 멀티클러스터 패턴
클러스터가 많아지거나, 시크릿 로테이션·감사 로그가 필요한 엔터프라이즈 환경이라면 ESO + Vault 조합이 훨씬 강력합니다. 솔직히 초기 설정은 손이 좀 가는데, 한 번 구축해두면 이후 관리가 확연히 편해집니다.
부트스트래핑 순서에 대한 현실적인 이야기:
ESO 자체를 Argo CD로 배포하고 싶을 때, ESO가 아직 없으면 ClusterSecretStore도 없다는 닭-달걀 문제가 생깁니다. 처음 ESO를 설치할 때는 Helm이나 직접 kubectl apply로 설치한 뒤, 이후 관리를 Argo CD로 위임하는 방식이 일반적입니다.
ClusterSecretStore로 Vault 백엔드 정의:
# 클러스터 전체에서 참조 가능한 Vault 백엔드 스토어
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: vault-backend
spec:
provider:
vault:
server: https://vault.example.com
path: secret
version: v2
auth:
kubernetes:
mountPath: kubernetes
role: external-secrets-role
serviceAccountRef:
name: external-secrets
namespace: external-secrets여기서 external-secrets-role은 Vault에 미리 정의된 Kubernetes Auth Role입니다. 이 Role에는 myapp/db 경로에 대한 read 권한이 부여되어 있어야 합니다. AWS EKS를 쓴다면 IRSA로, GKE라면 Workload Identity로 인증 방식이 달라집니다. Vault를 처음 접하신다면 "AWS Secrets Manager나 GCP Secret Manager로 대체 가능하다"는 점도 알아두시면 좋습니다 — ESO는 동일한 ExternalSecret 구조로 다양한 백엔드를 지원합니다.
ExternalSecret으로 개별 네임스페이스에서 시크릿 참조:
# Git에는 이 파일만 커밋됩니다 — 값은 없고 참조만 있습니다
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: db-credentials
namespace: my-app
spec:
refreshInterval: 1h # 1시간마다 Vault에서 최신 값 동기화
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: db-secret # 생성될 Kubernetes Secret 이름
creationPolicy: Owner
data:
- secretKey: password
remoteRef:
key: myapp/db # Vault KV 경로
property: password
- secretKey: username
remoteRef:
key: myapp/db
property: username| 필드 | 역할 |
|---|---|
refreshInterval |
Vault 값 변경 시 Kubernetes Secret 자동 갱신 주기 |
ClusterSecretStore |
네임스페이스 경계 없이 클러스터 전체에서 참조 가능 |
creationPolicy: Owner |
ExternalSecret 삭제 시 Kubernetes Secret도 함께 삭제 |
remoteRef.key |
Vault KV v2 경로 |
Argo CD drift 오탐 방지 설정:
ESO가 시크릿을 주기적으로 갱신하면, Argo CD가 이를 "Git과 다른 상태(drift)"로 잘못 감지하고 OutOfSync 경고를 냅니다. 실무에서 자주 맞닥뜨리는 상황인데, ignoreDifferences로 방지할 수 있습니다. 다만 /data 전체를 무시하면 ESO가 아닌 시크릿에서 생기는 실제 drift도 놓칠 수 있어서, managedFieldsManagers로 ESO가 관리하는 리소스에만 적용하는 게 낫습니다.
# Application 또는 AppProject에 추가
spec:
ignoreDifferences:
- group: ""
kind: Secret
jsonPointers:
- /data
managedFieldsManagers:
- external-secrets장단점 분석
장점
| 항목 | Sealed Secrets | External Secrets Operator |
|---|---|---|
| 설치 복잡도 | Helm 한 줄로 완성 | ESO + 외부 스토어 설정 필요 |
| 외부 의존성 | 없음 (클러스터 자체 완결) | 외부 스토어 필수 |
| 에어갭 환경 | 완벽 지원 | 불가 |
| 동적 로테이션 | 미지원 | refreshInterval로 자동화 |
| 멀티 프로바이더 | 해당 없음 | AWS/GCP/Azure/Vault 모두 지원 |
| GitOps 통합 | SealedSecret 자체가 Git 리소스 | Git에는 참조만, 더 깔끔 |
단점 및 주의사항
| 항목 | 설명 | 대응 방안 |
|---|---|---|
| Sealed Secrets — 키 유실 | 컨트롤러 삭제·키 분실 시 모든 SealedSecret 복호화 불가 | 30일 주기 키 백업, Vault/AWS SM 보관 |
| Sealed Secrets — 클러스터 종속 | 클러스터별 키가 달라 시크릿 공유가 복잡해짐 | BYO Key 전략으로 공통 키 사용 |
| ESO — 외부 장애 전파 | Vault/AWS SM 장애 시 신규 파드에 시크릿 주입 실패 | 캐싱 전략, Vault 고가용성 구성 |
| ESO — 파드 자동 재기동 미지원 | 시크릿 갱신 후 기존 파드는 새 값을 반영하지 못함 | Reloader 연동 |
| 공통 — Argo CD 클러스터 시크릿 | Spoke 클러스터 접근 토큰이 Hub argocd 네임스페이스에 노출 | ESO로 이 시크릿 자체도 Vault에서 주입 |
SecretStore vs ClusterSecretStore:
SecretStore는 네임스페이스 범위,ClusterSecretStore는 클러스터 전체 범위입니다. 멀티테넌트 환경에서는 팀별 네임스페이스에SecretStore를 두어 다른 팀의 시크릿 참조를 차단하는 방식이 더 안전합니다.
이 설정을 빠뜨리면 생기는 일
Sealed Secrets 키를 백업하지 않은 경우
"컨트롤러가 알아서 관리하겠지"라고 넘어갔다가 클러스터를 재구성할 때 모든 SealedSecret이 쓸모없어지는 상황이 생깁니다. 처음 컨트롤러를 설치할 때 키 백업 파이프라인도 함께 세워두시는 편이 나중에 편합니다.
ESO 도입 후 ignoreDifferences 설정을 빠뜨린 경우
시크릿이 자동으로 갱신될 때마다 Argo CD가 OutOfSync 상태를 보고하고, 팀 슬랙 채널에 알림이 쌓이기 시작합니다. 실제로 이 설정을 빠뜨려서 OutOfSync 알림이 수백 개 쌓인 경험이 있는데, 이후로는 ESO 배포와 동시에 이 설정을 함께 적용하는 게 루틴이 됐습니다. ESO 배포와 같은 PR에 넣어두시면 잊어버릴 일이 없습니다.
Hub 클러스터의 argocd 클러스터 시크릿을 평문으로 두는 경우
Spoke 클러스터 접근 토큰이 담긴 이 시크릿이 가장 중요한 공격 대상임에도, "어차피 argocd 네임스페이스 안이니 괜찮겠지"라며 방치하는 경우가 많습니다. 이 시크릿 자체도 ESO + Vault로 관리하는 패턴을 적용해두시면 훨씬 안심이 됩니다.
마치며
멀티클러스터 Argo CD 환경에서의 시크릿 관리는 "어떻게 숨기느냐"가 아니라 "어디서 관리하느냐"의 아키텍처 문제입니다. 클러스터 수가 적고 외부 의존성을 최소화하고 싶다면 Sealed Secrets, 엔터프라이즈 규모의 중앙화된 시크릿 관리와 자동 로테이션이 필요하다면 External Secrets Operator가 더 잘 맞습니다.
지금 바로 시작해볼 수 있는 3단계:
-
Sealed Secrets로 작게 시작해보기.
helm install sealed-secrets-controller sealed-secrets/sealed-secrets -n kube-system으로 컨트롤러를 설치하고,kubeseal --fetch-cert로 공개키를 받아 기존 시크릿 하나를 봉인해보시면 30분 안에 첫 SealedSecret을 Git에 올릴 수 있습니다. -
ApplicationSet Cluster Generator 연결해보기. 현재 관리 중인 클러스터에
env: staging같은 레이블을 달고, 위 예시의 ApplicationSet YAML을 조정해 적용해보시면 클러스터를 등록하는 즉시 Application이 자동 생성되는 경험을 하실 수 있습니다. 앱 저장소와 values 저장소가 분리되어 있다면sources멀티소스 구성을 함께 적용해보시면 좋습니다. -
ESO PushSecret으로 시크릿 전파 실험해보기. 이 단계는 ESO 설치, Vault 연동, ClusterSecretStore 설정이 완료된 이후의 심화 주제입니다. 준비가 되셨다면 Hub 클러스터에서
PushSecret리소스를 만들어 관리 클러스터의 시크릿을 Spoke 클러스터로 자동 전파해보시는 것도 좋습니다. 멀티클러스터 공통 시크릿을 중앙에서 한 번만 관리하고 싶을 때 특히 유용합니다.
참고 자료
- Argo CD 공식 문서 - Secret Management
- Argo CD 공식 문서 - Cluster Management
- Argo CD 공식 문서 - Cluster Generator (ApplicationSet)
- External Secrets Operator 공식 문서 - Overview
- External Secrets Operator - ClusterSecretStore
- A Comprehensive Overview of Argo CD Architectures 2025 | Codefresh
- ArgoCD ApplicationSet: Multi-Cluster Deployment Made Easy | Codefresh
- GitOps Secrets with Argo CD, HashiCorp Vault, and External Secret Operator | Codefresh
- Sealed Secrets | GitHub - bitnami-labs
- argocd-agent | GitHub - argoproj-labs
- Multi-cluster GitOps with Argo CD Agent | Red Hat Blog
- A Guide to Secrets Management with GitOps and Kubernetes | Red Hat Blog
- Sealed Secrets multi-cluster scenario | DEV Community
- Kubernetes Secrets Management in 2025 | Infisical Blog
- GitOps in 2025: From Old-School Updates to the Modern Way | CNCF