Step Functionsから ECS Task(Fargate) を動かすときのTips


Step FunctionsでECSのタスク(Fargate)を動かす時に, 詰まったポイントを中心にtipsとしてまとめます. **僕自身あまり詳しくない**+**日本語苦手**なので, 読みづらい部分があると思うんですけど, **我慢**して読んでください. 
## はじめに
### Lambdaの制限
Lamnda関数で時間のかかる処理を動かす時(APIを叩くとか？), 何かと問題があります. 例えば..
* 実行時間制限を設定しないといけないけど, 見込みを立てるのが難しい
* 15分を超える処理を実行できない
とか？ 

### 解決策?
そこでひとつのLamnda関数で処理を行うのではなく, 複数のLambda関数に分散させて,
* LambdaからLambdaを呼び出すとか
* Step FunctionsにLambda関数を組み込むとか

色々やってみたんですけど, 結局は実行時間の解決になってないし, そもそも実行時間に不安があるなら **ECSタスク(Fargate)で動かしたらいいのでは**? という結論に至りました. 

### 今回のテーマ
で, そもそもStep Functions上で動かしていたLambda関数をECSタスク(Fargate)に移行させたという背景もあるので, 
「Step Functionsから ECS Task(Fargate) を動かすときのTips」というテーマに至ります.

## 技術的な背景
### Step Functions
- AWSが提供しているワークフローサービス
- FargateやLambdaなどAWS上のサービスをタスクとして, フローの中に組み込むことができる

### ECS(Elastic Container Service)とは
AWSが提供するコンテナオーケストレーションサービス, kubernetesみたいな? ただしAWSにはEKS(Elastic Kubernetes Service)があるので, kubernetesとは違うはず.
**構成要素**
- クラスタ ... 実行の基盤 複数のサブネットにラップをかけるみたいなイメージ?
- サービス ... タスクをよりデーモンっぽくすることができる? ただしタスクでもデーモンのような動作は可能 **ポートの開放などはサービスで設定する..たしか**
- タスク ... タスク定義に基づいて起動するコンテナの集まり, docker-composeで起動する複数のコンテナ群みたいな？
- タスク定義 ... タスクを定義するテンプレートみたいなもの, docker-compose.ymlみたいな？



### ECS Task(Fargate)
- ECS上で動作するコンテナ向けのサーバレスなサービス


### LambdaとECS Task(Fargate)の比較
||Fargate|Lambda|
|:--|:--|:--|
|実行時間|制約なし|最大15分以内の実行時間, また自分で実行時間制限を指定する必要がある|
|コスト|実行時間に対してコストが発生. Lambdaに比べるとコストが高い|必要な時のみ起動するため, コストが低い. 毎月のリクエストが100万件までは無制限で無料枠になるので, 少ないリクエスト数であれば実質無料|
|学習コスト|ECS(コンテナオーケストレーション)についての理解が必須. またDockerの知識も必要|実行OSや制限を抑えれば, その他の学習することはあまりない. コンテナイメージを実行することもあるが, Lambda用のコンテナイメージのDockerfileが用意されているため, 学習コストはほぼゼロ|

参考: スタートアップCTOが解説するECS Fargateのユースケース＆Lambdaの使い分け！😎AWSのサーバーレスコンピューティングを使いこなしましょう🐕 -> https://www.ragate.co.jp/blog/articles/11589

- Lambda: 短い処理に向いている. 実行時間が長い処理や重い処理には向いていない
- Fargate: コンテナに対しての自由度が高いので, LambdaでできないことをFargateが包括しているというイメージ

個人的な使い方としては, 基本はLambdaを使って, Lambdaで無理だったらFarateを使うくらいの意識でいいのかな..という感じです. Lambdaでは制限が多いけど, Lambda専用のDockerfileが用意されており実装が楽, 逆にFargateは, 自由度が高いものの, コンテナイメージを自前で用意しないといけない..みたいな感じだと思います.


## 今回作ったもの
GitHub APIを叩いて, 指定したリポジトリ内のIssueの割合を可視化(matplotlib)+S3上に保存するフローを作成しています. 作った理由は特にありません.

### 全体の概要図
Step Function自体は, タスクとしてFargateを呼び出しているだけでシンプルな構成になっています. 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433565/494fd7d7-ca73-ee00-61e2-93d993a91a45.png)
基本的にECS側では, タスク定義の作成やロールの割り当て 以外はやることがない印象です.
タスク定義についても, docker-compose.ymlを書いてるみたいなイメージで, docker-compose.ymlを触ったことがある人ならすんなり設定を進められると思います(たぶん).  

### 生成した円グラフ
Issueに割り当てられたラベルの割合を元に作成しています. 
<img width="500" alt="Graph.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/433565/612a34f2-31e0-ce05-1416-4b03226a046d.png">


## 今回必要なもの
- ECS
    - ECSクラスター
    - ECSタスク定義
    - ECSタスク実行ロール 
- Step Functions
    - VPCサブネット
    - セキュリティグループ
- ECR
    - 自作のコンテナイメージ
- APIキー
    - GitHub APIキー
- etc...



## つまづいたポイント
### タスクとサービスの違い，というよりは使い方
**結論** -> **タスクは単体で動作させることができる**. 

とりあえず大事なのは**サービスはタスクの上位概念ではない**ということかなと思います. 個人的にはここに詰まってしましました. どちらもタスク定義書を元に, コンテナを動かしてるだけです. なのでタスク単体でも動かすことができます. 

参考: AWS ECSを構成する3つの概念（Cluster, Service, Task）https://scrapbox.io/keroxp/AWS_ECS%E3%82%92%E6%A7%8B%E6%88%90%E3%81%99%E3%82%8B3%E3%81%A4%E3%81%AE%E6%A6%82%E5%BF%B5%EF%BC%88Cluster,_Service,_Task%EF%BC%89


### Fargate(ECSタスク)でのコマンドの上書き
まずは, Dockerにおけるコマンドの上書きについて, 
**Dockerにおけるコマンド+コマンドの必要性**
```bash
$ docker run -it --rm --name aoimaru ubuntu:latest ls #最後の"ls"がコマンド
```
このコマンドを実行することで, **ubuntu環境下でのlsコマンドを再現**することができます <- **ここポイント**
要は, サーバを再現しているのではなく, 特定環境下でのコマンド(プロセス)を再現しているだけ. だからDockerでは**1コンテナ1プロセス**が推奨されています. ここが理解できれば, 上のコマンドで最後に"ls"を指定する必要がすんなり理解できるかなと思います. 

コマンドは, Dockerfileでも指定するんですけど, コマンドラインではコマンドの上書きができます.
例えば, Dockerfileの末尾がこんなDockerfileがあるとします.
```Dockerfile
...
ENTRYPOINT ["python", "app.py"]
CMD ["aoimaru", "atcoder"]
```
この時デフォルトでは, **$ python app.py aoimaru atcoder** というランタイムが実行されます.
で, 例えば以下のようなコマンド
```bash
$ docker run -it --rm original original:latest EC-CUBE ec-cube 
```
を実行すると, **$ python app.py EC-CUBE ec-cube** というランタイムが実行されます. 

同じように, コマンドラインではなく, Fargateでコマンドを上書きした上で, 実行する時, **ここに落とし穴があります**
**結論** -> **上書きを行うとき, スペースを空けない方がいい**
```
[EC-CUBE,ec-cube]  # OK
[EC-CUBE, ec-cube] # NG
         ↑ ここ
```
上記のようにスペースがある場合, コマンドライン的には, このような認識になってしまいます. 
落とし穴が分かりやすいように, 引数はダブルクォーテーションで囲みます
```bash
$ python app.py "EC-CUBE" " ec-cube"
                           ↑ ここに余計なスペースが入る
```
こうなると, 引数として正しく認識してくれない. 
**解決策** -> ランタイム側で無駄なスペースを削除する方法もあるが, 値を渡す段階でスペースを入れない方が無難です. 


### その他権限周りについて
基本的には, エラー(権限不足)が吐かれたタイミングでの対応でいいと思います. 
ただし, Step Functionsについては, フローの中で他のAWSサービス(Farateなど)を実行するため"iam:PassRole"などが必要です



## 実装
実際のプログラム(Python)は省略します. 
### Fargateで動かすコンテナイメージのDockerfile
```Dockerfile
FROM python:3.9.0

RUN set -xe && \
    pip install matplotlib \
                boto3 \
                PyGithub \
                # matplotlibで日本語を使うためのライブラリ, 
                japanize-matplotlib && \
    echo "backend: Agg" >> /etc/matplotlibrc && \
    mkdir /opt/app

WORKDIR /opt/app
COPY ./src/app.py .

ENTRYPOINT ["python", "app.py"]
CMD [引数の指定]
```

### Step Functions
```json
{
    "StartAt": "GithubIssue",
    "Status" : {
        "GithubIssue": {
            "Type": "Task",
            "Resource": "arn:aws:states::ecs:runTask",
            "Parameters": {
                "LaunchType": "FARGATE",
                "Cluster": "$クラスターのARN",
                "TaskDefinition": "$タスク定義のARN",
                "Overrides": {
                    "ContainerOverrides": [
                        {
                            "Name": "$コンテナ名",
                            "Commands.$": "$.{Step Functionsでの入力Json}"
                        }
                    ]
                },
                "NetworkConfiguration": {
                    "AwsvpcConfiguration": {
                        "Subnets": [
                            "$サブネットのID"
                        ],
                        "SrcurityGroups": [
                            "$セキュリティグループのID"
                        ],
                        "AssignPublicIp": "ENABLED"
                    }
                }
            },
            "End": true,
            "Comment": "任意のコメント"
        }
    }
}
```

## おわりに
タスクとサービスの違いがややこしい印象です. コマンドの上書きのスペースについても発見するのにそこそこ時間を割いてしまいました.
この記事が, このようなちょっとした詰まりポイントの解決や, 初めてFargateを触ってみる際の参考になれば幸いです.

## 参考文献
* スタートアップCTOが解説するECS Fargateのユースケース＆Lambdaの使い分け！😎AWSのサーバーレスコンピューティングを使いこなしましょう🐕 -> https://www.ragate.co.jp/blog/articles/11589 [1]

* AWS ECSを構成する3つの概念（Cluster, Service, Task ) https://scrapbox.io/keroxp/AWS_ECS%E3%82%92%E6%A7%8B%E6%88%90%E3%81%99%E3%82%8B3%E3%81%A4%E3%81%AE%E6%A6%82%E5%BF%B5%EF%BC%88Cluster,_Service,_Task%EF%BC%89 [2]