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

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

Cookpad Online Summer Internship 2020 を開催します!

$
0
0

f:id:sankichi92:20200519200452j:plain

エンジニア新卒採用を担当しているユーザー・決済基盤部の三吉です。

クックパッドでは、毎年恒例のサマーインターンシップを今年も開催します! スプリングインターンシップに続き、新型コロナウイルスの影響をふまえ、サマーインターンシップもオンラインでの開催です。

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

5 Day Engineer コース

エンジニア向けは、プラットフォーム別に以下の3コースを用意しました。

  • Webアプリ開発コース
  • Androidアプリ開発コース
  • iOSアプリ開発コース

前半2日間は講義・ハンズオン形式です。 クックパッドのアプリケーション開発手法や、サービス開発のノウハウについて学びます。 後半3日間は実践です。 社員エンジニアのサポートのもと、各自でテーマに沿ったアプリケーションを開発します。

昨年の 10 Day Tech コース と比べると期間は短くなっていますが、プラットフォーム別にコースを分けて領域を絞ることでグッと密度を上げています。 また、今年は以下の日程で2回開催予定なので、より参加しやすくなっています。

  • 日程A: 8/24(月) 〜 8/28(金)
  • 日程B: 9/7(月) 〜 9/11(金)

昨年の様子については、以下の記事をご覧ください。

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

インターンシップには、参加者各自の PC から Zoom や Slack 等を利用してオンラインで参加していただく予定です。 すでにスプリングインターンシップや各種勉強会、イベントをオンラインで開催しており、そこで得た知見をサマーインターンシップに活かすべく準備しています。

3 Day Product Designコース

デザイナー向けのコースもあります。 こちらは、デザインツール Figma を使って、クックパッドのデザインプロセスを講義と実習を通して学ぶコースになります。 以下の日程で開催予定です。

  • 8/8(土) 〜 8/10(月・祝)

詳細はインターンシップ特別サイトをご覧ください。


参加してくださる学生の皆さまのため、サマーインターンシップには毎年会社を挙げて取り組んでいます。 オンラインでの開催は初めてですが、オンラインならではの工夫を凝らしたいと考えています。

また、サマーインターンシップは5日間の短期ですが、長期の就業型インターンシップも通年で募集しています。 興味のある方は、以下のページからご応募ください。 https://cookpad.wd3.myworkdayjobs.com/ja-JP/new_grad

学生の皆さまのご応募をお待ちしています!


KPI 設定の難しさについての思索とそれに付随した細かな考察

$
0
0

こんにちは、事業開発部でプロジェクトマネージャー兼エンジニアをやっている新井(@SpicyCoffee)です。10 万円の申請書を書く前に 20 万円のパソコンを買いました。

クックパッドでは、毎日の料理を楽しみにするべく日々サービス開発がおこなわれています。本稿では、サービス開発の中でも重要かつ難解な「KPI の設定」について、私がプロジェクトマネージャーとして普段考えていることや注意している点を紹介します。

KPI を決めるのは難しい

サービス開発において KPI を設定し、それを改善するような施策や検証を繰り返していくことは基本中の基本です。しかしながら、現実には「KPI を設定する」という行為自体の難易度が非常に高く、日夜頭を悩ませている開発者のみなさんも多いのではないでしょうか。 以下では、その要因の一つである「KPI は複数の要件を満たす必要がある」ことについて考えます。

満たすべき要件

具体的にどういった要件を満たす必要があるかはケースによって変わることもあると思いますが、私は普段以下の3点を KPI が満たすべき要件として考えています。

  • ユーザー体験を表現する指標であること
  • 事業の収益に繋がる指標であること
  • 自分たちの施策で 動かすことが可能な指標であること

ユーザー体験を表現する指標であること

KPI を設定する行為は取り組むべき課題の設定であり、すなわちサービスの中で自分たちが次に改善するべきポイントを定義する行為であるとも言えます。このことから KPI は、その指標を改善することで なぜサービスを利用するユーザーの体験が向上するのかを説明できるものでなくてはなりません。

KPI を改善する方法は複数ある中で、この部分の論理立てが不十分なままプロジェクトを進めると「KPI はよくなるがユーザー体験は悪くなっている」というような方法を選んでしまうことにも繋がります。たとえば EC サイトの検索結果において「SEO に効果がある → "ページ閲覧数/セッション"を KPI に据えましょう」という意思決定をするのと、「複数の商品を見比べてもらうことで、ユーザーは真に満足する買い物をすることができるという仮説がある → "ページ閲覧数/セッション"を KPI に据えましょう」という意思決定をするのとでは、結果として設定した KPI が同じであっても意味合いが大きく異なります。

極端な例ではありますが、前者では "ページ閲覧数/セッション"を伸ばすために「1ページ辺りに表示される商品数を少なくする」といったような「指標しか見ていない施策」が実施される可能性が高くなります。一方、後者のようにユーザー体験と KPI を紐付けておくことで、例に上げたような施策はメンバーから異議が唱えられる可能性が高くなり、実施されづらくなります。このことは、ユーザー体験の担保が施策の実行に置いて一種の制約条件のように働いていると捉えることができるかもしれません。

事業の収益に繋がる指標であること

ユーザー体験と同様に重要になってくるのが、その指標を改善することで 事業の収益にいい影響を与えることができるか、そしてその規模は十分かという観点です。

長期間に渡ってユーザー体験をよくし続けるためには、その源泉となる収益を得ることについても必ず考えなければなりません*1。どれほど質の高いサービスや機能を提供できたとしても、それが収益につながらなければ継続的に改善を続けることは難しく、結果として機能を落としたりその領域から撤退したりすることになってしまいます。

したがって KPI は、現状存在しているマネタイズ方法に繋がるか、新しくマネタイズ方法を定義しそこに繋がるものである必要があります。

自分たちの施策で動かすことが可能な指標であること

当然のことではありますが、KPI は自分たちで 観測・改善ができるものでなくてはなりません。それを担保するためのポイントとして「実装・集計の容易さ」と「外部要因の少なさ」があげられます。

前者については、実際にログを仕込んだり集計をする作業が自分たちが持っているリソースで可能かどうかを考える必要があります。たとえば、サービスのログ基盤が大量のログを収集・加工するのに十分なほど整っていないのであれば、複数の画面操作を組み合わせたような複雑な指標は避けるべきでしょう*2。プロジェクトに与えられた時間が3ヶ月なのであれば「一ヶ月後の再利用率」のような観測に時間のかかる指標は避けた方が無難です。

後者については、KPI の変動要因が多すぎないかどうかを考える必要があります。たとえば、施策を打っていない状態で A/A テストをした結果に差があるような指標は、平時からの変動が激しすぎる(≒多くの外部要因がある)と考えて避けるべきです。また、一般的には指標を実現するために必要な操作が多くなるほど、ノイズに近い離脱・誤操作が発生しやすくなり、施策を打ったときの効果が見えづらくなってしまう傾向にあります。

どう考えるべきか

以上に挙げたポイントを満たすような KPI を設定するのは非常に難しく、どこから手を付ければいいかわからなくなることも多々あります。しかし、一段抽象化して考えれば、これは複数の制約条件を持つ問題をいかに解くかという話になります。一般に複数の制約を満たす必要がある問題を考える際には、制約の最も厳しい条件から考えた方が後の手戻りが少なくなります。したがって、まず最初に手を付けるべきは、自分たちの環境において どの条件が最も厳しい制約かを考えることです。

「複数の制約を満たす」と聞くといわゆるベン図が頭に浮かぶ人も多いかもしれません。たとえば『ビジョナリー・カンパニー2』には、「情熱をもって取り組めるもの」「自社が世界一になれるもの」「経済的原動力になるもの」のすべてを満たすコトを対象にビジネスをしなさいと書かれており、その図解として以下のベン図が掲載されています。

f:id:spicycoffee:20200612162408p:plain
『ビジョナリー・カンパニー2 飛躍の法則』より作成

私自身この考え方は好きで似たような図もよく頭に思い浮かべますが、一つだけ疑問があるのは「この円のサイズは果たして本当におなじなのだろうか?」という点です。そして上記の主張をこの疑問に沿って捉え直すと、「まずは 最も小さな円について考えよう」という話になります。

たとえば、すでに多くの機能を持ち、提供できるユーザー体験やそれを表現するための指標は大量に考えつくが、マネタイズの方法自体はそれほど多くないような大規模サービスでは下図左側のような図になります。この場合はまず事業の収益に繋がる指標をいくつか思い浮かべ、 その指標をよくしながらユーザー体験を向上させるにはどうすればいいか?という考え方をした方がよいでしょう。
逆に立ち上げたばかりのサービスでは、実現するべきユーザー体験はこれと決まっているが、マネタイズの方法については模索中で多くの可能性があり、ベン図は右側のような図になるかもしれません。この場合は、ユーザー体験を表現する指標をまず設定し、 その指標をよくすることで収益を上げるにはどうすればいいか?という考え方をするのがよさそうです。

f:id:spicycoffee:20200612162400p:plain

このように、組織や置かれている状況や個人の知識・経験によってそれぞれの円の大きさが変化する中で、最も小さな円=取りうる選択肢の少ない円についての要件から満たすように KPI を考えることで、すべての制約を満たした指標を設定しやすくなるのではないでしょうか。

またこれは、裏を返すと KPI や取り組むべき課題を設定する際には 円の一番小さいところから考えざるを得ないということでもあります。つまり、たとえば先にあげた大規模サービスの例においてよりよいユーザー体験を作りたいなら、逆説的に一番小さな収益性の円を大きくする必要があるのです。これは個人の行動にするとたとえば書籍等からビジネスに関する知識を得たり、組織の置かれている状況について情報を収集したりして、収益に繋げる方法を新たに発見するといった行為になります。ログ基盤が整っていないせいで実現可能性の円が小さいのであれば、ログ基盤を整えることで取りうるユーザー体験の選択肢が広がるということです。これは、サービス開発に技術力が必要になる証左でもあります。

f:id:spicycoffee:20200612162404p:plain

施策を実行する際の注意点

ここまでの話は KPI の設定について述べたものでした。ここからは、私が実際に実施する施策の中で指標に関して注意している以下の 3 点について述べます*3

  • KPI そのものも改善サイクルの中で変化しうる
  • 施策で追う指標は3点セットで設定する
  • 施策の採用ラインは必ず事前に設定する

KPI そのものも改善サイクルの中で変化しうる

KPI そのものも絶対に不変のものであるわけではありません。KPI の設定が課題の設定と密になっている以上、事業を取り巻く環境や自分たちのサービスに対する理解が変化する中で取り組むべき課題そのものが変化し、KPI を変更した方がよい可能性があることは頭に入れておくべきです。
もちろん中長期で改善を進めていく指標として設定する以上、あまりにコロコロ変化するのは好ましくありませんが、時には「この KPI は本当に追うべきなんだろうか(=この課題を本当に解決すべきなのだろうか)」という思考を持つことも重要です。特にプロジェクトが発足してすぐのタイミングでは、先にあげた3条件に対する理解がチームの中でも不十分な可能性が高く、施策を重ねる中でその精度を上げた結果 KPI が変化することはよくあることかと思います。

施策で追う指標は3点セットで設定する

実際に KPI を改善するために施策を実施する際には、観測する指標を以下の3点セットで設定するようにしています。

  • KPI
  • 機能利用率
  • 副作用指標

KPI

設定した KPI です。

機能利用率

施策の意図が実現できているかを確認するために、実装した 機能が実際に利用されているかが確認できる指標を設定します。たとえば「直帰率」という KPI を設定し、その改善のために LP に新しいコンテンツを設定した場合、そのコンテンツのタップ率等を設定することになります。この指標を確認しなかった場合、KPI が動いたとしてもそれが意図したユーザー体験の変化によるものであるということが担保できなくなってしまいます。

副作用指標

実施した施策によって 既存コンテンツに影響を与える可能性がある場合、その影響も観測する必要があります。先にあげた直帰率を改善するためのコンテンツの例であれば、その LP にもともと存在していた別導線のタップ率等を設定することになります。この指標を設定しなかった場合、意図したとおりに KPI が改善できたとしても「他の指標が悪化してしまい事業全体としてはマイナスになってしまっていた」というケースに気がつけなくなってしまいます。

施策の採用ラインは必ず事前に設定する

それぞれの指標がどの程度の数値になったときに 施策を採用するのかという目安の数字は必ず事前に設定します。事後になってから議論しようとすると、せっかく作ったのだから採用したい気持ちが勝ってしまったり、最悪の場合ロクな議論もなく施策が採用されたりすることになりかねません。むやみやたらに機能を増やしてもユーザーの混乱を招くことに繋がるため、施策の採用については慎重になるべきであり、そのためにも事前に期待される効果等から採用ラインを設定することには大きな意味があります。
加えて言うと PM の立場であれば、施策が成功したときと失敗したときのそれぞれで次にどういった手を打つのかということも事前に想定しておく必要があります。

終わりに

冒頭にも述べましたように、この記事は私が実際に KPI を設定したり、それに基づいて施策を実施したりする際に注意している点をまとめたものです。サービス開発についての知見はその性質上「絶対の正解」が存在せず、また、それゆえに明文化されることが少ないものでもあると思います。私自身にとっても、この記事は「言語化しづらい思考を明文化して残す」という挑戦の一つであったのですが、これがみなさんのサービス開発の参考になればうれしいです。

クックパッドでは、このようにサービス開発について考えを巡らせながら、自分で手を動かして実際に開発を進めることのできるエンジニアを大募集しております。興味のわいた方や、この記事の内容について話がしたい!と感じた方はぜひ気軽に声をかけていただければと思います。

採用ページ: https://info.cookpad.com/careers/

*1:事業が投資フェーズであり、会社全体としては別事業の収益でバランスを取っているケースなどは別です

*2:あなたがエンジニアであれば自ら基盤を整える選択肢を取ることもできます

*3:後半2つについては以前書いた記事でも触れているのでよければ合わせてお読みください → https://techlife.cookpad.com/entry/2018/02/10/150709

iOSでモダンなカスタムボタンを作ってみよう

$
0
0

お久しぶりです。モバイル基盤部のヴァンサン(@vincentisambart)です。

iOS標準のボタンクラスUIButtonが10年前に作られたものであって、当時存在していなかったAuto LayoutやDynamic Typeとの相性がよくありません。

Auto Layout、Dynamic Type、複数行表示、を活用するカスタムなボタンクラスを作ってみれば少し勉強になるかもしれません。

因みにDynamic Typeはあまり使われていない機能だと思われることがあるようですが、気になって調べてみたら、クックパッドのiOSアプリのユーザーの中で、3分の1がシステム標準でない文字サイズを使っていました。その半分が標準より小さい設定を使っていて、もう半分が標準より大きい設定を使っています。「さらに大きな文字」を有効にすると選べる「アクセシビリティサイズ」を使っているユーザーは全ユーザーの1%未満でした。

まずはシンプルに

ボタンを作るとき、適切な親クラスを考えるとき、UIButtonが最初に頭に浮かぶかもしれません。しかし、UIButtonの標準のサブビュー(titleLabelimageView)の配置はAuto LayoutやUIStackViewを活用できませんし、ボタンに別のUILabelを入れるとUIButton標準のtitleLabelも残っていて分かりにくいと思います。

UIButtonの代わりにその親クラスであるUIControlを使ってみましょう。実は、UIButtonに期待されている挙動の多くはUIControlがやってくれます。

カスタムボタンベースは以下のコードでいかがでしょうか。

publicfinalclassMyCustomButton:UIControl {
    privatestaticletcornerRadius:CGFloat=4privatelettitleLabel= UILabel()

    privatefuncsetUp() {
        // ユーザーの文字サイズの設定によってサイズの変わるフォントを使います// `UIFont.preferredFont(forTextStyle:)`の代わりに`UIFontMetrics.default.scaledFont(for:)`を使っても良いです
        titleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
        // Dynamic Typeの設定が変わるたびに、上記のフォントのサイズを新しい設定に合わせてほしいです。// 自動調整を有効にするには、この指定だけでなくフォントを`UIFont.preferredFont(forTextStyle:)`または`UIFontMetrics.default.scaledFont(for:)`で作成する必要があります。
        titleLabel.adjustsFontForContentSizeCategory =true
        
        titleLabel.numberOfLines =0// 行数制限なし
        titleLabel.textAlignment = .center

        // titleLabelがボタン全体を覆うように
        titleLabel.translatesAutoresizingMaskIntoConstraints =false
        addSubview(titleLabel)
        titleLabel.topAnchor.constraint(equalTo:topAnchor).isActive =true
        titleLabel.bottomAnchor.constraint(equalTo:bottomAnchor).isActive =true
        titleLabel.leadingAnchor.constraint(equalTo:leadingAnchor).isActive =true
        titleLabel.trailingAnchor.constraint(equalTo:trailingAnchor).isActive =true// 角丸を忘れず
        layer.cornerRadius =Self.cornerRadius
        clipsToBounds =true// 色をつけておく
        backgroundColor = .orange
        titleLabel.textColor = .white
    }

    publicoverrideinit(frame:CGRect) {
        super.init(frame:frame)
        setUp()
    }

    publicrequiredinit?(coder:NSCoder) {
        super.init(coder:coder)
        setUp()
    }

    publicvartitle:String {
        get {
            titleLabel.text ??""
        }
        set {
            titleLabel.text = newValue
        }
    }
}

実行してみると以下のようになります。

f:id:vincentisambart:20200616071827p:plain:w320

上記のコードだけでも、addTargetを使ってみればちゃんと動きます。ただし、ボタンを押すとタッチフィードバックがないので改善が少し必要です。

色変更

ボタンの色は押されているかどうかだけではなく、無効(disabled)になっているかどうかでも色が変わります。 色に影響ある状態を表現するためのenumを用意しておきましょう。

// `UIControl.State`と違って、この`enum`にはこのボタンの表示に影響ある状態しか入っていません。privateenumDisplayState {
    case disabled
    case enabled
    case enabledHighlighted
}

privatevardisplayState:DisplayState {
    // `isEnabled`と`isHighlighted`は`UIControl`の標準のプロパティです。if isEnabled {
        if isHighlighted {
            return .enabledHighlighted
        } else {
            return .enabled
        }
    } else {
        return .disabled
    }
}

その状態によって色を変えたいので、色を変えてくれるメソッドを用意しておきましょう。 以下のコードは選んだ色がちょっと適当ですし、文字や背景の色だけではなく、ふちの色も変えても良いかもしれないので、見た目に関してデザイナーに相談しても良いかもしれません。

privatefuncupdateColors() {
    lettextColor:UIColorletbackgroundColor:UIColorswitch displayState {
    case .disabled:
        textColor = .white
        backgroundColor = UIColor.white.darkened
    case .enabled:
        textColor = .white
        backgroundColor = .orange
    case .enabledHighlighted:
        textColor = UIColor.white.darkened
        backgroundColor = UIColor.orange.darkened
    }

    self.backgroundColor = backgroundColor
    titleLabel.textColor = textColor
}

因みに上記のdarkenedの定義は以下の通りです。もっと正しい計算があるかもしれませんが、ここはこれで十分でしょう。

privateextensionUIColor {
    vardarkened:UIColor {
        letdarkeningRatio:CGFloat=0.9varhue:CGFloat=0varsaturation:CGFloat=0varbrightness:CGFloat=0varalpha:CGFloat=0if getHue(&hue, saturation:&saturation, brightness:&brightness, alpha:&alpha) {
            return UIColor(
                hue:hue,
                saturation:saturation,
                brightness:brightness* darkeningRatio,
                alpha:alpha
            )
        } else {
            returnself
        }
    }
}

updateColors()を用意するだけではなく、正しいタイミングで呼ぶ必要もあります。 setUp()の最後で呼ぶのはもちろん、状態が変わるタイミングでも呼んでおきましょう。

publicoverridevarisHighlighted:Bool {
    didSet {
        updateColors()
    }
}

publicoverridevarisEnabled:Bool {
    didSet {
        updateColors()
    }
}

ボタンが押されている間に色が変わるようになりました。

f:id:vincentisambart:20200616071836p:plain:w320

ボタンが無効のときも色がちゃんと変わります。

f:id:vincentisambart:20200616071841p:plain:w320

サブタイトルと余白

タイトルだけではなく、サブタイトルも追加しておきましょう。そしてその周りに余白を入れておきましょう。

privatelettitleLabel= UILabel()
privateletsubtitleLabel= UILabel()
// シンプルさのためにinsetsを固定にしてあるが、変えられるようにした方が良さそうprivatestaticletinsets= NSDirectionalEdgeInsets(
    top:5,
    leading:5,
    bottom:5,
    trailing:5
)

privatefuncsetUp() {
    // ユーザーの文字サイズの設定によってサイズの変わるフォントを使います// `UIFont.preferredFont(forTextStyle:)`の代わりに`UIFontMetrics.default.scaledFont(for:)`を使っても良いです
    titleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
    titleLabel.adjustsFontForContentSizeCategory =true
    subtitleLabel.font = UIFont.preferredFont(forTextStyle: .subheadline)
    subtitleLabel.adjustsFontForContentSizeCategory =true

    titleLabel.numberOfLines =0// 行数制限なし
    titleLabel.textAlignment = .center
    subtitleLabel.numberOfLines =0// 行数制限なし
    subtitleLabel.textAlignment = .center

    letverticalStackView= UIStackView()
    verticalStackView.axis = .vertical
    verticalStackView.alignment = .center
    verticalStackView.translatesAutoresizingMaskIntoConstraints =false
    addSubview(verticalStackView)
    // 左右上下の制約にinsetsの値を活用しても良いのですが、今回はUIStackView.directionalLayoutMarginsを使ってみました
    verticalStackView.topAnchor.constraint(equalTo:topAnchor).isActive =true
    verticalStackView.bottomAnchor.constraint(equalTo:bottomAnchor).isActive =true
    verticalStackView.leadingAnchor.constraint(equalTo:leadingAnchor).isActive =true
    verticalStackView.trailingAnchor.constraint(equalTo:trailingAnchor).isActive =true// stack view内に余白を少し入れておきます
    verticalStackView.isLayoutMarginsRelativeArrangement =true
    verticalStackView.directionalLayoutMargins =Self.insets

    verticalStackView.addArrangedSubview(titleLabel)
    verticalStackView.addArrangedSubview(subtitleLabel)

    // stack viewのおかげで隠れたビューがスペースをとりません
    subtitleLabel.isHidden =true

    layer.cornerRadius =Self.cornerRadius
    clipsToBounds =true

    updateColors()
}

publicvarsubtitle:String {
    get {
        subtitleLabel.text ??""
    }
    set {
        subtitleLabel.text = newValue
        subtitleLabel.isHidden = newValue.isEmpty
    }
}

もちろんupdateColors()の最後にsubtitleLabelの色の更新も必要ですね。

subtitleLabel.textColor = textColor

f:id:vincentisambart:20200616071851p:plain:w320

タップ反応

見た目は大丈夫そうに見えるが、試してみたら、なぜかタップするとき反応しなくなりました…

実は、タップはverticalStackViewが全部受け取るようになりました。タップがボタン自体にたどり着きません。 以前動いていたのはUILabelisUserInteractionEnabledが標準でfalseだからです。UIStackViewはシンプルなUIViewのようにisUserInteractionEnabledが標準でtrueです。

setUp()の中で以下の1行を入れておけば上手く動くようになります。

verticalStackView.isUserInteractionEnabled =false// タッチイベントはこのボタンまで来てほしい

このボタンの中のタップが全部ボタンにたどり着いてほしいので、stackView.isUserInteractionEnabled = falseが良いのですが、UIStackViewの中のものにたどり着いてほしければ使えません。

これでボタンがちゃんと動くはずです。あとはレイアウトは自分のニーズに合わせて色々できます。

UIButtonを使わないおかげで、不要なサブビューが作られることはないが、UIButtonがやってくれて、UIControlがやってくれない機能を失ってしまう。その機能の1つがアクセシビリティです。

アクセシビリティ

アクセシビリティとは利用しやすさ、もっと多くの人がもっと多くの状況でアプリを使えるのを目指すことだと言っても良いのかな。今の自分がアプリを問題なく使えたとしても、メガネのない時の自分、30年後の自分、自分の親戚、にはアクセシビリティ機能が必要かもしれません。

上記のコードにadjustsFontForContentSizeCategory = trueが入っていて、Dynamic Typeというアクセシビリティ機能の一つを既に活用しています。

でもVoice Overなど、画面の中身を見て操作できるアクセシビリティ機能にとって、各ビューがどういうものなのか、どういう風に使えるのか、知るすべが必要です。

上記のコードのままだと一応Voice Overで操作はできるけど、「ボタン」として認識されていないので、操作できることに気づかれないかもしれません。

今回、アクセシビリティ対応は難しいことではありません:

  • 標準のUIControlが「accessibility element」ではないので、アクセシビリティ機能に無視されてしまいます。isAccessibilityElement = trueで認識されるようになります。
  • このビューがボタンであることをaccessibilityTraits = .buttonでシステムに伝えましょう。
  • isAccessibilityElement = trueをやったことで、Voice Overが中に入っているUILabelを音読しなくなるので、accessibilityLabelでボタンの中身を伝えましょう。\ 因みにUIButtonがaccessibility elementなので、UIButtonの中にUILabelを入れるときも同じ問題が起きます。
  • ボタンに画像しか入っていないときでも、何をやるボタンなのか分かるすべがないのでaccessibilityLabelにひとことを入れておきましょう。

以下のようになります。

isAccessibilityElement =truevaraccessibilityTraits:UIAccessibilityTraits= .button
if!isEnabled {
    accessibilityTraits.insert(.notEnabled)
}
self.accessibilityTraits = accessibilityTraits
accessibilityLabel = [title, subtitle].filter { !$0.isEmpty }.joined(separator:"\n")

もちろん上記のコードはtitlesubtitleisEnabledの変更時に呼んで情報を更新する必要がありますね。

最後に

iOSクックパッドアプリでは、このボタンの拡張したバージョンが一部の画面で使われています。 作った時、細かいところいくつかに引っかかったので、この記事が少しでも役に立っていただければと思って書いてみました。

iOSクックパッドアプリのDynamic Type対応はまだ対応していない画面がまだありますが、少しずつ改善していこうとしています。

すべてのコードを以下にまとめておきました。このコードをご自由に自分のアプリにお使いください。\ 必要であれば、ライセンスがないと困る人のためにちゃんとしたライセンスも入れておきました。

// This project is licensed under the MIT license.// // Copyright (c) 2020 Cookpad Inc.// // Permission is hereby granted, free of charge, to any person obtaining a copy// of this software and associated documentation files (the "Software"), to deal// in the Software without restriction, including without limitation the rights// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell// copies of the Software, and to permit persons to whom the Software is// furnished to do so, subject to the following conditions:// // The above copyright notice and this permission notice shall be included in// all copies or substantial portions of the Software.// // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN// THE SOFTWARE.publicfinalclassMyCustomButton:UIControl {
    privatestaticletcornerRadius:CGFloat=4privatelettitleLabel= UILabel()
    privateletsubtitleLabel= UILabel()
    privatestaticletinsets= NSDirectionalEdgeInsets(
        top:5,
        leading:5,
        bottom:5,
        trailing:5
    )

    privatefuncsetUp() {
        // ユーザーの文字サイズの設定によってサイズの変わるフォントを使う// `UIFont.preferredFont(forTextStyle:)`の代わりに`UIFontMetrics.default.scaledFont(for:)`を使っても良いです
        titleLabel.font = UIFont.preferredFont(forTextStyle: .headline)
        titleLabel.adjustsFontForContentSizeCategory =true
        subtitleLabel.font = UIFont.preferredFont(forTextStyle: .subheadline)
        subtitleLabel.adjustsFontForContentSizeCategory =true

        titleLabel.numberOfLines =0// 行数制限なし
        titleLabel.textAlignment = .center
        subtitleLabel.numberOfLines =0// 行数制限なし
        subtitleLabel.textAlignment = .center

        letverticalStackView= UIStackView()
        verticalStackView.axis = .vertical
        verticalStackView.alignment = .center
        verticalStackView.isUserInteractionEnabled =false// タッチイベントはこのボタンまで来てほしい
        verticalStackView.translatesAutoresizingMaskIntoConstraints =false
        addSubview(verticalStackView)
        // 左右上下の制約にinsetsの値を活用しても良いのですが、今回はUIStackView.directionalLayoutMarginsを使ってみました
        verticalStackView.topAnchor.constraint(equalTo:topAnchor).isActive =true
        verticalStackView.bottomAnchor.constraint(equalTo:bottomAnchor).isActive =true
        verticalStackView.leadingAnchor.constraint(equalTo:leadingAnchor).isActive =true
        verticalStackView.trailingAnchor.constraint(equalTo:trailingAnchor).isActive =true// stack view内に余白を少し入れておきます
        verticalStackView.isLayoutMarginsRelativeArrangement =true
        verticalStackView.directionalLayoutMargins =Self.insets

        verticalStackView.addArrangedSubview(titleLabel)
        verticalStackView.addArrangedSubview(subtitleLabel)

        // stack viewのおかげで隠れたビューがスペースをとりません
        subtitleLabel.isHidden =true

        layer.cornerRadius =Self.cornerRadius
        clipsToBounds =true

        updateColors()
        updateAccessibility()
    }

    privateenumDisplayState {
        case disabled
        case enabled
        case enabledHighlighted
    }

    privatevardisplayState:DisplayState {
        if isEnabled {
            if isHighlighted {
                return .enabledHighlighted
            } else {
                return .enabled
            }
        } else {
            return .disabled
        }
    }

    privatefuncupdateColors() {
        lettextColor:UIColorletbackgroundColor:UIColorswitch displayState {
        case .disabled:
            textColor = .white
            backgroundColor = .lightGray
        case .enabled:
            textColor = .white
            backgroundColor = .orange
        case .enabledHighlighted:
            textColor = UIColor.white.darkened
            backgroundColor = UIColor.orange.darkened
        }

        self.backgroundColor = backgroundColor
        titleLabel.textColor = textColor
        subtitleLabel.textColor = textColor
    }

    publicoverridevarisHighlighted:Bool {
        didSet {
            updateColors()
        }
    }

    publicoverridevarisEnabled:Bool {
        didSet {
            updateColors()
            updateAccessibility()
        }
    }

    publicoverrideinit(frame:CGRect) {
        super.init(frame:frame)
        setUp()
    }

    publicrequiredinit?(coder:NSCoder) {
        super.init(coder:coder)
        setUp()
    }

    publicvartitle:String {
        get {
            titleLabel.text ??""
        }
        set {
            titleLabel.text = newValue
            updateAccessibility()
        }
    }

    publicvarsubtitle:String {
        get {
            subtitleLabel.text ??""
        }
        set {
            subtitleLabel.text = newValue
            subtitleLabel.isHidden = newValue.isEmpty
            updateAccessibility()
        }
    }

    privatefuncupdateAccessibility() {
        isAccessibilityElement =truevaraccessibilityTraits:UIAccessibilityTraits= .button
        if!isEnabled {
            accessibilityTraits.insert(.notEnabled)
        }
        self.accessibilityTraits = accessibilityTraits
        accessibilityLabel = [title, subtitle].filter { !$0.isEmpty }.joined(separator:"\n")
    }
}

クックパッドのサービスメッシュ基盤を改善した話

$
0
0

こんにちは、技術部 SRE グループの ryojiro (@flyhigh_ro) です。今回はクックパッドでのサービスメッシュ基盤を改善した話を紹介します。クックパッドでのサービスメッシュの構成については以前の記事をご覧ください。

クックパッドでは多くのサービス間通信において Envoy を利用していますが、以下のような問題を抱えていました。

  • 改善前の Envoy のバージョンは v1.9.0 (2018/12 リリース) と古く、開発者はそれ以降に実装された機能を利用することが出来なかった。
  • CDS/RDS を cookpad/itachoによって生成しているため、 v1.9.0 で利用出来る機能であっても cookpad/itacho で実装されていなければその機能を利用できなかった。利用するためには cookpad/itacho にその設定を実装する必要があり、面倒だった。
  • cookpad/itacho で既に実装されている機能でも、ドキュメンテーションが不十分で目的の Envoy での設定に対応する itacho の設定が調べられず、cookpad/itacho の実装を調べることがあった。

上記の理由から、サービス開発者が Envoy v1.9.0 以降の機能や cookpad/itacho で実装されていない機能を利用したくても、すぐにその機能を利用することができずに、その機能を利用することを諦めることが何度かありました。SRE としては、サービス開発者にサービスメッシュを積極的に活用してもらいたいと考えていたので、サービスメッシュをもっと手軽に利用してもらうことを目的として、以下の内容でサービスメッシュ基盤を改善しました。

  • Envoy のアップデート
  • v1 xDS API の廃止
  • cookpad/itacho での itacho generate 廃止
  • xDS API の CI 整備

Envoy のアップデート

クックパッドで利用されている Envoy のバージョンは v1.9.0 と 2018/12 にリリースされた古いバージョンを利用していました。v1.9.0 でも機能としては十分でしたが、脆弱性が報告されていたり、古いバージョンを使い続けることでアップデートがどんどん大変になっていくことに懸念がありました。そのため、今回を機に最新のバージョンまで上げることにし、以降もバージョンアップしやすい環境を目指すことにしました。

段階的な移行

最初は Envoy を一気に v1.9.0 から v1.14.2 まで上げようと考えていましたが、以下の理由から一度 v1.12.0 にしてから v1.14.2 に上げることにしました。

v1.14.2 だと既に deprecated になっている設定があり、v1.9.0 と v1.14.2 で互換性のない設定があった

envoy.api.v2.route.HeaderMatcher.regex_match を例にすると、 v.1.14.2 では既に deprecated となっているため envoy.api.v2.route.HeaderMatcher.safe_regex_match へ移行する必要がありました。しかし envoy.api.v2.route.HeaderMatcher.safe_regex_match は v1.9.0 では実装されていません。一旦全ての Envoy を envoy.api.v2.route.HeaderMatcher.regex_match と envoy.api.v2.route.HeaderMatcher.safe_regex_match に対応しているバージョンへアップデートし、envoy.api.v2.route.HeaderMatcher.regex_match を envoy.api.v2.route.HeaderMatcher.safe_regex_match へと移行してから v1.14.2 にアップデートする必要がありました。

cookpad/itacho で利用しているライブラリの protobuf 定義が古く、v1.12.0 までの xDS リクエストにしか対応していなかった

cookpad/itacho で利用しているライブラリの protobuf 定義が古く、v1.13.0 以降の Envoy から送信される xDS request のデシリアライズに失敗していました。cookpad/itacho に原因があることはわかっていましたが、cookpad/itacho を開発した経験がなく、この対応にどの程度工数がかかるのか見積もることができませんでした。そこで、一旦 v1.12.0 へアップデートすることにして、その間に cookpad/itacho へ対応することにしました。

v1 xDS API の廃止

Envoy v1.10.0 で Bootstrap config の deprecated_v1 sds_config と command line config の –v2-config-only オプションが廃止、 v1.13.0 で v1 xDS API が廃止となりました。クックパッドではいくつかのアプリケーションで v1 xDS API を利用してたので、それらを全て v2 xDS API へと移行しました。Envoy 以外から v1 xDS API を利用しているアプリケーションもあったので、それらも v2 xDS API を利用するように変更しました。

cookpad/itacho での itacho generate 廃止

クックパッドでは CDS/RDS のレスポンスの生成に itacho generate を使用していました。itacho generate は指定された設定に沿って CDS/RDS を生成します。しかし、Envoy の設定名とそれを生成する itacho generate の設定名が一致していなかったり、ドキュメントが整備されていないことから、どのような記述をすればいいのかわからないとの声が上がっていました。実際に itacho generate の設定を確認するために直接実装を確認することもありました。また、新規の機能を利用する場合も cookpad/itacho へその機能を実装する必要があり、手軽に新規の機能を利用することが困難でした。これらの課題を解決するために、itacho generate で xDS API レスポンスを生成することをやめ、直接 xDS API レスポンスを記述するように変更しました。そのまま全てのレスポンスを記述すると冗長になってしまうので Jsonnet で記述するようにしました。共通の設定は関数化し、upstreams 毎に設定を libsonnet ファイルにまとめて、それらを import して利用することで簡潔に記述できるように工夫しています。以下は itacho generate での記述例とxDS API レスポンスをそのまま記述したときの例です。

itacho generate での記述例

https://gist.github.com/ryojiro/baac94ceb615949c7ea54e36ba94b70a

xDS API をそのまま記述した例

https://gist.github.com/ryojiro/cde4f0024cd29b6ed4ee10467519f1fb

このような記述にすることで、upstreams の設定を1箇所で管理しつつ、サービス毎に独自に upstream の設定を上書きすることも可能となっています。また、新しい設定を記述する時にも Jsonnet へ設定を追加するだけなので、手軽に Envoy の機能を利用できるようになりました。

xDS API の CI 整備

これまでは xDS API レスポンスを itacho generate によって生成していたので、正しい xDS API の形式となっていることが保証されていました。しかし Jsonnet で xDS API レスポンスを生成するように変更したことで、生成される xDS API レスポンスが正しいことが保証されなくなってしまいました。そこで、CI を整備して生成される xDS API レスポンスが正しい形式となっていることを事前に検証するようにしました。Envoy のドキュメントを読むと mode オプションvalidateを渡して起動することで、Envoy の設定が正しいかを検証できそうでしたが、ネットワーク通信が発生しないので xDS API サーバーを立てて生成した xDS API レスポンスを検証することはできませんでした (静的な設定ファイルのみ検証されます) 。検証したいのは CDS/RDS のレスポンスで、Envoy の static_resources との設定はほとんど同じだったので、CI では設定した xDS API レスポンスを静的な設定ファイルに変換し、その設定ファイルで Envoy を起動することで、設定した xDS API レスポンスが正しい形式で記述されているかを検証するようにしました。クックパッドでは現在 v1.12.0 と v1.14.2 の Envoy が混在しているので、どちらも valid な設定のみ追加できるように、それぞれのバージョンで検証するようにしています。

最後に

今回はサービスメッシュをサービス開発者により手軽に利用してもらうために、サービスメッシュ基盤を改善した話を紹介させていただきました。この改善によって、実際にサービス開発者が新しい Envoy の設定を追加して利用する事例も生まれています。Envoy は比較的新しいアプリケーションでまだ知見も少ないと思うので、これからサービスメッシュ基盤の改善を考えている方の参考になれば嬉しいです。

このエントリを読んで興味を持った方や、数千の規模で Envoy が利用されているサービスメッシュ基盤を改善したい方はぜひ以下のサイトよりご応募ください。

クックパッド採用サイト: https://cookpad.jobs

Trivy + AWSによるコンテナイメージ脆弱性検査パイプラインの構築

$
0
0

技術部セキュリティグループの水谷(@m_mizutani)です。最近はPCゲーム熱が再燃しており、今はCities: Skylinesに時間を溶かされ続けています。

クックパッドでは レシピサービスの継続的なサービス改善の他にも、生鮮食品販売プラットフォームの クックパッドマートやキッチンから探せる不動産情報サイト たのしいキッチン不動産をはじめとする新しいサービス開発にも取り組んでいます。さらに内部的なシステムも多数あり、動かしているアプリケーションの数は300以上に及びます。これらのアプリケーションには多くのOSSパッケージが利用されており開発を加速させますが、同時にOSSパッケージのアップデート、とりわけ脆弱性の修正にも向き合う必要があります。

これまでクックパッドでは(重大な脆弱性が見つかった場合を除いて)各サービスを担当するエンジニアが事業や開発の状況にあわせてパッケージのアップデートなどをしていました。しかし、管理すべきアプリケーションが多くなってきていることから、全社で統一したパッケージの脆弱性対応の仕組みを整える必要がでてきました。その一環として各アプリケーションのデプロイで使われるコンテナに含まれるパッケージの脆弱性を把握するための仕組みを整えました。

この記事では社内でのパッケージ脆弱性の検査に対してどのような要求があり、それをどうやって実現したのかを紹介します。

脆弱性スキャンのパイプライン構築における要件

現在、クックパッドでは大部分のアプリケーションがコンテナ化され、Amazon ECS(Elastic Container Service)上で動作しています。また、そこへのデプロイも主にCodeBuildを使ったCI(Continuous Integration)の環境が整備されています。そのため、このCIの仕組を利用することで脆弱性スキャンの機能を構築することにしました。

構築にあたってはいくつか解決しないといけない課題や要件があったため、それをまず紹介します。

要件1) 観測からはじめる

CI/CDにおける脆弱性管理の文脈では「CIのパイプラインで脆弱性を検査し、脆弱性があった場合はCIを止める」といったものが多く語られているように思います。検出されている脆弱性をすべて無くしてからしかデプロイできないようにする、というのは確かに理想形ではありますが、実際の事業に照らし合わせてみると必ずしも正しいとは言えないと考えています。

例えば1つのパッケージのバージョンを上げることで破壊的な変更が入る、あるいは連鎖的に複数のパッケージも更新する必要があり、結果的に大幅な改修が必要になってしまう、ということはままあることと考えられます。これが事業的に一刻も早くデプロイしなければならない状態だとすると、現場判断で脆弱性スキャンの機能を無効にせざるをえない、ということがありえます。

もちろん、攻撃が成功しやすい・影響が大きいような脆弱性の場合は事業を止めてでも修正する必要があります。しかし、脆弱性の中には複数の条件を突破しないと攻撃が成立しないような種類のものも少なからずあります。そしてそれはアプリケーションの設定や実行環境に依存するため、一律に判断するのは困難です。CVSSなどによるスコアリングでも、結局は環境などに依存してリスクが変動してしまい、これをセキュリティチームから開発チームに押し付けることは互いにとってあまり良い結果にならないのではと考えています。

そのため、まずはコンテナ内のパッケージの脆弱性がどのくらいあって、どのように変動しているかを把握し、どうすればリスクの極小化ができるかの仮設をたてて検証していく必要があります。そのためにも全体像を把握できるようにまずは観測できる環境を整えるという要求事項を設定しました。

要件2) CIと密結合にしない

いくつかの脆弱性スキャンツールはCIの途中で実行することを想定して作られており、CIのスクリプトなどに埋め込んでシンプルに実行することができます。しかし、アプリケーション数が多くなってくるとそれに比例して脆弱性スキャンツールを動かすための管理・統制にかかるコストが大きくなってしまいます。これは脆弱性スキャンツールの導入だけでなく、例えばツールの仕様が変わるなどしてうまく動かなくなった際の障害対応とメンテナンスの手間も含まれてきます。

先述したとおり、クックパッド内では300を超えるアプリケーションが動いており、それら全てのCIでそういった管理をするのはあまり現実的ではありませんでした。そのため、既存のCIの仕組みとは完全に独立させ、CI側に影響を与えないような疎結合なシステムを構築する必要がありました。これによって、今後さらにアプリケーションの数が増えても容易にスケールできることが期待されます。

要件3) 脆弱性の発見だけでなく修正もとらえる

脆弱性スキャンツールを使う主な目的は脆弱性のあるパッケージの発見であるため、検査結果をそのまま閲覧・通知することでこれは達成できます。しかし継続的にコンテナをメンテナンスしていく場合、コンテナに含まれる脆弱性が修正された、という情報も役に立つことがあります。

  • 脆弱性のあるパッケージが含まれていたコンテナイメージ修正の進捗状況を把握できる
  • 脆弱性のあるパッケージを更新したつもりのコンテナイメージをビルドした際、意図したとおりにパッケージが修正できたのか把握できる
  • 脆弱性が発見されてから修正されるまでの期間を計測できる

これらを実現するためには各コンテナイメージの脆弱性の状態を管理する必要があります。

要件4) ベースイメージに含まれているパッケージの脆弱性を識別できるようにする

クックパッドではアプリケーション用のコンテナイメージを作成する際に利用できる、社内共通のベースイメージが用意されています。このイメージにはおおよそ共通して使われるであろうパッケージが事前にインストールされており、これを使うことでアプリケーション用イメージごとのビルドのステップを短縮しています。

しかし、ベースイメージからビルドされたコンテナイメージの脆弱性をスキャンすると、ベースイメージにもともと入っていたパッケージの脆弱性とアプリケーション用に新たにインストールしたパッケージの脆弱性が混在した結果が出力されてしまいます。発生ポイントがどこであれ修正するべき脆弱性は修正しなければなりません。ですが、ベースイメージを管理しているチームとアプリケーションを開発しているチームが異なるため、脆弱性の発生レイヤが混在して通知されてしまうと、どのチームが対応するべき脆弱性なのかが判断しにくくなってしまいます。このため、検出された脆弱性がどのイメージをビルドした際に入り込んでしまったのかを識別できるようにしたい、という要求が生まれました。

ベースイメージが1つだけであれば、そのイメージの検査結果との差分をみることで脆弱性の発生ポイントを判定できますが、ベースイメージが複数あるとその紐付けの情報を管理する必要がでてきます。Dcokerfileからビルドする場合は FROMを見ることでベースイメージのレポジトリはわかりますが、いつビルドされたイメージが実際に使われているのかまではわかりません。とはいえ手動で管理するのはあまりにも煩雑なので、自動的に判定するような仕組みが必要になります。

脆弱性スキャンツールの選定

脆弱性スキャンのツールとしてはTrivyを採用しました。選定にあたって他のOSSや製品の脆弱性スキャンツールとも比較をしたのですが、

  1. 単体のバイナリだけで簡単にスキャンが実行でき、小回りがきくこと
  2. 入力や出力もシンプルになっており自分たちのシステムとのインテグレーションが容易であること
  3. OSのパッケージおよびrubyなどランタイムのパッケージの脆弱性もまとめて把握できること

という3つの理由からTrivyを使うことにしました。

ちなみに、クックパッドではCI/CDにおけるコンテナイメージの保存にはAmazon ECR(Elastic Container Registry)を利用しており、ECRのImage Scanningの機能を利用することも検討しました。しかし、スキャンできる対象がOSのパッケージのみだったことから採用を見送りました。

ちょうど先日、AWS Security Blog で How to build a CI/CD pipeline for container vulnerability scanning with Trivy and AWS Security HubというTrivyをCIに取り入れるというブログが公開されていました。このブログでもCodeBuildでのCIを想定しており、CIの中にTrivyによる脆弱性スキャンを実行して、その結果をSecurity Hubに格納するというアーキテクチャについて述べられています。このアプローチも小さくはじめるにはよい構成なのですが、先述した要件をクリアするのは十分ではなかったため、我々は別のアーキテクチャによって脆弱性スキャンのパイプラインを実現しました。

アーキテクチャと実装

TrivyとAWSの各種マネージメントサービスを利用し、コンテナイメージの脆弱性スキャンパイプラインを構築しました。AWSのサービスと接続することから、基本的な制御の部分にはLambdaを利用し、サーバレスなアーキテクチャになっています。デプロイにはAWS CDK(Cloud Development Kit)を利用しています。

また、アーキテクチャ図からは省いていますが、スキャン結果から得られたデータを確認するためのWeb管理コンソールも用意しています。

イメージのスキャン

f:id:mztnex:20200713200112p:plain
イメージスキャンに関連するAWS構成

クックパッドでは原則コンテナイメージをCodeBuildでビルドし、ECR(Elastic Container Registry)にプッシュしたのち、ECS(Elastic Container Service)へデプロイするという構成になっています。要件2の疎結合なアーキテクチャにするという観点から、今回はCodeBuild内で実行されるビルドのプロセスには一切手を加えず、ECRにプッシュされたイメージを利用することで、CI/CDのパイプラインに一切影響しないような構成にしました。

スキャンの開始は2つのトリガーがあります。1つはイメージがプッシュされた際にCloudWatch Events経由で送信されるECRイベント、もう1つは定期的(現在は24時間ごと)に発行されるCloudWatch EventsのScheduledイベントです。それぞれのトリガーによって起動されたLambdaがスキャンすべき対象のイメージの情報をキューとしてScanQueueに詰めます。定期的に実行されるトリガーはECRからレポジトリの一覧を取得し、そこからスキャンが必要なイメージを選定します。

ECRにプッシュされたイメージの中身は後からは変更されないため、同じ脆弱性を見つけるためには何度もスキャンする必要はありません。しかし脆弱性スキャンツールにTrivyを使う場合、新たに発見された脆弱性を見つけるためには脆弱性DBを更新して、再度検査をするというのがシンプルな対応になります。そのため、イメージがプッシュされたイベントとは別に定期実行の仕組みを取り入れました。

Trivyを使った実際のスキャンはFargate上で実行することにしました。Fargateを選択した主な理由は、1) 実行環境が独立しているため、ECSのように他のタスクに影響を及ぼさない、2) スケールアウトが容易、の2つになります。特に定期スキャンでは数百のイメージをスキャンするためのキューが一度に発生するため、スケールアウトによって短時間でスキャンを完了させられます。Fargate上ではこのパイプラインを制御するためのプログラムを動かしており、それがTrivyを起動させます。具体的には、次のような制御をしています。

  1. ScanQueueからスキャン対象イメージの情報を取得
  2. 脆弱性DBの更新(図中では割愛)
  3. Trivyの起動とスキャン結果の保存
  4. 対象イメージのレイヤ情報をECRから取得
  5. スキャン結果をS3に保存
  6. スキャン完了通知をResultQueueに送る

Trivyのスキャン結果は多少のメタデータを付与したあと、なるべくそのままS3に保存します。これのデータをもとに結果処理のLambdaが管理コンソールからの検索に必要なインデックス情報などをDynamoDBに保存します。

脆弱性の状態管理

f:id:mztnex:20200713200216p:plain
脆弱性の状態を管理するためのAWS構成

脆弱性の状態を管理するのに必要なのは「直前のスキャン結果との比較」です。これはRDBを使って管理するというようなアプローチもありますが、今回はS3に保存してあるスキャン結果を単純に比較してコンテナイメージに含まれる差分を計算する、という方法にしました。これによってイメージごとの差分計算処理が1つのLambdaに集約され、大量のリクエストがきても容易にスケールアウトできます。

差分計算の処理はシンプルに最新のスキャン結果と直前のスキャン結果を比較しているだけです。最新のスキャン結果が保存されたS3パスが(「イメージのスキャン」のアーキテクチャ図にもあった)スキャン結果処理のLambdaから送信されたQueueに、直前のスキャン結果が保存されたS3パスがDynamoDBにあります。これらをもとに、それぞれのスキャン結果をS3からダウンロードし、新しく出現した脆弱性と削除された脆弱性の情報を比較結果としています。比較結果のデータサイズがSQSのデータサイズ制限(256KB)を超える可能性があるので、比較結果を直接SQSには流さずS3へ保存しています。その後、SNS → SQS を経由して Lambda に通知を送り、DynamoDB上にある脆弱性の状態(未修正・修正済み)を更新したり、Slackに通知したりしています。

f:id:mztnex:20200713200248p:plain
新たな脆弱性が発見された、あるいは脆弱性が修正された際のSlack通知

管理コンソールからはどのコンテナイメージのどこにその脆弱性があり、それぞれの修正状況も把握できるようなユーザインターフェイスを用意しました。これによって社内での脆弱性対応の進捗が可視化されています。

f:id:mztnex:20200714103346p:plain
脆弱性の修正状況を確認できる管理コンソールのビュー

ベースイメージの判定

f:id:mztnex:20200713200445p:plain
ベースイメージを判定する手順の概要

「要件4) ベースイメージに含まれているパッケージの脆弱性を識別できるようにする」で説明したとおり、ベースイメージに含まれているパッケージの脆弱性とアプリケーション開発によって追加されたパッケージの脆弱性とを区別する仕組みを取り入れました。この判定には各イメージのLayer Digestを利用しています。ベースイメージを利用してイメージをビルドする場合、ビルドしたイメージは一部のレイヤーをベースイメージと共有しています。そのため、Layer Digestが一致すればそれ以前のレイヤーは基本的にすべてベースイメージのものである、と判断することが出来ます。

Trivyのスキャン結果には各脆弱性が含まれるレイヤーのLayer Digestが記載されているため、アプリケーションイメージのどのレイヤーがベースイメージ由来なのかがわかっていれば、脆弱性を含むパッケージがどちらに属しているのかも判断できます。どのレイヤーからベースイメージなのかを後から判定するため、スキャン結果とLayer Digestの一覧を組み合わせて保存しておく必要がありますが、残念ながらTrivyのスキャン結果に記載されていません。しかしLayer Digestの一覧はECRに保存されているため、代わりにECRへアクセスすることで取得できます。先述したとおり、fargate上でのスキャン時にはTrivyのスキャン結果とECR上のレイヤ情報の両方を取得し、組み合わせてS3へ保存しています。

このような仕組みでベースイメージを検出するために、検索用のデータストアとしてDynamoDBを使っています。DynamoDBに全てのイメージの最新レイヤーのLayer Digestをキーとして保存し、アプリケーションイメージの脆弱性一覧を表示するタイミングで全てのLayer Digestをバッチで問い合わせ、その結果からどこからベースイメージかを判定します。一覧表示のタイミングで検索しているのは、ベースイメージとアプリケーションイメージがほぼ同時に更新された際、スキャン結果の到着が前後する可能性があるためです。

この仕組を使うことで、どのレポジトリやタグがベースイメージとして使われているのかという情報をメンテナンスしなくても、自動的に判定ができるようになりました。また、ベースイメージが複数ある(ベースイメージAからベースイメージBが作られ、ベースイメージBからアプリケーションイメージが作られる)場合でも、同じ仕組みによって正確に複数のベースイメージを判定できます。管理コンソールでは次の図のようにベースイメージ由来の脆弱性はリンク先で確認するようなUIにしました。

f:id:mztnex:20200714103605p:plain
ベースイメージとアプリケーションイメージの脆弱性情報が分かれて表示される

コスト

今回のアーキテクチャではコスト削減を目的としていたわけではないのですが、結果としては一日あたりの動作コストが$6弱になりました。

その中でも支配的なのがDynamoDBで、1日あたり$4ほどのコストになっています。これはCapacity設定の最適値が読めないため on-demand capacity mode で動作させているためと考えられ、これは今後適切な値でRead/Write Capacityを設定しAuto scalingと併せて使うことで改善できると考えています。また、クエリについても改善の余地がありそうな部分はあり、そちらも今後リファクタしていきたいと考えています。

一方、CPUリソースが必要とされるTrivyのスキャンに関しては一日あたりおよそ$0.5ほどになっています。これはスケールイン・アウトがうまく機能していること、そしてFargate spotを使っていることで大きくコストを抑えていると見ています。Fargate spotなので処理の途中で停止してしまう可能性もありますが、どの段階で処理が止まってもやり直しがきき、かつ複数回処理が実行されても冪等になるように実装しているため、特に問題なく利用できています。

まとめ

この記事ではTrivyとAWSのマネージドサービスを使った、CI/CDと疎結合にコンテナイメージの脆弱性スキャンパイプラインの要件、アーキテクチャと実装の一部を紹介しました。これは永続的に疎結合のまま運用することを目指しているわけではなく、CI/CDの中に直接組み込むとしたらどのような仕組みや運用ポリシーが必要になるか?という課題を解くための前段階という意味合いもあります。技術部セキュリティグループでは引き続きどのようなパッケージの脆弱性管理の戦略をとれば事業開発のスピードへの影響を最小化しつつセキュリティを担保していけるか、という問題にチャレンジしていこうと考えています。

このようなエンジニアリングのチャレンジをするにあたり、クックパッドでは(引き続き)セキュリティエンジニアを募集しています。情報セキュリティに強い方だけでなく、むしろサービス開発を得意としつつセキュリティにも強い関心がある、という方にも興味を持っていただければ幸いです。

エンジニア社内留学制度を利用してAndroidアプリ開発を体験した話

$
0
0

こんにちは、事業開発部でデータ分析やデータエンジニアリングをやっている佐藤です。最近の楽しみはクックパッドマートで買ったコーヒー豆を挽いて淹れることです。

今日はクックパッド社内で実施されているエンジニア社内留学制度について紹介します。

エンジニア社内留学制度とは

エンジニア社内留学制度は「異動をすることなく短期的に他の部署でその部署の仕事をする制度」というもので2019年4月に作られました。 この制度は異動をせずに視野を広げたり自分のキャリアを考えるための制度であり、普段自分が関わらない技術や分野に対して新しいチャレンジをする機会を提供するための制度です。

エンジニア社内留学制度を利用することで、最大2ヶ月の間もとの部署の仕事から離れて留学先部署の業務に取りかかれます。これは全エンジニアが利用可能な制度です。
この制度の概要は上記のとおりですが、制度を利用して留学させる・受け入れる側を含めた関係者の狙いは下記のようなものとなります。

  • 留学生側
    • 他部署の業務に取り組むことで、視野を広げ、技術や分野において新しいチャレンジをする機会とする
  • 留学元部署
    • メンバーの目線を広げ、技術や分野の違うチャレンジをするなど成長の機会とする
    • 他部署の業務を詳細に知る社員を増やすことで、留学終了後もより円滑に協力できるようにする
  • 留学先部署
    • 短期的な開発リソースの確保
    • 自部署の業務を詳細に知る社員を増やすことで、留学終了後もより円滑に協力できるようにする

この制度が作られた後、サービス開発を行う部署から技術基盤の部署へのエンジニア留学が何件か実施されました。 自分はこの制度を利用して5月〜6月の2ヶ月間モバイル基盤部でAndroid留学を行いましたので、以降の内容ではそのAndroid留学に関して書いていきます。

Android留学の流れ

当記事の冒頭に書いたとおり、自分は普段は事業部でデータ分析やデータ整備作業などを主務として行っていました。 そんな自分が今回エンジニア社内留学制度を使ってAndroid開発に関する知識を身に着けようと思った動機はおおまかに下記の3つです。

  1. Androidエンジニアが足りないということで丁度モバイル基盤部がAndroid留学を募集してた(下記の図を参照)
  2. 部署でデータ分析をしているうちにモバイルの知識が必要になってきた
  3. Android留学を一回しておくと今後iOSで同じようなことをしたくなったときの取っ掛かりにもなりそう

f:id:ragi256:20200716142952p:plain
Android留学募集の様子

というわけで上長に相談し、次の目標を掲げての2ヶ月の社内留学が決定しました。

  • Android版クックパッドアプリのどの部分のコードでどうやってログデータを送ってるか把握する
  • Androidアプリのロギング処理をクライアント側で調査・デバッグできるようになる
  • 誰かが新たにロギング処理を仕込む際に、相談相手になったりコメントできるようになる
  • 今後もモバイル基盤部と協力してモバイルのログ周辺がより良くなるよう整備をしていけるようになる
  • モバイルエンジニアに依頼するばかりでなく自分でもログを仕込めるようになる

この時点でAndroidアプリ開発もKotlinもJavaも全く触れたこともありませんでした。完全に未経験の状態です。 このあたりの留学決定に関する流れは4月頭の1on1で相談したら即留学用チャンネルにinviteされ、3週間ほどの調整期間の後、留学を実施というスピード感でした。調整期間というのは元いた部署の仕事から離れても大丈夫なよう片付けるための期間だったので、特に何かしらの準備があったわけではありません。

やってみてどうだったか

留学期間で実際に着手したタスクは下記の4つでした。

  1. アプリ画面リファクタリングに伴うログ変更に関する調査と周知
  2. 古いコードのVIPER化
  3. モバイルアプリにあるロギング実装に関するドキュメント整備
  4. 旧ロギング実装のリファクタリング

各タスクについて個別に書いていきます。

1. アプリ画面リファクタリングに伴うログ変更に関する調査と周知

クックパッドが提供しているレシピアプリはiOS・Androidの両プラットフォームともにVIPERというレイヤードアーキテクチャを採用しています。このVIPERアーキテクチャ採用は2018年に決定したもので、今利用しているコードの中には旧アーキテクチャのままになっている箇所もあります。よって既存コードをVIPERのアーキテクチャに置き換える作業(通称VIPER化)が行われています。
最近行われたとある画面のVIPER化に伴って、意図せずログ送信内容が書き換わっている可能性が高いことがわかりました。そのため、その問題の調査と社内周知に留学初タスクとして取り掛かりました。実際にやったことはVIPER化の手順を追いかけ、ログ実装を読み、実際に送られたログデータの変化を確認するだけです。

2. 古いコードのVIPER化

初タスクでVIPER化の作業を追いかけて読んだため、Android開発の素振りとしてVIPER化に取り組むこととなりました。しかし、結論から言えばこのタスクは断念することとなりました。
理由は初めてのモバイルアプリ開発に対して、あまりに知識が足りなかったためです。開発するためのキャッチアップに時間を浪費してしまい、そのままでは定められた期間で留学の目的を達成することが困難と判断したためです。VIPERもそうですが、Rx・DI・マルチモジュール・Android知識など予め備えておくべき知識の諸々を学びながらの期間であったため、見てもらうためのPRの実装を作るまでに時間がかかってしまいました。初めてレシピアプリ開発に取り掛かる開発者も困らないようにと初学者用ドキュメントは整っており、それを読みながらの実装でしたがとにかく初めての概念が多いため覚えることがたくさんありました。
この点に関してはまずGoogle CodeLabsをやるのが良かっただろうというのが反省です。

3. モバイルアプリにあるロギング実装に関するドキュメント整備

VIPER化を断念した後、自分が何をするべきかを留学当初の目標に立ち返って考え、取り組むべき課題を考えることとしました。元々の目標の中心にあった「ログ周辺」の課題がなにかないか考えたところ、「レシピアプリ内で使われるロギングの実装がとっちらかっているように見えるのでなんとかしたい」という課題を留学期間中に感じていました。
そこで実際に取り組んだタスクがこのドキュメント整備と次の旧ロギング実装のリファクタリングです。
レシピアプリはiOS・Androidともに開発に参加しやすい状況を維持すべく、開発参加者への支援が手厚く用意されています。オンボーディングや開発者向けドキュメントなどがそうです。ですが、アプリから送られるログ周りに関しては専門家がいなかったため、包括的なドキュメントがありませんでした。 そこで留学という機会を利用して、レシピアプリ開発へ新規に参加するエンジニアでもロギング実装に困らないようなドキュメントを書きました。

4. 旧ロギング実装のリファクタリング

3番目のドキュメント整備タスクと並行して、古いログ送信処理を置きかえる作業を実施していました。旧ロギング実装はアプリ開発からしてみれば何か大きく問題点があるわけではなかったため、誰にも気づかれずそのままとなっていました。しかし、実際に送信されたログを保守・加工・分析を行っている側では微妙に扱いづらいものであり、ログデータを利用する側(分析者やデータ整備者)ではちょっとした負債となっていました。この分析サイドからみた負債を解消することが、旧ロギング実装リファクタリングの目的でした。 こういった負債の指摘やリファクタリング作業やドキュメント整備はログデータを送る側からも利用する側からも扱いやすい、より良いログデータ環境を目指そうという意識付けにも繋がりました。データ基盤はは送信箇所や分析箇所などの特定の箇所の改善では使いやすくなりません。実際の利用フローに合わせ、足並みを揃えてトータルの改善をすることで多くの人から喜ばれるデータ基盤となります。

上記4つのタスクをひたすらにこなしているうちに気づけば2ヶ月が経過してしまい、エンジニア社内留学が終了となりました。留学自体は終わりましたが、自分自身がクックパッド社内でデータに関わるいちエンジニアであるということには変わりがないため、今回得た経験を活かして今後もデータ分析環境の改善に取り組んでいくつもりです。

エンジニア社内留学からの副産物的成果

実際にやってみたところ、予想していなかった副産物的成果がいくつかありました。自分としては「完全なAndroid初心者では手取り足取り教えてもらうだけになりそう」と思っていたのですが、留学をしてみたら意外と好影響もあったようです。

1. Android入門者用のドキュメントが改善された

初めてのAndroid開発に参加するため、レシピアプリに関する全ドキュメントに目を通すこととなりました。この際に疑問に思ったところは片っ端から質問をするようにしていたため、ドキュメントの不備・陳腐化した内容・分かりにくい説明などはどんどん修正されていきました。

2. ログに関する議論が活発になった

留学先のモバイル基盤部はお昼会という名のデイリーミーティングと、週次で行われる振り返りミーティングがありました。リモート期間中だったので1これらのミーティングは全てZoom越しに行われました。このミーティングで同僚の着手タスクの概要や進捗状況を把握するわけですが、こういった日々の会話の中で常にログデータの取り扱いに関する話に対して質問やコメントをしたりし続けていました。
折しも社内でログの取り扱いに関する話題が活発化しているタイミングで、そういった議論に関して「今こういう話が活発ですよ」「このチャンネルでこういう議論がかわされていますよ」という誘導を会話の中でし続けていました。
ロギングのドキュメント整備で話し合う機会もあり、「他部署ではログデータをこう取り扱っている」といった部署横断的な知識の提供に繋がりました。

3. 今まで方針の定まっていなかったロギング実装に関して、話し合いの場を設けて合意をとった

「やったこと」の3つ目に書いてあるとおり、留学後にこなした業務の中で「ロギングのドキュメント整備」がありました。このドキュメント整備ですが、今まで明文化されていなかったものをドキュメントに書き起こすだけで済むかと思いきや、そうではありませんでした。
これまで言語化されていなかったため、明確になっていたなかった点がいくつもあったのです。ドキュメントを制定するに当たり、同時にプルリクエストレビューで多くの人と意識のすり合わせがなされました。また、PRだけでは決まりそうにない、ロギング実装に関する大きな意思決定のため有識者会議を開くこともありました。
多くの人が関わるクックパッドのレシピアプリ開発の方針決定に関わることになるとは留学前には考えていもいませんでした。

終わりに

クックパッドでのエンジニア社内留学制度の紹介と、その制度を利用したAndroid留学体験を紹介しました。
社内で異動することなく、別分野のエンジニア業務を体験してみるのは新鮮なことでしたし、自分が取り組める業務の幅も広がったと思います。また、初心者かつ異分野エンジニアが留学してみると、留学ならではの好影響も与えられるという発見がありました。

留学を終了した証書
無事に留学修了証書をもらいました


  1. クックパッドでは新型コロナウイルス感染症の拡大に伴い、2月から全従業員を対象に在宅勤務を実施しています。在宅勤務に対する取り組み例はこちら。記事1記事2

RailsアプリケーションのCIにDynamoDB Localを導入した話

$
0
0

こんにちは、事業開発部 サーバーサイドエンジニアの堀江(kentarohorie)です。

今回はRailsアプリケーションのCIにDynamoDB Localを導入した事例をご紹介します。

広告入稿システムとCI

クックパッドでは自社製の広告入稿システム・配信サーバーを運用しています。また広告の一部はDynamoDBを利用したアーキテクチャで入稿・配信されています。詳細は以前の記事「広告配信サーバーにおける DynamoDB Accelerator (DAX) 活用事例の紹介」で紹介されています。この入稿・配信のうち、広告入稿システムのCIに対してDynamoDB Localの導入を行いました。

広告入稿システムのCIではブランチへのpush、またはmasterへの変更をトリガーにCIサーバー上でスクリプトが実行されていました。CIサーバーにはMySQLやPostgreSQLの環境が用意されており、スクリプトが実行されるとサーバー上のDBを初期化してrspecが実行されていました。多くのテストでそれらのDBを利用したテストが実行されていましたが、DynamoDBに関しては実際のDBを使用できていませんでした。

そのため、DynamoDBを利用している箇所ではAWS SDK DynamoDBClientのput_itemやdelete_itemなどのメソッドを一つ一つstubしたテストが書かれていました。これは例えばDynamoDBを利用したコードが増えたり、その箇所を間接的に利用する必要があるコードが生まれた場合に、DynamoDBの利用を気にしながら必要に応じて都度stubするといった作業が必要になるということです。
例えば以下のようなstubがit句毎に書かれていました。

it "..."do
  expect(dynamodb_client).to receive(:delete_item).with(
    hash_including(
      table_name: "table_name",
      key: { pk: "product_key" },
    )
  )

  expect { subject }.to change { ... }.to(false)
end

こうした状況の中でDynamoDBを利用している箇所で、stubせずともテストを書けるようにしようというモチベーションがありました。

DynamoDB Local導入に必要な環境を整備

DynamoDB Localの導入にあたっては執筆時点で3つの方法がAWSで紹介されています。

  • Apache Mavenリポジトリとして利用
  • Java環境を用意して実行
  • Dockerイメージを利用

これら方法のうち、Dockerイメージを利用してDynamoDB Localを導入しました。理由は全社的にCodeBuildの利用が推進されており、CodeBuild上でDockerを利用してCIを回すという事例が社内に既に多く存在していたためです。CodeBuildはAWSが提供するCI/CD用ビルドサービスであり、Androidアプリ CIをCodeBuildに切り替えた事例などクックパッドでは広く活用されています。

上記検討の後、まずは既存のビルド部分をCodeBuildに置き換え、Codebuild上のDockerでテストを実行できる環境を用意しました。ビルド部分の置き換えはJenkinsのCodeBuildプラグインを利用しました。次に社内で用意されているCodeBuild用Dockerイメージをベースに広告入稿システムのDockerイメージを作り、MySQLやPostgreSQLを利用する処理はスクリプトを用意してdocker-compose up時に実行されるようにしました。具体的にはDBの初期化やrspecの実行などです。

f:id:kentarohorie:20200721123459p:plain
Before

f:id:kentarohorie:20200721123513p:plain
After

この置き換え作業では、既存のCIと比べた場合に可能な限りCI時間が長くならないことを意識して進めました。CodeBuildに置き換える場合これまでになかったDockerイメージのビルドや立ち上げといった工程が増えるためにCI時間が長くならざるをえません。しかしCI時間は短ければ短いほうが良いので、許容できる程度までCodeBuildでのCI時間を縮める必要がありました。

具体的には以下の工夫を行いました。

  1. CodeBuild上でのDockerイメージビルドはキャッシュを利用する
  2. docker-composeでマウントするファイルを可能な限り減らす

広告入稿システムはRailsで動いており、ビルド時間でネックになっていたのはnode_modulesとgemのインストール工程でした。当初はCodeBuildのS3キャッシュを利用してnode_modulesとgemをキャッシュする方針で作業を行っていました。しかしその方法ではnode_modulesとgemファイル群をCodeBuildサーバー(コンテナの外)に持つ必要があり、docker-composeでマウントする必要のあるファイルが多くなり結果コマンド実行時間が遅くなるという問題が発生しました。

次にDocker Layer Cacheを利用する方法を試しました。はじめはCodeBuildで用意されている「ローカルキャッシュ」のDocker Layer Cacheモードを利用していましたが、ライフスパンが30分程度と短いため、CIの稼働頻度が30分に一度回るほどは高くない広告入稿システムではあまり恩恵を受けれませんでした。

そこで最終的に、ECRを利用してDocker Layer Cacheすることになりました。具体的にはCodeBuildのPOST_BUILDフェーズでECRへDockerイメージをpushし、次のBuild時にそのイメージをキャッシュとして利用する、というようにしました。

phases:pre_build:commands:- ....
      - docker pull "${REPO}:latest" || true- ...
  build:commands:- ...
      - docker build --tag "rspec" --tag "${REPO}:latest" -- cache-from "${REPO}:latest" -f Dockerfile .
      - ...
  post_build:commands:- ...
      - docker push "${REPO}:latest"- ...

DynamoDB Localをテストへ導入

CodeBuildへの置き換えが完了した後はdocker-compose.ymlにAmazonが公式に配布しているDynamoDB Localイメージを組み込み、テスト時にそれを読み込むように設定しました。具体的にはAWSのconfigをアップデートする処理をテスト実行前に読み込むようにしました。広告入稿システムのテストでは他にAWSリソースを使用していなかったため、DynamoDBリソースに絞った設定はしませんでした。

次にテスト実行時にDBが初期化されるようにしました。 広告システム関連で使われているDynamoDBにはdynaというgemを利用したDB初期化の仕組みがあります。 dynaはDynamoDBをDSLで管理できるものです。したがって、テスト実行時のDB初期化はdocker-compose up時に走らせるscript内にDB初期化を行うdynaコマンドを実行することで達成しました。

最後に、広告入稿システムのテストでDynamoDBに関するstubを外していく作業を行いました。これでDynamoDB LocalのCI導入が完了しました。

導入結果

DynamoDB LocalをCIに導入することで以下を達成できました。

  • DynamoDBに関する処理のstubを考えずにテストが書けるようになった
  • DynamoDBに関するテストコードを、各人の環境で実行できるようになった
  • DynamoDBを利用したコードの保守性を向上させることができた
  • DynamoDBやClientの仕様変更に耐えやすいテストになった

導入後、DynamoDBに関する最初の作業としてDynamoDBのクライアントgem aws-sdk-dynamodbのアップデート作業を行いました。specではstubせずにDynamoDB Localにアクセスしているのでテストが通った結果に安心感を持つことができ、導入によるメリットを実感しました。

最後に

以上、広告入稿システムのCIにDynamoDB Localを導入した事例をご紹介しました。

クックパッドにはユーザーが触る画面を改善しているサービス開発領域や、収益を支えている広告領域など、様々な領域でエンジニアが活躍しています。そしてそれらの領域ではエンジニアを随時募集しています。興味を持っていただいた方のご応募をお待ちしております。

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

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

大規模なiOSアプリの画面開発を効率化するために動作確認用ミニアプリを構築する

$
0
0

こんにちは、モバイル基盤部の大川(@aomathwift)です。

iOSアプリの開発途中で画面のレイアウトなど僅かな変更を確認したい場合、最も確実な方法はアプリをビルドして該当の画面まで手動で遷移して確認する方法です。

この方法は特別なセットアップが必要なく単純明快な確認方法ですが、効率の面で問題があります。例えば一番の問題として挙げられるのがビルド時間の長さという問題です。アプリ開発の規模が拡大していくと、ちょっとした変更でもビルド待ちの時間が無視できないものとなっていきます。

本稿では、クックパッドアプリの開発において、機能単体で動作するミニアプリを構築して、プレビューサイクルを改善した取り組みについてお話しします。 f:id:aomathwift:20200731184036p:plain

iOSアプリの動作確認における問題点

クックパッドアプリの開発は、開発規模の拡大によって、ビルド時間の改善が大きな課題になっていました。 そこで、最近はその問題を解決すべく、大きなアプリを複数のモジュールに分け、分割してビルドできるようなマルチモジュール化に取り組んできました。

詳しくは2019年のCookpad Tech Confでの講演、「 〜霞が関〜 クックパッドiOSアプリの破壊と創造、そして未来」をご覧ください。

f:id:aomathwift:20200731184155p:plainこれは、マルチモジュール化が活発化した2019年9月から2020年6月まで、同一のマシンで計測した平均ビルド時間を集計したものです。 このマルチモジュール化の取り組みの結果、一回あたりのビルド時間が徐々に改善されてきているのがわかります。

しかしながら、小さな変更を確認したい場合やレイアウトを調整したい場合を考えると、まだまだストレスを感じる長さです。

また、新規のモジュールは、アプリを起動してからそのモジュールの機能や画面への接続が出来ていない状態から開発を始めます。 そのため、開発するモジュールの画面への遷移を先に実装することが必要です。

これをすべて同じ開発者が担当しているなら然程問題ではないかもしれませんが、この起動画面からの導線部分の実装とモジュールの開発を別の開発者が行っていた場合、モジュール開発を担当する人は仮の画面遷移を実装するなどの余計なコストが生じてしまいます。

Sandbox - 機能ごとに動作するミニアプリ

f:id:aomathwift:20200731184247p:plain
TechConf2019より引用

クックパッドのマルチモジュール化では、レシピの表示画面や、検索結果画面など、1機能に関連するいくつかの画面を1つのモジュールとして扱っています。この単位をFeature Moduleと呼んでいます。

Feature Moduleの導入により、アプリ全体をビルドせずとも、部分的にビルドすることができるようになりました。

これらのFeature Moduleはframeworkとしてクックパッドのメインターゲットでimportして利用しますが、先に述べたような動作確認における問題を解決するため、Feature Moduleを単体のアプリとして動作可能にしたのがSandboxアプリです。

以降、この部分的にビルドするSandboxアプリに対して、アプリ全体を結合してビルドするアプリは本体アプリと呼ぶことにします。

Sandboxアプリのメリット

本体アプリをビルドするより速くビルドできる

このSandboxアプリのわかりやすい恩恵は、本体アプリよりも極めて短い時間でビルドが終わる点です。

同じ少量の差分のビルドにかかる時間の計測結果を比較すると、本体アプリのビルドでは平均約20秒かかるのに対しSandboxアプリのビルドでは約5秒で済みます。 単純計算でビルド時間を1/4に抑えられるということになります。

実際のビルドの様子を見ても、Sandboxがものの一瞬で起動できることは一目瞭然です。

f:id:aomathwift:20200731184359g:plain
本体アプリのビルド
f:id:aomathwift:20200731184538g:plain
Sandboxアプリのビルド

Viewのレイアウトの僅かな値を変更して差分を確認したいときなどでも、ストレスなく開発することが可能になりました。

確認したい画面にすぐ辿り着ける

クックパッドのように機能の多いアプリでは、アプリトップから開発している画面にたどり着くまでがやや面倒な場合があります

また、決済終了後の画面など、表示する条件が複雑な画面も存在しています。

そこでSandboxアプリを利用すると、確認したい画面を一番最初に、好みの条件で起動することが出来ます。

繰り返し起動したい画面は一覧になっていて、ここから選択して表示することができます。

f:id:aomathwift:20200731184643g:plain

先に述べたように、まだアプリ起動時の画面からの導線が出来ていない画面のデバッグも容易に可能です。

Sandboxアプリの実現方法

上記のようなメリットを得られるSandboxアプリを実現するために、解決しなければならない問題がありました。

一つは、本体アプリでは多くの外部ライブラリに依存していますが、ビルド速度向上のためにできるだけこの依存ライブラリの利用を避けなければいけないということ、 もう一つは、Feature Module内の画面から、別のFeature Module内の画面に遷移するケースがありますが、このためには検証に必要ないモジュールのビルドも必要になるため、これもまた避けなければならないということです。

これらを考慮した上で、画面遷移やネットワークリクエストといった副作用を本体アプリに近い形で提供する工夫が必要になります。

そこで、クックパッドアプリでは Dependency Injection を利用した副作用を取り出すためのオブジェクトを用意し、これを経由して各画面から副作用を呼び出せるようにしています。 この仕組みをEnvironmentと呼んでいます。

これを利用し、Sandboxアプリではスタブ可能なダミーの実装を注入することで、本体アプリに影響を与えずに副作用を実現できるようにしました。

これによって、例えばネットワークリクエストも実際にリクエストを送るのではなく、予め用意したデータを注入して表示することができるようになっています。

f:id:aomathwift:20200731184817p:plain

Sandbox用のターゲットは各Feature Moduleごとに作成し、ターゲットごとにビルドすることでモジュール単位でのミニアプリの起動を実現しています。

Sandboxアプリを開発者に快適に利用してもらうための工夫

Sandbox用のコードはできるだけ自動生成する

Sandboxアプリを動かすためのコードは実際のプロダクションコードとは別に実装する必要があります。

画面の実装自体はプロダクションコードを参照するとはいえ一つのアプリとして立ち上げるわけですから、データのスタブやEnvironmentの実体の注入などそれなりにコードを記述する必要があります。

この手間が障壁となり、導入当初はSandboxアプリを利用せずアプリ全体をビルドするという開発者が多い状況でした。

そこで、Sandboxアプリのセットアップを簡単に行えるよう、コード生成の仕組みを用意しました。

コード生成にはGenesisというSwiftで実装されたOSSを利用しています。 これは、同じくSwiftで実装されたテンプレートエンジンであるStencilを利用し、簡単な設定とテンプレートを用意すれば、ソースコード生成の仕組みを実現できるツールです。

options:- name: sceneName
    question: Sandbox scene name?
    description: new Sandbox scene name to generate. (e.g. RecipeDetails).
    type: string
    required:true- name: moduleName
    question: Destination target?
    description: module name to generate new sandbox scene for. (e.g. RecipeDetails)
    type: string
    required:truefiles:- template: AppDelegate.swift.stencil
    path:"{{ moduleName }}AppDelegate.swift"

例えばこのようなコード生成定義を書いて、オプションとして作成したいSandboxのモジュール名や画面の名前を与えると、以下のテンプレートファイルの中で展開されます。

@testable import {{ moduleName }}
import UIKit

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    private let environment = StubbableEnvironment()
    var window: UIWindow?

    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        window = UIWindow(frame: UIScreen.main.bounds)
        
        // Inject Scenes to RootTableViewController
        let rootViewController = {{ sceneName }}ViewBuilder.build(environment: environment)
        window?.rootViewController = rootViewController
        window?.makeKeyAndVisible()
        return true
    }
}

そして、この定義に基づくコード生成スクリプトを実行すると、自分であれこれコードを書かなくとも、Sandboxを生成したいモジュール・画面の情報が反映されビルドできるようになります。

$ ./scripts/generate-sandbox
[14:00:14]: Welcome to Sandbox Scene generator
[14:00:14]: What target do you want to make sandbox for1. MyFeature
2. MyAwesomeFeature
?  1[14:00:22]: Enter new Sandbox Scene name to generate. Upper camel case is recommended. (like RecipeDetails)
MyFeatureDetail
[14:00:40]: Generating MyFeature/MyFeatureDetail
[14:00:40]: $ /path/to/ios-cookpad/scripts/mint run Genesis genesis generate /path/to/ios-cookpad/templates/SandboxScene.yml --destination /path/to/ios-cookpad --option-path /var/folders/p7/g0t6l0zx00sbdxxrnm7wq8d80000gp/T/options20200714-98239-1lwms1g.yml
[14:00:40]: ▸ Generated files:
[14:00:40]: ▸   Sandbox/MyFeature/MyFeatureDetailSandboxScene.swift
[14:00:40]: ▸   Sandbox/MyFeature/AppDelegate.swift

できるだけ実際のアプリに近い挙動になるようにする

Sandboxアプリでは、マルチモジュール化での依存関係の問題により、他のモジュールにある画面に遷移することはできません。

基本的に一つの画面をプレビューすることを想定しているSandboxアプリでこの画面遷移を厳密にプレビューできるようにする必要はありませんが、スムーズな動作確認ができるように、簡易的なViewをモックとして表示できるようにしました。

これにより、本体アプリとほぼ同じ挙動を想定した動作確認をすることができるようになっています。

f:id:aomathwift:20200731190028g:plain

この機能は、先に述べたコード生成により自動で実装されるほか、自分で実装する場合も一つのイニシャライザメソッドを呼べばセットアップできるように整備されています。

今後の展望

先日のWWDC2020で、SwiftUIの新しいPreview機能についての発表がありました。

https://developer.apple.com/videos/play/wwdc2020/10149

昨年発表されたXcode Previewsは、SwiftUIで構築した画面を、Xcode上でリアルタイムに確認できるような仕組みです。

今回のアップデートでは、プレビュー中にサンプルデータを流し込んで利用したり、Xcode Previewsによって起動した画面を実機上でインタラクティブに操作しながら確認したりする機能が加わり、今まさにSandboxアプリで実現していることがXcode Previewsで実現できるようになります。

加えて、Dynamic TypeやダークモードのPreviewなどの機能とも併せることで、より効率的に開発を行うことが可能になるでしょう。

現在、クックパッドアプリの画面はほぼUIKitで実装されたものですが、新しい機能の実装にSwiftUIを利用できないか試しているところです。 来る新しいXcode Previewsが利用可能になる日に向けて、SwiftUIによるView実装への移行と共に、プレビュー機能全体をXcode Previewsを利用したものに移行していく必要があると考えています。

既存のUIKitによる実装に関してはXcode PreviewsとSandboxアプリの機能を併せて利用したものを試行しながら、プレビュー確認環境全体の改善を進めていく予定です。

Xcode PreviewsとUIKitの併用については、メルカリさんが下記の記事で自社の事例を紹介しています。

まとめ

この記事では、クックパッドアプリにおけるSandboxアプリを利用した動作確認の効率化について紹介しました。

開発効率を上げるために、スピーディーで快適な動作確認環境は必要不可欠です。

クックパッドでは、より便利なプレビュー機能への改善を一緒に行っていただけるエンジニアを募集しています。


Amazon RDS/Auroraをクローンするシステムを作った話

$
0
0

こんにちは、技術部SRグループの菅原です。

最近、Ninja650からNinja1000に乗り換えました。パワーがあるせいで3速発進・4速発進が平気でできてしまい、シフトワークがどんどん下手になっています。精進したいものです。

この記事では、Amazon RDS/Auroraをクローンするシステムを作った話を書きます。

Amazon RDS/Auroraをクローンするシステム

サービス開発を行っていると、調査や検証でプロダクション環境で使われているデータベースが必要になることがあります。開発環境やステージング環境にもデータベースは存在するのですが、プロダクション環境のデータでしか再現しないバグの調査や、プロダクション環境のデータ量でのスキーマ変更の負荷の検証など、開発環境やステージング環境のデータベースではできない作業も多いです。しかし、オペレーションミスや個人情報へのアクセスを考えると、プロダクション環境のデータベースで直接作業をすることは大きなリスクを伴います。

Amazon Auroraのクローン作成機能を使うと、プロダクション環境に影響を及ぼさないクローンを作成できるのですが、個人情報にアクセスできてしまう点は解決できません。また、クローンの作成や削除には強力なIAMの権限が必要なため、管理者がクローンを作成して利用者に渡すような手間が発生していました。

そこで、それらの問題を解決し、開発者が手軽にプロダクション環境のデータベースを触れるように、Amazon RDS/Auroraをクローンするシステムを作成しました。

クローン作成の手順は以下の通りです。

  1. SlackでRubotyに対して @ruboty rds clone db-cluster:my-cluster db.t3.small 4hというコマンドを送る
    • 普段からChatOpsでデプロイが行われていること、作成したクローンDBの情報を共有しやすいことなどからインターフェースとしてSlackを利用しました
  2. RubotyがBarbequeのジョブを起動する
  3. Barbequeのジョブがクローンを作成する
    • Auroraの場合はクローン作成機能、RDSの場合はスナップショットから復元
  4. クローンDBのマスターユーザーのパスワードを変更する
  5. クローンDBのデータをマスキングする
  6. セキュリティグループを変更して、社内ネットワークからクローンDBにアクセスできるようにする

f:id:winebarrel:20200819092303p:plain

f:id:winebarrel:20200819092422p:plain

データのマスキングには同僚の@mozamimyが作ったDumptruckという社内ツールを利用しており、以下のようなJsonnetの設定ファイルに従ってデータをマスキングします。

{
  database: 'db_name',
  except: ['secure_%'], // `secure_`プリフィックスのテーブルはクローンDBにコピーしない
  rules: [
    {
      table: 'users',
      transforms: [
        {
          column: 'tel',
          value: "lpad(id, 12, '0')", // SQLでデータをマスキング
          inline_sql: true,
        },
        {
          column: 'email',
          value: "concat(id, '@example.com')",
          inline_sql: true,
        },
      ],
    },
  ],
}

作成したクローンDBは、利用後に開発者が自分で削除するか、利用期限が切れるとバッチ処理が自動的にクローンDBを削除します。

f:id:winebarrel:20200819092447p:plain

開発者が自分でDBの削除やパラメータの変更を行えるようにするため、クラスタIDやインスタンスIDには rcc-というプレフィックスを付け、IAMの対象リソースをrcc-*とした権限を開発者に付与しています。

まとめ

Amazon RDS/Auroraのクローンが手軽にできるようになったことにより、プロダクション環境のデータの調査や、データベースのパフォーマンスの検証がはかどるようになりました。また、データのマスキングを設定ファイルで管理することにより、どのカラムに秘匿情報が入っているかもわかりやすくなったと思います。プロダクション環境のデータベースを使った作業は管理者やSREに作業が集中しがちなので、このような形でなるべく開発者に権限を委譲していきたいです。

Rubyの開発を支える技術

$
0
0

こんにちは、遠藤(@mametter)です。RubyKaigi Takeout 2020お疲れさまでした。

現在クックパッドには、フルタイムでRubyの開発をしている人が2人います(笹田と遠藤)。 それぞれ、Ruby 3の目標である並列性と静的解析の実現をメインミッションに据えて活動していますが、実はそれ以外にもRubyの開発を支えるための活動をいろいろやっています。

今回は、遠藤が関わっている範囲で、「Ruby開発者会議を支える技術」「Ruby開発のリモート議論を支える技術」「Rubyの品質を支える技術」についてざっと紹介してみます。

1. Ruby開発者会議を支える技術

Rubyに対する機能提案などの議論は、原則として、バグトラッカ上で行われます。 しかし、設計者であり最終決定権を持つmatzの多忙などの理由で、それだけでは議論が停滞してしまうのも事実です。 そこでRubyでは、開発促進のために月例で開発者会議を行っています。 私はここ数年、この会議を運営することにコミットしています。

会議のプロセス

毎月、次のことを行っています(検討以外はだいたいすべて私がやってます)。

  • 開催の約1ヶ月前に議題を募集するチケットを作る(例:先月のチケット)。
  • 開催の数日前、挙げられた議題をmarkdownにまとめ、有志のコミッタとともに事前検討をする(準備会)。
  • 会議当日、matzおよび有志のコミッタと本検討をする。結論の出た提案については、なるべくその場でmatzに回答してもらう。
  • 開催の数日後、議事録を清書して公開する(例:先月の議事録)。

コミュニケーション技術

会議当日のコミュニケーションには、クックパッド提供のzoomと、オンラインmarkdown共同編集ツールであるhackmdを使っています。 コロナ以前は東京近郊でのオフライン会場もありました(matzは大体リモート参加)が、今年は完全オンラインです。 アジェンダおよび議事録の共同編集はいろいろ試した末、「リアルタイムの共同編集ができ、markdownで書けて、コード断片も書きやすい」ということでhackmdに落ち着いています。

準備会

当日の会議は午後半日、約5時間をかけていますが、20件程度の議題をさばくには十分とは言えず、効率化が課題でした。 そのため今年から、数日前に有志での事前検討を行っています。 これにより、当日の参加者が誰も議題を理解していない、という非効率が回避できるようになりました *1。 またこのフェーズで、事前に担当者に話題を振っておいたり、議題が曖昧なチケット・matz判断が不要そうなチケットに事前にフィードバックを返したりもします。 これにより、当日に議題をさばき切れないことはだいぶ減りました。

非互換の影響検討

仕様変更を議論する際には、かならず非互換の影響が議論されます。かつては互換性を気にしない言語とたびたび揶揄されていたRubyですが、現代ではかなり気を使うようになりました。

たとえば先日から、set.rbを組み込みにするという提案がたびたび議論されています(結論は出ていない)。 こういうとき、「トップレベルにSetクラスを自力定義している人は実際どのくらいいるか?」というような疑問が浮かびます。 このとき役に立つのがgem-codesearchです。

gem-codesearchがセットアップされているコミッタ用共用サーバにログインし、次のようにすることで、全最新版gemに対する高速grepができます。

$ gem-codesearch "^class Set$"
/srv/gems/ConstraintSolver-0.1/lib/extensions.rb:class Set
/srv/gems/GoNodes-0.0.1/lib/monkeypatch/set.rb:class Set
/srv/gems/Narnach-minitest-0.3.3/lib/set_ext.rb:class Set
/srv/gems/Ron-0.1.2/lib/ron.rb:class Set
/srv/gems/acapela-0.8.1/script/create_voices.rb:class Set
/srv/gems/annlat-0.0.1/lib/annlat/LaRuby.rb:class Set
/srv/gems/antlr4-0.9.2/lib/antlr4/base.rb:class Set
...

もちろん、この検索対象はあくまで公開gemだけなので、非公開アプリケーションのコードでどうかはわかりません。一方ここでコンフリクトする例が1つでも見つかったら諦めるというわけでもありません。それでも、非互換の影響の見積にはそうとう腐心しています。

なお、gem-codesearchはバックエンドにgoogle/codesearchを利用しています。codesearchはインデックスサイズに4GB制限があり、最近gemのコードサイズがこの制限に触れてしまったので、頭文字のアルファベット26文字+非アルファベット文字の27個にインデックスを分割するという延命をしたりしました(google/zoektも試しましたが、検索の起動がかなり遅く感じたので)。

クレジット:gem-codesearchはakrさんが開発し、hsbtさんがメンテナンスしています。コミッタ用共用サーバはRuby Associationのご提供で、gem-codesearchは私が管理しています。

dev-meeting-bot

議題を募集するチケットはコピペと手編集で作っていましたが、毎月となると、意外と重労働でした。 そこで、Slackボットを作りました。上述のチケットは、このボットにポンと作らせています。

他にも、最新の会議チケットを教えてくれる機能や、参加者にzoomやhackmdのURLをメールする機能が付いています。

クレジット:dev-meeting-botを含め以後紹介するSlackボットの多くは私が開発していますが、チケット作成機能は yebis0942 さんや znz さんのコントリビューションです。また、Herokuが計算機リソースを提供してくれています。

2. Ruby開発のリモート議論を支える技術

開発者会議以外でも、我々は日々Rubyの開発のための議論を行っています。 必要に応じてzoom(クックパッド提供)を使うこともありますが、ほとんどはSlack上での議論です。 そのためRuby開発者のSlackでは、議論を円滑にするためにいろいろなボットがうごめいています。 ざっとご紹介。

クレジット:Ruby開発者のSlackはRuby Associationにご提供いただいています。

all-ruby

all-rubyとは、これまでにリリースされたほぼすべてのruby(0.49から最新版まで)をビルドするスクリプト、およびその生成実行ファイルを集めたDockerイメージです。 みなさんのお手元でも、次のようにdocker pullして試すことができます。

$ docker pull rubylang/all-ruby
$ docker run --rm -ti rubylang/all-ruby
root@50d2785e9b39:/all-ruby# ./all-ruby -e 'puts "Hello"'
ruby-0.49             -e:1: syntax error
                  exit 1
ruby-0.50             -e:1: syntax error
                  exit 1
ruby-0.51             -e:1: undefined method `puts' for "main"(Object)
                  exit 1
ruby-0.54             -e:1:in method `puts': undefined method `puts' for "main"(Object)
                  exit 1
ruby-0.55             -e:1: undefined method `puts' for "main"(Object)
                  exit 1
...
ruby-0.76             -e:1: undefined method `puts' for "main"(Object)
                  exit 1
ruby-0.95             -e:1: undefined method `puts' for main(Object)
                  exit 1
...
ruby-1.0-961225       -e:1: undefined method `puts' for main(Object)
                  exit 1
ruby-1.0-971002       -e:1: NameError: undefined method `puts' for main(Object)
                  exit 1
...
ruby-1.0-971225       -e:1: NameError: undefined method `puts' for main(Object)
                  exit 1
ruby-1.1a0            -e:1: NameError| undefined method `puts' for main(Object)
                  exit 1
ruby-1.1a1            -e:1: NameError| undefined method `puts' for main(Object)
                  exit 1
ruby-1.1a2            -e:1: NameError:undefined method `puts' for main(Object)
                  exit 1
ruby-1.1a3            -e:1: NameError: undefined method `puts' for main(Object)
                  exit 1
...
ruby-1.1a9            -e:1: NameError: undefined method `puts' for main(Object)
                  exit 1
ruby-1.1b0            Hello
...
ruby-2.7.1            Hello
root@50d2785e9b39:/all-ruby#

Kernel#putsが入ったのはruby-1.1b0からだとわかりますね。このように、Rubyの仕様変更やバグを議論しているとき、どのバージョンから変わったのかを調べるのに役に立ちます。

これをより手軽に・議論中に試すために、Ruby開発者のSlackには「all-rubyボット」がいます。

all-ruby-bot

ボットのソースコードは公開されているので、興味がある人は設置してみてください(dockerコマンドが起動できて、かつSlackからのWebhookが受けられる計算機が必要です)。

クレジット:all-rubyはakrさんが作り(akrさんによる発表資料)、dockerイメージはhsbtさんがメンテナンスしています。Slackボットは私が作っています。

rubyfarm

all-rubyはリリース版ごとの実行ファイルでしたが、コミットごとの実行ファイルをためたイメージも作っています。 それがrubyfarmです。

これは、rubyfarm-bisectというgemから使うことを想定しています。 git bisectは、挙動が変わった(エンバグした・バグ修正された)タイミングをコミット単位で特定できるツールですが、rubyを毎回ビルドするのは比較的大変でした。 rubyfarm-bisectを使うと、コミットごとにビルドではなくdocker pullで済むので、待ち時間が大幅に減り、試行錯誤も容易になります。自動git bisectはいろんな理由で失敗することで有名なので、ビルド失敗がない&試行錯誤がやりやすい、というのはだいぶストレスを下げます。

ただ、Docker Hubがストレージを制限する方針を発表したので、非公開ローカルのレジストリに移行する予定です。

commit-link

挙動変更のあったコミットを特定できたら、それについてSlackで議論をするでしょう。 そのとき、コミットハッシュをコピペするだけでは、読む人が自分でgit showにかけたり、GitHubのリンクをたどったりする必要があって、ストレスフルでした。

そこで、Ruby開発者のSlackにはcommit-linkというボットがいます。

commit-botは全発言をウォッチしていて、10桁以上のコミットハッシュっぽいものがあったらGitHubを見てコミットが実在するか確認し、発見したらリンクを貼ってくれます。 地味ですが、非常に便利です。

その他

他にも、標準添付ライブラリのメンテナを教えてくれる"who-is-maintainer"や、脆弱性報告を議論する専用チャンネルを作ってくれる"h1-channel-creator"など、いろいろなボットがRuby開発議論を支えています。

3. Rubyの品質を支える技術

最後に、Rubyの品質を高めるためのCIの活動について。

Rubyには、CIがたくさんあります。 私が把握している限りで、GitHub Actions、Travis、AppVeyor CI(Windows)、弊社笹田による独自CI、そしてrubyci.orgです。 このようになっているのは(歴史的経緯もありますが)テストの目的がいろいろあるためです。 たとえば笹田独自CIは、まれにしか発生しないハイゼンバグ(GCやVM最適化周りでしばしば発生する)を洗い出すために同じコミットを何度も繰り返しテストし続けます。

rubyci

rubyci.orgは、たぶんRubyで一番古くからあるCIです。

もともとは、chkbuildというRuby専用のテスト実行ツールがあり、有志が自分の環境でchkbuildを実行した結果をキュレーションしたのがrubyci.orgでした。 しかし現在では、通常のCIはGitHub ActionsやTravisで十分になったので、主な特徴が変わっています。

  • GitHub Actionsにないような、ややマイナーな環境もカバーしている
  • コミッタは(ほとんどの)テスト環境に直接sshログインでき、デバッグができる
  • テスト中に表示される警告数も監視している

とくに、再現性の低いバグと戦わなければならないRubyコア開発においては、2番目の特徴は重要になっています。

現在では多くのテスト環境はAWS EC2インスタンス上で動作しています。これらの環境をセットアップしたり、コミッタのアカウントを自動定義したりするために、mitamaeを使った自動化もなされています(ruby/ruby-infra-recipe)。

クレジット:chkbuildはakrさんが作り、rubyci.orgはnaruseさんが作りました。その後のメンテナンスは主にhsbtさんと私が行っています。mitamaeによるプロビジョニングはhsbtさんが整理しました。AWSの費用はRuby Associationがカバーしてくれています。他にも、一部の環境はご提供を受けています(rubyci.org末尾のスポンサーリストを参照のこと)。

alerts-emoji

CIの結果を監視するのは苦行なので、みんなSlackなどへの通知を活用していると思います。 しかしRubyには多数のCIがあり、それぞれ好き勝手なフォーマットで通知を投げていたので、そのチャンネルを眺めて理解するだけでも苦行となっていました。

また、テスト実行に数時間かかる環境などもあるため、ちょっと古いコミットに対する通知が遅れてくることが多く、一体どのコミットに対する通知なのかパッと見でわからない、という問題もありました。

そこで、すべての通知を集約し、統一フォーマットで通知するようにしました。 また、どのコミットに対する通知かを視覚的にわかりやすくするため、コミットに適当な幾何学図形を割り当てることにしました。

なお、絵文字については非常に議論がありました。 私は10桁程度の16進数をパッと見で比較するのが苦手 *2なので、ランダムに選んだ絵文字を振るようにしました(絵文字のほうが視覚的に比較しやすい)。 しかしそうすると、絵文字から意味を読み取ってしまう人から、逆に認知コストが上がったという反対の声があがりました(たとえばスマイルマークや㊙のように意味のある絵文字)。 議論と試行錯誤の末、色違いの幾何学図形の絵文字を使うようにすることで、だいたいの人が満足できるようになりました。

クレジット:まず笹田が独自CI用の通知チャンネルを作りました。そのチャンネルに各種通知が参入して破綻したので、この統一チャンネルを遠藤が作り、その後k0kubunさんがいい感じに整理してくれました。

まとめ

Ruby開発は、多数の技術、および(自分を含めた :-)多数の人々によって支えられています。

ここに上げたものはあくまで遠藤がかかわっているものの一部だけで、上げきれなかったものもいろいろあります(すみません)。みなさんに感謝しながらやっています *3

*1:正確には、去年も私がすべての議題を理解するように努めていたのですが、丸一日消費してしまうことに加え、一人では議題を誤解したり、論点を洗い出せなかったりして、いまいちだったので、笹田と相談して準備会にトライすることになりました。

*2:ハッシュの桁数が、GitHub Actionsは7桁、rubyciは10桁、というように通知ごとにバラバラだったのが特に最悪でした。

*3:特にCIのAWS費用がなかなか大変なので、その費用をカバーしてくれているRuby Associationに寄付をいただけるとありがたいです :-)

Cookpad Pad 2 という自作キーボードノベルティをつくった話

$
0
0

こんにちは、高井です。みなさま Cookpad Online Summer Internship 2020、お疲れさまでした。

さて、今回はインターンのノベルティとして Cookpad Pad 2 という自作キーボードキットをノベルティとしてつくったので、その紹介と解説です。今年のインターンシップはリモート開催ということもあり、ふだんよりも豪華なノベルティをつくることができました。写真では見えませんが、裏側に Cookpad Online Summer Internship 2020 のロゴがプリントされていて、限定感を出しています。

f:id:takai_naoto:20200910181821j:plain

というわけで、本記事では Cookpad Pad 2 を例に取りながら、自作キーボードキットを作成する方法について解説します。キーボードの開発はさまざまなノウハウが公開されているため、実際のところそれほど難しくはありません。本記事ではキーボード開発についての完全な手順を説明するというよりも、有益な記事やリソースを紹介することで、全体の流れとして個々のノウハウを結びつけることを目指します。

キーボードを設計、開発するときの流れは次のようなものです。この流れに沿いながら、どのようにキーボードを開発するのかを見ていきましょう。

  • プロダクト構想
  • キーボード基板設計
    • 回路図設計
    • パーツ選定
    • プリント基板設計
  • キーボードケース設計
    • 設計
    • 検証(アセンブリ)
  • 製造
    • キーボード基板
    • キーボードケース
  • ファームウェア開発
    • QMK Firmware
    • VIA

また、Cookpad Pad 2 はオープンハードウェアとして GitHub でプロジェクトを公開しています。記事とあわせてこちらも参照してください。

https://github.com/cookpad/cookpad-pad/tree/cookpad-pad2

プロダクト構想

まず、最初に行なうのは、どのようなキーボードをつくるのか構想することです。Cookpad Pad 2 は 6 キーのマクロパッドです。デフォルトでは「C」「O」「K」「P」「A」「D」の 6 文字が打てるので、「cookpad」と打つときに便利なキーボードです。初代のモデルは、2019年頃に製作をして、ノベルティとして個人的に配布をしていました。

デザインは、この初代 Cookpad Pad を踏襲するものとします。 6 キーを格子状に配置したシンプルなデザインです。また、ケースは PCB を 2 枚のプレートで挟み込んだ、サンドイッチマウント構造にします。この構造は自作キーボードでよく採用される構造で、安価に作成できることがメリットです。

それから、コネクターとして USB Type-C を採用します。そのために、 Pro Micro というマイコンボードを利用しない方針とします。 Pro Micro は自作キーボードに必要な電子部品が実装されているので、基板の設計を単純にすることができます。一方で、Micro USB Type-Bコネクタが採用されていたり、ピンヘッダでマイコンボードを取り付けるため、設計上の制約がでてしまうという欠点があります。そこで、 Pro Micro の利用をするのではなく、直接基板に同等の機能を実装していきます。

キーボード基板設計

ここまで決まったら、基板の設計を行なっていきます。今回は構造が単純なサンドイッチマウントを採用しているため基板を先に設計しますが、アルミ切削ケースなどの場合はケースの設計から先に行なった方がよいでしょう。

基板の設計には KiCadをつかいます。 KiCad はフリーソフトウェアとして配布されているEDAツールです。独特の操作性を持つので、はじめての人であれば、「KiCadことはじめ」などを参考に、一通り触ってみてください。

回路図

キーボードの基板設計については「ai03's Keybaord PCB Designer Guide」を参照してください。 KiCad の利用方法から回路設計、PCB設計まで一通りが解説されているので、これを読むだけでもキーボード設計ができるようになります。

自作キーボードの MCU は ATmega32u4 を利用することが一般的ですが、今回は ATmega32u2 を利用しています。ATmega32u2 の方が若干フットプリントが小さいため、部品配置が楽になるかもしれないというのが選定理由です。どちらもキーボードファームウェアとして広く利用されている QMK Firmwareでサポートされている MCU です。特に理由がないのであれば ATmega32u4 を採用するのがよいとおもいます。

その他、TVSダイオードによる静電気放電対策やポリスイッチによる過電流対策を行なっています。キーボードのUSB-C関連の設計については、ai03氏が中心となって公開している Unified Daughterboard Projectが参考になるでしょう。今回のキーボードでは、それよりも簡略化された実装にしています。

回路図シンボルなども ai03氏が公開しているものを利用するのが便利です。ai03氏、いったい何者なんだ……。

実際の回路図は下記のようなものになります。左上がMCU関連、右上がキーマトリクス、左下がUSB関連、右下が電源関連となります。

f:id:takai_naoto:20200910181904p:plain

なお、このサイズのキーボードであれば、MCUのピンとスイッチを一対一で対応させればよいので、本来ならばキーマトリックスは不要です。このプロジェクトを拡張することで、好きなキーボードつくってもらえるようにという意図を込めてキーマトリクスを採用しています。そのとき、「KICADの他のプロジェクトから回路をコピーする」のやり方でコピーすると便利でしょう。

パーツ選定

プリント基板設計に進むにあたって、パーツの選定をします。今回は基板製造を Elecrowへ依頼するつもりでしたので、Elecrowが在庫しているElecrow Parts Libraryや、部品の取り寄せができるDigi-Key Electronicsなどから部品を選定します。

パーツの選定は既存のキーボードで利用されている部品から選定するのがよいでしょう。ai03氏のリポジトリからUnified Daughterboard ProjectKBD8X MKIIOrbitなどを参考にしています。USB-C のコネクタは TYPE-C-31-M-12を採用します。この部品は、USB2.0にのみ対応したコネクターで、その分安価です。

それから、コンデンサや抵抗など一般的な部品は、Elecrowの方で代替品に差し替えてもいいかと確認されることがあります。そちらの方がコスト的にもメリットがあるので、それらのパーツについては仕様を示すくらいの気持ちで選定しています。

Component Package Footprint URL
Ceramic Capacitor, 22pF 0603 Capacitor_SMD:C_0603_1608Metric Elecrow Parts Library
Ceramic Capacitor, 1uF 0603 Capacitor_SMD:C_0603_1608Metric Elecrow Parts Library
Ceramic Capacitor, 0.1uF 0603 Capacitor_SMD:C_0603_1608Metric Elecrow Parts Library
Ceramic Capacitor, 10uF 0603 Capacitor_SMD:C_0603_1608Metric https://www.digikey.com/product-detail/en/murata-electronics/GRM188R61E106KA73D/490-18214-2-ND/9867922
Diode, Generic SOD-123 Diode_SMD:D_SOD-123 https://www.digikey.com/product-detail/en/micro-commercial-co/1N4148W-TP/1N4148WTPMSCT-ND/717311
TVS Diode, 5.5V SOT143B random-keyboard-parts:SOT143B https://www.digikey.com/product-detail/en/nexperia-usa-inc/PRTR5V0U2X215/1727-3884-1-ND/1589981
Polyfuse, 500mA hold, 1A trip 1206 Fuse:Fuse_1206_3216Metric https://www.digikey.com/product-detail/en/bel-fuse-inc/0ZCJ0050AF2E/507-1803-1-ND/4156312
Resistor, 5.1k 0805 Resistor_SMD:R_0805_2012Metric https://www.digikey.com/product-detail/en/panasonic-electronic-components/ERA-6AEB512V/P5-1KDACT-ND/1465964
Resistor, 22 0805 Resistor_SMD:R_0805_2012Metric https://www.digikey.com/product-detail/en/panasonic-electronic-components/ERA-6AHD220V/P123893CT-ND/9467822
Resistor, 10k 0805 Resistor_SMD:R_0805_2012Metric https://www.digikey.com/product-detail/en/panasonic-electronic-components/ERA-6AEB103V/P10KDACT-ND/1465971
Low Profile Tactile Switch 5.2x5.2mm  5.25.21.5mm random-keyboard-parts:SKQG-1155865 https://www.digikey.com/product-detail/en/c-k/RS-187R05A2-DS-MT-RT/CKN10361CT-ND/2747199
ATMEGA32U2 Microcontroller 32-TQFP 7x7mm Package_QFP:TQFP-32_7x7mm_P0.8mm https://www.digikey.com/product-detail/en/microchip-technology/ATMEGA32U2-AU/ATMEGA32U2-AU-ND/2187167
Crystal, 16MHz 3.2x2.5mm, 4 pad Type-C:HRO-TYPE-C-31-M-12-Assembly https://www.digikey.com/product-detail/en/CX3225SB16000D0GZJC1/1253-1698-1-ND/5995245/
HRO-TYPE-C-31-M-12 8.94x7.3mm Crystal:Crystal_SMD_3225-4Pin_3.2x2.5mm https://lcsc.com/product-detail/USB-Type-C_Korean-Hroparts-Elec-TYPE-C-31-M-12_C165948.html

プリント基板設計

回路が決まり、部品も決まりましたので、あとは PCB に配置していくだけです。配線をしているとパズルを解いているような気分になります。どのようにしたら効率的で美しい配線ができるのか、工夫の見せどころです。KiCad Pcbnew をつかって設計をしていくのですが、細かい操作方法についてはここでは説明しません。前述の「KiCadことはじめ」や「ai03's Keybaord PCB Designer Guide」を参照してください。

最初に ai03 - Keybaord Plate Generatorをつかって、スイッチプレートを生成します。 Keyboard Layout Editorの Raw Data をコピーアンドペーストするだけでスイッチプレートを生成することができます。ファイルは DXF 形式でダウンロードできます。

次に、 KiCad Pcbnew にスイッチプレートファイルをインポートします。プレートにあわせてキースイッチのフットプリントを配置していきます。それから、その他の部品を配置します。USB 周辺や MCU 周辺から配線をしていくとよいでしょう。最後に、キーマトリクスをつくっておしまいです。このプロセスは、あまりキーボードに特有なことはありません。電源とGNDのパターンは太くする、水晶発振器はできるだけMCUに近いところに配置する、バイパスコンデンサは電源ピンの近くにする、USBは差動ペア配線なのでパターンを可能な限り平行に行なう、などに注意をして配線します。

f:id:takai_naoto:20200910181942p:plain

その他、ちょっとしたノウハウとなりますが、キー数が多いときにはスイッチ毎にダイオードを配置するのが面倒なときがあります。KiCad PcbnewはPythonでのスクリプティングが可能ですから、次のようなスクリプトをつくって配置しています。定数を変更してつかってみてください。

import pcbnew

START    = 1
STOP     = 42

X_OFFSET = 5937250
Y_OFFSET = 4857750
ORIENTATION = 90 * 10

board = pcbnew.GetBoard()

for i inxrange(START, STOP + 1):
    mx = board.FindModuleByReference("MX%d" % i)
    mx_pos = mx.GetPosition()

    new_pos = pcbnew.wxPoint(mx_pos.x + X_OFFSET, mx_pos. y + Y_OFFSET)

    diode = board.FindModuleByReference("D%d" % i)
    diode.SetPosition(new_pos)
    diode.SetOrientation(ORIENTATION)

    print("D%d moves to (%d, %d)" %(i, new_pos.x, new_pos.y))

pcbnew.Refresh()

キーボードケース設計

設計

次にサンドイッチマウントのケースをつくっていきます。サンドイッチマウントとは、PCBをスイッチプレートとボトムケースで挟み込むような構造です。キーボードのマウント方式については「Cheat sheet: Custom keyboard mounting styles」がよくまとまっています。

今回は、サンドイッチマウント構造でもPCBを2枚のプレートによって挟み込む、よりシンプルな構造です。当初、プレートにはアクリルを採用しようと考えていたのですが、ご時世によりアクリルが品薄になっていたため、プリント基板(FR4)で製作することにしました。

ケースの設計は Autodesk Fusion 360 をつかいます。 Fusion 360 の基本的な操作については「Fusion 360 で簡単なケースを作る(初心者向け) - Self-Made Keyboards in Japan」などを参照してください。また、公式サイトの 入門セミナーも参考になるでしょう。

まず、スイッチプレートのDXFファイルを読み込み、プレートを支えるための M2 スペーサーのスペースをつくります。廣杉計器 M2 スペーサーの直径は 4mm ですので、キースイッチと干渉しないようにします。

f:id:takai_naoto:20200910182015p:plain

それから、「押し出し」て、 M2 ねじを通すために 2.2mm くらいで「穴」を作成し、「フィレット」で角を丸めます。できた面にスケッチを作成して、それを DXF としてエクスポートすればスイッチプレートの完成です。ボトムプレートも同じような構造ですので、コピーをして履歴を編集すれば簡単に作成することができます。

f:id:takai_naoto:20200910182038p:plain

検証(アセンブリ)

PCB とケース用のプレートの設計が完成したので、実際に組み立てて問題が起こらないか確認してみましょう。KiCad では基板の3Dデータを STEP ファイルを出力することができます。これを Fusion 360 に取り込み、先程つくったプレートファイルと組み合わせます。

スイッチプレートとPCBの間隔は 3.4 mmです。これは、Cheryr MX シリーズのデーターシートを確認するとボトムハウジングの高さは 5 mm ですので、ここからプレートの厚みである 1.6 mmを引いた数字です。ボトムプレートとPCBの間隔は、PCBに実装されている USB-C コネクターの高さが 3.21 mmですから、それ以上あればよさそうです。 9 mm のスペーサーを利用することを考えて 4mm にします。

完成したら、部品の干渉など問題が発生していないことを確認します。

f:id:takai_naoto:20200910182057p:plain

製造

キーボード基板

基板の製造と電子部品の実装は Elecrow に発注します。 Elecrow は深圳にある工場で、品質と価格のバランスが良いことから、製品の製造のときにはいつも利用しています。発注にあたっては、 KiCad でガーバーフォーマットのデータを作成します。オプションを次の画像のように選択します。さらに、ドリルファイルも作成します。

f:id:takai_naoto:20200910182121p:plainf:id:takai_naoto:20200910182144p:plain

さらに、 Elecrow では生成されたファイルの拡張子を変更する必要があります。この作業は面倒で間違いやすい作業ですので、私はちょっとしたスクリプトを利用して拡張子の変更を行なっています。拡張子を変更したら、ガーバーデータをZip形式でアーカイブします。

#!/bin/bashPROJECT=$1OUTPUT=elecrow

mkdir"$OUTPUT"

cp "$PROJECT"-F_Cu.gtl      "$OUTPUT"/"$PROJECT".GTL
cp "$PROJECT"-B_Cu.gbl      "$OUTPUT"/"$PROJECT".GBL
cp "$PROJECT"-F_SilkS.gto   "$OUTPUT"/"$PROJECT".GTO
cp "$PROJECT"-B_SilkS.gbo   "$OUTPUT"/"$PROJECT".GBO
cp "$PROJECT"-F_Mask.gts    "$OUTPUT"/"$PROJECT".GTS
cp "$PROJECT"-B_Mask.gbs    "$OUTPUT"/"$PROJECT".GBS
cp "$PROJECT"-Edge_Cuts.gm1 "$OUTPUT"/"$PROJECT".GML
cp "$PROJECT"-PTH.drl       "$OUTPUT"/"$PROJECT"-PTH.TXT
cp "$PROJECT"-NPTH.drl      "$OUTPUT"/"$PROJECT"-NPTH.TXT

次に Excel で発注書をつくります。 Elecrow でも 発注テンプレートを用意していますし、実際に発注するときに利用したファイルもリポジトリに入れておきますので、参考にしてください。

Zip形式でアーカイブしたガーバーファイル、Excelの発注書を添付して service@elecrow.com へメールで発注をします。このとき、配送方法についても指定をするとよいでしょう。英語については自信がないのですが、下記のような文面でいつも送っています。

To whom it may concern,

I'd like to order PCBA service. Could you give me a quotation for attached files? The shipping address is as follows. I would prefer OCS as a shipping method.

XXXXX XX-XX-XX, XXXX, Tokyo XXX-XXX, Japan

Best Regards,

ほどなく担当者から見積が来るので、内容に問題がなければ PayPal で支払いをします。製造には、だいたい1ヶ月くらいかかるようです。製造のプロセスで何か問題が見付かれば、それも連絡してくれます。私は、ガーバーファイルでのフットプリントと部品のパッケージが一致しないミスをよくします。

キーボードケース

今回は FR4 、つまり PCB でプレートをつくるので、 KiCad でデータを作成します。 DXF ファイルをインポートするときにグラフィックレイヤーで Edge.Cuts 指定すれば、外形情報として取り込むことができますので、そのデータをもとにガーバーフォーマットのデータを書き出します。

発注は Elecrowのフォームから行ないます。プレートの厚さはデータシート上で 1.5mm なので、PCBではその厚さに近い 1.6mm を選択します。こちらは1週間ほどで届きます。

アクリルプレートを発注するときは、レーザー加工サービスによって指定のテンプレートがありますので、そのフォーマットに従って発注をします。個人的な利用であれば工房Emerge+のレーザー加工サービスを利用することが多いです。Illustrator形式ファイルのテンプレートにDXFファイルを取り込み、データを作成します。アクリルは 2mm のものを選びます。フォームから見積を依頼したうえで支払いをすると、こちらも1週間ほどで届きいます。

ファームウェア開発

ハードウェアができたらファームウェアをつくっていきましょう。今回は最低限キーボードとして動作するための実装を目指し、 QMK Firmeware や VIA のリポジトリに取り込んでもらうことを考えないようにします。

QMK Firmware

ファームウェアはフリーソフトウェアとして開発されている QMK Firmwareを利用します。QMK Firmware の開発は、事前にセットアップが必要です。公式ドキュメントの「Setting Up Your QMK Environment」を読み、セットアップをしてください。

QMK Firmware のトップディレクトリで util/new_keyboard.shを実行すると、 keyboards ディレクトリ以下にテンプレートができますので、そのファイルを編集していきます。Cookpad Pad 2 の差分を例にとりながら、編集する部分を見ていきましょう。

config.h:23-28ではUSBのベンダーID、プロダクトIDを定数として定義します。USBのベンダーID、プロダクトIDについては衝突しないことが求められていますが、テスト目的のIDなどは定義されていません。なので、「いい感じにする」必要があります。詳しくは「 USBのベンダーIDとプロダクトIDの話」などを参照してください。

#define VENDOR_ID 0xFEED#define PRODUCT_ID 0x9009#define DEVICE_VER 0x0002#define MANUFACTURER Cookpad Inc.#define PRODUCT Cookpad Pad#define DESCRIPTION A six keys macro pad made by Cookpad.

config.h:31-45では、キーマトリクスの定義を行ないます。Cookapd Pad は 2行 × 3列 のマトリクスですので、そのように定義をします。さらに回路図を確認し、ピンとの対応も定義します。

#define MATRIX_ROWS 2#define MATRIX_COLS 3

...

#define MATRIX_ROW_PINS { C6, C7 }#define MATRIX_COL_PINS { B7, B6, B5 }

Cookpad Pad 2 は MCU として ATmega32u2 を利用していますので rules.mk:2の部分も変更します。 ATmega32u4 であれば変更は必要ありません。

MCU = atmega32u2

cookpad_pad.h:29-36はキーボードの物理配列の定義です。キーボードによっては、行と列の二次元配列として定義されているキーマトリクスの全てが入力可能というわけではありません。そこで、キーボードの物理配列とキーマトリクスを対応させるためのマクロを定義します。

#define LAYOUT( \    K00, K01, K02, \    K10, K11, K12  \) \{ \    { K00, K01, K02 }, \    { K10, K11, K12 }  \}

keymaps/default/keymap.c:18-23で、キーボードの論理配列のデフォルト値を定義します。キーコードについては「Full List」を参照してください。

constuint16_t PROGMEM keymaps[][MATRIX_ROWS][MATRIX_COLS] = {
    [0] = LAYOUT(
        KC_C, KC_O, KC_K,
        KC_P, KC_A, KC_D
    )
};

変更が完了したらビルドが通るかどうかを確認してください。ファームウェアを書き込むには工場出荷時に書き込まれているブートローダのDFU機能をつかって、USB経由で書き込みをします。

$ make cookpad_pad:default
$ make cookpad_pad:default:dfu

VIA

VIAは QMK を前提につくられたキー設定ツールです。それまで、 QMK Firmware でキーレイアウトを変更するためには、プログラムを修正してコンパイルする必要がありました。 VIA を使うと、 GUI でコンパイルなしにキーマップを変更することができます。

f:id:takai_naoto:20200910182237p:plain

キーボードを VIA に対応させるためには QMK Firmware で VIA に対応したキーマップを作成する必要あります。対応する方法については公式サイトの「Configuring QMK」や「(設計者向け)VIA対応のファームウェアを作ろう」などを参照してください。

Cookpad Pad 2 でいうとkeymaps/via/rules.mkが VIA 対応となります。主だったものは VIA_ENABLEだけです。その他は、 VIA では対応していない機能を無効にしたり、ファームウェアのサイズを減らすためのオプションとなります。

VIA_ENABLE = yes
LINK_TIME_OPTIMIZATION_ENABLE = yes
EXTRAKEY_ENABLE = no
MOUSEKEY_ENABLE = no
CONSOLE_ENABLE = no
MIDI_ENABLE = no

それから、 VIA でつかう cookpad_pad.jsonも用意します。便宜上、 QMK のリポジトリに含めていますが、このファイルを含んだままプルリクエストを送ると、余分なファイルがあるとレビューで指摘されるので注意してください。

{"name": "Cookpa Pad",
    "vendorId": "0xFEED",
    "productId": "0x9009",
    "lighting": "none",
    "matrix": {"rows": 2, "cols": 3 },
    "layouts": {"keymap": [["0,0","0,1","0,2"],
            ["1,0","1,1","1,2"]]}}

まとめ

本記事では Cookpad Pad 2 を事例として、キーボードの基板設計からケース設計、製造、ファームウェア開発について簡単に解説をしてきました。実際のところ、まだまだ細かな解説をする部分はありますが、大まかな流れについては理解いただけたのではないでしょうか。

Cookpad Pad 2 は オープンハードウェアとして公開していますので、このプロジェクトを改変して新しいキーボードをつくってみることにチャレンジしてみてください。

Cookpad Online Summer Internship 2020 5 Day Engineer コースを開催しました

$
0
0

ユーザー・決済基盤部の三吉です。今年になってエンジニアの立場から新卒採用を担当しています。

5月の記事で告知したサマーインターンシップのうち、5 Day Engineer コースを 8/24〜28、9/7〜11 の2日程で開催しました。この記事ではその内容を紹介します。

3 Day Product Design コースについては、以下の記事をご覧ください。

5 Day Engineer コースは、前半2日間が講義形式、後半3日間が実践形式でした。 前半はプラットフォーム別の技術講義とサービス開発講義の2本立てです。 後半は PBL (Project-Based Learning) として、サービス開発の実践に取り組みます。

今年はオンラインでの実施となりましたが、例年同様たくさんの学生に参加いただきました。 以下、各パートの詳細です。

技術講義

初日のオリエンが終わった直後から2日目の午前までが技術講義でした。 申込時に選択した Web、Android、iOS のプラットフォーム別にクックパッドの開発手法について学びます。 例年はサーバーサイドの経験しかない学生にもモバイルの講義を受けてもらっていましたが、今年は募集時からプラットフォーム別だったので、例年より一歩踏み込んだ内容になっています。

Web

クックパッドで実際に行われている開発フローをベースに、AWS 上でのデプロイパイプラインの構築や Ruby on Rails アプリケーションの実装・改善を体験してもらいました。

Android

レシピの閲覧・投稿ができるアプリ MiniCookpad の開発を通して、Android 版クックパッドアプリで採用されているアーキテクチャやテスト手法を体験してもらいました。

iOS

Android と同じく、MiniCookpad の開発を通して iOS 版クックパッドアプリで採用されているアーキテクチャやテスト手法を体験してもらいました。内容の大筋は Android・iOS 間で事前にすり合わせています。

講義資料は リポジトリの Documents 以下にあります。

サービス開発講義

2日目午後はサービス開発講義です。 半日という短い時間でしたが、クックパッドのサービス開発手法について講義したのち、あるユーザーの課題を解決するアプリケーションの考案やプロトタイプの作成まで行いました。 技術講義とは打って変わってコードを一行も書かない講義でしたが、参加者から「クックパッドのサービス開発手法を知ることができた」と好評でした。

PBL

後半、3日目からはサービス開発を実践する PBL です。 「一人暮らしをしている人の料理が楽しみになるアプリケーション」というテーマをもとに各自でアプリケーションを開発し、最終日に成果発表します。 PBL 中は社員がメンターとしてつき、サービスの相談に乗ったり開発をサポートしたりしました。

オンライン開催にあたって

スプリングインターンシップもオンラインでの開催でしたが、複数日にまたがって実施するのはこのサマーインターンシップが初めてでした。 ここでは、オンライン開催にあたっての取り組みを紹介します。

まず、オンライン開催にあたり、インターンシップ全体を通して利用したツールは次のとおりです。

  • Zoom
    • インターンシップ開催中は常に Zoom ミーティングに参加してもらうようにしました。チームに分かれる場合や、PBL の作業時間、懇親会等ではブレイクアウトルームを活用し、こちらからの一方通行のコミュニケーションにならないよう意識しました。
  • Slack
    • Zoom のチャット機能は使わず、文字ベースのコミュニケーションは Slack に統一しました。講義中など声を出しにくい状況でも、随時 Slack で質問を受け付けました。
  • Kibela*1
    • 連絡事項の共有や日報の提出に利用しました。Slack がフロー情報を扱うのに対し、Kibela ではストック情報を扱います。

オンライン開催による難点として、学生がオフィスの様子を知ることができないという点があります(当然ですが)。 これについて、オンラインでのオフィスツアーを実施しました。 社員がオフィスからスマートフォンで Zoom ミーティングに参加し、都度解説しながらオフィスを一周するというものです。

一方で、オンライン開催ならではの出来事もありました。 PBL 期間中、ある学生は開発中のアプリを友だちに使ってもらってユーザーインタビューしていました。 また、スプリングインターンに続き、海外から参加する学生もいました。 これらはオンラインで物理的制約がないからこそ可能なことです。

おまけ: ノベルティ

f:id:sankichi92:20200915115625j:plain
Tシャツ、エコバッグ、ステッカー、クックパッドロゴウォーター
f:id:sankichi92:20200915115728j:plain
自作キーボードキット Cookpad Pad 2(組み立て済み、裏側)

写真のノベルティセット(Tシャツ、エコバッグ、ステッカー、自作キーボードキット)を休憩時用の軽食と合わせて事前に送付しました。 また、この他にサマーインターンシップのロゴが入った Zoom のバーチャル背景用の画像も配布しています。

自作キーボードキット Cookpad Pad 2 について詳しくは以下の記事をご覧ください。


以上が、Cookpad Online Summer Internship 2020、5 Day Engineer コースの開催報告です。 ご参加いただいた皆さま、本当にありがとうございました!

今年のサマーインターンシップは終わってしまいましたが、クックパッドでは就業型インターンシップを通年で募集しています。 興味のある方はぜひご応募ください!

*1:これまでのサマーインターンシップでは Groupad という内製の情報共有ツールを利用していたのですが、例年と違って学生各自の PC から参加してもらう形をとった都合上使用できませんでした。

iOSDC Japan 2020 に社員2名が登壇します

$
0
0

こんにちは!とくなり餃子大好き( id:tokunarigyozadaisuki)です。
すっかり秋の気配がしてきましたね。餃子を焼きやすい気候になって嬉しい!

さて、iOSと周辺技術を題材としたカンファレンス、iOSDC Japan 2020が今年は9月19日(土)〜9月21日(月・祝)にオンラインにて開催されますね!

トークのご紹介

クックパッドは、ゴールドスポンサーをさせていただいております。 9月21日(月・祝)12:30〜 Track A では、スポンサーセッションとして、@yujif_@n_atmarkが登壇します。

これまで公開したことのない、新しい取り組みに関するトークです。お楽しみに!

「クックパッドが、革新的な方法でまったく新しい買い物体験を皆様にお届けします」

クックパッドは「毎日の料理を楽しみにする」をミッションに、約20年前から料理のアイデアや工夫が流通する仕組みづくりに取り組んできました。現在も日本から世界中に広がる「プラットフォームをつくる」挑戦を続けています。

一方で、共働きの増加や核家族化、外食やデリバリーの普及など、毎日の料理を囲む環境は目まぐるしく変わってきています。

クックパッドはこれからの100年に向けて「レシピ」のその先にも取り組んでいます。 次の100年のスタンダードになる「食」の流通をつくるため、2018年に生鮮食品ECプラットフォーム「クックパッドマート」を開始しました。

「クックパッドマート」では今までにない仕組みで、毎日の献立づくり、買い物、調理すべてを変えることに挑んでおり、「新鮮で美味しい食材」を当たり前に安く買えるように、ゼロから食品流通プラットフォームの構築を進めています。

本セッションでは、クックパッドの新しい挑戦である「クックパッドマート」における開発や、既存の「レシピ」を組み合わせたまったく新しい買い物体験の実現、それらを支えるiOSアプリのアーキテクチャや SwiftUI の活用についてお話しします。

おわりに

カンファレンスには、他にも多くの社員が参加する予定です。スポンサーセッションなどに関してご質問やご感想等ございましたら、お気軽にお声がけください! 

クックパッドでは、iOSのサービス開発に一緒に取り組んでくれる仲間を募集しています。トークを見て少しでも興味を持っていただいた方にはこちらをご参照いただけましたら幸いです。

募集要項:iOSエンジニア

info.cookpad.com

Viewing all 802 articles
Browse latest View live