本記事はFIXERが提供する「cloud.config Tech Blog」に掲載された「Terraform の整合性問題を解決!3つのシナリオ別対処法」を再編集したものです。
はじめに
Infrastructure as Code(以下IaC)を実現するための一つのソフトウェアとしてTerraformが挙げられます。
Terraformは、宣言的な記述方式と豊富なプロバイダーサポートにより、多くの組織で標準的なIaCソフトウェアとして採用されています。
IaCとしてTerraformを導入するメリットとして、例えばインフラ構成の再現性確保やバージョン管理を行えることです。
しかし、Terraformを導入してこれらの恩恵を享受できる一方で、これを運用していく上で新たな課題も生じます。
本ブログでは、IaCを実現するためのソフトウェアであるTerraform それを運用するうえで発生する「整合性を失った状態」という課題に焦点を当て、その課題が発生する代表的なシナリオに沿って、それらの解決策を提示することを目的とします。
前提
本ブログでは下記を前提とします。
・IaCを実現するためのソフトウェアをTerraformに限定します
・Terraformで管理するインフラストラクチャーをAzureのリソースに限定します
・Terraformの操作は適切な権限を持ったセキュリティープリンシパルによる操作とします
・Terraformの設定ファイルに必要にProviderやBackendの定義、init操作などは省略します。
・使用するTerraformのバージョン: v1.14.2
・使用するリソースプロバイダーazurermのバージョン: v4.56.0
※ 本ブログの内容は上記のバージョンでの動作を前提としており、他のバージョンでは動作が異なる場合があります。
整合性とは
Terraformでインフラストラクチャーを運用する上で重要な概念の一つが「整合性」です。
まず、Terraformにおける整合性とは何かを確認しましょう。
Terraform における3つの要素
Terraformの整合性を理解する上で重要な3つの要素が存在します:
1. Terraformの設定ファイル(.tfファイル)
1. 望ましいインフラ構成を宣言的に記述したファイル
2. Stateファイル(.tfstate)
1. Terraformが管理するリソースの現在の状態を記録したファイル
3. 実際のクラウドリソース(Azure上のリソース)
1. クラウドプロバイダー上に実際に存在するインフラストラクチャー
整合性が保たれた状態
これら3つの要素に対して下記2つの状態が満たされている状態が、「整合性が保たれた状態」です:
・Terraformの設定ファイルで定義された内容 = Stateファイルに記録された内容
・Stateファイルに記録された内容 = 実際のクラウドリソースの定義
※ Terraformのロジック的にTerraformの設定ファイルで定義された内容と実際のクラウドリソースの定義の差分を直接確認する処理はないため、意図的にこの2つの評価は記述していません。
これらの整合性評価は必ずStateファイルを媒介して行います。
これらの条件を満たした状態では、`terraform plan`を実行しても差分が検出されず、「No changes. Your infrastructure matches the configuration.」というメッセージが表示されます。
整合性を失った状態
逆に「整合性を失った状態」とは、これら3つの要素のうち一つ以上に差分が発生している状態を指します。
具体的には以下のようなパターンが考えられます:
1. Terraformの設定ファイルで定義された内容 ≠ Stateファイルに記録された内容
2. Stateファイルに記録された内容 ≠ 実際のクラウドリソースの定義
3. 1,2 の両方
これらに差分が発生し、整合性を失った状態になると、Terraformによる適切なインフラ管理ができなくなり、予期しない動作や設定の不整合が生じる可能性があります。
次章では、このような整合性を失う具体的なシナリオと解決策について詳しく見ていきます。
整合性を失うシナリオと解決策
本章では、整合性を失う代表的な3つのシナリオを整理し、それぞれの解決策を体系的に説明します。
シナリオの分類と整理
この章で取り扱うシナリオは、以下のように分類できます:
| シナリオ | 発生箇所 | 主な原因 |
| Terraformによる操作以外でのリソース変更 | State ⇔ 実際のリソース | 緊急対応での手動変更 |
| 既存リソースのインポート | 設定ファイル ⇔ State | 既存リソースのTerraform化 |
| 外部ポリシーによる変更 | 設定ファイル ⇔ State | Azure Policyなどの自動適用 |
では、実際に整合性を失うシナリオについて具体例と解決策を確認します。
シナリオ1: Terraformによる操作以外でのリソース変更
発生状況
このシナリオは下記のようなきっかけにより発生します。
・緊急対応でAzure Portalから直接リソースを変更
・誤ってTerraform以外(例えばAzure CLIなど)でリソースを修正
・自動スケーリングなどによる予期しない変更
具体例:App Service Planのスケールアップ
Terraformで管理しているApp Service Planにおいて、CPU使用率のアラートが発生し、緊急対応としてAzure Portalからスケールアップを実施した直後の状況を想定します。
変更前と変更後のサイズはS1 → S2とします。
初期状態のTerraformのソースコードは以下の通りです。
resource "azurerm_resource_group" "this" {
name = "rg-blog"
location = "japaneast"
}
resource "azurerm_app_service_plan" "this" {
name = "asp-blog"
location = "japaneast"
resource_group_name = azurerm_resource_group.this.name
sku {
tier = "Standard"
size = "S1"
}
}
解決手順
このような状況下では以下のような手順を踏んで整合性を整えます。
1. `terraform plan -refresh-only`を実行してクラウド上のリソースとStateファイルの差分を確認する。
2. 1の内容に問題がなければ`terraform apply -refresh-only -auto-approve`を実行してクラウド上のリソースの設定変更内容をStateファイルに取り込む。
3. `terraform plan -refresh-only -detailed-exitcode`を実行してクラウド上のリソースとStateファイルに差分がないことを確認する。
4. `terraform plan`を実行して全体の差分を確認し、Terraformの設定ファイルを適切な値に書き換える。
5. `terraform plan -detailed-exitcode`を実行して差分がないことを確認する。
以下、各手順の実行例を示します。
まず、`terraform plan -refresh-only`を実行すると下記の差分(一部抜粋)が表示されます。
# azurerm_app_service_plan.this has changed
~ resource "azurerm_app_service_plan" "this" {
id = "/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/rg-blog/providers/Microsoft.Web/serverFarms/asp-blog"
name = "asp-blog"
tags = {}
# (10 unchanged attributes hidden)
~ sku {
~ size = "S2" -> "S1"
# (2 unchanged attributes hidden)
}
}
この状態ではAzure上のリソースとStateファイルに差分が発生しており、skuのsizeに差分が発生していることが確認できます。
App Service PlanのskuのsizeはS2が期待される値となるため、Azure上にあるリソースの設定値と等しくなるようにStateファイルの更新を受け入れるために`terraform apply -refresh-only -auto-approve`を実行します。
実行が完了したら、再度`terraform plan -refresh-only -detailed-exitcode`を実行してStateファイルとクラウド上のリソースの設定に差分がないことを確認します。
続いて、Terraformの設定ファイルを期待される状態に変更します。
まず、`terraform plan`を実行して全体の差分を確認します。
コマンド実行時には以下の通りが差分(一部抜粋)が出力されます。
~ update in-place
# azurerm_app_service_plan.this will be updated in-place
~ resource "azurerm_app_service_plan" "this" {
id = "/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/rg-blog/providers/Microsoft.Web/serverFarms/asp-blog"
name = "asp-blog"
tags = {}
# (10 unchanged attributes hidden)
~ sku {
~ size = "S2" -> "S1"
# (2 unchanged attributes hidden)
}
}
Plan: 0 to add, 1 to change, 0 to destroy.
差分が発生している箇所に対して望ましい状態となるようにTerraformの設定ファイルを修正します。
修正が完了したら、`terraform plan -detailed-exitcode`を実行して差分がないことを確認し、差分が無くなれば差分の取り込みが完了です。
変更後のTerraformの設定ファイル
resource "azurerm_resource_group" "this" {
name = "rg-blog"
location = "japaneast"
}
resource "azurerm_app_service_plan" "this" {
name = "asp-blog"
location = "japaneast"
resource_group_name = azurerm_resource_group.this.name
sku {
tier = "Standard"
+ size = "S2"
- size = "S1"
}
}
シナリオ2: 既存リソースのTerraform管理への移行
発生状況
このシナリオは以下のようなきっかけにより発生します:
・緊急作成したリソースを後からTerraformで管理したい場合
・手動で作成済みのリソースをTerraform管理に移行したい場合
・他のツールで管理していたリソースをTerraformに統合する場合
具体例:Log Analytics WorkspaceのTerraform化
現在、Azure上にTerraformで管理している リソースグループがあり、何らかの暫定対応でこのリソースグループの配下にLog Analytics WorkspaceをAzure Portalから作成した状況を想定します。
作成したLog Analytics Wrokspaceの設定は画像の通りとし、これをTerraformで管理できるようにします。
初期状態のTerraformのソースコードは以下の通りです。
resource "azurerm_resource_group" "this" {
name = "rg-blog"
location = "japaneast"
}
解決手順
このような状況下では以下のような手順を踏んで整合性を整えます。
1. 新しく作成したリソースの型と名前をTerraformの設定ファイルに記述する。
2. `terraform import`コマンドを実行して、tfstateにクラウド上のリソース定義を取り込む。
3. `terraform plan`コマンドを実行して、Terraformの設定ファイルとStateファイルの差分を確認する。
4. 差分の内容を解消する実装をTerraformの設定ファイルに加える。
5. `terraform plan -detailed-exitcode`を実行して差分が発生しないことを確認する。
以下、各手順の実行例を示します。
まず、Terraformの設定ファイルに型と名前を定義します。
型はazurermで定義されているLog Analytics Workspaceの型を使用し、名前はthisとします。
resource "azurerm_resource_group" "this" {
name = "rg-blog"
location = "japaneast"
}
+ resource "azurerm_log_analytics_workspace" "this" {}
続いて、下記のコマンドを実行します。※ ${SUBSCRIPTION_ID}, ${RESOURCE_GROUP_NAME}, ${WORKSPACE_NAME} は適切な値に置き換える必要があります。
terraform import azurerm_log_analytics_workspace.this /subscriptions/${SUBSCRIPTION_ID}/resourceGroups/${RESOURCE_GROUP_NAME}/providers/Microsoft.OperationalInsights/workspaces/${WORKSPACE_NAME}
実行が完了すると、StateファイルにTerraform外で作成したリソースの定義と設定が記述されます。
ここまで完了すればあとは `terraform plan`コマンドを駆使して、Terraformの設定ファイルをStateファイルと差分が発生しないようになるように修正すれば完了となります。
最終的なTerraformの設定ファイルは下記のようになります。
resource "azurerm_resource_group" "this" {
name = "rg-blog"
location = "japaneast"
}
+ resource "azurerm_log_analytics_workspace" "this" {
+ name = "log-blog-xyz"
+ location = azurerm_resource_group.this.location
+ resource_group_name = azurerm_resource_group.this.name
+ sku = "PerGB2018"
+ retention_in_days = 30
+ cmk_for_query_forced = false
+ immediate_data_purge_on_30_days_enabled = false
+ tags = {}
+ }
シナリオ3: クラウドサービスのポリシーによる差分
発生状況
このシナリオは以下のようなきっかけにより発生します:
・Azure Policyによるタグの自動付与
・セキュリティポリシーによる設定の強制変更
・コンプライアンス要件による自動修正
具体例:タグ継承ポリシーによる差分
Terraformで管理しているリソースグループにAzure Policyの「Inherit a tag from the resource group」ポリシーが適用されており、そのリソースグループ配下にLog Analytics Workspaceを作成した状況を想定します。
この場合、ポリシーによりリソースグループのタグが自動的に継承され、Terraform設定ファイルとの間に差分が発生します。
初期状態のTerraformのソースコードは以下の通りです。
resource "azurerm_resource_group" "this" {
name = "rg-blog"
location = "japaneast"
tags = {
purpose = "blog"
}
}
data "azurerm_policy_definition_built_in" "this" {
display_name = "Inherit a tag from the resource group"
}
resource "azurerm_resource_group_policy_assignment" "this" {
name = "Inherit a tag from the resource group"
resource_group_id = azurerm_resource_group.this.id
policy_definition_id = data.azurerm_policy_definition_built_in.this.id
parameters = jsonencode({
"tagName" = {
value = "purpose"
}
})
identity {
type = "SystemAssigned"
}
location = azurerm_resource_group.this.location
}
# Azure Policy の修復タスクを実施するために必要なロール
resource "azurerm_role_assignment" "this" {
scope = azurerm_resource_group.this.id
role_definition_name = "Tag Contributor"
principal_id = azurerm_resource_group_policy_assignment.this.identity[0].principal_id
}
resource "azurerm_log_analytics_workspace" "this" {
name = "log-blog-xyz"
location = azurerm_resource_group.this.location
resource_group_name = azurerm_resource_group.this.name
sku = "PerGB2018"
retention_in_days = 30
cmk_for_query_forced = false
immediate_data_purge_on_30_days_enabled = false
# タグは明示的に設定していない
}
解決手順
このような状態では以下のような手順を踏んで整合性を整えます。
1. `terraform plan`を実行して、差分を確認します。
2. 差分の内容をTerraformの設定ファイル上で管理する必要があるかを判断し、管理する必要があれば差分の解消を、管理する必要がなければ、対象の設定をignore_changesとします。
以下、各手順の実行例を示します。
まず、`terraform plan`を実行すると下記のような差分(一部抜粋)が発生しています。
~ update in-place
# azurerm_log_analytics_workspace.this will be updated in-place
~ resource "azurerm_log_analytics_workspace" "this" {
id = "/subscriptions/${SUBSCRIPTION_ID}/resourceGroups/rg-blog/providers/Microsoft.OperationalInsights/workspaces/log-blog-xyz"
name = "log-blog-xyz"
~ tags = {
- "purpose" = "blog" -> null
}
# (16 unchanged attributes hidden)
}
Plan: 0 to add, 1 to change, 0 to destroy.
差分を確認したところ、今回は下記の理由から対象の差分をignore_changesとします。
・tagがAzure Policyによって確実に付与されること
・tagに追加・更新・削除があった場合、修正の範囲が広範になること(今回はリソースが1つだが、リソースが増えると大変)
方針が決まったので、Terraformの設定ファイルを下記のように設定し、`terraform plan -detailed-exitcode`を実行して差分がないことを確認すれば完了です。
resource "azurerm_resource_group" "this" {
name = "rg-blog"
location = "japaneast"
tags = {
purpose = "blog"
}
}
data "azurerm_policy_definition_built_in" "this" {
display_name = "Inherit a tag from the resource group"
}
resource "azurerm_resource_group_policy_assignment" "this" {
name = "Inherit a tag from the resource group"
resource_group_id = azurerm_resource_group.this.id
policy_definition_id = data.azurerm_policy_definition_built_in.this.id
parameters = jsonencode({
"tagName" = {
value = "purpose"
}
})
identity {
type = "SystemAssigned"
}
location = azurerm_resource_group.this.location
}
# Azure Policy の修復タスクを実施するために必要なロール
resource "azurerm_role_assignment" "this" {
scope = azurerm_resource_group.this.id
role_definition_name = "Tag Contributor"
principal_id = azurerm_resource_group_policy_assignment.this.identity[0].principal_id
}
resource "azurerm_log_analytics_workspace" "this" {
name = "log-blog-xyz"
location = azurerm_resource_group.this.location
resource_group_name = azurerm_resource_group.this.name
sku = "PerGB2018"
retention_in_days = 30
cmk_for_query_forced = false
immediate_data_purge_on_30_days_enabled = false
+ lifecycle {
+ ignore_changes = [tags]
+ }
}
整合性を失わないための運用
ここまで、Terraformのリソース管理状態に整合性を失った場合のシナリオや解決策について触れてきました。
ただし、これは整合性が失われた場合による対処法にとどまるものであり、Terraformを用いた運用を進めていくうえでは下記を留意する必要があります。
・可能な限り整合性が失われた状態を発生させないようにする。
・整合性が失われた状態が発生した場合、あらかじめ決められた時間間隔で検知して修復することで、整合性が失われた状態が長期間放置されることを防ぐようにする。
これらの運用を実現するために、Terraformによる運用のルールを決めることが最善です。
例えば以下のような運用ルールが挙げられます。
運用ルール
1. Terraform管理リソースの手動変更禁止
1. Terraformで管理しているリソースは、原則としてTerraform経由でのみ変更する
2. 緊急時の手動変更は事後に必ずTerraform設定に反映
2. 定期的な差分チェックによる監視
1. 定期的に`terraform plan -detailed-exitcode`を実行し、予期しない差分がないか確認
2. CI/CDパイプラインでの自動チェックの導入
3. リソース変更の監視設定
4. 予期しない変更が発生した際の対応フローや手順の文書化
3. Stateファイルの適切な管理
1. リモートStateの使用(Azure Storage、Terraform Cloudなど)
2. Stateファイルのバックアップとバージョン管理
4. 適切なlifecycle設定
1. prevent_destroyによる重要リソースの削除防止
2. ignore_changesによる意図的な差分の無視
5. 周知と理解
1. チームメンバーへのTerraform運用ルールの醸成
2. 手動変更のリスクと影響の共有
3. Terraformの内部処理内容の詳細理解
6. Policy as Codeの導入
1. Terraform Sentinelを活用した、変更内容の強制
2. 不適切な設定変更の事前防止
まとめ
本ブログでは、Terraformでクラウドリソースを管理するために必要な整合性についてをはじめ、Terraformの整合性を失う3つの主要なシナリオとその解決策について解説しました。
これらの知識を活用することで、Terraformを使用したインフラ管理をより安全かつ効率的に行うことができます。
Terraformの背後の処理に対する理解も深めつつ、適切な運用を行い、Terraformで安全なIaC運用ライフを送りましょう!
余談
TerraformのソースコードはGitHub上で公開されています。
このブログで取り扱った差分チェックに関する処理は、例えばcontext_plan.goやevaluate.goなどで実装されています。
コードリーティングを進めていくにあたって、Terraform CoreがTerraformの設定ファイルとStateファイルの差分を、Resource ProviderがStateファイルとクラウド上にあるリソースの差分を確認するという責任分離が理解できて面白かったりします。
(後者はクラウドサービス固有のAPIを叩く必要があるので、Resource Providerの債務になっているのは必然でしょうか。)
これらのソースコードを読むことで、Terraformの内部動作についてより深く理解することができるので、興味がある方はコードリーティングしてみましょう!
中島悠太/FIXER
2023年度新卒で入社。エンジニア1年生。
社内での担当はまだ未定。
社外では42Tokyoという場所でエンジニアとしてのスキルを研鑽しています。


この連載の記事
-
TECH
アプリ開発の手戻りを防ぐ 「入力チェック(バリデーション)」の設計方法 -
TECH
WSL2でのGitHubの認証をできる限り簡単に行う方法 -
TECH
MacでGitHub CLIの認証を行う方法 -
TECH
ゆるく理解する自作シェル実装1:そもそもシェルってどんなもの? -
TECH
プロンプトエンジニアリングのコツは「5W1Hを忘れずに」 -
TECH
GitHubの 超・超・超 基本的な使い方まとめ -
TECH
業務で使えるExcel関数テクニック − 関数を使った動的な範囲指定のコツ -
TECH
zshの初期設定がダサい…。表示内容を自分好みにカスタマイズしていく -
TECH
Proxmox VE+OpenMediaVaultで自宅用NASを作ってみた -
TECH
Chrome拡張はVue.jsで作るのがおすすめ -
TECH
gitコマンド、長いしだるいしMMS(マジ・短く・したい) - この連載の一覧へ



