In this article, we look at how to manage Terraform State. First, we cover what Terraform state is and why it is required before looking at some best practices for storing, organizing, and isolating your state files. We then move on to look at referencing the remote state utilizing a data source, and, finally, how to use the terraform state
command to manipulate the contents of the state file.
We will cover:
Terraform logs information about the resources it has created in a state file. This enables Terraform to know which resources are under its control and when to update and destroy them. The terraform state file, by default, is named terraform.tfstate
and is held in the same directory where Terraform is run. It is created after running terraform apply
.
The actual content of this file is a JSON formatted mapping of the resources defined in the configuration and those that exist in your infrastructure. When Terraform is run, it can then use this mapping to compare infrastructure to the code and make any adjustments as necessary.
Read more about the elements of Terraform architecture.
Note: New versions of Terraform are placed under the BUSL license, but everything created before version 1.5.x stays open-source. OpenTofu is an open-source version of Terraform that expands on Terraform’s existing concepts and offerings. It is a viable alternative to HashiCorp’s Terraform, being forked from Terraform version 1.5.6.
State files, by default, are stored in the local directory where Terraform is run. If you are using Terraform to test or for a personal project, this is fine (as long as your state file is secure and backed up!). However, when working on Terraform projects in a team, this becomes a problem because multiple people will need to access the state file.
Also, when using automation and CI/CD pipelines to run Terraform, the state file needs to be accessible, and permission must be given to the service principal running the pipeline to access the storage account container that holds the state file. This makes shared storage the perfect candidate to hold the state file. Permissions can be granted as needed. Azure Storage accounts or Amazon S3 buckets are an ideal choice. You can also use a tool such as Spacelift to manage your state for you.
You should store your state files remotely, not on your local machine! The location of the remote state file can then be referenced using a backend
block in the terraform
block (which is usually in the main.tf
file).
The example below shows a configuration using a storage account in Azure:
terraform {
backend "azurerm" {
resource_group_name = "terraform-rg"
storage_account_name = "terraformsa"
container_name = "terraformstate"
key = "terraform.tfstate"
}
}
It is not a good idea to store the state file in source control. This is because Terraform state files contain all data in plain text, which may contain secrets. Storing secrets anywhere other than a secure location is never a good idea and definitely should not be put into source control. To avoid this, for example, in Azure, Azure Key Vault can be referenced.
Secondly, most version control systems do not allow the locking of files, which may cause issues when multiple people attempt to access the file at the same time. Lastly, storing state files in version control can introduce human error, as the latest state file would need to be pulled each time Terraform is run. This could result in an old state file being used and unexpected changes to the infrastructure. (Read more: Terraform Force-Unlock Command)
Using a remote backend instead solves these challenges. Locking is natively supported; the state file will be automatically loaded, and encryption can be used to secure the state file on disk and in transit. Remote backend storage such as Azure Storage or Amazon S3 is designed to be highly available. They also support versioning so you can roll back to a previous state file should it get corrupted. Lastly, it is inexpensive to use. Usually, state files are very small and can normally fit into the free tier.
State files should be isolated to reduce the “blast radius”. Usually, projects are structured in a single folder and use a single state file for all resources. This immediately introduces risk, as a mistake in the configuration could change the state file and cause unwanted consequences to all your resources.
Isolating through multiple state files
A better way is to use multiple state files for parts of your infrastructure. Logically separating resources from each other and giving them their own state file in the backend means that changes to one resource will not affect the other. Different state files for different environments are also a good idea.
Check our guide on How to Manage Multiple Terraform Environments Efficiently.
For example, consider we have an Azure SQL Database, Azure VNET, and Azure Web App in our project. The infrastructure is the same across three environments: development, UAT (user acceptance testing), and production. Each of the three resources would have its own state file for each environment, providing a strong separation. Taking it one stage further, you may wish to use a completely separate storage account for each environment in different subscriptions.
Assuming we are holding all state files in a single storage account, the container folder structure for the remote backend could end up looking something like the example below:
terraformstate
--development
--webapp.tfstate
--sqldb.tfstate
--vnet.tfstate
--UAT
--webapp.tfstate
--sqldb.tfstate
--vnet.tfstate
--production
--webapp.tfstate
--sqldb.tfstate
--vnet.tfstate
The backend configuration for the Azure Web App in the development environment:
terraform {
backend "azurerm" {
resource_group_name = "terraform-rg"
storage_account_name = "terraformsa"
container_name = "terraformstate"
key = "development\webapp.tfstate"
}
}
Making a change to the Azure Web App in development will have no effect on the Azure Web App in production.
Isolation through Terraform workspaces
Isolation can also be achieved using Terraform workspaces. These are less useful in production environments but are good for a quick way to test isolated environments. Using workspaces to manage actual environments is discouraged. Terraform has one workspace by default (called default!). State files are isolated to each workspace.
You can create a new workspace using the terraform workspace new
command.
$ terraform workspace new development
Created and switched to workspace "development"! You're now on a new, empty workspace. Workspaces isolate their state, so if you run "terraform plan" Terraform will not see any existing state for this configuration.
Now, whenever you create a new workspace and you want to run your terraform code, this creates a terraform.tfstate.d directory which will hold the state for your workspace. Let’s create two other workspaces, one for stage and the other one for production:
$ terraform workspace new stage
Created and switched to workspace "stage"! You're now on a new, empty workspace. Workspaces isolate their state, so if you run "terraform plan" Terraform will not see any existing state for this configuration.
$ terraform workspace new prod
Created and switched to workspace "prod"! You're now on a new, empty workspace. Workspaces isolate their state, so if you run "terraform plan" Terraform will not see any existing state for this configuration.
Now, let’s create a local variable called cidr_block that will take different values depending on the selected workspace:
locals {
cidr_block = terraform.workspace == "dev" ? "10.0.0.0/16" : terraform.workspace == "stage" ? "11.0.0.0/16" : "12.0.0.0/16"
}
output "cidr_block" {
value = local.cidr_block
}
Because the last workspace we were on is prod, the value should be 12.0.0.0/16, let’s run a terraform apply and see it in action:
terraform apply
Changes to Outputs:
+ cidr_block = "12.0.0.0/16"
You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.
Do you want to perform these actions in workspace "prod"?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
cidr_block = "12.0.0.0/16"
Now, let’s switch to dev and run a terraform apply
:
terraform workspace select dev
Switched to workspace "dev".
terraform apply
Changes to Outputs:
+ cidr_block = "10.0.0.0/16"
You can apply this plan to save these new output values to the Terraform state, without changing any real infrastructure.
Do you want to perform these actions in workspace "dev"?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value: yes
Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
Outputs:
cidr_block = "10.0.0.0/16
Now, if we go inside the terraform.tfstate.d we will see the corresponding directories for the workspaces and two generated tfstate files for the dev and prod workspaces:
tree .
.
├── dev
│ └── terraform.tfstate
├── prod
│ └── terraform.tfstate
└── stage
4 directories, 2 file
Useful command quick reference when working with workspaces:
terraform workspace list
— list your workspaces.
terraform workspace new [workspace name]
— create a new workspace.
terraform workspace select [workspace name]
— switch to the specified workspace.
terraform_remote_state
is a data source that can be used to fetch details from the remote state file directly. This is useful when you need to reference the outputs of configurations that are stored in different state files. When an output
block is defined in your configuration, the contents are included in the state file. These details can then be referenced elsewhere in your project.
Read more about the Terraform remote state.
terraform_remote_state data source example
For example, again, consider we have an Azure SQL Database and Azure Web App. The configuration files for these should be set up to use different state files. When creating the database that the web app needs to connect to, we add an output
block to expose the resulting ID of the database after it has been created:
output "sqldb_id" {
value = azurerm_sql_database.example.id
description = "Database ID"
}
We can now reference the resulting SQL database ID in the Web App configuration code by configuring a data block using terraform_remote_state
:
data "terraform_remote_state" "dev_sqldb" {
backend = "azurerm"
config = {
storage_account_name = "terraformsa"
container_name = "terraformstate"
key = "development/sqldb.tfstate"
}
}
And then reference the name of the output where necessary in the configuration:
data.terraform_remote_state.dev_sqldb.outputs.sqldb_id
It is sometimes necessary to directly interact with the state file, either to check its contents, remove items when they have been imported incorrectly or no longer exist in the real infrastructure, or import items that already exist to bring them under Terraform management.
The terraform state
command can be used to perform advanced state management. All state management commands that modify the state create a timestamped backup of the state prior to making modifications.
Useful terraform state
commands
terraform state list
— List the contents of the state file.terraform state rm
— Remove an item from the state file.terraform state show
— Show a resource in the state file.
To import existing items into the state file that have been created by other methods in the infrastructure to bring them under Terraform control, the terraform import
command can be used. Each resource on the Terraform docs pages has an import section detailing how to use the command for a particular resource. For example, on the azurerm_sql_database docs page, it states that SQL Databases can be imported using the resource id
and gives an example.
terraform import azurerm_sql_database.database1 /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/myresourcegroup/providers/Microsoft.Sql/servers/myserver/databases/database1
The corresponding resource
block azurerm_sql_database.database1
needs to be written in the configuration file prior to using this command, with the settings matching the real infrastructure. Importing resources can be very time-consuming, so avoiding any manual creation of resources from the get-go is normally a wise idea!
To learn more, see our step-by-step tutorial: Importing Existing Infrastructure into Terraform.
When working with Terraform there are a couple of things you should consider about your infrastructure state, as this is critical for the successful and secure deployment of your IaC.
Here are a couple of best practices:
- Always use remote state – it goes without saying that the local state is planning to fail, especially when working inside a team. With remote state and locking mechanisms in place, you ensure that collaboration can be implemented and there won’t be any race conditions happening. See how to migrate your Terraform state.
- Implement state encryption – encryption should be enabled for state files at rest and in transit, and if your remote backend supports it, enable server-side encryption.
- Review Terraform plans before doing applies – this will ensure you understand the impact your changes have on your infrastructure and your state.
- Version your configuration and use modules wherever possible – Your configuration should always be versioned because it will make rollbacks easier. Using modules and versioning them, will also ensure that you can easily roll back to the previous configuration.
- Use Terraform Automation and Collaboration Software such as Spacelift for state management – Custom remote state backends are good, but Spacelift takes state management to the next level without you needing to lift a finger.
Spacelift can optionally manage the Terraform state for you, offering a backend synchronized with the rest of the platform to maximize convenience and security. You also get the ability to import your state during stack creation, which is very useful for engineers who are migrating their old configurations and states to Spacelift.
Behind the scenes, Spacelift uses Amazon S3 and is storing all the data in Ireland. It’s super simple to have Spacelift manage the state for you, as this behavior is achieved by default without you needing to do anything. You can read more about how it actually works here.
At the same time, it’s protected against accidental or malicious access as Spacelift is able to map state access and changes to legitimate Spacelift runs, automatically blocking all unauthorized traffic.
If you are interested in learning more about Spacelift, create a free account today or book a demo with one of our engineers.
Understanding aspects of Terraform state management and best practices is key to becoming proficient with Terraform. State files should be stored remotely as a general rule and isolated and organized so that separate state files exist for logical groups of resources and environments to reduce the “blast radius” if mistakes occur.
The terraform_remote_state
data source can be used to reference outputs
from state files. Finally the terraform state
and terraform import
commands can be used to manipulate the contents of the state file.
Cheers!
Manage Terraform Better and Faster
If you are struggling with Terraform automation and management, check out Spacelift. It helps you manage Terraform state, build more complex workflows, and adds several must-have capabilities for end-to-end infrastructure management.
Terraform State Cheatsheet
Grab our ultimate cheat sheet PDF and keep your IaC safe while managing State.