michimani.net

AWS Copilot と AWS CLI を使って ECS Exec を試す

2021-03-17

Amazon ECS で稼働中のコンテナに対して SSH 接続せずにコマンド実行できる機能 ECS Exec が発表されました。コールされる API は ExecuteCommand です。今回は AWS Copilot と AWS CLI を使って、稼働中のコンテナに対してコマンドを実行してみます。

目次

前提

Amazon ECS で稼働しているコンテナが存在することを前提とします。今回は、下記の記事内で構築した ECS タスクに対して実行することにします。

AWS Copilot で生成されるタスクロールに任意のポリシーをアタッチする - michimani.net

ExecuteCommand API

今回発表された API ExecuteCommand は、 ECS で稼働中のコンテナに対して、 SSH 接続せずにコマンドを実行することができる API です。対象のデータプレーンは EC2 および Fargate で、 Fargate のプラットフォームバージョンは 1.4.0 以上である必要があります。また、稼働中のコンテナには バージョン 1.50.2 以上の Container Agent Version がインストールされている必要があります。

詳しくは AWS の What’s New とブログを参照してください。

AWS Copilot で実行する

まずは AWS Copilot で実行してみます。

AWS Copilot は、今回の API 発表に合わせて公開された最新バージョン 1.4.0 が必要なのでアップデートしておきます。なお、私の環境 - macOS Big Sur でアップデートしようとしたらエラーになったので、その場合は Command Line Tools を再インストールしたりする必要があります。

macOS Big Sur で Copilot をアップデートしようとしたらエラーになった - michimani.net

Homebrew でインストールしている場合は下記のコマンドでアップデートします。

$ brew upgrade copilot-cli
$ copilot version
version: v1.4.0, built for darwin

copilot svc exec

AWS Copilot を使ってコマンドを実行する場合、 copilot svc exec コマンドを実行します。

$ copilot svc exec

すると、 Session Manager プラグイン がインストールされていないと言われたので、インストールします。

Looks like the Session Manager plugin is not installed yet.
Would you like to install the plugin to execute into the container? [? for help] (y/N) 

Session Manager プラグインがインストールされてコンテナに対してコマンドを実行するところで、今度は次のようなエラーが出ました。

Found only one deployed service list-s3-buckets-app in environment test
Execute `/bin/sh` in container list-s3-buckets-app in task e1c5b556450545e5a99895ced3143dc2.
✘ Failed to execute command /bin/sh. Is `exec: true` set in your manifest?
✘ execute command /bin/sh in container list-s3-buckets-app: execute command: InvalidParameterException: The execute command failed because execute command was not enabled when the task was run or the execute command agent isn’t running. Wait and try again or run a new task with execute command enabled and try again.

対象のコンテナでは、環境変数 enableExecuteCommandtrue がセットされている必要があります。 AWS Copilot で管理しているサービスの場合、 manifext.yml にて exec: true を記述します。

これに関しては下記の Copilot のドキュメントに書かれています。

Load Balanced Web Service - exec | AWS Copilot CLI

あらためて実行すると、あたかも SSH したかのようにターミナル上でコンテナ内のシェルが起動します。

$ copilot svc exec
Found only one deployed service list-s3-buckets-app in environment test
Execute `/bin/sh` in container list-s3-buckets-app in task xxxxxxxxxxxxxxxxxxxx05c661910a95.

Starting session with SessionId: ecs-execute-command-08dc04c5e87685c1d
/go #

あとは普通にコマンド実行するだけです。

/go # go version
go version go1.15.5 linux/amd64

/go # env
HOSTNAME=ip-10-0-0-179.ap-northeast-1.compute.internal
COPILOT_SERVICE_NAME=list-s3-buckets-app
SHLVL=1
HOME=/root
AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=/v2/credentials/7ce3249f-c697-0000-0000-xxxxxxxxxxxx
AWS_EXECUTION_ENV=AWS_ECS_FARGATE
AWS_DEFAULT_REGION=ap-northeast-1
COPILOT_ENVIRONMENT_NAME=test
ECS_CONTAINER_METADATA_URI_V4=http://169.254.170.2/v4/xxxxxxxxxxxxxxxxxxxx05c661910a95-3314152222
TERM=xterm-256color
ECS_CONTAINER_METADATA_URI=http://169.254.170.2/v3/xxxxxxxxxxxxxxxxxxxx05c661910a95-3314152222
PATH=/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
COPILOT_APPLICATION_NAME=hello-ecs
COPILOT_SERVICE_DISCOVERY_ENDPOINT=hello-ecs.local
LANG=C.UTF-8
COPILOT_LB_DNS=hello-Publi-SGUJQBQMXO00-1567060405.ap-northeast-1.elb.amazonaws.com
GOPATH=
AWS_REGION=ap-northeast-1
PWD=/go
GOLANG_VERSION=1.15.5

/go # exit


Exiting session with sessionId: ecs-execute-command-08dc04c5e87685c1d.

AWS CLI で実行する

続いて AWS CLI で、ECS で稼働中のコンテナに対してコマンドを実行してみます。

AWS CLI では、この記事執筆時点 (2021/03/16) で v1 の 1.19.28 のみ対応しています。

$ aws --version
aws-cli/1.19.28 Python/3.8.5 Darwin/20.3.0 botocore/1.20.28

この時点で v2 の最新は 2.1.30 ですが、まだ対応していませんでした。多分、いつもの流れなら数日後に対応されるでしょう。

ecs execute-command

AWS CLI では ecs- execute-command コマンドを使います。

$ aws ecs execute-command help
...

SYNOPSIS
            execute-command
          [--cluster <value>]
          [--container <value>]
          --command <value>
          --interactive | --non-interactive
          --task <value>
          [--cli-input-json <value>]
          [--generate-cli-skeleton <value>]
...

主なパラメータは下記のとおりです。

クラスター名とタスク ID が必要になるので、先に取得しておきます。

$ CLUSTER_NAME=$( \
  aws ecs describe-clusters \
  --clusters "$(\
    aws ecs list-clusters \
    --query "clusterArns[?contains(@, \`hello-ecs\`)] | [0]" \
    --output text
  )" \
  --query "clusters[0].clusterName" \
  --output text)
$ TASK_ARN=$(\
  aws ecs list-tasks \
  --cluster "${CLUSTER_NAME}" \
  --query "taskArns[0]" \
  --output text)

実行します。まずは --interactive で実行してみます。

$ aws ecs execute-command \
--cluster "${CLUSTER_NAME}" \
--task "${TASK_ARN}" \
--command "go version" \
--interactive

The Session Manager plugin was installed successfully. Use the AWS CLI to start a session.


Starting session with SessionId: ecs-execute-command-05539dfac9c3bd94d
go version go1.15.5 linux/amd64


Exiting session with sessionId: ecs-execute-command-05539dfac9c3bd94d.

セッションマネージャーが起動し、コマンドの実行結果が出力され、セッションが終了しました。

「interactive とは…」と一瞬思いましたが、実行したコマンドが go version なので対話もクソもなかったです。

ということで、 /bin/sh を実行してみます。

$ aws ecs execute-command \
--cluster "${CLUSTER_NAME}" \
--task "${TASK_ARN}" \
--command "/bin/sh" \
--interactive

The Session Manager plugin was installed successfully. Use the AWS CLI to start a session.


Starting session with SessionId: ecs-execute-command-027f2f4e471434482
/go #

Copilot のときと同様に、シェル起動して あたかも SSH したかのように見えます。

あとは普通にコマンドを実行して、logout または exit で終了します。

/go # go version
go version go1.15.5 linux/amd64
/go # env
HOSTNAME=ip-10-0-0-179.ap-northeast-1.compute.internal
COPILOT_SERVICE_NAME=list-s3-buckets-app
SHLVL=1
HOME=/root
AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=/v2/credentials/7ce3249f-c697-0000-0000-xxxxxxxxxxxx
AWS_EXECUTION_ENV=AWS_ECS_FARGATE
AWS_DEFAULT_REGION=ap-northeast-1
COPILOT_ENVIRONMENT_NAME=test
ECS_CONTAINER_METADATA_URI_V4=http://169.254.170.2/v4/xxxxxxxxxxxxxxxxxxxx05c661910a95-3314152222
TERM=xterm-256color
ECS_CONTAINER_METADATA_URI=http://169.254.170.2/v3/xxxxxxxxxxxxxxxxxxxx05c661910a95-3314152222
PATH=/go/bin:/usr/local/go/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
COPILOT_APPLICATION_NAME=hello-ecs
COPILOT_SERVICE_DISCOVERY_ENDPOINT=hello-ecs.local
LANG=C.UTF-8
COPILOT_LB_DNS=hello-Publi-SGUJQBQMXO00-1567060405.ap-northeast-1.elb.amazonaws.com
GOPATH=
AWS_REGION=ap-northeast-1
PWD=/go
GOLANG_VERSION=1.15.5
/go # exit


Exiting session with sessionId: ecs-execute-command-027f2f4e471434482.

non-interactive は現時点でサポートされていない

ちなみに --non-interactive は現時点ではサポートされていないようです。

$ aws ecs execute-command \
--cluster "${CLUSTER_NAME}" \
--task "${TASK_ARN}" \
--command "go version" \
--non-interactive

The Session Manager plugin was installed successfully. Use the AWS CLI to start a session.


An error occurred (InvalidParameterException) when calling the ExecuteCommand operation: Interactive is the only mode supported currently.

CloudTrail で証跡の確認

ExecuteCommand API の実行に関しては、 CloudTrail で証跡を確認することができます。

Copilot で実行した場合

{
    "eventVersion": "1.08",
    "userIdentity": {
        "type": "AssumedRole",
        "principalId": "XXXXXXXXXXXXXXXXBBZHM:1615903571022057000",
        "arn": "arn:aws:sts::000000000000:assumed-role/hello-ecs-test-EnvManagerRole/1615903571022057000",
        "accountId": "000000000000",
        "accessKeyId": "ASIA2FQKQ3TURQ676CHH",
        "sessionContext": {
            "sessionIssuer": {
                "type": "Role",
                "principalId": "XXXXXXXXXXXXXXXXBBZHM",
                "arn": "arn:aws:iam::000000000000:role/hello-ecs-test-EnvManagerRole",
                "accountId": "000000000000",
                "userName": "hello-ecs-test-EnvManagerRole"
            },
            "webIdFederationData": {},
            "attributes": {
                "mfaAuthenticated": "false",
                "creationDate": "2021-03-16T14:06:11Z"
            }
        }
    },
    "eventTime": "2021-03-16T14:06:12Z",
    "eventSource": "ecs.amazonaws.com",
    "eventName": "ExecuteCommand",
    "awsRegion": "ap-northeast-1",
    "sourceIPAddress": "153.225.234.16",
    "userAgent": "aws-ecs-cli-v2/v1.4.0 (darwin) aws-sdk-go/1.37.31 (go1.15.6; darwin; amd64)",
    "requestParameters": {
        "cluster": "hello-ecs-test-Cluster-XXXXXXXXXXdZ",
        "container": "list-s3-buckets-app",
        "command": "/bin/sh",
        "interactive": true,
        "task": "xxxxxxxxxxxxxxxxxxxx05c661910a95"
    },
    "responseElements": {
        "clusterArn": "arn:aws:ecs:ap-northeast-1:000000000000:cluster/hello-ecs-test-Cluster-XXXXXXXXXXdZ",
        "containerArn": "arn:aws:ecs:ap-northeast-1:000000000000:container/69a7d1af-0000-0000-0000-xxxxxxxxxxxx",
        "containerName": "list-s3-buckets-app",
        "interactive": true,
        "session": {
            "sessionId": "ecs-execute-command-08dc04c5e87685c1d",
            "streamUrl": "wss://ssmmessages.ap-northeast-1.amazonaws.com/v1/data-channel/ecs-execute-command-08dc04c5e87685c1d?role=publish_subscribe",
            "tokenValue": "HIDDEN_DUE_TO_SECURITY_REASONS"
        },
        "taskArn": "arn:aws:ecs:ap-northeast-1:000000000000:task/hello-ecs-test-Cluster-XXXXXXXXXXdZ/xxxxxxxxxxxxxxxxxxxx05c661910a95"
    },
    "requestID": "6b08cb73-b9f3-4394-bda9-5c9d1691bf9c",
    "eventID": "9ce15175-3700-48c6-8e50-081950d14f65",
    "readOnly": false,
    "eventType": "AwsApiCall",
    "managementEvent": true,
    "eventCategory": "Management",
    "recipientAccountId": "000000000000"
}

AWS CLI で実行した場合

{
    "eventVersion": "1.08",
    "userIdentity": {
        "type": "IAMUser",
        "principalId": "AIDAIXKQKK3IJ7GXIEEGE",
        "arn": "arn:aws:iam::000000000000:user/HOGE",
        "accountId": "000000000000",
        "accessKeyId": "XXXXXXXXXXXXXXXXXL2X",
        "userName": "HOGE"
    },
    "eventTime": "2021-03-16T14:55:36Z",
    "eventSource": "ecs.amazonaws.com",
    "eventName": "ExecuteCommand",
    "awsRegion": "ap-northeast-1",
    "sourceIPAddress": "153.225.234.16",
    "userAgent": "aws-cli/1.19.28 Python/3.8.5 Darwin/20.3.0 botocore/1.20.28",
    "requestParameters": {
        "cluster": "hello-ecs-test-Cluster-XXXXXXXXXXdZ",
        "command": "/bin/sh",
        "interactive": true,
        "task": "arn:aws:ecs:ap-northeast-1:000000000000:task/hello-ecs-test-Cluster-XXXXXXXXXXdZ/xxxxxxxxxxxxxxxxxxxx05c661910a95"
    },
    "responseElements": {
        "clusterArn": "arn:aws:ecs:ap-northeast-1:000000000000:cluster/hello-ecs-test-Cluster-XXXXXXXXXXdZ",
        "containerArn": "arn:aws:ecs:ap-northeast-1:000000000000:container/69a7d1af-0000-0000-0000-xxxxxxxxxxxx",
        "containerName": "list-s3-buckets-app",
        "interactive": true,
        "session": {
            "sessionId": "ecs-execute-command-027f2f4e471434482",
            "streamUrl": "wss://ssmmessages.ap-northeast-1.amazonaws.com/v1/data-channel/ecs-execute-command-027f2f4e471434482?role=publish_subscribe",
            "tokenValue": "HIDDEN_DUE_TO_SECURITY_REASONS"
        },
        "taskArn": "arn:aws:ecs:ap-northeast-1:000000000000:task/hello-ecs-test-Cluster-XXXXXXXXXXdZ/xxxxxxxxxxxxxxxxxxxx05c661910a95"
    },
    "requestID": "87faac46-6ae1-4887-8218-599e0db3ba85",
    "eventID": "ae940740-7a41-4355-a0a0-714d608ece7c",
    "readOnly": false,
    "eventType": "AwsApiCall",
    "managementEvent": true,
    "eventCategory": "Management",
    "recipientAccountId": "000000000000"
}

実行したコマンドも証跡には含まれていますが、これはあくまでも ExecuteCommand API コール時に指定したコマンドです。今回試したように /bin/sh を実行して interactive モードになったあとに実行したコマンドについては CloudTrail で確認することができません。ここは監査的に悩ましい点かと思います。

まとめ

ECS で稼働中のコンテナに対して SSH せずにコマンド実行できる API が公開されたので AWS Copilot と AWS CLI それぞれを使ってコマンド実行してみた話でした。

コマンドを実行してその結果が出力されるだけなのかなと想像してましたが、あたかもコンテナに SSH したかのようにコマンドを実行できたので、感動しました。

これで稼働中コンテナの調査はやりやすくなった反面、簡単にコマンド実行できるようになったので、必要なときだけ IAM Role で許可するなどして制限しておいたほうが良いのかなと思いました。というのも、 ExecuteCommand API コール時のコマンドは CloudTrail で確認できますが、シェルを起動して対話的に実行したコマンドは追えないからです。

とは言え、この操作を実現するために SSH 用のインスタンスを保持していた場合は、それらが不要になりますし、諸々の調査はかなり捗るようになりそうです。

追記

あとでドキュメントを確認したところ、クラスター作成時に ExecuteCommand API で対話的に実行したコマンドに関する設定をすることで、 CloudWatch Logs または S3 にそのログが出せそうでした。

Using Amazon ECS Exec for debugging - Amazon Elastic Container Service

これは次回試してみます。 書きました。

ECS Exec の Interactive モードで実行したコマンドのログを CloudWatch Logs および S3 に出力する - michimani.net

以上、よっしー (michimani) でした。