Terraform AWS Tutorial: Provision EC2, VPC, and S3 Infrastructure
Learn how to use Terraform to provision AWS infrastructure including EC2 instances, VPCs, security groups, and S3 buckets. This hands-on tutorial walks you through real-world examples of managing AWS resources with Infrastructure as Code.
📑 Table of Contents
- Prerequisites
- Setting Up AWS Credentials
- Option 1: AWS CLI Configuration
- Option 2: Environment Variables
- Option 3: Terraform Provider Block
- Project Structure
- Provider Configuration
- Variables Configuration
- Creating a VPC
- Security Groups
- EC2 Instances
- S3 Buckets
- Outputs
- Deploying the Infrastructure
- Testing Your Infrastructure
- Cleaning Up
- Best Practices
- Conclusion
Prerequisites
- Terraform installed (version 1.0+)
- AWS account with programmatic access
- AWS CLI installed and configured
- Basic understanding of AWS services
Setting Up AWS Credentials
Option 1: AWS CLI Configuration
# Configure AWS CLI
aws configure
# Enter your credentials:
AWS Access Key ID: AKIAXXXXXXXXXX
AWS Secret Access Key: xxxxxxxxxxxxxxxxxx
Default region name: us-east-1
Default output format: json
Option 2: Environment Variables
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
export AWS_REGION="us-east-1"
Option 3: Terraform Provider Block
# Not recommended for production - use for testing only
provider "aws" {
region = "us-east-1"
access_key = "your-access-key"
secret_key = "your-secret-key"
}
Project Structure
aws-terraform-project/
├── main.tf
├── variables.tf
├── outputs.tf
├── providers.tf
├── vpc.tf
├── ec2.tf
├── s3.tf
├── security-groups.tf
└── terraform.tfvars
Provider Configuration
Create providers.tf:
terraform {
required_version = ">= 1.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
ManagedBy = "Terraform"
Project = var.project_name
}
}
}
Variables Configuration
Create variables.tf:
variable "aws_region" {
description = "AWS region to deploy resources"
type = string
default = "us-east-1"
}
variable "environment" {
description = "Environment name"
type = string
default = "dev"
}
variable "project_name" {
description = "Project name for tagging"
type = string
default = "terraform-demo"
}
variable "vpc_cidr" {
description = "CIDR block for VPC"
type = string
default = "10.0.0.0/16"
}
variable "public_subnet_cidrs" {
description = "CIDR blocks for public subnets"
type = list(string)
default = ["10.0.1.0/24", "10.0.2.0/24"]
}
variable "private_subnet_cidrs" {
description = "CIDR blocks for private subnets"
type = list(string)
default = ["10.0.10.0/24", "10.0.20.0/24"]
}
variable "instance_type" {
description = "EC2 instance type"
type = string
default = "t3.micro"
}
variable "key_name" {
description = "SSH key pair name"
type = string
default = ""
}
Creating a VPC
Create vpc.tf:
# Get available AZs
data "aws_availability_zones" "available" {
state = "available"
}
# VPC
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "vpc-main"
}
}
# Internet Gateway
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "igw-main"
}
}
# Public Subnets
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 = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = {
Name = "subnet-public-${count.index + 1}"
Type = "Public"
}
}
# Private Subnets
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 = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "subnet-private-${count.index + 1}"
Type = "Private"
}
}
# Public Route Table
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 = "rt-public"
}
}
# Associate public subnets with public route table
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
}
Security Groups
Create security-groups.tf:
# Web Server Security Group
resource "aws_security_group" "web" {
name = "sg-web-server"
description = "Security group for web servers"
vpc_id = aws_vpc.main.id
# HTTP
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTP"
}
# HTTPS
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTPS"
}
# SSH
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # Restrict in production!
description = "SSH"
}
# All outbound traffic
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "sg-web-server"
}
}
# Database Security Group
resource "aws_security_group" "db" {
name = "sg-database"
description = "Security group for databases"
vpc_id = aws_vpc.main.id
# MySQL from web servers only
ingress {
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = [aws_security_group.web.id]
description = "MySQL from web servers"
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "sg-database"
}
}
EC2 Instances
Create ec2.tf:
# Get 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"]
}
}
# Get latest Ubuntu 22.04 AMI
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
}
# EC2 Instance
resource "aws_instance" "web" {
ami = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
subnet_id = aws_subnet.public[0].id
vpc_security_group_ids = [aws_security_group.web.id]
key_name = var.key_name != "" ? var.key_name : null
root_block_device {
volume_size = 20
volume_type = "gp3"
encrypted = true
delete_on_termination = true
}
user_data = <<-EOF
#!/bin/bash
yum update -y
yum install -y httpd
systemctl start httpd
systemctl enable httpd
echo "Hello from Terraform!
Instance: $(hostname)
" > /var/www/html/index.html
EOF
tags = {
Name = "web-server-1"
Role = "webserver"
}
}
# Elastic IP for web server
resource "aws_eip" "web" {
instance = aws_instance.web.id
domain = "vpc"
tags = {
Name = "eip-web-server"
}
}
S3 Buckets
Create s3.tf:
# Generate random suffix for unique bucket name
resource "random_id" "bucket_suffix" {
byte_length = 4
}
# S3 Bucket for static assets
resource "aws_s3_bucket" "assets" {
bucket = "assets-bucket-${var.environment}-${random_id.bucket_suffix.hex}"
tags = {
Name = "Assets Bucket"
}
}
# Bucket versioning
resource "aws_s3_bucket_versioning" "assets" {
bucket = aws_s3_bucket.assets.id
versioning_configuration {
status = "Enabled"
}
}
# Bucket encryption
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"
}
}
}
# Block public access
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
}
# S3 Bucket for logs
resource "aws_s3_bucket" "logs" {
bucket = "logs-bucket-${var.environment}-${random_id.bucket_suffix.hex}"
tags = {
Name = "Logs Bucket"
}
}
# Lifecycle policy for logs
resource "aws_s3_bucket_lifecycle_configuration" "logs" {
bucket = aws_s3_bucket.logs.id
rule {
id = "archive-old-logs"
status = "Enabled"
transition {
days = 30
storage_class = "STANDARD_IA"
}
transition {
days = 90
storage_class = "GLACIER"
}
expiration {
days = 365
}
}
}
Outputs
Create outputs.tf:
output "vpc_id" {
description = "VPC ID"
value = aws_vpc.main.id
}
output "public_subnet_ids" {
description = "Public subnet IDs"
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
description = "Private subnet IDs"
value = aws_subnet.private[*].id
}
output "web_server_public_ip" {
description = "Public IP of web server"
value = aws_eip.web.public_ip
}
output "web_server_public_dns" {
description = "Public DNS of web server"
value = aws_instance.web.public_dns
}
output "assets_bucket_name" {
description = "Assets S3 bucket name"
value = aws_s3_bucket.assets.bucket
}
output "logs_bucket_name" {
description = "Logs S3 bucket name"
value = aws_s3_bucket.logs.bucket
}
Deploying the Infrastructure
# Initialize Terraform
terraform init
# Validate configuration
terraform validate
# Preview changes
terraform plan
# Apply changes
terraform apply
# View outputs
terraform output
Testing Your Infrastructure
# Get the public IP
terraform output web_server_public_ip
# Test web server (wait a minute for user_data to complete)
curl http://$(terraform output -raw web_server_public_ip)
# SSH to instance (if key_name was provided)
ssh -i your-key.pem ec2-user@$(terraform output -raw web_server_public_ip)
Cleaning Up
# Destroy all resources
terraform destroy
# Confirm with yes
Best Practices
- Use variables for values that change between environments
- Enable encryption for S3 buckets and EBS volumes
- Restrict security groups to specific IP ranges in production
- Use remote state for team collaboration
- Tag all resources for cost tracking and organization
- Use data sources for AMIs instead of hardcoding IDs
Conclusion
You have learned how to provision a complete AWS infrastructure using Terraform, including VPC, subnets, security groups, EC2 instances, and S3 buckets. This foundation can be expanded to include RDS databases, load balancers, auto-scaling groups, and more complex architectures.
Was this article helpful?
About Ramesh Sundararamaiah
Red Hat Certified Architect
Expert in Linux system administration, DevOps automation, and cloud infrastructure. Specializing in Red Hat Enterprise Linux, CentOS, Ubuntu, Docker, Ansible, and enterprise IT solutions.