こんにちは、つくぼし(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を作成します。
ecs.tf
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を選択します。
container_definitions.json
[
{
"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を指定します。
ecs.tf
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
で指定したポート名を入力します。
ecs.tf
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
ファイルを別途作成しておきます。
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)でした!