Skip to content

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?

  1. Afor_each over a map of zone => subnet number, using cidrsubnet(var.vpc_cidr, 4, each.value)
  2. Bcount = length(var.zones), with cidr_block = cidrsubnet(var.vpc_cidr, 4, count.index)
  3. Cfor_each = toset(var.zones), with cidr_block = cidrsubnet(var.vpc_cidr, 4, index(var.zones, each.key))
  4. 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.

Question 2 · choose 1

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?

  1. Alookup(local.sizes, var.environment, "t3.small")
  2. Blocal.sizes[var.environment]
  3. Ctry(local.sizes[var.environment], local.sizes["dev"])
  4. 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.

  • Ctry(local.sizes[var.environment], local.sizes["dev"])

    Incorrect

    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.

Question 3 · choose 1

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?

  1. Aanytrue([for c in var.allowed_cidrs : can(cidrhost(c, 0))])
  2. Balltrue([for c in var.allowed_cidrs : can(cidrhost(c, 0))])
  3. Ccan(cidrhost(var.allowed_cidrs, 0))
  4. Dlength(var.allowed_cidrs) > 0 && can(cidrhost(var.allowed_cidrs[0], 0))
Show the answer and why
  • Aanytrue([for c in var.allowed_cidrs : can(cidrhost(c, 0))])

    Incorrect

    anytrue returns true when at least one element is true, so a list with one valid and one broken prefix passes.

  • Balltrue([for c in var.allowed_cidrs : can(cidrhost(c, 0))])

    Correct

    The for expression tests each entry, can turns a cidrhost error into false, and alltrue is true only when every element is true.

  • Ccan(cidrhost(var.allowed_cidrs, 0))

    Incorrect

    cidrhost expects one prefix string, not a list, so the call always errors and can returns false even for a valid list.

  • Dlength(var.allowed_cidrs) > 0 && can(cidrhost(var.allowed_cidrs[0], 0))

    Incorrect

    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.

Question 4 · choose 1

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?

  1. Alist(string)
  2. Btuple([string])
  3. Cmap(any)
  4. 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.

Question 5 · choose 1

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?

  1. Asetproduct(var.environments, var.apps)
  2. Bconcat(var.environments, var.apps)
  3. Czipmap(var.environments, var.apps)
  4. 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.

Question 6 · choose 1

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?

  1. Aelement(data.aws_availability_zones.available.names, var.az_count)
  2. Bchunklist(data.aws_availability_zones.available.names, var.az_count)
  3. Cslice(data.aws_availability_zones.available.names, 0, var.az_count)
  4. Dsubstr(data.aws_availability_zones.available.names, 0, var.az_count)
Show the answer and why
  • Aelement(data.aws_availability_zones.available.names, var.az_count)

    Incorrect

    element returns a single element at one index, not the first N elements.

  • Bchunklist(data.aws_availability_zones.available.names, var.az_count)

    Incorrect

    chunklist regroups every name in the list instead of selecting the first N names.

  • Cslice(data.aws_availability_zones.available.names, 0, var.az_count)

    Correct

    slice takes consecutive elements from the start index up to, but not including, the end index.

  • Dsubstr(data.aws_availability_zones.available.names, 0, var.az_count)

    Incorrect

    substr works on a string, not on a list of zone names.

slice(list, start, end) is the list counterpart of substr; the end index is exclusive, so slice(names, 0, 3) returns three names.

Practice domain 1 →Practice all domains →