Vault 없이 EKS 멀티클러스터 시크릿을 자동 동기화하는 구조 — AWS Secrets Manager + IRSA + ESO 실전 연동
Vault 얘기가 나올 때마다 팀 내에서 슬쩍 눈치를 보게 되는 분들 계시죠? 저도 그랬습니다. HA 구성에 언씰 자동화, 백업 정책, 장애 대응 런북까지… Vault 자체가 또 하나의 중요 인프라가 되어버리는 순간, "이걸 꼭 써야 하나?"라는 생각이 드는 건 자연스러운 반응입니다.
AWS 환경이라면 이 고민을 다르게 접근할 수 있습니다. External Secrets Operator(ESO)와 AWS Secrets Manager를 조합하면, Vault 없이도 EKS 멀티클러스터 시크릿 동기화 파이프라인을 프로덕션 수준으로 구축할 수 있습니다. AWS Secrets Manager는 AWS가 SLA를 보장하는 매니지드 서비스라 직접 운영할 서버가 없고, IRSA(IAM Roles for Service Accounts)로 인증하면 정적 Access Key를 클러스터 어디에도 두지 않아도 됩니다. ESO는 ExternalSecret이라는 Kubernetes 리소스를 통해 Secrets Manager의 값을 Kubernetes 네이티브 Secret으로 자동 동기화해줍니다. 이 용어들이 낯설어도 괜찮습니다. 아래에서 차근차근 풀어드립니다.
이 글에서는 "왜 IRSA인가", "ClusterSecretStore와 ExternalSecret은 어떻게 연결되는가", "멀티클러스터에서 어떤 IAM 구조를 잡아야 하는가" 에 대한 그림을 함께 그려보겠습니다. EKS를 운영해본 경험이 있다면 바로 따라올 수 있고, Kubernetes가 처음이신 분도 개념 섹션을 읽고 나면 전체 흐름은 파악할 수 있을 겁니다.
핵심 개념
External Secrets Operator가 하는 일
ESO는 Kubernetes 오퍼레이터입니다. AWS Secrets Manager, GCP Secret Manager, Azure Key Vault 같은 외부 시크릿 스토어에서 값을 읽어, 클러스터 안의 네이티브 Secret 오브젝트로 자동 동기화해줍니다. 핵심은 이 동기화가 선언적이라는 점입니다. 어떤 외부 키를 어떤 Kubernetes Secret으로 만들지 YAML로 선언해두면, ESO 컨트롤러가 주기적으로 API를 호출해 값을 맞춰줍니다.
ESO가 제공하는 CRD는 세 가지입니다.
| 리소스 | 범위 | 역할 |
|---|---|---|
SecretStore |
네임스페이스 | 특정 네임스페이스 전용 스토어 연결 |
ClusterSecretStore |
클러스터 전체 | 모든 네임스페이스가 공유하는 스토어 |
ExternalSecret |
네임스페이스 | 외부 시크릿 키 → Kubernetes Secret 매핑 |
멀티클러스터 환경에서는 ClusterSecretStore를 주로 씁니다. 클러스터 하나에 스토어 설정을 한 번만 해두면, 모든 네임스페이스의 ExternalSecret이 이를 참조할 수 있거든요.
CNCF Sandbox 프로젝트: ESO는 현재 CNCF Sandbox에 소속된 프로젝트로, v0.9~v0.10 계열에서 빠르게 안정화되고 있습니다.
PushSecret(Kubernetes → 외부 방향 동기화), Generator 리소스, 향상된 메트릭 등이 최근 추가됐습니다.
IRSA가 인증을 어떻게 해결하는가
솔직히 처음 IRSA를 접했을 때 "이게 정말 되는 건가?" 싶었습니다. Access Key도 없이 IAM 역할을 assume한다니 마법처럼 느껴졌거든요.
원리는 이렇습니다. EKS 클러스터는 OIDC(OpenID Connect) 공급자를 가집니다. Kubernetes ServiceAccount에 IAM 역할 ARN을 애노테이션으로 달면, 그 ServiceAccount로 실행되는 파드가 발급받는 토큰을 AWS STS가 OIDC를 통해 검증합니다. 검증이 통과되면 sts:AssumeRoleWithWebIdentity로 임시 자격증명이 발급됩니다.
ESO Pod
└─ ServiceAccount (annotated: eks.amazonaws.com/role-arn)
└─ EKS OIDC Provider가 토큰 서명
└─ AWS STS가 검증 → 임시 자격증명 발급
└─ Secrets Manager API 호출 가능이 흐름을 이해하면 IAM 신뢰 정책(Trust Policy)과 권한 정책(Permission Policy)을 각각 어디에 두어야 하는지도 자연스럽게 파악됩니다. 아래 예시에서 두 가지 모두 코드로 보여드립니다.
EKS Pod Identity (2024~): AWS는 2023년 말 IRSA의 후속으로 EKS Pod Identity를 출시했습니다. OIDC 공급자를 별도 설정할 필요 없이 IAM 역할을 파드에 직접 연결합니다. 신규 클러스터라면 Pod Identity를 권장하며, ESO도 공식 지원합니다. 이 글에서는 현재 가장 널리 쓰이는 IRSA 기준으로 설명하지만, 구조적 흐름은 동일합니다.
멀티클러스터 동기화 흐름 한눈에 보기
dev / staging / prod 세 클러스터가 있다고 가정해보겠습니다. 각 클러스터는 독립된 EKS이고, 시크릿은 하나의 AWS Secrets Manager에 중앙화합니다.
AWS Secrets Manager (중앙)
├─ prod/my-app/database
├─ staging/my-app/database
└─ dev/my-app/database
EKS-prod EKS-staging EKS-dev
├─ ESO (Helm 설치) ├─ ESO ├─ ESO
├─ ClusterSecretStore ├─ ClusterSecretStore ├─ ClusterSecretStore
│ └─ IRSA 역할-prod │ └─ IRSA 역할-staging │ └─ IRSA 역할-dev
└─ ExternalSecret └─ ExternalSecret └─ ExternalSecret
→ k8s Secret db-secret → k8s Secret db-secret → k8s Secret db-secret각 클러스터의 IRSA 역할에는 해당 환경 경로의 시크릿만 읽을 수 있는 IAM 정책을 붙입니다. prod 클러스터가 dev 경로를 읽지 못하게 하는 건 IAM 정책 레벨에서 해결합니다.
실전 적용
아래 네 가지 패턴은 각각 독립적으로 사용할 수 있습니다. 자신의 상황에 맞는 섹션부터 골라 읽으셔도 됩니다.
기본 패턴: 단일 계정에서 여러 클러스터 연결하기
ESO를 처음 도입할 때 가장 많이 만나는 구성입니다. 동일 AWS 계정 내 dev/staging/prod 클러스터가 공통 Secrets Manager를 바라봅니다. 인프라 초기 셋업이나 소규모 팀에서 빠르게 시작하기 좋은 출발점입니다.
1단계: IRSA 역할 생성
ESO ServiceAccount에 붙일 IAM 역할이 필요합니다. 역할에는 두 가지 정책이 있습니다.
신뢰 정책(Trust Policy) — 어떤 ServiceAccount가 이 역할을 assume할 수 있는지 정의합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::<ACCOUNT-ID>:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/<OIDC-ID>"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.ap-northeast-2.amazonaws.com/id/<OIDC-ID>:sub": "system:serviceaccount:external-secrets:external-secrets",
"oidc.eks.ap-northeast-2.amazonaws.com/id/<OIDC-ID>:aud": "sts.amazonaws.com"
}
}
}
]
}권한 정책(Permission Policy) — ESO가 Secrets Manager에서 실제로 무엇을 할 수 있는지 정의합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret",
"secretsmanager:ListSecretVersionIds"
],
"Resource": "arn:aws:secretsmanager:ap-northeast-2:<ACCOUNT-ID>:secret:prod/*"
}
]
}Resource 경로를 환경별로 제한(prod/*, staging/*, dev/*)하면 클러스터 간 시크릿 격리가 IAM 레벨에서 자연스럽게 됩니다.
2단계: ESO Helm 설치
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets \
external-secrets/external-secrets \
-n external-secrets \
--create-namespace \
--values eso-values.yamlHelm --set 플래그로 eks.amazonaws.com/role-arn 애노테이션을 직접 전달하면 셸(bash, zsh, fish)마다 이스케이프 처리가 달라 실패하는 경우가 꽤 있습니다. values.yaml을 따로 만드는 쪽이 훨씬 안정적입니다.
# eso-values.yaml
serviceAccount:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::<ACCOUNT-ID>:role/eso-role-prod3단계: ClusterSecretStore 정의
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: aws-secrets-manager
spec:
provider:
aws:
service: SecretsManager
region: ap-northeast-2
auth:
jwt:
serviceAccountRef:
name: external-secrets
namespace: external-secrets| 필드 | 설명 |
|---|---|
service: SecretsManager |
AWS Secrets Manager를 provider로 지정 |
auth.jwt.serviceAccountRef |
IRSA가 설정된 ServiceAccount 참조 |
region |
시크릿이 저장된 리전 |
4단계: ExternalSecret으로 Kubernetes Secret 생성
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-db-credentials
namespace: my-app
spec:
refreshInterval: 1h
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: db-secret
creationPolicy: Owner
data:
- secretKey: DB_PASSWORD
remoteRef:
key: prod/my-app/database
property: password
- secretKey: DB_USERNAME
remoteRef:
key: prod/my-app/database
property: username| 필드 | 설명 |
|---|---|
refreshInterval |
Secrets Manager API를 호출하는 주기 (비용과 실시간성 트레이드오프) |
secretStoreRef |
참조할 ClusterSecretStore 이름 |
target.creationPolicy: Owner |
ESO가 Secret 소유자가 되어 ExternalSecret 삭제 시 함께 정리 |
remoteRef.key |
Secrets Manager의 시크릿 이름 |
remoteRef.property |
JSON 시크릿 내 특정 키 |
이렇게 하면 my-app 네임스페이스에 db-secret이라는 Kubernetes Secret이 자동 생성됩니다. refreshInterval마다 Secrets Manager의 최신 값으로 갱신됩니다.
비용 계산 예시:
ExternalSecret50개에refreshInterval: 30m을 설정하면 하루 2,400회, 월 약 72,000 API 호출이 발생합니다. Secrets Manager API는 10,000호출당 $0.05이므로 API 호출 비용은 월 약 $0.36입니다. 시크릿 저장 비용(시크릿당 월 $0.40)과 합산해 자신의 규모로 대입해보시면 됩니다.
크로스 계정 패턴: 허브-스포크 구조로 중앙 시크릿 관리하기
팀별 또는 환경별로 AWS 계정이 분리된 경우에 딱 맞는 구성입니다. 이 패턴을 처음 구현할 때 IAM 역할 체이닝 개념 때문에 좀 헤맸는데, 구조만 이해하면 생각보다 단순합니다.
[각 클러스터 계정] [중앙 시크릿 계정]
IRSA 역할 (<CLUSTER-ACCOUNT-ID>) → 중앙 역할 (<CENTRAL-ACCOUNT-ID>)
└─ sts:AssumeRole 허용 └─ secretsmanager:GetSecretValue 등중앙 계정 역할 — 신뢰 정책
각 클러스터 계정의 IRSA 역할을 Principal로 등록합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::<CLUSTER-A-ACCOUNT-ID>:role/eso-role-cluster-a",
"arn:aws:iam::<CLUSTER-B-ACCOUNT-ID>:role/eso-role-cluster-b"
]
},
"Action": "sts:AssumeRole"
}
]
}중앙 계정 역할 — 권한 정책
신뢰 정책만 있으면 역할을 assume할 수는 있지만 아무것도 할 수 없습니다. Secrets Manager 접근 권한도 함께 붙여줘야 합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret",
"secretsmanager:ListSecretVersionIds"
],
"Resource": "arn:aws:secretsmanager:ap-northeast-2:<CENTRAL-ACCOUNT-ID>:secret:*"
}
]
}ClusterSecretStore에 role 필드 추가
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: aws-secrets-manager-central
spec:
provider:
aws:
service: SecretsManager
region: ap-northeast-2
role: arn:aws:iam::<CENTRAL-ACCOUNT-ID>:role/eso-secrets-reader
auth:
jwt:
serviceAccountRef:
name: external-secrets
namespace: external-secretsrole 필드 하나를 추가하는 것만으로 ESO가 다른 계정의 IAM 역할을 assume해 시크릿을 읽어옵니다.
GitOps 통합 패턴: ArgoCD와 함께 ExternalSecret 배포하기
요즘 실무에서 가장 자주 보이는 조합입니다. ArgoCD로 GitOps를 운영하는데 "시크릿 값을 Git에 넣을 수도 없고, 그렇다고 수동으로 따로 관리하기도 번거롭고"라는 고민이 생길 때 딱 맞는 패턴입니다.
ExternalSecret YAML에는 시크릿 참조만 있고 실제 값은 없기 때문에 Git에 커밋해도 민감 정보가 노출되지 않습니다.
Git Repository
└─ manifests/
├─ deployment.yaml
├─ service.yaml
└─ external-secret.yaml ← 민감 정보 없음, Git 저장 안전
ArgoCD가 external-secret.yaml 배포
└─ ESO가 Secrets Manager에서 값 pull
└─ Kubernetes Secret 자동 생성# external-secret.yaml (Git에 저장되는 파일)
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-secrets
namespace: production
annotations:
argocd.argoproj.io/sync-wave: "-1"
spec:
refreshInterval: 30m
secretStoreRef:
name: aws-secrets-manager
kind: ClusterSecretStore
target:
name: app-secrets
creationPolicy: Owner
dataFrom:
- extract:
key: prod/my-app/all-secrets
dataFrom.extract: 개별data항목 대신 JSON 시크릿의 모든 키-값을 한 번에 Kubernetes Secret으로 변환합니다. 키가 10개 넘어가면 개별data항목으로 하나씩 나열하는 것보다 훨씬 편합니다.
시크릿 교체 자동화: Reloader로 파드 재시작 자동화하기
AWS Secrets Manager에서 시크릿이 교체되면 ESO가 다음 refreshInterval에 Kubernetes Secret을 갱신합니다. 그런데 여기서 중요한 점이 하나 있습니다.
시크릿 주입 방식에 따라 재시작 필요 여부가 달라집니다. 시크릿을 볼륨으로 마운트한 경우에는 kubelet이 파일을 자동으로 갱신하기 때문에 파드 재시작이 필요하지 않습니다. 반면 환경변수(env.valueFrom.secretKeyRef)로 주입했다면, 파드가 시작될 때 값을 한 번만 읽어오기 때문에 Secret이 바뀌어도 파드는 계속 이전 값을 씁니다. 이 경우에만 재시작이 필요합니다.
Reloader를 함께 사용하면 환경변수 주입 케이스에서도 Secret 변경을 감지해 자동으로 롤링 재시작을 처리해줍니다.
helm install reloader stakater/reloader -n reloader --create-namespaceapiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
annotations:
secret.reloader.stakater.com/reload: "app-secrets"
spec:
# ...app-secrets가 변경되면 Reloader가 감지해 연결된 Deployment를 롤링 재시작합니다. 자격증명 교체 주기를 짧게 가져가면서 다운타임 없이 시크릿을 갱신하는 패턴입니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| Vault 운영 부담 제거 | HA 구성, 언씰, 백업 없이 AWS 매니지드 서비스 활용 |
| 정적 자격증명 Zero | IRSA/Pod Identity로 Access Key 불필요. CloudTrail로 모든 API 호출 감사 가능 |
| 자동 동기화 | refreshInterval로 시크릿 교체가 클러스터에 자동 반영 |
| 멀티 네임스페이스 중앙화 | ClusterSecretStore 하나로 모든 네임스페이스가 동일 스토어 참조 |
| GitOps 친화적 | ExternalSecret은 민감 값 없는 순수 참조 오브젝트로 Git 저장 안전 |
| Provider 확장성 | GCP/Azure 등으로 전환 시 인터페이스 변경 없이 provider만 교체 |
단점 및 주의사항
| 항목 | 내용 | 대응 방안 |
|---|---|---|
| AWS 비용 | 시크릿당 월 $0.40 + API 호출당 $0.05/10,000회 | refreshInterval을 적절히 늘리거나 단순 값은 SSM Parameter Store(Standard Tier 무료) 고려 |
| 동기화 지연 | refreshInterval만큼 최대 지연 발생 |
긴급 시 kubectl annotate externalsecret app-secrets force-sync=$(date +%s) -n my-app으로 수동 트리거 |
| 클러스터별 설치 반복 | ESO Helm 배포 + IRSA 구성을 클러스터마다 수행 | Terraform 모듈 또는 ArgoCD ApplicationSet으로 자동화 |
| OIDC 공급자 관리 | 클러스터마다 OIDC 엔드포인트가 달라 IAM 신뢰 정책에 각각 등록 필요 | 신규 클러스터는 EKS Pod Identity로 전환해 복잡도 해소 |
| ClusterSecretStore 권한 범위 | 잘못 구성하면 모든 네임스페이스가 민감 시크릿 접근 가능 | namespaceSelector 또는 명시적 namespaces 목록으로 접근 범위 제한 |
SSM Parameter Store: AWS Systems Manager의 파라미터 저장소로, Standard Tier는 무료입니다. 단순한 문자열 값 저장에 적합하며, ESO에서
service: ParameterStore로 전환해 사용할 수 있습니다. JSON 구조의 시크릿이나 자동 교체 기능이 필요하다면 Secrets Manager가 더 적합합니다.
실무에서 가장 흔한 실수
1. ClusterSecretStore의 ServiceAccount 네임스페이스를 잘못 지정하는 경우
저도 초기에 serviceAccountRef의 namespace를 애플리케이션 네임스페이스(my-app)로 적어서 30분을 날린 적이 있습니다. 여기에 들어가는 값은 ESO가 설치된 네임스페이스(external-secrets)여야 합니다. 앱 네임스페이스를 적으면 Secret이 생성되지 않고 SecretSyncError 상태로만 남게 됩니다.
2. IAM 역할 신뢰 정책에 OIDC 조건을 빠뜨리는 경우
StringEquals 조건에 system:serviceaccount:external-secrets:external-secrets처럼 정확한 ServiceAccount를 지정하지 않으면, 해당 OIDC 공급자의 어떤 ServiceAccount든 역할을 assume할 수 있게 됩니다. "일단 동작하니까"라고 넘어가기 쉬운 포인트인데, 의도치 않은 권한 확대로 이어질 수 있어서 조건을 명시하는 것을 권장합니다.
3. refreshInterval을 너무 짧게 설정하는 경우
개발 환경에서 테스트하면서 refreshInterval: 30s로 설정해두고 잊어버리면, 수십 개의 ExternalSecret이 분당 수백 번 API를 호출하게 됩니다. 저도 한번 그랬는데 AWS 청구서를 보고서야 알아챘습니다. 기본값인 1h부터 시작해 필요에 따라 조정하는 것을 권장합니다.
마치며
이 구조를 실제로 운영에 올린 뒤 가장 먼저 체감되는 변화는, 배포 파이프라인에서 시크릿 관련 슬랙 알림이 조용히 사라지는 것이었습니다. "DB 비밀번호 바꿨는데 클러스터에 반영이 안 됐다"는 류의 메시지가 없어지고, 시크릿 교체가 그냥 알아서 잘 됩니다.
ESO + AWS Secrets Manager + IRSA 조합은 Vault 없이도 EKS 환경에서 프로덕션 수준의 시크릿 관리 파이프라인을 구축할 수 있는 현실적인 선택지입니다. 아직 시도해보지 않으셨다면, 아래 순서로 시작해볼 수 있습니다.
1단계: ESO Helm 설치
helm repo add external-secrets https://charts.external-secrets.io
helm install external-secrets external-secrets/external-secrets \
-n external-secrets --create-namespaceIRSA 없이 먼저 accessKeyID/secretAccessKey를 Kubernetes Secret으로 직접 주입하는 방식으로도 동작 확인이 가능합니다. 완전한 IRSA 설정 전에 ESO 자체의 동작을 빠르게 확인하고 싶을 때 유용한 방법입니다.
2단계: 테스트 시크릿 연동
AWS 콘솔에서 dev/test/myapp이라는 이름으로 JSON 시크릿을 하나 생성한 뒤, 위 예시의 ExternalSecret YAML에서 remoteRef.key를 그 경로로 바꿔 적용해볼 수 있습니다.
kubectl get secret db-secret -o jsonpath='{.data.DB_PASSWORD}' | base64 -d이 값이 Secrets Manager에 넣은 값과 일치하면 동기화가 정상적으로 동작하는 겁니다.
3단계: IRSA 구성으로 정적 자격증명 제거
EKS 클러스터에 OIDC 공급자를 활성화하고, eksctl create iamserviceaccount 또는 Terraform의 aws_iam_role로 IRSA 역할을 만든 뒤 ESO ServiceAccount에 역할 ARN을 애노테이션으로 달면 됩니다. 이 단계까지 완료되면 클러스터 어디에도 AWS 자격증명이 존재하지 않는 상태가 됩니다.
참고 자료
- External Secrets Operator 공식 문서 — AWS Secrets Manager Provider
- ESO Security Best Practices 공식 가이드
- EKS Workshop — External Secrets Operator
- AWS 공식 블로그 — AWS Secrets Manager와 ABAC를 활용한 EKS 시크릿 관리
- AWS Containers 블로그 — ESO with EKS Fargate
- GitHub — external-secrets/external-secrets
- Securing GitOps with External Secrets Operator & AWS Secrets Manager | Codefresh — ESO v0.9+ 기준
- Secrets Auto-Rotation in Kubernetes with AWS Secrets Manager and Reloader | Medium — ESO v0.9+ 기준
- ESO on Amazon EKS — Terraform-first 가이드 | Medium — ESO v0.9+ 기준