The Problem: Dev Environments Running 24/7 #
Our development Azure environment was costing $8,400/month. A significant chunk was:
- Azure Firewall Premium: ~$1,200/month (running 24/7)
- VPN Gateways (2x): ~$800/month
- ExpressRoute Gateway: ~$600/month
Developers needed these resources during work hours (8am-6pm Pacific). But they ran all night, every weekend, and during holidays.
Manual reminders didn’t work:
- “Remember to deallocate the firewall when done!”
- “Please shut down VPN gateways over the weekend!”
People forget. They’re busy. They’re focused on shipping code, not infrastructure costs.
The Insight: Developer Experience vs. Cost Optimization Isn’t Binary #
The key realization: You don’t need to choose between developer experience and cost savings.
What developers need:
- ✅ Resources available during work hours
- ✅ Fast startup when they need them
- ✅ Zero manual intervention
What finance needs:
- ✅ Resources not running when unused
- ✅ Predictable, optimized costs
- ✅ Clear cost attribution
Automated cleanup achieves both.
The Solution: Scheduled Deallocation Pipeline #
I created an Azure DevOps pipeline that runs every night and deallocates expensive resources in dev environments:
# azure-pipelines-cleanup.yml
name: Nightly Dev Environment Cleanup
schedules:
- cron: "30 6 * * *" # 6:30 UTC = 11:30 PM Pacific (after work hours)
displayName: Nightly cleanup
branches:
include:
- main
always: true # Run even if no code changes
trigger: none # Only run on schedule
variables:
- group: platform-dev-credentials
- name: subscriptionId
value: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx'
jobs:
- job: DeallocateExpensiveResources
displayName: 'Deallocate Dev Environment Resources'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: AzureCLI@2
displayName: 'Deallocate Azure Firewall'
inputs:
azureSubscription: 'Azure-Dev-ServiceConnection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
echo "Deallocating Azure Firewall in Dev environment..."
# Deallocate firewall (preserves configuration, stops billing)
az network firewall delete \
--name fw-hub-dev \
--resource-group rg-hub-dev
echo "Firewall deallocated. Configuration preserved in ARM templates."
- task: AzureCLI@2
displayName: 'Deallocate VPN Gateways'
inputs:
azureSubscription: 'Azure-Dev-ServiceConnection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
echo "Deallocating VPN Gateways..."
az network vnet-gateway delete --name vng-hub-dev-vpn1 --resource-group rg-hub-dev
az network vnet-gateway delete --name vng-hub-dev-vpn2 --resource-group rg-hub-dev
echo "VPN Gateways deallocated."
- task: AzureCLI@2
displayName: 'Deallocate ExpressRoute Gateway'
inputs:
azureSubscription: 'Azure-Dev-ServiceConnection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
echo "Deallocating ExpressRoute Gateway..."
az network vnet-gateway delete --name vng-hub-dev-er --resource-group rg-hub-dev
echo "ExpressRoute Gateway deallocated."
- task: Bash@3
displayName: 'Send Notification'
inputs:
targetType: 'inline'
script: |
echo "✅ Dev environment cleanup completed at $(date)"
echo "Resources will be recreated on next deployment."
# Add Teams/Slack notification here if desired
Morning Redeployment (Optional) #
For teams that want resources ready in the morning, add a morning pipeline:
schedules:
- cron: "0 15 * * 1-5" # 3:00 PM UTC = 7:00 AM Pacific, Mon-Fri
displayName: Morning environment prep
branches:
include:
- main
But we found developers prefer on-demand deployment - they run the deployment pipeline when needed (takes 60 minutes, but they control it).
Critical Safety Measures #
Never run cleanup against production:
schedules:
- cron: "30 6 * * *"
branches:
exclude:
- main # Or use explicit environment checks
Add subscription validation:
CURRENT_SUB=$(az account show --query id -o tsv)
ALLOWED_SUB="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" # Azure-Dev
if [ "$CURRENT_SUB" != "$ALLOWED_SUB" ]; then
echo "❌ ERROR: Not running in Dev subscription!"
echo "Current: $CURRENT_SUB"
echo "Expected: $ALLOWED_SUB"
exit 1
fi
The Results #
Cost Savings:
- Before: $8,400/month for dev environment
- After: $2,500/month (70% reduction)
- Annual savings: ~$70,000
What We Deallocate Nightly:
- Azure Firewall Premium: ~$1,200/mo → $40/mo (96% savings)
- VPN Gateways (2x): ~$800/mo → $0 (100% savings, recreated on-demand)
- ExpressRoute Gateway: ~$600/mo → $0 (100% savings, recreated on-demand)
Developer Impact:
- ✅ Zero complaints (they just redeploy when needed)
- ✅ Configuration always preserved (in IaC)
- ✅ Faster iteration (forces IaC testing)
Why This Works #
- Automated = Reliable: No human decisions, no forgotten shutdowns
- IaC-Friendly: Encourages treating infrastructure as code, not pets
- Configurable: Easy to adjust schedule or skip cleanup on-demand
- Auditable: Pipeline logs show exactly what was deallocated and when
Alternative Approaches #
For VM-heavy environments:
# Stop all VMs in resource group
az vm deallocate --ids $(az vm list -g rg-dev --query "[].id" -o tsv)
For Azure Virtual Desktop:
# Stop session hosts (preserves configuration)
az desktopvirtualization sessionhost update \
--resource-group rg-avd-dev \
--host-pool-name hp-dev \
--name sessionhost-0 \
--allow-new-session false
az vm deallocate --ids $(az vm list -g rg-avd-dev --query "[].id" -o tsv)
The Lesson #
Developer experience and cost optimization aren’t mutually exclusive. Good automation makes both possible.
Stop asking people to remember to save money. Build systems that save money automatically while preserving (or improving) developer experience.
Related posts:
Part of a series on lessons learned managing enterprise Azure infrastructure at scale.