ECS Service ConnectをTerraformでデプロイしてみた
こんにちは、つくぼし(tsukuboshi0755)です!
皆さんは、re:Invent期間中に発表された、ECSでのマイクロサービス間通信に関する新機能であるService Connectはご存知でしょうか。
本機能により、ECSサービス間の通信を、従来より簡単に設定できるようになっています。
今回はこのService Connectについて、既にTerraformでもリリースされているので実装してみたいと思います!
概要
1つのCloudMap名前空間に、クライアント用/サーバー用のNginxコンテナをそれぞれ配置するAWS構成を、Terraformコードでデプロイします。
また動作確認の手順としては、クライアント側のECSサービスに対してECS Execで接続した後、サーバー側のECSサービスに対してcurlコマンドでリクエストを送信し確認してみます。
なお、デプロイツールにTerraformを使用する点以外は、こちらのCDK記事とほぼ同様の実施内容となります。
Terraformコードのポイント
名前空間
Service Connectを使用する場合、Terraformでは事前にCloudMap名前空間を作成する必要があります。
CloudMap名前空間は、aws_service_discovery_http_namespaceリソースで作成できます。
今回は新たにtest-namespaceを作成します。
resource "aws_service_discovery_http_namespace" "namespace" { name = "test-namespace" }
タスク定義
ECSタスク定義用のJSONコンテナ定義ファイルにおいて、PortMapping
に対し、Service Connect用の追加設定が必要になります。
name
には、Service Connectで使用するポート名を入力する必要があります。
今回はwebserverとしています。
appProtocol
では、Service Connectプロキシ用のプロトコルを"tcp/http/grpc"のいずれかから選択できます。
今回はhttpを選択します。
[ { "name": "nginx-container", "image": "nginx:latest", "essential": true, "memory": 128, "portMappings": [ { // ServiceConnectのポート名で必要 "name": "webserver", "protocol": "tcp", "containerPort": 80, // ServiceConnectプロキシ用のプロトコルを、"tcp/http/grpc"のいずれかで指定可能 "appProtocol": "http" } ], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "/ecs/nginx", "awslogs-region": "ap-northeast-1", "awslogs-stream-prefix": "nginx" } } } ]
サービス(Service Connect設定)
ECSサービスを設定するaws_ecs_serviceリソースに対して、service_connect_configuration
の追記が必要になります。
クライアント側ECSサービスについては、以下の通りとなります。
初めに、Service Connect機能をONにするために、enabled
をtrueで指定します。
次に、Service Connectで作成されるEnvoyコンテナのログ設定を、log_configuration
に記載します。(項目はJSONコンテナ定義のlogConfiguration
フィールドとほぼ同義です)
最後に、namespace
に事前に作成したCloudMap名前空間のARNを指定します。
resource "aws_ecs_service" "client" { name = "nginx-client" ... service_connect_configuration { enabled = true log_configuration { log_driver = "awslogs" options = { awslogs-group = "/ecs/svccon-client" awslogs-region = "ap-northeast-1" awslogs-stream-prefix = "svccon-client" } } namespace = aws_service_discovery_http_namespace.namespace.arn } }
サーバー側ECSサービスについては、クライアント側ECSサービスの設定項目に加えて、service
の設定が必要になります。
client_alias
では、Service Connectプロキシで使用するポート番号を指定します。
今回は80を指定します。
port_name
では、JSONコンテナ定義におけるportMappings
フィールドのname
で指定したポート名を入力します。
resource "aws_ecs_service" "server" { name = "nginx-server" ... service_connect_configuration { enabled = true log_configuration { log_driver = "awslogs" options = { awslogs-group = "/ecs/svccon-server" awslogs-region = "ap-northeast-1" awslogs-stream-prefix = "svccon-server" } } namespace = aws_service_discovery_http_namespace.namespace.arn ## 追加設定 service { client_alias { port = 80 } port_name = "webserver" } } }
なお細かい箇所ですが、log_configuration
で指定したCloudWatch Log Groupは自動では作成されないため、このままですとコンテナが起動できません。
options
のawslogs-create-group
をtrueにする事でもLog Groupを作成できますが、今回はCloudWatchLog用のcwlogs.tf
ファイルを別途作成しておきます。
resource "aws_cloudwatch_log_group" "log_nginx" { name = "/ecs/nginx" } # クライアント側ECSサービス用ServiceConnectロググループ resource "aws_cloudwatch_log_group" "log_connect_client" { name = "/ecs/svccon-client" } # サーバー側ECSサービス用ServiceConnectロググループ resource "aws_cloudwatch_log_group" "log_connect_server" { name = "/ecs/svccon-server" }
動作確認
上記のポイントを踏まえた上でTerraformコードを作成し、ECSをデプロイした後、ECSサービス間通信が実現できているか動作確認します。
それではクライアント側のECSサービスにECS Execでログインします。
The Session Manager plugin was installed successfully. Use the AWS CLI to start a session. Starting session with SessionId: ecs-execute-command-0c49e0c07efac4988 #
クライアント側のECSサービスで、サーバー側のECSサービスに対してcurlコマンドでリクエストを送信します。
# curl http://webserver.test-namespace:80 <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> <style> html { color-scheme: light dark; } body { width: 35em; margin: 0 auto; font-family: Tahoma, Verdana, Arial, sans-serif; } </style> </head> <body> <h1>Welcome to nginx!</h1> <p>If you see this page, the nginx web server is successfully installed and working. Further configuration is required.</p> <p>For online documentation and support please refer to <a href="http://nginx.org/">nginx.org</a>.<br/> Commercial support is available at <a href="http://nginx.com/">nginx.com</a>.</p> <p><em>Thank you for using nginx.</em></p> </body> </html>
問題なく接続できました!
なお/etc/hosts
の中身を見ると、サーバー側のECSサービスに関するDNS名が書き込まれている事が分かります。
# cat /etc/hosts 127.0.0.1 localhost 10.0.1.220 ip-10-0-1-220.ap-northeast-1.compute.internal 127.255.0.1 webserver.test-namespace 2600:f0f0:0:0:0:0:0:1 webserver.test-namespace
Terraformコード全体
最後に、今回AWSにデプロイしたTerraformコード全体を記載しておきます。
$ tree . ├── container_definitions.json ├── cwlogs.tf ├── ecs.tf ├── iam.tf ├── provider.tf ├── sg.tf ├── version.tf └── vpc.tf
container_definitions.json
[ { "name": "nginx-container", "image": "nginx:latest", "essential": true, "memory": 128, "portMappings": [ { "name": "webserver", "protocol": "tcp", "containerPort": 80, "appProtocol": "http" } ], "logConfiguration": { "logDriver": "awslogs", "options": { "awslogs-group": "/ecs/nginx", "awslogs-region": "ap-northeast-1", "awslogs-stream-prefix": "nginx" } } } ]
cwlogs.tf
# ロググループ resource "aws_cloudwatch_log_group" "log_nginx" { name = "/ecs/nginx" } resource "aws_cloudwatch_log_group" "log_connect_client" { name = "/ecs/svccon-client" } resource "aws_cloudwatch_log_group" "log_connect_server" { name = "/ecs/svccon-server" }
ecs.tf
# タスク定義 resource "aws_ecs_task_definition" "task" { family = "nginx-task" cpu = "256" memory = "512" network_mode = "awsvpc" task_role_arn = aws_iam_role.task_role.arn execution_role_arn = aws_iam_role.execution_role.arn requires_compatibilities = ["FARGATE"] container_definitions = file("./container_definitions.json") } # クラスター resource "aws_ecs_cluster" "cluster" { name = "test-cluster" } # 名前空間 resource "aws_service_discovery_http_namespace" "namespace" { name = "test-namespace" } # サービス(クライアント側) resource "aws_ecs_service" "client" { name = "nginx-client" cluster = aws_ecs_cluster.cluster.arn task_definition = aws_ecs_task_definition.task.arn desired_count = 1 launch_type = "FARGATE" platform_version = "1.4.0" enable_execute_command = true network_configuration { assign_public_ip = true security_groups = [aws_security_group.sg.id] subnets = [aws_subnet.public_1.id, aws_subnet.public_2.id] } service_connect_configuration { enabled = true log_configuration { log_driver = "awslogs" options = { awslogs-group = "/ecs/svccon-client" awslogs-region = "ap-northeast-1" awslogs-stream-prefix = "svccon-client" } } namespace = aws_service_discovery_http_namespace.namespace.arn } } # サービス(サーバー側) resource "aws_ecs_service" "server" { name = "nginx-server" cluster = aws_ecs_cluster.cluster.arn task_definition = aws_ecs_task_definition.task.arn desired_count = 1 launch_type = "FARGATE" platform_version = "1.4.0" enable_execute_command = true network_configuration { assign_public_ip = true security_groups = [aws_security_group.sg.id] subnets = [aws_subnet.public_1.id, aws_subnet.public_2.id] } service_connect_configuration { enabled = true log_configuration { log_driver = "awslogs" options = { awslogs-group = "/ecs/svccon-server" awslogs-region = "ap-northeast-1" awslogs-stream-prefix = "svccon-server" } } namespace = aws_service_discovery_http_namespace.namespace.arn service { client_alias { port = 80 } port_name = "webserver" } } }
iam.tf
# ECSタスクロール data "aws_iam_policy_document" "ecs_task_doc" { statement { effect = "Allow" principals { type = "Service" identifiers = ["ecs-tasks.amazonaws.com"] } actions = ["sts:AssumeRole"] } } resource "aws_iam_role" "task_role" { name = "nginx-task-role" assume_role_policy = data.aws_iam_policy_document.ecs_task_doc.json } data "aws_iam_policy_document" "ecs_exec_doc" { ## SSMサービス関連のアクセス許可 version = "2012-10-17" statement { effect = "Allow" actions = [ "ssmmessages:CreateControlChannel", "ssmmessages:CreateDataChannel", "ssmmessages:OpenControlChannel", "ssmmessages:OpenDataChannel" ] resources = ["*"] } } resource "aws_iam_policy" "ecs_exec_policy" { name = "AmazonECSExecPolicy" policy = data.aws_iam_policy_document.ecs_exec_doc.json } resource "aws_iam_role_policy_attachment" "task_attachement" { role = aws_iam_role.task_role.name policy_arn = aws_iam_policy.ecs_exec_policy.arn } # ECSタスク実行ロール resource "aws_iam_role" "execution_role" { name = "nginx-execution-role" assume_role_policy = data.aws_iam_policy_document.ecs_task_doc.json } resource "aws_iam_role_policy_attachment" "execution_attachement" { role = aws_iam_role.execution_role.name policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" }
provider.tf
# プロバイダー情報 provider "aws" { region = "ap-northeast-1" }
sg.tf
# デフォルトセキュリティグループ(デフォルトルール削除のため) resource "aws_default_security_group" "dfsg" { vpc_id = aws_vpc.vpc.id } # ECSサービス用セキュリティグループ resource "aws_security_group" "sg" { name = "ecs-sg" description = "ecs-sg" vpc_id = aws_vpc.vpc.id tags = { Name = "nginx-sg" } } resource "aws_security_group_rule" "ingress" { type = "ingress" from_port = 80 to_port = 80 protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] security_group_id = aws_security_group.sg.id } resource "aws_security_group_rule" "egress" { type = "egress" from_port = 0 to_port = 0 protocol = -1 cidr_blocks = ["0.0.0.0/0"] security_group_id = aws_security_group.sg.id }
version.tf
# バージョン情報 terraform { required_version = ">=1.0.0" required_providers { aws = { source = "hashicorp/aws" version = "~> 4.0" } } }
vpc.tf
# VPC resource "aws_vpc" "vpc" { cidr_block = "10.0.0.0/16" enable_dns_hostnames = true enable_dns_support = true tags = { Name = "test-vpc" } } # サブネット resource "aws_subnet" "public_1" { vpc_id = aws_vpc.vpc.id cidr_block = "10.0.0.0/24" map_public_ip_on_launch = true availability_zone = "ap-northeast-1a" tags = { Name = "test-public-subnet-1" } } resource "aws_subnet" "public_2" { vpc_id = aws_vpc.vpc.id cidr_block = "10.0.1.0/24" map_public_ip_on_launch = true availability_zone = "ap-northeast-1c" tags = { Name = "test-public-subnet-2" } } # インターネットゲートウェイ resource "aws_internet_gateway" "igw" { vpc_id = aws_vpc.vpc.id tags = { Name = "test-igw" } } # ルートテーブル resource "aws_route_table" "rtb" { vpc_id = aws_vpc.vpc.id tags = { Name = "test-public-rtb" } } resource "aws_route" "tf_route_igw" { route_table_id = aws_route_table.rtb.id gateway_id = aws_internet_gateway.igw.id destination_cidr_block = "0.0.0.0/0" } resource "aws_route_table_association" "associate_1" { route_table_id = aws_route_table.rtb.id subnet_id = aws_subnet.public_1.id } resource "aws_route_table_association" "associate_2" { route_table_id = aws_route_table.rtb.id subnet_id = aws_subnet.public_2.id }
最後に
今回はECS Service ConnectをTerraformで実装してみました。
ECSでのマイクロサービス間通信を実現する上で、Service ConnectはELB/Service Discoveryに代わる有力な選択肢になり得ます。
ECSでのマイクロサービス間通信設定に悩んでいる方は、ぜひService Connectの導入を検討してみてはいかがでしょうか。
なお、他の記事でAWSマネジメントコンソール/CDK/Copilot CLIでのService Connect設定方法が紹介されているので、ぜひご参照ください。
以上、つくぼし(tsukuboshi0755)でした!