State Backend
OpenTofu/Terraform supports remote state storage via various backends that you normally configure in your .tf files as follows:
terraform { backend "s3" { bucket = "my-tofu-state" key = "frontend-app/tofu.tfstate" region = "us-east-1" encrypt = true dynamodb_table = "my-lock-table" }}Unfortunately, the backend configuration does not currently support expressions, variables, or functions. This makes it hard to keep your code DRY if you have multiple OpenTofu/Terraform modules. For example, consider the following filesystem layout, which uses different OpenTofu/Terraform modules to deploy a backend app, frontend app, MySQL database, and a VPC:
Directorybackend-app
- main.tf
Directoryfrontend-app
- main.tf
Directorymysql
- main.tf
Directoryvpc
- main.tf
To use remote state with each of these modules, you would have to copy/paste the identical 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/tofu.tfstate and the key for frontend-app/main.tf might be frontend-app/tofu.tfstate.
In addition, the resources used for remote state will be provisioned somewhere else, and that somewhere else needs to be managed. Most users end up using “click-ops” to provision the S3 bucket and DynamoDB table used for AWS remote state (clicking around in the AWS console until they have what they need). This is error-prone, difficult to reproduce, and makes it hard to do the right thing consistently (e.g., enabling versioning, encryption, and access logging).
Luckily, Terragrunt has built-in tooling to make it easy to manage remote state.
Generating remote state settings with Terragrunt
Section titled “Generating remote state settings with Terragrunt”To fill in the settings via Terragrunt, create a root.hcl file in the root folder, plus one terragrunt.hcl file in each of the OpenTofu/Terraform modules:
- root.hcl
Directorybackend-app
- main.tf
- terragrunt.hcl
Directoryfrontend-app
- main.tf
- terragrunt.hcl
Directorymysql
- main.tf
- terragrunt.hcl
Directoryvpc
- main.tf
- terragrunt.hcl
In your root.hcl file, you can define your entire remote state configuration just once in a generate block, to generate a backend.tf file that includes the backend configuration:
generate "backend" { path = "backend.tf" if_exists = "overwrite_terragrunt" contents = <<EOFterraform { backend "s3" { bucket = "my-tofu-state" key = "${path_relative_to_include()}/tofu.tfstate" region = "us-east-1" encrypt = true dynamodb_table = "my-lock-table" }}EOF}This instructs Terragrunt to create the file backend.tf in the working directory (where Terragrunt calls tofu/terraform)
before it runs any OpenTofu/Terraform commands, including init. This allows you to inject this backend configuration
in all the units that include the root file and have terragrunt properly initialize the backend configuration with
interpolated values.
To inherit this configuration in each unit, such as mysql/terragrunt.hcl, you can
configure Terragrunt units to automatically include all the settings from the root root.hcl file as follows:
include "root" { path = find_in_parent_folders("root.hcl")}The include block here configures the mysql Terragrunt unit to merge in partial configurations from the root.hcl file specified via the path parameter. It behaves exactly as if you had copy/pasted the generate configuration into mysql/terragrunt.hcl, but this approach is much easier to maintain!
The next time you run terragrunt, it will automatically configure all the settings for the backend, if they aren’t configured already, by calling tofu/terraform init.
The terragrunt.hcl files above use two Terragrunt built-in functions:
-
find_in_parent_folders(): This function returns the absolute path to the first file it finds in the parent folders above the current unit with a given file name. In the example above, the call tofind_in_parent_folders("root.hcl")inmysql/terragrunt.hclwill return/absolute/path/to/root.hcl. This way, you don’t have to hard code thepathparameter in every unit. -
path_relative_to_include(): This function returns the relative path between the unit and the path specified in itsincludeblock. We typically use this in a rootroot.hclfile so that each unit stores its OpenTofu/Terraform state at a differentkey. For example, themysqlunit will have itskeyparameter resolve tomysql/tofu.tfstateand thefrontend-appmodule will have itskeyparameter resolve tofrontend-app/tofu.tfstate.
Read Functions docs for more info.
Create remote state resources automatically
Section titled “Create remote state resources automatically”The generate block is useful for allowing you to set up the remote state backend configuration automatically, but
this introduces a bootstrapping problem: how do you create and manage the underlying storage resources for the remote
state? For example, when using the s3 backend, OpenTofu/Terraform
expects that the S3 bucket already exists for it to upload/download the state objects.
Ideally, you can manage the backend resources you use for storing state using OpenTofu/Terraform, but where does the
state for that module go? How do you create those resources before you run tofu/terraform, if you need to run
tofu/terraform to create them?
To handle this, Terragrunt supports a different block for managing the backend configuration: the remote_state block.
Here is an example of using the remote_state block to configure the S3 backend using both an S3 bucket and a DynamoDB table:
remote_state { backend = "s3" generate = { path = "backend.tf" if_exists = "overwrite" } config = { bucket = "my-tofu-state" key = "${path_relative_to_include()}/tofu.tfstate" region = "us-east-1" encrypt = true dynamodb_table = "my-lock-table" }}Like the approach with generate blocks, this will generate a backend.tf file that contains the remote state
configuration. In addition, when you run terragrunt backend bootstrap or pass
--backend-bootstrap, Terragrunt creates the S3 bucket my-tofu-state and DynamoDB table my-lock-table if they
don’t exist in your account, and configures them based on the specified configuration parameters.
Terragrunt bootstraps three backends. Each has its own page for what Terragrunt creates, the settings it accepts, and its known limitations:
For all other backends, the remote_state block operates in the same manner as generate, currently. If you are not intending for Terragrunt to automatically perform
automated bootstrapping of remote state resources, you are advised to use generate blocks to configure the OpenTofu/Terraform backend instead.
Disabling automatic remote state bootstrapping
Section titled “Disabling automatic remote state bootstrapping”You can disable automatic remote state bootstrapping by setting remote_state.disable_init (it’s called this for legacy reasons). This will skip the automatic creation of remote state resources (e.g. S3 buckets, GCS buckets, Azure storage accounts) by Terragrunt, while still allowing OpenTofu/Terraform to initialize already provisioned backends normally. This can be handy when running commands such as run --all validate as part of a CI process where you do not want Terragrunt to create or modify remote state resources.
In previous versions, disable_init = true passed -backend=false to terraform init, preventing OpenTofu/Terraform from initializing the backend entirely. The new behavior passes -backend-config=KEY=VALUE arguments instead — OpenTofu/Terraform will attempt to connect to the backend. Ensure backend resources exist and credentials are valid before using disable_init = true.
Note that --backend-bootstrap defaults to false, so Terragrunt does not create backend resources by default regardless of disable_init. The disable_init flag additionally prevents bootstrap even when --backend-bootstrap is explicitly set.
Skipping backend initialization entirely
Section titled “Skipping backend initialization entirely”If you need to skip backend initialization entirely (the previous behavior of disable_init), pass -backend=false directly to OpenTofu/Terraform via extra_arguments:
terraform { extra_arguments "no_backend_init" { commands = ["init"] arguments = ["-backend=false"] }}Or directly on the command line: terragrunt run -- init -backend=false
The following example demonstrates using an environment variable to configure disable_init:
remote_state { # ...
disable_init = tobool(get_env("TG_DISABLE_INIT", "false"))}Further reading
Section titled “Further reading”- Managing your remote state like this is really valuable when you organize your units into a stack. Reading about those concepts will help you understand how to organize your infrastructure such that different units stored in isolated state can interact with each other.
- See the
remote_stateblock reference for the full list of supported attributes. - See the
generateblock reference for details on how code generation works. - Check out the terragrunt-infrastructure-catalog-example and terragrunt-infrastructure-live-stacks-example repos for fully-working sample code that demonstrates how to use Terragrunt to manage remote state.