Deployment, Service and Ingress

A Deployment runs copies of your container, a Service gives them one stable address, and an Ingress puts a hostname in front.

Syntaxkubectl apply -f deployment.yaml -f service.yaml -f ingress.yaml

A Kubernetes cluster is a group of machines that runs containers for you. A container is your compiled program and the files it needs, packed into an image. The smallest thing a cluster runs is a pod: one or more containers that share a network address. You never create pods by hand. You write a description of what you want, in a text format called YAML, and send it with kubectl apply. The cluster then changes reality to match the description, and keeps it matching.

The three objects

A Deployment says which image to run and how many copies. It creates the pods and replaces any that die. A Service gives those pods one stable name and address inside the cluster. Individual pods come and go and their addresses change; the Service name does not. An Ingress maps a public hostname and URL path onto a Service, so traffic from outside the cluster arrives.

Labels are the only glue. The Deployment finds its pods because spec.selector.matchLabels matches the labels in spec.template.metadata.labels. The Service finds the same pods through its own spec.selector. No object holds a pointer to another.

What trips people up

Your server must listen on :8080, not 127.0.0.1:8080. The second form accepts connections only from inside the container, so the Service reaches nothing and every request times out. An Ingress also does nothing on its own: the cluster needs an ingress controller running, and pathType is required on every path.

The example below is the whole program the Deployment runs. Run it, then curl localhost:8080/healthz to see the same reply the cluster will get.

Example

Example · go
package main

import (
	"fmt"
	"net/http"
	"os"
)

func main() {
	// The Deployment sets PORT. 8080 is used when nothing sets it.
	port := os.Getenv("PORT")
	if port == "" {
		port = "8080"
	}

	mux := http.NewServeMux()
	mux.HandleFunc("GET /healthz", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "ok")
	})
	mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "hello from the pod")
	})

	// ":" + port binds every network interface in the container.
	// "127.0.0.1:" + port would accept connections only from inside
	// the container, and the Service would reach nothing.
	addr := ":" + port
	fmt.Println("listening on", addr)
	// ListenAndServe returns only on failure, so report it and stop.
	if err := http.ListenAndServe(addr, mux); err != nil {
		fmt.Println("server stopped:", err)
	}
}

// Output:
// listening on :8080

When to use it

  • Put the first version of a Go HTTP service on a cluster and reach it from your laptop.
  • Give a service a stable in-cluster address so other services can call it by name.
  • Pass a database password into a container from a Secret instead of baking it into the image.
  • Serve one hostname over TLS and route two URL paths to two different services.

More examples

deployment.yaml

This is the object you write first, and the one you edit most. Three things are worth copying. The image carries a real version, 1.4.2, not the tag latest: with latest you cannot tell which build a pod is running, and a restart can quietly pick up a different one. containerPort is given a name, http, so probes and the Service can refer to it by name instead of repeating the number. The database password is not in the file; valueFrom reads it out of a Secret at start-up, which keeps it out of version control.

Example · yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: shop-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: shop-api        # must equal the pod labels below
  template:
    metadata:
      labels:
        app: shop-api
    spec:
      containers:
        - name: app
          image: registry.example.com/shop-api:1.4.2
          ports:
            - name: http
              containerPort: 8080
          env:
            - name: PORT
              value: "8080"    # quoted: env values are strings
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: shop-api-db
                  key: url

Service and Ingress, with the short form of ports

Two objects in one file, separated by the three dashes YAML uses between documents. The Service is the part other services in the cluster use: they call http://shop-api/ and the cluster resolves that name. Reach for the Ingress only when traffic comes from outside. Note that the Ingress points at a Service by name and at the Service's port, 80, not at the container's 8080. pathType has no default in networking.k8s.io/v1, so leaving it out is a validation error rather than a silent fallback.

Example · yaml
apiVersion: v1
kind: Service
metadata:
  name: shop-api
spec:
  type: ClusterIP          # the default: reachable inside the cluster
  selector:
    app: shop-api          # picks the same pods as the Deployment
  ports:
    # Longhand, every field written out.
    - name: http
      protocol: TCP        # TCP is the default and may be left out
      port: 80             # the port the Service answers on
      targetPort: http     # the container port named "http"
    # Shorthand for the same thing when both numbers match:
    #   - port: 8080
    # Leaving targetPort out makes it equal to port. Identical result.
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: shop-api
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.example.com
      secretName: shop-api-tls
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix   # required; Exact is the alternative
            backend:
              service:
                name: shop-api
                port:
                  number: 80

Applying it and checking what happened

The loop you will live in. apply is declarative: run it twice and the second run changes nothing, so the same command creates and updates. When a pod does not come up, describe is the command that tells you the truth; the events list names things like ImagePullBackOff (the cluster cannot fetch your image) or CrashLoopBackOff (your program exits on start-up, and the logs say why). port-forward is the quickest way to prove a Service works before blaming the Ingress.

Example · bash
# Send all three objects. -f repeats, or names a whole folder.
kubectl apply -f deployment.yaml -f service.yaml -f ingress.yaml

# What exists now, and whether the pods actually started.
kubectl get deploy,svc,ingress,pods -l app=shop-api

# A pod that will not start: the events at the bottom say why.
kubectl describe pod -l app=shop-api

# Your program's own output, following new lines as they arrive.
kubectl logs -l app=shop-api --tail=50 -f

# Reach the Service without an Ingress: forward a local port to it.
kubectl port-forward svc/shop-api 8080:80
# then, in another terminal:
curl localhost:8080/healthz

Discussion

  • Be the first to comment on this lesson.