Scope: Self-managed Kubernetes on two AWS EC2 Ubuntu nodes; install the community ingress-nginx v1.15.1 bare-metal/NodePort manifest, repair cross-node Calico networking, deploy two NGINX versions behind ClusterIP Services, configure path-based routing with URL rewriting, and validate each layer.
Production warning (November 2026): The community
kubernetes/ingress-nginxproject was retired in March 2026 and no longer receives security fixes. The exact v1.15.1 manifest below reproduces this existing training/lab cluster only; do not deploy it as a new production ingress solution. For new production deployments, evaluate a maintained Gateway API implementation or actively supported Ingress controller. See Kubernetes retirement announcement.
Learning objectives
By the end, you will know how to:
- Install the ingress-nginx controller and understand its NodePort and admission-webhook Services.
- Identify a Calico networking failure using routes, Pod IPs, and webhook timeouts.
- Enable VXLAN on an existing Calico IPPoolย when that is appropriate for your cluster.
- Deployย
app1ย andยapp2ย usingยscmgalaxy/nginx-devopsschoolv1ย andยscmgalaxy/nginx-devopsschoolv2. - Routeย
/app1ย andย/app2ย through aย single Ingressย with NGINX-specific path rewriting. - Validate Kubernetes resources, networking, HTTP responses, and cleanup.
Lab environment and architecture
These addresses and ports are from the example lab. Replace them with values from your own cluster.
| Component | Lab value | Purpose |
|---|---|---|
| Kubernetes | v1.35.10 | Self-managed cluster |
| Control-plane node | 172.31.10.122 | Run kubectl and host networking tests |
| Worker node | 172.31.13.14 | Runs the Ingress controller in this lab |
| CNI | Calico | Pod networking |
| Calico Pod CIDR | 192.168.0.0/16 | Pod address pool |
| Ingress controller | ingress-nginx v1.15.1 | Lab controller, now unmaintained |
| Ingress Controller Service | NodePort | HTTP/HTTPS external entry point |
| Lab HTTP NodePort | 31960 | Discover dynamically; value may differ |
| Admission Service IP | 10.101.95.207:443 | Kubernetes API server -> webhook |
| Admission Pod IP | 192.168.131.65:8443 | Example worker Pod endpoint |
| App1 | scmgalaxy/nginx-devopsschoolv1 | First demo page |
| App2 | scmgalaxy/nginx-devopsschoolv2 | Second demo page |
Browser / curl
|
http://WORKER_IP:HTTP_NODEPORT
|
v
ingress-nginx controller
(NodePort Service)
|
Ingress: path-based-ingress
/ \
/app1(/|$)(.*) /app2(/|$)(.*)
rewrite to / rewrite to /
| |
v v
app1-service app2-service
ClusterIP ClusterIP
| |
v v
app1 Pod app2 Pod
NGINX image v1 NGINX image v2
Kubernetes API server -> admission ClusterIP:443 -> controller Pod:8443
Inter-node Pod packets -> Calico VXLAN over UDP 4789 (in this lab)
Why NodePort? This is a manually created Kubernetes cluster on EC2, not an EKS cluster automatically provisioning an AWS Load Balancer. The bare-metal ingress-nginx manifest exposes a NodePort Service. ClusterIP application Services remain private. EC2 private IPs are accessible only from networks that can reach the VPC; public Internet access also requires suitable networking and narrowly scoped security group rules.
Prerequisites
- An existing, working Kubernetes cluster with theย
kubectlย context set to the correct cluster. - Cluster-admin permissions for installing the controller and changing Calico resources.
- Two schedulable nodes (the demo can run on one, but cross-node networking troubleshooting requires at least two).
- Calico already installed if using the Calico-specific repair steps;ย this tutorial does not install Calico from scratch.
- EC2 security groups and NACLs allowing the required node-to-node traffic (seeย Phase 2).
curl,ยiproute2, and optionallyยtcpdumpย on the control-plane host.
kubectl config current-context
kubectl get nodes -o wide
kubectl get pods -n kube-system -o wide
kubectl get ingressclasses
Code language: JavaScript (javascript)
For clean file organization:
mkdir -p ~/ingress-path-lab
cd ~/ingress-path-lab
Phase 1 โ Install the ingress-nginx controller
1.1 Apply the exact lab manifest
Lab-only / legacy installation. This is the version used in the original demonstration.
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.15.1/deploy/static/provider/baremetal/deploy.yaml
Code language: JavaScript (javascript)
This creates resources including the ingress-nginx namespace, controller Deployment, Services, IngressClass, admission webhook configuration, certificate Jobs and RBAC.
1.2 Verify controller and webhook are ready
kubectl rollout status deployment/ingress-nginx-controller \
-n ingress-nginx --timeout=180s
kubectl get pods -n ingress-nginx -o wide
kubectl get svc -n ingress-nginx
kubectl get ingressclass
kubectl get validatingwebhookconfigurations
kubectl get jobs -n ingress-nginx
Code language: JavaScript (javascript)
Expected resource names (actual NodePort numbers vary):
ingress-nginx-controller NodePort ... 80:<HTTP_NODEPORT>/TCP,443:<HTTPS_NODEPORT>/TCP
ingress-nginx-controller-admission ClusterIP ... 443/TCP
IngressClass: nginx -> k8s.io/ingress-nginx
Code language: HTML, XML (xml)
The admission webhook listens inside the controller Pod on TCP 8443; the admission Service exposes it at TCP 443. These are not the public application ports.
1.3 Discover the NodePort instead of hardcoding it
HTTP_NODEPORT=$(kubectl get svc ingress-nginx-controller -n ingress-nginx \
-o jsonpath='{.spec.ports[?(@.name=="http")].nodePort}')
HTTPS_NODEPORT=$(kubectl get svc ingress-nginx-controller -n ingress-nginx \
-o jsonpath='{.spec.ports[?(@.name=="https")].nodePort}')
kubectl get svc ingress-nginx-controller -n ingress-nginx
printf 'HTTP NodePort: %s\nHTTPS NodePort: %s\n' "$HTTP_NODEPORT" "$HTTPS_NODEPORT"
Code language: PHP (php)
In this example, HTTP was 31960 and HTTPS was 30251.
Phase 2 โ Diagnose and fix the Calico cross-node routing issue
Only apply this change if you have the same problem. A healthy Calico cluster can use VXLAN, IP-in-IP, or unencapsulated routing with correctly configured BGP/underlay routes.
ipipMode: NeverandvxlanMode: Neverare not inherently invalid; the failure in this lab was that there was no functional route to the other node’s Pod CIDR.
2.1 The observed error
Attempting to create ing.yaml originally failed with:
Error from server (InternalError):
failed calling webhook "validate.nginx.ingress.kubernetes.io":
Post "https://ingress-nginx-controller-admission.ingress-nginx.svc:443/networking/v1/ingresses?timeout=10s":
context deadline exceeded
Code language: JavaScript (javascript)
The controller Pod was 1/1 Running, the admission Service and EndpointSlice existed, and the controller logged Starting validation webhook address=":8443".
Interpretation: A Running Pod and populated Service endpoints are not proof of network reachability from the API server to the webhook.
2.2 Inspect admission Service and routes
kubectl get svc ingress-nginx-controller-admission -n ingress-nginx -o wide
kubectl get endpointslices -n ingress-nginx \
-l kubernetes.io/service-name=ingress-nginx-controller-admission
kubectl get pods -n ingress-nginx -o wide
kubectl get validatingwebhookconfiguration ingress-nginx-admission -o yaml
Code language: JavaScript (javascript)
Find the webhook Pod IP from the EndpointSlice/Pod output. In the lab it was 192.168.131.65.
On the control-plane node:
ip route get 192.168.131.65
curl --noproxy '*' -kv --connect-timeout 5 \
https://192.168.131.65:8443/
curl --noproxy '*' -kv --connect-timeout 5 \
https://10.101.95.207:443/
Code language: PHP (php)
Before the fix, both curl commands timed out, and routing looked like:
192.168.131.65 via 172.31.0.1 dev enX0 src 172.31.10.122
Code language: CSS (css)
That was the AWS VPC gateway, not an appropriate configured overlay route to the remote Pod. This is a strong sign of missing remote Pod routing in this particular cluster, not a generic rule that any route through a gateway is wrong.
2.3 Check the existing Calico IPPool
kubectl get ippools.crd.projectcalico.org -o yaml
kubectl get pods -n kube-system -l k8s-app=calico-node -o wide
ip -d link show vxlan.calico || true
ip -d link show tunl0 || true
Code language: JavaScript (javascript)
Original lab IPPool:
metadata:
name: default-ipv4-ippool
spec:
cidr: 192.168.0.0/16
ipipMode: Never
vxlanMode: Never
natOutgoing: true
Code language: JavaScript (javascript)
No functioning cross-node Pod route was present. The vxlan.calico interface did not exist, and tunl0 was down.
2.4 Check AWS network requirements before making the change
For Calico VXLAN, allow UDP 4789 in both directions between the EC2 nodes (for example, using an appropriate EC2 security-group-to-security-group rule, or narrowly scoped private node IP rules). Check custom network ACLs and host firewalls as well.
| Calico transport | Node-to-node traffic |
|---|---|
| VXLAN | UDP 4789 |
| IP-in-IP | IP protocol number 4 |
| BGP | TCP 179 |
| Unencapsulated/direct routing | Pod CIDR reachability provided by the underlay and firewall rules |
Do not open UDP 4789 to the public Internet. Allow client NodePort HTTP access only from trusted IP ranges for this lab.
Operator-managed Calico: If you installed Calico through the Tigera operator, check the
Installationresource. Operator-managed pools should be changed viaInstallation.spec.calicoNetwork.ipPools, not patched directly; the operator can revert direct IPPool edits. The direct IPPool command below applies to this lab’s manifest-managed pool.
2.5 Back up and enable VXLAN Always (manifest-managed Calico)
This changes network dataplane behavior and can briefly interrupt live connections. Back up your configuration and use a maintenance window on shared clusters.
<em># Backup</em>
kubectl get ippools.crd.projectcalico.org \
default-ipv4-ippool -o yaml > calico-ippool-backup.yaml
<em># Verify the pool name and current mode before modifying it</em>
kubectl get ippools.crd.projectcalico.org \
default-ipv4-ippool \
-o jsonpath='{.spec.cidr}{" IPIP="}{.spec.ipipMode}{" VXLAN="}{.spec.vxlanMode}{"\n"}'
<em># Enable overlay routing on the existing IPPool</em>
kubectl patch ippools.crd.projectcalico.org default-ipv4-ippool \
--type=merge \
-p '{"spec":{"ipipMode":"Never","vxlanMode":"Always"}}'
Code language: PHP (php)
Why
Always, notCrossSubnet? The EC2 nodes are in the same VPC subnet (172.31.0.0/20).CrossSubnetmay skip encapsulation between them and therefore would not solve the missing Pod-CIDR underlay routing in this lab.Alwaysencapsulates cross-node traffic even when the nodes share a subnet.
For Tigera operator-managed Calico, instead inspect:
kubectl get installations.operator.tigera.io default -o yaml
Code language: CSS (css)
Use the operator’s documented encapsulation: VXLAN setting for the appropriate IPPool definition in that Installation resource. Do not apply the direct patch if the operator manages the pool.
2.6 Verify VXLAN and the new route
kubectl get ippools.crd.projectcalico.org default-ipv4-ippool -o yaml
ip -d link show vxlan.calico
ip route get 192.168.131.65
kubectl get pods -n kube-system -l k8s-app=calico-node -o wide
Code language: JavaScript (javascript)
Observed after the fix in this lab:
vxlan.calico: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1410
vxlan id 4096 local 172.31.10.122 dev enX0 dstport 4789
192.168.131.65 via 192.168.131.80 dev vxlan.calico src 192.168.90.202
Code language: CSS (css)
Important: The interface and route prove that VXLAN is configured; they do not alone prove packets cross between EC2 hosts. Retest both webhook endpoints:
curl --noproxy '*' -kv --connect-timeout 5 \
https://192.168.131.65:8443/
curl --noproxy '*' -kv --connect-timeout 5 \
https://10.101.95.207:443/
Code language: JavaScript (javascript)
Any genuine HTTP response (including an expected 404/400 for /) demonstrates reachability. The bare curl to / is not a functional admission-validation test; it simply tests connection and TLS. The definitive admission check is that the subsequent Ingress creation succeeds.
If timeouts continue: check both EC2 security groups, return traffic/NACLs and host firewalls, and capture traffic while making a request:
sudo tcpdump -ni enX0 udp port 4789
<em># In a second terminal, repeat curl to the Pod IP and Service IP.</em>
Code language: PHP (php)
If needed, review Calico logs:
kubectl logs -n kube-system -l k8s-app=calico-node \
--all-containers=false --tail=100
Code language: JavaScript (javascript)
MTU note from this lab
Calico also logged Failed to auto-detect host MTU - no interfaces matched the MTU interface pattern. The EC2 interface was named enX0; VXLAN later showed an MTU of 1410.
This is a separate item to review if you observe packet loss or large-request failures. Check the underlay interface MTU, tunnel MTU and Calico’s configured detection settings before making any changes. Do not blindly assign a new MTU to every cluster.
ip link show enX0
ip -d link show vxlan.calico
kubectl get felixconfigurations.crd.projectcalico.org -o yaml
kubectl get cm calico-config -n kube-system -o yaml 2>/dev/null || true
Code language: JavaScript (javascript)
Phase 3 โ Deploy two application Deployments and ClusterIP Services
3.1 Create apps.yaml
Use different Docker images so that you can recognize which backend answered each request.
cat > apps.yaml <<'YAML'
apiVersion: apps/v1
kind: Deployment
metadata:
name: app1
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: app1
template:
metadata:
labels:
app: app1
spec:
containers:
- name: nginx
image: scmgalaxy/nginx-devopsschoolv1
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: app1-service
namespace: default
spec:
type: ClusterIP
selector:
app: app1
ports:
- name: http
port: 80
targetPort: 80
protocol: TCP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: app2
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: app2
template:
metadata:
labels:
app: app2
spec:
containers:
- name: nginx
image: scmgalaxy/nginx-devopsschoolv2
ports:
- containerPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: app2-service
namespace: default
spec:
type: ClusterIP
selector:
app: app2
ports:
- name: http
port: 80
targetPort: 80
protocol: TCP
YAML
Code language: JavaScript (javascript)
3.2 Deploy and verify
kubectl apply -f apps.yaml
kubectl rollout status deployment/app1 --timeout=120s
kubectl rollout status deployment/app2 --timeout=120s
kubectl get deployments app1 app2
kubectl get pods -l 'app in (app1,app2)' -o wide
kubectl get svc app1-service app2-service
kubectl get endpointslices -l kubernetes.io/service-name=app1-service
kubectl get endpointslices -l kubernetes.io/service-name=app2-service
Code language: JavaScript (javascript)
Example ClusterIP addresses previously assigned in the lab:
app1-service ClusterIP 10.102.53.234 80/TCP
app2-service ClusterIP 10.103.156.112 80/TCP
They may change if Services are recreated. The Services must have ready Pod endpoints for requests to succeed.
3.3 Validate each Service before introducing Ingress
You can curl the ClusterIP from a cluster node if your node-to-Service networking is working:
curl -i --noproxy '*' http://10.102.53.234/
curl -i --noproxy '*' http://10.103.156.112/
Code language: JavaScript (javascript)
Or discover the current Service IPs automatically:
APP1_IP=$(kubectl get svc app1-service -o jsonpath='{.spec.clusterIP}')
APP2_IP=$(kubectl get svc app2-service -o jsonpath='{.spec.clusterIP}')
curl -i --noproxy '*' "http://${APP1_IP}/"
curl -i --noproxy '*' "http://${APP2_IP}/"
Code language: PHP (php)
Expected: Both root pages return HTTP 200 if the images serve /. App1 and App2 should show their respective version-specific content.
Phase 4 โ Configure path-based Ingress with URL rewriting
4.1 Understand why simple Prefix rules produced 404
The first Ingress used:
- path: /app1
pathType: Prefix
backend:
service:
name: app1-service
port:
number: 80
A Kubernetes Prefix match selects the backend Service but does not remove the prefix. The backend NGINX received /app1 instead of / and returned:
HTTP/1.1 404 Not Found
nginx/1.15.10
In this lab the controller reported NGINX 1.27.1, while the 404 page identified 1.15.10, which was a useful clue that the error came from the backend application, not the controller’s default backend. The specific version string can vary.
4.2 Create the final ing.yaml
This Ingress is controller-specific: the nginx.ingress.kubernetes.io/* annotations and regex capture rewrite depend on community ingress-nginx. Other ingress controllers may require different configuration.
cat > ing.yaml <<'YAML'
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: path-based-ingress
namespace: default
annotations:
nginx.ingress.kubernetes.io/use-regex: "true"
nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
ingressClassName: nginx
rules:
- http:
paths:
- path: /app1(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: app1-service
port:
number: 80
- path: /app2(/|$)(.*)
pathType: ImplementationSpecific
backend:
service:
name: app2-service
port:
number: 80
YAML
Why ImplementationSpecific? Kubernetes native path types are Exact, Prefix, and ImplementationSpecific. The NGINX-specific regular expression must be handled by the controller, rather than interpreted as a Kubernetes-native Prefix rule.
Why /$2? In each expression, (/|$) is capture group $1, and (.*) is capture group $2. Ingress-nginx rewrites the forwarded URL to / plus the content of $2.
| Incoming path | Selected Service | Path the Pod receives |
|---|---|---|
/app1 | app1-service:80 | / |
/app1/ | app1-service:80 | / |
/app1/index.html | app1-service:80 | /index.html |
/app2 | app2-service:80 | / |
/app2/health | app2-service:80 | /health |
/app3 | No matching application rule | Controller default behavior (typically 404) |
4.3 Apply and verify the Ingress
kubectl apply --dry-run=client -f ing.yaml
kubectl apply -f ing.yaml
kubectl get ingress
kubectl describe ingress path-based-ingress
kubectl get ingress path-based-ingress -o yaml
Code language: JavaScript (javascript)
You should see IngressClass nginx, paths for both Services, and the two rewrite annotations.
If kubectl apply fails with an admission-webhook timeout, return to Phase 2. Do not delete the webhook as a normal fix. Admission validation is useful and should stay enabled.
Phase 5 โ End-to-end validation
5.1 Confirm networking, backend health, and controller
kubectl get nodes -o wide
kubectl get pods -n ingress-nginx -o wide
kubectl get pods -l 'app in (app1,app2)' -o wide
kubectl get svc app1-service app2-service
kubectl get ingress path-based-ingress
kubectl get ingressclass nginx
Code language: JavaScript (javascript)
5.2 Discover NodePort and worker node address
HTTP_NODEPORT=$(kubectl get svc ingress-nginx-controller -n ingress-nginx \
-o jsonpath='{.spec.ports[?(@.name=="http")].nodePort}')
WORKER_IP=172.31.13.14 <em># Replace with a reachable node private IP</em>
printf 'Testing http://%s:%s\n' "$WORKER_IP" "$HTTP_NODEPORT"
Code language: PHP (php)
Example: http://172.31.13.14:31960.
5.3 Test both paths
curl -i --noproxy '*' "http://${WORKER_IP}:${HTTP_NODEPORT}/app1"
curl -i --noproxy '*' "http://${WORKER_IP}:${HTTP_NODEPORT}/app2"
Code language: JavaScript (javascript)
Expected: HTTP/1.1 200 OK and the body of NGINX demo v1 at /app1 and demo v2 at /app2 (assuming both images serve their root pages correctly).
Additional checks:
<em># Rewritten to /index.html by ingress-nginx</em>
curl -i --noproxy '*' "http://${WORKER_IP}:${HTTP_NODEPORT}/app1/index.html"
curl -i --noproxy '*' "http://${WORKER_IP}:${HTTP_NODEPORT}/app2/index.html"
<em># Negative test โ no matching rule</em>
curl -i --noproxy '*' "http://${WORKER_IP}:${HTTP_NODEPORT}/does-not-exist"
Code language: PHP (php)
5.4 Check from a local laptop if EC2 NodePort is not directly reachable
The lab uses private EC2 IP addresses. If your laptop is not connected to the VPC, use SSH/VPN/SSM to access a node, or run this temporary port-forward from a machine where kubectl can reach the cluster:
kubectl -n ingress-nginx port-forward \
svc/ingress-nginx-controller 8080:80
From another terminal on that same machine:
curl -i http://127.0.0.1:8080/app1
curl -i http://127.0.0.1:8080/app2
Code language: JavaScript (javascript)
Port forwarding does not require exposing the NodePort to the public Internet.
5.5 Confirm which backend handled the request
kubectl get pods -l app=app1 -o wide
kubectl get pods -l app=app2 -o wide
kubectl logs deployment/app1 --tail=30
kubectl logs deployment/app2 --tail=30
kubectl logs -n ingress-nginx \
deployment/ingress-nginx-controller --tail=60
Code language: JavaScript (javascript)
Compare page content and each application’s access logs after hitting /app1 and /app2. Different Docker images make routing demonstrations much clearer.
Phase 6 โ Troubleshooting reference
| Symptom | Likely cause | Checks / fix |
|---|---|---|
failed calling webhook ... context deadline exceeded | API server cannot reach admission Service | Verify EndpointSlice, Pod route, CNI, kube-proxy, security groups, TCP 8443 / Service 443 |
| Pod IP and ClusterIP both time out from control plane | Cross-node Pod networking/underlay or firewall issue | ip route get POD_IP; verify Calico overlay and UDP 4789 |
vxlan.calico missing after patch | Calico not configured/reconciled for VXLAN | IPPool mode, operator ownership, calico-node logs and DaemonSet settings |
vxlan.calico is UP but curl still times out | Packets blocked or reverse path broken | EC2 SG/NACL, host firewall, tcpdump -ni enX0 udp port 4789 |
Ingress admission succeeds but /app1 is 404 from backend NGINX | Prefix path was not removed | Use regex pattern and rewrite annotation in Phase 4 |
Ingress returns 404 for /app3 | No matching rule | Expected unless another/default backend is configured |
| HTTP 503 from Ingress | Service has no ready backends, wrong name/port, or backend unreachable | kubectl get endpointslices; Pod readiness; service selectors |
| NodePort request cannot connect | Wrong NodePort, no reachable node IP, or blocked ingress traffic | Query actual Service NodePort; test from node/VPC or port-forward |
| 502/504 or intermittent issues | Backend/process failure, CNI problems, traffic policies | Logs, Pod health, NetworkPolicies, MTU and packet captures |
LoadBalancer Service EXTERNAL-IP is <pending> | No cloud LB integration in self-managed cluster | For this lab use the ingress-nginx NodePort Service |
Deep-dive diagnostic commands
<em># Ingress details</em>
kubectl describe ingress path-based-ingress
kubectl get ingress path-based-ingress -o yaml
<em># Controller and admission endpoints</em>
kubectl get svc -n ingress-nginx
kubectl get endpointslices -n ingress-nginx \
-l kubernetes.io/service-name=ingress-nginx-controller-admission
kubectl logs -n ingress-nginx deployment/ingress-nginx-controller --tail=100
<em># Networking and routes</em>
kubectl get ippools.crd.projectcalico.org -o yaml
kubectl get pods -n kube-system -o wide
ip route
ip -d link show vxlan.calico
<em># Backend Pods and endpoints</em>
kubectl describe deployment app1
kubectl describe deployment app2
kubectl get endpointslices -l kubernetes.io/service-name=app1-service
kubectl get endpointslices -l kubernetes.io/service-name=app2-service
Code language: HTML, XML (xml)
Avoid the unsafe shortcut: Changing the admission webhook to
failurePolicy: Ignoreis not a networking fix. It can mask errors. If temporarily used for an isolated lab diagnosis, restore the original validation policy afterward and verify that admission calls succeed normally.
Phase 7 โ Optional cleanup and rollback
Delete only the demo Ingress and applications
kubectl delete -f ing.yaml
kubectl delete -f apps.yaml
Code language: JavaScript (javascript)
Remove the lab controller only if it is not used elsewhere
kubectl delete -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.15.1/deploy/static/provider/baremetal/deploy.yaml
Code language: JavaScript (javascript)
Caution: This can disrupt other Ingress resources that depend on the same controller. Do not remove it on a shared cluster without checking usage.
Roll back Calico encapsulation only when you have a working alternative route
Do not blindly revert VXLAN to Never: that would recreate the original cross-node failure unless you first set up working BGP/unencapsulated routing or an alternative overlay. Keep the backup file for review; do not apply a stale backup automatically to a changed production cluster.
Final success checklist
- [ ]ย
kubectl get nodesย shows both nodesยReady. - [ ]ย
ingress-nginx-controllerย is ready and itsยNodePortย Service exists. - [ ] Calico IPPool encapsulation is intentional, with required node-to-node transport allowed.
- [ ]ย
ip route get <REMOTE_POD_IP>ย shows a usable route to the remote Pod. - [ ] Admission webhook Service/Pod TCP connectivity works from the control plane.
- [ ] Bothย
app1ย andยapp2ย Deployments are ready. - [ ] Both ClusterIP Services have ready Pod endpoints.
- [ ]ย
path-based-ingressย was accepted by Kubernetes admission validation. - [ ]ย
GET /app1ย returns application v1 content. - [ ]ย
GET /app2ย returns application v2 content. - [ ]ย
GET /app1/index.htmlย andย/app2/index.htmlย work with URL rewriting.
Command summary
<em># 1. Controller (legacy lab-only)</em>
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.15.1/deploy/static/provider/baremetal/deploy.yaml
<em># 2. Calico: only if the manifest-managed IPPool lacks working cross-node routing</em>
kubectl patch ippools.crd.projectcalico.org default-ipv4-ippool \
--type=merge -p '{"spec":{"ipipMode":"Never","vxlanMode":"Always"}}'
<em># 3. Applications</em>
kubectl apply -f apps.yaml
<em># 4. Ingress</em>
kubectl apply -f ing.yaml
<em># 5. Verify</em>
kubectl get pods,deploy,svc,ingress
kubectl get svc -n ingress-nginx
curl -i http://172.31.13.14:31960/app1
curl -i http://172.31.13.14:31960/app2
Code language: PHP (php)
The two YAML manifests are supplied in full in Phases 3 and 4. The 31960 port and 172.31.13.14 address in the summary are the example lab valuesโdiscover your actual NodePort and node address before testing.
References
- Kubernetes โ Ingress concepts and path types
- ingress-nginx โ Installation on bare-metal clusters (archived documentation)
- ingress-nginx โ Rewrite example and capture groups
- Calico โ Overlay networking with VXLAN and IP-in-IP
- Calico โ Network port requirements
- Calico โ IPPool reference
- Kubernetes โ Ingress NGINX retirement announcement
- Kubernetes โ January 2026 retirement/security statement
Lab snapshot and troubleshooting history: November 12, 2026. Addresses, resource names, versions, and port numbers reflect the example cluster unless explicitly described as dynamic.
I’m Rajesh Kumar, a DevOps, SRE, DevSecOps, Cloud, and Platform Engineering expert passionate about sharing practical knowledge, real-world experiences, and industry best practices. I have worked at Cotocus and regularly write about technology, travel, investing, health, product reviews, and digital marketing through my various platforms.
I publish technical articles at DevOps School, travel stories at Holiday Landmark, stock market insights at Stocks Mantra, health and fitness guidance at My Medic Plus, product reviews at TrueReviewNow, and SEO and digital marketing strategies at Wizbrand.
Find Trusted Cardiac Hospitals
Compare heart hospitals by city and services โ all in one place.
Explore Hospitals