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.
kubectl apply -f deployment.yaml -f service.yaml -f ingress.yamlA 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
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 :8080When 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.
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: urlService 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.
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: 80Applying 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.
# 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