前回、前々回の続きで、Terraform で ECS 環境の構築を行います。 今回は、ECS の部分を構築します。
はじめに:最終的なゴールと今回の目標の確認
- 最終的な目標は、外部からのリクエストを受けられる ECS サービスの構築です。
- 下図の青く囲ったところが前回まで構築した部分、赤く囲ったところが今回の目標です。
ECS の構築
ECS の構成要素
AWS のコンテナオーケストレーションツールの ECS について簡単に説明します。
- 以前触れた EKS はコンテナオーケストレーターとして Kubernetes を使用できました。一方、ECS のコンテナオーケストレーターには AWS 独自のものが採用されています。
- ECS は主に以下の構成要素からなります:
ECS で Web サーバを構築する
- ここで、 ECS を用いて Web サーバを構築します。
ECS クラスタとタスクの定義
- まず、ECS クラスタとタスク定義を以下のように設定します:
- ECS クラスタの設定はクラスタ名を指定するだけです。
- タスク定義では以下の内容を設定します。
family
: タスク定義名のプレフィックスを指定。ここで指定した名前にリビジョン番号を付与したものがタスク定義名になる- 下の例では、「linkode-example:1」のようになり、番号はタスク定義更新時にインクリメントされる。
cpu
とmemory
: タスクが使用するリソースのサイズを設定。- 設定できる値の組み合わせは決まっている*1。
network_mode
: Fargate 起動タイプの場合は、awsvpc
を指定requires_compatibilities
: 起動タイプを指定。container_definitions
: コンテナ定義を実装。外部ファイルに記述して読み込むか、下のように json の内容をjsonencode
で囲って直接 tf ファイル内に記述する。
# ECS クラスター resource "aws_ecs_cluster" "example" { name = "example" } # タスク定義 resource "aws_ecs_task_definition" "example" { family = "linkode-example" cpu = "256" memory = "1024" network_mode = "awsvpc" requires_compatibilities = ["FARGATE"] container_definitions = jsonencode([ { "name" : "example", "image" : "nginx:latest", "essential" : true, "portMappings" : [ { "protocol" : "tcp", "containerPort" : 80 } ] } ]) }
ECS サービスの定義
- ECS サービスは以下のように設定します:
cluster
: 先ほど作成した ECS クラスタの ARN を指定。task_definition
: 先ほど作成したタスク定義の ARN を指定。desired_count
: ECS サービスが維持するタスク数。launch_type
: 起動タイプを指定。platform_version
: デフォルトでは LATEST が指定されるが、最新バージョンを指さない場合がある*2ため、明示的にバージョンを指定する。health_check_grace_period_seconds
: タスク起動時のヘルスチェック猶予期間を秒単位で指定(デフォルトは 0 秒)。- 十分な猶予期間を設定しておかないとヘルスチェックが NG となってしまい、タスクの起動と終了が無限に続いてしまう。
network_configuration
: ネットワーク構成を記述。assign_public_ip
: パブリック IP アドレスを割り当てるか否かを指定。security_groups
: セキュリティグループを指定。subnets
: サブネットを指定。
load_balancer
: ターゲットグループとコンテナの名前、ポート番号を指定し、ロードバランサーと関連付ける。lifecycle
: タスク定義のライフサイクルを設定。- Fargate の場合、デプロイのたびにタスク定義が更新され、
terraform plan
実行時に差分が出るため、 Terraform ではタスク定義の変更を無視すべき。 - よって、
ignore_changes
でタスク定義の変更を無視するように設定する。
- Fargate の場合、デプロイのたびにタスク定義が更新され、
# 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.4.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_a.id, aws_subnet.private_c.id, ] } load_balancer { target_group_arn = aws_lb_target_group.ecs.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] }
- ここまでの内容を
ecs.tf
に記述します。
. |-- alb.tf |-- ecs.tf // 新規作成したファイル |-- main.tf |-- network.tf |-- route53.tf |-- s3.tf `-- security_group |-- main.tf |-- output.tf `-- variable.tf
ALB ターゲットグループとリスナールールの作成
- 前回作成した
alb.tf
にターゲットグループの設定を追記します。target_type
: ターゲットの種類を指定。ECS インスタンス, IP アドレス, Lambda 関数が指定可能。deregistration_delay
: 登録解除する前に ALB が待機する時間を設定。health_check
: ヘルスチェックの設定。path
: ヘルスチェックで使用するパスhealthy_threshold
: 正常判定を行うまでのヘルスチェック実行回数unhealthy_threshold
: 異常判定を行うまでのヘルスチェック実行回数timeout
: ヘルスチェックのタイムアウト時間(秒単位で指定)interval
: ヘルスチェックの実行間隔(秒単位で指定)matcher
: 正常判定を行うために使用する HTTP ステータスコードport
: ヘルスチェックで使用するポートtraffic_port
を指定すると、上のport
で指定したポート番号が使用される
protocol
: ヘルスチェック時に使用するプロトコル
- ALB, ターゲットグループを ECS サービスと同時に作成するとエラーになるため、
depends_on
で依存関係を記述。
resource "aws_lb_target_group" "ecs" { name = "example" target_type = "ip" vpc_id = aws_vpc.example.id port = 80 protocol = "HTTP" deregistration_delay = 300 health_check { path = "/" healthy_threshold = 5 unhealthy_threshold = 2 timeout = 5 interval = 30 matcher = 200 port = "traffic-port" protocol = "HTTP" } depends_on = [aws_lb.example] }
- 更に、前回作成した、 HTTPS のリスナーにリスナールールを追加し、上で作成したターゲットグループに転送されるように設定します。
priority
: 優先順位を設定。数字が低いほど優先順位が高い。aws_lb_listener
で記述したdefault_rule
は最も優先順位が低い。
condition
: パスベースやホストベース等で条件を指定。
resource "aws_lb_listener_rule" "ecs" { listener_arn = aws_lb_listener.https.arn priority = 100 action { type = "forward" target_group_arn = aws_lb_target_group.ecs.arn } condition { path_pattern { values = ["/*"] } } }
適用と確認
- ここまで作成したら、適用します。
$ terraform fmt $ terraform get $ terraform validate $ terraform plan $ terraform apply
- 完了したら、
curl
かブラウザからアクセスし、 nginx のデフォルトが表示されることを確認します。
$ curl https://linkode-example.ml <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> ...
Fargate におけるログの設定
- Fargate では仮想マシンに直接ログインできないため、 CloudWatch Logs と連携し、ログが記録できるように設定する必要があります。
IAM ポリシーのモジュール化
- ECS に権限を付与するにあたって、 IAM ロールをモジュール化しておきます。
main.tf
の内容(クリックして展開)
resource "aws_iam_role" "default" { name = var.name assume_role_policy = data.aws_iam_policy_document.assume_role.json } data "aws_iam_policy_document" "assume_role" { statement { actions = ["sts:AssumeRole"] principals { type = "Service" identifiers = [var.identifier] } } } resource "aws_iam_policy" "default" { name = var.name policy = var.policy } resource "aws_iam_role_policy_attachment" "default" { role = aws_iam_role.default.name policy_arn = aws_iam_policy.default.arn }
output.tf
の内容(クリックして展開)
output "iam_role_arn" { value = aws_iam_role.default.arn } output "iam_role_name" { value = aws_iam_role.default.name }
variable.tf
の内容(クリックして展開)
variable "name" { type = string } variable "policy" { type = string } variable "identifier" { type = string }
. |-- alb.tf |-- ecs.tf |-- iam_role // 新規作成したディレクトリ | |-- main.tf | |-- output.tf | `-- variable.tf |-- main.tf |-- network.tf |-- route53.tf |-- s3.tf `-- security_group |-- main.tf |-- output.tf `-- variable.tf
ECS に付与する IAM ロールの定義
- 上で作成したモジュールを利用して、 ECS タスク実行 IAM ロールを定義します。
- ECS タスク実行 IAM ロールでの使用が想定されている AmazonECSTaskExecutionRolePolicy を参照します。
- 参照したポリシーを用いて、ポリシードキュメントを定義します。
- モジュールを利用する際の
identifier
にはecs-tasks.amazonaws.com
を指定し、この IAM ロールを ECS で使うことを宣言します。
data "aws_iam_policy" "ecs_task_execution_role_policy" { arn = "arn:aws:iam::aws:policy/service-role/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 = ["*"] } } 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 }
CloudWatch Logs の定義と Docker コンテナのロギング設定
- まず、CloudWatch Logs を以下のように定義します。
resource "aws_cloudwatch_log_group" "for_ecs" { name = "/ecs/example" retention_in_days = 180 }
- 先程の IAM の設定と上記 CloudWatch Logs の設定を
cloudwatch.tf
に記述して保存します。
. |-- alb.tf |-- clowdwatch.tf // 新規作成したファイル |-- ecs.tf |-- iam_role | |-- main.tf | |-- output.tf | `-- variable.tf |-- main.tf |-- network.tf |-- route53.tf |-- s3.tf `-- security_group |-- main.tf |-- output.tf `-- variable.tf
- 次に、Docker コンテナが CloudWatch Logs にログを投げられるようにします。
aws_ecs_task_definition
の設定を以下のように変更します。execution_role_arn
を追加し、上で設定した IAM ロールを付与します。- コンテナ定義の
logConfiguration
でログの設定を追加します。
resource "aws_ecs_task_definition" "example" { family = "linkode-example" cpu = "256" memory = "1024" network_mode = "awsvpc" requires_compatibilities = ["FARGATE"] container_definitions = jsonencode([ { "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 } ] } ]) execution_role_arn = module.ecs_task_execution_role.iam_role_arn // 新規追加した行 }
- ここまで記述できたら、適用します。
$ terraform fmt $ terraform get $ terraform validate $ terraform plan $ terraform apply
- 正しく設定できていれば、ヘルスチェックのログが CloudWatch Logs に飛びます。
まとめ
- ECS の環境を作成し、ALB とつなげることによって、外部からのリクエストを受けられるアプリケーション環境を構築することが出来ました。
- また、Docker コンテナのログを CloudWatch Logs に投げられるようにしたことで、 Fargate で稼働するコンテナのログも記録・確認出来るようになりました。
- 次回は、自作のコンテナイメージをデプロイするパイプラインを構築します。