Skip to content

Terraform State

Terraform stores information about your infrastructure within a state file.

Terraform uses state to determine which changes to make to your infrastructure, based on the HCL configuration it finds in your code. Changes in state will lead to the resources being modified, added or removed.

Before any changes are applied to your infrastructure, Terraform performs a refresh to sync the current state, in order to check for any resources’ that may have encountered configuration drifts.

You can view and manipulate Terraform state using the terraform state command.

CommandDescription
listList resources in the state
mvMove an item in the state
pullPull current state and output to stdout
pushUpdate remote state from a local state file
replace-providerReplace provider in the state
rmRemove instances from the state
showShow a resource in the state

Example terraform state list output

Terminal window
data.aws_ami.latest_amazon_linux
data.aws_vpc_endpoint_service.s3
aws_instance.web_server
aws_internet_gateway.igw
aws_route.public_route
aws_route_table.private
aws_route_table.public
aws_route_table_association.private["eu-west-2a"]
aws_route_table_association.private["eu-west-2b"]
aws_route_table_association.private["eu-west-2c"]
aws_route_table_association.public["eu-west-2a"]
aws_route_table_association.public["eu-west-2b"]
aws_route_table_association.public["eu-west-2c"]
aws_security_group.web_server
aws_subnet.private["eu-west-2a"]
aws_subnet.private["eu-west-2b"]
aws_subnet.private["eu-west-2c"]
aws_subnet.public["eu-west-2a"]
aws_subnet.public["eu-west-2b"]
aws_subnet.public["eu-west-2c"]
aws_vpc.vpc
aws_vpc_endpoint.s3
aws_vpc_endpoint_route_table_association.s3_az_association

Example terraform state show aws_vpc.vpc output

Terminal window
resource "aws_vpc" "vpc" {
arn = "arn:aws:ec2:eu-west-2:<id>:vpc/vpc-05f087bb5414aab4c"
assign_generated_ipv6_cidr_block = false
cidr_block = "192.168.0.0/16"
default_network_acl_id = "acl-017e916dbee0b5d63"
default_route_table_id = "rtb-06b2809db725c55ef"
default_security_group_id = "sg-0b5a4febc36710404"
dhcp_options_id = "dopt-6c222d04"
enable_dns_hostnames = true
enable_dns_support = true
enable_network_address_usage_metrics = false
id = "vpc-05f087bb5414aab4c"
instance_tenancy = "default"
ipv6_netmask_length = 0
main_route_table_id = "rtb-06b2809db725c55ef"
owner_id = "<id>"
tags = {
"App" = "webapp"
"Name" = "webapp-vpc"
"Prefix" = "webapp"
"Region" = "eu-west-2"
}
tags_all = {
"App" = "webapp"
"Name" = "webapp-vpc"
"Prefix" = "webapp"
"Region" = "eu-west-2"
}
}

Terraform state files contain a full snapshot of your infrastructure, including any values marked as sensitive in your configuration. These values are stored in plain text inside the state file. This means database passwords, API keys, and other secrets are readable by anyone with access to the file.

When Terraform creates or reads a resource that includes sensitive attributes, those values end up in the state file as-is. For example, an aws_db_instance resource stores the master password in state, and an aws_secretsmanager_secret_version stores the secret value. The sensitive flag on variables and outputs only masks values in CLI output — it does not encrypt them in state.

Remote backends like S3, Azure Blob Storage, and Google Cloud Storage support encryption at rest. When using S3 as a backend, enable server-side encryption on the bucket so that the state file is encrypted on disk:

terraform {
backend "s3" {
bucket = "my-terraform-state"
key = "prod/terraform.tfstate"
region = "eu-west-2"
encrypt = true
dynamodb_table = "terraform-locks"
}
}

Setting encrypt = true tells Terraform to request server-side encryption when writing the state file to S3. This protects the data at rest but does not prevent authorised users from reading the decrypted contents.

Treat your state file like a secret. Restrict who and what can read or write it:

  • Use IAM policies to limit access to the S3 bucket (or equivalent storage) to only the roles and users that run Terraform.
  • Apply a bucket policy that denies unencrypted uploads and enforces HTTPS.
  • If you use DynamoDB for state locking, restrict access to the lock table as well.
  • In team environments, prefer remote backends with access controls over passing state files around manually.

State files should never be committed to Git or any other version control system. They contain sensitive data and change with every apply, making them a poor fit for source control.

Add the following to your .gitignore:

*.tfstate
*.tfstate.backup
.terraform/

If you are using a remote backend, Terraform does not write a local state file by default, which removes the risk of accidental commits. For local state workflows, always verify that your .gitignore excludes state files before pushing changes.

Importing Existing Resources with import Blocks

Section titled “Importing Existing Resources with import Blocks”

When you have infrastructure that was created outside of Terraform — manually in the console, by another tool, or before your team adopted Terraform — you need a way to bring those resources under Terraform management. The import block (Terraform 1.5+) lets you do this declaratively in your configuration files, rather than running one-off CLI commands.

Before Terraform 1.5, the only way to import a resource was the terraform import CLI command:

Terminal window
terraform import aws_s3_bucket.assets my-existing-bucket

This works, but it has drawbacks. The command only updates the state file — it does not generate any configuration. You still need to write the matching resource block by hand, and if the configuration does not match the real resource, the next plan will show unexpected changes. The command also cannot be reviewed in a pull request or shared with your team, since it runs interactively.

The import block solves these problems by making imports part of your configuration:

  • Import operations are visible in terraform plan output before anything changes.
  • They can be reviewed in version control alongside the resource configuration.
  • They work in CI/CD pipelines without interactive input.

An import block takes two arguments: to (the Terraform resource address) and id (the cloud provider’s identifier for the existing resource):

import {
to = aws_s3_bucket.assets
id = "my-existing-bucket"
}
resource "aws_s3_bucket" "assets" {
bucket = "my-existing-bucket"
}

When you run terraform plan, Terraform reads the real resource using the provider, compares it against your resource block, and reports the import. Running terraform apply writes the resource into state. After a successful apply, you can remove the import block — it only needs to be present for one apply cycle, similar to moved blocks.

Generating Configuration with -generate-config-out

Section titled “Generating Configuration with -generate-config-out”

Writing the matching resource block by hand can be tedious, especially for resources with many attributes. The -generate-config-out flag on terraform plan generates a configuration file from the imported resource’s actual state:

Terminal window
terraform plan -generate-config-out=generated.tf

This reads the real resource and writes a resource block to generated.tf with all of its attributes filled in. You can then review the generated file, remove any read-only or computed attributes, and move the configuration into your main files.

A typical workflow looks like this:

  1. Add the import block to your configuration with the resource address and cloud ID.
  2. Run terraform plan -generate-config-out=generated.tf to create the matching resource configuration.
  3. Review generated.tf, clean up unnecessary attributes, and move the resource block into the appropriate file.
  4. Run terraform plan again to confirm the import shows no unexpected changes.
  5. Run terraform apply to write the resource into state.
  6. Remove the import block from your configuration.
  • The id format depends on the provider. For AWS resources, it is usually the resource’s ARN or name. Check the provider documentation for the correct format.
  • import blocks support for_each (Terraform 1.7+), so you can import multiple resources of the same type in a single block.
  • If the generated configuration does not match the real resource exactly, terraform plan will show a diff. Adjust your configuration until the plan is clean before applying.
  • Like moved blocks, import blocks are one-time operations. Remove them after the import is complete to keep your configuration tidy.