← blog
AWSElastic BeanstalkEKSKubernetes

Elastic Beanstalk Cluster Mode: Beanstalk is now EKS underneath

Elastic Beanstalk turned 15 this year, and AWS just rebuilt what runs under it. Cluster Mode drops your applications onto Amazon EKS instead of EC2 instances you own. I deployed into it, watched every stage, and wrote down what actually happened.

01What actually changed

Upload a zip of Python source. No Dockerfile. Get a running container on EKS. That is Elastic Beanstalk Cluster Mode, launched this week — Beanstalk apps now run on EKS instead of EC2 instances you own. Cloud Native Buildpacks handle the containerization: I uploaded four files (app.py, requirements.txt, Procfile, runtime.txt), Beanstalk detected Python, built the image, pushed it to ECR, and ran it.

In Standard Mode, Beanstalk provisions EC2 instances in your account, installs a platform on them, and manages the Auto Scaling group. You can SSH in. You can see the instances. That is still there and still fully supported.

Cluster Mode is different. You pick Cluster as the deployment type, hand over source code, a Dockerfile, or a container image, and Beanstalk creates and operates an EKS cluster underneath. The first deployment for a given set of subnets triggers cluster creation. Subsequent deployments reuse it.

The console makes the tradeoff unusually explicit — Standard is labelled free-tier eligible with full instance-level access; Cluster is labelled best for multiple apps that share infrastructure, powered by EKS.

The choice that defines everything downstream. Note what Standard keeps: free-tier eligibility and instance-level access.
The choice that defines everything downstream. Note what Standard keeps: free-tier eligibility and instance-level access.

02Setting it up

Environment details are unchanged from Standard — application name, environment name, domain prefix.

Environment details. Identical to Standard Mode.
Environment details. Identical to Standard Mode.

The application bundle is four files, zipped flat. No Dockerfile — that is deliberate, so Cloud Native Buildpacks has to do the containerization.

Four files at the zip root: app.py, Procfile, requirements.txt, runtime.txt. No wrapping folder.
Four files at the zip root: app.py, Procfile, requirements.txt, runtime.txt. No wrapping folder.
# app.py (excerpt)
@app.route("/healthz")
def healthz():
    return "ok", 200

@app.route("/burn")
def burn():
    blob = bytearray(400 * 1024 * 1024)   # ~400MB
    return f"allocated {len(blob)} bytes on {socket.gethostname()}"

/healthz gives the load balancer something real to hit. /burn allocates 400MB on demand, which is how we test memory limits later.

Local file upload with Buildpack selected as the image build type. Builder image left blank.
Local file upload with Buildpack selected as the image build type. Builder image left blank.

03The Builder image field is the one to understand

A Cloud Native Buildpacks builder is a container image bundling two things: a set of buildpacks (language detectors and compilers) and a base run image. Beanstalk hands it your source, it detects the language from files like requirements.txt, installs the runtime, and emits a runnable OCI image. That mechanism is the whole "no Dockerfile required" promise.

Leave it blank and you get the default. But read the supported builders carefully, because the language coverage is not uniform:

The three Paketo builders Beanstalk documents — and their architecture coverage.
The three Paketo builders Beanstalk documents — and their architecture coverage.
BuilderLanguagesArchitecture
paketobuildpacks/builder-jammy-baseGo, Node.js, Java, Python, Ruby, .NET, NGINX, Apache HTTP Serveramd64 only
paketobuildpacks/builder-jammy-fullSame, plus PHP and common C librariesamd64 only
paketobuildpacks/ubuntu-noble-builderJava, Node.js, .NET, NGINX, Apache HTTP Serveramd64 and arm64

The non-obvious trap: the only arm64-capable builder does not list Python, Ruby, Go or PHP. So if you pick Graviton nodes and deploy Python source code, buildpacks has nothing to detect with. Graviton is still available to you — but via a Dockerfile or a prebuilt arm64 container image, not the source-code path. "Bring your source, we handle the rest" quietly means amd64 for half the supported languages.

In my case the field would not stay blank. Leaving the default gave me nothing that would create; I filled in paketobuildpacks/builder-jammy-base and only then did the environment create. That value is consistent with everything else I observed: jammy-base covers Python, and it is amd64-only.

That is also why the architecture choice sits further down the same page, well away from the builder field that constrains it.

amd64 selected. Instance category left automatic, node pool left empty to use the cluster's shared nodes.
amd64 selected. Instance category left automatic, node pool left empty to use the cluster's shared nodes.

04Container attributes are Kubernetes requests and limits

Two things matter here. The plain CPU and Memory fields are reservations — what the scheduler sets aside, and what your bin-packing and bill are based on. The limit fields are ceilings. CPU over its limit is throttled: slow, alive. Memory over its limit is terminated. Not throttled. Killed.

The second thing: autoscaling targets are percentages of the request, not the limit. An 80% CPU target against a 250m request means replicas are added at 200m of real usage.

250m CPU / 512Mi memory requested, 1Gi memory limit, 1 to 3 replicas, 80% targets on both CPU and memory.
250m CPU / 512Mi memory requested, 1Gi memory limit, 1 to 3 replicas, 80% targets on both CPU and memory.

Note the info text on Memory limit: if left empty, it defaults to the Memory value. So a blank limit is not "no limit" — it is a limit equal to your request.

05Networking

Cluster subnets carry a warning worth reading twice: environments with the same subnets share an EKS cluster, and you cannot change subnets after creating the environment. That single field decides both your blast radius and whether the next environment gets a ten-minute cluster build or a fast deploy.

Default VPC public subnets, a dedicated internet-facing ALB.
Default VPC public subnets, a dedicated internet-facing ALB.
Health check pointed at /healthz, 15s interval, 2 healthy / 2 unhealthy thresholds.
Health check pointed at /healthz, 15s interval, 2 healthy / 2 unhealthy thresholds.

06Observability is on by default

Logs and metrics ship to CloudWatch without configuration. Traces are opt-in. Auto-instrumentation injects OpenTelemetry for supported languages without touching your code — I set it to Python to see what arrives that I never wrote.

Log and metric collection to CloudWatch enabled by default; traces off.
Log and metric collection to CloudWatch enabled by default; traces off.
Auto-instrumentation set to Python. Rolling deployment with max surge 1, max unavailable 0.
Auto-instrumentation set to Python. Rolling deployment with max surge 1, max unavailable 0.

Max surge 1 with max unavailable 0 means a new replica comes up before the old one goes away — zero downtime, but it briefly needs capacity for two replicas even when your minimum is one.

07Three IAM roles, created for you

The console creates all three. Worth knowing what each is for, because in CLI or IaC you supply these ARNs yourself.

Cluster role for the EKS control plane, node role for workers, observability role for shipping logs, metrics and traces.
Cluster role for the EKS control plane, node role for workers, observability role for shipping logs, metrics and traces.

08What actually happens after you hit Create

This is the part the announcement glosses over. The event stream is a genuinely good teaching artifact.

17:22:55 — createEnvironment is starting. Nineteen seconds later it has resolved the default VPC's four public subnets.
17:22:55 — createEnvironment is starting. Nineteen seconds later it has resolved the default VPC's four public subnets.

Then, in sequence:

Forty-eight seconds from submit to a running CodeBuild job.
Forty-eight seconds from submit to a running CodeBuild job.

That CodeBuild project is the first real surprise. The "managed" build is a CodeBuild project created in your account, with a buildspec you can read.

Build succeeded. 17:23 to 17:25 — roughly two minutes for a Flask app.
Build succeeded. 17:23 to 17:25 — roughly two minutes for a Flask app.
The generated buildspec: ECR login, then docker buildx with a registry-backed cache layer.
The generated buildspec: ECR login, then docker buildx with a registry-backed cache layer.

Two details worth pulling out of that buildspec. It logs into both your private ECR and ECR Public, and it uses docker buildx with --cache-from and --cache-to pointed at a docker-cache tag in your own repository. Your build cache is a tag in your ECR repo. That is a line item, and it is also why second and subsequent builds get faster.

The output lands in a Beanstalk-named private ECR repository:

149.82 MB image, tagged v1, pushed at 17:25:03 — two minutes eight seconds after the build started.
149.82 MB image, tagged v1, pushed at 17:25:03 — two minutes eight seconds after the build started.

A Flask app with two dependencies produces a 150MB image. That is the buildpack base image, not your code. Reasonable, but worth knowing before you reason about pull times or ECR storage across a hundred applications.

09Then you wait for CloudFormation

With the image pushed at 17:25:03, the environment still is not up. Beanstalk is now waiting on a CloudFormation stack named beanstalk-cluster-<uuid>.

Polling the same stack at 17:25:33 and again at 17:27:34.
Polling the same stack at 17:25:33 and again at 17:27:34.

That stack is the EKS cluster. Eight minutes in, it is still in Creating.

Kubernetes 1.36, standard support until August 2027, still Creating. The EKS console cannot even show Kubernetes resources yet.
Kubernetes 1.36, standard support until August 2027, still Creating. The EKS console cannot even show Kubernetes resources yet.

The polling runs every two minutes, and it runs for a while. The cluster assignment finally completes at 17:36:17 — the CloudFormation wait alone accounted for just under eleven minutes. Platform components come up seven seconds later, and application resources are submitted to the cluster at 17:36:32.

Eleven minutes of polling, then three events in fifteen seconds once the cluster exists.
Eleven minutes of polling, then three events in fifteen seconds once the cluster exists.

So the honest end-to-end number for a first environment: 13 minutes 37 seconds from clicking Create to application resources being submitted. Of that, roughly 2 minutes was building a container image and about 11 minutes was waiting for an EKS cluster to exist. Your application is not the slow part. The cluster is.

The cluster name is generated, the Kubernetes version is chosen for you, and the support window is visible in the EKS console — which means someone, eventually, has to care about that upgrade. The whole point of Cluster Mode is that it should be AWS. Worth verifying against a real cluster in a year rather than taking it on faith.

10It launched at 16m 11s

Deployment passed health checks at 17:38:47, environment launched at 17:39:07. Reported duration: 16 minutes 11 seconds.
Deployment passed health checks at 17:38:47, environment launched at 17:39:07. Reported duration: 16 minutes 11 seconds.

Application resources hit the cluster at 17:36:32, health checks passed just over two minutes later, and Beanstalk called the environment launched at 17:39:07. The console reports the whole run as 16m 11s.

And the app is there:

The hostname is a Kubernetes pod name. Beanstalk is not hiding that from you.
The hostname is a Kubernetes pod name. Beanstalk is not hiding that from you.

That hostname — deployment-demo-new-eks-env-555964b898-nn4mw — is the tell. Deployment name, ReplicaSet hash, pod suffix. The abstraction is real but it is thin, and anything in your app that logs a hostname is now logging Kubernetes pod identity. Useful for correlation, mildly surprising if you were promised you would never think about Kubernetes.

/healthz returning ok. The health check has something real to hit.
/healthz returning ok. The health check has something real to hit.
/info. Note the empty env object — no EB_ variables injected, unlike Standard Mode's platform environment.
/info. Note the empty env object — no EB_ variables injected, unlike Standard Mode's platform environment.

And the control case for the memory test: with a 1Gi limit against a 512Mi request, a 400MB allocation succeeds.

419,430,400 bytes allocated, replica survives. This is the baseline before dropping the limit to 256Mi.
419,430,400 bytes allocated, replica survives. This is the baseline before dropping the limit to 256Mi.

11What the metrics actually show

Environment health 10, CPU 0.0808 cores, memory 76.1MB working set. Note the metric names: container_cpu_usage_seconds_total, container_memory_working_set_bytes.
Environment health 10, CPU 0.0808 cores, memory 76.1MB working set. Note the metric names: container_cpu_usage_seconds_total, container_memory_working_set_bytes.

Those metric names are Kubernetes cAdvisor metrics, surfaced directly in the Beanstalk console. Again: thin abstraction, and in this case a helpful one.

The numbers are the more interesting part. The app uses 76MB against a 512Mi request and 0.08 cores against a 250m request. That is roughly 15% memory and 32% CPU utilisation of what is reserved — and reservation is what drives bin-packing and therefore cost. On a single environment this is invisible. Across a portfolio it is the entire economics of Cluster Mode: every over-provisioned request is capacity the scheduler cannot give to anything else. Right-sizing requests matters more here than it ever did on Standard.

There is also an "Investigate with AI" entry point on the metrics panel, which is a different thing from the log AI analysis below.

12Logs, and the limit of the AI troubleshooting

Logs are pull-based: you click Request logs, Beanstalk retrieves the last 100 lines or the full set, and the retrieved file expires after 15 minutes.

Gunicorn 22.0.0 starting, listening on 0.0.0.0:8080, sync workers booting. The Procfile worked exactly as written.
Gunicorn 22.0.0 starting, listening on 0.0.0.0:8080, sync workers booting. The Procfile worked exactly as written.

Worth noticing at the top of that log: an ImportError for libsqlite3.so.0. The buildpack run image does not carry it. Nothing in this app needs sqlite, so it is harmless here — but it is a concrete illustration of why the builder choice matters. The jammy-full builder exists precisely for dependencies that need common C libraries. On jammy-base, you find out at runtime.

Now the finding I most wanted from this exercise. The AI Analysis button is greyed out, with a tooltip:

“AI analysis is only available when the environment is unhealthy.”
“AI analysis is only available when the environment is unhealthy.”

That is a reasonable product decision and a real constraint worth stating plainly. The AI troubleshooting is a reactive tool, not an inspection tool. You cannot ask it to explain a healthy-but-odd environment, sanity-check a config, or investigate slow-but-alive behaviour. It waits for the health check to fail. In Cluster Mode, where you do not have kubectl and the logs are pull-based and expire in fifteen minutes, that narrows your options for the whole class of problems that never trip a health check.

13Teardown

Type-to-confirm termination. The environment URL is released and associated resources go with it.
Type-to-confirm termination. The environment URL is released and associated resources go with it.

Terminating asks you to type the environment name, which is the right amount of friction. Read the event it emits, though: "Starting to delete application resources." Application resources.

Terminating the environment does not delete the EKS cluster. I terminated my only environment, and the cluster was still there. I had to go to the EKS console and delete it by hand — typing the cluster name to confirm.

Deleting the cluster manually. Note the second warning: associated scrapers are not removed with it.
Deleting the cluster manually. Note the second warning: associated scrapers are not removed with it.

This follows logically from the design — the cluster is shared across every environment on those subnets, created by its own CloudFormation stack, and described by the service as a one-time operation. It should not vanish when one tenant leaves. But the consequence is that the documented teardown path leaves a billing EKS control plane behind. Anyone who terminates their environments, sees the Beanstalk console go empty, and assumes they are done will keep paying the control plane fee for a cluster running nothing.

And read the second warning in that dialog, because it survives both teardowns: associated scrapers are not deleted with the cluster. If you enabled metrics collection — which is on by default — there is an Amazon Managed Prometheus scraper billing separately. Click View scrapers before you close that tab.

So the real teardown checklist, in order:

  1. Terminate every Beanstalk environment
  2. Delete the associated scrapers (Amazon Managed Prometheus)
  3. Delete the EKS cluster manually
  4. Delete the orphaned beanstalk-cluster-* CloudFormation stack
  5. Check EC2 for a surviving load balancer
  6. Delete the ECR repository — it holds both your image and the docker-cache tag the buildspec writes, so two billed artifacts per application
  7. Delete the /aws/elasticbeanstalk/... CloudWatch log groups

Deleting the application itself is a separate type-to-confirm step after the environment is gone:

Environment terminated, application still listed. Deleting it is its own confirmation.
Environment terminated, application still listed. Deleting it is its own confirmation.
Applications (0). Note that Restore terminated environment is greyed out.
Applications (0). Note that Restore terminated environment is greyed out.

Seven steps, one of which the service does for you. For a product whose entire proposition is "AWS takes operational responsibility," the exit is remarkably manual — and it is the one part of the lifecycle where getting it wrong costs money quietly, forever.

14The complete event log

Every event from create to terminate, in order. This is the most useful artifact of the whole exercise — it is the service narrating its own internals.

17:22:55  createEnvironment is starting.
17:23:14  Using default VPC public subnets. subnets='[subnet-0b503c7e5bc6a5580,
          subnet-0335d39e8bd9ff495, subnet-0846724ffcecf449d, subnet-07d980776fa7582a4]'
17:23:25  Starting cluster assignment for environment: Demo-New-Eks-env
17:23:37  Image build started for version v1
17:23:43  Started image build CodeBuild execution: arn:aws:codebuild:us-west-2:...
17:24:02  Creating CloudFormation stack for cluster infrastructure named:
          beanstalk-cluster-182eeccb-... This is a one-time operation and
          generally takes about 10 minutes.
17:24:10  Environment health has transitioned to No Data.
17:25:17  Image build completed for version v1 with image URI
          ...elasticbeanstalk-demo-new-eks-7a910c:v1@sha256:67aea994...
17:25:33  Waiting for CloudFormation stack 'beanstalk-cluster-182eeccb-...'
17:27:34  Waiting for CloudFormation stack 'beanstalk-cluster-182eeccb-...'
17:29:35  Waiting for CloudFormation stack 'beanstalk-cluster-182eeccb-...'
17:31:36  Waiting for CloudFormation stack 'beanstalk-cluster-182eeccb-...'
17:33:37  Waiting for CloudFormation stack 'beanstalk-cluster-182eeccb-...'
17:36:17  Successfully completed cluster assignment for cluster:
          arn:aws:eks:us-west-2:...:cluster/beanstalk-cluster-182eeccb-...
17:36:24  Waiting for platform components to become ready
17:36:32  Application resources submitted to EKS cluster. Waiting for them to become ready.
17:36:47  Creating load balancer named: 2ba46c89eadf3ae4-demo-new-eks-en.
          This may take a few minutes.
17:38:32  Application replica ready: deployment-demo-new-eks-env-555964b898-nn4mw
17:38:32  Deployment ready: deployment-demo-new-eks-env
17:38:47  Load balancer ready: 2ba46c89eadf3ae4-demo-new-eks-en
17:38:47  Deployment passed health checks
17:38:56  Application available at Demo-New-Eks-env.eba-dpcr9y8z.us-west-2.elasticbeanstalk.com
17:39:07  Successfully launched environment: Demo-New-Eks-env
17:40:10  Environment health has transitioned from No Data to Ok.
          Insufficient request rate to determine application health.
17:43:25  requestEnvironmentInfo is starting.
17:43:26  Starting diagnostic log collection.
17:43:33  Diagnostic logs collected successfully.
17:45:18  Starting to terminate environment: Demo-New-Eks-env
17:45:30  Starting to delete application resources

Four things in there are worth pulling out.

AWS tells you the cost up front. At 17:24:02: creating the CloudFormation stack for cluster infrastructure is "a one-time operation and generally takes about 10 minutes." It took just over twelve. The service is explicit that this is paid once — which is the whole shared-infrastructure argument, stated in an INFO event rather than a marketing page.

The build and the cluster run in parallel. The image build completed at 17:25:17, while the CloudFormation stack was still being created. Beanstalk is not serialising these. So a slower build — a Java service with a real dependency tree — would likely cost you nothing extra on a first deploy, because it is hiding inside the cluster wait. On a second deploy, with no cluster to build, your build time becomes the entire deploy time.

Health reporting has its own lag. The environment launched at 17:39:07 but health only moved from No Data to Ok at 17:40:10 — a further minute — with the note "Insufficient request rate to determine application health." A freshly launched environment with no traffic cannot be assessed. Combined with AI Analysis only unlocking when unhealthy, the first minutes of a new environment are the least observable.

Log collection is an explicit, audited operation. Requesting logs shows up as three events: requestEnvironmentInfo, collection starting, collection succeeded. Logs are not streaming to you by default — you ask, the service goes and gets them, and the retrieved file expires in fifteen minutes.

15The shape of the timeline

StageElapsed
Submit → subnets resolved19s
→ CodeBuild running48s
→ image pushed to ECR2m 08s
→ EKS cluster assigned13m 22s
→ app resources submitted13m 37s
→ health checks passed15m 52s
→ environment launched16m 11s
→ health reporting Ok17m 15s

The build is not the bottleneck. The cluster is — about eleven of those sixteen minutes. And that cost is paid once per set of subnets, which is exactly why Cluster Mode's economics depend on you running more than one application.

Deploy a second environment onto the same subnets and you should skip the entire CloudFormation wait. That delta is the whole business case, and it is the number worth measuring yourself.

16Cost, honestly

There is no charge for Cluster Mode itself. The underlying resources are another story: the EKS control plane fee, EKS Auto Mode compute at roughly a 12% premium over EC2 instance cost, ECR (including that build cache), CodeBuild minutes, and CloudWatch. It is not Free Tier eligible.

AWS is unusually direct about where the line sits. Workloads under $500/month are called out as a poor fit — the control plane fee and Auto Mode premium add overhead a single application cannot offset through bin-packing. Also staying on Standard: single-environment use cases, Windows and .NET Framework on IIS, and anything that cannot be containerized.

17What Cluster Mode cannot do yet

Going through the Actions menu on a Cluster environment turns up a consistent tooltip: Not supported for EKS environments. Several familiar Beanstalk operations are simply absent.

Restore environment — not supported for EKS environments.
Restore environment — not supported for EKS environments.
Rebuild environment — not supported. Restart app server(s) is greyed out too.
Rebuild environment — not supported. Restart app server(s) is greyed out too.
Clone environment — not supported. Swap environment domain is greyed as well.
Clone environment — not supported. Swap environment domain is greyed as well.

Collectively that is: no restore of a terminated environment, no rebuild, no clone, no restart of app servers, no domain swap. Some of these are conceptually odd under Kubernetes — "restart app servers" does not map cleanly onto a Deployment you do not control, and rebuild means something different when the infrastructure is shared. Others are real gaps. Clone is how a lot of teams stand up a staging copy of production. Swap environment domain is how a lot of teams do blue/green.

Traffic-splitting deployments with automatic rollback are listed as a Cluster Mode capability, so the blue/green story exists — it just is not the domain-swap mechanic Beanstalk users have muscle memory for. If your runbooks contain the words "clone the environment" or "swap the CNAME," they need rewriting before you migrate, and that is a bigger task than changing a radio button.

18What I would actually do

Set a budget alert before you create anything, and write the teardown checklist down before you need it. The cluster outliving the environment is the failure mode that will actually cost someone money.

Right-size your requests before anything else. 76MB of real usage against a 512Mi reservation is the kind of gap that costs nothing on one environment and costs real money across forty.

Do not migrate. Deploy something new into Cluster Mode — a low-stakes internal service — and watch the bill for a month with only one application on the cluster. That is your worst case. Then add a second and see whether the sharing math holds.

And check the builder matrix before you promise anyone Graviton — and know that the AI troubleshooting will not talk to you until something is already broken.

Beanstalk was always the service you picked when you did not want to think about the compute. Cluster Mode keeps that promise while quietly making EKS the default answer for teams with a portfolio. Whether that is simplification or abstraction debt depends entirely on how many applications you are running.


Reference: AWS News Blog — AWS Elastic Beanstalk introduces Cluster Mode · Cluster Mode documentation