Terraformのdynamic blockでHCL設定を動的に生成する方法|繰り返し設定の簡潔化と実務パターン

宮崎智広 この記事の監修:宮崎智広(Linux実務・教育歴20年以上・受講者3,100名超)
HOMELinux技術 リナックスマスター.JP(Linuxマスター.JP)Terraform > Terraformのdynamic blockでHCL設定を動的に生成する方法|繰り返し設定の簡潔化と実務パターン
「セキュリティグループのingressルールをTerraformで管理したいが、許可ポートの数だけブロックを静的にコピーしていたら100行を超えてしまった」
「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行で書ける


「このままじゃマズい」と感じていませんか?
参考書を開く気力もない、同年代に取り残される不安——
でも安心してください。プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
図解60P/登録10秒/解除も3秒 / 詳細はこちら

なぜ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"] } }

ポートが3つならまだ管理できますが、10個・20個になると重複だらけのコードになります。環境(dev/stg/prd)でポートのリストが変わる場合、コピーしたファイルを別々に編集することになり、変更漏れや設定の乖離が起きます。

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"] }, ] }

次に、このリストをdynamic blockで展開します。

# 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"] } }

ingress{}ブロックが変数のリストから自動生成されます。許可ポートを追加するときは変数リストに1行追加するだけで、resource{}本体に手を入れる必要はありません。

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 } } }

「ingress.value」が「rule.value」になりました。iteratorを指定した場合、content{}内では必ず指定したiterator名を使います(元のブロック名は使えなくなります)。

マップをfor_eachに渡す場合は「iterator名.key」と「iterator名.value」の両方が使えます。リストの場合は「iterator名.key」がインデックス番号(0, 1, 2...)、「iterator名.value」がリストの各要素になります。iteratorを指定した後で元のブロック名を使ってしまうエラーは実務でよく見るミスです。注意してください。
現場で通用する安全なLinuxサーバー構築の「型」を体系的に身につけたい方へ、Terraformのdynamic block設計からモジュール化・CI/CDパイプライン統合まで、実務で使えるIaCスキルをハンズオン形式で習得できるセミナーを開催しています。
>> 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 } } }

開発環境ではebs_volumesを空リスト`[]`にすれば追加ボリュームなし、本番環境ではリストに複数要素を定義、というtfvarsによる制御が可能です。for_eachの対象が空リストの場合、dynamic blockはブロックを1つも生成しません。これはエラーではなく、意図した動作です。

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 }

権限のセットをリスト変数で一元管理することで、環境ごとのtfvarsで許可リソースのARNを差し替えるだけで済みます。開発環境では厳しいリソース制限、本番環境では本番バケットのARNに変更、といった使い分けが変数の差し替えのみで実現できます。

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"] } }

for式の`if !rule.prd_only || var.env == "prd"`が、prd_only=false(全環境共通)のルールと、prd_only=trueかつ本番環境のルールだけを通過させるフィルタです。

開発環境で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 ... } }

本番環境ではprd_only=trueのルールも含め3つのingressブロックが生成されることをplanで確認できます。

トラブルシュート: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に渡す値が、terraform planの時点で確定していないリソース属性を含んでいる。applyで初めて決まる値(作成するリソースのIDやARN)はfor_eachに使えない。
対処: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)を指定した場合、content{}内では必ずiterator名を使う必要がある。元のブロック名「ingress」での参照はエラーになる。
対処:`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式の結果が空リストになると、dynamic blockはブロックを0個生成する(エラーにはならない)。セキュリティグループの場合、ingressブロックがゼロでも作成されるが、実質的に全ポート遮断になる。
対処: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を使う
dynamic blockとHCL変数設計(variable・locals・output・data source)を組み合わせることで、環境差異による設定変更を変数リストの操作だけで吸収できます。またmoduleとfor_each・countと組み合わせれば、リソース単位の繰り返しとブロック単位の繰り返しの両方をカバーできる、再利用性の高いIaC設計が実現します。
Terraformのdynamic block設計を含む、HCL設計・tfstate管理・GitHub ActionsによるCI/CDまで、現場のIaC実務を体系的に学べるセミナーを開催しています。Linuxサーバー構築から始まる実践ハンズオン形式で、すぐに使えるスキルを習得できます。
>> Terraform実践セミナーの詳細はこちら

無料メルマガで学習を続ける

Linuxの実践スキルをメールで毎週お届け。
登録は1分、解除もいつでも可。

登録無料・いつでも解除できます

暗記不要・1時間後にはサーバーが動く

3,100名以上が実践した「型」を無料で公開中

プロのエンジニアはコマンドを暗記していません。
「現場で使える型」を効率よく使いこなしているだけです。
その「型」を図解60Pにまとめた入門マニュアルを、完全無料でプレゼントしています。

登録10秒/合わなければ解除3秒 / 詳細はこちら

Linux無料マニュアル(図解60P) 名前とメールで30秒登録
宮崎 智広

この記事を書いた人

宮崎 智広(みやざき ともひろ)

株式会社イーネットマーキュリー代表。現役のLinuxサーバー管理者として20年以上の実務経験を持ち、これまでに累計3,100名以上のエンジニアを指導してきたLinux教育のプロフェッショナル。「現場で本当に使える技術」を体系的に伝えることをモットーに、実践型のLinuxセミナーの開催や無料マニュアルの配布を通じてLinux人材の育成に取り組んでいる。

趣味は、キャンプにカメラ、トラウト釣り。好きな食べ物は、ラーメンにお酒。休肝日が作れない、酒量を減らせないのが悩み。最近、ドラマ「フライトエンジェル」を観て涙腺が崩壊しました。