Tanzu Application PlatformではWorkloadにはK8sのSecretをService Binding経由で設定して機密情報を設定するのが一般的ですが、K8sのSecretに機密情報を置きたくないという場合に HashiCorp Vaultを使いたいですね。
K8sからVaultにアクセスするには
- Vault Agent Injectorを使う
- Container Storage Interface (CSI) Volumeを使う
- External Secrets Operatorを使う
がありますが、
- TAPのWorkloadにCSI Volumeの設定をするのがhackなしでは難しい
- External SecretsはVaultのSecretをK8sのSecretにsyncするので、K8sのSecretに機密情報を置きたくないというニーズを満たなさない
ということで、Vault Agent Injectorを使います。
Tanzu Application Platform 1.4でExternal Secretが実験的にサポートされています。
https://docs.vmware.com/en/VMware-Tanzu-Application-Platform/1.4/tap/external-secrets-about-external-secrets-operator.html
今回はこちらの記事で作成したKind上のTAPにWorkloadをデプロイして、Vault上に設定したSecretを参照させます。
TAPだとMulti Cluster構成が一般的で、VaultはWorkloadが載るRun Clusterとは別の場所で動く可能性が高いので、今回は既存のVaultに対してWorkloadからアクセスします。以下のチュートリアルを参考にしました。
https://developer.hashicorp.com/vault/tutorials/kubernetes/kubernetes-external-vault
目次
- Vaultの起動
- Vault Agent Injectorのインストール
- Kubernetes Auth Methodの有効化
- アプリケーション用のSecretを登録
- Workloadのデプロイ (application.propertiesの追加)
Vaultの起動
K8sクラスタ外でVaultを起動します。今回はdevモードで起動します。
vault server -dev -dev-root-token-id root -dev-listen-address 0.0.0.0:8200
Vault Agent Injectorのインストール
TAPをインストールしたクラスタ (Multi Cluster構成の場合はRun Cluster) にVault Agent Injectorをインストールします。
Helm Chartを使用します。
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
helm template
でマニフェストを生成します。injector.externalVaultAddr
にK8sクラスタから見たVaultのURLを指定します。
helm template vault hashicorp/vault -n vault --set "injector.externalVaultAddr=http://host.docker.internal:8200" > vault-agent-injector.yaml
kubectl create ns vault
kubectl apply -f vault-agent-injector.yaml -n vault
Podを確認。
$ kubectl get pod -n vault
NAME READY STATUS RESTARTS AGE
vault-agent-injector-547d8fc8db-kxtd7 1/1 Running 0 14s
Kubernetes Auth Methodの有効化
Kubernetes Auth Methodの有効化します。
vault auth enable kubernetes
Service Accountに対するトークンを作成します。
kubectl apply -n vault -f -<<EOF
apiVersion: v1
kind: Secret
metadata:
name: vault-token
namespace: vault
annotations:
kubernetes.io/service-account.name: vault
type: kubernetes.io/service-account-token
EOF
VaultにアクセスするK8sの情報を設定します。
TOKEN_REVIEW_JWT=$(kubectl get secret -n vault vault-token -otemplate='{{index .data "token" | base64decode}}')
KUBE_CA_CERT=$(kubectl get secret -n vault vault-token -otemplate='{{index .data "ca.crt"| base64decode}}')
KUBE_HOST=$(kubectl config view --raw --minify --flatten --output='jsonpath={.clusters[].cluster.server}')
vault write auth/kubernetes/config \
token_reviewer_jwt="$TOKEN_REVIEW_JWT" \
kubernetes_host="$KUBE_HOST" \
kubernetes_ca_cert="$KUBE_CA_CERT" \
issuer="https://kubernetes.default.svc.cluster.local"
アプリケーション用のSecretを登録
アプリケーションには https://github.com/making/vehicle-api を使用します。このアプリはPostgreSQLにアクセスするシンプルなSpring Bootのアプリで、Vaultとは完全に独立しています。
アクセスするPostgreSQLには ElephantSQL のfree planを使用します。
ElephantSQLでインスタンスを作成し、インスタンスの情報をvehicle-api用のSecretとして登録します。
vault kv put secret/vehicle-api/config \
host='floppy.db.elephantsql.com' \
username='ixyepwbw' \
password='QIDpi7cBKioNGyp7JeN8ZMTL-rIWL_9B' \
database='ixyepwbw'
登録された情報を確認します。
$ vault read secret/data/vehicle-api/config
Key Value
--- -----
data map[database:ixyepwbw host:floppy.db.elephantsql.com password:QIDpi7cBKioNGyp7JeN8ZMTL-rIWL_9B username:ixyepwbw]
metadata map[created_time:2023-01-11T08:46:51.471224Z custom_metadata:<nil> deletion_time: destroyed:false version:1]
このSecretに対する読み取り専用のPolicyを作成します。
vault policy write vehicle-api - <<EOF
path "secret/data/vehicle-api/config" {
capabilities = ["read"]
}
EOF
Workloadのデプロイ (application.propertiesの追加)
Workloadをデプロイする、demo
namespaceのdefault
Service Accountに対するRoleを作成し、先に作ったPolicyをアタッチします。
vault write auth/kubernetes/role/vehicle-api \
bound_service_account_names=default \
bound_service_account_namespaces=demo \
policies=vehicle-api \
ttl=24h
このSecretにアクセスするためのannotationsをWorkloadに設定します。
Secretの内容を/vault/secrets/application.properties
に書き込み、アプリからこのファイルを読み込むように環境変数SPRING_CONFIG_IMPORT
を設定します。
cat <<EOF > workload.yaml
apiVersion: carto.run/v1alpha1
kind: Workload
metadata:
labels:
app.kubernetes.io/part-of: vehicle-api
apps.tanzu.vmware.com/workload-type: web
name: vehicle-api
spec:
params:
- name: annotations
value:
autoscaling.knative.dev/minScale: '1'
vault.hashicorp.com/agent-inject: 'true'
vault.hashicorp.com/role: vehicle-api
vault.hashicorp.com/agent-inject-secret-application.properties: secret/data/vehicle-api/config
vault.hashicorp.com/agent-inject-template-application.properties: |
{{- with secret "secret/data/vehicle-api/config" -}}
spring.datasource.url=jdbc:postgresql://{{ .Data.data.host }}/{{ .Data.data.database }}
spring.datasource.username={{ .Data.data.username }}
spring.datasource.password={{ .Data.data.password }}
{{- end -}}
env:
- name: SPRING_CONFIG_IMPORT
value: /vault/secrets/application.properties
source:
git:
url: https://github.com/making/vehicle-api
ref:
branch: main
EOF
tanzu apps workload apply -f workload.yaml -n demo
# or kubectl apply -f workload.yaml -n demo
vault.hashicorp.com/agent-inject-template-...
をtanzu
CLIの--annotation
で設定できませんでした。
CLIだけでWorkloadを作成する場合は次のようなパラメータの指定が必要です。tanzu apps workload apply vehicle-api2 \ -n demo\ --git-repo https://github.com/making/vehicle-api \ --git-branch main \ --type web \ --app vehicle-api \ --env SPRING_CONFIG_IMPORT=/vault/secrets/application.properties \ --param-yaml annotation='{"autoscaling.knative.dev/minScale":"1","vault.hashicorp.com/agent-inject":"true","vault.hashicorp.com/agent-inject-secret-application.properties":"secret/data/vehicle-api/config","vault.hashicorp.com/agent-inject-template-application.properties":"{{- with secret \"secret/data/vehicle-api/config\" -}}\nspring.datasource.url=jdbc:postgresql://{{ .Data.data.host }}/{{ .Data.data.database }}\nspring.datasource.username={{ .Data.data.username }}\nspring.datasource.password={{ .Data.data.password }}\n{{- end -}}","vault.hashicorp.com/role":"vehicle-api"}'
アプリのデプロイが完了して、Workloadを確認します。Podにvault agentのサイドカーが含まれるのでコンテナ数が3/3
になります。
$ tanzu apps workload get -n demo vehicle-api
📡 Overview
name: vehicle-api
type: web
💾 Source
type: git
url: https://github.com/making/vehicle-api
branch: main
📦 Supply Chain
name: source-to-url
RESOURCE READY HEALTHY TIME OUTPUT
source-provider True True 134m GitRepository/vehicle-api
image-provider True True 131m Image/vehicle-api
config-provider True True 131m PodIntent/vehicle-api
app-config True True 131m ConfigMap/vehicle-api
service-bindings True True 131m ConfigMap/vehicle-api-with-claims
api-descriptors True True 131m ConfigMap/vehicle-api-with-api-descriptors
config-writer True True 131m Runnable/vehicle-api-config-writer
🚚 Delivery
name: delivery-basic
RESOURCE READY HEALTHY TIME OUTPUT
source-provider True True 130m ImageRepository/vehicle-api-delivery
deployer True True 130m App/vehicle-api
💬 Messages
No messages found.
🛶 Pods
NAME READY STATUS RESTARTS AGE
vehicle-api-00001-deployment-7b6d9f7f49-rl25z 3/3 Running 0 30m
vehicle-api-build-1-build-pod 0/1 Completed 0 34m
vehicle-api-config-writer-trr8r-pod 0/1 Completed 0 31m
🚢 Knative Services
NAME READY URL
vehicle-api Ready https://vehicle-api-demo.127-0-0-1.sslip.io
To see logs: "tanzu apps workload tail vehicle-api --namespace demo"
アプリにアクセスして、データベースからデータが取れることを確認します。
$ curl -sk https://vehicle-api-demo.127-0-0-1.sslip.io/vehicles
[{"id":1,"name":"Avalon"},{"id":2,"name":"Corolla"},{"id":3,"name":"Crown"},{"id":4,"name":"Levin"},{"id":5,"name":"Yaris"},{"id":6,"name":"Vios"},{"id":7,"name":"Glanza"},{"id":8,"name":"Aygo"}]
Vaultから取得したSecretがファイルに書き込まれていることを確認します。
POD_NAME=$(kubectl get pod -n demo -l serving.knative.dev/service=vehicle-api -ojsonpath='{.items[0].metadata.name}')
$ kubectl exec -it ${POD_NAME} -n demo -c workload -- ls -la /vault/secrets/
total 8
drwxrwxrwt 2 root root 60 Jan 11 09:14 .
drwxr-xr-x 3 root root 4096 Jan 11 09:14 ..
-rw-r--r-- 1 _apt cnb 170 Jan 11 09:14 application.properties
$ kubectl exec -it ${POD_NAME} -n demo -c workload -- cat /vault/secrets/application.properties
spring.datasource.url=jdbc:postgresql://floppy.db.elephantsql.com/ixyepwbw
spring.datasource.username=ixyepwbw
spring.datasource.password=QIDpi7cBKioNGyp7JeN8ZMTL-rIWL_9
Actuatorのenv
エンドポイントにアクセスして、実際にこのファイルから設定が読み込まれていることを確認します。
kubectl port-forward ${POD_NAME} -n demo 8081:8081
$ curl -s localhost:8081/actuator/env | jq .
{
"activeProfiles": [],
"propertySources": [
{
"name": "server.ports",
"properties": {
...
}
},
{
"name": "servletContextInitParams",
"properties": {}
},
{
"name": "systemProperties",
"properties": {
...
}
},
{
"name": "systemEnvironment",
"properties": {
...
}
},
{
"name": "Config resource 'file [/vault/secrets/application.properties]' via location '/vault/secrets/application.properties'",
"properties": {
"spring.datasource.url": {
"value": "jdbc:postgresql://floppy.db.elephantsql.com/ixyepwbw",
"origin": "URL [file:/vault/secrets/application.properties] - 1:23"
},
"spring.datasource.username": {
"value": "ixyepwbw",
"origin": "URL [file:/vault/secrets/application.properties] - 2:28"
},
"spring.datasource.password": {
"value": "******",
"origin": "URL [file:/vault/secrets/application.properties] - 3:28"
}
}
},
{
"name": "Config resource 'class path resource [application.properties]' via location 'optional:classpath:/'",
"properties": {
...
}
}
]
}
TAPからVaultにできました。K8sのSecretを使うことなく、TAPから機密情報を取得できることが確認できました。