---
# === IDENTITY ===
id: software/devops/terraform-aws-basic/2026
canonical_question: "Terraform reference: AWS basic infrastructure"
aliases:
  - "How to set up AWS VPC EC2 S3 with Terraform"
  - "Terraform AWS provider configuration reference"
  - "Basic AWS infrastructure as code with Terraform"
  - "Terraform S3 backend state management AWS"
entity_type: software_reference
domain: software > devops > Terraform AWS Basic
region: global
jurisdiction: global
temporal_scope: 2024-2026

# === VERIFICATION ===
last_verified: 2026-02-28
confidence: 0.93
version: 1.0
first_published: 2026-02-28

# === TEMPORAL VALIDITY ===
temporal_validity:
  status: stable
  last_breaking_change: "AWS provider v5.0 (2023) — removed deprecated arguments, S3 bucket refactor"
  next_review: 2026-08-27
  change_sensitivity: medium

# === CONSTRAINTS ===
constraints:
  - "Always use remote state backend (S3 + DynamoDB) for team collaboration — never commit terraform.tfstate"
  - "Pin provider versions with ~> constraint to avoid breaking changes (e.g., ~> 5.0)"
  - "Never store secrets in Terraform state or .tf files — use aws_secretsmanager_secret or SSM parameters"
  - "Use separate state files per environment (dev/staging/prod) — never share state across environments"
  - "Run terraform plan before every apply — review changes before they execute"

# === SKIP CONDITIONS ===
skip_this_unit_if:
  - condition: "You need GCP infrastructure instead of AWS"
    use_instead: "software/devops/terraform-gcp-basic/2026"
  - condition: "You prefer AWS CloudFormation or SAM"
    use_instead: "software/devops/aws-lambda-sam/2026"

# === AGENT HINTS ===
inputs_needed:
  - key: "infra_scope"
    question: "What AWS resources do you need?"
    type: choice
    options: ["VPC + EC2", "VPC + ECS/Fargate", "S3 + CloudFront", "RDS + VPC", "Full stack (VPC + EC2 + RDS + S3)"]
  - key: "environment_count"
    question: "How many environments (dev/staging/prod)?"
    type: choice
    options: ["Single environment", "Multiple environments (workspaces)", "Multiple environments (separate dirs)"]

# === DISTRIBUTION ===
canonical_source: "https://knowledgelib.io/software/devops/terraform-aws-basic/2026"
suggested_citation: "Source: knowledgelib.io — AI Knowledge Library (verified 2026-02-28)"

# === RELATED UNITS ===
related_kos:
  depends_on: []
  related_to:
    - id: software/devops/aws-lambda-sam/2026
      label: "AWS Lambda + API Gateway (SAM)"
  solves: []
  alternative_to:
    - id: software/devops/terraform-gcp-basic/2026
      label: "Terraform GCP Basic Infrastructure"
  often_confused_with: []

# === SOURCES ===
sources:
  - id: src1
    title: "AWS Provider Documentation"
    author: HashiCorp
    url: https://registry.terraform.io/providers/hashicorp/aws/latest/docs
    type: official_docs
    published: 2025-06-01
    reliability: authoritative
  - id: src2
    title: "Create Infrastructure Tutorial"
    author: HashiCorp
    url: https://developer.hashicorp.com/terraform/tutorials/aws-get-started/aws-create
    type: official_docs
    published: 2025-01-15
    reliability: authoritative
  - id: src3
    title: "Backend Best Practices - AWS Prescriptive Guidance"
    author: AWS
    url: https://docs.aws.amazon.com/prescriptive-guidance/latest/terraform-aws-provider-best-practices/backend.html
    type: official_docs
    published: 2025-03-01
    reliability: authoritative
  - id: src4
    title: "Terraform State Management Best Practices in AWS"
    author: AWS DevOps Blog
    url: https://aws.amazon.com/blogs/devops/best-practices-for-managing-terraform-state-files-in-aws-ci-cd-pipeline/
    type: technical_blog
    published: 2024-09-15
    reliability: high
  - id: src5
    title: "terraform-aws-modules/vpc/aws"
    author: Anton Babenko
    url: https://registry.terraform.io/modules/terraform-aws-modules/vpc/aws/latest
    type: community_resource
    published: 2025-01-01
    reliability: high
  - id: src6
    title: "Terraform Best Practices for AWS Users"
    author: Bill Wang
    url: https://github.com/ozbillwang/terraform-best-practices
    type: community_resource
    published: 2025-06-01
    reliability: moderate_high
  - id: src7
    title: "Terraform Anti-Patterns"
    author: Reaver
    url: https://reaverops.medium.com/terraform-anti-patterns-practices-to-steer-clear-of-b7ce2784e85d
    type: technical_blog
    published: 2024-12-01
    reliability: moderate_high
---

# Terraform Reference: AWS Basic Infrastructure

## TL;DR

- **Bottom line**: Terraform with the AWS provider (`hashicorp/aws ~> 5.0`) lets you define VPC, EC2, S3, RDS, and IAM as code — version-controlled, reproducible, and reviewable infrastructure.
- **Key tool/command**: `terraform init && terraform plan && terraform apply`
- **Watch out for**: Local state files (`terraform.tfstate`) — always configure an S3 + DynamoDB backend before your first `apply` or you risk state loss and team conflicts.
- **Works with**: Terraform 1.6+, AWS provider ~> 5.0, OpenTofu 1.6+ (drop-in compatible).

## Constraints
<!-- Agents: read this section before recommending any action from this unit.
     These are hard constraints that must not be violated. -->

- Always use remote state (S3 + DynamoDB) — never commit `terraform.tfstate` to version control.
- Pin provider versions with `~>` constraint (e.g., `~> 5.0`) — unversioned providers break on major updates.
- Never store secrets in `.tf` files or state — use `aws_secretsmanager_secret` or SSM Parameter Store.
- Separate state per environment — dev/staging/prod must never share a state file.
- Run `terraform plan` and review output before every `apply` — no blind applies.
- Enable state locking (DynamoDB) to prevent concurrent modifications.

## Quick Reference

| Resource | Terraform Type | Key Arguments | Notes |
|---|---|---|---|
| VPC | `aws_vpc` | `cidr_block`, `enable_dns_hostnames` | Foundation of networking |
| Subnet | `aws_subnet` | `vpc_id`, `cidr_block`, `availability_zone` | Public vs private via route table |
| Internet Gateway | `aws_internet_gateway` | `vpc_id` | Required for public subnets |
| NAT Gateway | `aws_nat_gateway` | `subnet_id`, `allocation_id` | Required for private subnet egress |
| Route Table | `aws_route_table` | `vpc_id`, routes | Associate with subnets |
| Security Group | `aws_security_group` | `vpc_id`, `ingress`, `egress` | Stateful firewall rules |
| EC2 Instance | `aws_instance` | `ami`, `instance_type`, `subnet_id` | Use `data.aws_ami` for latest AMI |
| S3 Bucket | `aws_s3_bucket` | `bucket` | Separate resources for ACL, versioning |
| RDS Instance | `aws_db_instance` | `engine`, `instance_class`, `db_name` | Use `aws_db_subnet_group` |
| IAM Role | `aws_iam_role` | `assume_role_policy` | Attach policies separately |
| Elastic IP | `aws_eip` | `domain = "vpc"` | For NAT Gateway or static IPs |
| Key Pair | `aws_key_pair` | `public_key` | For SSH access to EC2 |
| S3 Backend | `backend "s3"` | `bucket`, `key`, `dynamodb_table` | State storage + locking |

## Decision Tree

```
START
├── First time setting up Terraform?
│   ├── YES → Start with S3 backend + VPC module (Step 1-3 below)
│   └── NO ↓
├── Need a full VPC with public/private subnets?
│   ├── YES → Use terraform-aws-modules/vpc (saves 100+ lines)
│   └── NO ↓
├── Single server or container workload?
│   ├── Single server → EC2 with Security Group (Example 1)
│   ├── Containers → ECS/Fargate with ALB
│   └── NO ↓
├── Need a database?
│   ├── YES → RDS in private subnet (Example 2)
│   └── NO ↓
├── Static website hosting?
│   ├── YES → S3 + CloudFront (Example 3)
│   └── NO ↓
└── DEFAULT → VPC + EC2 + S3 (full stack example)
```

## Step-by-Step Guide

### 1. Configure the S3 backend for remote state

Set up remote state before anything else. [src3] [src4]

```hcl
# backend.tf
terraform {
  required_version = ">= 1.6.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }

  backend "s3" {
    bucket         = "my-terraform-state-123456"
    key            = "prod/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

provider "aws" {
  region = var.aws_region

  default_tags {
    tags = {
      Environment = var.environment
      ManagedBy   = "terraform"
      Project     = var.project_name
    }
  }
}
```

**Verify**: `terraform init` → `Initializing the backend... Successfully configured the backend "s3"!`

### 2. Create the S3 state bucket and DynamoDB lock table

Bootstrap the backend resources (run this once, manually or with a separate config). [src3]

```hcl
# bootstrap/main.tf — run this first, then move state to S3
resource "aws_s3_bucket" "terraform_state" {
  bucket = "my-terraform-state-123456"

  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  versioning_configuration {
    status = "Enabled"
  }
}

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 = "aws:kms"
    }
  }
}

resource "aws_dynamodb_table" "terraform_locks" {
  name         = "terraform-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }
}
```

**Verify**: `aws s3 ls s3://my-terraform-state-123456` → bucket exists

### 3. Define a VPC with public and private subnets

Use the community VPC module for a production-ready network. [src5]

```hcl
# network.tf
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.5.0"

  name = "${var.project_name}-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b", "us-east-1c"]
  private_subnets = ["10.0.1.0/24", "10.0.2.0/24", "10.0.3.0/24"]
  public_subnets  = ["10.0.101.0/24", "10.0.102.0/24", "10.0.103.0/24"]

  enable_nat_gateway     = true
  single_nat_gateway     = true  # Use one NAT GW to save cost in dev
  enable_dns_hostnames   = true
  enable_dns_support     = true

  tags = {
    Environment = var.environment
  }
}
```

**Verify**: `terraform plan` → shows VPC, subnets, NAT gateway resources

### 4. Launch an EC2 instance

Deploy a server in the public subnet with a security group. [src2]

```hcl
# compute.tf
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]

  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
}

resource "aws_security_group" "web" {
  name_prefix = "${var.project_name}-web-"
  vpc_id      = module.vpc.vpc_id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_instance" "web" {
  ami                    = data.aws_ami.amazon_linux.id
  instance_type          = "t3.micro"
  subnet_id              = module.vpc.public_subnets[0]
  vpc_security_group_ids = [aws_security_group.web.id]

  root_block_device {
    volume_size = 20
    volume_type = "gp3"
    encrypted   = true
  }

  tags = {
    Name = "${var.project_name}-web"
  }
}
```

**Verify**: `terraform apply` → instance running, then `aws ec2 describe-instances --filters "Name=tag:Name,Values=*-web"`

### 5. Create an S3 bucket with versioning

Set up object storage with proper security defaults. [src1]

```hcl
# storage.tf
resource "aws_s3_bucket" "assets" {
  bucket = "${var.project_name}-assets-${data.aws_caller_identity.current.account_id}"
}

resource "aws_s3_bucket_versioning" "assets" {
  bucket = aws_s3_bucket.assets.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_public_access_block" "assets" {
  bucket = aws_s3_bucket.assets.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_server_side_encryption_configuration" "assets" {
  bucket = aws_s3_bucket.assets.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

data "aws_caller_identity" "current" {}
```

**Verify**: `terraform apply` → `aws s3 ls` shows the bucket

### 6. Define variables and outputs

Create reusable variable definitions and useful outputs. [src2]

```hcl
# variables.tf
variable "aws_region" {
  description = "AWS region"
  type        = string
  default     = "us-east-1"
}

variable "environment" {
  description = "Environment name"
  type        = string
  default     = "dev"
  validation {
    condition     = contains(["dev", "staging", "prod"], var.environment)
    error_message = "Environment must be dev, staging, or prod."
  }
}

variable "project_name" {
  description = "Project name used for resource naming"
  type        = string
}

# outputs.tf
output "vpc_id" {
  value = module.vpc.vpc_id
}

output "public_subnets" {
  value = module.vpc.public_subnets
}

output "instance_public_ip" {
  value = aws_instance.web.public_ip
}

output "s3_bucket_name" {
  value = aws_s3_bucket.assets.id
}
```

**Verify**: `terraform output` → shows all output values

## Code Examples

### HCL: Complete VPC + EC2 + RDS stack

> Full script: [hcl-complete-vpc-ec2-rds-stack.txt](scripts/hcl-complete-vpc-ec2-rds-stack.txt) (35 lines)

```hcl
# Input:  Terraform configuration files
# Output: VPC with public/private subnets, EC2 web server, RDS PostgreSQL
resource "aws_db_subnet_group" "main" {
  name       = "${var.project_name}-db"
  subnet_ids = module.vpc.private_subnets
# ... (see full script)
```

### HCL: S3 static website with CloudFront

> Full script: [hcl-s3-static-website-with-cloudfront.txt](scripts/hcl-s3-static-website-with-cloudfront.txt) (48 lines)

```hcl
# Input:  Terraform configuration
# Output: S3 bucket + CloudFront distribution for static site hosting
resource "aws_s3_bucket" "website" {
  bucket = "www.${var.domain_name}"
}
# ... (see full script)
```

## Anti-Patterns

### Wrong: Local state with no backend

```hcl
# BAD — state stored locally, no locking, no backup
terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
  # No backend block = local state in terraform.tfstate
}
```

### Correct: S3 backend with DynamoDB locking

```hcl
# GOOD — remote state with encryption and locking
terraform {
  backend "s3" {
    bucket         = "my-tf-state"
    key            = "prod/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}
```

### Wrong: Hardcoded values everywhere

```hcl
# BAD — hardcoded AMI, region, instance type
resource "aws_instance" "web" {
  ami           = "ami-0c55b159cbfafe1f0"  # Stale AMI ID
  instance_type = "t2.micro"
  subnet_id     = "subnet-abc123"  # Hardcoded subnet
}
```

### Correct: Use variables and data sources

```hcl
# GOOD — dynamic AMI lookup, parameterized config
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]
  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
}

resource "aws_instance" "web" {
  ami           = data.aws_ami.amazon_linux.id
  instance_type = var.instance_type
  subnet_id     = module.vpc.public_subnets[0]
}
```

### Wrong: Monolithic configuration in one file

```hcl
# BAD — 2000 lines in main.tf with VPC, EC2, RDS, S3, IAM, CloudFront...
# Impossible to review, slow to plan, huge blast radius
```

### Correct: Modular structure with separate files

```
# GOOD — organized by concern
project/
├── backend.tf        # Provider + backend config
├── variables.tf      # Input variables
├── outputs.tf        # Output values
├── network.tf        # VPC, subnets, routing
├── compute.tf        # EC2, ASG, ALB
├── database.tf       # RDS, ElastiCache
├── storage.tf        # S3 buckets
└── iam.tf            # Roles, policies
```

## Common Pitfalls

- **Forgetting `prevent_destroy` on state bucket**: Accidentally deleting the S3 state bucket loses all state. Fix: add `lifecycle { prevent_destroy = true }` to the state bucket resource. [src3]
- **Not enabling bucket versioning on state**: State corruption with no recovery. Fix: enable `versioning_configuration { status = "Enabled" }`. [src4]
- **Using `default` security group**: The default SG allows all traffic within the VPC. Fix: always create explicit security groups with minimal rules. [src1]
- **Unencrypted EBS volumes and S3 buckets**: Compliance violation in most organizations. Fix: set `encrypted = true` on `root_block_device` and use `aws_s3_bucket_server_side_encryption_configuration`. [src6]
- **Stale AMI IDs**: Hardcoded AMI IDs become outdated. Fix: use `data "aws_ami"` with filters for the latest version. [src2]
- **Missing `create_before_destroy` on security groups**: Updates fail because the old SG is still attached. Fix: add `lifecycle { create_before_destroy = true }`. [src7]

## Diagnostic Commands

```bash
# Initialize and download providers
terraform init

# Preview changes without applying
terraform plan -out=tfplan

# Apply a saved plan
terraform apply tfplan

# Show current state
terraform state list

# Show details of a specific resource
terraform state show aws_instance.web

# Import existing AWS resource into state
terraform import aws_instance.web i-1234567890abcdef0

# Validate configuration syntax
terraform validate

# Format all .tf files
terraform fmt -recursive

# Show outputs
terraform output -json
```

## Version History & Compatibility

| Version | Status | Breaking Changes | Migration Notes |
|---|---|---|---|
| Terraform 1.9+ | Current | None | HCL functions improvements |
| Terraform 1.6–1.8 | Supported | `terraform test` GA | — |
| AWS Provider 5.x | Current | S3 bucket args split into resources | Use separate `aws_s3_bucket_*` resources |
| AWS Provider 4.x | Deprecated | — | Upgrade: change `acl` to `aws_s3_bucket_acl` |
| OpenTofu 1.6+ | Active | Fork of Terraform 1.6 | Drop-in compatible, MPL-2.0 licensed |

## When to Use / When Not to Use

| Use When | Don't Use When | Use Instead |
|---|---|---|
| Defining reproducible multi-resource infrastructure | One-off CLI commands or simple scripts | AWS CLI, bash scripts |
| Team collaboration with review workflows | Rapid prototyping with frequent changes | AWS Console, Pulumi |
| Multi-cloud or hybrid deployments | AWS-only with CloudFormation expertise | CloudFormation, SAM |
| Infrastructure needs version control and audit trail | Managing Kubernetes resources | Helm, Kustomize, ArgoCD |
| Long-lived infrastructure (VPCs, RDS, S3) | Ephemeral/disposable environments | Docker Compose, LocalStack |

## Important Caveats

- Terraform state contains sensitive data (passwords, keys) in plaintext — always encrypt the S3 backend and restrict access with IAM policies.
- `terraform destroy` is irreversible for stateful resources (RDS, S3 with data) — always back up data before destroying and use `prevent_destroy` lifecycle rules.
- Provider upgrades can change resource behavior — always test provider version bumps in dev first, and read the changelog.
- The AWS provider does NOT wait for resources to be fully operational — use `depends_on` and verify with health checks post-apply.

## Related Units

- [Terraform GCP Basic Infrastructure](/software/devops/terraform-gcp-basic/2026)
- [AWS Lambda + API Gateway (SAM)](/software/devops/aws-lambda-sam/2026)
