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
kubectlis 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.labelsassigns the labelapp: order-service- this is how Kubernetes “identifies” Pods; Service and Deployment use this label to select related objects.resources.requestsis the minimum resources the scheduler needs to place the Pod on a node;limitsis the ceiling, exceeding which the Pod will be throttled (CPU) or killed (memory - OOMKilled).containerPort: 8080is 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: 3requires Deployment to always maintain 3 Pods running simultaneously - if a Pod dies, Deployment automatically creates a new Pod to replace it.selector.matchLabelsmust matchtemplate.metadata.labels. This is the required link between Deployment and the Pods it manages.readinessProbedetermines whether the Pod receives traffic (used by Service);livenessProbedetermines 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-servicematches the label on the Pod (set by Deployment intemplate.metadata.labels) - Service automatically finds and tracks these Pods via an intermediate object called Endpoints.port: 80is the port Service listens on within the cluster;targetPort: 8080is 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
NodePortfor production is an anti-pattern - limited port range, no TLS termination or health-based routing like Ingress. For production on cloud, preferLoadBalanceror 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
selectorandlabelsare misspelled or mismatched. Always runkubectl 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.