Secure and Scale ECS on AWS
Deploy ECS Fargate in private subnets with an ALB and auto scaling.
Introduction
30 Second Summary
When you deploy containers in the cloud, two things keep engineers up at night: making the app reachable by users while keeping it invisible to attackers, and handling traffic spikes without crashing or overpaying.
In this project, you will rebuild your ECS Fargate deployment from the ground up with production-grade security and auto scaling. You will isolate your tasks in private subnets, route traffic through a load balancer, and configure automatic scaling based on CPU utilization.
What You'll Build
You will open your browser, hit a load balancer URL, and see your containerized app respond instantly, even though the tasks running it have no public IP addresses and are completely unreachable from the internet.
By the end of this project, you'll have:
- A secure VPC with public and private subnets, where you can confirm your tasks have only private IPs and all inbound traffic flows through the Application Load Balancer.
- Fine-grained IAM roles that give your container code permission to read from S3 while keeping infrastructure permissions separate, proving least-privilege security works at the task level.
- A target tracking auto scaling policy that automatically adjusts your task count based on CPU utilization, with live CloudWatch metrics and alarms you can observe in real time.
- Secret Mission: Add a second service with path-based routing on the same ALB, so different URL paths reach different microservices.
Are there any prerequisites?
This project assumes you have completed Project 1 (basic ECS Fargate deployment) and have a container image in ECR. You also need an existing AWS account with AdministratorAccess.
No additional tooling is required. This is a 100% console-based project.
Before We Start
Before you touch the AWS console, let's lock in what you're about to build and why it matters for the SAA-C03 exam.
Build a Secure VPC with Public and Private Subnets
In Project 1, your ECS tasks ran in a public subnet with a public IP address. That is the number one anti-pattern on the SAA-C03 exam.
Production workloads belong in private subnets where they are unreachable from the internet. In this step, you will build a VPC with proper network segmentation: public subnets for internet-facing resources and private subnets where your ECS tasks will live.
In this step, get ready to:
- Create a VPC with public and private subnets across 2 Availability Zones.
- Create security groups that control traffic between the ALB and ECS tasks.
- Verify route tables are correctly configured for internet access.
Create the VPC using the wizard
AWS provides a VPC and more wizard that creates your VPC, subnets, route tables, and gateways all in one go. This saves you from creating each resource manually.
- Open the Amazon VPC console at https://console.aws.amazon.com/vpc/.
- On the VPC dashboard, click Create VPC.
- For Resources to create, choose VPC and more.
- For Name tag auto-generation, enter ecs-secure. The wizard will append suffixes like -vpc, -subnet-public, etc.
- For IPv4 CIDR block, keep the default 10.0.0.0/16.
- For Number of Availability Zones (AZs), choose 2.
- For Number of public subnets, choose 2.
- For Number of private subnets, choose 2.
- For NAT gateways, choose In 1 AZ. This gives private subnets outbound internet access while keeping costs lower for a learning project.
- For VPC endpoints, choose S3 Gateway. This creates a free gateway endpoint so your tasks can reach S3 without going through the NAT Gateway.
- For DNS options, keep both options enabled.
- Click Create VPC.
The wizard takes a few moments to create all the resources. Wait for the status to show all resources as successfully created.
What did the wizard create?
The VPC and more wizard created six key resources for you in one action.
- A VPC named ecs-secure-vpc with the CIDR range 10.0.0.0/16.
- 2 public subnets and 2 private subnets spread across 2 Availability Zones.
- An Internet Gateway attached to the VPC (for public subnet internet access).
- A NAT Gateway in one public subnet (for private subnet outbound access).
- Route tables wired up for each subnet type.
- An S3 Gateway endpoint for free, private-network access to S3.
Why In 1 AZ instead of 1 per AZ?
A NAT Gateway costs approximately $0.045/hour. Choosing 1 per AZ would create two NAT Gateways and double that cost. For a learning project, one NAT Gateway is enough to demonstrate the concept. In production you would deploy one per AZ for high availability.
- Confirm the VPC was created by checking that the workflow page shows all resources with green checkmarks.
- Click View VPC to see your new ecs-secure-vpc in the VPC list.
Don't see the VPC?
Make sure you are looking in the correct AWS Region. The VPC is created in whatever Region you have selected in the console header. If you see an empty VPC list, check the Region dropdown in the top-right corner.
Help me troubleshoot
Create the security groups
You need two security groups that work together like a chain. The ALB security group allows HTTP traffic from the internet. The ECS tasks security group only accepts traffic from the ALB security group.
- In the VPC console left navigation pane, click Security Groups.
- Click Create security group.
- For Security group name, enter alb-sg.
- For Description, enter Allow HTTP from internet.
- For VPC, select ecs-secure-vpc from the dropdown. Remove any pre-selected VPC first.
- Under Inbound rules, click Add rule.
- Set Type to HTTP, which automatically fills port 80.
- Set Source to Anywhere-IPv4 (0.0.0.0/0).
- Leave the Outbound rules as the default (all traffic allowed).
- Click Create security group.
You should see alb-sg listed with its inbound rule. Now create the second security group for ECS tasks.
- Click Create security group again.
- For Security group name, enter ecs-tasks-sg.
- For Description, enter Allow HTTP from ALB only.
- For VPC, select ecs-secure-vpc.
- Under Inbound rules, click Add rule.
- Set Type to HTTP (port 80).
- For Source, choose Custom, then type alb-sg in the search field and select the alb-sg security group from the dropdown.
- Leave the Outbound rules as the default (all traffic allowed).
- Click Create security group.
Why reference a security group instead of an IP range?
When you set the source of a rule to another security group, AWS automatically allows traffic from any resource associated with that group. This means the ECS tasks will accept traffic from the ALB regardless of the ALB's IP address.
This is the security group chaining pattern that the SAA-C03 exam tests. It enforces least-privilege access: only the ALB can talk to your tasks, not the open internet.
Can't find alb-sg in the Source dropdown?
Make sure you selected ecs-secure-vpc as the VPC for this security group. Security groups only appear in the Source dropdown if they belong to the same VPC. Also confirm you are typing alb-sg (not alb_sg) in the search field.
Help me troubleshoot
Verify the route tables
The VPC wizard created route tables automatically. Before moving on, you need to confirm that traffic flows correctly: public subnets route to the Internet Gateway, and private subnets route through the NAT Gateway.
Before you check, think about what you expect to see. The public subnet route table should send 0.0.0.0/0 traffic to the Internet Gateway. The private subnet route table should send 0.0.0.0/0 traffic to the NAT Gateway. Do you know why the targets differ?
- In the VPC console left navigation, click Route tables.
- Find the route table named ecs-secure-rtb-public (or similar with public in the name). Click on it.
- Click the Routes tab. Confirm you see a route with destination 0.0.0.0/0 and a target starting with igw- (the Internet Gateway).
- Go back to Route tables and find the route table named ecs-secure-rtb-private1- (or similar with private in the name). Click on it.
- Click the Routes tab. Confirm you see a route with destination 0.0.0.0/0 and a target starting with nat- (the NAT Gateway).
Why do the targets differ?
The public subnet routes 0.0.0.0/0 directly to the Internet Gateway because resources there have public IPs and can receive inbound internet traffic.
The private subnet routes 0.0.0.0/0 to the NAT Gateway instead. The NAT Gateway lets private resources reach the internet (to pull container images, for example) without being reachable from the internet. This is the core of network segmentation.
Missing the 0.0.0.0/0 route in your private route table?
If you chose None for NAT gateways during VPC creation, the private route table will not have a 0.0.0.0/0 route. You will need to delete this VPC and recreate it with the In 1 AZ NAT gateway option selected.
Help me fix my route table
Your VPC is ready. You have public and private subnets, a NAT Gateway for outbound access, and two security groups that chain together to restrict traffic. Next up, you will create IAM roles that give your container the exact permissions it needs.
Create IAM Roles and a New Task Definition
Your VPC is locked down with proper network segmentation. Now it is time to set up the IAM permissions that control what your containers can actually do.
In Project 1, you likely used the default execution role and no task role. The SAA-C03 exam specifically tests whether you understand the difference. The execution role lets ECS pull images and write logs. The task role gives your application code access to AWS services like S3. Without a separate task role, your container has zero AWS permissions at runtime.
In this step, get ready to:
- Create a task role that grants your container S3 read access.
- Confirm the task execution role from Project 1 still exists.
- Build a new task definition that references both roles with CloudWatch logging.
Create the task role (ecsTaskS3ReadRole)
The task role is what gives your application code inside the container permission to call AWS APIs. You will create a role that allows your container to read objects from S3.
- Open the IAM console at https://console.aws.amazon.com/iam/.
- In the left navigation pane, choose Roles, then choose Create role.
- For Trusted entity type, choose AWS service.
- For Service or use case, choose Elastic Container Service, then choose the Elastic Container Service Task use case.
- Choose Next.
- On the Add permissions page, do not select any managed policies. You will add an inline policy after creating the role. Choose Next.
- For Role name, enter ecsTaskS3ReadRole.
- Review the role, then choose Create role.
You should see a success banner confirming the role was created. Now you will add the S3 permissions as an inline policy.
- In the Roles list, search for ecsTaskS3ReadRole and click the role name to open it.
- Select the Permissions tab.
- Choose Add permissions, then Create inline policy.
- Switch to the JSON tab in the policy editor.
- Replace the contents with the following policy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:ListBucket"
],
"Resource": "*"
}
]
}
What does this policy do?
This policy grants two S3 permissions. s3:GetObject lets the container download files from S3 buckets. s3:ListBucket lets the container list the contents of S3 buckets.
Using * as the resource keeps things simple for this learning project. In production, you would scope this to a specific bucket ARN.
- Choose Next.
- For Policy name, enter S3ReadAccess.
- Choose Create policy.
You should see the S3ReadAccess inline policy now listed under Permissions policies on the role's detail page.
Don't see the inline policy?
Make sure you clicked Create inline policy (not Attach policies). Attached managed policies and inline policies appear in separate sections on the Permissions tab.
help me troubleshoot my ecsTaskS3ReadRole inline policy
Confirm the task execution role exists
The task execution role is a separate role that ECS itself uses behind the scenes to pull your container image from ECR and write logs to CloudWatch. It was likely created during Project 1.
- In the IAM console, choose Roles from the left navigation pane.
- In the search box, type ecsTaskExecutionRole.
- Click the role name to open it.
- On the Permissions tab, confirm that AmazonECSTaskExecutionRolePolicy is attached.
What is the difference between these two roles?
The task execution role is for ECS infrastructure. It lets the ECS agent pull your image from ECR and push logs to CloudWatch. Your container code never sees or uses this role directly.
The task role is for your application code. It lets the container itself call AWS APIs like S3, DynamoDB, or SQS. This is the role that follows the principle of least privilege for each service.
✔️ I see ecsTaskExecutionRole with the policy attached
You are all set. Both IAM roles are ready.
ⓧ I cannot find ecsTaskExecutionRole
If the role does not exist, create it now by following these steps:
- Choose Create role.
- For Trusted entity type, choose AWS service.
- For Service or use case, choose Elastic Container Service, then choose Elastic Container Service Task.
- Choose Next.
- In the Add permissions section, search for AmazonECSTaskExecutionRolePolicy, then select the policy.
- Choose Next.
- For Role name, enter ecsTaskExecutionRole.
- Choose Create role.
Create the task definition (secure-app)
Now you will tie both roles together in a new task definition. This tells ECS exactly how to run your container, including which roles to use, how much CPU and memory to allocate, and where to send logs.
- Open the ECS console at https://console.aws.amazon.com/ecs/v2.
- In the navigation pane, choose Task definitions.
- On the Create new task definition menu, choose Create new task definition.
- For Task definition family, enter secure-app.
- For Launch type, confirm AWS Fargate is selected.
- Expand the Task roles section.
- For Task role, select ecsTaskS3ReadRole.
- For Task execution role, select ecsTaskExecutionRole.
Now configure the task size and container settings.
- For CPU, select .25 vCPU (256 CPU units).
- For Memory, select .5 GB (512 MB).
- In the Container - 1 section, for Name, enter a container name (e.g., app).
- For Image URI, paste the ECR image URI from Project 1.
- Under Port mappings, set Container port to 80 and Protocol to TCP.
- Expand the Logging section and confirm Use log collection is turned on with Amazon CloudWatch selected. This enables the awslogs log driver.
- Scroll to the bottom and choose Create.
Why awsvpc network mode?
Fargate uses awsvpc as its only network mode. This means each task gets its own elastic network interface (ENI) with a private IP address. Security groups attach directly to the task, giving you network-level isolation between tasks.
You did not need to select awsvpc manually because it is automatic for Fargate. The SAA-C03 exam expects you to know this is the only option for Fargate tasks.
Before you verify, what do you expect to see when you open the task definition? Think about which fields confirm that both roles are correctly assigned.
- After the task definition is created, you will land on the secure-app detail page.
- Expand the Task roles section and confirm Task role shows ecsTaskS3ReadRole and Task execution role shows ecsTaskExecutionRole.
- Confirm the task size shows .25 vCPU and .5 GB memory.
- Confirm the container port mapping shows port 80.
Don't see your roles in the task definition?
If either role field is empty or shows the wrong value, you may have skipped the Task roles section (it is collapsed by default). Create a new revision of the task definition and expand that section to select the correct roles.
help me fix my ECS task definition roles
Your IAM roles and task definition are ready. Next up, you will deploy this task definition into your private subnets and see what happens when a container tries to pull an image without internet access.
Deploy to Private Subnets and Observe the Failure
In Step 2, you gave your container two distinct identities: one for ECS infrastructure (pulling images, writing logs) and one for your application code (reading S3). Now it is time to actually run these tasks in the right place.
Production workloads belong in private subnets where they are unreachable from the internet. But that isolation introduces a connectivity requirement. Your task needs to pull its container image from ECR, which lives outside the VPC. If outbound routing is not configured correctly, the task will never start.
In this step, get ready to:
- Create an ECS cluster using Fargate.
- Deploy a service into private subnets and observe what happens.
- Verify the NAT Gateway route and confirm a running task.
Create the ECS cluster
An ECS cluster is a logical grouping that holds your services and tasks. You need one before you can deploy anything.
- Open the ECS console at https://console.aws.amazon.com/ecs/v2.
- In the left navigation pane, choose Clusters.
- Click Create cluster.
- Under Cluster configuration, for Cluster name, enter secure-cluster.
- Leave Infrastructure set to AWS Fargate (serverless) (this is the default).
- Click Create.
Wait for the cluster status to show Active. This takes a few seconds.
What is a cluster?
A cluster is a logical boundary, not a piece of infrastructure you manage. With Fargate, AWS handles all the underlying compute. The cluster just groups your services together so you can manage them as a unit.
Create a service in private subnets
Now you will deploy your secure-app task definition as a service running in your private subnets. This is where the security boundary gets real.
- From within your secure-cluster cluster page, select the Services tab.
- Click Create.
- Under Environment, choose Launch type and confirm it is set to FARGATE.
- Under Deployment configuration, for Task definition family, select secure-app.
- For Service name, enter secure-app-service.
- For Desired tasks, enter 1.
- Expand the Networking section.
- For VPC, select ecs-secure-vpc.
- For Subnets, remove any public subnets and select only the two private subnets. You can identify them by their route table association (they route to the NAT Gateway, not the Internet Gateway).
- For Security group, remove the default security group and select ecs-tasks-sg.
- For Public IP, make sure it is set to Turned off. This is critical. Your tasks must NOT have public IPs.
- Click Create.
Why turn off the public IP?
With awsvpc network mode, each Fargate task gets its own Elastic Network Interface (ENI). If you assign a public IP, the task is directly reachable from the internet. Turning it off means the task is isolated in the private subnet with no inbound internet path.
The only outbound path to the internet is through the NAT Gateway you set up in Step 1.
Before you check on your task, take a moment to predict: do you think it will successfully pull the container image and reach RUNNING status?
- In the secure-app-service page, click the Tasks tab.
- Watch the task status. It will initially show Provisioning.
If the NAT Gateway route from Step 1 is correctly in place, your task will transition to RUNNING after 30 to 60 seconds. If your route table is misconfigured (for example, if you chose "None" for NAT Gateways when creating the VPC), the task will stay stuck in PROVISIONING and eventually stop with a CannotPullContainerError.
What is CannotPullContainerError?
This error means the Fargate task tried to reach ECR to download your container image, but it could not establish a network connection. In a private subnet with no public IP, the only path to the internet is through a NAT Gateway. If the private subnet's route table does not have a 0.0.0.0/0 route pointing to a NAT Gateway, the task is completely isolated and cannot pull anything.
This is one of the most common exam scenarios on the SAA-C03: Fargate tasks failing in private subnets due to missing NAT Gateway routes.
Verify the route table and confirm a running task
Whether your task is running or stuck, the next action is the same: verify the networking is correct. This is the troubleshooting skill the SAA-C03 exam tests.
- Open the VPC console at https://console.aws.amazon.com/vpc/.
- In the left navigation, click Route tables.
- Find the route table associated with your private subnets (it will be named something like ecs-secure-vpc-rtb-private or similar, depending on the name auto-generated by the VPC wizard).
- Click on it and select the Routes tab.
- Confirm there is a route with destination 0.0.0.0/0 and target pointing to a NAT Gateway (nat- prefix).
Task stuck in PROVISIONING or showing CannotPullContainerError?
If you do not see a 0.0.0.0/0 route to a NAT Gateway in the private route table, you likely selected "None" for NAT Gateways when creating the VPC in Step 1. Go back to the VPC console, create a NAT Gateway in one of your public subnets (allocate an Elastic IP), then add a 0.0.0.0/0 route to that NAT Gateway in the private subnet's route table.
Also confirm that your ecs-tasks-sg security group allows all outbound traffic (0.0.0.0/0 on all ports). Without outbound access, the task cannot reach ECR even with a NAT Gateway.
After fixing the route, return to the ECS console. Select your service and click Update, then check Force new deployment and click Update again. This restarts the task with the corrected networking.
help me troubleshoot my ECS task failing with CannotPullContainerError in a private subnet
- Once the route is confirmed (or fixed), return to the ECS console and navigate to your secure-app-service in secure-cluster.
- Click the Tasks tab and confirm the task shows status Running.
- Click on the running task ID to open its details.
- Scroll down to the Configuration section and look at the ENI details. You should see a Private IP address (something like 10.0.x.x) and no public IP.
This confirms your task is running in complete network isolation. It has no public IP, no inbound internet access, and its only outbound path is through the NAT Gateway.
Your task is running securely in a private subnet. Nobody on the internet can reach it directly. In the next step, you will put an Application Load Balancer in front of it so users can access your app without exposing the tasks.
Attach an Application Load Balancer
Your tasks are running in private subnets with no public IP. That is exactly the security posture you want. But it creates a new problem: nobody on the internet can reach your application.
The production-appropriate solution is an Application Load Balancer sitting in your public subnets. The ALB accepts inbound traffic from the internet and forwards it to your private tasks. It also runs health checks that automatically replace unhealthy tasks and enables path-based routing for microservices.
In this step, get ready to:
- Create an IP target group for your Fargate tasks.
- Set up an internet-facing Application Load Balancer in your public subnets.
- Recreate your ECS service with the ALB attached and verify healthy targets.
Create an IP target group
Before creating the ALB, you need a target group that tells the load balancer where to send traffic. Fargate tasks using awsvpc network mode require a target type of IP (not Instance). Each task gets its own private IP, and the ALB routes directly to those IPs.
- In the AWS console, open the EC2 service.
- In the left navigation pane, scroll to the Load Balancing section and select Target Groups.
- Click Create target group.
- Under Basic configuration, select IP addresses as the target type.
- Enter ecs-app-tg as the Target group name.
- Set Protocol to HTTP and Port to 80.
- Select ecs-secure-vpc from the VPC dropdown.
- Under Health checks, confirm the Health check path is set to /.
- Click Next.
- Skip registering targets (ECS will handle this automatically). Click Create target group.
Why IP target type?
With awsvpc network mode, each Fargate task gets its own Elastic Network Interface with a private IP address. The ALB routes traffic directly to those IPs. The Instance target type only works for EC2 instances, not for Fargate tasks.
You should see ecs-app-tg listed in the Target Groups table with no registered targets yet.
Create the Application Load Balancer
Now create the ALB itself. It sits in your public subnets and accepts traffic from the internet on port 80.
- In the left navigation pane, select Load Balancers.
- Click Create load balancer.
- Under Application Load Balancer, click Create.
- Enter ecs-app-alb as the Load balancer name.
- Set Scheme to Internet-facing.
- Under Network mapping, select ecs-secure-vpc as the VPC.
- Select both Availability Zones and choose the public subnet in each AZ.
- Under Security groups, remove the default security group and select alb-sg.
- Under Listeners and routing, confirm the protocol is HTTP on port 80.
- In the Default action dropdown, select ecs-app-tg (the target group you just created).
- Click Create load balancer.
The ALB takes a minute or two to provision. Wait until the State changes from Provisioning to Active.
Copy the DNS name now
Once the ALB is active, find the DNS name in the ALB's Description section. Copy it somewhere handy. You will use it to test your application in the final substep.
ALB stuck in Provisioning?
If the ALB stays in Provisioning for more than 5 minutes, confirm you selected the public subnets (not the private ones) in the network mapping. An ALB placed in private subnets with no internet gateway cannot finish provisioning.
help me troubleshoot my ALB stuck in provisioning state
Recreate the ECS service with the ALB attached
Here is an important AWS limitation: after you create an ECS service, its load balancer configuration cannot be changed from the AWS Management Console. Since your current service was created without an ALB, you need to delete it and create a new one.
Why delete and recreate?
The ECS console does not allow modifying load balancer settings on an existing service. This is a known constraint. The cleanest approach is to delete the service and create a fresh one with the ALB attached from the start.
- Navigate to the ECS console at https://console.aws.amazon.com/ecs/v2.
- Select Clusters, then click secure-cluster.
- Open the Services tab.
- Select your existing service and click Delete.
- Check Force delete service to stop running tasks immediately.
- Type delete in the confirmation prompt and click Delete.
Wait until the service disappears from the list. Now create the new service with the ALB attached.
- In the Services tab of secure-cluster, click Create.
- Under Compute options, select Launch type and choose FARGATE.
- Under Deployment configuration, select secure-app as the task definition Family.
- Set Desired tasks to 1.
- Expand the Networking section.
- Select the two private subnets from ecs-secure-vpc.
- Select ecs-tasks-sg as the security group (remove the default one).
- Set Public IP to Turned off.
Now attach the load balancer.
- Expand the Load balancing section.
- Select Application Load Balancer as the load balancer type.
- Under Application Load Balancer, choose Use an existing load balancer and select ecs-app-alb.
- Under Container, confirm the container name and port 80 are shown.
- Choose Use an existing target group and select ecs-app-tg.
- Click Create at the bottom of the page.
What happens next?
ECS launches a new Fargate task in your private subnets. It automatically registers the task's private IP with ecs-app-tg. The ALB starts sending health check requests to the task on port 80 at the / path.
If a task fails the health check, ECS stops it and launches a replacement. This loop continues until your service reaches the desired task count with all targets healthy.
Wait 1-2 minutes for the task to start and pass its health checks.
- In the ECS console, click into your new service and open the Health and metrics tab to confirm the task status is RUNNING.
- Switch to the EC2 console, navigate to Target Groups, and click ecs-app-tg.
- Open the Targets tab. You should see a registered target with status healthy.
Before you test, think about this: your task has no public IP, but the ALB is internet-facing in the public subnets. What do you expect to happen when you paste the ALB DNS name into your browser?
- Open a new browser tab and paste the ALB DNS name you copied earlier.
You should see your containerized application responding. The ALB received your request on its public IP and forwarded it to the task's private IP inside the private subnet.
Seeing a 503 error?
A 503 Service Unavailable means the ALB has no healthy targets to forward to. Check these common causes:
- The task might still be starting. Wait another minute and refresh.
- Confirm ecs-tasks-sg allows inbound traffic on port 80 from alb-sg. If the security group source is wrong, the health checks fail silently.
- In Target Groups > ecs-app-tg > Targets tab, check the Health status column for the specific failure reason.
help me troubleshoot a 503 error from my ALB
Your application is now accessible through the ALB, even though the tasks have no public IP addresses. Next up, you will configure auto scaling so the service adjusts its task count based on demand.
Configure Auto Scaling and CloudWatch Metrics
Your application is live behind the ALB, and traffic flows smoothly to a single task. But what happens when traffic spikes? That one task gets overwhelmed, response times climb, and requests start failing.
A fixed task count also wastes money during quiet periods. Target tracking auto scaling solves both problems by automatically adjusting your task count to maintain a CPU utilization target. In this step, you'll configure the policy and see how CloudWatch alarms drive the scaling decisions.
In this step, get ready to:
- Enable auto scaling on your ECS service with minimum and maximum task counts.
- Create a target tracking policy that scales based on CPU utilization.
- Verify the auto-created CloudWatch alarms and view live metrics for your service.
Enable auto scaling on the ECS service
Auto scaling needs boundaries. You set a minimum task count (so the service never drops below a safe baseline) and a maximum (so costs don't spiral during unexpected load).
- Return to the ECS console at https://console.aws.amazon.com/ecs/v2.
- On the Clusters page, select secure-cluster.
- In the Services section, select your service.
- Click Set the number of tasks.
- Under Amazon ECS service task count, select Use auto scaling.
- For Minimum number of tasks, enter 1.
- For Maximum, enter 4.
- Click Save.
What do minimum and maximum mean here?
The minimum is the floor. ECS will never scale below this number, even during zero traffic. The maximum is the ceiling. No matter how high CPU goes, ECS won't launch more tasks than this limit.
For a learning project, 1 to 4 keeps costs low while still demonstrating the scaling behaviour.
After saving, the policies page appears. This is where you'll create the actual scaling rule.
Create a target tracking scaling policy
A target tracking policy works like a thermostat. You set a target (50% CPU), and AWS automatically adds or removes tasks to stay near that number.
- Click Create scaling policy.
- For Scaling policy type, select Target tracking.
- For Policy name, enter cpu-target-50.
- For Metric type, select ECSServiceAverageCPUUtilization.
- For Target utilization, enter 50.
- Leave the Scale-in cooldown period and Scale-out cooldown period at the default of 300 seconds.
- Click Create scaling policy.
You should see the cpu-target-50 policy listed in the service's auto scaling configuration.
Why target tracking instead of step scaling?
Target tracking is a "set and forget" approach. You declare the desired CPU percentage, and AWS handles the math of how many tasks to add or remove. It scales out fast to protect availability and scales in more gradually to avoid flapping.
Step scaling gives you more control with explicit thresholds and custom adjustment sizes, but requires more configuration. The SAA-C03 exam tests when to use each. Target tracking fits most workloads. Step scaling fits scenarios where you need faster response or custom step sizes.
What about the cooldown periods?
The 300-second cooldown prevents scaling thrash. After a scale-out event, the policy waits 5 minutes before considering another scale-out. This gives new tasks time to absorb traffic and stabilise the metric before another decision is made.
Don't see the Create scaling policy button?
Make sure you clicked Save after enabling Use auto scaling and setting the min/max values. The policy creation page only appears after that initial save completes.
If you navigated away, return to your service, click Set the number of tasks again, and look for the Create scaling policy button on the resulting page.
Help me find the auto scaling configuration for my ECS service.
Verify CloudWatch alarms and metrics
When you create a target tracking policy, AWS automatically creates two CloudWatch alarms. One triggers scale-out (CPU above target) and the other triggers scale-in (CPU below target). You never create these manually.
Before you check CloudWatch, what do you think the current alarm states will be? With just one task running at low CPU, which alarm do you expect to be in the ALARM state?
- Open the CloudWatch console by searching for CloudWatch in the AWS search bar and selecting it.
- In the left navigation, click Alarms.
- Find the two alarms auto-created by your scaling policy. They contain your service name and policy name in their titles.
You should see one alarm in the ALARM state (the scale-in alarm, because CPU is well below 50%) and one in the OK state (the scale-out alarm). The service stays at 1 task because that is the configured minimum.
Why doesn't it scale in below 1?
Even though the scale-in alarm is active, ECS respects your minimum task count. The alarm fires, but the scaling action has nothing to do because you already have the minimum number of tasks running. This is the safety floor at work.
- In the left navigation, click Metrics, then All metrics.
- Under the AWS namespaces section, select ECS.
- Select ClusterName, ServiceName.
- Find secure-cluster and check the boxes for CPUUtilization and MemoryUtilization.
The graph displays live CPU and memory utilization for your service. You'll see CPU is well below 50%, confirming why the scale-in alarm is active. ECS publishes these metrics at 60-second resolution by default.
Don't edit or delete auto-created alarms
Target tracking creates and manages these CloudWatch alarms automatically. If you edit or delete them manually, the scaling policy breaks. They are cleaned up automatically when you delete the policy.
Your ECS service now scales automatically. The ALB distributes traffic across however many tasks are running, CloudWatch watches CPU utilization, and the target tracking policy adds or removes tasks to keep utilisation near 50%. This is the complete production pattern for resilient, cost-efficient containerised workloads on AWS.
Secret mission
Add a Second Service with Path-Based Routing
Real-world architectures run multiple microservices behind a single ALB, using path-based routing to direct traffic to different services. Can you add a second ECS service and wire up a listener rule so that /api/* goes to your new service while / continues hitting the original?
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 a NAT Gateway and an Application Load Balancer that incur hourly charges while running.
Cost warning
The NAT Gateway costs ~$0.045/hour and the ALB costs ~$0.0225/hour. Together with Fargate tasks, leaving everything running adds up to approximately $1.50/day.
If you are not actively working on this project, choose Pause or Delete below to avoid unexpected charges on your AWS bill.
Resources you used:
- NAT Gateway (in ecs-secure-vpc public subnet) with an associated Elastic IP.
- Application Load Balancer (ecs-app-alb) with two target groups (ecs-app-tg and ecs-api-tg).
- ECS cluster (secure-cluster) with two Fargate services (secure-app and api-service).
- VPC (ecs-secure-vpc) with 2 public subnets, 2 private subnets, Internet Gateway, and S3 Gateway endpoint.
- IAM role (ecsTaskS3ReadRole) with an inline S3 read policy.
- CloudWatch log group and auto-scaling alarms.
- S3 bucket (created for the task role demo).
Keep everything running
No action needed. Choose this if you are still actively building or plan to continue working on this project today.
- Your NAT Gateway, ALB, and Fargate tasks will continue running and incurring charges (~$1.50/day).
- Your ECS services will remain accessible via the ALB DNS name.
- Auto scaling will continue to monitor CPU utilization and adjust task count if needed.
Pause - I'll come back to this later
Shut down your ECS tasks to reduce costs while keeping the infrastructure in place. The NAT Gateway and ALB will still incur hourly charges even with zero tasks running.
- Open the ECS console at https://console.aws.amazon.com/ecs/v2.
- Select secure-cluster, then select the secure-app service.
- Click Update, set Desired tasks to 0, and click Update.
- Repeat for the api-service service (if you completed the Secret Mission).
- To resume later, update both services back to desired count 1.
What still costs money while paused?
The NAT Gateway (~$0.045/hour) and ALB (~$0.0225/hour) run regardless of task count. To stop all charges, you must fully delete the resources using the Delete tab.
Delete - I don't want to use this again
Remove all project resources and stop all charges. Follow these steps in order, because some resources depend on others.
Step 1: Delete the ECS services and cluster.
- Open the ECS console at https://console.aws.amazon.com/ecs/v2.
- Select secure-cluster.
- In the Services tab, select the secure-app service and click Delete. Type delete to confirm.
- Repeat for the api-service service (if you completed the Secret Mission).
- Wait for both services to finish draining (tasks stop).
- Click Delete Cluster. In the confirmation box, type delete secure-cluster and confirm.
Step 2: Delete the ALB and target groups.
- Open the EC2 console and navigate to Load Balancers in the left sidebar.
- Select ecs-app-alb, choose Actions, then Delete load balancer. Type confirm and delete.
- Navigate to Target Groups in the left sidebar.
- Select ecs-app-tg, choose Actions, then Delete. Confirm the deletion.
- Repeat for ecs-api-tg.
Step 3: Delete the NAT Gateway and release the Elastic IP.
- Open the VPC console at https://console.aws.amazon.com/vpc/.
- In the left sidebar, click NAT gateways.
- Select the NAT Gateway associated with ecs-secure-vpc, choose Actions, then Delete NAT gateway. Type delete to confirm.
- Wait a few minutes for the NAT Gateway to finish deleting.
- In the left sidebar, click Elastic IPs.
- Select the Elastic IP that was associated with the NAT Gateway (it will show as unassociated), choose Actions, then Release Elastic IP addresses. Confirm the release.
Step 4: Delete the VPC.
- In the VPC console left sidebar, click Your VPCs.
- Select ecs-secure-vpc, choose Actions, then Delete VPC.
- The console shows a summary of resources it will delete (subnets, route tables, Internet Gateway, security groups, S3 Gateway endpoint). Review the list, type delete, and confirm.
Step 5: Delete the IAM role.
- Open the IAM console at https://console.aws.amazon.com/iam/.
- In the left sidebar, click Roles.
- Search for ecsTaskS3ReadRole, select it, and click Delete. Type the role name to confirm.
- Keep ecsTaskExecutionRole if you plan to use ECS again. Otherwise, search for it and delete it the same way.
Step 6: Delete the S3 bucket and CloudWatch log group.
- Open the S3 console, select the bucket you created for the task role demo, click Empty to remove all objects, then click Delete to remove the bucket.
- Open the CloudWatch console, navigate to Log groups in the left sidebar.
- Select the log group created by your ECS task definition, choose Actions, then Delete log group(s). Confirm the deletion.
Nice Work!
Nice Work!
Nice work! You just built a production-grade ECS Fargate architecture that mirrors how real containerized applications run on AWS.
You've learned how to:
- Build a secure VPC with public and private subnets, a NAT Gateway for outbound access, and network segmentation that keeps your ECS tasks isolated from the internet.
- Deploy Fargate tasks in private subnets behind an Application Load Balancer with separate IAM roles for task execution and application-level access, demonstrating least-privilege security.
- Configure target tracking auto scaling that automatically adjusts task count based on CPU utilization, with CloudWatch alarms managing the scaling triggers.
- Secret Mission: implemented path-based routing with multiple microservices sharing a single ALB.
Ready to quiz yourself?