Deploy a Microservice with Terraform
Use Terraform to provision AWS ECS Fargate and deploy a containerized microservice.
Introduction
30 Second Summary
Imagine if every time you needed a new server, you had to manually click through dozens of settings, hope you remembered the right configuration, and pray nothing broke. That is how most teams used to manage their cloud infrastructure.
In this project, you will define an entire cloud infrastructure in code using Terraform, then deploy a live containerized microservice on AWS with a single command. You will also destroy and rebuild it to experience the power of Infrastructure as Code.
What You'll Build
A fully functional microservice infrastructure on AWS, defined entirely in Terraform code, that provisions networking, a container cluster, and a live web service accessible from your browser.
By the end of this project, you'll have:
- A Terraform project with provider configuration, networking resources, and ECS Fargate service definitions.
- A running containerized microservice on AWS ECS Fargate, accessible via a public IP address in your browser.
- Hands-on experience with the full Terraform lifecycle: writing infrastructure code, planning changes, deploying resources, and destroying them.
- Secret Mission: Add a second microservice (an API backend) to your cluster and run both services on the same infrastructure.
Are there any prerequisites?
You need an AWS account (free tier eligible, but requires a credit card on file) and Homebrew installed on macOS. No prior experience with Terraform, AWS, or containers is required.
Define Your Network Infrastructure
Your tools are installed and your project directory is ready. Now it's time to write the Terraform code that defines the cloud network your microservice will live in.
Every cloud application needs a network foundation before it can run. In this step, you'll use HCL (HashiCorp Configuration Language) to define a VPC, subnets, and a security group that controls traffic to your service.
In this step, get ready to:
- Configure the AWS provider and create a VPC.
- Add public subnets and an internet gateway.
- Create a security group for HTTP traffic.
Configure the AWS provider and create a VPC
Before Terraform can create any AWS resources, it needs to know which cloud provider to use and where to deploy. You'll configure this in two files: a variable definition and the main configuration.
- Define the AWS region variable in variables.tf by pasting this code:
variable "aws_region" {
description = "AWS region to deploy resources"
type = string
default = "us-east-1"
}
What does this code do?
This defines a reusable variable called aws_region with a default value of us-east-1. Instead of hardcoding the region everywhere, you reference this variable so the entire project can be redeployed to a different region by changing one value.
- Save variables.tf.
- You won't see a change yet. The provider in the next chunk uses this variable.
- Switch to main.tf and paste the following provider configuration:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
required_version = ">= 1.5.0"
}
provider "aws" {
region = var.aws_region
}
What does this code do?
The terraform block tells Terraform which provider plugin to download. You're pinning the AWS provider to version ~> 6.0 (any 6.x release).
The provider block configures how Terraform connects to AWS. It reads the region from var.aws_region, which resolves to us-east-1 from the variable you just defined.
- Save main.tf.
- Initialize the Terraform project by running this command in your terminal:
terraform init
- You should see Terraform download the AWS provider plugin and print Terraform has been successfully initialized! at the end of the output.
What does terraform init do?
This command reads your required_providers block, downloads the AWS provider plugin, and stores it in a hidden .terraform directory. It also creates a .terraform.lock.hcl file that locks the exact provider version for consistency.
Seeing an error during init?
Make sure your main.tf file is saved. Terraform reads from disk, so unsaved changes in your editor won't be picked up.
If you see a credentials error, confirm that aws configure was completed in the previous step with valid keys.
Help me troubleshoot my terraform init error.
Now that the provider is initialized, you can start defining resources.
- In main.tf, add the VPC resource below the provider block:
# --- Networking ---
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "microservice-vpc"
}
}
What is a VPC?
A Virtual Private Cloud is your own isolated section of the AWS network. Think of it as renting a private floor in a massive office building.
The cidr_block of 10.0.0.0/16 gives your VPC 65,536 private IP addresses to work with. enable_dns_hostnames lets resources inside the VPC receive DNS names, which is required for ECS tasks to communicate externally.
- Save main.tf.
- You won't see a visible change yet. The next chunk adds subnets and routing so the VPC can reach the internet.
Add public subnets and an internet gateway
A VPC alone is a sealed box. To let traffic flow in and out, you need subnets (subdivisions of the network) and a gateway that connects them to the public internet.
- In main.tf, add the two subnet resources below the VPC:
resource "aws_subnet" "public_a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "${var.aws_region}a"
map_public_ip_on_launch = true
tags = {
Name = "microservice-public-a"
}
}
resource "aws_subnet" "public_b" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
availability_zone = "${var.aws_region}b"
map_public_ip_on_launch = true
tags = {
Name = "microservice-public-b"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "microservice-igw"
}
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "microservice-public-rt"
}
}
resource "aws_route_table_association" "public_a" {
subnet_id = aws_subnet.public_a.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "public_b" {
subnet_id = aws_subnet.public_b.id
route_table_id = aws_route_table.public.id
}
What does this code do?
- aws_subnet.public_a and aws_subnet.public_b create two subnets in different availability zones for redundancy. map_public_ip_on_launch = true ensures any container placed here gets a public IP address.
- aws_internet_gateway.main is the door between your VPC and the public internet.
- aws_route_table.public sends all traffic destined for 0.0.0.0/0 (anywhere outside the VPC) through the internet gateway.
- The two aws_route_table_association resources link each subnet to the route table so both subnets use the same routing rules.
Why two subnets in different availability zones?
AWS availability zones are physically separate data centers within a region. Spreading your subnets across two zones (us-east-1a and us-east-1b) means your service can survive if one data center has an outage. ECS Fargate requires at least two subnets for high availability.
- Save main.tf.
- You won't see a visible change yet. The security group in the next chunk completes the network, and then you'll verify everything with terraform plan.
Create a security group
The last piece of your network is a security group. This acts as a firewall that controls which traffic can reach your microservice.
- In main.tf, add the security group below the route table associations:
resource "aws_security_group" "ecs_service" {
name = "microservice-ecs-sg"
description = "Allow HTTP inbound traffic for ECS service"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTP from anywhere"
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 = {
Name = "microservice-ecs-sg"
}
}
What does this security group do?
The ingress block allows inbound HTTP traffic on port 80 from any IP address (0.0.0.0/0). This is what lets people visit your microservice from their browser.
The egress block allows all outbound traffic. Your container needs outbound access to pull its image from Docker Hub during startup.
- Save main.tf.
- Verify your entire configuration by running this command:
terraform plan
- Terraform outputs a list of resources it plans to create. You should see your VPC, two subnets, the internet gateway, route table, route table associations, and security group in the plan. No errors should appear.
What does terraform plan do?
The plan command reads your .tf files and calculates what changes are needed to match your desired state. It shows you exactly what Terraform will create, modify, or destroy without actually touching AWS. Think of it as a dry run.
Seeing errors in terraform plan?
If you see a syntax error, check for missing closing braces } or mismatched quotes in main.tf. HCL is sensitive to bracket matching.
If you see a provider error, make sure you ran terraform init after adding the required_providers block.
Help me fix my terraform plan error.
✔️ Awesome, I've got everything!
Great. Double check you've saved both main.tf and variables.tf.
ⓧ I'd like to double check the full code
Here is the complete main.tf at this point:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
required_version = ">= 1.5.0"
}
provider "aws" {
region = var.aws_region
}
# --- Networking ---
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "microservice-vpc"
}
}
resource "aws_subnet" "public_a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "${var.aws_region}a"
map_public_ip_on_launch = true
tags = {
Name = "microservice-public-a"
}
}
resource "aws_subnet" "public_b" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
availability_zone = "${var.aws_region}b"
map_public_ip_on_launch = true
tags = {
Name = "microservice-public-b"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "microservice-igw"
}
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "microservice-public-rt"
}
}
resource "aws_route_table_association" "public_a" {
subnet_id = aws_subnet.public_a.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "public_b" {
subnet_id = aws_subnet.public_b.id
route_table_id = aws_route_table.public.id
}
resource "aws_security_group" "ecs_service" {
name = "microservice-ecs-sg"
description = "Allow HTTP inbound traffic for ECS service"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTP from anywhere"
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 = {
Name = "microservice-ecs-sg"
}
}
And here is the complete variables.tf:
variable "aws_region" {
description = "AWS region to deploy resources"
type = string
default = "us-east-1"
}
Your entire network is defined in code. Next up, you'll add the ECS cluster and deploy your container onto this network.
Deploy Your ECS Fargate Cluster
Your network is ready. You have a VPC, two public subnets, an internet gateway, and a security group that allows HTTP traffic.
Now you need somewhere to actually run your container. ECS Fargate handles container orchestration without requiring you to manage servers. In this step, you'll define the cluster, permissions, container specification, and service that together launch a running microservice.
In this step, get ready to:
- Create an ECS cluster and configure IAM permissions.
- Define a Fargate task definition with an nginx container.
- Create an ECS service to launch your container on the network.
Create the ECS cluster and IAM execution role
An ECS cluster is a logical grouping for your containers. It does not provision any servers on its own. It acts as the namespace where your services and tasks live.
- In main.tf, scroll to the bottom of the file (after the security group resource) and add this code:
# --- ECS Cluster ---
resource "aws_ecs_cluster" "main" {
name = "microservice-cluster"
tags = {
Name = "microservice-cluster"
}
}
What does this code do?
This creates an ECS cluster named microservice-cluster. Containers don't run here directly. Instead, you'll create services that launch tasks inside this cluster.
- Save main.tf.
- You won't see a change yet. The next chunk configures the permissions this cluster needs to pull container images.
When ECS launches a container, it needs permission to pull the container image and send logs to AWS. An IAM execution role grants these permissions.
- In main.tf, add this code directly below the ECS cluster resource:
# --- IAM Role for ECS Task Execution ---
resource "aws_iam_role" "ecs_task_execution" {
name = "microservice-ecs-execution-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}
]
})
tags = {
Name = "microservice-ecs-execution-role"
}
}
resource "aws_iam_role_policy_attachment" "ecs_task_execution" {
role = aws_iam_role.ecs_task_execution.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
What does this code do?
The aws_iam_role resource creates a role that the ECS service is allowed to assume. The assume_role_policy says only the ecs-tasks.amazonaws.com service can use this role.
The aws_iam_role_policy_attachment resource attaches AWS's managed policy AmazonECSTaskExecutionRolePolicy to your role. This policy grants permission to pull container images from ECR and write logs to CloudWatch.
- Save main.tf.
- Confirm your syntax is valid by running this command in your terminal:
terraform validate
- You should see Success! The configuration is valid. printed in your terminal.
Seeing a validation error?
- Check that every opening brace { has a matching closing brace }. The jsonencode block has nested braces that are easy to miss.
- Confirm you placed the new code after the last closing brace of the security group resource, not inside it.
- Help me debug this issue
Define a Fargate task definition
A task definition is a blueprint for your container. It tells ECS which image to pull, how much CPU and memory to allocate, and which ports to expose.
- In main.tf, add this code below the IAM policy attachment resource:
# --- ECS Task Definition ---
resource "aws_ecs_task_definition" "web" {
family = "microservice-web"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = "256"
memory = "512"
execution_role_arn = aws_iam_role.ecs_task_execution.arn
container_definitions = jsonencode([
{
name = "web"
image = "nginx:alpine"
essential = true
portMappings = [
{
containerPort = 80
hostPort = 80
protocol = "tcp"
}
]
}
])
tags = {
Name = "microservice-web-task"
}
}
What does this code do?
- requires_compatibilities = ["FARGATE"] tells AWS this task runs on Fargate (serverless), not on EC2 instances you manage.
- network_mode = "awsvpc" gives the task its own network interface with a private IP. This is required for Fargate.
- cpu = "256" and memory = "512" allocate 0.25 vCPU and 512 MiB of memory. This is the smallest and cheapest Fargate configuration.
- The container_definitions block specifies the nginx:alpine image, a lightweight web server that serves a welcome page on port 80.
- Save main.tf.
- You won't see a change yet. The task definition tells ECS what to run, but you still need a service to actually launch it.
Create the ECS service
The ECS service is what actually launches and maintains your container. It ensures the desired number of tasks are always running and connects them to your network.
- In main.tf, add this code below the task definition resource:
# --- ECS Service ---
resource "aws_ecs_service" "web" {
name = "microservice-web"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.web.arn
desired_count = 1
launch_type = "FARGATE"
network_configuration {
subnets = [aws_subnet.public_a.id, aws_subnet.public_b.id]
security_groups = [aws_security_group.ecs_service.id]
assign_public_ip = true
}
tags = {
Name = "microservice-web-service"
}
}
What does this code do?
- cluster points to the ECS cluster you created earlier, telling the service where to run.
- task_definition references the nginx blueprint so the service knows which container to launch.
- desired_count = 1 means one copy of the container runs at all times. If it crashes, ECS automatically restarts it.
- The network_configuration block places the task in your public subnets, attaches the security group (allowing port 80), and assigns a public IP so you can reach it from a browser.
- Save main.tf.
- Preview all the resources Terraform will create by running this command:
terraform plan
- You should see a plan listing approximately 12-15 resources to add, with no errors. Look for resources like aws_ecs_cluster.main, aws_ecs_task_definition.web, and aws_ecs_service.web in the output.
Seeing errors in the plan?
- If you see a "reference to undeclared resource" error, confirm that your resource names match exactly. For example, aws_iam_role.ecs_task_execution must match the name in the role's resource block.
- If you see a JSON encoding error, check that container_definitions uses jsonencode([...]) with square brackets inside the parentheses.
- If the plan shows 0 resources, make sure you saved main.tf after adding all the new code.
- Help me debug my terraform plan
✔️ Awesome, I've got everything!
Great. Double check you've saved main.tf and your plan shows no errors.
ⓧ I'd like to double check the full code
Here is the complete main.tf file at this point in the project:
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.0"
}
}
required_version = ">= 1.5.0"
}
provider "aws" {
region = var.aws_region
}
# --- Networking ---
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "microservice-vpc"
}
}
resource "aws_subnet" "public_a" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
availability_zone = "${var.aws_region}a"
map_public_ip_on_launch = true
tags = {
Name = "microservice-public-a"
}
}
resource "aws_subnet" "public_b" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.2.0/24"
availability_zone = "${var.aws_region}b"
map_public_ip_on_launch = true
tags = {
Name = "microservice-public-b"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "microservice-igw"
}
}
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "microservice-public-rt"
}
}
resource "aws_route_table_association" "public_a" {
subnet_id = aws_subnet.public_a.id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "public_b" {
subnet_id = aws_subnet.public_b.id
route_table_id = aws_route_table.public.id
}
resource "aws_security_group" "ecs_service" {
name = "microservice-ecs-sg"
description = "Allow HTTP inbound traffic for ECS service"
vpc_id = aws_vpc.main.id
ingress {
description = "HTTP from anywhere"
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 = {
Name = "microservice-ecs-sg"
}
}
# --- ECS Cluster ---
resource "aws_ecs_cluster" "main" {
name = "microservice-cluster"
tags = {
Name = "microservice-cluster"
}
}
# --- IAM Role for ECS Task Execution ---
resource "aws_iam_role" "ecs_task_execution" {
name = "microservice-ecs-execution-role"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ecs-tasks.amazonaws.com"
}
}
]
})
tags = {
Name = "microservice-ecs-execution-role"
}
}
resource "aws_iam_role_policy_attachment" "ecs_task_execution" {
role = aws_iam_role.ecs_task_execution.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}
# --- ECS Task Definition ---
resource "aws_ecs_task_definition" "web" {
family = "microservice-web"
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = "256"
memory = "512"
execution_role_arn = aws_iam_role.ecs_task_execution.arn
container_definitions = jsonencode([
{
name = "web"
image = "nginx:alpine"
essential = true
portMappings = [
{
containerPort = 80
hostPort = 80
protocol = "tcp"
}
]
}
])
tags = {
Name = "microservice-web-task"
}
}
# --- ECS Service ---
resource "aws_ecs_service" "web" {
name = "microservice-web"
cluster = aws_ecs_cluster.main.id
task_definition = aws_ecs_task_definition.web.arn
desired_count = 1
launch_type = "FARGATE"
network_configuration {
subnets = [aws_subnet.public_a.id, aws_subnet.public_b.id]
security_groups = [aws_security_group.ecs_service.id]
assign_public_ip = true
}
tags = {
Name = "microservice-web-service"
}
}
Your entire microservice infrastructure is now defined in code. Next up, you'll run terraform apply to provision all of these resources on AWS and see your container running live.
Deploy and Verify Your Microservice
Your Terraform configuration is validated and ready to go. The terraform plan showed all your resources without errors.
Now it is time to bring your infrastructure to life. In this step, you will run terraform apply to provision every resource on AWS, then find and visit your running microservice in a browser.
In this step, get ready to:
- Deploy all your infrastructure to AWS with a single command.
- Locate the public IP of your running Fargate task.
- Verify the nginx welcome page loads in your browser.
Apply the Terraform configuration
The terraform apply command reads your .tf files, calculates what needs to be created, and provisions every resource on AWS.
- Deploy your infrastructure by running this command:
terraform apply
What does this command do?
Terraform shows you a plan (just like terraform plan) and then asks you to confirm before making any changes. It will create your VPC, subnets, internet gateway, security group, ECS cluster, IAM role, task definition, and service all at once.
- Terraform will display the plan and prompt you to confirm. Type yes and press Enter.
- Wait 2-3 minutes for Terraform to finish provisioning all resources. You will see each resource being created in your terminal output.
- When the apply completes, you should see a message that says Apply complete! followed by a count of resources added.
Seeing an error during apply?
If you see an error about invalid credentials, run aws configure again and double check your Access Key ID and Secret Access Key are correct.
If you see a resource limit error, make sure your AWS account is not in a sandbox that restricts VPC or ECS creation. New accounts sometimes have regional limits.
Help me debug my terraform apply error
Find your running service
Your Fargate task is now running, but you need its public IP address to visit it in a browser. The public IP is assigned to the task's network interface.
- Get the task ARN by running this command:
aws ecs list-tasks --cluster microservice-cluster --region us-east-1
- Copy the task ARN from the output (the long string starting with arn:aws:ecs:).
- Describe that task to find its network details by running:
aws ecs describe-tasks --cluster microservice-cluster --tasks [[TASK_ARN="your task ARN from the previous command"]] --region us-east-1
- In the output, look inside the attachments section for a detail named networkInterfaceId (it starts with eni-).
- Use that ENI ID to look up the public IP by running:
aws ec2 describe-network-interfaces --network-interface-ids [[ENI_ID="your eni-xxxx ID from the previous output"]] --query "NetworkInterfaces[0].Association.PublicIp" --output text
Why is finding the IP multi-step?
ECS Fargate assigns the public IP to the task's Elastic Network Interface (ENI), not directly to the container. The describe-tasks output gives you the ENI ID, and then you query EC2 for the public IP attached to that ENI.
Alternatively, you can find the public IP in the AWS Console. Open ECS, select Clusters, then microservice-cluster, then Tasks. Click the running task and find the public IP under Configuration.
Now add output blocks to outputs.tf so Terraform displays useful information after every apply.
- In VS Code, open outputs.tf and paste the following code:
output "cluster_name" {
description = "Name of the ECS cluster"
value = aws_ecs_cluster.main.name
}
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "service_name" {
description = "Name of the ECS service"
value = aws_ecs_service.web.name
}
What do Terraform outputs do?
Outputs print key values to your terminal after every terraform apply. They act like a summary of your deployed infrastructure. You can also access them later with terraform output.
- Save outputs.tf.
- Apply the updated configuration to register the new outputs by running:
terraform apply
- Type yes when prompted. This time no new resources are created, but your outputs will display at the bottom of the terminal.
- You should see your cluster name (microservice-cluster), VPC ID, and service name (microservice-web) printed as outputs.
Seeing a syntax error?
Make sure each output block in outputs.tf has matching opening and closing braces. Check that you have not accidentally left any content from a previous step inside the file.
Help me fix my outputs.tf syntax error
✔️ Awesome, I've got everything!
Great. Double check you have saved outputs.tf and that your outputs appeared in the terminal.
ⓧ I'd like to double check the full code
output "cluster_name" {
description = "Name of the ECS cluster"
value = aws_ecs_cluster.main.name
}
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "service_name" {
description = "Name of the ECS service"
value = aws_ecs_service.web.name
}
Test the live service
You found the public IP. Now it is time to confirm your containerized microservice is serving traffic.
- Open your web browser.
- Paste the public IP address you found earlier into the address bar and press Enter.
- You should see the nginx welcome page confirming your container is live.
What is running behind that page?
Your browser is connecting to an nginx:alpine container running inside ECS Fargate. AWS pulled the container image, started it in a Fargate task, attached a public IP, and is now routing HTTP traffic to port 80 on that container.
All of this was created by your Terraform code. No manual clicking in the AWS Console was needed.
- Verify Terraform is tracking all your resources by running:
terraform state list
- You should see a list of all managed resources including aws_vpc.main, aws_ecs_cluster.main, aws_ecs_service.web, and others.
Nginx page not loading?
Make sure you are using http:// (not https://) in your browser address bar. The security group only allows port 80 (HTTP).
If the page times out, the task might still be starting up. Wait 1-2 minutes and try again. You can check the task status in the AWS Console under ECS, then Clusters, then microservice-cluster, then Tasks. The task should show a status of RUNNING.
Help me troubleshoot my ECS service
Your microservice is live and serving traffic from AWS. Next up, you will experience the true power of Infrastructure as Code by destroying and rebuilding everything with a single command.
Destroy and Rebuild Your Infrastructure
Your microservice is live and serving traffic. But here is the real test of Infrastructure as Code: can you tear everything down and bring it all back with a single command?
In a traditional setup, recreating this infrastructure would mean hours of clicking through the AWS Console. With Terraform, your .tf files are the blueprint. You will destroy everything and prove that your code alone can rebuild the full stack from scratch.
In this step, get ready to:
- Destroy all AWS resources with a single Terraform command.
- Rebuild the entire infrastructure from your code.
- Verify the microservice is accessible at a new public IP.
Destroy all infrastructure
The terraform destroy command is the inverse of terraform apply. It reads your state file and removes every resource Terraform created.
- Destroy all your AWS resources by running this command:
terraform destroy
What will Terraform ask?
Terraform will show a plan of every resource it intends to destroy, then prompt you with: "Do you really want to destroy all resources?" Only typing yes will proceed. Any other input cancels the operation.
- Type yes when prompted and press Enter.
Terraform removes each resource one by one, respecting dependency order. This takes 2-3 minutes.
You should see progress lines for each resource followed by a final message: "Destroy complete! Resources: N destroyed."
- Confirm your state is now empty by running:
terraform state list
The command returns no output at all, confirming every resource has been removed from state.
Seeing a dependency error during destroy?
Some resources (like security groups attached to running services) can fail to delete if AWS reports a dependency conflict. Run terraform destroy again. Terraform will retry only the remaining resources.
If the error persists, check the AWS Console to see if the ECS service is still draining. Wait a minute and try again.
help me debug a dependency error during terraform destroy
Why is this different from deleting resources in the console?
When you delete resources manually in the AWS Console, Terraform's state file still thinks they exist. The next terraform apply would try to manage resources that are already gone, causing errors.
Using terraform destroy keeps your state file and the real world perfectly in sync. Your .tf files remain unchanged, ready to recreate everything.
Rebuild with one command
Your configuration files are still exactly as you wrote them. Terraform does not modify your .tf files when it destroys resources. This means you can recreate everything from scratch.
- Recreate all your infrastructure by running:
terraform apply
- Type yes when prompted and press Enter.
Terraform provisions all resources again. This takes 2-3 minutes.
You should see "Apply complete! Resources: N added, 0 changed, 0 destroyed." at the end.
- Verify all resources are back in state by running:
terraform state list
You should see all your managed resources listed, including aws_vpc.main, aws_ecs_cluster.main, aws_ecs_service.web, and more.
- Find the public IP of your new running task using the AWS CLI:
aws ecs list-tasks --cluster microservice-cluster --region us-east-1
- Copy the task ARN from the output, then describe it to find the public IP:
aws ecs describe-tasks --cluster microservice-cluster --tasks [[TASK_ARN="your task ARN from the previous command"]] --region us-east-1
Where is the public IP?
In the JSON output, look for the attachments array. Inside it, find the details array and look for the entry with name set to networkInterfaceId. You can also check the ECS section in the AWS Console under Clusters > microservice-cluster > Tasks to see the public IP directly.
- Open the public IP in your browser on port 80.
- You should see the nginx welcome page, confirming your microservice is running again at a brand new IP address.
Why is the IP different?
Every time ECS Fargate launches a task, AWS assigns a new public IP from its pool. The old IP was released when you destroyed the infrastructure. This is normal behavior for ephemeral cloud resources.
Nginx page not loading?
The ECS service may still be starting the task. Wait 1-2 minutes after terraform apply completes, then try the IP again.
If you still cannot reach it, make sure you are using http:// (not https://) in your browser. The security group only allows port 80.
help me troubleshoot why my ECS task is not reachable after terraform apply
The power of Infrastructure as Code
You just proved that your entire cloud infrastructure is reproducible, disposable, and version-controlled. If something breaks in production, you can tear it down and rebuild with confidence.
This is the exact workflow DevOps teams use every day. Environments are created and destroyed on demand for testing, staging, and disaster recovery.
Secret mission
Add a Second Microservice
Your cluster is running a single web service. In production, microservice architectures rarely stop at one. Can you extend your Terraform configuration to deploy a second service on the same cluster and prove your infrastructure supports multiple containers?
Clean Up Your Resources
Clean Up Your Resources
Decide whether to keep your resources running, pause them to come back later, or delete them entirely. Running Fargate tasks incur compute charges, so destroying resources when you are done is recommended.
Cost warning
While Fargate tasks are covered by the AWS free tier for the first 12 months (750 hours per month at 0.25 vCPU / 0.5 GB), leaving tasks running indefinitely will incur charges. A single task with 0.25 vCPU and 0.5 GB memory costs approximately $0.01/hour.
With two services running, you could accumulate costs quickly if you forget about them. Running terraform destroy removes everything and stops all charges immediately.
Resources you used:
- AWS VPC, subnets, internet gateway, route table, and route table associations.
- Security group (microservice-ecs-sg).
- ECS cluster (microservice-cluster) with two running Fargate services.
- ECS task definitions (web and API).
- IAM role for ECS task execution (microservice-ecs-execution-role).
- Local terraform-microservice project directory on your Desktop.
Keep everything running
No action needed. Choose this if you are still actively experimenting with your infrastructure or want to explore Terraform changes.
- Your two Fargate tasks will continue running and remain accessible via their public IPs.
- Monitor your spending in the AWS Billing Dashboard under **Cost Explorer**.
- Terraform and the AWS CLI remain installed on your machine for future projects.
Pause - I'll come back to this later
Fargate does not have a native pause option. The only way to stop compute charges is to remove the running tasks entirely.
The recommended approach is to run terraform destroy to tear everything down. When you are ready to come back, run terraform apply to recreate the full infrastructure in minutes. Your Terraform files stay on your machine and act as the blueprint.
- Destroy all cloud resources by running this command from inside your terraform-microservice directory:
terraform destroy
- Type yes when prompted to confirm.
- Keep your local terraform-microservice folder. You can rebuild everything later with terraform apply.
Delete - I don't want to use this again
Remove all project resources and start fresh. This stops all charges and removes local project files.
- Destroy all AWS resources by running this command from inside your terraform-microservice directory:
terraform destroy
- Type yes when prompted to confirm.
- Verify in the AWS Console that the VPC, ECS cluster, and IAM role are gone.
- Remove the local project directory by running:
cd ~/Desktop
rm -rf terraform-microservice
- Remove the AWS CLI installer package that was downloaded during setup:
rm -f ~/AWSCLIV2.pkg
What about Terraform and the AWS CLI?
Terraform and the AWS CLI are general-purpose development tools you may want to keep for future projects. If you want to uninstall them completely:
- Uninstall Terraform:
brew uninstall hashicorp/tap/terraform
- Uninstall the AWS CLI:
sudo rm /usr/local/bin/aws
sudo rm /usr/local/bin/aws_completer
sudo rm -rf /usr/local/aws-cli
That's a Wrap!
That's a Wrap!
Nice work! You just deployed a fully containerized microservice on AWS, with every piece of infrastructure defined as Terraform code.
You've learned how to:
- Write Terraform HCL to define a complete cloud infrastructure including a VPC, subnets, security groups, an ECS cluster, task definitions, and services.
- Deploy a live containerized microservice on AWS ECS Fargate accessible via a public IP address.
- Execute the full Terraform lifecycle: init, plan, apply, destroy, and re-apply, proving your infrastructure is reproducible with a single command.
- Secret Mission: Deployed a second microservice on the same cluster, proving the architecture supports multiple services.
Ready to quiz yourself?