Real infrastructure is never a single, static set of resources — it scales horizontally (more EC2 instances during IPL match traffic), varies by environment (production has three subnets across three AZs; development has one), and adapts to feature flags (only enable WAF on production, only enable detailed monitoring when a specific variable is set). Terraform's meta-arguments and expression language provide the primitives to express this variability declaratively, without resorting to copy-paste duplication or complex procedural logic. The four constructs that handle variability are: 'count' (creates N instances of a resource), 'for_each' (creates one instance per element in a collection), dynamic blocks (generates repeated nested blocks programmatically), and conditional expressions (selects between values based on a condition). Mastering these four constructs is what separates Terraform configurations that are tightly parameterisable from those that require manual edits for every environment change.
Understanding when to use each construct — and when to avoid it — is as important as understanding the syntax. 'count' is appropriate for resources that are genuinely interchangeable (you want 3 of them and don't care which is which), like availability zones when they are truly equivalent. 'for_each' is appropriate for resources with distinct identities (each has a meaningful key), like S3 buckets named by purpose or security group rules named by description. Dynamic blocks are appropriate when a resource has a list of nested blocks that vary in number and content, like ALB listener rules or IAM policy statements. Conditional expressions handle binary choices based on a condition. Combining these incorrectly — using count where for_each is appropriate, or nesting conditionals in ways that obscure intent — produces configurations that are hard to read, hard to debug, and brittle under changes.