En nuestra migración de Docker Cloud a Elastic Beanstalk, nos encontramos con la necesidad de correr tareas de rake periódicamente en los ambientes. Amazon recomienda algunas formas de hacerlo, por ejemplo usando Lambda, .ebextensions o el worker tier.

La opción con Lambda la descartamos por complejidad y seguridad. Requiere subir el .pem a S3 y que la función lo baje, se conecte por SSH a la instancia y ejecute el comando.

La opción con el worker tier también fue descartada porque requería mantener un ambiente extra.

La opción con ebextensions tenía un pequeño problema: todas las instancias tienen el crontab, pero tenemos comandos que solo queremos correr en una instancia a la vez. Esto lo pudimos resolver con una pequeña modificación:

files:
    "/tmp/crontab":
        mode: "000644"
        owner: root
        group: root
        content: |
            * * * * * mycommand
container_commands:
  01_remove_old_crontab:
      command: "crontab -r || exit 0"
  02_install_crontab:
      command: "crontab /tmp/crontab"
      leader_only: true

Pero nos trajo otros problemas:

Si mejoramos cómo identificar al ‘líder’ de la versión estable, podemos poner el crontab en todas las instancias, solucionando ambos problemas. Para eso definimos al líder como la instancia más antigua y los comandos validan si son el líder antes de correr.

Nuestra validación entonces:

Nuestra ebextension finalmente quedó:

# .ebextensions/crontab.config
files:
  "/opt/elasticbeanstalk/bin/is_leader.sh":
    mode: "000755"
    owner: root
    group: root
    content: |
      #!/bin/bash

      # get all the instances of this environment sorted, the first in the list is the leader
      # exit 1 if this instance is the leader, -1 otherwise

      INSTANCE_ID=$(/opt/aws/bin/ec2-metadata -i | awk '{print $2}')
      REGION=$(/opt/aws/bin/ec2-metadata -z | awk '{print substr($2, 0, length($2)-1)}')
      STACK_TAG="aws:cloudformation:stack-name"

      STACK_NAME=$(aws ec2 describe-tags \
        --output text \
        --filters "Name=resource-id,Values=${INSTANCE_ID}" \
                  "Name=key,Values=${STACK_TAG}" \
        --region "${REGION}" \
        --query "Tags[*].Value")

      # now get the oldest instance of this stack
      LEADER=$(aws ec2 describe-instances \
        --output text \
        --filters "Name=tag:$STACK_TAG,Values=$STACK_NAME" \
        --region "${REGION}" \
        --query "Reservations[*].Instances[*].[LaunchTime,InstanceId]" | \
        sort | awk '{print $2}')

      if [ $INSTANCE_ID = $LEADER ]
      then
        exit 0
      else
        exit -1
      fi
  "/tmp/crontab":
    mode: "000644"
    owner: root
    group: root
    content: |
      * * * * * /opt/elasticbeanstalk/bin/is_leader.sh && mycommand
container_commands:
  01_remove_old_crontab:
      command: "crontab -r || exit 0"
  02_install_crontab:
      command: "crontab /tmp/crontab"