Skip to content

Azure deployment

Owner: Cloud Ops

Returns to: 3. Configure & install Helm

This path provisions AKS, Azure Database for PostgreSQL Flexible Server, Azure Key Vault, AKS Workload Identity, ingress-nginx, cert-manager, and a static public IP.

Clone the public Terraform mirror and check out the chaindb-vX.Y.Z release tag supplied by Arbium. Keep the Terraform tag aligned with the Helm chart and runtime image version; do not deploy from an unpinned main branch.

Terminal window
git clone https://github.com/try-caret/arbium-terraform.git
cd arbium-terraform
export ARBIUM_TERRAFORM_DIR="$PWD"
git checkout chaindb-vX.Y.Z
cd azure/customer
cp envs/example.tfvars envs/<environment>.tfvars
az login
az account set --subscription <subscription-id>
az aks install-cli

Create an Azure Storage account and blob container for Terraform state in a resource group outside the Arbium deployment resource group. Enable blob versioning and grant the deployer Storage Blob Data Contributor.

Choose a globally unique, 3–24 character lowercase storage account name:

Terminal window
export TFSTATE_RESOURCE_GROUP=arbium-tfstate
export TFSTATE_STORAGE_ACCOUNT="<globally-unique-storage-account-name>"
export TFSTATE_LOCATION="<azure-region>"
az group create \
--name "$TFSTATE_RESOURCE_GROUP" \
--location "$TFSTATE_LOCATION"
az storage account create \
--name "$TFSTATE_STORAGE_ACCOUNT" \
--resource-group "$TFSTATE_RESOURCE_GROUP" \
--location "$TFSTATE_LOCATION" \
--sku Standard_LRS \
--kind StorageV2 \
--min-tls-version TLS1_2 \
--https-only true \
--allow-blob-public-access false
az storage account blob-service-properties update \
--account-name "$TFSTATE_STORAGE_ACCOUNT" \
--resource-group "$TFSTATE_RESOURCE_GROUP" \
--enable-versioning true
export DEPLOYER_OBJECT_ID="$(az ad signed-in-user show --query id -o tsv)"
export TFSTATE_ACCOUNT_ID="$(az storage account show \
--name "$TFSTATE_STORAGE_ACCOUNT" \
--resource-group "$TFSTATE_RESOURCE_GROUP" \
--query id -o tsv)"
az role assignment create \
--assignee-object-id "$DEPLOYER_OBJECT_ID" \
--assignee-principal-type User \
--role "Storage Blob Data Contributor" \
--scope "$TFSTATE_ACCOUNT_ID"
az storage container create \
--account-name "$TFSTATE_STORAGE_ACCOUNT" \
--name tfstate \
--auth-mode login

For an automation identity, set DEPLOYER_OBJECT_ID to its service-principal object ID and use the matching --assignee-principal-type. Azure role assignments can take several minutes to propagate; retry the container command if it initially returns an authorization error.

The public Terraform release intentionally excludes environment-specific backend wiring. Create an untracked backend.tf in azure/customer:

terraform {
backend "azurerm" {}
}

Create the backend directory, then add backends/<environment>.azurerm.hcl with Azure AD authentication:

Terminal window
mkdir -p backends
resource_group_name = "<terraform-state-resource-group>"
storage_account_name = "<terraform-state-account>"
container_name = "tfstate"
key = "azure/customer/<environment>/terraform.tfstate"
use_azuread_auth = true

Do not commit either file. Confirm during terraform init that Terraform says it configured the azurerm backend. A Missing backend configuration warning means backend.tf is absent and Terraform is using local state; stop before applying.

Register the resource providers used by the foundation:

Terminal window
for rp in Microsoft.Network Microsoft.ContainerService \
Microsoft.DBforPostgreSQL Microsoft.KeyVault \
Microsoft.ManagedIdentity Microsoft.Compute Microsoft.Storage; do
az provider register --namespace "$rp" --wait
done

Set the subscription, tenant, region, non-overlapping network ranges, AKS node sizing, and PostgreSQL protection in the copied .tfvars file.

subscription_id = "<azure-subscription-id>"
tenant_id = "<entra-tenant-id>"
location = "centralus"
environment = "<environment>"
name_prefix = "arbium"
general_node_min_count = 1
general_node_max_count = 3
postgres_backup_retention_days = 7
secret_names = [
"scheduler", "registry", "gemini", "enrollment", "jwt", "license"
]
# Add "scim" when SCIM provisioning is enabled. Also add
# "admin-client-id" and "admin-client-secret" when enabling the fleet console.
ingress_static_ip_enabled = true

Confirm that the selected region offers both the configured AKS VM SKU and PostgreSQL Flexible Server SKU for the customer subscription:

Terminal window
export AZURE_LOCATION=centralus
export AKS_VM_SIZE=Standard_D2as_v7
az aks get-versions --location "$AZURE_LOCATION" --output table
az vm list-skus \
--location "$AZURE_LOCATION" \
--resource-type virtualMachines \
--size "$AKS_VM_SIZE" \
--output table
az postgres flexible-server list-skus \
--location "$AZURE_LOCATION" \
--output table

The PostgreSQL CLI lists the Burstable offering as Standard_B1ms; the equivalent Terraform input is B_Standard_B1ms. If the intended Kubernetes version or either SKU is missing or restricted, choose a supported combination before applying. Keep secret values out of .tfvars.

Terminal window
terraform init -reconfigure \
-backend-config=backends/<environment>.azurerm.hcl
terraform fmt -recursive
terraform validate
terraform plan -var-file=envs/<environment>.tfvars
terraform apply -var-file=envs/<environment>.tfvars

The apply creates the resource group, VNet, AKS, private PostgreSQL server, Key Vault, workload identity, static ingress IP, External Secrets Operator, ingress-nginx, cert-manager, and reloader.

Terminal window
$(terraform output -raw get_credentials_command)
kubectl get nodes
kubectl get pods -n external-secrets
kubectl get pods -n ingress-nginx
kubectl get pods -n cert-manager

Terraform writes the database connection URL and PostgreSQL admin password to Key Vault. Populate the remaining values through the customer’s approved secret workflow:

Key Vault secret Value
arbium-<env>-scheduler Random scheduler token
arbium-<env>-enrollment Random enrollment secret
arbium-<env>-jwt Random 32-byte JWT secret
arbium-<env>-gemini Approved Gemini API key, when enabled
arbium-<env>-license Arbium license key
arbium-<env>-registry JSON {"username":"...","token":"..."} for GHCR
arbium-<env>-scim Random SCIM bearer token, when SCIM is enabled
arbium-<env>-admin-client-id Arbium Admin app client ID, when enabled
arbium-<env>-admin-client-secret Arbium Admin app secret, when enabled

Keep Key Vault values out of source control, Terraform variable files, and ticket comments.

7. Configure DNS and certificate resources

Section titled “7. Configure DNS and certificate resources”

For production, create an A record for the customer hostname pointing at:

Terminal window
terraform output -raw ingress_ip

Keep the record DNS-only while Let’s Encrypt performs HTTP-01 validation. Replace the operations email in manifests/cluster-issuer.yaml, then apply the cluster-scoped issuer from the Terraform checkout:

Terminal window
kubectl apply -f \
"$ARBIUM_TERRAFORM_DIR/azure/customer/manifests/cluster-issuer.yaml"

Replace <ingress-host> in manifests/tls-certificate.yaml with the same customer hostname. For a temporary smoke test without customer DNS, you may instead use terraform output -raw ingress_host, which returns an <ingress-ip>.sslip.io hostname. Do not treat that generated hostname as the production endpoint. Apply the Certificate after Helm creates the arbium namespace.

Create ~/arbium-install and save this as ~/arbium-install/<environment>.azure.values.local.yaml, outside source control. Omit extraSecrets when neither SCIM nor the fleet console is enabled; omit only admin-ui-oidc when SCIM is enabled without the fleet console.

global:
region: <azure-region>
imagePullSecrets:
- name: ghcr-pull
externalSecrets:
enabled: true
provider: azurekv
vaultUrl: <terraform-output-key_vault_uri>
tenantId: <entra-tenant-id>
serviceAccount:
annotations:
azure.workload.identity/client-id: <terraform-output-eso_identity_client_id>
dataMappings:
DATABASE_URL: arbium-<env>-database-url
SUPABASE_DB_URL: arbium-<env>-database-url
CHAINDB_SCHEDULER_TOKEN: arbium-<env>-scheduler
ARBOR_AGENT_ENROLLMENT_SECRET: arbium-<env>-enrollment
ROOTS_INTUNE_PILOT_ENROLLMENT_SECRET: arbium-<env>-enrollment
GEMINI_API_KEY: arbium-<env>-gemini
JWT_SECRET: arbium-<env>-jwt
ARBIUM_LICENSE_KEY: arbium-<env>-license
imagePullSecret:
enabled: true
secretId: arbium-<env>-registry
targetSecretName: ghcr-pull
extraSecrets:
scim-bearer:
SCIM_BEARER_TOKEN: arbium-<env>-scim
admin-ui-oidc:
OIDC_CLIENT_ID: arbium-<env>-admin-client-id
OIDC_CLIENT_SECRET: arbium-<env>-admin-client-secret
secrets:
create: false
existingSecret: chaindb-runtime
license:
existingSecret: chaindb-runtime
cloudSqlProxy:
enabled: false
config:
databaseSsl: require
databaseTlsCaStore: system
ingress:
enabled: true
host: arbium.<customer-domain>
embedder:
enabled: true
gpu:
enabled: false

The supplied Azure preset uses the CPU embedder because the Terraform baseline creates one general-purpose node pool. A GPU deployment requires an additional AKS GPU pool plus matching node selectors and tolerations.

Terminal window
terraform output cluster_name
terraform output key_vault_name
terraform output key_vault_uri
terraform output eso_identity_client_id
terraform output database_url_secret_name
terraform output ingress_ip

Continue to 3. Configure & install Helm. After Helm creates the namespace, apply the certificate:

Terminal window
kubectl apply -n arbium -f \
"$ARBIUM_TERRAFORM_DIR/azure/customer/manifests/tls-certificate.yaml"