Elastic Beanstalk ワーカー環境の定期的なタスクとFIFOキューを併用する


概要

If you configure your worker environment with an existing SQS queue and choose an Amazon SQS FIFO queue, periodic tasks aren't supported.

FIFOキューと定期的なタスクが併用できない旨が記載されています。

対策方針

オートスケールによって台数が増減しても、ワーカー環境の定期的なタスク は重複実行されないようになっています。
その仕組みとしては、DynamoDBの AWSEBWorkerCronLeaderRegistry に書き込みできたインスタンスをリーダー(Leader)として、そのリーダーインスタンスのみで実行することでそれを実現しています。

そのリーダーの選出を流用し、ワーカー環境のEC2でcron実行をさせることで表題の併用を実現してみます。

検証環境

Ruby 2.6 running on 64bit Amazon Linux 2/3.1.1

Amazon Linux 2 なので注意してください。

ステップ : ダミーのcron.yamlを作成し、 AWSEBWorkerCronLeaderRegistry を書き込みさせる

cron.yaml
version: 1
cron: # UTC
  - name: "dummy-job"  # 何でもよい
    url: "/health"  # 何でもよい
    schedule: "7 7 7 7 7" # 何でもよい

ステップ : 自身がLeaderか判定するスクリプトを用意

bin/eb_is_worker_leader
#!/usr/bin/env bash

# EC2でないときはexit
if [[ ! -f /var/lib/cloud/data/instance-id ]]; then
  exit
fi
instance_id=$(cat /var/lib/cloud/data/instance-id)

# AWSEBWorkerCronLeaderRegistry のテーブル名を取得
table_name=$(awk -F': ' '$1=="registry_table" {printf $2}' /etc/aws-sqsd.d/default.yaml)
# 定期的に更新されている、leader_idを取得する (ex: i-XXXXX.${registration-record.worker_id})
leader_id=$(aws dynamodb get-item --region ${AWS_REGION} --table-name ${table_name} --key '{"id": {"S": "leader-election-record"} }' | jq -r .Item.leader_id.S)

echo ${leader_id} | grep -q ${instance_id}
exit $?

ステップ : cronをセット

ステップ2で作成した bin/eb_is_worker_leader が成功したら、処理を実行するようにcronをセットします。
例えば、Rubyでcronをセットする whenever を使う場合、下記のようになります。

schedule.rb
job_type :leader_runner, "cd :path && bin/eb_is_worker_leader && bin/rails runner -e :environment ':task' :output"

every :hour do
  leader_runner "SomeModel.ladeeda"
end

なお、whenever で用意されている runner は下記のとおりです。
比べると bin/eb_is_worker_leader が追加されているだけなのがわかるかと思います。

job_type :runner,  "cd :path && bin/rails runner -e :environment ':task' :output"


おまけ: wheneverでcronを更新する
.platform/hooks/postdeploy/XX_update_cron.sh
#!/usr/bin/env bash

# Workerでなければ何もしない
env_name=$(jq -r .Name /opt/elasticbeanstalk/config/ebenvinfo/envtier.json)
if [[ ! ${env_name} = 'Worker' ]]; then
  exit
fi

/opt/elasticbeanstalk/.rbenv/shims/bundle exec whenever --user webapp --update-crontab


まとめ

これらによって、すべてのワーカーインスタンスでcronが実行されます。
cronではリーダーのみ処理が継続されるため、定期的なタスクの重複した実行が防げます。

似たような解決策

どちらも Amazon Linux 1 が対象です

これらはインスタンス数でLeaderを判断しています。
この記事のやり方では、 AWSEBWorkerCronLeaderRegistry を流用することで、シンプルな実装になるかなと思っています。