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 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-name
de la instancia actual (durante los deploys las instancias nuevas tienen otro valor) - Trae los
LaunchTime
eInstanceId
de 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"