この記事は ハンズラボ Advent Calendar 2017 15日目の記事です。
まえおき
devとopsが異なるチームでopsが不定期にデータを抽出・集計したい時にopsが利用できるツールやアプリがあれば依頼が無くなりお互い幸せですよね
・・・というのはわかっていたものの中々腰が上がらなかったのですが、serverlessフレームワークを利用してみたかったので丁度よい題材という事で今回Slackのスラッシュコマンドを作成した話です。
Why スラッシュコマンド
SPAなどのWebアプリケーションを作成するのも一つの手ですが、Slackのスラッシュコマンドでどんなスラッシュコマンドを実行したのかも含めて結果を表示できれば、チームメンバーは他のメンバーがどういったコマンドを実行してタスクをこなしているのかを見る事が出来ます。
これにより他のメンバーも、使い方を理解してそのコマンドを使っていくことが出来ます。
背中を見て育つって寸法です。
立ちはだかる3秒タイムアウト
serverlessを利用してAWSという事で構成はAPI Gateway + Lambdaです。
今回始めてスラッシュコマンドを作成したのですが、スラッシュコマンドは3秒以内に応答を返さなければタイムアウトになってしまいますが、LambdaからDynamoやS3のデータを処理するにしても大体3秒以上かかるものです。
タイムアウトになってしまうと、スラッシュコマンドの実行者がどんなコマンドを実行したのかが、他の人には見えてきませんので、今回の目的としては3秒以内に応答を返す必要があります。
参考: https://api.slack.com/slash-commands
絶対3秒以上かかるんだけど、どうすんのってことですがSlackからのリクエストデータに response_url
パラメータがあり、このURLへデータを送信する事で3秒が経過した後にも応答を表示する事が出来ます。
これを利用して、API Gatewayに接続されているLambdaから他のLambda functionを非同期で呼び出し、3秒以内にレスポンスを返し、非同期実行されたfunction側で response_url
へ処理結果を返す形にすると目的が達成出来ます。
(3秒以内にコールバックを呼び出して処理を継続して response_url
に応答すればいいんじゃないかと思って最初実装していたのですが、Contextが終了しないとLambdaはAPI Gatewayに応答しなかったのでLambda functionを分けて非同期呼び出しにしてやる必要があります)
デフォルトでは、コールバックは Node.js ランタイムのイベントループが空になるまで待機してから
処理を停止し、呼び出し元に結果を返します。
図にするとこんな感じになります。
npm_modulesブラックホール
今回の構成だとAPI Gatewayから起動されるLambdaは他のLambdaを呼び出し、3秒以内にレスポンスを返すだけですので、ほぼnpmモジュールが必要ありませんし3秒以内に応答するためにコンテナの起動速度は可能な限り高速化したいのでサイズを小さくしたい欲求があります。
というわけで理想はそのfunctionにはnpmモジュール無しで、各function毎に必要なモジュールのみをひっぱってきてzipして欲しいのですが、serverlessフレームワークはデフォルトだと node_modules
をそのまま全functionにぶっこんできます。ひどい。
serverlessフレームワークのissueで過去に議論され、packageセクションに individually
オプションを追加し、各functionでinclude, excludeを追加できるような対応となっていました。ですが、最近の node_modules
のディレクトリ構成は階層がフラットなのでfunction毎に依存するモジュールだけを列挙するのは現実的ではありません。(そうじゃなくても現実的ではないです)
救世主webpack plugin
「各function毎に必要なモジュールを解決してまとめて欲しい」・・・そう、最早フロントエンドには欠かせないモジュールバンドラの出番ですね。
webpackでバンドルして、それをserverlessでアップロードして〜というタスクを書いてもいいんですが丁度良くserverlessのwebpackプラグインというのがあります。
individually: true
と共にserverless-webpackプラグインを利用すると各function handler毎に依存しているモジュールをバンドルしてくれます。これで各functionのpackageディレクティブには何も記述する必要はなくなりました。やったね!
plugins:
- serverless-webpack
package:
individually: true
functions:
index: # <- API Gatewayから呼ばれるfunc
handler: src/index/index.handler
description: "endpoint of /checkpos"
events:
- http:
path: checkpos
method: post
barcode: # <- indexからinvokeされるfunc
handler: src/subcommand/barcode.handler
memorySize: 832
timeout: 300
description: "barcod subcommand of /checkpos"
announce: # <- indexからinvokeされるfunc
handler: src/subcommand/announce.handler
memorySize: 384
timeout: 300
description: "announce subcommand of /checkpos"
aws-sdkバンドルしちゃう問題
必要なモジュールを解決してバンドルしてくれるのは良いのですが、AWS Lambdaはaws-sdkパッケージはLambda側に入っているのでバンドルする必要はありません。
はい!externalsですね!ここの設定に ["aws-sdk"]
と書いておけば除外されます。
参照: https://webpack.js.org/configuration/externals/
externals: ["aws-sdk"],
webpack使うんならLoaderでTypeScriptコンパイルしようぜ
デプロイ手法を tsc && serverless deploy
にするんでもいいんですが、どうせwebpack使うんなら ts-loader
でコンパイルさせようって思いますよね。はい!コチラです!
module: {
rules: [
{
test: /\.ts(x?)$/,
loader: "ts-loader",
},
],
},
tsconfig.json
のオプションにうっかり "allowJs": true
と書くとtsc
ではコンパイル通るのにts-loaderでは挙動が変わってビルドに失敗したりしました。全部TypeScriptで書けってお達しなんでしょうか。
完成!
完成です!コチラが設定ファイル群です!(Lambdaの実コードは特に面白い事はないので割愛)
各種設定パラメータをまとめたconfig.jsonがステージングの時と本番の時で異なるのでdeploy
とdeploy:prod
で分けています。
yarn run deploy
あるいは yarn run deploy:prod
でデプロイしてます。
{
"name": "slashcheckpos",
"version": "1.0.0",
"description": "",
"main": "handler.js",
"license": "ISC",
"private": true,
"scripts": {
"lint": "tslint -c tslint.json src/**/*.ts",
"lint:fix": "tslint -c tslint.json --fix src/**/*.ts",
"build": "tsc",
"build:watch": "tsc -w",
"config": "cp ../config.json.stg ./src/config.json",
"deploy": "yarn run config && sls deploy -v",
"config:prod": "cp ../config.json.prod ./src/config.json",
"deploy:prod": "yarn run config:prod && sls deploy -v --stage prod"
},
"dependencies": {
"axios": "^0.17.1",
"debug": "^3.1.0",
"luxon": "^0.2.3"
},
"devDependencies": {
"@types/aws-lambda": "0.0.21",
"@types/node": "^8.0.20",
"aws-sdk": "^2.157.0",
"json-d-ts": "^1.0.1",
"prettier": "^1.7.4",
"serverless": "^1.24.1",
"serverless-webpack": "^4.1.0",
"ts-loader": "^3.1.1",
"tslint": "^5.6.0",
"tslint-config-prettier": "^1.5.0",
"tslint-plugin-prettier": "^1.3.0",
"typescript": "^2.4.2",
"webpack": "^3.8.1",
"webpack-node-externals": "^1.6.0"
},
"prettier": {
"printWidth": 120,
"trailingComma": "all"
}
}
利用するaws cliプロファイルを分けています。本番の時はステージ変数を--stage
でprodに指定すれば諸々切り替わるcustom
セクションを書いてます。IAM権限はありもののRoleで良い感じのものがあるので、SAMで作成せずにそれを利用するようにしました。厳密に権限設定するならSAMで書いたほうがよいでしょう。
今回の構成上、index関数から別の関数をinvokeする必要があり名前が必要なのですが、serverlessによってLambda関数名が<service名>-<stage名>-<function名>
になるのでLambdaの環境変数としてservice名とstage名を設定するようにして取得しています。
service: slashcheckpos
frameworkVersion: ">=1.19.0 <2.0.0"
plugins:
- serverless-webpack
provider:
name: aws
runtime: nodejs6.10
stage: ${opt:stage, self:custom.defaultStage}
profile: ${self:custom.profiles.${self:provider.stage}}
role: ${self:custom.roles.${self:provider.stage}}
region: ap-northeast-1
memorySize: 128
timeout: 30
versionFunctions: false
environment:
service: ${self:service}
stage: ${self:provider.stage}
custom:
defaultStage: dev
profiles:
dev: hands-pos-stg
prod: hands-pos-prod
roles:
dev: arn:aws:iam::XXXXXXXXXXXX:role/backend
prod: arn:aws:iam::XXXXXXXXXXXX:role/backend
package:
individually: true
functions:
index:
handler: src/index/index.handler
description: "endpoint of /checkpos"
events:
- http:
path: checkpos
method: post
barcode:
handler: src/subcommand/barcode.handler
memorySize: 832
timeout: 300
description: "barcod subcommand of /checkpos"
announce:
handler: src/subcommand/announce.handler
memorySize: 384
timeout: 300
description: "announce subcommand of /checkpos"
webpack.
全く追ってないですがserverless-webpackのlib.entries
でhandlerをリストアップしてくれるんだろうと思ってます。
const path = require("path");
const slsw = require("serverless-webpack");
module.exports = {
entry: slsw.lib.entries,
resolve: {
extensions: [".js", ".json", ".ts", ".tsx"],
},
output: {
libraryTarget: "commonjs",
path: path.join(__dirname, ".webpack"),
filename: "[name].js",
},
externals: ["aws-sdk"],
target: "node",
module: {
rules: [
{
test: /\.ts(x?)$/,
loader: "ts-loader",
},
],
},
};
まとめ
serverless
開発のAWSアカウントでテストした後に本番のAWSアカウントにデプロイして、スラッシュコマンドの向き先を本番のAPI Gatewayに変更したあとは開発側の構成リソースが不要になるので、serverlessフレームワーク使っていると sls remove
でまとめてサクッと削除できて便利でした。
VSCode
Atomも好きなんですがTypeScript書くならVSCodeだな、となりました。