HCL dynamic blocks and for_each — the ingress map that expands
Expand nested blocks from a map. Key instances by name, not by position.
<!-- hal:authoritative:yaml -->
Expand nested blocks from a map. Key instances by name, not by position.
§I — Frame
09-11 taught the backend block as a special HCL surface: no expressions inside, partial attributes merged at init. Leave it.
Today tf_day_dev_counter is 18. Eighteen mod 3 is 0. HCL depth. Ops wants an ingress map that becomes either nested blocks or separate rule resources without reshuffling state when one port disappears.
§II — Two expansion tools
Tool one. for_each on a resource. Creates one instance per map key (or set element). The instance address includes the key: aws_vpc_security_group_ingress_rule.rules["https"]. Removing "ssh" destroys only that instance.
Tool two. dynamic blocks. Expands nested blocks inside one resource (classic inline ingress on aws_security_group, or module blocks that expect repeated nested shapes). The iterator is for_each inside dynamic "ingress".
variable "ingress_rules" {
type = map(object({
from_port = number
to_port = number
protocol = string
cidr_ipv4 = string
description = string
}))
}
resource "aws_vpc_security_group_ingress_rule" "rules" {
for_each = var.ingress_rules
security_group_id = aws_security_group.app.id
cidr_ipv4 = each.value.cidr_ipv4
from_port = each.value.from_port
to_port = each.value.to_port
ip_protocol = each.value.protocol
description = each.value.description
}
When you must keep inline blocks (older module contract), the same map feeds dynamic:
resource "aws_security_group" "app" {
name = "app-sg"
vpc_id = aws_vpc.main.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_ipv4]
description = ingress.value.description
}
}
}
Prefer the separate-rule for_each form when you control the module. Use dynamic when the schema forces nested blocks.
§III — Why count loses
Lab 19 exists because count = length(list) keys instances by index. Delete index 0 and every later instance moves. for_each keys by map key. Delete "ssh" and "https" keeps its address.
Rule one. Stable string keys beat positional indexes for sets that shrink.
Rule two. each.key / each.value are the only iterators inside a for_each resource. Inside dynamic, the label name (ingress) is the iterator.
Rule three. count and for_each cannot share one resource block. Pick one.
§IV — Close instruction
Type both shapes against a three-entry ingress map (https, ssh, health). Remove ssh. Confirm separate-rule plan destroys one address. Then break a count-based version on purpose and watch indexes slide. Pair: Ops owns AWS SG vs NACL shelves; Cert names Associate stems for dynamic, for_each, count, and splat.