Serverless Frameworkを使ってAWS LambdaとAPI Gatewayを試した

サーバーレスでアプリケーションを構築するためのNode.jsのフレームワークServerlessを使って、
AWS LambdaAmazon API GatewayをつかってSlack Botを試してみたときのメモ。

SlackのメッセージをAPI GatewayにPOSTして、AWS Lambdaで処理をして、webhookを使ってslackに投稿する。

AWS CLIを端末(Mac)にインストールする

なにはともあれ、AWSのコマンドラインインターフェイスを使うので端末にインストールする。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py"
sudo python get-pip.py

The directory '/Users/YOU/Library/Caches/pip/http' or its parent directory is not owned by the current user and the cache has been disabled. Please check the permissions and owner of that directory. If executing pip with sudo, you may want sudo's -H flag.
The directory '/Users/YOU/Library/Caches/pip' or its parent directory is not owned by the current user and caching wheels has been disabled. check the permissions and owner of that directory. If executing pip with sudo, you may want sudo's -H flag.
Requirement already up-to-date: pip in /Library/Python/2.7/site-packages/pip-8.1.1-py2.7.egg
Collecting wheel
Downloading wheel-0.29.0-py2.py3-none-any.whl (66kB)
100% |████████████████████████████████| 71kB 1.3MB/s
Installing collected packages: wheel
Successfully installed wheel-0.29.0

sudo pip install awscli

aws configure
AWS Access Key ID [None]: XXXXXXXXXXXXXXXXXXX
AWS Secret Access Key [None]: xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Default region name [None]: tokyo
Default output format [None]: json

ls -la ~/.aws
total 16
-rw------- 1 YOU staff 39 5 1 08:32 config
-rw------- 1 YOU staff 116 5 1 08:32 credentials

IAM(Identity and Access Management)ユーザーとポリシーの作成

AWSのダッシュボードの IAM からユーザーを作成する。
今回はserverless-admin という名前で作成する。

認証情報は後ほど必要なので認証情報はダウンロードしておく。

続いてポリシーをアタッチする。

IAMページのポリシーを選択し、AdministratorAccess を選んでアタッチする。

Serverlessのインストールとプロジェクト作成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
node -v
v5.10.1

npm i -g serverless

serverless
_______ __
| _ .-----.----.--.--.-----.----| .-----.-----.-----.
| |___| -__| _| | | -__| _| | -__|__ --|__ --|
|____ |_____|__| \___/|_____|__| |__|_____|_____|_____|
| | | The Serverless Application Framework
| | serverless.com, v0.5.5
`-------'

Commands
* Serverless documentation: http://docs.serverless.com
* You can run commands with "serverless" or the shortcut "sls"
* Pass "--help" after any <context> <action> for contextual help

"project" actions:
create
install
init
remove

"function" actions:
run
create
deploy
logs
remove
rollback

"endpoint" actions:
deploy
remove

"event" actions:
deploy
remove

"dash" actions:
deploy
summary

"stage" actions:
create
remove

"region" actions:
create
remove

"resources" actions:
deploy
remove
diff

"plugin" actions:
create

"variables" actions:
list
set
unset

You can run commands with “serverless” or the shortcut “sls” とあるように
serverlessslsでaliasされているとのこと。

つづいて、プロジェクトの作成を行う。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
sls project init
_______ __
| _ .-----.----.--.--.-----.----| .-----.-----.-----.
| |___| -__| _| | | -__| _| | -__|__ --|__ --|
|____ |_____|__| \___/|_____|__| |__|_____|_____|_____|
| | | The Serverless Application Framework
| | serverless.com, v0.5.5
`-------'

Serverless: Initializing Serverless Project...
Serverless: Enter a name for this project: (serverless-b1ntox) serverless-greeints
Serverless: Enter a new stage name for this project: (dev)
Serverless: For the "dev" stage, do you want to use an existing Amazon Web Services profile or create a new one?
> Existing Profile
Create A New Profile
Serverless: Select a profile for your project:
> default
Serverless: Creating stage "dev"...
Serverless: Select a new region for your stage:
us-east-1
us-west-2
> eu-west-1
eu-central-1
ap-northeast-1
Serverless: Creating region "eu-west-1" in stage "dev"...
Serverless: Deploying resources to stage "dev" in region "eu-west-1" via Cloudformation (~3 minutes)...
Serverless: Successfully deployed "dev" resources to "eu-west-1"
Serverless: Successfully created region "eu-west-1" within stage "dev"
Serverless: Successfully created stage "dev"
Serverless: Successfully initialized project "serverless-greeints"

プロジェクトを作成すると以下のようになる。

1
2
3
4
5
6
7
8
9
10
11
tree -L 2
.
│ 
└── serverless-greeints
├── _meta
│   ├── resources
│   └── variables
├── admin.env
├── package.json
├── s-project.json
└── s-resources-cf.json

プロジェクト毎に次のようなs-project.jsonが作成される。

1
2
3
4
5
{
"name": "serverless-greeints",
"custom": {},
"plugins": []
}

admin.envにはAWSのAccess Keysのパスが書き込まれる。
初期値はdefaultとなり、$HOME/.awsを参照する。

1
AWS_DEV_PROFILE=default

_meta/variablesにはプロジェクトごとのstageやregionなどの設定ファイルがjsonで保持されている。
たとえばstateがdev、reagionがus:westなら該当する設定ファイルはs-variables-dev-uswest.jsonとなる。

s-resources-cf.jsonはCloudFormationのテンプレートファイル。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "The AWS CloudFormation template for this Serverless application's resources outside of Lambdas and Api Gateway",
"Resources": {
"IamRoleLambda": {
"Type": "AWS::IAM::Role",
"Properties": {
"AssumeRolePolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"lambda.amazonaws.com"
]
},
"Action": [
"sts:AssumeRole"
]
}
]
},
"Path": "/"
}
},
"IamPolicyLambda": {
"Type": "AWS::IAM::Policy",
"Properties": {
"PolicyName": "${stage}-${project}-lambda",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:${region}:*:*"
}
]
},
"Roles": [
{
"Ref": "IamRoleLambda"
}
]
}
}
},
"Outputs": {
"IamRoleArnLambda": {
"Description": "ARN of the lambda IAM role",
"Value": {
"Fn::GetAtt": [
"IamRoleLambda",
"Arn"
]
}
}
}
}

_metaディレクトリはデフォルトで作成される.gitignoreに記載されているため、基本的にgit管理されないようになっている。

functionを触ってみる

API Gateway、Lambdaで実行されるfunctionを作成します。

1
2
3
4
5
6
7
8
9
10
11
cd serverless-greeints
sls function create functions/hello
Serverless: Please, select a runtime for this new Function
nodejs4.3
python2.7
> nodejs (v0.10, soon to be deprecated)
Serverless: For this new Function, would you like to create an Endpoint, Event, or just the Function?
> Create Endpoint
Create Event
Just the Function...
Serverless: Successfully created function: "functions/hello"
1
2
3
4
5
6
tree -L 2 functions
functions
└── hello
├── event.json
├── handler.js
└── s-function.json

Amazon API Gatewayのマッピングテンプレートとなるs-function.jsonは以下のようになる。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
{
"name": "hello",
"runtime": "nodejs",
"description": "Serverless Lambda function for project: serverless-greeints",
"customName": false,
"customRole": false,
"handler": "handler.handler",
"timeout": 6,
"memorySize": 1024,
"authorizer": {},
"custom": {
"excludePatterns": []
},
"endpoints": [
{
"path": "hello",
"method": "GET",
"type": "AWS",
"authorizationType": "none",
"authorizerFunction": false,
"apiKeyRequired": false,
"requestParameters": {},
"requestTemplates": {
"application/json": ""
},
"responses": {
"400": {
"statusCode": "400"
},
"default": {
"statusCode": "200",
"responseParameters": {},
"responseModels": {
"application/json;charset=UTF-8": "Empty"
},
"responseTemplates": {
"application/json;charset=UTF-8": ""
}
}
}
}
],
"events": [],
"environment": {
"SERVERLESS_PROJECT": "${project}",
"SERVERLESS_STAGE": "${stage}",
"SERVERLESS_REGION": "${region}"
},
"vpc": {
"securityGroupIds": [],
"subnetIds": []
}
}

実際にfunctionを実行する。

ここは本来はAPI Gateway側で実行されるのだが、Serverlessではローカルとリーモートどちらでも手元から実行できるようになっている。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# stageを選択してないのでlocalで実行
sls function run hello
Serverless: Running hello...
Serverless: -----------------
Serverless: Success! - This Response Was Returned:
Serverless: {
"message": "Go Serverless! Your Lambda function executed successfully!"

# stageを選択してdeployed stageで実行
sls function run hello -s dev
Serverless: Running hello...
Serverless: WARNING: This variable is not defined: region
Serverless: -----------------
Serverless: Success! - This Response Was Returned:
Serverless: {
"message": "Go Serverless! Your Lambda function executed successfully!"
}

試したところで、AWSにデプロイする。
今回はダッシュボードとなるdashをデプロイする。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
 sls dash deploy
_______ __
| _ .-----.----.--.--.-----.----| .-----.-----.-----.
| |___| -__| _| | | -__| _| | -__|__ --|__ --|
|____ |_____|__| \___/|_____|__| |__|_____|_____|_____|
| | | The Serverless Application Framework
| | serverless.com, v0.5.5
`-------'

Use the <up>, <down>, <pageup>, <pagedown>, <home>, and <end> keys to navigate.
Press <enter> to select/deselect, or <space> to select/deselect and move down.
Press <ctrl> + a to select all, and <ctrl> + d to deselect all.
Press <ctrl> + f to select all functions, and <ctrl> + e to select all endpoints.
Press <ctrl> + <enter> to immediately deploy selected.
Press <escape> to cancel.


Serverless: Select the assets you wish to deploy:
hello
function - hello
endpoint - hello - GET
- - - - -
> Deploy
Cancel

Serverless: Deploying the specified functions in "dev" to the following regions: eu-west-1
Serverless: ------------------------
Serverless: Successfully deployed the following functions in "dev" to the following regions:
Serverless: eu-west-1 ------------------------
Serverless: hello (serverless-greeints-hello): arn:aws:lambda:eu-west-1:0000000000:function:serverless-greeints-hello:dev

Serverless: Deploying endpoints in "dev" to the following regions: eu-west-1
Serverless: Successfully deployed endpoints in "dev" to the following regions:
Serverless: eu-west-1 ------------------------
Serverless: GET - hello - https://xxxxxxxxx.execute-api.eu-west-1.amazonaws.com/dev/hello

最後のエンドポイントが表示されているので、実際にリクエストしてみる。
レスポンスが帰って来ればOK。

1
2
curl https://xxxxxxxxx.execute-api.eu-west-1.amazonaws.com/dev/hello
{"message":"Go Serverless! Your Lambda function executed successfully!"}

functionを編集する

一通り触ってみたので、実際にfunctionを編集していく。
まずs-functions.jsonを以下のように変更する。
ちなみにAPI GatewayではVTL(Velocity Template Language)記法が使われる。

マッピングに使用できる変数などはマッピングテンプレートリファレンスにまとまっている。

1
2
3
4
-      "requestTemplates": {},
+ "requestTemplates": {
+ "application/json": "{\"message\": \"$input.params('message')\"}"
+ },

これでjson形式のリクエストでmessageを受け取ることができる。

Lambdaで実行する処理を書いていく。

handler.jsを編集する。

1
2
3
4
5
6
7
8
'use strict';

module.exports.handler = function(event, context) {
var message = event.message || 'Good bye';
return context.done(null, {
message: 'Hello ' + message + ' !'
});
};

単純にリクエストを待ち受けて、簡単なメッセージを返す。
先ほどrequestTemplatesを修正したので、messageを受け取ることができる。

手元でfunctionを実行する。

1
2
3
4
5
6
7
sls function run hello
Serverless: Running hello...
Serverless: -----------------
Serverless: Success! - This Response Was Returned:
Serverless: {
"message": "Hello Good bye !"
}

また、リクエストされるmessageを渡してfunctionを実行するには、
event.jsonにリクエストを追加する。

1
{ "message": "World" }

再度、ローカルでfunctionを実行する。

1
2
3
4
5
6
sls function run hello
Serverless: Running hello...
Serverless: -----------------
Serverless: Success! - This Response Was Returned:
Serverless: {
"message": "Hello World !"

想定した動きとなったので、実際にfunctionをAWS側にデプロイする。
ちなみに今回もdash(ダッシュボード)をデプロイしているが、endpointのみでも良い。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
sls dash deploy
_______ __
| _ .-----.----.--.--.-----.----| .-----.-----.-----.
| |___| -__| _| | | -__| _| | -__|__ --|__ --|
|____ |_____|__| \___/|_____|__| |__|_____|_____|_____|
| | | The Serverless Application Framework
| | serverless.com, v0.5.5
`-------'

Use the <up>, <down>, <pageup>, <pagedown>, <home>, and <end> keys to navigate.
Press <enter> to select/deselect, or <space> to select/deselect and move down.
Press <ctrl> + a to select all, and <ctrl> + d to deselect all.
Press <ctrl> + f to select all functions, and <ctrl> + e to select all endpoints.
Press <ctrl> + <enter> to immediately deploy selected.
Press <escape> to cancel.


Serverless: Select the assets you wish to deploy:
hello
function - hello
endpoint - hello - GET
- - - - -
> Deploy
Cancel

Serverless: Deploying the specified functions in "dev" to the following regions: eu-west-1
Serverless: ------------------------
Serverless: Successfully deployed the following functions in "dev" to the following regions:
Serverless: eu-west-1 ------------------------
Serverless: hello (serverless-greeints-hello): arn:aws:lambda:eu-west-1:0000000000:function:serverless-greeints-hello:dev

Serverless: Deploying endpoints in "dev" to the following regions: eu-west-1
Serverless: Successfully deployed endpoints in "dev" to the following regions:
Serverless: eu-west-1 ------------------------
Serverless: GET - hello - https://xxxxxxxxx.execute-api.eu-west-1.amazonaws.com/dev/hello

実際にリクエストをしてみる。

1
2
3
4
5
curl https://xxxxxxxxx.execute-api.eu-west-1.amazonaws.com/dev/hello
{"message":"Hello Good bye !"}%

curl https://xxxxxxxxx.execute-api.eu-west-1.amazonaws.com/dev/hello\?message\=World
{"message":"Hello World !"}%

ということで、Amazon API GatewayとAWS Lambdaをひととおり触った。

POSTメソッドに変更する

1
2
3
4
5
6
7
8
9
-      "method": "GET",
+ "method": "POST",

- "requestTemplates": {
- "application/json": "{\"message\": \"$input.params('message')\"}"
- },
+ "requestTemplates": {
+ "application/json": "{\"message\": \"$util.escapeJavaScript($input.json('$.message'))\"}"
+ },

jsonのpostデータをescape処理して受け取ります。

1
2
3
4
5
6
sls endpoint deploy

Serverless: Deploying endpoints in "dev" to the following regions: eu-west-1
Serverless: Successfully deployed endpoints in "dev" to the following regions:
Serverless: eu-west-1 ------------------------
Serverless: POST - hello - https://xxxxxxxxx.execute-api.eu-west-1.amazonaws.com/dev/hello

最後にPOSTのエンドポイントが表示された。
実際にリクエストを投げてみる。

1
2
curl -H "Content-type: application/json" -X POST -d "{\"message\": \"World\"}" https://xxxxxxxxx.execute-api.eu-west-1.amazonaws.com/dev/hello
{"message":"Hello \"World\" !"}%

問題なくレスポンスが帰ってきた。

Slack Bot を作ってみる

ここまで出来たので、これらを使ってSlack Botを作ってみる。
まずはapi.slack.comからTokenを発行する。

続いて、AWS Lambdaで実行されるhandler.jsを編集する。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
'use strict';

var https = require('https');

function parse(data, separator) {
var res = {};
data.body.split('&').forEach(function(val) {
var tmp = val.split(separator, 2);
var k = tmp[0],
v = tmp[1];
res[k] = v;
});

return res;
}

module.exports.handler = function(event, context) {
// Incoming Web Hook のURL
var SLACK_HOOKURL = SLACK_INCCOMING_WEBHOOK_URL;
var SLACK_TOKEN = SLACK_TOKEN;

var data = parse(event, '=');
var user_name = data.user_name;
var response_text = decodeURIComponent(data.text.replace(data.trigger_word, '').replace(/\+/g, ' '));

if(data.token !== SLACK_TOKEN) {
return context.done({'text': ':sob: Failed Invalid Token' }, null);
}

var message = ":hand: @" + user_name + " Hello " + (response_text || "Good bye");
var options = {
hostname: 'hooks.slack.com',
port: 443,
path: SLACK_HOOKURL,
method: 'POST',
headers: { 'Content-Type': 'application/json', }
};

// @metionを使うために`link_names`を設定
var data = {
"text": message,
"link_names": 1,
};

var req = https.request(options, function(res) {
res
.on('data', function (chunk) {
// If success, post a response back into the Slack channel
return context.done(null, {'text': ':ok_woman: Success'});
})
.on('error', function (e) {
return context.done({'text': ':sob: Failed ' + e.message}, null);
})
});

req.on('error', function(e) {
return context.done({'text': ':sob: Failed ' + e.message}, null);
});

req.write(JSON.stringify(data));
req.end();
};

シンプルにslack apiを実行するだけのBotにした。

次にAWSからSlack Botに対して送られたメッセージにHookするIncoming WebHookを作成する。

今回は設定項目を以下のようにした

項目
Post to Channel #general (default)
Webhook URL https://hooks.slack.com/services/XXXXXXXX/YYYYYYYYY
Customize Name incoming-webhook (default)

ここで作成したURLに対してPOSTすることでSlackに通知される。

1
curl -X POST -H 'Content-type: application/json' --data '{"text": "@here This is posted to <#general> and comes from *monkey-bot*.", "channel": "#general", "username": "monkey-bot", "icon_emoji": ":monkey_face:", "link_names": 1}' https://hooks.slack.com/services/XXXXXX/YYYYYY/ZZZZZZ

続いて、Slack BotがメッセージをAWSに送信するために使うHookとなるOutgoing WebHookを作成する。

項目
Channel #general (default)
Trigger Word(s) greet
URL(s) https://xxxxxxxxx.execute-api.eu-west-1.amazonaws.com/dev/hello
(作成したGate Way API URL)
Token generateされているtoken
Customize Name outgoing-webhook (default)

Trigger Wordでslackに投稿すると、URLにpostされるようになる。

ここまでできたので、実際にBotで遊んでみる。

ということで、Serverless を使って、Amazon Gate Way API & AWS LambdaでSlack Botができた。
サーバーを作り、管理することないため、アプリケーションに注力できるたりが素晴らしい。

AWS Lambdaのようなイベントドリブンなサービスを使うことで、Botやインターネットに接続する機器からのリクエストを受け付けるアプリケーションが容易に作成できる。
またイベント数によりオートスケールしていくので、サーバー管理も気にすることはない。

AWS Lambdaの他にもMicrosoftはAzure Functionsというイベント駆動システムを提供している。

今回利用したServerlessはAmazon API GatewayとAWS Lambdaを中心としたイベントドリブンなサービスを開発するフレームワークだったが、AIやBotの盛り上がりから今後もっと様々なプロジェクト生まれていきそうな感じがする。

参考にしたページ

serverless docs
Serverless Framework & AWS API Gateway CORS
Amazon API Gateway とは?
API Gateway のマッピングテンプレートリファレンス

Comments