Ingress Resources
HTTP routing, TLS termination, and production-ready Ingress templates in Helm
The Problem Ingress Solves
In Kubernetes, a Service of type ClusterIP is only reachable inside the cluster. Switching to LoadBalancer provisions an external IP, but each Service gets its own load balancer. For a microservices application with ten Services, that means ten cloud load balancers — ten external IPs, ten billing line items, and ten DNS records to manage.
Ingress solves this by placing a single reverse proxy (the Ingress controller) behind one LoadBalancer. Traffic enters through that single entry point and is routed to the correct backend Service based on rules you declare in an Ingress resource. Those rules can match on:
- Hostname —
app.example.comgoes to the frontend,api.example.comto the backend - Path —
/apiroutes to the API service,/to the web service - TLS termination — HTTPS is terminated at the Ingress controller; internal traffic stays on plain HTTP
This architecture dramatically simplifies external access, reduces cost, and centralises TLS certificate management.
Ingress Controllers
An Ingress resource on its own does nothing. It is a declarative configuration object — a set of routing rules. An Ingress controller is the component that reads those rules and configures the underlying proxy. Without a controller running in your cluster, Ingress resources are ignored.
| Controller | Proxy | Best For |
|---|---|---|
| ingress-nginx | NGINX | General-purpose, widest community support |
| Traefik | Traefik | Auto-discovery, built-in Let's Encrypt, middleware |
| HAProxy Ingress | HAProxy | High-throughput, low-latency L7 |
| AWS ALB Controller | AWS ALB | EKS environments, tight AWS integration |
| GCE Ingress | Google CLB | GKE environments, Google Cloud integration |
You install a controller into the cluster (typically via its own Helm chart) before creating Ingress resources. Many clusters run multiple controllers side by side — one for public traffic, another for internal.
IngressClass
When multiple controllers are installed, Kubernetes needs to know which controller should implement a given Ingress. The IngressClass resource links an Ingress to a specific controller:
apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
name: nginx
annotations:
ingressclass.kubernetes.io/is-default-class: "true"
spec:
controller: k8s.io/ingress-nginx
An Ingress references its class via spec.ingressClassName. If no class is specified and a default IngressClass exists (annotated with is-default-class: "true"), the default controller picks it up. In a Helm chart, the class name should always be configurable through values.
Path Types
Every path in an Ingress rule requires a pathType. This field controls how the path is matched against incoming requests:
| pathType | Behaviour | Example |
|---|---|---|
Prefix |
Matches the URL path prefix split by /. A path of /api matches /api, /api/, and /api/users. |
Most common for APIs |
Exact |
Matches the URL path exactly. /api matches only /api, not /api/. |
Health checks, specific endpoints |
ImplementationSpecific |
Matching behaviour depends on the IngressClass. Avoid unless you need controller-specific regex matching. | Legacy or advanced use cases |
Required Field
Always specify pathType explicitly. It is a required field since networking.k8s.io/v1 and omitting it will cause a validation error.
The Helm Ingress Template
A well-structured Ingress template wraps the entire resource in a conditional so it is only rendered when the user opts in. Here is the full pattern:
{{- if .Values.ingress.enabled }}
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: {{ include "myapp.fullname" . }}
labels:
{{- include "myapp.labels" . | nindent 4 }}
{{- with .Values.ingress.annotations }}
annotations:
{{- toYaml . | nindent 4 }}
{{- end }}
spec:
{{- if .Values.ingress.className }}
ingressClassName: {{ .Values.ingress.className }}
{{- end }}
{{- if .Values.ingress.tls }}
tls:
{{- range .Values.ingress.tls }}
- hosts:
{{- range .hosts }}
- {{ . | quote }}
{{- end }}
secretName: {{ .secretName }}
{{- end }}
{{- end }}
rules:
{{- range .Values.ingress.hosts }}
- host: {{ .host | quote }}
http:
paths:
{{- range .paths }}
- path: {{ .path }}
pathType: {{ .pathType }}
backend:
service:
name: {{ include "myapp.fullname" $ }}
port:
number: {{ .port | default 80 }}
{{- end }}
{{- end }}
{{- if .Values.ingress.defaultBackend }}
defaultBackend:
service:
name: {{ .Values.ingress.defaultBackend.service.name }}
port:
number: {{ .Values.ingress.defaultBackend.service.port.number }}
{{- end }}
{{- end }}
Key patterns to note:
- Conditional creation — The entire template is wrapped in
{{- if .Values.ingress.enabled }}. Default tofalsein values so Ingress is opt-in. - Range over hosts — The
hostsarray supports multi-host routing out of the box. - Dollar sign (
$) — Inside arangeloop, the dot (.) is rebound to the current element. Use$to access the root context for helper calls. - Default port —
{{ .port | default 80 }}provides a safe fallback.
TLS Configuration
TLS is configured in two parts: the tls block in the Ingress spec and the Secret that holds the certificate. You can create the Secret manually or let cert-manager handle it automatically.
Manual TLS Secret
kubectl create secret tls myapp-tls \
--cert=tls.crt \
--key=tls.key
Automated with cert-manager
Add cert-manager annotations to your Ingress and it will provision and renew certificates automatically:
ingress:
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
tls:
- secretName: myapp-tls
hosts:
- myapp.example.com
cert-manager watches for Ingress resources with its annotation, creates a Certificate resource, completes the ACME challenge, and stores the resulting certificate in the named Secret. The Ingress controller then loads the certificate and serves HTTPS traffic.
cert-manager Secret Ownership
When using cert-manager, the secretName in tls must not already exist as a manually-created Secret. cert-manager needs to own the Secret lifecycle.
Annotations Deep Dive
Annotations are the primary mechanism for configuring controller-specific features. Since the Ingress spec itself is minimal, annotations carry the bulk of the real-world configuration. Here are the most commonly used nginx-ingress annotations:
URL Rewriting
nginx.ingress.kubernetes.io/rewrite-target: /$2
Used with regex path capture groups to strip or rewrite path prefixes before forwarding to the backend.
HTTPS Redirect
nginx.ingress.kubernetes.io/ssl-redirect: "true"
Automatically redirects HTTP requests to HTTPS. Enabled by default when TLS is configured.
Request Size Limits
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
Controls the maximum allowed request body size. Essential for file upload endpoints.
Rate Limiting
nginx.ingress.kubernetes.io/limit-rps: "10"
nginx.ingress.kubernetes.io/limit-connections: "5"
Protects backends from excessive traffic. Requests exceeding the limit receive a 503 response.
Basic Authentication
nginx.ingress.kubernetes.io/auth-type: basic
nginx.ingress.kubernetes.io/auth-secret: basic-auth
nginx.ingress.kubernetes.io/auth-realm: "Authentication Required"
Requires HTTP Basic Auth using credentials stored in a Kubernetes Secret. Useful for protecting staging environments.
The Complete values.yaml Pattern
A production-ready values.yaml ingress block should expose every configurable aspect while providing sensible defaults:
ingress:
enabled: false
className: nginx
annotations: {}
# nginx.ingress.kubernetes.io/rewrite-target: /
# cert-manager.io/cluster-issuer: letsencrypt-prod
hosts:
- host: myapp.example.com
paths:
- path: /
pathType: Prefix
port: 80
tls: []
# - secretName: myapp-tls
# hosts:
# - myapp.example.com
defaultBackend: {}
# service:
# name: default-backend
# port:
# number: 80
This pattern works with helm install --set ingress.enabled=true for quick enablement, and with a full values override file for production deployments. The commented-out examples serve as documentation for users of your chart.
Multi-Host Ingress
The hosts array naturally supports routing to multiple domains. Each host can have its own set of paths, and the TLS block can cover all of them with a single or multiple certificates:
ingress:
enabled: true
hosts:
- host: www.example.com
paths:
- path: /
pathType: Prefix
- host: api.example.com
paths:
- path: /v1
pathType: Prefix
port: 8080
- path: /v2
pathType: Prefix
port: 8080
tls:
- secretName: example-com-tls
hosts:
- www.example.com
- api.example.com
A wildcard certificate (*.example.com) can simplify this further, covering all subdomains with a single TLS entry.
Default Backend
The defaultBackend field specifies where traffic goes when no rule matches. This is useful for serving custom 404 pages or catching misconfigured routes. If omitted, the Ingress controller typically returns its own default 404 page.
In your Helm template, make the default backend optional:
{{- if .Values.ingress.defaultBackend }}
defaultBackend:
service:
name: {{ .Values.ingress.defaultBackend.service.name }}
port:
number: {{ .Values.ingress.defaultBackend.service.port.number }}
{{- end }}
Summary
Ingress is the standard Kubernetes mechanism for exposing HTTP and HTTPS routes from outside the cluster to Services within it. In a Helm chart, the Ingress template should be conditionally created, support multiple hosts and TLS entries, and expose annotations for controller-specific configuration. The values.yaml should default to enabled: false with commented examples showing common configurations.
Key points to remember:
- An Ingress controller must be installed separately — the Ingress resource alone does nothing
- Use
IngressClassto select a controller when multiple are installed - Always specify
pathType— it is a required field - Use annotations for rewriting, rate limiting, authentication, and cert-manager integration
- Guard the entire template with
if .Values.ingress.enabled - Use
$instead of.for root context access insiderangeloops