michimani.net

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

2021-03-21

先日公開された ECS Exec の Interactive モードで実行したコマンドのログを CloudWatch Logs および S3 に出力してみます。

目次

手順

手順については下記の ECS のドキュメントを参考にしています。

Using Amazon ECS Exec for debugging - Amazon Elastic Container Service

やってみる

今回は、適当な Web サーバーを ECS で実行して、そのタスクに対して ECS Exec でコマンドを実行し、そのログを確認してみます。

ECS タスクの作成・起動

まずは、 ECS Exec を実行する対象となる ECS タスクを作成・起動します。手順については以前に書いた下記の記事とほぼ同じなので、重複する部分は折りたたんでます。(VPC、サブネットまわりについては前回のものを使います)

ECS タスクを作って実行して CloudWatch でログを確認するまでを AWS CLI だけでやってみた - michimani.net

なお、記事執筆時点では ECS Exec が AWS CLI v2 に対応していないため、 AWS CLI v1 1.19.30 を使います。

$ aws --version
aws-cli/1.19.30 Python/3.8.5 Darwin/20.3.0 botocore/1.20.30

追記

v2 に関しても 2.1.31 で対応していました。

aws-cli/CHANGELOG.rst at 2.1.31 · aws/aws-cli

0. 必要な環境変数の設定

まずは必要な環境変数を設定します。
$ APP_NAME="ecs-exec-test"
$ AWS_ACCOUNT_ID=$( \
  aws sts get-caller-identity \
  --query 'Account' \
  --output text)

1. ECR リポジトリ作成

ECR にリポジトリを作成します。
$ ECR_REPO_URI=$( \
  aws ecr create-repository \
  --repository-name ${APP_NAME} \
  --region ap-northeast-1 \
  --query "repository.repositoryUri" \
  --output text)

2. Dockerfile を作成

ECS タスクとして実行するアプリケーションを、下記の Dockerfile で定義します。

FROM ubuntu:20.10

RUN apt-get update \
&& apt-get install nginx -y

COPY index.html /var/www/html

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

index.html は下記の内容です。

Hello ECS Exec!

3. ビルドして ECR に Push

ビルドして ECR に Push します。
# ビルド
$ docker build -t ${APP_NAME} .

# タグ付け
$ docker tag ${APP_NAME}:latest ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${APP_NAME}:latest

# ECR にログイン
$ aws ecr get-login-password \
--region ap-northeast-1 \
| docker login \
--username AWS \
--password-stdin ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com

# push
$ docker push ${AWS_ACCOUNT_ID}.dkr.ecr.ap-northeast-1.amazonaws.com/${APP_NAME}:latest

4. クラスター作成

クラスター作成時に、 ECS Exec で実行したログを出力する CloudWatch Log のロググループ、及び S3 バケットを指定します。

$ aws ecs create-cluster \ 
--cluster-name ${APP_NAME}-cluster \
--configuration executeCommandConfiguration="{ \
  logging=OVERRIDE, \
  logConfiguration={ \
    cloudWatchLogGroupName=/dev/${APP_NAME}, \
    s3BucketName=${APP_NAME}-${AWS_ACCOUNT_ID}, \
    s3KeyPrefix=test \
  } \
}"

5. タスク実行ロール作成

タスク実行ロール作成します。
# AssumeRole Policy Document 作成
$ cat <<EOF > assume-role-policy-doc-for-task-exec-role.json
{
  "Version": "2008-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs-tasks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF

# IAM Role 作成
$ TASK_EXEC_ROLE_ARN=$( \
  aws iam create-role \
  --role-name ${APP_NAME}-task-exec-role \
  --assume-role-policy-document file://assume-role-policy-doc-for-task-exec-role.json \
  --query "Role.Arn" \
  --output text)

# アタッチする IAM Policy の ARN 取得
$ POLICY_ARN=$(\
  aws iam list-policies \
  --path-prefix "/service-role/" \
  --max-items 1000 \
  --query "Policies[?PolicyName == \`AmazonECSTaskExecutionRolePolicy\`].Arn" \
  --output text)

# Role に Policy をアタッチ
$ aws iam attach-role-policy \
--role-name ${APP_NAME}-task-exec-role \
--policy-arn ${POLICY_ARN}

6. タスクロール作成

タスクロールを作成します。
今回作成する ECS タスクでは、 ECS Exec を実行/そのログを出力するために CloudWatch Logs、 S3、 SSM への権限が必要になります。

# AssumeRole Policy Document 作成
$ cat <<EOF > assume-role-policy-doc-for-task-role.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs-tasks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}
EOF

# IAM Role 作成
$ TASK_ROLE_ARN=$( \
aws iam create-role \
--role-name ${APP_NAME}-task-role \
--assume-role-policy-document file://assume-role-policy-doc-for-task-role.json \
--query "Role.Arn" \
--output text ) \
&& echo "${TASK_ROLE_ARN}"

# アタッチする IAM Policy を作成
$ cat <<EOF > policy-for-task-role.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetBucketLocation"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetEncryptionConfiguration"
      ],
      "Resource": "arn:aws:s3:::${APP_NAME}-${AWS_ACCOUNT_ID}"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::${APP_NAME}-${AWS_ACCOUNT_ID}/*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:DescribeLogGroups"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogStream",
        "logs:DescribeLogStreams",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:ap-northeast-1:${AWS_ACCOUNT_ID}:log-group:/dev/${APP_NAME}:*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "ssmmessages:CreateControlChannel",
        "ssmmessages:CreateDataChannel",
        "ssmmessages:OpenControlChannel",
        "ssmmessages:OpenDataChannel"
      ],
      "Resource": "*"
    }
  ]
}
EOF

# タスクロール用のポリシー作成
$ TASK_ROLE_POLICY_ARN=$( \
  aws iam create-policy \
  --policy-name "${APP_NAME}-task-role-policy" \
  --path "/sample/" \
  --policy-document file://policy-for-task-role.json \
  --query "Policy.Arn" \
  --output text)

# タスクロールを作成
$ aws iam attach-role-policy \
--role-name ${APP_NAME}-task-role \
--policy-arn ${TASK_ROLE_POLICY_ARN}

7. タスク定義作成

タスク定義を作成します。
$ cat <<EOF > task-definition.json
{
  "family": "${APP_NAME}",
  "networkMode": "awsvpc",
  "containerDefinitions": [
    {
      "name": "${APP_NAME}-app",
      "image": "${ECR_REPO_URI}",
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "/dev/${APP_NAME}-task",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "dev"
        }
      }
    }
  ],
  "requiresCompatibilities": [
    "FARGATE"
  ],
  "taskRoleArn": "${TASK_ROLE_ARN}",
  "executionRoleArn": "${TASK_EXEC_ROLE_ARN}",
  "cpu": "256",
  "memory": "512"
}
EOF

# タスク定義を作成
$ TASK_DEF_ARN=$( \
aws ecs register-task-definition \
--cli-input-json file://task-definition.json \
--query "taskDefinition.taskDefinitionArn" \
--output text) \
&& echo "${TASK_DEF_ARN}"

8. アプリケーションログ用の ロググループ作成

アプリケーションログ用の ロググループ作成します。
$ aws logs create-log-group \
--log-group-name /dev/${APP_NAME}-task

9. ExecuteCommand ログ用のロググループ、 S3 バケット作成

ECS Exec で実行したコマンドのログを出力する CloudWatch Logs のロググループ、 S3 バケット作成します。

$ aws s3 mb "s3://${APP_NAME}-${AWS_ACCOUNT_ID}"
$ aws logs create-log-group \
--log-group-name "/dev/${APP_NAME}"

10. タスク実行

タスクを実行します。このとき --enable-execute-command オプションを付けて ecs run-task を実行します。

$ TASK_ARN=$(\
  aws ecs run-task \
  --cluster ${APP_NAME}-cluster \
  --task-definition "${TASK_DEF_ARN}" \
  --network-configuration "awsvpcConfiguration={subnets=[${SUBNET_ID}],assignPublicIp=ENABLED}" \
  --launch-type FARGATE \
  --enable-execute-command \
  --query "tasks[0].taskArn" \
  --output text)
実行状況確認
$ aws ecs describe-tasks \
--tasks ${TASK_ARN} \
--cluster ${APP_NAME}-cluster \
--query "tasks[0].lastStatus" \
--output text

11. レスポンス確認

タスクのステータスが RUNNING になったら、自動的に割り当てられた Public IP に対してリクエストして、レスポンスを確認します。

割り当てられた パブリック IP を取得

ecs describe-tasks で実行中タスクの詳細を取得します。その中に含まれる networkInterfaceId をもとにして、 ec2 describe-network-interfaces を使って ENI の詳細を取得し、その中に含まれる PublicIp を取得します。

ENDPOINT=$(\
  aws ec2 describe-network-interfaces \
  --network-interface-ids $(\
    aws ecs describe-tasks \
    --tasks ${TASK_ARN} \
    --cluster ${APP_NAME}-cluster \
    --query "tasks[0].attachments[0].details[?name=='networkInterfaceId'].value" \
    --output text) \
  --query "NetworkInterfaces[0].PrivateIpAddresses[0].Association.PublicIp" \
  --output text)

レスポンス確認

$ http ${ENDPOINT}
HTTP/1.1 200 OK
Accept-Ranges: bytes
Connection: keep-alive
Content-Length: 14
Content-Type: text/html
Date: Sun, 21 Mar 2021 06:28:56 GMT
ETag: "6056e3af-e"
Last-Modified: Sun, 21 Mar 2021 06:02:09 GMT
Server: nginx/1.18.0 (Ubuntu)

Hello ECS Exec!

http コマンドについては下記の記事をご覧ください。

CLI で http リクエストするなら HTTPie が便利 - michimani.net

ECS Exec でコマンド実行

タスクが起動したので、 ECS Exec でコンテナに対してコマンドを実行してみます。

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

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


Starting session with SessionId: ecs-execute-command-0e5f16ff1ee160a21
root@68e6de2197e642e7b106a5d205e05641-2282341209:/# cd /var/www/html/
root@68e6de2197e642e7b106a5d205e05641-2282341209:/var/www/html# ls
index.html
root@68e6de2197e642e7b106a5d205e05641-2282341209:/var/www/html# echo 'Hello AWS !!!' >| index.html
root@68e6de2197e642e7b106a5d205e05641-2282341209:/var/www/html# cat index.html 
Hello AWS !!!
root@68e6de2197e642e7b106a5d205e05641-2282341209:/var/www/html# exit
exit


Exiting session with sessionId: ecs-execute-command-0e5f16ff1ee160a21.

今回は、 index.html の中身を Hello AWS !!! に書き換えています。

その結果として、レスポンスが変わってます。

$ http ${ENDPOINT}
HTTP/1.1 200 OK
Accept-Ranges: bytes
Connection: keep-alive
Content-Length: 14
Content-Type: text/html
Date: Sun, 21 Mar 2021 06:28:56 GMT
ETag: "6056e3af-e"
Last-Modified: Sun, 21 Mar 2021 06:13:59 GMT
Server: nginx/1.18.0 (Ubuntu)

Hello AWS !!!

CloudWatch Logs で ECS Exec のログを確認

上記で実行したコマンドのログを CloudWatch Logs で確認してみます。直近のログストリームを取得し、そのログストリーム内のログイベントを取得しています。

# 最新のログストリーム名を取得
$ LOG_ST_NAME=$(\
  aws logs describe-log-streams \
  --log-group-name /dev/${APP_NAME} \
  --query 'max_by(logStreams[], &lastEventTimestamp).logStreamName' \
  --output text)

# ログストリームを指定してログイベントを取得
$ aws logs get-log-events \
--log-group-name /dev/${APP_NAME} \
--log-stream-name "${LOG_ST_NAME}" \
--limit 5 \
--query 'events[].[join(``, [ to_string(timestamp) ,`: `,message])]' \
--output text

1616307128716: Script started on 2021-03-21 06:12:07+00:00 [<not executed on terminal>]
root@68e6de2197e642e7b106a5d205e05641-2282341209:/# cd /var/www/html/
root@68e6de2197e642e7b106a5d205e05641-2282341209:/var/www/html# ls
index.html
root@68e6de2197e642e7b106a5d205e05641-2282341209:/var/www/html# echo 'Hello AWS !!!' >| index.html
root@68e6de2197e642e7b106a5d205e05641-2282341209:/var/www/html# cat index.html 
Hello AWS !!!
root@68e6de2197e642e7b106a5d205e05641-2282341209:/var/www/html# exit
exit

Script done on 2021-03-21 06:12:07+00:00 [COMMAND_EXIT_CODE="0"]

実行したコマンド、及び出力された内容がログに記録されています。

なお、マネジメントコンソールで確認すると下記のような状態で確認できます。

ちなみに S3 バケットに出力されたログは…

S3 バケットに出力されたログは、 CloudWatch Logs に出力されている内容と同じです。

Script started on 2021-03-21 06:12:07+00:00 [<not executed on terminal>]
]0;root@68e6de2197e642e7b106a5d205e05641-2282341209: /root@68e6de2197e642e7b106a5d205e05641-2282341209:/# 
]0;root@68e6de2197e642e7b106a5d205e05641-2282341209: /root@68e6de2197e642e7b106a5d205e05641-2282341209:/# cd /var/www/html/

]0;root@68e6de2197e642e7b106a5d205e05641-2282341209: /var/www/htmlroot@68e6de2197e642e7b106a5d205e05641-2282341209:/var/www/html# ls

index.html

]0;root@68e6de2197e642e7b106a5d205e05641-2282341209: /var/www/htmlroot@68e6de2197e642e7b106a5d205e05641-2282341209:/var/www/html# echo ''H'e'l'l'o' 'A'W'S' '!'!'!' >~ 



| 
 index.html

]0;root@68e6de2197e642e7b106a5d205e05641-2282341209: /var/www/htmlroot@68e6de2197e642e7b106a5d205e05641-2282341209:/var/www/html# cat index.html 

Hello AWS !!!

]0;root@68e6de2197e642e7b106a5d205e05641-2282341209: /var/www/htmlroot@68e6de2197e642e7b106a5d205e05641-2282341209:/var/www/html# exit

exit


Script done on 2021-03-21 06:12:07+00:00 [COMMAND_EXIT_CODE="0"]

まとめ

ECS Exec の Interactive モードで実行したログを CloudWatch Logs および S3 バケットに出力して内容を確認してみた話でした。

前回の記事では、単発のコマンドは CloudTrail に記録されることは書きましたが、今回は Interactive に実行したコマンドについても記録できることを確認できました。実行したコマンドだけでなく実行時に出力された内容もログに記録されるので、監査目的だけでなくトラブルシュートの作業ログとしても、あとから確認できるのは良さそうです。

この流れ (?) で CloudShell での操作もログに記録できるようになるといいのになーと思ってます。

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