Edited at

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

最近、経理とか労務とか総務とかのバックオフィス側のエンジニアになり、いわゆる社内の事務手続き的な作業を 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)

シナリオ 2 の図: パブリックサブネットとプライベートサブネットを持つ VPC


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)



これで 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 使ってみました。


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 で書きました。


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 にアクセスして新しい App を作るか既存を選択するかして、 Slash Commands のセクションで Command の Request URL に上記URLを指定して、自分のワークスペースにインストールしたら設定完了です。

Screen Shot 2019-05-22 at 18.56.02.png

コマンド実行してみます

Screen Shot 2019-05-22 at 18.56.57.png

返ってきました。

Screen Shot 2019-05-22 at 18.57.31.png


まとめ


  • アプリケーションのコード書くことより社内システムに繋げるための環境構築の方が大変

  • だいたい連携先のエンプラ系外部サービスはAPIが使いづらい

  • こういった領域をやってるエンジニアとナレッジを共有しあいたい