How do I make it so that my Terraform code can deploy to multiple environments without copy-pasting or using a complex folder structure like Terragrunt—and how can we address the small differences between each environment?
A little while back I had some ideas about doing this in a super-generic way, with the additional requirements of:
-
Only having one set of AWS keys -
Using GitLab CI/CD -
Keeping the CI/CD YAML as simple as possible
Okay, that’s a lot—but the solution is surprisingly simple.
The idea is to link Git branches to AWS accounts and split up your state files with Terraform workspaces. Sounds good—so how do we handle authentication?
variable "workspace_iam_roles" {
default = {
default = "arn:aws:iam::111111111111:role/terraform-sa-role"
}
}
provider "aws" {
region = var.region
assume_role {
role_arn = var.workspace_iam_roles[terraform.workspace]
}
}
In providers.tf this defines the workspace-to-account relationship. Here we only have one entry, default, which is linked to account 111111111111 via an assumed role called terraform-sa-role. We use the built-in terraform.workspace variable to pick the right role for each environment.
Adding More Environments
variable "workspace_iam_roles" {
default = {
default = "arn:aws:iam::111111111111:role/terraform-sa-role"
dev = "arn:aws:iam::222222222222:role/terraform-sa-role"
preprod = "arn:aws:iam::333333333333:role/terraform-sa-role"
staging = "arn:aws:iam::333333333333:role/terraform-sa-role"
}
}
By switching our Terraform workspace we tell Terraform which role—and therefore which account—to use. In this example preprod and staging share the same account.
Auto-Selecting Workspace in GitLab CI
Switching workspaces manually is a pain—so let’s couple them to branches. When you’re on preprod, CI will automatically select the preprod workspace (and its role).
before_script:
- terraform init
- |
# Map master → default, otherwise use branch name
if [ "$CI_COMMIT_REF_SLUG" = "master" ]; then
WORKSPACE="default"
else
WORKSPACE="$CI_COMMIT_REF_SLUG"
fi
terraform workspace new $WORKSPACE || true
terraform workspace select $WORKSPACE
Now CI_COMMIT_REF_SLUG drives both your workspace and AWS account.
Handling Environment-Specific Differences
Use *.tfvars files:
vars/
default.tfvars
dev.tfvars
preprod.tfvars
production.tfvars
Then in your plan job:
tf-plan:
stage: tf-plan
script:
- terraform plan \
-var-file="vars/${WORKSPACE}.tfvars" \
-out=plan.tfplan
artifacts:
paths:
- plan.tfplan
Each file sets the small differences—like desired_count in an Auto Scaling Group—so production can use 40 hosts, preprod only 1, and so on.
CI Authentication Flow
- CI AWS Keys (stored in GitLab CI variables) can assume any role in
workspace_iam_rolesand read/write the remote state in S3. terraform inituses those keys to initialize the backend (S3).- During
apply, the AWS provider block assumes the per-workspace IAM role to make changes in the target account.
Local Workflow with Atmos
For the same convenience locally, check out Atmos (https://github.com/Spengreb/atmos) small Python wrapper that:
- Manages AWS credentials and role assumption
- Mirrors the CI branch→workspace mapping
- Lets you run Terraform locally without extra flags