TerraformPilot

Cloud Computing

Terraform on AWS Free Tier: Complete Guide to Free Infrastructure (2026)

Deploy real infrastructure on AWS Free Tier with Terraform. Includes EC2, S3, RDS, Lambda, and DynamoDB examples — all within free tier limits. No charges if you follow this guide.

LLuca Berton3 min read

What You'll Build (For Free)

#

This guide shows you how to use Terraform with the AWS Free Tier to deploy real infrastructure without spending a cent. You'll provision:

  • EC2 instance (t2.micro/t3.micro — 750 hours/month free)
  • S3 bucket (5 GB storage free)
  • DynamoDB table (25 GB storage, 25 read/write capacity units free)
  • Lambda function (1M requests/month free)
  • RDS instance (db.t3.micro — 750 hours/month free, 20 GB storage)

Important: AWS Free Tier lasts 12 months from account creation for most services. Always-Free services (Lambda, DynamoDB) remain free indefinitely.

Prerequisites

#

AWS Free Tier Limits (2026)

#
ServiceFree Tier AllowanceDuration
EC2 (t2.micro/t3.micro)750 hours/month12 months
S35 GB storage, 20,000 GET, 2,000 PUT12 months
RDS (db.t3.micro)750 hours/month, 20 GB12 months
DynamoDB25 GB, 25 RCU/WCUAlways free
Lambda1M requests, 400,000 GB-secondsAlways free
CloudWatch10 custom metrics, 10 alarmsAlways free
SNS1M publishesAlways free
SQS1M requestsAlways free
API Gateway1M REST API calls/month12 months

Project Setup

#

Create your project structure:

mkdir terraform-aws-free-tier && cd terraform-aws-free-tier

Provider Configuration

#

Create providers.tf:

terraform {
  required_version = ">= 1.9"
 
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}
 
provider "aws" {
  region = var.aws_region
 
  default_tags {
    tags = {
      Project     = "terraform-free-tier"
      Environment = "learning"
      ManagedBy   = "terraform"
    }
  }
}

Variables

#

Create variables.tf:

variable "aws_region" {
  description = "AWS region for resources"
  type        = string
  default     = "us-east-1"  # Most free tier resources available here
}
 
variable "project_name" {
  description = "Project name prefix for resource naming"
  type        = string
  default     = "free-tier-lab"
}

Deploy EC2 Instance (Free Tier)

#

Create ec2.tf:

# Get the latest Amazon Linux 2023 AMI
data "aws_ami" "amazon_linux" {
  most_recent = true
  owners      = ["amazon"]
 
  filter {
    name   = "name"
    values = ["al2023-ami-*-x86_64"]
  }
 
  filter {
    name   = "virtualization-type"
    values = ["hvm"]
  }
}
 
# Security group allowing SSH
resource "aws_security_group" "web" {
  name_prefix = "${var.project_name}-web-"
  description = "Allow SSH and HTTP"
 
  ingress {
    description = "SSH"
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]  # Restrict to your IP in production
  }
 
  ingress {
    description = "HTTP"
    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"]
  }
 
  tags = {
    Name = "${var.project_name}-web-sg"
  }
}
 
# EC2 instance — t2.micro is free tier eligible
resource "aws_instance" "web" {
  ami                    = data.aws_ami.amazon_linux.id
  instance_type          = "t2.micro"  # FREE TIER: 750 hours/month
  vpc_security_group_ids = [aws_security_group.web.id]
 
  root_block_device {
    volume_size = 8    # FREE TIER: up to 30 GB EBS
    volume_type = "gp3"
    encrypted   = true
  }
 
  user_data = <<-EOF
    #!/bin/bash
    yum update -y
    yum install -y httpd
    systemctl start httpd
    systemctl enable httpd
    echo "<h1>Hello from Terraform on AWS Free Tier!</h1>" > /var/www/html/index.html
  EOF
 
  tags = {
    Name = "${var.project_name}-web-server"
  }
}

Deploy S3 Bucket (Free Tier)

#

Create s3.tf:

resource "aws_s3_bucket" "data" {
  bucket_prefix = "${var.project_name}-data-"
 
  tags = {
    Name = "${var.project_name}-data"
  }
}
 
resource "aws_s3_bucket_versioning" "data" {
  bucket = aws_s3_bucket.data.id
  versioning_configuration {
    status = "Enabled"
  }
}
 
resource "aws_s3_bucket_server_side_encryption_configuration" "data" {
  bucket = aws_s3_bucket.data.id
 
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"  # Free — no KMS charges
    }
  }
}
 
# Block all public access (security best practice)
resource "aws_s3_bucket_public_access_block" "data" {
  bucket = aws_s3_bucket.data.id
 
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

Deploy DynamoDB Table (Always Free)

#

Create dynamodb.tf:

resource "aws_dynamodb_table" "app" {
  name         = "${var.project_name}-sessions"
  billing_mode = "PROVISIONED"
 
  # FREE TIER: 25 RCU + 25 WCU
  read_capacity  = 5
  write_capacity = 5
 
  hash_key  = "session_id"
  range_key = "timestamp"
 
  attribute {
    name = "session_id"
    type = "S"
  }
 
  attribute {
    name = "timestamp"
    type = "N"
  }
 
  ttl {
    attribute_name = "expires_at"
    enabled        = true
  }
 
  tags = {
    Name = "${var.project_name}-sessions"
  }
}

Deploy Lambda Function (Always Free)

#

Create lambda.tf:

data "archive_file" "lambda" {
  type        = "zip"
  output_path = "${path.module}/lambda.zip"
 
  source {
    content  = <<-EOF
      exports.handler = async (event) => {
        return {
          statusCode: 200,
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({
            message: "Hello from Terraform + Lambda!",
            timestamp: new Date().toISOString(),
            event: event
          })
        };
      };
    EOF
    filename = "index.js"
  }
}
 
resource "aws_iam_role" "lambda" {
  name_prefix = "${var.project_name}-lambda-"
 
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "lambda.amazonaws.com"
      }
    }]
  })
}
 
resource "aws_iam_role_policy_attachment" "lambda_basic" {
  role       = aws_iam_role.lambda.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
 
resource "aws_lambda_function" "api" {
  filename         = data.archive_file.lambda.output_path
  source_code_hash = data.archive_file.lambda.output_base64sha256
  function_name    = "${var.project_name}-api"
  role             = aws_iam_role.lambda.arn
  handler          = "index.handler"
  runtime          = "nodejs20.x"
  memory_size      = 128   # Minimum — saves free tier quota
  timeout          = 10
 
  tags = {
    Name = "${var.project_name}-api"
  }
}
 
# Function URL (free alternative to API Gateway)
resource "aws_lambda_function_url" "api" {
  function_name      = aws_lambda_function.api.function_name
  authorization_type = "NONE"
}

Deploy RDS Instance (Free Tier)

#

Create rds.tf:

resource "aws_db_instance" "postgres" {
  identifier_prefix = "${var.project_name}-"
 
  engine         = "postgres"
  engine_version = "16.4"
  instance_class = "db.t3.micro"  # FREE TIER: 750 hours/month
 
  allocated_storage     = 20   # FREE TIER: up to 20 GB
  max_allocated_storage = 20   # Prevent auto-scaling beyond free tier
  storage_type          = "gp2"
  storage_encrypted     = true
 
  db_name  = "appdb"
  username = "dbadmin"
  password = var.db_password  # Use terraform.tfvars or env var
 
  skip_final_snapshot = true  # OK for learning environments
  publicly_accessible = false
 
  backup_retention_period = 0  # Disable backups (saves storage costs)
 
  tags = {
    Name = "${var.project_name}-postgres"
  }
}
 
variable "db_password" {
  description = "Database master password"
  type        = string
  sensitive   = true
  default     = "ChangeMe123!"  # Override with TF_VAR_db_password env var
}

Outputs

#

Create outputs.tf:

output "ec2_public_ip" {
  description = "EC2 instance public IP"
  value       = aws_instance.web.public_ip
}
 
output "s3_bucket_name" {
  description = "S3 bucket name"
  value       = aws_s3_bucket.data.id
}
 
output "dynamodb_table_name" {
  description = "DynamoDB table name"
  value       = aws_dynamodb_table.app.name
}
 
output "lambda_url" {
  description = "Lambda function URL"
  value       = aws_lambda_function_url.api.function_url
}
 
output "rds_endpoint" {
  description = "RDS endpoint"
  value       = aws_db_instance.postgres.endpoint
}
 
output "estimated_monthly_cost" {
  description = "Estimated cost"
  value       = "$0.00 (within AWS Free Tier limits)"
}

Deploy Everything

#
# Initialize Terraform
terraform init
 
# Preview what will be created
terraform plan
 
# Deploy (type 'yes' when prompted)
terraform apply
 
# Test the Lambda function
curl $(terraform output -raw lambda_url)

⚠️ Avoiding Unexpected Charges

#

Follow these rules to stay within the free tier:

  1. Don't run multiple t2.micro instances — you only get 750 hours total (1 instance × 24h × 31 days = 744 hours)
  2. Stay in one region — free tier limits are global, not per-region
  3. Set billing alerts:
resource "aws_budgets_budget" "free_tier" {
  name         = "free-tier-alert"
  budget_type  = "COST"
  limit_amount = "1"
  limit_unit   = "USD"
  time_unit    = "MONTHLY"
 
  notification {
    comparison_operator       = "GREATER_THAN"
    threshold                 = 80
    threshold_type            = "PERCENTAGE"
    notification_type         = "ACTUAL"
    subscriber_email_addresses = ["your-email@example.com"]
  }
}
  1. Destroy resources when done:
terraform destroy
  1. Check the AWS Free Tier Usage dashboard at console.aws.amazon.com/billing/home#/freetier

What's NOT Free (Common Mistakes)

#
ResourceWhy It Costs Money
NAT Gateway~$32/month — use public subnets for learning
Elastic IP (unattached)$3.60/month per unused EIP
EBS snapshots beyond 1 GB$0.05/GB/month
Data transfer out > 100 GB$0.09/GB
t2.micro in multiple regions750h is global, not per-region
RDS Multi-AZNot free tier eligible

Clean Up

#

Always destroy your lab resources when you're done learning:

terraform destroy -auto-approve

Verify in the AWS Console that all resources are terminated.

Next Steps

#

Hands-On Courses

#

Conclusion

#

The AWS Free Tier gives you real infrastructure to practice Terraform — EC2, S3, RDS, Lambda, and DynamoDB all at zero cost for 12 months. The key is staying within limits: one t2.micro, one db.t3.micro, and careful monitoring with billing alerts. Use terraform destroy when you're done, and you'll never see an unexpected bill.

#AWS#Terraform#Free Tier#Infrastructure as Code#Beginners

Share this article