OKE 플랫폼 엔지니어링 랩 3편 — Vault + External Secrets로 Kubernetes 시크릿 관리하기
이 포스트는 OKE 플랫폼 엔지니어링 랩 시리즈의 3편입니다.
- 1편 — Terraform + Terragrunt로 OKE 클러스터 프로비저닝하기
- 2편 — ArgoCD App-in-Apps로 인프라 배포하기
- 3편 — Vault + External Secrets로 Kubernetes 시크릿 관리하기 ← 현재
- 4편 — Istio + cert-manager로 TLS 인증서 자동화하기
1. 배경
Kubernetes에서 시크릿을 관리할 때 가장 먼저 부딪히는 문제는 Git에 민감 정보를 올릴 수 없다는 점입니다.
ArgoCD 기반 GitOps 환경에서는 모든 상태를 Git으로 관리하는 것이 원칙인데, DB 비밀번호나 API 토큰 같은 값을 그대로 레포에 올릴 수는 없습니다. 그렇다고 Secret 매니페스트를 GitOps 흐름 밖에서 수동으로 관리하면 “모든 것을 코드로”라는 원칙이 깨집니다.
이 문제를 해결하는 조합이 **HashiCorp Vault + External Secrets Operator(ESO)**입니다.
- Vault: 시크릿을 중앙에서 저장하고 접근을 제어합니다.
- External Secrets Operator: Vault의 시크릿을 읽어 K8s Secret으로 동기화합니다.
Git에는 “어떤 시크릿을 어디서 가져오는지”를 담은 ExternalSecret 매니페스트만 올리고, 실제 민감한 값은 Vault에서 관리하는 방식입니다.
2. Vault 설치
ArgoCD App-in-Apps 구조에서 Vault는 infra-core 그룹에 포함되어 Sync Wave -2에 배포됩니다. External Secrets보다 늦게 설치되지만, ClusterSecretStore 연동은 그 이후에 이루어지기 때문에 문제가 없습니다.
# infra/core/vault-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: vault
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "-2"
spec:
project: default
sources:
- repoURL: https://helm.releases.hashicorp.com
chart: vault
targetRevision: "0.32.0"
helm:
valueFiles:
- $values/infra/vault/values.yaml
- repoURL: https://github.com/<your-org>/gitops.git
targetRevision: main
ref: values
destination:
server: https://kubernetes.default.svc
namespace: vault
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
Helm values는 다음과 같습니다. 스토리지는 NFS 기반 PVC를 사용하고, Ingress 대신 Istio VirtualService로 외부에 노출합니다.
# infra/vault/values.yaml
server:
image:
repository: docker.io/hashicorp/vault
dev:
enabled: false # 프로덕션 모드
ha:
enabled: false # 단일 인스턴스
dataStorage:
enabled: true
size: 5Gi
storageClass: nfs # NFS 동적 프로비저닝
ui:
enabled: true
ingress:
enabled: false # Istio VirtualService 사용
injector:
enabled: false # Sidecar 주입 비활성화
3. Vault 초기화
Vault가 처음 기동되면 sealed 상태입니다. 데이터를 복호화할 키가 없는 상태이므로, 먼저 초기화와 unseal을 진행해야 합니다.
# Vault Pod에 접속
kubectl exec -it vault-0 -n vault -- /bin/sh
# 초기화 (Unseal Key 5개, Threshold 3개 생성)
vault operator init
# 출력된 Unseal Key 3개로 순차적으로 unseal
vault operator unseal <UNSEAL_KEY_1>
vault operator unseal <UNSEAL_KEY_2>
vault operator unseal <UNSEAL_KEY_3>
# Root Token으로 로그인
vault login <ROOT_TOKEN>
vault operator init 실행 시 Unseal Key 5개와 Root Token이 출력됩니다. 이 값들은 반드시 안전한 곳에 보관해야 합니다. Vault Pod가 재시작될 때마다 다시 unseal이 필요합니다.
4. Kubernetes Auth 설정
External Secrets Operator가 Vault에 접근하려면 인증 방법을 설정해야 합니다. 클러스터 내부에서 ServiceAccount 토큰으로 인증하는 Kubernetes Auth 방식을 사용합니다.
# Kubernetes Auth Method 활성화
vault auth enable kubernetes
# K8s API 서버 정보로 Auth 설정
vault write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc:443"
다음으로 External Secrets가 사용할 Policy와 Role을 생성합니다.
# secret/prod/* 경로에 대한 읽기 권한 Policy 생성
vault policy write external-secrets - <<EOF
path "secret/data/prod/*" {
capabilities = ["read"]
}
EOF
# external-secrets ServiceAccount와 Policy를 연결하는 Role 생성
vault write auth/kubernetes/role/external-secrets \
bound_service_account_names=external-secrets \
bound_service_account_namespaces=external-secrets \
policies=external-secrets \
ttl=1h
이 Role은 external-secrets 네임스페이스의 external-secrets ServiceAccount가 external-secrets Policy로 Vault에 접근할 수 있도록 허용합니다.
5. External Secrets 설치
External Secrets Operator는 Vault와 독립적으로 설치됩니다. Sync Wave -3으로 설정한 이유는 ESO의 CRD(ClusterSecretStore, ExternalSecret 등)가 먼저 클러스터에 등록되어야, 이후 Platform 그룹에서 ClusterSecretStore 매니페스트를 apply할 때 오류가 나지 않기 때문입니다.
# infra/core/external-secrets-app.yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: external-secrets
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "-3"
spec:
project: default
sources:
- repoURL: https://charts.external-secrets.io
chart: external-secrets
targetRevision: "2.1.0"
helm:
valueFiles:
- $values/infra/external-secrets/values.yaml
- repoURL: https://github.com/<your-org>/gitops.git
targetRevision: main
ref: values
destination:
server: https://kubernetes.default.svc
namespace: external-secrets
syncPolicy:
automated:
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- ServerSideApply=true # CRD 등 대규모 리소스 적용 시 필요
# infra/external-secrets/values.yaml
installCRDs: true
serviceAccount:
name: external-secrets
6. ClusterSecretStore 구성
Vault 초기화와 Kubernetes Auth 설정이 끝났다면, External Secrets가 Vault를 백엔드로 사용할 수 있도록 ClusterSecretStore를 생성합니다.
# infra/external-secrets/store/cluster-secret-store.yaml
apiVersion: external-secrets.io/v1
kind: ClusterSecretStore
metadata:
name: vault-backend
spec:
provider:
vault:
server: "http://vault.vault.svc.cluster.local:8200"
path: "secret"
version: "v2" # KV v2 사용
auth:
kubernetes:
mountPath: "kubernetes"
role: "external-secrets" # 앞서 생성한 Role
serviceAccountRef:
name: external-secrets
namespace: external-secrets
Vault 서버 주소는 클러스터 내부 DNS(vault.vault.svc.cluster.local)를 사용합니다. 외부 도메인이 아닌 내부 통신이므로 HTTP로도 충분합니다.
이 리소스는 infra-platform 그룹의 ArgoCD Application으로 배포됩니다. Vault와 External Secrets가 모두 올라온 뒤에 적용되어야 하기 때문입니다.
7. ExternalSecret 실전 예시 — cert-manager Cloudflare 토큰
ClusterSecretStore가 준비되었다면 실제로 시크릿을 동기화해볼 수 있습니다. cert-manager의 DNS-01 챌린지에 필요한 Cloudflare API 토큰을 예시로 사용합니다.
Vault에 시크릿 저장
vault kv put secret/prod/cert-manager cf-api-token=<CLOUDFLARE_API_TOKEN>
ExternalSecret 생성
# infra/cert-manager/config/cloudflare-token-secret.yaml
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: cloudflare-token-secret
namespace: cert-manager
spec:
refreshInterval: 1h # 1시간마다 Vault와 동기화
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: cloudflare-token-secret
creationPolicy: Owner # ESO가 Secret의 소유자로 관리
data:
- secretKey: api-token # K8s Secret의 key 이름
remoteRef:
key: secret/prod/cert-manager # Vault 경로
property: cf-api-token # Vault의 field 이름
동작 흐름
flowchart TD
A["ExternalSecret 생성\nk8s 리소스 선언"]
B["ESO → ClusterSecretStore 참조\nExternal Secrets Operator"]
C["Vault — Kubernetes Auth 인증\nServiceAccount 토큰으로 인증"]
D["secret/prod/cert-manager → cf-api-token 읽기\n↻ 1시간 주기 동기화"]
E["cloudflare-token-secret K8s Secret 생성/갱신\ncert-manager ClusterIssuer가 참조"]
F["Cloudflare DNS-01 → Let's Encrypt 인증서 발급"]
A --> B --> C --> D --> E --> F
Git에는 ExternalSecret 매니페스트만 올라가 있고, 실제 토큰 값은 Vault에서만 존재합니다. 이 패턴을 그대로 응용해 다른 시크릿(DB 비밀번호, OAuth Client Secret 등)도 동일하게 관리할 수 있습니다.
8. 마무리
Vault + External Secrets 조합으로 구성하면 위 흐름이 만들어집니다.
Git에는 민감한 값이 전혀 없고, 시크릿 갱신도 vault kv put 한 줄로 끝납니다. ESO가 다음 동기화 주기에 자동으로 K8s Secret을 업데이트해 주기 때문에 애플리케이션 재배포 없이 시크릿을 교체할 수 있습니다.