株式会社ZOZOのブログ - TECH PLAY

TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

1025

はじめに こんにちは、計測プラットフォーム部の歌代です。 普段はZOZOSUITやZOZOMATといった計測系プロダクトの「計測」に関わる部分の検証や、新規プロダクトに必要なデータ集め、また精度検証などサービス構築から、UI/UXの分析・評価など幅広く業務を行っています。 2020年2月にリリースした足計測ツール、ZOZOMATでは足のサイズをスマートフォンで測るという新しい体験をサービスとして提供開始しました。新たな体験にも関わらず、事前に工夫をすることで、使い方に関するユーザーからの問い合わせ件数を大きく抑えることに成功しました。 今回はZOZOMATリリースの際に、問い合わせ数削減につなげるために行った工夫や事前調査の内容を紹介します。 ZOZOMATとは 既に他のテックブログの記事でも紹介していますが、ZOZOMATは足のサイズを計測するために開発された足専用の計測マットです。 計測者自身のスマートフォンとZOZOMATを使って計測した情報を元に、靴の推奨サイズを提案するサービスを利用可能です。まだお試しいただけていない方、ご興味のある方は無料で配布中なのでぜひこちらをご確認ください。 zozo.jp 2021年1月末現在、ZOZOMATの計測件数は130万件以上あるのに対し、問い合わせ件数は数十件程度に留まっています。問い合わせ発生率としては極めて少なく、その中でもほとんど開発チームが関わる必要のある問い合わせがありませんでした。 ZOZOMATでは、どのようにして問い合わせ件数を抑え込むことができたのでしょうか。実施した内容は至ってシンプル、そしてやって当然な内容も多々ありますが、「事前にやっておいてよかった」と思うものがいくつもあります。ここではそれらの中からいくつかピックアップしてご紹介します。 ZOZOMAT開発時の工夫点 UI/UX改善を開発初期から行える体制を構築する 元々、私は品質管理部に在籍していました。そこでZOZOSUITの精度検証や端末疎通チェックをしていました。 ZOZOSUITを検証する際、すでに実装まで完了したUI/UXに対し、調査を実施し、その過程で「ここがわかりにくい」「ここが伝わりにくい」という情報を把握していました。社内で最も多くZOZOSUITの計測を経験したのは間違いなく私だと思います。 ZOZOSUITの精度検証の過程で気がついた点を、開発チームやUI/UXを担当するデザイナー、サービスチームにフィードバックしていたところ、現在の上司から「サービスを開発する段階からUI/UXについて意見をもらえないか」と声をかけられ、現在のチームに異動することになりました。 ZOZOSUITの計測を多く経験した人の声を、ZOZOMATのデザインや開発にいち早く反映させるために、チーム構成から変更されました。これにより、より良いUI/UXのためのアプローチをしやすい体制になりました。 ユーザビリティテストで問題点を見つける ユーザビリティテストは、読者の中にも実施されている方もいらっしゃるでしょう。一言で表すと「サービスやアプリの使い勝手の部分を評価するテスト」です。 「サービスを使って目的を果たせているか?」「チュートリアル動画はわかりやすいか?」「エラーメッセージでユーザーの不安を払拭できるか?」などを、社内スタッフや外部被験者を使って、評価・テストを行います。 ユーザビリティテストは、突き詰めていくとユーザビリティテストを専門に行う会社ができるほど、奥深いものです。しかし、簡易的なものでも十分効果があります。ユーザビリティテストだけではありませんが、テストはやった分だけ何かしらの成果・結果が伴ってきます。その中でも、リリース前のユーザビリティテストはその効果が大きいテストです。 ユーザビリティテストの目的は「使い勝手に影響を与える大きな問題(大勢の人間がよく間違う箇所)」を見つけることです。例えば10人テストして、10人がそれぞれ違うミスをしていた場合、それは被験者個別の問題であって、サービス・アプリの問題ではない可能性が高いです。しかし、逆に10人テストして、8人が同じ場所で同じミスをしていた場合、それはサービス・アプリのその該当箇所に問題があると言えます。 開発の試作段階のものを評価したい際や事業の方向性に関わる重要な決定する際には、サービスの詳細を知らされていない社内スタッフに依頼、ユーザビリティテストを実施し、スピーディーに問題点を洗い出し、サービスチームや開発、デザイナーにフィードバックします。すべて社内で完結させることにより、早いサイクルで実施できます。 ここでのポイントは、開発が完了したものに対して、ユーザビリティテストを実施するのではなく、開発途中の段階でユーザビリティテストを実施することです。開発終盤での修正は大きな手戻りが発生するため、リソース・時間的に追い込まれ、精神的な負担も大きくなります。その負担が発生しないよう、ZOZOMATプロジェクトでは開発と並行してユーザビリティテストを実施しました。開発やサービスの方向性の変更や決断を容易に行うことができ、サービス品質の向上に大きく寄与しました。 それでは実際にZOZOMATで行ったユーザビリティテストを例に、ポイントを説明します。 ユーザビリティテスト実施時のポイント UI/UXの工夫 実際にZOZOMATではユーザビリティテストを使った検証をいくつも実施しました。その中でも開発当初には「UI/UX」に関するユーザビリティテストを行いました。 ZOZOMATは開発当初、2種類のUI/UXデザイン案があり、どちらにするべきかサービスチームを中心に議論が行われていました。 開発担当、サービス担当、デザイン担当と議論した結果、2種のデザインはそれぞれに良さがあり、またそれぞれに弱点があるように思えました。 そこで社内と社外の両方から被験者を招き、テストの目的の詳細を説明しないまま、計測成功率を調査しました。 A案:6方向から足をスキャンする際、スマホ画面を計測するエリアの色に着色して、計測方向の指示を出すもの B案:スキャンの際、スマートフォンの画面上に、ZOZOMATの緑色の足形を表示させ、実際のZOZOMATの緑色の足形と重ねるもの 被験者は20〜30代、40〜50代、60代以上の3グループで男女5名ずつ、合計30人を集めました。そして、A案→B案の順でテストを実施するAチームと、B案→A案の順でテストを実施するBチームに分け、両方のUI/UXを試してもらい、成功率の比較とどちらがわかりやすかったかのインタビューをしました。 結果として、B案に比べA案の方が成功率が高く、またユーザーインタビューからもA案の方がわかりやすいという意見があり、ZOZOMATはA案のUI/UXを採用することになりました。 A案はあまり難しいことを考えずに、計測する足の周りを6方向からスキャンするという直感的でわかりやすいという特徴がありました。その結果、B案をブラッシュアップしてコストをかけるのではなく、A案にリソースを集中させることができました。開発の早い段階で、より効率の良い選択肢を選べました。その結果、リソースを有効活用してサービスの作り込みをすることにより、問い合わせ発生率を大幅に低く抑えることができました。 ユーザビリティテストは、問題点を見つけるだけでなく、サービスやUI/UXの方針を客観的に評価でき、リリース後のサービスの姿を先に垣間見ることができます。そのため、入念にユーザビリティテストを実施することは、非常に重要だと言えます。 Androidの複数端末疎通チェックによる動作確認 ZOZOTOWNのアプリはiOSとAndroidの両方で提供しています。そして、ZOZOMATはそのアプリ内で利用できます。 開発の流れは、まず初めに端末モデルに機能や仕様に差の少ないiOSのアプリを中心に組み立てられ、その次に同じ仕様をAndroidにコンバートしていきます。 iOSではほとんど端末モデル間に差分がありませんが、Androidは国内外の様々なメーカーで製造されているため、機種によるスペックの違いや設定の違いなどが顕著です。そのため、アプリに対して、どのような影響・違いがあるのかを確認するテストが必要となり、それをAndroid複数端末疎通チェックと呼んで実施しています。 ZOZOTOWNでよく利用されるAndroid端末を社内検証機として一部保有していますが、世の中の全種類を網羅することは現実味がありません。不足する部分は外部の検証機関の協力も得ながら、全アクセスの95〜99%をカバーできるよう、端末検証を実施しています。 ZOZOMATのテストを実施していると、一部の機種でカメラの挙動の影響により開発の仕様とは異なるスキャン結果が返され、うまく計測できないということがリリース前に発覚しました。このテストをリリース前に実施することで、想定通りの動作をしない端末が存在することを把握し、そのようなアプリが対応しきれない機種がある場合はヘルプやお知らせなどでユーザーに周知することができます。改修コスト次第では、テストの結果を受け、リリースまでに原因調査と改修をすることも可能です。 また、Androidはモデルにより端末のスペック差が大きく、計測はできても、足の計測にかかる計算時に想定以上の時間を要する場合もあります。そのような端末についても事前にテストをして把握しておくことで、ヘルプなどにその旨を記載できるほか、サポートデスクへ事前に情報共有をし、いざ問い合わせがあった場合でも、その情報を元に適切な対処法を案内できます。 このようなテストを時間を確保して実施した結果、リリース前に問題が発生しそうな端末に関する情報を得ることができ、リリース後に大きな混乱や慌ただしい追加調査が必要となることがありませんでした。 エラーメッセージの整理 ZOZOMATの開発時に、UI/UXに次いで議論したテーマが「音声メッセージ」「エラーメッセージ」の伝え方です。 ZOZOMATの計測は、ユーザーがスマートフォンを片手で持った状態で、計測する足の周囲を6箇所、膝の高さから、スマートフォンをやや斜めに向けてスキャンしてもらいます。 上記の説明を読んでもなかなか実際の光景をイメージすることは難しいかと思います。実際の計測時に、この「姿勢とスマートフォンの角度と距離」を正確に伝えることにとても苦戦していました。 ZOZOMATの注文画面に掲載しているイメージ映像や、アプリ起動後のチュートリアル動画で、実際の計測方法を動画で伝えています。 しかし、実際にユーザーが計測する際には以下の2つの想定と異なった計測をしてしまうことが想定されました。 スマートフォンを横向きに持ってスキャンしてしまう スマートフォンをマットに対して水平に持ってしまう ZOZOMATのUI/UXの特徴として、ZOZOSUITの計測時でも利用していた「音声案内による正しい計測姿勢への誘導」があげられます。「スキャン位置が遠すぎる/近すぎる」のようなエラーは検出も容易で、かつ音声メッセージでの修正が可能でした。 しかし、「横持ち」や「水平持ち」はシステムによるエラー検出が難しく、また音声案内で正しい持ち方を案内しても、テストをしてみると「ユーザーは案内されている通りの正しい持ち方をしてるつもりであり、どこが間違えているのかイメージが湧かない」という理由で、音声案内がより混乱を招くという事態に陥っていました。 そこでポップアップによる「正しい計測姿勢の案内」を特定の間違いを連続で検出した際に表示する仕組みを実験してみました。チュートリアル動画で案内した内容をもう一度、ポップアップ画面で案内するという仕様にしましたが、それでも「何が間違っているのかわからない」というテスト結果でした。人間は一度思い込むとなかなかそれを修正することが難しいということを実感させられました。 ポップアップでは、正しい計測姿勢や持ち方を案内した後、最もよく発生する「横持ち」と「水平持ち」ではなく、正しいスマートフォンの持ち方や角度を改めてユーザーに静止画で提示する仕様に切り替えました。これにより、足に対してスマートフォンは「縦向き」「少し斜めにしてスキャンする」ことが、ようやくほぼすべてのユーザーに伝わるようになりました。 エラーメッセージの伝え方はとても難しいものです。すべての発生し得る行動を予測し、丁寧に説明していると、過剰な説明によってユーザーに余計な負担をかける事になります。「アプリとしての機能や使い方を適切に伝えつつも、説明は最低限にとどめておき、直感的でわかりやすいUI/UX」が理想的です。 最後に 本記事で紹介した内容は、特別なスキルや経験を必要とするものではなく、どのサービスでも導入できるものです。もちろんスキルを身につけることによって精度が向上していきますが、第一歩として導入するだけでも効果が得られます。テストは、開発やシステム構築が完成した最後の工程と思われがちですが、開発と並行してテストを実施することで、様々なメリットがあることが今回のZOZOMATでの検証を通して体験できました。 目的とスピード感を持って検証に臨めば、開発やデザインの手戻りといった余計な工数を削減でき、同じ開発期間であってもさらに効率的・効果的にサービスを磨き上げることができます。 皆様も現在開発中の案件があれば、開発と並行したユーザビリティテストを実施してみてはいかがでしょうか。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
はじめに こんにちは、ZOZO研究所の平川( @china_syuke )です。 ZOZO研究所では今年度から、ファッションコーディネートアプリ「 WEAR (ウェア)」のデータを用いた調査リリースを執筆しています。一般的によく見るアンケート調査と違い、機械学習を用いてこれまで数値化されていない情報を調査しました。 この記事では、リリースした中でも面白いアプローチで調査した、第二弾「洋服の「丈」に関する流行の変化」に焦点を当てながら調査リリースの進め方・工夫したこと・課題に感じたことを紹介します。 https://press-tech.zozo.com/entry/20200820_WEAR_Research2 press-tech.zozo.com 目次 はじめに 目次 機械学習を用いた調査リリースの執筆工程 保有しているデータから仮説を立てる 統計処理のために数値化すべき項目を検討する 数値化するための機械学習の手法を検討し、仮説を立証するための筋道を立てる ターゲットの読者に伝わりやすい文章へ変換する 調査リリースの醍醐味 調査リリースの課題 短期間で調査するための道具の整備が必要 思い通りの結果にならない 仮説の質を上げる まとめ さいごに 機械学習を用いた調査リリースの執筆工程 最近では様々な企業が自社でのアンケートや蓄積されたデータを使用した調査リリースを出しています。サービスなどで蓄積されたデータを解析していくことは、消費者の動向や社会情勢を分析していく上でもとても意義のある調査です。また、自社サービスの宣伝や保有しているデータのアピールにも繋がります。 今回の記事では以下に注目して順を追って執筆の工程を説明していきます。 保有しているデータから仮説を立てる 統計処理のために数値化すべき項目を検討する 数値化するための機械学習の手法を検討し、仮説を立証するための筋道を立てる ターゲットの読者に伝わりやすい文章へ変換する 保有しているデータから仮説を立てる 通常の調査リリースでは、アンケートを調査対象として仮説を立てていくことが多いです。今回の調査リリースではアンケートは集計せずに、自社サービスであるWEARに投稿されたコーディネート画像を利用して仮説を立てていきます。 WEARに投稿されたコーディネート画像から近年どのようにファッションが変化しているか仮説を立てる 少量のサンプリングした画像データで仮説立てした傾向を確認していく 何か面白い変化が出そうであればもっと深堀していく もちろん立てた仮説が上手く立証できることは少なく、1と2の手順を繰り返し行いブラッシュアップしていきます。 今回の例で言うと、「最近はトップス短め/ボトムス長めの傾向がある」という仮説のもと小規模のデータでどのような傾向が出るか確認をしていきます。そして、何か面白いものが見えてきそうであれば、実際にデータの範囲を広げより細かな調査をしていきます。 統計処理のために数値化すべき項目を検討する アンケート調査の場合はすでに数値として結果が出ているため、統計データとして扱って執筆が行えます。画像データの場合はそのままでは数値として扱うことができません。統計による集計をするためにまずは「画像データから何を数値化したら調査を行えるのか」を検討します。 今回は「コーディネート画像上でのトップスとボトムスの比率」を知ることができれば良いので、それを得るための解き方を考えていきます。 数値化するための機械学習の手法を検討し、仮説を立証するための筋道を立てる 画像データを数値化するための機械学習の手法を検討していきます。今回の例で言うと、以下のような仮説立証への筋道が考えられます。 トップスとボトムスの画像上の比率を知る → 洋服の領域検出でトップスとボトムスの矩形を取得 画像上での被写体の身長を知る → 骨格検出で画像上の各部位の長さを定義 1,2より、画像上での身長に対するトップスとボトムスの比率を算出 ここで、「なぜ実際の商品サイズや検出された矩形の長さをそのまま扱うのではなく骨格検出などを用いて比率を出したのか?」という疑問が生じます。コーディネート画像から洋服のサイズを検出する際には以下のような状況が考えられます。 ポーズの影響を受ける タックインなどで本来の丈の長さより短く着こなしている場合がある 被写体の身長・撮影位置は一定ではないので検出された矩形だけでは比較できない これらを解決するために骨格検出を採用しました。 このように、1つの調査に対して様々な手法を組み合わせて問題を解いていきます。 ターゲットの読者に伝わりやすい文章へ変換する アンケート調査と違い機械学習を用いた調査は、調査工程が複雑になります。調査リリースの読者ターゲットは幅広く、弊社の場合はファッション関係のリリースのため技術系の読者でないことも想定されます。いかに前提知識のない読者に対して分かりやすく伝えるかが重要になってきます。 上図で示したように画像を用いて視覚的に分かりやすく説明するなど「可視化」を意識して記事を書くよう心がけました。 調査リリースの醍醐味 調査リリースの醍醐味は「目には見えないファッションの流行を数値化し、複雑な内容を分かりやすく読者に伝えられる」ことです。 今まで街角などで「こういうコーディネートや色が増えてきたなぁ」と感覚として感じていたものが実際に数値として出てファッションの動向も観測できました。機械学習をファッションと繋げてイメージしやすく伝えることで、難しく感じる機械学習という分野がより親しみやすくなり興味を持つきっかけになることを感じました。また、ファッションの動向は社会の動向にも密接に関係していると言われます。調査で出た数値が社会とどのような関係を持っているのかを読み解いていく過程も醍醐味の1つと言えます。 調査リリースの課題 これまでに調査リリースを3本出せましたが様々な課題が見つかりました。 短期間で調査するための道具の整備が必要 四半期に1回のリリースを目標としていたため、仮説立てからリリースまでのスケジュールが3か月程度でした。 調査リリースごとに仮説が異なるため、使用する機械学習の手法も異なります。多様な手法を取り入れることは、それぞれに対して調査が必要になるためスケジュールの遅延が発生しやすいです。そのため、様々な手法を短期間で扱えるよう機械学習のライブラリを準備しておく必要があります。また、画像の背景除去に使用するセマンティックセグメンテーションなどの前処理は非常に時間がかかります。そのような処理を高速化できる環境の整備をしていく必要があります。 思い通りの結果にならない これはどの調査リリースにも言えるのですが、仮説が必ずしも正しいとは限りません。 今回で言うと「最近はトップス短め/ボトムス長めの傾向がある」という仮説に対し、「トップス短め/長めの二極化している」という結果が得られました。もし得られた結果からうまくストーリーが立てられないと調査期間を延ばす、もしくは仮説自体を変更するという選択をしないといけません。 このようなリスクを回避するために初めから仮説を複数用意しておくか、下記の「 仮説の質を上げる 」ことが必要になってきます。 仮説の質を上げる 結論を言うと、調査リリースのキモは仮説の質を上げるに尽きます。 今回の調査で得られた、年毎のトップス丈の割合推移の図を紹介します。 図. トップスが短め/長めの二極化した結果を表すグラフ 年毎にグラフの山が左へ移動しトップス丈が短くなることを予想しましたが、実際はその山が徐々に二極化していくという面白い様子が観測できます。 当初は「トップス短め/ボトムス長めの傾向」という仮説でした。調査の工程で単純な商品サイズのデータではなく、画像から読み取れるコーディネートの比率に焦点を当て、コーディネートの傾向も考慮するように仮説の質を高めていきました。このように当初の仮説の質を上げることはストーリーの肉付けに繋がり、より面白いストーリーを構築できます。機械学習を用いた調査リリースは、調査を細分化する傾向があるので仮説の質を上げやすいと感じました。 仮説を細分化して質を上げると、仮説を複数検討しながら調査できるので、結果的に仮説の立て直しの手戻りを減らすことが期待できます。 まとめ 通常のアンケートベースの調査リリースと違い、機械学習を用いて数値化されていないデータを調査することは調査の難易度が上がります。機械学習を使用することで調査スケジュールは不安定になり、アンケートベース以上の課題が生まれました。しかし、数値のみのデータでは観測できないより深い調査が行えるため、仮説自体も質が高くなり面白いストーリーに仕上げることができると感じました。 この取り組みを通して研究所が何を行っているかをよりライトに発信できる仕組み作りができました。運用フローの課題を改善しつつ、ファッションの動向を素早く感知できるようなプラットフォームを開発して、より読者に興味を持ってもらえる調査リリースを出していきたいです。 今回取り上げた記事の他にも調査リリースを出しているので読んでみてください。 https://press-tech.zozo.com/entry/20200610_WEAR_Resarch1 press-tech.zozo.com https://press-tech.zozo.com/entry/20201218_WEARsearch3 press-tech.zozo.com さいごに ファッションの動向は社会・経済・様々なことに密接に関係し変化していきます。ZOZO研究所には様々な観点からファッションに関する謎を解明する環境が整っております。 ZOZO研究所ではMLエンジニア、バックエンドエンジニアのメンバーを募集しております。今回紹介した調査リリースもまだまだ始まったばかりの取り組みで、一緒に調査していただけるメンバーを募集しております。ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co https://hrmos.co/pages/zozo/jobs/0000029 hrmos.co
こんにちは。ECプラットフォーム部データエンジニアの遠藤です。現在、私は推薦基盤チームに所属して、データ集計基盤の運用やDMP・広告まわりのデータエンジニアリングなどに従事しています。 以前、私たちのチームではクエリ管理に Looker を導入することで、データガバナンスを効かせたデータ集計基盤を実現しました。詳細は、以前紹介したデータ集計基盤については以下の過去記事をご覧ください。 techblog.zozo.com 本記事では、データ集計基盤に「データバリデーション」の機能を加えて常に正確なデータ集計を行えるように改良する手段をお伝えします。 データバリデーションとは バリデーション導入後のデータ集計基盤 ジョブネット構築 テンプレートによる効率的なDAGの作成 DAG間の依存関係の設定方法 バリデーションDAGのタスク構成 まとめ データバリデーションとは データバリデーションとはデータの安全性・妥当性を確認・検証することです。データバリデーションは以下の視点から検証することが一般的です。 データ型:型(Int・Stringなど)として妥当なデータか データ形式:データの形式(「空値が許されるか?」「指定された範囲内の値であるか?」など)として妥当なデータか ビジネスロジック:開発時に定めた集計定義的に妥当なデータか 今回は上記に示された3つの観点から、絶えず流れてくるデータが集計仕様どおりになっているかを検証することでデータ集計における正確性向上へのさらなる改善を図ります。 RDBではテーブルの各カラムにおけるデータの型があらかじめ定められているので、「データ型」における妥当性はデータ集計開始時には既にクリアしています。一方、「データ形式」・「ビジネスロジック」における妥当性は依然不明であり、データ集計前に何らかの方法で検証することが必要です。 バリデーション導入後のデータ集計基盤 データマートの更新は、GCPのApache AirflowマネージドサービスであるCloud Composerを用いたデータ集計基盤を構築することで実現しています。Apache AirflowはPythonで定義したワークフローをスケジュール・モニタリングするためのプラットフォームです。 なお、Cloud Composer・Apache Airflowの詳しい説明はここでは省きます。 Cloud Composer公式サイト ・ Airflow公式サイト をそれぞれご覧ください。 まず、BigQueryへのデータ取り込み完了直後にCloud Pub/SubをKickして、Cloud Functions経由でデータ集計基盤を起動させます。 従来は起動後すぐにデータマートを更新していましたが、今回はデータマート更新で用いるデータ群が全て妥当なデータであることを確認してからデータマートを更新するようにします。以下の図は、データ取り込み完了後からCloud Composer内の処理までを、データバリデーション導入前後で比較したものです。 データバリデーション導入前後のフローを比較すると、導入後はジョブネット構築・バリデーションが主に追加されています。これらを次項で説明します。 ジョブネット構築 Cloud Composerでは、個々の処理をタスクと呼び、タスクに名前をつけ処理内容を記述します。そして、それらのタスクの依存関係をDAG(有向非巡回グラフ)で定義することによりワークフローを構築します。DAGの作成方法の詳細は Cloud Composer公式ページ内の「DAG(ワークフロー)の作成」 をご覧ください。 基本的にCloud Composerにおけるワークフローは1つのDAG内で定義します。しかし、ワークフローの規模が大きくなるにつれてタスクの依存関係が複雑になりDAGが肥大化するといった管理面でのデメリットが発生します。 DAGの肥大化を防ぐため、ワークフローを複数のDAGを組み合わせたジョブネットとして定義するように構築します。なお、ジョブネットとは一般的には実行順序を指定した1つ以上のジョブの集まりのことを指します。 ジョブネットでは2種類のDAGを作成します。 バリデーションDAG:バリデーションのタスクを行うDAG。バリデーション項目数と同じ数だけ作成される。 データマート更新DAG:データマート更新のタスクを行うDAG。依存するバリデーションDAG全てが正常終了しなければ起動しないようにする。更新するデータマート数と同じ数だけ作成される。 バリデーションDAGとデータマート更新DAG間の依存関係はワークフロー内で自動的に設定するようにします。ワークフロー全体の最初のステップとして、この依存関係を把握してジョブネットを構築します。 具体的に言うと、バリデーション・データマート更新でそれぞれ使用する全クエリを取得してクエリ構文を解析します。以下の図のように、データマート更新クエリから集計元テーブルを割り出し、その集計元テーブルと各バリデーションのクエリで用いるテーブルを一致させることで依存するDAG同士を結びつけていきます。 このように、クエリ解析で得られたDAG間の依存関係データはCloud Composer環境にカスタムプラグインとしてインストールすることで、実行順序が動的に変化するジョブネットを構築しました。 さて、Cloud Composerでジョブネットを構築するにあたり、以下の工夫した2点について解説します。 テンプレートによる効率的なDAGの作成 DAG間の依存関係の設定方法 テンプレートによる効率的なDAGの作成 バリデーションDAG・データマート更新DAGは関数でDAGのテンプレートをそれぞれ用意して、引数に任意の値を渡すことでDAGを作成します。例えば、バリデーションDAGにおけるコーディングは以下のようになります。 from airflow.models import DAG def validation_dag (validation_info): dag_id = "validation_" + validation_info[ 'validation_id' ] dag = DAG(dag_id, default_args=default_args, schedule_interval= None , catchup= False ) # 中略 return dag for validation_info in validation_config: validation_id = validation_info[ 'validation_id' ] globals ()[validation_id] = validation_dag(validation_info) バリデーションDAGのテンプレートを関数 validation_dag で作成します。なお、引数はバリデーションの詳細情報を格納した配列、返り値はDAGのオブジェクトです。 関数 validation_dag を呼び出した結果をグローバル変数に格納すれば、バリデーションDAGが効率的に作成されるようになります。 DAG間の依存関係の設定方法 Cloud ComposerではDAG内のタスクを定義する際にAirflowで用意されているOperatorを用います。異なるDAG同士の依存関係を設定するには、 ExternalTaskMarker・ExternalTaskSensor というOperatorを用います。 ExternalTaskMarker(Airflow Version 1.10.8以降に実装)は別のDAGのタスクを実行させるOperatorです。以下のコード例のように external_dag_id と external_task_id に後続の別のDAGのタスクを指定します。 from airflow.sensors import external_task_sensor start_following_dag_task = external_task_sensor.ExternalTaskMarker( task_id=f "start_following_dag_{update_datamart_dag_id}" , external_dag_id=f "{update_datamart_dag_id}" , external_task_id=f "start_update_{update_datamart_table_name}" , execution_date = "{{ execution_date }}" , dag=validation_dag,) 一方、ExternalTaskSensorは別のDAGのタスクのステータスを定期的に確認するOperatorです。以下のコード例のように external_dag_id と external_task_id に先行の別のDAGのタスクを指定します。 from airflow.sensors import external_task_sensor verify_leading_dag_task = external_task_sensor.ExternalTaskSensor( task_id=f "verify_leading_dag_{validation_dag_id}" , external_dag_id=f "{validation_dag_id}" , external_task_id=f "check_status_{validation_dag_id}" , timeout= 600 , allowed_states=[ 'success' ], failed_states=[ 'failed' , 'skipped' ], execution_date_fn= lambda dt:dt + timedelta( 0 ), mode= "reschedule" , dag=update_datamart_dag,) ExternalTaskSensorのタスクでは、指定した対象タスクのステータスを定期的に確認します。タスク verify_leading_dag_task のステータスはそのステータスが allowed_states と同じステータスにならなければ success になりません。これにより正常終了が必須である先行ジョブの依存関係が設定できます。 このように、以上に挙げた2つのOperatorを用いたタスクをDAGに組み込むことで複数のDAG間の依存関係を実装しています。 バリデーションDAGのタスク構成 バリデーションDAGは以下の図に示されるタスクのフローで構成されます。 先行の依存関係であるジョブネット構築DAGにおける最後のタスクのステータスが success になるまでバリデーションDAGの実行を待機します。 success になったことを確認したらバリデーションクエリを実行します。 バリデーションクエリの実行結果はCloud Pub/Subを用いてデータ転送します。これは今後Dataflowを用いてBigQueryテーブルに蓄積させたりすることでバリデーション結果を時系列で解析できるようにするためです。 次に、実行結果が閾値内であるかどうかを判定します。閾値内であれば依存する後続のデータマート更新DAGを実行させるようにして、逆に閾値内でなければエラー処理としてSlackにメッセージを送ることで不具合を通知します。 これにより、データマートへはデータバリデーションで妥当性が示されたデータのみ更新するようにします。逆に、データバリデーションでエラーが生じたデータを含むものは更新を全て意図的に止めることで、データマート更新前にクエリなどを修正するように促します。 ちなみに、Apache Airflowにはクエリ実行結果をチェックするタスクのOperatorである BigQueryValueCheckOperator が用意されています。しかし、今回はバリデーション結果の閾値判定を柔軟に処理できるPythonOperatorを使用しました。 まとめ データ集計基盤にデータの質を担保する処理「データバリデーション」を導入することで、集計結果への正確さの向上を図った取り組みを紹介しました。 データバリデーションのおかげで仕様変更などでデータに変化が生じた場合に適切なタイミングでアラートされるようになりました。そのアラートに対応することで、データの仕様が常に把握できている状態になり、仕様に即さない集計結果が出力されることはなくなりました。 また、Cloud Composer上で動的にジョブネットを構築する仕組みも提案しました。これにより、複雑で動的に変化する依存関係を伴うワークフローが手動で逐一設定することなく効率よく定義できるようになりました。 近年では膨大なデータを貯めておくことが容易になった反面、意図しない集計トラブルやコスト的に非効率な集計を起こしやすくなりました。 本記事が、膨大なデータに対する集計のクオリティ管理に関する問題を解決し、データガバナンス強化の足がかりを作る手助けになれば幸いです。 このように、推薦基盤チームではさまざまなシステムを支援するデータ集計基盤の開発・運用に取り組んでいます。チームメンバーを絶賛募集していますので、ご興味のある方は以下のリンクからぜひご応募ください! www.wantedly.com
こんにちは。SRE部BtoBチームの岩切です。普段はBtoB事業における自社ECサイトの運用保守・監視をしています。 今回2020年11月27日にオンラインで開催された AWS GameDay に参加しました。 本記事では、GameDayイベントで得た学びから実際の業務へどのような効果があったかを共有します。 AWS GameDayについて 今回のAWS GameDayでは、開催前の別日に事前勉強会が2時間、GameDay本番が5時間ほど開催されました。 合わせて17社31チームの123名が参加しており、参加人数の多さからGameDayへの注目度が伺えます。 GameDay概要 GameDayはAWS環境に触れた経験のある人向けです。障害時のトラブルシュートを学び、本番での対応方法を実践的に体験学習できます。以下が概要です。 障害対応の訓練。 本番同様の環境がAWSから提供される。 アプリケーションを稼働し続けスコアを獲得し、チーム対抗でスコアを競う。 内部及び外部の変更や脅威に対し得て柔軟に対応する。 参加レポート それでは、実際の開催2か月前から当日までの動きを紹介します。 参加申し込み 開催の約2か月前に社内向けのAWS GameDay参加者募集が開始されました。 社内でチームが調整され、私のチームは社内メンバー4名での参加となりました。AWS GameDay専用の Slack チャンネルで事前コミュニケーションを図ることもできました。 事前勉強会 AWSの担当者より事前勉強会として、GameDayの紹介及びサービスの紹介が実施されました。内容としては下記サービスの基本的機能の紹介がありました。 リージョンとアベイラビリティゾーン Amazon VPC セキュリティグループ vs. ネットワークACL AWS ELBの種類及び使い方 Amazon EC2インスタンス オートスケーリング Amazon ECS、Amazon EKS、Amazon ECR、AWS Fargate Amazon Lambda & Amazon API Gateway Amazon DynamoDB & Amazon RDS Amazon CloudWatch & AWS X-Ray Amazon CloudTrail 上記以外にもGameDayについての説明があり、適切なサービス運用の大切さと作業分担の大切さをGameDayでは重要視しており、日々の業務にも通じるとの説明がありました。 また疑問点や質問があった場合は適宜Slackで受け付けていただき、分かりやすい回答をいただくことができました。 GameDay当日 当日は休憩を含みながら5時間ほどのWorkshop形式で進行されました。 オープニングからフルオンラインで開催され、AWS側で用意されたオンライン会場に各自が参加しました。 オープニング後には各チームに分かれ、それぞれ好きなツールを使いコミュニケーションすることになります。私のチームでは引き続きSlackを利用し、通話機能でやり取りをしました。 異なる会社のメンバーでチームを組むことも考えられるため、あらかじめチャットツールが用意されているのは、参加の敷居を下げてくれていると感じました。 コミュニケーションについても活発に行うことができ、終始穏やかな雰囲気ですすめることができました。 また開会式では上位3チームに景品があるということも発表され、参加者全員が熱意に満ちていました。 予測できない脅威、変動、変更 全チーム共通で競技の目的を与えられ、とあるサービスのスコアを競いました。 サービスのデプロイだけではスコア上位を目指すことはできず、競技中はAWS側から与えられる予測できない脅威や変更に対応し続けなければいけません。 また予測できない脅威に対して、その場しのぎの対応をしても再度同じ脅威が襲ってくる場合もありました。こういった状況であったこともあり、「実際のサービスにおいても自動復旧を担保しないといけない」といった意識を持つことができました。 サービス運用と作業分担の大切さ 私のチームでは、あらかじめAWSの実務経験をヒアリングしておき、作業の向き不向きや、AWS知識のレベルを認識合わせしました。しかし、作業分担をあらかじめ明確に分けていなかったため、AWS側からの脅威に太刀打ちができませんでした。みんなでサービスのデプロイをするのではなく、各自がしっかりと役割を持っていた方が良かったです。これは、実際のサービスの運用でも同じことが言えるので、学びのひとつでした。 当日を振り返ると、少なくとも以下の作業分担をしっかりを明文化しておくべきだと感じました。 全体進行 デプロイ 障害対応 連携する他サービスの監視 途中からは役割分担ができるようになったのですが、もちろん担当者だけでは対応できない場合もありました。そのため、チームメンバーにどこで助けを求めるかを決めるのもチーム進行の大切な要素であると感じました。 またどうしても作業がうまくいかない場合や、チーム全体の知識で解決できなかった場合は、AWS側から助けをいただくこともできました。 Award & Review Session 競技後、表彰と競技の振り返りを行いました。 スコアボードを見ると上位層は僅差であり、上位3位のどのチームも作業分担を大切にしているとのことでした。 全体的に終始和やかな雰囲気で進み、競技終了後もSlackでは挨拶が盛んでした。 今回は開催されませんでしたが、本来であればWorkshop後にはGameDay前の各参加者の取り組みを紹介するLT大会もあるとのことでした。 GameDayに参加して得られたこと 業務効果 AWS GameDayでは、事前学習も含めてAWSに関する知識を普段以上に吸収できました。そこで学んだ内容が実際に実務で役に立つこともありました。 業務中に下記事象が発生し、GameDayでの経験から即時で復旧できました。 作業者がS3に設定ファイルをリリース。 設定ファイルのフォーマットエラーが発生。 サービスエラーが発生し作業者とは別の運用者が検知。 夜間に検知したため、いつ・誰が・反映したかの確認が難航。 「CloudTrail」で調査し作業者の特定。 作業者に復旧対応の依頼を行い、サービス復旧。 CloudTrailはサービスの存在は知っていたのですが、これまで触れることはありませんでした。AWS GameDayで初めてCloudTrailを経験していたからこそ、これを活用して問題点の即時特定できました。 カオスエンジニアリング AWS GameDayではトラブルシューティングの練習をしましたが、本番稼働しているサービスではシステムの耐障害性のテストが難しいでしょう。 AWSでは障害を意図的に起こすカオスエンジニアリングサービスの AWS Fault Injection Simulator が2021年に提供される予定です。 こちらは本番環境を「ダウンしないサービス」ではなく、「自動復旧が容易なサービス」へ切り替える手助けになるかもしれません。 私も興味があるため、機会があれば別記事でご紹介させていただきます。 AWS GameDayに参加して AWS GameDayは以前から行われているのは知っていてましたが、今回初参加となりました。 GameDayに参加したことで、運用しているサービスの設計や、連携している別サービスが自動復旧性を担保できているか確認する良い機会となりました。 GameDay以外にも障害訓練する機会はいくつかあるかと思います。それらの訓練ももちろん価値があります。しかし、GameDayでは具体的なAWSのサービスが登場した特別な訓練であり、AWSを活用したサービスの運営者にとって、この具体的なサービスを用いた訓練は非常に価値があるものと言えます。 みなさんも本番障害が発生した際の機会損失や社会的信用の低下を防ぐためにも、サービスの自動回復を今一度振り返ってみてはいかがでしょうか? さいごに ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
こんにちは、ZOZOテクノロジーズ CTO室の池田( @ikenyal )です。 ZOZOテクノロジーズでは、2/9に 第二回 AWSマルチアカウント事例祭り を開催しました。 zozotech-inc.connpass.com AWSを活用する複数社が集まり、事例に関してお話しする祭典が「AWSマルチアカウント事例祭り」です。専門性の高い、ここでしか聞けないコアなトークをお届けしました。特にAWSを使用している方、AWSのマルチアカウント運用を始めたい方、AWSのマルチアカウント運用に課題を感じている方に向けたイベントです。 登壇内容 まとめ ZOZOテクノロジーズ、ニフティ、Classiよりそれぞれ1名ずつ、合計3名が登壇しました。 マルチアカウントでのIAMユーザ把握と可視化 IAMユーザー棚卸しへの取り組み (株式会社ZOZOテクノロジーズ 光野 達朗 / @kotatsu360 ) AWS導入から3年 AWSマルチアカウント管理で変わらなかったこと変えていったこと (ニフティ株式会社 石川 貴之) セキュリティインシデントを乗り越えるために行ったマルチアカウントでの取り組みについて (Classi株式会社 大南 賢亮) 最後に ZOZOテクノロジーズでは、プロダクト開発以外にも、今回のようなイベントの開催など、外部への発信も積極的に取り組んでいます。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
ZOZO研究所 の森下( @IshyMore )です。本記事では、数式とソースコードを含む教材を用いてテレワーク環境下で輪講を実施した際に、スムーズに輪講を進められるよう工夫した点について紹介します。 目次 目次 輪講の目的 教材の選定理由 内容が基本的で、応用範囲が広い 数式に対応したソースコードが載っている 演習問題が大量にある 輪講の進め方 輪講運用のための仕組みづくり Jupyter Notebookによる統一 Jupyter Notebook用diffツールの採用 ライセンスの明記 演習問題ごとにファイルを新規作成 Pythonの環境構築 フォーマッタの検討 フォーマットチェックの自動化 コミュニティへの貢献 参加者への事後アンケート まとめ おわりに 輪講の目的 ZOZO研究所では日々の研究開発の傍ら、論文の輪読会、興味・関心事を紹介する勉強会、研究者を招待しての講演会など行っています。輪講も開催しており、メンバー同士で互いに議論しながら教材を読むことで、基礎知識の定着を図っていました。 ところが、昨今の新型コロナウィルスの感染拡大に伴い 弊社では在宅勤務が中心 になりました。それまで開催していた輪講は、オフィスに置いてある本を利用して同じ場に集って実施していましたが、新型コロナウィルスのためオフィスに出社することが困難となりました。 そこで、それまで開催していた輪講を中断し、新しい教材で輪講を開始することにしました。しかし、普段であれば同じ場所でホワイトボードを使いながらしていたような議論ができなくなり、新たな輪講の形式を模索する必要がありました。 教材の選定理由 まず輪講の教材として、 統計的機械学習の数理100問 with Python という本を選びました。この教材はタイトルの通り、機械学習の基本的な内容を全100問の問題を解きながら身につけようという内容になっています。 今回こちらの本を採用した理由は以下の3つです。 内容が基本的で、応用範囲が広い 数式に対応したソースコードが載っている 演習問題が大量にある 内容が基本的で、応用範囲が広い 今年度はZOZO研究所に新卒のMLエンジニアが配属されたため、機械学習の基本的な内容を学べる教材が適していました。また、ZOZO研究所には様々なバックグラウンドの研究者が集まっており、専門も機械学習に限らず数理最適化・制御理論・コンピュータグラフィクスなど幅広いです。それらの最新の知見は別途論文の輪読会や技術共有会などを開催して常にキャッチアップしています。 そのため、輪講ではより基本的なレベルでかつ多くのメンバーに役立つような内容を扱うよう意識しました。 数式に対応したソースコードが載っている ZOZO研究所は機械学習に関するシステムを開発することも多く、理論と実装の両方を学べる教材が望ましいです。採用した教材は、Bitbucketのリポジトリに本文中の登場するソースコードを全て公開しており、そのような用途に適していました。 演習問題が大量にある 演習問題を解くためには、自然と教材を読み込まざるを得ないので理解が捗ります。過去に別の輪講を主催していた際には演習問題がない教材を扱っていたのですが、流し読みで終わる人と、実際に手を動かす人では内容の理解度に差がついていました。 そこで、演習問題が大量にある教材を選び毎回1人1問をノルマに演習問題の解答を作成してもらうことで、曖昧な理解なままで終わるのを防止しました。 輪講の進め方 輪講は週1回90分の時間をとって開催しました。以下が1回の流れです。 事前準備 該当範囲の本文を読む 担当の演習問題を解いて、解答をGitHubにPull Requestとして提出 疑問点を社内Wikiに記載 輪講当日 各自が担当した問題の解答を画面共有しながら解説 社内Wikiに記載された疑問点を全員で議論 輪講後 その日の議論の内容を社内Wikiに記載 Pull Requestの解答に問題がなければマージ 上述の事前準備を参加者全員がすることで輪講をスムーズに進めることができました。作成した解答はGitHubリポジトリのmainブランチへ、Pull Requestとして提出してもらいますが、詳細は後述します。なお、輪講後のタスクは、幹事である私が担当していました。 以上の形式で進めると、議事録である社内Wikiには、我々がハマった箇所とそれに対する解決策が全て記載されることになります。これにより、輪講へ途中参加する際のキャッチアップが容易になるだけでなく、輪講終了後にも教材を独学する人の助けとなる資料が完成します。 また、今回上記の教材を読むにあたって、問題を解くと同時に本文中のソースコードのリファクタリングも行いました。特にコーディングスキルの高いメンバーはよりスマートな実装や、より速い実装を提案してくれます。それらを共有することで、ベテランから開発経験の浅いメンバーに対してうまく技術を伝達する機会を作ることができました。 輪講運用のための仕組みづくり テレワーク環境下では気軽にホワイトボードを使って議論ができません。今回採用した教材はソースコードを書くだけでなく数式を使って計算・証明をする演習問題も多く、それらをどのように参加者間でスムーズに共有するかという点に課題がありました。 そこで、以下に述べる工夫をしました。 Jupyter Notebookによる統一 数式の記述であればTeXファイルが望ましいですが、GitHub上でTeXファイルの数式は表示できません。 そこで、各自担当する演習問題の解答は、全てJupyter Notebookファイルで作成するようにしました。Jupyter Notebookファイル上でLaTeX記法を用いることにより、GitHub上で数式を表示できるためです。なお、本文中のソースコードは全てJupyter Notebookファイルで公開されていたことも理由に含みます。 Jupyter Notebook用diffツールの採用 Jupyter Notebookファイルの中身はJSON形式なので、リファクタリング前後の差分をGitHubのWeb画面上では綺麗に表示できません。 そこで、Jupyter Notebookファイルの差分を綺麗に表示できる、 nbdime を導入しました。 上図は、nbdimeでリファクタリング前を左半分に、リファクタリング後を右半分に表示したものです。 ライセンスの明記 公開されているソースコードのリファクタリングを試みる場合、複製・配布・改良の範囲はOSSライセンスによって決定されます。今回選んだ教材のソースコードには、ライセンスが未記載だったため、そのまま使用すると著作権侵害に該当する恐れがありました。 そこで、教材の著者に公開されているソースコードのライセンスを明記していただきました。これにより、社内のリポジトリへの複製が可能になり、ソースコードを改変できるようになりました。 演習問題ごとにファイルを新規作成 演習問題1問につき、1つのJupyter Notebookファイルを新規作成し、そこへ解答を記載するようにしました。教材の演習問題は100問と非常に多いので、章ごとにJupyter Notebookファイルを作成することも考えました。しかし、同じJupyter Notebookファイルを共同編集することでコンフリクトが発生しかねないので、このような方針としました。 また、公開されているソースコードは章ごとに1つのJupyter Notebookファイルでまとめられており、それを元にリファクタリングしたJupyter Notebookを新規作成しました。 Pythonの環境構築 Pythonのバージョンを指定し、必要なパッケージは requirements.txt を配布することで、参加者全員が同一の環境でコーディングできるようにしました。Pythonのバージョンを指定したのは、公開されているソースコードのJupyter Notebookファイルのメタデータに実行されたPythonのバージョンが記載されていたためです。 ここで考慮すべき点は、全ての公開されているソースコードが動作するような requirements.txt を作成することです。 公開されているソースコードはJupyter Notebookファイルのみで構成されており、その中で import が適宜記載されているスタイルでした。また、我々はnbdimeなど教材に記載されていないパッケージも利用していたため、 requirements.txt が自明ではありませんでした。 試しに、パッケージのバージョン指定をせずに requirements.txt を手動で作成したところ、依存関係でインストールエラーが発生しました。そこで、 Poetry で依存関係を解決し requirements.txt を作成しました。なお、 pandas などのバージョンが変わるとメソッドが変わってしまうパッケージはマイナーバージョンまで固定しました。 ちなみに、Pythonパッケージの依存関係を解決するだけでは、パッケージが問題なく動作するとは限りません。例えば、 LightGBM はインストールでエラーにはなりませんが、 import 実行時にエラーが発生します。この場合、事前に brew install で必要なパッケージをインストールする必要あったので、その旨をGitHubリポジトリに明記しました。 Poetryを利用したのはあくまでも、初めの段階でパッケージの依存関係を解決するためだけであり、参加者各自が環境構築する際は requirements.txt で行っていました。後になって考えると、Pythonのバージョンを明示的に指定できて、仮想環境も自動作成されるPoetryで環境構築した方が良かったのかもしれません。今後の改善ポイントです。 フォーマッタの検討 解答の作成やリファクタリングは、各メンバーが個別に行うため、コーディングスタイルに細かい差が出てしまいます。 そこで、Jupyter Notebookファイルのフォーマッタとして nb_black を導入することでコーディングスタイルの統一を目指しました。 nb_blackを使うには、それをインストールした上で、 %load_ext nb_black または %load_ext lab_black という文字列が入力されたセルを実行します。すると、後に実行されるセルに対して Black のフォーマットが適用されます。なお、上記nbdimeの使用例の図の右側は、nb_blackを実行済のソースコードです。 他の選択肢として Jupyter Black もありましたが、これはGUIで実行するものであり、後述するGitHub ActionsやGitHub Webhooksと相性が悪かったので見送りました。 フォーマットチェックの自動化 nb_blackが適用されていることをレビュアが毎回確認する手間を減らすため、GitHub Actionsを用いてフォーマットチェックを自動化しました。 実際に使用したワークフローを以下に示します。 Pull Requestが作成される度に、 %load_ext nb_black または %load_ext lab_black がJupyter Notebookファイルに含まれているかチェックしています。チェックを通過しないPull Requestはmainブランチへのマージができないようにしました。 name : Python nb_black on : push : branches : [ main ] pull_request : branches : [ main ] jobs : build : runs-on : ubuntu-latest steps : - uses : actions/checkout@v2 - name : Set up Python 3.7 uses : actions/setup-python@v2 with : python-version : 3.7 - name : Check code style with nb_black run : | count=0 for file in $(find . -not -path "*/\.*" -and -not -name "textbook_??.ipynb" -and -type f -name "*.ipynb" ); do chk_nb_black=$(cat $file | jq '.cells[] | select(.cell_type == "code").source[] | contains("%load_ext nb_black")' ) chk_lab_black=$(cat $file | jq '.cells[] | select(.cell_type == "code").source[] | contains("%load_ext lab_black")' ) if [[ $chk_nb_black == * true * ]] || [[ $chk_lab_black == * true * ]] ; then echo OK : $file elif [ -z "$chk_nb_black" ] ; then echo OK : $file else count=$(( count + 1 )) echo NG! : $file fi done if [ $count -gt 0 ] ; then exit 1 fi 実は、ここで行われている判定はあくまで文字列が存在するかどうかについてであり、実際にフォーマッタが実行されているかどうかは確認していません。 例えば、コーディングを全て終えてから %load_ext nb_black とセルに入力すると、Blackが実行されていないもののGitHub Actionsのチェックを通過してしまいます。Blackが実行されたかどうかを厳密にチェックすることも考えましたが、そこを厳格化するための費用対効果は低そうだったので、このような簡易的な確認で十分と判断しました。 なお、Pull Request時に自動判定する機能はGitHub Actionsの他にGitHub Webhooksもありますが、今回は実装が容易なGitHub Actionsを採用しています。 コミュニティへの貢献 他の参加者に解説できるくらい教材を丁寧に読み込んでいくなかで、細かい誤植を見つけることがありました。このような誤植は社内Wikiで逐次報告されるため、社内のメンバーに対しては誤植が原因で進まないという状況は減らせました。しかし、同じ教材を読み進めている社外の方たちは同じ問題に直面するはずです。 そこで、教材の著者が主催しているFacebookグループにも、逐次報告しました。すべての報告を著者自身に確認していただき、その都度、疑問を完全に解消していきました。 その結果、報告した内容は全て公式の正誤表に掲載され、新しく出版された英語翻訳版ではその箇所が修正されました。この貢献を認めていただき、英語翻訳版の謝辞には私の名前が掲載されています。 なお、弊社はOSS活動を推奨しており、OSS活動は職務として認められています。 techblog.zozo.com これは余談ですが、過去1か月以内にFacebookグループで最もエンゲージメントの高い投稿をした人に与えられる「Facebookの盛り上げ達人」という謎のバッジを獲得しました。また、著者が出版した続編の帯には「Facebookページが盛り上がっています」と記載されているのですが、この盛り上がりの一端を担っているのは我々ZOZO研究所のメンバーであろうと想像しています。 参加者への事後アンケート 輪講終了後、参加メンバーに対してアンケートを実施しました。「参加してよかったか」と「業務に(直接的でも間接的にでも)活かせそうか」を、5段階で評価してもらった結果が以下の図です。 多くの参加者が「参加してよかった」とポジティブに感じてくれたようです。実際に、議事録やGitHubを上手く使いこなすことで議論が白熱したり、詳しい人からの知識の伝授が発生したりとテレワーク環境下でも問題なく輪講が実施できました。 業務に活かせるかに関しては、控えめな結果となりました。教材の内容が機械学習の基本的な内容であるため、特定のタスクに特化した知識は得られないことと、機械学習の数理部分が機械学習システムの開発のごく一部でしかないことを反映しているのだと考えています。 まとめ テレワーク環境下で初めて輪講を行い、そこで得られた知見を本記事にまとめました。また、輪講のシステムのために幹事として準備したこともまとめ、結果として教材の謝辞に名前が載ることになりました。 そして、輪講参加メンバーに対するアンケートでも、概ね好意的な評価が得られました。読者の皆様がテレワーク環境下で輪講や勉強会を開催する際に、本記事の内容が少しでも参考になれたら幸いです。 おわりに ZOZOテクノロジーズでは、各種エンジニアを募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 tech.zozo.com
こんにちは、ZOZOテクノロジーズSREチームリーダー兼組織開発チーム所属の指原( @sashihara_jp )です。 この記事では2019年12月から全11回開催してきた「マネジメント勉強会」を通じて分かってきたZOZOテクノロジーズの組織課題と、これから取り組もうとしているその解決方法を紹介します。 ZOZOテクノロジーズの社員構成 マネジメント勉強会とは 立ち上げまでの道のり 運営メンバーの勧誘 経営層への企画提案 勉強会の命名 1年間で実施したテーマ 第1回 各チームで実施しているチームビルディング施策の共有 第2回 書籍「1on1マネジメント」を読んだ上で内容について議論 第9回 採用面接で質問している内容について意図と効果共有 マネジメント勉強会を通じて分かってきたZOZOテクノロジーズの現状 1.組織の急拡大による弊害 2.現場のコンフリクト 3.マネジメントと人材育成 組織開発チームの立ち上げについて 1.組織の急拡大による弊害への対応 2.現場のコンフリクトへの対応 3.マネジメントと人材育成への対応 今後の展望 マネジメント勉強会のリニューアル 組織開発チームが目指す組織 最後に ZOZOテクノロジーズの社員構成 まず、弊社について簡単に説明します。弊社は株式会社ZOZOの100%子会社であり、親会社のZOZOと子会社のZOZOテクノロジーズで主な役割が異なっています。ZOZOにはZOZOTOWNを運営するために必要なブランド営業・マーケティング・カスタマーサポート・物流まわりなどのスタッフが在籍しています。 一方、ZOZOテクノロジーズにはシステム開発をするために必要なスタッフ、エンジニア・デザイナー・リサーチャーなどが在籍しています。2020年2月時点ではZOZOテクノロジーズには約400名の従業員が在籍し、そのうちエンジニアは約350名ほどを占めています。 マネジメント勉強会とは そんなエンジニアの比率が高い会社の中で、マネジメント勉強会を立ち上げた経緯から説明します。 まず、ある法則を紹介します。米国の人事コンサルタント会社ロミンガー社の調査によると、ビジネスにおいて人は70%を仕事上の経験、20%を同僚からの助言やフィードバック、10%を研修などのトレーニングから学ぶと言われています。これは「7・2・1の法則」とも呼ばれ、企業研修などでもよく引用されています。 わたし自身、弊社に入社してから3年ほどSREチームのマネージャーとしてマネジメントをしてきてこの「7・2・1の法則」を実感してきました。仕事上の経験は当然積むのでそこからの学びはもっとも多く、マネジメント関連の書籍もたくさん読んで可能な限り自学してきました。ただ、それだけでは自分の成長速度に限界を感じていたのも事実でした。 そこで自分自身の成長速度を加速させるために上記法則の「同僚からの助言やフィードバック」を強化したいと考え、そのための仕組みを他のマネージャーたちと一緒に作りたいと思い立ちました。わたしが成長速度に物足りなさを感じていたのと同様に、弊社の他のマネージャーもそれぞれ悩みや、それに対する解決方法を持っていて、お互いに共有しあうことで成長したいと感じているのではと思ったからです。 また、マネジメント勉強会の立ち上げを検討し始めた2019年10月はZOZOグループが買収され前代表の前澤が退任した直後でした。強力なトップダウン型リーダーがいなくなったタイミングだったので、自分たちも変わらなければという意識が全社的に強まっていた時期でもあり、会社としてもマネージャー層のマネジメントスキル向上や横の情報共有を強化することが重要であるのではという考えもありました。 立ち上げまでの道のり そのような考えからマネジメント勉強会の立ち上げを思い立ち、下記のようなステップで初回の開催を計画していきました。 運営メンバーの勧誘 経営層への企画提案 勉強会の命名 運営メンバーの勧誘 まず、マネジメント勉強会を立ち上げるにあたって最初に考えたのが「1人で運営するべきではない」ということでした。複数人の運営メンバーを擁立することで下記のようなメリットがあります。 運営にかかる工数を役割分担することで分散できる 運営メンバーで相談しながら運営することで会のクオリティを向上できる 開催の継続力が高まる そこで以前から社内でマネジメントに関して関心が強かった荒井( @arara_jp )と鶴見( @_tsurumiii )に声をかけて、運営チームが確立されたことでマネジメント勉強会の立ち上げを決めました。 経営層への企画提案 次にマネジメント勉強会の立ち上げについて経営層に企画の共有をしました。 弊社はエンジニアが多い会社なので技術的な勉強会は社内で日常的に行われており、勉強会の開催自体はよくあることなので通常であれば経営層に許可を取る必要はありません。しかしマネジメント勉強会については組織横断型を想定しており、マネジメント層のスキル向上など経営課題にも関連する内容だったので企画について事前共有をしました。 結果、経営層からは応援するという前向きな意見をもらうと同時に、勉強会の中で出てきた「マネージャー層が感じている課題感や制度設計に関する要望」などについて教えてほしいという要望をもらい現在まで開催毎に経営層へのフィードバック会を実施しています。 勉強会の命名 この会は経営層から言われて業務命令で立ち上げたわけでもなく、人事が研修として公式な業務で行っているわけでもなく、一般のチームリーダーが自主的に必要性を感じて周囲に声をかけて始めた試みです。 そこでこの勉強会を立ち上げるに伴いもっとも意識したのは「ネーミングを間違えない」ということでした。具体的には「マネジメントのことを誰かに教えてやるという上から目線を感じない名前」にしようということです。これは設立理由にも記述したように、わたし自身が周りのマネージャー陣と一緒に「マネージャーとして成長していきたい」という想いから始まっているので運営メンバーは参加者を教育するような立場ではなく、あくまで参加者と同じ目線でいたいということです。ネーミングが上から目線になることで、多くのマネージャー陣の共感を得られず成果を挙げられないまますぐ終わるというようなことだけは避けたいと思っていました。 当初、勉強会の目的の1つにジョブマネジメントだけでなくピープルマネジメントの重要性を広めたいという気持ちもあったので「ピープルマネジメント研究会」みたいな案もありました。しかし、これだと一部の意識高い人だけが参加するというような印象を受けて参加ハードルが高いだろうと運営メンバーで話し合い、立ち上げ当初のネーミングとしては「新人マネージャー勉強会」に落ち着きました。 今の「マネジメント勉強会」とは異なる名前ですが、これは経験の浅いマネージャーも参加しやすいようにハードルを下げたいという意図がありました。また、運営メンバー陣もマネジメント歴がそれほど長いわけではないので「皆一緒なんですよ」というスタンスを強調したものでした。この名前は数回開催したあとに、逆に経験のあるマネージャーが参加しづらいのではという意見があり現在の「マネジメント勉強会」に変更しています。 1年間で実施したテーマ マネジメント勉強会は現在に至るまでの約1年間をかけ、合計11回開催してきました。各回の参加人数はテーマによってまちまちですが10名程度から最大で50名程度となっています。累計では全管理職の約8割がいずれかの回に参加しています。 これまでに下記のテーマを選定し開催してきました。 第1回 各チームで実施しているチームビルディング施策の共有 第2回 書籍「1on1マネジメント」を読んだ上で内容について議論 第3回 外部講師を招いての1on1勉強会 第4回 評価についての悩みや疑問を共有する会 第5回 他チームリーダーへの質問・相談会 第6回 マネージャーのキャリアプランについて考える会 第7回 外部講師を招いてのピープルマネジメント勉強会 第8回 コロナ禍でのチームマネジメント方法の工夫を共有する会 第9回 採用面接で質問している内容について意図と効果共有 第10回 新人事制度についての質問、フィードバック会 第11回 書籍「1on1ミーティング」を読んだ上での内容について議論 内容については各マネージャーが抱えている課題や悩みについて共有することで他のマネージャーから解決案のヒントを得るという形式や、会社の新制度導入タイミングに合わせた企画が多いです。また、全員で同じ書籍を読んで読書会のような方法をとったり、外部講師を呼んで講演会のような形式をとったりと参加メンバーが飽きないような工夫もしてきました。 いくつかテーマの紹介とそれぞれの効果を簡単に紹介します。 第1回 各チームで実施しているチームビルディング施策の共有 第1回では各チームで実施しているチームビルディング施策について紹介し合いました。たとえば下記のような施策が紹介されました。 人生(モチベーション)グラフ 過去のモチベーションが上がった出来事、下がった出来事を人生グラフにして開示しあうことでお互いのパーソナリティを知る。 チーム内ZOZOエール 会社で採用しているUniposというピアボーナス制度を活用し、日頃の感謝やバリューを体現した行動を賞賛しあう時間を毎週の定例の中に作る。 なお、チーム内ZOZOエールについては、以下の記事で紹介しています。 techblog.zozo.com 他にもたくさんの施策がありましたが、他のチームがやって成功している事例を取り入れることができ有意義な内容となりました。コロナ禍になる前でしたが、当時からリモートワークが制度として導入されていたこともあり、人間関係を円滑にするための工夫をそれぞれのチームが実施していることが印象的でした。 第2回 書籍「1on1マネジメント」を読んだ上で内容について議論 第2回はピープルマネジメントについての書籍「1on1マネジメント」を参加者で事前に読んできて内容について当日議論するという内容でした。事前に課題図書を作ることで参加ハードルは上がってしまうのですが、共通の書籍を読んでくることで目線のすり合わせができた状態で1つのテーマについて話すことができとても好評でした。 この書籍を参加者全員が読むことでピープルマネジメントの大切さについてインストールされたマネージャーが増えたことは会社として価値のあったことだと思います。 第9回 採用面接で質問している内容について意図と効果共有 第9回では採用をテーマにして議論しました。面接の際にどのような質問をすると候補者の本音が引き出せやすいかなどの知見の共有です。また、応募者が現在の採用ページを見るとこの部分で迷うのではないかという意見や、こんな面接官トレーニングをしたらいいのではという要望も出てきたので後日、採用人事へフィードバックする会も実施しました。 この会でも参加したマネージャーから多くの意見や悩みが出てきて、今まで会社として拾い上げることができていなかった声を拾えた意義ある会となりました。 マネジメント勉強会を通じて分かってきたZOZOテクノロジーズの現状 このようにマネジメント勉強会を開催していく中で、会社の局所的な課題やマネージャーが持つ個々の悩みについては横の情報共有をすることで、ある程度は解消することができました。しかし、マネジメント勉強会だけでは解決が難しい下記のような3つの大きな課題もZOZOテクノロジーズにはあることが分かってきました。 1.組織の急拡大による弊害 ZOZOテクノロジーズはこの3年間で社員数が200人から450人に急増してきました。この急激な規模拡大の影響からZOZOグループ全体で目指している上位戦略が現場に伝わりづらくなってしまい、やりがいや達成感を感じづらくなっているという問題が起きているようでした。また、規模が大きくなることで隣の部署やチームが何をしているのかよく分からないという関心の希薄化も生まれていました。 2.現場のコンフリクト ZOZOテクノロジーズはグループ内にあった7社が合併してできた会社であるため、さまざまな文化が混ざりあった状態です。それぞれの会社で大切にしていた価値観が時にぶつかり合うこともありました。また、文化が融合し、それまでのどの文化とも違う「新しいZOZOらしさ」が生まれましたが、その変化の速さに追いつけない社員も出てきました。 technote.zozo.com 3.マネジメントと人材育成 そして現場のマネージャー層がもっとも困っていたのは自身のマネジメントスキル向上についてでした。各々のチームの中で各リーダーがさまざまな工夫をしているものの、会社としてスキルや役割の標準化、マネージャー育成支援が十分にできているわけではなかったことからマネジメントスキルの属人化が進んでいました。また、現場の社員もキャリアパスや育成についての明確な指針がないことで不安を感じているという状態でした。 上記の3つの大きな課題はグループ会社が合併する前まではそれほど大きな問題にはなっていませんでした。それは会社の規模がまだ小さく、合併もしていない1つのみの会社だったので文化も単一、縦と横のつながりが強固で、やりがいと達成感を感じやすい環境だったからです。しかし、グループ会社が合併し、文化が混ざりあった状態で下図のような変化が起きてきました。 出典: 1on1ミーティング「対話の質」が組織の強さを決める この図では横軸が「上司・同僚との関わり具合」、縦軸が「ストレッチ経験の量」を表しています。どちらも高い状態だと「成長実感職場」となります。現在のZOZOテクノロジーズは上述のような歴史的背景から「上司・同僚の関わり具合」が徐々に薄れてきており、左上の「挑戦させすぎ職場」となっている可能性が高いです。 組織開発チームの立ち上げについて このような3つの大きな組織課題と「上司・同僚の関わり具合」が減っていることによる「挑戦させすぎ職場」になっている状態を脱却するため、新たに「組織開発」をテーマにした新チームを立ち上げることにしました。組織開発に詳しい専門家を他社からリーダーとしてスカウトし、わたし自身もメンバーとしてこのチームに参加しています。組織開発チームでは具体的に上記課題を以下のように解決していくことを目指しています。 1.組織の急拡大による弊害への対応 上位戦略の伝達のしづらさについては2020年10月から開始された新評価制度により全社目標、部門目標、個人目標の連携を強めています。自分の仕事がどのように上位戦略に結びついているか実感しやすくなることを目的としています。また、その連携を強めるための手法として週1回30分の1on1を必須として全社導入を始めています。隣のチームが何をしているか分からないという問題については階層別研修やコミュニケーション施策を強めることで解消を目指します。 2.現場のコンフリクトへの対応 新しいZOZOらしさへの適応についても2020年10月に制定された新バリューの浸透施策により共通の価値観を醸成すると共に、同じ目標に向かっていけるよう目標管理制度を導入しています。また、個別の組織課題についても組織開発チームとして支援をします。 3.マネジメントと人材育成への対応 マネジメントスキルについても会社として役割の明文化、スキルの標準化を目指し各種研修、支援を開始します。また、現場社員についても等級定義やキャリアパスを明示的に提示し、キャリア関連施策を実施します。 今後の展望 マネジメント勉強会のリニューアル これまでマネジメント勉強会は初期運営メンバーが、その都度必要だと思うテーマについて検討して実施してきました。今後は上述の会社全体の課題から逆算したゴール設定をし、戦略的に他の社内施策や勉強会とも連携調整しながら進めていく必要があると考えています。運営メンバーも追加募集を始めており、より会社に貢献するような会へと進化させていきます。 組織開発チームが目指す組織 発足した組織開発チームでは「創造性を解き放つ人と組織をつくる」をミッションとし、上記のような必要な施策や制度推進に取り組むことで、組織の課題解決をしていきたいと考えています。そしてグループのビジョンである「世界中をカッコよく、世界中に笑顔を。」の実現を目指します。 最後に まだまだたくさん課題のある会社ですが、スキル・マインド共に高い魅力的な社員が多く、組織課題をうまく解決していくことで飛躍するポテンシャルが非常に高い組織です。やるべき方向性は明確で進化の真っ最中です。 一緒にサービスを作り上げてくれる方はもちろん、さまざまな職種でZOZOテクノロジーズをいい組織にしてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
計測プラットフォーム部バックエンドチームの鈴木です。 この記事では、Akka gRPCを利用しているScalaアプリケーションのZOZOMATに対してKamonを通じてAPMを導入した際に得られた知見、うまくいかなかった内容やその対応策を紹介します。 Akkaとは 最初にAkkaについて簡単に紹介します。Akkaは、JVM上で並行および分散アプリケーションの構築を容易にするツールキットとランタイムです。 Actorモデルの実装であるAkka Actorsを中心とし、Akka StreamsやAkka HTTP、Akka Clusterなど様々なツールが提供されています。詳しくは 公式ドキュメント や Akka実践バイブル を読むことで深く理解できます。書籍で紹介されているAkkaのAPIは、今では古いものとなっていますが、Akkaの楽しさを知るにはとても良い本です。 私たちが開発しているZOZOMATではAkka Actors、Akka HTTP、Akka Cluster、Akka gRPCなどを利用しており、Akkaのツールセットの恩恵を非常に受けています。 APMとは アプリケーションを構築していくために、APMの導入も必要になってきます。 APMはApplication Performance Management、つまりアプリケーション性能管理の略称です。アプリケーション性能管理のツールとして私たちはDatadogを利用することにしました。 APMの機能を利用するためのJavaエージェントがDatadogから提供されています。しかし、私たちのアプリケーションは以前からKamonを利用していたので、DatadogのJavaエージェントは利用せずにKamonからDatadogに連携してAPMを利用することにしました。 Kamonを利用した上でも、分散トレーシングとしてDatadogは利用可能です。なお、私たちのアプリケーションでは1つのアプリケーション内でのトレーシングに留まっているので、この記事では単に「トレーシング」と呼称します。 Kamonとは 上述のKamonについて紹介します。 KamonはJVM上で動くアプリケーションのモニタリングツールであり、JVMのメトリクス取得や本記事でも紹介するトレーシングを行うことができます。 Kamonは収集ツールのレポート先としてDatadogやNew Relic、Kamonが提供しているKamon APMなどを選択可能です。 私たちはトレーシングより以前にメトリクス取得のためにKamonを導入していて、Datadogをレポート先として利用していました。 本記事で利用する「Span」とはトレーシングの文脈において1つのアプリケーション操作を表します。「DBへ問合せる」「HTTPリクエストを送る」「計算する」など考えられます。 基本的にはSpanの作成は自由にでき、粒度はアプリケーション実装者が自由に決められます。 KamonにおけるSpanの詳しい説明は以下のページに丁寧な記述があります。 kamon.io 公式ドキュメントを参考に導入してみる Akka HTTPをJSONサーバとして利用、またはPlay Frameworkを利用している場合、公式ドキュメントに沿って進めることで簡単にアプリケーションに導入できます。 kamon.io kamon.io kamon.io KamonがInstrumentationを提供しているので、それに従って進めれば良いのです。Instrumentationに関しての補足は後述します。 これに従えば、ソースコードの修正はほぼ必要ありません。簡単ですね。 しかし、動作確認をする際に1つポイントがありました。 Kamonのデフォルトのサンプリングロジックでは、アプリケーション起動直後のアクセスは、そのアクセス量に関係なくサンプリングされません。 Kamonのコントリビュータによる Issue でも言及されていますが、開発時などに動作確認したいときには、 application.conf に下記のような記述を追加してサンプリングのロジックを変更することをお勧めします。常にサンプリングが行われるロジックが選択されるので、動作確認時の混乱が減ります。 kamon.trace.sampler=always 動かしてみたがSpanがDatadogに送信されない チュートリアルを参考にしながら、私たちが開発しているZOZOMATのアプリケーションへ導入してみましたが、Spanはいつまで経っても反映されませんでした。 リクエストに紐づくSpanが作成されていることはログを出力することで確認できましたが、そのSpanがDatadogに送信されることはありませんでした。 調査を進めると、私たちのアプリケーションがAkka gRPCを利用していることが原因でした。 では、なぜSpanがDatadogに送信されなかった原因を解説する前に「なぜソースコードの変更なしでトレーシングが実現できるのか」を説明していきます。 なぜソースコードの変更なしでトレーシングが実現できるのか ずばり Kamonから「Instrumentation」が提供されているから です。 この「Instrumentation」は Byte Buddy を利用して実装されているモジュールです。Byte BuddyはJavaのバイトコードを操作して既存のクラスを変更できるライブラリです。 KamonのInstrumentationはByte Buddyを利用してAkkaやJDBCの実装を拡張しています。そのため、Akka Actorsがメッセージを送信するときや、JDBCがSQLを実行するときにKamonのAPIを呼び出すように拡張されています。 実際にByte Buddyを利用してJDBCが拡張されている処理は以下で確認できます。 github.com 拡張されたコードの処理は、巡り巡って StatementMonitor のようなKamon自身を呼び出す処理に到達します。 なぜZOZOMATではチュートリアルにそって導入できなかったのか Akka HTTPのInstrumentationが提供されているのに、なぜ私たちのアプリケーションはチュートリアルに沿って導入できなかったのか。 それはAkka HTTP用のInstrumentationが Directiveを利用した際にKamonのAPIが呼びだされる ようになっていたからでした。 Akka HTTPのDirectiveは簡単に言うとHTTPルーティングを記述するためのクラスです。Akka gRPCはDirectiveを呼び出す代わりに、Akka HTTPを拡張してgRPCを受け付けるライブラリなため、KamonのSpanを送信するために必要なAPIが呼ばれていなかったのです。 1 また、私たちのアプリケーションではSpanは送信されないものの、リクエストを受信した時にSpanの作成はされていました。これは、Akka HttpのInstrumentationではAkka gRPCを利用した場合でもAkka Httpの内部で利用されるクラスに拡張がされていたからでした。 Akka gRPCでもKamonのSpanが送信されるようにする Akka gRPCでもKamonのSpanが送信されるようにする方法を見つけました。 APIからSpanを送信するために必要な takeSamplingDecision メソッドを呼ぶようにする。 この対応をすることで、SpanはDatadogに連携されるようになりました。これで解決。 かと思ったら、ここに来て新しい問題に気付きました。 Akka gRPCからのリクエストを処理する部分で例外が発生した場合にSpanが送信されていませんでした。ここまでご覧頂いている方なら察しがつくかと思います。 原因は、ここでも同様に takeSamplingDecision を呼ぶ必要がある、という点でした。 Spanの範囲内で処理に失敗したことを宣言する fail メソッドは、Akka HTTPのInstrumentationではDirectiveモジュール内で呼び出されていました。そのため、Akka gRPCで利用されるモジュールでは takeSamplingDecision が呼び出されませんでした。 Akka gRPCで生成されるクラスには、エラーハンドリングにおいて共通の処理を記述できる場所が存在します。 github.com github.com この共通処理に fail メソッドを呼ぶ実装を加えて一件落着しました。 以下が、ここまでに言及してきた修正を加えた簡単なソースコードの例です。 XXXServicePowerApiHandler と XXXServiceImpl はAkka gRPCがprotoファイルから生成されるクラスと、そのクラス内の処理を実装するクラスを仮定したものです。 object Main extends App { implicit val actorSystem: ActorSystem = ActorSystem() val handler: HttpRequest => Future[HttpResponse] = { request => Kamon.currentSpan().takeSamplingDecision() XXXServicePowerApiHandler( XXXServiceImpl, { actorSystem => throwable => Kamon.currentSpan().fail(throwable) GrpcExceptionHandler.defaultMapper(actorSystem)(throwable) } )(actorSystem)(request) } Http().bindAndHandleAsync( handler, interface = "0.0.0.0" ) } APMの導入を経て得られたもの 上述の対応により、トレースが正しく動作するようになり、APMの導入を無事に完了できました。レイテンシ増加の疑いのあったソースコードの処理時間を計測できたり、突然発生したエラーの原因調査のための材料が増えました。 導入ができただけでなく、試行錯誤をする過程でKamonについての理解が深められ、Kamonのソースコードを読むだけでなく、バグを発見して 簡単なPull Request を送ることもできました。 なお、Pull Requestの作成は、過去のテックブログの記事にある OSSへの貢献 - Issueから始めるチーム活動 の一環として実施できました。 techblog.zozo.com 今回、Akka gRPCとKamonのインテグレーションの実現のためにアプリケーション側にコードを追加しました。しかし、他のライブラリのようにInstrumentationで実装できるとカッコいいですよね。今後取り組んでいこうと思います。 最後に 計測プラットフォーム部バックエンドチームでは、ZOZOMATをはじめとする計測技術でよりオンラインでの購入体験を向上させたいバックエンドエンジニアを募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! www.wantedly.com Spanには種類がいくつかあり、Akka HTTPがInstrumentationによって作成するSpanは takeSamplingDecision を呼び出す必要があるSpanでした。 ↩
こんにちは。福岡研究所の岩本( @odiak_ )です。 みなさん、Kotlinのコルーチンを使っていますか? 私は、最近久しぶりにAndroidのコードを触る機会があり(3年ぶりくらいでしょうか)、以前から存在は知っていたものの詳しく知らなかったコルーチンを少し使ってみました。まずドキュメントを読んでみたのですが、よくデザインされているなと感じました。今回は使っていませんが、ChannelやFlowなども良さそうです。 この記事では、Kotlinのコルーチンを支える言語機能の1つである、suspend修飾子付き関数の動きをバイトコードから読み解いていきます。 対象読者としては、KotlinをAndroidアプリの開発やサーバーサイドで使用していて、言語処理系の挙動にも興味がある方を想定しています。 コルーチンの紹介 ご存知ではない方のために、Kotlinのコルーチンについて簡単に紹介しておきます。 コルーチンは、軽量スレッドのようなものです。コルーチンを起動すると、それはどこかのスレッドで実行されます。デフォルトの動作ではコルーチン毎にスレッドを起動することはなく、プールされたスレッドで実行されるので、大量のコルーチンを一度に起動しても問題ありません。コルーチンは、次のように使います。 import kotlinx.coroutines.* fun main(args: Array<String>) { GlobalScope.launch { println( "hello" ) delay( 1000L ) println( "world" ) } Thread.sleep( 1200L ) } ここでは単純に1つのコルーチンを立ち上げて、helloと表示した1秒後にworldと表示しています。より高度な使い方としては、いくつかのコルーチンを立ち上げて並行して何か計算したり、コルーチン同士で通信し合いながら処理を行ったりといったことも可能です。 詳しくは、 Kotlinのドキュメント を読んでみてください。 suspend修飾子付き関数 このようなコルーチンの機能の背景にいる登場人物としては大きく分けて、Kotlinの言語自体に備わっている基本的な機能と、コルーチンのライブラリ(kotlinx.coroutines)の2つがあります。前者の言語自体の機能のうち、suspend修飾子付き関数(以降、suspend関数と呼びます)は特に特徴的なものです。この記事では、suspend関数について深く掘り下げていきます。 先ほどの例で挙げたコードでは、関数 delay はsuspend関数です。また、 GlobalScope.launch に渡しているラムダ式も、明示されてはいませんがsuspend関数です。 suspend関数では、非同期的な処理をまるで同期的な処理のように呼び出すことができます。suspend関数を使わない場合、非同期的な処理を呼び出すにはコールバック関数などを渡す必要がありました。例えば次のように。 fun getPost(id: String, callback: (Content) -> Unit ) { /* ... */ } fun decodeContent(content: String, callback: (String) -> Unit ) { /* ... */ } fun getContent(id: String, callback: (String) -> Unit ) { getPost(id) { post -> decodeContent(post.content) { content -> callback(content) } } } コールバックを使うと、ネストが深くなってしまいコードが読みづらくなる上に、条件的に処理を呼び出すなどの複雑なコードが書きづらくなります。これを、コルーチンを使って書くと次のように、まるで同期的な処理のように書くことができます。 suspend fun getPost(id: String): Post { /* ... */ } suspend fun decodeContent(content: String): String { /* ... */ } suspend fun getPostContent(id: String): String { val post = getPost(id) val content = decodeContent(post.content) callback(content) } suspend関数を呼び出すと、呼び出した側の処理は一旦そこで中断されます。呼び出された関数の処理が終わると、呼び出した側の処理が再開されます。 先ほど関数 delay がsuspend関数であると書きましたが、 delay を呼び出した場合も同じように一度処理が中断され、指定した時間が経過してから処理が再開されます。注意したいのは、 Thread.sleep が処理をブロックするのとは違い、 delay のようなsuspend関数は処理をブロックはしないということです。 解説している動画を見てみたが、腑に落ちない 使っていて、ふと疑問が頭に浮かびました。こんな魔法のようなものがどうやって動いているんだろう、と。実行されるのはJVMの上だし、Javaにはこんな機能ありません。 そこで、そういった内部の話を解説しているというYouTube動画を見てみました。 KotlinConf 2017 - Deep Dive into Coroutines on JVM by Roman Elizarov この動画の前半で、suspend関数はコンパイルされると継続渡しスタイルに変換されて、しかもその継続はステートマシン的なもので表現されるので効率的だよというような話をしています。 最初に見たときは、大まかには理解できたものの、どこか腑に落ちない感覚がありました。 そこで、suspendの付いた関数を使ったコードをJVM向けにコンパイルして、そのバイトコードを見てみることにしました。 以下で行っていることは、先ほどの動画で話されている内容を実際に手を動かして確認してみた、という部分が多いです。 ただ、私はそのステップを踏むことで理解が大幅に進みましたし、その過程は非常に楽しいものでした。 バイトコードを読んでみる Kotlinのソースコードをコンパイルするといくつかのクラスファイルができます。それをjavapコマンド(JDKに付属しています)でダンプしてみます。 今回は、次のようなソースコードを使いました。 package net.odiak.kotlin_coroutines_experiment import kotlinx.coroutines.* suspend fun s1(): Int { println( "hello" ) delay( 1000L ) println( "world" ) return 42 } fun main(args: Array<String>) { runBlocking { println(s1()) } } こちらをコンパイルすると、 AppKt , AppKt$s1$1 , AppKt$main$1 という3つのクラスができます。 この記事では、そのうち AppKt と AppKt$s1$1 の2つを見てみます。それぞれを、 javap -c AppKt のように -c オプション付きで表示してみます。すると、次のようになります。 (長くなるので AppKt$main$1 を省略しましたが、読者の皆さんにはぜひ自身でコンパイルして結果を確かめてみていただきたいです) public final class net.odiak.kotlin_coroutines_experiment.AppKt { public static final java.lang.Object s1(kotlin.coroutines.Continuation<? super java.lang.Integer>); Code: 0: aload_0 1: instanceof #11 // class net/odiak/kotlin_coroutines_experiment/AppKt$s1$1 4: ifeq 39 7: aload_0 8: checkcast #11 // class net/odiak/kotlin_coroutines_experiment/AppKt$s1$1 11: astore 4 13: aload 4 15: getfield #15 // Field net/odiak/kotlin_coroutines_experiment/AppKt$s1$1.label:I 18: ldc #16 // int -2147483648 20: iand 21: ifeq 39 24: aload 4 26: dup 27: getfield #15 // Field net/odiak/kotlin_coroutines_experiment/AppKt$s1$1.label:I 30: ldc #16 // int -2147483648 32: isub 33: putfield #15 // Field net/odiak/kotlin_coroutines_experiment/AppKt$s1$1.label:I 36: goto 49 39: new #11 // class net/odiak/kotlin_coroutines_experiment/AppKt$s1$1 42: dup 43: aload_0 44: invokespecial #20 // Method net/odiak/kotlin_coroutines_experiment/AppKt$s1$1."<init>":(Lkotlin/coroutines/Continuation;)V 47: astore 4 49: aload 4 51: getfield #24 // Field net/odiak/kotlin_coroutines_experiment/AppKt$s1$1.result:Ljava/lang/Object; 54: astore_3 55: invokestatic #30 // Method kotlin/coroutines/intrinsics/IntrinsicsKt.getCOROUTINE_SUSPENDED:()Ljava/lang/Object; 58: astore 5 60: aload 4 62: getfield #15 // Field net/odiak/kotlin_coroutines_experiment/AppKt$s1$1.label:I 65: tableswitch { // 0 to 1 0: 88 1: 127 default: 151 } 88: aload_3 89: invokestatic #36 // Method kotlin/ResultKt.throwOnFailure:(Ljava/lang/Object;)V 92: ldc #38 // String hello 94: astore_1 95: iconst_0 96: istore_2 97: getstatic #44 // Field java/lang/System.out:Ljava/io/PrintStream; 100: aload_1 101: invokevirtual #49 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 104: ldc2_w #50 // long 1000l 107: aload 4 109: aload 4 111: iconst_1 112: putfield #15 // Field net/odiak/kotlin_coroutines_experiment/AppKt$s1$1.label:I 115: invokestatic #57 // Method kotlinx/coroutines/DelayKt.delay:(JLkotlin/coroutines/Continuation;)Ljava/lang/Object; 118: dup 119: aload 5 121: if_acmpne 132 124: aload 5 126: areturn 127: aload_3 128: invokestatic #36 // Method kotlin/ResultKt.throwOnFailure:(Ljava/lang/Object;)V 131: aload_3 132: pop 133: ldc #59 // String world 135: astore_1 136: iconst_0 137: istore_2 138: getstatic #44 // Field java/lang/System.out:Ljava/io/PrintStream; 141: aload_1 142: invokevirtual #49 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V 145: bipush 42 147: invokestatic #65 // Method kotlin/coroutines/jvm/internal/Boxing.boxInt:(I)Ljava/lang/Integer; 150: areturn 151: new #67 // class java/lang/IllegalStateException 154: dup 155: ldc #69 // String call to 'resume' before 'invoke' with coroutine 157: invokespecial #72 // Method java/lang/IllegalStateException."<init>":(Ljava/lang/String;)V 160: athrow public static final void main(java.lang.String[]); Code: 0: aload_0 1: ldc #81 // String args 3: invokestatic #87 // Method kotlin/jvm/internal/Intrinsics.checkNotNullParameter:(Ljava/lang/Object;Ljava/lang/String;)V 6: aconst_null 7: new #89 // class net/odiak/kotlin_coroutines_experiment/AppKt$main$1 10: dup 11: aconst_null 12: invokespecial #90 // Method net/odiak/kotlin_coroutines_experiment/AppKt$main$1."<init>":(Lkotlin/coroutines/Continuation;)V 15: checkcast #92 // class kotlin/jvm/functions/Function2 18: iconst_1 19: aconst_null 20: invokestatic #98 // Method kotlinx/coroutines/BuildersKt.runBlocking$default:(Lkotlin/coroutines/CoroutineContext;Lkotlin/jvm/functions/Function2;ILjava/lang/Object;)Ljava/lang/Object; 23: pop 24: return } final class net.odiak.kotlin_coroutines_experiment.AppKt$s1$1 extends kotlin.coroutines.jvm.internal.ContinuationImpl { java.lang.Object result; int label; public final java.lang.Object invokeSuspend(java.lang.Object); Code: 0: aload_0 1: aload_1 2: putfield #12 // Field result:Ljava/lang/Object; 5: aload_0 6: aload_0 7: getfield #16 // Field label:I 10: ldc #17 // int -2147483648 12: ior 13: putfield #16 // Field label:I 16: aload_0 17: invokestatic #23 // Method net/odiak/kotlin_coroutines_experiment/AppKt.s1:(Lkotlin/coroutines/Continuation;)Ljava/lang/Object; 20: areturn net.odiak.kotlin_coroutines_experiment.AppKt$s1$1(kotlin.coroutines.Continuation); Code: 0: aload_0 1: aload_1 2: invokespecial #30 // Method kotlin/coroutines/jvm/internal/ContinuationImpl."<init>":(Lkotlin/coroutines/Continuation;)V 5: return } 初めて見る方は何が何やら…という感じだと思いますが、コメントにクラス名やメソッド名が書いてあるのでなんとなく読めるのではと思います。 JVMはスタックマシンなので、命令を呼ぶことでスタックに値を入れたり出したりします。 それぞれの命令がどのような意味かは、リファレンスや本で必要なところだけ見てください。 まず、名前からも推測できますが、3つのクラスがどのようなものかを紹介します。 AppKt には、App.ktに含まれるトップレベル関数が、staticメソッドとして定義されている AppKt$s1$1 は、関数 s1 専用の継続クラス 継続クラスとは、ここでは kotlin.coroutine.Continuation インタフェースを実装したクラスを指す 簡単に言うと、suspend関数の途中から続きを実行するために用いられるコールバックのようなもの 2つのフィールドを持っている 戻り値のオブジェクトを保持する result どこに戻るべきかを表す label AppKt$main$1 は、関数 main 専用の継続クラス兼、 runBlocking に渡すラムダ関数 AppKt$main$1 とは継承している継続クラスが少し違うが、大きな差はない まずは、 AppKt.s1 を見ていきます。動画でも紹介されていた通り、引数に継続オブジェクトが追加されています。 また、戻り値は Integer ではなく Object になっています。戻り値がObjectなのは、メソッドの処理がまだ終わっていない場合に COROUTINE_SUSPENDED という特殊なオブジェクトを返すためです。 0-2行目: 第1引数が AppKt$s1$1 のインスタンスではない場合は、39行目にジャンプします。 7-11行目: 第1引数を AppKt$s1$1 にキャストして変数に入れます。この変数を cont と呼ぶことにしましょう。 13-33行目: 今回はあまり関係ないですが、 cont のフィールド label の最上位ビットを見て、フラグ操作をしています。最上位ビットが1の場合は、 AppKt$s1$1.invokeSuspend から呼ばれた場合です。その場合、最上位ビットを0にして cont の label に代入し、49行目にジャンプします。最上位ビットが0の場合は、39行目にジャンプします(このケースは、再帰呼び出しの場合です。なので今回は関係ありません) 39-47行目: AppKt$s1$1 のインスタンスを新しく作り、ローカル変数 cont に入れます。コンストラクタの引数は、 s1 の第1引数である継続オブジェクトです。 (簡単にいうと、引数に渡された継続オブジェクトをs1用の継続クラスでラップしているということです) 49-54行目: cont のフィールド result を変数に代入します。 result と呼ぶことにしましょう。 55-58行目: Kotlinが定義している COROUTINE_SUSPENDED というオブジェクトを取得し、変数に代入します。 62-65行目: cont の label を読み、その値を元に処理を分岐します。 0の場合:88行目へ 1の場合:127行目へ それ以外:151行目へ( IllegalStateException を投げるだけ) 88-89行目: cont.result が例外を表現している場合は例外を投げる関数 throwOnFailure (Kotlinの Result というinlineクラスのメソッド)を呼びます。ただし、 result はこの時は初期値(null)のままなので、呼び出す意味はあまりないと思われます。 92-101行目: println("hello") を呼び出します。 111-112行目: cont.label に1を設定します。 104-115行目(スタックの関係で行数が前後しています): 関数 delay を呼び出します。引数は、1000Lと cont です。 118-126行目: delay の戻り値が COROUTINE_SUSPENDED なら、同じ値をreturnしてs1から抜けます。そうでない場合は、132行目にジャンプします。 127-128行目( label が1の場合はここにジャンプしてくる): 先ほどと同じく throwOnFailure を呼び出します。つまり、 delay の実行が失敗していないかをチェックするわけです。 132-142行目: println("world") を呼び出します。 145-150行目: 42という数値をreturnして s1 から抜けます。 151行目以降: IllegalStateException を投げているだけです。基本的にはここを通りません。 次に、 AppKt$s1$1.invokeSuspend のコードを読んでみますが、その前に invokeSuspend の立ち位置を理解しておきましょう。 invokeSuspend は、その祖先クラス( ContinuationImpl , BaseContinuationImpl )または Continuation インタフェースを見るとその役割が分かる ContinuationImpl や BaseContinuationImpl の実装は こちら Continuation の定義は こちら まず、 Continuation インタフェースは resumeWith というメソッドを持っており、このメソッドは名前の通り中断した処理を再開する 例えば、delay関数に継続オブジェクトを渡した場合、一定時間が経つとその継続オブジェクトの resumeWith が呼ばれる BaseContinuationImpl における resumeWith の実装では、自身の invokeSuspend メソッドを呼び出す そこで COROUTINE_SUSPENDED が返ってきたらメソッドは終了 それ以外の値が返ってきた場合、内部に持っている継続オブジェクトへと関心を移す s1 のコードで見たように、 BaseContinuationImpl は他の継続オブジェクトをラップする それが同様に BaseContinuationImpl であれば、また invokeSuspend を呼んで、同じことを繰り返す その他の Continuation であれば、 resumeWith を呼び出して終了 はい、 invokeSuspend が BaseContinuationImpl における重要なメソッドであることが分かったところで、 AppKt$s1$1.invokeSuspend を読んでいきましょう。と言っても、大変短いです。 0-2行目: 第1引数をフィールド result に入れます。 6-13行目: フィールド label の値を取り出し、最上位ビットを1にして代入します。これは先ほども見たように、 s1 が s1 自身から再帰呼び出しとして呼び出されたのか、 invokeSuspend から呼び出されたのかを区別するためのフラグです。 16-20行目: 自身(this)を引数にして s1 を呼び出し、その戻り値をreturnします。 コードを読んで分かったことのまとめ 関数 s1 を中心にいろいろ読んでみましたが、ここで少しまとめておきましょう。 suspend関数 suspend fun s1() -> Int をコンパイルすると、少しシグネチャが変わった関数 fun s1(Continuation<Int>) -> Any? と関数 s1 のための継続クラス AppKt$s1$1 ができる 継続クラスには、待ち合わせていた処理の戻り値を保持する result と、 s1 内で処理を再開する位置を表す label という2つのフィールドがある 継続クラスには、 invokeSuspend というメソッドが定義されており、それは中断されていた関数 s1 の処理を再開するときに呼ばれる コンパイルされた関数 s1 の動作について 引数に指定された継続オブジェクトは、 AppKt$s1$1 の invokeSuspend から呼び出された場合を除いて継続クラス AppKt$s1$1 でラップされる AppKt$s1$1 の label によって、コード内の指定の位置にジャンプする label が0の場合(初期状態)は始めから "hello"と出力する label を1に設定する 継続オブジェクトを引数に含めて delay 関数を呼ぶ delay 関数は COROUTINE_SUSPENDED を返すので一旦処理を中断し、 COROUTINE_SUSPENDED をreturnして s1 を抜ける label が1の場合は途中から delay 関数の実行が失敗していた場合は、例外を投げて終了する "world"と出力する 42をreturnして s1 を抜ける これを踏まえて、コンパイルされた s1 を実行する流れをざっくりとまとめてみます。 何らかの Continuation インタフェースを実装したオブジェクト(継続オブジェクト)を用意 その継続オブジェクトを引数にして s1 を呼び出す 継続オブジェクトを AppKt$s1$1 でラップする "hello"を出力する ラップした継続オブジェクトの label を1にする ラップした継続オブジェクトを引数に含めて delay を呼び出す delay は COROUTINE_SUSPENDED を返すので、 s1 も同じ値を返して終了 delay に指定した時間が経つ(あるいは実行が失敗する)と、何者かによりラップした継続オブジェクトの resumeWith が呼ばれる resumeWith が同オブジェクトの invokeSuspend を呼び出す invokeSuspend が同オブジェクトを引数に入れて s1 を呼び出す 今度は継続オブジェクトをラップせずにそのまま使う なぜなら invokeSuspend が s1 を呼ぶとき、 label にフラグを立てているから なお、 s1 でフラグは戻される 最初に呼ばれた時とは label の値が変わっているため、続きから処理が行われる delay の実行が失敗していた場合は、例外を投げて終了 "world"を出力する 42を返して s1 が終了 なお、戻り値は BaseContinuationImpl における resumeWith の実装によって、最初に s1 へ渡された継続オブジェクトへと渡される いかがでしたか? suspend関数がコンパイルされて、同期処理のように書かれたコードがコールバック渡しのように変換されている様子が少しでもお分かりいただけたでしょうか? この説明ではいろいろなものを省略したので、例えば次のような疑問を抱くかもしれません。 最初の継続オブジェクトはどこで作られるの? delay 関数がコールバックを呼ぶ仕組みはどうなっているの? s1 関数にループや再帰呼び出しが含まれていた場合はどうなるの? コードを読んだり同じようにコンパイルしてみたりすれば分かりますが、おまけとして少し触れておきます。 最初の継続オブジェクトはどこで作られるのか これはkotlinx.couroutinesライブラリの方を読むと何となく分かります。最初の継続オブジェクトは、 launch や runBlocking などの普通の関数からsuspend関数を呼び出すような関数の中で作られています。 あまり詳しくは読んでいませんが、継続オブジェクトなどいろいろな物を用意して、スレッドに実行させたり処理を待ち合わせたりしているようです。 delay 関数がコールバックを呼ぶ仕組み これも軽く読んだ程度ですが、イベントループのような物を使っています。 suspend関数にループや再帰呼び出しが含まれていた場合 ループの場合も、大きくは変わりません。JVMの世界では、ループも単にジャンプを含んだコードになるだけです。 ただし、関数が再び呼び出された際に、ループなどで使用している変数を復元する必要が出てきます。 そこで、継続クラスにフィールドが追加されて、変化する可能性のあるローカル変数をそこに保存しておきます。再び呼び出された際は、適切な場所にジャンプされ、そこで変数を復元することで処理を再開できます。 再帰の場合も特別なことはありません。再帰呼び出しが行われると、継続オブジェクトが同じクラスでどんどんラップされていきます。 resumeWith がネストした継続オブジェクトを辿ってくれるので、他のsuspend関数を呼び出す場合と全く同じです。 おわりに 「Kotlinのsuspend関数がバイトコードレベルでどう動いているのか?」という素朴な問いに答えるため、いろいろと調べてみました。 調査するにあたって、コンパイルしたバイトコードを読んだり、関連するライブラリのコードを読んだりしました。やや大変でしたが、程よい難しさで、ソースコード読みの練習としても良かった気がします。 (余談ですが、今回kotlinx.coroutinesなどのコードをGitHub上で読んでいました。今思うと、IDEなど使えばもっと楽だったのでは、という気もします) 今回、「継続」という概念について初めて知りました。まだ雰囲気しか分かっていないので、個人的にもう少し掘り下げてみたいです。 最後までご覧いただきありがとうございました。 ZOZOテクノロジーズでは、各種エンジニアを採用中です。ご興味のある方は以下のリンクからご応募ください。 tech.zozo.com
こんにちは、ZOZOTOWN部フロントエンドチームの高橋( @anaheim0894 )です。 2020年5月からZOZOTOWN部では、「UI/UX改善プロジェクト」を立ち上げ、小さなUI改善を進めるチームを発足しました。 そこでこのプロジェクトの紹介をしながら、その工夫したポイントをお伝えします。新しいプロジェクトを立ち上げる際の参考になれば幸いです。 なぜプロジェクトを立ち上げたか 現在の ZOZOTOWN のWebサイトには大きく以下の2つの問題点が存在していました。 いろいろな機能、仕様、デザイン、宣伝などが盛り込まれ、見づらくなる 大規模案件や新機能に時間を取られ、サイト細部のアップデートが滞ることによりUXが低下する 直近のZOZOTOWN開発チームは、ZOZOSUITやZOZOMATのような大規模案件を中心に開発へ取り組んできました。 これまでも「改善を自分達で考えて実装する」をチームで意識し、それがチームの強みでもありました。しかし、大規模案件の対応をしていると、優先度的にもなかなか小規模な改善案件に取り組むことができない状況でした。 その結果、大規模案件に多くのメンバーの工数を割り振る必要があるため、チームの強みである「改善を自分達で考えて実装する」という経験が少なくなってきていることに危機感を感じました。 そこで、「改善を自分達で考えて実装しリリースする」にフォーカスしたボトムアップのプロジェクトを目指した「UI/UX改善プロジェクト」を発足させました。 プロジェクトの目的 このプロジェクトは、以下の3点を達成できる組織になることを目指しています。 1:小規模な改修のPDCAを回せる体制作り 「『ここをこうしたらいいのに』と思っていた小規模な案件を実施し、短期間でリリースする。その結果、UI/UXブランディング向上につながる」という目的を掲げました。 1案件がなるべく大規模な開発へ膨れ上がらないよう、1案件あたりの開発期間を約2〜3週間程度に収めることを目標とし、PDCAを高速に回せる体制としました。 また、上記の開発期間の短縮化のために、今回はアプリの改善は除外してWebサイト改善に特化させました。アプリに関しては体制が整ったら、このプロジェクトでも取り組み方を検討する予定です。 2:改善を止めない仕組み作り これまでも多くの改善案件が提案され、実際にリリースされているものも数多くあります。しかし改善案件を考えても、大規模案件によってその改善案件の優先度が低くなり、結局その案件がストップしてしまうことも多々ありました。 そこで、関係部署のマネージャー陣にプロジェクトの目的や意義を説明し、今回のこのプロジェクトでやろうとしている小規模な改善案件に一定時間を確保することを合意してもらいました。プロジェクトで上げた案件は、大規模案件と並べて全体での優先度付けはせず、確保できた時間内でプロジェクト独自に優先度付けをして進めていくことにしました。 とはいえ大規模案件も並行して進めなければいけないため、1日の10〜20%程の時間を今回のUI/UXプロジェクトに割り当てることで、大規模案件を進めつつ小規模な改善案件も止めない仕組み作りを行いました。 3:改善案を自分で考えられるメンバーの育成 案件の進め方以外にこのプロジェクトの目的として「教育」があげられます。 事業の大きな方向性を決める大規模案件では、複数の意思決定者の議論を経てある程度イメージが具体化された状態で開発に取り掛かることが多いです。もちろん大規模案件の中でもボトムアップで進む案件も多くあります。 そのため自分で機能やUI/UXを考える機会が減っていき、結果的に改善案を自分で考えられるメンバーが育たない懸念がありました。 このプロジェクトを通じて、自分達で考えて提案していき、自己成長のきっかけにすることを目的としました。 プロジェクト体制 プロジェクト立ち上げのタイミングということもありますが、小規模な改善案件をなるべく短期間でPDCAを回せるようにプロジェクトに参加する人数は最小限に絞りました。 プロジェクト責任者 デザイナー フロントエンドエンジニア バックエンドエンジニア データアナリスト プロジェクト進行管理者 上記の役割で1名ずつ部署内公募で募りました。 部署内公募のメリット 今回は、特に改善案件に取り組みたい気持ちが強い人を集めたかったため、部署内公募を利用しました。 最終的に自分で考える力をつけ発揮してもらうため、責任者や各リーダーから指名するよりも、自ら手を挙げてもらうことでモチベーションの高いメンバーを募ることができます。さらに公募への立候補の際に課題を提出してもらうので、さらにその効果が高くなりました。 改善案の考え方と作り方 「 ユーザビリティテスト 」を行い、その結果を元に改善案件を考えていきました。 ユーザビリティテストとは、Webサイトを実際に被験者のユーザーに使っていただき、その様子を観察するテスト手法です。経験や勘に基づく主観的なWebサイト評価ではなく「どこが良くて、どこが悪くて、どこをどう改善すべきか」を客観的に探ることができます。 ユーザビリティテストは社内でも知見が少なかったため、実際のユーザーではなく開発者以外の別部署の社員を被験者として実験的に実施しました。 UI/UXの改善は数値化が難しく、改善案を考えてもそれが本当にユーザーのためになっているのかが判断しにくいケースが多くあります。開発者だけで改善案を考えていってしまうとユーザー視点を忘れてしまい、改善案に偏りがでてしまいます。ユーザーが実際に使っている様子を観察し、どこで躓いているのか迷っているのかを明確にし、改善案を考えていく方針にしました。 プロジェクトによってリリースできた案件紹介 プロジェクトでは2020年上期の改善テーマを「ログイン/新規会員登録ページ」にしました。それにより実際にリリースされた案件の一部を開発者視点でご紹介します。 フォームのパスワード表示/非表示切り替え機能の追加 フォームにパスワードを入力した際、入力したパスワードの文字列を確認できるよう、目のアイコンをタップすることで表示/非表示の切り替えを可能にしました。 入力されたパスワードはセキュリティの観点から、基本的にマスク状態にするのが良いです。しかし、入力したパスワードが長い場合、どんなパスワードを入力したのか分からなくなります。 そこで目のアイコンを付け、表示/非表示の切り替えを可能とすることでUX向上に繋がります。 実装方法 非表示時 < input type = "password" > 表示時 < input type = "text" > アイコンをタップしたときに、JavaScriptを用いてInputのtype属性を変更するだけで実装できます。 注意点としてMicrosoft Edge、Internet Explorer 11ではブラウザ標準機能として実装されているため、独自実装する場合、この2つについて除外する必要があります。 郵便番号のハイフンを有無どちらでも入力可能に改修 新規会員登録フォームの必須項目として郵便番号の入力がありますが、ハイフンなしでの登録から「ハイフンあり・なしどちらでもOK」としました。 ユーザーによって、郵便番号というのはハイフンありで慣れている方もいれば、なしで慣れている方もいます。 実装方法 ハイフンが入った状態の値が送信された場合、受け取ったデータからハイフンを取り除く処理を追加しています。ちょっとした一手間を加えるだけですが、UXが向上する重要な処理です。 PCの必須項目を分かりやすくする改修 PCのUIでは、必須ラベルが元々グレーになっていましたが、赤色に変更しました。 この対応により、必須項目が分かりやすくなり、入力漏れを防ぐことができます。多くのサイトの必須ラベルは、赤色になっている場合が多いです。世の中のベーシックUIに合わせることは、普段使っているUIや慣れているUIと同じになるため、UX向上に繋がります。 ブラウザや端末に保存してある情報の自動補完ができるよう改修 HTMLのautocomplete属性を正しく設定することで、あらかじめブラウザや端末に保存してある情報を自動補完できます。 ZOZOTOWNでは新規会員登録画面の郵便番号とメールアドレスのinput要素にautocomplete属性を付与しました。 実装方法 郵便番号 < input autocomplete= "postal-code" > メールアドレス < input autocomplete= "email" > この属性を付与するだけで自動補完の実現が可能です。 postal-code、emailの他にもautocompleteで様々な指定が可能です。詳しくは以下のサイトにまとまっていますので、ぜひ参考にしてみてください。 developer.mozilla.org SPサイトドロワーメニューのログイン導線改善 ブラウザ版のスマートフォン向けUIにおいて、ドロワーメニュー内のログイン、新規会員登録への導線を見直しました。 Beforeでは、ログインしようとした場合、ログインページに到達するまでに3ステップ必要でした。 Before 1:ドロワーメニューを開く 2:ログインメニューを開く 3:各IDでログインページに遷移 また、ドロワーを開いてもその画面内には新規会員登録への導線が無く、そこからログインメニューを開かないと導線が表示されない状況でした。 Afterでは、「ログイン・新規会員登録」と表記を変更。開閉メニューを撤廃し、ログインページに遷移してからアクションする遷移に変更しました。 After 1:ドロワーメニューを開く 2:ログインページに遷移 これにより、ログインページに到達するまでのステップが減り、2ステップで到達することが可能になるとともに、新規会員登録への導線も分かりやすくできました。 利用頻度が高い項目の場合、無駄なステップを極力減らすことでUX向上に繋げられます。 まとめ ZOZOTOWNのUIは、様々なECサイトのお手本となる存在だと私は考えています。 サービス開発は派手な施策や新機能に着目されることが多く、既存機能や既存UIの細かい改善への取り組みは後回しになってしまうことも多いかと思います。こういった地道な取り組みはサービス運営にとても大切であり、必ず数年後大きな結果に結びつきます。このプロジェクトを通し、細かいUI改善をコツコツと積み重ねた結果、ZOZOTOWNのUIブランディングに繋がると考えています。 Webの世界では良いUIを真似することはよくあることです。 今後もZOZOTOWNのUIは様々なサイトから真似される存在であり続け、そのUIを生み出し続けることがファッションEC全体を盛り上げることだと考えています。 リアル店舗と比較して、ECサイトは会員登録やログインなど、本来洋服を購入する行動には不必要なことをしなければなりません。この「不必要なこと」を見つめ直し、極限まで改善していくことにより、「洋服に出会い購入する」体験をより良くしていきたいと思います。 最後に ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方はもちろん、このようなUI/UX改善の取り組みに興味のある方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
こんにちは、MA基盤チームの田島です。ZOZOTOWNでは、ユーザコミュニケーションの手段としてLINE、MAIL、アプリへのPUSH通知を利用しユーザへのお知らせを実現しています。 その中でも、現在ユーザへのコミュニケーション強化の一環としてアプリPUSH通知(以降、PUSH通知)の強化をしようと考えています。ZOZOTOWNのPUSH通知は今まで、とある外部SaaS(本記事で出てくるSaaSはすべてこの外部SaaSを表します)を利用していました。しかし、PUSH通知チャネルの強化をする上で、利用していたSaaSでは要件を満たせない部分がありました。そこでPUSH通知のためのツールとしてFirebase Cloud Messaging(FCM)に移行しました。 本記事ではPUSH配信基盤の紹介並びに、移行時に行ったことを紹介します。PUSH配信基盤の構成や、PUSH配信ツール移行時の参考にしていただければ幸いです。 目次 目次 FCM移行前のPUSH配信システム 1. 初回アプリ起動時にiOSでAPNsトークンをAndoridではFCMトークンを取得 2. APNs/FCMトークンをPUSH配信用SaaSに連携し、SaaSのPUSH配信用のトークンを取得 3. 取得したPUSH配信用トークンをZOZOTOWNのAPIに連携しユーザと紐付けた形で保存 4. 保存したトークンを定期的にIIASというMA用のDWHに連携 5. IIASで配信対象ユーザを抽出 6. アプリケーションからSaaSに配信を依頼 7. PUSH配信用SaaSがAPNs/FCMへ配信リクエストしユーザにPUSH通知が届く 移行のきっかけ 2つの配信基盤 バッチ配信基盤 リアルタイム配信基盤 リアルタイム配信基盤でのPUSH通知 移行前に利用していたPUSH配信用SaaSの特徴 移行先の選定 大量リクエストに対応できる iOS/Androidにおいて統一したインタフェースで配信が可能 最新機能への追従が早い iOS/Androidへの依存ライブラリが少ない 配信結果をBigQueryにエクスポートが可能 Firebase移行後の構成 1. 初回アプリ起動時にiOS/AndroidそれぞれがFirebaseからFCMトークンを取得 2. 取得したFCMトークンをZOZOTOWNのAPIに連携しユーザと紐付けた形で保存 3. 保存したFCMトークンを定期的にIIASに連携 4. IIASでセグメントを抽出し配信対象を作成 5. 抽出したセグメントを配信情報と一緒にBigQueryへ連携 6. アプリケーションからFCMへ配信リクエストしユーザにPUSH通知が届く 移行手順 移行の要件 既存のPUSH通知を止めることなくシームレスに移行する FCMで利用するFirebaseプロジェクトをiOS/Androidで統一する 移行手順 1. iOSからPUSH配信用SaaSでの配信、並びにFCM両方で配信 2. iOS/AndroidでFCMのみ配信できるようにしバージョンアップ済みユーザ全員へFCMで配信 3. FCMのみで配信 今後の展望 既存の課題 トークンの連携 2つのDWH 2つのワークフローエンジン 構想 まとめ 終わりに FCM移行前のPUSH配信システム はじめに、FCM移行前のPUSH配信システムがどのようになっていたかを紹介します。配信は以下のような流れで行われます。 初回アプリ起動時にiOSでAPNsトークンをAndoridではFCMトークンを取得 APNs/FCMトークンをPUSH配信用SaaSに連携し、SaaSのPUSH配信用のトークンを取得 取得したPUSH配信用トークンをZOZOTOWNのAPIに連携しユーザと紐付けた形で保存 保存したトークンを定期的にIIASというMA用のDWHに連携 IIASで配信対象ユーザを抽出 アプリケーションからPUSH配信用SaaSに配信を依頼 PUSH配信用SaaSがAPNs/FCMへ配信リクエストしユーザにPUSH通知が届く この流れについてそれぞれ詳しく説明します。 1. 初回アプリ起動時にiOSでAPNsトークンをAndoridではFCMトークンを取得 ユーザがiOS/Androidアプリをインストールし最初にアプリケーションを起動するとiOSはAPNsトークンを、AndroidはFCMトークンを取得します。iOSに関して詳細には、アプリを起動し通知許諾ダイアログからユーザが許諾をしたタイミングでAPNsへデバイスを登録しAPNsトークンを取得します。PUSH配信用のSaaSは配信にAPNs/FCMを利用しており、それぞれのトークン連携が必要となります。 2. APNs/FCMトークンをPUSH配信用SaaSに連携し、SaaSのPUSH配信用のトークンを取得 続いて取得したAPNs/FCMトークンをPUSH配信用SaaSへ連携します。APNs/FCMトークンを連携するとそのトークンに対応するPUSH配信用SaaSでの配信用のトークンが取得できます。 最終的にはこの配信用トークンを利用しPUSH配信用SaaSへPUSH配信のリクエストをすることで、ユーザへPUSH配信を送ることができます。 3. 取得したPUSH配信用トークンをZOZOTOWNのAPIに連携しユーザと紐付けた形で保存 続いて取得したPUSH配信用SaaSのPUSH配信用トークンを保存します。ZOZOTOWNのAPIを経由しDBに保存されます。このとき、どのユーザのトークンであるかがわかるようにユーザIDと紐付けた形でトークンを保存します。 4. 保存したトークンを定期的にIIASというMA用のDWHに連携 ZOZOTOWNの全ユーザおよびセグメント(一部の配信対象)向け配信をする際には、IIASを利用して配信用トークンを含めた配信対象ユーザを一括抽出します。そのため、保存した配信用トークンをIIASへ連携する必要があります。 5. IIASで配信対象ユーザを抽出 前述の通り、データ連携が完了し配信のタイミングになると配信対象ユーザをセグメントとして抽出します。セグメントごとに配信文言や、通知タップ時の遷移先URL、それらに紐付いたトークンのリストなどを抽出します。 6. アプリケーションからSaaSに配信を依頼 続いて先程作成したリストを元に、PUSH配信用SaaSに対し配信依頼をします。このとき、配信文言やPUSH通知タップ時の遷移先URLなどと共に、トークンのリストを渡すことでまとめて配信依頼ができます。 7. PUSH配信用SaaSがAPNs/FCMへ配信リクエストしユーザにPUSH通知が届く 最後に配信依頼を受けたPUSH配信用SaaSがAPNs/FCMに配信リクエストすることで、実際にユーザへPUSH通知が届きます。 移行のきっかけ 上記のような仕組みでZOZOTOWNのPUSH配信は送られていました。冒頭で触れたように今回PUSH配信チャネルの強化をする上で利用していたSaaSの利用を諦める必要がありました。そのきっかけについて紹介します。 2つの配信基盤 きっかけを説明する上でZOZOTOWNの配信基盤についての前提知識が必要になります。ZOZOTOWNには配信の仕組みとして、大きく分けて2つの基盤が存在します。1つ目がバッチ配信基盤で、もう1つがリアルタイム配信基盤です。 バッチ配信基盤 バッチ配信基盤は先程の「FCM移行前のPUSH配信システム」で紹介したようにIIASから定期的にセグメント抽出し、抽出されたユーザに対して配信を送るといった仕組みです。バッチ配信での対象チャネルとしてはMAIL・LINE・PUSH通知・サイト内お知らせの4つの配信チャネルがあります。 リアルタイム配信基盤 リアルタイム配信基盤はその名の通りリアルタイムに配信をするための基盤です。何がリアルタイムかというと、ユーザ行動や商品情報などの変更イベントが発生するとそれを起点としてリアルタイムにユーザへの配信します。 例えばある商品の価格が下がったとします。その際にその商品をお気に入り登録しているユーザに対し、リアルタイムに「あなたのお気に入り商品が値下がりしました」といった通知をします。またリアルタイムに配信するだけでなく、ユーザごとに時間最適化を行い、ユーザがよく参照する時間に配信するといったこともしています。このリアルタイム配信基盤の配信チャネルは現状MAIL・LINEの2つです。 リアルタイム配信基盤についてより詳しく知りたい場合は以下の記事をご参照ください。 techblog.zozo.com リアルタイム配信基盤でのPUSH通知 前述の通り、リアルタイム配信基盤ではPUSH通知での配信はされていません。今回PUSH通知チャネルの強化としてリアルタイム配信基盤でのPUSH通知を追加することになりました。しかしリアルタイム配信の特性上、既存のPUSH配信用SaaSでは実現できないことがありました。 移行前に利用していたPUSH配信用SaaSの特徴 移行前に利用していたPUSH配信用SaaSは同一内容の大量配信を得意としており、一括で同じメッセージをたくさんのユーザに送ることが簡単にできるようになっていました。その理由としては以下の特性が挙げられます。 100,000ユーザに対して同一メッセージを送りたい場合1回のAPIリクエストで配信が可能 よって、例えば1,000,000人に同じメッセージの配信をしたい場合はたったの10回APIリクエストをするだけで配信が可能となっており、アプリケーションの実装がかなり楽にできるようになっていました。 しかし、今回やりたいリアルタイム配信ではユーザごとにパーソナライズされたメッセージを送る必要があります。そのため例えば1,000,000人に対してPUSH通知を送る場合、最大1,000,000回APIリクエストする必要があります。現状のリアルタイム配信基盤で配信している秒間リクエスト数を考えると、これまで利用していたPUSH配信用SaaSではそのリクエストをさばき切れないことがわかりました。そこで、既存のPUSH配信用SaaSの利用を諦め別のツールを利用することを決心しました。 また、これまで利用していたPUSH配信用SaaSではただPUSH通知を配信するだけではなく、PUSH配信周辺のマーケティングのためのサービスも充実していました。しかし、ZOZOTOWNのマーケティングのビジネスロジックは内製しているため、有効活用できていませんでした。 移行先の選定 移行後はFirebase Cloud Messaging(FCM)を利用することにしました。移行先をFCMにした理由としては以下の特徴が挙げられます。 大量リクエストに対応できる iOS/Androidにおいて統一したインタフェースで配信が可能 最新機能への追従が早い iOS/Androidへの依存ライブラリが少ない 配信結果をBigQueryにエクスポートが可能 以上について1つずつ説明します。 大量リクエストに対応できる 今回の移行の最大の目的としてリアルタイム配信基盤からのPUSH配信の実現があります。そのため、リアルタイム配信基盤からの配信リクエストを十分にさばけないといけません。具体的な数値は明示できませんが、リアルタイム配信基盤からの現状のリクエスト数に対し、その数倍のリクエストが発生してもFCMでは問題ないことがわかりました。 iOS/Androidにおいて統一したインタフェースで配信が可能 PUSH配信を実装しようと思った際には、iOSは直接APNsを利用し、AndroidはFCMを利用するということが考えられるでしょう。しかしiOS/Androidそれぞれで配信の仕組みを変えた場合、複雑さが増すのと同時に実装すべきものが2種類に増えて実装コストが膨らみ、ユーザへの価値提供のタイミングが遅れると判断しました。そこで統一したインタフェースで配信可能なFCMを採用しました。 最新機能への追従が早い iOS/AndroidのOSにてPUSH通知関連の新機能が実装された場合、活用できそうな機能であればいち早くサービスに利用したいです。そのためPUSH配信に利用するツール側がその機能に早急に追従してくれるものを選ぶ必要があります。AndroidにおいてはFCMがもともと配信ツールとして推奨されています。また、iOSに関してもオプションで直接APNsのパラメータを渡すことができるため問題ないと判断しました。 iOS/Androidへの依存ライブラリが少ない もともとiOS/AndroidではFirebaseを利用していました。そのため、依存ライブラリやそのバージョン等の問題で導入できないという障壁がありませんでした。 配信結果をBigQueryにエクスポートが可能 弊社では全社のデータ分析基盤として、BigQueryを利用しています。そして、FCMでは配信実績をBigQeuryに直接エクスポートが可能です。そのため、最終的な配信実績を集計する場合わざわざ外部からBigQueryへデータ連携をし直すという手間が省けます。FCMからBigQueryへのエクスポート手順に関しては以下の記事をご参照ください。 qiita.com 以上のことからFCMが最も移行先として相応しいと判断しました。 Firebase移行後の構成 最終的にはリアルタイム配信基盤でPUSH通知を送るという目的があります。ただし既存のバッチ配信の仕組みが存在するため、まずはバッチ配信の処理をFCMへ移行しました。 移行手順の紹介の前にまずは、移行後の最終的な構成について紹介します。現状としてはあくまで第一段階として、配信ツールをFCMへの移行をしただけなので課題は様々残っています。それら課題に関しては後ほど今後の展望として紹介します。以下が移行後の配信の流れです。 初回アプリ起動時にiOS/AndroidそれぞれがFirebaseからFCMトークンを取得 取得したFCMトークンをZOZOTOWNのAPIに連携しユーザと紐付けた形で保存 保存したFCMトークンを定期的にIIASに連携 IIASでセグメントを抽出し配信対象を作成 抽出したセグメントを配信情報と一緒にBigQueryへ連携 アプリケーションからFCMへ配信リクエストしユーザにPUSH通知が届く 以上の流れについてそれぞれ詳しく説明します。 1. 初回アプリ起動時にiOS/AndroidそれぞれがFirebaseからFCMトークンを取得 ユーザがiOS/Androidアプリをインストールし最初にアプリケーションを起動するとFirebaseからFCMトークンを取得します。iOSに関して詳細には、アプリを起動し通知許諾ダイアログからユーザが許諾をしたタイミングでAPNsへデバイスを登録しAPNsトークンを取得します。そして取得したAPNsトークンをFCMに渡すことでFCMトークンを取得します。 2. 取得したFCMトークンをZOZOTOWNのAPIに連携しユーザと紐付けた形で保存 移行前はPUSH配信用SaaSの配信用トークンをZOZOTOWNのAPIに連携し保存していました。移行後は直接FCMを利用するためFCMトークンをユーザと紐付けた形でDBへ保存します。 3. 保存したFCMトークンを定期的にIIASに連携 次にPUSH配信用SaaSの配信用トークンをIIASへ連携していたように、FCMトークンをIIASへ連携します。 4. IIASでセグメントを抽出し配信対象を作成 移行前と同じようにIIASで配信対象ユーザを抽出します。このときFCMトークンを含んだリストを抽出します。 5. 抽出したセグメントを配信情報と一緒にBigQueryへ連携 次にIIASで抽出したトークンリストを配信文言等の情報と一緒にBigQueryへ連携します。BigQueryへわざわざ連携する理由としてはいくつかあります。ZOZOTOWNではDWHとしてBigQueryを利用しています。しかしMAでは以前から分析基盤としてオンプレミス環境でIIAS( 正確には前世代機であるNetezzaおよびPureDataのリプレイスを重ねてきた )を利用していました。この環境を今後BigQueryへ移行する計画があり、その際にアプリケーションのロジックを変えないために今からBigQueryを中心とした配信アプリケーションを作成しました。 また、移行先の選定理由で紹介したようにFCMでの配信実績はBigQueryに直接エクスポートが可能です。そのためBigQueryに配信のための情報が溜まっているとあとから解析しやすいという理由もあります。 今の段階でIIASでのセグメント抽出を廃止しなかった理由としては、変更すべき範囲が大きすぎてしまい移行期間がかなり伸びてしまうため、IIASの利用廃止は後回しにしました。 6. アプリケーションからFCMへ配信リクエストしユーザにPUSH通知が届く 最後にBigQueryのデータを利用しアプリケーションからFCMへ直接リクエストすることでユーザへPUSH通知が届きます。このときPUSH配信用SaaS利用時は同一メッセージについて同時にリクエストできるユーザ数が100,000件でした。それに対し、FCMでは1,000件ずつしか配信できないため、高速に配信するためには工夫が必要となりました。それについては機会があれば別の記事で紹介いたします。 移行手順 続いてどのように配信ツールを移行したかについて紹介します。 移行の要件 移行に際して、以下の要件を満たす必要がありました。 既存のPUSH通知を止めることなくシームレスに移行する FCMで利用するFirebaseプロジェクトをiOS/Androidで統一する それぞれについて紹介します。 既存のPUSH通知を止めることなくシームレスに移行する ツールの移行に伴って、移行を理由にPUSH通知での配信を止めることはユーザへの価値提供の機会を損なうことになってしまいます。そのため、ツールの移行を今まで通りの配信が担保された状態で行う必要がありました。 FCMで利用するFirebaseプロジェクトをiOS/Androidで統一する 移行前の仕組みでも紹介した通り、外部のPUSH配信用SaaSではAndroidの配信でFCMを間接的に利用していました。Androidにおいては基本的にはiOSと同じ共通のFirebaseプロジェクトを利用していましたが、PUSH配信用SaaSで利用するFCMのみ別のプロジェクトを利用していました。これは以前よりAndroidの実装を複雑にする要因となっていました。今回FCMへ移行するに当たり、Firebaseプロジェクトを統一することで配信処理がシンプルになるなど、バックエンドの構成的にも統一することによるメリットがありました。 移行手順 配信の移行は以下の3つの手順に分けました。 iOSからPUSH配信用SaaSでの配信、並びにFCM両方で配信 iOS/AndroidでFCMのみ配信できるようにしバージョンアップ済みユーザ全員へFCMで配信 FCMのみで配信 1. iOSからPUSH配信用SaaSでの配信、並びにFCM両方で配信 はじめに今まで利用していたPUSH配信用SaaSとFCMで同時並行に配信できる仕組みを実現します。これにより、iOS/Androidの実装と配信側の実装をぞれぞれのタイミングで進めることが可能となります。また、FCMでの配信になにか問題が発生した場合、既存のPUSH配信用SaaSでの配信に戻せばいいため安全に移行できます。アプリと配信側の実装や切り替えを一斉に行うと、すべてのキャンペーン配信を同時に移行しなければなりません。しかし今回実施するようにどちらからも配信できるような環境を用意することで、一部の配信のみをFCM側で配信し、徐々にFCMでの配信に移行していくことができます。 この段階では、iOSのみ既存のPUSH配信用SaaSとFCMどちらでも配信可能な状態にしました。Androidではこのフェーズを挟みませんでした。それはFCMのGCPプロジェクトの移行が関係しています。前述の通り、Androidでは既存のPUSH配信用SaaSで利用するFCMのためだけに専用のGCPプロジェクトを利用していました。そして今回の移行段階で、FCMにおいてもiOSと共通のGCPプロジェクトに移行させます。そのため既存のPUSH配信用SaaSとFCMでの配信をどちらも行おうとすると、2つのGCPプロジェクトのFCMを利用するため実装がかなり複雑になってしまいます。 2. iOS/AndroidでFCMのみ配信できるようにしバージョンアップ済みユーザ全員へFCMで配信 最初のフェーズで、すべてのキャンペーン配信がFCM経由で成功したことを確認できたら次のフェーズに移ります。 続いてiOS/AndroidでFCMからの配信しか受け取らないバージョンをリリースします。このタイミングでは、バージョンアップ済みのユーザにはFCMで、バージョンアップ前のユーザには既存のPUSH配信用SaaSで配信します。 3. FCMのみで配信 最後にほとんどのユーザがFCM対応バージョンに移行しきったタイミングで既存のPUSH配信用SaaSからの配信を止めます。これによってFCMでの配信に100%切り替わります。このとき、事前にバージョンアップしないとPUSH通知が停止する旨をアプリ上にてお知らせすることでユーザにバージョンアップを促すことができました。 以上のように既存のPUSH配信用SaaSからFCMへシームレスに移行できました。全体として2,3か月をかけての移行となりました。 今後の展望 移行後の構成を再掲します。 以上の構成では、FCMへの移行は完了していますが、課題がいくつか残っています。ここでは、それらについて説明し、最終的な構想を紹介します。 既存の課題 トークンの連携 2つのDWH 2つのワークフローエンジン トークンの連携 前述の通り、PUSH配信用のFCMトークンは最初にZOZOTOWNのDBに保存され定期的に配信用のDWHに連携されています。これは、MAというマーケティング向けのシステムがZOZOTOWNと切り離されているためです。これにより、データ連携という工程が発生し、リアルタイムな配信を難しくしています。しかしFCMトークンはMAでのみ利用するもののため、ZOZOTOWNのDBに保存するメリットはほとんどありません。 2つのDWH ZOZOTOWNのDWHとしてはBigQuery、配信用のDWHとしてはIIASを利用していると紹介しました。そして最終的にはBigQueryにDWHを統一させたいため、IIASで行っている処理をBigQueryに移管します。またそのために必要なデータもすべてBigQueryに移行する必要があります。 2つのワークフローエンジン 最終的にPUSH配信はアプリケーションからFCMにリクエストすることで行われます。また、元々の配信もアプリケーションからPUSH配信用SaaSにリクエストすることで行われていました。そして、それらはワークフローツールを介して起動しています。元々、PUSH配信用SaaSで配信していたアプリケーションはJP1というワークフローツールから起動し、オンプレミス環境で動作しています。また、JP1ではスケジューリングの機能を利用し定期的にバッチ配信をしています。新しく作成したFCMで配信するアプリケーションはDigdagというワークフローツールから起動されAWS上で動作しています。そして現状では、Digdagではスケジューリングの機能を使わずJP1からキックする形で利用しています。以下がアプリケーションの流れです。 移行前 移行後 Digdagから直接アプリケーションを起動しない理由としては、セグメントの抽出処理はまだJP1側のアプリケーションで行っているからです。今回そこもスコープが大きくなるため移行を後回しにしました。そのため最終的には以下のようなDigdagを中心としたアプリケーションにすることを目指しています。 構想 以上の課題をなくし、最終的には以下のようなシステムを構想しています。 まず、前述の課題を解決しDWHをBigQuery、ワークフローツールをDigdagにします。そして、トークンの取得をZOZOTOWNのDBから切り離し、MAとして管理しているDBに直接保存して利用します。それに加えてリアルタイム配信ができるように、専用のAPIを用意しリアルタイム配信が可能となるような状態を目指します。 まとめ 今回、外部のPUSH配信用SaaSからFCMへ配信ツールを移行した流れを紹介しながら、ZOZOTOWNでのPUSH配信基盤について紹介しました。移行において、PUSH通知を止めないよう安全に移行できました。この移行事例や配信基盤が事例として少しでもお役に立てれば幸いです。 終わりに 本記事で紹介したようにZOZOTOWNでは、多くのユーザへ高速かつ正確にコンテンツを配信するための基盤を開発運用しています。まだまだ発展の途中ですので、ご興味のある方は以下のリンクからご応募ください。 https://hrmos.co/pages/zozo/jobs/0000016 hrmos.co
こんにちは。ZOZOテクノロジーズZOZOTOWN部 検索チーム 兼 ECプラットフォーム部 検索基盤チームの有村( @paki0o )です。 ZOZOTOWNではこれまで度々紹介してきた通り、検索エンジンとしてElasticsearchを利用しています。リクエスト元のサーバーサイドのアプリケーションはJava(Spring Boot)で書かれており、クライアントにはHigh Level Rest Client(以下、HLRC)を使用しています。 www.elastic.co techblog.zozo.com HLRCを実際にプロダクション環境で運用していく中で、サービスのSLAを満たすために安定稼働させるための設定や、効率的に通信するための設定などを細かく指定しました。現在の設定にたどり着くまで、ドキュメント上で表現されていなかったり機能が用意されていなかったり等様々な苦労があったので、まだ道半ばですが現時点で辿り着いた設定内容についてご紹介いたします。同じくHLRCを利用されている方の参考になれば幸いです。 タイムアウト値の設定 コネクション数の設定 通信のgzip圧縮対応 インデキシングバッチでのトラフィック検証 検証結果 適用前 適用後 検索リクエストでのCPU負荷検証 検証結果 HLRCのシングルトンインスタンスのエラーハンドリング・再作成 実装 まとめ 最後に タイムアウト値の設定 公式ドキュメント にも記載のある通り、HLRCでは3種のタイムアウト値が設定可能です。なお、この値はHLRC固有のものではなく、内部で使用しているApache HttpClient共通の設定項目です。(ref : RequestConfig ) 設定項目 説明 デフォルト値 connection request timeout コネクション取得時のタイムアウト -1(undefined) connect timeout コネクション確立時のタイムアウト 1000ms socket timeout ソケット通信の監視用タイムアウト値 30000ms HLRCにてこの設定を上書きするためには、setRequestConfigCallbackにてRequestConfigを上書きします。 RestHighLevelClient client = new RestHighLevelClient( RestClient.builder( new HttpHost(host, port, "https" )) .setRequestConfigCallback(requestConfigBuilder -> requestConfigBuilder .setSocketTimeout(socketTimeout) .setConnectTimeout(connectTimeout) .setConnectionRequestTimeout(connectionRequestTimeout))); 公式のドキュメントには一部タイムアウト値の設定方法に関する記述はありましたが、 connection request timeout に関する記述は無く、またデフォルト値の設定もありませんでした。そのため、実際の運用では connection request timeout にも独自の適切な値を入れ運用しています。 コネクション数の設定 コネクション数も同様に、クライアント生成時にオプションとして設定可能です。こちらもHLRC固有のものではなく、内部で使用しているApache HttpClient共通の設定項目です。(ref : ドキュメント ) 設定項目 説明 デフォルト値 max conn per route IP、 ポート単位の最大接続数 10 max conn total 最大接続数 30 RestHighLevelClient client = new RestHighLevelClient( RestClient.builder( new HttpHost(host, port, "https" )) .setHttpClientConfigCallback(httpAsyncClientBuilder -> httpAsyncClientBuilder .setMaxConnPerRoute(maxConnPerRoute) .setMaxConnTotal(maxConnTotal))); リリース後しばらくはデフォルトの設定で運用していました。しかし実運用中、クラウド障害に起因したElasticsearchのレスポンス速度低下が発生した際、受け付けたリクエストがコネクション取得待ちで詰まる現象が発生しました。そのため現在はコネクション数に任意の値を追加し、合わせてクエリタイムアウト設定値も見直して運用しており、設定後同様の挙動は見られていません。 なお、ここまでで紹介したタイムアウト・コネクション数のHLRCにおけるデフォルト値は、 org.elasticsearch.client.RestClientBuilder に記載されています。 通信のgzip圧縮対応 弊社が検索を 全面的にElaticsearchへ移行 した2020年4月時点(Elasticsearch v7.6.2)では、HLRCがgzip圧縮に対応していませんでした。もう少し具体的な説明をすると、リクエストの圧縮には対応しておらず、レスポンスの圧縮は RequestOptions を用いヘッダへ Accept-Encoding: gzip を付与する必要がありました。しかし、Low Level Rest Clientが圧縮されたレスポンスの解凍に対応していなかったため、エラーが発生しており利用を断念していました。 そこからしばらくIssueをウォッチしていたところ、8月リリースのv7.9.0でレスポンスの解凍が、12月リリースのv7.10.1でリクエストの圧縮がサポートされていました。またv7.10.1では、クライアント生成時のオプションに指定することで、必要最低限の設定でリクエスト・レスポンスの圧縮が可能となりました。 github.com github.com 弊社ではインデキシングバッチと検索APIがそれぞれ独立したシステムで動いており、その両方がHLRCを利用していますが、それぞれリクエストの特徴が異なります。 インデキシングバッチ 検索API リクエスト量 < 5req/sec > 100req/sec リクエストサイズ > 10MB/req < 5KB/req そのため、今回はそれぞれの特徴に合った検証となるよう、インデキシングバッチではトラフィック検証を、検索では受け側であるElasticsearchのCPU負荷検証を行いました。 なお、本検証は一般的なgzip圧縮による効果の検証であり、Elasticsearchに限られたものではない点にご注意ください。 インデキシングバッチでのトラフィック検証 インデキシングバッチ側では、上述の通りトラフィックがどの程度減少するのかを確認しました。 gzip圧縮適用のためHLRC生成時に指定する設定は以下の通りです。 RestHighLevelClient client = return new RestHighLevelClient( RestClient.builder( new HttpHost(host, port, Constants.HTTP_SCHEME)) .setCompressionEnabled( true ) 上記の設定により以下2点が有効化されます。 Request Bodyの圧縮 Request Headerへの Accept-Encoding: gzip の付与 検証結果 以下、圧縮前後の比較検証です。なお、インデキシングのリクエストはbulkで行っており、1リクエスト辺りのサイズは圧縮前で約1MBです。 平均 最大 改善率 1.5MiB/sec 3MiB/sec - 100KiB/sec 210KiB/sec 93% 適用前 適用後 インデキシングバッチはApp Engine上で稼働しており、トラフィックに基づいた課金が発生しています。この改善によって送信量が格段に下がり、バッチの運用にかかる費用を1カ月当たり2~3万円ほど下げられました。 検索リクエストでのCPU負荷検証 一般的に、圧縮されたリクエストで通信するケースでは、圧縮側・解凍側それぞれ追加のCPUコストが発生します。これはElasticsearchでも当てはまることで、無圧縮状態の通信より必要となるリソースの増加が考えられます。 そのため、実際の検索リクエストをサンプリングしたものを用いて、CPU負荷がどの程度上昇するか検証しました。 検証はElastic Cloud上に構築した専用クラスタで行い、Coordinating Nodeを1台、Data/Master Eligible Nodeを3台用意しました。 検証結果 以下が実際にリクエスト数を増やした際のCoordinating NodeのCPU負荷遷移です。 上図の通り、gzip圧縮ありのケースの方が、無しのケースに比べてCPU負荷が5%~10%増しでした。この上昇幅を致命的とみるかどうかはケースバイケースですが、あくまでも1ノードに対して負荷をかけた場合の値であり、本番同等のスケールでは致命的なレベルでないと判断しました。 HLRCのシングルトンインスタンスのエラーハンドリング・再作成 Spring BootでHLRCを利用する際、DIコンテナに登録しシングルトンなオブジェクトとして使うことは一般的な利用方法かと思いますが、そのエラーハンドリング周りに一癖あり苦労させられました。 具体的にはHLRC固有の問題ではなく、さらにそのバックエンドで用いられているLow Level Rest Clientが抱えている問題であり、Issueとしても挙げられています。内部で利用しているHttpClientのステータスが使用不可( I/O reactor status:STOPPED )な状態になった際、そこからのリカバリ策が現状用意されていません。 github.com ここで解決策として以下の2つの手段が挙げられていましたが、直感的にも確実にリカバリが可能なことから弊チームでは後者を採用することとしました。 HttpClientに対して 独自のコールバック を定義し、I/O reactorを再作成する インスタンス自体を一旦破棄し、再作成する 実装 手元で事象を再現したところ、利用不可な状態になるケースではその直前に ConnectionClosedException の発生が確認できたため、この例外を捕捉した場合だけ再作成することとしました。 @Repository public class ElasticsearchAdapter { @Autowired private EsConfig esConfig; private RestHighLevelClient esClient; @Autowired public void setEsClient(RestHighLevelClient esClient) { this .esClient = esClient; } public SearchResponse search(SearchRequest searchRequest) { SearchResponse searchResponse = null ; try { searchResponse = esClient.search(searchRequest, RequestOptions.DEFAULT); } catch (Exception exception) { reCreateClientOnError(exception, esClient); } return searchResponse; } private void reCreateClientOnError(Exception e, RestHighLevelClient client) { if (e instanceof ConnectionClosedException) { try { client.close(); this .setEsClient(esConfig.esClientReCreate()); } catch (Exception ex) { throw new Exception(); } } } } @Component @Configuration public class EsConfig { @Value( "${spring.elasticsearch.es-username}" ) private String esUsername; @Value( "${spring.elasticsearch.es-password}" ) private String esPassword; @Value( "${spring.elasticsearch.es-host}" ) private String esHost; @Value( "${spring.elasticsearch.es-port}" ) private Integer esPort; private final Integer socketTimeout = xxxx; private final Integer connectTimeout = xxxx; private final Integer connectionRequestTimeout = xxxx; private final Integer maxConnPerRoute = xxxx; private final Integer maxConnTotal = xxxx; @Bean(name = "esClient" , destroyMethod = "close" ) RestHighLevelClient esClient() { return getEsClient(); } public RestHighLevelClient esClientReCreate() { return getEsClient(); } private RestHighLevelClient getEsClient() { final CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials( AuthScope.ANY, new UsernamePasswordCredentials(esUsername, esPassword)); RestHighLevelClient client = new RestHighLevelClient( RestClient.builder( new HttpHost(esHost, esPort, "https" )) .setHttpClientConfigCallback(httpAsyncClientBuilder -> httpAsyncClientBuilder .setDefaultCredentialsProvider(credentialsProvider) .setMaxConnPerRoute(maxConnPerRoute) .setMaxConnTotal(maxConnTotal)) .setRequestConfigCallback(requestConfigBuilder -> requestConfigBuilder .setSocketTimeout(socketTimeout) .setConnectTimeout(connectTimeout) .setConnectionRequestTimeout(connectionRequestTimeout)) .setCompressionEnabled( true ) ); return client; } } ElasticsearchAdapter リポジトリに登録された esClient オブジェクトを再作成することで、利用不可なクライアントを破棄・上書きしています。 この設定を適用して以降、利用不可なクライアントが生き残りエラーとなるケースに遭遇することは無くなりました。ただ、実装の素直さで言うと前者の実装方法の方が綺麗であることは間違いないため、機会があればそちらも検証予定です。 まとめ 本記事では、High Level Rest Clientのプロダクション環境での運用で得たノウハウについてまとめました。設定項目が多いだけに安定稼働までは苦労がありましたが、各種リクエストが全てラップされており、多くの恩恵を受けているので今後も利用を続けていきたいです。 余談ですが、クライアントの名称がドキュメント上では High Level Rest Client 、パッケージ上では RestHighLevelClient と語順に揺らぎがあり記事にする上で辛かったです。 最後に ZOZOテクノロジーズでは、このようなちょっとした改善・チューニングが好きなエンジニアはもちろん、これからの検索を更に改善していきたいエンジニアを募集しています。全国フルリモートでの採用もあるので、ご興味のある方は以下のリンクからぜひご応募ください! tech.zozo.com
こんにちは。SRE部の横田・秋田です。普段はZOZOTOWNのリプレイスや運用に携わっています。 私たちは2020年11月13日から14日にかけてフル・オンラインで開催された Hardening 2020 H3DX に参加しました。本記事では、過去にオフライン開催のHardening Projectに参加経験のある秋田と、今回が初のHardening Project参加となった横田の体験を振り返り、「サービスを守る訓練」の重要性を再確認してみます。 Hardening 2020 H3DXについて WASForum Hardening Project により開催されたイベントです。 Hardening 2020 H3DXは技術競技会であるHardening Dayが1日、全参加チームの施策発表などを聴講形式で進行するSoftening Dayが1日という、合計2日の2部構成で開催されました。 それぞれ、以下のような特徴があります。 Hardening Day 各チームに同じ条件で守るべきEコマースサイトなどのシステム環境(20台程度のサーバー)が提供される 競技中に次々と発生するサイバー攻撃やインシデントに対してチームで速やかに対応する マーケットプレイスからセキュリティ機器の購入や人員の増援なども各チームの判断で行える 技術面だけでは無くビジネスの売上、顧客対応など様々な観点から得点を競い合う 競技時間は8時間、振り返りや順位発表などは2日目のSoftening Dayで実施される Softening Day 全チームがHardening開催当日までの取り組みや、実際に行った当日の施策などを発表 運営側の攻撃チームなどからの解説、振り返り 優勝チームや協賛社が選ぶMVPチームなどの結果発表 過去の開催は沖縄などの地域で競技が行われていましたが、昨今の状況もあり今回はフル・オンラインでの開催となりました。 体験レポート 開催1か月前から当日までの動きを紹介します。 開催前日までの動き 開催の約1か月前に当日のチームメンバーが発表されました。 チーム名やチーム内での役職(CEOなどいくつかの役職が必須指定される)を期日までに運営へ報告する必要がありますが、基本的には各チームの裁量で必要に応じてMTGや事前準備などを実施します。 私たち2名は参加申し込み時に希望した通りの同じチームでの参加となり、合計10名のチーム編成となりました。ただし、必ず希望が通るわけではないようです。 多くのメンバーがほぼ面識の無い状態から始まりましたが、私たちはチーム発表当日に他のチームメンバーがコミュニケーションの場を提供してくれました。 開催当日までは以下のツールでコミュニケーションを行い徐々に打ち解けていくことができました。 Discord によるテキストチャットコミュニケーション Zoom やDiscordによるボイスチャンネルを活用したオンラインコミュニケーション 過去のHardening Project経験者のリードや過去のSoftening Dayの YouTube映像 を参考に以下のような事前準備が行われました。 スキルマップシートを作成してチームメンバーの経験などを相互に把握する 当日の役割分担を決める 役職以外にもLinux担当/Windows担当/カスタマーサポート対応担当 当日の連携手段を決める マーケットプレイスで何を購入するのかを決める 稟議書など必要になる可能性がある文面の雛形を作成する 当日利用できそうなスクリプトなどを各自用意する Googleドライブ を活用しチーム内の資料やツールを共有管理する タスク管理のツールを検討する際には Trello や Miro などが案として上がりましたが、ほとんどのメンバーがTrelloやMiroといったツールを利用したことがありませんでした。そこで、チームメンバー全員が利用経験のあるGoogleスプレッドシートを活用することにしました。 Hardening Day前日 前日に発表される資料があるので、その資料の読み込みと当日のタスク対応順序や連携方法などの詳細についての最終確認をチームで行いました。各担当ごとの事前に決めたタスクの内容をいくつか紹介します。 Linux担当 各Linuxサーバーにてアカウントの初期パスワード変更 各CMSの初期パスワード変更 Webアプリケーションのバックアップ crontabの確認 Windows担当 ローカルアカウントの初期パスワード変更 タスクスケジューラの確認 サービスの確認 Windowsファイアウォールの確認 カスタマーサポート対応担当 メールの送受信確認 マーケットプレイス用稟議書の準備 在庫の確認 Webページの正常性の確認 Hardening Day当日 いよいよ当日となりました。例年の開催では現地会場に参加者全員が集まり競技をしますが、Hardening 2020 H3DXはフル・オンラインでの開催のためDiscordで集合が確認できたチームからZoom会場へと向います。 オープニングを経て、実際に競技が始まります。今回は競技環境への接続は Apache Guacamole というツールを介して行いました。 次々に仕掛けられる攻撃 競技開始後から衛るべきサイトや様々な環境に対して次々に攻撃が仕掛けられてきます。 具体的にどのような攻撃が発生していたかについては、今後の参加者の楽しみを奪ってしまうことになるため割愛しますが、とにかく次々と問題が発生します。 あらかじめ決めておいた作業を実施することや、発生している問題に対して対応を進めることも重要なのですが、状況を共有しあうことがHardeningにとっては非常に大切です。 やらなくてはいけないことは技術的なことだけではない システムに対して手を加えていくことは当然必要なのですが、Hardening Dayはその他にもやらなければいけないことが多々存在しました。全てを記載できないのですが、例えば以下のような内容です。 発生してしまったインシデントに対する各方面への報告 メールを利用した顧客対応 サイトの売り上げ向上につなげるミッション 事前にある程度役割を決めていても、その担当者だけでは手が回らなくなってしまうこともありました。その際にはチームメンバーとコミュニケーションを取り、優先順位をつけて対応します。時には早急な復旧を不要と判断してシステムを放置する、という決断も行いました。 リモートならではのチーム内の工夫 オフライン開催では近くにチームメンバーがいますが、今回はリモート開催です。情報共有を円滑にするために今回チームで以下のような工夫をしました。 各役割ごとにDiscordのボイスチャンネルを作成しメンバーは自分の役割のボイスチャンネルに常駐する 10人全員が1つのボイスチャンネルに入っていると、なかなか全員で会話することが難しくなるため、今回は事前に決めておいた役割ごとに数人程度のボイスチャンネルを用意しました。 各役割ごとのテキストチャットチャンネルも作成して作業記録はそこに残す テキストチャットを追えば今どのような流れで対応がされているのかを後から参加したメンバーも確認できます。 これらの工夫をしていたため、当日はコミュニケーションについては特に不自由さを感じませんでした。 8時間という競技時間中、迫りくる攻撃やその他の対応により全員かなり疲れていましたが、離脱者も出ずになんとかHardening Dayを終えることができました。 Softening Day 前述の通り、前日の振り返りを行い、各チームがその内容を発表しました。 そして、表彰セッションです。 チームの結果は 総合優勝には手が届きませんでしたが技術点と対応点が1位でした。 この結果は、チームの全員がそれぞれの役割を果たしてくれておかげだと思います。しかし、売り上げが伴わなかったことはとても残念なことだったので、次回以降の教訓として活かしていければと思います。 Hardening 2020 H3DXに参加して 今回、横田はHardening Projectに初参加、秋田はオンライン形式には初参加でした。 普段はZOZOTOWNのSREエンジニアとして業務を行っていますが、Hardeningを通じて経営的な判断をすることの難しさや、実際の顧客対応の大変さなどが擬似的とはいえ体験できたことはとても良かったです。様々な問題に対して行ったアクション全てが正解だったとは言えませんが、競技だからこそ恐れずに失敗することができたとも言えます。 また競技中にチームで行っていたコミュニケーション手段の工夫などは実際に業務の中でトラブルが発生した際にも活かせるシーンが非常に多いと感じました。 コミュニケーションツールなどのインシデント対応をする際の環境やフローを整備する重要性、それらを全員が合意して認識していることの重要性を改めて認識しました。また、実際のインシデントが発生する前に訓練によって擬似的に体験しながら自分が行うべき行動を考えたり、チームで役割分担の再確認をする重要性も感じました。今後も積極的にHardening Projectに参加していきたいと思います。皆さんも是非Hardening Projectに参加し、起こりうるインシデントに備えましょう! ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
こんにちは。SRE部の塩崎です。七味唐辛子の粉末を7種類に分類するという趣味を発展させて、おっとっとを新口動物と旧口動物に分類するという趣味を最近発明しました。 BigQueryは非常にパワフルなData WareHouse(DWH) SaaSであり、大容量のデータを一瞬で分析できます。しかし、課金額がスキャンしたデータ量に比例するという特徴があるため、意図せずに大量のデータをスキャンしてしまい大金を溶かしてしまうことを懸念する人もいます。 qiita.com そのため、課金額が大きすぎるクエリを発見した際にSlackへ通知する仕組みを作りました。GCP Organization内の全プロジェクトで実行されたBigQueryの監査ログをリアルタイムにチェックすることによってこの仕組みは実現されています。本記事では作成したシステムを紹介します。 なお、本記事は以下のQiita記事に着想を得たものであり、 @irotoris さんにはこの場を借りてお礼申し上げます。 qiita.com 最初は元記事で公開されているソースコードをそのまま使用することを考えていたのですが、ライセンス表記がなかったため独自で実装し直し、我々のユースケースにおいて不足する機能を付け足しました。 BigQueryのコストを抑える方法 最初にBigQueryのコスト削減に有効な機能の紹介と、それらだけでは不十分であり、監査ログのリアルタイムスキャン機能を作成した理由について説明します。 Custom Quotas まず、BigQueryのコストを抑える方法として、プロジェクトレベル・ユーザーレベルにQuotaを割り当てる機能があります。 cloud.google.com この機能を使えば、特定のユーザーが高額なクエリを実行しすぎた際に、それ以上のクエリの実行を停止できます。 しかし、ユーザー毎にQuotaの上限を変えたい場合は、そのユーザーが属するプロジェクトを変える必要があります。プロジェクトを適切に分けてある場合はこの機能の導入がしやすいですが、そうでない場合は事故を防ぐために相当大きめのQuotaとして設定する必要が出てきてしまいます。特に落ちてはいけない大事なバッチが実行されるプロジェクトとアドホックなクエリが実行されるプロジェクトの分離は必須です。そうしない場合はアドホックなクエリによってQuotaが全部使い果たされてしまい、大事なバッチがusageQuotaExceededエラーになってしまいます。 我々の環境ではこれらの分離がまだまだ不十分であったため、この機能の導入は一旦見送りました。分離が十分にできた時点で導入を再度検討することを考えております。 Reservations 次に紹介するのがReservations機能です。 cloud.google.com この機能を使うと、スキャンしたデータ量によらず毎月固定の金額が課金されます。どれだけの計算量が必要なのかというのをSlotという単位であらかじめコミットします。このSlotはCPUを仮想化した概念で1Slotが1CPUに相当する量です。 cloud.google.com このReservations機能を使うことで、「お値段定額クエリ実行し放題プラン」になると勘違いしやすいですが、実際は異なります。 実際は「お値段定額『Slotの枠内で』クエリ実行し放題プラン」です。購入したSlot数を超えたパフォーマンスが出ることはないため、Slotを大量に消費するクエリが同時に実行された場合は、クエリの実行時間が伸びるという結果になります。Slotはプロジェクト毎でしか割り当てできないので、大事な集計バッチとアドホッククエリのプロジェクトを変えないと、集計バッチのSLAを担保できません。 我々の環境ではプロジェクトごとにReservations機能の有効・無効を切り替え、コストを最適化することを試みています。定常的にクエリが実行されているプロジェクトではこの機能を有効化しコストの予測可能性を高めます。 一方でクエリの実行頻度が低いプロジェクトやクエリの実行頻度が一定でないプロジェクトでは従来の料金プランと最低60秒の枠でSlotを購入できるFlex Slotsを利用しようとしています。 いずれのプロジェクトに対しても、スキャン量が多すぎるクエリや、Slot使用量が多すぎるクエリを発見しアラートを上げる仕組みが必要です。 そのため、BigQueryの課金ログをリアルタイムにチェックし、スキャン量やSlot使用量などが多すぎるクエリを通知する仕組みを作成しました。 インフラ構成 今回構築したシステムのインフラ構成図を以下に示します。 GCPのマネージドサービスを活用した、イベントドリブンかつサーバレスな構成です。 BigQueryの監査ログはデフォルトではONになっているため、Cloud Loggingに送られています。Organization内の全プロジェクトから送られてくる監査ログをAggregated sinks機能で集約し、Cloud Pub/Subの1つのTopicに集めます。 その後、Cloud Pub/SubのPush SubscriptionでCloud Runを起動します。HTTPリクエストのBodyの中に監査ログが含まれているため、Cloud Run上に実装したアプリケーションでクエリがリソースを使いすぎていないかどうかをチェックし、Slackに通知します。 ここからは各サービスの連携方法をTerraformのtfファイルを交えながら説明していきます。 Cloud Logging → Cloud Pub/Sub 一番最初にCloud Pub/SubのTopicを作成します。これについては特に詳しい説明は不要かと思います。 resource " google_pubsub_topic " " bq-police " { name = " bq-police " } 次に先程作成したTopicに対して、Cloud LoggingからpublishするためのLog Sinkを作成します。Organization内の全てのログを出力するために、Aggregated sinks機能を使っています。このSinkを作成するためにはOrganizationの logging.configWriter を付与したアカウントで terraform apply をする必要があります。 cloud.google.com org_id パラメーターは、 gcloud organizations list コマンドを実行すると確認できます。filterで指定しているクエリは以下のページを参考にして作成しました。古いタイプの課金ログ(AuditData)と新しいタイプの課金ログ(BigQueryAuditMetadata)の2種類があり、新しい側の利用が推奨されている点に注意が必要です。 cloud.google.com resource " google_logging_organization_sink " " bq-police-org " { name = " bq-police-org " destination = " pubsub.googleapis.com/${google_pubsub_topic.bq-police.id} " org_id = " 123456789012 " # 各自の環境で変える include_children = true filter = <<- EOT protoPayload.metadata." @type " = " type.googleapis.com/google.cloud.audit.BigQueryAuditMetadata " protoPayload.metadata.jobChange.job.jobConfig.type = " QUERY " EOT } そして、Cloud Loggingのサービスアカウント(writer_identity)にTopicへのpublish権限を付与します。Log Sinkはそれぞれ固有のサービスアカウントで実行されており、そのサービスアカウントに対する書き込み権限の付与が必要です。 cloud.google.com resource " google_pubsub_topic_iam_member " " bq-police-org " { project = google_pubsub_topic.bq - police.project topic = google_pubsub_topic.bq - police.name role = " roles/pubsub.publisher " member = google_logging_organization_sink.bq - police - org.writer_identity } Cloud Pub/Sub → Cloud Run 次にCloud Pub/SubとCloud Runの連携について説明します。 まずは、Cloud Runのサービスを作成します。アラートを発報するための閾値( THRESHOLD_* )やアラート対象から除外するユーザー( EXEMPTED_USERS )などを環境変数で設定できるようにしています。また、Slackに通知するためのWeb hookのURLは後述するBerglasで設定できるようにしています。 resource " google_service_account " " bq-police-cloud-run " { account_id = " bq-police-cloud-run " display_name = " BQ Police(Cloud Run) " } resource " google_cloud_run_service " " bq-police " { name = " bq-police " location = " us-central1 " template { spec { containers { image = " gcr.io/プロジェクトID/bq-police:latest " resources { limits = { cpu = " 1000m " memory = " 256Mi " } } env { name = " TZ " value = " Asia/Tokyo " } env { name = " THRESHOLD_TOTAL_BILLED_BYTES " value = " 0 " } env { name = " THRESHOLD_TOTAL_SLOT_MS " value = " 0 " } env { name = " EXEMPTED_USERS " value = "" } env { name = " SLACK_WEBHOOK_URL " value = " sm://プロジェクトID/slack_webhook_url " } } container_concurrency = 10 service_account_name = google_service_account.bq - police - cloud - run.email } metadata { annotations = { " autoscaling.knative.dev/maxScale " = " 10 " " client.knative.dev/user-image " = " gcr.io/プロジェクトID/bq-police:latest " " run.googleapis.com/client-name " = " terraform " } } } autogenerate_revision_name = true } Cloud Pub/SubからCloud Runを呼び出すために、Push Subscriptionを作成します。今回作成したCloud Runサービスは呼び出すための認証が必要なため、Pushする際にHTTPヘッダーへ認証情報を埋め込む設定をします。このSubscription専用のサービスアカウントを作成し、serviceAccountTokenCreatorのロールを与えます。これにより、このサービスアカウントは自身のOIDCトークンを取得できるようになります。 push_config で生成されたOIDCトークンをHTTPヘッダーに埋め込むように設定します。 cloud.google.com なお、このOIDCトークンを埋め込んだリクエストを生成するという方式はCloud Pub/Sub以外のプロダクトでも活用できます。以下の記事で詳しく解説されています。 medium.com そして、Cloud Pub/SubのサービスアカウントにCloud Runサービスの run.invoker ロールを付与することで、この認証済みリクエストに対する認可をします。 resource " google_service_account " " bq-police-pubsub " { account_id = " bq-police-pubsub " display_name = " BQ Police(Cloud Pub/Sub) " } resource " google_project_iam_member " " bq-police-pubsub " { member = " serviceAccount:${google_service_account.bq-police-pubsub.email} " role = " roles/iam.serviceAccountTokenCreator " } resource " google_pubsub_subscription " " bq-police " { name = " bq-police " topic = google_pubsub_topic.bq - police.name push_config { push_endpoint = google_cloud_run_service.bq - police.status [ 0 ] .url oidc_token { service_account_email = google_service_account.bq - police - pubsub.email } } } resource " google_cloud_run_service_iam_member " " bq-police-pubsub " { service = google_cloud_run_service.bq - police.name location = google_cloud_run_service.bq - police.location role = " roles/run.invoker " member = " serviceAccount:${google_service_account.bq-police-pubsub.email} " } アプリケーション 今回はアプリケーションの実行基盤にCloud Runを使っています。そのため、App Engine(Standard Environment)やCloud Functionsと比較して使用できる言語・フレームワークの自由度が高いです。使い慣れているという理由で、Ruby + Sinatraを使って実装してみました。特別なことはしていないので、詳しい説明は省略します。 require ' json ' require ' yaml ' require ' erb ' require ' sinatra ' require ' faraday ' THRESHOLD = { total_billed_bytes : ENV .fetch( ' THRESHOLD_TOTAL_BILLED_BYTES ' , ' 0 ' ).to_i, total_slot_ms : ENV .fetch( ' THRESHOLD_TOTAL_SLOT_MS ' , ' 0 ' ).to_i, } EXEMPTED_USERS = ENV .fetch( ' EXEMPTED_USERS ' , '' ).split( ' , ' ) WEBHOOK_URL = ENV [ ' SLACK_WEBHOOK_URL ' ] class AuditLog attr_reader :project_id , :total_billed_bytes , :total_slot_ms , :principal_email , :start_time , :end_time , :query def self . from_pubsub_format (data) # Cloud Pub/Subから送られてくるBigQueryの監査ログはBase64でエンコードされたJSON self .new( JSON .load( Base64 .decode64(data[ ' message ' ][ ' data ' ]))) end def initialize (log) # Ref: https://cloud.google.com/bigquery/docs/reference/auditlogs/rest/Shared.Types/BigQueryAuditMetadata @project_id = log[ ' resource ' ][ ' labels ' ][ ' project_id ' ] @principal_email = log[ ' protoPayload ' ][ ' authenticationInfo ' ][ ' principalEmail ' ] @total_billed_bytes = log[ ' protoPayload ' ][ ' metadata ' ][ ' jobChange ' ][ ' job ' ][ ' jobStats ' ][ ' queryStats ' ][ ' totalBilledBytes ' ].to_i @total_slot_ms = log[ ' protoPayload ' ][ ' metadata ' ][ ' jobChange ' ][ ' job ' ][ ' jobStats ' ][ ' totalSlotMs ' ].to_i @start_time = Time .parse(log[ ' protoPayload ' ][ ' metadata ' ][ ' jobChange ' ][ ' job ' ][ ' jobStats ' ][ ' startTime ' ]) @end_time = Time .parse(log[ ' protoPayload ' ][ ' metadata ' ][ ' jobChange ' ][ ' job ' ][ ' jobStats ' ][ ' endTime ' ]) @query = log[ ' protoPayload ' ][ ' metadata ' ][ ' jobChange ' ][ ' job ' ][ ' jobConfig ' ][ ' queryConfig ' ][ ' query ' ] end def duration end_time - start_time end def total_billed_gb total_billed_bytes.to_f / 1024 / 1024 / 1024 end def cost # Ref: https://cloud.google.com/bigquery/pricing/#on_demand_pricing total_billed_gb.to_f / 1024 * 5 end def total_slot_s total_slot_ms.to_f / 1000 end end class BqPolice < Sinatra :: Base def alert? (audit_log, threshold, exempted_users) if exempted_users.include?(audit_log.principal_email) return false end if audit_log.total_billed_bytes >= threshold[ :total_billed_bytes ] || audit_log.total_slot_ms >= threshold[ :total_slot_ms ] true else false end end def format_message (audit_log) YAML .load( ERB .new( File .read( ' slack_message.yml.erb ' )).result_with_hash( audit_log : audit_log)) end def post_to_slack (webhook_url, message) Faraday .post(webhook_url, JSON .dump(message), ' Content-Type ' => ' application/json ' ) end post ' / ' do audit_log = AuditLog .from_pubsub_format( JSON .load(request.body.read)) if alert?(audit_log, THRESHOLD , EXEMPTED_USERS ) message = format_message(audit_log) post_to_slack( WEBHOOK_URL , message) end status 200 end end 上記のRubyコードで参照している slack_message.yml.erb を以下に示します。 username : 'BigQuery Police' icon_emoji : ':cop:' channel : '#投稿したいチャンネル' attachments : - text : "BigQuery cost alert" fallback : "BigQuery cost alert" color : 'danger' fields : - title : 'User' value : <%= audit_log.principal_email %> short : true - title : 'Project' value : <%= audit_log.project_id %> short : true - title : 'Billed bytes (GB)' value : <%= audit_log.total_billed_gb.round(2) %> short : true - title : 'Cost (USD)' value : <%= audit_log.cost.round(2) %> short : true - title : 'Start time' value : <%= audit_log.start_time.getlocal.to_s %> short : true - title : 'End time' value : <%= audit_log.end_time.getlocal.to_s %> short : true - title : 'Duration (sec)' value : <%= audit_log.duration.round(2) %> short : true - title : 'Slot time (sec)' value : <%= audit_log.total_slot_s.round(2) %> short : true - title : 'Query' value : |- ``` <%= audit_log.query.slice(0, 3000).gsub("\n", " \n " ) %> ``` short : false このアプリケーションを動かすためのDockerイメージを作成します。SlackのWeb hook URLはSecret Managerに格納されており、それをBerglas経由で取得しています。そのため、Berglasのバイナリをコンテナ内に入れ、Berglas経由でPumaを起動しています。Pumaの設定ファイルやGemfileは省略します。 FROM ruby:2.7-slim COPY --from=us-docker.pkg.dev/berglas/berglas/berglas:latest /bin/berglas /bin/berglas RUN apt-get -qq update && \ apt-get -qq -y install build-essential --fix-missing --no-install-recommends WORKDIR /usr/src/app COPY Gemfile Gemfile.lock ./ ENV BUNDLE_FROZEN=true RUN gem install bundler && bundle config set --local without ' test ' && bundle install COPY . ./ CMD [ " /bin/berglas ", " exec ", " -- ", " /usr/local/bundle/bin/puma ", " -C ", " puma.rb " ] Cloud RunがBerglas経由でSecret ManagerからSlack Web hook URLを取得できるように、以下のコマンドを実行しておきます。 berglas create sm://プロジェクト名/slack_webhook_url " SlackのWeb hook URL " berglas grant sm://プロジェクト名/slack_webhook_url --member serviceAccount:Cloud Runのサービスアカウント まとめ BigQueryの監査ログをリアルタイムにCloud Runで処理することによって、BigQueryで高額なクエリを実行されたときにいち早く気づくことができるようになりました。スキャン量に対するアラート条件だけでなく使用したSlot数に対する条件を指定できるようにし、オンデマンド課金のプロジェクトでもFlat rate課金のプロジェクトでも使用できました。 なお、上図は意図的に閾値を厳しくした時の通知であり、実際にはこのレベルのクエリでアラートを発報させてはいません。 ZOZOテクノロジーズでは多数の社員から使われるデータ基盤のデータガバナンスを高められる人材を募集しています。ご興味のある方は以下のリンクからご応募ください。 https://hrmos.co/pages/zozo/jobs/0000017 hrmos.co
こんにちは、ZOZOテクノロジーズ CTO室の池田( @ikenyal )です。 2020年のZOZOテクノロジーズのテックブログは、本記事で100本目に到達しました。1年間の公開記事数としては、過去最多であり、百の大台に乗れたことを嬉しく思います。 それを記念し、この記事ではテックブログに力を入れる理由であったり、どのような運用をしているか・何に気をつけているのかについて触れたいと思います。スケジュールとマイルストーンの仕組み化、公開日時を調整する際のポイント、記事の編集・校正時のポイント、日々の執筆者トレーニングについてもご紹介します。 techblog.zozo.com なぜテックブログに力を入れるのか 「テックブログを頑張ろう」というフレーズは採用を強化していきたい多くの企業で耳にするものだと思います。 では、なぜテックブログにコストをかけてまで注力するのでしょうか。ZOZOテクノロジーズにおいても、今年その根幹に立ち返り、再度議論をしました。 テックブログの目的に立ち返る 私の所属するCTO室では技術広報も職務領域です。そのため、テックブログに関してもその目的の明確化を行い、KPIを定めた運用を続け、エンジニアに対してそれらを伝えていく責任があります。 なお、 弊社のテックブログ は、現在のZOZOテクノロジーズ(当時のスタートトゥデイテクノロジーズ)に吸収合併された一社であるVASILYのテックブログが前身です。 techblog.zozo.com VASILY時代より、現在のCTO今村が先頭に立ち、テックブログを継続していました。その当時からテックブログを実施する目的は変わっていません。その目的に関しては今村の以下の記事で社内向け説明ページのキャプチャを添えて触れられています。 note.com 目的からやらないといけないことを細分化する しかし、大きな目的が定められていたとしても、それだけでは四半期や半年、1年単位で何をやるべきかが明確になりません。 そこで、CTO室と 広報 で連携し、日々の技術広報のKPIを定められるよう、直近から3年間で技術広報が目指す先、つまり技術広報戦略を定めました。 まず、「最終的に目指す先」を定め、そこから「それを実現するためには何が必要なのか」を順に考えていきます。「何をどの順に達成しないといけないのか」のステップを明確にしていくのです。 これをすることにより、テックブログをやることによって最終的に何を目指していて、それに向けて今は何をやらないといけない段階なのかが可視化できます。そして、それを用いることで社員へ明確な説明ができると同時に納得をしてもらえる材料になります。実際に今年、この技術広報の背景をCTO室通信(毎月CTO室が公開している社内報)で解説し、社員から「技術広報やテックブログの背景を知ることができて良かった」という意見も得られ好評でした。 では、弊社の場合、どのようにステップを分解していったのかを紹介します。 ZOZOの経営戦略に「MORE FASHION x FASHION TECH」が掲げられています。 corp.zozo.com このZOZOグループの大きな目標を技術の力で実現させることが、我々ZOZOテクノロジーズという企業が果たさねばならない最終目標です。 次に、その「MORE FASHION x FASHION TECH」が実現されるために必要な状況を考えます。「世の中により価値のあるプロダクトを届ける」ということが、必要だと考えました。利用していただくための「価値あるプロダクト」がなければ、世界中の皆様に何も届けることができない、という考え方です。 さて、そのようなプロダクトを届けるためには何が必要でしょうか。何か構想があってもそれを作り上げるエンジニアが不在では形になりません。つまり、素晴らしいエンジニアが会社に集結し、会社全体の技術力の向上が必要でしょう。 そして、次に素晴らしいエンジニアが集まる会社になるために何をすべきかを考えます。エンジニアが入社したくなるような「会社のファン」を増やすために、認知度向上のための技術発信が必要でしょう。いわゆる、技術ブランディングに力を入れる必要があるのです。弊社もここに注力をしている状況です。 このように、最終的なゴールから直近で達成しなければならないポイントを明確にすることにより、「なぜやるのか」「どのような意味があるのか」が自然と明確になります。 ここまでの流れをまとめると、以下のようなステップに分解できました。 直近から3年間の目指す先を明確化する 直近の目標である「発信によって認知を広め、会社のファンを増やす」ための方針を定めます。詳細は割愛しますが、弊社では3カ年計画を定め、2020年度・2021年度・2022年度で、それぞれどのような状況を目指すのかを具体的に定めました。それらを元に、テックブログ、外部登壇や自社主催のイベント開催など、それぞれの技術広報における施策のKPIを定め、CTO室と広報の定例で数値を追うようにしました。 具体的なKPIを定めることにより、並行する多くの施策をバランス良く継続できます。 テックブログの業務だけを行う専任者がいることは稀であり、エンジニアが他の業務と掛け持ちで運営していることが多いかと思います。そのような状況だと「気づいたらやらなくなっていた」「忙しくて放置状態になっていた」という状況が起こりがちでしょう。そのような状況が発生しかけたとしても、定例で数値を確認することになるので、早期のリカバリが可能になり、継続的な施策として定着させる手助けになるでしょう。 どのような運用をしているのか ここでは、テックブログを日々どのように運用しているのか、その一部をご紹介します。この運用に関しては、CTO室の設立後、仕組み化を大きく導入できた領域です。 弊社のテックブログの記事はすべてCTO室のレビューを必須としており、私が担当しています。執筆開始前の概要確認から、執筆後の編集・校正をすべて行っています。他の業務をこなしつつも、100本の記事をレビューしてきました。まるで編集長ですね。 なぜそこまでチェックをしているのかと言うと、前述の「技術ブランディングのため」というのが答えです。テックブログは個人の作業メモや備忘録ではありません。企業の公式な発信媒体であり、それらの記事が読者の皆様に価値を与えないと意味がありません。「価値」とは、チュートリアルに載っていない細かい部分の新しい知見の共有であったり、新規性のある独自に工夫をした手法・アーキテクチャ、ZOZOTOWNの大規模トラフィックにいて得られる知見、などです。我々の発信よって、それを参考にした社外のエンジニアのスキル向上につなげ、業界全体・社会への還元につながれば幸いです。 また、記事の事前チェックにより、間違った内容だけでなく、社外秘の情報や数値が含まれていないか、いわゆる炎上につながるような不適切な表現がないか、を確認する役割も大きいです。それらが一度でも外部発信に含まれてしまうと、ブランディングに影響を与え、それまでの苦労が一瞬で水の泡となりかねません。 スケジュールとマイルストーンの仕組み化 まず、CTO室が四半期(3か月)ごとに、エンジニアが所属する部に記事の募集をかけます。この段階で、「どのチームが、どの週に記事を公開するか」までを確定させます。これを四半期が始まる1か月程前に確実に行うことにより、期初から計画的なペースで記事を公開できるようになります。そして、慌ただしくなりがちな期初の執筆者に対しても早めに調整・リマインドをできる環境を作っています。 その際に、 前述の今村の記事 でも紹介しているようなマイルストーンを定義し、チェックボックス形式でその進捗を管理しています。それぞれのマイルストーンに対し、記事公開の何日前までに完了しなければいけないのかの期日も定め、その期日を過ぎてもチェックボックスに動きがない執筆者に対してリマインドと状況確認を行います。 リマインドの期日には多少の余裕を持たせているため、ここで問題が発生してもリカバリが可能です。場合によっては、その次週公開予定の執筆者と公開日の調整を行うことも可能です。 この早めの遅延検知により、「実は期日ぎりぎりで未着手でした」という事態を防止します。期日ぎりぎりでの執筆になると、執筆者自身も辛くなりますし、運営側もレビュー時間の不足やスケジュールの破綻が起こって辛い状況になります。場合によっては、期日の兼ね合いで不完全な状況での記事公開をせざるを得ないということにもなりかねません。誰も得をせず、お互いに不幸な状況になってしまいます。そのような状況に陥ると、テックブログに対して全員が後ろ向きになり、施策が停滞してしまう可能性が高くなります。 ここからは、運営者として気をつけているポイントをいくつか紹介します。 公開日時を調整する際のポイント 1つ目は公開日時を調整する際のポイントです。 弊社のテックブログも以前は更新頻度が一定ではなく、停滞する時期もありました。そこから定期的な発信を続けたところ、テックブログ自体のアクティブユーザー数が維持・増加することが見えてきました。そのため、1日にまとめて記事を公開するのではなく 1日1本ずつ継続して公開する ようにしています。同じ週の担当になった執筆者の中で、進捗状況を考慮しながら公開日が別になるように割り振っています。 さらに、公開時間も調整しています。特に情報解禁の時間であったり、プレスリリースに合わせたりする必要がなければ、基本的に 午前11時の公開 をしています。これは、Google Analyticsの曜日時間帯別PVのヒートマップから、1番読まれる時間帯の開始時刻に合わせての設定です。平日と休日でも読まれ方に差があるので、土日祝日の公開は基本的に避けています。なお、土日祝日を避けるのは、万が一、記事の内容に修正が必要になった場合、迅速な対応が取りにくくなるリスクを避ける意味もあります。 記事の編集・校正時のポイント 2つ目に記事の編集・校正時のポイントです。 固有名詞の表記を正しく書く ことは当たり前のことですが、指摘する回数が非常に多いものです。例えば、「Slack」を「slack」と表記されるパターンをよく目にします。Slackの場合、ロゴの文字は小文字のsなのですが、固有名詞としては大文字のSで始まるSlackが正しい表記です。記事で固有名詞を使う際には、記憶やロゴの見た目で判断せず、公式サイトで今一度確認をしましょう。GitHubをGithubとするパターン、YouTubeをYoutubeとするパターンも非常に多いです。 また、我々はZホールディングスグループなので、ヤフーとも親戚関係です。そのため、以前よりもヤフーの名称を社員が利用する機会が増えました。Yahoo!Japan、Yahoo!JAPAN、Yahoo!JAPAN、Yahoo・Yahoo!など多種多様な表記をされることがあるのですが、これらは正しい表記ではありません。会社としては「ヤフー」「ヤフー株式会社」、サービスとしては「Yahoo! JAPAN」と書くのが正しい表記です。年賀状やメールを出す際に相手の名前の漢字に気をつけますよね。会社名もそれと同じです、 正しい表記で記載するのはマナー だと考えています。 他には、 表記揺れ も修正対象です。記事内で表記揺れがあったり、同じ事象を指す用語が別の呼び方になっていると、読み手が理解しにくくなるため、統一をします。ここで気をつけているのは、 テックブログ内でルールをがちがちにはしていない ところです。「ください」はひらがなで、「出来る」はひらがなで、などしても良いのですが、そこまではしていません。執筆者はそれぞれ別なので、 どの表記を採用するのかは執筆者ごとの個性として捉え、記事内での統一に留める ようにしています。ただし、その個性も、正しい表記を利用していることが大前提です。 加えて、テックブログなので、「!」の多用や、「〜な気がします」「〜かもしれません」などの曖昧な表記があれば修正をしています。記号類は冒頭や末尾の挨拶部分であれば良いのですが、テックブログとして真面目に発信している本文内では適さないと判断しています。また、曖昧な表現で断言しないことも同様に適しません。自信のない内容や、断言しきれない「個人的の見解」ばかりだと、テックブログとして読者が参考にして良いのか判断に困ってしまいます。執筆の際には自信を持って言い切れるように、調査や準備をしておく必要があります。 なお、textlintによる自動校正も導入していますが、最後は人による確認が必要でしょう。 日々の執筆者トレーニング そもそも、テックブログを書く行為はある程度のスキルや経験がどうしても必要になってしまいます。そのため、テックブログをいきなり書く自信がないエンジニアには、アドベントカレンダーでその練習をしてもらったりしています。2020年のアドベントカレンダーでは、12月の25日間で合計100本の記事を発信できました。 qiita.com qiita.com qiita.com qiita.com どのような効果があったのか それでは、実際に継続的な発信をすることによって、どのような効果があったのかを紹介します。 前述の直近の目標である「発信によって認知を広め、会社のファンを増やす」を計測する方法は色々とあり、ブランディング調査などもその一環として行っています。しかし、それらは毎日実施することはできず、定期的な計測になってしまいます。 そのため、日々の動きを確認できる、Google Analyticsでのアクティブユーザーの推移を一つの指標としています。 今回はその推移を紹介します。 これは2020年のアクティブユーザー数の推移を示したグラフです。 そして、次に示すのが2020年のテックブログの公開記事数です。 統計的に有意かどうかまでは分析していませんが、直感的には記事公開が多くなればアクティブユーザーが増えているように見受けられます。 また、2020年に入り、採用面接時にテックブログについて言及してもらえる回数も増えてきました。 2020年の人気記事紹介 2020年に公開した記事の中から、閲覧数が多い記事5位を紹介します。なお、実際の閲覧数ランキングでは、2020年の記事以外の過去の記事も多く入っています。有益な記事を公開すれば、何年も閲覧され続けることが分かります。 techblog.zozo.com techblog.zozo.com techblog.zozo.com techblog.zozo.com techblog.zozo.com 最後に ZOZOテクノロジーズでは、プロダクト開発以外にも、イベントの開催やテックブログなど、外部への発信も積極的に取り組んでいます。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! https://tech.zozo.com/recruit/ tech.zozo.com
はじめに こんにちは。2020年5月に入社しましたMA基盤チームの辻岡です。 MA基盤チームでは、マーケティングに関わる様々なプロダクトやシステムの施策開発・運用を行っています。その中の1つにリアルタイムマーケティングシステムというものがあります。 これまでこのシステムには検証環境が存在しませんでした。そこで、検証環境を新たに作る事でシステムの開発や運用の効率化並びに品質の担保に貢献した事について紹介します。 また、検証フェーズの効率化手段としてDigdagを利用したデータ転送機能は使ってみると想像以上に便利だったので、実装方法について詳しく紹介します。効率化手段の1つとして参考にして頂けたら幸いです。 目次 はじめに 目次 リアルタイムマーケティングシステムの概要 リアルタイムマーケティングシステム バッチ配信システム analyzer バッチ配信システムの処理の流れ ユーザ抽出・コンテンツ生成の実装方法 ユーザ抽出の実装例 仕様 実装方法 実運用での課題 コンテンツ生成のSQL文は長く複雑 ローカルでの検証が困難 検証環境が導入される前 既存改修を検証する方法は「本番に影響を与えないように気をつけながら試行錯誤」のみ 修正と確認のリードタイムが長くなる 監視担当に余計な負荷をかける 検証環境を導入した後の効率問題 デプロイの自動化 データ転送の自動化 データ転送機能の実装方法 本番環境と検証環境間の接続について digファイル構成 PostgreSQLのデータ転送方法 SQL Serverのデータ転送方法 IDENTITY設定の壁 bcpを使ったデータ転送 ymlファイルを利用したデータ定義 検証データの定義を保持するドキュメントとしての有用性 改善した検証環境で検証を行うメリット 本番稼働しているものを改修する際の検証が楽になる 修正と確認のリードタイムが短くなる 余計な監視通知を抑制できる 気付きが多くなる まとめ おわりに リアルタイムマーケティングシステムの概要 これから紹介するシステムについて、いくつかの登場人物を説明します。 リアルタイムマーケティングシステム バッチ配信システム analyzer リアルタイムマーケティングシステム 以下の判定(最適化)を行った上で対象のお客様(ユーザ)に対しておすすめの商品(コンテンツ)を配信しているのがリアルタイムマーケティングシステムです。 よく見るチャネル(LINE、メール等)に配信する よく見る時間帯に配信する n日に1通の頻度で配信する 分かりやすいように具体例を1つあげます。ランキング急上昇中のアイテムのうち、おすすめ商品を紹介したい場合、お客様に合わせた適切なチャネル・時間帯・頻度を判定して送る事で、よりコンテンツを見て頂けるようになります。以下が実際のメールでの配信例です。 リアルタイムマーケティングシステムという名前ですが、リアルタイムの配信だけでなくバッチでの配信も行っています。今回はそのバッチ配信システムのキャンペーン実装について取り上げます。 バッチ配信システム 特定条件のユーザを特定時間に抽出し、最適化した上でコンテンツを配信する仕組みをバッチ配信システムと呼んでいます。 analyzer 最適化を高速で行うシステムのコアとなるアプリケーションをanalyzerと呼んでいます。 これはJBoss Enterprise Application Platform(JBoss EAP)上で動いており、関連機能も複数利用しています。例えば、Excelでルール判定を行うためのDecision Managerを最適化に利用し、分散キャッシュストアであるJBoss Data Grid(JDG)を処理の高速化のために利用しています。 バッチ配信システムの処理の流れ バッチ配信は以下のような流れで行われます。 データの流れは以下のようになっています。 処理の中でSQL ServerとPostgreSQLの2種類のDBを利用しています。処理の説明前にこの2つの用途について記載します。 SQL Serverは、ZOZOTOWNの各DBをレプリケーションしたDBです。ユーザ、商品、注文情報等ZOZOTOWNに関するデータを格納しています。 PostgreSQLは、リアルタイムマーケティングシステム専用のDBです。後続処理で利用するデータ、JDGに格納した実績や集計結果、BigQueryやDWH等外部で生成したデータを格納しています。外部データの連携にはDigdagを利用しています。 では処理の流れを説明していきます。 ユーザ抽出は、配信したい対象のユーザのリストを取得する処理です。リアルタイムマーケティングシステムではMyBatisを採用しているため、MyBatisのMapper XMLを利用してSQL Server経由でデータ抽出を行うためのSQL文を追加していきます。データ抽出後、結果を対象ユーザテーブル(PostgreSQL)に格納します。後続処理がこのデータを読み込みます。 最適化設定はanalyzer上で行われます。実装はDecision Managerを利用しているためExcelに設定値を追記します。今回の利用用途では、恩恵はあまり受けないので詳細は割愛します。この機能により、最適な時間帯に配信対象テーブル(PostgreSQL)へ対象ユーザIDや配信キャンペーンID等を書き込み、コンテンツ生成を行う処理がこのデータを読み込みます。 コンテンツ生成は、対象ユーザに配信コンテンツを表示・配信するための情報を取得します。例えば氏名、メールアドレス、商品名、画像名、金額等が該当します。配信対象テーブルを読み込み、最適化設定で判定したチャネルに合わせて必要情報を取得します。メールの場合はユーザ抽出と同様、Mapper XML経由でデータ抽出を行い、結果をcsvファイル出力します。LINEの場合はanalyzerから直接SQL文を実行し、結果をjsonに格納します。 配信処理は、ユーザにコンテンツをチャネル別に配信する処理です。メールの場合は、出力したcsvをMPSE(MailPublisher Smart Edition)というメール配信サービスにAPIで送っています。送ったcsvの値をMPSE側に予めセットしたデザインテンプレート(html, txt)へ差込む事で配信します。LINEの場合は、analyzerで格納したjsonをLINEのAPIリクエストに持たせてアクセスする事で配信します。 このシステムの全体的な仕組みの詳細については 先のブログ をご参照ください。 techblog.zozo.com このうちMapper XMLを使ったユーザ抽出とメール配信時のコンテンツ生成のSQL文実装は、検証の効率化による恩恵を大きく受けました。イメージがつくよう、詳しく実装方法を説明します。 ユーザ抽出・コンテンツ生成の実装方法 架空のTシャツ訴求キャンペーンのユーザ抽出の実装方法を例に説明します。ユーザ抽出とコンテンツ生成の基本構成は同じで、SQL文だけが異なります。 ユーザ抽出の実装例 仕様 前日のTシャツのアクセスが100回以上の会員が対象。 データの抽出方法について説明しておきます。 以下の2テーブルを利用します。 access_aggregation at PostgreSQL user at SQL Server 抽出方法の流れは、まずaccess_aggregationテーブルで、 category が tshirt 、 access が100以上の userid のリストを抽出します。このあと紹介する方法を使って、SQL Serverから該当のPostgreSQLのデータを抽出しtempテーブル( #aggregation )に格納します。そして、userテーブルから退会フラグ( isleave )が0かつ #aggregation に該当する id を抽出します。 このようなテーブル構成イメージです。 実装方法 以下のようなリソースのファイル構成の状態で、キャンペーンのユーザ抽出用のSQL文を記載するxmlを追加します。 resources └──mybatis ├──mappers │ ├──common.xml #共通ファイル │ └──campaign_tshirt.xml #新規追加 └──config.xml #共通ファイル 以下のようなSQLにより、データを抽出します。 common.xml <sql id = "create_tmp_aggregation" > <![ CDATA [ CREATE TABLE #aggregation ( userid INT NOT NULL PRIMARY KEY (userid) ); ]]> </sql> <sql id = "aggregation" > <include refid = "create_tmp_aggregation" > <![ CDATA [ INSERT INTO #aggregation ( userid ) SELECT userid FROM openquery(rds, ' SELECT userid FROM access_aggregation WHERE category = ''tshirt'' AND access >= 100 AND date = current_date + interval ''-1DAY'' GROUP BY userid ') LFD; ]]> </sql> campaign_tshirt.xml <? xml version = "1.0" encoding = "UTF-8" ?> <! DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd" > <mapper namespace = "jp.zozo.marketing.realtime.campaign" >     <include refid = "aggregation" > <select id = "campaign_tshirt" resultType = "map" > SELECT id FROM user INNER JOIN #aggregation ON user.id = userid WHERE isleave = 0 </select> </mapper> PostgreSQLのデータは、SQL ServerのSQL文から リンクサーバ という他DBにアクセスできるデータベースエンジンを経由し取得します。 リンクサーバを経由したデータ抽出は全て common.xml というファイルに記載します。Tシャツキャンペーンのmapper用xml( campaign_tshirt.xml )に userid 抽出のために、SQL ServerのSQL文を記載します。 あとは campaign_tshirt.xml を、アプリケーションが読み込むよう設定されている config.yml に追記します。 config.xml <!-- 既存 --> <mapper url = "../mappers/common.xml" /> <!-- 新規追加 --> <mapper url = "../mappers/campaign_tshirt.xml" /> これにより、アプリケーションがTシャツキャンペーンの対象者を抽出し、対象データテーブル(PostgreSQL)にinsertする事で後続処理に続きます。メールコンテンツ生成も同様の方法でSQL文を生成し、csvを出力して後続処理に続きます。 実装はこれで完了です。実装の流れは一見単純なものですが、実運用では以下の課題が潜んでいます。 実運用での課題 ここでは、実運用で発生した課題を2つ紹介します。 コンテンツ生成のSQL文は長く複雑 コンテンツ生成をするためには、複雑で長いSQL文を作成する事が殆どです。 取得できるデータとSQL文を駆使してコンテンツに必要なデータを抽出するため、仕様の複雑化による影響を受けやすい事が原因です。例えば以下のようなSQL文が挙げられます。 出力カラム数200個 カラムを組み合わせた加減乗除の計算式 caseごとに分岐されたデータ加工 条件判定後の番号(row_number等)振り直し 入れ子状態のサブクエリ(サブクエリのサブクエリのサブクエリ) 説明のために実際のものを簡素化し、DB情報等も変えていますが、以下のようなイメージです。 <!-- 対象ユーザの商品情報 --> <sql id = "item_info" > <![ CDATA [ CREATE TABLE #ITEM_INFO ( MEMBER_ID INT NOT NULL ,ITEMID INT NOT NULL ,IMAGENAME VARCHAR(50) NOT NULL ,ITEMNAME NVARCHAR(100) NOT NULL ,BRANDNAME NVARCHAR(100) NOT NULL ,PRICE NVARCHAR(15) NOT NULL ,DISCOUNT NVARCHAR(10) ,DISCOUNTCOLOR VARCHAR(10) ,COUPONCOLOR NVARCHAR(10) ,COUPONPOINT NVARCHAR(15) ,CMTST NVARCHAR(10) ,CMTEND NVARCHAR(10) ,RANK SMALLINT NOT NULL PRIMARY KEY (MEMBER_ID, RANK) ); INSERT INTO #ITEM_INFO SELECT MEMBER_ID ,ITEMID ,IMAGENAME ,CASE WHEN LEN(ITEMNAME) <= 36 THEN ITEMNAME ELSE LEFT(ITEMNAME, 34) + '...' END AS ITEMNAME --商品名 ,CASE WHEN LEN(BRANDNAME) <= 15 THEN TBNAME ELSE LEFT(BRANDNAME, 14) + '...' END AS BRANDNAME --ブランド名 LTRIM(RTRIM(FORMAT(ROUND((PRICE * TAXRATE), 0), N'#,0'))) AS PRICE ,CONVERT(VARCHAR(3), FLOOR((1.0 - (SALEPRICE * TAXRATE) / (SALEPRICE * TAXRATE)) * 100)) + '%OFF' AS DISCOUNT --割引率 ,CASE WHEN DISCOUNTFLAG = 1 THEN 'discounted' ELSE NULL END AS DISCOUNTCOLOR --価格の文字色 ,COUPONCOLOR ,COOUPONPOINT ,CASE WHEN COOUPONPOINT IS NULL THEN '<!--' ELSE NULL END AS CMTST --クーポンがない場合コメントアウト ,CASE WHEN COOUPONPOINT IS NULL THEN '-->' ELSE NULL END AS CMTEND --クーポンがない場合コメントアウト ,ROW_NUMBER() OVER (PARTITION BY MEMBER_ID ORDER BY RANK) AS RANK --条件判定後に掲載順を振り直す FROM #TARGET_MEMBER_ITEM AS MODEL --PostgreSQLで抽出した配信対象xモデル化データx外部連携データ (作成SQL文略) INNER JOIN ITEM ON MODEL.ITEMID = ITEM.ITEMID INNER JOIN SHOP ON ITEM.SHOPID = SHOP.SHOPID ・ ・ ・ LEFT OUTER JOIN #EXCLUDED_ITEM AS EXCLUDED ON ITEM.ITEMID = EXCLUDED.ITEMID --掲載対象外の商品情報(作成SQL文略) LEFT OUTER JOIN #COUPON_INFO AS COUPON ON SHOP.SHOP_ID = COUPON.SHOP_ID --クーポン情報(作成SQL文略) WHERE EXCLUDED.ITEMID IS NULL --対象外の商品を除外する ・ ・ ) AS BASE WHERE RANK <= 18; ]]> </sql> <!-- 対象商品をデザインテンプレート差込のためメンバーごとに横変換 --> <sql id = "main_info" > <![ CDATA [ SELECT /* カラム名はデザインテンプレート依存 */ /* ユーザ情報 */ EMAIL ,NAME ,MEMBER_ID ,TEMPLATEID --デザインテンプレートID /* メインコンテンツ情報 */ /* 商品1 */ ,MITEMID01 ,MITEMNAME01 ,MIMAGENAME01 ,MBRANDNAME01 ,MPRICE01 ,MDISCOUNTRATE01 ,MDISCOUNT01 ,MCOUPONCOLOR01 ,MCOUPONPOINT01 ,MCMTST01 ,MCMTEND01 /*商品2*/ ,MITEMDETAILID02 ,MITEMID02 ・ ・ ・ ,MCMTST18 ,MCMTEND18 /* MAT対象者はコメントアウト */ ,CASE WHEN MAT_MEMBER_ID IS NOT NULL THEN '<!--' ELSE NULL END AS COMMENDSTART ,CASE WHEN MAT_MEMBER_ID IS NOT NULL THEN '-->' ELSE NULL END AS COMMENDEND ,DATEPART(year, CURRENT_TIMESTAMP) AS CURRENTYEAR ,DATEPART(month, CURRENT_TIMESTAMP) AS CURRENTMONTH ,DATEPART(day, CURRENT_TIMESTAMP) AS CURRENTDAY ,DATEPART(hour, CURRENT_TIMESTAMP) AS CURRENTHOUR ,DATEPART(minute, CURRENT_TIMESTAMP) AS CURRENTMINUTE FROM #TARGET_MEMBER AS TARGET --MODELから生成した対象者xMAT対象情報(作成SQL文略) INNER JOIN( SELECT MEMBER_ID ,ITEMID AS MITEMID01 ・ ・ ・ ,CMTEND AS MCMTEND01 FROM #ITEM_INFO WHERE RANK = 1 ) AS ITEM01 ON TARGET.MEMBER_ID = ITEM01.MEMBER_ID ・ ・ ・ INNER JOIN( SELECT MEMBER_ID ,ITEMDETAILID AS MITEMDETAILID18 ・ ・ ・ FROM #ITEM_INFO WHERE RANK=18 )AS ITEM18 ON ITEM17.MEMBER_ID = ITEM18.MEMBER_ID ]]> </sql> <!-- PC用コンテンツ --> <select id = "pc_select" parameterType = "map" resultType = "java.util.LinkedHashMap" > <include refid = "target_member_item" /> <include refid = "target_member" /> <include refid = "excluded_item" /> <include refid = "coupon_info" /> <include refid = "item_info" /> <include refid = "main_info" /> <![ CDATA [ SELECT /* main_infoでselectした項目全部 */ EMAIL ・ ・ ・ /* PC版のみバナー情報 */ ,HEADERBANNER ,HEADERURL ,FOOTERFILENAME01 ,FOOTERURL01 ・ ・ ・ ,FOOTERURL06 FROM ( <include refid="main_info" /> ) AS MAININFO CROSS JOIN BANNER_INFO; ]]> <!-- セッションに残らないようにtmpテーブル削除 --> DROP #TARGET_MEMBER_ITEM; DROP #TARGET_MEMBER; DROP #EXCLUDED_ITEM; DROP #ITEM_INFO; DROP #COUPON_INFO; </select> <!-- モバイル用コンテンツ --> <select id = "mobile_select" parameterType = "map" resultType = "java.util.LinkedHashMap" > <include refid = "target_member_item" /> <include refid = "target_member" /> <include refid = "excluded_item" /> <include refid = "coupon_info" /> <include refid = "item_info" /> <include refid = "main_info" /> <!-- セッションに残らないようにtmpテーブル削除 --> DROP #TARGET_MEMBER_ITEM; DROP #TARGET_MEMBER; DROP #EXCLUDED_ITEM; DROP #ITEM_INFO; DROP #COUPON_INFO; </select> 慣れてしまえば一発で想定SQL文を書く事も可能ですが、慣れていないとSQL文からは実行結果の推測すら難しいでしょう。 ローカルでの検証が困難 ユーザ抽出の実装例で、リンクサーバを経由してPostgreSQLからデータを抽出する工程があったのを覚えていますか。リンクサーバの存在がローカル環境での検証を困難にした理由の1つです。 リンクサーバ経由の接続のためにOLE DBというデータソースアクセス機能を利用する必要があります。そのため、OLE DBで接続を行うための「OLE DBプロバイダー」が必要でした。Windows環境であればプロバイダーも存在しますが、開発者の大半がmacOS/Linux環境を利用しており、対応プロバイダーが見つからなかったため断念しました。 よって、ローカルでバッチ配信システムを動かしてもリンクサーバ経由の接続ができないため、ローカルでは処理を通貫して確かめる事はできない状態でした。 検証環境が導入される前 新しい人の増加や組織が変化する中、実装頻度が増え仕様はより複雑化していました。しかし先の通りローカル環境もなく、本番環境でしか検証ができない状況でした。当時の私のように慣れていない人の場合、以下のような状況に陥りました。 既存改修を検証する方法は「本番に影響を与えないように気をつけながら試行錯誤」のみ 既にリリースされたキャンペーンを修正する必要がある場合、本番稼働中のそのキャンペーンに影響しないよう、影響を与えない範囲で試行錯誤を少しずつして確認を行っていました。確認方法を検討するだけで長い時間を要する事もありました。 修正と確認のリードタイムが長くなる 入念に目視確認を行った後に本番リリースし検証。リリース後の検証を動かして何か見つかれば修正。このようなリリースを何度も繰り返します。動作確認してない、かつ本番リリースなので、自然と確認時間も長くなります。私は入社当初、何度もこのような本番リリースを繰り返しました。 監視担当に余計な負荷をかける 本番環境では監視設定があり、エラーはSlackの本番監視用チャネルに通知する仕組みが入っています。今回の検証時のエラーも例外ではありません。これは他の監視の妨げにもなりかねず、余計な監視アラート通知は皆に負荷をかける行為です。繰り返しの本番リリースに加えて、この状況は私の作業効率を下げていきました。 これらの課題を解決するため、AWS上にリアルタイムマーケティングシステムを動かす検証環境を作りました。同時に、検証環境でバッチ配信システムの検証も可能にしました。しかし、問題も発生しましたので、その問題について説明します。 検証環境を導入した後の効率問題 検証環境を導入し、本番環境に依存して余分に増えていた作業時間は短縮されました。しかし、検証環境で検証をするための準備に時間がかかるという問題が残っていました。 これを解決するために下記2点を行いました。 デプロイの自動化 データ転送の自動化 デプロイの自動化 当初、検証環境でのデプロイは全て手動で行っていましたが、検証用ブランチ(staging)にマージ後、CircleCIが自動で検証環境にファイルをコピーするよう、デプロイ手順を自動化しました。 データ転送の自動化 検証環境により、処理の流れを確認できるようになりましたが、処理をするためのデータが入っていないと想定結果を返すかどうかの確認ができません。当時はこの検証環境のデータ不備により、本番環境で気付いた後に、再検証を行うという検証環境の導入前と同様の状況になりました。 その後、都度本番から検証に必要なデータを取得してinsertするようにしました。利用するテーブルやケースが多いためデータの準備にとても時間がかかりました。 そこで、Digdagを使ったデータ転送機能を追加しました。 Digdagを使ったデータ転送については、やってみると効率化だけでなく、品質担保にも有用でした。 これらの改善により、検証からリリースまでのフローは以下のように効率改善されました。 Digdagを選択した理由は、既に他のデータ連携の実装で多用していたため知見もあり、比較的導入しやすかった事が主な理由でした。しかし実際に使ってみると、検証ケースが書かれたドキュメントとして残せたり、追加改修の時に流用しやすかったりと利点が多くありました。そのため、結果的にこの選択は正しいものでした。 Digdagを使ったデータ転送について、以下で実装方法と共に説明していきます。 データ転送機能の実装方法 PostgreSQLとSQL Serverの本番データを検証環境に転送する機能の実装方法について説明します。 SQL Serverではテーブル定義による壁があり、定番のEmbulkではなく別の方法を使っています。同じ壁にぶつかった方の参考になればと思います。 なお、環境変数やsecretsの設定方法、利用しているDockerイメージの詳細説明についてはトピックから外れるため一部省略しています。予めご了承ください。 また、本番環境に個人情報や情報区分の高いものがある場合は取り扱いに注意してください。 本番環境と検証環境間の接続について 環境を跨いだVPC間の接続では AWS Transit Gateway を利用しています。Transit Gatewayはルーターのような役割で、VPCを接続(Attachment)し、通信させたいサーバの接続情報をルートテーブルに設定する事でVPC間の通信を実現します。セキュリティグループの制御により単一方向のみ接続を許可しています。 digファイル構成 キャンペーンごとにどのデータ定義をしたか判別しやすくするため、専用の実行digファイル( TestDataxxxx.dig )とデータ定義の設定( config/xxxx )を用意します。キャンペーンに依存しない共通処理はsub、taskディレクトリに保持しています。 config データ定義に関する情報を記した設定ファイル群 sub 実行digファイルの中で実行するサブタスク群 tasks サブタスクの中で実行するスクリプト群 以下のような構成で管理しています。中のファイルについては後述します。 ProductionToStaging ├── TestDataCampaignA_postgres.dig ├── TestDataCampaignA_sqlserver.dig ├── config │ └── campaignA │ ├── postgres │ │ └── ranking.yml.liquid │ └── sqlserver │ └── target.yml ├── sub │ ├── build_query_parameter.dig │ ├── copy_sqlserver.dig │ ├── postgresql_environment.dig │ ├── prepare_environment.dig #環境変数の設定 │ ├── run_embulk.dig │ └── secrets.dig #secrets設定 └── tasks ├── bcp_copy.sh └── query_parameter.rb PostgreSQLのデータ転送方法 PostgreSQL同士でのデータ転送にはEmbulkを利用しました。input、output共にPostgreSQL用のJDBCプラグインを利用します。 embulk-input-postgresql embulk-output-postgresql Embulkは異なるDBやストレージ間でデータ転送できる事が特徴ですが、今回のようなPostgreSQL同士のデータ転送でも利用できます。以下の例のように、設定をliquidというテンプレートエンジンを使ってinput( in: )とoutput( out: )情報を記載する事で実現します。 target.yml.liquid in : type : postgresql host : {{ production_postgres_hostname }} port : {{ production_postgres_port }} user : {{ production_posrgres_user }} password : {{ production_postgres_password }} database : {{ database }} query : #人気ランキング上位3位 SELECT user_id ,item_id ,rank FROM ranking WHERE rank <= 3 out : type : postgresql host : {{ staging_postgres_hostname }} port : {{ staging_postgres_port }} user : {{ staging_postgres_user }} password : {{ staging_postgres_password }} database : {{ database }} table : ranking mode : truncate_insert 上記のようにinputに記載の query で転送データを定義しています。 digファイルに対象テーブル名(=liquidファイル名)をymlの配列型式で指定し、Embulkを実行します。検証データが増えた際にテーブルが増やせるようにするためです。 TestDataCampaignA_postgres.dig timezone : Asia/Tokyo +prepare_environment : !include : sub/prepare_environment.dig _export : wf : name : Transfer production data to staging for testing campaignA campaign_id : campaignA target_table_names : - ranking +prod_to_staging_postgres : for_each> : target_table_name : ${target_table_names} _parallel : false _do : +psql_to_psql : !include : sub/run_embulk.dig run_embulk.dig _export : docker : image : ${embulk_docker_image} pull_always : true sh> : embulk -J-Duser.timezone=Asia/Tokyo run -b /var/lib/embulk config/${campaign_id}/postgres/${target_table_name}.yml.liquid SQL Serverのデータ転送方法 Embulkを利用したかったのですが、以下のような壁がありました。 IDENTITY設定の壁 SQL ServerではIDENTITY設定されたカラムが存在するテーブルを転送する必要がありました。何も設定しない場合、insert時にエラーとなります。 例えば以下のようなテーブルをinsertした場合を考えてみます。 # IDENTITY設定されたテーブル作成 create table target_user(id integer identity primary key, name varchar ( 100 )); # データinsert insert into target_user(id,name) values ( 1 , ' username ' ); すると、以下のようなエラーメッセージがでます。 [S0001][544] Cannot insert explicit value for identity column in table 'target_user' when IDENTITY_INSERT is set to OFF. IDENTITY_INSERT設定がOFFの場合、IDENTITY設定のカラムの値は自動付与するので設定できないというエラーです。例だと、id=1の指定を外せばinsertができます。 今回はIDENTITY設定をされたカラムの値もそのまま転送したかったので、以下のようなステートメントを実行して、IDENTITY設定カラムに値を指定できるように設定し直す必要がありました。 SET IDENTITY_INSERT target_user ON ; 同一セッション内で SET IDENTITY_INSERT を実行する方法が見つからなかったため、Embulk利用を断念しました。そのため、SQL Serverのデータはbcp(bulk copy program)を利用して転送する事にしました。 bcpを使ったデータ転送 bcp は一括でデータをコピーするユーティリティです。インポート時に -E オプションをつける事で、IDENTITY設定カラムでも値を割り当てずにデータファイルの値を利用するため、転送が可能になります。 さらにデフォルトではトリガーを実行しないため、ヒントオプション -h に FIRE_TRIGGES 引数を指定する事でインポート時にinsertトリガーを実行します。 インポート前に重複データを削除する際にはsqlcmdを利用しています。sqlcmdはコマンドラインからSQL文を実行できるユーティリティです。 以下のようなシェルを用意する事で転送を実現できました。 bcp_copy.sh #!/usr/bin/env bash set -e BIN_PATH="/opt/mssql-tools/bin" # 対象テーブル・条件のデータをエクスポート ${BIN_PATH}/bcp "SELECT * FROM rtm.dbo.${table_name} WHERE ${where}" queryout data.dat -N -S ${sqlserver_host},${sqlserver_port} -d rtm -U digdag -P ${secret_db_sqlserver_password} # 重複エラーにならないよう、同条件のデータを削除 ${BIN_PATH}/sqlcmd -S ${staging_sqlserver_host},${staging_sqlserver_port} -d rtm -U ${staging_sqlserver_user} -P ${secret_db_sqlserver_stg_password} -Q "DELETE FROM rtm.dbo.${table_name} WHERE ${where}" # エクスポートしたデータをインポート ${BIN_PATH}/bcp rtm.dbo.${table_name} in data.dat -E -N -S ${staging_sqlserver_host},${staging_sqlserver_port} -U ${staging_sqlserver_user} -P ${secret_db_sqlserver_stg_password} -h FIRE_TRIGGERS copy_sqlserver.dig _export : table_name : ${condition.table_name} where : ${condition.where} docker : image : ${mssql-tools_image} sh> : tasks/bcp_copy.sh ymlファイルを利用したデータ定義 bcpのシェルに記載の ${table_name} と ${where} に入る情報は、ymlファイル内に以下のようなjson形式でキャンペーンのケース毎にテーブル、条件を持たせています。 target.yml - { table_name : User, where : user_id = 1111111 } # 担当者のuser_id - { table_name : Item, where : 'item_id in (10000, 100001, 100002)' } # ランキングで利用する商品 SQL Serverデータ転送の場合、このtarget.ymlに検証データを定義しています。 ymlに記載した情報はRubyで以下のようにjson情報をパラメータとして渡します。 query_parameter.rb require ' yaml ' module Tasks class QueryParameter def load # digファイルで定義したcampaign_idパラメータ campaign_id = Digdag .env.params.fetch( " campaign_id " , 0 ) # データの定義ファイルを読み込む File .open( " config/ #{ campaign_id } /sqlserver/target.yml " , " r " ) do |f| params = YAML .load(f, symbolize_names : true ) # 検証データの条件を後続タスク(bcp)で利用するため、Digdagのstoreパラメータとして格納 Digdag .env.store( " sqlserver_conditions " .to_sym => params) end end end end query_parameter.dig _export : docker : image : ruby:2.6.1 rb> : Tasks::QueryParameter.load require : 'tasks/query_parameter' これらをdigファイルに記載してデータ転送を実現します。 TestDataCampaignA_sqlserver.dig _export : wf : name : Transfer production data to staging for testing campaignA campaign_id : campaignA +prod_to_staging : +load_sqlserver_params : !include : sub/query_paramter.dig +loop_sqlserver : for_each> : condition : ${sqlserver_conditions} _do : +run_sqlserver_copy : !include : sub/copy_sqlserver.dig 以上がSQL Serverのデータ転送方法です。 これらのデータ転送機能を運用する中で以下の利点がありました。 検証データの定義を保持するドキュメントとしての有用性 データ転送機能で実装したクエリは、検証データの定義を保持するドキュメントとしても有用でした。 同じキャンペーン運用中に何か追加改修が必要になった場合は、同一データまたは追加の検証ケースを追記して検証ができるため、追加改修時の効率化も行えます。 私たちの場合は現在と未来の実装を正確に素早く行うための手段として、Digdagのデータ転送機能は適した手段でした。 改善した検証環境で検証を行うメリット 今回の経験により、整備された検証環境で検証を行うメリットとして以下が挙げられます。 本番稼働しているものを改修する際の検証が楽になる 修正と確認のリードタイムが短くなる 余計な監視通知を抑制できる 気付きが多くなる 本番稼働しているものを改修する際の検証が楽になる 検証環境で本番環境と同様の検証ができるようになった結果、既に動いてるキャンペーンに影響しないように検証するための試行錯誤を本番環境でする必要はなくなりました。 修正と確認のリードタイムが短くなる 事前に検証環境で検証ができるようになった結果、事前検証が可能になりリリース時の確認点を減らすことができました。検証をするのは検証環境なので本番に影響を与える事なく、迅速に修正・確認を実施できます。 余計な監視通知を抑制できる 監視担当に余計な負荷をかける行為からも脱却する事ができるため、全体の効率化にもつながります。 気付きが多くなる 作業効率が良くなり作業時間に余裕が生まれる事で本来考えるべき事へ集中できるようになります。実際に、検証環境ができてから仕様の過不足や必要な検証ケースについて担当者とやり取りをする機会が増えました。そのためキャンペーンの品質担保へも貢献できていると言えます。 以上のように、整備された検証環境の存在によって品質と効率を上げる事ができました。 まとめ 成功まで何度も失敗を繰り返すことのできる検証環境は、システムに慣れている人と慣れていない人の差をカバーする手段として大きく機能しました。 検証環境を利用した後の効率低下のリスクについては、CircleCIやDigdagを利用して効率化を上げる事でカバーできました。本番リリース特有の確認時間が減り、余計な監視通知もなくなるため、全体効率は上がっていると言えます。 さらにDigdagによるデータ転送自動化は、効率化だけでなく品質担保にも繋がりました。 皆さんも様々な方法で品質と効率のバランスを保つために試行錯誤していると思います。私たちのこういった運用改善が少しでも皆さんのお役に立てば幸いです。 おわりに 私たちは他にも多くのマーケティングに関わるシステムを開発・運用しています。今回のような環境整備も含めて、既存システムの品質と効率のバランスを保ちながら、新しい施策のための新規機能やリプレイスを検討しています。 興味を持たれた方は採用ページ等をチェックしてみてください! https://hrmos.co/pages/zozo/jobs/0000016 hrmos.co
こんにちは、ZOZOテクノロジーズ CTO室の池田( @ikenyal )です。 エンジニアが12月に思い浮かべるキーワードは何でしょう。「アドベントカレンダー」ですね。 弊社も毎年アドベントカレンダーに参加しており、今年も記事100本の公開を完走しましたので、概要をお伝えします。 ZOZOテクノロジーズ Advent Calendar 2020 今年は合計4個のカレンダーを実施したため、12/1-25の期間に合計100本の記事を公開しました。 qiita.com qiita.com qiita.com qiita.com 実施概要 アドベントカレンダーは任意参加で実施しています。 アドベントカレンダーはエンジニアのアウトプットの練習に適したイベントです。弊社ではテックブログをアウトプットの主軸に置いていますが、「テックブログを書く自信が無い」「テックブログに書くにはネタが小粒」のような場合に、アドベントカレンダーは良い機会になります。 なお、テックブログ上に記事を公開してアドベントカレンダーとして登録した記事も3本ありました。 人気があった記事 はてなブックマークのブックマーク数が上位だったものを紹介します。 12/4の #3 の記事、 takewell による「 2020 年の瀬の JS ビルド&バンドルツールの検討 」 zenn.dev webpack (シェア 76%)を代表とする ESNext (ECMAScript Next Generation) なコードをレガシーブラウザにビルドしたり、コードを単一ファイルにバンドルしたりするツールが数多く存在します。(以降、ビルド&バンドルなどの事前変換処理をプリプロセスと表記します。) これらプリプロセスツールに関して ”アプリには webpack、ライブラリには rollup.js” という常套句があります。しかし、この常套句は本当に妥当なのでしょうか? また、今年は新たなるプロプロセスツールとして snowpack や Rome などが登場しました。こうしたツールの登場があっても、この常套句は今尚通用するのでしょうか? 本稿では、これらの疑問と 2020 年年末時点のプロプロセスツールを調べ、将来性や使い分けについて、それぞれ検討します。 12/5の #3 の記事、 cozima による「 OSSへの貢献 - Issueから始めるチーム活動 」 techblog.zozo.com この記事では、今年4月に社内で策定されたOSSポリシーに基づいて、チームでOSSに貢献する活動に取り組んだ話を紹介します。 12/13の #3 の記事、 takewell による「 WebAssembly の利用シナリオを調べる 」 zenn.dev wasm でできるようになること or これまでネイティブでしか実用性の観点から実現できなかったことが wasm を使ってブラウザでも実現できるようになることはなんなのか調べてみました。 本稿はそれらのまとめと、使いどころがあるかもしれないと考えた 3 つの wasm ライブラリについて紹介します。 12/25の #2 の記事、 sonots による「 ZOZO プラットフォームSREとコロナ禍におけるチームリーディング術 」 sonots.medium.com 約一年前の2020年1月に SRE Next 2020 にて ZOZO MLOps のチームリーディングとSRE (Engineering) というタイトルで私のチームリーディング術についての発表を行いました。それから約一年が経とうとしているので、一年の振り返りの意味も含めて、アップデートしたいと思います。 12/25の #3 の記事、 kyuns による「 ZOZOテクノロジーズの2020年の振り返りと現状 」 techblog.zozo.com 毎年アドベントカレンダーの25日目にZOZOテクノロジーズの1年を僕がまとめて記事にする、というのがアドベントカレンダーの恒例となっていたのですが、すでに今年は上記noteにて、色々な取り組みを紹介させていただいたので、今回この記事では上記では紹介できなかった2020年の変化にフォーカスして、プロジェクトの進捗や組織の変化についてお伝えしたいと思います。 過去のアドベントカレンダー ZOZOテクノロジーズでは、2019年・2018年もアドベントカレンダーに参加しています。 ZOZOテクノロジーズ Advent Calendar 2019 qiita.com qiita.com qiita.com qiita.com qiita.com ZOZOテクノロジーズ Advent Calendar 2018 qiita.com qiita.com qiita.com 最後に ZOZOテクノロジーズでは、プロダクト開発以外にも、今回のような外部への発信も積極的に取り組んでいます。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
はじめに こんにちは。SRE部BtoBチームの田村です。BtoBチームが担当してるサービスには、クラウドで構成されているFBZ、オンプレ環境で稼働しているブランド様の自社ECシステム支援事業もあります。その自社ECシステムでは、バッチ処理が多数稼働しています(執筆時点でバッチ214本)。 バッチ処理のスケジュールはWindowsのタスクスケジューラで管理しており、定期メンテナンスや新規サービスリリースの際に運用者が手作業でタスクスケジューラの設定を更新していました。 手作業を伴い、繰り返される要素もあったため、我々のチーム内で「バッチの運用」はトイルの1つとして認識されていました。また、 前回の記事 と同様に、設定戻し漏れのリスク、作業工程の履歴を別帳票で管理する煩雑な運用があり、万全とは言えない状態でした。そこで、前回の記事をヒントにトイル撲滅運動としてタスクスケジューラ設定の自動反映の仕組みを構築したので、その方法について共有します。 目指す姿 今回目指す、GitHub ActionsによるWindowsタスクスケジューラ自動反映のフローは以下の通りです。 開発者によるPull Request作成 承認者によるレビューとマージ実行 Pull Requestマージイベントによる発火 セルフホストランナーでワークフロー実行 Windowsタスクスケジューラへの反映 実現するために対応したこと 目指す姿を実現するために以下のことを対応しました。 セルフホストランナーの導入 セルフホストランナーにschtasksコマンド実行権限を付与 ワークフロー設定 タスクスケジューラ自動反映スクリプト作成 セルフホストランナーの導入 GitHub Actionsワークフローを独自環境で実行できるセルフホストランナーを導入します。今回は、Windowsタスクスケジューラ設定を反映する必要のある、開発環境と本番環境のサーバーに導入しました。 導入手順は、公式サイトの セルフホストランナーの追加 をご確認ください。 なお、「パブリックリポジトリ」でのセルフホストランナーの利用は推奨されておりません。必ず公式の情報を確認のうえ、十分に検証したうえでご利用ください。 カスタムラベルの登録 セルフホストランナーに対してカスタムラベルを設定することで、開発環境と本番環境どちらで実行させるかを指定できます。開発環境では dev というカスタムラベルを付け、ワークフローで以下のように設定すると開発環境のランナーで実行できます。追加手順は、公式サイトの セルフホストランナーとのラベルの利用 をご確認ください。 セルフホストランナーにschtasksコマンド実行権限を付与 セルフホストランナー導入直後はschtasksコマンドの実行権限がないため、下記手順で実行権限を付与します。 セルフホストランナー登録時に作成したactions-runnerディレクトリのプロパティを確認 セキュリティタブに追加されている GITHUB_ActionsRunner_hoge のユーザーを確認 C:\Windows\System32\Tasks に GITHUB_ActionsRunner_hoge のフルアクセス権限を付与 ワークフロー設定 ワークフローの内容は以下の通りです。 特定ブランチに対するPull Requestイベントかつ、特定ファイルに変更があった場合のみ実行 追加・変更されたファイル差分を取得 反映コマンド確認用のスクリプト実行 Pull Requestがマージされたら自動反映スクリプトを実行 下記の設定は、ワークフローファイルのサンプルの一部です。 name : Apply task scheduler (dev_server) # 特定ブランチに対するPull Requestイベント、特定ファイルの変更のみ実行 on : pull_request : types : [ opened, synchronize, closed ] branches : - 'hoge_branch' paths : - 'hoge_path' jobs : schtasks : # 開発環境ホストランナー runs-on : [ self-hosted, windows, x64, dev ] steps : - name : Checkout uses : actions/checkout@v2 # git diff時の文字化け解消 - name : Git config quotepath run : git config --local core.quotepath false # Pull Request baseブランチフェッチ - name : Fetch base_sha run : git fetch origin ${{ github.event.pull_request.base.sha }} # 差分ファイルリストをdiff.txtへ出力 - name : Output diff.txt run : git diff ${{ github.event.pull_request.base.sha }} --diff-filter=ACMR --name-only > diff.txt # 反映コマンドの確認(マージ前チェック用:schtasksコマンドの出力) - name : Check command run : tasks/check.ps1 # マージされたら、反映スクリプト実行 - name : Apply if : github.event.pull_request.merged == true run : tasks/apply.ps1 タスクスケジューラ自動反映スクリプト作成 必要なschtasksコマンドを生成して実行する「自動反映スクリプト」はPowerShellで書きました。同リポジトリ内に反映スクリプトファイルを置き、ワークフローから実行するようにしています。反映スクリプトの処理の流れは以下の通りです。 必要なファイル差分リストの取得 反映コマンドの作成 反映コマンドの実行 以下は、実際にGitHub Actionsが実行された結果です。 運用について BtoBチームではリリース可能なメンバーは少数に限定しており、リリース内容のレビューを実施する実質的な承認者をリリーサーと呼んでいます。リリーサーのみに自動反映スクリプトの実行権限を与えるため、特定ブランチへのマージ権限をリリーサーに限定しました。また、開発者はそのブランチに向けてPull Requestを作成してもらうことにしました。 リリーサーがPull Requestのレビューとマージをすることでタスクスケジューラ反映の自動化を実現しています。これにより、本番環境への反映が必ず承認者を経由し実施される安全運用となっています。 まとめ 今回の対応によって冒頭で申し上げたような計画的なタスクスケジューラ設定についてはある程度自動化できました。ただ、緊急時においては対応スピードの観点で要件を満たさないため、どうしても手作業での設定変更は残ってしまいます。ベターな対応について運用面含め検討は続きます。 また、我々のプロダクトではリリースを含め、まだまだ手作業を必要とするものが残っています。今回の取り組みで、手作業部分を解消し自動反映後の監視の仕組み強化など、本来注力したいことに対しエンジニアの労力を割くいい流れができました。 すべてを自動化することはできませんが、手作業を伴って繰り返されるタスクについて着目し、定期的に仕組み化を検討するトイル撲滅運動を継続していきます。 さいごに ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
こんにちは、ZOZOテクノロジーズ CTOの今村( @kyuns )です。この記事はZOZOテクノロジーズ Advent Calendar 2020 #3の25日目の記事になります。今年はZOZOテクノロジーズとして4つのアドベントカレンダー、全100個の記事がありますので、ぜひご覧ください。ちなみに前日の記事は @sashihara_jp の「 コロナ禍の中のリモートワークでの弊社各チームのマネジメントの工夫について 」でした。 CTOとしての2年半の取り組みに関しては、先日公開した記事でも紹介していますので、そちらもご覧ください。 note.com ちなみに、毎年アドベントカレンダーの25日目にZOZOテクノロジーズの1年を僕がまとめて記事にする、というのがアドベントカレンダーの恒例となっていたのですが、すでに今年は上記noteにて、色々な取り組みを紹介させていただいたので、今回この記事では上記では紹介できなかった2020年の変化にフォーカスして、プロジェクトの進捗や組織の変化についてお伝えしたいと思います。 会社の状況 ヤフーとのPMI(Post Merger Integration) 昨年の 2019年まとめブログ でも紹介したように、Zホールディングス(以降、ZHD)によるTOBが成立し、ZOZOはZHDの子会社となりました。 また、今までZOZOの代表を務めていた前澤が引退、澤田社長の新体制になり新しいZOZOの歴史がはじまった2020年でした。買収の大きな目的にも両社でシナジーを生んでいくということが、重要となっていたため、2020年はZHD・ヤフーとZOZOとのシナジーを少しずつ模索していく1年でした。 ヤフーとの取り組み PayPayモール ZOZOTOWN 2019年12月には、PayPayモールの中にZOZOTOWNがオープンしました。 一見、「同じものが買えるのになぜPayPayモールに出店?」と思うかもしれませんが、ヤフーとZOZOTOWNでは、そもそも存在するユーザー層が違うため、我々としては新しいユーザーへのリーチが望めます。プロジェクト自体もヤフーと初の共同プロジェクトということもあり、お互いに尽力してかなり短い期間で実現することができ、両社の仲が深まった案件でした。こちらも順調に売上が伸びてきています。 PayPay連携 ZOZOTOWNでPayPay決済が利用できるようになりました。導入直後から決済手段として選ばれており、PayPayの人気の高さが伺えます。 検索連携 ZOZOTOWNの大規模なデータや行動ログを活用して、キーワード検索時のサジェストや検索結果の改善を進めました。 こちらの取り組みの一環として、ヤフーと共同で機械学習を用いた検索結果の改善も進めています。 また、ヤフーと共同でZOZOTOWNの商品をヤフーの検索結果に表示する新たな施策も開始され、リリースされました。 その結果、ヤフーからZOZOTOWNの導線がより良い形となりました。 エンジニアリングリソース支援 合流したからにはお互いのエンジニアの交流やリソース支援を柔軟に行っていきたいと考えています。 ヤフーには特に検索周りで非常にノウハウを持っているエンジニアがいます。現在ヤフーから数名のエンジニアに出向してもらい、共同でZOZOTOWNの検索エンジンの改善を行っています。我々も検索特化のエンジニアなどが少ない現状ですので、このようなスキル特化型人材のリソース支援は非常にありがたく、双方にとって非常に良い取り組みとなっています。 新型コロナウイルスの影響 今年は新型コロナウイルスの影響が非常に大きい1年でした。 ファッションブランドさんや店鋪が大打撃を受ける中、我々としても、「なんとしてでもサイトの運営を続けなければならない」という思いのもと、安全に業務が行えるように3月からオフィスを閉鎖し、フルリモート勤務への体制へと切り替えました。 幸い2年前から東京オリンピックを見据えて、リモートの準備を整えていたおかげで、最初の頃は若干の混乱がありましたが、1,2ヶ月経った頃にはZOZOとZOZOテクノロジーズともにリモートでの勤務ができる状態になりました。 プロジェクト紹介 2020年にもZOZOが展開する各プロジェクトで色々な進展がありました。現在進行中のプロジェクトの進捗をいくつか紹介したいと思います。 ZOZOTOWNリプレイス ZOZOTOWNリプレイスに関しては、2020年1月にプロジェクトを仕切り直し、新しく開発ロードマップを引き直しました。振り返ってみると、ロードマップ通りの進捗ができた1年だったと思います。 主な成果 VMware Cloud on AWSによるスケーラブルなインフラの実現 検索エンジンのSQL Server脱却、全面Elasticsearch化による検索速度劇的改善 マルチクラウド廃止によるコスト削減 ID認証基盤リプレイスによる安定稼働 API Gateway化 APIガイドライン策定 Infrastructure as Code、CI/CD環境整備 詳しくはこちらの記事をご覧ください。 speakerdeck.com 基幹システム ZOZOBASEにおけるオペレーションや、基幹システム周りの作業効率化についても改善を繰り返しました。 例えばヤマトさんとデータ連携を行い、配送ステータスが可視化して見えるようになりました。 他にも、新しい倉庫の稼働の対応や、自動包装機の導入、PayPay決済導入など、非常に多くの改修改善を行った1年でした。 また、こちらのシステム自体もZOZOTOWNと同様、ZOZOTOWNのリリース当初から利用されています。そのため、リプレイスにも着手して、VBScript / SQL Server / IISの仕組みをクラウドに持っていったり、別のデータベースや、別の言語に置き換えるような取り組みも開始できています。 MA(マーケティングオートメーション) ユーザーコミュニケーション周りも色々な内製化が進んだ1年でした。 ZOZOTOWNやWEARのプッシュ通知の仕組みは、以前は3rdパーティ製のものを利用していたのですが、それらを内製化、FCMを利用したものに置き換えました。これにより、より正確なプッシュ通知のデータを取得することができるようになりました。 また、ZOZOTOWNのLINEアカウントを運用するためのツールをLINE Official Account Managerから新しく作った内製化ツールに置き換え、より柔軟に、リッチにユーザーコミュニケーションを実現できるようになりました。 また、機械学習によるスコアリングを活用し、新規出店したブランドの効率的な顧客コミュニケーション施策を行いました。GoogleのCloud AutoML Tablesを活用することで、機械学習モデル作成の大部分を自動化し、非エンジニアでも検証したいターゲット変数を設定してモデルを作成できる仕組みを整えました。 www.tsuhanshimbun.com ZOZOMAT 今年3月、昨年から予約を受け付けていたZOZOMATをリリースしました。 「ZOZOSUITの次は何か」と期待されていたものですが、次に我々がターゲットとしたのは足の計測でした。 こちらは自分の足のサイズを正確に計測できる仕組みとなっており、計測後、自分の足にフィットする靴を探すことができるようになっています。 また、ZOZOMAT対応シューズの型数も現在ではリリース時よりも10倍以上に増えており、今後も対応型は増えていく予定です。 ZOZOMATの良さはやはりその計測精度だと思います。精度に関してのクレームはほとんどないぐらいまで誤差無く測れる状態となっています。ZOZOSUITのときの開発プロセスの失敗などを活かし、計測チームの頑張りのおかげで、かなりクオリティの高いものができていると思います。 こちらもすでに100万人以上の方が計測を行ってくれています。 ZOZOSUIT 2 先日の決算発表にて、 ZOZOSUIT 2を発表 いたしました。 「ZOZOSUITって無くなったんじゃないの!?」と世間では思われていたと思います。しかしながら実は水面下でZOZOSUIT Ver.2の開発を進めていました。そして、前回よりもはるかに計測精度の高いZOZOSUITを完成させました。 今回我々はいきなりユーザーに配布するのではなく、まずは我々と一緒になってこのZOZOSUIT 2の可能性を模索してくれるパートナーを募集することにしました。一般に出回るかどうかはまだわかりませんが、さらなる計測精度の向上、そして新しい分野への応用ということにチャレンジしていきたいと思います。 LP の最後には「ZOZOXXXXX COMING SOON」という文字が見えますが、果たしてこれは何でしょうね...! YOUR BRAND PROJECT / D2C 10月には「 YOUR BRAND PROJECT by ZOZO 」というインフルエンサーの方々と共同でファッションブランドを立ち上げる新規事業を開始しました。ブランドの立ち上げにおける商品の企画や製造、生産、販売、物流、カスタマーサポートなどの運営を全面的に我々が支援するプロジェクトとなっています。 例えば丸山礼さんのブランドなどは、わずか5分で全商品が完売するなど、好調な出だしを見せています。 BtoB事業 今年の4月には 「Fulfillment by ZOZO」 (以降、FBZ)というブランド様向けのBtoB事業を行っていた子会社のアラタナをZOZOとZOZOテクノロジーズへと吸収合併しました。 FBZはZOZOTOWN出店企業の自社ECのフルフィルメント支援サービスで、自社EC運営のための撮影・採寸・梱包・配送などの各種フルフィルメント業務をZOZOTOWNの物流センター「ZOZOBASE」が受託し、設備投資や人件費、在庫保管料などの負担なしに、自社ECの運営が可能なサービスとなっています。 今年もクライアント数が増え、取扱高は過去最高を更新し続けており、現在ではラルフローレンさんやUNITED ARROWSさんなど、50以上のブランド様のサイトの運営業績好調で、取扱高は過去最高を更新し続けています。 ZOZO研究所 WEARのデータを使った取り組み WEARの大規模データを使った取り組みを進めました。4月には 髪型別コーデ検索機能 をラボページにてリリース。機械学習を用いて投稿写真内の髪型を使って投稿を検索できるようにしました。 ZOZOならではの研究成果だなと思いますし、AIを用いた画像検索だけでなく、九州工業大と共同研究しているような基礎技術も用いられています。詳しい制作秘話は 研究所メンバーのインタビュー記事 をご覧ください。 また、定期的に 調査リリース を掲載。WEARの大規模データを活用して流行の変遷をデータから読み解いていきました。 慶應義塾大学との共同研究 慶應義塾大学と 「ファッションに特化したIoTノード開発および グラフィカルユーザーインターフェースの設計に関する共同研究」 を開始しました。こちらの研究では主に以下の2つのことを目指します。 ファッションアイテムの見た目に溶け込む各種 IoTノードの開発 ファッションアイテムにセンサーやアクチュエータなどの機能を実装した小型デバイスの開発を行います。これらの開発したデバイス間のデータのやりとりや充電方法、配線のあり方について、ユーザーの服飾習慣に調和するソリューションをデザインと技術の両面より探索します。 デザイナーやユーザーが手軽に開発できる設計環境の開発 ウェアラブルプロダクトの開発者以外でも、これらの機能を手軽に実装できるソフトウェアのプロトタイプや、本システムを通じて作成したプロダクトのユースケース探索などを行います。具体的には、提供するプラットフォームを用いてデザイナーやユーザーが簡単に開発することができ、自身の希望に寄り添ったカスタマイズが可能となるデバイスの設計を目標とします。 ECCV採択 今年は研究成果として、ZOZO研究所の斎藤侑輝、中村拓磨、共同研究者で和歌山大学講師である八谷大岳氏、統計数理研究所・総合研究大学院大学教授 福水健次氏(斎藤の博士課程指導教員)の書いた「Exchangeable Deep Neural Networks for Set-to-Set Matching and Learning」という論文がECCVに採択されました。 こちらは以前から研究を続けてきた集合マッチングをテーマとしており、今後のZOZOTOWNの推薦アルゴリズムにも活かされる機会が出てくるでしょう。 詳しいアルゴリズムはブログにて解説しています。 techblog.zozo.com Open Bandit Data & Pipelineの公開 8月には、Open Bandit Dataとして、ZOZOTOWN上での実際の推薦アルゴリズムから取得された 2,800万件超のファッション推薦データを公開しました。 また、データだけでなく、オフライン検証を行うのにも役立つPipelineも同様に公開しました。 本取り組みに関する研究成果として、トップ国際会議のワークショップ(ICML、RecSys、NeuIPS)を含む国内外の多くの場で発表しています。 このように、サービスにおける実際のデータを公開していくような取り組みも始めています。 GitHub - st-tech/zr-obp: Open Bandit Pipeline: a python library for bandit algorithms and off-policy evaluation 画像検索 2019年にリリースされた「画像検索機能」では、2020年7月に新たに「財布」で画像検索機能が使えるようになりました。 今後も、水着・浴衣など画像検索対応カテゴリーの拡充を行なっていくと同時に、より良い検索体験の提供のために精度や使い勝手の向上を重ねていきます。 コーポレートエンジニアリング 今回新型コロナウイルスの影響により、最も忙しかった部門だと思います。社員全員がリモートワークできるように、様々な仕組みを整えました。 詳しくは note の方をご覧ください。 Azure ADを中心とした認証基盤の構築 VPNの仕組み刷新+ゼロトラストベースのアクセスの仕組みの構築 オンプレファイルサーバーの廃止 MDM、EDRの刷新、CASBの構築 各種ルールの制定 組織の変化 2020年は組織においても、大きな変化がありました。 リプレイスを見据えた組織へ ZOZOTOWNリプレイスを進めていく上で、どのような組織体制がベストなのかを考えた結果、実現したいシステムアーキテクチャに即した形で組織を本部に分けました。エンジニアの数も300名を超え、1つの開発部にまとめるのは限界だったので、丁度よいタイミングでした。 技術推進室の設置 ZOZOTOWNリプレイスを進めていくにあたって、非常に多くのことを決めていく必要性があります。そこで、技術開発本部の中に技術推進室を作り、ZOZOTOWNリプレイスに関する様々なことをとりまとめるようにしました。 全社横断して仕様を決めないといけない部分や、方向性を統一しないといけないようなものの調整をこのチームが担っています。 例えばAPIを開発するときのガイドラインやデータベース設計のガイドライン、APIとしての、SLAやSLOを確認できるダッシュボードの設定や、個人情報へアクセスする際のフローの整備などチーム横断でのとりまとめなどを、CTO室との連携を行い、スムーズに各チームが開発できるような調整を行っています。 CISO室およびZOZOグループリスクマネジメント委員会の設置 昨年ZOZO CSIRT組織を立ち上げて社内のセキュリティリスクの洗い出しやインシデント対応などを取りまとめてきましたが、ZHD傘下となったこともあり、更にセキュリティやリスクマネジメントを強化していく必要性が出てきました。 システムにおけるセキュリティリスクだけではなく、ERMの観点などからも対応していく必要があったので、CISO室を設置し、ZOZOグループリスクマネジメント委員会を発足しました。会社におけるリスクというのは多岐にわたります。それぞれ分科会という形で、各分野ごとに対応をしていく体制を整えました。 今年を振り返ってみて 2020年は1年のうち3/4がリモートでの仕事でしたが、生産性を著しく落とすこと無く業務ができた1年でした。新型コロナウイルスの影響を大きく受けましたが、業績自体はオンラインショッピングの需要の高まりの影響も受け、好調を維持できています。我々の働き方自体も大きくアップデートされた1年でした。 来年には新しく西千葉オフィスができたりしますが、オンラインとオフラインをうまく使い分けながら、また世間をあっと驚かせるような「想像のナナメウエ」の取り組みをしていきたいと思います。 ZOZOテクノロジーズもこの1年で大きくファッションテックカンパニーへと変化を遂げれたと思います。そんな変化が激しいZOZOテクノロジーズ を一緒に盛り上げてくれる仲間も絶賛募集中です。 この記事を読んで、ZOZOテクノロジーズに応募してみたいと思ったエンジニアの方は下記の採用ページからぜひご応募ください。 その際に「テックブログみました」と書いていただけると幸いです。 tech.zozo.com
こんにちは、CTO室兼SRE部テックリードの光野(kotatsu360)です。AWS・ウィスキー・葉巻が好きです。 普段はAWSのアカウントが複数ある状況(マルチアカウント環境)において、セキュリティや品質の維持をどのように行うかについて取り組んでいます。色々と資料も公開しているので、よろしければご覧ください。 speakerdeck.com さて、本記事では AWS re:Invent 2020 を取り上げます。今年もre:Inventに合わせ大量のリリース・アップデートが行われました。リリースは180を超え、楽しくもキャッチアップに奔走しております。この大量のリリースの中から、弊社の状況を鑑みて特に目を引いたものをピックアップします。 AWS re:Invent 2020 今年のアツいアップデート5選 VPC Reachability Analyzer S3で強い書き込み後の読み込み整合性がサポート Babelfish for Aurora PostgreSQL Amazon ECR Public AWS Systems Manager Fleet Manager まとめ We are hiring AWS re:Invent 2020 簡単にAWS re:Inventについて触れたいと思います。AWS re:InventはAWSが主催する、一年で最大のイベントです。 今年で9回目を迎える「AWS re:Invent」は、 AWSのクラウドサービスに関わる技術的なセミナー・ハンズオンセッションなど、 2,500を超えるセッション(2019年実績)を提供しており、お客様が主体的に体験できる、学習機会が豊富なグローバルカンファレンスです。 AWS re:Invent 今回はオンラインで無料開催!|AWS 例年、この開催直前より既存サービスのアップデートが増え、期間中は基調講演を中心に新規サービスの公開が繰り返されます。AWSに関わるエンジニアとしては目が逃せません。これまでは一貫してアメリカはラスベガスのホテルを借りてのイベントでしたが、今年はオンライン開催となりました。 re:Invent 2019は1週間のイベントでしたが、今年はオンラインということもあってか11月30日 ~ 12月18日(+ 1月12日 ~ 14日)と3週間に渡って開催されました。2021年1月にも3日間の追加開催が決まっています。 なお、ZOZOテクノロジーズは2018、2019と現地にてイベント参加を行い、2019では現地から参加レポートを公開しております。 techblog.zozo.com 今年のアツいアップデート5選 まさかのMacインスタンス提供 1 から始まったre:Invent 2020ですが、ここでは特に私個人としてアツいアップデートを紹介したいと思います。 記事の量・新鮮さでいえば期間中ほぼリアルタイムで更新をされ続けているクラスメソッドさんの Developers.IO が何よりも圧倒的です 2 。ここでは、ZOZOテクノロジーズという組織にとってそれはどのような課題を解決するのか、どのような意味を持つのかについて着目してまとめています。日常業務で、マルチアカウント管理に携わっているということもあり、コンプラや運用系サービスが多めです。予めご了承ください。 VPC Reachability Analyzer aws.amazon.com 1つ目はVPC Reachability Analyzerです。VPC内のリソース同士を指定して、その間が到達可能かを検証するためのサービスです。以下の画像はVPCを2つ用意し、VPC Peeringから一方のVPCに存在するEC2インスタンスまでの疎通を確認するものです。Network ACLやSecurity Group、ENIといった複数の要素を経てEC2インスタンスへ到達しています。経路上のどこかに問題があり到達できない場合、それをエラーとして表示してくれます。 弊社は、現在クラウド移行の真っ最中であり、AWS Direct Connectによるデータセンターとの閉域網接続を多用しています。一部ではAWS Transit Gatewayの利用も始まり、オンプレミスとAWS間のネットワークの到達性を確保するのが課題となっています。これまで検証用インスタンスを立ててtracerouteによる地道な確認をしていた部分も、これによってどこからどこまでなら到達するのか、その検証が簡易になることを期待しています 3 。 また、AWSらしくサービス間連携による新しい自動化についても期待が持てます。CloudWatch EventsからLambdaを経由すれば定期分析からの疎通性監視が可能となります。深夜帯に動くバッチがあるが、日中の何気ない操作で疎通が切れており、深夜の実行時に判明する。そんな問題を日中帯に検知することも可能になるやもしれません。 S3で強い書き込み後の読み込み整合性がサポート aws.amazon.com 2つ目はS3から。御存知の通り、S3では新規オブジェクトのPUTに対しては書き込み後の読み込み整合性、オブジェクトの更新・削除については結果整合性を提供してきました。そのため、オブジェクトを更新する場合、その直後のGETでは新旧2つのオブジェクトが混在する可能性を許容する必要がありました。 (上記ブログエントリより引用) しかし、今回のアップデートでこの制約が取り払われ、更新・削除についても強い書き込み後の読み込み整合性が保証されることとなります。ブログエントリには、DELETEに関する記述がありませんが公式ドキュメントにはDELETEでも強い整合性が保証されると記載されています。 Amazon S3 provides strong read-after-write consistency for PUTs and DELETEs of objects in your Amazon S3 bucket in all AWS Regions. This applies to both writes to new objects as well as PUTs that overwrite existing objects and DELETEs. In addition, read operations on Amazon S3 Select, Amazon S3 Access Control Lists, Amazon S3 Object Tags, and object metadata (e.g. HEAD object) are strongly consistent. Introduction to Amazon S3 - Amazon Simple Storage Service 弊社でもログや分析用データの保存先として必ずS3が登場します。上書きをするケースはそれほど多くないものの、アプリケーション設計においてしばしば見逃されがちな部分で手戻りの原因ともなっていました。昔からのS3ユーザとして驚くべきリリースであると同時に今後S3を利用する開発者に対してより優しいサービスになったと考えています。 Babelfish for Aurora PostgreSQL aws.amazon.com 3つ目はAndy JassyのKeynoteで発表された、Babelfish for Aurora PostgreSQLです。SQL Serverに対するクエリのみならず、トリガ・ストアドプロシージャや関数など含めて全て透過的に変換するレイヤーを提供するものです。現在はプレビュー版のため、利用にはリクエストの許可を待つ必要があります。 ( Babelfish for Aurora PostgreSQL (Preview) | Amazon Web Services より引用) Babelfish adds an endpoint to PostgreSQL that understands the SQL Server wire protocol Tabular Data Stream (TDS), as well as commonly used T-SQL commands used by SQL Server. Support for T-SQL includes elements such as the SQL dialect, cursors, catalog views, data types, triggers, stored procedures, and functions. With Babelfish enabled, you don’t have to swap out database drivers or take on the significant effort of rewriting and verifying all of your applications’ database requests. Want more PostgreSQL? You just might like Babelfish | AWS Open Source Blog AWS Database Migration Serviceとは異なり、あくまでもPostgreSQLのエンドポイントとして振る舞うというのが非常に興味深いプロダクトです。 弊社が提供する ZOZOTOWN や WEAR は、いずれもSQL ServerをDBとして利用しています。またストアドプロシージャを多用し、ビジネスロジックの多くはDB上に存在します。これはZOZOTOWNができた当時主流な設計でしたが、現在ではスケーラビリティの観点からあまり採用されない形かと思います。AWSへはリファクタリングを伴うリプレイスが行われており、そこではAmazon Aurora MySQL/PostgreSQLといったOSS互換のDBエンジンを採用しています。オンプレミスに存在するSQL Serverを将来どうするかについてはまだ検討のさなかですが、切り替えコストの小さい選択肢が増えることについて、歓迎しています。GAまで目が話せません。 Amazon ECR Public aws.amazon.com 4つ目は、コンテナイメージをパブリックに共有できるECR Publicです。11月頭にDocker Hubのrate limitsに対応するためのブログポストがありましたが、そちらでも告知されていました 4 。 ECR Publicは、Docker Hub以外の選択肢が誕生すること以上に、これによってAWSで利用されているエージェントやツールのコンテナイメージが一箇所に集約されたことを歓迎しています。弊社では可能な限りマネージドサービスを採用することで、管理範囲を狭める努力をしています。概ね問題ありませんが、LambdaのランタイムやFargateで配置されるFluent Bitが実際どのようなものか、確認に時間をかけることがありました。 これらは Amazon ECR Public Gallery として、コンテナイメージが公開されており、開発効率の向上に期待できます。 AWS Systems Manager Fleet Manager aws.amazon.com 最後は、AWS Systems Manager Fleet Managerです。SSM Agentを通じて集約した情報をOSの垣根を越えて一元的に閲覧できるサービスになります。 As described in the documentation, managed instances includes those running Windows, Linux, and macOS operating systems, in both the AWS Cloud and on-premises. Fleet Manager gives you an aggregated view of your compute instances regardless of where they exist. New – AWS Systems Manager Fleet Manager | AWS News Blog SSM Agentのドキュメントにはまだmac1インスタンスへのインストール方法が見当たりませんが、ブログエントリを見るにMacの場合でも管理できるようです。いくつかインスタンスを追加した状態が次の画像です。 詳細を見ていくと、これまでのマネージドインスタンスやインベントリといった機能を集約した新しいUIが表示されます。 またユーザ管理が可能になりました! 弊社では、先の通りDBにSQL Serverを利用しており、そこにアクセスするアプリケーションはWindows Serverの上で動作しています。リファクタリングされLinuxベースになるアプリケーションがある一方で、負荷分散のため既存アプリケーションをそのままクラウドリフトする計画も動いています。その場合、多種多様なOSがAWS上に混在するので、OS横断で俯瞰できる機能にはとても期待しています。 なお、当初ユーザが作れると聞き「これはSSH管理が変わるか!」と思いましたが、現時点ではユーザとパスワードの設定まででした。また、CLI 5 を見る限りまだAPIは公開されておらず、あくまでもWeb UIから見えるという機能になっています。今後、様々なアップデートがあるはずなので、継続的に追っていきます。 まとめ re:Inventのリリースから、独断と偏見で注目のリリースを5つ抜粋してご紹介しました。ご存じないリリースがあった場合、それらを知るきっかけになれば何よりです。 日々の構築・運用作業を助けるVPC Reachability Analyzer 着実に進歩を遂げるS3 DBエンジンの選択に新しいアプローチを提案するBabelfish for Aurora PostgreSQL コンテナ界隈に新しいパブリックレジストリを提供し、オフィシャルコンテナイメージが集約されるAmazon ECR Public EC2インスタンスに新しい可視性を提供するAWS Systems Manager Fleet Manager 新しいサービスをリリースしつつも、既存サービスの着実な改善を続けるAWS。エンジニアとして、また一ファンとして、キャッチアップと業務改善に取り組む所存です。 2021年1月の追加開催も今から楽しみでなりません! We are hiring ZOZOテクノロジーズCTO室では、世の中の情報を常に取り込み組織を変えていくことに興味がある方を探しています! 是非、以下のリンクからご応募ください! https://hrmos.co/pages/zozo/jobs/0000040 hrmos.co New – Use Amazon EC2 Mac Instances to Build & Test macOS, iOS, iPadOS, tvOS, and watchOS Apps | AWS News Blog ↩ 開催期間中および本記事の執筆でも大変お世話になっています。 AWS re:Invent 2020 の記事一覧 | Developers.IO ↩ 将来といわず、本記事の検証環境作成で早速役に立ちました・・・! ↩ Advice for customers dealing with Docker Hub rate limits, and a Coming Soon announcement | Containers ↩ ssm — AWS CLI 2.1.14 Command Reference ↩