
배경 상황
쿠버네티스 환경에서 깃옵스(GitOps)를 하려고 하면 k8s 매니페스트 파일을 깃헙이나 깃랩같은 코드 저장소에 저장하게 된다. 이렇게 ArgoCD를 이용해서 애플리케이션 부트스트래핑 구성을 하게 되면 OAuth 인증용 크레덴셜, 코드 저장소 연결 정보등을 필연적으로 쿠버네티스 Secrets 오브젝트로 저장해야하는 일이 발생한다. 이러한 민감 정보들을 깃 저장소에 그대로 저장하는 것은 보안적으로 바람직하지 않다.
그러면 깃 레포지토리에 민감정보를 그대로 올리지 않고 사용할 수 있는 방법은 어떤 것들이 있을까?
깃 저장소에 민감정보를 저장하지 않고 사용하려면 외부 저장소에 저장 후 런타임에서 주입하는 형태로 가야하는데, 쿠버네티스에서는 대표적으로 Secrets Store CSI driver, Sealed Secrets, External Secrets 세 가지 방법이 있다. CSI 드라이버는 Pod에 크레덴셜 정보를 직접 주입하는 형태로 활용되어서 Secrets 오브젝트를 참조하는 ArgoCD 구성에는 맞지 않을 듯 했고, Sealed Secrets도 많이 사용되는 듯 했지만 kubeseal라는 별도 툴을 활용해서 Secrets를 암호화 한 뒤에 사용해야하는 절차가 번거롭게 느껴져서 External Secrets를 선택했다.
참고로 깃 레포에는 직접적으로 크레덴셜이 저장되지 않는다고 해도 Secrets 오브젝트의 경우 etcd에 base64 인코딩한 형태로 저장된다. 따라서 사실상 암호화라고 보기 어렵고 etcd 자체의 암호화가 필요하다. 참고로 Amazon EKS를 사용하는 경우 Control Plane 영역은 AWS의 관리 영역으로 EBS 암호화 구성이 되어있어 클러스터 운영자가 직접적으로 신경 쓸 필요는 없고 자체 관리형 클러스터를 운영 중인 경우에만 관리가 필요하다.
External Secrets Operator(ESO)란?

External Secrets 프로젝트는 외부 API의 시크릿을 쿠버네티스로 동기화 하기 위해 만들어진 프로젝트이다.
External Secrets를 설치하면 외부 시크릿 관리 시스템(AWS Secrets manager, Parameter store, Hashicorp Vault, Azure Key vault, IBM Cloud Secrets manager 등)에 저장된 크레덴셜을 호출해 값을 읽고 쿠버네티스 시크릿에 자동으로 주입해준다.
ESO는 ExternalSecret, SecretStore, ClusterSecretStore 세 가지 커스텀 리소스를 활용해 시크릿의 라이프사이클을 관리할 수 있게 해준다.
External Secrets 설치 방법(Terraform 활용)
테라폼으로 External Secrets를 구성하려면 IRSA 모듈과 Helm 리소스를 사용하면 간단하게 구성할 수 있다.
AWS IRSA 모듈을 활용하면 자주 활용하는 Helm 차트 등의 권한 구성을 손쉽게 할 수 있다. 아래와 같이 oidc provider 정보는 EKS 모듈에서 가져오고(EKS와 별도로 구성했다면 remote state에서 불러와서 사용), namespace_service_accounts 항목에는 실제로 배포될 External Secrets의 네임스페이스와 Service Account 이름을 설정한다. External Secrets를 위한 IRSA 리소스를 구성해야 External Secrets Pod가 AWS 리소스에서 시크릿 값을 호출 할 수 있게 된다.
module "irsa-external-secrets" {
source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
version = "5.55.0"
role_name = "external-secrets-role"
attach_external_secrets_policy = true
oidc_providers = {
main = {
provider_arn = module.eks.oidc_provider_arn
namespace_service_accounts = ["external-secrets:external-secrets"]
}
}
}
다음으로는 helm provider에서 제공하는 helm_release리소스를 활용해 external-secrets 프로젝트를 EKS 클러스터에 배포한다. values에서는 annotation에 위에서 구성한 IRSA의 IAM Role ARN을 추가해준다.
resource "helm_release" "external-secrets" {
name = "external-secrets"
namespace = "external-secrets"
create_namespace = true
repository = "<https://charts.external-secrets.io>"
chart = "external-secrets"
version = "0.16.1"
wait = true
values = [
<<-EOT
serviceAccount:
create: true
automount: true
annotations:
eks.amazonaws.com/role-arn: ${module.irsa-external-secrets.iam_role_arn}
EOT
]
depends_on = [module.irsa-external-secrets]
}
External Secrets를 활용해 Secrets Manager에 저장된 크레덴셜 불러오기

위에서 ESO는 ExternalSecret, SecretStore, ClusterSecretStore 세 가지 커스텀 리소스를 활용한다고 했다.
위의 구성도에서 확인할 수 있듯이 SecretStore/ClusterSecretStore는 어떤 외부 API에 어떻게 접근할 지 정의하고, External Secrets는 저장된 크레덴셜 정보 중 실제로 어떤 정보를 가져올 지 정의하게 된다.
먼저 SecretStore는 아래와 같이 작성할 수 있다. spec.provider.aws.service 필드에서 어떤 AWS 서비스에 접근할지 정의하고(SecretsManager 혹은 ParameterStore), spec.provider.aws.auth 필드에서는 AWS 리소스에 어떤 방식으로 인증해서 접근할지를 정의하게 된다.
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: secretstore-sample
spec:
provider:
aws:
service: SecretsManager
region: eu-central-1
auth:
jwt:
serviceAccountRef:
name: my-serviceaccount
위 예시는 앞에서 생성한 IRSA를 활용해 접근했을 때의 구성 예시이다. IRSA를 활용하게 되면 인증 시 jwt 토큰을 발급받아 인증하게 된다. 이 외에도 Pod Identity를 활용해 접근하는 방식이나 Access Key를 활용해 접근하는 방식이 있는데, 액세스키는 활용하지 않도록 한다(액세스키를 활용하게 되면 AWS 크레덴셜을 깃 레포에 저장하게 됨).

참고로 ClusterSecretStore의 경우에는 특정 네임스페이스가 아니라 여러 네임스페이스나 클러스터 레벨로 시크릿 접근을 구성할 때 사용한다.
이렇게 크레덴셜을 어떻게 가져올지를 SecretStore에서 정의했다면 이제 ExternalSecret을 통해 실제로 값을 불러와서 Secrets 오브젝트를 생성하도록 한다.
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: example
spec:
secretStoreRef:
name: secretstore-sample
kind: SecretStore
target:
name: secret-to-be-created
creationPolicy: Owner
data:
- secretKey: secret-key-to-be-managed
remoteRef:
key: provider-key
version: provider-key-version
property: provider-key-property
- secretStoreRef에서는 어떤 secretStore를 활용해 외부 API에 접근할 것인지 구성하게 되고, 위에서 생성한 secretstore-sample 라는 이름의 SecretStore를 정의해준다.
- target에서는 Secrets 리소스를 어떻게 생성할 것인지를 정의하게 되는데 spec.target.name에서 정의한 이름으로 Secrets가 생성된다.
- spec.data에서는 실제로 가져올 크레덴셜 값들을 정의한다. secretKey에서는 Secrets 리소스 생성시의 키 값을 정의하고 (템플릿 문법을 활용해서 어떤 키 값에 크레덴셜을 매핑할지도 선택할 수 있다), RemoteRef 필드에서는 가져올 Secrets manager 리소스 명세를 작성하면 된다.
이렇게 External Secrets 리소스까지 배포하게 되면 자동으로 Secrets 리소스가 생성되고, 외부 API의 크레덴셜이 변경되면 컨트롤러가 변경점을 감지하고 Secrets를 자동으로 업데이트 해주게 된다.