谷本 心 in せろ部屋

はてなダイアリーから引っ越してきました

Dapr Advent Calendar 16日目 - Dapr + k8sでステート保存とシークレットストア

こんにちは Dapr Advent Calendar 16日目です。最近ちょくちょく周りの人からDaprに興味があるとか、Daprのメリットに魅力を感じるという話を耳にするようになりました。とは言えまだ「業務で使う」ところまで行く人はなかなかいないのですが、来年にはv1.0リリースから1周年を迎えますし、ぼちぼち使う人も出てくるんじゃないかなと思っています。

k8s + Dapr + PostgreSQLでステート保存。あとシークレットストア

今回はk8s + Dapr上のアプリケーションからPostgreSQLへの読み書きを行います。ただ、単純に読み書きするだけなら、過去に作ったアプリケーションを使い、k8s上にデプロイするためのマニフェストファイルを作って kubectl apply するだけで済んでしまうので、あまり面白くありません。せっかくなのでシークレットストアも一緒に使い、アカウント情報などを秘匿する形にしてみましょう。

f:id:cero-t:20211216103045p:plain
今回作るアプリケーション

アプリケーションの作成

今回動かすアプリケーションは Dapr Advent Calendar 4日目 - Daprでデータストアにアクセス で作成したものです。そちらを先に読んでからこのエントリーを読んでください。

シークレットストアについては Dapr Advent Calendar 7日目 - Daprでシークレットストアを使う でも解説しているため、そちらも参考にしてください。

ソースコードgithubに置いてあります。

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

これでイメージが作成できました。

マニフェストファイルの作成

次に、このアプリケーションをデプロイするためのマニフェストファイルを作成します。

k8s/state-app.yaml

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を起動するためのマニフェストファイルは次のような内容になります。パスワードは平文で指定しています。

k8s/postgresql.yaml

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_PASSWORDsecretpassword を指定しています。

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=

typeOpaque にすると、シークレットの値としてbase64エンコードされた文字列を指定することができます。type には他にも kubernetes.io/tlskubernetes.io/service-account-token などを指定することができますが、通常はこの Opaque で問題なさそうです。

data には key: value 形式でシークレットのキーとbase64エンコードした値を指定します。

これで、シークレットストアの準備ができました。

ちなみにbase64エンコードしているとは言え、これではパスワードを平文と保存していることと大差ないですよね。よりセキュアにするためには、シークレットストアをマニフェストファイルではなくkubectlコマンドで作成するとか、作成したシークレットの詳細を閲覧できるユーザを絞るなどする必要があります。その辺りはk8sの使いこなしの話になるため、今回は割愛したいと思います。

PostgreSQLのマニュフェストファイルからシークレットストアを参照する

続いて、上で作成したPostgreSQLを起動するマニフェストファイルを修正し、シークレットストアを参照するようにします。次のような内容になります。

k8s/postgresql.yaml

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に接続するための設定ファイルは次のようになります。

k8s/statestore.yaml

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エンコードする

まずは接続文字列を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接続設定ファイルからシークレットストアを参照するように修正しましょう。次のようになります。

k8s/statestore.yaml

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 logskubectl describe pods コマンドなどで詳細を確認してみてください。

次のコマンドで、保存したデータを取得します。

curl localhost:8082/read/cero_t

次のようなデータが取得できます。

{
  "name": "Shin Tanimoto",
  "twitter": "@cero_t"
}

問題なくPostgreSQLにデータが保存され、取得できることが分かりました。

f:id:cero-t:20211216120656p:plain
k8sのシークレットストアを参照するアプリケーション

どのシークレットストアを使うべきか

今回はk8sでシークレットストアを作成し、k8sマニフェストファイルと、Daprの接続設定ファイルからそれぞれ利用する形にしました。一方 Dapr Advent Calendar 7日目 - Daprでシークレットストアを使う で利用したDaprのシークレットストア機能については出番がありませんでした。この機能はどういう時に使うべきなのでしょうか。

僕自身の使い分けとしては、正味の話、Daprのシークレットストアを使う機会は今のところありません。ローカル環境で(k8sなしで)開発する時はシークレットストアを使うことがなく、設定ファイルに直接パスワードなどを書いていますし、k8sを利用する検証環境や運用環境では、k8sのシークレットストアを使うためです。

「アプリケーション内から外部サービスを利用する際に必要となるアカウント情報」などは、Daprのシークレットストアに問い合わせて取得する可能性があるのですが、僕自身の経験としては、そのような情報は開発時には application.yaml に書いておき、k8s環境ではk8sのシークレットストアから取得した情報を使って、application.yamlの値を環境変数で上書きするようにしています。そうするとDaprのシークレットストアの出番がなくなるのですよね。

Daprをローカル環境でもk8s環境でもなく、個別のサーバにデプロイして運用する際には、もしかしたらDaprのシークレットストア機能の出番になるかも知れませんね。そのように使ってみることがあれば、またブログなどで紹介したいと思います。

まとめ

  • Daprの設定ファイルからk8sのシークレットストアを参照することができます
  • k8sで運用するシステムにおいては、Dapr側のシークレットストア機能を使うことはないかも知れません

今回の話を要約すると、ずいぶんシンプルになっちゃいましたね。

それでは、また明日!

追記

まきんぐさんから、平文のままシークレットを作ることもできるよと教えてもらいました。知らなかった!

世のサイトの例ではだいたいbase64エンコードされた文字列を使っているようですが、base64にすることで「カジュアルに目に触れてしまった時にパッとは値が分からない」くらいの効果ならあるものの、そんな頼りない効果が不要であれば、平文で作ってしまっても良いでしょうね。