背景
とある番組で、誕生花についての話をされていました。そのときに、ふと「誕生花が取得できるAPIを作ろう」と思い立ったお話。
アイデア
- 適当にどっかの誕生花が載ってるサイトを探す
- スクレイピングする
- CSVに出力する
- APIを作る
- CSVを元にPOSTする
- 月日を元にAPI叩いて誕生花取得
こんな感じでなんとかなるだろうと思いやってみました。この記事は4〜6までですね。
ちなみに1〜3はこちらの記事で。
構成
せっかくAPIを作るので、はやりのフレームワークを使おうと思い、今回使ったのが以下の構成。
- AWS API Gateway
- AWS Lambda
- AWS DynamoDB
- SERVERLESS
この構成を最近使う機会があったんですが、そのときは自分一人で作っていないので、今回もう一度使ってみました。
正確ではないかもしれませんが、簡単にそれぞれがどういうものかを説明します。
API Gateway
エンドポイントが簡単に作れるAWSのサービス。Httpリクエストを受け付けて、どこかしらに受け流してくれる。AWSではLambdaと組み合わせて利用することが多いようです。今回はリクエストのパラメータ等々をパースしてLambdaに流しています。GUIで触ることが出来ます。
Lambda
イベントドリブンの関数を登録できるサービス。今回は、API Gatewayに来たリクエストをGatewayからLambdaに流してもらって、処理をするようにしています。ここにAPIとして実装したい処理を書きます(DBにPUT/GETしたり)。今回はNode.js使っていますが、PythonやJavaとかでも書けるみたいです。GUIで触ることが出来ます。
DynamoDB
NoSQLDBです。すごく気軽に利用できます。GUIで触ることが出来ます。
SERVERLESS
API Gateway + Lambdaの組み合わせでWebAPIを作りたい時のフレームワークです。まじで恐ろしいくらい早くAPIをデプロイ出来ます。GatewayやLambdaはGUIでも作れるんですが、すべてをコードで書いて、Git管理ができる点も非常に優れています。最近結構ホットです。
それぞれの細かい使い方は各自調べてください。導入系の記事はわりとあります。
実装
API
できるだけRESTにしようと思い、今回は以下の様な構成にしました。handler.jsにPOSTとGETの処理を書き、s-function.jsonにPOSTとGETのエンドポイントを作るように設定。
functions
└── data
├── event.json
├── handler.js
└── s-function.json
前回使ったときもs-function.jsonにかなりハマったんですが、今回もわりとハマりました。最終的には以下の様な構成。
{
"name": "data",
"runtime": "nodejs4.3",
"description": "Serverless Lambda function for project: yourProjectName",
"customName": false,
"customRole": false,
"handler": "handler.handler",
"timeout": 6,
"memorySize": 1024,
"authorizer": {},
"custom": {
"excludePatterns": []
},
"endpoints": [
{
"path": "data",
"method": "POST",
"type": "AWS",
"authorizationType": "none",
"authorizerFunction": false,
"apiKeyRequired": false,
"requestParameters": {},
"requestTemplates": {
"application/json": "{\"httpMethod\":\"$context.httpMethod\", \"body\":\"$util.escapeJavaScript($input.json('$'))\"}"
},
"responses": {
"400": {
"statusCode": "400"
},
"default": {
"statusCode": "200",
"responseParameters": {},
"responseModels": {
"application/json;charset=UTF-8": "Empty"
},
"responseTemplates": {
"application/json;charset=UTF-8": ""
}
}
}
},
{
"path": "data",
"method": "GET",
"type": "AWS",
"authorizationType": "none",
"authorizerFunction": false,
"apiKeyRequired": false,
"requestParameters": {"integration.request.querystring.month": "method.request.querystring.month", "integration.request.querystring.day": "method.request.querystring.day"},
"requestTemplates": {
"application/json": "{\"httpMethod\":\"$context.httpMethod\",\"month\": \"$input.params('month')\",\"day\": \"$input.params('day')\"}"
},
"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": []
}
}
今回の知見として新たに得られたのは、requestTemplatesの部分です。\$context.httpMethodにGET/POSTといったHttpリクエストの種類が格納されています。それをLambdaへeventオブジェクトの要素として渡すことで、Lambda内の処理を切り替えています。
POSTの場合は、リクエストボディも渡す必要があるので、それはbodyとして渡しているのですが、ContentTypeがJSONの場合は\$input.json('\$')でリクエストボディを全て取得できるようです。それを\$util.escapeJavaScriptで包んで渡してあげるとLambda側でリクエストボディがStringとして受け取れますので、JSON.parse(event.body)でリクエストボディをJSONとして取得できるようになりました。
GETの場合は、パラメータはクエリストリングとして渡ってきますので、requestParametersにも記述が必要なのと、\$input.params('month')といったような形で処理をする必要があるようです。
もっと深い理解が必要ではありますが、こうすれば動くというのはわかるようになってきました。AWSのマッピングリファレンスを読んでもいまいち理解できないんですよね。。。どなたか詳しい方がいらっしゃればコメントいただけると幸いです〜。
あとはいい感じにhandler.jsにeventの処理とDynamoDBの処理をかけば一応は完成です。特にたいした処理をしていないので割愛。
データのPOST
CSV出力していたデータを一行ごとに読み込んで、APIにPOSTします。これもNode.jsで書きました。
var fs = require('fs'),
readline = require('readline'),
rs = fs.ReadStream('./yourData.csv'),
rl = readline.createInterface({'input': rs, 'output': {}});
var request = require('request');
var url = 'yourApiUrl';
"hoge";
rl.on('line', function (line) {
var array = line.trim().split(',');
month = array[0];
day = array[1];
name = array[2];
var data = {
"month": month,
"day": day,
"flowerName": name,
"imgUrl": imgUrl
};
//オプションを定義
var options = {
url: url,
method: 'POST',
json: data
};
//リクエスト送信
request(options, function (error, response, body) {
if(error) console.log(error);
});
});
rl.resume();
ここでハマったのが、optionsです。調べているとjson: trueとしている記事が非常に多かったのですが、僕の場合はうまく動きませんでした。jsonにはリクエストボディを書かないと想定通りに動かないみたいです。
これで、APIにデータが格納され、GETすることができるようになりました。
Slack + Hubot
APIはひとまず完成したのでGETします。細かいエラー処理は割愛すると以下の様な感じです。
var request = require('request');
var qs = require('querystring');
var Client = require('node-rest-client').Client;
var url = 'yourApiUrl';
var client = new Client();
// slackの動作部分
module.exports = function (robot) {
robot.hear(/誕生花(.*)/, function (msg) {
var date = msg.match[1];
var array1 = date.split('月');
var month = array1[0];
var day = array1[1].split('日')[0];
var data = {
"month": month,
"day": day
};
var args = {
headers: {
'Content-type': 'application/json'
},
parameters: data
};
client.get(url, args, function(data, response){
console.log(data);
if(data.error){
return msg.send(data.error);
}else{
robot.emit('slack.attachment',{
channel: msg.envelope.message.room,
content:{
pretext: date + "の誕生花はこんな感じやわ〜",
image_url: data.imgUrl,
title: data.flowerName,
text: data.flowerWord + "\n" + data.caption
}
});
}
});
});
};
なぜかrequestライブラリを使うとクエリストリングの渡し方が全然わからなくてうまく動かなかったので、使うライブラリを変えました。node-rest-clientです。もっと早くからこっちを使うべきだった。POSTのほうもこっちで書き換えようと思いましたが、とりあえず使わないし放置。
処理の内容は大したことしていないので説明しません。見せ方は下記を参考にしました。
- [slack] hubot に gyazo の image を表示させる
- Slack公式 Attaching content and links to messages
まとめ
別になんてことない便利でもなんでもないAPIと機能ですが、これを通じて得たものがでかかったのでよしとします。
今現在は、URLさえわかればAPI叩き放題なので、勉強も兼ねてUserPools+Cognitoで認可されていないと叩けないようにしようかな。