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_arguments
block in the child has the same name as anextra_arguments
block in the parent, then the child’s block will override the parent’s.- Specifying an empty
extra_arguments
block in a child with the same name will effectively remove the parent’s block.
- Specifying an empty
- If an
extra_arguments
block in the child has a different name thanextra_arguments
blocks in the parent, then both the parent and child’sextra_arguments
will be effective.- The child’s
extra_arguments
will be placed after the parent’sextra_arguments
on the terraform command line. - Therefore, if a child’s and parent’s
extra_arguments
include.tfvars
files with the same variable defined, the value from the.tfvars
file from the child’sextra_arguments
will be used by terraform.
- The child’s
- The
source
field in the child will overridesource
field 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.tfvars
file it finds in the parent folders above the currentterraform.tfvars
file. In the example above, the call tofind_in_parent_folders()
inmysql/terraform.tfvars
will return../terraform.tfvars
. This way, you don’t have to hard code thepath
parameter in every module. -
path_relative_to_include()
: This function returns the relative path between the currentterraform.tfvars
file and the path specified in itsinclude
block. We typically use this in a rootterraform.tfvars
file so that each Terraform child module stores its Terraform state at a differentkey
. For example, themysql
module will have itskey
parameter resolve tomysql/terraform.tfstate
and thefrontend-app
module will have itskey
parameter 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
bucket
you specify inremote_state.config
doesn’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.