External Secrets Operator란?
External Secrets Operator(ESO)는 AWS Secrets Manager, HashiCorp Vault, GCP Secret Manager 등 외부 시크릿 저장소의 값을 Kubernetes Secret으로 자동 동기화하는 오퍼레이터입니다. 기존 K8s Secret은 Base64 인코딩일 뿐 암호화가 아니며, Git에 커밋하면 그대로 노출됩니다. ESO를 사용하면 시크릿의 원본을 외부 저장소에서 관리하면서, 클러스터 내에서는 일반 Secret처럼 투명하게 사용할 수 있습니다.
이 글에서는 ESO 아키텍처, 주요 Provider 연동, 시크릿 자동 갱신, 그리고 운영 시 보안 베스트 프랙티스까지 깊이 있게 다루겠습니다.
왜 External Secrets인가?
| 방식 | 장점 | 단점 |
|---|---|---|
| K8s Secret (평문) | 간단, 추가 도구 불필요 | Git 노출 위험, 암호화 미지원 |
| SealedSecrets | Git 커밋 가능 (암호화) | 클러스터 키 의존, 자동 갱신 없음 |
| SOPS + Kustomize | GitOps 친화적 | CI 파이프라인 복잡도 증가 |
| External Secrets | 중앙 관리, 자동 갱신, 멀티 Provider | 오퍼레이터 운영 필요 |
아키텍처와 핵심 CRD
# ESO의 핵심 리소스 3가지:
#
# 1. SecretStore / ClusterSecretStore
# → 외부 시크릿 저장소 연결 정보
#
# 2. ExternalSecret
# → "어떤 시크릿을 가져올지" 선언
#
# 3. Kubernetes Secret
# → ESO가 자동 생성/갱신하는 결과물
#
# 흐름:
# SecretStore (Vault/AWS 연결)
# ↓
# ExternalSecret (매핑 정의)
# ↓
# K8s Secret (자동 생성 + 주기적 동기화)
설치
# Helm으로 설치
helm repo add external-secrets https://charts.external-secrets.io
helm repo update
helm install external-secrets external-secrets/external-secrets
-n external-secrets
--create-namespace
--set installCRDs=true
--set webhook.port=9443
# 설치 확인
kubectl get pods -n external-secrets
# external-secrets-xxxxx 1/1 Running
# external-secrets-cert-controller 1/1 Running
# external-secrets-webhook 1/1 Running
AWS Secrets Manager 연동
# 1. IAM 인증용 Secret 생성 (또는 IRSA 사용)
kubectl create secret generic aws-credentials
--from-literal=access-key=AKIAIOSFODNN7EXAMPLE
--from-literal=secret-key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
-n default
---
# 2. SecretStore 정의
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secrets-manager
namespace: default
spec:
provider:
aws:
service: SecretsManager
region: ap-northeast-2
auth:
secretRef:
accessKeyIDSecretRef:
name: aws-credentials
key: access-key
secretAccessKeySecretRef:
name: aws-credentials
key: secret-key
---
# 3. ExternalSecret으로 시크릿 매핑
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-database-credentials
namespace: default
spec:
refreshInterval: 1h # 1시간마다 동기화
secretStoreRef:
name: aws-secrets-manager
kind: SecretStore
target:
name: database-credentials # 생성될 K8s Secret 이름
creationPolicy: Owner # ExternalSecret 삭제 시 Secret도 삭제
data:
- secretKey: DB_HOST # K8s Secret의 key
remoteRef:
key: prod/database # AWS Secrets Manager의 시크릿 이름
property: host # JSON 내 특정 필드
- secretKey: DB_PASSWORD
remoteRef:
key: prod/database
property: password
- secretKey: DB_USERNAME
remoteRef:
key: prod/database
property: username
HashiCorp Vault 연동
# Vault SecretStore (Kubernetes Auth 방식)
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore # 클러스터 전역 사용 가능
metadata:
name: vault-store
spec:
provider:
vault:
server: "https://vault.internal:8200"
path: "secret"
version: "v2" # KV v2 엔진
auth:
kubernetes:
mountPath: "kubernetes"
role: "external-secrets"
serviceAccountRef:
name: "external-secrets"
namespace: "external-secrets"
---
# Vault의 시크릿을 K8s Secret으로 동기화
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: api-keys
namespace: production
spec:
refreshInterval: 30m
secretStoreRef:
name: vault-store
kind: ClusterSecretStore # 클러스터 레벨 참조
target:
name: api-keys
data:
- secretKey: STRIPE_SECRET_KEY
remoteRef:
key: secret/data/production/stripe
property: secret_key
- secretKey: SENDGRID_API_KEY
remoteRef:
key: secret/data/production/sendgrid
property: api_key
dataFrom: 전체 시크릿 매핑
# 개별 키 매핑 대신 전체 JSON을 한 번에 가져오기
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-config
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: SecretStore
target:
name: app-config
dataFrom:
- extract:
key: prod/app-config
# AWS에 저장된 JSON:
# {"DB_HOST":"...", "DB_PORT":"5432", "REDIS_URL":"..."}
# → K8s Secret에 모든 키-값 자동 매핑
- find:
# 패턴으로 여러 시크릿 한 번에 가져오기
name:
regexp: "prod/microservice-.*"
tags:
environment: production
시크릿 템플릿
# 가져온 값을 조합하여 새로운 형태로 변환
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: database-url
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: SecretStore
target:
name: database-url
template:
engineVersion: v2
data:
# 여러 값을 조합하여 Connection String 생성
DATABASE_URL: "postgresql://{{ .username }}:{{ .password }}@{{ .host }}:{{ .port }}/{{ .database }}?sslmode=require"
# .env 파일 형태로도 생성 가능
config.env: |
DB_HOST={{ .host }}
DB_PORT={{ .port }}
DB_NAME={{ .database }}
data:
- secretKey: username
remoteRef:
key: prod/database
property: username
- secretKey: password
remoteRef:
key: prod/database
property: password
- secretKey: host
remoteRef:
key: prod/database
property: host
- secretKey: port
remoteRef:
key: prod/database
property: port
- secretKey: database
remoteRef:
key: prod/database
property: database
Pod에서 사용
# ESO가 생성한 Secret은 일반 K8s Secret과 동일하게 사용
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-api
spec:
template:
spec:
containers:
- name: app
image: payment-api:v2.0
envFrom:
- secretRef:
name: database-credentials
- secretRef:
name: api-keys
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: database-url
key: DATABASE_URL
자동 갱신과 롤링 업데이트
# 시크릿 갱신 시 Pod 자동 재시작 (Stakater Reloader 연동)
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-api
annotations:
# Reloader: Secret 변경 감지 → 롤링 업데이트
reloader.stakater.com/auto: "true"
spec:
template:
spec:
containers:
- name: app
envFrom:
- secretRef:
name: database-credentials
# 또는 체크섬 기반 수동 트리거
# Pod 템플릿에 annotation으로 시크릿 해시 포함
# → 시크릿 변경 시 해시 변경 → 자동 롤아웃
모니터링과 디버깅
# ExternalSecret 상태 확인
kubectl get externalsecrets -A
# NAME STORE REFRESH STATUS
# app-database-credentials aws-secrets-manager 1h SecretSynced
# api-keys vault-store 30m SecretSynced
# broken-secret aws-secrets-manager 1h SecretSyncedError
# 상세 에러 확인
kubectl describe externalsecret broken-secret
# Events:
# Warning UpdateFailed Secret does not exist:
# ResourceNotFoundException: Secrets Manager can't find
# the specified secret.
# SecretStore 연결 상태 확인
kubectl get secretstores -A
# NAME AGE STATUS CAPABILITIES READY
# aws-secrets-manager 5d Valid ReadOnly True
# Prometheus 메트릭 (Grafana 대시보드용)
# external_secrets_sync_calls_total
# external_secrets_sync_calls_error
# external_secrets_status_condition
보안 베스트 프랙티스
| 항목 | 권장 사항 |
|---|---|
| 인증 | IRSA(AWS), Workload Identity(GCP) 사용 — 정적 키 지양 |
| 범위 | SecretStore를 네임스페이스별 분리, ClusterSecretStore 최소화 |
| RBAC | ExternalSecret 생성 권한을 팀별로 제한 |
| 갱신 주기 | 민감도에 따라 15m~24h, API 요금 고려 |
| etcd 암호화 | K8s Secret은 여전히 etcd에 저장 → EncryptionConfiguration 필수 |
| 감사 | 외부 저장소의 접근 로그 모니터링 |
마무리
External Secrets Operator는 Kubernetes 시크릿 관리의 사실상 표준으로 자리 잡았습니다. 외부 저장소를 Single Source of Truth로 두고 ESO가 동기화를 담당하면, GitOps 워크플로우에서 시크릿을 안전하게 관리하면서도 개발자 경험을 해치지 않습니다. 특히 refreshInterval을 통한 자동 갱신과 template을 통한 값 조합은 시크릿 로테이션 자동화의 핵심입니다.