dely engineering blog

レシピ動画サービス「kurashiru」を運営するdelyのテックブログ

jQueryへの依存を外す方法

こんにちは!dely でフロントエンドエンジニアをしている @all__user です。

この記事は dely Advent Calendar 2018 の8日目の記事です。

昨日は、iOSエンジニアのほりぐち( @takaoh717 )が「iOS版クラシルの開発からリリースまでの流れ」というタイトルで投稿しました。

tech.dely.jp

iOS 版 kurashiru の開発体制の遍歴がよく分かるような内容となっていますので、ぜひチェックしてみてください!

はじめに

ここ一年間で Web 版 kurashiru のフロントエンドは Rails から Vue の SPA へと少しづつ置き換えられてきました。
今回はその中でも jQuery への依存を外す際に行ったことにフォーカスを当ててまとめてみたいと思います。

目次

リプレース or 少しづつ置き換え

現在 jQuery を使った Web サイトを運用していて SPA への移行を考えている方の中には、リプレースするか少しづつ置き換えていくかで悩んでいる方もいるかもしれません。

最終的に kurashiru では少しづつ置き換える方法を取りました。
リプレースという選択肢もありましたが、割けるリソースの数、 SEO への影響などもろもろを考慮すると、エイヤっ!で移行するにはリスクが大きすぎると判断したためです。

稼働中の Web サイトを SPA へ書き換えるという作業は、いってみれば走っている車のタイアを交換するようなものです。人が住んでいるマンションを、人が住んでいる状態のまま建て替えるようなものと言ってもいいかもしれません。

jQuery を残したまま Vue に置き換えていくこともできますが、jQuery を使用している部分が無くなるまで jQuery と Vue が併存することになります。バンドルサイズはできるだけ小さく抑えたかったので、jQuery への依存を先に外すことにしました。

polyfill を入れる

Array.fromArray.prototype.findIndex などのメソッドは古いブラウザでサポートしていない場合があります。
どのブラウザでもこれらのメソッドを安全に呼び出せるように polyfill を導入しました。

これまではマルチブラウザ対応を jQuery がやってくれていましたが、依存を外していくにあたり別の方法でサポートする必要があります。

kurashiru では polyfill.io という CDN を使用して、User-Agent ごとに最適な polyfill を読み込むようにしています。

github.com

Sprockets 👉 Webpacker

まず最初に Sprockets のエントリーポイントをすべて Webpacker に移しました。

github.com

Webpacker はデフォルトで CoffeeScript に対応しているので、対象となる CoffeeScript ファイルを import したファイルを用意して Webpacker のエントリーポイントに移すだけです。

gem 👉 npm

gem の jquery-rails を削除し npm の jquery に置き換えます。

www.npmjs.com

CoffeeScript 👉 TypeScript

decafjs を使って CoffeeScript を JavaScript へ自動変換します。

www.npmjs.com

と、これで動いてくれれば良いのですが、まあまあ動かないところがあります。
ソースコードを読みながら地道に修正しつつ TypeScript に書き換えていきました。
decafjs はあくまでも TypeScript 版の下書きを作ってくれるツールくらいの感覚で使用しました。

jQuery 👉 VanillaJS

VanillaJS とは特定のフレームワークを使わずに DOM の標準 API のみを使って書くことを、バニラアイスのようなプレーンな状態ということに例えてよく使われている表現です。

この工程では特に、ここまでの過程で TypeScript に書き換えてきたことが効いてきます。
TypeScript で型をしっかり縛ることで typo や型エラーなどのケアレスミスで消耗すること無く、安心してリファクタリングを進められます。

$() 👉 Array.from(document.querySelectorAll())

jQuery のメソッドは基本的にセレクタにマッチした要素全てに対して操作を行いますが、標準 API では配列のループを回して一つ一つの要素に対して操作を行う必要があります。
.querySelectorAll() の戻り値は Array ではなく NodeList なので、Array.from() を使って Array に変換しておきます。

.each() 👉 .forEach()

.each() のコールバックに渡る引数は index, element の順ですが、.forEach() の場合は逆の element, index の順となります。

.width() 👉 .getBoundingClientRect().width

.getBoundingClientRect() は実行コストが少し気になりますが、.width().height() を再現する際にとても便利です。

.slideUp() 👉 ?

.slideUp() のように単純に標準 API へ置き換えることができないメソッドは、個別に機能を作って対応しました。
すでに Vue のアニメーションの仕組みにのっとって定義されたモーション用のクラス(.fade-enter, .fade-enter-active, .fade-enter-to みたいなやつ)があったので、それを使い、いい感じのタイミングで要素の classList に対してクラスの付け外しを行うことで再現しました。

各工程で動作確認をする

これまでの各工程の一つ一つはそれほど難しくないかもしれませんが、全ての工程を一気にやってしまうと、ちょっとした不具合があった際に、どこまでロールバックすれば良いかが分からなくなってしまいます。
できるだけ各工程の間に動作検証をいれるようにして作業を進めました。

挙動を変えないようにする

上記のように書き換えを行っていると、どうしても途中で挙動を変えたくなる箇所が出てきます。
あーこうした方がいいのにな、よしついでに直しちゃおう、っていうことが必ず出てきます。
でも、これをやってしまうと、何か不具合が起きた際に、書き換えにミスがあったのか、挙動を変えたことが原因なのかが分からなくなってしまいます。
気になった部分はコメントなどに残しておき、動作を確認できてから手を付けたほうが、着実に作業を進められます。

動作検証はビジュアルリグレッションテストで

動作確認を各工程で行う、と一口に言っても何をどこまで行えば良いのか、というのは非常に難しい問題です。
kurashiru ではビジュアルリグレッションテストという方法を使って動作検証を行いました。

ビジュアルリグレッションテストとは、コードに変更を加える前後でスクリーンショットを取り、その画像を比較することで動作検証を行うというテスト手法です。
これを各工程ごとに実施することで、書き換え前後で挙動が変わっていないことを確認しながら作業を進められます。

BackstopJS

kurashiru では BackstopJS というツールを使用してビジュアルリグレッションテストを行っています。

github.com

CI/CD などに組み込みやすいように Docker イメージが提供されていたり、コンフィグ類も過不足なく柔軟に設定できるためとてもオススメです。

意図的に 1px padding を変更した例
意図的に 1px padding を変更した例

差分が強調表示されるので分かりやすい
差分が強調表示されるので分かりやすい

一見まったく同じ見た目でも、きちんと差分を見つけ出すことができます。

まとめ

jQuery の依存を外すまでの工程を順を追ってご紹介してきました。

  1. polyfill の導入
  2. Sprockets から Webpacker への移行
  3. gem から npm への移行
  4. CoffeeScript から TypeScript への移行
  5. jQuery から VanillaJS への移行

細かい変換を繰り返しながら、各工程で動作確認をしていくことが重要です。

と、ここまで書いてきましたが、実際にはかけられる工数との兼ね合い、移行後のコードがその後どう使われるのかによっても違うので、ある程度エイヤっ!で書き換えることも正直たくさんありました。
そのあたりはよしなにやっていきましょう。

さいごに

SPA 化にあたっては、紆余曲折、チーム内でもさまざまな議論が行われました。
他にも Webpacker つらい、、、などご紹介したい話はたくさんありますが、それはまた別の機会にご紹介できればと思います!

明日はプロダクトデザイナーの @kassy が「ユーザーの声に振り回されないデザインの改善プロセス」というタイトルで投稿します。
こちらもぜひご覧ください!

iOS版クラシルの開発からリリースまでの流れ

この記事はdely Advent Calendar 2018の7日目の投稿です。
昨日は、Androidエンジニアのkenzoが「Androidにいい感じの動きをさせていく話(ViewPagerとその他Viewとの連動)」というタイトルで投稿しました。 Android開発を行っている方はぜひチェックしてみてください。

Qiita: https://qiita.com/advent-calendar/2018/dely
Adventar: https://adventar.org/calendars/3535

1. はじめに

こんにちは、delyでkurashiruのiOSエンジニアをしているほりぐち( @takaoh717 )です。
今回は、delyが運営しているレシピ動画サービス「kurashiru(クラシル)」のiOSアプリがどのようなフローを経てストアに公開されているかをざっくりとご紹介したいと思います。
この内容が、同じようなチーム規模の会社の方やこれからリリースフローなどを確立していくフェーズの方々などの参考になれば幸いです。  

2. dely開発部のチーム体制

クラシルは現在アプリダウンロード数が両OSで1400万を超える規模のサービスですが、iOSチームは現在2人体制で開発を行っています。
この人数だと当然リソースが潤沢と言える状況ではないため、何をどのように行うかがとても重要です。 また、delyの開発チームにとって最も重要なことはユーザに本質的な価値を提供するサービスを作ることです。 そのためにはスピード感をもって開発を進めることや施策の最適な優先順位決め、プロダクトの品質を保つための仕組みづくりなどの点に特に考慮する必要があると思っています。  

3. スクラム開発

クラシルの開発チームは半年ほど前から1週間を1スプリントとしたスクラム開発を行っています。

f:id:takaoh717:20181205180337p:plain
施策立案からリリースまでの全体の流れ

スプリント計画会

まず、スプリントの始まりには計画会を開いています。
ここで、今デザインフェーズにあるものと実装フェーズにある施策をそれぞれ確認し、そのスプリントで必要な要件定義・デザイン・実装のタスクの洗い出しと担当の割り振り、工数見積もりを行います。  

デザインフェーズ

UIデザイン、プロトタイピング実装、インタビューなどのサイクルを回します。
このあたりの話についての詳細は PM奥原のこちらのスライドをご覧いただくと分かりやすいと思いますが、ここで重視しているのはきちんとユーザーに使われるものをリリースするための仕組みです。  

実装フェーズ 

デザインフェーズが完了すると、プロトタイピングを行ったエンジニアやデザイナーが開発仕様書を作成します。
弊社ではドキュメントの共有にQiita:Teamを活用しているため、仕様書もQiita:Teamを使って書きます。また、Sketchで作成されたUIもZeplinで共有されます。
実装フェーズの実装者は「開発仕様書」「Zeplin上のUIデザイン」「プロトタイプ」を確認しながら、アプリの実装を進めます。
デザインフェーズで実装されたプロトタイプは基本的にはリリースするアプリには含まない方針で作られているものなので、ここではプロトタイプのコードは使用せずに新しくコードを書いていきます。
(プロトタイピング段階でプロダクトコードレベルで作り込まれたUIコンポーネントなどはそのまま使用することがあります。)   

4. iOSチームの開発スタイル

1スプリントの流れ

f:id:takaoh717:20181205202251p:plain
iOSチームの1スプリントの動き

  • 水:スプリント計画会で今スプリントのタスクが決定
  • 木〜火:実装
  • 水〜金:PRレビュー → 修正 → デバッグ
  • 金:Appleの審査に提出
  • 月:リリース

iOSチームは1スプリント(1週間)をざっくりとこのような形で動いています。
スプリントの開始が水曜日になっているのは、iOSの審査のタイミングを考慮してこの形が理想だろうと考えた結果です。
また、ここまできっちりとスケジュールを決めている理由は、実装後のデバッグや修正の時間をきちんと確保し、少ないリソースでもプロダクトの品質を確保できるような仕組みづくりが必要だったからです。
スケジュールを固定にしていなかった頃は、少し無理な実装スケジュールでも直前に割り込みで対応したりなどして、リリース前の検証が疎かになったり審査提出予定日のギリギリまでエンジニアが頑張るなどという問題が起きていました。

Gitのブランチ運用

  • /master
    • AppStoreで配信しているアプリと同じ状態
  • /develop
    • 次のリリースで配信されるもの
  • /feature
    • 何らかの新しい機能を実装をするためのブランチ(developから派生)
  • /improvement
    • 新機能ではないがユーザが見て分かる改善を行うためのブランチ(developから派生)
  • /fix
    • バグ・不具合の修正を行うためのブランチ(developから派生)
  • /refactor
    • リファクタリングを行うためのブランチ(developから派生)
  • /other
    • CI/CDやライブラリなどのツールに関する修正やアップデートを行うためのブランチ(developから派生)
  • /release/v.◯.◯.◯
    • スプリントが終わった段階で次のアップデートでリリースされるバージョン
  • /hotfix
    • releaseブランチを作成してから不具合などの緊急度の高い修正必要になった場合に修正を行うためのブランチ(releaseから派生)

Gitのブランチは現在上記のような運用を行っています。基本的な形はGit flowに近いと思います。
開発作業用のブランチを5種類に分けているのは、ブランチの名前だけでどういう変更が含まれたものかはっきりさせるために分けています。
また、この名称と同じタグをプルリクに設定してそこからCHANGELOGに追記されるようにしているため、その部分とも連動しています。

流れ

  1. masterブランチ → developブランチ派生
  2. developブランチ → 実装内容に応じて、開発ブランチ(feature or improvement or fix or refactorブランチ)派生
  3. 開発ブランチで作業、PR作成、レビュー
  4. 開発ブランチ → developにマージ
  5. releaseの要件が定まったら(主にスプリントの最終日)、develop → releaseブランチ派生
  6. releaseブランチで緊急度の高いバグがあった場合に releaseブランチ → hotfixブランチ派生
  7. hotfixブランチで修正、PR作成、レビュー
  8. hotfix → release、hotfix → developにそれぞれマージ
  9. releaseブランチからアーカイブビルドを行い、AppStoreConnectにアップロード
  10. リリース後に release → master、release → develop にそれぞれマージ

この運用方式にしてから半年以上が経過していますが、今は特に問題なく運用ができています。
以前は様々なタイミングで緊急の修正を行った際などのフローが不明確だったことなどがあり、何度かコミュニケーションが発生したりコンフリクトの修正が必要になったりと無駄な時間が発生してしまうことがありました。

デバッグ

releaseブランチを作成したら、社内のメンバーにデバッグをしてもらうためにDeployGateでアプリの配布を行います。
DeployGateでの配布にはSlack、fastlane、Bitriseなどを活用して作業を自動化してあります。
ここでは次にリリースをするバージョンの変更箇所を中心に確認します。また、バグや不具合などの問題以外でも少し気になったことなどもよく報告されることがあります。

審査提出 → リリース

デバッグとhotfixの修正が全て完了したら、審査に提出します。
審査通過後は、プロモーションコードをSlackで配布し、社内のメンバーにも最終確認を行ってもらいます。
ここで問題が見つからなければそのままリリースを行います。

リリース後

リリースした後は、まずはログの確認やデバッグ時に見つからなかったクラッシュ、バグの確認を行います。
さらに、必要に応じてiOSエンジニアもSQLを書いてデータ分析を行い、分析結果をもとにさらなる改善をしていきます。

5. おわりに

以上がクラシルのiOS開発からリリースまでの大まかな流れです。
この記事で紹介した仕組みは全てこの1年以内に確立したものです。弊社の開発チームはまだまだ歴史も浅く、メンバーも少ない少数精鋭チームですが、プロダクトを第一に考えて柔軟に仕組み作りを行っています。
もし機会があれば、もう少し深掘りした説明もどこかでしようかと思います。

また、弊社では、プロダクト志向でクラシルの開発ができるiOSエンジニアを募集しています。
少しでも興味があったり、記事に対してご意見などある方がいらっしゃればぜひお声がけください!

www.wantedly.com

明日はフロントエンドエンジニアの @all_user が「jQueryへの依存を外す方法」というタイトルで投稿します。こちらもぜひご覧ください!

Androidにいい感じの動きをさせていく話(ViewPagerとその他Viewとの連動)

こんにちは。delyでAndroidのエンジニアをしているkenzoです。
この記事はdely Advent Calendar 2018の6日目の記事です。

Qiita: https://qiita.com/advent-calendar/2018/dely Adventar: https://adventar.org/calendars/3535

 

昨日は検索エンジニアのsakuraが「クラシルの検索をよくするために1年間取り組んだこと」を書きました。
普段検索に携わる方はもちろん、それ以外の方にとっても面白い内容となっていますので、ぜひご覧ください。

はじめに

弊社のプロダクト開発はデザインフェーズと実装フェーズの2つのフェーズに分かれています。(詳しくはこちら↓)

speakerdeck.com

後半の実装フェーズでは、前半のデザインフェーズで出来上がったデザインを元に画面・機能を作成していくことになります。
デザインフェーズでは、かなりいい感じに動くプロトタイプを作成したり、それを用いてユーザーテストを行ったりと、より良いものをユーザーに提供できるように弊社のデザイナーやデザインエンジニアが頑張ってくれています。

で、下のようなデザインが出来上がっているわけです。(これはデザインの指示ではなくて実際に実装した画面ですが)

f:id:kenzo_aiue:20181203183239g:plain

え、めちゃ動く。。。 (はじめてデザインを見たときのきもち)

初めて見ると一瞬たじろぐかもしれませんが、動きも含めてユーザーに提供する価値なので、難しそうだからといって実装しないわけにはいきません。

今回はこのようないい感じに動くデザインを実装に落とし込んでいく際にやったことの一例を紹介します。

ViewPagerとその他Viewとの連動

まずはこちらをご覧ください。

f:id:kenzo_aiue:20181204135415g:plain

このようにViewPagerを用いたアプリの場合、現在開いているページによってViewPager外に表示される内容が変わることがあると思います。(画面下部や右上の★)

もちろんこれでOKな場合も多いとは思いますが、もう少し細かい動きにこだわったUIを作成したい場合もあるかと思います。

たとえばこんな感じです。

f:id:kenzo_aiue:20181204141554g:plain

今回はこれの実装方法をご紹介します。

用いるのはOnPageChangeListeneronPageScrolledです。

val viewPager: ViewPager = findViewById<ViewPager>(R.id.view_pager)
viewPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
    
    override fun onPageScrollStateChanged(state: Int) {
    }

    override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
    }

    override fun onPageSelected(position: Int) {
    }
})

onPageScrolledViewPagerのスクロール中に呼び出され、その際に引数として下記の値が渡されます。 (positionOffsetPixelsは今回は使いません)

  • position: 表示されている最初のページのindex
  • positionOffset: positionで指し示されるページからのオフセットの割合を0以上1未満の値で
  • positionOffsetPixels: position`で指し示されるページからのオフセットのpixel値

つまり、どのタイミングでどんな値が返るかというと、、

position: 0 positionOffset: 0.0

f:id:kenzo_aiue:20181204145013p:plain

position: 0 positionOffset: 0.31944445

f:id:kenzo_aiue:20181204145041p:plain

position: 0 positionOffset: 0.6259259

f:id:kenzo_aiue:20181204145142p:plain

position: 1 positionOffset: 0.0

f:id:kenzo_aiue:20181204145216p:plain

という感じです。

以上のような値がスクロールしている間に何度も呼ばれるonPageScrolledで渡されます。

この値を使ってその他Viewのtranslationやalpha、scaleをセットします。
上のサンプルの「いいいいい」のViewの例だと、

  • positionが0の時はpositionOffsetの増加に従って表示位置が上がる
  • positionが1の時はpositionOffsetの増加に従って表示位置が下がる
  • positionが2(上記以外)の時は常に表示位置が下のまま

なので、下記のようにtransilationYをセットします。

override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
    view.translationY = when (position) {
        0 -> view.height - view.height * positionOffset
        1 -> view.height * positionOffset
        else -> view.height.toFloat()
    }
}
// viewは「いいいいい」のviewです

このような動きになります。

f:id:kenzo_aiue:20181204141929g:plain

もう1つ、上のサンプルの「★」のViewの例だと、

  • positionが0の時(下記以外)は不透明度が0のまま
  • positionが1の時はpositionOffsetの増加に従って不透明度が上がる(だんだん見えるようになる)
  • positionが2の時は不透明度が1のまま(ページが増えるようならpositionOffsetの増加に従って不透明度を下げる)

なので、下記のようにalphaをセットします。

override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) {
    star.alpha = when (position) {
        1 -> positionOffset
        2 -> 1f // 今回はpositionの最大が2なのでこれでよいですが、ページが増える場合は1f - positionOffsetとします
        else -> 0f
    }
}
// starは「★」のviewです

このような動きになります。

f:id:kenzo_aiue:20181204133352g:plain

これで、ViewPagerをスクロールさせた時に他のViewと連動させることができました。

おわりに

Androidでも他のフロントエンドでも、動きのある画面を作るのはとっつきにくかったり難しかったりと、実装に若干のハードルはあると思います。
ですが、やっぱり自身がユーザーとしてサービスに触れる際に、いい感じに動いてくれると気持ちよかったり、わかりやすかったりと、良い体験ができていると感じます。
使ってくれる人がより良い体験をできるように、ものづくりをする人間としてはそういうとこ頑張らないとなーと思うところです。

 

ちなみに上でデザインの例としてあげた「クラシルかんたん献立機能」が今回リリースされました!
使ってみてもらえるととても嬉しいです。
アプリはこちら: Android iOS

 

明日はiOSエンジニアの堀口の「iOS版クラシルの開発からリリースまでの流れ」です。お楽しみに!

クラシルの検索をよくするために1年間取り組んだこと

こんにちは: )

delyで検索エンジニアをしているsakura (@818uuu) です。この記事はdely Advent Calendar 2018の5日目の記事です。

4日目の記事は、SREの井上の「超手軽に構築する!サーバレスなWEBパフォーマンス定点観測基盤」でした。WEBパフォーマンスの定点観測・分析に興味がある方はぜひ読んでみてください。

tech.dely.jp

はじめに

私はdelyに入社した2018年1月から検索エンジニアをしています。

delyが運営している料理動画アプリ「クラシル」では訪れたユーザーのほとんどが検索をする、いわば検索がコアになっているサービスです。
検索エンジニアとしてその検索体験を少しでも向上させるべく日々試行錯誤しています。

弊社のエンジニアが検索体験について語っているインタビュー記事もありますので、ぜひご覧ください。

careerhack.en-japan.com

私が入社する前まで弊社には検索エンジニアという役職は存在せず、他の役職のエンジニアが検索部分を掛け持ちで担当していました。
従って、会社にとって検索エンジニアという役職を置くのは初めての試みでしたし、私自身も検索エンジニアという役職についたのは今回が初めてです。

この記事ではクラシルの検索を改善するために、検索エンジニアとして1年間クラシルの検索に向き合った記録をお届けします。
検索エンジニアを目指している方やサービスを運営していて「検索機能は提供しているけどなかなか手をつけれていない」と悩みを抱えている方にとってこの記事が少しでもご参考になると幸いです。

目次

検索離脱率の改善

結果:辞書シノニムの整備を行い、検索離脱率を5%低下させることに成功しました🎊

検索エンジニアになって特に気にかけたのがこの検索離脱率という指標でした。入社当初は「一般的な検索離脱率が何%くらいなのか」という知見すらなかったため、その数値が高いのか低いのかすら判断ができませんでした。

調査を進めていくうちに、検索離脱率が高い一番の理由は辞書シノニムであることに気づきました。
辞書シノニム管理の運用についての詳細はこちらに記載しています。

tech.dely.jp

検索キーワードごとに検索離脱率を出し、検索離脱率が高いキーワードから優先的に対処していきました。
辞書シノニム改善をはじめてすぐに結果が表れ、運用を地道に続けていると結果的に検索離脱率を5%以上低下させることができました。

検索結果0件のキーワードの削減

結果:検索結果0件のキーワードを1000個以上削減・対応し、ユーザーの検索体験を向上させることが出来ました📒

クラシルには23,000件以上のレシピ動画があり一般的な料理はほぼ網羅していますが、たまに検索結果が0件のキーワードが見つかります。 「今流行りのチーズドッグを作りたい!」と思って「チーズドッグ」と検索してくれたとしても検索結果が0件だと、検索体験は非常に悪いですよね。

検索結果が0件の要因は主に2つあります。
1つは先ほど上述した辞書シノニムが未整備の場合、もう1つはそのキーワードに合致するレシピがそもそも存在しない場合です。

キーワードに合致するレシピがそもそも存在しない場合は、調理部さんと相談してレシピ動画を作成していただきます。 検索結果が0件キーワードの一覧の生データをただ渡すだけだとレシピのかぶりが発生してしまったり、実は既にレシピ作成中だったりして手間が発生してしまうので、調理部さんにも定期的にアドバイスを伺い工夫しながらデータをお渡ししています。

辞書シノニムの改善と新たにレシピを作成してもらったことで1年間で少なくとも1000個以上のキーワードに対応することができました。

f:id:sakura818uuu:20181205111437p:plain:w200
『とろーり チーズドッグ』も調理部さんと相談して作成していただいたレシピのひとつです。今ではたべれぽが50件以上ある人気のレシピです。

人気のキーワードの機能追加

結果:人気のキーワードの機能追加により、入力の手間を省きタップするだけで人気のキーワードで検索できるようになりました👆
また、この機能は分析・改善もうまくいっています。


今年新しくリリースされた機能の1つに、"人気のキーワードの表示"があります。 クラシル内でよく検索されているキーワードや急上昇しているキーワードを表示する機能です。


ハロウィンなど特別なイベント時にはそのイベントに関連する人気のキーワードを表示するようにしました。 この施策はとても数値がよかったです🎈

f:id:sakura818uuu:20181204164426p:plain:w150
ハロウィンの時の人気のキーワード

人気の検索キーワードでどのキーワードがクリックされやすいかなどを分析し、それに基づいて運用方法を改善したり新たな施策を考えたりしています。そして、その分析結果を定期的に社内共有サービスにレポートしています。

人気の検索キーワード(新規機能)の分析を始めた際に学んだことを共有しておきます。
新規機能をリリースした後「全体の何%にこの機能は使われているのか」や「どれくらいの人がこの機能を使ってくれているのか」などを必ず調べると思います。
しかしリリースしてすぐに分析してしまうと、まだアプリのバージョンをアップデートしていないユーザーも多く正確な概算値を測ることができません。 例えば、人気のキーワード機能はリリースしてから約10日間経ってからイベント数が落ち着きました。

f:id:sakura818uuu:20181204154423p:plain:w300
人気のキーワードのイベント数推移 : 2週間ほど経ってから数値が落ち着きました

他の新規機能でも約2週間経ってから数値が落ち着いた事例があります。つまり、新規機能は2週間以上たってはじめて全体の何%に使われているのかがわかるということです。 (私はこのことを知らずにリリースして1週間で速報値をレポートしてしまいました😨)

クラシルでは開発のサイクルがとてもはやい反面、既にリリースされた機能に対してケアできていないことも何度かありました。
人気の検索キーワードはリリースされてからも継続的に分析することでうまく改善を回せている好例だと思います。

検索品質ガイドラインの策定

結果:検索品質ガイドラインを策定し、クラシルの検索の方向性をドキュメント化しました👣ただし運用に難ありです。

Googleには検索エンジンの品質を評価するために検索品質ガイドラインを設けています。Googleのを参考にクラシルでも検索品質ガイドラインを策定してみる挑戦もしました。

f:id:sakura818uuu:20181204161905p:plain
Google General Guidelines(Google検索品質評価ガイドライン)

tech.dely.jp

ただ、これは作ったばかりで改善の余地が多いのと運用がままなっていないという現状があります。(それだけ伸びしろがあるということですね…笑)

検索絞り込み機能の追加

結果:検索絞り込み機能がリリースされましたが、想定より使用してくれるユーザー数が少なかったです😔

f:id:sakura818uuu:20181203175701j:plain

今年から検索絞り込み機能も増えました。 含めたくないキーワードやジャンル、15分以内で作れる料理など特定の条件に応じて絞り込むことができる機能です。

検索絞り込み機能は、ユーザーからこういう機能がほしい!とアプリレビューやCSを通して要望のあった機能のひとつです。もちろん社内でもこんな機能があればいいね〜と案で上がっていました。
私は主に分析を担当したのですが、実際にリリースしてみると想定より利用者数がものすごく少なかったのです。 具体的にいうと検索ユーザーの数%(一桁)しか絞り込み機能を利用していなかったのです。
ジャンルの精度が悪いのかな?と思い精度をほぼ100%にしましたがそれでも使ってくれるユーザーの数は増えませんでした。

数値を見守り続けたところ、少ないのではなくこれが定常的な値ということがわかりました。 検索をした後になにかタップする障壁がいかに高いのかを思い知らされた反面、通常の検索でユーザーが求めているものを検索結果を叶えなければ…と強く感じるのでした。

検索情報のリサーチと共有

結果:情報収集を地道に続けた結果、クラシルの検索ログを深い視点で見ることができるようになりました!

検索エンジニアはユーザーがどんなキーワードで検索しているか毎日チェックします👀
クラシルの検索キーワードのログから以下のことを確認します。

  • ユーザーがどんな食材でレシピを探しているのか
  • どんなキーワードが急上昇しているか
  • いまトレンドの食材/メニューはなにか
  • 検索下位にはどんなキーワードが入っているか(辞書シノニムが未整備の可能性が高いため)

検索ログの意味合いをきちんと読み取るために、世間一般の食にまつわる情報をたくさん検索し情報収集しています。
ユーザーはクラシル以外にも無数の食との接触点を持っています。
「新大久保で流行っているメニューをクラシルでも作りたい」「テレビで〇〇がダイエットに効くって評判だけどクラシルでもその食材を使ったレシピあるかな?」などそこから食べたいものや作りたいレシピを決めることも多いでしょう。

食との接触点一覧

  • Twitter
  • instagram
  • Youtube
  • グルメサイト
  • ニュースサイト
  • ブログ
  • 流行りの飲食店
  • など

「この情報収集する作業本当に価値を生んでいるのかな?」と思うときもあったのですが、地道に続けているとクラシルの検索ログがどのキーワードでどのくらい影響を受けるのかが少しずつわかってきました👏その他にも副次的に良い効果がたくさん生まれました!

上記で調べた情報は社内に定期的に共有しています📘

f:id:sakura818uuu:20181205114432j:plainf:id:sakura818uuu:20181205114452j:plain
検索情報共有の一部

さいごに

クラシルの検索を改善するために、検索エンジニアとして1年間クラシルの検索に向き合った記録をお届けしました。 いかがでしたでしょうか。

この記事を一言で伝えると、 「自社の検索の性質をよく把握し、その性質に合わせて検索改善・分析をすることが重要」ということです。

検索エンジニア1年目を振り返ると、ものすごく手探りで自分の無力さを感じた1年間でした。
経験・技術力不足でなかなかうまくいかなかったり、周りに同じ職業の相談相手がいなくて悩むことがとても多かったり…… 。 でも、この1年間やってきたことは無駄ではなかったと思います。無駄ではなかったと思いたいし、思い切りたいです。

検索エンジニア2年目は1年目よりももっと検索をよくできると信じています。
そして、いつか胸を張って検索エンジニアですといえるようになりたいです。


明日は、弊社のAndroidエンジニアの川口より「Androidにいい感じの動きをさせていく話(ViewPagerとその他Viewとの連動) - dely engineering blog」の話が書かれます。是非、お楽しみにしていてください!

超手軽に構築する!サーバレスなWEBパフォーマンス定点観測基盤

はじめに

本記事はdely Advent Calendar 2018の4日目の記事です。

dely Advent Calendar 2018 - Adventar

dely Advent Calendar 2018 - Qiita

昨日は弊社の機械学習エンジニアの辻がNixOSについての記事を書きましたので興味のある方は是非読んでみてください。

tech.dely.jp

こんにちは!delyでSREをやっている井上です。 本記事では、WEBパフォーマンスの定点観測の仕組みを手軽に構築出来るようにしたのでその紹介をしたいと思います。

本記事では下記のサービスやツールを利用しています。

  • AWS
  • Terraform
  • sitespeed.io

前置きはいいから構築方法だけ知りたいという方は こちら!

f:id:gomesuit:20181202191536p:plain

目次

WEBパフォーマンスについて

2018年7月のGoogleのスピードアップデートでも分かる通り、WEBページの読み込み速度はPCかモバイルかに関わらずより一層重要視されてきています。 読み込み速度の遅いWEBページを改善しようとなったとき、まずはじめに現状を把握する必要がありそのためにはWEBのパフォーマンス計測を行う必要があります。

現在、WEBのパフォーマンスを計測する手段としては

などいくつかありますが、本記事では現在delyで利用しているsitespeed.ioというツールを紹介したいと思います。

sitespeed.ioについて

sitespeed.ioはオープンソースのWEBパフォーマンス計測ツールです。

www.sitespeed.io

主な機能

sitespeed.ioを使うことで下記のようなデータをWEBブラウザで閲覧することが可能になります。

各種スコア

f:id:gomesuit:20181126225433p:plain:w600

Waterfall

f:id:gomesuit:20181126225636p:plain:w600

Visual Metrics

f:id:gomesuit:20181126225720p:plain:w600

Browser Metrics

f:id:gomesuit:20181126225736p:plain:w600

レンダリング時の動画再生

f:id:gomesuit:20181126225824p:plain:w600

スコアに対するアドバイスの表示

f:id:gomesuit:20181126225851p:plain:w600

レスポンスの遅いリクエスト

f:id:gomesuit:20181126225927p:plain:w600

サイズの大きい画像やjavascript

f:id:gomesuit:20181126225915p:plain:w600

主な特徴

また前段で挙げたWEBパフォーマンス計測ツールと違い、下記のような特徴を持っています。

  • Dockerによる簡単な実行

公式のdocker imageがあるので、下記のコマンドによって簡単に実行することが可能です。

$ docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io:7.7.2 https://www.sitespeed.io/
  • 結果のhtml出力

実行後、計測結果がhtmlで出力されているのでWEBブラウザで簡単に閲覧することが可能になっています。

※ YYYY-MM-DD-HH-mm-ss は実行時刻が入ります

$ open sitespeed-result/www.sitespeed.io/YYYY-MM-DD-HH-mm-ss/index.html

他にも下記のような機能を標準で備えているので、柔軟な運用が可能になっています。

  • htmlをS3にアップロードする機能
  • 結果をSlackに通知する機能
  • 結果のメトリクスをjson形式で出力する機能

定点観測について

WEBのパフォーマンスにおいてボトルネックを特定するだけであれば、その時点で数回の計測を実施すればよいですが、ボトルネックに対して対策を行った後、どの程度改善したのかについて知りたい場合は対策後に再度計測を実施する必要があります。

複数の対策を長期的に実施していく場合などは、対策を実施する度に計測を実施する必要があり非常に大変です。

また、上記とは反対に何かのタイミングで意図せずパフォーマンスが悪くなっていないかをチェックしたいときやパフォーマンスが悪くなった前後でまたそのタイミングで何が原因かなどは遡って確認したいこともあります。

そういった要件を達成するためには、一定間隔で繰り返しWEBパフォーマンス計測を実施する必要があります。

定点観測の方法

定期的にWEBのパフォーマンス計測を行うには下記のような方法が考えられます。

  • 定期的に手動で実施

一番単純なのは定期的に手動で行うことです。 ただ現実的には手作業で行うため忘れることがあったり、そもそも同じ時間に実行するのが難しかったりします。

  • サーバを構築してcronやCIツールで定期的に実行

次はサーバを立ててcronで実行したりJenkinsなどのCIツールで定期的に実行する方法です。 この方法は、他の処理が裏側で動いていてパフォーマンス計測に影響があることを考慮する必要があったり、 そもそもサーバの管理が必要になってしまうことにデメリットを感じてしまいます。

  • ECSのタスク実行機能やAWS Batchで定期的に実行

理論的には実現できそうですが、WEBパフォーマンスの定点観測を行うだけなので少し大げさな気がします。

上記に3つほど方法を挙げて見ましたが、どれもいまいちという感じです(主観)。 そこで提案したいのがAWSのCodeBuildによる定期実行です!

CodeBuildのメリット

CodeBuildは本来CIで利用するサービスですが、サーバレスな実行環境としても優れていると思います。 WEBパフォーマンス計測の実行環境として使うことにおいては下記のようなメリットがあります。

  • サーバレスなのでリソースを気にしなくてよい
  • 実行時間による従量課金
  • CloudWatch Event連携でcronフォーマットによる定期実行も可能

定点観測基盤の構築方法

ここからは具体的な構築方法を紹介します。

本記事で使用するコードは全てGithubにあげています。 github.com

AWSのリソースをTerraformを使ってコード化しているので、定点観測基盤を手軽に構築することが可能です。

構成図

f:id:gomesuit:20181202191536p:plain

  • CodeBuild

sitespeed.ioを実行する実行環境として利用します。

  • CloudWatch Event

CodeBuildを定期的に実行するスケジューラとして利用します。

  • S3

sitespeed.ioによる計測結果の格納と、HTMLのホスティングとして利用します。

  • Glue

sitespeed.ioによる計測結果のスキーマを定義するために利用します。

  • Athena

sitespeed.ioによる計測結果に対してSQLを実行するために利用します。

前提

基盤構築に伴って最低限下記が実行環境に設定されている必要があります。

  • awscliのインストールとcredentialの設定

Terraformの利用に伴ってAWSのアクセスキーとシークレットキーの設定が必要です。

  • Terraformのインストール

  • IAMに対するTerraformを実行するための十分な権限設定

  • AWS CodeBuildとGitHubの接続

AWSコンソール上のCodeBuildにおいてプロジェクト設定時のページで下記のように、「GitHubアカウントを切断」と表示されている必要があります。されていない場合は「GitHubに接続」ボタンからGitHub連携を済ませてください。

f:id:gomesuit:20181201200034p:plain

CodeBuildとGitHubが接続されていないとTerraformのapply時にエラーになりますのでご注意ください。

準備

1.レポジトリのフォーク

下記のレポジトリをご自身のアカウントにForkします。Cloneして新しくレポジトリを作成しても大丈夫です。

GitHub - gomesuit/webperf-by-codebuild

2.Terraformのtfstateファイル管理用のS3バケットの作成

空のS3バケットを1つ作成します。 既存のものでも大丈夫ですが、tfstateファイルのkeyをコードにべた書きしているので新規でS3バケットを作成することをおすすめします。

3.terraform.tfvarsの作成

sitespeed.ioの出力したhtmlをS3のホスティング機能を使って参照するため、自身のIPでアクセス制限をかけます。 またCodeBuildがソースを取得する先を、手順で作成したレポジトリのURLに変更します。

サンプルファイルがあるのでコピーしてから、

$ cd terraform
$ cp terraform.tfvars.sample terraform.tfvars

ファイル内のIPアドレスとレポジトリを変更します。

# S3のアクセス元IP
my_ip = "XXX.XXX.XXX.XXX/32"

# 作成した自身のGitHubレポジトリ
git_repository = "https://github.com/<user-name>/<repository-name>"

4.backend.tfvarsの作成

Terraformのtfstateファイル管理用のS3バケットを terraform init のタイミングで指定するためファイルを作成します。

サンプルファイルがあるのでコピーしてから、

# ./terraform ディレクトリで実施
$ cp backend.tfvars.sample backend.tfvars

ファイル内のバケット名を変更します。

# tfstateを格納するS3バケット
bucket = "自身で作成したS3バケットの名前"

5.Terraform作業ディレクトリの初期化

下記のコマンドでTerraformの作業ディレクトリを初期化します。 エラーがでなければOKです。

# ./terraform ディレクトリで実施
$ terraform init -backend-config backend.tfvars

6.対象サイトの指定

計測するページのURLをurls.csvに記載します。 一行に ドメイン名,対象URL,カテゴリ の順で記載します。 複数行記載すると1回のCodeBuildの実行で複数のURLが計測されます。 カテゴリ名は対象URLをカテゴライズするために付与します。任意の文字列を入力してください。

例えば https://www.kurashiru.com/ を計測対象URLとする場合、下記のようになります。 トップページなのでカテゴリはtopとしました。 計測対象のWEBサイトはご自身が管理されているものを記載してください。

www.kurashiru.com,https://www.kurashiru.com/,top

構築

Terraformを実行します。

# ./terraform ディレクトリで実施
$ terraform apply

しばらくすると下記のようにコンソールに出力されるので、確認の後に「yes」と入力するとリソースの生成が始まります。

Plan: 16 to add, 0 to change, 0 to destroy.

Do you want to perform these actions?
  Terraform will perform the actions described above.
  Only 'yes' will be accepted to approve.

  Enter a value:

上記コマンドで生成したリソースを削除する場合は下記コマンドを実行します。

# ./terraform ディレクトリで実施
$ terraform destroy

sitespeed.ioによる計測結果の参照

最大1時間待つか設定済みのCodeBuildを手動で1度実行するとS3に計測結果が置かれます。

HTML

S3バケットにhtmlが出力されているのでブラウザで開くことで計測結果を閲覧することができます。

例えばURLは下記のようなものになります。

https://s3-ap-northeast-1.amazonaws.com/webperf-by-codebuild-d85de1e5e348/raw/www.sitespeed.io/desktop/top/2018/12/01/15/38/index.html

f:id:gomesuit:20181202005018p:plain

SQL

AWSコンソールのAthenaでSQLを実行すると結果が取得できるようになっています。

SQL例

SELECT * FROM "webperf_by_codebuild_d85de1e5e348"."json" limit 10;

f:id:gomesuit:20181202004551p:plain

VIEW作成

このままでも問題はないのですが、SQLを作成するときにSQLが複雑にならないようにVIEWを設定するのをおすすめします。 例えば下記のようなVIEWを生成することでS3のURLとtimestampのカラムを事前に定義しておくことが可能です。

SQLの下記の部分を自身でTerraformを実行した結果に置き換えてください。

  • 「<Terraformによって生成されたS3バケット>」
  • 「<Terraformによって生成されたデータベース>」
CREATE OR REPLACE VIEW "<Terraformによって生成されたデータベース>"."site" AS 
SELECT
  CAST("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("YEAR", '-'), "MONTH"), '-'), "DAY"), ' '), "hour"), ':'), "minute") AS timestamp) "time"
, "concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"('https://s3-ap-northeast-1.amazonaws.com/<terraformによって生成されたS3バケット>/raw/', "domain"), '/'), "device"), '/'), "category"), '/'), "year"), '/'), "month"), '/'), "day"), '/'), "hour"), '/'), "minute"), '/index.html') "link"
, *
FROM
  "<Terraformによって生成されたデータベース>"."json"

置き換え例

CREATE OR REPLACE VIEW "webperf_by_codebuild_d85de1e5e348"."site" AS 
SELECT
  CAST("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("YEAR", '-'), "MONTH"), '-'), "DAY"), ' '), "hour"), ':'), "minute") AS timestamp) "time"
, "concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"("concat"('https://s3-ap-northeast-1.amazonaws.com/webperf-by-codebuild-d85de1e5e348/raw/', "domain"), '/'), "device"), '/'), "category"), '/'), "year"), '/'), "month"), '/'), "day"), '/'), "hour"), '/'), "minute"), '/index.html') "link"
, *
FROM
  "webperf_by_codebuild_d85de1e5e348"."json"

結果例

SELECT * FROM "webperf_by_codebuild_d85de1e5e348"."site" limit 10;

f:id:gomesuit:20181202022902p:plain

URLとtimestampのカラムが追加されていることを確認できます。

Redashによる可視化例

弊社では例えば下記のSQLを実行することによって、

SELECT
  *
FROM
  "<Terraformによって生成されたデータベース>"."site"
WHERE
  device = 'desktop'
  AND time >= current_timestamp - interval '1' month
  AND category = 'top'
  AND domain = 'www.kurashiru.com'
ORDER BY
  time desc;

下記のような結果を得ています。

f:id:gomesuit:20181127175929p:plain

また上記の結果を下記のようにRedashを使ってグラフ化しています。

f:id:gomesuit:20181127163011p:plain

上記は弊社での例ですが、sitespeed.ioによって様々なメトリクスが取得されているので色々な指標の相対的な変動を計測することが可能になっています。

カスタマイズ

sitespeed.ioの試行回数の調整

WEBサイトのパフォーマンスはサードパーティーコンテンツによって影響を受けるのでリクエストするたびにパフォーマンスにずれが発生することが多いです。 そのため1回の計測で数回施行することが一般的です。 sitespeed.ioは設定ファイルのパラメータを変更することで試行回数を調節することができます。

設定ファイルはレポジトリのルートにあるconfig.jsonというファイルで、iterationsというパラメータを3から変更することで施行回数を調節することが可能です。

{
  "browsertime": {
    "iterations": 3,
    "speedIndex": true
  },
  "s3": {
    "region": "ap-northeast-1"
  },
  "crawler": {
    "depth": 1
  },
  "plugins": {
    "load": ["analysisstorer"]
  }
}

計測間隔の調整

計測のトリガーはCloudWatch Eventで行っています。そのためCloudWatch Eventの設定を変更することで計測間隔の調整をすることが可能です。

Terraformの該当ファイルは terraform/codebuild_trigger.tf です。schedule_expressionの cron(0 * * * ? *) を変更することで間隔を変更することが可能です。

反映するにはもう一度 terraform apply を実行します。

料金について

本記事の設定で実際にかかっている料金をご参考までにお伝えします。

現在4つのURLの計測を1時間毎に実行していますが、CodeBuildの該当料金は1ヶ月に約$40になります。 決して安くはないですが、サーバの管理がいらないため負荷や障害を気にせず運用することが可能です。

さいごに

WEBパフォーマンスの定点観測についてお話しました。

手間をかけずにとりあえずパフォーマンス計測を始めたいという要件にもってこいではないでしょうか。ぜひ皆様のWEBパフォーマンス分析の参考にしていただけたらと思います。

明日は検索エンジニアのsakura (@818uuu) が「クラシルの検索エンジニアが検索をよくするために取り組んだこと まとめ」というタイトルで投稿します!お楽しみに!

tech.dely.jp

ぼくが普段使っているOS - NixOSの話

 こんにちは。

開発部の辻です。普段はデータサイエンティスト・機械学習エンジニアをやっています。

 

本記事はdely Advent Calendar 2018の3日目の記事です。

adventar.org

 

昨日12月2日は、弊社delyの開発部プロダクトーナー兼GM奥原の記事でした。この1年でdelyは「kurashiru」の成長とともに体制も大きく変化しました。そんな中、若きプロダクトオーナー兼GMとなった彼の苦悩や意気込みなどが赤裸々に綴られています。どうぞご一読ください。

 

tech.dely.jp

 

さて、今回は特定のテーマに沿う必要はないとのことだったので、機械学習とは関係なく、ぼくが普段使っているクライアントOSの話をさせてもらおうと思います。ふつう、クライアントPCで使うOSといえば、MacOSやWindowsを使ってる方が多いかと思いますが、エンジニアの方ですと、なんらかのLinuxディストリビューションを使っている人もそれなりに多いのではないでしょうか?ぼく自身もとあるLinuxをクライアントOSとして3年ほど使っていまして、ちょっとクセの強いOSではあるのですが、使っていくうちにだんだんと安心感を感じてくる面白いOSなので、この機会にぜひご紹介させて頂きたいと思います。

 

ぼくとNixOSとの出会い

ぼくはNixOS(最新バージョンは18.09)というOSを使っています。多分ほとんどの方がご存じないと思います(笑)。ぼくはこのOSを2015年の中頃から使い始めて、最初の頃はMac上のVirtualBoxに割り当てて使っていたのですが、ある日ふと気がついたら、自宅で使っているほぼすべてのPCがNixOSになってしまいました。もちろんdelyでの開発に使っているクライアントマシンもこのNixOSです。

 

NixOS Linux

 

f:id:long10:20181126114437p:plain

  

NixOSについて

NixOSのサイトに行くと、この様な記載があります。

 

The Purely Functional Linux Distribution

NixOS is a Linux distribution with a unique approach to package and configuration management. Built on top of the Nix package manager, it is completely declarative, makes upgrading systems reliable, and has many other advantages

つまり、純粋関数型Linuxディストリビューションなのが売りと言っています。この説明にもあるように、NixOSはNixパッケージマネージャーの上に構築されていて、完全に宣言的に扱うことが可能となっています。というわけでまずはNixパッケージマネージャーについてご紹介したいと思います。

 

Nixパッケージマネージャー

Nix: The Purely Functional Package Manager

 

こちらのNixのサイトに行くと、次のように記載されています。

 

The Purely Functional Package Manager

Nix is a powerful package manager for Linux and other Unix systems that makes package management reliable and reproducible. It provides atomic upgrades and rollbacks, side-by-side installation of multiple versions of a package, multi-user package management and easy setup of build environments.

やはり、純粋関数型なんですね。要するに、NixOSというのは純粋関数型パッケージマネージャー上で動くOS、だから純粋関数型OSというわけなんです。このNixパッケージマネージャーついては、いわゆる通常のパッケージマネージャーの一つですので、aptやyum、homebrewなどと同じようにパッケージを管理するのに扱えます。以下のコマンドでmacOSにインストールすることも可能です。

 インストールコマンド

curl https://nixos.org/nix/install | sh

 

Nixパッケージマネージャーは、nixpkgs(the Nix Packages Collection)に登録されているライブラリの中からパッケージをインストールします。ソースはgithubでリポジトリ管理されていて、Hydraという継続ビルド管理システムで管理されています。

 

github.com

   

Nixの仕組み

 もう少しNixの仕組みについて見ていきたいと思います。 

こちらがNix研究の元となったEelco Dolstraさんの論文です。非常にエキサイティングな内容となっています。

https://grosskurth.ca/bib/2006/dolstra-thesis.pdf 

 

全体としてはこのような外観となっています。

 

[仕組み全体の外観] 

f:id:long10:20181127131118p:plain

 

[主な仕組み]

  • パッケージレシピ関数ファイルが一意に管理される(導出コンパイル用)
  • パッケージレシピ関数はNix言語(Nix Expression Language)で記述する
  • Nix言語はパッケージ管理用DDL
  • NixはNix言語のコンパイラの役割
  • パッケージレシピ関数ファイルはNixストアにインストールされる
  • 導出コンパイル = パッケージレシピ関数のビルド + Nixストアへインストール

 

[Nix言語]

構文例はこちら

Nix Expression Language - NixOS Wiki

Nixにおけるすべての設定関数はこのNix言語で記述します。nix repl を使って構文を試すことができます。

nix-repl> rec{x = 1; y = 3; z = x + y;}.z
=> 4

 

[Nixストア]

  • インストールパッケージは必ずNixストアにインストールされる

  • ストア内に同じパッケージの複数バージョンが共存できる(※なので、HDDの結構ボリュームが消費されます。それらを整理するためにGC機能があります。)

  • インストールパッケージは、ストア内の導出ハッシュ(一意に生成されたハッシュ)によって特定されたフォルダにインストールされる

  • この一意なハッシュ名フォルダにインストールパッケージのすべてのデータが格納される(以下はtreeコマンドの例)

    $ readlink $(which tree)
    /nix/store/zzljry2r5w14rmvg3p9lz7ym326rfcpp-tree-1.7.0/bin/tree
    
    $ tree /nix/store/zzljry2r5w14rmvg3p9lz7ym326rfcpp-tree-1.7.0/
    /nix/store/zzljry2r5w14rmvg3p9lz7ym326rfcpp-tree-1.7.0/
    ├── bin
    │   └── tree
    └── share
        └── man
            └── man1
                └── tree.1.gz

 

[Nixチャンネル(nix-channel)について]

  • Nixチャンネルは特定の条件を満たすnixpkgsのバージョンを提供する仕組み
    • Hydraでビルドとテストが成功している場合に提供される
  • Nixチャンネルの種類
    • 安定チャンネル、例 nixos-18.09
    • 不安定(開発): nixos-unstable
    • サーバ向け、例 nixos-18.09-small, nixos-unstable-small
  • Nixストア内にはNixチャンネルの最新nixpkgsが入っていてNixに利用される
  • Hydraでインストールパッケージがビルドされ、自動的にバイナリパッケージが生成される(これが特定のチャンネルとして提供される) 

 

NixOSの仕組み

NixOSは、このようにNixパッケージマネージャーを中心で構成されているOSなのでした。まとめるとNixOS自体はこのような外観をしています。

 

NixOS全体の外観 

f:id:long10:20181127140859p:plain

 

 

NixOSを使うと以下のような恩恵を受けられます。

  • モジュールシステム(宣言型設定)
  • Dependency Hellからの脱却
  • ロールバック
  • 軽量コンテナー (systemd-nspawn利用)
  • Dev/Opsツール (NixOps)

 

モジュールシステム(宣言型設定)

 

なんと言ってもNixOSといえばこれという機能です。

様々な設定やパッケージ管理、システムの自動化などを宣言的に管理することができます。モジュールシステムはnixpkgs内に含まれていてNix言語を使って記述します。

 

NixOSのメインとなる情報はこのファイルに宣言的に記載します。

 /etc/nixos/configuration.nix

 

宣言的に設定した例をいくつか紹介します。 

プロビジョニングの例

environment.systemPackages = with pkgs; [
  firefox
  termite
  tmux
];

 systemdのサービス

systemd.services.ircSession = {
  wantedBy = [ "multi-user.target" ];
  after = [ "network.target" ];
  serviceConfig = {
    Type = "forking";
    User = "username";
    ExecStart = ''${pkgs.tmux}/bin/tmux new-session -d -s irc -n irc ${pkgs.irssi}/bin/irssi'';
    ExecStop  = ''${pkgs.tmux}/bin/tmux kill-session -t irc'';
  };
};

 cronのサービス

systemd.services.cron = {
 enable = true;
 systemCronJobs = [
 "30 00 * * * sh hogehoge.sh"
 ];
};

 

Dependency Hellからの脱却

 

インストールパッケージは、ストア内の導出ハッシュによって特定されたフォルダにインストールされるので、たとえばバージョンの異なる2つのパッケージが上書きされることもなく(別のハッシュ値が生成されるため)、またパッケージが依存する別のパッケージについてもこの導出ハッシュを用いてビルドされるため、相互的に依存し合うパッケージは存在しません。ですので、インストール時に毎回、環境依存に怯えながらインストールすることはなくなります。(この点に関してはDockerを使えば問題は解決すると思いますが、NixOS上でもDockerはもちろん使えますので、ぼくの場合は用途によって適宜使い分けています。)

 

ロールバック

 

なんらかのsystem設定を間違えてしまったために、OSが起動しなくなってオワタという経験も、NixOSを使えばさっとロールバックできるから安心です。 

  • コマンドでロールバック可能

  • ブート画面でロールバック可能

 

 GRUB画面で前のリビジョンを選択して起動する

f:id:long10:20181127132620p:plain

 

NixOSの再構築

  • nixos-rebuild コマンドで変更した設定を適応する

    $ nixos-rebuild switch
  • 設定をビルドのみする場合(コンパイルチェック)

    $ nixos-rebuild build
  • 宣言的にOSを管理できるので、configuration.nixを別のPCにコピーし、この手順で適応すれば同じ環境を再現できる

 

NixOps Dev/Opsツール

 

NixOSのDev/OpsツールとしてNixOpsがあります。NixOSはDev/Opsを大変重要視した設計思想となっていて、NixOpsを使うことで、デプロイ先のハードウェアや仮想マシンに対してNixOSのセットを展開することができます。また、NixOSのシステム構成管理はクライアントPCとシームレスに管理でき、宣言的なアプローチをネットワークにも拡張し、さらにそのプロビジョニングを追加することができます。ぼくが所有している複数のNixOSマシンについてもNixOpsを用いて設定構成をネットワーク拡張して使用しています。

 

NixOSの不便なところ

 NixOSはとても便利かつ安心して利用できるOSなのですが、すべての人にとってそう言えるかといえばなかなかそれは難しい面もあります。

たとえばこんな点です。

  • クセが強いので慣れるまでに時間がかかる
  • 利用できるパッケージが他のパッケージより少ないので、ないパッケージは自分でビルドする必要がある(ものによっては結構難しい。。)
  • ちょっとしたことを試すのが面倒(そんなときこそDockerを使いましょう)

Dockerのサービス起動もたったのこれだけです。

{
  virtualisation = {
    docker.enable = true;
  };
}

 

終わりに

いかがでしたでしょうか?

NixOSの特性についてざっくりとご紹介させて頂きました。NixOSを使うことで、いわゆる”Dependency Hell”から開放され、依存関係の完結が保証されることで、堅牢な管理や安全なロールバックが可能となります。NixOSには今回ご紹介できなかった機能がまだまだたくさんありますので、もしご興味を持っていただけたらぜひお試し頂けると嬉しいです。

 

不定期開催ですがMeetupやってます!

www.meetup.com

 

余談ですが、今年のNix Conference(Nixcon 2018)で、Nixを使ったデータパイプラインの設定についてのセションがあって個人的にとても興味深かったです。機械学習やパイプライン基盤にもNixOSを使うと面白そうですね。

 

Georges Dubus - Nix for data pipeline configuration.pdf - Google ドライブ

 


Georges Dubus - Nix for data pipeline configuration

 

さて、dely Advent Calendarの明日のタイトルは、『超手軽に構築する!サーバレスなWEBパフォーマンス定点観測基盤』です。お楽しみに! 

「明日からプロダクトマネージャー」と言われたら

f:id:okutaku:20140228120022j:plain

こんにちは!

dely, Inc.でプロダクトマネージャー兼開発部ジェネラルマネージャーをしている奥原 (@okutaku0507) といいます。この記事はdely Advent Calendar 2018の2日目の投稿です。

先日は、弊社のAndroidエンジニアでマネージャーを勤めている梅森から「AWS CodeBuild+AWS SAM(Lambda)+Slackで最高なAndroid CI環境を作る」というタイトルで投稿がありました。梅森はDroidKaigi 2019にも登壇する予定です。

tech.dely.jp

弊社が運営しているレシピ動画サービスのkurashiruはよく知っているけど、開発チームは何しているのかわからないという方に、少しでも弊社の開発部のことを知っていただければ幸いです。もし、同職種や弊社に興味を持ってくれた方がいましたら、僕のtwitterのDMでも良いので、連絡していただけたら嬉しいです。

 

1. はじめに

僕はdely, Inc.に入社した2016年から元々はずっとサーバーサイド (Ruby on Rails) のエンジニアをしていました。そして、今年 (2018年) の5月ごろからプロダクトマネージャーになり、ゴリゴリと施策などを回しています。

良くも悪くも、急成長するベンチャーではあらゆる役職がそもそも存在していなかったり、ほいほい空いたりします。奇しくも、弊社にはプロダクトマネージャーという役職は存在していませんでした。CTOがその業務を担っていた状態でした。しかしながら、CTOは執行役員も勤めているので、組織のことや技術的な視点から会社の未来を創造しなければなりません。プロダクトのことを常に考えるにはキャパがオーバーしてしまいます。そのため、プロダクトマネージャーの登用は急務でした。されど、kurashiru規模のプロダクトマネージャーを外から採用するにはかなりのハードルがあり、採用は暗礁に乗り上げていました。そこで、僕が言われた言葉は、、、

 

「おっくー、明日からプロダクトマネージャーで!」

 

そう、これが僕が未経験ながらプロダクトマネージャーのとてつもなく重い門を叩くことになった転機でした。なぜ、僕がプロダクトマネージャーになろうと思ったのか、今後のキャリアや成長に関する考えは後述しますので、もし同じようなキャリアに悩んでいる方がいれば、少しでも参考になれば幸いです。

 

2. プロダクトマネージャーとは

日本でも当たり前になったプロダクトマネージャーですが、会社、組織、プロダクトにいよって、本当に様々なことをする役職です。ですが、その本質をざっくり定義すると「プロダクトの成長に責任を持つ人」だと想っています。

残念なことに有料の記事ですが、この記事はわかりやすく書かれていて、とっかかりやすかったです (ステマではありません...) 。

newspicks.com

この記事にもあるようにプロダクトマネージャーには「ミニCEO」としての責務があると思っています。ちょっと表現が強いですが、えふしんさん (@fshin2000) のツイートも核心をついていると思っています。

 以下の記事の中でYamottyさん (@yamotty3) がおっしゃること、そのままだと思います。

PMの役割や必要なスキルは、会社やチームによって全然違います。10Xであれば、僕以外のメンバーは全員エンジニアなので、僕は社長業に加えてPMとして、デザインや顧客開発を含むエンジニアリング以外のプロダクトに関する全ての仕事をやります。逆に、あらゆる職種が揃っている会社なら、PMは調整力や巻き込み力のほうが重要かもしれません。

www.fastgrow.jp

また、以下の記事にあるように、メルカリのように同じプロダクトを触っていたとしても一つの領域をまるっと任せるというスタイルもあります。

www.fastgrow.jp

では、dely, Inc.ではどのような業務をプロダクトマネージャーがこなしているのかというと、「何でも屋」という役割が大きいです。ざっくりプロダクトマネージャーとして僕がこなしていることを箇条書きで書きます。

  • ユーザーインタビューやデータ分析を通して、解決すべき課題を特定する
  • ユーザー、ビジネス、グロース、デザインの課題に対して解決策を発案する
  • 課題とそれに対する解決策をプロダクトに反映させるために開発プロセスを回すファシリテートをする
  • カスタマーサクセスを行う
  • コードを書く
  • KPIの報告を行う
  • ドキュメントおじさんとなって開発仕様書を書く
  • 部署間調整を通して優先度付けを行う
  • 意思決定の説明を行い、チームから納得を得る
  • エンジニア/デザイナーの採用を行う
  • twitterでエゴサする
  • etc...

そんなこんなを行なっていると、一瞬で太陽が沈みます。

前述した、Yamottyさんの記事の引用にもあるように、dely, Inc.では人は全く足りてませんが、あらゆる職種が最低一人は揃っていて、部署が存在します。10人も満たない組織で、一人一人が全ての領域の細部まで把握して意思決定して行動していく像とは、現実的には異なっています。そのため、限られた開発リソースの中、部署間の調整を行い、部署横断的な視点を持って、今会社にとって何をすべきかを考えて優先度を決定し、仲間の納得を得てチームをドライブさせていく力が求められています。

僕は元々はサーバーサイドのエンジニアであるため、現時点ではエンジニアリングの領域に強みを持っています。そのため、直近で行なっている僕の仕事としては、不確実性に向き合う組織を創り上げて、リーンなプロダクト開発フローを確立していくことが挙げられます。

リーンなプロダクト開発について、メルペイのプロダクトマネージャーをされている川嶋さん (@tsumujikaze) が書かれたnoteがとても参考になります。

note.mu

今では、このリーンなプロダクト開発も軌道にのってきて不確実性に向き合う強いチームになったと思っています。このリーンなプロダクト開発をどのように弊社で実践しているのかをAWS Dev Day 2018で登壇させていただく機会に恵まれたので、その時の資料を掲載しておきます。

speakerdeck.com

 

3. 「明日からプロダクトマネージャー」と言われたら

ここまできて本題について書きたいと思います。エンジニアから経験もしたことないプロダクトマネージャーになった僕のケースですので、ツッコミどころ満載かと思いますが、そこらへんはご容赦していただければ幸いです。

組織文化にもよりますが、dely, Inc.では様々な役職をぽーんっと任せることがよくあります。僕が任されたプロダクトマネージャーもそんな感じで渡ってきました。そして、 前述しましたが、弊社にはプロダクトマネージャーがいませんでしたので、実質的に引き継ぎなどはありませんでした。

3.1 形から入る

そのため、僕がまず着手したのは「プロダクトマネージャーとは?」を調査し、自分の中のプロダクトマネージャー像を確立させることでした。

まずは、ググりまくりました。「プロダクトマネージャー とは」と。そして、様々な記事や先駆者の言動を見ていく中で、プロダクトマネージャーは総称であり、今では当たり前だと思っていますが、様々な形態があることがわかりました。つまり、プロダクトマネージャーとは、プロダクトの成功に責任を持ち、なんでもやる役職だということがわかりました。なので、弊社のkurashiruというサービスのプロダクトマネージャーは自分で形成していくしかなかったのです。誰も正解を教えてはくれません。「さて、困ったぞ。何をすればいいんだ。」という状態でした。

3.2 整理する

何をすればいいのかわからないというというカオスな状態だったので、まずは整理が必要だと思いました。具体的には、プロダクト開発に直接的に関わっている部署、弊社でいうとタイアップ広告事業部、マーケティング部、ブランド部 (ブランド醸成に関わり、主にコンテンツの制作を行なっている)、そして開発部として、プロダクトに変更を加える施策として何を考えているのか、ヒアリングすることから始めました。

元々、サーバーサイドエンジニアとして様々なデータを扱っていたり、管理サイトの構築などを通して他部署とのコミュニケーションがあったので、部署間でどのような施策を考えているのかは肌感はありましたが、さらに具体的に聞くことで、工数見積もりやそれぞれの部署でのプライオリティの高さなどを聞くことで、施策の解像度を高めていきました。

3.3 優先度を決める

全ての部署が一つのプロダクトを良くしようと動いていたとしても、基本的には開発リソースは一つで、ほとんどの場合それもパツパツだと思います。その中で、何をすべきかを考えることは容易ではないし、その意思決定はその時の気分に左右されることなく、再現性がなければなりません。

そのために、執行役員でCTOである大竹 (@EntreGulss) にBizサイドの諸々をコーチングしてもらう時間を設けてもらいました。プロダクトの行く末を左右するプロダクトマネージャーには会社のほぼ全ての情報を頭にインプットしておく必要があります。そのため、経営層の考えを聞くことは不可欠でした。

各部署の責任者であるジェネラルマネージャーは部署間で情報を共有できていたとしても、プロダクトに変更を加える必要がある各部署の大小ある施策の全て頭に入れておくことは難しいと思います。そのため、プロダクトマネージャーは全ての情報を自分に集積し、今会社にとって解決しなければならない課題は何かを決定しなければなりません。各部署で優先度が高い施策であっても、それがプロダクトにすぐに反映できるとは限りません。その優先度を決めた背景をロジカルに説明する責任も同時にプロダクトマネージャーが負っているわけです。

3.4 実行する

なぜ、今その施策をするのか。施策のwhyを開発に関わる全ての人が知っていることはとても重要です。納得感を持って開発できるかが施策の良し悪しを左右することもあると考えています。その責任もプロダクトマネージャーにはあります。そのためには、ドキュメンテーション能力が不可欠です。僕は、ここで自分の意思決定を言語化することにとても苦戦しました。あらゆることを総合して考えて意思決定を行なっているので、それらを順序立てて、全ての人に理解してもらえる粒度で言葉に落とすことはとても難しいです。そのため、自分のドキュメントで納得していなさそうなメンバーが入れば席まで行って、納得してもらえるまで議論することもありました。

そして、実際の開発では今まで実装段階ではアジャイルを取り入れていたものの、要件定義をウォーターフォールで行なっていたがために、課題はあっていても解決策が間違っているということが度々あり、それが開発での課題となっていました。要件定義もアジャイルで行おうという、前述したリーンな開発プロセスの定着にメンバーとともに注力しました。要件定義フェーズでは、最初は細部まで介入して、それらが上手く回るようにフローの改善を行なったりしていました。

そして、部署間で実行する施策や全社規模で注力する機能については、スケジュールを常に更新、可視化し、ステークホルダーには通知するなどを行い、部署間の摩擦や間違ったものを作ってしまうようなリスクを減らすことも行いました。

3.5 プレイヤーから離れる

自分がプレイヤーから離れる、これが一番難しかったことです。最近では新卒からプロダクトマネージャーを任せるケースやプロダクトマネージャーが一般化したことで転職するケースもあると思います。しかしながら、内部からエンジニアあるいはデザイナーからプロダクトマネージャーになるケースが多いのではないでしょうか。その時に、しっかりと自分のプレイヤーとしての業務を引き継ぐ時間があれば良いのですが、弊社のようにいきなりプロダクトマネージャーを任されるケースでは、そうはいきませんでした。

僕の場合は、サーバーサイドを書きながらプロダクトマネージャーとしての業務を遂行する必要があります。自分がコードを書くことが好きで、開発リソースがパツパツ且つやりたいことがたくさんある中でついつい自分を戦力としてカウントして、様々なことをこなしてしまいがちでした。しかしそれではどっちつかずになってしまい、自分もキャパオーバーになりかけていました。心を鬼にして、自分がプレイヤーから離れる決断をしなければなりませんでした。幸いなことに、弊社サーバーサイドチームは少数ながら精鋭が集まっており、徐々に自分が担当するタスクを減らしていくことで、フェードアウトすることができました。

 

4. プロダクトマネージャーとして大切なこと

半年間という短い期間ながら、プロダクトマネージャーを経験していて、大切だと思った考えをお伝えしたいと思います。

4.1 執着し、諦めない

前述した、えふしんさんのツイートにもあるように、プロダクトマネージャーには24時間のマインドシェアをプロダクトに費やすことができるかが資質なのではと思います。費やすという表現も適切ではなくて、気がついたらプロダクトのことを考えていたくらいがちょうどいいと思います。自分でサービスを立ち上げたことがある人なら思い当たる節があるのではないでしょうか。気がつけば、自分のサービスを使ってもらうにはどうすればいいのかを考えていたということがあるはずです。

そして、プロダクトは順調な時ばかりではありません。長く苦しい時期に差し掛かる時もあります。苦しい時でもプロダクトに寄り添い、更なる成長ができることを諦めない。頭がちぎれるまで考える。時には自分も泥臭いことをする。そんな諦めの悪さも必要だと思っています。

4.2 自分の枠組みだけで考えない

つい自分の枠組みで考えてしまうことがあります。例えば、どうやればDAUをあげることができるかという課題に対して、開発側で考えたら、リテンションを上げるための施策を考えてしまいがちです。それ自体は間違いではないのですが、その答えはプロダクト開発の外にあるかも知れません。もしかしたら、リアルイベントを開催したら思わぬ盛況ぶりで、SNSでバイラルしまくり結果として、自分のプロダクトをグロースさせることができるかも知れません。つまり、DAUを上げるためにはプロダクト変更はいらずに、マーケの施策で解決できるかも知れません。そうなると、マーケティング的な視点など自分の枠組みの外で考えることも必要になるわけです。

そのためには、常に社内外でアンテナを高く貼り、様々なサービスの施策をみて自分たちのプロダクトにローカライズさせるとどうなるかを考えておく必要があります。ふとした瞬間に自分の頭の中に一つ一つのピースがぴったりと繋がることもあるのです。

4.3 逆算思考

それはちゃんと目標から逆算して考えているのか、と自分に常に問うことは大切です。スマートニュースのプロダクトマネージャー をされている前田さんの記事から引用します。

また、大きなジャンプを生むために、目標は逆算で設定しています。

「1年後にユーザーあたり収益を3倍にする」といった厳しい目標を立てて、そこから逆算して考えるということを日々行う。これは、非常に重要な状況設定だと思います。

seleck.cc

この記事にあるように、例えば1年後にユーザーあたりの収益を3倍にするという目標を立てたとします。この値は事業計画から引っ張ってくるのでいいと思います。今の実際の成長率を厳しめにみて、成長曲線を引いた時に、1年後には収益は2倍にもいかないことがわかったとします。そうすると、このままやっていては目標が達成できないことに気が付くわけです。そうすると、違う手を考えなければなりません。その中で思わぬ手が思いつき、一気にグロースさせることができることもあります。

 4.4 解決すべき課題設定で全てが決まる

今僕らが全力を掛けてでも解決すべき課題は何であるかを考えることがプロダクトマネージャーの本質だとも思っています。その施策が成功し、プロダクトを大きくグロースあるいは改善させることができるかは課題を設定した時点で決まっていることもあります。そのため、ユーザーあるいはビジネスにおける課題を発見し、それらに優先順位をつけて解決していく、そこにプロダクトマネージャーの手腕が問われていると思います。

4.5 難しい課題にフォーカスする

エンジニアあるいはデザイナーのリソースはとてつもなく貴重です。プロダクトには大小様々な問題があります。改善リストを作りちょっと運用しただけでも、かなりの数の解決しなければならない課題が見つかることでしょう。その時によく思い出すメタファーは、以下の記事にある瓶と石と砂とコーヒーです。

tabi-labo.com

この記事で言っていることはプロダクト開発にも適応されます。瓶の中に、最初から砂を詰めていると、肝心な時にそれらに邪魔されて石を詰めることができなくなってしまいます。プロダクト開発においても、目先の小さい施策に気を取られてしまっては、本当に僕らが解くべき、難しく大きな課題を解けないまま時間だけが過ぎてしまうのです。小さい機能でも、アプリはリリースされたら取り返しがつかない場合が多いので、隅々までチェックすると思います。結果として、大小関わらず一つの機能をリリースするには、ある程度の工数がかかるわけです。その中で、解くべき大きな課題に対して全力で取り組む、このマインドを持っておくことが大切だと感じています。

 

5. プロダクトマネージャーになろうと思った理由

なぜ、プログラムを書くことが大好きな僕がプロダクトマネージャーになろうと思ったかを簡単に書いておきます。

僕自身のエンジニアになろうと思った理由が「テクノロジーを通して世の中を幸せにしたい」という人生の目標があるからです。その中で、自分はプログラムを書くことにハマっただけで、それは手段に過ぎないと思っています。世の中を幸せにするプロダクトを自分の手で創っていくためには、一人のエンジニアとしては限界があると思っていたし、意思決定をしていくためには自分のレイヤーを上げていくしかないと考えていました。そのための選択として、一番良いと思ったのがプロダクトマネージャーになることでした。

 

6. 最後に

「明日からプロダクトマネージャー」と言われた自分の経験を通して、弊社のプロダクトマネージャーの役割やマインドセットについて紹介しました。いかがでしたか。

プロダクトマネージャーは何でも屋であるが故に、大変な場面も多々あります。しかしながら、世の中をより良くしていく実感は強く、やりがいのある役職だと思っています。

弊社では、ユーザーあるいはビジネスの課題に対して成果に執着していけるプロダクトマネージャーを募集しています。これからさらに、弊社と弊社が運営しているプロダクトはどれも面白くなると自負しています。プロダクトマネージャー以外の役職でも、もし興味があれば、僕のtwitterアカウントでも何でもいいので、声をかけていただけると幸いです。是非お茶でもしましょう。

明日は、弊社の機械学習エンジニアの辻より自身が使っているNixOSの話が書かれると思います。是非、お楽しみにしていてください。

tech.dely.jp