Cloud 35 min read

AWS OpenTofu #112: Understanding OpenTofu State

Deconstruct the terraform.tfstate JSON file. Master state inspection commands, resource metadata tracking, sensitive data handling, and safe state manipulation.

Mohammad Rizky Prawira portrait
Mohammad Rizky Prawira
terminal Series Guide • Part 12 of 15

AWS + OpenTofu Level 100: Infrastructure as Code from Zero

Part of the CLI-first comprehensive learning path from foundational setup to production cloud architectures.

To most beginners, OpenTofu feels like pure magic: you write declarative configuration in .tf files, type tofu apply, and virtual cloud resources materialize in AWS.

You change a variable, run tofu apply again, and OpenTofu intelligently determines that out of thirty resources, only one needs to be updated and zero need to be destroyed.

How does OpenTofu know this? How does it map the lines of code in your text editor to live resources in an AWS data center halfway across the world?

The answer lies in a single file: terraform.tfstate.

State is the brain, memory, and single source of truth for your infrastructure. But state is also where things go terribly wrong for inexperienced engineers:

  • A developer runs git add . and accidentally commits database passwords to a public GitHub repo.
  • Two engineers run tofu apply simultaneously on their laptops, causing state race conditions and corrupted resources.
  • A teammate renames a resource block in .tf code, and OpenTofu threatens to destroy and recreate a production database containing terabytes of live customer records.

In this lab, we open the black box. You will deconstruct the JSON anatomy of terraform.tfstate, master the tofu state CLI suite (list, show, mv, rm), uncover the critical security hazard of plaintext secrets, and learn how senior engineers manipulate state safely.


🎯 What You Will Learn

State ArchitectureCLI State ManipulationEnterprise Security & Hygiene
• The lineage and serial consistency engine• Inspecting resources via tofu state list & show• Why sensitive = true does NOT protect state files
• How OpenTofu matches HCL code to AWS resource IDs• Renaming resources without destruction (tofu state mv)• Preventing state file leakage via root .gitignore
• State file schema versions and metadata tracking• Decoupling live AWS assets safely (tofu state rm)• The risks of concurrent execution and split-brain state
• Storing computed attributes from the AWS API• State synchronization lifecycle during tofu plan• Preparing for remote backends (S3 + DynamoDB locking)

💰 Estimated Lab Cost

ResourceConfigurationFree Tier CostStandard Account (Past Free Tier)
Amazon S3 Storage1 Standard Bucket (metadata storage only)$0.00$0.00 (Negligible < $0.001)
AWS IAM Roles & Policies1 IAM Role for State Demo$0.00$0.00 (Always 100% Free)
Random Provider ResourcesIn-memory cryptographic IDs$0.00$0.00 (Free Provider)
Compute & NetworkingZero EC2 instances or NAT Gateways$0.00$0.00
Total Estimated CostComplete Hands-On Session (~1 hour)$0.00$0.00

[!NOTE] This lab relies primarily on zero-cost AWS metadata resources (S3, IAM) and the local OpenTofu state engine. Whether your account is within its first 12 months or well past Free Tier, the total cost of completing this lab is $0.00.


🔬 What Exactly Is OpenTofu State?

OpenTofu is a declarative tool. You declare what you want the world to look like, not how to build it.

However, the AWS API is imperative:

  • POST /CreateBucket
  • GET /DescribeInstances
  • DELETE /SecurityGroup

To bridge the gap between your declarative code and AWS’s imperative APIs, OpenTofu requires a mapping mechanism:

  [ Declarative Code (.tf) ]
      aws_s3_bucket.data_lake
                 │
                 ▼
  [ Single Source of Truth ]  <---- terraform.tfstate (JSON Mapping)
                 │                  Maps "aws_s3_bucket.data_lake"
                 │                  to AWS ARN: "arn:aws:s3:::corp-data-lake-9921"
                 ▼
  [ Real Cloud Infrastructure ]
      AWS S3 Bucket in us-east-1

The Three Responsibilities of State

  1. Resource Mapping: OpenTofu maps the human-readable HCL name (aws_s3_bucket.data_lake) to the physical AWS identifier (e.g., corp-data-lake-9921). Without state, OpenTofu would have no idea which of the 500 S3 buckets in your AWS account belongs to this specific project.
  2. Tracking Metadata & Dependencies: State tracks the Directed Acyclic Graph (DAG) of dependencies. If an EC2 instance depends on a Subnet, and that Subnet depends on a VPC, OpenTofu records these relationships so it knows the exact sequence in which to destroy or recreate resources.
  3. Performance Optimization (Caching): In large environments with thousands of resources, querying the AWS API for every single attribute on every run would trigger AWS API rate limits (HTTP 429 Too Many Requests). State caches resource attributes locally so planning can be evaluated rapidly.

🛠️ Step 1: Project Setup

Create a dedicated workspace directory for Lab 112:

mkdir -p ~/opentofu-labs/lab-112-opentofu-state
cd ~/opentofu-labs/lab-112-opentofu-state

We will create three files:

  1. providers.tf — AWS and Random providers.
  2. main.tf — Multi-resource topology containing an S3 bucket, an IAM role, and a sensitive token.
  3. outputs.tf — Outputs to highlight sensitive masking behavior.

📄 Step 2: Provider Configuration

Create providers.tf:

# providers.tf
terraform {
  required_version = ">= 1.8.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.50"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.6"
    }
  }
}

provider "aws" {
  region = "us-east-1"

  default_tags {
    tags = {
      Project     = "OpenTofu-Level-100"
      Lab         = "112-Understanding-State"
      ManagedBy   = "OpenTofu"
      Environment = "Lab"
    }
  }
}

📦 Step 3: Infrastructure Configuration with Secrets

Create main.tf:

# main.tf

# 1. Generate a random suffix for globally unique S3 bucket naming
resource "random_id" "bucket_suffix" {
  byte_length = 4
}

# 2. Generate a simulated sensitive database password / API token
resource "random_password" "db_auth_token" {
  length  = 24
  special = true
}

# 3. Create an S3 Storage Bucket
resource "aws_s3_bucket" "audit_logs" {
  bucket        = "lab112-audit-logs-${random_id.bucket_suffix.hex}"
  force_destroy = true

  tags = {
    Name        = "Lab 112 Audit Logs"
    Description = "Storage for state demonstration"
  }
}

# 4. Create an IAM Role
resource "aws_iam_role" "app_service" {
  name = "lab112-app-service-role-${random_id.bucket_suffix.hex}"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
      }
    ]
  })
}

📤 Step 4: Outputs

Create outputs.tf:

# outputs.tf
output "bucket_name" {
  description = "Name of the provisioned S3 bucket"
  value       = aws_s3_bucket.audit_logs.id
}

output "role_arn" {
  description = "ARN of the provisioned IAM role"
  value       = aws_iam_role.app_service.arn
}

output "db_token" {
  description = "Simulated secret database token"
  value       = random_password.db_auth_token.result
  sensitive   = true # Masked in terminal output!
}

🚀 Step 5: Initialize and Apply

Initialize the directory:

tofu init

Notice what files exist right now:

ls -la

Only your .tf files and the .terraform / .terraform.lock.hcl provider cache exist. There is no terraform.tfstate file yet.

Now apply the configuration:

tofu apply -auto-approve

Look at the terminal output for our outputs:

Apply complete! Resources: 4 added, 0 changed, 0 destroyed.

Outputs:

bucket_name = "lab112-audit-logs-a1b2c3d4"
db_token = <sensitive>
role_arn = "arn:aws:iam::123456789012:role/lab112-app-service-role-a1b2c3d4"

Notice that OpenTofu printed db_token = <sensitive>. It masked the password in your terminal to protect your screen from bystanders.

Now list the directory again:

ls -la

You will see a brand new file: terraform.tfstate.


🧬 Step 6: Deconstructing the State File JSON

Let’s open terraform.tfstate and inspect its anatomy. You can view it using cat or format it with jq:

cat terraform.tfstate

The state file is standard, human-readable JSON. Let’s dissect the critical fields:

{
  "version": 4,
  "terraform_version": "1.8.2",
  "serial": 1,
  "lineage": "9b12cf34-5678-4abc-def0-1234567890ab",
  "outputs": { ... },
  "resources": [ ... ]
}

1. version: The State Format Specification

"version": 4 represents the schema format used by the OpenTofu/Terraform engine to parse state files. Version 4 has been the gold standard since Terraform 0.12.

2. serial: The Monotonic Monologue

"serial": 1 is an integer that increments by 1 every single time an apply or state-modifying command executes.

  • When you first create infrastructure: serial = 1.
  • When you add a tag and re-apply: serial = 2.
  • When you destroy a resource: serial = 3.

[!IMPORTANT] Why serial matters: In remote backends (like AWS S3 with DynamoDB state locking), the serial number prevents split-brain corruption. If Alice runs tofu apply at serial = 5, and Bob tries to apply against an outdated local cache with serial = 4, OpenTofu rejects Bob’s run with a state conflict error!

3. lineage: The Project’s Unique DNA

"lineage" is a randomly generated UUID assigned when the state file is born. It permanently identifies this specific infrastructure lifecycle. If you accidentally point a project to the wrong state backend, OpenTofu checks the lineage. If the UUID does not match, it halts immediately to prevent overwriting someone else’s infrastructure.

4. resources: The Resource Array

Under "resources", each managed asset is cataloged:

{
  "mode": "managed",
  "type": "aws_s3_bucket",
  "name": "audit_logs",
  "provider": "provider[\"registry.opentofu.org/hashicorp/aws\"]",
  "instances": [
    {
      "schema_version": 0,
      "attributes": {
        "arn": "arn:aws:s3:::lab112-audit-logs-a1b2c3d4",
        "bucket": "lab112-audit-logs-a1b2c3d4",
        "id": "lab112-audit-logs-a1b2c3d4",
        "region": "us-east-1",
        "tags": {
          "Description": "Storage for state demonstration",
          "Environment": "Lab",
          "Lab": "112-Understanding-State",
          "ManagedBy": "OpenTofu",
          "Name": "Lab 112 Audit Logs",
          "Project": "OpenTofu-Level-100"
        }
      },
      "dependencies": [
        "random_id.bucket_suffix"
      ]
    }
  ]
}

Observe that the state file stores attributes you never wrote in your .tf file, such as:

  • The full Amazon Resource Name (arn)
  • The AWS region
  • The combined default tags merged with resource-specific tags
  • The dependency link to random_id.bucket_suffix

🚨 The Plaintext Secret Hazard

Earlier, OpenTofu masked db_token as <sensitive> in the terminal.

Now, inspect how that token is stored inside terraform.tfstate. Use jq or grep:

grep -C 3 "result" terraform.tfstate

Or query with jq:

jq '.resources[] | select(.type=="random_password") | .instances[0].attributes.result' terraform.tfstate

Output:

"v8#K9$mP!2xQzL@7wR&4tY*1"

The password is right there in 100% plain, unencrypted text.

[!CAUTION] CRITICAL SECURITY LESSON: Setting sensitive = true in OpenTofu variables or outputs ONLY masks the value from terminal output, plan diffs, and console logs. It does NOT encrypt the state file. Every database password, private TLS key, IAM secret key, and API token managed by OpenTofu is written in plaintext to terraform.tfstate.

How to Protect State Secrets in Production

  1. Never commit state files to Git: Ensure your root .gitignore contains *.tfstate and *.tfstate.*.
  2. Always use Remote Backends: Store state in Amazon S3 with mandatory Server-Side Encryption (aws:kms), TLS-only bucket policies (aws:SecureTransport), and restricted IAM permissions.
  3. Audit Access: Treat access to the state file with the same security classification as root production database access.

🧰 Step 7: Mastering the tofu state CLI

Never edit terraform.tfstate directly in a text editor like VS Code or Vim. A single misplaced comma or schema mismatch will corrupt your state file and strand your infrastructure.

Instead, use the safe, built-in tofu state subcommands:

1. tofu state list

Lists every resource address currently tracked by state:

tofu state list

Output:

aws_iam_role.app_service
aws_s3_bucket.audit_logs
random_id.bucket_suffix
random_password.db_auth_token

2. tofu state show <address>

Inspects all recorded attributes for a single resource without digging through JSON:

tofu state show aws_s3_bucket.audit_logs

Output:

# aws_s3_bucket.audit_logs:
resource "aws_s3_bucket" "audit_logs" {
    arn                         = "arn:aws:s3:::lab112-audit-logs-a1b2c3d4"
    bucket                      = "lab112-audit-logs-a1b2c3d4"
    force_destroy               = true
    id                          = "lab112-audit-logs-a1b2c3d4"
    region                      = "us-east-1"
    tags                        = {
        "Description" = "Storage for state demonstration"
        "Environment" = "Lab"
        "Lab"         = "112-Understanding-State"
        "ManagedBy"   = "OpenTofu"
        "Name"        = "Lab 112 Audit Logs"
        "Project"     = "OpenTofu-Level-100"
    }
}

🔄 Step 8: Refactoring Code Without Destruction (tofu state mv)

Here is a common real-world catastrophe:

You decide that aws_iam_role.app_service is poorly named. You want to rename the HCL block to aws_iam_role.backend_worker.

You open main.tf and edit the name:

# In main.tf - Changing the HCL identifier
resource "aws_iam_role" "backend_worker" {
  name = "lab112-app-service-role-${random_id.bucket_suffix.hex}"
  # ...
}

Now run tofu plan:

Plan: 1 to add, 0 to change, 1 to destroy.

  - destroy aws_iam_role.app_service
  + create aws_iam_role.backend_worker

Disaster! OpenTofu thinks you deleted the old role and created a brand new one. If this were a database or a live storage volume, OpenTofu would destroy your live data!

The Solution: tofu state mv

You can tell OpenTofu: “This is the exact same resource; I just renamed the HCL block in my code.”

Run tofu state mv:

tofu state mv aws_iam_role.app_service aws_iam_role.backend_worker

Output:

Move "aws_iam_role.app_service" to "aws_iam_role.backend_worker"
Successfully moved 1 object(s).

Now update outputs.tf so it references the new address:

# In outputs.tf
output "role_arn" {
  description = "ARN of the provisioned IAM role"
  value       = aws_iam_role.backend_worker.arn
}

Now run tofu plan:

tofu plan

Output:

No changes. Your infrastructure matches the configuration.

Zero downtime. Zero destruction. You refactored your infrastructure code safely!


✂️ Step 9: Decoupling Resources Safely (tofu state rm)

What if your company decides that a particular resource (e.g., an S3 bucket or database) should no longer be managed by OpenTofu, but must NOT be deleted from AWS?

If you simply delete the aws_s3_bucket block from main.tf and run tofu apply, OpenTofu will call the AWS API and delete the bucket from Amazon Web Services!

The Solution: tofu state rm

tofu state rm removes the resource from OpenTofu’s state tracking without touching the live asset in AWS.

Let’s remove the S3 bucket from state:

tofu state rm aws_s3_bucket.audit_logs

Output:

Removed aws_s3_bucket.audit_logs
Successfully removed 1 resource instance(s).

Now list your state resources:

tofu state list

Output:

aws_iam_role.backend_worker
random_id.bucket_suffix
random_password.db_auth_token

The bucket is gone from state!

Now check AWS directly using the AWS CLI:

aws s3 ls | grep lab112-audit-logs

The bucket is still alive and healthy in AWS! OpenTofu has simply forgotten about it.


🧹 Step 10: Clean Teardown

Because we removed the S3 bucket from OpenTofu state, tofu destroy will not delete that bucket. Let’s delete the bucket manually using the AWS CLI, and let OpenTofu clean up the rest:

1. Delete the orphaned bucket via AWS CLI

BUCKET=$(aws s3 ls | grep lab112-audit-logs | awk '{print $3}')
if [ -n "$BUCKET" ]; then
  aws s3 rb s3://$BUCKET --force
  echo "Deleted orphaned bucket: $BUCKET"
fi

2. Destroy remaining OpenTofu resources

tofu destroy -auto-approve

Output:

aws_iam_role.backend_worker: Destroying...
aws_iam_role.backend_worker: Destruction complete after 2s
random_password.db_auth_token: Destroying...
random_id.bucket_suffix: Destroying...
Destroy complete! Resources: 3 destroyed.

3. Observe the final state file

Look at terraform.tfstate now:

cat terraform.tfstate

Notice that "resources": [] is now an empty array. The serial number has incremented, but all resource tracking has been cleanly wiped.


🏋️ Hands-On Challenge

Solidify your state mastery with this challenge:

  1. In a new folder or scratch directory, create an aws_ssm_parameter named /config/app_key storing a dummy string "super-secret-123".
  2. Run tofu apply and use tofu state show to inspect the parameter.
  3. Use jq to locate the secret value inside terraform.tfstate.
  4. Refactor the resource block name in your .tf file from aws_ssm_parameter.config to aws_ssm_parameter.core_config and use tofu state mv to prevent OpenTofu from deleting and recreating it.
  5. Tear down the resources with tofu destroy.

📖 Summary & Key Takeaways

In this lab, you demystified the internal mechanics of OpenTofu state:

  1. State as the Single Source of Truth: State maps declarative HCL names to live AWS physical identifiers (ARNs, IDs, VPC endpoints).
  2. serial and lineage: The serial number increments monotonically to prevent split-brain write collisions; lineage guarantees that operations target the correct infrastructure lifecycle.
  3. The Plaintext Secret Reality: The sensitive = true attribute only masks values in CLI stdout. All secrets are stored in cleartext inside terraform.tfstate. State files must always be encrypted and protected like root credentials.
  4. The tofu state CLI Suite:
    • tofu state list: Enumerate tracked resources.
    • tofu state show: Inspect live cached attributes.
    • tofu state mv: Refactor code safely without destroying live cloud infrastructure.
    • tofu state rm: Untrack and decouple resources from OpenTofu without deleting them in AWS.
  5. Never Edit State by Hand: Direct manual edits risk syntax corruption and schema invalidation. Always use tofu state commands.

🚀 What’s Next? (Lab 113)

What happens when an engineer logs into the AWS Web Console and manually deletes a security group rule, or alters an S3 bucket policy behind OpenTofu’s back?

This is known as Infrastructure Drift.

In [AWS OpenTofu #113: Drift Detection & Resource Import], we will:

  • Simulate out-of-band ClickOps drift on live AWS resources.
  • Use tofu plan and tofu refresh to detect unauthorized changes.
  • Reconcile drift cleanly back to our declarative baseline.
  • Use the modern import block to adopt pre-existing, unmanaged AWS resources into OpenTofu code.

Related & Recommended Guides

Continue exploring related systems architectures and engineering field notes.