002 · Domain 1: Scalable Terraform Configuration Design · 20% of the exam
Task 1.1: Translate business requirements into infrastructure code
Turning a requirement into configuration: variables with types and validation for what callers may choose, for_each over a map for each named thing the business asks for, functions that derive the rest, and module calls that hide the detail.
Study it
From requirements to code: typed inputs, maps of named things and module calls
Lesson coming
Sample questions
Try each one before opening the answer. Every option is explained, with the HashiCorp documentation page that proves it.
Question 1 · choose 1
A network team turns this requirement into code: "Each availability zone
gets one /20 carved from the VPC range. Zones are added and retired over
time, and retiring a zone must never change the range of any other zone's
subnet." The VPC range is var.vpc_cidr ("10.40.0.0/16"). Which design meets
the requirement?
Afor_each over a map of zone => subnet number, using cidrsubnet(var.vpc_cidr, 4, each.value)
Bcount = length(var.zones), with cidr_block = cidrsubnet(var.vpc_cidr, 4, count.index)
Cfor_each = toset(var.zones), with cidr_block = cidrsubnet(var.vpc_cidr, 4, index(var.zones, each.key))
DOne cidrsubnets(var.vpc_cidr, 4, 4, 4) call, with each zone's subnet taking its own element
Show the answer and why
Afor_each over a map of zone => subnet number, using cidrsubnet(var.vpc_cidr, 4, each.value)
Correct
Each zone keeps the subnet number written next to it, so removing a map entry destroys only that zone's subnet and leaves every other range and instance key untouched.
Bcount = length(var.zones), with cidr_block = cidrsubnet(var.vpc_cidr, 4, count.index)
Incorrect
Removing a zone from the middle of the list shifts every later index, so later subnets get new instance addresses and new ranges, which forces their replacement.
Cfor_each = toset(var.zones), with cidr_block = cidrsubnet(var.vpc_cidr, 4, index(var.zones, each.key))
Incorrect
The keys are stable, but the subnet number still comes from the zone's position in the list, so retiring an earlier zone renumbers the ranges of the zones after it.
DOne cidrsubnets(var.vpc_cidr, 4, 4, 4) call, with each zone's subnet taking its own element
Incorrect
cidrsubnets numbers its results by position; removing an argument for a retired zone shifts the ranges allocated after it, which the requirement forbids.
A range that must survive changes to its neighbours needs an identity that does not depend on list position. Keying instances by zone and storing an explicit subnet number per zone keeps both the instance address and the calculated CIDR stable when entries are added or removed.
Sizing per environment lives in one map, local.sizes, keyed by environment name. The platform owner's rules are: adding an environment must need only a new map entry, and a run for an environment that has no entry must stop at plan instead of quietly using another environment's size. Which expression for the instance size meets both rules?
DA validation block with contains(["dev", "stage", "prod"], var.environment)
Show the answer and why
Alookup(local.sizes, var.environment, "t3.small")
Incorrect
lookup returns the default when the key is missing, so an unknown environment silently gets t3.small instead of stopping the plan.
Blocal.sizes[var.environment]
Correct
Indexing a map with a key it does not contain is an evaluation error, so the plan stops for an environment without an entry, and new environments need only a new map entry.
try returns the first expression that succeeds, so a missing key falls back to the dev size instead of stopping the plan.
DA validation block with contains(["dev", "stage", "prod"], var.environment)
Incorrect
It stops unknown names, but the hard-coded list must be edited alongside the map, which breaks the rule that a new map entry is enough.
Map indexing fails loudly on a missing key, which is exactly the behavior wanted here. lookup with a default and try with a fallback both turn the same mistake into a silent default.
Security's rule for a firewall module reads: "Every entry in allowed_cidrs must be a valid network prefix, and one bad entry anywhere in the list must be reported with the module's own message." The variable is list(string). Which validation condition implements the rule?
Aanytrue([for c in var.allowed_cidrs : can(cidrhost(c, 0))])
Balltrue([for c in var.allowed_cidrs : can(cidrhost(c, 0))])
It checks that the list has entries and that the first one is valid, so a broken prefix later in the list passes.
A rule about every element of a collection translates into a for expression that tests each element, combined with alltrue. alltrue also returns true for an empty collection, so add a length check if the list must not be empty.
A module creates one IAM user per name that the caller passes. Names are unique and their order does not matter. Which variable type lets the resource use for_each = var.user_names with no conversion?
Alist(string)
Btuple([string])
Cmap(any)
Dset(string)
Show the answer and why
Alist(string)
Incorrect
for_each does not convert lists to sets, so a list needs toset() first.
Btuple([string])
Incorrect
A tuple is an ordered sequence, and for_each accepts only a map or a set of strings.
Cmap(any)
Incorrect
A map would need a key and a value for each user, which the caller does not have.
Dset(string)
Correct
HashiCorp suggests declaring set(string) for a variable that a module uses directly in for_each, so no conversion is needed.
for_each takes a map or a set of strings. When a module's input is a collection of unique names, declaring it as set(string) states that intent and avoids toset() at every use.
The requirement says: "Every application gets a queue in every environment." The module has var.environments and var.apps, both lists of strings. Which function produces one element per required queue?
Asetproduct(var.environments, var.apps)
Bconcat(var.environments, var.apps)
Czipmap(var.environments, var.apps)
Dflatten([var.environments, var.apps]) over both lists
Show the answer and why
Asetproduct(var.environments, var.apps)
Correct
setproduct returns every combination of elements from the given collections, one pair per environment and application.
Bconcat(var.environments, var.apps)
Incorrect
concat joins the two lists end to end; it does not pair their elements.
Czipmap(var.environments, var.apps)
Incorrect
zipmap pairs elements by position into a map, so each environment gets only one application.
Dflatten([var.environments, var.apps]) over both lists
Incorrect
flatten removes nesting and returns one flat list of names, not combinations.
For "every X with every Y" requirements, setproduct gives the Cartesian product; project it into a map with a unique key per pair before using it in for_each.
A network module must place subnets in the first var.az_count availability zones that data.aws_availability_zones.available.names returns. Which expression yields those zone names as a list?