Module 7: Eks Specifics For Production
7. Hands-on: the EKS YAML, shown and explained
Description
This module's previous six lessons built the vocabulary and syntax piece by piece: the managed control plane, the three forms of compute, Karpenter, IRSA/Pod Identity, the AWS Load Balancer Controller. This lesson gathers them into a single flow, start to finish, exactly as a real team would write it preparing andes-cargo-cluster's migration: an eksctl configuration file, an IAMServiceAccount, and the AWS Load Balancer Controller's IngressClass. None of this runs. Every block is verified, field by field, against the official eksctl.io/docs.aws.amazon.com/eks documentation cited at the foot of each section — but the "(representative)" label accompanies every command and every expected output, with no exception.
Connection to the module
This is, deliberately, this module's "hands-on" lesson — with the name you already saw in lesson 1's map: not "hands-on executed," like in the previous six modules, but "the YAML shown and explained." If you got here expecting to see a real terminal with eksctl create cluster running against an AWS account, go back to lesson 1 — that expectation got corrected there, before this lesson started.
Step 1 — The cluster configuration file: cluster.yaml
eksctl, the official EKS command-line tool, accepts two ways of creating a cluster: loose terminal flags, or a declarative configuration file (ClusterConfig). For a real production cluster — with more than one node type, a fixed region, Fargate profiles and managed node groups coexisting — the configuration file is the pattern eksctl's own documentation recommends, for the same reason you already know from Terraform: it's reproducible, reviewable in a pull request, and doesn't depend on someone remembering the exact flag sequence.
cluster.yaml (representative) — verified, field by field, against docs.aws.amazon.com/eks/latest/eksctl, rebuilding the andes-cargo-cluster cluster with what this module's lessons 2-6 already covered:
apiVersion: eksctl.io/v1alpha5
kind: ClusterConfig
metadata:
name: andes-cargo-cluster
region: us-east-1
version: "1.34"
managedNodeGroups:
- name: andes-cargo-managed-ng
instanceType: t3.medium
desiredCapacity: 2
minSize: 2
maxSize: 4
privateNetworking: true
fargateProfiles:
- name: andes-cargo-fargate-batch
selectors:
- namespace: andes-cargo
labels:
workload-type: batch
iam:
withOIDC: true
Every top-level block answers a different question, exactly like you already learned to read an ArgoCD Application in Module 5:
metadata.name: andes-cargo-cluster— the exact same name, reused for the third time in this ecosystem:aws-serverless-and-containers-guideleft it documented on ECS,kindmade it real in this guide's Module 1, and here it appears as the name the real EKS cluster would have if this migration were executed.managedNodeGroups— the form of compute this module's lesson 3 recommended forandes-cargo-status-api: no special AMI requirements,always-on, the default starting point.fargateProfiles— added here only as a syntax example (not becauseandes-cargo-status-apineeds it): a Fargate profile for future batch workloads in theandes-cargonamespace, showing both forms of compute can coexist on the same cluster.iam.withOIDC: true— the field that creates the cluster's OIDC provider, the exact piece IRSA needs (lesson 5) to be able to exist. Without this field set totrue, noIAMServiceAccountwith IRSA from this same file could work.
Application command (representative):
eksctl create cluster -f cluster.yaml
What to expect (representative) — the event sequence documented by eksctl for creating a cluster with this configuration, rebuilt from the official reference, never seen on a real terminal for this lesson:
[ℹ] eksctl version 0.<version>
[ℹ] using region us-east-1
[ℹ] setting availability zones to [us-east-1a us-east-1b]
[ℹ] subnets for us-east-1a - public:192.168.0.0/19 private:192.168.32.0/19
[ℹ] subnets for us-east-1b - public:192.168.64.0/19 private:192.168.96.0/19
[ℹ] nodegroup "andes-cargo-managed-ng" will use "" [AmazonLinux2/1.34]
[ℹ] using Kubernetes version 1.34
[ℹ] creating EKS cluster "andes-cargo-cluster" in "us-east-1" region with managed nodes
[ℹ] will create 2 separate CloudFormation stacks for cluster itself and the initial managed nodegroup
[ℹ] if you encounter any issues, check CloudFormation console or try 'eksctl utils describe-stacks --region=us-east-1 --cluster=andes-cargo-cluster'
[ℹ] building cluster stack "eksctl-andes-cargo-cluster-cluster"
[ℹ] deploying stack "eksctl-andes-cargo-cluster-cluster"
[ℹ] building managed nodegroup stack "eksctl-andes-cargo-cluster-nodegroup-andes-cargo-managed-ng"
[ℹ] deploying stack "eksctl-andes-cargo-cluster-nodegroup-andes-cargo-managed-ng"
[ℹ] waiting for the control plane to become ready
[✔] saved kubeconfig as "~/.kube/config"
[ℹ] EKS cluster "andes-cargo-cluster" in "us-east-1" region is ready
This process's real duration, consistently documented by AWS and the community, is 10 to 15 minutes — orders of magnitude slower than Module 1's kind create cluster, which finished in under a minute. That time difference, by itself, is already indirect evidence of the underlying difference between the two: kind boots local Docker containers; eksctl orchestrates, underneath, complete CloudFormation stacks that create a VPC, subnets, a real managed control plane, and an Auto Scaling Group with real EC2 instances.
Step 2 — An IAMServiceAccount with IRSA, for andes-cargo-status-api
With iam.withOIDC: true already applied (Step 1), the cluster has its own OIDC provider. Now the ServiceAccount with IRSA this module's lesson 5 already explained can be declared, this time inside the same cluster.yaml instead of as a loose eksctl create iamserviceaccount command — the pattern a real team would prefer to keep the cluster's entire state in one versioned file:
Fragment added to cluster.yaml (representative):
iam:
withOIDC: true
serviceAccounts:
- metadata:
name: status-api-service-account
namespace: andes-cargo
attachPolicyARNs:
- "arn:aws:iam::000000000000:policy/StatusApiReadOnlyPolicy"
roleName: StatusApiTaskRole
What to expect (representative) when applying this change with eksctl update iamserviceaccount -f cluster.yaml --approve on an already-existing cluster:
[ℹ] 1 iamserviceaccount (andes-cargo/status-api-service-account) was included (based on the include/exclude rules)
[ℹ] 1 task: { create IAM role for serviceaccount "andes-cargo/status-api-service-account" }
[ℹ] building iamserviceaccount stack "eksctl-andes-cargo-cluster-addon-iamserviceaccount-andes-cargo-status-api-service-account"
[ℹ] deploying stack "eksctl-andes-cargo-cluster-addon-iamserviceaccount-andes-cargo-status-api-service-account"
[ℹ] created serviceaccount "andes-cargo/status-api-service-account"
The result, inside the cluster, is exactly the annotated ServiceAccount lesson 5 already showed:
kubectl describe serviceaccount status-api-service-account -n andes-cargo
Name: status-api-service-account
Namespace: andes-cargo
Annotations: eks.amazonaws.com/role-arn: arn:aws:iam::000000000000:role/StatusApiTaskRole
Image pull secrets: <none>
Mountable secrets: <none>
Tokens: <none>
And the final piece, andes-cargo-status-api's Deployment referencing that ServiceAccount — the only new field compared to Module 2's real deployment.yaml, added with no other existing field touched:
spec:
template:
spec:
serviceAccountName: status-api-service-account
containers:
- name: andes-cargo-status-api
image: 000000000000.dkr.ecr.us-east-1.amazonaws.com/andes-cargo-status-api:latest
Step 3 — The AWS Load Balancer Controller's IngressClass
This lesson's final piece is the IngressClass this module's lesson 6 already explained in depth, shown here as part of the complete configuration flow, with andes-cargo-status-api's real Ingress pointing at it:
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: alb
annotations:
ingressclass.kubernetes.io/is-default-class: "true"
spec:
controller: ingress.k8s.aws/alb
Application command (representative):
kubectl apply -f ingressclass.yaml
What to expect (representative):
ingressclass.networking.k8s.io/alb created
With the ingressclass.kubernetes.io/is-default-class: "true" annotation, any new Ingress that does not explicitly declare an ingressClassName would use this class by default — a convenience decision the project itself documents, useful when a cluster has a single Ingress controller installed (unlike a cluster with ingress-nginx and the AWS Load Balancer Controller coexisting, where explicitly declaring ingressClassName on every Ingress, as Module 4's real ingress.yaml already does, avoids any ambiguity).
The complete flow, at a glance
THIS LESSON'S COMPLETE REPRESENTATIVE FLOW
1. eksctl create cluster -f cluster.yaml
└─ creates VPC, subnets, control plane (AWS), managed node group,
Fargate profile, and the OIDC provider (iam.withOIDC: true)
│
2. eksctl update iamserviceaccount -f cluster.yaml --approve
└─ creates the StatusApiTaskRole role + the annotated ServiceAccount
(IRSA, this module's lesson 5)
│
3. helm install aws-load-balancer-controller ...
└─ the controller itself needs ITS OWN IAMServiceAccount
(this module's lesson 6), before this step
│
4. kubectl apply -f ingressclass.yaml
└─ declares the "alb" class for the Ingress to use
│
5. kubectl apply -f andes-cargo-k8s/ (the same repository from M5)
└─ deployment.yaml (with serviceAccountName added),
service.yaml, ingress.yaml (with ingressClassName: alb) — ALL
EXECUTED IN LESSON 8'S REAL DOCUMENT
This diagram's last step is, deliberately, the direct connection to lesson 8: the andes-cargo-k8s/ repository ArgoCD already actually syncs on kind (Module 5) is the same repository that, with this module's specific adjustments, would apply against a real EKS cluster — the next lesson's migration document details, resource by resource, exactly which of those adjustments are needed.
Common mistakes
Copying and running these commands against a real AWS account without having read this module's lesson 1 (cost expectation). What happens: someone, motivated by seeing complete, real syntax, copies eksctl create cluster -f cluster.yaml and runs it against their own AWS account, with no cost calculation done. How to spot it: if you're about to run any command from this lesson against an AWS account with active billing, without first having checked an EKS control plane's approximate cost (an hourly rate, documented by AWS, on top of the cost of the EC2 nodes this cluster.yaml also creates). How to fix it: if you want to experiment with this syntax against a real account, do it with explicit knowledge of the cost and a clear cleanup plan (eksctl delete cluster) — this lesson shows it for pedagogical purposes, verified against official documentation, not as an invitation to run it with no cost evaluation first.
Assuming managedNodeGroups and fargateProfiles are mutually exclusive inside the same cluster.yaml (technical). What happens: someone thinks a cluster has to pick a single form of compute, and is surprised to see both blocks in this lesson's same file. How to spot it: if your own cluster.yaml declares only one of the two sections, assuming it's mandatory to pick one. How to fix it: as you already saw in this module's lesson 3, a real EKS cluster can combine managed node groups, self-managed nodes, and Fargate profiles at the same time — this lesson's cluster.yaml demonstrates it on purpose, with a managed node group for the main workload and a Fargate profile for a separate secondary one.
Forgetting andes-cargo-status-api's IAMServiceAccount and the AWS Load Balancer Controller's own are two completely different resources (overlap of neighboring concepts). What happens: someone confuses the ServiceAccount Step 2 creates for the application's code with the one lesson 6 already showed for the load balancer controller itself. How to spot it: if your application plan includes only one IAMServiceAccount, expecting it to serve both purposes. How to fix it: they're two separate identities, with different roles and policies — status-api-service-account (with StatusApiTaskRole, read-only permission over Shipments) is for your application's code; aws-load-balancer-controller (with AWSLoadBalancerControllerIAMPolicy) is for the controller itself, which needs permission to create/modify load balancers on your account — the same least-privilege discipline, applied to two different consumers.
Exercises
Exercise 1 — Trace which cluster.yaml field enables each piece of this module. Without looking back at Step 1, say which exact cluster.yaml field corresponds to: (a) the OIDC provider IRSA needs; (b) the recommended form of compute for andes-cargo-status-api; (c) the control plane's Kubernetes version.
See solution
(a) iam.withOIDC: true. (b) managedNodeGroups (with the name andes-cargo-managed-ng). (c) metadata.version ("1.34" in this lesson's example). If you named all three without rereading Step 1, you have the file's complete anatomy clear.
Exercise 2 — Explain why andes-cargo-status-api's real Deployment only needs one new field. A colleague asks you how many fields of the real deployment.yaml (Module 2) would need to change to run on EKS with IRSA. Answer them with the exact number and the field's name.
See solution
Just one: spec.template.spec.serviceAccountName, pointing at status-api-service-account. Everything else in the Deployment — replicas, resources.limits/requests, probes (Module 3), the label selector — stays exactly the same. The only other real change, mentioned in Step 2, is the image reference (image:), which would move from kind's local registry:3 registry to a real ECR repository — unrelated to IRSA itself, just to where the image lives.
Exercise 3 — Predict what would happen if you applied ingressclass.yaml without having installed the AWS Load Balancer Controller first. Based on what you already know about IngressClass as a Kubernetes object (Module 4) and this module's lesson 6, predict: if you apply this lesson's IngressClass before the controller is running, does the command fail?
See solution
It doesn't fail — an IngressClass is, by itself, just a declarative object Kubernetes accepts and stores in etcd, with no check for whether any real controller is listening for that name. kubectl apply -f ingressclass.yaml would complete successfully (ingressclass.networking.k8s.io/alb created) exactly as if the controller were already running. The problem would show up later, only once someone applies a real Ingress with ingressClassName: alb: that object would be left with no address assigned (empty ADDRESS), because no active controller would claim that class — the same symptom, from a different cause, as lesson 6's "Common mistakes."
Summary and next step
This lesson gathered, into a single flow verified against eksctl.io and docs.aws.amazon.com/eks, all of this module's YAML/CLI: a complete cluster.yaml (managed node group, Fargate profile, OIDC provider), an IAMServiceAccount with IRSA for status-api-service-account/StatusApiTaskRole, and the AWS Load Balancer Controller's IngressClass — every block explicitly labeled as representative, none executed against a real AWS account. You confirmed andes-cargo-status-api's real Deployment (Module 2) would only need one new field (serviceAccountName) to run with IRSA on EKS, and traced the complete dependency flow: first the cluster, then the identities, only at the end the application objects.
Before moving on you should be able to: explain which cluster.yaml field enables each piece from this module's lessons 2-6; name the single new field the real Deployment would need; and describe the complete dependency sequence, from the cluster to the Ingress.
Next lesson: this module's project — Andes Cargo's migration plan, kind → real EKS. There you're going to build this module's only document with genuinely executed evidence: a resource-by-resource table of what changes and what doesn't change in andes-cargo-k8s/ — built against that repository's ten real manifests, not against any external API.
Resources
- Amazon EKS — Creating and managing clusters (
eksctl) — the exact source for theClusterConfigformat and theeksctl create cluster -f cluster.yamloutput rebuilt in this lesson. eksctl— IAM Service Accounts — complete syntax for theiam.serviceAccountsblock inside aClusterConfig.- Amazon EKS — Assign IAM roles to Kubernetes service accounts — cross-verification of the resulting annotated
ServiceAccount. - AWS Load Balancer Controller — IngressClass — the exact source for this lesson's
IngressClass. kubernetes-and-eks-in-production-guide(NIEVA), Module 2, lesson 5 —andes-cargo-status-api's realdeployment.yaml, the base file this lesson adds a single new field to.