Terraform で Azure AD アプリケーションをオシャレに定義しようとして諦める

Azure AD を扱っていると、やはりどうしても Infrastructure as Code を実現したくなる瞬間が多々あります。そんな時、[Terraform の Azure Active Directory Provider](https://www.terraform.io/docs/providers/azuread/index.html)を用いるという選択肢は有用でしょう。
便利そうですね！ 使ってみましょう。

私の場合、Azure AD  `azuread_application` という Resource を扱う必要があり、更に今回の用途では、Microsoft Graph API のパーミッションを付与する必要がありました。これの `resource_app_id` が必要そうです。[Graph Explorer](https://developer.microsoft.com/ja-jp/graph/graph-explorer) で見つけてみましょう。

```
GET https://graph.microsoft.com/beta/servicePrincipals?$filter=displayName eq 'Microsoft Graph'
(中略)
            "appDisplayName": "Microsoft Graph",
            "appId": "00000003-0000-0000-c000-000000000000",
(中略)
            "appRoles": [
                {
                    "allowedMemberTypes": [
                        "Application"
                    ],
                    "description": "Allows the app to manage all schedules, schedule groups, shifts and associated entities in the Teams or Shifts application without a signed-in user.",
                    "displayName": "Read and write all schedule items",
                    "id": "b7760610-0545-4e8a-9ec3-cce9e63db01c",
                    "isEnabled": true,
                    "origin": "Application",
                    "value": "Schedule.ReadWrite.All"
                },
                {
                    "allowedMemberTypes": [
                        "Application"
                    ],
                    "description": "Allows the app to read all schedules, schedule groups, shifts and associated entities in the Teams or Shifts application without a signed-in user.",
                    "displayName": "Read all schedule items",
                    "id": "7b2ebf90-d836-437f-b90d-7b62722c4456",
                    "isEnabled": true,
                    "origin": "Application",
                    "value": "Schedule.Read.All"
                },
```

Microsoft Graph API の `app_id` は `00000003-0000-0000-c000-000000000000` であることがわかりますね！ 
Microsoft Graph API が用意しているパーミッションの ID などもずらりと出てきており、これを使うと良さそうです。

それでは Terraform に落とし込んでいきましょう……。


```hcl
resource "azuread_application" "foo_app" {
  name = "foo_app"

  required_resource_access {
    # Microsoft Graph API Application ID
    resource_app_id = "00000003-0000-0000-c000-000000000000"

    resource_access {
      # Directory.Read.All
      id   = "06da0dbc-49e2-44d2-8312-53f166ab848a"
      type = "Scope"
    }

    resource_access {
      # User.Read
      id   = "e1fe6dd8-ba31-4d61-89e7-88639da4683d"
      type = "Scope"
    }
  }
}
```

問題はなさそうです。ですが、冗長で Terraform らしくない感じがしますね。

Azure Active Directory Provider のドキュメントを眺めていると、 [azuread_service_principal](https://www.terraform.io/docs/providers/azuread/d/service_principal.html)というデータソースが存在している事にそのうち気付くでしょう。
これを使えば、 Microsoft Graph API の `application_id` さえ記述すれば、データソースの返り値をうまいこと使い回せそうです……定義してみましょう。

```hcl
data "azuread_service_principal" "microsoft_graph" {
  application_id = "00000003-0000-0000-c000-000000000000"
}

resource "azuread_application" "foo_app" {
  name = "foo_app"

  required_resource_access {
    resource_app_id = data.azuread_service_principal.microsoft_graph.application_id
    resource_access {
      # Directory.Read.All
      id   = "06da0dbc-49e2-44d2-8312-53f166ab848a"
      type = "Scope"
    }

    resource_access {
      # User.Read
      id   = "e1fe6dd8-ba31-4d61-89e7-88639da4683d"
      type = "Scope"
    }
  }
}
```

いいですね！
`microsoft_graph` というデータソースには、そのサービスプリンシパルで利用できる全パーミッションがぶら下がっています。 `resource_access` ブロック内の各パーミッションの ID も、もう少し Terraform らしく書けそうです。
つまり、全パーミッションの中から、実際に利用したいパーミッションだけをフィルタしつつ、 `resource_access` を繰り返し記述せずに済むように記述していきましょう。

```hcl
data "azuread_service_principal" "microsoft_graph" {
  application_id = "00000003-0000-0000-c000-000000000000"
}

resource "azuread_application" "foo_app" {
  name = "foo_app"

  required_resource_access {
    resource_app_id = data.azuread_service_principal.microsoft_graph.application_id

    dynamic "resource_access" {
      for_each = matchkeys(
        data.azuread_service_principal.microsoft_graph.app_roles.*.id,
        data.azuread_service_principal.microsoft_graph.app_roles.*.value,
        [
          "Directory.Read.All",
          "User.Read"
        ]
      )
      content {
        id   = resource_access.value
        type = "Role"
      }
    }
  }
}
```

いいですね、 Terraform らしい記述になったのではないでしょうか。
[Dynamic configuration blocks](https://www.terraform.io/docs/configuration/expressions.html#dynamic-blocks) と [`matchkeys` Functions](https://www.terraform.io/docs/configuration/functions/matchkeys.html) は Terraform v0.12 で使えるようになった機能なので、Terraform 本体のバージョンが古い場合は先に Terraform のアップデートをする必要があるでしょう。

私の場合、Azure Portal 上で手動で設定したものを Terraform に落とし込む必要がありました。 `terraform import` を行ってから `terraform plan` を流してみましょう……。

おっと、 `matchkey` Function はマッチした第一引数のインデックス順に従うため、 `data.azuread_service_principal.microsoft_graph.app_roles` の順序に従うことになり、Azure AD 側で既に設定されているパーミッションの順序と差分が生じてしまいました。
具体的には、実際には `User.Read` が先頭に、 `Directory.Read.All` が2番目に設定されているようです。

おしゃれな記述を諦めて、データソースを使いつつ、実際に設定されているパーミッションとの順序に従うように Terraform を書き換えてみましょう。

```hcl
data "azuread_service_principal" "microsoft_graph" {
  application_id = "00000003-0000-0000-c000-000000000000"
}

resource "azuread_application" "foo_app" {
  name = "foo_app"

  required_resource_access {
    resource_app_id = data.azuread_service_principal.microsoft_graph.application_id

    resource_access {
      id   = [for x in data.azuread_service_principal.microsoft_graph.app_roles : x.id if x.value == "User.Read"][0]
      type = "Role"
    }
    resource_access {
      id   = [for x in data.azuread_service_principal.microsoft_graph.app_roles : x.id if x.value == "Directory.Read.All"][0]
      type = "Role"
    }
  }
}
```

Terraform 経由でこのリソースを作成していれば、Dynamic configuration blocks でおしゃれに記述できたものの、あいにく、既存のリソースを import する形になった為に、おしゃれな Terraform を諦める形になってしまいましたが、それでも諸々の ID をベタ書きするよりは遥かにマシな記述になったのではないでしょうか。

みなさんもぜひ現実と Terraform の理想との折り合いをつけていきながら、快適な Terraform 生活を過ごしていきましょう！