AWS Step Function&ECSでスケジュールバッチ基盤を4ヶ月運用した振り返り


社内で、一部サービスをowned k8s基盤でECSへの移行を行いました。

その際、マイクロサービスのバッチ基盤を刷新し、4ヶ月運用した学びをまとめました。

# 要約

- フルマネージドで、実行履歴管理、再実行が整備され、運用が楽になった
- DataDogと連携することで、実行時間や全体の実行数管理など細かく可視化できた
- Cloud Watchのスケジュール実行は少し癖がある（記法、実行精度）
- 冪等性のあるバッチを心がける

# フルマネージドなStep FunctionsとECSのスケジュールバッチ基盤

下記を意識して設計しました。

- マネージドで運用コストを最小限
- 実行履歴や再実行性
- ジョブの可視化

## 全体構成図

![ecs_batch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525723/a9b599da-4f28-1d5c-326b-efa19d902bb4.png)


## 概要

定時になったらCloud Watch EventsがStep Functions経由で、ECSでコンテナが起動されます。ECS上で、専用のコンテナをpullして実行するので、デプロイや他のジョブの影響を受けません。

デプロイはCloud Formation、監視はDataDogで行います。

Step Functions を経由することで、実行履歴保持、再実行が容易になります。エラー発生時の再トライなども設定することが可能です。

[「20190731 Black Belt Amazon ECS Deep Dive」](https://www.slideshare.net/AmazonWebServicesJapan/20190731-black-belt-online-seminar-amazon-ecs-deep-dive-162160987)に、このアーキテクチャが載っており、参考にしました。


![スクリーンショット 2019-11-13 18.03.43.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525723/58543a73-cbb7-f59c-47b1-3d7081ae5356.png)

## Step Functionsを知らない方のために

lambdaやECSなどのサービス間での処理の動きを制御できるサービスです。

Step Functions単体で何かすることはできません。しかし、lambdaを実行した結果によって、SESからメールを送るといった、サービス間でのワークフローを定義することができます。

実行履歴も1000件まで保持でき、下記のようなダッシュボードが提供されています。

![スクリーンショット 2019-11-14 14.27.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525723/4c7e9dc5-1622-a692-0b82-9051242d5e86.png)

![スクリーンショット 2019-11-14 14.28.54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525723/cce5e647-bff5-c739-0a88-0e372c0115d9.png)


# AWS BatchやECSタスクスケジューラーを見送った理由

## AWS Batch

近いサービスとして、フルマネージドのバッチ基盤サービスAWS Batchがあります。

しかし単発実行を想定して設計されています。Black Beltの資料を読んだ結果、スケジュール実行はメインの用途ではないように感じました。そのため、予期せぬトラブルに会う可能性を考慮して、採用を見送りました。

[20190911 AWS Black Belt Online Seminar AWS Batch
](https://www.slideshare.net/AmazonWebServicesJapan/20190911-aws-black-belt-online-seminar-aws-batch?ref=https://aws.amazon.com/jp/blogs/news/webinar-bb-aws-batch-2019/)

## ECSタスクスケジューラー

また、ECSにはデフォルトで、簡易的なタスクスケジューラのしくみがあります。Cloud Watchで直接ECSのコンテナが起動される仕組みです。しかし、これは実行履歴や再実行の機能がありません。ログも乏しいため、他の部分での作り込みが必要で、運用面が辛いように思いました。

# Cloud Formationによるデプロイ
この構成だと、バッチ一つにつき、Step FunctionsとCloud Watchが1つずつ必要です。そのため全体のリソース数が多くなるので、Cloud Formationで管理しています。

Cloud Formationには、マクロという機能がありリソースの作成前に、任意の処理でテンプレートを変更できます。実態は、AWS Lambdaです。現状、マクロを使わずに、似たリソースを大量に作るのには難しいと思います。

コマンドが記載された下記のようなyamlのリストをもとに、一つのCloud FormationのCloud Watch, Step Functionsのyamlテンプレートを複製し量産します。

```
- Name: updatepvrank
 Decsription: pvランキング全部更新する
 Schedule: cron(50 0-14,17-23 * * ? *)
 Command: updatepvrank updateAll
```

このリストは、事前にs3にアップロードしておき、Cloud Formationsマクロ内で取得します。ファイルのサイズが大きく、パラメータストアや、Cloud Formation内に記載できなかったので、s3にあげるようにしました。

この処理は、gitlab-ci上から実行しています。開発者は、yamlのリストを修正し、CIのマニュアルジョブを実行することで、バッチの追加・削除ができます。

開発者が意識しない裏側の部分ですが、Cloud Formationマクロを使うことでなかなか複雑になってしまったので、課題を感じています。

# Datadogでのダッシュボード・アラート監視

基本的に監視はDatadogに集約しています。

Step FunctionsとECSのコンテナのログをCloudWatchに出力しています。それをLambda経由で、DataDogに投入。DataDog から、Slackに実行が失敗したら、通知を流しています。これらのログからダッシュボードを作成し、監視しています。

以前まで、cronで設定したジョブが失敗してもメールがくるだけで、ダッシュボードもなく、全体を把握できているとは言い難い状態でした。今回、かなり細かいところまで可視化できました。

- APMの取得
- コンテナ起動も含めた実行時間の可視化
- 実行履歴の可視化
- ダッシュボードの作成

![スクリーンショット 2020-07-08 13.22.12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/525723/56e89f10-cbf4-8eb0-a21f-d0fb89bbafed.png)


# Cloud Watchの癖

Cloud Watchの中でも、スケジュール実行できる Cloud Watch Eventsはとても便利です。
しかし、いくつか特徴があります。

## cronライクな記法

cronライクで定義できますが、記法が少し特殊です。

下記が比較ですが、年のフィールドがあります。 また、[日]フィールドと[曜日]フィールドの両方で、`*`が使用できず、片方は`？`を使用する必要があったり、癖があります。

Cron記法

```
分 時 日 月 曜 コマンド
```

Cloud WatchのCronライクな記法

```
cron(分 時 日 月 曜 年)
```

あとは、k8s cronJobも同じですが、UTCで時間設定が必要なので、ドキドキします。

このあたりの記法の補正もCloud Formationのマクロでできるのですが、やりすぎかと思い、そのままにしています。

## 実行時間誤差

>CloudWatch Eventsは、スケジュール式に第2レベルの精度を提供しません。cron式を使用した最高の解像度は1分です。
>
>CloudWatchイベントとターゲットサービスの分散性により、スケジュールされたルールがトリガーされてからターゲットサービスがターゲットリソースの実行を受け入れるまでの遅延は数秒になる場合があります。スケジュールされたルールはその分以内にトリガーされますが、正確な0秒にはトリガーされません。

[Troubleshooting CloudWatch Events - Amazon CloudWatch Events -> RuleDidNotTriggerOntime](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/CWE_Troubleshooting.html#RuleDidNotTriggerOntime)

数ヶ月間運用してきましたが、実行精度は概ね5-10秒程度の遅延で収束していました。

## 障害と多重実行
Cloud Watchは最低一回実行することを保証するので、2回実行されることがごく稀にあります。

>1つのイベントに応じて、ルールが複数回トリガーされました。CloudWatch Eventsは、ルールをトリガーしたり、ターゲットにイベントを配信したりするためにどのような保証を提供しますか？
>
>まれに、同じルールが単一のイベントまたはスケジュールされた時間に対して複数回トリガーされるか、特定のトリガーされたルールに対して同じターゲットが複数回呼び出されることがあります。

[Troubleshooting CloudWatch Events - Amazon CloudWatch Events -> RuleTriggeredMoreThanOnce](https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/CWE_Troubleshooting.html#RuleTriggeredMoreThanOnce)

[2020年東京リージョンでの大規模障害](https://xtech.nikkei.com/atcl/nxt/news/18/07639/)では、5回くらい多重実行されました。この時は、Cloud Watchにも障害が出ていたので、その影響だと思います。ECSも再起動がされなかったり、いろいろおかしかったですね。

平時でもごく稀にあるようなので、冪等性を担保してバッチを作ることが求められます。

# まとめ
Step FunctionsとCloud Watchをベースにすることで、運用コストの大幅な削減、システムの可視化をすることができました。しかし、Cloud Watchの癖など、少し不便さを感じている箇所もあります。

Cloud Watchの特徴から、厳密な実行精度をもとめられるシステムでは、採用できないと思います。

しかし要件が合致するなら、一考する価値はあるかもしれません。

今回は、[AWS Loft Tokyo](https://aws.amazon.com/jp/start-ups/loft/tokyo/)にも相談に行き、この構成に決めました。不安があり近い場合は行ってみると、かなり踏み込んだ相談ができると思います。