Kubernetes Basics: Pod, Deployment, and Service

1. Overview

Kubernetes has dozens of resource types, but 80% of daily work revolves around three concepts: Pod, Deployment, Service. This article helps you understand the nature of each resource, how they connect with one another, and write a set of YAML manifests to deploy a real application running on a cluster.

After reading, you’ll be able to deploy a Spring Boot application to Kubernetes yourself, scale it to multiple replicas, and expose it via Service - all with just kubectl and a few YAML files.

Note: This article assumes you already have a cluster (Minikube, Kind, or cloud) and kubectl is pointing to the correct context.

2. Pod - The Smallest Deployment Unit

2.1. What is a Pod

A Pod is a group of one or more containers that share a network namespace (same IP, same port space) and volumes. In practice, most Pods contain only one application container.

apiVersion: v1
kind: Pod
metadata:
  name: order-service-pod
  labels:
    app: order-service
spec:
  containers:
    - name: order-service
      image: nonladev/order-service:1.0
      ports:
        - containerPort: 8080
      resources:
        requests:
          cpu: "250m"
          memory: "256Mi"
        limits:
          cpu: "500m"
          memory: "512Mi"

Explanation:

  • metadata.labels assigns the label app: order-service - this is how Kubernetes “identifies” Pods; Service and Deployment use this label to select related objects.
  • resources.requests is the minimum resources the scheduler needs to place the Pod on a node; limits is the ceiling, exceeding which the Pod will be throttled (CPU) or killed (memory - OOMKilled).
  • containerPort: 8080 is merely informational; it doesn’t automatically expose the port outside.

Apply and verify:

kubectl apply -f order-service-pod.yaml
kubectl get pods
NAME                 READY   STATUS    RESTARTS   AGE
order-service-pod    1/1     Running   0          5s

2.2. Why You Shouldn’t Create Pods Directly

A Pod created “bare” like above has a major problem: if the Node hosting the Pod dies, or the Pod crashes, no one automatically recreates it. Pods have no self-healing or scaling mechanism.

Note: In a production environment, you almost never create Pods directly. Always use Deployment (or StatefulSet, Job) to manage Pods.

3. Deployment - Managing Pod Lifecycle

3.1. Declaring a Deployment

Deployment creates and manages a set of Pods through ReplicaSet, ensuring the correct number of replicas are always running, and supports zero-downtime rolling updates.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
        - name: order-service
          image: nonladev/order-service:1.0
          ports:
            - containerPort: 8080
          readinessProbe:
            httpGet:
              path: /actuator/health/readiness
              port: 8080
            initialDelaySeconds: 10
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /actuator/health/liveness
              port: 8080
            initialDelaySeconds: 15
            periodSeconds: 10

Explanation:

  • replicas: 3 requires Deployment to always maintain 3 Pods running simultaneously - if a Pod dies, Deployment automatically creates a new Pod to replace it.
  • selector.matchLabels must match template.metadata.labels. This is the required link between Deployment and the Pods it manages.
  • readinessProbe determines whether the Pod receives traffic (used by Service); livenessProbe determines whether the Pod needs to restart. Confusing these two probes is a very common mistake when learning Kubernetes.

Apply and observe:

kubectl apply -f order-service-deployment.yaml
kubectl get deployments
kubectl get pods -l app=order-service
NAME            READY   UP-TO-DATE   AVAILABLE   AGE
order-service   3/3     3            3           12s

NAME                             READY   STATUS    RESTARTS   AGE
order-service-7d4f9c8b6d-2kx9p   1/1     Running   0          12s
order-service-7d4f9c8b6d-7h2mn   1/1     Running   0          12s
order-service-7d4f9c8b6d-xq1lw   1/1     Running   0          12s

3.2. Scale and Rolling Update

Scale up/down replicas without editing YAML:

kubectl scale deployment order-service --replicas=5

Rolling update when changing image - Deployment automatically replaces old Pods with new ones using a rolling strategy, with no downtime:

kubectl set image deployment/order-service order-service=nonladev/order-service:1.1
kubectl rollout status deployment/order-service
Waiting for deployment "order-service" rollout to finish: 2 out of 3 new replicas have been updated...
deployment "order-service" successfully rolled out

If the new version has a bug, roll back to the previous version immediately:

kubectl rollout undo deployment/order-service

3.3. Configuring Rolling Update Strategy

spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 1
      maxSurge: 1

maxUnavailable: 1 allows at most 1 Pod to be “unavailable” during update; maxSurge: 1 allows creating at most 1 extra Pod beyond the declared replicas. With replicas: 3, the cluster will have 2-4 Pods running during the transition, but never fewer than 2.

4. Service - Stable Pod Exposure

4.1. Why You Need Service

Pods have their own IP, but this IP changes every time the Pod is recreated (crash, rolling update, scale). Service provides a stable address (ClusterIP + DNS name) in front of the Pod group, automatically load-balancing traffic based on label selector - regardless of how many times the underlying Pods change.

apiVersion: v1
kind: Service
metadata:
  name: order-service
spec:
  selector:
    app: order-service
  ports:
    - port: 80
      targetPort: 8080
  type: ClusterIP

Explanation:

  • selector.app: order-service matches the label on the Pod (set by Deployment in template.metadata.labels) - Service automatically finds and tracks these Pods via an intermediate object called Endpoints.
  • port: 80 is the port Service listens on within the cluster; targetPort: 8080 is the port the container actually exposes. These two numbers don’t have to be the same.
  • type: ClusterIP (default) is only accessible from within the cluster - suitable for internal services between microservices.

Verify Service and Endpoints:

kubectl apply -f order-service-service.yaml
kubectl get svc order-service
kubectl get endpoints order-service
NAME            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
order-service   ClusterIP   10.96.142.201   <none>        80/TCP    4s

NAME            ENDPOINTS                                   AGE
order-service   10.244.1.5:8080,10.244.1.6:8080,+1 more     4s

4.2. Service Types

Type Access Scope When to Use
ClusterIP Within cluster only Communication between internal microservices
NodePort Exposes fixed port (30000-32767) on all Nodes Quick testing, dev/on-prem environment
LoadBalancer Allocates public IP via cloud provider Expose service to Internet on cloud

Example to expose externally for quick testing on Minikube:

apiVersion: v1
kind: Service
metadata:
  name: order-service-external
spec:
  selector:
    app: order-service
  ports:
    - port: 80
      targetPort: 8080
      nodePort: 30080
  type: NodePort
minikube service order-service-external --url

Note: Using NodePort for production is an anti-pattern - limited port range, no TLS termination or health-based routing like Ingress. For production on cloud, prefer LoadBalancer or Ingress Controller.

5. Combining All Three in a Real Application

The complete pipeline: client calls into Service → Service load-balances to Pods managed by Deployment.

Client
  │
  ▼
Service (order-service, ClusterIP 10.96.142.201:80)
  │  (label selector: app=order-service)
  ▼
┌─────────────┬─────────────┬─────────────┐
│  Pod #1     │  Pod #2     │  Pod #3     │
│  :8080      │  :8080      │  :8080      │
└─────────────┴─────────────┴─────────────┘
       ▲ managed by
  Deployment (order-service, replicas: 3)

For convenience, you can combine Deployment and Service into one file, separated by ---:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
        - name: order-service
          image: nonladev/order-service:1.0
          ports:
            - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: order-service
spec:
  selector:
    app: order-service
  ports:
    - port: 80
      targetPort: 8080
kubectl apply -f order-service-all.yaml

6. Write Tests for Readiness Endpoint Before Deploying

Before Kubernetes probes rely on /actuator/health/readiness, you should ensure this endpoint returns the correct status in your Spring Boot code.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
management.endpoint.health.probes.enabled=true
management.health.readiness-state.enabled=true
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderServiceHealthTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void readinessProbe_shouldReturnUp() {
        ResponseEntity<String> response =
                restTemplate.getForEntity("/actuator/health/readiness", String.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).contains("\"status\":\"UP\"");
    }
}

This test calls TestRestTemplate on the actual readiness endpoint, confirming HTTP 200 and body contains UP. This is precisely the endpoint that readinessProbe in Deployment will call periodically - if the test fails here, the Pod will never be “Ready” on the cluster either.

7. Common Mistakes

  • Selector doesn’t match label: Service or Deployment can’t find any Pod (empty Endpoints) because selector and labels are misspelled or mismatched. Always run kubectl get endpoints <svc-name> to verify.
  • Missing readinessProbe: Service still routes traffic to Pods that haven’t finished starting up, causing 502/503 errors right after deployment.
  • Missing resource requests/limits: Pods might be scheduled on the wrong node by the scheduler, or be OOMKilled unexpectedly when traffic increases.
  • Using bare Pods for production: no self-healing, lost Pod is lost forever, must recreate manually.
  • Image tag latest: rolling update doesn’t detect changes, Kubernetes thinks the image hasn’t changed and won’t repull. Always use a specific version tag.

8. Conclusion

Pod is the smallest unit for running containers, Deployment manages the lifecycle and scaling of a set of Pods, and Service provides a stable address to access them even as the underlying Pods change constantly. Mastering these three building blocks is the foundation for you to progress to Ingress, ConfigMap/Secret, or Helm charts later. For your next step, try: deploy a real Spring Boot application, observe the rolling update, and try killing a Pod by hand to see how Deployment recovers.

kubernetesdevopscontainerorchestration