A template repository for a Terraform root module that deploys to several AWS accounts through the Terraform reusable workflow. It follows Best practices for using the Terraform AWS Provider in AWS Prescriptive Guidance.
The template includes:
- An S3 backend with native state locking, completed for each environment by
envs/<environment>/backend.tfbackend. - Version constraints with an upper bound for the AWS Provider, and a committed dependency lock file with checksums for Linux, macOS, and Windows.
allowed_account_idson the AWS Provider, so that a run with credentials for the wrong account stops before it changes anything.- Unit tests that mock the AWS Provider and need no AWS credentials.
- Deploy and destroy workflows that plan in each environment, wait for the environment's approval where GitHub supports required reviewers, and apply the reviewed plan.
- TFLint, Checkov, terraform-docs, and pre-commit hooks.
Do the steps marked (Reusable module) only when you use this template for a reusable module instead of a root module.
- Update CONTRIBUTING.md for your repository.
- Update CODEOWNERS with the users or teams that review pull requests.
- Install the pre-commit hooks.
- Replace the description at the top of this README with what your module deploys.
- In each AWS account, create the state bucket, its AWS KMS key, the GitHub OIDC identity provider, and the plan and apply IAM roles. See Option 1: S3 backend with GitHub OIDC in the reusable workflow README. A repository created from this template after July 15, 2026 uses GitHub's immutable OIDC subject format,
repo:OWNER@OWNER-ID/REPO@REPO-ID, so write the trust policies with that prefix. Get it withgh api repos/OWNER/REPO/actions/oidc/customization/sub --jq .sub_claim_prefix. - Create a GitHub environment for each environment (
dev,test,prod). Forprod, add required reviewers and a deployment branch rule that allows onlymain. Required reviewers need a public repository, or GitHub Enterprise Cloud for a private or internal one. Without them, each environment applies right after its plan. See Requirements in the reusable workflow README. - Create the repository variable
TF_VERSION(for example,1.17.0) and the repository secrets<ENV>_AWS_PLAN_ROLE_ARN,<ENV>_AWS_APPLY_ROLE_ARN, andPLAN_ENCRYPTION_KEY. - In a private or internal repository with GitHub Code Security enabled, create the repository variable
CODE_SCANNING_ENABLEDwith the valuetrue, so that the Checkov workflow uploads its results to code scanning. Public repositories upload them without this variable. In other repositories, the results are only in the workflow log. - In each
envs/<environment>/folder, set your account ID interraform.tfvarsand your state bucket inbackend.tfbackend. - In the Deploy and Destroy workflows, set your environments and Regions, and pin the reusable workflow to the full commit SHA of the release you use.
- (Reusable module) Delete the
envsfolder, the rootterraform.tfvarsfile, thebackend "s3"block, theprovider "aws"block, the Deploy and Destroy workflows, and.terraform.lock.hcl. Reusable modules don't configure providers or backends, and Terraform reads the lock file only from the root module. - (Reusable module) Change the AWS Provider constraint in
versions.tfto a lower bound only, such as>= 6.0, so that callers can choose the version. - (Reusable module) Remove
locals.tfand thetagsvariable if the module doesn't need them. The root module that calls a reusable module usually sets the tags. - (Reusable module) Add an
examplesfolder with a root module that calls the reusable module. - Delete this checklist and start coding.
providers.tf declares a partial S3 backend with the settings that every environment shares:
terraform {
backend "s3" {
encrypt = true
use_lockfile = true
}
}Each environment completes it with envs/<environment>/backend.tfbackend, so that its state stays in a bucket in its own account:
bucket = "amzn-s3-demo-bucket-dev"
key = "aws-terraform-template/terraform.tfstate"
region = "us-east-1"
kms_key_id = "alias/terraform-state"The reusable workflow passes this file to terraform init. For local runs, pass it yourself:
terraform init -backend-config=envs/dev/backend.tfbackendTo switch environments locally, run terraform init -reconfigure with the other environment's file.
-
In
providers.tf, replace thebackend "s3"block with an emptycloudblock, and delete thebackend.tfbackendfiles:terraform { cloud {} }
-
Pass the organization, workspace, and token to the reusable workflow:
with: tf-organization: ${{ vars.TF_ORGANIZATION }} tf-workspace: my-app-dev secrets: tf-token: ${{ secrets.TF_TOKEN }} plan-encryption-key: ${{ secrets.PLAN_ENCRYPTION_KEY }}
For execution modes, dynamic provider credentials, and Terraform Enterprise, see Option 2 in the reusable workflow README.
- Deploy: pull requests plan
devand post the plan as a comment. Onmain, each environment plans, waits for its GitHub environment's approval, and applies that saved plan. Then the next environment starts. An environment without required reviewers applies right after its plan. - Destroy: run it manually. It destroys
prod, thentest, thendev, and each destroy plan waits for its environment's approval. Each destroy also appears in the environment's deployment history as a deployment that is now inactive. See Destroy workflow in the reusable workflow README.
Each environment reads envs/<environment>/terraform.tfvars. The workflow also passes region and environment from its inputs. For sensitive values, use the reusable workflow's secret-tfvars secret with ephemeral variables. See Sensitive variables.
tests/unit.tftest.hcl mocks the AWS Provider with mock_provider, so the tests need no AWS credentials. They check the variable validation and the values that the module computes. Run them locally the same way as the Terraform test PR workflow:
terraform init -backend=false
terraform testIntegration tests create real resources, check them, and destroy them. Put them in tests/integration, configure the provider in them with allowed_account_ids set to a dedicated test account, and run them with credentials for that account:
terraform test -test-directory=tests/integrationFor more information, see Best practices for testing in the guide.
Pin every module that you call, and review a new version before you upgrade to it.
-
Public Terraform Registry: set
sourceto the module address andversionto an exact version. This is the simplest option, and Dependabot can propose upgrades. The trade-off is that the publisher controls which code a version number points to.module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "6.7.3" }
-
Git repository pinned to a commit: set
refto a full commit SHA, and record the release in a comment. The code that runs is always the code that you reviewed. The trade-off is that each upgrade means finding and reviewing a new SHA.module "vpc" { source = "git::https://github.com/terraform-aws-modules/terraform-aws-vpc.git?ref=b3abd6df2ecf052451a361ed55b8f06f8742a795" # v6.7.3 }
-
Private registry: for modules that your organization publishes in HCP Terraform or Terraform Enterprise, use the registry address and an exact version, such as
source = "app.terraform.io/my-organization/vpc/aws". -
Local path: for modules in the same repository, use a relative path, such as
source = "./modules/network".
Don't use a branch name as ref. The code behind a branch changes without review.
To download modules from private GitHub repositories in the workflows, set enable-github-modules: true and pass a token that can read those repositories as the github-token secret. The workflow's own GITHUB_TOKEN can read only this repository, so use a GitHub App installation token or a fine-grained personal access token with read-only contents access. For details, see Private modules on GitHub.
| Name | Version |
|---|---|
| terraform | >= 1.11 |
| aws | ~> 6.0 |
| random | ~> 3.7 |
| Name | Version |
|---|---|
| random | ~> 3.7 |
No modules.
| Name | Type |
|---|---|
| random_pet.main | resource |
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| allowed_account_ids | AWS account IDs that this configuration can deploy to. The AWS Provider stops the run if the credentials belong to any other account. | list(string) |
n/a | yes |
| environment | Name of the environment where the resources are deployed, such as dev or prod | string |
n/a | yes |
| region | AWS Region where the resources are deployed | string |
n/a | yes |
| tags | Tags that the AWS Provider adds to every resource, in addition to Environment | map(string) |
{} |
no |
| Name | Description |
|---|---|
| hello_world | Test output |
| random_pet | Dummy output |
See CONTRIBUTING for more information.
This library is licensed under the MIT-0 License. See the LICENSE file.