ECS Exec のログ記録はタスクロールで行われるため注意しよう

2023.02.11

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは!AWS事業本部コンサルティング部のたかくに(@takakuni_)です。

ECS Exec のログ記録について、権限周りで詰まった点があったのでご紹介したいと思います。

3行まとめ

  • ECS Exec ではタスクロールを利用してログの配信を行う
  • ECS クラスターの logging プロパティがDEFAULTの制御であってもタスクロールでログの配信を行う
  • DEFAULTの場合、awslogsログドライバーが使われるがタスクロールを利用してログの配信を行う

ECS Exec

ECS Exec とは、 Amazon ECS ( Amazon EC2 インスタンスまたは AWS Fargate ) で実行されているコンテナに対してコマンドを実行できる機能です。

稼働しているコンテナに対して、直接デバックを行えるため大変便利な機能です。

ログに関して

ECS Exec ではコマンドの実行履歴をログとして出力できます。ログの設定は、 ECS Exec を実行するタスクに関連づいた ECS クラスターで行います。

以下の3種類に設定できます。

  • NONE:ログを記録しない
  • DEFAULTawslogs ログドライバーで送信する
    • logConfiguration を null にした場合はログを配信しない
  • OVERRIDE: 指定したCloudWatch Logs , Amazon S3 またはその両方に送信するよう設定する

当初考えていたこと

上記のDEFAULT設定を利用すれば、タスクロールに権限を付与しなくていいと思っていました。

理由は以下のドキュメントにある通り、 AWS Fargate 環境下で awslogs ログドライバーを利用する場合は、タスク実行ロールがログの配信を担うためです。

以下に示しているのは、タスク実行 IAM ロールの一般的なユースケースです。

タスクは AWS Fargate または外部インスタンスでホストされています…

・ Amazon ECR プライベートリポジトリからコンテナイメージをプルします。

・ awslogs ログドライバーを使用して CloudWatch Logs にコンテナログを送信します。詳細については、「awslogs ログドライバーを使用する」を参照してください。

実際は、タスクロールを利用してログの配信を行う仕様だったため、動作を確認してみようと思います。

実際にやってみた

フォルダ構造は以下の通りです。必要に応じて適宜見たいファイルをクリックしてください。

├── ecs.tf
├── iam_policy_document
│   ├── assume_ecs_task.json
│   ├── iam_task_nginx.json
│   └── iam_task_nginx_mistake.json
├── providers.tf
├── task_definition
│   └── nginx.json
├── terraform.tfstate
├── terraform.tfstate.backup
└── vpc.tf
ecs.tf

ecs.tf

######################################
# CloudWatch Logs Configuration
######################################
resource "aws_cloudwatch_log_group" "nginx" {
  name              = "/ecs/${local.prefix}/nginx"
  retention_in_days = 1

  tags = {
    Name = "/ecs/${local.prefix}/nginx/"
  }
}

resource "aws_cloudwatch_log_group" "nginx_exec" {
  name              = "/ecs/${local.prefix}/nginx-ecs-exec"
  retention_in_days = 1

  tags = {
    Name = "/ecs/${local.prefix}/nginx-ecs-exec"
  }
}

######################################
# ECS Cluster Configuration
######################################
resource "aws_ecs_cluster" "cluster" {
  name = "${local.prefix}-cluster"

  configuration {
    execute_command_configuration {
      # logging    = "OVERRIDE"
      logging = "DEFAULT"

      log_configuration {
        cloud_watch_log_group_name     = aws_cloudwatch_log_group.nginx_exec.name
      }
    }
  }

  tags = {
    Name = "${local.prefix}-cluster"
  }
}

######################################
# ECS Task Configuration
######################################
resource "aws_iam_role" "task_exec" {
  name               = "${local.prefix}-task-exec-role"
  assume_role_policy = file("${path.module}/iam_policy_document/assume_ecs_task.json")

  tags = {
    Name = "${local.prefix}-task-exec-role"
  }
}

resource "aws_iam_role_policy_attachment" "task_exec" {
  role       = aws_iam_role.task_exec.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

resource "aws_iam_role" "task_nginx" {
  name               = "${local.prefix}-task-nginx-role"
  assume_role_policy = file("${path.module}/iam_policy_document/assume_ecs_task.json")

  tags = {
    Name = "${local.prefix}-task-nginx-role"
  }
}

resource "aws_iam_policy" "task_nginx" {
  name   = "${local.prefix}-task-nginx-policy"
  policy = file("${path.module}/iam_policy_document/iam_task_nginx.json")

  tags = {
    Name = "${local.prefix}-task-nginx-policy"
  }
}

resource "aws_iam_role_policy_attachment" "task_nginx" {
  role       = aws_iam_role.task_nginx.name
  policy_arn = aws_iam_policy.task_nginx.arn
}

data "aws_ecr_repository" "amazon_linux" {
  name = "ecr-public/amazonlinux/amazonlinux"
}

resource "aws_ecs_task_definition" "nginx" {
  family                   = "${local.prefix}-nginx-td"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  memory                   = 512
  cpu                      = 256
  execution_role_arn       = aws_iam_role.task_exec.arn
  task_role_arn            = aws_iam_role.task_nginx.arn
  container_definitions = templatefile("${path.module}/task_definition/nginx.json", {
    image_url         = "public.ecr.aws/nginx/nginx:latest",
    log_group_name    = aws_cloudwatch_log_group.nginx.name,
    region            = data.aws_region.current.name,
    log_stream_prefix = "sample-nginx"
  })
  lifecycle {
    ignore_changes = [
      container_definitions
    ]
  }
}

######################################
# ECS Task Configuration
######################################
resource "aws_security_group" "nginx" {
  name        = "${local.prefix}-nginx-sg"
  description = "${local.prefix}-nginx-sg"
  vpc_id      = aws_vpc.vpc.id

  ingress {
    description = "My IP"
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = ["${chomp(data.http.ifconfig.response_body)}/32"]
  }

  egress {
    description = "All Connection"
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  tags = {
    Name = "${local.prefix}-nginx-sg"
  }
}

resource "aws_ecs_service" "nginx" {
  name                   = "${local.prefix}-nginx-service"
  cluster                = aws_ecs_cluster.cluster.id
  task_definition        = aws_ecs_task_definition.nginx.arn
  launch_type            = "FARGATE"
  desired_count          = 1
  enable_execute_command = true

  network_configuration {
    security_groups  = [aws_security_group.nginx.id]
    subnets          = [aws_subnet.public_a.id]
    assign_public_ip = true
  }

  tags = {
    Name = "${local.prefix}-nginx-service"
  }
}
iam_policy_document/

iam_policy_document/assume_ecs_task.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "",
      "Effect": "Allow",
      "Principal": {
        "Service": "ecs-tasks.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

iam_policy_document/iam_task_nginx.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ssmmessages:CreateControlChannel",
        "ssmmessages:CreateDataChannel",
        "ssmmessages:OpenControlChannel",
        "ssmmessages:OpenDataChannel"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": "logs:DescribeLogGroups",
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogStream",
        "logs:DescribeLogStreams",
        "logs:PutLogEvents"
      ],
      "Resource": "*"
    }
  ]
}

iam_policy_document/iam_task_nginx_mistake.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ssmmessages:CreateControlChannel",
        "ssmmessages:CreateDataChannel",
        "ssmmessages:OpenControlChannel",
        "ssmmessages:OpenDataChannel"
      ],
      "Resource": "*"
    }
  ]
}
providers.tf

providers.tf

######################################
# Provider Configuration
######################################
terraform {
  required_version = "~> 1.3.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "4.46.0"
    }
  }
}

provider "aws" {
  region = "ap-northeast-1"
}

######################################
# Data Configuration
######################################

data "aws_caller_identity" "self" {}
data "aws_region" "current" {}

data "http" "ifconfig" {
  url = "http://ipv4.icanhazip.com/"
}

######################################
# Environment Variables Configuration
######################################
variable "system" {
  type    = string
  default = "nginx"
}

variable "env" {
  type    = string
  default = "sample"
}

locals {
  prefix = "${var.system}-${var.env}"
}
task_definition

task_definition/nginx.json

[
  {
    "name": "nginx",
    "image": "${image_url}",
    "essential": true,
    "linuxParameters": {
      "initProcessEnabled": true
    },
    "portMappings": [
      {
        "containerPort": 80,
        "hostPort": 80,
        "protocol": "tcp"
      }
    ],
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-group": "${log_group_name}",
        "awslogs-region": "${region}",
        "awslogs-stream-prefix": "${log_stream_prefix}"
      }
    }
  }
]
vpc.tf

vpc.tf

######################################
# VPC Configuration
######################################
resource "aws_vpc" "vpc" {
  cidr_block           = "10.0.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "${local.prefix}-vpc"
  }
}

######################################
# Public Subnet Configuration
######################################
resource "aws_internet_gateway" "igw" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "${local.prefix}-igw"
  }
}

resource "aws_route_table" "public" {
  vpc_id = aws_vpc.vpc.id

  tags = {
    Name = "${local.prefix}-public-rtb"
  }
}

resource "aws_route" "public_igw" {
  route_table_id         = aws_route_table.public.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.igw.id
}

resource "aws_subnet" "public_a" {
  vpc_id            = aws_vpc.vpc.id
  availability_zone = "${data.aws_region.current.name}a"
  cidr_block        = "10.0.0.0/24"

  tags = {
    Name = "${local.prefix}-public-a-subnet"
  }
}

resource "aws_route_table_association" "public_a" {
  subnet_id      = aws_subnet.public_a.id
  route_table_id = aws_route_table.public.id
}

あっている方

まずはタスクロールに権限が付与されている方をデプロイしてみます。

iam_policy_document/iam_task_nginx.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ssmmessages:CreateControlChannel",
        "ssmmessages:CreateDataChannel",
        "ssmmessages:OpenControlChannel",
        "ssmmessages:OpenDataChannel"
      ],
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": "logs:DescribeLogGroups",
      "Resource": "*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogStream",
        "logs:DescribeLogStreams",
        "logs:PutLogEvents"
      ],
      "Resource": "*"
    }
  ]
}

ECS Exec を実行して、適当にコマンドを打ってみます。

[cloudshell-user@ip-10-6-77-133 ~]$ aws ecs execute-command --cluster nginx-sample-cluster --container nginx --command /bin/bash --interactive --task arn:aws:ecs:ap-northeast-1:111111111111:task/nginx-sample-cluster/a9112a0a96ed445cbc93a90807e34e37

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


Starting session with SessionId: ecs-execute-command-0d656f7d108ce1d22
root@ip-10-0-0-143:/# echo "hello world"
hello world
root@ip-10-0-0-143:/# ping google.com
bash: ping: command not found
root@ip-10-0-0-143:/# aws
bash: aws: command not found
root@ip-10-0-0-143:/# curl localhost
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
root@ip-10-0-0-143:/# exit
exit


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

CloudWatch Logs を確認するとログストリームが作成されていることが確認できました。

間違っている方

続いて、タスクロールの IAM ポリシーを変更します。

ecs.tf

~~~~~~~~~~~~~~~~~~(省略)~~~~~~~~~~~~~~~~~~~~~~~~
resource "aws_iam_policy" "task_nginx" {
  name   = "${local.prefix}-task-nginx-policy"
+  policy = file("${path.module}/iam_policy_document/iam_task_nginx_mistake.json")
-  policy = file("${path.module}/iam_policy_document/iam_task_nginx.json")

  tags = {
    Name = "${local.prefix}-task-nginx-policy"
  }
}
~~~~~~~~~~~~~~~~~~(省略)~~~~~~~~~~~~~~~~~~~~~~~~

変更後の IAM ポリシーは以下の通りです。

iam_policy_document/iam_task_nginx_mistake.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "ssmmessages:CreateControlChannel",
        "ssmmessages:CreateDataChannel",
        "ssmmessages:OpenControlChannel",
        "ssmmessages:OpenDataChannel"
      ],
      "Resource": "*"
    }
  ]
}

こちらも適当に ECS Exec でコマンドを打ってみます。

[cloudshell-user@ip-10-6-77-133 ~]$ aws ecs execute-command --cluster nginx-sample-cluster --container nginx --command /bin/bash --interactive --task arn:aws:ecs:ap-northeast-1:622809842341:task/nginx-sample-cluster/a9112a0a96ed445cbc93a90807e34e37

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


Starting session with SessionId: ecs-execute-command-0289df90ce62e19cc
root@ip-10-0-0-143:/# echo "hello world"
hello world
root@ip-10-0-0-143:/# ping google.com
bash: ping: command not found
root@ip-10-0-0-143:/# aws
bash: aws: command not found
root@ip-10-0-0-143:/# curl localhost
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
root@ip-10-0-0-143:/# exit
exit


Exiting session with sessionId: ecs-execute-command-0289df90ce62e19cc.

CloudWatch Logs を確認しましたが、残念ながらログストリームの数は増えていませんでした。

CloudTrail を確認してみる

「ログストリームが作成できませんでした。つまり、タスクロールが権限不足です。」はあまりにも強引なので、 CloudTrail で確認してみました。

すると、直近のイベント履歴から「AccessDenied」とエラーが返されたイベントが見つかりました。

ちなみにユーザー名の「a9112a0a96ed445cbc93a90807e34e37」は ECS Exec を利用したタスク ID です。

もう少しエラーの中身を確認してみます。すると、errorMessageからタスクロールを利用して、ログ配信が行われていることがわかります。

よって、ECS Exec のログ配信はタスクロールが使われていることがわかりました。

{
  "eventVersion": "1.08",
  "userIdentity": {
      "type": "AssumedRole",
      "principalId": "AROAXXXXXXXXXXXXXXXXX:a9112a0a96ed445cbc93a90807e34e37",
      "arn": "arn:aws:sts::111111111111:assumed-role/nginx-sample-task-nginx-role/a9112a0a96ed445cbc93a90807e34e37",
      "accountId": "111111111111",
      "accessKeyId": "ASIAXXXXXXXXXXXXXXXX",
      "sessionContext": {
          "sessionIssuer": {
              "type": "Role",
              "principalId": "AROAXXXXXXXXXXXXXXXXX",
              "arn": "arn:aws:iam::111111111111:role/nginx-sample-task-nginx-role",
              "accountId": "111111111111",
              "userName": "nginx-sample-task-nginx-role"
          },
          "webIdFederationData": {},
          "attributes": {
              "creationDate": "2023-02-11T06:04:12Z",
              "mfaAuthenticated": "false"
          }
      }
  },
  "eventTime": "2023-02-11T06:35:04Z",
  "eventSource": "logs.amazonaws.com",
  "eventName": "CreateLogStream",
  "awsRegion": "ap-northeast-1",
  "sourceIPAddress": "13.XXX.XXX.XXX",
  "userAgent": "aws-sdk-go/1.41.4 (go1.18.3; linux; amd64) exec-env/AWS_ECS_FARGATE amazon-ssm-agent/3.1.1732.0",
  "errorCode": "AccessDenied",
  "errorMessage": "User: arn:aws:sts::111111111111:assumed-role/nginx-sample-task-nginx-role/a9112a0a96ed445cbc93a90807e34e37 is not authorized to perform: logs:CreateLogStream on resource: arn:aws:logs:ap-northeast-1:111111111111:log-group:/ecs/nginx-sample/nginx:log-stream:ecs-execute-command-0289df90ce62e19cc because no identity-based policy allows the logs:CreateLogStream action",
  "requestParameters": null,
  "responseElements": null,
  "requestID": "846035a4-1b10-4776-8b9e-a44b058d416d",
  "eventID": "95a42f28-d6de-406c-91df-e24980696995",
  "readOnly": false,
  "eventType": "AwsApiCall",
  "managementEvent": true,
  "recipientAccountId": "111111111111",
  "eventCategory": "Management",
  "tlsDetails": {
      "tlsVersion": "TLSv1.2",
      "cipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
      "clientProvidedHostHeader": "logs.ap-northeast-1.amazonaws.com"
  }
}

まとめ

以上、「ECS Exec のログ記録はタスクロールで行われるため注意しよう」でした。

awslogs ログドライバーは、標準出力/標準エラー出力では、タスク実行ロール。 ECS Exec ではタスクロールを利用します。

地味なはまりどころですが、どなたかの参考になれば幸いです。(ちなみにOVERRIDEログ設定もタスクロールを使用すると明記されいているため、総じて ECS Exec はタスクロールを使用すると覚えていいかと思いました。)

以上、AWS事業本部コンサルティング部のたかくに(@takakuni_)でした!