OrkStageOrkStage

Microsoft Azure

Connect an Azure subscription with a service principal, prepare the Blob state container, and see what OrkStage creates on Container Apps and Flexible Server.

Preview

The Azure adapter is covered by unit tests and its OpenTofu modules pass tofu validate. It has not been verified against a live subscription yet.

OrkStage creates Azure resources with OpenTofu (hashicorp/azurerm 5.x) in a throwaway runner container, one isolated state per resource, kept in a Blob container in your own subscription. Health, secrets, logs and the restart action use the Azure Resource Manager, Key Vault and Log Analytics REST APIs.

What OrkStage creates

Every resource gets its own resource group (<name>-rg), so destroying it removes everything it created.

Template resourceCapabilityWhat gets created
networkvnet (default)Virtual network with a delegated Container Apps subnet, a delegated PostgreSQL subnet and a private endpoint subnet, optional NAT gateway, Log Analytics workspace, the Container Apps environment that the services of the network share, private DNS zone for Redis
compute.container-servicecontainer-apps (default)Container app with a user assigned identity, external ingress when public, CPU scale rule from autoscaling, Key Vault backed secrets for dependencies, Storage Blob Data Contributor on dependent storage accounts. Its own environment when there is no network.
database.postgresqlflexible-server (default)Flexible server with private access through the delegated subnet and a private DNS zone, database app, Key Vault holding the generated credentials
cache.redismanaged-redis (default)Azure Managed Redis (private endpoint when there is a network), Key Vault holding the access key
object-storageblob-storage (default)Storage account with one container, versioning, soft delete, lifecycle rules for retentionDays and the archive tier
ingresscontainer-apps-ingress (default)Nothing: each service keeps the HTTPS address of the built in Container Apps ingress
ingressfront-doorFront Door Standard profile and endpoint with one route and origin group per routed service

Services receive <ID>_HOST, <ID>_PORT, <ID>_URL, <ID>_BUCKET and <ID>_ENDPOINT for each dependency, and <ID>_CREDENTIALS with the JSON credentials from Key Vault (username, password, host, port, database, url). AZURE_CLIENT_ID points Azure SDKs at the app's identity for storage access.

PostgreSQL passwords are generated with an ephemeral resource and written through write only attributes, so they never enter OpenTofu state. Module outputs carry only the Key Vault secret id (secret_ref).

Production resources get Key Vault purge protection, a CanNotDelete lock on databases and storage accounts, and resource groups that refuse to be deleted while they still hold resources.

Target configuration

config fieldRequiredMeaning
tenantIdyesDirectory (tenant) id of the service principal
subscriptionIdyesSubscription that receives the resources
clientIdyesApplication (client) id of the service principal
stateResourceGroupyesResource group of the state storage account
stateStorageAccountyesStorage account for OpenTofu state, 3 to 24 lowercase letters and digits
stateContainernoBlob container for state. Defaults to orkstage-tfstate.

Credential secret

OrkStage signs in with a service principal and a client secret. The secret behind credentialRef is a JSON object:

{ "clientSecret": "<client secret>" }

A plain string is also accepted and is taken as clientSecret. The values reach OpenTofu only as runner environment variables (ARM_TENANT_ID, ARM_CLIENT_ID, ARM_CLIENT_SECRET, ARM_SUBSCRIPTION_ID). Workload identity federation, with no stored secret, is planned.

Prepare the subscription

Run these once per subscription with the Azure CLI, signed in as someone who can create role assignments.

SUB=<subscription id>
LOCATION=eastus
az account set --subscription "$SUB"

# Service principal for OrkStage
az ad sp create-for-rbac --name orkstage-deployer --role Contributor --scopes "/subscriptions/$SUB"
# Note appId (clientId), password (clientSecret) and tenant (tenantId).
APP_ID=<appId from the output>
az role assignment create --assignee "$APP_ID" --role "User Access Administrator" --scope "/subscriptions/$SUB"

# State storage: versioned, no shared keys, Microsoft Entra access only
az group create --name orkstage-state --location "$LOCATION"
az storage account create --name <stateaccount> --resource-group orkstage-state --location "$LOCATION" \
  --sku Standard_LRS --kind StorageV2 --min-tls-version TLS1_2 --allow-blob-public-access false \
  --allow-shared-key-access false
az storage account blob-service-properties update --account-name <stateaccount> --resource-group orkstage-state \
  --enable-versioning true --enable-delete-retention true --delete-retention-days 30
az storage container create --name orkstage-tfstate --account-name <stateaccount> --auth-mode login
az role assignment create --assignee "$APP_ID" --role "Storage Blob Data Contributor" \
  --scope "/subscriptions/$SUB/resourceGroups/orkstage-state/providers/Microsoft.Storage/storageAccounts/<stateaccount>"

OrkStage never creates the state container. Validation reports it as missing until it exists.

Permissions

RoleScopeWhy
ContributorsubscriptionCreate resource groups and every resource, register resource providers
User Access AdministratorsubscriptionRole assignments (Key Vault and storage access for apps, Key Vault access for OrkStage) and production delete locks
Storage Blob Data Contributorstate storage accountRead and write OpenTofu state

Owner on the subscription covers the first two. OrkStage gives itself Key Vault Secrets Officer on the vaults it creates, so it can write generated credentials and read them back for resources on other providers.

Resource providers are registered on first use (Microsoft.App, Microsoft.OperationalInsights, Microsoft.ManagedIdentity, Microsoft.Network, Microsoft.DBforPostgreSQL, Microsoft.Cache, Microsoft.KeyVault, Microsoft.Storage, Microsoft.Cdn). Validation warns about the ones that are not registered yet.

Regions

Regions are Azure locations such as eastus, brazilsouth or westeurope. List the ones the target may use in allowedRegions.

Actions for triggers

CapabilityActionParametersEffect
container-appsrestartnoneRestarts the active revision of the container app; its replicas are recreated

Logs come from the ContainerAppConsoleLogs_CL table of the Log Analytics workspace.

Known limits

  • Container Apps run on the Consumption profile: up to 4 vCPU and 8 GiB per replica, and memory is always twice the vCPU, so the size follows the larger of cpu and memoryMiB.
  • Images must be pullable without credentials. Private registries are not wired yet.
  • The CPU scale rule cannot wake an app from zero, so autoscaled apps keep at least one replica.
  • PostgreSQL backups cannot be turned off (minimum 7 days). High availability needs the General Purpose tier, so micro and small move to GP_Standard_D2ds_v5 when HA is requested.
  • Private PostgreSQL needs a network in the same placement slot. With publicAccess, the server accepts connections from Azure services only.
  • Managed Redis replication is on or off: any replicas above zero, or production, turns high availability on.
  • Blob Storage has shared keys turned off. Clients use Microsoft Entra identities, so the storage account hands no keys to resources on other providers.
  • Credentials published outside Azure Key Vault (another provider's secret_ref) are copied into Container Apps secrets at deploy time and reach the app as <ID>_USERNAME, <ID>_PASSWORD and so on (see plan and cost).
  • Front Door routes are created before the apps behind them. A route answers once its service has been deployed and joined its origin group. Only public apps can be origins.
  • Zone redundancy of the Container Apps environment (availabilityZones above 1) needs a region with availability zones.

On this page