Provision Your First AWS Infrastructure with Terraform

Learn Terraform from scratch by provisioning an S3 bucket, EC2 instance, and security group on AWS using infrastructure as code with state management.

Domains & Technologies

Domains
AWS-CLOUD-ENGINEERING
Technologies
TERRAFORMAWS

Blueprint Walkthrough

Architecture Overview

This project introduces Terraform — the most widely used Infrastructure as Code tool in the DevOps industry. Instead of clicking through the AWS console to create resources, you describe what you want in a .tf file, run one command, and Terraform creates everything automatically.

You will provision three real AWS resources: an S3 bucket for file storage, an EC2 instance to run a server, and a security group to control access. You will also learn how Terraform tracks what it has created using a state file — the mechanism that makes updates and deletions safe.

Bash
Your Terraform Code (.tf files)
|
v
terraform plan
(shows what will change)
|
v
terraform apply
(creates the resources)
|
+----+----+
| |
v v
S3 Bucket EC2 Instance
(storage) (+ security group)
| |
+---------+
|
terraform.tfstate
(tracks what exists)
Problem Solved

Manually creating AWS resources through the console causes two major problems. First, nobody can see exactly how the infrastructure was built — there is no record of which settings were chosen, which checkboxes were ticked, or why. When something breaks, reconstruction is guesswork.

Second, creating the same setup in a second environment (staging, production) means clicking through every screen again and hoping you match the original exactly. Mistakes creep in. Environments drift apart.

Terraform solves both problems. Your .tf files are the exact specification of your infrastructure — checked into Git, reviewed in pull requests, and applied identically across every environment.

Step-by-Step Implementation Guide

Step 1: Install Terraform and Configure AWS

Bash
## Install Terraform using tfenv (manages multiple versions)
git clone https://github.com/tfutils/tfenv.git ~/.tfenv
export PATH="$HOME/.tfenv/bin:$PATH"
echo 'export PATH="$HOME/.tfenv/bin:$PATH"' >> ~/.bashrc
tfenv install 1.7.0
tfenv use 1.7.0
terraform --version
## Expected: Terraform v1.7.0
## Configure AWS credentials
## Create an IAM user in AWS console with programmatic access
## Attach the AdministratorAccess policy (for learning only)
aws configure
## AWS Access Key ID: your-access-key
## AWS Secret Access Key: your-secret-key
## Default region name: ap-south-1
## Default output format: json
## Verify AWS credentials work
aws sts get-caller-identity
## Expected: Shows your AWS account ID and IAM user name
Security

For this learning project, AdministratorAccess is acceptable. In a real job, always use the principle of least privilege — only grant the permissions Terraform actually needs. Create specific IAM policies for each use case.

Step 2: Understand the Terraform Workflow

Before writing any code, understand the three commands you will use constantly:

  • terraform init — Downloads the AWS provider plugin. Run once per new project.
  • terraform plan — Shows what Terraform WILL DO without actually doing it. Always run this before apply.
  • terraform apply — Actually creates, updates, or destroys resources. Always review the plan first.
  • terraform destroy — Deletes everything Terraform created. Useful for cleanup.
Bash
## Create your project directory
mkdir terraform-basics && cd terraform-basics

Step 3: Write Your First Terraform Configuration

Create main.tf:

HCL
## Tell Terraform which providers (cloud SDKs) to use
terraform {
required_version = ">= 1.7.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
## Configure the AWS provider
## This is how Terraform knows which region to use
provider "aws" {
region = "ap-south-1" # Mumbai, India
}

Create variables.tf — this keeps your configuration flexible:

HCL
## Variables let you reuse the same code with different values
## Think of them like function parameters
variable "project_name" {
description = "Name prefix for all resources we create"
type = string
default = "devops-learning"
}
variable "environment" {
description = "Deployment environment"
type = string
default = "development"
# Validation ensures only valid values are accepted
validation {
condition = contains(
["development", "staging", "production"],
var.environment
)
error_message = "Must be development, staging, or production."
}
}
variable "my_ip" {
description = "Your IP for SSH access (find at checkip.amazonaws.com)"
type = string
}

Create outputs.tf — values Terraform prints after applying:

HCL
## Outputs are like the return value of your Terraform code
## They print useful information after terraform apply completes
output "s3_bucket_name" {
description = "Name of the created S3 bucket"
value = aws_s3_bucket.main.bucket
}
output "ec2_public_ip" {
description = "Public IP of the EC2 instance"
value = aws_instance.web.public_ip
}
output "ec2_instance_id" {
description = "Instance ID for AWS console"
value = aws_instance.web.id
}

Step 4: Create an S3 Bucket

Add to main.tf:

HCL
## Local values — computed values reused throughout the config
locals {
common_tags = {
Project = var.project_name
Environment = var.environment
ManagedBy = "terraform"
Owner = "devops-network-student"
}
}
## S3 Bucket for file storage
## Resource format: resource "TYPE" "LOCAL_NAME"
## TYPE = AWS resource type
## LOCAL_NAME = name used within Terraform code only
resource "aws_s3_bucket" "main" {
# Bucket names must be globally unique across ALL AWS accounts
# Adding a random suffix ensures uniqueness
bucket = "${var.project_name}-${var.environment}-files"
tags = local.common_tags
}
## Enable versioning — keeps old versions of files when overwritten
resource "aws_s3_bucket_versioning" "main" {
bucket = aws_s3_bucket.main.id
versioning_configuration {
status = "Enabled"
}
}
## Block all public access — S3 buckets are private by default
## This resource makes that explicit and prevents accidental exposure
resource "aws_s3_bucket_public_access_block" "main" {
bucket = aws_s3_bucket.main.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
## Enable server-side encryption — all files encrypted at rest
resource "aws_s3_bucket_server_side_encryption_configuration" "main" {
bucket = aws_s3_bucket.main.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}

Step 5: Create a Security Group and EC2 Instance

Add to main.tf:

HCL
## Security Group — controls which traffic can reach the EC2 instance
## Like a virtual firewall specific to this instance
resource "aws_security_group" "web" {
name = "${var.project_name}-web-sg"
description = "Allow SSH from my IP and HTTP from anywhere"
# Allow SSH only from your IP address
ingress {
description = "SSH from my IP only"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["${var.my_ip}/32"]
}
# Allow HTTP web traffic from anywhere
ingress {
description = "HTTP from internet"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Allow all outbound traffic
# (so the server can download packages, reach AWS APIs, etc.)
egress {
description = "All outbound traffic"
from_port = 0
to_port = 0
protocol = "-1" # -1 means all protocols
cidr_blocks = ["0.0.0.0/0"]
}
tags = local.common_tags
}
## Find the latest Ubuntu 22.04 AMI automatically
## This data source queries AWS for current AMI IDs
## Much better than hardcoding an AMI ID that may become outdated
data "aws_ami" "ubuntu" {
most_recent = true
owners = ["099720109477"] # Canonical (Ubuntu's publisher)
filter {
name = "name"
values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
## EC2 Instance
resource "aws_instance" "web" {
ami = data.aws_ami.ubuntu.id
instance_type = "t2.micro" # Free tier eligible
vpc_security_group_ids = [aws_security_group.web.id]
# User data runs when the instance first boots
# This installs a basic web server automatically
user_data = <<-EOF
#!/bin/bash
apt update -y
apt install -y nginx
systemctl start nginx
systemctl enable nginx
echo '<h1>Provisioned by Terraform!</h1>' \
> /var/www/html/index.html
EOF
tags = merge(local.common_tags, {
Name = "${var.project_name}-web-server"
})
}

Step 6: Initialise, Plan, and Apply

Bash
## Step 1: Initialise Terraform
## Downloads the AWS provider plugin (only needed once per project)
terraform init
## Expected: Terraform has been successfully initialized!
## Step 2: Validate your configuration syntax
terraform validate
## Expected: Success! The configuration is valid.
## Step 3: Create a terraform.tfvars file with your values
cat > terraform.tfvars << EOF
project_name = "devops-learning"
environment = "development"
my_ip = "$(curl -s https://checkip.amazonaws.com)"
EOF
## Step 4: Preview what Terraform will create
terraform plan
## Expected: Plan: 7 to add, 0 to change, 0 to destroy
## Read every line — understand what is being created
## Step 5: Apply the configuration
terraform apply
## Type 'yes' when prompted to confirm
## Expected: Apply complete! Resources: 7 added.
## Step 6: See the outputs
terraform output
## Expected:
## ec2_instance_id = "i-0abc123"
## ec2_public_ip = "13.x.x.x"
## s3_bucket_name = "devops-learning-development-files"
Tip

Always run terraform plan before terraform apply. The plan output tells you exactly what will be created, changed, or destroyed. Never apply without reviewing the plan — this habit saves you from expensive mistakes.

Step 7: Explore State and Make a Change

The state file is how Terraform knows what already exists.

Bash
## View the state file (do not edit manually)
cat terraform.tfstate
## Expected: JSON file listing all created resources and their IDs
## List all resources in state
terraform state list
## Expected:
## aws_instance.web
## aws_s3_bucket.main
## aws_s3_bucket_versioning.main
## aws_s3_bucket_public_access_block.main
## aws_s3_bucket_server_side_encryption_configuration.main
## aws_security_group.web
## data.aws_ami.ubuntu
## Show details of a specific resource
terraform state show aws_instance.web
## Expected: All attributes of the EC2 instance
## Make a change — add a tag to the EC2 instance
## Edit main.tf, add 'Team = "platform"' to the instance tags
## Then run plan to see the change
terraform plan
## Expected: Plan: 0 to add, 1 to change, 0 to destroy
## The ~ symbol means modify in place
terraform apply
## Terraform only changes the tag — everything else stays the same

Step 8: Clean Up

Bash
## Destroy all resources when done
## This is important — EC2 instances cost money when running
terraform plan -destroy
## Review what will be deleted
terraform destroy
## Type 'yes' to confirm
## Expected: Destroy complete! Resources: 7 destroyed.
## Verify in AWS console that everything is gone
aws ec2 describe-instances \
--filters "Name=tag:Project,Values=devops-learning" \
--query 'Reservations[].Instances[].State.Name'
## Expected: ["terminated"]
Validation & Testing
Bash
## 1. Verify S3 bucket was created with encryption
aws s3api get-bucket-encryption \
--bucket devops-learning-development-files
## Expected: SSEAlgorithm = AES256
## 2. Verify S3 bucket blocks public access
aws s3api get-public-access-block \
--bucket devops-learning-development-files
## Expected: all four settings = true
## 3. Verify EC2 is running and Nginx works
EC2_IP=$(terraform output -raw ec2_public_ip)
curl http://$EC2_IP
## Expected: HTML with 'Provisioned by Terraform!'
## 4. Test idempotency (the most important Terraform property)
## Run apply again — nothing should change
terraform apply
## Expected: Apply complete! Resources: 0 added, 0 changed, 0 destroyed.
## Running apply twice is safe — Terraform only changes what differs
## 5. Verify state file exists
ls -la terraform.tfstate
## Expected: File exists with recent timestamp
## 6. Test changing a variable value
## Change environment to 'staging' in terraform.tfvars
## Run plan — Terraform will want to recreate resources
## with the new name (staging instead of development)
terraform plan
## Expected: Several resources to be destroyed and created
## (because the bucket name includes the environment)
## This shows how Terraform handles configuration changes
## 7. Clean up before paying for EC2
terraform destroy --auto-approve
echo "Terraform project complete!"