はじめに
新卒研修で、コンテナアプリケーションを構築する機会がありました。
今回はWebサーバとAPIサーバをそれぞれECS Fargate上で動しています。
開発の中で、Next.jsのServer ActionやSSRからAPIサーバを呼び出す必要が出てきました。
WebサーバからAPIサーバへの内部通信をどう実現するかが今回の論点です。
結論として、AWS Cloud Mapを利用しました。
理由や具体的な設定方法は、以降で説明します。
想定読者
本記事の想定読者は以下です。
- ECS Fargate上のサービス間で内部通信を実現したい方
- 内部通信の選択肢(ALB、Service Connect、Cloud Mapなど)を比較検討したい方
技術スタック
- 言語:TypeScript
- パッケージ管理ツール:npm
- フレームワーク
- Webサーバ:Next.js
- APIサーバ:Hono
- クラウド:AWS(ECS Fargate、ALB、Cloud Map)
- IaC:Terraform
全体の流れ
以降では、次の順で内部通信の設計を説明します。
- 現状と目指す姿
- 課題と制約
- なぜAWS Cloud Mapを選んだのか
- どのように実現するか
- まとめ
現状と目指す姿
現状
これまでは、ALBがクライアントからのリクエストをパスに応じてWebサーバとAPIサーバへ振り分けていました。
これでは、WebサーバとAPIサーバが内部通信することはありません。
目指す姿
WebサーバからAPIサーバへ内部通信を今回新たに実現したいです。
簡単にいうと、下図の緑色の矢印線を追加することです。
課題と制約
課題
やりたいことは「WebサーバからAPIサーバを直接呼び出せるようにすること」です。
直接呼び出すには、相手の接続先を特定する必要があります。
接続先を特定する上での課題は、コンテナをデプロイするたびにIPアドレスが変化することです。
制約
次の制約のもとで課題を解決したいと考えました。
特に2つ目が重要です。
通信を実現するだけなら、既存ALBのパスルーティング(/api/*)に乗せるのが素直です。
今回は内部通信に留めたいため、既存ALBを経由するとAPIが公開されてしまいます。
なお、今回はスケーラビリティといった拡張性は考慮していません。
| 制約 | 理由 |
|---|---|
| 既存のALB・ネットワーク構成は維持する | 動いている構成を壊したくない |
| 内部通信は外向けALBを経由しない | 内部用APIまで外部に公開される |
| コストを抑えたい | 予算上限がある |
なぜAWS Cloud Mapを選んだのか
内部通信を実現するため、三つの方法を調査しました。
その結果を機能、既存構成への影響、コストを軸に表で整理しました。
最終的な決め手は、構成のシンプルさと、増加コストの小ささでした。
今回の要件だけであれば、AWS Cloud Map で十分です。
| 機能 | 既存構成への影響 | コスト | |
|---|---|---|---|
| 内部ALB追加 | ○ ルーティングが分かりやすい ヘルスチェックやパス制御可能 |
△ 内部ALBの新規追加が必要 |
× 既存ALBとあわせて固定費が二重にかかる |
| ECS Service Connect | ◎ 名前解決 リトライやロードバランシングが組み込み |
△ 各タスクにEnvoyサイドカーが追加される |
△ サイドカーの分だけFargateのコストが増える |
| AWS Cloud Map(採用) | △ プライベートDNS名で名前解決は可能 負荷分散やリトライはできない |
○ 既存ALBは変更不要 ECSサービスへの設定追加のみ |
◎ 追加コストがほぼゼロ (プライベートホストゾーンで月$0.5程度) |
どのように実現するか
手順1.セキュリティグループを設定する
WebサーバからAPIサーバへの通信を許可します。
セキュリティグループはデフォルトで受信を拒否するため、名前解決ができても通信できません。
backend(API)のセキュリティグループに、frontend(Web)からのアクセスを許可するルールを追加します。
# API 側のセキュリティグループ。frontend からの 8080 を許可する
resource "aws_security_group_rule" "backend_from_frontend" {
type = "ingress"
from_port = 8080
to_port = 8080
protocol = "tcp"
security_group_id = aws_security_group.backend.id
source_security_group_id = aws_security_group.frontend.id
}
手順2.AWS Cloud MapにプライベートDNS名前空間とサービスを作る
VPC内だけで利用できるプライベートDNSの名前空間を作ります。
ここでは example.local とし、APIサーバ用のサービスを backend として登録します。
これにより内部からは backend.example.local で名前解決できるようになります。
# VPC 内だけで名前解決できるプライベート DNS 名前空間(example.local)
resource "aws_service_discovery_private_dns_namespace" "main" {
name = "example.local"
vpc = aws_vpc.main.id
}
# backend.example.local を提供するサービスディスカバリサービス
resource "aws_service_discovery_service" "backend" {
name = "backend"
dns_config {
namespace_id = aws_service_discovery_private_dns_namespace.main.id
# Fargate(awsvpc)ではタスクごとにENIのIPが割り振られる。AレコードでそのIPを特定する
dns_records {
type = "A"
ttl = 10
}
routing_policy = "MULTIVALUE"
}
# ヘルスチェックは ECS 側の状態に任せる
health_check_custom_config {
failure_threshold = 1
}
}
手順3.ECSサービスを名前空間に登録する
既存のECSサービス定義に、service_registries ブロックを追加します。
ここが今回の追加差分で、ALBの設定には触れません。
resource "aws_ecs_service" "backend" {
# ... 既存の設定(cluster、task_definition、network_configuration など)はそのまま ...
# 追加ブロック. タスクを AWS Cloud Map に登録する
service_registries {
registry_arn = aws_service_discovery_service.backend.arn
}
}
最終的な内部通信の流れ
WebサーバがAPIサーバを呼び出すと、通信は次のように流れます。
-
名前解決 :Webサーバが
backend.example.localをCloud Mapに問い合わせる - IP応答 :Cloud Mapが稼働中タスクのIPを返す
- リクエスト :Webサーバが、取得したIPへリクエストを送り、APIサーバへ到達する
Webサーバは名前を呼ぶだけで、実際のIPを意識せずAPIサーバへ到達できます。
まとめ
本記事では、AWS Cloud Mapを使ったECSサービス間通信を紹介しました。
アクセスが少ない場合はCloud Mapを利用し、
アクセスが増加した場合はECS Service Connectを利用するとよいと分かりました。
また、調査を通し要件に対して機能が過剰であるとコストがかさむことも実感しました。
ここまで読んでいただきありがとうございました!