.NET 6 と Daprを使った分散サービス開発 その11 Secret Store


Daprでシークレット管理

前回は、Daprに備わっているBinding (バインディング)を用いて、RabbitMQでのPubSubを扱ってきました。今回は、接続文字列やDBなどのパスワード、APIのアクセスキーなどのシークレット管理について触れていきたいと思います。

今までの前提の上でバインディングの機能を追加しますので、このページから読まれている方は、前提として以下を読んできてください。


Daprでシークレット管理

マイクロサービスや分散サービスなどでは様々な言語を用いて、これらシークレットを共通で管理するケースも多く、どの言語からも、ローカルでもKubernetesでも、同じように取得できる必要もあり、どのようにルールを定めて運用するか悩ましい所でもあります。
そして、ハードコードする事だけは避けたいものです。

これらに加えて、AWS、Azure、GCPなどのクラウドでは、これらのHSMキー管理のサービスがありますから、対応していくのも大変面倒ですし、ローカルで自分だけってケースであれば、常にそれらを使うというシチュエーションでもないケースも多分にあります。

Secret store(シークレットストア)コンポーネント

そこで、Daprではシークレット管理を抽象化するコンポーネントがあり、コンポーネント設定によって必要に応じて切り替える事ができます。

普段はローカル環境で環境変数で開発しておき、いざAzureなどにデプロイする際にはAzure Key Vaultで管理するとすれば良い事になります。

  • Environment Variables (環境変数)
  • Local file (ローカルファイル)
  • Kubernetes secrets
  • AWS Secrets Manager
  • Azure Key Vault
  • GCP Secret Manager
  • HashiCorp Vault

コンポーネントを設定

今回は最も簡単なセッティング方法である、ローカルファイルに集約する方法で試します。componentsフォルダに以下のようにファイルを構成しました。
また、nestedSeparatorによって キー名:ネストされたキー名で呼び出す事ができるようになります。

secret.yaml
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: daprsecret
spec:
  type: secretstores.local.file
  metadata:
    - name: secretsFile
      value: ./secrets.json
    - name: nestedSeparator
      value: ":"

また、合わせてsecret.jsonをルートフォルダに作ります。パスが解決できていない場合、Dapr側のログにも見つからないとログでますので、ロードできているか確認しましょう。

例として、以下のようなファイルを作成しました。

nestedSeparatorが設定されているので、以下のファイルの場合では、connectionStrings:productdbというキー名での呼び出しも可能です。

secret.json
{
    "azureStorageKey": "Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==",
    "redisPassword": "",
    "connectionStrings": {
        "customerdb": "Server=127.0.0.1;Port=3306;Database=customerdb;Uid=guest@localhost;Pwd=Passw0rd!;SslMode=Required;",
        "productdb": "Server=127.0.0.1;Port=3306;Database=productdb;Uid=guest@localhost;Pwd=Passw0rd!;SslMode=Required;"
    }
}

他のコンポーネントから読み出す

daprのコンポーネント設定そのものに、このようなシークレットが含まれています。そこで、まずは他のコンポーネントの設定をこれらのシークレットデータに置き換える方法について確認しましょう。
以下は、今まで設定してきたState Store用のRedisを設定しているコンポーネント設定のYMALです。ローカルからアクセスしていますので、特にパスワードの設定をしていないのですが、ここでは設定していると思ってください。

statestore.yaml (設定前)
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: statestore
spec:
  type: state.redis
  version: v1
  metadata:
  - name: redisHost
    value: localhost:6379
  - name: redisPassword
    value: ""

設定後は、以下のようになります。

statestore.yaml (設定後)
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: statestore
spec:
  type: state.redis
  version: v1
  metadata:
  - name: redisHost
    value: localhost:6379
  - name: redisPassword
    secretKeyRef:
      name: redisPassword
      key: redisPassword
auth:
  secretStore: daprsecret

設定しているキーが同じ名前なので、ちょっと疑問に思うかもしれません。
これはキー設定の環境によっては、異なる為で、これはローカルから今回jsonを設定しているケースの設定だからです。

    secretKeyRef:
      name: redisPassword
      key: redisPassword

Kubernetes やAzure Key Vaultなどの場合は、このnameとkeyは設定が異なる場合があります。
例えば、以下のようにKubernetes でSecretを作って、読み込みたい場合であるとして

(キーの設定)
kubectl create secret generic daprsecrets --from-literal=redisPassword=secretPassword -n appsecret
    secretKeyRef:
      name: daprsecrets 
      key: redisPassword

となります。

アプリケーション側から呼び出す

今回は、前回使ったpub-serviceのプロジェクトを修正して、シークレットをロードしてみます。

Program.cs
using Dapr.Client;

namespace PubService
{
    class Program
    {
        static async Task Main(string[] args)
        {
            string PUBSUB_NAME = "daprpubsub";
            string TOPIC_NAME = "AppStatus";
            while (true)
            {
                System.Threading.Thread.Sleep(3000);
                Random random = new Random();
                int stateId = random.Next(1, 1000);
                CancellationTokenSource source = new CancellationTokenSource();
                CancellationToken cancellationToken = source.Token;
                using var client = new DaprClientBuilder().Build();

                await client.PublishEventAsync(PUBSUB_NAME, TOPIC_NAME, stateId.ToString(), cancellationToken);
                Console.WriteLine("Published data: " + stateId);

                // 以下を新規に追加
                var storageKey = await client.GetSecretAsync("daprsecret", "azureStorageKey");
                Console.WriteLine($"Load secret : {storageKey["azureStorageKey"]}");

                var connStr = await client.GetSecretAsync("daprsecret", "connectionStrings:customerdb");
                Console.WriteLine($"Load connection string : {connStr["connectionStrings:customerdb"]}");

            }
        }
    }
}

tyeの起動と確認

ここまで来たらTyeを起動して、確認しましょう。
サイドカーとして起動したDaprからシークレットを読み取り、コンソールに表示しているログが出力されているはずです。