top of page

Flujo de automatización para ejecutar scripts en instancias EC2 con SSM y Terraform

  • hace 4 días
  • 6 Min. de lectura
Automation-workflow-to-execute-scripts-on-EC2-instances-with-SSM-and-Terraform



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:

  1. Se dispara la Maintenance Window.

  2. AWS selecciona instancias que coinciden con los tags.

  3. Las tareas se ejecutan en orden de prioridad.

  4. Cada tarea ejecuta un SSM Document.

  5. 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.



  1. 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:


resource-in-aws
resource-in-aws-2
resource-in-aws-3
resource-in-aws-4
resource-in-aws-5
resource-in-aws-6

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.


resource-in-aws-7

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


resource-in-aws-8
resource-in-aws-9
resource-in-aws-10

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


resource-in-aws-11
resource-in-aws-12

¡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.



joaquin-san-roman


Joaquín San Román

Cloud Engineer

 
 
bottom of page