報道を自動化するエンジニアはゲーム自動化の夢を見るか

Pythonエンジニアの @kimihiro_n です。 今回は先日行った社内勉強会の話を。

社内勉強会

弊社では月1回、社内の開発者で勉強会を行っています。 内容はLT大会だったり、もくもく会だったり、ハンズオンだったりと幹事の人が好きにテーマを設定して実施しています。 おすすめの本を持ち寄ってビブリオバトル をやった時もありました。

今回、久しぶりに自分の担当が回ってきたわけですが、「AI同士の対戦」みたいなのやってみたいなーと前から思ってました。 AIというと大げさですが、各自プログラムしたゲームのCOM(CPUともいう)を持ち寄って、誰が一番強いのかを決めるみたいな内容です。 ニュースや報道の自動化を進めている弊社のエンジニアだったらゲームの自動操縦だってお手の物のはず…!

COM VS COM で一番強いチームを決める

実際にどうやってプログラム同士を戦わせるかですが、頭に浮かんだのは昔あった「カルネージハート」というゲームです。 アルゴリズムを組み込んだロボット同士を3on3で戦わせて遊ぶゲームなのですが、これがよく出来ていて一時期PSP版にハマっていました。 コーディングはテキストではなくてGUIのパネルみたいなのを並べて作るのですが、条件分岐やループ、関数的な別モジュールの利用といったようにかなり高度なことまで実装できます。 なのでカルネージハートとPSPを人数分用意して…と言いたいところですが、無理だったので代替案を考えることにしました。

ゲームを作る

Web 上でそういったプログラム同士の対戦をするサイト、どこかで見かけた覚えがあったので当初それを掘り起こして使おうと思ってました。 しかしサイト自体が閉鎖してしまったのか検索が甘かったのか、望みのものが見つかりませんでした…。 こうなったら自作するしか…ということで1人ハッカソンを始めました。

作りたいゲーム

  • 操作が単純である
  • ルールも単純である
  • 言語に不慣れなエンジニアでも楽しめる
  • 運要素が少ない(頑張ればその分勝ちやすい)
  • 2人1チームでも協力できる

ぱっと浮かんだ要件は上記のような感じでした。 勉強会の枠は2時間なので、その中で準備・実装・試合をこなすにはそれなりにシンプルであることが重要です。 また開発者といっても「サーバーサイドエンジニア」「アプリエンジニア」、「MLエンジニア」のように専門がそれぞれ異なります。 普段その言語を書いているエンジニアが強くて、他のエンジニアは動かすので精一杯みたいなのだと面白みにかけてしまいます。 なので基本文法を抑えていれば楽しめるようなゲームが望ましいです。 あとはプログラムを改善した分だけ強くなれる性質があったらなー、とか2人で分担や相談できたらなーみたいなことを漠然と思っていました。

Pong ゲーム

f:id:nsmr_jx:20190322171713p:plain
Pong

いろいろ考えた末思い浮かんだのが Pong ゲームでした。 操作が上か下かの2択でシンプル。ルールもボールを跳ね返して相手のゴールに入れるだけで単純です。 また、複数バーを用意することでチームっぽさも出すことができます。 ボールの動きが単純なので最終的な移動地点を予測してしまえば終わってしまう欠点はあるのですが、勉強会の短い時間であればちょうどいい難易度かなというのもポイントです。

Pyxel で作る Pong ゲーム

作るものが決まったので早速実装です。 当初みんな馴染みが多いだろうと Javascript で実装する気満々だったのですが、事前アンケートを取ってみたら Python のほうが慣れてる人多くて Python で作ることにしました。

github.com

ちょうど Python で使ってみたかった Pyxel(ピクセル) というゲームエンジンがあったのでこれ幸いと利用してみることに。

サンプルにあるように

import pyxel

pyxel.init(160, 120)

def update():
    if pyxel.btnp(pyxel.KEY_Q):
        pyxel.quit()

def draw():
    pyxel.cls(0)
    pyxel.rect(10, 10, 20, 20, 11)

pyxel.run(update, draw)

Pyxel 初めて触ったゲームエンジンですが、updateで毎フレームの内部状態を更新して、drawでレンダリングしてあげれば利用できるというシンプルな仕組みでとても書きやすかったです。 ドット絵を描くエディタやサウンド編集ツールなんかもついていてかなり凝ったゲームも作れそうです。今回は時間なくてシンプルなドットを描画するのみにとどまりましたがいつかちゃんとしたゲーム作りもチャレンジしたいです。

github.com

で、なんとか出来た対戦ゲームがこちら。 当たり判定に若干怪しいところとかがありますが、大きな理不尽なく遊べるくらいにはなりました。 コマンドラインの引数でチームのプログラムへのモジュールパスを受け取って対戦が可能になっています。

    def atk_action(self, info: GameInfo, state: State) -> int:
        return random.randint(-2, 2)

    def def_action(self, info: GameInfo, state: State) -> int:
        return random.randint(-1, 1)

各自に実装してもらうものも極力シンプルにしていて、毎フレームごとに呼び出される関数が何を返すかで前衛、後衛が上下に移動する仕組みになっています。 Python 不慣れでも苦なく動かせるのではないでしょうか。

pip で配布

Python で作ったゲーム、どうやって配布しようと思っていたのですが、pip で配布してしまえばいいのではという事に気づいてしまったので pongpy という名前で登録しました。 PC ゲームつくるならこの配布方式強いですね。アプリ化とか考えると Python では鬼門ですが…。

勉強会当日

チームビルディング

事前告知してインストールに支障がないかとかを見てもらいつつ、勉強会を迎えました。 最初にペア or ソロ参戦かでチームを作ってもらい、1時間強ほど実装を進めてもらいました。

f:id:nsmr_jx:20190322174456j:plain
作業風景
前衛と後衛で分担して実装をすすめてるチームが多かったですね。 後衛のほうが幅が広くボールがくるまで時間があるので、こっちを強化すると格段に勝ちやすくなります。

また1セットごとに左右が入れ替わって戦う仕組みをいれてあるのですが、ここへの対応をちゃんと入れないと前衛が自チーム側にボールを返して自爆してしまうみたいな罠があります。 実装の後回しになってしまいやすい部分ですがちゃんと考慮してくれたチームがいくつかあって開発者冥利に尽きました。

トーナメント

f:id:nsmr_jx:20190322173941p:plain
トーナメント

実装が出揃ったらいよいよトーナメント戦です。 GitLab にレポジトリを作って、マージリクエストという形でソースコードを出してもらいました。

f:id:nsmr_jx:20190322181628p:plain
観戦の様子
AirMac で画面をミラーリングすると Pyxel がエラーを吐いてしまうなどのトラブルがありましたが、1試合1分ほどで決着がつくので始まってからはサクサクと進みました。 やはり対戦状況が画面で見れるっていうのは楽しいですね。予想以上に盛り上がってよかったです。 (非エンジニアでも見て楽しめるので全社で観戦者募っても良かったかもしれない…。)

f:id:nsmr_jx:20190320200647g:plain
白熱の決勝戦
そして決勝戦です。かなり接戦でいい勝負していました。 前衛の青に当たると上下への振れ幅を大きく返せるので決め手になりやすいです。

f:id:nsmr_jx:20190322100102j:plain
優勝チーム
見事優勝した sugibayashi チームです👏 副賞として Pong にちなんだ ポンジュースがプレゼントされましました。

f:id:nsmr_jx:20190322155112g:plain
参考実装
ちなみに参考実装として用意していたボールの予測を実装したチーム同士の戦いです。 このチームに勝ってくれるのを密かに期待してましたが流石に1時間だと厳しかったみたいで…。 1日単位でハッカソンみたいな機会があったらまたぜひやってみたいですね。 DQNとかの機械学習を持ち出してくるチームもあると思いますし。

Kotlin NativeでAlfredのWorkflowを作ろう

この記事はJX通信社Advent Calendarの23日目です。

お久しぶりです。sakebookです。本日もKotlinネタです。タイトルの通り、今回はKotlin NativeでAlfredのWorkflowを作った話です。

tech.jxpress.net

Alfred

Macアプリです。ショートカットでいろんなアプリを起動させることができます。

その中でもWorkflowという機能が強力で、このために課金しました(課金しなくても十分便利ですが)。

同僚が自作のWorkflowを作っていて便利そうにしていたので、自分でも作ってみることにしました。

Workflowを作る

全くのはじめてだったのですが、こちらの記事がわかりやすかったです。

kuronat.hatenablog.com

記事内でも書かれてますが、要はJSONを吐くことができればいいわけです。

Kotlinでjarを生成して動かそうと思ったのですが、環境に依存するのと、どうせなら早く動くものがいいと思ってKotlin Nativeでやってみることにしました。

できたもの

areaと入力してから都道府県名や市区町村コードを入力することで候補が表示されます。データはJSONにしてworkflow呼び出し時に渡すようにしてます。

具体的にはworkflowで次のように実行してます。

$ ./実行ファイル.kexe query データ.json

f:id:sakebook:20181224150234g:plain
workflowを動かしてる画面

コードはこちらです。

github.com

環境は以下の通りです

  • Gradle Wrapper:4.7
  • kotlin-native-gradle-plugin:1.3.11
  • kotlinx-serialization-runtime-native:0.9.0

詰まったこと

  • Gradleのバージョン
    • 5.0ではじめは試してたのですが、依存解決がうまく行きませんでした。
  • pluginの記述
    • applyの方で書くかpluginsで書くかによってsettings.gradleも異なってきます。
  • Kotlin NativeからKotlinx.serializationを使う
    • Gradle5.0のコマンドで生成されるプロジェクトだとkonanを使っており、解決できませんでした。

多くのハマりどころはあったのですが、最終的にKotlinx.serializationのKotlin Nativeのサンプルを使うのが一番シンプルでした。この辺りは別途まとめたいと思ってます。

github.com

まとめ

Kotlin Nativeは単純に動かすだけならシンプルなのですが、込み入ったことをやろうとするとまだまだ情報が足りなくてハマることが多いです。しかし今までなかった選択肢を増やせるので、積極的にチャレンジして行きたい領域でもあります。

参考

Guides and Tutorials / Alfred

A Basic Kotlin/Native Application / Kotlin Programming Language

Gradle for Kotlin/Native / Kotlin Programming Language

GraphQL を RESTful API と比較しながら実装して理解する

この記事は GraphQL Advent Calendar 2018JX通信社 Advent Calendar 2018の19日目です。

NewsDigest で API のバックエンドとして GraphQL を本番利用して1年ぐらい経ちました。GraphQL 自体は、「クエリ言語」という位置づけですが、実際には「API のスキーマ」という使われ方(a.k.a. Anti-REST な何か)が多いと思います。

f:id:yamitzky:20181215174940p:plain

実際、NewsDigest の API としては、もともと RESTful なものを運用してたのですが、GraphQL へ移行しました。つまり、

  • RESTful の代わりとしての利用
  • プライベートな API
  • アプリから使う API
  • サーバーサイドエンジニアがメンテするもの(BFFな文脈で、フロント側のエンジニアがメンテするケースも多いと思いますが)

といった使い方です。GitHub のようなパブリックな API でもなければ、Facebook のようにリソースが複雑に絡まりあっているようなものでもなく、ただシンプルに「アプリ用の、RESTful API の代わり」として使っている感じです。

さて、GraphQL が流行ってくるにつれて「GraphQL は何をやってくれるものなの?」「どう実装したら良いの?」という話が聞かれたりしたので、「REST と比較して実装みたらわかりやすいんじゃないか」という趣旨の記事です。

RESTful vs GraphQL

まず、RESTful な API では、次のような URL が生えてきます。

  • GET /books
  • GET /books/1
  • POST /books
  • PUT /books/1
  • DELETE /books/1

一方で GraphQL な API では、最低限必要なのは次の URL だけです。 *1

  • POST /graphql

そのかわりに、POST のペイロードとして次のようなものを送ります。REST における GET /books は、

{
    books {
        author
        name
    }
}

といった具合です。 *2

RESTful な API

今回は簡略化して、 GET /booksPOST /books だけを扱います。

例として、Node の express で実装してみます。

const express = require('express')
const bodyParser = require('body-parser')
const app = express()
app.use(bodyParser.json())

const BooksDB = {
  db: [{ author: "yamitzky", name: "Road to GraphQL" }],
  list () {
    return this.db
  },
  add (book) {
    this.db = [...this.db, book]
  }
}

app.get('/books', (req, res) => {
  const books = BooksDB.list()
  res.json({ books })
})

app.post('/books', (req, res) => {
  const book = req.body
  BooksDB.add(book)
  res.json(book)
})

app.listen(3000)

実装としては単純で、GET /booksPOST /books というエンドポイントに、それぞれ「本の一覧を取得」と「本を追加」という処理を書いたハンドラー(コールバック)を追加しているだけです。

これは express 固有の話じゃなくて、他の API 用フレームワーク等(例えば Flask や Go の net/http) でも「パスを定義して、それを処理するハンドラーを書く」というのはだいたい同じでしょう。Rails のような MVC な設計であれば、Controller の Action を生やしていくイメージです*3

GraphQL な API

次に、 apollo というライブラリを使って、GraphQL な API を実装してみます。まずは、 GET /books に該当するものから実装してみましょう。

const { ApolloServer, gql } = require('apollo-server')

const BooksDB = {
  db: [{ author: "yamitzky", name: "Road to GraphQL" }],
  list () {
    return this.db
  },
  add (book) {
    this.db = [...this.db, book]
  }
}

const typeDefs = gql`
  type Book {
    name: String
    author: String
  }

  type Query {
    books: [Book]
  }
`

const resolvers = {
  Query: {
    books () {
      return BooksDB.list()
    }
  },
}

const server = new ApolloServer({ typeDefs, resolvers });
server.listen()

先程の REST な API と比較して新しく出てきた概念は、 typeDefsresolvers を定義しているところです。前者は、GraphQL のスキーマを定義していて、後者は、スキーマ上に定義されたリソースをどのように解決するか(要はDBから取ってきてオブジェクトを返すもの)を定義したリゾルバーです。この「スキーマ定義」と「リゾルバー」に出てくる Querybooks といった変数が、ちょうど、対応しています。

これはまさに、「URL(パス)の定義」と「ハンドラー」の対応関係と一緒です。RESTful な API においては、URL とリソースは対応しているわけですから、REST と GraphQL の違いは、 「リソースをどのような URL で定義するか」と「リソースをどのようなスキーマで定義するか」という違い になります。

今度は、POST /books も追加してみましょう。GraphQL での Create/Update/Delete の操作は、各操作を関数っぽく定義していきます。*4

const typeDefs = gql`
  type Book {
    name: String
    author: String
  }

  type Query {
    books: [Book]
  }

  type Mutation {
    addBook(author: String!, name: String!): Book!
  }
`

const resolvers = {
  Query: {
    books () {
      return BooksDB.list()
    }
  },
  Mutation: {
    addBook (root, args) {
      const book = args
      BooksDB.add(book)
      return book
    }
  }
}

Mutation(Create/Update/Delete)に関しても、Query と同じように、フィールドとリゾルバーを定義します。

今回は apollo というライブラリの例ではありますが、Python の graphene や Go の gqlgen などでも全く同じで、「スキーマにフィールドを定義して、それに対応したリゾルバーを用意する」という形になります(graphene はスキーマを class で定義する、という違いはあります)。

GraphQL が提供しているもの

GraphQL 自体は冒頭にも述べたように、ただの「クエリ言語」です。なので、その 実装や、どういうデータベースをバックエンドに持つか、あるいは、HTTPで提供するかどうかさえも関心外 です。

BFF の文脈では、GraphQL な API が提供するのは「秩序ある神エンドポイント /graphql」です。「1リクエストで、必要なリソースだけをすべて取得する」という手段が提供することによって、無秩序にフロントエンド向け URL を生やすということがなくなるわけです。

また、Prisma のようなものもあります。こういうのは、「レールに乗っけると便利になる、フルスタックフレームワーク」みたいな感じで認識しておくと良いと思います(使ったことないけど)。すでにあるデータベースなどの資産を活かして移行したいような場合とかは、apollo のような薄いものを使うと良いと思います。

GraphQL を使うべきか?

GraphQL は、秩序を守りながら API を提供する手段に過ぎないです。 なので絶対に GraphQL を使うべき理由も、絶対に GraphQL を使うべきでない理由も、あんまりない ような気がしています。

けど、便利なケース/便利じゃないケースはあります。便利じゃないケースとしては、ファイルアップロード(multipart form)とかは一工夫必要です。ちなみに、 NewsDigestでの「REST API の代わりとして使う」は普通に便利なケース でした。

あとは、Prisma や AppSync*5apollo-link-state *6 のような、GraphQL のルールに則ることで便利に使えるものも多いので、活用すると DX が上がると思います*7

メリットに関する補足 ↓

yamitzky.hatenablog.com

まとめ

まとめると、 RESTful な API の「URL を定義して、対応するハンドラーを実装する」はGraphQL における「スキーマを定義して、対応するリゾルバーを実装する」に該当する ということです。

また、JX通信社では、一緒に GraphQL API を作りたい仲間を募集中です。ちなみに GraphQL API は Go 使ってます。

www.wantedly.com

*1:実際には GET でも取得できるケースも多いと思います。また、どういう URL かは、GraphQL の責務ではありません。例えば、 PUT /hogehoge というエンドポイントだけを生やして、GraphQL API を作ることもできます

*2:イメージをつきやすくするために、かなり簡略化した例です

*3:Rails 久しく触ってないので、誤りがあればすみません

*4:こちらの例では Book 型で返してますが、成功したか失敗したかだけを Bool で返しても特に問題ないです

*5:マネージドな GraphQL APIサーバーです

*6:Redux のような状態管理のライブラリで、フロントエンドの状態管理を GraphQL のスキーマに乗っけることができます

*7:NewsDigestではいずれも使ってないのですが・・・笑

ユーザーレビューを感情分析するBotについて

この記事はJXAdvent Calendarの17日目の記事です。 こんにちは、以前秩父での開発合宿の記事を書いたぬまっち(@nuMatch) です。
弊社のSlackにはユーザーからのコメントが届くチャンネルがあります。
その内訳は

Android-Review      : Google Playに投稿されるユーザーレビューコメント
iOS-Review          : iTunes Storeに投稿されるユーザーレビューコメント

Android-Feedback    :
iOS-Feedback        : NewsDigestの「ご意見・ご要望」画面から投稿されるフィードバックコメント

の4種類で、日々開発の参考にさせて頂いております。

頂いたご意見の中には当然好意的な意見と否定的な意見両方があるのですが、
その度合いを数値として残しておければ分析の役に立つのではと思い、Botを作成したので記事として公開します。

(そしてAndroid開発以外に手を出すのも、初めての挑戦です)

今回作るBotにやらせてみたいのは
- 好意的な意見と否定的な意見の比率は施策によって遷移するのかを分析したい - Slackチャンネルで流れていくだけでは勿体無いので、コメントを蓄積してしたい

の二点になります。

SlackBotをHerokuにデプロイさせた理由

という事でBotを作ろうと思ったのですが、正直自分はAndroid開発以外のことは皆無でして、どこからどう始めたらいいのか分かりません。 そこで考えたのは今想定されるユースケースと自分の環境から最適解を見つけ出していこう、というものでした。

  • 言語選定 ここは弊社のサーバサイドエンジニアに知見を伺いたい事情もあったので、 Python を選択しました。
    その道のエキスパートが沢山いらっしゃるので、ここは相談しやすさを重視です。

  • SlackBot 前述の通り、コメントはSlackに流れてくるのですからBotはSlackから効率良く取得できるものでありたいです。 Python3系をサポートしていることから、Slackbotライブラリがチョイスされました。

  • Heroku 作ったBotはデプロイしないことには動いてくれません。当たり前ですがいつまでもローカル環境で動かしていては自宅の電気代も勿体無いです。 ここはCTOに相談したところHerokuを推して頂きました。PaaSの中で選ぶならAWSの知見が社内に溜まっているとは思いましたが、
    「デプロイ以外の細かなセキュリティ周りの設定だとかで躓く可能性が高い、初めての体験はなるべく躓かないほうが良い」
    という助言を頂いたところで、今回のBotは Heroku環境 で動作させる事が確定です。

感情分析をどう扱うのか(APIの選定)

次はある意味心臓部にあたる、感情分析をどうするか、という点について。
Google感情解析API(CLOUD NATURAL LANGUAGE API)というGoogle Cloud Platformのサービスがあります。 簡単に説明すると、事前に用意された機械学習モデルにREST APIでアクセスする事が出来て、感情分析結果を取得する事が出来るものです。

  • 感情分析の範囲
    0.1 ~ 1.0 ポジティブ
    0.0 ニュートラル
    -0.1 ~ -1.0 ネガティブ

コメントを投げると上記の範囲でレスポンスを返却してくれるので、ネガポジの度合いを絶対値で判断する事が出来ます。

ユーザーコメントが届く度にコメントをAPIに乗せて投げてあげれば、感情分析はGoogle Cloud Platform側で担保する。これでレビュー解析Botは形になりそうです。

大まかな流れ

f:id:numatch-jx:20181217173133p:plain

①レビューコメントの投稿

デプロイしたBotをレビューが投稿されるSlackチャンネルに招待し、 コメントの通知を受け取れるようにデコーダを実装させます。

from slackbot.bot import listen_to

@listen_to(r'.*')
def mention_func(message):
    body = message.body

SlackBotライブラリに用意されている@listen_toデコーダで投稿されたMessage全体を受け取ります。 その中からbodyを部分を取り出して感情分析にかけたいユーザーコメントを取得します。

  • Androidレビュー/フィードバック
  • iOSレビュー/フィードバック

の4パターンのコメントが投稿されるので、取得したbodyの内容で判別の上、適切な形で取り出したうえでGoogle感情解析APIに投稿します。

②コメントの感情分析 / ③ネガポジスコアの返却

Google Cloud PlatformにてGoogle Natural Language API(感情分析API)の登録を完了させるとAPIキーを発行する事が出来ます。
Heroku側でダッシュボード内でAPIキーを環境変数として登録しておく事で、Botがデプロイ環境で感情分析APIを扱うことが出来るようになります。

環境変数の登録
f:id:numatch-jx:20181216202549j:plain
from google.cloud import language

# 環境変数からAPIキーを取得
info = json.loads(os.environ.get('環境変数に登録したKey'))
credentials = service_account.Credentials.from_service_account_info(info)
client = language.LanguageServiceClient(credentials=credentials)

# 感情分析APIにリクエストを投げる
sentiment = client.analyze_sentiment(document=document).document_sentiment

sentiment.scoreで感情分析のスコアを取り出すことが出来ます。

④スコアの投稿

スコアは-1.0 ~ 1.0の範囲で返却されますが、端数になりがちなので小数点以下で四捨五入しておきます。

ounded_score = round(sentiment.score, 3) 

message.reply()で投稿されたSlackメッセージに対してスレッド返信を行えます。 また、スコアが正の値ならポジティブ、負の値ならネガティブなので対応した絵文字でSlackのユーザーコメントにリアクションをしてみます。

message.reply('NegaPosi Score: {}'.format(rounded_score), in_thread=True)
message.react(negaposi(rounded_score))

解析結果

ポジティブ ネガティブ
f:id:numatch-jx:20181217150910j:plain f:id:numatch-jx:20181217150938p:plain
  • ポジティブコメント
    「地震情報が素早い世界中の情報が分かる」はポジティブな意見そうですが、実際のスコアが0.7と判定されており、感情分析結果としてもポジティブと捉えられています。
  • ネガティブコメント 「天気予報は出ないのですか?」は残念ながらネガティブそうです。スコアを確認すると-0.6ですね。APIのレスポンスでもネガティブとして判断されているようです。

  • 絵文字とスレッド 絵文字がリアクションとして投稿されるので、スレッドを開かなくてもネガポジの解析結果を判断する事が出来ます。 スレッド返信されておりSlack上でスコアを確認することも出来て良さそうです!

データベース登録と、その結果

最初にBotを作ろうとした理由のうち、

  • 好意的な意見と否定的な意見の比率は施策によって遷移するのかを分析したい

は実現が出来たので、

  • Slackチャンネルで流れていくだけでは勿体無いので、コメントを蓄積してしたい

も実現するべく、次はデータベースへのInsert機能をつけたいと思います。 PostgreSQLはHerokuが公式でサポートしているので都合が良さそうです。

Add-onsでHerokuにPostgreSQLを追加するとダッシュボードから確認出来るようになります。

Herokuが自動的に環境変数に登録してくれる
f:id:numatch-jx:20181216202758j:plain

PostgreSQLのDBに接続するためのパスワード等は、Herokuの環境変数へと自動で登録されるのでラクチンです。

  • 保存しておきたい4つのプロパティ

今後スコアを分析するの必要そうなプロパティは事前に考えておきました。
下記の4つの値を保持しておくと、今後役立ちそうです。

comment : 感情分析にかけたユーザーコメント
score : ネガポジスコア
type : OSの種別・レビュー/フィードバックの種別
created_at : Insert日時 

後からレビュー分析に使えるようなColumn情報を定義してCREATE TABLE(今回のテーブル名はreviews)しておきます。 PythonからDB接続するためにはドライバが必要になるので、psycopg2をimportしたうえでコネクションオブジェクトの取得を行います。

def get_connection():
    dsn = os.environ.get('Herokuが登録してくれた環境変数Key')
    return psycopg2.connect(dsn)

コネクションオブジェクトからDBカーソルを取得したら、必要なプロパティに対してInsertを実行します。

def insert_review(comment, score, type):
    with get_connection() as conn:
        with conn.cursor() as cur:
            cur.execute('INSERT INTO reviews (comment,score,type,created_at) VALUES (%s,%s,%s,%s)',
                        (comment, score, type, datetime.now().strftime("%Y/%m/%d %H:%M:%S")))
            conn.commit()

レビューコメントや感情分析スコアが無事にDB保存されている事を確認できたら、今回の目標は達成となります!

感情分析スコアDB
f:id:numatch-jx:20181217151025j:plain

作ってみて

今回、ユーザーレビューを感情分析してSlackに投稿する/DBに保存する機能のBotを作りました。 Android開発以外は門外漢で通ってきた自分ですが、質問に快くレスをくれた弊社の開発チームのおかげで無事にデプロイまで漕ぎ着ける事が出来たので感謝しかありません。 こういったチャレンジに取り組みやすい環境が弊社には揃っていると思います。

ところですが、Botに愛着を持ってもらう為に Cook-Tool-Reviewと名付けました。(個人的にですが)

なんでCook-Toolなのか。 それは弊社開発チームのプロジェクトは基本的にお菓子の名前で呼ばれている事から発想しました。 お菓子の調理を手伝う調理器具みたいな役割、Botがそんな風に役に立ったらいいなって気持ちを込めて名付けました。 今後もサービス開発の助けになるようなCook-Tool-Botを増やしていければいいなって思います。

サーバーレスなシステムをプラットフォームに依存せず作る 〜 #devboost 登壇に寄せて〜

この記事は Serverless Advent Calendar 2018JX通信社 Advent Calendar 2018の16日目です。

12月15日、Developers Boostにて「なぜサーバーレス『と』Dockerなのか 〜インフラ運用を最小化するサービス開発〜」というタイトルで発表させていただきました。本稿はその補足記事です。

※ 登壇資料中にもありますが、今回は サーバーレス=FaaS(AWS Lambda など)として扱います 。また、記事中の例示はすべて AWS Lambda のものです。

JX通信社の NewsDigest では、サーバーのデプロイ環境として、Docker クラスターやサーバーレスを活用 しています。お互い同列なデプロイ環境として考えて、ケースバイケースで使い分け ているような感じです。

サーバーレスや Docker の本番利用の事例も増えてきていると思いますが、サーバーレスかコンテナのどちらかしか使わないのではなく、どちらもメリット・デメリットある中で「どのように使い分けるのか」という判断が重要です。また、特にサーバーレスに関しては 「チームでシステム開発するには」 という視点を忘れずに持っておくべきだと考えています。

サーバーレスのチーム開発

チームでシステム開発をする際に、だいたい次のものは必要なのではないでしょうか。

  • プログラムを書くこと(ここは当たり前ですね)
  • 簡単にローカル環境が作れること
  • 自動テスト
  • 簡単にデプロイできること
  • CI/CD
  • 監視

サーバーレスを「ただのデプロイ先」として考えると当然、サーバーレスであっても、全く同じものが必要です。

f:id:yamitzky:20181215183218p:plain

サーバーレスに依存しないサーバーレスバッチ

サーバーレスを「ただのデプロイ先」として扱うには、アプリケーションの作り方も特定のプラットフォームに依存せず動くように工夫する必要があります。

まずは、次のようなコードを見てみましょう。次のプログラムは、CloudWatch Events で指定された text を標準出力する Lambda 関数です。

def lambda_handler(event, context):
    # CloudWatch Eventにて、JSONパラメータを設定 {"text": "text to show"}
    print(event['text'])

当然ながら、このシステムは、Lambda 上でしかろくに動きません。このような場合、次のようにリファクタリングをします。

import argparse

def show(text: str):
    print(text)

def lambda_handler(event, context):
    # CloudWatch Eventにて、JSONパラメータを設定 {"text": "text to show"}
    show(event['text'])

if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('text')
    args = parser.parse_args()
    show(args.text)

こうすると、このシステムは 「サーバーレス環境でも動くだけの、ただの CLI プログラム」 として抽象化されます! ただの CLI プログラムなので、ただの CLI プログラムとして、テストをしたりすれば良いということになります。手元で検証するときも python main.py hogehoge と実行すれば、ただの CLI プログラムとして適切に実行されます。

サーバーレスに依存しないサーバーレス API

上記の例はバッチのようなシステムの用途の想定ですが、API についても同様です。

awsgiZappa を使うと、普通の Python の API が AWS Lambda にデプロイできる API に早変わり します。次の例は、ただの Flask 製 API としても、サーバーレスな API としてもデプロイできます。

import awsgi
from flask import Flask, jsonify

app = Flask(__name__)

@app.route('/')
def index():
    return jsonify(status=200, message='OK')

def lambda_handler(event, context):
    return awsgi.response(app, event, context)

このような形であれば、テストも「ただの Flask API」としてテストすれば良いですし、環境構築等も同様です。

同様の「サーバーレスじゃない API をサーバーレスでも動くようにするライブラリ」は Node(express)Go などでもあるみたいです。

これらのライブラリは、「今あるものをサーバーレスに移行する」というモチベーションが多いような気もしますが、個人的には新規プロジェクトでも積極的に使っています。

デメリットとしては、

  • 余分なライブラリを介するので遅くなるかもしれない
  • 「エンドポイントごとに関数をわける」みたいな設計にはならない

というのがあったりするので一長一短ですが、serverless フレームワークに乗せるにしても「プラットフォームに依存せず動く」という方針はキープしておくと良いように思います。

サーバーレスのテスト

あまり特別なことはしていませんが、サーバーレスではマネージドサービスが頻出するので、dynamodb-locallocalstack のようなモックをよく活用しています。

ただしこのやり方は Twelve-Factor App の開発/本番一致に反していますし、モックサービスは完璧ではありません。「本物のマネージド・サービスを使うべきだ」という意見もあると思います(一長一短ですね)。

また、t_wada さんによる「外部に依存したコードもテストで駆動する」も参考になると思います(Alexaスキルをテスト駆動開発しているものです)。

speakerdeck.com

サーバーレスなプロジェクトのローカル環境例

発表では docker-compose を使うとさらっと述べたのですが、サーバーレスなプロジェクトも、Docker クラスターへデプロイするプロジェクトも、基本的に docker-compose を使って環境構築がさらっとできるようになっています。JX 通信社のアクティブなリポジトリだとほぼ全て docker-compose.yml が用意されています。

例えば、次のように定義します。

version: '3.5'
services:
  app:
    build: .
    command: python -m flask run --host 0.0.0.0
    volumes:
      - .:/usr/src/app
    depends_on:
      - dynamodb

  dynamodb:
    image: amazon/dynamodb-local

こうしておくと、「ただの DynamoDB を使う一般的なプロジェクト」として扱うことができますし、サーバーレス環境にデプロイする際に必要な Liunx 下でのライブラリなども取れて便利です。dynamodb-admin みたいな、GUI のものも入れておくと便利です。

余談1:デプロイ

デプロイについては、

を使ってるケースが多いです。Apexも使ってましたが、最近は使ってないです。今から始めるならserverlessとかも良いと思います(参考:サーバーレスのメリット&本質を、AWS Lambdaを使って理解しよう)。

余談2:始め方

懇親会では「どうやって始めたの?」という質問も聞かれました。

サーバーレスもDockerクラスターも、まずは新規のプロジェクトで試しに本番運用してみて「良さそう」となってから横展開しました。サーバーレスは tech.jxpress.net でも1章割いて紹介したログ基盤が使い始めたプロジェクトです。

まとめ

伝えたかったのは、

  • IaaS/VPS を直接運用している場合は、サーバーレス化/Docker化するとインフラ管理が楽になる
  • 「サーバーレス vs コンテナ」じゃなくて、どっちも併用して使い分けたい
  • サーバーレスであっても「チーム開発」の視点を持ちたい
  • そのために プラットフォームに依存しないように作りましょう (本稿)

といったところです。

今回は貴重な登壇の機会をいただき、デブストオーガナイザーの近藤さん、スタディスト北野さん、ありがとうございました!

また、発表では拙いところもあったと思いますが、何かわからないことがあれば Twitter 上で @yamitzky までメンションを飛ばしていただければと思います。

www.wantedly.com