ECS on FargateでBastion用のコンテナをスケジュール起動、停止する

こんにちは、香田です。

今回はECS on FargateでBastion用のコンテナをスケジュール起動、停止する方法について紹介していきます。

AWS環境を構築するにあたって、terraformを利用し各リソースを作成していくので必要に応じてインストールしてください。

ECS on Fargateで稼働させるBastionコンテナについて

今回構築するBastion(踏み台ホスト)ですが、下記のようにECS on Fargate上でコンテナとして構築していきます。

BastionへのログインにはECS Execを使用し、Application Auto Scalingで指定したスケジュールで起動停止するように設定する流れとなります。

ECS Execの詳細については下記を参照してみてください。

デバッグ用にAmazon ECS Exec を使用

Bastionコンテナイメージの作成

はじめにBastionコンテナ用のDockerfileを作成します。

Bastionコンテナへ必要なパッケージは下記のDockerfileへ追加しインストールしていきます。

FROM amazonlinux:2

RUN amazon-linux-extras install -y

RUN yum update -y \
    && yum install \
    systemd \
    tar \
    unzip \
    sudo \
    jq \
    less \
    -y

RUN curl -OL https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip \
    && unzip awscli-exe-linux-x86_64.zip \
    && ./aws/install

RUN useradd "ec2-user" && echo "ec2-user ALL=NOPASSWD: ALL" >> /etc/sudoers

CMD ["/sbin/init"]

Terraform AWSプロバイダの設定

AWSのリソース作成にterraformを利用するため、AWSプロバイダを設定します。

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

ECR リポジトリの作成

作成したコンテナイメージの保存先としてECRを作成します。

イメージは5世代まで保持するようにライフサイクルポリシーを設定します。

resource "aws_ecr_repository" "bastion" {
  name                 = "bastion"
  image_tag_mutability = "MUTABLE"

  image_scanning_configuration {
    scan_on_push = true
  }
}

resource "aws_ecr_lifecycle_policy" "bastion" {
  repository = aws_ecr_repository.bastion.name
  policy = jsonencode({
    "rules" : [
      {
        "rulePriority" : 1,
        "description" : "最新のイメージを5つ保持",
        "selection" : {
          "tagStatus" : "any",
          "countType" : "imageCountMoreThan",
          "countNumber" : 5
        },
        "action" : {
          "type" : "expire"
        }
      }
    ]
  })
}

CloudWatch Logsの作成

BastionコンテナとECS Exec実行時のログ出力先としてCloudWatch Logsを作成します。

resource "aws_cloudwatch_log_group" "ecs_exec" {
  name              = "/ecs/ecs-exec"
  retention_in_days = 14
}

resource "aws_cloudwatch_log_group" "bastion" {
  name              = "/ecs/bastion"
  retention_in_days = 14
}

ECS クラスターの作成

ECS クラスターを作成します。

resource "aws_ecs_cluster" "bastion" {
  name = "bastion-cluster"

  setting {
    name  = "containerInsights"
    value = "enabled"
  }

  configuration {
    execute_command_configuration {
      logging = "OVERRIDE"
      log_configuration {
        cloud_watch_log_group_name = aws_cloudwatch_log_group.ecs_exec.name
      }
    }
  }
}

IAM ロールの作成

ECSで利用するIAM ロールとして、「ECS タスク実行 IAM ロール」と「タスク用のIAM ロール」を作成します。

ECS タスク実行 IAM ロールを作成します。

resource "aws_iam_role" "ecs_task_execution_role" {
  name = "ecs-task-execution-role"
  path = "/"
  assume_role_policy = jsonencode({
    "Version" : "2012-10-17",
    "Statement" : [
      {
        "Action" : "sts:AssumeRole",
        "Principal" : {
          "Service" : "ecs-tasks.amazonaws.com"
        },
        "Effect" : "Allow"
      }
    ]
  })
}

resource "aws_iam_role_policy" "ecs_task_execution_role" {
  name = "ecs-task-execution-role"
  role = aws_iam_role.ecs_task_execution_role.name

  policy = jsonencode({
    "Version" : "2012-10-17",
    "Statement" : [
      {
        "Effect" : "Allow",
        "Action" : [
          "ecr:GetAuthorizationToken",
          "ecr:BatchCheckLayerAvailability",
          "ecr:GetDownloadUrlForLayer",
          "ecr:BatchGetImage",
          "logs:CreateLogStream",
          "logs:PutLogEvents"
        ],
        "Resource" : "*"
      }
    ]
  })
}

タスク用のIAM ロールを作成します。

resource "aws_iam_role" "ecs_task_role" {
  name = "ecs-task-role"
  path = "/"
  assume_role_policy = jsonencode({
    "Version" : "2012-10-17",
    "Statement" : [
      {
        "Action" : "sts:AssumeRole",
        "Principal" : {
          "Service" : "ecs-tasks.amazonaws.com"
        },
        "Effect" : "Allow"
      }
    ]
  })
}

resource "aws_iam_role_policy" "ecs_task_role" {
  name = "ecs-task-role"
  role = aws_iam_role.ecs_task_role.name

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

ECS タスク定義の作成

Bastionコンテナ用のタスク定義を作成します。

data "aws_region" "current" {}

resource "aws_ecs_task_definition" "bastion" {
  family                   = "bastion"
  requires_compatibilities = ["FARGATE"]
  network_mode             = "awsvpc"
  cpu                      = "256"
  memory                   = "512"
  task_role_arn            = aws_iam_role.ecs_task_role.arn
  execution_role_arn       = aws_iam_role.ecs_task_execution_role.arn
  container_definitions = jsonencode([
    {
      "name" : "bastion",
      "image" : "${aws_ecr_repository.bastion.repository_url}:latest",
      "command" : [
        "sleep",
        "86400"
      ],
      "essential" : true,
      "linuxParameters" : {
        "initProcessEnabled" : true
      }
      "logConfiguration" : {
        "logDriver" : "awslogs",
        "options" : {
          "awslogs-group" : aws_cloudwatch_log_group.bastion.name,
          "awslogs-stream-prefix" : "bastion",
          "awslogs-region" : data.aws_region.current.name
        }
      }
    }
  ])
}

ECS サービスの作成

Bastionコンテナ用のECS サービスを作成します。

ECS サービスで利用するサブネットは、デフォルトVPCのサブネットを利用しています。

Security Groupはアウトバウンド ルールのみ追加し作成しています。

resource "aws_default_vpc" "default" {}

resource "aws_security_group" "bastion" {
  name   = "bastion"
  vpc_id = aws_default_vpc.default.id
}

data "aws_subnet_ids" "default" {
  vpc_id = aws_default_vpc.default.id
}

resource "aws_security_group_rule" "bastion_egress_all" {
  security_group_id = aws_security_group.bastion.id
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]
}

resource "aws_ecs_service" "bastion" {
  name                   = "bastion"
  cluster                = aws_ecs_cluster.bastion.name
  task_definition        = aws_ecs_task_definition.bastion.arn
  launch_type            = "FARGATE"
  desired_count          = "0"
  enable_execute_command = true

  network_configuration {
    subnets = data.aws_subnet_ids.default.ids
    security_groups = [
      aws_security_group.bastion.id
    ]
    assign_public_ip = true
  }

  lifecycle {
    ignore_changes = [
      desired_count,
    ]
  }
}

Application Auto Scalingでスケジュール設定

Application Auto Scalingを利用し、ECS サービスをスケジュールにて起動、停止するように設定します。

スケジュール設定として月曜〜金曜日で9時に起動し、21時に停止するように設定しています。

resource "aws_appautoscaling_target" "bastion" {
  max_capacity       = 0
  min_capacity       = 0
  resource_id        = "service/${aws_ecs_cluster.bastion.name}/${aws_ecs_service.bastion.name}"
  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace  = "ecs"

  lifecycle {
    ignore_changes = [
      min_capacity,
      max_capacity
    ]
  }
}

resource "aws_appautoscaling_scheduled_action" "bastion_scale_out" {
  name               = "bastion-scale-out"
  service_namespace  = aws_appautoscaling_target.bastion.service_namespace
  resource_id        = aws_appautoscaling_target.bastion.resource_id
  scalable_dimension = aws_appautoscaling_target.bastion.scalable_dimension
  schedule           = "cron(00 09 ? * MON-FRI *)"
  timezone           = "Asia/Tokyo"
  scalable_target_action {
    min_capacity = 1
    max_capacity = 1
  }
}

resource "aws_appautoscaling_scheduled_action" "bastion_scale_in" {
  name               = "bastion-scale-in"
  service_namespace  = aws_appautoscaling_target.bastion.service_namespace
  resource_id        = aws_appautoscaling_target.bastion.resource_id
  scalable_dimension = aws_appautoscaling_target.bastion.scalable_dimension
  schedule           = "cron(00 21 ? * MON-FRI *)"
  timezone           = "Asia/Tokyo"

  scalable_target_action {
    min_capacity = 0
    max_capacity = 0
  }
}

AWSリソースの作成

terraform applyを実行しAWSリソースを作成します。

$ terraform init
$ terraform apply

ECRへイメージ登録

イメージをビルドしECRへ登録していきます。

ECRへログインします。

$ export ECR_URI_BASE=$(aws sts get-caller-identity --query Account --output text).dkr.ecr.ap-northeast-1.amazonaws.com
$ aws ecr get-login-password | docker login --username AWS --password-stdin $ECR_URI_BASE

ビルドしECR リポジトリへ登録します。

$ docker build -t bastion:latest .
$ docker tag bastion:latest $ECR_URI_BASE/bastion:latest
$ docker push $ECR_URI_BASE/bastion:latest

Bastionコンテナへログイン

Application Auto Scalingのcron設定を適宜調整し、Bastionコンテナを起動させます。

ECS Execでコンテナへログインします。

$ export ECS_CLUSTER=bastion-cluster
$ export TASK_ID=$(aws ecs list-tasks --cluster ${ECS_CLUSTER} | jq -r ".taskArns[]" | cut -d "/" -f 3)
$ aws ecs execute-command --cluster ${ECS_CLUSTER} --task ${TASK_ID} \
--container bastion --command "/bin/bash" --interactive

コンテナよりexitでログアウトすると、CloudWatch Logsに下記のように操作履歴のログが出力されているのが確認できるはずです。

さいごに

ECS on FargateでBastion用のコンテナをスケジュール起動、停止する方法如何でしたでしょうか?

Application Auto Scalingでスケジュール起動、停止することで、業務時間以外などは基本停止運用し利用料金を抑えることが可能となります。

またECS Execを利用することで、SSH接続が不要となる為SSHキーやOSユーザの管理が不要となり、IAMユーザ側に集約できるので管理コストの面でもメリットがあるのではないでしょうか。

Bastionコンテナの配置先サブネットもECS Execによって、プライベートネットワークに配置可能となりセキュアに運用できるのではないでしょうか。

最後までご覧いただきありがとうございます。

SNSでもご購読できます。