Import State Infrastructure
On this page, you will:
- Import the S3 bucket used for Terraform state
- Import the DynamoDB table used for state locking
- Configure lifecycle rules and encryption settings in code
Understanding the Challenge
Importing Terraform's own state infrastructure creates a unique situation: you're using Terraform to manage the very resources that store Terraform's state. This circular dependency requires careful handling.
The good news is that Terraform handles this gracefully. Once the resources are imported, Terraform can manage them like any other resource. The key is ensuring your configuration exactly matches what exists.
Gather State Infrastructure Information
First, retrieve details about your existing state resources:
# Get S3 bucket details
aws s3api get-bucket-versioning --bucket terraform-state-YOUR_ACCOUNT_ID --profile infrastructure-admin
# Get bucket encryption
aws s3api get-bucket-encryption --bucket terraform-state-YOUR_ACCOUNT_ID --profile infrastructure-admin
# Get bucket lifecycle configuration
aws s3api get-bucket-lifecycle-configuration --bucket terraform-state-YOUR_ACCOUNT_ID --profile infrastructure-admin
# Get DynamoDB table details
aws dynamodb describe-table --table-name terraform-state-lock --profile infrastructure-admin
Replace YOUR_ACCOUNT_ID with your actual AWS account ID.
Add Variables for State Infrastructure
Update variables.tf to include state-related variables:
# State infrastructure naming
variable "state_bucket_name" {
description = "Name of the S3 bucket for Terraform state"
type = string
default = null # Will default to terraform-state-{account_id}
}
variable "state_lock_table_name" {
description = "Name of the DynamoDB table for state locking"
type = string
default = "terraform-state-lock"
}
Update terraform.tfvars if you used a custom bucket name:
# State Infrastructure (only needed if you used non-default names)
# state_bucket_name = "my-custom-bucket-name"
# state_lock_table_name = "my-custom-lock-table"
Create State Infrastructure Configuration
Create state_infrastructure.tf:
# =============================================================================
# Terraform State Infrastructure
# =============================================================================
# These resources store Terraform's own state. Import them carefully.
locals {
state_bucket_name = coalesce(var.state_bucket_name, "terraform-state-${var.aws_account_id}")
}
# -----------------------------------------------------------------------------
# S3 Bucket for Terraform State
# -----------------------------------------------------------------------------
resource "aws_s3_bucket" "terraform_state" {
bucket = local.state_bucket_name
# Prevent accidental deletion of state bucket
lifecycle {
prevent_destroy = true
}
tags = {
Name = local.state_bucket_name
Purpose = "TerraformState"
Environment = "Shared"
}
}
# Bucket versioning - enables rollback for state files
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
# Server-side encryption
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
bucket_key_enabled = true
}
}
# Block all public access
resource "aws_s3_bucket_public_access_block" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
# Lifecycle rules for cost optimisation
resource "aws_s3_bucket_lifecycle_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
id = "MoveOldVersionsToIA"
status = "Enabled"
noncurrent_version_transition {
noncurrent_days = 30
storage_class = "STANDARD_IA"
}
noncurrent_version_expiration {
noncurrent_days = 90
}
}
}
# -----------------------------------------------------------------------------
# DynamoDB Table for State Locking
# -----------------------------------------------------------------------------
resource "aws_dynamodb_table" "terraform_state_lock" {
name = var.state_lock_table_name
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
point_in_time_recovery {
enabled = true
}
# Prevent accidental deletion of lock table
lifecycle {
prevent_destroy = true
}
tags = {
Name = var.state_lock_table_name
Purpose = "TerraformStateLock"
Environment = "Shared"
}
}
Prevent Destroy
The prevent_destroy lifecycle rule stops accidental deletion of state infrastructure. If you ever need to remove these resources, you must first remove or set this to false in your configuration.
Create Import Blocks
Add to imports.tf:
# =============================================================================
# State Infrastructure Imports
# =============================================================================
# S3 Bucket
import {
to = aws_s3_bucket.terraform_state
id = "terraform-state-123456789012" # Replace with your bucket name
}
# S3 Bucket Versioning
import {
to = aws_s3_bucket_versioning.terraform_state
id = "terraform-state-123456789012" # Same bucket name
}
# S3 Bucket Encryption
import {
to = aws_s3_bucket_server_side_encryption_configuration.terraform_state
id = "terraform-state-123456789012" # Same bucket name
}
# S3 Bucket Public Access Block
import {
to = aws_s3_bucket_public_access_block.terraform_state
id = "terraform-state-123456789012" # Same bucket name
}
# S3 Bucket Lifecycle Configuration
import {
to = aws_s3_bucket_lifecycle_configuration.terraform_state
id = "terraform-state-123456789012" # Same bucket name
}
# DynamoDB Table
import {
to = aws_dynamodb_table.terraform_state_lock
id = "terraform-state-lock"
}
Update Bucket Name
Replace terraform-state-123456789012 with your actual bucket name.
Plan the Import
terraform plan
Review the output. You should see resources to be imported with no changes:
Plan: 6 to import, 0 to add, 0 to change, 0 to destroy.
Common Configuration Differences
If Terraform shows planned changes, check these common issues:
Lifecycle Rule ID Mismatch
The lifecycle rule ID must match exactly. Check your current configuration:
aws s3api get-bucket-lifecycle-configuration \
--bucket terraform-state-YOUR_ACCOUNT_ID \
--profile infrastructure-admin
Update the id in your lifecycle rule to match.
Missing Tags
If you didn't add tags when creating the resources manually, Terraform will want to add them. This is safe to apply.
Encryption Configuration
Verify the encryption algorithm matches:
aws s3api get-bucket-encryption \
--bucket terraform-state-YOUR_ACCOUNT_ID \
--profile infrastructure-admin
Apply the Import
terraform apply
Type yes when prompted.
Expected output:
aws_s3_bucket.terraform_state: Importing... [id=terraform-state-123456789012]
aws_s3_bucket.terraform_state: Import complete [id=terraform-state-123456789012]
aws_s3_bucket_versioning.terraform_state: Importing...
aws_s3_bucket_versioning.terraform_state: Import complete
...
Apply complete! Resources: 6 imported, 0 added, 0 changed, 0 destroyed.
Verify the Import
Check the imported resources in state:
terraform state list | grep -E "(s3_bucket|dynamodb_table)"
Expected output:
aws_dynamodb_table.terraform_state_lock
aws_s3_bucket.terraform_state
aws_s3_bucket_lifecycle_configuration.terraform_state
aws_s3_bucket_public_access_block.terraform_state
aws_s3_bucket_server_side_encryption_configuration.terraform_state
aws_s3_bucket_versioning.terraform_state
Add Outputs
Update outputs.tf:
# State Infrastructure
output "state_bucket_name" {
description = "Name of the Terraform state S3 bucket"
value = aws_s3_bucket.terraform_state.id
}
output "state_bucket_arn" {
description = "ARN of the Terraform state S3 bucket"
value = aws_s3_bucket.terraform_state.arn
}
output "state_lock_table_name" {
description = "Name of the Terraform state lock DynamoDB table"
value = aws_dynamodb_table.terraform_state_lock.name
}
output "state_lock_table_arn" {
description = "ARN of the Terraform state lock DynamoDB table"
value = aws_dynamodb_table.terraform_state_lock.arn
}
Clean Up Import Blocks
After successful import, you can optionally remove the import blocks from imports.tf. They're only needed for the initial import. However, keeping them doesn't cause issues - Terraform will simply skip resources already in state.
Keep or Remove?
Some teams prefer to keep import blocks as documentation of what was imported. Others remove them to keep the configuration clean. Choose what works best for your team.
Commit Your Work
git add terraform/aws/
git commit -m "Import Terraform state infrastructure"
Troubleshooting
Error: Bucket Does Not Exist
If you see:
Error: error reading S3 Bucket: NoSuchBucket
Verify:
- The bucket name is correct (including your account ID)
- You're using the
infrastructure-adminprofile - The profile has access to the correct AWS account
aws s3 ls --profile infrastructure-admin | grep terraform
Error: Lifecycle Configuration Not Found
If you see:
Error: error getting S3 Bucket Lifecycle Configuration
The bucket might not have a lifecycle configuration. Either:
- Create one manually (see Remote State Setup)
- Or remove the lifecycle import block and let Terraform create it
Error: Table Does Not Exist
If you see:
Error: ResourceNotFoundException: Requested resource not found
Verify:
- The table name is correct (
terraform-state-lockby default) - You're in the correct region
- Check the table exists:
aws dynamodb list-tables --profile infrastructure-admin
What's Next
You've successfully imported the state infrastructure:
- S3 bucket managed in code
- Bucket versioning configured
- Encryption enabled
- Public access blocked
- Lifecycle rules active
- DynamoDB table managed in code
Continue to import IAM users →