この記事の3行まとめ
はじめに
IaCという言葉、よく聞きます。Infrastructure as Code、つまりインフラの構成をコードで管理するという考え方で、サーバーやネットワークの世界ではもう当たり前になりつつあるようです。
ただ、私自身は一度も触ったことがありませんでした。名前と、だいたいの意味を知っているだけ。
気になっていたことが、2つありました。
ひとつは、IdPのようなID基盤でもIaCは使われるのか。サーバーやネットワークの事例はよく見かけますが、Entra IDをコードで管理しているという話はあまり聞きません。そもそも対応しているのかどうかも知りませんでした。
もうひとつは、仮に使えるとして、その価値がどこにあるのか。管理画面をクリックして設定するのと、コードを書いて実行するのとで、いったい何が変わるのか。作業が速くなるという話なのか、それとも別の何かなのか。
読んで分かった気になるより、自分で試したほうが早いと思いました。そこで、個人のアカウントと無料枠だけを使って、Entra IDをTerraformで触ってみました。比べる相手は管理画面での手作業です。手順書を書いて、そのとおりに操作するという、よくあるやり方にしました。
そのなかで特に確かめたかったのは、変更の履歴を本当に追えるのか、という点です。IaCの利点としてよく挙げられるものですが、どういう意味で追えるのかが、説明を読んだだけでは分かりませんでした。
何を試したか
やったことはとても単純です。
まったく同じ内容のセキュリティグループを、2つ作りました。ひとつは手順書を書いてEntra管理センターから手作業で、もうひとつはTerraformのコードを書いて実行しました。
そのうえで、2つとも同じ文言で、申請も承認もなしに管理画面から書き換えました。現場でときどき起きる、いわゆる無断変更の再現です。
そのあとで、それぞれの方式が変更に気づけるかどうかを見ています。
今回やっていないことも先に書いておきます。Pull Requestによる承認や、作業者と承認者を分ける職務分離、CI/CDからの自動実行までは踏み込んでいません。手元でTerraformを動かしただけの段階です。ここは次の機会に回します。
環境を用意する
条件はひとつだけ決めていました。会社の環境は一切使わず、個人のアカウントと無料枠だけで完結させることです。検証で本番のID基盤を触るわけにはいきません。
用意したのは、検証用に新しく作った個人のMicrosoftアカウントと、そこから作成したAzureの無料アカウントです。Entra IDのテナントもこの流れで手に入ります。今回作るのはEntra IDのなかのグループだけなので、課金対象のAzureリソースは1つも作っていません。
道具はTerraformとAzure CLIです。どちらもHomebrewで入れました。
手順書で作ってみる
最初に手順書のほうから作りました。作るのはセキュリティグループを1つ、名前はmanual-lab-usersで。
手順書は77行になった
手順書はAIに作ってもらいました。変更申請から作業手順、切り戻しまで入れた形で、と頼んだら、それらしいものが出てきます。比較を公平にしたかったので、出てきたものは削らずにそのまま使いました。
全文はこうです。テナントのドメインだけ伏せています。
# 変更作業手順書:検証用セキュリティグループの作成
## 1. 変更申請
| 項目 | 内容 |
| --- | --- |
| 変更ID | CHG-001 |
| 目的 | 業務アプリの利用者管理に用いるセキュリティグループを新規作成する |
| 対象テナント | 既定のディレクトリ(<テナントのドメイン>) |
| 変更対象 | Microsoft Entra ID / セキュリティグループ 1件 |
| 影響範囲 | 新規作成のため既存への影響なし。作成されるグループにメンバーは含まない |
| 作業予定日時 | YYYY-MM-DD HH:MM |
| 作業者 | (記入) |
| 承認者 | (記入) |
| リスク | 誤ったテナントでの作成。グループ名の誤入力 |
| 切り戻し条件 | 誤ったテナントに作成した場合、または名称誤りが判明した場合 |
> 注: 本検証では申請者・作業者・承認者がすべて同一人物である。本来は職務分離のため作業者と承認者を分ける必要がある。
## 2. 事前確認
| # | 確認事項 | 確認方法 | 結果 |
| --- | --- | --- | --- |
| 2-1 | 対象テナントに管理者権限でログインできる | Entra 管理センターにログイン | ☐ |
| 2-2 | ログイン先テナントが対象テナントである | 画面右上のアカウント情報でテナント名を確認 | ☐ |
| 2-3 | 同名のグループが存在しない | グループ一覧で `manual-lab-users` を検索 | ☐ |
## 3. 作業手順
| # | 操作 | 入力値 | 期待結果 | 証跡 |
| --- | --- | --- | --- | --- |
| 3-1 | https://entra.microsoft.com にアクセスし、ログインする | — | ホーム画面が表示される | — |
| 3-2 | 画面右上でテナント名を確認する | — | 「既定のディレクトリ」と表示される | ★取得 |
| 3-3 | 左メニュー「ID」→「グループ」→「すべてのグループ」を開く | — | グループ一覧が表示される | ★取得 |
| 3-4 | 「新しいグループ」を選択する | — | グループ作成画面が表示される | — |
| 3-5 | 「グループの種類」を選択する | セキュリティ | 選択が反映される | — |
| 3-6 | 「グループ名」を入力する | `manual-lab-users` | 入力値が表示される | — |
| 3-7 | 「グループの説明」を入力する | `手順書方式の検証用グループ` | 入力値が表示される | — |
| 3-8 | 「Microsoft Entra ロールをグループに割り当てる」を選択する | いいえ | 選択が反映される | — |
| 3-9 | 「メンバーシップの種類」を選択する | 割り当て済み | 選択が反映される | — |
| 3-10 | 入力内容を確認する(3-5〜3-9 をすべて見直す) | — | 全項目が指定どおり | ★取得 |
| 3-11 | 「作成」を選択する | — | 作成完了の通知が表示される | ★取得 |
> 注: Entra 管理センターの UI 表記は変更されることがある。ラベルが一致しない場合は作業を中断し、手順書を改訂してから再開すること。
## 4. 作業後確認
| # | 確認事項 | 確認方法 | 結果 |
| --- | --- | --- | --- |
| 4-1 | グループが一覧に表示される | グループ一覧で `manual-lab-users` を検索 | ☐ |
| 4-2 | グループ名が正しい | グループ詳細画面 | ☐ |
| 4-3 | 説明が正しい | グループ詳細画面 | ☐ |
| 4-4 | グループの種類がセキュリティである | グループ詳細画面 | ☐ |
| 4-5 | オブジェクト ID を記録した | グループ詳細画面からコピー | ☐ |
## 5. 切り戻し手順
| # | 操作 | 期待結果 |
| --- | --- | --- |
| 5-1 | グループ一覧で対象グループを選択する | 詳細画面が表示される |
| 5-2 | 「削除」を選択し、確認ダイアログで承諾する | グループが削除される |
| 5-3 | 一覧に表示されないことを確認する | 検索結果が0件になる |
> 注: 削除されたグループは30日間は復元可能だが、それ以降は完全に削除される(要確認)。
## 6. 作業結果記録(作業者が記入)
| 項目 | 内容 |
| --- | --- |
| 実施日時 | |
| 実施者 | |
| 実施結果 | ☐ 完了 / ☐ 一部完了 / ☐ 中止 |
| 手順との差異 | |
| 発生したエラー | |
| 作成されたオブジェクト ID | |
| 取得した証跡 | |
| 確認者 | |全部で77行です。これが手順書方式の成果物になります。
グループを1つ作るだけで、変更申請の項目が10個、事前確認が3つ、作業手順が11ステップ、作業後確認が5項目、切り戻し手順が3ステップ、記入欄が8つ。作業そのものより、その前後に書くことのほうが多い文書です。
3分で終わった
作業自体はあっという間でした。管理センターを開いて、グループを新規作成して、名前と説明を入れて、作成ボタンを押すだけです。3分もかかっていません。
手作業が速いことは、IaCと比べたときに軽視されがちですが、はっきりした利点だと思います。
手順書が実物と合っていなかった
ところが、書いてあるとおりには進みませんでした。
手順書の作業手順3-8に「Microsoft Entraロールをグループに割り当てる」で『いいえ』を選ぶ、とあったのですが、実際の作成画面にその項目がありませんでした。テナントのライセンス状況によって表示が変わるようです。
AIが書いたものを実物と突き合わせずにそのまま持っていったので、作業を始めるまで気づきませんでした。作ってから実行するまで、1時間も経っていません。
手順書は環境差やUIの変更で簡単に陳腐化するのだろうと思います。それにしても、こんなに早く実物で確認することになるとは思いませんでした。実運用でも、画面が変わるたびに書き直し続けることになりそうです。
もうひとつ、あとから確かめたことがあります。手順書の5章の注記は「削除されたグループは30日間は復元可能だが、それ以降は完全に削除される(要確認)」と、AI自身が確認を保留していました。実際に調べると、Microsoft 365グループとクラウドセキュリティグループは削除後30日間は復元でき、この期間は変更できません。つまり注記の内容自体はおおむね合っていました。ただし、セキュリティグループの復元はプレビュー機能で、パブリッククラウドでしか使えません。今回作ったのはまさにクラウドセキュリティグループなので、切り戻し手順の土台をプレビュー機能に預けていたことになります。AIが「要確認」と書いた箇所は、たいてい本当に確認が要るのだと思いました。
参考:Restore a deleted Microsoft 365 group or cloud security group
Terraformで作ってみる
次に同じものをTerraformで作ります。名前はterraform-lab-usersにしました。
コードは19行だった
こちらもAIに書いてもらいました。ファイルは1つだけです。
terraform {
required_providers {
azuread = {
source = "hashicorp/azuread"
version = "~> 3.0"
}
}
}
# 対象テナントをコードに固定する。意図しないテナントには適用されない。
provider "azuread" {
tenant_id = "<テナントID>"
}
resource "azuread_group" "lab" {
display_name = "terraform-lab-users"
description = "Terraform管理の検証用グループ"
security_enabled = true
}19行です。手順書の77行と比べると短く見えますが、短いから優れているという話ではありません。この2つは表現しているものが違います。手順書は「誰が、いつ、どういう条件で、どう操作し、どう確認するか」という作業のプロセスを書いています。コードのほうは「最終的にどういう状態であってほしいか」だけを書いていて、手順は一切書いていません。
ひとつだけ補足すると、providerブロックに対象テナントのIDを直接書いています。これを書いておくと、意図しないテナントには構造的に適用できません。検証用のテナントと業務で使うテナントを取り違える事故は、実際に起こりえます。手順書に「対象テナントを確認する」と書くのと違って、この一行には強制力があります。
名前でつまずいた
書きながら引っかかったところを、まとめて書いておきます。どれもEntra IDをTerraformで触ろうとすると最初に出てくる話です。
まず、管理したいのはMicrosoft Entra IDなのに、Providerの名前がazureadです。Entra用のものを探したのですが、見つかりませんでした。Entra IDが2023年までAzure Active Directoryという名前だったためで、製品名は変わってもProvider名はhashicorp/azureadのまま据え置かれています。HashiCorpのチュートリアルも「Manage Microsoft Entra ID users and groups」というタイトルで、中身はこのazureadを使っていました。
参考:Manage Microsoft Entra ID users and groups
同じことが他の道具にもあります。認証に使ったAzure CLIはコマンド名がazで、グループの一覧を出すにはaz ad group listと打ちます。このadもActive Directoryのことでした。テナントを手に入れる入口も、Azureの無料アカウントです。管理しているのはEntra IDで、Azureのリソースは1つも作っていないのに、道具の名前にはAzure時代のものが残っています。検索するときは、新旧どちらの名前でも引いたほうが早いと分かりました。
次に紛らわしかったのが、azurermという別のProviderです。名前が似ているので、最初はどちらを使うのか分かりませんでした。調べたら担当が分かれていて、azureadはMicrosoft Entra IDを見ていて、裏でMicrosoft Graph APIを叩いています。対象はユーザー、グループ、アプリの登録といったIDそのもので、単位はテナント。azurermのほうはAzureのリソースで、仮想マシンやストレージやネットワークが対象、単位はサブスクリプションとリソースグループになります。今回使ったのはazureadだけです。
ややこしそうだと思ったのが、その境目です。たとえば「作ったグループに、あるサブスクリプションの閲覧者権限を与える」という作業は、Providerをまたぐようです。グループを作るのがazureadで、そのグループにAzure側の権限を割り当てるのがazurermのazurerm_role_assignmentになります。ここはまだ試していません。
「ロール」という言葉が2種類あるのも、しばらく飲み込めませんでした。グローバル管理者のようなEntra IDのディレクトリロールと、所有者や閲覧者のようなAzureのRBACロールは別物で、担当するProviderも違います。日本語だとどちらも「ロールの割り当て」なので、読んでいて何度か混乱しました。
実行前に何が起きるか見える
Terraformにはplanというコマンドがあります。何も変更せず、これから何が起きるかだけを表示します。
Plan: 1 to add, 0 to change, 0 to destroy.グループを1つ追加する、削除や変更はない、と言っています。
手順書方式には、これに相当するものがありません。手順書のレビューは人が読んで判断するものですし、実行前に機械が差分を出してくれるという発想自体がありませんでした。
実際に適用するのはapplyです。確認を求められるのでyesと答えると、グループが作られました。
所要時間は手作業と同じだった
ここは記事の中でいちばん正直に書きたいところです。
Terraform側も3分程度でした。手作業と互角です。
Terraformを触るのは初めてだったので、調べる時間やインストールの時間まで含めれば、むしろ手作業より遅く、比較にもなりません。
IaCの効果が初回の構築で出ないというのは、実務でもよく言われることだと思います。今回それを実感しました。差が出るのは2回目以降の変更や、同じ構成を別の環境に複製するときです。そしてもうひとつの差は、このあと出てきます。
管理画面では2つの区別がつかない
グループが2つ揃いました。管理センターで一覧を開くと、こう見えます。
どちらが手作業で作られ、どちらがコードで作られたのか、画面からは分かりません。名前が違うだけで、種類もメンバーシップも同じです。Entra IDから見れば、2つは完全に対等なオブジェクトです。
ところが、Terraform側から同じものを見ると景色が変わります。管理しているリソースを一覧するコマンドがあるので打ってみます。
$ terraform state list
azuread_group.lab1つしか出てきません。
手作業で作ったグループは、Terraformの視野に入っていません。存在しないものとして扱われています。
この落差が、このあとの結果をそのまま説明することになります。
両方を勝手に書き換える
ここから本題です。
管理センターから、2つのグループの説明文を手で書き換えました。申請も承認もありません。文言は両方とも説明を勝手に書き換えましたに統一しています。
同じ文言、同じ操作、同じ人、2分違いです。条件を完全に対等にしたうえで、あとは方式の違いだけを見ます。
terraform planを打つ
無断変更を起こした状態で、何も変更を加えずにplanを打ちます。
# azuread_group.lab will be updated in-place
~ resource "azuread_group" "lab" {
~ description = "説明を勝手に書き換えました" -> "Terraform管理の検証用グループ"
id = "/groups/5517e118-..."
# (29 unchanged attributes hidden)
}
Plan: 0 to add, 1 to change, 0 to destroy.
コマンドひとつ、数秒です。しかも「何かが変わった」ではなく、どの属性が、何から何に変わったのかまで出ています。
差分の向きにも注目してください。左が現在の実環境の値、右がコードに書いてある値です。Terraformは「実環境がコードからずれている」と報告しています。比較の基準になる「あるべき状態」がコードとして存在しているので、こういう言い方ができます。
隠れている29個の属性にも触れておきます。Terraformはグループの30以上の属性をすべて突き合わせて、変わっていない29個を「変わっていない」と確認したうえで省略表示しています。手順書に書かれていた作業後チェックリストは5項目でした。しかもその5項目は、作業直後に1回動くだけで、無断変更のあとには誰も実行していません。
そしてもうひとつ、この出力で重要なのは書かれていないことのほうです。
manual-lab-usersは、1文字も出てきません。
ほぼ同じ時刻に、同じ文言で、同じように書き換えたのに、Terraformは片方について完全に沈黙しています。管理下にないからです。
ここを読み飛ばすと、この記事の話は「IaCを入れれば安心」という結論になってしまいます。実際はそうではありません。IaCが守ってくれるのは、IaCの管理下にあるものだけです。管理外のリソースは、コードを導入していても手順書時代とまったく同じく野放しになります。
既存環境にTerraformを後から入れる場合、手作業で作られたリソースは自動的には管理下に入りません。既存分をどうやって取り込むかが、実務では最初の壁になりそうだと感じました。
「監査ログがあるから大丈夫」を確かめる
ここまで書くと、情シスの方なら当然こう思うはずです。
Entra IDには監査ログがある。無断変更なんて、そこを見れば分かるのでは。
なので実際に見にいきました。
開いたら別のもので埋まっていた
管理センターの監査ログを開いてみると当然ながら画面いっぱいにAdd service principalが並んでいます。Microsoft Graph、Azure Resource Manager、Office 365 Configure、Azure ESTS Serviceと続き、どれもテナントを作った瞬間に自動生成されたイベントでした。自分がやった操作は1件も見えません。
作ってから2時間しか経っていない、グループが2つしかないテナントでこれです。並び順が古い順になっていたのが直接の原因ですが、実運用のテナントなら、規模によっては1日に数千件のイベントが記録されることもあるでしょう。
「監査ログを見れば分かる」というのは、正確には「監査ログを探しにいけば分かる」でした。誰かが能動的に探しにいかないかぎり、そこに記録があること自体には意味がありません。
探し方を変えると見つかった
並び順を新しい順に変えたところ、目的のイベントが見つかりました。
12:07:03 Update group terraform-lab-users
12:05:05 Update group manual-lab-usersさきほど手で書き換えた2件が、表示されています。
ここで大事なのは、manual-lab-usersの変更もちゃんと記録されていることです。Terraformが完全に沈黙した領域を、監査ログはきちんとカバーしていました。監査ログは管理下かどうかに関係なく、テナント全体を見ています。これは手順書方式にとってフェアな事実なので、はっきり書いておきます。
もうひとつの制約
保持期間についても書いておきます。Microsoftの公式ドキュメントによれば、Entra ID Freeの監査ログとサインインログの保持期間は7日間です。P1とP2で30日になります。それより長く保持したい場合は、Azure Monitor経由でストレージに転送するなどの構成が別途必要になります。
参考:Microsoft Entra data retention
半年後に「あの変更は誰がいつ何のためにやったのか」を説明する必要が出たとき、Freeのままでは監査ログには何も残っていません。
片方だけが元に戻った
ここでterraform applyを実行しました。コードに書いた状態へ戻す操作です。
そのうえで、2つのグループの状態を並べてみます。
terraform-lab-users Terraform管理の検証用グループ
manual-lab-users 説明を勝手に書き換えました
ほぼ同じ時刻に、同じ人が、同じ文言で書き換えた2つのグループです。15分後、片方だけが元の状態に戻り、もう片方は書き換えられたまま放置されています。
誰もmanual-lab-usersを直しませんでした。直すべきだと教えてくれる仕組みが、どこにもなかったからです。
是正のあと、監査ログをもう一度見た
applyを実行したので、監査ログにも記録が増えているはずです。開いてみると、確かに3件目がありました。
12:21:51 Update group terraform-lab-users ← Terraformによる是正
12:07:03 Update group terraform-lab-users ← 管理画面から手で書き換えた
12:05:05 Update group manual-lab-users ← 管理画面から手で書き換えたこれでようやく、コード経由の変更と手作業の変更が、同じログの上に並びました。見比べられます。
由来は区別できなかった
気になったのは、この3件のうちどれがTerraform由来でどれが手作業由来か分かるのか、という点です。
一覧を見るかぎり、3件は同じ形をしていました。アクティビティ名はどれもUpdate groupで、状態はすべて成功、ユーザーエージェントは3件とも空欄です。
詳細画面も開いて、管理画面から書き換えたとき(12:07)と、Terraformが書き換えたとき(12:21)を並べました。
どちらも開始者の欄には「種類:ユーザー」とオブジェクトIDがあるだけで、表示名もアプリケーション名も入っていません。TerraformはAzure CLI経由で認証しているので、そこにMicrosoft Azure CLIのような情報が出るかと思っていましたが、少なくともこの画面では確認できませんでした。
つまり、監査ログからは、コード経由の変更と管理画面からの手作業を区別できませんでした。承認を経た変更なのか、誰かが勝手に触ったのかも、当然分かりません。
ただしこれは、Terraformを私自身のアカウント(Azure CLIのユーザー認証)で動かしていたからでもあります。ユーザーとしてサインインした場合、開始者に残るのはユーザーであって、呼び出し元のアプリは記録されていませんでした。実行用のサービスプリンシパルを分けてアプリとしてサインインすれば、開始者の欄はそのアプリになるので、どのIDが実行したかは区別できるようになるはずです。ただしそれで分かるのはIDであって、Terraform経由かどうかでも、承認された変更かどうかでもありません。ここは今回試していません。
予想が外れた部分
ここで、私の予想がはっきり外れたことがひとつあります。
詳細画面には「変更されたプロパティ」というタブがあります。書き始める前は、監査ログは「変わった」ことしか記録していないだろうと思っていました。開いてみたら、こうなっていました。
Description ["説明を勝手に書き換えました"] → ["Terraform管理の検証用グループ"]変更前後の値が、しっかり入っていました。terraform planが出したのと同じ情報です。
これを確認せずに記事を書いていたら、「監査ログは中身を持っていない」という誤った説明をするところでした。監査ログは、想像していたよりずっとよくできています。
ただ、この事実が分かったことで、本当の差がどこにあるのかがはっきりしました。
では、コードの側には何が残っているのでしょうか。ここを確認しないまま話を進めるわけにはいかないので、次に見ていきます。
Terraform側に記録は残っていたのか
Entra IDの監査ログはひととおり見ました。では、Terraformの側には何が残っているのか。作業ディレクトリを開いて、あるものを数えてみました。
main.tf…現在のあるべき状態の定義terraform.tfstate…現在の実環境の状態。33個の属性が記録されているterraform.tfstate.backup…その1世代前.terraform.lock.hcl…使っているProviderのバージョン固定
これだけでした。気づいたことが2つあります。
ひとつは、どれも「今」しか表していないことです。main.tfを見れば今どうあるべきかは分かりますが、いつ誰が何のためにその行を書いたのかは、どこにも書いてありません。Stateも現在の状態だけで、履歴はバックアップの1世代分しかありません。
もうひとつは、実行した記録がどこにも残っていないことです。planの出力もapplyの出力も、ターミナルの画面に流れただけでした。スクリーンショットを撮っていなければ、いつ何を適用したのかを後から示すものは何もありません。
Entra ID側の監査ログにはUpdate groupが残っていました。しかしそれはEntra側の記録であって、Terraform側の記録ではありません。「どのコードを適用した結果なのか」は、そこには書かれていません。
つまり今回作った構成は、あるべき姿の定義は持っているけれど、何をいつ実行したかの記録は持っていないという状態でした。半分しかありません。
Terraform自身が最初から警告していた
思い返すと、Terraformは教えてくれていました。planを打ったときの出力の末尾に、こう書いてあります。
Note: You didn't use the -out option to save this plan, so Terraform can't
guarantee to take exactly these actions if you run "terraform apply" now.-outというオプションを使っていないので、いまapplyしてもこのplanとまったく同じ動作になる保証はない、という意味です。
読み飛ばしていましたが、調べてみると大事な話でした。terraform plan -out=tfplanのように結果をファイルに保存しておくと、そのplanをレビューして、承認されたそのファイルをapplyに渡せるそうです。承認した内容と実行した内容が一致することを、仕組みとして担保できます。そういう使い方があるとは思っていませんでした。
私がやったのは、planの画面を見て納得して、そのあと素のapplyを打つという流れでした。目で見て納得したという事実は、どこにも残りません。
記録を残すなら、何が必要か
記録を残すには何が必要なのか。調べながら整理していったら、5つ出てきました。
1. Gitでコードの変更履歴を残す
.gitignoreは最初に作ったのに、git initをしていませんでした。準備だけして使っていなかったので、コードの変更履歴は1件もありません。
ここを入れると、いつ誰がどの行を変えたのかが残ります。そしてコミットメッセージという形で「なぜ変えたのか」も残ります。
これは道具を増やさずにその場で試せるので、実際にやってみました。次の節に書きます。
2. Stateをリモートに置く
terraform.tfstateは手元のファイルが1つあるだけです。これが消えると、コードと実環境の対応関係が分からなくなります。バックアップも1世代しかありません。バージョニングの利くリモートの保管先に置く必要があります。
なおStateには実環境の値が33個記録されていました。今回はグループなので中身は無害でしたが、扱うものによっては秘密情報がそのまま平文で入るようです。そのためGitに入れてはいけないファイルだと理解しました。
3. planを保存して、承認されたものを適用する
上に書いた-outの話です。これをやらないと、承認と実行のあいだに何が起きても分かりません。
4. 実行ログを保管する
手元で実行しているかぎり、ログはターミナルにしか残りません。CI/CDから実行する形にすると、誰が起動したか、どのコミットを適用したか、出力は何だったかが、パイプラインの記録としてまとめて残ります。
5. 定期的にplanを打つ
これが一番大事だと感じました。
今回、無断変更が見つかったのは、私が意図的にplanを打ったからです。打たなければ、差分はそこにあるまま、何も起きません。設定は書き換わったままで、誰も気づかないままになります。
planは、打てば答えを返してくれます。けれども打つ人がいなければ、手作業の運用と何も変わりません。定期実行にして、差分が出たら通知が飛ぶところまで作らないと、検知の仕組みとしては完成しないという結論になりました。
Gitを入れて、同じ変更をもう一度やってみた
上に挙げた5つのうち、1つ目のGitだけは道具を増やさずに試せます。環境がまだ動いているので、その場でやってみました。
手順は4つです。git initして現在の構成をコミットし、main.tfの説明文を1行だけ変え、変更理由をコミットメッセージに書いて、terraform applyで適用しました。
変更内容は、グループの説明文を「Terraform管理の検証用グループ」から「検証用。Terraform管理のため管理画面からの直接変更は禁止」に変えるというものです。棚卸しのときにコード管理下だと判別できるようにしたかった、という理由で変えています。
コミットしたあとにgit log -pを打つと、こうなります。
残っているものを数えると、4つ並んでいます。誰が変えたのか(Author)、いつ変えたのか(Author Date)、何を変えたのか(差分の赤と緑)、そしてなぜ変えたのか(本文の2行)です。
ただし、この4つは同じ強さではありません。「誰が」と「いつ」はGitが確認した事実ではなく、コミットする側が申告した値です。名前も日付も手元の設定で決められるので、他人の名前を入れたコミットもそのまま通ります。認証された「誰が」「いつ」を持っているのは次に見る監査ログの側です。
次に、まったく同じ変更をEntra IDの監査ログ側で見てみます。
Descriptionの前後の値は、git logの差分と同じものが入っています。ここは前の節で確認したとおりです。
ただ、「棚卸しのときに判別できるようにする」という理由は、どこにもありません。探して見つからなかったのではなく、そういう項目が存在しません。監査ログは何が起きたかを記録する仕組みなので、当然といえば当然です。
そしてもうひとつ、この変更が承認されたものかどうかも書かれていません。記録の形は、前に見た無断変更のときとまったく同じです。
planの差分だけでは、正しい変更かどうか分からない
ここで気づいたことがあります。
今回のplanの出力は、無断変更のときとほとんど同じ形をしていました。
~ description = "Terraform管理の検証用グループ" -> "検証用。Terraform管理のため管理画面からの直接変更は禁止"
Plan: 0 to add, 1 to change, 0 to destroy.けれども、差分が出た理由は逆です。無断変更のときは実環境を勝手に書き換えられたから差分が出ました。今回はコードを意図的に書き換えたから差分が出ています。
Terraformから見れば、どちらも「コードと実環境がずれている」という同じ状態です。planの出力だけでは、そのずれが正しい変更なのか無断変更なのかを区別できません。
区別できるのは、その差分に対応するコミットがGitにあるかどうかです。コミットがあれば意図した変更で、なければ誰かが実環境を直接触ったということになります。
planは「ずれているか」を教えてくれます。しかし「ずれていていいのか」は教えてくれません。それを判断する材料は、Gitの側にあります。
わかったこと
手順書とコードで同じものを作り、両方に同じ無断変更を仕掛けて、記録の残り方を追いかけてきました。分かったことを整理します。
差は「中身の有無」ではなかった
検証を始める前、私は「コードにすれば変更履歴を追える」という素朴な期待を持っていました。実際に試した結果は、もう少し込み入っていました。
変更内容そのものは、監査ログにも残っていました。誰がいつ何をどう変えたかは、Entra IDの側でもきちんと記録されています。そこは差ではありませんでした。
差が出たのは、次の3点です。
ひとつ目は、問いの立て方です。監査ログが答えるのは「何が起きたか」で、過去のイベントが時系列に並んでいます。そこから現在の状態を知るには、全部のイベントを頭のなかで積み上げる必要があります。しかもFreeでは7日で消えるので、8日前の変更は積み上げられません。対してterraform planが答えるのは「今ずれているか」で、これは現在の状態そのものです。
ふたつ目は、答えを得るまでの手間です。監査ログは探しにいかなければ何も出てきません。並び順を変えたり、1件ずつ開いて中身を見たりします。誰も探しにいかなければ、Freeなら記録は静かに7日で消えていきます。planのほうも打つ人は必要ですが、打てば管理下の全リソースについて一度に答えが返ります。どちらも人が動かなければ始まらない点は同じで、違うのは動いたあとの手間の量です。
そして3つ目が、いちばん本質的だと感じた点です。
監査ログは「DescriptionがAからBに変わった」と記録します。しかし、AとBのどちらが正しいのかは、監査ログには書かれていません。
正しさを判定するには、監査ログの外側に「あるべき姿の定義」が必要です。手順書方式では、それは承認済みの変更申請書の山であり、突き合わせるのは人間です。IaCでは、その定義がコードそのものなので、判定がplanの中に組み込まれています。
結論
今回の検証で私が持ち帰ったのは、記録が3つの層に分かれているという整理です。どれも別のものを持っていて、互いを代替しません。
Entra IDの監査ログは「何が起きたか」を持っています。テナント全体のあらゆる変更を、Terraformの管理下かどうかに関係なく記録します。範囲がいちばん広いのはここです。ただし、その変更が正しいのかどうかは判断できません。そしてFreeでは7日で消えます。
コードと実環境の差分は「今ずれているか」を持っています。あるべき姿がコードとして書かれているので、planを打てば判定まで返ってきます。ただし管理下のリソースしか見ませんし、打つ人がいなければ動きません。
Gitは「なぜそうしたか」を持っています。コミットメッセージに書いた理由は、監査ログにもStateにも入っていませんでした。そして、差分が正しい変更なのか無断変更なのかを判断する材料も、ここにあります。ただし今回入れたのはコードの変更履歴までで、いつ何を適用したかという実行の記録は、まだどこにも残っていません。
3つのうちどれかを選ぶという話ではありませんでした。範囲を広く取るのが監査ログ、判定基準を持つのがコード、理由を持つのがGitです。IaCを入れるというのは、後ろの2つを足すことなのだと理解しました。
実際の運用に落とすなら、まず「何をコードの管理下に置くか」を決めるところからだと思います。全部を一度に入れる必要はなく、変更が頻繁で影響の大きいものから順に移していく形になりそうです。管理外に残したものについては、監査ログを定期的に見にいく運用が引き続き要ります。
正直、これだけだと恩恵は薄い
ここまで書いておいて何ですが、今回やったことは本当に初歩です。グループを1つ作って、勝手に書き換えて、差分を見ました。それだけです。
そして正直に言うと、この規模ではIaCの恩恵はほとんどありません。
手作業なら3分で終わる話に、小一時間かかりました。書き方を調べ、Stateの置き場を気にして、Gitは入れ忘れていました。管理画面をクリックするだけなら、どれも起きなかった話です。
差がついたのは無断変更を検知できた1点だけで、しかもそれはplanを打つ人がいて初めて動きます。
じゃあ無駄だったかというと、そうは思っていません。ただ、効いてくるのはもっと後なんだろうな、という感触が残りました。
どこから効き始めそうか
触ってみて、たぶんこの3つだと思っています。
数が増えたとき。グループ1個なら手作業のほうが速いですが、30個を同じルールで作るなら、コードを1回書いて回すほうが速く、間違えません。
同じものを何度も作るとき。今回の19行は、テナントIDを変えれば別のテナントでそのまま動きます。手順書のほうは、読む人が毎回読み替えることになります。
触る人が増えたとき。1人でやっているうちは「今どうなっているか」が頭の中にあります。3人になると消えます。
逆に言うと、この3つのどれにも当てはまらないなら、無理にIaCにしなくていいと思います。今回の私がまさにそうでした。
効きそうだな、と思ったこと
やりながら、こういう使い方なら恩恵がありそうだな、と思ったことがいくつかありました。どれも試していないので、感触だけです。
ひとつは監査です。聞かれるのはだいたい「今どうなっているか」と「なぜそうなっているか」の2つで、それぞれmain.tfとgit logが答えます。ただし読む側に技術が要るので、監査人にplanの出力を見せて話が通じるのかは分かりません。
もうひとつはAIと組み合わせることです。管理画面のクリック操作はAIに渡せませんが、コードなら渡せます。差分を説明させたり、レビューさせたり、変更案を書かせたり。ここで面白いのは、今回の検証と同じ構造になっていることです。コード化されていないものは、AIの視野にも入りません。manual-lab-usersがplanから見えなかったのと、まったく同じです。
そしてHRと繋げる方向です。人事システムで入社や異動が登録されたら、それを起点にグループが自動で変わる、という形。ただしMicrosoft側にHRプロビジョニングや動的グループ(どちらもP1以上)がすでにあるので、Terraformの仕事は「箱とルール」までで、「中身が誰か」は任せることになりそうです。メンバーシップまでコードで持つとplanが毎日ノイズだらけになって、肝心の1件が埋もれます。監査ログを開いたときと同じことが起きます。
このあたりは、また機会があれば試してみます。
今回試していないこと
冒頭にも書きましたが、今回やったのは手元でTerraformを動かしただけです。ガバナンスという観点では、まだ入り口に立ったところだと思っています。
冒頭で挙げたPull Requestによる承認、職務分離、CI/CDからの自動実行は、どれも手つかずのままです。記録の残し方として挙げた5つも、Gitを試しただけで、Stateのリモート化、planの保存、実行ログの保管、定期実行が残っています。
そこに2つ足すとすれば、手作業で作った既存のグループをあとからTerraformの管理下に取り込む作業と、状態ファイルを失ったときにどう復旧するかです。
特に既存グループの取り込みは、今回manual-lab-usersが完全に無視されたことで、実務では避けて通れない論点だと分かりました。既存環境にIaCを入れるとき、最初にぶつかるのはおそらくここです。
このあたりを順番に試して、続きを書いていく予定です。今回と同じく、うまくいかなかったところも含めて書きます。
無料枠という条件が、統制の上限を決めていた
書き終えてから気づいたことがあります。
上に挙げた制約のうち、監査ログが7日で消えることと、Stateが手元のファイル1つしかないことは、どちらもAzure側のサービスを使えば解けるはずです。監査ログはazurerm_log_analytics_workspaceとazurerm_monitor_aad_diagnostic_settingを作ればLog Analyticsに流し込めて、保持期間を自分で決められます。Stateはazurerm_storage_accountに置いてバージョニングを有効にすれば、世代管理までできます。いずれも今回は試していません。ログの種類によってはP1/P2が前提になる条件もあるので、公式ドキュメントでの確認が必要です。
参考:azurerm_monitor_aad_diagnostic_settingのドキュメント
ただし、どちらも課金が発生します。azureadが扱うものはEntra ID Freeの範囲でほぼ無料でしたが、azurermが作るものはほぼすべて従量課金です。Log Analyticsも、取り込んだ量と保持期間で課金されます。
つまり「個人のアカウントと無料枠だけで完結させる」という最初に決めた条件が、そのまま今回の統制の上限を決めていたことになります。7日で消える監査ログも、失えば終わりのStateも、検証の都合ではなく予算の結果でした。
実務では順番が逆になります。統制をどこまで効かせたいかを決めて、それに必要なコストを積みます。今回は無料という制約が先にあったので、届く範囲がそこで止まりました。
おわりに
全部を個人のアカウントと無料枠だけでやりました。必要だったのは、個人のMicrosoftアカウント、Azureの無料アカウント、Terraform、Azure CLIの4つだけです。
分かったことのひとつは、IdPをコードで管理するという体験自体は、無料枠のままで最後まで通せるということです。グループを作り、無断変更を仕掛け、planで検知して、applyで戻すところまで、費用は1円もかかっていません。Entra IDがIaCに対応しているのかどうかも知らなかった状態からでも、そこまでは届きます。
ここまでやってみると今度は有料環境で色々と試してみたくなります。
実際にEntra IDを利用されている方は、検証用テナントを作成してもっと試してみると面白いかもしれません。