はじめに
マルチクラウド構成の検討とか色々とやってる中で、アプリケーションで使用するシークレットを取得するにあたって、マネージドサービスのKubernetes(EKS/GKE)からどうやって呼び出すか?を検討しました。環境の前提条件などから色々とハマったので、導入からシークレットの取得までを整理したいと思います。
※間違ってたらコメントなどで指摘を頂けますとありがたいです。
前提条件
AWS
- EKSクラスタが構築されていること
- ノードグループではなく、Fargateで起動する(ここが色々と制限になりました)
- FargateプロファイルはDefaultとKube-systemを別個に作成
- ParametorStoreへの値の設定は事前に行っている
Google Cloud
- GKEクラスタが構築されていること
- モードはAutoilotとする
- SecretsManagerへの値の設定は事前に行っている
いろいろ
- kubectlの認証などは省略してます
- ファイルの中身は記載しますが、applyコマンドは省略してます
検討コンポーネント
まず、AWS側でサンプルアプリをデプロイしてシークレットを取得するまでを検討しました。
SecretStoreSCIDriver
ボリュームを通してシークレットストアとK8sを統合するドライバ。
ただし、Fargate起動の場合は、ボリュームマウント関連で制限があるため、本ドライバは使えませんでした。
https://github.com/aws/secrets-store-csi-driver-provider-aws/issues/34
kubernetes-external-secrets(KES)
次に確認したのが上記です。日本語でもSecretsを取得する方法で検索すると良く出てくるものでした。が、正式に非推奨になり、記事タイトルにあるExternalSecretsOperator(ESO)に変わりました。
https://github.com/external-secrets/kubernetes-external-secrets/issues/864
ExternalSecretsOperator(ESO)
そのため、KESの後継であるESOを使用してSecretsを取得することとしました。が、2021年11月にKESで非推奨のアナウンスが出たので、公式ドキュメントはあるにせよ日本語の情報が多くない。よし、なら導入して書こう(本記事)
クラウド間で違うコンポーネントを使うのは避けるため、Google Cloudも同じESOを使用することとしました。
ESO概念図
※公式サイト(https://external-secrets.io/v0.4.1/) から引用
ExternalSecretsOperatorのPodが起動し、各クラウドサービスの権限を設定したServiceAccountを使用してシークレットを取得します。KESではアクセスに使う設定と、K8s内Secretsの設定がまとまっていましたが、ESOではアクセス設定(SecretStore/ClusterSecretStore)とSecretsの紐づけ(ExternalSecret)が別となっているので、役割分担もわかりやすいかなと思います。
- SecretStore
- アクセスに使用する設定
- K8sにあるServiceAccountを指定する
- 名前空間を考慮する必要がある
- ClusterSecretStore
- アクセスに使用する設定
- K8sにあるServiceAccountを指定する
- 名前空間を横断して共通で使用できる
- ExternalSecrets
- フェッチするデータの宣言
- どのSecretStoreを使用して取得するかを指定する
- K8s Secretsの定義、どこから取得するかを指定する
- 名前空間を考慮する必要がある
構築する
を参照しつつ、色々と前提条件の環境に合うように修正を実施しました。
AWS
Terraform
OIDCプロバイダとRole関連
権限のリソース周りなどは読み替えてください。個人的には*ですべての権限などを与えたくなかったので、下記のようにしています。あと、コード化したいので、可能な限りTerraformに寄せました。
Terraformコード
##
# Create OIDC Provider (K8s Cluster)
##
resource "aws_iam_openid_connect_provider" "k8s_oidc" {
url = "[ClusterのOpenID Connect プロバイダー URL]"
client_id_list = [
"sts.amazonaws.com",
]
# 証明書のサムプリントはEKS Clusterで基本は共通
thumbprint_list = [
"[値を手動で確認するなどして入れる]",
]
}
data "aws_iam_policy_document" "secrets_inline_policy" {
statement {
actions = [
"kms:GenerateDataKey",
"kms:Decrypt",
]
resources = ["*"]
}
statement {
actions = [
"secretsmanager:GetSecretValue",
"secretsmanager:DescribeSecret",
"secretsmanager:ListSecretVersionIds",
]
resources = [
"arn:aws:secretsmanager:[REGION]:[ACCOUNT]:secret:[RESOURCE]"
]
}
statement {
actions = [
"ssm:GetParameters",
"ssm:GetParameter",
]
resources = [
"arn:aws:ssm:[REGION]:[ACCOUNT]:parameter[RESOURCE]"
]
}
}
data "aws_iam_policy_document" "secrets_assume_role" {
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
# K8sのOIDCを設定する
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.k8s_oidc.arn]
}
# 信頼関係を制限する
condition {
test = "StringEquals"
variable = "[ClusterのOpenIDConnectプロバイダーURLからhttps://を削除した値]:sub"
values = [
"system:serviceaccount:default:test-eso-sa",
]
}
}
}
## ESO向けにRoleを作成する
resource "aws_iam_role" "k8s_serviceaccount_secrets" {
name = "ExternalSecretOperator-role"
description = "Managed by Terraform / K8s ESO ROLE"
assume_role_policy = data.aws_iam_policy_document.secrets_assume_role.json
inline_policy {
name = "ExternalSecretOperator-inlinepolicy"
policy = data.aws_iam_policy_document.secrets_inline_policy.json
}
depends_on = [
aws_iam_openid_connect_provider.k8s_oidc,
]
}
kubernetes
helm
ガイドでは新しくNamespaceを作成しているが、Fargateプロファイル上存在しないので、Defaultで動かすこととします。FargateでHelmInstallを行う場合は、regionとvpcIDが必要になります。
helm install external-secrets external-secrets/external-secrets \
-n default \
--set clusterName=[クラスタ名] \
--set serviceAccount.create=false \
--set serviceAccount.name=test-eso-sa \
--set region=[REGION] \
--set vpcId=[ClusterがあるVPCのID]
上手く動くと、ワークロードにexternal-secretsのDeploymentが存在し、Podが起動します。
ServiceAccount
###
# ESOに対してSSM/SecretsManagerからSecretsを取得することを許可するServiceAccount(IAM紐づけ)
###
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
app.kubernetes.io/component: secrets
app.kubernetes.io/name: external-secrets
name: test-eso-sa
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::[ACCOUNT]:role/ExternalSecretOperator-role
SecretStore / ExternalSecret
###
# Secretの認証方法の設定
###
apiVersion: external-secrets.io/v1alpha1
kind: SecretStore
metadata:
name: secretstore-sample
spec:
provider:
aws:
service: ParameterStore
region: [REGION]
auth:
jwt:
serviceAccountRef:
name: test-eso-sa
apiVersion: external-secrets.io/v1alpha1
kind: ExternalSecret
metadata:
name: example
spec:
refreshInterval: 1h
secretStoreRef:
name: secretstore-sample
kind: SecretStore
target:
name: multicloud-db-info
creationPolicy: Owner
data:
- secretKey: dburl
remoteRef:
key: [ParametorStoreのPath]
- secretKey: user
remoteRef:
key: [ParametorStoreのPath]
- secretKey: password
remoteRef:
key: [ParametorStoreのPath]
ハマった点
公式のHelmだと名前空間を新しく作るが、Fargateなのでkube-systemで良いかと軽く考えると、各リソース間の検索ができずにErrorとなりました(Notfound)
ExternalSecret > SecretStore > ServiceAccount > IAM
と呼び出すので、同じ名前空間でないと見れないという単純なミスでした。
Google Cloud
Terraform
ServiceAccountとRoleBinding関連
OIDCプロバイダーは内部のためかAWSと違い不要でした。Secretsを読むためのServiceAccountを作成し、必要なRoleをBindingしています。Roleはあまり制限かけてませんが、AWSとの習熟度の違いですのでご容赦ください。本当なら制限かけるべきかと思っています。公式からリンクされているGoogle Cloudのドキュメントだとコマンドベースで記載されていますが、こちらも、コード化したいので、Terraformに寄せました。
Terraformコード
###
# Service Account(ExternalSecretsOperator用ユーザ)
###
resource "google_service_account" "eso" {
account_id = "eso-sa"
display_name = "ExternalSecrets Account"
description = "ExternalSecretsOperator Use SecretManager"
}
###
# Role Binding(ESOへSecretsを読み込む権限を)
###
resource "google_project_iam_binding" "secret-pull" {
project = local.const.google.project
role = "roles/secretmanager.secretAccessor"
members = [
"serviceAccount:${google_service_account.eso.email}",
]
}
###
# GKEに対して、WorkloadIdentityUserを付与する
###
resource "google_service_account_iam_binding" "gkec" {
service_account_id = google_service_account.eso.name
role = "roles/iam.workloadIdentityUser"
members = [
"serviceAccount:[project].svc.id.goog[default/external-secrets]",
]
}
kubernetes
helm
ガイドでは新しくNamespaceを作成しているが、AWSに合わせてDefaultで動かすこととします。AWSより引数が少なく簡単にインストールできます。
helm install external-secrets \
external-secrets/external-secrets \
-n default
ServiceAccount
###
# ESOに対してSSM/SecretsManagerからSecretsを取得することを許可するServiceAccount(IAM紐づけ)
###
apiVersion: v1
kind: ServiceAccount
metadata:
labels:
app.kubernetes.io/component: secrets
app.kubernetes.io/name: external-secrets
name: external-secrets
annotations:
iam.gke.io/gcp-service-account: [Terraformで作ったGCPのServiceAccountEmail]
SecretStore / ExternalSecret
apiVersion: external-secrets.io/v1alpha1
kind: ClusterSecretStore
metadata:
name: example
spec:
provider:
gcpsm:
projectID: [project]
auth:
workloadIdentity:
clusterLocation: [REGION]
clusterName: [Cluster名]
serviceAccountRef:
name: external-secrets
namespace: default
apiVersion: external-secrets.io/v1alpha1
kind: ExternalSecret
metadata:
name: example
spec:
refreshInterval: 1h
secretStoreRef:
name: example
kind: ClusterSecretStore
target:
name: multicloud-db-info
creationPolicy: Owner
data:
- secretKey: dburl
remoteRef:
key: [SecretManagerの名前]
- secretKey: user
remoteRef:
key: [SecretManagerの名前]
- secretKey: password
remoteRef:
key: [SecretManagerの名前]
確認方法
上手く設定ができれば、以下のように見えます。ただ、Base64でHash化されているだけなので、EKSやGKEではsecretを暗号化する設定を作成時に可能なので、入れておく方が良いと思います。
kubectl get secrets
NAME TYPE DATA AGE
multicloud-db-info Opaque 3 2d1h
kubectl describe secrets multicloud-db-info
Name: multicloud-db-info
Namespace: default
Labels: <none>
Annotations: reconcile.external-secrets.io/data-hash: XXXXXXX
Type: Opaque
Data
====
dburl: 10 bytes
password: 32 bytes
user: 5 bytes
個人的に考える分界点
Terraformで環境を管理することを考えると、取得するアクセス方法まではプラットフォームチームなどのクラスタを作成・管理しているチームが準備した方が楽なのかな?と思います。セクションごとにアクセスできるシークレットを制限することが多いと考えるためです。
さいごに
Pod自体に権限を付与して取得する方法とか、ConfigMapを使う方法など、色々とやりかたはあるとは思います。ただ、アプリケーション部門が使いかつマルチクラウド構成と考えたら、準備する各種Deploymentなどのファイルの記述はできうる限り統一した方が負荷が少ないかなと感じました。定期的に更新をしてくれますし。
ノードの管理をしないサーバレスのKubernetesでは、EKSよりやはりGKEが統合が進んでいて使いやすいというのも感じました。