Deploy on Azure AKS
Install a self-hosted deployment into your own Azure subscription, on an AKS cluster with a sandbox-capable node.
Audience: IT (the enabler)
This guide assumes you are comfortable operating AKS. It does not cover general Kubernetes administration.
Before you start, read Cluster and node requirements. The cluster this guide builds is not a generic one, and its storage and runtime prerequisites are the part most likely to catch you out.
Reference Terraform is available.
The v3/azure-aks/reference-terraform (opens in a new tab) directory of the self-hosted resources repository provisions everything in Part 1, installs the sandbox runtime, creates each deployment's database and roles, and renders a completed values file for each deployment, so installing the application is the one step left to you.
It serves each deployment at an Azure-generated Azure Front Door hostname, the option in Step 1, so a proof of concept needs no domain or certificate of your own; its README explains the limits of that choice.
Treat it as a starting point to adapt, not a shape to keep: give your coding agent the directory, this guide, and your organization's standards, for example "Adapt this Terraform to our conventions: our naming and tagging, our existing virtual network, our state backend, and a hostname on our domain with its certificate in our Key Vault. Keep everything the deployment guide requires. Show me the plan before you change anything."
Review the result against Part 1: the node shape, the Kubernetes version, the workload identity federation, and the database roles are requirements; naming, networking, and the tool you provision with are yours to choose.
Part 1: Pre-deployment bill of materials
Hand this to procurement and your cloud and security teams on day 0.
Decide these carefully, they are painful to retrofit:
- Hostname. TLS, the OIDC redirect URI, and ingress all derive from it. Changing it later means re-issuing certificates and reconfiguring your identity provider.
- Region. Moving regions later means a rebuild.
Components
| Category | Component | Spec | Notes |
|---|---|---|---|
| Compute | AKS cluster | AKS 1.36 or newer, with the OIDC issuer, workload identity, and the managed Gateway API enabled, and the application routing add-on installed with its Gateway API implementation | Metrics Server and the Azure Disk CSI driver are built in. See Cluster and node requirements |
| Compute | Application node | A single x86_64 Standard_D32s_v5 (32 vCPU, 128 GiB), in a node pool fixed at one node with autoscaling off. It is the cluster's only node pool, so AKS system pods share the node | Hosts the application and every sandbox run. Not interchangeable with a general-purpose node pool. See Node sizing |
| Storage | Node OS disk | Managed disk, 200 GiB | Sized for the node image and container images |
| Storage | Data root StorageClass | Created by the application chart on the Azure Disk CSI driver: Premium SSD (Premium_LRS), encrypted at rest with platform-managed keys, WaitForFirstConsumer, expandable, reclaimPolicy: Delete | Nothing to create. Overridable with a custom class when you need provisioned IOPS or a customer-managed key; see Advanced configuration |
| Storage | Data root volume | A Premium SSD managed disk, 100 GiB by default, that a Pod of the application claims from that StorageClass on the node | A cache for materialized workspaces and sandbox images, not a system of record. Deleted with the node; a replacement starts cold. See The data root and Data root performance |
| Network | Virtual network | A node subnet for the cluster (Azure CNI Overlay) with the Microsoft.Storage service endpoint, and a subnet delegated to PostgreSQL Flexible Server | With CNI Overlay, the database and the storage account see the application's traffic from the node subnet |
| Network | Cluster ingress | A Gateway on the application routing add-on's Gateway API implementation, with the TLS certificate for your hostname in Azure Key Vault | You operate the Gateway and terminate TLS there. See Ingress |
| Network | DNS record | Your chosen hostname resolving to the Gateway's load balancer | Not needed with an Azure-generated hostname; see Step 1 |
| Data | PostgreSQL | Azure Database for PostgreSQL Flexible Server, PostgreSQL 17 or 18, General Purpose tier with at least 4 vCores, private access only. Allow-list PGCRYPTO and CITEXT in the azure.extensions server parameter | Primary application data store. A deployment holds well over a hundred connections across its pools, so check max_connections allows for it |
| Data | Blob storage | A general-purpose v2 storage account with one private container for the deployment, public blob access disabled, and network access limited to the node subnet | Durable storage for files, artifacts, and the workspace filesystem |
| Data | Key Vault key | An RSA key in Azure Key Vault, for example 3072-bit, that permits the encrypt, decrypt, wrap key, and unwrap key operations | Protects stored credentials. Keep the key enabled; see Step 3 |
| Identity | OIDC IdP app | An application registered in Entra ID, Okta, or Auth0 | See Identity providers (OIDC) |
| Identity | Workload identity | A user-assigned managed identity with Storage Blob Data Contributor on the container and Key Vault Crypto User on the key, federated with a Kubernetes service account in the application namespace | Create it before the install and name the service account in the values file in Step 6 |
| Identity | Gateway certificate identity | A second user-assigned managed identity with Key Vault Secrets User on the vault that holds the TLS certificate, federated with a service account in the Gateway's namespace | The add-on reads the certificate as this identity. Kept apart from the application's, which needs no access to the certificate |
| Access | Azure and AKS admin access | Permissions for the deploying administrator, plus the Azure CLI, kubectl, and helm on their workstation | |
| Egress | Outbound HTTPS | Unrestricted egress recommended | Needed for the Sema4.ai services, the sandbox runtime's chart and image, and everything you connect the platform to. See Network endpoints |
Node sizing
The application node is a single Standard_D32s_v5: 32 vCPU and 128 GiB on Intel processors.
It carries the whole platform and every concurrent sandbox run, with headroom for several teams sharing the platform; see Allocatable capacity for what actually reaches the platform.
On Azure, nested virtualization is a property of the VM size, not a setting: Standard_D32s_v5 supports it, and there is nothing to enable.
If you choose a different size, confirm in the Azure documentation that it supports nested virtualization before you create the node pool.
This release does not scale horizontally, so the node is the ceiling on concurrent agent work for the life of the deployment. Replacing the node costs downtime and a cold cache rather than data. See Caveats and limits.
Data root performance
Premium SSD performance scales with disk size. At the 100 GiB default the data root is a P10 disk, with 500 IOPS and 100 MB/s; a 512 GiB data root is a P20, with 2,300 IOPS and 150 MB/s. Materialization is metadata-heavy, so a data root that is short of IOPS shows up as slower runs rather than as an error.
Raise the size with vfs.dataRoot.size when you need more, at install or later, without a restart.
For IOPS that do not depend on size, use a StorageClass of your own; see Advanced configuration.
Ingress
We recommend Microsoft's managed ingress for AKS, the application routing add-on's Gateway API implementation (opens in a new tab), and this guide builds on it. You create one Gateway, which terminates TLS with your certificate from Key Vault, and the application attaches its routes to it.
It is the only ingress configuration we test on Azure. The add-on's managed NGINX is built on the upstream ingress-nginx project, which is end of life (opens in a new tab).
The application itself works with any ingress controller or Gateway API implementation that meets the requirements in Using your own ingress, and the values file names it in place of the Gateway.
Part 2: Deployment steps
Step 1: Pick the hostname
Choose the hostname on a domain you control, for example finance-agents.company.com.
Whether it resolves publicly or only inside your network is your choice.
TLS, the OIDC redirect URI, and the Gateway configuration all derive from it.
You point DNS at the Gateway in Step 8.
Import or issue a TLS certificate for the hostname into Azure Key Vault, and note its identifier, https://<vault>.vault.azure.net/certificates/<name>.
Or use a hostname that Azure generates
You can use an Azure-generated hostname instead of one on your own domain.
Azure Front Door (opens in a new tab) (Standard or Premium) gives each endpoint a hostname of the form <endpoint>-<hash>.z01.azurefd.net, and serves it over HTTPS with a certificate that Microsoft issues and rotates.
You need no domain, no DNS record, and no certificate of your own, which suits a proof of concept.
The reference Terraform takes this approach, and its README describes what it trades away.
Azure generates the hostname when you create the endpoint, so create the Front Door endpoint first and use its hostname wherever this guide asks for yours. By default, an endpoint that you delete and recreate with the same name in the same Entra ID tenant gets the same hostname back. Moving to a hostname of your own later is a hostname change like any other; see the warning in Part 1.
With a Front Door hostname, TLS terminates at Front Door instead of at the Gateway, and some later steps change:
- Step 1: skip the certificate.
- Step 3: give the Gateway an HTTP listener on port 80 with no certificate and no hostname, and make it the origin of the Front Door endpoint. Front Door reaches it over plain HTTP, because Front Door accepts an HTTPS origin only with a publicly trusted certificate for the origin's hostname. Use a network security group to admit only the
AzureFrontDoor.Backendservice tag to the Gateway's load balancer. The Gateway certificate identity is not needed. - Step 6: set
sectionNamein the values file to the name of the HTTP listener. - Step 8: skip it; the hostname already resolves to Front Door.
Front Door's own limits then apply to every request: it waits at most 240 seconds for a response from the origin, and it closes idle and long-lived WebSocket connections on its own schedule (opens in a new tab).
Step 2: Register the OIDC application
Register an OIDC application in your identity provider and capture the Discovery URL, Client ID, and Client Secret.
Configure it with:
- Callback URL:
https://<hostname>/api/v1/auth/callback - Logout URL:
https://<hostname> - Application login URI:
https://<hostname>/login - Allowed web origin:
https://<hostname> - Scopes:
openid,profile,email - Client authentication: client secret in the request body (
client_secret_post)
Follow the guide for your provider: Microsoft Entra ID · Auth0 · Okta.
Step 3: Provision Azure infrastructure
Provision the infrastructure using the bill of materials as the spec. The end state must include:
- An AKS cluster, version 1.36 or newer, with the OIDC issuer, workload identity, the managed Gateway API, and the application routing add-on's Gateway API implementation enabled, and
kubectlaccess configured against it. - The application node, a
Standard_D32s_v5with a 200 GiB OS disk, as the only node in a node pool of one. - PostgreSQL Flexible Server, version 17 or 18, in private access mode in the cluster's virtual network, with
PGCRYPTOandCITEXTin itsazure.extensionsparameter. - A storage account and a blob container, reachable from the node subnet.
- A Key Vault with an RSA key that permits encrypt, decrypt, wrap key, and unwrap key, and the TLS certificate from Step 1.
- The application namespace, a Kubernetes service account in it, and a user-assigned managed identity that holds Storage Blob Data Contributor on the container and Key Vault Crypto User on the key, federated with that account. You name the account in the values file in Step 6.
- The Gateway, in a namespace of its own, serving your hostname over HTTPS with the certificate from Step 1. You name it in the values file in Step 6.
Node and storage checks
Node bootstrap needs nothing platform-specific: no custom data, no script, no packages, and no data volume. The node runs the stock AKS node image, and nested virtualization comes from the VM size. Bootstrap does not install the sandbox runtime; that is Step 4, and it prepares nodes that join later on its own.
There is no StorageClass to create: the chart creates the data root's class itself, as described in the bill of materials.
Confirm the node and the storage prerequisites:
- The node is
Ready. /dev/kvmis present on the node.kubectl get csidriver disk.csi.azure.comreturns the driver.
Check /dev/kvm before you install the sandbox runtime. Nothing
downstream notices a missing device: the runtime installs and reports
success, the platform comes up healthy, and only agent runs fail. The
reference Terraform ships a one-shot check,
k8s/kvm-check.yaml (opens in a new tab).
Save it, run kubectl apply -f kvm-check.yaml, and read
kubectl -n default logs job/kvm-check: a healthy node lists the /dev/kvm
device and the vmx CPU flag.
Application identity
The federation is the part to get exactly right. The service account carries the identity's client ID. The identity trusts the cluster's OIDC issuer for exactly that namespace and account:
kubectl create namespace <namespace>
kubectl -n <namespace> create serviceaccount <service-account>
kubectl -n <namespace> annotate serviceaccount <service-account> \
azure.workload.identity/client-id=<identity-client-id>
az identity federated-credential create \
--resource-group <resource-group> \
--identity-name <identity-name> \
--name <namespace>-<service-account> \
--issuer "$(az aks show --resource-group <resource-group> --name <cluster> \
--query oidcIssuerProfile.issuerUrl --output tsv)" \
--subject system:serviceaccount:<namespace>:<service-account> \
--audiences api://AzureADTokenExchangeGrant the workload identity Key Vault Crypto User on the RSA key, and allow encrypt, decrypt, wrap key, and unwrap key operations. Missing permissions prevent the application from starting. For vaults using access policies, grant Get, Encrypt, Decrypt, Wrap Key, and Unwrap Key instead.
If the vault restricts network access, allow the node subnet and enable its Microsoft.KeyVault service endpoint.
Enable purge protection for production vaults to prevent permanent deletion during the soft-delete retention period.
Ingress and the Gateway
Turn on the ingress (Azure CLI 2.86.0 or newer).
The first command installs the application routing add-on with its NGINX controller off and the Key Vault secrets provider on; on a cluster that already has the add-on, run az aks approuting update with the same flags.
The second installs the managed Gateway API definitions and the add-on's implementation of them.
The third lets the secrets provider pick up a renewed certificate:
az aks approuting enable --resource-group <resource-group> --name <cluster> \
--nginx None --enable-kv
az aks update --resource-group <resource-group> --name <cluster> \
--enable-gateway-api --enable-app-routing-istio
az aks addon update --resource-group <resource-group> --name <cluster> \
--addon azure-keyvault-secrets-provider --enable-secret-rotationkubectl get gatewayclass approuting-istio should show the class as accepted.
The add-on reads the TLS certificate from Key Vault as an identity you give it, federated with a service account in the Gateway's namespace. Create the namespace and the account, grant the Gateway certificate identity from Part 1 Key Vault Secrets User on the vault, and federate it with the account:
kubectl create namespace <gateway-namespace>
kubectl -n <gateway-namespace> create serviceaccount <gateway-service-account>
kubectl -n <gateway-namespace> annotate serviceaccount <gateway-service-account> \
azure.workload.identity/client-id=<gateway-identity-client-id>
kubectl -n <gateway-namespace> label serviceaccount <gateway-service-account> \
azure.workload.identity/use=true
az role assignment create \
--assignee-object-id <gateway-identity-principal-id> \
--assignee-principal-type ServicePrincipal \
--role "Key Vault Secrets User" \
--scope <key-vault-resource-id>
az identity federated-credential create \
--resource-group <resource-group> \
--identity-name <gateway-identity-name> \
--name <gateway-namespace>-<gateway-service-account> \
--issuer "$(az aks show --resource-group <resource-group> --name <cluster> \
--query oidcIssuerProfile.issuerUrl --output tsv)" \
--subject system:serviceaccount:<gateway-namespace>:<gateway-service-account> \
--audiences api://AzureADTokenExchangeThen create the Gateway with kubectl apply -f.
It has one HTTPS listener for your hostname, named https, and admits routes from the application namespace only:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: <gateway>
namespace: <gateway-namespace>
spec:
gatewayClassName: approuting-istio
listeners:
- name: https
hostname: <hostname>
port: 443
protocol: HTTPS
tls:
mode: Terminate
options:
# The certificate from Step 1. Leave the version off so a renewed
# certificate is picked up.
kubernetes.azure.com/tls-cert-keyvault-uri: https://<vault>.vault.azure.net/certificates/<name>
kubernetes.azure.com/tls-cert-service-account: <gateway-service-account>
allowedRoutes:
namespaces:
from: Selector
selector:
matchLabels:
kubernetes.io/metadata.name: <namespace>The Gateway's own namespace keeps its load balancer, and so the address your DNS record points at, in place when you reinstall the application. The load balancer is public by default. To keep it private, add the internal load balancer annotation to the Gateway's spec:
spec:
infrastructure:
annotations:
service.beta.kubernetes.io/azure-load-balancer-internal: "true"Wait for the add-on to provision the Gateway, then confirm it synced the certificate into a Secret named kv-gw-cert-<gateway>-https:
kubectl -n <gateway-namespace> wait --for=condition=programmed gateway/<gateway> --timeout=300s
kubectl -n <gateway-namespace> get secretFor more on this setup, including publishing the DNS record from Azure DNS automatically, see Configure Azure DNS and TLS with the application routing Gateway API implementation (opens in a new tab).
Step 4: Install the sandbox runtime
Install Kata Containers into the cluster, once, following Install the sandbox runtime. Verify it before continuing:
kubectl get runtimeclass kata-clh
kubectl get nodes -l katacontainers.io/kata-runtime=trueNothing checks this at install time. The application may install without the sandbox runtime, but it will not operate correctly: agent runs fail, and the failure does not point back at the missing runtime. Make sure both commands above pass before you proceed.
Step 5: Create the database and its roles
The application does not create its own database, and it connects with three least-privilege roles rather than the server administrator. The server has no public endpoint, so connect from inside the virtual network. A throwaway Pod in the cluster works, and prompts for the administrator password:
kubectl run psql --rm -it --restart=Never --image=postgres:18-alpine -- \
psql -h <server>.postgres.database.azure.com -U <admin-user> -d postgresRun the following, with your own names and strong passwords:
CREATE ROLE <migrator_role> LOGIN PASSWORD '<migrator_password>'
NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
CREATE ROLE <definer_role> NOLOGIN
NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION BYPASSRLS;
CREATE ROLE <app_role> LOGIN PASSWORD '<app_password>'
NOSUPERUSER NOCREATEDB NOCREATEROLE NOREPLICATION NOBYPASSRLS;
GRANT <definer_role> TO <migrator_role>;
CREATE DATABASE <database>;
GRANT <migrator_role> TO CURRENT_USER;
ALTER DATABASE <database> OWNER TO <migrator_role>;
GRANT CONNECT ON DATABASE <database> TO <app_role>, <definer_role>;
REVOKE <migrator_role> FROM CURRENT_USER;The migrator role applies the schema on install and upgrade, the app role is what the application connects as at runtime, and the definer role owns privileged functions and cannot log in; see The database roles for the full rationale.
The migrator enables the pgcrypto and citext extensions itself, which is why both must be on the server's allow-list.
The Flexible Server administrator is not a PostgreSQL superuser, but on PostgreSQL 16 and newer it can create the definer role with BYPASSRLS.
Capture the database name, the three role names, and the two passwords: they go into the values file.
Step 6: Prepare your values file
Save the template as my-values.yaml and replace every REPLACE_ME before installing.
Keep the file for upgrades.
This template requires application version 3.2.5 or later.
my-values-v3-azure-aks.example.yaml# Helm values for a self-hosted deployment on Azure AKS.
# Requires application version 3.2.5 or later.
# Replace every REPLACE_ME and save as my-values.yaml for installs and upgrades.
# Application URL: https://<hostname>[:port], with no path.
# Use the same hostname in your identity provider and routing configuration.
applicationUrl: https://REPLACE_ME
# The Kubernetes service account that gives the application access to Azure
# resources. Create it in the application namespace, annotate it with the
# client ID of the user-assigned managed identity
# (azure.workload.identity/client-id), and federate the identity with it.
serviceAccount:
create: false
name: REPLACE_ME
# The application database and its three roles, created before the install
# (see "Create the database and its roles" in the AKS guide).
postgres:
host: REPLACE_ME
database: REPLACE_ME
appRole: REPLACE_ME
appPassword: REPLACE_ME
definerRole: REPLACE_ME
migratorRole: REPLACE_ME
migratorPassword: REPLACE_ME
# Azure resources provisioned in Step 3 of the deployment guide.
infrastructure:
platform: azure
azure:
# The storage account name only: 3 to 24 lowercase letters and digits.
storageAccountName: REPLACE_ME
# The container name only. The optional key prefix keeps several
# deployments apart in one container.
blobContainerName: REPLACE_ME
# blobKeyPrefix: ""
# RSA key URL: https://<vault>.vault.azure.net/keys/<name> (omit the version).
# Allow encrypt, decrypt, wrap key, and unwrap key operations.
# Grant the managed identity Key Vault Crypto User on the key.
keyVaultKeyUrl: REPLACE_ME
api:
config:
# Encrypts exported project archives. Generate with: openssl rand -hex 32
# Back up this key in your secrets manager and keep it across upgrades.
projectPortabilityKeys: '{"active":"REPLACE_ME"}'
# The OIDC application you registered against your hostname in Step 2.
auth:
oidc:
# Example discovery URLs for common providers:
# Microsoft Entra ID: https://login.microsoftonline.com/4653d2c1-77fa-412c-bf6f-db9af1f37a97/v2.0/.well-known/openid-configuration
# Okta: https://integrator-2550551.okta.com/.well-known/openid-configuration
server: REPLACE_ME # your provider's discovery URL; the bare issuer URL also works
clientId: REPLACE_ME
clientSecret: REPLACE_ME
tokenEndpointAuthMethod: client_secret_post # send the client secret in the token request body
# Gateway from Step 3. Its HTTPS listener must serve your TLS certificate
# and allow routes from the application namespace.
httpRoute:
enabled: true
parentRefs:
- name: REPLACE_ME # the Gateway's name
namespace: REPLACE_ME # the Gateway's namespace
sectionName: https # the listener's name
hostnames:
# Your hostname from Step 1, as on the Gateway's listener.
- REPLACE_METhe data root defaults to 100 GiB.
Increase vfs.dataRoot.size and run a Helm upgrade to expand it without a restart.
On Azure, a larger disk also provides more IOPS; see Data root performance.
For custom storage, URLs, or ingress settings, see Advanced configuration.
Step 7: Install the application
Open the Enterprise Portal (opens in a new tab) and select the team whose license is assigned to the prod-selfhosted-v3 channel.
Follow the portal's installation flow and run its Export credentials and log in commands.
Use the my-values.yaml you prepared above and skip the preflight plugin step.
Install the latest available version, replacing <release-name> with the name you want to give this deployment and <your-namespace> with your application namespace:
helm install <release-name> \
oci://registry.sema4.ai/sema4ai/prod-selfhosted-v3/blockparty \
--values my-values.yaml \
--namespace <your-namespace>Omit --version to install the latest version available on the v3 channel.
If you need a specific release, add --version <chart-version> using the exact chart version shown in the portal.
Keep the release name, namespace, and values file for future upgrades.
Install into the namespace that holds the service account from Step 3, not into a new one.
Expect the VFS and sandbox Pods to sit in ContainerCreating for a few minutes on a first install: they bind the data root, and they start once the data-root workload has claimed, formatted, and mounted it.
Step 8: Point DNS at the Gateway
Read the load balancer address from the Gateway:
kubectl -n <gateway-namespace> get gateway <gateway> \
-o jsonpath='{.status.addresses[0].value}{"\n"}'Create the DNS record resolving your hostname to that address. Sign-in does not complete until the hostname resolves and TLS is serving.
Step 9: Validate
- Check the data root: the claim is
Boundand thedata-rootlog ends withdata root ready. See Verify the data root. - Check the workload identity:
kubectl -n <namespace> describe pod -l app.kubernetes.io/component=vfs | grep AZURE_listsAZURE_CLIENT_ID,AZURE_TENANT_ID, andAZURE_FEDERATED_TOKEN_FILE. Run the same check withapp.kubernetes.io/component=api. If they are missing, check the service account annotation and federated credential from Step 3. The application needs this identity to start and access blob storage. - Check the route:
kubectl -n <namespace> get httproute -o jsonpath='{range .items[*].status.parents[*].conditions[*]}{.type}={.status} {end}{"\n"}'showsAccepted=TrueandResolvedRefs=True. IfAcceptedisFalse, the Gateway does not admit routes from the application namespace, or the hostname does not match its listener. - Browse to
https://<hostname>. Sign-in should redirect to your identity provider. - Sign in with a permitted user and confirm you land in the workspace.
- Choose the workspace's models. Agents, workflows, and Sai do not run until the workspace has a Primary and a Fast model, and every page shows the banner Choose this workspace's models until then. Click Choose models, or go to Configuration > Models, choose both models, and click Save models. The Image model is optional.
The first user of a new deployment becomes its admin. Sign in yourself first, before opening access more widely.
Step 10: Protect your keys and backups
- Back up the database.
- Keep the Key Vault key enabled and accessible for as long as you retain the database or its backups.
- Store
api.config.projectPortabilityKeysin your secrets manager so you can decrypt exported project archives.
Permanently deleting the Key Vault key makes stored credentials unrecoverable, even from a database backup. Disabling it prevents the application from accessing those credentials until access is restored.
Next steps
To upgrade this deployment, follow Update the application.
With the application running, continue to Administration to manage model platforms, connect integrations and data sources, and set up teams. For day-2 behavior (resizing the data root, replacing the node, upgrading the sandbox runtime), see the Operations reference.