AltitudeIQ Pre-Install Checklist
Audience: your Azure / Cloud Ops team. Run through this before the scheduled install window (ideally 2–3 business days ahead) so anything that needs a ticket filed has time to clear.
Target: one Azure subscription, one region of your choice.
Installer: Zero-dependency Go binary run from any machine with az access to the target subscription.
Each item below lists: what to check, how to check, pass criteria, and if it fails, what to do. Items are ordered by likelihood of being a blocker in a typical enterprise Azure tenant.
1. Subscription + RBAC
1.0 Target subscription designated for AltitudeIQ
Check: your Azure / Cloud Ops team has designated which Azure subscription will host AltitudeIQ. This can be an existing subscription with a dedicated resource group, or a net-new subscription stood up specifically for this install.
Pass: a subscription has been agreed by your cloud governance team and the subscription ID is in hand.
Fail (no subscription yet): if your tenant doesn't already have a suitable subscription, your procurement / EA team needs to provision one through the Microsoft EA portal (or your Cloud Solution Provider partner) before install day. New EA subscription provisioning typically takes 1–5 business days, so file the request at least a week ahead of the agreed install window.
Recommendation: a dedicated subscription gives the cleanest operational boundary for AltitudeIQ — distinct cost reporting, simpler RBAC scoping, and a one-command teardown path (az group delete against the install RG, no risk of touching neighbouring workloads). A shared subscription with a dedicated resource group is also fine; the installer scopes everything to the RG it creates.
1.1 Target subscription is named and accessible
Check: you know the exact subscription ID that will host AltitudeIQ, and you can list resource groups in it.
az account show --subscription <SUB_ID> --query '{name:name, id:id, tenantId:tenantId}' -o json
az group list --subscription <SUB_ID> -o table | head -5
Pass: subscription name and ID match expectations; az group list returns without a permission error.
Fail: you may be signed into the wrong tenant (az login --tenant <TENANT_ID>) or missing at least Reader role on the subscription.
1.2 Installer-running user has Owner (or Contributor + User Access Administrator) at the subscription scope
The installer creates a new resource group and grants a user-assigned managed identity two roles on that RG: Contributor (to provision resources) and Role Based Access Control Administrator (to grant the Container Apps environment AcrPull on the customer's ACR at runtime). That second role assignment requires the installer to hold Owner OR User Access Administrator at the subscription scope at run time.
az role assignment list \
--subscription <SUB_ID> \
--assignee <YOUR_AAD_UPN> \
--all \
--query "[?scope=='/subscriptions/<SUB_ID>'].{role:roleDefinitionName, scope:scope}" -o table
Pass: Owner OR (Contributor + User Access Administrator) at the subscription scope.
Fail: ask your Azure admin to grant one of the above for the duration of the install window. After install completes, you can downgrade to Reader; the managed identity owns the RG-scoped operations going forward.
1.3 No subscription-level deny-assignment blocks our scope
Large org subscriptions sometimes carry deny assignments from Azure Blueprints or Management Group policy that override role assignments.
az deployment sub list --subscription <SUB_ID> --query "[?contains(name, 'denyAssignment')]" -o table
az lock list --subscription <SUB_ID> -o table | head
Pass: no deny assignments that block Microsoft.Resources/resourceGroups/write, Microsoft.Authorization/roleAssignments/write, or Microsoft.App/*.
Fail: open a ticket with the Azure policy team. We can work around most deny assignments by targeting a specific subscription they've already approved, but the scope has to be provided explicitly.
2. Azure Policy
2.1 No policy blocks our resource types in the target region
AltitudeIQ provisions:
Microsoft.Resources/resourceGroupsMicrosoft.Network/virtualNetworks,networkSecurityGroupsMicrosoft.KeyVault/vaults(soft-delete enabled, purge protection on)Microsoft.ManagedIdentity/userAssignedIdentitiesMicrosoft.OperationalInsights/workspacesMicrosoft.App/managedEnvironments,containerAppsMicrosoft.ContainerRegistry/registries(Standard SKU)Microsoft.DBforPostgreSQL/flexibleServers(Standard_D4ds_v5 GeneralPurpose on Starter and Professional; Standard_E4ds_v5 MemoryOptimized on Enterprise; Standard_E16ds_v5 MemoryOptimized on Enterprise Plus)Microsoft.Storage/storageAccounts(Standard_LRS on Starter; Standard_GRS on Professional and Enterprise; Standard_GZRS on Enterprise Plus)Microsoft.Insights/components+ alert rules
Check:
az policy state list --subscription <SUB_ID> --filter "complianceState eq 'NonCompliant'" --query "[].{policy:policyDefinitionName, resource:resourceType}" -o table | head -20
az policy assignment list --subscription <SUB_ID> --query "[].{name:displayName, scope:scope}" -o table
Pass: no deny policies on the resource types above for region East US 2 (or the agreed region).
Fail: usually fixable by either (a) adding an exemption for the AltitudeIQ RG once it exists, or (b) adjusting the Bicep parameters to match policy requirements (e.g. if policy requires tags, we parameterise tags).
2.2 No policy blocks public ingress on Container Apps
The dashboard, backend API, and frontend are served from Container Apps with public ingress (HTTPS-only, TLS terminated by Azure). Some tenants have a policy blocking public ingress on Container Apps or requiring VNet integration.
Check:
az policy assignment list --subscription <SUB_ID> \
--query "[?contains(displayName, 'Container Apps') || contains(displayName, 'ingress') || contains(displayName, 'Ingress')].{name:displayName, enforcement:enforcementMode}" \
-o table
Pass: either public ingress is allowed (no matching policies returned), OR you have a private-endpoint pattern we can adapt to (in which case please flag so we can plan).
Fail for Starter / Professional: Starter and Professional use public ingress by default. If policy requires private-only, escalate to the Enterprise tier conversation — it has VNet integration and private ingress.
2.3 Key Vault purge protection policy is compatible
AltitudeIQ provisions Key Vaults with enablePurgeProtection: true and 90-day soft-delete retention. Some tenants have stricter retention (180 / 365 days).
Check:
az policy assignment list --subscription <SUB_ID> \
--query "[?contains(displayName, 'Key Vault') || contains(displayName, 'soft-delete') || contains(displayName, 'purge')].{name:displayName, enforcement:enforcementMode}" \
-o table
Pass: policy allows >= 90 days soft-delete retention. Stricter is fine.
Fail: none expected — Key Vault compliance policies typically only require purge protection to be on, which matches.
3. Quotas
3.1 Container Apps, PostgreSQL, and Managed Identity quotas in the target region
# Container Apps environments — default quota is 15 per subscription, 5 per region
az rest --method GET \
--uri "https://management.azure.com/subscriptions/<SUB_ID>/providers/Microsoft.App/locations/eastus2/usages?api-version=2024-03-01" \
--query "value[].{name:name.value, current:currentValue, limit:limit}" -o table
# PostgreSQL Flexible Server quota — GeneralPurpose vCores
az postgres flexible-server list-skus --location eastus2 --query "[].name" -o tsv | head -5
Pass: at least 1 Container Apps environment slot free, 1 PG Flexible Server slot free, 8 vCores of GeneralPurpose quota.
Fail: file a quota increase at least 3 business days ahead — Microsoft often takes 1–3 business days to approve PG quota in East US 2.
3.2 Multi-Zone HA quota (action required for Professional, Enterprise, Enterprise Plus)
Starter installs ship with HA = Disabled (single-AZ) by design — that tier is for evaluation / non-production work and trades resilience for cost. Professional, Enterprise, and Enterprise Plus ship with HA = ZoneRedundant, which requires Multi-Zone HA quota on the subscription in the chosen region.
If you're installing the Starter tier: no action needed from your team.
If you're installing Professional or higher and your subscription has not yet been granted Multi-Zone HA quota in the chosen region, the PostgreSQL deploy will fail until Azure approves the quota request — file the request at least 3 business days ahead. Capacity-constrained regions may take longer; if quota is declined, switch to a different region or temporarily install Starter while the quota clears.
Check: confirm the chosen region exposes multiple zones to your subscription:
az vm list-skus --location <REGION> --resource-type virtualMachines \
--query "[?name=='Standard_D2s_v5'].locationInfo[0].zones" -o tsv | tr '\t' '\n' | sort -u
Pass: at least zones 1, 2, 3 returned. If the command returns nothing, your subscription does not see availability zones in this region — escalate to your Microsoft account team or switch regions.
4. Outbound networking
4.1 Hostnames reachable from the AltitudeIQ resource group
During bootstrap the reconciler pulls container images from GHCR, registers with the AltitudeIQ Control Plane, and streams telemetry:
| Hostname | Port | Why |
|---|---|---|
ghcr.io |
443 | Image pull during bootstrap + updates |
*.pkg.github.com |
443 | GHCR layer storage (redirect target for ghcr.io) |
cp.altitudeiq.ai |
443 | Control Plane registration, heartbeat, telemetry |
*.azurecr.io (customer's own) |
443 | Internal — images land here after GHCR import |
*.vault.azure.net (customer's own) |
443 | Internal — reconciler reads its secrets |
*.postgres.database.azure.com (customer's own) |
5432 | Internal — app → DB |
*.blob.core.windows.net (customer's own) |
443 | Internal — app → object storage |
*.azurecontainerapps.io (customer's own) |
443 | Internal — backend ↔ frontend ↔ matching-service |
dc.services.visualstudio.com |
443 | Application Insights ingestion |
*.applicationinsights.azure.com |
443 | Application Insights (regional) |
*.in.applicationinsights.azure.com |
443 | Application Insights ingestion (regional) |
Check: if you use Azure Firewall / NSG-based outbound filtering, confirm the hostnames above are allowlisted. The *.altitudeiq.ai and ghcr.io entries are the only non-Azure destinations.
How to self-verify (recommended). A laptop or Azure Cloud Shell egresses from outside the VNet where AltitudeIQ will run, so it does not exercise your NSG / Azure Firewall / forced-tunnel rules. To actually test from inside the VNet, run the probe from a VM in the target subnet (or any subnet that shares the same outbound path — typical hub-and-spoke setups route every spoke through the same firewall, so any spoke works):
Step 1. Provision a throwaway Linux VM in the AltitudeIQ resource group's VNet subnet, with no public IP:
az vm create -g <RG_NAME> -n aiq-egress-probe \
--image Ubuntu2204 --size Standard_B1ls \
--vnet-name <VNET_NAME> --subnet <SUBNET_NAME> \
--public-ip-address "" --admin-username probe --generate-ssh-keys
Step 2. Connect via Azure Bastion (recommended) or az ssh vm if the VNet already has a jump host:
az network bastion ssh -g <RG_NAME> --name <BASTION_NAME> \
--target-resource-id $(az vm show -g <RG_NAME> -n aiq-egress-probe --query id -o tsv) \
--auth-type ssh-key --username probe --ssh-key ~/.ssh/id_rsa
Step 3. From the VM, run a probe across every external hostname in the table above:
for h in ghcr.io pkg-containers.githubusercontent.com \
cp.altitudeiq.ai \
management.azure.com login.microsoftonline.com \
dc.services.visualstudio.com; do
printf '%-45s ' "$h"
curl -sS -o /dev/null -w '%{http_code}\n' --max-time 5 "https://$h/" || echo FAIL
done
Step 4 — interpreting results. Every line should return an HTTP status code (any 2xx/3xx/4xx is fine — it means TLS completed and the request reached the host). Could not resolve host, connection timeouts, or curl: (35) SSL errors indicate the destination is being blocked by an NSG rule, Azure Firewall application rule, or upstream proxy. Cross-reference the failing hostnames against §4.2 below.
Step 5. Delete the probe VM and its associated NIC / disk (the per-resource cleanup avoids leaving orphans behind):
az vm delete -g <RG_NAME> -n aiq-egress-probe --yes
az network nic delete -g <RG_NAME> -n aiq-egress-probeVMNic 2>/dev/null || true
az disk list -g <RG_NAME> --query "[?contains(name, 'aiq-egress-probe')].name" -o tsv \
| xargs -I{} az disk delete -g <RG_NAME> -n {} --yes
The customer-owned wildcard hostnames in the table (*.azurecr.io, *.vault.azure.net, *.postgres.database.azure.com, *.blob.core.windows.net, *.azurecontainerapps.io, *.applicationinsights.azure.com) don't exist until install completes, so they can't be tested pre-install — but if they all use Azure's Service Tags via Azure Firewall (Storage, AzureKeyVault, Sql, MicrosoftContainerRegistry), then management.azure.com succeeding from inside the VNet is a strong indicator the Service Tag routing works.
Pass: outbound HTTPS to the list above is permitted from the AltitudeIQ resource group.
Fail: file the firewall change at least 3 business days ahead. We can also run a pre-install connectivity probe from our side if you want a second confirmation before install day.
4.2 No tenant-level egress restriction that would block GHCR
Some enterprise tenants route all outbound through a tenant-level egress proxy that strips non-allowlisted TLS. If you use one, confirm ghcr.io and cp.altitudeiq.ai are in the proxy's allowlist.
Check: the ghcr.io, pkg-containers.githubusercontent.com, and cp.altitudeiq.ai rows in the §4.1 self-verify loop above cover this. If those three rows return an HTTP status (any 2xx/3xx/4xx) from inside the VNet, you're cleared. If they FAIL while Azure-owned hostnames in the same loop succeed, your egress proxy is stripping them.
Fail: this is the single most common last-minute blocker. File the change ahead of time.
5. Conditional Access
5.1 The installer-running user can complete OAuth from the install workstation
The installer authenticates via az login (browser or device code). If you have a Conditional Access policy requiring a compliant device, specific location, or MFA for management plane operations, the workstation running the installer must satisfy it.
Check: on the install workstation, run:
az login
az account set --subscription <SUB_ID>
az group create --name delete-me-preinstall-probe --location eastus2 --tags temporary=true
az group delete --name delete-me-preinstall-probe --yes
Pass: all four commands succeed without MFA loops, device-compliance denials, or "AADSTS" errors.
Fail: the error codes (e.g. AADSTS53000 for non-compliant device, AADSTS50158 for MFA) tell you which policy tripped. File a temporary exception for the install workstation, or run the installer from a device that already satisfies policy.
5.2 Service-principal-free install confirmed
The installer does not require creating a service principal. It uses your interactive login for the lifetime of the install. After install, only the installer-provisioned managed identity operates on the resource group — no long-lived user credential is stored anywhere.
No action needed from your team, but confirm this with your identity team if they ask.
6. Naming + tagging conventions
6.1 Resource group naming
The installer creates a resource group named rg-{appName} where {appName} is supplied via --app-name. Set --app-name to whatever resource group naming your Azure team prefers (the installer prepends rg-).
Fail: if your naming policy requires a specific prefix or suffix (e.g. rg-prod-pharma-altitudeiq-001), please confirm ahead so we set --app-name accordingly. Length limit: 3–20 chars, lowercase, alphanumeric + hyphens.
6.2 Mandatory tags
Many enterprise policies deny resource creation without specific tags (Cost Center, Owner, Environment, Data Classification, etc.).
Check: list any Azure Policy assignments that deny based on tags:
az policy assignment list --subscription <SUB_ID> --query "[?contains(displayName, 'tag') || contains(displayName, 'Tag')].displayName" -o tsv
Pass: no tag-required deny policies, OR the required tags are documented and we can pass them as installer parameters.
Fail: send us the tag names + values; we'll plumb them through the Bicep. This is a ~5-line change on our side.
7. Backup / DR readiness
7.1 Geo-redundant backup region is accessible
Professional, Enterprise, and Enterprise Plus use geo-redundant PostgreSQL backup with 35-day retention (paired region depends on the region you chose). Starter uses locally-redundant backup with 7-day retention — single-region by design, no cross-region data movement to worry about.
No action required from your team unless you're installing Professional or higher and your tenant has policy blocking cross-region resource data residency.
Pass:
- Starter: automatically passes — locally-redundant backup stays in-region.
- Professional / Enterprise / Enterprise Plus: the paired region for your chosen region is acceptable for PostgreSQL backups.
Fail (Professional and above only): if your organization requires in-region backup only, flag ahead — we can switch to locally-redundant backup at install time, or you can install the Starter tier instead.
7.2 Subscription or RG delete-lock policy
Some enterprise subscriptions auto-apply CanNotDelete locks at the RG level. This doesn't block install, but it does prevent the teardown-and-retry flow if something goes wrong on install day.
Check:
az lock list --subscription <SUB_ID> -o table
Pass: no locks on the (yet-to-be-created) RG name. Locks at subscription-level that permit write-and-create are fine.
Fail: if the lock will be applied automatically post-creation, let us know so we can plan the mitigation path (usually: install, then lock, then re-installs need the lock lifted first).
8. SAP / Oracle connectivity (if either data source is in scope for v1)
Covered in the main customer guide section "Data Source Connectivity". Confirm the following ahead of install day:
8.1 SAP ECC / S/4HANA
- Network path exists from the AltitudeIQ RG to the SAP ERP host (VPN, ExpressRoute, or peering). Test from an existing Azure VM:
nc -zv <sap-host> 3300 # RFC gateway port - BAPI credentials issued for an AltitudeIQ-scoped user (separate from operator credentials).
- SAP JCo libraries uploaded to the AltitudeIQ Storage Account (documented in main guide).
8.2 Oracle
- Network path exists to the Oracle listener (port 1521). Test from an existing Azure VM in the same VNet (or the probe VM from §4.1 if it's still up):
nc -zv <oracle-host> 1521 - Oracle user provisioned with SELECT ANY TABLE on the target schemas (or a specific grant list if that's narrower for your organization). Confirm from a SQL client:
SELECT * FROM session_privs WHERE privilege = 'SELECT ANY TABLE';
8.3 Target systems not in scope for v1
If either SAP or Oracle is out of v1 scope, skip that subsection. Can be added later — both integrations are additive.
9. Day-of-install window
9.1 Expected duration: 30–45 minutes
| Phase | Expected time |
|---|---|
| Installer bootstrap Bicep | 10–15 min (PG Flexible Server is the long pole) |
| Reconciler provisions application layer | 10–15 min |
| Health verification (5 checks) | <5 min |
Fail modes during the window are usually:
- Container Apps quota denied → 1.5 hours to file + approve
- PG quota denied in region → 1–3 business days
- Firewall blocks GHCR → ~30 min if your firewall team is on-call
9.2 Rollback plan
If the install fails unrecoverably, teardown is az group delete --name rg-<appName> --yes. Destructive; cleans up everything the installer created. No customer data risk because no customer data has been loaded yet.
Retry after teardown is safe — the reconciler has a self-heal path that handles Azure's 90-day Key Vault soft-delete recovery cleanly, so a same-day retry with the same --app-name works without intervention.
10. Green-light signal
Once items 1.1 through 4 pass, your team can green-light the install window. Items 5, 6, 7.2, 8 are common tripwires — resolve those before install day, not during it. Item 7.1 is informational.
Confirmation back to AltitudeIQ on the day of:
- Installer-running user's AAD UPN
- Chosen
--app-name - Chosen region
- Any customer-specific tags to pass as
--tags key=value - The green light from your security team
We'll be on call during the install window to troubleshoot any surprises.