Motivation
Terraform supports remote state storage via a variety of backends that you configure as follows:
terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "frontend-app/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "my-lock-table"
}
}
Unfortunately, the backend configuration does not support interpolation. This makes it hard to keep your code
DRY if you have multiple Terraform modules. For example,
consider the following folder structure, which uses different Terraform modules to deploy a backend app, frontend app,
MySQL database, and a VPC:
├── backend-app
│ └── main.tf
├── frontend-app
│ └── main.tf
├── mysql
│ └── main.tf
└── vpc
└── main.tf
To use remote state with each of these modules, you would have to copy/paste the exact same backend configuration
into each of the main.tf files. The only thing that would differ between the configurations would be the key
parameter: e.g., the key for mysql/main.tf might be mysql/terraform.tfstate and the key for
frontend-app/main.tf might be frontend-app/terraform.tfstate.
To keep your remote state configuration DRY, you can use Terragrunt. You still have to specify the backend you want
to use in each module, but instead of copying and pasting the configuration settings over and over again into each
main.tf file, you can leave them blank:
terraform {
# The configuration for this backend will be filled in by Terragrunt
backend "s3" {}
}
Filling in remote state settings with Terragrunt
To fill in the settings via Terragrunt, create a terraform.tfvars file in the root folder and in each of the
Terraform modules:
├── terraform.tfvars
├── backend-app
│ ├── main.tf
│ └── terraform.tfvars
├── frontend-app
│ ├── main.tf
│ └── terraform.tfvars
├── mysql
│ ├── main.tf
│ └── terraform.tfvars
└── vpc
├── main.tf
└── terraform.tfvars
In your root terraform.tfvars file, you can define your entire remote state configuration just once in a
remote_state block, as follows:
terragrunt = {
remote_state {
backend = "s3"
config {
bucket = "my-terraform-state"
key = "${path_relative_to_include()}/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "my-lock-table"
}
}
}
The remote_state block supports all the same backend types
as Terraform. The next time you run terragrunt, it will automatically configure all the settings in the
remote_state.config block, if they aren’t configured already, by calling terraform
init.
In each of the child terraform.tfvars files, such as mysql/terraform.tfvars, you can tell Terragrunt to
automatically include all the settings from the root terraform.tfvars file as follows:
terragrunt = {
include {
path = "${find_in_parent_folders()}"
}
}
The include block tells Terragrunt to use the exact same Terragrunt configuration from the terraform.tfvars file
specified via the path parameter. It behaves exactly as if you had copy/pasted the Terraform configuration from
the root terraform.tfvars file into mysql/terraform.tfvars, but this approach is much easier to maintain!
The child .tfvars file’s terragrunt.terraform settings will be merged into the parent file’s terragrunt.terraform
settings as follows:
- If an
extra_argumentsblock in the child has the same name as anextra_argumentsblock in the parent, then the child’s block will override the parent’s.- Specifying an empty
extra_argumentsblock in a child with the same name will effectively remove the parent’s block.
- Specifying an empty
- If an
extra_argumentsblock in the child has a different name thanextra_argumentsblocks in the parent, then both the parent and child’sextra_argumentswill be effective.- The child’s
extra_argumentswill be placed after the parent’sextra_argumentson the terraform command line. - Therefore, if a child’s and parent’s
extra_argumentsinclude.tfvarsfiles with the same variable defined, the value from the.tfvarsfile from the child’sextra_argumentswill be used by terraform.
- The child’s
- The
sourcefield in the child will overridesourcefield in the parent
Other settings in the child .tfvars file’s terragrunt block (e.g. remote_state) override the respective
settings in the parent.
The terraform.tfvars files above use two Terragrunt built-in functions:
-
find_in_parent_folders(): This function returns the path to the firstterraform.tfvarsfile it finds in the parent folders above the currentterraform.tfvarsfile. In the example above, the call tofind_in_parent_folders()inmysql/terraform.tfvarswill return../terraform.tfvars. This way, you don’t have to hard code thepathparameter in every module. -
path_relative_to_include(): This function returns the relative path between the currentterraform.tfvarsfile and the path specified in itsincludeblock. We typically use this in a rootterraform.tfvarsfile so that each Terraform child module stores its Terraform state at a differentkey. For example, themysqlmodule will have itskeyparameter resolve tomysql/terraform.tfstateand thefrontend-appmodule will have itskeyparameter resolve tofrontend-app/terraform.tfstate.
See the Interpolation Syntax docs for more info.
Check out the terragrunt-infrastructure-modules-example and terragrunt-infrastructure-live-example repos for fully-working sample code that demonstrates how to use Terragrunt to manage remote state.
Create remote state and locking resources automatically
When you run terragrunt with remote_state configuration, it will automatically create the following resources if
they don’t already exist:
-
S3 bucket: If you are using the S3 backend for remote state storage and the
bucketyou specify inremote_state.configdoesn’t already exist, Terragrunt will create it automatically, with versioning enabled. -
DynamoDB table: If you are using the S3 backend for remote state storage and you specify a
dynamodb_table(a DynamoDB table used for locking) inremote_state.config, if that table doesn’t already exist, Terragrunt will create it automatically, including a primary key calledLockID.
Note: If you specify a profile key in remote_state.config, Terragrunt will automatically use this AWS profile
when creating the S3 bucket or DynamoDB table.