Composition and Provider Passing

Scenario: A child module hardcodes its own provider credentials directly inside its code, and as a result it can never be reused against a different account, tested in isolation, or safely shared with another team.

You learned in Module 3 that provider configuration (credentials, region, endpoint) typically belongs in the root module. Modules make this concrete: a well-designed child module should not configure its own provider at all - instead it should declare, via required_providers, which provider(s) it needs, and let the calling root (or an intermediate module) hand down an actual configured provider through the module block's providers argument. When a child needs more than one configuration of the same provider (say, primary and disaster-recovery), it declares configuration_aliases for the ones it expects, and the caller maps real aliases onto those names explicitly - the same aliasing concept from Module 3, now crossing a module boundary.

On overall shape: prefer a relatively flat composition, where the root module directly wires together a handful of focused, independent child modules through their inputs and outputs, over deeply nesting modules inside modules inside modules. Deep nesting doesn't make anything more "organized" in practice - it hides real dependencies several layers down, and makes it much harder to look at a plan and understand just how large a change's actual blast radius is.

Analogy: The root module is an orchestra conductor. Individual sections (the child modules - strings, brass, percussion) stay independently rehearsable and reusable in other performances; the conductor's job is specifically to select which performers play in this particular concert and coordinate how their outputs combine - not to reach into the cello section and personally restring an instrument.

A worked configuration

module "certificate" {
  source = "./modules/certificate"
  providers = { fakecloud = fakecloud.primary }
  domain     = module.service.domain
}

Resist the convenience of passing an entire resource object between modules just because it's easy to type - publish deliberately minimal, stable outputs (an ID, an address, a domain name) instead of the whole object. This keeps your modules loosely coupled to the specific provider schema underneath, meaning an internal refactor inside one module (say, upgrading how it builds a resource) doesn't force every module that consumes its outputs to change in lockstep.

Common mistakes in production

Goal: Treat the plan as a change contract: understand every create, update, replacement, and destroy before approving it. Reinforce this lesson in the tf-certificate-renewal Terraform lab. Open /labs/terraform and choose slug tf-certificate-renewal; the lab runs real Terraform against the offline FakeCloud provider.