Phase 02 · Local Networking
NGINX Ingress — deep dive
ansible/roles/nginx-ingress/  ·  Helm v4.11.3 · ingress-nginx
MetalLB gives the cluster one real IP. NGINX Ingress sits behind that IP and routes traffic to the right service based on the hostname. Without it, every service would need its own IP and port — instead, everything flows through one entry point and gets routed by domain name.
01 LoadBalancer vs Ingress — Why We Need Both
Without Ingress — one IP per service (not scalable)
────────────────────────────────────────────────────────────
192.168.56.200  →  ArgoCD
192.168.56.201  →  Grafana
192.168.56.202  →  Online Boutique
... one MetalLB IP consumed per service


With Ingress — one IP, routing by hostname
────────────────────────────────────────────────────────────
                    192.168.56.200  (MetalLB assigns this to NGINX)
                           │
               ┌───────────┼───────────┐
               ▼           ▼           ▼
       argocd         grafana         app
       .lab.local      .lab.local      .lab.local
           ↓               ↓               ↓
       ArgoCD svc      Grafana svc     Boutique svc

A LoadBalancer service gives you direct access to one service on one IP. An Ingress is a routing layer — it sits in front of many services and dispatches requests by hostname, so you only consume one LoadBalancer IP for the entire cluster's external traffic.

02 Installation via Helm
# roles/nginx-ingress/tasks/main.yml

# 1. Install Helm on the master VM
- name: Install Helm
  shell: curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash
  args:
    creates: /usr/local/bin/helm   # idempotent — skip if already installed

# 2. Add the ingress-nginx chart repo
- name: Add ingress-nginx Helm repo
  command: helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx

# 3. Install the chart
- name: Install NGINX Ingress Controller
  command: >
    helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx
    --namespace ingress-nginx
    --create-namespace
    --version 4.11.3
    --set controller.service.type=LoadBalancer
    --set controller.admissionWebhooks.enabled=false
    --timeout 10m0s
  • get-helm-3 install script — the official Helm installer. Detects the OS and architecture, downloads the correct binary, and places it in /usr/local/bin/helm. The creates: argument makes it idempotent — skipped if the binary already exists.
  • helm repo add — adds the official ingress-nginx chart repository. This is maintained by the Kubernetes community (not NGINX Inc — different project with a different chart).
  • helm upgrade --install — installs the chart if it doesn't exist, upgrades it if it does. This single command is idempotent and safe to re-run.
  • --create-namespace — creates the ingress-nginx namespace if it doesn't exist. Without this flag, the install fails if the namespace is absent.
  • controller.service.type=LoadBalancer — the key setting. This creates a LoadBalancer service for the NGINX controller, which MetalLB sees and assigns an IP from the pool. Without this it defaults to NodePort.
  • controller.admissionWebhooks.enabled=false — disables the admission webhook and its pre-install validation job. On resource-constrained local VMs the job can't start within the default 5-minute timeout, causing the entire install to fail. Ingress resources are still accepted — validation just isn't enforced server-side.
  • --timeout 10m0s — extends the Helm operation timeout from the default 5m to 10 minutes. Gives the controller pod enough time to pull its image and reach Running state on slow or cold VMs.
03 What Gets Created in the Cluster
# After helm install, inside the cluster:
# (admissionWebhooks disabled — only one service created)

NAMESPACE       NAME                        TYPE           CLUSTER-IP       EXTERNAL-IP      PORT(S)
ingress-nginx   ingress-nginx-controller    LoadBalancer   10.103.174.240   192.168.56.200   80:31787/TCP,443:31809/TCP

# The controller pod:
ingress-nginx   ingress-nginx-controller-7bf6f99bc6-tlgm5   Running   1/1
  • ingress-nginx-controller (LoadBalancer) — this is the service MetalLB assigns an IP to. All external HTTP/HTTPS traffic enters the cluster through this single IP on ports 80 (HTTP) and 443 (HTTPS).
  • No admission service — because admissionWebhooks.enabled=false, the ingress-nginx-controller-admission ClusterIP service and its validation job are not created. Ingress objects are still accepted — they just aren't validated server-side before admission.
  • ingress-nginx-controller pod — the actual NGINX process running inside Kubernetes. It watches for Ingress objects cluster-wide and auto-generates its NGINX config from them. No restart needed when Ingress rules change.
04 How Ingress Resources Work
# Example Ingress resource (used in Phase 03 + 04)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: argocd-ingress
  annotations:
    kubernetes.io/ingress.class: "nginx"
spec:
  rules:
    - host: argocd.lab.local
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: argocd-server
                port:
                  number: 80

You don't configure NGINX directly — you create Ingress objects in Kubernetes and the controller auto-generates NGINX config from them. Every time you apply an Ingress resource, NGINX reloads without any restarts.

  • host: argocd.lab.local — NGINX matches incoming requests by the HTTP Host header. When a request arrives for argocd.lab.local, it proxies it to the argocd-server service.
  • pathType: Prefix — matches any path starting with / (i.e., everything). You can get more specific with exact paths or regex for complex routing.
  • kubernetes.io/ingress.class: nginx — tells the NGINX Ingress controller to manage this resource. If you had multiple ingress controllers installed, this annotation picks the right one.
05 /etc/hosts — Local DNS
# C:\Windows\System32\drivers\etc\hosts  (open as Administrator)

# k8s-observability-platform — lab.local domains
192.168.56.200  argocd.lab.local
192.168.56.200  grafana.lab.local
192.168.56.200  app.lab.local

lab.local is not a real DNS domain — no DNS server on the internet knows about it. The hosts file is your OS's local DNS override: before asking any DNS server, your OS checks this file first.

  • All three point to the same IP192.168.56.200 (or whatever MetalLB assigned). They all arrive at NGINX, which then routes each to the correct service by hostname.
  • Must open Notepad as Administrator — the hosts file is write-protected on Windows. Right-click Notepad → "Run as administrator" → File → Open the hosts file path.
  • Why not a real domain? — no cost, no DNS propagation wait, works offline, and stays 100% local. For a real cluster you'd set up proper DNS or use something like nip.io.
Your actual IP may differ. MetalLB assigns the first available IP from the pool — usually 192.168.56.200 but check the Ansible debug output or run kubectl get svc -n ingress-nginx to confirm the actual assigned IP before updating your hosts file.
06 End-to-End Traffic Flow
Browser: http://grafana.lab.local
         │
         │  1. OS checks hosts file
         │     grafana.lab.local → 192.168.56.200
         │
         │  2. ARP: who has 192.168.56.200?
         │     MetalLB Speaker replies → k8s-master MAC
         │
         ▼
192.168.56.200:80  (ingress-nginx-controller LoadBalancer service)
         │
         │  3. kube-proxy DNAT → NGINX pod
         │
         ▼
NGINX Ingress Controller
         │
         │  4. Reads Host header: grafana.lab.local
         │     Matches Ingress rule → grafana service:80
         │
         ▼
Grafana Pod  (Phase 04)
07 Verify Phase 02 is Working
# From inside k8s-master (vagrant ssh k8s-master)

# MetalLB components
kubectl get pods -n metallb-system
kubectl get ipaddresspool -n metallb-system
kubectl get l2advertisement -n metallb-system

# NGINX Ingress — confirm LoadBalancer got an IP
kubectl get svc -n ingress-nginx

# Expected output:
# NAME                       TYPE           EXTERNAL-IP      PORT(S)
# ingress-nginx-controller   LoadBalancer   192.168.56.200   80:xxx/TCP,443:xxx/TCP

# From your laptop — test NGINX responds
# curl http://192.168.56.200  (should return 404 — no Ingress rules yet)

A 404 from NGINX at the IP is actually a success at this stage — it means NGINX is reachable and running, but no Ingress rules exist yet to route the request anywhere. Those get created in Phase 03 and 04 when services are deployed.