Terraform Pro Providers Through Modules — the nest the provider still has to answer
A socket is not a configuration. A nest is not a wish.
<!-- hal:authoritative:yaml -->
§I — Frame
Associate-003 is closed. Three Pro fires on this arc opened run tasks, remote unlock, and test-run commands. 07-28 closed Objective 3 at the root: version constraints, the lockfile, two aliases in one file. Lab 06 is that root. Lab 14 is the leftover.
Lab 14's starter configures provider "aws" twice at the root, reads two regions at the root, and never opens a module. The README is the exam: pass the aliased provider into a child, declare configuration_aliases in the child, keep configuration in the root, prove both regions in outputs.
Coin it: the nest the provider still has to answer. On Lab 14 the nest is a socket. On today's Ops shirt the nest is settings.database_flags. Both are things the child consumes and the child does not invent.
This is not 07-28. That lesson's aliases lived in the root. A candidate who copies alias = "secondary" into the child has configured a plugin. The Pro item is the socket, not the alias keyword.
This is not 08-21. That lesson's command was plan versus apply. Today's command is validate versus a module call that forgot the providers map.
§II — Domain Foundations: root configures, child consumes
Three objects, three owners.
The provider block. Lives in the root. Holds region, project, assume_role, impersonate_service_account, aliases. Each block is a process. Brikman says use aliases sparingly for that reason. The Pro stem that puts a provider block in a child and asks why the root's impersonation did not apply is this object.
**The required_providers table.** Lives in root and child. Names source address and version bound. The child adds configuration_aliases when it needs more than one configuration of the same type. The table is not a configuration. It is a declaration of what the module is willing to consume.
**The providers map on the module call.** Lives only at the call site. Left side is the child's socket. Right side is the root's configured plugin. Omit the map and the child inherits the default plugin of that type, if one exists. Lab 14 exists because the default is the wrong region the moment a second configuration appears.
A fourth object rides along: the lock file. The root writes it. The child never does. 07-28 spent the Associate reading (hashes, terraform providers lock). Today's reading is narrower. The child's version bound intersects the root's bound. An empty intersection fails init. A successful init can still fail validate if the pinned schema dropped a nest the child wrote. Lab 28 drills the operators (~>, >=, !=). Do not spend this fire on operator arithmetic. Name the intersection and move.
Brikman, Creating Modules That Can Work with Multiple Providers, is the leftover chapter heading. The book works the AWS shirt. The rule is shirtless. Configuration in the root. Sockets in the child. The map at the call.
§III — Lab 14 Flavor: the module the starter never opened
The broken file, in full, is eight meaningful lines:
provider "aws" {
region = "us-east-1"
}
provider "aws" {
alias = "secondary"
region = "us-west-2"
}
data "aws_region" "primary" {}
data "aws_region" "secondary" {
provider = aws.secondary
}
output "primary_region" {
value = data.aws_region.primary.name
}
output "secondary_region" {
value = data.aws_region.secondary.name
}
This plan can pass. That is the trap. The lab metadata says success mode is plan. A candidate who makes the starter plan and walks away has not done Lab 14. The README's tasks are the exam: fix source and aliasing, pass default and aliased providers into a child, make the child declare provider requirements, show both regions in outputs.
The child that sits the item:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
configuration_aliases = [aws.primary, aws.secondary]
}
}
}
data "aws_region" "primary" {
provider = aws.primary
}
data "aws_region" "secondary" {
provider = aws.secondary
}
output "primary_region" {
value = data.aws_region.primary.name
}
output "secondary_region" {
value = data.aws_region.secondary.name
}
The call that sits the item:
module "regions" {
source = "./modules/regions"
providers = {
aws.primary = aws
aws.secondary = aws.secondary
}
}
output "primary_region" {
value = module.regions.primary_region
}
output "secondary_region" {
value = module.regions.secondary_region
}
Four fails the stem will offer.
- The child writes
provider "aws" { alias = "secondary"; region = "us-west-2" }. Configuration leaked into the child. The root's alias is now decorative.
- The child declares
configuration_aliases = [aws.secondary]and the call passes onlyaws.secondary. The primary socket is empty. The primary data source talks to a default the child was not given, or validate fails on a missing required provider.
- The call writes
providers = { aws = aws.secondary }. One socket, the default, filled with the west plugin. Both data sources that do not name a provider now read west. The output pair lies.
- The child uses
provider = aws.secondaryon a resource without declaringconfiguration_aliases. Terraform looks for a provider block in the child, does not find one, and errors on a reference that was never a socket.
Lab 06 is the same two root providers without the module. If a stem never mentions a child, it is Lab 06 and 07-28 already owns it. If the stem mentions a child, it is Lab 14.
Lab 14's versions.tf already pins hashicorp/aws to ~> 6.0 and required_version = ">= 1.6, < 2.0". Leave that pin in the root. Copying it into the child is allowed and often correct, because the child is stating the bound it was written against. Copying the provider block into the child is not the same act. The version table is a declaration. The provider block is a process. Candidates collapse them because both live under a terraform or provider heading on the page. The exam separates them on purpose.
Outputs are the proof the README asked for, and they are a second trap. A pair of outputs that print us-east-1 and us-west-2 from root data sources prove the aliases. A pair of outputs that print the same strings from module.regions prove the sockets. The strings are identical. The path is the item. If a stem shows two passing outputs and asks whether the child received both plugins, you do not yet know. You need the call's providers map, or you need a resource in the child that set provider = aws.secondary and still planned.
§IV — Schema Flavor: the nest the exam will not call Cloud SQL
The Pro authoring items have started asking why a dynamic block failed validate. The wrong answers will say "for_each needs a set" and "count cannot be used inside settings." The right answer is: the label on dynamic must be a nested block the provider schema published.
Today's Ops shirt is the specimen.
settings {
dynamic "database_flags" {
for_each = var.flags
content {
name = database_flags.key
value = database_flags.value
}
}
}
Legal. database_flags is a repeatable nest on google_sql_database_instance.settings.
settings {
dynamic "tuning" {
for_each = var.flags
content {
work_mem = tuning.value
}
}
}
Illegal. tuning is not a nest. Validate fails before plan. No GCP credential is required for that failure. The plugin answered as a document.
A stem that uses AWS security-group ingress is Lab 21, the 07-25 syntax drill. A stem that uses an invented nest is today's leftover. Know both. The exam will not print "Cloud SQL." It will print a resource type you have not memorized and a nest that looks plausible. Your test is: did the schema name it?
google versus google-beta is the second schema question. Two source addresses. Two documents. A nest that lives only in the beta document requires the beta plugin on the resource (provider = google-beta) and the beta socket on the module call. Passing only google and hoping is fail 3 from §III wearing a GCP shirt.
§V — Worked Example: one Lab 14 fix, one Cloud SQL stem
Fix. Move the two aws_region data sources into modules/regions. Declare configuration_aliases = [aws.primary, aws.secondary]. Keep both provider blocks in the root. Pass both through providers. Re-export both names. terraform validate in the root must pass. terraform plan must show us-east-1 and us-west-2. The starter already shows those names. The lab is not the names. The lab is the path the names took.
Stem. A module publishes a Cloud SQL instance. The caller wants work_mem = 64MB and writes settings { tuning { work_mem = "64MB" } }. terraform validate fails. The candidate adds dynamic "tuning". Validate still fails. What is the smallest legal change?
Put the flag in the published nest:
database_flags {
name = "work_mem"
value = "64MB"
}
Or stamp that nest from a map, which is the Dev lesson's dynamic "database_flags". Do not invent tuning. Do not move the provider block into the child to "make validate see google." Validate already sees google. Validate is refusing a nest google did not publish.
A second stem, because Pro likes a pair. The same module is called without a providers map. The root has provider "google" in the app project and provider "google" { alias = "host" } in the host project. The instance lands in the app project. The private network lives in the host project. Apply fails in the API on a network the app project's plugin cannot see. The fix is the map, and a child socket for the host plugin if the peering resources live in the same module. Two sockets. One cloud. Not 07-28's two regions of AWS, even though the HCL keyword is the same.
§VI — Connection to Today's Ops + Dev Lessons
Ops: settings is legal because the google plugin said so. connection_name is last apply. ipv4_enabled is the door. authorized_networks is a repeated nest, not a VPC firewall.
Dev: required_providers plus configuration_aliases plus the providers map. dynamic is a label. The label is a schema entry. Implicit inheritance is luck.
Cert: Lab 14 is the exam shirt for the Dev sockets. The Cloud SQL nest is the exam shirt for the Ops schema. One theme, two stems, one leftover the Associate day did not reach.
08-15's speculative plan still cannot apply. Today's validate still cannot invent a nest. The shared discipline is "what is allowed before the cloud hears you."
08-18's force-unlock is a burial. Today's failed validate is a diagnosis. Do not force-unlock a state to make a nest exist.
08-21's plan run still reads last apply. Today's module call still inherits the default plugin if you omit the map. Two different last-defaults. Do not mix them.
§VII — Practice Questions
aws provider blocks and two aws_region data sources at the root. Has the lab been completed? Why?provider = aws.secondary on a data source but does not declare configuration_aliases. What happens?providers map. The root has a default google in the app project and google.shared in the host project. Where does google_sql_database_instance land, and why?terraform validate fails on settings { tuning { work_mem = "64MB" } } inside google_sql_database_instance. The candidate wraps tuning in dynamic. What happens, and what is the legal nest?hashicorp/google to ~> 5.0. The root pins ~> 6.0. What fails, and which file never appears in the child?§VIII — Closing
A socket is not a configuration. A nest is not a wish. Lab 14 is the module the starter never opened. Cloud SQL is the schema the DBA nest did not survive. The child consumes. The plugin answers. The root configures.
Name the object first. Provider block, required_providers table, providers map, lock file. Then name the failure. Then decide whether you are looking at Lab 06, Lab 14, or an invented nest.
Examine well. If the child configured the plugin, you failed Lab 14. If the dynamic label is a word you coined, you failed validate. The nest the provider still has to answer is both items. Write the sockets. Write the published nests. Leave the plugin configuration where Brikman left it, in the root, and let validate refuse every word the schema did not publish. The exam will not grade a pretty starter plan. It will grade the path the region names took and the nest the loaded plugin actually agreed to stamp.
Related
- Prior arc: Pro test runs, plan versus apply (2026-08-21)
- Domain hub: Cross-References/domains/01-Earth-DevOps
- Grounding tome: Terraform: Up and Running (Brikman Ch.7, Creating Modules That Can Work with Multiple Providers) · Lab 14