AI EngineeringZero to ProductionHome·About·What’s new·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.
📋 Before you startThis page builds on: Pods, Deployments & Services · AWS foundations. Jumping in? Skim those first — or open the recap.
30-second recap
  • Pods, Deployments & Services: A Pod runs containers, a Deployment keeps N identical Pods alive and rolls out updates, a Service gives them one stable address.
  • AWS foundations: IAM controls who can do what, regions decide where things run, and every AWS service is called through an authenticated API.

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

Start with the meeting you've sat in. Someone says "let's just run our own model on Kubernetes — we already have a cluster, and it'll be cheaper than paying Bedrock per token." It sounds obviously right. Six weeks later a GPU node has been sitting at 8% utilisation, billing $4/hour around the clock, and the "cheaper" option has quietly out-spent the managed one it replaced — while also consuming an engineer's week to keep the cluster patched. The decision that felt like a no-brainer was actually the most expensive of the three options, chosen for the wrong reason. This section exists so you never make that call by reflex.

Here's the mental model that prevents it: think of the three services as a staircase of control, not a menu of rivals. Renting a hotel room (Bedrock) means you never see the plumbing — you pay per night and walk away. Renting a serviced apartment (SageMaker) gives you your own kitchen but the building still handles maintenance. Buying the house (EKS) gives you total freedom to knock down walls — and total responsibility for the roof, the boiler, and the property tax. Each step down trades less operational burden for more control, and — this is the part people miss — every step down also adds a bill that never sleeps.

So the rule for choosing is deliberately conservative: stay as high on the staircase as your hardest requirement allows. You only descend when the layer above genuinely cannot do the job — not because the lower layer feels more "real" or more in your control. The mistake to name out loud is descending for cost alone: a managed API that scales to zero is almost always cheaper than a GPU you rent by the hour and forget to turn off. The legitimate reasons to drop to EKS are control (a custom serving stack, sidecars, a specific runtime), consolidation (AI sharing one control plane, RBAC, and GitOps with services you already run on k8s), and portability (plain Kubernetes runs anywhere; the managed layers lock you to AWS). Cost only joins that list at genuinely high, sustained utilisation — and even then you have to prove it, not assume it. Keep the staircase in your head as you read the table below.

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
🗺️ How to read this diagram

Read it left→right: each box hands off more control (and more ops) to you than the one before it.

  • The left box, "Bedrock — managed API", is the least you run: you call a foundation model over an API and AWS owns everything behind it.
  • The arrows between the boxes mark the escalation of control — moving right, you take on more of the platform yourself, from the middle "SageMaker — managed platform" box to the right.
  • The right box, "EKS — self-run k8s", is the payoff of full control: you run the Kubernetes cluster and the serving stack, trading convenience for portability and customisation.

In short: Bedrock → SageMaker → EKS is a slider from "managed for you" to "self-run", with more control and more ops the further right you go.

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

Picture the failure this solves. A traffic spike hits, ten vLLM pods go Pending because they each want a GPU, and… nothing happens. Your cluster has no spare GPU capacity, and the old autoscaler is staring at a fixed "GPU node group" you sized for average load, not peak. Users wait, pods sit unscheduled, and someone scrambles to bump the group by hand. The opposite failure is just as painful: you over-provision that group "to be safe," the spike never comes, and a rack of idle GPUs bills you all night. The whole point of GPU autoscaling is to escape both traps — capacity that appears when pods need it and vanishes when they don't.

The older tool, Cluster Autoscaler, thinks in terms of node groups you defined in advance — an Auto Scaling Group of, say, "g5.xlarge nodes." It can only add or remove copies of shapes you already chose, like a car park where every space is pre-marked "compact only." That's fine until a pod needs a shape you didn't pre-mark, or until you're paying for half-empty groups. Karpenter flips the model: instead of scaling pre-cut groups, it reads the pending pods directly, works out the cheapest EC2 instance type from a set you allow that actually fits what they asked for, and launches exactly that — a valet who parks each car in the perfect-sized bay rather than forcing everyone into "compact." When load falls, it consolidates: it repacks the surviving pods onto fewer nodes and terminates the ones left under-used, so you stop paying for slack.

Why this matters so much for AI specifically: GPU instances are the most expensive things you'll ever schedule, so the cost of a wrong-sized or idle node is brutal, and GPU demand is spiky (a burst of inference, a training job that ends). Karpenter's just-in-time provisioning and aggressive consolidation are therefore not just a convenience — they are the mechanism that keeps GPU spend bounded. You describe the rules with two custom resources: a NodePool (the Kubernetes-facing constraints — which instance families are allowed, Spot vs on-demand, a hard GPU cap, how eagerly to consolidate) and an EC2NodeClass (the AWS-level facts — which AMI, which subnets, which node IAM role). Keep that split in mind reading the YAML: NodePool = what to schedule, EC2NodeClass = how to build the box.

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).

pending pods pod · gpu:1 pod · gpu:1 pod · gpu:1 Karpenter picks cheapest fit g5 GPU node launched just-in-time load falls → consolidate repack + terminate under-used node removed Karpenter scales pods, not pre-cut groups. It reads the pending GPU pods, launches the cheapest instance that actually fits, then consolidates idle nodes away when the burst clears — so you never pay for a GPU node sitting idle.
🗺️ How to read this diagram

Follow the arrows left→right for the scale-up, then the curved arrow back down for the scale-down.

  • The red boxes on the left are unscheduled Pending pods, each asking for a GPU — the cluster has no free capacity, so they sit and wait.
  • The middle Karpenter box reads those pending pods directly and picks the cheapest EC2 instance from your allowed set that actually fits their request — no pre-defined node group needed.
  • The green node on the right is that right-sized GPU instance, launched just-in-time so the pods schedule.
  • The amber curved arrow back to "repack + terminate" is the payoff: when load falls, Karpenter consolidates — repacking surviving pods onto fewer nodes and killing the under-used one, so idle GPU spend stops.

In short: Pending pods in → right-sized GPU node out → idle node consolidated away. Capacity appears when pods need it and vanishes when they don't.

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)

Here's the pitch that gets a finance director excited and an engineer nervous. "We can cut our inference bill by a third by moving off NVIDIA GPUs onto Amazon's own chips." It can be true — and it can also be a two-week detour that ends in "our model doesn't compile." The tension in this section is exactly that gap between a real cost win and a real compatibility tax, and the skill is knowing which one you're walking into before you commit a platform to it.

The analogy that keeps it straight: a GPU (NVIDIA, via CUDA) is a Swiss Army knife — it runs almost any model you throw at it because the whole ML world has spent a decade building for CUDA. Amazon's custom silicon — Inferentia (the inf2 instances, tuned for inference) and Trainium (trn1 and newer, tuned for training) — is more like a specialised power tool: cheaper and faster per unit of work for the jobs it was built for, useless for the ones it wasn't. You program these chips through the Neuron SDK, and the critical difference from a GPU is that you don't just load your model and run — you must compile it with the Neuron compiler first, ahead of time, into a form the chip understands.

That compile step is where the tax lives, and it's the mistake to name loudly: not every model or operator is supported. If your architecture and its ops are on the supported list, compilation is a one-time build step and you're rewarded with better tokens-per-dollar. If it isn't — an exotic layer, a newer architecture the compiler hasn't caught up to — you can hit a wall that no amount of config fixes, and the "savings" evaporate into engineering time. On EKS the scheduling side is reassuringly familiar: just as K7 used a device plugin to advertise nvidia.com/gpu, a Neuron device plugin (a DaemonSet) advertises the chips as aws.amazon.com/neuron, and pods request that resource instead. Same Kubernetes pattern, different resource name. The honest posture: treat Neuron as a genuine lever for high-volume, steady inference on a supported model — and always prototype the compile-and-load-test step on your actual model before you build anything around it. The table and lab below show the trade-off and the scheduling glue.

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.

GPU (g5/p4/p5) · CUDA your model runs directlynvidia.com/gpu Neuron (inf2/trn1) · Neuron SDK your model compileNeuron compiler supported → serves unsupported → wall The compile checkpoint is the Neuron tax. A GPU runs almost any model straight away; Neuron first forces your model through the compiler — a one-time build if it's supported, a dead end if it isn't.
🗺️ How to read this diagram

Read the two lanes top and bottom: the top is the GPU path, the bottom is the Neuron path for the same model.

  • The top lane shows a model flowing straight into a green "runs directly" box — CUDA's decade-old ecosystem means almost anything just runs, scheduled as nvidia.com/gpu.
  • The bottom lane inserts a purple compile step (the Neuron compiler) that every model must pass through before it can run — there is no "just load and go".
  • The branch after compile is the whole risk: a green "supported → serves" path (better tokens-per-dollar) or a red "unsupported → wall" path where an exotic op or newer architecture the compiler hasn't caught up to simply fails.

In short: GPU = run anything now; Neuron = compile first, then win on cost if your model is supported. Prototype that compile step before you commit a platform to it.

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

Two stories sit behind this section, and every EKS-AI team lives one of them. The first: an engineer discovers Spot GPUs, moves the customer-facing chat endpoint onto them to save 70%, and three days later AWS reclaims the single node with a two-minute warning — the endpoint goes dark during business hours and the "saving" becomes an incident review. The second, quieter and more common: nobody looks at utilisation for a month, and the invoice reveals a GPU node that ran at 5% the entire time, burning thousands of dollars doing almost nothing. One story is about using the discount recklessly; the other is about not watching the meter at all. This section is the antidote to both.

Start with the discount itself. Spot capacity is EC2's spare inventory sold at a steep discount off on-demand prices — the catch is that AWS can take it back whenever it needs the hardware, giving you only about a two-minute warning before the instance disappears. Think of it as flying standby: dramatically cheaper, but you might get bumped, so it's perfect when being bumped is merely annoying and catastrophic when it isn't. That single property — reclaimable on short notice — cleanly sorts every AI workload into "fine on Spot" or "not." Batch and offline inference, experimentation, and training that checkpoints are all interruptible: a reclaim just reschedules the work or resumes from the last checkpoint, costing minutes. A single-replica, latency-critical endpoint is the opposite: one reclaim is one outage. The professional move for anything in between is a base-plus-burst split — enough on-demand replicas to serve minimum traffic no matter what, with Spot layered on only for the peaks.

But cheap capacity is only half the discipline; the other half is seeing where the money goes, because the deadliest EKS-AI cost isn't a dramatic mistake — it's the idle GPU nobody noticed. You cannot govern what you cannot see, so the order of operations is: make GPU spend visible first (cost-allocation tags so spend is attributable per team and model, CloudWatch or Container Insights for node utilisation, Kubecost for per-namespace Kubernetes cost), then bound it (the Karpenter GPU cap and consolidation from section 2), and only then argue about which service is cheapest. This is why Karpenter's consolidation is a cost feature, not just a scheduling one — it directly attacks idle time, the single biggest silent bill. Keep that hierarchy — survive interruptions, then watch the meter, then compare — as you read the workload table and the offline cost math below.

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.

~2-min reclaim: can it absorb it? Spot = spare capacity, taken back fast yes, retryable no, latency-critical ✅ pure Spot batch · training w/ checkpoint · dev on-demand base + Spot only for burst PDB + replicas + drain-on-warn Reclaim-tolerance sorts every AI workload. If a 2-minute reclaim just reschedules the work, run pure Spot; if it's an outage, keep an on-demand base and layer Spot on only for the peaks.
🗺️ How to read this diagram

Start at the top box — the one question that decides everything — then follow the two branches down.

  • The top box is the single test: when AWS reclaims the Spot node on ~2 minutes' notice, can the workload absorb it?
  • The left green branch ("yes, retryable") lands on pure Spot — batch/offline inference, checkpointed training, and dev, where a reclaim just reschedules or resumes and costs minutes.
  • The right red branch ("no, latency-critical") lands on an on-demand base that alone serves minimum traffic, with Spot added only for burst.
  • The amber box below is the survival kit that makes even the risky side safe: enough replicas, a PodDisruptionBudget, and draining the node on the 2-minute warning.

In short: One question — "can it survive a 2-minute reclaim?" — routes a workload to pure Spot or to an on-demand base with Spot burst.

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

You already know how to serve a model on Kubernetes — K7 taught the mechanics. So why a whole section on doing it "the AWS way"? Because the moment you're on EKS, three mundane-sounding questions turn into security and cost decisions that sink teams: where do the multi-gigabyte weights come from, where does the container image come from, and how does the pod get permission to fetch them? Get the last one wrong and you end up with an AWS access key hard-coded into a Dockerfile — the single most common way cloud credentials leak, and a finding that fails every security audit. This section is the AWS glue around the generic serving you already understand.

The picture to hold is a supply chain with a security checkpoint. The weights — often tens of gigabytes — live in S3, AWS's object store, because baking them into a container image would make it enormous and slow to pull. The serving image (your vLLM or KServe runtime) lives in ECR, AWS's private container registry. At start-up an init container syncs the weights from S3 onto a local volume, then the real server container boots and reads them. Finally the endpoint is exposed to the world through the AWS Load Balancer Controller — an ALB for ordinary HTTP traffic, or an NLB when you need raw TCP or gRPC. That's the flow the diagram traces: S3 → init container → pod → load balancer.

The checkpoint in that chain — and the reason this section earns its place — is IRSA, IAM Roles for Service Accounts. Here's the analogy: instead of handing the pod a permanent master key it carries in its pocket (a static access key), IRSA is a valet system. You annotate the pod's Kubernetes ServiceAccount with an IAM role's ARN; when the pod starts, it presents its identity to the cluster's OIDC provider and is handed a temporary, automatically-rotated credential scoped to exactly that one role — nothing more. If the pod is compromised, the attacker inherits only what that narrow role allows (say, read-only on one S3 prefix), and the credential expires on its own. This is the exact AWS mirror of the least-privilege principle K7 enforced with RBAC: RBAC bounds what a pod can do to the cluster; IRSA bounds what it can do to AWS. The same mechanism is what lets an EKS pod call Bedrock in the hybrid pattern from section 1 without ever storing a key. The labs below wire it up: a ServiceAccount, a pod that pulls from S3, and the tight IAM policy that makes the role nearly powerless.

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
🗺️ How to read this diagram

Follow the single arrow chain left→right — it traces one model's journey from storage to a public endpoint.

  • The left box, "S3 — weights", is where the model files live at rest, outside the pod.
  • The middle two boxes are the pod's startup path: the "initContainer — pull via IRSA" box fetches the weights using its IRSA role, then hands them to the "Pod (vLLM) — serves model" box that actually runs inference.
  • The right box, "ALB/NLB — ingress", is the payoff: the load balancer exposes the running vLLM pod so callers can reach it.

In short: weights in S3 → init container pulls them via IRSA → vLLM pod serves the model → ALB/NLB exposes it.

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

Imagine the incident you'd never live down. You give an operating agent read-only access to diagnose cluster problems, feeling safe because its Kubernetes RBAC is tightly scoped — it can't delete a Deployment, can't touch a secret. Then, during an incident, a poisoned log line or a hallucinated command nudges it, and it terminates three EC2 nodes and starts listing every S3 bucket in the account. Nothing it did violated its RBAC. How? Because on EKS the agent had a second identity you forgot to lock down. That blind spot is the entire subject of this section.

The mental shift is this: on a plain Kubernetes cluster, an agent has one boundary — its RBAC, which governs what it can do to cluster objects. On EKS, the same pod also carries an AWS identity via IRSA (the mechanism from section 5), and that identity governs what it can do to the AWS account — EC2, S3, Bedrock, even the EKS control-plane API itself. So an "in-cluster" agent has two locks on two different doors: RBAC on the cluster door, and its IAM role on the AWS door. The dangerous, invisible mistake is scoping the first door carefully and leaving the second wide open — because RBAC has nothing to say about whether the pod can terminate a node or read a bucket. Those live entirely on the AWS side, outside Kubernetes' view.

The AWS-specific surface an operating agent touches is worth naming so you can bound each part: alongside the Kubernetes API there's the EKS control-plane API (node groups, add-ons), CloudWatch and Container Insights as the evidence source it reads to diagnose, and — the sharp edge — that IAM boundary. The safety recipe is the ch08 / K7 pattern with one extra AWS lock bolted on: keep the agent diagnosis-first (read cluster state and CloudWatch, explain the incident, propose a fix rather than apply it), gate every mutation behind dry-run plus human approval or a GitOps PR, and scope both boundaries narrowly — a tight RBAC role and a tight IRSA policy. The rule to carry away: on EKS, "least privilege" is never one setting. It's two, and the AWS one is the boundary teams forget until the blast radius escapes the cluster.

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.

operating agent one pod, two identities cluster door AWS door 🔒 Kubernetes RBAC what it can do to the cluster 🔒 IRSA / IAM role EC2 · S3 · Bedrock · EKS API forget this lock → blast radius escapes On EKS, "least privilege" is two locks, not one. RBAC bounds the agent inside the cluster; its IRSA/IAM role bounds what it can do to the AWS account. Scoping only the first leaves the AWS door wide open.
🗺️ How to read this diagram

Start at the single agent at the top, then follow the two arrows down to the two independent locks it must pass.

  • The top box is the one operating agent — but note the subtitle: it carries two identities at once.
  • The left green lock is Kubernetes RBAC, the "cluster door": it governs what the agent can do to cluster objects (Deployments, secrets).
  • The right green lock is the IRSA/IAM role, the "AWS door": it governs what the same pod can do to the AWS account — terminate EC2 nodes, read S3, call Bedrock, even hit the EKS control-plane API.
  • The red line under the AWS door is the trap: RBAC says nothing about the AWS side, so a tightly-scoped cluster role with an over-broad IAM role lets an "in-cluster" agent escape into the whole account.

In short: Two doors, two locks — scope RBAC and IRSA. Locking only the cluster door leaves the AWS account exposed.

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