Install on Azure
Deploy AIOE to Azure Container Apps with the Bicep template, with PostgreSQL, Redis, a registry and logs.
The Azure template (main.bicep in your AIOE release) builds a complete AIOE in one resource group: the API and console on Azure Container Apps, Azure Database for PostgreSQL Flexible Server, Azure Managed Redis, a container registry, a managed identity that pulls from it, and a Log Analytics workspace. It runs the same images as the Helm chart.
The deployment has two phases, because a container app cannot start before its image is in the registry: first the infrastructure, then the apps.
What it creates
Names start with aioe-<env>, where env is a parameter (for example prod).
| Resource | Name | Size and settings |
|---|---|---|
| Log Analytics workspace | aioe-<env>-logs | Pay as you go, 30-day retention. Container logs go here. |
| User-assigned identity | aioe-<env>-id | Holds AcrPull on the registry; every app pulls with it. |
| Container registry | aioe<env>acr | Basic, admin user off. |
| PostgreSQL Flexible Server | aioe-<env>-pg | PostgreSQL 16, Burstable B2s, 32 GB, 14-day backups, no high availability, no geo-redundant backup. Database aioe, admin user aioe. |
| Azure Managed Redis | aioe-<env>-redis | Balanced B0, TLS 1.2 or later, port 10000, no eviction. |
| Container Apps environment | aioe-<env>-cae | Sends logs to the workspace. |
| API container app | aioe-<env>-api | 0.5 vCPU, 1 GiB. One to five replicas, scaling on 200 concurrent requests. External HTTPS ingress, port 3001. |
| Console container app | aioe-<env>-console | 0.25 vCPU, 0.5 GiB. One to three replicas. External HTTPS ingress, port 8080. |
| Relay and gateway apps | aioe-<env>-relay, aioe-<env>-gateway | Only with deployMesh=true. See Overlay and relays. |
Database network access
The template lets Container Apps reach PostgreSQL through its public endpoint, with a firewall rule that admits Azure-internal addresses only. Moving the environment into a virtual network with a private endpoint is a hardening step the template does not do for you.
Before you start
- The Azure CLI, signed in, with rights to create resources and role assignments in a resource group.
- Your two app registrations, from Connect your identity provider.
- Access to the
aioe-apiandaioe-consoleimages for your release. The console image has the API's public address built into it, so it must be built for your API host. Ask Mojo Up if your release does not include one. - The two host names you will use, for example
aioe.example.comandconsole.aioe.example.com, and access to their DNS zone. - A Key Vault, or another secret store, for the database password and the platform's keys.
Parameters
| Parameter | Default | What it is |
|---|---|---|
env | prod | Short environment name used in every resource name. |
location | the resource group's | Azure region. |
deployApps | false | false for phase 1, true once the images are in the registry. |
imageTag | latest | Tag of aioe-api and aioe-console to run. |
apiHost, consoleHost | Mojo Up's own | Your API and console host names. Always set them. |
postgresAdminPassword | (required, secure) | Password of the database admin user aioe. |
entraTenantId | (required) | Your Entra tenant ID. The issuer is built from it. |
entraApiAudience | (required) | The API registration's client ID. |
entraPublicClientId | (required) | The client registration's client ID. |
entraApiScope | (required) | The full API scope, for example api://aioe.example.com/access. |
tenantSlug, tenantOrganisation | Mojo Up's own | Your organisation's short name and display name. Always set them. |
tenantDomains | Mojo Up's own | Your email domains, comma separated. The API host and its default Azure host name are added for you. |
deployMesh | false | Deploy the overlay's relay and gateway. |
meshImageTag | latest | Tag of the aioe-mesh-services image. |
gatewaySecret | empty (secure) | Shared secret between the API and the gateway. Needed with deployMesh. |
signingKey | empty (secure) | The platform's Ed25519 signing key as a JWK. Set it: the API can run up to five replicas, and they must share one key. |
secretsKey | empty (secure) | 32 random bytes, base64, sealing the secrets the platform keeps. Empty means it keeps none. |
entraClientSecret | empty (secure) | The API registration's client secret, for Azure DevOps through Entra. Empty means Azure DevOps links with a personal token only. |
apiCertificateName, consoleCertificateName | empty | Names of the managed certificates for your host names, once bound. Every later deployment must carry them. |
How to make the keys is in Platform keys and rotation.
Deploy
Create the resource group and keep the password
RG=rg-aioe
az group create -n $RG -l australiaeast
PW=$(openssl rand -base64 24)
# Store $PW in your Key Vault now; every later deployment needs it.Write a parameters file
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"env": { "value": "prod" },
"imageTag": { "value": "<release tag>" },
"apiHost": { "value": "aioe.example.com" },
"consoleHost": { "value": "console.aioe.example.com" },
"entraTenantId": { "value": "<tenant-id>" },
"entraApiAudience": { "value": "<API registration client ID>" },
"entraPublicClientId": { "value": "<client registration client ID>" },
"entraApiScope": { "value": "api://aioe.example.com/access" },
"tenantSlug": { "value": "example" },
"tenantOrganisation": { "value": "Example Pty Ltd" },
"tenantDomains": { "value": "example.com" }
}
}Do not put secure values in this file. Pass them on the command line from your secret store, or in a temporary parameters file you never commit.
Phase 1: the infrastructure
az deployment group create -g $RG -n aioe-phase1 \
--template-file main.bicep --parameters @parameters.json \
--parameters deployApps=false postgresAdminPassword="$PW"
ACR=$(az deployment group show -g $RG -n aioe-phase1 \
--query properties.outputs.acrLoginServer.value -o tsv)Put the images in the registry
Import or push aioe-api and aioe-console with your release tag into the new registry, for example:
az acr import -n ${ACR%%.*} --source <source registry>/aioe-api:<release tag> --image aioe-api:<release tag>
az acr import -n ${ACR%%.*} --source <source registry>/aioe-console:<release tag> --image aioe-console:<release tag>Add --username and --password when the source registry is private.
Phase 2: the apps
az deployment group create -g $RG -n aioe-phase2 \
--template-file main.bicep --parameters @parameters.json \
--parameters deployApps=true postgresAdminPassword="$PW" \
signingKey="$SIGNING_KEY" secretsKey="$SECRETS_KEY"The API applies the database migrations before it serves. Find the apps' default addresses with:
az deployment group show -g $RG -n aioe-phase2 --query properties.outputs -o jsonapiDefaultFqdn and consoleDefaultFqdn answer straight away; /healthz on the API reports its version.
Add your host names
Container Apps needs two records per host name: a CNAME to the app's default host name, and a TXT record asuid.<host> carrying the environment's verification ID.
az containerapp env show -g $RG -n aioe-prod-cae \
--query properties.customDomainConfiguration.customDomainVerificationId -o tsv| Type | Name | Value |
|---|---|---|
| CNAME | aioe | aioe-prod-api.<environment domain> |
| TXT | asuid.aioe | the verification ID |
| CNAME | console.aioe | aioe-prod-console.<environment domain> |
| TXT | asuid.console.aioe | the verification ID |
| CNAME | aioe-discover | aioe.example.com (workbench discovery, see DNS discovery) |
The environment domain is the environmentDomain output. Then bind a managed certificate to each host name:
az containerapp hostname add -g $RG -n aioe-prod-api --hostname aioe.example.com
az containerapp hostname bind -g $RG -n aioe-prod-api --hostname aioe.example.com \
--environment aioe-prod-cae --validation-method CNAME
az containerapp hostname add -g $RG -n aioe-prod-console --hostname console.aioe.example.com
az containerapp hostname bind -g $RG -n aioe-prod-console --hostname console.aioe.example.com \
--environment aioe-prod-cae --validation-method CNAMEFind the certificate names the binding created (az containerapp env certificate list -g $RG -n aioe-prod-cae --managed-certificates-only -o table) and add them to your parameters file as apiCertificateName and consoleCertificateName.
Carry the certificate names on every deployment
A deployment without apiCertificateName and consoleCertificateName replaces the apps' custom-domain bindings with none, and your host names stop answering until you bind them again.
Logs
Container logs, including the API's JSON log lines, go to the Log Analytics workspace aioe-<env>-logs. For traces and metrics, set OTEL_EXPORTER_OTLP_ENDPOINT on the API app: see Observability.
Next steps
- Publish the discovery record on each email domain.
- Upgrade to a new release by running phase 2 again with a new
imageTag. - Add the overlay with
deployMesh=true.