Kubernetes Ingress: exposing your application to the world

1. Overview

When you have a Service running in your cluster but want external users to access it via a pretty domain like api.nonladev.com instead of a messy IP:port, that’s when you need Ingress. This guide explains what Ingress is, how it differs from Service type LoadBalancer, how to install an Ingress Controller, and how to write Ingress resources for common scenarios: path-based routing, host-based routing, and TLS. By the end, you’ll be able to expose a Spring Boot application to the outside world with a custom domain and HTTPS.

2. Why You Need Ingress

2.1. The Problem with Service

Service types NodePort and LoadBalancer work well, but have limitations:

  • LoadBalancer: each Service needs its own external IP → costly if you have many services (each cloud LB charges separately).
  • NodePort: exposes an odd port (30000-32767), has no TLS termination, and offers no routing based on domain/path.

Suppose you have 5 microservices to expose. Using LoadBalancer means 5 cloud load balancers and 5 external IPs. Ingress solves this by using a single entry point to route traffic to multiple internal Services, based on hostname or path.

2.2. What Is Ingress?

Ingress is an API object that defines HTTP/HTTPS routing rules from outside into Services within your cluster. Ingress itself does nothing — it needs an Ingress Controller (a Pod running a reverse proxy like NGINX, Traefik, or HAProxy) to read the rules and enforce them.

Client → Ingress Controller (LoadBalancer/NodePort) → Ingress rules → Service → Pod

Note: If your cluster hasn’t installed an Ingress Controller, creating an Ingress resource won’t do anything — it’s just config “lying dormant” waiting for the controller to read it.

3. Installing an Ingress Controller

The most popular choice is ingress-nginx. Install it using Helm:

<!-- not applicable — use Helm/kubectl, not Maven dependency -->
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx --create-namespace

This command creates the ingress-nginx namespace, deploys the Ingress Controller along with a Service of type LoadBalancer to receive external traffic. Check the status:

kubectl get pods -n ingress-nginx
kubectl get svc -n ingress-nginx
NAME                                       TYPE           EXTERNAL-IP     PORT(S)
ingress-nginx-controller                   LoadBalancer   34.123.45.67    80:31234/TCP,443:31235/TCP

This external IP is the single entry point for all Ingresses in your cluster.

4. Sample Deployment and Service

Before writing an Ingress, you need a Service to route to. Suppose you have an order-service application:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 2
  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

The order-service Service exposes port 80 and forwards to container port 8080. Note that this Service does not need type LoadBalancer — using ClusterIP (the default) is sufficient, since the Ingress Controller will reach the Service via the internal network.

5. Basic Ingress Resource — Host-Based Routing

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: order-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
    - host: api.nonladev.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: order-service
                port:
                  number: 80

Let’s break it down:

  • ingressClassName: nginx — specifies which controller handles this rule (important if your cluster has multiple controllers).
  • rules.host — only traffic with header Host: api.nonladev.com will match this rule.
  • path + pathType: Prefix — any request starting with / (i.e., all requests) will be forwarded to order-service:80.
  • The rewrite-target annotation — specific to ingress-nginx, used to rewrite the URL before forwarding (different controllers have different annotations).

Apply and test:

kubectl apply -f order-ingress.yaml
curl -H "Host: api.nonladev.com" http://34.123.45.67/
{"service":"order-service","status":"UP"}

In a production environment, you’d point the DNS record api.nonladev.com to the Ingress Controller’s external IP, and you wouldn’t need to fake the Host header like above.

6. Path-Based Routing — Multiple Services Sharing a Domain

A common scenario: one domain, multiple services on different paths.

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-gateway-ingress
spec:
  ingressClassName: nginx
  rules:
    - host: api.nonladev.com
      http:
        paths:
          - path: /orders
            pathType: Prefix
            backend:
              service:
                name: order-service
                port:
                  number: 80
          - path: /customers
            pathType: Prefix
            backend:
              service:
                name: customer-service
                port:
                  number: 80

Requests to api.nonladev.com/orders/123 route to order-service, while /customers/55 routes to customer-service. pathType: Prefix means matching by prefix — different from Exact (must match precisely) and ImplementationSpecific (controller interprets freely, often used for regex).

Note: The order of paths doesn’t affect matching in Ingress v1 — the controller automatically selects the longest matching path first. But write your paths clearly and without overlap to avoid hard-to-debug issues.

7. TLS — HTTPS for Ingress

To enable HTTPS, you need a Secret containing the certificate and key, then reference it in the Ingress.

kubectl create secret tls nonladev-tls \
  --cert=tls.crt --key=tls.key
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: order-ingress-tls
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.nonladev.com
      secretName: nonladev-tls
  rules:
    - host: api.nonladev.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: order-service
                port:
                  number: 80

spec.tls declares which hosts use which certificates. The controller automatically handles TLS termination — decrypting HTTPS at the Ingress and forwarding plain HTTP to the internal Service. Your Pods don’t need to know anything about TLS.

In practice, you usually use cert-manager to automatically obtain Let’s Encrypt certificates instead of creating the Secret manually:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: order-ingress-tls
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.nonladev.com
      secretName: nonladev-tls
  rules:
    - host: api.nonladev.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: order-service
                port:
                  number: 80

The cluster-issuer annotation requests cert-manager to automatically create and renew the nonladev-tls Secret — no manual intervention needed.

8. Writing Tests to Verify Your Exposed Endpoints

Once your Ingress is working, you can write a simple integration test to verify your controller handles the paths correctly (test at the Spring Boot application level, not the YAML directly):

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderControllerIT {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void shouldReturnOrderWhenCalledWithOrdersPrefix() {
        ResponseEntity<String> response =
                restTemplate.getForEntity("/orders/123", String.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).contains("orderId");
    }
}

This test ensures your Spring Boot application correctly handles the /orders/123 path that the Ingress rewrites to — separating backend code errors from Ingress configuration errors.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

9. Common Mistakes

  • Forgetting ingressClassName: if your cluster has multiple Ingress Controllers (e.g., both nginx and traefik), the Ingress won’t know which controller to use → either nothing happens or both process it incorrectly.
  • Wrong Service type: leaving the backend Service as LoadBalancer is unnecessary and wasteful — only use ClusterIP.
  • Mismatched pathType: using Exact when you need Prefix causes sub-paths (like /orders/123) to return 404 because only /orders matches exactly.
  • DNS not set up yet: test with curl -H "Host: ..." before configuring real DNS to avoid wasting time waiting for DNS propagation.
  • Wrong rewrite annotation for your controller: ingress-nginx annotations don’t work with Traefik or AWS ALB Ingress Controller — each controller has its own set of annotations, read the relevant docs carefully.

When NOT to use Ingress: if your application uses a protocol other than HTTP/HTTPS (e.g., raw TCP, complex gRPC streaming, or UDP), standard Ingress won’t work well — consider Gateway API or a direct LoadBalancer Service type for TCP/UDP.

10. Conclusion

Ingress is a centralized HTTP/HTTPS routing layer that lets you expose multiple Services to the outside via a single entry point, saving on LoadBalancer costs and making it easy to manage domains, paths, and TLS. Key points to remember: Ingress resources are just rules; you need an Ingress Controller to enforce them; choose the right ingressClassName and pathType; use cert-manager to automate TLS. For non-HTTP applications, consider Gateway API instead of forcing Ingress.

kubernetesingressnetworkingdevopsnginx