Building a Robust, Extensible Kubernetes Ingress Architecture
In modern cloud-native deployments, exposing multiple services under consistent hostnames (e.g., heunify.com, api.heunify.com, www.heunify.com) while ensuring security, TLS automation, rate-limiting, and future extensibility is critical. Below is a production-ready Kubernetes ingress architecture on AWS (3-node EC2 cluster) designed to meet these requirements.
1. Design Requirements & Constraints
We have a 3-node Amazon EC2 Kubernetes cluster (each node untainted, serving as both control-plane and worker). Within this cluster:
-
Services to expose
- A frontend application (hosted at
heunify.comandwww.heunify.com). - A backend application (hosted at
api.heunify.com). - A MariaDB StatefulSet (3 replicas), which is not directly exposed externally.
- A frontend application (hosted at
-
Key objectives
-
Single, unified external entry point (load balanced across all nodes).
-
TLS/HTTPS termination using Let’s Encrypt certificates (automatically obtained and renewed).
-
Rate limiting, IP blacklisting, and logging on ingress traffic.
-
Hostname and path routing:
heunify.com&www.heunify.com→frontendserviceapi.heunify.com→backendservice
-
Future extensibility: new namespaces (e.g.,
redis,kafka,spark) and new subdomains (e.g.,redis.heunify.com,kafka.heunify.com) should slot in seamlessly.
-
2. High-Level Architecture & Components
Below is a conceptual diagram of the final state. All components are Kubernetes-native; two controllers are installed via Helm, plus a handful of YAML resources:
┌─────────────────────┐ │ DNS (Route 53) │ │ heunify.com, *.heunify.com │ └─────────┬───────────┘ │ ▼ ┌─────────────────────┐ │ AWS ELB (LoadBalancer) │ ← created by the Ingress Controller’s Service └─────────┬───────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ NGINX Ingress Controller (ns: ingress-nginx) │ │ • Watches all Ingress resources across namespaces │ │ • Terminates TLS via Secret: heunify-tls-secret │ │ • Applies rate limits, blacklist snippets, path-based routing │ └─────────┬───────────────────────────────────────────────────────┘ │ ┌──────────────────┐ ┌──────────────────┐ │ frontend-ingress │ │ backend-ingress │ │ (ns: frontend) │ │ (ns: backend) │ └───────┬──────────┘ └───────┬──────────┘ │ │ ▼ ▼ ┌───────────────┐ ┌───────────────┐ │ frontend-svc │ │ backend-svc │ │ (ClusterIP) │ │ (ClusterIP) │ └───────┬───────┘ └───────┬───────┘ │ │ ▼ ▼ [frontend Pods] [backend Pods] Separately: ┌───────────────────────────────────────────────────────────────────┐ │ cert-manager (ns: cert-manager) │ │ • ClusterIssuer: letsencrypt-prod (ACME HTTP-01 via nginx) │ │ • Certificate: heunify-tls (covers heunify.com, www, api) │ │ • Secret: heunify-tls-secret (mounted by NGINX to serve TLS) │ └───────────────────────────────────────────────────────────────────┘
Key points:
- Ingress Controller (NGINX) lives in its own namespace (
ingress-nginx) and manages a single AWS ELB (LoadBalancer Service). - Ingress Resources reside in app-specific namespaces (
frontend,backend). Each one declares host/path rules, references a shared TLS Secret. - cert-manager (in namespace
cert-manager) uses a ClusterIssuer to request and auto-renew a Certificate covering all relevant DNS names. - DNS (Route 53, or your preferred provider) points
heunify.com,www.heunify.com, andapi.heunify.comat the ELB’s external address.
3. Step-by-Step Implementation
Two main tasks:
- Install controllers (
ingress-nginxandcert-manager) via Helm. - Apply a single YAML file that sets up namespaces, ClusterIssuer, Certificate, and Ingress resources.
3.1. Install Controllers via Helm
3.1.1. Install NGINX Ingress Controller
# 1) Add the ingress-nginx repository helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx helm repo update # 2) Install into namespace "ingress-nginx" helm install ingress-nginx ingress-nginx/ingress-nginx \ --namespace ingress-nginx --create-namespace \ --set controller.publishService.enabled=true
-
Creates:
- A Deployment of NGINX Ingress controller pods.
- A Service of type
LoadBalancer, which provisions an AWS ELB with a single external IP or DNS name.
-
Use
kubectl get svc -n ingress-nginxto retrieve theEXTERNAL-IPor DNS. Point your DNS provider to that value.
3.1.2. Install cert-manager
# 1) Add the Jetstack repository helm repo add jetstack https://charts.jetstack.io helm repo update # 2) Install into namespace "cert-manager" helm install cert-manager jetstack/cert-manager \ --namespace cert-manager --create-namespace \ --set installCRDs=true
- Deploys cert-manager controller pods and installs CRDs (
Issuer,ClusterIssuer,Certificate, etc.). - Prepares the cluster to automatically request and renew Let’s Encrypt certificates.
3.2. Apply the “Mega-Manifest” YAML
Save the following content as mega-ingress-setup.yaml and run:
kubectl apply -f mega-ingress-setup.yaml
################################################################ # 1. Create Namespaces ################################################################ apiVersion: v1 kind: Namespace metadata: name: frontend --- apiVersion: v1 kind: Namespace metadata: name: backend ################################################################ # 2. ClusterIssuer (Let’s Encrypt via HTTP01 using nginx) ################################################################ apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: name: letsencrypt-prod spec: acme: server: https://acme-v02.api.letsencrypt.org/directory email: you@heunify.com # Replace with your admin email privateKeySecretRef: name: letsencrypt-prod solvers: - http01: ingress: class: nginx # Must match ingress.class --- ################################################################ # 3. Certificate covering all hostnames (frontend & API) ################################################################ apiVersion: cert-manager.io/v1 kind: Certificate metadata: name: heunify-tls namespace: frontend # TLS Secret lives in "frontend" spec: secretName: heunify-tls-secret # Name of the Secret for TLS issuerRef: name: letsencrypt-prod kind: ClusterIssuer commonName: heunify.com dnsNames: - heunify.com - www.heunify.com - api.heunify.com # Include API hostname here --- ################################################################ # 4. Frontend Ingress (namespace: frontend) ################################################################ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: frontend-ingress namespace: frontend annotations: kubernetes.io/ingress.class: "nginx" nginx.ingress.kubernetes.io/limit-connections: "20" nginx.ingress.kubernetes.io/limit-rps: "10" nginx.ingress.kubernetes.io/whitelist-source-range: "0.0.0.0/0" nginx.ingress.kubernetes.io/server-snippet: | location ~* ^/(wp-admin|config|user) { return 444; } spec: tls: - hosts: - heunify.com - www.heunify.com secretName: heunify-tls-secret rules: - host: heunify.com http: paths: - path: / pathType: Prefix backend: service: name: frontend-svc port: number: 80 - host: www.heunify.com http: paths: - path: / pathType: Prefix backend: service: name: frontend-svc port: number: 80 --- ################################################################ # 5. Backend (API) Ingress (namespace: backend) ################################################################ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: backend-ingress namespace: backend annotations: kubernetes.io/ingress.class: "nginx" nginx.ingress.kubernetes.io/limit-connections: "20" nginx.ingress.kubernetes.io/limit-rps: "10" nginx.ingress.kubernetes.io/whitelist-source-range: "0.0.0.0/0" spec: tls: - hosts: - api.heunify.com secretName: heunify-tls-secret rules: - host: api.heunify.com http: paths: - path: / pathType: Prefix backend: service: name: backend-svc port: number: 80
What this manifest does:
-
Namespaces
- Creates
frontendandbackendfor separation of resources.
- Creates
-
ClusterIssuer
letsencrypt-prod(ACME HTTP-01 via nginx). Cert-manager uses it to obtain/renew Let’s Encrypt certificates for any namespace.
-
Certificate
heunify-tlsinfrontendnamespace coversheunify.com,www.heunify.com, andapi.heunify.com.- When issued, it stores TLS cert+key in the Secret
heunify-tls-secret.
-
Ingress: frontend-ingress
- In namespace
frontend. - Annotations for rate-limiting (20 connections, 10 req/sec) and a snippet to drop requests probing
/wp-admin,/config,/user. - TLS references the shared
heunify-tls-secret(contains the Let’s Encrypt certificate). - Routes traffic for
heunify.comandwww.heunify.comtofrontend-svc:80.
- In namespace
-
Ingress: backend-ingress
- In namespace
backend. - Same rate-limit annotations.
- Routes
api.heunify.comtobackend-svc:80, using the sameheunify-tls-secret.
- In namespace
4. Detailed Component Explanations
4.1. NGINX Ingress Controller
-
Deployment & Service
- Installed via Helm in namespace
ingress-nginx. - Creates a Deployment (NGINX pods) and a Service of type
LoadBalancer. - AWS automatically provisions an ELB (Classic or Network LB) for that Service.
- Installed via Helm in namespace
-
Ingress Class
- Each Ingress resource must set
kubernetes.io/ingress.class: "nginx". The controller watches only resources matching its class.
- Each Ingress resource must set
-
Dynamic Configuration
- The controller continuously monitors Ingress objects cluster-wide and regenerates NGINX configuration as needed.
- On any Ingress create/update/delete, NGINX is reloaded with the new rules.
-
Rate Limiting & IP Whitelisting
nginx.ingress.kubernetes.io/limit-connections: "20"enforces a max of 20 concurrent connections per client IP.nginx.ingress.kubernetes.io/limit-rps: "10"enforces a max of 10 requests per second per client IP.nginx.ingress.kubernetes.io/whitelist-source-range: "0.0.0.0/0"allows all IPs (adjust as needed to restrict access).
-
Custom Snippets
- Using
nginx.ingress.kubernetes.io/server-snippet, you can inject raw NGINX directives. For example, blocking requests matching regex paths (e.g.,/wp-admin,/config,/user) by returning 444 (immediate connection drop).
- Using
4.2. cert-manager & ACME
-
cert-manager
- An in-cluster controller that automates issuance and renewal of X.509 certificates (Let’s Encrypt, Vault, etc.).
- Relies on ACME (Automatic Certificate Management Environment) to interact with Let’s Encrypt.
-
ClusterIssuer
-
A cluster-scoped resource named
letsencrypt-prod. -
Configured to use ACME HTTP-01 challenge via
ingress.class: nginx. -
When a Certificate references this issuer, cert-manager orchestrates the ACME HTTP-01 flow:
- Creates a temporary
Challengeresource. - Patches an Ingress to serve a challenge token under
/.well-known/acme-challenge/.... - Let’s Encrypt validates the challenge by requesting that path on
heunify.com(through the NGINX Ingress). - When validation succeeds, cert-manager stores the resulting certificate and private key in a Secret.
- Creates a temporary
-
-
Certificate Resource
- Declares
commonNameanddnsNames. In this setup, it includesheunify.com,www.heunify.com, andapi.heunify.com. - References the
ClusterIssuer. - On creation, cert-manager spawns a Challenge → NGINX serves it → Let’s Encrypt validates → Secret
heunify-tls-secretis populated. - Renewal: cert-manager automatically re-runs the ACME flow ~30 days before expiry, updating the Secret without manual intervention.
- Declares
4.3. Ingress Resources
4.3.1. Frontend Ingress (Namespace: frontend)
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: frontend-ingress namespace: frontend annotations: kubernetes.io/ingress.class: "nginx" nginx.ingress.kubernetes.io/limit-connections: "20" nginx.ingress.kubernetes.io/limit-rps: "10" nginx.ingress.kubernetes.io/whitelist-source-range: "0.0.0.0/0" nginx.ingress.kubernetes.io/server-snippet: | location ~* ^/(wp-admin|config|user) { return 444; } spec: tls: - hosts: - heunify.com - www.heunify.com secretName: heunify-tls-secret rules: - host: heunify.com http: paths: - path: / pathType: Prefix backend: service: name: frontend-svc port: number: 80 - host: www.heunify.com http: paths: - path: / pathType: Prefix backend: service: name: frontend-svc port: number: 80
-
Namespace: Keeps the Ingress resource close to the Services it routes (
frontend). -
Annotations:
ingress.class: "nginx"ensures the NGINX controller picks it up.limit-connections,limit-rps,whitelist-source-rangeenforce per-IP rate limiting and allow-all IPs by default.server-snippetblocks common attack probes (e.g.,/wp-admin,/config,/user) by returning 444.
-
TLS: Uses the shared Secret
heunify-tls-secret(populated by cert-manager) for bothheunify.comandwww.heunify.com. -
Rules: Two
rulesentries forheunify.comandwww.heunify.com, each routing/tofrontend-svc:80.
4.3.2. Backend (API) Ingress (Namespace: backend)
apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: backend-ingress namespace: backend annotations: kubernetes.io/ingress.class: "nginx" nginx.ingress.kubernetes.io/limit-connections: "20" nginx.ingress.kubernetes.io/limit-rps: "10" nginx.ingress.kubernetes.io/whitelist-source-range: "0.0.0.0/0" spec: tls: - hosts: - api.heunify.com secretName: heunify-tls-secret rules: - host: api.heunify.com http: paths: - path: / pathType: Prefix backend: service: name: backend-svc port: number: 80
- Shares the same TLS Secret (
heunify-tls-secret) as the frontend Ingress, coveringapi.heunify.com. - Rate-limit annotations match those in frontend.
- Rule: Routes all traffic for
api.heunify.comtobackend-svc:80.
Why include
api.heunify.comin the Certificate? Without that DNS name in thednsNameslist, TLS handshakes forapi.heunify.comwould fail. Bundling all hostnames into one multi-SAN certificate reduces complexity and stays well within Let’s Encrypt’s limit (100 names per certificate).
5. Security & Operational Best Practices
-
Rate Limiting
- Prevents denial-of-service from a single IP.
limit-connections: 20ensures a client can’t open more than 20 concurrent connections.limit-rps: 10ensures no more than 10 requests/sec per IP.
-
IP Blacklisting / Custom Snippets
- The
server-snippetdirective drops requests matching regexes (e.g.,/wp-admin). - For more advanced application-layer filtering, consider adding a WAF (ModSecurity) sidecar.
- The
-
fail2ban / Node-Level Protection
- Deploy a DaemonSet that tails NGINX access logs on each node.
- If an IP repeatedly triggers suspicious patterns (e.g., probing non-existent
.phpfiles), fail2ban can automatically ban it via iptables at the node level.
-
TLS & Let’s Encrypt Automation
- ACME HTTP-01 is straightforward, since the NGINX controller already serves port 80.
- cert-manager handles renewals ~30 days before expiration.
- The TLS Secret (
heunify-tls-secret) is updated transparently; NGINX reloads on secret change.
-
Namespace Isolation
- The Ingress Controller and cert-manager reside in dedicated namespaces (
ingress-nginx,cert-manager). - Application namespaces (
frontend,backend, etc.) remain focused on business logic. - RBAC boundaries ensure the controller only needs read access to Services/Secrets, while apps don’t modify controller configuration.
- The Ingress Controller and cert-manager reside in dedicated namespaces (
-
DNS Configuration
-
After Helm install, run:
kubectl get svc ingress-nginx-controller -n ingress-nginx -
Note the
EXTERNAL-IP(AWS ELB DNS). -
In Route 53 (or your provider), create:
heunify.com A (alias) → <ELB DNS> www.heunify.com A (alias) → <ELB DNS> api.heunify.com CNAME → <ELB DNS>
-
-
IP Whitelisting (Optional)
-
Currently set to allow all (
0.0.0.0/0). -
To restrict, replace with your corporate network:
nginx.ingress.kubernetes.io/whitelist-source-range: "203.0.113.0/24,198.51.100.0/24"
-
-
Monitoring & Logging
- Ingress-nginx can export Prometheus metrics.
- Forward access logs to a central system (e.g., Elasticsearch/Fluentd/Kibana stack or CloudWatch).
- Integrate with fail2ban for automated IP bans.
6. Extensibility for Future Services
Adding new services or subdomains follows the same pattern:
-
Create a new namespace, e.g.,
redis. -
Deploy your Service (e.g.,
redis-svc:6379). -
Create a new Ingress resource in namespace
redis, referencingheunify-tls-secret. Example:apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: redis-ingress namespace: redis annotations: kubernetes.io/ingress.class: "nginx" spec: tls: - hosts: - redis.heunify.com secretName: heunify-tls-secret rules: - host: redis.heunify.com http: paths: - path: / pathType: Prefix backend: service: name: redis-svc port: number: 6379 -
Add DNS record
redis.heunify.com CNAME → <ELB DNS>
If desired, you can create individual Certificates per-namespace rather than one mega-certificate. Cert-manager allows that by referencing the same ClusterIssuer in each namespace. However, a multi-SAN certificate is simpler until you hit Let’s Encrypt’s limit (100 names per certificate).
7. Conclusion
This architecture combines:
- NGINX Ingress Controller (Helm-managed, AWS ELB fronted)
- cert-manager (ClusterIssuer + Certificate for automation)
- Namespace-scoped Ingress resources with rate-limits, blacklists, path-based routing
- Single AWS ELB as the public entry point
Result:
- A single unified entry point (ELB DNS) for all HTTP/HTTPS traffic.
- Automatic Let’s Encrypt TLS with zero manual renewals.
- Granular, per-host, per-path routing to multiple backend services.
- Built-in security: connection limits, request rate limits, IP blocking for known attack patterns.
- Horizontal scalability: add more Ingress resources in new namespaces for future microservices, all served by the same NGINX controller and certificate.
By following these principles, you gain a secure, maintainable, and highly available ingress layer on Kubernetes.
Continue reading
More systemJoin the Discussion
Share your thoughts and insights about this system.