ECS Task Definitions and Fargate Services: Deploying KloudGov (Part 3)
Docker-and-ECR
ByOlaniyi Oladimeji
ECS Task Definitions and Fargate Services: Deploying KloudGov (Part 3)

Part 1 built two Docker images and pushed them to ECR. Part 2 provisioned the storage layer with Terraform and stood up an empty ECS cluster on Fargate. Neither image has actually run anywhere yet; this post is where that changes.

Like the EC2-based series deployed manually before introducing Ansible, this post deploys KloudGov to ECS entirely by hand: creating the task definition through the console, launching the service, and watching it come up behind a load balancer created on the fly. The goal is the same as it was back then: feel the actual steps involved before any of this gets automated in a future post.

Step 1: Creating the Task Definition

A task definition in ECS is the container equivalent of the systemd service file from the EC2 series; it describes exactly what should run, how much compute it gets, and what permissions it has. Unlike that systemd file, though, a single task definition describes two containers running together as one unit: the Flask application and its Nginx reverse proxy, mirroring how they were built as separate images in Part 1.

In the ECS console, go to Task definitions → Create new task definition:

Task definition family: kloudgov-fullstack
Infrastructure requirements: AWS Fargate
Task size: 1 vCPU / 3 GB Memory
Task role: KloudGovECSExecutionRole
Task execution role: KloudGovECSExecutionRole

That task size figure is worth understanding correctly: 1 vCPU and 3 GB are allocated to the entire task, not to each container individually. Both the app and nginx containers share that same budget. Fargate doesn’t provision separate compute per container within a task unless you explicitly cap each one below the task total.

On reusing one role for both fields: KloudGovECSExecutionRole was built in Part 1 with three policies: S3, DynamoDB, and AmazonECSTaskExecutionRolePolicy. Using it for both the task role (what the application code inside the container can do to talk to S3 and DynamoDB) and the task execution role (what ECS itself does on the task’s behalf: pull the image from ECR, write logs to CloudWatch) works correctly, since all the necessary permissions exist on this one role. It’s not the tightest possible setup, though a production configuration would typically split these into two separate roles: the execution role has only ECR/CloudWatch permissions, and the task role has only the S3/DynamoDB access the application needs. For this lab-scale deployment, one role covering both is a reasonable simplification, consistent with the broad FullAccess policies used elsewhere in this project.

Container 1: The Application

Name: app
Image URI: <YOUR_AWS_ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/kloudgov-app (pull the exact URI from the kloudgov-app repository in ECR; it’s a private repository from Part 1, using the standard account-scoped ECR URI format, not the public.ecr.aws gallery format)
Essential container: Yes
Container port: 8000
Port name: app-8000-tcp

Marking this container essential means ECS treats its exit as a signal that the whole task has failed. If the app container crashes, ECS stops the entire task (both containers) and, under the service configuration from Step 2, replaces it automatically.

Environment variables (replace with your actual values from Part 2’s Terraform output):

AWS_BUCKET = kloudgov-michigan-s3-****
AWS_DYNAMODB_TABLE = kloudgov-michigan-dynamodb
AWS_REGION = us-east-1
US_STATE = michigan

These are functionally identical to the environment variables baked into the systemd unit back in the EC2/Ansible series: same four values, same purpose, just injected through ECS’s task definition instead of a Jinja2-rendered config file.

Click Add container to move to the second one.

Container 2: The Web Server

Name: nginx
Image URI: <YOUR_AWS_ACCOUNT_ID>.dkr.ecr.us-east-1.amazonaws.com/kloudgov-nginx
Essential container: No
Container port: 80
Port name: nginx-80-tcp
App protocol: HTTP

Marking nginx as not essential is an interesting choice here, and it’s worth being explicit about what it actually controls. If this container stops, ECS does not automatically treat the whole task as failed the way it would for the app container. In practice, since Nginx is the only public entry point into this task, you’d still notice immediately if it went down, but the task itself wouldn’t be forcibly restarted purely because of that. This is a defensible choice for a lab, but worth reconsidering for production, where the reverse proxy going down is just as much a real outage as the application itself failing.

Click Create once both containers are configured.

How These Two Containers Actually Talk to Each Other

This is worth explaining directly, since it’s not obvious from the console alone. Fargate tasks use awsvpc network mode, which means every container in the same task shares a single network interface; they can reach each other over localhost, exactly like two processes on the same machine. That’s why Nginx’s config from Part 1 proxies to http://localhost:8000 rather than another container’s IP address: from Nginx’s perspective, the Flask app is running on localhost, just in a neighboring container sharing the same network namespace.

Step 2: Creating the Service

A task definition describes what to run; a service deploys it and keeps it running, exposing it to the outside world. From the task definition you just created, click Deploy → Create service:

Service name: kloudgov-svc
Existing cluster: kloudgov-ecs-cluster (the cluster created in Part 2; note this is the correct name to select, not a shortened variant)
Compute options / Launch type: FARGATE
Platform version: LATEST
Service type: Replica
Desired tasks: 2

The desired tasks: 2 is doing real work here; it’s the mechanism providing high availability. Rather than one task handling all traffic (and becoming a single point of failure), ECS keeps two identical task instances running at all times, across different underlying infrastructure. If one fails a health check or crashes, ECS replaces it automatically while the other continues serving traffic. Worth remembering this doubles the Fargate cost flagged back in Part 2: two tasks running the same 1 vCPU/3 GB allocation, not one.

Networking
VPC: Default
Security Group: Default

The “Default” security group in most AWS accounts doesn’t allow any inbound traffic by default. Before this service will actually be reachable, you need to add an inbound rule permitting HTTP traffic explicitly:

Type: HTTP
Port: 80
Source: 0.0.0.0/0

Without this, the load balancer in the next section will have no way to reach the Nginx container, regardless of how correctly everything else is configured.

Load Balancing
Check Use load balancing
Load balancer type: Application Load Balancer
Load balancer name: kloudgov-lb
Container to receive traffic: select the nginx container (the frontend/proxy), not the app. The ALB should forward traffic to the reverse proxy, which then handles routing internally to the Flask app over localhost, exactly as described above.
Target group name: kloudgov-tg

This is a meaningful departure from how the earlier EC2-based series handled load balancing there; the ALB was defined explicitly in Terraform (Part 4 of that series). Here, it’s created directly through the ECS console as part of standing up the service, entirely by hand, matching this post’s overall theme of manual-first before automation.

Click Create. This step takes a genuine while to complete. Fargate needs to provision the underlying infrastructure for both tasks, register them with the target group, and pass initial health checks before the service reports as stable.

Step 3: Validating the ECS Components

Once the service finishes deploying, confirm each layer independently:

Cluster shows as running, with the service active inside it.
Two tasks running, providing the high availability configured in Step 2. Check EC2 → Load Balancing → Target Groups to see both tasks registered as targets, and check EC2 → Instances to confirm there genuinely are no EC2 instances running. This is the clearest possible confirmation that Fargate is doing exactly what it’s supposed to: running containers without any underlying server for you to manage.


Infrastructure: the two tasks are placed in different Availability Zones automatically, which is what makes “high availability” meaningful here; a zone-level outage wouldn’t take down both tasks simultaneously.
Service → Networking: note the DNS Name listed here. This is the load balancer’s public address, and it’s what you’ll actually use to reach the application in the next step.

Step 4: Testing the Application

Open the DNS name from Step 3 in a browser, and confirm the application is genuinely functional end-to-end- not just reachable, but actually reading and writing to the DynamoDB table and S3 bucket provisioned back in Part 2:

First Name: Olaniyi
Last Name: Oladimeji
Employee Role: DevOps Engineer
Annual Salary (USD): 145000
Scanned ID (PDF): upload a sample file, such as a driver’s license scan

Successfully submitting this confirms that the full path is working: ALB → Nginx container → Flask app container (over localhost) → DynamoDB (for the record) → S3 (for the uploaded file), genuinely proving the containerized architecture end-to-end, not just that a page loads.

Step 5: Destroying All Resources

Given Fargate’s real, no-free-tier cost, teardown matters more here than anywhere else in this project. Work through it in this specific order:

1. Delete the service first:

ECS → select the cluster → Service → Delete, choosing Force delete (since the service has running tasks, a standard delete will refuse until those are stopped; force delete handles both in one step).

2. Then deregister and delete the task definition:

Task Definitions → select kloudgov-fullstack → Actions → Deregister
Filter by status: Inactive
Select it again → Actions → Delete

3. Delete the cluster itself.

4. Delete the ECR repositories, both kloudgov-app and kloudgov-nginx, since these otherwise persist indefinitely (image storage in ECR isn’t free either, though it’s far cheaper than Fargate compute).

5. Remove the PDF manually from the S3 bucket. This connects directly to the known bug from the EC2 series: deleting an employee record removes the DynamoDB entry but leaves the uploaded file behind in S3. If you don’t clear this manually, the next step will fail.

6. Destroy the S3 bucket and DynamoDB table via Terraform:

cd /home/ec2-user/kloud-gov-infrastructure/terraform

terraform destroy -auto-approve

If you skipped Step 5 and the bucket still contains the uploaded PDF, this will fail with the same “bucket not empty” error seen elsewhere in this project. Go back and clear the object manually, then re-run.

Close the remote connection and stop the EC2/IDE instance once everything is confirmed torn down.

What This Three-Part Series Actually Demonstrated

Looking back across all three posts: Part 1 packaged the application into two portable, versioned container images. Part 2 adapted an existing Terraform module to separate storage concerns from compute concerns entirely, and stood up a cluster with no servers to manage. Part 3 took those two images and, through nothing but console clicks, turned them into a highly available, load-balanced, auto-healing service with genuinely zero EC2 instances involved anywhere in the running application.

That last point is worth sitting with. Compare this to the EC2-based series: no SSH sessions, no systemd units, no Nginx installed by hand, no OS patches to think about. The tradeoff is real too: less direct control, a task definition to understand instead of a server to inspect, and a cost model that starts the moment a task launches rather than when you choose to provision one. Neither approach is strictly better; they’re genuinely different ways of trading control for operational simplicity, and now you’ve built both, hands-on, well enough to know which one actually fits a given problem.

Key Takeaways

A task definition’s CPU/memory allocation is shared across every container in the task, not assigned per container individually.
essential: true on a container means its failure takes down the whole task; essential: false means it can fail without triggering an automatic task replacement. Choose this deliberately, not by default.
Containers within the same Fargate task communicate over localhost. Since awsvpc network mode gives every container in a task a shared network interface, this is exactly why Nginx’s config points at localhost:8000 rather than another container’s address.
The default security group typically blocks all inbound traffic; “allow port 80” isn’t automatic and needs an explicit inbound rule before the ALB can reach anything.
Reusing one IAM role for both the task role and task execution role works when that role’s permissions happen to cover both use cases, but splitting them is the more precise, least-privilege pattern for anything beyond a lab.
Desired task count directly multiplies Fargate cost; two tasks for high availability means genuinely double the compute cost of one, not a free reliability upgrade.
Teardown order matters with ECS: service before task definition, task definition before cluster, and don’t forget ECR repositories and any orphaned S3 objects, which persist (and cost money) independently of the compute layer.

With this series complete, KloudGov now exists in two fully working, hands-on-validated forms: EC2 with Ansible automation, and ECS with Fargate, a genuinely useful comparison for understanding when each approach actually fits.

{{ reviewsTotal }}{{ options.labels.singularReviewCountLabel }}
{{ reviewsTotal }}{{ options.labels.pluralReviewCountLabel }}
{{ options.labels.noReviewsLabel }}
{{ options.labels.newReviewButton }}
{{ userData.canReview.message }}

Related Posts

EKS-KloudGov-Cluster
AWS EKS Cluster Setup: Provisioning Kubernetes with eksctl and Terraform (Part 1)
This new series adds a third architecture: Kubernetes, via Amazon EKS (Elastic Kubernetes Service), fronted...
Docker-and-ECR
Terraform ECS Automation: Fully Automated, HTTPS-Secured Fargate Services (Part 4)
Everything built by hand in Part 3 the task definition, the target group, the ALB,...
Docker-and-ECR
Terraform for ECS: Provisioning S3, DynamoDB, and Fargate Cluster (Part 2)
The interesting part of this post isn't creating new resources from scratch; it's adapting the...