AI EngineeringZero to ProductionHome·About·Contact
AWS AI Automation · Chapter W15

AI/ML on EKS

The AWS-specific angle on running AI/ML yourself: when EKS beats SageMaker or Bedrock, GPU-node autoscaling with Karpenter, the Inferentia/Trainium (Neuron) accelerators, Spot GPUs and cost governance, and serving models the AWS way with S3 / ECR / IRSA / ALB. Assumes EKS basics (K5) and generic AI-on-K8s (K7) — this lesson does not repeat them.

⏱️ ~1.5 hours🧪 AWS × EKS🎯 Advanced→Expert
⚙️ To run this for realEverything here needs real AWS infrastructure:
  • An EKS cluster + AWS credentials (aws configure), plus eksctl/kubectl and (for Karpenter) a Karpenter install — all of this costs money (GPU nodes especially).
  • The YAML/CLI is illustrative and correct in shape, but Karpenter CRDs, Neuron device-plugin manifests, instance types and prices change — verify every one against the current AWS / Karpenter / Neuron docs before relying on it.
The cost-math snippets run offline with plain Python — no AWS needed. IDs/ARNs are placeholders; swap in your own.
🌱 What this lesson is NOTIt is not another EKS primer and not another GPU-scheduling walkthrough. nodeSelectors, taints/tolerations, the NVIDIA device plugin, PVC weights, the autoscale-on-queue signal, KServe, and AIOps/RBAC safety are all covered generically in K7; managed-EKS basics (control plane, node groups, Fargate, IAM) in K5. Here we add only the AWS-specific layer on top of that knowledge.

Learning objectives

  • Decide between EKS, SageMaker, and Bedrock for a given AI workload — and mix them.
  • Autoscale GPU nodes with Karpenter (vs Cluster Autoscaler): consolidation and instance flexibility.
  • Know when Inferentia/Trainium (Neuron) beats GPU — and the compatibility tax you pay.
  • Use Spot GPUs for interruptible AI and make GPU cost on EKS visible and bounded.
  • Serve a model the AWS way: pull from S3, images from ECR, access via IRSA, expose via ALB/NLB.

When EKS for AI: the EKS vs SageMaker vs Bedrock decision

AWS gives you three very different ways to run AI, and the mistake is treating them as competitors when they are layers. Bedrock is a managed model API — you call a foundation model (W2) and never see a server. SageMaker (W8/W9) is a managed ML platform — you own the model but AWS owns the serving/ training plumbing. EKS is self-run Kubernetes — you own everything down to the node, in exchange for maximum control and portability. You move down that list only when the layer above genuinely cannot meet a hard requirement.

Bedrock managed API SageMaker managed platform EKS self-run k8s
Signal in the requirementLands onWhy
Call an FM (Claude, Titan…) by API; no infraBedrockPay-per-token, scales to zero, no servers (W2)
Host/fine-tune your own model, want managed endpointsSageMakerOwns serving, autoscaling, pipelines (W8/W9)
Already run everything on Kubernetes; want one control planeEKSAI shares the same cluster, RBAC, GitOps, tooling
Need custom serving stack (vLLM, KServe, sidecars, Neuron)EKSFull control of the runtime, not a fixed endpoint contract
Multi-cloud / portability / avoid platform lock-inEKSPlain k8s runs anywhere; SageMaker/Bedrock do not
Small team, no k8s expertise, wants fastest pathBedrock/SageMakerEKS is a lot of undifferentiated ops to carry
Hybrid is the common real answerThese mix cleanly. A typical stack: Bedrock for the LLM calls (managed, no GPU to babysit) + EKS for your own microservices, RAG orchestration, and any custom/self-hosted models — the EKS pods reach Bedrock over the API using IRSA (section 5). Or SageMaker endpoints for the heavy model, EKS for the app tier. Choosing "EKS" rarely means "EKS for everything".
Don't pick EKS to save money by defaultSelf-running GPU inference on EKS is not automatically cheaper than a SageMaker endpoint or Bedrock tokens — you take on cluster ops, GPU right-sizing, and the risk of idle GPUs burning money 24/7. EKS wins on control and portability; the cost win only shows up at sustained, well-utilised scale (section 4). Verify current pricing before you assume a saving.

GPU node autoscaling with Karpenter

K7 covered scheduling a pod onto a GPU node; the AWS question is where do the GPU nodes come from. The classic answer is Cluster Autoscaler, which scales pre-defined node groups (Auto Scaling Groups) up and down. Karpenter is the AWS-native alternative: instead of fixed groups, it looks at pending pods and provisions right-sized EC2 instances directly, picking instance types from a flexible set — then consolidates (repacks and terminates under-used nodes) as load falls.

Cluster AutoscalerKarpenter
Unit it scalesPre-defined node groups (ASGs)Individual EC2 instances, chosen just-in-time
Instance-type flexibilityFixed per groupA wide set you allow; picks per pending pod
ConsolidationLimitedActively repacks & removes under-used nodes
GPU fitMust pre-size GPU groupsProvisions the GPU shape the pending pod needs
Spot handlingVia ASG mixed instancesFirst-class: Spot + on-demand in one NodePool

For GPU AI that means: a burst of pending vLLM pods requesting nvidia.com/gpu can cause Karpenter to launch, say, a g5 instance in seconds; when the burst clears, consolidation tears it back down so you are not paying for an idle GPU. You express what is allowed with two CRDs — a NodePool (scheduling constraints/limits) and an EC2NodeClass (the AWS-level details: AMI, subnets, IAM role).

Example code for learning — review, test, and adapt it before running against real or production systems. Commands can create, change, or delete resources. See the Terms & Disclaimer.
YAML · a Karpenter GPU NodePool + EC2NodeClass (needs an EKS cluster + Karpenter; verify against current Karpenter CRDs)
gpu-nodepool.yamlapiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: gpu
spec:
  template:
    spec:
      # only launch GPU instance families; let Karpenter pick the cheapest that fits
      requirements:
        - key: karpenter.k8s.aws/instance-family
          operator: In
          values: ["g5", "g6", "p4d", "p5"]     # verify families/availability in your region
        - key: karpenter.sh/capacity-type
          operator: In
          values: ["spot", "on-demand"]          # allow both; Spot preferred when available
      taints:
        - key: nvidia.com/gpu                     # repel non-GPU pods (K7)
          effect: NoSchedule
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: gpu
  limits:
    nvidia.com/gpu: 16          # hard cap on total GPUs this pool may create (cost guardrail)
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 2m        # reclaim idle/under-used GPU nodes quickly
---
apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: gpu
spec:
  amiFamily: AL2023            # verify the current GPU-capable AMI family/alias
  role: "KarpenterNodeRole-my-cluster"   # the node IAM role (placeholder)
  subnetSelectorTerms:   [{ tags: { "karpenter.sh/discovery": "my-cluster" } }]
  securityGroupSelectorTerms: [{ tags: { "karpenter.sh/discovery": "my-cluster" } }]
Karpenter CRDs and instance families move — verifyAPI groups/versions (karpenter.sh/v1, karpenter.k8s.aws/v1), field names, and which GPU families (g5/g6/p4d/p5) exist and are available in your region have all changed across Karpenter releases. Treat the YAML above as shape, not gospel — check the current Karpenter docs and your region's instance availability. The portable idea is: constrain to GPU families, allow Spot, cap total GPUs, consolidate aggressively.
The limits block is a cost guardrailThat limits: nvidia.com/gpu: 16 is the cheapest insurance on this page: it hard-caps how many GPUs the pool can ever spin up, so a runaway HPA or a bad manifest can't provision 200 GPUs overnight. Pair it with consolidation and you bound both the count and the idle time.

AWS AI accelerators: Inferentia & Trainium (Neuron)

GPUs are not the only silicon on AWS. Amazon builds custom AI chips: Inferentia (inf2 instances) for inference and Trainium (trn1/newer) for training. You program them through the Neuron SDK, and on EKS a Neuron device plugin advertises the chips as a schedulable resource — the same idea as the NVIDIA device plugin in K7, but the resource is aws.amazon.com/neuron instead of nvidia.com/gpu.

DimensionGPU (g5/p4/p5)Neuron (inf2/trn1)
Schedulable resourcenvidia.com/gpuaws.amazon.com/neuron
Best atGeneral; broad model/framework supportCost/throughput on supported models
ToolchainCUDA — huge ecosystemNeuron SDK — you compile the model first
Model compatibilityRuns almost anythingMust be supported/compilable for Neuron
PortabilityRuns on any GPU cloudAWS-only silicon

The pitch is cost and throughput: for a model that Neuron supports well, inf2 can serve more tokens per dollar than a comparable GPU. The tax is real, though: you must compile the model with the Neuron compiler ahead of time, and not every model/operator is supported — the ecosystem is narrower and younger than CUDA's. It is a genuine win for high-volume inference on a supported architecture, and a frustrating dead end if your model isn't supported. Prototype the compile step before committing.

YAML · scheduling a pod onto an inf2 Neuron node (needs an EKS cluster + Neuron device plugin; verify vs current Neuron docs)
neuron-pod.yaml# The Neuron device plugin (a DaemonSet, like K7's NVIDIA one) must be installed first;
# it advertises aws.amazon.com/neuron as a schedulable resource.
apiVersion: v1
kind: Pod
metadata:
  name: neuron-inference
spec:
  nodeSelector:
    node.kubernetes.io/instance-type: inf2.xlarge   # verify the instance type exists in your region
  containers:
    - name: server
      image: <account>.dkr.ecr.<region>.amazonaws.com/my-neuron-model:pinned  # compiled for Neuron
      resources:
        limits:
          aws.amazon.com/neuron: 1        # request 1 Neuron device (NOT nvidia.com/gpu)
Be honest about Neuron maturityNeuron is a legitimate cost lever, not a free lunch. Instance types, the device-plugin manifest, the resource name, and — above all — which models compile and run well shift release to release. Do not design a platform around Neuron until you have compiled and load-tested your model on it. Verify everything against the current AWS Neuron documentation.

Spot GPUs & cost governance on EKS

The single biggest EKS-AI cost lever is Spot capacity: EC2 spare capacity at a large discount off on-demand, in exchange for the fact that AWS can reclaim the instance with a short (~2-minute) warning. That reclaim risk is exactly why Spot fits interruptible AI work — batch/offline inference, experimentation, and fault-tolerant training with checkpointing — but is a poor fit for a single-replica, latency-critical endpoint that can't absorb a sudden node loss.

WorkloadSpot?Why
Batch / offline inference✅ yesRetryable; a lost node just reschedules the job
Training with checkpointing✅ yesResume from the last checkpoint after a reclaim
Dev / experimentation✅ yesInterruptions are cheap here
User-facing low-latency endpoint⚠️ mixedKeep on-demand base capacity; Spot only for burst
Single-replica critical service❌ noOne reclaim = an outage

Handle interruptions rather than hope: run enough replicas across instances so losing one is survivable, set a PodDisruptionBudget, and let a node-interruption handler (Karpenter consumes the interruption/rebalance signals natively) drain the node gracefully on the 2-minute warning. For training, checkpoint frequently so a reclaim costs minutes, not the run.

Make GPU cost visible before you try to cut itYou cannot govern what you cannot see. Tag GPU workloads (cost-allocation tags on the nodes/namespaces) so spend is attributable per team/model; watch GPU-node utilisation in CloudWatch (or Container Insights) and via Kubecost for per-namespace k8s cost. The number that matters most is idle GPU time — a GPU node at 5% utilisation is the classic silent EKS-AI bill, and the reason Karpenter consolidation (section 2) is a cost feature, not just a scheduling one.

How does GPU-on-EKS cost compare to a SageMaker endpoint or Bedrock tokens? Qualitatively: Bedrock bills per token and scales to zero — cheapest when traffic is spiky or low, no idle cost at all. SageMaker endpoints bill per instance-hour (like a reserved GPU) but AWS runs the serving. EKS is also per instance-hour but you own utilisation — it only beats the others when you keep the GPUs genuinely busy and amortise the ops effort. Idle capacity flips EKS from cheapest to most expensive. The offline calculator in the exercises below makes this concrete; verify current prices before trusting any absolute number.

Serving LLMs on EKS the AWS way

K7 showed the generic serving mechanics (weights on a PVC, vLLM, KServe, autoscale on queue depth). The AWS-specific glue is where the bytes and permissions come from: model weights live in S3, container images in ECR, the inference endpoint is exposed through the AWS Load Balancer Controller (ALB for HTTP, NLB for raw TCP/gRPC), and — critically — pods get AWS permissions via IRSA (IAM Roles for Service Accounts), not baked-in keys.

S3 weights initContainer pull via IRSA Pod (vLLM) serves model ALB/NLB ingress

IRSA is the AWS-native way to give a pod least-privilege AWS access. You annotate the pod's ServiceAccount with an IAM role ARN; the pod then assumes that role via the cluster's OIDC provider and gets temporary credentials scoped to exactly that role — no long-lived keys in the image or env. This is the mechanism a pod uses to read model weights from S3, or to call Bedrock in the hybrid pattern from section 1.

YAML · an IRSA ServiceAccount + a pod that pulls weights from S3 (needs an EKS cluster with an OIDC provider + the IAM role; costs money)
irsa-s3-serve.yaml# 1) A ServiceAccount annotated with the IAM role it should assume (IRSA).
apiVersion: v1
kind: ServiceAccount
metadata:
  name: model-loader
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/eks-model-reader  # placeholder
---
# 2) A pod that uses that SA; an initContainer syncs weights from S3 before vLLM starts.
apiVersion: v1
kind: Pod
metadata:
  name: llm
spec:
  serviceAccountName: model-loader          # -> pod gets the role's temporary creds via IRSA
  initContainers:
    - name: fetch-weights
      image: amazon/aws-cli:latest          # pin a version in prod
      command: ["aws", "s3", "sync", "s3://my-models/my-llm/", "/models/my-llm/"]
      volumeMounts: [ { name: models, mountPath: /models } ]
  containers:
    - name: vllm
      image: <account>.dkr.ecr.<region>.amazonaws.com/vllm:pinned   # image from ECR
      args: ["--model", "/models/my-llm"]
      resources: { limits: { nvidia.com/gpu: 1 } }
      volumeMounts: [ { name: models, mountPath: /models } ]
  volumes:
    - name: models
      emptyDir: {}
JSON · a least-privilege IAM policy for that role — read-only on exactly one model prefix (placeholder ARNs)
model-reader-policy.json{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadOneModelPrefix",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::my-models",
        "arn:aws:s3:::my-models/my-llm/*"
      ]
    }
  ]
}
KServe still applies — the details live in K7You can absolutely put KServe in front of this for a standard InferenceService, scale-to-zero, and canary rollouts — that generic serving layer is covered in K7. On AWS the only additions are the ones above: S3 for weights, ECR for images, ALB/NLB via the AWS Load Balancer Controller, and IRSA for permissions.
IRSA over static keys, alwaysNever bake an AWS access key into a model-serving image or a ConfigMap. IRSA gives short-lived, automatically-rotated, role-scoped credentials — a leaked pod can only do what that one role allows (here: read one S3 prefix). This is the AWS face of the least-privilege principle K7 enforces with RBAC. (Newer clusters may offer EKS Pod Identity as an alternative association mechanism — verify which your cluster uses.)

AI operating EKS on AWS

K7 built the generic AIOps agent (read-only kubectl diagnosis, RBAC/dry-run/approval as the enforced safety layers) and it ties back to the ch08 DevOps-agent capstone. The AWS-specific surface an agent touches on EKS is worth naming, because it widens the blast radius beyond the cluster: alongside the Kubernetes API there is the EKS control-plane API (managing node groups, add-ons), CloudWatch / Container Insights as the evidence source, and — the sharp edge — the pod's IAM boundary.

On EKS the agent has two boundaries, not oneA cluster agent on EKS is bounded by both its Kubernetes RBAC (what it can do to the cluster, per K7) and its IAM role via IRSA (what it can do to AWS — EC2, S3, Bedrock, the EKS API itself). An over-broad IAM role can let an "in-cluster" agent terminate nodes or read every bucket, entirely outside k8s RBAC. Scope both: narrow RBAC and a narrow IRSA policy. Diagnosis-first still applies — read CloudWatch and cluster state, propose fixes, gate every mutation. This is the ch08 safety gate with an extra AWS lock.

🪜 Practice ladder beginner → industry

Six graded exercises, easy to real-world. Try each before opening its solution.

Exercise 1 · Pick the right AWS layer for four workloadsBeginner

Context: Teams reach for EKS out of habit when Bedrock or SageMaker would be simpler and cheaper. The first skill is matching the workload to the layer.

Your task: For four workloads, choose Bedrock, SageMaker, or EKS and justify each in one line.

Requirements:

  • Workload A: a chat feature that calls Claude, tiny team, spiky traffic
  • Workload B: a fine-tuned open model to serve behind a managed endpoint, no k8s expertise
  • Workload C: LLM inference that must share one cluster/RBAC/GitOps with 30 existing microservices
  • Workload D: a self-hosted model that must also run on-prem later (portability)
  • Give a one-line reason per choice tied to control, cost, or portability

💡 Hint: Only go down the Bedrock → SageMaker → EKS ladder when the layer above cannot meet a hard requirement.

Show solution

A → Bedrock. Pay-per-token, scales to zero, no servers — ideal for a small team and spiky traffic; owning infra would be pure overhead.

B → SageMaker. You own the model but want a managed endpoint and no cluster ops — SageMaker's hosted serving/autoscaling fits, and the team has no k8s skills to justify EKS.

C → EKS. The deciding requirement is one control plane — sharing the existing cluster, RBAC, and GitOps with 30 services makes EKS the natural home even though Bedrock/SageMaker could serve the model.

D → EKS. Portability is the hard requirement: plain Kubernetes runs on-prem later; SageMaker and Bedrock are AWS-only and would lock you in.

The patternA and B stay high on the managed ladder; C and D drop to EKS only because a hard requirement (shared control plane / portability) forces it — not because EKS is "better".
Exercise 2 · Write a Karpenter GPU NodePool with a cost guardrailIntermediate

Context: Karpenter provisions GPU nodes just-in-time from pending pods, but an unbounded pool can spin up a fortune in GPUs. The professional version always caps and consolidates.

Your task: Write a Karpenter NodePool that launches only GPU instance families, allows Spot, hard-caps total GPUs, and consolidates idle nodes.

Requirements:

  • Constrain instance-family to GPU families only
  • Allow both spot and on-demand capacity types
  • Taint the nodes so non-GPU pods stay off (per K7)
  • Set a limits.nvidia.com/gpu cap as a cost guardrail
  • Enable consolidation so idle GPU nodes are reclaimed
  • Label it as needing a cluster + Karpenter and hedge the CRD versions

💡 Hint: The limits block and consolidation are the two lines that turn a scaling feature into a cost-control feature.

Show solution

A GPU NodePool that is flexible on instance type, Spot-friendly, capped, and self-consolidating:

YAML · GPU NodePool (needs EKS + Karpenter; verify CRD versions/fields against current Karpenter docs)
gpu-nodepool.yamlapiVersion: karpenter.sh/v1
kind: NodePool
metadata: { name: gpu }
spec:
  template:
    spec:
      requirements:
        - { key: karpenter.k8s.aws/instance-family, operator: In, values: ["g5","g6","p4d","p5"] }
        - { key: karpenter.sh/capacity-type, operator: In, values: ["spot","on-demand"] }
      taints:
        - { key: nvidia.com/gpu, effect: NoSchedule }
      nodeClassRef: { group: karpenter.k8s.aws, kind: EC2NodeClass, name: gpu }
  limits: { nvidia.com/gpu: 16 }                 # hard cap = cost guardrail
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 2m
VerifyKarpenter API groups/versions and the available GPU families change between releases and regions — confirm against the current Karpenter docs before applying.
Exercise 3 · Design Spot-GPU interruption handling for two workloadsAdvanced

Context: Spot GPUs are the biggest cost lever on EKS, but a 2-minute reclaim warning will take down anything that can't absorb a node loss. The design differs sharply by workload.

Your task: Design how to safely use Spot GPUs for (a) a batch inference job and (b) a user-facing latency-critical endpoint, or explain why Spot is wrong for one of them.

Requirements:

  • State the ~2-minute reclaim reality and what consumes the warning signal
  • For the batch job: how a reclaim is made harmless
  • For the endpoint: the base/burst split (what stays on-demand)
  • Name at least two k8s mechanisms (replicas, PDB, interruption handling) you rely on
  • Be explicit about which workload should NOT be pure-Spot

💡 Hint: Interruptible + retryable ⇒ Spot; single-replica + latency-critical ⇒ on-demand base with Spot only for burst.

Show solution

The reality: AWS can reclaim a Spot instance with ~2 minutes' warning. Karpenter (or a node-termination handler) consumes the interruption/rebalance signal and cordons + drains the node so pods reschedule gracefully instead of being killed cold.

(a) Batch inference → pure Spot is fine. The job is retryable: if a node is reclaimed mid-batch, the work item simply reschedules onto another node. Run it as a Job with retries; a reclaim costs a few minutes, never correctness.

(b) User-facing endpoint → NOT pure Spot. Keep an on-demand base capacity that alone can serve minimum traffic, and add Spot only for burst replicas. A single-replica Spot endpoint is an outage waiting for a reclaim.

Mechanisms: (1) enough replicas spread across instances/AZs so losing one is survivable; (2) a PodDisruptionBudget so draining never drops below the safe replica count; (3) interruption handling (Karpenter-native) to drain on the warning; and for training, frequent checkpointing so a reclaim resumes rather than restarts.

Rule of thumbInterruptible + retryable ⇒ Spot. Single-replica + latency-critical ⇒ on-demand base, Spot for burst only.
Exercise 4 · Compute the EKS-vs-SageMaker break-even for a GPU workload (offline)Expert

Context: "Self-hosting on EKS is cheaper" is only true above a utilisation break-even. Modelling it offline stops the argument being a vibe.

Your task: Write an offline Python function that, given an hourly GPU node price, hours run, utilisation, and a SageMaker/Bedrock alternative cost, reports the effective cost per useful hour and which option wins. Runs offline.

Requirements:

  • EKS cost = node_hourly × hours_run, regardless of utilisation (you pay for idle)
  • Effective cost per useful hour = EKS cost / (hours_run × utilisation)
  • Compare against a provided managed-alternative cost for the same useful work
  • Return both the effective unit cost and the cheaper option
  • Demonstrate a low-utilisation case (managed wins) and a high-utilisation case (EKS wins), offline

💡 Hint: Idle time is the whole story: divide the fixed node bill by only the useful hours to expose what low utilisation really costs.

Show solution

The key move is dividing the fixed node bill by only the useful hours — that is where idle GPUs hurt.

Python · offline, no AWS needed
break_even.pydef eks_vs_managed(node_hourly, hours_run, utilisation, managed_total):
    """utilisation in (0,1]; managed_total = cost of the same useful work on SageMaker/Bedrock."""
    eks_total = node_hourly * hours_run                 # you pay for idle time too
    useful_hours = hours_run * utilisation
    eks_per_useful = eks_total / useful_hours           # true unit cost of useful work
    winner = "EKS" if eks_total < managed_total else "managed"
    return {
        "eks_total": round(eks_total, 2),
        "eks_per_useful_hour": round(eks_per_useful, 2),
        "winner": winner,
    }

# Low utilisation: an idle-heavy EKS GPU node loses to the managed option
print(eks_vs_managed(node_hourly=4.0, hours_run=730, utilisation=0.10, managed_total=1200))
# -> {'eks_total': 2920.0, 'eks_per_useful_hour': 40.0, 'winner': 'managed'}

# High utilisation: a well-packed EKS node wins
print(eks_vs_managed(node_hourly=4.0, hours_run=730, utilisation=0.85, managed_total=3500))
# -> {'eks_total': 2920.0, 'eks_per_useful_hour': 4.71, 'winner': 'EKS'}
Prices are illustrativeThe 4.0/hr and managed totals are placeholders — plug in current AWS pricing. The shape is the lesson: EKS only wins when utilisation is high enough that the fixed node bill beats the managed alternative.
Exercise 5 · Grant a model-serving pod least-privilege S3 access via IRSAProfessional

Context: A serving pod needs to read weights from S3 but should be able to do nothing else. IRSA + a tight IAM policy is the AWS-native least-privilege answer — no static keys.

Your task: Produce the IRSA ServiceAccount annotation and a least-privilege IAM policy that lets a pod read exactly one model prefix in one bucket, and explain why this beats env-var keys.

Requirements:

  • Annotate the ServiceAccount with eks.amazonaws.com/role-arn
  • IAM policy allows only s3:GetObject/s3:ListBucket
  • Scope the resource to one bucket + one prefix, not *
  • Explain why IRSA (temporary, rotated, role-scoped) beats baked-in keys
  • Note the boundary: a leaked pod can read only that one prefix

💡 Hint: Scope the S3 Resource ARNs to the single prefix; the SA annotation is what wires the pod to the role via the cluster OIDC provider.

Show solution

The ServiceAccount annotation ties the pod to an IAM role via the cluster's OIDC provider; the policy makes that role near-powerless:

YAML + JSON · IRSA SA and its least-privilege policy (placeholders)
irsa-sa.yaml# ServiceAccount -> role (IRSA)
apiVersion: v1
kind: ServiceAccount
metadata:
  name: model-loader
  annotations:
    eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/eks-model-reader

# IAM policy attached to that role — read-only, one prefix:
# {
#   "Version": "2012-10-17",
#   "Statement": [{
#     "Effect": "Allow",
#     "Action": ["s3:GetObject", "s3:ListBucket"],
#     "Resource": ["arn:aws:s3:::my-models",
#                  "arn:aws:s3:::my-models/my-llm/*"]
#   }]
# }

Why IRSA beats env-var keys: the pod assumes the role and receives temporary, auto-rotated credentials scoped to exactly this role — there is no long-lived secret to leak, commit, or forget to rotate. A compromised pod inherits only read on my-models/my-llm/*; it cannot touch other buckets, write, or delete. That bounded blast radius is the whole point, and it is the AWS mirror of the K7 RBAC principle.

Verify the association mechanismSome clusters now use EKS Pod Identity instead of the OIDC-based IRSA annotation — the least-privilege policy is identical; only the wiring differs. Check which your cluster uses.
Exercise 6 · Serve a fine-tuned model to prod on EKS under a cost ceilingIndustry scenario

Context: Representative scenario: leadership wants your fine-tuned open model served to production on EKS with a hard monthly GPU budget — and an on-call agent to help operate it. Design it end to end, AWS-specifically.

Your task: Produce a design covering node provisioning, Spot strategy, weights/permissions, cost guardrails against the ceiling, and how an operating agent is bounded on EKS — naming the failure mode each choice defends against.

Requirements:

  • Karpenter GPU NodePool with a GPU cap tied to the budget + consolidation
  • Spot strategy: on-demand base for the endpoint, Spot for burst/batch, with interruption handling
  • Weights from S3 via IRSA (least-privilege), images from ECR, ALB/NLB ingress
  • Cost visibility: tags + CloudWatch/Kubecost, alarm on idle GPU utilisation
  • An operating agent bounded by both RBAC and IRSA, diagnosis-first, mutations gated
  • Name the top failure mode each choice defends against

💡 Hint: Two boundaries, not one: on EKS the agent is capped by its k8s RBAC AND its IAM/IRSA role — scope both, and let Karpenter's GPU limit enforce the budget mechanically.

Show solution

Provisioning & budget. A Karpenter GPU NodePool constrained to GPU families with a hard limits.nvidia.com/gpu derived from the monthly ceiling (GPUs × hourly × 730 ≤ budget), plus aggressive consolidation so idle GPU nodes are reclaimed in minutes. Defends against: a runaway autoscale or idle GPUs blowing the budget — the cap enforces the ceiling mechanically, not by hope.

Spot strategy. An on-demand base of endpoint replicas that alone serves minimum traffic, with Spot burst replicas and any offline/batch inference on pure Spot. Karpenter consumes the interruption signal to drain gracefully; a PodDisruptionBudget protects the safe replica count. Defends against: a Spot reclaim turning into an outage.

Weights & permissions. Weights synced from S3 by an init container using an IRSA ServiceAccount scoped read-only to one prefix; the serving image comes from ECR; the endpoint is exposed via the AWS Load Balancer Controller (ALB for HTTP, NLB for gRPC). Defends against: leaked static keys and per-replica cold re-download / over-broad S3 access.

Cost visibility. Cost-allocation tags per team/model, CloudWatch/Container Insights + Kubecost for per-namespace spend, and an alarm on low GPU utilisation (the silent bill). Defends against: discovering the overspend on the invoice instead of on a dashboard.

Operating agent — two boundaries. Diagnosis-first: read-only kubectl + CloudWatch evidence, the agent explains incidents and proposes fixes (the ch08/K7 pattern). Any mutation is gated by dry-run + human approval / GitOps PR, and the agent is capped by both narrow k8s RBAC and a narrow IRSA IAM role — so it can neither delete cluster resources nor terminate nodes / read other buckets via AWS. Defends against: an injected or hallucinated command escaping the cluster boundary into the AWS account.

The AWS-specific trapOn EKS an "in-cluster" agent with a broad IAM role can act on your whole AWS account regardless of its k8s RBAC. Scope RBAC and IRSA. Verify all GPU/Karpenter/Neuron/pricing specifics against current AWS docs — they change.

✓ Checkpoint — you can move on when you can…

  • State when EKS beats SageMaker and Bedrock — and describe a hybrid (Bedrock + EKS via IRSA).
  • Explain how Karpenter provisions and consolidates GPU nodes, and where the cost cap lives.
  • Say when Inferentia/Trainium (Neuron) beats GPU and the compatibility tax it charges.
  • Choose Spot vs on-demand for a given AI workload and describe interruption handling.
  • Write an IRSA ServiceAccount + least-privilege IAM policy for a pod pulling weights from S3.
  • Name the two boundaries (RBAC + IRSA) that bound an operating agent on EKS.

Knowledge check

check yourself
✓ Knowledge check

A team says "we'll move our Claude calls off Bedrock onto a self-hosted model on EKS to save money." What is the flaw in that reasoning, and when would EKS actually be the right call?

Show answer
The flaw is assuming self-hosting is automatically cheaper. Bedrock is pay-per-token and scales to zero — no idle cost — whereas an EKS GPU node bills per hour whether busy or not, so at spiky or low volume EKS is usually more expensive once you add idle GPU time and cluster ops. EKS becomes the right call when a hard requirement forces it: needing a custom serving stack, sharing one control plane/RBAC/GitOps with existing services, portability/multi-cloud, or genuinely high, sustained utilisation where a well-packed GPU beats the managed unit cost. Control and portability — not a reflexive cost saving — are the real reasons to drop to EKS.
✓ Knowledge check

An LLM-serving pod on EKS needs to read weights from S3, and later an ops agent will run in the same cluster. Why is it not enough to secure these with Kubernetes RBAC alone, and what is the AWS-specific control?

Show answer
Kubernetes RBAC bounds what a pod/agent can do to the cluster, but on EKS a pod also has an AWS identity — and if that identity is over-broad it can read every S3 bucket, call Bedrock, or even terminate nodes / hit the EKS API, entirely outside k8s RBAC. The AWS-specific control is IRSA (IAM Roles for Service Accounts): annotate the pod's ServiceAccount with a role ARN so it assumes a role via the cluster OIDC provider and gets temporary, rotated, least-privilege credentials — here, read-only on one S3 prefix. So a pod/agent on EKS has two boundaries: RBAC (cluster) and IRSA/IAM (AWS). Scope both narrowly; never use static keys.
© 2026 studybydoing.in · AI Engineering: Zero to Production · All rights reserved. · About · Privacy Policy · Terms · Contact
Educational content, provided as-is and without warranty. Code samples are examples — review, test, and adapt them before using in production. See the Terms of Use & Disclaimer. Use at your own risk.
© studybydoing.in