しばやん雑記

Azure Serverless とメイドさんが大好きなフリーランスのプログラマーのブログ

Azure AD における Client Secret の期限切れ問題を Terraform でスマートに解決する

Azure AD にアプリケーションや Service Principal を作成すると大体必要になる Client Secret ですが、有効期限を持っているので期限が切れる前に新しい Client Secret を生成する必要があります。

昔は無期限で作成することも出来ましたが、今はセキュリティ上の理由によって最長でも 2 年、推奨は 6 か月となっています。期限を持っていることによって困るのが、App Service Authentication などに設定している値を定期的に更新する必要があることです。

しかし Terraform を使って全て構成する場合には Client Secret を time_rotating によって、一定の期間で再生成することが出来ます。前回紹介した定義でも利用しています。

テストのために有効期限を短くすると、以下のように Terraform によって再生成されることが確認できます。

明示的に terraform apply を実行する必要はありますが、毎回 Azure Portal や Azure CLI を使って Client Secret を再生成して、アプリケーションに設定することを考えると圧倒的に楽で安全です。

Terraform Provider for Azure AD で time_rotating が利用できるリソースは以下の 2 つです。Azure AD B2C への対応が行われると Policy Key 向けリソースが増えそうです。

Azure AD 周りも全て Terraform で管理することによって、Client Secret は良い感じに再生成することが出来ますが、この再生成時の挙動が非常に重要になって来ます。

先に Client Secret が削除されてしまうと、新しい値が生成されてアプリケーションに再設定されるまでの間だけ正しく動作しないことになります。例えば以下のような定義を用意して実行します。

# 確認のために 1 時間で作り直すようにしている
resource "time_rotating" "test" {
  rotation_hours = 1
}

resource "azuread_application_password" "test" {
  application_object_id = azuread_application.test.object_id
  end_date_relative     = "2h"

  rotate_when_changed = {
    rotation = time_rotating.test.id
  }
}

実際には Client Secret は App Service に設定しているのですが、本題とあまり関係ないので省略しています。

Client Secret を作成してから 1 時間後には再生成が行われるのですが、その時の terraform apply の実行ログが以下のようになります。先に Client Secret の削除が行われるので、再生成されるまでの間はアプリケーションの動作に影響が出てしまいます。

azuread_application_password.test: Destroying... [id=.../password/...]
azuread_application_password.test: Still destroying... [id=.../password/..., 10s elapsed]
azuread_application_password.test: Still destroying... [id=.../password/..., 20s elapsed]
azuread_application_password.test: Destruction complete after 20s
time_rotating.test: Creating...
time_rotating.test: Creation complete after 0s [id=2022-02-23T06:25:33Z]
azuread_application_password.test: Creating...
azuread_application_password.test: Creation complete after 5s [id=.../password/...]
azurerm_app_service.test: Modifying... [id=/subscriptions/.../resourceGroups/rg-rotate-test/providers/Microsoft.Web/sites/app-rotate-test]
azurerm_app_service.test: Still modifying... [id=/subscriptions/.../resourceGroups/rg-rotate-test/providers/Microsoft.Web/sites/app-rotate-test, 10s elapsed]
azurerm_app_service.test: Modifications complete after 16s [id=/subscriptions/.../resourceGroups/rg-rotate-test/providers/Microsoft.Web/sites/app-rotate-test]

Apply complete! Resources: 2 added, 1 changed, 1 destroyed.

先に新しい値を作成し、必要なリソースに再設定してから削除するために、Terraform のリソースに組み込みで用意されている lifecycle ブロックの create_before_destroy プロパティを設定します。

このプロパティを設定すると、名前の通り削除する前に新しいリソースを作成してくれます。

実際に以下のように create_before_destroyazuread_application_password に追加して、再度 Client Secret の再生成時の挙動を確認しておきます。

# 確認のために 1 時間で作り直すようにしている
resource "time_rotating" "test" {
  rotation_hours = 1
}

resource "azuread_application_password" "test" {
  application_object_id = azuread_application.test.object_id
  end_date_relative     = "2h"

  rotate_when_changed = {
    rotation = time_rotating.test.id
  }

  # 既存の Client Secret を削除する前に新しいものを作成するようにする
  lifecycle {
    create_before_destroy = true
  }
}

この定義で terraform apply を実行したログは以下のようになります。

意図した通りに新しい Client Secret を作成し、App Service に設定した後に古い Client Secret を削除していることが確認できます。この順序だと terraform apply の実行中でもアプリケーションに影響しません。

time_rotating.test: Creating...
time_rotating.test: Creation complete after 0s [id=2022-02-22T17:09:22Z]
azuread_application_password.test: Creating...
azuread_application_password.test: Creation complete after 5s [id=.../password/...]
azurerm_app_service.test: Modifying... [id=/subscriptions/.../resourceGroups/rg-rotate-test/providers/Microsoft.Web/sites/app-rotate-test]
azurerm_app_service.test: Still modifying... [id=/subscriptions/.../resourceGroups/rg-rotate-test/providers/Microsoft.Web/sites/app-rotate-test, 10s elapsed]
azurerm_app_service.test: Modifications complete after 15s [id=/subscriptions/.../resourceGroups/rg-rotate-test/providers/Microsoft.Web/sites/app-rotate-test]
azuread_application_password.test (deposed object 34e1bf83): Destroying... [id=.../password/...]
azuread_application_password.test: Still destroying... [id=.../password/..., 10s elapsed]
azuread_application_password.test: Still destroying... [id=.../password/..., 20s elapsed]
azuread_application_password.test: Destruction complete after 21s

Apply complete! Resources: 2 added, 1 changed, 1 destroyed.

ぶっちゃけ Deployment Slot を使ったリリースの場合は、多少のダウンタイムが発生しても影響ないのですが、Terraform で Deployment Slot の Swap を行うのは結構面倒なので避けています。

ここまでの結果を踏まえると Azure AD の Client Secret を Terraform で作成する場合の最適な定義は、以下のようなものになるはずです。Client Secret 自体は 180 日の期限にしつつ time_rotating は余裕を持った期限にして、Client Secret の期限ギリギリ更新になるのを避けます。

# Client Secret を作り直すタイミングは余裕をみて期限切れ 30 日前にしている
resource "time_rotating" "test" {
  rotation_days = 150
}

# 推奨に従い 180 日で期限が切れる Client Secret を作成する
resource "azuread_application_password" "test" {
  application_object_id = azuread_application.test.object_id
  end_date_relative     = "4320h"

  rotate_when_changed = {
    rotation = time_rotating.test.id
  }

  lifecycle {
    create_before_destroy = true
  }
}

多くの方が Client Secret の期限切れ問題に悩まされてきたと思いますが、Terraform Provider for Azure AD を使うとスマートに解決できるのでかなりおすすめです。

これまで正直 Azure AD に対する苦手意識というか、分かりにくさしか感じていなかったのですが、Terraform を使ってコードに落とし込むことでかなり理解が進んでいます。