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


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

前記事:AWS CLI で AWS batch にジョブを送信する (全3回)とリンクしていますが、未読でも問題なく読めるように記載したつもりです。

参考:EC2 タスクを使用した AWS CLI のウォークスルー

目次

全4回です。

  1. はじめに
  2. タスクを実行してみる
  3. 一連の流れをスクリプト化する ← イマココ
  4. 汎用 docker イメージを使用する

AWS CLI 公式リファレンスはこちら aws ecs

前回までの課題

前回までに AWS CLI を順に実行してタスクを実行するまで実現しました。
しかし、インスタンスの起動やタスクの終了など待ちが発生するので、実際のシステムに組み込むためには「待ち」をどうにかして組み込む必要があります。
今回は、wait サブコマンドで実現する方法を記載します。

インスタンスの起動を待つ

AWS CLI には wait コマンドが各サブコマンドに実装されています。

以下は ecs run-instances した後、起動するまで待ち、さらにステータスチェックが終了するまで待つ例です。

# インスタンス起動
aws ec2 run-instances (省略) > run-instances.log
# インスタンス ARN 取得
INSTANCE_ID=$(jq -r '.Instances[0].InstanceId' run-instances.log)
# インスタンス起動を待つ
aws ec2 wait instance-running --instance-ids ${INSTANCE_ID}
# インスタンスステータスチェック完了を待つ
aws ec2 wait instance-status-ok --include-all-instances --instance-ids ${INSTANCE_ID}

タスクの終了を待つ

次にタスクの終了を待ちます。

# タスク実行
aws ecs run-task (省略) > run-task.log
# タスク ARN 取得
TASK_ARN=$(jq -r '.tasks[0].taskArn' run-task.log)
# タスク終了を待つ
aws ecs wait tasks-stopped --tasks ${TASK_ARN} --cluster ${CLUSTER_ARN}

ここで問題が発生します。どうやら aws ecs wait には最大待ち時間 (10分) があるようです。
10分経つとまだタスクが終わっていなくても "Waiter TasksStopped failed: Max attempts exceeded" というメッセージを表示して抜けてきてしまいます。

私たちのグループで作成しているタスクには数十分かかるものもありますので、これは困ります。
ということで、今回は ecs wait describe-tasks コマンドでタスクの状態を見て STOPPED でなければ再度 ecs wait tasks-stopped としましたが、他に解決方法があるような気もします。

wait コマンドをリトライする例

while :
do
    # タスクの情報取得
    aws ecs describe-tasks --tasks ${TASK_ARN} --cluster ${CLUSTER_ARN} \
        > describe-tasks.log

    # タスクの情報からステータスを取得
    TASK_STATE=$(jq -r '.tasks[0].lastStatus' describe-tasks.log)

    # STOPPED で抜ける
    if test "${TASK_STATE}" = "STOPPED"; then
        break
    fi

    # タスク終了を待つ
    aws ecs wait tasks-stopped --tasks ${TASK_ARN} --cluster ${CLUSTER_ARN}
done

一連の流れをスクリプト化する

これまでの内容をまとめたスクリプトが以下です。
ここからダウンロードして、先頭の環境変数を編集してから bash run-ecs.sh してみてください。

run-ecs.sh
####################################
# アカウント情報を環境変数にセットする
####################################

# アカウントID
export AWS_ACCOUNTID=123456789012
# リージョン
export AWS_REGION=ap-northeast-1
# デフォルトVPCのサブネットid
export SUBNET1=subnet-123a456b
export SUBNET2=subnet-789c012d
export SUBNET3=subnet-345e678f
# デフォルトセキュリティグループのID
export SECURITYGROUPID=sg-11335577
# SSH キー名
export KEY_NAME=mykey
# 作業用 S3 バケット名
export S3_BUCKET=mybucket
# Amazon ECS AMI の ID (リージョンに適したIDをセットすること)
export AMI_ID=ami-a99d8ad5

####################################
# クラスターの作成
####################################

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

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

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

# ロググループを作成
LOG_GROUP_NAME=mytask-$(date "+%Y%m%d-%H%M%S%Z")
aws logs create-log-group --log-group-name ${LOG_GROUP_NAME}

# コンテナ定義を作成
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": "${LOG_GROUP_NAME}",
                    "awslogs-region": "${AWS_REGION}",
                    "awslogs-stream-prefix": "ecs-test"
                }
            }
        }
    ],
    "taskRoleArn": "${ECSTASKROLE}",
    "family": "mytask"
}
EOF

# タスク定義を作成
aws ecs register-task-definition \
    --cli-input-json file://task_definition.json \
    > register-task-definition.log

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

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

# ユーザデータを作成
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

# インスタンスを起動
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)

# 起動完了を待つ
aws ec2 wait instance-running --instance-ids ${INSTANCE_ID}
aws ec2 wait instance-status-ok --include-all-instances --instance-ids ${INSTANCE_ID}

# 起動したインスタンスに名前を付ける
aws ec2 create-tags --resources ${INSTANCE_ID} --tags Key=Name,Value=ecs-task-instance

####################################
# タスク実行
####################################

# サンプルを S3 にアップロード
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}/

# タスク実行
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

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

TASK_ARN=$(jq -r '.tasks[0].taskArn' run-task.log)

# タスク終了を待つ
while :
do
    aws ecs describe-tasks --tasks ${TASK_ARN} --cluster ${CLUSTER_ARN} \
        > describe-tasks.log

    TASK_STATE=$(jq -r '.tasks[0].lastStatus' describe-tasks.log)

    if test "${TASK_STATE}" = "STOPPED"; then
        break
    fi

    aws ecs wait tasks-stopped --tasks ${TASK_ARN} --cluster ${CLUSTER_ARN}
done

####################################
# 片付け
####################################

# EC2 インスタンスを削除
aws ec2 terminate-instances --instance-ids ${INSTANCE_ID}
aws ec2 wait instance-terminated --instance-ids ${INSTANCE_ID}

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

# クラスターを削除
aws ecs delete-cluster --cluster ${CLUSTER_ARN}

上記スクリプトが1回で成功した場合は問題ありませんが、何回かやり直した場合はタスク定義のリビジョンが複数できていると思いますので AWS コンソールから手動で削除してください。

logは自動削除していませんので、手動で消します。

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

AWS CLI で実行する場合は以下です。

# ログストリームを削除
# 複数ある場合はそれぞれ削除してください
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

今回は wait コマンドを使用して、タスク実行のスクリプト化を行いました。
次回、汎用 docker イメージを使用するでは、AWS CLI や docker run の時に実行するスクリプトファイルが入っていない docker イメージを使用する方法を記載します。