はじめに

サービスディスカバリ設定をbridgeモードで行いたいなと思った時に世の中にはawsvpcモードのFargate記事ばかりかつ、bridgeモードはawsvpcモードと異なりAレコードを使えず、SRVレコードという初耳レコードのみ利用可能で、名前解決もよしなにやってくれないということで、名前解決方法などもわからず困っていました。
しかし、普通に実装できたので一応残しておこうと思います。
実装はPythonで行っています。

ENI制限なければ全てよしなにやってくれるawsvpcモード使えば良いですし、今回はECSインスタンスなのでプライベートIPでも通信できるため、需要少なそうですが。。。

探した感じこちらのRedisを取り上げたGoでのものしか見つかりませんでした。

Terraform構築も初なので、容赦お願いします。。。

構成

本当はもう少し複雑ですが、簡単な流れを構成として記載しました。

ECSインスタンスAサービスのコンテナ処理から、ECSインスタンスBサービスで動くコンテナのAPIにサービスディスカバリでアクセスしたいというものになります。
PythonでSRVレコードを処理して、PythonのAPIに接続しにいきます。

Route 53とCloud Mapへの登録などは、サービスディスカバリ設定で自動的に作成されます。

SRVレコードとは

詳細説明は割愛しますが、簡単にいうとサービスの場所の情報であるホスト名やポート、プロトコルなどを持っているレコードになります。

Terraform構築

アクセスしたいサービス側にサービスディスカバリ設定を行います。
今回ですと、ECSインスタンスBサービスになります。

ecs_cluster.tf

// プライベートDNSネームスペースの作成

resource "aws_service_discovery_private_dns_namespace" "ecs_namespace" {
name = "ecs.internal"
description = "Private DNS namespace for ECS services"
vpc = data.terraform_remote_state.common.outputs.vpc_id
}

// サービスディスカバリ設定

resource "aws_service_discovery_service" "service_b" {
name = "service_b"

dns_config {
namespace_id = aws_service_discovery_private_dns_namespace.ecs_namespace.id

dns_records {
ttl = 10
type = "SRV" // bridgeモードはAレコード使用不可のためSRVレコード指定
}

routing_policy = "MULTIVALUE"
}

health_check_custom_config {
failure_threshold = 1
}
}

ecs_service_b.tf

resource "aws_ecs_service" "service_b_containers" {
name = "${var.prefix}-containers"
cluster = aws_ecs_cluster.ecs-cluster.id
task_definition = aws_ecs_task_definition.service_b_containers.arn
desired_count = 1
launch_type = "EC2"
deployment_minimum_healthy_percent = 0
deployment_maximum_percent = 100
placement_constraints {
type = "memberOf"
expression = "attribute:TaskType == service_b"
}

# ... (省略)

// サービスの登録追加
service_registries {
registry_arn = aws_service_discovery_service.service_b.arn
container_name = "task_b_container" // タスク内のコンテナ名
container_port = 80 // 設定しているコンテナポート
}

# ... (省略)

SRVレコードを利用してサービス検出実装

こちらは、ECSインスタンスAサービスのタスクAのコンテナの実装内容となります。

aws_service_discovery_serviceで指定したnameとaws_service_discovery_private_dns_namespaceのnameを「.」繋ぎでアクセスできます。
今回の場合ですと、service_b.ecs.internal とすればSRVレコードへアクセスでき、
レコード情報が返却されます。

そこからポートを取得しIPアドレスを割り出して、取得します。
これを利用して、ECSインスタンスBサービスのタスクBのコンテナへアクセスできます。

import socket
import dns.resolver

def resolve_ookami_api_url():
try:
answers = dns.resolver.resolve('service_b.ecs.internal', 'SRV')
for rdata in answers:
hostname = rdata.target.to_text(omit_final_dot=True) // ホスト名取得
ip_address = socket.gethostbyname(hostname) // ホスト名をIPアドレスに変換
return f"http://{ip_address}:{rdata.port}" // URLとしてアプリ側で利用する
except (dns.exception.DNSException, socket.gaierror):
return http://xxxxxx:xxxx

最後に

今回はコンテナ数がかなり多くENIの上限引き上げしたとしても、引っかかってしまうためawsvpcモードではなくbridgeモードを利用しました。

基本的にはawsvpcモードでFargate使いたいなとは思いましたが、フルマネージド型に甘えない良い勉強の機会になりました。

参考文献

公式ドキュメント:サービス検出を使用して Amazon ECS サービスを DNS 名で接続する
ECSでService Discoveryを使ってみよう