Manage Thousands of F5 NGINX Ingress VirtualServerRoutes with Kustomize

Introduction

F5 NGINX Ingress VirtualServerRoutes require maintaining route files and manually updating VirtualServer references — a dual-maintenance problem that doesn't scale. This tutorial automates the integration so you only manage route files, and everything else is generated automatically.

 

The Challenge

A recent customer engagement highlighted a common scalability problem with F5 NGINX Ingress Controller: managing thousands of routes across multiple domains within monolithic VirtualServer manifests.

The Scale Problem:

  • Few domains (2-5 host configurations)
  • Thousands of routes per domain (1000+ first-level paths like /api, /admin, /user, and more)
  • Multiple teams contributing routes independently
  • Frequent updates requiring full manifest redeployment

Why Single Manifests Don't Scale:

A traditional VirtualServer with 1000+ routes becomes:

  • Unmaintainable: 5000+ line YAML files that are difficult to read and edit
  • Error-prone: Small syntax errors break the entire routing configuration
  • Collaboration nightmare: Multiple teams editing the same massive file creates merge conflicts
  • Deployment bottleneck: Any route change requires redeploying the entire configuration
  • Rollback complexity: Failed deployments affect all routes, not just the problematic ones

Example of the problem:

# A single VirtualServer with hundreds of routes becomes unwieldy
spec:
  routes:
    - path: /api/v1/users
      action: { pass: users-service }
    - path: /api/v1/orders  
      action: { pass: orders-service }
    - path: /api/v1/payments
      action: { pass: payments-service }
    # ... 997 more routes
    - path: /admin/dashboard
      action: { pass: admin-service }

 

 

The Standard VirtualServerRoute Problem

Even when breaking routes into individual VirtualServerRoute manifests, NGINX Ingress Controller requires dual maintenance:

     1. Create the VirtualServerRoute file:

# routes/payment-route.yaml
apiVersion: k8s.nginx.org/v1
kind: VirtualServerRoute
metadata:
  name: payment-route
spec:
  # Route configuration here

     2. Manually add the route reference to the VirtualServer:

# virtualserver.yaml - Must be edited separately
spec:
  routes:
    - path: /payment
      route: nginx-ingress/payment-route  # Manual reference required

This creates ongoing maintenance overhead:

  • Two-place editing: Every route addition/removal requires changes in two files
  • Synchronization errors: Easy to forget updating the VirtualServer references
  • Manual bookkeeping: Developers must remember to add route references
  • Deployment complexity: Both files must be updated and deployed together

 

 

The Solution

This tutorial eliminates the dual-maintenance problem through complete automation:

Your workflow becomes:

  1. Add route: Create new-route.yaml in the routes/ directory
  2. Remove route: Delete the file from routes/ directory
  3. Deploy: Run ./build-kustomize.sh && kustomize build . | kubectl apply -f -

Everything else is automatically generated:

  • Route references in the VirtualServer
  • Kustomize configuration files
  • Proper resource inclusion and patching

The result: Transform thousands of routes requiring dual maintenance into a streamlined workflow where you only manage route files - all integration is automated.

 

 

Critical Requirements for VirtualServerRoute

Before creating any route files, ensure each VirtualServerRoute includes:

  1. Required host field: Must exactly match the host in your main VirtualServer
  2. Correct path prefixes: Subroute paths must begin with the route path defined in the VirtualServer

Example showing the relationship:

# In VirtualServer (auto-generated by script):
- path: /login
  route: nginx-ingress/login-route

# In VirtualServerRoute file:
spec:
  host: myapp.example.com  # REQUIRED: Must match VirtualServer host
  subroutes:
    - path: /login/details  # REQUIRED: Must start with /login
    - path: /login/status   # REQUIRED: Must start with /login

 

Directory Structure

Your project should follow this structure. The kustomization.yaml and routes-patch.yaml files are auto-generated.

.
├── base/
│   ├── kustomization.yaml
│   └── virtualserver.yaml      # Base VS definition
├── routes/
│   ├── login-route.yaml      # Example VSR
│   └── profile-route.yaml    # Example VSR
├── build-kustomize.sh        # The single script to generate Kustomize configs
├── .gitignore
├── kustomization.yaml          # <-- Generated by script
└── routes-patch.yaml         # <-- Generated by script

 

Setup Instructions

1. Create the Base VirtualServer

Define your main VirtualServer without any of the dynamic route references. This file should contain defaults, TLS configuration, and upstreams that might be shared or used by the base path.

# base/virtualserver.yaml
apiVersion: k8s.nginx.org/v1
kind: VirtualServer
metadata:
  name: main-application
  namespace: nginx-ingress # Ensure this matches your environment
spec:
  host: myapp.example.com
  tls:
    secret: myapp-tls
    redirect:
      enable: true
  upstreams:
    - name: main-app
      service: nginx-test-svc
      port: 80
  # Define a base/default route here if needed.
  routes:
    - path: /
      action:
        pass: main-app
    # Dynamic routes for VirtualServerRoutes will be added via patch.

2. Create Base Kustomization File

# base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - virtualserver.yaml

3. Create the Kustomize Generator Script

This is the core of the automated solution. This single script finds all *-route.yaml files in the routes/ directory and generates both the kustomization.yaml and routes-patch.yaml files.

#!/bin/bash
# build-kustomize.sh
#
# This script automatically generates BOTH the kustomization.yaml AND the
# routes-patch.yaml files based on the contents of the 'routes/' directory.
#
set -e

# --- Configuration ---
ROUTES_DIR="routes"
KUSTOMIZATION_FILE="kustomization.yaml"
PATCH_FILE="routes-patch.yaml"
NAMESPACE="nginx-ingress" # Ensure this matches your VS/VSR namespace

# --- Part 1: Generate kustomization.yaml ---
echo "Generating ${KUSTOMIZATION_FILE}..."

# Start with the static parts of the kustomization file
cat > $KUSTOMIZATION_FILE << EOL
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization

resources:
  # Include the base VirtualServer definition
  - base
EOL

# Find all route files and append them to the resources list
# The shell's glob expansion (*) is reliable and used here to build the file.
for route_file in ${ROUTES_DIR}/*-route.yaml; do
  if [[ -f "$route_file" ]]; then
    # Add the file path to the kustomization resources list
    echo "  - ${route_file}" >> $KUSTOMIZATION_FILE
  fi
done

# Add the final static patch section
cat >> $KUSTOMIZATION_FILE << EOL

patches:
  - path: ${PATCH_FILE}
EOL
echo "Successfully generated ${KUSTOMIZATION_FILE}."


# --- Part 2: Generate routes-patch.yaml ---
echo -e "\nGenerating ${PATCH_FILE}..."

# Start with the patch header
cat > $PATCH_FILE << EOL
apiVersion: k8s.nginx.org/v1
kind: VirtualServer
metadata:
  name: main-application
  namespace: ${NAMESPACE}
spec:
  routes:
    # Include the base route with its action
    - path: /
      action:
        pass: main-app
EOL

# Find all route files and append their references to the patch
find "${ROUTES_DIR}" -maxdepth 1 -name '*-route.yaml' -print0 | while IFS= read -r -d $'\0' route_file; do
  if [[ -f "$route_file" ]]; then
    resource_name=$(basename "$route_file" .yaml)
    path_prefix=$(echo "$resource_name" | sed 's/-route$//')

    cat >> $PATCH_FILE << EOL
    # Route added from ${route_file}
    - path: /${path_prefix}
      route: ${NAMESPACE}/${resource_name}
EOL
  fi
done
echo "Successfully generated ${PATCH_FILE}."

Make the script executable:

chmod +x build-kustomize.sh

4. Create an Initial Route File

Create at least one VirtualServerRoute file in the routes/ directory so the script has something to find.

# routes/login-route.yaml
apiVersion: k8s.nginx.org/v1
kind: VirtualServerRoute
metadata:
  name: login-route
  namespace: nginx-ingress # Should match NAMESPACE in script and VS
spec:
  host: myapp.example.com  # REQUIRED: Must match VirtualServer host
  upstreams:
    - name: login-service
      service: login-svc
      port: 80
  subroutes:
    - path: /login/details  # REQUIRED: Must start with /login prefix
      action:
        pass: login-service
    - path: /login/status   # REQUIRED: Must start with /login prefix
      action:
        pass: login-service

5. Git Ignore Generated Files

Add the generated files to your .gitignore to avoid committing them to version control.

# .gitignore
kustomization.yaml
routes-patch.yaml

 

 

Workflow

Your daily workflow is now streamlined. You only need to manage files in the routes/ directory and run the helper script.

Initial Generation and Deployment

1. Run the generator script to create your Kustomize configuration for the first time

./build-kustomize.sh

2. (Optional) Verify the changes with a dry-run build.

kustomize build .

This shows you the final, combined manifest that will be sent to the cluster. Inspect it to ensure your base VirtualServer has the new route reference and that the VirtualServerRoute resource is included.

3. Apply the changes to your cluster.

kustomize build . | kubectl apply -f -

Adding a New Route

1. Create a new VirtualServerRoute file in the routes/ directory (e.g., account-route.yaml). Ensure the filename follows the name-route.yaml convention and includes the required host field

# routes/account-route.yaml
apiVersion: k8s.nginx.org/v1
kind: VirtualServerRoute
metadata:
  name: account-route
  namespace: nginx-ingress
spec:
  host: myapp.example.com  # REQUIRED: Must match VirtualServer host
  upstreams:
    - name: account-service
      service: account-svc
      port: 80
  subroutes:
    - path: /account/profile    # REQUIRED: Must start with /account
      action:
        pass: account-service
    - path: /account/settings   # REQUIRED: Must start with /account
      action:
        pass: account-service

2. Run the generator script.

./build-kustomize.sh

3. Apply the changes.

kustomize build . | kubectl apply -f -

Removing an Existing Route

1. Delete the file from the routes/ directory (e.g., rm routes/login-route.yaml).

2. Run the generator script. The script will see the file is gone and will automatically regenerate kustomization.yaml and routes-patch.yaml without the corresponding entries.

./build-kustomize.sh

3. Apply the changes. kubectl apply will update the VirtualServer to remove the route reference and will remove the VirtualServerRoute object from the cluster (if using --prune or a GitOps tool).

kustomize build . | kubectl apply -f -

 

Troubleshooting

Validating Your Configuration

After applying changes, always check the status of your resources to ensure they were accepted:

# Check VirtualServer status
kubectl describe virtualserver main-application -n nginx-ingress

# Check VirtualServerRoute status
kubectl describe virtualserverroutes -n nginx-ingress

Understanding Events and Warnings

When troubleshooting, pay attention to the most recent events in the output. Early warning events are often remnants from the deployment sequence and can be safely ignored if recent events show success.

Example of a normal deployment sequence:

Events:
  Type     Reason                     Age   From                      Message
  ----     ------                     ----  ----                      -------
  Warning  AddedOrUpdatedWithWarning  55s   nginx-ingress-controller  Configuration for nginx-ingress/main-application was added or updated with warning(s): VirtualServerRoute nginx-ingress/login-route doesn't exist or invalid
  Warning  AddedOrUpdatedWithWarning  55s   nginx-ingress-controller  Configuration for nginx-ingress/main-application was added or updated with warning(s): VirtualServerRoute nginx-ingress/logout-route doesn't exist or invalid
  Normal   AddedOrUpdated             54s   nginx-ingress-controller  Configuration for nginx-ingress/main-application was added or updated
  Normal   AddedOrUpdated             54s   nginx-ingress-controller  Configuration for nginx-ingress/main-application was added or updated

Interpretation:

  • 55s ago: VirtualServer applied/updated before VirtualServerRoutes existed - NGINX warns about missing dependencies
  • 54s ago: VirtualServerRoutes created, VirtualServer automatically becomes valid
  • Current state: All dependencies satisfied, configuration valid (focus on the most recent AddedOrUpdated events)

Common Issues

1. Missing host field: VirtualServerRoute resources must include spec.host that matches the VirtualServer host

Error: "spec.host: Required value"
Solution: Add host: myapp.example.com to each VirtualServerRoute

2. Invalid path prefixes: Subroute paths must start with the route path from the VirtualServer

Error: Path mismatch
Solution: Ensure /login/details starts with /login prefix

3. Namespace mismatch: All resources must be in the same namespace

Error: VirtualServerRoute doesn't exist
Solution: Verify all resources use the same namespace

4. Upstream not found: VirtualServer references an upstream that doesn't exist

Error: "spec.routes[0].action.pass: Not found: 'upstream-name'"
Solution: Ensure upstream name in routes matches upstream name in upstreams section

Checking Resource Status

Look for these indicators of success:

VirtualServer:

  • State: Valid
  • Reason: AddedOrUpdated
  • Recent Normal AddedOrUpdated events

VirtualServerRoute:

  • State: Valid
  • Referenced By: nginx-ingress/main-application
  • Recent Normal AddedOrUpdated events

If you see State: Invalid or Reason: Rejected, check the error message and events for specific guidance on what needs to be fixed.

Eliminating Dependency Warnings

The warnings shown above are normal during deployment but can be eliminated by applying resources in the correct order:

# Apply VirtualServerRoutes first
kustomize build . | yq 'select(.kind == "VirtualServerRoute")' | kubectl apply -f -

# Then apply VirtualServer
kustomize build . | yq 'select(.kind == "VirtualServer")' | kubectl apply -f -

This ensures VirtualServerRoutes exist before the VirtualServer references them, preventing dependency warnings.

Published Jun 17, 2025
Version 1.0
No CommentsBe the first to comment