S-JIS[2021-07-11/2021-07-13] 変更履歴

Amazon ECS awsコマンド

AWS ECSはAWS CLIで操作することが出来る。

  • AWS CLI version 2のecs

ECSクラスターの作成

ECSクラスターは以下のコマンドで作成する。

aws ecs create-cluster --cluster-name ecs-test-cluster

EC2コンテナインスタンスの作成

ECS(Docker)のホストとしてEC2インスタンス(EC2コンテナと呼ばれる)を使う場合、EC2インスタンスを作成してECSクラスターに登録する必要がある。

EC2コンテナインスタンスの作成元となるAMIは、Amazon ECS-optimized AMIを使う。
このAMIでは、EC2インスタンス起動時にECSプロセスが実行され、ECSクラスターへの登録が行われる。
登録に成功すると、ECSのウェブコンソールの該当クラスターの表示で、EC2インスタンスの個数が増える。

EC2コンテナインスタンスには/etc/ecs/ecs.configという設定ファイルがあり、登録先となるECSクラスター名をそこに書く。
EC2インスタンス起動時にecs.configを作成するよう、EC2インスタンス作成時のユーザーデータスクリプトを記述する。

# EC2インスタンスの作成
aws ec2 run-instances --image-id AMI 〜 --user-data ユーザーデータスクリプト.sh

ユーザーデータスクリプト.sh:

#!/bin/bash
echo ECS_CLUSTER=ecs-test-cluster >> /etc/ecs/ecs.config

ちなみに、--user-dataで指定したユーザーデータスクリプトは、EC2インスタンスの/var/lib/cloud/instance/user-data.txtというファイルで保存されている。


ecs.configにはプロキシーの設定を書くことも出来る。
Amazon Linux HTTP プロキシのユーザーデータスクリプトの例


EC2コンテナを起動したのにECSクラスターに登録されない場合、登録に失敗していると思われる。

EC2コンテナインスタンスのec2-userにログインし(EC2インスタンス作成時にキーペアを指定しておき、それでログインする)、ログを確認する。

view /var/log/ecs/ecs-agent.log

ECSクラスターへの登録の為にhttps://ecs.ap-northeast-1.amazonaws.com/にアクセスするようだが、エラーになっている場合、そこに繋がらない可能性が高い。

例えばプロキシーを使っている場合、/etc/ecs/ecs.configにプロキシーの設定が書けるので、それを変更する。
ecs.configを変更した後は以下のコマンドでECSプロセスを再起動する。

sudo systemctl restart ecs

参考:aikan-umetsuboさんのECSインスタンスがクラスターに登録されない


タスク定義

Docker上で実行するコマンドは、タスクとして定義しておく。
(タスク定義自体にはECSクラスターは関係しない)

タスク定義はjsonファイルで、使用するDockerイメージや稼動するホストの種類(EC2かFargateか)および実行するコマンドを記述する。

タスク定義.json:

{
  "family": "test-task",
  "containerDefinitions": [
    {
      "name": "test-app",
      "image": "123456789123.dkr.ecr.ap-northeast-1.amazonaws.com/test-ecs:latest",
      "essential": true,
      "entryPoint": ["sh", "-c"],
      "command": ["exit 123"]
    }
  ],
  "requiresCompatibilities": [
    "EC2"
  ],
  "cpu": "256",
  "memory": "512"
}
aws ecs register-task-definition --family test-task --cli-input-json file://~/タスク定義.json

imageにAmazon ECRに登録したDockerイメージを指定する。
requiresCompatibilitiesがホストの種類(EC2やFARGATE)。
cpuは必要とするCPUで、「vCPU×1024」を指定する。
memoryは使用するメモリー。単位はMiB(メビバイト)。
commandに実行するコマンドを書く。

entryPointとcommandが合体して実行される。
上記の例だと、「sh -c 'exit 123'」が実行されることになる。(sh -cは、実行するコマンドを引数の文字列で指定するもの)
(タスク実行時に引数の値を変えたい場合は、run-taskのオーバーライド機能を使う)

ECSでは(Dockerでは?)、標準出力に出力された文字列を取得できないので、試しに実行する場合はexitコマンドで戻り値を見るのが良いと思う。
CloudWatchを使用して標準出力の内容を取得する例


DockerコンテナのホストとしてFargateを使う場合は、networkModeやexecutionRoleArnも指定する。

{
  〜
  "requiresCompatibilities": [
    "FARGATE"
  ],
  "networkMode": "awsvpc",
  "cpu": "256",
  "memory": "512",
  "executionRoleArn": "arn:aws:iam::123456789123:role/ecsTaskExecutionRole"
}

タスクの実行

以下のコマンドでタスクを実行する。

aws ecs run-task --cluster ecs-test-cluster --task-definition test-task --propagate-tags TASK_DEFINITION

タスクを実行すると、実行情報がjson形式で返ってくる。
その中にtaskArn(タスクID)がある。

run-taskはタスクの実行を開始するだけで、終了は待たない。(処理本体は非同期で実行される)
なので、taskArnを使って結果を確認する

以下のようにすると、taskArnだけを取得できる。(エラーが発生するとTASK_ARNが空になる上に、エラー理由が握りつぶされるので、あまり良くないが)

TASK_ARN=$(aws ecs run-task 〜 --query "tasks[].taskArn" --output text)

タスク定義でFARGATEが指定されている場合、DockerコンテナのホストはFargateになる。
ただ、Fargateも実体としてはEC2インスタンスが起動されると思う。

FargateのEC2インスタンスにサブネットやらセキュリティーグループやらを指定したい場合は、run-taskの--network-configurationオプションのawsvpcConfigurationで指定する。


タスク定義で指定されたmemoryがホストで確保できない場合(他のタスクが使用中で自分の分が確保できない場合)は、以下のようなjsonが返ってくる。[2021-07-13]
(aws ecs run-task自体の終了コードは0)

{
    "tasks": [],
    "failures": [
        {
            "arn": "arn:aws:ecs:ap-northeast-1:123456789123:container-instance/〜",
            "reason": "RESOURCE:MEMORY"
        }
    ]
}

ホスト(コンテナインスタンス)が複数台ある場合は、複数行出力されることがある。

ホストが起動していない場合はreasonが「AGENT」になる。

API の失敗の理由


タスクのオーバーライド

タスクの実行時に、タスク定義の一部を上書きする機能がある。
実行するコマンドの引数を変えたい場合等に利用する。

オーバーライド.json:

{
  "containerOverrides": [
    {
      "name": "test-app",
      "command": ["exit 22"]
    }
  ]
}
aws ecs run-task 〜 --overrides file://~/オーバーライド.json

上記の例だと、コマンドそのものがオーバーライド.jsonのものに上書きされる。


ファイルを使わず直接記述することも出来る。

aws ecs run-task 〜 --overrides '{"containerOverrides": [{"name": "test-app", "command": ["exit 22"]}]}'
VALUE=22
OVERRIDE=$(cat << EOF
{
  "containerOverrides": [
    {
      "name": "test-app",
      "command": ["exit $VALUE"]
    }
  ]
}
EOF
)
aws ecs run-task 〜 --overrides "$OVERRIDE"

タスク定義とオーバーライドの組み合わせの例。

タスク定義 オーバーライド 備考
"command": ["exit 123"] なし 「exit 123」が実行される。
"command": ["exit 123"] "command": ["exit 22"] 「exit 22」が実行される。
"command": ["exit 123"] "command": ["exit $TEST"],
"environment": [
  {
    "name": "TEST",
    "value": "33"
  }
]
環境変数を使用する例。
「exit 33」が実行される。
"command": ["exit $TEST"] "environment": [
  {
    "name": "TEST",
    "value": "44"
  }
]
タスク定義で環境変数を使用し、オーバーライドで環境変数を定義する例。
「exit 44」が実行される。

タスクの確認

タスクの実行状況・実行結果は以下のコマンドで確認する。

aws ecs describe-tasks --cluster ecs-test-cluster --tasks $TASK_ARN
{
    "tasks": [
        {
            "attachments": [],
            "availabilityZone": "ap-northeast-1c",
            "clusterArn": "arn:aws:ecs:ap-northeast-1:123456789123:cluster/ecs-test-cluster",
〜
            "containers": [
                {
                    "containerArn": "arn:aws:ecs:ap-northeast-1:123456789123:container/ecs-test-cluster/〜",
                    "taskArn": "arn:aws:ecs:ap-northeast-1:123456789123:task/ecs-test-cluster/〜",
                    "name": "test-app",
                    "image": "123456789123.dkr.ecr.ap-northeast-1.amazonaws.com/test-ecs:latest",
〜
                    "lastStatus": "STOPPED",
                    "exitCode": 123,
〜
                    "cpu": "0"
                }
            ],
            "cpu": "256",
〜
            "lastStatus": "STOPPED",
            "launchType": "EC2",
            "memory": "512",
〜
            "stopCode": "EssentialContainerExited",
            "stoppedAt": "2021-07-10T10:19:01.908000+09:00",
            "stoppedReason": "Essential container in task exited",
            "stoppingAt": "2021-07-10T10:19:01.908000+09:00",
            "tags": [],
            "taskArn": "arn:aws:ecs:ap-northeast-1:123456789123:task/test-cluster/〜",
            "taskDefinitionArn": "arn:aws:ecs:ap-northeast-1:123456789123:task-definition/test-task:24",
            "version": 3
        }
    ],
    "failures": []
}

lastStatusが状態を表している。主に、実行中はPENDING、終了するとSTOPPEDになる。(→タスクのライフサイクル
stopCodeはエラーコードのようなもの。たぶんEssentialContainerExitedは正常終了。
stoppedReasonはエラーメッセージ。

exitCodeはコマンドの戻り値(終了コード)。


実行が終わるまでポーリングするなら、以下のような感じか。

while true
do
  STATUS=$(aws ecs describe-tasks 〜 --query "tasks[].lastStatus" --output text)
  if [ "$STATUS" = "STOPPED" ]; then
    break
  else
    sleep 3
  fi
done

この例では3秒間隔にしてみたが、run-taskのドキュメントによると、数秒から始めて間隔を増やしていき、最大は5分間隔だそうだ。


実行が失敗した場合、stopCodeやstoppedReasonにエラーコード・エラー理由が入ってくる。

            "containers": [
                {
                    "lastStatus": "STOPPED",
                    "reason": "CannotStartContainerError: Error response from daemon: OCI runtime create failed: container_linux.go:349: starting container process caused \"process_linux.go:319: getting the final child's pid from pipe caused \\\"read init-p: connection reset by peer\\\"\": u",
                }
            ],

            "lastStatus": "STOPPED",
            "stopCode": "TaskFailedToStart",
            "stoppedReason": "Task failed to start",

コンテナのエラーメッセージ(containersのreason)の中に「停止タスクのエラーコード」が入っていることがある。
例えばCannotPullContainerError(DockerイメージがECRから持ってこられない)とか。


エラー確認

describe-tasksでタスクの実行結果が確認できるが、エラーが発生した場合は結果のjson(containersのreason)にエラー理由 が出力される。[2021-07-13]

また、EC2コンテナインスタンスで実行した場合は、EC2インスタンスの/var/log/ecs/ecs-agent.logでもエラーが確認できる。
(jsonに入ってくるエラー理由のメッセージは長すぎると途中でカットされるので、ecs-agent.logの方が全文を見ることができて良いかも)


タスク定義(あるいはオーバーライド)のmemoryが小さすぎると、以下のようなエラーが発生する。

EC2コンテナインスタンスの/var/log/ecs/ecs-agent.log:

level=info time=2021-07-10T08:10:35Z msg="Task engine [arn:aws:ecs:ap-northeast-1:123456789123:task/ctc-ecs-digdag-test-cluster/〜]: error transitioning container [ctc-ecs-digdag-test-app (Runtime ID: 〜)] to [RUNNING]: Error response from daemon: OCI runtime create failed: container_linux.go:349: starting container process caused \"process_linux.go:319: getting the final child's pid from pipe caused \\\"read init-p: connection reset by peer\\\"\": unknown" module=docker_task_engine.go
level=info time=2021-07-10T08:10:35Z msg="Managed task [arn:aws:ecs:ap-northeast-1:123456789123:task/ctc-ecs-digdag-test-cluster/〜]: Container [name=ctc-ecs-digdag-test-app runtimeID=〜]: handling container change event [RUNNING]" module=task_manager.go
level=warn time=2021-07-10T08:10:35Z msg="Managed task [arn:aws:ecs:ap-northeast-1:123456789123:task/ctc-ecs-digdag-test-cluster/〜]: error starting/provisioning container[ctc-ecs-digdag-test-app (Runtime ID: 〜)]; marking its desired status as STOPPED: Error response from daemon: OCI runtime create failed: container_linux.go:349: starting container process caused \"process_linux.go:319: getting the final child's pid from pipe caused \\\"read init-p: connection reset by peer\\\"\": unknown" module=task_manager.go


CloudWatchによる標準出力の取得

ECSタスクのコマンドが標準出力に出力したメッセージは、そのままでは取得できない。
CloudWatchにログとして出力して、そちらから取得するのが一般的なようだ。

参考:michimaniさんのECS タスクを作って実行して CloudWatch でログを確認するまでを AWS CLI だけでやってみた


CloudWatchにログ出力する場合、まず、出力先となるロググループ名を定義する。
そして、タスク定義の中で、ログ出力先としてロググループ名を指定する。

aws logs create-log-group --log-group-name test-log-group

ロググループを作っておかないと、そのロググループ名を使ったタスクの実行時に以下のようなエラーが発生する。
CannotStartContainerError: Error response from daemon: failed to initialize logging driver: failed to create Cloudwatch log stream: ResourceNotFoundException: The specified log group does not exist.

タスク定義.json:

{
  "family": "test-task",
  "containerDefinitions": [
    {
      "name": "test-app",
      "image": "123456789123.dkr.ecr.ap-northeast-1.amazonaws.com/test-ecs:latest",
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "test-log-group",
          "awslogs-stream-prefix": "log-prefix"
        }
      },
      "essential": true,
      "entryPoint": ["sh", "-c"],
      "command": ["echo Hello-example-ecs-app!"]
    }
  ],
  "requiresCompatibilities": [
    "EC2"
  ],
  "cpu": "256",
  "memory": "512"
}

なお、オーバーライドではログの定義を上書き(変更)することが出来ない。
(オーバーライドでコマンドの引数を変えるのと同様に、ログの出力先を変えたいことはあると思うのだが…。ロググループ名は変えられないとしても、せめて接頭辞くらい変えられたらいいのに)


出力されたログはCloudWatchのウェブコンソールから見られる他、以下のコマンドでも確認できる。

aws logs describe-log-streams --log-group-name test-log-group
{
    "logStreams": [
        {
            "logStreamName": "log-prefix/test-app/abcdef12345678901234567890abcdef",
            "creationTime": 1625891374154,
            "firstEventTimestamp": 1625891375047,
            "lastEventTimestamp": 1625891375047,
            "lastIngestionTime": 1625891375112,
            "arn": "arn:aws:logs:ap-northeast-1:123456789123:log-group:test-log-group:log-stream:log-prefix/test-app/abcdef12345678901234567890abcdef",
〜
        }
    ]
}

ログストリーム名が「ログストリーム接頭辞/アプリケーション名/ID」という構造で付けられる。
同じロググループに複数のログ出力を行うと、logStreamsにそれら全てが含まれる。(IDが異なるので区別はつく)

aws logs get-log-events --log-group-name test-log-group --log-stream-name log-prefix/test-app/abcdef12345678901234567890abcdef
{
    "events": [
        {
            "timestamp": 1625891375047,
            "message": "Hello-example-ecs-app!",
            "ingestionTime": 1625891375112
        }
    ],
〜
}

このmessageが、ECSタスクのコマンドが標準出力に出力した内容となる。


json形式だと扱いづらいので、以下のようなコマンドなら最新のメッセージだけ取得できる。

LOG_ST_NAME=$(aws logs describe-log-streams --log-group-name test-log-group --query "max_by(logStreams[?starts_with(logStreamName, 'log-prefix/')], &lastEventTimestamp).logStreamName" --output text)
aws logs get-log-events --log-group-name test-log-group --log-stream-name "$LOG_ST_NAME" --query 'events[].message' --output text

ちなみに、--queryで指定するクエリーはJMESPath構文というらしい。かなり色々なことが出来る。


ECSへ戻る / AWSへ戻る / 技術メモへ戻る
メールの送信先:ひしだま