Module 7: Eks Specifics For Production
8. Project: Andes Cargo's migration plan, `kind` → real EKS
Description
This is the only document in the entire Module 7 with executed evidence — not because it talks about EKS differently from the rest of the module, but because this project's real work requires no call to AWS at all: it's a resource-by-resource analysis of the ten real manifests that already exist in andes-cargo-k8s/, the repository Gitea serves and ArgoCD actually syncs since Module 5. Every row in this project's table is built against the literal content of those files — the same ones that ran against andes-cargo-cluster on kind — not against external documentation or any generic assumption of "this is usually how an EKS migration works."
Connection to the module
Lessons 2-7 of this module built, piece by piece, EKS's vocabulary and representative syntax: the managed control plane, node groups, Karpenter, IRSA/Pod Identity, the AWS Load Balancer Controller, and a complete cluster.yaml. This project adds no new concept — it takes all of that and applies it, with surgical precision, to the real repository that already exists, to answer a single honest question: of the ten files that today run andes-cargo-status-api on kind, how many survive without touching a single line, and how many need to change?
Starting point: the real repository, exactly as it stood when Module 5 closed
andes-cargo-k8s/, served by Gitea (andes-cargo/andes-cargo-k8s), synced by ArgoCD since Module 5, contains exactly ten manifests — nine inherited from Modules 1-4 (confirmed in Module 5, lesson 3: "a repository... with the nine manifests inherited from Modules 1-4"), plus application.yaml, added in Module 5 itself as ArgoCD's self-referential object:
andes-cargo-k8s/ — REAL REPOSITORY, 10 files
namespace.yaml M1 — the andes-cargo namespace
deployment.yaml M2 — the andes-cargo-status-api Deployment
service.yaml M2 — the status-api-service Service
configmap.yaml M3 — LocalStack endpoint + region + table
secret.yaml M3 — dummy AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY
hpa.yaml M3 — the HorizontalPodAutoscaler
ingress.yaml M4 — the Ingress rule (ingressClassName: nginx)
networkpolicy-default-deny.yaml M4 — the namespace's default-deny policy
networkpolicy-allow-ingress-nginx.yaml M4 — explicit permission from ingress-nginx
application.yaml M5 — the ArgoCD Application (self-referential)
This project doesn't include gatekeeper/ or kyverno/ (Module 6) in the main table, because those two directories are, by design, already independent of the underlying cluster: a Gatekeeper ConstraintTemplate/Constraint or a Kyverno ClusterPolicy reference no detail specific to kind or to EKS — they run exactly the same, with no change, against the same kube-apiserver on either one. They're picked up again at the end of this project, in the lab infrastructure section, with that same conclusion confirmed in writing.
The complete table: what changes, what doesn't, and why — resource by resource
| # | File | Changes | Exact reason |
|---|---|---|---|
| 1 | namespace.yaml | No | A namespace is a purely logical Kubernetes object — it has no field that depends on the underlying cloud. |
| 2 | deployment.yaml | Yes | Two fields change: (a) image: — moves from the local registry localhost:5001/andes-cargo-status-api:<tag> (integrated with kind, Module 1) to a real ECR repository (000000000000.dkr.ecr.us-east-1.amazonaws.com/andes-cargo-status-api:<tag>); (b) spec.template.spec.serviceAccountName: status-api-service-account gets added, the only new field this module's lesson 7 confirmed. Replicas, resources.limits/requests, and the three Module 3 probes don't change. |
| 3 | service.yaml | No | A ClusterIP-type Service references no cloud infrastructure detail — it keeps resolving internal traffic between Pods exactly the same. |
| 4 | configmap.yaml | Yes | Of the three real keys (DYNAMODB_ENDPOINT_URL, AWS_REGION, SHIPMENTS_TABLE_NAME), only one gets removed: DYNAMODB_ENDPOINT_URL pointed at localstack.localstack.svc.cluster.local:4566 — on a real EKS talking to real DynamoDB, boto3 needs no explicit endpoint_url, it uses the real regional endpoint by default. AWS_REGION: "us-east-1" and SHIPMENTS_TABLE_NAME: "Shipments" stay identical — the same real values aws-core-services-guide fixed from this case's origin. |
| 5 | secret.yaml | Becomes unnecessary | This file's two keys (AWS_ACCESS_KEY_ID: "test", AWS_SECRET_ACCESS_KEY: "test") are dummy credentials that exist only to authenticate against LocalStack. With IRSA or EKS Pod Identity (Module 7.5) injecting temporary credentials automatically into the ServiceAccount, andes-cargo-status-api would never need to read any AWS_ACCESS_KEY_ID from a Secret — the AWS SDK detects the federated credentials on its own, via the default credential chain. This file would, literally, stop applying. |
| 6 | hpa.yaml | No | The HorizontalPodAutoscaler reacts to Pod metrics (metrics.k8s.io), with no field specific to the underlying cloud — the exact same mechanism, run on kind in Module 3, works the same on EKS. The only thing that would change is the source of the real CPU reported (a real EC2 node's kubelet instead of a Docker container), invisible to the YAML itself. |
| 7 | ingress.yaml | Yes | ingressClassName: nginx → ingressClassName: alb (Module 7.6), plus controller-specific annotations get added (alb.ingress.kubernetes.io/scheme, alb.ingress.kubernetes.io/target-type). host, path, pathType, and backend.service — the fields that actually define the routing — don't change. |
| 8 | networkpolicy-default-deny.yaml | No | podSelector: {} with a default-deny incoming traffic policy references no infrastructure detail — the same total block by default applies the same on any conformant Kubernetes cluster. |
| 9 | networkpolicy-allow-ingress-nginx.yaml | Yes | This is the table's subtlest row: its current rule uses namespaceSelector: kubernetes.io/metadata.name: ingress-nginx — it allows traffic from the ingress-nginx controller's Pods, which run inside the cluster. With the AWS Load Balancer Controller, there's no Ingress Pod running inside the cluster to send traffic from there: the ALB, a resource outside the cluster, sends traffic directly to the Pod's IP (with target-type: ip) from the network interfaces AWS assigns inside the VPC. A namespaceSelector can't identify that source — there's no Kubernetes namespace to represent it — so the rule would need to change to an ipBlock block, with the real CIDR range of the subnets where the ALB lives. |
| 10 | application.yaml | No | ArgoCD's Application (source/destination/syncPolicy) is agnostic to the underlying cluster by design — the same watch and convergence mechanism works the same regardless of whether destination.server points at kind or at a real EKS cluster. In practice, only the kubeconfig/context ArgoCD uses to reach that cluster would change — a deployment detail of ArgoCD itself, not of the application.yaml object. |
The final count, verified twice
andes-cargo-k8s/ — TEN FILES, CATEGORIZED
UNCHANGED (5 of 10):
namespace.yaml · service.yaml · hpa.yaml
networkpolicy-default-deny.yaml · application.yaml
CHANGE, IN SPECIFIC FIELDS (4 of 10):
deployment.yaml → image: + serviceAccountName
configmap.yaml → removes DYNAMODB_ENDPOINT_URL (2 of 3 keys remain)
ingress.yaml → ingressClassName + ALB annotations
networkpolicy-allow-ingress-nginx.yaml → namespaceSelector → ipBlock
BECOME UNNECESSARY (1 of 10):
secret.yaml → replaced by IRSA/EKS Pod Identity
5 + 4 + 1 = 10 ✓
This project's central finding, stated with no embellishment: 50% of Andes Cargo's application manifests wouldn't change a single line when migrating from kind to EKS. Of the five that do change or disappear, none does so for a "Kubernetes is different on EKS" reason — each one changes because there's real AWS infrastructure around it that used to be simulated or nonexistent (a real container registry, real federated credentials, a real load balancer, a real VPC). It's the confirmation, with evidence and not a generic promise, of this entire guide's central thesis: what you learned against kind transfers with no translation to EKS — the translation that is needed is bounded, nameable, and fits in a single table.
What gets added around andes-cargo-k8s/ (not inside the repository)
The migration isn't just "edit five files" — there are new pieces this module already showed, representative, that would need to exist before the repository above could apply against a real cluster:
| New piece | From which lesson | Replaces, on kind, |
|---|---|---|
cluster.yaml (eksctl ClusterConfig) | Module 7.7 | kind-config.yaml (Module 1) |
IAMServiceAccount (status-api-service-account / StatusApiTaskRole) | Module 7.5 and 7.7 | Nothing — this identity mechanism doesn't exist on kind |
Managed node group (andes-cargo-managed-ng) | Module 7.3 and 7.7 | kind's two andes-cargo-cluster-worker/worker2 containers |
ECR repository (andes-cargo-status-api) | Implicit in table row 2 | The local registry:3 registry integrated with kind (Module 1) |
AWS Load Balancer Controller installed (with its own IAMServiceAccount) | Module 7.6 | ingress-nginx (Module 4) |
IngressClass alb | Module 7.6 and 7.7 | The nginx IngressClass ingress-nginx created automatically |
The lab infrastructure: what carries over, what becomes unnecessary
The lab's own real infrastructure namespaces — gitea, argocd, gatekeeper-system, kyverno — were never "Andes Cargo resources" (Module 1's own architecture already documented that); this section closes, with the same honesty as the rest of this guide, what happens to each one in a real migration. It also adds the piece this lab never installed:
- The data layer (DynamoDB, via
/shipments) — never installed in this lab, is where/shipments/<id>would move from representative to real. This is the most honest piece of this section: no module in this guide installed LocalStack (or anylocalstacknamespace) insideandes-cargo-cluster— that's why/shipments/<id>stayed representative since Module 2. On a real EKS, with a real AWS account available, a team would wireandes-cargo-status-apidirectly against the realShipmentsDynamoDB table (removingDYNAMODB_ENDPOINT_URLfrom theConfigMap, row 4 in the table above) — nothing would need simulating anymore, because real AWS would already be available. This is, precisely, the point in this migration where/shipments/<id>would stop being representative. gitea(Helm chart, Module 5) — still valid, no mandatory change. Gitea is a self-contained Git server, with no dependency on LocalStack or onkind— the same official Helm chart runs the same inside an EKS cluster. A real team could keep it (if it prefers self-hosted Git) or migrate the content to a managed provider (GitHub, GitLab) — neither is mandatory just from migrating to EKS itself.argocd(official manifest, Module 5) — no change. ArgoCD has no dependency onkind— the same pull-based sync mechanism runs the same, pointing at the same Gitea repository (or wherever the team decides to move it), against a differentdestination.server.gatekeeper-system/kyverno(Module 6) — no change. Confirmed when opening this project: neither Gatekeeper nor Kyverno has any field dependent on the underlying cloud — the same GatekeeperConstraintTemplate/Constraintand the same KyvernoClusterPolicywould apply, without editing a single line, against a real EKS cluster.
LAB INFRASTRUCTURE, AFTER MIGRATING
gitea ✓ STILL VALID — no mandatory change
argocd ✓ STILL VALID — no change
gatekeeper-system ✓ STILL VALID — no change
kyverno ✓ STILL VALID — no change
The four real infrastructure namespaces of this lab survive
the migration with no adjustment. The data layer (LocalStack)
was never a fifth namespace of this lab to "decommission" —
it was never installed — so there's nothing to shut down there:
instead, there's a real connection to wire up for the first time.
Common mistakes
Assuming "migrating to EKS" means rewriting the andes-cargo-k8s/ repository from scratch (effort overestimation). What happens: someone, hearing "real production migration," assumes a complete rewrite effort. How to spot it: if your effort estimate for this migration doesn't distinguish "edit five files with pinpoint changes" from "rewrite ten files from scratch." How to fix it: this project's table demonstrates, with evidence against each file's real content, that 50% doesn't change, and that none of the five that do change needs more than a handful of fields — this is exactly the evidence that corrects that overestimation.
Forgetting secret.yaml in the final count, treating it as "a file that changes" instead of "a file that disappears" (categorization). What happens: someone classifies secret.yaml in the same category as deployment.yaml or ingress.yaml ("changes"), without noticing the real conclusion is stronger: that file stops applying entirely. How to spot it: if your own summary says "6 of 10 files change" instead of "4 change, 1 becomes unnecessary, 5 don't change." How to fix it: go back to row 5 of the table — the exact reason (IRSA/Pod Identity completely replaces the need for static credentials) is stronger than a simple "value change": it's the complete elimination of the object category, not a content adjustment.
Thinking networkpolicy-allow-ingress-nginx.yaml needs no change because "NetworkPolicy doesn't depend on the cloud" (incorrect generalization from the same table's row 8). What happens: someone, seeing that networkpolicy-default-deny.yaml (row 8) doesn't change, generalizes that conclusion to the module's other NetworkPolicy. How to spot it: if your migration plan treats both network policies identically. How to fix it: the difference isn't whether a NetworkPolicy "depends on the cloud" in general — none does, as an object — but whether its specific selector references something that only exists in kind's topology with ingress-nginx running inside the cluster. default-deny uses podSelector: {}, with no external reference — it survives intact. allow-ingress-nginx explicitly selects a namespace that would have no real Pod running there in the EKS topology with the AWS Load Balancer Controller — that specific selection is what fails, not the NetworkPolicy concept in general.
Exercises
Exercise 1 — Recite the final count from memory. Without looking at this project's "The final count" section, write: how many of andes-cargo-k8s/'s ten files don't change, how many change in specific fields, and how many become unnecessary? Name each file in its correct category.
See solution
Unchanged (5): namespace.yaml, service.yaml, hpa.yaml, networkpolicy-default-deny.yaml, application.yaml. Change (4): deployment.yaml (image + serviceAccountName), configmap.yaml (removes one of three keys), ingress.yaml (ingressClassName + annotations), networkpolicy-allow-ingress-nginx.yaml (namespaceSelector → ipBlock). Become unnecessary (1): secret.yaml. If your count added up to 5+4+1=10 with no need to reread the table, you've mastered this project's central result.
Exercise 2 — Explain why secret.yaml disappears, without using the word "IRSA" or "Pod Identity." A colleague who hasn't reached this module's lesson 5 yet asks you why secret.yaml "becomes unnecessary" instead of just "changing values." Explain it to them in two sentences, without naming any specific AWS mechanism.
See solution
A complete explanation sounds, roughly, like this: "That file exists today only to give the application an AWS credential it can read, because there's no other way for a Pod on kind to prove who it is to a simulated AWS service. On a real EKS cluster, the cluster itself can automatically give that Pod a temporary identity, with no file needing to store any key or secret — the application simply stops needing to read any credential from anywhere."
Exercise 3 — Predict what would happen if someone applied networkpolicy-allow-ingress-nginx.yaml with no changes against an EKS cluster with the AWS Load Balancer Controller. Based on row 9 of this project's table, predict: if that file were applied with no change, would the ALB's traffic to andes-cargo-status-api pass, or get blocked? Justify with the exact mechanism.
See solution
It would get blocked. The default-deny policy (row 8, unchanged) keeps rejecting all incoming traffic by default; the only exception the andes-cargo namespace would have is the allow-ingress-nginx rule, which allows traffic specifically from Pods with the ingress-nginx namespace label. Since the ALB's traffic doesn't originate from any Pod in that namespace — nor from any namespace of the cluster, because the ALB lives outside the cluster — that namespaceSelector would never match, and the load balancer's real traffic would get blocked by the underlying default-deny policy, exactly as if no exception rule existed. That's the exact reason row 9 of the table marks this file as "changes," not "unchanged."
Summary and next step
This project built the only document with executed evidence in all of Module 7: a resource-by-resource table against andes-cargo-k8s/'s ten real manifests — the same repository Gitea serves and ArgoCD actually syncs since Module 5. The result, verified twice: 5 of 10 files don't change, 4 change in specific, bounded fields, and 1 (secret.yaml) becomes entirely unnecessary, replaced by IRSA/EKS Pod Identity. You also confirmed which new pieces get added around the repository (cluster.yaml, an IAMServiceAccount, a managed node group, an ECR repository, the AWS Load Balancer Controller), and what happens to the lab's own infrastructure: Gitea, ArgoCD, Gatekeeper, and Kyverno survive the migration with no adjustment, while the data layer — never installed as LocalStack in this lab — is where /shipments/<id> would stop being representative and start responding with real DynamoDB data.
Before closing this module you should be able to: recite the final count (5/4/1) from memory, with each file in its correct category; explain why secret.yaml disappears without naming any specific mechanism; and describe what would happen if networkpolicy-allow-ingress-nginx.yaml were applied without the change row 9 of this project identified.
This project closes Module 7 — the most honest module in this guide about what really costs real money, and the only one that deliberately left most of its content unexecuted. Module 8, the final capstone, picks andes-cargo-cluster back up on kind — the complete system, with Pods, networking, GitOps, and runtime guardrails, all executed — for a final end-to-end walkthrough: a change that crosses the whole gate, and one the gate stops.
Resources
kubernetes-and-eks-in-production-guide(NIEVA), Module 5, lesson 3 — the exact source of the original count ("nine manifests inherited from Modules 1-4") this project extends to ten withapplication.yaml.kubernetes-and-eks-in-production-guide(NIEVA), Module 3, lesson 4 — the literal content ofconfigmap.yaml/secret.yaml, the base for rows 4 and 5 of this project's table.kubernetes-and-eks-in-production-guide(NIEVA), Module 4, lesson 7 — the literal content ofnetworkpolicy-allow-ingress-nginx.yaml, the base for row 9.kubernetes-and-eks-in-production-guide(NIEVA), Module 7, lessons 3, 5, 6, and 7 — the vocabulary and representative syntax (node groups, IRSA/Pod Identity, AWS Load Balancer Controller,cluster.yaml) this project applies to the real repository.- Amazon EKS — What is Amazon EKS? — general reference for this migration's target service.