AWS App Runnerプライベートサービスにインターネットからアクセスし、WAFで保護する

本記事は、[AWS App Runner Advent Calendar 2022](https://qiita.com/advent-calendar/2022/aws-app-runner)の18日目の記事です。

App Runnerなにもわからんマンです。

AWSでコンテナベースのアプリケーションを開発する場合、
なんらかのコンテナオーケストレーションサービスを選定すると思います。

AWSでの選択肢は、ECSやEKS、App Runnerに、ROSAなんかもありますね。

App Runnerについては、正直「Production用途にはまだまだ」、ぐらいのぼんやりとしたイメージしかなかったので、2022年末時点での状況を調べておこうと思いました。

## お世話になったブログ記事等
https://go-to-k.hatenablog.com/entry/2022/02/13/035248

こちらのブログ、2022年2月に書かれた記事なのですが、
App RunnerとECSの違いがわかりやすく纏めてあり、
且つ2月以降のアップデートについても追記してあります！(すごい！)
本当に勉強になりました。ありがとうございました。

https://aws.amazon.com/jp/blogs/news/deep-dive-on-aws-app-runner-private-services/

こちらのクラスメソッドの[Takuya Shibata](https://dev.classmethod.jp/author/shibata-takuya/)さんの記事もとても勉強になりました。
2022年11月にリリースされた[App Runnerのプライベートサービス化](https://aws.amazon.com/jp/blogs/news/deep-dive-on-aws-app-runner-private-services/)のブログ、設定手順等も丁寧に書かれており、すごく実装イメージがわきました。ありがとうございました。

これで、App Runnerについては完全に理解した（気分になった)。

## ペインポイント
App Runner、プライベートサービス化がサポートされたことで、それまでの「**サービスはインターネットに全公開(アクセス制限の機能なし)**」の状態が回避できるようになったことは喜ばしいです。

喜ばしいですが・・・

プライベートサービス化は要するに「**VPCの中だけで閉じて使えますよ**」と理解しました。もう一声ほしいところです。

**App Runnerはプライベート化しつつ、なんとかしてアプリをWAFで保護しつつインターネットに公開できないか。**

その謎を解明すべく、我々調査隊はアマゾンの奥地へと向かった。

## 対応策案

### 案1: え？普通にELBとか経由して公開できないの？⇒失敗
App RunnerでPrivate endpointを有効化すると、自分たちで用意したVPCにVPC Endpointを生やすことができます。
![app_runner_proxy_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281143/2f7412be-b0cf-03ad-dd0c-f969baa40b68.png)
さらに作成したApp Runnerのサービスのドメイン名は

https://xxxxxxxxxx.ap-northeast-1.awsapprunner.com

のようなドメインが割り当てられますが、
このドメインがプライベートIPを返すようになるので、ELBからそこにアクセスすればええやんと思いました。

図にすると以下のとおりです。
![app_runner_proxy_03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281143/240b0790-35fc-4c5f-e9fa-9f44324644c3.png)

で、設定してみたのですが・・・

ALBのTarget Groupを設定しても、TargetはUnhealthyのままですね。

検証用VPCにEC2を立ててcurlしてみます。

VPC内で名前解決はできてます。
```
$ nslookup
> xxxxxxxxxx.ap-northeast-1.awsapprunner.com
Server:         10.0.0.2
Address:        10.0.0.2#53

Non-authoritative answer:
Name:   xxxxxxxxxx.ap-northeast-1.awsapprunner.com
Address: 10.0.1.240
Name:   xxxxxxxxxx.ap-northeast-1.awsapprunner.com
Address: 10.0.2.150
```

App RunnerのURLにcurlすると、VPC内から普通にアクセスできます。
※今回は、App Runnerのイメージとして[ECR Public Gallery](https://gallery.ecr.aws/nginx/nginx)のpublic.ecr.aws/nginx/nginx:stable-alpine を使いました。
```
$ curl https://xxxxxxxxxxx.ap-northeast-1.awsapprunner.com
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
（中略）
</body>
</html>
```

プライベートIPにcurlすると、TLSのセッション開始の途中で失敗していることがわかります。そりゃそうか。
```
$ curl https://10.0.1.240
curl: (35) OpenSSL SSL_connect: Connection reset by peer in connection to 10.0.1.240:443
```
単純にHostヘッダーつけてもダメな模様。そりゃそうか。
```
$ curl -H 'Host:xxxxxxxxxx.ap-northeast-1.awsapprunner.com' https://10.0.1.240/curl: (35) OpenSSL SSL_connect: Connection reset by peer in connection to 10.0.1.240:443
```

Target Groupの問題なので、NLBもダメそう。ELBはあきらめた。

### 案2: LambdaでApp Runnerにアクセスして結果をAPI Gatewayに返す⇒できた

じゃあ、LambdaをVPCにアクセスできるようにして、リバースProxy的に動かせばいいのでは？と考えました。

こういうイメージ。
![app_runner_proxy_05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281143/a72a1a12-b49b-f001-adec-adc0db278242.png)


#### Lambdaを書く
疎通できればいいので、細かいところはご容赦を。

例えば"apprunner-proxy"というLambda関数を書きます。
環境変数URLには、App RunnerのURLを設定して下さい。
```
from urllib import request
import urllib
import os
import json

def get_apprunner(event, context):
    apprunner_url = os.environ['URL']
    
    try:
        request_get = request.Request(url=apprunner_url)
        with request.urlopen(request_get) as res:
            content = res.read().decode()
        return {
            'statusCode': 200,
            'body': json.dumps(content),
            'isBase64Encoded': False,
            'headers' : {}
        }
    
    except (urllib.error.HTTPError) as error:
        status_code = error.code
        print(status_code)
```

#### LambdaをVPCに接続する
Lambda関数のConfiguration=>VPCから、接続するVPCと、Security Groupを設定します。

設定したVPCにENIが生えます。
![app_runner_proxy_06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281143/d58dfc62-675d-61c7-fc0a-f41df034da46.png)

#### Lambdaをテストする
Test eventはデフォルトのEvent JSONで作成し、Lambda関数をテスト実行します。

![app_runner_proxy_07.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281143/6946772e-7aa7-ece4-ebdf-19c2dbe176b6.png)

うまくいったようです。
Response bodyにnginxのコンテナから返されたHTMLが含まれています。

App Runnerのマネジメントコンソールの画面、Application Logsにて、Lambda(Python-urllib)からのアクセスを確認できます。
ソースIPは、わたしが作った検証用VPCでは使ってないアドレスなので、おそらくApp Runner内部のリクエストルーターのIPアドレス？ではないかと。

![app_runner_proxy_08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281143/1a5901c9-0a54-569a-69a5-48ea4479e3a1.png)

#### APIを作成する
Lambda関数からApp Runnerにアクセスできればもうこっちのもんです。
API GatewayでREST APIを作成しましょう。

Actionsから、Create Method。
![app_runner_proxy_09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281143/f4958aa2-399c-a102-28d4-b5329c5acdda.png)

GETリソースを作成し、

Integration typeはLambda Function、
Use Lambda Proxy integrationはチェック、
Lambda Functionには作成したLambda関数名(今回はapprunner-proxy)

を入力し、Saveします。

ActionsからDeploy API、
Deployment stageは[New Stage]、
Stage nameはdevとかstgとか設定し、Deployします。
![app_runner_proxy_10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281143/add53a53-557e-0329-a873-72486d5bf28b.png)

stgを作成すると、Invoke URLが発行されます。
ブラウザで、インターネット経由でアクセスしてみましょう。
![app_runner_proxy_11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281143/704032b9-7e38-61fa-6af7-ea827dc9bafe.png)

bodyに、nginxのコンテナから返されたHTMLが含まれています。
![app_runner_proxy_12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/281143/baf8e59d-74c6-b206-e857-c6bdf7c6b0f5.png)

#### WAFを設定する(IP sets)
今の状態だと、App RunnerプライベートサービスにAPI Gateway経由でインターネットから誰でもアクセスできてしまいます。

最低限、アクセス元のソースIPアドレスは絞りたいところです。

この手順、がんばって書こうと思いましたが、クラメソさんのブログに丁寧に解説してくださっている記事がありましたので、ご紹介いたします。
IP rulesを活用してソースIPアドレスの制限を行いましょう。
(手抜きですいません・・)

https://dev.classmethod.jp/articles/how-to-use-aws-waf-v2-to-filter-incoming-traffic-based-ip-address/

## というわけで
かなり無理矢理感のあるやり方で、AWS App Runnerプライベートサービスにインターネットからアクセスしてみました。

今回は疎通検証のための簡易なコードだったり設定ですので、(大丈夫と思いますが)Productionでの利用を検討されている場合はキチンと各種セキュリティや可用性、パフォーマンス系の設計・実装・テストをしましょう。

API GatewayがあるのでCloudFrontとも連携できますが、CloudFrontを利用するぐらいのアクセス数のあるユースケースだと、Lambdaがボトルネックになりそうです。
そういう場合は、素直にECS/Fargateでアプリを稼働させた方がよいかと。

App Runner、コンテナオーケストレーション部分が隠蔽されて、気軽にアプリを公開できる非常に面白いサービスだと思います。

Security GroupやWAFのネイティブ対応だったり、利用できるスペックの組み合わせが増えると、かなりユースケースも増えてくるんじゃないかと思うので、今後のRoadmapに注目しましょう！！！

https://github.com/aws/apprunner-roadmap/issues