最近、経理とか労務とか総務とかのバックオフィス側のエンジニアになり、いわゆる社内の事務手続き的な作業を Slash Commands で効率化しようとしていまして、結局バックエンドは VPC Lambda を SQS 越しに呼び出す形になった、という過程のメモになります。
先にサンプルコードをみたい方はこちら
https://github.com/boiyaa/vpc-lambda-as-slash-command-backend
バックオフィス系システムは外からアクセスしにくい
Slash Commands のバックエンドは、 GAS とか GAE とかの上に置くのが、費用低くて手間もかからなくていいのですが、バックオフィスが扱うデータにはセンシティブな情報が多く、かつそういったシステムはレガシーな作りだったりするので、ネットワークの外に出ていなかったり、出ていてもIP制限がかかってたりします。
なので GAS や GAE などの PaaS や FaaS の類はネットワーク構成しないでいいのがウリのサービスなだけにネットワーク構成できないので逆に上記のようなシステムにアクセスできません。
ということで、 AWS の VPC の中に環境構築しています。
AWS の方にしたのは、 NAT でIP固定したり、拠点間VPN接続で社内ネットワークにつないだりできることに加えて、 Lambda を VPC 内に作れるのでVMほどセキュリティを担保するコストがかからなそうといった点で選択しました。
AWS公式ドキュメントのこちらの構成を2つのアベイラビリティーゾーンで構築しています。
シナリオ 2: パブリックサブネットとプライベートサブネットを持つ VPC (NAT)
API Gateway から直接 Lambda だと、 Slash Commands がタイムアウトする
Slash Commands には3000ms
以内にレスポンス返さないとタイムアウトエラーを表示する制約がある(参照)ということで、ENI作成のあるVPC Lambdaの初回起動にほぼ間に合いませんでした。
なので、 API Gateway から SQS に Slash Commands ペイロードを渡して一旦空レスポンスを返し、 Lambda は SQS をソースマッピングして処理するようにし、こちらの方法で 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)
- Request body passthrough:
これで 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()
- Content-Type:
Lambda 関数としてビルドするときのTypeScriptとバンドラの設定
今回は Node.js ランタイムを選択していて、素で書いてdevDependencies除くのが面倒なので、バンドラをつかいました。普段 webpack か Parcel を使いますが、今回は簡単な関数なので勉強がてらに Rollup 使ってみました。
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 で書きました。
{
"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 にアクセスして新しい App を作るか既存を選択するかして、 Slash Commands
のセクションで Command の Request URL
に上記URLを指定して、自分のワークスペースにインストールしたら設定完了です。
まとめ
- アプリケーションのコード書くことより社内システムに繋げるための環境構築の方が大変
- だいたい連携先のエンプラ系外部サービスはAPIが使いづらい
- こういった領域をやってるエンジニアとナレッジを共有しあいたい