A Spring Boot service has become an image, made it to AWS, and now needs somewhere to run. At this point, the conversation often jumps from “we have a JAR” to “we need a Kubernetes cluster.” The leap may sound architectural; the control plane charge remains quite literal.
The useful comparison for a managed path in 2026 is between ECS Fargate and EKS Auto Mode. Not between ECS and an EKS cluster assembled from randomly chosen parts, nor between ECS Fargate and Fargate-on-EKS. The decision rests on two observable variables:
- does the application require anything at the node level, such as a
DaemonSet, privileged mode,HostNetwork, or a GPU? - is the EKS control plane floor justifiable given the replica-hours and the workload's CPU and memory footprint?
HTTP requests alone do not answer the second question. Fargate charges for resources allocated over time, not applause for each endpoint someone calls.
Prerequisites and scope of the comparison
The examples use:
- Spring Boot 4.1.1;
- Java 25 LTS with Eclipse Temurin 25;
- Linux x86_64;
- the
us-east-1region; - a
spring-api:4.1.1-jre25image already published to Amazon ECR; - AWS CDK 2.267.0 when infrastructure is shown as code;
- Kubernetes 1.36 on EKS Auto Mode.
Spring Boot 4.1.1 requires Java 17 or later, supports Java through version 26, and requires Spring Framework 7.0.9 or later. Java 25 is the LTS choice paired with this example; Java 26 is not LTS. The baseline therefore does not retreat to Boot 3 simply because it still appears in plenty of repositories.
ALB, NAT Gateway, public IPv4, traffic, CloudWatch, and additional storage costs are also outside this calculation. They exist in both designs in different combinations and belong in the project's estimate, but inventing a complete bill from half a table would merely be a meticulous way to be wrong.
First, the floor that appears before the application
The AWS Fargate pricing page lists the following per-second rates in its examples for Linux/x86 in US East (N. Virginia):
- US$ 0.000011244 per vCPU-second;
- US$ 0.000001235 per GB-second.
Billing starts when the image pull begins and ends when the task or pod stops, rounded to the second with a one-minute minimum for Linux. Twenty GB of ephemeral storage is included; additional storage is another billing dimension.
To keep the comparison readable, the conversion below is explicit arithmetic, not an hourly rate printed by AWS:
vCPU-hora = 0.000011244 × 3.600 = US$ 0.0404784
GB-hora = 0.000001235 × 3.600 = US$ 0.004446
1 vCPU por 730 h = 0.0404784 × 730 = US$ 29.55
2 GB por 730 h = 0.004446 × 2 × 730 = US$ 6.49
compute Fargate/mês ≈ US$ 36.04
This task with 1 vCPU and 2 GB running for 730 hours is a simple unit of comparison, not a bill forecast. If two replicas run for the entire month, multiply the compute cost by two. If the service scales or shuts down outside business hours, use the actual replica-hours.
Amazon ECS charges no additional orchestration fee. On the ECS Fargate path, then, there is no line item equivalent to the EKS cluster fee.
Amazon EKS, on the other hand, charges for the control plane:
- US$ 0.10 per cluster-hour during standard support;
- US$ 0.60 per cluster-hour during extended support.
Over 730 hours, that amounts to US$ 73 under standard support or US$ 438 under extended support, before the data plane. In Auto Mode, the data plane still includes the cost of EC2 instances and the Auto Mode fee itself. Public examples of this fee that use Oregon should not be transplanted to us-east-1, so no instance or Auto Mode price will be improvised here.
For a single 1 vCPU, 2 GB service running 24×7, the US$ 73 standard control plane floor already exceeds the roughly US$ 36.04 in Fargate compute calculated above. This does not prove that EKS is always more expensive. It shows that a cluster enters the conversation with a fixed cost that must be spread across workloads or justified by a Kubernetes capability.
| Dimension | ECS Fargate | EKS Auto Mode |
|---|---|---|
| Orchestration/control plane | No additional ECS fee | US$ 0.10/cluster-h standard; US$ 0.60/h extended |
| Data plane | vCPU, memory, and other Fargate dimensions | EC2 and Auto Mode fee, plus the control plane |
| Useful estimation unit | replica-hours × task CPU/memory | cluster-hours + node resources and hours |
| Operational API | ECS task/service | Kubernetes API |
| Per-node resources | No customer-managed node | Available according to node type and configuration |
The same JAR as an ECS Fargate task
A minimal definition for 1 vCPU and 2 GB separates the two IAM identities that are often mixed up:
{
"family": "spring-api",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "1024",
"memory": "2048",
"executionRoleArn": "arn:aws:iam::111122223333:role/ecsTaskExecutionRole",
"taskRoleArn": "arn:aws:iam::111122223333:role/spring-api-task",
"runtimePlatform": {
"cpuArchitecture": "X86_64",
"operatingSystemFamily": "LINUX"
},
"containerDefinitions": [
{
"name": "app",
"image": "111122223333.dkr.ecr.us-east-1.amazonaws.com/spring-api:4.1.1-jre25",
"essential": true,
"portMappings": [
{"containerPort": 8080, "protocol": "tcp"}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/spring-api",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "app"
}
}
}
]
}
The task execution role is used by the ECS/Fargate agent for actions such as pulling the image from ECR and sending logs through the awslogs driver. The application inside the container does not receive those credentials. The task role is the identity used by the Java code to access S3, SQS, DynamoDB, or another permitted service.
The task role's trust policy should accept ecs-tasks.amazonaws.com and restrict the source to reduce the risk of a confused deputy:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"Service": "ecs-tasks.amazonaws.com"},
"Action": "sts:AssumeRole",
"Condition": {
"ArnLike": {
"aws:SourceArn": "arn:aws:ecs:us-east-1:111122223333:*"
},
"StringEquals": {
"aws:SourceAccount": "111122223333"
}
}
}
]
}
If the task cannot pull the image or initialize the log driver, fix the permissions and configuration of the execution role; observe the deployment through ECS service events and the container logs in CloudWatch. If the application gets AccessDenied while accessing a resource, fix the task role policy rather than reflexively broadening the execution policy.
With CDK 2.267.0, the core of the definition makes CPU and memory explicit. The defaults of 256 CPU units and 512 MiB do not represent the task calculated above:
import * as ecs from 'aws-cdk-lib/aws-ecs';
const task = new ecs.FargateTaskDefinition(this, 'SpringTask', {
cpu: 1024,
memoryLimitMiB: 2048,
runtimePlatform: {
cpuArchitecture: ecs.CpuArchitecture.X86_64,
operatingSystemFamily: ecs.OperatingSystemFamily.LINUX,
},
});
task.addContainer('app', {
image: ecs.ContainerImage.fromEcrRepository(repo, '4.1.1-jre25'),
portMappings: [{ containerPort: 8080 }],
logging: ecs.LogDrivers.awsLogs({ streamPrefix: 'spring-api' }),
});
new ecs.FargateService(this, 'SpringService', {
cluster: ecsCluster,
taskDefinition: task,
});
When the underlying capacity fails, there is no customer-owned EC2 instance to repair or access over SSH. The ECS service scheduler maintains the desired count and places a new task. To confirm replacements and placement failures without guessing what happened, check the ECS service events and, if enabled, CloudWatch Container Insights metrics.
The same JAR on EKS Auto Mode
On EKS, the unit becomes a Deployment. The manifest below keeps the same request and limit of 1 CPU and 2 GiB per replica:
apiVersion: v1
kind: ServiceAccount
metadata:
name: spring-api
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: spring-api
spec:
replicas: 2
selector:
matchLabels:
app: spring-api
template:
metadata:
labels:
app: spring-api
spec:
serviceAccountName: spring-api
containers:
- name: app
image: 111122223333.dkr.ecr.us-east-1.amazonaws.com/spring-api:4.1.1-jre25
ports:
- containerPort: 8080
resources:
requests:
cpu: "1"
memory: "2Gi"
limits:
cpu: "1"
memory: "2Gi"
For application access to AWS services, EKS Pod Identity associates an IAM role with the ServiceAccount. The role's trust policy uses the pods.eks.amazonaws.com principal:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowEksAuthToAssumeRoleForPodIdentity",
"Effect": "Allow",
"Principal": {"Service": "pods.eks.amazonaws.com"},
"Action": ["sts:AssumeRole", "sts:TagSession"]
}
]
}
Pod Identity is the simpler path for new code and does not require an annotation on the ServiceAccount. IRSA remains available and uses the cluster's OIDC provider. In either case, the fix for an unauthorized pod is to review the association, the role, and the application policy; kubectl describe pod and an inspection of the ServiceAccount show the identity, events, and observable configuration in the cluster.
In CDK, the aws-eks-v2 module creates Auto Mode by default. The classic aws-eks module still exists; there is no need to invent a deprecation to justify new v2 code.
import * as eks from 'aws-cdk-lib/aws-eks-v2';
const cluster = new eks.Cluster(this, 'Apps', {
version: eks.KubernetesVersion.V1_36,
});
If an Auto Mode node becomes unhealthy, the service's managed monitoring and repair features act on the node, and the scheduler places the pods again according to the workload's constraints. The application, requests, limits, and any PodDisruptionBudget that blocks movement remain the team's responsibility. Use kubectl describe node, kubectl describe pod, and cluster events to observe scheduling failures; enable Container Insights when you need to correlate workload health in CloudWatch.
A Managed Node Group changes that division: the instances sit in the account, the group is backed by Auto Scaling, and automatic repair is optional. EKS manages the control plane, but the team still chooses the update strategy, capacity, and node configuration. Auto Mode reduces this work; it does not eliminate the Kubernetes API or its version lifecycle.
When the cluster is actually worth the cost
The first variable is binary and technical: is there a node-level requirement? A mandatory agent delivered as a DaemonSet, the use of privileged, HostNetwork, HostPort, a GPU, or a node-dependent networking or storage integration points to EKS with Auto Mode nodes or Managed Node Groups. An ordinary sidecar is not enough to reach that conclusion: ECS Fargate supports multiple containers in the same task, and Kubernetes supports sidecars in the pod.
The second variable is economic: what is the relationship between the control plane floor and the workload's compute cost? Build the calculation from data you can measure:
piso EKS padrão = horas do cluster × US$ 0.10
compute Fargate = vCPU-segundos × US$ 0.000011244
+ GB-segundos × US$ 0.000001235
Then compare them using expected replica-hours, not “monthly traffic.” Two services with the same number of requests may reserve very different amounts of CPU and memory. A business metric does not turn into a billing unit through sheer force of will.
EKS tends to make sense when the team already operates a Kubernetes platform with multiple Deployments and namespaces, needs CRDs, operators, GitOps, or shared policies, or requires the node capabilities listed above. The fixed cost can then be spread across workloads, and the Kubernetes API is part of the requirement rather than architectural decoration.
ECS Fargate tends to be the solid choice for an isolated internal JAR with no node requirement, maintained by a team that does not operate Kubernetes. There are fewer upgrade cycles and fewer platform objects between the image and the service.
The rollback threshold becomes objective:
- from EKS to ECS Fargate: step back when there is no node requirement and the control plane floor is equal to or greater than the Fargate compute cost of the workloads that would actually use the cluster;
- from ECS Fargate to EKS Auto Mode or MNG: step back when a node requirement appears that a sidecar cannot solve, or when a multi-tenant platform genuinely depends on the Kubernetes API;
- before accepting extended support: update the EKS minor version; leaving the cluster untouched raises the fee from US$ 0.10/h to US$ 0.60/h.
The option that combines both bills
Fargate-on-EKS exists, but it is a poor choice for a single internal JAR within this scope. The team pays for the EKS control plane, pays for Fargate compute, and still cannot run a DaemonSet in Fargate pods. There is also no support there for privileged mode, HostNetwork, HostPort, or GPUs.
If a pod assigned to the Fargate profile remains Pending because it depends on these capabilities, the fix is to turn the agent into a sidecar when that is technically valid or move the workload to Auto Mode/MNG nodes. kubectl describe pod shows events and scheduling reasons without requiring a group guessing session.
Fargate-on-EKS can work when a Kubernetes platform already exists and a few isolated pods fit the Fargate model. Using it to get “Kubernetes without nodes” for a single service combines the cluster floor with Fargate's limitations. It is the sort of composition that looks wonderfully elegant in the diagram and slightly less so on the bill.
Limitations and verification without a deployment
The examples do not measure performance and make no claim about differences in throughput, p95, or p99. They also do not price EC2 instances, the Auto Mode fee, networking, load balancers, logs, or storage. Revisit the decision using the region, replica count, and actual components of the environment.
Before any deployment, validate the artifacts locally:
jq empty task-definition.json
kubectl apply --dry-run=client --validate=true -f deployment.yaml
npx cdk synth
cdk synth produces CloudFormation locally for inspection. kubectl --dry-run=client validates the manifest's construction without talking to a cluster, although complete validation of the environment's APIs and policies still depends on the real cluster.
Do not use aws ecs register-task-definition as a “dry run”: the command registers a task definition in the account. To review the JSON, compare it with the official ECS task definition parameters and keep this step local.
During the first controlled deployment, watch:
- ECS: service events for task placement and replacement; logs and Container Insights for the application;
- EKS:
kubectl describe podandkubectl describe nodefor scheduling, identity, and disruptions; cluster events and Container Insights when enabled; - IAM: separate execution and task roles in ECS; the Pod Identity association or IRSA checked in EKS.
If the deployment is automated, authenticate GitHub Actions without long-lived keys by using OIDC on AWS. That handles the pipeline's identity; it does not replace the application's runtime roles.
Next step
Take a real service and write down only four values: minimum and maximum replicas, hours in each range, vCPU, and memory per replica. Calculate Fargate compute using the per-second prices and place it next to the EKS floor of US$ 0.10 per cluster-hour. Then mark the node requirement “yes” or “no.”
With no node requirement and the cluster floor dominating the cost, start on ECS Fargate. With a node requirement or a Kubernetes platform already shared by multiple workloads, evaluate EKS Auto Mode. The cluster should enter because it solves an observable need, not because the JAR got a Dockerfile and became ambitious.