Deploy a Dockerized App with Terraform
Provision AWS infrastructure as code and deploy a Flask app in Docker.
Introduction
30 Second Summary
Every time you set up a server by clicking through a web dashboard, you are doing work no one can reliably repeat. One wrong setting and the whole environment becomes a mystery to debug.
In this project, you will write Terraform code that provisions a complete AWS environment and deploys a Python app inside a Docker container to the cloud with a single command.
What You'll Build
You run terraform apply in your terminal, wait two minutes, then open a public IP address in your browser to see your Flask API responding from a live EC2 instance in the cloud.
By the end of this project, you'll have:
- A live public URL that returns a JSON response from your Flask app running on an AWS EC2 instance.
- Terraform configuration files that create and destroy your entire cloud infrastructure with a single command.
- A Docker image on Docker Hub that your EC2 instance automatically pulls and runs on startup.
- Secret Mission: Add a health check endpoint and configure Terraform to output the full application URL.
Are there any prerequisites?
This project assumes you have an AWS account (Free Tier eligible), a Docker Hub account, Docker and Python installed locally, and basic comfort with the terminal. Step 1 walks you through installing Terraform and configuring the AWS CLI.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters. You're going to deploy a Python Flask app inside a Docker container to a live AWS EC2 instance, with every piece of infrastructure defined as Terraform code.
Install Terraform and Configure AWS CLI
In this project, you'll define your cloud infrastructure as code and deploy a Dockerized app to AWS.
That starts with two essential tools: Terraform (which reads your configuration files and creates real AWS resources) and the AWS CLI (which authenticates your machine with Amazon's cloud). Without them installed and configured, there's no way to translate your .tf files into actual running infrastructure.
In this step, get ready to:
- Install Terraform CLI.
- Install and configure AWS CLI v2.
- Verify both tools are working and authenticated.
Install Terraform via the HashiCorp APT repository
Terraform is the tool that will read your infrastructure configuration files and create the corresponding AWS resources. Let's check if it's already on your machine.
- Check if Terraform is already installed by running this command:
terraform --version
✔️ I see version 1.0 or higher
You should see output starting with Terraform v followed by a version number like 1.15.8. Terraform is ready to go.
ⓧ I see an older version
If you see a version below 1.0, you need to upgrade. Run the install commands below to replace your outdated version with the latest from HashiCorp's official repository.
- Add the HashiCorp GPG key by running:
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
- Add the HashiCorp repository to your system by running:
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
- Update your package list and install Terraform by running:
sudo apt update && sudo apt install terraform
- Confirm the upgrade worked by running:
terraform --version
You should now see Terraform v1.x.x with a version of 1.0 or higher.
ⓧ Command not found
Terraform isn't installed yet. Let's install it from the official HashiCorp APT repository.
- Add the HashiCorp GPG key by running:
wget -O - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
- Add the HashiCorp repository to your system by running:
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(grep -oP '(?<=UBUNTU_CODENAME=).*' /etc/os-release || lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
- Update your package list and install Terraform by running:
sudo apt update && sudo apt install terraform
- Confirm the install worked by running:
terraform --version
You should see Terraform v1.x.x with a version of 1.0 or higher.
Seeing a permission denied error?
Make sure you're using sudo before the apt and gpg commands. These commands need administrator privileges to modify system packages.
If you're still stuck, help me troubleshoot my Terraform installation on Linux.
Install AWS CLI v2 and configure credentials
The AWS CLI lets your terminal communicate with AWS services. You'll use it to authenticate Terraform so it can create resources in your AWS account.
- Check if AWS CLI is already installed by running:
aws --version
✔️ I see version 2.x
If you see aws-cli/2.x.x in the output, your AWS CLI is ready. Skip ahead to the credential configuration below.
ⓧ I see version 1.x
AWS CLI version 1 is outdated. You need version 2 for this project.
- Download the AWS CLI v2 installer by running:
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
- Unzip the installer by running:
unzip awscliv2.zip
- Run the installer with the update flag to replace v1 by running:
sudo ./aws/install --update
- Confirm the upgrade by running:
aws --version
You should now see aws-cli/2.x.x in the output.
ⓧ Command not found
AWS CLI isn't installed yet. Let's install version 2.
- Download the AWS CLI v2 installer by running:
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
- Unzip the installer by running:
unzip awscliv2.zip
- Run the installer by running:
sudo ./aws/install
- Confirm the install by running:
aws --version
You should now see aws-cli/2.x.x in the output.
Seeing a curl or unzip error?
Make sure curl and unzip are installed on your system. You can install them with sudo apt install curl unzip and retry.
If you're still stuck, help me troubleshoot my AWS CLI v2 installation.
Now you need to give the AWS CLI your credentials so it can authenticate with your AWS account.
Where do I get my AWS credentials?
You need an Access Key ID and Secret Access Key from your AWS account. To create one, sign in to the AWS Console, navigate to IAM, select Users, choose your user, go to the Security credentials tab, and click Create access key.
Copy the Access Key ID and Secret Access Key somewhere safe. You'll only see the secret key once.
- Configure the AWS CLI with your credentials by running:
aws configure
What will aws configure ask me?
This command prompts you for four values. Enter them one at a time as each prompt appears:
- AWS Access Key ID: paste your access key from IAM.
- AWS Secret Access Key: paste your secret key from IAM.
- Default region name: enter us-east-1.
- Default output format: enter json.
Verify both tools work
Both tools are installed and configured. Before moving on, let's confirm everything is working together.
- Before you run this next command, think: what do you expect to see if your AWS credentials are configured correctly?
- Verify your AWS authentication by running:
aws sts get-caller-identity
You should see a JSON response containing your UserId, Account number, and Arn. This proves your terminal can talk to AWS.
- Confirm Terraform's version one more time by running:
terraform --version
You should see Terraform v1.x.x confirming the CLI is on your PATH and ready to use.
Seeing an error from aws sts get-caller-identity?
If you see a message about invalid credentials or unable to locate credentials, run aws configure again and double-check that you pasted the correct Access Key ID and Secret Access Key from IAM.
Make sure there are no extra spaces before or after the pasted values.
If you're still stuck, help me fix my AWS CLI authentication.
Your tools are installed and authenticated. Next up, you'll build a Flask app, package it in a Docker container, and push it to Docker Hub so your cloud infrastructure has something to deploy.
Build and Push Your Docker Image
Your development tools are installed and authenticated. Now you need the actual application that will run in the cloud.
In this step, you'll write a Python Flask API, package it into a Docker container image, and push that image to Docker Hub. Later, your EC2 instance will pull this image to run your app automatically.
In this step, get ready to:
- Create a Flask API with a JSON endpoint.
- Write a Dockerfile to containerize the app.
- Build, test, and push your Docker image to Docker Hub.
Create a Flask API that returns JSON
Flask is a lightweight Python web framework. Your app will have a single endpoint that returns a JSON response confirming the service is running.
- Create a file called app.py in your project directory and add the following code:
from flask import Flask, jsonify
# Create the Flask application
app = Flask(__name__)
# Define a single route that returns JSON
@app.route("/")
def home():
return jsonify({
"message": "Hello from Terraform-deployed Docker!",
"service": "flask-terraform",
"status": "running"
})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
What does this code do?
- Flask(__name__) creates a web application instance.
- @app.route("/") maps the root URL to the home() function.
- jsonify() converts a Python dictionary into a proper JSON response with the correct content-type header.
- host="0.0.0.0" makes the app listen on all network interfaces, which is required inside a Docker container.
- Create a file called requirements.txt to pin your Flask dependency:
flask==3.1.3
- Save both files.
- Confirm the files exist by running:
ls
You should see app.py and requirements.txt in the output.
Don't see both files?
- Make sure you saved both files in the same directory where your terminal is currently located.
- Run pwd to check your current directory and confirm it matches where you saved the files.
- Help me find where my project files are
Write a Dockerfile to package the app
A Dockerfile tells Docker exactly how to build your application into a container image. Each instruction adds a layer to the image.
- Create a file called Dockerfile (no file extension) in the same directory and add the following code:
# Start from a slim Python base image
FROM python:3.13-slim
# Set the working directory inside the container
WORKDIR /app
# Copy and install dependencies first (for Docker layer caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy the application code
COPY app.py .
# Document which port the app uses
EXPOSE 5000
# Run the Flask app when the container starts
CMD ["python", "app.py"]
What does each instruction do?
- FROM python:3.13-slim starts from a minimal Python 3.13 image (much smaller than the full version).
- WORKDIR /app sets the directory inside the container where your code will live.
- COPY requirements.txt . is copied separately from the app code so Docker can cache the dependency layer.
- CMD ["python", "app.py"] tells Docker what command to run when the container starts.
- Save Dockerfile.
- Confirm all three project files exist by running:
ls
You should see app.py, Dockerfile, and requirements.txt in the output.
Dockerfile not showing up?
- Make sure the filename is exactly Dockerfile with a capital D and no file extension (not Dockerfile.txt or dockerfile).
- Confirm it was saved in the same directory as your other project files.
- Help me create my Dockerfile correctly
✔️ Awesome, I've got everything!
Great. Make sure all three files are saved before moving on to the build.
ⓧ I'd like to double check the full code
Here are the three files you should have. Compare them with yours:
from flask import Flask, jsonify
app = Flask(__name__)
@app.route("/")
def home():
return jsonify({
"message": "Hello from Terraform-deployed Docker!",
"service": "flask-terraform",
"status": "running"
})
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
flask==3.1.3
FROM python:3.13-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 5000
CMD ["python", "app.py"]
Build, test locally, and push to Docker Hub
Your files define the app. Now you'll turn them into a runnable container image, prove it works on your machine, and upload it to a registry your cloud instance can reach.
- Build your Docker image by running this command:
docker build -t [[DOCKERHUB_USERNAME="your Docker Hub username"]]/flask-terraform:v1 .
What does this command do?
- docker build reads your Dockerfile and executes each instruction to create the image.
- -t tags the image with a name. The format username/repo:tag is required for pushing to Docker Hub later.
- The . at the end tells Docker to look for the Dockerfile in the current directory.
You should see Docker downloading the Python base image and installing your dependencies. The final line will show a successful build with an image ID.
Build failed?
- Make sure Docker is running. You can check by running docker info. If it fails, start Docker Desktop or the Docker daemon.
- Check that the . at the end of the build command is included. It tells Docker where to find the Dockerfile.
- Help me fix my Docker build error
Before you visit the URL in the next step, what do you expect a Flask API to return when you hit its root endpoint?
- Run your container locally to test it:
docker run -p 5000:5000 [[DOCKERHUB_USERNAME="your Docker Hub username"]]/flask-terraform:v1
- Open your browser and visit http://localhost:5000.
You should see the JSON response from your Flask app:
{
"message": "Hello from Terraform-deployed Docker!",
"service": "flask-terraform",
"status": "running"
}
What does -p 5000:5000 mean?
The -p flag maps port 5000 on your machine to port 5000 inside the container. Without it, the container's network is isolated and you can't reach the app from your browser.
Getting 'connection refused' or a blank page?
- Check your terminal output. Flask should print Running on http://0.0.0.0:5000. If you see an error instead, review your app.py for typos.
- Make sure nothing else is using port 5000. You can check with lsof -i :5000 on macOS/Linux.
- Help me get my Flask container running locally
- Stop the running container by pressing Ctrl+C in your terminal.
Your image works locally. Now push it to Docker Hub so your EC2 instance can pull it later.
- Log in to Docker Hub from your terminal by running:
docker login
What will I see?
Docker will open a browser window asking you to authenticate with your Docker Hub account. Follow the prompts to authorize access. Once complete, your terminal will confirm the login succeeded.
- Push your image to Docker Hub by running:
docker push [[DOCKERHUB_USERNAME="your Docker Hub username"]]/flask-terraform:v1
You should see Docker uploading each layer of your image. When it finishes, you'll see a digest hash confirming the push succeeded.
Push denied or unauthorized?
- Make sure the username in your image tag matches your Docker Hub username exactly. The tag format must be your-username/flask-terraform:v1.
- Run docker login again and confirm you see a "Login Succeeded" message before retrying the push.
- Help me push my Docker image to Docker Hub
Your Docker image is live on Docker Hub, ready for any machine to pull and run. Next up, you'll write Terraform configuration that defines the AWS infrastructure to host this container in the cloud.
Write Your Terraform Configuration
Your Docker image is on Docker Hub and ready to be pulled by any server on the internet. Now you need to define what cloud infrastructure you want before Terraform can create it.
This is where you experience the power of Infrastructure as Code: your entire cloud setup lives in version-controllable text files. You write code describing VPCs, subnets, and servers. Terraform reads that code and builds real resources in AWS.
In this step, get ready to:
- Create main.tf with the AWS provider, a VPC, public subnet, internet gateway, and route table.
- Create ec2.tf with a security group and an EC2 instance that pulls and runs your Docker image on boot.
- Create outputs.tf to display the public IP after deployment.
Create main.tf with the provider and networking resources
Every Terraform project starts with a provider configuration that tells Terraform which cloud platform to talk to and which region to use.
- Create a new file called main.tf in your project folder and add the following provider configuration:
# Tell Terraform which provider to use and which version
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
required_version = ">= 1.0"
}
# Configure the AWS provider to use us-east-1
provider "aws" {
region = "us-east-1"
}
What does this code do?
The terraform block tells Terraform to download the AWS provider plugin (version 5.x) so it knows how to create AWS resources.
The provider block sets your deployment region to us-east-1. All resources you define will be created in that region.
- Save main.tf.
- Initialize your Terraform project by running this command:
terraform init
You should see Terraform download the AWS provider and print Terraform has been successfully initialized!
Seeing an error about the provider?
Make sure you saved main.tf before running terraform init. Terraform reads the file to know which providers to download.
If you see a network error, check your internet connection. Terraform needs to download the provider plugin from the HashiCorp registry.
Help me fix this
Now you have the provider ready. Next, add the network resources that your EC2 instance will live inside.
- Add the following VPC and subnet resources below the provider block in main.tf:
# Create an isolated network for your app
resource "aws_vpc" "main" {
cidr_block = "10.0.0.0/16"
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "flask-terraform-vpc"
}
}
# Create a subnet where your EC2 instance will launch
resource "aws_subnet" "public" {
vpc_id = aws_vpc.main.id
cidr_block = "10.0.1.0/24"
map_public_ip_on_launch = true
availability_zone = "us-east-1a"
tags = {
Name = "flask-terraform-public-subnet"
}
}
What does this code do?
The VPC is your own private network inside AWS. Think of it as a walled-off section of the cloud that only you control.
The subnet is a smaller slice of that network. Setting map_public_ip_on_launch = true means any instance launched here automatically gets a public IP address you can reach from the internet.
- Save main.tf. The next chunk connects this subnet to the internet.
- Add the internet gateway, route table, and route table association below the subnet in main.tf:
# Attach an internet gateway so traffic can reach the public internet
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "flask-terraform-igw"
}
}
# Create a route table that sends all traffic to the internet gateway
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 = "flask-terraform-public-rt"
}
}
# Link the route table to the subnet
resource "aws_route_table_association" "public" {
subnet_id = aws_subnet.public.id
route_table_id = aws_route_table.public.id
}
Why do you need all three?
A VPC is isolated by default. The internet gateway opens a door to the internet. The route table tells traffic where to go ("send everything to the gateway"). The association links that route table to your specific subnet.
Without all three, your EC2 instance would have no way to reach the internet or be reached by you.
- Save main.tf.
- Validate the file by running:
terraform validate
You should see Success! The configuration is valid.
Seeing a syntax error?
Check for missing closing braces } in your resource blocks. Each opening { needs a matching }.
Also verify that string values like "10.0.0.0/16" are wrapped in double quotes.
Help me fix my syntax error
✔️ Awesome, I've got everything!
Great. Double check you have saved main.tf and move on to the next file.
ⓧ I'd like to double check the full code
Scroll up and confirm your main.tf has these three sections in order:
- The terraform block and provider block at the top.
- The aws_vpc and aws_subnet resources in the middle.
- The aws_internet_gateway, aws_route_table, and aws_route_table_association at the bottom.
Create ec2.tf with the security group and instance
Your network is defined. Now you need to tell Terraform what kind of server to launch inside it and which ports to open.
- Create a new file called ec2.tf in the same folder and add the security group:
# Define which ports are open on the instance
resource "aws_security_group" "flask" {
name = "flask-terraform-sg"
description = "Allow SSH and Flask traffic"
vpc_id = aws_vpc.main.id
# Allow SSH access on port 22
ingress {
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Allow Flask traffic on port 5000
ingress {
from_port = 5000
to_port = 5000
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
# Allow all outbound traffic
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "flask-terraform-sg"
}
}
What does this code do?
A Security Group acts like a firewall for your EC2 instance. It controls which traffic is allowed in and out.
You open port 22 for SSH (so you can log in to troubleshoot) and port 5000 for your Flask app (so browsers can reach it). The egress rule allows the instance to pull your Docker image from the internet.
- Save ec2.tf. The next chunk tells Terraform which operating system image to use.
- Add the AMI data source below the security group in ec2.tf:
# Find the latest Amazon Linux 2023 image automatically
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["al2023-ami-*-x86_64"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
}
What is a data source?
A data block reads information from AWS without creating anything. Here it finds the latest Amazon Linux 2023 AMI ID so you never have to hard-code a specific image that might go stale.
- Save ec2.tf.
- Run terraform validate again to confirm everything still looks good:
terraform validate
You should see Success! The configuration is valid.
- Now add the EC2 instance below the AMI data source in ec2.tf:
# Launch an EC2 instance that pulls and runs your Docker image
resource "aws_instance" "flask" {
ami = data.aws_ami.amazon_linux.id
instance_type = "t3.micro"
subnet_id = aws_subnet.public.id
vpc_security_group_ids = [aws_security_group.flask.id]
user_data = <<-EOF
#!/bin/bash
yum update -y
yum install -y docker
systemctl start docker
systemctl enable docker
docker pull [[DOCKERHUB_USER="your-dockerhub-username"]]/flask-terraform:v1
docker run -d -p 5000:5000 [[DOCKERHUB_USER="your-dockerhub-username"]]/flask-terraform:v1
EOF
tags = {
Name = "flask-terraform-instance"
}
}
What does this code do?
This creates a t3.micro instance (Free Tier eligible) running Amazon Linux 2023. It is placed in your public subnet with the security group you defined above.
The user_data script runs automatically when the instance boots. It installs Docker, pulls your image from Docker Hub, and starts the container on port 5000.
- Save ec2.tf.
✔️ Awesome, I've got everything!
Great. Double check you have saved ec2.tf and move on to the outputs file.
ⓧ I'd like to double check the full code
Scroll up and confirm your ec2.tf has these three sections in order:
- The aws_security_group resource at the top.
- The data "aws_ami" block in the middle.
- The aws_instance resource at the bottom (with your Docker Hub username in the user_data script).
Create outputs.tf and verify the full plan
The final file tells Terraform to print your instance's public IP address after deployment so you know where to find your app.
- Create a new file called outputs.tf and add this output definition:
# Print the public IP after terraform apply finishes
output "instance_public_ip" {
description = "Public IP of the Flask EC2 instance"
value = aws_instance.flask.public_ip
}
What does this code do?
An output block tells Terraform to display a specific value after it finishes creating resources. Here it prints the public IP so you can immediately visit your app without digging through the AWS console.
- Save outputs.tf.
Before you run this, what do you think terraform plan will show? How many resources do you think Terraform will create?
- Preview what Terraform will create by running:
terraform plan
You should see Terraform list 7 resources to add: a VPC, subnet, internet gateway, route table, route table association, security group, and EC2 instance.
What is terraform plan?
The terraform plan command shows you exactly what Terraform will do without actually doing it. It is a dry run that lets you review changes before committing real resources and money.
Notice that the data source (the AMI lookup) is not counted as a resource to create. It only reads information.
Seeing errors in the plan?
If Terraform reports a reference error (like "Reference to undeclared resource"), check that your resource names match exactly. For example, aws_vpc.main.id must match resource "aws_vpc" "main" in your code.
If you see a credentials error, run aws sts get-caller-identity to confirm your AWS CLI is still authenticated.
Help me fix my terraform plan error
Your infrastructure is defined in code and Terraform knows exactly what to build. Next up, you will run terraform apply to turn these text files into real, running cloud resources.
Deploy to AWS
In the last step, you wrote your entire cloud infrastructure as code and ran terraform plan to preview what Terraform would create. The plan showed all your resources ready to go, but nothing actually exists on AWS yet.
This is the moment your code becomes real infrastructure. Running terraform apply translates your text files into actual running cloud resources. In minutes, you will have a live EC2 instance serving your Flask API to the internet.
In this step, get ready to:
- Run terraform apply to create all AWS resources.
- Test your live Flask endpoint from a browser.
- Verify the Docker container is running on the EC2 instance.
Run terraform apply and confirm the plan
Terraform will show you the full execution plan one more time and ask for explicit confirmation before creating anything.
- Deploy your infrastructure by running this command:
terraform apply
What happens when you run this?
Terraform displays the same plan you saw with terraform plan, listing every resource it will create. It then asks for confirmation.
Only typing yes (the full word) will proceed. Any other input cancels the operation. This is your safety net before real resources are created.
- Review the plan output. You should see resources marked with + create for your VPC, subnet, internet gateway, route table, security group, and EC2 instance.
- Type yes and press Enter to confirm.
Terraform now creates each resource in parallel where possible, showing progress like Creating... and Creation complete for each one. The EC2 instance takes the longest (about 30-40 seconds).
When finished, you will see a line like Apply complete! Resources: 7 added, 0 changed, 0 destroyed. followed by an Outputs: section showing your instance's public IP address.
What is the Outputs section?
The Outputs: block at the end displays the values you defined in outputs.tf. In this case, it shows instance_public_ip with the IP address AWS assigned to your new EC2 instance.
This IP is what you will use to reach your Flask app from any browser in the world. Copy it now.
- Copy the IP address shown next to instance_public_ip in the output.
Seeing an error during apply?
- If you see a credentials error, run aws sts get-caller-identity to confirm your AWS CLI is still authenticated.
- If a specific resource fails, check the error message for quota limits or region availability. The us-east-1 region should have capacity for a t3.micro instance.
- help me debug a terraform apply error
Test the live endpoint
Your infrastructure is created, but is your app actually running? Before you check, think about what is happening on that new EC2 instance right now.
Before you visit the URL, do you think the app will respond immediately? What did the user_data script in ec2.tf need to do before the container is ready?
- Open your browser and visit:
http://[[EC2_PUBLIC_IP="your EC2 public IP from Terraform output"]]:5000
You will likely see a "connection refused" or "unable to connect" error in your browser. This is expected.
Why can't I connect yet?
Terraform reported Apply complete! because the infrastructure is created. But infrastructure ready does not mean application ready.
The user_data script on your EC2 instance is still running. It needs to update the system packages, install Docker, start the Docker service, pull your image from Docker Hub, and then start the container. This takes about 60-90 seconds after the instance launches.
This gap between "infrastructure exists" and "application is serving" is a real-world challenge in cloud deployment. Every production system accounts for it.
- Wait about 90 seconds, then refresh your browser.
You should now see a JSON response from your Flask app:
{
"message": "Hello from Terraform-deployed Docker!",
"service": "flask-terraform",
"status": "running"
}
Still seeing connection refused after 2 minutes?
- Make sure you are using http:// (not https://) and port 5000 in the URL.
- Confirm the security group in ec2.tf has an ingress rule for port 5000.
- Check that your Docker Hub image name in the user_data script matches the image you pushed. If it does not match, Terraform will need to be re-applied after you fix the name.
- help me debug why my Flask app isn't responding on the EC2 instance
Verify the deployment is complete
Seeing the JSON response in your browser proves your entire pipeline works. Your Flask code is running inside a Docker container on an EC2 instance that you never touched manually.
- Confirm the response contains all three fields: message, service, and status.
- Run this command to verify your Terraform state is tracking the resources:
terraform output instance_public_ip
This prints the same IP address you visited. It confirms Terraform saved the resource state locally in terraform.tfstate and can retrieve values from it on demand.
What is terraform.tfstate?
When Terraform creates resources, it records their actual state (IDs, IP addresses, configurations) in a local file called terraform.tfstate. This file is how Terraform knows what it created and what currently exists.
In the next step, when you run terraform destroy, Terraform reads this state file to know exactly which resources to delete. Without it, Terraform would have no way to find your resources.
Your app is live on the internet, deployed entirely through code. Next up, you will tear it all down with a single command and prove you have been charged nothing.
Destroy Everything
Your Flask app is live and reachable from any browser on the planet. That is the payoff of Infrastructure as Code.
But those AWS resources are still running. Leaving cloud resources alive after you finish a project is the most common way beginners rack up unexpected bills. Terraform solves this with a single command: terraform destroy. In this step, you will tear down every resource you created and prove nothing is left.
In this step, get ready to:
- Run terraform destroy to remove all AWS resources.
- Verify the resources are gone in the AWS console.
- Confirm $0 charges on the AWS billing dashboard.
Run terraform destroy
The terraform destroy command reads your state file, builds a plan to remove every resource it tracks, and asks you to confirm before doing anything irreversible.
Before you run it, think: how many resources do you expect Terraform to mark for destruction? Count the resource blocks across main.tf and ec2.tf.
- Destroy all your AWS infrastructure by running this command:
terraform destroy
What will you see?
Terraform will display a destroy plan listing every resource it intends to remove, each marked with a - prefix. At the bottom, you will see a summary line like Plan: 0 to add, 0 to change, 7 to destroy. followed by a confirmation prompt.
Terraform will ask: "Do you really want to destroy all resources?" Only yes is accepted. Any other input cancels the operation.
- Type yes and press Enter to confirm.
Terraform begins destroying resources one by one. You will see status lines like Destroying... and Destruction complete for each resource. The EC2 instance takes the longest (about 30-60 seconds).
When finished, you will see: Destroy complete! Resources: 7 destroyed..
Why does the order matter?
Terraform uses a dependency graph to determine destruction order. It terminates the EC2 instance before the security group, and the security group before the VPC. You cannot delete a VPC that still has resources inside it, so Terraform handles this automatically.
Seeing an error during destroy?
If you see a dependency error, run terraform destroy again. Transient AWS API timing issues occasionally cause the first attempt to fail on dependent resources.
If you see a credentials error, run aws sts get-caller-identity to confirm your AWS CLI session is still valid.
Still stuck? Why is terraform destroy failing with an error?
Verify resources are gone in the AWS console
The terminal says everything is destroyed. Now confirm it with your own eyes in the AWS console.
- Open the AWS Management Console in your browser at https://console.aws.amazon.com.
- Use the search bar at the top and type EC2. Select EC2 from the results.
- Click Instances in the left sidebar.
- Confirm the region is set to us-east-1 in the top-right corner of the console.
- Verify that flask-terraform-instance either shows a Terminated state or is no longer listed.
Terminated instances may linger in the list for up to an hour before AWS removes them. The key indicator is the Terminated state, which means the instance is no longer running or costing you money.
- Use the search bar again and type VPC. Select VPC from the results.
- Click Your VPCs in the left sidebar.
- Confirm that flask-terraform-vpc is no longer listed. You should only see the default VPC that AWS provides in every region.
What about subnets and security groups?
When Terraform destroyed the VPC, AWS automatically cleaned up all dependent resources inside it (the subnet, route table, and internet gateway). The security group was explicitly destroyed by Terraform before the VPC. Nothing remains.
Confirm $0 charges on the billing dashboard
The resources existed for only a few minutes. That falls well within the AWS Free Tier allowance for t3.micro instances. Confirm this by checking your billing page.
- In the AWS Management Console, click your account name in the top-right navigation bar.
- Select Billing and Cost Management.
- In the left navigation pane, click Bills.
- Select the current month under Billing period.
- Look at the Total amount. It should show $0.00 for EC2 charges (or no EC2 line at all if you have never used EC2 outside this project).
Why might I see a small charge?
If your account already had other resources running, you may see charges unrelated to this project. The key check is that EC2 under the current billing period does not show new charges from a t3.micro instance. A few minutes of usage on a Free Tier eligible instance costs $0.00.
Don't see billing data yet?
AWS billing data can take up to 24 hours to fully refresh. If you just destroyed the resources, you may not see any EC2 line item at all. That is the expected outcome for a few minutes of Free Tier usage.
If you are using an IAM user and cannot access the billing page, the root account owner needs to enable IAM access to billing from Account Settings.
Need more help? I cannot access the AWS billing page from my IAM user.
Secret mission
Add a Health Check and Auto-Output the URL
Production apps need health checks so monitoring tools can verify your service is alive. In this mission, you'll add a /health endpoint to your Flask app and use Terraform string interpolation to automatically output the full URL after deployment.
Clean Up Your Resources
Clean Up Your Resources
You already ran terraform destroy during the project, so your AWS infrastructure is gone. Decide whether to keep your local project files and Docker Hub images for future reference, or delete them entirely.
Cost warning
If you re-ran terraform apply after the final step and did not destroy, your EC2 instance is still running and consuming Free Tier hours. Run terraform destroy from your project folder immediately to avoid charges.
Resources you used:
- Local project files (flask-terraform folder on your Desktop containing app.py, Dockerfile, main.tf, ec2.tf, outputs.tf, requirements.txt).
- Docker Hub images (flask-terraform:v1 and flask-terraform:v2).
- AWS CLI credentials stored in ~/.aws/.
- Terraform CLI (installed via APT).
- AWS CLI v2 (installed from the bundled installer).
Keep everything running
No action needed. Choose this if you plan to continue experimenting with Terraform and AWS.
- Your local project files stay on your Desktop for reference or future modifications.
- Your Docker Hub images remain available for pulling onto new infrastructure.
- Terraform and AWS CLI stay installed and ready for your next project.
- Your AWS credentials in ~/.aws/ remain configured for future AWS CLI commands.
Pause - I'll come back to this later
Your AWS infrastructure is already destroyed. To free up local disk space while keeping your work recoverable, remove local Docker images but keep your project files and Docker Hub images.
- Remove the local Docker images by running:
docker rmi your-dockerhub-username/flask-terraform:v1
docker rmi your-dockerhub-username/flask-terraform:v2
- Keep your flask-terraform project folder on the Desktop. You can re-deploy anytime with terraform apply.
- Keep your Docker Hub images online. They cost nothing on the free Personal plan.
Delete - I don't want to use this again
Remove all project resources and start fresh. Follow each step below.
- Confirm your AWS infrastructure is destroyed by running this from your project folder:
terraform destroy
If you see "No changes. No objects need to be destroyed," everything is already gone.
- Delete your local project files by running:
rm -rf ~/Desktop/flask-terraform
- Delete your Docker Hub images:
Sign in to hub.docker.com. Navigate to Repositories and select your flask-terraform repository. Open the Tags tab, select the checkboxes next to v1 and v2, then click Delete.
- Remove local Docker images:
docker rmi your-dockerhub-username/flask-terraform:v1
docker rmi your-dockerhub-username/flask-terraform:v2
- Remove your AWS CLI credentials:
rm -rf ~/.aws/
- Uninstall AWS CLI v2:
sudo rm /usr/local/bin/aws
sudo rm /usr/local/bin/aws_completer
sudo rm -rf /usr/local/aws-cli
- Uninstall Terraform:
sudo apt remove terraform
That's a Wrap!
That's a Wrap!
Nice work! You just deployed a Dockerized Python app to AWS using nothing but code. No clicking around the console, no manual setup. Just Terraform files that spin up real cloud infrastructure on command.
You've learned how to:
- Containerize a Flask API with Docker and push the image to Docker Hub so any server in the world can pull and run it.
- Define a complete AWS network (VPC, subnet, internet gateway, route table, security group) and an EC2 instance entirely in Terraform configuration files.
- Run the full Terraform lifecycle: terraform init, terraform plan, terraform apply, and terraform destroy to create, inspect, and tear down cloud resources with a single command.
- Secret Mission: Added a health check endpoint to your Flask app and configured Terraform output interpolation to auto-generate the full application URL.
Ready to quiz yourself?