Swap しても Functions のアプリキーを変えたくない時の Key Vault を使った構成方法

本記事は、[Microsoft Azure Tech Advent Calendar 2023](https://qiita.com/advent-calendar/2023/microsoft-azure-tech) の 12/03 の投稿です。

# はじめに

Azure Functions をよりセキュアに使いたいという状況は多く発生すると思います。特に、HTTP トリガー関数をむやみやたらに実行されたくないというときには、関数ホストや関数コードごとに有効な、API キーのようなものがあればいいのになと思うかなと思います。Azure Functions ではそんな時に使えるキーが提供されています（もちろん、ネットワークを分離しておいたり、リクエスト元を認証したりといった方法で保護することも必要なこともあるので、要件次第で考えるポイントだと思います）。

それとは別に、Azure Functions では「スロット」という機能が用意されています。スロットはうまく使えば [関数ホストのアップデートをダウンタイムを可能な限り小さくして実現したり](https://jpazpaas.github.io/blog/2022/06/17/upgrade-azure-functions-runtime-from-v3-to-v4-using-slot.html)、[A/B テストに使えたり](https://azure.github.io/AppService/2020/08/03/ab_testing_app_service.html)、[Blue-Green デプロイに使えたり](https://willvelida.medium.com/implementing-blue-green-deployments-with-azure-web-apps-for-containers-29538aff0019)いろいろできます。

これらを一緒に使っている時に「スロットをスワップしたらアプリキーも交換されちゃった」という場面に遭遇しました。アプリキーはスワップされてほしくなかったので、それを実現することはできないだろうかと調べてみたので、その内容をまとめてみようと思います。

# Azure Functions が提供するアクセスキー

https://learn.microsoft.com/ja-jp/azure/azure-functions/security-concepts?tabs=v4#function-access-keys

> **関数のアクセス キー**
> 関数を使用すると、開発中に HTTP 関数のエンドポイントにアクセスするのをより困難にするようにキーを使用できます。 HTTP によってトリガーされる関数で HTTP アクセス レベルが anonymous に設定されている場合を除き、要求には API アクセス キーが含まれている必要があります。
> 
> キーにより既定のセキュリティ メカニズムが実現しますが、運用環境では HTTP エンドポイントをセキュリティで保護する追加オプションを検討することをお勧めします。 たとえば、パブリック アプリで共有シークレットを配布することは、良い慣例ではありません。 関数がパブリック クライアントから呼び出される場合、別のセキュリティ メカニズムの実装を検討することをお勧めします。 詳細については、[運用環境で HTTP エンドポイントを保護する](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-bindings-http-webhook-trigger?tabs=python-v2%2Cisolated-process%2Cnodejs-v4%2Cfunctionsv2&pivots=programming-language-csharp#secure-an-http-endpoint-in-production)を参照してください。

ドキュメントによればアクセスキーには「[関数レベルのキー](https://learn.microsoft.com/ja-jp/azure/azure-functions/security-concepts?tabs=v4#authorization-scopes-function-level)」と「[管理レベルのキー](https://learn.microsoft.com/ja-jp/azure/azure-functions/security-concepts?tabs=v4#master-key-admin-level)」 2 種類あるようです。

さらに「関数レベルのキー」には「各関数コードごとに有効なキー」と「全関数コードに対して有効な（すなわちホストレベルで有効な）キー」の 2 種類があるようです。両方について、既定で `default` という名前のキーが払い出されています。

> **承認スコープ (関数レベル)**
関数レベルのキーには、2 つのアクセス スコープがあります。
> 
> 関数: これらのキーは、それらが定義されている特定の関数にのみ適用されます。 API キーとして使用した場合は、その関数だけがアクセスできます。
> 
> ホスト: ホスト スコープのキーは、関数アプリ内のすべての関数にアクセスするために使用できます。 API キーとして使用した場合は、関数アプリ内のすべての関数がアクセスできます。
> 
> 各キーには、参照用に名前が付けられており、関数レベルおよびホスト レベルで "default" という名前の既定のキーがあります。 関数キーが、ホスト キーよりも優先されます。 2 つのキーが同じ名前で定義されている場合は、関数キーが使用されます。

「管理レベルのキー」はこれもホストレベルのキーであり、「アプリ内のすべての関数へのホスト レベルのアクセスを提供するだけでなく、ランタイム REST API への管理アクセスも提供」するキーです。むやみやたらに共有してはダメそうなキーですね。

適用範囲に注目してまとめるとこんな感じでしょうか。

- 関数ホストレベル
    - `default` という名前のアクセスキー
    - `_master` という名前の管理キー

- 各関数コードレベル
    - `default` という名前のアクセスキー

Azure Portal を開くと、確かに関数アプリレベルの [アプリ キー] ブレードには `default` と `_master` が書いてあります。

// 関数ホストレベルのキー
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169178/d724c97c-d84c-82e0-739d-2116dcb55024.png)

// 関数コードレベルのキー
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169178/f892d363-1a73-de87-b6ba-921b43cbdc03.png)

これらのキーは、既定では、アプリケーション設定 [`AzureWebJobsSecretStorageType`](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-app-settings#azurewebjobssecretstoragetype) に `Blob` と設定されているので

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169178/c5fff012-ef6d-16f7-fbf1-0bf2d41081bb.png)

アプリケーション設定 [`AzureWebJobsStorage`](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-app-settings#azurewebjobsstorage) に設定した接続文字列でアクセスできるストレージアカウント内の Blob コンテナ `azure-webjobs-secrets` に暗号化した上でファイルとして保存されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169178/da7ef4d8-32a6-f850-2849-56eb809d5213.png)

# そのままスワップするとどうなるのか

手元の Linux な従量課金プランでホストされている関数アプリを用意して、特に何も考えずに既定の設定のままスロットを交換すると実際にどうなるのかを確認してみると

// スワップ前
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169178/aa30179d-599b-64db-bda8-d19f864fc501.png)

// スワップ後
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169178/1e040212-5a6d-54a6-eb28-52889235e973.png)

確かに入れ替わってそうです。

# スワップしてもアプリキーを変えたくない時はどうすればいいか

前置きが長くなりましたが、スワップしてもアプリキーを変えたくない時は、

- スロット間でアプリキーを共通化させる

もしくは

- スロット間でアプリキーを別の値にして固定する

とすれば良さそうです。

既定ではストレージアカウントのストレージアカウント内の Blob コンテナがデータの保存先になってますが、そのストレージアカウントはアプリコードを保存するファイル共有も含んでいるので、せっかくなので Key Vault をアプリキーの保存先に使ってみることにします。この方がなんだか少しだけ構成がわかりやすい気がします。

勘のいい方はなんとなくどうすればいいかおわかりかもしれませんが、Key Vault リソースを用意して、関数アプリにシステム割り当てマネージド ID を割り当て Key Vault リソースへのアクセス権限を付与した上で、アプリケーション設定 [`AzureWebJobsSecretStorageType`](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-app-settings#azurewebjobssecretstoragetype) と [`AzureWebJobsSecretStorageKeyVaultUri`](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-app-settings#azurewebjobssecretstoragekeyvaulturi) を適切に設定することで構成できます。

- [`AzureWebJobsSecretStorageType`](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-app-settings#azurewebjobssecretstoragetype)

> **AzureWebJobsSecretStorageType**
キーの保存に使用するリポジトリまたはプロバイダーを指定します。 キーは、常に関数アプリに固有のシークレットを使用して、格納される前に暗号化されます。
> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169178/64bab109-eb2c-f595-b12c-697f5d7c1c07.png)


- [`AzureWebJobsSecretStorageKeyVaultUri`](https://learn.microsoft.com/ja-jp/azure/azure-functions/functions-app-settings#azurewebjobssecretstoragekeyvaulturi)

> **AzureWebJobsSecretStorageKeyVaultUri**
> キーを格納するために使用されるキー コンテナー インスタンスのURI。 バージョン 4.x 以降のバージョンの Functions ランタイムでサポートされています。 キーの格納にキー コンテナー インスタンスを使用する場合は、この設定をお勧めします。 この設定では、`AzureWebJobsSecretStorageType` を `keyvault` に設定する必要があります。
> 
> `AzureWebJobsSecretStorageKeyVaultUri` の値は、`https://` を含む、[Key Vault overview](Key Vault の概要) タブに表示されている、[Vault URI](コンテナー URI) の完全な値である必要があります。
> 
> コンテナーには、ホスティング リソースのシステム割り当てマネージド ID に対応するアクセス ポリシーが必要です。 アクセス ポリシーでは、Get、Set、List、Delete というシークレットのアクセス許可を、その ID に付与することを必要としています。

実は、同様の内容がドキュメントの[ここ](https://learn.microsoft.com/ja-jp/azure/azure-functions/security-concepts?tabs=v4#secret-repositories)にも書いてあります。

https://learn.microsoft.com/ja-jp/azure/azure-functions/security-concepts?tabs=v4#secret-repositories

> **シークレット リポジトリ**
> 既定では、キーは、`AzureWebJobsStorage` 設定によって指定されたアカウントの Blob Storage コンテナーに格納されます。 設定 `AzureWebJobsSecretStorageType` を使ってこの動作をオーバーライドし、キーを別の場所に格納することができます。
> 
> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169178/60641a43-c2de-1548-a1ab-d5dd60a5ff89.png)
> 
> キーのストレージに Key Vault を使うときに必要なアプリの設定は、マネージド ID の種類によって異なります。 Functions ランタイム バージョン 3.x では、システム割り当てマネージド ID のみがサポートされます。
> 
> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169178/7d9d62e6-b08c-1369-4b7e-4e6b1a12809b.png)

では実際にやってみます。

## スロット間でアプリキーを共通化させる

1. Key Vault リソースを作ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169178/e48eff68-48cb-1594-0723-0c0e17ec909b.png)

2. 無印スロット（プロダクションスロット）、stage スロットの両方に、[ID] ブレードからシステム割り当てマネージド ID を有効化します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169178/50a3af02-fc14-f915-df30-26e6e0946a7d.png)

3. Key Vault リソースの [アクセス制御 (IAM)] ブレードから 2. で付与したシステム割り当てマネージド ID のそれぞれに対して「キー コンテナー シークレット責任者」のロールを割り当てます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169178/b6bfb1f8-ba0f-6c3f-8689-5956fb52b4b3.png)

4. 無印スロット（プロダクションスロット）、stage スロットのアプリケーション設定 `AzureWebJobsSecretStorageType` と `AzureWebJobsSecretStorageKeyVaultUri` を次のように更新します。
    - `AzureWebJobsSecretStorageType`: 既定の `blob` から `keyvault` に
    - `AzureWebJobsSecretStorageKeyVaultUri`: Key Vault リソースの [概要] ブレードから取得できる [コンテナーの URI]（ `https://{Key Vault リソース名}.vault.azure.net` ）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169178/2bbbc85d-4dda-78a6-1cd7-c70300a1efe2.png)

5. 無印スロット（プロダクションスロット）、stage スロットのアプリキーが一致していることを確認します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169178/1278c412-0537-8598-82c6-3d51c4322e93.png)

## スロットごとにアプリキーを別の値にして固定する

この場合は、Key Vault リソースを 2 つ用意して、アプリケーション設定 `AzureWebJobsSecretStorageType` と `AzureWebJobsSecretStorageKeyVaultUri` を同様に設定した上で、それぞれをスロットに固有の設定として指定すれば良さそうです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/169178/b349a2e7-a272-0548-34d4-dc2feb9791f7.png)

# おわりに

Swap したときに関数アプリのアプリキーを変えたくない時に Key Vault を使って、スロット間でアプリキーを共通化する方法、スロットごとにアプリキーを別の値で固定する方法について実際に検証してみた内容をまとめてご紹介しました。

皆様の Functions を用いた開発場面のささやかな一助になれば嬉しいです！

Enjoy coding :wave: