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

状態管理
Terraform はインフラストラクチャ変更操作を実行する際の状態情報を状態ファイルに保存します。デフォルトでは現在の作業ディレクトリ内の terraform.tfstate ファイルに保存されます3。構成ファイルを変更して他のストレージバックエンド(S3、Postgres など)を指定することも可能です。terraform apply を実行するたびに、Terraform は現在の構成ファイルで宣言された状態と既存の状態ファイルを比較し、変更部分を計算して調整順序を自動的に決定した後、Provider を操作して状態変動を実現します。
Terraform のリソースインポートの難題
既存リソースのインポートはこれまで Terraform が批判されてきた問題点であり、Hashi Corp. は一貫して「すべてのインフラストラクチャは最初から Terraform で作成されるべきであり、リソースインポートの問題などは存在しない」という頑固で愚かな見解を堅持しているようです。
| 時期 | バージョン | 進展 | 問題 |
|---|---|---|---|
| 2014–2022 | v0.x–v1.4 | terraform import のみで、一度に 1 件ずつ、かつ全く設定を生成しない | インポート後でも自分で HCL4 を書く必要がある |
| 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 の設計哲学は宣言性と冪等性5であり、Hashi Corp. は「すべてはコード声明された状態が基準であり、Terraform でゼロからリソースを作成すべきだ」と信じています。しかし現実には、ほとんどの企業には大量の歴史的残存リソースがあり、「先にリソースがあり、後にコードがある」のが常态です。この矛盾を Hashi Corp. が認めたのは遅く、v1.12 の terraform query になって初めて公式にこの問題に対処しましたが、Provider エコシステムの追従状況……正直言って期待薄です。
Terraform のファイル構造
Terraform のファイル構造はシンプルで、メインプログラムは実行時、作業ディレクトリ内のすべての .tf ファイルを盲目的に読み取ります。情報さえ揃っていれば、ファイル名は何でも好きなように付けられます。私のファイル構造はこのようになります:
❯ 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はベース設定を保存するために使用されます:
| |
provider.tfは各 Provider の設定を保存するために使用されます:
| |
ここが空なのは、Credentials を構成ファイルに直接書くのではなく、環境変数を通じて渡せるからです。例えば Cloudflare の Provider は、CLOUDFLARE_API_TOKEN を api_token 変数の代替として受け入れます。
variables.tfはカスタム変数を宣言するために使用されます:
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 ファイルの変数も自動的に読み取ります。変数の主な用途は、他の構成ファイルから呼び出すことです。例えば:
| |
これらの基本設定があれば、terraform init を実行して Terraform 環境を初期化し、依存関係のバージョンをロックできます。残りの部分はすべてリソースの宣言となります。
既存リソースを 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 に配置して実行権限を与える方法もあります。
| |
このツールの使い方は比較的シンプルです。まず以下の環境変数が必要です:
| |
ヒントここでのコマンドは 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 です:
| |
このステップで現在のディレクトリに zone.tf という名前のファイルが生成され、以下のフォーマットのコンテンツが含まれます:
| |
これでドメインリソースはインポートされましたが、中身の設定はまだありません。次に、ドメイン内の DNS レコードをインポートします:
| |
このステップで現在のディレクトリに dns.tf という名前の構成ファイルが生成され、以下のフォーマットのコンテンツが含まれます:
| |
複数のドメインをインポートする場合は、環境変数 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 ブロックを生成:
| |
cloudflare_dns_record の import ブロックを生成:
| |
このステップで現在のディレクトリに import.tf が生成されます。必要なインポート情報が含まれており、役割は Terraform に上一步で生成された各 resource ブロックが、どのクラウドプロバイダーのリソース ID に対応するのかを教えることです。この ID はクラウドプロバイダー内部でリソースをマークするコードであり、普段はコントロールパネルに表示されません。API で特別にリクエストしない限り知ることはできません。Terraform はインポートプロセス中にこの ID を使用して、ローカルの定義がクラウド上のどのリソースに対応しているかを確認し、厳密な冪等性を実現します。
さて、ここでは terraform plan を実行します:
| |
Add、Change、Destroy のリソース数がすべて 0 の場合、インポート操作に問題がないことになります。そのまま terraform apply --auto-approve を実行すれば、リソースのインポート完了です。
| |
状態保存と継続的インテグレーション
IaC の重要な価値の一つは、Git ベースの複数人コラボレーションと CI の継続的インテグレーションを容易に実現できる点ですが、その前に解決すべき新たな問題があります——terraform.tfstateはどこに置くか:環境を変更するたびに辛苦入れてインポートした状態を失う人は誰も望んでいません。
Terraform は現在、以下の状態保存バックエンドをサポートしています:
- local
- remote
- azurerm
- consul
- cos
- gcs
- http
- Kubernetes
- oci
- oss
- pg
- s3
特別な要件がなければ、私のように s3 を選択することをお勧めします。Cloudflare R2 には無料枠があるので、使わない手はありません。
| |
S3 バックエンドの場合、Credentials を保存するには AWS_ACCESS_KEY_ID と AWS_SECRET_ACCESS_KEY の 2 つの環境変数を使用することをお勧めします:
| |
設定完了後、terraform init -migrate-state を実行すれば、設定はクラウド上に正常に保存されます。今後どこで設定を変更しても、terraform apply を実行しても、Terraform の State が同期していないことを心配する必要はありません。
続いて Github CI の設定ですが、実はとてもシンプルです。git push のたびに terraform init、terraform fmt、terraform apply をトリガーするだけです。以下が私の .github/workflows/apply.yml です:
| |
PR に対しては、CI が毎回 PR に terraform plan の出力を添付するように設定すべきです:
| |

今後のワークフロー
ここまでで、Terraform の「接管初期化」は終了し、日常保守フェーズに入りました。この段階では実際には 3 つのことしかありません:
- 新しいリソースの作成
- 既存リソースの変更
- 不要になったリソースの削除
よく使う操作は 2 つ:terraform plan と terraform apply。個人利用の場合、小さな変更はそのままコミットしても良いでしょう。チームで協力する場合、毎回変更は PR を通じた方が原則です。
インフラストラクチャの作成
今 DNS レコードを追加したり、Tunnel やオブジェクトストレージバケットを新しく作る場合のフローは以下の通りです:
最も理想的な plan 出力は:
X to add0 to change0 to destroy
何かを追加したいだけなのに to destroy が表示された場合は、焦らずにチェックしてください。引用ミスタイプ、変数間違い、またはリソースアドレスのうっかり変更などが原因であることがほとんどです。どこに問題があるのか仔細に確認してください。
修改と删除基础设施
変更フローは作成と似ていますが、もう一つのステップが増えます——変更が再構築を引き起こすかどうかの評価です。
多くの Provider フィールドは ForceNew となっており、フィールドを変えるつもりが Terraform は「了解、削除して再構築します」と言ってくるかもしれません。DNS のようなリソースでは大きな問題ではありませんが、クラウドインスタンスのようなリソースになると、削除して再構築すれば損失が発生します。
以下の順序で進めることをお勧めします:
本番環境にとって、作成や変更が遅くても大きな問題ではありません。重要なのは操作の正確性です。少し遅くても、間違えないようにしてください。IaC はスピードを競うのではなく、予測可能性を競うものです。
もしリソースを削除する場合(例えば DNS レコードの廃止、废弃 Tunnel の整理)、以下のフローに従ってください6:
日常のコラボレーション提案
- Credentials は環境変数や CI Secret に置き、
.tfやリポジトリに書き込まない - 重要なリソースには保護ポリシーを有効にし、誤削除を防ぐ
- ディレクトリをリソースタイプごとに分割する
- 定期的に
terraform planを実行してインフラストラクチャドリフトをチェック7し是正し、パネルでの誤操作による手動変更を避ける
このフローはとても面倒に見えますが、毎回の变更に記録があり、監査可能で、ロールバック可能、何より再現可能である点が、IaC が最も価値のある部分です。
参考
- Terraform を使って CloudFlare 上の DNS 解析レコードを管理する - Candinya
- Automate Terraform with GitHub Actions
- Query configuration files
- list block reference
- cloudflare/cf-terraforming
- Import Cloudflare resources
- Cloudflare Provider - Terraform Registry
Infrastructure as Code、機械可読な構成ファイルを使用して必要なインフラストラクチャのデプロイ方法を定義することを指します。 ↩︎
各プロバイダーの構成ファイルフィールド名や形式には違いがありますが、これはスクリプトを書くことで容易に変換できます。 ↩︎
特に注意すべき点は、状態ファイルにはデータベースのパスワードや API キーなど、プレーンテキストで保存される機密情報が含まれる可能性があるため、絶対に
.tfstateファイルを公開コードリポジトリにアップロードしないことです。 ↩︎HashiCorp Configuration Language。HashiCorp 自社開発の宣言型構成言語で、機械可読性と人間可読性の両立を目指している。 ↩︎
冪等性(Idempotence)とは、コンピュータシステムまたはインターフェースが同一の要求を複数回受け取った場合、影響が単回実行の結果と同じになる性質のこと。何度実行しても、システムの最終状態は常に一致する。 ↩︎
Terraform にリソースの管理から外れさせたいだけで、クラウド上で実際に削除したいわけではない場合は、
terraform state rmコマンドを使用してください。コードからリソースブロックを削除してapplyするのは避けてください。そうしないとクラウド上の実際のリソースも同時に削除されてしまいます。 ↩︎インフラストラクチャドリフト(Infrastructure Drift)とは、現実世界中でコンソールのクリックなど IaC 以外の経路でインフラストラクチャに変更を加え、実際の状態とコード内で宣言された状態が不一致になる現象のこと。 ↩︎

機能的なCookieを受け入れるまで、コメントは無効になっています。