はじめに
GMOコネクト株式会社でパブリッククラウドなどを使用したインフラ基盤の構築やフロントエンド、バックエンドのコーディングを担当させていただいている遠藤です。
前回の#いまから始めるクラウド環境構築のためのTerraformでは、Terraformの基礎知識についてざっくりと調査し、解説させていただきました。
その後、社内でTerraformを活用する機会が何度かあったため、実際に使用することで深まった知見を基に追加でおさらいしたほうが良い情報をまとめてみました。
前回のおさらい
前回のPart1では、Terraformとはなんぞや?といったところから始まり、事前準備の部分から実際に以下
- AWS で VPC+サブネット+EC2 を立てる
- GCP で VPC+サブネット+GCE を立てる
という、「まずコンピューティングリソースを動かす」ところまでを一緒に見ていったかと思います。
いわゆる『terraform init → plan → apply (→ destroy)』までの一連のライフサイクルを一通り触ってみた形ですね。
今回はその続きとして、実際に業務でTerraformを使用した際に使用したいくつかの機能、また大事だと感じたポイントをいくつかピックアップしてまとめてみます。
今回扱うテーマ
今回は主に以下の4つ(+おまけひとつ)の観点を主に取り上げたいと思います。
- モジュール化:ひとつの.tfファイルが肥大しすぎないようにする
- .tfvars:環境ごとに変わる値を整理する
- "terraform import":Terraform以外で作成した既存リソースをTerraform管理に寄せる
- CI/CD組み込み:毎回手元でApplyする作業の卒業
- (+おまけ)
実践的にTerraformを使用する際は、どうしても作成するリソースが大きくなりがちなので、これらのテーマはちょくちょく出てくる話かと思います。
1.モジュール化
1-1. 「main.tf一本で管理する」ことの限界
単純に自分のみで構築する小規模なプロジェクトであれば、main.tf 1枚にすべてをべた書きしたり、少数のファイル分割でなんとかなる(こともある)のですが、実際の業務では以下のような点を考慮しなければならないことが多いです。
- 環境が1環境だけではなく、たとえば開発/検証/本番などで分割されている
- サービスが肥大化、複雑化していきCloudRun、Pub/Sub、GCEなど、管理するリソースが増える
- 検証環境でも、リージョン違いなどで一部設定のみ違う類似構成を複数作る
こういった部分が出てくると、main.tfは一気に1000行~以上の長大なコードとなり、しかも一部分のみ違うコードが複数並んだり、他環境への影響がコードだけでは読み取りにくい地獄のようなコードが発生してしまいます。
ということで、ここでTerraformの機能である「モジュール」の出番となります。
1-2. モジュール化の際のファイル構成
正直ここはいくつか書き方があると考えていますが、今回取り扱う構成としては、Modules × environments の2フォルダを主軸とする構成です。
ざっくりと各フォルダを解説すると以下のような感じになります。
- modules/
- VPC、アプリサーバー、監視など「役割ごとのモジュール」を置く
- environments/
- dev/, stg/, prod/ など環境ごとのディレクトリ
- どのモジュールをどの値で使うか、という部分を主に記載する
この構成には以下のメリットがあります。
- 使いまわせるモジュールと、それを呼び出すする環境フォルダの分離による、権限の明確化とモジュール再利用の容易化
- 変更範囲を確認しやすいことによる、保守性の向上
ざっくりとした構成としてはたとえば以下のような感じになります。
infra/
├── modules/
│ ├── network/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── outputs.tf
│ ├── compute/
│ └── iam/
└── environments/
├── dev/
│ └── main.tf
├── stg/
│ └── main.tf
└── prod/
└── main.tf
重要な点としては、environments以下はできる限りモジュールを組み立てたり、変数に値を代入するだけのコードに寄せることです。
以下はdev/main.tfの例です。
module "network" {
source = "../../modules/network"
environment = "dev"
cidr_block = "10.0.0.0/16"
}
module "app" {
source = "../../modules/app"
environment = "dev"
subnet_id = module.network.public_subnet_id
}
反対に、モジュール側は「そのモジュールの対象のものをこうやって作る」といったロジックのみを書くようにしたほうが良さそうです。
2. .tfvarsで環境などの差分を寄せる
2-1. variables.tf と tfvars の関係性
-
variables.tf
- 変数の宣言のためのファイル
- 名前・型定義・説明・(必要な場合)デフォルト値などを記入する
-
.tfvars
- 宣言された変数に対し、実際にどのような値を入れるかを定義するファイル
- 環境ごとに違う設定値を使い分ける場合が特に効果を発揮する
例として、プロジェクトID、リージョンを切り替える場合は以下のような感じになります。
こちらで変数宣言
# variables.tf
variable "project_id" {
description = "Project ID"
type = string
}
variable "region" {
description = "Region for resources"
type = string
}
こちらで値の代入
# environments/dev/dev.tfvars
project_id = "example-dev"
region = "asia-northeast1"
# environments/prod/prod.tfvars
project_id = "example-prod"
region = "asia-northeast1"
実際に実行する際は、terraform コマンドの引数として-var-file=""の値を切り替えます。
terraform plan -var-file=../common/common.tfvars -var-file=dev.tfvars
terraform apply -var-file=../common/common.tfvars -var-file=dev.tfvars
こういった形で、環境によって変わる値は*.vfvarsに寄せることで、コードの共通化と、環境ごとの値の適切な切り替えがしやすくなります。
3.terraform import による、既存リソースのTerraformへの取り込み
3-1. terraform importとは
実際に実務などでTerraformを使用する際、要件定義の段階から厳密にTerraformのファイル定義などを決めておけばすべてを最初からIaC化できると思いますが、毎回そこまできれいに定義できないことも多々あり、また途中からTerraformを導入することになったり、作業スピード優先のためGUIで作成し、後からTerraform管理にする...といったことがしばしばあります。
そんな時に使用するのがこちらのimportコマンドになります。
ざっくりと概要を説明すると、現在の実クラウド環境の状態をstateファイル(現在の実リソースの状態を記録しているファイル)に登録する機能となります。
これによって、Terraform planなどを行った際の差分元にインポートした環境の設定が追加されるため、作成時にTerraform管理していなかったモジュールなどであっても差分管理のもととすることが可能です。
なお.tfファイルは自分で書く必要がありそうなので、完全に同一の環境をコードに落とし込みたいならおそらくimportした後、自分で.tfファイルを記載し、Planなどで差分を確認しつつ整合性を取る必要がありそうです。
3-2. importを使用した「2フェーズ構成」
Terraform未管理のモジュールなどをTerraform管理下に置き、そのうえで新たな設定を追加・変更したい場合、以下の2フェーズに分けたほうが無難です。
- フェーズ1:「現状再現」のみを行う
- まずは現状と同じ設定になるように HCL を整える
-
terraform planで差分がほぼ 0 になるまで調整する
- フェーズ2: その上で本来の修正を入れる
- 例: デフォルトのサービスアカウント → 専用のサービスアカウントに差し替える
- 例: env の値を tfvars に寄せる
こういった形で、慣れるまではTerraform管理下に移行して動作に問題がないかを確認した後、仕様変更を導入することを推奨します。
4.CI/CDへの組み込みについて
4-1. 自動化への道
こうして様々な環境やモジュールを取り込んでTerraformを触るチームや人数が増えていくと、
- 誰が・いつ・どの変更をapplyしたのか確認したい
- Planの中身をPRベースで紐づけたい
- 本番環境へは、限られた経路でだけapplyしたい
などの要求が出てくることがあります。
これらに関しては、Github ActionsやCloudBuildなどのCI/CDツールに載せることで実現できます。
たとえば、
- PRが出た場合
-
terraform fmt/terraform validate/terraform planを実行
-
- main ブランチにマージされたら:
-
terraform applyを実行など(最初は手動トリガにして様子を見たりして調整する)
-
以下、サンプルのワークフローです。
name: Terraform
on:
pull_request:
paths:
- 'infra/**'
push:
branches:
- main
paths:
- 'infra/**'
jobs:
terraform:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./infra
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.7.0 # 実際のバージョンに合わせる
- name: Terraform Init
run: terraform init
- name: Terraform Format
run: terraform fmt -check -recursive
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan (for PR)
if: github.event_name == 'pull_request'
run: terraform plan -no-color -var-file=environments/dev/dev.tfvars
- name: Terraform Apply (for main)
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
run: terraform apply -auto-approve -var-file=environments/prod/prod.tfvars
注意
実際にはこのコード以外に、
- GithubActionsのワーカーにGCPやAWSの実行権限をつける
- 記載内容に沿ったファイル、フォルダ構成にする
などの観点を注意する必要があります。
おまけ. terraform planをする際によく使う引数など
規模が大きくなってきたときに特に助かった 3 つの小技を紹介しておきます。
-
-targetオプション terraform plan -out-
terraform showでの .txt 出力
5-1. -target:この辺だけ plan したいときに
本来の Terraform の思想からすると「全体に対して plan/apply する」のが正しいのですが、
実務では「このリソース周辺だけ試したい」ということもよくあります。
たとえば、テスト用に急遽作るだけ作った一時的な変更を含めた.tfファイルが残っている場合などです。
その場合、以下のように-target=といった引数をつけて挙げることで、PlanやApplyなどの対象を絞ることが可能です。
例:
terraform plan \
-target=module.cloud_run.google_cloud_run_v2_service.app \
こうすることで、対象をmoduleフォルダ内のCloudRunサービスに絞ることができます。
- import 直後の確認や、特定の SA/IAM だけを変更したいときなど、影響範囲を自分で把握できているケースでは便利
- ただし、「依存関係を無視して一部だけ変更する」こともできてしまうので、常用ではなくスポット用途に留めるのが安全
という感想でした。
5-2. plan -out:そのプランだけ apply する
レビューや承認の観点では、OKを出したPlanと実際にApplyされる変更観点が完全に一致しているか?といった点を確認する必要があります。
その際に使用するのがterraform plan -out になります。
terraform plan \
-var-file=environments/dev/dev.tfvars \
-out=/home/Users/you/Documents/plan-app-dev.out
こういった形で引数を追加してあげることで、その時点のPlanをバイナリ形式で.outファイルにエクスポートすることが可能になります。
また、こちらの.outファイルをapplyの適用ソースとすることも可能です。
その場合、以下のように指定します。
terraform apply "/home/Users/you/Documents/plan-app-dev.out"
こうしておくと、plan → 承認 → apply の間にコードや state が変わっていても、
- 「承認済みの plan ファイル」に書かれている内容だけが適用される
という形にできます。
5-3. terraform show:人間が読む plan.txt を出す
-out で保存した plan はバイナリなので、そのままでは中身が読めません。
人間がレビューできる形にするには、terraform show などのコマンドを組み合わせます。
terraform show -no-color /home/Users/you/Documents/plan-app-dev.out \
> /home/Users/you/Documents/plan-app-dev.txt
-no-colorといったオプションがついている理由としては、Terraformの画面出力にはデフォルトでカラーコードが使用されており、単純にTxt化したのみだとカラーコードの記号が大量に出てきて見づらいためです。
-
.out: apply に使うバイナリの plan -
.txt: 承認フローやメール・チャットで共有するためのテキストレポート
と役割を分けておくと、インフラ変更のレビューや記録がだいぶやりやすくなりました。
なお、同じことは CI でもできます。例えば GitHub Actions なら:
- name: Terraform Plan (binary)
run: terraform plan -out=tfplan -no-color -var-file=environments/dev/dev.tfvars
- name: Save human-readable plan
run: terraform show -no-color tfplan > plan.txt
として plan.txt をアーティファクトに保存しておけば、「この PR のタイミングでどういう plan が出ていたか」を後から確認できます。
おわりに
今回は、Terraform を業務で使い続ける中で
- コードをモジュール化して
.tfの肥大化を防ぐ - tfvars で環境ごとの差分を整理する
-
terraform importで既存リソースを安全に取り込む - CI/CD に載せて、plan / apply をチームのフローに乗せる
-
-target/plan -out/terraform showで、大きくなった構成でも安全に変更する
といったあたりを、実際にやってみて分かった観点からまとめました。
これらの情報を抑えておくことで、よりチームで効率的にTerraformを使用することができ、IaCの強みを生かしていけると思います。