Service Wiring Basics

Introduction: The Networking Challenge in Kubernetes

When you deploy applications in Kubernetes, you quickly run into a fundamental problem: Pods are temporary. They can be created, destroyed, or rescheduled at any moment, and each time a Pod is created, it gets a new IP address. If your application needs to communicate with other Pods, how can it keep track of constantly changing IP addresses? The answer is Services. A Service provides a stable endpoint — a consistent way to reach a group of Pods, even as those Pods come and go. In this lesson, we'll explore the core mechanism that makes Services work: labels and selectors. This is the foundation that powers all Service types in Kubernetes.

Labels and Selectors

You've already learned about labels — those key-value pairs you attach to Kubernetes objects like Pods. As a quick reminder, labels are tags that describe what a Pod is or what it does (like app: web or tier: frontend). They appear in your Deployment YAML under the Pod template's metadata section, and when a Deployment creates Pods, it stamps each one with the labels you've defined.

Now we'll see how labels become the foundation of Kubernetes networking through selectors. A selector is a set of matching criteria that a Service uses to find Pods. When you create a Service, you specify a selector that describes which labels you're looking for. The Service then continuously queries Kubernetes for all Pods that have matching labels, and it routes traffic to those Pods.

Here's the critical insight: Services are NOT bound to specific Pods. They don't remember individual Pod names or IP addresses. Instead, they dynamically select any Pod that matches their selector criteria. This means if a Pod dies and a new one is created with the same labels, the Service automatically starts routing traffic to the new Pod. If you scale your Deployment up or down, the Service adapts immediately.

The matching process is straightforward. A Service's selector specifies one or more key-value pairs, and a Pod must have ALL of those labels to be selected. For example, if a Service selector says app: wired-demo and tier: web, then only Pods with both of those exact labels will receive traffic from that Service.

Let's see how selectors appear in a Service definition:

apiVersion: v1
kind: Service
metadata:
  name: web-wired
  labels:
    app: wired-demo
spec:
  type: ClusterIP
  selector:
    app: wired-demo
    tier: web
  ports:
    - name: http
      port: 80
      targetPort: 80

The selector section is where the magic happens. This Service is looking for Pods with the labels app: wired-demo AND tier: web. Any Pod that has both of these labels will be included in the Service's routing pool. The Service doesn't care about the Pod's name, its IP address, or when it was created — it only cares about the labels.

Creating a Properly Wired Deployment and Service

Let's put labels and selectors together by creating a complete example. We'll build a simple web application Deployment and a Service that routes traffic to it. The key is ensuring that the labels on the Pods match the selector in the Service — this is what we call "wiring" them together correctly.

First, let's create our Deployment. Save this as deployment-web.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-deployment-wired
  labels:
    app: wired-demo
    tier: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: wired-demo
      tier: web
  template:
    metadata:
      labels:
        app: wired-demo
        tier: web
    spec:
      containers:
        - name: nginx
          image: nginx:1.25
          ports:
            - containerPort: 80

Let's break down the important parts. The metadata.labels at the top are labels for the Deployment object itself — these are optional and mainly for organization. The crucial labels are under template.metadata.labels. These are the labels that will be stamped on every Pod created by this Deployment. We're using app: wired-demo to identify this as part of our demo application, and tier: web to indicate it's the web tier.

The spec.selector.matchLabels section tells the Deployment which Pods it should manage. This must match the labels in the Pod template; otherwise, the Deployment won't be able to track its Pods. The replicas: 2 means we want two copies of this Pod running. Finally, the container spec defines a simple nginx web server listening on port 80.

Now let's create the Service that will route traffic to these Pods. Save this as service-web.yaml:

apiVersion: v1
kind: Service
metadata:
  name: web-wired
  labels:
    app: wired-demo
spec:
  type: ClusterIP
  selector:
    app: wired-demo
    tier: web
  ports:
    - name: http
      port: 80
      targetPort: 80

The Service's selector section is the critical piece. It specifies app: wired-demo and tier: web, which exactly match the labels on our Pods. This means the Service will find and route traffic to any Pod with both of these labels. The type: ClusterIP creates an internal-only Service (we'll explore other types in future lessons). The ports section says the Service will listen on port 80 and forward traffic to port 80 on the selected Pods.

Let's apply both of these files on CodeSignal (remember, on your own machine you'd need to have kubectl configured, but CodeSignal has everything pre-installed):

kubectl apply -f deployment-web.yaml
kubectl apply -f service-web.yaml

You should see output like this:

deployment.apps/web-deployment-wired created
service/web-wired created

This confirms that both resources were created successfully. Now let's verify that our wiring is correct.

Verifying the Wiring with Endpoints

Creating a Service and a Deployment isn't enough — we need to verify that they're actually connected. Kubernetes provides a resource called Endpoints that shows us exactly which Pod IP addresses a Service is routing traffic to. Think of endpoints as the "phone book" that the Service uses — it's the list of actual destinations where traffic will be sent.

When a Service is properly wired to Pods, Kubernetes automatically creates an Endpoints object with the same name as the Service. This object contains the IP addresses and ports of all Pods that match the Service's selector. If the endpoints list is empty, it means the Service couldn't find any matching Pods — usually because the labels don't match.

Let's check our Pods first to see what labels they have:

kubectl get pods -l app=wired-demo,tier=web --show-labels

The -l flag filters Pods by labels, and --show-labels displays all labels on each Pod. You should see output like this:

NAME                                    READY   STATUS    RESTARTS   AGE   LABELS
web-deployment-wired-7d4f8c9b5d-abc12   1/1     Running   0          30s   app=wired-demo,tier=web,pod-template-hash=7d4f8c9b5d
web-deployment-wired-7d4f8c9b5d-xyz34   1/1     Running   0          30s   app=wired-demo,tier=web,pod-template-hash=7d4f8c9b5d

Perfect! Both Pods have the labels app=wired-demo and tier=web, which match our Service's selector. Now let's check the Service itself:

kubectl get service web-wired

This shows basic information about the Service:

NAME        TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
web-wired   ClusterIP   10.96.123.45    <none>        80/TCP    45s

The Service has been assigned a stable Cluster IP (10.96.123.45 in this example — yours will be different). This IP won't change even if all the Pods are replaced. Now let's look at the endpoints:

kubectl get endpoints web-wired

You should see output like this:

NAME        ENDPOINTS                           AGE
web-wired   10.244.0.5:80,10.244.0.6:80         50s

Excellent! The endpoints show two IP addresses (10.244.0.5 and 10.244.0.6), which are the actual IPs of our two Pods. This confirms that the Service has successfully found the Pods and is ready to route traffic to them. If you saw <none> under ENDPOINTS, it would mean the Service couldn't find any matching Pods.

For a more detailed view, use the describe command:

kubectl describe service web-wired

This gives you a comprehensive view of the Service configuration:

Name:              web-wired
Namespace:         default
Labels:            app=wired-demo
Annotations:       <none>
Selector:          app=wired-demo,tier=web
Type:              ClusterIP
IP Family Policy:  SingleStack
IP Families:       IPv4
IP:                10.96.123.45
IPs:               10.96.123.45
Port:              http  80/TCP
TargetPort:        80/TCP
Endpoints:         10.244.0.5:80,10.244.0.6:80
Session Affinity:  None
Events:            <none>

Pay special attention to the Selector line — it shows exactly which labels the Service is looking for. The Endpoints line confirms which Pod IPs are currently selected. This is your proof that the wiring is correct.

Dynamic Behavior: Services Adapt to Changes

Now let's see the real power of label-based selection. Because Services use selectors rather than hardcoded Pod references, they automatically adapt when Pods change. Let's demonstrate this by scaling our Deployment up from 2 replicas to 4:

kubectl scale deployment web-deployment-wired --replicas=4

You should see:

deployment.apps/web-deployment-wired scaled

The Deployment will immediately start creating two more Pods. Here's the crucial part: we don't need to update the Service at all. The Service's selector is still looking for Pods with app: wired-demo and tier: web, and the new Pods will have those exact labels because they're created from the same Deployment template.

Let's watch the endpoints update in real time:

kubectl get endpoints web-wired -w

The -w flag means "watch" — it will continuously display updates. You'll see something like this:

NAME        ENDPOINTS                                               AGE
web-wired   10.244.0.5:80,10.244.0.6:80                             2m
web-wired   10.244.0.5:80,10.244.0.6:80,10.244.0.7:80               2m5s
web-wired   10.244.0.5:80,10.244.0.6:80,10.244.0.7:80,10.244.0.8:80 2m8s

Watch how the endpoints list grows as new Pods come online! First, you see the original two IPs, then a third appears, then a fourth. The Service automatically discovers the new Pods and adds them to its routing pool. Press Ctrl+C to stop watching.

This dynamic behavior is what makes Kubernetes so powerful for running applications at scale. If a Pod crashes, the Deployment creates a new one with the same labels, and the Service automatically routes traffic to it. If you need more capacity, you scale up the Deployment, and the Service immediately starts using the new Pods. If you need less capacity, you scale down, and the Service stops routing to the terminated Pods. All of this happens automatically because of the label-selector relationship.

Let's verify the final state:

kubectl get pods -l app=wired-demo,tier=web

You should now see four Pods:

NAME                                    READY   STATUS    RESTARTS   AGE
web-deployment-wired-7d4f8c9b5d-abc12   1/1     Running   0          5m
web-deployment-wired-7d4f8c9b5d-xyz34   1/1     Running   0          5m
web-deployment-wired-7d4f8c9b5d-def56   1/1     Running   0          2m
web-deployment-wired-7d4f8c9b5d-ghi78   1/1     Running   0          2m

And the endpoints should show all four:

kubectl get endpoints web-wired
NAME        ENDPOINTS                                               AGE
web-wired   10.244.0.5:80,10.244.0.6:80,10.244.0.7:80,10.244.0.8:80 5m

This is the foundation of how all Kubernetes Services work, regardless of their type. Whether you're using ClusterIP, NodePort, or LoadBalancer Services (which we'll cover in future lessons), they all rely on this same label-selector mechanism to find and route traffic to Pods.

Summary and Practice Preparation

You've now learned the fundamental mechanism that powers Kubernetes networking: Services use selectors to dynamically find Pods based on labels. This label-selector relationship is not a one-time binding — it's a continuous query that adapts as Pods are created, destroyed, or scaled. When you create a Deployment, you define labels on the Pod template. When you create a Service, you define a selector that matches those labels. Kubernetes handles the rest, automatically maintaining the connection through the Endpoints resource.

In the upcoming practice exercises, you'll create your own Deployments and Services, verify their connections using endpoints, and observe how Services adapt when you scale Deployments. This hands-on experience will solidify your understanding of Service wiring. Remember: this foundation applies to all Service types we'll explore in future lessons — the only difference will be how traffic reaches the Service from outside the cluster.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal