Most deployment advice starts with architecture. Draw the boxes, pick the services, ship it. I believed that was the whole job until a client project showed me otherwise.
Two projects can have the same architecture diagram and still need very different deployments. What decides it is the features of the app and the shape of its traffic. This post is about the project that made that clear to me, and how I ended up on EC2 with Ansible instead of the more fashionable options.
The client
I took over a project and moved it onto EC2 first. The brief was short: keep it secure, and don't let it fall over when traffic jumps.
Traffic was not large, about 3K users a day. What made it interesting was how it moved:
| When | Traffic | What it meant |
|---|---|---|
| Normal day | About 3K users | Two modest servers are enough |
| Product releases/events | Double or more, in bursts | Extra capacity for a few hours, not all year |
| December holidays | A different pattern again | Don't pay for peak capacity every month |
The important detail is that I knew these dates in advance. Fixed servers would have been either oversized all year or too small on release day.
Which setup for which situation
Before the details, here is the short version. Find the row closest to your project.
| Your project looks like | Go for | Trade-off |
|---|---|---|
| Steady traffic, small team, spikes on known dates | EC2 + Auto Scaling + ALB | Cheapest at steady load, but you patch the servers |
| Sudden, unpredictable spikes | ECS on Fargate | Nothing to patch, but a higher price per unit |
| Small event-driven jobs (resize an image, send an email) | Lambda | Pay only when it runs, but 15 minute limit and DB connection pressure |
| WebSockets or long-lived connections | Fargate or EC2 | Connections stay open, a poor fit for Lambda |
| Heavy 24/7 load | ECS on EC2 + Savings Plans | Best price at constant load, but two layers to scale |
| A team already living in Kubernetes | EKS | Reuses existing skills, big operational cost |
ECS and Fargate are sometimes presented as rivals. They are not: ECS is the orchestrator, and Fargate is one way for ECS to run your containers.
The app has to be ready first
Before anyone touches a server, a few things must be true in the code. I have seen more "infrastructure problems" caused by these than by AWS itself.
- The app is stateless. If sessions or files live on one machine's disk, you cannot run two copies. Sessions go in JWT or Redis, uploads go to S3.
-
There is a
/healthendpoint. The load balancer and Auto Scaling both rely on it to decide whether an instance is alive. - I know my database connection limit. Every new instance brings its own connection pool. Ten instances with a pool of 20 is 200 connections, and the database may not allow that.
- Cron jobs run in one place. Otherwise they fire on every copy of the API at the same time.
None of this is infrastructure work. It is plain application code, and it decides what you can do afterwards.
The options, judged for this client
| Option | In its favour | Against it | For this client |
|---|---|---|---|
| EC2 + Auto Scaling + ALB | Cheapest at steady load, full control, my Ansible roles work as they are | I patch the OS, new instances take minutes to boot, I tune the scaling | My pick |
| ECS on Fargate | Nothing to patch, scales per task | Roughly 15 to 40% more per unit than raw EC2, no host access, no GPU | Right if nobody wants to own servers |
| Lambda | Near-zero cost when idle, instant scaling | 15 minute limit, DB connection pressure, awkward for WebSockets | Side jobs only |
| EKS | Huge ecosystem | Steep learning curve and running cost | Overkill |
I chose EC2 with Auto Scaling for three reasons. The traffic is small, I know the spike dates, and I already had Ansible roles that configure the servers. Scheduled scaling covers the known spikes without paying a Fargate premium all year. If nobody on the team wanted to own patching, I would flip that decision and use Fargate without a second thought.
Autoscaling: what scales on what
This is the part where being a developer helped most, because several of the real problems live in the app and not in AWS.
Minimum capacity. Two instances across two availability zones. If one dies, the site stays up.
The scaling rule. I use target tracking, either CPU around 60% or ALB requests per target. Requests per target follows actual traffic, so there are no thresholds to guess.
Known events. A scheduled action raises the minimum before a release or before December. A new instance takes minutes to become useful, so waiting for the spike to start is already too late.
Boot time. I bake an AMI after the Ansible role runs. New servers start ready instead of installing everything at boot.
Health checks. Use the ELB health check type, pointed at /health, so a broken instance is replaced automatically.
The database. Compare the maximum connections against instances multiplied by pool size. This is the most common surprise when a Node app scales out.
One Node-specific trap is worth knowing. Node runs on a single core per process. On a 2-vCPU instance running one Node process, CPU may never go much past 50%, so a CPU-based rule never fires. Run one process per core (PM2 cluster mode works) or scale on requests per target instead.
Before any release day, load test staging with k6 or Artillery. It tells you more than any estimate.
Security: EC2 versus Fargate
| Concern | EC2 | Fargate |
|---|---|---|
| OS patching | Me | AWS |
| SSH access | I have to lock it down | None (ECS Exec if you need a shell) |
| Isolation | I manage the shared host | Each task is isolated |
| IAM | Instance role | Task role per service |
| Host-level security agents | Possible | Not possible |
If compliance requires an agent on the host, that points to EC2. Otherwise Fargate gives you a smaller attack surface for less work.
On EC2 my Ansible hardening role covers the basics: key-only SSH with root login disabled and no public SSH (Session Manager or a locked-down IP), servers and database in private subnets with security groups chained from ALB to app to DB, secrets kept in Ansible Vault or Secrets Manager and never in the repo, and automated RDS backups together with a restore I have actually tested.
Ansible and Terraform: who does what
I work with Ansible, and this setup lives in it.
- Roles configure the servers: packages, users, SSH hardening, nginx, Docker.
- A playbook deploys a new version one server at a time, with a health check between each.
- Ansible Vault holds the deploy-time secrets.
-
A dynamic inventory (
aws_ec2) finds the instances that Auto Scaling created, by tag. Instances come and go, so a hard-coded host list breaks quickly. - The same roles bake the AMI.
Before changing anything, I preview it:
ansible-playbook -i inventory/prod.aws_ec2.yml site.yml --check --diff
And this is a trimmed version of the deploy playbook:
- hosts: web
become: true
serial: 1 # one server at a time, the ALB keeps traffic on healthy ones
vars_files:
- secrets.yml # encrypted with ansible-vault
tasks:
- name: Run the new version of the API
community.docker.docker_container:
name: api
image: "{{ app_image }}:{{ app_version }}"
state: started
recreate: true
restart_policy: always
published_ports: ["3000:3000"]
env:
DB_HOST: "{{ db_host }}"
DB_PASSWORD: "{{ db_password }}"
- name: Wait until /health answers
ansible.builtin.uri:
url: http://localhost:3000/health
status_code: 200
register: health
retries: 10
delay: 3
until: health.status == 200
I don't use Terraform on this project, but it is worth saying where it fits. It is strong at creating and tracking cloud resources such as the VPC, ALB, RDS and IAM, with state you can review. A team that rebuilds environments often may prefer it for that layer and keep Ansible for what runs inside the servers. The two are not rivals.
When I would choose differently
- If nobody wants to patch servers, or traffic becomes unpredictable, I would move to Fargate.
- If the load turns into a heavy 24/7 one, I would look at ECS on EC2 with Savings Plans.
- If the team grows into Kubernetes, EKS starts to make sense.
- For small side tasks like image resizing or emails, I would add Lambda next to the main app.
What I took from it
The server was never the real decision. The traffic shape, the app being ready to scale, and a setup I can repeat with one command were. Pick the simplest option that survives your busiest day, keep it in code, and load test it before that day arrives.
If you have deployed for a client with spiky traffic, I would like to hear what you chose and what you would change.
Top comments (0)