Terraform は IaC ツールであり、IaC とは「インフラストラクチャ・アズ・コード」を意味します。私たちが持つインフラストラクチャを宣言的なコードとして記述し、その後 terraform apply を実行することでインフラストラクチャのデプロイが完了します。同一の設定であれば、必ず同じインフラストラクチャがデプロイされます(Nix OS ユーザーにとっては大喜びの内容です)。
従来のインフラストラクチャ管理の大部分は人的作業と各種クラウドプロバイダーのダッシュボードに依存しており、以下のような課題が生じています:
| 課題 | 説明 |
|---|
| 再現性の欠如 | ダッシュボードでのクリック操作による設定変更は、誤りや見落としが発生しやすく、復元も困難 |
| 環境ドリフト | 手動操作により本番環境とテスト環境の設定が徐々に乖離し、極端なケースではテストでは問題ないのに本番でダウンすることがある |
| 拡張の難しさ | 新しい環境を追加するには多数の手動作業を繰り返す必要があり、時間がかかりミスも発生しやすい |
| 監査の難しさ | 変更履歴がないため、トラブル発生時に責任の所在を特定するのが難しい |
| 協力の難しさ | インフラストラクチャは少数の「詳しい人」によって管理されており、誰かが変更したい場合でもその人を探さなければならず効率が悪い |
これらの問題を解決するために、「インフラストラクチャ・アズ・コード」 という概念が提唱され、Terraform はその有名なソリューションの一つです。
実際のユースケースを想定してみます。例えば、毎年新しい GCP の$300 無料クレジットアカウントを購入する場合、安価ですが毎年 GCP のパネルで再度マシンを立て直す必要があります。しかし、Terraform を使えば、同じ設定のマシンをデプロイしたい場合、アカウントを変更するたびに API Token を新しいものに置き換え、terraform apply を実行するだけです。数分で前のアカウントと同じマシン、VPC、S3、ファイアウォール設定などを作成できます。
また、Cloudflare DNS や Tunnel の管理・移行においても、元の Tunnel の Ingress Rule を新しい Tunnel の Ingress Rule にコピーするだけで済みます。将来的に AliDNS、Route 53 など他のプロバイダーに移行する場合でも、データをそのままコピーすれば対応可能です。
特に複数人で協力する場合、Git と組み合わせることで変更の痕跡が残り、マージ競合の心配もありません。PR には変更プレビューが自動生成され、問題が発生すればすぐに前バージョンにロールバックできます。これらは従来の人手によるプロバイダーダッシュボードへの直接操作では到底不可能なことです。
Terraform は Go で書かれており、コンパイルされた出力は単一の実行可能ファイルであるため、インストールも非常に簡単です。Windows では Winget を直接使用できます:
1
| winget install Hashicorp.Terraform
|
Winget を使いたくない場合は、Scoop などの別のパッケージマネージャーを使用するか、事前コンパイルされたバイナリファイルを $PATH に配置するだけでインストールが完了します。
Linux 環境の場合は、対応するパッケージマネージャーを使用してインストールしてください。バイナリファイルを $PATH に配置する方法を使う場合は、sudo chmod +x terraform でファイルの実行権限を設定することを忘れないでください。
Provider(s)
前述の通り、Terraform はインフラストラクチャ・アズ・コードツールですが、ツール自体は特定のプラットフォームにバインドされておらず、Provider(s) を通じて各プラットフォームと連携します。利用可能な Provider(s) を確認するには、Terraform のレジストリを参照してください。

状態管理
Terraform はインフラストラクチャ変更操作を実行する際の状態情報を状態ファイルに保存します。デフォルトでは現在の作業ディレクトリ内の terraform.tfstate ファイルに保存されます。構成ファイルを変更して他のストレージバックエンド(S3、Postgres など)を指定することも可能です。terraform apply を実行するたびに、Terraform は現在の構成ファイルで宣言された状態と既存の状態ファイルを比較し、変更部分を計算して調整順序を自動的に決定した後、Provider を操作して状態変動を実現します。
既存リソースのインポートはこれまで Terraform が批判されてきた問題点であり、Hashi Corp. は一貫して「すべてのインフラストラクチャは最初から Terraform で作成されるべきであり、リソースインポートの問題などは存在しない」という頑固で愚かな見解を堅持しているようです。
| 時期 | バージョン | 進展 | 問題 |
|---|
| 2014–2022 | v0.x–v1.4 | terraform import のみで、一度に 1 件ずつ、かつ全く設定を生成しない | インポート後でも自分で HCL を書く必要がある |
| 2023.06 | v1.5 | import ブロックと -generate-config-out パラメータを導入し、設定を生成可能になった | それでも 1 行ずつ書く必要があり、既存リソースの ID も自分で提供しなければならない |
| 2024.01 | v1.7 | import ブロックが for_each をサポート | バッチ処理が可能になったが、ID はまだ自分で用意する必要がある |
| 2024 年下半期 | v1.12 | terraform query と list ブロックを導入し、ついにリソースの自動発見機能を達成 | ただしこの機能は Provider が実装する必要があり、多くの Provider が追いついていない |
長年の間、コミュニティは批判し続けていますが、「既存のリソースをどうやって Terraform にインポートするか」という問題は真剣に取り組まれていませんでした。「インフラストラクチャ・アズ・コード」ツールである Terraform の設計哲学は宣言性と冪等性であり、Hashi Corp. は「すべてはコード声明された状態が基準であり、Terraform でゼロからリソースを作成すべきだ」と信じています。しかし現実には、ほとんどの企業には大量の歴史的残存リソースがあり、「先にリソースがあり、後にコードがある」のが常态です。この矛盾を Hashi Corp. が認めたのは遅く、v1.12 の terraform query になって初めて公式にこの問題に対処しましたが、Provider エコシステムの追従状況……正直言って期待薄です。
Terraform のファイル構造はシンプルで、メインプログラムは実行時、作業ディレクトリ内のすべての .tf ファイルを盲目的に読み取ります。情報さえ揃っていれば、ファイル名は何でも好きなように付けられます。私のファイル構造はこのようになります:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| ❯ tree -a -I .git
.
├── .editorconfig
├── .github
│ ├── dependabot.yml
│ └── workflows
│ ├── terraform-apply.yml
│ ├── terraform-plan.yml
│ └── your-fork.yml
├── .gitignore
├── .terraform.lock.hcl
├── cf_dns_zones.tf
├── cf_tunnel.tf
├── dns_example_com.tf
├── dns_example_net.tf
├── dns_example_top.tf
├── dns_example_cn.tf
├── main.tf # ベース設定(terraform ブロック)
├── moved.tf # 移動したリソース
├── provider.tf # 各 Provider の設定
├── README.md
├── rename_resources.ps1
└── variables.tf # カスタム変数
|
main.tfはベース設定を保存するために使用されます:
1
2
3
4
5
6
7
8
9
10
11
12
13
| terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "~> 5"
}
tencentcloud = {
source = "tencentcloudstack/tencentcloud"
version = ">= 1.81.43"
}
}
}
|
provider.tfは各 Provider の設定を保存するために使用されます:
1
2
| provider "cloudflare" {}
provider "tencentcloud" {}
|
ここが空なのは、Credentials を構成ファイルに直接書くのではなく、環境変数を通じて渡せるからです。例えば Cloudflare の Provider は、CLOUDFLARE_API_TOKEN を api_token 変数の代替として受け入れます。
variables.tfはカスタム変数を宣言するために使用されます:
1
2
3
4
5
6
7
8
9
| variable "cloudflare_zone_example_com" {
description = "example.com の Cloudflare zone ID"
type = string
}
variable "cloudflare_zone_example_top" {
description = "example.top の Cloudflare zone ID"
type = string
}
|
これらの変数は TF_VAR_ で始まる環境変数を通じて入力でき、Terraform は terraform.tfvars ファイルの変数も自動的に読み取ります。変数の主な用途は、他の構成ファイルから呼び出すことです。例えば:
1
2
3
4
5
6
7
8
9
10
11
12
| resource "cloudflare_dns_record" "example_cname" {
content = "${cloudflare_zero_trust_tunnel_cloudflared.Production_Tunnel.id}.cfargotunnel.com"
name = "example.example.com"
proxied = true
tags = []
ttl = 1
type = "CNAME"
zone_id = var.cloudflare_zone_id_example_com # ここでは cloudflare_zone_example_com 変数が呼び出されている
settings = {
flatten_cname = false
}
}
|
これらの基本設定があれば、terraform init を実行して Terraform 環境を初期化し、依存関係のバージョンをロックできます。残りの部分はすべてリソースの宣言となります。
前述の通り、Terraform の状態管理という概念は素晴らしいですが、初期化時には新たな問題をもたらしました——デフォルト状態では、Terraform の状態ファイルは明らかに空です。
此時、私たちはクラウドサービスプロバイダー上の既存リソースの状態を Terraform にインポートする必要があります。そうすることで初めて、Terraform は私たちのインフラストラクチャをシームレスに引き継いで管理できます。皆さんも前節で Terraform リソースインポート問題に対する不満を確認したと思います。Cloudflare 上でホストされている DNS レコードのインポートを例にとると、前述のように Provider がサポートしていれば、Terraform 1.12 バージョン以降で導入された terraform query を使用できますが、多くの場合、Providers はこの新機能に対応していません。Cloudflare は対応していないタイプのひとつです。
幸いにも Hashi Corp. が真面目に解決しようとしなくても、Terraform を使ってインフラを管理している各社はそれぞれの知恵を出し合っています。Cloudflare は cf-terraforming という名のインポートツールを維持しており、多くの手動インポートの手間を省いています。まず cf-terraforming をインストールします。このツールは Go 言語で書かれているため、Go 言語環境が必要です。あるいは公式にコンパイルされたバイナリファイルを $PATH に配置して実行権限を与える方法もあります。
1
| go install github.com/cloudflare/cf-terraforming/cmd/cf-terraforming@latest
|
このツールの使い方は比較的シンプルです。まず以下の環境変数が必要です:
1
2
3
4
5
6
7
8
9
| # API Token を使用する場合
export CLOUDFLARE_API_TOKEN='Hzsq3Vub-7Y-hSTlAaLH3Jq_YfTUOCcgf22_Fs-j'
# API Key を使用する場合
export CLOUDFLARE_EMAIL='user@example.com'
export CLOUDFLARE_API_KEY='1150bed3f45247b99f7db9696fffa17cbx9'
# 対象とするドメインのゾーン ID を指定。アカウントリソース(Cloudflare Tunnel など)をインポートする場合は不要
export CLOUDFLARE_ZONE_ID='81b06ss3228f488fh84e5e993c2dc17'
|
ここでのコマンドは Bash を使用していることを前提としています。Bash 構文と互換性のない Shell を使用している場合は調整が必要です。例えば Windows 上の PowerShell では、環境変数のインポート構文は次のようになります:
1
| $env:CLOUDFLARE_API_TOKEN='Hzsq3Vub-7Y-hSTlAaLH3Jq_YfTUOCcgf22_Fs-j'
|
通常は CLOUDFLARE_API_TOKEN と CLOUDFLARE_ZONE_ID だけ設定すれば十分です。コンソールで API Token を作成する際は、この Token に必要な権限を付与することを忘れないでください。今回は DNS レコードだけをインポートするので、ゾーン DNS の編集権限だけ付与すれば OK です。

準備完了です。今から構成ファイルを生成しましょう:
まずアカウント内のドメイン構成をインポートします。つまり cloudflare_zone です:
1
2
3
| cf-terraforming generate \
--key $CLOUDFLARE_API_KEY \
--resource-type "cloudflare_zone" > zone.tf
|
このステップで現在のディレクトリに zone.tf という名前のファイルが生成され、以下のフォーマットのコンテンツが含まれます:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| resource "cloudflare_zone" "REDACTED" {
name = "REDACTED"
paused = false
type = "full"
vanity_name_servers = []
account = {
id = "REDACTED"
name = "REDACTED"
}
}
resource "cloudflare_zone" "REDACTED" {
name = "REDACTED"
paused = false
type = "full"
vanity_name_servers = []
account = {
id = "REDACTED"
name = "REDACTED"
}
}
|
これでドメインリソースはインポートされましたが、中身の設定はまだありません。次に、ドメイン内の DNS レコードをインポートします:
1
2
3
4
| cf-terraforming generate \
--zone $CLOUDFLARE_ZONE_ID \
--key $CLOUDFLARE_API_KEY \
--resource-type "cloudflare_dns_record" >> dns.tf
|
このステップで現在のディレクトリに dns.tf という名前の構成ファイルが生成され、以下のフォーマットのコンテンツが含まれます:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| resource "cloudflare_dns_record" "terraform_managed_resource_5deb14xxxxxb629bf123xxxxxxxc8f_0" {
content = "67.24.33.108"
name = "example.example.com"
proxied = true
tags = []
ttl = 1
type = "A"
zone_id = "81c7f2de8dfxxxxxx52629xxxxxxfc"
settings = {}
}
resource "cloudflare_dns_record" "terraform_managed_resource_89xxxxx0bf9cxxxxxx9a_1" {
content = "35.27.108.33"
name = "terraform.example.com"
proxied = true
tags = []
ttl = 1
type = "A"
zone_id = "8xxxxxx7644e428526xxxxxx"
settings = {}
}
|
複数のドメインをインポートする場合は、環境変数 CLOUDFLARE_ZONE_ID をそれぞれ設定し、コマンドを繰り返し実行してください。
ここで生成された構成ファイルはそのまま使用可能です。これが私たちが後に必要な Terraform 構成ファイルです。しかし、現時点では単に構成ファイルが生成されただけで、Terraform の状態はまだ空のままです。ここでいきなり terraform apply を実行すると、Terraform は文脈に関係なく新しくインポートした宣言をすべて新規リソースとして扱い、「Already Exists」というエラーメッセージを大量に表示します。そこで次に、生成された構成ファイルを Terraform の terraform.tfstate 状態にインポートする必要があります。
Terraform は 1.5 バージョンで import ブロックを導入し、以前のような 1 行ずつコマンドを入力する方式よりも現代的になりました。そのインポートフローは、まず import ブロックを含む .tf ファイルを生成し、次回の terraform apply 時に Terraform が自動的にインポート操作を実行します。
cloudflare_zone の import ブロックを生成:
1
2
3
4
5
| cf-terraforming import \
--resource-type "cloudflare_zone" \
--modern-import-block \
--key $CLOUDFLARE_API_KEY \
--zone $CLOUDFLARE_ZONE_ID >> import.tf
|
cloudflare_dns_record の import ブロックを生成:
1
2
3
4
5
| cf-terraforming import \
--resource-type "cloudflare_dns_record" \
--modern-import-block \
--key $CLOUDFLARE_API_KEY \
--zone $CLOUDFLARE_ZONE_ID >> import.tf
|
このステップで現在のディレクトリに import.tf が生成されます。必要なインポート情報が含まれており、役割は Terraform に上一步で生成された各 resource ブロックが、どのクラウドプロバイダーのリソース ID に対応するのかを教えることです。この ID はクラウドプロバイダー内部でリソースをマークするコードであり、普段はコントロールパネルに表示されません。API で特別にリクエストしない限り知ることはできません。Terraform はインポートプロセス中にこの ID を使用して、ローカルの定義がクラウド上のどのリソースに対応しているかを確認し、厳密な冪等性を実現します。
さて、ここでは terraform plan を実行します:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
| $ terraform plan
cloudflare_dns_record.minio_a: Refreshing state... [id=xxxxxxxxxxx53]
cloudflare_zero_trust_tunnel_cloudflared_config.raspberrypi: Refreshing state...
......
Terraform will perform the following actions:
# cloudflare_dns_record.terraform_managed_resource_0 will be imported
resource "cloudflare_dns_record" "terraform_managed_resource_REDACTED_0" {
content = "67.24.33.108"
created_on = "2026-04-08T10:18:12Z"
id = "5deb14c21xxxxxxx20f1c8f"
meta = jsonencode({})
modified_on = "2026-04-08T10:18:12Z"
name = "example.example.com"
proxiable = true
proxied = true
settings = {}
tags = []
ttl = 1
type = "A"
zone_id = "REDACTED"
}
# cloudflare_dns_record.terraform_managed_resource_1 will be imported
resource "cloudflare_dns_record" "terraform_managed_resource_89c149exxxxxxxxxxxba13xxxxxa_1" {
content = "35.27.108.33"
created_on = "2026-04-08T10:17:54Z"
id = "89cxxxxxxxxxxxxxxxxxx09a"
meta = jsonencode({})
modified_on = "2026-04-08T10:17:54Z"
name = "terraform.example.com"
proxiable = true
proxied = true
settings = {}
tags = []
ttl = 1
type = "A"
zone_id = "81xxxxxxxxxxxxxxxxxxxxxfc"
}
Plan: 2 to import, 0 to add, 0 to change, 0 to destroy.
────────────────────────────────────────────────────────────────────────────────────────────────────────
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.
|
Add、Change、Destroy のリソース数がすべて 0 の場合、インポート操作に問題がないことになります。そのまま terraform apply --auto-approve を実行すれば、リソースのインポート完了です。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
| $ terraform apply --auto-approve
cloudflare_dns_record.push_a: Refreshing state... [id=REDACTED]
Terraform will perform the following actions:
# cloudflare_dns_record.terraform_managed_resource_REDACTED_0 will be imported
resource "cloudflare_dns_record" "terraform_managed_resource_REDACTED_0" {
content = "67.24.33.108"
created_on = "2026-04-08T10:18:12Z"
id = "REDACTED"
meta = jsonencode({})
modified_on = "2026-04-08T10:18:12Z"
name = "example.example.com"
proxiable = true
proxied = true
settings = {}
tags = []
ttl = 1
type = "A"
zone_id = "REDACTED"
}
# cloudflare_dns_record.terraform_managed_resource_REDACTED_1 will be imported
resource "cloudflare_dns_record" "terraform_managed_resource_REDACTED_1" {
content = "35.27.108.33"
created_on = "2026-04-08T10:17:54Z"
id = "REDACTED"
meta = jsonencode({})
modified_on = "2026-04-08T10:17:54Z"
name = "terraform.example.com"
proxiable = true
proxied = true
settings = {}
tags = []
ttl = 1
type = "A"
zone_id = "REDACTED"
}
Plan: 2 to import, 0 to add, 0 to change, 0 to destroy.
cloudflare_dns_record.terraform_managed_resource_REDACTED_1: Importing... [id=REDACTED/REDACTED]
cloudflare_dns_record.terraform_managed_resource_REDACTED_1: Import complete [id=REDACTED/REDACTED]
cloudflare_dns_record.terraform_managed_resource_REDACTED_0: Importing... [id=REDACTED/REDACTED]
cloudflare_dns_record.terraform_managed_resource_REDACTED_0: Import complete [id=REDACTED/REDACTED]
Apply complete! Resources: 2 imported, 0 added, 0 changed, 0 destroyed.
|
状態保存と継続的インテグレーション
IaC の重要な価値の一つは、Git ベースの複数人コラボレーションと CI の継続的インテグレーションを容易に実現できる点ですが、その前に解決すべき新たな問題があります——terraform.tfstateはどこに置くか:環境を変更するたびに辛苦入れてインポートした状態を失う人は誰も望んでいません。
Terraform は現在、以下の状態保存バックエンドをサポートしています:
- local
- remote
- azurerm
- consul
- cos
- gcs
- http
- Kubernetes
- oci
- oss
- pg
- s3
特別な要件がなければ、私のように s3 を選択することをお勧めします。Cloudflare R2 には無料枠があるので、使わない手はありません。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| terraform {
backend "s3" {
bucket = "terraform"
key = "terraform.tfstate"
region = "auto"
endpoints = {
s3 = "https://REDACTED.r2.cloudflarestorage.com"
}
# R2 にはこれらの AWS 認証は不要
skip_credentials_validation = true
skip_metadata_api_check = true
skip_region_validation = true
skip_requesting_account_id = true
use_path_style = true
}
}
|
S3 バックエンドの場合、Credentials を保存するには AWS_ACCESS_KEY_ID と AWS_SECRET_ACCESS_KEY の 2 つの環境変数を使用することをお勧めします:
1
2
| export AWS_ACCESS_KEY_ID='REDACTED'
export AWS_SECRET_ACCESS_KEY='REDACTED'
|
設定完了後、terraform init -migrate-state を実行すれば、設定はクラウド上に正常に保存されます。今後どこで設定を変更しても、terraform apply を実行しても、Terraform の State が同期していないことを心配する必要はありません。
続いて Github CI の設定ですが、実はとてもシンプルです。git push のたびに terraform init、terraform fmt、terraform apply をトリガーするだけです。以下が私の .github/workflows/apply.yml です:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
| name: "Terraform Apply"
on:
push:
branches:
- main
env:
TF_IN_AUTOMATION: "true"
CLOUDFLARE_API_TOKEN: "${{ secrets.CLOUDFLARE_API_TOKEN }}"
AWS_ACCESS_KEY_ID: "${{ secrets.AWS_ACCESS_KEY_ID }}"
AWS_SECRET_ACCESS_KEY: "${{ secrets.AWS_SECRET_ACCESS_KEY }}"
TF_VAR_cloudflare_zone_id_example_com: "${{ vars.TF_VAR_CLOUDFLARE_ZONE_ID_EXAMPLE_COM }}"
TF_VAR_cloudflare_zone_id_example_top: ${{ vars.TF_VAR_CLOUDFLARE_ZONE_ID_EXAMPLE_TOP }}
jobs:
terraform:
name: "Terraform Apply"
runs-on: ubuntu-latest
permissions:
contents: read
concurrency:
group: terraform-apply
cancel-in-progress: false
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Setup Terraform
uses: hashicorp/setup-terraform@v4
- name: Terraform Init
run: terraform init -input=false
- name: Terraform Apply
run: terraform apply -input=false -auto-approve
|
PR に対しては、CI が毎回 PR に terraform plan の出力を添付するように設定すべきです:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
| name: Terraform Plan
on:
pull_request:
paths:
- "**/*.tf"
- ".github/workflows/terraform-plan.yml"
permissions:
contents: read
pull-requests: write
env:
TF_IN_AUTOMATION: "true"
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
TF_VAR_cloudflare_zone_id_example_com: "${{ vars.TF_VAR_CLOUDFLARE_ZONE_ID_EXAMPLE_COM }}"
TF_VAR_cloudflare_zone_id_example_top: ${{ vars.TF_VAR_CLOUDFLARE_ZONE_ID_EXAMPLE_TOP }}
jobs:
plan:
name: Terraform Plan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: hashicorp/setup-terraform@v4
- name: Terraform fmt
id: fmt
run: terraform fmt -check -recursive
continue-on-error: true
- name: Terraform Init
id: init
run: terraform init -input=false
- name: Terraform Validate
id: validate
run: terraform validate -no-color
- name: Terraform Plan
id: plan
run: terraform plan -input=false -no-color
continue-on-error: true
- name: Post Plan to PR
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
});
const botComment = comments.find(c =>
c.user.type === 'Bot' && c.body.includes('<!-- terraform-plan -->')
);
const planOutput = `${{ steps.plan.outputs.stdout }}`.substring(0, 65000);
const body = `<!-- terraform-plan -->
#### Terraform Plan
| Step | Result |
| -------- | --------------------------------- |
| fmt | \`${{ steps.fmt.outcome }}\` |
| init | \`${{ steps.init.outcome }}\` |
| validate | \`${{ steps.validate.outcome }}\` |
| plan | \`${{ steps.plan.outcome }}\` |
<details><summary>Plan 詳細を展開</summary>
\`\`\`terraform
${planOutput}
\`\`\`
</details>`;
if (botComment) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: botComment.id,
body
});
} else {
await github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body
});
}
- name: Fail if plan failed
if: steps.plan.outcome == 'failure'
run: exit 1
|

今後のワークフロー
ここまでで、Terraform の「接管初期化」は終了し、日常保守フェーズに入りました。この段階では実際には 3 つのことしかありません:
- 新しいリソースの作成
- 既存リソースの変更
- 不要になったリソースの削除
よく使う操作は 2 つ:terraform plan と terraform apply。個人利用の場合、小さな変更はそのままコミットしても良いでしょう。チームで協力する場合、毎回変更は PR を通じた方が原則です。
インフラストラクチャの作成
今 DNS レコードを追加したり、Tunnel やオブジェクトストレージバケットを新しく作る場合のフローは以下の通りです:
flowchart TD
C1[ブランチ作成
例:feat/add-minio-record] --> C2[resource ブロックを追加]
C2 --> C3[terraform fmt + validate]
C3 --> C4[terraform plan]
C4 --> C5{予期したリソースのみが追加されたか?}
C5 -- いいえ --> C6[設定を修正して plan を再実行]
C6 --> C4
C5 -- はい --> C7[PR を提出]
C7 --> C8[マージ後 CI apply]最も理想的な plan 出力は:
X to add0 to change0 to destroy
何かを追加したいだけなのに to destroy が表示された場合は、焦らずにチェックしてください。引用ミスタイプ、変数間違い、またはリソースアドレスのうっかり変更などが原因であることがほとんどです。どこに問題があるのか仔細に確認してください。
修改と删除基础设施
変更フローは作成と似ていますが、もう一つのステップが増えます——変更が再構築を引き起こすかどうかの評価です。
多くの Provider フィールドは ForceNew となっており、フィールドを変えるつもりが Terraform は「了解、削除して再構築します」と言ってくるかもしれません。DNS のようなリソースでは大きな問題ではありませんが、クラウドインスタンスのようなリソースになると、削除して再構築すれば損失が発生します。
以下の順序で進めることをお勧めします:
flowchart TD
M1[.tf 修正] --> M2[terraform plan]
M2 --> M3{replace/destroy 出現?}
M3 -- いいえ --> M7[影響範囲を確認]
M7 --> M8[terraform apply]
M3 -- はい --> M4[停止して変更内容を見直す]
M4 --> M5[必要に応じて lifecycle 保護を追加]
M5 --> M6[変更ウィンドウを計画]
M6 --> M8本番環境にとって、作成や変更が遅くても大きな問題ではありません。重要なのは操作の正確性です。少し遅くても、間違えないようにしてください。IaC はスピードを競うのではなく、予測可能性を競うものです。
もしリソースを削除する場合(例えば DNS レコードの廃止、废弃 Tunnel の整理)、以下のフローに従ってください:
flowchart TD
D1[リソース廃止の確認、業務/監視/スクリプト依存チェック] --> D2[resource ブロック削除、または count/for_each 調整]
D2 --> D3[terraform plan]
D3 --> D4{to destroy が予期通り?}
D4 -- いいえ --> D5[変更をロールバックし依存関係を調査継続]
D5 --> D1
D4 -- はい --> D6[ロールバックプランを用意し、低負荷時間帯を選択]
D6 --> D7[PR 審査通過]
D7 --> D8[terraform apply]
D8 --> D9[削除後の可用性チェック]日常のコラボレーション提案
- Credentials は環境変数や CI Secret に置き、
.tf やリポジトリに書き込まない - 重要なリソースには保護ポリシーを有効にし、誤削除を防ぐ
- ディレクトリをリソースタイプごとに分割する
- 定期的に
terraform plan を実行してインフラストラクチャドリフトをチェックし是正し、パネルでの誤操作による手動変更を避ける
このフローはとても面倒に見えますが、毎回の变更に記録があり、監査可能で、ロールバック可能、何より再現可能である点が、IaC が最も価値のある部分です。
参考
機能的なCookieを受け入れるまで、コメントは無効になっています。