The previous lesson containerised MercadoFresco's shop and got it running on an ECS cluster. The result was good but incomplete: underneath the cluster there are still EC2 instances that have to be sized, whose AMI has to be maintained, that are paid for whether full or empty, and that take two minutes to start when the cluster needs new capacity. We hid the problem behind a capacity provider; we did not remove it.

AWS Fargate removes it. It is the serverless compute engine for containers: you declare how much CPU and how much memory each task needs, and AWS provides the rest — the host, the kernel, the patches, the isolation — with no instance that anybody can see or list. This lesson migrates the shop service to Fargate, tunes its auto scaling to the Friday peak, moves the queue workers onto Fargate Spot and ends with an honest comparison between Fargate, EC2 and Lambda.

Cost warning. Fargate is billed by vCPU-hour and GB-hour consumed by the task, rounded to the second with a one-minute minimum. In eu-west-1 it comes to around 0.047 USD per vCPU-hour and 0.0051 USD per GB-hour on x86, and roughly 20 % less on arm64/Graviton. Fargate Spot discounts around 70 % in exchange for being interruptible. There is no free tier for Fargate: a forgotten task of 1 vCPU and 2 GB costs about 41 USD a month. Follow the cleanup section at the end. Prices are approximate and indicative; the in-depth cost analysis is module 11. Fictitious data and identifiers.

Contents

  1. What Fargate is and what stops existing
  2. The same service on EC2 and on Fargate
  3. Resource model: CPU, memory and ephemeral storage
  4. x86 and arm64: Graviton and the real saving
  5. Operational differences: no SSH, no hostPort, no DaemonSet
  6. ECS Exec: debugging with no server to touch
  7. Logging with FireLens and Fluent Bit
  8. Task start-up and the Friday peak
  9. Migrating the shop service to Fargate
  10. VPC endpoints: no longer depending on the NAT
  11. Auto scaling: target tracking, steps and schedules
  12. Fargate Spot and the queue workers
  13. One-off and scheduled tasks with EventBridge Scheduler
  14. Pipeline integration and blue/green
  15. The service in CDK with an L3 construct
  16. Fargate versus EC2 versus Lambda
  17. App Runner: one more step up the management ladder
  18. Cost comparison and cleanup
  19. Common mistakes and tips
  20. Exercises
  21. Conclusion

What Fargate is and what stops existing

Fargate is not a service you buy separately, nor a different console: it is a launch type — more precisely, a capacity provider — within the same Amazon ECS from the previous lesson. Task definitions, services, target groups, auto scaling and roles are the same objects. What changes is where the task runs and who is responsible for the machine.

What stops existing What it used to mean What it implies now
The AMI An operating system image to maintain and update Only your container image exists
Host patching Monthly window, staggered reboots, kernel CVEs AWS patches the host without you noticing
Cluster scaling An ASG, a capacity provider, targetCapacity There is no fleet of machines to scale
Task packing binpack and spread strategies, ENI limits, gaps that never fit Each task is its own unit
Idle capacity Instances running at four in the morning on a Tuesday You pay for the task, not for the gap
Host access SSH, docker exec, host volumes ECS Exec; see below
What is still yours Why it matters
The image Its size, its dependencies and its vulnerabilities remain your responsibility
Task CPU and memory There is no longer an instance to absorb a sizing mistake: here you pay exactly
The network Subnets, security groups, routes and endpoints are still your decisions
The permissions Execution role and task role, exactly as in 10-01
Availability Spreading tasks across AZs, sizing the minimum, defining the auto scaling

The sentence that sums up the change: Fargate does not remove the work of operating an application, it removes the work of operating a server. Marta still has to decide how many tasks there are on a Friday at 19:00; what she no longer has to decide is the size of the machines that host them.

Technically, each Fargate task runs in its own environment isolated by lightweight virtualisation — the same technology that underpins Lambda — which resolves the warning from 10-01 about the shared kernel: two tasks never share a kernel, not even two tasks of your own.

The same service on EC2 and on Fargate

graph TB
  subgraph EC2["ECS on EC2 (10-01)"]
    A1[ALB] --> S1[ECS service]
    S1 --> I1["m5.large instance<br/>AMI + ECS agent<br/>PATCH IT"]
    S1 --> I2["m5.large instance<br/>AMI + ECS agent<br/>PATCH IT"]
    I1 --> T1[Task]
    I1 --> T2[Task]
    I2 --> T3[Task]
    I2 --> H["Gap paid for<br/>and empty"]
    ASG["ASG + capacity provider<br/>start-up: 2 minutes"] -.manages.-> I1
    ASG -.manages.-> I2
  end
  subgraph FG["ECS on Fargate (10-02)"]
    A2[ALB] --> S2[ECS service]
    S2 --> F1["Task 1 vCPU / 2 GB<br/>AZ a"]
    S2 --> F2["Task 1 vCPU / 2 GB<br/>AZ a"]
    S2 --> F3["Task 1 vCPU / 2 GB<br/>AZ b"]
    S2 --> F4["Task 1 vCPU / 2 GB<br/>AZ b"]
    AAS["Application Auto Scaling<br/>start-up: 30-45 seconds"] -.manages.-> S2
  end

Both diagrams have the same ALB, the same service and the same tasks. The difference is in the middle layer: in the first there are two boxes representing machines — with their AMI, their agent and their empty, paid-for gap; in the second there are none.

Resource model: CPU, memory and ephemeral storage

In Fargate, cpu and memory at task level are mandatory and only accept specific combinations. You cannot ask for 1 vCPU with 1 GB, nor 3 vCPUs with anything.

vCPU (cpu) Valid memory (memory) Increment Typical case in MercadoFresco
256 (0.25) 512 MB, 1 GB, 2 GB fixed Sidecar, small one-off task
512 (0.5) 1 to 4 GB 1 GB Worker for cola-mercadofresco-correo
1024 (1) 2 to 8 GB 1 GB The shop and the order workers
2048 (2) 4 to 16 GB 1 GB Nightly load into Redshift
4096 (4) 8 to 30 GB 1 GB One-off analysis jobs
8192 (8) 16 to 60 GB 4 GB Rarely: better to split into more tasks
16384 (16) 32 to 120 GB 8 GB Almost never

Two practical consequences:

  • The combination is chosen before you know the real load, and almost always wrongly. The correct method is the one from module 5: look at CpuUtilized and MemoryUtilized in Container Insights for two weeks, take the 95th percentile and add 30 % of headroom. Over-provisioning in Fargate costs money from the first second, because there is no already-paid-for instance for the excess to hide in.
  • The granularity punishes applications with little CPU and a lot of memory. A task that needs 6 GB is forced to ask for at least 1 vCPU, even if it uses 5 % of it. When that happens a lot, it is worth checking whether the application should be a Lambda or whether there is a memory leak.

Ephemeral storage is 20 GB by default, extendable to 200 GB with ephemeralStorage. You are charged only for what exceeds the free 20 GB. It is a temporary disk that disappears when the task ends: it is there to unpack a file or hold a local cache, never for state. When shared persistence is needed, you mount EFS (02-02), which Fargate supports natively.

{
  "family": "mercadofresco-carga-analitica",
  "requiresCompatibilities": ["FARGATE"],
  "networkMode": "awsvpc",
  "cpu": "2048", "memory": "8192",
  "ephemeralStorage": { "sizeInGiB": 60 },
  "runtimePlatform": { "cpuArchitecture": "ARM64", "operatingSystemFamily": "LINUX" }
}

x86 and arm64: Graviton and the real saving

Fargate runs tasks on two architectures: X86_64 and ARM64, the latter on AWS Graviton processors. The switch is one field in the task definition, but it requires the image to be built for that architecture.

Aspect x86_64 arm64 (Graviton)
Price per vCPU-hour ~0.047 USD ~0.037 USD (-20 %)
Performance Baseline Equal or better on web, Python, Java and Node workloads
Image The usual one Has to be built for linux/arm64
Compatibility Total Fails with x86-only native binaries or Python wheels with no arm build
Available on Fargate Spot Yes Yes

Multi-architecture builds are handled in CodeBuild with docker buildx:

docker buildx create --use --name multiarch
docker buildx build --platform linux/amd64,linux/arm64 \
  --tag "${URI}:${TAG}" --push .
# Publishes a manifest index: ECR stores both images under the same tag
# and each task pulls the one matching its architecture.

For MercadoFresco the saving is real and measurable: the shop runs 2 base tasks plus up to 2 more at the peak, of 1 vCPU and 2 GB. Moving to Graviton brings the service's monthly cost down from about 85 USD to about 68 USD, 20 % without touching a line of business code. The requirement is to test it: Luis builds the image for both architectures, deploys arm64 to pre-production for a week, compares latency and errors on the mercadofresco-produccion dashboard, and only then changes production.

The concrete risk is dependencies: a Python wheel that only publishes x86 binaries forces you either to compile during the build — which the constructor stage of the 10-01 Dockerfile already allows — or to block the change. Better to find out in CodeBuild than in a deployment.

Operational differences: no SSH, no hostPort, no DaemonSet

Fargate imposes restrictions that are not arbitrary: they follow from the host not existing as far as you are concerned.

Restriction Why What you do instead
No SSH or docker exec There is no machine to connect to ECS Exec (next section)
No hostPort different from containerPort There are no host ports to map awsvpc gives one IP per task; there is no conflict
awsvpc network mode only Each task is its own ENI Security groups per task, target group of type ip
No DaemonSet or DAEMON tasks There are no nodes to put one per node on Sidecars: FireLens, X-Ray, OTel inside each task
No host volumes or privileged The host is not yours Ephemeral volumes, EFS, and limited linuxParameters
No GPU or special instance families You do not choose the machine EC2 still exists for those cases
No awsvpcTrunking to manage There is no instance with an ENI limit One of the expensive traps of 10-01 disappears

The restriction that grates most at first is the first one, and the second most is the DaemonSet: plenty of people ran one log-collection container per instance. In Fargate that pattern is replaced by a sidecar inside each task, which costs some CPU and memory per task but removes the coordination between nodes.

ECS Exec: debugging with no server to touch

ECS Exec opens a command session inside a running container using AWS Systems Manager, with no open ports, no SSH keys and no bastion. It needs three things:

  1. The service or task with --enable-execute-command.
  2. The task role — not the execution role — with SSM channel permissions.
  3. The SSM agent, which Fargate already includes from platform version 1.4 onwards.
{"Version": "2012-10-17", "Statement": [{
  "Effect": "Allow", "Resource": "*",
  "Action": ["ssmmessages:CreateControlChannel", "ssmmessages:CreateDataChannel",
             "ssmmessages:OpenControlChannel", "ssmmessages:OpenDataChannel"]
}]}
aws ecs execute-command --cluster ecs-mercadofresco \
  --task 9f2c1b7a4e0d4a6f8b3c5d7e1a2b3c4d \
  --container tienda --interactive --command "/bin/sh"

Three warnings to internalise before using it in production:

  • Everything is audited in CloudTrail (05-03) with the ExecuteCommand event: who, when, on which task and with which command. On top of that, sessions can be required to be logged to CloudWatch Logs or S3 with executeCommandConfiguration on the cluster, encrypted with alias/mercadofresco-datos. At MercadoFresco, using ECS Exec in production fires a notification to alertas-mercadofresco: it is not forbidden, it is watched.
  • Changes made inside a container do not survive. Editing a configuration file in the task works until the task is replaced, and then it silently disappears. ECS Exec is for diagnosing, not for fixing.
  • With readonlyRootFilesystem: true the file system is read-only, which is exactly what you want: if you need to write in order to debug, the answer is usually that a log is missing.

In practice, 90 % of what people used to go looking for over SSH is already in CloudWatch Logs Insights or in X-Ray. ECS Exec is for the remaining 10 %: checking a DNS resolution, verifying that an environment variable arrived as expected, or confirming that the process is listening on the right port.

Logging with FireLens and Fluent Bit

The awslogs driver from 10-01 still works and is the right default. When more is needed — routing to several destinations, filtering before sending, rewriting the format or sending a copy to an external system — the answer in Fargate is FireLens: a Fluent Bit sidecar that ECS configures automatically.

[
  {
    "name": "registro",
    "image": "public.ecr.aws/aws-observability/aws-for-fluent-bit:stable",
    "essential": true,
    "firelensConfiguration": { "type": "fluentbit", "options": { "enable-ecs-log-metadata": "true" } },
    "cpu": 64, "memoryReservation": 128
  },
  {
    "name": "tienda",
    "image": "555566667777.dkr.ecr.eu-west-1.amazonaws.com/mercadofresco/tienda@sha256:9c1e...f4a2",
    "essential": true,
    "logConfiguration": {
      "logDriver": "awsfirelens",
      "options": { "Name": "cloudwatch_logs", "region": "eu-west-1",
                   "log_group_name": "/ecs/mercadofresco-tienda", "log_stream_prefix": "tienda-",
                   "auto_create_group": "false" }
    }
  }
]

The cost of FireLens is one more container per task, some 64 CPU units and 128 MB, which in a four-task service is real money. MercadoFresco's rule: awslogs by default, FireLens only when there is a requirement awslogs does not cover, such as filtering access logs containing personal data before they leave the task.

Task start-up and the Friday peak

This is the point the lesson exists for. A Fargate task goes through these phases:

  1. Provisioning (PROVISIONING): the ENI is created in the subnet and the security group is attached to it. 3 to 10 seconds.
  2. Image pull (PENDING): it authenticates against ECR and pulls the missing layers. 5 to 25 seconds depending on image size and whether there is a VPC endpoint.
  3. Container start-up: the ENTRYPOINT runs. 1 to 5 seconds.
  4. Health checks: the ALB registers the target and waits for the consecutive successes. 15 to 40 seconds depending on the target group configuration.
Phase ASG with AMI (before) Fargate (now)
Machine boot 60-75 s (BIOS, kernel, cloud-init) 0 s: there is no machine
Network preparation Included 3-10 s (ENI)
Fetching the artefact Included in the AMI 5-25 s (image from ECR)
Application start-up 20-30 s 1-5 s
Registration and health checks 20-40 s 15-40 s
Total until it takes traffic ≈ 120 s ≈ 35-45 s

What that means for Friday at 19:00 with 900 orders/hour is concrete: when the ALBRequestCountPerTarget alarm crosses the threshold, the new capacity starts serving requests after 40 seconds instead of after two minutes. With the traffic ramp measured at MercadoFresco — from 300 to 900 orders/hour in about eight minutes — those 80 seconds of difference are the boundary between latency rising a little and latency setting off mercadofresco-alb-latencia-alta.

And there is a less obvious, more valuable consequence: with 40-second start-ups you can afford a lower minimum. Before, 2 instances always had to be running to absorb the ASG's reaction time. With Fargate the minimum can still be 2 tasks for availability — never fewer, to survive the loss of an AZ — but the extra headroom kept "just in case" stops being necessary.

The lever that shortens phase 2 is image size and the route to ECR. The shop's 220 MB image takes about 8 seconds from a VPC endpoint and about 20 through the NAT. That is another argument for the carefully written Dockerfile of 10-01 and for the next section.

Migrating the shop service to Fargate

The change is surprisingly small: the task definition from 10-01 already declared requiresCompatibilities: ["EC2", "FARGATE"] and awsvpc mode, precisely for this.

Step 1: set CPU and memory from data, not from intuition. Container Insights has been measuring for two weeks. The query Marta runs:

aws cloudwatch get-metric-statistics --namespace ECS/ContainerInsights \
  --metric-name CpuUtilized --statistics Maximum p95 --period 300 \
  --dimensions Name=ServiceName,Value=svc-mercadofresco-tienda Name=ClusterName,Value=ecs-mercadofresco \
  --start-time 2026-07-15T00:00:00Z --end-time 2026-07-29T00:00:00Z

The 95th percentile of CPU is 0.62 vCPU and that of memory 1.4 GB, with an absolute maximum of 0.81 vCPU at the Friday peak. The tightest valid combination above that with headroom is 1 vCPU and 2 GB. Dropping to 0.5 vCPU / 2 GB would be tempting — it saves 25 % — but it leaves Friday's task at 160 % of its CPU: latency would degrade exactly when it matters.

Step 2: create the service with the Fargate capacity provider.

aws ecs create-service --cluster ecs-mercadofresco --region eu-west-1 \
  --service-name svc-mercadofresco-tienda-fg --task-definition mercadofresco-tienda:24 \
  --desired-count 4 \
  --capacity-provider-strategy capacityProvider=FARGATE,weight=1,base=2 \
  --platform-version LATEST \
  --network-configuration 'awsvpcConfiguration={subnets=[snet-mercadofresco-app-a,snet-mercadofresco-app-b],
      securityGroups=[sg-mercadofresco-tienda],assignPublicIp=DISABLED}' \
  --load-balancers 'targetGroupArn=...targetgroup/tg-mercadofresco-tienda/abc123,containerName=tienda,containerPort=8080' \
  --health-check-grace-period-seconds 45 \
  --deployment-configuration '{"minimumHealthyPercent": 100, "maximumPercent": 200,
      "deploymentCircuitBreaker": {"enable": true, "rollback": true}}' \
  --enable-execute-command --propagate-tags SERVICE

Three details of the command:

  • assignPublicIp=DISABLED with private subnets. The tasks go in snet-mercadofresco-app-a and -b, with no public IP. That is the correct choice and it is what forces the next section: without a public IP, the task needs a route to ECR, S3, CloudWatch Logs and Secrets Manager, and that route is either an expensive NAT or some cheap endpoints.
  • There is no --placement-strategy. Placement strategies belong to EC2. Fargate spreads tasks across the declared subnets, so availability comes from declaring subnets in both AZs, not from a strategy.
  • --platform-version LATEST. The platform version is the equivalent of the host AMI, and AWS manages it. Pinning it to a specific version only makes sense if something new breaks, and then it is a temporary measure, not a permanent one.

Step 3: coexistence and cutover. Just as in 10-01, the ALB splits by weights between the EC2 service and the Fargate one: 90/10, then 50/50, then 0/100 over two weeks, watching TargetResponseTime and HTTPCode_Target_5XX. The day the weight reaches 100, the EC2 cluster's ASG is deleted and with it the AMI, the launch template and the patching cycle.

VPC endpoints: no longer depending on the NAT

A Fargate task in a private subnet talks to several AWS services just to start. If that communication goes out through the NAT, you pay twice: the NAT's hourly charge and every gigabyte processed.

Endpoint Type What for What fails without it
com.amazonaws.eu-west-1.ecr.api Interface ECR authentication and metadata CannotPullContainerError
com.amazonaws.eu-west-1.ecr.dkr Interface Image pull protocol CannotPullContainerError
com.amazonaws.eu-west-1.s3 Gateway The image layers live in S3 The pull hangs and times out
com.amazonaws.eu-west-1.logs Interface CloudWatch Logs The task starts and not a single log appears
com.amazonaws.eu-west-1.secretsmanager Interface Resolving the secrets ResourceInitializationError
com.amazonaws.eu-west-1.ssm Interface Parameter Store Same as the previous one
com.amazonaws.eu-west-1.ssmmessages Interface ECS Exec ECS Exec does not connect
com.amazonaws.eu-west-1.kms Interface Decrypting secrets and image Decryption fails

The S3 one is the most forgotten and the most important: it is a gateway endpoint, it is free, and without it the image pull does not work even if both ECR endpoints are in place. MercadoFresco already had vpce-mercadofresco-s3 from module 3, so that one is sorted.

for SERVICE in ecr.api ecr.dkr logs secretsmanager ssm ssmmessages kms; do
  aws ec2 create-vpc-endpoint --vpc-id vpc-mercadofresco \
    --vpc-endpoint-type Interface \
    --service-name "com.amazonaws.eu-west-1.${SERVICE}" \
    --subnet-ids snet-mercadofresco-app-a snet-mercadofresco-app-b \
    --security-group-ids sg-mercadofresco-endpoints \
    --private-dns-enabled \
    --tag-specifications 'ResourceType=vpc-endpoint,Tags=[{Key=Proyecto,Value=mercadofresco},
        {Key=Entorno,Value=produccion},{Key=Componente,Value=endpoints},
        {Key=Propietario,Value=plataforma},{Key=CentroCoste,Value=tecnologia}]'
done

The saving calculation for MercadoFresco, with 4.8 deployments a week, task replacements and daily scaling:

Item With NAT With interface endpoints
Image pulls ~55 GB/month × 0.045 USD = 2.50 USD Traffic through the endpoint: ~0.55 USD
Logs and API calls ~30 GB/month × 0.045 = 1.35 USD ~0.30 USD
Fixed cost 2 NATs × 0.045 USD/h ≈ 66 USD/month 7 endpoints × 2 AZs × 0.011 USD/h ≈ 112 USD/month
Total ≈ 70 USD/month ≈ 113 USD/month

And here it is worth being honest rather than repeating the slogan: with seven endpoints across two AZs, the endpoints come out more expensive than the NAT in an architecture the size of MercadoFresco's. The argument in their favour is not just price, it is three other things: the traffic never leaves the AWS network, which is a defensible security requirement in front of an auditor; task start-up is faster and more predictable; and the NAT stops being a single point of failure for start-up. Marta's decision is intermediate and reasoned: endpoints for ECR, S3 and Logs — the ones on the critical start-up path, four of them, costing about 64 USD — and keep the NAT for the rest, knowing that module 11 will revisit whether it is worth it.

Auto scaling: target tracking, steps and schedules

The three policy types combine, and each solves a different problem.

Type How it decides Strength Weakness
Target tracking Keeps a metric at a value Simple, self-regulating Reacts after the change
Step scaling Thresholds with different jumps Aggressive response to big jumps The steps have to be calibrated by hand
Scheduled scaling The clock Acts before the load arrives Blind to the unexpected

MercadoFresco's full configuration uses all three. The target tracking on ALBRequestCountPerTarget from 10-01 stays as the baseline. On top of it, the Friday scheduled scaling, which is the missing piece:

TARGET="--service-namespace ecs --scalable-dimension ecs:service:DesiredCount \
  --resource-id service/ecs-mercadofresco/svc-mercadofresco-tienda-fg"

# 16:45, fifteen minutes before the peak: raise the minimum to 6 tasks.
aws application-autoscaling put-scheduled-action $TARGET \
  --scheduled-action-name pico-viernes-inicio \
  --schedule "cron(45 16 ? * FRI *)" --timezone "Europe/Madrid" \
  --scalable-target-action MinCapacity=6,MaxCapacity=20

# 21:30, half an hour after the peak: return the minimum to 2.
aws application-autoscaling put-scheduled-action $TARGET \
  --scheduled-action-name pico-viernes-fin \
  --schedule "cron(30 21 ? * FRI *)" --timezone "Europe/Madrid" \
  --scalable-target-action MinCapacity=2,MaxCapacity=20

Three design decisions in those eight lines:

  • The minimum is scheduled, not the desired count. Setting DesiredCount would effectively disable target tracking during the window. By raising the minimum, capacity is guaranteed and target tracking can still grow above it if the Friday turns out better than expected.
  • --timezone "Europe/Madrid". Without it the cron is UTC, and in summer the peak arrives an hour before the scheduled action. It is a mistake that only shows up when the clocks change, in October, when nobody remembers touching anything.
  • Fifteen minutes before and half an hour after. Before, so the tasks are healthy when the traffic arrives; after, generously, because scaling back too early is what causes the oscillation seen as latency spikes at 21:05.

The cost of that window is easy to calculate and to defend: 4 extra tasks × 5 hours × 4.3 Fridays × 0.057 USD/h ≈ 4.9 USD a month. It is probably the best money MercadoFresco spends.

Fargate Spot and the queue workers

Fargate Spot runs tasks on spare AWS capacity at a discount close to 70 %, in exchange for AWS being able to reclaim it. When it does:

  1. It emits a state-change event to EventBridge and sends SIGTERM to the container.
  2. It waits for the task definition's stopTimeout, with a maximum of two minutes.
  3. It sends SIGKILL and the task is gone.

Two minutes of notice is a lot or a little depending on the workload:

MercadoFresco workload Fargate Spot? Reason
The shop (svc-mercadofresco-tienda-fg) No It is user traffic: an interruption during the Friday peak is exactly what we are avoiding
cola-mercadofresco-pedidos workers Yes, partly The message returns to the queue when visibility expires and another worker processes it; with idempotency (07-05) there are no duplicates
-correo, -almacen, -analitica workers Yes, almost entirely Maximum tolerance: nothing is synchronous or urgent
Nightly load into Redshift Yes It is retried; if it takes ten minutes longer, nobody notices
One-off data migration tasks No An interruption halfway through can leave inconsistent state

The right way to apply it is not "all Spot" but a split across capacity providers, which guarantees an on-demand floor:

aws ecs update-service --cluster ecs-mercadofresco \
  --service svc-mercadofresco-trabajadores \
  --capacity-provider-strategy \
      capacityProvider=FARGATE,weight=1,base=1 \
      capacityProvider=FARGATE_SPOT,weight=4,base=0 \
  --force-new-deployment

base=1 on the on-demand provider means the first task is always stable; the weight splits all the rest in a 1 to 4 ratio. With 10 tasks: 1 base plus 9 split as 1.8 on-demand and 7.2 Spot, that is, roughly 3 on-demand and 7 Spot.

Worker scenario Composition Approximate monthly cost
All on-demand 10 × (0.5 vCPU / 1 GB) ≈ 190 USD
1:4 split with base 1 3 on-demand + 7 Spot ≈ 97 USD
All Spot (not recommended) 10 Spot ≈ 57 USD

The saving from the 1:4 split is almost 50 % and it keeps a floor that survives a mass withdrawal of Spot capacity. The technical requirement that makes it safe is the one from the 10-01 exercise: a SIGTERM handler that finishes the message in flight and does not ask for another, with stopTimeout: 120 and a generous queue visibility timeout.

One-off and scheduled tasks with EventBridge Scheduler

Not everything is a service. The nightly load that feeds wg-mercadofresco-analitica (06-04) runs once a day and finishes. On EC2 that was either an instance left running or a cron on a machine somebody had to look after; on Fargate it is a scheduled one-off task.

aws scheduler create-schedule --name mercadofresco-carga-analitica-nocturna \
  --schedule-expression "cron(0 3 * * ? *)" --schedule-expression-timezone "Europe/Madrid" \
  --flexible-time-window '{"Mode": "FLEXIBLE", "MaximumWindowInMinutes": 30}' \
  --target '{
    "Arn": "arn:aws:ecs:eu-west-1:111122223333:cluster/ecs-mercadofresco",
    "RoleArn": "arn:aws:iam::111122223333:role/rol-scheduler-mercadofresco",
    "EcsParameters": {
      "TaskDefinitionArn": "arn:aws:ecs:eu-west-1:111122223333:task-definition/mercadofresco-carga-analitica:7",
      "LaunchType": "FARGATE", "TaskCount": 1,
      "CapacityProviderStrategy": [{"capacityProvider": "FARGATE_SPOT", "weight": 1}],
      "NetworkConfiguration": {"awsvpcConfiguration": {
        "Subnets": ["snet-mercadofresco-app-a", "snet-mercadofresco-app-b"],
        "SecurityGroups": ["sg-mercadofresco-tienda"], "AssignPublicIp": "DISABLED"}}
    },
    "RetryPolicy": {"MaximumRetryAttempts": 2, "MaximumEventAgeInSeconds": 3600},
    "DeadLetterConfig": {"Arn": "arn:aws:sqs:eu-west-1:111122223333:mercadofresco-pedidos-fallidos"}
  }'

What matters in that configuration: the flexible 30-minute window lets AWS pick the moment within the window, which reduces contention and fits Spot perfectly; the retry policy turns a transient failure into an automatic retry; and the DLQ guarantees that a persistent failure leaves a trace instead of vanishing. It is the same pattern as 07-05 applied to a scheduled task.

For a one-off run — a migration, a data repair — aws ecs run-task with the same network configuration and --launch-type FARGATE is enough. If the job has several steps with dependencies between them, the right tool is Step Functions (07-04), which can launch ECS tasks and wait for them to finish.

Pipeline integration and blue/green

The pipeline-mercadofresco-tienda from module 8 changes little: where there used to be a CodeDeploy deployment onto an ASG, there is now a deployment onto an ECS service.

graph LR
  G["GitHub<br/>mercadofresco-tienda<br/>conn-mercadofresco-github"] --> B["CodeBuild<br/>build-mercadofresco-tienda<br/>docker build + push"]
  B --> E["ECR 555566667777<br/>mercadofresco/tienda:v1.7.0-c4d8e12"]
  E --> R["Replication<br/>to 111122223333"]
  R --> P["CodePipeline<br/>deployment"]
  P --> Q["Lambda<br/>mercadofresco-puerta-calidad<br/>scan + tests"]
  Q --> D["CodeDeploy blue/green<br/>app-mercadofresco-tienda"]
  D --> V["tg-mercadofresco-verde<br/>test port 8443"]
  V --> S["build-mercadofresco-humo"]
  S --> T["tg-mercadofresco-tienda<br/>real traffic"]

CodeDeploy blue/green on ECS works differently from EC2, and the difference is an improvement: CodeDeploy creates a complete new task set with the new revision, registers it in tg-mercadofresco-verde, lets you run the smoke tests against the ALB's test port with no real traffic, and only then shifts the production listener to the green group. The blue set is kept for the termination wait time, so a rollback is a listener change: seconds.

# appspec.yaml for CodeDeploy on ECS
version: 0.0
Resources:
  - TargetService:
      Type: AWS::ECS::Service
      Properties:
        TaskDefinition: <TASK_DEFINITION>          # the pipeline substitutes this
        LoadBalancerInfo:
          ContainerName: "tienda"
          ContainerPort: 8080
        PlatformVersion: "LATEST"
Hooks:
  - AfterAllowTestTraffic: "arn:aws:lambda:eu-west-1:111122223333:function:mercadofresco-puerta-calidad"

With the canary deployment configuration ECSCanary10Percent5Minutes, 10 % of traffic goes to the green set for 5 minutes; if mercadofresco-alb-latencia-alta or mercadofresco-pedidos-fallidos fire during that interval, CodeDeploy rolls back on its own. It is the improvement over the 10-01 circuit breaker: that one detects tasks that do not start; this one detects tasks that start and respond badly.

MercadoFresco's DORA metrics improve where you would expect: restoration goes from 4 minutes to under 1, because rolling back no longer means deploying anything, only moving a listener.

The service in CDK with an L3 construct

All of the above is about twenty lines in the infra-cdk/ project from 09-02, thanks to a level 3 construct that creates the ALB, the target group, the service, the task definition, the roles and the log group.

import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as ecsp from 'aws-cdk-lib/aws-ecs-patterns';

const service = new ecsp.ApplicationLoadBalancedFargateService(this, 'Tienda', {
  cluster,
  serviceName: `svc-mercadofresco-tienda-${config.nombre}`,
  cpu: 1024,
  memoryLimitMiB: 2048,
  desiredCount: config.nombre === 'produccion' ? 4 : 2,
  runtimePlatform: { cpuArchitecture: ecs.CpuArchitecture.ARM64 },
  taskImageOptions: {
    image: ecs.ContainerImage.fromEcrRepository(repository, tag),           // by digest in production
    containerPort: 8080,
    environment: { ENTORNO: config.nombre, COLA_PEDIDOS: queue.queueName },
    secrets: {
      BD_CONTRASENA: ecs.Secret.fromSecretsManager(rdsSecret, 'password'),
      ENDPOINT_CACHE: ecs.Secret.fromSsmParameter(cacheParameter),
    },
    logDriver: ecs.LogDrivers.awsLogs({ streamPrefix: 'tienda', logRetention: 30 }),
  },
  taskSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
  publicLoadBalancer: true,
  circuitBreaker: { rollback: true },
  enableExecuteCommand: config.nombre !== 'produccion',
});

service.targetGroup.configureHealthCheck({
  path: '/salud', healthyThresholdCount: 2, interval: cdk.Duration.seconds(15),
});

// Least privilege with the grant* methods: they include the KMS ones almost nobody remembers.
queue.grantSendMessages(service.taskDefinition.taskRole);
cartsTable.grantReadWriteData(service.taskDefinition.taskRole);

const scaling = service.service.autoScaleTaskCount({ minCapacity: 2, maxCapacity: 20 });
scaling.scaleOnRequestCount('PorPeticiones', {
  requestsPerTarget: 120,
  targetGroup: service.targetGroup,
  scaleOutCooldown: cdk.Duration.seconds(30),
  scaleInCooldown: cdk.Duration.minutes(5),
});
scaling.scaleOnSchedule('PicoViernes', {
  schedule: appscaling.Schedule.cron({ weekDay: 'FRI', hour: '16', minute: '45' }),
  minCapacity: 6,
});

The warning from 09-02 still applies: run cdk synth and read the template the first time. This construct makes decisions for you — it creates a public ALB, opens its security group to 0.0.0.0/0 on port 80, creates a log group with a default retention — and you have to check they match what you want. In production, MercadoFresco adds the certificate and redirectHTTP: true so nothing is ever served over HTTP.

Note also enableExecuteCommand: config.nombre !== 'produccion': ECS Exec enabled in development and pre-production, and disabled in production unless explicitly turned on during an incident. It is a governance decision expressed in one line of infrastructure.

Fargate versus EC2 versus Lambda

All three run code without you buying hardware, and the choice is made on objective criteria.

Criterion EC2 Fargate Lambda
Unit The instance The task (container) The invocation
Cost model Per instance-hour, full or empty Per vCPU-hour and GB-hour of the task Per invocation and GB-second, with a free tier
Cost with constant 24/7 load The lowest with good packing and Savings Plans Intermediate The highest
Cost with intermittent load The highest (you pay for the idle time) Intermediate The lowest (no idle time paid)
Cold start 2 min (new instance) 35-45 s 100 ms - 2 s
Maximum duration Unlimited Unlimited 15 minutes
Maximum memory Up to TB 120 GB 10 GB
Local state Persistent disk Ephemeral (or EFS) Ephemeral (or EFS)
Control of the environment Total: kernel, GPU, families Image and resources Managed runtime or image
Operational work High: AMI, patches, scaling Low: only the image Minimal
Portability AMI, tied to AWS OCI image: runs anywhere Tied to the Lambda model
When to choose it GPU, per-core licences, huge constant workloads, kernel requirements Long-running containerised services: the default choice Events, irregular spikes, short integrations

MercadoFresco's reasoned decision, component by component:

Component Choice Why
The shop Fargate on-demand Long-running process, continuous traffic, 40 s start-up is enough, no idle time to pay for
Order workers Fargate, 1 on-demand + 4 Spot Interruption-tolerant with idempotency; -50 % cost
Email and warehouse workers Fargate Spot Nothing urgent or synchronous
mercadofresco-generar-miniaturas Lambda Triggered by an S3 event, lasts seconds, with long idle periods
-cobrar-pago, -reservar-stock, -asignar-reparto Lambda Short steps of the mercadofresco-procesar-pedido machine; containerising them would be a step backwards
Nightly load into Redshift One-off task on Fargate Spot It runs for 25 minutes: past Lambda's 15
Nothing EC2 No component needs a kernel, a GPU or a per-core licence any more

The criterion that settles most of the doubts: if the process lives waiting for requests, it is Fargate; if the process wakes up on an event and dies, it is Lambda; if you need control of the machine, it is EC2. And the economic criterion, refined in module 11: below a high sustained utilisation, Fargate beats EC2 because it does not pay for idle time; above it, EC2 with Savings Plans (11-05) regains the advantage.

App Runner: one more step up the management ladder

AWS App Runner goes one step further: you give it a container image or a source repository, and it creates the service, the load balancer, the certificate, the domain and the auto scaling, including scaling to zero. There is no VPC to design, no target group, no task definition.

What matters is where it stops fitting, and that is why MercadoFresco does not use it: it gives far less network control — access to the VPC requires a connector and there are still limitations — it does not support sidecars or multi-container tasks, its deployment model does not offer CodeDeploy's canary blue/green, and its per-unit cost is higher than Fargate's. It is an excellent option for a simple HTTP service or a prototype, and a bad fit for a shop with Aurora in a private subnet, queues, a cache and a pipeline with a quality gate. It is mentioned because it is the question that always comes up: the answer is that the management ladder is not a ranking, it is a trade-off between convenience and control.

Cost comparison and cleanup

The full calculation for the shop service, with approximate eu-west-1 prices:

Fargate x86, a 1 vCPU and 2 GB task: 1 × 0.04656 + 2 × 0.00511 = 0.05678 USD/hour.

Scenario Calculation Monthly
2 base tasks 24/7 2 × 0.05678 × 730 82.9 USD
+ Friday window (4 extra × 5 h × 4.3) 4 × 5 × 4.3 × 0.05678 4.9 USD
Fargate x86 total ≈ 88 USD
Fargate arm64 total (-20 %) ≈ 70 USD
Previous ASG: 2 × m5.large on-demand 2 × 0.107 × 730 156 USD
+ EBS 2 × 30 GB gp3 2.4 USD
Previous EC2 total ≈ 158 USD

MercadoFresco goes from about 158 USD to about 70 USD a month on the shop, with arm64, and gets rid of the work of maintaining AMIs and patches along the way. With the workers on Spot, the module's total saving comes to around 150 USD a month.

With two honest caveats, because the comparison is not complete without them. The first: the EC2 instances could be bought with Savings Plans and drop 30-40 %, which narrows the gap considerably — and Fargate also accepts Compute Savings Plans, the subject of 11-05. The second: if the EC2 cluster were very well packed, with the shop and the workers sharing instances at 85 % occupancy, EC2 would be competitive. The problem is that such packing has to be achieved and maintained, and that is exactly the kind of work Fargate removes.

Cleanup, in strict order:

# 1. Scheduled actions and scaling policies: otherwise they put tasks back
aws application-autoscaling delete-scheduled-action $TARGET --scheduled-action-name pico-viernes-inicio
aws application-autoscaling deregister-scalable-target $TARGET
# 2. The service down to zero and deleted
aws ecs update-service --cluster ecs-mercadofresco --service svc-mercadofresco-tienda-fg --desired-count 0
aws ecs delete-service --cluster ecs-mercadofresco --service svc-mercadofresco-tienda-fg --force
# 3. EventBridge Scheduler schedules
aws scheduler delete-schedule --name mercadofresco-carga-analitica-nocturna
# 4. THE INTERFACE ENDPOINTS: they cost by the hour even with no traffic
aws ec2 describe-vpc-endpoints --filters Name=vpc-id,Values=vpc-mercadofresco \
  --query 'VpcEndpoints[?VpcEndpointType==`Interface`].VpcEndpointId' --output text \
  | xargs -r aws ec2 delete-vpc-endpoints --vpc-endpoint-ids
# 5. The cluster and the log group
aws ecs delete-cluster --cluster ecs-mercadofresco
aws logs delete-log-group --log-group-name /ecs/mercadofresco-tienda

Step 4 is this lesson's expensive leftover: seven interface endpoints across two AZs cost about 112 USD a month even if not a single byte goes through them. A forgotten endpoint is the most common silent leak after unassociated elastic IP addresses.

Common Mistakes and Tips

Mistake: asking for an invalid CPU and memory combination. RegisterTaskDefinition fails with an unhelpful message. Tip: keep the table to hand; with the CDK, memoryLimitMiB and cpu are validated at synthesis, which is earlier.

Mistake: sizing by intuition. Over-provisioning in Fargate is paid for from the first second. Tip: 95th percentile from Container Insights over two weeks, plus 30 %, and review after a month.

Mistake: private subnets with neither endpoints nor NAT. The task sits in PROVISIONING and dies with CannotPullContainerError. Tip: before migrating, check the route to ECR api, ECR dkr, S3, Logs and Secrets Manager; the S3 one is a gateway, it is free and it is the most forgotten.

Mistake: believing that going without a NAT is always cheaper. Seven interface endpoints across two AZs beat the NAT in a mid-sized architecture. Tip: put endpoints only on the critical start-up path and decide the rest with module 11 data.

Mistake: scheduling DesiredCount instead of the minimum. It effectively disables target tracking during the window. Tip: schedule MinCapacity and let the reactive policy keep working above it.

Mistake: forgetting --timezone in scheduled scaling. The peak shifts by an hour when the clocks change. Tip: explicit Europe/Madrid on every scheduled action, and check the last week of October.

Mistake: putting the shop on Fargate Spot to save money. An interruption with two minutes' notice during the Friday peak is exactly what the course has spent ten modules avoiding. Tip: Spot only where the interruption retries itself, and always with an on-demand base.

Mistake: not handling SIGTERM in the workers. With Spot, every interruption turns into a reprocessed or lost message. Tip: a handler that finishes the message in flight and does not ask for another, stopTimeout: 120 and a generous visibility timeout.

Mistake: using ECS Exec to fix something. The change disappears at the next replacement and leaves a system nobody can reproduce. Tip: ECS Exec to diagnose; the fix goes through the pipeline. And in production, with a notification to alertas-mercadofresco.

Mistake: switching to arm64 without testing. A Python wheel with no arm binary breaks start-up. Tip: docker buildx with both platforms, a week in pre-production and a latency comparison before touching production.

Tip: set --platform-version LATEST and do not pin it. Pinning a platform version means giving up the improvements and the patches, which is exactly what you came to buy.

Tip: keep minimumHealthyPercent: 100 in production. With Fargate there is no longer an instance gap limiting maximumPercent, so 100/200 costs no reserved capacity, only a few minutes of double billing per deployment.

Tip: set an alarm on RunningTaskCount against DesiredTaskCount. It is the earliest signal of a start-up problem, a Fargate quota issue or IP exhaustion in the subnet.

Exercises

Exercise 1: sizing and deciding the capacity split

The cola-mercadofresco-pedidos workers process each message in 12 seconds on average, with a peak of 900 messages/hour on Fridays from 17:00 to 21:00 and around 150 messages/hour the rest of the time. Container Insights shows each worker at a 95th percentile of 0.31 vCPU and 780 MB. The internal service agreement is that no order waits more than 3 minutes in the queue. Design: (a) the task's CPU and memory combination, with justification; (b) how many tasks are needed at the peak and off-peak, with the calculation; (c) the complete auto scaling policy, stating which metric you use and why not CPU; (d) the split between FARGATE and FARGATE_SPOT with its base and weight, and what happens if AWS withdraws all Spot capacity during the peak; and (e) the approximate monthly cost of your design.

Exercise 2: the migration that got stuck in PROVISIONING

Luis migrates the shop service to Fargate in pre-production. The tasks sit in PROVISIONING for several minutes and end with ResourceInitializationError: unable to pull secrets or registry auth. He checks the following and everything looks right: the execution role has the ECR and Secrets Manager permissions, the task definition is the same one that worked on EC2, and the subnets are snet-mercadofresco-app-a and -b. Diagnose (a) the three possible network causes, in order of likelihood, with the concrete check for each; (b) why the same role and the same definition did work with the EC2 launch type; (c) which specific VPC endpoint would fix the most likely case and why its type matters; and (d) which two checks you would add to the pipeline so this failure never reaches a deployment again.

Exercise 3: the proposal to move everything to Lambda

Sara comes back from a conference with a proposal: get rid of Fargate and move the whole shop to Lambda behind API Gateway, "because you only pay per request and it scales to zero". She brings the figure that the shop receives about 300 orders/hour off-peak and that at four in the morning there is no traffic. Respond with technical and economic judgement: (a) three concrete technical reasons why MercadoFresco's shop is a poor fit for Lambda; (b) the approximate calculation comparing the cost of both options with the real traffic; (c) which part of her proposal is right and where it is already being applied in the current architecture; (d) what you would measure before ruling it out entirely; and (e) how you would explain it to her in one sentence that does not sound like "no".

Solutions

Solution 1

(a) The combination. The 95th percentile is 0.31 vCPU and 780 MB. Adding 30 % of headroom: 0.40 vCPU and 1.01 GB. The next valid combination above that is 512 (0.5 vCPU) with 1 GB of memory. Going up to 1 vCPU would double the cost for 20 % of unused CPU, and dropping to 0.25 vCPU is not possible because it does not reach. Cost per task: 0.5 × 0.04656 + 1 × 0.00511 = 0.0279 USD/hour.

(b) How many tasks. One worker processes 3600 / 12 = 300 messages/hour. Off-peak, 150 messages/hour need 1 task, but the minimum must be 2 for availability: a single task means zero capacity while it is being replaced. At the peak, 900 messages/hour need 900 / 300 = 3 tasks just to keep up, and that is exactly the point where the queue does not grow but does not recover from a backlog either. To meet the 3 minutes with headroom and absorb the variability, 5 tasks are used at the peak, giving 1500 messages/hour of capacity and allowing an accumulated backlog to be drained.

(c) The auto scaling policy. CPU is no use: a worker consuming messages is always equally busy, whether the queue holds 10 or 10,000 messages, so CPU does not reflect the backlog. The correct metric is the backlog per task, calculated with a CloudWatch metric maths expression: ApproximateNumberOfMessagesVisible / RunningTaskCount. With a 3-minute objective and 300 messages/hour per task, the target is 300 / 60 × 3 = 15 messages per task.

{"TargetValue": 15.0, "ScaleOutCooldown": 60, "ScaleInCooldown": 300,
 "CustomizedMetricSpecification": {"Metrics": [
   {"Id": "visibles", "MetricStat": {"Metric": {"Namespace": "AWS/SQS",
      "MetricName": "ApproximateNumberOfMessagesVisible",
      "Dimensions": [{"Name": "QueueName", "Value": "cola-mercadofresco-pedidos"}]},
      "Stat": "Average"}, "ReturnData": false},
   {"Id": "tareas", "MetricStat": {"Metric": {"Namespace": "ECS/ContainerInsights",
      "MetricName": "RunningTaskCount",
      "Dimensions": [{"Name": "ServiceName", "Value": "svc-mercadofresco-trabajadores"},
                     {"Name": "ClusterName", "Value": "ecs-mercadofresco"}]},
      "Stat": "Average"}, "ReturnData": false},
   {"Id": "retraso", "Expression": "visibles / MAX([tareas, 1])", "ReturnData": true}]}}

A scheduled action is added for Fridays, raising MinCapacity to 5 at 16:45 and returning it to 2 at 21:30, with --timezone "Europe/Madrid": scaling up by the clock avoids the first few minutes of backlog that a reactive policy cannot avoid by definition.

(d) The capacity split. capacityProvider=FARGATE,weight=1,base=2 plus capacityProvider=FARGATE_SPOT,weight=3,base=0. The two floor tasks are stable, and of the three extra peak tasks roughly 0.75 go on-demand and 2.25 to Spot. If AWS withdraws all Spot capacity during the peak, the 2 on-demand tasks remain, with capacity for 600 messages/hour against 900 incoming: the queue grows by about 300 messages/hour and the 3-minute agreement is breached in about twenty minutes. The mitigation has two parts: the auto scaling policy detects the backlog and asks for new tasks, which the on-demand provider can serve — the weight split applies to new tasks, and with Spot unavailable ECS launches on-demand; and an alarm on ApproximateAgeOfOldestMessage notifies alertas-mercadofresco if the oldest message passes 3 minutes. What you must not do is set base=0: the stable floor is what turns a Spot withdrawal into a degradation instead of an outage.

(e) Monthly cost. Off-peak: 2 tasks × 0.0279 × (730 − 17) ≈ 39.8 USD. At the peak (4 h × 4.3 Fridays = 17.2 h): 2 on-demand × 0.0279 × 17.2 ≈ 0.96 USD, plus 3 extra tasks mostly on Spot ≈ 3 × 0.0084 × 17.2 ≈ 0.43 USD. Total ≈ 41 USD a month, against the 156 USD of the dedicated worker ASG there used to be.

Solution 2

(a) The three network causes, by likelihood.

  1. The ECR and Secrets Manager interface endpoints are missing, and there is no NAT in the pre-production subnet. It is the most likely cause because the error mentions registry auth and secrets, which are exactly the first two outbound calls a task makes. Check: aws ec2 describe-route-tables on the -app-a and -b subnets looking for the 0.0.0.0/0 route, and aws ec2 describe-vpc-endpoints filtered by VPC.
  2. The endpoints' security group does not allow inbound traffic from the tasks'. Interface endpoints are ENIs with their own security group: if sg-mercadofresco-endpoints does not accept port 443 from sg-mercadofresco-tienda, they exist but cannot be used. Check: the inbound rules of sg-mercadofresco-endpoints.
  3. The endpoint's private DNS is disabled. Without --private-dns-enabled, the name secretsmanager.eu-west-1.amazonaws.com still resolves to the public IP and the traffic tries to leave where there is no exit. Check: the endpoint's PrivateDnsEnabled field.

(b) Why it did work on EC2. Because with the EC2 launch type, what pulls the image and resolves the secrets is the instance's ECS agent, which uses the instance's network route and its security group. If those instances were in a subnet with a NAT or had a more permissive security group, everything worked. On moving to Fargate, connectivity comes to depend on the task's ENI, with the subnets and security group declared in networkConfiguration. It is the same boundary shift that awsvpc was about in 10-01, now visible: the network no longer belongs to the machine, it belongs to the task.

(c) The specific endpoint and why its type matters. For the most likely case three are needed: ecr.api, ecr.dkr and secretsmanager, all three of them interface endpoints (a private ENI with a security group, charged by the hour and by GB). But the one that usually goes missing without showing its face is s3, which is a gateway: it is not an ENI but an entry in the route table, it is free, and without it the image layers — which are stored in S3 — are not downloaded even if both ECR endpoints are perfect. Confusing the two types leads people to put an interface endpoint on S3, which works but costs money unnecessarily in this case.

(d) Two checks in the pipeline. The first, a deployment test in pre-production with the same network configuration as production: the failure shows up where it should. The second, an AWS Config rule (05-04) or a CDK test (09-02) verifying that every subnet used by an ECS service has either a route to a NAT or the five critical-path endpoints. It is an architectural invariant and therefore it is tested as code, not remembered. As a backstop, checking that the circuit breaker is enabled with rollback: true would have turned this incident into an automatic rollback instead of tasks spinning in the void.

Solution 3

(a) Three technical reasons.

  1. The Aurora connection. Every concurrent Lambda opens its own connection, and with 300 orders/hour in concurrency spikes that exhausts the aurora-mercadofresco-pedidos pool quickly. It is solved with RDS Proxy, which adds cost and one more piece; on Fargate, each task keeps a stable pool and the problem does not exist.
  2. The cold start with warm state. The shop keeps the catalogue cache in memory, preloaded from mercadofresco-catalogo at start-up. On Fargate that cost is paid once per task and lasts hours; on Lambda it would be paid at every cold start, and the first request of each execution environment would be slow just as a peak arrives.
  3. The rewrite. The shop is a WSGI application running under Gunicorn; taking it to Lambda requires an adapter or splitting it into functions, plus a change to the session model, the static files and the observability. It is a project of several weeks with dubious benefit, right after finishing the migration to containers.

(b) The calculation. With 300 orders/hour on average and about 8 HTTP requests per order, that comes to around 2,400 requests/hour, that is, 1.75 million a month. Assuming 250 ms on average and 2 GB of memory: 1.75 M × 0.25 s × 2 GB = 875,000 GB-s. At 0.0000167 USD per GB-s that is about 14.6 USD, plus 0.35 USD of invocations, plus API Gateway: 1.75 M × 3.5 USD/million ≈ 6.1 USD. Total ≈ 21 USD, against the 70 USD of Fargate arm64. Sara is right on the raw number. But three items are missing that change the conclusion: RDS Proxy (about 30 USD/month), the cost of the rewrite (weeks of work, which at team cost far exceeds the annual saving of 588 USD) and the risk of latency from cold starts during the Friday peak, which is precisely the problem this module has just solved.

(c) Where she is right and where it is already applied. She is right on the principle: do not pay for idle capacity. And that principle is already applied where it fits: mercadofresco-generar-miniaturas is triggered by an S3 event and nothing is paid between photo uploads; -cobrar-pago, -reservar-stock and -asignar-reparto are short steps of the state machine; and the queue workers scale down to the minimum off-peak. The architecture is already hybrid by design: Lambda where the work is sporadic and short, Fargate where the process lives waiting for requests.

(d) What I would measure before ruling it out. Three concrete things: the real latency distribution of the shop by percentile in X-Ray, to know how much a cold start would weigh at p99; the number of simultaneous connections to Aurora at the Friday peak, to size whether RDS Proxy would be enough; and the real cost of Fargate over a full month with arm64 and the scheduled scaling already in place, because the comparison is being made against a number that has not been measured yet. Without those three figures, both positions are opinions.

(e) The sentence. "You are right on the principle, and that is why we are already applying it: everything that is triggered by an event and dies within seconds is already on Lambda. The shop is the opposite — a process that lives waiting for requests and keeps a cache and connections — and for that, Fargate costs fifty dollars more a month and saves us a rewrite, an RDS Proxy and the cold starts on Friday at seven."

Conclusion

The instances are gone. MercadoFresco's shop now runs on Fargate, and there is no longer any machine for Marta to list, size, patch or leave running.

You are clear on what disappears — the AMI, host patching, cluster scaling, task packing, idle capacity and even the ENI limit per instance — and on what is still yours: the image, the CPU and memory, the network and the permissions. With the sentence that orders the rest: Fargate does not remove the work of operating an application, it removes the work of operating a server. And with the real resource model: the valid CPU and memory combinations, the 20 GB of ephemeral storage extendable to 200, and the decision to size with the 95th percentile from Container Insights plus 30 %, because over-provisioning here is paid for from the first second. Plus arm64 with Graviton, which brings the service down from 88 to 70 USD a month with docker buildx and a week of testing in pre-production.

You have the operational differences and what to do about each: ECS Exec instead of SSH, audited in CloudTrail and with the rule that it is for diagnosing and not for fixing; FireLens with Fluent Bit as a sidecar where there used to be a DaemonSet, with awslogs as the default because the sidecar costs CPU in every task; and awsvpc as the only network mode, which is no longer a restriction but the normal way of working. And you have the number that justifies the whole module: from 120 seconds to 35-45 until a new task takes traffic, with the breakdown by phase and what it means on Friday at 19:00 when traffic goes from 300 to 900 orders/hour in eight minutes.

You have the complete migration: an unchanged task definition because it already declared FARGATE, private subnets snet-mercadofresco-app-a and -b with no public IP, a security group per task, and the VPC endpoints on the critical path — with the honest analysis that seven endpoints across two AZs come out more expensive than the NAT, and the reasoned decision to deploy only the four that matter. The three-layer auto scaling: target tracking as the baseline, and above all the Friday scheduled scaling, which raises the minimum to 6 at 16:45 with an explicit Europe/Madrid and costs 4.9 USD a month. Fargate Spot with the base=1, weight 1:4 split for the workers and the criterion that decides where it belongs: only where an interruption with two minutes' notice retries itself. The scheduled tasks with EventBridge Scheduler for the nightly load into Redshift, with a flexible window, retries and a DLQ. And the pipeline with CodeDeploy's canary blue/green on tg-mercadofresco-tienda and -verde, which brings restoration down from 4 minutes to under 1 because rolling back means moving a listener; all of it in twenty lines of CDK with ApplicationLoadBalancedFargateService.

And you have the honest comparison of Fargate versus EC2 versus Lambda, with the criterion that settles most of the doubts — if the process lives waiting for requests, it is Fargate; if it wakes up on an event and dies, it is Lambda; if you need control of the machine, it is EC2 — and MercadoFresco's component-by-component decision, in which EC2 no longer appears in a single row.

One question remains that somebody on the team will ask this very week, and it deserves a serious answer rather than a shrug: everybody talks about Kubernetes. It is the de facto standard for container orchestration, it has an enormous ecosystem, it works the same on any cloud and a great many people know how to use it. Are we making a mistake by staying with ECS?

In 10-03, "Amazon EKS", that is answered with data rather than preferences. You will see what Kubernetes is and what it solves that ECS does not, its minimal but real vocabulary, what EKS manages and what is still yours, the four compute options including Auto Mode, the complete manifests for deploying the shop with probes, Ingress and the AWS Load Balancer Controller, IRSA and Pod Identity as the equivalents of the task role, Karpenter for the nodes, and the central argument almost nobody writes down: the hidden cost of version upgrades. Together with the ECS versus EKS comparison and MercadoFresco's reasoned decision, along with the objective criteria that would change it.

© Copyright 2026. All rights reserved