Terraformメモ

docker-compose.yml
services:
terraform:
image: hashicorp/terraform:1.6.2
volumes:
- ./setup:/tf/setup
- ./deploy:/tf/deploy
working_dir: /tf
environment:
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}
- AWS_SESSION_TOKEN=${AWS_SESSION_TOKEN}
- AWS_DEFAULT_REGION=ap-northeast-1
- TF_WORKSPACE=${TF_WORKSPACE}
setup/main.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "5.23.0"
}
}
backend "s3" {
bucket = "devops-practice-terraform-state-2503"
key = "tf-state-setup"
region = "ap-northeast-1"
encrypt = true
dynamodb_table = "devops-practice-terraform-lock"
}
}
provider "aws" {
region = "ap-northeast-1"
default_tags {
tags = {
Environment = terraform.workspace
Project = var.project
Contact = var.contact
ManageBy = "Terraform/setup"
}
}
}
setup/variables.tf
variable "tf_state_bucket" {
description = "Name of S3 bucket in AWS for storing TF state"
default = "devops-practice-terraform-state-2503"
}
variable "tf_state_lock_table" {
description = "Name of DynamoDB table for TF state locking"
default = "devops-practice-terraform-lock"
}
variable "project" {
description = "Project name for tagging resources"
default = "devops-practice-terraform"
}
variable "contact" {
description = "Contact name for tagging resources"
default = "xxx"
}
init
docker compose run --rm terraform -chdir=setup init
% docker compose run --rm terraform -chdir=setup init
WARN[0000] The "TF_WORKSPACE" variable is not set. Defaulting to a blank string.
Initializing the backend...
Successfully configured the backend "s3"! Terraform will automatically
use this backend unless the backend configuration changes.
Initializing provider plugins...
- Finding hashicorp/aws versions matching "5.23.0"...
- Installing hashicorp/aws v5.23.0...
- Installed hashicorp/aws v5.23.0 (signed by HashiCorp)
Terraform has created a lock file .terraform.lock.hcl to record the provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.
Terraform has been successfully initialized!
You may now begin working with Terraform. Try running "terraform plan" to see
any changes that are required for your infrastructure. All Terraform commands
should now work.
If you ever set or change modules or backend configuration for Terraform,
rerun this command to reinitialize your working directory. If you forget, other
commands will detect it and remind you to do so if necessary.
fmt
docker compose run --rm terraform -chdir=setup fmt
validate
docker compose run --rm terraform -chdir=setup validate

この terraform
ブロックは、Terraformの動作に必要なプロバイダの指定と、状態ファイルの保存先(バックエンド)の設定を行っています。以下に詳しく解説します:
terraform { ... }
ブロックの役割
🔷 Terraformのグローバル設定(プロバイダ、バックエンドなど)を定義するブロックです。
required_providers
ブロック
① Terraformで使う**プロバイダ(クラウドやサービス連携のプラグイン)**を指定します。
required_providers {
aws = {
source = "hashicorp/aws"
version = "5.23.0"
}
}
解説:
項目 | 説明 |
---|---|
aws |
使用するプロバイダ名。ここでは AWS |
source |
プロバイダの提供元。HashiCorp社製なので hashicorp/aws
|
version |
利用するバージョンを固定。ここでは 5.23.0 にピン留め(将来のバージョン差異による不具合を防ぐ) |
backend "s3"
ブロック
② Terraformの状態ファイル(.tfstate
)の保存先を ローカルではなくS3にする ための設定です。
backend "s3" {
bucket = "DevOps-Recipe-App-TF-State"
key = "tfstate-setup"
region = "us-east-1"
encrypt = true
dynamodb_table = "DevOps-Recipe-App-API-TF-Lock"
}
解説:
項目 | 説明 |
---|---|
bucket |
手動で作成したS3バケット名。Terraformの状態ファイルをこの中に保存します。 |
key |
S3バケット内でのファイル名(またはパス)。この場合 tfstate-setup 。他に tfstate-deploy を使うこともある。 |
region |
上記のS3バケットおよびDynamoDBが存在するAWSリージョン。例:us-east-1
|
encrypt |
S3に保存される状態ファイルをサーバーサイド暗号化する設定。true で有効 |
dynamodb_table |
手動で作成したDynamoDBのテーブル名。**Terraformのロック管理(状態変更の同時実行防止)**に使用される |
🔸 なぜこの設定が重要なのか?
理由 | 内容 |
---|---|
✅ S3 バケット | Terraformの状態ファイルをチームで共有・一元管理できるようにするため |
✅ DynamoDB テーブル | 複数人が同時にTerraformを実行しても衝突しないようにロック制御するため |
✅ バージョン固定 | すべての環境で同じTerraform挙動になるようにするため(トラブル防止) |
✅ 補足
-
backend
に変数(例:var.bucket
)は使用できません(Terraformの仕様制限) - したがってバケット名やテーブル名は 直接記述する必要があります
このブロックは Terraform プロジェクトの基盤として最も重要な部分の一つで、チーム開発・CI/CD対応・本番運用を見据えた構成になっています。
この provider "aws"
ブロックは、Terraformで AWSとやり取りする際の設定 を記述したものです。以下で詳しく解説します。
provider "aws"
の役割
🔷 Terraformが「どのクラウド(サービス)と、どのような設定で接続するか」を指定するものです。
ここでは AWSプロバイダを us-east-1 リージョンで使う という設定になっています。
🔸 解説:各項目の意味
provider "aws" {
region = "us-east-1"
-
region = "us-east-1"
→ AWSリソースをデプロイするリージョンを指定しています。
例:EC2、S3、IAMなどすべてこのリージョンで作られます。
default_tags {
tags = {
environment = terraform.workspace
project = var.project
contact = var.contact
managed_by = "terraform/setup"
}
}
default_tags
ブロックとは?
🔹 Terraform で作成する すべてのAWSリソースに共通で自動付与されるタグ を定義できます。
タグは AWS管理画面上でリソースの識別や管理、コスト配分に使われるメタ情報です。
各タグの意味:
キー | 値 | 説明 |
---|---|---|
environment |
terraform.workspace |
Terraformのワークスペース名(例: default , staging , production )。環境名として使われます。 |
project |
var.project |
プロジェクト名。variables.tf で定義された変数を参照(例: recipe-app-api ) |
contact |
var.contact |
管理者の連絡先(メールなど)。誰に連絡すればよいか分かるように |
managed_by |
"terraform/setup" |
このリソースが何によって管理されているか。手動ではなくTerraformコードによる管理であることを明示 |
✅ メリット
-
タグの一括設定:個々のリソースに毎回
tags = {}
を書かなくてよくなる - ヒューマンエラー防止:タグのつけ忘れを防げる
- リソースの可視性向上:AWSコンソール上で、誰が・何のために作成したかが明確になる
- コスト管理・分析にも便利:タグでプロジェクトごとにコストを集計できる
terraform.workspace
とは?
🧩 補足:Terraformの「ワークスペース名」を取得する特殊変数です。
- 例:
-
default
:デフォルトのワークスペース -
staging
やproduction
:開発環境や本番環境など、環境ごとに使い分け可能
-
✅ まとめ
この provider "aws"
ブロックは、AWSへの接続情報と、全リソースに共通のタグ付けルール を設定しています。
実務でもとても重要で、環境ごとの識別・管理・保守性の向上 に大きく貢献します。

#######################################################################
# Create IAM user and policies for Continuous Deployment (CD) account #
#######################################################################
resource "aws_iam_user" "cd" {
name = "app-api-cd"
}
resource "aws_iam_access_key" "cd" {
user = aws_iam_user.cd.name
}
#########################################################
# Policy for Teraform backend to S3 and DynamoDB access #
#########################################################
data "aws_iam_policy_document" "tf_backend" {
statement {
effect = "Allow"
actions = ["s3:ListBucket"]
resources = ["arn:aws:s3:::${var.tf_state_bucket}"]
}
statement {
effect = "Allow"
actions = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"]
resources = [
"arn:aws:s3:::${var.tf_state_bucket}/tf-state-deploy/*",
"arn:aws:s3:::${var.tf_state_bucket}/tf-state-deploy-env/*"
]
}
statement {
effect = "Allow"
actions = [
"dynamodb:DescribeTable",
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem"
]
resources = ["arn:aws:dynamodb:*:*:table/${var.tf_state_lock_table}"]
}
}
resource "aws_iam_policy" "tf_backend" {
name = "${aws_iam_user.cd.name}-tf-s3-dynamodb"
description = "Allow user to use S3 and DynamoDB for TF backend resources"
policy = data.aws_iam_policy_document.tf_backend.json
}
resource "aws_iam_user_policy_attachment" "tf_backend" {
user = aws_iam_user.cd.name
policy_arn = aws_iam_policy.tf_backend.arn
}
以下ではこの Terraform コードを 文法(構文)的な観点から1行ずつ解説します。
Terraform は HCL (HashiCorp Configuration Language) という宣言型の構成言語を使っています。
✅ 全体構成と文法ルール
このコードは以下のブロックに分かれています:
-
resource
:実際の AWS リソースを作成する -
data
:外部データや構成情報を読み込む(参照のみ、作成はしない) -
variable
:変数の定義(ここでは出てきませんがvar.xxxx
で参照されています)
🔷 1. IAM ユーザーとアクセスキーの作成
resource "aws_iam_user" "cd" {
name = "app-api-cd"
}
-
resource
:リソース定義ブロック -
"aws_iam_user"
:リソースタイプ(プロバイダ名 + リソース種別) -
"cd"
:ローカル名(Terraform内部で参照する識別子) -
{ ... }
:ブロック本体(属性・引数を記述)
resource "aws_iam_access_key" "cd" {
user = aws_iam_user.cd.name
}
-
user = aws_iam_user.cd.name
:他リソースの属性値を参照(ドット記法) -
aws_iam_user.cd.name
:先に定義した IAM ユーザーのname
属性を取得
🔷 2. ポリシーの定義(data ブロック)
data "aws_iam_policy_document" "tf_backend" {
-
data
:データソース定義。AWSに問い合わせるか、構成データを生成 -
"aws_iam_policy_document"
:ポリシー文書を生成するTerraform組み込みデータ型 -
"tf_backend"
:このブロックのローカル名
statement
ブロック:ポリシーの個々の許可ルールを記述
⬇︎ statement {
effect = "Allow"
actions = ["s3:ListBucket"]
resources = ["arn:aws:s3:::${var.tf_state_bucket}"]
}
-
statement
:ポリシーの1つの「許可ルール」 -
effect = "Allow"
:許可(または"Deny"
)を意味する -
actions
:許可する API アクションのリスト([]
はリスト構文) -
resources
:対象となる AWS リソース(ARN形式) -
${var.tf_state_bucket}
:Terraformの変数埋め込み(文字列補完)
→ HCL では 文字列の中に ${}
で変数や属性を埋め込める
statement {
effect = "Allow"
actions = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"]
resources = [
"arn:aws:s3:::${var.tf_state_bucket}/tf-state-deploy/*",
"arn:aws:s3:::${var.tf_state_bucket}/tf-state-deploy-env/*"
]
}
-
actions
もresources
も複数あるので[]
でリスト化 -
*
はワイルドカード(S3 内の全オブジェクトを対象とする)
statement {
effect = "Allow"
actions = [
"dynamodb:DescribeTable",
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem"
]
resources = ["arn:aws:dynamodb:*:*:table/${var.tf_state_lock_table}"]
}
-
arn:aws:dynamodb:*:*:table/
の*
は、任意のリージョン・アカウントIDを意味する - リソースの特定部分だけ動的に変数
${var.tf_state_lock_table}
で埋め込んでいる
🔷 3. IAM ポリシー作成(data ブロックの出力を利用)
resource "aws_iam_policy" "tf_backend" {
name = "${aws_iam_user.cd.name}-tf-s3-dynamodb"
description = "Allow user to use S3 and DynamoDB for TF backend resources"
policy = data.aws_iam_policy_document.tf_backend.json
}
-
name
:リソース名。ここでも${}
を使った文字列補完 -
policy = data...json
:data
ブロックで生成したポリシー文書を JSON 形式で取得
→ .json
は aws_iam_policy_document
に特有の属性。生成されたポリシーJSON文字列を返す。
🔷 4. ポリシーを IAM ユーザーにアタッチ
resource "aws_iam_user_policy_attachment" "tf_backend" {
user = aws_iam_user.cd.name
policy_arn = aws_iam_policy.tf_backend.arn
}
-
aws_iam_user.cd.name
:ユーザー名(識別子) -
aws_iam_policy.tf_backend.arn
:ポリシーの ARN(Amazon Resource Name)
✅ 文法のポイントまとめ
文法要素 | 解説 |
---|---|
resource |
AWS上に実際のリソースを作成 |
data |
外部情報の読み込み・生成(ここではポリシー文書) |
${...} |
文字列補間。変数やリソースの属性を挿入 |
[] |
配列(リスト)を表す |
{} |
ブロック(オブジェクト)を定義 |
. (ドット記法) |
リソースやデータの属性へアクセス |
arn: |
AWSリソースを一意に示すID文字列(リテラル) |
必要に応じて、次はこのコードの「構成設計上のベストプラクティス」的な視点でも解説できますので、気軽に言ってください!
このTerraformコードは、CI/CD用のIAMユーザーを作成し、Terraformのバックエンド(S3とDynamoDB)へのアクセス権限を与える設定をしています。以下に詳しく解説します。
✅ 全体の目的
- GitHub Actions や GitLab CI/CD などの自動化ジョブが AWS 上で Terraform を実行できるようにする。
- そのための IAM ユーザー(
app-api-cd
)を作成し、S3バケットとDynamoDBテーブルに対する 最小限のアクセス権限を付与する。
🔸 IAMユーザーとアクセスキーの作成
resource "aws_iam_user" "cd" {
name = "app-api-cd"
}
-
ユーザー名:
app-api-cd
- CI/CD 用のプログラムユーザーを作成。
resource "aws_iam_access_key" "cd" {
user = aws_iam_user.cd.name
}
- 上記ユーザー用の アクセスキーとシークレットキーを作成。
- これを使って CI/CD ジョブが AWS にアクセスする。
🔸 ポリシー(Terraformの状態管理に必要な権限)
data "aws_iam_policy_document" "tf_backend" {
...
}
この data
ブロックは、IAM ポリシーを Terraformの構文(HCL)で定義するものです。
✅ ポリシーの中身:
statement
: S3バケットの一覧取得
🔹 1つ目の actions = ["s3:ListBucket"]
resources = ["arn:aws:s3:::${var.tf_state_bucket}"]
-
.tfstate
ファイルを保存するS3バケットの「一覧取得」権限 - バケットの存在確認や状態チェックに必要
statement
: オブジェクト操作(Get/Put/Delete)
🔹 2つ目の actions = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"]
resources = [
"arn:aws:s3:::${var.tf_state_bucket}/tf-state-deploy/*",
"arn:aws:s3:::${var.tf_state_bucket}/tf-state-deploy-env/*"
]
- Terraform の
.tfstate
ファイル(本番・ステージングなど)にアクセスするための権限 -
tf-state-deploy/*
およびtf-state-deploy-env/*
はそれぞれ、Terraform の workspace に対応した状態ファイルの保存場所
statement
: DynamoDB テーブルへの操作権限
🔹 3つ目の actions = [
"dynamodb:DescribeTable",
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:DeleteItem"
]
resources = ["arn:aws:dynamodb:*:*:table/${var.tf_state_lock_table}"]
- DynamoDB テーブルに対するロック操作用の権限
- Terraform は
.tfstate
を更新するときに 排他制御(state locking) を行うため、これらの権限が必要
🔸 ポリシーを作成・IAMユーザーにアタッチ
resource "aws_iam_policy" "tf_backend" {
name = "${aws_iam_user.cd.name}-tf-s3-dynamodb"
...
}
- 上記
data
ブロックで作ったポリシードキュメントを実際のポリシー(AWS上の実体)として作成
resource "aws_iam_user_policy_attachment" "tf_backend" {
user = aws_iam_user.cd.name
policy_arn = aws_iam_policy.tf_backend.arn
}
- 作成したポリシーを
app-api-cd
ユーザーにアタッチ(適用)
🧠 補足まとめ
種類 | 内容 |
---|---|
IAMユーザー |
app-api-cd (CI/CD用) |
アクセスキー | マシン接続用のシークレット情報を生成 |
S3アクセス権 |
tf-state-deploy など .tfstate を読み書きするため |
DynamoDB権限 |
terraform apply 時のロック機構のため |
ポリシー形式 | JSONではなくTerraformの aws_iam_policy_document で定義 |
✅ 結果
このコードにより、CI/CDで使う専用IAMユーザーに、Terraformバックエンドに必要な最小限のアクセス権限が安全に付与されます。

resource
の後の "aws_iam_policy"
や "aws_iam_user_policy_attachment"
は 決まった語句 です。
✅ これは Terraform が認識する AWS のリソースタイプ なので、勝手に変えることはできません。
🔹 文法構造(リソースブロック)
resource "<プロバイダ名>_<リソース種別>" "<ローカル名>" {
...
}
-
"<プロバイダ名>_<リソース種別>"
:Terraform で定義されたリソースの正式なタイプ名- 例:
aws_iam_user
aws_iam_policy
aws_s3_bucket
-
aws_instance
(EC2)
- 例:
-
"<ローカル名>"
:任意の名前(Terraform 内で参照するための識別子)
🔸 例:OKな書き方(正しい構文)
resource "aws_iam_policy" "my_rule" {
...
}
-
aws_iam_policy
:決まったAWSリソースタイプ(これは固定) -
my_rule
:任意に自分で決めたローカル名(好きな名前をつけられる)
❌ NGな書き方(リソースタイプを勝手に変えている)
resource "aws_iam_policy_my_rule" "cd" {
...
}
-
aws_iam_policy_my_rule
:❌ Terraform はこの名前のリソースタイプを知らないため、エラーになります。
✅ ローカル名は自由につけられる
resource "aws_iam_policy" "tf_backend" {
この "tf_backend"
の部分は、たとえば次のように変えても構文的には問題ありません:
resource "aws_iam_policy" "main_policy_for_ci_user" {
※ただし、他の箇所で参照するときはこの名前を使う必要があるので、意味のある・わかりやすい名前をつけるのがベストです。
✅ まとめ
要素 | 内容 | 変更可能? |
---|---|---|
"aws_iam_policy" |
リソースタイプ(Terraform定義済み) | ❌ 固定 |
"tf_backend" (ローカル名) |
任意の識別子(Terraform内で使う) | ✅ 自由に変更可 |
もっと詳しく知りたい場合は、Terraform Registry(公式リファレンス) で aws_iam_policy
などのリソースタイプ一覧を見るのもおすすめです。

% docker compose run --rm terraform -chdir=setup apply
WARN[0000] The "TF_WORKSPACE" variable is not set. Defaulting to a blank string.
data.aws_iam_policy_document.tf_backend: Reading...
data.aws_iam_policy_document.tf_backend: Read complete after 0s [id=3858198220]
Terraform used the selected providers to generate the following execution plan. Resource actions are indicated with the following
symbols:
+ create
Terraform will perform the following actions:
# aws_iam_access_key.cd will be created
+ resource "aws_iam_access_key" "cd" {
+ create_date = (known after apply)
+ encrypted_secret = (known after apply)
+ encrypted_ses_smtp_password_v4 = (known after apply)
+ id = (known after apply)
+ key_fingerprint = (known after apply)
+ secret = (sensitive value)
+ ses_smtp_password_v4 = (sensitive value)
+ status = "Active"
+ user = "app-api-cd"
}
# aws_iam_policy.tf_backend will be created
+ resource "aws_iam_policy" "tf_backend" {
+ arn = (known after apply)
+ description = "Allow user to use S3 and DynamoDB for TF backend resources"
+ id = (known after apply)
+ name = "app-api-cd-tf-s3-dynamodb"
+ name_prefix = (known after apply)
+ path = "/"
+ policy = jsonencode(
{
+ Statement = [
+ {
+ Action = "s3:ListBucket"
+ Effect = "Allow"
+ Resource = "arn:aws:s3:::devops-practice-terraform-state-2503"
},
+ {
+ Action = [
+ "s3:PutObject",
+ "s3:GetObject",
+ "s3:DeleteObject",
]
+ Effect = "Allow"
+ Resource = [
+ "arn:aws:s3:::devops-practice-terraform-state-2503/tf-state-deploy/*",
+ "arn:aws:s3:::devops-practice-terraform-state-2503/tf-state-deploy-env/*",
]
},
+ {
+ Action = [
+ "dynamodb:PutItem",
+ "dynamodb:GetItem",
+ "dynamodb:DescribeTable",
+ "dynamodb:DeleteItem",
]
+ Effect = "Allow"
+ Resource = "arn:aws:dynamodb:*:*:table/devops-practice-terraform-lock"
},
]
+ Version = "2012-10-17"
}
)
+ policy_id = (known after apply)
+ tags_all = {
+ "Contact" = "xxx@gmail.com"
+ "Environment" = "default"
+ "ManageBy" = "Terraform/setup"
+ "Project" = "devops-practice-terraform"
}
}
# aws_iam_user.cd will be created
+ resource "aws_iam_user" "cd" {
+ arn = (known after apply)
+ force_destroy = false
+ id = (known after apply)
+ name = "app-api-cd"
+ path = "/"
+ tags_all = {
+ "Contact" = "xxx@gmail.com"
+ "Environment" = "default"
+ "ManageBy" = "Terraform/setup"
+ "Project" = "devops-practice-terraform"
}
+ unique_id = (known after apply)
}
# aws_iam_user_policy_attachment.tf_backend will be created
+ resource "aws_iam_user_policy_attachment" "tf_backend" {
+ id = (known after apply)
+ policy_arn = (known after apply)
+ user = "app-api-cd"
}
Plan: 4 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ cd_user_access_key_id = (known after apply)
+ cd_user_access_key_secret = (sensitive value)
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
aws_iam_user.cd: Creating...
aws_iam_user.cd: Creation complete after 1s [id=app-api-cd]
aws_iam_access_key.cd: Creating...
aws_iam_policy.tf_backend: Creating...
aws_iam_access_key.cd: Creation complete after 0s [id=xxx]
aws_iam_policy.tf_backend: Creation complete after 1s [id=arn:aws:iam::xxx:policy/app-api-cd-tf-s3-dynamodb]
aws_iam_user_policy_attachment.tf_backend: Creating...
aws_iam_user_policy_attachment.tf_backend: Creation complete after 0s [id=app-api-cd-20250327013316978000000001]
Apply complete! Resources: 4 added, 0 changed, 0 destroyed.
Outputs:
cd_user_access_key_id = "xxx"
cd_user_access_key_secret = <sensitive>
%

terraform init と terraform applyの違いを教えてください
はい!terraform init
と terraform apply
は Terraform の中でも特によく使うコマンドですが、それぞれ役割がまったく異なります。イメージしやすいように、実生活に例えながら説明します。
🛠 Terraform の役割ざっくり
Terraform は「インフラの設計図(コード)」を元に、実際にクラウド(AWS など)にその設計図通りのリソース(サーバー、DB など)を作ってくれるツールです。
terraform init
:準備フェーズ(プロジェクトの初期化)
🔹 🔧 何をする?
- Terraform の設定ファイルを読み込んで、
- 必要な プロバイダー(例: AWS, GCP)プラグインのダウンロード
-
.terraform/
フォルダの作成(キャッシュや依存情報の保存) - バックエンド(状態管理方法) の設定
🏗 実生活で例えると?
工事を始める前に、必要な工具や設計図、業者の連絡先を揃えるフェーズ。
✅ いつ使う?
- 初回実行時
-
provider
を追加・変更したとき -
.tf
ファイルで backend 設定を変更したとき
terraform apply
:実行フェーズ(インフラの構築)
🔹 🚀 何をする?
-
.tf
ファイルをもとにインフラの 変更内容(plan)を確認 - その内容に ユーザーが確認して OK を出すと、実際にクラウドにリソースを作成・更新・削除する
🏗 実生活で例えると?
設計図に基づいて、実際に建築工事を始めるフェーズ。
✅ いつ使う?
- 実際に AWS などにリソースを作成・更新したいとき
📝まとめ(違い)
コマンド | 目的 | 役割 |
---|---|---|
terraform init |
初期化 | 環境準備、provider の取得など |
terraform apply |
インフラの構築 | 実際にクラウドに反映させる |
もし plan
との違いも気になっていたら、それも説明できますよ!

terraform init をするタイミング ".tf ファイルで backend 設定を変更したとき"の具体例を教えてください
Terraform の backend
(バックエンド)は、Terraform の状態(state)ファイルをどこに保存するかを指定する仕組みです。そして .tf
ファイルでこの backend
の設定を変更したときは、再度 terraform init
を実行する必要があります。
backend
設定の具体例
✅ Terraform の状態(.tfstate
)ファイルは、デフォルトではローカルファイルに保存されます。ですが、チームで共有したり安全に保管するために、S3 などのリモートストレージに置くこともできます。
📄 例1:最初はローカル backend の場合(明示しない)
# main.tf
provider "aws" {
region = "ap-northeast-1"
}
resource "aws_s3_bucket" "example" {
bucket = "my-example-bucket"
}
この場合、Terraform の状態ファイルはローカルに terraform.tfstate
として保存されます。
📄 例2:S3 に backend を変更する場合(backend 設定を追加)
# backend.tf
terraform {
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "env/dev/terraform.tfstate"
region = "ap-northeast-1"
}
}
このように backend "s3"
を追加・変更すると、Terraform の状態ファイルの保存場所が ローカル → S3 に変わります。
init
が必要なのか?
⚠️ なぜ Terraform は backend
の設定を読み取って、初期化(.terraform/
に情報を保存)します。
そのため、backend
を変更した場合は 再度 init
を実行して設定を適用し直す必要があるのです。
Terraform はこのときにこういうメッセージを出します:
Initializing the backend...
Do you want to copy existing state to the new backend?
💡補足:よくある backend の種類
backend 種類 | 用途 |
---|---|
local |
ローカルファイルで管理(デフォルト) |
s3 |
AWS S3 に保存 |
gcs |
GCP Cloud Storage に保存 |
azurerm |
Azure Blob Storage に保存 |
remote |
Terraform Cloud / Enterprise |
✅ まとめ
.tf
ファイルで次のような変更をした場合に terraform init
を再実行します:
- backend を 新しく追加したとき(例: ローカル → S3)
- backend の パラメータを変更したとき(例: S3 の key や bucket 名)
- backend の 種類を変更したとき(例: S3 → Terraform Cloud)

for_eachとは
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
dynamic "route" {
for_each = var.enable_nat_gateway ? [1] : []
content {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main[0].id
}
}
tags = {
Name = "${var.project}-private-rt"
Environment = var.environment
}
}
ここでの for_each
のループ対象は dynamic "route"
ブロックの中身(content {}
) です。
dynamic "route"
の意味
✅ Terraform の dynamic
ブロックは、「可変長の入れ子ブロックを動的に生成」するための仕組みです。
この例では、aws_route_table
リソースの中で route
ブロックを動的に生成しています。
for_each
の対象は?
🧠 for_each = var.enable_nat_gateway ? [1] : []
-
true
のとき →[1]
(長さ1のリスト)→ 1回ループ →route
ブロックが1つ生成される -
false
のとき →[]
(空のリスト)→ 0回ループ →route
ブロックが生成されない
つまり、route
ブロック自体を生成するか否かを制御しているのです。
✨ イメージ
var.enable_nat_gateway |
生成される route ブロック |
---|---|
true |
0.0.0.0/0 → NAT Gateway |
false |
生成されない |
✅ 通常の書き方と比較
通常(静的)だとこう書きます:
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main[0].id
}
しかし enable_nat_gateway = false
のときにこの nat_gateway_id
を使おうとするとエラーになります。
☑️ だからこそ dynamic
+ for_each
で生成を条件付きにしているわけです!
必要であれば count
で代替するバージョンや、他の dynamic
利用例もお見せできますよ!
countで記述した場合
dynamic
を使わず、count
を使って 条件付きで aws_route
リソースを分離定義するパターンもよく使われます。以下に代替例を示します。
dynamic
を使った元のコード(ルート内に条件付き)
✅ resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
dynamic "route" {
for_each = var.enable_nat_gateway ? [1] : []
content {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main[0].id
}
}
tags = {
Name = "${var.project}-private-rt"
Environment = var.environment
}
}
count
による代替構成(aws_route
を別リソースに分離)
✅
aws_route_table
本体
resource "aws_route_table" "private" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.project}-private-rt"
Environment = var.environment
}
}
条件付きのルート(NAT用)
resource "aws_route" "private_nat" {
count = var.enable_nat_gateway ? 1 : 0
route_table_id = aws_route_table.private.id
destination_cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main[0].id
}
✅ 比較とポイント
比較点 |
dynamic ブロック |
count を使ったリソース分離 |
---|---|---|
Terraformの書き方 | 柔軟でネスト内の条件制御向き | ブロック自体を分けて記述 |
利点 | 1リソース内で完結 | 可読性が高い・シンプルに制御可能 |
注意点 | ネストが深くなりがち | 複数リソースになるので参照に注意 |
☑️ 結論
- 小さく単純な構成なら
count
ベースの分離型の方が読みやすくおすすめ。 - ルートが複数あって複雑な条件が絡む場合は
dynamic
が便利。
dynamicの後の文字列について
dynamic "route"
の "route"
の部分は 動的に生成するブロック名であり、これは そのリソースが受け入れる有効なブロック名である必要があります。
つまり:
✅ 結論:
"route"
は リソース aws_route_table
において、明示的に定義できるブロック名である必要があります。
何でもよいわけではなく、Terraform のリソース仕様で許されているブロック名でなければエラーになります。
具体例:
aws_route_table
の場合):
OKな例(resource "aws_route_table" "example" {
vpc_id = "vpc-123456"
dynamic "route" {
for_each = [1]
content {
cidr_block = "0.0.0.0/0"
gateway_id = "igw-123456"
}
}
}
ここで route
は 公式ドキュメント にある route
ブロックの定義に対応します。
NGな例(存在しないブロック名を使うとエラー):
resource "aws_route_table" "example" {
vpc_id = "vpc-123456"
dynamic "foo" {
for_each = [1]
content {
cidr_block = "0.0.0.0/0"
gateway_id = "igw-123456"
}
}
}
🔴 foo
は aws_route_table
に存在しないブロックなので、Terraform apply 時にエラーになります。
☑️ 補足
-
dynamic
ブロックの記法は、変化する数の ネストブロック(list/map形式) に対して使います。 -
"route"
のように Terraformリソースが本来受け入れるブロック名である必要があります。

なぜ[0]でアクセス?
output "nat_gateway_id" {
description = "NAT Gateway ID"
# NAT Gatewayは条件付きで作成されるため、存在しない場合はnullを返す
value = var.enable_nat_gateway ? aws_nat_gateway.main[0].id : null
}
✅ 結論から言うと:
aws_nat_gateway.main[0].id
と書かれているのは、aws_nat_gateway.main
が count
を使って複数作成される可能性がある「リスト状のリソース」だからです。
🔍 詳細解説:
Terraformで count = 1
や count = N
を指定してリソースを定義すると、そのリソースは「インデックス付きのリスト形式」になります。
たとえば:
resource "aws_nat_gateway" "main" {
count = var.enable_nat_gateway ? 1 : 0
...
}
このように count
を使っている場合、Terraform は aws_nat_gateway.main
を 単体のリソースではなく、配列のような形式で管理します。
- つまり:
-
count = 1
ならaws_nat_gateway.main[0]
-
count = 3
ならaws_nat_gateway.main[0]
,[1]
,[2]
など
-
- 逆に
count
を使っていなければ、.id
のように直接アクセスできます。
💡 例の違い:
単一リソース(countなし)
resource "aws_nat_gateway" "main" {
allocation_id = ...
subnet_id = ...
}
# アクセス方法:
aws_nat_gateway.main.id
複数リソース(countあり)
resource "aws_nat_gateway" "main" {
count = 1
allocation_id = ...
subnet_id = ...
}
# アクセス方法:
aws_nat_gateway.main[0].id
☑️ まとめ
条件 | アクセス方法 |
---|---|
count を使っていない |
aws_nat_gateway.main.id |
count = 1 または >1
|
aws_nat_gateway.main[0].id |
質問の箇所は count = var.enable_nat_gateway ? 1 : 0
により「0 または 1個のリソース」となるため、
Terraformは常にリスト(配列)として扱う仕様になっており、インデックスアクセス [0]
が必要なのです。
for_eachとcountの違い
for_each
を使った場合と count
の違いは、リソースへのアクセス方法や拡張性に大きく関係してきます。
for_each
を使うとどうなるか?
✅ 1. リソースは「マップ形式」で管理される
例えば:
resource "aws_nat_gateway" "main" {
for_each = var.enable_nat_gateway ? { "main" = 1 } : {}
allocation_id = aws_eip.nat[each.key].id
subnet_id = aws_subnet.public[0].id
}
- この場合、アクセスはこうなります:
aws_nat_gateway.main["main"].id
count
vs for_each
の違い比較表
🔁 特徴 | count |
for_each |
---|---|---|
型 | 配列(リスト) | マップ(辞書) |
アクセス方法 | aws_xxx.resource[0] |
aws_xxx.resource["key"] |
null許容・0件対応 | 柔軟だが順番に依存する | 柔軟、名前付きで明示的 |
複数リソースの条件付き作成 | count = var.create ? 1 : 0 |
for_each = var.create ? {"key"=1} : {} |
実行プランでの差分に強い | 要素順の変更で diff が起きやすい | 明示的な key 管理で差分が起きにくい |
典型的用途 | 単純な繰り返し・個数指定のリソース | 名前付きインスタンス(複数 NAT など) |
🧠 どっちを使うべき?
シチュエーション | 推奨 |
---|---|
数だけ変えたいとき(N個作る) | ✅ count
|
名前付き・個別に制御したい(AZごと等) | ✅ for_each
|
🧪 補足:アクセスエラーの例
-
count
を使ったのに.id
と書いてしまうと:
# ❌ エラーになる
output "nat_id" {
value = aws_nat_gateway.main.id
}
│ The given "count" argument references the index of a list of resources. To reference individual resources use: aws_nat_gateway.main[0]
✅ まとめ
-
count
➤ 数で制御、配列アクセス[0]
-
for_each
➤ 名前で制御、マップアクセス["key"]
- 条件付きで1つだけ作るならどちらも使えるが、明示的なキーを管理したいときは
for_each
が便利