Terraform State and Providers: How Terraform Remembers and Connects – T3

Welcome back! So far, you’ve learned what Terraform is and how to use variables in Terraform to make your code flexible.

Now it’s time to understand two crucial concepts: State and Providers.

Think of state as Terraform’s memory, and providers as Terraform’s way of talking to different services. Without these, Terraform would be like a person with amnesia trying to order food in a foreign language – not very effective!

What is Terraform State?

Every time you run terraform apply, Terraform needs to remember what it created. That’s where state comes in.

Let’s see this in action with a simple example:

resource "local_file" "example" {
  content  = "Hello from Terraform!"
  filename = "example.txt"
}

Run this:

terraform init
terraform apply

Now look in your folder. You’ll see two things:

  1. example.txt – the file Terraform created
  2. terraform.tfstate – this is Terraform’s memory!

Looking Inside the State File

Let’s peek inside terraform.tfstate. It’s a JSON file that looks something like this:

{
  "version": 4,
  "terraform_version": "1.12.0",
  "resources": [
    {
      "mode": "managed",
      "type": "local_file",
      "name": "example",
      "instances": [
        {
          "attributes": {
            "content": "Hello from Terraform!",
            "filename": "example.txt"
          }
        }
      ]
    }
  ]
}

What does this mean?

  • Terraform remembers it created a local_file called example
  • It knows the content and filename
  • It can compare this with your current configuration

Why Does Terraform Need State?

Let’s see what happens when you change your configuration:

resource "local_file" "example" {
  content  = "Hello from Updated Terraform!"  # Changed this line
  filename = "example.txt"
}

Run terraform plan:

Terraform will perform the following actions:

  # local_file.example will be updated in-place
  ~ resource "local_file" "example" {
      ~ content = "Hello from Terraform!" -> "Hello from Updated Terraform!"
        # (1 unchanged attribute hidden)
    }

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

See what happened? Terraform compared your new configuration with what’s in the state file and figured out it needs to update the file. Pretty smart!

State Helps Terraform Make Decisions

State helps Terraform answer three important questions:

  1. What exists? – What resources has Terraform created?
  2. What changed? – How is your new configuration different?
  3. What to do? – Should it create, update, or delete resources?

Let’s see this with another example:

variable "create_file" {
  type    = bool
  default = true
}

resource "local_file" "conditional" {
  count    = var.create_file ? 1 : 0
  content  = "This file may or may not exist"
  filename = "conditional.txt"
}

New concept: count tells Terraform how many of this resource to create. If var.create_file is true, create 1. If false, create 0 (none).

Try this:

# Create the file
terraform apply

# Now remove it
terraform apply -var="create_file=false"

Terraform knows to delete the file because the state tells it the file used to exist!

The Problem with Local State

Local state (the terraform.tfstate file on your computer) works fine when you’re working alone. But what happens when:

  • You work in a team?
  • You want to run Terraform from different computers?
  • Your computer crashes?

That’s where remote state comes in.

Remote State: Sharing Terraform’s Memory

Remote state stores the state file in a shared location like:

  • AWS S3 bucket
  • Azure Storage Account
  • Google Cloud Storage
  • Terraform Cloud

Here’s a simple example using Terraform Cloud (free for small teams):

terraform {
  cloud {
    organization = "your-org-name"
    workspaces {
      name = "my-first-workspace"
    }
  }
}

resource "local_file" "shared" {
  content  = "This state is stored remotely!"
  filename = "shared.txt"
}

What’s new here:

  • terraform block configures Terraform itself
  • cloud section tells Terraform to store state in Terraform Cloud
  • Replace your-org-name with your actual organization name

What Are Providers?

Now let’s talk about providers. Providers are like translators that help Terraform talk to different services.

We’ve been using the local provider (for creating files), but there are providers for:

  • AWS (Amazon Web Services)
  • Azure (Microsoft Cloud)
  • Google Cloud Platform
  • Kubernetes
  • And hundreds more!

Your First Real Provider: AWS

Let’s try creating something in AWS. First, you need AWS credentials set up, but don’t worry – we’ll keep it simple.

# Configure the AWS provider
provider "aws" {
  region = "us-west-2"
}

# Create an S3 bucket
resource "aws_s3_bucket" "my_bucket" {
  bucket = "my-unique-terraform-bucket-12345"  # Must be globally unique
}

What’s new:

  • provider "aws" configures the AWS provider
  • region tells AWS which region to use
  • aws_s3_bucket creates a storage bucket in AWS
  • Bucket names must be globally unique across all of AWS

Provider Authentication

Before AWS provider works, you need to authenticate. Here are the simple ways:

Method 1: AWS CLI (Easiest) If you have AWS CLI installed:

aws configure
# Enter your credentials when prompted

Method 2: Environment Variables

export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"

Method 3: In the Provider (Not recommended for production)

provider "aws" {
  region     = "us-west-2"
  access_key = "your-access-key"
  secret_key = "your-secret-key"
}

A Complete Example with Variables

Let’s combine everything we’ve learned:

# Variables for flexibility
variable "bucket_name" {
  type        = string
  description = "Name of the S3 bucket"
  default     = "my-terraform-bucket"
}

variable "aws_region" {
  type        = string
  description = "AWS region"
  default     = "us-west-2"
}

# Configure the provider
provider "aws" {
  region = var.aws_region
}

# Create resources
resource "aws_s3_bucket" "example" {
  bucket = "${var.bucket_name}-${random_string.suffix.result}"
}

resource "random_string" "suffix" {
  length  = 8
  special = false
  upper   = false
}

# Output information
output "bucket_name" {
  value = aws_s3_bucket.example.bucket
}

output "bucket_region" {
  value = var.aws_region
}

New concepts:

  • random_string resource creates random text (helpful for unique names)
  • length = 8 makes it 8 characters long
  • special = false means no special characters
  • upper = false means no uppercase letters

Understanding Provider Requirements

When you use a new provider, you need to tell Terraform about it:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
    random = {
      source  = "hashicorp/random"
      version = "~> 3.1"
    }
  }
}

What this means:

  • source = "hashicorp/aws" – where to download the AWS provider
  • version = "~> 5.0" – use version 5.0 or newer (but not 6.0)

Provider Versions: Why They Matter

Providers get updated regularly. Specifying versions prevents surprises:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "= 5.31.0"  # Exact version
    }
  }
}

Version formats:

  • = 5.31.0 – exactly this version
  • >= 5.31.0 – this version or newer
  • ~> 5.31.0 – this version up to (but not including) 5.32.0

Local vs Remote State in Practice

Let’s see the difference:

Local State Setup:

# No backend configuration - uses local state
provider "aws" {
  region = "us-west-2"
}

resource "aws_s3_bucket" "local_state_bucket" {
  bucket = "local-state-example-12345"
}

Remote State Setup (S3 Backend):

terraform {
  backend "s3" {
    bucket = "my-terraform-state-bucket"
    key    = "terraform.tfstate"
    region = "us-west-2"
  }
}

provider "aws" {
  region = "us-west-2"
}

resource "aws_s3_bucket" "remote_state_bucket" {
  bucket = "remote-state-example-12345"
}

What’s new:

  • backend "s3" stores state in an S3 bucket
  • key is the filename for the state file
  • You need to create the state bucket first!

State Locking: Preventing Team Conflicts

When multiple people work on the same Terraform project, state locking prevents disasters. Imagine two people running terraform apply at the same time – chaos!

terraform {
  backend "s3" {
    bucket         = "my-terraform-state-bucket"
    key            = "terraform.tfstate"
    region         = "us-west-2"
    dynamodb_table = "terraform-locks"  # This enables locking!
  }
}

What happens with locking:

  1. Person A runs terraform apply → Terraform creates a lock
  2. Person B tries to run terraform apply → Gets an error “State is locked”
  3. Person A finishes → Lock is released
  4. Person B can now run their command

Note: You need to create the DynamoDB table first for S3 backend locking.

State File Best Practices

Here are the golden rules for managing state files:

🔒 Security: • Never commit state files to version control (add *.tfstate* to .gitignore) • State files contain sensitive information (passwords, keys, etc.) • Use remote state with encryption enabled • Restrict access to state storage (S3 bucket permissions, etc.)

👥 Team Collaboration: • Always use remote state for team projects • Enable state locking to prevent conflicts • Use consistent backend configuration across team • Document state backend setup for new team members

💾 Backup and Recovery: • Enable versioning on S3 buckets storing state • Regular backups of state files • Test state recovery procedures • Keep multiple backup copies in different locations

🔧 Maintenance: • Use terraform refresh to sync state with reality • Clean up unused resources regularly • Monitor state file size (large states slow down operations) • Split large projects into multiple state files

⚠️ What NOT to do: • Never manually edit state files • Don’t delete state files without proper backup • Avoid storing secrets directly in Terraform configuration • Don’t ignore state locking errors

Essential State Management Commands

Here are the most useful commands for working with state:

Basic State Inspection

# See what's in your state
terraform show

# List all resources in state
terraform state list

# Get detailed info about a specific resource
terraform state show aws_s3_bucket.example

# Refresh state from real world (sync with actual resources)
terraform refresh

Moving and Renaming Resources

# Move a resource to a new name in state
terraform state mv aws_s3_bucket.old_name aws_s3_bucket.new_name

# Move resource to a different module
terraform state mv aws_s3_bucket.example module.storage.aws_s3_bucket.example

Example of when to use state mv:

# You had this:
resource "aws_s3_bucket" "storage" {
  bucket = "my-bucket"
}

# You want to rename it to:
resource "aws_s3_bucket" "main_storage" {
  bucket = "my-bucket"
}

# Use: terraform state mv aws_s3_bucket.storage aws_s3_bucket.main_storage

Removing Resources from State

# Remove resource from state (but keep the actual resource)
terraform state rm aws_s3_bucket.unwanted

# Import existing resource into state
terraform import aws_s3_bucket.example my-existing-bucket-name

State Backup and Recovery

# Create a backup of current state
terraform state pull > backup.tfstate

# Push a state file to remote backend
terraform state push backup.tfstate

Using Multiple Cloud Providers

You can use multiple providers in the same configuration:

# Configure multiple providers
provider "aws" {
  region = "us-west-2"
}

provider "azure" {
  features {}
}

provider "google" {
  project = "my-gcp-project"
  region  = "us-central1"
}

# Use resources from different providers
resource "aws_s3_bucket" "aws_storage" {
  bucket = "my-aws-bucket-12345"
}

resource "azurerm_storage_account" "azure_storage" {
  name                     = "myazurestorage12345"
  resource_group_name      = "my-resource-group"
  location                 = "East US"
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

resource "google_storage_bucket" "gcp_storage" {
  name     = "my-gcp-bucket-12345"
  location = "US"
}

Provider Aliases: Same Provider, Different Configurations

Sometimes you need the same provider with different settings. Use aliases:

# Default AWS provider for us-west-2
provider "aws" {
  region = "us-west-2"
}

# AWS provider for us-east-1 with alias
provider "aws" {
  alias  = "east"
  region = "us-east-1"
}

# AWS provider for Europe with alias
provider "aws" {
  alias  = "europe"
  region = "eu-west-1"
}

# Use default provider (no alias needed)
resource "aws_s3_bucket" "west_bucket" {
  bucket = "my-west-bucket-12345"
}

# Use aliased providers
resource "aws_s3_bucket" "east_bucket" {
  provider = aws.east
  bucket   = "my-east-bucket-12345"
}

resource "aws_s3_bucket" "europe_bucket" {
  provider = aws.europe
  bucket   = "my-europe-bucket-12345"
}

What’s new:

  • alias = "east" creates a named version of the provider
  • provider = aws.east tells the resource which provider to use
  • You can have as many aliases as you need

Real-World Multi-Region Example

Here’s a practical example creating backups across regions:

variable "bucket_name" {
  type        = string
  description = "Base name for buckets"
  default     = "myapp-data"
}

# Primary region provider
provider "aws" {
  region = "us-west-2"
}

# Backup region provider
provider "aws" {
  alias  = "backup"
  region = "us-east-1"
}

# Random suffix for unique names
resource "random_string" "suffix" {
  length  = 6
  special = false
  upper   = false
}

# Primary bucket
resource "aws_s3_bucket" "primary" {
  bucket = "${var.bucket_name}-primary-${random_string.suffix.result}"
}

# Backup bucket in different region
resource "aws_s3_bucket" "backup" {
  provider = aws.backup
  bucket   = "${var.bucket_name}-backup-${random_string.suffix.result}"
}

# Enable versioning on both buckets
resource "aws_s3_bucket_versioning" "primary" {
  bucket = aws_s3_bucket.primary.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_versioning" "backup" {
  provider = aws.backup
  bucket   = aws_s3_bucket.backup.id
  versioning_configuration {
    status = "Enabled"
  }
}

# Outputs
output "primary_bucket" {
  value = {
    name   = aws_s3_bucket.primary.bucket
    region = "us-west-2"
  }
}

output "backup_bucket" {
  value = {
    name   = aws_s3_bucket.backup.bucket
    region = "us-east-1"
  }
}

Multi-Provider with Different Accounts

You can even use different AWS accounts:

# Production account
provider "aws" {
  region = "us-west-2"
  # Uses default credentials
}

# Development account
provider "aws" {
  alias  = "dev"
  region = "us-west-2"
  profile = "dev-account"  # Different AWS CLI profile
}

# Staging account  
provider "aws" {
  alias  = "staging"
  region = "us-west-2"
  assume_role {
    role_arn = "arn:aws:iam::123456789012:role/TerraformRole"
  }
}

# Resources in different accounts
resource "aws_s3_bucket" "prod" {
  bucket = "prod-bucket-12345"
}

resource "aws_s3_bucket" "dev" {
  provider = aws.dev
  bucket   = "dev-bucket-12345"
}

resource "aws_s3_bucket" "staging" {
  provider = aws.staging
  bucket   = "staging-bucket-12345"
}

Practical State Command Examples

Let’s see these commands in action:

Scenario 1: Renaming a Resource

# Original configuration
resource "aws_s3_bucket" "data" {
  bucket = "my-data-bucket-12345"
}

# You want to rename it to be more specific
resource "aws_s3_bucket" "user_data" {
  bucket = "my-data-bucket-12345"
}

Commands to run:

# 1. Move the resource in state
terraform state mv aws_s3_bucket.data aws_s3_bucket.user_data

# 2. Plan to verify no changes needed
terraform plan

# Should show "No changes" because we just renamed it

Scenario 2: Resource Created Outside Terraform

# Someone created a bucket manually, now you want Terraform to manage it
terraform import aws_s3_bucket.imported_bucket actual-bucket-name

# Verify it's now in state
terraform state show aws_s3_bucket.imported_bucket

Scenario 3: Cleaning Up State

# Remove resource from state but keep the actual resource
terraform state rm aws_s3_bucket.temporary

# List what's left in state
terraform state list

# Refresh state to catch any external changes
terraform refresh

When Things Go Wrong: State Issues

Sometimes state gets out of sync. Here are simple fixes:

Problem: Real resource was deleted outside of Terraform Solution:

terraform plan  # Will show it needs to recreate
terraform apply # Recreates the resource

Problem: You accidentally deleted the state file Solution: You can import existing resources:

terraform import aws_s3_bucket.example my-bucket-name

A Practical Multi-Resource Example

Let’s create a simple web hosting setup:

# Variables
variable "project_name" {
  type        = string
  description = "Name of the project"
  default     = "webapp"
}

# Configure provider
provider "aws" {
  region = "us-west-2"
}

# Random suffix for unique names
resource "random_string" "suffix" {
  length  = 6
  special = false
  upper   = false
}

# S3 bucket for website
resource "aws_s3_bucket" "website" {
  bucket = "${var.project_name}-${random_string.suffix.result}"
}

# Upload a simple HTML file
resource "aws_s3_object" "index" {
  bucket = aws_s3_bucket.website.bucket
  key    = "index.html"
  content = <<-EOF
    <!DOCTYPE html>
    <html>
    <head>
        <title>${var.project_name}</title>
    </head>
    <body>
        <h1>Welcome to ${var.project_name}!</h1>
        <p>This website is hosted on AWS S3 and managed by Terraform.</p>
    </body>
    </html>
  EOF
  content_type = "text/html"
}

# Outputs
output "bucket_name" {
  value = aws_s3_bucket.website.bucket
}

output "website_file" {
  value = "index.html uploaded successfully"
}

New concepts:

  • aws_s3_object uploads a file to the bucket
  • key is the filename in the bucket
  • content_type tells AWS what kind of file it is

Provider Best Practices

Provider Configuration: • Pin provider versions to avoid surprises (version = "~> 5.0") • Use variables for provider configuration (regions, profiles, etc.) • Don’t hardcode credentials in your code • Use one provider block per provider (don’t repeat them) • Use aliases when you need multiple configurations of the same provider

Multi-Provider Tips: • Keep provider configurations at the top of your files • Use consistent naming for provider aliases • Document which provider is used for what purpose • Be careful with cross-provider dependencies

Quick Reference

Local State (default):

# No special configuration needed
# State stored in terraform.tfstate

Remote State with Locking (S3 + DynamoDB):

terraform {
  backend "s3" {
    bucket         = "state-bucket"
    key            = "terraform.tfstate"
    region         = "us-west-2"
    dynamodb_table = "terraform-locks"
  }
}

Single Provider:

provider "aws" {
  region = "us-west-2"
}

Multiple Providers:

provider "aws" {
  region = "us-west-2"
}

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

# Use aliased provider
resource "aws_s3_bucket" "example" {
  provider = aws.east
  bucket   = "my-bucket"
}

Essential State Commands:

terraform state list                    # List all resources
terraform state show <resource>         # Show resource details
terraform state mv <old> <new>          # Rename resource in state
terraform state rm <resource>           # Remove from state
terraform import <resource> <id>        # Import existing resource
terraform refresh                       # Sync state with reality

What’s Next?

Excellent work! You now understand Terraform’s core infrastructure concepts:

State Management – How Terraform tracks your infrastructure:

  • ✅ Local vs remote state
  • ✅ State locking for team collaboration
  • ✅ Essential state commands (mv, rm, import, refresh)
  • ✅ State file best practices and security

Provider Configuration – How Terraform connects to services:

  • ✅ Single and multiple provider setups
  • ✅ Provider aliases for different configurations
  • ✅ Multi-region and multi-account deployments
  • ✅ Version management and authentication

Advanced Operations – Professional state management:

  • ✅ Moving and renaming resources
  • ✅ Importing existing infrastructure
  • ✅ State backup and recovery procedures
  • ✅ Cross-provider resource management

In our next post, we’ll explore Terraform Resources and Data Sources. You’ll learn:

  • Creating different types of cloud resources
  • Using data sources to reference existing resources
  • Building dependencies between resources
  • Reading and using provider documentation effectively
  • Resource lifecycle management

The solid foundation in state and providers you’ve built today will be crucial for managing complex infrastructure!


Ready to start building real cloud infrastructure? Stay tuned for Resources and Data Sources!

Akhilesh Mishra

Akhilesh Mishra

I am Akhilesh Mishra, a self-taught Devops engineer with 11+ years working on private and public cloud (GCP & AWS)technologies.

I also mentor DevOps aspirants in their journey to devops by providing guided learning and Mentorship.

Topmate: https://topmate.io/akhilesh_mishra/