Azure Deployment Guide (AKS)
Complete guide for deploying StateSet Sandbox on Azure Kubernetes Service (AKS).Prerequisites
- Azure CLI configured with appropriate permissions
- kubectl installed
- Helm 3.x installed
- Domain name for API endpoint
- Azure subscription with required resource providers enabled
Quick Start (Kustomize Overlay)
Use the Azure-specific overlay to apply the correct ingress/controller defaults:kubectl apply -k k8s/overlays/azure
# or
./scripts/deploy-k8s.sh azure
# or (CLI)
stateset-sandbox deploy azure
stateset-sandbox deploy azure --preflight --wait --smoke
stateset-sandbox is the @stateset/sandbox-cli binary. It also installs a stateset alias, but that
name collides with @stateset/cli, whose stateset has no deploy command, so use the long name.
For smoke checks, export credentials before running deploy:
export STATESET_API_KEY="your_api_key"
export STATESET_API_URL="https://api.sandbox.stateset.app/api/v1"
read -rsp "Admin password: " ADMIN_PASSWORD; echo
bash scripts/create-controller-secret.sh \
--admin-username admin \
--admin-password "$ADMIN_PASSWORD"
unset ADMIN_PASSWORD
# Edit the placeholder SAS token
vi k8s/overlays/azure/secret.yaml
Architecture Overview
┌─────────────────────────────────────────────────────────────────────────────────┐
│ Microsoft Azure │
│ ┌───────────────────────────────────────────────────────────────────────────┐ │
│ │ VNet (10.0.0.0/16) │ │
│ │ ┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐ │ │
│ │ │ Subnet: AKS │ │ Subnet: Database │ │ Subnet: Redis │ │ │
│ │ │ 10.0.0.0/20 │ │ 10.0.16.0/24 │ │ 10.0.17.0/24 │ │ │
│ │ │ ┌──────────────┐ │ │ ┌──────────────┐ │ │ ┌──────────────┐ │ │ │
│ │ │ │ AKS Cluster │ │ │ │ Azure DB for │ │ │ │ Azure Cache │ │ │ │
│ │ │ │ - System Pool│ │ │ │ PostgreSQL │ │ │ │ for Redis │ │ │ │
│ │ │ │ - Sandbox │ │ │ │ Flex Server │ │ │ │ │ │ │ │
│ │ │ │ Pool │ │ │ └──────────────┘ │ │ └──────────────┘ │ │ │
│ │ │ └──────────────┘ │ │ │ │ │ │ │
│ │ └────────────────────┘ └────────────────────┘ └────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Azure Blob │ │ Key Vault │ │ Application │ │ Container Registry │ │
│ │ Storage │ │ Secrets │ │ Gateway │ │ (ACR) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────────┘
Step 1: Set Up Azure Environment
# Login to Azure
az login
# Set subscription
export SUBSCRIPTION_ID="your-subscription-id"
az account set --subscription $SUBSCRIPTION_ID
# Set variables
export RESOURCE_GROUP="stateset-sandbox-rg"
export LOCATION="eastus"
export AKS_CLUSTER="stateset-sandbox-aks"
# Register required providers
az provider register --namespace Microsoft.ContainerService
az provider register --namespace Microsoft.DBforPostgreSQL
az provider register --namespace Microsoft.Cache
az provider register --namespace Microsoft.KeyVault
az provider register --namespace Microsoft.Storage
az provider register --namespace Microsoft.Network
# Create resource group
az group create --name $RESOURCE_GROUP --location $LOCATION
Step 2: Create Virtual Network
# Create VNet
az network vnet create \
--resource-group $RESOURCE_GROUP \
--name stateset-sandbox-vnet \
--address-prefix 10.0.0.0/16
# Create subnet for AKS
az network vnet subnet create \
--resource-group $RESOURCE_GROUP \
--vnet-name stateset-sandbox-vnet \
--name aks-subnet \
--address-prefix 10.0.0.0/20
# Create subnet for PostgreSQL
az network vnet subnet create \
--resource-group $RESOURCE_GROUP \
--vnet-name stateset-sandbox-vnet \
--name db-subnet \
--address-prefix 10.0.16.0/24 \
--delegations Microsoft.DBforPostgreSQL/flexibleServers
# Create subnet for Redis
az network vnet subnet create \
--resource-group $RESOURCE_GROUP \
--vnet-name stateset-sandbox-vnet \
--name redis-subnet \
--address-prefix 10.0.17.0/24
# Get subnet IDs
AKS_SUBNET_ID=$(az network vnet subnet show \
--resource-group $RESOURCE_GROUP \
--vnet-name stateset-sandbox-vnet \
--name aks-subnet \
--query id -o tsv)
DB_SUBNET_ID=$(az network vnet subnet show \
--resource-group $RESOURCE_GROUP \
--vnet-name stateset-sandbox-vnet \
--name db-subnet \
--query id -o tsv)
Step 3: Create Azure Container Registry
# Create ACR
az acr create \
--resource-group $RESOURCE_GROUP \
--name statesetsandboxacr \
--sku Premium \
--admin-enabled false
# Get ACR ID
ACR_ID=$(az acr show --name statesetsandboxacr --query id -o tsv)
Step 4: Create AKS Cluster
# Create managed identity for AKS
az identity create \
--resource-group $RESOURCE_GROUP \
--name stateset-sandbox-identity
IDENTITY_ID=$(az identity show \
--resource-group $RESOURCE_GROUP \
--name stateset-sandbox-identity \
--query id -o tsv)
IDENTITY_CLIENT_ID=$(az identity show \
--resource-group $RESOURCE_GROUP \
--name stateset-sandbox-identity \
--query clientId -o tsv)
# Create AKS cluster
az aks create \
--resource-group $RESOURCE_GROUP \
--name $AKS_CLUSTER \
--location $LOCATION \
--kubernetes-version 1.29 \
--node-count 3 \
--node-vm-size Standard_D4s_v3 \
--network-plugin azure \
--network-policy azure \
--vnet-subnet-id $AKS_SUBNET_ID \
--service-cidr 10.1.0.0/16 \
--dns-service-ip 10.1.0.10 \
--enable-managed-identity \
--assign-identity $IDENTITY_ID \
--enable-workload-identity \
--enable-oidc-issuer \
--enable-addons monitoring \
--attach-acr statesetsandboxacr \
--nodepool-name systempool \
--nodepool-labels node-type=controller \
--zones 1 2 3 \
--tier standard
# Add sandbox node pool with Kata Containers
az aks nodepool add \
--resource-group $RESOURCE_GROUP \
--cluster-name $AKS_CLUSTER \
--name sandboxpool \
--node-count 2 \
--node-vm-size Standard_D8s_v3 \
--node-osdisk-size 100 \
--workload-runtime KataMshvVmIsolation \
--os-sku AzureLinux \
--labels node-type=sandbox isolation=kata \
--node-taints stateset.com/sandbox=true:NoSchedule \
--min-count 0 \
--max-count 20 \
--enable-cluster-autoscaler \
--zones 1 2 3
# Get credentials
az aks get-credentials --resource-group $RESOURCE_GROUP --name $AKS_CLUSTER
Alternative: gVisor Node Pool
If you prefer gVisor over Kata:# Add gVisor node pool (requires custom VM image)
az aks nodepool add \
--resource-group $RESOURCE_GROUP \
--cluster-name $AKS_CLUSTER \
--name gvisorpool \
--node-count 2 \
--node-vm-size Standard_D8s_v3 \
--labels node-type=sandbox isolation=gvisor \
--node-taints stateset.com/sandbox=true:NoSchedule \
--min-count 0 \
--max-count 20 \
--enable-cluster-autoscaler
Step 5: Create Azure Database for PostgreSQL
# Create private DNS zone
az network private-dns zone create \
--resource-group $RESOURCE_GROUP \
--name privatelink.postgres.database.azure.com
# Link DNS zone to VNet
az network private-dns link vnet create \
--resource-group $RESOURCE_GROUP \
--zone-name privatelink.postgres.database.azure.com \
--name stateset-sandbox-link \
--virtual-network stateset-sandbox-vnet \
--registration-enabled false
# Create PostgreSQL Flexible Server
az postgres flexible-server create \
--resource-group $RESOURCE_GROUP \
--name stateset-sandbox-pg \
--location $LOCATION \
--admin-user pgadmin \
--admin-password "YOUR_STRONG_PASSWORD" \
--sku-name Standard_D4s_v3 \
--tier GeneralPurpose \
--version 15 \
--storage-size 128 \
--subnet $DB_SUBNET_ID \
--private-dns-zone privatelink.postgres.database.azure.com \
--high-availability ZoneRedundant \
--backup-retention 7
# Create database
az postgres flexible-server db create \
--resource-group $RESOURCE_GROUP \
--server-name stateset-sandbox-pg \
--database-name sandbox
# Get connection string
PG_HOST=$(az postgres flexible-server show \
--resource-group $RESOURCE_GROUP \
--name stateset-sandbox-pg \
--query fullyQualifiedDomainName -o tsv)
Step 6: Create Azure Cache for Redis
# Create Redis cache
az redis create \
--resource-group $RESOURCE_GROUP \
--name stateset-sandbox-redis \
--location $LOCATION \
--sku Premium \
--vm-size P1 \
--subnet-id /subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Network/virtualNetworks/stateset-sandbox-vnet/subnets/redis-subnet \
--enable-non-ssl-port false \
--minimum-tls-version 1.2 \
--zones 1 2 3
# Get Redis connection info
REDIS_HOST=$(az redis show \
--resource-group $RESOURCE_GROUP \
--name stateset-sandbox-redis \
--query hostName -o tsv)
REDIS_KEY=$(az redis list-keys \
--resource-group $RESOURCE_GROUP \
--name stateset-sandbox-redis \
--query primaryKey -o tsv)
Step 7: Create Azure Blob Storage
# Create storage account
az storage account create \
--resource-group $RESOURCE_GROUP \
--name statesetsandboxstorage \
--location $LOCATION \
--sku Standard_ZRS \
--kind StorageV2 \
--min-tls-version TLS1_2 \
--allow-blob-public-access false \
--https-only true
# Create container for artifacts
az storage container create \
--account-name statesetsandboxstorage \
--name artifacts \
--auth-mode login
# Enable versioning
az storage account blob-service-properties update \
--resource-group $RESOURCE_GROUP \
--account-name statesetsandboxstorage \
--enable-versioning true
# Set lifecycle management
az storage account management-policy create \
--resource-group $RESOURCE_GROUP \
--account-name statesetsandboxstorage \
--policy '{
"rules": [{
"name": "deleteOldArtifacts",
"enabled": true,
"type": "Lifecycle",
"definition": {
"filters": {"blobTypes": ["blockBlob"], "prefixMatch": ["artifacts/"]},
"actions": {
"baseBlob": {"delete": {"daysAfterModificationGreaterThan": 90}},
"version": {"delete": {"daysAfterCreationGreaterThan": 30}}
}
}
}]
}'
Step 8: Create Azure Key Vault
# Create Key Vault
az keyvault create \
--resource-group $RESOURCE_GROUP \
--name stateset-sandbox-kv \
--location $LOCATION \
--sku premium \
--enable-rbac-authorization
# Get Key Vault ID
KEYVAULT_ID=$(az keyvault show --name stateset-sandbox-kv --query id -o tsv)
# Add secrets
az keyvault secret set \
--vault-name stateset-sandbox-kv \
--name jwt-secret \
--value "$(openssl rand -base64 32)"
az keyvault secret set \
--vault-name stateset-sandbox-kv \
--name database-encryption-key \
--value "$(openssl rand -hex 32)"
az keyvault secret set \
--vault-name stateset-sandbox-kv \
--name stripe-secret-key \
--value "your-stripe-secret-key"
az keyvault secret set \
--vault-name stateset-sandbox-kv \
--name redis-password \
--value "$REDIS_KEY"
# Grant AKS identity access to Key Vault
az role assignment create \
--role "Key Vault Secrets User" \
--assignee $IDENTITY_CLIENT_ID \
--scope $KEYVAULT_ID
Step 9: Configure Workload Identity
# Get OIDC issuer URL
OIDC_ISSUER=$(az aks show \
--resource-group $RESOURCE_GROUP \
--name $AKS_CLUSTER \
--query oidcIssuerProfile.issuerUrl -o tsv)
# Create user-assigned identity for the controller
az identity create \
--resource-group $RESOURCE_GROUP \
--name sandbox-controller-identity
CONTROLLER_IDENTITY_CLIENT_ID=$(az identity show \
--resource-group $RESOURCE_GROUP \
--name sandbox-controller-identity \
--query clientId -o tsv)
# Create federated credential
az identity federated-credential create \
--name sandbox-controller-fed \
--identity-name sandbox-controller-identity \
--resource-group $RESOURCE_GROUP \
--issuer $OIDC_ISSUER \
--subject system:serviceaccount:stateset-sandbox:sandbox-controller
# Grant permissions
az role assignment create \
--role "Key Vault Secrets User" \
--assignee $CONTROLLER_IDENTITY_CLIENT_ID \
--scope $KEYVAULT_ID
az role assignment create \
--role "Storage Blob Data Contributor" \
--assignee $CONTROLLER_IDENTITY_CLIENT_ID \
--scope /subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Storage/storageAccounts/statesetsandboxstorage
Step 10: Deploy StateSet Sandbox
Create Namespace
kubectl apply -f k8s/namespace.yaml
Configure Service Account with Workload Identity
# azure-service-account.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: sandbox-controller
namespace: stateset-sandbox
annotations:
azure.workload.identity/client-id: "YOUR_CONTROLLER_IDENTITY_CLIENT_ID"
labels:
azure.workload.identity/use: "true"
Install CSI Secrets Store Driver
# Install Secrets Store CSI driver
helm repo add csi-secrets-store-provider-azure https://azure.github.io/secrets-store-csi-driver-provider-azure/charts
helm install csi-secrets-store-provider-azure csi-secrets-store-provider-azure/csi-secrets-store-provider-azure \
--namespace kube-system
Create SecretProviderClass
# azure-secret-provider.yaml
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: azure-keyvault-secrets
namespace: stateset-sandbox
spec:
provider: azure
parameters:
usePodIdentity: "false"
useVMManagedIdentity: "false"
clientID: "YOUR_CONTROLLER_IDENTITY_CLIENT_ID"
keyvaultName: "stateset-sandbox-kv"
tenantId: "YOUR_TENANT_ID"
objects: |
array:
- |
objectName: jwt-secret
objectType: secret
- |
objectName: database-encryption-key
objectType: secret
- |
objectName: stripe-secret-key
objectType: secret
- |
objectName: redis-password
objectType: secret
secretObjects:
- secretName: sandbox-controller-auth
type: Opaque
data:
- objectName: jwt-secret
key: jwt-secret
- objectName: database-encryption-key
key: database-encryption-key
- objectName: stripe-secret-key
key: stripe-secret-key
- objectName: redis-password
key: redis-password
Create ConfigMap
# azure-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: sandbox-controller-config
namespace: stateset-sandbox
data:
# Azure-specific configuration
SANDBOX_IMAGE: "statesetsandboxacr.azurecr.io/stateset-sandbox:latest"
# Database (Azure PostgreSQL)
DATABASE_URL: "postgres://pgadmin:PASSWORD@stateset-sandbox-pg.postgres.database.azure.com:5432/sandbox?sslmode=require"
# Redis (Azure Cache)
REDIS_HOST: "stateset-sandbox-redis.redis.cache.windows.net"
REDIS_PORT: "6380"
REDIS_TLS: "true"
# Storage (Azure Blob)
STORAGE_PROVIDER: "azure"
STORAGE_BUCKET: "artifacts"
AZURE_STORAGE_ACCOUNT: "statesetsandboxstorage"
# AZURE_STORAGE_SAS_TOKEN is injected via secret in the Azure kustomize overlay.
# AZURE_STORAGE_ENDPOINT: "https://statesetsandboxstorage.blob.core.windows.net"
# Isolation (Kata Containers on AKS)
DEFAULT_ISOLATION: "kata"
SANDBOX_RUNTIME_CLASS: "kata-mshv-vm-isolation"
# Warm pool
WARM_POOL_ENABLED: "true"
WARM_POOL_SIZE: "5"
# CORS
CORS_ORIGIN: "https://sandbox.yourdomain.com"
APP_URL: "https://api.sandbox.yourdomain.com"
# Logging
LOG_LEVEL: "info"
NODE_ENV: "production"
Update Deployment for Azure
# azure-deployment-patch.yaml
spec:
template:
metadata:
labels:
azure.workload.identity/use: "true"
spec:
serviceAccountName: sandbox-controller
containers:
- name: sandbox-controller
volumeMounts:
- name: secrets-store
mountPath: "/mnt/secrets-store"
readOnly: true
volumes:
- name: secrets-store
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: azure-keyvault-secrets
Deploy Components
kubectl apply -f azure-service-account.yaml
kubectl apply -f azure-secret-provider.yaml
kubectl apply -f azure-configmap.yaml
kubectl apply -f k8s/rbac.yaml
kubectl apply -f k8s/deployment.yaml
kubectl apply -f k8s/service.yaml
kubectl apply -f k8s/network-policy.yaml
kubectl apply -f k8s/resource-quota.yaml
Create Kata RuntimeClass
# kata-runtimeclass.yaml
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
name: kata-mshv-vm-isolation
handler: kata-mshv-vm-isolation
scheduling:
nodeSelector:
kubernetes.azure.com/kata-mshv-vm-isolation: "true"
tolerations:
- key: stateset.com/sandbox
operator: Exists
effect: NoSchedule
Step 11: Configure Application Gateway Ingress
Option A: Application Gateway Ingress Controller (AGIC)
# Enable AGIC addon
az aks addon update \
--resource-group $RESOURCE_GROUP \
--name $AKS_CLUSTER \
--addon ingress-appgw \
--appgw-name stateset-sandbox-agw \
--appgw-subnet-cidr 10.0.20.0/24
# azure-ingress-agic.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: sandbox-controller
namespace: stateset-sandbox
annotations:
kubernetes.io/ingress.class: azure/application-gateway
appgw.ingress.kubernetes.io/ssl-redirect: "true"
appgw.ingress.kubernetes.io/backend-protocol: "http"
appgw.ingress.kubernetes.io/health-probe-path: "/health"
appgw.ingress.kubernetes.io/request-timeout: "300"
# WAF policy (optional)
# appgw.ingress.kubernetes.io/waf-policy-for-path: "/subscriptions/.../applicationGatewayWebApplicationFirewallPolicies/sandbox-waf"
spec:
tls:
- hosts:
- api.sandbox.yourdomain.com
secretName: sandbox-tls-secret
rules:
- host: api.sandbox.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: sandbox-controller
port:
number: 80
Option B: NGINX Ingress Controller
# Install NGINX ingress controller
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm install ingress-nginx ingress-nginx/ingress-nginx \
--namespace ingress-nginx \
--create-namespace \
--set controller.service.annotations."service\.beta\.kubernetes\.io/azure-load-balancer-health-probe-request-path"=/healthz
Configure TLS Certificate
# Install cert-manager
helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager \
--namespace cert-manager \
--create-namespace \
--set installCRDs=true
# Create ClusterIssuer
cat <<EOF | kubectl apply -f -
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-production
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: your-email@example.com
privateKeySecretRef:
name: letsencrypt-production
solvers:
- http01:
ingress:
class: nginx
EOF
Step 12: Set Up Monitoring
Azure Monitor for Containers
Already enabled during cluster creation. Configure additional monitoring:# Enable Container Insights with Prometheus metrics
az aks update \
--resource-group $RESOURCE_GROUP \
--name $AKS_CLUSTER \
--enable-azure-monitor-metrics
Log Analytics Queries
// Query sandbox creation events
ContainerLog
| where LogEntry contains "sandbox.created"
| project TimeGenerated, LogEntry
| order by TimeGenerated desc
| take 100
// Query error rates
ContainerLog
| where LogEntry contains "error"
| summarize ErrorCount = count() by bin(TimeGenerated, 5m)
| render timechart
// Query sandbox metrics
InsightsMetrics
| where Name == "stateset_sandbox_active_sandboxes"
| summarize avg(Val) by bin(TimeGenerated, 1m)
| render timechart
Create Azure Alerts
# Create action group
az monitor action-group create \
--resource-group $RESOURCE_GROUP \
--name sandbox-alerts \
--short-name sandbox \
--email-receiver name=admin email=admin@example.com
# Create alert for high error rate
az monitor metrics alert create \
--resource-group $RESOURCE_GROUP \
--name "high-error-rate" \
--scopes "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.ContainerService/managedClusters/$AKS_CLUSTER" \
--condition "avg percentage>=5" \
--window-size 5m \
--evaluation-frequency 1m \
--action sandbox-alerts \
--description "High error rate in Stateset Sandbox"
Prometheus & Grafana (Optional)
# Install Prometheus stack
helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
helm install prometheus prometheus-community/kube-prometheus-stack \
--namespace monitoring \
--create-namespace \
--set prometheus.prometheusSpec.serviceMonitorSelectorNilUsesHelmValues=false
# Apply ServiceMonitor
kubectl apply -f k8s/prometheus/service-monitor.yaml
kubectl apply -f k8s/prometheus/alerts.yaml
Step 13: Configure Autoscaling
Cluster Autoscaler
Already enabled on sandbox node pool. Configure behavior:# Update autoscaler profile
az aks update \
--resource-group $RESOURCE_GROUP \
--name $AKS_CLUSTER \
--cluster-autoscaler-profile \
scale-down-delay-after-add=10m \
scale-down-unneeded-time=10m \
max-graceful-termination-sec=600
Horizontal Pod Autoscaler
# hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: sandbox-controller
namespace: stateset-sandbox
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: sandbox-controller
minReplicas: 3
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
KEDA (Event-Driven Autoscaling)
# Install KEDA
helm repo add kedacore https://kedacore.github.io/charts
helm install keda kedacore/keda --namespace keda --create-namespace
# keda-scaledobject.yaml
apiVersion: keda.sh/v1alpha1
kind: ScaledObject
metadata:
name: sandbox-controller-scaler
namespace: stateset-sandbox
spec:
scaleTargetRef:
name: sandbox-controller
minReplicaCount: 3
maxReplicaCount: 20
triggers:
- type: prometheus
metadata:
serverAddress: http://prometheus-server.monitoring.svc.cluster.local
metricName: stateset_sandbox_active_sandboxes
query: stateset_sandbox_active_sandboxes
threshold: "50"
Security Hardening
Azure Policy for AKS
# Enable Azure Policy addon
az aks enable-addons \
--resource-group $RESOURCE_GROUP \
--name $AKS_CLUSTER \
--addons azure-policy
# Assign built-in policies
az policy assignment create \
--name "deny-privileged-containers" \
--policy "95edb821-ddaf-4404-9732-666045e056b4" \
--scope "/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.ContainerService/managedClusters/$AKS_CLUSTER"
Microsoft Defender for Containers
# Enable Defender for Containers
az security pricing create \
--name Containers \
--tier Standard
# Enable Defender for AKS cluster
az aks update \
--resource-group $RESOURCE_GROUP \
--name $AKS_CLUSTER \
--enable-defender
Private Endpoints
# Create private endpoint for PostgreSQL
az network private-endpoint create \
--resource-group $RESOURCE_GROUP \
--name pg-private-endpoint \
--vnet-name stateset-sandbox-vnet \
--subnet aks-subnet \
--private-connection-resource-id $(az postgres flexible-server show --resource-group $RESOURCE_GROUP --name stateset-sandbox-pg --query id -o tsv) \
--group-id postgresqlServer \
--connection-name pg-connection
# Create private endpoint for Storage
az network private-endpoint create \
--resource-group $RESOURCE_GROUP \
--name storage-private-endpoint \
--vnet-name stateset-sandbox-vnet \
--subnet aks-subnet \
--private-connection-resource-id $(az storage account show --resource-group $RESOURCE_GROUP --name statesetsandboxstorage --query id -o tsv) \
--group-id blob \
--connection-name storage-connection
Confidential Computing (Optional)
For maximum isolation with Confidential VMs:# Add confidential computing node pool
az aks nodepool add \
--resource-group $RESOURCE_GROUP \
--cluster-name $AKS_CLUSTER \
--name confpool \
--node-count 2 \
--node-vm-size Standard_DC8s_v3 \
--os-sku AzureLinux \
--labels node-type=sandbox isolation=confidential \
--node-taints stateset.com/sandbox=true:NoSchedule \
--min-count 0 \
--max-count 10 \
--enable-cluster-autoscaler \
--enable-sgxquotehelper
Cost Optimization
Azure Reserved Instances
Purchase reserved capacity for predictable workloads:az reservations reservation-order purchase \
--reservation-order-id "00000000-0000-0000-0000-000000000000" \
--sku Standard_D4s_v3 \
--location eastus \
--billing-scope-id "/subscriptions/$SUBSCRIPTION_ID" \
--term P1Y \
--billing-plan Monthly \
--quantity 3
Spot VMs for Sandbox Nodes
# Add spot node pool
az aks nodepool add \
--resource-group $RESOURCE_GROUP \
--cluster-name $AKS_CLUSTER \
--name spotpool \
--priority Spot \
--eviction-policy Delete \
--spot-max-price -1 \
--node-vm-size Standard_D8s_v3 \
--labels node-type=sandbox lifecycle=spot \
--node-taints stateset.com/sandbox=true:NoSchedule,kubernetes.azure.com/scalesetpriority=spot:NoSchedule \
--min-count 0 \
--max-count 20 \
--enable-cluster-autoscaler
Estimated Monthly Costs
| Resource | Configuration | Est. Cost |
|---|---|---|
| AKS Management | Free tier | $0 |
| System Node Pool | 3x Standard_D4s_v3 | ~$350 |
| Sandbox Node Pool | 2-10x Standard_D8s_v3 (Spot) | ~150−750 |
| Azure PostgreSQL | Standard_D4s_v3 HA | ~$450 |
| Azure Cache Redis | Premium P1 | ~$300 |
| Application Gateway | WAF v2 | ~$280 |
| Blob Storage | 100GB + transactions | ~$25 |
| Egress | ~500GB/month | ~$45 |
| Key Vault | Operations | ~$5 |
| Total | ~1,600−2,200/month |
Troubleshooting
Common Issues
Pod identity not working# Check workload identity configuration
kubectl describe serviceaccount sandbox-controller -n stateset-sandbox
# Verify federated credential
az identity federated-credential show \
--name sandbox-controller-fed \
--identity-name sandbox-controller-identity \
--resource-group $RESOURCE_GROUP
# Test connection from a pod
kubectl run -it --rm debug --image=postgres:15 --restart=Never -- \
psql "postgres://pgadmin:pass@stateset-sandbox-pg.postgres.database.azure.com:5432/sandbox?sslmode=require"
# Check node pool configuration
az aks nodepool show \
--resource-group $RESOURCE_GROUP \
--cluster-name $AKS_CLUSTER \
--name sandboxpool
# Check RuntimeClass
kubectl get runtimeclass kata-mshv-vm-isolation -o yaml
Useful Commands
# View AKS diagnostics
az aks show \
--resource-group $RESOURCE_GROUP \
--name $AKS_CLUSTER \
--query "agentPoolProfiles[].{name:name, count:count, vmSize:vmSize}"
# Stream logs
kubectl logs -f -l app=sandbox-controller -n stateset-sandbox
# Check node status
kubectl get nodes -o wide
# View resource usage
kubectl top nodes
kubectl top pods -n stateset-sandbox
Support Resources
The Azure cluster above runs sandboxed, untrusted code. Do not reuse an existing
AKS cluster that also runs your own services: the isolation story in the
security guide assumes a
dedicated node pool, and a shared one gives a sandboxed process neighbours
worth attacking.
Next steps
Security guide
Read before this cluster takes untrusted code — network policy is what contains it.
Operations
Health probes, metrics and runbooks for the cluster you just built.
Production guide
Capacity, warm pools and the settings that decide cost.
Other providers
The same deployment on the other two clouds.