Fargateタスクをスクリプトのように気軽に同期実行しよう

この記事は [Wanoグループ Advent Calendar 2022](https://qiita.com/advent-calendar/2022/wano-group) の3日目の記事です!

# ちょっとそのDocker Image走らせたいだけなんだが....というケース
- おもしろそうだが手元で動かない巷のDocker Image
- cronの重い処理(の一部)をちょいと切り出したいだけ
- 「ちょっと今回の作業だけ重い処理を走らせたいだけ」

こういうの...ありませんか?

## ケース1. おもしろそうだが手元で動かない巷のDocker Image

ホストのアーキテクチャ依存でどうしても問題がでるDocker環境とかありますよね。(TensorFlowとかそういうとこある)



そういうときにちょろっとコンテナをAWS上で動かしたいことがありますが、

- ちょっと試したいだけのケースでもタスク定義を作らなきゃいけない
- なんらかのIaCツールの知識が必要になる
- コンソールに入って見なきゃいけなかったり


…でなかなか億劫です。
```docker run ``` したいだけ...。


##  ケース2. cronの重い処理(の一部)を切り出したいだけ

既存環境でcronやジョブスケジューラは自前/オンプレですでに集中管理してるが、処理自体は重いのでFargateなどにちょろっと移譲したい場合。  

こっちはこっちで、真面目にやるとそれなりの構成管理の面倒くささがあります。
- 実行時のログだのはCloudwatchLogsとかの方を見なきゃいけない
- エラーだの監視だのは定石通りまじめにやるとイベントドリブンの構成をしなきゃいけない。

インフラの面倒くささ、プロセスを非同期ジョブ化することの面倒臭さ両方を兼ね備えます。

## ケース3. 「ちょっと今回の作業だけ重い処理を走らせたいだけ」なんだが...

運用まわりではたまーにあるかと思います。
一時的なコンピューティングリソースが必要なだけだがバッチとしてはなかなか重い、みたいなとき。  
めんどくさいとこは同上です。



# 同期実行ツール ecs-task-runner を使う
そこで [github.com/pottava/ecs-task-runner](https://github.com/pottava/ecs-task-runner)を使います。

似たようなものを作ろうとしたときに以下の記事を見て知りました。
https://qiita.com/sawanoboly/items/25ae48f125868ecdb204

このツールは、AWS Fargate の同期タスクランナーです。Fargate で Docker コンテナを実行し、完了するまで待機してくれます。

便利なところは、タスク定義やCloudWatch Logなどのリソースは一時的に作成され、タスクの終了後に自動削除してくれるところです。




# 試しに実行する

今回動かしたいDocker Image は、音声ファイルをボーカルやドラム、ギターなどいくつかのステムに分割してくれるdeezerの[spleeter](https://github.com/deezer/spleeter)です。

publicのimageはもちろん ecs-task-runnerで直接指定できますが、今回は自前でDocker Image を作っておきます。

### Docker Image
Docker Imageはこういうのです。
```docker
ARG BASE=python:3.6

FROM ${BASE}

ARG SPLEETER_VERSION=2.3.1
ENV MODEL_PATH /model

RUN mkdir -p ${MODEL_PATH}
RUN apt-get update && apt-get install -y ffmpeg libsndfile1
RUN pip install musdb museval
RUN pip install spleeter==${SPLEETER_VERSION}

# Install AWS CLI v2
# https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-linux.html
RUN curl https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip -o /tmp/awscliv2.zip \
    && unzip /tmp/awscliv2.zip -d /opt \
    && /opt/aws/install -i /usr/local/aws-cli -b /usr/local/bin \
    && rm /tmp/awscliv2.zip \
    && rm -rf /opt/aws \
    && aws --version

ENV AWS_PAGER=""

ENTRYPOINT ["sh" , "-c"]
```

実行側とのファイルのやりとりくらいは必要になることが多いので、S3経由でアクセスくらいはできるように、``` aws-cli ``` は追加で入れておくとオススメです。

IaCは基本しなくてよいのですが、privateなイメージを利用する時はimageの置き場所くらいは必要になります。
```bash
aws ecr create-repository --repository-name ${REPOSITORY_NAME} --region ap-northeast-1
```

本筋ではないので省きますがビルドやECS pushに便利なMakefileは下に置いておきます。


<details><summary><strong>よくつかうコマンド集</strong></h2> </summary>

依存環境変数
```
REPOSITORY_NAME
TASK_ROLE
```

```makefile
REPOSITORY_NAME=<YOUR REPO NAME>
$(eval export AWS_ACCOUNT_ID := $(shell aws sts get-caller-identity | jq -r '.Account'))
$(eval export AWS_REGION := $(shell aws configure get region))
REPOSITORY_URI=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/${REPOSITORY_NAME}

# 依存コマンドCheck
install-check:
	which aws
	which jq
	which docker
	which ecs-task-runner
	

# ECRリポジトリ作成したいとき
infra-create-ecr:
	aws ecr create-repository --repository-name ${REPOSITORY_NAME} --region ap-northeast-1

# Docker Buildしたいとき
docker-build:
	cd docker && \
	docker build --platform linux/amd64 -t ${REPOSITORY_NAME}:latest .

# ECRにpushしたいとき
docker-push:
	export AWS_PAGER="" && \
	aws ecr get-login-password | docker login --username AWS --password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com && \
	docker tag ${REPOSITORY_NAME}:latest ${REPOSITORY_URI}:latest && \
	docker push ${REPOSITORY_URI}:latest

# ecsとs3との間の疎通テスト
ecs-test:
	USER= ecs-task-runner run ${REPOSITORY_URI}:latest --command="echo start! && aws s3 ls" --task-role-arn=${TASK_ROLE} 


```


</details>

### 実行

ECR等にimageをpush後、ECSのタスク定義などを作らずにいきなりDocker Image を実行できます!
(ここではS3に繋ぎたいのでタスクロールを指定していますが、用途によってはこれもいりません) 


```bash
USER= ecs-task-runner \
  run ${REPOSITORY_URI}:latest --command="aws s3 cp s3://xxx/tmp/in/音声.wav ./ && spleeter separate -o audio_output -p spleeter:2stems ./音声.wav && aws s3 cp audio_output s3://xxx/tmp/out/ --recursive " \
  --task-role-arn=${TASK_ROLE} \
  --cpu=2048 --memory=10240

```
今回のケースだとこのまま1分ほど待つと...

```
Fri Dec  2 21:36:51 JST 2022
{
  "container-1": [
    "2022-12-02T21:38:14+09:00: Completed 417 Bytes/417 Bytes (7.6 KiB/s) with 1 file(s) remaining\rdownload: (略)
    "2022-12-02T21:38:14+09:00: Fri Dec  2 12:38:14 UTC 2022",
....
```
上記コマンドの実行結果として標準出力にタスクの実行結果/ログが返りました!
ここに至るまでのタスク定義やCloudwatch Logは一時的に作られるものの、あとで確認するとちゃんと消えています。  

同期処理の都合上呼び出し側のプロセス自体が死ぬとゴミが残る可能性もありますが、基本は同じ定義グループやロググループが使われるので、特に気にすることもなさそうです。

# ということで便利

自分のユースケースだと本番での実行よりは技術検証や趣味での気軽な利用が主になりそうですが、存在を覚えておくとアイディア次第でなにかと便利かと思います。

オプションで基本なんでもできるので、[本家のパラメータ](https://github.com/pottava/ecs-task-runner#parameters)を参照してください。