Kubernetes Administration — Level by Level›17 · Deploy an app end to end

Lesson 17 of 32 · Level 3 — Application delivery

Deploy an app end to end

Ship one application properly, from Deployment to Service to Ingress: probes, resources and rolling updates on the Deployment, ports and selectors on the Service, host rules on the Ingress, and a hop-by-hop method to find exactly where a request stops.

Practitioner
Key wordsDeploymentServiceIngressreadiness probeliveness proberesourcesrolling updateport vs targetPorthop-by-hop debuggingpodinfo

The three layers

Ingress   "podinfo.127.0.0.1.nip.io/  →  Service podinfo:80"
   │
Service   "port 80  →  pods with app=podinfo, targetPort 9898"
   │
Deployment "3 replicas of podinfo, healthy, with resources, rolling updates"

Each layer has one job, and each can be checked on its own. This lesson builds all three for one app, carefully, on the ingress-enabled kind cluster from lesson 05.

Opening a lemonade stand: the Deployment is the team of sellers (and a rule that there are always three of them). The Service is the stand's fixed name board, so customers don't care which seller serves them. The Ingress is the signpost at the park entrance saying "Lemonade → this way". If customers can't find lemonade, you check the signpost, then the name board, then whether any sellers are actually working.

Layer 1: the Deployment

# deploy.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: podinfo
  labels: { app: podinfo }
spec:
  replicas: 3
  selector:
    matchLabels: { app: podinfo }
  strategy:
    type: RollingUpdate
    rollingUpdate: { maxUnavailable: 0, maxSurge: 1 }
  template:
    metadata:
      labels: { app: podinfo }
    spec:
      containers:
        - name: podinfo
          image: ghcr.io/stefanprodan/podinfo:6.7.0
          ports:
            - name: http
              containerPort: 9898
          readinessProbe:
            httpGet: { path: /readyz, port: http }
            periodSeconds: 5
          livenessProbe:
            httpGet: { path: /healthz, port: http }
            initialDelaySeconds: 5
            periodSeconds: 10
          resources:
            requests: { cpu: 50m, memory: 64Mi }
            limits: { memory: 128Mi }
  • Readiness decides when a pod gets traffic; liveness restarts a stuck container. Keep liveness about the process only (see SRE & Production Incident Response, lesson 03).
  • maxUnavailable: 0, maxSurge: 1: a new pod must be Ready before an old one is removed.
  • Requests let the scheduler place pods sensibly (lesson 23 goes deeper).

Layer 2: the Service

# svc.yaml
apiVersion: v1
kind: Service
metadata:
  name: podinfo
spec:
  selector: { app: podinfo }       # must match the pod labels
  ports:
    - name: http
      port: 80                     # what clients use
      targetPort: http             # the container port (by name: 9898)

Using the port name (http) as targetPort means the Service keeps working if the container port number changes.

Layer 3: the Ingress

# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: podinfo
spec:
  ingressClassName: nginx
  rules:
    - host: podinfo.127.0.0.1.nip.io
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service: { name: podinfo, port: { name: http } }
$ kubectl create namespace shop-web
$ kubectl apply -n shop-web -f deploy.yaml -f svc.yaml -f ingress.yaml
$ kubectl rollout status deploy/podinfo -n shop-web
$ curl -s http://podinfo.127.0.0.1.nip.io/ | head -5
{
  "hostname": "podinfo-7c9d8f6b8-x2kqp",
  "version": "6.7.0",

Run the curl a few times: the hostname changes as the Service spreads requests across pods.

Roll out, roll back

$ kubectl set image deploy/podinfo podinfo=ghcr.io/stefanprodan/podinfo:6.7.1 -n shop-web
$ kubectl rollout status deploy/podinfo -n shop-web
$ kubectl rollout history deploy/podinfo -n shop-web
$ kubectl rollout undo deploy/podinfo -n shop-web

In real teams, the image change is a Git commit and GitOps applies it (see GitOps with Argo CD). Choose image tags that exist in your registry; the versions above are examples.

Debug hop by hop

When "the site doesn't work", walk the path from the inside out, and stop at the first failing hop:

Hop Command Healthy looks like
1. Pods kubectl get pods -n shop-web All Running, READY 1/1
2. Endpoints kubectl get endpointslices -n shop-web Pod IPs listed on port 9898
3. Service in-cluster kubectl run t --rm -it --image=busybox -n shop-web -- wget -qO- http://podinfo JSON response
4. Ingress kubectl describe ingress podinfo -n shop-web Right host/path/backend, ADDRESS set
5. Outside curl -v http://podinfo.127.0.0.1.nip.io/ HTTP 200

Most outages in this path are label/selector mismatches, wrong ports, failing readiness probes, or a missing ingressClassName.

Try it: break each layer

  1. Deploy the app as above and confirm step 5 works.
  2. Change the Service selector to app: podinfo-x; walk the hops and find where it breaks (which status code does NGINX return?). Fix it.
  3. Set targetPort: 8080; observe the difference between "no endpoints" and "connection refused". Fix it.
  4. Make the readiness probe path /nope; watch a rollout stall with maxUnavailable: 0 while the old pods keep serving. Roll back.
  5. Remove ingressClassName and see whether your controller still picks it up (depends on whether a default IngressClass is set).

Going deeper: production-ready defaults

  • Add a PodDisruptionBudget and topology spread so drains and node failures don't take all replicas at once.
  • Package the three manifests as a Helm chart or Kustomize base with per-environment overlays (see GitOps with Argo CD, lesson 04).
  • Add HPA on CPU or requests per second (lesson 24), and alerts on the Ingress error rate (see Observability with OpenTelemetry, lesson 06).

Recap

  • Deployment (probes, resources, rolling strategy) → Service (selector, port/targetPort) → Ingress (class, host, path).
  • Readiness gates traffic; maxUnavailable: 0 + maxSurge: 1 keeps capacity during rollouts.
  • Debug inside out: pods → endpoints → Service in-cluster → Ingress → outside.

This site is a public version of my personal engineering knowledge hub. It intentionally excludes confidential company information and internal operational details.