The previous lesson ended with a question somebody on the team was going to ask that very week: 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 there are plenty of people who know how to use it. If MercadoFresco has just moved its whole shop onto containers and has settled on ECS, is it getting this wrong?
This lesson answers with data. You will see what Kubernetes really is and what it solves that ECS does not, what Amazon EKS manages and what is still your job, how MercadoFresco's shop is deployed with complete manifests, and what the cost is that almost never appears in the comparisons: the recurring work of version upgrades. At the end, MercadoFresco's reasoned decision and the objective criteria that would change it. And with it, the close of the module and the jump to module 11.
Cost warning. The EKS control plane costs 0.10 USD per hour per cluster, around 73 USD a month, regardless of size. With extended support — when the Kubernetes version leaves standard support — it goes to 0.60 USD per hour, around 438 USD a month, and it is an automatic charge that catches a lot of people out. On top of that come the nodes (EC2 or Fargate), the load balancer and the add-ons. A forgotten test cluster is one of the most expensive leftovers in the course: follow the cleanup section at the end. Fictitious data and identifiers.
Contents
- What Kubernetes is and what it solves that ECS does not
- The bare minimum vocabulary, properly understood
- The declarative model and the reconciliation loop
- What EKS manages and what is still yours
- Compute options: nodes, Fargate and Auto Mode
- Creating a cluster with
eksctland with CDK kubectland access to the cluster- Authentication: access entries versus
aws-auth - IRSA and EKS Pod Identity: AWS permissions for a pod
- The manifests for MercadoFresco's shop
- Ingress and the AWS Load Balancer Controller
- Auto scaling: HPA, Cluster Autoscaler and Karpenter
- Add-ons: why an empty EKS is no use at all
- Version upgrades: the hidden cost
- Observability: Container Insights, Prometheus and Grafana
- ECS versus EKS: the honest comparison
- MercadoFresco's decision and what would change it
- Cost and cleanup
- Common mistakes and tips
- Exercises
- Module wrap-up and transition to module 11
What Kubernetes is and what it solves that ECS does not
Kubernetes is an open-source container orchestrator, born at Google and donated to the Cloud Native Computing Foundation. It does the same as ECS — keeping containers running, spreading them across machines, replacing them, exposing them over the network — with one fundamental difference in design: Kubernetes is an extensible platform with an API of its own, not a vendor's service.
That difference produces three capabilities ECS does not have:
- Real portability. A Kubernetes manifest works on EKS, on Google Kubernetes Engine, on Azure Kubernetes Service, on a cluster of your own and on Luis's laptop with
kind. An ECS task definition only works on AWS. For MercadoFresco this is not urgent today, but it is the weightiest argument in favour of EKS. - An ecosystem that solves problems you do not want to solve. There are thousands of components ready to install: certificate management (
cert-manager), progressive delivery (Argo Rollouts, Flagger), GitOps (Argo CD, Flux), service meshes (Istio, Linkerd), operators that manage databases, queues or search engines as if they were native resources. On ECS, almost all of that is either provided by AWS or written by you. - Extensibility through the API. With CRDs (Custom Resource Definitions) and operators you can create your own resource types. That is what allows you to run
kubectl applyagainst a file declaring "I want a Kafka cluster with three replicas" and have an operator build it and keep it running. ECS has no equivalent.
And it produces a cost that has to be named with equal clarity: Kubernetes is substantially more complex. There are more concepts, more pieces to install, more configuration that can be wrong and more versions to upgrade. The right question is not "which is better?" but "does the problem I have justify that complexity?".
The bare minimum vocabulary, properly understood
These are the objects you need to know to read a manifest without getting lost. The right-hand column anchors each one to what you already know from earlier modules.
| Object | What it is | Rough equivalent |
|---|---|---|
| Cluster | The whole thing: control plane plus nodes | ECS cluster |
| Node | A machine that runs workloads; usually an EC2 instance | An instance in the ECS cluster |
| Pod | The smallest unit: one or more containers sharing network and storage | ECS task |
| ReplicaSet | Guarantees that there are N identical pods | The part of the ECS service that replaces things |
| Deployment | Manages ReplicaSets and orchestrates rolling updates | ECS service |
| Service | A stable name and IP for a set of pods | Cloud Map or ECS Service Connect |
| Ingress | External HTTP routing towards Services | ALB rules (03-03) |
| Namespace | A logical partition inside the cluster | Grouping by project or environment |
| ConfigMap | Non-sensitive configuration as key-value pairs | environment in the task definition |
| Secret | Sensitive configuration (base64-encoded, not encrypted by default) | secrets from Secrets Manager |
| DaemonSet | One pod per node | Does not exist on Fargate; it does on ECS over EC2 |
| Job / CronJob | A task that finishes, one-off or scheduled | run-task / EventBridge Scheduler |
Two warnings about that table, because they are a constant source of errors:
- A Kubernetes
Secretis not encrypted by default. It is base64-encoded, which is not encryption: anyone with read permission on the namespace sees it in the clear. It is solved with etcd encryption using KMS and, better still, with the secrets CSI driver that brings the values in from Secrets Manager (see below). - The
Deploymentis not the service and theServiceis not the Deployment. The Deployment manages the pods; the Service gives them a stable name. In ECS, both things live inside the "service" object, and that merging is one of the reasons ECS is simpler and less flexible.
The declarative model and the reconciliation loop
Kubernetes does not execute orders: it compares states. You declare the desired state and a set of controllers works continuously so that the actual state matches it.
graph LR
U["kubectl apply -f<br/>manifest.yaml"] --> API["API server<br/>control plane"]
API --> ETCD[("etcd<br/>desired state")]
API --> CM["Controllers<br/>(Deployment, ReplicaSet...)"]
CM -->|"compares desired<br/>with actual"| API
CM --> SCH["Scheduler<br/>picks a node"]
SCH --> KL["kubelet on the node"]
KL --> POD["Running pods"]
POD -->|"actual state"| API
API -.->|"difference detected"| CM
The practical consequence is that deleting a pod by hand achieves nothing: the ReplicaSet notices replicas are missing and creates another one within seconds. It is the same logic as the desiredCount in ECS, generalised to every object in the system and exposed as an API anyone can extend.
And the cultural consequence is more important: in Kubernetes the manifest is the infrastructure. This fits naturally with what you learned in module 9 and leads to GitOps: a Git repository is the source of truth and an agent in the cluster (Argo CD or Flux) applies whatever is in it, reverting any manual change. It is a more rigorous way of working than module 8's, and also one more piece to install, upgrade and operate.
What EKS manages and what is still yours
Amazon EKS is managed Kubernetes: AWS operates the control plane and you operate everything else. The concrete list matters, because almost every disappointment with EKS comes from expecting more than it offers.
| AWS manages | Detail |
|---|---|
| The control plane | API server, scheduler, controllers and etcd, replicated across three AZs |
| Its availability and scaling | It sizes itself; there are no masters to administer |
etcd backups |
Automatic; you neither see nor manage them |
| The version upgrade process | One button or one command, with pre-flight validations |
| The IAM integration | Authentication of AWS users and roles against the cluster |
| The managed add-ons | VPC CNI, CoreDNS, kube-proxy, EBS CSI and others, with compatible versions |
| Still yours | Detail |
|---|---|
| The nodes | Their type, their AMI, their patching and their replacement (except with Fargate or Auto Mode) |
| Deciding when to upgrade | And testing it, and running it, three or four times a year |
| The compatibility of your manifests | Kubernetes APIs are retired between versions and break things |
| All the cluster software | Load balancer controller, autoscaler, secrets controller, metrics |
| The network | VPC, subnets, security groups, IP allocation to pods |
| Security inside the cluster | RBAC, network policies, pod security policies |
| Observability | Metrics, logs and traces, just as on ECS |
The sentence to remember: EKS manages the control plane, not the cluster. A freshly created EKS cluster cannot deploy anything useful: it is missing the load balancer controller, the autoscaler, the secrets controller and the metrics. That distance between "cluster created" and "cluster operational" is where most of the work lives.
Compute options: nodes, Fargate and Auto Mode
The pods have to run somewhere, and EKS offers four models.
| Option | Who manages the nodes | Patching | Scaling | When it makes sense |
|---|---|---|---|---|
| Managed node groups | AWS creates and replaces them; you trigger the update | Guided by AWS, with no outage if you configure it | Cluster Autoscaler or Karpenter | The general case |
| Self-managed nodes | You, with an ASG of your own | All yours | Yours | Custom AMI, very specific requirements |
| EKS on Fargate | Nobody: there are no nodes | None | Per pod | Isolated workloads, no DaemonSet |
| EKS Auto Mode | AWS manages nodes, scaling and patches | AWS, with automatic recycling | Built in, based on Karpenter | Anyone who wants EKS with minimal operations |
EKS on Fargate works with Fargate profiles: you declare which namespaces or labels make a pod run without a node. It has important limitations you need to know about before relying on it: it does not support DaemonSets, nor persistent storage other than EFS, nor hostPort, nor privileged pods, and it runs one pod per Fargate task with the resource rounding that implies. The practical consequence is that many add-ons — which are installed as DaemonSets — cannot run on Fargate, so you almost always need at least a small node group for them.
EKS Auto Mode is AWS's answer to the argument in this lesson: it manages the nodes, the scaling, the patching and several essential add-ons, recycling the nodes automatically at intervals. It cuts operational work considerably in exchange for a surcharge on the compute price and less control. What it does not remove is the part that weighs most: you still decide and carry out the Kubernetes version upgrades and you are still responsible for your manifests being compatible.
Creating a cluster with eksctl and with CDK
eksctl is the community's official tool and the quickest route. It is declarative: you give it a file and it builds the VPC — or uses yours — the control plane, the node groups and the add-ons.
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: eks-mercadofresco
region: eu-west-1
version: "1.31"
tags:
Proyecto: mercadofresco
Entorno: desarrollo
Componente: orquestacion
Propietario: plataforma
CentroCoste: tecnologia
vpc:
id: vpc-mercadofresco # the module 3 VPC is reused
subnets:
private:
eu-west-1a: { id: snet-mercadofresco-app-a }
eu-west-1b: { id: snet-mercadofresco-app-b }
public:
eu-west-1a: { id: snet-mercadofresco-publica-a }
eu-west-1b: { id: snet-mercadofresco-publica-b }
clusterEndpoints:
publicAccess: true # restricted by CIDR below
privateAccess: true
publicAccessCIDRs: ["203.0.113.0/24"] # corporate network only
managedNodeGroups:
- name: ng-mercadofresco-general
instanceTypes: ["m6g.large"] # Graviton, as in 10-02
amiFamily: AmazonLinux2023
minSize: 2
maxSize: 8
desiredCapacity: 3
privateNetworking: true # nodes in private subnets only
volumeSize: 50
volumeType: gp3
labels: { rol: general }
updateConfig: { maxUnavailablePercentage: 25 }
addons:
- name: vpc-cni
version: latest
configurationValues: '{"env":{"ENABLE_PREFIX_DELEGATION":"true"}}'
- name: coredns
- name: kube-proxy
- name: aws-ebs-csi-driver
- name: eks-pod-identity-agent
iam:
withOIDC: true # essential for IRSA
serviceAccounts:
- metadata: { name: aws-load-balancer-controller, namespace: kube-system }
wellKnownPolicies: { awsLoadBalancerController: true }
cloudWatch:
clusterLogging:
enableTypes: ["api", "audit", "authenticator"]Four decisions in that file deserve attention:
publicAccessCIDRsrestricted. By default the API server endpoint is reachable from any IP on the internet — protected by authentication, but exposed. Restricting it to the corporate network, or moving to private access only, is one of the first things an audit looks at.privateNetworking: true. Nodes in private subnets, consistent with everything MercadoFresco did in module 3 and with what you learned in 10-02 about VPC endpoints. The same endpoints from the previous lesson are needed here.withOIDC: true. Without the OIDC provider there is no IRSA, and without IRSA the only way to give a pod AWS permissions is the node role, which is a serious flaw: every pod on that node would inherit those permissions.ENABLE_PREFIX_DELEGATION. Without this, each pod consumes a secondary IP on the node's ENI and the pods-per-node limit is reached long before CPU or memory runs out. It is the EKS equivalent ofawsvpcTrunkingfrom 10-01, and it catches people out just as much.
In CDK (09-02), the equivalent is shorter but demands the same understanding:
const cluster = new eks.Cluster(this, 'MercadoFrescoEks', {
clusterName: 'eks-mercadofresco',
version: eks.KubernetesVersion.V1_31,
kubectlLayer: new KubectlV31Layer(this, 'CapaKubectl'),
vpc,
vpcSubnets: [{ subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS }],
defaultCapacity: 0, // nodes are declared separately
authenticationMode: eks.AuthenticationMode.API, // access entries, no aws-auth
clusterLogging: [eks.ClusterLoggingTypes.API, eks.ClusterLoggingTypes.AUDIT],
});
cluster.addNodegroupCapacity('General', {
instanceTypes: [new ec2.InstanceType('m6g.large')],
amiType: eks.NodegroupAmiType.AL2023_ARM_64_STANDARD,
minSize: 2, maxSize: 8, desiredSize: 3,
subnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS },
});
// Manifests are infrastructure too: they are deployed with the stack.
cluster.addHelmChart('LoadBalancerController', {
chart: 'aws-load-balancer-controller',
repository: 'https://aws.github.io/eks-charts',
namespace: 'kube-system',
values: { clusterName: cluster.clusterName, serviceAccount: { create: false, name: 'aws-load-balancer-controller' } },
});kubectl and access to the cluster
kubectl is the client. It is configured with one command that writes the context into ~/.kube/config using the current AWS credentials:
aws eks update-kubeconfig --name eks-mercadofresco --region eu-west-1
kubectl get nodes # the 3 nodes in Ready
kubectl get pods -A # the system pods: coredns, aws-node, kube-proxy
kubectl config current-context # ALWAYS check which cluster you are pointing atThe last command looks trivial and is not: kubectl points at the last cluster configured, and kubectl delete deployment tienda does not ask which environment you are in. It is the cdk destroy --all of 09-02 in another shape. The discipline that prevents it is twofold: separate accounts per environment (09-04), so that development credentials cannot touch the production cluster, and an indicator of the active context in the terminal prompt.
Authentication: access entries versus aws-auth
Kubernetes has its own permission system, RBAC, with Role, ClusterRole, RoleBinding and ClusterRoleBinding. EKS connects that system to IAM: IAM says who you are, RBAC says what you can do inside the cluster.
Historically that connection was made through a ConfigMap called aws-auth, and it was a notorious source of incidents: one syntax error while editing it could lock everybody out of the cluster irreversibly, with no way out but to recreate it. Today the correct approach is access entries, which are an AWS API: they are created with the CLI or with CDK, audited in CloudTrail and do not depend on editing YAML by hand.
# The platform team, cluster administrators
aws eks create-access-entry --cluster-name eks-mercadofresco \
--principal-arn arn:aws:iam::333344445555:role/AWSReservedSSO_AdministracionPlataforma_abc \
--type STANDARD
aws eks associate-access-policy --cluster-name eks-mercadofresco \
--principal-arn arn:aws:iam::333344445555:role/AWSReservedSSO_AdministracionPlataforma_abc \
--access-scope type=cluster \
--policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSClusterAdminPolicy
# The developers, only in their namespace and unable to delete the cluster
aws eks associate-access-policy --cluster-name eks-mercadofresco \
--principal-arn arn:aws:iam::333344445555:role/AWSReservedSSO_DesarrolloCompleto_def \
--access-scope type=namespace,namespaces=mercadofresco \
--policy-arn arn:aws:eks::aws:cluster-access-policy/AmazonEKSEditPolicyThe permission sets are the ones from 09-04: AdministracionPlataforma, DesarrolloCompleto and AnalisisDatos. The correspondence comes out clean: the same identity mechanism that governs the five accounts also governs the cluster, with no parallel Kubernetes users and no separate credentials. It is one of the genuine advantages of EKS over self-managed Kubernetes.
IRSA and EKS Pod Identity: AWS permissions for a pod
The exact equivalent of the ECS task role (10-01). A shop pod needs to send to cola-mercadofresco-pedidos and read mercadofresco-carritos, and it needs to do so with its own permissions, not the node's.
| Mechanism | How it works | Advantages | Drawbacks |
|---|---|---|---|
| Node role | Every pod inherits the instance's role | Zero configuration | Unacceptable: any pod can do whatever any other pod does |
| IRSA (roles for service accounts) | The cluster's OIDC provider plus a trust policy that references it | Mature, works everywhere | Somewhat laborious per-role configuration, tied to one cluster |
| EKS Pod Identity | An agent on the node and an association between service account and role | Simpler, reusable across clusters, no OIDC | Requires the add-on; does not apply to EKS on Fargate |
With Pod Identity, which is the recommended option today:
aws eks create-pod-identity-association --cluster-name eks-mercadofresco \
--namespace mercadofresco --service-account sa-tienda \
--role-arn arn:aws:iam::333344445555:role/rol-pod-mercadofresco-tiendaAnd the role's trust policy, which is what changes compared with a normal role:
{"Version": "2012-10-17", "Statement": [{
"Effect": "Allow",
"Principal": { "Service": "pods.eks.amazonaws.com" },
"Action": ["sts:AssumeRole", "sts:TagSession"]
}]}The permissions policy is literally the same as the ECS task role's in 10-01: sqs:SendMessage on the queue, dynamodb:*Item on the carts table and s3:GetObject on the catalogue photos. The same least-privilege principle, a different way of anchoring it to the workload.
The manifests for MercadoFresco's shop
Here is what in ECS was a task definition plus a service. We start with the Namespace, the ServiceAccount and the ConfigMap:
apiVersion: v1
kind: Namespace
metadata:
name: mercadofresco
labels: { proyecto: mercadofresco, entorno: desarrollo }
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: sa-tienda
namespace: mercadofresco
# No annotations: with Pod Identity the association is made from the AWS API.
---
apiVersion: v1
kind: ConfigMap
metadata:
name: cfg-tienda
namespace: mercadofresco
data:
ENTORNO: "desarrollo"
COLA_PEDIDOS: "cola-mercadofresco-pedidos"
TABLA_CARRITOS: "mercadofresco-carritos"The Deployment, which is the heart of the deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: tienda
namespace: mercadofresco
labels: { app: tienda, proyecto: mercadofresco }
spec:
replicas: 3
revisionHistoryLimit: 5 # how many revisions can be rolled back to
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0 # equivalent to ECS minimumHealthyPercent 100
maxSurge: 2 # equivalent to maximumPercent 200
selector:
matchLabels: { app: tienda }
template:
metadata:
labels: { app: tienda, proyecto: mercadofresco }
spec:
serviceAccountName: sa-tienda
# Spreads the pods across AZs: the equivalent of spread by AZ in ECS.
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels: { app: tienda }
securityContext:
runAsNonRoot: true
runAsUser: 10001
fsGroup: 10001
terminationGracePeriodSeconds: 30 # equivalent to ECS stopTimeout
containers:
- name: tienda
image: 555566667777.dkr.ecr.eu-west-1.amazonaws.com/mercadofresco/tienda@sha256:9c1e...f4a2
ports:
- containerPort: 8080
name: http
envFrom:
- configMapRef: { name: cfg-tienda }
env:
- name: BD_CONTRASENA
valueFrom:
secretKeyRef: { name: sec-tienda-rds, key: password }
resources:
requests: { cpu: "500m", memory: "1Gi" } # what the scheduler reserves
limits: { cpu: "1000m", memory: "2Gi" } # the hard ceiling
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
volumeMounts:
- { name: temporal, mountPath: /tmp }
startupProbe: # time to start without liveness killing it
httpGet: { path: /salud, port: 8080 }
periodSeconds: 5
failureThreshold: 12 # up to 60 s for the first start-up
readinessProbe: # decides whether it receives traffic
httpGet: { path: /salud, port: 8080 }
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
livenessProbe: # decides whether the container restarts
httpGet: { path: /salud, port: 8080 }
periodSeconds: 20
timeoutSeconds: 3
failureThreshold: 3
lifecycle:
preStop:
exec: { command: ["sleep", "5"] } # margin for the ALB to deregister
volumes:
- name: temporal
emptyDir: { sizeLimit: 1Gi }The points to understand in that manifest:
- The three probes are three different questions and confusing them causes incidents.
startupProbeanswers "has it finished starting?" and suspends the other two until it succeeds.readinessProbeanswers "can it take traffic now?" and its failure takes the pod out of the load balancing without killing it.livenessProbeanswers "is it hung?" and its failure restarts the container. The classic mistake is pointinglivenessProbeat a/saludthat queries Aurora: when the database is slow, Kubernetes restarts every pod at once and turns a latency problem into a total outage. requestsandlimitsare not the same thing.requestsis what the scheduler reserves in order to decide which node the pod fits on;limitsis the ceiling. If CPU goes over the limit, the pod is throttled (it slows down); if memory goes over it, the kernel kills it with OOM and you will seeOOMKilledin the events. Settingrequestsfar below real usage produces overcommitted nodes that fall over together.maxUnavailable: 0withmaxSurge: 2is exactly ECS's100/200: there is never less healthy capacity than desired, at the price of paying for a few minutes of double capacity during the deployment.preStopwith a short wait solves the same problem as target group draining: when Kubernetes decides to terminate a pod, the ALB can take a few seconds to stop sending it traffic. Without that wait, requests are lost on every deployment.- The referenced
Secretis not written by hand. MercadoFresco uses the secrets CSI driver with the AWS provider, which mountsmercadofresco/produccion/rds/mfadminfrom Secrets Manager and optionally syncs it as a KubernetesSecret. That way the secret does not live in Git or inetcdin base64.
The Service, which gives the pods a stable name:
apiVersion: v1
kind: Service
metadata:
name: svc-tienda
namespace: mercadofresco
spec:
type: ClusterIP # internal only: the Ingress is what exposes it outside
selector: { app: tienda }
ports:
- port: 80
targetPort: 8080
name: httpIngress and the AWS Load Balancer Controller
An Ingress on its own does nothing: it is a declaration that needs a controller to translate it into a real load balancer. On EKS, that controller is the AWS Load Balancer Controller, which creates and configures a genuine ALB (03-03) from the annotations.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: ing-tienda
namespace: mercadofresco
annotations:
alb.ingress.kubernetes.io/scheme: internet-facing
alb.ingress.kubernetes.io/target-type: ip # straight to the pod, no hop via the node
alb.ingress.kubernetes.io/listen-ports: '[{"HTTPS":443}]'
alb.ingress.kubernetes.io/ssl-redirect: '443'
alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:eu-west-1:333344445555:certificate/abc-123
alb.ingress.kubernetes.io/healthcheck-path: /salud
alb.ingress.kubernetes.io/healthcheck-interval-seconds: '15'
alb.ingress.kubernetes.io/wafv2-acl-arn: arn:aws:wafv2:eu-west-1:333344445555:regional/webacl/waf-mercadofresco-cdn/abc
alb.ingress.kubernetes.io/tags: Proyecto=mercadofresco,Entorno=desarrollo,Componente=tienda,Propietario=plataforma,CentroCoste=tecnologia
alb.ingress.kubernetes.io/group.name: mercadofresco # several Ingresses share one ALB
spec:
ingressClassName: alb
rules:
- host: tienda.mercadofresco.example
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: svc-tienda
port: { number: 80 }Three observations that connect back to earlier modules. target-type: ip makes the ALB send traffic straight to the pod's IP, without going through the node's NodePort: it is the exact equivalent of the ip-type target group from 10-01, and it saves a network hop. group.name lets several Ingresses share a single ALB, which matters because each ALB costs around 17 USD a month in fixed cost and creating one per service adds up fast. And the WAF and certificate annotations show that, underneath, it is still the same ALB from module 3 with the same protections from module 4: EKS changes how it is declared, not what you get.
Auto scaling: HPA, Cluster Autoscaler and Karpenter
In Kubernetes there are two kinds of scaling and they are independent: pod scaling and node scaling. Confusing them is the cause of the complaint "I have configured auto scaling and it does not scale".
The HorizontalPodAutoscaler is the equivalent of ECS service auto scaling:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: hpa-tienda
namespace: mercadofresco
spec:
scaleTargetRef: { apiVersion: apps/v1, kind: Deployment, name: tienda }
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource: { name: cpu, target: { type: Utilization, averageUtilization: 65 } }
behavior:
scaleUp:
stabilizationWindowSeconds: 30 # grow fast, as in 10-01
policies: [{ type: Percent, value: 100, periodSeconds: 30 }]
scaleDown:
stabilizationWindowSeconds: 300 # shrink slowly
policies: [{ type: Percent, value: 25, periodSeconds: 60 }]Scaling on requests per target — which 10-01 showed to be better than CPU — additionally requires installing KEDA or the CloudWatch metrics adapter here, because the native HPA only understands CPU and memory. It is a small, representative example of the general pattern: what in ECS is a parameter, in Kubernetes is usually one more component to install and maintain.
For the nodes there are two options:
| Cluster Autoscaler | Karpenter | |
|---|---|---|
| How it decides | Adjusts the size of predefined node groups | Picks the right instance type for the pending pods |
| Speed | Minutes | Seconds to launch; the start-up is still EC2's |
| Efficiency | Limited by the group's instance types | High: it consolidates and picks the optimal size |
| Spot | Possible, with separate groups | Native, with replacement when the notice arrives |
| Complexity | Low, it is the traditional option | Medium: another controller to install and upgrade |
Karpenter is the recommended option today and it is genuinely good: it watches the pods that do not fit, decides which instance would host them best, launches it and, when there is spare capacity, consolidates the workloads and shuts nodes down. But it is worth seeing the whole picture: to get in EKS what comes built into Fargate — capacity appearing when it is needed and disappearing when it is not — you have to install, configure, upgrade and watch over one more controller. And even with Karpenter, the new pod waits for an EC2 instance to start: the two minutes MercadoFresco wanted to eliminate reappear whenever there is no room.
Add-ons: why an empty EKS is no use at all
A freshly created cluster has three things and is missing many. This is the real minimum list needed to run MercadoFresco's shop:
| Add-on | What for | Managed by AWS? |
|---|---|---|
VPC CNI (aws-node) |
Gives each pod a VPC IP | Yes |
| CoreDNS | DNS resolution inside the cluster | Yes |
| kube-proxy | Network rules for the Services | Yes |
| EKS Pod Identity Agent | AWS permissions per pod | Yes |
| EBS CSI / EFS CSI | Persistent volumes | Yes |
| AWS Load Balancer Controller | Turns an Ingress into an ALB | No: you install and upgrade it |
| Secrets Store CSI + AWS provider | Brings secrets in from Secrets Manager | No |
| Metrics Server | The metrics the HPA needs | No |
| Karpenter or Cluster Autoscaler | Node scaling | No |
| CloudWatch Observability | Container Insights and logs | Yes (an add-on) |
| KEDA (optional) | Scaling on external metrics | No |
| cert-manager (optional) | Internal certificates | No |
The five marked "No" are software MercadoFresco would have to install with Helm, version, test and upgrade, each with its own life cycle and its own compatibility matrix against the Kubernetes versions. None of them is hard on its own. The sum of them is, and it is the part of EKS that never appears in an afternoon's comparison.
On ECS, the equivalent of that entire table is: nothing. The load balancer is a service parameter, the secrets are a field in the task definition, the scaling is Application Auto Scaling and the metrics come with Container Insights.
Version upgrades: the hidden cost
This is the central argument of the lesson and the one that weighs most in the final decision.
Kubernetes releases about three minor versions a year. EKS gives standard support for about 14 months per version; after that it automatically enters extended support, which lasts about 12 months more and costs 0.60 USD per hour instead of 0.10, that is, around 438 USD a month per cluster instead of 73. When extended support ends, AWS upgrades the cluster itself.
What that means in real work, three or four times a year and per cluster:
- Read the release notes and spot the retired APIs. Kubernetes retires API versions between minor releases, and a manifest that used to work stops applying. Tools such as
kubenthelp find them, but the review is yours. - Check the compatibility of every add-on: the load balancer controller, Karpenter, the secrets CSI, KEDA, the CNI. Each has its own matrix, and sometimes the add-on has to be upgraded before the cluster.
- Upgrade the control plane, which takes around 25-40 minutes and cannot be rolled back.
- Upgrade the nodes, replacing instances in batches and draining pods, with the usual risk that a badly set
PodDisruptionBudgetblocks the drain. - Test all of it in a pre-production cluster first, which means maintaining that cluster, with its 73 USD a month control plane and the same add-ons.
| Activity | Frequency | Estimated effort |
|---|---|---|
| Minor version upgrade (pre-production and production) | 3 a year | 2-4 person-days each time |
| Out-of-cycle add-on upgrades | 6-10 a year | 2-4 hours each time |
| Tracking CVEs in the cluster components | Ongoing | 2-4 hours a month |
| Approximate annual total | 20-30 platform working days |
Twenty or thirty days a year is more than a month of one person's time. That is the hidden cost of EKS, and Auto Mode does not remove it: Auto Mode takes care of the nodes, but the Kubernetes versions, the compatibility of your manifests and the add-ons you install are still yours.
On ECS that work simply does not exist. There is no ECS version to upgrade; AWS evolves the service without anyone planning anything. It is the most honest comparison you can make between the two, and for a small platform team it is decisive.
Observability: Container Insights, Prometheus and Grafana
Everything from module 5 still applies; what changes is the ecosystem's native tooling.
- Container Insights is enabled with the CloudWatch observability add-on and gives cluster, node, pod and container metrics, plus the container logs in
/aws/containerinsights/eks-mercadofresco/application. It is the most direct route and the one that adds the fewest pieces. - Amazon Managed Service for Prometheus is managed Prometheus, compatible with
PromQL. It fits well because most of the ecosystem's components — the load balancer controller, Karpenter, KEDA — already expose metrics in Prometheus format. It is collected with the CloudWatch agent or the OpenTelemetry collector, and you pay per sample ingested and stored. - Amazon Managed Grafana is managed Grafana, with access through IAM Identity Center (09-04) and data sources pointing at Prometheus, CloudWatch and X-Ray. You pay per active user per month.
- The control plane logs —
api,audit,authenticator— are enabled on the cluster and go to CloudWatch Logs. The audit one is what answers "who deleted that Deployment", and it is worth enabling from day one because it is not retroactive.
The honest observation: the Prometheus plus Grafana combination is more powerful than CloudWatch for Kubernetes metrics, with better querying and a catalogue of ready-made dashboards. And it is also more pieces: two more services to configure, two more bills and two more places to look during an incident. For MercadoFresco, with the mercadofresco-produccion and -negocio dashboards already built in 05-01, the gain does not justify the fragmentation.
ECS versus EKS: the honest comparison
| Criterion | Amazon ECS | Amazon EKS |
|---|---|---|
| Learning curve | Days: five concepts | Weeks or months: dozens of objects and patterns |
| Control plane cost | 0 USD | 73 USD/month, or 438 with extended support |
| Portability | None: AWS only | Total: any cloud or your own servers |
| Ecosystem | Whatever AWS offers | Enormous: operators, GitOps, meshes, thousands of Helm charts |
| Extensibility | Very limited | CRDs and operators: the API itself is extended |
| AWS integration | Native and with no extra pieces | Good, but through controllers you install yourself |
| Upgrades | None: AWS does it | 3 a year, 20-30 working days annually |
| Add-ons to maintain | None | 5-8 components with their own cycle |
| People needed | Any team that knows AWS | Someone who knows Kubernetes, not someone learning it |
| Job market | Smaller | Broad: it is a transferable skill |
| Fargate | Yes, fully integrated | Yes, with limitations (no DaemonSet, no EBS) |
| Multi-cloud and hybrid | No | Yes, including on-premises installations |
| Speed to reach production | Days | Weeks |
| Ceiling of complexity it supports | Medium | Very high |
Neither column is "the good one". The correct reading is: EKS buys flexibility, portability and ecosystem, and pays for them with complexity, people and recurring work. If you need what it buys, the price is reasonable. If you do not need it, you have bought yourself work.
MercadoFresco's decision and what would change it
MercadoFresco stays on ECS with Fargate. And the reason should be written down, because a decision with no written arguments gets revisited every six months depending on the last conference somebody went to.
The arguments, in order of weight:
- The platform team is Marta and half of Luis. Twenty or thirty days a year of upgrades across two people who also handle the network, security, the pipeline and on-call is not an affordable cost: it would be 10-15 % of their total capacity spent maintaining the orchestrator rather than the shop.
- There is no requirement ECS does not cover. The shop is an HTTP service with external state, some workers that consume queues and some event-driven functions. None of that needs CRDs, operators, service meshes or advanced pod scheduling.
- There is no multi-cloud requirement. MercadoFresco is on AWS and there is no plan and no commercial pressure to change that. Kubernetes portability is insurance, and insurance that is never going to be used is just a premium.
- Speed matters. Module 10 has taken the shop from EC2 to containers in weeks. With EKS, those same weeks would have gone on standing up the cluster, the add-ons and the team's learning, without having moved the shop yet.
- The direct cost counts too, even if it is the smallest argument: 73 USD a month per cluster per environment is around 220 USD a month for development, pre-production and production, against 0 USD for ECS. It is little money compared with the cost of people, but it is not zero.
And the objective criteria that would change the decision, written in advance so that any future review is a check rather than a debate:
| Criterion | Concrete threshold | Why it would tip the balance |
|---|---|---|
| Prior team experience | Two or more people with Kubernetes in production | The curve stops being a cost and becomes an asset |
| Multi-cloud or hybrid requirement | A contract, a regulation or a warehouse with local compute | It is the only thing ECS cannot deliver in any form |
| Need for ecosystem operators | A component that only exists as a Kubernetes operator | Reimplementing it costs more than adopting EKS |
| Size of the platform team | Four or more dedicated people | There is capacity to absorb the recurring work |
| Number of services | More than 30-40 services with different teams | Namespaces, RBAC and GitOps start to genuinely pay off |
| Advanced scheduling requirements | Complex affinities, shared GPUs, priority between workloads | The Kubernetes scheduler is far superior |
As long as none of those thresholds is met, the answer to "should we move to Kubernetes?" is no, and here is why. The day two of them are met, the answer changes, and this table says exactly when.
With one nuance that avoids the opposite mistake: Luis should learn Kubernetes anyway. It is a marketable skill and it gives you the judgement to assess other people's architectures. Learning it does not oblige you to adopt it, and adopting it without knowing it is the worst possible combination.
Cost and cleanup
| Item | Approximate monthly cost |
|---|---|
| Control plane, standard support | 73 USD per cluster |
| Control plane, extended support | 438 USD per cluster |
3 on-demand m6g.large nodes |
≈ 165 USD |
| EBS volumes for the nodes (3 × 50 GB gp3) | ≈ 12 USD |
| ALB created by the Ingress | ≈ 17 USD plus usage |
| Managed Prometheus and Grafana | From ~20 USD depending on usage and users |
| A minimal, realistic development cluster | ≈ 270-290 USD/month |
Multiplied by three environments, an EKS setup equivalent to MercadoFresco's current one would cost in the order of 800 USD a month against roughly 250 USD for ECS with Fargate and arm64 — and that comparison does not include the 20-30 platform days a year, which is the larger cost.
Cleanup, and here the order matters more than in any other lesson: the AWS resources created from inside the cluster — the Ingress's ALB, the EBS volumes of the PersistentVolumeClaims — are not deleted when the cluster is deleted and are left orphaned and billing.
# 1. FIRST the Kubernetes objects that create AWS resources
kubectl delete ingress --all -n mercadofresco # deletes the ALB
kubectl delete pvc --all -n mercadofresco # deletes the EBS volumes
kubectl delete svc --all -n mercadofresco # deletes any type LoadBalancer NLB
# 2. Check they really have gone before carrying on
aws elbv2 describe-load-balancers --query 'LoadBalancers[?contains(LoadBalancerName,`k8s`)].LoadBalancerArn'
aws ec2 describe-volumes --filters Name=status,Values=available --query 'Volumes[].VolumeId'
# 3. Now the whole cluster (eksctl deletes nodes, roles and the stack)
eksctl delete cluster --name eks-mercadofresco --region eu-west-1
# 4. Usual leftovers: log groups and the OIDC provider
aws logs delete-log-group --log-group-name /aws/eks/eks-mercadofresco/cluster
aws iam list-open-id-connect-providersIf you delete the cluster before the Ingresses, the controller that knew how to delete the ALB disappears with it and the load balancer carries on billing indefinitely. It is the most expensive and most frequent leftover in EKS.
Common Mistakes and Tips
Mistake: a livenessProbe that queries the database. When Aurora is slow, Kubernetes restarts every pod at once and a degradation turns into an outage. Tip: liveness should only check that the process responds; dependencies belong in readiness.
Mistake: not setting a startupProbe. An application that takes 40 seconds to start is restarted by liveness before it finishes, in a loop. Tip: a startupProbe with a generous failureThreshold; it suspends the other two until it succeeds.
Mistake: requests far below real usage. The nodes end up overcommitted and several pods fall over together. Tip: requests close to the real 95th percentile and limits with headroom, measured with Container Insights.
Mistake: granting AWS permissions through the node role. Every pod on the node inherits everything. Tip: Pod Identity or IRSA always, even in development, because what is done in development gets copied into production.
Mistake: editing aws-auth by hand. One syntax error locks everybody out of the cluster with no way back. Tip: use access entries (AuthenticationMode: API) and do not touch that ConfigMap.
Mistake: storing Kubernetes Secret objects in Git. Base64 is not encryption. Tip: the secrets CSI driver with Secrets Manager, and etcd encryption with KMS enabled when the cluster is created.
Mistake: deleting the cluster before the Ingresses. The ALB is left orphaned and billing. Tip: the cleanup order above, with verification between steps.
Mistake: letting the cluster drift into extended support unnoticed. The bill multiplies by six in silence. Tip: a calendar alarm at 12 months for each version and a per-account budget (11-04) that catches the jump.
Mistake: one ALB per Ingress. Ten services, ten load balancers, 170 USD a month. Tip: alb.ingress.kubernetes.io/group.name to share one ALB between Ingresses.
Mistake: choosing EKS for the CV. It is a real and sincere reason, and a bad technical one. Tip: if the team wants to learn Kubernetes, let them stand up a lab cluster with its own budget; production is decided with the criteria in the table.
Tip: if you choose EKS, choose Karpenter and Auto Mode from the start too. Starting with Cluster Autoscaler and migrating later is duplicated work.
Tip: enable the control plane audit logs on day one. They are not retroactive, and they are the only thing that answers "who deleted that Deployment".
Tip: set topologySpreadConstraints by zone on every production Deployment. Without them, the scheduler can put all three replicas in the same AZ and one zone going down leaves you with no service.
Exercises
Exercise 1: translating the workers service into Kubernetes
The cola-mercadofresco-pedidos workers run today on Fargate with 0.5 vCPU and 1 GB, scaling from 2 to 8 tasks according to queue backlog, with a 1:4 split between on-demand and Spot. Write the Kubernetes equivalent: (a) the complete Deployment, with the probes appropriate to a process with no HTTP server and with graceful shutdown; (b) how you solve auto scaling on queue depth, saying which additional component is needed and why the native HPA is not enough; (c) how you achieve the equivalent of the split between on-demand and Spot capacity; and (d) list every piece you have had to add to the cluster to achieve what in ECS were three parameters.
Exercise 2: the probe incident
On a Friday at 19:20, with the peak under way, Aurora suffers a degradation and queries go from 20 ms to 900 ms. Two minutes later the whole shop disappears: every pod is in CrashLoopBackOff, the ALB has no healthy targets and mercadofresco-alb-latencia-alta has given way to a total failure. The database, meanwhile, is still responding — slow, but alive. Explain (a) the exact causal chain that turned a degradation into a total outage; (b) which specific piece of manifest configuration is to blame and why on ECS, with the target group health check, the outcome would have been different; (c) how the three probes are redesigned so that this incident is a degradation and not an outage; (d) what mechanism you would add in the shop's code to degrade gracefully; and (e) which alarm would have warned sooner.
Exercise 3: revisiting the decision, eighteen months later
Eighteen months have passed. MercadoFresco has grown: it operates in four cities, it has 22 services instead of 3, the platform team is now 4 people of whom 2 have worked with Kubernetes in a previous job, and its largest wholesale customer requires by contract that its instance of the orders service can run in its own data centre. On top of that, the data team wants to use a Kubernetes operator to manage its processing pipelines. Apply the criteria table from the lesson and answer: (a) which thresholds are met now; (b) what your recommendation is and with what level of confidence; (c) if you recommend migrating, how you would do it without repeating the mistakes of the 09-04 migration plan; (d) what stays on ECS even if you migrate; and (e) what total annual cost you would estimate for the decision, including people.
Solutions
Solution 1
(a) The Deployment.
apiVersion: apps/v1
kind: Deployment
metadata: { name: trabajadores-pedidos, namespace: mercadofresco }
spec:
replicas: 2
selector: { matchLabels: { app: trabajadores-pedidos } }
template:
metadata: { labels: { app: trabajadores-pedidos } }
spec:
serviceAccountName: sa-trabajadores
terminationGracePeriodSeconds: 120 # equivalent to ECS stopTimeout 120
topologySpreadConstraints:
- { maxSkew: 1, topologyKey: topology.kubernetes.io/zone,
whenUnsatisfiable: ScheduleAnyway, labelSelector: { matchLabels: { app: trabajadores-pedidos } } }
containers:
- name: trabajador
image: 555566667777.dkr.ecr.eu-west-1.amazonaws.com/mercadofresco/trabajadores@sha256:71ab...c3d9
resources:
requests: { cpu: "500m", memory: "1Gi" }
limits: { cpu: "500m", memory: "1Gi" }
securityContext: { runAsNonRoot: true, runAsUser: 10002, readOnlyRootFilesystem: true }
# No HTTP: it runs a command that checks the heartbeat mark in /tmp.
livenessProbe:
exec: { command: ["python", "-m", "trabajadores.salud"] }
periodSeconds: 30
failureThreshold: 3
startupProbe:
exec: { command: ["python", "-m", "trabajadores.salud"] }
periodSeconds: 5
failureThreshold: 12
volumeMounts: [{ name: temporal, mountPath: /tmp }]
volumes: [{ name: temporal, emptyDir: {} }]There is no readinessProbe and that is deliberate: readiness decides whether a pod receives traffic from a Service, and nobody sends traffic to a queue worker. Adding one would be noise with no effect. liveness is essential, for the same reason as in 10-01: a hung worker does not die, it stops consuming the queue and nobody notices. Graceful shutdown is the same SIGTERM handler from that exercise, with terminationGracePeriodSeconds: 120 as the equivalent of the stopTimeout.
(b) Auto scaling on queue depth. The native HPA only understands CPU and memory from the resource metrics, and you have already seen that the CPU of a queue consumer does not reflect the backlog. You need KEDA, which installs an external metrics adapter and brings a specific scaler for SQS:
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata: { name: escalado-trabajadores, namespace: mercadofresco }
spec:
scaleTargetRef: { name: trabajadores-pedidos }
minReplicaCount: 2
maxReplicaCount: 8
cooldownPeriod: 300
triggers:
- type: aws-sqs-queue
metadata:
queueURL: https://sqs.eu-west-1.amazonaws.com/333344445555/cola-mercadofresco-pedidos
queueLength: "15" # messages per replica, as in 10-02
awsRegion: eu-west-1
identityOwner: operatorThe alternative without KEDA is the CloudWatch metrics adapter plus an HPA with an external metric, which is more hand-rolled. Either way, it is one more component to install, version and upgrade in order to get what in ECS was one Application Auto Scaling policy.
(c) The equivalent of the on-demand / Spot split. There is no capacityProviderStrategy; you get there with Karpenter, declaring two NodePool objects, one with capacity-type: on-demand and another with spot, and using a preferred nodeAffinity towards Spot in the Deployment plus topologySpreadConstraints on the karpenter.sh/capacity-type label to guarantee that at least one replica lands on on-demand. You also need the interruption handler, which Karpenter includes: it listens for EC2's two-minute notice and drains the node. Result: what in ECS were two lines of --capacity-provider-strategy here are two NodePools, affinity rules, topology constraints and a controller.
(d) The pieces added. To match three ECS parameters you have needed: Karpenter (nodes and Spot), KEDA (queue-based scaling), Metrics Server (the basis for the HPA), EKS Pod Identity Agent (the pod's SQS permissions) and, if you want equivalent observability, the CloudWatch Observability add-on. Five components, each with its version, its compatibility with the Kubernetes version and its own upgrade cycle. That is exactly the arithmetic of the add-ons section.
Solution 2
(a) The causal chain. Aurora degrades to 900 ms. The shop's /salud queries the database, so it starts taking longer than the 3 seconds of timeoutSeconds. The livenessProbe accumulates 3 consecutive failures — about 60 seconds with periodSeconds: 20 — and Kubernetes restarts the container. On restarting, the pod has to start up again, preload the catalogue cache and reconnect to the same slow Aurora, so the start-up fails as well. Since all the pods query the same database, this happens to all three at once and in lockstep. The repeated restarts lead to CrashLoopBackOff, with growing waits between attempts. And there is an aggravating factor: the mass restarts open and close connections against an already saturated Aurora, making the root cause worse. A complete feedback loop.
(b) The culprit and the difference with ECS. The culprit is the livenessProbe pointing at a /salud that checks external dependencies. In ECS, the target group health check has a different effect: an unhealthy target is taken out of the load balancing, and the service only replaces tasks if the task dies or if the deployment requires it. The ECS equivalent of liveness is the container's healthCheck, which most people configure more loosely or do not set at all. In other words: Kubernetes is more aggressive by default and gives you a more dangerous tool, and that is why it demands understanding. The difference is not that ECS is better, it is that Kubernetes lets you shoot further.
(c) Redesigning the three probes.
livenessProbe→/vivo, an endpoint that only confirms that the process responds and the event loop is not blocked. Without touching Aurora, without touching the cache, without touching the network. WithperiodSeconds: 20andfailureThreshold: 5, so that a transient spike does not cause a restart.readinessProbe→/listo, which does check the dependencies: connections to Aurora and to ElastiCache. Its failure takes the pod out of the load balancing but does not kill it, so when Aurora recovers the pod goes back to receiving traffic without having restarted. Here failure is reversible and cheap.startupProbe→/listowith a widefailureThreshold(12 × 5 s = 60 s), covering the catalogue preload and suspending the other two in the meantime.
With that design, the Friday incident would have been: every pod marked not ready, the ALB returning 503 on the requests that need the database, the pods alive and with their cache warm, and immediate recovery once Aurora recovered. A degradation, not an outage.
(d) Graceful degradation in the code. Have /listo distinguish between "I can do nothing" and "I can do part of it". The shop can serve the catalogue from mercadofresco-catalogo in ElastiCache even if Aurora is slow; what it cannot do is confirm orders. With a circuit breaker (07-05) around the calls to Aurora, the shop stops trying after N failures, returns a clear message at the checkout step and carries on serving browsing. The customer sees "we cannot confirm orders right now" instead of a blank page, and Aurora stops receiving the avalanche of retries that was preventing its recovery.
(e) The alarm that would have warned sooner. The Aurora latency one, ReadLatency and WriteLatency, with the threshold on the 99th percentile and not on the average — the average takes far too long to move. It would have fired a minute into the degradation, before the first livenessProbe had accumulated its three failures, giving real room to act. In addition, an alarm on pod restarts (pod_number_of_container_restarts in Container Insights) would have identified the feedback loop immediately, which is the piece of information that was missing to diagnose it in the heat of the moment.
Solution 3
(a) Thresholds now met. Four of the six:
| Criterion | Met? | Detail |
|---|---|---|
| Prior experience | Yes | 2 of 4 people with Kubernetes in production |
| Multi-cloud or hybrid | Yes, and contractually | The wholesaler requires execution in its data centre |
| Ecosystem operators | Yes | The data team wants a pipeline operator |
| Team size | Yes | 4 dedicated people |
| Number of services | Partly | 22, below the 30-40 threshold but close |
| Advanced scheduling | No | Nothing requires it yet |
(b) Recommendation. Yes, migrate to EKS, with high confidence. The decisive criterion is the second one: the contractual hybrid requirement is the only thing ECS cannot satisfy in any way, and it is not a preference but an obligation to a customer. The other three reinforce that the cost is now affordable: there is experience, there is a team and there is a concrete ecosystem need. The decision taken eighteen months ago was not wrong: it was correct given the data available then, and that is precisely the value of having written the criteria down in advance.
(c) How to migrate. By applying the lessons of 09-04 and of module 10 itself:
- A new cluster, not a conversion. The development cluster first, with Auto Mode or Karpenter from the start so as not to duplicate work.
- A non-critical service first. The reporting or notifications service, not the shop. You learn on something whose failure does not cost orders.
- The add-ons before the workloads, with their versions pinned in Git and deployed with Helm from the pipeline, never by hand.
- Real, extended coexistence. ECS and EKS side by side for months, with the ALB splitting by weights just as in 10-01 and 10-02. No overnight cutover.
- The shop last, and with the same 90/10 split used to move from EC2 to Fargate.
- An explicit deadline per phase, which is the mistake 09-04 pointed out: half-finished migrations last years if nobody sets milestones.
- The wholesale customer's installation at the end, once the team already operates EKS comfortably, not as the first project.
(d) What stays on ECS even if you migrate. Whatever gains nothing from the change: the Lambdas (-cobrar-pago, -reservar-stock, -generar-miniaturas, -asignar-reparto), which are not containers and remain the best option for sporadic work; the scheduled tasks that load into Redshift, which work perfectly with EventBridge Scheduler; and probably the workers on the less critical queues for quite some time, because migrating them adds nothing and does consume attention. Migrating everything for aesthetic consistency is an expensive mistake: consistency is measured in criteria, not in technologies.
(e) Estimated annual cost. Infrastructure: 3 clusters × 73 USD × 12 = 2,628 USD of control planes, plus the node differential against Fargate, which with Karpenter and Spot used well may even be favourable — call it neutral. People: 25 days a year of platform work on upgrades and cluster maintenance, plus a one-off cost of 2-3 person-months for the migration. With an estimated internal cost of 400 USD a day, the recurring part is around 10,000 USD a year and the migration around 20,000-25,000 USD one-off. Total for the first year: in the order of 35,000 USD. That is the number to put in front of management alongside the wholesale customer's contract, because the decision is not a technical one: it is an investment with a concrete commercial justification.
Conclusion
MercadoFresco has finished the module with the shop off the machines and with a reasoned answer to the Kubernetes question.
You understand Kubernetes properly: an extensible orchestrator with an API of its own, whose real value lies in portability, the ecosystem and extensibility with CRDs and operators; its minimum vocabulary — Pod, ReplicaSet, Deployment, Service, Ingress, Namespace, ConfigMap and Secret, with the warning that a Secret is base64 and not encryption; and the declarative model with its reconciliation loop that explains why deleting a pod by hand achieves nothing and why GitOps fits so naturally. You are clear on what EKS manages — a control plane across three AZs, etcd, the upgrade mechanism and the IAM integration — and on the sentence that avoids most disappointments: EKS manages the control plane, not the cluster.
You have the four compute options with their real limits — including the fact that EKS on Fargate does not support DaemonSets, which forces you to keep nodes for the add-ons — cluster creation with eksctl and with CDK, aws eks update-kubeconfig and the discipline of checking the context before typing a delete. And modern authentication: access entries instead of aws-auth, with the permission sets from 09-04 governing the cluster too, and Pod Identity as the exact equivalent of the ECS task role.
You have the complete manifests for the shop: a Deployment with the three probes properly separated — startup to get going, readiness to receive traffic, liveness to restart — requests and limits as distinct concepts, spreading across AZs with topologySpreadConstraints, preStop so as not to lose requests and an unprivileged security context; the Service; and the Ingress with the AWS Load Balancer Controller, which creates the same ALB from module 3 with the WAF from module 4, with target-type: ip and group.name so as not to pay for one load balancer per service. Plus auto scaling on two planes: HPA for the pods, Karpenter for the nodes, and the observation that matching three ECS parameters took five components.
And you have the argument almost nobody writes down: the hidden cost of upgrades. Three versions a year, 14 months of standard support, an automatic jump from 73 to 438 USD a month on entering extended support, and 20-30 platform working days a year reviewing retired APIs, compatibility matrices and node drains. On ECS that work does not exist. Together with the honest comparison between the two — EKS buys flexibility, portability and ecosystem, and pays for them with complexity, people and recurring work — and MercadoFresco's decision: it stays on ECS with Fargate, because of a two-person platform team, the absence of requirements ECS does not cover, the absence of any multi-cloud need and speed; with the six objective criteria, written in advance, that would change that decision the day they are met.
What has changed for MercadoFresco in this module. There is no longer an AMI to rebuild when a dependency changes: there is an image in mercadofresco/tienda versioned per commit and deployed by digest. There is no longer an operating system to patch: the host belongs to AWS and Inspector's continuous scanning warns about what is genuinely yours. And there are no longer two-minute start-ups: there are tasks that take traffic in forty seconds, with scheduled scaling that also has them ready before the Friday peak arrives.
graph TB
U[Customers] --> CF["CloudFront E2QWERTY123ABC<br/>+ waf-mercadofresco-cdn"]
CF --> ALB["alb-mercadofresco-tienda<br/>tg-mercadofresco-tienda / -verde"]
ALB --> FG["ECS Fargate<br/>svc-mercadofresco-tienda-fg<br/>arm64, 2-20 tasks"]
FG --> AU[("aurora-mercadofresco-pedidos")]
FG --> EC[("mercadofresco-catalogo<br/>ElastiCache")]
FG --> DY[("mercadofresco-carritos")]
FG --> SQS["cola-mercadofresco-pedidos"]
SQS --> TR["ECS Fargate + Spot<br/>svc-mercadofresco-trabajadores"]
TR --> SF["mercadofresco-procesar-pedido<br/>payment, stock and delivery Lambdas"]
ECR["ECR 555566667777<br/>mercadofresco/tienda"] -.digest.-> FG
PIPE["pipeline-mercadofresco-tienda<br/>blue/green canary"] -.deploys.-> FG
OBS["Container Insights, X-Ray<br/>mercadofresco-produccion"] -.observes.-> FG
And here comes the question nobody has asked yet and that the manager will ask on Monday morning. The architecture is complete: it is elastic, secure, observable, deployable from a pipeline, described in code and spread across five accounts. But how much does all this cost? Is it money well spent? How does it break down between production, pre-production and development, and between the four cities? Is there anything switched on that nobody uses? And what could be committed to in advance to pay less?
In module 11, "Best practices and cost management", that is answered methodically. 11-01 reviews the complete architecture with the Well-Architected Framework and its six pillars, which is the structured way to audit what has been built. 11-02 brings order to tagging and cost allocation, turning the five mandatory tags into reports somebody can actually read. 11-03 analyses the real bill with Cost Explorer. 11-04 sets limits and alerts with Budgets. 11-05 commits capacity with Savings Plans and reserved instances, which is where the Fargate versus EC2 comparison from 10-02 is finally settled. And 11-06 closes the course with the final project: designing, justifying and budgeting a complete architecture for MercadoFresco, with everything learned across eleven modules.
AWS Course
Module 1: Introduction to AWS
- What Is AWS?
- Setting Up Your AWS Account
- AWS Global Infrastructure
- The AWS Management Console
- AWS CLI and SDKs
Module 2: Core AWS Services
Module 3: Networking and Content Delivery
Module 4: Security and Identity
- AWS Identity and Access Management (IAM)
- AWS Key Management Service (KMS)
- Secrets Manager and Parameter Store
- AWS Shield
- AWS WAF
Module 5: Monitoring and Management
Module 6: Databases
Module 7: Application Integration
- Amazon SQS
- Amazon SNS
- Amazon EventBridge
- AWS Step Functions
- Integration Patterns: Idempotency, Retries and Dead-Letter Queues
