{"id":79852,"date":"2026-11-12T09:38:29","date_gmt":"2026-11-12T09:38:29","guid":{"rendered":"https:\/\/www.devopsschool.com\/blog\/?p=79852"},"modified":"2026-11-12T09:38:31","modified_gmt":"2026-11-12T09:38:31","slug":"kubernetes-ingress-controller-using-path-based-routing-complete-hands-on-lab-with-calico-vxlan","status":"publish","type":"post","link":"https:\/\/www.devopsschool.com\/blog\/kubernetes-ingress-controller-using-path-based-routing-complete-hands-on-lab-with-calico-vxlan\/","title":{"rendered":"Kubernetes Ingress Controller using Path-Based Routing: Complete Hands-On Lab with Calico VXLAN"},"content":{"rendered":"\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>Scope:<\/strong>&nbsp;Self-managed Kubernetes on two AWS EC2 Ubuntu nodes; install the community ingress-nginx&nbsp;<strong>v1.15.1<\/strong>&nbsp;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.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Production warning (November 2026):<\/strong>&nbsp;The community&nbsp;<code>kubernetes\/ingress-nginx<\/code>&nbsp;project was&nbsp;<strong>retired in March 2026<\/strong>&nbsp;and no longer receives security fixes. The exact v1.15.1 manifest below reproduces this existing&nbsp;<strong>training\/lab<\/strong>&nbsp;cluster only;&nbsp;<strong>do not deploy it as a new production ingress solution<\/strong>. For new production deployments, evaluate a maintained Gateway API implementation or actively supported Ingress controller. See&nbsp;<a href=\"https:\/\/kubernetes.io\/blog\/2025\/11\/11\/ingress-nginx-retirement\/\">Kubernetes retirement announcement<\/a>.<\/p>\n<\/blockquote>\n\n\n\n<h2 class=\"wp-block-heading\">Learning objectives<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">By the end, you will know how to:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Install the ingress-nginx controller and understand its NodePort and admission-webhook Services.<\/li>\n\n\n\n<li>Identify a Calico networking failure using routes, Pod IPs, and webhook timeouts.<\/li>\n\n\n\n<li>Enable VXLAN on an existing Calico IPPool\u00a0<strong>when that is appropriate for your cluster<\/strong>.<\/li>\n\n\n\n<li>Deploy\u00a0<code>app1<\/code>\u00a0and\u00a0<code>app2<\/code>\u00a0using\u00a0<code>scmgalaxy\/nginx-devopsschoolv1<\/code>\u00a0and\u00a0<code>scmgalaxy\/nginx-devopsschoolv2<\/code>.<\/li>\n\n\n\n<li>Route\u00a0<code>\/app1<\/code>\u00a0and\u00a0<code>\/app2<\/code>\u00a0through a\u00a0<strong>single Ingress<\/strong>\u00a0with NGINX-specific path rewriting.<\/li>\n\n\n\n<li>Validate Kubernetes resources, networking, HTTP responses, and cleanup.<\/li>\n<\/ol>\n\n\n\n<h2 class=\"wp-block-heading\">Lab environment and architecture<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">These addresses and ports are from the example lab.&nbsp;<strong>Replace them with values from your own cluster.<\/strong><\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">Component<\/th><th class=\"has-text-align-left\" data-align=\"left\">Lab value<\/th><th class=\"has-text-align-left\" data-align=\"left\">Purpose<\/th><\/tr><\/thead><tbody><tr><td>Kubernetes<\/td><td>v1.35.10<\/td><td>Self-managed cluster<\/td><\/tr><tr><td>Control-plane node<\/td><td><code>172.31.10.122<\/code><\/td><td>Run&nbsp;<code>kubectl<\/code>&nbsp;and host networking tests<\/td><\/tr><tr><td>Worker node<\/td><td><code>172.31.13.14<\/code><\/td><td>Runs the Ingress controller in this lab<\/td><\/tr><tr><td>CNI<\/td><td>Calico<\/td><td>Pod networking<\/td><\/tr><tr><td>Calico Pod CIDR<\/td><td><code>192.168.0.0\/16<\/code><\/td><td>Pod address pool<\/td><\/tr><tr><td>Ingress controller<\/td><td>ingress-nginx&nbsp;<code>v1.15.1<\/code><\/td><td>Lab controller, now unmaintained<\/td><\/tr><tr><td>Ingress Controller Service<\/td><td><code>NodePort<\/code><\/td><td>HTTP\/HTTPS external entry point<\/td><\/tr><tr><td>Lab HTTP NodePort<\/td><td><code>31960<\/code><\/td><td>Discover dynamically; value may differ<\/td><\/tr><tr><td>Admission Service IP<\/td><td><code>10.101.95.207:443<\/code><\/td><td>Kubernetes API server -&gt; webhook<\/td><\/tr><tr><td>Admission Pod IP<\/td><td><code>192.168.131.65:8443<\/code><\/td><td>Example worker Pod endpoint<\/td><\/tr><tr><td>App1<\/td><td><code>scmgalaxy\/nginx-devopsschoolv1<\/code><\/td><td>First demo page<\/td><\/tr><tr><td>App2<\/td><td><code>scmgalaxy\/nginx-devopsschoolv2<\/code><\/td><td>Second demo page<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<pre class=\"wp-block-preformatted\"> <code>                        Browser \/ curl\n                               |\n                 http:\/\/WORKER_IP:HTTP_NODEPORT\n                               |\n                               v\n                   ingress-nginx controller\n                        (NodePort Service)\n                               |\n                   Ingress: path-based-ingress\n                        \/             \\\n             \/app1(\/|$)(.*)         \/app2(\/|$)(.*)\n                rewrite to \/          rewrite to \/\n                       |                 |\n                       v                 v\n                 app1-service       app2-service\n                  ClusterIP          ClusterIP\n                       |                 |\n                       v                 v\n                   app1 Pod           app2 Pod\n                 NGINX image v1     NGINX image v2\n\n   Kubernetes API server -&gt; admission ClusterIP:443 -&gt; controller Pod:8443\n   Inter-node Pod packets -&gt; Calico VXLAN over UDP 4789 (in this lab)\n<\/code><\/pre>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>Why NodePort?<\/strong>&nbsp;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.<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Prerequisites<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>An existing, working Kubernetes cluster with the\u00a0<code>kubectl<\/code>\u00a0context set to the correct cluster.<\/li>\n\n\n\n<li>Cluster-admin permissions for installing the controller and changing Calico resources.<\/li>\n\n\n\n<li>Two schedulable nodes (the demo can run on one, but cross-node networking troubleshooting requires at least two).<\/li>\n\n\n\n<li>Calico already installed if using the Calico-specific repair steps;\u00a0<strong>this tutorial does not install Calico from scratch<\/strong>.<\/li>\n\n\n\n<li>EC2 security groups and NACLs allowing the required node-to-node traffic (see\u00a0<strong>Phase 2<\/strong>).<\/li>\n\n\n\n<li><code>curl<\/code>,\u00a0<code>iproute2<\/code>, and optionally\u00a0<code>tcpdump<\/code>\u00a0on the control-plane host.<\/li>\n<\/ul>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">kubectl config current-context\nkubectl <span class=\"hljs-keyword\">get<\/span> nodes -o wide\nkubectl <span class=\"hljs-keyword\">get<\/span> pods -n kube-system -o wide\nkubectl <span class=\"hljs-keyword\">get<\/span> ingressclasses\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">For clean file organization:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">mkdir -p ~\/ingress-path-lab\ncd ~\/ingress-path-lab\n<\/code><\/span><\/pre>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Phase 1 \u2014 Install the ingress-nginx controller<\/h1>\n\n\n\n<h2 class=\"wp-block-heading\">1.1 Apply the exact lab manifest<\/h2>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>Lab-only \/ legacy installation.<\/strong>&nbsp;This is the version used in the original demonstration.<\/p>\n<\/blockquote>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">kubectl apply -f https:<span class=\"hljs-comment\">\/\/raw.githubusercontent.com\/kubernetes\/ingress-nginx\/controller-v1.15.1\/deploy\/static\/provider\/baremetal\/deploy.yaml<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">This creates resources including the&nbsp;<code>ingress-nginx<\/code>&nbsp;namespace, controller Deployment, Services, IngressClass, admission webhook configuration, certificate Jobs and RBAC.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">1.2 Verify controller and webhook are ready<\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">kubectl rollout status deployment\/ingress-nginx-controller \\\n  -n ingress-nginx --timeout=<span class=\"hljs-number\">180<\/span>s\n\nkubectl <span class=\"hljs-keyword\">get<\/span> pods -n ingress-nginx -o wide\nkubectl <span class=\"hljs-keyword\">get<\/span> svc -n ingress-nginx\nkubectl <span class=\"hljs-keyword\">get<\/span> ingressclass\nkubectl <span class=\"hljs-keyword\">get<\/span> validatingwebhookconfigurations\nkubectl <span class=\"hljs-keyword\">get<\/span> jobs -n ingress-nginx\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Expected resource names (actual NodePort numbers vary):<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\">ingress-nginx-controller              NodePort     ...    80:<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">HTTP_NODEPORT<\/span>&gt;<\/span>\/TCP,443:<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">HTTPS_NODEPORT<\/span>&gt;<\/span>\/TCP\ningress-nginx-controller-admission    ClusterIP    ...    443\/TCP\n\nIngressClass: nginx  -&gt;  k8s.io\/ingress-nginx\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">The admission webhook listens&nbsp;<strong>inside the controller Pod on TCP 8443<\/strong>; the admission Service exposes it at&nbsp;<strong>TCP 443<\/strong>. These are&nbsp;<strong>not<\/strong>&nbsp;the public application ports.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">1.3 Discover the NodePort instead of hardcoding it<\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">HTTP_NODEPORT=$(kubectl get svc ingress-nginx-controller -n ingress-nginx \\\n  -o jsonpath=<span class=\"hljs-string\">'{.spec.ports&#91;?(@.name==\"http\")].nodePort}'<\/span>)\n\nHTTPS_NODEPORT=$(kubectl get svc ingress-nginx-controller -n ingress-nginx \\\n  -o jsonpath=<span class=\"hljs-string\">'{.spec.ports&#91;?(@.name==\"https\")].nodePort}'<\/span>)\n\nkubectl get svc ingress-nginx-controller -n ingress-nginx\nprintf <span class=\"hljs-string\">'HTTP NodePort: %s\\nHTTPS NodePort: %s\\n'<\/span> <span class=\"hljs-string\">\"$HTTP_NODEPORT\"<\/span> <span class=\"hljs-string\">\"$HTTPS_NODEPORT\"<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">In this example, HTTP was&nbsp;<code>31960<\/code>&nbsp;and HTTPS was&nbsp;<code>30251<\/code>.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Phase 2 \u2014 Diagnose and fix the Calico cross-node routing issue<\/h1>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>Only apply this change if you have the same problem.<\/strong>&nbsp;A healthy Calico cluster can use VXLAN, IP-in-IP, or unencapsulated routing with correctly configured BGP\/underlay routes.&nbsp;<code>ipipMode: Never<\/code>&nbsp;and&nbsp;<code>vxlanMode: Never<\/code>&nbsp;are&nbsp;<strong>not inherently invalid<\/strong>; the failure in this lab was that there was&nbsp;<strong>no functional route to the other node&#8217;s Pod CIDR<\/strong>.<\/p>\n<\/blockquote>\n\n\n\n<h2 class=\"wp-block-heading\">2.1 The observed error<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Attempting to create&nbsp;<code>ing.yaml<\/code>&nbsp;originally failed with:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-built_in\">Error<\/span> <span class=\"hljs-keyword\">from<\/span> server (<span class=\"hljs-built_in\">InternalError<\/span>):\nfailed calling webhook <span class=\"hljs-string\">\"validate.nginx.ingress.kubernetes.io\"<\/span>:\nPost <span class=\"hljs-string\">\"https:\/\/ingress-nginx-controller-admission.ingress-nginx.svc:443\/networking\/v1\/ingresses?timeout=10s\"<\/span>:\ncontext deadline exceeded\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">The controller Pod was&nbsp;<code>1\/1 Running<\/code>, the admission Service and EndpointSlice existed, and the controller logged&nbsp;<code>Starting validation webhook address=\":8443\"<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Interpretation:<\/strong>&nbsp;A Running Pod and populated Service endpoints are not proof of&nbsp;<strong>network reachability from the API server to the webhook<\/strong>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2.2 Inspect admission Service and routes<\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">kubectl <span class=\"hljs-keyword\">get<\/span> svc ingress-nginx-controller-admission -n ingress-nginx -o wide\n\nkubectl <span class=\"hljs-keyword\">get<\/span> endpointslices -n ingress-nginx \\\n  -l kubernetes.io\/service-name=ingress-nginx-controller-admission\n\nkubectl <span class=\"hljs-keyword\">get<\/span> pods -n ingress-nginx -o wide\n\nkubectl <span class=\"hljs-keyword\">get<\/span> validatingwebhookconfiguration ingress-nginx-admission -o yaml\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Find the webhook Pod IP from the EndpointSlice\/Pod output. In the lab it was&nbsp;<code>192.168.131.65<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On the&nbsp;<strong>control-plane node<\/strong>:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">ip route get <span class=\"hljs-number\">192.168<\/span><span class=\"hljs-number\">.131<\/span><span class=\"hljs-number\">.65<\/span>\n\ncurl --noproxy <span class=\"hljs-string\">'*'<\/span> -kv --connect-timeout <span class=\"hljs-number\">5<\/span> \\\n  https:<span class=\"hljs-comment\">\/\/192.168.131.65:8443\/<\/span>\n\ncurl --noproxy <span class=\"hljs-string\">'*'<\/span> -kv --connect-timeout <span class=\"hljs-number\">5<\/span> \\\n  https:<span class=\"hljs-comment\">\/\/10.101.95.207:443\/<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Before the fix,&nbsp;<strong>both curl commands timed out<\/strong>, and routing looked like:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\">192<span class=\"hljs-selector-class\">.168<\/span><span class=\"hljs-selector-class\">.131<\/span><span class=\"hljs-selector-class\">.65<\/span> <span class=\"hljs-selector-tag\">via<\/span> 172<span class=\"hljs-selector-class\">.31<\/span><span class=\"hljs-selector-class\">.0<\/span><span class=\"hljs-selector-class\">.1<\/span> <span class=\"hljs-selector-tag\">dev<\/span> <span class=\"hljs-selector-tag\">enX0<\/span> <span class=\"hljs-selector-tag\">src<\/span> 172<span class=\"hljs-selector-class\">.31<\/span><span class=\"hljs-selector-class\">.10<\/span><span class=\"hljs-selector-class\">.122<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">That was the AWS VPC gateway, not an appropriate configured overlay route to the remote Pod.&nbsp;<strong>This is a strong sign of missing remote Pod routing in this particular cluster<\/strong>, not a generic rule that any route through a gateway is wrong.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2.3 Check the existing Calico IPPool<\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">kubectl <span class=\"hljs-keyword\">get<\/span> ippools.crd.projectcalico.org -o yaml\n\nkubectl <span class=\"hljs-keyword\">get<\/span> pods -n kube-system -l k8s-app=calico-node -o wide\n\nip -d link show vxlan.calico || true\nip -d link show tunl0 || true\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Original lab IPPool:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">metadata:\n  name: <span class=\"hljs-keyword\">default<\/span>-ipv4-ippool\n<span class=\"hljs-attr\">spec<\/span>:\n  cidr: <span class=\"hljs-number\">192.168<\/span><span class=\"hljs-number\">.0<\/span><span class=\"hljs-number\">.0<\/span>\/<span class=\"hljs-number\">16<\/span>\n  <span class=\"hljs-attr\">ipipMode<\/span>: Never\n  <span class=\"hljs-attr\">vxlanMode<\/span>: Never\n  <span class=\"hljs-attr\">natOutgoing<\/span>: <span class=\"hljs-literal\">true<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">No functioning cross-node Pod route was present. The&nbsp;<code>vxlan.calico<\/code>&nbsp;interface did not exist, and&nbsp;<code>tunl0<\/code>&nbsp;was down.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2.4 Check AWS network requirements&nbsp;<strong>before<\/strong>&nbsp;making the change<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">For Calico VXLAN, allow&nbsp;<strong>UDP 4789 in both directions between the EC2 nodes<\/strong>&nbsp;(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.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">Calico transport<\/th><th class=\"has-text-align-left\" data-align=\"left\">Node-to-node traffic<\/th><\/tr><\/thead><tbody><tr><td>VXLAN<\/td><td>UDP 4789<\/td><\/tr><tr><td>IP-in-IP<\/td><td>IP protocol number 4<\/td><\/tr><tr><td>BGP<\/td><td>TCP 179<\/td><\/tr><tr><td>Unencapsulated\/direct routing<\/td><td>Pod CIDR reachability provided by the underlay and firewall rules<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Do&nbsp;<strong>not<\/strong>&nbsp;open UDP 4789 to the public Internet. Allow client NodePort HTTP access only from trusted IP ranges for this lab.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>Operator-managed Calico:<\/strong>&nbsp;If you installed Calico through the Tigera operator, check the&nbsp;<code>Installation<\/code>&nbsp;resource. Operator-managed pools should be changed via&nbsp;<code>Installation.spec.calicoNetwork.ipPools<\/code>, not patched directly; the operator can revert direct IPPool edits. The direct IPPool command below applies to this lab&#8217;s&nbsp;<strong>manifest-managed<\/strong>&nbsp;pool.<\/p>\n<\/blockquote>\n\n\n\n<h2 class=\"wp-block-heading\">2.5 Back up and enable VXLAN Always (manifest-managed Calico)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This changes network dataplane behavior and can briefly interrupt live connections. Back up your configuration and use a maintenance window on shared clusters.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">&lt;em&gt;<span class=\"hljs-comment\"># Backup&lt;\/em&gt;<\/span>\nkubectl get ippools.crd.projectcalico.org \\\n  <span class=\"hljs-keyword\">default<\/span>-ipv4-ippool -o yaml &gt; calico-ippool-backup.yaml\n\n&lt;em&gt;<span class=\"hljs-comment\"># Verify the pool name and current mode before modifying it&lt;\/em&gt;<\/span>\nkubectl get ippools.crd.projectcalico.org \\\n  <span class=\"hljs-keyword\">default<\/span>-ipv4-ippool \\\n  -o jsonpath=<span class=\"hljs-string\">'{.spec.cidr}{\"  IPIP=\"}{.spec.ipipMode}{\"  VXLAN=\"}{.spec.vxlanMode}{\"\\n\"}'<\/span>\n\n&lt;em&gt;<span class=\"hljs-comment\"># Enable overlay routing on the existing IPPool&lt;\/em&gt;<\/span>\nkubectl patch ippools.crd.projectcalico.org <span class=\"hljs-keyword\">default<\/span>-ipv4-ippool \\\n  --type=merge \\\n  -p <span class=\"hljs-string\">'{\"spec\":{\"ipipMode\":\"Never\",\"vxlanMode\":\"Always\"}}'<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>Why&nbsp;<code>Always<\/code>, not&nbsp;<code>CrossSubnet<\/code>?<\/strong>&nbsp;The EC2 nodes are in the same VPC subnet (<code>172.31.0.0\/20<\/code>).&nbsp;<code>CrossSubnet<\/code>&nbsp;may skip encapsulation between them and therefore would not solve the missing Pod-CIDR underlay routing in this lab.&nbsp;<code>Always<\/code>&nbsp;encapsulates cross-node traffic even when the nodes share a subnet.<\/p>\n<\/blockquote>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>For Tigera operator-managed Calico<\/strong>, instead inspect:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">kubectl<\/span> <span class=\"hljs-selector-tag\">get<\/span> <span class=\"hljs-selector-tag\">installations<\/span><span class=\"hljs-selector-class\">.operator<\/span><span class=\"hljs-selector-class\">.tigera<\/span><span class=\"hljs-selector-class\">.io<\/span> <span class=\"hljs-selector-tag\">default<\/span> <span class=\"hljs-selector-tag\">-o<\/span> <span class=\"hljs-selector-tag\">yaml<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Use the operator&#8217;s documented&nbsp;<code>encapsulation: VXLAN<\/code>&nbsp;setting for the appropriate IPPool definition in that Installation resource. Do not apply the direct patch if the operator manages the pool.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2.6 Verify VXLAN and the new route<\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-14\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">kubectl <span class=\"hljs-keyword\">get<\/span> ippools.crd.projectcalico.org default-ipv4-ippool -o yaml\n\nip -d link show vxlan.calico\n\nip route <span class=\"hljs-keyword\">get<\/span> 192.168.131.65\n\nkubectl <span class=\"hljs-keyword\">get<\/span> pods -n kube-system -l k8s-app=calico-node -o wide\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Observed after the fix in this lab:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-15\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">vxlan<\/span><span class=\"hljs-selector-class\">.calico<\/span>: &lt;<span class=\"hljs-selector-tag\">BROADCAST<\/span>,<span class=\"hljs-selector-tag\">MULTICAST<\/span>,<span class=\"hljs-selector-tag\">UP<\/span>,<span class=\"hljs-selector-tag\">LOWER_UP<\/span>&gt; <span class=\"hljs-selector-tag\">mtu<\/span> 1410\n    <span class=\"hljs-selector-tag\">vxlan<\/span> <span class=\"hljs-selector-tag\">id<\/span> 4096 <span class=\"hljs-selector-tag\">local<\/span> 172<span class=\"hljs-selector-class\">.31<\/span><span class=\"hljs-selector-class\">.10<\/span><span class=\"hljs-selector-class\">.122<\/span> <span class=\"hljs-selector-tag\">dev<\/span> <span class=\"hljs-selector-tag\">enX0<\/span> <span class=\"hljs-selector-tag\">dstport<\/span> 4789\n\n192<span class=\"hljs-selector-class\">.168<\/span><span class=\"hljs-selector-class\">.131<\/span><span class=\"hljs-selector-class\">.65<\/span> <span class=\"hljs-selector-tag\">via<\/span> 192<span class=\"hljs-selector-class\">.168<\/span><span class=\"hljs-selector-class\">.131<\/span><span class=\"hljs-selector-class\">.80<\/span> <span class=\"hljs-selector-tag\">dev<\/span> <span class=\"hljs-selector-tag\">vxlan<\/span><span class=\"hljs-selector-class\">.calico<\/span> <span class=\"hljs-selector-tag\">src<\/span> 192<span class=\"hljs-selector-class\">.168<\/span><span class=\"hljs-selector-class\">.90<\/span><span class=\"hljs-selector-class\">.202<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-15\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\"><strong>Important:<\/strong>&nbsp;The interface and route prove that VXLAN is configured; they do&nbsp;<strong>not<\/strong>&nbsp;alone prove packets cross between EC2 hosts. Retest both webhook endpoints:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-16\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">curl --noproxy <span class=\"hljs-string\">'*'<\/span> -kv --connect-timeout <span class=\"hljs-number\">5<\/span> \\\n  <span class=\"hljs-attr\">https<\/span>:<span class=\"hljs-comment\">\/\/192.168.131.65:8443\/<\/span>\n\ncurl --noproxy <span class=\"hljs-string\">'*'<\/span> -kv --connect-timeout <span class=\"hljs-number\">5<\/span> \\\n  <span class=\"hljs-attr\">https<\/span>:<span class=\"hljs-comment\">\/\/10.101.95.207:443\/<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-16\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Any genuine HTTP response (including an expected&nbsp;<code>404<\/code>\/<code>400<\/code>&nbsp;for&nbsp;<code>\/<\/code>) demonstrates reachability. The bare&nbsp;<code>curl<\/code>&nbsp;to&nbsp;<code>\/<\/code>&nbsp;is&nbsp;<strong>not a functional admission-validation test<\/strong>; it simply tests connection and TLS. The definitive admission check is that the subsequent Ingress creation succeeds.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>If timeouts continue:<\/strong>&nbsp;check both EC2 security groups, return traffic\/NACLs and host firewalls, and capture traffic while making a request:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-17\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">sudo tcpdump -ni enX0 udp port <span class=\"hljs-number\">4789<\/span>\n&lt;em&gt;<span class=\"hljs-comment\"># In a second terminal, repeat curl to the Pod IP and Service IP.&lt;\/em&gt;<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-17\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">If needed, review Calico logs:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-18\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">kubectl logs -n kube-system -l k8s-app=calico-node \\\n  --all-containers=<span class=\"hljs-literal\">false<\/span> --tail=<span class=\"hljs-number\">100<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-18\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">MTU note from this lab<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Calico also logged&nbsp;<code>Failed to auto-detect host MTU - no interfaces matched the MTU interface pattern<\/code>. The EC2 interface was named&nbsp;<code>enX0<\/code>; VXLAN later showed an MTU of&nbsp;<code>1410<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">This is a&nbsp;<strong>separate item to review<\/strong>&nbsp;if you observe packet loss or large-request failures. Check the underlay interface MTU, tunnel MTU and Calico&#8217;s configured detection settings before making any changes. Do not blindly assign a new MTU to every cluster.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-19\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">ip link show enX0\nip -d link show vxlan.calico\nkubectl <span class=\"hljs-keyword\">get<\/span> felixconfigurations.crd.projectcalico.org -o yaml\nkubectl <span class=\"hljs-keyword\">get<\/span> cm calico-config -n kube-system -o yaml 2&gt;\/dev\/null || true\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-19\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Phase 3 \u2014 Deploy two application Deployments and ClusterIP Services<\/h1>\n\n\n\n<h2 class=\"wp-block-heading\">3.1 Create&nbsp;<code>apps.yaml<\/code><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Use different Docker images so that you can recognize which backend answered each request.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-20\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">cat &gt; apps.yaml &lt;&lt;<span class=\"hljs-string\">'YAML'<\/span>\n<span class=\"hljs-attr\">apiVersion<\/span>: apps\/v1\n<span class=\"hljs-attr\">kind<\/span>: Deployment\n<span class=\"hljs-attr\">metadata<\/span>:\n  name: app1\n  <span class=\"hljs-attr\">namespace<\/span>: <span class=\"hljs-keyword\">default<\/span>\n<span class=\"hljs-attr\">spec<\/span>:\n  replicas: <span class=\"hljs-number\">1<\/span>\n  <span class=\"hljs-attr\">selector<\/span>:\n    matchLabels:\n      app: app1\n  <span class=\"hljs-attr\">template<\/span>:\n    metadata:\n      labels:\n        app: app1\n    <span class=\"hljs-attr\">spec<\/span>:\n      containers:\n        - name: nginx\n          <span class=\"hljs-attr\">image<\/span>: scmgalaxy\/nginx-devopsschoolv1\n          <span class=\"hljs-attr\">ports<\/span>:\n            - containerPort: <span class=\"hljs-number\">80<\/span>\n---\napiVersion: v1\n<span class=\"hljs-attr\">kind<\/span>: Service\n<span class=\"hljs-attr\">metadata<\/span>:\n  name: app1-service\n  <span class=\"hljs-attr\">namespace<\/span>: <span class=\"hljs-keyword\">default<\/span>\n<span class=\"hljs-attr\">spec<\/span>:\n  type: ClusterIP\n  <span class=\"hljs-attr\">selector<\/span>:\n    app: app1\n  <span class=\"hljs-attr\">ports<\/span>:\n    - name: http\n      <span class=\"hljs-attr\">port<\/span>: <span class=\"hljs-number\">80<\/span>\n      <span class=\"hljs-attr\">targetPort<\/span>: <span class=\"hljs-number\">80<\/span>\n      <span class=\"hljs-attr\">protocol<\/span>: TCP\n---\napiVersion: apps\/v1\n<span class=\"hljs-attr\">kind<\/span>: Deployment\n<span class=\"hljs-attr\">metadata<\/span>:\n  name: app2\n  <span class=\"hljs-attr\">namespace<\/span>: <span class=\"hljs-keyword\">default<\/span>\n<span class=\"hljs-attr\">spec<\/span>:\n  replicas: <span class=\"hljs-number\">1<\/span>\n  <span class=\"hljs-attr\">selector<\/span>:\n    matchLabels:\n      app: app2\n  <span class=\"hljs-attr\">template<\/span>:\n    metadata:\n      labels:\n        app: app2\n    <span class=\"hljs-attr\">spec<\/span>:\n      containers:\n        - name: nginx\n          <span class=\"hljs-attr\">image<\/span>: scmgalaxy\/nginx-devopsschoolv2\n          <span class=\"hljs-attr\">ports<\/span>:\n            - containerPort: <span class=\"hljs-number\">80<\/span>\n---\napiVersion: v1\n<span class=\"hljs-attr\">kind<\/span>: Service\n<span class=\"hljs-attr\">metadata<\/span>:\n  name: app2-service\n  <span class=\"hljs-attr\">namespace<\/span>: <span class=\"hljs-keyword\">default<\/span>\n<span class=\"hljs-attr\">spec<\/span>:\n  type: ClusterIP\n  <span class=\"hljs-attr\">selector<\/span>:\n    app: app2\n  <span class=\"hljs-attr\">ports<\/span>:\n    - name: http\n      <span class=\"hljs-attr\">port<\/span>: <span class=\"hljs-number\">80<\/span>\n      <span class=\"hljs-attr\">targetPort<\/span>: <span class=\"hljs-number\">80<\/span>\n      <span class=\"hljs-attr\">protocol<\/span>: TCP\nYAML\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-20\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h2 class=\"wp-block-heading\">3.2 Deploy and verify<\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-21\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">kubectl apply -f apps.yaml\n\nkubectl rollout status deployment\/app1 --timeout=<span class=\"hljs-number\">120<\/span>s\nkubectl rollout status deployment\/app2 --timeout=<span class=\"hljs-number\">120<\/span>s\n\nkubectl <span class=\"hljs-keyword\">get<\/span> deployments app1 app2\nkubectl <span class=\"hljs-keyword\">get<\/span> pods -l 'app in (app1,app2)' -o wide\nkubectl <span class=\"hljs-keyword\">get<\/span> svc app1-service app2-service\n\nkubectl <span class=\"hljs-keyword\">get<\/span> endpointslices -l kubernetes.io\/service-name=app1-service\nkubectl <span class=\"hljs-keyword\">get<\/span> endpointslices -l kubernetes.io\/service-name=app2-service\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-21\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Example ClusterIP addresses previously assigned in the lab:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">app1-service   ClusterIP   10.102.53.234    80\/TCP\napp2-service   ClusterIP   10.103.156.112   80\/TCP\n<\/code><\/span><\/pre>\n\n\n<p class=\"wp-block-paragraph\">They may change if Services are recreated. The Services must have ready Pod endpoints for requests to succeed.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">3.3 Validate each Service before introducing Ingress<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">You can curl the ClusterIP from a cluster node if your node-to-Service networking is working:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-22\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">curl -i --noproxy <span class=\"hljs-string\">'*'<\/span> http:<span class=\"hljs-comment\">\/\/10.102.53.234\/<\/span>\ncurl -i --noproxy <span class=\"hljs-string\">'*'<\/span> http:<span class=\"hljs-comment\">\/\/10.103.156.112\/<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-22\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Or discover the current Service IPs automatically:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-23\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">APP1_IP=$(kubectl get svc app1-service -o jsonpath=<span class=\"hljs-string\">'{.spec.clusterIP}'<\/span>)\nAPP2_IP=$(kubectl get svc app2-service -o jsonpath=<span class=\"hljs-string\">'{.spec.clusterIP}'<\/span>)\n\ncurl -i --noproxy <span class=\"hljs-string\">'*'<\/span> <span class=\"hljs-string\">\"http:\/\/${APP1_IP}\/\"<\/span>\ncurl -i --noproxy <span class=\"hljs-string\">'*'<\/span> <span class=\"hljs-string\">\"http:\/\/${APP2_IP}\/\"<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-23\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\"><strong>Expected:<\/strong>&nbsp;Both root pages return HTTP&nbsp;<code>200<\/code>&nbsp;if the images serve&nbsp;<code>\/<\/code>. App1 and App2 should show their respective version-specific content.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Phase 4 \u2014 Configure path-based Ingress with URL rewriting<\/h1>\n\n\n\n<h2 class=\"wp-block-heading\">4.1 Understand why simple Prefix rules produced 404<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The first Ingress used:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">- path: \/app1\n  pathType: Prefix\n  backend:\n    service:\n      name: app1-service\n      port:\n        number: 80\n<\/code><\/span><\/pre>\n\n\n<p class=\"wp-block-paragraph\">A Kubernetes&nbsp;<code>Prefix<\/code>&nbsp;match&nbsp;<strong>selects the backend Service but does not remove the prefix<\/strong>. The backend NGINX received&nbsp;<code>\/app1<\/code>&nbsp;instead of&nbsp;<code>\/<\/code>&nbsp;and returned:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">HTTP\/1.1 404 Not Found\nnginx\/1.15.10\n<\/code><\/span><\/pre>\n\n\n<p class=\"wp-block-paragraph\">In this lab the controller reported NGINX&nbsp;<code>1.27.1<\/code>, while the 404 page identified&nbsp;<code>1.15.10<\/code>, which was a useful clue that the error came from the&nbsp;<strong>backend application<\/strong>, not the controller&#8217;s default backend. The specific version string can vary.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">4.2 Create the final&nbsp;<code>ing.yaml<\/code><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">This Ingress is&nbsp;<strong>controller-specific<\/strong>: the&nbsp;<code>nginx.ingress.kubernetes.io\/*<\/code>&nbsp;annotations and regex capture rewrite depend on community ingress-nginx. Other ingress controllers may require different configuration.<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">cat &gt; ing.yaml &lt;&lt;'YAML'\napiVersion: networking.k8s.io\/v1\nkind: Ingress\nmetadata:\n  name: path-based-ingress\n  namespace: default\n  annotations:\n    nginx.ingress.kubernetes.io\/use-regex: \"true\"\n    nginx.ingress.kubernetes.io\/rewrite-target: \/$2\nspec:\n  ingressClassName: nginx\n  rules:\n    - http:\n        paths:\n          - path: \/app1(\/|$)(.*)\n            pathType: ImplementationSpecific\n            backend:\n              service:\n                name: app1-service\n                port:\n                  number: 80\n          - path: \/app2(\/|$)(.*)\n            pathType: ImplementationSpecific\n            backend:\n              service:\n                name: app2-service\n                port:\n                  number: 80\nYAML\n<\/code><\/span><\/pre>\n\n\n<p class=\"wp-block-paragraph\"><strong>Why&nbsp;<code>ImplementationSpecific<\/code>?<\/strong>&nbsp;Kubernetes native path types are&nbsp;<code>Exact<\/code>,&nbsp;<code>Prefix<\/code>, and&nbsp;<code>ImplementationSpecific<\/code>. The NGINX-specific regular expression must be handled by the controller, rather than interpreted as a Kubernetes-native&nbsp;<code>Prefix<\/code>&nbsp;rule.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Why&nbsp;<code>\/$2<\/code>?<\/strong>&nbsp;In each expression,&nbsp;<code>(\/|$)<\/code>&nbsp;is capture group&nbsp;<code>$1<\/code>, and&nbsp;<code>(.*)<\/code>&nbsp;is capture group&nbsp;<code>$2<\/code>. Ingress-nginx rewrites the forwarded URL to&nbsp;<code>\/<\/code>&nbsp;plus the content of&nbsp;<code>$2<\/code>.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">Incoming path<\/th><th class=\"has-text-align-left\" data-align=\"left\">Selected Service<\/th><th class=\"has-text-align-left\" data-align=\"left\">Path the Pod receives<\/th><\/tr><\/thead><tbody><tr><td><code>\/app1<\/code><\/td><td><code>app1-service:80<\/code><\/td><td><code>\/<\/code><\/td><\/tr><tr><td><code>\/app1\/<\/code><\/td><td><code>app1-service:80<\/code><\/td><td><code>\/<\/code><\/td><\/tr><tr><td><code>\/app1\/index.html<\/code><\/td><td><code>app1-service:80<\/code><\/td><td><code>\/index.html<\/code><\/td><\/tr><tr><td><code>\/app2<\/code><\/td><td><code>app2-service:80<\/code><\/td><td><code>\/<\/code><\/td><\/tr><tr><td><code>\/app2\/health<\/code><\/td><td><code>app2-service:80<\/code><\/td><td><code>\/health<\/code><\/td><\/tr><tr><td><code>\/app3<\/code><\/td><td>No matching application rule<\/td><td>Controller default behavior (typically 404)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">4.3 Apply and verify the Ingress<\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-24\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">kubectl apply --dry-run=client -f ing.yaml\nkubectl apply -f ing.yaml\n\nkubectl <span class=\"hljs-keyword\">get<\/span> ingress\nkubectl describe ingress path-based-ingress\nkubectl <span class=\"hljs-keyword\">get<\/span> ingress path-based-ingress -o yaml\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-24\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">You should see&nbsp;<code>IngressClass<\/code>&nbsp;<strong>nginx<\/strong>, paths for both Services, and the two rewrite annotations.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If&nbsp;<code>kubectl apply<\/code>&nbsp;fails with an admission-webhook timeout, return to&nbsp;<strong>Phase 2<\/strong>. Do&nbsp;<strong>not<\/strong>&nbsp;delete the webhook as a normal fix. Admission validation is useful and should stay enabled.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Phase 5 \u2014 End-to-end validation<\/h1>\n\n\n\n<h2 class=\"wp-block-heading\">5.1 Confirm networking, backend health, and controller<\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-25\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">kubectl <span class=\"hljs-keyword\">get<\/span> nodes -o wide\nkubectl <span class=\"hljs-keyword\">get<\/span> pods -n ingress-nginx -o wide\nkubectl <span class=\"hljs-keyword\">get<\/span> pods -l 'app in (app1,app2)' -o wide\nkubectl <span class=\"hljs-keyword\">get<\/span> svc app1-service app2-service\nkubectl <span class=\"hljs-keyword\">get<\/span> ingress path-based-ingress\nkubectl <span class=\"hljs-keyword\">get<\/span> ingressclass nginx\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-25\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h2 class=\"wp-block-heading\">5.2 Discover NodePort and worker node address<\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-26\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">HTTP_NODEPORT=$(kubectl get svc ingress-nginx-controller -n ingress-nginx \\\n  -o jsonpath=<span class=\"hljs-string\">'{.spec.ports&#91;?(@.name==\"http\")].nodePort}'<\/span>)\n\nWORKER_IP=<span class=\"hljs-number\">172.31<\/span><span class=\"hljs-number\">.13<\/span><span class=\"hljs-number\">.14<\/span>  &lt;em&gt;<span class=\"hljs-comment\"># Replace with a reachable node private IP&lt;\/em&gt;<\/span>\n\nprintf <span class=\"hljs-string\">'Testing http:\/\/%s:%s\\n'<\/span> <span class=\"hljs-string\">\"$WORKER_IP\"<\/span> <span class=\"hljs-string\">\"$HTTP_NODEPORT\"<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-26\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\"><strong>Example:<\/strong>&nbsp;<code>http:\/\/172.31.13.14:31960<\/code>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">5.3 Test both paths<\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-27\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">curl -i --noproxy <span class=\"hljs-string\">'*'<\/span> <span class=\"hljs-string\">\"http:\/\/${WORKER_IP}:${HTTP_NODEPORT}\/app1\"<\/span>\n\ncurl -i --noproxy <span class=\"hljs-string\">'*'<\/span> <span class=\"hljs-string\">\"http:\/\/${WORKER_IP}:${HTTP_NODEPORT}\/app2\"<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-27\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\"><strong>Expected:<\/strong>&nbsp;<code>HTTP\/1.1 200 OK<\/code>&nbsp;and the body of NGINX demo v1 at&nbsp;<code>\/app1<\/code>&nbsp;and demo v2 at&nbsp;<code>\/app2<\/code>&nbsp;(assuming both images serve their root pages correctly).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Additional checks:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-28\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">&lt;em&gt;<span class=\"hljs-comment\"># Rewritten to \/index.html by ingress-nginx&lt;\/em&gt;<\/span>\ncurl -i --noproxy <span class=\"hljs-string\">'*'<\/span> <span class=\"hljs-string\">\"http:\/\/${WORKER_IP}:${HTTP_NODEPORT}\/app1\/index.html\"<\/span>\ncurl -i --noproxy <span class=\"hljs-string\">'*'<\/span> <span class=\"hljs-string\">\"http:\/\/${WORKER_IP}:${HTTP_NODEPORT}\/app2\/index.html\"<\/span>\n\n&lt;em&gt;<span class=\"hljs-comment\"># Negative test \u2014 no matching rule&lt;\/em&gt;<\/span>\ncurl -i --noproxy <span class=\"hljs-string\">'*'<\/span> <span class=\"hljs-string\">\"http:\/\/${WORKER_IP}:${HTTP_NODEPORT}\/does-not-exist\"<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-28\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h2 class=\"wp-block-heading\">5.4 Check from a local laptop if EC2 NodePort is not directly reachable<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">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&nbsp;<code>kubectl<\/code>&nbsp;can reach the cluster:<\/p>\n\n\n<pre class=\"wp-block-code\"><span><code class=\"hljs\">kubectl -n ingress-nginx port-forward \\\n  svc\/ingress-nginx-controller 8080:80\n<\/code><\/span><\/pre>\n\n\n<p class=\"wp-block-paragraph\">From another terminal on that&nbsp;<strong>same machine<\/strong>:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-29\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">curl -i http:<span class=\"hljs-comment\">\/\/127.0.0.1:8080\/app1<\/span>\ncurl -i http:<span class=\"hljs-comment\">\/\/127.0.0.1:8080\/app2<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-29\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Port forwarding does not require exposing the NodePort to the public Internet.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">5.5 Confirm which backend handled the request<\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-30\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">kubectl <span class=\"hljs-keyword\">get<\/span> pods -l app=app1 -o wide\nkubectl <span class=\"hljs-keyword\">get<\/span> pods -l app=app2 -o wide\n\nkubectl logs deployment\/app1 --tail=30\nkubectl logs deployment\/app2 --tail=30\n\nkubectl logs -n ingress-nginx \\\n  deployment\/ingress-nginx-controller --tail=60\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-30\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">Compare page content and each application&#8217;s access logs after hitting&nbsp;<code>\/app1<\/code>&nbsp;and&nbsp;<code>\/app2<\/code>. Different Docker images make routing demonstrations much clearer.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Phase 6 \u2014 Troubleshooting reference<\/h1>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th class=\"has-text-align-left\" data-align=\"left\">Symptom<\/th><th class=\"has-text-align-left\" data-align=\"left\">Likely cause<\/th><th class=\"has-text-align-left\" data-align=\"left\">Checks \/ fix<\/th><\/tr><\/thead><tbody><tr><td><code>failed calling webhook ... context deadline exceeded<\/code><\/td><td>API server cannot reach admission Service<\/td><td>Verify EndpointSlice, Pod route, CNI, kube-proxy, security groups, TCP 8443 \/ Service 443<\/td><\/tr><tr><td>Pod IP and ClusterIP both time out from control plane<\/td><td>Cross-node Pod networking\/underlay or firewall issue<\/td><td><code>ip route get POD_IP<\/code>; verify Calico overlay and UDP 4789<\/td><\/tr><tr><td><code>vxlan.calico<\/code>&nbsp;missing after patch<\/td><td>Calico not configured\/reconciled for VXLAN<\/td><td>IPPool mode, operator ownership,&nbsp;<code>calico-node<\/code>&nbsp;logs and DaemonSet settings<\/td><\/tr><tr><td><code>vxlan.calico<\/code>&nbsp;is UP but curl still times out<\/td><td>Packets blocked or reverse path broken<\/td><td>EC2 SG\/NACL, host firewall,&nbsp;<code>tcpdump -ni enX0 udp port 4789<\/code><\/td><\/tr><tr><td>Ingress admission succeeds but&nbsp;<code>\/app1<\/code>&nbsp;is 404 from backend NGINX<\/td><td>Prefix path was not removed<\/td><td>Use regex pattern and rewrite annotation in&nbsp;<strong>Phase 4<\/strong><\/td><\/tr><tr><td>Ingress returns 404 for&nbsp;<code>\/app3<\/code><\/td><td>No matching rule<\/td><td>Expected unless another\/default backend is configured<\/td><\/tr><tr><td>HTTP 503 from Ingress<\/td><td>Service has no ready backends, wrong name\/port, or backend unreachable<\/td><td><code>kubectl get endpointslices<\/code>; Pod readiness; service selectors<\/td><\/tr><tr><td>NodePort request cannot connect<\/td><td>Wrong NodePort, no reachable node IP, or blocked ingress traffic<\/td><td>Query actual Service NodePort; test from node\/VPC or port-forward<\/td><\/tr><tr><td>502\/504 or intermittent issues<\/td><td>Backend\/process failure, CNI problems, traffic policies<\/td><td>Logs, Pod health, NetworkPolicies, MTU and packet captures<\/td><\/tr><tr><td>LoadBalancer Service EXTERNAL-IP is&nbsp;<code>&lt;pending&gt;<\/code><\/td><td>No cloud LB integration in self-managed cluster<\/td><td>For this lab use the ingress-nginx&nbsp;<code>NodePort<\/code>&nbsp;Service<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Deep-dive diagnostic commands<\/h3>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-31\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">em<\/span>&gt;<\/span># Ingress details<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">em<\/span>&gt;<\/span>\nkubectl describe ingress path-based-ingress\nkubectl get ingress path-based-ingress -o yaml\n\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">em<\/span>&gt;<\/span># Controller and admission endpoints<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">em<\/span>&gt;<\/span>\nkubectl get svc -n ingress-nginx\nkubectl get endpointslices -n ingress-nginx \\\n  -l kubernetes.io\/service-name=ingress-nginx-controller-admission\nkubectl logs -n ingress-nginx deployment\/ingress-nginx-controller --tail=100\n\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">em<\/span>&gt;<\/span># Networking and routes<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">em<\/span>&gt;<\/span>\nkubectl get ippools.crd.projectcalico.org -o yaml\nkubectl get pods -n kube-system -o wide\nip route\nip -d link show vxlan.calico\n\n<span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">em<\/span>&gt;<\/span># Backend Pods and endpoints<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">em<\/span>&gt;<\/span>\nkubectl describe deployment app1\nkubectl describe deployment app2\nkubectl get endpointslices -l kubernetes.io\/service-name=app1-service\nkubectl get endpointslices -l kubernetes.io\/service-name=app2-service\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-31\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>Avoid the unsafe shortcut:<\/strong>&nbsp;Changing the admission webhook to&nbsp;<code>failurePolicy: Ignore<\/code>&nbsp;is 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.<\/p>\n<\/blockquote>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Phase 7 \u2014 Optional cleanup and rollback<\/h1>\n\n\n\n<h2 class=\"wp-block-heading\">Delete only the demo Ingress and applications<\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-32\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">kubectl <span class=\"hljs-keyword\">delete<\/span> -f ing.yaml\nkubectl <span class=\"hljs-keyword\">delete<\/span> -f apps.yaml\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-32\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h2 class=\"wp-block-heading\">Remove the lab controller only if it is not used elsewhere<\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-33\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\">kubectl <span class=\"hljs-keyword\">delete<\/span> -f https:<span class=\"hljs-comment\">\/\/raw.githubusercontent.com\/kubernetes\/ingress-nginx\/controller-v1.15.1\/deploy\/static\/provider\/baremetal\/deploy.yaml<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-33\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\"><strong>Caution:<\/strong>&nbsp;This can disrupt other Ingress resources that depend on the same controller. Do not remove it on a shared cluster without checking usage.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Roll back Calico encapsulation only when you have a working alternative route<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Do&nbsp;<strong>not<\/strong>&nbsp;blindly revert VXLAN to&nbsp;<code>Never<\/code>: 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.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">Final success checklist<\/h1>\n\n\n\n<ul class=\"wp-block-list\">\n<li>[ ]\u00a0<code>kubectl get nodes<\/code>\u00a0shows both nodes\u00a0<code>Ready<\/code>.<\/li>\n\n\n\n<li>[ ]\u00a0<code>ingress-nginx-controller<\/code>\u00a0is ready and its\u00a0<code>NodePort<\/code>\u00a0Service exists.<\/li>\n\n\n\n<li>[ ] Calico IPPool encapsulation is intentional, with required node-to-node transport allowed.<\/li>\n\n\n\n<li>[ ]\u00a0<code>ip route get &lt;REMOTE_POD_IP><\/code>\u00a0shows a usable route to the remote Pod.<\/li>\n\n\n\n<li>[ ] Admission webhook Service\/Pod TCP connectivity works from the control plane.<\/li>\n\n\n\n<li>[ ] Both\u00a0<code>app1<\/code>\u00a0and\u00a0<code>app2<\/code>\u00a0Deployments are ready.<\/li>\n\n\n\n<li>[ ] Both ClusterIP Services have ready Pod endpoints.<\/li>\n\n\n\n<li>[ ]\u00a0<code>path-based-ingress<\/code>\u00a0was accepted by Kubernetes admission validation.<\/li>\n\n\n\n<li>[ ]\u00a0<code>GET \/app1<\/code>\u00a0returns application v1 content.<\/li>\n\n\n\n<li>[ ]\u00a0<code>GET \/app2<\/code>\u00a0returns application v2 content.<\/li>\n\n\n\n<li>[ ]\u00a0<code>GET \/app1\/index.html<\/code>\u00a0and\u00a0<code>\/app2\/index.html<\/code>\u00a0work with URL rewriting.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Command summary<\/h2>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-34\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php\">&lt;em&gt;<span class=\"hljs-comment\"># 1. Controller (legacy lab-only)&lt;\/em&gt;<\/span>\nkubectl apply -f https:<span class=\"hljs-comment\">\/\/raw.githubusercontent.com\/kubernetes\/ingress-nginx\/controller-v1.15.1\/deploy\/static\/provider\/baremetal\/deploy.yaml<\/span>\n\n&lt;em&gt;<span class=\"hljs-comment\"># 2. Calico: only if the manifest-managed IPPool lacks working cross-node routing&lt;\/em&gt;<\/span>\nkubectl patch ippools.crd.projectcalico.org <span class=\"hljs-keyword\">default<\/span>-ipv4-ippool \\\n  --type=merge -p <span class=\"hljs-string\">'{\"spec\":{\"ipipMode\":\"Never\",\"vxlanMode\":\"Always\"}}'<\/span>\n\n&lt;em&gt;<span class=\"hljs-comment\"># 3. Applications&lt;\/em&gt;<\/span>\nkubectl apply -f apps.yaml\n\n&lt;em&gt;<span class=\"hljs-comment\"># 4. Ingress&lt;\/em&gt;<\/span>\nkubectl apply -f ing.yaml\n\n&lt;em&gt;<span class=\"hljs-comment\"># 5. Verify&lt;\/em&gt;<\/span>\nkubectl get pods,deploy,svc,ingress\nkubectl get svc -n ingress-nginx\ncurl -i http:<span class=\"hljs-comment\">\/\/172.31.13.14:31960\/app1<\/span>\ncurl -i http:<span class=\"hljs-comment\">\/\/172.31.13.14:31960\/app2<\/span>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-34\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"wp-block-paragraph\">The two YAML manifests are supplied in full in&nbsp;<strong>Phases 3 and 4<\/strong>. The&nbsp;<code>31960<\/code>&nbsp;port and&nbsp;<code>172.31.13.14<\/code>&nbsp;address in the summary are the&nbsp;<strong>example lab values<\/strong>\u2014discover your actual NodePort and node address before testing.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">References<\/h2>\n\n\n\n<ol class=\"wp-block-list\">\n<li><a href=\"https:\/\/kubernetes.io\/docs\/concepts\/services-networking\/ingress\/\">Kubernetes \u2014 Ingress concepts and path types<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/kubernetes.github.io\/ingress-nginx\/deploy\/\">ingress-nginx \u2014 Installation on bare-metal clusters (archived documentation)<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/kubernetes.github.io\/ingress-nginx\/examples\/rewrite\/\">ingress-nginx \u2014 Rewrite example and capture groups<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/docs.tigera.io\/calico\/latest\/networking\/configuring\/vxlan-ipip\">Calico \u2014 Overlay networking with VXLAN and IP-in-IP<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/docs.tigera.io\/calico\/latest\/getting-started\/kubernetes\/requirements\">Calico \u2014 Network port requirements<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/docs.tigera.io\/calico\/latest\/reference\/resources\/ippool\">Calico \u2014 IPPool reference<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/kubernetes.io\/blog\/2025\/11\/11\/ingress-nginx-retirement\/\">Kubernetes \u2014 Ingress NGINX retirement announcement<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/kubernetes.io\/blog\/2026\/01\/29\/ingress-nginx-statement\/\">Kubernetes \u2014 January 2026 retirement\/security statement<\/a><\/li>\n<\/ol>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Lab snapshot and troubleshooting history: November 12, 2026. Addresses, resource names, versions, and port numbers reflect the example cluster unless explicitly described as dynamic.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Scope:&nbsp;Self-managed Kubernetes on two AWS EC2 Ubuntu nodes; install the community ingress-nginx&nbsp;v1.15.1&nbsp;bare-metal\/NodePort manifest, repair cross-node Calico networking, deploy two NGINX versions behind ClusterIP Services, configure path-based routing&#8230; <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_joinchat":[],"footnotes":""},"categories":[11138],"tags":[],"class_list":["post-79852","post","type-post","status-publish","format-standard","hentry","category-best-tools"],"_links":{"self":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/79852","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/comments?post=79852"}],"version-history":[{"count":1,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/79852\/revisions"}],"predecessor-version":[{"id":79853,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/posts\/79852\/revisions\/79853"}],"wp:attachment":[{"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/media?parent=79852"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/categories?post=79852"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.devopsschool.com\/blog\/wp-json\/wp\/v2\/tags?post=79852"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}