Launching Your First EC2 Instance with a Security Group
Scenario
You have just joined a small startup as their first cloud engineer. The team needs a simple web server running on AWS to host an internal tool. Your manager has asked you to provision it using Terraform so it can be version-controlled and reproduced easily.
Your task is to deploy a single Amazon EC2 instance running Amazon Linux 2, open port 80 for HTTP and port 22 for SSH, and output the public IP so the team can access it immediately.
- Configure the aws provider for the us-east-1 region.
- Use a data source to look up the latest Amazon Linux 2 AMI — never hardcode AMI IDs.
- Create an aws_security_group allowing inbound HTTP (80) and SSH (22) from anywhere, and all outbound traffic.
- Create an aws_instance using the looked-up AMI, attached to the security group, with a t3.micro instance type.
- Add meaningful tags: Name, Environment, and ManagedBy.
- Output the instance's public IP and instance ID.
Additional Context
The startup is using a single shared AWS account for now. There is no VPC customisation yet — use the default VPC. The instance does not need persistent storage beyond the root volume. Keep the root volume at 20 GB with the gp3 volume type. The instance should have a public IP automatically assigned so the team can SSH in immediately.
versions.tf
# versions.tf
# Lock the Terraform and provider versions so this config is reproducible.
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}variables.tf
# variables.tf
variable "aws_region" {
description = "AWS region to deploy into."
type = string
default = "us-east-1"
}
variable "instance_type" {
description = "EC2 instance type."
type = string
default = "t3.micro"
}
variable "environment" {
description = "Environment label used in tags."
type = string
default = "dev"
}main.tf
# main.tf
provider "aws" {
region = var.aws_region
}
# Look up the latest Amazon Linux 2 AMI.
# Using a data source means you always get the current patched image
# rather than a hardcoded AMI ID that goes stale or changes per-region.
data "aws_ami" "amazon_linux_2" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
# Security group — controls what traffic can reach the instance.
resource "aws_security_group" "web_sg" {
name = "web-server-sg-${var.environment}"
description = "Allow HTTP and SSH inbound traffic"
# Allow HTTP from anywhere (0.0.0.0/0 = all IPv4 addresses)
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "HTTP from internet"
}
# Allow SSH — in production, lock this to your IP range.
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
description = "SSH access"
}
# Allow all outbound traffic (required to install packages, call APIs, etc.)
egress {
from_port = 0
to_port = 0
protocol = "-1" # -1 means all protocols
cidr_blocks = ["0.0.0.0/0"]
description = "All outbound traffic"
}
tags = {
Name = "web-server-sg"
Environment = var.environment
ManagedBy = "terraform"
}
}
# EC2 instance — the virtual machine itself.
resource "aws_instance" "web_server" {
# AMI ID comes from the data source above — always current.
ami = data.aws_ami.amazon_linux_2
instance_type = var.instance_type
# Attach the security group we just defined.
vpc_security_group_ids = [aws_security_group.web_sg]
# Automatically assign a public IP (needed to access via SSH/HTTP).
associate_public_ip_address = true
# Root volume: 20 GB gp3 (faster and cheaper than gp2).
root_block_device {
volume_size = 20
volume_type = "gp3"
encrypted = true
}
tags = {
Name = "web-server"
Environment = var.environment
ManagedBy = "terraform"
}
}outputs.tf
# outputs.tf
output "instance_id" {
description = "EC2 instance ID."
value = aws_instance.web_server.
}
output "public_ip" {
description = "Public IP address of the web server."
value = aws_instance.web_server.public_ip
}
output "ami_used" {
description = "AMI ID that was resolved and used."
value = data.aws_ami.amazon_linux_2
}Workflow Commands
# Initialise — downloads the AWS provider plugin
terraform init
# Preview the plan — see what Terraform will create before touching AWS
terraform plan
# Apply — creates the resources (approx. 30 seconds)
terraform apply
# Test the web server is reachable (replace with your actual IP from output)
curl http://$(terraform output -raw public_ip)
# Destroy when finished to avoid ongoing charges
terraform destroyApply complete! Resources: 2 added, 0 changed, 0 destroyed. Outputs: ami_used = "ami-0abcdef1234567890" instance_id = "i-0abcdef1234567890" public_ip = "54.123.45.67"

