「EC2に追加するEBSボリュームの数を環境ごとに変えたいのに、resource{}自体をfor_eachで増やしても内部のブロックを制御できない」
Terraformを実務で使い始めると、ブロック単位での繰り返し設定という壁にぶつかります。resource{}全体をfor_eachで増やすことはできても、resource{}の内部にある特定のネストブロック(ingress・ebs_block_device・tags等)を動的に生成するには、別の構文が必要です。それがdynamic blockです。
この記事では、TerraformのHCLでネストブロックを動的に生成するdynamic blockの仕組みを解説します。基本構文から始め、iterator引数のカスタマイズ・セキュリティグループへの実務適用・for式との組み合わせ・よくあるエラーの切り分けまで、設計パターンとして体得できます。
実行環境:Terraform 1.8.x(RHEL 9.4 / Ubuntu 24.04 LTS で動作確認済み)
この記事のポイント
・dynamic blockはresource内のネストブロックをリストや集合から動的に生成するHCL構文
・for_each + content{} の組み合わせが基本形。iterator引数で参照名を変更できる
・セキュリティグループのingress・EBSボリューム追加が代表的な実務ユースケース
・for式をfor_each内で使うと、変数リストの変換・フィルタリングを1行で書ける
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
なぜdynamic blockが必要なのか
まず問題を具体的に確認します。AWSセキュリティグループを定義するとき、許可するポートが複数ある場合、静的な記述だとこうなります。# 静的記述の例 ── ポートが増えるたびにブロックをコピーする resource "aws_security_group" "web" { name = "web-sg" vpc_id = var.vpc_id ingress { from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } ingress { from_port = 443 to_port = 443 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } ingress { from_port = 8080 to_port = 8080 protocol = "tcp" cidr_blocks = ["10.0.0.0/8"] } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } }
for_eachでresource{}を複数作ることはできても、resource{}の内部にあるingress{}ブロックの数を変数で制御することはできません。これを解決するのがdynamic blockです。
dynamic blockの基本構文
dynamic blockの基本形は次のとおりです。# dynamic blockの基本形 resource "リソースタイプ" "名前" { # 通常の属性 dynamic "ネストブロック名" { for_each = 繰り返し元(リスト・マップ・集合) content { # ブロック内で使う属性 属性名 = ネストブロック名.value.キー } } }
・dynamic "ネストブロック名":動的に生成したいブロックの名前を指定する。セキュリティグループなら「ingress」
・for_each:繰り返す元データ。リスト・マップ・set・for式の結果を受け取れる
・content{}:各繰り返しで生成されるブロックの中身。for_eachの各要素が展開される
・ネストブロック名.value:for_eachの各要素を参照する。マップの場合は.keyと.valueが使える
先ほどの静的なセキュリティグループをdynamic blockで書き直します。まず変数定義から。
# variables.tf variable "ingress_rules" { description = "セキュリティグループのingressルール一覧" type = list(object({ from_port = number to_port = number protocol = string cidr_blocks = list(string) })) default = [ { from_port = 80, to_port = 80, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }, { from_port = 443, to_port = 443, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"] }, { from_port = 8080, to_port = 8080, protocol = "tcp", cidr_blocks = ["10.0.0.0/8"] }, ] }
# main.tf resource "aws_security_group" "web" { name = "web-sg" vpc_id = var.vpc_id dynamic "ingress" { for_each = var.ingress_rules content { from_port = ingress.value.from_port to_port = ingress.value.to_port protocol = ingress.value.protocol cidr_blocks = ingress.value.cidr_blocks } } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } }
iterator引数で参照名をカスタマイズする
デフォルトでは、content{}内でブロック内の値を参照するとき「ネストブロック名.value」という形式を使います。上の例だと「ingress.value.from_port」です。ブロック名が長かったり、コードの可読性を上げたい場合はiterator引数で参照名を変更できます。
# iteratorで参照名を「rule」に変更する例 resource "aws_security_group" "web" { name = "web-sg" vpc_id = var.vpc_id dynamic "ingress" { for_each = var.ingress_rules iterator = rule # デフォルトの「ingress」を「rule」に変更 content { from_port = rule.value.from_port to_port = rule.value.to_port protocol = rule.value.protocol cidr_blocks = rule.value.cidr_blocks } } }
マップをfor_eachに渡す場合は「iterator名.key」と「iterator名.value」の両方が使えます。リストの場合は「iterator名.key」がインデックス番号(0, 1, 2...)、「iterator名.value」がリストの各要素になります。iteratorを指定した後で元のブロック名を使ってしまうエラーは実務でよく見るミスです。注意してください。
>> Terraform実践セミナーの詳細はこちら
実務ユースケース
セキュリティグループ以外にも、dynamic blockが役立つ実務シーンを2つ紹介します。1. EC2のEBSボリュームを動的に追加する
EC2インスタンスに追加のEBSボリュームをアタッチするebs_block_deviceブロックも、dynamic blockで管理できます。# variables.tf variable "ebs_volumes" { description = "追加EBSボリュームのリスト" type = list(object({ device_name = string volume_size = number volume_type = string encrypted = bool })) default = [ { device_name = "/dev/sdb", volume_size = 100, volume_type = "gp3", encrypted = true }, { device_name = "/dev/sdc", volume_size = 200, volume_type = "gp3", encrypted = true }, ] }
# main.tf resource "aws_instance" "app" { ami = var.ami_id instance_type = var.instance_type dynamic "ebs_block_device" { for_each = var.ebs_volumes iterator = vol content { device_name = vol.value.device_name volume_size = vol.value.volume_size volume_type = vol.value.volume_type encrypted = vol.value.encrypted } } }
2. IAMポリシーのStatementを動的に構成する
aws_iam_policy_documentのstatementブロックも、dynamic blockで動的に組み立てられます。権限セット(ポリシーステートメント)を変数リストで管理し、環境や用途に応じて内容を変える実務ユースケースです。# variables.tf variable "iam_statements" { description = "IAMポリシーのStatementリスト" type = list(object({ sid = string actions = list(string) resources = list(string) effect = string })) default = [ { sid = "AllowS3ReadOnly" actions = ["s3:GetObject", "s3:ListBucket"] resources = ["arn:aws:s3:::my-app-bucket", "arn:aws:s3:::my-app-bucket/*"] effect = "Allow" }, { sid = "AllowCloudWatchLogs" actions = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"] resources = ["arn:aws:logs:ap-northeast-1:123456789012:*"] effect = "Allow" }, ] }
# main.tf data "aws_iam_policy_document" "app" { dynamic "statement" { for_each = var.iam_statements iterator = stmt content { sid = stmt.value.sid actions = stmt.value.actions resources = stmt.value.resources effect = stmt.value.effect } } } resource "aws_iam_policy" "app" { name = "${var.env}-app-policy" policy = data.aws_iam_policy_document.app.json }
for式との組み合わせでリストを変換・フィルタリングする
dynamic blockのfor_eachにはfor式の結果を渡せます。変数リストをそのまま使うだけでなく、条件フィルタリングや値の変換を加えた動的生成が可能になります。3. 本番環境のみ特定ポートを許可する(条件フィルタリング)
# variables.tf variable "ingress_rules" { type = list(object({ from_port = number to_port = number protocol = string cidr_blocks = list(string) prd_only = optional(bool, false) # 本番専用フラグ })) default = [ { from_port = 80, to_port = 80, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"], prd_only = false }, { from_port = 443, to_port = 443, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"], prd_only = false }, { from_port = 8443, to_port = 8443, protocol = "tcp", cidr_blocks = ["0.0.0.0/0"], prd_only = true }, ] }
# main.tf resource "aws_security_group" "web" { name = "${var.env}-web-sg" vpc_id = var.vpc_id dynamic "ingress" { # for式でフィルタリング: prd_onlyがtrueのルールは本番環境のみ生成 for_each = [ for rule in var.ingress_rules : rule if !rule.prd_only || var.env == "prd" ] iterator = rule content { from_port = rule.value.from_port to_port = rule.value.to_port protocol = rule.value.protocol cidr_blocks = rule.value.cidr_blocks } } egress { from_port = 0 to_port = 0 protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } }
開発環境でterraform planを実行して確認します。
# TF_VAR_envにdevを渡してplan $ TF_VAR_env=dev terraform plan # 出力(ingressブロックが2つ生成される ─ ポート8443は除外) # aws_security_group.web will be created + resource "aws_security_group" "web" { + ingress { + from_port = 80 + to_port = 80 ... } + ingress { + from_port = 443 + to_port = 443 ... } } # TF_VAR_envにprdを渡してplan(ポート8443のブロックが追加される) $ TF_VAR_env=prd terraform plan + resource "aws_security_group" "web" { + ingress { from_port = 80 ... } + ingress { from_port = 443 ... } + ingress { from_port = 8443 ... } }
トラブルシュート:dynamic blockのよくあるエラー
4. 「The for_each value depends on resource attributes...」エラー
Error: Invalid for_each argument on main.tf line 15, in dynamic "ingress": 15: for_each = aws_instance.app.*.network_interface The "for_each" value depends on resource attributes that cannot be determined until apply, so Terraform cannot determine the full set of keys that will identify the instances of this resource.
対処:for_eachにはplanで確定できる静的な値(variable・locals・data source)を渡す。どうしても動的な値が必要な場合は`-target`で依存リソースを先にapplyしてから対象リソースをapplyする。
5. content{}内でブロック名の参照が動かない
Error: Reference to undeclared resource on main.tf line 18, in resource "aws_security_group" "web": 18: from_port = ingress.value.from_port A managed resource "ingress" "value" has not been declared.
対処:`iterator = rule`と指定した場合は`rule.value.from_port`で参照する。iteratorを指定しない場合はブロック名`ingress.value.from_port`で参照する。どちらかに統一する。
6. for式のフィルタ後が空になり期待通りにならない
# フィルタ条件のミスで空リストになる例 dynamic "ingress" { for_each = [ for rule in var.ingress_rules : rule if rule.prd_only && var.env == "dev" # dev環境ではprd_only=trueが全除外 ] ... } # 結果: ingress{}ブロックが1つも生成されない
対処:for式のフィルタ条件を見直す。開発環境では最低限のルール(HTTP 80番等)が必ず残るように条件を設計する。terraform planで生成されるブロック数を必ず確認してからapplyすること。
本記事のまとめ
Terraformのdynamic blockについてまとめます。| ポイント | 内容 |
|---|---|
| dynamic blockの用途 | resource{}内のネストブロック(ingress・ebs_block_device等)をリストやマップから動的に生成する |
| 基本構文 | dynamic "ブロック名" { for_each = リスト; content { ブロック名.value.キー } } |
| iterator引数 | content{}内の参照名を変更できる。長いブロック名を短縮して可読性を上げる時に使う |
| for式との組み合わせ | for_eachにfor式の結果を渡し、条件フィルタリングや変換後のリストを動的生成の元データにできる |
| 空リストの扱い | for_eachが空の場合、dynamic blockはブロックを0個生成する(エラーではない) |
| for_eachの制限 | planで値が確定しないリソース属性は渡せない。variable・locals・data sourceを使う |
>> Terraform実践セミナーの詳細はこちら
3,100名以上が実践した「型」を無料で公開中
プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。
登録10秒/合わなければ解除3秒 / 詳細はこちら

無料メルマガで学習を続ける
Linuxの実践スキルをメールで毎週お届け。
登録は1分、解除もいつでも可。
登録無料・いつでも解除できます