In 10-02 we built a cluster with kubeadm and discovered everything it entails: preparing machines, configuring containerd, generating certificates that expire after a year, maintaining etcd's quorum, rehearsing restores and upgrading node by node. We ended with a clear warning: operating that in production demands a dedicated team with on-call duty.

In 10-05 we set up GitOps, and now the entire state of the Rutas Norte platform lives in Git. That raises a natural question: if the cluster is replaceable and rebuilds itself, why would we maintain it ourselves?

This lesson answers that question. We will look at what the provider manages and what remains yours, we will compare EKS, AKS and GKE on the criteria that genuinely decide things, we will close the identity federation left pending in 03-06, we will take advantage of spot instances for Rutas Norte's tolerant workloads, and we will finish with a reasoned recommendation.

Contents

  1. The shared responsibility model
  2. Comparing EKS, AKS and GKE
  3. Identity: IRSA and Workload Identity
  4. Nodes: managed groups, templates and spot instances
  5. What changes in what you already know
  6. Upgrading a managed cluster's version
  7. Real cost and the levers for saving
  8. Portability and vendor lock-in
  9. The decision for Rutas Norte
  10. Common mistakes and tips
  11. Exercises
  12. Conclusion

  1. The shared responsibility model

A managed cluster is not "Kubernetes without work". It is a different boundary between what the provider does and what you do.

flowchart TB
    subgraph prov["Provider's responsibility"]
        P1[apiserver, scheduler,<br/>controller-manager]
        P2[etcd: replicas,<br/>backups, encryption]
        P3[Control plane<br/>certificates]
        P4[High availability<br/>across zones]
        P5[Control plane<br/>patching]
    end
    subgraph yours["Your responsibility"]
        T1[Workloads<br/>and their resources]
        T2[Nodes: type, size,<br/>OS patching]
        T3[Network: VPC, subnets,<br/>NetworkPolicies]
        T4[RBAC, PSS,<br/>secrets]
        T5[Cost]
        T6[Backups of the DATA]
    end
    style prov fill:#e8ffe8
    style yours fill:#fff4e8

The full table, contrasted with kubeadm

Responsibility kubeadm (10-02) Managed
Control plane machines Yours The provider's
apiserver, scheduler, controller-manager Yours The provider's
etcd: deployment, quorum, backups Yours (etcdctl snapshot) The provider's
Control plane certificates Yours (kubeadm certs renew, expiring after a year) The provider's
Control plane high availability Yours (HAProxy, 3 nodes) The provider's
Control plane upgrades Yours (kubeadm upgrade apply) The provider's (you choose when)
Scaling the control plane with load Yours The provider's
apiserver audit log Yours (configure it) The provider's (you switch it on)
The nodes' operating system Yours Yours (provider images, you apply them)
Upgrading the kubelet on the nodes Yours Yours (assisted)
CNI Yours (install Calico) The provider's by default, replaceable by you
Workloads, probes, resources Yours Yours
RBAC, Pod Security Standards, policies Yours Yours
NetworkPolicies Yours Yours
Secrets and their management Yours Yours
Backups of the applications' data Yours (Velero, 05-06) Yours (Velero)
Application monitoring Yours (Prometheus, 07-03) Yours
Cost Yours Yours
Regulatory compliance and personal data Yours Yours

What you need to understand

The control plane disappears from your worries. The certificates expiring after a year, etcd's quorum, the snapshots, the HAProxy in front of the three control nodes: all of that goes away. That is roughly half of lesson 10-02.

Everything else stays the same. The nodes are still yours, with an operating system that has to be patched. RBAC is still yours. The NetworkPolicies are still yours. And if bookings-postgres loses data, that is your problem, not the provider's.

A very frequent mistake: "it is managed, so backups are taken care of". False. The provider backs up etcd (the objects' definitions), not your applications' volumes. You back up bookings-postgres's customer data yourself, with Velero or the provider's snapshots. We saw this in 05-06 and it still holds.

The golden rule: a managed cluster takes half the Kubernetes operations work off your hands. The other half, the half you studied in modules 2 to 9, remains entirely yours.

  1. Comparing EKS, AKS and GKE

Control plane cost

EKS AKS GKE
Monthly control plane cost ~$73 per cluster $0 on the free tier; ~$73 with an SLA ~$73 per cluster (one free per account)
SLA included 99.95% Only on the standard tier 99.95% (regional) / 99.5% (zonal)
Does it apply per cluster? Yes Yes Yes, except for the first

With three environments in separate clusters, that is around $220/month just for existing. It is one of the reasons why many companies separate environments by namespace within one cluster (as Rutas Norte does) rather than by cluster.

Warning: AKS's free tier has no financial SLA. For rutas-norte-pro that may be unacceptable, and then the cost evens out.

Version cycle and support window

EKS AKS GKE
Minor versions supported ~4 at once ~3 Depends on the channel
Standard support 14 months 12 months 14 months (regular channel)
Extended support (paid) Yes, up to 26 months Yes Yes
Automatic upgrades Patch versions only Configurable Yes, by channel
Release channels No No Rapid, regular, stable

GKE's channel system is a real operational difference: you choose "stable" and Google keeps the cluster on a proven version, upgrading it in the maintenance window you specify. It is the closest thing to never thinking about versions.

In EKS and AKS, the cycle is more manual. And if you let the version lapse, both end up forcing the upgrade, which is worse than planning it.

Networking model and pod IP allocation

This is the most important and least known technical difference, and the one that causes serious surprises in production.

EKS (VPC CNI) AKS (Azure CNI) AKS (kubenet) GKE (VPC-native)
Pod IP A real VPC one A real virtual network one Overlay with NAT A real VPC one (alias)
Does it consume IPs from your network? Yes, one per pod Yes, one per pod No Yes, from a secondary range
Pods per node Limited by the instance type Configurable, reserved in advance High Configurable
Performance Native, no overlay Native NAT, more latency Native
External resources reach the pod directly Yes Yes No Yes

The problem this causes in practice: on EKS, every pod consumes a real IP from your VPC. A t3.medium instance supports 3 network interfaces with 6 IPs each: 17 pods maximum, regardless of having free CPU and memory. If your subnet is a /24 (254 addresses), you run out of IPs for pods long before you run out of CPU.

# On EKS: check a node's real pod limit
kubectl get node ip-10-0-1-42.eu-west-1.compute.internal \
  -o jsonpath='{.status.allocatable.pods}{"\n"}'
17

And the symptom when they run out:

Warning  FailedCreatePodSandBox  2m  kubelet
  Failed to create pod sandbox: plugin type="aws-cni" failed:
  add cmd: failed to assign an IP address to container

A Pending pod with no shortage of CPU or memory. Baffling if you do not know the model.

Mitigations on EKS: enable prefix delegation mode (ENABLE_PREFIX_DELEGATION), which multiplies the available IPs per interface by 16; use larger instances; or plan generous VPC ranges from the outset (a /16, not a /24).

On AKS with classic Azure CNI, you have to reserve in advance maxPods × number of nodes addresses. Plan badly and you cannot scale. The overlay variant (Azure CNI Overlay) solves this and is the recommended option today.

On GKE with VPC-native, secondary alias IP ranges are used, which separates pod addressing from node addressing. It is the cleanest model of the three.

Identity integration

EKS AKS GKE
Mechanism IRSA (IAM Roles for Service Accounts) or Pod Identity Workload Identity (based on Entra ID) Workload Identity Federation
Basis An OIDC token projected into the pod An OIDC token An OIDC token
Maturity Very high (since 2019) High Very high (the oldest)
Configuration complexity Medium Medium Low

All three have converged on the same mechanism: OIDC. We go into it in section 3.

Node groups and automatic modes

EKS AKS GKE
Managed groups Yes Yes (node pools) Yes (node pools)
Node-free mode Fargate (per pod) Virtual nodes (ACI) Autopilot
Node autoscaler Cluster Autoscaler or Karpenter Cluster Autoscaler or NAP Built in or auto-provisioning
Automatic type provisioning With Karpenter Yes (NAP) Yes

GKE Autopilot deserves a mention of its own: it is a mode in which, from your point of view, there are no nodes at all. You declare pods with their requests and you pay for exactly that. There are no nodes to patch or to size, no DaemonSets of your own, no hostPath and no privileges. It is Kubernetes reduced to its workload interface.

  • In favour: zero node operations, exact billing for what you request, hardened security by default.
  • Against: strong restrictions (you cannot run Falco from 08-06 as is, nor certain DaemonSets), a higher unit cost if your pods are badly sized, less control.

AWS Fargate is similar but more limited: it does not support DaemonSets at all, nor persistent block storage, nor privileged. For notifications-worker it can fit; for bookings-postgres, no.

Autoscaling

EKS AKS GKE
Cluster Autoscaler You install it Built in, you enable it Built in
Advanced alternative Karpenter (recommended) NAP Auto-provisioning
Node start-up speed 40-90 s (Karpenter: ~40 s) 60-120 s 40-80 s
Automatic instance type selection Karpenter does NAP does Yes

Recalling 09-03: Karpenter does not work with predefined node groups, it picks the optimal instance type for the pending pods. If occupancy-reports needs 8 CPUs once a night, Karpenter brings up exactly the right instance, uses it and retires it. It is a notable difference in cost and in speed.

Managed add-ons and ecosystem

Add-on EKS AKS GKE
CNI Managed (VPC CNI) Managed Managed
CoreDNS, kube-proxy Managed Managed Managed
CSI driver Managed Managed Managed
Ingress controller AWS Load Balancer Controller (you install it) Application Gateway (managed) GKE Ingress (native)
Metrics and logs CloudWatch Container Insights Azure Monitor Cloud Operations (the most integrated)
Service mesh App Mesh (being retired) / Istio Managed Istio Anthos Service Mesh
Image scanning ECR scanning Defender for Containers Artifact Analysis
Policy None native Azure Policy Policy Controller

Summary table

Criterion EKS AKS GKE
Control plane cost Medium Low (free without an SLA) Medium (1 free)
Support window Long (14 m) Medium (12 m) Long (14 m)
Networking model Real IPs, limit per instance Flexible, requires planning The cleanest
Identity IRSA, very mature Workload Identity The simplest
Autoscaling Karpenter, the best Good Very good
Node-free mode Fargate (limited) Virtual nodes Autopilot, the best
Ease of operation Medium Medium-high High
Ecosystem and tooling The largest Good Very good
Integration with the rest of the cloud Very good Excellent if you already use Microsoft Very good
Learning curve High (AWS IAM) Medium Low

The honest conclusion: the best choice is nearly always the cloud where the rest of your infrastructure already lives. The technical difference between the three is smaller than the cost of operating in two clouds at once. If you are starting from scratch and only looking at Kubernetes, GKE is the most polished; if your company already lives in AWS, EKS with Karpenter is excellent; if your company is a Microsoft shop, AKS integrates without friction.

  1. Identity: IRSA and Workload Identity

Here we close what we flagged in 03-06.

The problem

bookings-api needs to read a fares file from a storage bucket and publish messages onto a queue. The traditional way:

# THE BAD WAY. Do not do this.
apiVersion: v1
kind: Secret
metadata:
  name: cloud-credentials
stringData:
  ACCESS_ID: "AKIAIOSFODNN7EXAMPLE"
  ACCESS_SECRET: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"

Problems: the credential is permanent, it has to be rotated by hand, it lives in a Secret (base64, not encrypted, 03-02), and if it leaks it works from anywhere in the world.

The solution: OIDC federation

All three providers converge on the same mechanism. The cluster exposes an OIDC issuer; the kubelet projects a short-lived signed token carrying the ServiceAccount's identity into the pod; the cloud provider trusts that issuer and exchanges the token for temporary credentials.

sequenceDiagram
    participant P as Pod bookings-api
    participant K as kubelet
    participant O as Cluster OIDC<br/>issuer
    participant I as Provider IAM
    participant S as Bucket / Queue

    K->>O: requests a token for the SA
    O-->>K: signed JWT token (1 h)
    K->>P: projects it into<br/>/var/run/secrets/.../token
    P->>I: "exchange this token for credentials"
    I->>O: verifies the signature against the public keys
    I-->>P: temporary credentials (15 min - 1 h)
    P->>S: accesses with those credentials

No permanent credential. Nothing to rotate. The token renews itself and only works for that ServiceAccount in that cluster.

IRSA on EKS

# 1. Create the cluster's OIDC identity provider (once only)
eksctl utils associate-iam-oidc-provider --cluster rutas-norte-pro --approve

# 2. Create the role and associate it with the ServiceAccount
eksctl create iamserviceaccount --name bookings-api --namespace rutas-norte-pro \
  --cluster rutas-norte-pro \
  --attach-policy-arn arn:aws:iam::123456789012:policy/RutasNorteFaresRead --approve

The magic is in the role's trust policy, whose condition limits who can assume it:

"Condition": { "StringEquals": {
  "oidc.eks.eu-west-1.amazonaws.com/id/EXAMPLE...:sub":
    "system:serviceaccount:rutas-norte-pro:bookings-api",
  "oidc.eks.eu-west-1.amazonaws.com/id/EXAMPLE...:aud": "sts.amazonaws.com"
}}

Note the sub: only the bookings-api ServiceAccount in the rutas-norte-pro namespace can assume this role. Not another SA, and not the same SA in another namespace.

# The manifest that goes into Git: one annotation, no secret
apiVersion: v1
kind: ServiceAccount
metadata:
  name: bookings-api
  namespace: rutas-norte-pro
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/RutasNorteBookingsApi

In the Deployment, serviceAccountName: bookings-api is enough: no credential variables, because the provider's SDK detects the projected token automatically. You can verify it with kubectl exec ... -- ls /var/run/secrets/eks.amazonaws.com/serviceaccount/.

Note: AWS has introduced EKS Pod Identity, which is simpler (an association in the EKS API, with no OIDC provider and no annotation). It is the recommended option for new clusters; IRSA is still needed if the workload runs outside EKS.

Workload Identity on GKE and on AKS

The mechanism is the same; the commands and the annotation change.

# --- GKE ---
gcloud container clusters update rutas-norte-pro \
  --workload-pool=rutas-norte-project.svc.id.goog --region=europe-west1
gcloud iam service-accounts create rn-bookings-api
gcloud iam service-accounts add-iam-policy-binding \
  [email protected] \
  --role roles/iam.workloadIdentityUser \
  --member "serviceAccount:rutas-norte-project.svc.id.goog[rutas-norte-pro/bookings-api]"

# --- AKS ---
az aks update -g rutas-norte -n rutas-norte-pro --enable-oidc-issuer --enable-workload-identity
ISSUER=$(az aks show -g rutas-norte -n rutas-norte-pro --query "oidcIssuerProfile.issuerUrl" -otsv)
az identity create -g rutas-norte -n rn-bookings-api
az identity federated-credential create --name rn-bookings-api-fed \
  --identity-name rn-bookings-api --resource-group rutas-norte --issuer "${ISSUER}" \
  --subject "system:serviceaccount:rutas-norte-pro:bookings-api"
EKS (IRSA) AKS GKE
Annotation on the SA eks.amazonaws.com/role-arn azure.workload.identity/client-id iam.gke.io/gcp-service-account
Extra requirement on the pod None The azure.workload.identity/use: "true" label None
Configuration steps 3 4 3
Credential lifetime 15 min - 12 h 1 h 1 h
Simpler alternative EKS Pod Identity

What matters for you: in all three cases, the manifest that goes into Git contains an annotation with an identifier, never a secret. It is exactly the model we were after in 03-06 and it fits perfectly with GitOps (10-05).

  1. Nodes: managed groups, templates and spot instances

Managed node groups

A managed node group is a set of homogeneous machines the provider maintains: it creates them, joins them to the cluster, upgrades them gradually and replaces them if they fail.

# An eksctl example for Rutas Norte
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata: { name: rutas-norte-pro, region: eu-west-1, version: "1.30" }

managedNodeGroups:
  # General group: workloads serving customer traffic
  - name: general
    instanceType: m6i.large
    minSize: 3
    maxSize: 12
    desiredCapacity: 4
    availabilityZones: [eu-west-1a, eu-west-1b, eu-west-1c]  # a real spread (09-05)
    volumeSize: 60
    volumeType: gp3
    updateConfig: { maxUnavailablePercentage: 25 }

  # Spot group: workloads that tolerate interruption
  - name: spot
    instanceTypes: [m6i.large, m5.large, m5a.large, m6a.large]
    spot: true                        # <-- spot instances
    minSize: 0
    maxSize: 20
    labels: { rutasnorte.example/spot: "true" }
    taints:
      # Nothing is scheduled here unless it explicitly tolerates it
      - { key: rutasnorte.example/spot, value: "true", effect: NoSchedule }

Note the instanceTypes with four types in the spot group: the more types you accept, the lower the chance that the provider runs out of capacity and interrupts everything at once.

Spot instances

They are the provider's spare capacity at a 60-90% discount, with a trade-off: the provider can reclaim it with a 2-minute warning (AWS), 30 seconds (Azure) or 30 seconds (GCP).

Rutas Norte workload Suitable for spot? Why
web-store No Customer-facing; an interruption is a lost sale
bookings-api No The same, and it also holds booking sessions
bookings-postgres Absolutely not Critical state; an interruption during a write is a risk
redis-cache With care It is a cache, but losing it all at once causes a stampede on the database
notifications-worker Yes It processes a queue; if a pod dies, the message goes back on the queue
occupancy-reports Yes A nightly CronJob; if it fails, it retries

The manifests, using what we learned in 06-05 and 09-05 (the full detail is in the solution to exercise 1):

    spec:
      # 1. TOLERATE the spot group's taint
      tolerations:
        - { key: rutasnorte.example/spot, operator: Equal,
            value: "true", effect: NoSchedule }

      # 2. PREFER spot nodes, but accept normal ones if there is no
      #    capacity (preferredDuringScheduling, NOT required)
      affinity:
        nodeAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              preference:
                matchExpressions:
                  - { key: rutasnorte.example/spot, operator: In, values: ["true"] }

      # 3. SPREAD across zones: an entire zone can run out of
      #    spot capacity all at once
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels: { app: notifications-worker }

      # 4. Headroom to finish the message in flight within the warning
      terminationGracePeriodSeconds: 90
      containers:
        - name: worker
          lifecycle:
            preStop:
              exec: { command: ["/app/drain", "--wait", "60s"] }

And a PDB with minAvailable: 1, because even though they are interruptible, we do not want them all going down at once (09-05).

An important nuance about the PDB: a PodDisruptionBudget protects against voluntary disruptions (draining a node to upgrade it). A spot instance interruption is involuntary: the provider takes the machine away, PDB or no PDB. The PDB helps because the termination handlers (the AWS Node Termination Handler, or Karpenter's native handling) cordon and drain the node on receiving the warning, and that drain does respect the PDB.

occupancy-reports, being a nightly CronJob (06-03), is an even clearer case: on top of the toleration it can carry a hard nodeSelector on rutasnorte.example/spot: "true" — if there is no capacity, waiting is acceptable — and a generous backoffLimit: 6, because interruption is to be expected.

Karpenter: the modern alternative on EKS

Instead of defining groups with fixed types, you declare constraints and Karpenter chooses:

apiVersion: karpenter.sh/v1
kind: NodePool
metadata: { name: rutas-norte-batch }
spec:
  template:
    spec:
      taints:
        - { key: rutasnorte.example/spot, value: "true", effect: NoSchedule }
      requirements:
        - { key: karpenter.sh/capacity-type, operator: In, values: ["spot"] }
        - { key: kubernetes.io/arch, operator: In, values: ["amd64", "arm64"] }  # ARM is cheaper
        - { key: karpenter.k8s.aws/instance-category, operator: In, values: ["c", "m", "r"] }
        - { key: topology.kubernetes.io/zone, operator: In,
            values: ["eu-west-1a", "eu-west-1b", "eu-west-1c"] }
      nodeClassRef: { group: karpenter.k8s.aws, kind: EC2NodeClass, name: default }
  limits: { cpu: "200", memory: 400Gi }
  disruption:
    # Consolidate: if the pods fit on fewer nodes, migrate them and
    # retire the surplus. A direct saving.
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 60s
    expireAfter: 720h                    # renew nodes every 30 days

Differences from the Cluster Autoscaler (09-03):

Cluster Autoscaler Karpenter
How it decides Scales predefined groups Picks the optimal instance for the pending pods
Instance types Those of the group Hundreds, according to the constraints
Speed 60-120 s ~40 s
Consolidation Limited Active and aggressive
Spot interruption With a separate handler Built in
Availability All three providers AWS only (and Azure in development)

  1. What changes in what you already know

This section revisits the course's earlier lessons and points out what is different in the cloud.

The StorageClass (05-04)

In minikube we used standard, a node directory. In the cloud:

# EKS with EBS gp3
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata: { name: rutas-norte-ssd }
provisioner: ebs.csi.aws.com
parameters: { type: gp3, iops: "6000", throughput: "250", encrypted: "true" }
allowVolumeExpansion: true
# CRITICAL: the volume is not created until the pod is scheduled,
# so that it is created in the SAME zone as the node.
volumeBindingMode: WaitForFirstConsumer
reclaimPolicy: Retain
Aspect Local Cloud
Access modes Everything "works" with one node Genuine ReadWriteOnce: a block disk mounts on one machine only
ReadWriteMany With hostPath, yes Requires a shared file system (EFS, Azure Files, Filestore), more expensive and slower
Zones They do not exist A disk belongs to a zone. If the pod is scheduled in another, it will not start
Expansion Sometimes not Yes, online
Encryption at rest No Yes, with managed keys
Cost 0 Per GB/month and per IOPS

The classic mistake: bookings-postgres with volumeBindingMode: Immediate creates the disk in eu-west-1a; the scheduler places the pod in eu-west-1c; the pod stays Pending for ever with volume node affinity conflict. The fix: WaitForFirstConsumer, which is the default in all three providers' StorageClasses.

The LoadBalancer Service (04-02)

Locally it stayed in <pending> and needed minikube tunnel. In the cloud it genuinely works:

apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
  annotations:
    # A network (layer 4) balancer, faster and cheaper than the layer 7 one
    service.beta.kubernetes.io/aws-load-balancer-type: "external"
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip"
    service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
spec:
  type: LoadBalancer
  # Preserve the client's real IP. Without this, the logs and the
  # rate limiter see the node's IP, not the user's.
  externalTrafficPolicy: Local

Things to learn:

  • Every LoadBalancer Service costs money (around $20-25/month plus traffic). That is why only one is used for the Ingress controller and everything else goes through Ingress rules (04-04).
  • The annotations are provider-specific. They are the least portable part of your manifests (section 8).
  • Deleting the Service deletes the balancer. And if the balancer has a DNS record pointing at it, that breaks.
  • externalTrafficPolicy: Local preserves the client IP but requires a pod with endpoints on every node; otherwise traffic to that node is dropped.

All three providers additionally offer a managed Ingress controller (AWS Load Balancer Controller, Application Gateway Ingress Controller, GKE Ingress) that creates a layer 7 balancer straight from the Ingress object. It is convenient, but it ties you to the provider more than ingress-nginx does.

Image registries (08-05)

Provider Registry Scanning Authentication from the cluster
AWS ECR Basic or enhanced (Inspector) Via the node role or IRSA, no imagePullSecrets
Azure ACR Defender for Containers Direct attachment to AKS
GCP Artifact Registry Artifact Analysis Via the node's service account

The practical advantage: imagePullSecrets disappears. The node authenticates with its cloud identity. One less Secret to manage.

And it dovetails with 08-05: signing with Cosign and the admission policy work the same, with the bonus that all three registries offer automatic scanning that feeds into what we learned in 08-06.

Metrics and logs (07-03, 07-05)

Your own Prometheus The provider's service
Cost The storage and the nodes Per metric or per GB ingested
Long retention Requires Thanos or Mimir Included
PromQL queries Yes AWS and GCP yes (compatible); Azure with KQL
Portability Total None
Operation Yours The provider's
Correlation with the rest of the cloud Manual Native

Pragmatic advice: keep Prometheus (07-03) for the applications' metrics, because your alerts and dashboards are portable, and use the provider's service for the logs (07-05), because operating EFK at scale is expensive and painful, and the managed service's cost usually pays off. It is the combination most teams choose.

Beware of log costs: bookings-api with LOG_LEVEL=debug in production can generate hundreds of GB per month. All three providers charge for ingestion.

  1. Upgrading a managed cluster's version

Compared with the kubeadm procedure (10-02), this is a walk in the park. But it still demands planning, and whoever treats it as a button gets an unpleasant surprise.

# EKS: control plane and node groups, separately
aws eks update-cluster-version --name rutas-norte-pro --kubernetes-version 1.31
aws eks update-nodegroup-version --cluster-name rutas-norte-pro --nodegroup-name general

# AKS
az aks upgrade -g rutas-norte -n rutas-norte-pro --kubernetes-version 1.31.1

# GKE
gcloud container clusters upgrade rutas-norte-pro --master --cluster-version 1.31.1
gcloud container clusters upgrade rutas-norte-pro --node-pool general

Why it still demands planning

1. Removed APIs are still your problem. If a manifest in k8s/base uses an API that disappears in 1.31, the cluster upgrades and your application stops deploying.

# Before upgrading, ALWAYS
kubectl get --raw /metrics | grep apiserver_requested_deprecated_apis

# And scan the manifests
kubent --context rutas-norte-pro
pluto detect-files -d k8s/

2. Third-party add-ons have their own compatibility matrices. cert-manager, ingress-nginx, KEDA, the Prometheus Operator, the CSI driver: each supports a range of versions. Upgrading the cluster without upgrading them can break them.

Add-on Check beforehand
cert-manager The project's compatibility matrix
ingress-nginx The supported versions table
KEDA The minimum Kubernetes version
Prometheus Operator The CRDs' version
Argo CD The supported apiserver version
The provider's CSI driver The managed add-on's version

3. Upgrading the nodes restarts every pod. The provider drains each node in turn. That exercises:

  • Your PodDisruptionBudgets (09-05): if they are misconfigured, the drain hangs or, worse, gets skipped with --force.
  • Your readiness probes (07-01): if bookings-api takes 45 seconds to become ready and the drain moves fast, there is an outage.
  • Your terminationGracePeriodSeconds and your preStop hook.
  • Your capacity to absorb the temporary loss of a node: if the cluster is 95% full, draining a node leaves pods Pending.

4. The control plane and the nodes are upgraded separately, and you have to respect the version skew (10-02): nodes at most 3 minors below the apiserver, and never above.

The recommended procedure for Rutas Norte

1. Read the release notes and the removed APIs.
2. Run kubent/pluto over k8s/ and fix whatever turns up.
3. Review each add-on's compatibility matrix;
   upgrade whichever need it FIRST.
4. Upgrade rutas-norte-dev. Wait a week of normal use.
5. Upgrade rutas-norte-pre. Run the load tests with k6 (09-06).
6. Verify that the PDBs allow draining:
     kubectl get pdb -A -o wide
     kubectl drain <a-node> --dry-run=server
7. Maintenance window on rutas-norte-pro:
     a. Control plane first.
     b. Check everything is still Synced and Healthy in Argo CD.
     c. One node group at a time, with a low maxUnavailable.
     d. Watch the Grafana dashboards throughout.
8. Update the version declared in the local script from 10-01
   so the team develops against the same version.

That last point closes the circle with the module's first lesson: the local environment must follow production.

  1. Real cost and the levers for saving

What the bill is made of

Item Typical weight Comment
Nodes (compute) 50-70% The big line item, nearly always
Storage 10-20% PV disks, snapshots, logs
Network traffic 5-20% Egress to the internet and between zones
Load balancers 5-10% ~$20-25/month each
Control plane 2-5% ~$73/month per cluster
Managed services Variable Metrics, logs, scanning

A common surprise: traffic between availability zones is billed. If bookings-api is in zone A and bookings-postgres in zone B, every query crosses a billed boundary. At high volume, that is hundreds of euros a month. It is an argument in favour of zone affinity... which clashes with the high availability of 09-05. It is a real trade-off that has to be decided deliberately.

The levers, in order of return

1. Sizing properly (what we learned in 09-02). It is the lever with the best effort-to-saving ratio, and hardly anybody uses it. The typical pattern: somebody set requests: {cpu: 1, memory: 2Gi} "just in case" and the pod uses 80m and 200Mi. The scheduler reserves what you asked for, so you are paying 12 times what you use.

kubectl top pods -n rutas-norte-pro --sort-by=memory
kubectl describe vpa bookings-api -n rutas-norte-pro   # VPA in Off mode: recommends without touching
  Container Recommendations:
    Container Name:  api
    Target:  Cpu: 127m   Memory: 312Mi

Adjusting requests to the recommendation across the whole platform usually cuts the compute bill by between 30 and 50%.

2. Real autoscaling (09-01 and 09-03). With the HPA adjusting pods and Karpenter or the Cluster Autoscaler adjusting nodes, you pay for the real load and not for the annual peak. Rutas Norte has peaks at bank holidays: without autoscaling, you either size for the bank holiday all year round, or you fall over on the bank holiday.

3. Spot instances. A 60-90% discount on notifications-worker and occupancy-reports.

4. Switching off environments nobody uses at night. rutas-norte-dev does nothing between 20:00 and 08:00 or at weekends. That is 128 out of 168 weekly hours: a 76% saving in that environment.

# A CronJob that scales to zero at night (06-03). The switch-on one is
# identical with schedule "0 8 * * 1-5" and --replicas=1.
apiVersion: batch/v1
kind: CronJob
metadata: { name: shutdown-dev, namespace: rutas-norte-dev }
spec:
  schedule: "0 20 * * 1-5"
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: environment-scaler
          restartPolicy: OnFailure
          containers:
            - name: kubectl
              image: bitnami/kubectl:1.30.4
              command: ["sh", "-c", "kubectl scale deploy,statefulset --all --replicas=0 -n rutas-norte-dev"]

Careful with GitOps: if Argo CD has selfHeal on in rutas-norte-dev, it will undo the scale to zero within three minutes. That is why in the solution to exercise 1 of 10-05 we left selfHeal: false in dev. The clean alternative is to scale the node group instead of the Deployments, or to use an AppProject sync window.

5. Usage commitments. All three providers discount 30-55% in exchange for committing to spend or capacity for 1 or 3 years (Savings Plans and Reserved Instances on AWS, Reservations on Azure, Committed Use Discounts on GCP).

The recommended strategy: commit only the stable baseline (the nodes that are always on), leave the peak on demand and the tolerant workloads on spot.

flowchart TB
    A["Total load"] --> B["Stable baseline<br/>1-3 year commitment<br/>-40%"]
    A --> C["Peaks<br/>on demand<br/>full price"]
    A --> D["Tolerant workloads<br/>spot<br/>-70%"]
    style B fill:#e8ffe8
    style D fill:#e8f4ff

6. Cost visibility per team. Tools such as OpenCost or Kubecost break the spend down by namespace, label or team. Without that, nobody knows that occupancy-reports costs €400/month and nobody has any incentive to fix it. The labels from 02-07 (app.kubernetes.io/part-of, environment) are the basis of that breakdown.

Summary of levers

Lever Typical saving Effort Risk
Adjusting requests with the VPA 30-50% of compute Low Low if done in phases
Pod and node autoscaling 20-40% Medium Low
Spot instances 60-90% of the suitable workloads Medium Medium (the app has to be prepared)
Switching environments off at night 60-75% of dev/pre Low Very low
Usage commitments 30-55% of the baseline Low Medium (a commitment of years)
Reducing cross-zone traffic 5-15% High Clashes with HA
Reviewing log retention 10-30% of observability Low Low

  1. Portability and vendor lock-in

All of this counts for little if in two years you want to change cloud and cannot. Let us be precise about what is portable at Rutas Norte.

What is portable

Element Portable Comment
Deployment, StatefulSet, DaemonSet, Job, CronJob Totally A standard API
Service, Ingress (rules) Totally The rules yes; the annotations no
ConfigMap, Secret Totally
RBAC, ServiceAccount Totally Except the identity annotations
HPA, PDB, topologySpreadConstraints Totally
NetworkPolicy Totally If the destination CNI implements them
PVC (the class name) High The PVC is portable; the StorageClass has to be redefined
Kustomize manifests Totally
Helm charts Totally
Argo CD Applications Totally
Prometheus dashboards and alerts Totally If you use Prometheus and not the provider's service

What is NOT portable

Element Why Cost of migrating
LoadBalancer Service annotations service.beta.kubernetes.io/aws-* does not exist on Azure Low: rewrite one block
The StorageClass's parameters ebs.csi.aws.com versus disk.csi.azure.com Low: one file
The identity annotation on the SA IRSA / Workload Identity Low: one annotation
The provider's IngressClass ALB, Application Gateway, GKE Ingress Medium
Managed data services (RDS, Cloud SQL) Outside Kubernetes High
Managed queues and messaging (SQS, Service Bus) Outside Kubernetes High
The provider's metrics and logs Proprietary queries and dashboards High
Provider-specific infrastructure as code CloudFormation, ARM, Deployment Manager Medium (Terraform mitigates it)

The obvious pattern: what is inside Kubernetes is portable; what is outside is not. Vendor lock-in does not come from EKS, AKS or GKE, but from the services surrounding the cluster.

How to minimise lock-in

1. Isolate the non-portable parts in Kustomize overlays. You already have this set up from 10-04:

k8s/
├── base/                      <- 100% portable
├── components/
│   ├── provider-aws/          <- StorageClass, IRSA annotations, ALB
│   ├── provider-azure/
│   └── provider-gcp/
└── environments/pro/
    └── kustomization.yaml     <- enables the current provider's component

Changing cloud means changing one line in components:. The base is not touched.

2. Use ingress-nginx rather than the provider's controller. It is a layer of abstraction that costs a little performance and saves an entire migration. Ingress rules and nginx annotations work the same in all three clouds.

3. Use Prometheus for the metrics. Your alerts and dashboards are years of accumulated operational knowledge. In PromQL they are portable; in the provider's query language, they are not.

4. Terraform rather than the native tool. It does not make the infrastructure portable, but it does make the mental model and the workflows portable.

5. Decide deliberately about the databases. Running bookings-postgres as a StatefulSet with an operator (06-01, 06-07) is portable; using the provider's managed service is more convenient and less portable. Both are defensible decisions; what is not acceptable is making the decision without noticing.

PostgreSQL on Kubernetes A managed service
Portability High Low
Operation (backups, replicas, patching) Yours, with the operator The provider's
Cost Lower Higher
Performance at large scale Requires tuning Optimised
Risk of data loss Depends on your diligence Lower

6. Be realistic. Total portability costs money and complexity, and many companies never change cloud. A sensible goal: "we could migrate in three months with effort, not in three days". That gives you negotiating power and protects you from an abusive price change, without paying the tax of total abstraction.

  1. The decision for Rutas Norte

The facts of the case

  • A small company: two people in platform, six in development, three in support.
  • No 24×7 on-call cover.
  • Traffic with strong, predictable peaks: bank holidays and holiday seasons, 8-10 times the base load.
  • Customers' personal data in bookings-postgres: GDPR requirements on location and encryption.
  • A tight budget.
  • It already uses AWS object storage and transactional email for other things.

We rule out the self-managed cluster

With two people in platform and no on-call rota, kubeadm (10-02) is ruled out. The decisive question is: who restores etcd at 4 in the morning on a Sunday? If the answer is "we wait until Monday", you cannot operate rutas-norte-pro on a cluster of your own. The control plane saving (about €220/month with three clusters) does not come close to offsetting the risk.

The choice: EKS

Criterion Weight Assessment
They already use AWS for other services High One cloud, one bill, one identity
Karpenter for the bank-holiday peaks High The best node autoscaling of the three
Spot for notifications-worker and occupancy-reports High A direct saving on workloads already identified as tolerant
Very mature IRSA Medium Closes 03-06 with no credentials in Secrets
European regions (GDPR) High eu-west-1 complies
A 14-month support window Medium With a small team, more time between upgrades is valuable
Control plane cost Low $73/month is affordable
The IAM learning curve Medium (against) It is the weak point, but the team already knows it

If Rutas Norte were not already using AWS, the recommendation would be GKE: the cleanest networking model, simpler Workload Identity, release channels that reduce upgrade work, and Autopilot as an option for the non-production environments.

The concrete design

AWS account, eu-west-1 region

CLUSTER 1: rutas-norte-pro
  Kubernetes 1.30, regional control plane (3 zones)
  Node groups:
    - general: m6i.large, 3-12 nodes, on demand,
               3 zones, with a usage commitment on 3 nodes
    - spot:    managed by Karpenter, spot, 0-20,
               with the rutasnorte.example/spot taint
  Workloads:
    web-store, bookings-api, bookings-postgres, redis-cache -> general
    notifications-worker, occupancy-reports -> spot

CLUSTER 2: rutas-norte-non-production
  Namespaces rutas-norte-dev and rutas-norte-pre
  A single group: m6i.large, 2-6 nodes, MOSTLY SPOT
  Automatic shutdown from 20:00 to 08:00 and at weekends

PLATFORM (in both clusters, via GitOps):
  Argo CD, cert-manager, ingress-nginx, KEDA,
  kube-prometheus-stack, External Secrets Operator, Velero

DATA:
  bookings-postgres as a StatefulSet with an operator (portability)
  Backups with Velero + EBS snapshots, replicated to another region

Two clusters, not three. Separating pro from the rest is a real security boundary; separating dev from pre does not justify it: namespaces with quotas (03-04) and NetworkPolicies (04-06) are enough, and they save $73/month plus the operational work.

The estimated cost

Item Monthly
Control plane × 2 clusters $146
pro nodes: 3 on demand with a commitment + 1 average ~$210
pro spot nodes (2 on average) ~$35
Non-production nodes (spot, 40 h/week) ~$45
EBS storage (300 GB gp3 + snapshots) ~$45
Load balancers (1 per cluster) ~$50
Egress and cross-zone traffic ~$60
Managed logging and metrics ~$40
Approximate total ~$630/month

Without the saving levers (everything on demand, no spot, no night-time shutdown, no commitments, with oversized requests) the same platform would cost in the order of $1,400-1,600/month. The levers in section 7 are not theory: they are more than half the bill.

The honest conclusion

A managed cluster is not cheaper than your own in pure infrastructure cost. It is cheaper in total cost, because that includes the work you do not have to do and the risk you do not take on. For Rutas Norte S.L., with two people in platform and no on-call rota, it is the only defensible option.

And there is a symmetry worth noting: thanks to GitOps (10-05), the decision is reversible. The entire state of the platform is in Git. If tomorrow there is a reason to change cloud or to build a cluster of our own, you bring up the new cluster, install Argo CD, apply the root Application and twenty minutes later the platform is standing. The data migrates separately, which is the hard part, but the platform rebuilds itself.

Common Mistakes and Tips

1. Believing that "managed" includes backups of your data. The provider backs up etcd, not your volumes. bookings-postgres is your responsibility: Velero (05-06) and snapshots, with a rehearsed restore.

2. Not planning the IP addressing. On EKS with VPC CNI, every pod consumes a real VPC IP. A /24 runs out quickly and the symptom (FailedCreatePodSandBox) does not mention IPs obviously. Plan generous ranges and enable prefix delegation.

3. One LoadBalancer Service per application. Each one costs $20-25/month. Use one for the Ingress controller and route with Ingress rules (04-04).

4. A StorageClass with volumeBindingMode: Immediate in a multi-zone cluster. The disk is created in one zone, the pod is scheduled in another, and the pod never starts. Use WaitForFirstConsumer.

5. Putting stateful workloads on spot instances. bookings-postgres on spot is a recipe for losing data. Only workloads that tolerate dying without warning, with an appropriate terminationGracePeriodSeconds and preStop hook.

6. A single instance type in the spot group. If the provider runs out of that type, it takes all your nodes at once. Declare 4-6 equivalent types.

7. Carrying on with static credentials in Secrets. IRSA, Workload Identity and their Azure equivalent eliminate permanent credentials. It is one of the best cost-to-benefit security improvements there is.

8. Upgrading the version without checking the removed APIs or the add-ons. The cluster upgrades without a hitch and your application stops deploying, or ingress-nginx stops starting. kubent and the compatibility matrices, before touching anything.

9. Oversized requests "just in case". It is the leading cause of overspend in Kubernetes. The VPA in Off mode (09-02) gives you the real numbers within a week.

10. Ignoring cross-zone traffic. In very chatty applications it can be a significant line item. Measure it before deciding; it is a real trade-off against the high availability of 09-05.

11. Not labelling for cost allocation. Without environment, app.kubernetes.io/part-of and a team label, the bill is one global number nobody can reduce. The labels from 02-07 are the basis of OpenCost or Kubecost.

12. Adopting Autopilot or Fargate without checking the restrictions. Falco (08-06), some networking DaemonSets and anything requiring privileges may not work. Try it in a non-production environment first.

13. Confusing "less operations" with "no operations". You are still responsible for the nodes, RBAC, policies, secrets, observability and cost. Modules 2 to 9 of this course remain entirely applicable.

Exercises

Exercise 1: manifests for spot instances

Write the complete set of manifests that lets notifications-worker run on a spot node group marked with the taint rutasnorte.example/spot=true:NoSchedule, while still working if there is no spot capacity. It must include: tolerations, preferred node affinity, topology spread, terminationGracePeriodSeconds with a preStop hook, a PDB and a KEDA ScaledObject. Explain why you use preferred affinity and not a nodeSelector, and what the PDB really protects against in a spot interruption.

Exercise 2: a provider component with Kustomize

Design the k8s/components/provider-aws/ component containing everything AWS-specific that Rutas Norte needs: the rutas-norte-ssd StorageClass with encrypted EBS gp3, a patch adding the IRSA annotation to the bookings-api and notifications-worker ServiceAccounts, and a patch adding the network balancer annotations to the Ingress controller's Service. Explain how it would be enabled from the pro overlay and what would have to change to migrate to Azure.

Exercise 3: a cost reduction plan

The rutas-norte-pro bill is $1,450/month: $980 of compute, $180 of storage, $120 of load balancers (5 LoadBalancer Services), $90 of traffic and $80 of logs. Every pod has requests: {cpu: 500m, memory: 1Gi} and kubectl top shows an average usage of 90m and 220Mi. There are no spot instances and no commitments. Propose a prioritised plan with the diagnostic commands, the estimated saving of each measure and the associated risk.

Solutions

Solution 1

apiVersion: apps/v1
kind: Deployment
metadata:
  name: notifications-worker
  namespace: rutas-norte-pro
spec:
  selector:
    matchLabels: { app: notifications-worker }
  template:
    metadata:
      labels:
        app: notifications-worker
        app.kubernetes.io/part-of: rutas-norte
    spec:
      tolerations:
        - key: rutasnorte.example/spot
          operator: Equal
          value: "true"
          effect: NoSchedule
      affinity:
        nodeAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
            - weight: 100
              preference:
                matchExpressions:
                  - { key: rutasnorte.example/spot, operator: In, values: ["true"] }
      topologySpreadConstraints:
        - maxSkew: 1
          topologyKey: topology.kubernetes.io/zone
          whenUnsatisfiable: ScheduleAnyway
          labelSelector:
            matchLabels: { app: notifications-worker }
      terminationGracePeriodSeconds: 90
      containers:
        - name: worker
          image: registry.rutasnorte.example/notifications-worker:1.8.4
          lifecycle:
            preStop:
              exec: { command: ["/app/drain", "--wait", "60s"] }
          resources:
            requests: { cpu: 200m, memory: 256Mi }
            limits:   { memory: 512Mi }
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata: { name: notifications-worker, namespace: rutas-norte-pro }
spec:
  minAvailable: 1
  selector:
    matchLabels: { app: notifications-worker }
---
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata: { name: notifications-worker, namespace: rutas-norte-pro }
spec:
  scaleTargetRef: { name: notifications-worker }
  minReplicaCount: 1
  maxReplicaCount: 30
  triggers:
    - type: redis
      metadata:
        address: redis-cache:6379
        listName: email-queue
        listLength: "20"

Preferred affinity and not a nodeSelector: nodeSelector is a hard constraint. If there is no spot capacity in the region — which happens precisely at peak times, when you need it most — the pods would stay Pending indefinitely. With preferredDuringScheduling, the scheduler tries the spot node and, failing that, uses an on-demand one: you pay more, but the service does not stop.

What the PDB protects: nothing against the interruption itself (the provider takes the machine). It protects during the drain that the termination handler (or Karpenter) runs on receiving the 2-minute warning: that drain respects the PDB, so two nodes are not emptied simultaneously leaving the queue without consumers.

Solution 2

# k8s/components/provider-aws/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
resources:
  - storageclass.yaml
patches:
  - path: irsa-patch.yaml
    target: { kind: ServiceAccount, name: "bookings-api|notifications-worker" }
  - path: lb-patch.yaml
    target: { kind: Service, name: ingress-nginx-controller }

storageclass.yaml is the one from section 5 (provisioner ebs.csi.aws.com, encrypted gp3, WaitForFirstConsumer). The two patches are minimal strategic merges; the target takes care of who they apply to, so the patch's metadata.name is irrelevant:

# irsa-patch.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: IGNORED
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/RutasNortePro
---
# lb-patch.yaml
apiVersion: v1
kind: Service
metadata:
  name: ingress-nginx-controller
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: "external"
    service.beta.kubernetes.io/aws-load-balancer-nlb-target-type: "ip"
spec:
  externalTrafficPolicy: Local
# k8s/environments/pro/kustomization.yaml
components:
  - ../../components/network-policies
  - ../../components/high-availability
  - ../../components/provider-aws       # <-- one line

Migrating to Azure: create k8s/components/provider-azure/ with provisioner: disk.csi.azure.com, the azure.workload.identity/client-id annotation (plus the azure.workload.identity/use: "true" label on the pods) and Azure's balancer annotations. Then change that single line. k8s/base/ is not touched at all.

Solution 3

Priority Measure Diagnosis Saving Risk
1 Adjust requests to 150m/320Mi kubectl top pods, VPA in Off mode ~$450 Low, in phases
2 Consolidate 5 balancers into 1 + Ingress kubectl get svc -A --field-selector spec.type=LoadBalancer ~$96 Medium (a DNS change)
3 Spot for the worker and the reports Already identified as tolerant ~$80 Medium
4 A usage commitment on the baseline The 90-day node average ~$130 Medium (1-3 years)
5 Log retention from 30 to 7 days; LOG_LEVEL=warn Volume ingested per service ~$50 Low
6 Review orphaned PVs and old snapshots kubectl get pv --field-selector status.phase=Released ~$40 Low
Total ~$846 A bill from ~$1,450 to ~$604
# Diagnostic commands
kubectl top pods -A --sort-by=cpu
kubectl get vpa -A -o custom-columns='NS:.metadata.namespace,NAME:.metadata.name,CPU:.status.recommendation.containerRecommendations[0].target.cpu,MEM:.status.recommendation.containerRecommendations[0].target.memory'
kubectl get svc -A --field-selector spec.type=LoadBalancer
kubectl get pv --field-selector status.phase=Released

Order and risk: you start with the requests adjustment because it has the greatest saving and the lowest risk, applying it first in pre, verifying with k6 (09-06) and then in pro component by component. The usage commitment goes last on purpose: committing to a capacity only makes sense after you have reduced it, or you would be paying upfront for your own oversizing.

Conclusion

Module 10 ends here. The essentials of this lesson:

  • The shared responsibility model takes the entire control plane off your hands — etcd, certificates, quorum, upgrades, high availability — which is roughly half the work of 10-02. Everything else, including the nodes, RBAC, policies, secrets, observability, backups of your data and cost, remains yours.
  • EKS, AKS and GKE are more alike than different. The differences that genuinely decide things are the networking model and pod IP allocation, the version cycle, the autoscaling (Karpenter stands out) and the automatic modes (Autopilot stands out). And above all: the cloud where your infrastructure already lives.
  • Identity federation — IRSA, Workload Identity and their Azure equivalent — eliminates permanent credentials from Secrets. The manifest that goes into Git carries an annotation, not a secret. It closes what we promised in 03-06.
  • Spot instances are a real saving for notifications-worker and occupancy-reports, provided they come with tolerations, preferred affinity, topology spread, preStop and a PDB.
  • What you already knew changes in concrete ways: the StorageClass is the provider's and needs WaitForFirstConsumer, the LoadBalancer finally gets an IP (and a bill), the registry authenticates with the node's identity, and metrics and logs can be delegated.
  • Upgrading still demands planning: removed APIs, add-on compatibility matrices, PDBs that allow draining and probes that survive the restart.
  • Cost is mastered with six levers, and the first two — adjusting requests with the VPA and switching off what nobody uses — have the best effort-to-benefit ratio.
  • And portability depends on what you put outside Kubernetes, not on which managed offering you choose. Isolating the provider-specific parts in a Kustomize component leaves the base 100% portable.

For Rutas Norte S.L., the recommendation is EKS in eu-west-1, with two clusters, Karpenter, spot instances for the tolerant workloads and the saving levers applied from day one. And thanks to GitOps, that decision is reversible.

This brings the tour of the ecosystem to a close. You now have all the pieces: local and self-built clusters, packaging with Helm, overlays with Kustomize, continuous deployment with GitOps and a managed platform to rest it all on. Added to the nine previous modules — pods, services, configuration, networking, storage, advanced patterns, observability, security and scaling — you now know all the pieces individually.

What is missing is seeing them work together. In Module 11: Case Studies and Real-World Applications we will walk through complete scenarios from start to finish: deploying a web application from scratch, running stateful applications, a complete CI/CD pipeline, blue-green and canary deployment strategies, multi-cluster management and, to close, real production operations: incidents, runbooks and cost control. That is where knowledge turns into judgement.

Kubernetes Course

Module 1: Introduction to Kubernetes

Module 2: Core Kubernetes Components

Module 3: Configuration and Secret Management

Module 4: Networking in Kubernetes

Module 5: Storage in Kubernetes

Module 6: Advanced Kubernetes Concepts

Module 7: Monitoring and Logging

Module 8: Kubernetes Security

Module 9: Scaling and Performance

Module 10: Kubernetes Ecosystem and Tooling

Module 11: Case Studies and Real-World Applications

Module 12: Preparing for Kubernetes Certification

© Copyright 2026. All rights reserved