# Infrastructure as Code with Terraform on AWS

Introduction

Managing cloud infrastructure manually through the AWS Console is error-prone and doesn't scale. Terraform by HashiCorp lets you define infrastructure as code (IaC), enabling version control, reproducibility, and automation.

In this tutorial, you'll build a production-ready AWS environment with:

  • A custom VPC with public and private subnets
  • An EC2 web server with security groups
  • An RDS MySQL database in a private subnet
  • Outputs for easy access to resources
  • Prerequisites

  • AWS account with IAM credentials
  • Terraform >= 1.6 installed
  • Basic understanding of AWS networking
  • Step 1: Install and Configure Terraform

    # Install Terraform (macOS)
    

    brew tap hashicorp/tap

    brew install hashicorp/tap/terraform

    # Verify installation

    terraform version

    # Configure AWS credentials

    export AWS_ACCESS_KEY_ID="your-access-key"

    export AWS_SECRET_ACCESS_KEY="your-secret-key"

    export AWS_DEFAULT_REGION="ap-northeast-1"

    Step 2: Project Structure

    Create the following directory structure:

    terraform-aws-demo/
    

    ├── main.tf # Root module

    ├── variables.tf # Input variables

    ├── outputs.tf # Output values

    ├── terraform.tfvars # Variable values

    ├── modules/

    │ ├── vpc/

    │ │ ├── main.tf

    │ │ ├── variables.tf

    │ │ └── outputs.tf

    │ ├── ec2/

    │ │ ├── main.tf

    │ │ ├── variables.tf

    │ │ └── outputs.tf

    │ └── rds/

    │ ├── main.tf

    │ ├── variables.tf

    │ └── outputs.tf

    Step 3: Define the VPC Module

    # modules/vpc/main.tf
    

    resource "aws_vpc" "main" {

    cidr_block = var.vpc_cidr

    enable_dns_hostnames = true

    enable_dns_support = true

    tags = {

    Name = "${var.project_name}-vpc"

    Environment = var.environment

    }

    }

    resource "aws_subnet" "public" {

    count = length(var.public_subnet_cidrs)

    vpc_id = aws_vpc.main.id

    cidr_block = var.public_subnet_cidrs[count.index]

    availability_zone = var.availability_zones[count.index]

    map_public_ip_on_launch = true

    tags = {

    Name = "${var.project_name}-public-${count.index + 1}"

    }

    }

    resource "aws_subnet" "private" {

    count = length(var.private_subnet_cidrs)

    vpc_id = aws_vpc.main.id

    cidr_block = var.private_subnet_cidrs[count.index]

    availability_zone = var.availability_zones[count.index]

    tags = {

    Name = "${var.project_name}-private-${count.index + 1}"

    }

    }

    resource "aws_internet_gateway" "main" {

    vpc_id = aws_vpc.main.id

    tags = {

    Name = "${var.project_name}-igw"

    }

    }

    resource "aws_route_table" "public" {

    vpc_id = aws_vpc.main.id

    route {

    cidr_block = "0.0.0.0/0"

    gateway_id = aws_internet_gateway.main.id

    }

    tags = {

    Name = "${var.project_name}-public-rt"

    }

    }

    resource "aws_route_table_association" "public" {

    count = length(aws_subnet.public)

    subnet_id = aws_subnet.public[count.index].id

    route_table_id = aws_route_table.public.id

    }

    # modules/vpc/variables.tf
    

    variable "project_name" {

    type = string

    }

    variable "environment" {

    type = string

    default = "dev"

    }

    variable "vpc_cidr" {

    type = string

    default = "10.0.0.0/16"

    }

    variable "public_subnet_cidrs" {

    type = list(string)

    default = ["10.0.1.0/24", "10.0.2.0/24"]

    }

    variable "private_subnet_cidrs" {

    type = list(string)

    default = ["10.0.10.0/24", "10.0.20.0/24"]

    }

    variable "availability_zones" {

    type = list(string)

    default = ["ap-northeast-1a", "ap-northeast-1c"]

    }

    # modules/vpc/outputs.tf
    

    output "vpc_id" {

    value = aws_vpc.main.id

    }

    output "public_subnet_ids" {

    value = aws_subnet.public[*].id

    }

    output "private_subnet_ids" {

    value = aws_subnet.private[*].id

    }

    Step 4: Define the EC2 Module

    # modules/ec2/main.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 = var.vpc_id

    ingress {

    from_port = 80

    to_port = 80

    protocol = "tcp"

    cidr_blocks = ["0.0.0.0/0"]

    }

    ingress {

    from_port = 443

    to_port = 443

    protocol = "tcp"

    cidr_blocks = ["0.0.0.0/0"]

    }

    ingress {

    from_port = 22

    to_port = 22

    protocol = "tcp"

    cidr_blocks = [var.ssh_cidr]

    }

    egress {

    from_port = 0

    to_port = 0

    protocol = "-1"

    cidr_blocks = ["0.0.0.0/0"]

    }

    tags = {

    Name = "${var.project_name}-web-sg"

    }

    }

    resource "aws_instance" "web" {

    ami = data.aws_ami.amazon_linux.id

    instance_type = var.instance_type

    subnet_id = var.subnet_id

    vpc_security_group_ids = [aws_security_group.web.id]

    key_name = var.key_name

    user_data = <<-EOF

    #!/bin/bash

    yum update -y

    yum install -y httpd php php-mysqlnd

    systemctl start httpd

    systemctl enable httpd

    echo "<h1>Hello from Terraform!</h1>" > /var/www/html/index.html

    EOF

    root_block_device {

    volume_size = 20

    volume_type = "gp3"

    encrypted = true

    }

    tags = {

    Name = "${var.project_name}-web"

    Environment = var.environment

    }

    }

    Step 5: Define the RDS Module

    # modules/rds/main.tf
    

    resource "aws_db_subnet_group" "main" {

    name = "${var.project_name}-db-subnet"

    subnet_ids = var.private_subnet_ids

    tags = {

    Name = "${var.project_name}-db-subnet-group"

    }

    }

    resource "aws_security_group" "db" {

    name_prefix = "${var.project_name}-db-"

    vpc_id = var.vpc_id

    ingress {

    from_port = 3306

    to_port = 3306

    protocol = "tcp"

    security_groups = [var.web_sg_id]

    }

    tags = {

    Name = "${var.project_name}-db-sg"

    }

    }

    resource "aws_db_instance" "main" {

    identifier = "${var.project_name}-db"

    engine = "mysql"

    engine_version = "8.0"

    instance_class = var.db_instance_class

    allocated_storage = 20

    storage_type = "gp3"

    storage_encrypted = true

    db_name = var.db_name

    username = var.db_username

    password = var.db_password

    db_subnet_group_name = aws_db_subnet_group.main.name

    vpc_security_group_ids = [aws_security_group.db.id]

    skip_final_snapshot = true

    multi_az = false

    tags = {

    Name = "${var.project_name}-db"

    Environment = var.environment

    }

    }

    Step 6: Root Module Configuration

    # main.tf
    

    terraform {

    required_version = ">= 1.6.0"

    required_providers {

    aws = {

    source = "hashicorp/aws"

    version = "~> 5.0"

    }

    }

    }

    provider "aws" {

    region = var.aws_region

    }

    module "vpc" {

    source = "./modules/vpc"

    project_name = var.project_name

    environment = var.environment

    }

    module "ec2" {

    source = "./modules/ec2"

    project_name = var.project_name

    environment = var.environment

    vpc_id = module.vpc.vpc_id

    subnet_id = module.vpc.public_subnet_ids[0]

    instance_type = "t3.micro"

    key_name = var.key_name

    ssh_cidr = var.ssh_cidr

    }

    module "rds" {

    source = "./modules/rds"

    project_name = var.project_name

    environment = var.environment

    vpc_id = module.vpc.vpc_id

    private_subnet_ids = module.vpc.private_subnet_ids

    web_sg_id = module.ec2.web_sg_id

    db_name = "appdb"

    db_username = var.db_username

    db_password = var.db_password

    }

    # variables.tf
    

    variable "project_name" {

    type = string

    default = "techsfree-demo"

    }

    variable "environment" {

    type = string

    default = "dev"

    }

    variable "aws_region" {

    type = string

    default = "ap-northeast-1"

    }

    variable "key_name" {

    type = string

    }

    variable "ssh_cidr" {

    type = string

    default = "0.0.0.0/0"

    }

    variable "db_username" {

    type = string

    sensitive = true

    }

    variable "db_password" {

    type = string

    sensitive = true

    }

    # outputs.tf
    

    output "vpc_id" {

    value = module.vpc.vpc_id

    }

    output "web_public_ip" {

    value = module.ec2.public_ip

    }

    output "rds_endpoint" {

    value = module.rds.endpoint

    sensitive = true

    }

    Step 7: Deploy Your Infrastructure

    # Initialize Terraform (downloads providers)
    

    terraform init

    # Preview changes

    terraform plan -var="key_name=my-key" \

    -var="db_username=admin" \

    -var="db_password=SecurePass123!"

    # Apply changes

    terraform apply -auto-approve \

    -var="key_name=my-key" \

    -var="db_username=admin" \

    -var="db_password=SecurePass123!"

    # View outputs

    terraform output

    # Destroy when done (avoid charges!)

    terraform destroy -auto-approve

    Step 8: Best Practices

    Use Remote State

    terraform {
    

    backend "s3" {

    bucket = "my-terraform-state"

    key = "prod/terraform.tfstate"

    region = "ap-northeast-1"

    dynamodb_table = "terraform-locks"

    encrypt = true

    }

    }

    Use tfvars for Environments

    # environments/prod.tfvars
    

    project_name = "techsfree"

    environment = "prod"

    instance_type = "t3.small"

    terraform apply -var-file="environments/prod.tfvars"
    

    Use terraform fmt and terraform validate

    # Format all .tf files
    

    terraform fmt -recursive

    # Validate configuration

    terraform validate

    Summary

    You've learned to:

  • Structure Terraform projects with reusable modules
  • Build a VPC with public/private subnets
  • Deploy EC2 instances with security groups
  • Provision RDS databases in private subnets
  • Follow IaC best practices (remote state, variables, formatting)
  • Terraform makes infrastructure reproducible and auditable. Combine it with CI/CD pipelines for fully automated deployments.

    Next Steps

  • Add an Application Load Balancer (ALB)
  • Implement auto-scaling groups
  • Set up Terraform Cloud for team collaboration
  • Explore terragrunt for DRY configurations