Deploy a 3-Tier API to ECS Fargate
Deploy a Flask API to ECS Fargate with ALB, private subnets, and DynamoDB.
Introduction
30 Second Summary
Every time you use a web app, your request passes through multiple layers before reaching the database. In production, each layer runs in isolation so that a single failure does not take down the entire system.
In this project, you will deploy a Python API to AWS ECS Fargate behind a load balancer, with containers running in private subnets that read and write data to DynamoDB. You'll build the entire architecture from scratch through the AWS Console.
What You'll Build
Picture curling a public URL from your terminal, watching your request flow through a load balancer into a container running in a private subnet, and getting back a JSON response with data straight from DynamoDB.
By the end of this project, you'll have:
- A live API endpoint you can curl from anywhere, proving traffic flows from an Application Load Balancer through your Fargate container to DynamoDB and back.
- A container image you built locally and pushed to Amazon ECR, running inside a private subnet with zero direct internet exposure.
- Hands-on understanding of every VPC networking component (subnets, NAT Gateway, route tables, security groups) and how they connect to form a production-ready network.
- Secret Mission: Add ECS Service Auto Scaling to handle traffic spikes automatically.
Are there any prerequisites?
You need Docker installed on your Mac and an AWS account. New AWS accounts receive up to $200 in credits over 6 months, which covers this project's costs.
Step 1 walks you through AWS account creation and CLI setup if you haven't done that yet.
Before We Start
Before we dive into the AWS Console, let's lock in what you're about to build and why it matters.
A 3-tier architecture separates your public entry point, your application logic, and your data layer into isolated network zones. This is how production workloads run on AWS. You're about to build one from scratch.
Set Up AWS Account and VPC Networking
Your API containers need a secure network to run in. Without proper isolation, they would be directly exposed to the internet with no security boundary between public traffic and your application.
A VPC (Virtual Private Cloud) solves this by letting you define exactly which resources are publicly reachable and which stay hidden. In this step, you'll create a VPC with public subnets for your load balancer and private subnets for your containers.
In this step, get ready to:
- Sign into the AWS Management Console.
- Create a VPC with public and private subnets using the console wizard.
- Verify that route tables connect each subnet tier to the correct gateway.
Sign into the AWS Management Console
Your AWS account and CLI are already configured from the previous step. Now you need access to the AWS Management Console where you'll build the VPC networking.
- Open your browser and navigate to console.aws.amazon.com.
- Sign in with your AWS account credentials (root user email and password).
- In the top-right corner, confirm your region is set to US East (N. Virginia) us-east-1. If it shows a different region, click the region dropdown and select US East (N. Virginia).
Why us-east-1?
This project uses us-east-1 for all resources. Keeping everything in one region avoids cross-region latency and simplifies troubleshooting.
Create a VPC with the VPC and more wizard
AWS provides a wizard that creates an entire network stack in one step: the VPC itself, subnets across multiple availability zones, route tables, an Internet Gateway, and a NAT Gateway.
- In the AWS Console search bar at the top, type VPC and select VPC from the results.
- On the VPC dashboard, click Create VPC.
- For Resources to create, select VPC and more.
What does VPC and more do?
This option creates not just the VPC, but also subnets, route tables, an Internet Gateway, and optionally a NAT Gateway. It wires everything together automatically so you don't have to create and associate each resource individually.
Now configure the VPC settings. Fill in the following fields:
- For Name tag auto-generation, enter ecs-lab.
- For IPv4 CIDR block, enter 10.0.0.0/16.
- For Number of Availability Zones, choose 2.
- For Number of public subnets, choose 2.
- For Number of private subnets, choose 2.
- For NAT gateways, choose In 1 AZ.
- For VPC endpoints, choose None.
Check the Preview pane on the right side of the page. You should see a diagram showing your VPC with 4 subnets across 2 availability zones, connected to an Internet Gateway and a NAT Gateway.
- Click Create VPC.
AWS takes about 30 seconds to create all resources. You'll see a progress screen with green checkmarks appearing next to each resource as it's created.
Why public AND private subnets?
Public subnets have a direct route to the Internet Gateway, making resources in them reachable from the internet. Your load balancer will live here.
Private subnets have no direct internet route. Your containers will live here, completely hidden from the outside world. The only way traffic reaches them is through the load balancer.
What is a NAT Gateway?
A NAT Gateway lets resources in private subnets make outbound internet requests (like pulling a container image from ECR) without being directly accessible from the internet. It acts as a one-way door: traffic can go out, but nothing from the internet can initiate a connection in.
NAT Gateway costs money
The NAT Gateway charges approximately $0.045 per hour while it exists, plus data processing fees. You'll delete it at the end of this project in the Before You Go section. Don't leave it running overnight if you want to avoid unexpected charges.
Verify your route tables
The wizard created route tables that control how traffic flows between your subnets and the internet. Before moving on, confirm these routes are correct.
- Once all resources show green checkmarks, click View VPC.
- Select the Resource map tab to see a visual diagram of your VPC.
- Click Show details to display CIDR ranges and route counts.
Before you check the route tables, think about this: which gateway should the public route table point to, and which should the private route table point to?
- In the left navigation pane, click Route tables.
- Find the route table with the name ecs-lab-rtb-public (or similar). Click on it to select it.
- Select the Routes tab. Confirm you see a route with Destination 0.0.0.0/0 and Target pointing to igw- (your Internet Gateway).
- Now find and select the route table named ecs-lab-rtb-private (or similar).
- Select the Routes tab. Confirm you see a route with Destination 0.0.0.0/0 and Target pointing to nat- (your NAT Gateway).
You should see the public route table sending all internet-bound traffic to the Internet Gateway, and the private route table routing through the NAT Gateway. This confirms your network isolation is working correctly.
What do these routes mean?
The destination 0.0.0.0/0 means "all traffic not destined for the VPC itself." In the public route table, this traffic goes to the Internet Gateway (direct internet access). In the private route table, it goes to the NAT Gateway (outbound-only internet access).
Don't see the routes?
Make sure you're looking at route tables tagged with ecs-lab in their name. Your AWS account may have a default VPC with its own route table. Filter by VPC to show only the ecs-lab VPC's route tables.
If no resources were created at all, go back to the VPC dashboard and check for error messages. The NAT Gateway requires an Elastic IP, which the wizard allocates automatically.
Help me troubleshoot my VPC route tables. I used the VPC and more wizard with name tag ecs-lab but I cannot see the expected routes to igw- and nat- in my route tables.
Your network foundation is ready. You have a VPC with proper isolation: public subnets for internet-facing resources and private subnets for your containers. Next up, you'll build and push the container image that will run inside these private subnets.
Build and Push Your API Container to ECR
Your VPC networking is in place. Traffic can flow between public and private subnets, and your NAT Gateway gives private resources a path to the internet.
Now you need something to actually run inside those private subnets. In this step, you'll write a Python Flask API, package it into a Docker container image, and push that image to Amazon ECR (Elastic Container Registry). This gives ECS the artifact it needs to launch your containers.
In this step, get ready to:
- Create a Flask API with health check and DynamoDB endpoints.
- Create an ECR private repository to store your container image.
- Build the Docker image locally and push it to ECR.
Create the project directory and API files
Your API needs three files: the application code, a dependency list, and a Dockerfile that tells Docker how to package everything into an image.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir ecs-lab-api
cd ecs-lab-api
- Confirm the folder exists by running:
ls
You should see an empty directory (no output). You are now inside ecs-lab-api on your Desktop.
- Open your preferred text editor and create a new file called app.py inside the ecs-lab-api folder. Add the following code:
import os
import json
from flask import Flask, request, jsonify
import boto3
# Create Flask app
app = Flask(__name__)
# Connect to DynamoDB table using environment variables
dynamodb = boto3.resource("dynamodb", region_name=os.environ.get("AWS_DEFAULT_REGION", "us-east-1"))
table = dynamodb.Table(os.environ.get("DYNAMODB_TABLE", "ecs-lab-items"))
# Health check endpoint
@app.route("/", methods=["GET"])
def health():
return jsonify({"status": "healthy"}), 200
What does this code do?
This sets up the foundation of your API:
- It imports Flask for the web server and boto3 (the AWS SDK for Python) to talk to DynamoDB.
- The DynamoDB connection reads its configuration from environment variables. This makes the container portable across different environments.
- The health check at / returns a simple JSON response. The load balancer will use this endpoint to confirm the container is alive.
- Below the health check function in app.py, add the remaining routes and the main block:
# List all items from DynamoDB
@app.route("/items", methods=["GET"])
def get_items():
response = table.scan()
return jsonify(response.get("Items", [])), 200
# Create a new item in DynamoDB
@app.route("/items", methods=["POST"])
def create_item():
item = request.get_json()
if not item or "id" not in item:
return jsonify({"error": "id is required"}), 400
table.put_item(Item=item)
return jsonify(item), 201
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
What do these routes do?
These two endpoints handle your API's data operations:
- GET /items scans the DynamoDB table and returns all items as JSON.
- POST /items validates that the incoming JSON has an id field, then writes it to DynamoDB.
- The if __name__ block starts the server on port 5000 when run directly.
- Save app.py.
- Confirm the file runs by executing this command:
python3 app.py
You should see an error like ModuleNotFoundError: No module named 'flask'. That is expected. Flask is not installed on your local machine, but it will be inside the Docker container.
Seeing 'command not found' for python3?
Try running python app.py instead (without the 3). Some systems alias Python differently.
Help me fix python not being found on my Mac
✔️ Awesome, I've got everything!
Great. Double check you have saved app.py before moving on.
ⓧ I'd like to double check the full code
Your app.py should contain two sections. Scroll up and confirm:
- The imports, app setup, DynamoDB connection, and health check route (first chunk).
- The get_items route, create_item route, and if __name__ block (second chunk).
- Create a new file called requirements.txt in your ecs-lab-api folder with the following content:
flask==3.1.3
boto3==1.43.54
Why pin exact versions?
Pinning versions ensures your container builds the same way every time. Without pins, a future pip install could pull a breaking update and your image would silently change behaviour.
- Create a new file called Dockerfile (no file extension) in your ecs-lab-api folder with the following content:
# Use a lightweight Python base image
FROM python:3.12-slim
# Set the working directory inside the container
WORKDIR /app
# Install dependencies first (layer caching)
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy the application code
COPY app.py .
# Expose the port Flask runs on
EXPOSE 5000
# Start the Flask server
CMD ["python", "app.py"]
What does this Dockerfile do?
Each line builds a layer in your container image:
- python:3.12-slim provides a minimal Python runtime (much smaller than the full image).
- Copying requirements.txt and running pip install before copying app.py means Docker can cache the dependency layer. Changing your app code does not re-install dependencies.
- EXPOSE 5000 documents which port the container listens on. The load balancer will send traffic here.
- Save Dockerfile.
- Confirm all three files exist by running:
ls
You should see Dockerfile, app.py, and requirements.txt listed.
Missing a file?
Make sure you saved each file inside the ecs-lab-api folder on your Desktop, not in a parent or child directory.
Help me find my missing project files
Create an ECR repository
Amazon ECR is a managed container registry. It stores your Docker images so that ECS Fargate can pull them into your private subnets when launching tasks.
- Switch back to the AWS Management Console in your browser.
- In the search bar at the top, type ECR and select Elastic Container Registry.
- In the left navigation pane, click Repositories.
- Make sure the Private tab is selected.
- Click Create repository.
- Under Visibility settings, confirm Private is selected.
- In the Repository name field, enter ecs-lab-api.
- Leave all other settings at their defaults.
- Click Create repository.
You should see your new ecs-lab-api repository appear in the repositories list.
- Click the repository name ecs-lab-api to open it.
- Click View push commands.
A modal appears with four commands customized to your AWS account ID and region. Keep this window open. You will use these commands in the next substep.
Why use the console's push commands?
The push commands include your unique AWS account ID and region baked in. Copying them directly avoids typos in the long ECR URI that you would otherwise need to assemble by hand.
Build and push the Docker image to ECR
With your files ready and ECR waiting, you can now build the container image and push it to your repository.
- In your terminal, make sure you are still inside the ecs-lab-api folder on your Desktop.
- Authenticate Docker to your ECR registry by running the first push command from the modal. It looks like this:
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin [[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]].dkr.ecr.us-east-1.amazonaws.com
What does this command do?
This pipes a temporary authentication token from the AWS CLI into docker login. It proves to ECR that your local Docker client is allowed to push images to your private registry.
You should see Login Succeeded in the output.
Seeing 'Unable to locate credentials'?
Make sure you configured the AWS CLI in Step 1 with aws configure. Run aws sts get-caller-identity to verify your credentials are working.
Help me fix my AWS CLI credentials
- Build your Docker image by running:
docker build -t ecs-lab-api .
Docker downloads the python:3.12-slim base image, installs your dependencies, and copies your app. The final line shows a success message with the image ID.
Build failing?
Make sure Docker Desktop is running on your Mac. You can verify by running docker info in your terminal.
If you see a syntax error, double check your Dockerfile has no extra spaces before FROM and no trailing characters on each line.
Help me troubleshoot my Docker build
- Tag the image for your ECR repository by running:
docker tag ecs-lab-api:latest [[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]].dkr.ecr.us-east-1.amazonaws.com/ecs-lab-api:latest
- Push the tagged image to ECR by running:
docker push [[AWS_ACCOUNT_ID="your 12-digit AWS account ID"]].dkr.ecr.us-east-1.amazonaws.com/ecs-lab-api:latest
You will see layer upload progress bars. Once complete, all layers show Pushed.
Copy the full image URI
Copy your full ECR image URI (the one you just pushed to, ending in /ecs-lab-api:latest). You will need this URI when creating the ECS task definition in a later step.
Before you verify, what do you expect to see when you query ECR for your image? Think about what tag you pushed.
- Verify your image landed in ECR by running:
aws ecr describe-images --repository-name ecs-lab-api --region us-east-1
You should see your image listed with the latest tag in the imageTag field of the output.
No images returned?
Make sure the docker push command completed without errors. If you see an authorization error, re-run the aws ecr get-login-password authentication command (tokens expire after 12 hours) and push again.
Help me troubleshoot my ECR image push
Your container image is now stored in ECR, ready for ECS to pull it into a running task. Next up, you will create the DynamoDB table and IAM roles your container needs to read and write data.
Create DynamoDB Table and IAM Roles
Your container image is sitting in ECR, ready to be pulled by ECS Fargate. But your Flask API expects to read and write items to a database, and it needs permission to do so.
In this step, you'll create the DynamoDB table your API stores data in, and set up IAM roles that grant your container the right permissions. Without these roles, your container would get AccessDenied errors every time it tries to touch DynamoDB.
In this step, get ready to:
- Create a DynamoDB table for storing API items.
- Create an IAM task role with DynamoDB permissions.
- Verify both roles and the table are correctly configured.
Create the DynamoDB table
Your API's app.py references a table called ecs-lab-items. You need to create this table in the DynamoDB console before deploying your container.
- In the AWS Management Console, search for DynamoDB and open the DynamoDB console.
- In the left navigation pane, choose Tables.
- Choose Create table.
- For Table name, enter ecs-lab-items.
- For Partition key, enter id and leave the type as String.
- For Table settings, keep the default selection of Default settings.
- Choose Create table.
Wait a few seconds for the table status to change from Creating to Active.
What is DynamoDB?
DynamoDB is a fully managed NoSQL database from AWS. It uses on-demand capacity by default, which means you pay per request and never have to provision or manage servers.
Your API uses it as a key-value store. Each item has an id field as its primary key, and you can add any other fields you want alongside it.
Table stuck in Creating status?
DynamoDB tables usually become Active within 5-10 seconds. If yours stays in Creating for more than a minute, refresh the page. Check that you selected the correct region (us-east-1) in the top-right corner of the console.
Still stuck? Help me troubleshoot my DynamoDB table creation.
Create the IAM task role
ECS Fargate uses two separate IAM roles for every task. The task role gives your application code (the Flask API) permission to call AWS services like DynamoDB. The task execution role gives ECS itself permission to pull your container image from ECR and write logs to CloudWatch.
You'll create the task role first, then confirm the execution role exists.
- In the AWS Management Console, search for IAM and open the IAM console.
- In the left navigation pane, choose Roles.
- Choose Create role.
- For Trusted entity type, choose AWS service.
- For Service or use case, choose Elastic Container Service.
- Choose the Elastic Container Service Task use case.
- Choose Next.
- In the Add permissions section, search for AmazonDynamoDBFullAccess.
- Select the checkbox next to AmazonDynamoDBFullAccess, then choose Next.
- For Role name, enter ecsLabTaskRole.
- Choose Create role.
You should see a success banner confirming the role was created.
Why two separate roles?
The task role (ecsLabTaskRole) gives your application code permission to call DynamoDB. It is what your Flask API uses when it calls table.scan() or table.put_item().
The task execution role (ecsTaskExecutionRole) gives ECS itself permission to pull your container image from ECR and send logs to CloudWatch. Your application code never uses this role directly.
Now confirm the task execution role exists. AWS may have already created it for you if you've used ECS before.
- Return to the Roles list in the IAM console.
- In the search box, type ecsTaskExecutionRole.
✔️ I see ecsTaskExecutionRole in the list
The role already exists. No action needed. Continue to the verification substep below.
ⓧ No results found
You need to create it. Follow these steps:
- Choose Create role.
- For Trusted entity type, choose AWS service.
- For Service or use case, choose Elastic Container Service.
- Choose the Elastic Container Service Task use case.
- Choose Next.
- Search for AmazonECSTaskExecutionRolePolicy and select the checkbox next to it.
- Choose Next.
- For Role name, enter ecsTaskExecutionRole.
- Choose Create role.
Verify both roles and the table
Before you predict, think about what you expect to see: which policy should be attached to each role?
- In the IAM console Roles list, search for ecsLabTaskRole and click on it.
- Open the Permissions tab.
You should see AmazonDynamoDBFullAccess listed as an attached policy. This confirms your container will be able to read and write items to DynamoDB.
- Go back to the Roles list and search for ecsTaskExecutionRole.
- Click on it and open the Permissions tab.
You should see AmazonECSTaskExecutionRolePolicy attached. This gives ECS permission to pull images from ECR and write logs to CloudWatch.
- Switch to the DynamoDB console and choose Tables in the left navigation.
- Confirm your ecs-lab-items table shows status Active.
What did you just prove?
You have a live database table ready to accept items, a task role that grants your container DynamoDB access, and an execution role that lets ECS pull your image. These three pieces are the data and permissions layer of your 3-tier architecture.
Don't see the policy attached?
If ecsLabTaskRole shows no policies, you may have skipped the permissions step during role creation. Click Add permissions > Attach policies, search for AmazonDynamoDBFullAccess, select it, and choose Add permissions.
Still stuck? Help me fix my IAM role permissions for ECS.
Your data layer is ready. Next up, you'll create the Application Load Balancer that serves as the public front door to your architecture.
Create the Application Load Balancer
Your container image is in ECR, your database is ready, and your IAM roles are set. But right now, nothing can reach your containers from the internet.
The Application Load Balancer is the front door to your architecture. It sits in the public subnets and forwards traffic to your containers in the private subnets. Without it, there is no way for users to reach your API.
In this step, get ready to:
- Create a security group that allows HTTP traffic to the load balancer.
- Create an Application Load Balancer in your public subnets.
- Create a target group and wire it to the ALB listener.
Create a security group for the ALB
Your ALB needs a security group that allows inbound HTTP traffic from anywhere on the internet. You will create this first, then attach it to the ALB during creation.
- In the AWS Console, type VPC in the top search bar and select VPC.
- In the left navigation pane, click Security groups.
- Click Create security group.
- For Security group name, enter ecs-lab-alb-sg.
- For Description, enter Allow HTTP inbound for ALB.
- For VPC, select your ecs-lab VPC.
- Under Inbound rules, click Add rule.
- Set Type to HTTP, which auto-fills port 80.
- Set Source to Anywhere-IPv4 (0.0.0.0/0).
- Click Create security group.
You should see a success banner confirming ecs-lab-alb-sg was created.
Why allow 0.0.0.0/0 on port 80?
The ALB is your public entry point. Allowing all IPv4 addresses means any user on the internet can send HTTP requests to it. Your containers remain safe because they sit in private subnets with a separate, locked-down security group.
Create the target group
A target group tells the ALB where to send traffic. You will point it at port 5000 because that is where your Flask container listens.
- In the left navigation of the EC2 Console, under Load Balancing, click Target Groups.
- Click Create target group.
- For Choose a target type, select IP addresses.
- For Target group name, enter ecs-lab-tg.
- For Protocol, keep HTTP.
- For Port, change it to 5000.
- For VPC, select your ecs-lab VPC.
- Expand the Health checks section and confirm Health check path is set to /.
- Click Next.
- On the Register targets page, do not add any targets. Click Create target group.
You should see ecs-lab-tg listed with 0 registered targets.
Why IP type instead of Instance?
ECS Fargate tasks use the awsvpc networking mode, which assigns each task its own private IP address. The ALB registers these IPs directly, so you need a target group of type IP rather than Instance.
Create the Application Load Balancer and test it
Now you will create the ALB itself, attach the security group, and wire the listener to forward traffic to your target group.
- In the left navigation of the EC2 Console, under Load Balancing, click Load Balancers.
- Click Create load balancer.
- Under Application Load Balancer, click Create.
- For Load balancer name, enter ecs-lab-alb.
- For Scheme, select Internet-facing.
- For IP address type, keep IPv4.
- Under Network mapping, select your ecs-lab VPC.
- Check both public subnets (one per Availability Zone).
- Under Security groups, remove the default security group and select ecs-lab-alb-sg.
- Under Listeners and routing, keep the default HTTP listener on port 80.
- For Default action, select the target group ecs-lab-tg.
- Scroll to the bottom and click Create load balancer.
AWS will begin provisioning your ALB. Wait until the State changes from Provisioning to Active (this takes 2-3 minutes).
What did you just wire together?
The ALB lives in your two public subnets and accepts HTTP requests on port 80. When a request arrives, the listener forwards it to ecs-lab-tg on port 5000.
The target group currently has zero registered targets. In the next step, your ECS service will register its tasks here automatically.
Now test the ALB. Before you run this, what do you think you will see when you send a request to the ALB? There are no containers behind it yet.
- On the load balancer details page, copy the DNS name value (it looks like ecs-lab-alb-123456789.us-east-1.elb.amazonaws.com).
- In your terminal, curl the ALB by running this command:
curl http://[[ALB_DNS="your ALB DNS name from the console"]]
You get a 503 Service Unavailable response. This is expected. The ALB is live and listening, but it has no targets to forward traffic to.
Why 503 and not 404?
A 503 means the load balancer itself is working, but every target in the target group is either unhealthy or missing. The target group has zero registered IPs right now. Once you deploy your ECS service in the next step, it will register its task IPs and the 503 disappears.
Not seeing 503?
If you see a Could not resolve host error, your ALB may still be in Provisioning state. Wait another minute and try again.
If you get a timeout, confirm you copied the full DNS name including the region and .elb.amazonaws.com suffix.
Help me troubleshoot my ALB returning unexpected responses.
Your front door is open and waiting. Next up, you will deploy your ECS Fargate service and connect it to this target group so real traffic flows end-to-end.
Deploy the ECS Fargate Service
Your ALB is live and listening for traffic. When you curled it at the end of the last step, it returned a 503 because there are no targets registered behind it yet.
In this step, you'll connect all the pieces: create an ECS cluster, define how your container runs as a task definition, and deploy it as a service wired to the ALB target group. Once the service is running, the 503 disappears and your API responds through all three tiers.
In this step, get ready to:
- Create an ECS Fargate cluster and task definition.
- Create a security group that only allows traffic from the ALB.
- Deploy the ECS service and verify end-to-end API access.
Create the ECS cluster and task definition
An ECS cluster is a logical grouping for your services and tasks. The task definition is the blueprint that tells Fargate which container image to run, how much CPU and memory to give it, and which IAM roles to use.
- Open the ECS console at https://console.aws.amazon.com/ecs/v2.
- Click Clusters in the left sidebar.
- Click Create cluster.
- Enter ecs-lab-cluster in the Cluster name field.
- Under Infrastructure, keep AWS Fargate selected (this is the default).
- Click Create.
You should see a success banner and your ecs-lab-cluster listed on the Clusters page.
What is an ECS cluster?
A cluster is a logical boundary that groups your services and tasks. It does not represent a physical server. With Fargate, AWS manages the underlying compute. You just tell the cluster what to run.
Now you'll create the task definition that describes your container. This is a multi-section form.
- Click Task definitions in the left sidebar.
- Click Create new task definition.
- Enter ecs-lab-task in the Task definition family field.
- Under Infrastructure requirements, select AWS Fargate as the launch type.
- Set CPU to .25 vCPU.
- Set Memory to .5 GB.
- For Task role, select ecsLabTaskRole.
- For Task execution role, select ecsTaskExecutionRole.
Why two different roles?
The task role (ecsLabTaskRole) gives your running container permissions to call AWS APIs like DynamoDB. The execution role (ecsTaskExecutionRole) gives ECS itself permissions to pull your image from ECR and write logs to CloudWatch.
Separating these roles follows the principle of least privilege. Your app code never gets ECR pull permissions, and ECS never gets DynamoDB access.
Now scroll down to the container definition section.
- In the Container - 1 section, enter api in the Name field.
- For Image URI, switch to the ECR console in a new tab, click your ecs-lab-api repository, and copy the URI column value for the latest tag. Paste it back in the task definition form.
- Under Port mappings, enter 5000 for Container port and select TCP as the protocol.
- Expand the Environment variables section.
- Add a variable with Key = AWS_DEFAULT_REGION and Value = us-east-1.
- Add another variable with Key = DYNAMODB_TABLE and Value = ecs-lab-items.
- Scroll to the bottom and click Create.
You should see a success banner confirming ecs-lab-task:1 was created.
What does the task definition do?
It tells Fargate exactly how to run your container: which image to pull, how much compute to allocate, which port to expose, and what environment variables to inject. Every time ECS launches a task, it reads this blueprint.
Don't see your roles in the dropdown?
Make sure you created ecsLabTaskRole with the trusted entity set to Elastic Container Service Task (not EC2 or Lambda). If the role trust policy is wrong, it won't appear in the ECS task definition form.
Help me troubleshoot my ECS task definition roles not appearing in the dropdown.
Create a security group for ECS tasks
Your containers need their own security group. The critical rule: only allow inbound traffic on port 5000 from the ALB's security group. This ensures containers are never directly accessible from the internet.
- Open the EC2 console and click Security Groups in the left sidebar under Network & Security.
- Click Create security group.
- Enter ecs-lab-ecs-sg for the Security group name.
- Enter a description like Allow traffic from ALB only.
- Select your ecs-lab VPC from the VPC dropdown.
- Under Inbound rules, click Add rule.
- Set Type to Custom TCP, Port range to 5000, and Source to ecs-lab-alb-sg (start typing the name and select it from the dropdown).
- Click Create security group.
You should see your new ecs-lab-ecs-sg security group with the inbound rule referencing ecs-lab-alb-sg.
Why restrict to the ALB security group?
By referencing the ALB's security group as the source (instead of an IP range), you create a chain: internet traffic hits the ALB on port 80, and only the ALB can reach your containers on port 5000. Even if someone discovers your container's private IP, they cannot connect directly.
Can't find the ALB security group in the dropdown?
Make sure you selected the correct ecs-lab VPC first. Security groups are VPC-scoped, so the ALB's group only appears when the correct VPC is selected.
Help me find my ALB security group when creating the ECS security group.
Deploy the service and verify end-to-end
You now have a cluster, a task definition, and a security group. The final step is creating a service that ties them together and wires the ALB target group so traffic flows through all three tiers.
- Return to the ECS console and click Clusters in the left sidebar.
- Click your ecs-lab-cluster.
- Select the Services tab and click Create.
- Under Deployment configuration, select ecs-lab-task as the Task definition family.
- Enter ecs-lab-service as the Service name.
- Set Desired tasks to 1.
Now configure the networking section.
- Expand Networking.
- For VPC, select your ecs-lab VPC.
- For Subnets, select both private subnets (remove any public subnets that were auto-selected).
- For Security group, remove the default and select ecs-lab-ecs-sg.
- Set Public IP to Turned off.
Why private subnets and no public IP?
Your containers live in private subnets with no direct internet access. They reach the internet (to pull images from ECR) through the NAT Gateway you created in Step 1. Inbound traffic arrives only through the ALB. This is the production pattern: containers are hidden, the ALB is the only public entry point.
Finally, wire the load balancer.
- Expand Load balancing.
- Select Application Load Balancer as the load balancer type.
- Choose your existing ecs-lab-alb from the dropdown.
- Under Container, select api 5000:5000 and choose Use an existing target group.
- Select ecs-lab-tg as the target group.
- Click Create at the bottom of the page.
ECS will now provision a Fargate task in one of your private subnets. This takes 1-3 minutes.
- On the service page, click the Tasks tab.
- Wait until the task status changes from PROVISIONING to RUNNING.
- Switch to the EC2 console, click Target Groups, select ecs-lab-tg, and check the Targets tab. Wait until the target health shows healthy.
What happens behind the scenes?
When the service launches, ECS places a task in a private subnet and attaches an elastic network interface (ENI) to it. The task pulls your container image from ECR through the NAT Gateway. Once the container starts, ECS automatically registers the task's private IP with the ecs-lab-tg target group. The ALB starts sending health checks to port 5000, and when it gets a 200 response from your / endpoint, the target is marked healthy.
Before you run the final test, what do you think the ALB will return now that a healthy target is registered?
- Copy your ALB DNS name from the EC2 Load Balancers page and test it by running:
curl http://[[ALB_DNS="your ALB DNS name (e.g. ecs-lab-alb-123456789.us-east-1.elb.amazonaws.com)"]]/
You should see:
{"status": "healthy"}
The 503 is gone. Traffic is flowing from the ALB through your Fargate container.
- POST an item to DynamoDB by running:
curl -X POST -H "Content-Type: application/json" -d '{"id":"1","name":"test"}' http://[[ALB_DNS="your ALB DNS name"]]/items
You should see the item echoed back with a 201 response:
{"id": "1", "name": "test"}
- GET the items back by running:
curl http://[[ALB_DNS="your ALB DNS name"]]/items
You should see your item returned in a list, proving the full 3-tier flow works: ALB to Fargate container to DynamoDB and back.
Still getting 503 or connection timeout?
Check these in order:
- In the ECS Tasks tab, verify the task status is RUNNING. If it shows STOPPED, click the task ID and check the Stopped reason field. A CannotPullContainerError means the task cannot reach ECR. Confirm your private subnet route table has a route to the NAT Gateway.
- In the target group Targets tab, check if the target is unhealthy. This usually means the health check on / is failing. Verify your security group ecs-lab-ecs-sg allows inbound TCP 5000 from ecs-lab-alb-sg.
- If the task stops with an AccessDeniedException related to DynamoDB, confirm the ecsLabTaskRole has the AmazonDynamoDBFullAccess policy attached.
Help me troubleshoot my ECS Fargate task that isn't starting or responding.
Your 3-tier architecture is fully operational. Traffic enters through the internet-facing ALB, routes to your Fargate container in a private subnet, and the container reads and writes to DynamoDB. Next up, you'll make this architecture resilient with auto scaling in the Secret Mission.
Secret mission
Add ECS Service Auto Scaling
Your service runs a single task. Under real traffic, that single container would become a bottleneck. In this secret mission, you'll configure ECS Service Auto Scaling so your architecture can automatically add or remove tasks based on CPU load.
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. This project uses several AWS resources that incur hourly charges while active.
Cost warning
Your NAT Gateway costs approximately $0.045/hr and the ALB costs approximately $0.0225/hr. Together with Fargate task billing, costs accumulate at roughly $0.07/hr ($1.70/day). Delete your resources when you are done to avoid unexpected charges.
If you created a new AWS account, the $200 free credits cover all costs as long as you tear down resources within a few hours.
Resources you used:
- ECS Service (ecs-lab-service) running Fargate tasks.
- ECS Cluster (ecs-lab-cluster).
- Application Load Balancer (ecs-lab-alb) and Target Group (ecs-lab-tg).
- NAT Gateway and associated Elastic IP.
- VPC (ecs-lab) with 4 subnets, route tables, Internet Gateway, and security groups.
- ECR Repository (ecs-lab-api).
- DynamoDB Table (ecs-lab-items).
- IAM Roles (ecsLabTaskRole and ecsTaskExecutionRole).
- Local project files (app.py, requirements.txt, Dockerfile).
- AWS CLI v2 installed on your Mac.
Keep everything running
No action needed. Choose this if you are still actively building or experimenting with your deployment.
- Your resources will continue incurring charges at approximately $0.07/hr (~$1.70/day).
- Your ALB DNS URL remains live and reachable.
- The AWS CLI and Docker remain installed on your Mac for future projects.
Pause - I'll come back to this later
Stop the biggest cost drivers while keeping your configuration intact so you can resume later.
- In the ECS Console, select ecs-lab-cluster, open the Services tab, select ecs-lab-service, choose Update, set Desired tasks to 0, then choose Update.
- In the VPC Console, select your NAT Gateway and choose Actions, then Delete NAT gateway. This stops the biggest hourly charge.
- Release the Elastic IP associated with the NAT Gateway: go to Elastic IPs in the VPC Console, select the unassociated IP, and choose Actions then Release Elastic IP addresses.
- To resume later, recreate the NAT Gateway in a public subnet and update the private route table to route 0.0.0.0/0 through it. Then set the service desired count back to 1.
Delete - I don't want to use this again
Remove all project resources completely. Follow this order to avoid dependency errors.
- Delete the ECS service: In the ECS Console, go to Clusters and select ecs-lab-cluster. Open the Services tab, select ecs-lab-service, then choose Delete. Check Force delete service, type delete, and choose Delete.
- Delete the ECS cluster: On the cluster page, choose Delete Cluster. Type delete ecs-lab-cluster to confirm.
- Delete the ALB: In the EC2 Console under Load Balancers, select ecs-lab-alb, choose Actions, then Delete load balancer. Confirm by typing confirm.
- Delete the Target Group: Under Target Groups, select ecs-lab-tg, choose Actions, then Delete.
- Delete the NAT Gateway: In the VPC Console under NAT gateways, select your NAT Gateway, choose Actions, then Delete NAT gateway. Wait for the state to change to Deleted.
- Release the Elastic IP: Under Elastic IPs, select the IP that was associated with the NAT Gateway, choose Actions, then Release Elastic IP addresses.
- Delete the VPC: Under Your VPCs, select the ecs-lab VPC, choose Actions, then Delete VPC. The console removes subnets, route tables, Internet Gateway, and security groups automatically. Confirm the deletion.
- Delete the ECR repository: In the ECR Console, select ecs-lab-api, choose Delete, type delete to confirm. This removes all stored images.
- Delete the DynamoDB table: In the DynamoDB Console, select ecs-lab-items, choose Delete, uncheck the option to create a backup, type confirm, then choose Delete.
- Delete the IAM roles: In the IAM Console under Roles, search for and delete ecsLabTaskRole. Select it, choose Delete, and confirm. Repeat for ecsTaskExecutionRole.
- Delete the local project files: Open Finder, navigate to your Desktop, and move the project folder (containing app.py, requirements.txt, and Dockerfile) to the Trash.
- Uninstall the AWS CLI v2 by running the following commands in your terminal:
sudo rm /usr/local/bin/aws
sudo rm /usr/local/bin/aws_completer
sudo rm -rf /usr/local/aws-cli
You do not need to close your AWS account. With all resources deleted, there are no ongoing charges. Keep the account for future projects.
That's a Wrap!
That's a Wrap!
Nice work! You just deployed a fully functional 3-tier API to ECS Fargate with traffic flowing from a public Application Load Balancer through containers in private subnets to a DynamoDB table.
You've learned how to:
- Build a production-style 3-tier architecture with a custom VPC, public and private subnets, a NAT Gateway, and security groups that enforce network isolation.
- Containerize a Python Flask API, push the image to Amazon ECR, and deploy it as a Fargate service with separate IAM roles for task execution and application permissions.
- Wire an Application Load Balancer to forward internet traffic to containers running in private subnets, proving end-to-end connectivity by curling a live public URL.
- Secret Mission: Configure ECS Service Auto Scaling so your service handles traffic spikes automatically by adding and removing tasks based on CPU utilization.
Ready to quiz yourself?