App Service 証明書の有効期限を Azure Functions を使用して監視する

2023年1月3日追記：Azure Functions ランタイムバージョン 4.x サポートのための新しいコードについて [新しい記事があります](https://qiita.com/shogo-ohe/items/fd1ae6f66644f246edfd)

# はじめに
Azure 上の証明書を [Key Vault](https://docs.microsoft.com/ja-jp/azure/key-vault/) に保存されて運用されることが多いですが、これらクラウド上で管理対象となっている証明書の有効期限切れを知る方法は 2020 年 10 月時点では提供されていません。

Azure 上で証明書をご利用頂く場合、Key Vault に保管し、各サービスからは Key Vault よりインポート頂くのが一般的なご利用方法の一つとなります。<br />
例: [App Service で証明書をご利用頂く場合](https://docs.microsoft.com/ja-jp/azure/app-service/configure-ssl-certificate#import-an-app-service-certificate)、[Key Vault の場合](https://docs.microsoft.com/ja-jp/azure/application-gateway/configure-keyvault-ps)

Azure 上では [App Service 証明書](https://docs.microsoft.com/ja-jp/azure/app-service/configure-ssl-certificate) という TLS/SSL 証明書の発行サービスでは証明書をご購入頂くことが出来、[自動更新をオン](https://docs.microsoft.com/ja-jp/azure/app-service/configure-ssl-certificate#renew-certificate) にするとことで、有効期限を特に気にする必要がなくなりますが、それでも有効期限切れ前を知りたいという需要があり、同様のお問い合わせを頂いている状況です。

そこで今回は、Azure Functions を使用して証明書の有効期限をチェックし、メールで通知する仕組みをサンプルコードとともにご紹介しようと思います。


# 証明書管理の概要
仕組みとしてはシンプルで、Azure Functions から Key Vault SDK でアクセスし、格納されている証明書の有効期限を評価します。
評価結果を SendGrid を使用して、メールで通知します。
Functions の [Timer トリガー](https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-bindings-timer?tabs=csharp) を使用することで、週一回や月に1度など定期的に実行することで有効期限の状況を確認することが出来ます。

処理の流れとしては以下の通り。
![diagram.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349915/3426a663-79d9-83a9-c305-5c8f5a7c857d.jpeg)
(1) Azure Functions から KeyVault へアクセス。証明書 (シークレット) の有効期限を確認します
(2) 有効期限情報を 有効、期限切れ間近、有効期限切れ の3種類に分類したメールを作成
(3) SendGrid 経由でメール通知


この仕組みの構築には、以下のリソースが必要です。
- Azure Key Vault (監視対象)
- Azure Functions
  - 今回は [Runtime v3](https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-versions), Node.js を使用
  - 従量課金プランを使用しましたが、既に Web Apps などをご利用頂いている場合、App Service Plan を間借りして実行することも可能です
- SendGrid


# リソースの準備
それぞれのリソースを準備します。既に作成済のリソースがある場合、そちらを流用頂いても問題ありません。
各リソースの作成とアクセスに必要な情報の獲得が主な作業内容です。

## Key Vault の準備
Key Vault リソースや証明書は既に存在することを前提とします。もし無い場合は [Key Vault リソースを作成](https://docs.microsoft.com/ja-jp/azure/key-vault/certificates/tutorial-import-certificate#create-a-vault) し、[証明書] の [作成/インポート] から適当な自己署名証明書を作成して下さい。

Key Vault で必要な準備は、サービスプリンシパルの準備です。具体的には、Key Vault SDK を使用してアプリケーションからアクセスしますが、その際に使用する認証情報が必要です。
以下のドキュメントに記載がありますが、コマンドと作業内容だけ抜粋しますので、ご確認下さい。<br />
 参考: [サービス プリンシパルの作成](https://docs.microsoft.com/ja-jp/azure/key-vault/secrets/quick-create-node#create-a-service-principal)

Azure CLI の [az ad sp create-for-rbac](https://docs.microsoft.com/ja-jp/cli/azure/ad/sp?view=azure-cli-latest#az-ad-sp-create-for-rbac) コマンドを使用してサービス プリンシパルを作成します。
```az ad sp create-for-rbac --skip-assignment -n "http://<my-unique-service-principal-name>" --sdk-auth```
コマンドの結果得られたキーと値のペアは、アプリケーションの環境変数に登録するため記録しておいて下さい。

続いて、サービス プリンシパルにキー コンテナーへのアクセス権を付与します。
```az keyvault set-policy -n <your-unique-keyvault-name> --spn <clientId-of-your-service-principal> --secret-permissions delete get list set --key-permissions create decrypt delete encrypt get list unwrapKey wrapKey```

後ほどアプリケーション設定に以下の名前で、az ad sp create-for-rbac コマンドで得られたサービスプリンシパルを登録します。

```
AZURE_CLIENT_ID = "clientId" の値
AZURE_CLIENT_SECRET = "clientSecret" の値
AZURE_TENANT_ID = "tenantId" の値
KEY_VAULT_NAME = アクセス先 (サービスプリンシパルを作成した) Key Vault 名
```

## SendGrid の準備
SendGrid アカウントを作成し、API Key の取得、ならびに送信元として使用するメールアドレスの登録を行います。

SendGrid アカウントの作成については、以下のドキュメントを参照して下さい。
参考: [SendGrid アカウントの作成](https://docs.microsoft.com/ja-jp/azure/sendgrid-dotnet-how-to-send-email#create-a-sendgrid-account)

API Key の取得については、以下のドキュメントを参照して下さい。
参考: [SendGrid API キーを確認するには](https://docs.microsoft.com/ja-jp/azure/sendgrid-dotnet-how-to-send-email#to-find-your-sendgrid-api-key) <br />
注意点として、SendGrid API Key は1度しか表示されません。もし作成時に記録していない場合は作り直しとなりますので注意して下さい。

得られた SendGrid API Key は、後ほどアプリケーション設定に以下の名前で登録します。
```SENDGRID_API_KEY = SendGrid API Key の名前```


最後に送信元として使用するメールアドレスの登録 (Sender Identity) です。SendGrid では指定されたメールアドレスを送信元とするメールを送信することが出来ます。しかし、そのままでは他人へのなりすましなどの問題があるため、事前のメールアドレスの登録と確認が行われています。

まず、Azure ポータルより [Sender Identity] のリンクをクリックすると SendGrid の Sender Authentication ページにジャンプします。SendGrid のメニューでは [Settings] > [Sender Authentication] に該当します。
![sendgrid-1a.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349915/f6a5ca17-7291-e26b-12cf-e7561f77550a.jpeg)
![sendgrid-1b.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349915/ea944a48-877e-480c-db62-ca15d87269e5.jpeg)


[Single Sender Verification] に登録済のメールアドレスが表示されていますので、確認の上で [Verify a Single Sender] をクリックします。
![sendgrid-2b.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349915/76a45be5-5ddd-9dd4-08ec-8fd4800459b3.jpeg)

新しいメール送信元を設定する場合、[Create a Sender] というページが表示されますので、そちらから登録します。
![sendgrid-3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349915/2305226f-d8d7-fcf3-4482-179b648b83ff.jpeg)

登録が完了すると [Single Sender Verification] のページに表示され、確認のメールが送信されます。
![sendgrid-4.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349915/1df24cdf-a10b-ea1f-fc7d-88001df53401.jpeg)

場合によっては迷惑メール似分類されている場合があるので届いていない場合は確認してみてください。
![sendgrid-5.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349915/f4535587-f38b-edac-454b-334130ebef77.jpeg)


[Verify  Single Sender] リンクをクリックすることで、メールアドレスの確認が完了し、[Single Sender Verification] のページの [VERIFIED] にて、チェックマークが付きます。
これで、SendGrid の準備は完了です。


## Functions の準備
Azure Functions を実行するリソースを作成します。リソースの作成方法はいくつかありますが、Node.js で実行される Functions を作成したいので、JavaScript の実行される ランタイム スタック で作成します。ホスティング方法は [消費量 (サーバーレス)] を指定します。リソースの作成方法は以下のドキュメントを参照して下さい。

- [Azure Portal から Function App を作成する](
https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-create-function-app-portal)
- [Quickstart: Create a JavaScript function in Azure using Visual Studio Code](https://docs.microsoft.com/ja-jp/azure/azure-functions/create-first-function-vs-code-node)


使用するホスティングプランとして、App Service Plan を使用することも出来ますが、アイドル状態が続くと自動的にサイトが停止してしまうため、[構成] > [全般設定] にて、[常時接続] がオンになっている必要があります。この設定は App Service Plan の Basic 以上の価格プランでのみ有効化できるため、注意が必要です。詳細は以下のドキュメントを参照して下さい。

- [Azure Functions のスケールとホスティング - 常にオン](https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-scale#always-on)
- [Investigating and reporting issues with timer triggered functions not firing](https://github.com/Azure/azure-functions-host/wiki/Investigating-and-reporting-issues-with-timer-triggered-functions-not-firing)


## 環境変数の設定
Azure Functions に SendGrid の API キーなどを登録します。こうすることでコード内から環境依存の情報を排除することが出来、API キーなどの変更をポータルから行うことが出来るようになります。
ポータルの [構成] > [アプリケーション設定] より、以下のアプリケーション設定を登録します。 

| 名前                             | 値 | 
|:---------------------------------|:------------|
| AZURE_CLIENT_ID                  | az ad sp create-for-rbac コマンドで得られた "clientId" の値      |
| AZURE_CLIENT_SECRET              | az ad sp create-for-rbac コマンドで得られた "clientSecret" の値  |
| AZURE_TENANT_ID                  | az ad sp create-for-rbac コマンドで得られた "tenantId" の値      |
| KEY_VAULT_NAME                   | 監視対象とする KeyVault のリソース名                             |
| SECRET_EXPIRATIONDATE_THRESHOLD  | 証明書の有効期限を警告対象とする残り日数の閾値                    |
| SENDGRID_API_KEY                 | SendGrid の API キー                                            |
| SENDGRID_ADDR_FROM               | SendGrid でメールを送信する際の送信元として使うメールアドレス      |
| SENDGRID_ADDR_TO                 | SendGrid でメールを送信する際の宛先 (通知先) として使うメールアドレス        |

[高度な編集] をクリックすることで、登録する際の、JSON 形式でまとめて登録することが出来ます。サンプルを記載するので、[高度な編集] で既存の設定を開いて追記する際に活用下さい (末尾に追加する場合、要素を区切る "," が必要です)。

```json
    {
      "name": "AZURE_CLIENT_ID",
      "value": "",
      "slotSetting": false
    },
    {
      "name": "AZURE_CLIENT_SECRET",
      "value": "",
      "slotSetting": false
    },
    {
      "name": "AZURE_TENANT_ID",
      "value": "",
      "slotSetting": false
    },
    {
      "name": "KEY_VAULT_NAME",
      "value": "",
      "slotSetting": false
    },
    {
      "name": "SECRET_EXPIRATIONDATE_THRESHOLD",
      "value": "",
      "slotSetting": false
    },
    {
      "name": "SENDGRID_ADDR_FROM",
      "value": "",
      "slotSetting": false
    },
    {
      "name": "SENDGRID_ADDR_TO",
      "value": "",
      "slotSetting": false
    },
    {
      "name": "SENDGRID_API_KEY",
      "value": "",
      "slotSetting": false
    },
    {
      "name": "SENDGRID_PASSWORD",
      "value": "",
      "slotSetting": false
    }
```

# アプリケーションのデプロイ
アプリケーションは GitHub の以下のレポジトリで公開しています。git clone などでお手元でコピーして下さい。
https://github.com/ShogoOhe47/azure-keyvault-secret-expirationdate-checker

お手元で実行頂く際、local.settings.json にアプリケーション設定同様の環境変数を設定する必要があります。サンプルは [docs/local.settings.sample.json](https://github.com/ShogoOhe47/azure-keyvault-secret-expirationdate-checker/blob/main/docs/local.settings.sample.json) にありますので、こちらを参考にして下さい。

アプリケーションのローカルでの実行方法、並びにデプロイ方法は以下のドキュメントをご参照下さい。

- [Quickstart: Create a JavaScript function in Azure from the command line - Run the function locally](https://docs.microsoft.com/ja-jp/azure/azure-functions/create-first-function-cli-node?tabs=azure-cli%2Cbrowser#run-the-function-locally)<br />
  ```func start```
- [Quickstart: Create a JavaScript function in Azure from the command line - Deploy the function project to Azure](https://docs.microsoft.com/ja-jp/azure/azure-functions/create-first-function-cli-node?tabs=azure-cli%2Cbrowser#deploy-the-function-project-to-azure)<br />
  ```func azure functionapp publish <APP_NAME>```


タイマートリガーの関数が実行されるタイミングは [function.json](https://github.com/ShogoOhe47/azure-keyvault-secret-expirationdate-checker/blob/main/secret-expirationdate-checker/function.json) の "schedule" で指定します。
初期値ではテスト用として 45 秒に1度実行になっていますが、実運用では週1回程度、多くても1日1回 (App Service 証明書側の自動更新も1日1回のみ実行のため) で十分と考えられます。以下を参考に調整を行って下さい。

- [NCRONTAB 式](https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-bindings-timer?tabs=csharp#ncrontab-expressions)

```
// function.json, "schedule" 書き換え案
"0 50 23 * * *"    // UTC 毎日 23:50, 日本時間 8:50 に実行
"0 50 23 * * Mon"  // UTC 毎週月曜日 23:50, 日本時間 毎週火曜日 8:50 に実行
```

# 実行結果の例
正常に実行できると以下のようなメールが届きます。メッセージの記載方法などはコードを変更することで対応できますのでご確認下さい。
![result-mail.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/349915/5936c7ef-1c9e-081a-4ced-fa37705cdec6.jpeg)

# 最後に
App Service 証明書の有効期限の監視を行いたいというお問い合わせが多く、こちらのサンプルが参考となれば幸いです。
コード自体もシンプルに創ってあります (KeyVault から取得したシークレットの有効期限を基に仕分けを行い、仕分け結果を基にメールの内容を作文している) ので変更などはやりやすいかと思います。バグや機能拡張が出来たら是非とも教えて下さい！