読者です 読者をやめる 読者になる 読者になる

まとまってない文章を晒すのに抵抗があったけど、メモを垂れ流したら仕事がうまく回りだした件について

Zucks Ad Networkでデータ解析をしています、@yuu_itoです。

気づいたら3月も半ばですね。花粉で目がしょぼしょぼします。

メモを取ることについて書いていきます。

きっかけ

技術調査のために論文を集めGoogle Docsにまとめていた時、 とりあえずまとめた後に共有しますねと連絡したら、 メモはGitHub Issueへのコメントで書いておいたら?というのが始まり。

f:id:u110:20170313092305p:plain

やってみて気づいたこと

まとまっていない状態の文章を晒すことに抵抗があったのですが、やってみると嬉しいことがありました。

1. Issueに気づいた人がコメントをくれる。

弊社ではGitHubとSlackを連携してIssueの更新をSlackに通知しています。 取り組んでいること、考えていることを書いていると

  • 「なんかデータの傾向がイメージと違った」→「それ~だからかも」
  • 「計測しているデータ、不要なものも含まれている」→ 「あ、それ実はこっちのテーブルが~」

のように、気づいた人からコメントをもらえました。 メモのひとつが小さいと何か言えそうな人のコメントもしやすいと思います。

経過がSlackに流れていることでチームメンバーが自分のやっていることを知っているので 次のアクションについて話しても説明が不要だったりしました。 (まとめるとはなんだったのか)

2. 経過がわかるので迷走したら振り返ることができる。

本当にこれはよくあることなのですが、のめり込みすぎて「結局何したかったんだっけ?」という状態に陥っても そのとき考えていること、疑問に思っていたことなども含めてメモに書いておくと 今までの作業の流れをはっきりを振り返ることができ、どの時点で問題があったのか気づくことができました。

f:id:u110:20170313092315p:plain

まとめ

  • 経過がわかるようにメモを書き出しておくと良いことがある。
  • 人に見えるとこに書いておくと、ヒントをもらいやすい。
  • 方針に迷ったり、悩んでるときこそ、考えていることを書き起こすと良い。

たまにSlackのチャンネルはメモがずっと並んでしまいますが、 自分の所属するチームでは許容してもらえてると認識しています。

インフラチームと開発チームの垣根をなくすためにAWSのCI環境を構築した話

こんにちは、VOYAGE GROUP システム本部の @s-tajima です。

PHPカンファレンス2016 の「老舗メディアが改善に取り組んでいる話」でもお話した通り、長年オンプレミス環境で稼働してきたECナビを、AWSに移転しようというプロジェクトが進行しています。

そしてなんと先日、約24時間のメンテナンスを経てECナビの本体(Webサーバ, 管理画面サーバの一部, データベースサーバ)がAWSに移転しました!

AWS移転において得た知見, 構築したシステム等は数多くありますが、今回はCloudFormationとTravis CIを用いて 生産的安全手軽 なAWSのCI環境を構築したお話です。

背景

ECナビは、500万人を超える会員を抱えたVOYAGE GROUPが運営している中でも特に大きなメディアの1つです。
今回、そんなECナビのインフラ調達期間の削減、検証環境構築の簡易化、レガシー環境からの効率的な脱却等、ユーザーさんに価値を届けるスピードを加速させるための施策の1つとしてAWSに移転することを決めました。

17年もの長い期間オンプレミスで稼働してきたシステムのため移転に必要な作業は数多くあり、その中の一作業であるAWS環境の構築 だけに それほど大きな手間を掛けるわけにはきません。

このような背景を考慮して、ECナビのAWS環境の構築は

  • ユーザーさんに価値を届けるスピードを加速させるという目的を阻害しない 生産的 な環境
  • 多くのユーザーさんを抱えるメディアとしてそれに見合った 安全 な環境
  • 大きな手間をかけず 手軽 に実現できる環境

を目指して進めようと考えました。

生産的な環境 とは

オンプレミス環境での運用時、よく見られた光景が

  • ECナビ開発チーム「新しいサーバー使いたいわ。インフラチームさん用意してー。」
  • インフラチーム「はいはい。でもちょっと忙しいから来週まで待ってね。」
  • ECナビ開発チーム「(う、サーバーがもらえないと先にすすめない。。)」

というものです。 ECナビ開発チームのメンバーからすると、 自分でコントロールのできない待ち時間となり、時間のロスが発生してしまっています。

このようなやり取りは、弊社以外でも多く見れるかと思います。
オンプレミス環境で運用していると、データセンターの運用や物理機器の調達等を特定のチームで担当したほうが効率がよい場面が多く、自然とこのような分業が進んでしまうのかと思います。

AWSに移転した後、さきほどのやりとりが

  • ECナビ開発チーム「新しいEC2インスタンス使いたいわ。インフラチームさん用意してー。」
  • インフラチーム「はいはい。でもちょっと忙しいから来週まで待ってね。」

となってしまうとオンプレミス時代のような時間のロスがそのまま残ってしまいます。
本来はECナビ開発チームのメンバーが新しいサーバーを使いたくなった時には、待ち時間なく自ら環境を作成できるようになるとよさそうです。

今回はこのような時間のロスをせずに済む生産的な環境を目指しました。

安全な環境 とは

では、前述のような生産的な環境を実現するために、単純にメンバーみんなに権限だけ渡して
各自の好きなようにAWSリソースの操作をしてもらうようにして終わりでよいでしょうか?

もちろんこのようなやり方でうまくいく場合もあるかと思いますが、 そのような環境では、

  • オペレーションミスによって本番用のEC2を誤ってターミネートしてしまった。
  • いつの間にか本番用のRDSがインターネットのどこからでもアクセスできるように設定されてしまっていた。
  • 設定した経緯がわからず、不要だと判断し削除したSecurityGroupの接続許可設定が、
    実は使われている頻度は少ないがとても重要なものだった。

といった事故が起きてしまう心配が残ります。

ここまで極端な例はなかなか起きないとしても、 多くの人が好き勝手にAWSリソースを操作でき、その変更内容の管理ができていないような環境では多くのユーザーさんを抱えるメディアとしてそれに見合った安全な環境を提供できてるとは言えないと考えました。

理想としては、

  • AWSリソースの操作はレビューを通してから行う
  • AWSリソースの操作があったタイミングで通知を行う
  • AWSリソースの操作の内容は記録に残し、後から履歴や経緯を追える

ということができるような安全な環境を目指しました。

手軽に実現できる環境 とは

ここまでの要件を満たせるような環境は、サーバーを用意し、便利なツールを導入し、さらに必要であれば自らコードを書くことで(つまり時間さえかければ)いくらでも実現できるでしょう。

しかし、自前でこのような環境を用意しようとすると権限管理、バックアップ、冗長化、監視等、障害時の対応等、本当に必要だった要件に加えて考えなければいけないことが次から次へと出てきます。

このあたりを可能な限りマネージドサービスにまかせてしまうことで、手軽に環境が用意できることを目指しました。

実現方法

ここまでの要件を満たすために、今回はこのような環境を構築しました。

f:id:s_tajima:20170217133520p:plain

この構成では以下のような流れでAWSリソースを操作します。

  1. GitHubで管理されたCloudFormationのテンプレートを修正し、Pull Requestを作成 (作業者)
  2. Pull Requestの作成/更新をフックにビルドを実行 (Travis CI)
    • 2.1 ValidateTemplateを実行
    • 2.2 CreateChangeSetを実行
  3. Pull Requestの内容とChangeSetの内容をレビュー (レビュアー)
    • 問題がなければLGTM
    • 問題があれば指摘して2からやり直してもらう
  4. ExecuteChangeSetを実行 (作業者)
    • 4.1 CloudFormationがAWSリソースを操作
  5. AWSリソースの変更結果を通知 (CloudFormation -> Lambda -> Slack)
  6. 変更の結果を確認 (作業者 or/and レビュアー)
    • 問題がなければ次に進む
    • 問題があれば2からやり直し
  7. Pull Requestをマージ (作業者)

いくつかのポイントを説明します。

まず1つ目のポイントは、AWSリソースの操作にCloudFormationを使い、そのテンプレートをGitHubのリポジトリで管理している点です。 このリポジトリには、弊社のメンバーであれば誰でもPull Requestを送ることができます。 そのため、必要だと思ったメンバーが自らテンプレートを書き、それを適用することですぐに必要なリソースを構築することができるようになっています。

変更作業の前にPull RequestやChangeSetのレビューを受けることで、意図しない変更や経緯のわからない変更がされてしまうことを防ぎます。

ChangeSet(変更セット)についてはこちらに詳しく記載してあります。
https://aws.amazon.com/jp/blogs/news/new-change-sets-for-aws-cloudformation/

また、CloudFormation操作の通知はSlackに飛ばされるようになっていて、もちろんCloudTrailもAWS Configも有効にしてあります。
このように、 生産的安全 な環境を実現しました。

2つめのポイントは、Travis CIを利用している点についてです。 これは言うまでもなく 手軽 にCIの環境を手に入れるための手段です。 しかしそれだけではなく、Travis CIを 安全 に利用するための工夫をしています。

Travis CIに登録してあるIAMユーザーの権限は、 ValidateTemplate, CreateChangeSet に限定しています。
そのため、もし仮にTravis CIからIAMのクレデンシャル情報が漏れてしまっても、その情報だけでは ExecuteChangeSet を実行できないためAWSリソースを自由に操作することはできません。

また、テンプレートを編集できるメンバーは ExecuteChangeSet と必要なリソースの参照の権限しか持ちません。
CreateChangeSet の実行をTravis CIからのみに絞ることで、本CIフローから外れたAWSリソースの操作が行われてしまうのを防いでいます。

まとめ

以上、CloudFormationとTravis CIを用いた 生産的安全手軽 なAWSのCI環境の構築のお話でした。

冒頭の自己紹介の通り、僕はシステム本部所属のインフラチームのメンバーなのですが、PHPカンファレンスの資料にあるような交換留学の一環で現在はECナビの開発チームのメンバーと共にECナビの環境改善に全力を注いでいます。
今回の話もインフラチームと開発チームの垣根をなくすためのとてもよい施策になったと思っています。

お知らせ

現在、(ECナビを含む)メディア領域、システム本部ではそれぞれ一緒に働く仲間を募集しています。

どちらのチームにも、まだまだ解決したい課題がたくさんあります。
ご興味のある方はぜひお気軽にご連絡ください。

#再演 します。「エンジニアの技術力評価は難しい? - 5年間運用してきた技術力評価制度の改善の歴史 ‒」現役の評価者/被評価者も参加予定!

技術力評価会 再演

こんにちは、月日が経つのは早いものでCTO歴が6年半を越えたmakogaです。

ご縁があり、今年の1/12(木)にRegional SCRUM GATHERING Tokyo 2017で登壇しました。内容はエンジニアの技術力評価を5-6年掛けてどう改善してきたかです。

翌日スライドを公開したところ、多くの方に見てもらえ、はてブTwitterなどでたくさんのコメントもいただきました。

コメントを読むと、さまざまな考え方があると感じます。また、当日の懇親会や後日会った方との意見交換はとても楽しいものでした。今後も技術力評価会を改善していくにあたり、もっとたくさんの方と意見交換していきたいと思い、今回 #再演 イベントを開催することにしました。

また、当日の質疑応答では「うちでやろうとすると、評価するのを嫌がるエンジニアが出てくると思うのですが、そういうのはなかったですか?」というような質問がありました。それを社内のSlackにpostしたところ下記のようなやりとりがありました。

f:id:voyagegroup_tech:20170214175119p:plain

Regional SCRUM GATHERING Tokyo 2017ではVOYAGE GROUPからの参加は私だけでしたが、今回の #再演 イベントでは現役の評価者/被評価者も参加予定です。「評価されるって実際どうなの?」「評価者大変じゃない?」など、参加者の方々から疑問を投げかけてもらえると盛り上がるのではないかと期待しています。

エンジニアの評価や育成、組織における制度設計・運用などに興味がある人たちとの交流を楽しみにしていますので、まずは気軽に下記connpassイベントをクリックし「このイベントに申し込む」をポチっと押してみませんか!

connpass.com

GitHubにおけるPull RequestのAssign/Mergeを自動化して開発を加速させる

事業を支える技術 開発フロー

皆さんこんにちは. 現在はfluctにてfluct DRという広告配信システムの開発を行っております, 大関です.

GitHub上でのチーム開発では, レビューの依頼や, CIが通ったことを確認した上でのPull Requestのマージといった複数の作業が発生しますが, これらはGitHubのUIを複数回クリックする必要があり, 非常にストレスフルな作業です.

本稿では, こうした定形作業を自動化するbotとしてpopukoを開発・導入することで, 我々開発者のストレスを軽減するとともに, より堅牢かつフィードバックの多い開発が実施できるようになった事例を紹介します.

GitHubでの開発はとてもクリック操作が多い

前段でも述べたように, GitHubを用いたチーム開発においては, 数多くの定形作業が存在します. コードレビューの可能な人を探してレビューを依頼する, 依頼の度に対象者をAssigneeに追加する, コードをレビューする, upstream(多くの場合においてmasterブランチ)とのコンフリクトが発生していないかを確認する, reviewerによる許可が出た後にマージボタンを押す, Pull Requestをupstreamにマージした後もCIがgreenのままでいるかを確認する, などなど. GitHubのデフォルトのUIだけでは, これらは手動で実施する必要があり, 非常にストレスフルな作業です.

また, それぞれのPull Requestの状態についてはラベルで管理していない限りは一覧した場合に判断しにくく, かといってラベル管理している場合はそれはそれでラベルを付け替える手間が発生してしまいます.

我々はソフトウェア開発者ですので, このような鬱屈とした作業に関しては直ちに自動化を行い, クリック作業を減らし, ストレスの多い生活から開放されなければなりません.

自動化したい

このようなGitHubにおけるPull Request作業の自動化という分野については, homuと呼ばれる高機能かつ素晴らしいbotが存在しています. homuはGitHubにおけるPull Requestのマージ処理の自動化やTravisCIとの連携など数多くの機能を持つbotです. かつてMozillaにてRust Languageのtech leadを務めていたGraydon Hoareの語った

The Not Rocket Science Rule Of Software Engineering: automatically maintain a repository of code that always passes all the tests

の思想に基づき開発されたborsの流れを組んでおり, Rust ProjectならびにServo Projectで使われています.

ですが, 本家側の更新は止まって久しく, MozillaにてforkされたバージョンはMozillaのビルドインフラやServoプロジェクトに合わせた変更がなされており, 他のプロジェクトが自前でホストする形式での利用を積極的に薦める状態にはありません.

自前でホストするのを諦めるのであれば, homu.ioを使用するという選択肢もあります. しかしながら, 使用するに際して, 運営元の不明瞭な外部サービスにプライベートリポジトリへのアクセスを認めることとなり, セキュリティ上の観点からは決して望ましいものではありません.

また, homuは各リポジトリごとのreviewerの設定を, 中央集権されたhomu向けの設定ファイルに記述することで管理しています. ですが, 私達は, このような設定に関しては各リポジトリごとに管理可能にし, それぞれのリポジトリ内でreviewerやbotの機能の設定を完結させたいという意志がありました.

これらの理由から私達は, 自分達の要求に足るだけの最小機能だけをpopukoとして再実装し, 私達のプロダクト開発に導入することとしました.

popuko

popukoは以下のモデルでPull Request駆動による開発を補助します

  • webhookを起点にして動作を行う
    • コメントの書き込みややpushイベントに反応して動作する
  • ラベルを用いた状態の可視化を行う
    • rebase必須, レビュー待ち, マージ待ち, など
  • 各リポジトリのトップレベルディレクトリに配置されたOWNERS.jsonファイルに書かれた設定に基づき動作する

この原則に基づきに, 現在, 以下の機能を実装しています.

  • Pull Requestに書き込まれたコメントに基づいて何かする
    • reviewerの割当とラベルを変更する
      • Pull Requestがapproveされたらラベルを変え, 一旦upstreamとマージした結果を試す仮ブランチを作り, CIがgreenになることが保証されたら正式にマージする.
  • upstream側が変更され, Pull Requestがマージできなくなった場合, push logとともに其の旨を当該Pull Requestに書き込む

ラベルによる状態の可視化

popukoは以下のラベルを用いてPull Requestの状態を管理します. このラベル名はServo Projectのラベル定義を参考にしています.

ラベル名 意味
S-awaiting-review レビュー待ち
S-needs-rebase conflictしてるのでrebaseが必要
S-awaiting-merge マージ待ち
S-fails-tests-with-upstream Auto-Mergeを用いてマージしようとしたらテストが失敗してしまった

具体的な機能

それでは, それぞれの機能について見ていきましょう.

reviewerをassignする

r? @<reviewer> の形式でPull Requestにコメントすることにより, <reviewer>をAssigneesに登録し, ラベルをS-awaiting-reviewに変更します

popukoがreviewerをAssigneesに登録してラベルを変更する例
f:id:saneyuki_s:20170210193146p:plain

masterとのconflictを検知する

masterブランチにpushが為されたことをwebhookのpushイベント経由で受取り, openなPull Requestを巡回します. 巡回したPull Requestのうち, conflictを起こしているものに対して以下を行います

  • ラベルをS-needs-rebaseに付与する
  • pushイベント経由で取得したchangeset urlをコメントし, コンフリクトの原因となっている可能性の高い変更をコメントする

popukoによるconflict通知の例
f:id:saneyuki_s:20170210193216p:plain

マージ可能であると知らせる

@<botname> r+もしくは@<botname> r=<reviewer>とコメントすることにより, 当該Pull Requestの最新のcommit hashと共にreviewerの名前をコメントに書き込み, ラベルをS-awaiting-mergeに変更します. 他の作業とは異なり, このコメントを書き込める(botが反応する)のは, OWNERS.jsonファイルによって予め指定されたユーザーだけです.

Auto-Merge

OWNERS.jsonauto_merge.enabledtrueに指定されている場合, popukoはPull Requestの自動マージを試みます. フローは以下の通り:

  1. reviewerが@<botname> r+とコメントする
  2. popukoはマージ先のブランチに対して仮ブランチ(autoと呼びます)を作成する
  3. 2で作成されたauto branchに対して, Pull Requestの内容を仮マージする
  4. auto branchに対してCIを実行する(TravisCIであれば, CI対象ブランチを制限していなければ自動的にCIが動き出す)
  5. CIが完了したら其の結果を確認し, greenであれば改めて正式にPull Requestをマージする

これにより, masterにマージされるPull Requestは, マージ後も含めて常にgreenとなることが自動的に保証された上でマージされるようになります.

popukoによるブランチの取扱の図
f:id:saneyuki_s:20170210190956j:plain

popukoによるauto mergeの例
f:id:saneyuki_s:20170210193120p:plain

OWNERS.json

各リポジトリごとのrootディレクトリに配置します.

ここに@<botname> r+を発行できるreviewerの名前を記載しておくことで, popukoはそれを用いて, 許可した人間のみがPull Requestにマージ可能である旨を示せるようにしています.

導入の結果, 何が変わったのか?

popukoの導入の結果, 当初の目的通り, 以下の点が達成されました.

  • 3~4クリックが1クリックに減った,
  • 常にmasterの状態がgreenのまま維持されるようになった

特に前者に関しては, SlackとGitHubの二重のレビュー依頼をしている同僚も多かったのですが, popukoにより, GitHubに書くだけで自動的にassignまで行われるようになったのでGitHubだけで完結させることのほうが合理的になりました. Slackでレビュー依頼が必要な場合は, 長期間放置されているものの催促や緊急性の高い内容に限られるようになり, Slack上での会話の濃度も上がりました.

後者についても, Continuous Delivery/Deploymentの観点から非常に役に立ちます. 「動かない(既存のテストケースで洗い出せる問題のある)コードをウッカリmasterブランチに対してlandしてしまう」という人為的ミスが起こらなくなるので, より堅実かつ問題の切り分けを行いやすいプロダクトリリースを実現するための礎となります.

また, 導入前は「これ本当に便利なの?」と懐疑的だった同僚も, 今では「無いと仕事にならない. 病み付きになる」と述べるようになるなど, 「無くても成立するが, 一度使ってみると離れがたい引力がある」種類の道具となりました.

併せて社内に#popukoという名前のbotのサポート・開発専用channelを用意し, popukoのリポジトリへの設定方法についても Popuko as a Service (PaaS) と銘打って文書化しているため, 使用したい社内のリポジトリについては各リポジトリの判断で自由に導入できるようにしています.

FAQ

popukoって名前の由来は?

popukoは Practical Organized Productive Unlimited Kabuki-nized Operation の略です

popukoに機能追加するくらいならhomuをforkしたほうが良かったのでは?

forkせずスクラッチしたのは出来心も多分にありますが, Goでスクラッチしたのは以下の技術的な判断です:

  • go-gihubという便利なGitHub APIラッパーがあった
  • GitHubのREST APIを大量に呼び出すという性質と, 数多くのリポジトリで同時多発的にトリガーとなるコメントが書き込まれ, それを捌くことを考えた場合, goroutineによる処理モデルは当botの扱う問題に適していると判断した
  • シングルバイナリを生成して適当な環境に放り込んで動くのは便利
    • ゲリラ的に正規導入するかわからないプロジェクトを始めるときは, Dockerコンテナとか作らずに, とにかく大雑把に動かせることが重要
    • アプリケーション性質的にはNode.jsでも良かったが, とにかく単一ファイルを放り込んで終わりにしたかったので, この点に見合わなかった.
  • 別にhomuの機能全部は要らなかった
    • 「こんなの直ぐに実装終わるだろ!」と思った

類似のhomu再実装は他にもいくつか存在しますが, 以下の理由からそれらの採用は行っていません.

  • mgattozzi/thearesia
    • これは現在でも未完成なので, 自然と対象外になった.
  • gullintanni/gullintanni
    • r? @<reviewer>のようなhighfive相当の機能が実装されておらず, highfiveのデプロイも必要な事を考えると, homu + highfiveの構成をそのまま用意するのと大差は無かった.
    • reviewerに相当する設定をリポジトリ単位にコードとして管理させる機能も見当たらなかった
  • rultor
    • GitHub botのためにJVMを動かしたくはなかった(JVMを多用しているチームであれば, 有効だったかもしれない)
    • コマンド体系が(私は)良いとは思えなかった
    • highfive相当の機能も実装されていない

そもそもとして「 botと対話するインターフェースによって自動化がなされる 」というのが重要な点です. それを実現する実装は必ずしも重要ではありません. 今後, popukoへの機能追加が見合わないと判断した場合, 改めてhomuをforkすることや類似のbotに乗り換えることも勿論有りえます.

SlackからGitHub botを操作するほうが良いのでは?

私は良いとは思っていません. GitHubはコードを取り扱うサービスであり, コードおよびGitの操作に関してはGitHub上で完結させるべきだと考えています。merge botのヘルスチェックや再起動はSlackからの操作でも良いと思いますが、Gitの操作に関してはGitHub上でbotへの指示がなされるべきです.

masterとのconflictを検知する」とあるけれども, popukoはmaster以外に向けたPull Requestに対しては検知できないの?

現在のところは検知できる実装にはなっていません.

根本的な前提として, 我々のチームについてはmaster以外のブランチに向けてPull Requestを送る開発習慣が存在せず, masterブランチをtrunkと見做す方式でのTrunk駆動開発に基づいてプロダクトの開発をしています. feature branchを切ることや, release branch専用のPull Requestを用意することは頻度として稀です.

また, 設定を各リポジトリ毎に分散して持たせている以上, masterに相当するブランチは各リポジトリごとに持たせるべきではあります. このdesign上の制約により, masterブランチ以外も自由にtrunkとして設定可能にすると, pushイベントが発生するたびに設定ファイル取得のためにGitHub APIを消費してしまうという問題が有ります.

こうした点を考慮し, 課題であるとは認識していますが, 私達の実用上の観点から直ちに修正するべき課題であるとは考えていません.

まとめ

以下がまとめとなります

  • 面倒くさいことは自動化しましょう
    • 便利なのでGitHubにmerge botを導入しよう
  • masterブランチ(trunk)が常にgreenであることが機械的に保証される安心感はとても良い

お知らせ

弊社には AJITO という社内バーがあり, 毎夜のようにエンジニアが現れ, 酒を嗜み, 軽食をつまみつつ, エンジニアリング談義を行っています( #ajitingで既にご存知の方も多いと思います). 「私も一緒に(こういう)エンジニアリング談義をしたい!」という方がいらっしゃいましたら, 是非とも弊社エンジニア or @tech_voyageに, お声掛けいただければと思います.

また, fluctを含むVOYAGE GROUPアドテクユニットでは, 一緒に働いてくださる仲間を募集しております. VOYAGE GROUPや広告配信に興味を持たれましたら, お気軽にご連絡ください.

2016年を締め括るROCK FESの話

こんにちは。@kanufyです。
みなさん、2016年はいかがでしたか?
楽しめましたか?エンジニアリングしましたか?ROCKしましたか?

VOYAGE GROUP Advent Calender 2016
最終日はROCKの話をしたいと思います。

会社でROCKするんだよ、それがROCKだ

弊社にはサークル活動 (※1)があります。
色々なサークルがあるのですが、音楽好きが集まり、なんかよくわかんないけどいろんな楽器やってワイワイしようぜ!っていうサークルがあります。
その名もVOYAROCKサークル!

今日は、先日12/22にVOYAROCKのコンサートが開催されたのでその様子をお伝えしようかと思います。

エンジニアも営業も法務も役職に限らず

VOYAROCKに限らず、弊社の様々なサークルメンバーは職種に限らず多種多様であり、サブメンバーを含めるとエンジニアから法務といった様々の職種の方が所属しています。
そのため、普段関わらなかった人とも関われる機会となり、サークル活動は社内の情報交換の場としても使われていることが多いです。

VOYAROCKランチコンサート

VOYAROCKでは、定期的にコンサートを開催しており、前回は8月末にゲーム音楽をテーマとして開催されました。
12/22に行われたVOYAROCKコンサートでは、映画音楽をテーマとしてセットリストが組まれました。

  • 戦場のメリークリスマス
  • Goodbyedays
  • 桜流し
  • なんでもないや

昔から最新の映画音楽ですね!
弊社のバースペースであるAJITOにて開催されました。

f:id:kanufy:20161224014433j:plain:w500

f:id:kanufy:20161224014452j:plain:w500

f:id:kanufy:20161224014512j:plain:w500

VOYAROCK FES

同日の夜には場所を変えて大会議室にて夜の部が開催されました!
セットリストは以下になります。

  • なんでもないや~Band ver.~
  • Stand by me
  • Shape of My Heart
  • Thinking Out Loud
  • 月のしずく
  • swallowtail butterfly ~あいのうた~

映画音楽じゃないものもありますが、お酒を飲みつつROCKに浸る。
これぞフェスですね!

f:id:kanufy:20161224225817j:plain:w500

f:id:kanufy:20161224081846j:plain:w500

フェスのあとにはおかわりタイムということで、各自やりたい曲をやったり即興でセッションが行われたり
アンコールをしたりと楽しい時間を過ごしました。
VOYAROCKではどんどん機材も充実してきており、PAのスキルアップもできたりしますよ(笑)

なお、この記事を書いているのは雪山から帰還中の新幹線の中です。
わたし実はスキースノボサークルの部長やってます。そう、サークル活動で合宿中です。ROCKですね。
サークル活動にクリスマスイブとか関係ないですよね!

それでは2017年もVOYAGE GROUPをよろしくお願いします。

企業ブースで実コード公開をした話 #phpcon2016

php

この記事はVOYAGE GROUP techlog / Advent Calendar 2016の記事として書いています。

こんにちは、@pro_shunsukeです。

VOYAGE GROUPはPHPカンファレンス2016にスポンサーとして協賛させていただきました。PHPカンファレンス2016に関してはこのブログの中で事前告知発表のあとがき、また企業ブースを出して学んだことなどを記事として掲載しています。今回でPHPカンファレンス2016に関する記事は4つ目という事になります。

さて、上記の記事にも詳しく書かれているのですが、弊社ではスポンサーとしてプレゼン発表や企業ブースを出展させていただきました。どちらもとても多くの方に興味を持っていただきました。今回はその中でも特に興味を持っていただいた 企業ブースでの実コード公開 をした際の知見ついて書こうと思います。実コードを見せるに至った経緯や当日の反応、また全体を通しての反省点を記事として残していこうと思います。

なぜ実コードを見せる事になったのか?

カンファレンス当日は弊社の@dkkomaによる「老舗メディアが改善に取り組んでいる話」という発表が行われました。

speakerdeck.com

17年間運営しているECナビというサービスについて、歴史を踏まえてどのように改善に取り組んできたのかについて発表しました。 しかしやはり発表だけではどうしても綺麗事のように聞こえてしまう部分があり、より具体的な取り組みが分からないのでは?という事になりました。そこで 実際に改善したコードの具体例を紹介 することで、見に来ていただいた人に1つ1つの地道な改善を実感していただきたいと思いました。

どのようなコードを選んだのか?

実際にプロダクトで使われているGithubの Pull Request(PR) を3つ選び、差分を見ていただきながら改善の様子を紹介したり、レビューの様子を紹介しました。3つのPRは、発表の中でもありましたKAIZEN会*1で出たコードを選びました。また、 ドメイン知識の必要のない外向けに分かりやすい内容のもの を選びました。

当日紹介した実コードの例

f:id:pro_shunsuke:20161217154120p:plain

当日の反応

とても興味をもっていただいた人が多く、さまざまな反応をいただきました。

反応していただいた意見

以下のような意見を多くいただきました。

  • 「分かる。大変だよね」
  • 「どうやったらうちの組織でも改善出来る体制になるだろう?」

地道な改善作業に対する共感の声がありました。

また、改善するのは大切だけれどなかなか手が出せない、どうしたらそのような体制づくりが出来るだろう?という相談も多かったです。難しい相談ですが、1つは長くサービスを続けていく上では継続的な改善が必要だという事に対するプロデューサーの理解があった事が大きかったと思います。

どのような人に興味を持っていただいたか?

興味をもっていただいた人は30代, 40代くらいのベテラン感のある人が多く、若者にはあまり響かなかったようでした。やはり「改善」に焦点を当てていたこともあって、業界の経験が豊富な人により響いたようでした。

全体としての反省

基本的にはとてもよかったのですが、いくつか反省点もありました。

体制について

予想を上回る反応をいただいた事はとても嬉しい事でしたが、 当初想定していた体制では興味を持っていただいた人全員には説明しきれなかった という事がありました。

最初は説明者1人とモニター1台で望みましたが、徐々に来場者が多くなるに連れて説明が間に合わなくなりました。当日は臨時で説明者を増やし 3人体制 にしたことで、ピークのランチタイムにはちょうど良くなりました。

お客さんの要望について

例えば以下のような要望がありました。

  • 「プロジェクト全体のディレクトリ構造はどうなっているの?」

当初はPRだけを見せるつもりでいたので、ディレクトリ構造までは見せることが出来ませんでした。しかし後になって考えてみたところ、ディレクトリ構造くらいなら見せても良かったのでは、ということになりました。 どこまでを見せてよく、どこまでは見せてはいけないのかは最初にバチッと決めておいた方がよかった です。

最後に

反省点はあったものの、当日は予想を上回る人に興味をもっていただきとても嬉しかったです!次回同じような機会がありましたらこの反省を活かして、より良いブースを出展出来たらと思っています。

また、発表やブースを見に来ていただいた方々に 少しでも改善の文化が伝わっていただけたら と思っています。

それでは明日の記事もお楽しみに!

*1:KAIZEN会についてはこちらの記事でも詳しく紹介しています。 ECナビ KAIZEN会を実施しました

CTOからの挑戦状2016 2ndを手伝った際に書いたPythonコードを晒してみる。

Python

この記事はVOYAGE GROUPのAdvent Calendar 2016 11日目の記事として書かれています。 techlog.voyagegroup.com

みなさんこんにちは。ara_ta3 と言います。
今回はVOYAGE GROUPとサポーターズで行ったイベント
CTOからの挑戦状 2016 2nd にて採点したり、
学生向けに解答を用意したりしたのでその時の話をします。

supporterz.jp

参加された学生の方に参考になれば幸いですし、
今回はPythonでのLintや型ヒント + mypyの実装も記載しているので、
それらの使い方の参考にもなれば幸いです。

CTOからの挑戦状 2016 2nd とは?

  • 問題を解いて賞金が得られる
  • 問題はLV1, LV2, LV3がある
  • LV1は早い者勝ちで賞金 が得られる
  • LV2, LV3はCTO含む3人のエンジニアがコードを読み、評価順に基づいて賞金が得られる
    • 例えば
      • 変数や、メソッド、関数の命名がある程度適切か
      • 適度に処理がメソッド、関数に切り分けられているか
      • テストケースは適切か
    • みたいなところを見たりしました。

問題について

LV1 ~ LV3の問題はgistに記述していて、それぞれ下記URLになります。
問題はピザや寿司の割引クーポンの最適化問題という感じですね。

Level.1 - 2016 2nd - Challenge CTO of VOYAGE GROUP · GitHub

Level.2 - 2016 2nd - Challenge CTO of VOYAGE GROUP · GitHub

Level.3 - 2016 2nd - Challenge CTO of VOYAGE GROUP · GitHub

これらの問題を自分も解いてみたので、
そのコードで気をつけた所などをつらつらと書いてみようかと思います。
Pythonの解答数が最も多かったので、私もPythonで書いてみました。
Versionは3.5.2です。

書いてみたコード

準備

Python詳しくなかったので、開発環境周りをとりあえず調べてみました。

[PR]Pythonの環境については別途Qiitaで記事も書いているので是非ご覧いただければと・・・ qiita.com

ざっとやったこととしては

みたいなところです。

LV1

問題は簡単に言うと、
複数の割引クーポンを使ってピザ・寿司の支払金額を出来る限り少なくすると言ったものでした。

問題
Level.1 - 2016 2nd - Challenge CTO of VOYAGE GROUP · GitHub

私の解答
CTO Challenge 2016 2nd LV1 · GitHub

主に書いたコードは下記のような感じです。
単純に高い割引額のクーポンから使って行けば良さそうですね。
とりあえずLV1のケースをテストしました。
for文の中くらいのスコープなら適当な変数名でもいいけど、
少し広めなスコープのときは妥当そうな命名をつけるようにしました。

また、"準備" の部分に色々書いたりしましたが、
このタイミングではあまり複雑じゃなかったのでmypyは入れませんでした。

lowest_amount_for_using_coupon = 1000


def select_optimum_combination(amount, coupons):
    if amount <= lowest_amount_for_using_coupon:
        return []
    sorted_coupons = sorted(coupons, reverse=True)
    rest = amount
    used_coupons = []
    for c in sorted_coupons:
        if rest < c:
            break
        rest -= c
        used_coupons.append(c)

    return used_coupons


def test_select_optimum_combination_case1():
    selected = select_optimum_combination(1000, [500, 500, 200, 100, 100, 100])
    assert selected == []


def test_select_optimum_combination_case2():
    selected = select_optimum_combination(1210, [])
    assert selected == []


def test_select_optimum_combination_case3():
    selected = select_optimum_combination(1210, [500, 500, 200, 100, 100, 100])
    assert selected == [500, 500, 200]


def test_select_optimum_combination_case4():
    selected = select_optimum_combination(1530, [500, 500, 200, 100, 100, 100])
    assert selected == [500, 500, 200, 100, 100, 100]


def test_クーポンが降順になっていなくても割引額が高い順に割り引いてくれること():
    selected = select_optimum_combination(1530, [500, 200, 100, 500, 100, 100])
    assert selected == [500, 500, 200, 100, 100, 100]

LV2

LV2では少し仕様が増えました。
入力がLV1では金額でしたが、メニューに変わりました。
また、クーポンの使用回数に制限が加わりました。
さらにメニューの種類によって使えるクーポンが増えました。
めんどくさいですね

問題
Level.2 - 2016 2nd - Challenge CTO of VOYAGE GROUP · GitHub

私の解答
CTO Challenge 2016 2nd LV2 · GitHub

CouponSelectorというサービスクラスにロジックを入れました。
テスト済のLV1で使った関数はそのまま利用したかったので、
CouponSelectorクラスのクラスメソッドとしてリファクタリングしました。
このタイミングで型ヒントを入れてみました。
メソッドの引数にCouponやMenuクラスを含むDictやListなどの少し複雑な型を渡したくなり、
それら以外は渡さないで欲しいことを明示的にわかるようにしたくて入れました。
Python3.5.2現在では型ヒントだけではPythonの処理系はなにもしてくれないのですが、
mypyで意図しない型の入力がないか確認しています。

coupon_selector.py

from typing import List, Dict
from functools import reduce
from collections import Counter
from menu import Menu

Coupon = int


class CouponSelector():
    def __init__(self,
                 limitation_of_coupons: Dict[Coupon, int],
                 coupons_available_for_pizza: List[Coupon],
                 lowest_amount_for_using_coupon: int
                 ) -> None:
        self.limitation_of_coupons = limitation_of_coupons
        self.coupons_available_for_pizza = coupons_available_for_pizza
        self.lowest_amount_for_using_coupon = lowest_amount_for_using_coupon

    def select_optimum_combination_by_menus(
            self,
            menus: List[Menu],
            coupons: List[Coupon]):

        amount = reduce(lambda amount, m: amount + m.price, menus, 0)
        available_coupons = _filter_limited_coupons(
            coupons,
            self.limitation_of_coupons
        )
        if not _pizza_exists(menus):
            available_coupons = [c for c in available_coupons
                                 if c not in self.coupons_available_for_pizza]
        return self.select_optimum_combination(amount, available_coupons)

    def select_optimum_combination(self, amount: int, coupons: List[Coupon]):
        if amount <= self.lowest_amount_for_using_coupon:
            return []
        sorted_coupons = sorted(coupons, reverse=True)
        rest = amount
        used_coupons = []
        for c in sorted_coupons:
            if rest < c:
                break
            rest -= c
            used_coupons.append(c)

        return used_coupons


def _pizza_exists(menus: List[Menu]):
    return any(m.is_pizza() for m in menus)


def _filter_limited_coupons(
        coupons: List[Coupon],
        coupon2limit: Dict[Coupon, int]):
    counter = Counter(coupons)
    for c, n in counter.items():
        counter[c] = min(coupon2limit[c], n)
    return list(counter.elements())

menu.py

from typing import Dict, List, Tuple


class Menu(object):
    def __init__(self, name, price):
        self.name = name
        self.price = price

    def is_pizza(self):
        return type(self) == PizzaMenu


class PizzaMenu(Menu):
    def __init__(self, name, size, price):
        super(PizzaMenu, self).__init__(name, price)
        self.size = size


class SideMenu(Menu):
    def __init__(self, name, price):
        super(SideMenu, self).__init__(name, price)

テストケースはこちら

LV3

LV3ではセットメニューという概念が加わりました。
クーポンを使うよりもセットメニューの方がお得など、なんとなくありそうな仕様ですね。
セットメニュー特定のサイドメニューが無料になるものもあれば、
任意のサイドメニューが無料になるもの、
具体的な割引額が決まっているものもあります。
またセットメニューを頼める時間帯が決まっているものもあるようです。
仕様がめんどくさいですね。

クーポンの割引額とセットメニューの割引額をそれぞれで出して両者を比較すればいいかなと考えました。
なので比較する部分を除くと、LV2から追加されたのはSetMenuのディスカウント額を計算するプログラムのみです。

問題
Level.3 - 2016 2nd - Challenge CTO of VOYAGE GROUP · GitHub

私の解答
CTO Challenge 2016 2nd LV3 · GitHub

SetMenuDiscounterというサービスクラスを作成しました。
処理の流れは
注文されたメニューと注文された時間から利用可能なセットメニュー一覧を取得し、
一覧の中で最も高い割引額のセットメニューを決めて返す。
と言ったものです。
工夫したのは、クーポンが利用可能か判定する部分を関数で表現しました。
仕様に1種類しかないので、抽象化するにはまだ早いかなと考えたためです。
今見返してみて、 SetMenuDiscount クラスのどのメソッドを見るべきが悩んだので、
privateメソッドかどうかも意識して書けばよかったなと思いました。

from datetime import datetime
from typing import List, Tuple, Callable
from menu import Menu


class SetMenuDiscounter():
    def __init__(self, discounts: List[SetMenuDiscount])->None:
        self.discounts = discounts

    def decide_discount(self,
                        ordered: List[Menu],
                        current_time: datetime
                        )->Tuple[SetMenuDiscount, int]:

        availables = [d for d in self.discounts
                      if d.is_available(ordered, current_time)]
        if not availables:
            return (None, 0)

        best, *rest = availables
        best_discount = best.calculate_discount(ordered)
        for d in rest:
            p = d.calculate_discount(ordered)
            if best_discount < p:
                best = d
                best_discount = p
        return (best, best_discount)


class SetMenuDiscount(object):
    def __init__(self, name: str,
                 required_menus_combinations: List[List[Menu]],
                 available_time_rules: List[Callable[[datetime], bool]],
                 discount_target_menus: List[Menu],
                 discount: int = None)->None:
        self.name = name
        self.required_menus_combinations = required_menus_combinations
        self.available_time_rules = available_time_rules
        self.discount_target_menus = discount_target_menus
        self.discount = discount

    def is_available(self, ordered_menus: List[Menu],
                     ordered_time: datetime)->bool:
        return self.in_available_period(ordered_time) \
            and self.is_matched(ordered_menus)

    def is_matched(self, ordered_menus: List[Menu])->bool:
        ordered_menu_set = set(ordered_menus)
        for combination in self.required_menus_combinations:
            c = set(combination)
            if c.intersection(ordered_menu_set) == c:
                return True
        return False

    def in_available_period(self, ordered_time: datetime)->bool:
        if self.available_time_rules is None:
            return True
        return any(map(lambda fn: fn(ordered_time), self.available_time_rules))

    def get_target_discount_menus(self, ordered_menus: List[Menu])->List[Menu]:
        t = set(self.discount_target_menus).intersection(set(ordered_menus))
        return list(t)

    def has_amount_discounts(self)->bool:
        return self.discount is not None

    def has_menu_discounts(self)->bool:
        return len(self.discount_target_menus) > 0

    def calculate_discount(self, ordered_menus: List[Menu])->int:
        price = 0
        if self.has_menu_discounts():
            menus = self.get_target_discount_menus(ordered_menus)
            prices = map(lambda m: m.price, menus)
            m = max(prices, default=0)
            price = max(price, m)
        if self.has_amount_discounts():
            price = max(price, self.discount)
        return price

テストケース

まとめ

CTOからの挑戦状 2016 2ndの際に書いたLV1, LV2, LV3のコードを晒してみました。
それぞれほんの少しですが、意図や工夫した点を書いてみました。
工夫した点を少しまとめると

  • coreなコード(ここで言うとcoupon selector)に仕様的なものが含まれないようにしました。
    • クーポンの具体的な割引額や使える枚数の制限など。
  • パフォーマンスを考えるよりも処理の流れがわかりやすくなるように心がけました。
    • クーポンが1億枚手元にあるからそこから選ぼうということはあまりないかなと考えてます。
  • 変数などの命名は出来る限りわかりやすくするようにしました。
  • Lintや型チェックなどを入れてテストを書かなくてもエラーがある程度わかるようにしました。

説明がわかりづらい、ここなんでこんなクソ実装なんだ!
とかありましたら是非我らがAJITOでお話でもできればと思うので、お声がけください!w

voyagegroup.com

感想

  • テストケースに不備があったらごめんなさい・・・指摘頂ければ幸いです。
  • あまり触ったことないコードに触るとき形(コーディング規約やLintなど)から入るといい感じにそれっぽくかけるから良い
    • ただし、エディタ設定沼にはまらないように注意
  • CTOからの挑戦状のコード書くの大変・・・w
  • 型ヒントとライブラリで頑張るならコンパイルでわかる(言語がサポートしてくれている)方が個人的にはいいなぁ