AWSで特定のパスに対してIP制御を実現するパターン

## はじめに
[フューチャー Advent Calendar 2022](https://qiita.com/advent-calendar/2022/future)の16日目です。
昨日は[@tutuz](https://qiita.com/tutuz)さんの[技術記事執筆のススメ](https://qiita.com/tutuz/items/a2db0a78e5977b3d942b)でした。

AWSでアプリケーションに対してIP制御をかけたい、というニーズに応えようとすると、

- Security Group
- WAF
- etc...

といった感じで、いくつか出てきますが、これは言わずもがなそのインフラのアーキテクチャによって、向き不向きや利用の可否が変わってきます。
今年、AWSインフラの追加開発をやっていく中で、「特定のアプリケーションパスのみIP制御を実施したい」というニーズが生まれ、これについて短期間ながらいくつかパターンを試したので、その時のことを思い出していきながら書いていこうと思います。

## 前提
以下のアーキテクチャのような形で、フロントエンドからAPIという形でFargateを叩いています。

![arch.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382465/eb48a65d-beab-65c1-c23f-70b18703bf65.png)

## 特定のパスにIP制御をかけるパターン
今回、そもそものニーズとして、「リリースしたあと、動作確認を行なったあとにユーザ公開したい」というニーズがありました。
なので、実体のコンテンツはすでに存在しながらも、ユーザに見せない工夫が必要になります。
非常に短期間ながら、この要件をどうやって解決しようかと思い、以下のサービスに対してIP制御をかけることを試みました。

- WAF(今回はClassic)
- CloudFront
- ALB

これら3択の中から最終的に私が選択したのは**CloudFront**になるのですが、なぜその結論に至ったかと書いていきます。

### WAF
今回はWAF Classicを利用していることが前提となります。
WAF Classicでは最大で10個までRulesに設定することが可能です。
(参考：[ウェブ ACL あたりのルールの数](https://docs.aws.amazon.com/ja_jp/waf/latest/developerguide/classic-limits.html#:~:text=Marketplace%20%E3%83%AB%E3%83%BC%E3%83%AB%E3%82%B0%E3%83%AB%E3%83%BC%E3%83%97-,%E3%82%A6%E3%82%A7%E3%83%96%20ACL%20%E3%81%82%E3%81%9F%E3%82%8A%E3%81%AE%E3%83%AB%E3%83%BC%E3%83%AB%E3%81%AE%E6%95%B0,-10))

ACLのRulesに空きがあればこれで対応できたものの、これまでの対応で既に10個埋まってしまっていたこと、上限緩和もハードリミットなのでできず、というところで、WAFで対応することは諦めました。

### ALB

ALBでIP制限をかけることも可能です。
私が検討した方法は、ALBのリスナールールに対して許可IPからアクセスを受けたら裏側のコンテナにルーティングし、それ以外からは403を返すことでした。
ただ、今回の要件はフロントエンドに対して制御をかけたいので、そもそもALBでかけても要件が達成できないことに気が付き、他の方法を検討することにしました。

### CloudFront
最終的に選択することになったCloudFrontですが、どのように実装したのかについて書いていきます。
CloudFrontには、ビヘイビアという項目があり、あるパスに対してはどのようなルールでルーティングを行う、リダイレクトを行うといった、ユーザのアクセスに対して、適切なルーティングをおこなってくれます。今回触っているインフラについてはAPIごとにLBが分かれているため、このビヘイビアでパスとALBを紐づけていました。

このビヘイビアにはLambda(Lambda@Edge)やCloudFront Funtcionsを設定することができ、

- ビューワーリクエスト
- ビューワーレスポンス
- オリジンリクエスト
- オリジンレスポンス

の4つに対して設定することができます。イメージで言うと下のような図になります。

![request.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/382465/c8d6d914-c97d-c49f-00ea-4579a5035e04.png)

今回、**ユーザからのアクセス**に対して制限を行いたいので、ビューワーリクエストに対して設定することにしました。
また、Lambda@EdgeかCloudFront Funtcionsかの選択については、PJ内部でLambdaのソースが基本的にPythonで書かれていることや、急ぎで必要だったことからLambda@Edgeで開発することにしました。
早速ですが、Lambdaのソースになります。

```python
def ip_list(host):
    IP_LIST = ['xxx.xxx.xxx.xx', 'yyy.yy.yy.yy']
    return IP_LIST

def lambda_handler(event, context):

    request = event['Records'][0]['cf']['request']
    client_ip = event['Records'][0]['cf']['request']['clientIp']

    IP_WHITE_LIST = ip_list(request['headers']['host'][0]['value'])

    if client_ip in IP_WHITE_LIST:
        return request

    response = {
        'status': '403',
        'statusDescription': 'Forbidden',
        'headers': {
            'cache-control': [
                {
                    'key': 'Cache-Control',
                    'value': 'max-age=100'
                }
            ],
            "content-type": [
                {
                    'key': 'Content-Type',
                    'value': 'text/plain'
                }
            ],
            'content-encoding': [
                {
                    'key': 'Content-Encoding',
                    'value': 'UTF-8'
                }
            ]
        },
        'body': 'path ' + request['uri'] + ' is forbidden.'
    }
    return response
```

上記のLambdaは、`ip_list`で許可IPの一覧を作成し、アクセスしてきたIPが許可IPならそのままリクエストを通し、もしそれ以外のIPであれば403を返すようにしています。
期待するレスポンスはソースで簡単に変えられるのでニーズに応じて使い分けましょう。

今回はケースとしてなかったですが、環境ごと打ち分けたい場合も基本はソースの中にすべて埋め込む必要があるのが注意点です。
Lambda@Edgeは環境変数が使えないためです。

この他、追加しないといけないリソースとしてIAMなどがありますが、今回は割愛します。

## まとめ
AWSで特定のパスへのアクセス制御パターンはいつくかありますが、今回説明したCloudFront&Lambda@Edgeパターンも含め、比較的簡単に作りやすいものが多いのではないかというのが個人的な感触でした。
今回はリリース方式の検討を行った際に出てきた話なので、最低限の作りをしていますが、場合によってはもっと作り込みをしないといけないときもあるかと思いますので、いざというときのために実装パターンは準備しておくのはありだなと改めて思いました。