Back to Blog

Setting Up Terraform with Remote S3 Backend & State Locking

A step-by-step guide to configuring a local workstation for HashiCorp Terraform on Windows, establishing an encrypted S3 remote state backend with DynamoDB locking, importing live resources, and parameterizing HCL modules safely for Git.

1. Local Workstation & CLI Setup (Windows)

In production, infrastructure code is written in an IDE like VS Code and authenticated using local CLI profiles rather than embedded secrets. Install Terraform and the AWS CLI via PowerShell:

# Install Terraform & AWS CLI via Winget (PowerShell as Admin)
winget install -e --id HashiCorp.Terraform
winget install -e --id Amazon.AWSCLI

# Authenticate local machine with AWS IAM credentials
aws configure

2. Repository Configuration & Git Security

Terraform state files (.tfstate) can contain sensitive resource outputs and must never be committed to public Git repositories. Set up a local project folder and initialize a strict .gitignore file:

# .gitignore
.terraform/
*.tfstate
*.tfstate.backup
*.tfvars
.terraform.lock.hcl

3. Anatomy of Terraform Blocks & Implicit Dependencies

Terraform uses HashiCorp Configuration Language (HCL). Resources reference each other dynamically using the syntax provider_type.logical_name.attribute. Terraform evaluates these references to infer execution order automatically.

# Example: storage.tf
resource "aws_s3_bucket" "portfolio_bucket" {
  bucket = "portfolio-dev-assets-2026"
}

# Referenced block: Wait for bucket creation before setting access rules
resource "aws_s3_bucket_public_access_block" "allow_public" {
  bucket                  = aws_s3_bucket.portfolio_bucket.id
  block_public_acls       = false
  block_public_policy     = false
  ignore_public_acls      = false
  restrict_public_buckets = false
}

4. Establishing the Remote State Backend

To prevent state file loss and enable concurrent execution safety, state must be stored in S3 and locked using DynamoDB.

Step 4a: Define Backend Infrastructure (backend_setup.tf)

resource "aws_s3_bucket" "tf_state_bucket" {
  bucket        = "alain-puron-terraform-state-bucket-2026"
  force_destroy = true
}

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

resource "aws_dynamodb_table" "tf_state_locks" {
  name         = "alain-puron-terraform-state-locks-2026"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}

Step 4b: Migrate State to Remote Backend (backend.tf)

terraform {
  backend "s3" {
    bucket         = "alain-puron-terraform-state-bucket-2026"
    key            = "global/s3/terraform.tfstate"
    region         = "us-west-2"
    dynamodb_table = "alain-puron-terraform-state-locks-2026"
    encrypt        = true
  }
}

Execute initialization to transfer local state into S3:

terraform init

5. Adopting Existing Resources via terraform import

If infrastructure resources already exist in AWS prior to writing Terraform code, running terraform apply triggers a 409 Conflict or ResourceInUseException. Use terraform import to adopt live infrastructure into state tracking:

# Syntax: terraform import . 
terraform import aws_s3_bucket.tf_state_bucket alain-puron-terraform-state-bucket-2026
terraform import aws_dynamodb_table.tf_state_locks alain-puron-terraform-state-locks-2026

6. Parameterizing Code (Variables, Locals, & Outputs)

Replace hardcoded strings with configurable input variables, calculated local values, and exported outputs to ensure reusability across environments.

variables.tf:

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

variable "environment" {
  type        = string
  default     = "dev"
  description = "Deployment environment"
}

outputs.tf:

output "bucket_arn" {
  value       = aws_s3_bucket.portfolio_bucket.arn
  description = "ARN of the managed asset bucket"
}

7. Standard Core Workflow Summary

  • terraform init: Downloads provider plugins and connects to the S3 remote backend.
  • terraform plan: Performs a dry run, calculating exact differences between local `.tf` code and live AWS state.
  • terraform apply: Acquires the DynamoDB execution lock and provisions planned resources.
  • terraform destroy: Safely tears down every resource managed within the project state file.