Serverless Framework + IBM WatsonでAMIMOTOマネージドのプランレコメンドAPIを作る
「Tradeoff AnalyticsでAMIMOTOマネージドの最適プランを調べる」というところまでは前回やりました。 https://wp-kyoto.cdn.rabify.me/check-amimoto-mana […]
目次
「Tradeoff AnalyticsでAMIMOTOマネージドの最適プランを調べる」というところまでは前回やりました。
https://wp-kyoto.cdn.rabify.me/check-amimoto-managed-plan-by-tradeoff-analytics/
今回はそれをAPIとして利用できるようにしていきます。
やりたいこと
AMIMOTOマネージドを検討してる人に、「この条件ならこのプランがいいですよ」というのをレコメンドしたい。
変数サンプル
とりあえずプラン表に載っている項目は数値化できるだろうということで、ざっと値をまとめました。
| パラメータ | 値 |
|---|---|
| max_pv | 想定している最大PV |
| min_pv | 想定している最小PV |
| max_price | 想定している予算上限 |
| min_price | 想定している予算下限 |
| max_wp | 想定している最多WP数 |
| min_wp | 想定している最少WP数 |
| db_list | DBの種類 |
とりあえずサンプル
本当はJSONでリクエストbodyから投げたかったけども、動くもの優先でざっと。
要望をリクエストする
$ curl "https://EXAMPLE.execute-api.us-east-1.amazonaws.com/dev/plans?max_pv=3000000&min_pv=2000000&min_price=200&max_price=800&min_wp=5"
クエリだけだとなんのことやらなので、このお客さんの要望を表にまとめました。
| パラメータ | 値 |
|---|---|
| PV数 | 2,000,000 ~ 3,000,000PV |
| 予算 | $200 ~ 800 |
| DB | 特にこだわらない |
| WordPress | 5つはインストールしたい |
候補一覧
ちなみにAMIMOTOマネージドのプランはJSONで以下のようにまとめています。
[{
"key": "1",
"name": "t2.micro",
"values": {
"price": 30,
"pv": 100000,
"db": "EC2",
"wp": 3
}
},{
"key": "2",
"name": "t2.small",
"values": {
"price": 60,
"pv": 300000,
"db": "EC2",
"wp": 3
}
},{
"key": "3",
"name": "t2.medium",
"values": {
"price": 150,
"pv": 500000,
"db": "EC2",
"wp": 3
}
},{
"key": "4",
"name": "c4.large",
"values": {
"price": 200,
"pv": 1000000,
"db": "EC2",
"wp": 5
}
},{
"key": "5",
"name": "c4.xlarge",
"values": {
"price": 300,
"pv": 3000000,
"db": "EC2",
"wp": 5
}
},{
"key": "6",
"name": "c4.2xlarge",
"values": {
"price": 900,
"pv": 5000000,
"db": "EC2",
"wp": 5
}
},{
"key": "7",
"name": "c4.4xlarge",
"values": {
"price": 1600,
"pv": 10000000,
"db": "EC2",
"wp": 5
}
},{
"key": "8",
"name": "c4.8large",
"values": {
"price": 3500,
"pv": 20000000,
"db": "EC2",
"wp": 5
}
},{
"key": "9",
"name": "w-Small",
"values": {
"price": 800,
"pv": 3000000,
"db": "RDS-Single",
"wp": 5
}
},{
"key": "10",
"name": "w-Large",
"values": {
"price": 1200,
"pv": 6000000,
"db": "RDS-Single",
"wp": 5
}
},{
"key": "11",
"name": "w-XLarge",
"values": {
"price": 1600,
"pv": 10000000,
"db": "RDS-Single",
"wp": 10
}
},{
"key": "12",
"name": "w-2XLarge",
"values": {
"price": 3200,
"pv": 20000000,
"db": "RDS-Single",
"wp": 10
}
},{
"key": "13",
"name": "HA-Small",
"values": {
"price": 1200,
"pv": 3000000,
"db": "RDS-Multi",
"wp": 5
}
},{
"key": "14",
"name": "HA-Large",
"values": {
"price": 1800,
"pv": 6000000,
"db": "RDS-Multi",
"wp": 5
}
},{
"key": "15",
"name": "HA-XLarge",
"values": {
"price": 2000,
"pv": 10000000,
"db": "RDS-Multi",
"wp": 10
}
},{
"key": "16",
"name": "HA-2XLarge",
"values": {
"price": 4600,
"pv": 20000000,
"db": "RDS-Multi",
"wp": 10
}
}
]
全部で16プラン。カスタマイズプランやWooCommerceプランを含めるともっとあります。
この中から価格とPVだけでなくDBサーバーの有無やWPインストール数を考慮しつつ最適なプランを選ばないといけません。大変です
戻り値
さて、IBM WatsonのTradeoff Analyticsに聞いた結果を見てみましょう。
{
"statusCode": 200,
"body": {
"problem": {
"pv": {
"low": 2000000,
"high": 3000000
},
"price": {
"low": 200,
"high": 800
},
"wp": {
"low": 5,
"high": 10
},
"db": [
"RDS-Multi",
"RDS-Single",
"EC2"
]
},
"result": [
{
"key": "5",
"name": "c4.xlarge",
"values": {
"price": 300,
"pv": 3000000,
"db": "EC2",
"wp": 5
}
},
{
"key": "9",
"name": "w-Small",
"values": {
"price": 800,
"pv": 3000000,
"db": "RDS-Single",
"wp": 5
}
}
]
}
}
どうやら2プランに絞れたようです。
こちらも表にまとめてみましょう。
| c4.xlarge | w-Small | |
|---|---|---|
| 想定最大PV数 | 3,000,000PV | 3,000,000PV |
| 月額料金 | $300 | $800 |
| DBサーバー | 無し(EC2内) | Amazon RDS * 1 |
| 推奨WordPressインストール数 | 5 | 5 |
16個から選ぶのは大変ですが、これなら「DBサーバーの有無で料金変わりますがどうしますか?」と提案できそうですね。
裏側
使ったのは以下の3つです。
- Serverless Framework: APIとバックエンド作成
- Watson Developer Cloud Node.js SDK: Tradeoff Analyticsへのリクエスト送信
- Lodash: データ処理
package.json
ライブラリはすべてnpmで管理します。
Serverlessはバージョン1.6系からの変更にまだ対応できてないので、1つ前のを使ってます。
{
"name": "sls-amimoto-managed-suggestion-api",
"version": "1.0.0",
"description": "Suggest your best AMIMOTO Managed Plan api",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "hideokamoto",
"license": "MIT",
"devDependencies": {
"serverless": "^1.5.0"
},
"dependencies": {
"lodash": "^4.17.4",
"watson-developer-cloud": "^2.15.5"
}
}
serverless.yml
API側はServerlessで定義します。
requestのbodyにJSONを突っ込みたいところですが、勉強不足なもので全部クエリストリングにしてます。
service: aws-nodejs
provider:
name: aws
runtime: nodejs4.3
package:
include:
- node_modules/
functions:
watson:
handler: handler.hello
events:
- http:
path: plans
method: get
integration: lambda
request:
parameters:
querystrings:
max_pv: false
min_pv: false
max_price: false
min_price: false
max_wp: false
min_wp: false
db_list: false
handler.js
肝心のLambdaのコードはこちらです。
思いっきりクレデンシャル情報書かないといけなかったため、GitHubにあげずにここにコード書いてます。
Lambdaの環境変数使えばいいはずなんで、対応でき次第GitHubにあげるかも。
'use strict';
const array = require('lodash/array')
const TradeoffAnalyticsV1 = require('watson-developer-cloud/tradeoff-analytics/v1');
module.exports.hello = (event, context, callback) => {
console.log(event)
let max_price = 0
if (event.query.max_price !== undefined) {
max_price = Number(event.query.max_price)
}
let min_price = 0
if (event.query.min_price !== undefined) {
min_price = Number(event.query.min_price)
}
let max_pv = 0
if (event.query.max_pv !== undefined) {
max_pv = Number(event.query.max_pv)
}
let min_pv = 0
if (event.query.min_pv !== undefined) {
min_pv = Number(event.query.min_pv)
}
let max_wp = 10
if (event.query.max_wp !== undefined) {
max_pv = Number(event.query.max_wp)
}
let min_wp = 1
if (event.query.min_wp !== undefined) {
min_wp = Number(event.query.min_wp)
}
const price = {
"low": min_price,
"high": max_price
}
const pv = {
"low": min_pv,
"high": max_pv
}
const wp = {
"low": min_wp,
"high": max_wp
}
const tf = new Tradeoff()
const db = tf.getDbParams(event)
const params = tf.getProblems(pv, price, wp, db)
const tradeoff_analytics = new TradeoffAnalyticsV1({
"password": "YOUR_BLUEMIX_PASSWORD",
"username": "YOUR_BLUEMIX_USERNAME"
});
tradeoff_analytics.dilemmas(params, function(err, res) {
if (err) {
console.log(err);
} else {
const result = tf.parseSolutions(res)
const response = {
statusCode: 200,
body: {
'problem': {
'pv': pv,
'price': price,
'wp': wp,
'db': db
},
'result': result
},
}
callback(null, response)
}
})
}
class Tradeoff {
getDbParams (event) {
if (event.query.db_list === undefined) {
const target_default = [
"RDS-Multi",
"RDS-Single",
"EC2"
]
return target_default
}
const target_query = event.query.db_list.split(",")
return target_query
}
parseSolutions(data) {
const solutions = data.resolution.solutions
const options = data.problem.options
let result = []
for (let i = 0; i < solutions.length; i++) {
if (solutions[i]['status'] === 'FRONT') {
let key = array.findIndex(
options,
function(o) {
return o.key == solutions[i]['solution_ref'];
}
)
result.push(options[key])
}
}
return result
}
getProblems(pv, price, wp, db) {
const problems = {
"subject": "amimoto",
"columns": [
{
"key": "price",
"type": "numeric",
"goal": "min",
"is_objective": true,
"full_name": "Price",
"range": price,
"format": "currency: 'USD$' : 2"
},{
"key": "pv",
"type": "numeric",
"goal": "max",
"is_objective": true,
"full_name": "Monthly Pageview",
"range": pv,
"format": "number:2"
},{
"key": "db",
"type": "categorical",
"goal": "max",
"is_objective": true,
"full_name": "Database Type",
"range": db,
"preference": [
"EC2",
"RDS-Single",
"RDS-Multi"
]
},{
"key": "wp",
"type": "numeric",
"goal": "max",
"is_objective": true,
"full_name": "WordPress installation ",
"range": wp
}
],
"options": [{
"key": "1",
"name": "t2.micro",
"values": {
"price": 30,
"pv": 100000,
"db": "EC2",
"wp": 3
}
},{
"key": "2",
"name": "t2.small",
"values": {
"price": 60,
"pv": 300000,
"db": "EC2",
"wp": 3
}
},{
"key": "3",
"name": "t2.medium",
"values": {
"price": 150,
"pv": 500000,
"db": "EC2",
"wp": 3
}
},{
"key": "4",
"name": "c4.large",
"values": {
"price": 200,
"pv": 1000000,
"db": "EC2",
"wp": 5
}
},{
"key": "5",
"name": "c4.xlarge",
"values": {
"price": 300,
"pv": 3000000,
"db": "EC2",
"wp": 5
}
},{
"key": "6",
"name": "c4.2xlarge",
"values": {
"price": 900,
"pv": 5000000,
"db": "EC2",
"wp": 5
}
},{
"key": "7",
"name": "c4.4xlarge",
"values": {
"price": 1600,
"pv": 10000000,
"db": "EC2",
"wp": 5
}
},{
"key": "8",
"name": "c4.8large",
"values": {
"price": 3500,
"pv": 20000000,
"db": "EC2",
"wp": 5
}
},{
"key": "9",
"name": "w-Small",
"values": {
"price": 800,
"pv": 3000000,
"db": "RDS-Single",
"wp": 5
}
},{
"key": "10",
"name": "w-Large",
"values": {
"price": 1200,
"pv": 6000000,
"db": "RDS-Single",
"wp": 5
}
},{
"key": "11",
"name": "w-XLarge",
"values": {
"price": 1600,
"pv": 10000000,
"db": "RDS-Single",
"wp": 10
}
},{
"key": "12",
"name": "w-2XLarge",
"values": {
"price": 3200,
"pv": 20000000,
"db": "RDS-Single",
"wp": 10
}
},{
"key": "13",
"name": "HA-Small",
"values": {
"price": 1200,
"pv": 3000000,
"db": "RDS-Multi",
"wp": 5
}
},{
"key": "14",
"name": "HA-Large",
"values": {
"price": 1800,
"pv": 6000000,
"db": "RDS-Multi",
"wp": 5
}
},{
"key": "15",
"name": "HA-XLarge",
"values": {
"price": 2000,
"pv": 10000000,
"db": "RDS-Multi",
"wp": 10
}
},{
"key": "16",
"name": "HA-2XLarge",
"values": {
"price": 4600,
"pv": 20000000,
"db": "RDS-Multi",
"wp": 10
}
}
]
}
return problems
}
}
ハマりどころ
Watson Developer Cloud Node.js SDK重い
Lodashも入れてるからしょうが、Lambdaのファイルサイズがまさかの50 MB超えを果たしました。
そのため$ sls deploy functionでデプロイしようとすると落ちます。sls deploy使ってください。
import {TradeoffAnalyticsV1} from = 'watson-developer-cloud'とかしてバべったら容量減らせる・・・のかな。(未検証)
APIの戻り値がパースしにくい
判定結果と候補値のキーが一致しているというわけでもないので、内部でぐるぐるまわして判定かける or そのまま投げ返すという選択になります。
内部で処理しようとした結果Lodashも入れることになってファイルサイズエラいことになってます。
promise対応してない
AWS SDKに慣れてしまってたせいというのもありますが、Watson Developer Cloud Node.js SDKがcallbackオンリーなのに気づくのにちょっと時間かかりました。
まとめ
いろいろアレな感じのするコードになってはいますが、クエリ投げたらレコメンドしてくれる仕組みまではできました。
あとはフロントでクエリ作成と結果表示できるようにすればいろいろ素敵な感じになるんじゃないかなと思います。