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

    まとめ

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

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

    広告ここから
    広告ここまで
    Home
    Search
    Bookmark