開発日報

窓際エンジニアの開発備忘。日報は嘘です。

【連載】terraform によるAWS環境構築入門 第6回 ~ ECSを用いたコンテナオーケストレーション ~

はじめに

この記事ではコンテナオーケストレーションサービスである ECS(Elastic Container Service)を学びます。
実際にDockerコンテナをパブリックなサブネットに配置します。

サンプルコードは以下です
https://github.com/Yu-s/terraform-aws

連載記事一覧

本連載での使用技術

  • AWS
  • Docker
  • VPC
  • ECS(fargate)
  • ELB
  • IAM
  • Route53
  • RDS
  • ElastiCache
  • codexxx

1. Web サーバの構築

ここではECSをプライベートネットワークに配置し、nginxコンテナを起動します。 ALB経由でリクエストを受け取り、ECS上のnginxコンテナが処理します。

ECS クラスタ

ECSクラスタは、以下のようにクラスタ名を指定するだけで作成できます。

# ECSクラスタの定義
resource "aws_ecs_cluster" "example" {
  name = "example"
}

タスク定義

コンテナの実行単位を「タスク」と呼びます。
たとえばRailsアプリケーションの前段にnginxを配置する場合、ひとつのタスクの中でRailsコンテナとnginxコンテナが実行されます。
タスクは「タスク定義」から生成されます。

# タスク定義
resource "aws_ecs_task_definition" "example" {
  family                   = "example"
  cpu                      = "256"
  memory                   = "512"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  container_definitions    = file("./container_definitions.json")
}
  • 「family」はタスク定義のプレフィックスです。ファミリー にリビジョン番号を付与したものがタスク定義名になります。
  • Fargate起動タイプの場合は、network_modeに「awsvpc」を指定します
  • requires_compatibilitiesに「Fargate」を設定します。
  • コンテナ定義は「container_definitions.json」に記載します
[
  {
    "name": "example",
    "image": "nginx:latest",
    "essential": true,
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-region": "ap-northeast-1",
        "awslogs-stream-prefix": "nginx",
        "awslogs-group": "/ecs/example"
      }
    },
    "portMappings": [
      {
        "protocol": "tcp",
        "containerPort": 80
      }
    ]
  }
]
  • name - コンテナの名前
  • image - 使用するコンテナイメージ
  • essential - タスク実行に必須かどうかのフラク
  • portMappings - マッピングするコンテナのポート番号

ECSサービス

ECSサービスは指定した数のタスクを維持します。
なんらかの理由でタスクが終了してしまった場合、自動的に新しいタスクを起動してくれます。
インターネットからのリクエストはALBで受け、そのリクエストをコンテナにフォワードします。
ECSサービスは以下のように定義します。

resource "aws_ecs_service" "example" {
  name                              = "example"
  cluster                           = aws_ecs_cluster.example.arn
  task_definition                   = aws_ecs_task_definition.example.arn
  desired_count                     = 2
  launch_type                       = "FARGATE"
  platform_version                  = "1.3.0"
  health_check_grace_period_seconds = 60

  network_configuration {
    assign_public_ip = false
    security_groups  = [module.nginx_sg.security_group_id]

    subnets = [
      aws_subnet.private_0.id,
      aws_subnet.private_1.id,
    ]
  }

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

  lifecycle {
    ignore_changes = [task_definition]
  }
}

module "nginx_sg" {
  source      = "./security_group"
  name        = "nginx-sg"
  vpc_id      = aws_vpc.example.id
  port        = 80
  cidr_blocks = [aws_vpc.example.cidr_block]
}
  • clusterには、先゙で作成したECSクラスタを設定します。
  • task_definition先で作成したタスク定義を設定します。
  • ECSサービスが維持するタスク数はdesired_countで指定します。
  • health_check_grace_period_secondsに、タスク起動時のヘルスチェック猶予期間を設定します。タスクの起動に時間がかかる場合、十分な猶予期間を設定しておかないとヘルスチェックに引っかかり、タスクの起動と終了が無限に続いてしまいます。
  • network_configurationには、サブネットとセキュリティグループを設定します。
  • load_balancerでターゲットグループとコンテナの名前、ポート番号を指定し、ロー ドバランサと関連付けます。

2. Fargateによるロギング

CloudWatchLogs

retention_in_daysで、ログの保持期間を指定します。

resource "aws_cloudwatch_log_group" "for_ecs" {
  name              = "/ecs/example"
  retention_in_days = 180
}

ECSタスク実行IAMロール

AmazonECSTaskExecutionRolePolicyの参照を定義しましょう

data "aws_iam_policy" "ecs_task_execution_role_policy" {
  arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

ポリシードキュメント

aws_iam_policy_documentデータソースで「source_json」を使うと、既存のポリシーを継承できます。
そこで以下のようにAmazonECSTaskExecutionRolePolicyを継承し権限を追加しましょう。

data "aws_iam_policy_document" "ecs_task_execution" {
  source_json = data.aws_iam_policy.ecs_task_execution_role_policy.policy

  statement {
    effect    = "Allow"
    actions   = ["ssm:GetParameters", "kms:Decrypt"]
    resources = ["*"]
  }
}

IAM ロール

iam_roleモジュールを利用してIAMロールを作成します。
identifierに「ecs-tasks.amazonaws.com」を指定し、このIAMロールをECSで使うことを宣言します。

module "ecs_task_execution_role" {
  source     = "./iam_role"
  name       = "ecs-task-execution"
  identifier = "ecs-tasks.amazonaws.com"
  policy     = data.aws_iam_policy_document.ecs_task_execution.json
}

Docker コンテナのロギング

それでは、DockerコンテナがCloudWatchLogsにログを投げられるようにします。

resource "aws_ecs_task_definition" "example" {
  family                   = "example"
  cpu                      = "256"
  memory                   = "512"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  container_definitions    = file("./container_definitions.json")
  execution_role_arn       = module.ecs_task_execution_role.iam_role_arn
}

「container_definitions.json」を以下のようにします。

[
  {
    "name": "example",
    "image": "nginx:latest",
    "essential": true,
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-region": "ap-northeast-1",
        "awslogs-stream-prefix": "nginx",
        "awslogs-group": "/ecs/example"
      }
    },
    "portMappings": [
      {
        "protocol": "tcp",
        "containerPort": 80
      }
    ]
  }
]

3. まとめ(最終成果物の作成)

以上の解説をまとめて、最終成果物を作成しましょう。 フォルダ構成は以下の通り。 今回新しくlb.tfが追加になります。

exaple
 |
 |--- iam_role
 |    |___ main.tf
 |
 |--- security_group
 |    |___ main.tf
 |
 |--- container_definitions.json (新)
 |
 |--- ecs.tf (新)
 |
 |--- lb.tf
 |
 |--- main.tf
 |
 |--- network.tf
 |
 |___ S3.tf

まず、「container_definitions.json」コンテナ定義を追記しましょう。

[
  {
    "name": "example",
    "image": "nginx:latest",
    "essential": true,
    "logConfiguration": {
      "logDriver": "awslogs",
      "options": {
        "awslogs-region": "ap-northeast-1",
        "awslogs-stream-prefix": "nginx",
        "awslogs-group": "/ecs/example"
      }
    },
    "portMappings": [
      {
        "protocol": "tcp",
        "containerPort": 80
      }
    ]
  }
]

次に、「ecs.tf」にクラスタ・タスク定義・IAM・セキュリティグループなどの情報を記載しましょう。

# ECSクラスタの定義
resource "aws_ecs_cluster" "example" {
  name = "example"
}

# タスク定義
resource "aws_ecs_task_definition" "example" {
  family                   = "example"
  cpu                      = "256"
  memory                   = "512"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  container_definitions    = file("./container_definitions.json")
  execution_role_arn       = module.ecs_task_execution_role.iam_role_arn
}

# サービス定義
resource "aws_ecs_service" "example" {
  name                              = "example"
  cluster                           = aws_ecs_cluster.example.arn
  task_definition                   = aws_ecs_task_definition.example.arn
  desired_count                     = 2
  launch_type                       = "FARGATE"
  platform_version                  = "1.3.0"
  health_check_grace_period_seconds = 60

  network_configuration {
    assign_public_ip = false
    security_groups  = [module.nginx_sg.security_group_id]

    subnets = [
      aws_subnet.private_0.id,
      aws_subnet.private_1.id,
    ]
  }

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

  lifecycle {
    ignore_changes = [task_definition]
  }
}

# ecsで使用するnginxインスタンス向けセキュリティグループ
module "nginx_sg" {
  source      = "./security_group"
  name        = "nginx-sg"
  vpc_id      = aws_vpc.example.id
  port        = 80
  cidr_blocks = [aws_vpc.example.cidr_block]
}

# cloud watch ロギング
resource "aws_cloudwatch_log_group" "for_ecs" {
  name              = "/ecs/example"
  retention_in_days = 180
}

# AmazonECSTaskExecutionRolePolicy の参照
data "aws_iam_policy" "ecs_task_execution_role_policy" {
  arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy"
}

#「AmazonECSTaskExecutionRolePolicy」ロールを継承したポリシードキュメントの定義
data "aws_iam_policy_document" "ecs_task_execution" {
  source_json = data.aws_iam_policy.ecs_task_execution_role_policy.policy

  statement {
    effect    = "Allow"
    actions   = ["ssm:GetParameters", "kms:Decrypt"]
    resources = ["*"]
  }
}

# ECSタスク実行ロールの作成
module "ecs_task_execution_role" {
  source     = "./iam_role"
  name       = "ecs-task-execution"
  identifier = "ecs-tasks.amazonaws.com"
  policy     = data.aws_iam_policy_document.ecs_task_execution.json
}

4. 実行

まずはinitしましょう。これをやらないと、モジュールの呼び出しができません。

$ terraform init

実行計画の確認。前回にも書きましたが、破壊的な変更が入っていないか入念に確認しましょう。

$ terraform plan

実行。実行されたら、AWSの管理画面から確認しましょう。
うまくいっていればIAMページ左上の「Search IAM」のフォームに「describe-regions-for-ec2」と入力て検索ができるはずです。

$ terraform apply