ECSタスクスケジューラで同じタスクが複数回トリガーされる問題をどうにかして回避したい

## 概要
EC2サーバ上にcronで設定されたバッチ処理を、ECS Fargate上に移行するにあたりECSタスクスケジューラ利用したいと考えているが、タスクが複数トリガーされる可能性があるのとのことなのでその対策を考えてみた

- https://qiita.com/tonishy/items/a706b941a9046c71b9ea

## 実現したいこと
ECSタスクスケジュール（cloudwatch events）により起動するタスクの多重起動防止、および該当の処理が一回のみ行われることの保証

## ECSタスクスケジューラの複数回数トリガーについて

ECSタスクスケジューラではcloudwatch eventsを使用しており、公式ドキュメントにも複数回起動する可能性があることが記述されている。これにより同時に同じタスクが複数起動する可能性があるので、例えば一日に一回しか実行したくないバッチ処理等が実行されてしまうことがあり得るのでこれを防ぎたい

- https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/CWE_Troubleshooting.html#RuleTriggeredMoreThanOnc

> 1 つのイベントに応じてルールが複数回トリガーされました。CloudWatch イベント で、ルールのトリガーまたはターゲットへのイベントの提供で何が保証されますか。

> まれに、単一のイベントまたはスケジュールされた期間に対して同じルールを複数回トリガーしたり、特定のトリガーされたルールに対して同じターゲットを複数回起動したりする場合があります。

ECサーバ上では一時ファイル等を作成し、バッチ処理実行時にファイルの存在確認を行なうことで二重起動を防止していたが、そのままECSに移行すると、コンテナが複数起動される状態が発生するため、そのままでは多重起動の制御は不可能となる

## 方法
下記はlambdaの冪等性について言及している公式ドキュメントですが、DynamoDBへの値により処理の有無を切り替える仕組みを導入を検討してみた 

- https://aws.amazon.com/jp/premiumsupport/knowledge-center/lambda-function-idempotent/


## Dockerfileの作成

- コンテナからDynamoDB接続用のawsコマンドを実行するために、aws-cliをインストールしています。コンテナ起動時にスクリプトファイルを起動するよう設定

```Dockerfile:Dockerfile
FROM amazonlinux:2

# aws-cliのインストール
RUN yum -y install python-pip
RUN pip install awscli --upgrade

# 起動用shellスクリプトのコピー
COPY docker/aws-cli/shell/run.sh /run.sh
RUN chmod 755 /run.sh
# 起動用スクリプトを使用
CMD ["bash", "run.sh"]
```

- 下記Docker-composeファイルですが、ECS上で実行する際はタスク定義にaws-cliコマンド実行のために環境変数の値を設定します。秘匿情報が含まれるAWS_ACCESS_KEY_ID、AWS_SECRET_ACCESS_KEYはParameter StoreのSecure String等を使用してコンテナへ値を渡すよう注意が必要

```yml:docker-compose.yml
version: '3'

services:
  aws-cli:
    image: [awsアカウントID].dkr.ecr.[region].amazonaws.com/aws-cli:latest
    build:
      context: .
      dockerfile: docker/aws-cli/Dockerfile
    environment:
      AWS_ACCESS_KEY_ID: [値を設定]
      AWS_SECRET_ACCESS_KEY: [値を設定]
      AWS_DEFAULT_REGION: [region]
      AWS_DEFAULT_OUTPUT: json
```

## 起動用スクリプトを作成

- 今回はDynamoDBへの処理およびバッチのメイン処理を起動すスクリプトファイルをdocker起動時に読み込むようにしてみた。DynamoDBへは起動するバッチの名称と、データの保持期間であるttlを登録している。キーが存在しない場合は通常通りバッチ処理が行われ、存在する場合は処理を行わずコンテナを終了する

- 何らかの理由によりDynamoDBの値が削除されず、バッチ処理が再実行されないことを防ぐために、TTLを設定してDynamoDBからデータを削除するといったことも可能

- 以下はDynamoDB TTLの設定についてのドキュメント
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/time-to-live-ttl-how-to.html


```shell:run.sh
#!/bin/bash

# ロック実行
echo 'start' >> /dev/stdout
ttl=`date -d "1 hour" "+%s"`

# dynamodbにキーを登録
aws dynamodb put-item --table-name fargate_task_lock_control --item "{\"task-name\":{\"S\":\"task-test\"},\"ttl\":{\"N\":\"${ttl}\"}}" --expect "{\"task-name\":{\"Exists\": false}}" >> /dev/stdout　2>&1
if [ ! $? = 0 ]; then
  echo 'ロックファイルが存在またはエラーにより処理が終了しました' >> /dev/stdout
  exit 1
fi

# メインのバッチ処理
sleep 10

# ロック解除
aws dynamodb delete-item --table-name fargate_task_lock_control --key "{\"task-name\":{\"S\":\"task-test\"}}" >> /dev/stdout 2>&1
if [ ! $? = 0 ]; then
  echo 'ロックファイルの削除に失敗しました' >> /dev/stdout
  exit 1
fi
echo 'end' >> /dev/stdout
```


## 異なるタイミングでタスクが起動する場合に対応できていない
[CloudWatch Eventをトリガーにすると複数回実行されることがある](https://qiita.com/aosho235/items/87cc5f6104392f5fcdc6) の記事に以下のような記述があった
> DynamoDBを使ってロックを実装しても、1個目のタスクが1分で終了してから2個目のタスクが起動されたことがあった。これはロックでは防げない…

こちらについてはいい対応方法がないか別途考える必要あり


## その他
cloudwatch eventsが一定の期間内に複数回起動した際に通知を行う方法として以下のようなことも可能

- https://dev.classmethod.jp/articles/amazon-cloudwatch-events-for-cloudwatch-logs-insights/
- https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/CloudWatch-Events-tutorial-CloudWatch-Logs.html


## 参考
- https://qiita.com/tonishy/items/a706b941a9046c71b9ea
- https://tech.willgate.co.jp/entry/2020/02/10/120000
- https://tech.connehito.com/entry/2017/09/13/171914
- https://pages.awscloud.com/rs/112-TZM-766/images/G-5.pdf
- https://qiita.com/nii_yan/items/c45c542e0cfc9a5183b3
- https://qiita.com/naomichi-y/items/d933867127f27524686a
- https://ascii.jp/elem/000/001/593/1593234/3/