AZURE LOCAL / LOCALBOX FIELD GUIDEL400 deployment and operations
L400 edition · Azure landing-zone design mapping · Recorded lab: October 2026 · Identifiers masked

4. Deploy AKS on Azure Local and NGINX

Readable vector view · pan horizontally on smaller screensOpen full-size SVG
Editable L400 Azure-icon engineering view for Deploy AKS on Azure Local and NGINX
Native draw.io vector SVG. Click to open full-size and zoom; save and open in draw.io to edit. Recorded lab and proposed landing-zone controls are labeled separately. Read all six architecture views and the control mapping.

4. Build a small AKS cluster on Azure Local

This creates AKS enabled by Azure Arc on the existing Azure Local instance, not a cloud-hosted AKS service. The infrastructure cluster still has two Azure Local nodes. The Kubernetes exercise adds separate virtual machines: one Linux worker and one mandatory control plane.

ComponentRecorded lab configurationBoundary
Worker1 × Standard_A2_v2; 2 vCPU / 4 GiBOne worker is not application-node high availability.
Control plane1 × Standard_K8S3_v1; 4 vCPU / 6 GiBRequired in addition to the worker; not a resilient control-plane design.
Kubernetes1.33.5 in the recorded deploymentDiscover currently supported versions before a new deployment.
NetworkVLAN 110; 10.10.0.0/24; node pool .101–.199; API .5Example lab addresses, not values to copy into an unrelated network.
ApplicationOne NGINX replica; ClusterIP serviceNo public load balancer or internet ingress.
Do not run this chapter against an existing cluster with the same name. First inspect its state. A generic portal “Prerequisites not met” card is not a diagnosis; verify the actual bridge, extensions, workload network, capacity, and authorization.

Admission contract: workload placement is not Kubernetes availability

Inputs: chapter 3's fresh platform readiness, supported version/role-specific sizes discovered on this custom location, nonoverlapping IP reservations, DNS/registry egress, an approved operator identity, and a reviewed manifest. The recorded Kubernetes 1.33.5 is historical, not a permanently supported selection. Reserve addresses for the API, all nodes, expected upgrades/replacements, and any separately planned service VIPs; never allocate a VIP or API address from an already occupied node pool. Check pod/service CIDRs against host, VPN and routed networks before creation.

The proposed ALZ target places workload ownership under Local's Applications subscription and infrastructure under Azure Local Clusters; this example still uses the existing single lab subscription/resource group. Validate supported provider/custom-location and cross-subscription relationships before implementing that separation. Resource organization and Azure RBAC govern who may create or connect; Kubernetes authorization governs API objects; network policy and actual routing govern application traffic. Microsoft Entra ID authentication does not grant AD cluster administration or guest SSH access.

The platform team owns bridge/operator health, shared capacity and logical-network prerequisites; the Kubernetes operator owns cluster lifecycle and namespace authorization; the application owner owns probes, dependencies, image provenance and restore. Security/governance require scoped roles and Audit-first compatible policies, not an inherited blanket Deny that disables required Arc connectivity. Tenant/billing sets the authorized cost boundary; management sets upgrade/restore evidence; automation pins CLI/schema/manifest inputs. These map this stage to the ALZ design areas without claiming an enterprise platform was deployed.

Production proposal: choose supported resilient control-plane and worker counts, failure-domain placement and spare capacity from the current AKS support matrix. Replicas must be schedulable on surviving workers during drain/failure; a PodDisruptionBudget cannot manufacture spare capacity. The one-control-plane, one-worker, one-replica lab cannot validate those properties. Cluster HA is also not backup: define workload/etcd or supported cluster-recovery protection, persistent-data RPO/RTO, and a tested application restore rather than assuming rescheduling recovers lost data.

4.1 Prepare the client and verify the platform

Use PowerShell 7 on your management computer. Enter private identifiers locally. This example uses a separate Azure CLI configuration directory so it does not change another workload's default subscription. Install a missing CLI extension only after confirming it is absent.

$ErrorActionPreference = 'Stop'
$Sub = Read-Host 'Subscription ID'
$Tenant = Read-Host 'Tenant ID'
$Rg = Read-Host 'Existing Azure Local resource-group name'
$Location = 'australiaeast'
$CustomLocationName = 'jumpstart'
$ClusterName = 'localbox-aks'
$env:AZURE_CONFIG_DIR = Join-Path $HOME '.azure-local-lab'
az login --tenant $Tenant --use-device-code --output none
if ($LASTEXITCODE) { throw 'Azure sign-in failed.' }
az account set --subscription $Sub
az extension list --query "[].{Name:name,Version:version}" -o table
# Only if missing:
# az extension add --name aksarc
# az extension add --name stack-hci-vm
# az extension add --name connectedk8s

$CustomLocation = az customlocation show --subscription $Sub `
  --resource-group $Rg --name $CustomLocationName --query id -o tsv
if ($LASTEXITCODE -or -not $CustomLocation) { throw 'Custom location is unavailable.' }
az arcappliance list --subscription $Sub --resource-group $Rg `
  --query "[].{Name:name,Provisioning:provisioningState,Status:status}" -o table
az resource list --subscription $Sub --resource-group $Rg `
  --query "[?contains(type,'extensions')].{Name:name,Type:type}" -o table
  1. Require the resource bridge to be Running and its provisioning to be Succeeded.
  2. Inspect the custom location's linked VM/AKS operator extensions in the portal; require Succeeded rather than reinstalling them blindly.
  3. On a cluster node, confirm both Azure Local nodes are Up, shared storage is Online, and capacity is available for the control plane and worker.
  4. Confirm the actual external virtual switch and VLAN routing on both nodes. The recorded lab switch was ConvergedSwitch(compute_management). Do not substitute a guessed switch name.

4.2 Create a dedicated workload logical network

Do not reuse the platform infrastructure logical network. Reserve the API address outside the node allocation pool and confirm that all addresses are free. The following uses the Azure CLI native entry point on Windows to preserve parentheses in the switch name; it is a direct CLI invocation, not a custom helper script. Change the executable path if your Azure CLI installation differs.

$Az = 'C:\Program Files\Microsoft SDKs\Azure\CLI2\python.exe'
$LnetName = 'localbox-aks-lnet-vlan110'
& $Az -IBm azure.cli stack-hci-vm network lnet create `
  --subscription $Sub --resource-group $Rg --location $Location `
  --custom-location $CustomLocation --name $LnetName `
  --vm-switch-name 'ConvergedSwitch(compute_management)' `
  --ip-allocation-method Static --address-prefixes 10.10.0.0/24 `
  --gateway 10.10.0.1 --dns-servers 192.168.1.254 --vlan 110 `
  --ip-pool-start 10.10.0.101 --ip-pool-end 10.10.0.199 --ip-pool-type vm
if ($LASTEXITCODE) { throw 'Logical network creation failed.' }
$Lnet = az stack-hci-vm network lnet show --subscription $Sub `
  --resource-group $Rg --name $LnetName -o json | ConvertFrom-Json
if ($Lnet.properties.provisioningState -ne 'Succeeded') {
  throw 'The workload logical network is not ready.'
}
$LnetId = $Lnet.id

This is a create operation for a new network. If the logical network already exists, inspect its switch, subnet, gateway, VLAN and pool instead of rerunning create to change it. VLAN 110 is meaningful only because the example lab router and node networking already support it.

4.3 Discover supported versions and sizes, then generate a key

az aksarc vmsize list --subscription $Sub --resource-group $Rg `
  --custom-location $CustomLocationName -o table
az aksarc get-versions --subscription $Sub --resource-group $Rg `
  --custom-location $CustomLocationName -o json
$Version = Read-Host 'Select a currently supported Linux Kubernetes version'
$Key = Join-Path $HOME '.ssh\azure-local-aks'
if (Test-Path $Key) { throw 'Choose a new dedicated key path or review the existing key.' }
New-Item -ItemType Directory -Path (Split-Path $Key) -Force | Out-Null
# PowerShell7: pass a genuinely empty passphrase, not a literal pair of quote characters.
$PSNativeCommandArgumentPassing = 'Standard'
ssh-keygen -t ed25519 -f $Key -N '' -C 'Azure Local AKS lab'
if ($LASTEXITCODE) { throw 'SSH key generation failed.' }

Keep the private key owner-restricted. Only the .pub file is supplied to cluster creation. Do not publish either credentials or kubeconfig. A listed size is not sufficient: it must also be supported for its intended role. The recorded small worker size was not used as the control-plane size.

4.4 Validate, review, then create once

The first command validates the same input used by the second. Stop on errors. Before the create command, check Azure for an existing cluster or active deployment with the selected name. Do not create a duplicate when a command times out.

$Create = @(
  'aksarc','create','--subscription',$Sub,'--resource-group',$Rg,
  '--name',$ClusterName,'--location',$Location,
  '--custom-location',$CustomLocation,'--vnet-ids',$LnetId,
  '--kubernetes-version',$Version,'--control-plane-count','1',
  '--control-plane-vm-size','Standard_K8S3_v1','--control-plane-ip','10.10.0.5',
  '--node-count','1','--node-vm-size','Standard_A2_v2',
  '--enable-azure-rbac','--ssh-key-value',"$Key.pub"
)
& $Az -IBm azure.cli @Create --validate
if ($LASTEXITCODE) { throw 'AKS validation failed; no create request should follow.' }

# Review the validation result and cloud deployment inventory before this write.
& $Az -IBm azure.cli @Create
if ($LASTEXITCODE) {
  throw 'Inspect the actual ARM deployment before deciding whether any retry is safe.'
}

The recorded CLI returned an early ResourceNotFound when a no-wait post-submit read raced creation. ARM was still running. The correct response was to inspect the existing deployment, not submit another cluster. This example waits instead of relying on that no-wait behavior. Cluster provisioning, Arc connectivity, Kubernetes Ready, and application readiness remain separate gates.

4.5 Authorize only the intended lab administrator

Azure RBAC is enabled at creation. Have an authorized administrator assign the intended user appropriate cluster-scoped access. The example below intentionally grants cluster administration for this disposable lab; production should use narrower namespace/workload roles. Enter the directory object ID after verifying the person and tenant. It is not an email guessed from a name.

$ClusterId = az connectedk8s show --subscription $Sub --resource-group $Rg `
  --name $ClusterName --query id -o tsv
if ($LASTEXITCODE -or -not $ClusterId) { throw 'The Arc cluster is not queryable yet.' }
$UserObjectId = Read-Host 'Verified intended administrator directory object ID'
az role assignment create --subscription $Sub --scope $ClusterId `
  --assignee-object-id $UserObjectId --assignee-principal-type User `
  --role 'Azure Arc Kubernetes Cluster Admin' --output none
if ($LASTEXITCODE) { throw 'Cluster-scoped role assignment failed.' }
az role assignment create --subscription $Sub --scope $ClusterId `
  --assignee-object-id $UserObjectId --assignee-principal-type User `
  --role 'Azure Arc Enabled Kubernetes Cluster User Role' --output none
if ($LASTEXITCODE) { throw 'Cluster-connect role assignment failed.' }

4.6 Connect with CLI only: Terminal A

Install kubectl and kubelogin on the client using the official client instructions if missing. In each terminal, set the same isolated Azure configuration, subscription, resource group and kubeconfig path. Do not copy this guide's original token cache; authenticate your own single-account context.

$env:AZURE_CONFIG_DIR = Join-Path $HOME '.azure-local-lab'
$Sub = Read-Host 'Subscription ID'
$Rg = Read-Host 'Existing resource-group name'
$ClusterName = 'localbox-aks'
$KubeConfig = Join-Path $HOME '.kube\azure-local-lab'
New-Item -ItemType Directory -Path (Split-Path $KubeConfig) -Force | Out-Null
Get-NetTCPConnection -State Listen -LocalPort 47031 -ErrorAction SilentlyContinue

# If the lab proxy is not already running, keep this command open in Terminal A.
az connectedk8s proxy --subscription $Sub --resource-group $Rg `
  --name $ClusterName --file $KubeConfig --port 47031 `
  --kube-context azure-local-lab

The proxy blocks the terminal. Port 47031 is the private Kubernetes API proxy, not the NGINX website. Explicit kubeconfig selection prevents changing an unrelated cluster context. If the proxy is already healthy, use it instead of opening another listener on the same port.

4.7 Apply the complete NGINX example: Terminal B

Set the same terminal variables, then save the following as nginx.yaml, or download the exact YAML file. It creates one namespace, one Deployment and one private service. The mutable image tag reproduces the recorded example; pin a reviewed digest for deterministic production delivery.

apiVersion: v1
kind: Namespace
metadata:
  name: nginx-demo
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
  namespace: nginx-demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      automountServiceAccountToken: false
      nodeSelector:
        kubernetes.io/os: linux
      containers:
        - name: nginx
          image: nginx:stable-alpine
          ports:
            - name: http
              containerPort: 80
          resources:
            requests:
              cpu: 25m
              memory: 32Mi
            limits:
              cpu: 250m
              memory: 128Mi
          securityContext:
            allowPrivilegeEscalation: false
            seccompProfile:
              type: RuntimeDefault
          readinessProbe:
            httpGet:
              path: /
              port: http
            initialDelaySeconds: 3
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /
              port: http
            initialDelaySeconds: 10
            periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: nginx
  namespace: nginx-demo
spec:
  type: ClusterIP
  selector:
    app: nginx
  ports:
    - name: http
      port: 80
      targetPort: http
kubectl --kubeconfig $KubeConfig get nodes
# Require both expected nodes Ready before applying the application.
kubectl --kubeconfig $KubeConfig apply --dry-run=client -f .\nginx.yaml
kubectl --kubeconfig $KubeConfig apply -f .\nginx.yaml
kubectl --kubeconfig $KubeConfig --namespace nginx-demo `
  rollout status deployment/nginx --timeout=180s
kubectl --kubeconfig $KubeConfig --namespace nginx-demo get deployment,pods,services

Expect the Deployment 1/1 Ready, a Running/Ready pod, and a ClusterIP service without an external IP. Resource requests/limits and probes are included. The pod does not mount a service-account token and the service is not an internet-facing load balancer.

4.8 Forward the website: Terminal B, then browser

Get-NetTCPConnection -State Listen -LocalPort 8087 -ErrorAction SilentlyContinue
# Keep this running only if port8087 is not already occupied by a working lab forward.
kubectl --kubeconfig $KubeConfig --namespace nginx-demo `
  port-forward service/nginx 8087:80 --address 127.0.0.1

Open http://127.0.0.1:8087/ on the same computer. In a third terminal, curl.exe --fail http://127.0.0.1:8087/ should return Welcome to nginx! The browser path is local port 8087 → kubectl → the Arc API connection → NGINX port 80. No host SSH connection or public firewall rule is needed.

Redacted actual NGINX page through a private AKS forwarding connection
Recorded successful NGINX application capture in isolated Edge. This is genuine application evidence, not a portal creation screenshot or a newly executed deployment.

Ctrl+C in Terminal B closes only web forwarding. Stop Terminal A only when no other lab session needs it. If a pod connection changes, port-forward can terminate; verify pod readiness and reconnect. If authentication expires, sign in to the isolated context again. Do not redeploy the cluster merely because a client tunnel stopped.

4.9 Diagnose and reverse at the smallest failed boundary

Required evidence before declaring the workload operational
Gate / execution contextSuccess evidenceFailure mode and recovery
Azure CLI client → ARM / Arc over HTTPS 443.One completed deployment, intended cluster ID, connected bridge/extensions, scoped create/connect/API roles.403 or denied deployment: inspect the exact action/scope and inherited policy. Do not grant every tenant user cluster administration.
AKS nodes / platform network.All expected Kubernetes nodes Ready, free IPs/capacity, API reachability, healthy kube-system DNS pods.NotReady or address exhaustion: inspect node events, allocation pools, VLAN/DNS/egress. Repair dependencies, not individual ATC-managed switches.
Pod → registry and service dependencies.Reviewed image/digest can be pulled; kube-DNS resolves required names; requests fit available memory/CPU.ImagePullBackOff: inspect registry authentication, DNS and HTTPS egress; Pending: inspect scheduling/events and resource requests before resizing.
Application / service / client forward.Readiness succeeds, restart count stable, service selectors yield ready endpoints, loopback HTTP response received.CrashLoopBackOff or failed probes: inspect redacted pod logs/events and probe path/port. A Running pod or ClusterIP alone is not success.

The client proxy and application forward are separate management sessions: local port 47031 reaches the Kubernetes API, while local port 8087 forwards to the selected application backend. Neither provides public ingress, a routable service VIP, or general access to the workload subnet. Keep both loopback-bound. Registry/DNS testing must originate from the relevant nodes/pods; a successful laptop download does not prove cluster egress. Follow the AKS logical-network requirements and distinguish external DNS reachability from kube-DNS service health.

Rollback application changes with the reviewed previous manifest/image; a Deployment rollback alone does not reverse changed ConfigMaps, Services or database schemas. Delete only the dedicated namespace when its contents are confirmed disposable; do not remove a shared logical network or infrastructure extension. For failed cluster provisioning, inspect outstanding operations and use the supported cluster lifecycle cleanup after approval, retaining evidence first. Never delete AKS-managed VMs in Hyper-V as a shortcut.

Handoff: retain sanitized version/size discovery, validation and operation timestamps, role scopes, node/readiness/restart/resource/probe results, DNS/registry checks, and the HTTP observation interval. The screenshot establishes only the recorded NGINX response through the lab forward, not a new deployment or sustained SLA. Alert delivery, update rollback and restore remain unverified until their owners demonstrate them. Remove temporary elevated lab role assignments when access is no longer needed.

Sources