Terragrunt allows you to keep your Terraform code DRY. When I tried it, I wondered how I could leverage it in a similar way where we can keep separate configuration files per environment, and during build/plan and deployment/apply time, pass variables/arguments to Terragrunt CLI.
In this post, I will be describing the setup we are using to push infrastructure changes to dev, stage and production environments. With this setup, we have built CI/CD pipeline to auto-deploy changes to lower environments and manual intervention for production push. We have many smaller repositories, separated by their purpose, for example AWS Organization, Account Bootstrap, IAM Role customization per account/env, Security setup, VPC/Networking setup, Application specific resources, Domain/Microservice resources etc.
Environment
- Operating System: macOS Monterey
- Terraenv (Utility to manage multiple version of Terragrunt and Terraform)
- Terragrunt: v0.36.1
- Terraform: v1.1.5
Installation Steps
Install Terraenv
brew tap aaratn/terraenv
brew install terraenv
Install Terraform and Terragrunt
#Install
terraenv terraform install 1.1.5
terraenv terragrunt install 0.36.1#Use installed version
terraenv terraform use 1.1.5
terraenv terragrunt use 0.36.1
Terraform Setup
- AWS Account. Ideally, AWS SSO Setup with your primary Identity Provider (e.g. Azure Active Directory).
- Setup AWS Credentials - AWS STS Token or IAM User Access/Secret Key (
~/.aws/credentials
or as environment variables) - S3 bucket and DynamoDB lock table for Terraform State (for multi-region setup, you would want to have separate bucket and DynamoDB table per region)
Validate AWS Credentials Setup
aws sts get-caller-identity{
"UserId": "...",
"Account": "...",
"Arn": "arn:aws:sts::...:assumed-role/..."
}OR{
"UserId": "...",
"Account": "...",
"Arn": "arn:aws:iam::...:user/username"
}
Directory Structure
Terragrunt Config (terragrunt.hcl)
terragrunt.hcl
is dependent on two environment variables.
config
: Name of Configuration file to useregion
: AWS Region to Deploy (config file is looked up in this directory)
Terragrunt Generated Files
Exclude terragrunt generated files (*-generated.tf
) in .gitignore
_remote-backend-generated.tf
: Contains terraform state config_provider-generated.tf
: Contains terraform provider config_default-data-generated.tf
: Sample data block, to lookup account id, short region code (e.g. us-east-1 => use1)
Note that we are using region
,app
, env
to form a unique path for remote states. {app}/{region}/{env}/terraform.tfstate
Terraform Files
No change to conventional terraform files, main.tf
, outputs.tf
and variables.tf
variables.tf
#Sample variables.tfvariable "region" { type = string }
variable "app" { type = string }
variable "env" { type = string }
variable "tags" { type = map }# Other variables
variable "num_servers" { type = number }
variable "bucket_prefix" { type = string }
outputs.tf
#Sample outputs.tf - shows variables from set config file in outputoutput "example" {
value = {
"tags" : var.tags
"region" : local.region
"num_servers" : var.num_servers
"bucket_name" : "${var.app}-${var.bucket_prefix}-${var.env}-${local.region_short}"
}
}
main.tf
is empty in our example, as we are just showcasing how variables from different config/environment files can be made available with this setup to terraform.
Configuration Files
config/common.yaml
contains configuration common across environments, like Application Name, Tags, Terraform State Bucket, Lock Table and any other config of the resources.
#config/common.yaml#used in terragrunt.hcl
tf_state_bucket: example-state-bucket
tf_state_bucket_region: us-east-1#app specific common config (app is also used in terragrunt.hcl)
app: exampletags:
Organization: Example-Org
config/us-east-1/dev.yaml
Environment specific file, for specific region (in this case, for us-east-1). Values in this config file will be different in one or more environments.
#config/us-east-1/dev.yaml. env is used in terragrunt.hcl as well
env: dev
num_servers: 1
bucket_prefix: mybucket
config/us-east-2/stage.yaml
Stage environment’s config file, for us-east-2 region.
#config/us-east-2/stage.yaml
env: stage
num_servers: 3
bucket_prefix: mybucket
config/us-east-2/prod.yaml
Stage environment’s config file, for us-east-2 region.
#config/us-east-2/prod.yaml
env: prod
num_servers: 6
bucket_prefix: mybucket
Note bucket_prefix
has the same value across all configuration files, so it can be moved to config/common.yaml
, other variable values app
, region_short
and env
are composed to create a unique bucket name for each environment.
Build & Deploy
Now that we have terragrunt setup, which uses environment variable config
to pick corresponding configuration file, and optionally uses region
environment variable, which defaults to us-east-1
.
Build (Terraform Plan) for Dev environment
export config=dev
terragrunt init && terragrunt plan
Build (Terraform Plan) for Stage environment
export config=stage
export region=us-east-2 #stage config is in us-east-2 sub-dir
terragrunt init && terragrunt plan
Deploy (Terraform apply) for Dev environment
export config=dev
terragrunt init && terragrunt apply
Deploy (Terraform apply) for Stage environment
export config=stage
export region=us-east-2 #stage config is in us-east-2 sub-dir
terragrunt init && terragrunt apply
As part of your CI/CD pipeline, you can easily come up with pipeline steps which will need to adjust config
and region
environment variables to get your Infrastructure as Code deployed to selected environment and region. An alternate approach can be terraform workspace, we started with terragrunt originally, and have been happy with our setup so far.
If you liked this story, feel free to follow so that you can be notified of my future posts. Please drop a note with comments/details on how you are making your Infrastructure as Code easy to manage with CI/CD Pipeline, to deploy the same code to multiple environments and multiple regions.
Some of my other stories you may like: