Subscription Vending
How to implement automated subscription provisioning with guardrails.
What is Subscription Vending?
Subscription vending is the automated provisioning of Azure subscriptions with:
- Pre-configured governance (management group placement)
- Baseline networking (spoke VNet, peering)
- Identity configuration (RBAC assignments)
- Compliance settings (policies, tags)
Vending Architecture
Vending Input Schema
Request Parameters
{
"subscriptionRequest": {
"subscriptionName": "sub-finance-prod-001",
"displayName": "Finance Production",
"workloadType": "corp",
"environment": "prod",
"managementGroup": "alz-landingzones-corp-prod",
"owner": {
"objectId": "aaa-bbb-ccc-ddd",
"email": "finance-team@contoso.com",
"displayName": "Finance Team"
},
"billingScope": "/providers/Microsoft.Billing/billingAccounts/xxx/enrollmentAccounts/yyy",
"network": {
"enabled": true,
"addressSpace": "10.10.0.0/16",
"connectToHub": true,
"hubVnetId": "/subscriptions/xxx/resourceGroups/rg-hub/providers/Microsoft.Network/virtualNetworks/vnet-hub"
},
"tags": {
"CostCenter": "CC-12345",
"Application": "Finance",
"Environment": "Production",
"Owner": "finance-team@contoso.com",
"Department": "Finance"
},
"budget": {
"amount": 10000,
"currency": "USD",
"alertThresholds": [50, 80, 100]
}
}
}
Bicep Implementation
Subscription Vending Module
// modules/subscription-vending/main.bicep
targetScope = 'managementGroup'
// Parameters
@description('Subscription display name')
param parSubscriptionName string
@description('Billing scope for subscription')
param parBillingScope string
@description('Management group to place subscription')
param parManagementGroupId string
@description('Owner object ID for initial RBAC')
param parOwnerObjectId string
@description('Tags to apply')
param parTags object
@description('Enable networking')
param parNetworkingEnabled bool = true
@description('Spoke VNet address space')
param parSpokeAddressPrefix string = '10.10.0.0/16'
@description('Hub VNet resource ID')
param parHubVnetId string = ''
@description('Location')
param parLocation string = 'eastus'
// Create Subscription
resource subscription 'Microsoft.Subscription/aliases@2021-10-01' = {
name: parSubscriptionName
properties: {
displayName: parSubscriptionName
billingScope: parBillingScope
workload: 'Production'
additionalProperties: {
managementGroupId: '/providers/Microsoft.Management/managementGroups/${parManagementGroupId}'
tags: parTags
}
}
}
// Module: Spoke Networking
module spokeNetworking 'spoke-networking.bicep' = if (parNetworkingEnabled) {
name: 'spoke-networking-${uniqueString(parSubscriptionName)}'
scope: subscription
params: {
parLocation: parLocation
parSpokeAddressPrefix: parSpokeAddressPrefix
parHubVnetId: parHubVnetId
parTags: parTags
}
}
// Module: RBAC
module rbacAssignment 'rbac.bicep' = {
name: 'rbac-${uniqueString(parSubscriptionName)}'
scope: subscription
params: {
parOwnerObjectId: parOwnerObjectId
}
}
// Module: Budget
module budget 'budget.bicep' = {
name: 'budget-${uniqueString(parSubscriptionName)}'
scope: subscription
params: {
parBudgetAmount: 10000
parOwnerEmail: parTags.Owner
}
}
// Outputs
output outSubscriptionId string = subscription.properties.subscriptionId
output outSpokeVnetId string = parNetworkingEnabled ? spokeNetworking.outputs.outSpokeVnetId : ''
Spoke Networking Module
// modules/subscription-vending/spoke-networking.bicep
targetScope = 'subscription'
param parLocation string
param parSpokeAddressPrefix string
param parHubVnetId string
param parTags object
// Variables
var varResourceGroupName = 'rg-network-${parLocation}'
var varSpokeVnetName = 'vnet-spoke-${parLocation}'
// Resource Group
resource resourceGroup 'Microsoft.Resources/resourceGroups@2022-09-01' = {
name: varResourceGroupName
location: parLocation
tags: parTags
}
// Spoke VNet
module spokeVnet 'vnet.bicep' = {
name: 'spoke-vnet'
scope: resourceGroup
params: {
parVnetName: varSpokeVnetName
parLocation: parLocation
parAddressPrefix: parSpokeAddressPrefix
parTags: parTags
}
}
// VNet Peering to Hub
module peeringToHub 'peering.bicep' = if (!empty(parHubVnetId)) {
name: 'peering-to-hub'
scope: resourceGroup
params: {
parLocalVnetName: varSpokeVnetName
parRemoteVnetId: parHubVnetId
parPeeringName: 'peer-to-hub'
parAllowGatewayTransit: false
parUseRemoteGateways: true
}
dependsOn: [spokeVnet]
}
output outSpokeVnetId string = spokeVnet.outputs.outVnetId
VNet Module
// modules/subscription-vending/vnet.bicep
param parVnetName string
param parLocation string
param parAddressPrefix string
param parTags object
var varSubnets = [
{
name: 'snet-workload'
addressPrefix: cidrSubnet(parAddressPrefix, 24, 0)
privateEndpointNetworkPolicies: 'Disabled'
}
{
name: 'snet-private-endpoints'
addressPrefix: cidrSubnet(parAddressPrefix, 24, 1)
privateEndpointNetworkPolicies: 'Disabled'
}
]
resource vnet 'Microsoft.Network/virtualNetworks@2023-05-01' = {
name: parVnetName
location: parLocation
tags: parTags
properties: {
addressSpace: {
addressPrefixes: [parAddressPrefix]
}
subnets: [for subnet in varSubnets: {
name: subnet.name
properties: {
addressPrefix: subnet.addressPrefix
privateEndpointNetworkPolicies: subnet.privateEndpointNetworkPolicies
}
}]
}
}
output outVnetId string = vnet.id
RBAC Module
// modules/subscription-vending/rbac.bicep
targetScope = 'subscription'
param parOwnerObjectId string
// Owner Role Assignment
resource ownerAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(subscription().id, parOwnerObjectId, 'Owner')
properties: {
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635')
principalId: parOwnerObjectId
principalType: 'Group'
}
}
Budget Module
// modules/subscription-vending/budget.bicep
targetScope = 'subscription'
param parBudgetAmount int
param parOwnerEmail string
resource budget 'Microsoft.Consumption/budgets@2023-05-01' = {
name: 'budget-monthly'
properties: {
category: 'Cost'
amount: parBudgetAmount
timeGrain: 'Monthly'
timePeriod: {
startDate: '${utcNow('yyyy-MM')}-01'
}
notifications: {
'actual-50': {
enabled: true
threshold: 50
operator: 'GreaterThanOrEqualTo'
contactEmails: [parOwnerEmail]
thresholdType: 'Actual'
}
'actual-80': {
enabled: true
threshold: 80
operator: 'GreaterThanOrEqualTo'
contactEmails: [parOwnerEmail]
thresholdType: 'Actual'
}
'actual-100': {
enabled: true
threshold: 100
operator: 'GreaterThanOrEqualTo'
contactEmails: [parOwnerEmail]
thresholdType: 'Actual'
}
}
}
}
Self-Service Portal
Azure Logic App Workflow
{
"definition": {
"$schema": "https://schema.management.azure.com/providers/Microsoft.Logic/schemas/2016-06-01/workflowdefinition.json#",
"contentVersion": "1.0.0.0",
"triggers": {
"http_request": {
"type": "Request",
"kind": "Http",
"inputs": {
"schema": {
"type": "object",
"properties": {
"subscriptionName": {"type": "string"},
"workloadType": {"type": "string"},
"ownerEmail": {"type": "string"},
"costCenter": {"type": "string"}
}
}
}
}
},
"actions": {
"validate_request": {
"type": "Compose",
"inputs": "@triggerBody()"
},
"trigger_devops_pipeline": {
"type": "Http",
"inputs": {
"method": "POST",
"uri": "https://dev.azure.com/org/project/_apis/pipelines/1/runs?api-version=7.0",
"headers": {
"Authorization": "Bearer @{parameters('devops_token')}"
},
"body": {
"templateParameters": {
"subscriptionName": "@{triggerBody()?['subscriptionName']}",
"workloadType": "@{triggerBody()?['workloadType']}",
"ownerEmail": "@{triggerBody()?['ownerEmail']}"
}
}
}
},
"send_notification": {
"type": "ApiConnection",
"inputs": {
"host": {
"connection": {
"name": "@parameters('$connections')['office365']['connectionId']"
}
},
"method": "post",
"path": "/v2/Mail",
"body": {
"To": "@{triggerBody()?['ownerEmail']}",
"Subject": "Subscription Provisioning Started",
"Body": "Your subscription request has been received."
}
}
}
}
}
}
GitHub Actions Pipeline
name: Subscription Vending
on:
workflow_dispatch:
inputs:
subscriptionName:
description: 'Subscription name'
required: true
workloadType:
description: 'Workload type (corp/online)'
required: true
default: 'corp'
environment:
description: 'Environment'
required: true
default: 'prod'
ownerObjectId:
description: 'Owner Azure AD Object ID'
required: true
addressSpace:
description: 'VNet address space'
required: true
default: '10.10.0.0/16'
permissions:
id-token: write
contents: read
jobs:
vend-subscription:
runs-on: ubuntu-latest
environment: platform
steps:
- uses: actions/checkout@v4
- name: Azure Login
uses: azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Deploy Subscription
uses: azure/arm-deploy@v1
with:
scope: managementgroup
managementGroupId: 'alz-landingzones-${{ inputs.workloadType }}'
region: eastus
template: ./modules/subscription-vending/main.bicep
parameters: >
parSubscriptionName=${{ inputs.subscriptionName }}
parBillingScope=${{ secrets.BILLING_SCOPE }}
parManagementGroupId=alz-landingzones-${{ inputs.workloadType }}-${{ inputs.environment }}
parOwnerObjectId=${{ inputs.ownerObjectId }}
parSpokeAddressPrefix=${{ inputs.addressSpace }}
parHubVnetId=${{ secrets.HUB_VNET_ID }}
- name: Notify Owner
uses: dawidd6/action-send-mail@v3
with:
server_address: smtp.office365.com
server_port: 587
username: ${{ secrets.EMAIL_USERNAME }}
password: ${{ secrets.EMAIL_PASSWORD }}
subject: 'Subscription Ready: ${{ inputs.subscriptionName }}'
to: platform-team@contoso.com
body: |
Subscription ${{ inputs.subscriptionName }} has been provisioned.
- Management Group: alz-landingzones-${{ inputs.workloadType }}-${{ inputs.environment }}
- Address Space: ${{ inputs.addressSpace }}
IP Address Management
IPAM Integration
Azure IPAM Integration
// Get next available CIDR from Azure IPAM
var parSpokeAddressPrefix = '10.${padLeft(string(subscriptionSequence), 2, '0')}.0.0/16'
Vending Checklist
✅ Pre-Requisites
- Billing scope configured
- Management groups created
- Hub network deployed
- Service principal with permissions
- IPAM solution in place
✅ Subscription Setup
- Subscription created
- Placed in correct MG
- Baseline tags applied
- Owner RBAC assigned
- Budget configured
✅ Networking
- Spoke VNet created
- Peered to hub
- DNS configured
- Route tables applied
- NSGs deployed
✅ Post-Provisioning
- Owner notified
- Documentation updated
- CMDB updated
- Access verified
Quick Reference Card
| Component | Purpose |
|---|---|
| Billing Scope | EA enrollment account |
| Management Group | Governance placement |
| Spoke VNet | Network isolation |
| Hub Peering | Connectivity |
| RBAC | Initial access |
| Budget | Cost control |
| Tags | Resource organization |
Next Steps
Continue to Platform Automation to learn about GitOps and CI/CD for landing zones.