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: truePero nos trajo otros problemas:
- Si una instancia es reemplazada, la nueva no tiene el crontab
- En deploys inmutables tenemos dos líderes, con la posibilidad de ejecutar dos veces los comandos.
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:
- Busca el tag aws:cloudformation:stack-namede la instancia actual (durante los deploys las instancias nuevas tienen otro valor)
- Trae los LaunchTimeeInstanceIdde las instancias del stack
- Si la instancia actual es la más antigua, es el líder.
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" 
                 
            