Kubernetes Deployments, Services & Ingress Explained
This covers the Deployment, Service, and Ingress settings, rollout strategy, readiness/liveness probes, and a PodDisruptionBudget, that decide whether a bad release is a non-event or an outage.
The Deployment: replicas, resources, and rollout strategy
The Deployment is the controller that keeps N copies of your pod running and manages the transition between versions. The part people skip is resources and strategy.
apiVersion: apps/v1
kind: Deployment
metadata:
name: checkout-api
labels:
app: checkout-api
spec:
replicas: 4
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: checkout-api
template:
metadata:
labels:
app: checkout-api
spec:
containers:
- name: checkout-api
image: registry.example.com/checkout-api:1.8.2
ports:
- containerPort: 8080
resources:
requests:
cpu: "250m"
memory: "256Mi"
limits:
cpu: "500m"
memory: "512Mi"maxUnavailable: 0 with maxSurge: 1 means Kubernetes always spins up one extra pod before tearing down an old one, so you never drop below full capacity, at the cost of a slightly slower rollout and briefly running N+1 pods, worth it every time for anything client-facing. The opposite setting (maxUnavailable: 1, maxSurge: 0) kills a pod before its replacement is ready, which is exactly how a "routine deploy" becomes a capacity incident on a service already close to its limits.
On resources: set requests to what the container actually needs under normal load: the scheduler uses this to place pods, and it's what PodDisruptionBudget and autoscaling math are based on. Keep CPU limits close to requests; a generous limit just moves noisy-neighbor throttling somewhere else. Memory is different: breach the limit and the container gets OOMKilled, so leave headroom there.
Readiness vs. liveness probes: two different questions
Mixing these two up causes more production incidents than anything else on this list: the two probes look identical in YAML but do opposite things when they fail.
- Readiness probe: "should this pod receive traffic right now?" Failing it removes the pod from the Service's endpoint list; it keeps running, just out of rotation.
- Liveness probe: "is this process alive enough to keep, or should it be killed and restarted?" Failing it kills the container.
readinessProbe:
httpGet:
path: /healthz/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /healthz/live
port: 8080
initialDelaySeconds: 15
periodSeconds: 10
failureThreshold: 3Never point your liveness probe at an endpoint that checks downstream dependencies (database, cache, third-party API): if the database has a slow minute, a liveness probe checking DB connectivity kills every pod at once, turning a database blip into a full outage. Readiness can check downstream health; liveness should only check whether its own process is responsive.
Set the failureThreshold × periodSeconds window too short on a liveness probe and it'll restart a pod that's merely slow under load, not broken, dropping in-flight requests. If the whole fleet hits the same load spike, that becomes a self-inflicted crash loop: the "fix" for slowness is repeatedly killing the pods trying to catch up. When in doubt, give liveness a longer, more forgiving window than readiness.
The Service: how traffic actually finds a pod
Pods are disposable and get new IPs constantly; a Service gives them a stable virtual IP and DNS name, using a label selector to decide membership.
apiVersion: v1
kind: Service
metadata:
name: checkout-api
spec:
type: ClusterIP
selector:
app: checkout-api
ports:
- port: 80
targetPort: 8080
protocol: TCPThe selector here (app: checkout-api) has to match the labels on the pod template in the Deployment, not the Deployment's own metadata. This is the most common copy-paste bug I see: someone renames the Deployment, forgets to update the pod template labels, and the Service silently has zero endpoints. kubectl get endpoints checkout-api is the first command I run whenever "the Service isn't working": an empty list means a selector mismatch nine times out of ten.
The Ingress: TLS termination and routing
ClusterIP only routes traffic inside the cluster. An Ingress maps an external hostname to that internal Service and, paired with cert-manager, handles TLS termination without you touching a certificate by hand.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: checkout-api
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
ingressClassName: nginx
tls:
- hosts:
- api.example.com
secretName: checkout-api-tls
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: checkout-api
port:
number: 80The cert-manager.io/cluster-issuer annotation triggers cert-manager to request and renew a certificate automatically, storing it in the checkout-api-tls Secret named under tls.secretName, so certificate expiry stops being something a human has to track.
The full request path, including a rolling update:
PodDisruptionBudget: surviving node drains
Everything above protects you during a rollout you control, not a node drain: a cluster autoscaler scaling down, a node cordoned for maintenance, an underlying VM replaced. Without a PodDisruptionBudget, Kubernetes is free to evict every pod of your Deployment on a single node at once, even down to zero replicas.
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: checkout-api-pdb
spec:
minAvailable: 2
selector:
matchLabels:
app: checkout-apiminAvailable: 2 tells the eviction API to never voluntarily take this Deployment below 2 ready pods. Node drains, autoscaler scale-downs, and kubectl drain all respect this, evicting pods one at a time instead of all at once. It's a small object, and the one most teams skip, right up until a routine node upgrade takes one down.
None of these objects are complicated individually. Teams get burned shipping them once, copying the YAML from a tutorial, and never coming back to add probes, a rollout strategy, or a PDB before real traffic hits. Add them up front, and a rollout gets boring. In production, boring beats interesting.
Want to actually run this in production?
This tutorial covers the concepts and architecture. If you want to implement it in your own infrastructure, or get good enough to own this problem long-term, I offer 1:1 mentoring built around your real environment, not a generic course.
This tutorial
- Core architecture & key concepts
- Illustrative code snippets
- The reasoning behind each decision
1:1 mentoring
- Working sessions on your own environment
- Direct answers to the edge cases you're hitting
- Feedback on your actual implementation
- Ongoing support as you build it out