Drop these files into the new repo. The role and bucket from the AWS bootstrap supply everything the backend block needs.The four outputs from the bootstrap (
state_bucket, tf_role_arn, aws_region, state_key_prefix) feed straight into this page. Once the files below are in place, the repo’s first terraform plan runs in under a minute — locally and in CI — with no static credentials anywhere.
Repo file layout
main.tf is whatever the repo actually manages — VPC, Bedrock agents, Proxmox guests, anything. The three files below set up the surrounding plumbing.
backend.tf
use_lockfile = true enables S3-native locking via conditional writes (Terraform ≥ 1.10, OpenTofu ≥ 1.10). No DynamoDB table, no separate lock service. encrypt = true instructs the client to send the SSE header on every PutObject; the bucket’s default SSE-S3 encryption already configured by the bootstrap handles the actual cipher.
There is no assume_role block here. The aws-vault profile (locally) and aws-actions/configure-aws-credentials@v4 (in CI) perform the AssumeRole before Terraform runs, exporting the role’s STS credentials into the subprocess environment. Terraform consumes those credentials directly.
providers.tf
default_tags at the provider level keeps individual resource declarations clean and prevents per-resource tag drift.
Local development
Add two profiles to `~/.aws/config`
The base profile holds your IAM user identity. The per-project profile chains off it and assumes the role:
mfa-base is added once per operator (same block in every repo’s docs). The tf-<project> block is repo-specific — copy it into ~/.aws/config the first time you clone the repo.Store the IAM user's access key in aws-vault
One time per operator. aws-vault writes the access key into its dedicated macOS keychain — nothing lands in
~/.aws/credentials:Run Terraform through the per-project profile
aws-vault prompts for your MFA token on the first invocation per cached session (Subsequent commands inside the
session_ttl = 1h), assumes the per-project role via the chained source_profile, and exports the role’s STS credentials into the subprocess:session_ttl window do not re-prompt for MFA.Need more depth on profile mechanics?
See the aws-vault page for keychain backends,
session_ttl tuning, the canonical aws-vault exec ... -- doppler run -- terragrunt plan envelope, and the anti-patterns the tool exists to prevent.CI/CD — GitHub Actions with OIDC
.github/workflows/terraform.yml:
environment: prod line ties the apply job to a GitHub Environment with required reviewers — apply pauses until a maintainer approves. Configure reviewers in the repo’s Settings → Environments → prod.
The same tf-<project> role used locally is the role CI assumes. The OIDC trust statements in the bootstrap (refs/heads/<branch_pattern> for push, pull_request for PR runs) gate which workflow events are allowed.
For the self-hosted RunsOn-on-AWS-spot runner alternative, the workflow shape is identical —
runs-on: ubuntu-latest swaps for a self-hosted runner label, and everything else stays the same. OIDC works the same way on self-hosted runners.Terragrunt variant
Terragrunt repos generate the samebackend.tf shape at runtime instead of hand-writing it. Drop a root.hcl (or terragrunt.hcl at the repo root) with:
terragrunt.hcl files at each environment include the parent and do not repeat backend config:
aws-actions/configure-aws-credentials@v4 replaces aws-vault exec, and doppler run replaces with whatever secret-injection path the workflow uses (Doppler CLI inside the runner, or repo secrets).
Where to go next
OpenTofu on AWS overview
The isolation model and naming conventions this repo implements.
AWS bootstrap
The admin-side module that created the bucket and role this repo points at.
OpenTofu check placement
Static checks in pre-commit, credentialed plan/apply in CI only.
CI/CD policy
The wider CI/CD shape — marketplace actions, runner choice, version pinning.