beginner ~75 min updated 2026-06-01
Terraform AWS EC2
Provision an EC2 instance, security group, and key pair on AWS Free Tier with Terraform. Practice the init, plan, apply, and destroy lifecycle with variables, outputs, and state inspection.
Objective
Provision and destroy a Free Tier EC2 instance with a security group using Terraform, and read its public IP from an output. You will exercise the full Terraform lifecycle: init, fmt, validate, plan, apply, state inspection, and destroy.
Prerequisites
- A free AWS account (this lab stays inside the Free Tier with
t3.micro) - AWS CLI v2 installed and authenticated (
aws sts get-caller-identityworks) - Terraform 1.7 or newer installed
- An IAM identity with permissions for EC2 and VPC describe/create actions
Architecture
Terraform reads the HCL configuration, builds a dependency graph, and calls the AWS API to create resources in your default VPC: one security group allowing SSH from your IP, and one t3.micro instance running Amazon Linux 2023 resolved dynamically through a data source. State is stored locally in terraform.tfstate.
terraform CLI AWS (us-east-1)
+--------------+ API +-----------------------+
| main.tf | ------> | default VPC |
| variables.tf | | +------------------+ |
| outputs.tf | | | Security Group | |
| | | | ingress 22/tcp | |
| local state | | +--------+---------+ |
+--------------+ | | |
| +--------v---------+ |
| | EC2 t3.micro | |
| | Amazon Linux 2023| |
| +------------------+ |
+-----------------------+
Steps
1. Create the project and provider configuration
mkdir terraform-ec2-lab && cd terraform-ec2-lab
# main.tf
terraform {
required_version = ">= 1.7.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = var.region
}
data "aws_ami" "al2023" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-2023*-x86_64"]
}
}
resource "aws_security_group" "lab" {
name_prefix = "tf-lab-"
description = "Allow SSH from my IP"
ingress {
description = "SSH"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = [var.my_ip_cidr]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
}
resource "aws_instance" "lab" {
ami = data.aws_ami.al2023.id
instance_type = var.instance_type
vpc_security_group_ids = [aws_security_group.lab.id]
tags = {
Name = "terraform-lab"
Environment = "lab"
}
}
2. Add variables and outputs
# variables.tf
variable "region" {
type = string
default = "us-east-1"
}
variable "instance_type" {
type = string
default = "t3.micro"
}
variable "my_ip_cidr" {
type = string
description = "Your public IP in CIDR form, e.g. 203.0.113.7/32"
}
# outputs.tf
output "instance_id" {
value = aws_instance.lab.id
}
output "public_ip" {
value = aws_instance.lab.public_ip
}
3. Initialize and validate
terraform init
terraform fmt
terraform validate
4. Plan with your IP
export TF_VAR_my_ip_cidr="$(curl -s https://checkip.amazonaws.com)/32"
terraform plan -out=tfplan
Review the plan: it should show 2 resources to add.
5. Apply
terraform apply tfplan
6. Inspect state and verify in AWS
terraform state list
terraform output public_ip
aws ec2 describe-instances \
--instance-ids "$(terraform output -raw instance_id)" \
--query 'Reservations[0].Instances[0].State.Name' --output text
Expected output
$ terraform plan -out=tfplan
Plan: 2 to add, 0 to change, 0 to destroy.
$ terraform apply tfplan
aws_security_group.lab: Creation complete after 3s [id=sg-0a1b2c3d4e5f]
aws_instance.lab: Creation complete after 33s [id=i-0123456789abcdef0]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
Outputs:
instance_id = "i-0123456789abcdef0"
public_ip = "54.211.34.120"
$ terraform state list
data.aws_ami.al2023
aws_instance.lab
aws_security_group.lab
Troubleshooting
Error: configuring Terraform AWS Provider: no valid credential sources: the AWS CLI is not configured. Runaws configureor exportAWS_ACCESS_KEY_IDandAWS_SECRET_ACCESS_KEY.UnauthorizedOperationduring apply: your IAM identity lacksec2:RunInstancesorec2:CreateSecurityGroup. Attach theAmazonEC2FullAccesspolicy for the lab.InvalidAMIID.NotFound: the AMI filter resolved nothing in your region. Confirmvar.regionand that the data source filter matches (aws ec2 describe-images --owners amazon --filters "Name=name,Values=al2023-ami-2023*-x86_64" --query 'Images[0].Name').- Plan wants to recreate the instance on every run: the dynamic AMI lookup found a newer image. Pin the AMI id in a variable or add
lifecycle { ignore_changes = [ami] }for the lab. VcpuLimitExceeded: your account hit the vCPU quota in that region. Switch region or request a quota increase;t3.microrarely hits this on fresh accounts.
Cleanup
terraform destroy -auto-approve
cd .. && rm -rf terraform-ec2-lab
# Verify nothing is left running:
aws ec2 describe-instances \
--filters "Name=tag:Name,Values=terraform-lab" "Name=instance-state-name,Values=running" \
--query 'Reservations' --output text