Dapr Advent Calendar 16日目 - Dapr + k8sでステート保存とシークレットストア
こんにちは Dapr Advent Calendar 16日目です。最近ちょくちょく周りの人からDaprに興味があるとか、Daprのメリットに魅力を感じるという話を耳にするようになりました。とは言えまだ「業務で使う」ところまで行く人はなかなかいないのですが、来年にはv1.0リリースから1周年を迎えますし、ぼちぼち使う人も出てくるんじゃないかなと思っています。
k8s + Dapr + PostgreSQLでステート保存。あとシークレットストア
今回はk8s + Dapr上のアプリケーションからPostgreSQLへの読み書きを行います。ただ、単純に読み書きするだけなら、過去に作ったアプリケーションを使い、k8s上にデプロイするためのマニフェストファイルを作って kubectl apply
するだけで済んでしまうので、あまり面白くありません。せっかくなのでシークレットストアも一緒に使い、アカウント情報などを秘匿する形にしてみましょう。
アプリケーションの作成
今回動かすアプリケーションは Dapr Advent Calendar 4日目 - Daprでデータストアにアクセス で作成したものです。そちらを先に読んでからこのエントリーを読んでください。
シークレットストアについては Dapr Advent Calendar 7日目 - Daprでシークレットストアを使う でも解説しているため、そちらも参考にしてください。
https://github.com/cero-t/dapr-advent-2021
また Dapr Advent Calendar 13日目 - Dapr + k8sでHello World で説明したツールなどがセットアップ済みであることを前提で説明を書いています。
作成したソースコードのおさらい
以前のエントリーで作成したアプリケーションのソースコードをおさらいします。データの読み書きを行うためのコントローラークラスです。
StateController.java
@RestController public class StateController { private RestTemplate restTemplate; @Value("http://localhost:${DAPR_HTTP_PORT}/v1.0/state/statestore") private String stateUrl; public StateController(RestTemplate restTemplate) { this.restTemplate = restTemplate; } @PostMapping("/write") public void write(@RequestBody Object message) { restTemplate.postForObject(stateUrl, message, Void.class); } @GetMapping("/read/{key}") public Object read(@PathVariable String key) { return restTemplate.getForObject(stateUrl + "/" + key, Object.class); } }
ステートを保存する /write
と、それを読み込む /read
というエンドポイントを提供しています。いずれもデータストア名に statestore
を指定して、DaprのStatemangent APIを呼び出しているだけです。
このアプリケーションは8082番ポートで起動するように設定してあります。
アプリケーションのイメージ作成
続いて、このアプリケーションのイメージを作成しましょう。
イメージ作成先がminikubeのイメージレジストリになるよう次のコマンドを実行します。
eval $(minikube docker-env)
そしてイメージ作成のコマンドを実行します。
cd (GitHubのディレクトリパス)/dapr-advent-2021/state ../mvnw spring-boot:build-image
これでイメージが作成できました。
マニフェストファイルの作成
次に、このアプリケーションをデプロイするためのマニフェストファイルを作成します。
apiVersion: apps/v1 kind: Deployment metadata: name: state-app labels: app: state spec: replicas: 1 selector: matchLabels: app: state template: metadata: labels: app: state annotations: dapr.io/enabled: "true" dapr.io/app-id: "state-app" dapr.io/app-port: "8082" spec: containers: - name: state image: state:1.0.0 ports: - containerPort: 8082 imagePullPolicy: IfNotPresent --- kind: Service apiVersion: v1 metadata: name: state-svc labels: app: state spec: selector: app: state ports: - protocol: TCP port: 8082 targetPort: 8082 type: LoadBalancer
以前作成した hello.yaml
と同じ構成ですので、説明は割愛します。
PostgreSQLのマニフェストファイルの作成
続いて、データストアとしてPostgreSQLを起動するためのマニフェストファイルを作成します。
パスワードが平文で書かれたマニフェストを作成する
PostgreSQLを起動するためのマニフェストファイルは次のような内容になります。パスワードは平文で指定しています。
apiVersion: apps/v1 kind: Deployment metadata: name: postgresql-deploy labels: app: postgresql spec: replicas: 1 selector: matchLabels: app: postgresql template: metadata: labels: app: postgresql spec: containers: - name: postgresql image: postgres:11-alpine imagePullPolicy: IfNotPresent ports: - containerPort: 5432 env: - name: POSTGRES_PASSWORD value: secretpassword --- kind: Service apiVersion: v1 metadata: name: postgresql-svc labels: app: postgresql spec: type: LoadBalancer selector: app: postgresql ports: - protocol: TCP port: 5432 targetPort: 5432
イメージを使ってPostgreSQLをデプロイするマニフェストファイルです。
パスワードは POSTGRES_PASSWORD
で secretpassword
を指定しています。
DB名とユーザ名は省略しているため、いずれも postgres
が使われます。
また、このPostgreSQLに他のpodからアクセスできるよう postgresql-svc
という名前のサービスを作り、5432番ポートにpod外からアクセスできるようにしています。
さて、上のマニフェストファイルにはパスワードが平文で書かれているため、このパスワードをシークレットストアに持っていきましょう。k8s自身がシークレットストアの機能を持っているため、今回はそれを使います。
base64エンコードされたパスワードを作成する
まずはパスワードをbase64エンコードするため、次のコマンドを実行します。
echo -n "secretpassword" | base64
-n
は、echoの表示時に改行を入れないためのオプションです。これを入れないと、secretpassword
の後ろに改行が入ったものがbase64エンコードされてしまいます。僕は一度それでヤラカシたことがあるので気をつけてください。
実行結果は、次のようになります。
c2VjcmV0cGFzc3dvcmQ=
念のため、base64デコードして復元できるかを確認しておきましょう。
echo c2VjcmV0cGFzc3dvcmQ= | base64 --decode
secretpassword
元の文字列が復元できました。
シークレットストアのマニフェストファイルを作成する
続いて、シークレットストアのマニフェストファイルを作成します。上でbase64エンコードしたパスワードを利用します。
k8s/postgresql-secret.yaml
apiVersion: v1 kind: Secret metadata: name: postgresql-secret type: Opaque data: postgres_password: c2VjcmV0cGFzc3dvcmQ=
type
を Opaque
にすると、シークレットの値としてbase64エンコードされた文字列を指定することができます。type
には他にも kubernetes.io/tls
や kubernetes.io/service-account-token
などを指定することができますが、通常はこの Opaque
で問題なさそうです。
data
には key: value
形式でシークレットのキーとbase64エンコードした値を指定します。
これで、シークレットストアの準備ができました。
ちなみにbase64エンコードしているとは言え、これではパスワードを平文と保存していることと大差ないですよね。よりセキュアにするためには、シークレットストアをマニフェストファイルではなくkubectlコマンドで作成するとか、作成したシークレットの詳細を閲覧できるユーザを絞るなどする必要があります。その辺りはk8sの使いこなしの話になるため、今回は割愛したいと思います。
PostgreSQLのマニュフェストファイルからシークレットストアを参照する
続いて、上で作成したPostgreSQLを起動するマニフェストファイルを修正し、シークレットストアを参照するようにします。次のような内容になります。
apiVersion: apps/v1 kind: Deployment metadata: name: postgresql-deploy labels: app: postgresql spec: replicas: 1 selector: matchLabels: app: postgresql template: metadata: labels: app: postgresql spec: containers: - name: postgresql image: postgres:11-alpine imagePullPolicy: IfNotPresent ports: - containerPort: 5432 env: - name: POSTGRES_PASSWORD valueFrom: secretKeyRef: name: postgresql-secret key: postgres_password --- kind: Service apiVersion: v1 metadata: name: postgresql-svc labels: app: postgresql spec: type: LoadBalancer selector: app: postgresql ports: - protocol: TCP port: 5432 targetPort: 5432
変更箇所は、パスワードの部分だけです。
この部分が、
- name: POSTGRES_PASSWORD value: secretpassword
次のようになります。
- name: POSTGRES_PASSWORD valueFrom: secretKeyRef: name: postgresql-secret key: postgres_password
パスワードを平文で書くのではなく valueFrom.secretKeyRef
としてシークレットストアを参照するようにしました。
name
にはシークレットストア名、key
にはシークレットのキーを指定します。
これでPostgreSQLのパスワードとしてシークレットストアの値を利用できるようになりました。今のところDaprは全く関係なくて、ただただk8sのマニフェストを作成しているだけです。
DaprからPostgreSQLへの接続設定ファイルの作成
続いて、DaprからPostgreSQLにアクセスするための設定ファイルを作成します。
接続文字列が平文で書かれた接続設定ファイルを作成する
DaprからPostgreSQLに接続するための設定ファイルは次のようになります。
apiVersion: dapr.io/v1alpha1 kind: Component metadata: name: statestore spec: type: state.postgresql version: v1 metadata: - name: connectionString value: host=postgresql-svc user=postgres password=secretpassword port=5432 connect_timeout=10 database=postgres
これは Dapr Advent Calendar 4日目 - Daprでデータストアにアクセス で作成した設定ファイルとほとんど同じです。ローカル環境向けに作ったファイルを、ほぼそのままk8s向けに流用できるのがDaprの良いところです。
metadata.name
にデータストア名として statestore
を指定しています。これはソースコードで指定した値に合わせます。
このファイルにもやはりパスワードが平文で書かれているため、接続文字列の部分をシークレットストアに移動させましょう。
接続文字列をbase64エンコードする
echo -n "host=postgresql-svc user=postgres password=secretpassword port=5432 connect_timeout=10 database=postgres" | base64
aG9zdD1wb3N0Z3Jlc3FsLXN2YyB1c2VyPXBvc3RncmVzIHBhc3N3b3JkPXNlY3JldHBhc3N3b3JkIHBvcnQ9NTQzMiBjb25uZWN0X3RpbWVvdXQ9MTAgZGF0YWJhc2U9cG9zdGdyZXM=
少し長いですが、base64エンコードされた文字列ができました。
シークレットストアのファイルに追加する
先ほど作ったシークレットストアのファイルに、接続文字列の値を追加します。
k8s/postgresql-secret.yaml
apiVersion: v1 kind: Secret metadata: name: postgresql-secret type: Opaque data: postgres_password: c2VjcmV0cGFzc3dvcmQ= connection: aG9zdD1wb3N0Z3Jlc3FsLXN2YyB1c2VyPXBvc3RncmVzIHBhc3N3b3JkPXNlY3JldHBhc3N3b3JkIHBvcnQ9NTQzMiBjb25uZWN0X3RpbWVvdXQ9MTAgZGF0YWJhc2U9cG9zdGdyZXM=
最後の行に connection
というキーで、base64エンコードされた接続文字列を追加しました。
PostgreSQLへの接続設定ファイルからシークレットを参照する
それでは、DaprのPostgreSQL接続設定ファイルからシークレットストアを参照するように修正しましょう。次のようになります。
apiVersion: dapr.io/v1alpha1 kind: Component metadata: name: statestore spec: type: state.postgresql version: v1 metadata: - name: connectionString secretKeyRef: name: postgresql-secret key: connection
最後の connectionString
の部分を修正しました。
修正前
- name: connectionString value: host=postgresql-svc user=postgres password=secretpassword port=5432 connect_timeout=10 database=postgres
修正後
- name: connectionString secretKeyRef: name: postgresql-secret key: connection
シークレットストアを参照するようにしました。こっちは valueFrom:
は挟まなくて良いんですね。ちょっと設定ファイルの文法をよく分かってないのですが、これで良いみたいです。
アプリケーションをデプロイしてアクセスする
イメージをk8sにデプロイ
それでは作成したイメージをk8sにデプロイします。
cd (GitHubのディレクトリパス)/dapr-advent-2021 kubectl apply -f k8s/postgresql-secret.yaml kubectl apply -f k8s/postgresql.yaml kubectl apply -f k8s/statestore.yaml kubectl apply -f k8s/state-app.yaml
まずシークレットストアを作成し、PostgreSQLをデプロイし、そのPostgreSQLにDaprからアクセスするための設定(コンポーネント)をデプロイし、最後にアプリケーションをデプロイする、という順番になっています。
ちなみに複数のファイルを指定してデプロイする際は、次のようにディレクトリを指定してデプロイすることも可能です。
kubectl apply -f k8s
こうすればk8sフォルダにあるすべてのマニフェストファイルに対して kubectl apply
が行われます。
また、k8s上にデプロイされたpodやコンポーネントと、手元にあるマニフェストファイルの差分を確認する際には次のコマンドが使えます。
kubectl diff -f k8s
差分を確認してから kubectl apply
することで、意図しない設定の更新などを避けることができますね。
まとめて全部削除したい時は、次のようにできます。
kubectl delete -f k8s
これも便利ですね。
k8s上のアプリケーションにアクセスする
それでは、起動したアプリケーションにアクセスしてみましょう。
次のコマンドを実行します。
kubectl port-forward service/state-svc 8082:8082
これでローカルPCの8082番ポート経由でminikubeにアクセスできるようになりました。
それでは、別のコンソールからアクセスしてデータを保存してみましょう。
curl -XPOST "localhost:8082/write" -H "Content-type:application/json" -d '[ { "key": "cero_t", "value": { "name": "Shin Tanimoto", "twitter": "@cero_t" } }, { "key": "BABYMETAL", "value": [ { "name": "SU-METAL", "alias": "Suzuka Nakamoto" }, { "name": "MOAMETAL", "alias": "Moa Kikuchi" }, { "name": "YUIMETAL", "alias": "Yui Mizuno" } ] } ]'
エラーなく応答が返ってくれば成功です。エラーが発生した場合は kubectl logs
や kubectl describe pods
コマンドなどで詳細を確認してみてください。
次のコマンドで、保存したデータを取得します。
curl localhost:8082/read/cero_t
次のようなデータが取得できます。
{ "name": "Shin Tanimoto", "twitter": "@cero_t" }
問題なくPostgreSQLにデータが保存され、取得できることが分かりました。
どのシークレットストアを使うべきか
今回はk8sでシークレットストアを作成し、k8sのマニフェストファイルと、Daprの接続設定ファイルからそれぞれ利用する形にしました。一方 Dapr Advent Calendar 7日目 - Daprでシークレットストアを使う で利用したDaprのシークレットストア機能については出番がありませんでした。この機能はどういう時に使うべきなのでしょうか。
僕自身の使い分けとしては、正味の話、Daprのシークレットストアを使う機会は今のところありません。ローカル環境で(k8sなしで)開発する時はシークレットストアを使うことがなく、設定ファイルに直接パスワードなどを書いていますし、k8sを利用する検証環境や運用環境では、k8sのシークレットストアを使うためです。
「アプリケーション内から外部サービスを利用する際に必要となるアカウント情報」などは、Daprのシークレットストアに問い合わせて取得する可能性があるのですが、僕自身の経験としては、そのような情報は開発時には application.yaml
に書いておき、k8s環境ではk8sのシークレットストアから取得した情報を使って、application.yamlの値を環境変数で上書きするようにしています。そうするとDaprのシークレットストアの出番がなくなるのですよね。
Daprをローカル環境でもk8s環境でもなく、個別のサーバにデプロイして運用する際には、もしかしたらDaprのシークレットストア機能の出番になるかも知れませんね。そのように使ってみることがあれば、またブログなどで紹介したいと思います。
まとめ
今回の話を要約すると、ずいぶんシンプルになっちゃいましたね。
それでは、また明日!
追記
まきんぐさんから、平文のままシークレットを作ることもできるよと教えてもらいました。知らなかった!
SecretのdataをstringDataにすればyamlに平文で設定できますよ。(知っててわざとbase64エンコードしてたら余計なお世話でした)
— Toshiaki Maki 💉💉 (@making) 2021年12月16日
世のサイトの例ではだいたいbase64エンコードされた文字列を使っているようですが、base64にすることで「カジュアルに目に触れてしまった時にパッとは値が分からない」くらいの効果ならあるものの、そんな頼りない効果が不要であれば、平文で作ってしまっても良いでしょうね。