Skip to main content

Someone wanted to learn this too, so Grasp built them a personal learning path.

You can keep exploring this one or create your own

Deploying a React Frontend with REST and WebSocket Ingress on AKS

Hello. Last lesson, you made the FastAPI backend reachable through an internal ClusterIP Service, but not from a browser outside AKS. This lesson adds the React frontend and one browser-facing entry point. You will route ordinary REST requests and WebSocket connections to the backend while keeping both application Services internal.

Allow about 45–55 minutes, including the readings and checks. This lab uses a public HTTP endpoint to verify routing, so use test data only: do not send passwords, session tokens, or other sensitive information through it. Before enabling ingress, check your remaining Azure budget and plan to remove the public endpoint when you finish.


Understand the two kinds of routing involved

Your backend Service already selects ready backend Pods. The frontend will have its own Deployment and internal Service. An Ingress resource then declares which URL paths should go to which Service; an ingress controller watches those declarations and configures a reverse proxy to handle the requests. The Ingress declaration alone does not run a proxy or create an external address.

The diagram depicts a client reaching a load balancer and reverse proxy that route requests toward application Services and Pods. The Ingress and IngressClass shown beside the controller are configuration objects, not additional stops taken by each HTTP request.

For this lab, the intended browser-visible paths are:

Browser request Internal destination Path received by the application
/ and frontend assets frontend Service Unchanged
/api/health/live, and other REST paths under /api/ backend Service /health/live, and the corresponding path without /api
/ws/updates backend Service /ws/updates, unchanged

The REST path rewrite is necessary for this example because the backend from the previous lesson has /health/live, not /api/health/live. The /ws route deliberately has no rewrite. If your actual FastAPI WebSocket endpoint has a different path, change both the browser URL and its Ingress rule to match it.

We will use the AKS application routing add-on to supply a managed NGINX ingress controller, rather than installing a second controller with Helm.

Managed NGINX ingress with the application routing add-on

Read Microsoft's AKS guide for the existing-cluster enablement command, the Ingress class supplied by the add-on, and how to find its public address.

In “Enable application routing using Azure CLI” → “Enable on an existing cluster,” read the enablement instruction. Under “Deploy an application” → “Create the Ingress object,” read the class explanation and its YAML example. Finally, in “Verify the managed Ingress was created,” locate the kubectl get ingress and kubectl get service examples. Focus on the distinction between creating a routing rule and obtaining a public address for the controller.


Check the cluster and the likely charges first

Enabling application routing may provision a public Azure load balancer address and runs controller workloads on your AKS nodes. The frontend Pod also needs node capacity. The additional bill depends on your region, Azure networking prices, any traffic, and whether your existing node can fit those workloads; do not assume this change is free. The node, disk, ACR, and any PostgreSQL instance you have kept running continue to accrue charges.

Using the previous lesson’s illustrative node rate of roughly USD 0.04–0.08 per hour, 50 minutes of node time is roughly USD 0.03–0.07, plus those other resources and any new networking charges. This is not an estimate for leaving ingress running all month. Check the current prices and budget alerts from the earlier cost-planning module before proceeding; if an additional public endpoint is outside your budget, stop before enabling the add-on.

Cost of the following checks: they create no new Azure resources, but existing resources continue to run.

kubectl config current-context
kubectl get nodes
kubectl -n app-lab get deployment backend
kubectl -n app-lab get service backend
kubectl -n app-lab get endpointslice \
  -l kubernetes.io/service-name=backend
kubectl get ingressclass

Confirm that the backend is ready and that its Service has a ready destination. Also check that your AKS cluster uses a managed identity, which the add-on requires. If the webapprouting.kubernetes.azure.com IngressClass already exists, check whether application routing is already enabled rather than enabling it again.

If your AKS configuration is Terraform-managed, enabling an add-on with the CLI is a change outside Terraform. Record it, inspect the next Terraform plan for drift, and reconcile the desired configuration before a later apply. Do not apply a plan that unintentionally removes the add-on during this lab.


Deploy the frontend as an internal workload

A React build is static browser code, but something in your container must serve that code over HTTP—for example, NGINX. The manifest below assumes your versioned frontend image in ACR listens on port 80, serves / successfully, and is configured to return the frontend entry page for any client-side routes your application uses. Verify those facts against your image; a development server or a different production image may use another port.

Create k8s/frontend-deployment.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
  namespace: app-lab
spec:
  replicas: 1
  selector:
    matchLabels:
      app: grasp-frontend
  template:
    metadata:
      labels:
        app: grasp-frontend
    spec:
      containers:
        - name: frontend
          image: yourregistry.azurecr.io/frontend:your-version
          ports:
            - name: http
              containerPort: 80
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              cpu: 250m
              memory: 256Mi
          readinessProbe:
            httpGet:
              path: /
              port: http
            initialDelaySeconds: 3
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /
              port: http
            initialDelaySeconds: 10
            periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: frontend
  namespace: app-lab
spec:
  type: ClusterIP
  selector:
    app: grasp-frontend
  ports:
    - name: http
      port: 80
      targetPort: http

Replace the image name and tag, and adjust the port and probe path to match your container. These resource settings are small starting values, not measured requirements; a Pod that cannot be scheduled or repeatedly exceeds its limits needs investigation. Like the backend Service, this frontend Service remains internal.

Before deploying the image, ensure the browser code uses browser-reachable URLs. A Kubernetes name such as backend.app-lab.svc.cluster.local is useful inside the cluster, but a visitor’s browser cannot resolve it. For this example, use /api as the REST base path and construct WebSocket URLs from the page’s current host:

const apiBase = "/api"; // for example: fetch(`${apiBase}/items`)

const wsScheme = window.location.protocol === "https:" ? "wss" : "ws";
const socket = new WebSocket(
  `${wsScheme}://${window.location.host}/ws/updates`
);

Use your real REST and WebSocket endpoint paths. Take care not to add /api twice if your frontend already includes it in individual requests. If these URLs are compiled into a React production build, changing a Kubernetes environment variable afterward will not change the JavaScript already built into the image: rebuild and push a versioned image instead, unless you implemented runtime configuration.

Cost of applying and testing the frontend: a Deployment and ClusterIP Service do not themselves request another public IP. The Pod consumes existing billable node capacity; if it cannot fit, do not increase node capacity before revisiting your budget. ACR storage and image pulls may also contribute to your bill.

kubectl apply -f k8s/frontend-deployment.yaml
kubectl -n app-lab rollout status deployment/frontend --timeout=120s
kubectl -n app-lab get pods -l app=grasp-frontend
kubectl -n app-lab get endpointslice \
  -l kubernetes.io/service-name=frontend

A ready frontend Pod and a ready Service destination establish the internal half of the route. They do not yet show that a browser can reach it.


Enable one ingress controller and declare the paths

If the application routing add-on is not already enabled, set your actual resource group and cluster name, then enable it:

export RESOURCE_GROUP="your-aks-resource-group"
export CLUSTER_NAME="your-aks-cluster"

az aks approuting enable \
  --resource-group "$RESOURCE_GROUP" \
  --name "$CLUSTER_NAME"

kubectl get ingressclass
kubectl -n app-routing-system get service nginx

Wait if the controller Service has no external IP yet. The class used below must appear as webapprouting.kubernetes.azure.com. Do not install another ingress controller just to compensate for an add-on that has not finished starting.

Cost of enabling the add-on: this is the step most likely to add networking charges, including a public IP/load balancer, alongside controller resource use on billable nodes. The precise amount is region- and configuration-dependent. Check the actual Azure resources created and remove the add-on after the lab if you do not need it.

The REST rewrite needs an NGINX annotation. We will put it on a separate Ingress object, so it does not also rewrite the frontend’s asset URLs or WebSocket paths.

Configure multiple ingress controllers and NGINX ... - Microsoft Learn

Read Microsoft's guidance on per-Ingress annotations and URL rewriting to understand why the REST route below needs a capture group, while the other routes do not.

In “Configuration per ingress resource through annotations,” read how annotations are applied. Then go to “URL rewriting.” Begin at the path mismatch, and inspect the example using rewrite-target and use-regex. You can leave the other annotation examples for later; this lab does not require CORS because the planned browser requests are same-origin.

Create k8s/app-ingress.yaml:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: backend-rest
  namespace: app-lab
  annotations:
    nginx.ingress.kubernetes.io/use-regex: "true"
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  ingressClassName: webapprouting.kubernetes.azure.com
  rules:
    - http:
        paths:
          - path: /api(/|$)(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: backend
                port:
                  number: 8000
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: frontend-and-websocket
  namespace: app-lab
spec:
  ingressClassName: webapprouting.kubernetes.azure.com
  rules:
    - http:
        paths:
          - path: /ws
            pathType: Prefix
            backend:
              service:
                name: backend
                port:
                  number: 8000
          - path: /
            pathType: Prefix
            backend:
              service:
                name: frontend
                port:
                  number: 80

The REST expression matches /api and paths beneath /api/. Its second capture group is the remainder of the path, which NGINX forwards after the leading slash: /api/health/live reaches FastAPI as /health/live. The /ws rule passes /ws/updates through unchanged, allowing an HTTP WebSocket upgrade to reach the backend. The / rule catches frontend requests that do not match the more specific paths.

This is an IP-address, hostless lab configuration, not a production exposure design. It has no TLS configuration. Do not put authentication credentials or sensitive application data through it; a production deployment needs an appropriate hostname, HTTPS certificate, and access controls.

Cost of applying the Ingress objects: the rules themselves do not require a second public load balancer; they use the controller you enabled. The controller and its public networking resources keep accruing any applicable charges while present.

kubectl apply -f k8s/app-ingress.yaml
kubectl -n app-lab get ingress
kubectl -n app-lab describe ingress backend-rest
kubectl -n app-lab describe ingress frontend-and-websocket

Verify the whole browser path, including WebSockets

Retrieve the controller’s public IP. If it is empty, inspect the controller Service and wait rather than testing a guessed address:

kubectl -n app-routing-system get service nginx

INGRESS_IP=$(kubectl -n app-routing-system get service nginx \
  -o jsonpath='{.status.loadBalancer.ingress[0].ip}')

printf 'Ingress IP: %s\n' "$INGRESS_IP"

Once an IP appears, check the frontend page and the rewritten backend health route:

curl -i "http://${INGRESS_IP}/"
curl -i "http://${INGRESS_IP}/api/health/live"

Use a real, non-sensitive REST endpoint from your application as a further check. A successful health request proves that the route reaches FastAPI, but it does not prove that every application REST path is correct.

Open http://<INGRESS_IP>/ in a browser and inspect its developer tools:

  1. Confirm that the page and its JavaScript assets load. A page that returns HTML but has missing assets may have an incorrect frontend build path or static-server configuration.
  2. Trigger a normal REST action. In Network, check its request URL, response, and any browser-console errors. It should use the same host as the page and a path under /api/.
  3. Trigger the application’s WebSocket feature. In Network → WS, look for the actual /ws/... request, a successful 101 Switching Protocols handshake, and the expected messages. A 101 alone proves the upgrade, not that your application’s message handling works.

Browsers use ws:// for an HTTP page and wss:// for an HTTPS page; do not hard-code ws:// into a future HTTPS build. The managed NGINX proxy can handle the WebSocket upgrade, but the backend still needs a matching WebSocket endpoint, and the frontend must call its correct path. If your connection closes only after being idle, review the NGINX “Custom connection timeout” subsection in the reading rather than changing timeouts before you have observed a problem.

When something fails, narrow down the layer instead of changing several rules at once. Check both EndpointSlices again, then inspect the Ingress descriptions and relevant Pods:

kubectl -n app-lab get endpointslice \
  -l kubernetes.io/service-name=backend
kubectl -n app-lab get endpointslice \
  -l kubernetes.io/service-name=frontend
kubectl -n app-lab describe pods -l app=grasp-frontend
kubectl -n app-lab describe pods -l app=grasp-backend

No ready destination points toward a workload, selector, or readiness problem. A frontend that loads but calls backend:8000 from browser JavaScript has a browser URL problem. A 404 on /api/... may mean the REST rule did not match or the rewritten path is not a real FastAPI route. A failed WebSocket handshake calls for checking its exact browser URL and backend route before adding proxy annotations. Inspect application logs cautiously: avoid logging credentials or WebSocket message contents that contain private data.

Cost of verification: these requests create no new Azure resource. Node and public-networking charges continue during testing; external traffic may add usage charges. Keep the test brief and use only non-sensitive data over this HTTP endpoint.


Clean up the public endpoint when you finish

Delete the Ingress rules first. If this was a temporary add-on enabled solely for this lab, disable it and verify that its public Service and associated Azure networking resources have been removed:

kubectl delete -f k8s/app-ingress.yaml

az aks approuting disable \
  --resource-group "$RESOURCE_GROUP" \
  --name "$CLUSTER_NAME"

kubectl -n app-routing-system get service nginx

Skip the disable command if another workload relies on the add-on or your reviewed Terraform configuration expects it to remain enabled. Deleting the frontend workload is separate:

kubectl -n app-lab delete service frontend
kubectl -n app-lab delete deployment frontend

You may retain the frontend and backend manifests for later delivery work. Removing Pods or Ingress rules does not stop AKS node, disk, ACR, or database charges. If pausing the course, use your reviewed Terraform destroy procedure for the lab infrastructure and verify deletion, accounting separately for resources such as ACR and PostgreSQL.

The key distinction is now practical: Services provide stable internal destinations; Ingress rules describe browser-facing paths; a running controller and its public entry point make those rules reachable. Your React build must use URLs the browser can reach, while REST rewriting and an unchanged WebSocket path must agree with the routes FastAPI actually implements. Next, the course moves from manual image deployment toward building and delivering versioned images with GitHub Actions.

Can't find a good explanation? Sign up and we'll make it for you

Sign up