본문 바로가기
배포·CI,CD·클라우드

External Secrets로 시크릿 자동 주입: AWS Secrets Manager/KMS · Kubernetes 네이티브 시크릿 관리

by yamoojin83 2025. 11. 1.

External Secrets로 시크릿 자동 주입: AWS Secrets Manager/KMS · Kubernetes 네이티브 시크릿 관리

GitOps로 배포를 표준화했더라도 민감한 값(DB 비밀번호, API 키, OAuth 시크릿)을 Git에 넣을 수는 없습니다. 이 글에서는 External Secrets Operator(ESO)AWS Secrets Manager의 값을 Kubernetes Secret에 자동으로 동기화하는 방법을 정리합니다. 장점은 코드에는 참조만 남기고, 실제 값은 KMS로 암호화된 Vault(Secrets Manager)에 보관한다는 점입니다. PR 승격만으로도 환경별 시크릿을 안전하게 배포할 수 있습니다.


1) 개념 한 장 요약

[AWS Secrets Manager] --(IAM 자격)--> [ESO] --(주기 동기화)--> [K8s Secret] --mount/env--> [Pod]
  • SecretStore: 외부 비밀 저장소의 연결 정의(AWS, GCP, Azure, Vault 등)
  • ExternalSecret: 어떤 키를 어떻게 가져와 K8s Secret으로 만들지 선언
  • Refresh: 주기적으로 재동기화(회전 key 자동 반영)

2) 설치(Helm)

helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets -n external-secrets --create-namespace
kubectl get pods -n external-secrets

컨트롤러/웹훅/CRD가 설치됩니다.


3) AWS 자격 증명 연결(두 가지 방식)

3-1) IRSA(권장, EKS)

EKS의 ServiceAccount에 IAM Role을 연결해 키 없이 액세스합니다.

# (요지) IAM 정책: Secrets Manager 읽기
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["secretsmanager:GetSecretValue","secretsmanager:DescribeSecret","secretsmanager:ListSecrets"],
    "Resource": "arn:aws:secretsmanager:ap-northeast-2:123456789012:secret:prod/*"
  }]
}

해당 Role을 External Secrets 컨트롤러/네임스페이스의 ServiceAccount와 연결합니다.

3-2) 액세스키(테스트용)

Kubernetes Secret로 자격을 담고, ESO가 이를 사용해 AWS에 접근합니다(운영 비권장).


4) SecretStore/ClusterSecretStore 정의

여러 네임스페이스에서 공유하려면 ClusterSecretStore, 단일 네임스페이스면 SecretStore를 씁니다.

apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
  name: aws-sm
spec:
  provider:
    aws:
      service: SecretsManager
      region: ap-northeast-2
      auth:
        jwt:
          serviceAccountRef:
            name: external-secrets
            namespace: external-secrets

IRSA를 쓴다면 위처럼 jwt.serviceAccountRef만 지정하면 됩니다.


5) ExternalSecret로 값 매핑 선언

예: prod 네임스페이스의 애플리케이션이 db-secret이라는 K8s Secret을 쓰도록 만듭니다.

apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: db-secret
  namespace: prod
spec:
  refreshInterval: 1h
  secretStoreRef:
    kind: ClusterSecretStore
    name: aws-sm
  target:
    name: db-secret                      # 생성될 K8s Secret 이름
    creationPolicy: Owner
    template:
      type: Opaque
      metadata:
        labels:
          app: order-api
  data:
    - secretKey: DB_URL                   # K8s Secret 키 이름
      remoteRef:
        key: prod/db                      # AWS Secrets Manager의 시크릿 이름
        property: url                     # (JSON 속성일 때) 해당 필드만
    - secretKey: DB_USER
      remoteRef:
        key: prod/db
        property: user
    - secretKey: DB_PASSWORD
      remoteRef:
        key: prod/db
        property: password

Secrets Manager에 값이 {"url":"...","user":"...","password":"..."} 형태로 저장되어 있다면 각 property로 분해되어 K8s Secret에 반영됩니다.


6) Pod 마운트/주입

생성된 K8s Secret은 Env 또는 volumeMount로 주입합니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-api
  namespace: prod
spec:
  template:
    spec:
      containers:
        - name: app
          image: registry.example.com/order-api:2.0.0
          env:
            - name: DB_URL
              valueFrom: { secretKeyRef: { name: db-secret, key: DB_URL } }
            - name: DB_USER
              valueFrom: { secretKeyRef: { name: db-secret, key: DB_USER } }
            - name: DB_PASSWORD
              valueFrom: { secretKeyRef: { name: db-secret, key: DB_PASSWORD } }

7) 자동 회전/버전 고정/폴백

  • refreshInterval: 기본 1시간 등 주기로 자동 갱신(Secrets Manager에서 교체 즉시 반영 원하면 더 짧게)
  • version: 특정 버전에 고정 가능(릴리스 단계에서 “잠시” 사용)
  • fallback: 원격 실패 시 이전 값 유지(서비스 연속성)
spec:
  data:
    - secretKey: API_KEY
      remoteRef:
        key: prod/payment
        version: AWSCURRENT  # 또는 구체 버전 ID
        decodingStrategy: None

8) GitOps와의 결합(Argo CD)

Git에는 ExternalSecret 선언만 커밋합니다. 실제 민감한 값은 클라우드에 있고, Argo CD가 ExternalSecret을 동기화하면 ESO가 K8s Secret을 생성합니다.

  • 장점: Git에는 민감값이 없고, 감사 추적은 선언 변경에만 남습니다.
  • 승격: prod로 PR 머지 → ExternalSecret이 같은 키를 읽어 prod용 시크릿을 자동 주입

9) 운영 체크리스트

  • 권한 최소화: IAM 정책 리소스를 좁게(예: arn:...:secret:prod/order-api/*)
  • 네임스페이스 격리: 팀/서비스별로 나누고, rbac.authorization.k8s.io로 Secret 조회 권한 제한
  • 회전 자동화: Secrets Manager에서 주기 회전 → refreshInterval과 연동
  • 감사: ESO 컨트롤러 로그/이벤트로 동기화 상태 모니터링, 실패 알림(Alertmanager)
  • 백업: 원천 저장소(Secrets Manager) 레벨의 백업/버전 관리 의존

10) 문제 해결 빠른 분기

  • Secret이 안 생김kubectl describe externalsecret db-secret -n prodstatus 확인
  • 권한 오류 → IRSA Role에 GetSecretValue 포함 여부, 리소스 ARN 스코프 확인
  • 값 일부 누락property 경로 오타/JSON 구조 불일치
  • 회전 미반영refreshInterval 단축, 컨트롤러 로그에서 cache 확인

부록) Secrets Manager에 JSON 저장 팁

{
  "url": "jdbc:postgresql://prod-db.example.com:5432/shop",
  "user": "shop",
  "password": "s3cr3t",
  "pool": { "max": 20, "timeoutMs": 5000 }
}

애플리케이션이 동일 키로 여러 값을 필요로 할 때 JSON 구조로 저장하면 ExternalSecret에서 property로 잘라 쓰기 편합니다.


마무리

External Secrets를 쓰면 시크릿 소스 오브 트루스는 클라우드 보안 저장소에 두고, Kubernetes에는 필요할 때만 복사본을 주입할 수 있습니다. GitOps 선언에는 외부 경로만 남기기 때문에 검토/승격/롤백 프로세스가 단순해지면서도 보안이 강화됩니다. 지금 운영 중인 서비스 하나에라도 ExternalSecret을 적용해 보고, 점진적으로 모든 민감값을 외부 저장소로 이전해 보세요.