¿Te has preguntado cuánto dinero estás desperdiciando por dejar VMs de desarrollo o testing encendidas durante las noches y fines de semana? En este artículo te mostraré cómo implementé una solución completa de automatización en Azure que no solo gestiona el ciclo de vida de tus VMs, sino que también te mantiene informado de cada operación mediante notificaciones inteligentes.
El Problema: VMs Olvidadas = Dinero Desperdiciado
En entornos de desarrollo y preproducción, es común que los equipos dejen las máquinas virtuales encendidas 24/7 “por si acaso”. Sin embargo, esto genera costos innecesarios:
- Una VM B2s en Azure cuesta aproximadamente €30/mes si se ejecuta continuamente
- Si solo necesitas esa VM 8 horas al día (de lunes a viernes), podrías estar pagando 3x más de lo necesario
- Multiplica esto por 10, 20 o 50 VMs en tu organización… ¡los números se disparan!
La solución: Automatizar el encendido y apagado de VMs basándose en horarios y etiquetas.
La Solución: Infraestructura como Código con Terraform
He desarrollado una solución completa usando Terraform que implementa:
✅ Azure Automation Account con identidad administrada
✅ Runbooks PowerShell para iniciar/detener VMs
✅ Schedules automatizados para ejecución programada
✅ Filtrado por tags para gestión granular
✅ Notificaciones por email con resumen de operaciones
✅ Monitorización con Log Analytics y alertas
✅ Conectividad privada mediante Private Endpoints
Arquitectura de la Solución
La arquitectura se compone de varios componentes de Azure trabajando en conjunto:
graph TB
subgraph "Recursos de Red"
VNET[Virtual Network<br/>10.248.4.0/24]
SUBNET[Subnet<br/>subnet-lab-01]
PDNS[Private DNS Zone<br/>privatelink.azure-automation.net]
PE[Private Endpoint<br/>Automation Account]
end
subgraph "Automation"
AA[Automation Account<br/>aa-prod<br/>System Assigned Identity]
RB_START[Runbook: vm-start.ps1<br/>PowerShell]
RB_STOP[Runbook: vm-stop.ps1<br/>PowerShell]
SCH_START[Schedule: Start<br/>Daily 08:00 AM]
SCH_STOP[Schedule: Stop<br/>Daily 05:00 PM]
VAR[Variable: ACTION_GROUP_ID]
end
subgraph "Monitorización"
LAW[Log Analytics Workspace<br/>law-lab-01]
AG[Action Group<br/>ag-vm-tag-email-lab-01]
ALERT[Scheduled Query Alert<br/>automation-runbook-finished]
end
subgraph "Máquinas Virtuales Target"
VM1[VM 1<br/>tag: environment=pre]
VM2[VM 2<br/>tag: environment=pre]
VM3[VM 3<br/>tag: environment=pre]
end
VNET --> SUBNET
SUBNET --> PE
PE --> AA
PDNS -.DNS Resolution.-> PE
SCH_START -->|Trigger| RB_START
SCH_STOP -->|Trigger| RB_STOP
AA --> RB_START
AA --> RB_STOP
AA --> VAR
RB_START -->|Start-AzVM| VM1
RB_START -->|Start-AzVM| VM2
RB_START -->|Start-AzVM| VM3
RB_STOP -->|Stop-AzVM| VM1
RB_STOP -->|Stop-AzVM| VM2
RB_STOP -->|Stop-AzVM| VM3
VAR -->|Action Group ID| RB_START
VAR -->|Action Group ID| RB_STOP
RB_START -->|Send Notification| AG
RB_STOP -->|Send Notification| AG
AG -->|Email| EMAIL[📧 ops@domain.com]
AA -->|Logs| LAW
RB_START -->|Execution Logs| LAW
RB_STOP -->|Execution Logs| LAW
LAW --> ALERT
ALERT -->|Trigger on Job Finish| AG
style AA fill:#0078D4,stroke:#005A9E,color:#fff
style RB_START fill:#50E6FF,stroke:#0078D4,color:#000
style RB_STOP fill:#50E6FF,stroke:#0078D4,color:#000
style LAW fill:#FFA500,stroke:#FF8C00,color:#000
style AG fill:#FFD700,stroke:#FFA500,color:#000
style VM1 fill:#90EE90,stroke:#32CD32,color:#000
style VM2 fill:#90EE90,stroke:#32CD32,color:#000
style VM3 fill:#90EE90,stroke:#32CD32,color:#000
style EMAIL fill:#FFB6C1,stroke:#FF69B4,color:#000
Componentes Principales
1. Automation Account
El cerebro de la operación. Configurado con:
- Identidad Administrada (System Assigned): No requiere credenciales explícitas
- Conectividad Privada: Mediante Private Endpoint para mayor seguridad
- SKU Basic: Suficiente para la mayoría de casos de uso
2. Runbooks PowerShell
Scripts que ejecutan la lógica de negocio:
vm-start.ps1: Enciende VMs filtradas por tagsvm-stop.ps1: Apaga VMs filtradas por tags
Cada runbook sigue este flujo:
sequenceDiagram
participant SCH as Automation Schedule
participant RB as Runbook
participant AAI as Managed Identity
participant AZURE as Azure ARM
participant VM as Virtual Machines
participant AG as Action Group
participant EMAIL as Email
SCH->>RB: Trigger (08:00 AM)
activate RB
RB->>AAI: Connect-AzAccount -Identity
AAI-->>RB: Token de autenticación
RB->>AZURE: Get-AzVM -Status
AZURE-->>RB: Lista de VMs
Note over RB: Filtrar VMs con<br/>tag: environment=pre
loop Para cada VM objetivo
RB->>VM: Verificar PowerState
VM-->>RB: Estado actual
alt VM no está running
RB->>VM: Start-AzVM
VM-->>RB: Operación iniciada
else VM ya está running
RB->>RB: Skip
end
end
Note over RB: Esperar 60 segundos
RB->>RB: Start-Sleep -Seconds 60
RB->>AZURE: Get-AzVM -Status (verificación)
AZURE-->>RB: Estados actualizados
RB->>RB: Construir resumen
RB->>AG: POST /createNotifications
AG->>EMAIL: Enviar email con resumen
deactivate RB
Note over EMAIL: 📧 Resumen recibido:<br/>VMs running/stopped<br/>Errores si los hay
3. Schedules Automatizados
Programaciones que disparan los runbooks:
- Inicio: Lunes a Viernes, 08:00 AM (excluye fines de semana)
- Parada: Viernes, 05:00 PM (las VMs permanecen apagadas todo el fin de semana)
- Zona horaria: Europe/Madrid (configurable)
- Días configurables: Puedes ajustar
week_dayspara incluir/excluir días específicos
4. Action Groups y Notificaciones
Sistema de notificaciones que envía emails con:
- Lista de VMs que se iniciaron correctamente
- Lista de VMs que no pudieron iniciarse
- Detalles de errores encontrados
- Estado final de cada VM
Implementación Paso a Paso
Paso 1: Preparar el Entorno
Clona el repositorio:
git clone https://github.com/luisadanmunoz/start-stop-VM-alarm.git
cd start-stop-VM-alarm
Paso 2: Configurar Variables
Crea tu archivo terraform.tfvars basándote en el ejemplo:
# Tags para identificar recursos
tags = {
environment = "lab"
project = "vm-automation"
}
# Resource Group
rg = {
rg1 = {
resource_group_name = "rg-automation-lab"
location = "spaincentral"
}
}
# Automation Account
automation_accounts = {
aa-prod = {
automation_account_name = "aa-vm-automation"
resource_group_name = "rg-automation-lab"
location = "spaincentral"
sku_name = "Basic"
identity_type = "SystemAssigned"
public_network_access_enabled = false
private_dns_zone_ids = "privatelink.azure-automation.net"
subnet = "subnet-automation"
}
}
# Runbooks
automation_runbooks = {
rb_vm_start = {
resource_group_name = "rg-automation-lab"
location = "spaincentral"
automation_account_name = "aa-vm-automation"
runbook_type = "PowerShell"
script_path = "runbooks/vm-start.ps1"
description = "Arranque automático de VMs"
}
rb_vm_stop = {
resource_group_name = "rg-automation-lab"
location = "spaincentral"
automation_account_name = "aa-vm-automation"
runbook_type = "PowerShell"
script_path = "runbooks/vm-stop.ps1"
description = "Parada automática de VMs"
}
}
# Schedules
automation_schedule = {
# OPCIÓN 1: Solo días laborables (Lunes a Viernes) - SIN FINES DE SEMANA
sch_vm_start_weekdays_0800 = {
name = "sch-vm-start-weekdays-0800"
resource_group_name = "rg-automation-lab"
automation_account_name = "aa-vm-automation"
frequency = "Week"
interval = 1
week_days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
vm_start_schedule_start_time = "2026-01-27T08:00:00+01:00" # Debe ser lunes
vm_start_schedule_description = "Arranque automático VMs pre (L-V)"
vm_start_schedule_timezone = "Europe/Madrid"
runbook_name = "rb_vm_start"
tag_key = "environment"
tag_value = "pre"
}
sch_vm_stop_weekdays_1700 = {
name = "sch-vm-stop-weekdays-1700"
resource_group_name = "rg-automation-lab"
automation_account_name = "aa-vm-automation"
frequency = "Week"
interval = 1
week_days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
vm_start_schedule_start_time = "2026-01-24T17:00:00+01:00" # Debe ser viernes
vm_start_schedule_description = "Parada automática VMs pre (L-V)"
vm_start_schedule_timezone = "Europe/Madrid"
runbook_name = "rb_vm_stop"
tag_key = "environment"
tag_value = "pre"
}
# OPCIÓN 2: Todos los días (incluidos fines de semana)
# Descomenta si prefieres que se ejecute también sábados y domingos
/*
sch_vm_start_daily_0800 = {
name = "sch-vm-start-daily-0800"
resource_group_name = "rg-automation-lab"
automation_account_name = "aa-vm-automation"
frequency = "Day"
interval = 1
vm_start_schedule_start_time = "2026-01-23T08:00:00+01:00"
vm_start_schedule_description = "Arranque automático VMs pre (diario)"
vm_start_schedule_timezone = "Europe/Madrid"
runbook_name = "rb_vm_start"
tag_key = "environment"
tag_value = "pre"
}
sch_vm_stop_daily_1700 = {
name = "sch-vm-stop-daily-1700"
resource_group_name = "rg-automation-lab"
automation_account_name = "aa-vm-automation"
frequency = "Day"
interval = 1
vm_start_schedule_start_time = "2026-01-23T17:00:00+01:00"
vm_start_schedule_description = "Parada automática VMs pre (diario)"
vm_start_schedule_timezone = "Europe/Madrid"
runbook_name = "rb_vm_stop"
tag_key = "environment"
tag_value = "pre"
}
*/
}
# Action Group para notificaciones
monitor_action_group = {
ag_lab_email = {
name = "ag-vm-automation-email"
resource_group_name = "rg-automation-lab"
short_name = "vmautoemail"
email_receiver_name = "ops-team"
email_receiver_email_address = "tu-email@dominio.com"
automation_account_name = "aa-vm-automation"
}
}
Paso 3: Configurar Role Assignments
Importante: Los permisos deben configurarse con los IDs reales de tus recursos:
role_assignments = {
# Permiso para gestionar VMs
ra_vm_contributor = {
scope = "/subscriptions/{sub-id}/resourceGroups/{rg-vms}"
role_definition_name = "Virtual Machine Contributor"
automation_account_name = "aa-vm-automation"
}
# Permiso para monitorización
ra_monitoring = {
scope = "/subscriptions/{sub-id}/resourceGroups/rg-automation-lab"
role_definition_name = "Monitoring Contributor"
automation_account_name = "aa-vm-automation"
}
# Permiso sobre Action Group
ra_action_group = {
scope = "/subscriptions/{sub-id}/resourceGroups/rg-automation-lab/providers/Microsoft.Insights/actionGroups/ag-vm-automation-email"
role_definition_name = "Contributor"
automation_account_name = "aa-vm-automation"
}
# Permiso para leer Log Analytics
ra_law_reader = {
scope = "/subscriptions/{sub-id}/resourceGroups/rg-automation-lab/providers/Microsoft.OperationalInsights/workspaces/law-automation"
role_definition_name = "Log Analytics Reader"
automation_account_name = "aa-vm-automation"
}
}
Paso 4: Desplegar con Terraform
# Inicializar Terraform
terraform init
# Validar configuración
terraform validate
# Ver plan de ejecución
terraform plan
# Aplicar cambios
terraform apply
Paso 5: Configurar Variable en Automation Account
Después del despliegue, configura manualmente la variable ACTION_GROUP_ID:
# Obtener el ID del Action Group
ACTION_GROUP_ID=$(az monitor action-group show \
--name ag-vm-automation-email \
--resource-group rg-automation-lab \
--query id -o tsv)
# Crear la variable en el Automation Account
az automation variable create \
--automation-account-name aa-vm-automation \
--resource-group rg-automation-lab \
--name ACTION_GROUP_ID \
--value "$ACTION_GROUP_ID"
O manualmente desde el portal de Azure:
- Navega al Automation Account
- Ve a Variables en el menú lateral
- Crea variable
ACTION_GROUP_IDcon el resource ID del Action Group
Paso 6: Etiquetar VMs
Para que una VM sea gestionada por la automatización, añádele el tag apropiado:
# Mediante Azure CLI
az vm update \
--resource-group rg-vms-prod \
--name vm-web-app-01 \
--set tags.environment=pre
# Mediante Terraform
resource "azurerm_linux_virtual_machine" "web_app" {
name = "vm-web-app-01"
resource_group_name = "rg-vms-prod"
location = "spaincentral"
# ... otras configuraciones ...
tags = {
environment = "pre"
managed_by = "automation"
}
}
Detalles Técnicos de los Runbooks
Script vm-start.ps1
El runbook de inicio implementa una lógica robusta:
param(
[string]$tagkey = "environment",
[string]$tagvalue = "pre",
[string]$subscriptionid = "",
[int]$waitseconds = 60
)
# 1. Autenticación con Managed Identity
Connect-AzAccount -Identity | Out-Null
# 2. Cambiar contexto si se especifica suscripción
if ($subscriptionid -and $subscriptionid.Trim().Length -gt 0) {
Set-AzContext -Subscription $subscriptionid | Out-Null
}
# 3. Obtener VMs con el tag especificado
$vms = Get-AzVM -Status
$targets = $vms | Where-Object {
$_.Tags.ContainsKey($tagkey) -and $_.Tags[$tagkey] -eq $tagvalue
}
# 4. Cargar Action Group ID
$actionGroupId = Get-AutomationVariable -Name "ACTION_GROUP_ID"
if (-not $actionGroupId) {
throw "No existe la variable ACTION_GROUP_ID"
}
# 5. Si no hay VMs, notificar y salir
if (-not $targets -or $targets.Count -eq 0) {
Send-ActionGroupMail `
"AVISO: no hay VMs con tag ($tagkey=$tagvalue)" `
"El runbook se ejecutó pero no encontró VMs."
return
}
# 6. Intentar iniciar cada VM
$startErrors = @()
foreach ($vm in $targets) {
$powerState = ($vm.Statuses |
Where-Object { $_.Code -like "PowerState/*" } |
Select-Object -First 1).DisplayStatus
if ($powerState -eq "VM running") {
Write-Output " - VM $($vm.Name) ya está arrancada. Skip."
continue
}
try {
Start-AzVM -ResourceGroupName $vm.ResourceGroupName `
-Name $vm.Name `
-ErrorAction Stop | Out-Null
Write-Output " - VM $($vm.Name) iniciada correctamente"
}
catch {
$startErrors += [PSCustomObject]@{
name = $vm.Name
error = $_.Exception.Message
}
}
}
# 7. Esperar y re-verificar estados
Start-Sleep -Seconds $waitseconds
$after = Get-AzVM -Status | Where-Object {
$_.Tags.ContainsKey($tagkey) -and $_.Tags[$tagkey] -eq $tagvalue
}
# 8. Clasificar resultados
$running = @()
$notRunning = @()
foreach ($vm in $after) {
$ps = ($vm.Statuses |
Where-Object { $_.Code -like "PowerState/*" }).DisplayStatus
if ($ps -eq "VM running") {
$running += $vm.Name
} else {
$notRunning += "$($vm.Name) [$ps]"
}
}
# 9. Enviar resumen por email
$subject = "Resultado start VMs por tag ($tagkey=$tagvalue)"
$desc = @"
Resumen:
- Running: $($running -join ", ")
- No running: $($notRunning -join ", ")
- Errores: $($startErrors | ForEach-Object { "$($_.name): $($_.error)" } -join " | ")
"@
Send-ActionGroupMail $subject $desc
Características Destacadas
- Manejo de errores robusto: Captura y reporta errores individuales sin detener el proceso
- Verificación post-inicio: Espera 60 segundos y verifica que las VMs realmente se iniciaron
- Notificaciones detalladas: Informa sobre éxitos, fallos y VMs que ya estaban encendidas
- Parámetros configurables: Permite personalizar tags, suscripción y tiempo de espera
- Idempotencia: Detecta VMs ya encendidas y las omite
Ejemplo de Notificación por Email
Cuando el runbook se ejecuta, recibes un email como este:
De: Azure Action Group <no-reply@microsoft.com>
Para: ops-team@tudominio.com
Asunto: Resultado start VMs por tag (environment=pre)
Resumen de ejecución del runbook vm-start.ps1:
✅ VMs en estado Running:
• vm-web-app-01
• vm-api-backend-01
• vm-database-01
⚠️ VMs que no se iniciaron:
• vm-test-server-01 [VM deallocated]
❌ Errores encontrados:
(ninguno)
Timestamp: 2026-01-22 08:01:23 UTC
Automation Account: aa-vm-automation
Runbook Job ID: 3f7a9c2b-4d8e-4f1a-9b6c-2e5d8f3a7c1b
Monitorización con Log Analytics
Todos los logs se centralizan en Log Analytics Workspace. Puedes ejecutar queries KQL para analizar:
Consultar ejecuciones recientes
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.AUTOMATION"
| where Category == "JobLogs"
| where RunbookName_s in ("rb_vm_start", "rb_vm_stop")
| project TimeGenerated, RunbookName_s, ResultType, JobId_g
| order by TimeGenerated desc
| take 50
Detectar fallos en runbooks
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.AUTOMATION"
| where Category == "JobStreams"
| where ResultType == "Failed"
| project TimeGenerated, RunbookName_s, StreamType_s, ResultDescription
| order by TimeGenerated desc
Analizar tiempo de ejecución
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.AUTOMATION"
| where Category == "JobLogs"
| where ResultType == "Completed"
| extend Duration = datetime_diff('second', EndTime_t, CreationTime_t)
| summarize AvgDuration=avg(Duration), MaxDuration=max(Duration)
by RunbookName_s
Alertas Automáticas
La solución incluye una alerta programada que se dispara cada vez que finaliza un job:
monitor_scheduled_query_rules_alert_v2 = {
alert_automation_job_finished = {
name = "automation-runbook-finished"
severity = 4 # Informational
evaluation_frequency = "PT5M" # Cada 5 minutos
window_duration = "PT5M"
time_aggregation_method = "Count"
operator = "GreaterThan"
threshold = 0
# Query KQL
query = <<-QUERY
AzureDiagnostics
| where ResourceProvider == "MICROSOFT.AUTOMATION"
| where Category == "JobLogs"
| where TimeGenerated > ago(5m)
| where ResultType in ("Completed", "Failed", "Stopped")
QUERY
}
}
Seguridad y Mejores Prácticas
1. Principio de Mínimo Privilegio
Los role assignments están limitados al ámbito mínimo necesario:
| Rol | Ámbito | Justificación |
|---|---|---|
| Virtual Machine Contributor | RG de VMs | Solo para start/stop, no para crear/eliminar |
| Monitoring Contributor | RG del Automation | Solo para escribir logs |
| Contributor | Action Group específico | Solo para enviar notificaciones |
| Log Analytics Reader | Workspace específico | Solo lectura de logs |
2. Conectividad Privada
- Private Endpoint: El Automation Account no es accesible desde internet
- Private DNS Zone: Resolución DNS privada para
privatelink.azure-automation.net - Network Security: Todo el tráfico permanece en la red virtual
3. Identidad Administrada
- Sin credenciales almacenadas
- Rotación automática de tokens
- Auditoría completa en Azure AD
4. Auditoría y Compliance
- Todos los logs centralizados en Log Analytics
- Retención configurable (30 días por defecto)
- Trazabilidad completa de cada operación
Costos de la Solución
Desglose de costos mensuales estimados (región Spain Central):
| Componente | Costo Mensual |
|---|---|
| Automation Account (Basic) | Gratis |
| Runbook execution (2 jobs/día × 2 min × 30 días) | ~€0.24 |
| Log Analytics (1 GB/mes ingerido) | ~€2.76 |
| Action Group (60 emails/mes) | Gratis (primeros 1,000) |
| Private Endpoint | ~€6.57 |
| Virtual Network | Gratis |
| TOTAL | ~€9.57/mes |
ROI: ¿Cuánto ahorras?
Si tienes 10 VMs B2s (€30/mes cada una) encendidas 24/7 pero solo las necesitas 8h/día:
- Costo actual: 10 VMs × €30 = €300/mes
- Costo optimizado: 10 VMs × €30 × (8h/24h) = €100/mes
- Ahorro mensual: €200
- Costo de la solución: €9.57
- ROI: Recuperas la inversión en 2 días ✅
Casos de Uso Reales
1. Entorno de Desarrollo - Solo Días Laborables (SIN fines de semana)
Escenario: VMs de desarrollo que solo se necesitan de lunes a viernes, horario laboral 08:00-18:00.
# VMs de desarrollo: encendidas 08:00-18:00 (lunes a viernes)
# Las VMs permanecen APAGADAS todo el fin de semana
automation_schedule = {
dev_start_weekdays = {
name = "dev-start-weekdays-0800"
resource_group_name = "rg-automation-lab"
automation_account_name = "aa-vm-automation"
frequency = "Week"
interval = 1
week_days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
vm_start_schedule_start_time = "2026-01-27T08:00:00+01:00" # Lunes
vm_start_schedule_timezone = "Europe/Madrid"
runbook_name = "rb_vm_start"
tag_key = "environment"
tag_value = "dev"
}
dev_stop_weekdays = {
name = "dev-stop-weekdays-1800"
resource_group_name = "rg-automation-lab"
automation_account_name = "aa-vm-automation"
frequency = "Week"
interval = 1
week_days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
vm_start_schedule_start_time = "2026-01-27T18:00:00+01:00"
vm_start_schedule_timezone = "Europe/Madrid"
runbook_name = "rb_vm_stop"
tag_key = "environment"
tag_value = "dev"
}
}
💰 Ahorro: Si tienes 10 VMs B2s (€30/mes cada una):
- Sin automatización: 10 VMs × €30 × 24h/día × 7 días = €300/mes
- Con automatización (10h/día × 5 días/semana): 10 VMs × €30 × (50h/168h) = €89/mes
- Ahorro mensual: €211 (70% de reducción)
2. Entorno de Testing - Todos los Días (incluye fines de semana)
Escenario: VMs de testing que necesitan estar disponibles 7 días a la semana para pruebas automatizadas.
# VMs de testing: solo durante horario extendido, todos los días
automation_schedule = {
test_start_daily = {
name = "test-start-daily-0700"
resource_group_name = "rg-automation-lab"
automation_account_name = "aa-vm-automation"
frequency = "Day"
interval = 1
vm_start_schedule_start_time = "2026-01-23T07:00:00+01:00"
vm_start_schedule_timezone = "Europe/Madrid"
runbook_name = "rb_vm_start"
tag_key = "environment"
tag_value = "test"
}
test_stop_daily = {
name = "test-stop-daily-2000"
resource_group_name = "rg-automation-lab"
automation_account_name = "aa-vm-automation"
frequency = "Day"
interval = 1
vm_start_schedule_start_time = "2026-01-23T20:00:00+01:00"
vm_start_schedule_timezone = "Europe/Madrid"
runbook_name = "rb_vm_stop"
tag_key = "environment"
tag_value = "test"
}
}
💰 Ahorro: 13 horas/día en lugar de 24 horas = 46% de reducción de costos
3. Preproducción - Solo Durante la Semana Laboral
Escenario: Entorno de preproducción que solo necesita estar activo cuando el equipo está trabajando.
# VMs de pre: Lunes inicio 07:00, Viernes parada 19:00
# Permanecen apagadas desde viernes noche hasta lunes mañana
automation_schedule = {
pre_start_monday = {
name = "pre-start-monday-0700"
resource_group_name = "rg-automation-lab"
automation_account_name = "aa-vm-automation"
frequency = "Week"
interval = 1
week_days = ["Monday"]
vm_start_schedule_start_time = "2026-01-27T07:00:00+01:00"
vm_start_schedule_timezone = "Europe/Madrid"
runbook_name = "rb_vm_start"
tag_key = "environment"
tag_value = "pre"
}
pre_stop_friday = {
name = "pre-stop-friday-1900"
resource_group_name = "rg-automation-lab"
automation_account_name = "aa-vm-automation"
frequency = "Week"
interval = 1
week_days = ["Friday"]
vm_start_schedule_start_time = "2026-01-24T19:00:00+01:00"
vm_start_schedule_timezone = "Europe/Madrid"
runbook_name = "rb_vm_stop"
tag_key = "environment"
tag_value = "pre"
}
}
💡 Nota: Las VMs se encienden solo el lunes y se apagan el viernes, permaneciendo apagadas desde viernes 19:00 hasta lunes 07:00 (60 horas apagadas cada semana).
4. VMs por Proyecto con Horario Personalizado
Escenario: Proyecto con equipo distribuido que trabaja en diferentes zonas horarias.
# VMs de proyecto específico con horario extendido
automation_schedule = {
project_alpha_start = {
name = "project-alpha-start-0600"
resource_group_name = "rg-automation-lab"
automation_account_name = "aa-vm-automation"
frequency = "Week"
interval = 1
week_days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
vm_start_schedule_start_time = "2026-01-27T06:00:00+01:00"
vm_start_schedule_timezone = "Europe/Madrid"
runbook_name = "rb_vm_start"
tag_key = "project"
tag_value = "alpha"
}
project_alpha_stop = {
name = "project-alpha-stop-2200"
resource_group_name = "rg-automation-lab"
automation_account_name = "aa-vm-automation"
frequency = "Week"
interval = 1
week_days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
vm_start_schedule_start_time = "2026-01-27T22:00:00+01:00"
vm_start_schedule_timezone = "Europe/Madrid"
runbook_name = "rb_vm_stop"
tag_key = "project"
tag_value = "alpha"
}
}
5. Configuración Avanzada - Días Específicos
Escenario: VMs que solo se usan ciertos días de la semana (por ejemplo, martes y jueves para demos).
# VMs solo para demos: martes y jueves
automation_schedule = {
demo_start_tue_thu = {
name = "demo-start-tue-thu-0900"
resource_group_name = "rg-automation-lab"
automation_account_name = "aa-vm-automation"
frequency = "Week"
interval = 1
week_days = ["Tuesday", "Thursday"]
vm_start_schedule_start_time = "2026-01-27T09:00:00+01:00" # Martes
vm_start_schedule_timezone = "Europe/Madrid"
runbook_name = "rb_vm_start"
tag_key = "purpose"
tag_value = "demo"
}
demo_stop_tue_thu = {
name = "demo-stop-tue-thu-1500"
resource_group_name = "rg-automation-lab"
automation_account_name = "aa-vm-automation"
frequency = "Week"
interval = 1
week_days = ["Tuesday", "Thursday"]
vm_start_schedule_start_time = "2026-01-27T15:00:00+01:00"
vm_start_schedule_timezone = "Europe/Madrid"
runbook_name = "rb_vm_stop"
tag_key = "purpose"
tag_value = "demo"
}
}
💰 Ahorro: Solo 12 horas/semana en lugar de 168 horas = 93% de reducción de costos
Comparativa de Configuraciones
| Configuración | Horas/Semana Encendida | % del Tiempo | Ahorro vs 24/7 |
|---|---|---|---|
| 24/7 (sin automatización) | 168 horas | 100% | 0% |
| Lunes-Viernes, 8h/día | 40 horas | 24% | 76% |
| Lunes-Viernes, 10h/día | 50 horas | 30% | 70% |
| Lunes-Viernes, 12h/día | 60 horas | 36% | 64% |
| Todos los días, 12h/día | 84 horas | 50% | 50% |
| Lunes inicio - Viernes parada | 120 horas | 71% | 29% |
| Martes y Jueves, 6h/día | 12 horas | 7% | 93% |
Troubleshooting Común
Problema: El runbook no se ejecuta
Solución:
- Verifica que el schedule tenga una fecha de inicio futura
- Confirma que el runbook esté publicado (estado “Published”)
- Revisa los jobs en el Automation Account
# Listar jobs recientes
az automation job list \
--automation-account-name aa-vm-automation \
--resource-group rg-automation-lab
Problema: Las VMs no cambian de estado
Solución:
- Verifica los permisos de la identidad administrada
- Confirma que las VMs tengan el tag correcto
- Revisa los logs del runbook
# Ver output del último job
az automation job get-output \
--automation-account-name aa-vm-automation \
--resource-group rg-automation-lab \
--name <job-id>
Problema: No llegan emails
Solución:
- Verifica que la variable
ACTION_GROUP_IDesté configurada - Confirma que el email sea correcto en el Action Group
- Revisa spam/correo no deseado
- Verifica permisos sobre el Action Group
# Verificar Action Group
az monitor action-group show \
--name ag-vm-automation-email \
--resource-group rg-automation-lab
Mejoras Futuras y Roadmap
Estoy trabajando en las siguientes mejoras para esta solución:
- Multi-suscripción: Gestionar VMs en múltiples suscripciones
- Retry logic: Reintentar operaciones fallidas automáticamente
- Dashboard personalizado: Visualizar métricas en Azure Dashboard
- Notificaciones a Teams: Integración con Microsoft Teams
- Start/stop bajo demanda: Webhook para control manual
- Machine Learning: Predicción de uso para optimizar horarios
- Cost Management: Integración con Azure Cost Management API
Conclusión
Esta solución de automatización de VMs en Azure demuestra el poder de Infrastructure as Code combinado con las capacidades de automatización nativas de Azure. Con una inversión mínima (menos de €10/mes), puedes ahorrar cientos de euros al mes optimizando el uso de tus recursos.
Beneficios clave:
- ✅ Reducción de costos de hasta 70% en entornos no productivos
- ✅ Infraestructura 100% como código (reproducible y versionada)
- ✅ Notificaciones automáticas para mantener visibilidad
- ✅ Seguridad mejorada con Private Endpoints e Identidad Administrada
- ✅ Monitorización completa con Log Analytics
- ✅ Fácil de extender y personalizar
¿Quieres implementarlo?
El código completo está disponible en mi repositorio de GitHub:
🔗 github.com/luisadanmunoz/start-stop-VM-alarm
Si tienes preguntas o sugerencias, no dudes en abrir un issue en el repositorio o contactarme directamente.
Recursos Adicionales
- Documentación Azure Automation
- Terraform AzureRM Provider
- Azure Monitor Action Groups
- PowerShell Az Module
Tags: #Azure #Terraform #InfrastructureAsCode #Automation #DevOps #CostOptimization #CloudComputing #PowerShell
¿Te ha resultado útil este artículo? Compártelo en tus redes sociales y ayuda a otros a optimizar sus costos en Azure.