The Nautilus DevOps team has already deployed a ReplicaSet to host an application that requires a highly available infrastructure. Your task is to expose the application running in the existing ReplicaSet by creating a Kubernetes NodePort Service.
Follow the specifications below to create the Service and ensure the application pods are accessible:
- A ReplicaSet named
httpd-replicasetis already running in the cluster. - The pods managed by the ReplicaSet use the following labels:
Assign labels
appashttpd_app, andtypeasfront-end. - Create a NodePort Service named
httpd-serviceto expose the application. - Set the NodePort to
30080. - Expose port
80of the application. Note: Do not delete or modify the configuration of the deployedReplicaSetapplication.
Introduction
You've deployed your application in Kubernetes, but how do users access it? This is one of the most common questions beginners face when working with Kubernetes. In this comprehensive guide, we'll walk through exposing an application using a NodePort Service—one of the simplest ways to make your application accessible from outside the cluster.
We'll use a real-world scenario from the Nautilus DevOps team: they have an application running in a ReplicaSet, and they need to expose it to users. By the end of this guide, you'll understand how to create and configure NodePort Services like a pro!
What You'll Learn
- What NodePort Services are and when to use them
- How to expose applications using NodePort
- How services work with ReplicaSets and pods
- How to verify your service is working correctly
- Best practices for production environments
Table of Contents
- Understanding NodePort Services
- The Scenario: Exposing an Application
- Step 1: Verify the Existing Application
- Step 2: Create the NodePort Service
- Step 3: Verify the Service
- Step 4: Test Application Access
- Understanding How It Works
- Common Issues and Solutions
- Advanced Configuration
- Best Practices
- Conclusion
Understanding NodePort Services
What is a NodePort Service?
A NodePort Service is a type of Kubernetes Service that exposes your application on a static port on each node's IP address. Think of it as a door into your cluster:
┌─────────────────────────────────────────────────────────────┐
│ Kubernetes Cluster │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Node 1 (10.244.164.51) │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ NodePort: 30080 │ │ │
│ │ │ ┌─────────────────────────────────────┐ │ │ │
│ │ │ │ Pod 1 (10.22.0.9:80) │ │ │ │
│ │ │ └─────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Node 2 (10.244.164.52) │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ NodePort: 30080 │ │ │
│ │ │ ┌─────────────────────────────────────┐ │ │ │
│ │ │ │ Pod 2 (10.22.0.10:80) │ │ │ │
│ │ │ └─────────────────────────────────────┘ │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ External Traffic: http://10.244.164.51:30080 │
└─────────────────────────────────────────────────────────────┘
Service Types Comparison
| Type | Description | When to Use |
|---|---|---|
| ClusterIP | Internal only, accessible within the cluster | Internal services, microservices |
| NodePort | External access on each node's IP | Development, testing, simple production |
| LoadBalancer | Cloud provider load balancer | Production, public applications |
| ExternalName | DNS alias to external services | Integrating external services |
NodePort Range
Valid NodePort Range: 30000 - 32767
Our Port: 30080 ✅ (Within valid range)
The Scenario: Exposing an Application
The Challenge
The Nautilus DevOps team has already deployed an application using a ReplicaSet. Now they need to expose it to users. Here are the requirements:
Existing Resources:
- ✅ ReplicaSet:
httpd-replicaset - ✅ Pods: 3 running pods
- ✅ Labels:
app=httpd_app,type=front-end
Task:
- Create a NodePort Service named
httpd-service - Set NodePort to
30080 - Expose port
80of the application
Current State
┌─────────────────────────────────────────────────────────────┐
│ Current State │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ReplicaSet: httpd-replicaset │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ Pod 1 (Running) │ │ │
│ │ │ Labels: app=httpd_app,type=front-end │ │ │
│ │ │ Port: 80 │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ Pod 2 (Running) │ │ │
│ │ │ Labels: app=httpd_app,type=front-end │ │ │
│ │ │ Port: 80 │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ │ ┌─────────────────────────────────────────────┐ │ │
│ │ │ Pod 3 (Running) │ │ │
│ │ │ Labels: app=httpd_app,type=front-end │ │ │
│ │ │ Port: 80 │ │ │
│ │ └─────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────┘ │
│ ❌ No Service → No external access │
└─────────────────────────────────────────────────────────────┘
Desired State
┌─────────────────────────────────────────────────────────────┐
│ Desired State │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ReplicaSet: httpd-replicaset │ │
│ │ 3 Pods with labels: app=httpd_app,type=front-end │ │
│ └────────────────────┬────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Service: httpd-service │ │
│ │ Type: NodePort │ │
│ │ NodePort: 30080 │ │
│ │ Selector: app=httpd_app,type=front-end │ │
│ └────────────────────┬────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ✅ External Access: http://<node-ip>:30080 │
└─────────────────────────────────────────────────────────────┘
Step 1: Verify the Existing Application
Before creating the service, let's verify the existing resources.
Check the ReplicaSet
kubectl get replicasets
Output:
NAME DESIRED CURRENT READY AGE
httpd-replicaset 3 3 3 108s
What This Tells Us:
- DESIRED: 3 - We want 3 pods
- CURRENT: 3 - 3 pods are running
- READY: 3 - All 3 pods are ready to serve traffic
Check the Pods and Their Labels
kubectl get pods --show-labels
Output:
NAME READY STATUS RESTARTS AGE LABELS
httpd-replicaset-5jbg6 1/1 Running 0 108s app=httpd_app,type=front-end
httpd-replicaset-5zttg 1/1 Running 0 108s app=httpd_app,type=front-end
httpd-replicaset-jpn7g 1/1 Running 0 108s app=httpd_app,type=front-end
What This Tells Us:
- All pods are running (
READY: 1/1) - Labels are correctly set:
app=httpd_app,type=front-end - These labels will be used to select the pods
Get ReplicaSet Details
kubectl describe replicaset httpd-replicaset
Key Output:
Name: httpd-replicaset
Selector: app=httpd_app,type=front-end
Replicas: 3 current / 3 desired
Pods Status: 3 Running / 0 Waiting / 0 Succeeded / 0 Failed
Containers:
httpd-container:
Image: httpd:latest
Port: 80/TCP
What This Tells Us:
- The ReplicaSet uses selector labels
- The container runs HTTPD on port 80
- All pods are healthy
Step 2: Create the NodePort Service
Now let's create the NodePort Service to expose the application.
Option A: Using YAML (Recommended)
Create a file or use inline YAML:
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Service
metadata:
name: httpd-service
spec:
type: NodePort
ports:
- port: 80
targetPort: 80
nodePort: 30080
protocol: TCP
selector:
app: httpd_app
type: front-end
EOF
Expected Output:
service/httpd-service created
Understanding the YAML:
apiVersion: v1 # Kubernetes API version
kind: Service # Resource type
metadata:
name: httpd-service # Service name
spec:
type: NodePort # Service type
ports:
- port: 80 # Service port
targetPort: 80 # Pod port
nodePort: 30080 # Node port (external)
protocol: TCP # Protocol
selector: # Pod selector
app: httpd_app # Match label
type: front-end # Match label
Option B: Using kubectl expose (Quick Command)
kubectl expose replicaset httpd-replicaset \
--name=httpd-service \
--type=NodePort \
--port=80 \
--target-port=80 \
--node-port=30080
Step 3: Verify the Service
Check Service Status
kubectl get services
Output:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
httpd-service NodePort 10.43.118.61 <none> 80:30080/TCP 11s
kubernetes ClusterIP 10.43.0.1 <none> 443/TCP 26m
What This Tells Us:
- TYPE: NodePort - It's a NodePort service
- PORT(S): 80:30080/TCP - Port 80 maps to NodePort 30080
- AGE: 11s - Just created!
Get Service Details
kubectl describe service httpd-service
Output:
Name: httpd-service
Namespace: default
Labels: <none>
Annotations: <none>
Selector: app=httpd_app,type=front-end
Type: NodePort
IP: 10.43.118.61
Port: <unset> 80/TCP
TargetPort: 80/TCP
NodePort: <unset> 30080/TCP
Endpoints: 10.22.0.10:80,10.22.0.9:80,10.22.0.11:80
Session Affinity: None
External Traffic Policy: Cluster
Events: <none>
What This Tells Us:
- Selector: Matches our pod labels
- Endpoints: 3 pods are available
- NodePort: 30080 is exposed
Check Endpoints
kubectl get endpoints httpd-service
Output:
NAME ENDPOINTS AGE
httpd-service 10.22.0.10:80,10.22.0.11:80,10.22.0.9:80 29s
What This Tells Us:
- The service has found 3 pods (endpoints)
- All pods are on port 80
- Traffic will be load-balanced between these pods
Step 4: Test Application Access
Get Node IP
NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
echo "Node IP: $NODE_IP"
Output:
Node IP: 10.244.164.51
Test with curl
curl http://$NODE_IP:30080
Output (HTML from Apache HTTPD):
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>It works! Apache httpd</title>
</head>
<body>
<p>It works!</p>
</body>
</html>
Success! 🎉 The application is accessible!
Understanding How It Works
Service-to-Pod Communication Flow
┌─────────────────────────────────────────────────────────────┐
│ External User │
│ http://10.244.164.51:30080 │
└──────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ Node (10.244.164.51) │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ NodePort: 30080 │ │
│ │ Receives request on port 30080 │ │
│ └────────────────────┬────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Service: httpd-service │ │
│ │ 1. Receives request on port 80 │ │
│ │ 2. Selects pods with matching labels │ │
│ │ 3. Load balances to available pods │ │
│ └────────────────────┬────────────────────────────────┘ │
│ │ │
│ ┌─────────────┼─────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Pod 1 │ │ Pod 2 │ │ Pod 3 │ │
│ │ 10.22. │ │ 10.22. │ │ 10.22. │ │
│ │ 0.9:80 │ │ 0.10:80 │ │ 0.11:80 │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────────┘
Service Selector Matching
Service Selector:
selector:
app: httpd_app
type: front-end
Pod Labels:
labels:
app: httpd_app ✅ Matches!
type: front-end ✅ Matches!
Result: The service finds all 3 pods!
Load Balancing
NodePort Services use round-robin load balancing:
Request 1 → Pod 1 (10.22.0.9:80)
Request 2 → Pod 2 (10.22.0.10:80)
Request 3 → Pod 3 (10.22.0.11:80)
Request 4 → Pod 1 (10.22.0.9:80)
...
Common Issues and Solutions
Issue 1: No Endpoints
Symptom:
Endpoints: <none>
Solutions:
# 1. Check pod labels
kubectl get pods --show-labels | grep httpd-replicaset
# 2. Check service selector
kubectl get service httpd-service -o jsonpath='{.spec.selector}'
# 3. Ensure labels match
# Service selector: app=httpd_app,type=front-end
# Pod labels must match exactly
Issue 2: NodePort Already in Use
Error:
Error: node port 30080 is already in use
Solution:
# 1. Check what's using the port
kubectl get services --all-namespaces | grep 30080
# 2. Use a different port
kubectl patch service httpd-service -p '{"spec":{"ports":[{"nodePort":30081,"port":80,"targetPort":80}]}}'
Issue 3: Connection Refused
Symptom:
curl: (7) Failed to connect to 10.244.164.51 port 30080: Connection refused
Solutions:
# 1. Check service status
kubectl get service httpd-service
# 2. Check endpoints
kubectl get endpoints httpd-service
# 3. Check node IP
kubectl get nodes -o wide
# 4. Check if pods are running
kubectl get pods
Issue 4: Service Not Created
Error:
error: service httpd-service already exists
Solution:
# 1. Check existing service
kubectl get service httpd-service
# 2. Delete and recreate
kubectl delete service httpd-service
kubectl create -f httpd-service.yaml
Advanced Configuration
1. Multiple Ports
apiVersion: v1
kind: Service
metadata:
name: httpd-service
spec:
type: NodePort
ports:
- name: http
port: 80
targetPort: 80
nodePort: 30080
protocol: TCP
- name: https
port: 443
targetPort: 443
nodePort: 30443
protocol: TCP
selector:
app: httpd_app
type: front-end
2. Session Affinity
apiVersion: v1
kind: Service
metadata:
name: httpd-service
spec:
type: NodePort
sessionAffinity: ClientIP
sessionAffinityConfig:
clientIP:
timeoutSeconds: 10800
ports:
- port: 80
targetPort: 80
nodePort: 30080
selector:
app: httpd_app
type: front-end
3. Preserve Client IP
apiVersion: v1
kind: Service
metadata:
name: httpd-service
spec:
type: NodePort
externalTrafficPolicy: Local
ports:
- port: 80
targetPort: 80
nodePort: 30080
selector:
app: httpd_app
type: front-end
4. Service with Annotations
apiVersion: v1
kind: Service
metadata:
name: httpd-service
annotations:
description: "HTTPD application service"
owner: "devops-team"
environment: "production"
spec:
type: NodePort
ports:
- port: 80
targetPort: 80
nodePort: 30080
selector:
app: httpd_app
type: front-end
Best Practices
✅ DO's
1. Use Specific Labels
selector:
app: httpd_app # ✅ Good
type: front-end # ✅ Good
2. Use Annotations for Documentation
metadata:
annotations:
description: "HTTPD NodePort service for frontend"
owner: "devops-team"
3. Monitor Endpoints
kubectl get endpoints httpd-service -w
4. Test Locally First
kubectl port-forward service/httpd-service 8080:80
5. Use Namespaces
metadata:
namespace: production
6. Set Appropriate NodePort
nodePort: 30080 # Within valid range
7. Document Service Purpose
metadata:
labels:
purpose: exposing-web-app
❌ DON'Ts
1. Don't Use Invalid NodePorts
# BAD - Below valid range
nodePort: 80
# BAD - Above valid range
nodePort: 40000
# GOOD - Within valid range
nodePort: 30080
2. Don't Use Conflicting Selectors
# BAD - No pods match
selector:
app: wrong_app
# GOOD - Correct labels
selector:
app: httpd_app
3. Don't Forget TargetPort
# BAD - Missing targetPort
ports:
- port: 80
# GOOD - With targetPort
ports:
- port: 80
targetPort: 80
4. Don't Expose Internally Without Need
# Only expose externally if needed
# Use ClusterIP for internal services
Quick Reference Commands
Service Commands
| Command | Description |
|---|---|
kubectl get services |
List all services |
kubectl get svc |
Short form |
kubectl describe service NAME |
Service details |
kubectl get endpoints NAME |
Check endpoints |
kubectl get service NAME -o yaml |
Service YAML |
Creation Commands
| Command | Description |
|---|---|
kubectl expose replicaset NAME --type=NodePort --port=80 --node-port=30080 |
Expose ReplicaSet |
kubectl apply -f service.yaml |
Create from YAML |
kubectl create service nodeport NAME --tcp=80:80 --node-port=30080 |
Create service |
Testing Commands
| Command | Description |
|---|---|
kubectl port-forward service/NAME LOCAL:REMOTE |
Port forward |
kubectl get nodes -o wide |
Get node IPs |
curl http://NODE_IP:NODE_PORT |
Test access |
Complete Verification Checklist
Use this checklist to verify your NodePort Service:
#!/bin/bash
echo "=== NodePort Service Verification ==="
echo -e "\n1. ReplicaSet Status:"
kubectl get replicaset httpd-replicaset
echo -e "\n2. Pod Status:"
kubectl get pods -l app=httpd_app,type=front-end
echo -e "\n3. Service Status:"
kubectl get service httpd-service
echo -e "\n4. Endpoints:"
kubectl get endpoints httpd-service
echo -e "\n5. Service Selector:"
kubectl get service httpd-service -o jsonpath='{.spec.selector}'
echo -e "\n6. NodePort Value:"
kubectl get service httpd-service -o jsonpath='{.spec.ports[0].nodePort}'
echo -e "\n7. Testing Access:"
NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')
NODE_PORT=$(kubectl get service httpd-service -o jsonpath='{.spec.ports[0].nodePort}')
curl -s -o /dev/null -w "HTTP Status: %{http_code}\n" http://$NODE_IP:$NODE_PORT
echo -e "\n8. Service Description:"
kubectl describe service httpd-service | grep -E "Name:|Type:|Selector:|NodePort:|Endpoints:"
Conclusion
Congratulations! You've successfully exposed an application using a Kubernetes NodePort Service. This is a fundamental skill that every DevOps engineer needs.
Key Takeaways
- NodePort Services expose applications on a static port on each node
- Selectors connect services to pods using labels
- Endpoints show which pods are being targeted
-
Port mapping:
nodePort → service port → targetPort → pod port - Load balancing happens automatically
- Verification is crucial - always test your services
What You Learned
✅ What NodePort Services are and how they work
✅ How to expose a ReplicaSet using NodePort
✅ How to verify service creation
✅ How to test application accessibility
✅ Advanced configuration options
✅ Common issues and solutions
✅ Best practices for production
Final Architecture
┌─────────────────────────────────────────────────────────────┐
│ ✅ Successfully Exposed! │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ReplicaSet: httpd-replicaset │ │
│ │ ✅ 3 pods running │ │
│ │ ✅ Labels: app=httpd_app,type=front-end │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Service: httpd-service │ │
│ │ ✅ Type: NodePort │ │
│ │ ✅ NodePort: 30080 │ │
│ │ ✅ Endpoints: 3 pods │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ✅ Access: http://10.244.164.51:30080 │
│ ✅ It works! 🎉 │
└─────────────────────────────────────────────────────────────┘
Next Steps
Now that you've mastered NodePort Services, consider exploring:
- LoadBalancer Services - For cloud environments
- Ingress Controllers - For advanced routing
- Service Meshes - For service-to-service communication
- Network Policies - For security
- ExternalDNS - For automatic DNS records
Resources
- Kubernetes Official Documentation - Services
- Kubernetes NodePort Services Guide
- KodeKloud Kubernetes Course
- Kubernetes Service Best Practices
Did this guide help you? Share your thoughts, questions, or experiences in the comments below! If you found this helpful, consider sharing it with your network.
Top comments (0)