> ## Documentation Index
> Fetch the complete documentation index at: https://docs.zeroset.com/llms.txt
> Use this file to discover all available pages before exploring further.

# AKS

> Deploying Nebula Enterprise on Azure Kubernetes Service with Workload Identity, Azure Database for PostgreSQL, Azure Blob Storage, and DynamoDB-compatible orchestration

This guide covers a production Nebula install on AKS using Azure-managed Postgres, Azure Blob Storage (S3-compatible endpoint), DynamoDB-compatible orchestration tables, and Azure Key Vault secrets via External Secrets Operator.

## Prereqs

Before `helm install`, the following must be in place on the cluster side.

### Cluster

* **AKS 1.30+**
* **OIDC issuer enabled** on the cluster (`az aks update --enable-oidc-issuer --enable-workload-identity`) — required for Workload Identity federation
* Cluster nodes must have outbound internet access, or images must be mirrored to Azure Container Registry (ACR) first

### Addons + controllers

| Component                                           | Purpose                                                  | Install reference                                                                              |
| --------------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
| Cluster Autoscaler (or Karpenter for Azure preview) | Node autoscaling                                         | AKS addon: `--enable-cluster-autoscaler`                                                       |
| nginx Ingress Controller (or AGIC)                  | HTTP/HTTPS ingress                                       | [kubernetes.github.io/ingress-nginx](https://kubernetes.github.io/ingress-nginx/deploy/#azure) |
| Azure Disk CSI Driver                               | Premium SSD volumes for graph-engine / compactor / Queue | AKS built-in: enabled by default on AKS 1.21+                                                  |
| cert-manager                                        | TLS certificate provisioning from Let's Encrypt          | [cert-manager.io/docs](https://cert-manager.io/docs/installation/)                             |
| External Secrets Operator (recommended)             | Sync from Azure Key Vault                                | [external-secrets.io](https://external-secrets.io/)                                            |

### Azure-managed resources (recommended)

* **Azure Database for PostgreSQL Flexible Server** in the same virtual network as the AKS cluster. Nebula requires `vector`, `pg_partman`, and `pg_cron`; in the Azure portal, navigate to **Server parameters** → `azure.extensions` and add the required extensions, then enable `pg_cron` preloading before bootstrap. Run `nebula-enterprise postgres provision` to create the Nebula database, user, extensions, and chart credential Secret. Private access (VNet-integrated) is strongly recommended.
* **Azure Blob Storage account** with a container for graph segments. The chart's object storage path uses Azure Blob's S3-compatible API endpoint — see the note under [Object storage](#install) below.
* **DynamoDB-compatible service** for orchestration state, reachable from Nebula pods. Before Helm install, run `nebula-enterprise orchestration dynamodb ensure` to create or verify the four `pk` / `sk` orchestration tables and writer-authority records; use `--endpoint-url` for non-AWS endpoints and set the matching `NEBULA_ORCHESTRATION_DYNAMODB_*` values in the chart.

**Known limitation:** the chart's `objectStorage` block emits S3-protocol environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `S3_ENDPOINT_URL`). Azure Blob exposes an S3-compatible endpoint (Storage account → Settings → S3 compatibility, currently in preview). Enable it and use HMAC access keys as the `credentialsSecret`. If the S3-compat preview is not available in your region or subscription tier, run a MinIO gateway in front of Azure Blob as a bridge.

### Workload Identity setup

Workload Identity replaces the legacy aad-pod-identity approach. Steps:

1. Create a managed identity in the same resource group as the cluster:
   ```bash theme={null}
   az identity create --name nebula-wi --resource-group <rg>
   ```

2. Federate the managed identity with the AKS OIDC issuer for the Nebula service account:
   ```bash theme={null}
   AKS_OIDC_ISSUER="$(az aks show --name <cluster> --resource-group <rg> \
     --query 'oidcIssuerProfile.issuerUrl' -o tsv)"
   az identity federated-credential create \
     --name nebula-federated \
     --identity-name nebula-wi \
     --resource-group <rg> \
     --issuer "$AKS_OIDC_ISSUER" \
     --subject "system:serviceaccount:nebula:<release>-nebula-sa" \
     --audience api://AzureADTokenExchange
   ```
   Replace `<release>` with your `helm install` release name (e.g. `nebula`).

3. Grant the managed identity **Storage Blob Data Contributor** on the Blob container and **Key Vault Secrets User** on the Key Vault if ESO uses the same identity.

4. Record the managed identity **Client ID** — you'll set it under `serviceAccount.annotations` in your values file.

## Install

### 1. Push images to your ACR

```bash theme={null}
tar -xzf nebula-enterprise-<version>.tar.gz
cd nebula-enterprise-<version>/
sha256sum -c checksums.txt
docker load -i images.tar

ACR=<your-registry>.azurecr.io
az acr login --name <your-registry>

docker tag nebula:enterprise-<version>              "${ACR}/nebula/nebula-runtime:<version>"
docker tag nebula-graph-engine:enterprise-<version> "${ACR}/nebula/graph-engine:<version>"
docker tag nebula-postgres:enterprise-<version>     "${ACR}/nebula/postgres:<version>"
docker push "${ACR}/nebula/nebula-runtime:<version>"
docker push "${ACR}/nebula/graph-engine:<version>"
docker push "${ACR}/nebula/postgres:<version>"
```

For air-gapped AKS (no public-registry egress), also mirror required third-party images to ACR and override `image.*.repository` in your values file:

```bash theme={null}
docker tag public.ecr.aws/docker/library/busybox:1.37.0       "${ACR}/busybox:1.37.0"
docker push "${ACR}/busybox:1.37.0"
```

Then set the mirrored repositories in your values file:

```yaml theme={null}
image:
  busybox:
    repository: busybox
```

### 2. Seed secrets in Azure Key Vault

Create a Key Vault and store one secret per Nebula key, or store a JSON blob at a single secret name and use ESO's `dataFrom` extraction. Example using individual secrets:

Generate the JWT RSA private key with the commands in [Service authentication](/enterprise/security#service-authentication) before setting `NEBULA-JWT-PRIVATE-KEY-PEM`.

```bash theme={null}
az keyvault secret set --vault-name <kv> --name OPENAI-API-KEY      --value "sk-..."
az keyvault secret set --vault-name <kv> --name NEBULA-SECRET-KEY   --value "$(openssl rand -hex 32)"
az keyvault secret set --vault-name <kv> --name NEBULA-SERVICE-API-KEY --value "$(openssl rand -hex 32)"
az keyvault secret set --vault-name <kv> --name NEBULA-WEBHOOK-HMAC-SECRET --value "$(openssl rand -hex 32)"
az keyvault secret set --vault-name <kv> --name NEBULA-JWT-KID --value "nebula-YYYY-MM"
az keyvault secret set --vault-name <kv> --name NEBULA-JWT-PRIVATE-KEY-PEM --file nebula-jwt-private.pem
az keyvault secret set --vault-name <kv> --name NEBULA-JWT-RETIRED-PUBLIC-KEYS-JSON --value "[]"
az keyvault secret set --vault-name <kv> --name NEBULA-INTERNAL-WAKE-TOKEN --value "$(openssl rand -hex 32)"
```

`NEBULA-JWT-RETIRED-PUBLIC-KEYS-JSON` can stay `[]` on a fresh install. Populate it only during JWT signing-key rotation; see [Service authentication](/enterprise/security#service-authentication).

For an empty Flexible Server, use the bundle helper as the canonical logical bootstrap:

```bash theme={null}
PGPASSWORD=<admin-password> \
./nebula-enterprise postgres provision \
  --namespace nebula \
  --admin-url "postgresql://postgres@<server>.postgres.database.azure.com:5432/postgres?sslmode=require" \
  --nebula-database nebula \
  --nebula-user nebula \
  --nebula-secret nebula-postgres-credentials
```

If your platform team provisions Postgres separately, mirror the same contract: a Nebula user/database, required extensions in the Nebula database, and a Kubernetes Secret with `username` and `password` keys. Run the read-only verifier before Helm install:

```bash theme={null}
PGPASSWORD=<admin-password> \
./nebula-enterprise postgres verify \
  --namespace nebula \
  --admin-url "postgresql://postgres@<server>.postgres.database.azure.com:5432/postgres?sslmode=require" \
  --nebula-database nebula \
  --nebula-user nebula \
  --nebula-secret nebula-postgres-credentials
```

### 3. Copy + fill the reference values file

The bundle ships `helm/examples/aks/values.yaml` with every AKS-specific knob pre-wired. Copy it, fill in the `<placeholder>` markers (ACR login server, Flexible Server hostname, Blob storage account, managed identity client ID, Key Vault name, domain), and save as `your-values.yaml`.

### 4. Install

```bash theme={null}
helm install nebula ./helm/nebula-<version>.tgz \
  -n nebula --create-namespace \
  -f helm/examples/_common/production-sizing.yaml \
  -f your-values.yaml
```

`_common/production-sizing.yaml` is the shared production-shape sizing block (replicas, CPU/memory requests + limits, persistence) used by all three cloud-managed K8s examples (EKS/AKS/GKE). Omit it to keep the chart's minimal-dev defaults; override per-workload in `your-values.yaml` to fit your AKS node SKUs.

The chart runs schema migrations and catalog-apply automatically via a per-revision Job (`<release>-nebula-migrations-<revision>`); API and worker pods gate startup on an init container that polls `public.nebula_release_contract` for the install's release row. `releaseContract.releaseId` and `releaseContract.gitSha` are stamped into the bundled values by `bundle.sh` and are consumed automatically.

### 5. Verify

```bash theme={null}
az aks get-credentials --name <cluster> --resource-group <rg>
kubectl -n nebula get pods
kubectl -n nebula get ingress nebula
curl -fsS https://nebula.<your-domain>.com/v1/health
```

## Upgrade

Pull the new bundle, push new images to your ACR, then:

```bash theme={null}
helm upgrade nebula ./helm/nebula-<new-version>.tgz \
  -n nebula \
  -f your-values.yaml
```

## Sizing reference

| Workload     | Starter                    | When to scale                                   |
| ------------ | -------------------------- | ----------------------------------------------- |
| API          | 2 replicas, 1 CPU / 2-4 GB | HPA on CPU >70% sustained                       |
| Worker       | 2 replicas, 2 CPU / 4-8 GB | HPA on queue depth (Orchestration metric)       |
| Graph engine | 2 replicas, 2 CPU / 4-8 GB | Manual; restart-sensitive (WAL replay)          |
| Compactor    | 1 replica, 1 CPU / 2-4 GB  | Single-writer; do not scale horizontally        |
| Queue        | 1 replica, 8 GB PVC        | Single-broker is fine up to \~10k workflows/min |

Recommended AKS node SKUs for the starter shape: `Standard_D4s_v5` (4 vCPU / 16 GB) for API, worker, and Orchestration; `Standard_D8s_v5` (8 vCPU / 32 GB) for graph-engine and compactor.

## Troubleshooting

<AccordionGroup>
  <Accordion title="Workload Identity not bound — pods receive 401 from Azure APIs">
    Check that the managed identity's federated credential subject exactly matches `system:serviceaccount:<namespace>:<release>-nebula-sa`. The release name prefix is part of the service account name. Confirm with `kubectl -n nebula get sa` and compare to `az identity federated-credential list --identity-name nebula-wi --resource-group <rg>`.
  </Accordion>

  <Accordion title="Ingress provisioning slow or stuck">
    nginx Ingress on AKS provisions a public Azure Load Balancer automatically. The provisioning can take 3-5 minutes on a fresh cluster. Check `kubectl -n ingress-nginx get svc ingress-nginx-controller` for the external IP assignment. If it stays in `Pending`, verify that the cluster's subnet has enough IP space and that the AKS service principal / managed identity has `Network Contributor` on the virtual network.
  </Accordion>

  <Accordion title="pgvector missing on first start — API reports 'extension vector does not exist'">
    The `azure.extensions` server parameter must include `vector` before bootstrap. Then run `nebula-enterprise postgres provision` or have your platform workflow satisfy `nebula-enterprise postgres verify`; the contract requires the extension to be enabled at the server level and installed in the Nebula database.
  </Accordion>

  <Accordion title="Blob credentials rejected — graph-engine 'InvalidAccessKeyId'">
    Azure Blob's S3-compatible endpoint requires HMAC keys, not the storage account connection string. Generate HMAC keys under **Storage account** → **Access keys** → **Enable S3 compatible HMAC**. Store the Access Key ID and Secret Access Key in the Kubernetes Secret referenced by `objectStorage.credentialsSecret` with keys `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` (those exact uppercase names — the chart's `nebula.objectStorageEnv` helper reads them via `secretKeyRef.key`).
  </Accordion>
</AccordionGroup>
