Terraform EC2 + Security Group + Docker
Declare your infrastructure instead of clicking it.
Overview
Now the whole environment becomes code. Terraform creates the EC2 instance, a least-privilege security group, an IAM role, a key pair, and basic networking — all reproducible with `terraform apply` and destroyable with `terraform destroy`. This is the jump from 'a server I built' to 'infrastructure I can recreate anywhere'.
Architecture
Terraform talks to the AWS API to create the security group, IAM role, key pair, and EC2 instance. The instance still bootstraps Docker via user-data, but every resource now lives in code and state.
- 1You run `terraform plan` to preview exactly what will change.
- 2`terraform apply` creates the SG, IAM role, key pair, and EC2 instance.
- 3User-data installs Docker and runs the container on boot.
- 4`terraform destroy` removes every resource when you're done.
Terraform CLI calls the AWS API to provision a security group, IAM role, key pair, and an EC2 instance running a Docker container; Terraform state tracks all of it.
What you'll understand
- Express infrastructure as declarative, version-controlled code.
- Understand Terraform state — what it is and why it matters.
- Create a scoped security group and an IAM role instead of clicking in the console.
- Tear everything down cleanly with a single command.
Prerequisites
- Completed Level 1 (Bootstrap Script)
- Terraform CLI installed (v1.6+)
- AWS credentials configured locally (aws configure)
- Understanding of EC2 + security groups from earlier levels
Generated files
The files this template produces. Copy any of them straight into your project.
4 files
Defines the security group, IAM role, and EC2 instance.
terraform {
required_version = ">= 1.6"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.0" }
}
}
provider "aws" {
region = var.region
}
# Scoped security group: SSH from your IP, HTTP/HTTPS from anywhere.
resource "aws_security_group" "web" {
name = "${var.name}-sg"
description = "Web + restricted SSH"
ingress {
description = "SSH from operator"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.operator_cidr]
}
ingress {
description = "HTTP"
from_port = 80
to_port = 80
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 = { Project = var.name }
}
resource "aws_instance" "web" {
ami = var.ami_id
instance_type = "t3.micro"
key_name = var.key_name
vpc_security_group_ids = [aws_security_group.web.id]
iam_instance_profile = aws_iam_instance_profile.web.name
user_data = <<-EOF
#!/usr/bin/env bash
dnf install -y docker && systemctl enable --now docker
docker run -d --name web -p 80:80 --restart unless-stopped nginx:1.27-alpine
EOF
tags = { Name = var.name, Project = var.name }
}Step-by-step guide
- 1
Initialize Terraform
Download the AWS provider and set up the working directory.
terraform initPulls the AWS provider plugin.
- 2
Preview the plan
Always look before you leap. `plan` shows every resource Terraform will create — nothing is applied yet.
terraform plan -var operator_cidr="$(curl -s ifconfig.me)/32"Scopes SSH to your current public IP automatically.
- 3
Apply
Create the security group, IAM role, and instance. Terraform records everything in its state file.
terraform applyType 'yes' to confirm the plan.
The state file describes your real infrastructure — keep it safe (Level 9 moves it to a remote backend).
- 4
Reach the app
Open the public_ip output in a browser. The same box as before — but now it's fully described by code.
- 5
Destroy it
Prove the value of IaC: tear the whole thing down in one command, then recreate it any time.
terraform destroyRemoves every resource it created.
AI insight
Ask the assistant to explain, review, or recommend — authored for this template.
What this architecture is doing
Terraform is now the thing that talks to AWS. From a few .tf files it creates the security group, an IAM role, a key pair, and the EC2 instance, and it remembers what it made in a state file so it can update or destroy it precisely.
- —Declarative: you describe the end state, Terraform makes it so.
- —State = Terraform's memory of your real resources.
- —plan shows the diff before anything changes.
Security notes
SSH is now scoped to your IP
InfoThe security group only allows SSH from operator_cidr, not the whole internet.
IAM role enables keyless access
InfoAttaching AmazonSSMManagedInstanceCore lets you use Session Manager instead of SSH keys.
HTTP still open to the world
LowPort 80 is public (expected for a web app), but there's no TLS in this minimal config.
Reuse the Nginx + Certbot pattern from Level 1, or terminate TLS at a load balancer later.
Cost notes
EC2 t3.micro
Same instance cost as earlier levels; Terraform doesn't add charges, it just manages them.
Leaving it applied
IaC makes it easy to forget a running apply. Destroy when you're done to avoid a surprise bill.
Cleanup guide
Tear it down when you're done — the fastest way to avoid a surprise bill.
- 1
Run terraform destroy — this is the whole point of IaC.
Billingterraform destroy - 2
Confirm no leftover key pairs or Elastic IPs remain in the console.