🟢 Welcome to my World!
WORKSPACE
TIMELINE
root/welcome.hi

NodePort, LoadBalancer, or Ingress? — Choosing the Right Way to Expose Your Kubernetes App

NodePort, LoadBalancer, or Ingress? — Choosing the Right Way to Expose Your Kubernetes App


I deployed my app, the containers are happily running, the CPU is humming along… but how do I actually reach it? It’s a common moment of confusion for developers new to Kubernetes: you’ve written your Deployment, applied it, and confirmed the Pods are alive, but your application feels locked away in a private, isolated cluster network. If you try to curl localhost, you get nothing. If you look for an IP address, it’s an internal one that your laptop can’t resolve.

In this third post of our Kubernetes series, we’ll demystify how traffic actually flows into your cluster and build a clear path to expose your app to the outside world, taking it all the way from local development to a robust production setup.

What You’ll Learn

  • The conceptual Kubernetes networking model and why Services are strictly necessary.
  • The four core Service types (ClusterIP, NodePort, LoadBalancer, ExternalName) and when to use them.
  • A first look at the modern Gateway API that replaces legacy Ingress.
  • A line-by-line code walkthrough of exactly how a Deployment connects to a Service.
  • The reasoning behind the configuration choices you make in your manifests.
  • Common networking gotchas that leave developers scratching their heads in frustration.

The Core Concept: Finding Your Way Through the Cluster

To understand networking in Kubernetes, think of a Pod as an apartment, and the containers as the rooms inside it. While apartments have addresses (IP addresses), in Kubernetes, Pods are inherently ephemeral—they are created, destroyed, and recreated constantly when you scale up, scale down, or execute rolling updates. If you try to send a request directly to a specific Pod’s IP address, it might be vacant or completely non-existent tomorrow. We need a much more reliable mechanism.

Enter the Service. A Service acts as the front desk concierge or a permanent phone directory. It provides a single, unmoving, permanent IP address and a stable DNS name. No matter which Pods come and go, the Service keeps an constantly updated ledger of the healthy ones and routes your incoming traffic to them. You talk to the Service; the Service talks to the Pods.

Behind the scenes, a component called kube-proxy runs on every node, constantly watching the API server for changes to Services and their associated Pod endpoints, updating networking rules dynamically to make this seamless.

Kubernetes offers four main ways to expose your app via Services:

1. ClusterIP (The Default) This assigns an internal, private virtual IP to your Service. It is only reachable from within the cluster. This is perfect for internal east-west communication—for instance, if your frontend application needs to talk to your backend API, or if your backend needs to query an in-cluster database. It provides a secure boundary because nothing on the public internet can route to a ClusterIP.

2. NodePort (The Quick Dev Path) NodePort opens a specific, static port (between 30000–32767) on every single node (machine) in your cluster. If someone hits any node’s IP on that specific port, they are transparently routed to your Service. It’s fantastic for local development (like with Minikube, kind, or Docker Desktop) or quick testing. However, it’s an operational and security nightmare for real-world production because it requires opening high-numbered firewall ports and exposes the raw node IP.

3. LoadBalancer (The Cloud Native Way) This builds directly upon NodePort. When you declare a LoadBalancer Service on a managed cloud provider (like AWS EKS, Google GKE, or Azure AKS), Kubernetes automatically calls out to the cloud provider’s API to provision a real, external cloud load balancer. This external load balancer acts as a single, stable public entry point that routes traffic into your cluster’s NodePorts. While incredibly convenient, be aware that each LoadBalancer Service typically provisions a dedicated cloud resource, which can get expensive if you have dozens of microservices.

4. ExternalName (The Alias) This Service type is unique; it acts as a simple CNAME DNS record. It allows you to alias an external service (like a managed RDS database outside the cluster or a third-party API) as if it were a native Kubernetes Service residing inside your cluster. This is great for keeping your configurations environment-agnostic.

The Future is Here: Gateway API Historically, when you wanted to handle HTTP routing (like sending /api to one service and /web to another) or manage SSL/TLS certificates, you used an Ingress controller. However, Ingress grew complex, fragmented, and heavily reliant on vendor-specific custom annotations.

The modern, standardized solution is the Gateway API. It uses a standardized, role-oriented design that separates concerns: infrastructure operators manage the GatewayClass and Gateway resources, while application developers simply deploy an HTTPRoute to define how their specific app’s traffic is handled. It’s cleaner, more expressive, and natively supports advanced deployment patterns like traffic splitting, header matching, and canary releases.

Implementation Deep Dive: Connecting the Pieces

Let’s see this in action using our kubernetes-demo codebase.

First, we need our Pods running. Here is the relevant snippet from our deployment.yaml that defines our application Pods:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: kubernetes-demo-api
  labels:
    app: kubernetes-demo-api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: kubernetes-demo-api
  template:
    metadata:
      labels:
        app: kubernetes-demo-api
    spec:
      containers:
        - name: kubernetes-demo-api
          image: USERNAME/kubernetes-demo-api:latest
          ports:
            - containerPort: 6789

And here’s what our service.yaml looks like to expose those Pods:

apiVersion: v1
kind: Service
metadata:
  name: kubernetes-demo-api-service
  labels:
    app: kubernetes-demo-api
spec:
  selector:
    app: kubernetes-demo-api
  ports:
    - protocol: TCP
      port: 6789 # Service port
      targetPort: 6789 # Container port
  type: NodePort # change to LoadBalancer if running in cloud

Let’s break down the “WHY” behind these configurations line-by-line, as understanding the why is what separates beginners from experts:

Why do we need a separate Service resource at all instead of just exposing the Deployment? Deployments manage Pods, and Pods are ephemeral. When you deploy a new version of USERNAME/kubernetes-demo-api:latest, the old Pods are terminated and new ones are created with entirely different IP addresses. The Service provides a stable endpoint that abstracts away this constant churn. Clients just talk to the Service, and the Service handles the dynamic routing.

Why is selector: app: kubernetes-demo-api critical? Notice how the selector in our service.yaml exactly matches the labels in the template.metadata.labels of our deployment.yaml. This is the crucial link. Kubernetes uses this selector to constantly scan the cluster for Pods with matching labels. If they match, the Service adds their IPs to its active routing list (Endpoints). Without this exact match, your Service routes to absolutely nothing, and traffic goes nowhere.

Why is port vs targetPort separated? Decoupling these gives you incredible flexibility. port: 6789 is the port that the Service itself exposes to the rest of the cluster (the front door). targetPort: 6789 is the actual port your application container is listening on (the internal room). While they match in our demo, you could easily have the Service listen on a standard port 80 or 443 for external simplicity, but route traffic to a non-privileged targetPort like 8080 inside the container for security purposes.

Why type: NodePort for local dev? In our demo, we use NodePort. Local environments like Minikube don’t have AWS Elastic Load Balancers sitting around. A NodePort bypasses the need for external cloud infrastructure by simply opening a port directly on the local node’s IP address, making it instantly reachable from your local workstation browser or CLI.

Why the comment about changing to LoadBalancer if running in the cloud? When you take this to AWS, GCP, or Azure, you want a real, public-facing entry point. Changing the type to LoadBalancer triggers the cloud provider to spin up a managed external load balancer automatically, attaching a real public IP to your Service.

Why does NodePort use a high range (30000-32767)? Kubernetes explicitly reserves this specific port range for NodePorts to guarantee they will never conflict with well-known system ports (like 80 for HTTP, 443 for HTTPS, or 22 for SSH) running on the underlying host machines.

Finally, how do we know what port was dynamically assigned to our NodePort? We can look at how we deploy this in our deploy.sh script on line 24:

kubectl get services kubernetes-demo-api-service

When you run this command, it will output a mapped port under the PORT(S) column (for example, 6789:31234/TCP). The first number is the internal port, and the second is the mapped NodePort. You simply visit your node’s IP at port 31234 to reach your app!

Common Mistakes / Gotchas

[!WARNING] Gotcha: Port Confusion A very common mistake is mixing up port and targetPort. Remember: port is the front door of the Service. targetPort must exactly match the containerPort your application is actively binding to inside the Docker container. If your Node app listens on 3000 but your targetPort says 8080, traffic will hit a wall.

[!IMPORTANT] Gotcha: Label Mismatches If your Service has a selector like app: backend but your Deployment labels say app: api, your Service will silently fail to find any endpoints. Traffic will hit a dead end, and you’ll get connection timeouts. Always double-check your spelling and casing!

[!CAUTION] Gotcha: NodePort in Production Never use NodePort as your primary public ingress in production. Exposing high-numbered ports across your entire infrastructure requires complex firewall rules, bypasses centralized WAF/traffic inspection, and provides no standard port 80/443 access for users. Rely on a LoadBalancer or the Gateway API for production workloads.

Summary / Key Takeaways

  • Services Provide Stability: Pods are constantly dying and restarting; Services remain permanent. They are the stable front doors for your applications.
  • Understand the Types: Use ClusterIP for internal traffic, NodePort for local debugging, and LoadBalancer or Gateway API for public external access.
  • Labels Are the Glue: The selector in your Service must perfectly match the labels in your Pod template. This is how traffic finds its way.
  • Port vs TargetPort: port is what the Service exposes; targetPort is what your container actually listens on. Keep them straight to avoid debugging nightmares.
  • Look to the Future: If you need complex HTTP routing, skip legacy Ingress and look straight to the modern Gateway API (HTTPRoute resources).

What’s Next

Now that your traffic is successfully reaching your app, how does Kubernetes know if your app is actually healthy and ready to receive requests? What happens if your Node.js event loop gets blocked or your database connection drops? In our next post (Post #4), we’ll dive into Health Checks: Liveness, Readiness, and Startup Probes—the critical mechanisms Kubernetes uses to automatically kill frozen apps and pull overloaded ones out of the load balancer rotation. Stay tuned!

TERMINAL
1: zsh
sirishgurung@portfolio:-$cat ./contact.txt
▄▄   ▄▄▄▄ ▄▄▄▄ ▄▄  ▄▄▄      ▄▄ ▄ ▄ ▄▄▄▄ ▄▄▄▄ ▄▄ ▄      ▄▄▄▄ ▄▄▄▄ ▄▄▄▄▄ ▄▄▄▄ ▄▄▄▄ ▄▄ ▄ ▄▄▄▄ ▄▄▄▄ ▄▄
██   ██ ▀  ██  ▀  ██▀       ██ █ █ ██ █ ██ █ ██ █       ██  ██ █ ██    ██ ▀  ██  ██ █ ██ ▀ ██ █ ██
██   ██▀   ██     ▀██▄      ██ █ █ ██ █ ██▄▀ ██▄▀       ██  ██ █ ██ ▄▄ ██▀   ██  ██▄█ ██▀  ██▄▀ ██
 █▄▄  █ █  ▐█      ▄▀▀       █ █ ▀  █ █ ▀█ █  █ █       ▐█   █ █ ▐▀ ▀▌  █ █  ▐█   █ █  █ █ ▀█ █ ▀▀
▀▀▀▀ ▀▀▀▀  ▀▀     ▀▀▀       ▀▀▀▀▀▀ ▀▀▀▀ ▀▀ ▀ ▀▀ ▀       ▀▀  ▀▀▀▀ ▀▀▀▀▀ ▀▀▀▀  ▀▀  ▀▀ ▀ ▀▀▀▀ ▀▀ ▀ ▀▀
      
I'd love to hear from you!>Get in touch