K8s Networking: NodePort vs LoadBalancer vs Ingress

One of the most confusing decisions for engineers new to Kubernetes: how do I expose my application? After managing 20+ EKS clusters at Charter Communications, here's my mental model for choosing the right approach every time.

The Three Options at a Glance

NodePort

Opens a port on every node

A Service type that exposes your app using a high port (30000-32767) on each worker node.

User → Node IP:30080 → Service → Pod
  • Simple setup, no external dependencies
  • Works in any environment
  • Not user-friendly (IP:port access)
  • Hard to manage in production
Best for: Labs, testing, internal access

LoadBalancer

One public entry point per service

Asks your cloud provider to create an external load balancer (e.g. AWS ALB/NLB).

User → Load Balancer → Service → Pods
  • Easy public access
  • Cloud-native integration
  • One LB per service = expensive
  • No path-based routing
Best for: Exposing one app publicly

Ingress

Smart routing for many services

Manages HTTP/HTTPS traffic with clean URLs, path/host routing, and TLS termination.

User → LB → Ingress Controller → Service → Pods
  • Clean URLs & path-based routing
  • TLS termination & cert management
  • Single entry point for many services
  • Requires an Ingress Controller
Best for: Production HTTP/HTTPS

When to Use What: Decision Tree

Quick Decision Guide

Need to expose a service for local development or testing?
→ NodePort
Need to expose a single TCP/UDP service (not HTTP) publicly?
→ LoadBalancer
Need to expose multiple HTTP services with clean URLs?
→ Ingress
Need TLS termination and cert auto-renewal?
→ Ingress + cert-manager
Need advanced routing (canary, A/B, traffic splitting)?
→ Ingress + Istio or Gateway API

Real-World Example: NodePort

apiVersion: v1
kind: Service
metadata:
  name: my-app-nodeport
spec:
  type: NodePort
  selector:
    app: my-app
  ports:
    - port: 80          # Service port (internal)
      targetPort: 8080  # Container port
      nodePort: 30080   # External port (30000-32767)
# Access: http://<any-node-ip>:30080

Real-World Example: LoadBalancer

apiVersion: v1
kind: Service
metadata:
  name: my-app-lb
  annotations:
    service.beta.kubernetes.io/aws-load-balancer-type: "nlb"
    service.beta.kubernetes.io/aws-load-balancer-scheme: "internet-facing"
spec:
  type: LoadBalancer
  selector:
    app: my-app
  ports:
    - port: 443
      targetPort: 8080
      protocol: TCP
# AWS provisions an NLB automatically
# Access: http://<lb-dns-name>

Real-World Example: Ingress

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: app-ingress
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    cert-manager.io/cluster-issuer: "letsencrypt-prod"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.myapp.com
        - app.myapp.com
      secretName: myapp-tls
  rules:
    - host: api.myapp.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 80
    - host: app.myapp.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: frontend-service
                port:
                  number: 80
# Single LB serves multiple services via host/path routing

Pro Tip from Production

At Charter, we use Ingress with AWS ALB Controller for all HTTP services and LoadBalancer (NLB) for gRPC and raw TCP workloads. The key insight: don't create one LoadBalancer per microservice. With 100+ services, that's 100 AWS load balancers at ~$20/month each = $24,000/year wasted. Use Ingress to fan out from a single entry point.

Found this helpful?

Check out more infrastructure templates in my Code Lab, or connect on LinkedIn.