이번에는 쿠버네티스 Secrets를 AWS Secrets Manager와 연동하여 사용하는 작업을 학습해봅니다.
https://www.eksworkshop.com/docs/security/secrets-management/secrets-manager/
Managing Secrets with AWS Secrets Manager | EKS Workshop
Provide sensitive configuration like credentials to applications running on Amazon Elastic Kubernetes Service with AWS Secrets Manager.
www.eksworkshop.com
핵심 개념
Kubernetes 시크릿은 클러스터 운영자가 비밀번호, OAuth 토큰, SSH 키 등의 민감한 정보를 관리할 수 있게 도와주는 리소스입니다. 이러한 시크릿은 Pod 내 컨테이너에 데이터 볼륨으로 마운트되거나 환경 변수로 노출될 수 있어, Pod 배포와 민감한 데이터 관리를 분리할 수 있습니다.
시크릿 관리의 문제점
최근 트랜드에서는 GitOps를 중심으로 Kubernetes 리소스의 YAML 매니페스트를 관리하고 Git 저장소를 사용해 버전 관리하는 경우가 많아졌습니다. 그런데 Kubernetes에서 Secrets의 경우 암호화된다고 하지만 base64로 인코딩되어, 누구든지 base64로 디코딩하면 값을 취득할 수 있습니다. 이것을 Git에 올리는 것은 매우 위험한 행위죠. 이런 이유로 Kubernetes 시크릿을 위한 YAML 매니페스트를 클러스터 외부에서 관리하기는 어렵기 때문에 AWS Secrets Manager를 통해서 관리하고자 하는 것입니다.
시크릿 관리 접근 방식
일반적으로 시크릿을 외부저장소에 두는 방법은 다음 두 가지가 있으며, 이번 워크샵은 AWS Secrets Manager를 이용하고자 합니다.
- Sealed Secrets for Kubernetes: 시크릿을 암호화하여 안전하게 저장하고 관리하는 방법
- AWS Secrets Manager: AWS의 관리형 서비스를 사용하여 시크릿을 안전하게 저장하고 관리하는 방법
환경 준비 (EKS 클러스터에 다음과 같은 애드온이 필요합니다)
- kubernetes Secrets Store CSI Driver
- AWS Secrets and Configuration Provider(ASCP)
- External Secrets Operator
Kubernetes Secrets Store CSI Driver와 ASCP를 통해 Secrets Manager에 저장된 시크릿을 Kubernetes Pod의 볼륨으로 마운트 할 수 있습니다.
작동 방식
AWS Secrets and Configuration Provider(ASCP)를 사용하면 Amazon EKS에서 실행되는 워크로드가 IAM 역할과 정책을 통한 세밀한 접근 제어를 사용하여 Secrets Manager에 저장된 시크릿에 접근할 수 있습니다. Pod가 시크릿에 접근을 요청하면 ASCP는 다음과 같은 과정을 거칩니다:
- Pod의 ID를 검색
- 이 ID를 IAM 역할로 교환
- 해당 역할을 맡음
- Secrets Manager에서 해당 역할에 허용된 시크릿만 검색
External Secrets 접근 방식
Kubernetes와 AWS Secrets Manager를 통합하는 또 다른 방법은 External Secrets Operator를 사용하는 것입니다. 이 운영자는 AWS Secrets Manager의 시크릿을 Kubernetes 시크릿으로 동기화하며, 추상화 레이어를 통해 전체 수명 주기를 관리합니다. Secrets Manager의 값을 자동으로 Kubernetes 시크릿에 주입합니다.
https://external-secrets.io/latest/
Introduction - External Secrets Operator
Introduction External Secrets Operator is a Kubernetes operator that integrates external secret management systems like AWS Secrets Manager, HashiCorp Vault, Google Secrets Manager, Azure Key Vault, IBM Cloud Secrets Manager, CyberArk Conjur, Pulumi ESC an
external-secrets.io

자동 시크릿 교체 지원
두 접근 방식 모두 Secrets Manager를 통한 자동 시크릿 교체를 지원합니다.
- External Secrets를 사용할 때는 업데이트를 위한 새로고침 간격을 구성할 수 있습니다.
- Secrets Store CSI Driver는 Pod가 항상 최신 시크릿 값을 갖도록 하는 rotation reconciler 기능을 제공합니다.
실습
일단 AWS Secrets Manager에 시크릿 정보를 생성해보겠습니다. 사용자 이름과 비밀번호를 포함하는 JSON 형식의 자격증명을 담은 시크릿을 만듭니다.
export SECRET_SUFFIX=$(openssl rand -hex 4)
export SECRET_NAME="$EKS_CLUSTER_NAME-catalog-secret-${SECRET_SUFFIX}"
aws secretsmanager create-secret --name "$SECRET_NAME" \
--secret-string '{"username":"catalog_user", "password":"default_password"}' --region $AWS_REGION

잘 생성되었는지 시크릿정보를 확인해봅니다.
username과 password 필드가 정상적으로 저장되었는지 확인해보고, catalog_user와 default_password 값을 확인해봅니다.
aws secretsmanager describe-secret --secret-id "$SECRET_NAME"

콘솔에서 실제 정보를 확인할 수 있었습니다.

AWS Secrets and Configuration Provider (ASCP) 설정하기
먼저 Secret Store CSI Driver DaemonSet과 Pod들을 확인합니다.
kubectl -n secrets-store-csi-driver get pods,daemonsets -l app=secrets-store-csi-driver

다음으로, AWS용 CSI Secrets Store Provider DaemonSet과 Pod들을 확인합니다:
kubectl -n kube-system get pods,daemonset -l "app=secrets-store-csi-driver-provider-aws"

SecretProviderClass 설정하기
CSI 드라이버를 통해 AWS Secrets Manager에 저장된 시크릿에 접근하기 위해 SecretProviderClass가 필요합니다. 이것은 AWS Secrets Manager의 정보와 일치하는 드라이버 구성과 특정 매개변수를 제공하는 네임스페이스가 지정된 CRD 입니다.
워크샵에서는 다음과 같이 명령어 한 줄로 작업을 진행했습니다.
cat ~/environment/eks-workshop/modules/security/secrets-manager/secret-provider-class.yaml \
| envsubst | kubectl apply -f -
실질적으로 진행되는 작업은 다음과같이 SPC라는 CRD를 생성하는 것입니다.
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: catalog-spc
namespace: catalog
spec:
provider: aws
parameters:
objects: |
- objectName: "$SECRET_NAME"
objectType: "secretsmanager"
jmesPath:
- path: username
objectAlias: username
- path: password
objectAlias: password
secretObjects:
- secretName: catalog-secret
type: Opaque
data:
- objectName: username
key: username
- objectName: password
key: password
해당 내용을 조금 더 자세히 살펴보겠습니다.
parameters:
objects: |
- objectName: "$SECRET_NAME"
objectType: "secretsmanager"
jmesPath:
- path: username
objectAlias: username
- path: password
objectAlias: password
먼저 Objects 매개변수는 Secrets Manager에 저장할 eks-workshop/catalog-secrets이라는 시크릿을 의미합니다. jmesPath를 사용해 JSON 형식의 키:값을 추출합니다.
secretObjects:
- secretName: catalog-secret
type: Opaque
data:
- objectName: username
key: username
- objectName: password
key: password
secretObjects에서는 해당 Secrets Manager 데이터를 Kubernetes 시크릿으로 생성하고 동기화하는 방법을 나타냅니다.
Pod에 마운트되면 SecretProviderClass라는 CRD를 통해서 Kubernetes의 시크릿을 생성하고 동기화 합니다.
즉 해당 CRD를 통해서 AWS Secrets Manager의 값을 추출하고, 쿠버네티스 Secret과 동기화시키는 작업을 진행합니다.
생성확인
실제 쿠버네티스에서 secret이 잘 생성되었는지 확인해보겠습니다.
kubectl get secretproviderclass -n catalog catalog-spc -o yaml | yq '.spec.secretObjects'

Secrets Manager 값을 Kubernetes Pod에 마운트 하기
이제 pod에 실제 값을 마운트해보겠습니다. 먼저 catalog 배포와 catalog 네임스페이스의 기존 시크릿을 살펴보겠습니다.
kubectl -n catalog get deployment catalog -o yaml | yq '.spec.template.spec.containers[] | .env'

현재 catalog 배포에는 /tmp에 마운트된 emptyDir외에 추가 볼륨이나 볼륨마운트가 없습니다.
kubectl -n catalog get deployment catalog -o yaml | yq '.spec.template.spec.volumes'
kubectl -n catalog get deployment catalog -o yaml | yq '.spec.template.spec.containers[] | .volumeMounts'

AWS Secrets Manager 시크릿 마운트하기
이제 catalog Deployment를 수정해 시크릿을 사용하도록 하겠습니다.
kubectl kustomize ~/environment/eks-workshop/modules/security/secrets-manager/mounting-secrets/ \
| envsubst | kubectl apply -f-
kubectl rollout status -n catalog deployment/catalog --timeout=120s
다음과 같이 마운트 됩니다.
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/created-by: eks-workshop
app.kubernetes.io/type: app
name: catalog
namespace: catalog
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/component: service
app.kubernetes.io/instance: catalog
app.kubernetes.io/name: catalog
template:
metadata:
annotations:
prometheus.io/path: /metrics
prometheus.io/port: "8080"
prometheus.io/scrape: "true"
labels:
app.kubernetes.io/component: service
app.kubernetes.io/created-by: eks-workshop
app.kubernetes.io/instance: catalog
app.kubernetes.io/name: catalog
spec:
containers:
- env:
- name: DB_USER
valueFrom:
secretKeyRef:
key: username
name: catalog-secret
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
key: password
name: catalog-secret
envFrom:
- configMapRef:
name: catalog
image: public.ecr.aws/aws-containers/retail-store-sample-catalog:0.4.0
imagePullPolicy: IfNotPresent
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 3
name: catalog
ports:
- containerPort: 8080
name: http
protocol: TCP
readinessProbe:
httpGet:
path: /health
port: 8080
periodSeconds: 5
successThreshold: 3
resources:
limits:
memory: 512Mi
requests:
cpu: 250m
memory: 512Mi
securityContext:
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
volumeMounts:
- mountPath: /etc/catalog-secret
name: catalog-secret
readOnly: true
- mountPath: /tmp
name: tmp-volume
securityContext:
fsGroup: 1000
serviceAccountName: catalog
volumes:
- csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: catalog-spc
name: catalog-secret
- emptyDir:
medium: Memory
name: tmp-volume
해당 내용을 통해서 CSI 드라이버를 이용해 이전에 검증한 SecretProviderClass로 AWS Secret Manager의 시크릿을 Pod 내부에 /etc/catalog-secret 마운트 경로 마운트 합니다. 이를 통해서 Pod에 반영되게 됩니다.
변경사항 확인
이제 실제 Deployment 정보를 통해서 CSI Secret Store Driver를 사용하는 새로운 볼륨과 /etc/catalog-secrets에 마운트된 볼륨 마운트를 확인해봅니다.
kubectl -n catalog get deployment catalog -o yaml | yq '.spec.template.spec.volumes'
kubectl -n catalog get deployment catalog -o yaml | yq '.spec.template.spec.containers[] | .volumeMounts'

이렇게 Pod 컨테이너 내에 파일 시스템에서 파일로 민감 정보fi를 접근할 수 있도록합니다. 이 방식을 통해서 환경 변수로 시크릿 값을 노출하지 않고 소스 시크릿이 수정될 때 자동으로 업데이트 되는 등 여러 이점이 있습니다.

Pod 내부의 마운트된 시크릿 확인하기
마운트 경로에는 /etc/catalog-secret에 eks-workshop-catalog-secret, password, username 이렇게 3가지가 있습니다.
다음 명령어를 통해서 확인해보면 잘 마운트가 된것을 확인할 수 있습니다.
kubectl -n catalog exec deployment/catalog -- ls /etc/catalog-secret/
kubectl -n catalog exec deployment/catalog -- cat /etc/catalog-secret/${SECRET_NAME}
kubectl -n catalog exec deployment/catalog -- cat /etc/catalog-secret/username
kubectl -n catalog exec deployment/catalog -- cat /etc/catalog-secret/password

환경 변수는 이제 SecretProvierClass에 의해 CSI Secret Store 드라이버를 통해 자동으로 생성된 catalog-secret에서 가져옵니다.
kubectl -n catalog get deployment catalog -o yaml | yq '.spec.template.spec.containers[] | .env'
kubectl -n catalog get secrets

External Secrets Operator(ESO) 활용하기
해당 External Secrets Operator 설치 여부를 확인합니다. pod가 잘 떠있고 sa에 IRSA가 잘연결되어 있습니다.
kubectl -n external-secrets get pods
kubectl -n external-secrets get sa
kubectl -n external-secrets describe sa external-secrets-sa | grep Annotations

ClusterSecretStore 생성하기
모든 네임스페이스의 ExternalSecrets가 참조할 수 있는 클러스터 전체의 SecretStroe인 ClusterSecretStore리소스를 생성해야 합니다.
cat ~/environment/eks-workshop/modules/security/secrets-manager/cluster-secret-store.yaml \
| envsubst | kubectl apply -f -
작업내용은 다음과 같습니다.
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: "cluster-secret-store"
spec:
provider:
aws:
service: SecretsManager
region: $AWS_REGION
auth:
jwt:
serviceAccountRef:
name: "external-secrets-sa"
namespace: "external-secrets"
ClusterSecretStore는 AWS Secrets Manager와 인증하기 위해 Service Account에 연결된 JWT 토큰을 사용합니다.
ExternalSecret 생성 및 배포
kubectl kustomize ~/environment/eks-workshop/modules/security/secrets-manager/external-secrets/ \
| envsubst | kubectl apply -f-
kubectl rollout status -n catalog deployment/catalog --timeout=120s
apiVersion: apps/v1
kind: Deployment
metadata:
labels:
app.kubernetes.io/created-by: eks-workshop
app.kubernetes.io/type: app
name: catalog
namespace: catalog
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/component: service
app.kubernetes.io/instance: catalog
app.kubernetes.io/name: catalog
template:
metadata:
annotations:
prometheus.io/path: /metrics
prometheus.io/port: "8080"
prometheus.io/scrape: "true"
labels:
app.kubernetes.io/component: service
app.kubernetes.io/created-by: eks-workshop
app.kubernetes.io/instance: catalog
app.kubernetes.io/name: catalog
spec:
containers:
- env:
- name: DB_USER
valueFrom:
secretKeyRef:
key: username
name: catalog-external-secret
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
key: password
name: catalog-external-secret
envFrom:
- configMapRef:
name: catalog
image: public.ecr.aws/aws-containers/retail-store-sample-catalog:0.4.0
imagePullPolicy: IfNotPresent
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 3
name: catalog
ports:
- containerPort: 8080
name: http
protocol: TCP
readinessProbe:
httpGet:
path: /health
port: 8080
periodSeconds: 5
successThreshold: 3
resources:
limits:
memory: 512Mi
requests:
cpu: 250m
memory: 512Mi
securityContext:
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 1000
volumeMounts:
- mountPath: /tmp
name: tmp-volume
securityContext:
fsGroup: 1000
serviceAccountName: catalog
volumes:
- emptyDir:
medium: Memory
name: tmp-volume
리소스를 수정한 뒤 적용을 위해 재시작 합니다.
kubectl rollout status -n catalog deployment/catalog --timeout=120s
생성된 External Secret 리소스를 살펴보겠습니다.
kubectl -n catalog get externalsecrets.external-secrets.io catalog-external-secret -o yaml | yq '.spec'

시크릿을 동기화하는 전략을 살펴볼 수 있습니다. refreshInterval은 1시간이므로 1시간 단위로 기본 동기화를 진행합니다.
다음 명령어를 통해서 시크릿 동기화 현황을 볼 수도 있습니다.
kubectl -n catalog get externalsecrets.external-secrets.io

SecretSynced 상태는 AWS Secrets Manager에서 성공적으로 동기화되었다는 것입니다. 이 구성은 key 매개변수를 통해 AWS Secrets Manager 시크릿을 참조하고, 앞서 생성한 ClusterSecretStore를 참조합니다.
ExternalSecret을 생성하면 자동으로 해당 Kubernetes 시크릿이 생성됩니다. 이 시크릿은 External Secrets Operator에 의해 소유됩니다.
kubectl -n catalog get secrets

catalog Pod가 새 시크릿 값을 사용하고 있는지 확인해봅시다.
kubectl -n catalog get pods
kubectl -n catalog get deployment catalog -o yaml | yq '.spec.template.spec.containers[] | .env'

이렇게 AWS Secrets and Configuration Provider(ASCP)와 External Secrets Operator(ESO)를 둘 다 사용해봤습니다.
- ASCP는 시크릿을 환경변수로 노출하지 않고도 AWS Secrets Manager에서 직접 볼륨으로 마운트할 수 있지만, 볼륨 관리가 필요한 것은 단점입니다.
- ESO는 쿠버네티스의 시크릿 수명 주기 관리를 단순화하고 클러스터 전체 SecretStore 기능을 제공하지만 볼륨마운트는 불가능하다는 단점이 있습니다.