· 16 min read

Why You Should Deploy Services with ECS

This article was auto-translated from Chinese. Some nuances may be lost in translation.

I really dislike using AWS in small teams, but if you’re unfortunate enough to have to host web services on AWS, I strongly recommend using ECS.

As for why it’s unfortunate, let’s talk about that next time 🥲.

What Is ECS?

ECS (Elastic Container Service) is AWS’s container orchestration service, responsible for managing the lifecycle of your Docker containers. Before diving in, here are a few core concepts:

Cluster

A logical grouping of compute resources. You can think of it like a data center where all your containers run. The cluster itself is free; what costs money are the compute resources running inside it.

Task Definition

The blueprint or specification for your containers. It defines which Docker image to use, how much CPU and memory to allocate, environment variables, port mappings, log configurations, and more. Each modification generates a new revision, making tracking and rollbacks straightforward.

Task

The actual running container instance instantiated from a Task Definition. A single Task Definition can run multiple Tasks simultaneously. They can be long-running or one-off tasks.

Service

A higher-level abstraction that manages Tasks. It ensures that a specified number of Tasks remain running continuously, automatically spinning up a new one if a Task crashes. It also handles integration with ALBs, networking, and Security Groups to route traffic to healthy Tasks.

Fargate

AWS’s serverless compute engine. With Fargate, you don’t need to manage EC2 instances yourself—you simply tell AWS how much CPU and memory your container requires, and AWS handles the underlying machines. The alternative is the EC2 launch type, where you manage your own instances. That gives you more flexibility, but also a significantly heavier operational burden.

Put simply, the relationship is: a Cluster contains multiple Services; each Service runs multiple Tasks based on a Task Definition; and those Tasks actually run on either Fargate or EC2.

If You Have to Use AWS, Go Straight to ECS

Looking at the list above, your immediate reaction might be that spinning up an EC2 instance directly is much simpler.

On the surface, yes. But managing an EC2 instance is like caring for a pet: it gets sick, it ages, and it requires constant attention (the following are all true disaster stories 😢):

  • OS security updates: Kernel patches, glibc vulnerabilities, OpenSSL updates… If you don’t update, you’re running exposed; if you do update, it might break your application.
  • Memory management: Set too little swap, and you get OOM-killed; set too much, and performance crawls. When an OOM happens, you might not even be able to SSH in, leaving you staring helplessly at the console.
  • Log rotation: Forget to configure it, and six months later the disk fills up, immediately crashing the service.
  • Process management: When your application crashes, what restarts it? systemd? pm2? Every single one requires extra setup.
  • SSH key management: Who has access? Did anyone delete keys when an employee left? (Better not know the answer.)
  • Deployment method: rsync? scp? Custom deploy scripts? Praying every time you deploy.
  • Environment drift: An EC2 instance that has been running for six months looks nothing like a fresh one. Someone installed something manually or changed a config, but nobody remembers what, and nobody dares rebuild it.

The container philosophy is the exact opposite: every deployment starts from a clean image, environments don’t drift, and if something breaks, you just tear it down and start fresh.

Some might argue that EC2 can also prevent drift using Launch Templates or AMIs, or maintain identical state across instances using Ansible. In practice, however, you have to consider:

  • ECS inherently starts from a Docker image, and the Dockerfile typically lives in the same repository as the application code. It’s easy to track and can be debugged locally. Ansible, on the other hand, is usually managed separately, adding cognitive overhead for developers.
  • Managing AMIs and Launch Templates means introducing yet another tool just for AMIs, such as Packer, which is overkill for many applications.

Based on my own experience and that of peers around me (over the past three years, across four close friends), virtually every larger company has migrated to containerization.

Configured properly, it can even be cheaper than VPS instances. Containerization also offers huge operational advantages, such as environment consistency, deployment reproducibility, resource utilization, and horizontal scalability.

Next comes the choice between ECS and EKS.

The Barrier to Entry for EKS Is Higher Than You Think

EKS is managed Kubernetes on AWS.

It sounds great, but Kubernetes itself is notoriously complex: Pods, Deployments, Services, Ingresses, ConfigMaps, Secrets, Namespaces, Helm charts, and various CRDs. EKS only manages the control plane for you; everything else is still on your shoulders.

If your team has dedicated SREs and already uses Kubernetes, EKS is a sensible choice. But for most teams of 3 to 10 people, it’s bringing a sledgehammer to crack a nut. ECS is far simpler, and it holds several major advantages over EC2.

Don’t Want to Manage Servers? Use Fargate

Pair ECS with Fargate, and all you need to define is the image your container runs and how much CPU and memory it needs. AWS handles the underlying machines. None of the headaches mentioned earlier—OS patches, disk monitoring, SSH key management—apply. You pay for the actual container runtime, not an idle instance.

Built-in Blue/Green Deployments

The concept of Blue/Green deployment is straightforward: the version currently running in production is “Blue.” When a new version is deployed, it runs on another set of containers called “Green.” Both sets exist simultaneously, but traffic is still directed to Blue. You can verify Green via a test listener (e.g., exposing port 8080) to ensure everything works before routing traffic over.

Compared to traditional rolling updates, the biggest benefits of Blue/Green are:

  • Test before switching: The new version is already running in the production environment, against the same database and environment variables. You can verify functionality via the test listener rather than discovering breakages after deployment.
  • Gradual traffic shifting: CodeDeploy supports Canary and Linear strategies. For example, you can shift 10% of traffic, observe for 5 minutes, and only switch the rest if everything looks good, instead of going all-in at once.
  • One-click rollback if disaster strikes: Green blew up? Just click a button in the CodeDeploy console to roll back to Blue. No git revert, no rerunning pipelines, and no manual task definition edits.

Contrast this with rollbacks on EC2: SSH in, manually revert to the old version, restart the process, and pray. Blue/Green offers a vastly superior experience here.

For the core philosophy of blue/green deployments, check out this article by my talented former colleague Henry on how he implemented blue/green deployments at Hahow. Even though it’s been 9 years, the concepts remain just as applicable.

Docker Image Management Integrated with ECR

ECS and ECR are natively integrated. You specify the ECR image URI directly in the Task Definition, configure the IAM permissions, and you’re good to pull.

An essential rule: never use the latest tag. Use the commit hash as the tag for every build. That way, you always know exactly which version of the code is running in production, making it traceable when incidents occur. ECR also supports tag immutability, preventing anyone from overwriting existing tags by design.

You can configure an ECR Lifecycle Policy to retain Docker images according to set rules, automatically purging unused or expired images.

From a security perspective, development and production environments should be completely isolated. In AWS, this is typically done using different accounts, meaning ECR should also be separated by environment.

However, this means pushing the same Docker image to different ECR registries—a deliberate trade-off made for security. Personally, I prefer sharing a single ECR registry across environments. There is no single right answer; it depends on your team’s current goals.

Auto Scaling

ECS Services natively support auto scaling. They can automatically scale the number of tasks based on CPU utilization, memory, or ALB request count, or even custom metrics like SQS queue depth.

The setup isn’t complicated—just define a target tracking policy.

EC2 also has Auto Scaling Groups, but you still have to maintain AMIs, launch templates, and ensure that newly spawned instances are consistent with existing ones. Scaling with ECS + Fargate simply means running a few more containers—clean and straightforward.

Note that auto scaling is suspended during Blue/Green deployments. You can handle this by scripting scaling policy adjustments before and after the pipeline.

Logging and Monitoring

ECS natively supports shipping container stdout/stderr to CloudWatch Logs; just configure the awslogs log driver in the Task Definition. There’s no need to install CloudWatch Agents on EC2, configure log groups, or deal with log rotation. When a container is killed and restarted, the logs are still safe in CloudWatch.

Paired with CloudWatch Container Insights, you can view CPU, memory, and network utilization for every service and task out of the box, without having to install node exporters or set up Prometheus yourself.

Security Considerations

ECS + Fargate has an often-overlooked advantage when it comes to security: no SSH.

It sounds like a limitation, but it’s actually a great thing. No SSH means nobody can “temporarily” log in to tweak things, nobody can sneakily install software, and nobody can forget to log out. All changes must go through Task Definitions and CI/CD pipelines, making it immutable infrastructure by default.

If you do need to debug, you can use ECS Exec (backed by SSM Session Manager) to access the container, and every session comes with an audit trail.

Quick Summary

ECS isn’t an exciting technology choice, but it’s practical, stable, and has a reasonable learning curve. In the AWS world, the boring choice is often the best choice (and sometimes the pricier one).

Design Your Workflow Around Deployment First

If you asked me what the single most important thing is when building container services on AWS, I’d say: nail deployment first. Figure out how code travels from git push to running in production before anything else.

In ECS, deployment refers to packaging the latest code into a Docker image, updating the Task Definition, and triggering the deployment process.

Deployment workflows for testing and production should be considered separately based on their goals:

  • Test environment: Prioritize agility; minimize the time and complexity between code merge and environment deployment.
  • Staging environment: Mirror the production workflow as closely as possible to ensure any errors are caught before release.
  • Production environment: Strictly isolated from testing environments to prevent developers from directly touching production.

Deployment is the part of the development lifecycle with the longest-lasting impact. Once networking is configured, you rarely touch it; monitoring can be added gradually later. But deployment is something you encounter every single day, with every PR. A poor deployment experience drags down the entire team’s engineering velocity.

In the AWS ecosystem, deployments typically follow the CodePipeline + CodeBuild + CodeDeploy path. If you’re concerned about vendor lock-in, you can adjust parts of the pipeline.

For instance, if you use GitHub Actions, AWS provides official Actions you can integrate with. But regardless of the path you choose, you still need to handle:

  1. Building the image and pushing it to ECR
  2. Updating the image URI in the Task Definition
  3. Updating the ECS Service to trigger deployment
  4. Waiting for the service to stabilize

For production environments, you might also need Blue/Green deployments (via CodeDeploy), manual approval gates, and cross-account ECR image syncing. Every added layer is another opportunity for things to go wrong.

Adopt IaC as Early as Possible

If you configure these AWS resources purely by clicking around in the Console, you’ll live in constant fear.

One slip of the finger on a Security Group inbound rule, or an IAM Policy modified “temporarily” and forgotten, could cost you an entire day to debug. Not to mention, when it’s time to build a second environment (staging), you’ll realize you have no recollection of how production was configured.

Managing AWS resources with Terraform should be a day-one decision, not something you “tidy up later when there’s time.”

This also highlights a trade-off when balancing deployment ease with IaC. The core philosophy of Terraform is using declarative configurations to bring infrastructure to a desired state. In practice, however, an ECS deployment typically only involves:

  • Declaring a new Task Definition applied with the Docker image tag being deployed
  • Updating the Service and triggering deployment

Running Terraform for every deployment is rather cumbersome since the underlying infrastructure rarely changes. Here’s the setup I commonly use.

Terraform’s lifecycle block lets you ignore changes to the Task Definition’s image URI. This allows Terraform to manage infrastructure while your CI/CD pipeline manages application deployments, without stepping on each other’s toes. In my opinion, this is one of the most practical tips for ECS deployments:

resource "aws_ecs_service" "app" {
  # ...
  lifecycle {
    ignore_changes = [task_definition]
  }
}

Without IaC, your infrastructure is a black box that only the person who initially set it up understands—and that person has usually already left the company.

How Deployment Complexity Eats Away at Your Money

Many teams think “a slower deployment doesn’t matter” or “it’s just a few extra steps,” but this seemingly minor friction adds up to quantifiable costs.

  • Slow deployments
  • Too many steps per deployment
  • Manual steps increasing error rates
  • Increased cognitive load for developers
  • Fear of making substantial changes
  • Paving the way for larger mistakes

Quantifying these hidden costs:

Time to Production

Assume a 5-person engineering team, where each engineer costs around NT2Mperyear(includingbenefitsandequipment),whichworksouttoanhourlyrateofroughlyNT2M per year (including benefits and equipment), which works out to an hourly rate of roughly NT1,000.

MetricSimple DeploymentComplex Deployment
Deployment Duration10 mins45 mins
Deployments / Week155
Failure Rate3%15%
Recovery Time (MTTR)15 mins2 hours
Total Time Spent on Deploys / Week~3 hours~8 hours

On deployment alone, a complex setup consumes 5 more hours per week than a simple one. For a 5-person team, that wastes around 25 hours a week, adding up to 1,300 hours a year—equivalent to approximately NT$1.3M.

The Hidden Cost of Bugs

Another hidden cost of difficult deployments is a higher bug rate. Suppose each failed change requires an additional 8 hours of investigation and remediation:

MetricHigh-Frequency DeploymentLow-Frequency Deployment
Deployments / Month6020
Change Failure Rate5%30%
Failures / Month36
Cost per Fix (Person-Hours)8 hours16 hours (issues are usually more complex)
Total Monthly Cost to Fix24 hours96 hours

Low-frequency deployments demand an extra 72 hours a month just fixing bugs. Over a year, that’s 864 hours, or roughly NT$860K.

And that doesn’t even factor in:

  • Opportunity cost: Time engineers spend debugging could have been spent shipping new features.
  • User churn: Bugs hitting production degrade user experience; the resulting drop in MAU is hard to quantify, but very real.
  • Psychological toll: A team where every deployment feels like defusing a bomb cannot maintain high morale.

The Vicious Cycle

Leadership isn’t technically savvy and feels like the engineering team is constantly messing up.

When the team asks to improve deployment pipelines, it rarely has an immediate, visible impact on the product, so leadership tends to postpone it. Because the deployment process never improves, the on-the-ground developers bear the brunt of the cost, trust keeps eroding, and leadership stops believing in the engineering team’s proposals.

The cost of this vicious cycle is far higher than most realize:

  • The compounding cost of delayed improvements: Earlier, we calculated the hidden cost of complex deployments to be around NT2.25Mannually.Delayingimprovementsbyoneyearvaporizesthatsum;delayingbytwoyearsmeanslosingNT2.25M annually. Delaying improvements by one year vaporizes that sum; delaying by two years means losing NT4.5M.
  • Turnover costs: The most direct consequence of low morale is attrition. The replacement cost for an engineer is roughly 50%–150% of their annual salary (recruiting, interviewing, onboarding, and ramp-up time). Based on an annual cost of NT2M,replacingonepersoncostsaroundNT2M, replacing one person costs around NT1M–3M. If a 5-person team loses just one extra engineer each year due to poor deployment experience, that’s an added cost of NT$1M–3M/year.
  • Decision costs of a trust deficit: When leadership distrusts the tech team, they reject technical proposals, causing technical debt to compound. If one improvement proposal is blocked per quarter—each capable of saving NT500K/yearblockingfourproposalsayearleaves NT500K/year—blocking four proposals a year leaves **~NT2M** on the table.
ItemAnnual Cost
Ongoing hidden deployment costsNT$2.25M
Additional employee turnoverNT$1M–3M
Opportunity cost of delayed improvements~NT$2M
Total~NT$5.25M–7.25M/year

That amounts to the annual compensation of roughly 2 to 3 engineers, and it worsens over time—the longer you wait to improve, the higher the accumulated cost, the fewer people stick around, and the harder it becomes to break the cycle.

Summary

Conservatively speaking, the hidden annual cost of a cumbersome deployment workflow for a 5-person team sits around NT$2M–2.5M. And that doesn’t even count the few thousand dollars NAT Gateways quietly burn every month, the cost of idle resources, or the hours you spend trying to decipher your AWS bill.

That’s roughly equivalent to the annual salary of a junior engineer.

Your deployment pipeline is worth investing in from day one. The sooner you get it right, the more the savings compound with every single day and every single PR.

Conclusion

If your team is already locked into AWS, ECS is currently my top recommendation for container services. It’s far less headache-inducing than EC2 and much more pragmatic than EKS. Paired with Fargate, your operational burden drops dramatically.

Yet ECS itself is only one piece of the puzzle. In a real-world setup, you still have to tackle VPC network planning, ALB traffic routing, IAM permission management, ECR image governance, and CloudWatch monitoring configurations.

These components are tightly intertwined; a single misconfiguration can send you down a multi-hour debugging rabbit hole. Choosing ECS is just the starting line—it’s only complete once the surrounding infrastructure is thoroughly architected alongside it.

I’ve recently encountered similar requirements across several projects, so this post serves as a synthesis of my thoughts. If this post gets feedback from readers, I’ll write a follow-up detailing how to design practical ECS deployment workflows and architectures. (Though let’s be honest, I’ll probably write it anyway even if no one replies 😂).

Or maybe… you might not need AWS at all?

Related Posts

Explore Other Topics