ECS Execでコンテナに出入り自由の環境を手に入れよう

2021年の1月に入社した @hidenba です。入社後は開発プロセスの改善をしたりインフラの刷新なんかをしていたら、あれよあれよという間にゴールデンウィークもおわり最近はバイオ村であそんでいます。

みなさんECS使ってますか!! EC2だとSSHでログインできるのにECSを使うと出来ない(というか大変)だったりしてぐぎぎぎぎぎってなったりしてないでしょうか?そんなあなたに朗報です。ECS Execの登場で、より簡単にECSのコンテナに接続してコンテナ内部での作業が可能になっています。

今回は、プライベートなサブネット内にECS Execで接続可能なコンテナ内でnginxを動作させるECSをTerraformで作っていこうと思います。(VPCやALB等のネットワークやロードバランサー周りの設定は割愛します)

Terraformの作成

セキュリティリグループの作成

resource "aws_security_group" "ecs" {
  name = "ecs"
  description = "ecs"

  vpc_id = aws_vpc.this.id

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

resource "aws_security_group_rule" "ecs" {
  security_group_id = aws_security_group.ecs.id

  type = "ingress"

  from_port = 80
  to_port = 80
  protocol = "tcp"

  cidr_blocks = [aws_vpc.this.cidr_block]
}

TaskExecutionのIAMロールの作成

resource "aws_iam_role" "task_execution" {
  name = "TaskExecution"

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

TaskExecutionのIAMロールポリシー作成

resource "aws_iam_role_policy" "ecs" {
  role = aws_iam_role.task_execution.id

  policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
      {
        "Action": [
          "logs:CreateLogGroup",
          "logs:CreateLogStream",
          "logs:PutLogEvents",
          "logs:DescribeLogGroups",
          "logs:DescribeLogStreams",
          "ssm:GetParameters",
          "secretsmanager:GetSecretValue",
          "kms:Decrypt"
        ],
        "Effect": "Allow",
        "Resource": "*"
      }
    ]
  })
}

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

ECS Exec用のIAMロールポリシーの作成

ECS ExecはSystemsManagerのセッションマネージャを利用してAWSCLIからコンテナにアクセスするのでSSMのIAMロールポリシーが必要となります

resource "aws_iam_policy" "ecs_exec" {
  name = "ecs_exec"

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

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

ロググループの作成

ログ設定をすることでnginxのアクセスログとECS Execで接続時の操作ログを見ることができるようになります

resource "aws_cloudwatch_log_group" "this" {
  name = "ecs"
}

ECSクラスターの作成

resource "aws_ecs_cluster" "this" {
  name = "ecs"
  capacity_providers = ["FARGATE"]
}

ECSタスク定義の作成

task_role_arn にIAMロールを設定します

initProcessEnable を有効にすることで、ゾンビSSMエージェントの子プロセスが全て削除されるようになります(この設定はなくてもECS Execで接続は出来ます)

resource "aws_ecs_task_definition" "this" {
  family = "taskdef"

  container_definitions = jsonencode([
    {
      "name": "app",
      "image": "nginx",
            "linuxParameters": {
        "initProcessEnabled": true
      },
      "logConfiguration": {
        "logDriver": "awslogs",
        "options": {
          "awslogs-group": "ecs",
          "awslogs-region": "ap-northeast-1",
          "awslogs-stream-prefix": "app"
        }
      },
      "portMappings": [
        {
          "hostPort": 80,
          "protocol": "tcp",
          "containerPort": 80
        }
      ]
    }
  ])

  task_role_arn = aws_iam_role.task_execution.arn
  execution_role_arn = aws_iam_role.task_execution.arn
  cpu                      = "256"
  memory                   = "512"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
}

ECSサービスの作成

enable_execute_command でECS Execを有効化します

resource "aws_ecs_service" "this" {
  name = "ecs_service"
  cluster = aws_ecs_cluster.this.id
  task_definition = aws_ecs_task_definition.this.arn
  launch_type = "FARGATE"

  desired_count = 1
  enable_execute_command = true

  load_balancer {
    target_group_arn = aws_lb_target_group.this.arn
    container_name = "app"
    container_port = "80"
  }

  network_configuration {
    subnets = [aws_subnet.private-1a.id, aws_subnet.private-1c.id]
    security_groups = [aws_security_group.ecs.id]
    assign_public_ip = false
  }

  lifecycle {
    ignore_changes = [
      desired_count,
      task_definition,
      load_balancer,
    ]
  }
}

VPCエンドポイントの設定

プライベートサブネット内のコンテナにはVPCエンドポイントを利用してアクセスすることが出来ます。

resource "aws_vpc_endpoint" "this" {
  for_each = toset(
    [
      "ec2",
      "ssm",
      "ec2messages",
      "ssmmessages"
    ]
  )

  vpc_id  = aws_vpc.this.id
  service_name = "com.amazonaws.ap-northeast-1.${each.value}"
  vpc_endpoint_type = "Interface"
  subnet_ids = [aws_subnet.private-1a.id, aws_subnet.private-1c.id]
  security_group_ids = [aws_security_group.vpc_endpoint.id]
  private_dns_enabled = true
}

Terraformの実行

設定は以上になります。Terraformを実行してECSを作成します

コンテナに接続

AWS CLIを利用してコンテナに接続を行います

タスクIDを確認

taskArns の XXXXXXXXXX の部分がタスクIDになります

$ aws ecs list-tasks --cluster ecs 
{
    "taskArns": [
        "arn:aws:ecs:ap-northeast-1:ACCOUNT:task/ecs/XXXXXXXXXX"
    ]
}

接続

クラスタecs のタスクIDXXXXXXXXXX のコンテナ app に接続します

$ aws ecs execute-command \
--cluster ecs \
--task XXXXXXXXXX  \
--container app \
--interactive \
--command "/bin/bash"

ログの確認

CludWatchロググループの ecs にECS Execでアクセスした際のログが出力されています

まとめ

ECS Execを利用するためには

  1. ECS Exec用のIAMロールをタスク定義のタスクロールに設定する
  2. ECSサービスでECS Execを有効にする
  3. プライベートサブネットの場合はVPCエンドポイントを作成する

とっても簡単にECSのコンテナに接続することができるようになります!既存の設定に追加するのも簡単なので是非お試しください!!