Skip to main content

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

ComponentPurpose
Billing ScopeEA enrollment account
Management GroupGovernance placement
Spoke VNetNetwork isolation
Hub PeeringConnectivity
RBACInitial access
BudgetCost control
TagsResource organization

Next Steps

Continue to Platform Automation to learn about GitOps and CI/CD for landing zones.