Skip to main content

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 LinePurposeConnectivityPoliciesBudgetUse Cases
SandboxExperimentationNoneRelaxedLow ($500/mo)Learning, POCs, testing
DevelopmentDev/test workloadsOptionalModerateMedium ($2K/mo)Non-prod applications
Corp-ConnectedProduction appsHub VNetStrictHigh ($10K/mo)Enterprise apps, databases
OnlineInternet-facingNo hubStrictHigh ($10K/mo)Public websites, SaaS
Data & AnalyticsBig data platformsOptionalData governanceVariableSynapse, 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:

  1. User submits request via ServiceNow
  2. Approval workflow (manager + platform team)
  3. ServiceNow triggers webhook to Azure DevOps pipeline
  4. Pipeline runs Terraform to provision subscription
  5. 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