AWS CLI で Amazon ECS のタスクを実行する (第２回)

AWS CLI を使って AWS ECS でタスクを実行するために必要なコマンドをまとめます。
この記事を読むためには AWS EC2、AWS S3 および docker に対する基本的な知識が必要です。
サービス、メトリクス、タスクのスケジューリングについては記載しません。

前記事：[AWS CLI で AWS batch にジョブを送信する (全３回)](https://qiita.com/aokad/items/f28ffa5ae984678817f6)とリンクしていますが、未読でも問題なく読めるように記載したつもりです。

参考：[EC2 タスクを使用した AWS CLI のウォークスルー](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ECS_AWSCLI_EC2.html)

## 目次

全４回です。

1. [はじめに](https://qiita.com/aokad/items/b3762e3cc10db6104858)
2. タスクを実行してみる                        ← イマココ
3. [一連の流れをスクリプト化する](https://qiita.com/aokad/items/0eacf96fcb2378c7a36c)
4. [汎用 docker イメージを使用する](https://qiita.com/aokad/items/0e6a22082122cebc30bb)

AWS CLI 公式リファレンスはこちら [aws ecs](https://docs.aws.amazon.com/cli/latest/reference/ecs/index.html)

## 1. AWS CLI の準備

手順はこちらを参照してください。 [AWS CLI のインストールと設定](https://qiita.com/aokad/items/88fe9312eea1c12ba8a2)

今回は JSON を扱うために `jq` コマンドも使用しますのでインストールしてください。

https://stedolan.github.io/jq/download/

## 2. docker イメージを作成する（オプション）

今回の作業手順ではこちらで用意した docker イメージ [aokad/aws-wordcount](https://hub.docker.com/r/aokad/aws-wordcount/) を使用しますので、この作業は不要です。
独自イメージを使用する場合は [上記イメージの DockerFile](https://raw.githubusercontent.com/aokad/qiita_aws_batch/master/Dockerfile) を参考にしてください。

この記事においてイメージに必要なもの（最終的にはなくてもよいです）

 - AWS CLI ... `aws configure` はしないこと（タスク実行時に自動で一時的なアカウントが設定されます）
 - docker run の時に実行するスクリプトファイル

AWS ECR を使用する場合は [AWS CLI で Amazon ECR に docker イメージを push する](https://qiita.com/aokad/items/17a06c2384041bd60d16) を参照してください。

## 3. AWS 情報を環境変数にセット

この記事ではコピペ実行できるようにアカウントの違いを環境変数にセットしておきます。

 - AWS_ACCOUNTID ..... 自分のアカウントID
 - AWS_REGION ..... `aws configure` で設定したリージョン
 - SUBNET1,2,3 ..... 「AWSコンソール」→「VPCダッシュボード」→「VPC」からデフォルトVPCのサブネットを確認してください
 - SECURITYGROUPID ..... 「AWSコンソール」→「VPCダッシュボード」→「セキュリティグループ」からデフォルトのセキュリティグループをのIDを確認してください。新しく作成する場合は22番ポートを開けてください。
 - KEY_NAME ... 作成するインスタンスの SSH キーです。無ければ作製してください。
 - S3_BUCKET ... 作業用の S3 バケットです。任意の名称で作成してください。

設定例

```Bash
export AWS_ACCOUNTID=123456789012
export AWS_REGION=ap-northeast-1
export SUBNET1=subnet-123a456b
export SUBNET2=subnet-789c012d
export SUBNET3=subnet-345e678f
export SECURITYGROUPID=sg-11335577
export KEY_NAME=mykey
export S3_BUCKET=mybucket
```

## 4. クラスターを作成

まず、クラスターを作ります。ここでは名前だけ設定します。

```Bash
aws ecs create-cluster \
    --cluster-name myCluster \
    > create-cluster.log
```

作成したクラスターの ARN を取得します。タスクの実行で使用します。
※ ARN とは、Amazon リソースネームの略で、AWS リソースを一意に識別するためのものです。

```Bash
CLUSTER_ARN=$(jq -r '.cluster.clusterArn' create-cluster.log)
```

作成したクラスターは「AWSコンソール」→「Elastic Container Service」→「Amazon ECS」→「クラスター」から確認できます。

![amazon-ecs-1.png](https://qiita-image-store.s3.amazonaws.com/0/226829/f5ab71e5-e539-392d-6e87-d75c8aad7648.png)

## 5. タスク定義を作成

まず、タスク定義に必要なコンテナ定義を作成します。
コンテナ定義には docker コンテナを起動するための情報やログの出力方法、実行に必要な cpu や memory を設定します。
また、入力ファイルと出力ファイルのパスを環境変数 (docker run するときの --env オプション) として受け渡しできるように "environment" に項目だけ作成します。
※ 実際の値はタスクの実行でセットします。

```Bash
ECSTASKROLE="arn:aws:iam::${AWS_ACCOUNTID}:role/AmazonECSTaskS3FullAccess"

cat << EOF > task_definition.json
{
    "containerDefinitions": [
        {
            "name": "mytask-definision",
            "image": "aokad/aws-wordcount:0.0.1",
            "cpu": 1,
            "memory": 800,
            "essential": true,
            "entryPoint": [
                "ash",
                "-c"
            ],
            "command": [
                "ash run.sh \${INPUT} \${OUTPUT}"
            ],
            "environment": [
                {
                  "name": "INPUT",
                  "value": ""
                },
                {
                  "name": "OUTPUT",
                  "value": ""
                }
            ],
            "logConfiguration": {
                "logDriver": "awslogs",
                "options": {
                    "awslogs-group": "mytask",
                    "awslogs-region": "${AWS_REGION}",
                    "awslogs-stream-prefix": "ecs-test"
                }
            }
        }
    ],
    "taskRoleArn": "${ECSTASKROLE}",
    "family": "mytask"
}
EOF
```

タスク定義を登録します。

```Bash
aws ecs register-task-definition \
    --cli-input-json file://task_definition.json \
    > register-task-definition.log
```

作成したタスク定義の ARN を取得します。タスクの実行で使用します。

```Bash
TASK_DEFINITION_ARN=$(jq -r '.taskDefinition.taskDefinitionArn' register-task-definition.log)
```

## 6. ロググループを作成

AWS Batch は自動で作ってくれましたが、Amazon ECS では自分で作ります。

```Bash
aws logs create-log-group --log-group-name mytask
```

## 7. EC2 インスタンスを起動する

AWS Batch は自動で作ってくれましたが、Amazon ECS では自分で作ります。
Amazon ECS AMI を使用して EC2 インスタンスを起動します。

Amazon ECS AMI はリージョンごとにimage-idが違います。今回は東京リージョン (ap-northeast-1) を使用していますが、別リージョンを使用する場合は[こちら](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ECS_AWSCLI_EC2.html#AWSCLI_EC2_launch_container_instance)から選択してください。

```Bash
export AMI_ID=ami-a99d8ad5
```

ユーザデータを作成します。
ユーザデータとは、AWS EC2 でインスタンス起動時に実行できるスクリプトです。
詳しくはこちら：[Linux インスタンスでの起動時のコマンドの実行](https://docs.aws.amazon.com/ja_jp/AWSEC2/latest/UserGuide/user-data.html)

```Bash
cat << EOF > userdata.sh
Content-Type: multipart/mixed; boundary="==BOUNDARY=="
MIME-Version: 1.0

--==BOUNDARY==
Content-Type: text/cloud-boothook; charset="us-ascii"

# Install nfs-utils
cloud-init-per once yum_update yum update -y
cloud-init-per once install_nfs_utils yum install -y nfs-utils

cloud-init-per once docker_options echo 'OPTIONS="\${OPTIONS} --storage-opt dm.basesize=30G"' >> /etc/sysconfig/docker

#!/bin/bash
# Set any ECS agent configuration options
echo "ECS_CLUSTER=${CLUSTER_ARN}" >> /etc/ecs/ecs.config

--==BOUNDARY==--
EOF
```

まず、クラスター名もしくはARNを指定して起動したインスタンスをクラスターに登録します。
そのために必要なツールはあらかじめ Amazon ECS AMI に用意されています。

```Bash:上記ユーザデータより該当箇所を抜粋
cat << EOF > userdata.sh
...
echo "ECS_CLUSTER=${CLUSTER_ARN}" >> /etc/ecs/ecs.config
...
EOF
```

今回の例では 30GByte のディスクをアタッチし、docker コンテナから使用できるストレージも 30G に増加したいので、docker の設定を起動時に書き換えるようにします。

```Bash:上記ユーザデータより該当箇所を抜粋
cat << EOF > userdata.sh
...
# Install nfs-utils
cloud-init-per once yum_update yum update -y
cloud-init-per once install_nfs_utils yum install -y nfs-utils

cloud-init-per once docker_options echo 'OPTIONS="\${OPTIONS} --storage-opt dm.basesize=30G"' >> /etc/sysconfig/docker
...
EOF
```

EC2 インスタンスを起動します。

```Bash
aws ec2 run-instances \
  --image-id ${AMI_ID} \
  --security-group-ids ${SECURITYGROUPID} \
  --key-name ${KEY_NAME} \
  --user-data "file://userdata.sh" \
  --iam-instance-profile Name="ecsInstanceRole" \
  --instance-type t2.micro \
  --block-device-mappings "[{\"DeviceName\":\"/dev/xvdcz\",\"Ebs\":{\"VolumeSize\":30,\"DeleteOnTermination\":true}}]" \
  --count 1 \
  > run-instances.log

INSTANCE_ID=$(jq -r '.Instances[0].InstanceId' run-instances.log)
```

起動したインスタンスに名前を付けておきます。（オプション）

```Bash
aws ec2 create-tags --resources ${INSTANCE_ID} --tags Key=Name,Value=ecs-task-instance
```

インスタンスが起動完了するまで待ちましょう。

成功するとクラスターに登録されています。

![amazon-ecs-2.png](https://qiita-image-store.s3.amazonaws.com/0/226829/f17e17d7-64d9-839f-4ad8-334f1c782c89.png)


## 8. タスクの実行

ジョブを投入する前に、入出力用の S3 バケットを作成し、適当なデータを置いておきます。

```Bash
cat << EOF > Humpty.txt
Humpty Dumpty sat on a wall,
Humpty Dumpty had a great fall.
All the king's horses and all the king's men
Couldn't put Humpty together again.
EOF

aws s3 cp Humpty.txt s3://${S3_BUCKET}/
```

タスク定義で作成したコンテナ定義 （docker コンテナを起動するための情報）を上書きするための情報をファイル出力します。
今回は入力ファイルと出力ファイルのパスをセットします。

```Bash
cat << EOF > containerOverrides.json
{
    "containerOverrides": [
        {
            "name": "mytask-definision",
            "environment": [
                {
                    "name": "INPUT",
                    "value": "s3://${S3_BUCKET}/Humpty.txt"
                },
                {
                    "name": "OUTPUT",
                    "value": "s3://${S3_BUCKET}/Humpty.count.ecs.txt"
                }
            ]
    }]
}
EOF
```

いよいよタスクを実行してみます。
投入するクラスター、タスク定義、上記で作成したタスク独自の情報を渡します。

```Bash
aws ecs run-task \
    --cluster ${CLUSTER_ARN} \
    --task-definition ${TASK_DEFINITION_ARN} \
    --overrides file://containerOverrides.json
```

作成したタスクは「AWSコンソール」→「Elastic Container Service」→「Amazon ECS」→「クラスター」→「myCluster」→「タスク」から確認できます。
表示されない場合は「必要なタスクのステータス」を「Running」「Stopped」で切り替えてみてください。

![amazon-ecs-3.png](https://qiita-image-store.s3.amazonaws.com/0/226829/99283191-0039-fca9-8499-1c8962190ae2.png)

タスク終了後に新しくタスクを投入したい場合は、この「8. タスクの実行」だけを繰り返せばよいですが、並列実行させるためには「7. EC2 インスタンスを起動する」も行う必要があります。

AWS S3 のバケットに Humpty.count.ecs.txt が出力されていれば成功です。

```Bash
$ aws s3 ls s3://${S3_BUCKET}/Humpty.count.ecs.txt
2018-04-13 13:53:03        589 Humpty.count.ecs.txt
```

なお、スクリプトの実行ログは「AWSコンソール」→「ClowdWatch」→「ログ」→ロググループ「mytask」を選択→ログストリームから確認できます。

## 9. 片付け

AWS batch はコンピュート環境やタスク定義には料金がかからず実際に立ち上げたインスタンスとデータ転送量、AWS ECR イメージサイズ、ClowdWatch（ログ）に課金されます。

インスタンスを削除します。
AWS Batch は自動で削除してくれましたが、Amazon ECS では自分で削除します。
ひとまずこれで安心です。

```Bash
aws ec2 terminate-instances --instance-ids ${INSTANCE_ID}
```

タスク定義、クラスターを削除します。
AWSコンソールから削除してもよいですが、AWS CLI でのコマンド例も記載しておきます。

```Bash
# タスク定義を削除
aws ecs deregister-task-definition --task-definition ${TASK_DEFINITION_ARN}

# クラスターを削除
# インスタンスが起動していると削除できないため、インスタンスの削除完了を待ってから実行してください
aws ecs delete-cluster --cluster ${CLUSTER_ARN} 
```

AWSが出力した log を削除します

 1.「AWSコンソール」→「ClowdWatch」→「ログ」→該当するロググループをクリック→ログストリームを選択して、「ログストリームの削除」
 2. AWSコンソール」→「ClowdWatch」→「ログ」→該当するログを選択→ログストリームを選択して、「アクション」→「ロググループの削除」

AWS CLI で実行する場合は以下コマンドを使用します。

```Bash
# ログストリームを削除
# 複数ある場合はそれぞれ削除してください
aws logs delete-log-stream --log-group-name mytask --log-stream-name ecs-test/mytask-definision/{36桁のコード}

# ロググループを削除
aws logs delete-log-group --log-group-name mytask
```

---

今回はコマンドを順に実行しました。
インスタンスの起動やタスクの終了など待ちが発生するので、実際に使うためには待ちをどうにかして実装する必要がありそうです。
次回、[一連の流れをスクリプト化する](https://qiita.com/aokad/items/0eacf96fcb2378c7a36c)では、wait サブコマンドで実現する方法を記載します。