API Gateway経由でのみアクセス可能なAPIをECSで構築する

API Gatewayでのみパブリックアクセス可能なAPIをECSで構築する方法です。構成はこの図のようになります。

![Untitled Diagram-5.png](https://qiita-image-store.s3.amazonaws.com/0/43355/79426b50-35e3-d6db-8e2e-9372ed84c644.png)

ECSのロードバランサにはApplication Load Balancer(ALB)の方を使用することも多いと思いますが、この図のNetwork Load Balancer(NLB)をALBに変えることはできません。API GatewayからVPC内のプライベートAPIに接続するにはVPC Linkを介する必要があり、VPC LinkはNLBにしか対応していないためです。

NLBだとALBのように細かい制御はできませんが、そこはAPI Gatewayの側で行えば問題ないでしょう。


----

## 前提ECSタスク

タスクに登録されているコンテナの定義は以下のようになっており、

<img src="https://qiita-image-store.s3.amazonaws.com/0/43355/acc8a805-da8f-5255-17d7-b16daa484ee7.png" width="400">

`http://サーバ名:8080/hello`

にGETでアクセスすると `Hello3` と返すだけのものです。

----

## NLBの作成

最初にロードバランサーを作成します。

### ロードバランサーの設定

「スキーム」に「内部」を指定し、サブネットもプライベートサブネットを指定することで、内部向けNLBとして作成します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/43355/2e94f85d-20b3-e304-4ed7-ffed0e0b5796.png)

API GatewayからNLBまでの通信が保護されているという明確な情報が見つけられなかったため、この手順ではリスナーは一応TLSにしておきます。べつに通常のTCPリスナーでも問題ないかもしれません。通常のTCPリスナーなら、ドメイン登録やTLS証明書の取得は不要になります。

### セキュリティ設定の構成

TLSの場合、あとでNLBに割り当てるドメイン名に対応する証明書を割り当てておきます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/43355/029c7008-a00c-706c-823e-1e9bd245ae65.png)

この例では、`*.app.{秘密}.net` のワイルドカード証明書を指定しました。

### ルーティングの設定

ルーティングの設定では、ターゲットの種類を `IP`、プロトコルを `TCP`、ポートを転送先のポート（この例では `8080`）にしておきます。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/43355/2a62e117-2305-3ff5-f31e-f2b4923c802c.png)

### ターゲットの登録

ターゲットは何も登録しません。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/43355/32b04fde-3c22-0042-be90-21550910cbbb.png)


### 作成完了

エラーが出なければOKです。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/43355/2307d935-4eea-634e-c46d-cd23d0ae8406.png)

### NLBをDNSに登録

NLBをDNSに登録します。Route 53 を使う場合は、`Create Record Set` からAliasとして登録します。

<img src="https://qiita-image-store.s3.amazonaws.com/0/43355/6bfc303d-0b2c-2b7e-35a0-70cb451401a9.png" width="450">

この例では、`app-1.app.{秘密}.net` ドメインをNLBに割り当てました。

----

## ECSの設定

ECSコンソールから、対象のサービスを作成します。

### サービス設定

このあたりは何でもかまいません。例ではFargateを使用しています。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/43355/53875a7d-fb81-978c-8eab-729c378188d1.png)

![image.png](https://qiita-image-store.s3.amazonaws.com/0/43355/24ccd750-2489-22dd-9f3d-5f10d7c7f32b.png)

### ネットワーク構成

サブネットはプライベートを選択します。今回の構成とは関係ありませんが、ECSインスタンスは外部にアクセスできる必要があるため[^1]、パブリックIPを`DISABLED`にする場合は、NATゲートウェイなどが別途必要です。

[^1]: https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ECS_instances.html _"コンテナインスタンスには、Amazon ECS サービスエンドポイントと通信するために外部ネットワークアクセスが必要なため、コンテナインスタンスにパブリック IP アドレスがない場合は、NAT (ネットワークアドレス変換) を使用してこのアクセスを提供する必要があります。"_

![image.png](https://qiita-image-store.s3.amazonaws.com/0/43355/f273be8a-9268-89ba-1734-6feded9cd065.png)

セキュリティグループの設定では、NLBのターゲットが`IP`なので、NLBのIPアドレス範囲からアクセス可能なように制限を行います。[^2]

[^2]: https://docs.aws.amazon.com/ja_jp/elasticloadbalancing/latest/network/load-balancer-target-groups.html#target-type _"ターゲットを IP アドレスで指定する場合、送信元 IP アドレスはロードーバランサノードのプライベート IP アドレスとなります｡"_

![image.png](https://qiita-image-store.s3.amazonaws.com/0/43355/f9d77704-518f-b70c-8d11-c7f5b66ab75e.png)


ELBタイプでNetwork Load Balancerを選択します。

<img src="https://qiita-image-store.s3.amazonaws.com/0/43355/da048dc0-8b2b-2f3c-d844-08e03dfa285a.png" width="650">

ELB名で作成したNLBを選択し、アクセス振り分け先のコンテナを選択して「ELBへの追加」ボタンを押します。

<img src="https://qiita-image-store.s3.amazonaws.com/0/43355/a43b1757-f1f2-f4c9-0d3c-dfa9d7486b4a.png" width="650">

ターゲットグループ名で、作成済みのターゲットグループを選択します。

<img src="https://qiita-image-store.s3.amazonaws.com/0/43355/98268147-d7f0-56c1-ac08-43621ef16440.png" width="600">

他の項目は勝手に設定されます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/43355/7994eb76-87d7-b965-f979-578427e90115.png" width="600">

### 作成完了

後の項目は任意で設定し、サービスを登録します。タスクが正常に起動すればOKです。

<img width="1058" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/43355/04fa0cf7-4e49-2da5-7a2f-c7427ddfa01f.png">

プライベートネットワーク内からは、NLBに割り当てたドメインでAPIを呼び出すことができるようになります。

<img src="https://qiita-image-store.s3.amazonaws.com/0/43355/26054991-ec13-947f-036d-699ef1a3f9c0.png" width="350">

----

## API Gatewayの設定

NLBは外部からアクセスできないため、この時点では外部からAPIにアクセスできません。API Gatewayを経由して外部からアクセスできるように設定を行います。

### VPC Linkの作成

API Gatewayのコンソールの「VPC リンク」メニューから、作成したNLBを対象とするVPCリンクを作成します。

<img src="https://qiita-image-store.s3.amazonaws.com/0/43355/fbb5513c-185a-1583-1dfe-c66f131e65dc.png" width="600">

ステータスが「利用可能」になるのを待ちます。数分時間がかかります。

<img src="https://qiita-image-store.s3.amazonaws.com/0/43355/9092571e-e215-28a2-8837-da6025607b79.png" width="450">


### APIメソッドの設定

API GatewayのAPIメソッドの設定で、NLB経由でECSタスクにアクセスするよう設定します。

「統合タイプ」で「VPC リンク」を選択し、「VPC リンク」で作成したVPCリンクを選択します。そして、エンドポイントURLに `https://{NLBに割り当てたドメイン}/{APIのパス}` を指定します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/43355/f3cd7047-5b23-2991-94f8-d6ed01bfdeae.png)

設定したら、デプロイして有効化します。

<img src="https://qiita-image-store.s3.amazonaws.com/0/43355/e380bddd-9982-762a-1d9d-5bd0ce3d026f.png" width="500">

発行された呼び出しURLを確認します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/43355/9ba56981-030d-22ef-bd5f-424f804deaf1.png)

----

## 呼び出し確認

`curl`コマンドなどで、発行された呼び出しURLにアクセスしてみます。

<img src="https://qiita-image-store.s3.amazonaws.com/0/43355/d91c2f0a-86ce-7071-834c-5e42165aa7dc.png" width="600">

----

## どうしてもALBでロードバランスしたい場合

API Gateway経由からしかアクセスさせたくないが、どうしてもNLBではなくALBでロードバランスしたい場合に考えられるやり方です。

### 内部向けALBの場合

以下で紹介されているように NLB->ALB の構成とし、ALBのIPアドレス変更ごとにNLBの宛先を変更するようにします。

https://qiita.com/naomichi-y/items/9840400e7833175d706b

ただ、AWSのブログには「プロダクションで使う前に必ずテストしてね！」と書いてあるので、本当にちゃんと安定して動くのかはよくわかりません…。

### 公開ALBの場合

この場合、公開ALBを、API Gatewayのみからアクセス可能にする必要があります。2019年3月時点では、以下の方法が考えられます。

- API Gateway からバックエンドの公開ALBへのリクエストに、`X-API-Key: {ランダムで推測不可能な文字列}` のように適当なHTTPヘッダで認証情報をセットします。
- ALB側でそのHTTPヘッダの値をチェックし、API Gateway側にセットした文字列と同じ文字列が無ければアクセスを拒否するようにします。以下の方法があります。
  - ALBのルール「HTTPヘッダー」で、値がAPI Gateway側にセットした文字列と一致した場合のみECSサービスにリクエストを転送するようにします。例：
    ![image.png](https://qiita-image-store.s3.amazonaws.com/0/43355/2923c8c4-9643-4b32-80bf-2534404b5eb0.png)
  - ALBにはAWS WAFが設定できるので、WAFで「ヘッダ `X-API-Key` がAPI Gateway側にセットした文字列と一致した場合のみアクセスを許可する」ルールを設定します。

認証情報を渡すので、HTTPS前提です。API全体を単一のプレーンテキストで守り、AWSコンソール上から値が丸見えなので、あんまり安全な感じもしません。