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

  1. What Kubernetes is and what it solves that ECS does not
  2. The bare minimum vocabulary, properly understood
  3. The declarative model and the reconciliation loop
  4. What EKS manages and what is still yours
  5. Compute options: nodes, Fargate and Auto Mode
  6. Creating a cluster with eksctl and with CDK
  7. kubectl and access to the cluster
  8. Authentication: access entries versus aws-auth
  9. IRSA and EKS Pod Identity: AWS permissions for a pod
  10. The manifests for MercadoFresco's shop
  11. Ingress and the AWS Load Balancer Controller
  12. Auto scaling: HPA, Cluster Autoscaler and Karpenter
  13. Add-ons: why an empty EKS is no use at all
  14. Version upgrades: the hidden cost
  15. Observability: Container Insights, Prometheus and Grafana
  16. ECS versus EKS: the honest comparison
  17. MercadoFresco's decision and what would change it
  18. Cost and cleanup
  19. Common mistakes and tips
  20. Exercises
  21. 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 apply against 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 Secret is 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 Deployment is not the service and the Service is 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"]
eksctl create cluster -f eks-mercadofresco.yaml     # 15 to 20 minutes

Four decisions in that file deserve attention:

  • publicAccessCIDRs restricted. 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 of awsvpcTrunking from 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 at

The 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/AmazonEKSEditPolicy

The 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-tienda

And 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. startupProbe answers "has it finished starting?" and suspends the other two until it succeeds. readinessProbe answers "can it take traffic now?" and its failure takes the pod out of the load balancing without killing it. livenessProbe answers "is it hung?" and its failure restarts the container. The classic mistake is pointing livenessProbe at a /salud that queries Aurora: when the database is slow, Kubernetes restarts every pod at once and turns a latency problem into a total outage.
  • requests and limits are not the same thing. requests is what the scheduler reserves in order to decide which node the pod fits on; limits is 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 see OOMKilled in the events. Setting requests far below real usage produces overcommitted nodes that fall over together.
  • maxUnavailable: 0 with maxSurge: 2 is exactly ECS's 100/200: there is never less healthy capacity than desired, at the price of paying for a few minutes of double capacity during the deployment.
  • preStop with 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 Secret is not written by hand. MercadoFresco uses the secrets CSI driver with the AWS provider, which mounts mercadofresco/produccion/rds/mfadmin from Secrets Manager and optionally syncs it as a Kubernetes Secret. That way the secret does not live in Git or in etcd in 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: http

Ingress 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:

  1. 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 kubent help find them, but the review is yours.
  2. 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.
  3. Upgrade the control plane, which takes around 25-40 minutes and cannot be rolled back.
  4. Upgrade the nodes, replacing instances in batches and draining pods, with the usual risk that a badly set PodDisruptionBudget blocks the drain.
  5. 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 logsapi, 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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-providers

If 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: operator

The 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. With periodSeconds: 20 and failureThreshold: 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/listo with a wide failureThreshold (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:

  1. A new cluster, not a conversion. The development cluster first, with Auto Mode or Karpenter from the start so as not to duplicate work.
  2. A non-critical service first. The reporting or notifications service, not the shop. You learn on something whose failure does not cost orders.
  3. The add-ons before the workloads, with their versions pinned in Git and deployed with Helm from the pipeline, never by hand.
  4. 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.
  5. The shop last, and with the same 90/10 split used to move from EC2 to Fargate.
  6. An explicit deadline per phase, which is the mistake 09-04 pointed out: half-finished migrations last years if nobody sets milestones.
  7. 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.

© Copyright 2026. All rights reserved