Skip to content

Terraform on AWS -- Complete Guide to Provisioning AWS Infrastructure

DodaTech 6 min read

Terraform on AWS lets you provision the full stack of AWS services -- compute, storage, networking, databases, and serverless -- using declarative HCL configuration files version-controlled in Git.

What You'll Learn

In this tutorial, you will learn how to configure the AWS provider, provision EC2 instances with security groups, create VPCs with subnets, manage S3 buckets with policies, deploy RDS databases, set up IAM roles, and follow security best practices for production AWS infrastructure.

Why It Matters

AWS offers over 200 services, and managing them through the console or CLI scripts does not scale. Terraform provides a single declarative language for all AWS resources, with plan preview, state tracking, and automated deployment through CI/CD pipelines.

Real-World Use

DodaTech runs its entire infrastructure on AWS using Terraform. Durga Antivirus Pro's backend uses EC2 auto-scaling groups, RDS Multi-AZ databases, S3 data lakes, and Lambda functions for malware analysis -- all defined in Terraform and deployed through GitHub Actions.

AWS Provider Configuration

graph TD
    A[Terraform AWS Config] --> B[AWS Provider]
    B --> C[EC2]
    B --> D[VPC]
    B --> E[S3]
    B --> F[RDS]
    B --> G[IAM]
    B --> H[Lambda]
    C --> I[Instances]
    C --> J[Security Groups]
    C --> K[Auto Scaling]
    D --> L[Subnets]
    D --> M[Route Tables]
    D --> N[Internet Gateway]
    style A fill:#ff9900,color:#fff
    style B fill:#ff9900,color:#fff

Provider Setup

# provider.tf
terraform {
  required_version = ">= 1.6"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

provider "aws" {
  region = "us-east-1"

  assume_role {
    role_arn     = "arn:aws:iam::123456789012:role/TerraformDeployer"
    session_name = "TerraformSession"
  }

  default_tags {
    tags = {
      Environment = var.environment
      ManagedBy   = "Terraform"
      Project     = var.project_name
    }
  }
}
terraform init

Expected output:

Initializing the backend...
Initializing provider plugins...
- Finding hashicorp/aws versions matching "~> 5.0"...
- Installing hashicorp/aws v5.72.0...
- Installed hashicorp/aws v5.72.0 (signed by HashiCorp)

EC2 Instance with Security Group

# ec2.tf
resource "aws_security_group" "web" {
  name        = "${var.environment}-web-sg"
  description = "Security group for web servers"
  vpc_id      = data.aws_vpc.default.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"]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = { Name = "${var.environment}-web-sg" }
}

resource "aws_instance" "web" {
  ami                    = data.aws_ami.amazon_linux_2.id
  instance_type          = var.instance_type
  subnet_id              = data.aws_subnet.public.id
  vpc_security_group_ids = [aws_security_group.web.id]

  user_data = <<-EOF
    #!/bin/bash
    dnf install -y nginx
    systemctl enable --now nginx
    echo "Hello from Terraform" > /usr/share/nginx/html/index.html
  EOF

  tags = {
    Name = "${var.environment}-web-server"
  }
}
terraform plan

Expected output:

Terraform will perform the following actions:

  # aws_instance.web will be created
  + resource "aws_instance" "web" {
      + ami                          = "ami-0abcdef1234567890"
      + instance_type                = "t3.medium"
      + vpc_security_group_ids       = (known after apply)
      + subnet_id                    = "subnet-0123456789abcdef0"
      + tags                         = { "Name" = "production-web-server" }
    }

Plan: 2 to add, 0 to change, 0 to destroy.

VPC with Public and Private Subnets

# vpc.tf
resource "aws_vpc" "main" {
  cidr_block           = var.vpc_cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = { Name = "${var.environment}-vpc" }
}

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.environment}-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.environment}-private-${count.index + 1}" }
}

resource "aws_internet_gateway" "main" {
  vpc_id = aws_vpc.main.id

  tags = { Name = "${var.environment}-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.environment}-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
}

RDS Multi-AZ Database

# rds.tf
resource "aws_db_instance" "main" {
  identifier     = "${var.environment}-database"
  engine         = "postgres"
  engine_version = "16.3"
  instance_class = var.db_instance_class

  allocated_storage     = var.db_storage_gb
  storage_encrypted     = true
  storage_type          = "gp3"
  multi_az              = var.environment == "production" ? true : false

  db_name  = var.db_name
  username = var.db_username
  password = random_password.master.result

  vpc_security_group_ids = [aws_security_group.database.id]
  db_subnet_group_name   = aws_db_subnet_group.main.name

  backup_retention_period = var.environment == "production" ? 30 : 7
  backup_window           = "03:00-04:00"
  maintenance_window      = "sun:04:00-sun:05:00"

  deletion_protection = var.environment == "production" ? true : false
  skip_final_snapshot = var.environment == "production" ? false : true
}

AWS IAM with Terraform

# iam.tf
resource "aws_iam_role" "lambda_exec" {
  name = "${var.environment}-lambda-exec"

  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_exec.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

resource "aws_iam_role_policy" "s3_access" {
  name = "s3-read-write"
  role = aws_iam_role.lambda_exec.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect = "Allow]
      Action = [
        "s3:GetObject",
        "s3:PutObject",
        "s3:ListBucket]
      ]
      Resource = [
        aws_s3_bucket.data.arn,
        "${aws_s3_bucket.data.arn}/*]
      ]
    }]
  })
}

Common Mistakes

1. Hardcoding AMI IDs

AMI IDs change per region. Use data.aws_ami data sources to look up the latest AMI dynamically.

2. Leaving Security Groups Too Permissive

Opening all ports to 0.0.0.0/0 creates security risks. Restrict ingress to specific CIDRs and ports.

3. Skipping Encryption

Unencrypted S3 buckets, EBS volumes, and RDS instances fail compliance audits. Enable encryption by default.

4. Not Using IAM Roles

Hardcoding AWS access keys in configuration is insecure. Use IAM roles with OIDC or environment credentials.

5. Missing Deletion Protection

Production databases without deletion_protection can be accidentally destroyed by <a href="/devops/terraform/">terraform</a> destroy.

Practice Questions

1. How do you configure the AWS provider with IAM role assumption? Set assume_role with role_arn and session_name in the provider "aws" block.

2. How do you retrieve the latest Amazon Linux 2 AMI dynamically? Use the data.aws_ami data source with filters for name, owner-alias, and most_recent = true.

3. What is the purpose of default_tags in the AWS provider? It applies common tags (Environment, ManagedBy, Project) to every resource the provider creates.

4. Challenge: Write a Terraform configuration that provisions an ALB with an EC2 auto-scaling group behind it, using a VPC module, and secured with an ACM certificate.

Mini Project: Complete AWS Web Stack

Provision a VPC with public and private subnets, an ALB in public subnets, an EC2 auto-scaling group in private subnets, an RDS PostgreSQL instance, and an S3 bucket for static assets. Configure security groups, IAM roles, and enable encryption everywhere.

Terraform on Azure
Remote State with S3 & DynamoDB

What's Next

Provision AWS infrastructure with Terraform for your workloads, then explore Azure with Terraform for multi-cloud deployments. Study Cloud Computing patterns for production-grade architectures.

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro