API Gateway HTTP APIのCloudMap統合でディスカバリー&ロードバランシング

API Gateway HTTP APIとCloudMapのプライベート統合についての記事が、あまり見つからなかったので書くことにした。

HTTP APIにはプライベート統合という機能があり、NLB/ALB/CloudMap経由でVPC内のサーバーにリクエストをお届けできる。
ELBのようにリクエストを仲介する形ではなく、CloudMapは振り分け先の選別に利用されるだけ。
[HTTP API のプライベート統合の使用 | Amazon API Gateway開発者ガイド](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/http-api-develop-integrations-private.html)

# 検証環境

検証環境は、ECS/Fargate環境にPHPで `$_SERVER` の内容を表示するだけのコンテナを3台配置した。また、ECSのサービス検出を利用してCloudMapにFargateタスクの情報を登録する形にした。

HTTP APIのルートは3つ。

- `GET /`
無条件に全てのタスクが対象になる。

- `GET /leader`
CloudMapのカスタム属性で `leader = true` に設定したタスク1台が対象になる。

- `GET /follower`
CloudMapのカスタム属性で `leader = false` に設定したタスク2台が対象になる。

Fargateタスク起動 → サービス検出への登録 → HTTP APIへのリクエストの流れを図示すると、このような感じ。

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/379/0f2713a0-c81e-64e4-609d-9fe056ea5c0c.png)

1. Fargateタスク起動
1. CloudMapサービスインスタンスの登録
1. Route53にSRVレコードを登録
1. インターネットからHTTP APIにアクセス
1. HTTP APIが `CloudMap:DiscoverInstances` APIを実行
1. VPC Link経由でFargateタスクにリクエストを送信
1. 対象のFargateタスクの中からロードバランシング

ここでのRoute53の役割りがよく分かっていないが、ECSコンソールからサービス検出の設定をした場合、DNS&APIでなくAPIだけのCloudMapサービスは選択ができなかった。

※ 検証に使用したDockerイメージはこちらにプッシュした。
https://hub.docker.com/r/ryo0301/php-env

# 動作検証

先に検証結果だけ書く。検証した内容は下記の通り。

## クエリー動作の検証

CloudMapへのクエリー処理を何パターンか試した。
図で言うと (5) の部分。

- クエリーの結果、対象Fargateタスクが0の場合
- クエリーの結果、対象Fargateタスクが1つの場合
- クエリーの結果、対象Fargateタスクが2つ以上の場合

### 対象Fargateタスクが0の場合

HTTP APIがCloudMapにクエリーした結果、対象のFargateタスクが見つからない場合は 500 Internal Server Error になる。
リクエストの振り先がないので、HTTP APIがエラーを返しておしまい。

### 対象タスクが1つの場合

当たり前のように対象のFargateタスクでリクエストが処理される。

### 対象タスクが2つ以上ある場合

2つ以上対象のFargateタスクがあると、その中でロードバランシングされる。
ラウンドロビンっぽい動き。

### ヘルスチェックの影響

CloudMapはサービスインスタンス(ここではFargateタスク)ごとに健全性のステータスを持てるので、ヘルスチェックを有効にしている場合は、そのステータスも考慮される。

今回は、すぐ「異常」になることもあってムカついたので、ヘルスチェックを無効化したCloudMapサービスを作成した上で、ECSサービスから利用することにした。
無効化した場合は常に「正常」なサービスインスタンスとして扱われる。

ちなみに、CloudMapが勝手に登録するRoute53のヘルスチェックは、ECSサービスやCloudMapサービスを削除してもしばらくは消えない。
手動でも消せないが、寝て起きた頃には消えてる。

## CloudMapサービスインスタンスの属性値変更の検証

サービスインスタンスの属性が更新され、クエリー結果が変わった時の動作を検証した。
クエリーAだけにヒットする属性値から、クエリーBだけにヒットする属性値に変更する。

サービスインスタンスの更新は非同期に行われるので、更新完了を確認してからHTTP APIにリクエストを投げた。
更新完了直後は、しばらくクエリーAとBのどちらにもヒットするようになってしまった。
30秒くらい経つと、クエリーBにのみヒットするようになった。

## CloudMapサービスインスタンスの追加/削除

サービスインスタンス自体を追加/削除した場合、どちらもCloudMapの更新が完了した時点で即時反映された。
更新時間は数秒なので、属性を変更するよりサービスインスタンスを削除→追加したほうが適切な要件もあるかもしれない。

# CloudMapへの登録について

検証環境ではECSのサービス検出を利用したので、FargateタスクのCloudMapへの登録などは自動的に行われた。
もし、EC2インスタンスなどを振り分け先に指定したい場合は、自前の処理でCloudMapにIPアドレスとポートさえ登録すれば動作する。

## CloudMapのインスタンス検出方式

インスタンス検出方式とは、CloudMapに登録されたリソースの情報を取得するための方法。
APIでCloudMapにクエリーを投げてリソースのIDを取得したり、Route53で名前解決してIPアドレスやポートを取得したり。

CloudMapには3種類のインスタンス検出方式があるが、そのどれでもHTTP APIとのプライベート統合は動作する。
（**API 呼び出しと公開 DNS クエリ** は試してないけど多分）
HTTP APIは `CloudMap:DiscoverInstances` を呼んでるだけっぽい。

- API 呼び出し
- API 呼び出しと VPC の DNS クエリ
- API 呼び出しと公開 DNS クエリ

[AWS Cloud Mapへの問い合わせ方式 | Developers.IO](https://dev.classmethod.jp/articles/cloud-map-perfect/#toc-3)

## CloudMapへのIPアドレスとポートの登録

ECSのサービス検出を利用してIPアドレスとポートを登録する場合は、CloudMapのインスタンス検出方式に **API 呼び出しと VPC の DNS クエリ** を選択した上で、SRVレコードを登録するよう設定しないといけない。Aレコードはサポートされていない模様。
Aレコードだとポートの情報がないから？

自前でIPアドレスとポートを登録する場合、AWSが予約しているキーを使って登録する。
なぜかCloudMapの開発者ガイドには定数の一覧とか何も書いてない。
インスタンス検出方式は、DNSを利用しないのであれば **API呼び出し** でいい。

- **AWS_INSTANCE_IPV4**
IPv4アドレスを登録する時は、このキーを使う。

- **AWS_INSTANCE_PORT**
ポートはこちらのキーで登録する。

他の予約キーは、こちらを参照。
[register-instance | AWS CLI v1](https://docs.aws.amazon.com/cli/latest/reference/servicediscovery/register-instance.html)
[register-instance | AWS CLI v2](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/servicediscovery/register-instance.html)

# 使いみち

どこで使えるんだろう・・・

1つ思いついたのは、RaftクラスターでLeaderにだけリクエストを投げたい、とか。
要するに、管理用のAPIを作る時に使えそうだと思った。
CloudMapを使う以上、高負荷なAPIには向いてないだろうし。

そうすると、複数台にリクエストをパブリッシュするような仕組みがあっても面白いかも。
今のところラウンドロビンで、どれか1台に振られてしまうので。

残念ながら、HTTP API は REST API とは違って、プライベートAPIエンドポイントを作成することが出来ない。
なので、管理用APIを作るにしても無駄にインターネットに公開されてしまうのはいただけない。