Flujo de automatización para ejecutar scripts en instancias EC2 con SSM y Terraform
- hace 4 días
- 6 Min. de lectura

1. Introducción
Este documento describe cómo utilizar el módulo Terraform de SSM Cronjobs para definir, desplegar y operar tareas operativas programadas en hosts EC2/ECS usando:
SSM Documents (para ejecutar scripts de shell).
SSM Maintenance Windows (para scheduling y orquestación).
Selección de instancias/recursos mediante tags.
Este módulo está diseñado para:
Jobs operativos (migraciones, mantenimiento, limpiezas).
Jobs que deben ejecutarse dentro de hosts o contenedores existentes.
Jobs que deben ejecutarse en un orden específico.
Jobs que deben ser auditables, observables y controlados.
2. Problema que resuelve
Antes de este módulo:
Scripts de shell ad-hoc.
SSH manual o cron.
Sin orden entre pasos.
Sin visibilidad en AWS.
Sin scheduling centralizado.
Con este módulo:
Todos los jobs están:
Definidos en Terraform.
Versionados.
Programados en AWS.
Logueados en CloudWatch.
Controlados con concurrencia, umbrales de error y cutoffs.
3. Arquitectura de alto nivel
Cada "cronjob" programado consiste en:
Uno o más SSM Documents (scripts de shell).
Una SSM Maintenance Window (el scheduler).
Una o más tareas de Maintenance Window.
Selección de targets mediante tags.
CloudWatch Logs.
Flujo de ejecución:
Se dispara la Maintenance Window.
AWS selecciona instancias que coinciden con los tags.
Las tareas se ejecutan en orden de prioridad.
Cada tarea ejecuta un SSM Document.
La salida se envía a CloudWatch Logs.
4. Conceptos clave
4.1 SSM Document
Un SSM Document es básicamente un script de shell versionado almacenado en AWS.
En nuestro módulo se crea así:
module "doc_python_migrate" { source = "...//ssm/ssm_cronjob" name = "Doc-Run-Python-Migrate" commands = [ "#!/bin/bash", "echo hello" ] } |
Esto genera:
Un SSM Document.
Con hash y versionado.
Y outputs:
document_name.
document_arn.
document_hash.
4.2 Maintenance Window (The Scheduler)
El scheduler real (tipo cron) es una SSM Maintenance Window.
Define:
Cuándo se ejecuta.
En qué zona horaria.
Por cuánto tiempo.
Cuántos targets concurrentes.
Cuántos errores están permitidos.
4.3 Tasks (pasos)
Cada Maintenance Window puede tener múltiples tareas.
Cada tarea:
Referencia un SSM Document.
Tiene una prioridad.
Se ejecuta en orden.
Esto permite patrones como:
makemigrations.
migrate.
cleanup.
restart services.
5. Patrón de uso real
5.1 Paso 1 — Crear Documents
Ejemplo: makemigrations
module "doc_python_makemigrations" { source = "...//ssm/ssm_cronjob" name = "Doc-Run-Python-MakeMigrations" commands = [ "#!/bin/bash", "echo Starting...", "docker exec ... python manage.py makemigrations" ] } |
Lo mismo para migrate.
5.2 Paso 2 — Crear la Maintenance Window programada
module "boxer_pricing_updates_mw" { source = "...//ssm/ssm_cronjob" mw_name = "testing-example-mw-django-apply-migrations" mw_schedule = "cron(0 /4 ? )" mw_timezone = "America/Buenos_Aires" mw_duration = 1 mw_cutoff = 0 mw_max_concurrency = "2" mw_max_errors = "1" log_retention = "7" enabled = true target_tag_key = "Cluster_name" target_tag_value = "testing-example-cluster" ecs_task_name = "ecs-test-" tasks = [ { name = "python-makemigrations" document = module.doc_python_makemigrations.document_name arn = module.doc_python_makemigrations.document_arn hash_type = module.doc_python_makemigrations.document_hash_type hash = module.doc_python_makemigrations.document_hash priority = 0 }, { name = "python-migrate" document = module.doc_python_migrate.document_name arn = module.doc_python_migrate.document_arn hash_type = module.doc_python_migrate.document_hash_type hash = module.doc_python_migrate.document_hash priority = 1 } ] } |
6. Scheduling
Usa EventBridge-style cron:
cron(0 /4 ? ) # every 4 hours |
Zona horaria configurable:
mw_timezone = "America/Buenos_Aires" |
7. Estrategia de targeting
Las instancias se seleccionan por tags:
target_tag_key = "Cluster_name" target_tag_value = "testing-example-cluster" |
Toda instancia con ese tag recibirá el comando.
8. Modelo de ejecución
Las tareas se ejecutan en orden ascendente de prioridad
Cada tarea se ejecuta en todas las instancias que coinciden
Controlado por:
mw_max_concurrency = "2" mw_max_errors = "1" |
Si se supera el umbral de error → la ejecución se detiene.
9. Logging y observabilidad
La salida se envía a CloudWatch Logs.
Retención controlada por:
log_retention = "7" |
Podés:
Auditar cada ejecución
Ver output por instancia
Debuggear errores fácilmente
10. Habilitar / deshabilitar
target_tag_key = "Cluster_name" target_tag_value = "testing-example-cluster" |
Deshabilitar mantiene la infraestructura pero detiene el scheduling.
11. . Casos de uso comunes
Migraciones en Django / Rails.
Jobs de limpieza.
Rotación de certificados.
Cache warming.
Operaciones sobre flotas con un click.
Ejemplo completo con Terraform
Prerrequisitos
Terraform ≥ 1.3
AWS Provider ≥ v5.x
Para utilizar esta solución, podes aplicar toda esta configuración utilizando el siguiente código de ejemplo:
locals { ecs_task_name = "ecs-test-" environment = "testing" project = "example" } module "doc_python_makemigrations" { source = "git@github.com:teracloud-io/terraform_modules.git//ssm/ssm_document?ref=feature/ssm_cronjobs"
name = "Doc-Run-Python-MakeMigrations" commands = [ "#!/bin/bash", "echo \"Starting makemigrations run on $(hostname)\"", "FAILED=0", "CONTAINER_IDS=$(docker ps --filter \"name=${local.ecs_task_name}\" --quiet || true)", "if [ -z \"$CONTAINER_IDS\" ]; then", " echo \"No matching containers found\"", " exit 0", "fi", "echo \"Found container IDs: $CONTAINER_IDS\"", "for cid in $CONTAINER_IDS; do", " cname=$(docker inspect --format '{{.Name}}' \"$cid\" | sed 's#^/##')", " echo \"---Executing inside container: $cname ----\"", " docker exec \"$cid\" bash -lc 'cd /app/src && python3 manage.py makemigrations'", " RC=$?", " if [ $RC -eq 0 ]; then", " echo \"OK\"", " else", " echo \"FAILED\"", " FAILED=1", " fi", " echo \"Command exit code for $cname: $RC\"", "done", "echo \"Completed makemigrations run on $(hostname)\"", "exit $FAILED" ] } module "doc_python_migrate" { source = "git@github.com:teracloud-io/terraform_modules.git//ssm/ssm_document" name = "Doc-Run-Python-Migrate" commands = [ "#!/bin/bash", "echo \"Starting migrate run on $(hostname)\"", "FAILED=0", "CONTAINER_IDS=$(docker ps --filter \"name=${local.ecs_task_name}\" --quiet || true)", "if [ -z \"$CONTAINER_IDS\" ]; then", " echo \"No matching containers found\"", " exit 0", "fi", "echo \"Found container IDs: $CONTAINER_IDS\"", "for cid in $CONTAINER_IDS; do", " cname=$(docker inspect --format '{{.Name}}' \"$cid\" | sed 's#^/##')", " echo \"---Executing inside container: $cname ----\"", " docker exec \"$cid\" bash -lc 'cd /app && python3 manage.py migrate'", " RC=$?", " if [ $RC -eq 0 ]; then", " echo \"OK\"", " else", " echo \"FAILED\"", " FAILED=1", " fi", " echo \"Command exit code for $cname: $RC\"", "done", "echo \"Completed migrate run on $(hostname)\"", "exit $FAILED" ] } module "example_maintenance_window" { source = "git@github.com:teracloud-io/terraform_modules.git//ssm/ssm_cronjob" mw_name = "${local.environment}-${local.project}-mw-django-apply-migrations" mw_description = "Maintenance window to apply missing migrations to Django app" mw_schedule = "cron(0 /4 ? )" mw_timezone = "America/Buenos_Aires" mw_duration = 1 mw_cutoff = 0 mw_max_concurrency = "2" mw_max_errors = "1" log_retention = "7" ecs_task_name = local.ecs_task_name enabled = true target_tag_key = "Cluster_name" target_tag_value = "${local.environment}-${local.project}-cluster" tasks = [ { name = "python-makemigrations" document = module.doc_python_makemigrations.document_name arn = module.doc_python_makemigrations.document_arn hash_type = module.doc_python_makemigrations.document_hash_type hash = module.doc_python_makemigrations.document_hash priority = 0 }, { name = "python-migrate" document = module.doc_python_migrate.document_name arn = module.doc_python_migrate.document_arn hash_type = module.doc_python_migrate.document_hash_type hash = module.doc_python_migrate.document_hash priority = 1 # Runs AFTER makemigrations } ] } |
También podés ajustar el parámetro commands para implementar un script en bash que se adapte a tus necesidades, o simplemente modificar el string entre ‘’ en la línea de docker exec.
Este código creará los siguientes recursos en AWS:






Este código asume que ya tenés instancias EC2 en ejecución con ECS o contenedores Docker corriendo dentro. Estas instancias deben tener tags alineados con lo que está configurado en el código de Terraform en esta sección.
module "example_maintenance_window" { ... target_tag_key = "Cluster_name" target_tag_value = "${local.environment}-${local.project}-cluster" ... |
Cuando se alcanza el horario de ejecución definido en la Maintenance Window, se ejecutarán todos los documents en el orden establecido en el código según la prioridad asignada.
Una vez ejecutado, podemos revisar el historial de ejecuciones en la pestaña “History” de la Maintenance Window.

Allí podemos ver los detalles de cada ejecución y revisar los logs asociados.



Y, por ejemplo, si ocurre un error, se muestra un mensaje como este y se detiene la ejecución:


¡Eso es todo! Ahora automatizaste la creación y eliminación de un workflow completamente funcional para ejecutar tareas o scripts en múltiples instancias que alojan múltiples contenedores, con un schedule definido y controles detallados de umbrales tanto para errores como para ejecuciones exitosas.

Joaquín San Román
Cloud Engineer



