Hedronite · Ops Lesson · 01-Earth-DevOps / Terraform · Sun 2026-09-20

Terraform AWS security group separate rules — versus network ACL

Stateful filter on the ENI path. Separate rule resources so one port change is one plan row.

Lesson Class: Ops (DevOps + Terraform + AWS Security Groups)
Cloud Referent: aws_security_group + aws_vpc_security_group_ingress_rule / egress_rule
Paired Dev: HCL dynamic blocks + for_each ingress map
Paired Cert: Associate dynamic blocks, for_each, count, splat
Grounding: Lab 19 · Lab 16 referenced · Brikman referenced · AWS provider docs
Parent SG
Identity and attachment. Not the whole rule list as one attribute.
Separate rule
One 5-tuple, one Terraform address, one plan row.
NACL
Stateless subnet ACL. Different shelf from SG.
Declare the parent. Address each rule. Keep NACL on its own shelf.

<!-- hal:authoritative:yaml -->

Stateful filter on the ENI path. Separate rule resources so one port change is one plan row. A NACL is a different shelf.

§I — Frame

09-17 wrote passwords through Azure write-only arguments. 09-14 imported a GCS bucket and moved its address. 09-11 injected S3 backend fields at init. Those fires stay filed.

Today the cloud referent is AWS networking filters. The concrete objects are aws_security_group plus separate aws_vpc_security_group_ingress_rule and aws_vpc_security_group_egress_rule resources. Inline ingress / egress blocks still work. They also force Terraform to treat the whole rule list as one attribute, which turns a single port edit into a noisy diff. Separate rule resources make each 5-tuple its own address in state.

Yesterday's Python Azure NSG census stays on its shelf. Do not redo association enumeration here. Today is declare and plan on AWS.

§II — Three shelves

ShelfWhat it isWhat it is not
Security groupStateful allow/deny evaluated with connection tracking; attaches to ENIsA subnet-stateless ordered ACL
Separate rule resourceOne ingress or egress 5-tuple with its own Terraform addressAn inline list attribute on the parent SG
Network ACLStateless ordered rules on a subnetA synonym for security group

AWS docs and the provider registry both still ship inline blocks. Prefer separate rule resources for anything you expect to change one port at a time in CI.

§III — Mechanism

resource "aws_security_group" "app" {
  name        = "app-sg"
  description = "app ENI filter"
  vpc_id      = aws_vpc.main.id
  tags        = { Name = "app-sg" }
}

resource "aws_vpc_security_group_ingress_rule" "https" {
  security_group_id = aws_security_group.app.id
  cidr_ipv4         = "10.0.0.0/8"
  from_port         = 443
  to_port           = 443
  ip_protocol       = "tcp"
  description       = "https from corp"
}

resource "aws_vpc_security_group_egress_rule" "all" {
  security_group_id = aws_security_group.app.id
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "-1"
}

Fact one. The parent SG owns identity and attachment points. Rules own the 5-tuple.

Fact two. Adding aws_vpc_security_group_ingress_rule.ssh creates one new resource in plan. Editing an inline ingress list often rewrites the whole set.

Fact three. A NACL rule (aws_network_acl_rule) is numbered, stateless, and subnet-scoped. Do not answer an SG question with a NACL rule number, and do not expect SG statefulness from a NACL.

Lab 19 trains the identity pain when count indexes shift under you. That pain is why tonight's Dev lesson expands ingress from a map with for_each / dynamic, and why Ops prefers separate rule addresses over a brittle list.

§IV — Ops drill

  1. 1. Declare VPC + SG parent (no inline ingress).
  2. 2. Add two ingress rule resources (443 corp CIDR, 22 bastion CIDR) and one egress.
  3. 3. terraform plan: confirm three rule addresses, not one SG attribute churn.
  4. 4. Change only the SSH CIDR. Plan should touch one rule resource.
  5. 5. Optional contrast: one aws_network_acl_rule on a public subnet with rule_no 100. Read both state addresses. Leave NACL deny-all defaults alone unless the ticket says otherwise.

Success: plan rows name rule resources; a single CIDR edit does not replace unrelated ports.

§V — Close instruction

Apply the drill in a throwaway VPC. Paste the plan summary that shows separate rule addresses. Pair: Dev expands an ingress map with dynamic / for_each; Cert names dynamic blocks, for_each vs count, and splat on the Associate objectives.