
Infrastructure
ECS vs Kubernetes: Paying for Portability Up Front
ECS charges no orchestration fee but needs AWS's control plane. What has no Kubernetes equivalent, what a migration involves, and how to price the exit.
Most comparisons of ECS and Kubernetes list features and name a winner, which misses the decision. ECS is simpler because it is AWS's: identity, networking, discovery, secrets and deployment safety are all AWS services wired together for you. That simplicity is real, and the bill for it comes in a currency nobody prices on day one, which is the cost of leaving. This article maps each ECS primitive to its Kubernetes counterpart and grades how portable it is. It shows why ECS Anywhere does not change the picture, compares both upgrade treadmills, gives a read-only script that counts your ECS-bound constructs, and lays out the migration in the order the dependencies force. Prices, quotas and support dates are as of 29 October 2026.
Different layers, not rivals
ECS is a proprietary scheduler API. Its objects (clusters, services, task definitions, tasks) exist only inside AWS, and the only implementation of that API is the one AWS runs. Kubernetes is a specification with many implementations. The CNCF's Certified Kubernetes programme tests distributions against the required APIs, and the CNCF states the purpose directly: conformance "enables interoperability from one Kubernetes installation to the next. It allows them the flexibility to choose between vendors."
So the honest framing is not ECS against EKS. EKS is one vendor's hosting of the portable API; you could equally run that API on your own hardware, or with another provider. AWS Fargate blurs this further, because Fargate is a compute option under both: ECS tasks can run on Fargate, and so can EKS pods. A workload being on Fargate therefore says nothing about its portability. The question is which API your manifests, pipelines and runbooks are written against.
- ECS: the API, the scheduler and the control plane are AWS's. Compute can be EC2, Fargate, Managed Instances or your own servers through ECS Anywhere.
- EKS: the control plane is AWS's, but the API is conformance-tested Kubernetes. Manifests written against it carry over to any other conformant cluster, minus whatever AWS-specific bindings you added.
- Self-run Kubernetes: you own the control plane and its on-call. The API is the same one EKS serves.
What ECS's simplicity actually buys you
Start with the price. AWS's ECS pricing page says that "there is no additional charge for Amazon ECS orchestration": on EC2 and Fargate there is no ECS management fee, and you pay only for the underlying compute. Two variants do carry a fee. ECS Managed Instances adds a per-instance management fee, and ECS Anywhere charges $0.01025 per registered on-premises instance per hour, billed per hour with a one-hour minimum.
Then the integration. A task IAM role lets code in the container call other AWS services, with the role's permissions "vended to containers running in the task", so there is no credential to manage. In awsvpc mode each task receives its own elastic network interface, which means security groups attach to tasks rather than hosts, and Fargate tasks must use awsvpc. Deployment safety is built in. The deployment circuit breaker can roll a failed rolling deployment back automatically, and native blue/green deployments let you validate a new revision with test traffic before production traffic reaches it, with lifecycle hooks and a post-cutover bake time during which the old revision stays up. ECS Express Mode, announced on 21 November 2025, goes further still: it launches web apps and APIs with an AWS-provided domain name, at no charge beyond the resources it creates.
The Kubernetes side of the ledger has a fee and a clock. EKS's version-support fee is $0.10 per cluster per hour in standard support and $0.60 in extended support; Provisioned Control Plane is a separate opt-in tier whose hourly charge is added on top of that version-support fee, not charged instead of it. Each minor version gets 14 months of standard support and then 12 months of extended support, and extended support is enabled by default. Extended support raises that fee sixfold; compute is unchanged. Once a version completes its 26-month lifecycle, EKS auto-upgrades the control plane. That is the default EXTENDED path: a cluster whose upgrade policy is set to STANDARD skips extended support and is auto-upgraded at the end of the 14-month standard period instead, not after 26 months, so turning off the higher fee also shortens the clock. In either case, EKS Auto Mode nodes may update with it, but managed node groups and self-managed nodes stay behind, so neglect ends in skew. Upstream is no gentler: the Kubernetes project maintains only the three most recent minor releases, each with roughly a year of patch support.
The concepts with no portable equivalent
Every ECS migration guide includes a mapping table. What matters is the grade on each row: does the concept translate mechanically, need redesigning, or have no portable equivalent, so that whatever replaces it is a new decision? The concept map below grades each row. The dashed edges are the ones a migration plan tends to underestimate.
The rows that hurt are identity, networking and discovery. A task role becomes a Kubernetes ServiceAccount, but on EKS that ServiceAccount still has to be bound to IAM. EKS Pod Identity does this by associating an IAM role with a service account, but it supports only pods on Linux EC2 nodes, not Fargate or Windows, and it is EKS-only: AWS says its components are available only on Amazon EKS, not on EKS Anywhere or on clusters you run yourself on EC2. IRSA is the alternative; either way, EKS swaps one AWS-bound binding for another. The portable path is a workload identity you own, such as SPIFFE/SPIRE, federated to AWS only where AWS APIs are actually called.
Networking is a redesign because the security boundary moves. An awsvpc task carries its own ENI and security groups. A Kubernetes pod is protected by NetworkPolicy, which is only an API object until a CNI enforces it. On EKS with the Amazon VPC CNI, network policies apply to EC2 Linux nodes only, not to Fargate or Windows nodes. A team that moves ECS-on-Fargate services to EKS-on-Fargate and translates each security group into a NetworkPolicy would end up with policies that nothing enforces.
Service Connect is ECS configuration that "builds both service discovery and a service mesh", grouping tasks by AWS Cloud Map namespace. Kubernetes gives you Service DNS for free, and a mesh only if you choose one. Secrets referenced with valueFrom are injected from Secrets Manager or Parameter Store as environment variables, and a changed secret is picked up only after a new deployment or task. The portable replacement is a Kubernetes Secret synced by the External Secrets Operator, which can read the same Secrets Manager entries on AWS today and a different backend tomorrow. Here is the artefact you will be translating:
{
"family": "orders-api",
"requiresCompatibilities": ["FARGATE"],
"networkMode": "awsvpc",
"cpu": "512",
"memory": "1024",
"taskRoleArn": "arn:aws:iam::123456789012:role/orders-api-task",
"executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
"containerDefinitions": [
{
"name": "orders-api",
"image": "123456789012.dkr.ecr.eu-west-1.amazonaws.com/orders-api:1.14.2",
"essential": true,
"portMappings": [{ "containerPort": 8080, "protocol": "tcp" }],
"secrets": [
{
"name": "DATABASE_URL",
"valueFrom": "arn:aws:secretsmanager:eu-west-1:123456789012:secret:orders/db-url-AbCdEf"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/orders-api",
"awslogs-region": "eu-west-1",
"awslogs-stream-prefix": "orders"
}
}
}
]
}Its Kubernetes counterpart is four objects. Two of them, the ServiceAccount binding and the NetworkPolicy, only mean something given choices made outside the manifest:
apiVersion: v1
kind: ServiceAccount
metadata:
name: orders-api
namespace: orders
# On EKS, bind this ServiceAccount to an IAM role with an EKS Pod Identity
# association (configured through the EKS API, not in this manifest).
# Pod Identity is EKS-only and supports Linux EC2 nodes only (no Fargate, no Windows).
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-api
namespace: orders
spec:
replicas: 3
selector:
matchLabels:
app: orders-api
strategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
template:
metadata:
labels:
app: orders-api
spec:
serviceAccountName: orders-api
containers:
- name: orders-api
image: 123456789012.dkr.ecr.eu-west-1.amazonaws.com/orders-api:1.14.2
ports:
- containerPort: 8080
env:
- name: DATABASE_URL
valueFrom:
secretKeyRef:
name: orders-db
key: url
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
memory: 1Gi
readinessProbe:
httpGet:
path: /healthz
port: 8080
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: orders-api
namespace: orders
spec:
selector:
app: orders-api
ports:
- port: 80
targetPort: 8080
---
# Stands in for the task's security group. Needs an enforcing CNI: the
# Amazon VPC CNI with network policy enabled applies it on EC2 Linux nodes
# only. It is NOT enforced for Fargate pods.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: orders-api-ingress
namespace: orders
spec:
podSelector:
matchLabels:
app: orders-api
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: storefront
ports:
- protocol: TCP
port: 8080Deployment safety is a redesign too. The ECS circuit breaker works only with the rolling-update (ECS) deployment controller, and ECS blue/green shifts traffic itself for services using a load balancer, Service Connect or VPC Lattice; for headless services it only replaces tasks. Readiness probes and surge settings cover the basic case; automated rollback needs a progressive-delivery tool such as Argo Rollouts. Capacity providers map loosely to node pools and an autoscaler. The shape translates; the sizing logic is new work.
ECS Anywhere is not an exit ramp
ECS Anywhere looks like the answer to the second-environment problem: it registers on-premises servers or VMs into an ECS cluster as external instances. Read the considerations before counting it as optionality. External instances need an IAM role to communicate with AWS APIs, and AWS publishes six regional domains that the ECS agent and SSM Agent on each instance must be able to reach, with traffic allowed and DNS resolution working for every Region you use: ecs-a-*.<region>.amazonaws.com, used when managing tasks; ecs-t-*.<region>.amazonaws.com, for task and container metrics; ecs.<region>.amazonaws.com, the ECS service endpoint; ssm.<region>.amazonaws.com, the Systems Manager endpoint; ec2messages.<region>.amazonaws.com, which Systems Manager uses between agent and service; and ssmmessages.<region>.amazonaws.com, which Session Manager needs to create and delete session channels. Any other AWS service a task calls, such as ECR for images or CloudWatch Logs, needs its own endpoint allowed as well. Read that list as a firewall rule set, because that is what it becomes. Your hardware runs the containers; the scheduler you depend on is still reached through an AWS Region, and an egress policy that drops one of those domains takes away exactly the job that list assigns to it: drop ecs-t and you lose task and container metrics, drop ssmmessages and Session Manager cannot open session channels.
The feature set also shrinks on external instances. The ECS Anywhere documentation lists:
- Capacity providers aren't supported.
- The
awsvpcnetwork mode isn't supported; Linux tasks usebridge,hostornone. - Service load balancing isn't supported.
- Service discovery isn't supported.
So the constructs that made ECS simple on AWS, per-task ENIs, load-balanced services and discovery, are exactly the ones that do not come with you on-premises. ECS Anywhere moves compute, not control. For a team asked to serve a sovereign or on-premises customer, it is a way to place work near data while keeping AWS as the control plane, not a way to remove AWS from the dependency graph. If removing AWS is the requirement, the repatriation playbook is the relevant document, and it starts from Kubernetes.
Pricing optionality before you need it
"How locked in are we?" is answerable with a count. The script below is read-only. It walks every cluster and service in one account and Region, and counts the constructs graded as redesigns above: Service Connect, Cloud Map registries, enabled circuit breakers, task roles, awsvpc task definitions and secrets references.
#!/usr/bin/env bash
# ecs-lockin-inventory.sh - count ECS-bound constructs in one account and Region.
set -euo pipefail
region="${AWS_REGION:?set AWS_REGION}"
svc_total=0 svc_connect=0 svc_registry=0 svc_breaker=0
td_total=0 td_task_role=0 td_awsvpc=0 secret_refs=0
declare -A task_defs=()
# Capture first: a failed aws call exits here, before any table prints.
out=$(aws ecs list-clusters --region "$region" --output json)
mapfile -t clusters < <(jq -r '.clusterArns[]' <<<"$out")
for cluster in "${clusters[@]}"; do
out=$(aws ecs list-services --region "$region" --cluster "$cluster" --output json)
mapfile -t services < <(jq -r '.serviceArns[]' <<<"$out")
# describe-services accepts at most 10 services per call.
for ((i = 0; i < ${#services[@]}; i += 10)); do
batch=$(aws ecs describe-services --region "$region" --cluster "$cluster" \
--services "${services[@]:i:10}" --output json)
svc_total=$((svc_total + $(jq '.services | length' <<<"$batch")))
svc_connect=$((svc_connect + $(jq '[.services[]
| select(any(.deployments[]?; .status == "PRIMARY"
and .serviceConnectConfiguration.enabled == true))] | length' <<<"$batch")))
svc_registry=$((svc_registry + $(jq '[.services[]
| select((.serviceRegistries // []) | length > 0)] | length' <<<"$batch")))
svc_breaker=$((svc_breaker + $(jq '[.services[]
| select(.deploymentConfiguration.deploymentCircuitBreaker.enable == true)]
| length' <<<"$batch")))
# EXTERNAL services may have no taskDefinition; skip those.
while read -r td; do
[[ -n "$td" ]] && task_defs["$td"]=1
done < <(jq -r '.services[].taskDefinition // empty' <<<"$batch")
done
done
for td in "${!task_defs[@]}"; do
def=$(aws ecs describe-task-definition --region "$region" \
--task-definition "$td" --output json)
td_total=$((td_total + 1))
[[ $(jq -r '.taskDefinition.taskRoleArn // empty' <<<"$def") ]] \
&& td_task_role=$((td_task_role + 1))
[[ $(jq -r '.taskDefinition.networkMode // empty' <<<"$def") == "awsvpc" ]] \
&& td_awsvpc=$((td_awsvpc + 1))
secret_refs=$((secret_refs + $(jq '[.taskDefinition.containerDefinitions[]
| (.secrets // [])[]] | length' <<<"$def")))
done
printf '%-40s %6s\n' "construct" "count"
printf '%-40s %6d\n' "services" "$svc_total" \
"services with Service Connect" "$svc_connect" \
"services with Cloud Map registries" "$svc_registry" \
"services with circuit breaker enabled" "$svc_breaker" \
"task definitions in use" "$td_total" \
"task definitions with a task role" "$td_task_role" \
"task definitions using awsvpc" "$td_awsvpc" \
"secrets valueFrom references" "$secret_refs"Turn the counts into an estimate, and label it as one. Multiply each row by your own judgement of the effort per construct, then add the fixed work that does not scale with service count: choosing and operating a CNI that enforces policy, an identity system, a secrets operator, a progressive-delivery tool, and rewriting the deployment pipeline. What you get is a written, reviewable number. The method in pricing your exit as a number covers the data-gravity and contract terms this script cannot see. Run the script per account and per Region, because ECS objects and Cloud Map namespaces are regional. The same discipline applies a layer down, in the infrastructure code: Terraform vs CloudFormation is the IaC version of this decision.
Then write down the triggers that should force a re-evaluation, before they arrive:
- A second environment outside AWS: another cloud, a colocation footprint, or a customer-hosted deployment.
- A sovereign or on-premises customer whose contract names where the control plane may run.
- A regulator or auditor asking for a documented exit plan for a critical service.
- The construct counts growing faster than the services they belong to, which tends to mean ECS-specific features are being adopted by default rather than by decision.
What a migration to Kubernetes involves
The order is set by dependencies: a container that cannot authenticate, find its peers or read its secrets is not running, however healthy the Deployment looks.
- Identity first. Map every task role to a ServiceAccount and decide the binding. On EKS with Linux EC2 nodes, Pod Identity or IRSA. On Fargate or Windows nodes, IRSA, because Pod Identity supports neither. Off AWS, a workload identity such as SPIFFE, federated to AWS for the calls that remain.
- Secrets. Install External Secrets Operator against the existing Secrets Manager entries, so values are not copied by hand. Remember that ECS picked changed secrets up only on a new deployment; decide whether your pods should behave the same way.
- Networking and discovery. Choose the CNI and confirm it enforces NetworkPolicy on the node types you will run. Translate security groups into policies, and replace Service Connect names with Service DNS or a mesh.
- Deployments. Rebuild rollout safety: readiness probes, surge settings and, where you used ECS blue/green, a progressive-delivery controller.
- Dual-run and cutover. Run both stacks behind the same load balancer or DNS, shift traffic by weight, and keep the ECS service deployable until the Kubernetes one has been through a full release cycle.
Fargate deserves its own warning, because "Fargate is Fargate" is the assumption that breaks. On EKS, Fargate does not support DaemonSets, privileged containers, GPUs, HostPort or HostNetwork, or EBS volume mounts, and the VPC CNI does not enforce network policies on Fargate pods. Node agents for logging, security and observability are commonly shipped as DaemonSets, so a Fargate-only EKS cluster needs sidecars or managed alternatives for each. Decide early whether the target is EC2-backed node groups, Fargate, or a mix.
One sizing note. ECS's non-adjustable quotas (5,000 services per cluster, 5,000 tasks per service, dropping to 1,000 for services that use service discovery, and 10 containers per task definition) are generous enough that a migration case built on them deserves suspicion.
Audit, on-call and the long game
Ask what evidence each option produces. On ECS, the record of who changed a service or task definition is in CloudTrail: AWS-hosted, AWS-formatted, held in your account but defined by AWS. On Kubernetes you add the API server's audit log, a stream you configure and retain yourself on a self-run cluster. For SOC 2 on self-hosted Kubernetes, owning that evidence is the point. On-call splits the same way. ECS puts the scheduler on AWS's pager and everything above it on yours. Self-run Kubernetes puts the control plane on yours too, which is the cost Docker Swarm vs Kubernetes weighs against team size.
The tooling above ECS keeps moving. Docker's Compose integration for ECS has been deprecated, and Docker tells users to rely on native cloud tools or third-party solutions instead. AWS announced on 6 March 2026 that the AWS Copilot CLI would reach end of support on 12 June 2026. It remains on GitHub as open source, but it no longer gets new features or security updates from AWS, and AWS pointed its users to ECS Express Mode or AWS CDK L3 constructs. Two developer front-ends for ECS have now been deprecated or have lost AWS support. Budget for ECS tooling being replaced while the API stays put.
Conformance enables interoperability from one Kubernetes installation to the next. It allows them the flexibility to choose between vendors.
The verdict is not a winner. ECS is the right answer when AWS is the only environment you will ever need and you have written down what leaving would cost: the counts, the estimate and the triggers. Kubernetes is the right answer when a second environment is probable, not merely possible, because then the redesign work above is a cost you will pay anyway, and it is cheaper before the constructs multiply. Either way, the sovereign move is the same: price the exit while it is still an option, not after it has become an emergency.
§FAQ/Common questions
Frequently asked
Is ECS cheaper than Kubernetes on AWS?
For orchestration itself, yes. ECS has no orchestration fee on EC2 or Fargate, while EKS charges $0.10 per cluster per hour in standard support and $0.60 in extended support (as of October 2026). At scale compute usually dominates, though on a small or idle cluster the control-plane fee can be a large share of the bill, and the cost of a future exit belongs in the comparison too.
Can I move an ECS task definition to Kubernetes directly?
The container image, ports, resources and environment translate into a Pod spec in a Deployment. The task IAM role, awsvpc security groups, Service Connect and secrets valueFrom references do not translate mechanically; each needs a Kubernetes design decision such as a ServiceAccount binding, a NetworkPolicy with an enforcing CNI, Service DNS or a mesh, and External Secrets Operator.
Does ECS Anywhere let me leave AWS?
No. External instances need an IAM role and must reach six regional ECS and Systems Manager endpoints, and they lose capacity providers, the awsvpc network mode, service load balancing and service discovery. ECS Anywhere moves where containers run, not who runs the scheduler.
Is EKS Pod Identity a portable replacement for ECS task roles?
No. Pod Identity is available only on Amazon EKS, not on EKS Anywhere or self-run clusters, and it supports only pods on Linux EC2 nodes, not Fargate or Windows. It swaps one AWS-bound identity binding for another. A workload identity you own, such as SPIFFE/SPIRE, is the portable path.
What happened to the AWS Copilot CLI?
AWS ended support for the Copilot CLI on 12 June 2026, as announced on 6 March 2026. It remains on GitHub as open source but receives no new features or security updates from AWS, which recommends ECS Express Mode or AWS CDK L3 constructs instead.
Further reading
- Vendor Lock-In in the Cloud: Pricing Your Exit as a Number
- Docker Swarm vs Kubernetes: Four Gates, Not Node Count
- Cloud Repatriation: The Kubernetes Engineering Playbook
- Terraform vs CloudFormation: Who Holds the Record
- SPIFFE/SPIRE on Kubernetes: Zero-Trust Workload Identity
- Kubernetes Secrets Are Still Broken: ESO Over Vault
- Kubernetes Upgrade Debt: Skew, Dead APIs, Safe Paths
- SOC 2 on Self-Hosted Kubernetes: Own the Evidence
Executive Briefing
Thirty minutes to clarify your infrastructure risk
Walk us through your vendor footprint and regulatory constraints. We will tell you honestly where sovereignty creates leverage — and where it does not. No pitch deck. No obligation.