External Secrets
External Secrets Operator
AWS Secrets Manager, HashiCorp Vault, Azure Key Vault 등 외부 비밀 저장소와 Kubernetes를 연동하여 Secret 리소스를 자동으로 동기화하는 Kubernetes 오퍼레이터입니다.
External Secrets Operator
AWS Secrets Manager, HashiCorp Vault, Azure Key Vault 등 외부 비밀 저장소와 Kubernetes를 연동하여 Secret 리소스를 자동으로 동기화하는 Kubernetes 오퍼레이터입니다.
External Secrets Operator(ESO)는 Kubernetes 환경에서 외부 비밀 관리 시스템과의 통합을 자동화하는 오픈소스 프로젝트입니다. CNCF Sandbox 프로젝트로, 기업 환경에서 중앙 집중화된 시크릿 관리를 Kubernetes 네이티브 방식으로 연동할 수 있게 해줍니다.
ESO의 핵심 개념은 ExternalSecret CRD(Custom Resource Definition)입니다. 이 리소스를 정의하면 ESO 컨트롤러가 외부 저장소에서 시크릿을 가져와 Kubernetes Secret으로 자동 생성하고, 주기적으로 동기화합니다. 이를 통해 GitOps 워크플로우에서 시크릿 값을 직접 노출하지 않고도 선언적으로 관리할 수 있습니다.
지원하는 백엔드로는 AWS Secrets Manager, AWS Parameter Store, HashiCorp Vault, Google Secret Manager, Azure Key Vault, IBM Cloud Secrets Manager, CyberArk Conjur, Doppler, 1Password 등이 있습니다. SecretStore 또는 ClusterSecretStore 리소스를 통해 백엔드 연결을 구성하며, 네임스페이스 단위 또는 클러스터 전체에서 재사용할 수 있습니다.
보안 모범 사례로는 최소 권한 원칙을 적용한 IAM 정책 설정, 주기적인 시크릿 로테이션 구성, 멀티 테넌트 환경에서의 SecretStore 분리 등이 있습니다. 또한 PushSecret 기능을 활용하면 Kubernetes Secret을 외부 저장소로 내보낼 수도 있어 양방향 동기화가 가능합니다.
# SecretStore 정의 - AWS Secrets Manager 연동
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: aws-secrets-store
namespace: production
spec:
provider:
aws:
service: SecretsManager
region: ap-northeast-2
auth:
jwt:
serviceAccountRef:
name: external-secrets-sa
---
# ExternalSecret 정의 - 외부 시크릿을 Kubernetes Secret으로 동기화
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: database-credentials
namespace: production
spec:
refreshInterval: 1h # 동기화 주기
secretStoreRef:
name: aws-secrets-store
kind: SecretStore
target:
name: db-secret # 생성될 Kubernetes Secret 이름
creationPolicy: Owner
template:
type: Opaque
data:
# 템플릿을 사용한 값 변환
connection-string: |
postgresql://{{ .username }}:{{ .password }}@{{ .host }}:5432/{{ .database }}
data:
- secretKey: username
remoteRef:
key: prod/database/credentials
property: username
- secretKey: password
remoteRef:
key: prod/database/credentials
property: password
- secretKey: host
remoteRef:
key: prod/database/credentials
property: host
- secretKey: database
remoteRef:
key: prod/database/credentials
property: database
from kubernetes import client, config
from kubernetes.client.rest import ApiException
import json
# Kubernetes 설정 로드
config.load_incluster_config() # 클러스터 내부에서 실행 시
# config.load_kube_config() # 로컬에서 실행 시
# Custom Objects API 클라이언트
api = client.CustomObjectsApi()
def get_external_secret_status(namespace: str, name: str) -> dict:
"""ExternalSecret 리소스의 동기화 상태 확인"""
try:
external_secret = api.get_namespaced_custom_object(
group="external-secrets.io",
version="v1beta1",
namespace=namespace,
plural="externalsecrets",
name=name
)
status = external_secret.get("status", {})
conditions = status.get("conditions", [])
# SecretSynced 조건 확인
for condition in conditions:
if condition["type"] == "Ready":
return {
"name": name,
"namespace": namespace,
"synced": condition["status"] == "True",
"reason": condition.get("reason", "Unknown"),
"message": condition.get("message", ""),
"last_sync": status.get("refreshTime")
}
return {"name": name, "synced": False, "reason": "NoCondition"}
except ApiException as e:
return {"name": name, "error": str(e)}
def list_all_external_secrets() -> list:
"""클러스터 내 모든 ExternalSecret 상태 조회"""
results = []
external_secrets = api.list_cluster_custom_object(
group="external-secrets.io",
version="v1beta1",
plural="externalsecrets"
)
for es in external_secrets.get("items", []):
metadata = es["metadata"]
status = get_external_secret_status(
namespace=metadata["namespace"],
name=metadata["name"]
)
results.append(status)
return results
def check_secret_sync_health() -> dict:
"""전체 ExternalSecret 동기화 상태 헬스체크"""
all_secrets = list_all_external_secrets()
total = len(all_secrets)
synced = sum(1 for s in all_secrets if s.get("synced", False))
failed = [s for s in all_secrets if not s.get("synced", False)]
return {
"total": total,
"synced": synced,
"failed_count": len(failed),
"failed_secrets": failed,
"health": "healthy" if len(failed) == 0 else "degraded"
}
# 사용 예시
if __name__ == "__main__":
# 특정 ExternalSecret 상태 확인
status = get_external_secret_status("production", "database-credentials")
print(f"동기화 상태: {status}")
# 전체 헬스체크
health = check_secret_sync_health()
print(f"전체 상태: {health['synced']}/{health['total']} 동기화됨")
if health["failed_secrets"]:
print("동기화 실패한 시크릿:")
for secret in health["failed_secrets"]:
print(f" - {secret['namespace']}/{secret['name']}: {secret.get('reason')}")