Skip to content

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-identity works)
  • 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. Run aws configure or export AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.
  • UnauthorizedOperation during apply: your IAM identity lacks ec2:RunInstances or ec2:CreateSecurityGroup. Attach the AmazonEC2FullAccess policy for the lab.
  • InvalidAMIID.NotFound: the AMI filter resolved nothing in your region. Confirm var.region and 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.micro rarely 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