Pods, Deployments and Services

Pods, Deployments and Services: the three objects that put a .NET API on a cluster, and the loop that keeps them matching.

Syntaxkubectl apply -f deployment.yaml # Deployment -> ReplicaSet -> Pods

Kubernetes runs containers across a group of machines. You do not start a container yourself. You write down what you want, and Kubernetes keeps reality matching what you wrote. That compare-and-correct loop is called the control loop, and it never stops.

Pods

A pod is the smallest thing Kubernetes runs. It is one or more containers that share one IP address. A .NET API is normally one container in one pod. Pods are disposable. Every replacement pod gets a new name and a new IP, so nothing may depend on either.

Deployments

A Deployment is the object you actually write. It names the image, says how many copies (replicas) to keep, and describes the pod to create. Kubernetes turns it into a ReplicaSet, and the ReplicaSet creates the pods. Delete a pod and a new one appears, because the count no longer matches. kubectl create deployment orders --image=registry.example.com/orders:v1 is shorthand for a simple Deployment; kubectl get deployment orders -o yaml prints the long form it wrote.

Services

Because pod IPs change, callers need a fixed address. A Service is that address. It is a name in cluster DNS, orders inside the same namespace or orders.default.svc.cluster.local from anywhere, and it spreads connections over every pod whose labels match its selector.

What trips people up

A Service finds pods by labels, never by name. containerPort only documents a port; it opens nothing. The .NET container images listen on 8080 from .NET 8 onwards, where it was 80 before, so targetPort is 8080.

Example

Example · csharp
// The whole API that the Deployment and Service below run.
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// One resource, so there is something to curl through the Service.
app.MapGet("/orders/1", () => new { id = 1, total = 42 });

// A probe will call this in the next lesson. For now it proves
// the process is listening.
app.MapGet("/healthz", () => Results.Ok("Healthy"));

// The .NET container images set the listening port to 8080, so the
// Service forwards to 8080 without any extra configuration here.
app.Run();

When to use it

  • Put an existing containerised .NET API on a cluster for the first time.
  • Give another team a stable in-cluster address for your service instead of a pod IP.
  • Work out why requests to a Service time out when the pods are clearly running.

More examples

The Deployment

The three replicas are identical and interchangeable. Reach for this file whenever you add a .NET service to a cluster: it is the smallest Deployment that is still correct. Note that the selector is fixed once the Deployment exists; changing it later is rejected, so pick the label now.

Example · yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: orders
spec:
  replicas: 3
  selector:
    matchLabels:
      app: orders          # must match the pod labels below
  template:
    metadata:
      labels:
        app: orders        # the label the Service will look for
    spec:
      containers:
        - name: api
          image: registry.example.com/orders:v1
          ports:
            - containerPort: 8080   # documentation only
          env:
            - name: ASPNETCORE_ENVIRONMENT
              value: Production

The Service in front of it

Another service in the cluster now calls http://orders/orders/1 and does not care which pod answers. ClusterIP is right for an API that sits behind an ingress; a Service that must be reached from outside the cluster is covered in the Ingress lesson.

Example · yaml
apiVersion: v1
kind: Service
metadata:
  name: orders
spec:
  type: ClusterIP          # the default: reachable inside the cluster only
  selector:
    app: orders            # every pod with this label gets traffic
  ports:
    - name: http
      port: 80             # the port the Service answers on
      targetPort: 8080     # the port in the container

Watching the control loop work

The endpointslices check is the first thing to run when requests to a Service fail: an empty list means the selector matches no pods. Deleting a pod by hand is the cheapest way to see that the Deployment, not you, owns how many pods exist.

Example · bash
kubectl apply -f deployment.yaml -f service.yaml

kubectl get pods -l app=orders
# NAME                      READY   STATUS    RESTARTS   AGE
# orders-6f8c9d7b54-2j4kq   1/1     Running   0          21s

# Does the Service actually point at anything?
kubectl get endpointslices -l kubernetes.io/service-name=orders

# Delete a pod and the ReplicaSet replaces it.
kubectl delete pod orders-6f8c9d7b54-2j4kq
kubectl get pods -l app=orders

Discussion

  • Be the first to comment on this lesson.