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
- The shared responsibility model
- Comparing EKS, AKS and GKE
- Identity: IRSA and Workload Identity
- Nodes: managed groups, templates and spot instances
- What changes in what you already know
- Upgrading a managed cluster's version
- Real cost and the levers for saving
- Portability and vendor lock-in
- The decision for Rutas Norte
- Common mistakes and tips
- Exercises
- Conclusion
- 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.
- 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"}'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 containerA 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.
- 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 --approveThe 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/RutasNorteBookingsApiIn 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).
- 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 daysDifferences 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) |
- 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: LocalThings 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: Localpreserves 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.
- 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 generalWhy 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-apitakes 45 seconds to become ready and the drain moves fast, there is an outage. - Your
terminationGracePeriodSecondsand yourpreStophook. - 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.
- 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 touchingAdjusting 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 |
- 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 componentChanging 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.
- The decision for Rutas Norte
The facts of the case
- A small company: two people in
platform, six indevelopment, three insupport. - 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 regionTwo 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 lineMigrating 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=ReleasedOrder 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-workerandoccupancy-reports, provided they come with tolerations, preferred affinity, topology spread,preStopand 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
requestswith 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
- What Is Kubernetes?
- Kubernetes Architecture
- Key Concepts and Terminology
- Setting Up a Kubernetes Cluster
- The Kubernetes CLI: kubectl
- Objects, YAML Manifests and the Declarative Model
- The Course Project: the Rutas Norte Platform
Module 2: Core Kubernetes Components
- Pods
- ReplicaSets
- Deployments
- Updates, Rollbacks and Deployment Strategies
- Services
- Namespaces
- Labels, Selectors and Annotations
Module 3: Configuration and Secret Management
- ConfigMaps
- Secrets
- Environment Variables
- Resource Quotas and Limits
- LimitRanges and Quality of Service (QoS) Classes
- ServiceAccounts and API Access from Pods
Module 4: Networking in Kubernetes
- Cluster Networking
- Service Types
- Internal DNS and Service Discovery
- Ingress Controllers
- TLS and Certificate Management with cert-manager
- Network Policies
Module 5: Storage in Kubernetes
- Volumes
- Persistent Volumes
- Persistent Volume Claims
- Storage Classes
- Dynamic Provisioning, Expansion and Snapshots
- Backup and Restore of Persistent Data
Module 6: Advanced Kubernetes Concepts
- StatefulSets
- DaemonSets
- Jobs and CronJobs
- Init Containers, Sidecars and Multi-Container Patterns
- Scheduling: Affinity, Taints and Tolerations
- Custom Resource Definitions (CRDs)
- Operators and the Controller Pattern
Module 7: Monitoring and Logging
- Health Checks and Probes
- Metrics Server and kubectl top
- Monitoring with Prometheus
- Visualization and Alerting with Grafana and Alertmanager
- Centralized Logging with Elasticsearch, Fluentd and Kibana (EFK)
- Application Debugging and Cluster Events
Module 8: Kubernetes Security
- Role-Based Access Control (RBAC)
- Security Contexts and Container Hardening
- Pod Security Policies and Pod Security Standards
- Network Security
- Image Security
- Auditing, Scanning and Vulnerability Management
Module 9: Scaling and Performance
- Horizontal Pod Autoscaling
- Vertical Pod Autoscaling
- Cluster Autoscaling
- Event-Driven and Custom-Metric Scaling with KEDA
- High Availability: PodDisruptionBudgets and Topology
- Performance Tuning
Module 10: Kubernetes Ecosystem and Tooling
- Minikube and Local Environments with kind
- Kubeadm
- Helm
- Kustomize
- GitOps with Argo CD and Flux
- Managed Kubernetes: EKS, AKS and GKE
Module 11: Case Studies and Real-World Applications
- Deploying a Web Application
- Running Stateful Applications
- CI/CD with Kubernetes
- Deployment Strategies: Blue-Green and Canary
- Multi-Cluster Management
- Production Operations: Incidents, Runbooks and Costs
