世間のリバースプロキシサービスが怖いのでワンライナーでAWS上に立てられるようにした

# きっかけ

Slack botの開発を始めたところ非常にやりにくかった。
手元のPCにSlackアプリがあり、自作のSlack botも起動している状態で、それがお互いに通信するにはSlackのサーバーを経由する必要があるからだ。
そうすると、Slackサーバーから手元のSlack botにアクセスできるようにしなければならない。

ということで、色々調べてみるとリバースプロキシサービスを使って開発している人が多いようだった。
例えば、ngrok, servo, localhost.runなどを使っている人がいた。
ただ、外部のサービスに社内のチャット内容とかトークンとか流したくない・・・と思ったので自分の管理下のAWS VPC内にリバースプロキシを立てることにした。

最終的にこんな感じで起動できるようになった。

```sh
$ yarn -s generate 2> /dev/null | aws ec2 run-instances --launch-template LaunchTemplateName=reverse-proxy --user-data file:///dev/stdin
```

# 出来ること

リバースプロキシで出来るようにしたこと。
実際はCaddyがやってくれてるので、自分では大したことしてない。

- **HTTPSのリバースプロキシになる**
HTTPSでアクセスでき上流にプロキシする。
ここで上流とはSlack bot = お手元のPCのこと。

- **自動でDNSにドメインを登録する**
起動時にDNSにドメインを自動登録する。
AWS VPC上に起動する前提なのでDNSはRoute53を使う。

- **自動でTLS証明書を取得する**
登録したドメインでLet's Encryptから証明書を取得する。
ここがCaddyの機能。

- **自動で停止する**
SSHのセッションが切れたら指定時間後に自動でターミネートする。
開発を終えてリモートポートフォワーディングを切断したら放っておけば停止する。
停止前に再接続するとカウンターがリセットされる。

# 遣り残したこと

- **NPM**
手元で作って使ってただけなのでNPMモジュールとして動かそうとしてなかった。

- **Let's Encryptからのお叱り**
何度も起動していると都度TLS証明書を取得してしまうので行儀が悪い。
EIPを割り当てればよさそうだが、今のところその前にDNS登録してしまいそう・・・

- **Slackサーバーからのリクエストの検証**
これはSlack限定の話だが、SlackサーバーからSlack botへのリクエストは `SigningSecret` で検証ができる。
Caddyのプラグインとしてリクエストの検証機能があれば、botを作らずともリバースプロキシだけ立てておいてエンドポイントのVerifyを通すことが出来るようになると思う。
あと、お手元のPCでリクエストの検証を行う危険も回避できる。

# 実際はただのuser-data

ここの `bin/generate.js` を見てもらうと分かる通り、実際はサーバーを実装したのではなくほぼCaddyにお任せのEC2インスタンス用user-dataを動的に生成するスクリプトである。

<a href="https://github.com/ryo0301/instant-reverse-proxy"><img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/379/918d80bc-269f-0c3d-ce4c-d4a067cc10f8.png" width="460px"></a>

cloud-init の `per-instance` でCaddyのインストールや起動設定を行い `per-boot` でRoute53にドメインをUPSERTしている。

# 使用方法

## 起動

NPMモジュールになっていないので `clone` しないといけない。

```sh
$ git clone https://github.com/ryo0301/instant-reverse-proxy
$ cd instant-reverse-proxy
```

`.env` に設定を書く。
`HOSTED_ZONE_DOMAIN` に設定するドメインはあらかじめRoute53に用意しておく。
Hosted Zone IDはこのドメイン名から逆引きしている。
残りは適宜設定。

ちなみに、Slack botのエンドポイントとしてSlack側に設定するURLはこんな感じ。
> https:// `HOST_NAME` . `HOSTED_ZONE_DOMAIN` /slack/events/

```sh
$ cat <<EOF > .env
HOST_NAME=$USER
HOSTED_ZONE_DOMAIN=proxy.example.com
CADDY_DL_URL=https://github.com/caddyserver/caddy/releases/download/v2.0.0/caddy_2.0.0_linux_amd64.tar.gz
SHUTDOWN_TIMER_HOUR=1
UPSTREAM_PORT=3000
LOCALE=ja_JP.UTF-8
TIMEZONE=Asia/Tokyo
EOF
```

これでuser-dataが出力されるはず。
これを手動で貼り付けてもいい。

```sh
$ yarn -s generate
```

また、起動するEC2インスタンスに付けるIAMロールも必要。
必要なアクションはこの2つ。

- route53:ChangeResourceRecordSets
- route53:ListHostedZonesByName

EC2のローンチテンプレートを使うと起動が楽になる。
テンプレートにはさきほどのIAMロールの設定など諸々設定しておく。
これで1行で起動できるようになった。

```sh
$ yarn -s generate 2> /dev/null | aws ec2 run-instances --launch-template LaunchTemplateName=reverse-proxy --user-data file:///dev/stdin
```

## 接続

### リモートポートフォワーディング

リバースプロキシが起動したらリモート→お手元のPCにポートフォワードする。
接続できなかったらEC2インスタンスの状態を見たりSSHでログインしてみて欲しい。

```sh
$ ssh -i ~/.ssh/id_rsa -l ec2-user i-XXXXXX -N -R 3000:localhost:3000
```

### SessionManagerを使う場合

普段はEC2 Instance ConnectとSessionManagerを使って接続している。
事前にSessionManager Pluginのインストールや `~/.ssh/config` に設定を追加する必要がある。

```sh
$ cat <<EOF >> ~/.ssh/config
Host i-* mi-*
  StrictHostKeyChecking no
  UserKnownHostsFile /dev/null
  ProxyCommand sh -c "aws ssm start-session --target %h --document-name AWS-StartSSHSession --parameters 'portNumber=%p'"
EOF
```

こちらはEC2 Instance Connectを使う場合。
公開鍵をEC2メタデータサービスに送ったあと、対象のEC2インスタンスに60秒間だけログイン可能になる。
送信に成功したら対応する秘密鍵でポートフォワードする。

```sh
$ ssh-keygen -t rsa -b 4096
$ aws ec2-instance-connect send-ssh-public-key --instance-id i-XXXXXX --instance-os-user ec2-user --availability-zone ap-northeast-1a --ssh-public-key file:///$HOME/.ssh/id_rsa.pub
```

# 最終的な開発環境

Slack botの開発環境は、AWSの構成も含めて最終的にこんな感じになった。
AWSの構成についてはこちらにまとめた。
[[AWS]踏み台不要、キーペア登録不要、ポート開放不要のSSH接続環境を作る | Qiita](https://qiita.com/ryo0301/items/1eb2ec1a8e867f561f45)

![reverse-proxy.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/379/0fadbc16-3876-4dda-0f07-0ceb92f05ec4.png)