Skip to main content

Share Terraform Code Across Components

Components keep needing the same Terraform code: the same variable blocks in every variables.tf, the same provider block in every root module. This tutorial shows how to write that code once and share it with every component, using the built-in providers section for provider configuration and three options for everything else.

You will learn

  • Share Terraform code at a smaller level than a module: a variable declaration, a provider block, a context.tf
  • Generate provider configuration from stack configuration with the providers section, with no HCL to copy
  • For other shared code, use Atmos Scaffold, generate blocks, or vendored Terraform mixins, depending on how your team works
  • Keep shared code in its own *.mixin.tf files so it never collides with component-specific code

The Problem​

Say you define three components and want to pass cost_center to each of them. Every component needs its own cost_center variable declaration in its variables.tf. The same happens with other common variables, such as a secret_arn that is declared with identical code in several components, and with provider definitions, where the same provider block is repeated in every root module.

Terraform modules do not solve this. A module is too coarse. What you need is code reuse at the level of an HCL snippet, so that common variable declarations and provider definitions are written once.

By the end of this tutorial, you will have:

  • one shared cost_center variable declaration
  • one provider configuration, defined in the stack and generated for every component
  • every component receiving them without hand-edited copies
  • a way to change the shared code once and roll it out to all components

Before You Start​

Two pairs of terms are easy to confuse. Knowing the difference makes the rest of the tutorial straightforward.

Setting a variable versus declaring a variable

Setting a variable gives it a value in stack configuration under vars. You can set it once at a shared level and every component instance inherits it. Declaring a variable means writing the variable block in the component's Terraform code. Terraform needs a declaration in every component that uses the variable, and setting a value in a stack does not replace that step.

Stack mixin versus Terraform mixin

A stack mixin is a YAML file that you import to share configuration values across stacks. A Terraform mixin is a drop-in .tf file that shares code, such as variable declarations and provider blocks. This tutorial is about Terraform mixins and other ways to share code.

In short, stack mixins and catalogs share values. Terraform mixins, scaffold templates, and generate blocks share code.

Organize the Shared Files​

With any of the approaches below, the shared code lives in its own files, named by a convention such as the .mixin.tf suffix that Cloud Posse uses. Component-specific code stays in variables.tf and your other files.

components/terraform/my-component/
├── main.tf # Component-specific
├── variables.tf # Component-specific variables only
├── cost_center.mixin.tf # Shared: the cost_center variable
└── versions.mixin.tf # Shared: required Terraform and provider versions

Terraform loads every .tf file in a directory, so separate files behave exactly like one file. The only rule is that a variable must be declared in one place, so the shared file and variables.tf must not both declare it.

Here are the two shared files used in the rest of this tutorial. A good first test is the shared cost_center variable plus one more shared file.

cost_center.mixin.tf
variable "cost_center" {
type = string
description = "Cost center, set in the stack configuration."
}
versions.mixin.tf
terraform {
required_version = ">= 1.5.0"

required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
}

If a component already has a terraform block with required_providers, remove it from the component and keep the shared file. Terraform rejects the same provider requirement declared twice.

Providers Are a Special Case​

Provider blocks are the one piece of shared code that Atmos can generate for you without any HCL to copy. The providers section in stack configuration describes provider settings, and Atmos writes them to a providers_override.tf.json file in the component directory every time you run an atmos terraform command. Terraform treats an override file as configuration for the provider, so the component does not need a provider block of its own.

Define the provider once in a catalog default that your stacks import. The terraform.providers section applies to every Terraform component in scope:

stacks/catalog/_defaults.yaml
terraform:
providers:
aws:
region: "{{ .vars.region }}"
default_tags:
tags:
CostCenter: "{{ .vars.cost_center }}"
ManagedBy: Atmos

The values are Go templates rendered from the component's vars, so the region and tags come from the stack. Make sure templating is enabled in atmos.yaml, or Atmos writes the {{ ... }} text into the file unrendered:

atmos.yaml
templates:
settings:
enabled: true

Atmos writes the rendered values into the generated file as literals. That also removes the need to declare a region variable in the component just to feed the provider.

components/terraform/my-component/providers_override.tf.json
{
"provider": {
"aws": {
"region": "us-east-2",
"default_tags": {
"tags": {
"CostCenter": "platform",
"ManagedBy": "Atmos"
}
}
}
}
}

A component can override any part of the provider settings with its own providers section. The two levels are deep-merged, with the component winning. Use dotted keys such as aws.uswest2 to configure aliased providers for multi-region components.

Add providers_override.tf.json to .gitignore, because Atmos regenerates it.

The providers section configures providers. It does not declare which providers a component requires, and it does not cover logic that needs Terraform itself, such as a provider block that reads a data source. Those pieces are still shared code, which is what the rest of this tutorial is for. Cloud Posse's shared provider mixins, described in the vendoring tab of Step 1, are an example of the second kind.

Choose an Approach​

Scaffoldgenerate blocksVendoring and Terraform mixins
What it doesCreates files from a shared, versioned templateWrites shared files into each component directory from stack configurationPulls shared files from a remote source into your repo
When it runsWhen you generate, or later update, the filesBefore Terraform runs, every time, if auto_generate_files is enabledWhen you run atmos vendor pull
Where the shared content livesA local directory, a Git repository, or an OCI repositoryInline in a stack manifest, or in a file pulled in with !include.rawA Git, OCI, HTTP, or S3 source, pinned to a version
Result in each componentA real file you can edit and commitA generated file, usually gitignoredA real file, committed to the repo

Pick Scaffold when you want versioned templates that can also create other things (whole components, stack configuration) and you want later template changes to merge into files that already exist. Pick generate blocks when you want the files to be impossible to drift and you want content that can vary by stack. Pick vendoring when you want plain committed files pulled from a repo of shared code, pinned to a version.

The steps below use tabs. Choose your approach in any tab group and every step on this page switches to match.

Step 1: Publish the Shared Files​

First put the shared files, cost_center.mixin.tf and versions.mixin.tf, somewhere your whole team can reach them.

Atmos Scaffold is like cookiecutter on steroids. It can copy, update, and template any file, and it can pull templates from a local directory, a Git repository, or an OCI registry. Atmos does not care what the files are, so a template can hold nothing but your shared .tf files.

A scaffold template is a directory with a scaffold.yaml manifest and the files to generate. Put the two shared files next to the manifest:

terraform-boilerplate/
├── scaffold.yaml
├── cost_center.mixin.tf
└── versions.mixin.tf
terraform-boilerplate/scaffold.yaml
apiVersion: atmos/v1
kind: AtmosScaffoldConfig
metadata:
name: terraform-boilerplate
description: Shared variable declarations and version constraints
version: 1.0.0
spec:
fields: []

Files without a .tmpl suffix or an atmos:template marker are copied as written, so the HCL passes through untouched. Check the manifest with atmos scaffold validate:

atmos scaffold validate ./terraform-boilerplate

To share the template with your team, publish the directory to a Git repository or to an OCI registry you already use. The version is part of the reference, so every team gets the same files and moves to a newer version on purpose.

Step 2: Get the Files into a Component​

Point atmos scaffold generate at the template and the component directory:

atmos scaffold generate ./terraform-boilerplate ./components/terraform/my-component --defaults --force

The component directory already contains files, so the initial generation requires --force. Files with matching paths can be overwritten; review the template's filenames before running it. Unrelated component files remain in place. For subsequent template changes, use --update to merge changes as shown in Step 5.

Atmos writes cost_center.mixin.tf and versions.mixin.tf into the component. It does not copy scaffold.yaml. It also records how the files were produced under .atmos/, which is what makes later updates possible. Commit that directory together with the generated files.

For initial generation from a published template, use either source format:

atmos scaffold generate oci://ghcr.io/example/terraform-boilerplate:v1.0.0 ./components/terraform/my-component --force
atmos scaffold generate https://github.com/example/terraform-boilerplate.git ./components/terraform/my-component --ref v1.0.0 --force

Step 3: Set the Value in a Stack​

The shared file only declares cost_center. The value still comes from stack configuration. Set it once at a shared level and every component instance inherits it:

stacks/dev.yaml
vars:
cost_center: platform
region: us-east-2

The region value supplies the provider configuration shown above.

Step 4: Verify​

Run any component. Terraform sees cost_center declared in cost_center.mixin.tf, and the value comes from the stack:

atmos terraform plan app --stack dev

If the plan fails with a duplicate declaration, the component's own variables.tf still declares cost_center. Delete that declaration and keep the shared file.

Step 5: Change the Shared Code Later​

Most scaffolding tools generate once and stop. Scaffold can re-apply later template changes to files that were already generated, using a three-way merge:

atmos scaffold generate oci://ghcr.io/example/terraform-boilerplate:v1.1.0 ./components/terraform/my-component --update

The default manual merge strategy surfaces any conflict that remains. --max-changes (default 50) stops an update that would touch too many lines.

Trade-offs​

  • Pro: one versioned source of truth, distributed through a registry you already run. The result is ordinary files that appear in code review and work with plain terraform outside Atmos.
  • Pro: templates can ask for validated inputs, include files conditionally, and run hooks such as terraform fmt after generation.
  • Con: updates can conflict when someone edits a scaffolded file by hand. Keeping shared code in separate *.mixin.tf files and component code elsewhere avoids most conflicts.
  • Con: someone has to author, version, and publish the template, and updates only happen when someone runs --update. It is not continuous sync.

See also: atmos scaffold, atmos scaffold list, and the examples/scaffolding and examples/scaffolds examples.

Troubleshooting​

Duplicate variable declaration

The shared file and variables.tf both declare the same variable. Delete the declaration from variables.tf and keep the shared file.

Reference to undeclared input variable for var.cost_center

The shared file is missing from the component directory. With generate blocks, run atmos terraform generate files or enable auto_generate_files. With Scaffold or vendoring, run the generate or pull command again and make sure the target path is the component directory.

The value is set but Terraform warns about an undeclared variable

Setting a variable in a stack does not declare it. Add the declaration through one of the shared files.

scaffold --update reports conflicts

Someone edited a scaffolded file by hand. Move component-specific changes out of the *.mixin.tf files, or choose a merge strategy explicitly.

Next Steps​

  • Move the rest of your repeated declarations into shared files: secret_arn, tags, and context.tf
  • Move repeated provider blocks into the stack providers section and delete the hand-edited copies from each component
  • Read about generate, Scaffold, and Vendoring in more depth