100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Terraform & Infrastructure as Code
50 minintermediate

State Practice — Remote Backend Setup

What You'll Build

You will bootstrap a remote Terraform backend using a dedicated bootstrap configuration, then migrate the CricketPulse infrastructure from local state to the remote S3 backend. You will verify that state locking works by simulating a concurrent apply scenario, use `terraform state` commands to inspect and manipulate state, and practice the production plan-file workflow. By the end of this exercise, the CricketPulse infrastructure is managed with a production-grade state configuration that supports team collaboration.

Analogy🏏Cricket
🏏 Think of it like cricket: This exercise is your first bat at the nets — not a match, but the first real contact between bat and ball. You're not yet managing complex match strategies or multi-ground tournaments; you're simply practicing the forward defensive — the fundamental stroke. Provisioning a single VM with its network prerequisites is the forward defensive of Terraform: not glamorous, but the foundation of every subsequent shot. Each command (init, plan, apply) is a distinct movement to practise until it becomes automatic, because every future Terraform operation builds on this same three-step sequence.

Prerequisites

  • Completed Lesson 4 exercise — the CricketPulse EC2 infrastructure with local state.
  • AWS account with permissions to create S3 buckets and DynamoDB tables.
  • Terraform CLI >= 1.7.0 installed.
  • AWS CLI installed for verifying S3 bucket contents — `brew install awscli` or download from aws.amazon.com/cli.
  • Understanding of S3 bucket versioning and DynamoDB from Lessons 5–7.

Setup & Project Structure

Create a dedicated bootstrap configuration in a separate directory for the state backend infrastructure. This separation is essential — the bootstrap config manages the S3 bucket and DynamoDB table; the main config uses them.

Analogy🏏Cricket
🏏 Think of it like cricket: setting up separate files for providers, network, and compute is like organising your kit bag before a match with distinct compartments — pads in one, gloves in another, bat and helmet each in their place. Just as a well-ordered kit bag lets you grab exactly the gear you need without rummaging, splitting your Terraform config by concern lets you find the network or provider settings instantly. Just as every player knows the batting order is written down so nobody argues about who's next in, the file-per-concern convention means any teammate reading your CricketPulse config knows precisely where the compute or networking lives. And just as a tidy kit bag survives a chaotic changeroom, a readable structure survives a growing configuration. The payoff: like a professional who never scrambles for a missing glove at the crease, you keep the config navigable and calm as the infrastructure grows.
bash
# Project structure
mkdir -p cricketpulse-infra/bootstrap
mkdir -p cricketpulse-infra/infrastructure

# The bootstrap config creates the state backend
# The infrastructure config uses the created backend
tree cricketpulse-infra
# cricketpulse-infra/
#   bootstrap/
#     main.tf           # Creates S3 bucket + DynamoDB table
#     providers.tf      # Local state (bootstrap uses local state!)
#     outputs.tf
#   infrastructure/
#     providers.tf      # Uses the remote backend created by bootstrap
#     main.tf           # CricketPulse EC2 + security group
#     outputs.tf

cd cricketpulse-infra

Step 1 — Foundation

Write and apply the bootstrap configuration that creates the S3 bucket (with versioning and encryption) and DynamoDB table for state locking. The bootstrap configuration itself uses local state — a necessary compromise for the initial setup.

Analogy🏏Cricket
🏏 Think of it like cricket: Step 1 builds the ground infrastructure before the match begins. The VPC is the stadium boundary walls — the outer container that defines the playing area. The subnet is the designated field within the boundary. The internet gateway is the public entrance gate that allows spectators (traffic) to enter. You build the ground infrastructure before placing the players (EC2 instances) on the field.
hcl
# bootstrap/providers.tf
terraform {
  required_version = ">= 1.7.0"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.31" }
  }
  # No backend block — bootstrap uses LOCAL state intentionally
}

provider "aws" {
  region = "ap-south-1"
}

# bootstrap/main.tf
resource "aws_s3_bucket" "tf_state" {
  bucket        = "cricketpulse-tf-state-${data.aws_caller_identity.current.account_id}"
  force_destroy = false  # Prevent accidental deletion
  tags = { Name = "Terraform State", ManagedBy = "terraform-bootstrap" }
}

data "aws_caller_identity" "current" {}

resource "aws_s3_bucket_versioning" "tf_state" {
  bucket = aws_s3_bucket.tf_state.id
  versioning_configuration { status = "Enabled" }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "tf_state" {
  bucket = aws_s3_bucket.tf_state.id
  rule {
    apply_server_side_encryption_by_default { sse_algorithm = "AES256" }
  }
}

resource "aws_s3_bucket_public_access_block" "tf_state" {
  bucket = aws_s3_bucket.tf_state.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_dynamodb_table" "tf_locks" {
  name         = "cricketpulse-tf-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"
  attribute { name = "LockID"; type = "S" }
  tags = { Name = "Terraform State Locks" }
}

# bootstrap/outputs.tf
output "state_bucket" { value = aws_s3_bucket.tf_state.id }
output "lock_table"   { value = aws_dynamodb_table.tf_locks.name }

Step 2 — Core Logic

Apply the bootstrap configuration and then configure the infrastructure config to use the remote backend. Migrate existing local state to the remote backend using `terraform init -migrate-state`.

Analogy🏏Cricket
🏏 Think of it like cricket: Step 2 is placing the players on the prepared ground. The security group is the access accreditation policy — which players (traffic sources) are allowed through which gates (ports). The EC2 instance is the opening batsman taking the crease. The apply command is the toss coin flip that starts the match — the moment the plan becomes real infrastructure.
bash
# Apply bootstrap (creates S3 bucket + DynamoDB table)
cd bootstrap
terraform init && terraform apply -auto-approve

# Note the outputs
STATE_BUCKET=$(terraform output -raw state_bucket)
echo "State bucket: $STATE_BUCKET"

cd ../infrastructure

# infrastructure/providers.tf (with remote backend)
cat > providers.tf << EOF
terraform {
  required_version = ">= 1.7.0"
  required_providers {
    aws = { source = "hashicorp/aws", version = "~> 5.31" }
  }
  backend "s3" {
    bucket         = "$STATE_BUCKET"
    key            = "cricketpulse/dev/terraform.tfstate"
    region         = "ap-south-1"
    dynamodb_table = "cricketpulse-tf-locks"
    encrypt        = true
  }
}

provider "aws" {
  region = "ap-south-1"
  default_tags {
    tags = { ManagedBy = "terraform", Project = "cricketpulse" }
  }
}
EOF

# If you have existing local state, migrate it:
terraform init -migrate-state
# Initializing the backend...
# Do you want to copy existing state to the new backend? yes
# Successfully configured the backend 'S3'!
# State migrated to remote backend.

# Verify state is in S3
aws s3 ls s3://$STATE_BUCKET/cricketpulse/dev/
# terraform.tfstate (should appear in S3)

Step 3 — Integration & Enhancement

Verify state locking by opening two terminals and running `terraform apply` simultaneously. Practice the production plan-file workflow with the remote backend.

Analogy🏏Cricket
🏏 Think of it like cricket: Step 3 is making a tactical adjustment mid-match — changing the batting position (instance type) of an already-fielded player. Some changes are in-place (the batsman changes their stance without leaving the crease — `~` in the plan). Others require the player to leave and re-enter (the player goes back to the pavilion and returns — `-/+` replace in the plan). Terraform's plan output tells you which type of change is required before you commit to it.
bash
# Simulate state locking (two terminals)

# Terminal 1: Start a slow apply
terraform apply &
# Acquiring state lock... Lock acquired!
# aws_instance.cricketpulse_api: Modifying...

# Terminal 2: Try to apply simultaneously
terraform apply
# Error: Error acquiring the state lock
# Error message: ConditionalCheckFailedException
# Lock Info:
#   ID:        xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
#   Path:      cricketpulse-tf-state.../cricketpulse/dev/terraform.tfstate
#   Operation: OperationTypeApply
#   Who:       user@hostname
#   Created:   2024-01-15 10:30:00
# ← Second apply correctly blocked

# Production plan-file workflow
# Step 1: Generate plan file
terraform plan -out=cricketpulse-$(date +%Y%m%d).plan

# Step 2: Review the saved plan
terraform show cricketpulse-$(date +%Y%m%d).plan | head -20

# Step 3: Apply the saved plan (guarantees exact reviewed actions)
terraform apply cricketpulse-$(date +%Y%m%d).plan

# Verify state in S3 (should show new version)
aws s3api list-object-versions \
  --bucket $STATE_BUCKET \
  --prefix cricketpulse/dev/terraform.tfstate \
  --query 'Versions[].{ID:VersionId,Date:LastModified}' \
  --output table

Step 4 — Testing & Verification

Inspect state commands and clean up the exercise.

Analogy🏏Cricket
🏏 Think of it like cricket: verifying the provisioned VM works, inspecting the state file, then destroying it cleanly is like the post-innings routine — confirm the scoreboard matches what happened on the field, review the official scorebook ball by ball, then clear and cover the ground so nothing is left running overnight. Just as you'd check the batsman's recorded runs actually reflect the shots played, you verify the infrastructure Terraform claims to have built is genuinely reachable and functioning. Just as the scorebook is the authoritative record you consult to settle any dispute, the state file is where you inspect exactly what Terraform tracks as existing. And just as a responsible side clears its kit and covers the pitch after stumps to avoid damage and cost, `terraform destroy` tears everything down cleanly so no resources keep billing you. The payoff: like a match properly closed out and the ground restored, you confirm the work, understand the record, and leave a clean slate behind.
bash
# State inspection commands
terraform state list
terraform state show aws_instance.cricketpulse_api
terraform state pull | python3 -m json.tool | head -30

# Verify drift detection
terraform plan -refresh-only
# Should show no changes if infrastructure matches state

# Clean up infrastructure
terraform destroy -auto-approve

# Clean up bootstrap (careful: this deletes the state bucket)
# Only do this when you're completely done
cd ../bootstrap
# aws s3 rm s3://$STATE_BUCKET --recursive  # Empty bucket first
# terraform destroy -auto-approve

echo 'State exercise complete!'

Warning: When using the S3 remote backend, the state bucket name must be globally unique across all AWS accounts. Using your AWS account ID as a suffix (as shown in the bootstrap configuration) ensures uniqueness: `cricketpulse-tf-state-123456789012`. If you choose a bucket name that already exists in any other account, the S3 bucket creation will fail with a 'BucketAlreadyExists' error. Do NOT delete the bootstrap state (the local terraform.tfstate in the bootstrap directory) — you'll need it to manage the state bucket and DynamoDB table later.

Extension Challenge: Configure S3 bucket lifecycle rules on the state bucket to transition old state versions to S3 Glacier after 30 days and expire them after 90 days. This reduces storage costs while maintaining recovery capability. Also add an S3 bucket notification that sends an SNS alert whenever the state file is modified — this creates an audit trail of when Terraform apply operations occurred. Configure the SNS topic to send an email to your team's infrastructure alerts distribution list.

  • The bootstrap pattern creates the state backend infrastructure (S3+DynamoDB) using local state, then all subsequent configurations use the remote backend.
  • `terraform init -migrate-state` safely moves existing local state to the newly configured remote backend — run it after adding the backend block to providers.tf.
  • State locking verification is possible by running two simultaneous applies — the second should fail with 'Error acquiring state lock' immediately.
  • The production plan-file workflow (`plan -out=file`, then `apply file`) guarantees that exactly the reviewed plan is applied, even if infrastructure changed between plan and apply.
  • Enable S3 bucket versioning on the state bucket to create automatic backups of every state version — essential for recovery from state corruption.
  • The S3 state bucket must be globally unique — use your AWS account ID as a suffix to guarantee uniqueness across all accounts.
Lesson 8 of 24
0% complete