Quantcast
Channel: クックパッド開発者ブログ
Viewing all 734 articles
Browse latest View live

「ぶっちゃけお給料はどうですか?」夜の合同説明会を今年も開催します!(クックパッド・freee・グリー・はてな)

$
0
0

f:id:mirakui:20200108164043j:plain

エンジニアとしての就職を考えている学生のみなさま、こんにちは。クックパッド CTO の成田です。 就職先やインターン先を探している方も、これから探し始める方も、そもそもどんな会社にどんな仕事があるのか、イメージは沸きますか?

企業のホンネ、知ってますか?

よくある就職説明会では、各企業の人事担当者の方や、現場のエンジニアの方とお話しする機会があると思います。しかし、本当に皆さんの聞きたいホンネは、そういった場で教えてもらえるでしょうか。

クックパッドを含むインターネット系の企業では、エンジニア組織のこと(エンジニア採用基準、育成、評価、給与など)は CTO が意思決定権を持っている事が多いため、裏事情やホンネを聞きたければ、CTO に聞くのが一番手っ取り早く、正確です。

そんな機会を用意したく、4年前からクックパッドでは「夜の合同説明会」を他社と合同で開催しています。

夜の合同説明会って?

夜の合同説明会の趣旨は、CTO やその相当職のパネラー達が、参加者からの質問に何でも答える、というものです。 もし聞きたい質問がまだないという場合は、他の人の質問を聞いているだけでも大丈夫です。

各社、都合の悪いことも含めて本当に何でも答えちゃうので、回答内容は SNS への投稿を禁止しています。悪しからず。 そういう、普通のお堅い会社説明会と違った雰囲気をつくるために、会場はクラブをお借りしています。クラブで開催するのはただの演出なので、踊ったりする必要は無いし、ドレスコードもないので、勉強会のような感覚で安心して気軽にお越しください。

パネラー

今年のパネラーは以下の方々です。

藤本真樹 (グリー株式会社 取締役 執行役員常務 最高技術責任者) @masaki_fujimoto

大西康裕 (株式会社はてな 執行役員 サービス・システム開発本部長) @yasuhiro_onishi

成田一生 (クックパッド株式会社 執行役 CTO) @mirakui

横路 隆 (freee株式会社 共同創業者 最高技術責任者) @yokoji

日時・申し込み方法

日時は 2020/01/22(水) 19時開始で、場所は東京・六本木です。

以下の connpass ページからお申し込みください。会場の住所等も記載しています。

夜の合同説明会 - クックパッド, freee, グリー, はてな - connpass

どんな質問があるの?

過去3回開催しています。過去の回に出た質問の例を紹介します。

  • 新卒採用ではどんなエンジニアが欲しいですか?
  • 学生時代にやっておくべき事はなんですか?
  • ぶっちゃけお給料はどうですか?
  • 新卒が成果を出せるようになるまでの期間はどのくらいを見込んでいますか?
  • 儲かるのと社会貢献、どっちが大事ですか?
  • 新卒に対して求めるのは「カルチャーマッチ」「技術」どちらですか?
  • いいエンジニアとはなんですか?
  • 転職して出て行く人についてどう思いますか?
  • 新卒採用では、休学や留年をした人についてどのような印象がありますか?
  • 各社のホンネの裏話を一つずつ教えてください
  • 他社の10年先を行ってるような技術はありますか? また、そういう技術のための投資をしていますか?

おわりに

「夜の合同説明会」は、毎年とても有意義なホンネのトークセッションが行われており、好評をいただいています。なかなかこのパネラー達の話をまとめて聞ける機会はないので、学生のみなさま、お誘い合わせのうえ是非ご参加ください!

夜の合同説明会 - クックパッド, freee, グリー, はてな - connpass


license-tools-pluginの移行に関するお知らせ

$
0
0

はじめまして、クックパッドでAndroid開発を担当している吉田です。 本記事は弊社がOSSとして公開していたAndroid向けのライセンス管理プラグインに関するお知らせです。

2行まとめ

旧license-tools-pluginをアーカイブした背景

クックパッドでは社内で利用していたライブラリのいくつかをOSSとして公開します。これらは国内外を問わず多くの方にご利用頂いていることを観測しており、皆様の開発が僅かでも効率化されていればとても嬉しく思います。 さてOSSについてまわるのがメンテナンス問題です。クックパッドのエンジニアもここ数年で入れ替わりライブラリの著者が既に在籍していないケースもあります。license-tools-pluginもそのケースに当てはまっており、issueやPRが届く度に持ち回りで対応していたのですが直近ではほとんど対応出来ていない状態でした。そういった経緯から一度チーム内で話し合いの場を設けて議論したのですが、「ずるずる公開し続けているよりは一思いにアーカイブしよう」という結論になりました。

新たにLicenseToolsPluginを開発した背景

私個人としてlicense-tools-pluginは非常に気に入っているプラグインの一つでした。過去には紹介記事も書いています。

techlife.cookpad.com

愛着のあるOSSだったため継続してメンテナンスしたかったのですが、一番の心理的な障壁がGroovyで実装されている点でした。そんな折にふとKotlinでプラグインをフルスクラッチ出来るのではないかと思い、休日にコアロジックを移植してみた所思いの外上手く行きました。後日そのような話をテックリードとの1on1で話したところ「業務時間使ってプラグインの開発をやっていいよ」とあっさり承認を頂いたので100% Kotlinによるフルスクラッチプロジェクトを始めることにしました。これは蛇足というか弊社のアピールですがクックパッドはOSS活動に理解があるのでライブラリ開発の7~8割は業務時間を利用して書いています。
そのようなわけでフルスクラッチ化のプロジェクトも無事完了し、クックパッドのAndroidアプリは既に新しいライセンスプラグインに置きかえました。互換性の問題や大きな不具合は解決したと思いますので、みなさまにも安心してご利用頂けると思います。また機能はシンプルですがgoogleから公式のプラグインが提供されているのでこちらをご検討ください。

developers.google.com

新プラグインへの移行

コマンドやオプションは互換性があるため、cookpad/license-tools-pluginをご利用のプロジェクトではプラグインだけ新しいものに切り替えれば動作します。これは一例ですがアプリケーションモジュールの差分は以下のようになるかと思います。

buildscript {
    repositories {
        google()
        jcenter()
        mavenLocal()
    }
    dependencies {
-        classpath 'com.cookpad.android.licensetools:license-tools-plugin:1.7.0'
    }
}
- apply plugin: 'com.cookpad.android.licensetools'+plugins {+  id "com.cookpad.android.plugin.license-tools" version "${latest_version}"+}

apply plugin: 'com.android.application'

$latest_versionはレポジトリを参照して最新のバージョンをご利用ください。初めて利用するという方やその他詳細について知りたいという方はレポジトリのREADMEをご覧ください。

github.com

DroidKaigi2020のお知らせ

Droidkaigi 2020では「KotlinではじめるGradle Plugin」というセッションを話させて頂くことになりました。プラグイン開発はAARライブラリの開発より情報量が圧倒的に少なかったり、馴染みの薄いGroovy実装による解説記事が多かったりと初心者にはやや敷居が高いですが、難しいことは少なく業務効率の改善にも繋がりやすい技術だと思いますのでご興味があればカンファレンス会場まで足を運んで頂けると嬉しく思います。

クックパッド Android アプリ CI を CodeBuild に切り替えた話

$
0
0

こんにちは、モバイル基盤部の加藤です。 モバイル基盤部では開発者の開発環境や CI 環境の改善に取り組んでいます。 今回はその中でクックパッド Android アプリの CI 環境を CodeBuild へ移行した取り組みを紹介します。

クックパッド Android アプリで先行して移行を行った話となっていますが、他プロジェクトに関しては今後順次移行する予定となっています。

これまでの CI 環境

この記事では以前 Android アプリの CI 環境を紹介した Genymotion On Demandを使うようになってAndroidのCIがさらに1分短縮した話からの差分を中心にご紹介します。 上記の記事をまだ読まれてない方はぜひご一読の上この記事を読まれることをおすすめします。 これまでの CI 環境の概要を説明すると以下のような図の構成となっていました。

f:id:ksfee:20200129181539p:plain

既存環境の問題点

以前の記事でも言及している Genymotion Cloud(旧 Genymotion On Demand) の問題点に加え、現在の CI 環境が抱えている問題についてもそれぞれ解決策を考えました。

  1. 並列実行上限数の壁
    • 3台構成ではピーク時にキューが詰まってしまうことがある
    • ピーク時にシュッと台数を増やせない
    • → CodeBuild の利用による並列実行上限限数の緩和
  2. コストが高い
    • 社内の CI 環境はほぼすべて AWS 上で構築されていますが、その中でも Android 環境は上位に入る高コストとなっていました
      • アプリのビルド時間を短縮するために性能が良い高価なインスタンスを常時稼働状態であることが大きな要因
    • → CodeBuild の従量課金制によるコスト削減
  3. Genymotion Cloud が辛い
    • Instrument Test の実行環境として提供
    • Google Play Service が利用できない
      • アプリ内課金など実行できないテストが発生してしまう
    • エミュレータとの接続が原因不明のエラーで不安定になり、その都度再起動する必要があった
    • 実行環境(API Level や 端末種別)をプロジェクト側で個別に設定できない
      • 実行ごとに AMI を変更することは難しいため
    • → Firebase Test Lab の利用

コスト面に関してはただ高いから移行すべきということではなく、他の2つの問題点とあわせ見直しを図ったものです。 これらの問題を複合的に捉え、今回は EC2 + Genymotion Cloudから CodeBuild + Firebase Test Labという以下のような構成に変更しました。

f:id:ksfee:20200129181645p:plain

CodeBuild への移行

モバイルアプリの CI サービスとして Bitrise や Circle CI 等のマネージドサービスも有名ですが上述した問題の解決に加え、AWS 上に構築されたモバイル開発に必要なツール群(社内向けアプリ配信基盤、GitHub Enterprise、社内 Maven 等)とのやりとりのしやすさという点を重視して CodeBuild を採用しました。 移行を検討した段階で社内での普及が徐々に進んでおり、環境を整えやすかったという理由もあります。 それぞれの問題点に対しての採用理由は後述します。

CodeBuild について簡単に説明すると AWS が提供する CI/CD 用のビルドサービスであり、Docker 上に構築したビルド環境をいくつかのインスタンスタイプから選択して実行が可能なマネージドサービスとなっています。 並列実行も可能で、最大並列実行数である60までは自動的にスケールし、キャパシティを事前に設定する必要がありません*1

1. 並列実行上限数の壁

元々3台構成で Android CI を運用していましたが、日中開発が活発な時間帯に多くのジョブが同じタイミングで実行されてしまうと、高頻度で CI が詰まってしまうことがありました。 ジョブ単体でみればそのほとんどが実行時間が10分前後のものばかりなので、キューが詰まったとしても何時間も待つことはありませんが、CI 実行を多少なりとも待つ場面を何度観測しました。

この問題に対して CodeBuild では最大並列実行数が増えたため、キューに積まれた状態のまま実行を待つという状況が発生しなくなりました。 現在最大並列実行数に届いてしまう状況は観測していませんが、今後多くのプロジェクトで CodeBuild が利用された結果、キューが詰まるようになってしまう状況も考えられるため、今後改善が必要になるかもしれません。

またピーク時のみ実行数が一時的に増加するという状況に対して、事前にキャパシティを設定する必要がない CodeBuild の従量課金制は非常に相性が良かったです。

2. コストが高い

以前の環境では EC2 インスタンスを c4.4xlarge + m4.large(Genymotion Cloud)という構成で3セット利用していました。 これらのインスタンスを仮にオンデマンドインスタンスで起動し続けた場合、月に約$2400のコストがかかります(2019年12月時点の東京リージョン)。 これは社内で運用している CI 環境の中でもかなりの高コストとなっており、課題感がありました。

上述した料金は Android CI 環境全体のコストであり、今回はクックパッドアプリの CI 環境のみを移行した段階における内容なので、まだ全体のコストの比較が可能な段階ではないので具体的な値を記述することができません。 しかし大雑把にコストを見積もった限りでは、コストを約1/6に抑えることができるようになる予定です(あくまで予定です)。

また Genymotion Cloud に変わる Firebase Test Lab については、実行数制限がない Blaze プランで実機デバイスでも $5/hour という価格帯なので、Genymotion Cloud と比較すると雀の涙程度の料金となっています。 (参考としてクックパッド Android アプリの Instrument Test は1回10分未満程度で実行可能です)

3. Genymotion Cloud が辛い

CodeBuild では EC2 と同様に KVM 等を利用して Android エミュレータのハードウェアアクセラレーションは利用できないため、CodeBuild とは別で Android 実行環境を用意する必要がありました。スケールの面を主軸に考え、マネージドサービスを中心に検討を行いました。

Android 実行環境を検討するにあたり考慮した点は以下の3点です。

  • Google Play Service が利用可能
  • 柔軟に実行数をスケール可能
    • CodeBuild の実行数に合わせてスケール可能な状態に
  • 各プロジェクトで実行環境の設定が可能

Google Play Service を CI 環境でも利用したい(主にテストが書きたい等)という要望があがり始めていたので、今回の CI 環境移行のタイミングで改善することとなりました。 また CodeBuild の実行状況に合わせて柔軟にスケールが可能であり、かつ CodeBuild からできるだけ容易に接続できるかという点を考慮しました。 さらに特定のスペックの端末でテストを実行したいというような要望も合わせて検討しました。

現在 Android 実行環境を提供しているマネージドサービスはたくさん存在しています。 AWS にも DeviceFarm という機能がありますが、プロビジョニング時間や端末のバラエティ、またエミュレータが利用できない等のいくつかの理由から利用を諦めました。 Bitrise や Circle CIなどの環境も考慮しましたが、AWS 上に構築された社内リソースをうまく活用できるという点からも CodeBuild を採用したので、これにうまく組み合わせることができる Firebase Test Labを採用しました。

Firebase Test Lab では実機とエミュレータの2種類から実行環境を選択でき、エミュレータを選択することでデバイスの空き状況を気にせずに並列実行を行うことができ、また Google Play Service が利用可能なデバイスを利用であり、かつ Google が提供するサービスというのが大きな決め手となりました。 以前から Test Lab の RoboTestを利用しており、その延長線で Instrument Test の実行も Test Lab に移したという面もあります。

実行方法にも触れておくと、CodeBuild から以下のような独自の Gradle タスクを利用して実行しています。

afterEvaluate {
    def testDirs = android.sourceSets.androidTest.java.srcDirs
    // Instrument Test が存在しないモジュールはスキップif (!project.files(testDirs).getAsFileTree().isEmpty()) {
        android.testVariants.all { variant ->
            File targetApk
            def testApk = outputs.first().outputFile
            def script = "script file"// 引数を使って gcloud コマンドを実行するスクリプトdef runner = android.defaultConfig.testInstrumentationRunner

            if (android.hasProperty('applicationVariants')) {
                task connectedDebugTestLabAndroidTest(type: Exec, dependsOn: ['assembleDebug', 'assembleDebugAndroidTest']) {
                    targetApk = android.applicationVariants.first().outputs.first().outputFile
                    executable script
                    args targetApk, testApk, runner
                }
            } elseif (android.hasProperty('libraryVariants')) {
                task connectedDebugTestLabAndroidTest(type: Exec, dependsOn: ['assembleDebugAndroidTest']) {
                    // ライブラリモジュール用のダミー APK
                    targetApk = File.newInstance("dummy.apk")
                    executable script
                    args targetApk, testApk, runner
                }
            }
        }
    }
}

通常の Instrument Test の実行と同様に、テストが実装されていない Gradle モジュールは実行をスキップする仕様になっています。 また Test Lab のインタフェース上、ライブラリモジュールであってもテストターゲットとなる APK/AAB のアップロードが必要なのでダミーの APK を送信するようにしています。

f:id:ksfee:20200129181745p:plain

実行時間の短縮

ここまで CodeBuild と Test Lab の構成について説明しましたが、移行を検証する中でこれまでのジョブの実行を移しただけではビルド時間が約3倍になってしまうことがわかりました。 以前の CI 環境では処理能力の高いインスタンスを利用していたためビルド時間が早く、またインスタンスは常駐しており、かつ3台に集約していたため、インスタンス内の Gradle キャッシュをうまく利用できていました。 しかし CodeBuild を利用し実行環境が分散された結果、その恩恵を受けることができなくなってしまいました。 移行した結果ビルド時間が伸びてしまうことではいくら並列実行が可能になったとしても、結果的に開発者の待ち時間が伸びてしまい本末転倒です。 そこでビルド時間の最適化を図るためいくつかの取り組みを行いました。

キャッシュ

CodeBuild でジョブの実行に利用していた Docker イメージには Android SDK と必要最低限のツール類だけが含まれていました。 このためジョブを実行する度にプロジェクトごとに必要な外部ライブラリ等をダウンロードする必要があり、さらに CodeBuild から外部ネットワークへの通信が非常に遅く、ダウンロードのプロセスがオーバーヘッドとなることがわかりました。 そこで外部ネットワークへ接続せず、ジョブ実行時にすでにキャッシュデータがダウンロードされている状態を作り出すことを考えました。

S3 キャッシュ

はじめに CodeBuild のキャッシュ機能を試しました。 CodeBuild にはS3キャッシュとローカルキャッシュという2種類のキャッシュ機能があります。 S3キャッシュはその名の通りS3をストレージとしたキャッシュですが、データ量が数GBほどになると保存と復元がオーバーヘッドになってしまいます*2。 ローカルキャッシュは速度の心配はありませんが揮発するタイミングが早く、1日に何度か揮発してしまうため、その都度ビルド時間が増加してしまうことは許容できないと判断しS3キャッシュを利用することにしました。 ただ前述したとおり、キャッシュとして扱うデータ量が多くなってしまうとS3との通信がオーバーヘッドとなってしまうことがわかったので、可能な限り扱うデータ量を少なくする必要がありました。

そこで外部ライブラリ等の比較的静的なもの($HOME/.gradle/caches/jars-3等)はS3キャッシュで、比較的動的に置き換わるビルドキャッシュを Gradle ビルドキャッシュサーバで扱うことにしました。 Gradle ビルドキャッシュサーバについてはドキュメント通りの利用方法なので、ここでは割愛します。

f:id:ksfee:20200129181819p:plain

キャッシュの利用によってビルド時間は以前の約1.5倍程度まで短縮しました。 S3キャッシュの復元・更新がオーバーヘッドとなり、これ以上の短縮は難しい状況でした。

Docker イメージによるキャッシュ

オーバーヘッドとなる S3 キャッシュを利用せず、かつ外部ライブラリのキャッシュをジョブごとに取得しない方法として Docker イメージにキャッシュを載せる方法を試しました。 この方法の懸念点として Docker イメージのサイズ肥大化によって CodeBuild のプロビジョニング時間が伸びる可能性がありましたが、Docker イメージのサイズが約1GBから約3.5GBとなってもプロビジョニング時間にほぼ差はなかったため、復元時間をほぼ0にすることができました。 また Docker イメージの更新はジョブ実行とは別で定期的に実行するようにしたため、更新時間も0にすることができました。

Docker イメージに外部ライブラリをダウンロードは analyze*Dependenciesタスクを実行することで実現しています。

ジョブの分割

CI ジョブはそれぞれ実行時間が重要なものとそうでないものがありますが、特に重視したのは Pull Request に合わせて実行されるジョブでした(以後 PR ジョブ)。 開発者が普段開発する中で最も実行機会の多いPRジョブですが実行に時間がかかり過ぎてしまうと、レビューが通ったとしても無駄に待つ必要がある、CIでテストを実行したいのにいつまで経っても終わらない等の問題が発生してしまいます。

PR ジョブではユニットテスト、Lint、ライセンスチェック、社内向けアプリ配信サービスへのアプリアップロードなど、非常に多くのタスクを実行していました。 これらのタスクは互いに依存しておらず、また CodeBuild を利用することで最大並列実行数が緩和されたことからジョブの分割実行を試すことにしました。 ジョブを複数に分けることで、後続のタスクでキャッシュを活かすことができなくなる等の理由で個々のタスク実行時間は伸びてしまいましたが、結果的に実行時間の短縮につなげることができました。

ここまでの取り組みで PR ジョブにおいては以前の CI 環境と比べ、同等またはそれ以下の実行時間を実現することができました。

f:id:ksfee:20200129181904p:plain

今後の課題

冒頭に挙げた問題点はほぼ解消できたように見えますが、まだまだ課題があります。 前述したように、現在S3キャッシュがオーバーヘッドとなっていますが、Gradle キャッシュに保存される外部ライブラリをまるごと保存しています。 ライブラリのバージョンが更新され新しい JAR や AAR を使うようになっても、古いバージョンがそのままキャッシュとして保存されてしまいデータ量が増え続けてしまうので、プロジェクトが依存するライブラリのバージョンに合わせて必要最低限のキャッシュを維持する仕組みを構築する必要があります。

また冒頭に記述したとおり、まだ全ての Android アプリの環境が移行されたわけではないので、今後も他のアプリの移行を推進して並列実行数の緩和やコスト削減などにつなげていく予定です。

今回CI用に Gradle ビルドキャッシュサーバを導入しましたが、キャッシュの読み取りは各開発者の環境においても活用できるので、今後開発者の手元からも利用できるようするなどの展望があります。

まとめ

今回はクックパッド Android アプリの CI 環境をいくつかの問題点を考慮し、CodeBuild + Firebase Test Lab という環境を構築し、またその中で行った取り組みについて紹介しました。 並列数上限の緩和、また各プロジェクトごとで構築可能なビルド環境、スケールする Android 実行環境によって開発者ドリブンな CI 環境を構築することができました。 また移行により増加してしまったビルド時間については、キャッシュやジョブの分割による工夫により以前と同程度の実行時間を実現することができました。

CI は開発者が開発を効率化や仕組み化するために用いるものであるため、今後も日々の開発の効率化を念頭に環境整備を行っていく予定です。

モバイル基盤部ではこのように開発環境の改善及び仕組み化を行っているので、ご興味がある方はぜひ一度クックパッドオフィスまでお気軽に遊びに来てください。

https://info.cookpad.com/careers/

Cookpad TechConf 2020 開催中止のお知らせ

$
0
0

私たちは、クックパッドのエンジニアやデザイナーによる食と料理の課題解決についてご紹介するために、 Cookpad TechConf 2020の開催を3月9日に予定しておりました。

しかしながら、今日現在、新型コロナウイルスの世界的な感染拡大に歯止めがかかっておらず、かつ有効な対処方法について未だに見通しが立たない状況です。つきましては、来場者のみなさまやスタッフの健康を第一に考えた結果、まことに遺憾ではございますが、 Cookpad TechConf 2020の開催を中止することを決定いたしました。

今回発表する予定だった講演の内容は、何らかの形でみなさまにお届けできるように検討を始めています。みなさまのご理解を頂けますよう、何卒よろしくお願いいたします。

クックパッド株式会社 執行役 CTO 成田 一生

【開催レポ】ユーザー体験を生み出したい学生エンジニア説明会 〜グッドパッチ×クックパッド〜

$
0
0

こんにちは。事業開発部の黒田です。2020年2月7日にユーザー体験を生み出したい学生エンジニア説明会 〜グッドパッチ×クックパッド〜を開催しました。

thumbnail

サービス開発やユーザー体験に興味のある学生の方々に向けて、グッドパッチとクックパッドのエンジニアがサービス開発の真髄や面白さについてお話させていただきました。

クックパッドからは黒田重田百瀬小室が登壇し、クックパッドのエンジニアが日々どんなことを考えてサービス開発をしているのかを中心に発表しました。

また、グッドパッチからは石井さん中谷さん榎本さんに登壇いただき、グッドパッチのエンジニア&デザイナーが日々どのようにしてユーザー体験を作り上げているのかを中心に発表していただきました。

発表プログラム

Cookpad

黒田 健太「サービス開発を1年間経験して感じたこと」

黒田からはサービス開発に対して、学生時代に感じていたことと新卒エンジニアとして入社し、実際に企業で経験して感じていることとの差分について話しました。

重田 桂誓「エンジニアリングを軸にデザインもやる、UXエンジニアという働き方」

重田からはUXエンジニアとしての働き方を中心に、どのようにしてサービス開発をしているのか、ユーザー体験を作り上げているのかを発表しました。

百瀬 凌也 & 小室 直 「コーディングテスト解説」

百瀬と小室からは実際の過去のコーディングテストを題材に新卒エンジニア・選考官それぞれの立場から「普段レビューどんなところをみているか」、「コードを書くときにどんなことを意識しているか」、「面接時の会話のポイント」などを解説しました。実際の業務の中でどういったことを意識してコーディングしているかなどは特に私自身にとってもためになることが多い解説でした。

Goodpatch

石井 大雅「Goodpatchの自社プロダクト開発」

石井さんからはGoodpatchに入社して得られる経験を中心にどのように自社プロダクトが開発されているのかについて発表していただきました。

中谷 文彦「Goodpatchのエンジニア組織と開発プロセス」

中谷さんからはどのようなデザインプロセスでプロダクトが生み出されていくのかをご紹介いただきました。

榎本 直「デザイナー目線で考えるエンジニアとの働き方」

榎本さんからはエンジニアとデザイナーがどのように協働しているのか、具体例をもとに発表していただきました。

Q&Aセッション

最後に登壇者を交えてQ&Aセッションを行いました。「サービス開発者としての課題との向き合い方」を中心にたくさんの質問をいただきました。登壇者間でもサービス開発に対する考え方が微妙に異なっていて、面白いディスカッションをすることができました。

最後に

Q&Aセッションや懇親会で学生の方々からサービス開発やユーザー体験設計に関する質問を数多くされ、自分にとっても再度サービス開発について考え直す良い機会になりました。

また、クックパッドではサービス開発や体験設計をしていきたいエンジニアを随時募集しています。ご興味を持って頂けた方のご応募をお待ちしています。

新卒採用: https://info.cookpad.com/careers/new-graduates/

キャリア採用: https://info.cookpad.com/careers/jobs/

広告配信サーバーにおける DynamoDB Accelerator (DAX) 活用事例の紹介

$
0
0

メディアプロダクト開発部マーケティングサービス開発グループの我妻謙樹です。クックパッドにおける広告開発システム全般の新規開発・保守・運用を担当しています。

マーケティング事業全般やチーム体制については、前回の記事でご紹介しました。こちらを読んで頂ければ、メディアプロダクト事業部をめぐる組織体制や、マーケティングサービス開発グループの技術スタックについて概要を掴んでいただけると思います。

今回は、その記事でも触れた広告配信サーバーの技術的な取り組みについてご紹介します。その中でも特に、Amazon DynamoDB Accelerator (DAX)の活用に焦点を絞ってお伝えします。

背景

従来、広告をアプリ側で表示させるためには、マーケティングサービス開発グループがオーナーとして開発している広告 SDK を、クックパッド本体アプリに組み込み、非同期に広告配信サーバーにリクエストを行うことで実現していました。

今回、iOS アプリにて大きな仕様変更が行われることになりました。その新しいバージョンでは、"モダンBFFを活用した既存APIサーバーの再構築"で紹介されている、Orcha と呼ばれる BFF Server を通じて、必要なレスポンスをクライアントに返すことが決まっていました。そこで、広告を表示させるにあたって、従来のように非同期で直接広告配信サーバーにリクエストするのではなく、BFF Server としての Orcha の立ち位置を利用し、Orcha から広告配信サーバーを問い合わせるようにできないか、という議論が要件定義フェーズで生じてきました。

f:id:itiskj:20200220122635j:plain
アーキテクチャ概要

広告配信システム概観についてまとめた前回の記事の時点では、広告配信サーバは、iPhone/Android/Web からの HTTP リクエストを直接受け付けることを前提に書かれた Web サーバーでした。したがって、既存の広告配信サーバーに手を加え、gRPC によるサービス間通信を受け付けるような実装拡張を検討しました。

しかしながら、要件定義が進むにつれて、既存の広告配信サーバーの機能拡張がパフォーマンスの観点から実現が難しい、ということが明らかになっていきました。

Orcha から広告を問い合わせる場合、広告配信サーバーの Latency が、ユーザリクエスト全体の Latency に影響します。具体的に言うと、Orhca の Latency が A ms, 広告配信サーバーの Latency が B ms あった場合、全体の Latency は (A+B) ms となります。ですから、広告によって本体のユーザ体験に悪影響を与えないために、Orcha に可能な限り早く広告を返すことが求められました。そこで、広告配信サーバーの SLO を「Latency」、SLI を「p95 で 15ms」を目標としました。

ところが、既存の広告配信サーバーの Latency は、p95 で 100ms, p99 で 120-140ms 前後でした。そして、当初のデータモデルの都合上、どう Scale-up/Scale-out したところで、目標とする SLI/SLO を達成できないことがわかりました。

というのも、既存の広告配信サーバーでは、AWS RDS (MySQL) + Elasticache (memcached) という構成でした。memcached はそれなりに早く、1 回の get command に対して 1-2 ms で返すことができていました。しかし、取り組むべき本質的課題は、既存のデータモデルのリレーションにありました。既存のビジネスロジックでは、広告抽選をするために、構造上 30-40 回、多いときには 50-60 回程度、 memcached に get command を送る必要がありました。すなわち、p95 で 100ms だったとすると、そのうちほぼ半数以上が memcached 部分で占めていることが観測できていました。

この時点で既存の広告配信サーバーでは、目標とする SLI/SLO を達成できないと判断せざるを得ませんでした。

解決策

そこで、以下の順番で広告配信サーバーをリプレースすることを決断しました。

  • refactoring
    • a-1. 過去の技術的負債を極限まで返済し、コード自体を削減
    • a-2. 仕様そのものを見直し、コード自体を削減
  • re-architecture
    • b-1. アクセスパターンを全て洗い出した上で、データモデルを根本から見直す
    • b-2. RDBMS (MySQL) -> NoSQL (DynamoDB) に移行

「a-1」および「a-2」は、調査的リファクタリング(exploratory refactoring)1の一環として、既存のビジネスロジックを理解すること、及び次の re-architecture のフェーズのリスクと作業コストを削減するための前準備として行いました。仕様そのものの要不要をディレクター陣と調整するといった、地道だが必要不可欠な取り組みも行いました。

「b-1」フェーズでは、既存の広告配信サーバーにおける全ての SQL 発行パターン、及び入稿から配信までの一連のデータフローの INPUT/OUTPUT を整理しました。そうして非正規化されたデータモデルを、「b-2」のフェーズに置いて DynamoDB に格納し、広告抽選を行うために 1 回の BatchGetItem を発行するだけで、既存のロジックを実現できるようになりました。

DynamoDB 自体は水平方向でのスケール性に優れ、数 ms でレスポンス結果を返してくれます。しかしながら、広告配信のターゲティング機能における DynamoDB の利用実績より、Latency に不安定性であることがわかっていました。具体的には、Max の Latency が 数百 ms 以上まで跳ねあがることが一日に数回の頻度で発生していました。それだけでなく、アプリケーションの性質上、読み込みの回数が多いため、DynamoDB への直接の操作回数を減らしてインフラコストを抑える必要がありました。更には、将来のサービスの成長に伴って、 容易に Read を scale-out できるようにしておく必要もありました。

以上の理由から、DAX に白羽の矢が立ちました。

DAX とは

DAX については、以下の特徴があげられます。

  • DynamoDB をバックエンドとすることに特化した in-memory cache store。
  • Single-leader 構成。Primary node がすべての Write を受け付ける。Replica nodes が Item を複製する。
  • DynamoDB への書き込みは Write-through, すなわち DynamoDB への書き込みリクエストの結果まで同期的に行う
  • キャッシュ戦略としては Least Recently Used (LRU) の他、Negative cache や TTL など必要最低限は実装されている

その他の詳細については、official developer guideの他、チームに展開した際の以下資料を参考にしてください

結果

結果からいうと、平常時で 5ms 前後でレスポンスを返すことができています。旧広告配信サーバーの平均 100~120ms と比較し、概ね 20x の改善を実現することができました。レイテンシが不安定でスパイクになっているのは、Cache の TTL が切れるタイミングだと判断しています。その場合でも DynamoDB にリクエストをしてせいぜい 10-15ms で返せています。

下記は負荷試験実行時の結果であり、まだ既存の全広告商品をリプレース後のサーバーが捌いているわけではないとはいえ、仕組み的には広告商品が増えても DAX/DynamoDB へのリクエスト数は増えません。したがって、ECS task の CPU/Memory や DAX instance type を、キャッシュに乗せるアイテム数(working set) に応じて正しく設定さえしていれば、この値からおおきくブレない想定です。

f:id:itiskj:20200220122716j:plain
Grafana Dashboard 抜粋

また、完全な移行によってインフラコストも大きくコストダウンできることを見込んでいます。詳細な計算は伏せますが、旧配信サーバーで利用している AWS RDS + AWS ElastiCache (memcached) から DynamoDB + DAX に移行した結果、月間で 100,000 JPY 前後のコスト最適化が実現できます。更に、アプリケーションサーバーも Ruby (Rails) から Go への移行をしているので、ECS Service の Running Tasks 数の削減によるコスト低下も見込んでいます。

コスト最適化の取り組みは、"インフラのコスト最適化の重要性と RI (リザーブドインスタンス) の維持管理におけるクックパッドでの取り組み"にて紹介されているように、SRE チームが技術力を結集して、最適化に必要な基盤の整備や情報提供を行ってくれています。この取組のおかげで、アプリケーション開発者としても要件を達成しつつインフラコストを最適化するための土壌が養われつつあります。

また、DAX Cluster が再起動中であったり、万一疎通不可能であった場合を考慮し、DynamoDB へリクエストを Fallback する仕組みを実装しています。DAX への操作は DynamoDB と透過的であるため、必要以上にアプリケーションコードに複雑性を持ち込むこと無く実現できたのも、DAX を選択下からの特長でしょう。

制約条件

しかしながら、DAX はもちろん銀の弾丸ではありません。癖の強いミドルウェアですので、本番に導入する際は、以下の制約条件を十分に吟味してから検討してください。

  • DynamoDB 以外のデータストアをキャッシュすることは不可能
    • ただし、複数 DynamoDB Table を使うことはできる
  • DAX SDK の品質およびサポート状況の精査
    • 例えば、Ruby の SDK は存在しないため、自前実装が必要
    • 例えば、aws-dax-goには実装されていない API が多数存在する 2
  • Single-leader 構成のため、書き込みワークロードが求められるアプリケーションの場合は性能に注意
    • 許されるなら Write-Aroundと呼ばれる書き込み戦略をとることはできる
  • TTL はすべての Item に共通であり、個別に TTL を設定することはできない
    • 例えば、「この Item は TTL 1min, この Item は TTL 60min」といった柔軟な TTL の設定ができない
  • TTL の変更のために Parameter Group を更新する場合、実行中の instance に適用することができない
    • ダウンタイム無しに適用する場合、別 Cluster を立てた上で徐々にリクエストを切り替え、旧 Cluster を落とす、といった運用が必須

運用・保守

次に、DAX を用いたアプリケーションの可用性を中長期目線で担保するための取り組みについて、以下の観点から紹介します。

  • Monitoring
  • Alerting
  • Runbook
  • Maintenance

Monitoring

DAX の CloudWatch metrics 一覧については Developer Guideから確認できます。そのうち、アプリケーションの性質および事業の優先度から、以下の metrics を優先的に監視することにしています。

Metrics Description
CPUUtilization % of CPU utilization
CacheHitRatio ItemCacheHits / (ItemCacheHits + ItemCacheMisses)
ThrottlingRequestCount # of requests throttled by the node or cluster
FailedRequestCount # of requests that resulted in an error reported
EvictedSize Check whether the working set is increasing or not

Metrics 一覧については、Grafana Dashboard にまとめています。ここでは、DAX に限らずアプリケーションの状態を一覧できるような状態を作っています。デプロイ前後の監視体制時や、障害時の原因切り分けに利用することが目的です。

f:id:itiskj:20200220125257j:plain
Grafana Dashboard DAX 関連パネル

また、策定した SLI/SLO も Grafana Dashboard に表示させています。これによって、コンテキストがわからない新規メンバーでもアプリの正常状態を判断し、中長期的な改善の良し悪しの判断に利用できる状態の達成を目指しています。

その他、github.com/prometheus/client_golang3を利用したアプリケーションの状態も Monitoring しています。DAX 関連でいうと、DAX への問い合わせ時に Goroutine/Channel を利用した実装をしているため、Goroutine の挙動や GC の状態なども同じ Dashboard から閲覧できるようにしています。必要に応じて custom metrics も計測できるので、CloudWatch metrics だけでは測れない項目を監視フローに導入するのも容易です。

f:id:itiskj:20200220122811j:plain
Grafana Dashboard Goroutine 数パネル

Grafana に CloudWatch Metrics を表示させる場合、Dashboard TemplateGrafana Labs > Dashboardに公開したので、そちらを参考にしてください。なお、一点注意としては、Grafana Dashboard 作成時における DAX metrics の補完機能は v6.6.0にて追加されています。

Alerting

Monitoring だけでは、ある日突然 working set が増加しアプリケーションが応答しづらくなったり、Goroutine を利用した実装不具合よる memory leak を発生させたり、といった事象に気づけません。そこで、以下の Alerting を導入しています。

CloudWatch Alarm

以下の 2 つの Metrics について CloudWatch Alarm を設定しています。

  • CPUUtilization
    • CloudWatch Metrics の値をそのまま利用
  • CacheHitRatio

通知については、よくあるパターンですが、以下の経路で開発メンバーの Slack channel に通知しています。

CloudWatch Alarm --> SNS Topic --> AWS Lambda --> Slack

社内では Terraform によって AWS リソースが管理されており、適用のみ一部の権限があるユーザーに絞っている、メンバーであれば誰でも閾値を変更したり、追加・削除したりできる状態を実現できています。

Runbook

チームでは Runbook を活用し、障害発生時でも、オーナーシップを持つ実装者以外でも Scale-up や Scale-out などができるような状態を目指しています。

Alerting 通知の際に Runbook URL を含めることによって、障害発生時において、ビジネス影響の確認から障害対応までの一連のフローを事前に分かる範囲でドキュメント化し、誰でも対応できる状態の達成を目指しています。

Alerting とは、通知の設定をすればいい、というものでは決してありません。アプリケーションの可用性を向上させ、事業の成長に貢献してこそ意味があります。Alerting が通知された後、適切なアクションやアプリケーションの修正を通じて初めて品質が向上するのであり、そこまでセットで考えてチームに導入しないと意味がありません。4

筆者にも、苦い思い出があります。以前、"cookpad storeTV の広告配信を支えるリアルタイムログ集計基盤"でご紹介したとおり、storeTV の広告配信ログ基盤において、ストリーム処理における Best Practices に則り、遅延ログの検出および Alerting の仕組みを構築しました。私がシステム構築後運用を他のメンバーに移譲した後、通知は来るものの、システムの設計思想や背景、アクションプランを適切に引き継ぎできていなかったため、割れ窓となってしまい、見るべきエラーを見落としてしまう、という状況を作ってしまっていました。

その時の反省を活かし、誰でも最低限の運用保守はできるようなチームの文化を築くことを目標に置きました。具体的な手順は勉強会で共有したり、その際のハマりどころを知見として展開すると行った取り組みも合わせて展開しています。更に、一度設定した閾値も、チームの状況、メンバーのスキル、アプリケーションの性質、サービスの成長に伴って柔軟に削除・チューニングできるよう、Metrics の意味の共有と目線合わせも、勉強会などの手段を通じて行っています。

Maintenance

DAX では、メンテナンス時のイベントを SNS Topic に通知させることができます。

When a maintenance event occurs, DAX can notify you using Amazon Simple Notification Service (Amazon SNS). To configure notifications, choose an option from the Topic for SNS notification selector. You can create a new Amazon SNS topic, or use an existing topic. https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DAX.cluster-management.html#DAX.cluster-management.custom-settings

Scale-up や Scale-out、TTL の変更のための Parameter Group 更新による再起動時など、DAX Cluster に何らかの構成変化がある場合、アプリケーションの監視体制に入る必要があります。こちらも CloudWatch Alarm と類似の以下の構成で開発メンバーの Slack channel に通知しています。

DAX Events --> SNS Topic --> AWS Lambda --> Slack

まとめ

以上、広告配信サーバーにおける DAX の活用事例について紹介してきました。

マーケティング領域は、技術的にチャレンジングな課題も多く、かつ事業の売上貢献に直結することが多い、非常にエキサイティングな領域です。また、アドネットワークではなく、自社の事業で専用の配信サーバーとユーザーデータを保持するからこその事業の面白さもあるため、事業開発に興味・関心が高い人にとっても活躍の可能性が大いにある場です。

メディアプロダクト開発部では、一緒に働いてくれるメンバーを募集しています。少しでも興味を持っていただけたら、以下からエントリーをしてください。



  1. 『レガシーソフトウェア改善ガイド』より

  2. d.unImpl()で確認できる関数は現在未実装

  3. https://grafana.com/grafana/dashboards/6671に Prometheus client を利用して取得できる Metrics の Grafana Dashboard テンプレートが存在します

  4. 『入門 監視』3 章にも「監視とは、あるシステムやそのシステムのコンポーネントの振る舞いや出力を観察しチェックし続ける行為である。アラートは、この目的を達成するための1つの方法でしか無いのである」とあるとおり、Alerting という手段自体が目的とならないように意識したい

クックパッドの在宅勤務環境

$
0
0

コーポレートエンジニアリング担当 VP の @kani_bです。 昨今急速に拡大している新型コロナウイルス感染症の感染拡大リスクを鑑みて、従業員や関係者の皆さまの安全確保を目的に、クックパッドでは 2/18 (火) からまずは2週間ほど、国内拠点の全従業員(正社員、契約社員、パート・アルバイト、派遣社員、通常在席の業務委託)を対象に在宅勤務の原則化を実施することになりました。

クックパッド、新型コロナウイルスの拡大防止対策で、全従業員を対象に在宅勤務(Work from Home)を実施 | クックパッド株式会社

この記事では、現在クックパッドでどのような環境づくりのもと、在宅勤務が行われているかをご紹介します。 どの会社の方も同じような状況にあるかと思いますが、「他社ではどうやっているか」の一例として参考にしていただけると嬉しいです。

仕事に利用するシステム

クックパッドでは、業務にインターネットからアクセスできる各種クラウドサービスを多く利用しています。社内ツールもそのほとんどがインターネットからアクセスできるようになっています。 特に日常的に使われているのは以下のようなサービスです。

  • オフィススイート: G Suite
  • コミュニケーション: Slack, Zoom
  • コラボレーション: GitHub.com, GitHub Enterprise
  • Wiki, ドキュメンテーション: Groupad (内製ツール)
  • ワークフロー: ServiceNow
  • 人事・会計: Workday

日常業務の多くのシーンでこれらのシステムを活用しています。また、ここに挙げたツールは、雇用形態や勤務時間・国によらず原則ほぼすべての従業員が利用可能になっています。また業務の多くをこれらを活用する形で設計しているため、例えば特定部署でほとんど利用されていない、といった事象が起きにくくなっています。

環境の調査

在宅勤務の原則化を行うにあたり、オフィスとなる全従業員の自宅について、どういった環境があるかがわからなければ方針を決めることも難しいです。そのため全従業員を対象としたアンケートを Google Forms を使って行いました。具体的には以下のような項目をヒアリングしました。

  • 自宅に安定して接続できるインターネット環境はあるか
  • 遠隔会議のために利用しているマイクはあるか
  • その他、会社からの支援が必要な項目

在宅勤務開始にあたり、「あれもこれもサポートしなければ!」と先に考えてしまうと、実は需要のなかったことに時間を使ってしまうことになりかねません。準備を進めつつ、従業員の状況を把握しておくことが、より効果的な対応のためには重要です。

PC の持ち出し

クックパッドではもともと、個人に割り当てられた PC の持ち出しをほとんどの場合において許可しています 。持ち出しにあたっては、パスワードの設定などはもちろんのこと、内蔵記憶領域の暗号化など必要な設定が必ず行われるようになっています。 MDM (Mobile Device Management) として Microsoft Intune や Jamf Pro を利用し、管理を行っています。

PC 上のイベントは EDR (Endpoint Detection and Response) 製品を利用して監視やイベント対応を行っています。現在は主に CrowdStrike Falcon を利用しています。 また、特に機微な情報に触れるようなケースでは、VDI (仮想デスクトップ環境) として Amazon WorkSpaces を利用し、クライアント PC から接続して利用するようにしています。

備品の持ち出し

日頃から希望者にはマウスやキーボード、プライバシーフィルターなどの備品を (可能な限り希望に沿うものを) 貸し出しており、それらの社外持ち出しに特に制限はありませんでした。 備品の中でも特に大きいものとして、PC 用ディスプレイがあります。個人差はありますが、ディスプレイの有無によって作業効率が大きく変わることがあります。 今回は、「オフィスが主、自宅が副」の状態から、「自宅が主」に変わるということもあり、ディスプレイの持ち出しについても許可することにしました。

在宅勤務の原則化が決定されたのち、全従業員対象のアンケートで「ディスプレイの持ち出しを希望するか」を集計し、数を見積もりました。その後、破損防止のための梱包材を急遽用意し、各自で自宅にディスプレイを送付できるようにしました。

インターネット環境

スマートフォンでできることが増え、通信速度も年々向上している昨今、自宅にいわゆる固定のインターネット環境を持たない方もいます。最初に行った環境調査により人数の見積もりができたため、そういった方には会社からモバイルルーターの貸し出しを行いました。モバイルルーターの在庫も大量には持ち合わせていないため、テザリングの利用を前提に、会社で貸与できる携帯電話も活用しています。

現在のところ、モバイルルーターを利用している従業員の多くは問題なく業務できているようですが、高解像度の画像や動画を扱う業務が多いと厳しいという声も聞こえており、契約データ量については注意が必要です。クックパッドでも対応を検討しています。

VPN の利用

クックパッドでは以前より、社内ネットワークなどいわゆる境界を用いたセキュリティ対策の排除を進めていました。*1

ほぼすべての社内システムは Azure AD, もしくは G Suite アカウントを用いた SSO環境下にあり、 HTTPS を必須化するなどインターネットから直接アクセスされることを前提に社内システムを構築しています。またほぼすべての認証 (エンジニアが利用する SSH なども含む) では 2FA が必須化されています。

現状では、利用するソフトウェアの制約により Google Drive などに移行されていないファイルサーバ、開発時に利用するデータベースなど、主に Web ではないトラフィックについて VPN を必要とする箇所が若干残っています。今回の在宅勤務の原則化に伴い、既存の VPN 環境のキャパシティ不足が懸念されたため、一次対応としてエンジニアには sshuttleを利用した VPN への切り替えをアナウンスしました。上記のような取り組みにより、キャパシティ不足に陥ることなく問題なく稼働しています。

遠隔会議

クックパッドでは遠隔会議のためのツールとして Zoomのライセンスを全従業員に発行し、利用しています。日常的に Zoom を使ったミーティングを行う従業員は多いため、利用そのものに大きな問題はありませんでした。 しかし、遠隔会議ではマイクを含めた音声品質が非常に重要です。オフィスではほぼすべての会議室に Zoom Rooms と専用ハードウェアを導入しているため、非常に快適な音声環境が得られますが、自宅ではそううまくいきません。

PC 内蔵のマイクでは、キータイプ音が混じったり音をうまく拾わなかったりなど、長時間の会議には不向きなことがわかっていました。特に、イヤホン・ヘッドホンなど声を聴くためのデバイスは分離しておいたほうが良いようです。 そのため、外付けマイクやヘッドホンをお持ちでない方には以下のような対応を推奨しています。

  • iPhone やスマートフォンに付属する純正のイヤホンマイクの利用
    • ミニピンジャックのものは Windows や MacBook でも動作する
  • スマートフォンやタブレットからの参加
    • PC は別途参加させ、画面共有などに利用する

Zoom ではバーチャル背景機能がよく使われています。これは、部屋の中などを映したくない際に背景を任意の画像に合成してくれる機能です。最近は動画を使えるようになっており、ミーティング開始時のウケを狙いたい人などによく使われています。

また、Zoom による会議がデフォルトになったことによって、会議のためのツールもスライドではなく Google Docs によるドキュメント共有が使われるなど、変化が起きています。これにより非同期に質問を書き込めるようになったり、Google Slides にふせんのようなテンプレートを用意したりするなど、会議の実施方法が進化しておりそれが便利、という声もありました。

社外の方への参加依頼

これまでオフィスで行っていたお客様との打ち合わせや、採用面接についても、Zoom を使った形への切り替えをお願いしています。 今回の目的を達成するためには、従業員だけでなく社外のお客様にご協力いただくことが必要不可欠です。特に遠隔会議においては、環境のセットアップなどでご負担をお願いすることも多いため、お客様向けの簡単なご案内を作成しお送りする試みをはじめています。

ログ収集と運用

在宅勤務を可能にする上で、ほとんどの会社がまず気にかけるのはシステムのセキュリティでしょう。クックパッドでは、これまで紹介したセキュリティに関する仕組みの多くについて、そのログ収集と監視をセキュリティ設計の上での重要事項としています。

現在、ログはすべてセキュリティログ検索基盤に集約され、疑わしいイベントなどが発生した場合に調査できるようになっています。詳しい実装については以下の記事をご覧ください。 Amazon Athena を使ったセキュリティログ検索基盤の構築 - クックパッド開発者ブログ

で、実際どう?

現在の体制となって4日が経過しました。現在のところ、ほぼ全員が在宅勤務をしています。 コーポレートエンジニアリング部では、システム利用で困った際のヘルプデスク(サービスデスク)も担当していますが、この業務のほとんどもリモート環境で行われています。

今回の措置が発表された直後から、社内では #remote-work_ideas というチャンネルが Slack に作られ、椅子やマイクなどの情報交換のほかに、様々な試みが社内で行われています。私が見かけたものをいくつか紹介します。

入退室自由な雑談部屋

一人で黙々と作業をしていると、人に会うこともなく寂しさや不安を感じる方も多いようです。そうした人向けに Zoom ミーティングを作成し、誰でも入れるようにしています。オフィスでもなんとなく起きる程度の雑談を Zoom 内に再現する試みです。

現在では色々な単位で部屋が存在し、部署内で実施されているものから部署関係なく「なんとなく」で人が集まっているものまで様々です。実際の様子はこんな感じ。

f:id:kani_b:20200221123137p:plain

会社の受付にいそうな CTO をはじめ、南国やアメリカにいそうな人たちはバーチャル背景を利用しているだけで、自宅から接続しています。 参加者からは、「自宅だと全く人の目がないのでだらけがちだが、Zoom に接続しておくことで一定の緊張感が保てる点が好き」といった声もあがっています。

リモート昼ごはん

クックパッドのオフィスには大きなキッチンがあり、各自で料理をすることができるようになっています。が、お昼時はどうしても混み合いますし、打ち合わせなどで移動もあり、作れないことも…

原則在宅勤務となったことで、それぞれの自宅キッチンで料理する機会が図らずも生まれています。この機会を活かして、自宅でクックパッドを使って料理をする社員が増えています。Slack に #リモート昼ごはん というチャンネルが生まれ、今は Instagram や Twitter に投稿する社員もいるようです。 #クックパッド社員のお昼ごはん などで覗いてみてください。

f:id:kani_b:20200221123232p:plain

ラジオ体操

在宅勤務では、打ち合わせなどで移動する必要のない反面、意識して体を動かさないといつのまにかつらい状態になっているという声がよく上がっています。 リングフィットアドベンチャーをやっている様子を配信している人や、チームで時間をとって Zoom で中継しながらラジオ体操をやる部署などが出てきているようです。


まだ4日ではありますが、各自・各チームが様々なアイデアをもって仕事を進めています。在宅勤務の実施にあたっては、生産性など様々な議論が行われていますが、我々も継続的な効果測定や改善を行っていきたいと考えています。 すでに実施している方、これから実施される方、検討中の方などにお役に立てば幸いです。

質問などありましたら @kani_bまでお気軽に。

*1:昨今では、いわゆるゼロトラストネットワーク、BeyondCorp という形で浸透している概念に近いものです

「このレシピは何人分?」を機械学習で推定する

$
0
0

研究開発部の原島です。在宅勤務中は部のメンバーと 3 時にラジオ体操をしています。今日はラジオ体操の話はおいといてレシピの分量の話をします。

1 人分、2 個分、三枚分、約 4 皿、5 杯くらい、18 cm タルト型、...

クックパッドの一部のレシピは 1 人分のカロリーが計算されています。計算されたカロリーは検索結果の絞り込みや献立の作成などに使用されています。

ここでポイントとなるのは「1 人分」というところです。

レシピには、下図のように、その分量が記入されています。クックパッドの全レシピのうち、大体 50% のレシピの分量は「N 人分」という表記です。これらのレシピは、レシピ全体のカロリーを N で割ることで、1 人分のカロリーが計算できます。

f:id:jharashima:20200225174134p:plain
レシピの分量

一方、残りの 50% のレシピの分量は「N 人分」という表記ではありません。その半分(全体の 25%)はそもそも表記がありません。これは単に分量の記入が任意のためです。では、残りの 25% はどうなっているのでしょうか?

答えは様々です。「M 個分」や「M 枚分」、「M 皿分」のように「人」ではない助数詞による表記もあれば、「M cm タルト型」や「直径 M cm シフォン型」、「M cm 丸形 L 個」のように何らかの型の大きさとその個数による表記もあります。

こういった分量を「N 人分」に換算するにはどうすれば良いのでしょうか。同じ「M 個分」でもマフィンのレシピとケーキのレシピで N は違うでしょう。また、M は全角の時も半角の時も漢数字の時もあります。接頭辞や接尾辞も付いたり付かなかったりします。さて、どうすれば良いのでしょうか。

人手で頑張る

やっぱりまずはこれでしょう。人間の能力はすごいです。どんな表記でもちゃんとパースして、常識的な N に換算することができます。悩んだ時もレシピのタイトルや完成写真、各材料の分量などを参考にして、違和感のない N を選択することができます。

実際、1 日に十数レシピ(1 年で数千レシピ)の分量が人手で「N 人分」に換算されています。これは、配信記事で取り上げるレシピのカロリーを計算する過程などで換算されるものです。換算の際は、まず、一人のアノテーターが仮の N を決定します。そして、別のアノテーターがチェックして、最終的な N を決定します。

なお、チェック時のアノテーター間の一致率は大体 67% です。また、残りの 33% もそれほど大きな違いがないケース(e.g., 一人が N = 3、もう一人が N = 4)が多いです。これは、そもそも N の候補が少ないことが幸いしています。クックパッドのレシピは家庭用のものが多いので、大体 75% のレシピで N は 1 〜 4 です。

このように、できるのであれば、人手で換算するのが一番です。ただ、表記が「N 人分」でない(かつ、表記はある)レシピは数十万品あります。これらの分量を全て人手で換算するのはさすがに大変です。1 年で数千レシピを換算する今のペースでは大体 100 年くらいかかりそうです(その間に新しいレシピが投稿されるので、実際はもっとかかりそうです)。

機械学習を試す

そこで、機械学習の出番です。最近はライブラリやマネージドサービスが充実し、マイクロサービス化も促進されたので、機械学習をサービスで利用するハードルがぐっと下がりましたね。もちろんまだハードルはありますが、4 〜 5 年前と比較するとだいぶ楽になりました。機械学習は誰でも利用できる手段になりつつあります。

さて、今回開発したモデルは二つあります。一つ目は下図の single-source model です。このモデルはレシピの分量(もしくは、レシピのタイトル)を入力として N を出力します。より具体的な挙動は以下の通りです。

  1. 分量(もしくは、タイトル)をサブワードに分割
  2. 分割されたサブワードをエンコーダーに入力
  3. エンコーダーの結果を全結合層に入力
  4. softmax でいずれかの N(後述する実験では 1 〜 20)に分類

f:id:jharashima:20200225175932p:plain
single-source model

二つ目は下図の multi-source model です。このモデルは分量とタイトルの両方を入力として N を出力します。より具体的な挙動は以下の通りです。入力が複数になったことと、エンコーダーの出力を concat すること以外は single-source model と同じです。

  1. 分量とタイトルをそれぞれサブワードに分割
  2. 分割されたサブワードを各エンコーダーに入力
  3. エンコーダーの出力を concat して全結合層に入力
  4. softmax でいずれかの N に分類

f:id:jharashima:20200225174010p:plain
multi-source model

かなりシンプルなモデルではないでしょうか?単に、分量かタイトル(もしくは、分量とタイトル)の情報から N を推定するというだけです。表記のパースは最初から諦めて、サブワードに分割してニューラルネットワークに突っ込んでいます。

補足することがあれば、回帰問題でなく、分類問題としていることくらいでしょうか。これは、タルトやケーキなどのレシピにおける N が 8 や 6、4 のことが多かったからです。最初は回帰問題としていたのですが、N を連続値とするより離散値とするメリットの方が大きそうでした。

正解率は?

さて、このシンプルなモデルがどこまで通用するのでしょうか。同僚の @himktにも手伝ってもらって、実験してみました。

実験には、分量表記が「N 人分」でない(かつ、表記がある)5,279 品のレシピを使用しました。これらを人手で「N 人分」に換算し、その 80% を訓練データに、10% を開発データに、10% をテストデータに使用しました。

モデルの設定は以下の通りです。

  • 埋め込み層: 20 次元
  • エンコーダー: 20 次元の LSTM(2 層)
  • 全結合層
    • single-source model: 20 次元(2 層)
    • multi-source model: 40 次元と 20 次元(それぞれ 1 層)

次元数などはいずれも開発データで調整しました。また、サブワードの分割には sentencepiece を、その学習にはクックパッドの 310 万品のレシピを使用しました。

タイトル 分量 正解率
RE47%
ML (single)28%
ML (single)63%
ML (multi)62%

結果は上表の通りです。RE(regular expression)はベースラインで、正規表現にもとづいて N を決定しました。具体的には、「M 個分」や「M 枚分」から M を抽出し、そのまま N としました。一方、ML(machine learning)は提案手法で、N は 1 〜 20 としました。また、正解率は初期値が異なる 5 回の平均値です。

表を見ると、分量の情報を使用したモデルの正解率は 62 〜 63% でした。アノテーター間の初見の一致率が大体 67% なので、なかなか良い正解率といえるのではないでしょうか。一方、タイトルの情報だけを使用したモデルの正解率は 28% でした。さすがに分量の情報を使用せずに推定するのは無理がありそうです。

意外だったのは、分量の情報のみを使用した single-source model の正解率が multi-source model の正解率より高かったことです。本質的には、タイトルの情報を使用せずに N を推定するのは不可能です。上でも言及したように、同じ「M 個分」でもタイトルが「マフィン」のレシピと「ケーキ」のレシピで N は違うでしょう。

single-source model が multi-source model に勝利(その差は 1% ですが)した理由はおそらく二つあります。一つ目は分量の情報のみでも、ある程度は、人数分が推定できたことです。ベースラインの正解率が 47% だったことから、47% のデータは「M 個分」などの M がそのまま正解の N だったことが分かります。こういった場合はタイトルの情報が必要なかったのかもしれません。

もう一つは、単に、multi-source model がタイトルの情報をうまく利用できなかった可能性があるということです。ベースラインで対応できなかった 53% のデータではタイトルの情報が有用に思われます。しかし、今回の実験では、二つのエンコーダーを学習させるには訓練データが少なかったのかもしれません。

以上を踏まえて、今は single-source model を試用しつつ、multi-source model を改善しているところです。訓練データを追加していけばどこかで multi-source model が勝利するのではないかなと予想しています。

タイトル分量正解singlemulti
素朴なレーズンパン1 斤888
ツナポテトのミニコロッケ☆お弁当にも8 個分484
甘さ控えめのクッキー鉄板 1 枚分161010

最後に成功例と失敗例を紹介します。一つ目の例は single-source model と multi-source model の両方が正解しました。「1 斤」を「8 人分」と換算したのはなかなか面白いです。二つ目の例は multi-source model だけが正解しました。「ミニコロッケ」は「2 個分」で「1 人分」と学習してくれたのでしょうか。三つ目の例は両方とも不正解でした。この場合、材料欄の情報(e.g., 各材料の分量)も利用しなければ正解するのは難しそうです。この辺りも今後の課題です。

おわりに

まとめると、クックパッドのレシピの約 25% は分量の表記が「N 人分」ではない(かつ、表記はある)ものの、その 60%(全体の 15%)は機械学習で N を推定できます。残りの 40%(全体の 10%)は訓練データを追加するなり、材料欄の情報を利用することで、N を推定できる可能性があります。

モデル自体を改善するのも一つの手です。Transformer や BERT(流行ってますね)を利用するのもありかもしれません。ただ、運用面を考慮するとモデルは軽量なものが良いので、この辺りは悩ましいところでもあります。やっぱり訓練データを追加するのが一番シンプルで、現実的な気もします。

ところで、最近、複数の学生から「新卒採用ってもう始まっていますか」という質問をいただきました。始まっております。機械学習や自然言語処理、画像認識で毎日の料理を楽しみにすることに興味がある方は、是非、以下のページからご応募ください。お待ちしております。

info.cookpad.com


AWS リソース管理の Terraform 移行

$
0
0

技術部 SRE グループの鈴木 (id:eagletmt) です。クックパッドでは Codenize.toolsを用いて様々なリソースをコードで管理してきましたが、現在では大部分が Terraform へと移行しています。Terraform の使い方等については既に沢山のドキュメントや紹介記事があるので本エントリでは触れず、なぜ Terraform へと移行しているのか、どのように Terraform を利用しているのかについて書いていきます。

Terraform 移行の理由

クックパッドでは自分と同じく SRE グループに所属している菅原 (id:winebarrel) によって開発された Codenize.tools のツール群を利用して IAM、Route 53、CloudWatch Alarm、CloudWatch Events 等をコードで管理し、いわゆる GitOps を実践してきました。Codenize.tools による AWS リソース管理は基本的に1アカウント内のすべてのリソースを対象に動作します *1。これに従うと、ある AWS サービスに属する全てのリソースの管理は1つのリポジトリに集約されることになります。実際、社内には cloudwatch というリポジトリや iam というリポジトリが存在します。この構成は1つのチームが1つの AWS アカウント内のすべてのリソースを管理しているような場合は抜け漏れ無くコードで管理できるため非常に有効です。cloudwatch と iam を1つのリポジトリにまとめるか別のリポジトリに分けるかという選択肢はありますが、1つの AWS アカウント内のすべてのリソースを SRE グループが管理していたクックパッドでは自然な構成でした。

しかしマイクロサービス化が進みセルフサービス化が進むと、様々なチームで様々なリソースが必要になり、SRE グループがあらゆる AWS リソースを管理することが困難になっていきました。新しくアプリケーションをデプロイしたい人たちにとっても、複数のリポジトリに別々の pull-request を出す必要があり面倒に感じられていました。また、ELB + ECS/EC2 + RDS という伝統的な構成ではなく AWS SAM (Serverless Application Model) を利用したサーバレスな構成も選択されるようになり、CloudFormation で管理されるリソースも増えていきました。このような状況では Codenize.tools の「1アカウント内のすべてのリソースを対象に動作」という挙動は次第にフィットしなくなっていきました。

そこで、選択的にリソースを管理することができ、多くの現場で利用されている Terraform へと移行する方針になりました。これまでも Codenize.tools の対象外だった Auto Scaling Group や RDS インスタンス等の管理に Terraform は使われていましたが、Codenize.tools の対象でも Terraform を利用するように移行が始まりました。現時点では IAM、Route 53 以外の AWS リソースは一通り Terraform への移行が完了しています。

Terraform 運用

全面的に Terraform へと移行するにあたって、いくつか工夫した点があるのでそれぞれ紹介します。

tfstate の単位

Terraform では管理対象のリソースに関する情報を state ファイル (以下 tfstate と呼ぶ) にまとめているわけですが、Terraform を利用するにあたってこの tfstate をどのような単位で分割するのかという話題があります。1つの tfstate ですべての AWS リソースを管理するのは少なくともクックパッドの規模では無謀で、もしそうしたら terraform plan の時間が非常に長くなってしまいます。 クックパッドでは1つのリポジトリに Terraform ファイルを集約し、その中でプロジェクト単位でディレクトリを分けて記述していくことにしました。1つのディレクトリが1つの tfstate に対応します。

.
├── service-1
│   ├── aws.tf
│   ├── backend.tf
│   └── rds.tf
├── service-2
│   ├── acm.tf
│   ├── aws.tf
│   ├── backend.tf
│   ├── iam.tf
│   ├── rds.tf
│   ├── security_group.tf
│   └── vpc.tf
└── service-3
     ├── acm.tf
     ├── aws.tf
     ├── backend.tf
     ├── elb.tf
     ├── s3.tf
     ├── sg.tf
     └── vpc.tf

1つのリポジトリにしたのは一覧性を確保するためと、後述する CI の整備を楽にするためです。

linter の整備

AWS リソースの追加、削除、変更は Terraform 用のリポジトリへの pull-request で行うわけですが、pull-request に対する CI として terraform plan の結果を表示したり terraform fmt 済みかチェックしたりすることに加えて、独自に用意した linter を適用しています。Terraform 向けの linter というと tflintが既に存在していますが、tflint がカバーしているような一般的なルールではなく、社内独自のルールを強制したかったため自作しました。 ルールとしては現時点では

  • タグ付け可能なリソースには Project タグを必ず設定する
    • クックパッドの AWS アカウントではコスト分配タグとして Project というタグが設定されており、コスト管理のために Project タグを設定しなければならない
  • Aurora MySQL を使うときに特定のエンジンバージョンを禁止
    • クックパッドでの典型的なワークロードで致命的な問題が発生するエンジンバージョンがあるため、そのバージョンの指定を避ける

を強制しています。

ちなみにこの linter を pull-request に対して実行するにあたって、見易さの観点から GitHub の Checks の機能を利用することにしました。linter のように行単位で指摘する箇所が分かる場合、Checks を使うと見易く表示できます。 https://developer.github.com/v3/checks/

remote state の取り扱い

tfstate の保存場所としては S3 を使っていますが、RDS インスタンスの master user のパスワードのようなセンシティブな値の取り扱いを考慮する必要があります。たとえば aws_rds_clusterを新規に作成するとき、master_password に直接パスワードを書くと GitHub リポジトリで社内全体に公開されてしまいます。そこで SSM の Parameter Store に SecureString として保存して aws_ssm_parameterで参照したり、Vault の KV backend に保存して vault_generic_secretで参照したりといった方法を思い付きますが、これにより Terraform ファイル上からはパスワードが消えても tfstate にパスワードが平文で記録されてしまいます。この問題は upstream でも認識されていて tfstate 自体をセンシティブなデータとして扱うことを推奨しています。 https://www.terraform.io/docs/state/sensitive-data.html

しかしながら社内のエンジニアであればどのプロジェクトでも terraform plan は実行できるという状態を目指したかったので、パスワードのようなセンシティブな値は tfstate にはダミーの値を指定するという方針を試してみています。たとえば aws_rds_cluster を新規作成する場合は

resource "aws_rds_cluster" "my-awesome-app" {
  ...
  master_password = "pasuwa-do"
  ...
}

のように記述して terraform apply し、その後 mysql コマンド経由や ModifyDBCluster API で正式なパスワードに変更します。API を通じて master_password を得る手段が無いので Terraform は tfstate にある値を信じるしかなく、tfstate にも Terraform ファイルにも pasuwa-do と書かれているので差分が発生せず、センシティブな値を tfstate にも Terraform ファイルにも書き込まずに Terraform でリソースを管理することができています。

今後

Codenize.tools から Terraform への移行は進んでいるものの、最初の移行時にはプロジェクト単位に分割することを諦めたため、Terraform 管理へと変更はできていても適切なプロジェクト内の tfstate で管理させることはまだ十分にはできていません。現在はたとえば cloudwatch のような tfstate に様々なプロジェクト向けの CloudWatch Alarm が混ざって管理されている状態です。これを分解していくことは地味な作業ではありますが、今後も少しずつプロジェクト単位で管理された状態へと移していこうとしています。

また、多くの現場で実践されていそうな Terraform の自動適用もまだ実践できていません。Terraform 管理への移行や Terraform 管理内での tfstate の変更も徐々に落ち着いていくと思われるので、master にマージされたら自動的に terraform apply される状態を目指したいです。

*1:--exclude や --target で対象を限定できるようになっているものもあります

iOSアプリのメモリリークを発見、改善する技術

$
0
0

こんにちは。事業開発部の岡村 (@iceman5499) です。 普段はクックパッドアプリ(iOS)を開発しています。

先日、アプリケーションが特定の条件で意図せぬ状態に陥り、アプリケーションが重くなって端末が発熱する、というバグが発見されました。 調査の結果、このバグはメモリリークが原因で発生していました。 この反省を踏まえメモリリークを検知するテストを導入したため、本記事ではその事例を紹介したいと思います。

(本記事ではクックパッドアプリとはiOS版の「クックパッド」アプリのことを指すものとします)

クックパッドアプリにおけるメモリリークの影響

クックパッドアプリはレシピの検索をコア機能としています。 検索は重い処理ですがAPIを通してサーバ上で行われるため、アプリは結果を表示するだけです。そのためメモリを多く必要としません。 これまでにも何度かメモリリークが発生している状況はありましたが、メモリを多く必要としないため多少の無駄があってもアプリの動作に影響がありませんでした。

クックパッドアプリで用いられているクラスの大半は自力で動くようなことはせず、RunLoop等のイベントループによって動作します。 UIKitを使用しているとインスタンスのRunLoopからの除去は自然に実現できるため、メモリリークが起こってもそのインスタンスは止まっていて、無害な状態であることが多いです。

しかしながら、今回は不運にも単独でイベントループを発生させるインスタンスがメモリリークしてしまいました。 本来はそのインスタンスがメモリから解放されたタイミングでイベントループが止まるはずでしたが、メモリから解放されなかったことにより停止されないイベントループが永遠に無駄な処理を続けていました。 その結果、そのインスタンスが多数メモリリークしてしまうとアプリケーションの動作に影響するほど負荷がかかってしまいました。

どのようにしてメモリリークが起こってしまうのか

iOSアプリケーション開発の現場で起こるメモリリークは大抵、循環参照が原因となっています。 循環参照によるメモリリークは参照カウント方式のガベージコレクション環境において発生しうる問題で、SwiftやObjective-Cを使っていると起こりうるものです。(なお、ここでは循環参照がどのような状態であるかの説明は省略します。) クックパッドアプリではRxSwiftというライブラリを多用しているため、クロージャを経由してうっかりメモリリークする形の循環参照を引き起こしてしまうケースが多いです。

よくある循環参照の例

実際にクックパッドアプリで起こっていた循環参照の実装の例を紹介します。

自身が所持するObservableのobserverとして自身が所持されるパターン

ちょっとタイトルがややこしいですが、最もシンプルなタイプのものです。

// ViewController.swiftletstream:PublishSubject<Void>letdisposeBag= DisposeBag()
funcbind() {
    stream.subscribe(onNext: {
        self.doSomething() // selfがキャプチャされている
    })
    .disposed(by:disposeBag)
}

RxSwiftでは Observableを購読するとその購読がobserverオブジェクトとして Observableに保持されます。(例の PublishSubjectObservableの一種です。) この例ではobserverはさらに onNext:に渡されているクロージャを保持します。そしてさらにクロージャは selfを保持しています。ここで selfはこの実装を持つ ViewControllerクラスであるとします。 その結果、 self → stream → observer → クロージャ → selfとして循環参照となります。

この循環参照の解決策としては、次のように selfを弱参照でキャプチャする方法が挙げられます。

stream.subscribe(onNext: { [weak self] inself?.doSomething()
})

暗黙クロージャ渡しパターン

次の例はどうでしょうか。これもたまにある例で、気づくことが難しい循環参照です。

// ViewController.swiftletstream:PublishSubject<Int>letdisposeBag= DisposeBag()
funcbind() {
    stream.subscribe(onNext:doSomething) // この場合もselfが強参照されてしまう
        .disposed(by:disposeBag)
}

funcdoSomething(_ value:Int) { ... }

onNext:にクロージャを使わずに関数を渡すことで、すっきりとした表記になっています。 ところがこの doSomething、一見関数ポインタを渡しているように見えますが、実際はコンパイラが裏側で selfをキャプチャしたクロージャを生成しているため、次のコードと同じ意味になっています。

stream.subscribe(onNext: { self.doSomething($0) })

これは先程と同じパターンの循環参照となります。 対処法としては先程と同じように [weak self]を用いるのが良いでしょう。 あるいは、 doSomethingの処理が selfに依存していないならばそれをstatic関数にしてしまうという手もあります。(static関数になった場合その関数の実行に selfが必要なくなるため、上で紹介したコンパイラが裏側でやる処理が無くなります。)

funcbind() {
    stream.subscribe(onNext:Self.doSomething)
        .disposed(by:disposeBag)
}

staticfuncdoSomething(_ value:Int) { ... }

複数クラスにまたがって循環しているパターン

// Presenter.swiftclassPresenter {
    letvalue:Observable<Int>init(view:View, interactor:Interactor) {
    value = interactor.stream
        .filter { view.isXXX } // ここで view を強参照でキャプチャしている
        .map { ... }
    }
}

// View.swiftclassViewController:View {
    varpresenter:Presenter!varisXXX:Bool {
        ...
    }
    letdisposeBag= DisposeBag()

    funcbind(presenter:Presenter) {
        presenter.value.subscribe(...)
            .disposed(by:disposeBag)
        ...self.presenter = presenter
    }
}

これは複数のクラスにまたがる例です。 Presenterが生成する valueには viewがキャプチャされており、その川を viewである ViewControllerクラスが購読します。 つまり ViewControllerからみて、 self → presenter → value → filterの内部で使われるオブジェクト → クロージャ → view(== self)として参照関係が発生し、循環参照が成立します。

selfがクロージャにキャプチャされている場合はなんとなくアンテナが反応しやすいのですが、そうでないケースはうっかり見過ごされやすいです。 これの対応も同様に弱参照を用いることになります。

.filter { [weak view] _ in view?.isXXX ==true }

メモリリークを検知するテストの導入

「よくある循環参照の例」を見てわかるように、循環参照はうっかり見逃しやすいため人目のレビューをすり抜けてしまいます。 またコンパイラによって検知することもできません。

そこで、XCTAssertNoLeakを使ってテストを書くことにしました。

github.com

XCTAssertNoLeakは対象のインスタンス内でメモリリークが発生しているかを検知する機能を提供するテスト用ライブラリです。 2019年のtry!Swiftで発表されたライブラリで、メモリリークを検知するテストを書くことができます。

f:id:iceman5499:20200303111804p:plain
XCTAssertNoLeak

https://github.com/tarunon/XCTAssertNoLeakのREADME.mdより

ただ引数にオブジェクトを渡すだけで、簡単にリークしているオブジェクトをリストしてくれる素敵なライブラリです。

XCTAssertNoLeakの動作原理

XCTAssertNoLeakはどのようにしてメモリリークを検知しているのでしょうか。 基本的な戦略としては、インスタンスをweakポインタに格納し、スコープが変化したタイミングでポインタの中身が nilになっているかどうかで判定をしています。

インスタンスが持つプロパティ群に格納された子インスタンスや、そのさらに孫インスタンスを全て確保するためには Mirrorが用いられています。

Mirrorを使い全プロパティを探索して参照型の値を全てweakポインタに確保し、スコープを抜けたあとそのweakポインタがちゃんと nilになっているか確認する、という感じです。

テスト記述に関する注意点

XCTAssertNoLeakは非常に簡単に利用できるようになっていますが、仕組み上いくつか気をつけないといけない部分があります。

ローカル変数のスコープに注意する

XCTAssertNoLeakは引数に渡したオブジェクトが検査されますが、ローカル変数にオブジェクトを保持してしまうとそのローカル変数が参照を持つためにテストに用いられるweakポインタが nilにならず、メモリリーク扱いになってしまいます。

// NGletviewController= MyViewController()
XCTAssertNoLeak(viewController) // faild! 

回避するためには XCTAssertNoLeakの引数にオブジェクトを右辺値として渡す必要があります。 クックパッドアプリでは次のようにしてテストを記述しています。

funcbuild() ->AnyObject {
    RecipeDetailsViewBuilder.build(....) // 初期化処理
}
XCTAssertNoLeak(build())

初期化処理をローカル関数にラップし、返り値をそのまま XCTAssertNoLeakに放り込むことでローカル変数にテスト対象のインスタンスを保持しないようにしています。

シングルトンは例外設定

次のテストを見てみましょう。 f:id:iceman5499:20200303111725p:plain

NotificationCenterがリークしていると怒られています。 シングルトンは開放されないため、XCTAssertNoLeakから見るとメモリリークしているものとして判定されます。 このような状況に対応するために CustomTraversableというプロトコルが用意されています。

extensionNotificationCenter:CustomTraversable {
    varignoreAssertion:Bool { true }
}

メモリリークしていると判定されるクラスに対してextensionで ignoreAssertion: Boolを実装することで、そのエラーを無視することができます。 CustomTraversableにはこの手のケースに対応するための口がいくつか用意されています。

シングルトンが保持するオブジェクト

シングルトンを無視設定するところまでは良かったですが、 ignoreAssertionするだけではそのオブジェクトに連なっているオブジェクトがさらにリーク判定されてしまいます。( ignoreAssertionはそのインスタンスの子プロパティ群ごと無視はしません)

classAwesomeObject {}

classMySingleton {
    staticletshared= MySingleton()
    privateletobject= AwesomeObject()
}

extensionMySingleton:CustomTraversable {
    varignoreAssertion:Bool { true }
}

classMyViewController:UIViewController {
    letobject= AwesomeObject()
    letdependency= MySingleton.shared
}

classNoLeakTests:XCTestCase {
    functestMyViewController() {
        // failed - 1 object occured memory leak.//        - self.dependency.object
        XCTAssertNoLeak(MyViewController())
    }
}

このコードの例の場合、 self.dependency.objectがリークしたという判定になります。 あまりこのようなケースはありませんが、現実のアプリケーションは複雑であるためまれにこのケースに遭遇します。

これの対応を考えることは難しいです。例えば次のようにシングルトンに持たれた AwesomeObjectのみ例外設定することを考えます。

extensionAwesomeObject {
    varignoreAssertion:Bool {
        self=== MySingleton.shared.object // コンパイルエラー: 'object' is inaccessible due to 'private' protection level
    }
}

このように書きたいところですが、アクセス修飾子の関係でこの処理は記述することができません。 対象のオブジェクトがアプリケーション内に実装されていればやりようはあるかもしれませんが、外部のライブラリなどとなると難しくなってきます。

クックパッドアプリではこのケースは諦めて AwesomeObjectignoreAssertionは常に trueを返すようにしています。

まとめ

XCTAssertNoLeakのおかげで、メモリリークを検知するテストを実現することができました。 このテストを実装してすぐ、僕の変更で循環参照を引き起こしてしまいテストに怒られてしまったので早速効果を発揮しました。 テストを導入する過程で見つかったメモリリークもいくつかあり、今まで見つかってなかったメモリリークも浮き彫りにすることができました。

このようにしてiOSアプリのメモリリークは解消しましたが、モバイルアプリの品質安定にはまだまだ手が足りていない状況です。 クックパッドではモバイルアプリの品質を安定させたいiOS/Androidエンジニアを募集しています。 https://info.cookpad.com/careers/

スプリングインターンシップをオンラインで開催致します!

$
0
0

こんにちは、レシピ事業開発部の赤松( @ukstudio )です。

毎年恒例となっている春のインターンシップですが、今年も開催致します!新型コロナウィルスの影響をふまえ、オンラインで実施することに致しました。当日はお手持ちのマシンからZoomで参加頂く予定です。

大規模トラフィックを支える技術

今年はエンジニア向けに1コース用意しました。題しまして「春ダッシュスペシャル 大規模トラフィックをさばくアプリケーションのパフォーマンスチューニングを学ぼう!」です!以下の日程・場所で開催致します。

  • 開催日: 4月29日(水・祝) 13時〜17時
  • 開催場所: オンライン(Zoom)

クックパッドは現在74カ国/地域・32言語に展開しており、月間の利用者数も約9,300万人にのぼる大規模サービスです。ユーザーからのアクセスだけでもピーク時には秒間数千アクセスにも達します。

このコースではこのような大規模トラフィックを支えるための技術を実践を通して学ぶことができる内容となっています。大規模トラフィックに関する技術の経験は個人で得るにはなかなか難しい部分もあると思いますので、この機会にぜひ体験してみてください。

以下のインターンシップ特別サイトからご応募頂けます。

internship.cookpad.com

オンラインでの開催について

当日は講師がZoomで画面共有をしながら講義を進めることになります。実はこの形式だと、オフラインの時と比べてスクリーンが見やすい、声が聞きやすい部分もあります。

一方でオンライン開催だと質問がしづらいんじゃないか、講義についていけなかったら置いてけぼりになるんじゃないかという不安があるかもしれません。当日はメインで話す講師とは別にTAも用意しております。TAがSlackでのサポートや、場合によっては1対1でのZoom MTGでサポートします。

実際に既にオンラインでの勉強会やイベントを弊社で行なっていますが、スプリングインターンもオンラインでできる!という手応えを感じています。ぜひオンラインというところに気後れせずに応募して頂けたらなと思います。

デザイナー向け UIデザインとサービス開発

デザイナー向けにも1コース用意しています。こちらは「クックパッド流!UIデザインをFigmaで体験しよう」とFigmaを用いたUIデザインとサービス開発の基礎を体験することができるコースです。デザイナーの方はぜひこちらにお申し込みください。詳細については後日noteの方に記事が公開される予定なので、そちらを見てもらえればと思います。

note.com

  • 開催日: 4月25日(土) 13時〜17時
  • 開催場所: オンライン(Zoom)

応募締切は4月6日

エンジニア向けもデザイナー向けも応募の締め切りは4月6日までとなっております。みなさまのご応募お待ちしております!

internship.cookpad.com

テストケース作成を仕様詳細化の手段とする実験

$
0
0

こんにちは。 テストエンジニアからサービス開発エンジニアにロールチェンジした、茂呂一子です。 先日リリースしました、iOSクックパッドアプリのリニューアルプロジェクトに参加し、サービス開発エンジニアとしての第一歩を踏み出しました。

今回は、アプリのリニューアルをすすめていく中で、試してみたことについて、お話しします。

アプリリニューアルの内容やそのデザイン意図については、13年続いた「つくれぽ」をリニューアルした話|Misaki Kubosaka|noteが詳しいので、こちらをお読みください。

リニューアルプロジェクト第1フェーズの問題点

iOSアプリのリニューアルプロジェクトは、とても大きく、機能を段階的にリリースするため、3つのフェーズに分けて開発していくことが決まっていました。 そのため、開発チームはメンバーの追加をしつつ、複数回の開発サイクル(仕様決定、設計、実装、検証)を繰り返すことになりました。

クックパッドのサービス開発では、主に、ディレクターとデザイナーが企画と仕様決めを行い、エンジニアが実装し、ディレクター/デザイナーが作成するテストケースを元に検証を行うという方法が取られます。 以後の「ディレクター」は、企画と仕様決めに責任を持つディレクターとデザイナーの両方を指します。

私が参加したのは第1フェーズの途中からで、そこではテストケース作成をはじめとした検証を担当しました。 第1フェーズでは、 テスト期間の開始間際まで仕様の整理が行われていたり、不明瞭になっていた箇所への仕様追加がされたりしていました。 どうにか第1フェーズの開発を終え、リリースすることはできましたが、仕様が不明瞭なまま開発をすすめていくことに大きな不安を感じました。

その次の第2フェーズでは、私は開発エンジニアとしてモバイルアプリ開発をすることにしていました。 第1フェーズの反省から、いかに仕様の決定を早期に行うかを考え、仕様の抜けを早く検知する手段を講じる必要がありました。

第1フェーズのすすめ方の問題点はいくつかありました。

  • ディレクターが仕様を決めるが、複雑なユーザー状態すべてを考慮できなかった
  • 影響範囲が大きいため、たくさんの仕様の検討会が行われており、ディレクターが結論を精査する時間がとれなかった
  • どこまで決まっているかの管理をディレクター任せにしてしまったことで、ごく少数の人間だけが仕様を知っている状態が発生した
  • 実装担当は共有された情報から仕様を理解していたが、細かな認識の齟齬があることに後々まで気づけなかった
  • 後々発覚した認識の齟齬を埋めるために、仕様追加がされ、開発スケジュールがずるずると伸びた

仕様を実現していく上で必要な情報共有が不足している、ディレクターから実装者への一方通行であることが問題と考えました。

そこで、仕様の不明瞭な点を実装開始前に明かにする、そのために情報共有の精度をあげる方法を探すことにしました。

テストケースの作成を通じて、仕様を詳細化する

仕様の情報共有の精度をあげる方法を2案考えました。

  1. ディレクターに仕様詳細化をお願いし、その共有を実装担当者とする時間を設ける
    • 第1フェーズでは、やっているつもりだができていない状態だった
    • タイトなスケジュールの中では、実現可能性を考慮してセカンドプランを選択するべき場面があるが、それをディレクターだけでは判断できない
  2. ディレクターと実装担当者が会話した上で、実装担当者が仕様を詳細化する
    • 仕様の検査に確実に2者の視点が入るので、情報共有の不足を低減できる
    • 実装担当者のシリアルタスクのため、実装前に詳細化を完了しやすくなり、その結果、仕様追加の追跡がしやすく無理な変更の抑制ができる

案1は、第1フェーズで結果的にうまくいかなかった方法とあまり差がないこと、また、実装のコストや難易度の反映が遅くなる危険があったため、案2を採用しました。

私が属した機能グループでこの取り組みを行いました。大きく2つの機能を実装するグループです。

新機能に対して、ディレクターと実装担当者間で仕様の共有会を行い、そのインプットを元に実装担当者が仕様を詳細化しました。 このとき、ユーザー状態や利用状況の網羅性をあげるため、分析が網羅的になるようテストケースを作成するという手段をとりました。

テストケースというフォーマット

テストケースは、一般に、状況設定と操作と期待結果の組み合わせを列挙するものです。 とある機能において取り得る状況を網羅するには、テストケースの状況設定を細かく分析できるかが鍵です。 状況設定を細かく分析しやすいと考えたので、テストケースのフォーマットを利用することにしました。

そのとき仕様したフォーマットは以下のものです。

f:id:ichiko_revjune:20200316114550j:plain
テストケースのテンプレート

一部は選択式にしつつ、その他のデータやユーザーの条件は自由設定にしています。 これまでの経験から仕様に現れない操作は忘れられたがちなため、操作は選択式にしました。

例えば、画面の通信エラーが起きたときの振舞いは未定義になりがちなので「通信エラー」、文字入力に関するバリデーションエラーの扱いを考慮してもらうために「文字入力」などを用意しました。 他には、細かな機能の出し分けがあるため、ユーザーステータスも選択式にしました。

仕様詳細化の効果

実装担当者は、テストケース作成を通じて、不明点/検討されていない点をリストアップしました。それをディレクター、デザイナーと共に解消した上で、実装を開始しました。

成果物としたテストケースは、その後ディレクターが加筆して検証フェーズで利用されました。 加筆といっても、この取り組みをしたのは新機能まわりだけだったので、ディレクターが新規に作成した既存機能との関連を見るテストケースの方が圧倒的に多いです。

検証フェーズでは、実装担当者が作成したテストケースの周辺では、ディレクターの追加したテストケースによって、大きな仕様差異が発覚することはありませんでした。

仕様詳細化をする段階で、問題を発見できたことで、第1フェーズに比べて安定した進行でリリースまで漕ぎ着けることができました。

リリース後の振り返りでは、エンジニアからは「実装開始前にテストケースができていたことで、仕様の不明点が洗い出せてよかった」と高評価を得ました。 一方、ディレクター陣からは、エンジニアが作ったテストケースにディレクターが手を入れるという形をとったため、検証の信頼性が低いので今後はディレクターがテストケースを作る、という評価を得ました。

実装担当者がテストケースを作成する是非

一般的に、実装担当者が作成するテストケースでは見つけられない不具合が多い、という信頼性の低さがあります。 テストケースが先でも、実装が先でも、先に考えた理解の範囲に引きずられて、網羅性が上がらないことは想像しやすいと思います。

ディレクター陣からの評価が下がった原因も、「実装担当者が作成したテストケースを検証に使った」ことにあると考えられます。

詳細化の分析手段としてテストケース作成のフォーマットに載せたことで、仕様の検証精度は上がった可能性はありました。 実装担当者からの反応はよかったので、仕様を理解する、過不足なく機能を実現することには貢献したと考えられます。

しかし、その成果物を流用させてしまったことで、 検証能力が低いテストケースであるために不具合件数が減ったのか、 実装精度が高いことで不具合件数が減ったのか、の区別ができなくなっていました。

私が検証のためのテストケース作成と完全に分離せず、成果物の流用を許容してしまったことで、有用性の評価が十分にできなかったのは残念でした。 あくまで詳細化の過程の成果物であり、検証用途ではないとするべきでした。

この手の信頼性問題は、コンセンサスを得ていない手段を使ったことで結果が下がった可能性をゼロにできなければ、マイナスに取られる他ないので致し方ないと思っています。

反省点はありますが、「仕様を考えている人以外も混じえて、実装開始前に仕様を明かにする」ことで、スムーズに開発をすすめることはできました。

次回は、成果物が一人歩きしても問題ないよう、用途を制限することと、誤解の生じる利用のされ方を回避することが必要になるでしょう。 分析の助けになることが重要のため、成果物の形をテストケース以外にできると、外部からの誤解も減らせると思えるので、他の形を模索したいと思います。

クックパッドではモバイルアプリの品質を安定させたいiOS/Androidエンジニアを募集しています。 https://info.cookpad.com/careers/

サーバーレスで作るセキュリティアラート自動対応フレームワーク

$
0
0

技術部セキュリティグループの水谷 ( @m_mizutani ) です。ここしばらくはフルリモートワーク体制になったので運動不足解消のためウォーキングをしたり筋トレしていたら、リモートワーク前より健康になった疑惑があります。

クックパッドのセキュリティチームでは日々のセキュリティ監視を効率化するため、独自のフレームワークを構築して利用しています。具体的には、セキュリティアラートが発生した際に自動的に様々なデータソースから関連情報を収集し、収集した情報をもとにアラートのリスクを評価、そして評価結果をもとに自動対応をするという一連のワークフローを実現するフレームワーク DeepAlertをAWS上にサーバーレスで構築しました。この記事では、このフレームワークを構築した経緯やアーキテクチャ、仕組みについて解説します。

セキュリティアラートの対応

ここでは、セキュリティ侵害が発生している可能性があるものについて管理者に対応を求めるようなメッセージをセキュリティアラートと呼んでいます。これはセキュリティ防御・監視装置(Firewall、IDS/IPS、WAF、AV、EDR、などなど)から直接アラートとして発せられることもありますし、ログの中から見つかった不審な活動や外部からの連絡によって発覚する事象など、発生の経緯は様々です。共通しているのは組織内でセキュリティ上の問題が発生している可能性があり、状況に応じてなんらかの対応が必要である、ということです。今回は特にセキュリティ防御・監視装置から発報されるものを中心に説明します。

セキュリティアラートは組織内のセキュリティの侵害の可能性を見つけてくれる便利な情報ではありますが、実際に脅威ではないものを発報してしまうケースが多々あります。これは主に次の2つの理由が挙げられます。

  • 防御・監視システムは見える範囲に限りがあり、別システム(特に社内システムなど)の情報とつきあわせないと判断できないこともセキュリティアラートとして発報してしまう
  • 防御・監視システムは一般化されているため、それぞれの会社や事業部ごとに特有の業務や文脈に対応しきれない場合がある

これによっていわゆる誤検知・過検知が発生してしまうため、担当するセキュリティエンジニアが都度調査・分析してアラートの実際の深刻度を判断する必要があります。この作業は組織のセキュリティを維持するための大切な業務ですが、日々持続的に発生するために他の業務を徐々に圧迫してしまい、件数が多くなることで担当者を疲弊させてしまいます。

通常、防御・監視システムにはホワイトリスト機能が備わっており、指定した条件に一致するアラートは発報しないよう設定ができます。しかし、実際には先述した通り他のシステムの情報と突き合わせないと判断が難しいケースやホワイトリスト機能では除外条件を表現しきれないケースがあり、容易に対応できるものでもありません。

まとめると、実際のセキュリティアラートの対応では、まずアラートの調査と分析(影響有無の判断)があり、その後で必要に応じて何らかのアクションをする、といった流れになります。この一連の作業を効率化するために、自律的に複数の情報を組み合わせて必要な対応をしてくれる仕組みを作ることにしました。

設計方針

セキュリティの対応をコード化する

この仕組を作るにあたっては対応の部分をなるべくコード化する、ということを指針として取り組みました。近年、サービス開発の分野ではインフラの管理にDevOps や Infrastructure as Code の概念によってリソースの管理や手続きをコード化する、という取り組みが盛んになっていると思います。これはセキュリティにも応用できる考え方であり、コード化することによって対応の自動化だけでなく、対応内容の明確化や変更履歴の管理、手続きに対してテストができるようになる、といった恩恵を受けることができます。

セキュリティアラートの対応はその組織の構成、活動内容、文化や発生時の文脈に依存するも多くあり、全てのアラートを一律に自動化して対応できるものではありません。しかし、多くのアラートについては決められた手順で関連する情報を調査し、得られた情報をもとに定められた基準に従ってリスクを評価することができると考えられます。自分が前職でSOC(Security Operation Center)に勤めていたときも、明文化こそされていなかったものの定型化された対応は多く存在し、かつアナリストの間で共有されていました*1。もちろん、定型的に判断しきれないアラートや新しい脅威に対してはセキュリティエンジニアによるきめ細やかな分析が必要になりますが、そうでないものはなるべく自動化することで、エンジニアがより本質的な作業に注力できるようになります。

そのため、このフレームワークを作るにあたっては「誰がやっても同じ結果になるものは人手を介さないようにする」という機能の実現を目指し、全体をコード化するという方針で設計しました。

その他の機能要件

セキュリティのコード化以外にも、次のような要件を考慮して設計しました。

  • 容易な機能拡張:関連する情報の検索や最終的な対応は、様々なデータソースやインターフェイスに対応する必要があります。また、状況に応じて機能を追加・削除していくと考えられるため、なるべく自動対応のメインのシステムとは疎結合になるようにするべきと考えました。
  • 低コスト運用:「横断的に複数のデータソースを使ってアラートの精度を上げる」といったアプローチは新しいものではなく、昔からSIEM(Security Information & Event Manager)でも同じような取り組みがされていました。しかし、多くのSIEMはリアルタイムにイベントを処理するような設計となっているため高い処理能力が求められ、高価になってしまう傾向があります。もちろんお金で解決すべきところにはお金を投入するべきですが、セキュリティは直接的にビジネスに貢献するものではないこともあり、工夫次第でコストを抑えられるならそうするべきと考えました。
  • 弾力性:現状、クックパッドでは平均して一日あたり数件のアラートしか発生していませんが、今後の新しい脅威や方針の変化にともなってアラートの流量が増える可能性があります。そうした場合にスケールアップでしか処理量の性能をあげられないとするとすぐに限界がきてしまい、対応が滞ってしまう可能性があります。もともとの設計で速やかにスケールアウト・スケールインができるような弾力性を備えておくことで、突発的な流量の変化にも耐えられるようになります。

セキュリティアラート自動対応フレームワークの実装

アーキテクチャ概要

f:id:mztnex:20200317083451p:plain

設計で説明したような機能を実現するため、 DeepAlertというAWS上にサーバーレスで構築されたフレームワークを実現しました。これはセキュリティアラートを外部から受け取り、Inspector、Reviewer、Emitterという3つの役割を持つAWS Lambda Functionと連携して動作します。それぞれのLambda Functionの役割は次のとおりです。

  • Inspector:アラートに出現したIPアドレス、ドメイン名、ユーザ名に関して内外のデータソースにアクセスし、必要な情報を収集します。例えば外部から接続してきたIPアドレスであればブラックリストに掲載されているか、ドメイン名であればどういったサービスに使われているか、内部システムのユーザ名であればアラート直前までの行動ログなどを収集し、それらの結果をDeepAlertに返します。
  • Reviewer:アラートおよびInspectorが調査した結果を元にそのアラートのリスクを評価します。評価結果はシンプルに safe(影響なし)、unclassified(不明)、urgent(影響あり、要対応)の3種類のみにしています。
  • Emitter:Inspectorの調査結果、そしてReviewerの評価結果を元に対応を請け負います。対応も色々種類があり、調査や評価の結果をSlackなどを通じて通知する、外部の特定のIPアドレスからの接続を遮断する、あるいは対象ホストを隔離する、というような処理を想定しています。この対応も、影響ありの状況だったら即座に遮断したり、影響がなければ記録だけして通知はしない、というような評価結果に基づいた動作の振り分けも考慮しています。

これらのLambda FunctionはDeepAlertとは独立しており、特にInspectorとEmitterは任意の種類、数を接続することが可能になっています。それぞれAWSのSNS(Simple Notification Service)、SQS(Simple Queue Service)、Step Functionsを使うことでDeepAlertと連携しています。より具体的なアーキテクチャが次の図になります。大まかな動作として3段階に分かれており、Inspectorを動かす 1) 調査フェイズ、Reviewerを動かす 2) 評価フェイズ、そしてEmitterを動かす 3) 対応フェイズとなっています。

f:id:mztnex:20200317083605p:plain

この通り、DeepAlertはInspector、Reviewer、Emitterを動かすためのフレームワークとして実装しました。これまで様々な改良を続けてきたのでやや異なる部分はありますが、この仕組で約2年ほど運用し、その間にInspector、Reviewer、Emitterを必要に応じて入れ替えてきました。

Lambda、Step Functions、SNS、SQS、DynamoDBのみでサーバーレス構成として実装したため、料金は完全に利用量に基づいて計算されるようになりました。具体的には後述しますが、流量が少なければ非常に安価に使うことができます。また、各サービスにリソースの上限は設けられているものの、その限界までは人間の手を介することなく自動的にスケールアウト・スケールインしてくれます。これによって運用における金銭的コスト・人的コストの両方を極力抑えられています。

Pluggableな機能拡張

要件のパートで説明したとおり、情報収集をするInspectorと最終的に対応をするEmitterは状況の変化に応じて機能を追加・変更・削除していく頻度が多くなっています。我々はこの仕組を2年ほど運用していますが、その過程でも監視すべき対象が変わったりチームの運用方法にあわせて調査対象や対応方法が変化しています。

そこで、Inspector、EmitterはDeepAlert本体とは疎結合な形でデプロイできるようにしました。DeepAlertのリソースはAWS SAM(Serverless Application Model)およびCloudFormationでまとめてデプロイしていますが、InspectorやEmitterはそれぞれ任意の複数種類のLambdaをDeepAlertとは別のSAMでデプロイしてもいいですし、Cloud9で実装したものをデプロイするでも問題ありません。InspectorとEmitterはそれぞれSNS経由で必要な情報を受け取って起動し、InspectorはSQS(ContentQueue)で調査結果をDeepAlert側に戻します。また、Inspectorが関連する情報を調査する過程で、新たに調査すべき要素(例えば調査対象のユーザが使っていた別のIPアドレスや、マルウェアのハッシュ値からそのマルウェアが使っていたCommand & ControlサーバのIPアドレスなど)が発見された場合も、その情報をSQS(AttributeQueue)を通じてDeepAlertに戻して再度その要素についてInspectorが調査する、というフィードバックの仕組みも実装されています。このようにSNSとSQSだけを用いてintegrationする仕組みにすることで、Inspector、Emitterの動作がDeepAlert全体の動作に影響を与えないようにしています。

ちなみに、これまでクックパッド内では次のようなInspector、Emitterを運用してきました。カッコ内はアクセスするデータストアやサービスになります。(すでに利用しなくなったものも含みます)

  • Inspector
    • IPアドレス、ドメイン名、ファイルのハッシュ値がマルウェアに関連しているかを調査する(VirusTotal、Malwarebytes)
    • 出現したURLのスキャンし、どのようなサイトだったのかの情報を調査する(urlscan.io)
    • 社員の誰がそのIPアドレスを利用していたのかというログの抽出(社内のIPアドレス管理DB)
    • そのホストに自社管理のセキュリティソフトがインストールされているかの確認(CrowdStrike Falcon)
    • IPアドレス、ドメイン名、ユーザ名に関連する直近のログの抽出(社内のセキュリティログ検索基盤)
  • Emitter
    • 評価結果に基づいてアラートの通知(Slack)
    • アラート対応の割り振り(PagerDuty)
    • 調査結果アラート情報をまとめて保存(GitHub Enterprise)

Emitterについては、本来は被害をうけたと見込まれるホストをネットワークから隔離したり、証拠保全のプログラムを実行したり、ということも想定はしていました。しかし、幸いにも私自身が入社して以来、そういったことを即座に実行する必要があるようなインシデントに遭遇したことがなく、サービスに影響するような能動的な対応をどのくらいの確信度で実行するべきかというルール化ができていないため、そういった機能はまだ実装していません。これについては今後の課題としたいと考えています。

一般的なプログラミング言語でコード化したポリシー

先述したとおり、Reviewerは調査で集められたアラートの情報をもとに、そのアラートが実際の被害を及ぼしたのかを評価します。評価の方法や仕組みは Lambda Function にコードとして自由に記述できるようにしました。現在、クックパッド内ではGo言語を使ってポリシーを記述していますが、DeepAlert側で規定した Lambda Function に対する入力と出力のインターフェースに則っていればどのような言語で記述できます。

SIEMをはじめとする多くの製品では独自の記法でポリシーを記述ようになっています。これはポリシーに記述する要素を厳選し、入力する内容を減らすことで容易に表現できることを目的としていると考えられます。このような仕組みになっていることで、単純なポリシーであれば低い学習コストで記述できるようになります。しかし複雑な条件を扱う必要が出てくると、ポリシーを分割して見通しを良くするということができなかったり、任意のテストができないために検証のコストが大きくなってしまう、という課題に直面しがちです。また、デバッグの手段が用意されていない場合も多く、ひたすらトライ&エラーを繰り返して検証する必要がある、という問題にも悩まされます。ポリシーの記述力もあまり柔軟ではない場合が多く、愚直な処理を繰り返し書かなければいけなかったり、ちょっとしたデータ形式の変換などができず消耗するといったこともしばしばありました。

DeepAlertを実装する際、一般的なプログラミング言語でポリシーをコード化することでこれらの問題の多くを解決できると考えて、入出力のインターフェースだけを定義しました。DSLやライブラリを使って評価するような機能を提供しないことによって、言語の種類に対する依存も極力ないようにしました。これによって、通常のプログラミングにおけるコード整理やテストの技法を取り込むことが可能となり、ポリシーが複雑化しても見通しがよくテスト可能な形で記述することができるようになります*2

Reviewerの入出力定義

Reviewerに対する入力のサンプルを以下に示します。

{"id": "61a97323-b7dc-4b13-a30d-7b423388da5f",
    "alerts": [{"detector": "AWS GuardDuty",
            "rule_name": "High Severity Finding",
            "rule_id": "guardduty/high_sev",
            "alert_key": "xxxxxxxxxxxx",
            "description": "Unusual resource permission reconnaissance activity by PowerUser.",
            "timestamp": "2020-03-12T18:07:10Z",
            "attributes": [{"type": "ipaddr",
                    "key": "remote IP address (client)",
                    "value": "198.51.100.1",
                    "context": ["remote",
                        "client"
                    ]},
                {"type": "username",
                    "key": "AWS username",
                    "value": "mizutani@cookpad.com",
                    "context": ["subject"
                    ]}]}],
    "sections": [{"author": "addrmap",
            "type": "host",
            "content": {"activities": [{"last_seen": "2020-03-12T00:46:17.000928Z",
                        "principal": "mizutani",
                        "remote_addr": "198.51.100.1",
                        "service_name": "AzureAD",
                        "owner": "Cookpad"
                    },
                    {"last_seen": "2020-03-12T12:39:11Z",
                        "principal": "mizutani",
                        "remote_addr": "198.51.100.1",
                        "service_name": "Falcon",
                        "owner": "Cookpad"
                    }]}}]}

元にしているのはAmazon GuardDutyから発報されたアラートです。説明のためにいろいろと省略していますが、基本となる要素は含まれています。(詳細な定義については こちらから参照することができます)

まず alertsがセキュリティ監視・防御システムから発報されたアラートになります。このアラートについても独自のフォーマットになっているため、発報するシステムとDeepAlertの間で1つLambdaを挟んでフォーマットの変換をしています。attributesの部分にはそのアラートに出現した属性値になります。それぞれIPアドレスやユーザ名といった型を付けているのは従来のSIEMなどと同じですが、 contextというフィールドをもたせることでその属性値の意味がわかるようにしています。これは、例えば「Source IP address」という型でアラートの属性値が正規化されていたとしても、それが内部と外部のどちらのネットワークを意味するのかであったり、何か攻撃をした側なのか、それとも攻撃を受けた側なのかということが発報時の文脈によって変わってしまうという問題に対応するための説明用フィールドとなっています。

そして、Inspector によって収集された情報を格納したのが sectionsになります。ここでは社用PCがどのIPアドレスからどのサービスを使っているかというaddrmapという内製ツールからの情報が付与されています。これを見ることで AzureAD および CrowdStrike Falcon でもアラートがあがったIPアドレスから同様に接続があったことが示されています。こういった情報をReviewerが参照し、ポリシーで影響あり・なしの判断ができるのであればそれを出力として伝える、判断できないのであればセキュリティエンジニアの判断に委ねる、といった処理をしています。

この入力のスキーマについてはSTIXのような既存の脅威情報を記述する構造を利用することも考えましたが、本来の目的が違うこと、我々がやりたいことから見て機能が過剰であること、互換性を維持する意味があまりないことから独自の形式にしました*3

一方、出力についてはシンプルで、severityreasonの2つを入れるのみです。先述したとおり、severitysafeunclassifiedurgentの3段階でのみ表現されます。

{"severity": "safe",
    "reason": "The device accessing to G Suite is owned by Cookpad"
}

記述されたポリシーの例

具体的なポリシーの記述例を以下に示します。

// AWSへの不審なログインのアラートを評価するポリシーの例func handleAlert(ctx context.Context, report deepalert.Report) (deepalert.ReportResult, error) {
    for _, alert := range report.Alerts {
        // アラートが対象のものでなかったら評価しないif report.RuleID != "guardduty/high_sev" {
            returnnil, nil
        }

        // オフィスのIPアドレスからのアクセスの場合はこのポリシーでは評価しないif hasOfficeIPAddress(alert.Attributes) {
            returnnil, nil
        }
    }

    // Inspectorによる調査結果を抽出
    reportMap, err := report.ExtractContents()
    if err != nil {
        returnnil, err
    }

    // アクセス元のホストに関して Inspector が取得した情報をチェックfor _, hostReports := range reportMap.Hosts {
        for _, host := range hostReports {
            for _, owner := range host.Owner {
                // そのホストの所有者が Cookpad のものであると確認できるログがあった場合、if owner == "Cookpad" {
                    return&deepalert.ReportResult{
                        // Safe(影響なし)と判断する
                        Severity: deepalert.SevSafe,
                        Reason:   "The device accessing to G Suite is owned by Cookpad.",
                    }, nil
                }
            }
        }
    }

    returnnil, nil
}

func main() {
    lambda.Start(handleAlert)
}

このコードは説明のために簡略化していますが、おおまかな流れは実際のものと変わりません。先程の入力データの例では、不審なログインのアラートに対して、Inspectorが別の社内向けサービスを利用していたという情報を付与していました。このポリシーではその付与された情報を確認して、それが社員のPCが使っているIPアドレスからのアクセスなのか、それとも全く関係ない海外のサーバなどからのものなのかを確認し、もし社員のPCであると考えられる場合は safe(影響なし)という判断を返します。もし判断に足る情報がなければ、何も返さないことで unclassified(不明)と判定されます。

このポリシーとして記述された関数 handleAlertを用いることによって、アラート評価のテストを記述することができるようになります。この例では1つアラートに対するポリシーだけを記述していますが、実際には複数種類のアラートに対応できるようなコードを書く必要があります。新しくポリシーを追加したり、既存のものを変更した時、意図していない変更がまぎれていないかを確認するために常にテストで確認ができることで、自信を持ってポリシーをデプロイすることができるようになります。

その他のアーキテクチャの工夫

  • DynamoDBのベストプラクティスにもあるように、データストアは一つのDynamoDBのテーブルに押し込めています。このテーブルは複数アラートの集約、アラート情報の一時的な保持、新しく出現した属性値の管理、Inspectorの調査結果の保持などに利用しています。
  • 調査フェイズと評価フェイズにおいてLambdaの実行制御にStep Functionsを挟んでいるのは、アラートの到着から調査を開始するまでにわざと遅延を入れるためです
    • Inspectorの調査活動でも特にログを検査するタイプのものは、アラートが到着した直後ではまだログを参照できる状態になっていないことがあるため、数分程度待ってからInspectorを起動します
    • また、Inspectorが疎結合で任意の数実行されることから、DeepAlert側では同期的にInspectorの制御はしていません。そのため非同期に実行されたInspectorの結果を待つためにもStep Functionsを使っています

DeepAlert導入の効果

深刻度の自動評価による運用負荷の軽減

この仕組みを運用し始めておよそ2年ほどになりますが、2019年の実績では約50%ほどのアラートを人間が確認する前に影響なしであることを確認できました。これによって、対応に割く時間を大幅に減らす事ができました。

実際には影響がなかった場合でも全体の傾向の変化があった場合には気づきたいので、Slackで「影響なしのアラートが発生した」ということだけは通知させています。

f:id:mztnex:20200317083704p:plain

また、「影響なし」以外の判断がされたものについても、GitHub Enterpriseでアラートの詳細を記載したIssueを作成し、その上で対応の管理をしています。Issueについても最初にセキュリティアラートとしてDeepAlertが受信した情報だけでなく、Inspectorが取得した情報もあわせて記載しています。例として以下に示しているIssueでは、セキュリティログ検索システムへのリンクやVirusTotalへInspectorが問い合わせて取得した情報もあわせて掲載しています。これらの情報だけでは機械的に判定ができなかったわけではありますが、セキュリティエンジニアが自分で調査する際にも同じような情報をもとに作業することになるので、これらの情報が予め掲載されているということは作業時間の短縮に繋がります。このような点から自動評価できなかったアラートに対しても運用の負荷が下がっていると言えます。

f:id:mztnex:20200317083720p:plain

〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜 中略 〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜

f:id:mztnex:20200317083735p:plain

コスト

結論から言うと、直近半年のデータから計算された実費用は一ヶ月あたりで約 $1.3 でした。アーキテクチャの説明でも述べた通り、DeepAlertはクラウドの金銭的運用コストおよび弾力性を考えてLambda、Step Functions、DynamoDB、SQS、SNSといったリソースのみで構成されています。原則としてどれも使ったリソース量に応じてのみ課金される構成が可能であり、対処するアラートの流量が少なかったり、アラートの発生頻度にむらがあるような場合でもコストを小さく抑えやすくなっています。

まとめ

DeepAlertは、既存のSIEMのように「複数のイベントを組み合わせて(精度の高い)セキュリティアラートを発報する」という考え方ではなく、「(精度の低い)セキュリティアラートに対して情報を付与し、精度を高める」といった戦略になっています。そのため、純粋な機能面でのできること自体はSIEMのサブセットという位置づけになってしまいますが、運用のしやすさやコストメリットなどから、このアプローチが最も効果的であると判断して取り組んできました。このように既存の製品だけにとらわれず、それが本当に必要であれば自分たちで作る、といったところまでやりきれるのが、事業会社でセキュリティをやる楽しさの一つなのではないかなと思います。

クックパッドではこうしたセキュリティにまつわる課題を一緒に解決していくエンジニアを絶賛募集しています。興味のある方はぜひこちらをご参照いただくか、ご質問などあれば水谷( @m_mizutani)などまでお声がけください。

*1:これは自分たちの業務を楽にするというだけでなく、SOC全体のクオリティを一定に保つ、という効果もあったと考えています

*2:かならずしも見通しがよいコードが書かれることが保証されるものではありませんが、できる余地があることが重要だと考えています

*3:ただしSTIXなどからDeepAlertの形式に変換するというのは意味があるかもしれないので、必要があれば実装したいと考えています

Bridging the Gap Between Engineering and Design

$
0
0

Hello there, it's Dave Fox! I'm an iOS engineer from the Creation Development department in Cookpad. (@wowitzdave)

In this post, I'm going to talk a bit about how I used custom internal tooling and prototyping tools to help Cookpad's design and development workflow.

I'll look at how product and engineering teams can interact with each other more efficiently and ways in which we, as engineers, can empower designers to make their visions become a reality more easily.

Introduction

Last year, I began work on the internal Cookpad iOS renewal project. As part of its development, I was responsible for creating a new UI component to display user images in a style known as the Ken Burns effect.

The effect consists of panning and zooming around images and cross-fading between each one to give a sense of depth and dynamism.

Here's a short look at it in the iOS Cookpad app today...

Technical Implementation

Looking at the above effect, it doesn't seem too complex to implement... Just display some images with a zoom or a pan and fade between them. However, there are many settings and attributes applied to each image's animations and also to the slideshow's root animation as a whole. Each and every one of these values needs to be carefully considered to create the right "feel". Let's take a look at the parameters our animation requires:

For The Overall Slideshow
  • How long (in seconds) should we show each image?
  • How long (in seconds) should the duration of the cross fade be?

Additionally, each image uses either a zoom or a panning animation. These styles also have individual settings...

For Panning Images
  • From which anchor points should the animation start from and end at?
  • What speed should the animation happen at?
For Zooming Images
  • At which anchor point in the image should we zoom from?
  • What scale should we zoom from and to?
  • What speed should the animation happen at?

In order to create the exact effect the product team wants, these are all parameters the designers want to be able to define.

In a feature like this, it is often difficult for designers to imagine exactly what values they want. Often, design and development will work on a "trial and error" basis, tweaking values back and forth. With this many settings though, it would take a lot of time from two people to achieve the final goal. The workflow may look something like this:

f:id:davefox:20200402140954p:plain

The issue with this flow is the "Cross-department communication" bottleneck. Because of rapid iteration, this happens frequently and takes the time of both engineering and design teams to communicate changes. Each iteration takes a lot of time and disturbs the working pattern of all members involved on a feature.

A Product-Centric and Tools-Driven Approach

I had a lot of other tasks to work on as part of the renewal project but I knew the product team would want to iterate on this feature a lot so I decided I wanted to create a workflow which could reduce friction between design and engineering, keep both sides as productive as possible and, at the same time, create a toolset that is easy to use and familiar to designers. This was important to enable design to be as creative as possible.

Giving Designers Greater Control

To achieve this workflow, I decided to make a testbed application that would allow the design team to reiterate and play with all these animation values.

Then, when they were satisfied with the final animation. I could simply take their finalised values and copy them into the main iOS app codebase.

This would make the following workflow possible:

f:id:davefox:20200402141230p:plain

Designers perform both the review and adjustment stages

Testbed Application Implementation

So, I started off with a basic implementation of the Ken Burns effect with some default values and then created a simple application with screens to alter the animation settings and view a live preview of how the animation would look in the final product.

Let’s take a look at what this app ended up looking like:

Overall Settings Zoom Settings Pan Settings
f:id:davefox:20200402141310p:plainOverall animation settingsf:id:davefox:20200402141326p:plainZoom scale and anchor point settingsf:id:davefox:20200402141343p:plainPan from and to anchor point settings

As the design team modifies the animation values, they can preview their settings at any time on the home screen. This screen shows the animation in a number of different sizes and aspect ratios so the designers can see how things will look in a variety of different contexts within the Cookpad iOS application:

The designers can quickly and easily play with these values and, once happy, I have one sit-down meeting with them and integrate their final values into the application.

Results

When I think of the merits of this kind of approach to feature development, I look at two main areas:

  1. Engineering time and effort
  2. Friction between the wants of designers and the final output of engineering.
Time Taken

The design team made many many iterations of these values within the testbed application but because they could iterate in isolation, I didn’t need to spend any of my time on each change. With the "one time integration" approach, bringing the product team's vision into the main app only took me about 15 minutes of my time.

To this end, the amount of time taken from creating the prototype application and finalising the settings within the app was quite short so I think it was definitely worth it to take this approach.

Interaction With Designers

I had good feedback from the design department. Designers work visually and want to tweak settings and values on-the-fly. Giving them a visual interface to perfect this feature is more in tune with how those departments work so I think they found this approach more natural and efficient. It also allowed them to use their creativity to the fullest as there was no engineering bottleneck in the way.

Thanks for Reading!

Have you used tooling like this before? Was it helpful and how did it help you integrate with your design and product teams?

I believe that engineers shouldn't just code. They should also engage in the product development lifecycle. I think that understanding what product teams want and helping them realise their visions makes us better engineers and helps us grow as contributors across the entirety of a project, not just its codebase.

To that end, custom tooling like this can help bring us closer to the product team and helps them realise their visions more easily, often resulting in better, more cohesive products.

Thanks again for checking this post out. I hope it helps you and your team make better products going forward...

在宅勤務環境の継続的改善

$
0
0

コーポレートエンジニアリング担当 VP の @kani_bです。 新型コロナウイルス感染症の拡大リスクを鑑みて、従業員や関係者の皆さまの安全確保を目的に、クックパッドでは 2/18 (火) から、国内拠点の全従業員(正社員、契約社員、パート・アルバイト、派遣社員、通常在席の業務委託)を対象に在宅勤務の原則化を実施しています。現在は5月末まで継続する予定としています。

クックパッド、新型コロナウイルス感染症の拡大に伴う在宅勤務(Work from Home)を5月末まで継続のお知らせ | クックパッド株式会社

また、クックパッドでは、今の状況にあわせた、料理に関する様々な取り組みを進めています。そうした取り組みを集めたページをオープンしていますので、こちらもぜひご覧ください。
私たちは、料理でつながろう | クックパッド株式会社

さて、在宅勤務が開始された 2 月に、在宅勤務に対する取り組みについてブログ記事で紹介しました。早いもので、在宅勤務開始からすでに2 ヶ月が経過しました。1 年のうちの 1/6 を在宅勤務で過ごしたことになります。
クックパッドの在宅勤務環境 - クックパッド開発者ブログ

この記事では、先ほどご紹介したような様々な取り組みを支える、在宅勤務のための体制づくりについて、前回の記事からのアップデートをご紹介します。

設備やシステムの改善

在宅勤務が長期化するにつれて、短期的には許容されていた課題を解決していく必要が出てきました。 まず、設備やシステムに対してどのような改善を行っているかを紹介します。

オフィスチェア・オフィスデスクのレンタル

まずはじめに課題となったのは、自宅環境に在宅勤務に適した椅子や机がなく、座椅子やベッドなどでの作業を余儀なくされている例が多く存在することでした。 机はもちろんですが、椅子は特に生産性、そして心身の健康に大きく影響します。 そこで、2 月末に在宅勤務の延長が決定された段階で、オフィスチェアやデスクを会社負担にてレンタルし、各家庭に配送することにしました。 ちなみに、購入ではなくレンタルとしているのは、購入と比較して少ない費用で多くの従業員により良い椅子を届けられることや、在宅勤務終了後に居室の現状復帰が可能である (そもそも机や椅子などを置くことを想定していない場合が多い) ことなどを考慮しています。

このほかに、オフィスで利用しているモニターについても希望者には各家庭へ配送しているため、オフィスに近い環境をつくることができるようになりつつあります。

写真にある左の机は、会社から送付しているモニターと、実際にレンタルしたデスク・チェアを配置したものです。 f:id:kani_b:20200422031107j:plain

インターネット環境の改善

前回の記事でも紹介しましたが、クックパッドでは在宅勤務開始当初より 4G 回線を使ったモバイル Wi-Fi ルーターを貸し出し、テザリングの利用を前提に社用の携帯電話などを活用していました。 しかし、遠隔会議や業務によっては画像・動画のやり取りが多く、契約している通信容量を大幅に超過してしまう例や、そもそも帯域幅・レイテンシーが要件に見合わないケースが出てきました。 まずできる対応として、 4G 回線ではなく WiMAX2+ を利用できるモバイルルーターを用意し、随時交換を行ってきました。しかし、環境による速度差が大きいだけでなく、大容量なデータ通信を行う従業員の業務は相変わらずサポートするのが難しい状況にあります。また、モバイル Wi-Fi ルーターの新規調達も需要増大により難しくなってきました。

そのような状況を受け、各家庭にできる限り、いわゆる固定のインターネット環境を用意していただくのが、今後の対応長期化などを見込んだ上では最適と考えました。 そこでまず、在宅勤務を機に自宅にインターネット環境を用意する従業員に向け、工事費や契約事務手数料などの初期費用を負担することにしました。スムーズな利用開始のため、必要に応じてコーポレートエンジニアリング部 (ヘルプデスク) にて開通までの事務手続きや技術サポートを行うことにしています。

また、すでに家庭にインターネット環境があるものの、Wi-Fi 環境が良くなかったり、そもそも接続先 ISP がキャパシティ不足に陥っていたり… という例もあります。そうした方に向けては、社内 Wiki, ドキュメンテーションツールとして使われている Groupad に、Wi-Fi 環境の見直し方や有線 LAN 接続への切り替え方を案内する記事を書いたり、 Slack 内のあちこちでインターネット環境に詳しいエンジニアたちがコミュニティベースのサポートを行ったりしています。

契約書の電子化

この状況下においても安全に企業活動を進めるためにも、可能な限りの電子化に努める必要があります。すでに日本中でも多くの議論が起こっていますが、クックパッドにおいても、契約書類について全面的に電子サインを利用することとしました。 グローバル対応の必要性などから、 DocuSignを利用しています。こうした電子化の動きは、一社だけでなくできるだけ多くの方々にご協力いただくことではじめて成り立ちますので、クックパッドとのお取引がなくとも、ぜひご検討いただけますと嬉しいです。

Zoom Webinar の利用

コミュニケーションツールとしての Zoom 利用については前回のブログに書いた通りですが、全社ミーティングのようなイベントには Zoom Webinarを用いています。 配信者や利用者が普段利用している Zoom クライアントの操作感のまま利用できることはもちろんですが、現在ベータ版として提供されている言語通訳機能がよく活用されています。 この機能は、通訳そのものを行う機能ではなく、通訳者の方が発言者の発言を聞きながら通訳用の音声チャンネルに通訳音声を流すことのできる機能です。 クックパッドには、日本語を話す日本人だけでなく、業務に英語を使う従業員も多数在籍しています。これまでも全社ミーティングなどではレシーバーを用いた同時通訳を提供していました。この機能を利用することで、Webinar 環境においても、利用者側では言語選択をするだけで、同時通訳音声を聞きながら発表を見ることが可能になります。通訳者の方にも遠隔から協力をいただき、誰ひとりオフィスに来る必要なくこのような環境を実現できています。

また、全社ミーティングを Webinar に切り替えたことにより場所の制約がなくなりました。これによって、各自が見やすい環境で視聴でき、音声も聞き取りやすく、質疑応答やその後のフォローアップもしやすくなっています。結果として、オフィスでの全社ミーティングよりも参加者が増加しています。

勤怠システムの Slack 対応

在宅勤務期間中は、勤怠システムへの勤務時間登録を、システムにログインした上で行う必要があります。在宅勤務開始から様々な部署の Slack チャンネルを眺めていたところ、多くの人が勤怠システムに勤務時間を登録した上で Slack に出退勤を知らせるといったことをしていました。 出退勤の報告にはカスタム絵文字が主に使われていたため、絵文字の発言に反応して勤怠システムに打刻を行う bot を開発することで、出退勤の連絡を Slack に書き込むだけで、報告と同時に打刻を行うことができるようになりました。 f:id:kani_b:20200422031245p:plain

ツールの使い方に関する情報発信

課題解決のためのツール導入は解決の入り口でしかなく、「ツールをどう使うか」という文化形成によって解決されることがほとんどです。 在宅勤務の原則化によって、特にコミュニケーションツールを中心に、ツールの使われ方の傾向が変わってきました。それに伴っていわゆる「Slack 疲れ」といった、コミュニケーションへの新しい疲労も起きつつあります。 現在の環境や会社に合ったコミュニケーションの形を模索するため、ツールの使い方についても積極的な発信やそれをもとにした議論を行っています。以下にその一部を紹介します。

  • 「Slack 疲れ」の軽減
    • 「通知を受け手がコントロールする」「正しくメンションを使う」「即レスを期待しない」といったある種のコツがあると考えられる
    • Do not disturb (おやすみモード) の活用や Activity タブの利用などについて解説
    • 先日リリースされたチャンネルのセクション化についても解説
  • 「遠隔会議(Zoom)疲れ」の軽減
    • 部屋の概念がない遠隔会議ではミーティングが延びやすいため、Google カレンダーの会議迅速化オプションなどの活用
    • 気分によってカメラをオフにして話す
    • 在宅勤務を行う上で重要な家族との関係を保つためにも、お子さんが入り込むといった場面をみんなで許容する

社内外でのナレッジシェア

環境改善の他にも、在宅勤務そのものや在宅勤務における仕事をよりよくするためにできることを積極的にシェアして共有しています。公開されている記事について、簡単に紹介しておきます。

ほかにも、

  • 営業を担当する社員によるオンライン商談・お客様向けセミナー実施の Tips
  • 各家庭での料理の情報
  • マイクやカメラ、デスクを整理する便利グッズなどの情報
  • リングフィットアドベンチャーの販売情報

など、様々な情報が Groupad や Slack で共有されています。

おわりに

クックパッドの在宅勤務環境は、このように、環境面の整備だけでなく、社員ひとりひとりの積極的な情報共有文化や、助け合いに支えられています。全員が等しく在宅勤務をする、という経験のない状況を、様々な議論を交えながら少しずつ改善しています。 今回、そして前回の記事もあわせて、打ち手に悩まれている方にお役に立てば幸いです。

また、今回ご紹介したような環境を一緒に支えていくコーポレートエンジニアをはじめ、エンジニアに限らず様々なポジションにて積極的な採用を続けています。採用情報サイトも合わせてぜひご覧ください。

質問がありましたら@kani_bまでお気軽に。


在宅勤務環境を改善する社内オンラインハッカソンを開催した話

$
0
0

CTO の成田です。星による記事、 在宅勤務環境の継続的改善でもご紹介した通り、クックパッドでは国内外のグループ全体で在宅勤務に切り替えており、同時に勤務環境づくりに取り組んでいます。

先の記事では、椅子・机などのファシリティレンタルや、IT システムの整備、コミュニケーションの改善など、様々な取り組みをご紹介しました。本稿では、このような取り組みの一環として開催した、在宅勤務環境改善の社内ハッカソン「Hackarade Remote」についてご紹介します。

テーマは「私の Work From Home の課題解決」

社員同士のコミュニケーションの問題や、家庭環境の問題など、長期の在宅勤務には様々な課題があることはご存じかと思います。今回のハッカソンでは「在宅勤務で困っているかもしれない誰か」を想像で助けるのではなく、「自分が困っていること」を自分で解決するということに主眼を置いてテーマを設定しました。多くの人が助かるような最大公約数的な課題解決はすでに会社として取り組んでいるため、ここではそういった全体施策ではキャッチアップできないような粒度の課題を自分で見つけて自分で解決するというのが狙いでした。

作品例

まずは受賞作品の 3 作をご紹介します。

リモートユーザーインタビュー便利くん

リモートユーザーインタビュー便利くん

@hiragram による作品です。こちらは CTO 賞を受賞しました。

クックパッドではサービス開発の際にユーザーインタビューを多用するのですが、いまはこれまでのような対面でのインタビューを実施できなくなっています。そのためユーザーさんと Zoom でやりとりをしながら開発中のプロトタイプを触ってもらうのですが、現時点の Zoom の iOS アプリでは、たとえばクックパッドアプリの画面を共有してもらいながらユーザーさんの表情も見る、ということができません。そのため、アプリを使ってもらった際のユーザーさんの反応を感じ取りにくいという課題がありました。

そこで開発された「リモートユーザーインタビュー便利くん」は、iOS クックパッドアプリの開発版において、アプリの操作画面とカメラ映像を iOS 版 Zoom アプリを使って配信できる機能を追加したものです。内部的には、Zoom アプリの ReplayKit を利用しているそうです。

また @hokaccha からは、「リモートユーザーインタビュー便利くん for Web」という Web 版の実装が投稿されました。

リモートユーザーインタビュー便利くん for Web

Workcloud と Slack を連携するやつ

Workcloud と Slack を連携するやつ

@takonomura の作品です。こちらはスタッフの投票で決める投票賞で 1 位を獲得しました。

在宅勤務では、どうしても勤務時間の記録が煩雑になりがちです。クックパッドでは大多数の社員がフレックスタイム制で働いていて、 Workcloud(勤怠システム)を利用した打刻を行っており、勤務開始時や退勤時に Slack でチームメンバーに共有する文化があります。 この作品は Workcloud での打刻時に Slack に出退勤のメッセージを自動で投稿してくれたり、Slack で出退勤に関する発言をしたときに、打刻忘れを検知して Slack で通知してくれます。

ハッカソンの後日談としては、この作品とは別に、Workcloud の API を経由して勤怠を記録できる Slack bot が社内で開発されたため、Slack での発言をトリガーにして出勤/退勤の打刻ができるようになりました。

f:id:kani_b:20200422031245p:plain

みんなが買ったもの

みんなが買ったもの

こちらは @hogelog の作品。在宅勤務の不便を解消するためにみんながいろいろなものを購入していると思います。この作品はこれまで Amazon で買ってきたものを購入履歴から投稿することで、みんなが買っている便利なものを知ることができるサービス。この作品は人事本部長賞を受賞しました。

他の応募作品の例

上記の受賞作以外にも個性的な作品が多く出てきたので、ここではその一部をスクリーンショットとともに紹介します。

Nintendo SwitchのJoy-Conを使って赤ちゃんを抱っこしながらコードレビュー
id:yosuke403「Nintendo SwitchのJoy-Conを使って赤ちゃんを抱っこしながらコードレビュー」: 自宅で育児をしながらでもなんとか仕事を進めるためのツール。

さぎょイプ
@morishin 「さぎょイプ」: オンラインチャットを誰かと繋ぎっぱなしにして黙々と作業をする文化、通称さぎょイプを Zoom で行うためにマッチングを支援する Web サービス。

さんぽルート記録アプリ
@star__hoshi 「さんぽルート記録アプリ」: 近所の道を踏破するための記録アプリ。

Nippo Reader
@h13i32maru 「Nippo Reader」: 社内ブログに書かれた同僚の日報を読みやすくする Electron アプリ。

開催を振り返って

今回のハッカソンを振り返って、いくつか気付いた点を書きます。

見落としていた困りごとはまだまだ沢山ある

椅子や机がない、インターネット回線がない、などの、わかりやすく緊急度の高い課題は会社として取り組んできました。しかし今回のハッカソンで出てきた作品には、その作品を見て初めて「確かに私もそれ困ってた」と認識できるものが沢山ありました。在宅勤務において、まだ自分で認識してすらいない困りごとはまだ無数に残っているように思います。

在宅勤務においてまとまった時間を確保するのが難しかった

物理出社をしていたころは、エンジニア全員の予定を一日ブロックして、一日がかりのハッカソンイベントを行うというのはそれほど難しくありませんでした。しかし在宅勤務では、特に家族と暮らしている方はプライベートの時間と仕事の時間が入り乱れながら働いていたりするので、全員同時に丸一日拘束したイベントというのはこれまでより難易度が高いように思います。そのため、今回は提出締め切りまで 1 週間ほどの猶予を設け、その 1 週間のなかで「8 時間まで開発に使ってよい」というルールにしました。

チーム参加より個人参加のほうが動きやすかった

今回はチーム参加も可としていたので、チームを組もうとする人達は何組かありましたが、結果として全ての参加者が個人として参加しました。在宅勤務ではチームメンバーと同期を取りながら短時間で成果を出すのは難しい、というのは、普段の業務でも見られる傾向です。

全社イベントの価値を見直した

今回のハッカソンをやってみて、部署を超えて同じことに取り組む、同じ話題を持つ、同じ事で盛り上がる、ということはいまの状況下においては重要な意味を持つと改めて実感しました。在宅勤務では、仕事上直接繋がりのない人とは、視界にも入らず本当に疎遠になってしまいます。

おわりに

全員が在宅勤務に切り替えたときに、これまでと同じパフォーマンスが同じように発揮できると私は考えていません。しかし、在宅勤務環境はこうすれば全員普通に働ける! というような、全員に当てはまるようなベストプラクティスは存在しません。家庭環境の違いやオンラインコミュニケーションの得意さなど、在宅勤務の課題は人それぞれ違いがあるためです。 クックパッドは幸いにしてもともとグローバル展開によって複数の国に拠点があり、世界中どこにいても仕事ができるように情報システム環境を作ってきました。とはいえ全員が在宅勤務という状況は私たちにとっても初めて経験することです。 在宅勤務期間の長期化において鍵となるのは、私はひとり一人のサバイバル能力だと考えています。つまり、自分に合った働き方・休み方・作業環境・自己管理方法などを自分の力で見つけ出し、実行していく能力です。自宅の中で何が起こっているのかは会社やマネージャーは知ることができませんし、ひとり一人サポートしてあげられることは限られているからです。今回のハッカソンは、そういった自分で自分の課題を見つけて解決することの重要さを思い出すきっかけにできたと感じています。

在宅勤務でも雑談がしたい!在宅勤務の課題解決の取り組み

$
0
0

こんにちは、@morishin127です。クックパッドの在宅勤務環境 - クックパッド開発者ブログでも書かれていた通り、クックパッドは2月18日から現在に至るまで全従業員が原則在宅勤務となっています。突然の在宅勤務体制の中でも社員がなるべくいつも通りの生産性を発揮できるように様々な取り組みを行っています。このような状況になる以前から Slack や Zoom、GitHub Issue などのコミュニケーションツールが職種を問わず日常的に利用されていたため、比較的物理的な制約を受けないコミュニケーションができていたと感じていますが、それでもやはり全員が在宅勤務になると色々な課題が生じました。従業員に向けたアンケートでは次のような課題が挙がっていました。

  • 仕事とプライベートの切り替えが難しい
  • 同僚や上司との気軽なコミュニケーションがしにくい
    • いちいち Zoom をつなぐのはハードルが高い
  • チームの雰囲気を感じ取るのが難しい
  • 家に籠もっていて運動不足になる
  • デスク、イスなどの環境が悪く、体の負担が大きい
    • ソファやローテーブル、座椅子などで作業している
  • Zoom が連続してリフレッシュが難しい
  • ネット速度が遅い
  • 休み時間にも仕事の Slack の流量が増え、気が休まらない
  • 共有のホワイトボードやカンバンがないので、情報共有が難しい
  • 気分転換がしにくい
  • 人に会いたい、さみしい
  • 子供の泣き声等で集中しにくい
  • オフィスの Zoom Rooms だと画面が大きく、細かい字の資料も共有しやすいが、ラップトップだと字が小さく見にくい
  • モチベーションを維持するのが難しい
  • 日本語の会話は得意だが、読み書きは得意でないため、テキストコミュニケーションが増えたのはストレス(英語話者)
  • 同僚の機嫌や感情を感じ取りづらい
  • 紙の書類を扱う仕事があり、在宅では難しい
  • 孤独感を感じる

課題解決のための社内ハッカソン

そんな中、CTOから社内ハッカソン開催のアナウンスがありました。テーマは「私の Work From Home の課題解決」で、最大8時間の業務時間を使って課題を解決する何かを作ろうという催しです。もちろん在宅での参加で、作品はデモ動画の形で提出という形式でした。

ハッカソン自体については CTO がこちらの記事を書いてくれたので合わせてご覧ください 💁

techlife.cookpad.com

f:id:morishin127:20200416155553p:plain
社内アナウンスの記事

雑談可能な Zoom 部屋「さぎょイプ」

ハッカソンには多くの社員が参加し色々な作品が生まれました。この記事では私が作ったアプリケーションを紹介したいと思います。

「同僚や上司との気軽なコミュニケーションがしにくい」「チームの雰囲気を感じ取るのが難しい」「孤独感を感じる」といった課題に着目し、雑談可能な Zoom 部屋を用意して外から今誰が入っているかがわかる「さぎょイプ」というアプリケーションを作りました。

f:id:morishin127:20200415234905p:plain
アプリのスクリーンショット(ハンゲームの麻雀ロビーって言われた...)

さぎょイプ」って死語な気もしますが、要するに同僚と Zoom を繋ぎっぱなしで業務をすることです。シングルページの Web アプリケーションで、「どこに入ってもok、何も言わずに出入りok、いる人に突然話しかけてもok、何も喋らずもくもく作業しててok」というルールだけを掲げて複数の Zoom ミーティングを並べています。このアプリを作る前から社内で「さぎょイプ」活動はあったのですが、Zoom のミーティングにはホストという概念があり、ホストが別のミーティングのホストになると前のミーティングは閉じられてしまうという問題や、今誰が入っているか外から見ることができないという問題があったのでアプリケーションを作りました。

利用シーン

朝起きて業務を開始すると誰かしらが入っているのでそこに入って、大抵は互いに何も喋らず黙々と作業をしていたりします。喋ってないのに意味あるのかと思うかもしれませんが、やっぱり同僚の顔が見えていると漠然とした不安のような孤独感のような気持ちを抱きづらくなるのを感じます。

f:id:morishin127:20200421172856j:plain
Zoom の様子

ちょっとした質問や雑談も気軽にできるので、業務の効率化にも繋がります。サービス開発のようなアイデアをゼロから生み出すような作業は意外と雑談の中から生まれたりするものです。「雑」の有用性についてはこちらの記事をご覧ください。

techlife.cookpad.com

技術的な話

Zoom の管理者権限を持って Zoom アプリを作成したり、Firebase プロジェクトを作成したりする必要があるのでそのまま簡単に動かせるわけではありませんが、アプリケーションのソースコードを GitHub に置きました。興味のある方はご参考にどうぞ。

github.com

Zoom のミーティングにはホストという概念があり、ホストが別のミーティングのホストになると前のミーティングは閉じられてしまうという問題や、今誰が入っているか外から見ることができないという問題があった

と述べましたが、この問題の解決のために API 経由でライセンスユーザーを複数作成し、1ユーザーにつき1ミーティングをホストする形でさぎょイプ用のミーティングを用意することで解決しています。ライセンスユーザーなのでお金はかかります😇 詳細な手順はリポジトリの READMEに記載しました。

アプリケーションはブラウザで動くクライアントアプリと Cloud Functions の2つでできています。データストアは Firebase の Cloud Firestore を利用しています。Zoom ミーティングの参加・退出イベントを受け取ってリアルタイムで状態を画面に反映させたかったため、その辺りを簡単に実現できる Cloud Firestore と Firebase を選択しました。

Zoom APIには特定のミーティングに参加中のユーザーのリストを取得するエンドポイントが存在せず(ナンテコッタ)、ミーティングの状態を知るにはユーザーのミーティングへの参加・退出時に飛ばせる Webhook イベントを受け取るしかありません。この Webhook イベントを Cloud Functions で受け取り、さぎょイプ用に用意した Zoom ミーティングへの参加・退出イベントであれば Firestore にそのミーティングの状態を書き込むということをしています。

クライアントアプリは Firestore の変更を購読し、参加者のリストを表示しているだけです。デプロイは Firebase Hosting でも良かったんですが社内アクセスに制限したかったので GitHub Enterprise の GitHub Pages として公開しました。静的なページなので S3 でもなんでも大丈夫です。

クライアントアプリは Reactで実装されたシンプルなアプリで、同僚である KOBA789 お手製の KOBA789/frontend-templateを使用しました。クローンするだけで TypeScript で React アプリが書け、スタイルが Emotionで書け、 Parcelがバンドルしてくれます。今回はハッカソンということで8時間のタイムアタックだったので、こういうボイラープレートが大変ありがたかったです。

おわりに

この記事では自分の作ったアプリの紹介だけに止まりましたが、ハッカソンでは他にも色々な課題解決が生まれました。他の作品についてはこちらの記事をご覧ください。

昨今の状況から多くの職場で働き方が変わり、皆さまの環境でもこれまでに無かった課題が生まれていると思います。皆さまの身の周りで行われている課題解決もよければ発信して教えていただけたらなと思います。みんなでこの状況を乗り切っていきましょう。

Ruby3 さみっと online 開催報告

$
0
0

Ruby インタプリタの開発をしている技術部の笹田です。以前から自主的にリモートワーク状態だったので、あまり仕事環境は変わっていません。が、子供の保育園の登園を自粛しているため、色々大変です(主に育休中の妻が)。日常がはやく戻ってくれることを祈るばかりです。

さて、去る 4/17 (金) に、Ruby3 さみっと online というウェビナーイベント(オンラインイベント)を開催しました(Ruby3 さみっと online - connpass)。今年の12月にリリースされると言われている Ruby 3 に関するトピックに絞った発表会です。本稿では、このイベントについてご報告します。

RubyKaigi 2020 が、4月から9月に延期されたので、Ruby 3 開発のマイルストーンがちょっと宙ぶらりんになってしまいました。 そこで、一つお披露目する機会を作ろうと企画したのがこのイベントです。

イベントによって、Ruby 3 開発者に締め切り効果をもたらす、それから Ruby 3 に関する進捗を他の方にも聞いて貰い、ご意見を募る、というのを狙っています。 総じて Ruby 3 開発のためのイベントですね。もちろん、興味ある方が楽しんで下されば、それにこしたことはありません。

平日にもかかわらず、多くの方にご参加頂きまして、ありがとうございました。zoom のログによれば、250人以上の方にご参加頂いたようです。

なお、このイベントはクックパッドが開催した、というわけでもないのですが、企画運営がクックパッドの開発者であること、zoom アカウントの提供がクックパッドだったこと、それから他に適当な場所も知らないので、ここでご報告します。

発表

プログラムは次のような感じでした。

  • 09:00-09:30 Opening / Ruby 3 by Matz (zoom 練習時間)
  • 09:30-10:30 Fiber (Samuel)
  • 10:30-11:30 JIT (k0kubun)
  • 11:30-12:30 Guild → Ractor (ko1)
  • 12:30-13:30 Lunch break
  • 13:30-15:00 Ruby 3 type activities (mame, soutaro)
  • 15:00-15:15 Roadmap for RubyGems 4 and Bundler 3 (hsbt)
  • 15:15-15:30 Proposal of Proc#using (shugo)
  • 15:30-15:45 Real Terminal Testing Framework (aycabta)
  • 15:45-15:50 Windows and UTF-8 (usa)
  • 15:50- Ruby3 Q&A

だいたいオンタイムで進みました。資料は https://hackmd.io/@ko1/ruby3samittoに(あるものは)あります。

Ruby 3 のメインゴールは JIT compile、Concurrency それから静的解析です。それらの大きな話に1時間ずつ(静的解析は二人で1.5時間)と大雑把に割り当てました。質疑応答も十分行えたのではないかと思います。また、その他の話題として、4人の方に「こんなことします」みたいな話をして頂きました。最後に Q&A タイムは、雑多な話題をのんびりと続けて、いつになくグダグダな時間になりました。

内容の詳細は、発表資料を見て下さい。

ランチブレイク中なども、zoom での中継は続けており、発表権限がある人たちで雑談していました。

アンケート結果

開催中に Google form でアンケートを作って、最後に参加者の方に伺いました。58名の方から回答を頂きました。

f:id:koichi-sasada:20200428180034p:plain
「大雑把にどうでした?」という質問への結果

今回のイベントについて「大雑把にどうでした?」という質問については、ごらんの通り、好評だったことがわかります(もちろん、好評だった人しか回答していなかったという可能性はあります)。少なくとも、60人弱の人達が楽しんで頂けたのしたら良かったです。

良かった発表については、一番好評だったのが「Ruby 3 Q&A」という結果でした。好評なら良かったんですが、グダグダ過ぎなかったかな、あれ。

感想では、次のような意見を頂きました(一部抜粋)。

良かった点:

  • リアルイベントと違ってゆるく参加できるのは良かった
  • お金かからない。単一セッションなので全部見れるのが良かった。
  • 家にいながら参加できるの大変助かります(配信などいつもありがとうございます)
  • オンラインでしたが物理イベントよりもホスト側との距離が近いと勝手に感じました。
  • リアルタイムに参加者の方が質問したりして、ライブ感があってよかったです。
  • アンケートなど交えていた点
  • Slack上でコミュニケーションを取りながらや、アンケートをフィードバックしながら発表を聞けるのはとても良かったと感じました
  • Rubyコミッターが普段どのように議論しているのかが感じられて良かったです。
  • RubyKaigi な感じがとても良かった
  • ゆるい進め方が良かったです
  • こういった会がある事自体がいいですね。Zoomとかでみんなの顔や声があって繋がるの、ここ最近の閉塞感を和らげるのにとても良かったとおもいます。
  • 緊急事態宣言の状況に対して、家にいながら Ruby 3 について聞けて良かったです。
  • このイベントを開催してくださったこと自体がとてもよかったことですし、内容もすごくよかったです
  • Samuelのライブコーディング見れたのがとても良かった!

改善案:

  • 視聴者からのリアクションが見えるようになると良さそう
  • 身内ノリが多いのはRubyコミュニティならではかなあと思ったけどとにかく身内ノリは多かった
  • 休憩中なら分かりやすく「休憩中(Rubyistの雑談の時間)」って書いてあると嬉しい
  • 休憩時間をこまめにほしかった。
  • 仕事をしながらの参加だと理解が追いつかないので、録画があると嬉しいです
  • 朝早すぎて起きれなかった
  • 情報量の多い資料は、今どの部分について話しているのかがわかりづらかったのでマウスポインタなどを活用して欲しい。

省力開催のウェビナー

我々が設定した目的は Ruby 3 開発を促進することなので、凝ればいくらでも時間がつぎ込めるイベント運営は極力省力化を目指しました。その決意の表れとして、「さみっと」という気の抜けた名前にしています(サミットとか Summit だと、なんか真面目にやらないといけない感じがしません?)。

ウェビナーという形式は初めてだったので、ちょっと運営に関するメモを残しておきます。

開催準備

  • 我々にとって手慣れたツールである zoom のウェビナーを利用する
    • ウェビナーの利用は初めてだったので、前日にリハーサルをしました。
    • セキュリティの懸念点から、zoom だと参加できない人もいるという声も聞きましたが、それはしょうがないとしました。
  • 運営ミーティングは1回だけ(1時間くらい)
    • 遠藤さんと1時間くらいでさっと決めました。
  • スケジュールはてきとーに決める
    • Ruby3 に関する3目標に関する人達の予定を抑えてスケジュール決定。
    • matz が平日のほうが都合が良いってことだったので、平日で。
    • あとは可能な人・希望する人だけ発表してもらう。
  • タイムテーブルはゆるく作る
    • もちろんシングルセッション。
    • 時間に余裕を持たせて、あとから発表希望者をプログラムに追加。
  • 募集などは Connpass のページ(https://rhc.connpass.com/event/169873/)だけ(発表者募集は ruby-dev ML を利用)
  • 日本語を公式コンテンツとする
    • 情報発信は日本語のみで行いました。
    • 英語話者(Samuel)が居ましたが、日本語しかないことを了承して頂きました。
    • 資料作成がやっぱ日本語だけだと本当に楽ですね...。
  • コンテンツ管理が面倒なレコーディングはしない

運営については、労力と時間はほとんどかけずに済ますことができました。 もうちょっと宣伝やっても良かったかも?

期間中の運営

zoom のウェビナーは、一般視聴者と、発言ができるパネリストの2つに分かれています。発表者がパネリストになるのは問題ないのですが、その他に誰がパネリストになるかは検討する必要があります。当日は、なんかしゃべりたそうな人を見つけたら、片っ端からパネリストにする、という運用を行いました。

一応、zoom のチャットや slack や twitter などを見て、発表者にフィードバックすることがあれば、気づいたパネリストが発表者にフィードバックする、というような感じで行いました。

Zoom では、参加者にリアルタイムアンケートをとる機能があるのですが、ウェビナーを立ち上げたホスト(笹田)のみが作成できるというものだったらしく、私が思いついた質問を参加者の方に投げかけるということを何度か行いました。ただ、一人の人間だけでやっていたので、質問が広げられなかった感じはします。

zoom のログを見ると、最大で160人が同時接続し、250人ほどが期間中に接続したようです(名寄せをちゃんとやっていないので、同じ人が複数デバイスで接続している場合があります)。

f:id:koichi-sasada:20200429033500p:plain
接続数の推移

お昼の接続数が最大だったんですが、やっぱり昼休みは見やすかったんですかね。

f:id:koichi-sasada:20200429033547p:plain
ユーザごとの滞在時間を昇順にソート

ずっと見て下さっていた方もいれば、ちょっと覗いてみた、という方も居そうです。

最後の発表者4人のうち、3人が 4pm から用事があるということを結構直前に知ったので、ちょっと順番を入れ替えました。そのあたり、少し事前に聞いておいても良かったかも知れません(が、そういう忙しい人でも、一部だけでも参加してくれるのは、ウェビナーの良い点ですね)。

たくさんのご参加、ありがとうございました。

おわりに

Ruby3 さみっと online というウェビナーイベントの開催についてレポートしました。

おかげさまで、省力開催にもかかわらず、ウェビナー開催は初めての経験でしたが、大きな失敗もなく開催することができました。発表者の皆様、ご参加頂いた皆様、それから運営を手伝って下さった皆様に、改めて感謝いたします。

また、こういう機会を作って Ruby 3 マイルストーンを用意して、開発を促進していければと思います。完成するといいなぁ。

インフラにかかるコストを正しく「説明」するための取り組み

$
0
0

技術部 SRE グループの mozamimyです。

クックパッドでは、 SRE が中心となって、サービスを動かす基盤の大部分である AWS のコスト最適化を組織的に取り組んでいます。

昨年夏に公開した記事である、インフラのコスト最適化の重要性と RI (リザーブドインスタンス) の維持管理におけるクックパッドでの取り組みでは、

  • なぜインフラのコスト最適化が必要なのか、具体的にどのような考え方に沿って進めてゆけばよいのか。
  • SRE が一括して管理する AWS のリソースプールそのもののコスト最適化を実践するための具体的な取り組みの一例として、RI のモニタリングや異常時の対応フローによる維持管理。

といった話題にフォーカスしました。

今回は、インフラにかかるコストを正しく「説明」するための取り組みということで、コスト最適化に貢献する社内アプリケーションである Costco (CostConsole の略です) と、その設計思想や目指すところについて解説します。多分に社内コンテキストを含むツールなので OSS とはしていませんが、読者の皆さんの組織で同様の仕組みを構成するときの役に立つことでしょう。

今回ご紹介するトピックは、ある程度のパブリッククラウド (特に AWS) の知識があれば前回の記事の予備知識がなくとも読める内容となっています。しかしながら、読者の皆さんの組織に応用することを考えると、その背景を知っておくと理解がより深まると思いますので、前回の記事の、特に前半部分を読んだ上で今回の記事を読むことをおすすめします。

以降、単にコストと表記した場合は金銭コストのことを指すこととします。

あなたのサービスのインフラコスト、妥当な金額ですか?

いきなりですが、真か偽で答えることのできる、一つの問について考えてみましょう。

「あなたが運用しているサービスにかかっているインフラのコストは妥当な金額ですか?」

この問について、それが合っているか否かはさておき、確信を持って答えられる人は少ないのではないでしょうか。もし自信を持って答えられるのであれば、あなたの組織は高いレベルでコストを管理することができているでしょう。

では、なぜこの問に対して自信を持って答えることができないのでしょうか。理由は簡単で、インフラにかかっているコストの状況を継続的に把握できていないからです。逆に言えば、かかっているコストの妥当性を評価する仕組みを用意し、定期的にふりかえる場を持つことで、この問に答えるための根拠となるのです。

インフラコストの妥当性について考える

前節で「サービスにかかっているコストは妥当かどうか」という問について考えました。そもそもクラウドの特長として「必要な分を必要なだけ利用できる」という点があげられます。つまり「必要な分を必要なだけ利用しているのだから、健全に決まっているじゃないか」と言えそうな気がします。

しかしながら当然そのようなことはなく、大なり小なりどこかにムダが発生しているのが常です。オーバープロビジョニングな EC2 や RDS インスタンスなど、クラウドを利用しているどのような組織でも探せばどこかにムダが見つかることでしょう。

オーバープロビジョニングはもっとも分かりやすい例ですが、アーキテクチャの差異による必要コストの違いを適切に評価することはもっと厄介です。AWS の場合、SQS や SNS、 EC2 といったプリミティブなものから、RDS や ElastiCache といった高レベルなミドルウェアを提供するマネージドサービスまで、多様なビルディングブロックを組み合わせてアーキテクチャを構成できます。つまり、達成したい目的を満たすアーキテクチャの可能性はいくつもあるわけです。

アーキテクチャの違いによるコスト差の一例として、ECS における ELB の利用方法があげられます。ECS を素直に利用する場合、ECS に組み込まれている ELB サポートを利用し、1 サービスに対して 1 つの ELB を紐付ける構成がもっとも素朴で一般的な構成です。ただし、無数にある社内向けアプリケーションについてはどうでしょうか? ひとつひとつに ELB を割り当てると、その積み重ねで無視できないコストになります。そこで、社内アプリケーション向けに 1 つの ELB を共有し、ECS サービスと ELB の割当部分を自前で作り込んで運用する、という構成も考えられます*1

後者の場合では、運用・作り込みのコストを対価として、金銭的なコストを軽減しているといえます。実際、クックパッドでも社内アプリケーションについては ELB を共有する構成が標準となっています。このように、自前で作るコストと金銭コストを天秤にかけるというシチュエーションはしばしば起こり、その評価を正しく行うためにもかかっているコストを分類して正しく把握することは重要です。

「必要な分を必要なだけ」というクラウド利用料金と組織の「財布」のギャップを埋める

そして「必要な分を必要なだけ利用できる」というのは、組織の財布の仕組みとも相性があまり良くありません。クックパッドもそうですが、一般的な組織では 1 年を基本単位として予算を決定し、それに基づいて資金を使っていくことになります。クラウドの料金は、基本は使った分だけ払うという仕組みなので、ここにギャップがあります。これは社会の仕組みであり資金が有限なのは変えられないことなので、わたしたちはそのギャップと戦う必要があります。クラウドを利用する場合でも予算案を提出し、ステークホルダーに対して必要な理解を得て、責任を持ってその予算内でやりくりする必要があるのです。

どの程度予算に沿うことを強く要請されるかは、組織の持つ規模や資金状況によって様々だと思われますが、少なくとも予算の利用状況を「説明」できるようにすることはどのような組織でも共通でしょう。

説明ができれば、どこにムダがあるのかを明らかにし、必要であれば手を打つこともできます。説明ができれば、次年度の予算案も正しい根拠とともに立てて理解を得ることができます。説明ができれば、もし予算をオーバーしても正しく理由付けができます。

「説明」できるようになるために必要なこと

説明できるようになるために何が必要なのかを考え、わたしたちは以下の 2 点に行き着きました。

  • コストを適切に分類し、何にどの程度コストがかかっているのかを把握できるようにする。
  • 定期的 (1 ヶ月に 1 回程度のスパン) にふりかえりの場を持ち、その月と年始からのコストの様子をまとめて蓄積する。

わたしたちはソフトウェアエンジニアであり、そのスキルは問題を解決するための強力な武器となります。後ほど詳しく説明しますが、いくつかの選択肢を考えた上でコストを管理するためのコンソールアプリケーションを開発することにし、そのツールに CostConsole の略で Costco と名付けました。

Costco が持つ機能とそれにより達成できること

Costco は多分に社内コンテキストを含むツールなので、残念ながら OSS とはしていません。しかしながら、その機能一覧とそれによって達成できること、設計思想は読者のみなさんの組織で同様のアプリケーションを作る参考になるはずです。

では、百聞は一見にしかずということで、まずどのような機能を持つのか一通り説明してから、その設計思想について説明していきます。Costco の機能一覧は以下のとおりです。

  • 予算と月次のふりかえりに関する機能
    • Cost Explorer のフィルタとして予算を定義する機能
    • 当月のコストの予算に対する進捗を一覧できる機能
    • 月次でコストをレポートとしてまとめる機能
  • 購入決裁に関する機能
    • 年間のインフラ用の購入決裁として割り当てられた金額を設定できる機能
    • 購入決裁の利用状況を確認できる機能
  • コスト配分タグに関する機能
    • コスト配分タグにコメントなどのメタデータをつけ、管理する機能
    • コスト配分タグをカテゴライズして、まとめて扱う機能
    • 特定のコスト配分タグをもつ AWS リソースを一覧できる機能
  • その他コストの管理に役立つ細かい機能

Costco が扱っているデータのほとんどは Cost Explorer API から取得したデータそのものや、それらを加工したものです。API のコールには 1 リクエストあたり 0.01USD と、積み重なるとそれなりに高額になりうる料金がかかるため、バッチ処理で定期的に Costco に取り込むようにしています。

予算と月次のふりかえりに関する機能

予算を Cost Explorer のフィルタとして定義する

Costco には予算という概念があります。これは組織としての予算に対して Cost Explorer のフィルタを紐付けるものです。ここで設定した Cost Explorer のフィルタは「ある予算が何を意味しているのか」をコード (JSON) として直接説明しています。ここに日本語のような曖昧さは発生しません。さらに良いことに、このフィルタは実際に動きます。フィルタを使って Cost Explorer で簡単にコストを可視化できるのです。

たとえば、クックパッドが日本で展開しているサービスすべてをひっくるめた予算は、以下のようなフィルタで定義しています。

{"and": [
    {"dimensions": {"key": "REGION","values": ["","ap-northeast-1","global"
        ]
      }
    },
    {"not": {"dimensions": {"key": "LINKED_ACCOUNT","values": ["***************"
          ]
        }
      }
    },
    {"not": {"dimensions": {"key": "SERVICE","values": ["Tax"
          ]
        }
      }
    }
  ]
}

Costco に入力された日本で展開しているサービスをひっくるめた予算

Costco に入力された日本で展開しているサービスをひっくるめた予算

 

クックパッドでは、REGION の dimension が ap-northeast-1・global・No region に分類されるコストを日本国内向けサービスの費用として、それ以外を国外向けサービスの費用として扱っており、このフィルタはそれをコードとして表現しています。(諸事情によりある AWS アカウントは集計から除外しています)

予算名から分かるように、国内サービス全体の予算をさらに細かく分割し、研究開発にかかる費用や開発者が自由に利用できるサンドボックス AWS アカウントにかかる費用など、それぞれに予算を設定しています。たとえば日本の研究開発にかかる費用は以下のように定義しています。

{"and": [
    {"dimensions": {"key": "REGION","values": ["ap-northeast-1"
        ]
      }
    },
    {"dimensions": {"key": "LINKED_ACCOUNT","values": ["************"
        ]
      }
    },
    {"not": {"dimensions": {"key": "SERVICE","values": ["Tax"
          ]
        }
      }
    }
  ]
}    

Costco に入力された日本の研究開発にかかる予算

Costco に入力された日本の研究開発にかかる予算

アカウント ID は黒塗りにしていますが、ここには研究開発のために開発者が自由に利用できる AWS アカウントが設定されています。

Cost Explorer のフィルタと予算を紐付けて管理するというやり方は、AWS の機能の一つである AWS Budgets にインスパイアされたものです。ただし、AWS Budgets ではフィルタに not 条件を書けないというわたしたちのユースケースにおいては致命的な弱点があり、それは Costco を開発した理由の一つになっています。

定義した予算をもとに当月のコストの進捗と予測を一覧する

ここで設定した予算をもとに、以下のように当月のコストの進捗と予測を一覧できます。この予測は、Cost Explorer の GetCostForecast API で取得した値です。

当月のコストの進捗と予測を一覧する画面

当月のコストの進捗と予測を一覧する画面

 

大部分が黒塗りになっていて少しわかりにくいですが、左から 3 番目のカラムに各予算に対する当月のコストの着地点の予測が表示されています。予測額が予算を上回っている場合黄色⛈で表示され、収まっていれば緑☀️で表示されます。また、左から 4 番目のカラムには予算に対する今月の料金の進捗が金額と%で表示されています。

SRE グループの毎週の定例ミーティングでは、この画面を起点として、詳細は Cost Explorerを見ながらその週のコストの様子を簡単にふりかえっています。

月次レポートとしてコストを自動集計し日本語で所見をまとめる

この機能は Costco の目玉機能であり、設計思想を体現している機能の一つです。まずはその内容を見てみましょう。

Costco によって自動集計された 2020-02 の月次レポート

Costco によって自動集計された 2020-02 の月次レポート

このようなレポートが毎月 Costco のバッチ処理によって自動的に生成されます。具体的な数字は黒塗りにしていますが、スクリーンショットは 2020-02 分の実際のレポートです。

対実績のカードでは、予算に設定された金額と実績を比べ、予算を超過しているかそうでないかを一覧することができます。

クックパッドでは、従来 EC2 について RI を購入していましたが、 RI の維持管理を楽にするために既存の RI が切れ次第、Savings Plans (Compute) に置き換えを進めています。Savings Plans (Compute) では、一定のルールにしたがってリザーブが自動的に適用されるため、結果的にどのリージョンにどの程度 Savings Plans が適用されたかは Cost Explorer API から取得したデータを使って自前で計算する必要があります。

予算定義の節で述べたように、クックパッドでは国内向けサービス (JP) と国外向けサービス (UK) にかかるコストはリージョンで区別しています。また、RI や Savings Plans といった先払いする料金については、以下の画像のようにそれを毎月均等に割って振替えるという経理処理をしています。

先払いする AWS 料金の毎月の振替の例

先払いする AWS 料金の毎月の振替の例

この割合は毎月変わりうるため、月次レポートの「Savings Plans の JP/UK 利用割合」カードに Cost Explorer から得られたデータを計算して出した割合を表示し、経理のスタッフが確認できるようになっています。

一番下のカードは担当者 (今は SRE グループのわたしが見ています) が所見を書く欄となっており、月内での社内の動きや Cost Explorer をドリルダウンして調べた結果をもとに、コストの様子についてまとめています。

この 2020-02 のレポートは特に印象的で、所見に書かれているように MediaLive の利用料金が大きく増加していることに気づくことができています。このあと開発チームに相談してコスト増加の原因の解明と修正をしたのですが、Costco によってアノマリの早期発見ができた良い例です。

実際に AWS から請求書が届いて利用料金が fix するまでは月次レポートの実績の値が変化する可能性があるため、月が変わったすぐの状態では以下のような表示にしています。請求書が届き次第、レポートの所見欄を書いて凍結ボタンを押すという運用にしています。

請求が未確定な月次レポート

請求が未確定な月次レポート

また、年間を通して予算の状況を確認できる画面もあります。

年間を通した予算ごとのコスト状況を一覧できる画面

年間を通した予算ごとのコスト状況を一覧できる画面

予算に関連する機能で達成できること

ここまで説明したように、Costco を使って定期的に AWS の利用状況をコスト面からふりかえることで、早い段階で異常に気づくことができる上に、毎月の利用状況をレポートという形で蓄積することができます。

定期的にレポートを蓄積しておくことで、年末の次年度の予算案の作成や購入決裁の申請など、社内での手続きを楽に進めることができます。夏休みの終盤に宿題の処理に慌てなくていいように少しずつ進めておく、というイメージに近いです。

また、この画面は全社員が見られるようになっているため、エンジニアはもちろんのこと、コストに興味のあるすべての人に情報は開かれています。特に経理といったバックオフィスのスタッフにも利用されており、それだけでもウェブアプリケーションとして内製した価値があると感じています。

このように、Costco そのものというよりは、Costco を中心としたコストの定期的な評価の機会を持つというオペレーションが良い結果をもたらしています。

購入決裁に関する機能

一般的な他の組織と同様、お金を使う場合は予算とは別にそれをもとにした購入決裁 (いわゆる稟議とも呼ばれるものです) を申請し、承認を受ける必要があります。実際に支払いが発生すると、承認を受けた金額の枠からその分が引かれ、消化していく形になります。

RI や Savings Plans で先払いした金額は予算上は毎月の償却として扱いますが、購入決裁においては実際に支払った分が消化されます。つまり、予算と購入決裁の消化状況に差が発生するため、購入決裁は独立して定期的に確認しておく必要があります。

クックパッドでは ERP として Workday を採用しており、購入決裁そのものやその承認プロセスの管理、金額の枠の消化状況 (請求情報) は Workday のデータベースに格納されています。コーポレートエンジニアリング部の尽力によって、Workday に格納されている請求情報を Informatica Cloud によって S3 バケットにエクスポートできるようになっています。Costco では、そのようにして連携されたデータ (AWS に関する請求情報のリスト) をバッチで取り込むことで、以下のように AWS の購入決裁の消化状況を簡単に確認できるようにしています。

購入決裁とそれに紐づく請求情報を一覧する画面

購入決裁とそれに紐づく請求情報を一覧する画面

このように、購入決裁の消化状況も予算とあわせて定期的にチェックすることで、枠が不足しつつあることを早めに察知できます。その場合は、追加の購入決裁を出す手続き (稟議) をすることになります。

コスト配分タグに関する機能

クックパッドでは、コスト配分タグとして以下のタグのキーを設定しています。

  • Project
  • Name
  • Role
  • Environment
  • Resource
  • Owner

個々のタグの役割の詳しい説明は記事の趣旨を超えるため割愛しますが、このうち特に重要なのが Project タグで、このタグに設定された値によって AWS リソースがどのプロジェクトで利用されているかを分類しています。

たとえばクックパッドのレシピサービスで利用されているリソースは Project=cookpad、クックパッドマートに関わるリソースには Project=mart というような粒度でつけています。また、個々のユーザ向けのサービスによる分類だけでなく、たとえば VPC 運用に必要なものには Project=infra-vpc というタグをつけていたりもします。

Costco とは別の仕組みとなるためここでは詳細な解説は割愛しますが、EC2 や RDS、ElastiCahce、S3 など、AWS コスト全体に対して料金が支配的なサービスについては、Project タグの付与を強制する仕組みも内製で整備しています。

クックパッドでは AWS Organizations やアカウント間での VPC 共有などの機能が発達する前から長らく AWS を利用しており、一つの本番用アカウントにあらゆるアプリケーションが動くという構成になっています。アカウントが分かれていればそれだけである程度コストの分類が可能となりますが、そうではないため Project タグを整理することは特に重要な作業となっています。

Project タグをカテゴライズしてまとめて扱う機能

Project タグによる分類は便利ですが良くも悪くも粒度として細かく、もっとマクロな視点でコストを眺めたいときには不便です。くわえて、現状で約 300コの Project タグの値が存在しており、数が多くてそれらをフラットに扱うのは困難です。

そこで、Project タグの値をカテゴリという単位にまとめられるようにしました。カテゴリの一覧は以下のような感じになっています (一部です)。

Project タグのカテゴリ一覧 (抜粋)

Project タグのカテゴリ一覧 (抜粋)

漏斗のボタンを押すと、以下のように Cost Explorer に遷移し、自動的に Project タグによるフィルタが設定された状態でそのカテゴリ全体でかかっているコストを簡単に視覚的に確認することができます。以下のスクリーンショットは ECS・Hako カテゴリ*2の例です。

Cost Explorer によるカテゴリにかかるコストの可視化例

Cost Explorer によるカテゴリにかかるコストの可視化例

各カテゴリには、以下のようにいくつかの Project タグの値がぶら下がっています。これも ECS・Hako カテゴリの例です。

ECS・Hako カテゴリに属する Project タグの値一覧

ECS・Hako カテゴリに属する Project タグの値一覧

このように、クックパッドにおける ECS・Hako エコシステムはいくつかのプロジェクト(≒アプリケーション) によって構成されていることがわかります。カテゴリの場合と同様、漏斗のボタンを押すことで Cost Explorer に遷移してコストを視覚的に把握できます。また、虫眼鏡のボタンを押すと、そのタグがつけられた AWS リソースを検索することができます。

Project=hako-console で検索した AWS リソースの一覧

Project=hako-console で検索した AWS リソースの一覧

このスクリーンショットは Project=hako-console の例です。

各 Project タグの値を複数のカテゴリに所属させるような、N:M (つまりタグに対するタグ) の関係で管理することも考えましたが、経験上、そのようなデータ構造にすると人間の認知能力の限界を超えてしまって収拾がつかなくなるため、あえて自由度を下げてカテゴリ:タグの値 を 1:N の関係で管理する仕様にしています。

Project タグの値にメタデータをつけて秩序をもたらす

Project タグの値が増えてくると、その値が持つコンテキストを頭の中で把握しておくことが困難になります。また、クローズしたアプリケーションやサービスに関連する Project タグに特別なフラグをつけて区別したくなります。その他にも、新たなプロジェクトが始まる際には Project タグの値を追加することになりますが、その値が適切な粒度がどうか、命名が適切かどうかを考え、誰かがレビューする必要があります。

これらの問題を解決するため、Costco に Project タグの値に対して以下のようなメタデータをつけて管理する機能を実装しました。

  • コメント
  • 非推奨フラグ

一見して値の意味がわかりにくいものについては、以下のスクリーンショットの infra-cost-optimization のようにコメントを書いて役割を明示することができます。

Project=infra-cost-optimization にコメントを付与している例

Project=infra-cost-optimization にコメントを付与している例

また、すでにクローズしたアプリケーションに関係する Project タグや、間違って新たに作ってしまったようなタグには非推奨フラグを設定し、今後新たに作る AWS リソースにはそのタグを付与しないように勧告することができます。

非推奨フラグが設定された Project タグの値

非推奨フラグが設定された Project タグの値

また、非推奨フラグが付与された Project タグによるコストが発生している場合、今月のコスト予測の画面で以下のような警告が表示され、AWS リソースの削除漏れに気づくことができます。

非推奨フラグが設定された Project タグによるコストの警告

非推奨フラグが設定された Project タグによるコストの警告

Project タグの値として設定できる値はホワイトリスト管理されており、AWS リソース管理の Terraform 移行 - クックパッド開発者ブログで説明されている Terraform 用のリポジトリにそのホワイトリストがあります。ホワイトリストの内容は以下のような素朴な改行区切りのテキストファイルで、このリポジトリの master ブランチにコミットが積まれたときに webhook を介してホワイトリストの内容を Costco の DB に取り込むようになっています。

Project タグがとりうる値のホワイトリスト

Project タグがとりうる値のホワイトリスト

新しくプロジェクトを立ち上げる際には、当然そのアプリケーションで利用する RDS インスタンスなどの AWS リソースが必要になります。リソースの追加はこの Terraform 管理用のリポジトリに pull request を出すことになるので、その pull request にホワイトリストに対する変更を含め、SRE がレビューするという自然なフローになっています。その結果、みだりに Project タグの値が増えることがなくなり、秩序を保つことができます。

もしホワイトリストに存在しない Project タグの値が検出されたり、どのカテゴリにも分類されていない値がある場合、Costco の画面に以下のように警告が表示されます。その場合にはメッセージの内容に従い、警告を消すようにします。

未知の Project タグの値や未分類の値が検出されたときの警告

未知の Project タグの値や未分類の値が検出されたときの警告

Project タグにメタデータをつけて管理するというアイデアは、dmemo の設計思想*3からインスパイアされたものです。時間の経過によって Project タグが持つコンテキストは徐々に失われてしまい、「このタグは何に利用されていたものだっけ?」となってしまうことがあります。そうならないよう、Costco では Project タグの値に対してコメントを書いたり非推奨フラグを付与することで、明示的に使われていない Project タグの値を区別できるようにしているのです。

予算の設定機能は AWS 謹製の AWS Budgets と役割が競合しているのですが、この Project タグの管理機能は Costco オリジナルなものであり、月次レポート機能と並んで個人的にとても気に入っている機能です。コスト最適化の作業の一環で、Project=management のような意味が広くて分類として機能していない Project タグの値を整理する作業を進めているのですが、タグごとの AWS リソースの検索機能を含め、とても役に立っている実感があります。

その他コストの管理に役立つ細かい機能

ここまで Costco のもっとも重要な二大機能である予算管理と Project タグ管理について説明しましたが、その他にもコスト管理に役立つ細かい機能を実装しています。

その一つの例として、以下のスクリーンショットのような Route 53 で購入したドメインの請求情報を一覧できる画面があります。

Route 53 で購入したドメインの請求一覧

Route 53 で購入したドメインの請求一覧

このように、コストを管理するためのコンソールが存在することで、日々のコスト関連のオペレーションを楽にするための機能も実装することができます。その結果、SRE の文脈でいうところのトイルやオーバーヘッドによる負荷を軽減することができます。

Costco を内製するまでに検討した別の手法との比較

コストの妥当性を説明するという目的を達成するためのアプローチとして、わたしたちは Costco を作って運用するという道を今のところは歩んでいます。とはいえ、道はそれだけではないはずです。ツールを内製するという判断をするまでに、別の手法も検討しました。

VS. AWS Budgets

機能の紹介で述べたように、Cost Explorer のフィルタを予算額と紐付けて管理するというコンセプトは AWS の Budgets サービスと役割が競合しているため、一見して車輪の再発明をしているように見えます。

しかしながら、現状の AWS Budgets では Cost Explorer で使えるフィルタを完全に再現できないという問題があります。具体的には、AWS Budgets ではフィルタに not 条件を含めることができません。

また、月次レポートをまとめる場合にも、何らかの Wiki のようなシステムに AWS Budgets の情報を転記する必要があって面倒です。Savings Plans の国内向けサービス側と国外向けサービス側の利用割合のような、社内固有の情報も AWS Budgets からは取得できないため、結局スクリプトで集計した結果を転記する必要があります。

AWS Budgets は強力なツールですが、残念ながらわたしたちのユースケースはそれでは満たせないことがわかっています。

VS. CUR & Tableau ダッシュボード

AWS のコスト関連の機能の一つに、コストと使用状況レポート (Cost and Usage Report: CUR)というものがあります。これは Cost Explorer が利用しているコストの生データに (おそらく) 近いもので、設定次第で任意の S3 バケットに出力することができます。

クックパッドの DWH チームの尽力により CUR が Redshift Spectrum で簡単にクエリできるように整備されており、社内の Tableau を利用することで CUR のデータをベースにしたダッシュボードを作れるようになっています。ただし、CUR のデータは良くも悪くも「生」なデータで、非常に多くのカラムがあり、それらを正しく組み合わせてダッシュボードを作ることは、まさに Tableau 職人の技といえるでしょう。

わたしは残念ながらその域には達していないことと、Route 53 のドメイン請求一覧の機能などは Tableau では実現できないため、Costco のようなウェブアプリケーションの形にしました。その他にも、CUR だけでは Cost Explorer API で取得できるようなコストの予測値は得られませんし、Cost Explorer API では簡単に得られる償却コストの計算も難しいです。

ただし、今後 Cost Explorer API では得られないような情報を Costco で扱いたくなった場合には、CUR のデータをもとに Redshift で集計して Costco に取り込む、というニーズが発生するかもしれません。また、Costco 上でグラフィカルに表示することが難しい場合に Tableau ダッシュボードにリンクするなど、うまく活用していく道があるかどうかは常に考えています。

まとめ

まず冒頭でインフラコストの妥当性と、それを評価するために何が必要なのかを説明しました。そして具体的な実装例として、クックパッドで内製している Costco というコスト管理コンソールを紹介し、それによって達成されることをご紹介しました。

Costco では、

  • 予算を Cost Explorer のフィルタと紐付けて管理し、月次レポートの一部を自動生成する機能
  • Project タグ (コスト配分タグのひとつ) にメタデータを付与して管理しやすくする機能
  • その他コストに関するオペレーションを楽にする機能

を実装することで、それらを使ってコストの妥当性を定期的に評価し、コストの状況に変化があった場合に「なぜその変化が起きたのか」ということを「説明」できる状態にすることを目指しています。また、エンジニアの枠にとらわれずに全社員が Costco を見ることで、誰もがなんとなくインフラのコスト状況がわかるようになり、経理などのバックオフィスのスタッフもコスト最適化の推進に巻き込んでいくこともねらいの一つです。

ともすれば、Costco のようなアプリケーションを開発することは、組織の規模感や状況によってはオーバーエンジニアリングとなるかもしれません。しかしながら、その根底にある考え方はあらゆる組織で役立つものだと信じています。たとえば、コスト配分タグの管理を Google スプレッドシートなどのツールで小さく始めてみるのも良いかもしれません。

これからインフラのコスト最適化に取り組んでいこうという方や、今まさに取り組んでいるという方に、この記事が届いて役立てていただければ幸いです。

*1:今では ALB に ECS によるターゲットグループを複数紐付けることで、このような構成にすることは簡単になりました。かつては ELB をもたないような ECS サービスを作り、Consul などのミドルウェアでサービスディスカバリを提供し、その情報をもとに NGINX などのウェブサーバからプロキシするというような構成を自作する必要がありました

*2:クックパッドにおける ECS の利用やそれを支える Hako エコシステムについて知りたい場合はhttps://techlife.cookpad.com/entry/2018/04/02/140846や  https://logmi.jp/tech/articles/320723が参考になります

*3:https://techlife.cookpad.com/entry/2016/08/08/103906

系列ラベリングによる NPS コメントのポジティブ・ネガティブ部分の抽出

$
0
0

こんにちは。研究開発部の深澤(@fukkaa1225)と申します。

クックパッドでは、顧客のロイヤルティを測る指標であるNPS(ネットプロモータースコア)のアンケートを毎月実施しています。 このNPSアンケートで集まってきたユーザの声(フリーコメント)は、クックパッドにとって大変貴重なものです。しかし、毎月多くの声が届くこともあり、担当者だけで目を通して集計するというのは難しくなってきました。そこで昨年、予め定義したカテゴリにコメントを自動で分類するシステムを構築し、既に稼働させています。 NPSアンケートを自動分類した話 - クックパッド開発者ブログ

このシステムによって「いただいたコメントが何を話題にしているか」はある程度自動的に把握できるようになりました。次に課題となったのは、例えば「このコメントはレシピの多さに関するものである。でもその中にはポジティブな部分とネガティブな部分が混じっている。これを分離できないか?」というものでした。

これはもちろん、人間であればコメントを見て容易に把握し、抽出できるでしょう。では、それを自動で行えるようにしたいとき、みなさんはどのような手段でこれを実現させるでしょうか。ルールベースだけでこうした抽出問題を解くのは骨が折れそうです。ここは機械学習の力を借りることにします。

本稿では、「このNPSコメントのどの部分がポジティブな記述で、どの部分がネガティブな記述なのか」を抽出するシステムの、機械学習モデルの実験について紹介します。

まとめ

  • あるコメントからポジティブ・ネガティブ部分を抽出する今回のタスクを、系列ラベリングと捉えて学習に必要なデータを作成。
  • CRF++、Bidirectional-LSTM、BERTをベースとしたモデルで実験。
  • sudachiで分かち書きし、学習済みword2vecにchiVeを用いたBidirectional-LSTMのモデルが最も高いF1値を記録した。しかし、CRF++と大きな差は見られなかった。
  • 引き続きエラー分析を行って、NPSコメントを業務改善に活かしていけるようなシステムの開発に努めていきます。

学習データを作ろう

さて、機械学習で取り掛かるぞということで、さっそく学習データを作っていきます。どんなデータがあればよいのかを考えてみます。

今回実現したい機能は

クックパッドはたくさんレシピがあってありがたいが、ありすぎて選びきれない時もあるというコメントに対して、

  • Positive: たくさんレシピがあってありがたい
  • Negative: ありすぎて選びきれない時もある

といったように、ポジティブ・ネガティブに紐づく表現を抜き出すことです。

このようなタスクは Sequence Labeling(系列ラベリング)Token Classificationなど色々な呼び方ができると思います。各形態素ごとに「この形態素は{ポジティブ・ネガティブ}な箇所の{始点・中間・終点}なのか」を分類する問題として捉えられるでしょう。

ということで、アノテーションは以下のようなデータを作ってもらうことにします。

クックパッドは#pたくさんレシピがあってありがたい#pが、#nありすぎて選びきれない時もある#n

ポジティブな箇所は #p、 ネガティブな箇所は #nで囲んでもらうようにします。アノテーションの体制は、2人のアノテータの方にそれぞれタグ付けをしていただいた上で、別の1人のアノテータの方にそれらの結果を統合してもらいます。これで統一性を確保するようにします。

実際に学習する際はこのタグ付けしてもらったデータをパースして、形態素ごとにBIOESラベル(e.g., BはBegin、IはInside、EはEnd、SはSingle、OはOther)を付与していきます。

B-positive たくさん
I-Positive レシピ
I-Positive が
I-Positive あって
E-Positive ありがたい

このようなルールのもとで、データを作っていってもらいました。最初に4,000、その後しばらく解析を進めながらもう一度ガッとタグ付けしていただいて、最終的に得られたデータ数は10,000コメント程度となりました。

どんなモデルでやるか

では、こうして得られたデータを使って抽出モデルを作成していきます。 まずはじめに考えるのはCRFですね。言わずと知れた系列ラベリングが得意なモデルです。これはCRF++をつかって容易にモデリングできます。

次に考えるのが、やはりディープラーニングを使う手法です。計算コストは当然CRFよりも高くなりますが、今回のような自然言語を扱うタスクにおいては十分な精度を出すことが期待されます。今回のタスクにおいては以下のようなアーキテクチャのモデルをベースとします。このモデルは[Lample+, 2016]で提案されたもので、インターンの学生の方が実装してくださいました。 単語をベクトルに変換するEmbedding層と文字列をBidirectional-LSTMでencodeする層を用意して、それらの出力値をconcatし、Bidirectional-LSTMに通すような構造です。 単語ベースの方は学習済みword2vecの重みを使います。

f:id:fufufukakaka:20200515101407p:plain
今回ベースとしたモデル[Lample+, 2016]

このモデルをベースとして、

  • 文字列のencoderをCNNにする
  • 学習済みword2vecで以下のものを試す
    • wikipediaコーパスで学習したもの
    • クックパッド手順コーパス(クックパッドに掲載されているレシピの手順を抜き出したもの)で学習したもの
    • ワークスアプリケーションズ徳島人工知能NLP研究所が公開している国語研日本語ウェブコーパスで学習したもの(chiVe)
  • Bidirectional-LSTMをtransformerに置き換える
  • tokenizerをsudachiにし、形態素を正規化する

といったモデルを試していきます。

また、これに加えてやはり外せないだろうということでBERTも実験対象に加えます。 ベースのモデルはhuggingfaceに東北大の乾・鈴木研究室が提供している bert-base-japanese-whole-word-maskingを利用します。

バリエーションとしては以下の2つです。

  • BERT論文にならって、BERTから得られるtokenごとの出力値をそのまま使いfine-tuningする
  • 最終層にCRFを入れてfine-tuningを行う(BERT論文ではCRFは入っていなかった)

これは個人的な経験なのですが、自分が担当したタスクでBERTを用いて勝てたことがなかなかなく、今回も祈るような気持ちでBERTにトライしました。

まとめると以下の3パターンのモデルで実験を行います。

  • CRF++
  • 文字列encode+単語encode{by 学習済みword2vec} → Bidirectional-LSTM
    • 学習済みword2vecやtokenizerで何を選ぶか、LSTM層をtransformerにするか否かなどのバリエーションあり
  • BERT
    • 最終層にCRFをつけるかどうか

実験の管理

さてここで少し本筋から外れますが、僕がどのようにこれらの実験を管理していたかについて述べたいと思います。

僕は実験のパラメータをyamlで管理するのが好きです。いつもだいたい以下のようなyamlを用意しています。

{実験名}:
  char_encode: LSTM
  transformer_encode:Falsechar_lstm_layer:1lstm_layer:1char_embedding_dim:50lstm_char_dim:25word_embedding_dim:300lstm_dim:100crf_drop_out:0.5lstm_drop_out:0.0tokenizer: sudachi
  normalized_token:Trueembed_path: /work/cache/chive-1.1-mc5-20200318.txt

python src/run_experiment.py --exp_name={実験名}

そしてこれを実験名を引数に入れたらyaml内の変数を展開してくれるwrapperを経由して、実験コードを流すようにしていました。出力結果も{実験名}_{日時}.logのような名前にすることで、同じ実験名での結果だということがわかりやすくなるようにしています。

yamlで設定を記述することで、パラメータの調整をする際に実験コードそのものに手を入れる必要がなくなります。設定ファイルだけで済むのはとても気楽で、そういった理由からここ数年は個人的にこのスタイルでやっています。

見通しも、ひたすらargparseclickの引数に渡しつづけるよりも良くなっているような気がします。モデルのtokenizerなのか単なるパラメータなど含めて書こうと思えば階層的に書ける点も好きです。

最近だとfacebookが出しているHydraなどもあり(今回は使っていませんでした)、yamlでパラメータ管理するのがどんどん楽になっており、ありがたいですね。

実験結果と考察

以上のような過程を踏みつつ、実験を行いました。得られた結果の中から主要なものを以下に表で示したいと思います(いずれのF1値もmicro-average)。

Name all_f1 negative_f1 positive_f1
Bidirectional-LSTMによる文字列encode・単語embed:chiVe→Stacked-Bidirectional-LSTM(tokenizer: sudachi) 0.609 0.4495 0.6717
CRF++ 0.6024 0.4839 0.6438
CNNによる文字列encode・単語embed:クックパッド手順コーパス→Stacked-Bidirectional-LSTM(tokenizer: mecab) 0.5607 0.3977 0.6181
Bidirectional-LSTMによる文字列encode・単語embed:クックパッド手順コーパス→transformer(tokenizer: mecab) 0.5129 0.3439 0.5695
Bidirectional-LSTMによる文字列encode・単語embed:クックパッド手順コーパス→Stacked-Bidirectional-LSTM(tokenizer: mecab) 0.5066 0.3102 0.5751
Bidirectional-LSTMによる文字列encode・単語embed:wikipedia→Stacked-Bidirectional-LSTM(tokenizer: mecab) 0.4898 0.351 0.5308
BERT-with-CRF 0.419 0.248 0.5
BERT 0.3843 0.2074 0.4734

chiVeを用いて最終層をStacked Bidirectional-LSTMにしたモデルが最も高いF1値を記録しました。しかしCRF++が想定以上によい結果を出しており、両者の差はほとんどないという結果になっています。

両者にあまり大きな差がないことから、いくつかの可能性が考えられます。今回採用したニューラルネットのモデルがBidirectional-LSTMを多用する計算コストの高いものであることから、恐らくデータ数が十分でなかった可能性が高いと現在は考えています。

BERTに関しては、なにかミスがあったのかなというくらいに低い結果となってしまいました。前述したようにBERT単体では相性が悪いのかもしれません。BERTにCRF層を加えたものでF1値の増加は確認できるので、全く機能していないというわけではないと思われますが、なにか根本的な改善が求められているということに変わりはなさそうです。引き続きBERTの勝利を願ってエラー分析をしていきたいと思っております。

ポジティブなコメント、ネガティブなコメント、それぞれのF1値に目を向けてみるとポジティブなコメントの抽出精度はどの手法でもネガティブなコメント抽出の精度よりも高くなっています。これは学習データにおけるラベルの不均衡に要因があると考えています。データの中でポジティブとしてタグ付けがされたのが7,218箇所あったのに対し、ネガティブとしてタグ付けが行われたものは2,346箇所と大きく差が開いていました。データ数が十分でなくネガティブに関するモデルの学習がうまく進まなかったことが考えられます。

最後にCRF++とchiVeを用いたStacked Bidirectional-LSTMの二者に絞ってエラーだった予測結果をいくつか見てみたいと思います。 基本的に短い文章でポジティブかネガティブのどちらかだけ出現するときはよく正解します。対照的に、長い文章・ポジティブとネガティブの両方出現するときに間違っていることが散見されました。

f:id:fufufukakaka:20200515101701p:plain
前半のポジティブな記述を取れていない例

こちらの例ではどちらも前半の「いいと思う」が取れていませんが、後半は捉えられています。

f:id:fufufukakaka:20200515101750p:plain
Stacked Bidirectional-LSTMが前半の記述を取れていない例

こちらは、CRF++のみが前半の「料理初心者だったためとても重宝している」がとれています(ただし、短めにとっています)。

f:id:fufufukakaka:20200515101811p:plain
CRF++が範囲を短めに取っている例

上の例と同じミスとして、CRF++が短めに範囲を捉えているケースがいくつかありました。

出来上がったシステムの全体像

さて、こうして作成された抽出モデルによって、NPSを解析するシステム全体は現在以下のような状態になっています。

f:id:fufufukakaka:20200515101828p:plain
NPSを解析するシステムの簡略図

毎月のNPS実施に合わせてコメント抽出・カテゴリ分類バッチが起動します。それらコメントはカテゴリごとに関連するslackチャンネルに通知されます。また解析結果は、NPSに関する数値を統合的に取り扱うために開発されているダッシュボードに取り込まれ、視覚的に分かりやすい形で残るようになっています。

今後について

NPSに対する解析は、ユーザの方々からの貴重なご意見を業務に役立てていく上で非常に重要なことであると感じています。より正確に、そして迅速に意見を取り込んでいけるように、引き続き自動解析システムの発展に努めていく所存です。

Viewing all 734 articles
Browse latest View live