#phpcon2018 でVOYAGE GROUPのエンジニア評価制度ってどんな感じなのか再演しました!(資料・動画あり)

目次


こんにちは!

ECナビエンジニアのゆきみねです。

12月15日に行われた PHPカンファレンス2018 で、VOYAGE GROUPのエンジニア評価制度「技術力評価会」ってどんな感じなのか再演しました。

発表資料・動画は公開してます

speakerdeck.com

VOYAGEのエンジニア評価制度ってどんな感じなのか25分で再演

動画はこちら

反響をまとめてみました

togetter.com

いただいた質問への回答

発表中にお知らせした通り、いただいた質問にお答えします。


【質問】
評価面談は発表30分+質疑応答60分とあったけど、
このMarkdown資料準備するのに時間どれくらいかかるんだろう

【回答】
せっかくなのでアンケートを取ってみたところ、
「評価会資料の作成にかけるのは2日未満」という人が8割でした。

初回は時間をかけた人も、何回かやっていくうちに
かける時間が短縮されているようです(僕もそうでした)。

評価会資料の作成にどのくらい時間をかけていますか?(素振り、素振り後の修正含む)

同アンケートの補足コメント

  • 題材による気がします!
  • 普段のissueを書くときから評価会を意識する. まとめを適度に挟むように書くことで資料を書く手間を減らすようになった.
  • 2回目以降でこれくらいです。だいたい1営強というところ。
  • 3回目あたりから資料作成は埋めるだけって感じ作業になってきてて、日頃の業務で意識してることと評価会資料との差異を減らしていく感じになってる
  • 規模が大きい + 相手にどれだけ事前に情報を提供すればいいか悩む + 本番で言葉が出てこないことが無いように資料作成に時間がかかってしまう
  • 初回は2日くらいかけたけど、少しずつ短くなっている。(仕事をしていく中で周りの人に説明するための資料をそのまま技術力評価会に使うなど、取り組み方がわかった)

【質問】
評価者による面談で評価を決めるとなると、評価者によってバラツキが出たりすると思うのだけど、
その辺の平準化とかどう解決してるのか気になる

【回答】
評価者のバラツキを少なくするためにやったこと / やってること は以下の通りです。

1. 最初の300回くらいはCTOが全てに同席し、CTOがフィードバックした
2. 評価者のペアを前回と同じにならないように組み合わせている
3. 評価結果レポートをCTO、上長、評価者ですり合わせしている

【質問】
発表で例示されていたアドバイス文は年1回もらうものとしては
ちょっと細かい粒度の内容だったけど、ただの例かな

【回答】
技術力評価会の実施頻度は半年に1度です。

発表で出したものはサンプルではなく評価者が実際に書いたものです。

できるだけ具体的に書こうと意識しているため細かい粒度に見えるかもしれません。

また、技術力評価会はあくまでも能力の部分だけで、
実績も含めてトータルのフィードバックは別途行われています。

【質問】
制度全体の話とかも気になる

【回答】
制度全体の話は是非以下の資料・記事をご覧ください。

2019年1月にまた実演します

2019年1月30日に技術力評価会の実演イベントを行います。

PHPカンファレンスでは 再演 でしたが、1月のイベントは、参加者の皆さんの前で、VOYAGE GROUP エンジニアを 本当に 評価します。

ありがたいことに、一般枠は埋まってしまいましたが、PHPカンファレンス2018に参加してくださった方用の枠に若干余裕があります。

他の人のエンジニア評価が気になるという方、是非遊びにきてください。

voyagegroup.connpass.com

ありがとうございました!

現地へ観に来てくださった皆さん、発表資料・動画を見てくださった皆さん、発表練習に協力してくださった皆さん、応援してくださった皆さん、ありがとうございました!

25分という発表時間で、どうすれば伝わるかと頭をひねった数ヶ月でしたが、無事評価会の雰囲気をお伝えできたようでよかったです。

2019年もよろしくお願い致します。

他社エンジニアの評価って気になりませんか? phpcon2018ブースでVOYAGE GROUP技術力評価制度を説明して反響が大きかったことベスト3

こんにちは!!

VOYAGE GROUP 新卒2年目エンジニア
しゅーぞー(@ShuzoN__)です.

2018/12/15(土)に開催されたPHPカンファレンス2018にスポンサーブーススタッフとして参加してきました.

f:id:namu_r21:20181225113705j:plain

今回は, phpcon2018ブースで新卒エンジニアがVOYAGE GROUP技術力評価制度を説明して反響が大きかったことベスト3 と題して, 他社のエンジニアから反響のあった弊社の評価制度についてクローズアップしていきたいと思います.

技術力評価制度を軸にしたVOYAGE GROUPブース

VOYAGE GROUPのスポンサーブースでは, 弊社の技術力評価制度である「技術力評価会」を軸として出展を行いました.

弊社エンジニアの本物の評価結果を展示し, 実際に来場者の方々に手にとって見ていただいていました.

f:id:namu_r21:20181225112631j:plain

みなさん他社エンジニアの評価は気になるそうで, 多くの方から評価制度や結果について聞いていただけました.

90分で自分の仕事を他のエンジニアにアピールする評価制度「技術力評価会」

みなさんの会社では, どのように技術力の評価を受けるでしょうか?

会場で聞いたところによると他社では以下のような制度であると聞きました.

  • そもそもエンジニアが評価に関与していない
  • 非エンジニアである上長が10分程度の面談で最終判断を下す
  • 数値的なKPIを設定し目標達成型で評価が行われる
    • 営業志向の会社はこの制度が多い印象
  • 割り込みや改善業務を考慮に入れていない締切達成駆動な評価制度
  • 技術力とは関係がない人柄ベースの評価の比重が大きい
  • 隣の人の評価結果や役職, グレードを知らない

営業や総合職の評価制度と同じ, もしくは引きづられた形の数値型評価制度が多く見受けられました. エンジニアの働き方に特化した評価制度はあまり耳にしませんでした.

f:id:namu_r21:20181225114259j:plain

ここで反響が大きかったことベスト3の発表です!

では, 早速 VOYAGE GROUPの評価制度「技術力評価会」で反響が大きかったことベスト3です.

  • 1位: プレゼン形式で1人あたり90分のプレゼンをエンジニア全員がやること
  • 2位: 評価や昇格結果が全社に公開されていること
  • 3位: 社外のエンジニアが招聘されエンジニアを評価すること

それぞれ細かく見ていきます.

1位: プレゼン形式で1人あたり90分のプレゼンをエンジニア全員がやること

f:id:namu_r21:20181225115304p:plain

弊社では, エンジニア1人1人に90分の時間を与えられ, 他事業部のエンジニア2名に半年間の仕事で最も良かったものをプレゼンテーションする機会を与えられます.

発表者の一方的なプレゼンではなく対話的に深掘りしていくため, 判断力や知識量, 説明力も問われます.

そこで, どこまでが本人の判断でどこからがチームの判断なのか洗い出されていきます.

やらなかったことや進め方まで細かく聞かれるため総合的な技術力を求められる制度になっています.

その90分間のプレゼン評価を元に, 評価者2名と事業部の技術責任者とCTOの意見を含めた評価結果レポートが決まります.

この評価結果レポートが事業責任者/本部長に渡り、給与やグレードに大きく影響を与えます。

ネタ選びやプレゼン, 対話を行うことで納得感が得やすい評価制度になっています.

実際の評価風景はこんな感じです. 写真はf-codeさんからお借りしています.

f:id:namu_r21:20181225114559j:plain

技術力評価会について詳細を知りたい方は以下の資料をご覧ください. 弊社CTOが7年かけて改良して来た評価制度について書いています.

speakerdeck.com

2位: 評価や昇格結果が全社に公開されていること

他社の話を聞いていると, 隣の人の評価やグレードを知らない と仰っている方がいらっしゃいました.

弊社では, GitHub上に評価用リポジトリが用意され, 全エンジニアの評価結果が社内公開されています.

つまり, 隣で働いている人の評価を自由に読むことが可能です.

また, 半期に一度, 昇格者が発表されるため, 誰がどのグレードなのかみんな知っています.

VOYAGE GROUPに新卒で入社したため, 公表されているのは当たり前だと思っていました.

これに関しては僕自身も驚きましたし, 来場者の方も反応が良かったです.

当日のブースでは, ブーススタッフの本物の評価資料/結果を展示し, 手にとって見ていただきました.

外部に公開できる程度に透明性が高い評価制度と言えると思います.

ちなみに, 株式会社f-codeさんも評価会を見学しています. 見学レポートはこちら.

www.wantedly.com

3位: 社外のエンジニアが招聘されエンジニアを評価すること

来場者の方に聞いたところ, 同じ会社にいる人間が同じ会社にいる人間を評価する形式が多いように見受けられました.

弊社では, 社外からCTOクラスのエンジニアを社外評価者として招聘し, 弊社エンジニアを評価する制度があります. 毎回, 全体の1割ほどのエンジニアが社外評価者の評価を受けます.

社外からの評価を行うことについてCTOは以下のように考えています.

  • 人数が少ない技術領域では, 評価者/被評価者の組み合わせのパターンが少ない. 社外から識者を招聘することで, 新しい視点や気付きが得られる機会を増やしたい.
  • 技術力評価会を数年実施することでVOYAGE GROUPでの価値観がすり合ってきた. それ自体は良いことだが, タコツボ化していくリスクもある. 社外の目を入れることで, 自分たちでは気づけないバイアスを知りたい.

僕自身はこういう効果もあると考えています.

  • 世間の市場価値に見合った評価を受けられること
  • 外部の意見を取り入れることで社内では上がってこない案や思考を取り入れられる

最近では, オミカレCTO @soudai1025 さん, cookpad CTO @mirakui さん, はてな チーフエンジニア @songmu さんなど著名なエンジニアを招聘しています.

@songmu さんから弊社の評価制度についてコメントをいただいているので掲載します.

評価プロセスをオープンにして質を高めて行く取り組みの中で,それを社外にまでオープンにしたことは超絶ウルトラCです.社外評価者は劇薬的に強力であり,それを上場企業が適正に運用していることは恐るべきことです.私も社外評価者として関わらせてもらっていますが,その際,評価に関わる社内の情報をかなり見せてもらえます.それだけ社外の人を信用できるのは会社の制度としても凄いことで,毎回身の引き締まる思いで評価をさせていただいています.

当たり前だと思っていた弊社の評価制度はすごく珍しいものだった

ブーススタッフとして働いて見たところ, 「弊社の評価制度はめちゃくちゃ珍しいもの」であることに気づきました.

  • ここまで“評価“に時間と金をかけている会社は少ない
  • 「評価されづらい仕事を評価するための仕組み」のポイントは“時間をかけた対話“にあった
  • 隣の人の評価結果/グレードを見れるのは当たり前じゃない
  • そもそもエンジニアの評価をエンジニア以外の人が行なっている
  • 社外のエンジニアに評価してもらうことは、ほとんどの会社でやっていない

評価制度に対してここまで真摯にお金と時間をかけて行なっている会社は会場で聞いた限りはなさそうでした.

7年間の改良を経て, 市場価値にあった & 透明性のある評価が行われ, ただ評価をもらうだけではなく成長のアドバイスも添えてフィードバックが行われます.

被評価者だけでなく評価者も評価結果が社内に公開されるなど良い質問ができるかという部分で評価されています.

社内エンジニア全員の成長のために行われているという意図をはっきりと感じることができました.

僕自身も3回評価を受けましたが, 納得感を得やすく, 弱点を理解しやすい良い制度だと思います.

他社エンジニアの評価制度って気になりませんか?

ところで, 他社エンジニアの評価制度って気になりませんか?

2019/01/30(水) にVOYAGE GROUP本社で 弊社のエンジニアを皆さんの目の前で本当に評価する イベントを開催します. そして後日, Twitter上で評価結果を公開します.

きっと同じ会社でも他人が評価される様は見ることができないと思います.
弊社の仕事ぶりや評価制度について知るいい機会です.

お酒や食べ物もご用意いたしますので気軽に来てくださいね.

応募はこちらから可能です. 枠が少なくなって来ていますのでお早めに!

voyagegroup.connpass.com

PeXのRailsを2年ぶりに4.2から5.0にアップデートしました。キャッシュやセッションまわりでのエラーにご注意!

VOYAGE GROUPの駒崎です。PeXというポイント交換サービスの開発運用をやっています。

PeXは2016年3月にSymfonyからRuby on Railsにフルリニューアルを果たし、そこから2年ほどRailsのバージョンが4.2で止まっていました。 PeXというサービスを今後長く運用していくためにも、Railsに乗り続けるためにも、という考えで2018年7月頃に5.0へアップデートしました。(実は現時点ではRails5.2にアップデートされているのですが)

Railsのアップデートを行うまでの流れと、リリース後にキャッシュ、セッション周りでハマったことをここにまとめます。

Railsアップデートでやったこと

  • gemのバージョンを最新にする。
  • gemのバージョンを最新にアップデートし続ける仕組みを作る。
  • Railsのバージョンを4.2から5.0にする。

3行で言うとこの流れで進めました。 gemのバージョンを最新にし、アップデートし続ける仕組みを作るまでに2ヶ月くらい。Railsのバージョンを4.2から5.0にするのに1ヶ月程、かかりました。

Railsのバージョンアップを行うと依存しているgemのバージョンも上げることになるのですが、同時に行うと非常に大変なのでまずは周辺gemのアップデートを行いました。

次にRailsのバージョンアップ作業中および今後のバージョンアップを踏まえ、gemのバージョンを最新にし続けるような仕組みを導入しました。

最後にRailsのバージョンを上げました。これは Rails アップグレードガイド | Rails ガイド を見つつ進めました。多分普通にアップデートする分には問題ないはずなので、プロダクト固有のハマったところを紹介したいと思います。

規模感

参考に rake stats の結果です。テストが厚めに書かれています(素敵)。

+----------------------+--------+--------+---------+---------+-----+-------+
| Name                 |  Lines |    LOC | Classes | Methods | M/C | LOC/M |
+----------------------+--------+--------+---------+---------+-----+-------+
| Controllers          |  12864 |  10130 |     291 |    1307 |   4 |     5 |
| Helpers              |    278 |    222 |       0 |      49 |   0 |     2 |
| Jobs                 |    195 |    140 |       8 |      16 |   2 |     6 |
| Models               |  18907 |  10610 |     279 |     958 |   3 |     9 |
| Mailers              |    585 |    501 |      31 |      42 |   1 |     9 |
| Javascripts          |   3231 |   2628 |       0 |     404 |   0 |     4 |
| Libraries            |  40410 |  31991 |    1002 |    3728 |   3 |     6 |
| Tasks                |   1027 |    857 |       6 |      52 |   8 |    14 |
| Config specs         |     35 |     30 |       0 |       0 |   0 |     0 |
| Decorator specs      |   1323 |   1156 |       0 |       0 |   0 |     0 |
| Feature specs        |  35347 |  30387 |       3 |      33 |  11 |   918 |
| Helper specs         |     95 |     85 |       0 |       0 |   0 |     0 |
| Job specs            |    292 |    256 |       4 |       4 |   1 |    62 |
| Lib specs            |  39849 |  34079 |       6 |     153 |  25 |   220 |
| Mailer specs         |    112 |     94 |       0 |       0 |   0 |     0 |
| Model specs          |  30794 |  23455 |       0 |      18 |   0 |  1301 |
| Presenter specs      |    136 |    113 |       0 |       0 |   0 |     0 |
| Request specs        |   3521 |   3067 |       0 |       0 |   0 |     0 |
+----------------------+--------+--------+---------+---------+-----+-------+
| Total                | 189001 | 149801 |    1630 |    6764 |   4 |    20 |
+----------------------+--------+--------+---------+---------+-----+-------+
  Code LOC: 57079     Test LOC: 92722     Code to Test Ratio: 1:1.6

gemのバージョンを最新にする

周辺gemをアップデートし、最後にRailsを4.2系の最新にするのがゴールです。 これまでgemアップデートはセキュリティFixのみ行ってきたので、2年以上前のバージョンで止まっているgemがたくさんあります。これを全て最新にしていきます。

bundle updateでRails以外の各gemを最新にする

不要なバージョン固定を外し、 bundle update でgemを最新化します。 細かく書くと長くなるので割愛しますが、 unicorn , sidekiq などサービスへの影響が大きそうなgemは個別にアップデートし、development, test groupのgemはまとめてアップデートしていきました。 また、gemの一部の機能しか使っていなかったり、バージョンアップで大きな変更が行われ追随していくのが辛そうなgemを精査して削除も行いました。例えば cells というgemは3系から4系で大きな変更が行われ、gemを使うと楽になるというよりgemを使うために頑張るみたいな本末転倒になりそうだったのでView専用のコンポーネントを自前で実装し、削除をしました。

gemのバージョンを最新にアップデートし続ける仕組みを作る

一度gemを最新化して終わりだと、次回以降のバージョンアップ作業がまた辛い作業になってしまい手付かずになってしまいます。 そこで、毎週bundle updateして Gemfile.lock を更新したPullRequestが作られるようにしました。 こんな感じでupdateされる各gemとchangesのリンクがついたPRを勝手に作るようになっています。

f:id:dkkoma:20181210180200p:plain

月曜にPullRequestを自動作成し、誰かがレビューして翌日くらいにはリリースをするようにしました。 最初は自分で何度かやってみてからフローをチームに共有し、あとはやりたい人がやる形で今は回っています。 毎週やれているとそこまでボリュームがないので、gemのCHANGELOGを眺められたり、こんなgemに依存してたんだって発見があるので良いです。

Rails4.2から5.0にアップデートしていた間も、gemの自動アップデートは別途やっていました。

Railsのバージョンを4.2から5.0にする

Rails自体のアップデートは Rails アップグレードガイド | Rails ガイド が充実していますし、Web上にも知見が転がっておりテストが書いてあれば不安は少ないです。何よりRails本体で大きな変更をするときは、DEPRECATION WARNINGを経て変更を行ってくれている点が多く、まずはアップデートしてからDEPRECATION WARNINGを消していくということがやりやすくなっています。

おおまかには以下の流れで進めました。

  • Rails5.0でいらなくなる大変お世話になったgemを削除 🙏
    • activerecord-mysql-awesome
    • quiet_assets
    • 等々
  • 雑に bundle update -> bin/rails app:update でテストを流し、落ちているテストを直していきます。
  • Rails アップグレードガイド | Rails ガイド を参考に進めましたが差分を小さくするため、いくつかはこのタイミングではやりませんでした。
    • ApplicationRecord の導入は後回しにしました。
    • ActiveSupport.halt_callback_chains_on_return_false = false をいれ、beforeコールバックの修正を避けました。
子PullRequestを作ってspec/以下のディレクトリ毎にPullRequestをわけた

まずはテストを直していくのですが量が多いので、 spec/model だけ通すなどいくつかにPullRequestをわけて進めました。 スコープが明確なのとFile changedが小さく抑えられると読みやすくなり、レビューアに優しいです。

PullRequestで変更点を解説する

バージョンアップ作業をやってる人にとっては小さいことを自分で積み重ねているので自明ですがメモしきれないことが多いしノッているのでそもそもメモったりしないし、 変更量が多くなって見る人には辛いのでレビュー前にPullRequest上で適宜コメントをいれていました。

PullRequestの概要に全体の内容をザクッと説明してリンク等もつけたり f:id:dkkoma:20181210181030p:plain

パッと見なんでこの変更入ったんだろう?って思われそうなとこにコメントいれたり f:id:dkkoma:20181210181216p:plain

その上でチーム全員にバージョンアップで変わる点を認識してもらえるように、全員にざっと目を通してもらったりもしました。

ハマったところ

キャッシュまわりでハマったのが印象的だったのですが、Web上で情報をあまり見かけなかったのでここに残しておきます。

キャッシュにActiveRecord_Relationがキャッシュされているとエラーになる

PeXでは redis-rails というgemを利用して、キャッシュストアにRedisを使っています。

検証環境にRailsアップデート後のアプリケーションをデプロイして確認したところ、

NoMethodError: undefined method `binds' for #<Array:0x00007f7de27c4ec8>

というエラーが起きてました。

該当のコードはcontrollerで Headline というmodelのスコープを利用してキャッシュに読み込む箇所でした。

      @headlines = Pex::Cache.fetch("headline", expires_in: 5.minutes) do
        Headline.limit(5).order("updated_at DESC")
      end

そもそもこれだと ActiveRecord_Relation がキャッシュされていて、キャッシュの恩恵がないのですがそれは置いておいて、 Rails4.2のコードでキャッシュされた ActiveRecord_Relation を5.0でロードするとどうなるかという話です。 Rails5.0から ActiveRecord_Relation の実装が変わっていて、 https://github.com/rails/rails/blob/v5.0.7/activerecord/lib/active_record/relation/query_methods.rb#L119where_clause にあたるものが、4.2時代は Array だったらしくそのキャッシュをロードすると Array として復元されます。 Array に対して bind というメソッドをcallしようとしますが、 Array#bind はないのでエラーになります。

対応としては ActiveRecord_Relation をキャッシュするのをやめ、viewでfragmentキャッシュするように修正した結果、Rails4.2でキャッシュを生成させてからRails5.0にアップデートしてキャッシュを読み込んでも動作するようになりました。

また、切り戻しによってRailsをバージョンダウンしたときにも同じ問題が起きそうなので確認してみます。 Rails5.0のアプリケーションで生成したキャッシュをRails4.2のアプリケーションで読み込むテストをしてみたところ、以下のように marshal_load メソッドでエラーが出ました。

TypeError: instance of ActiveRecord::LazyAttributeHash needs to have method `marshal_load'

今度は ActiveRecord_Relation ではなく ActiveRecord が依存したクラスのロードを行うところでエラーが起きているようです。 結局 ActiveRecord のオブジェクトをキャッシュに入れる限りは、切り戻しは出来ないということがわかりました。何かあった時に切り戻しが出来ないのは困ります。

そこで、以下のようにバージョンアップ後のキャッシュキーに rails5-0 というprefixをつけ、Railsバージョンを変えた時にキャッシュ全体が切り替わるようにしました。

- config.cache_store = :redis_store, ENV['CACHE_STORE']
+ config.cache_store = :redis_store, ENV['CACHE_STORE'] + '/rails5-0'

この方法を取るとリリース時にキャッシュが全て吹き飛ぶのと同じことになるので、アクセスが少なめの時間(PeXでは昼過ぎ頃)にリリースすることにしました。 キャッシュがなくてもサービスが落ちない程度に適切にインデックスは張ってあるため、この方法で問題なさそうだと判断しました。

sessionにActiveRecordのオブジェクトが入ってて、バージョンの前後でsessionのロードができなくなった

上記の問題が解決し、ようやくリリースした後しばらく production.log を眺めていると、頻度は少ないのですが以下のようなエラーが起きていました。

ActionView::Template::Error (uninitialized constant ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::Column)

backtraceがログに出ておらず再現条件がわからなかったのですが、前述のキャッシュでハマっていたことからキャッシュが怪しそうと見て redis monitor コマンドなどを使ってエラーが起きたタイミングのredisのクエリを調べていたところ、とあるアクションを行ったユーザのセッションに ActiveRecord のオブジェクトが格納されていて、セッションのロード時にエラーになっていることがわかりました。

このケースに該当する方はサービスが全く利用できなくなってしまっているため、切り戻しを行いRails4.2に戻しました。 が、今度はリリースから切り戻しまでの2時間ほどの間に、とあるアクション(先程と同じもの)を行ったユーザがRails5.0の ActiveRecord のオブジェクトがセッションに格納され、Rails4.2のコードではロードできなくなってしまっていました。 バージョンを進めても戻してもエラーが出るユーザがでてしまい、詰んでいます...。

悩んだ結果、ロードできない(壊れた)セッションは破棄して再ログインしてもらうしかないため、以下のようなコードを ApplicationController に定義して、セッション削除(強制ログアウト)&トップページへリダイレクトすることにしました。

  before_action :migrate_session

  def migrate_session
    session[:user_id]
  rescue StandardError => e
    # 壊れたsessionのloadが走るとエラーになるのでredisからセッションを消して強制的にログアウト状態にする
    sid = request.cookies[ENV['SESSION_KEY']]
    redis = Redis.new(url: ENV['SESSION_STORE'])
    redis.del "cache:#{sid}"
    redirect_to :root
  end

session.delete でなく redis.del にしている理由は、 session にアクセスするとセッションからのロードが走ってしまいエラーを避けられないためです。 その後、セッションに ActiveRecord を格納しないようにする根本対応は別途行いました。

まとめ

gemのバージョンアップを日頃からやっておく
  • フレームワークバージョンアップ時にまとめて頑張るのではなく、週次など日頃からgemのバージョンアップをしておく。
  • これをやっておけばRails本体のアップデートはそこまで大変ではない。(Rails3時代は結構大変だった記憶ですが楽になりましたね)
キャッシュに注意
  • ActiveRecord などライブラリのオブジェクトを突っ込んでいるとバージョンアップ後にエラーが起きることがある。
    • バージョンアップでキャッシュキーは変える。
    • キャッシュが全部吹き飛んでもサービス継続出来る程度にインデックスが適切に作成されていると安心、スロークエリがでていないか日頃から確認しておく。
  • セッションに ActiveRecord などライブラリのオブジェクトを突っ込んでいるとバージョンアップ後にエラーが起きる。強制ログアウトは最後の手段なのでセッションにはプリミティブなオブジェクトをいれるようにする。

PHPカンファレンス2018で技術力評価会を再演します!企業ブースでは評価資料の公開も!

こんにちは! Zucks アドネットワーク エンジニアのしゅーぞー(@ShuzoN__)です.

12/15(土)は何の日かご存知でしょうか?
そうです! PHPカンファレンス2018 ですね!

PHPカンファレンスは国内最大級のPHPイベントです.
PHPer であれば1度は行ってみたいイベントですよね.

VOYAGE GROUPは4年連続プラチナスポンサーとして協賛しています.

昨年はレガシーシステムからの移行がテーマ

昨年はブースにて実例や実コード, 実際に経験した話を交えた事例紹介を行いました.
多くの方にご覧いただき賑わいのある出展となりました.

techlog.voyagegroup.com techlog.voyagegroup.com

今年は「事業, 世代, 専門性, 会社を超えた成長のサイクル」がテーマ!

弊社の特徴的な評価制度「技術力評価会」を軸としたセッション発表, ブース展示を行います.

セッション発表

今年も, VOYAGE GROUPエンジニアが 1セッション登壇 いたします.

ECナビエンジニア 林が 技術力評価会を25分で再演 いたします.

企業ブース

技術力評価会で用いられた実際の評価資料や結果レポートの公開を行います.

また, ajitofmの収録風景動画や弊社エンジニアが寄稿したWEB+DB PRESSの展示などもございます.

セッション発表では, 技術力評価会を再演します

セッション発表では, VOYAGE GROUPのエンジニア3名が登壇し, 技術力評価会を25分で再演します!

実際に評価会で用いられた題材を元に, ステージ上で評価会を行います!

題材は「14年続くポイントサイト"ECナビ"のポイント失効自動化について」です.

  • 発表者: 林 志嶺(@yuk1mine) ECナビ エンジニア
  • 評価者: 小賀 昌法 (@makoga) VOYAGE GROUP CTO
  • 評価者: 前田 雅央 (@brtriver) Zucks アドネットワーク リードエンジニア

技術力評価会は, 半年間で行った自分の仕事のうち最も技術的に推せる物を発表し, 他事業部のエンジニアに評価してもらう制度です.

改善業務のような「大事だけど評価されにくそうな仕事」に対しては評価が難しくなると思います.

そのような仕事に対して, VOYAGE GROUPはどのような評価を行うのか, 評価の仕組みを持っているのかを見ていただけます.

当日資料をちょっとだけチラ見せします!

f:id:namu_r21:20181212133909p:plain:w300 f:id:namu_r21:20181213121653p:plain:w300 f:id:namu_r21:20181212170656p:plain:w300 f:id:namu_r21:20181212170806p:plain:w300 f:id:namu_r21:20181212133339p:plain:w300

技術力評価会に関しては, 他社からも仕組みを知りたいという要望の声が多くなっています.
前回評価会はf-codeさんが見学に来てくれました.

www.wantedly.com

企業ブースでは実際の評価会発表資料や結果レポート, ajitofmの収録風景を公開!

以下のテーマを予定しています.

  1. 技術力評価会で用いられる評価軸, 実際の発表資料や結果レポートの公開
  2. 弊社エンジニアが寄稿したWEB+DB PRESSの展示
  3. ajitofm収録風景の動画公開
  4. VOYAGE GROUPの特色でもある事業ポートフォリオ紹介

企業ブースでは技術力評価会で用いられた資料や実際の評価結果を手にとって見ていただくことができます.

セッション発表後は, 登壇者もブースにて待機しております.

発表に関する質問, 相談の場としてもご利用ください.

またWEB+DB PRESSの展示やajitofmの収録風景を見ていただくことができます.

興味がある方はぜひいらしてくださいね.

今回は初のノベルティも登場

今回はVOYAGE GROUPノベルティを作成しました.

会場に来ていただければ手に入ります! コースターはブースにて配布, ホールドリングは懇親会の景品として出品いたします.

f:id:namu_r21:20181212142409p:plain:w300 f:id:namu_r21:20181212142447p:plain:w300

2019/1/30(水)にみなさんの前で公開ガチ評価会を行います

参加者の皆さんの前で, VOYAGE GROUP エンジニアを本当にその場で評価します.

本来, 技術力評価会は90分時間をかけて実施されますが, 本イベントでは, 短縮してお送りいたします.

後日, VOYAGE GROUPのTwitterアカウント(@tech_voyage)にて, 実際の評価結果を公表します.

評価制度に興味がある方は是非ご参加ください. 参加はconnpassから申し込み可能です.

f:id:namu_r21:20181212140326p:plain

voyagegroup.connpass.com

当日会場でお会いしましょう!

VOYAGE GROUPからは CTO 小賀, 登壇者 林, 前田, 若手3名(僕を含む)の計6名で参加します.

当日はVOYAGE GROUP企業ブースにてお待ちしております!

PHPカンファレンスへの参加はまだ間に合います! connpassから応募してください!

phpcon.connpass.com

技術力評価会、外部評価者運営レポート

こんにちはシステム本部 三浦@hironomiuです。

少し時間が経ちましたが、2018年7月から8月に開催された、20期下期の技術力評価会の「外部評価者運営レポート」をエントリーしたいと思います。

今回、私は主に外部評価者のアサイン、社外見学者との調整などの運営部分を担当していましたので、 なかなか表に出てこないと思われる運営内容や、苦労したところ、技術力評価会において外部評価者を交えたエピソードなどについてエントリーしたいと思います。

技術力評価会とは?

技術力評価会そのものについては以下のエントリーで詳しく載っていますので、このエントリーと合わせて是非ご覧ください。

seleck.cc

seleck.cc

外部評価者とは?

技術力評価会ではVOYAGE GROUP内のエンジニアのみならず、より的確な評価を下せるケースが想定される場合には 社外の技術者に依頼し評価を行って頂いています。こちらを外部評価者と呼びます。

今回は8名の方に外部評価者として参加していただきました。(超豪華!)

@mizchiさん
@songmuさん
@slightairさん
@hotchpotchさん
@soudai1025さん
@onkさん
@mirakuiさん
@voluntasさん

技術力評価会後@voluntasさんに社外評価者についてエントリーしていただけました。是非ご覧ください。

medium.com

エントリー後半部分では@soudai1025さんによるEX技術力評価会のレポートもありますので是非最後までお読みください。

社外見学者とは?

技術力評価会に興味を持たれた企業様向けに、実際に行われる技術力評価会の見学会を行っています。

今回は 株式会社エフ・コードの社員の方達が見学に来社されました。

見学後、株式会社エフ・コード社の方がレポートをエントリーしていただけました。是非ご覧ください。

www.wantedly.com

運営周りの紹介

冒頭にある通り、私は主に外部評価者のアサイン、社外見学者との調整などの運営部分を担当しました。

具体的には

  1. 外部評価者との契約周りの取りまとめ
  2. 社外見学者とのNDAなどの取りまとめ
  3. 外部評価者、評価される側の被評価者とのコミュニケーション手段の確立
  4. 社外見学者、評価者、評価される側の被評価者とのコミュニケーション手段の確立
  5. 外部評価者に対して技術力評価会、評価会後の評価擦り合わせの日程調整
  6. 社外見学者向けの評価者の評価レポートなどの展開
  7. 外部評価者を交えた技術力評価会の振り返りと打ち上げ

などを行いました。

1. 外部評価者との契約周りの取りまとめ

所謂、業務委託契約を各外部評価者と締結していきます。報酬金額の算定、契約期間、反社会的勢力の排除、秘密保持などを盛り込んだ契約書を法務に依頼し、 法務にて整えてもらった契約書を外部評価者と締結します。今回は個人請負契約以外に法人に対する業務委託のケースも発生したり、報酬は消費税を盛り込み源泉税を差し引いた金額が支払われるなど、エンジニアリングとは違った知見を得ました。

2. 社外見学者とのNDAなどの取りまとめ

技術力評価会は実際の業務にて行った資料やソースコードを用いて行われます。当然業務のコアな部分についても言及しますので、社外見学者の方とも外部評価者と同様に、会社対会社と言う形でNDAを締結することで担保します。こちらについても法務にこの要件を伝え作成してもらった契約書を今回ですと株式会社エフ・コード社と締結の窓口などを行いました。

3. 外部評価者、評価される側の被評価者とのコミュニケーション手段の確立

技術力評価会では評価者に対して被評価者から、様々な情報が提供されます。例えば、評価対象業務の概要と背景、評価して欲しいポイント、具体的なソースコード、仕様書やチケットなどが提供されます。評価者、被評価者間で評価会当日までに提供した資料に対して疑問に思うことなどについてのコミュニケーションを円滑にで行えるよう、弊社ではVOYAGE GROUPチームのSlackに特定のチャンネルのみアクセス可能なアクセスコントロールの設定をしコミュニケーションをはかれるようにしています。

4. 社外見学者、評価者、評価される側の被評価者とのコミュニケーション手段の確立

社外見学者におきましても外部評価者、被評価者とのコミュニケーション手段の確立と同様に事前情報の共有、事後についても評価結果などの共有などを行えるようVOYAGE GROUPチームのSlackに特定のチャンネルのみアクセス可能なアクセスコントロールの設定をしコミュニケーションをはかれるようにしています。

5. 外部評価者に対して技術力評価会、評価会後の評価擦り合わせの日程調整

技術力評価会は社内評価者2名、被評価者1名で基本行われます。そこに場合によっては外部評価者1名が加わります。 だいたい1ヶ月以上前から4名の空いている日程を、こちらで把握し調整するのですが、会議室含め、この調整はとても苦労しました。 7月8月は夏休みの時期であることと、毎年8月はエンジニア志望学生向けに3週間のインターン「Treasure」が開催され、 相当数のエンジニアがこの「Treasure」に講師、サポータとして参加するため更に空いてる日を探すのが難しいためです。

評価会後の擦り合わせは、外部評価者はリモートによる参加でも可としていますが評価者2名に被評価者の評価会におけるサポータ、CTOも交えて行われるため、 評価会以上に日程の調整は大変でした。 理由は単純で技術力評価会は基本S2グレード以下(弊社はG、S2、S3、S4の4段階のグレードがあります)は行うため、約40~50名の評価会がこの時期に発生します。 そのため、評価会の設定に時間が経つほど、その後の擦り合わせではCTOは全てに参加するためCTOの空き時間が日増しになくなり調整できる余地がなくなってしまうのです。

この日程調整周りはもう少し、私などの運営側が事前に関係各位が初動から終了まで見渡せたるように工夫した上で、スマートに調整できる手段は課題だと考えています。

6. 社外見学者向けの評価者の評価レポートなどの展開

社外見学者におきましては、技術力評価会当日の評価会見学だけでは不十分だと考えています。 やはり、参加した評価会において、評価者の評価結果やフィードバックについても知れることがベターだと考えています。 そのため、今回ですと、社外見学者として参加された株式会社エフ・コード社向けに見学された技術力評価会の評価者のFBを公開可能な範囲でシェアしました。

7. 外部評価者を交えた技術力評価会の振り返りと打ち上げ

技術力評価会は社内でも毎回振り返り&改善会を開いています。これによってより、エンジニアクルーの評価に対する納得度をあげ、業務に集中し結果としてエンジニアとしても成長できる施策だと考えています。

当然、外部評価者につきましても、参加による効果や課題について、振り返り&改善会を開くことで次回に向けて、更にベストな評価会とすることができると考えています。 今回の振り返り会においても外部評価者などから有意義な意見などが続出し次回に向けた手応えを感じました。

外部評価者を交えた技術力評価会の振り返り風景

f:id:hironomiu:20180912192643j:plain

以下は外部評価者含め参加者全員で振り返りに記載したKPの内容についての転記です。

外部評価者のふりかえり

Keep

  • 被評価者の技術ドメインや事業ドメインと親和性の高い外部評価者をアサインでき有意義な評価会となった hironomiu
  • 自分が関わった評価会では、外部評価者の見解で社内とは違う視点が随所に感じられ被評価者だけでなく社内の評価者も勉強になった hironomiu
  • 日程調整がとてもスムーズだった voluntas
  • slack/github で閲覧できる範囲が大きく、やりやすかった mizchi
  • 外部評価者であっても施策の前提条件や制約などが理解できるようわかりやすくまとめられていて評価に集中できた slightair
  • スケジュールを丁寧にまとめてくださって助かりました Songmu
  • 関係ないけど御社のインターンと話せてよかった Songmu
  • 実際のRepositoryに権限をもらえたのは良かったし、評価会のFB後の権限が付与されてるので改善も見えて最高 Soudai
  • 外部評価者がきたときに ajitofm を収録するのは賢い mirakui
  • 新鮮な気持ちになれる yowatari
  • soudaiさん ++ hironomiu
  • たくさんアドバイスもらえた missann
  • 放言温度感をつかめた Songmu
  • 18:00くらいにセッティングして、そのままAJITOで延長線出来たの良かった Soudai
  • 新卒の人とかが見学者で居るのは良いなって思った Soudai
  • もっと色んな人が見学できるの良さそう Soudai
  • 例えばS3とかS4を目指す人がそういうボーダーの評価のときの参考になるので見たいなって気持ちになるとおもう Soudai
  • 活きの良い若手の話を聞けるのはキラキラしてて最高だった Soudai
  • 優秀な若者や、強い評価者とコミュニケーション取れるのは良かった Songmu
  • VGがこういう挑戦的な試みをしているのは非常に参考になる Songmu

Problem

  • 例. 事業理解に時間を使いすぎコードへのつっこみが少なくなった makoga
  • 外部評価者、社内の評価者2名、被評価者との計4名との評価会の日程調整はもう少しスマートにしたい hironomiu
  • 評価会後の評価すり合わせ(外部評価者、評価者2名、サポータ)計4名との日程調整も同様にスマートにしたい hironomiu
  • 都度日程調整をするよりも一連の流れを事前にシェアしまとめて調整できると全員が幸せになれそう hironomiu
  • 複数の会社が関わったり忖度しないといけないクソコード発生過程があったりを推し量るのが難しかった mizchi
  • 社内のどうしても話せない事情が話せないので少し困った missann
  • 無限にわかる Soudai
  • 無限にわからない katzchang
  • 被評価者の評価なのかプロジェクトの内容の評価をすべきなのかわからないタイミングがあった。 slightair
  • 話の流れで脱線しがち、時間伸びがち人の評価の場であれば、その人の行動や判断、そこに至るプロセスについて話すべきな気がする反面、その判断はおかしいでのはないか、このほうが良いのではみたいな議論に発展していて、被評価者にとってはよいお土産になっている感じはあったので良し悪しの判断は難しい人によって資料の内容はバラバラだった、味がある感じなのかもしれないけれど slightair
  • 評価してほしいポイントとか明確に書いてあるほうがやりやすい
  • なぜそれが必要でどのように開発を進めたか、結果どうなったか みたいなのがほしい
  • 実装した機能の一覧やそのPRのリンクを並べられても評価が難しい(少なくとも普段一緒に業務をしていない社外評価者にとっては)事業理解のための機会が欲しかったかも。インフラのレビューをするためには、事業をそれなりにわかっていないと、サービスレベルに対する認識がずれがち mirakui
  • 請求書周りは外部評価者に対してちょっと不親切だったと思う(反省)もう少し請求書を提供しやすく請求書について説明するようにしたい hironomiu
  • 個人的に慣れてしまった気がする Songmu
  • どこまでその人の裁量を超えるか?っていうのをアドバイスするのは難しい Soudai
  • 外部評価者に合わせた内容を発表内容にしてる感があって悩ましいなって思った Soudai

EX技術力評価会

振り返り後、全員で乾杯し打ち上げが盛り上がってきたところで、@soudai1025さんによる、EX技術力評価会が急遽開催されました。AJITOにホワイトボードを持ち込まれ、@soudai1025さんの熱の篭った説明が繰り広げられています。

f:id:hironomiu:20180912211317j:plain

EX技術力評価会の後日、弊社エンジニアクルーからFBが出るなどEX技術力評価会当日以降も盛り上がりました。

評価FB @katzchangさん

f:id:hironomiu:20181113111223p:plain

評価FB @ajiyoshiさん

f:id:hironomiu:20181116094416p:plain

終わりに

技術力評価会、外部評価者運営レポートでした。適切な社外評価者を招待することは単純に技術力評価の精度を高めるだけに留まらず、社内の評価者との擦り合わせなどで、評価者の気付きの機会としても機能していると感じました。日程調整など大変でまだまだ改善の余地はたくさんありますが、今後も気持ちよく外部評価者に参加いただき、よりエンジニアクルーの納得度の高い技術力評価会の要素としてあって欲しいと考えています。

KDD2018, AdKDD参加レポート

こんにちは@hagino3000です。インターネット広告配信システムの開発をしております。去年に引き続き今年も国際会議のKDDに参加してきました。本稿は私がアドテクと業務に関係する発表を聴講したレポートになります。

KDDとは

KDD 2018 | London, United Kingdom

KDDはデータマイニング分野のトップ会議です。採択論文はResearch Track PapersとApplied Data Science Track Papersに分かれており、後者は実際のアプリケーションに適用した題材が対象です。よって、アプリケーション開発現場で対面する問題をいかに解いたか、なぜその手法を利用したのかについて発表・議論される場であるのが特徴です。Facebook, Amazon, LinkedIn, Microsoft, Airbnb, Netflix, Alibaba, Google といったネット企業が多く発表しています。

Tutorial Day

1日目はTutorial Day、午前はOnline Evaluationのセッション、午後は因果推論のセッションに参加しました。

Online EvaluationのTutorialはYandexにおけるA/Bテストの彼等の失敗事例やベストプラクティス、メトリクスの選定基準に始まり、テクニカルな話では統計的検定をオンライン評価の状況設定(Sequencial Testing)に適用するための手法の紹介。途中で組織的な話になり、A/Bテストの内容と結果をチェックするエキスパートがチームに配置されておりリリース判定をしているという話が印象的でした。質疑になると去年のA/B Test Tutorialを担当したMicrosoft ExP Teamのメンバーが存在感を発揮していたのも面白かったです。

因果推論のTutorialは超満員で参加者の注目度の高さが伺えました。こちらは統計的因果推論の基礎と準実験で使う手法を中心に解説があり最後にDoWhyを開発した話がありました。私の今の業務ではRCT一択で準実験をやる事が無いのですが、いつか役に立ちそうです。

2018 AdKDD & TargetAd

2日目のWorkshop Dayはインターネット広告がテーマのAdKDDに参加すると決めていました。メディア・SSP・DSP・データプロバイダ・アカデミアと様々な立場での研究発表と招待講演が丸一日あります。

adkdd-targetad.wixsite.com

個人的に一番だったのはVahab Mirrokni氏が登壇した点です。私が彼の論文を読んでいた最中というのが大きいですが、Online Ad Allocationの歴史と今の潮流が参考になったのとRTBのオークションメカニズムを改良するとSocial Welfareが増やせる[1]という話は夢があって素晴らしいと感じました。メカニズムデザインは不勉強で理解しきれない所が多くありましたが、価格メニューや業務設計に役立つので注目しています。

f:id:hagino_3000:20181029163116j:plain:w550
Market Algorithms Research for Display & Search Ads

DSPにおけるRTB入札最適化の発表は予算制約付き繰り返しオークションで1st Price Auctionと2nd Price Auctionが混在する設定なのが新しかったです。最適化問題を主問題と双対問題で交互に解く[2]のは入札最適化にしろ広告配信選択にしろ頻出パターンなので習得する必要があるなと。

他にも2nd Price AuctionのReserve Price設定手法の歴史、AUCをミニバッチで計算して省コスト化する手法を開発して利用している話、Facebookにおける広告キャンペーンの効果測定など興味深いネタが多く非常にエキサイティングでした。

本会議

本会議は主にApplied Data Science Trackを聴講しました。講演はマーケットデザイン研究の実社会適用[3]や予測による差別[4]といった、データマイニングが実社会に与える影響。産業界からはAmazon, Criteo, LinkedIn各社の研究トピックと、組織でいかにインパクトのある仕事を成すかという話がありました。

ここではネット広告関連で面白かった発表を2つ紹介します。

Audience Size Forecasting: Fast and Smart Budget Planning for Media Buyers

KDD 2018 | Audience Size Forecasting: Fast and Smart Budget Planning for Media Buyers

DSPの広告運用者向けに広告キャンペーンの配信セグメント設定と入札金額設定からインプレッションボリュームを推定する機能を作った話です。予算を考慮して適切なサイズの配信セグメントを作るのに利用しているそうです。

論文はビジネス要件が丁寧に書いてあり、配信セグメントをどのように作っているか、なぜこの機能が必要なのか詳細に記されているためDSPの運用者にとっては非常に参考になるでしょう。 制約は著者らのDSPの入札リクエストの規模が1,000億/Dayあるため、ナイーブにログから条件にマッチする入札リクエストをカウントするのでは実用に耐えない点。これを解決するために集合の圧縮表現としてMin-Hashを使って条件にマッチするデバイスの数の推定値を得たり、入札金額に対する勝率を関数フィッティングで求めたりしている。入力は次の通り

  • 配信セグメント設定
    • EnvType (Web or App or Both)
    • Device Type (desktop, smartphone, tablet, or any combination of these)
    • Ad Type (Display or Video)
    • Geographical Area
    • Viewability (Top X%を指定)
    • ターゲティング端末リスト、除外端末リスト
  • 入札金額設定
    • price for the average or maximum bid

「User Feedback」の節に実現した機能の利用者の感想があるのもApplied Scienceならでは。一つの機能を実現するのに、回帰・集合の圧縮・関数フィッティングと様々なテクニックの合わせ技になっている所がエンジニア的に面白かったです。

Optimization of a SSP’s Header Bidding Strategy using Thompson Sampling

KDD 2018 | Optimization of a SSP’s Header Bidding Strategy using Thompson Sampling

SSPがヘッダー入札の収益を最大化するための方策。SSP同士のオークションの入札金額をバンディットアルゴリズムで求めています。

f:id:hagino_3000:20181029163654p:plain

オークションに勝利した時のSSPの収益はDSPへの請求と媒体への支払いの差額となるため、期待収益はこれとオークションの勝率の積で表わされます。入札金額に対する勝率を求めるには他社の最高入札金額の分布が必要となりますが、この分布のパラメータθが学習できれば収益を最大化する入札金額が求まるというアプローチです。実装には対数正規分布を採用しています。 ここでパラメータθを学習するための配信(探索)と収益を得るための配信(活用)のバランスが必要となりますが、Thompson Samplingを利用して実現しています。パラメータθの事後分布の更新は非常に計算コストが高いためMCMCのオンライン版であるParticle Filterを採用したとあります。 実験データの作り方も一工夫しています。ヘッダー入札(1st Price Auction)において買い手は他者の入札金額が得られませんが、特定のパブリッシャで通常のRTBを行なった時のDSP各社の入札ログを2群に分けて仮想的なヘッダー入札の状況を作って実験用の最高入札金額を得ています。著者がSSPの人間だから可能な技ですね。

バンディットアルゴリズム適用事例として上手いなと思いました。オークションで使える対数正規分布に限らず任意の分布に一般化した話が導入になっている点も好きです、Thompson Samplingで上手くやりましたに留まらない所が。 しかしDSPからすると最終的な入札金額がSSPに操作されるので嬉しくは無いでしょう。そこに違和感を感じたのは自分だけでは無く、質疑でも「そんな契約をしているの?」というやりとりがありました。また、SSP同士のオークションが2nd Priceで、DSP同士のオークションが1st Priceの方がオークションメカニズム的に優れている気はするので、何故現状がこうなっているかも調べてみようかと。

感想

去年と比較すると1st Price Auctionを扱う発表が増えたのが印象に残りました。入札ロジックで対応するDSPの話があったかと思えば、ヘッダー入札は「入札する価値なし」としてフィルタで落しているDSPもあるのが興味深かったです。価格調整やマッチングになるとミクロ経済学・ゲーム理論のテクニックが登場するため、アプリケーション開発者でも習得すると便利なのがわかります。

学ぶ事が多く気になっている研究者に直接質問できたりと嬉しい事もありました。惜しむらくは自分の発表が無い事ですが、Applied Data Science Trackは新規性のある手法で無くともインパクトのある適用事例であれば採択されているので狙っていきたいです。

脚注

[1] Mirrokni, Vahab S., et al. "Dynamic Auctions with Bank Accounts." IJCAI. 2016.

[2] Lobos, Alfonso, et al. "Optimal Bidding, Allocation and Budget Spending for a Demand Side Platform Under Many Auction Types." arXiv preprint arXiv:1805.11645 (2018).

[3] https://www.kdd.org/kdd2018/keynotes/view/alvin-e.-roth

[4] https://hagino3000.blogspot.com/2018/10/kddandcausalinference.html に少し書きました

スキーマ定義言語 Protocol Buffers と protoc-gen-swagger を使って Web API のスキマを埋めよう

VOYAGE Lighthouse Studio の海老原 (@co3k) です。先日 30 歳になった記念としてタイトルはオヤジギャグです。

さて、普段は 神ゲー攻略 というゲーム攻略サイトを運営しているのですが、とある派生サービスを立ち上げるにあたり、 Web API スキーマ定義を gRPC に基づく形式の Protocol Buffers で書き、 protoc-gen-swagger プラグインを介して OpenAPI 定義ファイルとして生成する、というアプローチを採りました。

yugui さんの素晴らしい記事、「今さらProtocol Buffersと、手に馴染む道具の話」によってスキーマ定義言語としての Protocol Buffers がにわかに注目を浴びて以降、似たようなことをやりたいという方もいらっしゃるのではないでしょうか。

ところが、おそらく単体で protoc-gen-swagger プラグインを使う人はまだまだ限られているようで、

  • 機能が充分でなかったり、不安定であったりする
    • 複数スキーマのマージやエラーレスポンスの定義が割と最近サポートされた
    • 「デフォルト認証状態のリセット」が最近までできなかったうえに segfault で落ちていた
  • ドキュメントが不足している

という具合に、ちょっとまだ導入にあたってハードルが高めなのが正直なところです。

ただ、悪い話ばかりでもありません。ここでひとつ朗報なのですが、 2018/09/09 にリリースされた v1.5.0 では、前者の問題を解決するための変更が多く取り込まれています。

そのうちいくつかは、海老原がサービス開発中に必要となったものを修正し、取り込んでもらったものです。以下がその pull request です。

また、もちろん、他の方がおこなっておられる変更にも、 protoc-gen-swagger 単体で用いる際に有用となるものがあります。たとえば以下のようなものです。

しかしドキュメントは相変わらずありません!

そういったわけで、本エントリではこの選択をした理由のご紹介と、 v1.5.0 の新機能なども含む protoc-gen-swagger の使い方について簡単に説明していきます。同じようなアプローチを取る方の一助になれば幸いです。

TL;DR

読者の便宜のために、本エントリでご紹介する .proto ファイルと .swagger.json ファイルの例、および生成のための Makefile をまとめたリポジトリをご用意しました。ぜひご活用ください。 https://github.com/co3k/protobuf-swagger-example/

このアプローチを採った理由

  • JSON Hyper-Schema を書くのは (JSON を書かなかったとしても) 正直つらい
  • REST API をやっていくならエコシステム的に Swagger (と OpenAPI) 1 一強といった状況である
  • OpenAPI 仕様ではスキーマ部の定義に JSON Schema を記述することになるが、これも正直つらい
  • あっ、ちょうどいい感じのスキーマ定義言語として Protocol Buffers があるじゃん!
  • しかも protoc が思ったよりも強力じゃん! 知らなかった!
  • しかもしかも grpc-gateway をよく見たら protoc-gen-swagger なるプラグインがあるじゃん!

そもそも海老原は 4 年ほど前の LT、「JSON Schema で Web API のスキマを埋めよう」で触れているように (タイトルはダジャレです)、JSON Hyper-Schema による Web API スキーマ定義をこれまでずっと続けていました。これは JSON Hyper-Schema そのものにアドバンテージを感じていたというより、何でもいいので何らかのスキーマ定義を必要としていたことと、必要に迫られて contribute もした Heroku 製の prmd というドキュメント生成ツールを気に入っていたから、という側面が強いです。

正直 JSON Hyper-Schema や JSON Schema が気に入っていたかというとそんなことはなくて、かなり無理して書いていた感が強いです。もちろん JSON は human-writable ではないので YAML で書くわけですが、 JSON も YAML も汎用的なフォーマットであるがゆえに、どうしても持ってまわった書き方にならざるを得ないところがあります。

百聞は一見にしかずということで、 VOYAGE GROUP のバースペース AJITO の T シャツ裏に印字された JSON のスキーマ定義を考えてみましょう。印字された JSON は以下に 2 示す 3 とおり 4 です。 AJITO で過ごすことを ajiting と呼んでいるのですが、その ajiting とはどういったものか、というのを紹介するような内容となっています。

{ "#ajiting": {
    "description": "coding, discussion and other.",
    "url": "http://ajito.vg",
    "location": {
      "longitude": "35.6553195",
      "latitude": "139.6937795"
    },
    "beer": "free"
  }
}

この JSON 表現のスキーマを JSON Scheme によって表現してみると、たとえば以下のようになるでしょうか。

{
  "type": "object",
  "properties": {
    "#ajiting": {
      "type": "object",
      "properties" : {
        "description": {
          "type": "string"
        },
        "url": {
          "type": "string"
        }
        "location": {
          "type": "object",
          "properties": {
            "longitude": "string",
            "latitude": "string"
          }
        },
        "beer": "string"
      }
    }
  }
}

対して、 Protocol Buffers の場合はそもそもスキーマ定義を目的として作られたフォーマットであるため、簡潔な記述で済みます。

syntax = "proto3";

message Ajiting {
  message GeoCoordinate {
    string latitude = 1;
    string longitude = 2;
  }

  string description = 1;
  string url = 2;
  GeoCoordinate location = 3;
  string beer = 4;
}

message AjitingResponse {
  Ajiting ajiting_message = 1 [json_name = "#ajiting"];
}

さっそく Web API スキーマ定義を書いてみよう!

前置きはここまでとして、さっそく WebAPI のスキーマ定義を書いてみましょう。

ここで必要となってくるのが Protocol Buffers に関する以下のような知識です。

  • message の定義に関する知識
  • service の定義に関する知識
  • option に関する知識

これらについて理解しておけば、簡単な Web API 定義を書くには充分です。順番に見ていきましょう。

message の定義に関する知識

さて、リクエストやレスポンスなどを表現する message の記法については既に示したとおりです。詳しくは Language Guide を通読していただくとして、肝心なところだけ拾って説明していきます。

まずは = 1 などの謎の代入文についてですが、これは「タグ」と呼ばれるもので、フィールドを一意に識別するための番号です。が、最終的に JSON シリアライズするうえでは重要でない概念なので、とにかく 1 から順番に機械的につけていけばよい、と覚えておいてください。

また、 message は入れ子にすることができます。以下のような形です。

message Ajiting {
  message GeoCoordinate {
    string latitude = 1;
    string longitude = 2;
  }

  GeoCoordinate location = 3;
}

無名 message のようなものを定義したくなるところですがそれはできません。これでも JSON Schema に比べればまだシンプルなので、ここは名前付けの機会をもらえたと思ってグッとこらえましょう 5

それから忘れてはならないのは配列表現でしょうか。 AJITO T シャツに印字された JSON には以下のようなプロパティが存在します。

"beer": "free"

しかしこれは実に遠慮がちな表現で、 ajiting において free なのは beer だけではありません。せっかくなので Protocol Buffers における配列表現を用いつつ実態に合わせて修正してみましょう。

Protocol Buffers には repeated フィールドがあります。これは JSON シリアライズした場合に配列表現となります。

というわけで Ajiting の定義から beer を取り除き、文字列値の配列を表す以下の記述で置き換えます。

repeated string free = 5;

これによって、以下のような表現が合法となりました。よかったですね 6

"free": [
  "beer", "cocktail", "non-alcoholic cocktail", "juice",
  "talking", "coding", "playing instruments"
]

Web APi スキーマ定義用途であれば、 message について知っておくべきことはこれだけです。あとは 基本的な型 を確認しながら書いていきましょう。

service の定義に関する知識

続いて service の定義についてです。これは RPC 7 におけるメソッド定義の集合です……という説明より、実際にやってみたほうが多分早いですね。

ではさっそく何か service とメソッドを定義してみましょう。「ajiting とは心の所作」とはよく言ったもので、人にとって様々な ajiting があります。議論の場としての ajiting、作業スペースとしての ajiting、娯楽の場としての ajiting、 AJITO 以外での ajiting――ということで、全世界のいろいろな ajiting を一覧するメソッドがあれば便利そうですね。

まずは空の service を定義します。

service AjitingService {
}

次に、この service にメソッドを定義します。

service AjitingService {
  rpc ListAjiting(ListAjitingRequest) returns (ListAjitingResponse);
}

rpc に続く文字列がメソッド名です。続く括弧の中身がリクエストとなる message で、 returns に後続する括弧の中身がレスポンスとなる message です。もちろん ListAjitingRequest も ListAjitingResponse もまだ定義していないのでここで一緒に定義してしまいましょう。

まずリクエストについてですが、条件に合った ajiting の一覧が取得できたら便利そうではないでしょうか。ということで、検索クエリを指定できるような感じのメッセージを考えてみます。

message ListAjitingRequest {
  string query = 1;
}

レスポンスは Ajiting の配列と、あとは全件数でも返しておきましょうか。

message ListAjitingResponse {
  int32 num = 1;
  repeated Ajiting ajitings = 2;
}

基本的な service 定義についてはここまでの知識で充分です。

ちなみに service をどういう単位で作っていくか、というところですが、いろいろ試してみた感じ REST でいうところのリソース単位で細かく区切っていくと収まりが良さそうでした。

それからメソッドの命名については Google Cloud APIs の API Design Guide 内 Standard Methods の命名規則にとりあえず従ってつけています。この辺はお好みですが、いずれにせよある程度の一貫性があるといいですね。

option と google.api.http に関する知識

ここまでの知識だけではまだ Web API スキーマ定義を作ることはできません。ほとんどの場合、 message については特別な考慮をすることなく JSON にシリアライズ可能ですが、 Protocol Buffers でいうところの service と、 REST の文脈でいうところのリソースとメソッドという概念が結びついていません。

そうは言っても、もちろん、 service の概念を REST で表現したいというのは Protocol Buffers そのものがカバーする領域ではありません。こういうときに活躍するのが option です。これは端的にいうとファイル、 message やそのフィールド、 service やそのメソッドなどに対して任意のメタ情報を付加できる仕組みです。この仕組みの存在が、 Protocol Buffers をシンプルでありながらもパワフルな武器として成立させています。

protoc-gen-swagger (と、 grpc-gateway) は google.api.http 型のメッセージをメソッドに対する option として解釈できます。

まずはこのメッセージの定義をファイルの先頭あたりで import します。

import "google/api/annotations.proto";

そのうえで、たとえば先程の ListAjiting(ListAjitingRequest) に対して GET /v1/ajiting をマップするのであれば、以下のようにします。

rpc ListAjiting(ListAjitingRequest) returns (ListAjitingResponse) {
  option (google.api.http).get = "/v1/ajiting";
}

ListAjitingRequest には query というフィールドがありますが、デフォルトではすべてのフィールドはクエリパラメータとして扱われます。

もしパスパラメータとして扱いたい場合は、 (google.api.http).get の文字列に URI Template でお馴染みの形式で含んであげればよいです。パスパラメータに記述したフィールドは、クエリパラメータとして扱われなくなります。

rpc ListAjiting(ListAjitingRequest) returns (ListAjitingResponse) {
  option (google.api.http).get = "/v1/ajiting/{query}";
}

GET 以外のメソッドの場合も見てみましょう。既存の ajiting を PUT で編集する以下のメソッドを考えます。

rpc UpdateAjiting(UpdateAjitingRequest) returns (AjitingResponse);

UpdateAjitingRequest の定義は以下のようになるでしょうか。

message UpdateAjitingRequest {
  int32 id = 1;
  string description = 2;
  string url = 3;
  Ajiting.GeoCoordinate location = 4;
  repeated string free = 5;
}

では REST における PUT メソッドの定義を書いていきます。

rpc UpdateAjiting(UpdateAjitingRequest) returns (AjitingResponse) {
  option (google.api.http) = {
    put: "/v1/ajiting/{id}"
    body: "*"
  }
}

GET のときは違い、パスパラメータに使われなかったフィールドは、そのままではリクエストボディとして扱われません。そのため、残りのフィールドをすべてリクエストボディに含むよう、 body: "*" を指定しています。この指定がない場合はリクエストボディが空であるとみなされます。

ここで、 * 以外を指定することもできます。 UpdateAjitingRequest を以下のように変えて、 Ajiting を再利用するようにしてみましょう。

message UpdateAjitingRequest {
  int32 id = 1;
  Ajiting ajiting = 2;
}

こうしておけば、以下のように書けます。

rpc UpdateAjiting(UpdateAjitingRequest) returns (AjitingResponse) {
  option (google.api.http) = {
    put: "/v1/ajiting/{id}"
    body: "ajiting"
  };
}

OpenAPI 定義ファイルの生成

ここまでで Web API を表す Protocol Buffers 定義が出来上がりました。このファイルを ajiting.proto として保存しておきましょう。

syntax = "proto3";

import "google/api/annotations.proto";

message Ajiting {
  message GeoCoordinate {
    string latitude = 1;
    string longitude = 2;
  }

  string description = 1;
  string url = 2;
  GeoCoordinate location = 3;
  repeated string free = 5;
}

message AjitingResponse {
  Ajiting ajiting_message = 1;
}

message ListAjitingRequest {
  string query = 1;
}

message ListAjitingResponse {
  int32 num = 1;
  repeated Ajiting ajitings = 2;
}

message UpdateAjitingRequest {
  int32 id = 1;
  Ajiting ajiting = 2;
}

service AjitingService {
  rpc ListAjiting(ListAjitingRequest) returns (ListAjitingResponse) {
    option (google.api.http).get = "/v1/ajiting";
  }

  rpc UpdateAjiting(UpdateAjitingRequest) returns (AjitingResponse) {
    option (google.api.http) = {
      put: "/v1/ajiting/{id}"
      body: "ajiting"
    };
  }
}

ではさっそく protoc-gen-swagger で OpenAPI 定義ファイルを生成します。

まず Protocol Buffers (と protoc) をインストール したうえで、

$ go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger

によって protoc-gen-swagger を入手します。

あとは以下のコマンドを叩くだけ 8

$ protoc -I. -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/ --swagger_out=json_names_for_fields=true:. ajiting.proto

これで ajiting.swagger.json が生成されます。できあがったものは https://github.com/co3k/protobuf-swagger-example/blob/ce7a439ef0c692388549d3e18ab796bb3f46d5e7/01-ajiting.swagger.json に置いたので、 https://editor.swagger.io/ などでご確認ください。なかなかいい感じではないでしょうか?

ちなみに、 v1.4.1 から、複数の .proto ファイル定義を単一の .swagger.json ファイルにまとめられるようになりました。以下のように allow_merge パラメータに true を指定するだけです。便利!

$ protoc -I. -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/ --swagger_out=json_names_for_fields=true,allow_merge=true:. *.proto

OpenAPI 特有の設定をガッツリ書いていこう!

最小限のスキーマ定義はもうここまでで充分なわけですが、 Swagger のエコシステムをフル活用しようとすると、まだ物足りないところもあります。認証関連の設定であったり、ドキュメント生成やバリデーションなどですね。 protoc-gen-swagger はこのあたりもバッチリサポートしています。

そういった場合に必要な OpenAPI 特有の設定を Protocol Buffers 上で表現するのに、 option が大活躍するわけです。 protoc-gen-swagger が細かい設定類を定義するためのメッセージ群を用意してくれている ので、これを使っていきましょう 9

Swagger オブジェクト (ルートオブジェクト) の定義

一番外側のスコープで option を記述すると、ファイルレベルのメタ情報を付加することができます。

protoc-gen-swagger が提供する Swagger メッセージ (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) によって、 OpenAPI 仕様のルートオブジェクトである Swagger オブジェクトへの拡張をおこなうことができます。

まずは必要なファイルを import して、

import "protoc-gen-swagger/options/annotations.proto";

以下のように記述します。

option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
    swagger: "2.0";
    info: {
        title: "ajiting-api";
        description: "ajiting 用の Web API です。";
        version: "1.0";
    }
    host: "api.ajiting.example.com";
    schemes: HTTPS;
    security_definitions: {
        security: {
            key: "OAuth2";
            value: {
                type: TYPE_OAUTH2;
                flow: FLOW_ACCESS_CODE;
                authorization_url: "https://ajiting.example.com/oauth/authorize";
                token_url: "https://ajiting.example.com/oauth/token";
            }
        }
    }
    security: {
        security_requirement: {
            key: "OAuth2";
            value: {
            };
        }
    }
    responses: {
      key: "403";
      value: {
        description: "リソースへのアクセス権がない場合のレスポンスです。";
      }
    }
    responses: {
      key: "404";
      value: {
        description: "リソースが見つからなかったときのレスポンスです。";
      }
    }
};

はい、見てのとおり OpenAPI の Swagger オブジェクトをほぼそのまま Protocol Buffers として定義し直したような形になっているので、詳しいことは https://github.com/OAI/OpenAPI-Specification/blob/3.0.0/versions/2.0.md#swagger-object を見ながら設定していただければよろしいかと思います。

細かい注意点としては、

  • Protocol Buffers 側のキーはスネークケースで書く必要があります
  • 特定の値しか許容しない schemessecurity_definitionstype などは enum が定義されていて、その値を使っていく形になります。このあたりのドキュメントは存在しないので、 openapiv2.proto を見ながらどのような型を受け容れるのかを確認していく必要があります
  • reserverd キーワードで予約されているフィールド (Swagger オブジェクトであれば paths, definitions, tags) には未対応です

といったあたりでしょうか。

Operation オブジェクトの定義

OpenAPI における Operation は Protocol Buffers のメソッドに対応します。メソッドに対して grpc.gateway.protoc_gen_swagger.options.openapiv2_operation の option を設定することで、この Operation を拡張できます。以下は先程定義した AjitingService の ListAjiting の認証設定を上書き (ファイルレベルでは OAuth2 を強制するが、 ListAjiting は認証なしでアクセスできるようにする) してみましょう。

service AjitingService {
  // みんなの #ajiting を一覧する
  //
  // みんながそれぞれに思う #ajiting を一覧します。
  // query が指定されている場合はその条件に従った #ajiting を絞り込みます。
  rpc ListAjiting(ListAjitingRequest) returns (ListAjitingResponse) {
    option (google.api.http).get = "/v1/ajiting";
    option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = {
      security: {};  // ここでデフォルトの認証設定を上書きする
    };
  }
}

OpenAPI において、 Operation の security は Security Requirement Object の配列です。ファイルレベルで定義した認証設定とは別な認証設定を渡すことができるわけですが、ここで空配列を指定することで、認証設定を取り除く、つまり認証なしでのアクセスが許可されるようになります。

また、メソッドに対するコメントは、一行目が OpenAPI における Operation の summary として、空行を挟んでそれ以降の文字列が description として扱われます。最終的に OpenAPI 定義からドキュメント等を生成したい場合などに有用でしょう。

Schema オブジェクトの定義

OpenAPI における Schema は Protocol Buffers の message に対応します。こちらも grpc.gateway.protoc_gen_swagger.options.openapiv2_schema によって拡張可能です。

// 緯度経度情報
//
// 世界測地系 (WGS84) における緯度経度情報を表します。
// 日本測地系 2000 や 日本測地系 2011 などの他の測地系の値は受け容れませんので、
// 事前に変換をおこなっておく必要があります。
message GeoCoordinate {
  option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = {
    json_schema: {
      required: ["latitude", "longitude"]
    }
  };

  string latitude = 1;
  string longitude = 2;
}

ここでは、 json_schema によって JSON Schema を定義できます。なんだか本末転倒な気もしますが、いまのところ細かいバリデーションや required については JSON Schema 経由で定義していくしかありません。

message に対するコメントは、メソッドに対するコメントと同様に、 OpenAPI 定義における Schema の title ないし description として扱われます。

フィールドに対する JSON Schema Validation 定義を追加する

OpenAPI 定義上で利用できる JSON Schema Validation は、たとえば文字列のパターンを制限したい場合や、文字数制限をおこなう場合などに有用でした。

これを Protocol Buffers 上でも定義しましょう。フィールドに対するオプション grpc.gateway.protoc_gen_swagger.options.openapiv2_field を用いることで、以下のように表現できます。

string latitude = 1 [(grpc.gateway.protoc_gen_swagger.options.openapiv2_field) = {pattern: "^[\\-]?[0-9]{0,2}\\.[0-9]+$"}];
string longitude = 2 [(grpc.gateway.protoc_gen_swagger.options.openapiv2_field) = {pattern: "^[\\-]?[0-9]{0,3}\\.[0-9]+$"}];

特定のステータスコードのレスポンスを定義する

Swagger オブジェクトや Operation オブジェクトは responses を受け容れます。これによって特定のステータスコードのレスポンスを定義することができます。 Swagger オブジェクトに対する例を再掲します。

option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
    responses: {
      key: "403";
      value: {
        description: "リソースへのアクセス権がない場合のレスポンスです。";
      }
    }
    responses: {
      key: "404";
      value: {
        description: "リソースが見つからなかったときのレスポンスです。";
      }
    }
};

この例は説明を付加しただけでレスポンス自体の定義はおこなっていません。 RFC7807 に従ったエラーレスポンスを以下のように Protocol Buffers で定義します。

message ErrorResponse {
  string type = 1;
  int32 status = 2;
  string title = 3;
  string detail = 4;
  string instance = 5;
}

JSON Schema からはこれを以下のように参照できます。

responses: {
  key: "403";
  value: {
    description: "リソースへのアクセス権がない場合のレスポンスです。";
    schema: {
      json_schema: {
        ref: ".ErrorResponse";
      }
    }
  }
}

ガッツリ書いた結果を生成してみよう!

ということで、最終的な .proto ファイルは以下のような形になりました。実際には Swagger オブジェクトの定義や ErrorResponse などは独立した .proto に分けたいところですね。

syntax = "proto3";

import "google/api/annotations.proto";
import "protoc-gen-swagger/options/annotations.proto";

option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
    info: {
        title: "ajiting-api";
        description: "ajiting 用の Web API です。";
        version: "1.0";
    }
    host: "api.ajiting.example.com";
    schemes: HTTPS;
    security_definitions: {
        security: {
            key: "OAuth2";
            value: {
                type: TYPE_OAUTH2;
                flow: FLOW_ACCESS_CODE;
                authorization_url: "https://ajiting.example.com/oauth/authorize";
                token_url: "https://ajiting.example.com/oauth/token";
            }
        }
    }
    security: {
        security_requirement: {
            key: "OAuth2";
            value: {
            };
        }
    }
    responses: {
      key: "403";
      value: {
        description: "リソースへのアクセス権がない場合のレスポンスです。";
        schema: {
          json_schema: {
            ref: ".ErrorResponse";
          }
        }
      }
    }
    responses: {
      key: "404";
      value: {
        description: "リソースが見つからなかったときのレスポンスです。";
        schema: {
          json_schema: {
            ref: ".ErrorResponse";
          }
        }
      }
    }
};

// エラーレスポンスオブジェクト
//
// [RFC7807](https://tools.ietf.org/html/rfc7807) に従ってエラーの内容を表します。
message ErrorResponse {
  string type = 1;
  int32 status = 2;
  string title = 3;
  string detail = 4;
  string instance = 5;
}

// ajiting を表現するオブジェクト
//
// ajiting の緯度経度情報や、どのような ajiting がおこなわれるか、何が free なのかを表します。
message Ajiting {
  // 緯度経度情報
  //
  // 世界測地系 (WGS84) における緯度経度情報を表します。
  // 日本測地系 2000 や 日本測地系 2011 などの他の測地系の値は受け容れませんので、
  // 事前に変換をおこなっておく必要があります。
  message GeoCoordinate {
    option (grpc.gateway.protoc_gen_swagger.options.openapiv2_schema) = {
      json_schema: {
        required: ["latitude", "longitude"]
      }
    };

    // 緯度
    //
    // 世界測地系 (WGS84) における緯度情報を表します。
    string latitude = 1 [(grpc.gateway.protoc_gen_swagger.options.openapiv2_field) = {pattern: "^[\\-]?[0-9]{0,2}\\.[0-9]+$"}];

    // 経度
    //
    // 世界測地系 (WGS84) における経度情報を表します。
    string longitude = 2 [(grpc.gateway.protoc_gen_swagger.options.openapiv2_field) = {pattern: "^[\\-]?[0-9]{0,3}\\.[0-9]+$"}];
  }

  // ajiting の説明
  string description = 1;

  // ajiting の URL
  string url = 2;

  // ajiting がおこなわれる場所
  GeoCoordinate location = 3;

  // 何が free な ajiting か
  repeated string free = 5;
}

// ajiting を返すためのレスポンス
//
// T シャツの裏に印字されているように #ajiting をキーとする `Ajiting` を返します
message AjitingResponse {
  Ajiting ajiting_message = 1 [json_name = "#ajiting"];
}

// ajiting 一覧取得用リクエスト
message ListAjitingRequest {
  // 希望する ajiting の条件を指定するための検索クエリ
  string query = 1;
}

// ajiting 一覧取得用レスポンス
message ListAjitingResponse {
  // 該当する ajiting の件数
  int32 num = 1;

  // 該当する ajiting の一覧
  repeated AjitingResponse ajitings = 2;
}

// ajiting 更新要リクエスト
message UpdateAjitingRequest {
  // 更新する ajiting の ID
  int32 id = 1;

  // 更新する ajiting オブジェクトの内容
  Ajiting ajiting = 2;
}

service AjitingService {
  // みんなの #ajiting を一覧する
  //
  // みんながそれぞれに思う #ajiting を一覧します。
  // query が指定されている場合はその条件に従った #ajiting を絞り込みます。
  rpc ListAjiting(ListAjitingRequest) returns (ListAjitingResponse) {
    option (google.api.http).get = "/v1/ajiting";
    option (grpc.gateway.protoc_gen_swagger.options.openapiv2_operation) = {
      security: {};
      responses: {
        key: "404";
        value: {
          description: "指定した条件に当てはまる ajiting がなかった場合に返すレスポンスです。";
        }
      };
    };
  }

  // #ajiting を更新する
  //
  // 指定した内容によって #ajiting を更新します。
  rpc UpdateAjiting(UpdateAjitingRequest) returns (AjitingResponse) {
    option (google.api.http) = {
      put: "/v1/ajiting/{id}"
      body: "ajiting"
    };
  }
}

ではこれで ajiting.swagger.json を生成してみましょう。もちろんできあがったものはこちらにございます! https://github.com/co3k/protobuf-swagger-example/blob/ce7a439ef0c692388549d3e18ab796bb3f46d5e7/02-ajiting.swagger.json

まとめ

  • grpc-gateway 付属の protoc プラグイン、 protoc-gen-swagger を用いて Protocol Buffers ファイルから OpenAPI 定義ファイルを生成する方法をご紹介しました
  • そのうえで必要となる基本的な Protocol Buffers の書き方と、 OpenAPI 定義に踏み込んだ発展的な option の利用方法をご紹介しました

弊チームでは生成した OpenAPI 定義を基に、クライアント (TypeScript) 側スクリプトを AutoRest で、サーバ (Go) 側スクリプトを go-swagger によって生成することでかなり楽をできている実感があります。みなさんにもぜひお試しいただければ幸いです。


  1. 本エントリでは、ツール等を含めた Swagger のエコシステムを含めて Swagger と呼び、 OpenAPI v2 仕様そのものへの言及については OpenAPI と呼んで区別することにします。なお、 protoc-gen-swagger はいまのところ OpenAPI v2 準拠なので、 OpenAPI v3 は本エントリのスコープ外です。

  2. 現在 VOYAGE GROUP は url フィールドに記載された http://ajito.vg を所有していませんのでご注意ください。

  3. 既報のとおり VOYAGE GROUP は 2019 年にオフィス移転を予定しており、 AJITO も生まれ変わります。 ajiting の際にはこの location フィールドの座標をアテにせず、 Web サイトに掲載された最新のアクセス情報 をご参照ください。

  4. この free は「言論の自由 (free speech)」ではなく「ビール飲み放題 (free beer)」の方の free です。

  5. 一応このように定義しておけば、外から Ajiting.GeoCoordinate として参照できるというメリットもあります。

  6. この free は「言論の自由 (free speech)」の free でもあり「ビール飲み放題 (free beer)」の free でもあります。

  7. もちろん gRPC も含みますがこれに限定されません。

  8. JSON スキーマ上で #ajiting というキー名を利用可能にするために json_names_for_fields オプションを指定しています。通常はあまり気にすることはないと思いますが、覚えておいて損はないオプションかもしれません。ちなみにこのオプションは v1.5.0 にて最近追加されたものです。

  9. なお、ここから説明することは本当にドキュメントがないので心して読んでください。