Subscription Vending
What is Subscription Vending?
Subscription vending is an automated self-service process that enables application teams to request and receive fully configured Azure subscriptions on-demand—similar to vending machine dispensing items.
Instead of manually creating subscriptions and configuring governance, teams fill out a request form and receive a ready-to-use subscription with:
- Pre-configured networking
- Security policies applied
- Logging and monitoring enabled
- Budget alerts set
- RBAC permissions assigned
┌─────────────────────────────────────────────────────────┐
│ Subscription Vending Flow │
└─────────────────────────────────────────────────────────┘
Team Submits Request (GitHub Issue / ServiceNow)
│
▼
┌───────────────────────────┐
│ Validate Request │ ← Check approvals, quotas
└───────────┬───────────────┘
│
▼
┌───────────────────────────┐
│ Create Subscription │ ← Azure Billing API
└───────────┬───────────────┘
│
▼
┌───────────────────────────┐
│ Apply Baseline Config │ ← Terraform/Bicep module
│ - Management group │
│ - Azure policies │
│ - Networking (VNet) │
│ - Logging integration │
│ - Budget alerts │
│ - RBAC roles │
└───────────┬───────────────┘
│
▼
┌───────────────────────────┐
│ Notify Team │ ← Subscription details
└───────────────────────────┘
Why Implement Subscription Vending?
Without Subscription Vending
❌ Manual subscription creation (days/weeks)
❌ Inconsistent configurations
❌ Missing security controls
❌ No standardization
❌ Platform team bottleneck
❌ Shadow IT (teams use personal accounts)
With Subscription Vending
✅ Self-Service: Teams get subscriptions in minutes, not days
✅ Consistency: Every subscription follows same baseline
✅ Compliance: Security and governance from day one
✅ Scalability: Platform team doesn't block application teams
✅ Visibility: Centralized inventory of all subscriptions
✅ Cost Control: Budgets and tagging enforced automatically
When to Implement Subscription Vending
✅ Implement When:
- Frequent subscription requests: Creating 2+ subscriptions per month
- Multiple teams: 5+ application teams deploying to Azure
- Governance requirements: Need consistent policy enforcement
- Compliance mandates: Regulated industry (finance, healthcare, government)
- Cloud Center of Excellence exists: Centralized platform team
- Subscription sprawl: Hard to track who owns what
⏳ Consider Deferring When:
- Small organization: 1-2 teams, low growth
- Single application: Not planning multi-subscription architecture
- Pilot phase: Still evaluating Azure
Subscription Types (Product Lines)
Define product lines for different subscription categories:
| Product Line | Purpose | Connectivity | Policies | Budget | Use Cases |
|---|---|---|---|---|---|
| Sandbox | Experimentation | None | Relaxed | Low ($500/mo) | Learning, POCs, testing |
| Development | Dev/test workloads | Optional | Moderate | Medium ($2K/mo) | Non-prod applications |
| Corp-Connected | Production apps | Hub VNet | Strict | High ($10K/mo) | Enterprise apps, databases |
| Online | Internet-facing | No hub | Strict | High ($10K/mo) | Public websites, SaaS |
| Data & Analytics | Big data platforms | Optional | Data governance | Variable | Synapse, Databricks, Data Lake |
Example: Sandbox Subscription Configuration
Characteristics:
- No production data allowed
- Auto-delete resources >30 days old
- Limited VM sizes (Standard_B2s only)
- No VNet peering
- Lower quotas
# Terraform: Sandbox subscription vending
module "sandbox_subscription" {
source = "Azure/lz-vending/azurerm"
version = "~> 4.0"
subscription_alias_enabled = true
subscription_billing_scope = var.billing_account_id
subscription_display_name = "sub-${var.team_name}-sandbox"
subscription_alias_name = "sub-${var.team_name}-sandbox"
subscription_workload = "DevTest"
# Place in Sandbox management group
subscription_management_group_id = "mg-sandbox"
# Networking: Isolated VNet
virtual_network_enabled = true
virtual_networks = {
vnet1 = {
name = "vnet-${var.team_name}-sandbox"
address_space = ["10.100.0.0/24"]
location = var.location
resource_group_name = "rg-${var.team_name}-networking-sandbox"
subnets = {
default = {
address_prefixes = ["10.100.0.0/26"]
}
}
}
}
# Role assignments
role_assignment_enabled = true
role_assignments = {
owner = {
principal_id = var.team_ad_group_id
definition = "Owner"
relative_scope = ""
}
}
# Resource tags
subscription_tags = {
Environment = "Sandbox"
Team = var.team_name
CostCenter = var.cost_center
AutoDelete = "30days"
DataClassification = "Internal"
}
}
Example: Production Corp-Connected Subscription
Characteristics:
- Hub VNet peering
- Strict Azure policies
- No public IPs allowed
- Mandatory encryption
- Higher VM quotas
# Terraform: Production subscription vending
module "prod_subscription" {
source = "Azure/lz-vending/azurerm"
version = "~> 4.0"
subscription_alias_enabled = true
subscription_billing_scope = var.billing_account_id
subscription_display_name = "sub-${var.team_name}-prod"
subscription_alias_name = "sub-${var.team_name}-prod"
subscription_workload = "Production"
# Place in Corp management group
subscription_management_group_id = "mg-landing-zones-corp"
# Hub-connected networking
virtual_network_enabled = true
virtual_networks = {
spoke = {
name = "vnet-${var.team_name}-prod"
address_space = ["10.10.0.0/16"]
location = var.location
resource_group_name = "rg-${var.team_name}-networking-prod"
subnets = {
app = {
address_prefixes = ["10.10.1.0/24"]
}
data = {
address_prefixes = ["10.10.2.0/24"]
}
privatelink = {
address_prefixes = ["10.10.3.0/24"]
}
}
# Peer to hub VNet
vnet_peering = {
to_hub = {
name = "peer-to-hub"
remote_virtual_network_id = var.hub_vnet_id
allow_forwarded_traffic = true
use_remote_gateways = true
}
}
}
}
# Role assignments
role_assignment_enabled = true
role_assignments = {
contributor = {
principal_id = var.team_ad_group_id
definition = "Contributor"
relative_scope = ""
}
reader = {
principal_id = var.security_team_ad_group_id
definition = "Reader"
relative_scope = ""
}
}
# Resource tags
subscription_tags = {
Environment = "Production"
Team = var.team_name
CostCenter = var.cost_center
DataClassification = "Confidential"
SLA = "99.9%"
}
}
# Budget alert
resource "azurerm_consumption_budget_subscription" "prod" {
name = "budget-${var.team_name}-prod"
subscription_id = module.prod_subscription.subscription_id
amount = 10000
time_grain = "Monthly"
time_period {
start_date = "2024-01-01T00:00:00Z"
}
notification {
enabled = true
threshold = 80
operator = "GreaterThan"
threshold_type = "Forecasted"
contact_emails = [
var.team_email,
"finance@example.com"
]
}
notification {
enabled = true
threshold = 100
operator = "GreaterThan"
contact_emails = [
var.team_email,
"finance@example.com"
]
}
}
Request Process
Option 1: GitHub Issues (Simple)
Create a GitHub repository for subscription requests:
1. Issue Template
<!-- .github/ISSUE_TEMPLATE/subscription-request.yml -->
name: Subscription Request
description: Request a new Azure subscription
title: "[SUB REQUEST] <Team Name> - <Environment>"
labels: ["subscription-request", "pending-approval"]
body:
- type: input
id: team-name
attributes:
label: Team Name
description: Name of the team requesting the subscription
placeholder: "platform-team"
validations:
required: true
- type: dropdown
id: product-line
attributes:
label: Subscription Type
description: Select subscription product line
options:
- Sandbox
- Development
- Corp-Connected Production
- Online Production
- Data & Analytics
validations:
required: true
- type: input
id: cost-center
attributes:
label: Cost Center
placeholder: "CC-12345"
validations:
required: true
- type: input
id: budget
attributes:
label: Monthly Budget (USD)
placeholder: "5000"
validations:
required: true
- type: textarea
id: justification
attributes:
label: Business Justification
description: Why is this subscription needed?
validations:
required: true
- type: input
id: owner-email
attributes:
label: Subscription Owner Email
placeholder: "owner@example.com"
validations:
required: true
- type: checkboxes
id: acknowledgment
attributes:
label: Acknowledgment
options:
- label: I understand this subscription will inherit Azure policies from the management group
required: true
- label: I will tag all resources appropriately
required: true
- label: I will monitor costs and stay within budget
required: true
2. GitHub Actions Workflow
# .github/workflows/subscription-vending.yml
name: Subscription Vending
on:
issues:
types: [labeled]
permissions:
issues: write
id-token: write
contents: read
jobs:
provision-subscription:
if: contains(github.event.issue.labels.*.name, 'approved')
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Parse Issue
id: parse
uses: actions/github-script@v7
with:
script: |
const body = context.payload.issue.body;
// Extract values from issue body (simplified)
const teamName = body.match(/Team Name:\s*(.+)/)[1].trim();
const productLine = body.match(/Subscription Type:\s*(.+)/)[1].trim();
const costCenter = body.match(/Cost Center:\s*(.+)/)[1].trim();
const budget = body.match(/Monthly Budget.*:\s*(.+)/)[1].trim();
core.setOutput('team_name', teamName);
core.setOutput('product_line', productLine);
core.setOutput('cost_center', costCenter);
core.setOutput('budget', budget);
- name: Azure Login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
- name: Terraform Init
run: terraform init
working-directory: ./subscription-vending
- name: Terraform Apply
run: |
terraform apply -auto-approve \
-var="team_name=${{ steps.parse.outputs.team_name }}" \
-var="product_line=${{ steps.parse.outputs.product_line }}" \
-var="cost_center=${{ steps.parse.outputs.cost_center }}" \
-var="budget=${{ steps.parse.outputs.budget }}"
working-directory: ./subscription-vending
- name: Get Subscription ID
id: subscription
run: |
SUB_ID=$(terraform output -raw subscription_id)
echo "subscription_id=$SUB_ID" >> $GITHUB_OUTPUT
working-directory: ./subscription-vending
- name: Comment on Issue
uses: actions/github-script@v7
with:
script: |
const comment = `✅ **Subscription Provisioned Successfully**
**Subscription ID:** \`${{ steps.subscription.outputs.subscription_id }}\`
**Team:** ${{ steps.parse.outputs.team_name }}
**Type:** ${{ steps.parse.outputs.product_line }}
**Next Steps:**
1. Access the subscription: [Azure Portal](https://portal.azure.com/#@example.com/resource/subscriptions/${{ steps.subscription.outputs.subscription_id }})
2. Review applied policies
3. Start deploying resources
**Important:**
- Budget alerts configured for ${{ steps.parse.outputs.budget }} USD/month
- All resources must be tagged with \`Team\`, \`Environment\`, \`CostCenter\`
- Monitor costs daily: [Cost Management](https://portal.azure.com/#blade/Microsoft_Azure_CostManagement/Menu/costanalysis)`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: comment
});
github.rest.issues.update({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
state: 'closed',
labels: ['provisioned']
});
Option 2: ServiceNow / Internal Portal (Enterprise)
For larger organizations, integrate with enterprise service catalog:
Workflow:
- User submits request via ServiceNow
- Approval workflow (manager + platform team)
- ServiceNow triggers webhook to Azure DevOps pipeline
- Pipeline runs Terraform to provision subscription
- ServiceNow updated with subscription details
Baseline Configuration
Every vended subscription should include:
1. Management Group Assignment
resource "azurerm_management_group_subscription_association" "example" {
management_group_id = var.management_group_id
subscription_id = module.subscription.subscription_id
}
2. Azure Policies
# Example policies applied at management group level (inherited)
- Require specific tags
- Enforce naming conventions
- Deny public IPs (production only)
- Require encryption at rest
- Allowed VM sizes
- Allowed locations
3. Diagnostic Settings
# Send all logs to central Log Analytics
resource "azurerm_monitor_diagnostic_setting" "subscription" {
name = "diag-subscription-logs"
target_resource_id = "/subscriptions/${module.subscription.subscription_id}"
log_analytics_workspace_id = var.central_log_analytics_id
enabled_log {
category = "Administrative"
}
enabled_log {
category = "Security"
}
enabled_log {
category = "Policy"
}
metric {
category = "AllMetrics"
enabled = false
}
}
4. Resource Groups
# Pre-create standard resource groups
resource "azurerm_resource_group" "networking" {
name = "rg-${var.team_name}-networking-${var.environment}"
location = var.location
}
resource "azurerm_resource_group" "compute" {
name = "rg-${var.team_name}-compute-${var.environment}"
location = var.location
}
resource "azurerm_resource_group" "data" {
name = "rg-${var.team_name}-data-${var.environment}"
location = var.location
}
5. Networking
# VNet with standardized subnets
resource "azurerm_virtual_network" "main" {
name = "vnet-${var.team_name}-${var.environment}"
address_space = var.vnet_cidr
location = var.location
resource_group_name = azurerm_resource_group.networking.name
tags = var.tags
}
# Network Security Groups
resource "azurerm_network_security_group" "app" {
name = "nsg-app-${var.environment}"
location = var.location
resource_group_name = azurerm_resource_group.networking.name
# Baseline security rules
security_rule {
name = "DenyAllInbound"
priority = 4096
direction = "Inbound"
access = "Deny"
protocol = "*"
source_port_range = "*"
destination_port_range = "*"
source_address_prefix = "*"
destination_address_prefix = "*"
}
}
6. RBAC Assignments
# Team gets Contributor
resource "azurerm_role_assignment" "team_contributor" {
scope = "/subscriptions/${module.subscription.subscription_id}"
role_definition_name = "Contributor"
principal_id = var.team_ad_group_id
}
# Security team gets Reader
resource "azurerm_role_assignment" "security_reader" {
scope = "/subscriptions/${module.subscription.subscription_id}"
role_definition_name = "Reader"
principal_id = var.security_team_ad_group_id
}
# Finance team gets Cost Management Reader
resource "azurerm_role_assignment" "finance" {
scope = "/subscriptions/${module.subscription.subscription_id}"
role_definition_name = "Cost Management Reader"
principal_id = var.finance_team_ad_group_id
}
7. Budget Alerts
resource "azurerm_consumption_budget_subscription" "main" {
name = "budget-${var.team_name}-${var.environment}"
subscription_id = module.subscription.subscription_id
amount = var.monthly_budget
time_grain = "Monthly"
time_period {
start_date = formatdate("YYYY-MM-01'T'00:00:00'Z'", timestamp())
}
notification {
enabled = true
threshold = 80
operator = "GreaterThan"
threshold_type = "Actual"
contact_emails = [var.team_email, "finance@example.com"]
}
notification {
enabled = true
threshold = 100
operator = "GreaterThan"
threshold_type = "Forecasted"
contact_emails = [var.team_email, "finance@example.com", "leadership@example.com"]
}
}
Best Practices
1. Treat Subscriptions as Cattle, Not Pets
✅ Do: Automate subscription creation and deletion
❌ Don't: Manually configure each subscription
2. Implement Approval Workflows
# GitHub environment with approvals
environment: subscription-vending
protection_rules:
- type: required_reviewers
reviewers:
- platform-team
- finance-team
3. Use Service Catalog
Provide teams with pre-defined subscription types:
- Sandbox ($500/mo budget)
- Development ($2K/mo)
- Production ($10K/mo)
4. Automate Decommissioning
# Auto-delete sandbox subscriptions after 90 days
resource "azurerm_policy_definition" "auto_delete_sandbox" {
name = "auto-delete-sandbox-subs"
policy_type = "Custom"
mode = "All"
display_name = "Auto-delete Sandbox Subscriptions"
policy_rule = jsonencode({
if = {
allOf = [
{
field = "type"
equals = "Microsoft.Subscription"
},
{
field = "tags['Environment']"
equals = "Sandbox"
},
{
field = "tags['CreatedDate']"
less = "[addDays(utcNow(), -90)]"
}
]
}
then = {
effect = "delete"
}
})
}
5. Maintain Subscription Inventory
# Export to Azure Table Storage
resource "azurerm_storage_table" "subscriptions" {
name = "subscriptioninventory"
storage_account_name = azurerm_storage_account.platform.name
}
# Track: Team, Environment, Cost Center, Created Date, Budget
Common Pitfalls
❌ 1. No Approval Process
Issue: Teams create unlimited subscriptions
Fix: Require manager + platform team approval
❌ 2. No Budget Controls
Issue: Runaway costs
Fix: Enforce budgets, alert at 80%, auto-shutdown at 100%
❌ 3. Inconsistent Tagging
Issue: Can't track costs by team
Fix: Enforce tags via Azure Policy (deny creation without tags)
❌ 4. Manual Networking
Issue: IP address conflicts
Fix: Automate VNet creation with IPAM (IP Address Management)
❌ 5. No Subscription Lifecycle
Issue: Zombie subscriptions cost money
Fix: Implement auto-deletion for sandbox, quarterly reviews for production
Monitoring and Governance
Track Key Metrics
-- KQL query: Subscriptions created per month
Resources
| where type == "microsoft.subscription/aliases"
| extend CreatedDate = todatetime(tags["CreatedDate"])
| summarize Count=count() by bin(CreatedDate, 30d)
| render timechart
Compliance Checks
# Run compliance scan
az policy state trigger-scan --resource-group rg-platform-governance
# Export non-compliant resources
az policy state list \
--filter "complianceState eq 'NonCompliant'" \
--query "[].{Resource:resourceId, Policy:policyDefinitionName}" \
--output table