プライベートリソースにアクセスする Slash Commands バックエンドの実装

最近、経理とか労務とか総務とかのバックオフィス側のエンジニアになり、いわゆる社内の事務手続き的な作業を [Slash Commands](https://api.slack.com/slash-commands) で効率化しようとしていまして、結局バックエンドは VPC Lambda を SQS 越しに呼び出す形になった、という過程のメモになります。

先にサンプルコードをみたい方はこちら
https://github.com/boiyaa/vpc-lambda-as-slash-command-backend


## バックオフィス系システムは外からアクセスしにくい

Slash Commands のバックエンドは、 [GAS](https://developers.google.com/apps-script/) とか [GAE](https://cloud.google.com/appengine/) とかの上に置くのが、費用低くて手間もかからなくていいのですが、バックオフィスが扱うデータにはセンシティブな情報が多く、かつそういったシステムはレガシーな作りだったりするので、ネットワークの外に出ていなかったり、出ていてもIP制限がかかってたりします。

なので GAS や GAE などの PaaS や FaaS の類はネットワーク構成しないでいいのがウリのサービスなだけにネットワーク構成できないので逆に上記のようなシステムにアクセスできません。

ということで、 AWS の [VPC](https://aws.amazon.com/vpc/) の中に環境構築しています。
AWS の方にしたのは、 NAT でIP固定したり、拠点間VPN接続で社内ネットワークにつないだりできることに加えて、 [Lambda](https://aws.amazon.com/jp/lambda/) を VPC 内に作れるのでVMほどセキュリティを担保するコストがかからなそうといった点で選択しました。

AWS公式ドキュメントのこちらの構成を２つのアベイラビリティーゾーンで構築しています。
[シナリオ 2: パブリックサブネットとプライベートサブネットを持つ VPC (NAT)](https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/VPC_Scenario2.html)
![シナリオ 2 の図: パブリックサブネットとプライベートサブネットを持つ VPC](https://docs.aws.amazon.com/ja_jp/vpc/latest/userguide/images/nat-gateway-diagram.png)


## API Gateway から直接 Lambda だと、 Slash Commands がタイムアウトする
Slash Commands には`3000ms`以内にレスポンス返さないとタイムアウトエラーを表示する制約がある([参照](https://api.slack.com/slash-commands#responding_basic_receipt))ということで、ENI作成のあるVPC Lambdaの初回起動にほぼ間に合いませんでした。

なので、 API Gateway から SQS に Slash Commands ペイロードを渡して一旦空レスポンスを返し、　Lambda は SQS をソースマッピングして処理するようにし、[こちらの方法](https://api.slack.com/slash-commands#responding_response_url)で Slack にメッセージ送信するようにしました。


## API Gateway から SQS に Slash Commands のペイロードを送り、空レスポンスを返す設定が分かりづらい
Slash Commands の場合は application/x-www-form-urlencoded でリクエストしてくるので、以下のように設定しました。

##### Integration Request
- Mapping Templates
  - Request body passthrough: `Never`
  - Content-Type: `application/x-www-form-urlencoded`
  - Template: `Action=SendMessage&QueueUrl=$util.urlEncode('${aws_sqs_queue.slash_commands.id}')&MessageBody=$util.urlEncode($input.body)`

これで SQS からレスポンスがくるのですが、そのまま返してしまうと Slack にメッセージが出てしまうので、何も出さないように以下の設定をしました

##### Method Response
- HTTP Status: `200`
- Content-Type: `application/json`
- Models: `Empty`

##### Integration Response
- Method response status: `200`
- Mapping Templates
  - Content-Type: `application/json`
  - Models: `#stop()`


## Lambda 関数としてビルドするときのTypeScriptとバンドラの設定
今回は Node.js ランタイムを選択していて、素で書いてdevDependencies除くのが面倒なので、バンドラをつかいました。普段 webpack か Parcel を使いますが、今回は簡単な関数なので勉強がてらに [Rollup](https://github.com/rollup/rollup) 使ってみました。

```js:rollup.config.js
import commonjs from "rollup-plugin-commonjs";
import json from "rollup-plugin-json";
import typescript from "rollup-plugin-typescript";
import resolve from "rollup-plugin-node-resolve";

export default {
  input: "./src/main.ts",
  output: {
    file: "./dist/main.js",
    format: "cjs"
  },
  plugins: [commonjs(), json(), typescript(), resolve()]
};
```

コードは [TypeScript](https://www.typescriptlang.org/) で書きました。

```json:tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".",
    "esModuleInterop": true,
    "lib": [
      "es2015",
      "es2016",
      "es2017",
      "es2018",
      "esnext",
      "dom"
    ],
    "module": "commonjs",
    "outDir": "dist",
    "skipLibCheck": true,
    "sourceMap": true,
    "strict": true,
    "target": "es2018"
  }
}
```

関数の処理の流れは、

- Lambda イベントから SQS レコード を取り出す
- SQS レコード の body に API Gateway で設定した `$util.urlEncode($input.body)` の形で Slash Commands ペイロードが入っているので、オブジェクトにパースする
- ペイロードの `command` に応じてハンドルする
- 結果をペイロードの `response_url` にリクエストする

という感じになります。


## デプロイ
コマンド実行までの一連の流れはこんな感じになります。

今回のソース
https://github.com/boiyaa/vpc-lambda-as-slash-command-backend

こちらのREADMEの通りにLambda関数をビルドして、Terraformを実行すると、APIのURLとNATのIPアドレスがでます。

```
api_url = https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod
nat_ips = [
    111.111.111.111,
    222.222.222.222
]
```

このIPは Lambda から外部へリクエストした時のアドレスになりますので、IP制限のあるシステムにこちらを追加します。

APIのURLは、Slash Commands のバックエンドに使用します。
Slack の [Your Apps](https://api.slack.com/apps) にアクセスして新しい App を作るか既存を選択するかして、 `Slash Commands` のセクションで Command の `Request URL` に上記URLを指定して、自分のワークスペースにインストールしたら設定完了です。
<img width="548" alt="Screen Shot 2019-05-22 at 18.56.02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142668/b6e8cfb5-2231-86ef-8958-436128c8859c.png">


コマンド実行してみます
<img width="1201" alt="Screen Shot 2019-05-22 at 18.56.57.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142668/cc973522-52a8-5881-011e-0e7b19427ad7.png">

返ってきました。
<img width="1198" alt="Screen Shot 2019-05-22 at 18.57.31.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/142668/dc27f45c-9ec4-c37b-6d04-c8991600c56c.png">


## まとめ
- アプリケーションのコード書くことより社内システムに繋げるための環境構築の方が大変
- だいたい連携先のエンプラ系外部サービスはAPIが使いづらい
- こういった領域をやってるエンジニアとナレッジを共有しあいたい