ECS Fargate automates container orchestration on AWS specifically. Kubernetes does the same job in a cloud-agnostic, more configurable way, at the cost of a steeper learning curve — pods, deployments, and services are the three objects that make up almost everything you'll actually touch day to day.
4 min read
Both ECS and Kubernetes solve the same core problem — "run N copies of my container, replace failed ones, route traffic to healthy ones, roll out updates without downtime" — covered generically in the ECS Fargate lesson. Kubernetes takes a different design stance: instead of one cloud provider's opinionated managed service, it's an open, portable API that AWS (EKS), Google (GKE), Azure (AKS), or a self-hosted cluster can all implement identically. That portability and its far larger ecosystem come at a real cost — more concepts, more YAML, more to actually operate — which is why "start with ECS Fargate, reach for Kubernetes when its flexibility is worth the complexity" is a defensible default rather than a compromise.
A Pod is not a container — it's a wrapper around one or more containers that always run together, on the same machine, sharing the same network namespace (so they can reach each other over localhost) and optionally the same storage. In practice, most Pods run exactly one container; a second "sidecar" container (a log shipper, a proxy) is the main reason to add more.
apiVersion: v1
kind: Pod
metadata:
name: my-app-pod
spec:
containers:
- name: app
image: my-registry/my-app:v1.2.0
ports:
- containerPort: 3000
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"resources.requests is what the scheduler uses to decide which node has room for this Pod; resources.limits is a hard ceiling the container isn't allowed to exceed (a container hitting its memory limit gets killed — the OOMKilled gotcha covered in this domain's capstone lesson).
You almost never create a bare Pod directly in a real deployment, though, because a Pod created this way isn't replaced if it dies. That's what a Deployment is for.
A Deployment wraps Pods with a declaration of how many copies should exist and how updates should roll out. You don't tell Kubernetes "start 3 containers" — you declare "3 replicas of this Pod spec should exist," and a background control loop continuously reconciles reality toward that declaration.
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3
selector:
matchLabels:
app: my-app
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # allow 1 extra Pod during rollout
maxUnavailable: 0 # never drop below 3 healthy Pods
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: app
image: my-registry/my-app:v1.2.0Change the image tag and re-apply, and the Deployment controller performs the same rolling-update dance as ECS: start a new Pod, wait for it to be ready, retire an old one, repeat — governed by maxSurge/maxUnavailable the same way ECS uses maximum_percent/minimum_healthy_percent.
If a Pod crashes, the Deployment's underlying ReplicaSet notices the actual count has dropped below the desired count and starts a replacement — the same self-healing behavior as an ECS service, driven by the same "declare the end state, let a controller converge toward it" philosophy that runs through all of Kubernetes.
Pods are disposable — a rollout, a crash, or a scale-down event constantly creates and destroys them, and each one gets a new internal IP address every time. Nothing that depends on "the app" should ever hold onto a Pod's IP directly. A Service solves this by giving a stable, unchanging address that automatically routes to whichever Pods currently match a label selector.
apiVersion: v1
kind: Service
metadata:
name: my-app-svc
spec:
selector:
app: my-app
ports:
- port: 80
targetPort: 3000
type: ClusterIPselector: app: my-app is the entire mechanism — any Pod carrying that label is automatically part of this Service's routing, added and removed as Pods come and go, with zero manual wiring. Three type values matter for different situations:
| Type | Reachable from | Typical use |
|---|---|---|
ClusterIP (default) | Only inside the cluster | Internal service-to-service traffic (an API talking to an internal auth service) |
NodePort | Any cluster node's IP, on a specific port | Rare in production; mostly local/dev clusters |
LoadBalancer | The public internet, via a cloud load balancer the cluster provisions | The public-facing entry point to your app |
A typical app therefore has a LoadBalancer Service in front of its public-facing Deployment, and ClusterIP Services in front of any internal-only Deployments it talks to.
A Deployment declares and maintains a set of Pods; a Service gives that set of Pods a stable address regardless of which specific Pods currently exist. Everything else in Kubernetes — ConfigMaps, Secrets, Ingress, Horizontal Pod Autoscalers — builds on top of this same core: Pods as the unit of execution, Deployments as the reconciling desired-state layer, Services as the stable network identity.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What is a Kubernetes Pod?
2. What does a Kubernetes Deployment actually declare?
3. Why is a Service needed in front of a Deployment's Pods instead of addressing Pods directly?
4. When would you use a ClusterIP Service instead of a LoadBalancer Service?
Docker & Containers