K8s External Secrets 운영

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을 통한 값 조합은 시크릿 로테이션 자동화의 핵심입니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux