# Deployment, Service and Ingress: how a request reaches your Pods

> Every object between a browser and a container in a classic Kubernetes web app: Deployment, ReplicaSet, Pods, Service, EndpointSlice, Ingress, TLS Secret and HPA.

Interactive graph: https://k8wiz.com/graphs/deployment-service-ingress

This is the setup most people deploy first: a stateless web app behind an Ingress. You write five objects. Kubernetes and the ingress controller create the rest, and most debugging comes down to knowing which object created which.

The ingress controller here is Traefik, but the shape is the same with HAProxy, Contour or any other controller. The Service only finds Pods by label, and the controller sends traffic straight to Pod IPs that it reads from EndpointSlices. It doesn't go through the Service's ClusterIP.

## Resources

### Client Browser

outside the cluster

Sends `GET https://shop.example.com/`. DNS for `shop.example.com` points at the cloud load balancer.

### Load balancer Cloud load balancer

outside the cluster

Created by the cloud provider for the ingress controller's `type: LoadBalancer` Service. It forwards ports 80 and 443 to the controller Pods.

### Deployment traefik

comes with an add-on, apps/v1, namespace traefik

The ingress controller, installed with its Helm chart. It watches Ingress objects of its class, loads their TLS Secrets, and proxies each request to a Pod IP it reads from EndpointSlices.

### IngressClass traefik

comes with an add-on, networking.k8s.io/v1

Names the controller that serves Ingresses with `ingressClassName: traefik`. A cluster can run several controllers, and the class decides which one picks up an Ingress.

### Ingress shop

you write it, networking.k8s.io/v1, namespace shop

Maps the host `shop.example.com` and path `/` to the `shop` Service on port 80, and names the Secret with the TLS certificate. It only describes routing. The controller does the work.

### Secret shop-tls

you write it, v1, namespace shop

Holds the certificate and private key as `tls.crt` and `tls.key`. It must be in the Ingress's namespace. cert-manager can create and renew it for you.

### Service shop

you write it, v1, namespace shop

Selects Pods with the label `app: shop` and gives them one stable name, `shop.shop.svc.cluster.local`. Port 80 on the Service maps to port 8080 in the container.

### EndpointSlice shop-x7k2p

created by a controller, discovery.k8s.io/v1, namespace shop

Created by the EndpointSlice controller for the Service. It lists the IP of every Pod the selector matches, with a `ready` condition that follows the readiness probe. Ingress controllers and kube-proxy read this, not the Service.

### Deployment shop

you write it, apps/v1, namespace shop

Declares the Pod template and the rollout strategy. On every change to the template it creates a new ReplicaSet and scales the old one down.

### ReplicaSet shop-7d9f8c6b5

created by a controller, apps/v1, namespace shop

Created by the Deployment, one per version of the Pod template. The suffix is a hash of the template. It keeps the replica count of Pods running.

### Pod ×3 shop-7d9f8c6b5-xxxxx

created by a controller, v1, namespace shop

Three Pods created by the ReplicaSet, each with the label `app: shop` copied from the template. That label is the only link between them and the Service.

### ConfigMap shop-config

you write it, v1, namespace shop

Settings loaded as environment variables through `envFrom`. Pods read it only at start, so changing it needs a rollout, for example `kubectl rollout restart deployment/shop`.

### HorizontalPodAutoscaler shop

you write it, autoscaling/v2, namespace shop

Changes `spec.replicas` on the Deployment to keep average CPU near 70% of the requested 200m. It needs metrics-server and CPU requests on the containers.

## How the resources connect

- Client Browser → Load balancer Cloud load balancer: HTTPS :443 (traffic)
- Load balancer Cloud load balancer → Deployment traefik: forwards (traffic)
- Deployment traefik → Ingress shop: watches (references)
- Ingress shop → IngressClass traefik: ingressClassName (references)
- Ingress shop → Secret shop-tls: tls.secretName (references)
- Ingress shop → Service shop: backend.service (references)
- Deployment traefik → EndpointSlice shop-x7k2p: reads Pod IPs (references)
- Deployment traefik → Pod ×3 shop-7d9f8c6b5-xxxxx: proxies to Pod IP (traffic)
- Service shop → EndpointSlice shop-x7k2p: creates (creates)
- Service shop → Pod ×3 shop-7d9f8c6b5-xxxxx: selector app=shop (selects)
- EndpointSlice shop-x7k2p → Pod ×3 shop-7d9f8c6b5-xxxxx: lists IPs (references)
- Deployment shop → ReplicaSet shop-7d9f8c6b5: creates (creates)
- ReplicaSet shop-7d9f8c6b5 → Pod ×3 shop-7d9f8c6b5-xxxxx: creates (creates)
- Pod ×3 shop-7d9f8c6b5-xxxxx → ConfigMap shop-config: envFrom (references)
- HorizontalPodAutoscaler shop → Deployment shop: scales replicas (references)

## How it works

1. **The Deployment creates the Pods.** You apply the Deployment. It creates a ReplicaSet for the current Pod template, and the ReplicaSet creates three Pods. Each Pod gets the label `app: shop` from the template and reads its settings from the ConfigMap.
2. **The Service finds the Pods by label.** The Service has no list of Pods. It has a selector, `app: shop`. The EndpointSlice controller looks for Pods matching it and writes their IPs into an EndpointSlice, marking a Pod ready only once its readiness probe passes.
3. **The Ingress describes the route.** The Ingress says that requests for `shop.example.com` go to the `shop` Service on port 80. `ingressClassName: traefik` tells the Traefik controller this Ingress is its job. Other controllers in the cluster ignore it.
4. **The controller terminates TLS.** The controller reads `shop-tls` from the Ingress's namespace and serves its certificate for `shop.example.com`. If the Secret is missing or malformed, most controllers fall back to a self-signed default certificate, and browsers warn.
5. **A request reaches a Pod.** The browser connects to the cloud load balancer, which forwards to a controller Pod. The controller matches the host and path, picks a ready address from the EndpointSlice and opens a connection straight to that Pod IP on port 8080.
6. **The HPA adjusts the replica count.** Every 15 seconds the HPA compares average CPU use to 70% of the requests and sets `spec.replicas` on the Deployment. New Pods match the selector, so they show up in the EndpointSlice once they're ready. You don't have to change anything else.

## Common mistakes

- A selector that matches no Pods gives a Service with no endpoints, and the controller returns 503. `kubectl get endpointslices -n shop -l kubernetes.io/service-name=shop` shows an empty list when this happens. Compare the selector with `kubectl get pods -n shop --show-labels`.
- An Ingress without `ingressClassName` is ignored, unless one IngressClass has the `ingressclass.kubernetes.io/is-default-class: "true"` annotation.
- The Ingress, its backend Service and its TLS Secret must all be in the same namespace. An Ingress can't point at a Service in another namespace. Gateway API can, with a ReferenceGrant.
- A failing readiness probe takes the Pod out of rotation, and that's by design. If every Pod fails it, the app is down even though `kubectl get pods` shows them all as Running.
- Setting `spec.replicas` in a Deployment that an HPA manages makes every `kubectl apply` reset the count. Leave `replicas` out of the manifest once the HPA owns it.

## The manifests you write

```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: shop
  namespace: shop
spec:
  ingressClassName: traefik
  tls:
    - hosts: [shop.example.com]
      secretName: shop-tls
  rules:
    - host: shop.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: shop
                port:
                  number: 80
---
apiVersion: v1
kind: Secret
metadata:
  name: shop-tls
  namespace: shop
type: kubernetes.io/tls
data:
  tls.crt: LS0tLS1CRUdJTi... # base64 PEM
  tls.key: LS0tLS1CRUdJTi...
---
apiVersion: v1
kind: Service
metadata:
  name: shop
  namespace: shop
spec:
  selector:
    app: shop
  ports:
    - name: http
      port: 80
      targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: shop
  namespace: shop
spec:
  replicas: 3
  selector:
    matchLabels:
      app: shop
  template:
    metadata:
      labels:
        app: shop
    spec:
      containers:
        - name: shop
          image: ghcr.io/acme/shop:1.4.2
          ports:
            - containerPort: 8080
          envFrom:
            - configMapRef:
                name: shop-config
          readinessProbe:
            httpGet:
              path: /healthz
              port: 8080
          resources:
            requests:
              cpu: 200m
              memory: 256Mi
---
apiVersion: v1
kind: ConfigMap
metadata:
  name: shop-config
  namespace: shop
data:
  LOG_LEVEL: info
  PAYMENTS_URL: http://payments.payments.svc:8080
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: shop
  namespace: shop
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: shop
  minReplicas: 3
  maxReplicas: 10
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70
```
