TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

939

こんにちは! Google I/O 2019から3週間ほど経ちましたが、全国各地でGoogle I/O報告会が行われておりまだまだGoogle I/O熱はまだまだ続きそうですね。 Google I/Oには弊社からも3名( @rllllho , @ysk_ur 、山田)が参加しました。 参加レポートはすでに多くあるため、この記事では実際に参加したメンバーがGoogle I/Oのおすすめのセッションをピックアップしてご紹介します! Android関連のおすすめセッション 自己紹介 What’s New in Android Studio UI Design and Debugging Tools (Google I/O'19) LayoutEditor NavigationEditor ResourceManager LayoutInspector おすすめ理由 MLKit: Machine Learning for Mobile with Firebase (Google I/O'19) テキスト翻訳 物体検出からの検索 カスタムモデルの作成 おすすめ理由 Motional Intelligence: Build Smarter Animations (Google I/O'19) reentrant continuos smooth おすすめ理由 感想 Web関連のおすすめセッション 自己紹介 What’s New with Chrome and the Web (Google I/O ’19) Lazy loading LighthouseでPerformance Budgetの計測ができるように Portals Google Duplex on the web Fugu WebAuthn Speed at Scale: Web Performance Tips and Tricks from the Trenches (Google I/O ’19) Unlocking New Capabilities for the Web 感想 AI/ML関連のおすすめセッション 自己紹介 Designing Human-Centered AI Products (1)成功の定義についてと本当にAIを使う必要があるのか? (2)ユーザーのニーズをデータへと変換する おすすめ理由 Machine Learning Fairness: Lessons Learned おすすめ理由 感想 最後に Android関連のおすすめセッション 自己紹介 ZOZOテクノロジーズでAndroidアプリエンジニアをしております山田です。 ZOZOTOWNアプリのAndroidチームリーダーもしております。 Google I/Oは今回初参加だったのですがいろんな意味でワクワクドキドキできる3日間でした。 その中で見たときの印象が強かったセッションを3つ紹介したいと思います。 What’s New in Android Studio UI Design and Debugging Tools (Google I/O'19) UI実装におけるAndroid Studioの新しい機能について紹介しています。 機能の種類としては4つになります。LayoutEditor、NavigationEditor、ResourceManager、LayoutInspectorです。 (最後にちょっとだけOS Q向けの新しいAPIの話もあります) LayoutEditor ConstraintLayoutの制約を設定する話とサンプルデータを設定する方法について紹介しています。これらの方法が右クリックで出てくるメニューにも追加されより利用しやすくなりました。 NavigationEditor NavigationEditorを使って一から画面遷移を実装していく方法について紹介しています。 引数の設定もこのEditer上で表現できるので、より使いやすくなっている印象です。 ResourceManager これ自体が新しい機能で、リソースファイルの追加やアクセスの補助をする方法について紹介していました。ドラッグ&ドロップでの追加に対応しているのと、さらにそこから画面密度別の自動振り分けまでしてくれるのはお見事の一言です。リソースのグルーピングもできるようで数が増えてくるとだんだんアクセスが難しくなっていくAndroid開発のツラミも解消してくれるなかなか便利なツールだと思います。 LayoutInspector 設定された各属性値の値が見やすくなっただけでなく、リアルタイム修正まで対応できるようになったようです。また複雑なレイアウトを構築した際にぶち当たる下の階層のViewにアクセスできない問題も、3D表示にすることで解決してくれます。 こちらは今後追加される機能のようですがレイアウトのデバッグがかなり便利になりそうなので期待しています。 おすすめ理由 いずれのツールも開発効率を上げるための手助けになると思います。これを知っているのと知らないのとでは開発効率に差が出てくるでしょう。 とくにLayoutEditorとResourceManagerは特別な設定なしにすぐに利用できるので、試しに触ってみると良いと思います。 また、今回LayoutEditorの話の中で、RecyclerViewなどにサンプルデータを設定する機能を紹介していました。 機能自体は元々あったようですが、これは結構使いどころがある便利機能だと思うのでこれを機に知ることができてとても良かったです。 ZOZOTOWNアプリの商品を一覧で表示する画面なんかは色々なパターンの商品が並ぶので、プレビューで確認できるのはとても便利そうなので試してみました。 ブランド名が長い場合やセールだけの場合、クーポンだけの場合など様々なバリエーションをプレビューで表示できました。 これをビルドなしで確認できるのはやっぱり便利だと思います。 MLKit: Machine Learning for Mobile with Firebase (Google I/O'19) AndroidというよりはML Kitの話です、ML Kitを活用した例を3つ紹介しています。 また、それぞれデモに加えてどのような仕組みなのかを説明し、コードのサンプルまで紹介してくれます。 テキスト翻訳 カメラで文字を認識し、英語に翻訳するアプリのデモを行なっていました。 各言語のデータのサイズが25〜35Mのサイズに収まっており、それを事前にDLさせることでオフライン翻訳ができるようになったようです。 また各言語を英語に翻訳することが可能なので英語翻訳を仲介して様々な言語間の翻訳もできると話していました。 物体検出からの検索 こちらもカメラで物体をスキャンし、それの類似の商品を検索するデモから始まりました。 これに関してはデバイスだけでは完結しておらず、デバイス側では物体を検出するまで行います。そして、検索はクラウドでを行う流れで実現していました。 デバイス側の物体検出はロースペックのデバイスでもできスムーズにできるような仕組みにしたと話していました。 カスタムモデルの作成 画像から犬の種類を判別するためにカスタムモデルを作ります。 今までのとはちょっと毛色が違い、FirebaseのML Kitの話です。その中のAutoMLの話がメインとなっています。 そこに画像をアップロードするとラベルを設定してくれて、ラベルの変更も簡単にでき、トレーニングもできると話していました。 そして完成したモデルは簡単にアプリに組み込めるとのことでデモではカメラで犬の人形を撮影し犬の種類を検出していました。 おすすめ理由 いずれの例もやりたいことがわかりやすく、現実的な実用例だったと思います。さらに、その仕組みやコードの説明がシンプルなものになっていてとてもわかりやすかったです。 MLにすこしでも興味があれば絶対見るべきだと思います。 私は全然MLに関する知識はないのですが、見ていてすごい面白い、試してみたいと感じMLに対する興味が強くなりました。 Motional Intelligence: Build Smarter Animations (Google I/O'19) 良いアニメーションを実装する際のテクニックについて紹介しています。 アニメーション実装時には3つの気をつけるポイントがあると話しています。 reentrant アニメーション中に新しいアニメーションが発生しても問題が発生しないようにします。 ここではアニメーション中に同じアニメーションの命令が来たらキャンセルするようにする方法が紹介されていました。 後に書いているcontinuosとsmoothでは違うアニメーションが発生した時にどうするべきかが説明されています。 continuos 新しいアニメーションが発生した時に、アニメーションが飛ぶようになってはいけません。 そのためアニメーションの開始値を指定しないようにし、どんな状態からも自然にアニメーションが開始されるようにします。 実装方法としては ViewPropertyAnimator を使う方法が紹介されていました。 これを使うとViewから実行中のアニメーションオブジェクトが取得できるので、そこから続けて新しいアニメーションを実行するようです。 smooth 新しいアニメーションが発生した時に、アニメーションのスピードがリセットされてはいけません。 アニメーションのスピードに緩急をつけることってよくあると思います。開始は遅いけどだんだん加速していくやつとかですね。 1つ目のアニメーションがだんだん加速していっているのに、2つ目のアニメーションが始まった途端にまた加速が最初からになってしまうのはよくないという話です。 そこで前のアニメーションのスピードを持続させるために、 SpringAnimation を使う方法が紹介されていました。 これの終わりの状態を指定する関数 animateToFinalPosition を使ってスピードを維持したままアニメーションを変更させるようです。 ただし SpringAnimation は ViewPropertyAnimator と違って実行中のアニメーションオブジェクトが取得できません。 そのため、拡張関数でそれを用意するところも話の中にありました。 また、後半は発展的な内容になっていて、Google I/O 2019アプリに使われているアニメーションの実装方法について話していました。 AnimatedVectorDrawable でセッション予約ボタンを実装した話や、 ValueAnimator でセッションの絞り込みラベルを実装した話もありました。 AnimatedVectorDrawable の話では開始と終了の状態がそれぞれ複数あるような場合はパターンが増えすぎて大変になることがあると話していました。 その対策として、必ずローディングを経由するようにして組み合わせのパターンを減らすというような工夫が有効だと話していました。 おすすめ理由 アニメーションの実装って色々なやり方があると思っていてどう実装しようか悩むことが多かったのですが、この動画は私に1つの大きな指針を示してくれたように感じました。 また、ハードルが高そうに思えた SpringAnimation の汎用的な使い方も知ることできたのは大きかったと思います。 なので、今まではあまり身近に感じていなかったこのAPIも今後はどんどん使っていきたいと思っています。 そして、ZOZOTOWNアプリでも最近アニメーションの実装に力をいれていたので、メンバーにも共有して取り入れていきたいという気持ちもありピックアップしました。 感想 Androidに関するセッションはかなり多く、他にも面白いセッションは沢山ありました。まだまだまとめきれていないメモが沢山残っているので時間を見つけて整理していきたいです。 そして今回初めてのカリフォルニアだったのですが日が長いこと長いこと、19時とかでも普通に明るくてかなり驚きでした。さらに驚きなのは広大な土地ですね。Googleもそうですが道路も土地の使い方も大胆で、それだけで東京との時間の流れの違いを感じました。 また来年もチャンスがあれば是非参加したいです。次回はセッション以外のコンテンツも楽しめるようにもっと英語を勉強しておこうと思います。 Web関連のおすすめセッション 自己紹介 ZOZOテクノロジーズでバックエンドエンジニアをしている @rllllho です。 普段Webアプリケーションを開発することが多いため、Google I/OではWebに関する発表を主に聞いてきました。 What’s New with Chrome and the Web (Google I/O ’19) 初日の午後にKeynoteが行われていたシアターで発表されたセッションです。 Chrome関連のトピックが発表されています。 主なトピックは下記のようなものがありました。 Lazy loading imgやiframe属性に loading=lazy をつけることでlazy loadingが可能になりました。 おお、すげぇー。最新 Chrome でimg タグにloading のattribute追加するだけて自動的にImage lazy loading処理してくれる! #io19jp pic.twitter.com/8swb6uhclw — Nobuya Sato (@nobsato) 2019年5月7日 addyosmani.com LighthouseでPerformance Budgetの計測ができるように budget.jsonを作成し各項目に予算を設定します。この例ではユニクロの例が紹介されていました。 Portals iframeのような機能で違うドメインのサイトにシームレスに遷移が可能になります。動画内のデモ(14:00ごろ)ではレシピ紹介ページから各レシピをクリックすると違うドメインサイトにスムーズに遷移している例が紹介されていました。 株式会社はてなが開発している「となりのヤングジャンプ」の例では、読者が1話を読んでいる間に次の1話をプリフェッチして次の話への移動をスムーズにさせるという例も紹介されていました。 web.dev Google Duplex on the web 英語のECサイトでも自分の言語(紹介されていたのはヒンドゥー語)でいい感じに購入までの導線を導いてくれます。 Fugu セキュリティやプライバシーについてのプロジェクトのことです。有毒だけど正しく捌けば美味しいことからフグという名前がつけられたそうです。Cookieの取り扱いが厳格になったりフィンガープリントの抑制などを行う予定とのことです。 WebAuthn Webのアプリケーションの認証時に生体認証を用いることができます。Yahoo! JAPANでの導入例が紹介されていました。 PortalsやDuplexなど全体の発表からWebアプリケーションでネイティブアプリのようにリッチでシームレスな体験をできるようにさせたいという意思を感じました。 さらにFlutter for the webの発表もあり、ネイティブアプリとWebの距離が縮まっていることを感じます。個人的にはユニクロ、はてな、Yahoo! JAPANなど日本のアプリケーションが3つ紹介されていておおっとなりました! Chromeのブログにこのセッションについて軽くまとめられています。 blog.chromium.org Speed at Scale: Web Performance Tips and Tricks from the Trenches (Google I/O ’19) Webサイトの読み込み速度を向上させるためのTipsが紹介されているセッションです。 Lighthouseで計測を行い良いスコアをあげた15個のTipsが紹介されています。 Speed at Scale - talk summary. #io19 #Chrome pic.twitter.com/YTh2gYzlzw — Krishna Mohan (@KrishnaAnaril) 2019年5月14日 Netflixがlazy loadingを使って最初のページの表示画像を少なくしたことで初期ロード画像バイトを72%削減しCPUやメモリ使用率を向上させた話や、Twitterが2xより高解像度の3x,4xを使用せず2xまでしか使わないようにして画像サイズを33%削減した話、すべてのJavaScriptをdeferすることで表示を3秒早くした話など、すべてのTipsに具体的な会社と計測データが詳しく説明されており、webフロントに関わる人にはとても役に立つTipsが紹介されています。 最後に下記のサイトがWebサイトも紹介されていました。こちらもWebサイトを最適化するための手法が紹介されています。発表に出ていないものも紹介されています。 web.dev Unlocking New Capabilities for the Web ネイティブアプリにはできてWebにはできないギャップを埋めるために、設計・開発している機能のセッションです。 このセッションで紹介されていた機能は、Chromeだけの機能として開発しているのではなくWebの標準機能として設計開発を行なっているそうです。 WebからBluetoothを扱えるWeb Bluetooth API、ネイティブアプリのシェア機能のように他のアプリケーションに接続できるWeb Share API、バーコードリーダーなど物体検知ができるAPI、Google Mapsのような使用中に画面をオフさせたくない場合にスクリーンをオフしないようにするWake Lock、ローカルのファイルを読み書きできるFile System API、ローカルにあるフォントにアクセスできるFont Access APIなどワクワクするような機能が多く紹介されていました。 下記の記事にセッションで発表されたAPIや設計実装など発表で出てきたものがまとまっている記事です。 developers.google.com またこちらから新しいAPIをトライアルとして使用できます。 developers.chrome.com 感想 取り上げたセッションの中には言及されていませんでしたが、他にも新しい発表がありました。 GooglebotがChrome 41ではなく最新版のChromiumを使うようになったことが発表されました。今までSEOを懸念してSPAを導入していなかった方には朗報です。 webmasters.googleblog.com またFlutter関連ではFlutter for the webのリリースが発表されました! Flutterを使ってiOS/Android/Desktopアプリケーションに加え、Webのアプリケーションが開発できるようになりました。Flutter for webは、インタラクティブでリッチなコンテンツを提供することを目標としているそうで、実際にデモで見たNew York Timesのアプリケーションもぬるぬる動いていて感動しました! こちらの記事にFlutterを用いたマルチプラットフォーム開発について述べられています。 developers.googleblog.com セキュリティについての発表もGoogle I/Oでは多くあり、WebについてはサードバーティCookieの取り扱いがSame-Site属性を用い厳格になることも発表されました。 web.dev blog.chromium.org Web関連のセッション動画がYoutubeのプレイリストにまとまっているのでこちらもご参考ください。 www.youtube.com またおそらく有志の方がつくられたGoogle I/OのWeb関連の発表がまとめられているスライドがまとまっていてわかりやすかったのでご紹介させていただきます。 docs.google.com セッションもとても魅力的でしたが、参加して一番感動して行ってよかったなと思ったのは会場の雰囲気含めた体験です。多くの人が言及してますが、カンファレンスなのに外のシアターで発表、芝生で座りながらみることができ、セッションを聞きに行くというよりGoogle I/Oというお祭りに参加している気分でした! ご興味ある方はぜひ一度体験して欲しいです。GDG Japanの方も多く参加しており日本人の開発者の方と交流できる機会があったこともよかったです! AI/ML関連のおすすめセッション 自己紹介 ZOZOにおける多様な課題をAI/MLを用いて解決することを目指してプロダクト開発をしています。Google I/OでもAI/ML関連の情報を収集してきました。 ZOZOテクノロジーズでPMをしてる @ysk_ur です。 プロダクトの改善にAI/MLの導入を検討している方におすすめの動画を紹介します。 Designing Human-Centered AI Products IO中に発表されたユーザー志向なAIプロダクトを開発するためのガイドブックの People + AI Guidebook の紹介がされています。Googleフライトチームの事例を踏まえてAIを実際のプロダクトに活用する際の説明がされています。 People + AI Guidebookは6のとセクションから構成されていますがこのセッションではうち3つについて説明があります。 (1)成功の定義についてと本当にAIを使う必要があるのか? 「成功の定義はあるのか?」「AIによって最適化できるのか?」などAIを導入する以前の段階でのプロダクトの課題に設定についてです。AIプロダクトというテーマではありますが、 そもそも設定されている課題の解決にAIを使う必要があるのか? という本質的な話もあります。 『Automation(自動化)』と『Augmentation(拡張)』のどちらの特徴を活かすためにAIを導入するのか? という話があり、盲目的にAIで解決だ! ではなく、課題に対してAIの特徴を活かすことができるのかを改めて考えさせられます。 (2)ユーザーのニーズをデータへと変換する ユーザーのニーズをAIとしてのアウトプットにする必要があります。その定義ができないことには進むことができません。データを定義し、必要なデータを集め、モデルを学習する必要があります。 予測精度をそのままユーザーに伝えるのではなく、ユーザーとのコミュニケーション方法も設計する必要があるという説明があります。 予測精度をそのままの数値でユーザーに伝えるべきではなく、デザインをはじめとしたユーザーとのコミュニケーション方法も設計する必要があり、全てがトレードオフであるという説明があります。 おすすめ理由 AI/MLをプロダクトへ導入を検討する際に、課題となる成功判断や本当にAIを導入すべきかの考え方を学ぶことができます。 People + AI Guidebook と合わせてセッションを見ることをおすすめします。 実際にGoogleのプロダクトでの導入事例の紹介があるため理解が深まります。 Machine Learning Fairness: Lessons Learned マシーンラーニングの公平性を担保する過程でGoogleが学んだことについてのセッションです。 Googleのサービスは世界中に利用者がおり、様々なバックグラウンドの人がいるため、データを平等に扱う必要がありました。最適化させる過程でMLによる学習だけでなく、人間によるバイアスがかかるため、公平性を担保するデータセットの構築が難しいと発言されていました。 具体的な事例として、Google翻訳が紹介されていました。 トルコ語は性別がない言語のため、Google翻訳で英訳した際に勝手に性別を予測してしまったということです。 ユーザーの利用実績から学習するため、Google翻訳では上記のような課題が起こっており、翻訳結果画面でユーザーに選択させることでより効率的に学習させていくことを考えているとのことでした。 こういった問題に対しては、ダイバーシティ関連のデータセットも用意しフィードバックを集めて透明性をあげていると説明がありました。 おすすめ理由 実際にGoogleが直面したAI/MLの抱える課題の具体的な事例と公平性や透明性を担保していくためのGoogleの考えを知ることができます。 グローバルで利用されるプロダクトで、AI/MLを活用することで生じるダイバーシティなどの問題事例を知ることができます。 感想 PMとしてGoogle I/Oに参加している日本人は少なかったようです。テクノロジーを用いてプロダクトを作り、ユーザーへ届け、ビジネスを進める役割を担う人もぜひ参加してみるのが良いと思います。 イベント自体が非常に楽しいのはもちろんですが、GoogleがいかにAIに本気で取り組んでいるかを体感することができました。本気で取り組むからこそセキュリティ関連の話も多くありました。 また、I/Oの会場で行われてい Codelabs でエンジニア経験のない私でもML Kitを使って画像検知を行うAndroidアプリを作ることができました。 Keynoteで発言された「AI for Everyone」はユーザーとして恩恵を受けるだけでなく、製作者側に対しても言われてる発言なんだなと実感しました。 ビジネスの課題やユーザー体験からHowを考えるべきだとは思いますが、MLkitでどのようなことをできるか知ることでより良いプロダクト作りのきっかけが生まれる気がしました。 最後に カンファレンス参加に関わる渡航費・宿泊費などは全て会社に負担してもらいました! 自分から希望すれば参加する機会をいただけて本当に感謝です! ZOZOテクノロジーズでは、最新技術に対する感度が高く、最新技術をプロダクトに取り入れたい人を募集しています。 こちらからご応募ください。 tech.zozo.com
アバター
こんにちは、R&Dのプロジェクトマネージャーをしている 新井 です。 先日ZOZOテクノロジーズで半期に一度の 全社員総会「ZOZO Technologies Compass」 を開催しました。 代表取締役CINO(Chief Innovation Officer)金山はバーチャルキャラクターの箱猫カナヤマックス *1 で登場し、社員向けにプレゼンテーションを行いました。 今回はバーチャルキャラクターを使ってのプレゼン企画の実施裏側について紹介していきます。 ※会場でのプレゼンの様子。右下がバーチャルキャラクターの箱猫カナヤマックス。 話者の動きに応じて会場のスクリーンにもリアルタイムに動きが反映される。 ※別室での実際のプレゼンの様子。VRデバイスを使用しながらバーチャルキャラクターを通じてプレゼンテーション。 バーチャルキャラクタープレゼンの概要 登壇者がVRデバイスとコントローラを用いて事前に生成した3DCGアバターになり、会場とカメラ・マイクを接続して聴講者とのインタラクティブなやりとりを可能にするプレゼンテーションを実施しました。また今回はオーディエンスのいる会場とは別の会場を用意し、遠隔中継での発表も試みました。バーチャル空間にZOZO青山オフィスを忠実に再現し、キャラクターのバーチャルな見た目以外はあたかもリアルなオフィスでのプレゼンと同じになるような環境を用意しました。今回の企画実現にあたりCGの造詣が深い外部パートナーである株式会社GUNCY'SとVR法人HIKKYと共同で行いました。 小長井さん(写真左)、野澤さん(写真中央)、新津さん(写真右) −簡単に自己紹介をお願い致します。 株式会社GUNCY'SはCG全般の幅広い知識を武器にしている会社で、クライアントが望む答えに対して幅広いアプローチの中から、ゼロベースでコンテンツを作れる軍師集団です。私は元々CGプロダクションで長年経験を重ねてきましたが、近年はエンタメだけでなくCG技術を使って色々な業界でもCGへのニーズが広がってます。それらのイノベーションのお手伝いや、他の分野でも経験を活かしたいと思い2015年に会社を立ち上げました。(野澤) VR法人HIKKYは、VR空間でのイベント企画やプロモーションなどを行っているチームです。あるゲームのVRを使ったプロモーション施策の際に集まったメンバーが中心となって結成されました。(新津) −VTuber、バーチャルキャラクターが今普及している背景についてもう少しお聞かせください。 今でこそCGキャラクターには誰でもなれるようになりましたが、数年前は、キャラクターを作るのための専門的なツールが使える一部の人だけのものという印象が強かったです。しかし、Blenderなどの無料で使えるソフトが普及したことと、リアルのYouTuberとは違って顔出ししなくてもYouTuberになれるという点がブームに拍車をかけたと思います。Unityを使ってリアルタイムでキャラクターの口パクや動きが付けられて、それを配信に繋げられる簡単なシステムがあれば、だれでも好きなキャラクターになれるようになったというわけです。 一方で、だれでもバーチャルキャラクターになれるようにはなりましたが、「魅力的なキャラクター・動き・脚本(コンテンツ)」の3つの要素が揃わないと長続きしないと思います。可愛いキャラクターを作れたのはいいが、そのあとのコンテンツがなく継続的は取り組みができず視聴者が離れていく例はよく聞く話です。(野澤) VR法人HIKKYでは、VRChatの中でコミックマーケットの様なコミュニティを展開するバーチャルマーケットを主宰しており、3月には3日間でのべ12万人以上が来場してくれました。これだけの盛り上がりにVRChat開発元から非常に熱い賞賛を頂きました。(新津) −企画のはじまりについてお聞かせください きっかけは金山さんから、社員総会の場でバーチャルキャラクターになって登壇をしたいと打診があったことでした。我々としてはCGを使ってバーチャルキャラクターになりきるためのノウハウは色々あります。ただバーチャルな空間を作って、話者の動きをトレースして配信するだけなら、非常に簡単な話ですが、さらに一歩踏み込んで巷のVTuberやテレビの中継配信に引けを取らない動画配信を目指すことになりました。 今回の具体的な実施内容は ・VRデバイスを使用してCGキャラクターになりきる ・VRChat上に青山オフィスを模したバーチャル空間を用意 ・KeynoteスライドショーとVR映像をリアルタイムに合成 ・バーチャルキャラクターを演じている人の姿も会場に配信 ・会場の様子はサブモニターから確認可能に プラットフォームにVRChatを使うことで、我々がこの企画のために制作したVR青山オフィスをワールドに設定しVRオフィス内を自由に行き来できる場を作ることに成功しました。この青山オフィスは非常にユニークな作りをしており、リアルとバーチャルを行き来することができるという新たな世界を感じさせるものとなりました。事前にZOZOからオフィスの図面を提供してもらえたので、図面を元に正確に再現することができました。椅子などは新規に作らずアセットストアで購入したものを配置しましたが、中央にあるガラス張りの円会議室は今回のためにオリジナルで作りました。 CG上でガラスを表現するには、リアルタイムで反射の計算をする重い処理が必要になり、難しいものです。しかし事前にライティングをベイクしたマップを反映させることで求める画質を再現できました。バーチャルの場合、演出や表現に関しては予算と期間のなかであれば何でもできます。キャラクターにエフェクトを付けたり、ゲームや映画・アニメのシーンを再現することだって可能です。これらを容易にした背景にはUnityが登場し参入障壁がぐっと下がったことがあげられます。 今回のプレゼンテーションをVRChatを使って実現するために、VRコンテンツとして体験者が気分よく楽しめる環境を維持するための工夫が必要でした。高画質なデータをたくさん使うと演算処理をするためのフレームレートが落ちてしまいVR酔いを引き起こしやすくなってしまいます。そのためデータ量を制限し、計算コストが高い光の計算は事前処理を行うなどの工夫が必要です。 制作をする前にまず、実際のオフィスを下見させていただき、オフィスの図面や机や椅子などの備え付け品などの写真を撮影し参考にしました。今回特に重点的に再現をしたかったのは、青山オフィスの象徴的な存在である曲面ガラス張りの会議室でした。制作にはAutodeskのMayaを使いました。(図A) (図A)Mayaで作られた背景全体のラフモデリング ガラスの表現には、モバイルプラットフォームでも使えるシェーダーを選択しました。このシェーダーは、UnityのAssetStoreで無料でダウンロードできるのですが、擬似的な屈折表現なども行える優れたものでした。金属やガラスなどに物体が反射する表現には、リフレクションプローブを使用しており、計算負荷を抑えてながら品質の向上を目指しました。(図B、図C) (図B)リフレクションプローブなし (図C)リフレクションプローブあり   また、ZOZO社のフローリング床や壁は、ユニークな木目デザインになっていましたが、これらを写真をもとに作るのではなくSubstance Desingerというソフトを使いノードをつなぎ合わせて表現しています。このソフトを使うことでカスタマイズが容易に行え効率よく制作することができました。(図D) (図D)Substance Designerで制作したテクスチャ (図D)Substance Designerで制作したテクスチャ2 実際のオフィスの写真と見比べてみると、近い雰囲気で再現出来ている事がわかると思います。(図E、図F)今後は遠隔地に住んでいる方がこのVRオフィスの中でミーティングが出来るようになると面白いと思います。   (図E)青山オフィス社内写真 (図F)VR青山オフィス (図E)青山オフィス社内写真2 (図F)VR青山オフィス2 −動画の配信についてもう少し詳しくお聞かせください Open Broadcaster Software(OBS)を使ってバーチャルの映像とリアルの映像を合成しました。YouTube等でゲーム実況している人たちは大体この技術を使っています。バーチャルキャラクター操作用とプレゼンテーション操作用、バーチャル空間でキャラクターを撮影するカメラマン用の3台のパソコンをそれぞれ用意しました。それをネット回線を介して4台目のパソコンで受け取った映像をOBS内で合成し、再度ネット回線を介して会場へ配信しました。この4台目のパソコンが現実世界だと中継車と同じ役割をしています。会場への配信に5台目のパソコンを用意しビデオ会議用アプリのZoomを介して画面を会場のスクリーンに映し出しました。 本来であればZoomを使った画面共有の出力ではなく、会場側のパソコンでもVRChatに参加したり、他拠点からもVRChatに参加してバーチャル内ですべて完結させたいと思っていました。しかし、今回は有線LANを使用していても、はじめての試みでネット回線に不安があり、この構成になりました。ただし技術的には実現可能なので次回機会があればチャレンジしたいと思います。 会場と映像のシステム図 −最後にバーチャルキャラクターやVRの今後の展望についてお聞かせください バーチャルとリアルの距離が急速に縮まり、どちらの世界も行き来できる世界が生まれようとしています。今回の企画会議をするにあたり、スタッフとのやり取りは全てVRChat内で行いましたが驚くほどに違和感が薄れていっています。オリジナルのアバターを作り、自分のなりたい姿になれるように改造・編集するのが常識になりつつあります。VRデバイスを日常的に嗜む人はまだ限られていますが、バーチャル世界に自分自身を投影するニーズは増えつつあります。最近だとインスタグラムに撮影した写真をあげるのが普通ですが、それが自分のアバターやバーチャル空間でのできごとを切り取ってあげることも自然な出来事となるでしょう。 これらのARやVR、XRの技術革新が進めば進むほど現実とバーチャルの垣根はどんどんなくなり、生活の一部に馴染んでいきます。多様なサービスが次々と生まれていく中で様々なプラットフォームの間を自由に行き来できる汎用フォーマットが生まれたり、今後ますます発展していくことでしょう。 −最後に バーチャルとリアルでの垣根がどんどんなくなることでアパレル業界も新しいビジネスのチャンスがそこにはある。 ショップに並んで手に入れた欲しかった服を来てみたり、写真をとったり、友達に見せてたものが今後はバーチャル世界でも起こりうるかもしれません。リアルと同様にバーチャルでも着飾りたい、またはバーチャルでのとある体験の思い出アイテムとしてアバターの服を手に入れたいというニーズがより顕在化していくことがあればバーチャル×ファッションの可能性はこれからもっともっと拡大していくであろう。 ZOZOテクノロジーズでは、各職種で募集を行っています。技術的なチャレンジを続けながら成長したい皆様、ぜひご応募ください。求人に関する情報は こちら よりご覧ください。 *1 : ZOZOTOWN 公式キャラクター の箱猫マックスを今回用にアレンジしたキャラクターです
アバター
こんにちは。WEARリプレイスチームの id:takanamito です。 先日、社内で初めてAWS Fargate上でRailsを動かす環境をつくったので、その事例報告をしようと思います。 Fargate導入のきっかけ コンテナ環境で動かすにあたって考慮したこと assets配信 ログ出力 秘匿情報の注入 リソース監視 苦労した点 まとめ Fargate導入のきっかけ WEAR では先日RubyKaigi 2019のスポンサーセッションでお話したように、Ruby on Railsへのシステムリプレイス作業を進めています。 そんな中、手作業で行っている運用を管理画面上でツール化したいという要望が上がってきました。リプレイス作業中であるため、できれば新機能はRailsで実装をしたいところです。しかし管理画面に相当するアプリケーションをデプロイするインフラはまだありませんでした。 WEARリプレイスでは、インフラの構築先としてAWSを採用しています。AWS上で新規にRailsを動かす環境を作るとすると有力な選択肢は次の3つになるかと思います。 EC2上に環境をつくる ECS on EC2でコンテナを動かす Fargateでコンテナを動かす リプレイスプロジェクトでは、私たちアプリケーションエンジニアがインフラ構築/運用に参加する事を踏まえ、運用作業をできるだけ少なくしたいという想いがありました。 SREチームとも相談した結果、よりマネージドな環境でコンテナを運用するためFargateでRailsアプリケーションのコンテナを動かす方式を採用することにしました。 運用工数の削減については弊社の塩崎が以前に書いた記事で言及されているので合わせてご参照ください。 techblog.zozo.com 今回の記事では実際にRailsを動かしていくにあたって考慮したことについて詳細をお話します。 コンテナ環境で動かすにあたって考慮したこと 弊社では IQON など既に運用中のRailsアプリケーションがありますが、コンテナは使っておらずEC2インスタンス上にChefで環境を構築しています。 今回、社内で初めてコンテナで動かすRails環境を構築することになったため、以下のような点で新しい仕組みを考える必要がありました。 assets配信 ログ出力 秘匿情報の注入 リソース監視 現在運用中のRailsアプリケーションと、今回つくるFargate環境との比較を以下の表にまとめました。詳細をこの後に書いていきます。 非コンテナ(EC2) Fargate assets配信 Nginxからstaticに配信 Railsから直接配信 ログ出力 ファイルに出力 CloudWatch Logsに連携 秘匿情報の注入 インスタンスの環境変数として渡す コンテナ内に環境変数として渡す 本来、新規開発のアプリケーションであればデータベースマイグレーションの仕組みを考える必要がありますが、マイグレーションは既にRails以外の別の方法で行われているため考慮から外しました。 assets配信 よくあるRailsアプリケーションの構成ではNginxなどのリバースプロキシからassets配信をすると思いますが、今回はRailsから直接assets配信をすることにしました。 assets配信を考える際には以下の項目を考慮しました。 そもそも社内限定の管理ツールのためトラフィックが極めて少ない The Twelve-Factor Appに従いassetsを含めコンテナで完結させたい リバースプロキシを使わずにRailsでassets配信をするためCDNの利用も検討しましたが、そもそも社員しか使わないアプリケーションでトラフィックが極めて少なくレスポンスタイムも問題になるほど遅くなかったので導入は避けました。 また同時に asset_sync などのgemを使ってS3にホスティングすることも考えましたが、Herokuのドキュメントでも触れられているようにThe Twelve-Factor Appの考えに従いこちらも導入は避けました。 The twelve-factor app is completely self-contained and does not rely on runtime injection of a webserver into the execution environment to create a web-facing service. The Twelve-Factor App Using Asset Sync can cause failures. Heroku recommends using a a CDN instead of asset_sync whenever possible. Please Do Not Use Asset Sync | Heroku Dev Center ログ出力 実装した当時、FargateではawslogsドライバーのみがサポートされていたためRailsログはログドライバーを通じてCloudWatch Logsに送信されます。 デフォルトのRailsのログは複数行に改行されてしまい、CloudWatch Logs insightsでうまく検索できないため lograge gemを使ってjson形式に変換しています。 # config/environments/production.rb Rails .application.configure do config.lograge.enabled = true config.lograge.formatter = Lograge :: Formatters :: Json .new config.lograge.custom_options = lambda do | event | exceptions = %w( controller action format authenticity_token ) { host : event.payload[ :host ], timestamp : Time .zone.now, params : event.payload[ :params ].except(*exceptions), exception : event.payload[ :exception ], exception_object : event.payload[ :exception_object ], } end ... end 秘匿情報の注入 DBのパスワードなどの秘匿情報をどのようにコンテナ内部に渡すかも課題でした。 例えばRailsのEncrypted Credentialsを使うのも選択肢のひとつですが、弊社では以前からAWS Systems Managerパラメータストアを使った秘匿情報の管理運用をしていたためそれに合わせた仕組みを採用しました。 Fargateプラットフォームバージョン1.3から シークレットを扱える ようになっていますが今回は採用を見送りました。理由は後述します。 具体的にはdocker entrypointに指定しているシェルスクリプトで、パラメータストアから取得した値をコンテナの環境変数に埋めています。 # Dockerfile FROM ruby:2.6.3 RUN apt-get update -qq && apt-get install -y build-essential nodejs awscli # ...中略 WORKDIR /wear COPY Gemfile /wear/Gemfile COPY Gemfile.lock /wear/Gemfile.lock RUN bundle install COPY . /wear RUN bundle exec rails assets:precompile ENTRYPOINT ["./docker-entrypoint.sh", "bundle", "exec"] 環境変数は次のようにして埋めています。 #!/bin/bash set -e export DB_USER= $( aws ssm get-parameters --names "/db_user" ) export DB_PASSWORD= $( aws ssm get-parameters --names "/db_password" ) export SECRET_KEY_BASE= $( aws ssm get-parameters --names "/secret_key_base" ) exec " $@ " パラメータストアを使うことで秘匿情報にアクセスできる開発者を限定できる + CloudFormation(CFn)を使ってコードで権限管理が可能なところは利点かと思います。 そのため弊社ではAWSのオーケストレーションツールとしてCFnを使用しています。 Fargateプラットフォームバージョン1.3でサポートされたシークレットの仕組みを採用したかったのですが、今回のインフラ構築をしていた時点ではまだCFnにSecretの機能が反映されていなかったため、APIを通じてパラメータストアから値を取得する方法を選択しています。 開発状況はこのissueで報告されており、現在開発が進行中とのことです。 [ECS] [CloudFormation]: CloudFormation support for Secrets · Issue #97 · aws/containers-roadmap · GitHub リソース監視 コンテナのメトリクス監視にはDatadogを使用しています。こちらは先述の塩崎の記事で紹介されていたのと同様、サイドカーパターンでコンテナのメトリクスを収集しています。 苦労した点 今回、自分で構築するコンテナデプロイ環境の構築が初めてだったため、かなり色んなハマりポイントにつまずきながらの作業になってしまいました。 Fargateは通常sshできない環境での作業を強いられるためそれによる確認作業の進めにくさは感じました。 特にコンテナ起動で試行錯誤している最中に「環境変数が設定できているか」など、インスタンスに入ってコマンドを叩けば一瞬で終わるはずの作業ができないのはストレスでした。 またAWSマネージドサービスを多用しているため、書いたコードが悪いのか、IAMの権限不足が悪いのかなど原因の切り分けにも苦労する場面が多かったです。 私は最終的にsshが可能なコンテナイメージを用意して確認をしました。これからコンテナデプロイ環境を構築される方は最初に用意しておくと確認作業が捗るかもしれません。 一部ではありますが、今回の環境構築で使用したCFnのymlファイルを置いておきます。 ECSTaskDefinitionRailsApplication : Type : 'AWS::ECS::TaskDefinition' Properties : RequiresCompatibilities : - 'FARGATE' Cpu : 512 Memory : 1024 NetworkMode : 'awsvpc' TaskRoleArn : !GetAtt IAMRoleForRailsApplicationTaskRole.Arn # パラメータストアへのアクセス権限を付与 ExecutionRoleArn : !GetAtt IAMRoleForRailsApplicationTaskExecution.Arn ContainerDefinitions : - Image : !Sub ${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/${ECRRepositoryRailsApplication}:latest Name : 'rails_application' Cpu : 256 Memory : 512 Command : - 'puma' - '-e' - !GetAtt SSMParameterRailsEnv.Value Environment : - Name : 'RAILS_ENV' Value : !GetAtt SSMParameterRailsEnv.Value PortMappings : - ContainerPort : 80 HostPort : 80 Protocol : 'tcp' LogConfiguration : LogDriver : 'awslogs' Options : awslogs-group : !Ref LogsLogGroupForRailsApplication awslogs-region : !Ref AWS::Region awslogs-stream-prefix : 'rails_application' - Image : datadog/agent:latest Name : 'datadog' Cpu : 256 Memory : 512 Environment : - Name : 'DD_API_KEY' Value : !Ref DatadogAPIKey - Name : 'ECS_FARGATE' Value : 'true' LogConfiguration : LogDriver : 'awslogs' Options : awslogs-group : !Ref LogsLogGroupForRailsApplication awslogs-region : !Ref AWS::Region awslogs-stream-prefix : 'rails_application-datadog' ECSServiceRailsApplication : Type : 'AWS::ECS::Service' DependsOn : - IAMServiceLinkedRoleForECSRailsApplicationService Properties : Cluster : !Ref ECSClusterRailsApplication DesiredCount : 2 LaunchType : 'FARGATE' LoadBalancers : - ContainerName : 'rails_application' ContainerPort : 80 TargetGroupArn : !Ref ElasticLoadBalancingV2TargetGroupExternalRailsApplication NetworkConfiguration : AwsvpcConfiguration : AssignPublicIp : 'DISABLED' SecurityGroups : - !Ref EC2SecurityGroupECSRailsApplication Subnets : - !Ref EC2SubnetPrivateApplicationAZ1 - !Ref EC2SubnetPrivateApplicationAZ2 TaskDefinition : !Ref ECSTaskDefinitionRailsApplication まとめ 初期の環境構築に今までと一味違った苦戦があったことは事実ですが、EC2インスタンスの管理から解放されるメリットはやはり大きいと感じています。 弊社では今までの仕組みにとらわれず新しい事例を作ることに興味がある方、Webアプリケーションの開発/運用改善に興味がある方を募集しています。興味のある方はぜひ以下のリンクからご応募ください。 www.wantedly.com
アバター
こんにちは! SREチームでコンテナやパブリッククラウドを使ったインフラの構築や運用などを行っている @_inductor_ です。スペインのご飯が美味しすぎて日本に帰るのがつらい気持ちになっています。 本記事は、5月20日から23日にかけて行われているKubeCon + CloudNativeCon Europe 2019(以下KubeConまたはKubeCon EUと表記します)の参加レポートです。 昨年12月にシアトルにて開催されたKubeCon NAの様子については こちらの記事 をご覧ください。 今回のKubeCon EUはスペインのバルセロナで行われ、およそ8000人が参加しています。昨年のコペンハーゲンでは4000人超程度だったので、その倍の参加者数です! また、前回のKubeCon NAでは参加者が8000人(+2000人程度のキャンセル待ち)だったため、規模感としてもアメリカに引けを取らない程度の規模であることが伺えます。 このペースで増えていった場合、14年後には日本の全員が、20年後には地球上のすべての人類がKubeCon参加者になる見込みです。 KubeConではキーノート、セッション含め数多くの内容について話されますが、今回は僕が個人的に興味のあった2つのキーノートについてまとめてご紹介します。 Reperforming a Nobel Prize Discovery on Kubernetes - Ricardo Rocha, Computing Engineer & Lukas Heinrich, Physicist, CERN 1つめに紹介するキーノートは、CERNのエンジニアによる「ヒッグス粒子の発見をKubernetes上で再現する話」です。 CERNについて CERNはスイスにある世界最大の素粒子物理学の研究所で、宇宙の起源や形などを探ると言った理由でさまざまな基礎研究を行っている研究機関です。 ITの文脈においては、CERNはWebの起源でもあります。 CERNの研究者であるティム・バーナーズ・リーが論文を公開するために考案したのがWWW(HTTP)で、CERNの中で普及しましたが「これは世の中にも広めるべきだ」ということで公開されるようになりました。 今年の春にWebは30年の誕生日を迎え、それに合わせた 記念イベント も開かれました。 研究の話に戻ると、CERNは実験機関としても世界最大規模の施設を持っています。 CERNにある加速器は全周20km以上にも及び、光速に近いスピードの粒子を衝突させてその結果を解析しています。 加速器はテラバイト級のネットワークに接続されており、データセンターにつながっています。ストレージに蓄積されるデータは400TB/日にものぼります。 世界中のいたるところにある研究機関にもネットワークが繋がっていて70PB/年のデータをやり取りしています。 研究とKubernetesのかかわり 今日ここで話すのはこうした研究を動かすためにKubernetesをどのように使っているかです。 取り上げるのは「ヒッグス粒子」で、2013年のノーベル物理学賞を受賞した研究です。ヒッグス粒子の存在が証明されることによってビッグバンの起源などに関わる様々なことが説明できるようになるのですが、またたく間に崩壊して4つの粒子になってしまうため物理的に観測することはとても難しく、再現性のあるシミュレーションが重要になってきます。 シミュレーションにはJupyter Notebookを使い、グラフの可視化まで行います。このシミュレーションには70TBのデータセットを使い、20000を超えるKubernetesクラスタで処理します。 CERNでは従来よりOpenStackベースのインフラストラクチャを用いており、この研究に関してもCeph + OpenStack Magnum + Redisの構成で行われていました。 2018年にGoogleがCERN openlabの活動にジョインしたこと をきっかけに、GKE(Kubernetes)を使って研究を行ってみることになりました。 Google Cloud Platform(GCP)上での技術スタックとしては、Google Cloud Storage(GCS)+ Google Kubernetes Engine(GKE)+ Memorystoreとなっており、基本的なスタックは変えないままマネージドに移行しています。 デモを行った結果実験でGCSからPullするデータの瞬間転送速度は最大1.6Tbpsくらいで、かなりの規模のデータ転送が行われていることがわかります。 CERNで使っているデータセットはオープンデータポリシーに基づいているため「理想的には」どこであろうと誰であろうと実験を行うことができますが、現実的にはこの量のデータを処理するにはかなりのパワーが必要です。 そんな中で研究に使われるインフラの技術を入れ替えることにはどのような意味があるのでしょうか。 コンピューターを使った計算を行う場合、 OS(バージョンやディストリビューション、アーキテクチャ) (言語などの)インタプリタのバージョン といった差異が大きな違いを招いてしまうことがあります。 こうした問題を解決するにあたって、コンテナが大きな役割を果たしてくれました。 まとめ コンテナは再現性を「空間と時間(物理学における宇宙の定義)」に与えてくれる 世界中のどんなデータセンターでも、いつ実行しても同じように動かすことができる オープンサイエンスにおける大きなゲームチェンジャーになる How Spotify Accidentally Deleted All its Kube Clusters with No User Impact - David Xia, Spotify 2つめに紹介するキーノートは、Spotifyのエンジニアによる「何回か誤ってクラスタを削除してしまったがユーザーへの影響なしに復旧できた話」です。 Spotifyのサービス及びインフラ構成について Spotifyは1億を超える有料会員、2億を超えるMAUを抱える世界最大級の音楽ストリーミングサービスです。 SpotifyではGoogle Cloud Platform(GCP)でインフラを組んでおり、その中でもいくつかのサブシステムをGoogle Kubernetes Engine(GKE)で部分的に動かしています。 削除の経緯(Story Time) 前提として、SpotifyのGKEではUS、Europe、Asiaの3つのクラスタがあり、それぞれが同じ構成でリージョナルな冗長構成になっています。 ある日、オペミスでUSのクラスタを削除してしまいました。 また、別の機会には同僚が誤ってAsiaのクラスタを削除してしまい、復旧しようとしたものの、さらに間違えてUSのクラスタも追加で削除してしまいました。 これらオペミスが発生した背景としては、ブラウザ上で作業をしていたときに、テスト用のクラスタとプロダクション用のクラスタを横に並べていたことが原因でした。 幸いなことに、本事象に起因したユーザーへの影響はありませんでした。 とはいうものの、このような事象を食い止めるにはどうしたら良いのでしょうか。現実的には、「食い止める」ことはできないため、何らかの仕組みを設けて「防ぐ」必要があります。 削除から学んだこと クラスタの復旧には時間がかかること(3.25時間) 時間がかかった理由には以下の3つがありました 原因1: クラスタ作成スクリプトのバグがあった 原因2: ドキュメントの様々な不備があった 原因3: クラスタの作成処理はresumableではなく、All or Nothing(失敗したらまた1からやり直し)だった 削除事件から一ヶ月後 この失敗を踏まえて、Terraformを使ってインフラの構成をコード化することで削除するのを防ぐことにしました。 ところが、また事件が起きます。 前提として、全クラスタに対する共通設定やKubernetesのリソースを管理するためのGitリポジトリがあり、それを使って構成管理をします。また、このリポジトリにはクラスタの運用者と利用者の両方がコミットします。 Terraformの十分な知識がないまま運用していたので、tfstate(構成の詳細を管理するステートファイル)をGitリポジトリに含めていました。tfstateには各環境ごとにすべてのリソースのメタデータが入っていて、手動で触るのは非常に危険なので、S3などに保存してGitには含めないのが一般的です。 チームのエンジニアがAsiaリージョンにクラスタを新しく作成するためのPRを作成しました。このとき、CIでreviewビルドが走り、リモートのステートが 誤って更新されてしまいました。 このステートファイルがMasterに取り込まれると、本番のリソースにも影響が発生します。しかし、この時点ではレビュー用のCIが走っただけで、Masterにはマージされていませんでした。 次に、クラスタ利用者が、既存のRoleBindingに3つの新しいユーザーを追加するPRを作成しました。この変更は一見なんの問題も無さそうです。しかし、不幸なことに、問題はこの変更ではありませんでした。問題は、このユーザー追加がAsiaリージョンのクラスタに定義されていなかったことです。 このPRを先にマージした結果、不整合が発生してAsiaのクラスタが動かなくなりました。 この問題を修正するために、マージしていなかったAsiaのクラスタ定義のPRもマージしたところ、今度はサービスロールの権限不足でAsiaクラスタの作成時にエラーが発生しました。今までは手でクラスタを作成していたため問題がなかったのですが、サービスロールの設定を変えたことによって、Terraformから見たときに新しくロールを作り直すための権限が必要になりました。 これらの不整合の結果、今度はUSのクラスタにまで影響が波及しました。 3つのプロダクションクラスタのうち2つのクラスタがこの地球上から消え去ってしまいました。 開発者への影響 この事象におけるユーザーへの影響も前回と同じくありませんでした。しかし、開発者には以下のような影響がありました。 開発チームが非KubernetesなVMを追加で構築する必要があった マスタIPがハードコードされていたのでアップデートする必要があった kubectlに使うクレデンシャルを更新する必要があった 障害の振り返り(やっておいてよかったこと) 障害が実際に起きたのは事実ですが、上述の通りユーザーへの影響はありませんでした。これは不幸中の幸いとも言えますが、以下のような内容が実践できていたことが重要でした。 障害を前提に設計していたこと 大規模かつで複雑なインフラを段階的に移行したこと 学びの文化を持っていたこと どのように障害を意識してインフラを設計したか あらゆるサービスのK8sへの移行を「段階的に」した Kubernetes上で動かすアプリケーションの登録方法の工夫 非K8sな環境へのフェイルオーバー 大規模かつで複雑なインフラを段階的に移行したこと 障害発生時点で、SpotifyにおけるKubernetesの利用状況は実験段階(β)に過ぎませんでした。 そのため、開発チームには各サービスをいきなり全てKubernetesに移行することは推奨せず、部分的に取り入れるようにさせていました。 他にも、Kubernetesのインテグレーション、可用性、複数クラスタの管理について継続的に取り組んでいました。 Kubernetes上で動かすアプリケーションの登録方法の工夫 Kubernetes上で動かすアプリケーションの中で「あえて」Kubernetes wayに反した使い方をしている部分があります。 例えば、アプリケーション上でKubenernetesのService IPは使わず、Serviceのエンドポイントを定期的にポーリングしてサービスディスカバリの設定を更新しています。 非K8sな環境へのフェイルオーバー サービスディスカバリのシステムが再起動したことで、KubernetesのPodたちがサービスディスカバリの一覧から削除され、結果として既存のVM上で動いているサービスにアクセスが切り替わりました。 学びから得たベストプラクティス クラスタバックアップ 闇雲にバックアップするだけではなく、日常的にバックアップから復旧できることを証明しておくことが非常に重要です。 インフラのコード化 特に以下の部分について言及されていました。 ワークフローの標準化 新しいツールは段階的に入れる LinterとValidatorを入れる dry-runの結果を表示する(GitのPRコメントとか) featrueブランチは常に最新の状態に揃える Branch Protectionの導入(CIのステータス、レビューのApprove) 障害の試験 障害は起こるものだと考える 試験するときはちゃんとスケジュールする 運用者、利用者にアナウンスする 複数の障害シナリオを用意する 発生した問題は即座に記録、修正する Practice makes perfect 前は3時間以上かかったリストアが今は1時間で完了するようになった 学びの文化 失敗は失敗で終わらせるのではなくきちんと学びにすること。 失敗した人を責めないこと。 最後にもう一度、よかったことのまとめ 障害を前提に設計していたこと 大規模かつで複雑なインフラを段階的に移行したこと 学びの文化 Spotifyのインフラが目指す次のステップ 各サービスのオーナーたちに、アプリケーション全体がKubernetesで動かせるようになったことを伝える(βフェーズを脱した) 複数クラスタに跨った設定の管理とワークロードの分散する リージョン内で複数クラスタにサービスをデプロイすることで冗長性を確保する まとめ KubeCon EUには初めて参加したのですが、USよりも規模が小さいという予想をいい意味で裏切られました。 セッションの幅が広く、面白そうなものが固まっていたためどれに行けばよいかわからなくなってしまうこともありました。 全体的にはいままでのKubeConのスタイルと同じくコミュニティや事例・スポンサーの色を強く残しながらも、Spotifyのようなスタートアップの失敗事例をはじめCERNや大学などのアカデミックな事例にまで幅が広まっており、ますます盛り上がりを見せているように感じます。 セッションの動画を見たり記事を見ることで得られる情報も非常に勉強になりますが、やはり現地で空気感を味わいながら「参加」することも非常に重要なのだなと思えるようないいセッションを見ることができました。 冒頭でも触れましたが、スペインはご飯も美味しく人や気候も穏やかで非常に良かったです。 11月にSan Diegoで開催されるKubeCon NAも楽しみにしつつ、以上でレポートとさせていただきます。 ありがとうございました。
アバター
こんにちは。音声UIの開発をしている武田です。今年も Amazon Alexaのコンテスト が開催されます。このコンテストで専用の賞まで用意されている今熱い デザイン言語 、Alexa Presentation Languageでできることを紹介します。 はじめに APLとは 表示する際のトランジションを追加する トーストを実装する 相槌を打つ さいごに はじめに Amazon Alexaのスキル、「 コーデ相談 by WEAR (以下、コーデ相談)」をリリースしました。アイテム名から色々なコーディネートを探せるシンプルなスキルですが、Alexa Presentation Language(以下、APL)でできることを精一杯詰め込んでいます。この記事では、APLを使ってどんなチャレンジをしているか紹介します。ソースコードなどの実装の詳細は個別の記事で解説していく予定です。 APLとは APLはAlexaの音声でのコミュニケーションを補助するためのUIを記述する仕組みです。用いることで視覚的な表現だけではなく、JavaScriptのようなインタラクティブな表現が可能となります。 公式ドキュメント はこちらです。 例えば、写真を中央に表示するレイアウトはこのようになります。 { type: 'Container' , item: { type: 'Image' , id: 'image1' , source: 'https://image-url' , height: '100px' , width: '100px' } , alignItems: 'center' , justifyContent: 'center' , height: '100vh' , width: '100vw' } レイアウトは使用するコンポーネントの種類( Container や Image )を指定することで組んでいきます。コンポーネントに指定できるプロパティ( alignItems や height )はHTML/CSSに近いです。しかし、 alignItems は Container コンポーネントのみで指定できるといったようにコンポーネントごとの役割がはっきりと決まっています。 APLはレイアウト以外にもJavaScriptのような動的な制御が可能です。コンテンツの制御にはコマンドと呼ばれるものを用いて以下のように指定します。 { type: 'Scroll' , componentId: 'targetComponentId' , distance: 1 } コマンドもコンポーネントのように種類が存在しており、上記の例は指定したコンポーネント内をスクロールさせる命令です。コマンドはスキルの応答と同時に実行するか、タッチなどのインタラクションに合わせて実行できます。これから紹介する実装事例では、このコマンドをふんだんに使っていきます。 表示する際のトランジションを追加する 見ていただいた方が早いと思いますので、この処理が走っている検索結果の画面がこちらです。 普通に画像を表示しているだけに見えますが、以下のような処理を走らせています。 1番から順番に画像を表示 表示する際の透明度が連続的に変わるようにする 画像が表示されてから、それぞれの番号を表示 これらの処理はWebでしたら、CSSアニメーションで容易に実装可能ですが、APLでは連続的に値を書き換える便利な機能は存在しません。 SetValue というコマンドでプロパティの値を書き換えることは可能ですが、途中の値を0、0.1、0.2のように補完してくれる訳ではないのでいきなり最後の値に切り替わってしまいます。 そこでコーデ相談では、コンポーネントのスクロールイベントを監視し、スクロール位置をよしなに計算することで透明度の値を書き換えるようにしました。実装はこちらのブログを参考にしています。   Alexa Skill Teardown: Building the Interaction Model for the Space Explorer Skill スクロールはコマンドの例で紹介した Scroll を使うとして、スクロールイベントの監視と透明度の更新はこのようにしています。 { type: 'ScrollView' , id: 'targetComponentId' , item: { type: 'Container' , height: '1000vh' } , onScroll: [ { type: 'SetValue' , componentId: 'image1' , property: 'opacity' , value: '${event.source.value * 4}' } , ] , height: '100vh' } onScroll はスクロール位置の更新に合わせて実行されるコマンドを定義できるプロパティです。スクロール位置が格納されている event.source.value を計算し値を更新することで、連続的に値が変動するアニメーションを実現できるようになります。 onScroll には複数のコマンドを指定できますし、コマンドには delay という発火を遅らせるプロパティも存在しています。これらを組み合わせることで検索時のトランジションを実装しました。 またこの onScroll は画面に表示されていない要素がスクロールした場合でも、コマンドを発火してくれます。つまり透明度を0にしてユーザーに見えない状態でも大丈夫なので、コーデ相談ではトランジション専用のコンポーネントを用意して使っています。 トーストを実装する コーデ相談はスキルの中に仲良し度という概念が存在しており、仲良し度の上昇を知らせるためにトーストを用いています。 今回実装したトーストの動きを分解すると以下のような処理になります。 下から移動して出てくる 透明度が連続的に変わる 少し経ったら消える トランジションで紹介した SetValue コマンドは、2019年5月時点では opacity と text プロパティのみが更新できます。つまり、コンポーネントの位置を変更することが非常に厳しいと言えます。ただ、トーストのような下から移動する動きはコンポーネント内をスクロールさせることによって、同じような見た目を実現することが可能です。スクロールで動かせばそのまま onScroll が使えるので、透明度の更新もトランジションの時と同じ方法で実現できます。 トーストのコンポーネントを簡単に書くとこのようになります。 { type: 'ScrollView' , item: { type: 'Container' , id: 'notifyContainer' , items: [ { type: 'Text' , text: 'トーストのメッセージ' } ] , paddingTop: '100px' // 最初はメッセージを見せないための空間 } , onScroll: [ { type: 'SetValue' , componentId: 'notifyContainer' , property: 'opacity' , value: '${event.source.value}' } ] , height: '100px' } あとは、この ScrollView を表示のタイミングでスクロールさせてあげるだけです。 スクロールできる領域なのでユーザーが触ると動いてしまいますが、透明なコンポーネントを上に重ねればタッチイベントがスクロール領域に届かなくなるので大丈夫です。 ScrollView と挙動が似ている Pager コンポーネントは、ユーザーのインタラクションを無視するようにできるプロパティが用意されています。しかし、横方向しか移動できないため今回のトーストでは活用できませんでした。 相槌を打つ Alexaが反応を返すまでの流れは以下のような手順となります。 これは一般的な流れではありますが、外部のAPIを叩く場合などではサーバーの処理の時間が長くなり、ユーザーに返答が返るまでの時間が長くなってしまいます。そこでコーデ相談では、相槌のようにとりあえず一度返事をして、その後に重い処理を実行するようにしました。流れにするとこのようになります。 上記の流れでは空のリクエストを飛ばし直していますが、普通ですとユーザーが何かアクションを起こさなければサーバーにリクエストは飛びません。リクエストを飛ばすには SendEvent コマンドを実行すれば良いのですが、この SendEvent は 応答の際に実行する コマンドとして直接使用できません。そこでコーデ相談では、 SetPage コマンドと onPageChanged プロパティを組み合わせて SendEvent コマンドを実行するようにしています。 SetPage は Pager 内で表示しているコンポーネントを切り替えるためのコマンドで、以下のように onPageChanged で指定したコマンドを発火させられます。 { type: 'Pager' , items: [ { type: 'Container' } , { type: 'Container' } ] , onPageChanged: [ { type: 'SendEvent' , arguments : [] } ] } この Pager に対してページを切り替える SetPage コマンドを実行することで、サーバーにリクエストが送られるようになります。この流れにすることにより、全体的な処理が完了するまでの時間は伸びています。しかし人間と会話するときのように、まずは何かしらの返答を返すコミュニケーションが取れるようになったと感じています。 さいごに 全体的にだいぶ無理をした実装をしており、公式ブログで紹介されている方法を利用しているとはいえ無理矢理感は否めません。運用やサービスの信頼性を考えると不安がないと言えば嘘になります。例えば onScroll や onPageChanged は公式のドキュメントには載っていませんし、いつ仕様が変わるかわからないプロパティです。今回の実装は良くも悪くもAPLの限界を超えていると感じています。 しかし、ユーザーにとって良いであろう体験をプラットフォームが成熟していないからという理由で諦めたくもありませんでした。ユーザーからしたら、「プラットフォームがサポートしていない機能」も「無理をした不安定な実装」も関係ないからです。重要なのは使いにくかったりうまく動かない状況になったら、それを迅速に把握して修正することだと思います。情報のキャッチアップをして今の実装が明らかに適切でないならば直しますし、相槌をうたない方が使いやすいよね、となるなら一般的な流れに戻すつもりです。 ZOZOテクノロジーズのR&D新規開発チームでは今後普及するであろう技術を先行研究し、様々な技術を用いたサービスを開発しています。今回のAlexaの開発の様子は こちらの記事 で詳しく話しています。より良いユーザー体験を提供するために、技術を駆使して最高のプロダクトを作りませんか? www.wantedly.com
アバター
5月23日にリリースされた、音声だけでコーディネートを検索できるAmazon Alexaスキル「 コーデ相談 by WEAR (以下、コーデ相談)」 。 ZOZOテクノロジーズ初となる音声デバイス向けのプロダクトを担当した3人に音声アシスタントの現在と今回のプロダクトでチャレンジしたことを聞きました。 ■写真左:中村友香(以下、中村) プロダクトの品質を高めることならなんでもやる、プロジェクトマネージャー ■写真中央:武田修平(以下、武田) 庭いじりをこよなく愛する、エンジニア ■写真右:権美愛(以下、権) ユーザーが接触するところすべてに目を光らせる、デザイナー ー 今回の「コーデ相談」は音声アシスタントを使ったプロダクトですが、そもそも音声アシスタントってどのようなものなんですか? 中村: 音声アシスタントといっても色々あって、共通しているのは声でやりとりできるという点ですね。 最近だとGoogleアシスタントやAmazon Alexaは、テレビCMなどで露出が多いですし、身近になってきていると思います。 武田: フィクションの世界でいうと、ドラえもんやベイマックスとの会話がイメージしやすいかもしれないですね。例えばのび太が困ったことを伝えると、ドラえもんは状況を把握して解決するひみつ道具を提案してくれますよね。ドラえもんは実体がありますが、このやりとりって音声アシスタントと一緒ですよね。 ー そう言われると、ドラえもんって音声アシスタントっぽいですね(笑)。 権: ここまでくると、人間同士の会話と変わらないですよね。 中村: ただ現在の音声アシスタントのサービスを使ってみると、なかなか思い通りにいかないことが多いんですよね。 機械的というか、やっぱり人とのやりとりとは大きな違いがあります。図にするとこういうイメージです。 中村: 現在のGoogleアシスタントやAmazon Alexaは、図の3番にあたると思います。 武田: 音声の認識はAIが自動でやってくれるんですが、認識した内容に対してどう返答するかは、開発側がルールをつくって決めていくのでどうしても機械的になってしまうんですよね。「コーデ相談」では、機械的なコミュニケーションにならないように意識して設計しました。 ー 「コーデ相談」について教えてください。 中村: 「コーデ相談」は、音声だけでコーディネートを検索できるサービスです。 例えば「デニムジャケットのコーデを教えて」と話しかけると、WEARに投稿されている800万件以上のコーディネート画像のなかから、 デニムジャケットを使用したコーディネートが見つかります。 www.youtube.com ー どのようなユースケースを想定してつくったんですか? 権: ユーザーインタビューでファッションについて深掘りしていくと、毎朝の服選びに課題を感じているひとが多くいました。 具体的には「コーディネートがマンネリ化してしまう」「新しい服を買ったのに手持ちの服との合わせ方がわからない」といったことです。 朝の忙しい時間なのでスマホで検索するよりも、出かける準備をしながら音声アシスタントに相談できたら、天気予報を聞くように毎日使ってもらえるプロダクトにできると思いました。 武田: でもプロトタイプで検証した結果、「コーディネートを提案するだけでおもしろくない」という意見が出ました。 ー おもしろさって難しいですね。 武田: そうなんですよ。ただ言っていることもわかって、プロダクトを見た時に「試してみたい」「明日も使ってみたい」って思えないのはまずいですよね。 そこで、チームでどうしたらその「おもしろさ」というものが提供できるかを考えました。 アニメーションを派手にするとか、音楽を流すなどいろいろな案が出たのですが、これらの案はコアな体験である「声でコーディネートを探す」という部分とずれてしまいます。 悩んだ結果、最終的に実装することにしたのは「会話自体に変化をつける」でした。 ー なるほど、人間っぽくしたわけですね! 武田: 狙ったわけではないのですが、変化を追求した結果、これって要は人間らしさなんだろうなという気持ちになりましたね。結局目指すべきところはドラえもんだったなと。 権: 最初にやったのはセリフのパターンを増やすことでした。 同じあいさつでも「こんにちは」「はい、どうもー」「中村さんにお会いできるのを、楽しみにしていました!」など複数の種類があります。 中村: セリフを増やすにあたり、キャラ設定を行いました。 チーム内でPinterestの共有ボードを作り、「どんな人とファッションの相談をしたいか?」 「その人は、どんなイメージか?」を深掘って言語化を行い、統一された人格で話しているように設計しました。 権: 「仲良し度」という機能も追加しました。 繰り返し相談していくと親密度があがり、話すセリフが変化していくんです。 検索結果では「WEARで人気のコーデをご用意しました」というのですが、仲良し度が上がると「私がすきなコーデを表示します。冗談ですよ」と言ったりします。 中村: セリフを増やし、ランダム性と親密度を加えると、人間っぽくなっていきました。 それ以前の単調なやりとりとはまったく違う体験になりました。 音声アシスタントに命を吹き込んでいるような感覚でしたね。 ー 今後、「コーデ相談」はどのようになっていくでしょうか? 中村: 「専属のスタイリストが、いつでもコーディネートの相談に乗ってくれるサービス」を目指します。 私は元々オタクでファッションに関心が低く、特に「おしゃれ」に対してコンプレックスや恐怖心に近い感情を抱いていました。 元アパレル店員の同僚に悩みを打ち明けたところ、私がすきそうな服を教えてくれたり、着回しの相談にたくさん乗ってもらい、ファッションの楽しさがわかりはじめてきました。 「コーデ相談」は、「ファッション」や「おしゃれ」というキーワードに対して苦手意識を抱いているような人でも簡単になりたい自分像に近づけたり、ファッションに対して自信を持つキッカケを提供できるサービスにします。 今後も、単に新しい技術を活用するだけではなく、未来で当たり前になっている体験をいかに今の技術で実現するかに挑戦していきます。 いかがでしたでしょうか。「コーデ相談」は、Amazon Alexa対応デバイスに「アレクサ、コーデ相談を開いて」と呼びかけるだけですぐにご利用いただけます。 3人が声を枯らすほど何度もアレクサに呼びかけながらつくった「コーデ相談」を、ぜひ体験してみてください。 「コーデ相談 by WEAR」の詳細は こちら エンジニア・武田による技術的な記事は こちら www.wantedly.com
アバター
こんにちは。Webフロントエンドエンジニアの松井菜穂子です。 ZOZOテクノロジーズに入社して一年ほど経ちます。 あるサービスの立ち上げから運用まで、Webフロントエンドのチームリーダー・開発メンバーとして関わってきました。 当記事では、当社のWebフロントエンド開発現場にあった問題と、それぞれの課題に対して堅実に積み重ねた技術的な改善方法についてご紹介します。 はじめに モダンな技術でも負債は生まれる 負債を何故改善するのか 要因 その1: Vueコンポーネントを綺麗に分割する テンプレート コンポーネントクラス 使用例 解決策 テンプレート コンポーネントクラス 使用例テンプレート 使用例コンポーネントクラス コンポーネントカタログで汎用化を促す コード テンプレート(atoms.vue) コンポーネントクラス(atoms.vue) サンプル その2: Vuexをシンプルにする Stateのツリー構造を見直し 改修前のツリー構造 改修後のツリー構造 Actionの責務を最小限に限定 API通信の抽象化はAdapterクラスで行う APIレスポンスモデルからアプリケーションモデルへの変換はRepositoryクラスで行う Stateに関連しない処理はVuex外へ移動 その3: TypeScriptを活用する Repositoryの実装例 型関連のTips コンポーネントのData初期化 Enumへの変換 コンポーネントPropの例 アプリケーションモデルの例 その4: 開発ルールを制定する その5: 改善を可視化する おわりに はじめに 改善対象のプロダクトのWebフロントエンドは、以下のような技術で構成されているSPAです。 フレームワーク:Vue.js 状態管理:Vuex 言語:TypeScript これらは、当社の数ある開発プロダクトのスタックの中でも比較的モダンなものです。 インターネット上にたくさんの関連情報があり、OSSでもこれらの技術に対応した様々なライブラリが公開されています。 モダンな技術でも負債は生まれる そんな人気の技術を使っていて、何が問題なのか疑問に思われるかもしれません。 当社では、ZOZOTOWNやWEARのような大規模で息の長いサービスを運用するチームの他に、新規の事業を担当する開発チームをたくさん抱えています。 そういったチームでのスクラッチ開発は、まさにベンチャー企業のようなスピード感溢れるスタイルで行われています。 後々の保守変更に耐えうる技術構成を取るため、開発メンバーの手に馴染んでいないモダンなスタックを選定するというチャレンジをしたのが今回のパターンでした。 手探りで実装を進めた結果、各所で設計方針のブレが発生し、いわゆるレガシーな技術構成で開発するよりもかえって可読性が低く保守しづらいコードになってしまいます。 これ以降、この状態のコードを「負債」と表現します。 負債を何故改善するのか 負債の定義や、その改善によるビジネスインパクトがかかるコストに見合うかどうかの見極めは難しいものです。 今回も実際にその見極めが正確にできたわけではありませんが、それでも時間を割いてコードを改善しようと決めたのは、 前向きなチームを作ること が重要だと思ったからです。 試行錯誤しながらサービスを完成させていくようなフェーズは、開発メンバーがそれぞれ機械的にタスクをこなすだけでは成り立ちません。 ある程度機転を利かせながら時にはイレギュラーなことにも挑戦できる環境がもっとも大事だと思っています。 その環境を作るためには開発メンバーが前向きに仕事に取り組めるような雰囲気作りが必要です。 エンジニアが前向きに開発に励んでいるサービスはそうでないサービスに比べて長期的にビジネス成果も上がるものだと信じています。 しかし、負債がそれを阻害しているかもしれません。 当チームでは、下記のような理由でメンバーのモチベーションが下がってしまうことがありました。 要因 コード間の依存度が高く、変更作業の影響が読めない。 実装をする上で様々な知見を必要とする領域が多数あり、作業ハードルが高い。 良いコードを書いているという実感がない。 チーム作りといえばマネジメント関連の施策を思い浮かべがちですが、上記のような要因を取り除くことでも前向きなチーム作りができます。 今回は、当チームで積み重ねた地道な開発施策をご紹介させていただこうと思います。 その1: Vueコンポーネントを綺麗に分割する まずは、Vue.jsのプロダクトで大きなベースとなるVueコンポーネントの設計です。 Vueコンポーネントの多くには「 要因1. コード間の依存度が高く、変更作業の影響が読めない 」に当てはまる負債がありました。 例えば下記のような、リンクテキストを表現する LinkField コンポーネントです。 当プロダクトではクラス構文を用いてコンポーネントを定義しており、Vuexとのバインディングに vuex-class ライブラリを使用しています。 テンプレート < template > < router- link :to= "`${localePath}${to}`" >< slot ></ slot ></ router- link > </ template > コンポーネントクラス import { Component , Vue } from 'vue-property-decorator' import { Prop } from 'vue-property-decorator' import { State } from 'vuex-class' @Component class LinkField extends Vue { @Prop ( { type : String , required: true } ) to!: string // Propで遷移先パスを受け取る @State ( 'localePath' ) localePath?: string // Vuex Stateの"localePath"をバインド } Stateから取得している localePath という値は、以下のような仕様に基づいています。 URLパスの第一・第二要素は国と言語を表す 例: /us/en → アメリカ・英語、 /de/de → ドイツ・ドイツ語 サービス内のページ遷移は基本的に国・言語固定で行うため、遷移前のURLパスの第一・第二要素を保持したい 例: 商品一覧から注文一覧への遷移 /us/en/items → /us/en/orders 遷移前の国・言語を取得して遷移先に指定する処理を全てのリンク箇所で記述するのは面倒なので、共通化したい Stateに国と言語のパスを保持しておき、 LinkField コンポーネント内でパスを結合することで共通化 使用例 < link -field :to= "/orders" > 注文一覧へ </ link -field > <!-- to属性には第三要素以降のみを指定でOK --> 最初は便利でしたが、このように汎用的なコンポーネントがVuex State(=状態)と密に結合している設計だと、 LinkField コンポーネントが素直に使えない場面が出てきます。 例えば、国・言語を切り替えつつ他のページに遷移させるリンクを置きたいときです。 Stateの localePath はグローバルな状態を管理するプロパティであるため、 LinkField コンポーネントの都合で書き換えると他の要素に予期せぬ影響が出てしまいます。 localePath の変更なくコンポーネントの外からURLパスの第一・第二要素を渡したい場合、コンポーネントに手を入れずに実現できません。 こういった、状態と密結合である故に汎用的に使えないコンポーネントがたくさんありました。 コンポーネントの利用を諦めてスパゲッティコードのような記述をしてしまったり、限定的な用途のPropを追加したためさらに使いづらくなったりして、負債になっていきました。 解決策 当プロダクトでは、当初からコンポーネント分割の基準として導入していたAtomic Designという設計手法に基づいた簡潔なルールを元に、リファクタリングを行いました。 「Atom」「Molecule」レベルの汎用コンポーネントは、Vuex Stateを参照しない というルールです。 Atomic Designでの分割は、コンポーネントの見た目の粒度を基準に行っていました。 それに沿ってコンポーネントに閉じ込める共通実装は見た目基準のものだけにし、状態は切り離そうという考えです。 ただし、この設計手法をとっている例はよく聞きますが、どんなコンポーネントにとっても最適かというとそうではないかもしれません。 LinkField の例でも localePath に依存しないケースの方が少なく、そのためにわざわざ共通処理を引き剥がすのが正しいか、この1つをとれば色んな意見が出ると思います。 しかし目的は前向きなチーム作りなので、他への影響の配慮でストレスを感じない実装環境作りを最優先とし、汎用コンポーネントからVuexを分離することで可能な限り薄く作るようにしました。 テンプレート < template > < router- link :to= "to" >< slot ></ slot ></ router- link > </ template > コンポーネントクラス import { Component , Vue , Prop } from 'vue-property-decorator' @Component export default class LinkField extends Vue { @Prop ( { type : String , required: true } ) to!: string // Propで遷移先パスを受け取る } 使用例テンプレート < link -field :to= "`${localePath}/orders`" > 注文一覧へ </ link -field > 使用例コンポーネントクラス import { Component , Vue } from 'vue-property-decorator' @Component export default class Page extends Vue { localePath: string = '' // Vuex Stateから取得しても、ページ内で宣言してもOK } ここまで改修した後、 LinkField コンポーネントで共通化している見た目基準の実装が下線スタイルくらいであることに気づき、このコンポーネントは削除することになりました。 そして直接Vue Router内蔵の RouterLink を使用することになりました。 各ページコンポーネントの記述量は少し増えることもありますが、汎用コンポーネントの使い方について深く考えず実装できるようになり、コードを気持ちよく書くことができるようになります。 ある程度汎用コンポーネントの機能が上記のような見た目基準の粒度に揃うと、ページの実装がコンポーネントの単純な組み合わせ作業のみで完結します。 多方面への影響を考慮せずにシンプルな見積もりができるようになり、急なレイアウト変更のようなタスクにも前向きに取り組めるようになりました。 コンポーネントカタログで汎用化を促す 汎用コンポーネントがVuexを参照しない見た目基準の粒度であることを保障するため、コンポーネントカタログを役立てました。 コンポーネントカタログとは、コンポーネントの仕様書をプログラムで提供できる仕組みのことを指します。 Vue.jsを用いたプロダクトでは、よく Storybook が使われるようです。 当プロダクトでも初期段階でStorybookがインストールされていましたが、運用のハードルが高かったため廃止し独自のカタログを作成しました。 詳細は こちら の記事をご覧ください。Nuxt.js向けの説明ですが、大まかな設計はVue.jsにおいても同様です。 プロダクトに即したカタログ実装の例を以下に示します。 コード カタログ自体もVue.jsのSPAになっています。 Atomic Designを意識したコンポーネント分割のため、ページもAtomic Designのレベルで分割( atoms.vue / molecules.vue / organisms.vue )しました。 テンプレート( atoms.vue ) < template > < h1 > Atoms </ h1 > < h3 > CheckField </ h3 > < p > valueはBoolean, Arrayどちらでも対応します。 </ p > < props-spec :component= "CheckField" ></ props-spec > < div > inline設定 </ div > < div > < check-field v-model= "isInline" > inline </ check-field > </ div > < div > Booleanで使う </ div > < p > isCheck1 = {{ isCheck1 }}, isCheck2 = {{ isCheck2 }} </ p > < div > < check-field v-model= "isCheck1" :inline= "isInline" > isCheck1 </ check-field > < check-field v-model= "isCheck2" :inline= "isInline" > isCheck2 </ check-field > </ div > < div > Arrayで使う </ div > < p > someArr: {{ someArr }} </ p > < div > < check-field value = "a" v-model= "someArr" :inline= "isInline" > option a </ check-field > < check-field value = "b" v-model= "someArr" :inline= "isInline" > option b </ check-field > < check-field value = "c" v-model= "someArr" :inline= "isInline" disabled > option c </ check-field > </ div > < hr /> < h3 > PillButton </ h3 > < props-spec :component= "PillButton" ></ props-spec > < div > theme=default(デフォルト) </ div > < div > < pill- button > Button </ pill- button > </ div > < div > theme=black </ div > < div > < pill- button theme= "black" > Button </ pill- button > </ div > < div > theme=blue </ div > < div > < pill- button theme= "blue" > Button </ pill- button > </ div > < div > theme=red </ div > < div > < pill- button theme= "red" > Button </ pill- button > </ div > < div > disabled </ div > < p > themeが指定されていてもdisabledのスタイルで上書きされます。 </ p > < div > < pill- button disabled = "disabled" theme= "red" > Button </ pill- button > </ div > < div > clickイベントを指定した場合 </ div > < p > buttonClicked: {{ buttonClicked }} </ p > < div > < pill- button @click= "clickButton" > Button </ pill- button > </ div > < div > toを指定した場合 </ div > < p > clickイベントが指定されていても発火しません。 </ p > < div > < pill- button to= "/" @click= "clickButtonNotTriggered" > Button </ pill- button > </ div > < hr /> <!-- 以下、他コンポーネントのカタログ実装が続く --> </ template > コンポーネントクラス( atoms.vue ) import { Component , Vue } from 'vue-property-decorator' // プロダクトコードからコンポーネントをインポート import CheckField from '@/components/atoms/CheckField.vue' import PillButton from '@/components/atoms/PillButton.vue' @Component ( { components: { CheckField , PillButton } } ) export default class UiAtomsView extends Vue { CheckField = CheckField PillButton = PillButton isInline = true isCheck1 = true isCheck2 = true someArr = [] buttonClicked = false clickButton () { this .buttonClicked = true } clickButtonNotTriggered () { alert( 'This does not show up' ) } } サンプル PropsSpec という自作コンポーネントがプロダクトコードのコンポーネントのProp定義をパースし、表レイアウトで表示する作りになっています。 当チームではatom, moleculeの実装を追加・編集する際にはかならずカタログに仕様を記載する運用にしました。 この仕組みは、コンポーネントの一覧の提供だけでなく、実装者・コードレビュアーに汎用的かつシンプルな設計を意識させる働きをしてくれました。 カタログからはVuexを切り離しているので、Stateへの参照も自然に抑制できました。 何より、保守効率を考慮した汎用設計という面倒な作業が、綺麗に整頓されたカタログの画面を見ながら行えるためフロントエンドエンジニアにとって楽しい仕事になります。 このような工夫も、前向きなチーム作りに貢献した取り組みでした。 その2: Vuexをシンプルにする 次に、フロントエンドの重要な状態データの管理を担うVuexです。 VuexのState・Actionが扱いづらくなっていたことが、前向きになる上で最も大きな障壁だったかもしれません。 ここで抱えていた負債は「 2. 実装をする上で様々な知見を必要とする領域が多数あり、作業ハードルが高い 」に当てはまります。 バックエンドAPIとの通信に関わるデータがVuexで扱われていたため、構造的な記述がされていないと、外部仕様に詳しくないメンバーにとっては手を出しにくい領域となってしまいます。 その結果、他チームとの連携が多いメンバーにVuex周りの実装タスクが属人化し、忙しいときでも上手く作業分担ができないことでさらにチームの雰囲気が悪くなっていきます。 この状況を打開するため、Vuexで何を扱っているかということの明確化と、仕様に精通していないと手が出せない部分の線引きをしました。 Stateのツリー構造を見直し Vuexでは、Stateが保持する状態データの形式は決まっていません。 プロダクトの仕様に合わせて決定する必要がありますが、今回はその検討に時間を割かず思い思いに実装した結果、次のような保守性の低い設計となってしまいました。 改修前のツリー構造 ページ1モジュール ページ1の状態データ ページ1の状態データ リソース1モジュール リソース1の状態データ リソース1の状態データ ページ2の状態データ ページ3モジュール ページ3の状態データ ページ3の状態データ リソース2の状態データ リソース2の状態データ 共通モジュール グローバル画面要素1の状態データ ここで「リソース」とは固有画面に紐づかないようなデータのことを表しています(例:ログインユーザー)。 また、「グローバル画面要素」とは固有画面に紐づかない画面要素のことです(例:ヘッダー)。 ページ基準で切ったはずのモジュールの中にリソース基準のモデルのデータが入ったり、その逆だったりと、粒度の異なるデータが同列に混在している状態です。 上記は説明のため「ページ」や「リソース」といった汎用的な名称で区切っているため、混在している状況が明確ですが、実際の値はより複雑です。 それぞれのプロパティをよく見てデータバインド先の実装と照らし合わせないとモジュール・モデルが何基準なのか非常に分かりづらいです。 そこで、状態データはページ等の画面上の要素基準で切ることを基本ルールとして定め、リソースは共通モジュールに配置しました。 スコープを適切に伝えるため、単一画面のみに属する状態データはVuexに乗せず、ページコンポーネントのDataに閉じ込めました。 以下のように、Vuexには画面を跨る状態データと共通データのみが残ります。 改修後のツリー構造 ページカテゴリ1モジュール ページカテゴリ1の状態データ ページカテゴリ1の状態データ ページカテゴリ2モジュール ページカテゴリ2の状態データ ページカテゴリ2の状態データ 共通モジュール グローバル画面要素1の状態データ リソース1モジュール リソース1の状態データ リソース1の状態データ ここではただ完璧な設計を目指すのではなく、サービスやシステムの要件も意識して設計しました。 おそらくVuex Stateの使い方として、上記のような画面要素の基準でのモジュール分割はベストプラクティスではないかもしれません。 個人的にも、リソース基準でデータを管理する方がアプリケーションの保守性を高く保てるように感じていました。 しかし、状態データの主な取得元である既存APIがページに最適化されたインタフェース設計で実装されており、リソース基準でのモデルを作るのに苦労しそうでした。 そこで、APIの改修やモデル変換のための仕組み作りといったコストの大きい施策の実施を待たずに、今回は簡単な構成への変更に踏み切りました。 Stateの見通しが良くなり、画面跨ぎの状態データ設計を誰でも行えるようになりました。 Actionの責務を最小限に限定 Vuexで扱うデータのほとんどがAPI通信を介して取得されるので、非同期通信に関わる広範囲の領域の処理がActionに集約されていました。 Actionが状態管理ライブラリの一部としての役割を超え、以下のような機能を全て手続きとして詰め込んだ関数となり、読みにくいコードを生み出していました。 API通信の抽象化 API通信エラーハンドリング APIレスポンスモデルからアプリケーションモデルへの変換 この機能をそれぞれ適切なクラスに分割し、Actionからは必要な処理を呼び出すのみにしました。 API通信の抽象化はAdapterクラスで行う 当プロダクトにはAPI通信にまつわる共通仕様が複数あり、API連携機能を実装する上で全ての仕様を把握しておかなくても、肝心なロジックに集中して実装できるという状況が理想でした。 そのため、API通信ライブラリを直接使うのではなく、通信を抽象化したAdapterクラスの中に共通の振る舞いを隠蔽しました。 // Adapter class APIAdapter { private async baseRequestAPI () { // 通信の共通処理(エラーハンドリングも含む) // 実際にAPIへのリクエストを行う // 下記4メソッドからこのprivateメソッドを呼ぶ } async requestGetAPI < Res , Req = void >( endpoint: string , req: Req ) : Promise < Res > { // GETメソッドの処理 // reqをURLパラメータにシリアライズする処理など } async requestPostAPI < Req , Res >( endpoint: string , req: Req ) { // POSTメソッドの処理 } async requestPutAPI ( endpoint: string ) { // PUTメソッドの処理 } async requestDeleteAPI ( endpoint: string ) { // DELETEメソッドの処理 } } // 利用例 const apiAdapter = new APIAdapter () const orders = await apiAdapter.requestGetAPI < OrdersGetResponse >( '/orders' ) API通信と同様、他の外部連携ロジック用にもそれぞれのAdapterクラスを切りました。 CookieやLocalStorage等へのアクセスや、WebViewでの動作時のネイティブアプリとの連携等です。 小難しいシステム仕様に依存する処理が隠蔽でき、Actionの見通しが良くなります。 APIレスポンスモデルからアプリケーションモデルへの変換はRepositoryクラスで行う コンポーネントやVuexで扱う状態データに値を格納する際、APIレスポンスのモデルをフロントエンドで扱いやすくするためのアプリケーションモデルに変換する必要があります。 変換処理はコード量が多くなりがちなのでRepositoryパターンに見立ててクラス内にまとめました。 class OrderRepository { async getAll () : Promise < Array < Order >> { // データ取得・モデルの変換処理 } async get ( id: string ) : Promise < Order > { // データ取得・モデルの変換処理 } } Stateに関連しない処理はVuex外へ移動 当然ですが、ActionからはVuexでの状態管理に関連のない処理は排除しました。 Stateの整理をしたことによりVuexで扱わなくなったデータの取得処理は、Actionではなくコンポーネントのメソッドに移動しました。 値を加工するだけの純粋関数等、AdapterやRepositoryに置き場所のない処理はhelperファイルに退避させました。 上記のような改善を行ったことで、Vuex上の実装が「なんとなく難しそうなコード」に見えていたのが、内部でどんな処理を行っているか把握しやすくなりました。 その3: TypeScriptを活用する TypeScriptの観点でも負債がありました。 静的型付け言語に不慣れなメンバーも多く、宣言するのに最適な型がわからず、ひとまず動くコードを書くために any 型が多く使われていました。 any 型が多いということは、それだけでエンジニアのやる気を削ぐ原因になります。 多くのフロントエンドエンジニアにとって学習コストの高い型付き言語をわざわざ導入したメリットをしっかりと活かすため、「 要因2. 実装をする上で様々な知見を必要とする領域が多数あり、作業ハードルが高い 」を解決するアプローチとしてあらかじめサービス仕様に則ったモデルの型定義を用意しました。 アプリケーションモデルとAPIレスポンスモデルの定義は、チーム内外のメンバーとの会話が必須な作業です。 当チームではアプリケーションモデルを、限りなくユビキタス言語に近い言葉を使って表現する汎用的なモデルとして位置付けていました。 設計方針を統一するため、詳細機能の実装前にモデルのプロパティレベルの命名について十分な時間をとって話し合い、型に落とし込んで実装してしまいました。 APIレスポンスモデルについては、リーダーがバックエンドチームとすり合わせたインタフェース仕様をまとめて型定義しました。 今回は手作業で行いましたが、OpenAPIの自動生成の仕組みを使えばさらに効率的かと思います。 参考: OpenAPI3を使ってみよう!Go言語でクライアントとスタブの自動生成まで! 参考記事はGo言語の紹介ですが、OpenAPIのAPIクライアント自動生成はTypeScriptにも対応しています。 事前にモデルをまとめて定義しておけば、その後のRepositoryやコンポーネントの実装のハードルが格段に下がります。 Repositoryの実装例 class OrderRepository { async get ( id: string ) : Promise < Order > { const rawOrder = apiAdapter.requestGetAPI < OrderGetResponse >( `/orders/${id}` ) // OrderGetResponseは既に定義済み return { id: rawOrder. id , shipments: rawOrder.details.map ( d => toShipment ( d )) } // Order型になっていなければコンパイルエラーが出る } } OrderRepository.get() の実装者は以下を把握していなくても作業ができます。 GET /orders/{id} のインタフェース定義 アプリケーションモデルの定義。 OrderGetResponse > details を shipments と呼ぶこと等 定義済みの型を組み合わせて実装ができるので、 any が自然と減っていきます。 また「モデルの定義」という作業のみを事前に切り出しているため、メンバーがどのようなアプローチを経てモデルの定義をしているかが他メンバーからも把握しやすくなりました。 さらにモデリングやそのための情報収集に興味を持つメンバーが増え、作業属人化の解消にも繋がりました。 型関連のTips 上述のモデル関連以外にも、型を上手く使えていない場面があったので、helperで工夫して改善しました。 コンポーネントのData初期化 コンポーネントのDataが、ビューモデルのプロパティとしてどんなデータを持つかを型で表現できていないと、読みにくく手を入れたくないコンポーネントになってしまいます。 Vue.jsの仕様上、クラスの初期化時に値を代入できないData(APIから値を受け取る場合等)に対して一旦nullを代入する必要があります。 宣言部分を data: T | null = null のように記述しなければならず、本当に意味上nullableなのかどうかをよく考えて読み解く必要がありました。 これを解決する LateInit デコレータを作成しました。 詳細: クラススタイルVueコンポーネントの変数初期化を改善する またクラスの初期化時に値があるものでも、Dataの初期化時にコンポーネントのVueインスタンスにアクセスできないため一旦nullで初期化するような例がありました。 import { Component , Vue } from 'vue-property-decorator' @Component export default class Hoge extends Vue { // fuga: Fuga = this.getFuga() ←created等でアクセスするthisと異なるためNG fuga: Fuga | null = null // 一旦nullを代入 created () { this .fuga = this .getFuga () } } これを解決するため、 Data デコレータを作成しました。 export const Data = < T >( initializer: ( vm: Vue ) => T ) => createDecorator (( options , key ) => { options.mixins = [ ...options.mixins || [] , { data ( this : Vue ) { return { [ key ] : initializer ( this ) } } } ] } ) import { Component , Vue } from 'vue-property-decorator' @Component export default class Hoge extends Vue { @Data ( vm => vm.getFuga ()) fuga!: Fuga } Enumへの変換 型を使って仕様を伝える上で、TypeScriptのEnumは非常に有用です。 あるプロパティがどんな値を持ちうるかを列挙できるので、アプリケーションモデルやコンポーネントのPropの型に積極的に使いました。 コンポーネントPropの例 import { Component , Vue , Prop } from 'vue-property-decorator' @Component class PillButton extends Vue { @Prop ( { type : String , default : PillButton.Theme.Default } ) theme!: PillButton.Theme } namespace PillButton { export enum Theme { // クラスと同名のnamespace内でenumをexportすれば、import先の他コンポーネントからもPillButton.Themeとして参照できる Default = 'default' , Black = 'black' , Blue = 'blue' , Red = 'red' } } export default PillButton アプリケーションモデルの例 interface Gender { code: Gender.Type label: string } namespace Gender { export enum Type { Male = 'MALE' , Female = 'FEMALE' } } export default Gender APIレスポンスに含まれるコード値をアプリケーションモデルに詰める際、元の値はstring型のため、Enumに変換する必要がでてきます。 Gender.Type['MALE'] のようにして変換できれば良いのですが、これは Gender.Type.Male に上手く変換されてくれないため、変換関数を自作しました。 export function mapToEnum < T >( enumObject: T , value: any ) : T [ keyof T ] | undefined { if (typeof enumObject === 'object' ) { for (const key in enumObject ) { if ( enumObject.hasOwnProperty ( key ) && enumObject [ key ] === value ) { return enumObject [ key ] } } } else if ( enumObject instanceof Array ) { return enumObject.find ( value ) } } mapToEnum(Gender.Type, 'MALE') は Gender.Type.Male として使えます。 このように、複雑な型定義が必要な部分は色々な事情に詳しいメンバーが事前に実装しておくことで、他メンバーが前向きにTypeScriptを利用できるようになりました。 その4: 開発ルールを制定する ここから先は、「 要因3. 良いコードを書いているという実感がない 」を解消するための施策です。 コード改善の大半には、チーム内での決め事が付いて回ります。 作った仕組みが意図しない方法で利用されると改善の恩恵を受けられない場合があるため、作成時の前提を守って実装してもらうことが非常に重要です。 メンバーがコードを読んで前提を察してくれることを期待せず、決め事を開発ルールとしてテキスト化しておくと、やるべきことがクリアになってより前向きになれます。 当チームではGitHubリポジトリで開発を行なっているので、Pull Requestテンプレートにチェックリストとしてルールを記載しました。 レビュイーが当てはまる項目にチェックを付け、チェックされているルールが本当に遵守されているかをレビュアーが確認するフローとしました。 時として、どうしてもルールの例外を認めなければならない実装があります。 そういったケースが予想される場合には、「基本的にXXXする」というような強制力を弱めたルールを制定しておきました。 ルールが守れない事情をPull Requestのコメント等で説明することをレビュイーに促せるため、例外を認めるかどうかの点においてチーム内で合意形成できるようになります。 ルールの重要度が高くない場合や時間が取れない場合には、Pull Requestテンプレートへの記述でなくSlack等のコミュニケーションツールでの共有のみでも良いことにしました。 開発ルールを定める面倒臭さが、改善のための仕組み作りの足枷とならないようにするためです。 負債を生まないための開発ルールに対してチーム内で共通認識が生まれたことにより、良いコードを書くための道筋が見えるようになりました。 その5: 改善を可視化する 多くの開発チームでは、リファクタリングのみを行う期間が取れることの方が少なく、機能開発の傍らの作業となることが多いかと思います。 当チームでもビジネス案件をこなしながら改善を行なっていたので、常に「この改善に時間をかけて良いのだろうか」という不安が付き纏いました。 改善への取り組み中の段階ではその問いに対する答えは出ないので、改善タスクの区切りがついたところで振り返りをし、効果が出たことを可視化するよう努めました。 ただし冒頭で述べた通り、改善の効果は定量的に評価できないケースが多いです。 感覚的で曖昧な基準の評価になってしまいますが、前向きなチーム作りを目的とした施策なのでそれで良いと割り切るようにしました。 当社ではチームで隔週KPTを行なっているので、メンバーが自然にKeepとして「この改善をしたらXXXが書きやすくなった」等の所感を共有してくれました。 勿論、Problemとしてあまり効果が出なかったと感じた改善も共有されました。 可視化という観点では、タスク管理の厳密さ、透明性も前向きなチーム作りに貢献します。 当チームではJIRAのガントチャート機能を使用してタスクを整理しました。 ビジネスマイルストーンに合わせた案件リリーススケジュールの合間にリファクタリング作業が収まるようにチケットを切り、チーム内外の全員が参照できる状態にしました。 スケジュールの組み立ての際には、振り返りでの結果を元にして、やるべきリファクタリングを取捨選択します。 改善の効果測定の基準が曖昧な状態でも、上記のような可視化をすることで改善作業に自信が持てるようになってきます。 チケットの見積もりに収まる期間であれば時間を目いっぱい使うことができます。 またビジネス観点のスケジュールに与えるインパクトを常に確認しながら作業ができるため、伸び伸びと改善に打ち込むことができるようになりました。 おわりに コードの負債を改善するための取り組みを紹介しました。 エンジニアにとって毎日向き合う対象であるコードが綺麗になっていくことは、チームの雰囲気の向上に大きな効果をもたらします。 しかし、どれをとっても誰かが単独で取り組んでやり遂げられるものではなく、マクロ・ミクロ両方からの改善アプローチが必要です。 説明した施策を振り返ってみても、リーダーが漠然と感じた課題をメンバーに伝え、メンバーから「こんな方法で解決できるのでは」という提案があって初めて前進したパターンがほとんどでした。 また、リファクタリングとビジネス案件に割くコストをバランスよく割り振るためには、ビジネスサイドの協力が欠かせません。 紹介したVue.js+Vuex+TypeScriptの例に限らず、またWebフロントエンドの領域以外の方にとっても、改善の取り組みの参考になれば幸いです。 チームの雰囲気が暗い・後ろ向きになっていると感じたら、リーダーの方はメンバーをランチや飲みの場に誘うだけでなく、コードの負債について話し合いの場を設けてみてはいかがでしょうか。 メンバーの方は「負債があるせいでXXXできない」という場に遭遇したら、改善の時間が欲しいとリーダーに掛け合ってみてはいかがでしょうか。 今回ご紹介した施策には、以下の開発メンバーと共に取り組みました。 高橋智仁( @anaheim0894 ) 権守健嗣( @AmatsukiKu ) 茨木暢仁( @niba1122 ) 安藤正人 松浦麻奈未( @mtmn07384 ) ZOZOテクノロジーズには、こんな改善を日々実施しているチームや、改善施策を見守ってくれる環境があります。 一緒に前向きなチーム作りを盛り上げてくれるエンジニアを大募集中です。 ご興味のある方は、 こちら からぜひご応募ください。
アバター
こんにちは、広報の坂井です。ZOZOテクノロジーズ発足から1年。2019年4月現在、ZOZOテクノロジーズは3名の技術顧問を迎えています。iOSアプリ開発の第一人者である岸川克己氏、Ruby,Ruby on Railsコミッターの松田明氏、そしてRubyの生みの親であるまつもとゆきひろ氏です。 3名にインタビューを行い、ZOZOテクノロジーズの技術顧問になった背景や、エンジニア人生について語っていただきました。 岸川 克己 氏( @k_katsumi ) iOS/macOSアプリケーションの開発者。様々な企業にテクニカルアドバイザーとして関わる。また、多数のオープンソースライブラリをGitHubで公開。2019年1月よりZOZOテクノロジーズ技術顧問。 松田 明 氏( @a_matsuda ‏) Rubyコミッター。Ruby on Railsコミッター。 kaminari 、 action_args 、 active_decorator などのライブラリの作者。地域コミュニティAsakusa.rbの発起人。RubyKaigiのチーフ・オーガナイザー。最後のRuby Hero。2019年1月よりZOZOテクノロジーズ技術顧問。 まつもと ゆきひろ 氏( @yukihiro_matz ) 1965年生まれ。筑波大学第三学群情報学類卒業。プログラミング言語Rubyの生みの親。株式会社ネットワーク応用通信研究所フェロー、一般財団法人Rubyアソシエーション理事長、ZOZOテクノロジーズをはじめとした複数社の技術顧問、Herokuチーフアーキテクトなどを兼任。松江市名誉市民。通称 Matz。 やる気がある人がジョインすれば、さらに活躍できる風土 ー 技術顧問を引き受けてくださった経緯を教えていただけますか? Matz: VASILY(2017年10月 ZOZOグループにM&A)の頃に、社員から「技術顧問になってもらえませんか」と、声を掛けられたのがきっかけです。 当時、Rubyを作っている立場として、実際に使っている人との距離があるのを感じていました。自社サービスを提供しているVASILYなら、Rubyをどう使っていて、どこが使いにくい、というような意見も聞けるのではないかと思い、引き受けようと決めました。 VASILYは自分が初めて技術顧問を引き受けた会社ですが、その時に自分自身に対して決めたルールは、“自社サービスを提供する会社”であること。どのようなサービスを提供し、どのような技術を使うかを自分たちで決め、開発している会社と話がしたいと思いました。そういった意味では、VASILYやZOZOは条件にも合っていますし、ファッションという領域は自分が知らない分野でもあるので、面白そうだと感じました。 岸川: 自分も、社員の方に誘われたのがきっかけです。その方とはかれこれ4〜5年の付き合いになります。フルタイムの仕事があることもあり、これまでも積極的に技術顧問の仕事を増やしているわけではありませんでした。 しかし、熱心に依頼をされたこともあり、少しでも役に立てるのであればという思いで引き受けました。 ー ZOZOテクノロジーズの社員と実際に関わってみて感じることはありますか? 岸川: もう少し自由にチーム作りをやってみても良いのでは、と感じる部分がありますね。入る前から“こういうチームにしたい”というビジョンは聞いていて、そのために1つ1つこだわりていねいに取り組んでいるように感じます。 当事者のエンジニアが自信を持てていない瞬間もあると思うので、「そのやり方は間違ってない」「今のフェーズならその技術の選択はあり、なし」という、背中を押すようなアドバイスをしながら、役に立っていきたいです。 松田: 良い意味で、 若くて野心のある会社 という印象です。良いものをどんどん取り入れようとする気持ちが強く、伸びしろがあります。 やる気がある人がジョインすれば、さらに活躍できる風土があるので、その良さを活かしたサポートがしたいと感じています。 ー 今後、技術顧問としてどのようにZOZOテクノロジーズに関わってくださるのでしょうか? Matz: 技術顧問なので、直接ZOZOグループのソフトウェア開発をするわけではなく、開発エンジニアと話をしながら、彼らの知らないことを教えてあげたり、モチベーションをあげたりという手伝いができたらいいなと思っています。 本を読めば「この機能はこう働きます」「こうやって使います」ということは書いてありますが、なぜそれを導入したのか、なぜこうなっているか、ということは書いていないので、そのあたりを説明できるのは私の強みだと思っています。 大切なのは、とにかく書くこと ー エンジニアとして大事にしている考え方があれば教えてください。 松田:とにかくコードを書く ということです。「コードを書く」というのはもちろんオープンソース的な意味で、ですね。「現場の今ある課題をきちんとオープンなコードで解決する」というのは日頃から考えてます。かれこれ10年くらいは発表の度に同じことを言い続けてる感じです。 このへんの考え方は、Ruby界の先輩方の影響が大きいように思います。特にまつもとさんですね。こんなやり方で世界を変えた日本人は、他には居ませんよね。 岸川: 最終的にソースコードを公開するというのはとても大切だと考えています。エンジニアリングで大事なことはたくさんありますが、どう書くかという所に、結局1番違いが出ます。良いコードは良いコードとして存在するので、どんどん書いて小さなものからオープンにしていくことが大切だと考えています。私自身は、 ソフトウェアは最終的にオープンソースになって完成する のではないかという原理主義的な考えを持っています。 そして、改善し続けることもソフトウェアにとっては非常に重要なことです。何かを作って終わりではなく、常に改善が必要です。作る過程では良くないとされる選択をすることもありますが、理由を持ってその選択ができたり、あとでちゃんと直すことができたりというのが、今最も重要なエンジニアの仕事なのではないかと思っています。 ー これからの時代、エンジニアとしての道を歩むために大切なことはなんですか? Matz: 自分で道を切り開けることではないでしょうか。エンジニアは1人ではできないことが多く、チームでプロダクトを開発することが多いです。例えばRubyも、はじめは私1人で始めましたが、今では皆で作っています。 そういう中でも自分のポジションを維持して存在感を出したり、エンジニアとして尊敬される人になることが大切です。尊重されている人は搾取されません。搾取するような人とは無縁の道を歩めるエンジニアになれるといいですね。 岸川: これは、先日 The Art of Senior Engineering というイベントでもお話させていただいたことですが、私はエンジニアの将来は楽観的に捉えています。エンジニアとしての能力がちゃんとあれば心配ありません。能力というのは、ソースコードを読み書きする以外にはないと思っているので、読む時間、書く時間に関わり続ける仕事につくことは大事なのではないでしょうか。 私はフルタイムで働くFOLIOの中で、誰よりもコードを書いているという自負があります。一般的に言う大切なことである、学習することや変化に対応することは、好きであればいくらでもできるので、楽しんでやってほしいです。 コードの読み書きする能力以外に判断する基準はないので、「他の人はどうだろう」などといった不要なことを気にする必要はありません。成長に安易な近道はない。何かを犠牲にしてやる必要はなく、楽しんで力抜きながら継続していきたいですね。 松田: 今の時代、 プログラマーほどラッキーで幸せな仕事はない と僕は思ってます。プログラミングをしてたらお金がもらえるなんて、すごくないですか!?仕事に困ることもないし、特別な訓練とか難しい勉強をしてなくてもやる気さえあればどこからだってはじめることもできます。僕らは、趣味がプログラミングです。プログラミングがお仕事だと思ってる人たちに、技術顧問という現場から少し離れた立場だからこそ、「プログラミングって楽しいんですよ」というのを気軽に伝えていきたいですね。 また、自分はRubyのコミュニティーにすごく助けてもらいました。OSSコミュニティーの存在はとても大きくてありがたいものなので、皆さんにもなるべく会社に閉じこもらずに、コミュニティーとうまく関わって楽しんでほしいです。 ー 最後に、就任にあたってメッセージをお願いします。 Matz: ZOZOグループが行う技術的なチャレンジを一緒やっていきたいです。Rubyという言語やその周辺のコミュニティの知見を集約して、 ZOZOグループが今後直面する技術的な課題を共に乗り越えていきたい と考えています。 松田: しばらく関わってみて、まだまだ解決しなくてはならない課題がたくさんある会社だと思っています。それはネガティブな意味ではなく、やったらやった分だけすごくやりがいのある課題ばかりです。一緒に楽しみながら、解決していきましょう。 岸川: 社員の方に声を掛けられたという小さなきっかけでしたが、チームの人とやってみると、それぞれからチームや会社、プロダクトを本気で良くしていこうという気持ちが伝わってきます。その気持ちに応えられるよう、私も頑張ります。 最後に 3名の技術顧問の皆様、ありがとうございました。ZOZOテクノロジーズでは、各エンジニア職種で募集を行っています。技術的なチャレンジを続けながら成長したい皆様、ぜひご応募ください。 求人に関する情報は こちら よりご覧ください。 【お問い合わせ】 広報に関するお問い合わせは こちら
アバター
こんにちは! 2019/4/18 - 20に福岡国際会議場で開催されたRubyKaigi 2019にZOZOテクノロジーズもRubyスポンサーとして協賛しました。 弊社からも8名のエンジニア( @takanamito , @rllllho , @katsuyan121 , @TrsNium , @AmatsukiKu , @takeWakaMaru666 , Takehiro Shiozaki , @sh_ngsw )が参加し、SREスペシャリストである瀬尾( @sonots )が登壇しました! 今年のRubyKaigiは、60を超える講演があり、参加者も1000名を超える大規模なカンファレンスでした。 この記事では、多くの講演の中でも特に気になった講演を弊社から参加したメンバーがそれぞれ報告します。 またsonotsの登壇内容と、スポンサーとしての活動について報告します。 RubyKaigi 2019登壇 RubyKaigi 2019講演紹介 Performance Optimization Techniques of MessagePack-Ruby Pattern matching - New feature in Ruby 2.7 Building Serverless Applications in Ruby with AWS Lambda A Type-level Ruby Interpreter for Testing and Understanding Ruby for NLP Crystalball: predicting test failures How to use OpenAPI3 for API developer GraphQL Migration: A Proper Use Case for Metaprogramming? スポンサー スポンサーブース スポンサートーク 最後に おまけ RubyKaigi 2019登壇 今年のRubyKaigiではZOZOテクノロジーズの瀬尾( sonots )がメルペイ社の hatappi 氏と共同でRed Chainer and Cumo: Practical Deep Learning in Rubyというタイトルで登壇しました。 スライドは こちら です。 Rubyで深層学習を使えるようにするプロジェクトである Red Chainer と、Rubyにおける数値演算ライブラリである Numo/NArray のGPU対応を行った Cumo ライブラリの紹介および去年の発表からの進捗について解説しました。 hatappi氏は、Rubyにおける深層学習(DNN)ライブラリ周辺の現況の解説と、なぜRed Chainerを作り始めたのか動機について説明しました。現在のRed Chainerが抱えている課題として「速度」、「 他言語のDNNフレームワークとの連携」の2つをあげました。 速度の面はCumoが、連携の面はONNX(Open Neural Network Exchange Format)を仲介することによって解決できると説明しました。 hatappi氏は後者の解決を目的として新しく onnx-red-chainer というgemを作成したと紹介しました。 Preferred Networks, Inc.(PFN)が開発している Chainer (Python)で記述されたモデルを onnx-chainer でONNXモデルに変換し、そのONNXモデルからonnx-red-chainerでRed Chainer(Ruby)のモデルコードを自動生成できます。 sonotsは去年からの進捗として、CumoのRed Chainer組み込みと、CumoのConvolutional Neural Networks(CNN)対応について紹介しました。 Red Chainerによる正式なCumo対応によって、Numo(CPU)を使うのかCumo(GPU)を使うのか動的に選択できるAPIが増え、ユーザにとって格段に使いやすくなりました。 また、cuDNNライブラリを使うことによって、高速なCNNの学習に対応したと発表しました。昨今のDNN隆盛の走りとなったCNNの対応は「実用的な」DNNフレームワークとして必須の機能であり、そのCNNの1つであるResNet-18の学習が以前は23日かかっていたものが17時間と現実的な時間で終わるようになったと発表しました。「Ruby 3は3倍速になると言われていますが、Cumoを使うとRed Chainerの学習が32倍速になります」と成果を述べました。 最後にsonotsは自身が以前に一年半ほどPFNに出向して開発していた ChainerX について紹介しました。ChainerXによりPythonのオーバーヘッドが減り高速化し、新しいデバイスのサポートも容易になり、Python処理系のないIoTデバイスのような環境にもデプロイできるようになります。この ChainerXへのRubyバインディング開発 を行うのも1つのプロジェクトとして面白いのではないでしょうかと提案して締めくくりました。 RubyKaigi 2019講演紹介 Performance Optimization Techniques of MessagePack-Ruby Performance Optimization Techniques of MessagePack-Ruby - RubyKaigi 2019 from Sadayuki Furuhashi @katsuyan121 です。私からは @frsyuki 氏のMessagePackのRuby実装に関するお話を紹介します。 MessagePackは、JSONと互換性のあるバイナリシリアライズフォーマットです。MessagePackは50を超える言語をサポートしており、多数のプロジェクトに使われています。 公式ページ で対応言語の一覧やMessagePackを利用しているサービスを確認できます。 今回はそのうちのMessagePackのRuby実装についてのお話でした。 最初にRubyオブジェクトとMessagePackとのシリアライズ・デシリアライズがどのような仕組みで動作しているかの解説がありました。 仕組みはすごく単純で、すごくわかりやすい図で解説されていたのでぜひ資料をご覧ください。 次にシリアライズ・デシリアライズを高速化する仕組みについて紹介していました。 1つ目にZero-copy writeについて紹介がありました。 長い文字列に関してはシリアライズ時に rb_str_dup を利用し copy-on-write することで高速化をしているそうです。 しかし、 SHARABLE_SUBSTRING_P() が true の時にしか使えないという問題についても言及していました。 2つ目にReserved memory poolについての紹介がありました。 MessagePackで使う領域を予め確保し、利用時は確保した領域を使うことでmemoryの割当時間を短縮しているとのことでした。 ベンチマークではJSONのシリアライズ・デシリアライズよりも圧倒的にMessagePackのほうが早いことを示していました。 また最適化がどのように効いているかもベンチマークにあらわれていて感動しました。 最後にRuby以外の言語でJavaとC#実装のMessagePackについての紹介がありました。それぞれの言語で行われている最適化手法が紹介されており、すごく興味深い内容でした。 Q&AではRubyコミッタとの白熱した議論がみられ大変深い話を聞くことができました。ぜひスライドだけではなくYouTubeの動画が公開されたらご覧になることをおすすめします。 Pattern matching - New feature in Ruby 2.7 @rllllho です。私からは @k_tsj さんによるRuby 2.7に実験的に導入されたパターンマッチングについて構文の説明と設計方針について紹介します。 スライドは こちら です。 Ruby 2.7ではパターンマッチングはcase/whenに複数の変数代入できるものという扱いで取り入れられています。 パターンマッチングを使用する際は case in を用います。 基本的なパターンマッチングの記法は下記の様になります。 elseがない場合は、NoMatchingPatternErrorがraiseされます。 case in パターン in パターン else # どのパターンにも当てはまらない場合 end Rubyで頻繁に使用するArrayとHashにもパターンマッチングが使用できます。 またパターンに変数を使用するとマッチした値を変数に代入して使用できます。 # Array case [ 0 , [ 1 , 2 , 3 ]] in [a] # ここには当てはまらない in [ 0 , [a, 2 , b]] p a #=> 1 p b #=> 3 end # Hash case { a : 0 , b : 1 } in { a : 0 , x : 1 } # ここには当てはまらない in { a : 0 , b : var} p var #=> 1 end Hashのパターンマッチが使えると、パースしたJSONを簡単に扱える様になります。 # JSON { " name " : " Alice " , " age " : 30 , " children " : [ { " name " : " Bob " , " age " : 2 } ] } # パターンマッチングを使う場合 case JSON .parse(json, symbolized_names : true ) in { name : " Alice " , children : [{ name : " Bob " , age : age}]} p age #=> 2 end # パターンマッチングを使わない場合 person = JSON .parse(json, symbolized_names : true ) if person[ :name ] == " Alice " children = person[ :children ] if children.length == 1 && children[ 0 ][ :name ] == " Bob " p children[ 0 ][ :age ] #=> 2 end end 現在もネストとしたHashを扱うことができるdigという記法がありますが、パターンマッチングを使うことでより柔軟にネストしたHashを扱えるようになりそうです。 APIレスポンスのJSONを扱う時などに活躍しそうです。 パターンマッチングを設計する上で、以下の2つを意識しているとのことです。 互換性を保つ Rubyらしくあること 互換性を保つために、新しい予約語は導入しないと決めていたそうです。 case文でパターンマッチングを導入する際に、whenの代わりに何を使うかについては下記の条件を満たす必要があります。 語彙として自然であること 文法としてなりたつ。式の先頭にくることができない 最初はこれらを満たす様な予約語は思いつかず記号を駆使してなんとかしようと考えられていたそうなのですが、ある日inを思いついたそうです。 inという語彙はすでにfor文で使われており、上記の条件を満たすということで case in が採用されました。 まだ仕様は確定ではなく議論段階だそうですが、パターンマッチングが導入されることでさらにRubyを楽しく書くことができますね! スライドの終盤には、今後のパターンマッチングの機能提案などもありパターンマッチングのこれからも楽しみです。とてもワクワクする発表でした! Building Serverless Applications in Ruby with AWS Lambda スライドは こちら です。 Takehiro Shiozaki です。 昨年のre:Inventで発表されたように、aws Lambdaの上でRubyが動くようになりました。awsのAlex WoodさんによるRuby on Lambdaでアプリケーションを作る時に使うと便利なツールやライブラリの紹介がありました。AlexさんはLambdaでRubyを動作させるためのランタイムの開発者であり、またaws-sdk gemのメンテナでもあります。 発表の前半ではサーバーレスとは何かという事に関する概要の説明がありました。 サーバーレスとは単にサーバー(EC2インスタンスなど)をなくすという意味ではないとのことです。 アプリケーションコードとテストコード以外の煩雑な部分をLambdaやその周辺ツールに任せることで、開発者が本質に集中できるような思想とのことです。 ユーザーの認証・スケーリング・OSのパッチなどの問題などが前述した煩雑な部分の一例です。 後半ではより具体的にどんなツールやgemを使うと便利なのかを紹介されていました。 まず紹介されていたツールは aws sam cli です。 これそのものはPythonで書かれたツールですが、各種言語でサーバレスアプリケーションを作る時の便利ツールがまとまっており、Rubyにも対応しています。ローカル環境でLambda関数のテストをしたり、Lambda関数のデプロイを行うための機能などがあります。次に紹介されていたgemはaws-sdk-rubyです。 github.com awsでRubyを利用しているのならばほぼ間違いなく利用しているであろうgemです。 awsの各種サービスに対するアクセスをラップし、Rubyっぽいインタフェースを提供してくれます。 さらに aws-record というgemも紹介されていました。 このgemはDynamoDBのO/Rマッパーで、ActiveRecordのようなAPIによってDynamoDBに対してアクセスを行うことが出来ます。 また、スキーママイグレーション機能も持ち合わせています。LambdaとRDBとの相性は悪いので、サーバーレスアプリケーションではDynamoDBを利用することが多いです。 そのため、このgemもLambda on Rubyのアプリケーションを作る時よく利用することになりそうです。 我々は今回のRubyKaigiのブース企画のためにRuby on Lambdaを利用したサーバーレスアプリケーションを作成しました。 その時にはこちらで紹介されていたgemのいくつかを利用しました。 アプリケーション作成にあたっていくつかのハマりポイントなどもありましたので、後日TECH BLOGにもまとめたいと思います。 A Type-level Ruby Interpreter for Testing and Understanding A Type-level Ruby Interpreter for Testing and Understanding from mametter @AmatsukiKu です。 MatzさんのKeynoteでもあったように、Ruby 3に向けて静的解析の取り組みが進んでおり、今回のRubyKaigi中にも関連する講演がいくつかありました。特に気になった @mametter さんによるType Profilerについての講演を中心に紹介したいと思います。 Type Profilerは、 Steep や Sorbet といった技術と異なり、既存のRubyコードにSignatureなどの追加の記述を必要とせずに、静的解析を行える技術です。Type Profilerでは、これを実現するために、Rubyコードを型レベルで実行します。例えば、次のようなコードがあったとします。 def foo (n) n.to_s end foo( 42 ) 通常のRubyの実行であれば、上記のコードの処理はfooの呼び出しは引数の 42 に対し返り値が ’42’ になります。それがType Profilerで実行した場合には、引数は 42 の代わりに Integer が渡され、返り値も String となります。このように、Type Profilerは型レベルで実行していくことにより型情報を集めて、型エラーを検知します。 しかし、この方式には、いくつか問題もあります。まず、呼び出しのためのテストが必要となります。型レベルで実行するという形で解析するため、必然的に呼ばれていないメソッドは解析されないことになるからです。 次に、型レベルでは評価できない条件文が存在するという問題があります。例えば、次のようなコードがあったとします。 def foo (n) if n < 10 n else ‘error’ end end foo( 42 ) この場合、Type Profilerは n が Integer という情報しか持っていないため、 n < 10 を評価できません。そのため、Type Profilerでは、このような条件文に対しては条件に関係なく分岐全てを実行するというアプローチを取っています。 しかし、今度は、次の2つの問題が起こります。 条件が増えるほど状態の組み合わせが指数関数的に増えていくという状態爆発が発生する 本来、存在し得ない状態の組み合わせが発生する まず、前者については、同じ状態をマージするというアプローチである程度緩和できます。一方、後者については、そういった状態が発生するコードを書かない以外に現状の解決方法はないそうです。 Type Profilerは解析によって型エラーを出力するだけでなく、Type Signatureを出力することも予定しています。これは、SteepやSorbetといった別の静的解析ツールに使うもののプロトタイプという位置付けであり、実際に使う場合には手直ししたものを利用する想定のようです。 Type Profilerは既存のRubyコードのみから解析できるので、他の静的解析の技術に比べ、テストを必要とする点を考慮しても既存のプロダクトへ導入しやすいと感じました。 Type ProfilerがSignatureを生成し、それを元にしたSignatureで他の型検査器が検査を行うといったエコシステムの動向は今後も気になるところです。Signatureのフォーマットや共有方法については、 松本宗太郎さんの講演 でも紹介していたので、合わせてチェックすることをお勧めします。 Ruby for NLP @TrsNium です。 @youchan さんのRuby for NLPについて紹介します。 (スライド: http://youchan.org/RubyKaigi2019/ ) 今、ディープラーニングは画像処理や自然言語処理の分野などで注目されています。 しかし、多くの機械学習や深層学習ライブラリはPythonで実装・利用されており、Rubyでの実装・利用実績は少ないです。 このセッションでは機械学習タスクの1つである自然言語処理をRubyを用い紹介しています。要点を纏めつつ、実際にツールを使ってみた感想を述べたいと思います。 セッションは2つのパートに分かれており前半は基本的な自然言語処理の方法についての解説で、後半はRubyを使った自然言語処理の処理方法についての解説でした。 前半部では自然言語処理に使用する言語モデルには大きく2つの系統があり、確率的アプローチとニューラルネットワークがあるということが紹介されました。 日本語の言語処理で多く利用されている形態素解析ライブラリMeCabは確率的なアプローチを元にできています。一方で、ニューラルネットワークを用いたものは、機械翻訳やチャットボットなどで利用されています。 後半部のRubyでの自然言語処理部分では言語処理ライブラリの紹介やディープラーニングを用いいた実際のデモが紹介されていました。 言語処理ライブラリでは、StanfordNLPやRuby製のtreatやjuman_knpが紹介されてました。またディープラーニングフレームワークでは「Red-Chainer」の紹介がされ、デモでも使用されていました。デモでは語句を与えると、次の語句を予測するようなCLIツールを作成するようなものでした。 実際にRed-Chainerを用いWord2Vector(CBoW)の実装を行ってみました。 Word2Vectorでは単語をn次元のベクトルで表現するため、単語通しの演算ができます。 github.com Red-Chainerを実際に使ってみましたがRNNやCNNコンポーネントなどが足りず、まだまだコントリビュートできる場面があると思いました。Rubyを用いた機械学習や深層学習はまだ発展途上だと思うので、一緒に盛り上げていきたいです。 Crystalball: predicting test failures スライドは こちら です。 @takanamito です。 @p0deje 氏の Crystalball: predicting test failures について紹介します。 長くなりがちなテストの実行時間に対して変更したコードから関連するテストコードを絞り込んで実行するというアプローチのgem crystalball を紹介されていました。 セッションの中心はcrystalballの仕組みについてで、まず最初にcrystalballを構成する3つのフローについて説明がありました。 MapGenerator → Predictor → Runner という大きく3つのフローを経て実現されており、実際に仕組みを聞いてみるとRubyならではのアプローチで実装されていることがわかりました。 まず MapGenerator はspecのコードとアプリケーションが実装されているファイルとの関連を示す tmp/crystalball_data.yml を生成する役割を持っているとのことでした。 CRYSTALBALL=true bundle exec rspec . を叩き、1度全てのspecを実行することでこのファイルが生成されるようです。 Crystalball Manual に丁寧なドキュメントが用意されていてすごい。 この関連を取得するロジックの実装がかなり愚直で TracePoint , parser , モンキーパッチでひたすらspecに関連するクラスや、FactoryBotの定義などのsupport系ファイルまで関連を見に行く仕組みになっているとのことでした。 また Predictor ではgitから差分のあるファイルを読み取り MapGenerator で作ったテストとの関連データを使って実行するspecを絞り込み Runner で実際にspecを実行するようです。 実際に手元のRailsアプリケーションで使ってみたところ、修正したモデルのspecに限定して実行されている様子が出力されていました。 $ bundle exec crystalball I , [ 2019 - 04 -25T16: 42 : 28.037405 #84894] INFO -- : Crystalball starts to glow... I , [ 2019 - 04 -25T16: 42 : 28.117924 #84894] INFO -- : Starting RSpec. Run options : include { :ids =>{ " ./spec/models/hogehoge_spec.rb " =>[ " 1:4:1 " , " 1:5:1 " , " 1:1:3:1 " , " 1:1:4:1 " , " 1:1:5:1 " , " 1:1:6:1 " , " 1:1:7:1 " , " 1:1:1:1 " , " 1:2:2:1 " , " 1:2:3:1 " , " 1:3:1:1 " , " 1:3:2:1 " , " 1:3:3:1 " , " 1:2:1:1 " , " 1:1:2:1 " ]}} ....... FFF ..... いま私が関わっているプロダクトではまだspecの実行時間は課題になっていませんが、今後課題に上がってきたときは有力な選択肢になりそうです。 またgitのpre-commit hookなどを利用してcommitするタイミングで関連するテストだけを実行する様な使い方もできそうなので作業効率UPのために導入も検討しています。 How to use OpenAPI3 for API developer スライドは こちら です。 @sh_ngsw です。 Ota(@ota42y) さんのセッションについてご紹介します。 このセッションは、特にMicroserviceやBackends for frontendsの採用を検討(あるいはすでに採用)されている方にオススメです。 主に OpenAPI 3.0 と committee というgemについての発表でしたが、 これからのAPI開発の手法として大変参考になりました。 初めにOpenAPIの定義・使い方・アーキテクチャについて説明があり、続いてcommitteeによるvalidationの実装方法とOpenAPI 2.0から3.0への移行についての発表がありました。 OpenAPIはRESTful APIをYAML/JSONファイルで記述するフォーマットであり、OpenAPI 3.0はその最新のメジャーバージョンです。 OpenAPIは 周辺ツール との組み合わせによって様々な機能を利用できます。 committeeはその中でもAPIのrequest/responseのvalidationが実装できるgemです。例えば、URLのパス、requestの必須parameter、parameterのtypeなどが定義できます。これによって、APIの実装ミスや実装と定義の乖離をチェックできます。 昨今のサービス開発ではPaaS・SaaS連携やマルチデバイス対応を求められる機会が多々あります。そうした情勢ではOpenAPIによるAPI定義やAPIのvalidationの実装はますます増えていきそうです。 一方でOpenAPIの周辺ツールではまだ3.0に対応していない場合があります。それゆえにツール次第では安易に3.0へ移行できない可能性があるため要注意です(むしろPRチャンスでもあります!)。 たとえば、テストコードからAPI定義を生成できる rswag がそうです。 ちなみに弊社のエンジニアも openapi2ruby というOpenAPI用のgemを開発しています。このgemでは、OpenAPIのYAMLファイルからActiveModel::Serializerを継承するクラスを生成できます。 このserializerの自動生成によって、定義と実際のresponseの乖離の問題を解決できます。こちらはすでにOpenAPI 3.0に対応済みです! 今後もOpenAPIを中心としたAPI開発の事例が増えていきそうなので、動向を追っていきたいですね。 なお、過去のRubyKaigiでも同じAPI開発というテーマで、 大仲さん(@onk) が 『API Development in 2017』 というタイトルで発表されていました。 こちらの発表ではRESTful APIとRailsでのAPI開発の歴史がまとめられています。 合わせて読むとOpenAPIやcommitteeの立ち位置が俯瞰的に理解できるのでオススメです。 参考リンク: 『 RubyKaigi 2017 でどんな発表をしたか 』 『 スキーマファースト開発のススメ 』 GraphQL Migration: A Proper Use Case for Metaprogramming? スライドは こちら です。 @takeWakaMaru666 です。shawneegaoさんのGraphQLに関するセッションをご紹介します。 APIサーバーのGraphQL層をメタプログラミングを使って実装することで、ボイラープレートを排除しDX(Developer Experience)を向上させるというものでした。 shawneegaoさんはコントローラーの数が200を超えるRailsで書かれた大きなRestful APIをGraphQLでリプレイスする際にメタプログラミングを使ったそうです。 コントローラーを1つずつGraphQLのフィールドとして再実装する作業は非常に長い時間を要するものであり、また大量のボイラープレートがDXを損なうと判断したためメタプログラミングを使って実装することを考えたそうです。 具体的にはRailsのモデル層のクラスからGraphQLのタイプクラスを生成する部分と、生成したそれらのタイプをルートタイプ内でフィールドとして定義する部分をメタプログラミングでカバーしていました。 またメタプログラミングで実装しやすくするためにモデル層のクラスのインタフェースを統一していました。 このメタプログラミングを使った方法のメリットはボイラープレートが減ることだと思います。 デメリットとしては、Railsのモデル層にGraphQL依存のコードが書かれてしまうこと、可読性が失われること、デバッグが難しくなることなどが考えられます。 ボイラープレートの排除は魅力的ですが、可読性が失われたりと付随するデメリットがあまり無視できないので私が同じ立場であればやらない選択をすると思います。 ただしGraphQLのタイプを定義する作業の単調な部分を自動化するという部分には大賛成で、スクリプトを書いて自動生成できるところは自動生成するというのが落とし所な気がしました。 スポンサー 今年は、Rubyスポンサーとして、スポンサーブースの出展とスポンサートークの登壇をさせていただきました。 スポンサーブース 昨年に引き続き、今回もスポンサーブースを出展しました。 今回のブースでは、ZOZOSUITの展示に加え、以前本ブログでも紹介した ファッションチェックアプリ の展示を行いました。 また、今回の展示に際して、ファッションチェックアプリにRuby on AWS Lambdaを用いて実装したランキングの機能を加えました。 Ruby on AWS Lambdaは昨年11月から提供されたばかりということもあり、ブースを訪れた方々にも興味を持っていただけたようでした。 今回追加した機能の詳しい実装についてはまた別の記事で紹介するので、公開をお待ちください。 スポンサートーク スポンサートークでは、弊社の企業・事業紹介や、弊社がなぜRubyKaigiにスポンサーとして参加しているかについて話させていただきました。 最後に カンファレンス参加に関わる渡航費・宿泊費などは全て会社に負担してもらいました。自分から希望すれば参加する機会をいただけて本当に感謝です。ZOZOテクノロジーズでは、Rubyエンジニアを大募集中です! ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com おまけ RubyKaigiを楽しんでいる社員の様子です。 5Fにブースを出していました! ファッションチェックアプリのデバッグ中 Matzさんと写真をとらせてもらいました! 初日のOfficial Partyがまさかの商店街貸切! 楽しかったです!来年の松本も楽しみです!
アバター
こんにちは、広報の秋山です。 先日、青山オフィスに技術顧問であるまつもとゆきひろ(以下、Matz)氏が来社し、新入社員に向けた講演とパネルディスカッションを行いました。 まつもと ゆきひろ氏 (@yukihiro_matz) 1965年生まれ。筑波大学第三学群情報学類卒業。プログラミング言語Rubyの生みの親。株式会社ネットワーク応用通信研究所フェロー、一般財団法人Rubyアソシエーション理事長、ZOZOテクノロジーズをはじめとした複数社の技術顧問、Herokuチーフアーキテクトなどを兼任。松江市名誉市民。通称 Matz(マッツ)。 若手エンジニアの生存戦略とは 講演では、「若手エンジニアの生存戦略」というテーマで、「わらしべ長者」を例に取り上げました。 「わらしべ長者」とは 日本おとぎ話の一つ。お金持ちになりたいと願うある若者が、お寺で観音様に「どうか、お金持ちになれますように」とお願い。「ここを出て、最初につかんだ物が、お前を金持ちにしてくれるだろう」という観音様の言葉を信じ、お寺を出た。お寺を出て、最初につかんだものが「わらしべ」。この「わらしべ」を価値あるものに交換し続け、最終的に屋敷を手に入れ、お金持ち「長者」になったことから、この若者を「わらしべ長者」という。 「わらしべ長者」の話を通じて、以下が伝えられました。 「価値は作ることができる」 簡単に入手できるものから価値は作ることができます。自分が価値を生成することや、他人に対して自分のもつ価値をプレゼンすること、将来の期待度に価値を見いだすことも大切です。価値は手を加えることで増加させることができます。ぜひ若いうちに価値の実態を考えてみてください。 「価値は一様ではない」 現代の社会では均一な価値を見いだしがちですが、近所で買う缶ジュースと富士山で買う缶ジュースの値段が違うように、実際は場所や状況次第で価値は変化します。価値が高まる場所はどこかを常に考えることで、新たな価値に気づく事ができます。 「価値は高める事ができる」 知識をインプットする、経験を積む、スキルを伸ばすことにより、価値は高めることができます。現在の価値を考えるだけでなく、価値を高めるためにどうするべきかを考えて仕事に取り組んでください。 最後に、Matzさんから新入社員に向けてメッセージをいただきました。 一見価値が低いと感じるものを、価値が高いと感じる人に売り込むことで次の段階に進むことができます。これは人生において有効な戦略です。客観的な視点で自分の価値を高めていくことができる人、価値を作りだすことができる人は強いです。それは、社会人における一種の「生存戦略」でもあります。ぜひ皆さんも自分のもつリソース(知識・スキル・興味・態度など自分自身が持つもの)を棚おろしして、異なる視点を持つ人に価値高くプレゼンテーションしてみてください。今後会社組織の一員として、皆さんが自分自身の価値を高め、成長をしていくことを期待しています。 講演を聞いて、新入社員からMatzさんへ沢山の質問が投げかけられました。いくつかご紹介します。 ー 社会人になって「わらしべ長者」ような経験をしたことはありますか 当時、200名ほど新卒がいる大きな会社に就職したのですが、そのなかでもコンピューターサイエンスのスキルが高い方だったので、そこを最大限生かし、ステップアップしていきました。会社の求めているスキルを自分がさらに高め、活用することで高く評価してもらうことができました。 ー もし自分たちの年代であったら何を作りたいと思いますか プログラミング言語に対する興味がまだ強いので、またプログラミング言語を作ります。自分の考えを表現する方法をデザインすることは、魅力的であると感じています。 ー なぜ言語にRubyという名前をつけたのですか? 既に存在していたPerlがスペルは違うが宝石の名前なので、同じ宝石からつけたいと思っていました。最終的にはCoralかRubyで悩んだのですが、一番文字数も少ないしきれいだしRubyにしました(笑)。ちなみに、後で気付いたのですが宝石のパールは6月の誕生石でルビーは7月の誕生石、Perlの次だしちょうど良い結果になりました。 パネルディスカッションの様子 講演後、Matzさんと弊社社員によるパネルディスカッションを実施しました。 【パネラー】 まつもとゆきひろ氏 ZOZOテクノロジーズ 技術顧問 瀬尾直利 ZOZOテクノロジーズ 開発部 ML & SRE スペシャリスト    【ファシリテーター】 今村雅幸 ZOZOテクノロジーズ 執行役員 VP of Engineering 開発部部長 ー今振り返って、若いときにやっておいて良かったと思うことは何ですか? Matz: コンピューターサイエンスとプログラミングの勉強です。当時はインターネットもあまり普及していなかった事情もあるのですが、世の中にあるフリーソフトのソースコードを読んで勉強していたのは、やっておいて良かった勉強法だったと思います。 瀬尾: コンピューターサイエンスの基礎を学ぶことです。基礎を学んでおくことで、普段触れていない範囲の技術・知識が必要になった場合、それらの基礎があることで理解がしやすくなり、キャッチアップしやすくなる場面も多くなります。 ーお二人が働き始めた頃と今では技術のトレンドが変化してきていると思いますが、ご自身の業務範囲ではどんな変化がありましたか?そして、これからどう変わっていくと思いますか? Matz: 一番の変化はインターネットの存在です。昔はデスクトップアプリを作っていましたが、今はブラウザの中で全て完結可能なブラウザアプリが主流になりました。一方、言語やOSなど本質的にはあまり変わらない領域も存在します。そういった意味では自身の専門領域は大きく変わりませんでした。直近5〜10年で変わってこなかった部分は引き続き変わらないと思います。Webアプリケーションはまた違うアーキテクチャに変わってくかもしれませんね。 瀬尾: インフラの文脈だと大きく変わりました。昔は手作業でオペレーションをするのが当たり前の世界でしたが、今はクラウド上でインフラの設定ができるようになり、自動化も進んだことで手作業を主としていたエンジニアは少なくなりました。プログラムを書いて自動化する範囲が広がってきています。今後も変化は続くと思うので、これからも情報にアンテナをはり、時代の流れを見越して自身も行動をしていきたいです。 ー学生の頃の個人開発と、社会人としての仕事としての開発、どう違うと思いますか?何か気をつけるべきことありますか? Matz: 職業的ソフトウエア開発者になって一番違うと感じたのは、締め切りを強く意識するようになったことです。受託開発も多かったので、期日内に開発をするためのスケジュール管理を徹底しなければなりません。数多くの受託開発を通じて、理想のソフトウエア開発はエンジニアだけでなく関わるプロジェクトメンバー全員が同じ方向を向いていることが大切であると感じました。それぞれの特性や考えを尊重しつつ、期日に業務を仕上げるためにメンバーと協力して取り組む必要があります。 瀬尾: 一番の違いはチームであるということです。プログラミングという観点から言えば、周囲からレビューをもらうことができ、より良いものを作り上げていくことができます。個人ではなくチームなので、コミュニケーションをしっかりとって報告・連絡・相談を徹底すること、最終的なアウトプットのソースコードだけでなく、そこに至るまでの背景や動機なども記録として残し、連携を取りやすくすることも大切です。 ー他社とZOZOテクノロジーズの違いって何ですか? Matz: ファッションという軸を持ち、テクノロジーを駆使しながら社員の皆さんが同じ方向性を向いて自社サービス開発に努めているという印象です。個性豊かな社員も多く、前向きに業務に取り組まれていると感じます。 瀬尾: 良い意味で一般的なインターネット企業との違いは無い状態になってきたと思います。それは、エンジニアが働く上で、社内の制度などが良い環境に整ってきたという点です。現在でも、社内の変化は速く、働きやすい環境が整備されてきていると思います。また、人を喜ばせることが好きな社員が多く、真剣に仕事に打ち込みながらも、楽しく仕事ができています。 最後に 講演とパネルディスカッションを通じ、多くのインプットをしながら自分を見つめ直したことで、新たな気づきを得られたのではないかと思います。今後も新入社員の皆さん1人1人の価値を最大限に生かし、楽しく働きながら企業理念の達成に向けて社員一丸となって取り組んでいきたいと思います。新入社員の皆様の今後の活躍を期待しています! 採用情報 ZOZOテクノロジーズでは現在、各職種で正社員を募集しています。 気になる方は 求人ページ をご覧ください。 【お問い合わせ】 広報に関するお問い合わせは こちら
アバター
こんにちは! App EngineのスタンダードランタイムにRubyが追加されて喜んでいるバックエンドエンジニアの りほやん(高木) と、オレンジ色のチンアナゴは実は ニシキアナゴ という別種だったことに驚きを禁じ得ない塩ちゃん(塩崎)です。 4/9, 10, 11の期間で開催された Google Cloud Next '19 にZOZOテクノロジーズから高木と塩崎が参加しました! GCPの新しい機能や活用についての事例が多く紹介されました。 その中でも2人がカンファレンスで気になった技術を紹介します。 Cloud Run Cloud Runとは Cloud Runの特徴 実際に使ってみる 1. アプリケーションの準備 2. コンテナイメージをビルドする 3. Cloud Runにサービスを作成する App Engineとの違い サービスの比較 各サービスの概要 App Engine Cloud Run ユーザーが考えないといけないこと App Engine Cloud Run チーム App Engine Cloud Run サービスの使い分け Cloud Runのまとめ BigQuery Storage API 従来のAPI BigQuery Storage APIの特徴 各言語のサポート 欠点 BigQuery Storage APIのまとめ Cloud Data Fusion パイプラインの作成 データのプレビュー機能 コンポーネント コネクター 変換関数 エラー検知 Cloud Data Fusionのまとめ 興味深かったセッション Serverless関連 Migrating from a Monolith to Microservices Serverless on Google Cloud CI/CD in a Multi-Environment, Serverless World Design Pattern Evolutions for a Scalable Future Building a WorldWeb App with Ruby, App Engine, Serverless Container, and Spanner BigQuery系の興味深かったセッション Plaid's Journey to Global Large-Scale Real-Time Analytics With Google BigQuery Kohls and Datametica Share EDW Workload Migration Tips Data Warehousing With BigQuery: Best Practices Best Practices for Storage Classes, Reliability, Performance, and Scalability まとめ 最後に おまけ Cloud Run Google Cloud Next '19ではServerlessについての発表が多くありました。 その中で新しく発表されたCloud Runをピックアップして、どのようなサービスなのか他のサービスとどう違うのかについてまとめてみます! Cloud Runとは Cloud Run はHTTPリクエストを介して呼び出すことができるステートレスコンテナを実行できるようにするマネージドコンピューティングプラットフォームです。 複雑な設定なくサーバーレスなコンテナを作成できます。 サーバーレスな環境でコンテナを立ち上げたい!でも最初からKubernetesを構築するのはハードルが高い!という人にぴったりそうなサービスです。 4/12日現在はBeta版で使用できます。 Cloud Runの特徴 Cloud Runの特徴として下記の点が挙げられます。 コンテナイメージをあげることによって数十秒でHTTPで叩けるURLが発行される 裏側で Knative を使用している Cloud RunとCloud Run on GKEがある 素のCloud RunはKubernetesの設定をしなくてもよい Cloud Run on GKEを使うと自分でKubernetesを構築できる Cloud RunからCloud Run on GKE、またはCloud Run on GKEからCloud Runに切り替えられる 使用したリソースに対してのみ課金される 実際に使ってみる 簡単なチュートリアルに沿ってRubyのSinatraアプリケーションをCloud Runで立ち上げてみます。 チュートリアルは こちら から試すことができます。 今回はコンテナをビルドするところから始めてみます。 GCP上にプロジェクトがあり、Cloud SDKがインストールされた環境であることを前提とします。 1. アプリケーションの準備 アプリケーションとDockerfileを作成します。 アプリケーションのサンプルコードはチュートリアルにも掲載されていますが、下記のリポジトリにも上がっており、そのまま使うこともできます。 helloworld-ruby 2. コンテナイメージをビルドする アプリケーションとDockerfileが用意できたらCloud Buildを使ってコンテナイメージをビルドします。 % gcloud builds submit --tag gcr.io/[PROJECT-ID]/helloworld これでコンテナイメージがGCPにアップロードされました。 3. Cloud Runにサービスを作成する Cloud Run上にアプリケーションをのせるサービスを作成します。 今回はわかりやすくするためGUI上からサービスを作成しますが、CLIからサービスの作成&デプロイを行うこともできます。 3-1. 『CREATE SERVICE』をクリック 3-2. コンテナイメージの選択で先ほどビルドしたコンテナイメージを選択します。 latestタグがついたイメージを使います。 Cloud Run on GKEの場合は、事前にCloud Runを有効にしたクラスタを作成するとここでクラスタの選択が行えます。 3-3. サービス名を入力しロケーションを選択します。 現在は『us-central1』ロケーションしか選択できません。 3-4. 今回はpublicアクセスをできるようにするため、認証の『未認証の呼び出しを許可』のチェックボックスをオンにします。 これだけです! 『作成』ボタンを押すとサービスが作成されます。 ちなみにオプションの選択ができますが下記のようになっています。 数十秒ほどでサービス作成は完了し、URLが発行されます。 アクセスすると無事にHello Worldが表示され、アプリケーションが立ち上がっていると確認できました。 サービスの詳細画面からはログの確認や、裏で構築されているKubernetesのYAMLファイルを見ることができます。 App Engineとの違い Cloud Runに似たサービスとしてApp Engineがあります。 App Engine, Cloud Runがどのように違うのかを比べてみました。 下記のセッションがApp Engine, Cloud RunだけでなくCloud Function, Kubernetes Engine, Compute Engineとも比較しそれぞれのサービスをわかりやすく説明してくれているので、詳しく知りたい方はこちらをご覧ください。 サービスの比較 各サービスの概要・ユーザーが考えないといけないこと・チームの3つの軸で比べてみたいと思います。 各サービスの概要 App Engine 開発者がアプリケーション開発に集中できるようにすることが目的のサービス。 サポートされているランタイムが決まっています。 サポートされていない場合でもFlexible Environmentを用いればアプリケーションを立ち上げることができます。 コンテナは基本的に不要ですが、コンテナを使用して構築することもできます。 Cloud Run HTTPポートの空いたコンテナイメージがあればアプリケーションを作成できるサービスです。 よりフレキシブルに自分たちで制御したい場合は、Kubernetesを自分で用意することもできます。 コンテナを必ず使う必要があります。 ユーザーが考えないといけないこと App Engine GCPにはCloud Runに似たサービスとしてApp Engineがあります。 App Engine, Cloud Runのがどのように違うのかを比べてみました。 下記のセッションがわかりやすく説明してくれているので、詳しく知りたい方はこちらをご覧ください。 Cloud Run コンテナイメージ内のコード コンテナイメージのビルド チーム App Engine インフラ構築より、開発に注力したいチームが適しています。 Cloud Run 開発に注力しながらも、自分たちのチームでコンテナビルドツールやデプロイ設計も行いたいチームに適してます。 サービスの使い分け セッションでも話されていましたが、どのサービスを使うかはケースバイケースだと思います。 App Engineでサポートされているランタイムを使いたい、インフラを気にせず開発に注力したい場合はApp Engine、 開発に注力したいけどコンテナを使用したい、自分たちでコンテナをビルドしたりデプロイすることができる場合はCloud Runを使うのが良さそうです。 App EngineのFlexible Environmentでカスタムコンテナイメージを使用することもできますが、やはり本質的にはApp Engineはソースコードに注力したい人向けなのかなと思います。 コンテナを使用したい場合は、今後Cloud Runの方に寄っていくことになるのではないのかなと思います。 Cloud Runのまとめ 実際にCloud Runを使ってみて、5分ほどでスケーラブルなアプリケーションの作成・デプロイ・URLの発行までできてKubernetesの複雑さを意識することなく簡単にWebアプリケーションが立ち上がるのは開発体験としてよかったです。 最初はCloud Runを使って手早くアプリケーションを立ち上げて、必要になったタイミングでCloud Run on GKEに切り替えるという使い方ができれば、サービスの規模に合わせてインフラの選択肢が柔軟に変えられてとても良さそうだと思いました。 Beta版のため、制約も多く検証が必要そうですが、発表でもCloud Runの名前が多く登場していたのでこれからに期待したいです。 BigQuery Storage API BigQueryからデータを取り出すための新しいAPIです。 BigQueryのストレージ層に対してgRPCでクエリを投げることによって、従来のAPIと比較して高速かつリアルタイムにデータを取得できることが大きな特徴です。 BigQuery Storage API Overview 従来のAPI 従来のBigQueryからのデータ取り出しは主に以下の2つの方法で行われていました。 jobs.getQueryResult APIを利用したデータ取り出し この方法は取得したいデータをSQLを使って表現し、そのSQLの結果をHTTPで取得するものです。 REST APIを直に使ったことのある方は少ないと思いますが、BigQueryのweb UIやCloud SDK経由でほとんどの方が利用していると思います。 少量のデータを出力するときにはこの方法が適していますが、大量のデータを取り出す時にはスピードの遅さが目立つようになります。 extract jobを利用したデータ取り出し この方法はBigQueryに保存されているテーブルの内容をGoogleCloudStorage(GCS)にCSV、JSON、AVROなどの形式でバルク出力するものです。 getQueryResultと比較して大量のデータを出力できる一方で、バッチ処理でGCSに出力されるため最初の1行目を取得するまでの時間が長いという欠点があります。 Cloud SDK BigQuery Storage APIの特徴 Storage APIは上記2つのデータ取り出し方法とは異なる第3のデータ取り出し方法です。 BigQueryのストレージ層に対して直にgRPCでクエリを投げることによって上記2つのAPIの欠点を解消できます。 従来のAPIにはなかった素敵な機能がいくつもあるのでそれを紹介いたします。 Multiple Streams 同一セッション内で複数個のストリームを持たせることができ、それぞれのストリーム間でのデータは重複しないことが保証されています。 この機能は分散環境で複数のマシンからデータを読む時に特にパワーを発揮しそうですね。 Dynamic Sharding Streamを複数個作った時に各Streamに対するデータの割り当てをBigQuery側が自動的に行なってくれます。 例えばデータ処理速度の違うクライアントがあった時には、処理の遅いクライアントが担当しているStreamへのEnqueueは自動的に減らされるそうです。 そのため、それぞれのクライアントが自分の処理したい速度でデータを処理すればよく、クライアント側に負荷分散のロジックを入れる必要がないそうです。 Column Projection データを取得する時に必要なカラムだけを取得できます。 それによって、テーブル全体を取得する場合と比較して高速にデータの読み取りを行うことができます。 Column Filtering シンプルな条件式(SQLのWHERE句)であればデータ取り出しのタイミングでフィルタリングできます。 こちらの機能もデータ転送量を抑えることでデータの読み取りの高速化に寄与しています。 Snapshot Consistency セッションを作った瞬間のテーブルのスナップショットが保持されるもので、そのスナップショットはセッションを通して有効です。 そのため、複数のクライアントがあったとしても同一セッションを使っていれば整合性のある読み取りを行うことができます。 また、スナップショットの日時はデフォルトではセッションが作られたタイミングですが、過去の日時にすることも可能なようです。 そのため、1時間前の状態のテーブルからデータ取得をするといったこともできます。 各言語のサポート 現時点ではJava、Python、Goのみでサポートされています。 現状は機能を提供したてなので、これらの言語のみに注力しているようです。 ですが、将来的には対応言語が増える可能性もあるそうです。 また、Apache BeamやそのマネージドサービスであるCloud DataflowからBigQueryIOコネクターを使うことでAPIを使用することもできます。 BigQuery Storage API - Libraries and Samples 欠点 さて、ここまででBigQuery Storage APIの良いところばかりを書いてきましたが、欠点もあります。 主に以下の2点が欠点と感じました。 対応言語の少なさ CloudSDKが対応している言語(Java, Python, NodeJS, Ruby, Go, .NET, PHP)の半分程度の言語でしか、BigQuery Storage APIを使うことができません。 この欠点については将来的に対応言語が増える可能性もあるようですので、これからに期待です。 Column Filteringの柔軟度 Column Filteringを利用することによってSQLのWHERE句に相当する絞り込みができますが、WHERE句ほどの柔軟度はないようです。 以下のPythonライブラリのドキュメントによると、1つの条件式しか使用できず、また条件式の引数の片側は定数にする必要があるそうです。 Python Client for BigQuery Storage API OKな例: int_field > 10 data_field = CAST('2014-9-27' as DATE) NGな例: int_field1 > 10 AND int_field2 > 20 int_field1 > int_field2 クエリの柔軟性という観点から見た場合は従来のAPIのほうが優れているので、自分たちのワークロードが従来のAPIの性能限界に達していない状況での拙速な移行は避けるべきかとも思います。 BigQuery Storage APIのまとめ BigQuery Storage APIという新しいAPIの登場によってBigQueryの利用シーンが増えそうな予感がします。 このAPIは初期の段階からApache Beam/Cloud Dataflowサポートが行われています。 そのため、いままでBigQueryが苦手としてきた大量データのリアルタイムストリーミング処理用が主な利用用途と思います。 Cloud Data Fusion 2日目のkeynoteでも発表されていたフルマネージドなデータインテグレーションサービスです。 ETL/ELTデータパイプラインを作成でき、その実行はマネージされたインスタンスで実行されます。 このサービスは昨年にGoogleが買収したCDAPというサービスをベースにしているそうです。 CDAPは100% オープンソースなプロジェクトでオンプレ・クラウド両方のETLを作成できます。 パイプラインの作成 データ転送のためのパイプラインは以下の図に示すようにGUIから作成でき、またパイプラインの実行状況の確認もGUIから行えます。 作成したパイプラインは手動で実行させられますし、スケジューリング実行もできるようです。 さらにCloud Composerと連携し、Cloud ComposerからData Fusionのワークフローの実行を行うこともできるようになる予定です。 現時点ではETL処理はCloud Dataproc上で行われるそうですが、将来的にはCloud Dataflow上でも行うことができるようになるらしいです。 データのプレビュー機能 さらにETLワークフローを書く前にデータソース中身をプレビューし、その場で変換関数の結果もプレビューできるようです。 その場で変換関数の結果を見ることができるので、トライアンドエラーを高速に行うことができそうです。 コンポーネント ETLワークフローを実現するために以下のコンポーネントが標準で付属しています。 コネクター データソースとの接続のためのコンポーネントです。 これはEmbulkでいうところのinput-pluginやoutput-pluginに相当するものです。 以下に示すような標準的なGCPサービスには対応しています。 Google Cloud Storage(GCS), BigQuery, CloudSQL, Cloud Pub/Sub, Cloud Spanner, Cloud BigTable, Cloud Datastore さらに非GCPなクラウドサービスやオンプレに構築されたデータソースにも対応しています。 aws S3, aws Athena, ファイルシステム, MySQL, Netteza, etc. 変換関数 データの変換を行うためのコンポーネントです。 Embulkでいうところのfilter-pluginやparser-pluginに相当します。 データをコピーしたり、JSONをパースしたりできるようです。 また、既存の変換関数に不十分な部分があった場合は自分でJavaScriptの関数を追加することもできます。 エラー検知 エラーが発生した時にそれを他のコンポーネントに流し込むことができるようです。 発生したエラーを他のコンポーネントで処理することで柔軟なエラー処理を行うことができそうです。 Cloud Data Fusionのまとめ Cloud Data Fusionを使うことでETLワークフローを作成することができそうです。 GUIベースでワークフローを作成することができるため、プログラミングをすることができない人でもパイプラインの開発が可能になったのは大きなメリットかと思います。 扱うデータの規模が小さく、専任のデータエンジニアがいないようなチームではCloud Data Fusionを使うと幸せになれる予感がします。 一方でワークフローの規模がある程度大きくなるとGUIではなく、GitHubなどを用いたコードベースでの管理が欲しくなります。 Cloud Data Fusionがそのような使い方をできるようになるかどうか、今後も注目していきたいです。 興味深かったセッション Google Cloud Next '19では約600セッションととても多くのセッションがありました。 その中で面白かったセッションを紹介します。 Serverless関連 Migrating from a Monolith to Microservices モノリスなアプリケーションをマイクロサービスに移行する際のGCPをつかったベストプラクティスを紹介しています。 Serverless on Google Cloud GCP上にサーバーレスなアプリケーションどうやって構築するかについて紹介しています。 Cloud Runを使って実際にPDFを生成するアプリケーションを構築するデモもあります。 CI/CD in a Multi-Environment, Serverless World GCPのサーバーレスサービスのCI/CD戦略についてのセッションです。 Design Pattern Evolutions for a Scalable Future GCPのサーバーレスなサービスを使う際のデザインパターンを紹介しています。 Building a WorldWeb App with Ruby, App Engine, Serverless Container, and Spanner Rubyを使ってサーバーレスにアプリケーションを立ち上げた話。 Spannerも使ってみたようですが結局相性が悪そうでした。 BigQuery系の興味深かったセッション Plaid's Journey to Global Large-Scale Real-Time Analytics With Google BigQuery YouTubeにまだ動画が上がっていないので、上がり次第埋め込みリンクにします。 KARTEというサービスを提供しているPLAIDさんの発表です。 BigQueryでReal-Time処理をするときのアーキテクチャパターンを紹介しています。 Kohls and Datametica Share EDW Workload Migration Tips Kohls(アメリカやカナダで展開しているホームセンター)の分析基盤をオンプレからBigQueryに移行したという話です。 オンプレ時代の辛みから始まり、どういう戦略で移行を行ったのか、移行した結果どういう良いことが起こったのかを紹介しています。 まだ一部のシステムはオンプレにあるため、「俺達の戦いはこれからだぜ!」という終わり方をしています。 同じく分析基盤移行をしている身としては応援したくなるような場面が数多くありました。 来年のGoogle Cloud Nextではさらなる進化を遂げた分析基盤を見せてもらいたい気持ちでいっぱいです。 Data Warehousing With BigQuery: Best Practices 個人的に一番の神セッションです。 BigQueryの運用中にハマりがちな罠に対する転ばぬ先の杖が数多く紹介されていました。 特にslotに関する知見が深まったのは大きな収穫でした。 後半のteadsさんの事例紹介もCloud Dataflowを使ってラムダアーキテクチャをカッパアーキテクチャにするお手本のような内容でした。 Best Practices for Storage Classes, Reliability, Performance, and Scalability YouTubeにまだ動画が上がっていないので、上がり次第埋め込みリンクにします。 Google Cloud StorageのStorage Classが増えたという内容です。 どういう時にどのStorage Classを使うと信頼性やコストやパフォーマンスが良いのかがまとまっています。 まとめ Google Cloud Next '19はGCP関連の新機能が多く発表されました。 セッションは約600あり、それ以外にもスポンサーブースもあったりGCPのエキスパートの方とお話しする機会をいただいたりとても刺激を受けました。 SREについてGoogleのCREの方にお話しいただいたり勉強になりました。 この経験を普段のプロダクト開発に生かしていきたいと思います。 最後に カンファレンス参加に関わる渡航費・宿泊費などは全て会社に負担してもらいました! 自分から希望すれば参加する機会をいただけて本当に感謝です! ZOZOテクノロジーズでは、最新技術に対する感度が高く、最新技術をプロダクトに取り入れたい人を募集しています。 こちらからご応募ください。 tech.zozo.com おまけ おまけとして、Google Cloud Next '19に参加した際にとった写真を載せておきます。 楽しかったことが伝われば幸いです! 会場はMoscone Centerという会場でした。 お昼ご飯が提供される公園では桜が咲いていて天気も良く気持ちよかったです。 近くのフードコートがすべて貸し切られランチが提供されていました。 美味しいという話を聞いたSuper Duper Burgerを公園で食べている写真です。 肉肉しくて美味しかったです! スポンサーブースにあったCloud BuildとCloud Runのデモ。 とにかく規模が大きい。 想像していたのと違ったゴールデンゲートブリッジ。 真ん中が霧で見えない…ガイドブックで見たのは全部見れていたのにな… コンピュータ歴史博物館にも行ってきました。 世界初のコンピュータから自分が今使っているコンピュータまでの歴史が知れてとても面白かったです。
アバター
はじめまして。2019年1月に入社したSREスペシャリストの sonots です。最近MLOpsチームのリーダーになりました。今回の記事はMLOpsの業務とは関係がないのですが、3月に弊社で実施した会社用GitHub個人アカウントの廃止について事例報告します。 TL;DR 会社用GitHubアカウントを作るべきか否か問題 会社用GitHubアカウントの利用で抱えた問題 1. OSS活動時にアカウントを切り替える必要があり面倒 2. GitHubの規約に準拠していない 会社用アカウントを廃止した場合にセキュリティをどのように担保するか GitHubのSAML single sign-on (SSO)機能について 会社用アカウントの廃止およびSSO有効化の実施 会社用GitHubアカウントを使い続ける場合 私用GitHubアカウントに切り替える場合 Botアカウントの場合 Outside Collaboratorの場合 デプロイキーの場合 SSO有効化の進捗確認 強制排除されたユーザのサポート 新しい社内レギュレーション 課題:実現できなかったこと おわりに 追記: FAQ TL;DR 個人で会社用と私用の2つの無料GitHubアカウントを持つことはGitHubの規約「非」準拠だった 会社用GitHubアカウントを廃止し私用GitHubアカウントを利用する規定に変更した セキュリティを担保するためGitHubのSSO機能を利用した (2023-05-17 追記) 「現在は複数アカウントの作成は問題ないのではないか」という旨のご質問をいただきましたので、改めてGitHub社に確認しました。返答としては、複数の「無料」アカウント作成は記事公開時から変わらずNGであるとのことでした。 一方、現在では Enterprise Managed Users (略称EMU)という機能がリリースされており、こちらを利用することで無料アカウントとは別の会社用アカウントを保持することができるようになっています。 EMUは本記事を公開後にリリースされた、会社のIdPと紐付いた、会社のEnterprise Account下のOrganizationにのみ書き込み権限を持つユーザーアカウントを作成できる機能です。 EMUの規約 にはユーザーアカウント保有数の制限は設けられていないことから、規約上の問題を回避できます。EMUについては興味をお持ちの方はオフィシャルの The GitHub Blogの記事 を参照してください。 (2023-05-17 追記ここまで) 会社用GitHubアカウントを作るべきか否か問題 会社で github.com ビジネスプランを利用するにあたって、会社用に新たにGitHubアカウントを作るかどうかは各社それぞれ悩んでいるポイントだと思います。 GitHub - githubのアカウントって、会社用に新しく作りますか?|teratail github.comのアカウントは仕事用と私用で分ける方がいいの? - Islands in the byte stream 弊社ZOZOテクノロジーズは1年ほど前に github.com のビジネスプランを導入した際に、「会社用に新たにGitHubアカウントを作る」方針で運用をはじめました。 私用GitHubアカウントで会社リポジトリにアクセスできてしまうと、私用PCを紛失・盗難された場合に会社のソースコードが漏洩してしまうリスクがあるためです。 私用PCは会社で管理しておらず、セキュリティ強度が低い可能性が高いため、この時点では私用GitHubアカウントで会社リポジトリにアクセスすることを許可できませんでした。 会社用GitHubアカウントの利用で抱えた問題 そのような理由があり「会社用に新たにGitHubアカウントを作る」運用にしていたのですが、以下のような問題が出てきました。 OSS活動時にアカウントを切り替える必要があり面倒 GitHubの規約に準拠していない 1. OSS活動時にアカウントを切り替える必要があり面倒 現在ZOZOテクノロジーズでは外部への発表やOSS貢献が推奨されています。また入社以前からOSS活動をしていた社員が(私を含め)増えてきています。業務の一環としてOSS開発を行うこともあります。 会社リポジトリにアクセスする際は会社用GitHubアカウントを使い、OSS活動をする際は継続して私用GitHubアカウントを使いたい要求があり、切り替えに煩わしさがありました。 個人的にもブラウザのマルチユーザ機能を使ったり、作業中のリポジトリ名を取得して自動でSSH鍵を切り替える仕組みを作って、煩わしさを軽減させる努力はしました。 しかし、それでも煩わしさは完全には消せませんでした。 私は情シスのメンバーではなかったのですが、本案件を主導した動機として、この問題が大きく関わっています。 2. GitHubの規約に準拠していない GitHubの規約に準拠していないというのは以下の文です。 https://help.github.com/en/articles/github-terms-of-service One person or legal entity may maintain no more than one free Account (if you choose to control a machine account as well, that's fine, but it can only be used for running a machine). 個人で2つ以上の無料GitHubアカウントを保持してはいけないと記載されています。 この件についてビジネスプランで契約している場合はどうなるのか、GitHub社のソリューションエンジニアである ikeike443 氏に確認していただきました。 会社用に新たに作成したアカウントは有料GitHub組織に所属しているが、そのアカウント自体は無料アカウントであるため、この規約に準拠していないと返事をいただきました。またGitHubというサービス自体マルチアカウントで使うような設計になっていないため、GitHub社としてはアカウントの一本化を推奨するとのことでした。 1年前にビジネスプランを導入した際にはこの規約についての認識が不足しており、GitHub社の規約に準拠していない社内運用ルールになっていました。規約に準拠していない運用を行なっているのは、企業としてあるべき姿ではないため、GitHub社に相談し会社用GitHubアカウントの廃止の検討を始めました。 また他社事例に詳しい、とある著名フリーランスエンジニアの方からも、同様の理由から各社で会社用GitHubアカウントの廃止を進言してきたとの情報をいただいたことも後押しになりました。 会社用アカウントを廃止した場合にセキュリティをどのように担保するか 会社用GitHubアカウントを廃止する場合、元々懸念していたように、セキュリティ強度が落ちる可能性があります。 そこで、 ikeike443 氏にアドバイスを受けたところ、SSO機能を使うことでセキュリティ強度を(ある程度)担保できることがわかりました。 現在の弊社では社内システムのSSO化も進めており、以前と違いSSO機能を使うことができるようになりました。この機能によりセキュリティ強度を担保できることがわかったため、会社用GitHubアカウントの廃止を決断しました。 焦点となった、GitHubのSSO機能について簡単に説明します。 GitHubのSAML single sign-on (SSO)機能について 公式ドキュメントは About authentication with SAML single sign-on - GitHub Help にあります。 ビジネスプランに入っているGitHub組織の管理者は、GitHub組織の設定画面からSSO機能を有効化することで、会社の認証基盤とSAML連携したSSOを要求できます。 GitHub組織のSAML連携設定が行うと、一般ユーザがウェブブラウザからGitHubの対象組織にアクセスすると、上部に以下のようなSSO認証を促す警告が表示されるようになります。また https://github.com/orgs/組織名/sso のようなURLが発行されるので、そのリンクを踏むことでもSSO認証できます。 GitHub組織の管理者は、GitHub組織のSSO連携の設定画面で、SSO認証を強制することもできます。この機能を有効化すると、SSO認証を有効にしていない組織Memberが全員組織から排除されます。組織のOutside Collaboratorには影響がありません。 そして、ここが重要な点なのですが、一般ユーザが会社リポジトリにGitアクセスする際のSSH鍵に対してもSSOが要求されます。 公式ドキュメントは Authorizing an SSH key for use with a SAML single sign-on organization - GitHub Help にあります。個人のSSH鍵設定画面 https://github.com/settings/keys から、Enable SSOしてSSO認証することで会社リポジトリを git clone できるようになります。 会社PCにはSSO有効化したSSH鍵をおく 私用PCにはSSO有効化していないSSH鍵をおく という運用ルールにすることで、私用PCを紛失・盗難された場合でも私用PCからは会社リポジトリにはGitアクセスできないため、リスクを減らすことができました。これにより、会社用GitHubアカウントの廃止を決断できました。 1点、当初私が誤解していたので補足しておきますが、この機能は git clone 時に都度ポップアップウィンドウが立ち上がってSSO認証を求めるような機能ではなく、あくまでもブラウザ上で1度だけSSO認証しておくものになります。 会社用アカウントの廃止およびSSO有効化の実施 SSOが有効化されていることが、会社用GitHubアカウントを廃止するためのセキュリティ上の前提条件となったため、SSO有効化とGitHubアカウントの移行を同時に実施しました。 1週間のSSO有効化期間を設けたのち、3月末にSSO強制化を実施し、完全に移行を完了しました。 以下のような移行手順書を用意し(本物は画像も貼ってありリッチにしあがっていますが、簡略化しています)、弊社メンバーに展開してご協力いただきました。 会社用GitHubアカウントを使い続ける場合 私用GitHubアカウントを持ってない方の場合は簡単で、2ステップで完了しました。 https://github.com/orgs/組織名/sso のリンクをクリックしSSOを有効化する SSH鍵のSSOを有効化する 私用GitHubアカウントに切り替える場合 こちらはなかなか手間がかかります。旧アカウントで権限確認をしておき、新アカウントでSSO認証を行い、権限を以前と同等に設定しなおす必要がありました。 旧会社用アカウントでの権限(所属Team、自身がOwnerになっているリポジトリ)を確認しておく 旧会社用アカウントで発行しているPersonal access tokenがないか確認し、あれば切り替え後に再設定する ブラウザで私用GitHubアカウントにログインし直す https://github.com/orgs/組織名/sso のリンクをクリックしSSOを有効化する 会社用のSSH鍵を新たに生成し、そのSSH鍵のSSOを有効化する Teamへの招待、自身がOwnerになっていたリポジトリに対してAdmin権限の付与を実施してもらう 移行が確認でき次第、旧会社用アカウントでログインし https://github.com/settings/organizations にて会社組織からLeaveする(有料枠を空けるため) 所属Teamの一覧を簡単に取得する方法はなさそうでしたが、弊社では幸いTeam数が少なかったため手動確認で賄ってもらいました。FYI: GraphQL APIで取得することもできそうでした。 自身がOwnerになっているリポジトリ一覧を簡単に取得する方法はなさそうでしたが、自身がCollaboratorになっているリポジトリの一覧を https://github.com/settings/repositories から確認できるので、そこからアタリをつけて頂きました。 オプションとして、会社リポジトリの通知を私用メールアドレスではなく会社メールアドレスに飛ばしたい場合は https://github.com/settings/notifications から設定変更をして頂きました。 間違えて旧会社用アカウントでSSOしてしまった場合、GitHub組織管理者の方で、 https://github.com/orgs/組織名/people/ユーザ名/sso からRevokeすることで、無効化できました。 Botアカウントの場合 SSOを有効にしたほうが、セキュリティ強度が上がるという考えから、BotアカウントもSSO必須というルールにしました。そのため、SSOできるように社内の認証基盤にもアカウントを作る必要がありました。 Bot用社内アカウントの発行を依頼 ブラウザでBotアカウントにログインしなおす https://github.com/orgs/組織名/sso のリンクをクリックしSSOを有効化する SSH鍵のSSOを有効化する 注意点として、3と4の間で git clone できない瞬間が発生するため、Botが動いていない時間帯を狙い、手早くSSOを有効化してもらう必要がありました。 Outside Collaboratorの場合 GitHub組織Memberにのみ影響があるため、Outside Collaboratorには影響ありませんでした。 ただ、当初の想定ではOutside Collaboratorだと考えていた外部協力者が、Team機能を使うため組織Memberになっているケースがいくつかありました。関係各所に連絡して Outside Collaborator への変換 を実施させていただきました。 なお、MemberとOutside Collaboratorの違いおよび共通点は以下のようになります。 Outside CollaboratorはTeam機能を使えない 組織MemberはSSOを求められる。よって社内の認証基盤にアカウントが必要である どちらも有料枠(seats)を消費する Outside Collaboratorに変換すると、Teamに所属していた場合と同様の権限になるようにリポジトリごとに権限が与えられます。しかし、今後はTeam機能が使えなくなってしまうため、不便です。 社内の認証基盤の都合もあり実施できなかったのですが、外部協力者の方もMemberにしてSSOを有効化したほうがセキュリティ強度は高いですし、改善の余地はあると考えています。 デプロイキーの場合 ユーザではなくリポジトリに紐づくため、デプロイキーには影響ありませんでした。 SSO有効化の進捗確認 GitHub組織の管理者は sso:unlinked という検索フィルターでSSOをまだ有効化していないユーザの一覧を取得できました。この機能を用いて進捗状況をウォッチしました。 特にBotアカウントのSSO有効化が実施されていないと、SSO強制化を実施したタイミングでZOZOの本番システムに影響が出る危険性もあるため、重点的に確認しました。 強制排除されたユーザのサポート SSO強制化によりGitHub組織から強制排除されたユーザは、SSOリンクを踏むことで組織に再度所属できます。その際に所属Teamなどの権限は元どおりに戻すことができるため、特別なサポートは必要ありませんでした。 新しい社内レギュレーション SSO強制化を実施した後の新しい社内レギュレーションをまとめると以下のようになります。 私用のGitHubアカウントで会社リポジトリにアクセスすることを推奨する(無料マルチアカウントは規約非準拠となるため会社用無料GitHubアカウントの作成は非推奨とする) ブラウザからアクセスする際はSSO認証をすること(システム側で強制) SSH鍵もSSO認証をしてアクセスすること(システム側で強制) SSO認証したSSH鍵は会社PCにのみおくこと。私用PCにはSSO認証していない別のSSH鍵を用意して利用すること 会社PCの紛失・盗難にあった場合、SSO認証したSSH鍵を削除できるように、会社PCにはリモートワイプを導入すること(通常、貸与された時点で導入済み) GitHubアカウント、SSOアカウント共にMFA設定をすること(システム側で強制) 課題:実現できなかったこと Enable SSOしたSSH鍵が私用PCに置かれていないことを、機械的な仕組みで防ぐか、せめて検知をしたかったのですが難しいようでした。リポジトリを git clone した証跡などが取れると良いのですが、GitHub組織のAudit Logではそれらの証跡は取れないようでした。 今後のGitHubに期待しています。 おわりに ZOZOテクノロジーズで実施した github.com の会社用GitHubアカウントの廃止について報告しました。150人以上の組織で github.com のビジネスプランを利用し、SSOを利用している事例は珍しいとGitHub社の方から聞きましたので記事にしました。会社で github.com を利用する場合の事例として他社でも参考にしていただけたら幸いです。 弊社に入社してまだ3か月ですが、GitHubの運用のみではなく、他にも社内ツールやレギュレーションの改善が速いサイクルで実施されているのを目の当たりにしています。第二創業期のわちゃわちゃ感を味わいたい方にはおすすめです。 ZOZOテクノロジーズでは自発的に動いて会社に良い影響を与えてくれるメンバーを絶賛募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com 追記: FAQ Twitterやはてなブックマークであった質問コメントについて回答します Q. 会社で課金していないということ? ビジネスプランを利用しており、会社のGitHub組織に招待するユーザ数の枠で課金されています。それでもGitHubアカウントそのものは無料アカウントなので規約非準拠になるということでした。会社用に有料GitHubアカウント (Pro)を作ることにすると2重で課金される状態になります。会社用に作った無料アカウントは会社リポジトリのアクセス以外の用途でも使えるため、そのような判断になるのかと思われます。 Q. 私用アカウントを会社に紐づけて身バレしたくないのだが? 「新しい社内レギュレーション」では私用のGitHubアカウントで会社リポジトリにアクセスすることを推奨してはいますが、必須とはしていません。新しく無料アカウントを作ると規約非準拠となるので、そちらは会社としては非推奨になりますが、新しく有料アカウントを作って利用することは可能です。まだ社内事例がないため、社内レギュレーションには記載していませんが、そちらの支払いについては社内でこれから検討が必要です。 Q. GitHub Enterprise (Server) にはしないのですか? GitHub Enterprise Server(旧称 GitHub Enterprise)への移行は導入コストがかかることに加え、自分たちでアップデートも含め運用するコスト(手間)を払わなければならないので今回は導入を避けました。また、外部サービス連携も難しくなります。 CircleCI Enterprise など一部のサービスはGitHub Enterprise Server同様にオンプレで運用するプランも提供しているので利用可能ですが、そちらも自分たちで運用する必要がでてきます。さらに、新しい機能が github.com よりも遅れて導入されるなど欠点があります。複数組織を作れる点や、セキュリティ強度が高い点はもちろん利点になると思います。 今回のSSO化を実施するよりも圧倒的に導入・運用コストが高く、さらに不便になる点もあることから、(少なくとも今は)GitHub Enterprise Serverを導入しないという判断をしました。本記事は同様の理由からGitHub Enterprise Serverを導入せずに「会社で github.com を利用する場合の事例」として参考にしていただけたら幸いです。
アバター
こんにちは、ZOZOTOWN事業部にてiOSエンジニアをしている名取です。 2019/3/21-22にベルサール渋谷ファーストで行われたSwift言語の技術カンファレンス try! Swift 2019 Tokyo に参加してきました。 ZOZOテクノロジーズからはほぼ全員のiOSエンジニアが参加したほか、プラチナスポンサーとして企業ブースの運営も行いました! 本イベントに向けてZOZOテクノロジーズとしてもメンバー1人1人がたくさんのtry!(挑戦)をしてきましたのでその様子と話題のファッションチェックアプリの評判や当日の興味深かったセッション内容などについてお伝えいたします。 try! Swiftについて try! SwiftはSwift言語での開発における最新の応用事例について集まる国際コミュニティです。 try! Swift 2019 Tokyoは32カ国から900人の参加者が集まる大規模なカンファレンスとなりました。 発表内容はSwift言語やiOSアプリ開発を中心にmac OS、Server Side Swift、3Dレンダリングの話など多岐にわたりAppleのエンジニアも数名が登壇されました。 Appleにとってもtry! Swiftが重要なイベントであることが伺えます。 ZOZOテクノロジーズとしてのtry! 私たちはアプリ開発が大好きです! Swiftでプロダクトコードを書くのが楽しく、日々の技術の進化にメンバーみんながワクワクしています。 そんな私たちだからこそ、try! Swift 2019 Tokyoに参加するにあたって2つのtry!をしました。 CfPを出す 私たちZOZOテクノロジーズでは業務の一環としてプロダクトチームを跨いでiOSエンジニアが集まり、CfPのアイデア出しや議論から始めました。 一度書いたことがある方なら分かると思いますがCfPには大変な労力と技術的な知識・経験が必要となります。社内のiOSエンジニアで何度も議論を重ね、お互いに意見し合いながら1人1人が納得のいくCfPをつくりあげ提出しました。 その結果、ZOZOテクノロジーズからは Michael Petrie が見事スピーカーに選ばれ発表を行いました! Michael Petrieのスピーカー発表が決まってからも社内のiOSエンジニアで積極的にピアレビューを行い、活発に議論を重ねながら発表内容のブラッシュアップに貢献しました。 私はZOZOテクノロジーズに入社してまだ数ヶ月ほどしか経ちませんが、チームとしての協力体制がとても素晴らしくチーム開発が好きな人にはこれ以上ない環境だと感じています。 ファッションチェックアプリの作成 スポンサー企業として何ができるかを考えた時に、私たちらしさとは何かを考えました。私たちに共通しているのはアプリ開発が好きでファッション産業が好きということです。 そんな私たちだからこそアプリを開発して来場者に楽しんでもらうのが面白いのではないかと考え「ファッションチェックアプリ」の開発を行いました。 ファッションチェックアプリの詳細は こちら ファッションチェックアプリは元々DroidKaigiの企業ブースのためにKotlinとFlutterで作られたものでしたが、try! Swift 2019 Tokyoのために弊社の2人のiOSエンジニア【伴( @banjun )と西山】が協力してiOS対応をしました。 try! Swiftは国際的なカンファレンスであることからローカライズも行いました。ローカライズはFlutter側で実装することで、ブースでデモをしているときもアプリ上ですぐに言語切り替えができるようになりました。このようにしてiOS対応コードを入れつつもFlutterの利点も活かしたアプリになっています。 try! Swift 2019 Tokyo当日の雰囲気 会場はとても広く、世界中のSwiftデベロッパー900人が一堂に会しました。 try! Swiftは国際的なカンファレンスということもあり開催場所が日本であるにも関わらず多くの海外デベロッパーが参加しました。 メインのセッション会場とは別にスポンサーブースも設けられました。 当日のZOZOテクノロジーズのブースはこんな感じです! ブースではファッションチェックアプリの展示をはじめ、ZOZOテクノロジーズのiOSエンジニアが参加者の質問に答えたり技術的なこと会社のことなどについてお話をさせていただきました。 ファッションチェックアプリは非常に多くの方々に利用していただき、たくさんのシェアがあったのにはとても驚きました。 どうやって作っているのか、リリースの予定はあるのかなど多くのご意見をいただき参加者の方々との良い交流のきっかけにもなりました。 ブースの運営側としても非常に学びのあるとても楽しい時間となりました! SESSIONS 全32セッションの中から特に気になったセッションについて個人の感想などを踏まえながらいくつかご紹介いたします。 Keypath入門 @Benedikt Terhechte 出典 : https://speakerdeck.com key pathを使う最大のメリットはプロトコルだけでは実現できない型の抽象化を行うことだと思いました。 言い換えれば、リーダブルなコードの記述ができるとも言えるかと思います。 以下のような型の異なる複数の要素を持つstructがあったとします。 この時にSettingsEntryというkey pathを要素に持つstructを以下のように用意したとします。 すると、SettingsEntryという1つの型のみでsetttingの管理を行うことができるようになり抽象化できるというお話でした。 セッションの後半では 岸川克己さん の key pathを使ったライブラリ が紹介されており、key pathのユースケースは意外と多い印象を受けました。 プロダクトコードを書く上で汎用化/抽象化を行うことは私たち開発者が常に意識すべきことの1つです。 key pathはそのための1つの選択肢になると思い興味を持ちました。 PixarのようなグラフィックをSwiftで実現する @Michael Petrie 出典 : https://speakerdeck.com 弊社ZOZOテクノロジーズのMichael Petrieの発表です。 Swift実装のレイトレーシングを使って3Dレンダリングされた1本のショートムービーからこのセッションは始まりました。 Swiftでレイトレーシングの実装ができること、それもMetalなどのフレームワークが使われていないことに驚いた方も多いのではないでしょうか。まさに自分もその一人です。 サンプルコードを眺めているとimportされているのはFoundationのみであることに気づきます。 私たちが普段アプリ開発をする際は多くのライブラリやフレームワークをプロジェクトに取り入れその恩恵にあずかっています。しかしながら、Swift言語を深く知ることでレイトレーシングというPixarで使われている技術が自前実装で実現可能ということを知りとても刺激を受けました。 私たちiOSエンジニアが当たり前のように使っているSwiftでこんなことができるのかと学習意欲を掻き立てる素晴らしい発表内容でした。 Michael Petrieの発表はTwitterで話題になっていたほか、公開された サンプルコード は翌日のGitHubのトレンド入りするなど、5分という短い時間の中で大変注目を集めていました! 以下、発表を終えたMichael Petrieのコメントです。 Understanding a concept, and being able to articulate it in a way that is easy to understand are two entirely different things. Presenting at try! Swift was an opportunity to practice the latter, one for which I’m very grateful. Presenting on stage has highlighted the importance of being able to clearly explain what I know and have learnt. Through whatever means available, I think this is something every programmer should aim to do. ポートレートモードを自作しよう @Rina Kotake 出典 : https://speakerdeck.com Swiftを使って2次元画像の背景をぼかすという発想がそもそも自分の中になく、アウトプットとしてのクオリティも大変素晴らしかったことからとても興味深いセッションでした。 深度情報を持たない画像に対してセグメンテーションを行う場合は機械学習を利用するしかないと思っていましたが「GrabCut」という手法を用いることで綺麗に画像の前景抽出を行うことができることを初めて知りました。 2D画像は深度情報を持たないから背景をぼかすことはできないではなく、成し遂げたい課題に向かってtry!した発表内容は本イベントに相応しいものだと感じました。 魔法の法則 @Dave DeLong Ex-Apple iOS EngineerのDave DeLongさんの発表です。 彼の好きなSF小説作家ブランドン・サンダースンさんが掲げる3つの法則を魔法の法則と題し、どうしたら素晴らしいユーザー体験が実現できるかを発表されました。 とても印象的な内容だったので以下に3つの魔法とその解釈を記載いたします。 この3つの魔法の法則に従って行動すれば自ずと素晴らしいものになるとまとめられました。 【1】作者が魔法で満足のいくように葛藤を解決する能力は、読者がその魔法をどれだけよく理解しているかに正比例します。 これを普段私たちが開発するサービスやプロダクトに置き換えて考えると次のように言えると思います。 ある問題を解決するために新しい機能や改善を行ったとき、ユーザーが真に幸せになるためにはユーザーがそのことを正しく理解できていないといけない。すなわち、私たち開発者はユーザーがどんなことを行いたいのか、どんな問題を抱えているのかを適切に理解・想定しその最適解をサービスに組み込んでいく必要がありそのガイドを示すべきだと。 【2】制限は権力より重要 Dave DeLongさんは「できないことはできることよりも面白いことが多い」と述べられていました。 アプリの開発をするにあたっては多くの制限に直面します。時間、人数、能力、、、 私たち開発者は多くの制限がある中でどんな未来を実現すべきかを考え、常に最良の選択をする必要があります。 ただこうした多くの制限の中でこそ創造性が発揮され、時には思いも寄らない素晴らしい未来が実現するんだと述べられていました。 【3】新しいものを追加する前に、すでに持っているものを拡張してください 何か全く新しいものを追加することだけがユーザーを喜ばせるわけではなく、ある1つの機能に関して常に奥行きを加えることで、確実にユーザーを喜ばせることができる。 つまり、一見単純に見えるものが実は非常に奥深く複雑性がありニュアンスの多いものだということを発見した時にとてもワクワクするのだとDave DeLongさんは述べられていました。 サービスの開発者なら誰しもがユーザーを魅了する素晴らしいものを作りたいという気持ちがあるかと思います。 どうしたらユーザー体験が向上するのか。どうしたらユーザーは満足するのか。 絶対的な答えがない中で私たち開発者は常に試行錯誤しベストを尽くしています。 本セッションではそのような開発者への行動指針が提示されたように感じ深く共感するものでした。 おわりに try! Swift 2019 Tokyoに参加して改めてSwiftでできることが年々増えているように感じました。 セッションではServer Side Swiftの話を始め、ハードウェアハッキングやレイトレーシングの話などiOSアプリ開発の枠を超えた発表が多い印象を受けました。 また、OSSとしてコードを公開したりデモアプリを作ったりといった発表も多く発表から自身のアウトプットに持っていくスピーチはどれも洗練されたものでした。 たくさんの刺激や学びを得ると共に、来年こそはあの場所で発表したいと強く思いました! www.wantedly.com
アバター
こんにちは。iOS担当の遠藤です。 最近、私達のチームではUI実装をカスタムコンポーネントを使用して行うようにしました。今回はそのメリットと実装方法について紹介したいと思います。 はじめに 今までのUI実装では、カスタムビューごとにInterface Builderでテキストの色や、サイズを設定していました。 しかしこのやり方には、以下の問題がありました。 間違った色やサイズを設定してしまう恐れがある xibを作成するたびに色やサイズなどを設定するのは面倒 UIを変更する際に、変更範囲が広くなってしまう UI実装をする度にInterface Builderでテキストやボタンの色や数値を設定すると、間違った設定をしてしまう恐れがあります。そして、毎回この設定をInterface Builderで設定するのは面倒です。 また、変更箇所が多くなってしまうことも問題でした。 アプリを開発、運用する中で一番変化が多いのはUIです。テキストやボタンの色、テキストサイズやフォントなどを変更したいという要望は少なからずあると思います。 Xcode 9からはColor Literalが使用できるようになり、色の変更はとても簡単になりましたが、テキストサイズやフォントの変更は一括ではできません。 テキストサイズやフォントを変えるのに多くのxibを変更する必要が出てきます。すべてのxibを変更し、漏れがないかを確認するのは非常に大変です。 これらの問題を解決するために、カスタムコンポーネントを使用して実装することにしました。UIを実装する際の参考になれば幸いです。 カスタムコンポーネントとは? プロダクトで使用するテキストやボタンのデザインを定義したクラスをここではカスタムコンポーネントと呼んでいます。 UILabelやUIButtonを継承して実装し、xibでデザインを構成するのにこのコンポーネントを使用します。 カスタムコンポーネントを使用すると どう変わるか? カスタムコンポーネントにデザインの定義をしているので、xibにカスタムコンポーネントを配置するだけでUI実装が済みます。 このことではじめに挙げた問題は以下のように解決されました。 😣 間違った色やサイズを設定してしまう恐れがある ✅ 毎回Interface Builderでデザインの設定を行わないのでデザインの設定ミスが無くなります。 😣 xibを作成する度に色やサイズなどを設定するのは面倒 ✅ xibではカスタムコンポーネントを配置するだけなので、デザインの設定を行うのはカスタムコンポーネントを実装する際の一度だけになります。 😣 デザインを変更する際に、変更範囲が広くなってしまう ✅ カスタムコンポーネントのみを変更するだけで済みます。 はじめに挙げた問題が全て解決されました。カスタムコンポーネントを使用することでUIの実装が楽になり、変更にも強くなります。 実装について カスタムコンポーネントの実装について説明します。 カスタムコンポーネントを実装する上での制約 カスタムコンポーネントを実装する上で、2つの条件と1つの要望がありました。 条件 1. カスタムコンポーネントを使用する際に、Interface Builderでデザインが確認できること カスタムコンポーネントを使用することで、Interface Builderでデザインの確認ができなくなってしまうとあまり使い勝手はよくありません。 デザインを実装する上でも、運用をする上でもInterface Builderでデザインを確認できたほうがやりやすいです。 👍 👎 2. 型を保つこと コンポーネントをカスタマイズしたことで、 UILabel や UIButton の型ではなく UIView など別の型になってしまうと扱いづらくなってしまいます。 特に困るのが、xibにカスタムコンポーネントを配置した際に型が保てないと、 Attributes Inspector で要素の設定ができなくなってしまうことです。 👍 👎 要望 カスタムコンポーネント単体でのデザインを、Interface Builderで確認できること Interface Builderでカスタムコンポーネント単体のデザインを確認できないことで大きな支障はありません。 しかし、Interface Builderでカスタムコンポーネントのデザインを確認できることは便利なので、できるようにしたいです。 カスタムコンポーネントの実装 上記で挙げた3つの課題を満たすようにカスタムコンポーネントを実装します。 Intarface Builderでデザインの表示 カスタムコンポーネントのデザインをInterface Builderで表示したいので、まず、カスタムコンポーネントのクラスに @IBDesignable を設定します。 @IBDesignable を書くことで、Interface Builderを表示するときにビルドがはしり、デザインが反映されるようになります。 @IBDesignable を書いただけでは、Interface Builderにデザインの反映はされないので、 prepareForInterfaceBuilder() メソッドを書きます。 prepareForInterfaceBuilder() はInterface Builderでビルドされるときのみ呼ばれ、そのメソッドで書かれた内容がIntarface Builderに反映されます。 Interface Builderを表示するときにビルドされるのなら、その時呼ばれる初期化メソッドの init(frame:) にデザインの設定処理を書けば済むように思われます。 しかし、 init(frame:) にデザインの設定を書いてもInterface Builderには表示されません。 なぜなら、初期化が終わった後に、Interface Builderで設定されている値で上書きしてしまうからです。 // CustomButton.swift @IBDesignable class CustomButton : UIButton { override func prepareForInterfaceBuilder () { super .prepareForInterfaceBuilder() } } デザインの設定 次に、デザインの設定です。 デザインの設定はInterface Builderを使用せずに、コードで設定をします。 Interface Builderでデザインを設定したカスタムコンポーネントを使用するには、xibを読み込む必要があります。 カスタムビューを使用するときのように、 UINib を使用してxibを読み込んだ実装をしてみました。 // CustomButton.swift @IBDesignable class CustomButton : UIButton { override init (frame : CGRect ) { super . init (frame : frame ) } required init ?(coder aDecoder : NSCoder ) { super . init (coder : aDecoder ) } override func awakeFromNib () { super .awakeFromNib() loadNib() } override func prepareForInterfaceBuilder () { super .prepareForInterfaceBuilder() loadNib() } func loadNib () { let bundle = Bundle( for : type (of : self )) let nib = UINib(nibName : String (describing : type (of : self )), bundle : bundle ) let button = nib.instantiate(withOwner : self , options : nil ).first as ! UIButton button.frame = bounds addSubview(button) } } しかし、この実装では意図したように表示されません。UIButtonの上にxibから生成したボタンを貼り付けているため、このように二重に表示されます。 これを避けるにはUIButtonを継承せずに、UIViewで実装をすると解決します。 しかし意図したように表示できたとしても、型が UIView になってしまうためこのやり方はできません。 なのでxibではなく、デザインの設定をコードで行います。 // CustomButton.swift @IBDesignable class CustomButton : UIButton { override init (frame : CGRect ) { super . init (frame : frame ) setupAttributes() } required init ?(coder aDecoder : NSCoder ) { super . init (coder : aDecoder ) setupAttributes() } override func prepareForInterfaceBuilder () { super .prepareForInterfaceBuilder() setupAttributes() } private func setupAttributes () { layer.cornerRadius = 4.0 backgroundColor = UIColor(red : 0x33 / 0xFF , green : 0x33 / 0xFF , blue : 0x33 / 0xFF , alpha : 1.0 ) setTitleColor(.white, for : .normal) titleLabel?.font = UIFont.boldSystemFont(ofSize : 14.0 ) } } デザインを設定している実装が setupAttributes() です。 このメソッドを、 init(coder:) 、 init(frame:) , prepareForInterfaceBuilder() で呼び出します。 xibを読み込んで生成される場合は init(coder:) が呼ばれ、コードから生成する場合は init(frame:) が呼ばれるのでどちらにも setupAttributes() を書いています。 Interface Builderでも表示するために、 prepareForInterfaceBuilder() でも書いているのでInterface Builderでビルドされるときは setupAttributes() が2回呼ばれてしまいます。 2回呼ばれてしまいますが、Interface Builderでのビルド時のみということと、書かれる処理が色やサイズの設定で複数回呼ばれても結果が変わらないため、私達は問題にはならないという判断をしました。 カスタムコンポーネントのデザインをIntarface Builderで表示する カスタムコンポーネントのデザインをコードで実装すると、デザイン通りに実装できているのかぱっと見てわからないです。 そして、カスタムコンポーネント単体をxibで確認できるようにしたいという要望がまだ解決できていません。 ですが、 @IBDesignable と prepareForInterfaceBuilder() を設定しているので、カスタムコンポーネント単体をInterface Builderで確認できるようにすることもできるはずです。 そこで私達は、 CustomButton.xib というプレビュー用のxibを用意することで解決しました。 xibをAssistant Editorで表示しながらコードを書くことでカスタムコンポーネントのデザインを確認しながら実装できます。 このプレビュー用のxibはアプリにバンドルする必要がないので、 Target MemberShip のチェックを外しています。 これで全ての条件、要望が解決されました。 あとはこのカスタムコンポーネントをxibに配置すると、Intarface Builderで色やフォントを設定しなくても実装できるようになります。 まとめ 今回はカスタムコンポーネントを使用したUIの実装について紹介しました。 アプリ開発において、UIを実装することは多く、実装の度に何回も同じ設定をするのは面倒です。また、単純な作業なため気をつけていたとしてもミスが起きてしまいます。カスタムコンポーネントを使用することで、毎回の煩わしい作業とミスをなくすことができました。デザインを変更するときも、カスタムコンポーネントのみの対応で済みます。変更範囲が狭いので対応漏れの不安がなくなります。 変更に強くミスのないUI実装になるので、カスタムコンポーネントを導入することをおすすめします。ぜひ、試してみてください。 弊社ではUI実装の仕組み化にも積極的に取り組んでいます。少しでも興味がある方は、ぜひ一度オフィスにお越しください。 下記からのエントリーもお待ちしています。 www.wantedly.com
アバター
はじめに こんにちは! 2019年2月にZOZOテクノロジーズへサーバーサイドエンジニアとして入社した籏野( @gold_kou )と申します。 Qiita でも少し記事書いてます。 いきなりですが、皆さんはAPI仕様書をどのように管理されていらっしゃいますか? Confluence、Wiki、Markdown、Spreadsheet、Excelなど色々手段やツールはあると思います。私が担当しているプロジェクトではOpenAPIを導入しています。 この記事ではOpenAPIの基本と実際に導入して得られたノウハウをご紹介いたします。 OpneAPIの恩恵はただの管理の仕方にとどまらないので、ぜひこの記事を読んで開発効率化のお役に立てばと思います。 また、弊社のテックブログで以前、OpenAPI(Swagger)のバージョン2系に関する 開発効率を上げる!Swaggerの記法まとめ や 開発効率を上げる!Swaggerで作るWEB APIモック が投稿されておりますが、今回は対象バージョンが3系となります。 OpenAPI概要 OpenAPI Specification(OAS) OASはREST-APIの標準仕様です。OASのことを単にOpenAPIと呼ぶこともあります。 YAMLかJSON形式で記述します。 現在はバージョン3系が最新ですので、特別な事情がない限り3系を使いましょう。 2系から3系への変更点は様々あるのですが、一番大きな変更はComponentsオブジェクト(後述)が追加されたことです。 DRYにかけるため、OpenAPIが目指している "human readable" へ近づきました。 Swagger Toolsを活用することで効率的に記述できます。 OpenAPIを使うメリットとデメリット メリット 効率的に記述できる Swagger Editorのおかげ 3系からより効率的に human readable & machine readable APIクライアントとサーバースタブを自動生成できる OpenAPI Generatorのおかげ スキーマ駆動開発できる 開発工数を削減できる かっこいいビジュアルのAPI仕様書を作れる Swagger UIのおかげ バージョン管理しやすい 書き方に統一性を持たせられる 我々がOpenAPIを導入した理由は上記メリットのうち、特に「APIクライアントとサーバースタブを自動生成できる」点に魅力を感じたためです。 スキーマ駆動開発を実践しているわけではないのですが、APIを定義すればある程度のソースコードを自動生成できる一石二鳥感は充分な選定理由だと思います。 デメリット 学習コスト YAML/JSONの記法 自動生成のやり方 Swagger OpenAPIを勉強するうえで避けては通れないSwaggerについて説明します。 まず歴史的な話なのですが、もともとOpenAPIの前段としてSwagger Specificationというものがありました。 それがOpenAPI Initiativeという団体に管理が移ったことで、名称がOpenAPI Specificationに変更されました。 しかし、ツールセットの開発は現在もSwaggerで行われているものもあり、ツール名には「Swagger」が名残で残っています。 Swagger Tools OpenAPIを効率的に記載するためのOSSのツールセットです。 Swagger Editor ブラウザ上で記述するタイプのエディタです。インストール不要なので手軽に試せます。 リンクは こちら インターネット上でAPI情報を記載することに抵抗がある場合は、以下のようにローカルでDockerイメージをpullして、起動することもできます。 $ docker pull swaggerapi/swagger-editor $ docker run -d -p 80:8080 swaggerapi/swagger-editor ブラウザで localhost:80 にアクセスすれば、以下が表示されます。 また、 Visual Studio Code にはSwagger Viewer(プラグイン)が用意されています。 プログラミングと同じエディタで編集できるので便利です。 Swagger UI OpenAPIに則って記述されたスキーマをAPI仕様書化するツールです。 YAMLファイルやJSONのままでは人間には見るのが辛い部分もありますが、これを使えば統一されたカッコいいUIを提供します。 Swagger EditorやSwagger Viewerの右側はこれを利用しています。 APIクライアントツールとして利用することも可能です。認証まわりも対応していますので、トークンを埋め込んで実行することもできます。 Swagger Codegen OpenAPIに則って記述されたスキーマからAPIクライアントとスタブサーバーを自動生成するツールです。 自動生成により開発コストを削減するだけでなく、スタブサーバーがあることでフロントエンドの開発もバックエンドの開発を待たずに進めることができます。いわゆるスキーマ駆動開発というやつですね。 3系対応を進めるためSwagger Codegenをフォークした OpenAPI Generator の開発がコミュニティドリブンで進んでいるそうです。 後述ですが、私の担当プロジェクトではOpenAPI GeneratorのDockerコンテナを使用しています。 OpenAPIの基本記法(YAML) 公式サンプル を中心にYAMLでの基本記法をまとめます。 読めばなんとなくわかるのですが、一応1つずつ説明していきます。 また、サンプルには無くてもよく使う記法もいくつかピックアップします。 その他の記法や詳細は 公式ドキュメント をご参照ください。 ファイル名 ルートのファイル名は openapi.yml が推奨されていますが、それ以外に特に決まりはありません。 <システム名>.yml とかもよく見ます。 OpenAPIオブジェクト openapi フィールドでOpenAPIのバージョンを設定します。 openapi : "3.0.0" Infoオブジェクト メタ情報を設定します。 version フィールドでAPIドキュメントのバージョンを設定します。 title フィールドでAPIドキュメントのタイトルを設定します。 description フィールドで説明を設定します。 termsOfService フィールドでサービス規約を設定します。例では、内容が長くなるのでURLになっていますね。 contact フィールドで連絡先情報( name / email / url )を設定します。 license フィールドでライセンス情報( name / url )を設定します。 info : version : 1.0.0 title : Swagger Petstore description : A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification termsOfService : http://swagger.io/terms/ contact : name : Swagger API Team email : apiteam@swagger.io url : http://swagger.io license : name : Apache 2.0 url : https://www.apache.org/licenses/LICENSE-2.0.html Serverオブジェクト APIサーバー情報を設定します。 url フィールドでURLを設定します。今回の具体例は1つだけですが、リスト形式で設定できるため例えば、「ローカル環境用」「ステージング環境用」「プロダクション環境用」などをそれぞれ設定することも可能です。 servers : - url : http://petstore.swagger.io/api Pathsオブジェクト 各エンドポイント仕様を設定します。 Path Item オブジェクト(/petsなど)で1つ以上のパスを設定します。 Operation オブジェクト(postなど)で1つのパスの1つのメソッドの単位を設定します。 operationId フィールドでOpenrationオブジェクトを一意にする識別IDを設定します。APIクライアントを自動生成する際に使用されます。 requestBody フィールドでリクエストボディを設定します。 required: true とすることでリクエスト時にこのボディがあることを必須とします。 content でボディの中身を設定します。 schema フィールドでは $ref でcomponents配下に定義したスキーマを読み込み、DRYな記述ができます。もちろんここに直接記述することもできます。また、 $ref は外部ファイルも読み込めるため、ファイルを分割することも可能です。 responses フィールドでレスポンスを設定します。ステータスコードをキーにして、その他はdefaultとします。こちらも schema を $ref できます。 こちらの例には無いですが、 Tags オブジェクトで Operation オブジェクトをグループ化するためのタグを設定します。 name フィールドでtag名を設定します。 paths : /pets : post : description : Creates a new pet in the store. Duplicates are allowed operationId : addPet requestBody : description : Pet to add to the store required : true content : application/json : schema : $ref : '#/components/schemas/NewPet' responses : '200' : description : pet response content : application/json : schema : $ref : '#/components/schemas/Pet' default : description : unexpected error content : application/json : schema : $ref : '#/components/schemas/Error' Parameter オブジェクトでパラメータを設定します。 name フィールドでパラメータ名を設定します。 in フィールドでパラメータの場所を設定します。 query/header/path/cookie のいずれかを選択します。 query: /items?id=### のようにURL末尾に ? でパラメータを設定する場合です。 path: /items/{itemId} のようにパス内にパラメータを埋め込む場合です。 required フィールドでパラメータが必須かどうかを設定します。 in フィールドの値が path の場合は必然的に true になります。 paths : /pets : get : parameters : - name : tags in : query description : tags to filter by required : false style : form schema : type : array items : type : string paths : /pets/{id} : get : parameters : - name : id in : path description : ID of pet to fetch required : true schema : type : integer format : int64 Componentsオブジェクト 再利用する部品を定義します。 また、再利用しないとしてもリクエストボディやレスポンスは極力Componentsオブジェクトに記載することで、記載方法に統一性を持たせ可読性を向上できます。 Schema オブジェクトでスキーマを設定します。スキーマ名(Petなど)は $ref で参照する際に使用されます。スキーマ内でさらに $ref して入れ子構造にすることも可能です。 properties フィールドでプロパティ(パラメータ)を設定します。 type フィールドでは integer(整数)/number(少数)/string/boolean/array/object のいずれかを設定します。 format フィールドでは int32/int64/float/double/byte/binary/date/date-time/password のいずれかを設定します。 type フィールドと組み合わせます。 required フィールドでプロパティ単位に必須パラメータを設定します。 こちらの例には無いですが、 minimum と maximum フィールドで数値の下限上限を設定します。 こちらの例には無いですが、 example フィールドでそのプロパティが取りうる値を具体例として設定します。 components : schemas : Pet : allOf : - $ref : '#/components/schemas/NewPet' - required : - id properties : id : type : integer format : int64 NewPet : required : - name properties : name : type : string tag : type : string Error : required : - code - message properties : code : type : integer format : int32 message : type : string APIクライアントとスタブサーバーを自動生成する OpenAPIを利用するメリットの1つである自動生成についてです。 いくつか手段はありますが、今回はDockerを使用する方法です。 APIクライアント 上記の具体例でも使用していた petstore-expanded.yaml からAPIクライアント(Go言語)を自動生成します。 $ docker run -v ${PWD}:/local openapitools/openapi-generator-cli:v3.3.4 generate -i /local/petstore-expanded.yaml -g go -o /local/out/go [main] WARN o.o.c.ignore.CodegenIgnoreProcessor - Output directory does not exist, or is inaccessible. No file (.openapi-generator-ignore) will be evaluated. [main] INFO o.o.c.languages.AbstractGoCodegen - Environment variable GO_POST_PROCESS_FILE not defined so Go code may not be properly formatted. To define it, try `export GO_POST_PROCESS_FILE="/usr/local/bin/gofmt -w"` (Linux/Mac) [main] INFO o.o.c.languages.AbstractGoCodegen - NOTE: To enable file post-processing, 'enablePostProcessFile' must be set to `true` (--enable-post-process-file for CLI). [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/model_error.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/docs/Error.md [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/model_new_pet.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/docs/NewPet.md [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/model_pet.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/docs/Pet.md [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/api_default.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/docs/DefaultApi.md [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/api/openapi.yaml [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/README.md [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/git_push.sh [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/.gitignore [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/configuration.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/client.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/response.go [main] INFO o.o.codegen.DefaultGenerator - writing file /local/out/go/.travis.yml [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/.openapi-generator-ignore [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/.openapi-generator/VERSION すると、カレントディレクトリ以下に下記のようなディレクトリやファイルが生成されます。 これらをもとに開発を進めていけば定型部分がだいぶ自動生成されているので、開発工数を削減できるはずです。 上記で実行したopenapitools/openapi-generator-cliイメージのgenerateコマンドのオプションは以下です。 g: 生成コードの種類の指定(言語やFWなど) i: yamlファイルの指定 o: 出力パスの指定 また、gオプションで指定できるクライアントとサーバーのgeneratorの種類は以下です。 $ docker run --rm openapitools/openapi-generator-cli:v3.3.4 list The following generators are available: CLIENT generators: - ada - android - apex - bash - c - clojure - cpp-qt5 - cpp-restsdk - cpp-tizen - csharp - csharp-dotnet2 - csharp-refactor - dart - dart-jaguar - eiffel - elixir - elm - erlang-client - erlang-proper - flash - go - groovy - haskell-http-client - java - javascript - javascript-closure-angular - javascript-flowtyped - jaxrs-cxf-client - jmeter - kotlin - lua - objc - perl - php - powershell - python - r - ruby - rust - scala-akka - scala-gatling - scala-httpclient - scalaz - swift2-deprecated - swift3 - swift4 - typescript-angular - typescript-angularjs - typescript-aurelia - typescript-axios - typescript-fetch - typescript-inversify - typescript-jquery - typescript-node SERVER generators: - ada-server - aspnetcore - cpp-pistache-server - cpp-qt5-qhttpengine-server - cpp-restbed-server - csharp-nancyfx - erlang-server - go-gin-server - go-server - haskell - java-inflector - java-msf4j - java-pkmst - java-play-framework - java-undertow-server - java-vertx - jaxrs-cxf - jaxrs-cxf-cdi - jaxrs-jersey - jaxrs-resteasy - jaxrs-resteasy-eap - jaxrs-spec - kotlin-server - kotlin-spring - nodejs-server - php-laravel - php-lumen - php-silex - php-slim - php-symfony - php-ze-ph - python-flask - ruby-on-rails - ruby-sinatra - rust-server - scala-finch - scala-lagom-server - scalatra - spring (以下省略) スタブサーバー APIクライアント同様に、スタブサーバー(Go言語)を生成します。 gオプションで指定するものが違うだけですね。 $ docker run -v ${PWD}:/local openapitools/openapi-generator-cli:v3.3.4 generate -i /local/petstore-expanded.yaml -g go-server -o /local/out/go [main] WARN o.o.c.ignore.CodegenIgnoreProcessor - Output directory does not exist, or is inaccessible. No file (.openapi-generator-ignore) will be evaluated. [main] INFO o.o.c.languages.AbstractGoCodegen - Environment variable GO_POST_PROCESS_FILE not defined so Go code may not be properly formatted. To define it, try `export GO_POST_PROCESS_FILE="/usr/local/bin/gofmt -w"` (Linux/Mac) [main] INFO o.o.c.languages.AbstractGoCodegen - NOTE: To enable file post-processing, 'enablePostProcessFile' must be set to `true` (--enable-post-process-file for CLI). [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/go/model_error.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/go/model_new_pet.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/go/model_pet.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/go/api_default.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/api/openapi.yaml [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/main.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/Dockerfile [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/go/routers.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/go/logger.go [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/go/README.md [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/.openapi-generator-ignore [main] INFO o.o.codegen.AbstractGenerator - writing file /local/out/go/.openapi-generator/VERSION カレントディレクトリ以下に下記のようなディレクトリやファイルが生成されます。 特にスタブとして重要なのは下記ファイルです。 リクエストが来たら Status.OK を返すようになっています。 当然ながらビジネスロジックは記述されていません。 /* * Swagger Petstore * * A sample API that uses a petstore as an example to demonstrate features in the OpenAPI 3.0 specification * * API version: 1.0.0 * Contact: apiteam@swagger.io * Generated by: OpenAPI Generator (https://openapi-generator.tech) */ package openapi import ( "net/http" ) // AddPet - func AddPet(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(http.StatusOK) } // DeletePet - func DeletePet(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(http.StatusOK) } // FindPetById - func FindPetById(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(http.StatusOK) } // FindPets - func FindPets(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(http.StatusOK) } サーバー起動します。 $ go run main.go 2019/03/20 21:28:21 Server started curlリクエストすると200が返ってきました。 $ curl -s http://localhost:8080/api/pets -o /dev/null -w '%{http_code}\n' 200 クライアント開発チーム用にこのスタブを残してスキーマ駆動開発にしたり、サーバーサイド側の開発工数をさげたりできます。 なお、私が所属するプロジェクトでは、一連のコマンドをmakeコマンドで実行できるようにしています。 現場からのTips ここからは実際の開発で得られたノウハウやつまずいたこと、もう少し良くしたいと考えていることをご紹介したいと思います。 定義したobjcet型のプロパティが自動生成されなかった 以下は実例を簡略化し、一部抜粋したものです。SampleBに関する記述であることに着目してください。 SampleB : type : object properties : status : type : integer example : 200 message : type : string example : successfully resource : type : object properties : count : type : integer example : 1 results : type : array items : type : string example : "1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ" 以下が自動生成されたモデルです。 Resourceの型に着目してください。なぜか、定義したSampleBでなく別のSampleAのポインタ型のフィールドが宣言されています。 package httpmodel type SampleB struct { Status int32 `json:"status"` Message string `json:"message"` Resource * SampleAResource `json:"resource,omitempty"` //あれ、なんでAなの? } 原因は、既にプロパティ名とexample値が同一のobject型のプロパティがあることでした。 どうやら、OpenAPI Generatorはモデル自動生成時に同一のものがある場合は、YAMLファイル上でより上に定義されたものをDRYに生成してくれるようです。 ちなみに、あえてDRYにしたくない場合は、 $ref を使ったり、exmaple値を異なるものにすることでも回避できます。 array型プロパティを持つモデルが期待通りに生成されなかった 以下のように type: array を持つスキーマを定義し、モデルを自動生成したところ、期待通りにスライスをプロパティとしてもつモデルを生成できませんでした。 (以下は実例を簡略化し、一部抜粋したものです) components : schemas : RequestA : description : こちらはサンプルです type : array items : properties : sku30 : description : こちらはサンプルです example : 123abc type : string required : - sku30 // RequestA - こちらはサンプルです type RequestA struct { Inner [] map [ string ] interface {} `json:"inner,omitempty"` } 原因は こちらのPR でしょうか。 トップレベルにarrayかmapのプロパティがあるとgenerateしてくれないようです。 解決策は2通りあります。 1つ目は、 .openapi-generator-ignore ファイルに自動生成を無視するファイルを指定し、手動で実装する方法です。これを多用しすぎると自動生成の恩恵を受けられないため、OpenAPIを利用するメリットがかなり薄れてしまいます。 2つ目は、下記のように、 type: array を type: object で包む方法です。モデルが1つ増え、独自型のスライスのプロパティを持つことになります。 components : schemas : RequestA : description : こちらはサンプルです type : object properties : inner : type : array items : properties : sku30 : description : こちらはサンプルです example : 1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ type : string required : - sku30 type : object // RequestA - こちらはサンプルです type RequestA struct { Inner [] RequestAInner `json:"inner,omitempty"` } type RequestAInner struct { // こちらはサンプルです Sku30 string `json:"sku30"` } 命名に気を使う必要がある スキーマ名や operationId フィールド名などは自動生成コードでのstruct名やフィールド名、ファイル名などに反映されるものです。したがって、一意に認識しやすい名前をつける必要があります。 これを怠るとソースコードの可読性低下につながってしまいます。 こちらはOpenAPIならではの悩みです。 バリデーションを手動で実装している これは、今後改善したいと考えていることです。 現在、 ozzo-validaiton というバリデーションのライブラリを使用して、API仕様書の情報を見ながらバリデーションを手動で実装しています。 これは二度手間感がある上、人が実装しているので、バリデーション漏れなどのミスが発生する可能性もあります。 例えば、Rubyであれば oas_parser と json_schema というgemを組み合わせる方法があります。OpenAPIで定義したファイルのrequiredやtype、example値などの情報を使って自動でバリデーションを自動生成できます。Go言語でも同様のことができるライブラリを探そうと考えています リクエストパラメータのバリデーションライブラリの選定 go-playground/validator は、最もポピュラーなGo言語のバリデーションライブラリです。しかし、今回は別のライブラリ(上述)を採用しました。 理由としては、OpenAPIとの相性が悪いと判断したためです。 go-playground/validator はstructにバリデーション用のタグを記述するスマートな方法です。しかしながら、モデルを自動生成した際に上書きされてタグの記述内容が消失するケースもあります。 自動生成した後にタグを記述し、 .openapi-generator-ignore にファイル名を追記すれば解決するのですが、開発時の運用が複雑になってしまうと判断し、採用を見送りました。ここでの連携ができれば利便性がすごく高いと思います。 API仕様書とソースコードの乖離 開発時にAPIの仕様変更にドキュメントが追従できず、API仕様とソースコードで乖離が発生する経験はありますでしょうか。これは実装者とレビュアが普段の開発で注意し、定期的に乖離の発生状況を確認するべきでしょう。 OpenAPIを有効活用すれば乖離の発生を抑えることができます。 なぜならば、リクエストとレスポンス用のモデルは定義ファイルにしたがって自動生成されるため、レスポンスでの乖離は発生しません。 リクエストに関しては、リクエストパラメータのバリデーションに関して自動生成を導入していないケースでは、乖離が発生し得ます。例えばGo言語などの静的言語で実装しているのであれば、型の確認は可能ですが、ビジネスロジック面でのチェック(値の範囲など)まではできません。 まとめ 今回はOpenAPIの基本記法と、実際の開発現場で得られたつまずきやTipsをいくつかご紹介しました。 OpenAPIは単にAPI定義をスマートに記述できるだけでなく、そこからある程度まで自動生成してくれます。 良さそうだなと感じたら、OpenAPIを使ってみてください。 さいごに ZOZOテクノロジーズでは、技術でファッションの世界を変える仲間を募集しています。 お洒落に自信がある方・無い方どちらも歓迎です。 ご興味のある方は、以下のリンクからぜひご応募ください! ちなみに、私が担当しているプロジェクトでは現在、下記のような技術スタックやツールを扱っています。 よろしければご参考ください。 Go OpenAPI GitHub CircleCI AWS JIRA/Confluence Gsuite tech.zozo.com
アバター
こんにちは! ZOZOテクノロジーズ開発部の中坊( e_tyubo )です。 私の所属しているマーケティングオートメーション(MA)チームでは、ZOZOTOWNやWEAR等の各サービスで蓄積されたデータを集約したデータ基盤の運用を行なっております。我々MAチームはこの集約されたデータを用いて顧客分析を行いマーケティングに活用しています。 今回はその運用の中で生じた問題とその解決方法ついて紹介致します。 概要 データ基盤には定期的にデータを流し込む処理がスケジューリングされており、膨大なデータが毎日転送されています。 現在このデータ転送処理はサービス側のデータベースから直接データを流しておらず、一度ハブとなるデータベースに集約しています。 ZOZOTOWNとWEARはどちらもSQL Serverを使っているため、ハブとなるデータベースもSQL Serverを使っています。 SQL Serverでデータを転送する方法はいくつかありますが、定期的にバッチ処理でデータをまとめて転送する場合に注意すべきポイントがあったため対応を行いました。 この記事ではMAチームが運用しているSQL Server同士のデータ連携で生じた問題と実施した改善内容をご紹介します。 前提 SQL Server同士の連携 まずはデータ連携のシステムを紹介します ハブとなるデータベースをマーケティング用途に使うことからMarkeデータベースと呼びます。 ZOZOTOWNやWEARのデータベースからMarkeデータベースに連携する流れは下図のようになります。 このようにbcpコマンドを用いてデータのエクスポートとインポートを実現しています。 どのようなクエリでbcp outするかは次に説明する管理テーブルで定義します。 管理テーブル データ連携はMarkeデータベース上にある管理テーブルの情報をもとに実行されます。この管理テーブルには下記の情報が含まれます。 接続先DBの情報 連携タイミング 連携時に実行するSQL 取得結果を保存するMarkeデータベース上のテーブル名 例えば下記のようなデータが入っていたとすると、指定したタイミングにデータベースに対してSQLが実行され、その結果をMarkeデータベースのテーブルに保存します。 接続先 タイミング クエリ 保存先テーブル名 zozotown_db_1 夜間1回 SELECT * FROM zozotown_db.dbo.TableA TableA wear_db_1 日中に定期実行 SELECT column1, column2 FROM wear_db.dbo.TableB TableB 現在この管理テーブルに約300テーブル分のデータが入っており、日々データが更新されています。 課題 さて、ここからが本題です。運用上問題になったことは2つあります。 テーブル定義が変わる サービスを運用しているとテーブル定義の変更は起こり得ます。例えばサービス側のテーブルにカラムを追加すると、エクスポートするデータの形式が変化してしまいます。具体的には下図のように、SELECT * FROM ~を使っているケースで問題が起きます。 カラム追加時はMarkeデータベースにも追加しておかないと、インポートするデータとテーブル定義が異なることでエラーになります。これは実際に何度も発生していました。 そこで、すべての管理テーブルのレコードのクエリをカラム指定することにしました。そうすることで仮にカラムが追加されてもクエリの結果は変化しないため、問題が生じなくなります。もしカラムのデータが必要な場合は追加対応を行えば良いため、要件的にもすぐには問題になりません。 カラムが削除されたりカラムの定義が変わった場合は別途対応が必要になりますが、発生頻度が低いため考慮していません。 ソートされていないデータの大量インサート データ連携ではデータを全て入れ替えていますが、SELECT時にクラスター化インデックスを定義するカラムと同じ並び順でソートしないとINSERTに時間を要することがあります。 以降「クラスター化インデックスを定義するカラムと同じ並び順でソート」は長いので、「クラスター化インデックスでソート」と呼びます。 INSERTが遅くなる原因について説明します。 SQL Serverはページという容量8KBのまとまりでデータを保存していて、レコードの情報はページの中に含まれています。INSERTする時はクラスター化インデックスで規定された順番通りにデータを挿入しますが、その順序はページの中で保つ必要があります。ページの容量に限界が来るとページの新規作成かページの分割が必要になり、その挙動の違いでパフォーマンスが変わります。 まずクラスター化インデックスでソートされたデータを挿入するケースを考えます。 分かりやすくするために、主キーである整数型のidがクラスター化インデックスになっているケースを想定します。 ページの中に4レコードしか入らないと仮定すると、5レコード目を挿入する時は下図のように新しいページを作成します。 挿入されるデータの順序がクラスター化インデックスの順序と一致しているため、INSERT時にはページの新規作成だけで完結します。 次にクラスター化インデックスでソートされていないケースを考えます。 5レコード目はidの値で並び替えた場合既存のページに含まれる必要がありますが、すでに容量がありません。この時は下図のようにページの分割が発生します。 分割されて新たに生成されたページ1'には空き容量があるため、適切な領域にデータが追加されます。 データの量が多い時はこのページ分割の頻度次第で、パフォーマンスに影響する可能性があります。 適切にソートされていない場合、INSERT済みのデータ量が多いほどページ分割が発生しやすくなります。大量データをbcpでインポートする際にINSERT速度が低下する場合は、今回ご説明したページ分割が大量に発生している可能性を疑います。 ページ分割の発生を最小限にするためには全てのSELECT文にクラスター化インデックスを用いた適切なORDER BY句を設定する必要があります。 現状主キーがクラスター化インデックスに指定されているケースで対応が可能なため、主キーを基準にソートしました。 2つの課題をまとめると、下記の対策が必要です。 SELECT句を全てカラム指定にする ORDER BY句を主キーのカラムと同じ並び順で指定する 実際に対応の必要なレコードがかなりの数存在していたため、手動で書き換えると大変手間になることが予想されました。そこで管理テーブルに設定されてるクエリを更新するためのUPDATE文を自動生成することにしました。以下でその手法を紹介します。 改善方法 方針 すでに管理テーブルに入っているレコードを正しい形に修正する必要があるため、UPDATE文を対象となるレコード全てに対して作成します。 実現のためにPythonを使ってODBCドライバー経由でSQL Serverに接続できる pyodbc というライブラリを使いました。これは Microsoftの公式ドキュメント で言及されているSQL Serverにアクセスする方法です。 Pythonを使った理由はDigdagで利用することを念頭に置いているからです。 MAチームではワークフローを管理するツールとしてDigdagを利用しており、今後ワークフローに乗せることを想定しています。 実装の流れは下記のようになります。 現在連携されているテーブルの連携情報の一覧を取得 対象テーブルの定義を取得 対象テーブルの主キー情報を取得しORDER BY句を生成 既存のクエリにWHERE句が指定されていたら継承する 用意した情報からUDPATEクエリを生成しsqlファイルに出力 テーブル一覧を取得する まずは管理テーブルから同期対象のテーブル一覧を取ってきます。 この時に対象テーブル名とクエリを全て取得します。 対象テーブルの定義を取得 information_schemaを使ってカラム情報を取得します。 bcpコマンドを使ってインポートする際は、エクスポート時に指定するSELECT句のカラム順序を、インポート先のテーブルのカラム順序と合わせておく必要があります。そのため、ordinal_positionを基準にソートしています。 SELECT table_name, column_name FROM information_schema.columns ORDER BY ordinal_position 取得した情報からテーブル名毎のカラム定義のdictionaryをPythonのロジックの中で用意しておきます。 対象テーブルの主キー情報を取得しORDER BY句を生成 下記のシステムカタログビューを経由してテーブル毎に主キーで使われるカラム名を取得します。 それぞれの名前と用途と取得用クエリは下記の通りです。 名前 用途 sys.tables テーブル名の取得 sys.key_constraints 主キー制約の取得 sys.index_columns 制約の中で使われるindexの取得 sys.columns indexで使われるカラム名の取得 SELECT tbls.name, cols.name FROM sys.tables AS tbls INNER JOIN sys.key_constraints AS key_const ON tbls.object_id = key_const.parent_object_id AND key_const. type = 'PK' INNER JOIN sys.index_columns AS idx_cols ON key_const.parent_object_id = idx_cols.object_id AND key_const.unique_index_id = idx_cols.index_id INNER JOIN sys.columns AS cols ON idx_cols.object_id = cols.object_id AND idx_cols.column_id = cols.column_id ORDER BY tbls.name, idx_cols.key_ordinal 複合主キーの場合index内で使われるカラムの順序を合わせる必要があるため、テーブル毎にkey_ordinalを基準にソートしています。このクエリを使うと主キーに使われるカラム名を全て取得できるので、テーブル名毎にORDER BY句を作ります。 既存のクエリにWHERE句が指定されていたら継承する 既存のクエリのロジックを変えないために、既に指定されているWHERE句を抽出して新しいSELECT文に適用する処理が必要になります。 例えば下記のようなクエリがあった場合は、WHERE column2 >= 1を切り取る必要があります。 SELECT column1, column2 FROM db.schema. table WHERE column2 >= 1 Pythonを使うと下記の様な処理でクエリの中で存在するWHERE句以下を抽出できるので、保持しておきます。 where_start_pos = query.upper().find( 'WHERE' , 0 ) if where_start_pos >= 0 : where = query[where_start_pos: len (query)] また、WHERE句以降にORDER BYが使われているケースが存在する時は、同様のロジックでORDER BY以降を切り取って適切なものに置き換えを実施しました。 用意した情報からクエリを生成しsqlファイルに出力 これまでの処理でクエリを生成するために必要な情報は揃いました。これらを使ってUPDATE文を生成します。 用意したデータを下記の変数で保持しているとします。 target_tables: 連携しているテーブルのリスト column_map: テーブル毎のカラム定義のリスト primary_key_map: テーブル毎の主キーに使われるカラムのリスト where_map: テーブル毎の既存クエリのWHERE句 また、テーブル名毎の更新対象レコードidをrecord_id_mapという変数で持っているものとします。 Pythonの中でのクエリ生成処理の流れは簡略化すると下記の様になります。 update_query_format = "UPDATE manage_table SET select_query = %s WHERE id = %d" for table in target_tables: # テーブル毎に必要な情報を変数に入れる column_list = column_map[table] primary_key_list = primary_key_map[table] where = where_map[table] record_id = record_id_map[table] # 用意した情報を整形してSELECT文を生成(メソッドの詳細は省略) select_query = create_select_query(table, column_list, primary_key_list, where) # 用意したフォーマットを置換してUPDATE文を作成 update_query = update_query_format % (select_query, record_id) # 生成されたSQLを必要な場所に出力 最終的に出力されたUPDATE分を実行したら以降はカラム指定でテーブルが連携されるため、対策は完了です。 改善後の成果 まだ実施から3か月ほどしか経っていませんが、以降カラム追加に伴うエラーは発生しておりません。さらに実施後は新しく連携テーブルを追加する際はこの仕組みを使ってSELECT文を生成できるので、人為的なミスがおきにくくなっているだけでなく作業も効率化されています。 ORDER BY句を指定する等のクエリ生成時のルールをロジックに内包できるので、今後仕様の追加があっても容易に仕組に取り込むことが可能になったこともメリットだと思います。 今後の展望 本記事で紹介した機能の延長で、いずれはテーブル定義の差分を自動検知する仕組みを作りたいと思っています。Digdag上でワークフローとして定義し、定期的に必要なALTER文を生成して通知する仕組みを作ることでテーブル定義を常に正しい状態に維持しやすくなると考えています。 まとめ 本記事ではデータ統合のためのデータ連携の仕組みの一部と、それを安定化させるための取り組みを紹介しました。データ連携を改善する1つの方法として参考になれば幸いです。 MAチームではMA基盤上のアプリケーションの開発・運用だけでなく、データ連携の仕組みの開発・運用も行なっており、ビッグデータを活用したデータ基盤の改善に取り組んでいます。目立たない分野ですが非常にサービスへの影響は大きく、挑戦のしがいがあるチームです。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 https://www.wantedly.com/companies/zozo-tech/projects
アバター
はじめまして! ZOZOテクノロジーズ開発部の平田( @TrsNium )と申します。 業務ではデータ基盤の開発・運用を行っています。 よろしくお願いいたします。 今回複数のツールが混在していたデータ基盤を「Digdag・Embulk」に統一したので、その取り組みを紹介します。 概要 弊社のデータ基盤は注文情報や顧客情報などをSQL Serverから取得しBigQueryに転送しています。 以前のデータ基盤では「Talend」と「Embulk・Digdag」でデータの収集と転送をしていました。 Talendは、タスクのスケジューリングとデータ転送を行うツールです。 Digdagはタスクのスケジューリングをするツールで、Embulkはデータを転送を実行するツールです。 「Talend」と「Digdag・Embulk」は別々のチームが管理・運用をしており、運用負荷が高いという問題がありました。 そこでデータの収集と転送をするツールを、「Digdag・Embulk」に統一することで運用負荷を下げました。 はじめに、移行前後のデータ基盤について紹介します。 以前のデータ基盤 以下の図が移行前のデータ基盤です。 TalendがSQL Serverからデータを収集し、S3にデータを配置します。 その後、DigdagとEmbulkがS3に配置されたデータをBigQueryに転送します。 なお、途中にS3を介しているのはDWHとしてRedshiftを使用していた時代の名残です。 現在はDWHとしてBigQueryを使用しているので、S3が存在する必然性はありません。 移行後のデータ基盤 以下の図が移行後のデータ基盤です。 SQL ServerからS3、S3からBigQueryまでの転送をDigdagとEmbulkで行います。 ツールの紹介 Talendとは ここで、Talendが何かご存じでない方もいると思うので、紹介させていただきます。 TalendとはETLツール兼ワークフローエンジンです。 ETLとはあるシステムからデータの抽出・加工をし、別のシステムにエクスポートする工程のことです。 またTalendには以下の様な特徴があります。 ビジュアルスクリプトにより、プログラミングの知識がなくとも直感的にデータ転送をできる 多種多様なDBやクラウドストレージ、csvなどのファイルフォーマットをサポートしている データ加工処理をJavaで拡張可能 ビジュアルスクリプトにより、プログラミングの知識がなくとも直感的にデータ転送をできる Talendは主にビジュアルスクリプトで操作を行うことができます。 ビジュアルスクリプトは、エンジニア以外の人にも直感的で分かり易いマウスのドロップ&ドラッグで操作ができます。 多種多様なDBやクラウドストレージ、csvなどのファイルフォーマットをサポートしている Talendは主要なクラウドストレージ(S3, GCS, Azure Data Lake Storeなど)に対応しています。 それに加え、データフォーマットも多岐にわたり対応しているため、クラウドストレージにあるデータのフォーマットを気にせず利用できます。 そして様々なデータベース(MySQL, SQL Server, Netezzaなど)をサポートしています。 これらを組み合わせることで、データ転送や、データベース・クラウドストレージへのデータの参照と書き込みができます。 データ加工処理をJavaで拡張可能 Talendではデータ転送だけではなく、ルーチンと呼ばれるデータを加工する機能があります。 例えば日付型の値に対しては、 TalendDate.diffDate で2つの日付データから時間の差を求められます。 多くのルーチンがデフォルトで提供されているため、様々なデータの加工ができます。 またルーチンはユーザーが独自定義できるようになっています。 Javaでルーチンを拡張できるため、より柔軟にデータの加工ができます。 先の通り、拡張性・柔軟性・敷居の低さからTalendが利用されていました。 Digdag・Embulkとは DigdagとEmbulkの紹介をさせていただきます。 Digdagとはオープンソースのワークフローエンジンです。 複数のタスクをワークフローとして定義し、Digdagがワークフローを実行します。 Digdagには以下の様な特徴があります。 YAMLフォーマットでワークフローを定義する タスクに依存関係を持たせることができる タスクの実行環境が柔軟である DigdagはYAMLフォーマットでワークフローを記述ができるため、Git上でDigDagのワークフローを管理できます。 タスクは依存関係を持つことができ「データの加工→データの集計→通知」の様なワークフローを定義をできます。 この機能により、どの様なタスクでもワークフローとして定義できます。 またDigdagのタスク実行環境は自由に選択をできます。 これはタスクをDockerコンテナ内で行うことができるためです。 Embulkはバルクデータをバッチ処理するオープンソースのバルクデータローダーです。 Embulkはプラグインベースのバルクローダーであり、以下の様な特徴があります。 バッチ処理をYAMLフォーマットで定義をできる 様々なプラグインがオープンソースとして公開されている EmbulkはYAMLフォーマットでバッチ処理を定義できるため、Git上でワークフローの管理をできます。 また多くのプラグインがオープンソースとして公開されています。 プラグインは「input」「filter」「output」「parser」「formatter」などの複数種類があります。 これらのプラグインを組み合わせることで、多様なのデータソースからデータを読み込み、加工し、ターゲットソースに書き込むことができます。 更にEmbulkはデータを並列処理をするため、バッチの実行時間を短くできます。 弊社には embulk-output-bigquery や embulk-filter-column 、 embulk-filter-timestamp_format などの作者でもある @sonots 氏が所属しています。 Embulkに詳しいエンジニアが在籍していることで、Embulkに関する問題解決速度が速くなりました。 Talendの運用上の問題点 Talendを使ってきましたが運用する上で問題がありました。 実際に起きた問題は以下の通りです。 無償版のTalendはビジュアルスクリプトで書かれている部分をGitで管理できない CUI操作を行うことができない Talendには製品版のTalend PlatformとTalend Enterprise、無償版のTalend Open Studioがあります。 弊社で運用されていたTalendは無償版のTalend Open Studioでした。 無償版のTalendでは、開発管理の機能やジョブ実行機能などに制限があります。 そのうちの一つにプロジェクトの管理をGitで行うことができません。 Git上でプロジェクト管理ができないために「どのETLを変更したのかの把握」や「機能の追加・変更による障害の対応」などの難しさがありました。 また、GUI操作で基本操作をするため、反復的な操作を行う業務が困難でした。 この様なことを踏まえて他のツールへ置き換えることを検討しました。 EmbulkとDigdagでは以上の問題を解決できることや、Talendと同様にデータを処理できる点から選択しました。 またチーム内で、で複数の運用実績があるのも選択の決め手となりました。 Digdag・Embulkへの移行 Digdag・Embulkへの移行手順について紹介します。 Talendのリプレイスは以下の様な手順で行いました。 TalendのETLを把握する Talendと同様にEmbulkでデータを転送する Digdagでデータ転送のタスクをスケジューリングする Talendと並走してDigdagとEmbulkを実行する 移行前後のデータの差分を確認する DigdagとEmbulkへ移行する 1. TalendのETLを把握する TalendのETLを把握するため「Talendが参照しているテーブル」「どの様なルーチンを使用しているのか」「データ転送スケジュール」を調べました。 参照しているテーブルは200程度でした。 ルーチンは14種類あり「対象のカラムをコピーし別のカラムへ移す」「データの秘匿化」「文字列の正規化」「日付のフォーマットを変更する」などがありました。 またS3へのデータ転送は、1日に1回AM4:00〜AM7:30の間に終えられるようにスケジューリングされていました。 Embulkでは、Talendで行っていた14のルーチンと同様な処理をしなければなりません。 Digdagでは、Talendと同様にAM4:00〜AM7:30の間にデータを転送できるようにスケジューリングする必要がありました。 2. Talendと同様にEmbulkでデータを転送できるようにする データ転送ではEmbulkを使用します。 EmbulkでSQL Serverのデータを参照するには、 embulk-input-sqlserver プラグインを使用しました。 ルーチンで行われている処理は、 embulk-filter-ruby_proc プラグインで代替えしました。 embulk-filter-ruby_procでルーチンと同じ処理ができているかは、手順5の方法で確認をしました。 またEmbulkの設定ファイルはYAMLで記述されます。 約200のテーブルを手書きでEmbulkのYAMLフォーマットに変換するのは、現実的ではないと考えました。 そこでTalendのプロジェクトファイル中にある、マークアップ言語で記述されているETLの情報を利用しました。 TalendのETLの情報をパースするスクリプトを書き、EmbulkのYAMLをしました。 副次的なメリットとしてデータを参照するためのSQLや、データ加工の対象となるカラム名のtypoが発生しませんでした。 3. Digdagでデータ転送のタスクをスケジューリングする Digdagは、Embulkが行うデータ転送をスケジューリングするために使用します。 Digdagでは繰り返し処理を並列に実行をできます。 繰り返し処理は複数のテーブルを転送する際に利用されています。 しかし、並列にテーブルのデータ転送を行う処理を行うことは、SQL ServerのDBリソースを占有してしまう恐れがあります。 そこで以下の様なことに気をつけました。 データベースの許可されている同時接続数や計算リソースを使い切らないようにする 同時に並列で処理する各テーブルのデータサイズが均一になるようにグルーピングすることで、従来よりデータの転送所要時間を少なくする SQL Serverの同時接続数を超えないように処理するために、以下の図のようにスケジューリングしました。 上の図では、複数のデータ転送を纏めて幾つかのグループにしています。 グループ内のデータ転送は並列に処理されますが、グループ自体は逐次的に処理されます。 つまり、同時にデータベースへアクセスするEmbulkプロセス数の上限は、1グループあたりのタスク数に制限されます。 グループ内の数を減らすことによりデータベースへの負荷を軽減ができますが、同時に全体を通したデータ転送の時間は長くなるトレードオフの関係になります。 更に最適化されていないスケジューリングでは以下の図の様に、データ転送の所要時間が大きくなります。 この図のテーブルサイズは、各テーブルのデータサイズを表しています。 Costは全テーブルの転送にかかる時間を示しています。 1グループの転送時間はグループ内の最大転送時間に依存するため、Costは並列で転送されるグループの最大テーブルサイズに依存します。 つまり、全体のグループ内のテーブルサイズを均一にすることでCostの最適化ができます。 しかし上の図では、グループ内のテーブルサイズが均一になってないため最適化されていません。 そこで、テーブルサイズで全体をソートすることで、Costを以下の図のように最適化しました。 テーブルサイズでソートすることによりCostが最適化されました。 これによりデータベースの負荷を制限しつつ、転送時間を短くできました。 4. Talendと並走してDigdagとEmbulkを走らせる 次に、Digdagサーバーにワークフローを登録します。 ワークフローは以下の図のようになっています。 以上の図では「Talend」と「Digdag・Embulk」はデータの干渉をしないために別々のバケットとデータセットを利用しています。 またDigdagとEmbulkを動作させる際には以下のことを気をつけました。 SQL Serverから読み込んだデータをS3にデータを転送できること S3からBigQueryにデータを転送できること SQL ServerからS3にデータを転送できない場合には、YAMLのフォーマットエラーやembulk-filter-ruby_procで使用するRubyコードが原因でした。 例えば、S3からBigQueryでデータの転送できないケースとして、日付型のデータフォーマットの違いによりデータを転送できないことがありました。 Talendでは 2019/04/01 12:00:00 の様にフォーマットされているのに対し、Embulkでは 2019/04/01 12:00:00.000 の様にデータを送っていました。 Talendの 2019/04/01 12:00:00 の様なフォーマットで送られることが期待されているので、Embulkで送ったデータは日付をうまく読み取れず転送に失敗します。 これらの問題がないことを確認することで、正常にDigdag・Embulkが動作していること、Talendとのデータフォーマットに違いがないことを把握できます。 5. 移行前後のデータの差分を確認する データに差分がある場合、分析で行なっている集計処理が正しく行われないため、差分がないことを確認します。 データの差分の確認はBigQuery上で行いました。 BigQuery上でデータの差分の確認を行うのは、以下の様なメリットがあります。 差分があった際に、どのカラムから差分が発生しているかの確認が容易 複数の差分を同時に求めることができる 当初は、データの差分をS3上にPutされているデータをもとにPythonスクリプトで求めていました。 しかし、データサイズの大きなテーブルでは差分を求めるのに時間がかかることや、一度に大量のテーブルの差分を求めることができませんでした。 そこで、BigQueryへ複数リクエストすることで、一度に複数テーブルの差分結果を短時間で取得をできました。 BigQueryでは、以下のクエリで差分結果を取得しました。 ( select * from `production. Order ` except distinct select * from `test. Order `) union all ( select * from `test. Order ` except distinct select * from `production. Order `) production.OrderはTalendで転送しているデータセットのテーブルで、test.OrderがEmbulkで転送しているデータセットのテーブルです。 except演算子では、左側の入力クエリに存在し、右側の入力クエリには存在しない行を返します。 例えば select * from `production.Order` except distinct select * from `test.Order` をベン図で表すと以下の様になります。 production.Orderにのみあるデータ 以上の図の斜線部ではproduction.Orderにはあるが、test.Orderにはないレコードを表しています。 全体の差分を求めるには上記クエリのExcept前後のテーブルを入れ替え、unionで両方の差分を結合し求めることができます。 6. TalendのジョブをDigdagとEmbulkに置き換える TalendをDigdagとEmbulkに移行する際の手順を紹介します。 移行手順は以下の2つです。 Talendの設定を変更して、移行前ジョブを無効化する EmbulkのS3へ転送するBucketを変更する 以上の手順は以下の図の様なイメージです。 以上の図ではTalendの設定を無効化した後に、Embulkのデータ転送の向き先をTalendが転送していたProductionバケットに向けています。 また、安全に移行するために、1日で移行するのではなく複数日に分けました。 なぜなら、移行を全て同日にするのはリスクがあるからです。 例えば、1日で移行する際に複数の折り重なった障害が起きた場合、本番環境に与える障害の影響が大きくなります。 そこで、同日にすべての転送するのをやめ、複数日に分けることでリスクを分配しました。 統一したことによるメリット EmbulkとDigdagに統一することで、以下の様なメリットがありました。 Gitを用いてDigdagとEmbulkの設定が管理できるようになった 時間に余裕を持ってデータ転送を終了できるようになった Git上でDigdagとEmbulkの設定を管理できるようになった DigdagとEmbulkに置き換わったことにより、スケジューリングやデータ転送の設定をGit上で管理できるようになりました。 Git上で管理できるようになったことで、複数人に情報共有をしやすくなりました。 またGitのリポジトリ運用はGitHubFlowのため、レビュー段階でミスや改善点に気付けます。 時間に余裕を持ってデータ転送を終了できるようになった EmbulkとDigdagに置き換えることで余裕を持って、バッチを終了できるようになりました。 TalendがAM4:00 ~ AM7:30の間にデータ転送を終えていたので、それと同様にできる必要がありました。 結果的にデータ転送を1時間30分程度で終了できるようになり、データ転送に余裕を持つことができました。 これはEmbulkとDigdagがデータ転送を並列処理できるからです。 次のデータ基盤のステージ 今回データ基盤のツールの統一をしました。 しかし、データフローの最適化がまだなされていません。 現在のデータ基盤のデータフローを最適化した図は以下のようになります。 以上の図ではデータフローを最適化することで、SQL ServerからS3を経由するフローがなくなりました。 データフローの最適化を行うことでデータフロー全体の転送時間を短くでき、ワークフローの数が減るためDigdagとEmbulkの設定ファイルを少なくできます。 S3を経由するのを止め直接BigQueryにデータを転送すれば、全体のデータ転送時間は1〜2時間程度短くできます。 そしてEmbulkとDigdagの設定ファイルを少なくできるため、新規テーブルの転送設定を行う手間を軽減できます。 今回は紹介ができませんが、データベースのレプリケーションなどまだまだ改善の余地があります。 まとめ 本記事では、「Talend」を「Digdag・Embulk」に移行したことについて紹介しました。 データ基盤の構築や、リプレイスをする際の参考になれば幸いです。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 www.wantedly.com .visual_script{ font-size: 123%; }
アバター
こんにちは、サーバーサイドエンジニアの竹若です。今回GraphQLにおけるエラーハンドリングを調査、Ruby on Railsと graphql-ruby を使って実装する機会があったので、そこで得られた知見を共有させていただきたいと思います。(なお今回の実装はプロダクション環境には出ていません) GraphQLの仕様とプラクティス それではまず初めに、GraphQLが仕様に定めているレスポンスの返し方を見ていきましょう。 レスポンスのフォーマットに関するプラクティス GraphQLのプラクティスの1つに、レスポンスのhttp statusを200で統一し、レスポンスの errors キーにエラーの詳細な情報を持たせるというものがあります。 なぜならGraphQLではリクエストに複数のクエリを含めることができるからです。 https://www.graph.cool/docs/faq/api-eep0ugh1wa/#how-does-error-handling-work-with-graphcool Since GraphQL allows for multiple operations to be sent in the same request, it's well possible that a request only partially fails and returns actual data and errors. これはあくまでプラクティスであり仕様ではないのですが、周辺ツール( Apollo やgraphql-ruby)がこのプラクティスに従っているため私たちも基本的には従うことになります。 レスポンスのフォーマットに関する仕様 ではGraphQLはどのようにしてエラーを表現するのでしょうか? GraphQLの仕様を見てみましょう。 https://facebook.github.io/graphql/June2018/#sec-Errors GraphQLの仕様ではレスポンスのフォーマットはハッシュであり、中に data と errors というキーを含みます。 { " errors ": [ { " message ": " hogehoge ", " extensions ": { " bar ": " bar " } } ] , " data ": { " user ": { " name ": " takewaka " } } } data クエリの実行結果が入るキーです。 クエリの実行前にエラーが発生した場合、 data キーはレスポンスに含まれません。 errors クエリの実行中に発生したエラーが入るキーです。 クエリの実行中にエラーが発生しなかった場合、 errors キーはレスポンスに含まれません。 またレスポンスに data キーが含まれない場合、 errors キーは必ずレスポンスに含まれている必要があります。 なお errors のフォーマットも仕様で定められていて、中に message , location , path というキーが含まれます。( location と path はクエリの中にエラーの該当箇所が存在する場合にのみ含まれます) 上記3つのキーの他にキーを追加したい場合は、 extensions というキーを用意してその中に追加する仕様です。 なぜなら message や path などのキーと同じレベルにオリジナルのキーを追加してしまうと、そのオリジナルのキーと将来的に仕様に追加されるキーがバッティングを起こす可能性があるからです。 https://facebook.github.io/graphql/June2018/#sec-Errors GraphQL services should not provide any additional entries to the error format since they could conflict with additional entries that may be added in future versions of this specification. GraphQLを使っていて発生するエラーとその分類 さて、GraphQLがどのようにしてエラーを表現するかはわかりました。 次にGraphQLを使っていて起こるエラーにはどのようなものがあるのか見ていきましょう。 エラーを以下の2つの観点で見ていきます。 エラーの原因はクライアントなのか、サーバーサイドなのか エラーはどこで発生したか クライアントが原因のエラーは主に以下の3つに分類できます。 パースエラー クエリのシンタックスエラー バリデーションエラー クエリが型チェックで引っかかる クエリ実行時エラー 認証失敗など サーバーサイドが原因のエラーはRailsで実装したロジックのエラーです。 どのようなエラーが存在するかわかったところで、それぞれのエラーをどのような形のレスポンスで表現するのか見てみましょう。 Apolloなどのクライアントがレスポンスをパースしやすいように、レスポンスのフォーマットはGraphQLの仕様に準拠した形で統一したいです。 そこで errors キーの中の message キーにエラーの詳細なメッセージを入れて、 extensions キーの中の code キーにステータスコードを入れる方法をここでは見ていきます。 これは GraphQLの仕様書に載っているエラーレスポンスの例 と同じ方法です。 また Apollo Server では、 extensions キーの中に code をはじめとしたエラーに関するキーを含める方法を採用しています。 例えば認証エラーであればこのようにcodeの中に AUTHENTICATION_ERROR というステータスを入れます。 " errors ": [ { " message ": " permission denied ", " locations ": [] , " extensions ": { " code ": " AUTHENTICATION_ERROR " } } ] サーバーエラーの場合はこのようにcodeの中に INTERNAL_SERVER_ERROR というステータスを入れます。 " errors ": [ { " message ": " undefined method 'hoge' for nil ", " locations ": [] , " extensions ": { " code ": " INTERNAL_SERVER_ERROR " } } ] graphql-rubyで実装する方法 エラーをどのような形のレスポンスで表現するか決まったところで、graphql-rubyで実際に実装していきましょう。 graphql-rubyではエラーをどう拾ってどう返すか graphql-rubyでは GraphQL::ExecutionError かもしくはそのサブクラスをraiseすることで errors にエラーを含めることができます。 def resolve(name:) user = User.new(name: name) if user.save { user: user } else raise GraphQL::ExecutionError, user.errors.full_messages.join(", ") end end 認証エラー 例として認証エラーの実装を載せます。 この例ではログインをセッションで管理していてcurrent_userメソッドを呼ぶことでユーザーオブジェクトが取得できる設定です。 コントローラーで GraphQL::Schema#execute を実行する際にユーザーのログイン情報を context に入れて引数として持たせておきます。 class GraphqlController < ApplicationController def execute variables = ensure_hash(params[:variables]) query = params[:query] operation_name = params[:operationName] context = { current_user: current_user } result = SampleSchema.execute(query, variables: variables, context: context, operation_name: operation_name) render json: result #... end #... end resolve メソッドの中で context に入っているユーザーのログイン情報を見て認証エラーを吐かせています。 GraphQL::ExecutionError はキーワード引数として extensions を持っているのでオリジナルのキー(ここでいう code )を渡すことができます。 def resolve(name:, sex:) raise GraphQL::ExecutionError.new('permission denied', extensions: { code: 'AUTHENTICATION_ERROR' }) unless context[:current_user] #... end そうすることで認証エラーが発生した場合、このようなフォーマットでレスポンスを返すことができます。 " errors ": [ { " message ": " permission denied ", " locations ": [ { " line ": 3 , " column ": 3 } ] , " path ": [ " createUser " ] , " extensions ": { " code ": " AUTHENTICATION_ERROR " } } ] セーフティネット GraphQLのレスポンスはクライアントがパースしやすいようにフォーマットを統一することが重要です。 発生したエラーが最後まで rescue されずにいるとRailsの一般的な500エラーが返ってしまいます。 そうなるとクライアント側はhttp status 200で返ってくるGraphQLのエラーと、Railsの一般的な500エラーの両方をパースする準備をしなければなりません。 そこでサーバーサイドでエラーを最終的に受け止めるセーフティネットを用意したくなります。 graphql-ruby 1.8までは rescue_from メソッドを使ってこれを実現できます。 以下に rescue_from メソッドを使った実装例を示します。 class SampleSchema < GraphQL::Schema rescue_from(StandardError) { 'INTERNAL_SERVER_ERROR' } #... end こうすることでRailsの一般的な500エラーではなく、以下のようなGraphQLのエラーを返すことができます。 " errors ": [ { " message ": " INTERNAL_SERVER_ERROR " , } ] ただ rescue_from の欠点として、 errors 内の message キーの内容しか指定できないという点があります。 これはgraphql-errorsというgemを使って rescue_from にエラークラスのオブジェクトを渡すことで解決します。 GitHub - exAspArk/graphql-errors: Simple error handler for GraphQL Ruby しかしgraphql-rubyの機能として rescue_from にエラーオブジェクトを渡せてもいいのではないかと考えたのでパッチを書きました。 extend GraphQL::Schema::RescueMiddleware#attempt_rescue by masakazutakewaka · Pull Request #2140 · rmosolgo/graphql-ruby · GitHub このパッチは以下のようにブロックに GraphQL::ExecutionError オブジェクトを渡せるようにすることで extensions キーを使えるようにするものです。 class SampleSchema < GraphQL::Schema rescue_from(StandardError) do |message| GraphQL::ExecutionError.new(message, extensions: {code: 'INTERNAL_SERVER_ERROR'}) end #... end またこの rescue_from メソッドはgraphql-ruby 1.9から使えなくなります。 理由は rescue_from メソッドが定義されている GraphQL::Schema::RescueMiddleware クラスがgraphql-ruby 1.9から使えなくなるからです。 GraphQL - Interpreter graphql-ruby 1.9では現状 rescue_from メソッドに変わる何かは存在せず、どのような実装が追加されるかも未定というステータスです。 GraphQL::Execution::Interpreter and rescue_from compatibility · Issue #2139 · rmosolgo/graphql-ruby · GitHub 個人的にはgraphql-errorsがgraphql-rubyに上手く取り込まれてくれたらいいなと思っています。 複数エラー クエリを実行して発生した複数のエラーを1つのレスポンスに含めたい場合があります。 例えばユーザー登録などの複数の入力項目を持つMutationがあったとします。 入力内容が不正であった全ての入力項目にエラーメッセージを表示したい場合、複数のエラーをレスポンスに含めたくなります。 graphql-rubyにおいては GraphQL::Schema::Context#add_error を使うことで複数エラーをレスポンスに含めることができます。 https://www.rubydoc.info/github/rmosolgo/graphql-ruby/GraphQL%2FQuery%2FContext%2FFieldResolutionContext:add_error 以下は実装例です。 module Mutations class CreateUser < GraphQL::Schema::RelayClassicMutation argument :name, String, required: true argument :sex, String, required: true field :user, Types::UserType, null: true def resolve(name:, sex:) user = User.new({ name: name, sex: sex }) if user.save { user: user } else build_errors(user) return # これがないとrescue_fromに拾われてしまう end end def build_errors(user) user.errors.map do |attr, message| message = user[attr] + ' ' + message context.add_error(GraphQL::ExecutionError.new(message, extensions: { code: 'USER_INPUT_ERROR', attribute: attr })) end end end end 複数のエラーを含んだレスポンスは以下のようになります。 " errors ": [ { " message ": " hoge はすでに存在します ", " extensions ": { " code ": " USER_INPUT_ERROR ", " attribute ": " name " } } , { " message ": " fuge は一覧にありません ", " extensions ": { " code ": " USER_INPUT_ERROR ", " attribute ": " sex " } } ] また独自のエラータイプを定義してエラーの内容を data に含めるという方法も存在します。 https://github.com/rmosolgo/graphql-ruby/blob/master/guides/mutations/mutation_errors.md#errors-as-data しかし GraphQL::Schema::Context#add_error を使う方法を以下の理由で採用しました。 GraphQLの仕様上 errors は data と同じレベルに位置してる data の中にエラーを入れる場合、クエリ内にエラーのフィールドを明示的に書かないとエラーの情報を得ることができないのでエラーの受け渡し方として優れていない 独自のエラータイプを定義してエラーの内容を data に含める方法にも以下のような利点があると思います。 エラータイプを定義するのでエラーの構造をスキーマで共有できる クライアント側でエラーメッセージを表示したい場合に、エラーの情報をレスポンスから取り出すのが楽 まとめ この記事ではGraphQLにおけるエラーハンドリングの仕方とgraphql-rubyを使った実装例を紹介しました。 この記事の初めにGraghQLの仕様に軽く触れましたが、GraphQLの仕様はとても簡潔にまとめられているので読むことをお勧めします。 また紹介した graphql-ruby には見やすい場所にドキュメントされていない隠れAPIがあったりするので、ソースコードやissueを読んでみると色々発見できると思います。 この記事の最後の方で紹介した add_error メソッド が隠れAPIの1つです。 将来的にはプロダクションに出してから得られる知見も発信したいです。 GraphQLを使った開発に興味のある方がいましたら、ぜひ以下のリンクからご応募ください。お待ちしております! www.wantedly.com 参考 GraphQL API | Graphcool Docs Full Stack Error Handling with GraphQL + Apollo 🚀 – Apollo GraphQL graphql-ruby/overview.md at master · rmosolgo/graphql-ruby · GitHub graphql-ruby/execution_errors.md at master · rmosolgo/graphql-ruby · GitHub graphql-ruby/mutation_errors.md at master · rmosolgo/graphql-ruby · GitHub
アバター
DroidKaigiで展示したファッションチェックアプリについて こんにちは。ZOZOテクノロジーズ開発部山田( @yshogo87 )です。 DroidKaigi 2019ではプラチナスポンサーとして、ブースを出展させていただきました。 DroidKaigi 2019 そのコンテンツとしてファッションチェックアプリを展示させていただきました。 今回はファッションチェックアプリがどのような仕組みになっているかを説明させていただきます。 ファッションチェックアプリとは ファッションチェックアプリとは、ユーザーが撮影した全身の写真について、WEARに投稿されたコーディネートを元に作成した学習モデルを使用して採点を行うものになっています。 技術的構成 技術的な構成は下記のようになっています。 フロントエンド: Flutter バックエンド: Firebase(ML Kit、Cloud Firestore)、GCP(Cloud Vision API) このファッションチェックアプリは、別のイベントでも使う予定があり、iOSでも動かせるようにする必要があったため、クロスプラットフォームで開発できるFlutterを選択しました。 また、バックエンドもTensorFlow Liteで作成した学習済みモデルをFirebaseにアップロードすることで特別なAPIを作成することなく、SDK経由で簡単に使うことができるためFirebase ML Kitを選択しました。 Cloud Firestoreは検出された結果をログとして保存しています。 FlutterからFirebase ML Kitを使う Firebaseの導入 FirebaseはFlutterから使用することができます。 導入手順は こちら の公式ページをご参照ください。 Firebase ML KitをFlutterで使う Firebase ML KitをFlutterで使うための プラグイン もOSSで公開されています。 今回使用するFirebase ML Kit Custom Modelもこのプラグインに内包されています。このプラグインを使うと、AndroidとiOSを同時に一つのコードで動かすことができるので非常に便利です。 ただしCustom ModelをFlutterから使用する場合には一部状況で注意が必要です。今回はこのプラグインを使用せず実装しました。 FlutterからFirebase ML Kit Custom Modelを使うときの注意点 Custom ModelをFlutterから使用する場合、返却される型に注意が必要です。Dartではfloat型が存在しないので、Custom Modelからの結果がfloat型の場合うまく受け取れません。そこで、KotlinでFirebase ML Kitとのやりとり部分を書いてFlutter側に結果を返すコードを書きました。 実装 FlutterからKotlinのコードを実行する 次のように invokeMethod によってFlutterからKotlinのコードを呼び出し、その実行結果が result に返ってきます。 const platform = const MethodChannel( "firebaseCustomModel#run" ); try { final String result = await platform.invokeMethod( "getResult" , < String , dynamic > { 'imageFile' : cameraImage.path, 'gender' : widget.gender, }).catchError((err) { print( "エラーが発生しました" ); setState(() { _isError = true ; }); }); 次にKotlin側でFlutterからきたデータを受け取ります。 MethodChannel クラスを使ってコールバッククラスを設定することでFlutterから getResult という文字列でリクエストがくるとKotlin側で受け取れます。 override fun onCreate(savedInstanceState: Bundle?) { super .onCreate(savedInstanceState) GeneratedPluginRegistrant.registerWith( this ) MethodChannel(flutterView, "firebaseCustomModel#run" ).setMethodCallHandler( object : MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { val camera = call.argument<String>( "imageFile" ) val bitmap = BitmapFactory.decodeFile(camera) val gender = call.argument< Int >( "gender" ) startCustomModel(bitmap, gender, result) } }) } Flutterから設定されたパラメータは下記のコードで取得しています。 val camera = call.argument<String>( "imageFile" ) val bitmap = BitmapFactory.decodeFile(camera) val gender = call.argument< Int >( "gender" ) Flutterからネイティブのコードを呼び出す方法については公式ページがあるので詳しくは こちら をご覧ください。 Firebase ML Kitからデータを取得する ここからはFirebase ML KitからCustom Modelをダウンロードし、結果を取得します。 下記のコードは Firebase ML Kitのドキュメント を参考に実装しています。 fun startCustomModel(bitmap: Bitmap, gender: Int ?, result: MethodChannel.Result) { var conditionsBuilder: FirebaseModelDownloadConditions.Builder = FirebaseModelDownloadConditions.Builder().requireWifi() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { conditionsBuilder = conditionsBuilder .requireCharging() .requireDeviceIdle() } val conditions = conditionsBuilder.build() val cloudSource = FirebaseCloudModelSource.Builder( "batai" ) .enableModelUpdates( true ) .setInitialDownloadConditions(conditions) .setUpdatesDownloadConditions(conditions) .build() FirebaseModelManager.getInstance().registerCloudModelSource(cloudSource) val modelName = if (gender == 0 ) { "wear_model" } else { "wear_model_women" } val options = FirebaseModelOptions.Builder() .setCloudModelName(modelName) .build() val firebaseInterpreter = FirebaseModelInterpreter.getInstance(options) val inputOutputOptions = FirebaseModelInputOutputOptions.Builder() .setInputFormat( 0 , FirebaseModelDataType.FLOAT32, intArrayOf( 1 , 224 , 224 , 3 )) .setOutputFormat( 0 , FirebaseModelDataType.FLOAT32, intArrayOf( 1 , 1 )) .build() val batchNum = 0 val input = Array( 1 ) { Array( 224 ) { Array( 224 ) { FloatArray( 3 ) } } } for (x in 0 .. 223 ) { for (y in 0 .. 223 ) { val pixel = bitmap.getPixel(x, y) input[batchNum][x][y][ 0 ] = ((Color.red(pixel) - 128 ) / 128 ).toFloat() input[batchNum][x][y][ 1 ] = ((Color.green(pixel) - 128 ) / 128 ).toFloat() input[batchNum][x][y][ 2 ] = ((Color.blue(pixel) - 128 ) / 128 ).toFloat() } } val inputs = FirebaseModelInputs.Builder() .add(input) // add() as many input arrays as your model requires .build() firebaseInterpreter !! .run(inputs, inputOutputOptions) .addOnFailureListener { e -> // Task failed with an exception // ... e.printStackTrace() }.continueWith { task -> val labelProbArray = task.result !! .getOutput<Array<FloatArray>>( 0 ) result.success(getTopLabels(labelProbArray)[ 0 ]) } } private fun getTopLabels(labelProbArray: Array<FloatArray>): List<String> { val data = labelProbArray[ 0 ][ 0 ] val list = ArrayList<String>() list.add( " $data " ) return list } 下記のコードでは、先ほどの説明した通りFlutterではfloat型を受け取れなかったので、KotlinでString型に変換してFlutter側に返却しています。 val labelProbArray = task.result !! .getOutput<Array<FloatArray>>( 0 ) result.success(getTopLabels(labelProbArray)[ 0 ]) private fun getTopLabels(labelProbArray: Array<FloatArray>): List<String> { val data = labelProbArray[ 0 ][ 0 ] val list = ArrayList<String>() list.add( " $data " ) return list } Cloud Vision APIを叩く 開発期間内では学習済みモデルの精度が高められるか不安があったので、Cloud Vision APIも叩いて加点することにしました。 Cloud Vision APIでは写真の情報から写っている画像に対して得られる情報を返すGCPのサービスの1つです。 cloud.google.com ファッションチェックアプリでは、Cloud Visionから「cool」や「fashion」などのファッションチェックとしてプラスになりそうなキーワードが検出されると加点しています。 加点するキーワードについてはCloud Firestoreで管理しています。 Cloud Vision APIを叩くコードは下記になります。 _requestCloudVision(File cameraImage, String result) async { String url = "https://vision.googleapis.com/v1/images:annotate" ; String apiKey = "api key" ; List < int > imageBytes = cameraImage.readAsBytesSync(); Map json = { "requests" : [ { "image" : { "content" : base64Encode(imageBytes)}, "features" : [ { "type" : "LABEL_DETECTION" , "maxResults" : 100 , "model" : "builtin/stable" } ], "imageContext" : { "languageHints" : [] } } ] }; Response response = await http.post(url + "?key=" + apiKey, body : jsonEncode(json), headers : { "Content-Type" : "application/json" }); var body = response.body; print(body); var bodyJson = jsonDecode(response.body); List < dynamic > responces = bodyJson[ "responses" ]; if (responces == null || responces.length == 0 ) { _showErrorDialog( "Label not found" ); return ; } Map < String , dynamic > labelAnnotations = responces[ 0 ]; if (labelAnnotations != null && labelAnnotations.length != 0 ) { List < LabelAnnotationModel > list = [] ; for ( dynamic label in labelAnnotations[ "labelAnnotations" ]) { LabelAnnotationModel model = LabelAnnotationModel.fromJson(label); list.add(model); } } } このコードではlistに検出されたラベル一覧が格納される方法になります。 結果はログとしてCloud Firestoreに保存 撮影した写真以外のデータはCloud Firestoreに保存していてリアルタイムでログを見れるようにするためのアプリも実装しました。 こちらのアプリでは、Flutterだけで実装しているため、AndroidでもiOSの動かすことができます。 ログとして保存したのは下記です。 男女ごとのスコア Cloud Visionから検出されるキーワード キーワードにヒットした時の加点数 Firebase ML Kitから返却される数値の平均点と、Cloud Vision APIからよく出力されるキーワードを監視していました。 開発当初、学習済みモデルの出来が悪く全員が同じ点数になることが危惧されたのでCloud Vision APIからキーワードで加点するように対策をしていましたが、結果的に危惧していた事態は起きず、この機能の出番はありませんでした。 まとめ 本記事ではファッションチェックアプリの仕組みと、Firebase ML Kit、 Cloud Vision APIの簡単な使い方について紹介させていただきました。 ZOZOテクノロジーズでは、技術でファッションを盛り上げてくれる方を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! www.wantedly.com www.wantedly.com
アバター
こんにちは。ZOZOテクノロジーズ開発部の田島です。 今時のシステム開発ではさまざまなツールを利用することが当たり前になっています。 そして各種ツールは日々新しいものが開発され、今まで当たり前だったものがレガシーなツールと呼ばれることも珍しくありません。 弊社では、GitHubやCircleCI・Slackなど様々なツールを利用しています。 私達のチームでもこれらのツールを利用していますが、それ以外にもGitBucketやJenkins・Redmineを独自で管理し利用していました。 今回ある理由からそれらのツールをSaaSへ移行しました。その経緯と移行手順を紹介します。 概要 開発支援サーバの紹介 利用しているGitBucket・Jenkins・Redmineは開発支援サーバと呼ばれる一台のEC2インスタンスの上で動作していました。 やったこと これらのツールを以下の図のように、「GitBucketをGitHub」・「JenkinsをCircleCI」・「RedmineをGitHub issue」へ移行しました。 ツール移行の経緯 なぜ開発支援サーバーが必要だったのか まず開発支援サーバーが必要だった理由を紹介します。以下のような理由から開発支援サーバーの運用を行っていました。 プロジェクト発足当時、全社的に利用するツールが統一されていなかった セキュリティのルールが定まっていなく、SaaSを利用することがためらわれていた 開発支援サーバを利用しているプロジェクトは外部委託しているものだったためツールをコントロールできなかった なぜ移行を行ったのか 次になぜツールの移行を行ったのかを紹介します。以下のような理由から開発支援サーバーのツールの移行を決意しました。 全社的に利用するツールの統一が行われ始めた プロジェクトをまるごと内製化するため自分たちでツールをコントロールできるようになった 同じような役割のツールが複数存在し、情報が分散されてしまっていた EC2に開発支援サーバーを立てているためデータのバックアップなどすべて自分たちでやる必要があった サーバーがダウンすると3つすべてのツールにアクセスできなくなる 最も大きな理由としては、全社的に利用するツールが統一され始めたこと。外部委託をしていたプロジェクトをまるごと内製化し自分たちでツールをコントロールできるようになったことが主な動機となりました。 移行先の選定 それぞれのツールの移行先として、様々なツールを検討しました。最終的にどのような理由から移行先のツールを決定したのかを紹介します。 GitHub GitBucketの移行先をGitHubにした理由は以下になります。 全社的にリモートリポジトリがGitHubに統一された 開発者全員が利用したことのあるツールである 権限管理などが柔軟 SaaSのため自分たちで管理の必要がない 基本的には社内でリモートリポジトリがGitHubに統一されたことが大きな要因になりました。 CircleCI Jenkinsの移行先をCircleCIにした理由は以下になります。 全社的にCircleCIが利用されている チームメンバーがCircleCIに慣れている 設定をコードで管理できる Jenkinsでやっていたことがすべて実現できる SaaSのため自分たちで管理の必要がない チームメンバーがCircleCIに慣れており、かつやりたいことを柔軟に実現できたことが採用の大きな要因になりました。 GitHub issue Redmineの移行先をGitHub issueにした理由は以下になります。 進行中のタスクと完了したタスクを分けることができる 新たにツールの導入が必要ない 検索ができる SaaSのため自分たちで管理の必要がない 全社的にドキュメントはConfluenceにチケット管理はJiraに統一しているので、はじめはどちらかに移行することを検討しました。そしてRedmineの移行先の条件としては進行中のタスクと完了したタスクを分けることのできるツールが好ましいと判断しました。Jiraはこの条件に当てはまりますが、Redmineでは一部の作業履歴などドキュメントのように利用していました。また、Jiraでは工数管理も同時に行っているため移行先として好ましくないと判断しました。そこで今までの作業履歴が参照できれば良い程度の要件だったため、GitHub issueが適当であると判断しました。 ツールの移行 移行手順 ツールの移行は以下の順番で行いました。 GitBucket Jenkins Redmine まずはじめに、GitBucketの移行を行いました。Jenkinsの移行先であるCircleCIではGitHubとの連携を考えていました。そこで先にリポジトリをGitHubを移行する必要がありました。次にJenkinsを移行します。Redmineはどちらのツールにも依存をしていないため最後に移行をしました。以下ではそれぞれのツールの移行手順を紹介します。 GitBucket to GitHub 最初にGitBucketからGitHubへの移行手順を紹介します。 今回移行が必要だったリポジトリは10個ほどだったので無理にスクリプトを作成したりツールを利用せず手作業で移行を行いました。 移行作業では「 Gitリポジトリの中身を、ブランチとタグも含めて別リポジトリにコピーする 」の記事を参考にさせていただきました。 手順は以下の通りです(1〜4は上の記事通りに進めました) GitBucketのリポジトリを手元にcloneする すべてのブランチ・タグを取得 リモートリポジトリにGitHubを追加する GitHubにコンテンツをpush GitBucket上で作成されていたPull RequestをGitHub上で作成 Jenkinsの向き先をGitHubへ変更する 1. GitBucketのリポジトリを手元にcloneする まず手元に移行が必要なGitBucket上のリポジトリをCloneします。 $ git clone GitBucketのリポジトリ 2. すべてのブランチ・タグを取得 次に全てのブランチ・タグを取得します。 $ git branch -r | grep -v " \- > " | grep -v master | while read remote; do git branch --track " ${remote # origin/ } " " $remote " ; done $ git fetch --all $ git pull --all 3. リモートリポジトリにGitHubを追加する リモートリポジトリにGitHubを追加します。 origin の部分はGitBucketと同じものにして上書きしても別にしても作業しやすいものを指定してください。 $ git remote set -url origin GitHubのリポジトリ 4. GitHubにコンテンツをpush GitHubにコンテンツをpushします。 $ git push --all origin $ git push --tags 5. GitBucket上で作成されていたPull RequestをGitHub上で作成 GitBucket上で作成されていたPullリクエストをGitHubで作成します。 こちらも、数が多くなかったので手動で行いました。 6. Jenkinsの向き先をGitHubへ変更する 利用しているJenkinsではGitBucketからコードをPullしてビルドを行っていました。 次の章でJenkinsをCircleCIに移行しますが、移行が完了するまでJenkinsのリポジトリの参照先をGitHubへ変更する必要があります。 以下がその手順です。 GitHub用のデプロイキーを作成 github用のデプロイキーをリポジトリの数だけ発行します。以下では github-repository1 、 github-repository2 リポジトリ例でのデプロイキー作成の例になります。 $ ssh-keygen -l -f github-repository1.pem $ ssh-keygen -l -f github-repository2.pem .ssh/configにHostを追加 ~/.ssh/configの設定を発行したデプロイキーの数だけ追加します。 Host github-repository1 User git Port 22 HostName github.com IdentityFile ~/.ssh/github-repository1.pem TCPKeepAlive yes IdentitiesOnly yes Host github-repository2 User git Port 22 HostName github.com IdentityFile ~/.ssh/github-repository2.pem TCPKeepAlive yes IdentitiesOnly yes GitHubへデプロイキーを配置 GitHubの各リポジトリのSettingsでデプロイキーを登録します。 Jenkinsの設定を変更 最後にJenkinsのプロジェクトの設定から、ソースコード管理の欄を変更します。 Repository URLには先ほど。 .ssh/config で設定したHost名を利用します。またCredentialsにはデプロイキーを発行したユーザーと紐付いたものを利用します。 それぞれの項目の例は以下のようになります。 RepositoryURL: git@github-repository1:user_name/repository_name Credentials: Jenkins Jenkins to CircleCI 次にJenkinsをCircleCIへ移行します。今回Jenkinsで扱っているプロジェクトはすべてMavenで管理されたJavaアプリケーションでした。 そこで、以下の手順で移行を行いました。 Jenkinsでやっていることを確認 JenkinsでやっていることをCicrcleCIで再現 CircleCIによってビルドされたファイルの動作確認 1. Jenkinsでやっていることを確認 まずはじめにJenkinsでやっていることを確認する必要があります。 Jenkinsの各Mavenプロジェクトの「設定」で何をやっているのか把握を行いました。 1. Mavenでやっていることを確認 Jenkinsで扱っていたプロジェクトはすべてMavenプロジェクトのためビルド項目の「ルートPOM」と、「ゴールとオプション」を調べます。これによりJenkinsで実行されるMavenのゴール・オプションがわかります。以下の例では、単純に mvn clean package を行っていることがわかります。 2. JDKのバージョン確認 JavaのバージョンをCircleCI移行時に合わせる必要があるので、ビルド時のJDKのバージョンを調べておく必要があります。以下の例では、「OpenJDK 1.8」が利用されていることがわかりました。 3. ビルド・トリガの確認 どのようなタイミングでビルドが行われるかを確認します。私達のプロジェクトでは以下のように、定期的にビルドを実行するような設定になっていました。 4. 成果物の確認 最後にJenkinsの実行によって生成される成果物を確認します。CircleCI上でも同じように成果物を出力する必要があるため調べておく必要があります。 2. JenkinsでやっていることをCicrcleCIで再現 次にJenkinsでやっている内容をCircleCIで再現します。以下がその例になります。やっている内容はコメントをご参照ください。 version : 2.1 jobs : build-job : # Jenkinsサーバーがcentos7だったため同じOSを利用する docker : - image : centos:7 user : root working_directory : ~/repo # Jenkinsサーバーと同じ環境変数をセット environment : LANG : ja_JP.UTF-8 LANGUAGE : ja_JP:ja TZ : Asia/Tokyo steps : - checkout - run : name : 'Setup' command : | cp /usr/share/zoneinfo/Japan /etc/localtime # OpenJDKとMavenをインストール # localtimeの設定 - run : name : 'Install Dependencies' command : | yum -y install java-1.8.0-openjdk yum -y install maven # 依存ライブラリをローカルにダウンロード # NOTE : mvn dependency:go-offline command has a bug. Refer to https://issues.apache.org/jira/browse/MDEP-516 # - run: # name: 'Install dependency maven' # command: mvn dependency:go-offline # テストを実行 - run : name : 'Test' command : mvn clean test # パッケージング - run : name : 'Package' command : mvn package -Dmaven.test.skip=true # 成果物をCircleCI上の画面からダウンロードできるようにする - run : name : 'Set Artifacts' command : | mkdir /tmp/artifacts cp artifact.jar /tmp/artifacts - store_artifacts : path : /tmp/artifacts workflows : version : 2 build-deploy : jobs : - build-job Jenkinsでは mvn clean package のみを行っていました。しかし、CircleCIでは失敗した場合にどのタイミングで失敗したのかがわかりやすいようにテストとパッケージングを別々にしました。また、依存ライブラリのインストールも別にしたかったのですが問題が生じて断念しました。1つのプロジェクトで複数のMavenプロジェクトに分けている場合 mvn dependency:go-offline で失敗してしまいます。詳細は こちらのissue をご参照ください。 3. CIによってビルドされたファイルの動作確認 JenkinsでビルドしたjarとCircleCIでビルドしたjarを100%同じものにすることはできませんでした。そこで動作確認を開発環境やステージング環境できちんと検証する必要があります。検証ができたものから順次本番のjarをCircleCIでビルドされたものに切り替えて行きます。 Redmine to GitHub issue 最後にRedmineをGitHub issueへ移行する方法を紹介します。手順は以下のようになります。 移行のためのツールを探す RedmineとGithubの設定をする 移行ツールをfetchして自分好みにカスタマイズ redmine2githubを実行 1. 移行のためのツールを探す Redmineのチケットは300ほどあったため、流石にこれを手作業で移行すると日がくれてしまいます。そこで、これに関しては自動化することを検討しました。 今どきRedmineからどこかに移行するというニッチなツールが無く、最初は自分で作ろうと思いました。しかし、redmine2githubという素晴らしいツールのおかげで最小限の力で移行を実現できました。 以下がredmineからGitHubへ移行するためのツールで、Ahmy YulrizkaさんがGitHubで公開しています。利用するタイミングではライセンスが書かれていなかったのですが、事情を説明したところ丁寧にライセンスの付与をしていただくことができました。ちなみにライセンスはMITライセンスです。 https://github.com/yulrizka/redmine2github このツールでできないこと 以下のことはredmine2githubでは実現できません。しかし、今回の要件では作業履歴等が参照できれば良いという程度の要件だったためこのツールを利用することにしました。 ファイルの移行 Redmine上の他のチケットへのリンクの参照をGitHub issueに合わせて変換を行う 2. RedmineとGithubの設定をする redmine2githubを利用するために、Redmine及びGitHubの設定を行う必要があります。 Redmine 管理 > 認証のページから「RESTによるWebサービスを有効にする」 管理画面の承認ページから「RESTによるWebサービスを有効にする」を有効にします。 個人設定のAPIアクセスキーをメモ 個人設定の画面からAPIアクセスキーをメモします。これはredmine2github実行時に指定する必要があります。 csvをダウンロード チケット一覧のページで必要なチケットだけ絞り込んだら、以下の画面右下のCSVを選択しチケットの一覧をダウンロードします。 github GitHub側の設定はissueを作成できる権限のあるユーザーの作成が必要です。 各個人のユーザーでもかいまいませんが、そのユーザーとしてissueが作成されます。 3. 移行ツールをcloneして自分好みにカスタマイズ Redmineを日本語で利用している場合は、以下のようにカスタマイズしないとカテゴリや題名などを取得できません。 https://github.com/katsuyan-stt/redmine2github/pull/2/files#diff-222c979e694bb42a7f8468f67bbaddf0R111 redmine2githubは1ファイルのRubyスクリプトでできており、簡単にカスタマイズが可能です。自分のRedmine環境に合わせてカスタマイズして利用してください。 4. 実行 最後にredmine2github.rbを実行します。 オプションはredmine2githubの README を参照してください。 ruby redmine2github.rb リポジトリのユーザ/組織 リポジトリ名 ダウンロードしたCSVのパス -e 上の3でメモしたAPIキー,RedmineのURL -m -s -v -c 完了 移行したことによる意外なメリット ツールの移行によってEC2の料金や管理コストが減ることなどは考えていましたが、他にも以下のようなメリットがありました。 コードや問題の把握が加速した コードレビューがより活発になった CIツールの移行によってプロジェクトの理解が深まった コード管理やチケット管理を普段使い慣れているGitHubに移行したことにより、コードや問題の把握が加速しました。また、GitHubにコード管理が統一されたことによってコードレビューがより活発になりました。そして一番のメリットは、CIツールの移行によりプロジェクトの理解が深まったことです。CIツールの移行にはプロジェクトのビルドだけでなく、アプリケーションにもある程度詳しくなる必要があります。これにより、プロジェクトの理解を深めることができました。 今後の展望 以上のようにツール移行によりさまざまなメリットがありました。しかし、まだまだ改善の余地があり将来的には以下の実現を考えています。 CircleCIを利用した継続的デプロイメント(CD)の実現 今回CIをJenkinsからCircleCIに移行しましたが、Jenkinsでやっていたことを移行したのみで継続的デプロイメントの実現はできませんでした。もともとCDをしようという動きがないプロジェクトだったため簡単には実現できません。しかし、よりリリースのサイクルを早めユーザーに価値を提供できるようCDの実現を試みています。 まとめ 本記事ではEC2で管理していたツールをSaaSへ移行したことについて紹介しました。RedmineからGitHub issueへの移行など少しニッチな内容も含まれていましたが、少しでもお役に立てれば幸いです。 ツールの改善は直接ユーザーに価値を提供することはできません。しかし、チームや会社全体の作業効率やサービスの品質を高めるなど間接的にユーザーへの価値提供を加速させることができます。弊社では、チームや会社全体の業務の仕組み化を自ら進んで考えることができるエンジニアを募集しています。興味がある方は以下のリンクからぜひご応募ください。 www.wantedly.com
アバター