はじめに
この記事ではコンテナオーケストレーションサービスである ECS(Elastic Container Service)を学びます。
実際にDockerコンテナをパブリックなサブネットに配置します。
サンプルコードは以下です
https://github.com/Yu-s/terraform-aws
連載記事一覧
【連載】terraform によるAWS環境構築入門 最終回 ~ RDSとElastiCache ~
【連載】terraform によるAWS環境構築入門 おまけ1 ~ piplineを用いた継続的インテグレーション ~
本連載での使用技術
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