AWS

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つです。

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オンリーなのに気づくのにちょっと時間かかりました。

まとめ

いろいろアレな感じのするコードになってはいますが、クエリ投げたらレコメンドしてくれる仕組みまではできました。

あとはフロントでクエリ作成と結果表示できるようにすればいろいろ素敵な感じになるんじゃないかなと思います。

ブックマークや限定記事(予定)など

WP Kyotoサポーター募集中

WordPressやフロントエンドアプリのホスティング、Algolia・AWSなどのサービス利用料を支援する「WP Kyotoサポーター」を募集しています。
月額または年額の有料プランを契約すると、ブックマーク機能などのサポーター限定機能がご利用いただけます。

14日間のトライアルも用意しておりますので、「このサイトよく見るな」という方はぜひご検討ください。

広告ここから
広告ここまで

Related Category posts