A workload team submits a ticket for a new Azure subscription. Three weeks later, a subscription exists — manually created, placed in the wrong management group, and named subscription-1 because no naming convention was enforced. The team deploys anyway, into an environment with no tags, no diagnostic settings, and no budget alert. This is the default outcome when subscription creation is manual.
As a landing zone scales, the cost of manual onboarding compounds. Every manual entry is a governance gap. Subscription vending replaces this with a PR-based workflow: a workload team submits a YAML file; the pipeline creates the subscription, places it in the hierarchy, deploys the spoke network, and configures the monitoring baseline — consistently, in under 10 minutes.
By the end of this guide, you will:
- Identify which Azure billing account types support programmatic creation.
- Build a Terraform vending module that provisions a complete application landing zone.
- Implement the Bicep
sub-vendingAVM pattern module. - Design a PR approval workflow with automated validation.
This is Post 5 in the Azure Platform Engineering series.
Subscription Creation Prerequisites
Billing Account Compatibility
Programmatic creation requires specific billing roles:
- Enterprise Agreement (EA): Requires
Account Owneron the EA enrollment account. - Microsoft Customer Agreement (MCA): Requires
Billing Profile ContributororInvoice Section Owner. Note that MCA accounts default to a 5-subscription limit and 1 creation per 24 hours. Open a support ticket to increase these quotas early. - Pay-As-You-Go: Does not support programmatic creation.
Tip: Find your MCA billing IDs via CLI:
az billing profile list --account-name <account-name> --query "[].{Name:name, Id:id}"
What Every Vended Subscription Receives
The vending module is a contract. Every subscription receives:
- Hierarchy Placement: Correct MG tier (Corp, Online, Sandbox).
- Mandatory Tags:
Owner,CostCenter,Environment. - Spoke Networking: VNet with consistent naming and CIDR allocation.
- Hub Connectivity: Bidirectional peering with gateway transit.
- Egress Control: UDR routing all
0.0.0.0/0traffic through the hub Firewall. - Baseline RBAC:
Contributorfor the workload team. - Monitoring: Activity logs forwarded to the platform Log Analytics Workspace.
Terraform Vending Module
Subscription and Propagation
resource "azurerm_subscription" "workload" {
subscription_name = local.subscription_display_name
alias = local.subscription_display_name # Idempotent anchor
billing_scope_id = var.billing_scope
workload = var.environment == "sandbox" ? "DevTest" : "Production"
}
# Wait for Entra directory replication (90s)
resource "time_sleep" "wait_for_propagation" {
create_duration = "90s"
depends_on = [azurerm_subscription.workload]
}
Spoke Networking
module "spoke_vnet" {
source = "Azure/avm-res-network-virtualnetwork/azurerm"
version = "~> 0.7"
providers = { azurerm = azurerm.workload }
name = "vnet-${var.workload_name}-001"
address_space = [var.vnet_address_prefix]
subnets = {
workload = {
name = "snet-workload-001"
address_prefixes = [cidrsubnet(var.vnet_address_prefix, 2, 0)]
route_table_resource_id = module.spoke_udr.resource_id # Correct attribute
}
}
}
Bicep Vending Module
Use the official AVM pattern module br/public:avm/ptn/lz/sub-vending.
module subVend 'br/public:avm/ptn/lz/sub-vending:0.3.0' = {
name: 'subVend-${workloadName}'
params: {
subscriptionAliasEnabled: true
subscriptionDisplayName: subscriptionDisplayName
subscriptionAliasName: subscriptionDisplayName
subscriptionBillingScope: billingScope
subscriptionManagementGroupId: targetManagementGroupId
virtualNetworkEnabled: true
virtualNetworkAddressSpace: [vnetAddressPrefix]
hubNetworkResourceId: hubVnetResourceId
}
}
PR Approval Workflow
The workflow uses GitHub Actions to validate requests before they reach a platform engineer.
Validation Stage:
- Schema Check: Validates the YAML request.
- CIDR Overlap: Checks
ipam.yamlto ensure no address space conflicts. - Plan Preview: Posts the
terraform planoutput as a PR comment.
Apply Stage:
Executes the deployment only after the PR is merged to main.
Fixing GHA Path Parsing: Ensure your GitHub Script correctly parses multiline file lists:
// Correct regex splitting for multiline git diff output
const files = '${{ steps.changed.outputs.files }}'.split(/\s+/).filter(Boolean);
graph TD
User[Workload Team] -- Submit YAML --> PR[Pull Request]
subgraph CI_Validation [Automation Pipeline]
PR -- Trigger --> Valid[Schema & CIDR Validation]
Valid -- Plan --> Preview[Terraform Plan / What-If]
end
Preview -- Comment --> Approver[Platform Engineer]
Approver -- Merge --> Deploy[Deployment Phase]
subgraph Deployment [Azure Infrastructure]
Deploy --> Sub[Create Subscription]
Sub --> Net[VNet & Peering]
Net --> Gov[Apply Policy & RBAC]
end
Gov --> Notify[Notify Team: Ready]
Notes:
- PR-based self-service allows teams to request resources without manual ticketing delays.
- Validation steps prevent IP address overlaps and configuration errors before deployment.
- The final Notification provides the workload team with the credentials/access to their new, compliant environment.
Best Practices
- Isolate State: Use one Terraform state file per subscription to isolate the blast radius of failures.
- Mandatory Budgets: Make budget alerts a required field in the request schema.
- Log Category Support: When configuring diagnostic settings, ensure the requested categories (like
Administrative) are supported at the subscription level to avoid deployment warnings. - IPAM Source of Truth: Update your
ipam.yamlin the same pipeline run that provisions the subscription to prevent race conditions.
Sources
Next, move to Post 6: Centralized Monitoring. We will deploy the Log Analytics Workspace that receives the activity logs from these vended subscriptions.
