Target state follows the Azure AI Foundry pattern:
User → Container App (FastAPI) → Azure AI Foundry Agent
├── AI Services / GPT-4o (swedencentral)
├── Azure AI Search (vector + keyword)
└── PostgreSQL Flexible Server
↑
Blob Storage (document indexing)
Secrets (postgresql-password, openai-api-key, search-api-key) are stored in Key Vault. The Container App has a system-assigned managed identity with Get/List access; only AZURE_KEYVAULT_URI is passed as an env var, and the app fetches secrets from Key Vault at startup.
| Resource | Name pattern | Region |
|---|---|---|
| Resource Group | rg-<project>-<env>-uksouth |
uksouth |
| PostgreSQL Flexible Server | psql-<project>-<env> |
uksouth |
| Storage Account + container | st<project><env> |
uksouth |
| Azure Container Registry (Basic) | acr<project><env> |
uksouth |
| Container App Environment | app-<project>-<env>-env |
uksouth |
| Container App | app-<project>-<env> |
uksouth |
| Log Analytics Workspace | app-<project>-<env>-logs |
uksouth |
| Azure AI Search | srch-<project>-<env> |
uksouth |
| AI Services (AIServices kind) | aif-<project>-<env> |
swedencentral |
| GPT-4o deployment | gpt-4o (Standard, 30K TPM) |
swedencentral |
| text-embedding-3-small deployment | text-embedding-3-small (GlobalStandard, 30K TPM) |
swedencentral |
| Azure AI Foundry Hub | aif-<project>-<env>-hub |
swedencentral |
| Azure AI Foundry Project | aif-<project>-<env>-project |
swedencentral |
| Key Vault | kv-<project>-<env> |
uksouth |
Why swedencentral for AI?
gpt-4owith Standard SKU is not available inuksouth. AI Services, the Foundry Hub and Project are all co-located inswedencentralfor this reason.
az login
az account set --subscription "<your-subscription-name>"Run the commands in the Create remote backend section below. Note the storage account name printed at the end — you'll need it in the next step.
Edit terraform/backend.tf — uncomment the terraform block and set your storage account name:
terraform {
backend "azurerm" {
resource_group_name = "rg-fabric-tfstate-uksouth"
storage_account_name = "<your-storage-account-name>"
container_name = "tfstate"
key = "compliance-discovery.tfstate"
}
}The password is never committed — set it via .env (copied from the template) or as an environment variable.
Option A — .env file (recommended for local development):
cp .env.example .env # if an example exists, otherwise create .env
# Edit .env and set:
# TF_VAR_postgresql_admin_password=YourSecurePassword123!The Makefile reads .env automatically via -include .env + export.
Option B — environment variable:
# Fish shell
set -x TF_VAR_postgresql_admin_password "YourSecurePassword123!"# Bash / zsh
export TF_VAR_postgresql_admin_password="YourSecurePassword123!"Key values to check in terraform/terraform.tfvars:
project_name = "cmplianz" # max 8 chars — used in all resource names
environment = "hack"
location = "uksouth" # main region for most resources
openai_location = "swedencentral" # AI Services + Foundry must be here for gpt-4o
openai_model = "gpt-4o"
openai_model_version = "2024-11-20"Run these from the project root (where the Makefile lives):
make init # initialise backend and providers
make plan # format, validate, and preview all changes
make apply # provision the infrastructureIf make cannot find the target (e.g. running from a subdirectory), run Terraform directly:
cd terraform
terraform init
TF_VAR_postgresql_admin_password="..." terraform plan -out=tfplan
terraform apply tfplanThese issues were encountered on the hackathon subscription and are fixed in the current config:
| Error | Cause | Fix applied |
|---|---|---|
App Service 401 quota (Total VMs: 0) |
Subscription-level App Service limit | Switched to Azure Container Apps (no VM quota required) |
gpt-4o-mini not available in uksouth |
Regional model restriction | Moved AI Services to swedencentral |
gpt-4o-mini 2024-07-18 deprecated |
Model retired 31/03/2026 | Switched to gpt-4o 2024-11-20 |
gpt-4o GlobalStandard quota 0 |
No GlobalStandard quota on subscription | Switched to Standard SKU |
text-embedding-3-small Standard not in swedencentral |
SKU not available in region | Switched embedding deployment to GlobalStandard |
| PostgreSQL zone change blocked | Can't modify zone without HA pairing | Restored zone = "1" to match deployed state |
Microsoft.App provider not registered |
Container Apps namespace not registered | az provider register --namespace Microsoft.App |
After code changes, rebuild and push the image, then trigger a new revision:
# From the housing-association-compliance-db directory
az acr build \
--registry acrcmplianzhack \
--image compliance-api:latest \
.
# Update the running Container App
az containerapp update \
--name app-cmplianz-hack \
--resource-group rg-cmplianz-hack-uksouth \
--image acrcmplianzhack.azurecr.io/compliance-api:latestTerraform uses ignore_changes on the container image so it won't revert CLI-driven image updates on the next terraform apply.
make destroy# Log in to Azure
az login
# Set variables (replace with your naming standard/region)
RG_NAME="rg-fabric-tfstate-uksouth"
STORAGE_NAME="stfabrictfstate$(date +%s)" # unique name
CONTAINER_NAME="tfstate"
LOCATION="uksouth"
# Create Resource Group for State
az group create --name $RG_NAME --location $LOCATION
# Create Storage Account with versioning enabled (crucial for recovery during hackdays)
az storage account create \
--name $STORAGE_NAME \
--resource-group $RG_NAME \
--location $LOCATION \
--sku Standard_LRS \
--encryption-services blob
# Enable Blob Versioning for safety
az storage account blob-service-properties update \
--account-name $STORAGE_NAME \
--resource-group $RG_NAME \
--enable-versioning true
# Create Blob Container
az storage container create \
--name $CONTAINER_NAME \
--account-name $STORAGE_NAME \
--auth-mode login
echo "Save this storage account name for your backend.tf: $STORAGE_NAME"Fish shell note: Replace
VAR="value"assignments withset VAR "value"and run theazcommands individually using the literal values, as Fish does not supportVAR=valuesyntax.