Terraform Professional study materials
Terraform Professional study resources

The 46-Boolean Service Module

Terraform Professional · Put your knowledge to work in context.

All case studies

The 46-Boolean Service Module

The platform team publishes a service registry module (v4.2.0) bundling VPC, ALB, ECS service, and RDS. 22 application teams consume it. Every exception becomes a boolean: create_rds, internal_alb, enable_waf, and 43 more. Two consumers pin version = "~> 4.0", nine use a Git source with ?ref=main, and three vendored a fork. In v4.3.0 an optional argument's default changed and silently switched 11 teams' ALB from internal to internet-facing; one consumer's plan now also shows 3 RDS instances scheduled for replacement. Release notes are the only contract. Leadership wants onboarding to stay cheap - most teams just need the default shape.

1. Diagnose why this module keeps breaking consumers and propose a target structure that keeps onboarding cheap.

Read the worked answer

Two distinct failure modes are being conflated.

1. Boolean explosion. 46 booleans means 2^46 configurations with no tested contract, and every new exception widens the surface. Worse, several of them gate resources via count = var.x ? 1 : 0; toggling one boolean changes indices for sibling resources, so an unrelated change can produce spurious replacement. Nobody, including the authors, can know which combinations work.

2. Default flips are breaking changes. An optional() default is part of the API. Changing internal_alb's default from true to false in a minor version is a breaking change that happened to pass semver review - and ~> 4.0 happily accepts it.

Target shape: split into small modules (network, alb, ecs-service, rds) with object-typed variables and optional() defaults, then keep service as a thin composition module on the blessed path - no booleans. The escape hatch for the team that needs an internal-only ALB is composing network + alb directly, not another flag. Onboarding stays cheap because the composition module is still one call with sane defaults; exceptions stop multiplying inside it.

Treat defaults as API: a default change is a major version, tested in the module's own terraform test suite before publish.

2. A reviewer asks why nine teams on `?ref=main` are a problem and whether `.terraform.lock.hcl` protects them. Answer both, and say what the pinning policy should be.

Read the worked answer

No, the lock file does not help. .terraform.lock.hcl records provider package versions and hashes. It says nothing about module sources - module versions are resolved at init from the source and version/ref arguments. So ?ref=main means every terraform init can pull different code, and two engineers running plan on the same commit can be running different infrastructure code.

?ref=main is worse than a loose constraint because there is no version to report: a plan diff is not reproducible, rollback means editing the source string, and a broken commit is live the moment it merges.

Pinning policy:

  • Registry modules: version = "~> 4.2" - a constraint the registry enforces. Note ~> 4.0 accepts 4.x, which is what let the ALB default change through; for a module with a history of behaviour changes, tighten to the minor.
  • Git sources are only acceptable if pinned to a tag/SHA (?ref=v4.2.0); otherwise move to the private registry.
  • Consume vendored forks back into the registry, or freeze them explicitly and record who owns the divergence - a fork is a permanent maintenance liability, not a version.
  • Upgrades go through a PR that shows the plan diff, not through init -upgrade in a pipeline.

3. What must the module declare about providers, and what goes wrong if it doesn't? Include how a consumer passing a differently-aliased AWS provider is handled.

Read the worked answer

A published module should declare providers and never configure them.

terraform {
  required_version = ">= 1.6.0, < 2.0.0"
  required_providers {
    aws = {
      source                = "hashicorp/aws"
      version               = ">= 5.40.0, < 6.0.0"
      configuration_aliases = [aws, aws.replica]
    }
  }
}

What goes wrong without this:

  • Silent version drift. If the module omits required_providers, it inherits whatever the root resolves; two consumers can run the same module code against different provider majors, and a provider behaviour change looks like a module bug.
  • Wrong-region / wrong-account deploys. Without configuration_aliases, a consumer that needs the module in a second region or a replica account cannot pass providers = { aws = aws.us_east_1 } meaningfully - the module either ignores it or errors.
  • A configured provider block inside the module is the worst version: it makes the module unusable with multiple provider configurations, and consumers cannot override the region. Provider configuration belongs in the root.

The consumer side is then explicit: providers = { aws = aws, aws.replica = aws.dr } at the module call. Add required_version too, so a consumer on 1.4 finds out at init rather than at a syntax error deep in an expression like optional() with defaults.