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

TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

1025

ZOZOテクノロジーズでVRやARといったXR領域の利活用を推進しているWEAR部の諸星( @ikkou )です。 弊社に限った話ではありませんがCOVID-19の影響により、今までのようなオンサイトでのイベントをなかなか実施し難い状況が続いています。 例えば先日の『#技術書典 頒布本「ZOZO TECH BOOK」解説会』は弊社として初のオンラインイベントとなりました。 techblog.zozo.com いわゆる「勉強会」系のイベントもそうですが、オフィス見学といった社内の雰囲気を知るために重要なイベントも同様に実施できない状況です。 そこで先日、これもまた弊社としては初の試みとなる『バーチャルオフィス見学会』を実施することにしました。 zozotech-inc.connpass.com 『バーチャルオフィス見学会』に先駆けて、clusterに『ZOZOテクノロジーズ バーチャルオフィス』を公開しています。 cluster.mu 私はこのバーチャルオフィス作りの技術・運用まわりで携わったので、本記事ではその背景を紹介します。 手段の選定 プラットフォームの選定 ワールド作り Cluster Creator Kitの導入 clusterに最適化したシェーダーへの変更 VRChat向けシステムの削除 歩き回ることを想定したコライダーの削除と追加 植物の削除 バーチャルならではの「遊び」 エントランスのZOZOMATと箱猫マックス 持ち上げられるZOZO箱 リアルアバターの利用 プラットフォーム選定の補足 VRChatを選択しなかった理由 Mozilla Hubsを選択しなかった理由 まとめ 最後に 手段の選定 バーチャルオフィス見学を実施するにあたり、まずどのような手段で実施するか検討しました。大別すると次の2つになります。 1つ目はフォトリアルな全天球(360度)の写真や動画を使った方法です。 例えば国立科学博物館による「 おうちで体験!かはくVR 」は全天球写真を用いたウォークスルー型のバーチャル展示です。Matterportという特殊なカメラを使用して撮影しています。 www.kahaku.go.jp 2つ目は3D CGを使った方法です。 例えば「 Grani VR Office Tour 」はレーザースキャンを用いて実際のオフィスと同等の空間を3D CGで再現したバーチャルオフィスツアーです。 grani.jp それぞれにPros/Consがあり、個別の詳細は省きますが、今回は後者の3D CGで再現する方法を採りました。 これはオフィスの全天球写真が存在せず、しかし撮影のため外出自粛が求められる期間中にオフィスへの移動を避けたかったこと、そして後述する3D CGのアセットが既に存在していたことが理由です。 プラットフォームの選定 手段が決まった後、『バーチャルオフィス見学会』を実施するプラットフォームを選定しました。 具体的にはVRChat, cluster, Mozilla Hubsの3つの候補に絞りました。その上で、今回はマルチプラットフォーム対応のバーチャルSNSである「cluster」に青山オフィスを模した空間を再現する形を採りました。 cluster.mu clusterは誰もが自由に使える公式のイベント会場の他に、ユーザー独自の会場を作れるワールド機能があります。 つい最近ではグループ会社でもあるヤフーの『オープンコラボレーションスペース「LODGE」』が『バーチャルLODGE』として一足早くclusterのワールドとして公開されました。 note.com 今回はこのワールド機能で「バーチャルオフィス」を作成することにしました。 ワールド作り プラットフォームとしてclusterを選択しましたがclusterが提供するのは好きなイベント会場を作り、そこに集まってイベントを開催することで、会場そのものは自分自身で作る必要があります。 幸いなことに弊社では2019年に実施した社員総会で、VRChatに最適化した「バーチャルオフィス」を作成していました。今回はこのワールドをcluster向けに修正する形で対応しました。 techblog.zozo.com 修正点は次の通りです。空間そのものは出来上がっていたので、大きく手を入れる必要はありませんでした。 Cluster Creator Kitの導入 公式SDKとして提供されているCluster Creator Kitを導入し、もともと存在していたスクリーンと差し替える形で Standard Main Screen View と新たにコメントを表示する Standard Comment Screen View その他、最低限必要なオブジェクトを追加しています。 github.com 難しいポイントはないので、ドキュメント通りに設定すれば問題ありません。 VRChat向けの「バーチャルオフィス」では、エントランスから入って直ぐ目の前にある円会議室の中にのみスクリーンが用意されていました。しかし、数十人が1度に参加する可能性があるオフィス見学という特性を考慮してcluster向けの「バーチャルオフィス」では円会議室を出たところにもスクリーンを設置しています。 あわせて円会議室内のスクリーンは見やすさを考慮して「現実世界」よりも数割大きめに設置しています。 clusterに最適化したシェーダーへの変更 clusterはWindows, Mac, Android, iOSというマルチプラットフォームで動作する性質上、そのすべてのプラットフォームで期待する動作を求めるためにはジオメトリシェーダーが使えません。 VRChat向けの「バーチャルオフィス」では、円会議室を構成するガラス部分をはじめとする複数箇所でNGとなるシェーダーが使われていました。 ガラスの表現には、モバイルプラットフォームでも使えるシェーダーを選択しました。このシェーダーは、UnityのAssetStoreで無料でダウンロードできるのですが、擬似的な屈折表現なども行える優れたものでした。金属やガラスなどに物体が反射する表現には、リフレクションプローブを使用しており、計算負荷を抑えてながら品質の向上を目指しました。 https://techblog.zozo.com/entry/compass2019ss より引用 見栄えはとても良いのですが、様々な環境からオフィス見学を実現できるよう、今回はUnity標準のStandardシェーダーに変更しました。 VRChat向けに設定されていたシェーダー Unity標準のStandardシェーダー Standardシェーダーでも特徴的なガラスの曲面は再現できています。 VRChat向けシステムの削除 VRChat向けの「バーチャルオフィス」は文字通りVRChatで動作させることを意図しているので、VRChat向けの機能がいくつか内包されていました。これらは不要なので削除しました。clusterのワールドは不要なアセットを削除した方がアップロードもプレイ時のダウンロードも早くなります。 歩き回ることを想定したコライダーの削除と追加 VRChat向けの「バーチャルオフィス」は前述の通り「社員総会」で利用することを意識していたこともあってか、オフィス内を歩き回る「オフィス見学」用途としては成り立たない箇所がいくつかありました。そういった箇所は透明な壁となるコライダーを削除あるいは追加しました。 植物の削除 現実世界の青山オフィスには実に100鉢以上の緑が生い茂っています。これらをバーチャル世界でそのまま再現すると、少し歩きにくく感じてしまいます。その対策として青山オフィスの雰囲気を損なわない範囲で植物を削除しました。 オンサイトでのオフィス見学が再開した際には、ぜひ「バーチャルオフィス」との差分をその目で確かめてもらいたいです。 バーチャルならではの「遊び」 せっかくの「バーチャル」なので、現実世界の「青山オフィス」とは異なるちょっとしたアイテムを用意しました。 エントランスのZOZOMATと箱猫マックス 乗っかったところで実際に足のサイズは測れませんが、エントランスの左側足元に「ZOZOMAT」を設置しました。また、同エントランス右側にはZOZOTOWNの公式キャラクターである「箱猫マックス」を設置しました。 左: ZOZOMAT 右: ZOZOTOWN公式キャラクター「箱猫マックス」 持ち上げられるZOZO箱 箱猫マックスの身体を流用してオフィス内の複数箇所に「ZOZO箱」を複数設置しました。ZOZOTOWNを利用したことがある方ならお馴染みのあの黒い段ボール箱です。 Reference: https://note.com/zoooom/n/n06d63160f4bb 単純に置いてあるだけでは面白みに欠けるので、持ち上げたり積み上げられるようにしました。 ZOZO箱を持ち上げている様子 これはCluster Creator Kitで用意されている Grabbable Item コンポーネントを使うだけで簡単に実装できます。あわせて必要な Item , RigidBody , Movable Item 各コンポーネントも一緒に設定されるので、別途必要になるのは Item コンポーネントの名前を変えて、コライダーを追加するだけです。 Grabbable Itemの設定 ちなみに本記事の公開時点ではアイテムのリスポーン(初期位置に戻る仕組み)が実装されていません。そのため、1度動かされた「ZOZO箱」を「掃除」する手間を省くために公開版のワールドには含まれていません。 リアルアバターの利用 clusterの世界での見た目となるアバターは、原則として全ユーザー共通のものが用意され、顔の部分のみアカウントにアイコンとして設定している画像が表示される仕組みになっています。 標準アバターは顔部分にアイコンに設定した画像が表示される それとは別に、VRMという国産のVR向け3Dアバターファイルフォーマットのアバターを用意することで、自分が使いたいアバターを使用できます。 vrm.dev 今回は一部の社員に限りますが、フルボディ3Dスキャンした身体をclusterに最適化したVRMデータとすることで、いわゆる「リアルアバター」としてオフィス見学の旗振り役を務めました。 仕様に沿ったVRM形式のリアルアバターを設定した様子 リアルアバターを制作する手法は色々とありますが、今回は奇しくも1月時点でリアルアバター株式会社さんに撮影してもらっていたデータを活用しています。 www.real-avatar.com clusterでのVRMアバターは32,000ポリゴン制限があるので、UnityのMesh Simplifyで手早くポリゴン数を削減しています。 プラットフォーム選定の補足 補足にはなりますが、冒頭のプラットフォーム選定で挙げたVRChatとMozilla Hubsを選択しなかった理由を記載します。 VRChatを選択しなかった理由 今回は国産のVR SNSであるclusterを選択しましたが、実はVR SNSは全世界で100以上存在しています。その中でも日本国内のVRが好きな方々の中で特に有名なのが「 VRChat 」です。 つい最近では株式会社ウィゴーさんや、株式会社三越伊勢丹ホールディングスさんが企業として参加したバーチャルマーケット4のプラットフォームもこのVRChatです。 www.wwdjapan.com 前述の通り既存の「バーチャルオフィス」はVRChat向けに作られていたので、このVRChatを使えばcluster向けの修正も必要ありませんでした。 しかし、VRChatは一定以上のスペックを持ったWindows PCを必要とすること、そしてプライベートなワールドは運営と事前にフレンド登録が必要なことから選択しませんでした。 弊社がVR領域を中心とした事業を展開しているのであれば、VRChatを選択することもやぶさかではありません。しかし、今回は幅広い職種・職域の方が「来社」できることを考慮し、スマートフォンを含むマルチプラットフォームに対応しているclusterを選択した形になります。 Mozilla Hubsを選択しなかった理由 「MoziLla Hubs」はアプリのインストールを必要とせず、ブラウザだけで体験できる、いわゆるWebXR技術を活用したプラットフォームです。私自身はXR領域の中でも特にWebXRを推しているので、この Mozilla Hubs もリリース当初から強く推しています。 Mozilla Hubsでは「 Spoke 」というウェブアプリケーションを通してclusterやVRChatのように自分独自のバーチャル空間を構築できます。 SpokeにはVRChat向けのアセットであるFBXファイルをそのまま持ち込めないので、UniGLTFでSpokeでも扱えるGLBファイルにエクスポートしたものをインポートする必要がありました。しかし、Spokeに持っていくだけであれば大きな手間はかかりませんでした。 Spokeで円会議室を設置した様子 clusterやVRChatと違い、Mozilla Hubsは専用アプリのインストールを必要とせず、Windows, Mac, Android, iOSのどれでも普段使っているブラウザから気軽にアクセスできます そんな良いこと尽くしのMozilla Hubsですが、求める「バーチャルオフィス」を再現するにはアセットの容量制限(推奨は12MBで上限は128MBのところGLBファイルの容量だけで600MB超え)が厳しく、今回は諦めました。 公開を見合わせたMozilla Hubs版のバーチャル青山オフィス 緑とデスクを取り除いた形で、オフィスそのものと特徴的な円会議室だけであれば再現できたので、何らの形で使える機会を伺っています。 まとめ バーチャルSNSであるclusterをプラットフォームとして、VRChat向けワールドを改変したcluster向けのワールドで、『バーチャルオフィス見学会』を実施するまでの取り組みを紹介しました。 今回はVRChat向けの素材が手元にあったため、アイデア出しから実施までのスパンを短くできました。もしも手元になかった場合はゼロイチで「バーチャルオフィス」を作る必要があったので、高い再現性を求める場合は相応の工数が発生していたはずです。 5月25日を以て全都道府県で緊急事態宣言は解除されましたが、いわゆるアフターコロナ/ウィズコロナの世界ではオンサイトではなくオンライン、バーチャルで物事を実施する機会が増えていくと考えています。そのような世界で、こういった形でバーチャルオフィスを作るという手段もあるということが伝わりますと幸いです。 最後に ZOZOテクノロジーズでは今後も「オンライン配信」や『バーチャルオフィス見学会』など今まで取り組んでいなかったことにも積極的に取り組んでいきます。 一緒にサービスを作り上げてくれる方だけではなく、エンジニアの技術力向上や外部発信に興味のある方も募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com また、再掲になりますが、6月9日の『バーチャルオフィス見学会』も受付中です! zozotech-inc.connpass.com 現場からは以上です!
こんにちは、基幹システム部メンテナンスチームの矢野です。 今回は僕のチームで行っている毎日勉強会について書いていきたいと思います。 新しいインプットの機会創出 組織内の技術力のベースアップ施策 社内コミュニケーション このようなことを考えている方の参考になればと思います。 経緯 まず毎日勉強会というものが形作られた経緯ですが、チームまたは部署のために下記の3つの事柄の質を向上できないかとぼんやり頭の中で考えていたことがきっかけでした。 自部署のブランディング(必要とされる部署になる、自分たちの見解を持つ) 部署内コミュニケーション向上(朝礼など) 部署のメンバーがエンジニアとして渡り歩いていくための手助け(心理的安全性) まず1つ目は自部署のブランディングです。 ブランディングと書くとちょっとお高い感じに聞こえちゃいますが、要は社内で必要とされるよう部署の価値を上げていきたいということです。「ZOZOの基幹のことは基幹システム部に任せておけば大丈夫」という認識を社内に一層持ってもらいたい、そういった社内の信頼を上げるためにはどのようなことをしていけばいいか、どのような組織に成長させたいかを考えていました。 2つ目がコミュニケーションの向上です。 僕が所属しているメンテナンスチームは、2019年5月から発足した比較的新しいチームです。今まで別々のチームに所属していた4人が集まり、緊急施策で優先度が下がった案件やリファクタリングなどのビジネスにはかかわらないが、必要なシステム改修を行うチームとしてスタートしました。 全く別々のチームで業務を行っていたメンバーが集まったため、発足当初はコミュニケーションがうまく図れず4人で黙々と作業をこなす日々でした。チームリーダーとしては「これはもうちょっと雑談とかできるような雰囲気にしたほうがいいな」と感じ毎朝5分間の雑談タイムを始めました。ランダムでネタを提供してくれるアプリを使いながら毎日やっていたため、ネタには困りませんでした。しかし、3か月もするとマンネリ化してきますし、メンバーの距離も縮まってきたため朝の雑談タイムは一旦ストップしました。 でもこの朝の雑談タイムがきっかけで、チーム全員で何か1つのことをやるという時間は、別の形で続けていきたいなと思うようになりました。 朝礼は自分の中で永遠のテーマですね。マンネリ化しない有意義な朝礼。これが思いついたらまたテックブログ書きますね。 3つ目に部署のメンバーがエンジニアとして渡り歩いていくための手助けです。 ZOZOの基幹システムはすでに安定した環境が出来上がっているので技術的にはその環境内での改修が主になります。ビジネス部門や倉庫の要望を改修し形にしていきますがそうなるとどうしても業務だけでは新しい技術への接点が少なくなります。メンバーには、エンジニアとしての視野を少しずつでも広げていってもらいたい。そのためにはエンジニアとしてのアンテナの張り方や新しい技術に触れる機会をこちら側から提供できれば成長の支援になるのではないか。そんな思いもありました。自分たちは基幹システムを改修する人ではなく、あらゆる問題をシステムで解決する集団という風に意識を一段階アップできれば先に書いた他部署からの信頼にも寄与できるのではないかと思っています。 枠組み・やること では、上記のような考えを形にするにはどうすればいいんだろう?? 3つの軸で考えてみました。 絶対やりたくないこと 期待すること 現状(現実) 絶対やりたくないこと まず1つ目の軸ですが、これらを達成する手段として絶対にやりたくないことを考えてみたところ2つありました。 強制はイヤだ 途中離脱はイヤだ 真っ先に思ったのは強制的に何かをやらせることだけは避けたいということです。自主性を重んじた枠組みを作ることがこの枠組み自体を最大限有意義に活用でき、活用していくことで得られる成長やコミュニケーションにおいて都合よく働くと思ったからです。 中学生の時の実体験として「ギターを弾けるようになりたい」と思ったその時のモチベーション・初期衝動がギターを弾けるようになった一番の理由だと確信しています。 このことからも自主的に何かをやりたいと思った時が一番吸収する時期であることは間違いないです。 またやるからには途中で離脱できるような環境は作りたくないと思いました。ただ「途中離脱できない環境を作る」と考えると「絶対やりたくないこと」で書いた強制力が顔を出してきそうだったので、途中でやめられないようにレールを敷くのではなく、途中でやめてしまう理由を排除していって結果的に途中離脱がなくなるようにするといったイメージで考えを組み立てていくようにしました。 期待すること 2つ目の軸でこの枠組みに期待することは3つありました。 新しい技術に対するハードルを下げておきたい チームをまたいだ交流の活性化につなげたい 書籍購入補助制度を有効活用したい 先に書いたように現状ZOZOの基幹システムの開発は一定の技術を覚えればあとはビジネス部門や倉庫と話を詰めて案件を進めて行くことができます。ですが、今後モダンな環境へとリプレースする時期が必ずやってきます。そうなったとき、いきなりメンバーに使ったことのない技術を使って開発しろとなるとそれこそ効率的な開発などできませんし、その技術を習得するまでに時間もかかってしまい最悪案件を進められない時間が出てくる可能性もあります。経緯のところで記載した自部署のブランディングも保てなくなるかもしれません。なので、まずは新しい技術に対するハードルを下げておけるようなものにしたいと考えました。 次はチームをまたいだ交流を盛んにしたいというものです。弊社では、チームが組織の一番小さい単位になります。このチームという単位で業務を遂行する場面がほとんどなのでチーム内の交流は自ずと図れるのですが、その1つ上の「部」という単位での交流もさらに活性化させられたらいいなと思いました。最終的には部という枠も取っ払って社内の誰もが交流できる場を提供できたら最高ですね。 最後は書籍購入補助制度の有効活用です。弊社には書籍購入補助制度というものがあります。これは購入した書籍をレビューして経費申請すれば全額を精算できる制度です。会社の経費で購入した書籍は絶対に無駄にしたくないので(自腹で買った本であれば自分の好きにすればいいですが、経費はみんなが頑張って稼いだお金ですから無駄にしたくないですね)買ったからには最大限有効活用できる方法を見出してこの制度をさらに有意義に使えないかと考えました。ともあれ書籍を最後まで読むって達成感得られますよね。 現状(現実) 3つ目の軸には現状(現実)と書きましたが、これは実際に行われている勉強会などスキルアップするための機会についての問題点を考えてみました。 社内勉強会などで自分の興味が有るものが開催されるとは限らない(実体験) 講師がいる勉強会はわかったつもりになって終わることも結構ある(実体験) スケジュール調整できずに参加しなくなるとそのまま離脱してしまう(実体験) 興味があってもなかなか勉強に着手できる時間がとれない(実体験) 本を買って一人で勉強しても最後まで続かない(実体験) 全部実体験です。 こう見るとやっぱり自主性って成長にとって最大の栄養素なんだなと思えてきます。自主性をうまく成長へのモチベーションに変えられるような枠組みが作れれば上記のような問題も解決できるのではと思いました。 そして、ここから導き出した1つの答えがこちらです。 「 勉強したい人には勉強する時間を毎日1時間与える 」 これにより前述した問題点や希望することの中で解決できることがいくつかあります。 問題点・期待すること 解決理由 強制はイヤだ 勉強したいと思った人が使える時間なので強制ではなく完全自主性 新しい技術に対するハードルを下げておきたい 新しい技術も含め興味のある分野を習得できる時間として使える 社内勉強会などはあるが、自分の興味が有るものが開催されるとは限らない 興味を持った段階でそれについて学習できる時間が得られる 興味があってもなかなか勉強に着手できる時間が取れなそう 上長と相談の上、業務調整ができれば時間も確保できそう、というか時間を確保するために調整するというメリハリがつけられそう ただ、これだけではこの時間に何をやればいいか迷いそうなのでもう少し決めごとを作りました。 追加で考慮したいこと 途中離脱はイヤだ 書籍購入補助制度を有効活用したい 講師がいる勉強会はわかったつもりになって終わることも結構ある スケジュール調整できずに参加しなくなるとそのまま離脱してしまう 本を買って一人で勉強しても最後まで続かない チームをまたいだ交流の活性化につなげたい この辺りを何とか枠組みに追加できないかと考え、出てきた答えがこちらです。 「1冊の本を興味のあるメンバーを募って最後までやる」 これで追加で考慮したいことがカバーできそうです。 問題点・期待すること 解決理由 途中離脱はイヤだ 一冊の本を最後までやりきるという枠組みを作ることにより途中離脱を無くします。(終わらせる期限をつけないことがポイント) 書籍購入補助制度を有効活用したい 書籍を一冊最後までやりきるということを枠組みとします。 講師がいる勉強会はわかったつもりになって終わることも結構ある 興味がある人が集まって同レベルで勉強していくことで全員に当事者意識が生まれる。 スケジュール調整できずに参加しなくなるとそのまま離脱してしまう あえて期限を設けないので離脱する要因が減ります(後述します) 本を買って一人で勉強しても最後まで続かない 複数人でやることで補いあいながら最後まで進められる(複数人いることで見えない抑止効果もあるかも) チームをまたいだ交流の活性化につなげたい 勉強したい本が見つかった人はこの本の内容に興味がある人を誘います。賛同者をチーム外からも集められるので交流の場が広がります。 できた! 上記をまとめるとこんな枠組みが出来上がりました 勉強したい本が見つかった人は同じ興味を持った人を募り(最大4人)毎日1時間全員でその本を最後まで勉強する ここで補足です この枠組がなぜ「毎日」なのか、なぜ「4人」なのかというところです。(補足だけど重要なポイント) 参加型の勉強会を見てきて運営が難しそうなだと思ったところがあります。 問題点 理由 参加者の続けるモチベーション 回数を追うごとに参加者が減っていく 参加者の当事者意識 参加しているという事実だけでわかった気になってしまっている。(自分はこの手の人間です) 1回の欠席がフェードアウトのきっかけになる 講義を一度欠席するとだんだん内容がわからなくなり途中離脱を助長させる これって大勢を集めてやるからこそ生じる問題点ではないかと考えました。 講義の開催者側としては受講生は大勢いるので一人が休んだくらいでは講義を止められない。受講者側の心理としては大勢だと自分ひとりが休んだくらいでは講義は止まらない、最悪自分はついていけなくなるかもしれないが他の受講者に迷惑をかけることはないので欠席することへのハードルが下がる。一回休むと講義は進むので理解に遅れが生じ途中離脱へ・・・ この辺りは「毎日やる」「最大4人での開催」というルールを定めることでいい方向に持っていけそうでした。 なぜ毎日なのか まず前提としてこの枠組みは有休や急なミーティング等で参加できなくなるのはOKとしています。勉強会のことは気にせず有休をとったり緊急案件の対応してもらいたいです。一人が参加できなくなった場合はその日の会はスキップしますが翌日また全員が集まった日に続きをやればいいのです。毎日といっているのに矛盾していますが毎日参加者が全員集まれる日に開催するというラフな感じです。 スキップOKが前提なので例えば開催日を毎日ではなく週1回にすると次の会が次週になってしまいます。1週開くと進みが遅く途中離脱を助長させそうなので開催は基本毎日です。ここが「毎日」とした理由です。 このような枠組みなので前述しましたが期限は設けていません。誰も離脱することなく最後まで終わらせるため、できる日は毎日書籍が終わるまで全員参加で行うというルールです。 なぜ4人なのか 有休や急用での会のスキップを参加者に切り出しやすい最大人数は4人位と想定しています。講師なしで進めるので個人個人が当事者意識を持てる最大人数も4人と想定しています。みんなで協力しあえる最大人数も4人と想定しています。失敗しても恥ずかしくない最大人数も4人と想定しています。完全に感覚値なのですが皆さんも4人と5人では5人の方が上記のバランスが崩れそうな感じがしませんか? こうして出来上がった毎日勉強会の仕組みを利用して僕たちメンテナンスチームでは現在3冊目の本を絶賛勉強中です。 コロナ禍で全員在宅勤務ですが毎日14時にテレビ会議をつないでDockerの勉強をしています。進め方は扱う書籍によって変わってくるので枠組みの中には入れていません。適宜集まったメンバーで進め方を考えて勉強していけばいいと思います。演習やハンズオンが多くある本であればみんなでやりながら全員が同じようにできるまで進めるでもいいですし読み進めるような書籍であれば章や区切りやすいところまで読み進めまとめでディスカッションという形式でもいいと思います。そこは自由です。 4人のうちだれか一人でも置いてけぼりにならないようにというところにだけ気を付けて理解が早い人はフォローして進めることが大事だと思います。 会社や組織によって毎日1時間の時間を割くということは調整が難しい場面も出てくるかもしれません。上長や周囲と相談し業務調整の上、毎日1時間の勉強時間が取れればなるべく毎日4人がそろった日にみんなの興味のある書籍を進めるという気軽な気持ちで始めてもらえるといいと思います。 まとめると 毎日勉強会を開催すると参加者全員が興味のある書籍を当事者意識をもって最後まで読み切ることができます。 これにより、参加者は勉強したことはもちろんコミュニケーションの向上などが得られます。管理者としてはメンバーの学習機会の創出ができます。 またこの枠組みは一冊の本を最後までやりきることを目的としていてそれを達成できるようにスキップOK、毎日やる、参加者は4人、期限を設けないなど手軽に始められるようにも考慮されているのでぜひ調整がついたら一回やってみてもらいたいです。 今後社内に広がって所々で自然発生的に行われる文化になれば最高だなと思っています。 いかにシームレスに始められそして参加者がドライブしやすいインプット支援の方法を考えたら、毎日勉強会という1つの方法にたどり着いたというお話でした。 あとがき 上記のことが全部ひとりでできちゃう人はもちろんたくさんいると思います。自分はHowToやメソッドを考えるプロではないので実体験などから自分だったらこうやればストレスなく始められるな、こうやれば途中で諦めることなく続けられるなという目線で組み立てていきました。 仕組みを作る上で、はみ出さずにゴールまで導きたいときにはレールを敷くのではなく、なぜはみ出すのかを考えその理由を1つずつ排除していくことで自ずとはみ出さずにゴールまで行きつくという考え方があることに気付けたのもいい経験になりました。 この毎日勉強会の枠組みもなるべくレールを敷かないようにしていますのでフレキシブルな対応に頼っているところは多々あります。ルールとして決めるところは決める、でもそれ以外のところは当事者同士でフレキシブルに対応という形が取れれば管理しなくても自発的に動いていく仕組みができていくのかなと思いました。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
こんにちは、ZOZOTOWN部でAndroidエンジニア/TechLeadをしている堀江( @Horie1024 )です。本投稿では、ZOZOTOWNのAndroidチームで行っている「Codelab会」についてご紹介します。 Codelab会とは? Googleが公開している Codelabs は、AndroidだけでなくGCP、TensorFlow、Firebase、Flutter、Augmented Reality等の様々なトピックをカバーする、チュートリアル形式でまとめられた教育コンテンツです。 Androidに関するCodelab も多く公開されています。「Codelab会」は、ZOZOTOWN Androidチーム全員でCodelabに取り組む勉強会として2019年の7月からはじめました。 ZOZOTOWN Androidチームの課題 私がZOZOTOWN Androidチームに加わったのは2019年の4月で、ZOZOTOWN AndroidチームではAndroid開発を始めてからまだ日が浅いメンバーが多く「チームのAndroid開発に関する知識の底上げ」が課題でした。これはTechLeadとしてチームに加わった自分の課題でもあります。チームメンバーのAndroid開発に関するスキルアップサポートをどう行っていくか?を考える中で思いついたものの一つがCodelabの活用です。 Codelabでは、実際にAndroidアプリを作成するプロセスを通して手を動かしながら学習を進めることができ、Android開発に関する知識を身につけるのに非常に効果的です。個人としても新しい技術のキャッチアップにCodelabを活用していて、その効果を実感しています。一方でただCodelabをチームメンバーに勧めるだけではスキルアップサポートとして十分では無いと感じていました。 チームでCodelabを進めるCodelab会 「Codelab会」はチーム全員でCodelabに取り組む勉強会です。これまでに次のようなCodelabに取り組んできました。 Use Kotlin Coroutines in your Android App Android Room with a View - Kotlin Notification Channels and Badges (Kotlin) *1 Jetpack Compose basics Using Dagger in your Android app - Kotlin Background Work with WorkManager - Kotlin MDC-101 Android: Material Components (MDC) Basics (Kotlin) MDC-102 Android: Material Structure and Layout (Kotlin) MDC-103 Android: Material Theming with Color, Elevation and Type (Kotlin) チームでCodelabに取り組むことで次のようなメリットがあります。 定期的にCodelabに取り組む習慣ができる チーム全員の知識レベルを揃えられる Codelabの内容についてより理解を深められる 定期的にCodelabに取り組む習慣ができる 個人でCodelabに取り組むと習慣化するまで続かない場合があるでしょう。私個人の経験でもそうで、業務外で時間を取ってCodelabを進めるのはハードルが高く感じられる人が多いと思います。Codelab会では、業務時間内でスケジュールを確保して取り組むため、個人で取り組む場合より習慣化しやすいメリットがあります。現在、Codelab会は基本的に週一回の頻度で行っていてZOZOTOWN Androidチームの勉強会として定着しています *2 。 チーム内の知識レベルを揃えられる チーム全員が同じCodelabに取り組むことで、チーム内の知識レベルを揃えることができます。このメリットが役立った例として、「 Use Kotlin Coroutines in your Android App 」に取り組んだ例があげられます。この例では、Kotlin Coroutinesの基礎知識、Android開発においてどう使うのかという点でチーム全員の認識を揃えることでCoroutinesのZOZOTOWN Androidアプリへの導入が比較的スムーズに進められました。 codelabs.developers.google.com Codelabの内容についてより理解を深められる Codelabは全編英語であり、内容が高度な場合もあるため、理解するのに時間がかかることがよくあります。このことは、Codelabの途中で挫折する大きな要因となります。 Codelab会では、Codelabを進める中で理解できない点がある場合でも参加メンバーが相互にフォローしながら進めることでその場で相談できる状況を作り、内容についての理解を深めることができます。特にZOZOTOWN AndroidチームではAndroid開発を始めてからまだ日が浅いメンバーが多い状況だったので効果的でした。 Codelab会の実施方法 ここでは、Codelab会をどのように実施しているかについて解説します。Codelab会を実施するまでの流れは次の通りです。 取り組むCodelabの決定 進行役の決定と事前準備 参加メンバーの事前準備 Codelab会の実施と振り返り 取り組むCodelabの決定 最初に行うことは、Codelab会で取り組むCodelabを決定することです。Codelabを選ぶ基準は次のようにしています。 チームメンバーが興味のある内容であること チームの技術的な方向性にマッチする内容であること チームメンバーが興味のある内容であること 選択するCodelabは、チームメンバーが興味のある内容が望ましいです。ZOZOTOWN Androidチームでは候補は私が出す場合もありますが、やってみたいCodelabがあれば提案してもらうようチームメンバーにお願いしています。複数の候補がある場合、次のように投票で決めてしまうこともあります。 チームの技術的な方向性にマッチする内容であること 選択するCodelabがチームの技術的な方向性にマッチする内容であることも重要です。ZOZOTOWN Androidチームでは、チームの人数が増えてきたことからよりスケールする開発体制を目指す一環として、Jetpackの「 Guide to app architecture 」で紹介されているアーキテクチャをベースにした新しいアーキテクチャを採用し、リアーキテクチャを進めています。 リアーキテクチャを進めるにあたり、Codelab会では、チーム全体で新しいアーキテクチャについて理解を深める目的でLiveDataやViewModelといったLifecycleコンポーネント、Room、Kotlin Coroutines、Daggerを扱うCodelabを選択しています。 Use Kotlin Coroutines in your Android App Android Room with a View - Kotlin Using Dagger in your Android app Kotlin Coroutines Flowについても導入を検討していますが、導入前にCodelab会で次のCodelabを進める予定です。 https://codelabs.developers.google.com/codelabs/advanced-kotlin-coroutines/#0 codelabs.developers.google.com 進行役の決定と事前準備 Codelab会当日に会の進行を担当する進行役を決めます。進行役は、最初のうちはAndroid開発に十分な経験がある人が務めるのが良いでしょう。ZOZOTOWN Androidチームでは、私が進行役を務めています。 進行役が事前に行う準備は次の通りです。 スケジュールの調整 選択したCodelabの予習 スケジュールの調整 Codelab会のスケジュールを決め参加メンバーの予定を確保します。選択したCodelabを最後まで進めるのに複数回Codelab会を行う場合が多いため短いスパンで開催するのが望ましいです。 ZOZOTOWN Androidチームでは、初回から2回目の開催まで一ヶ月空いてしまい、参加メンバーからは前回の内容を忘れてしまうので期間を空けないで進めたいという要望を多く貰いました。そのため現在では週1回1時間で行っています。これより多くの時間を確保しようとすると疲れますし、チーム全員の予定を調整するのが難しくなるため1時間としています。 Codelab会のメリット でもお伝えした習慣化にもつながるので定期的な予定としてスケジューリングするのが良いでしょう。 選択したCodelabの予習 Codelab会の進行は、進行役が実際にCodelabを進める形で行います。会の進行を滞りなく行うのは進行役の重要な役割です。進行中に発生する問題として次のようなものがあげられます。 内容の理解に時間がかかりスムーズに進行できない サンプルプロジェクトがビルドできない 進行役がCodelab通りの結果にならず進行が止まる これらの問題について、選択したCodelabを事前に進めておくことで問題を事前に察知し回避できます *3 。 内容の理解に時間がかかりスムーズに進行できない 初見でCodelabの内容を理解して解説するのは難しく、内容の理解に時間がかかりスムーズに進行できない可能性が高いです。そのため、Codelabの内容を解説できるレベルまで理解できるよう、事前の予習を進める中でわからないと感じた点は調べ説明できるように準備しておきます。これにはCodelabで登場するサンプルコードへの理解も含みます。 サンプルプロジェクトがビルドできない 殆どありませんが、いざCodelabを始めてみるとサンプルプロジェクトをビルドできない場合があります。サンプルプロジェクトが依存するAndroid Plugin for Gradleやライブラリのバージョンを上げるなどするとビルドが失敗する場合があるので事前に確認しておくことは有用です。 「 Jetpack Compose basics 」のような開発中のツールやAPIを使用するCodelabは、特に事前に確認しておくことをオススメします。Codelab会で取り上げた際には、ビルドがうまくできない参加メンバーがいましたが、事前に確認しておいた結果サポートすることができました。 進行役がCodelab通りの結果にならず進行が止まる 進行役は、実際にCodelabを進めていきますが、Codelabで示されている結果と手元で実行した結果が異なると進行を止める結果となってしまいます。「 Background Work with WorkManager - Kotlin 」では、チェインしたWorkRequestが返す結果がCodelabで示されるものと手元で実行したものとで異なってしまい、その原因を探るために20分ほど時間を無駄にしてしまいました。事前にCodelabを進め、各チャプター終了後にコミットしtagを打つかブランチを切っておくとこの問題を防ぐことができます。 実際にコードを書きながら進めた方が進行しやすい場合もあるので、基本的にはコードを書きながら進め、進行が止まったら用意しておいたtagやブランチを活用すると良いでしょう。 参加メンバーの事前準備 Codelab会への参加メンバーは、選択したCodelabについてサンプルプロジェクトのCloneと初回のビルドを事前に行っておきます。これにより、会の時間を有効に活用できます。また、進行役は、Codelab会の前日や当日に参加メンバーへリマインドをすると親切です。 参加メンバーも事前にCodelabを進め内容把握しておくとより理解が深まるかもしれませんが、参加メンバーの負担が大きくなります。負担が大きくなると会を継続していくのが難しくなるため、現在特にルールを定めていません。 Codelab会の実施と振り返り Codelab会は次のような流れで実施します。 進行役が自分のPCの画面を参加メンバーに共有する 進行役がCodelabを実際に進める 不明点があるメンバーがいないか適宜確認しフォローする 終了後フィードバックを貰う 進行役が自分のPCの画面を参加メンバーに共有する 現在ZOZOテクノロジーズでは、原則リモートワークが義務付けられているため、チームメンバー全員がリモートワークをしている状況でCodelab会を進めています。 ZOZOテクノロジーズでは、 ビデオ会議システムとしてCisco Webex Teamsを利用できる ためCodelab会でも利用していますが、それ以外でもSlackのCallやZoom、Meetといった画面共有が可能なツールであれば問題ありません。全員がリモートでCodelab会をやってみた感想ですが、物理的な会議室を確保する必要が無いのは楽ですし、特に問題なく開催できています。 進行役がCodelabを実際に進める 進行役はCodelabを実際に進めていきます。この時、Codelabの内容を解説しながら進めていくのがコツです。内容プラスアルファの部分をどの程度解説するかですが、進行役が予習で詰まった箇所や理解が曖昧で調べた箇所などを進めながら解説しています。Codelabの内容から逸れる場合もありますが、参加したメンバーからは好評でした。 例えば、「 Use Kotlin Coroutines in your Android App 」を取り上げた際には、Codelabの内容に加え、Kotlinの文法やテストコードを書く際の留意点など併せて解説しています。 不明点があるメンバーがいないか適宜確認しフォローする Codelab会を進めていく中で、Codelabの内容に不明点があるメンバーがいないか適宜確認します。もし不明点があれば、進行役または他の参加メンバーがフォローするようにします。確認するタイミングはCodelabの難易度によって変えます。比較的簡単なチャプターであればそのチャプターの終了時、逆に難しければチャプターの各セクションごとに確認するようにしています。 例を上げると、「 Using Dagger in your Android app 」では、Dagger自体が理解する難易度が高いこともあり、セクションごとに確認を行いました。Codelabの内容について不明点を残したままにしてしまうとチーム全員にとって良くないので適切なフォローは大切です。 終了後フィードバックを貰う フィードバックを貰うことは非常に重要です。Codelab会終了後、できる限り早く参加メンバーにフィードバックを依頼し、コメントを貰うようにします。フィードバックを改善に活かすことで、より良いCodelab会の運営に繋げられます。 次のスクリーンショットは、2回目のCodelab会のフィードバックコメントです。このフィードバックを受け、次回のCodelab会からコードを書きながら説明する際にはゆっくりと、進行が滞らないよう進行役が事前にCodelabを進めておく運用にしました。 Codelab会を行った結果 Codelab会について参加したチームメンバーにアンケートを取り、次の項目について答えて貰いました。 Codelab会は業務に役立っているか? やって良かったCodelabは? 今後もCodelab会をやっていきたいか? Codelab会は業務に役立っているか? 全員から役立っていると回答を貰いました。業務時間内で定期的に時間を確保して行うことで、新しい技術・知識のキャッチアップに繋がっています。 チームメンバーからの回答をいくつか紹介します。 役に立っていると思います。 理由: 1)使いたいライブラリーと機能について調べる時間がないときに、Codelab会で触ってみるチャンス、 2)既に使用するライブラリーと機能の違う使い方を試すチャンス。 役立ってます! 理由: 時間的な問題で休日でしかCodelabをすることが出来なかったが、ZOZOTOWNの開発に携わるメンバーと行うことによって業務に組み込む前提で話が出来るのがかなり良いと思っております! 役立ってます! 理由: Codelabは一人でやっているとだんだん飽きてきてやらなくなるので、みんなでやると集中してできるし知識の幅も広がっていいなと思いました 役立ってます。 理由: Coroutine,Daggerは業務で使用するが、個人的には学習のハードルが高いのでなかなか取り掛かりにくいですがCodelab会として時間をとって学習すること、しっかりとした知識を持ったホーリーさんが解説するので噛み砕いて説明してもらえて挫折しないで最後までとりくめるから。 やって良かったCodelabは? DaggerとKotlin CoroutinesについてのCodelab、「 Using Dagger in your Android app - Kotlin 」と Use Kotlin Coroutines in your Android App をあげる意見が殆どでした。これは、実際の業務で使用している影響が大きいと考えられます。 また、Material Componentsシリーズ(MDC-101、MDC-102、MDC-103、MDC-104)をあげてくれたメンバーもいました。ZOZOTOWN Androidアプリ全体へのMaterial Designの適応はまだですが、社内でエンジニア、デザイナーを交えたMaterial Design勉強会を進めていて、今後順次適応していきたいと考えています。 チームメンバーからの回答をいくつか紹介します。 Dagger、MaterialDesign 理由: 個人的にDaggerについてとてもよかった。Daggerってpowerfullだけど複雑なツールですね、知識を広げるのは重要です。 最近やっているMaterialDesignについても毎回楽しみにしています。Materialライブラリーをバージョンアップとてもやりたい〜 Coroutine, Dagger 理由: ZOZOTOWN内に組み込む前段として、全員で進められたという点が良かったなと感じました。Codelabを行なったことによって理解力も高まってZOZOMATの開発でも実際に使うことが出来たのでとても良かったです! Coroutine、Dagger 理由: ZOZOTOWNの実装に必要かつドキュメントを見てもよく分からなかったためとても助かりました Coroutine、Dagger、MaterialDesign 理由: Coroutine、Daggerに関してはZOZOでの使用頻度が高まっているので業務を進める上で必要になってきているので。 MaterialDesignに関しては単純に楽しみながら進められているので。 今後もCodelab会をやっていきたいか? 全員から続けたいという回答を貰いました。また、次に取り組んでみたいCodelab( Learn advanced coroutines with Kotlin Flow and LiveData )をあげてくれたり、新しい技術について学び業務に取り入れていきたいとも回答を貰えたり、チームとして新しい技術を積極的に業務に取り入れていく姿勢に近づけられたとも感じています。 チームメンバーからの回答をいくつか紹介します。 是非続けたい。特にこれ: https://codelabs.developers.google.com/codelabs/advanced-kotlin-coroutines/#0 ぜひ! 理由: 古い歴史があるZOZOTOWNのアプリだからこそ、古き良きを重んじるだけでなく機能のブラッシュアップを進めるのではなく最新技術を盛り込んでいけるよう技術の底上げを行なっていきたいですね! やりたい 理由: ZOZOTOWNをどんどんリファクタリングしていくためにも知識を幅を広げるためにもこういう取り組みはやっていきたいです。 やりたい 理由: 常に新しい技術に触れる機会を持って取り入れることで、取り入れた方がいい技術であればみんなで触れて精査できるので。 Codelab会は「チームのAndroid開発に関する知識の底上げ」に繋がっているのか? アンケート結果からも分かるよう、Codelab会はチームへポジティブな影響を与えていますし、今後も続けることで「チームのAndroid開発に関する知識の底上げ」に繋がっていくと感じています。 チームとして学習した知識を活用し、ビジネス的な要求に答えつつ、ユーザーさんがより便利にZOZOTOWNアプリを利用できるよう普段の業務に取り組んでいきたいと思います。 まとめ 本投稿では、ZOZOTOWN Androidチームで取り組んでいる「Codelab会」について紹介しました。ZOZOTOWN AndroidチームではCodelab会を行うことで、チームのAndroid開発に関する知識の底上げに繋がっていると感じています。 一方で改善したい点もあり、特に進行役の担当が私に固定されてしまっている点は早急に改善したいです。参加メンバーで持ち回りで進行役を務めるなど改善をはかっていきたいと思います。 最後に、ZOZOテクノロジーズではAndroidエンジニアを募集しています。ご興味のある方はこちらからご応募ください。 hrmos.co *1 : 現在Deprecatedになっています。代替で推奨されるCodelabはこちらです。 https://codelabs.developers.google.com/codelabs/advanced-android-kotlin-training-notifications/index.html#0 *2 : 時期によっては案件の都合上実施できていないことがありました。。ただ、チーム内で忙しくても少しずつやった方が良かったという意見がでています。 *3 : こちらも業務時間で行っています。
こんにちは、WEAR部の繁谷です。 普段はバックエンドのエンジニアとしてWEARの開発を行っています。 ZOZOテクノロジーズは4月7日に「 髪型別コーデ検索 」をリリースしました。 プレスリリースは是非 こちら を御覧ください。 髪型別コーデ検索のフロントエンドはSPA(Single Page Application)でつくられており、こちらの開発を行った際に意識した設計について紹介します。 はじめに 髪型別コーデ検索は、 ZOZO研究所 の福岡チームが研究・開発したAIを活用し髪型からコーディネートを検索するAPIを利用して、SPAのWebサービスとして提供しています。 こちらは髪型別コーデ検索のアーキテクチャを簡単に示したものです。 今回私は研究所が提供するAPI以外のエンジニアリングに関する部分である、バックエンドのAPI開発、フロントエンドの開発、それらのインフラ構築を担当しました。 その中でもフロントエンドの開発は、SPAでの開発経験者がチーム内に私を含めて誰もいない状態から、一人で基礎の設計を行いその後開発メンバーを追加して素早くリリースすることを目指しました。 この過程で、どのように技術選定や設計、実装を行ったかを紹介します。 技術選定 髪型別コーデ検索のフロントエンド開発において、どのように技術選定を行ったかを説明します。 注力すべきことは何か 技術選定を行う前に、まず開発において注力すべきことを3つ定めました。 素早いリリース 技術的な挑戦 低い学習コスト 素早いリリース 今回の開発はWEARのユーザの皆様により良い検索体験を提供するために、研究所が開発した髪型別コーデ検索のAPIを効果検証することを目的としています。 そのため、適切に選択と集中をした上で素早くリリースし、PDCAを回すことを重視しています。 よって、3か月程度の開発期間で素早くリリースすることを目標においた上で、実装する内容を取捨選択していくことにしました。 技術的な挑戦 素早くリリースする必要がある一方、同時にエンジニアとして挑戦をする姿勢も我々にとって重要であるため、何らかの新しい技術の習得への挑戦を行うことにしました。 挑戦する内容を決めるにあたっては、当時のチーム内ではSPAでのフロントエンド開発の知見は全く無かったため、これらの技術の習得からリリースまでを開発期間内で行うこととしました。 低い学習コスト 技術的な挑戦は行いつつも、きちんとチームメンバーを巻き込み、素早く開発をスケールさせ、全体の開発スピードを上げる必要がありました。 SPA未経験のメンバーでも素早く開発に入れるよう、学習コストが低い技術選定を行い、適切な設計で開発環境を整備することを意識しました。 実際の検討内容 以上の注力する点を踏まえてフロントエンドの技術選定を行っていきました。 JavaScript vs. TypeScript TypeScriptは、昨今のフロントエンド開発ではデファクトスタンダードのようなものである認識であり、JavaScriptが書けるメンバーであれば学習コストは高くないため素直に採用しています。 React vs. Vue.js 当時の個人的な印象であり、コントリビュータの方々の認識と異なる可能性はありますが、 「簡単に使える」ことを意識しているVue.jsよりも設計を意識した「堅い」イメージのReactを選択しました。 今回は技術的な挑戦による技術力の向上を目的としており、設計力の向上につながると感じたためです。 React Hooks vs. Redux Reduxに関して優位性があった機能は、現在はほとんどがReactでも実現可能な認識です。 非同期処理は、 独自フック を使うことで同様のことができます。 バケツリレーと呼ばれる、親コンポーネントから子へのpropsの受け渡しは、 useContext によって解決できます。 Reducerによる状態管理は、 useReducer を使うことができます。 このように、以前はReduxでしか提供されていなかった機能も、今はReactで提供されています。 ただその中でも、 Redux DevTools によって状態の履歴を確認できることが、開発効率を向上させる上で効果的であったためReduxを選定することにしました。 また、学習コストが高いRedux middlewareは一切使用しないという方針をとっています。 SPA or SSR(Server Side Rendering) チームとしてReactによるSPAの経験もないため、いきなりSSRで開発するのは、学習コストが高いと判断しSPAを選んでいます。 また、WEARから直接アクセスを流すため、SEOを重視しないということもあります。 同様の理由でPWAも意識しない判断をしています。 SPAの設計 React、Reduxを用いたSPAにおいて、今回の開発でどのように設計したか説明します。 全体を通してSPA開発において特に悩みやすいポイントを、SPA未経験のメンバーが悩まず素早くコンポーネントを量産できるような設計を意識しています。 ディレクトリ構成 React、Reduxを用いたSPAではディレクトリ構成パターンが多くありますが、今回の開発は Ducksパターン を用いています。 これは、ReduxにおけるactionTypes, actions, reducerを、下記のツリーのように1つのファイルに書く非常にシンプルなパターンです。 ├── src │   ├── modules │   │   ├── coordinate.ts │   │   └── hairstyle.ts ファイルの中身は以下のように、actionTypes, actions, reducerをまとめて書きます。 // State const initialState = { hairstyles: null , } // Action const SHOW_HAIRSTYLES = 'SHOW_HAIRSTYLES' // ActionCreators export const showHairstyles = ( hairstyles: Hairstyles ) => { return { type : SHOW_HAIRSTYLES , payload: { hairstyles: hairstyles , } } } // Reducer const hairstyle = ( state = initialState , action: any ) => { switch ( action. type) { case SHOW_HAIRSTYLES: return { ...state , hairstyles: action.payload.hairstyles } default : return state } } Ducksパターンは、actionTypes, actions, reducerを1つのファイルに記述するため、そのファイルが肥大化し得ます。 それを解決するためにDucksパターンから派生した、 Re-Ducksパターン があります。 ただ、今回は比較的に小規模なプロジェクトで素早く実装したいため、よりシンプルなDucksパターンを選定しました。 結果的には、特に不都合はなく素早く実装ができたため、今回の開発においては有効なパターンでした。 コンポーネント設計 Reactにおいて、各コンポーネントをどの粒度で切り分け、どのようにコンポーネントツリーを組み上げるかということは大きな関心ごとでした。 このコンポーネント設計においてよく使われるのが、UI設計におけるメンタルモデルの Atomic Design です。 UIを Atoms Molecules Organisms Templates Pages という単位で分割して設計する考え方で、今回の開発でも部分的に取り入れています。 まず、今回の開発では、Atomic Designを取り入れるにあたって以下のルールを設定しました。 再利用するコンポーネントに対してのみAtomic Designを適用する。 Atomic Designはコンポーネントを設計するにあたって良い指標を示してくれます。 ただ、全てのコンポーネントに対して適用しようとした場合、設計コストが高く開発スピードは下がってしまいます。 よって、今回の開発では最初はAtomic Designを強く意識せず実装し、コンポーネントを再利用したくなった時にAtomic Designを適用するルールとしました。 そうすることで、開発スピードを落とさずに本当に再利用が必要なコンポーネントに対して、Atomic Designによる再利用性の向上のメリットを得られました。 具体的にどうやってコンポーネントを実装していくかを、髪型別コーデ検索の髪型の一覧を表示する部分を例として説明します。 まず、初期実装では、ページ毎にコンポーネントを切り出します。 このコンポーネントは react-router-dom などのルーターで読み込まれます。 // pages/Home.tsx const Home: React.FC = () => { return ( <> < Header / > < Hairstyles / > < Footer / > < / > ) } ページコンポーネントの配下のコンポーネントは、設計を意識しつつも自由に実装します。 // Hairstyles.tsx const Hairstyles: React.FC = () => { const hairstyleEntities = useSelector (( state: any ) => state.hairstyle.hairstyles ) const hairstyles = hairstyleEntities.map (( hairstyleEntity: HairstyleEntity ) => < div class= "hairstyle" > < img src = { hairstyleEntity.imgUrl } / > < p > { hairstyleEntity.name } < /p > < /div > ) return ( <> { hairstyles } < / > ) } 次に、上記のコードの <div class="hairstyle"> のところを再利用したくなった場合、これをコンポーネントに切り出しAtomic Designを適用していきます。 この際にAtomic Designの各要素を以下のように定義し、コンポーネントに分けていくようにします。 ※ 独自の定義で実際のAtomic Designの解釈とずれるところがあります。 Atoms : HTMLタグ1つと、それにスタイルを当てるタグでのみ構成され、状態を持たない Molecules : 高々数個のAtomsから構成され、1ページ内で複数回あらわれる可能性があり、他のコンポーネントとグループとしてまとまった状態で使用される Organisms : Molecules、Organismsから構成され、同じコンポーネントは1ページ内では高々1回しかあらわれない Templates : 再利用性が低いため使用しない Pages : ルータから参照され、基本的には再利用されない このように定義を決めることで、どのようにコンポーネントに落としていくかがイメージしやすくなります。 では、実際にAtomic Designを適用してみます。 まず、Atomic Designの適用対象の Hairstyles.tsx をOrganismsに分類します。 // organisms/Hairstyles.tsx const Hairstyles: React.FC = () => { const hairstyleEntities = useSelector (( state: any ) => state.hairstyle.hairstyles ) const hairstyles = hairstyleEntities.map (( hairstyleEntity: HairstyleEntity ) => < Hairstyle key = { hairstyleEntity. id } hairstyle = { hairstyleEntity } / > ) return ( <> { hairstyles } < / > ) } 再利用したい部分をMoleculesとしてコンポーネントに切り出します。 // molecules/Hairstyle.tsx const Hairstyle: React.FC < Props > = ( props ) => { return ( < div class= "hairstyle" > < Img imgUrl = { props.hairstyleEntity.imgUrl } / > < Text > { props.hairstyleEntity.name } < / Text > < /div > ) } imgタグやpタグはAtomsとしてコンポーネントに切り出します。 // atoms/Img.tsx const Img: React.FC < Props > = ( props ) => { return ( < img src = { props.imgUrl } / > ) } // atoms/Text.tsx const Text : React.FC < Props > = ( props ) => { return ( < p > { props.children } < /p > ) } いかがでしょうか。非常に簡単な例ではありますが素直にAtomic Designに落とし込むことができ、コンポーネントは再利用できそうなイメージができたと思います。 非同期処理 次に非同期処理です。 React、Reduxでは、非同期処理の実装の仕方も様々な方法があります。 前述の通り、今回の開発はRedux middlewareを使用しません。 redux-thunk や redux-saga といった、メジャーなRedux middlewareは使用せずReactの独自フックを用いて非同期処理を実装しています。 髪型の一覧を表示する例で独自フックを用いて非同期処理を行う実装をすると以下のようになります。 // organism/Hairstyles.tsx const Hairstyles: React.FC < Props > = ( props ) => { // 独自フック const [ loading , error ] = useGetHairstyles () const hairstyleEntities = useSelector (( state: any ) => state.hairstyle.hairstyles ) if ( error ) { return < Error / > } if ( loading ) { return < Loading / > } const hairstyles = hairstyleEntities.map (( hairstyleEntity: HairstyleEntity ) => < Hairstyle key = { hairstyleEntity. id } hairstyle = { hairstyleEntity } / > ) return ( < div class= "hairstyles" > { hairstyles } < /div > ) } // hooks/useGetHairstyles.ts export const useGetHairstyles = () => { const dispatch = useDispatch () const [ loading , setLoading ] = useState ( false ) const [ error , setError ] = useState < string | null >( null ) useEffect (() => { const getHairstyles = async () => { setLoading ( true ) // 非同期処理 const [ hairstyles , err ] = await HairstyleRepository.getHairstyles () if ( err ) { setError ( err.message ) } else { setError ( null ) dispatch ( showHairstyles ( hairstyles )) } setLoading ( false ) } getHairstyles () } , [ dispatch ] ) return [ loading , error ] } useGetHairstyles が独自フックとなります。 Reactの useEffect を用いてコンポーネントのマウント時に非同期処理を実行します。 独自フックから非同期処理の状態を呼び出し元のコンポーネントに返すことで、非同期処理の状態に応じたコンポーネントの出し分けを実装しています。 非同期処理のためのAPIクライアントはRepositoryパターンを用いて設計しているのと、エラーハンドリングにも工夫をしているため、ここについて詳しく説明します。 Repositoryパターン Reactにおいて、APIリクエストをする場合は axios などのライブラリを使用することが多いと思います。 この時、コンポーネントからデータソースへのアクセスロジックを切り離し隠蔽した上で、コンポーネントからこれらの実装を意識しなくても良い設計とすべきです。 このような設計において有効なデザインパターンがRepositoryパターンです。 実際に、Repositoryパターンを使用して、APIクライアントを実装してみます。 まずは、axiosを用いてAPIリクエストのコネクションを張る処理を共通化するクラスを作成します。 APIリクエストの設定値に関する処理はこのクラスに集約します。 // models/repository.ts export class Repository { private static connection () : AxiosInstance { return axios.create ( { baseURL: 'https://example.com' , timeout: 500 , headers: { 'Content-Type' : 'application/json' } } ) } public static async get < T >( path: string , config?: AxiosRequestConfig ) : Promise < T > { const connection = this .connection () const response: AxiosResponse = await connection.get < T >( path , config ) return response.data } } 次に、上記のクラスを用いて得たAPIのレスポンスデータを、それぞれのエンティティに落とし込むクラスを用意します。 以下は、髪型のリストデータをAPIで得てエンティティに落とし込む例です。 // models/hairstyle/repository.ts export const HairstyleRepository: IRepository = { async getHairstyles () : Promise < HairstyleEntities > { const hairstyles = await Repository.get < HairstylesSchema >( '/v1/hairstyles' ) const hairstyleEntities = hairstyles.map (( hairstyle: HairstyleSchema ) => new HairstyleEntity ( hairstyle ) ) return hairstyleEntities } } Repository.get する際にGenericsでレスポンスデータの型を指定することで、レスポンスデータがJSONであってもきちんと型チェックを行います。 // models/hairstyle/schema.ts export type HairstylesSchema = { hairstyles: HairstyleSchema [] } export type HairstyleSchema = { id: string name: string image_url: string } そして、エンティティは以下のようになります。 // models/hairstyle/entity.ts export class HairstyleEntity { id: string name: string imageUrl: string constructor( args: HairstyleSchema ) { this . id = args. id this .name = args.name this .imageUrl = args.image_url } } これで HairstyleRepository.getHairstyles() によって、コンポーネントからデータソースへのアクセスロジックを意識せずに、APIによるデータの取得ができるようになりました。 ただ、この実装はレスポンスのエラーハンドリングができていません。 次は、このエラーハンドリングについて見ていきます。 エラーハンドリング エラーハンドリングにおいても、APIリクエストの実装者からはシンプルになるように設計を行います。 エラーを扱う時に実装者からはHTTPであることや、サーバエラーなのか、クライアントエラーなのか、といったことは意識しなくても良いことを目指しました。 実際の実装を見ていきます。 axios.get はレスポンスエラーの場合は例外を投げるため、 axios.get の例外をcatchしエラー用のオブジェクトでくるんで Repository.get の2つ目の返り値で返すようにします。 // models/repository.ts export class Repository { public static async get < T >( path: string , config?: AxiosRequestConfig ) : Promise < [ T , null ] | [ null , RpcError ] > { const connection = this .connection () try { const response: AxiosResponse = await connection.get < T >( path , config ) return [ response.data , null ] } catch ( error ) { if ( error.response ) { return [ null , RpcError.buildFromHttpResponse ( error.code , error.response.data.display_message ) ] } else { return [ null , RpcError.buildCancelled () ] } } } } この時気をつけるべきことは error.response の中身です。 axiosはHTTPで表現できるサーバエラーの場合は、catchした error.response でステータスコードなどを取得できます。 ただし、タイムアウトなどのクライアントエラーは error.response がnullとなります。 この error.response がnullの場合も、きちんとオブジェクトで表現して実装者は意識しないようにしたいです。 よって、このサーバエラーとクライアントエラーを1つのオブジェクトで表現するために、RPCのようにエラーを表現してみました。 gRPCのエラーコードを参考に、HTTPのステータスコードをgRPCのエラーコードに置き換え、クライアントエラーはgRPCでいうところのCANCELLEDとして扱うようにしました。 参考 : https://github.com/googleapis/googleapis/blob/2433bd50656264a2ef9f684bf646fb4d250d39ff/google/rpc/code.proto 以下が実際のコードです。HTTPとgRPCの対応は今回のサーバが返すステータスコードのみ表現しています。 // models/rpcError.ts enum RpcCode { OK , CANCELLED , UNKNOWN , INVALID_ARGUMENT , DEADLINE_EXCEEDED , NOT_FOUND , ALREADY_EXISTS , PERMISSION_DENIED , RESOURCE_EXHAUSTED , FAILED_PRECONDITION , ABORTED , OUT_OF_RANGE , UNIMPLEMENTED , INTERNAL , UNAVAILABLE , DATA_LOSS , UNAUTHENTICATED , } export class RpcError { constructor(public code: RpcCode , public message: string ) { } static buildCancelled ( message?: string ) { return new RpcError ( RpcCode.CANCELLED , message || '予期せぬエラーが発生しました。時間を置いてもう一度お試しください。' ) } static buildFromHttpResponse (status : number , message: string ) { let code: RpcCode switch (status) { case 500 : code = RpcCode.UNKNOWN break case 400 : code = RpcCode.INVALID_ARGUMENT break case 504 : code = RpcCode.DEADLINE_EXCEEDED break case 404 : code = RpcCode.NOT_FOUND break case 409 : code = RpcCode.ALREADY_EXISTS break case 403 : code = RpcCode.PERMISSION_DENIED break case 401 : code = RpcCode.UNAUTHENTICATED break case 429 : code = RpcCode.RESOURCE_EXHAUSTED break case 503 : code = RpcCode.UNAVAILABLE break default : code = RpcCode.UNKNOWN } return new RpcError ( code , message ) } } これによって、エラーを扱う実装者からはエラーコードとエラーメッセージだけを意識すれば良いようになりました。 以上がSPAの設計で注力した点です。 これらによって、どこにコードを書けばいいのか、どのようにコンポーネントを組み上げればいいのか、非同期処理はどう実装すべきか、といった悩みが減りSPA未経験者もスムーズに開発に迎え入れることができたと思います。 おわりに 髪型別コーデ検索におけるSPA開発の技術選定や設計について紹介しました。 SPAに興味がある皆さんの参考になれば幸いです。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。 tech.zozo.com
こんにちは、ZOZOテクノロジーズ CTO室の池田( @ikenyal )です。 ZOZOテクノロジーズでは、 技術書典 応援祭 にて、有志で制作した技術同人誌【 ZOZO TECH BOOK VOL.1 】の頒布を行いました。現在も引き続き BOOTHにて頒布中です 。 zozotechnologies.booth.pm それに伴い、4/28と4/30の二日間、頒布をしている ZOZO TECH BOOK VOL.1 の解説会をオンラインで実施しましたので、そのレポートをお届けします。また、今回のイベントが弊社でも在宅勤務状態での初のオンラインイベントだったので、その配信の裏側も少しだけお伝えします。 zozotech-inc.connpass.com zozotech-inc.connpass.com 【オンライン】#技術書典 頒布本「ZOZO TECH BOOK」解説会 Vol.1 まとめ Vol.1は4/28に実施し、以下の章の解説を行いました。 zozotech-inc.connpass.com 第1章 ZOZOテクノロジーズの2019年の振り返りと現状 (今村 雅幸 / @kyuns & 池田 健人 / @ikenyal ) 第3章 速習GitHub Actions 〜 明日からの充実GitHub自動化ライフのための凝縮ポイント 〜 (川崎 庸市 / @yokawasa ) 第7章 はじめての本番デプロイ (光野 達朗 / @kotatsu360 ) 【オンライン】#技術書典 頒布本「ZOZO TECH BOOK」解説会 Vol.2 まとめ Vol.2は4/30に実施し、以下の章の解説を行いました。 zozotech-inc.connpass.com 第4章 Miro SDK入門 (堀江 亮介 / @Horie1024 ) 第5章 iOSアプリのクラッシュレポート、もう少し詳しく! (元 政燮) 休憩トーク〜ZOZO TECH BOOK表紙デザインの裏話〜 (MOZZY) 第6章 GoのCLIツールで服作りの業務効率化 (手塚 ⻯太 / @tzone99 ) 第2章 WebXRの現状確認 2020 Spring (諸星 一行 / @ikkou ) オンライン配信の試み 現在、ZOZOテクノロジーズでは在宅勤務を行っております。そのため、今回のイベントも登壇者含め全員が自宅より配信を行いました。弊社でも在宅勤務状態での初のオンラインイベントだったので、その配信の裏側も少しだけお伝えします。 ツールの選定 配信に使えるツールは数多く提供されています。その中で、今回の配信ではWebex EventsとYouTube Liveの併用を採用しました。なお、ZOZOテクノロジーズではWebexを標準ツールとしています。 techblog.zozo.com ツール選定時の流れを一部紹介します。 最初の大きな選択として、参加者による音声での質問など双方向性を重視するかどうかです。 この部分を重視する場合、 Zoomビデオウェビナー や Webex Events に会話可能な状態で参加してもらうことになるでしょう。参加者が不特定多数な場合は、発言者のコントロールを気をつけないと収拾がつかなくなる可能性もあるので注意が必要です。 今回はこの部分は重視しないため、次のステップとしてYouTube Liveの検討をしました。会社としてもオンラインイベントは初の試みであり、少しでも多くの人に届けたい思いがあります。社内で標準化しているWebexでは、ツールに馴染みのない方々もいらっしゃるでしょう。YouTubeであれば、多くの人が馴染みがあると考えたためYouTube Liveを使った配信をすることにしました。配信後にアーカイブとしてそのままYouTubeに公開もできます。 YouTube Liveで配信する際に、画面レイアウトを凝るかどうかを考えます。ZoomビデオウェビナーやWebex Eventsの標準の見え方で問題なければそのままツールを利用で良いでしょう。しかし、レイアウトを凝りたい場合には任意のテレカンツールをOBS(Open Broadcaster Software)使ってイベント用の画面を作って配信すると良いでしょう。今回のイベントではこの方式を選択しました。 このようなフレームを追加してOBSで配信を行いました。OBSはWindowsやMacで動作するOSSですが、最低限、GPUレンダリング可能な WindowsのPCを推奨します。 配信時に使う画像アセットは画像解像度 1920x1080(16:9比率)にて、以下のものをあらかじめ用意しておきます。 YouTube Live 配信開始前/配信後に表示する画像 登壇者の切り替え時などに流す幕間の画像 登壇者ごとの発表タイトルなどが含まれたテンプレート画像 画面の切り替えや登壇者の話し始めるタイミングなど、リハーサルで関係者全員が流れを確認しておく必要があるので、事前確認は必ず行いましょう。 最後に ZOZOテクノロジーズでは、プロダクト開発以外にも、今回のような技術書典への参加やイベントの開催など、外部への発信も積極的に取り組んでいます。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
はじめに BtoB開発部の増田です。 2020年4月1日より、株式会社アラタナからZOZOグループへの吸収統合を経て、ZOZOテクノロジーズ/BtoB開発部として新たなキャリアをスタートすることになりました。これまで同様、九州・宮崎にオフィスを構えており、現在約30名のエンジニアで開発を行っています。ZOZOグループの成長に貢献できるような拠点拡大を目指していきますので、よろしくお願いします! 今回、アラタナ時代にたくさんのブランドさまとともに歩んできたプロジェクト経験をもとに、プロジェクト管理の要点をまとめてみました。大規模なプロジェクトを管理していくためのヒントになれば幸いです。 プロジェクトの特徴 BtoB事業について ZOZOグループでは、メイン事業となるZOZOTOWNのサービス開発に加えて、ZOZOTOWN出店ブランドさまの自社ECシステムの開発支援、運用支援を行うBtoB事業を展開してきました。現在では、ZOZOTOWNと自社ECとで在庫を一元管理するための物流支援サービス 「Fulfillment by ZOZO」 を中心としたサービス開発を通して、ブランドさまの支援を行っています。 BtoB事業の大規模プロジェクト BtoB事業では、大小さまざまなプロジェクトを推し進めてきました。特に下記のような大規模プロジェクトは管理や進行の難易度が高くなります。ですが難易度が高い分、多くの気付きや知見、経験を得ることができます。今回は、BtoB事業で直面した下記のような大規模プロジェクトに焦点を当てて、特に気をつけているポイントをまとめてみます。 期間が1年以上の長期間に及ぶ 社内の複数部署だけでなく社外の多数のベンダーとも連携する システム改修の影響がブランドさまの特定部署だけでなく全体横断的に影響がある プロジェクト管理の要点 要点(1)全体像の整理 プロジェクト初期の段階で全体像をうまく整理できるかどうかは、その後の進行に大きく影響します。 特に、下記の3点は、プロジェクトの全体地図に相当するものです。プロジェクトの全体イメージを関係者全体に共有できるように、簡潔な形で可視化しておきます。プロジェクトに関与していないメンバーでも概要を把握できるような、わかりやすい説明資料を目指して作成します。 目的、納期、予算、要件一覧などをまとめた「プロジェクト定義書」 関係するシステム要素の相関関係を網羅的にまとめた「システム俯瞰図」 関連各社、各部門のキーパーソンをまとめた「プロジェクト体制表」 プロジェクト定義書 プロジェクトの目的をはじめとして、納期、予算や要件一覧などプロジェクトの大前提となることをまとめます。特にプロジェクトの目的は、進行過程でそもそも論が浮上したときに、原点に立ち戻るための指針です。プロジェクトを通して何を達成すべきかを正しく理解して、全体に共有しておく必要があります。 ✕✕✕ツールの導入 ◯年◯月までのECシステムリプレース のような表現だと目的の本質が伝わりません。 各セール企画の効果測定を高精度化し販促強化するために、✕✕✕ツールを導入しデータ分析の粒度を詳細化する 複雑化したEC運用を効率化するために、◯年◯月までにECシステムをリプレースし単一システムでのEC運用を可能にする のように具体的な目的を表現しておきます。 システム俯瞰図 プロジェクトに関連する登場人物(システム構成要素)を俯瞰的に配置します。 要素の洗い出しのためには、ブランドさまや関連ベンダー各社へのヒアリングを何度も重ねることになります。一見、プロジェクトとは無関係に思える構成要素が登場しても、ヒアリング過程で話題に上がったものは念のため俯瞰図には落とし込んでおきます。そうすることで、ヒアリングでの見落としを後の設計フェーズで発見しやすくなります。 俯瞰図を整理していく過程では、ブランドさまの運用の歴史的背景や将来設計を知ることができたり、ベンダーから提供されるサービスの仕様や開発スタイルを学ぶ機会があったりします。こうした社外との接点を多く持てるのは、BtoBプロジェクトの魅力のひとつです。 プロジェクト体制表 プロジェクトをともにする各社の担当者、キーパーソンを一覧化しておきます。 このとき、整理の区分を会社や組織の単位で分けてしまうと、体制表というよりも連絡表のような性質のものになってしまいます。互いに関連し合う担当者がわかりやすいように、プロジェクトを分割するテーマ単位で整理するようにします。そのうえで、各テーマの進捗管理や状況報告を担当する責任者を明確にしておきます。 要点(2)スケジュールの管理 プロジェクトの成否をもっとも左右するスケジュール管理。 唐突かつ抽象的に問われる「順調?」に対して、簡潔にズバッと回答できる状態を常に作っておきたいと、心から思います。BtoBの大規模プロジェクトでは、下記3つの粒度でスケジュールを管理しています。 長期スケジュールの全体像を月単位の粒度で俯瞰する「大日程表」 ブランドさま含む関連各社の計画を週単位の粒度で確認する「中日程表」 ブランドさま含む関連各社のタスクと進捗を1日単位で確認する「詳細日程表」 大日程表 プロジェクト全体のスケジュールを、月単位の粒度で表現したものです。 プロジェクトの初期段階で、各社のスケジュール感を可視化したり、開発スケジュールを詳細化するためのたたき台として利用します。細かなタスクの内容よりも、各月でプロジェクトはどんな作業をするフェーズになるのかを整理するための俯瞰資料になります。 中日程表 プロジェクトの関連各社の計画を、週単位の粒度で表現したものです。 各社のタスクをある程度細分化し、各社がどの時期にどんな作業をする計画になるかを可視化します。他社タスクとの関連や依存関係もここで表現しておき、着手に必要な先行タスクはどんなものがあるのかを把握できるようにします。 また、プロジェクトの進行フェーズに入ると、事前に予期できなかった課題が顕在化してきます。場合によっては計画の軌道修正が必要になることもあります。軌道修正によって後続の計画にどんな影響が発生するか、軌道修正の影響はリカバリー可能なものなのかを判断する際に、中日程表は有効です。 詳細日程表 プロジェクトの関連各社の計画を、もっとも細かい粒度に整理し1日単位の粒度で表現したものです。 進捗状況を実質的に評価するのは、詳細日程表(WBS/ガントチャート)です。各社の足並みを中日程表で俯瞰し、タスクレベルでの進捗が先行気味なのか遅延気味なのかをガントチャートで追跡します。 長期的なプロジェクトのタスクを事前にすべてリストアップしておくのは実質的には不可能です。ですが、中日程表に基づいて想定されるタスクを事前に細かく整理しておくことは、計画リスクの事前検知に有効です。各社と協調しながら、スケジュールを整理するとともに、各社動向の具体的な内容を詳細に理解していきます。 大規模プロジェクトでは、膨大なタスク量に圧倒される局面もあります。ですが、各社一体となって着実に実績を積み上げ目的を達成できた瞬間は、大きな達成感を実感できます。こうした日常では得難い達成感を多くの関係者と共有できるのも、BtoBプロジェクトの魅力のひとつです。 要点(3)プロジェクトチームのコミュニケーション 最後に、プロジェクトメンバーのコミュニケーションについてです。 大規模プロジェクトではミーティング参加者も多くなりやすいので、議論や合意形成が簡単に進まないこともあります。特に最近ではリモート形式でのミーティングが中心なったことで難易度が上がっており、対面で直接会話する以上にコミュニケーションでの想像力が要求されます。 いま伝えたことはちゃんと伝わったか いま伝えたことに何を感じているのか 質問の背景にはどんな意図や懸念があるのか いま議論になっている課題の本質はどんなことか など、相手の状況を想像しながら対話に配慮する必要があります。 定例的なミーティングでも事務的・義務的な場にするのではなく、人同士の大切なコミュニケーションの場と捉え、丁寧なコミュニケーションを関係者全員に対して心がけておくのが重要です。 感謝は最大限に伝える お願い事では必ず背景まで丁寧に伝える どんな質問にも真摯に答える そうしたことを積み重ねながら苦楽をともにしていくことで、プロジェクト終了後にも継続して交流を続けられるような協力関係を築くことができるのは、BtoBプロジェクトの魅力のひとつです。 まとめ 以上、BtoB事業の大規模プロジェクトにおける管理観点を簡単にまとめました。 全体像にしても、スケジュールにしても、全体概要から部分詳細化の流れでの情報整理を意識しています。開発を分担していただく関連各社が、システムの全体像や開発スケジュールの全体方針を把握した上で各社領域の開発を進めていけるような情報共有を行っています。 ZOZOテクノロジーズでは、BtoB事業の拡大に取り組んでいただけるエンジニアを絶賛募集中です。ブランドさまと近いプロジェクトに従事したい方、ブランドさまの課題や要望を一緒に考えながら開発に取り組みたい方、九州・宮崎でお仕事したい方など、ご興味ある方は こちら からぜひご応募ください! tech.zozo.com
組織拡大に伴うスケールアウトするTV会議需要をCisco Webexで構築したはなし こんにちは、ZOZOテクノロジーズ コーポレートエンジニアリング 伊藤琢巳です。 社内のTV会議システムをWebexにて構築しているのですが、昨今のリモートワークなどTV会議需要を検討されている方々に1つでも有益な情報を提供できたら幸いです。 課題 株式会社ZOZOは千葉市の海浜幕張に拠点を構えており、幕張周辺に住む社員が多く通勤時間も短く、開発チームはオフィスで直接コミュニケーションを取れる事が強味です。そして、株式会社ZOZOテクノロジーズの設立により拠点が幕張オフィスと青山オフィスに分かれ開発メンバーも分散しました。 青山オフィス竣工時には既に拠点間会議を行う準備としてTV会議機器を導入した会議室が整備されていました。幕張オフィスにはCisco TelePresence TX9000、青山オフィスにはCisco TelePresence SX20を導入しました。 その後、福岡にも福岡研究所として新しい拠点が稼働しはじめ、以下の課題に直面しました。そのため、スケールアウト可能な遠隔コミュニケーション構築が急務でした。 PCで行うTV会議ではカメラ・マイクの性能不足のため、機微なコミュニケーションが伝わりにくい機器の品質課題 デスクトップPCの比率が高く会議時にPCを会議に持ち込めないメンバーも存在していた チーム数が多く、会議室が不足して利用できないという場所の課題 TV会議システムを導入している会議室と導入していない会議室の2種類あるが、会議室の空き状況的にTV会議が不要な会議でもシステム導入済みの会議室を利用せざるを得ず、TV会議システムの稼働率が低かった 開発現場が利用しているTV会議システムの機器と全社で導入しているTV会議システムの機器の相互接続性の課題 利用しているプロトコルの違いから接続不可の状態だった 福岡研究所をはじめ日帰りで往復移動できない距離の拠点の増加によるTV会議増加の課題 拠点間でのコミュニケーションが増加し、TV会議需要が増加した 解決過程 以下では、上記の課題を踏まえ、どのように解決に向けて進めていったのかを説明します。 課題の解決法検討 先述の課題に関して、それぞれの解決方法を検討していきます。 品質の課題解決 それまでは、PC会議としてSkype for BusinessやZoomを利用していました。どころがデスクトップPCの利用者比率の高さや、ノートPCの利用者でも内蔵カメラ・マイクの品質では力不足です。 これらの課題を解決するためにCiscoの導入を検討しました。Ciscoでは参加人数に合わせた製品がそれぞれ用意されていたため、会議室スペースのサイズや用途に合わせて選択が可能でした。 場所の課題解決 各拠点に設置するCisco TelePresence端末(以後、TV会議機器)を適切な台数追加しました。現在、65台のTV会議機器が稼働中です。 相互接続性の課題解決 相互接続性の課題解決に向けて、大きく2つの方向性を検討しました。 まず、社内で利用が浸透し始めていたZoomでの構築を検討しました。これには社内外接続の際にバーチャルルームコネクタ構築が必要です。また、拠点の増加を踏まえた運用やスケールアウト面で断念しました。 次に、ポリコムやソニーなどその他のTV会議システムの検討をしました。先行導入していた高価なTV会議機器を活かす必要性がありました。相互互換性のため業界標準のH.323/SIP接続は必須で検討しました。 TV会議増加による管理の課題解決 拠点増によりTV会議を構築する初期設定から障害調査などの運用の際の考慮はが必要です。また、拠点増により1つの会議への同時接続数が増えるため、多地点接続オプションはも必須。そのため、端末を増やすにあたり検討した内容は以下の点があげられます。 TV会議機器は多地点接続を行うには上位グレードの高価な機器が必要となる 多地点オプション導入しても同時接続数は数接続程度 TV会議機器の管理には専用のサーバ群とネットワーク構築の維持管理が必要となる 社内外との相互接続を行うためのExpressway設置や接続ライセンス管理が必要 広帯域かつ低遅延ネットワークの構築と維持が必要 Webexの選択 検討の結果、Webexを選択しました。Webexにてどのように課題解決したのか説明します。 管理コストの削減 多地点対応検討した時の図 オンプレミス クラウド 上で示す図のように、オンプレミス環境で構築した場合TV会議機器のみではなく、相互接続するためのサーバ群の構築が必要となります。 そこへクラウド環境を導入することにより、TV会議機器はクラウドへ直接接続になり、ネットワークへ接続してアクティベーションコードの入力だけで機器の接続が完了できるようになります。 クラウド環境への移行でサーバ保守が不要により、サービス提供に注力できます。 外部ともTV会議可能な環境整備 登録された端末は外部公開されるSIPアドレス割当により、内線的な組織内接続のみでなく外部との接続も行うことが可能。グループ各社Webexを導入することでお互いの会議室SIPを登録しておくことで会議室を直接呼び出し可能。グループで契約統合した際にも各社Webexを採用していたため移行はとてもスムーズに完了。 クラウド会議室はTV会議機器のみではなくPC・モバイルアプリ、ブラウザからのアクセスも可能なため専用機器を持っていない外部のゲストの方とも会議可能です。 Zoomとの相互接続 H.323/SIPルームコネクタオプションを利用してWebexと相互接続性を維持。 Google hangout meetとの相互接続 メディア変換サービスであるPexip構築の試算。Google hangout meetとの相互での同時接続を増やすとミスマッチになり断念。 PC会議として利用にとどめ、品質対策として数名で会議を行いたい場合にWebカメラの貸し出しや会議マイクスピーカの貸し出しを実施。また、Cisco Room Kit MiniをUSB接続することでPC会議でも品質を高めるこ方法を準備しています。 どこでもTV会議可能な環境整備 クラウド会議の開催はどのTV会議端末、アプリ、ブラウザからでも主催をすることが可能。オンプレミスでは必要だった高価な多地点オプションが不要。 用途に適した機器の選定 狭い会議室、広い会議室、オープンスペースと用途に合わせた機器選定を行う場合には、多地点オプションのために上位機種を選択は不要になり機器選択の自由度向上。そのため多くの会議室へ設置が可能となり、会議室利用の均等化を促進。 導入効果 接続拠点 ※3カ国/11拠点/65台(2020/05現在) 国内 海外 幕張・青山間であっても日に2往復は現実的でない距離。福岡や宮崎に至ると往復は現実的でなく、日程調整は高コストになりました。 会議室間の高品位なCiscoの相互接続は拠点間の移動頻度を削減。 他拠点間での会議参加があっても、他メンバーとのコミュニケーションが可能です。 これは実際の移動による交通費だけでなく移動時間の削減による無理のない業務が可能。別拠点からでもコミュニケーションコストが下がることで心理的な安全性を確保。 設置したTV会議機器の紹介 DX80 Cisco Webex DX80を可搬式スタンド化しています。主にオフィス内で発生する必要なときに短時間で行うミーティングを拠点間でも実現させる目的で設置しており、予約制にはせずミーティングスペースへ数台設置しています。 可搬式スタンド化することで誰でも容易に移動が可能です。 狭いスペースでも利用が可能なため好評であり、会議室に設置済DX80もすべてこの仕様へ変更しました。 Room Kit Mini 参加メンバーが増えてくると特に問題になるのはマイクの性能不足です。前述のDX80は個人デスクで利用するサブモニタ的な扱いの製品でしたが、こちらのRoom Kit Miniはカメラ・マイク・スピーカをハドル利用できるように強化しています。 こちらも自立式の構成なため、容易に設置場所の変更可能です。 さらに、PCへUSB接続すると会議時にカメラ・マイク・スピーカーとして利用可能です。そのため、Google Meetなどのその他のPC会議システムでの利用もできます。 Room 55 主に参加メンバーが10人を超える会議での利用を想定した機器です。各拠点に可搬できるスタンドタイプのRoom 55を設定しています。 しかし、高価なため多くの台数の設定は現実的でないため、DX80やRoom Kit Miniで補完しています。 円会議室(Webex Codec Pro + SpeakerTrack 60 + Ceiling Microphone) 青山オフィスには広い円会議室が設定されています。 通常よりも広く、円形の会議室のためカスタマイズしてTV会議システムを構築しました。 カバー範囲の広いSpeakerTracker 60をスタンド化して設置、Ceiling Microphoneを2台設置して全体で良好な集音を実現させています。 推奨ソフトウェアの指定と利用方法の提示 社内ではWebex Teams利用を推奨。メンバーを事前にチーム招待、トピック毎にスペースを作成する事で場をつくり、ハドル的コミュニケーションに優れセキュリティと利便性のバランスを取ることが可能。 トピックに合わせたスペースを作成できるので、用途ごとのクラウド会議室が必要なタイミングで時間や場所にとらわれなく利用できます。チャットやホワイトボード、必要な資料スペースも完備いています。 外部のゲストを一時的にゲストとして招待し、ブラウザアクセスで利用可能です。メンバー以外の接続は受付処理が必要なため意図しない乱入を防止できます。 オフィスにいる場合、近くにあるCiscoのTV会議機器での資料共有が超音波を利用して容易に接続可能です。超音波で認証が完了するため、資料を共有する際のコネクタ不一致問題やケーブル長さ不足から解消されます。また、来客者など異なるネットワークを利用している場合でもクラウド経由で資料共有が可能になります。 Webex Eventsによる大規模会議やイベントの実施 Webex MeetingやWebex Eventsを利用。発言者・登壇者を固定、他メンバーのミュートや表情も確認し易いグリッド表示はじめ大規模開催に特化。 全グループ社員参加の社内イベント配信インフラとしてWebex Eventsを利用。YouTube Liveに比べると即時性に重点をおいているため動画品質は劣るが、低遅延による双方向性の確保。またURL認証でなくユーザ認証なので社外秘の情報でも安心して配信可能。 その他の導入時に行った施策 青山・幕張拠点を常時接続 会議室を予約して時間をそろえて会議、フリースペースで遠隔地と会議、と順に環境を整備してきました。その次のフェーズとして拠点間を常時接続を行い、他拠点のオフィスの様子が常に分かるようにしています。 場と場をつないでおくことで開発チーム内のコミュニケーション効率が向上し、お互いが別拠点にいても意識しあえる環境を構築しています。 要望のあるチームから順に導入しえおり、今後も徐々に追加予定です。 Azure ADとのSSO・プロビジョニング対応 社外秘情報でもやり取りできる環境を構築したことでコミュニケーションのコスト削減。次は大規模になってきたアカウンティング。プロビジョニングにてユーザアカウント作成の自動化、ログインのSSO化を整備。 社員がそれぞれ持っている会社のアカウントだけで、いつでもコミュニケーション可能となり、退職すると自動アカウント停止となるため運用コストが低減できます。 ログイン認証をAzure ADで一元管理をしているため、不自然なログイン検知の自動化もメリットになります。 海外拠点への導入 海外拠点があるため、そこへの導入も行いました。 機器の購入に関しては、Cisco TV会議機器を購入・保守契約は各国リセラーと契約しました。当初グループ会社ごとに機器の購入とクラウド契約を行ってSIPアドレスを共有していましたが、 現在ではクラウド契約を一本化し、機器も再アクティベーションで再利用しています。 また、弊社には中国の拠点もありますが、中国の場合はグレートファイアウォールに注意が必要です。北京接続用(暗号オプション無効)では接続できないので、現在はシンガポール接続用(暗号化オプション有効)を利用しています。事前にWebex各サーバとのネットワーク接続経路テストをするサイト Cisco Webex Network Test で確認をしましょう。 Googleカレンダーとの連携 社内で利用しているGoogleカレンダーのカレンダーリソースとの連携を強化しています。 時間になるとWebex会議への参加ボタンが自動表示されるのでワンタップ参加。次の予約時間前になると次の予約が表示されるため会議室利用の効率化。 どこでもコミュニケーションへの変化 それはZOZOテクノロジーズ全社員利用の開始直後に発生しました。 令和元年9月直撃した台風15号。大型台風の接近際して試験的開始していたリモートワーク対策済PCを持ち帰るアナウンス。近距離での通勤が多い幕張拠点ですがそれでも通勤は混乱。チームいつでもTV会議を行える環境、リモートワーク対応もすすめていたため無理して出社せず自宅からの業務が可能でした。 その後、グループ再編や東京オリンピックに向けてグループでの導入を進めていたところ、新型コロナウィルス感染症による全社規模のリモートワークが発生しました。拠点間をつなぐ目的で始めた環境整備でしたが、社員間をつなぐ役割へと変化してきました。 オフィス閉鎖やリモートワーク化により、利用形態が変化してきても柔軟に対応でき、全社導入した効果を実感しています。 同時に、接続元や業務形態の変化はコミュニケーションの仕方にも変化を与えていると感じます。 タスクについては以前よりJiraでのチケット管理をしていましたが、業務上近くにいるメンバーとの声かけはオフィスではいつでも出来ていたので、チームの進捗は週次で確認していました。しかし、リモートワークになってからはチーム内対策ポータルを作成し、朝礼でチーム内の作業を日次で確認するよう変更しています。 「この後ちょっといいですか?」や「誰が出席したら良いかな?」という話題を朝礼の時に投げかけ、チーム内の共有を促進しています。 また、ヘッドセットの準備を行い相手に聞きやすい環境を意識しています。ハウリングなどで聞き取りにくい、聞き取れないということが続くとストレスの原因になります。自分自身では気がつきにくい問題なので、相互に指摘するようアナウンスしています。 私も部屋に風鈴を下げていたのですが、「何か音がする」と言われはじめ、TV会議の向こう側へも風鈴の音は流れているという事に気がつきました。今では毎朝仕事を開始する前に風鈴を外して、仕事が終わったら風鈴を戻すのが習慣となっています。 最後に コーポレートエンジニアリング部では各サービスで得手不得手も違うので、相互補間を考慮してWebex以外のサービスも整備しています。 どこかのサービスが落ちた場合、不得手な環境であっても業務を継続出来る環境構築を目指しています。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
こんにちは!最近気になるニュースはスピノサウルスの尻尾の化石が発見されたこと 1 な、SRE部エンジニアの塩崎です。ZOZOテクノロジーズの前身となった会社の1つであるVASILYでは数多くのクローラーの開発・運用の担当をしてきました。 今回はその知見を生かして、クローラーを楽に運用するためのクラウドサービスを紹介します。 概要 データ解析を円滑に進めるためには、CSVやWeb APIなどの構造化されたデータが必要です。しかし全てのWebサイトにあるデータが構造化データを提供しているとは限りません。むしろ提供していないケースの方がはるかに多いです。そのため、Webクローラーを作成して構造化されていないWebページを解析し、構造化データを生成する必要があります。 しかし、Webクローラーの運用には数多くの「つらみ」があります。特に大量のWebページを1日1回などの頻度で定期的にクロールする際には「つらみ」が多くなります。少しでもこの「つらみ」を減らし、運用を楽にするためのクラウドサービスを紹介します。 クローラーの運用の「つらみ」とは クローラー運用でどのような「つらみ」があるのかを説明します。なお、ここで想定しているクローラーはクロール対象のサイト数が数十以上、ページ数が数百万以上、クロール頻度が毎日というかなり大規模なものです。 インフラの運用 EC2やGCEなどを使いクローラーを動作させるためのインフラを自分たちで管理するのは大変手間がかかります。大規模クローラーを運用する場合は1台のサーバーだけで処理が完結することはありません。多くの場合では非同期ジョブキューを活用した分散システムが必要になります。以前、このような仕組みを作った際には、Mesos + Marathonを使ったインフラを数人月の開発工数で構築しました。頑張って仕組みを作ってみたものの、運用をするために分散システムの知見が必要になり、運用者に求められるレベルが上がるという問題も同時にありました。 techblog.zozo.com サイトの構造が変わることへの対応 クローラーで最も運用負荷がかかるのは、サイト構造の変化への対応です。今日ではWebサイトのリリースは毎週・毎日行うことが当たり前のようになっています。そのため、あるタイミングで作成した要素をパースするための設定がある日突然使えなくなってしまう可能性があります。 Ajaxを使っているサイトの対応 サーバーからダウンロードしたHTML中に欲しい要素がない場合は、要素をパースするのが非常に面倒になります。Vue.jsやReactなどのSPAフレームワークで作られたページについては言うまでもないですが、意外と一部の項目のみがAjaxで取得されているケースも多いです。例えばECサイトの在庫状況のような頻繁に変更されうる情報をユーザーに提供するため、その部分のみをAjaxで取得することがあります。そのようなサイトをクロールするためにはChrome DevToolsなどを使いAjax通信を解析して、同等なAPI呼び出しを行う必要があります。 BANとの戦い 一部のWebサイトはWebクローラーからのアクセスを自動的に遮断するための対策を実装しています。多くの場合はIPアドレスを使い、一定の期間で閾値以上の回数のアクセスがあったらそれを遮断するという処理が行われています。もちろんサイトの運営に支障が出るレベルのアクセスをしたり、利用規約で禁止されているようなクロール行為をするのは論外です。ですが、常識的な 2 アクセス頻度であったとしてもブロックされるケースがあります。そのようなサイトをクロールするためにはIPアドレスを複数個用意し、適切にローテーションを行う必要があります。 選定のポイント 上記で出た「つらみ」を元にクローラー運用を楽にするためのポイントを考えていきます。 インフラの抽象化 大規模クローラーは分散システムになりがちですが、分散システムの運用はモノリシックなシステムの運用に比べて難易度が上がります。そのようなシステムの運用に慣れた人員を集めることはとても困難です。そのため、この部分を「いい感じに」隠蔽し、アプリケーションエンジニアがアプリケーションレイヤーのみに集中できるようなPaaS、SaaSが必要です。 パース結果をGUIで確認する機能 XPathやCSSセレクターなどを使ったパース結果をブラウザなどの画面でグラフィカルに確認したいです。クロール対象サイトのHTML構造が変わった後には速やかにそれに追従する必要があります。そのため、ブラウザ画面をクリックしてパースする要素を指定できる必要があります。わざわざHTMLコードを調べるのに比べ、グラフィカルに指定できる方が効率的です。 JavaScriptの実行をしてくれるヘッドレスブラウザ Ajax通信の解析は骨の折れる作業です。個人的な感覚では、HTML中に直に埋め込まれている要素を取得するのと比較して、数十倍の手間がかかります。ですので、JavaScriptの実行を行ってくれるヘッドレスブラウザがあり、onloadなどのイベントに紐づく処理がある程度実行されたあとのHTMLを取得したいです。 IPアドレスのローテーション機能 BAN対策のために複数個のIPアドレスを使い回し、必要に応じてそれらを使い回しできるような機能が欲しいです。 ProxyMesh や Tor のようにこの機能を単体で提供するSaaSやOSSもありますが、クローラー本体を動かすためのクラウドサービスに統合されていると使い勝手が良いです。 ベンダーロックインしてない 古くからあるクローラー用のクラウドサービスとして kimono は有名でしたが、2016年にPlantirによって買収されたタイミングでサービスの提供が打ち切られました。このような事象が発生した場合、長期的な戦略としては別のクラウドサービスへのマイグレーションが必要です。ですが短期的な視点に立つと、完全なマイグレーションが完了するまでの間のつなぎとして、自分たちのインフラでクローラーを動かす必要が出てくるかもしれません。 techcrunch.com また、スモールスタートで始めたプロジェクトが大きく成長し、十分な運用体制が取れるようになることもあります。そのときに、ベンダーロックインをしていなければ、自分たちのインフラに載せ替えることによってコストメリットを得るというオプションを考えることができます。 このことから、ベンダーロックインをしていない、OSS製品をベースにしたクラウドサービスを利用することにはメリットがあります。しかし、発生頻度などを考慮すると、他の項目と比べた優先度は低いです。 要件を満たしそうなクラウドサービスの検討 Scrapinghub scrapinghub.com 最初に紹介するのは Scrapinghub です。Python製のWebクローラーとして有名なScrapyの開発元が運営しているPaaSです。自社が主導的に開発を行い、OSSとしても公開している Scrapy の実行基盤を提供しています。ScrapyそのものはPythonで書かれたOSSであり、自身のPCやEC2上に構築したLinuxサーバーの上で動かすこともできます。 Webページをパースするための処理はすべてPythonコードで実装します。XPathやCSSセレクターを使って要素を取得したあとに、Pythonコードで文字列処理を実装可能です。Pythonが持っているエコシステムをフル活用できる柔軟性が魅力的です。また、requirements.txt形式でPaaS環境にライブラリをインストール可能であり、それに加えて独自のDockerイメージの利用も可能です。そのため、 MeCab を使った形態素解析などのネイティブライブラリが必要な処理も実装できます。 一方、パース結果をGUIで確認する機能は搭載されていません。同じ開発元がビジュアルスクレイピングツールである Portia というOSSを作っていましたが、他のクラウドサービスに比べるとGUIツールとしての機能が不足しているように感じます。ブラウザベースで抽出したい要素を選択すると、Scrapyを使ったクローラーのソースコードをアウトプットする機能があります。しかし、出力できるクローラーの自由度が高くないため、あくまで補助的なツールとしての利用に限定され、メイン処理はPythonコードで書く必要があります。また、ここ1年くらいコミットが一切ないので、メンテナンスされていない疑惑があります。そのため、パース結果をGUIで確認する機能はScrapyのエコシステムの外のツールに頼る必要があります。以前に以下のブログで紹介した、 XPath Helper というGoogle Chromeの拡張機能を使うことで、目的の要素を抽出するためのXPathを高速に見つけることができます。 techblog.zozo.com また、JavaScriptの実行をするためのヘッドレスブラウザの機能も搭載されています。ただ単にページロード時のレンダリングをするだけでなく、Lua言語で書かれた関数を渡すことによって、フォームのクリックイベントなどを処理できます。クローラー用に軽量のヘッドレスブラウザを独自開発しているという気合の入れ方が凄いです。このモジュールも Splash というOSSとして公開されており、自前のインフラでホスティングすることも可能です。 IPアドレスのローテーション機能もついています。Scrapyとの統合も簡単で、ソースコードに数行の設定を追加するだけでIPアドレスのローテーションを行えます。接続元IPのロケーションを選ぶことができるので、海外IPからの大量アクセスによるアクセス遮断を防げます。 このように、基本的な機能は全てOSSとして公開されているので、これらの機能をEC2などのIaaS上で動作させることも可能です。自前のサーバーでホスティングするためには Scrapyd というツールを使いデーモン化します。しかし、ScrapydのWebコンソールは機能が不足しており、また複数台のサーバーをまとめ上げる機能もありません。そのため、Scrapydだけではクローラークラスターの運用には不十分です。クラスターの運用をするためには、サードパーティツールである、 ScrapydWeb を組み合わせる必要があります。このツールは複数台のサーバーにインストールされているScrapydの状態を一元して監視したり、ジョブをスケジュール実行する機能があります。 ParseHub parsehub.com 次に紹介するのは ParseHub です。先程紹介したScrapinghubとは違い、プログラミングレスでクローラーを作ることができるSaaSです。ParseHubのクライアントツールを立ち上げると、ブラウザのような画面が立ち上がり、その画面でグラフィカルに要素を選択できます。クリックで要素を選択できるだけでなく、XPathやCSSセレクターを使った要素の選択ができる柔軟性も併せ持っています。要素を選択した後は、正規表現やJavaScriptで変換処理を行うことができます。さらに、変数・条件分岐・ループ処理・サブルーチンなどの基本的なプログラミング言語としての機能も有しています。 ヘッドレスブラウザのサポートはScrapinghubよりも充実しています。フォームをクリックしてその結果を取得するという操作などもグラフィカルに行うことができます。Scrapinghubではこのような処理を実現するためにLuaの関数を書く必要があり、手間になりがちです。 IPローテーションの機能もあり、BAN対策をすることもできます。ただし、接続元のIPアドレスのロケーションを指定する機能はないので、そのようなことをしたい場合は Crawlera や ProxyMesh などの外部サービスを使う必要があります。 これらのツールはOSSとして公開されていないので、がっつりとベンダーロックインされる点には注意が必要です。 なお、ParseHubに似たビジュアルスクレイピングサービスとして Octoparse や Import.io があります。ビジュアルスクレイピングツールを求めている場合はこれらの検討もしてみると良いかもしれません。 Apify apify.com 最後に紹介するのは Apify です。これはJavaScriptを使ってクローラーを作成できるPaaSです。ヘッドレスブラウザとしてGoogle ChromeとPuppeteerを使っています。 Apify SDK というOSSが公開されており、クローラーをローカル環境で動かすことも、Apifyが提供しているPaaSで動かすこともできます。 クローラーのマーケットプレイス の存在がApifyの面白い点です。ZOZOTOWNや楽天のような日本のWebサービスのクローラーはありませんが、GoogleやInstagramなどのグローバル展開しているサービスのクローラは数多くあります。クローラーのソースコードがGitHubで公開されているので、これらをforkして自分たちのニーズに合わせてカスタマイズすることもできます。 比較 ここで、今回紹介した3つのクラウドサービスが適しているケースをまとめます。 Scrapinghub Scrapinghubはプログラマー向けのサービスです。以下のケースではScrapinghubを使うのがいいでしょう。 PythonやJavaScriptなどの汎用プログラミング言語を日常的に使っている クローラーをコードベースで管理したい GitHubを用いたコードレビューやCI/CDパイプラインの仕組みを使いたい 機械学習や自然言語処理などのPythonが得意とする処理を組み込んだクローラーを作りたい 将来的に大規模化することが予想されており、自前のインフラで動かすための人員を確保できる予定がある ParseHub ParseHubはノンプログラマーでも扱えるような支援機能が豊富です。以下のケースでParseHubを使うとその良さが発揮されるでしょう。 ノンプログラマーでクローラーの開発・運用をする必要がある ページ内で複数回のクリックをしないと取得できない要素が多い ベンダーロックインをいとわない代わりに開発スピードを上げたい Apify 上記の2つと比較してApifyを積極的に選ぶ場面は多くありません。 あえて挙げるならば、以下のケースではApifyに優位性があります。 Apifyのマーケットプレイスにあるクローラーが自分たちの要件を満たしている クロール対象のサイトがGoogle Chrome固有の機能に強く依存している まとめ 我々のチームでは以上の検討をした結果、Scrapinghubを利用することにしました。クローラーの開発運用を行うメンバー全員がプログラマーであり、コードベースでの管理をする恩恵が大きいと判断したためです。GUIを使ったパース項目の確認機能が不十分ですが、XPath Helperを活用することによって不十分な点を埋め合わせることができます。 このようにZOZOテクノロジーズでは一度作ったシステムをそのままにせず、時代に合わせた最適なアーキテクチャを模索する営みを継続的に行っています。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com 古生物学:スピノサウルスは泳ぎが上手だった ↩ どの程度までなら常識的なのかは議論の対象になりがちですが、一例を挙げるなら 岡崎市立中央図書館事件 の判例で1秒間に1アクセス程度は常識的とみなされました。 ↩
こんにちは! ZOZOTOWN部の遠藤です。 iOS 13がリリースされて半年が経ちましたね。iOS 13といえばダークモード機能が注目を浴びましたが、それ以外にもたくさんの 新しい機能 が追加されました。 本記事では新しく追加されたフルページのスクリーンショットについて書いていきます。 はじめに ZOZOテクノロジーズではiOS技術のキャッチアップのために定期的に社内勉強会を行っています。その勉強会で話題に上がったフルページのスクリーンショットに興味を持ち、ZOZOTOWNではどのように使用できるかの調査をしてみました。 今回は調査して分かった内容をもとにフルページのスクリーンショットの機能と対応方法について紹介します。 少しでもフルページのスクリーンショット機能を実装する際のお役に立てれば幸いです。 フルページのスクリーンショットとは? フルページのスクリーンショットはiOS 13で追加されました。 フルページのスクリーンショット機能については、 iOS 13で利用できる新機能 の「システム体験」->「フルページマークアップ」に記載されています。 ウェブページ、iWorkの書類、Eメール、地図の全体をとらえたスクリーンショットを撮り、注釈を加えられます。 今まで、スクリーンショットは画面に写っている箇所しか撮れませんでしたが、iOS 13から画面外の要素もスクリーンショットで撮ることができるようになりました。 iOS 13のSafariでスクリーンショットを撮ると、このようにスクリーンショットのプレビューで「フルページ」というタブにページ全体のスクリーンショットが表示されます。 かつ、通常のスクリーンショットと同じくトリミングや文字を書くこともできます。 フルページのスクリーンショットに 対応するには? フルページのスクリーンショットについてはWWDC19の Introducing PencilKit で最後の5分ほど触れられています。 ユーザーがスクリーンショットを撮ると、iOS 13で追加された UIScreenshotService のdelegateが呼ばれます。 呼ばれたdelegateメソッドの返り値にPDFデータを渡すことで、フルページのスクリーンショットが表示されます。 スクリーンショットがPDFであることのメリット PDFは複数ページ持つことが可能です。iOS 13のフルページのスクリーンショットは単一ページのスクリーンショットも複数ページのスクリーンショットも対応しています。 単一ページ例: Safari 複数ページ例: Keynote フルページのスクリーンショット機能で提供するスクリーンショット Introducing PencilKit のセッションでフルページのスクリーンショットを採用している事例としてマップアプリが紹介されています。 マップアプリの通常のスクリーンショットではセミモーダルが表示されていますが、フルページのスクリーンショットでは非表示になっており、マップの情報をより多く見ることができます。 通常のスクリーンショット フルページのスクリーンショット Human Interface Guidelines にはスクリーンショットは画面に写っている内容を変えてはいけないと書かれていますが、フルページのスクリーンショットは必ずしもそうとは限らないようです。 どのようなスクリーンショットがフルページのスクリーンショットとして表示するのが推奨されているかは、Human Interface GuidelinesやUIScreenshotServiceのドキュメントなどに記載されていません。(2020/04/01時点) フルページのスクリーンショットに対応する この画面にフルページのスクリーンショットを対応します。 ここでのフルページのスクリーンショットの定義は、コンテンツが収まるほど長い仮想の端末で見たときの状態のスクリーンショットです。 フルページのスクリーンショットを撮る手法はたくさんあるかと思いますが、今回は2つの手法を試してみました。 手法1: windowの高さを変更して描画する windowの高さをコンテンツが収まるように更新してそのwindowを描画することで求めているスクリーンショットを撮ることができます。 import UIKit class ViewController : UIViewController , UIScreenshotServiceDelegate { override func viewDidAppear (_ animated : Bool ) { super .viewDidAppear(animated) view.window?.windowScene?.screenshotService?.delegate = self } override func viewWillDisappear (_ animated : Bool ) { view.window?.windowScene?.screenshotService?.delegate = nil } func screenshotService (_ screenshotService : UIScreenshotService , generatePDFRepresentationWithCompletion completionHandler : @escaping (Data?, Int, CGRect) -> Void ) { let contentHeight : CGFloat // コンテンツが収まる高さを計算する let renderer = UIGraphicsPDFRenderer(bounds : . init (origin : .zero, size : . init (width : view.frame.width , height : contentHeight ))) let data = renderer.pdfData { context in context.beginPage() let originalHeight = view.frame.height view.window?.frame.size.height = contentHeight view.window?.layer.render( in : context.cgContext ) view.window?.frame.size.height = originalHeight } UIGraphicsEndPDFContext() completionHandler(data as Data, 0 , .zero) } } 順番に説明します。 (1) screenshotServiceのdelegate設定 screenshotServiceのdelegateにselfを設定します。 override func viewDidAppear (_ animated : Bool ) { super .viewDidAppear(animated) view.window?.windowScene?.screenshotService?.delegate = self } (2) PDF作成の準備 UIGraphicsPDFRenderer を使用してPDFを作成します。 UIGraphicsPDFRendererの初期化でPDFを描画する領域のサイズを決めます。 Safariでは幅375ポイントのデバイスでフルページのスクリーンショットを撮ると、幅375ポイントのPDFが生成されました。今回は同じようにデバイス幅をそのまま使います。 func screenshotService (_ screenshotService : UIScreenshotService , generatePDFRepresentationWithCompletion completionHandler : @escaping (Data?, Int, CGRect) -> Void ) { let contentHeight : CGFloat // コンテンツが収まる高さを計算する let renderer = UIGraphicsPDFRenderer(bounds : . init (origin : .zero, size : . init (width : view.frame.width , height : contentHeight ))) let data = renderer.pdfData { context in context.beginPage() // viewの描画 } } (3) PDF作成 pdfData(actions:) のメソッドを使用し、actions内で描画するとPDFデータが取得できます。 描画をする際の注意点ですが、NavigationBarとTabBarも描画したい場合はviewのwindowを描画します。viewを描画してもviewの階層構造にないNavigationBarとTabBarは描画されないからです。 今回はNavigationBarとTabBarも描画したいので、viewではなくwindowを描画することにしました。 また、描画メソッドはCALayerの render(in:) を使用します。描画メソッドには drawHierarchy もありますが、windowの高さを高くしすぎると描画されないことがあるためです。 let data = renderer.pdfData { context in context.beginPage() let originalHeight = view.frame.height view.window?.frame.size.height = contentHeight view.window?.layer.render( in : context.cgContext ) view.window?.frame.size.height = originalHeight } UIGraphicsEndPDFContext() (4) completionHandlerにPDFを渡す 最後に作成したPDFを渡して完了です。 completionHandler はPDFの他に2つのパラメータがあります。この2つのパラメータは、スクリーンショットのプレビューで表示位置を指定するものです。 単一ページの場合はrectInCurrentPageを指定でき、複数ページの場合はindexOfCurrentPageのパラメータが指定できます。 rectInCurrentPageにCGRectZero、indexOfCurrentPagに0を指定するとPDFの一番上から表示されますが、ユーザーが表示していた位置を指定するのが良いでしょう。 completionHandler(data as Data, 0 , .zero) これでフルページのスクリーンショットを撮ることができます。 目的のスクリーンショットを得ることはできましたが、windowの高さを変更することは、レイアウト崩れや一度に全てのcellがロードされることで発生する負荷などの副作用が不安になります。 手法2: 1画面ずつスクロールして描画する windowの高さを変えずに、コンテンツを1画面ずつスクロールしながら描画します。 スクロールのたびにwindowを描画すると、NavigationBarとTabBarが繰り返し描画されてしまいます。 これを回避するために、スクロール時はスクロールのコンテンツのみを描画します。 先程紹介した(3)の描画処理を変更します。 ファーストビューはwindowを描画し、その後1画面ずつスクロールしてscrollViewを描画します。ここでの注意点はTabBarの描画です。 windowを描画することで、TabBarも描画され、スクロールコンテンツの途中にTabBarが表示されてしまいます。 なので、windowを描画する前にTabBarを非表示にし、スクロールコンテンツが全て描画されたあとにTabBarのみを描画します。 let data = renderer.pdfData { context in context.beginPage() let originalContentOffset = scrollView.contentOffset // 全てのコンテンツが表示されるのに必要なスクロール回数を計算する let numberOfScrolls = ceil(contentHeight / scrollView.frame.size.height) ( 0 ..< Int(numberOfScrolls)).forEach { if $0 == 0 { scrollView.contentOffset.y = 0 tabBar.isHidden = true view.window?.drawHierarchy( in : view.frame , afterScreenUpdates : true ) } else { let y = scrollView.frame.size.height * CGFloat( $0 ) scrollView.contentOffset.y = y scrollView.drawHierarchy( in : . init (origin : . init (x : 0 , y : y ), size : scrollView.frame.size ), afterScreenUpdates : true ) } } tabBar.isHidden = false // コンテンツの下に描画されるようにy座標を計算する var drawTabBarFrame = tabBar.frame drawTabBarFrame.origin.y = contentHeight - tabBar.frame.height tabBar.drawHierarchy( in : drawTabBarFrame , afterScreenUpdates : true ) scrollView.contentOffset = originalContentOffset } UIGraphicsEndPDFContext() これでwindowの高さを変更せずともフルページのスクリーンショットを撮ることができました。 しかし、この手法はwindowを伸ばす手法と異なるレイアウトの箇所が出てきます。 コンテンツが収まる高さで描画した スクリーンショット 1画面ずつスクロールして描画した スクリーンショット 本来右下のFloating Action Buttonの位置はTabBarのすぐ上にあるのですが、1画面ずつスクロールして描画したスクリーンショットではファーストビューと同じ位置で表示されています。ファーストビューでwindowを描画した際にTabBarは非表示にしましたが、Floating Action Buttonは非表示にしていないためです。 スクロールコンテンツの最後に表示する要素についてはTabBar同様にスクロールの最後に描画するなどの工夫が各画面で必要になると思われます。 2つの手法のメリット・デメリット 2つのフルページのスクリーンショットの手法を比較してみました。 メリット デメリット windowの高さを変更する手法 実装が簡単 windowの高さを変更することでの副作用 1画面ずつスクロールする手法 windowの高さを変更する手法と比べ副作用は低い 実装量が多い 実装量が多くなってしまいますが、1画面ずつ描画する手法の方が安全性は高いです。windowの高さを変える手法もリスクが許容できる画面であれば有効だと思います。 まとめ 今回はフルページのスクリーンショット機能と対応方法についての紹介でした。 フルページのスクリーンショットは、UIScreenshotServiceDelegateを設定しPDFを渡すことで対応できます。今回は長い端末で見たときの状態を目指しましたが、提供したい内容によっては複数ページや重要なところだけを抜き出したスクリーンショットを作ることも可能です。 この記事がフルページのスクリーンショットを実装の一助になれば幸いです。 ZOZOテクノロジーズでは、iOSエンジニアを募集しています。興味のある方はこちらからご応募ください! www.wantedly.com
こんにちは、ZOZOテクノロジーズ CTO室の池田( @ikenyal )です。 ZOZOテクノロジーズでは、現在開催中の 技術書典 応援祭 にて、有志で制作した技術同人誌【 ZOZO TECH BOOK VOL.1 】の頒布を開始しました。 本来であれば、 技術書典8 にゴールドスポンサーとして参加予定でしたが、 新型コロナウイルス感染症の影響により中止 になってしまいました。今回の 技術書典 応援祭は、その代わりとなるオンラインマーケット として実施されています。 ZOZO TECH BOOK VOL.1 頒布開始🎉 技術書典 応援祭にて、ZOZOテクノロジーズの有志によって制作された技術同人誌【ZOZO TECH BOOK VOL.1】の頒布を開始しました!詳細はリンクから💁‍♀️ #技術書典 #zozotech https://t.co/IVxfN9b0qv — 株式会社ZOZOテクノロジーズ (@zozotech) March 12, 2020 ZOZO TECH BOOK VOL.1 こちら のページにて電子版を頒布しています。 techbookfest.org 目次 第1章 ZOZOテクノロジーズの2019年の振り返りと現状(今村 雅幸 / @kyuns & 池田 健人 / @ikenyal) 第2章 WebXRの現状確認 2020 Spring(諸星 一行 / @ikkou) 第3章 速習GitHub Actions 〜 明日からの充実GitHub自動化ライフのための凝縮ポイント 〜(川崎 庸市 / @yokawasa) 第4章 Miro SDK入門(堀江 亮介 / @Horie1024) 第5章 iOSアプリのクラッシュレポート、もう少し詳しく!(元 政燮) 第6章 GoのCLIツールで服作りの業務効率化(手塚 ⻯太 / @tzone99) 第7章 はじめての本番デプロイ(光野 達朗 / @kotatsu360) サンプル 最後に ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
こんにちは。開発部基幹SREチームの廣瀬です。 弊社のサービスではDBMSとしてMicrosoft社のSQL Serverを使用している箇所があります。本記事では、2020年1月1日からスタートしたZOZOTOWN冬セールにおける負荷対策の一環で実施した、SQL ServerのCPUチューニングについてご紹介します。内容としては主に「どうやってプロダクション環境においてCPUボトルネックなクエリを見つけ出すか」についてです。 そのため、SQL Server以外のDBMSをお使いの方にも、「SQL Serverではこんな情報がとれるのか。MySQLだったら〇〇でとれる情報だな」というように比較しながら読んでいただけると嬉しいです。 チューニング実施に至った経緯 初めに、なぜCPUベースのチューニングを実施したかについてお話します。一言でいうと「ZOZOTOWNの冬セールで一部DBのCPUが負荷に耐えきれない試算となったから」です。ZOZOTOWNでは定期的に売上を多く記録するイベントが開催されています。その中でも毎年1月1日からスタートする冬セールは、1年間で最も売り上げが多いイベントの1つです。したがって、DBサーバーの負荷も高くなる傾向にあります。そのようなイベントを数カ月後に控えた昨年の10月ごろに開催されたイベントで、とあるDBのCPU負荷が高騰しました。このイベントは乗り切ったのですが、冬セールの方がより多くのトラフィックを見込んでいました。そのため、このままでは確実にCPUボトルネックによるスロークエリが多発すると判断し、チューニングを実施することにしました。なお、チューニング対象のSQL Serverはオンプレミスのため、スケールアップという手段は考えないこととします。 SQL Serverにおける情報収集の方法 SQL Serverのチューニングにおいて使用することが多い情報収集の方法を4種類説明します。 動的管理ビュー(DMV) サーバーの状態情報が格納されたVIEWのことを指し、沢山種類があります。リアルタイムで更新される情報のため、チューニングだけでなく、即応性が求められるトラブルシューティングでも活躍します。例えば、以下のDMVを使ったクエリを実行すると キャッシュされたクエリの総実行時間 総CPU時間 実行回数 実行プラン などを確認できます。 SELECT TOP 100 qt.text ,total_worker_time ,total_elapsed_time ,qs.execution_count ,qp.query_plan FROM sys.dm_exec_query_stats qs OUTER APPLY sys.dm_exec_sql_text(qs.sql_handle) AS qt OUTER APPLY sys.dm_exec_query_plan(plan_handle) AS qp 他にも、 インデックスの容量、使用回数(seek/scan/lookupごと)、最終アクセス日、断片化などの情報 現在実行中のクエリの抽出(獲得しているメモリサイズ、実行時間、待ち事象など) 現在のロック取得状況 など、豊富な種類の情報を取得することが可能です。各DMVのデータ保持期間は、「現在の状態のスナップショット」か「サーバー起動後の累積値」のいずれかに大別されます。 DMVを使った情報収集用のクエリを、 私のgithub でいくつか公開しています。 また、Microsoft MVPの 小澤さんのgithub で豊富なDMVクエリが公開されており、おすすめです。 拡張イベント 様々なイベントをキャッチできるトレースツール、といったイメージです。ドキュメントには「拡張イベントは、SQL Server に関する問題の監視とトラブルシューティングを行うために必要なデータを収集できるようにする、軽量なパフォーマンス監視システムです。」とあります。SQL Server 2008 R2までの主流だった SQL Server Profiler や SQL トレース が現在では非推奨となり、代わりに拡張イベントの使用が推奨されています。拡張イベントの方が、情報収集時の負荷がより軽量になったとされています。 設定はクエリベースでも、以下のようにGUIベースでも可能です。例として、下図では「実行完了に1秒以上かかったクエリ」をキャッチする設定を実施しています。 試しに5秒間waitするコマンドを実行した後にデータを表示すると、以下のようにイベントがキャッチできていることが分かります。 キャッチしたイベントは上図のようにGUIベースでも確認できますが、キャッチしたイベント量が多くなると確認や集計が難しくなります。その場合は、テーブルにエクスポートすることも可能なので、SQLを使って好きなように絞り込みや集計を行うことも可能です。 クエリストア クエリの実行統計を自動で保存してくれます。クエリの実行時間やCPU時間、実行プランの変化などが時系列に格納され、特定の時間帯の状態を後から追うことができます。CPUベースのチューニングなどを行う際にかなり強力な機能となります。また、実行プランの変化によりパフォーマンスが劣化したことを把握する目的では最も便利な機能です。 以下のようにGUIも用意されていますが、クエリベースで解析していくことも可能です。 システムモニター Windowsに標準で提供されている、メトリクス収集機能です。Windowsサーバー観点でのメトリクス(CPU使用率など)と、SQL Server観点でのメトリクス(秒間のバッチ実行数など)を取得できます。パフォーマンスモニターと呼ばれることも多い印象です。 ここまでで紹介した情報の取得方法は「取得タイミングや、特定の時間帯においてどのような状態であったか」というようなスナップショット的な情報を取得できるものが多いです。一方で、システムモニターについては「収集間隔で取得された時系列の情報」を取得することができます。「クエリの実行数が何時何分にスパイクしたか」というような状況を簡単に取得することができますので、時系列の状態の変化を取得したい場合にはシステムモニターが適しているケースがあります。 以下のようにGUIで収集するメトリクスを選択できます。 収集間隔は最短で1秒です。そのため、 Datadog や Zabbix といった監視製品よりも短い間隔で情報を収集したいときにも重宝します。私の肌感では、今回紹介した4つの情報取得機能の中では最も軽量です。この機能を有効化していることでの負荷増が気になったことはありません。 今回の調査で何がチャレンジングなのか 理想的には、全てのクエリが使用したCPU時間を収集できれば、確実にCPUボトルネックなクエリを洗い出すことができます。この目的に最も適している機能は、クエリストアです。ただし、今回の調査ではクエリストアは使用しませんでした。 理由としては、クエリストアを有効化したことで、トランザクションログの書き込み量が常に約2倍に増加してしまったためです。クエリストアはSQL Server2016から提供開始した機能で、現在ではSQL Serverにおける定番の情報収集ツールという位置づけになっています。しかしながら、今回のサーバーでは物理リソースへの負荷の増加が許容できる範囲を超えてしまったため、クエリストアを停止した状態で調査を実施しました。(クエリストアに関連した設定により改善される可能性もあるため、今後はクエリストアの使用を再開する予定です。) したがって、動的管理ビュー、拡張イベント、システムモニターを使ってCPUボトルネックなクエリを調査する必要があります。拡張イベントのフィルターの設定によっては、全クエリのCPU時間をキャッチすることは可能ですが、プロダクション環境での負荷増につながるため現実的ではありません。種々の情報を組み合わせながらボトルネックなクエリをどうやって探していくかがチャレンジングな部分になります。 システムモニターを使った調査 Batch Resp Statistics オブジェクト を使うことで、サーバーで実行されているクエリのCPU負荷の分布を確認することができます。 例えば、[CPU使用時間が10ms以上20ms未満のクエリの総実行時間]などです。Batch Resp Statistics(CPU Time:Total(ms))を積み上げ面グラフにしたものと、そのときのCPU負荷(Processor(_Total)\% Processor Time)をグラフにしたものを並べると同じような波形を示します。例として、ある日のCPU高負荷となった時間帯に収集した値をグラフ化したものを以下に示します。 この面グラフを確認することで、CPU負荷増に最も寄与しているのはCPU時間50ms-100msのバッチ(濃い青色)だと判断できます。ミリ秒単位の、単体ではなんら問題の無いクエリが大量に実行されたことでCPU負荷につながっているケースでした。数秒から数分単位でCPU時間を使用しているような、単体で高負荷なクエリがボトルネックとなるケースもあるため、CPU負荷をかけているクエリの性質をざっくり把握するのには非常に便利です。具体的なクエリまではこの方法では分からないのですが、拡張イベントで「CPU使用時間が50ms-100msのクエリ」をキャッチすることでクエリを特定することができます。拡張イベントでは全てのクエリ完了イベントをキャッチすることは負荷増につながる懸念があるため、Batch Resp Statisticsを使って「CPU使用時間をどの範囲でフィルタするか」を決定するという方法は有用です。 DMVを使った調査 ドキュメント には、DMVの一種であるsys.dm_exec_query_statsを使った、CPUボトルネックなクエリを特定するクエリサンプルとして、以下のSQLが紹介されています。 出典: こちら sys.dm_exec_query_statsには、以下の性質があります。キャッシュしているクエリがリコンパイルされると plan_generation_numがカウントアップ creation_timeがコンパイル時間にアップデート execution_count / total_worker_time / total_elapsed_time などが0にリセットされる (正確には、古いplan_generation_numの値をもったレコードが削除され、新しいplan_generationi_numの値をもったレコードがINSERTされるような気もしますが。) また、ドキュメントには以下の記載があります。 つまり、プランがキャッシュから削除されると、対応する行もこのビューから削除されます。 上記性質を踏まえると、MSのドキュメントに記載されているクエリは、「クエリがリコンパイルされることなく、かつ一切キャッシュアウトしない」という前提のもとで正確な値がとれる、ということになります。しかし、多くのクエリが実行されているプロダクション環境においては、クエリの再コンパイルが頻繁に起き、キャッシュアウトされることも想定されます。また、ドキュメントで紹介されているクエリは「平均のCPU使用時間が高いクエリ」を抽出することはできますが、サーバーにCPU負荷をかけているクエリを特定する、という目的であれば、「CPU負荷が高い時間帯における、総CPU使用時間が高いクエリ」を特定したいところです。こうした背景を踏まえて、DMVを使ってできる限り正確にCPU高負荷なクエリを見つける方法を考えてみました。 プロダクション環境でのリコンパイル発生状況を確認する sys.dm_exec_query_statsを使ってリコンパイル発生状況を確認してみます。 select * ,SUBSTRING(qt.TEXT, qs.statement_start_offset / 2 , ( CASE WHEN qs.statement_end_offset = - 1 THEN LEN( CONVERT (NVARCHAR( MAX ), qt.TEXT)) * 2 ELSE qs.statement_end_offset END - qs.statement_start_offset ) / 2 ) as statement from sys.dm_exec_query_stats qs OUTER APPLY sys.dm_exec_sql_text(qs.sql_handle) AS qt -- クエリテキスト用 OUTER apply sys.dm_exec_query_plan(plan_handle) as qp -- プランプラン用 WHERE creation_time > dateadd(SECOND, -60 , getdate()) AND execution_count >= 10 このクエリは、「過去60秒以内にリコンパイルされ、リコンパイル後の実行回数が10回以上」なクエリを抽出します。このクエリを実行してレコードがとれる場合は、「本当はCPUを沢山使っているけど、頻繁にリコンパイルされてtotal_worker_timeがリセットされているためにtotal_worker_timeが小さな値になっている」ような状況が発生してる可能性があります。 アイデア 特定の2点におけるsys.dm_exec_query_statsの情報を全てダンプして保存した場合、各レコードはクエリAまたはクエリBのいずれかに該当します。 クエリA:creation_time <= Time① クエリB:creation_time > Time① ※last_execution_time = Time② であると仮定します。プロダクション環境で常に実行され続けているようなクエリばかりの場合はこの仮定で問題ありません。 このとき、Time①とTime②の間の時間帯におけるCPU使用時間が高いクエリを見つけるために、クエリAとクエリBにおいて以下の計算を実施します。 クエリA:Time②のtotal_worker_time - Time①のtotal_worker_time クエリB:Time②のtotal_worker_time * (Time② - Time①) / (Time②-creation_time) これにより、コンパイル時間(=total_worker_timeの算出開始時間)が異なるクエリであっても、同一の時間間隔における使用CPU時間を推定し、その値が大きい順に並び替えることでCPUボトルネックなクエリを推定できると考えました。 作成したクエリと、クエリを使った解析手順 このクエリ を実行してまずダンプ用のテーブルを作成します。 このクエリ を(1分に1回など)定期的に実行します。最低2回取得できればOKです。 このクエリ の、@snapshot_time_earlierと@snapshot_time_laterの2か所に、実在するcollect_dateの値を入れて実行します。 下図のように、CPU使用時間の寄与率が高い順に結果が表示されるので、これらのクエリがチューニングできないかを検討すればOKです。 チューニングについて チューニング対象を特定できたら、あとは実際にクエリチューニングを実施し、同程度のトラフィックに対してCPU負荷がどの程度減少するかを評価します。本記事ではCPU観点でのボトルネッククエリの調査手法をメインに扱いますので、具体的なクエリチューニングの方法については触れません。 結果 下図は、チューニング実施前後における、同程度のバッチ実行数に対するCPU使用率の推移のグラフです。 クエリチューニングを実施したことで、ピーク時間帯におけるCPU負荷を50%以上削減できました。本対応の結果、2020年1月の冬セールで該当DBの負荷がボトルネックとなることは一切ありませんでした。このDBで実行されているクエリは1000種類以上あり、その中でチューニングしたのは10種類ほどでしたので、一部のクエリがCPU負荷へ大きく寄与していた結果となりました。 まとめ 本記事では、CPU使用率を下げる目的でチューニングを実施する際の調査手法について紹介しました。プロダクション環境でCPU過負荷となっていたサーバーに対して実際に調査を実施し、結果としてピーク時のCPU負荷を50%以上削減しました。今回の調査では、DMVを使ってボトルネックなクエリを特定し、チューニング前後の評価にはシステムモニターを使用しました。 調査を実施しているときはアプリケーションについての知識は必要ありません。ただし、実際にクエリをチューニングしていく際は、アプリケーションの開発者と協力してチューニングを進めていく方が良い結果が得られやすいということを体感しました。例えば、開発者であれば重いクエリの一部のJOINが現在では不要なので削除できる、といった判断を下すことができます。DBの調査を得意とする人と、アプリケーションの仕様を熟知した人とが一緒になってチューニングを実施するとより良い結果が得られると思います。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
こんにちは! 開発部の @ahiru_starrr です。 本稿では、ZOZOTOWN iOSにSnapshotTestを導入したのでその経緯や導入方法、導入するメリット・デメリット、どんな場面で役に立つのかなどについて書いていきます。 SnapshotTestがどのようなものかよく分からない方や導入を検討している方々のお役に立てれば幸いです。 SnapshotTestとは SnapshotTest導入の背景 2つの課題 エンジニア ↔︎ デザイナー間のコミュニケーションコスト レガシーからモダンへの取り組み 導入方法 環境変数を設定 実装方法 テストクラスを作成 recordModeを設定 テストコードを実装 フォルダ名を設定 ファイル名を設定 stubの設定 FBSnapshotVerifyView SnapshotTestのユースケース リファレンス画像を生成 SnapshotTest!!! SnapshotTest導入のメリット・デメリット まとめ SnapshotTestとは 構成済みのUIViewまたはCALayerからスナップショットを生成し「正しい状態との比較・差分」を検出するためのテストです。 開発を進める中で、「意図せずにデザインやレイアウトが崩れてしまう」ようなケースはしばしば起こるかと思いますがそれを防ぐためのテストとして大変効果的です。 SnapshotTest導入の背景 きっかけとなったのは、iOSDC Japan 2019の スナップショットテスト実戦投入 / Practical Snapshot Testing のセッションです。 このセッションで実際に導入した話を聞いたことで、ZOZOTOWNのプロジェクトに導入するメリットが鮮明になりました。 現状のZOZOTOWN iOSの開発には2つの課題がありSnapshotTestを導入することでそれらの解決・緩和が見込めると考えたのです。 2つの課題 エンジニア ↔︎ デザイナー間のコミュニケーションコスト レイアウトに関連する変更を加えた場合、ZOZOTOWNでは例え小さな変更であったとしても必ずデザイナーにデザイン確認を行なうフローがあります。 いわゆるデザインのリグレッションテストです。 ZOZOTOWNはデザインに強いこだわりのあるサービスのため、このフローは欠かすことができませんでした。 しかしながら、より効率的でスピーディーな開発を目指していく上でこのフローが無駄なコストであることはいうまでもありません。少なくとも「開発の前後でデザインに差分がないことを確認する」だけであればもっと機械的にできるはずです。 またZOZOTOWNは主に千葉と東京、福岡の3拠点で開発が行われています。 そのためデザイナーとエンジニアのコミュニケーションはSlackやテレビ会議で行うことが多いのもコミュニケーションコストの増加に拍車をかけていました。 レガシーからモダンへの取り組み ZOZOTOWNは長い歴史のあるサービスです。 iOSアプリは2010年にリリースされており、iOSアプリだけでみても約10年の歴史があります。 2010年というと、iPhone 4が発売された時期ですね。 長い年月とともに開発環境や開発言語は大きな進化を遂げましたが、一方でプロジェクトのソースコード量は膨大し、一部の取り残されたObjective-Cは大きな態度で居座り続けています。 この半年〜1年でチームメンバーは大きく変わりSwift化をはじめとするコードのモダン化への取り組みが活発に行われるようになりました。 リファクタリングも日常的に行われているため、その際のデザインのリグレッションテストをもっと機械的に効率よく行いたいと模索をしているところでした。 ソースコードは大きく変更したいものの、それに伴う画面のUIは変更したくない状況がZOZOTOWNでは頻繁にありました。 上記2つの課題を解決するためにSnapshotTestを導入しました。 導入方法 ZOZOTOWNでは「 iOSSnapshotTestCase 」×「 OHHTTPStubs 」の組み合わせで導入をしています。 iOSSnapshotTestCaseはUberがFacebookから引き継ぎ今も継続的にメンテナンスが行われているOSSです。 併せてスタブ用のレスポンスを返すためにOHHTTPStubsも導入しました。 ほとんどの画面はAPIからのレスポンスによって動的に構成されるため、SnapshotTestとの相性が非常に良いです。staticなjsonファイルをアプリに持たせておくことで常に同じ表示条件にてSnapshotTestができるのも大きなメリットだと思います。 上記2つのOSSはCocoaPodsにて導入しましたが、予めSnapshotTestを行うためのターゲットを作成しておきそのターゲットのみに導入をしました。 target 'SnapshotTests' do inherit ! : search_paths pod 'iOSSnapshotTestCase' pod 'OHHTTPStubs / Swift' end 環境変数を設定 OSSを導入したら環境変数の設定を行います。 これによりSnapshotTestを行った後の差分画像とリファレンス画像の保存先が設定されます。 XcodeのEdit SchemeからRunの項目を選択して、Environment Variablesに2つの環境変数を設定します。 IMAGE_DIFF_DIR $(SOURCE_ROOT)/$(PROJECT_NAME)SnapshotTests/FailureDiffs FB_REFERENCE_IMAGE_DIR $(SOURCE_ROOT)/$(PROJECT_NAME)SnapshotTests/ReferenceImages FailureDiffsディレクトリにはSnapshotTestを行った後の差分の画像が保存され、ReferenceImages_64ディレクトリにはリファレンス画像が保存されるようになります。 実装方法 下記はUIViewControllerのSnapshotTestの実装例です。 import FBSnapshotTestCase import OHHTTPStubs @testable import ZOZOTOWN class CartStockViewControllerTest : FBSnapshotTestCase { override func setUp () { super .setUp() folderName = "CartStockViewController" fileNameOptions = [.device, .OS, .screenSize, .screenScale] recordMode = false } func testCartSnapshotTest () { stub(condition : isPath ( "/stocklist.json" )) { _ in let stubPath = OHPathForFile( "stocklist_test.json" , type(of : self )) return fixture(filePath : stubPath ! , headers : ["Content-Type": "application/json"] ) } let cartStockViewController = CartStockViewController() cartStockViewController.view.layoutIfNeeded() let exp = expectation(description : "Screen Loaded" ) DispatchQueue.main.asyncAfter(deadline : .now() + 1.0 ) { self .FBSnapshotVerifyView(cartModalStockViewController.view) exp.fulfill() } wait( for : [exp] , timeout : 10.0 ) } } 順番に説明していきます。 テストクラスを作成 はじめにテスト用のファイルを作成し、上記で導入した2つのOSSをimportしてFBSnapshotTestCaseを継承したクラスを作成します。 import FBSnapshotTestCase import OHHTTPStubs @testable import ZOZOTOWN class CartStockViewControllerTest : FBSnapshotTestCase { } SnapshotTestではすでに実装済みの画面に対してテストを行う場合がほとんどのため @testable importを記述します。これにより、プロダクトコードが記述されているターゲット内のinternalで宣言されたソースコードにアクセス可能となります。 recordModeを設定 SnapshotTestを行う際はrecordModeの設定が必要となります。 recordMode 説明 true リファレンス画像を生成 false SnapshotTestを行う リファレンス画像すなわち正しいUIの状態のスナップショットを生成するときはrecordModeをtrueにセットしてSnapshotTestを実行します。 コードのリファクタリングなどを行った後でデザインのリグレッションテストを行う際にはrecordModeにfalseを設定してSnapshotTestを実行します。 テストコードを実装 フォルダ名を設定 folderName = "フォルダ名" folderNameに適当なフォルダ名をセットします。 これを設定するとReferenceImages_64/フォルダ名/以下にリファレンス画像が出力されるようになります。 ファイル名を設定 fileNameOptions = [.device, .OS, .screenSize, .screenScale] fileNameOptionsにてファイル名を設定します。 上記のような設定をした場合は「ファンクション_識別子_デバイス名_OSバージョン_スクリーンサイズ_倍率」のような画像名になります。 stubの設定 APIモックを用意する主な目的は最初の方でも述べましたが、APIのレスポンスによってテスト結果が変わるのを防ぐためです。 APIのレスポンスから動的に構成される画面のSnapshotTestを行う場合、リファレンス画像生成時のAPIのレスポンスとSnapshotTest実行時のAPIのレスポンスは同一である必要があります。 これを実現するために、OHHTTPStubsを導入しています。 stub(condition : isPath ( "/stocklist.json" )) { _ in let stubPath = OHPathForFile( "stocklist_test.json" , type(of : self )) return fixture(filePath : stubPath ! , headers : ["Content-Type": "application/json"] ) } isPathの引数には、画面の表示に必要な情報を取得しているAPIのエンドポイントを指定します。 OHPathForFileの第一引数には予め用意していおいたテスト用のjsonファイル名を指定します。 FBSnapshotVerifyView let cartStockViewController = CartStockViewController() FBSnapshotVerifyView(cartStockViewController.view) SnapshotTest対象のViewController(View)を生成し、FBSnapshotVerifyViewに渡してあげます。 CartStockViewControllerの内部ではViewのライフサイクルイベントをトリガーにAPIの通信処理がはしり、そのレスポンスを受け取って画面が組み立てられる実装になっています。 APIのレスポンス取得からレイアウト作成までは少し時間がかかるためexpectationを使って非同期処理を行なっています。 ZOZOTOWNでは極力既存の実装を変更しないことを優先してテストコードを書きましたが、この部分の実装に関しては 予めモックとなるモデルを用意しておきViewControllerの初期化時に渡す方法 ViewControllerの初期化後にテストFunc内からAPIをコールし画面のレイアウトを行う方法 など様々な実装が考えられると思います。 各プロダクトに応じて最善の実装をしていただければと思います。 SnapshotTestのユースケース ZOZOTOWNでのユースケースを例にSnapshotTestをしてみます。 テスト対象の画面はZOZOTOWNのカート画面です。 コミットログを見る限り、最後に変更が加えられたのはもう何年も前でObjective-Cで記述されている画面です。 今回はSwiftで書き直し、さらにViewの構成も変更しました。 リファレンス画像を生成 まずはリファレンス画像を生成します。いわば、カート画面の模範(正しい状態)となる画像です。 recordModeをtrueに設定してテストを実行することで、ReferenceImages_64/以下にリファレンス画像が生成されます。 SnapshotTest!!! リファレンス画像を生成後、カート画面のリファクタリングを行なったブランチにてSnapshotTestを実行します。 recordModeをfalseに設定してテストを実行し、リファクタリングの前後でUIに差分がなければテストは成功し、差分がある場合にはテスト失敗のアラートが表示されます。 実際にテストを実行したところ下記の画像がFailureDiffs/ディレクトリに出力されました。 リファクタの前後の画像を並べて比較すると差分がないように見えるのですが、実際には一部フォントサイズが違っていたりレイアウトが多少ずれていたりしました。 リファクタ前 リファクタ後 SnapshotTestにより、これら差分の検出がとても簡単に行えます。 諸々修正して再度SnapshotTestを行うと成功しました。 今までの開発では、リファクタリングが終わったタイミングでデザイナーにデザインの確認を依頼してエンジニアが再度修正、修正したら再度デザイナーに依頼....というフローがありましたが、SnapshotTestを導入したことによりこのフローが不要となりました。 デザイナー抜きでは実現できなかったタスクがエンジニアのみで完結できるようになったのです。 もちろんテストコードを書く必要はあるためその分のコストはかかってはしまいますが、それを差し引いても圧倒的に開発速度は向上します。 SnapshotTest導入のメリット・デメリット ここまでSnapshotTestのメリットを挙げてきましたがデメリットもあると思います。 当然ながら、デザインに意図的な変更を加えた場合SnapshotTestは失敗となってしまいます。 その時は再度SnapshotTestでリファレンス画像を生成し直したり、あるいはstubを修正するなどの必要が出てくる場合もあり運用していく上でのコストがかかってきます。 デザインのほんの一部を修正したいだけなのにSnapshotTestのテストの方も修正する必要が出てきて全体としての開発速度が落ちてしまっては本末転倒です。 注意点としては、プロダクトに導入することで本当に恩恵を受けることができるのかよく考える必要はあるかなと思いました。 メリット 導入コストが低い デザインのリグレッションテストをエンジニアのみで行える 開発速度の向上・開発の効率化が期待できる 意図しないデザイン・レイアウト崩れを自動で簡単に検出できる デメリット 運用コスト まとめ SnapshotTestは、その性質を理解し上手に使うことで高い費用対効果を得ることができます。 実際にSnapshotTestを導入してみて、「効率的なチーム開発」「開発速度の向上」を実現するための選択肢の1つとして大きな力を発揮すると感じました。 今回は局所的にSnapshotTestを利用するユースケースをご紹介しましたが、CI/CD環境にSnapshotTestのWorkflowを組むことでデザインのリグレッションテストの自動化も可能です。 この記事がSnapshotTest実装の一助になれば幸いです。 ZOZOテクノロジーズでは、iOSエンジニアを募集しています。 興味のある方はこちらからご応募ください! www.wantedly.com
こんにちは、ARやVRといったXR領域を推進しているInnovation Initiativeの @ikkou です。2020年1月7日から10日の4日間にかけてラスベガスで開催されたCES 2020に全日程で参加してきました。 CES期間中は現地からTwitterでリアルタイムにレポートしていましたが、本記事ではXR領域のトピックスを中心に、特に興味深かったものをお伝えします。 CESとは 展示会場について 溢れかえるAR Smart Glassesと勢いの弱まるVR HMD Nreal LightとポストNreal Light 突然現れたパナソニック社の眼鏡型VRグラス デルタ航空が示したパラレル リアリティ ディスプレイ バーチャルヒューマン“Neon”の衝撃 High-Tech Retailing Beauty Tech なぜCESに参加するのか まとめ おわりに CESとは The Venetianにある看板を前にして記念撮影する人が多い CESはCTA(Consumer Technology Association)が主催する、毎年1月にラスベガスで開催される世界最大級と言える「テクノロジーのショーケース」です。 日本語圏では「せす」と呼ぶ方もいますが、適切な読み方は「しーいーえす」です。 CES - The Most Influential Tech Event in the World かつては「家電見本市」と称されることが多かったCESですが、昨今のCESは自動運転車をはじめとして「家電」の領域を超えたものが数多く展示されています。主催であるCTAも「家電見本市ではない」と公式に謳っています。 Sands Expoのエスカレーターを上がった先にあるCESロゴ 年始にこの先数年のテクノロジーを俯瞰する場としてこれ以上に相応しいものはなく、日本からも多くのテクノロジー系メディアの方が参加しています。会期終了から少し時間が経ち、多くのテクノロジー系メディアでCES関連の記事は出揃った頃合いなので、既に関連記事を読んだ方も多いのではないでしょうか。 CESは主に「展示」と「講演」の2軸で進行しますが、今回は展示のみ参加しました。自身のCES参加は2018年、2019年に続く3回目となり、CES参加歴で言えばまだまだひよっこながら、過去回との差分を俯瞰できるようになってきました。 展示会場について CES 2020初日、開場直前のLVCC South Hall 1 Sands Expoにあるスタートアップ企業が多く集まるEureka Park CESの展示はTech East・Tech West・Tech Southという3つのエリアに大別されます。 特にLAS VEGAS CONVENTION CENTER(LVCC)から成るTech EastとSands Expoを中心とするTech Westにブースが多く集まっています。さらにTech Eastの中心となるLVCCはNorth・Central・Southに大別され、端から端に歩くだけでも一定の時間を要します。 https://www.ces.tech/Show-Floor/Official-Show-Locations.aspx 会期中の4日間という限られた時間の中で、すべてのブースを細かく見るのは不可能に近いので、今回はLVCCとSands Expoに絞って朝から夜まで歩き回りました。日付上は4日間ですが、最終日は昼過ぎから順次各ブースの撤収が始まるので、実質的には3.5日間程度となります。 溢れかえるAR Smart Glassesと勢いの弱まるVR HMD 私自身が体験したAR Smart GlassesとVR HMDの一部 私が初めて参加したCES 2018では多くのVR HMDが出展されていました。ところがCES 2019ではVR HMDよりもAR Smart Glassesが目立っていました。 CES 2020ではどうだったかと言うと、まずCES 2019で多くの関心を集めたNreal Lightの後を追うように、非常に良く似たメガネ型のデバイスが目立ちました。 Nrealのブースでは日本企業のMESON社やSynamon社によるデモを体験できた そしてVR HMDはかつてほどの勢いを感じられませんでした。もちろん全く展示がなかったわけでもなく、新製品が発表されることもありましたが、AR Smart Glassesの勢いには押されている印象を受けました。 FCA Groupのブースでは大型ディスプレイでのサイネージと合わせてVR HMDが使われていた これはVRがある程度の実用段階に入り、ハードウェアそのものよりコンテンツが重要になってきていることを示唆しているものと考えています。 事実AR/VRコーナー以外のブース、例えばLVCC Northにある自動車エリアでは、コンテンツを見せるためのデバイスとしてVR HMDを用いているブースをいくつも目にしました。 Nreal LightとポストNreal Light CES 2019で話題となりCES2020でも多くの人を集めていたメガネ型デバイスのNreal Lightですが、前述の通り「ポストNreal Light」とでも言うべき、デバイスの形状、プロモーションの打ち方が非常に良く似たメガネ型デバイスをいくつか見かけました。例えば MAD Gaze社のMAD Gaze GLOW 、 0glasses社のRealX 、 Pacific Future社のam glass などが挙げられます。 MAD Gaze GLOWのMAD Gazeブースとam glassのPacific Futureブース その多くはスマートフォンと接続するタイプのもので、SLAM 機能もなく、スマートフォンのディスプレイをメガネに拡張するだけのものも多々あります。今後もハードウェア先行でこういったメガネ型デバイスは数多く出てくると考えられますが、大事なのはそのデバイスを使う理由であるコンテンツだと考えています。多くのブースでは「デモ」以上の体験が出来なかったので、次のアクションが気になりました。 突然現れたパナソニック社の眼鏡型VRグラス AR Smart Glassesが目立った印象のCES2020ですが、決してVR関連の新発表がなかったわけではありません。 例えばLVCC Centralにとても大きなブースを構えていたパナソニック社は、CES初日の1/7に突然 「眼鏡型VRグラス」に関するプレスリリース を配信しました。 あくまで「参考出展」ということで、ケースの中に収められ誰もが体験できる状態にはなっていませんでしたが、今回は良い機会に恵まれてデモ機を試せました。 パナソニック社の眼鏡型VRグラス(※写真の撮影と公開許可は頂いています) その独特な見た目も相まって「○○○に似ている、いや△△△だ」といった見た目に関する感想が多く散見されましたが、たしかに思い切った見た目だと感じました。 聞くところによるとSoCが内蔵されているので、PCを必要としないスタンドアロンで動作することも想定されているようです。しかし、実働するデモではGeForce RTX 2080 SUPERという高価格帯のGPUを搭載したデスクトップPCとUSB Type-Cで接続する形を採っていました。 接続方法は前述のAR Smart Glasses郡と似ています。大きな違いはHDRで、片目4K・両目8Kの解像度且つ超高精細、そしてパナソニック社の音響ブランドであるテクニクスのイヤホンを使うことで超高音質を実現している点です。 実際に体験したところ、プロトタイプながらたしかに高画質・高音質という謳い文句に異論を挟む余地はありませんでした。あくまで「プロトタイプ」であるということは強調して説明され、発売時期も含めて不確定要素の強いデバイスです。しかし、日本発のハードウェアということもあり、CES 2020全体を通して個人的にとても期待を持ったプロダクトのひとつとなりました。 デルタ航空が示したパラレル リアリティ ディスプレイ CES 2020ではデルタ航空が航空会社として初めて基調講演とブース出展を果たしました。基調講演の中では公式アプリであるFly DeltaにAR機能が追加される旨の他、「パラレル リアリティ ディスプレイ」という新しいディスプレイの表示方法を提示しました。 デルタ航空によるパラレル リアリティ ディスプレイのデモ、筆者の名前である“IKKOU”の文字列は他の人には見えていない パラレル リアリティ ディスプレイは、1つのディスプレイで複数人、それも1人や2人ではなく100人程度に対して、それぞれに合わせた内容を表示できる技術です。 仕組みとしては先ずデモブース入場時に対象者を認識、ブース内に設置されたカメラが対象者をトラッキングし、対象者に合わせた内容を、マルチビュー・ピクセルを用いたディスプレイで表示しています。 私が見ている内容はデモを一緒に体験した他の3名には見えていない この技術はデモのためのものではなく、今年2020年中にデトロイト空港で実運用が開始されるとのことです。 news.delta.com バーチャルヒューマン“Neon”の衝撃 CES 2020において、“色々な意味で”もっとも注目を集めたと言っても過言ではない、そしてXR領域とも関わりが深いと言えるNeonについても触れておきます。 Neon社のCEOを務めるPranav Mistry氏による基調講演のワンシーン Neon社はSAMSUNG社の研究開発部門であるSTAR Labsを母体とする企業です。企業名と同じNeonという実在する人間と変わらない見た目を持つ“Artificial Human”分かりやすく言うとバーチャルヒューマンを開発しています。 https://www.neon.life/ www.neon.life 事前情報として公開された“本物の人間と変わらない見た目”のYouTube動画が話題になり、CES初日からブースにはたくさんの人が訪れ、そのあまりのリアルさに驚愕していました。私自身もCESが始まる前夜、あまりのリアルさにとても興奮したことを覚えています。 基調講演後のデモを眺める多くの人 そして迎えた当日、YouTubeに公開された動画や、ブースに展示されている“本物の人間と変わらない見た目”のものは紛うことなき人間であり、Neonではないイメージ映像であることが分かりました。これには多くの人が落胆し、騙されたと言う人まで現れました。 私自身も残念な気持ちになったのは事実ですが、実はイメージ映像であることは示されていたので、見せ方の是非は別として「騙す意図はなかった」と認識しています。 一部で疑義を醸している #NEON ですが、ブースで流れている “イメージ動画” ではない AI による “リアルタイムのもの” はこちらです。 確認したところ服は AI によるジェネレートではなく個々に要デザインとのことでした👖 #CES2020 pic.twitter.com/fI7xdk3aJW — HEAVEN ちゃん (@ikkou) 2020年1月9日 実際にNeonが動いているデモの様子を見ると分かりますが、少なくとも現時点ではYouTube動画やブースに並んでいるほど“本物の人間と変わらない見た目”ではないように見えます。しかし、決してクオリティが低いわけでもなく、今後の開発状況次第ではバーチャルヒューマンとして成立するであろう将来性を感じました。 High-Tech Retailing XR領域はハードウェアとして分かりやすいVR HMDやARグラスだけに限りません。例えばCES2019から新設されたカテゴリであるHigh-Tech Retailingエリアには、XR領域とも言えるプロダクトが複数ブースを出展していました。 両足を三次元計測するAlbert Scanner 例えば両足を3次元計測するAetrex 社の Albert Scannerはそのひとつです。実際に試してみたところ、わずかな時間で足の3Dデータとともに各種サイズが計測され、そのデータを基にして靴に合わせたインソールを作成して自宅に届けてくれるようでした。(残念ながら日本への発送は実施していませんでした) Beauty Tech High-Tech Retailingコーナーに出展しているブースは10数程度でしたが、他にも商品を3D化するソリューションや、スマートミラーを展開する企業が出展していました。また、Beauty Techでもあり、AR × メイクでは老舗とも言えるYouCam Makeupが比較的大きなブースを構えていました。 AR × メイクでは老舗とも言えるYouCam Makeupブース High-Tech Retailingコーナー以外では、アプリと連動するインスタントジェルネイルを提供するO'2NAILSブースが昨年に引き続き賑わっていました。 アプリと連動するインスタントジェルネイルを提供するO'2NAILS また、CES 2020で初出展となるインスタントタトゥーマシンを提供するPrinkerブースにもとても多くの人が訪れて試していました。 インスタントタトゥーマシンのPrinkerブース Prinkerはアプリで選んだ柄を一瞬でプリントできるインスタントタトゥーマシンです。水溶性のインクを使っていて、お風呂で簡単に洗い流せるので、例えばフェスなどのイベントで使うことが想定されています。実際に試しましたが、驚くほど簡単にプリントできました。 O'2NAILSもそうですが、こういった実体験可能なデモを設けているブースは軒並み盛り上がっている印象を受けました。 なぜCESに参加するのか CES 2020 INNOVATION AWARDSを受賞したハードウェアの集まるINNOVATION AWARDS SHOWCASE ここまで現地で見聞きしたものをいくつか紹介してきましたが、CESに関する情報はテクノロジー系メディアを中心として、大半のことはインターネット経由で知り得ることができます。これがテクノロジー系メディアのライターであれば現地に赴き、記事を書くこと自体が仕事となりますが、ライターではない私がなぜ時間とお金をかけて現地に行くのでしょうか。 これは先ずXR領域において『百聞は一“体験”に如かず』という考え方があるからです。 つまり、「百聞は一見にしかず」という言葉以上に、実際に自分の目で見て身を以て体験することが重要だと言えます。前評判では期待できなかったものが実は素晴らしいものである可能性もありますし、その逆もあり得ます。まだ“当たり前”からは少し遠い技術であるがゆえ、しっかりと説明責任を果たせるよう、他人の意見だけではなく、自分自身で語れる状態であることを大切にしています。 XR領域だけを目的とするのであれば、CESよりもMWC(Mobile World Congress)や、より専門性の高いAWE(Augmented World Expo)などが適切です。しかしXRは手段のひとつでしかなく、テクノロジー全体を見渡す意味ではCESに参加する意味があり、だからこそ継続的に参加して差分を理解して未来を予測することも意味があると考えています。 もちろん、良いデバイスや良いソリューションがあれば、どこよりも早くアライアンスを結んで事業に繋げるといった展示会での一般的な目的もあります。実際、日本から来ている方の一部は商談目的やバイヤーの方です。 まとめ 写真だけでも32,000枚近く撮影しているので、そのすべてを紹介することは出来ませんが、特にXR領域で気になった“一部”を紹介しました。 現地参加は3年目となるCESでしたが、初のCES参加時に得られたような感動はもうだいぶ薄くなりました。 そしてXR領域だけで言えば「爆発的な変化」というよりも「順当な変化」を感じる回となりました。テクノロジー全体で言えば、例年大きなブースで出展していた企業が今年は出展しなかったなど、本記事で取り上げていないが大きく変化した部分もありますし。逆にNeonやデルタ航空のように、初出展で挑戦的な試みを実施する企業も出てきました。 今はまだ目新しく映るXR領域も、いずれは社会に融け込んで当たり前のものになると考えています。それがいつなのか分かりませんが、この技術領域を推す身としては、テクノロジーの潮流を追いながら適切なタイミングで技術を生かすことを図っていきたいと改めて胸に誓いました。 おわりに 今回のCESは、 福利厚生のひとつである「セミナー参加制度」 を利用して参加しました。特に海外出張ともなると渡航費・宿泊費だけで相応の金額が必要となるので、こうして自分の目で見る機会が提供されるのは本当に有り難いことです。前述の通りXR領域とは「百聞は一体験に如かず」なので、ここで得た知見をうまく業務に生かしていきたい次第です。 最後に、ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! https://tech.zozo.com/recruit/ tech.zozo.com 特にXR × ファッション領域での「新規事業」に興味のある方は、一度お話しましょう:) 現場からは以上です!
どうも品質管理部のキムラリョーです。 Selenium & Pythonを利用した自動テストプロジェクトの再構築をDockerを使って簡単にしたい、という話です。 これまでの自動テスト 実行までに必要な手順 1. リポジトリクローン 2. Pythonインストール 3. pipで必要なパッケージをインストール 4. Dockerインストール 5. 自動テスト実行 ターミナルからmainを実行すると、Selenium Gridのコンテナを起動した後にtestautoが実行されます。testautoはSelenium Gridに接続してブラウザを操作しながらテストを行います。 Selenium Gridだから起動時などの設定で様々な形に切り替える事ができます。Nodeを増やしたら並列も可能だし、ヘッドレスも使えるし、気軽にブラウザの設定内容を変えられます。 このプロジェクトは作成者である自分だけが実行していました。試運転と本番実行を行いながら、好きなタイミングで気軽に動かせるならよかったので、自分のPC上で直接Pythonを動かします。 テスト処理という意味ではこのままの形でも大きな問題はありません。 ただ長く利用しているとPCの買い替えや実行PCの追加、実行者が増えたりなど、頻度が多いわけでは無いですが忘れた頃にプログラムの実行環境を再構築する必要が出てきます。 引っ越しって何かと面倒な作業が発生するので、できる限り簡単にしたいと考えました。 問題1「Pythonのインストールが面倒」 プロジェクトをGitHubからクローンしてきて、テストを実行するまでの間に必要な準備が以下の3つです。 - Pythonのインストール - pipで必要なパッケージをインストール - Dockerのインストール Dockerのインストールは特に問題ないと思うのですが、Pythonやpipに関しては簡単ではありません。 一番は、Pythonをよく知らない人にとってインストールは面倒すぎます。 Pythonを理解している人のPCで動かすにしても、localのPythonで動かすわけにはいかないですし。localがダメならpyenv? そうすると「Pythonをよく知らない人」にとってのハードルがどんどん上がります。プロジェクトを作成する自分としても把握が面倒ですし、Pythonそのものやpipインストールするパッケージのバージョン等を気にしたくないです。 テスト業務をメインとするメンバーはプログラミング言語に触れる機会が少ないですが、自動テストを作成する自分よりもテストへの理解度が高いです。Pythonを知らなくても実行できる状態にする事で役割分担を進めて行く事もできますので、無理にPythonを覚えるよりもメリットが大きいと思い、インストール手順をスキップできる方法で改善します。 DockerにはPython入りのコンテナがあるみたいなので、Python入りコンテナでプログラムが動くようにします。コンテナ起動時にはpipインストールも自動で行います。 問題1を改善した自動テスト 実行までに必要な手順 1. リポジトリクローン 2. Dockerインストール 3. サブコンテナ起動 4. メインコンテナ起動 5. 自動テスト実行 問題2「コンテナの中からコンテナを起動したい」 Pythonとpipに関しては「メインコンテナ起動」にまとめる事ができましたが、 mainがコンテナに入った事でmainからSelenium Gridが入ったサブコンテナを起動できなくなりました。それによって必要な手順に「サブコンテナ起動」が増えました。 コンテナの中はlinux、ホストとは異なる環境、別物です。 linux「Docker? なにそれ?」 とクジラの上にいるペンギンが灯台下暗し的な事を言い出します。コンテナを立ち上げたい時はホスト上から起動する必要があります。 もしmainからコンテナを起動できると「mainがtestautoの実行時、テスト内容に合わせた設定のSelenium Gridを起動する」といった事もできるので、今のうちに対応したいところです。 調べて見るとコンテナの中にコンテナを立ち上げる方法と、あくまでもホスト上にコンテナを立ち上げる方法があるみたいです。 - DinD [Docker in Docker] - DooD [Docker outside of Docker] メリットやデメリットなどの詳細については、上記の名称で検索するといくつか出てくると思います。 DinDの場合は、ホストで動いているDockerとはまた別のDockerが存在する事になってややこしい気がしました。 DooDの場合は、コンテナが全てホスト上に立ち上がるのでコンテナ同士の関係性が一切見えません。今回のプロジェクト以外でも同ホスト上にコンテナを立ち上げている場合は管理できなさそうです。 プロジェクトによってはセキュリティ面の確認は必須です、Docker imageも公式のimage限定にするとか。自分もまだ試してませんが、Rootlessモードにしてみるとか。 とりあえず今回はコンテナの管理に気を回す必要はなく、利用者も部内のみなので、DooD [Docker outside of Docker]を選択する事にしました。 自動テストでエラーを検知した際、その内容を確認し必要であれば手動テストも合わせて、検知したエラーが「自動テストの問題」か「テスト対象サイトの問題」かを判断しなければなりません。 この原因解明時に一番困るのが、短時間で原因を特定できず再現もできないエラーです。同PC内の別のプロジェクトによってなんらかの影響を受けた事が原因だった場合などは一番手間取るように思います。たとえ単純な原因だったとしても、別プロジェクトによるモノだと見落としやすくなりますし。 基本的には自動テスト専用PCにする、基本を崩す時は自身でなんとかする。という事でDooDにしました。 問題2を改善した自動テスト 実行までに必要な手順 1. リポジトリクローン 2. Dockerインストール 3. メインコンテナ起動 4. 自動テスト実行 mainとtestautoも切り離した方が綺麗だと思ったのでコンテナを分けました。 必要なコードを書いてみる 問題が解決した事でとりあえず起動までは進めそうなので、簡易的ですがコードを書いていきます。 ファイル構成 TestAutomation |_main | |_main.py | |_testauto | |_testauto.py | |_Dockerfile | |_requirements.txt | |_docker-compose.yml |_Dockerfile |_requirements.txt ./docker-compose.yml version : '3' services : main : build : . container_name : 'main' volumes : - /var/run/docker.sock:/var/run/docker.sock - ./main:/main - ./testauto/Dockerfile:/main/testauto/Dockerfile:ro - ./testauto/requirements.txt:/main/testauto/requirements.txt:ro tty : true environment : - HOST_ROOT_PATH=${PWD} command : python ./main.py mainコンテナを起動する為のdocker-composeファイルです。 docker.sockをマウントしてDockerの情報をホストとコンテナで共有します。 mainがtestautoコンテナを起動する時の為にtestautoのDockerfileとrequirements.txtをマウントしておきます。別にマウントせずにmainディレクトリの中に入れておいても良いんですが、この2つのファイルがtestautoの為の物なんだという事が一目でわかると思ったのでこの配置です。 environmentでホストから見たこのディレクトリのパスをコンテナ内の環境変数に設定します。 commandにmain実行を書く事で、メインコンテナ起動と同時に自動テスト実行も行うようにします。 ./Dockerfile & ./testauto/Dockerfile FROM python:3.7 ENV PYTHONUNBUFFERED 1 RUN mkdir /main か testauto WORKDIR /main か testauto COPY ./requirements.txt /main か testauto/ RUN pip install --upgrade pip RUN pip install -r requirements.txt mainとtestautoのdocker imageを構築する為のファイルです。 とりあえず簡単な内容なので2つぶんまとめて。コンテナ名になる部分だけをそれぞれ変更します。 ./requirements.txt docker==4.0.2 ./testauto/requirements.txt selenium==3.141.0 Dockerfileから参照するpip installの対象パッケージを書きます。 必要な物を追加。それぞれrequirements毎にコンテナが異なるので、同じパッケージが必要ならどちらにも書く。 ./testauto/testauto.py import time print ( 'start' ) for i in range ( 30 ): time.sleep( 1 ) print (i) print ( 'end' ) とりあえず実行されるかがわかればいいので、適当に書いておきます。 ./main/main.py import docker from docker.errors import ContainerError import os import re # docker-compose.ymlで指定した環境変数を使います HOST_ROOT_PATH = os.environ.get( 'HOST_ROOT_PATH' ) ################################################## # testauto container # def run_container_testauto (): client = docker.from_env() testauto_dir = f '{HOST_ROOT_PATH}/testauto/' name = 'testauto' docker_setting = { 'image' : name, 'name' : name, 'volumes' : { testauto_dir: { 'bind' : '/testauto' , 'mode' : 'rw' }, }, 'environment' : [ # 環境設定にhostPCのIPが入ります 'SELENIUM_GRID_HUB_IP=host.docker.internal' , ], 'command' : f 'python ./testauto.py' } # imageを探す、なければDockerfileのpathを渡して作成する if not client.images.list(name): client.images.build(path= './testauto' , tag=name) try : client.containers.run(**docker_setting) except ContainerError: print ( 'run_container_testauto error' ) con = client.containers.get(name) logs = con.logs() file_path = f 'logs/testauto.log' dir_path = os.path.dirname(file_path) if not os.path.exists(dir_path): os.makedirs(dir_path) with open (file_path, 'w' ) as f: f.write(logs.decode( 'utf-8' )) ################################################## # selenium grid container # def run_container_grid_hub (network_name): client = docker.from_env() docker_setting = { 'image' : 'selenium/hub:3.141.59-iron' , 'name' : 'selenium-hub' , 'detach' : True , 'tty' : True , 'network' : network_name, 'ports' : { '4444' : 4444 , }, 'environment' : [ 'TZ=Asia/Tokyo' , ] } client.containers.run(**docker_setting) def run_container_grid_node (network_name, number, browser= 'chrome' ): client = docker.from_env() docker_setting = { 'image' : f 'selenium/node-{browser}-debug:3.141.59-iron' , 'name' : f 'selenium-node-{number}' , 'detach' : True , 'tty' : True , 'network' : network_name, 'ports' : { '5900' : 55550 + (number * 10 ), '5555' : 5555 + number}, 'environment' : [ 'TZ=Asia/Tokyo' , 'NODE_MAX_INSTANCES=5' , 'NODE_MAX_SESSION=5' , 'HUB_PORT_4444_TCP_ADDR=selenium-hub' , 'HUB_PORT_4444_TCP_PORT=4444' ], } client.containers.run(**docker_setting) def run_selenium_grid (node_count= 1 ): network_name = 'grid' client = docker.from_env() # docker networkを探して、なければ作成する if not [v for v in client.networks.list() if network_name == v.name] client.networks.create(network_name) containers = client.containers.list() if [v for v in containers if 'selenium-hub' in v.name]: print ( 'hub あります' ) else : run_container_grid_hub(network_name=network_name) container_names = [v.name for v in client.containers.list()] for number in range (node_count): name = f 'selenium-node-{number}' if name not in container_names: run_container_grid_node(network_name=network_name, number=number) def remove_container_grid (): client = docker.from_env() for v in client.containers.list(): if re.match( r'selenium-(hub|node)(-[0-9]+)?' , v.name): v.stop() v.remove() ################################################## # run testauto # def run_testauto (): client = docker.from_env() run_selenium_grid() run_container_testauto() remove_container_grid() client.containers.prune() client.networks.prune() client.images.prune() if __name__ == '__main__' : run_testauto() サブコンテナを起動する処理です。流れは以下です。 1. Selenium Grid Hubコンテナを起動 2. Selenium Grid Nodeコンテナを起動 3. testautoコンテナを起動しテストが実行される 4. testautoコンテナが終了したら各コンテナの停止 https://pypi.org/project/docker/ https://docker-py.readthedocs.io/en/stable/ ここではコンテナ管理にDocker SDK for Pythonを利用します。Dockerの基本的な利用方法をPythonに置き換えるだけであればややこしい部分はないので、公式ドキュメントを巡ったらなんとなく使えると思います。 コンテナ起動時に実行されるcommandを設定したので、3ステップでtestautoが実行される状態になりました。 1. リポジトリクローン 2. Dockerインストール 3. メインコンテナ起動 試しに全て揃った状態でプロジェクトディレクトリに移動して docker-compose up -d --build を打ってみます。 testautoが合計30秒のtime.sleepで終了するまでに、起動コンテナの docker ps -a と、testautoコンテナのログ docker logs testauto を確認します。 一応上記コードは内容に間違いがなければ、testautoコンテナ終了時にそのログを書き出しておく処理をつけました。main/logsディレクトリ内にテキストファイルが保存されるかと思います。 諸々、確認ができたら完成です。 問題3「mainコンテナの起動に時間がかかる」 一件落着と思ったんですが、実際のプロジェクトで同じ対応を行った時、mainコンテナの起動にやたらと時間がかかっていました。 コンテナが起動するまでのDockerの動きを調べたのですが、コンテナのイメージを構築するタイミングで、Dockerfile以下の階層にあるファイルを全てコピーするらしいです。 今回だとmainコンテナ起動時には使わないファイル、一番大きくなるtestautoディレクトリをコピーしてしまうので、これに極端に時間がかかっているような気がしました。 そこでコピーするファイルを一部除外できるdockerignoreというのがある事を知りました。使い方はgitignoreと同じです。除外したいファイルを指定する形です。 gitignoreと書き方は同じですが、処理の流れが違うようなので、本当に必要な情報だけを書くようにしておくのが良いみたいです。試してはいないですが、ignoreの書き方によっては遅くなるとかなんとか。 dockerignoreを設定した結果、1分くらいかかっていた起動が、数秒に短縮されました。 問題4「何もしてないのにSeleniumのWebDriverが落ちた」 引っ越ししてからある程度時間が経った頃、突然よくわからないタイミングでテスト実行中にWebDriverを掴めなくなる事がありました。 エラーログ等をいろいろ調べていくと、スクリーンショットで失敗した後にWebDriverを掴めなくなっているようでした。さらにエラーが出たタイミングをみると、大きな画面を撮影しているテストでのみ落ちていました。ただ、撮影失敗した後でもWebDriverをちゃんと掴めているケースの方が多かったです。 そうなるとスクリーンショットではなくメモリとかの問題かと思いコンテナを確認していたら、shm_sizeの変更、もしくはdev/shmのマウントがされていない事に気づきました。shm_sizeはコンテナが使用するメモリのサイズです。 上記のコード「main.py」で指定しているdocker_settingに設定を追加します。 サイズ変更なら shmsize: 256 、マウントなら 'volumes': {'/dev/shm': {'bind': '/dev/shm', 'mode': 'rw'}}, です。 今回の改善でコンテナの起動方法がdocker-composeからPythonのDockerに変わりました。コンテナ内からdocker-composeでコンテナを立ち上げる方法がわからなかったのでこうなったのですが、書き移すような時は抜けがないか注意しないと気づきにくいです。 ありがちな凡ミスだと思ったので、ここに書き残しておきます。 おわり 自動テストの最終的な形ですが、せっかくなので外部の環境も記載しました。 最近はAppiumでのアプリテストも追加しようと拡張中です。 Appiumは実機で動かすのであればDockerに入れるのは難しい気がしているので、localで立ち上げておく必要があります。物理的に端末を用意してPCに接続しておかなければならないですし。 Appiumの環境を整えるというのもなかなか手間がかかるので、AppiumやSeleniumの専用PCを用意してip経由で接続というのも良いと思います。 今回の変更によって実行までに必要な手順は5ステップから3ステップに減りました。 変更前 1. リポジトリクローン 2. Pythonインストール 3. pipで必要なパッケージをインストール 4. Dockerインストール 5. 自動テスト実行 変更後 1. リポジトリクローン 2. Dockerインストール 3. メインコンテナ起動 単純に3/5になったというのではなく、そもそもPythonとpipの部分は他のステップと比べても特に時間がかかる部分なので、大きな削減になりました。気持ち的には1/5くらいになったのかな、と思っています。 今回はDinD [Docker in Docker]ではなくDooD [Docker outside of Docker]を利用しましたが、DinDについてももう少し試していこうと思っています。仕組みとしてもこれが正解という訳では無いですし。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください。 tech.zozo.com
こんにちは! 開発部SREチームの指原( @sashihara_jp )です。 弊社にはSREチームが複数あり、私が所属しているチームは主にZOZOMATなどの計測システム、ZOZOERPと呼ばれる服の生産に関するシステムなど複数のプロダクトを担当しています。 本記事では私がSREチームのマネージャーとしてチーム内で行ってきた施策についてご紹介したいと思います。現在チーム内で行っている施策は全部で10個程度あるのですが、今回チーム内で事前に各施策について満足度アンケートをとりました。ここでは満足度の高かった施策とそうでなかった施策について、その理由と共に解説していきたいと思います。 前提 満足度が特に高かった施策 1on1 チーム目標設定 意義目標 成果目標 行動目標 満足度が高かった施策 テックリードサポートタイム チーム内ZOZOエール 中長期タスク設定 KPT LT、技術記事執筆 輪読会 満足度があまり高くなかった施策 スキルマップ策定 もくもく会 まとめ 前提 組織のマネージャーの役割はチームとしての生産性やパフォーマンスの最大化だと言われています。そのための手法として昨今1on1(ワンオンワン)と呼ばれる上司と部下の1対1での頻繁な対話が注目されていますが、私がチーム内で行っている施策もこの1on1を軸にして、そこからヒアリングしたメンバーが持つ不満や不安への対策、メンバーからのやってみたいという提案などから生まれたものが中心となっています。 それぞれの施策に関しても最初から今の形に落ち着いているわけではなく試行錯誤の最中でメンバーからのフィードバックを反映しながら変化しています。今後もチームの生産性、パフォーマンスを最大化するために積極的に改善を試みていきたいと思っており、今回紹介する施策はあくまで現時点のものであり、これがベストな最終形だとは思っていません。 また、ここでは偉そうなことを書いていますが、私自身、ベテランのマネージャーではなく、これから勉強していきたいと思っている発展途上の身ですので、温かい目で読んでいただければ幸いです。 それでは、実際に行っている施策について紹介していきたいと思います。 満足度が特に高かった施策 1on1 冒頭でも紹介しました1on1ですが、今回アンケートで5人のメンバー全員がやってよかった、今後も続けて欲しい項目として挙げてくれており、非常に満足度の高い施策です。私のチームでは以下のような形式で実施しています。 1人30分、週に1回 事前に双方から話したいことを1つ以上用意してくる スプレッドシートに議事録を残して振り返るようにする メンバーは5人いるので毎週必ず3時間近く1on1に使っていることになります。よく1on1について聞かれる質問として「普段から隣の席でコミュニケーションを取っているから必要ないのでは?」ということがあります。しかし、実感としてはどんなに普段からコミュニケーションを取っていたとしても、 話す内容を考えることで自分やチームと向き合うことができる フィードバックを細かい頻度で実施できる という2つの大きなメリットがあるので、やるのとやらないのではチームや個人の成長速度が全く違うと感じています。 ただ実際、何を話すのかは最初、難しかったのも事実で、初期の頃は30分間雑談で終わってしまうことも多かったです。そこでお互いに事前にアジェンダを用意してくる形式に変更することで、しっかり向き合う時間になるよう工夫し、議事録も残して後で振り返ることができるようにしました。 内容として気をつけていることは タスクの進捗確認は別の機会があるので極力しない 単なる雑談だけの日があってもいい 普段はあまり考えないようなキャリアプランを考える機会にする プライベートも含めたメンタルヘルスについて確認する うまくいったタスク、いかなかったタスクの原因を振り返ることで、経験学習を促進する 仕事やチームへの不満、改善点を洗い出す などを意識して議題を設定しています。 基本的には何でも話してOKで、ここでなにか問題が発見されたのであれば解決案を一緒に考えることもありますし、答えが出ない場合はチーム全員で案を出し合ったり、その結果、次の施策に繋がったりすることもあります。 好きなドラマや、欲しい生活家電のことを話すだけの日もありますし、お互いの人となりが分かったり、仲良くなったり信頼関係を強化するだけでも十分だとも思っています。 チーム目標設定 次に、これも非常にメンバーの満足度の高かった施策です。チームでは以下のような3つの目標を掲げています。 意義目標 会社が持つ「世界中をカッコよく、世界中に笑顔を。」という企業理念を実現するために、自分たちが関わっている事業をなぜやるべきなのか、どのような未来を我々は作っていこうとしているのかという長期的な視点を忘れないようにしようという目標です。 成果目標 意義目標をブレークダウンして、事業として今期達成すべき具体的な成果、ゴールのイメージを成果目標としています。 行動目標 成果目標をさらにブレークダウンして、大枠でのタスクレベルにまで落とした目標を行動目標としています。実際に今期、成果目標を達成するために必要なタスクとしては何があるのかというレベル感です。 この3つの目標を立てるという考え方は「THE TEAM」という本を参考にして行っているのですが、この形式でなくてもいいと思っていて、大切なのはチームとしてのビジョンや目標を立てることだと思っています。 何事も先を見据えたゴール設定が適切にされていないと進む方向にブレが生じますし、目の前の仕事が必ずしも楽しいものだとは限らないのが常だと思いますので、自分の仕事が最終的に何につながっているのかということを示す指針がチームには必要だと思っています。 満足度が高かった施策 テックリードサポートタイム 弊社ではチームのマネージャーとは別にテックリードという役職が存在します。その名の通り技術でチームや会社を引っ張ることがミッションの役職で、チームメンバーに対して技術的なレビューだったりサポートをすることが求められます。 ただ、どこの会社やチームでもそうだと思いますが、技術的に優れている人ほど仕事が集まりやすく、常に忙しいという状況で、チームメンバーの中でも特に新人に近いメンバーはもっとレビューやフォローをしてほしいけど、テックリードがいつも忙しそうだから話しかけづらいというような課題がありました。 そこでまず、タスクの分配自体でテックリードの手をできるだけ空けられるように調整を行いました。次に、テックリードにはスケジュール上、帯で1日1時間の枠を確保してもらい、その時間はメンバーがテックリードに質問するだけの専用の時間にしてもらいました。 この施策はテックリードの光野さんという方にご協力いただいて「みつのアワー」という親しみを込めた名前で実施しています。「みつのアワー」を始めて半年以上が経ちますが、毎日ほぼ必ず利用されておりメンバーからも好評です。1on1もそうですが、その施策がないときに「いつでも話しかけていいよ」と言っても実際話しかけづらかったりするものですので、あえてそのための時間を設定するということが効果的なんだなという学びになっています。 チーム内ZOZOエール 弊社では wevox という組織のエンゲージメントをスコア化するツールを導入しているのですが、そのwevoxの結果で私たちのチームには「成果に対する承認の機会」が少ないということが分かりました。 振り返ってみるとたしかに、タスクが無事完了したことを祝ったり、仕事での良い動きや姿勢を褒めるというような機会は作っていなかったなと気付きました。そこで最近始めたのがチーム内ZOZOエールという仕組みです。 ZOZOエールというのは社内のピアボーナス制度で、日頃のありがとうや会社の理念やミッションを表現した行動、会社への貢献、感動したことに対して Unipos というツールを使って少額の報酬を与え合うことができるものです。ZOZOエールは社内のありがとうを可視化しようという試みで非常に良い仕組みだと思うのですが、私たちのチームではこちらも使っている人が少なく、活用できているとは言い難い状況でした。 そこでこのZOZOエールを活用し毎週金曜日の朝会の中で、この1週間チームの中で頑張っていた人、成果を出していた人、他の人の見本になるようないい行動や姿勢を見せていた人を褒め合おうというタイムを設定しました。 この褒める内容については仕事に限らなくてよくて、例えばウォーターサーバーの水を率先して替えてくれていましたとか、飲み会の幹事を率先してやってくれてありがとうとか、普段あまり褒められることがないようなポイントについても誰かが見つけて取りこぼさずにピックアップできるという良い仕組みになっているかなと思っています。 もちろん、大きなタスクを達成した時や、技術的に分からないことを教えてくれたなど、普通の仕事上での「おめでとう」や「ありがとう」についても投票されるので、ポジティブなフィードバックの文化が加速できていると思います。 中長期タスク設定 私たちのチームでは複数のプロダクトを担当しており、基本的には各プロダクトからの依頼をさばくというような仕事のスタイルだったのですが、案件のレベル感的に半年や1年かけて完成させるというよりは1週間以内で終わる小粒のタスクばっかりだよねという状況がありました。 また、仕事のタイプは「緊急度の高い低い」「重要度の高い低い」で分類できると思いますが、チームとしてやっている仕事は「緊急度が高くて重要度も高い仕事」か「緊急度が高くて重要度は低い仕事」に偏っていました。しかし「緊急度は高くないけれど重要度は高い仕事」の中にこそ、本当はやるべき価値の高いことが眠っていたりします。 そこで、そのような「緊急度は高くないけれど重要度は高い仕事」に中長期的に取り組む時間を作ることで、会社やプロダクトへの貢献だったりスキル面での向上を目指そうという中長期タスクを作ることにしました。現在は各プロダクトの案件以外のタスクとして全員が中長期タスクを持って取り組んでいます。 例えば弊社では50個近いAWSアカウントを管理していますが、それぞれのアカウントへのログインをシングルサインオンで管理することによって、利便性とセキュリティ面での向上を目指すことを中長期タスクの1つとして設定しています。 また、これらの中長期タスクについては、基本的にはプロダクトのタスク優先になってしまうので、半年後に振り返って何もしていませんでした、というようなありがちなことがないように毎週「中長期タスクサポート会」を実施しています。「中長期タスクサポート会」とは 進捗報告(なければないでも良い) 技術的だったりタスク調整的に困っていることがないか を毎週確認することで、タスクのフォローと締切効果を狙っていて、今のところ効果的に働いています。 KPT 2週間に1回必ず2時間かけてKPTを行っています。 KPTは前回から今回までの間で Keep(できたこと、良かったこと) Problem(できなかったこと、良くなかったこと) Try(次回までにやりたいこと) という3つをそれぞれ会議中に、3分ずつ考える時間を作って発表しあうというスタイルでやっています。 チームとしての課題を見つけたいという意味もあるのですが、いまはどちらかというと個人レベルでの成長への宣言(英語の勉強を始める、資格勉強を引き続き頑張る、システム構築を完了させる)が多いという印象です。 KPTもいろんなスタイルがあると思いますが、目標はたてるだけじゃ意味がなくて定期的に振り返るということが必要だと思いますので、定期開催し続けたいと思っています。 LT、技術記事執筆 もともと、私たちのチームは技術的なアウトプットの習慣がないメンバーが多いという現状がありました。技術ブログだったり、Qiitaで記事を書く、LTで発表するといったアウトプットは実際なかなか労力も必要なので敬遠されがちです。 しかし弊社では業界のリーディングカンパニーの1つとして技術のエコサイクルに貢献していくという目標を掲げています。また、文章を書いたり発表したりという準備をすること自体、本人の成長につながる行為でもありますので、積極的にアウトプットを推奨しています。 とは言え、いままでそういうことをあまりやったことない人がいきなり会社の代表として技術ブログや登壇資料を作ることができるかというと、最初はやはりハードルが高いため、私たちのチームでは週に1回ずつ技術に関する内容で、LT発表とブログ等で技術記事を書いてチーム内で共有するということを朝会で実施しており、担当は持ち回り制としています。 慣れてないメンバーにとっては負担もあり辛いとは思うのですが、メンバーのコンフォートゾーンな施策ばかりやっていても本人の成長に繋がりません。チームマネジメントの観点では、多少辛くてもやれば成長できるやるべきことは半強制的にでもやってもらう方がいいと考えており、施策として取り入れています。 輪読会 毎朝1時間、朝会をやっておりそのコンテンツとして30分を使って輪読会を実施しています。輪読会ではメンバーが読みたい本を募集して全員で同じ本を(会社の書籍購入補助で)購入し、毎日決まったページ数(5〜10ページ)を一緒に読んでいます。 読んでいる中で、気になったフレーズだったり、読解が難しかったところ、面白いと思ったところを議事録ページに記載しあって時間がきたらそれらについて全員で議論しています。 技術的アウトプットについてもそうですが、興味のある技術書があったとしても個人で時間をとって読み進めるのはなかなか大変だったりするので、強制的に時間を作ることで読み進めることができます。また、1人で読むより全員で内容について議論しながら読み進めていくことで、理解が深まるので学習効果を高めることができていると実感しています。 満足度があまり高くなかった施策 スキルマップ策定 新しいメンバーが増えたタイミングで、メンバーがスキル的に何ができて、何を経験したことがなく、何を今後身につけていきたいと思っていくのかという点について知りたいと思いスキルマップを作成しました。 スキルマップは例えばAWSのサービスなどを並べていて、それに対する経験と今後の興味についてマルバツで入力していくような形式です。 これに関してはある程度傾向を知ることができたものの、1回しか実施しておらず、ここから次のステップのタスク割当の参考等にあまり活用できていなくて、チームとしても効果的に使えなかったと思う施策でした。 もっと活用するためにPDCAを回してやり方を工夫していけば満足度が上がる余地はあると思います。 もくもく会 勉強会の一種でもくもく会というものがあります。自分の好きなテーマ、興味のあるテーマを持ち寄って一定の時間の中でインプット・アウトプットするというものです。 もくもく会は参加自由として1年程継続して行ってきたのですが、業務と関係のない仕事を業務時間を使ってやるということは、特に忙しいときほどストレスに感じるのも事実で、テーマ設定の難易度も高く最終的にあまり定着させることができませんでした。 Googleさんの20パーセントルールなどありますが、業務時間を使って業務と関係ないことをするというのは、なかなか高い自己マネジメント能力が必要だなという印象でした。 まとめ 以上のように全部で10個の私たちのチームで試してみたチームビルディングに関する施策を紹介してみました。 満足度と運用コストについてグラフにプロットしてみると以下のようになります。自チームに取り入れる際の参考にしていただければと思います。 いろいろ試行錯誤している最中ですが、やってみて思うのは、まずは取り組んでみることが大事だということです。多くの場合で何かしらポジティブな変化が起きますし、変化がなければ止めたり、工夫を加えて再チャレンジすればよいだけです。 マネージャーとして大切なことは何をしていけばチームメンバーが楽しく働けて、成長することができて、ひいてはプロダクト・会社の成長に貢献できるかを考え続け行動し続けることだと思います。 マネージャーがメンバーの延長でメンバーと同じようなことをし続けるのは楽で簡単なことですが、それではマネージャーとして何もしていないことと同義だと思うので、自分の役割を強く意識して行動に移していかないといけないと思っています。 最後にZOZOテクノロジーズではより良いチームを作っていくマネージャーを各職種で大募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 tech.zozo.com
こんにちは、ZOZOテクノロジーズのVPoEの今村( @kyuns )です。 この記事は ZOZOテクノロジーズ Advent Calendar 2019 の25日目の記事になります。今年はZOZOテクノロジーズとして5つのAdvent Calendar、全125個の記事がありますので、ぜひご覧ください。ちなみに前日の記事は @ikenyal の CTO室はじめました 〜新設CTO室が1年目にやったことと課題 でした。 https://qiita.com/advent-calendar/2019/zozo_tech https://qiita.com/advent-calendar/2019/zozo_tech2 https://qiita.com/advent-calendar/2019/zozo_tech3 https://qiita.com/advent-calendar/2019/zozo_tech4 https://qiita.com/advent-calendar/2019/zozo_tech5 昨年2018年12月のAdvent Calendarで このようなZOZOテクノロジーズの紹介記事 を書きました。 時が経つのは早いもので、あれからもう1年が過ぎ、その間にZOZOテクノロジーズにも非常に大きな変化がありました。 この記事では、2019年を振り返りながら最近のZOZOテクノロジーズの様子について紹介していきたいと思います。 会社の状況 ビッグニュース まず2019年9月12日、我々を震撼させる非常に大きなニュースがありました。 なんといっても 前ZOZO代表の前澤の退任 、そして ヤフーの親会社による買収 の2つのニュースです。 前澤社長の退任の衝撃 ZOZOを代表する人間といえば、やはり前澤という人間だったでしょう。月に行くと言ったり、1億円のお年玉を配ったり、大物女優さんと付き合ったり、昨年からメディアで目にしない日はないぐらい世間を賑わせる存在でした。 それと同時にカリスマ経営者であり、彼なしではZOZOTOWNがここまで成長することはありませんでした。 そんなZOZOの顔である彼がいなくなることは我々にとっても非常に大きな心配事でした。 ニュースの発表前は 「ニュースを知った社員たちはどういう反応をするだろう?」 「前澤のことが好きで入ってきた社員達が一気に辞めるのではないか」 「今後のZOZOTOWNの戦略はどうなるんだろう?」 と、いろんな事を考えながら当日を迎えました。 ニュース発表当日、スタッフ達は朝から一体何が起きたのか、事実を受け入れるまでに時間がかかっていた人が多かったように思えます。昨日までは、自分たちの会社を代表する存在だったのに、ニュースで知ったときにはもうZOZOをやめている、そんな天変地異みたいなが起きることがあるのかと。 ただ、この心配は杞憂に終わりました。 発表の後エンジニア達全員と面談をしましたが、前澤の決断という思いを汲み取ってか、みんな非常に前向きで、これからは自分たちでZOZOを支えていかないといけない、という強い意気込みを感じました。 実際に退任が直接的原因で会社をやめたという人は今のところ聞いていません。 今、ZOZO社員は澤田新社長の元、過去最大級にやる気に満ち溢れています。 ヤフー親会社による買収(TOB) 同時に発表されたのはヤフーの親会社、ZホールディングスによるZOZOのTOBのニュースでした。 僕自身、もともとヤフーに新卒入社して、Yahoo! FASHIONを立ち上げた経験があります。ヤフーでファッション業界を変えようとしたのです。しかしながら、その1年後僕と金山(現ZOZOテクノロジーズ CINO)はヤフーを辞め、VASILYという会社を創業し、ファッションサービスの開発を行う人生を歩みました。そして、数年の時を経て2017年にZOZOへとM&Aされたという経緯があります。 元ヤフー出身の僕としては初めてその事実を聞いたときにはさすがに衝撃すぎて変な声が出ましたが、13年の時を経てまたヤフーとともに日本のファッション業界を盛り上げれるなら、これはもう運命以外の何物でもないだろうと、今となってはワクワクしています。 そしてヤフーと手を組むことは 「日本でナンバーワンのファッションECを共に目指す」 ことが可能になることを意味します。 ZOZOとしてもこれほど大きな目標に更に向かっていける強力なパートナーを得たという意味では今後の協業が本当に楽しみです。 またエンジニアリング組織としても、協力してより強化していくことをヤフーCTOの藤門さんとも話し合いました。 会社の規模はどう変わったか ZOZOテクノロジーズには2019年12月時点で 約330名 の従業員が在籍しており、そのうちの約230名がエンジニアになります。10月にはZOZOUSEDが吸収合併された影響もあり、昨年からすると 約100名 ほど従業員が増えました。 また今年4月には14名の新卒社員が入社し、来年の4月には19名の新卒も入社予定、さらにZOZOのグループ会社であったアラタナの吸収合併も予定されているため、400人を超える組織になっていく予定です。 プロジェクト紹介 この1年でZOZOが行う事業にも非常に大きな変化がありました。 現在進行中のプロジェクトをいくつか紹介したいと思います。 ZOZOTOWN すでに完成していると思われがちなZOZOTOWNですが、非常に多くのプロジェクトが進行しています。ちょうど10月頃に これできる人いませんか? という求人ページを公開しましたが、その中からいくつかの案件を紹介します。 ZOZOTOWNリプレイス 昨年から続いているZOZOTOWNのリプレイスプロジェクトです。 オンプレミス環境および、VBScriptで構成されている今のZOZOTOWNのアプリケーションをマイクロサービス化し、クラウドへと移行を進めています。 現在まだプロジェクト道半ばですが、少しずつ本番での運用実績が溜まってきました。 最近では大きな負荷のかかるセール時でも、本番のトラフィックを一部クラウドへ流し、安定運用することが出来始めてきています。 とはいえ、まだまだ道のりは遠く色々試行錯誤しながら進めている最中です。 検索改善 ECサイトにおいて、商品検索は生命線といっても過言ではありません。 ファッション辞書の構築、検索結果のパーソナライズやサジェスト精度の向上など、まだまだ検索まわりにおいて改善することが山程あります。 われこそは、という検索のプロフェッショナルな方の協力を必要としています。 パーソナライズ ZOZOには膨大なファッションに関するデータが蓄積されていますが、まだそれを十分に活用できていません。それらビッグデータを活用し、機械学習などを用いて、一人一人に合わせた商品の表示を届けるようなパーソナライズの基盤を構築するプロジェクトになります。 機械学習や深層学習を用いて、最適な結果を突き詰めていくやりがいのあるプロジェクトになります。 基幹システム ZOZOTOWNを支える要と言っても過言ではないでしょう、ZOZOの強みは自社でのフルフィルメントにあります。その裏側を支えるシステムの改善全般を行います。BOと呼ばれる商品を管理するためのツールの改修はもちろんのこと、最近ではスマートファクトリーチームと連携して、今まで人間の手で商品のサイズを計測していたものをZOZOSUITの技術を応用し、商品を撮影した画像を解析することによって自動的に商品のサイズを算出するようなシステムの開発を行ったりしています。このように"ささげ"とよばれる作業を技術を用いて自動化していくようなことなども行っています。 MSP(マルチサイズプラットフォーム) 昨年最も話題となったZOZOSUITですが、事業方針の転換により、ZOZOSUITはカスタムオーダーのスーツの注文を除いて配布を停止しています。その背景としては、数百万件の体型データを集めたことにより、身長体重年齢などからある程度の体型を予測することが可能となったことや、プライベートブランドの事業方針の転換などがあります。 現在は、ZOZOSUITで培ったマルチサイズの服を作るという技術を用いて、ZOZOに出店してくださっているブランドさんの人気商品をマルチサイズで展開できるようなサービスへと事業転換をおこないました。それが マルチサイズ と呼ばれる事業です。 現在ZOZOTOWN上ではMSマークのある商品は自分の体形にあったサイズで購入することが可能となっています。また、MSP事業を支える上で必要なのが、効率の良い生産方法の確立です。例えば、型紙をそれぞれの体形のサイズにあったように作る作業、いわゆるグレーディングを自動で行うような研究開発なども行っています。 ZOZOMAT ZOZOSUITについで、我々が目指したのは足の計測でした。足の計測をミリ単位で行うシステム 「ZOZOMAT」 の開発を行っています。ZOZOMATのリリースも控えており、靴選びに革命をもたらすシステムの開発に取り組んでいます。 どのようなUI/UXにすれば、ユーザーがわかりやすく、高精度に計測できるのか、日々試行錯誤しながら靴選びの未来を模索しているプロジェクトになります。 中国版ZOZOTOWN「ZOZO」 12月にはZOZOTOWNの中国進出を開始しました。中国に現地法人を設立し、新しくファッションメディアECとして、WEARに蓄積されたコーディネートやトレンド情報発信に絡めながらファッションを楽しむことができるサービスとして新たに「ZOZO」をローンチしました。 世界進出の足がかりとしてまずは中国での成功に全力を尽くすプロジェクトになります。 PayPayモール出店 ヤフーとのシナジー第一弾はZOZOTOWNのPayPayモールへの出店です。 ちょうど今週PayPayモールへの出店を開始しました。 ZOZOTOWN PayPayモール店 PayPayモール内のZOZOTOWNは、在庫の連携なども行っており、システム的にもインテグレーションの強化を行っていきます。 また、今後はZOZOTOWNへのPayPay決済対応をはじめとして様々な連携作業を控えています。 日本一のファッションECをヤフーとともに作り上げるために、色々な施策を行っていくプロジェクトになります。 ZOZO研究所 ZOZO研究所ではいろんな研究が行われていますが、最近では 類似アイテム検索 のようなプロジェクトでもMLエンジニアが活躍しており、非常に高精度な類似画像検索を実現しています。このように研究開発した結果を、プロダクトに組み込めることもZOZO研究所の魅力の一つとなっています。また取り組み内容を Google Cloud Nextで発表 したり、アカデミックな分野以外での登壇の機会も増えてきました。 今後もパーソナライズをはじめとして、ZOZOTOWN側と協力しながら、様々なアルゴリズムの開発を行っていきます。 Innovation Initiative(新規事業) いわゆる新規事業を行うプロジェクトチームになります。その範囲は非常に幅広く、新規サービスの開発やIoT、スマートスピーカー、XRなど様々な新規事業開発を行っています。 世界中を飛び回って自分たちが必要とする技術を持っている会社を探したり、海外の企業とコラボレーションすることも多いチームになります。最近ではメルカリR4DでXRを推進していた @ikkou さんが入社して新しくXRチームが立ち上がったり、これからの活躍が楽しみなプロジェクトチームになります。 労働環境について ZOZOテクノロジーズの労働環境においても非常に多くの変化がありました。 勤務場所 勤務場所は変わらず青山、幕張、福岡の3拠点が存在します。 更に来年の4月にはアラタナも吸収合併されるため、宮崎にも拠点ができることになります。 環境、福利厚生、制度、手当など ZOZOテクノロジーズにはすでに非常に多くの福利厚生や制度がありますが、今年になってさらにいくつかの新しい制度が出来ました。この1年で、制度面から見ても他社に引けを取らないぐらいの水準になってきたかとは思います。昨年からの変更点をいくつか紹介いたします。 グループウェアをG Suiteへと移行 まず社内のグループウェアがOffice 365から G Suiteへ と変わりました。Office 365もいい面はあるのですが、オンライン上のツールが使いにくかったりプログラムからのインテグレーションが弱かったり、今後の効率化のことを考えてGoogle DriveやSpreadSheetを扱えるG Suiteへと移行しました。チャットツールとして利用しているSlackも現在Enterprise Gridへと移行を進めています。 フルフレックスタイム制度導入、リモートワーク解禁 今年新しく導入された制度で、一番インパクトがあったのは間違いなくこの2つの制度、 フルフレックスタイム と リモートワーク です。 今まではコアタイムありのフレックス制(コアタイムは10時〜17時)でしたがフルフレックスタイムへの移行にあたり、コアタイムは無くなりました。 またリモートワークの仕組みも整えたため、自宅などからも業務を行うことが可能になりました。 しかしながら、この2つを実現するためには多くの壁をクリアする必要がありました。 社員からの要望も非常に強く、我々としてもなんとか実現したかった制度でした。 実現するためにも多くの壁はあり、社内でのプロジェクト管理方法の統一、各チームごとでのローカルルールの許可、VPNの整備、貸与端末のセキュリティ強化など、非常に多くの人達の協力を経て実現するに至りました。 運用開始前はどうなるか心配な部分もありましたが、実際に運用を開始してみると非常に良い結果が出ており、平均残業時間は昨年と比べて約半分の月 20時間を切る ぐらいまでに減りました。運用を始めて4ヶ月が経ちますが、目立った悪用もなく、社員には好評です。 特にお子さんのいる家庭や時短で働いていた方々にも好評で、柔軟な働きかたを提供することが可能になりました。 特許取得に関しての奨励金 会社としてより多くの特許取得を奨励していくために、特許の出願に関しての奨励金の金額を定めました。 一般的な会社と比べても、高い水準の奨励金となっています。 リファラル採用強化 ZOZOレコと呼ばれる仕組みがあり、知り合いを紹介すると会社から謝礼金が支払われます。リファラル経由での採用も少しずつ増えており、より強力な仲間を増やすための仕組みを用意しています。 Unipos導入 ピアボーナスと呼ばれる仕組みです(社内ではZOZOエールと呼ばれています)。社員同士日々の感謝を伝え合う場として、普段表に見えないような社員の動きが可視化されたり、感謝が飛び交う場所になっています。もらったポイントは給与として支給されます。 病気休暇制度導入 こちらのプレスリリース にあるように、病気休暇制度を導入しました。例えば従来だとインフルエンザになった場合、5日間会社を休む必要があり、その場合は有休を消費する必要がありましたがこの制度によって有休を消費することなく、休暇を取得することが可能になりました。いざというときのために有休をとっておく、といったような心理的な負担も減り、より健全な有休取得が可能となりました。 他にも以下のような福利厚生や制度があります。 社員割引 住宅通勤手当(月5万円) 家族手当(一人につき5,000円) 夏季・冬期休暇(夏休み、冬休みがそれぞれ3日ずつ付与) 書籍購入補助(金額、冊数の上限なし) 資格取得補助(資格取得費用全額補助) セミナー、カンファレンス参加費用全額補助(国内外問わず、チケット代、ホテル代、交通費を全額補助) 副業許可 詳しくは こちら を御覧ください。 エンジニアリング組織の改善 エンジニア組織をより良くするために、4月にはCTO室を直下に新設し、自分自身の仕事をスケールできるようにするための体制を整えました。 CTO室でどのようなことを行ったかはCTO室の @ikenyal の CTO室はじめました 〜新設CTO室が1年目にやったことと課題 をご覧ください。 CTO室ではエンジニアのパフォーマンスが最大化できるように、様々な仕組みやフローの整備を行い続けています。 1年を振り返ってみて 昨年からは想像もつかないぐらいのいろんな変化があり、まさに変化を楽しんだと言える1年でした。 また、ZOZOテクノロジーズという会社を世間に知って貰う機会を増やせた1年だったかなと思います。 ファッションテックカンパニーとして、少しずつ着実に成長している実感があります。 もちろん、まだまだ発展途上ではあるので、そんな変化が激しいZOZOテクノロジーズ を一緒に盛り上げてくれる仲間も絶賛募集中です。 この記事を読んで、ZOZOに応募してみたいと思ったエンジニアの方は下記の採用ページからぜひご応募ください。 その際に「テックブログみました」と書いていただけると幸いです。 tech.zozo.com 我々と一緒に楽しく働きましょう!
はじめに こんにちは。WEAR iOSチームの坂倉 ( @isloop ) です。 この間リリースされたiPadOSはかなり盛りだくさんの内容でしたね。 個人的には、1つのアプリで複数のウィンドウを開ける「Multiple Windows」機能が一番気になりました。 この記事では、WWDC 2019のセッション Introducing Multiple Windows on iPad と Architecting Your App for Multiple Windows を参考にしながらWEARへの仮実装を通して「Multiple Windows」を解説します。 解説 条件 Multiple Windowsは以下の条件で動作します。 Xcode 11以降に含まれるiOS 13.0 SDK以上でビルドされたアプリ iPadOSがインストールされているiPad Supporting Multiple Windows on iPad | Apple Developer Documentation WEARにMultiple Windows機能を実装する 今回、WEARに以下の5つのMultiple Windowsの機能を実装してみました。 複数のウィンドウを開く ドラッグ&ドロップで新しいウィンドウを開く ロングタップからのコンテキストメニューから新しいウィンドウを開く 以前開いていたすべてのウィンドウの状態を復帰させる 現在開いているすべてのウィンドウのUIを更新する これらすべてを実装するのはなかなか大変そうに思えますが、意外に少ないコードで実装できます。 その1. 複数のウィンドウを開けるように、Xcode 11以前で作ったアプリをMultiple Windowsに対応させる 実は、Xcode 11で作ったプロジェクトならば設定画面の「Supports multiple windows」にチェックを入れるだけで対応できます。 しかしながら、Xcode 10以前に作られたプロジェクトの場合はコードを足さないと対応できません。 WEARは現在、Xcode 10.3で開発しているのでコードを足す必要がありました。 と言ってもほんの少しのコードで対応できます。以下の3つを足すだけです。 (1)AppDelegate.swiftに以下のメソッドを追加します。 func application (_ application : UIApplication , configurationForConnecting connectingSceneSession : UISceneSession , options : UIScene.ConnectionOptions ) -> UISceneConfiguration { return UISceneConfiguration(name : "Default Configuration" , sessionRole : connectingSceneSession.role ) } (2)Info.plistに以下のコードを追加します。 <key> UIApplicationSceneManifest </key> <dict> <key> UIApplicationSupportsMultipleScenes </key> <true/> <key> UISceneConfigurations </key> <dict> <key> UIWindowSceneSessionRoleApplication </key> <array> <dict> <key> UISceneConfigurationName </key> <string> Default Configuration </string> <key> UISceneDelegateClassName </key> <string> $(PRODUCT_MODULE_NAME).SceneDelegate </string> <key> UISceneStoryboardFile </key> <string> BrowserViewController </string> // 最初に表示するStoryboard名 </dict> </array> </dict> </dict> (3)SceneDelegate.swiftを新規作成して下のコードをそのまま貼り付けます。 import UIKit class SceneDelegate : UIResponder , UIWindowSceneDelegate { var window : UIWindow ? func scene (_ scene : UIScene , willConnectTo session : UISceneSession , options connectionOptions : UIScene.ConnectionOptions ) { guard let _ = (scene as ? UIWindowScene) else { return } } } これだけです。アプリを起動し「Dockのアイコンを長押し」してみましょう。 これまで表示されなかった項目「すべてのウィンドウを表示」が現れるようになり、1つのアプリで複数のウィンドウを開くことができます。 ちなみに、これまでUIApplicationDelegateで使用していたライフサイクルメソッドは使えなくなります。そのあたりの処理はUISceneDelegateのsceneDidBecomeActive / sceneWillResignActive / sceneWillEnterForegroundなどに移管する必要があります。 その2. ドラッグ&ドロップで新しいウィンドウを開けるようにする 次は、「ドラッグ&ドロップで新しいウィンドウを開く」を実装してみましょう。 流れとしては、NSUserActivityの中へ新しいウィンドウに必要な情報を入れて生成しUIDragInteractionDelegateを利用してSceneDelegateの「scene(_:willConnectTo:options:)」を呼び出し、新しいウィンドウを作ります。 (1)info.plistに有効なNSUserActivityTypesを定義します。 <key> NSUserActivityTypes </key> <array> <string> wear.multiple.windows.coordinateDetail </string> </array> (2)UICollectionViewの場合は「viewDidLoad()」あたりに以下のコードを追加します。 collectionView.dragDelegate = self (3)UICollectionViewDragDelegateの「collectionView(_:itemsForBeginning:at:)」 で、新しいウィンドウを開くのに必要な情報をNSUserActivityのuserInfoに入れてUIDragItemに渡します。(NSUserActivityのactivityTypeは、先ほどinfo.plistで指定した文字列にすること) extension ViewController : UICollectionViewDragDelegate { public func collectionView (_ : UICollectionView , itemsForBeginning _ : UIDragSession , at indexPath : IndexPath ) -> [UIDragItem] { let userActivity = NSUserActivity(activityType : "wear.multiple.windows.coordinateDetail" ) activity.userInfo?[ "Info" ] = items[indexPath.item] //新しいウインドウを開く際に必要な情報をuserInfoに入れる let itemProvider = NSItemProvider(object : url ) itemProvider.registerObject(userActivity, visibility : .all) let dragItem = UIDragItem(itemProvider : itemProvider ) return [dragItem] } } UITableViewもUITableViewDragDelegateがあるのでほぼUICollectionViewと同じように実装できます。UIButtonやUIViewの場合はUIDragInteractionとUIDragInteractionDelegateを使えば可能です。 ここで1つ注意ですが、 NSUserActivityのuserInfoに入れてもよい のは以下のタイプのみで、それ以外はアプリが落ちてしまうので気をつけてください。 Each key and value must be of the following types: NSArray, NSData, NSDate, NSDictionary, NSNull, NSNumber, NSSet, NSString, or NSURL. The system may translate file scheme URLs that refer to iCloud documents to valid file URLs on a continuing device. (4)SceneDelegate.swiftに新しいウィンドウを開くための処理を追加します。 タブをウィンドウ外にドラッグ&ドロップした際にSceneDelegateの「scene(_:willConnectTo:options:)」が走るので、そこに新しくウィンドウを開く処理を追加すれば実装完了です。 func scene (_ scene : UIScene , willConnectTo session : UISceneSession , options connectionOptions : UIScene.ConnectionOptions ) { if let userActivity = connectionOptions.userActivities.first { if userActivity.activityType == "wear.multiple.windows.coordinateDetail" , let info = userActivity.userInfo?[ "Info" ] as ? NSString { let vc = BrowserViewController.instantiate() vc.info = info window?.rootViewController = vc } } } その3. ロングタップからのコンテキストメニューから新しいウィンドウを開く ボタンのタップをきっかけに新しいウィンドウを開く動きを実装したいという需要も多いと思います。この場合も少しのコードで実装できます。 ここでは、UICollectionViewのセルをロングタップしてコンテキストメニューを出し「新しいウィンドウを開く」ボタンを押して開く実装例をご紹介します。 (1)UICollectionViewの長押しをハンドリングしてコンテキストメニューを表示させます。(触覚タッチを使った例) extension ViewController : UICollectionViewDelegate { open override func collectionView (_ collectionView : UICollectionView , contextMenuConfigurationForItemAt indexPath : IndexPath , point _ : CGPoint ) -> UIContextMenuConfiguration ? { let actionProvider : ([UIMenuElement]) -> UIMenu ? = { _ in let openNewWindowAction = UIAction(title : "新しいウインドウで開く" , handler : { [unowned self ] _ in //新しいウインドウを開く処理を openNewWindow(indexPath : IndexPath ) }) return UIMenu(title : "コンテキストメニュー" , image : nil , identifier : nil , children : [openNewWindowAction] ) } return UIContextMenuConfiguration(identifier : nil , previewProvider : nil , actionProvider : actionProvider ) } } (2)新しくウィンドウを開くのに必要な情報をNSUserActivityに追加して、UIApplicationの「requestSceneSessionActivation:userActivity:options:errorHandler:」を実行します。 func openNewWindow (indexPath : IndexPath ) { let activity = NSUserActivity(activityType : "wear.multiple.windows.coordinateDetail" ) activity.userInfo?[ "Info" ] = items[indexPath.item] //新しいウインドウを開く際に必要な情報をuserInfoに入れる UIApplication.shared.requestSceneSessionActivation( nil , userActivity : activity , options : nil ) } (3)UIApplicationの「requestSceneSessionActivation(:userActivity:options:errorHandler:)」を実行するとUISceneDelegateの「scene(:willConnectTo:options:)」が走るので、NSUserActivityから情報を取得し、それを元に画面を生成させます。 func scene (_ scene : UIScene , willConnectTo session : UISceneSession , options connectionOptions : UIScene.ConnectionOptions ) { if let userActivity = connectionOptions.userActivities.first { if userActivity.activityType == "wear.multiple.windows.coordinateDetail" , let info = userActivity.userInfo?[ "Info" ] as ? NSString { let vc = BrowserViewController.instantiate() vc.info = info window?.rootViewController = vc } } } その4. 以前に開いていたすべてのウィンドウの状態を復帰させる これで大体のMultiple Windows機能は実装できましたが、今のままだとアプリを閉じてしまったら閉じる前の状態を復旧できず毎回初期値の状態で起動してしまいます。 以下のコードを足すことで以前開いていたウィンドウの状態を復帰できます。 (1)SceneDelegate.swiftに以下のメソッドを追加します。 @available (iOS 13.0 , * ) func stateRestorationActivity ( for scene : UIScene ) -> NSUserActivity ? { return scene.userActivity } (2)ViewControllerにウィンドウの状態を保存したいタイミングに以下のコードを追加します。 let userActivity = NSUserActivity(activityType : "wear.multiple.windows.coordinateDetail" ) userActivity.title = webView.title userActivity.userInfo?[ "Info" ] = info view.window?.windowScene?.userActivity = userActivity (3)SceneDelegate.swiftの「scene(_:willConnectTo:options:)」にウィンドウの状態を保存している「session.stateRestorationActivity」から情報を取得するコードを追加します。 func scene (_ scene : UIScene , willConnectTo session : UISceneSession , options connectionOptions : UIScene.ConnectionOptions ) { if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity { if userActivity.activityType == "wear.multiple.windows.coordinateDetail" , let info = userActivity.userInfo?[ "Info" ] as ? NSString { let vc = BrowserViewController.instantiate() vc.info = info window?.rootViewController = vc } } } 早速、複数のウィンドウを開き一旦閉じてもう一度起動してみましょう。ウィンドウの状態が復帰できます。 その5. 現在開いているすべてのウィンドウのUIを更新する 現在開いているすべてのウィンドウのUIを変更したい場合は当然出てくると思います。例えば、Safariならば新しくブックマークを追加した場合、すべてのウィンドウにそれが反映されますよね。 これまでならViewControllerに付き1つのウィンドウだけ管理しておけば問題ありませんでした。しかし、Multiple Windowsは、同じViewControllerのウィンドウが複数並ぶことになります。アクティブなウィンドウだけが更新されるのはまずいわけですね。 すべてのウィンドウを更新する方法として、 Introducing Multiple Windows on iPad と Architecting Your App for Multiple Windows で「UserDefaultsによるKVOで行う方法」と「NotificationCenterで行う方法」の2つの方法が紹介されていました。 ケースバイケースでどちらの手段を使うか決めましょう。 UserDefaultsを用いたKVOの場合 UserDefaultsを拡張しKVOで値の変更を監視して全ウィンドウを更新する方法です。 (1)KVO用のKeyPathを作るExtensionを用意します。 extension UserDefaults { private static let isToolbarHiddenKey = "isToolbarHiddenKey" @objc dynamic var isToolbarHidden : Bool { get { return bool(forKey : UserDefaults.isToolbarHiddenKey ) } set { set (newValue, forKey : UserDefaults.isToolbarHiddenKey ) } } } (2)設定画面で、isToolbarHiddenを変更する処理を追加します。 UserDefaults.standard.isToolbarHidden = ! sender.isOn (3)NSKeyValueObservationをインスタンスで保持して、ViewControllerの「viewDidLoad()」に以下のコードを追加します。 private var observer : NSKeyValueObservation ? observer = UserDefaults.standard.observe(\UserDefaults.isToolbarHidden, options : .initial, changeHandler : { [weak self ] (_, _) in self ?.navigationController?.setToolbarHidden(UserDefaults.standard.isToolbarHidden, animated : true ) }) NotificationCenterの場合 ViewControllerが受け取ったイベントをModelControllerに送り、ModelControllerがModelを更新したら、各ViewControllerに通知してUIを更新する方法です。 (1)イベントを更新した際に通知をPostするデータ型を用意します。 enum UpdateEvent { case DeleteFavorite static let DeleteFavoriteName = Notification.Name(rawValue : "DeleteFavorite" ) func post () { switch self { case .DeleteFavorite : NotificationCenter. default .post( name : UpdateEvent.DeleteFavoriteName , object : self ) } } } (2)ModelControllerのメソッド内で処理を完了際に通知を送信する処理を追加します。 final class ModelController { func deleteFavorite () { // 削除処理 let event = UpdateEvent.DeleteFavorite event.post() } } (3)ViewControllerにModelControllerに追加したメソッドを実行する処理を追加します。 extension ViewController { private func deleteButtonAction () { modelController.deleteFavorite() } } (4)ViewControllerに通知を受信する処理と通知を受信した際に行う処理を追加します。 extension ViewController { private func setupNotification () { NotificationCenter. default .addObserver( self , selector : #selector(reload), name : FolderDetail.UpdateEvent.DeleteFavoriteName , object : nil ) } } @objc private func reload (notification : Notification ) { // 通知を受け取った際に行いたい処理を追加 } 実行すると、すべてのウィンドウにUIの変更が反映されます。 まとめ Multiple Windowsは、UIの実装はそれほど難しくないですが、データ保存などの設計をMultiple Windowsありきで考える必要があると感じました。 考えられるケースとしては、AとBのウィンドウで使用しているデータの保存先が同じでそれぞれリアルタイムに更新している場合ですね。 どちらを優先させるのか。またはウィンドウ別に保存させるようにするのか。アプリによってどれが最適なのかを考える必要があり、なかなかすぐに対応するのは難しいのではないかと感じました。 ただ、やはりMultiple Windowsが使えるようになれば使い勝手がとても良くなるのは間違いありません。WEARであれば、コーデの検索画面と詳細画面を一度に確認できたり、お気に入りフォルダの中身を確認しながらコーデを追加できたりと、アプリを巡るスピードが飛躍的に上がりましたので。 最後に、Multiple Windows機能をWEARに仮実装して感じたポイントを3つにまとめました。 Multiple WindowsのUIの実装はそれほど難しくない。ただし、1つのViewControllerを複数のウィンドウで開くことになるので、UIの更新やデータの保存には注意する必要あり。 NSUserActivityに必要な情報を渡し、それを用いて新しいウインドウを開く。ただし、NSUserActivityに渡せるタイプは限られているので注意すること。 AppDelegateのライフサイクルメソッドは使えなくなるので、SceneDelegateのライフサイクルメソッドを用いる必要がある。 この記事がMultiple Windows実装の一助になれば幸いです。 参考 Introducing Multiple Windows on iPad - WWDC 2019 - Videos - Apple Developer Architecting Your App for Multiple Windows - WWDC 2019 - Videos - Apple Developer Supporting Multiple Windows on iPad | Apple Developer Documentation Multiple Windows - System Capabilities - iOS - Human Interface Guidelines - Apple Developer さいごに ZOZOテクノロジーズは、iOSエンジニアを募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
こんにちは、開発部SREの田島です。私達のチームでは、JP1というツールをワークフローエンジンとして利用しています。JP1のジョブの監視は今まで外部にお願いしていましたが、異常が発生してからすぐ連絡してもらうことができない等の問題がありました。そこでJP1の機能とClojureで作成したスクリプトを組み合わせて異常検知の仕組みを作成しました。本記事ではJP1を使っている背景、並びに異常検知の仕組みをどのように実現したかについてご紹介します。 背景 JP1利用の背景 冒頭でも紹介しましたが私達のチームでは日立製作所が開発・発売しているJP1というツールをワークフローエンジンとして利用しています。多くのチームではDigdagやGCPのCloud Composerが主にワークフローエンジンとして採用されています。 実際私達のチームでもDigdagをフル活用しており、その運用の知見も得られているためワークフローエンジンはDigdagに集約するように動いています。そんな中JP1は約9年前に導入され様々なワークフローがその上で組まれています。そのため簡単にはDigdagに移行できません。 さらにJP1で稼働中のシステムは私達SREチームではなく別のチームが管理しているという状態でした。そこでまずはJP1の運用をSREチームで引き継ぎ、JP1自体の仕組み並びにJP1上で動いているシステムを理解することからはじめました。 JP1ジョブの異常検知 SREチームで運用を引き継ぐため、まずアラート対応に参加しました。そこで早速問題が発生しました。私達のチームでは基本的にSlackでのアラートをフックに当番がそのチャンネルにログを残しつつ対応するというフローをとっています。 しかしJP1のアラートは監視を外部にお願いしており、人間が異常かどうかを判断して電話またはメールで運用担当者に連絡してもらうというフローとなっていました。人間が異常かどうかを判断して連絡してもらうというフローのため異常が発生してから連絡まで最大で30分〜60分かかることがありました。 そこでまずは機械的にJP1の異常を検知し、Slackへ通知することにしました。 JP1について まずSlack通知をする上でJP1がどのような状態になったら通知するかについて検討する必要があります。しかしJP1を触ったことがまったくなかったので、最初にJP1を使った「Hello World」に挑戦しました。 ジョブネット まずはじめに、JP1にはジョブネットという概念があります。ジョブネットとは後ほど説明するジョブを集めたものです。GUI上でジョブをどのような順番で実行するかと言ったDagを作成できます。 ジョブ 次にジョブです。JP1では1つ1つの処理がジョブと呼ばれています。ジョブは以下のような画面から設定でき、実際に実行するコマンドやログの出力先などを指定できます。 以上は、hello.batを実行するような設定になっています。batファイルの中身は以下のとおりです。 @echo off setlocal enabledelayedexpansion cd %~dp0 echo "hello world!" ジョブネットグループ 最後にジョブネットグループです、これはジョブネットをまとめてグループ化する概念となっています。よって階層的には「ジョブネットグループ ∋ ジョブネット ∋ ジョブ」となります。 ジョブネットの実行 最後に設定したジョブネットを実行してみます。実行するには「実行を登録」という機能を使って即時実行、またはスケジューリング実行することが可能です。今回は即時実行してみます。実行の結果以下のようなログが指定したファイルに吐き出されました。 hello world! 以上でJP1でのHello Worldが成功しました。 ここからJP1での異常を検知するには、ジョブネットが異常終了した時Slackに通知すればよいことがわかりました。またジョブネットが正常終了した場合にも別のSlackチャンネルに通知できれば便利かなと考えました。 ジョブネットの異常検知 次にジョブネットが失敗した場合にどのようにSlack通知をするかを考える必要があります。ジョブが失敗した場合に外部へ通知するという機能があればいいのですがそのような機能はありませんでした。 そこで、JP1のイベントをきっかけにジョブを実行する機能があるのでそれを利用することにしました。 JP1イベント JP1はJP1上で起きたイベントをフックにしてジョブを起動する機能があります。イベントの一覧は こちら のリンクにまとめられています。 今回は以下のイベントが利用できそうです。 ジョブネット異常終了イベント ジョブネット正常終了イベント ジョブネット開始遅延イベント ジョブネット終了遅延イベント JP1のイベントをきっかけにジョブを実行する JP1では通常のジョブとは別にイベントジョブといった特殊なジョブをdagの中に組み込むことができます。以下がそのジョブと設定できる項目です。 紹介したリンクのイベントIDを指定することで、IDに紐づくイベントが発生したタイミングでジョブが実行されます。後続に通常のジョブを設定しておくことで、特定のスクリプトをあるイベントが発生したタイミングで実行が可能となります。ただしこのやり方だとジョブネットが実行中のタイミングでしかイベントを検知できません。そのためイベントを1回だけしか検知ができません。 JP1イベントのたびにジョブネットを実行する 以上の問題を解決するために、イベント受信監視という機能を利用します。イベント受信監視を利用するには「起動条件の設定」を利用します。 「起動条件の設定」を作成すると以下のようなジョブネットの条件フィールドが通常のジョブネットと並列に作成されます。フィールドにイベントジョブを並べてやることで、イベントが発生する毎に並列のジョブネットが実行されます。 条件フィールドの中のイベントはOR条件またはAND条件を指定することが可能です。下記ではOR条件となっているため、指定したイベントの中の1つでも当てはまればジョブが起動します。 実行登録時に、登録方法を「即時実行」起動条件を「設定されていれば使用する」に指定することで監視が開始されます。ジョブネットの状態が監視中となっていればイベントが監視されている状態となっています。 最終的なJP1の監視設定 それでは実際に監視ジョブを作成してみます。まず以下のように監視用のジョブネットを作成し、イベント監視を含むジョブを作成しました。 また、発火するイベントは以下のように設定しました。同じ条件が2つあることについては後ほど説明します。 環境変数の受け渡し イベントから発火されたジョブでは こちら のリンクに記載されているパラメータを利用できます。 しかし、このパラメータはJP1上でのマクロ変数として渡されます。マクロ変数を環境変数に変換することで起動されるスクリプトの中で利用できます。 「最終的なJP1の監視設定」の中の環境変数という項目でマクロ変数を環境変数に変換しています。以下のようなフォーマットで環境変数に指定することで、マクロ変数の値を環境変数に指定できます。 環境変数名=?AJS2マクロ変数? 無限イベント検知問題 JP1では発生したイベントに対して、イベントメッセージを正規表現のマッチングにより絞り込むことができます。絞り込みを利用しないと、イベントからキックされたジョブ自体も監視の対象となるため無限に正常終了の通知がSlackに流れてしまうという問題が発生しました。メッセージを確認すると、必ずジョブネットグループの名前が含まれていることがわかりました。そこで、監視用のジョブネットグループ「MONITORING」を作成し、イベントの文言に「MONITORING」が含まれていないという条件にすればいいと考えました。しかし、否定がJP1の正規表現では使えませんでした。 そこで最終的に、監視したいジョブネットグループの名前が含まれている場合にのみイベント発火を行うことで無限イベント検知を回避しました。また、JP1の正規表現ではOR条件も利用できないためイベント毎にジョブネットの数だけイベントジョブを用意してやる必要があります。 先程の例で2つずつイベントジョブが定義されていたのもこれが理由です。 実際の設定ではイベントジョブのメッセージの部分に正規表現で条件を書きます。以下はメッセージにTEST_WORKが含まれている場合にイベントを発火することを表しています。 Slackへの通知スクリプト 言語選定 イベントから発火されたジョブでは こちら のリンクにあるパラメータを環境変数に渡すことで利用できると紹介しました。それらの情報を利用して文言を組み立てSlackに通知します。 上記を実現するためにはスクリプトを作成する必要があります。 P1はWindows上で動作しているため、VBScriptやPowerShell・Windowバッチしかデフォルトでは利用できません。しかし、これらの言語は使い慣れていないかつ、複雑になりそうだったため採用しませんでした。 調べてみると、JP1から呼ばれているジョブはJavaで作成されていることがわかりました。よってJVM言語であれば追加でライブラリをインストールせずに利用できます。そこでJVMで動く言語を利用してスクリプトを作成することにしました。 Clojureの採用 JVMで動作する言語の中で以下の理由からClojureを採用しました。 独立した小さなスクリプトである 普段業務で使っていない言語である 普段使っている言語とパラダイムがぜんぜん違う 筆者がClojure好き まず今回作成するスクリプトは独立した小さなものなので、新しい技術スタックを試すには絶好のチャンスと考えました。 チームの中には新しい言語に触れるという事に抵抗がある人もいます。そこで、普段業務で使っていない言語を利用することでチームメンバーへの刺激になると考えました。 またClojureはS式や関数型といった特徴を持っています。このような普段使っている言語とはパラダイムがぜんぜん違う言語を使うことでチームメンバーへの新たな発見につながるのではと考えました。 そして何よりも筆者がClojure好きというのも採用に大きく影響しています。 Clojureを採用するデメリットとしては他の人が運用できず負債になるかもしれないというものが挙げられました。しかし、小さなスクリプトなので最悪まるっと別言語で書き換えられる、いつかはDigdagにワークフローエンジンを移行したいという理由から新しい技術スタックに挑戦することを優先しました。 ソースコード 以下がJP1からキックされるbatファイルと、batファイルから実行されるClojureスクリプトになります。 Clojureは Leiningen というツールで管理しています。 notification.bat @echo off setlocal enabledelayedexpansion cd %~dp0 echo "event" java -jar F:\JP1Notification\jp1-notification\target\uberjar\jp1-notification-0.1.0-SNAPSHOT-standalone.jar project.clj ( defproject jp1-notification "0.1.0-SNAPSHOT" :dependencies [[ org.clojure/clojure "1.10.0" ] [ clj-http "3.10.0" ] [ org.clojure/data.json "0.2.6" ]] :main ^ :skip-aot jp1-notification.core :target-path "target/%s" :profiles { :uberjar { :aot :all }} :plugins [[ lein-shell "0.5.0" ]] :shell { :env { "EVMSG" "KAVS0262-E ジョブネット(STK-MGR:/TEST_WORK/TAJIMA_TEST_HELLO:@P2944)が正常終了しました" "EVID" "4102:0" "EVDATE" "2019/10/23" "EVTIME" "15:32:29" "EVHOST" "hostname" "EVIPADDR" "192.168.111.111" }}) core.clj ( ns jp1-notification.core ( :gen-class ) ( : require [ jp1-notification.slack : refer [ send -to-slack ]] [ jp1-notification.event : refer [ event ]])) ( defn -main [& args ] ( send -to-slack ( event ))) event.clj ( ns jp1-notification.event ) ; event: http://itdoc.hitachi.co.jp/manuals/3020/30203S0733/AJSJ0164.HTM (def error-event-list [ "4107:0" "4123:0" "4127:0" ]) (def jobnet-group-production [ "Application" ]) ( defn get -jobnet-group [ message ] ( second ( re-find ( re-pattern "STK-MGR:/(.+?)/" ) message ))) ( defn get -event-level [ event-id ] (if ( .contains error-event-list event-id ) :error :info )) ( defn production? [ jobnet-group ] ( .contains jobnet-group-production jobnet-group )) ; env: http://itdoc.hitachi.co.jp/manuals/3020/30203K2143/AJSF0177.HTM ( defn event [] (let [ message ( System/getenv "EVMSG" ) event-id ( System/getenv "EVID" ) jobnet-group ( get -jobnet-group message )] { :event-id event-id :message message :date-time ( str ( System/getenv "EVDATE" ) " " ( System/getenv "EVTIME" )) :host ( System/getenv "EVHOST" ) :ip ( System/getenv "EVIPADDR" ) :jobnet-group jobnet-group :event-level ( get -event-level event-id ) :production? ( production? jobnet-group )})) slack.clj ( ns jp1-notification.slack ( : require [ clj-http.client :as client ] [ clojure.data.json :as json ])) (def slack-url "https://hooks.slack.com/services/xxxxxxxxxxxxxxxxxxxxxxxxxxxxx" ) (def production-channel-alert "#alert_cahannel" ) (def production-channel-notice "#notice_channel" ) (def test -channel "#test_channel" ) (def info-slack-title "[INFO] From JP1" ) (def error-slack-title "<!channel>\n[ERROR] From JP1" ) ( defn channel [ jobnet-group event-level production? ] (if -not production? test -channel (if ( = event-level :error ) production-channel-alert production-channel-notice ))) ( defn title [ event-level ] (if ( = event-level :error ) error-slack-title info-slack-title )) ( defn color [ event-level ] (if ( = event-level :error ) "danger" "good" )) ( defn payload [{ : keys [ event-id message date-time host ip jobnet-group event-level production? ]}] { :channel ( channel jobnet-group event-level production? ) :username "JP1 Event" :attachments [{ :color ( color event-level ) :title ( title event-level ) :text ( str "```" message "```" ) :fields [{ :title "DateTime" :value date-time : short true } { :title "Host" :value ( str host " (" ip ")" ) : short true }]}]}) ( defn send -to-slack [ eve ] ( client/post slack-url { :form-params { :payload ( json/write-str ( payload eve ))}})) 開発時の環境変数 Clojureのスクリプト開発の際にも今回1つ工夫をしています。開発時でもJP1から渡される環境変数を擬似的にClojureに渡してやる必要があります。そこで、今回は lein-shell というライブラリを使うことでそれを実現しています。 まずproject.cljの plugins に lein-shell を追加します。そして以下の行を追加し、実行コマンドの前に lein shell を付与してやることで環境変数がセットされます。 :shell { :env { "EVMSG" "KAVS0262-E ジョブネット(STK-MGR:/TEST_WORK/TAJIMA_TEST_HELLO:@P2944)が正常終了しました" "EVID" "4102:0" "EVDATE" "2019/10/23" "EVTIME" "15:32:29" "EVHOST" "hostname" "EVIPADDR" "192.168.111.111" }} 通知結果 実際の通知は以下のようになりました。上が異常終了時、下が正常終了時の通知です。期待通り通知されていることがわかります。 異常検知を自動化してどうなったか 最後に異常検知を自動化しSlack通知にすることで以下のようなメリットが見られたので紹介します。 異常の発生にすぐに気づくことができるようになった 今どのようなアラートが発生していて誰が対応しているのかがわかりやすくなった 過去のアラートが検索できるようになった JP1自体に詳しくなった アラート対応についての現状の見直し・改善のきっかけとなった まずJP1のジョブの異常が発生してから連絡が来るまでに時間がかかるという問題がありました。この問題は自動検知の仕組み導入により、リアルタイムで異常に気づくことができるようになりました。また、Slackにすべてのアラートが集約されるようになったため、対応のログがすべてSlack残ります。これにより今誰がアラート対応をしているのか、過去にどんなアラートが起こったのかを追うのが簡単になりました。 またこれは副作用ですが、JP1に異常の自動検知の仕組みを入れるためにはJP1自体を詳しく知る必要がありました。これによりシステム改善の第一歩としての現状把握に大きな貢献となりました。 そして何よりも、これをきっかけにチームでのアラート対応についての見直し・改善のきっかけに繋がりました。 今後の展望 アラートの異常検知を自動化することで様々なメリットが得られました。しかしまだまだ問題はあります。今問題となっているのは、夜中の大きなアラートに気づけないというものです。それを解決するための1つとしてPagerDutyの導入を検討してます。 またアラート対応者が誰かに偏っている、知見が誰かに偏っているといった問題があります。これについては、当番制度の見直しなどをチームで日々話し合い改善を行っています。 まとめ 以上で見たように、弊社には改善すべきことがたくさん眠っています。自分の技術スタックとはぜんぜん違う領域に対しても、積極的に挑戦し改善していけるようなエンジニアを募集しています。 tech.zozo.com あと、Clojureのコードレビューもお待ちしております。
こんにちは、開発部SREチームの西郷です。普段は生産管理のシステムやWEARのインフラに携わっています。弊社では多数のサービスでAWSを採用しており、更なる利活用と学びのため、12/2〜12/6に開催された AWS re:Invent 2019 へ参加してきました。本記事では、Keynoteの内容に加え、一緒に参加した開発部のメンバーが気になったSessionやWorkshop等についてもレポートしたいと思います。 AWS re:Inventとは Keynote KYN201: Monday Night Live KYN202: Keynote by Andy Jassy KYN203: Global Partner Summit Keynote KYN204: Keynote by Dr.Werner Vogels Session SEC207-L: Leadership session: AWS identity CMY303: Using Amazon CloudFront, AWS WAF, and Lambda@Edge to keep spammers out MOB317: Speed up native mobile development with AWS Amplify CMP323-R: [REPEAT] Optimize compute for performance and cost using AWS Compute Optimizer Workshop MGT305: Patch Compliance Using AWS Systems Manager And AWS Config AIM223-R6 - AWS DeepComposer: Get started with generative AI まとめ AWS re:Inventとは 1年に1回ラスベガスで行われるAWS最大のカンファレンスで、8回目の今年は65,000人以上が参加しました。AWSの新サービスや既存サービスのアップデート発表が目玉ではありますが、3,000以上の技術セッションやワークショップ、ハッカソン等が用意されている学習型カンファレンスです。6つのホテルで構成された会場が"Campus(キャンパス)"と呼ばれていることからも、学習の場として強く位置付けられていることがわかります。 Keynote KYN201: Monday Night Live 新規事業部の茨木です。専門はフロントエンドですが、プラットフォームにとらわれずプロトタイプ開発や技術検証に取り組んでいます。最初のセッションにあたるこのKeynoteでは、Amazon社の大規模計算に関する取り組みが紹介されました。大規模計算を行うスーパーコンピュータは専用のネットワークにより接続された複数のサーバーから構成されます。複数台のサーバーによる大規模な計算を高速・安全に実行するための具体的な取り組みとして、以下が紹介されました。 サーバー間ネットワークの改善 EC2の仮想化に最適化されたハードウェア(Nitro Controller)の導入 アプリケーション通信の改善(EFA) 現状、弊社でスーパーコンピュータを利用する場面はありませんが、機械学習などでは分散コンピューティングが登場します。この発表で紹介されたネットワーク改善のアプローチは、機械学習における計算速度の向上に寄与するかもしれません。 KYN202: Keynote by Andy Jassy SREチームのテックリード光野です。普段は、弊社におけるAWS利用のフォローやアーキテクトをしています。re:Inventでは例年3回のKeynoteが開催されますが、その中でもDay2はAWS CEO Andy Jassyが登場し、会期中もっとも盛り上がる舞台の1つです。今年も大変多くの、そして大きな発表が行われました。 まず、例年以上にデジタル・トランスフォーメーションについての言及が多かったように感じました。エンタープライズにおけるオンプレミスからクラウドへの移行、そして「オンプレミス」の壁を壊すためのリリース報告が行われました。 弊社にも倉庫という巨大な資産があり、その中で動くシステムがまた存在します。ハードウェア・ソフトウェア両面で、耐用年数を迎えれば入れ替える必要があるものの、一般的なWebサービスと同じ手段で入れ替えることはできません。 AWSのインフラをオンプレミスに持ち込むOutposts、リージョンの枠を超え地理的に近い場所へAWSを展開するLocal Zones、5GのエッジコンピューティングとなるWavelength。オンプレミスとクラウドの垣根をなくすプロダクトがあることは、将来の選択肢が増えるという点で、とてもうれしく思います。 そして、昨年に引き続き機械学習周りのリリースが怒涛のように行われました。IDEのSageMaker Studio、実験管理を行うSageMaker Experiments、モデルを決定づける要素を調査するSageMaker Debuggerなど。Keynote外での発表も含めればSageMakerだけで10近い新サービス・機能が発表されているのではないでしょうか。弊社はAWS Summit Tokyo 2019にて SageMaker利用における工夫 を発表させていただきましたが、殆どがマネージドで提供されてしまったのではと感じるほどのアップデートです。 最後に、機械学習などに比べれば一見地味ではあるものの、Nitro Systemによるネットワークの高速化やAQUA for Amazon Redshiftによるスループットの改善など増大するデータを正しく快適に扱う下支えについての発表が何よりも魅力的に映りました。 今後もユーザーが必要とするサービス・機能、そしてユーザーを快適にする改善の両面で目が離せないと感じさせる、そんなKeynoteでした。 KYN203: Global Partner Summit Keynote SREチームの指原です。3日目のKeynoteはAWSのHead of Worldwide Channels & AlliancesであるDoug YeumによるGlobal Partner Summitです。本KeynoteではAWSが支える、そしてAWSを支える数多くのパートナー企業に関して事例の紹介とアップデートがありました。 事例としてエネルギー事業を展開しているBP社と、レンタカー事業を展開しているAvis Budget Group社がそれぞれ登壇しました。両社は自社の大規模なシステムをオンプレミスからクラウドへ移行しています。また、単に移行するだけではなく例えばAmazon SagaMakerなどAWSのサービスをフルに活用することで、システムパフォーマンスの向上に成功しているそうです。そのようなダイナミックなデジタルトランスフォーメーションを実現できたのはAWSが認定している技術支援を行うパートナーネットワークのおかげだということでした。 弊社では事例にあったような技術支援を行うパートナー企業との取り組み自体は行っていません。しかし、社内ではZOZOTOWNやWEARのリプレイスという大きなプロジェクトが進んでおり、そのようなプロジェクトではオンプレミスからクラウドへの移行も含んでいます。そこでは事例にあったようにオンプレミスのサーバーを単にEC2に移行するようなことではなく、実際にEKSやFargateを使って再構築を行い従来より柔軟なスケールアウトや運用負荷の低減が実現しています。クラウド移行の際にはサービスをフル活用して効率化を図るという点が共感したところでした。 また、パートナーネットワークについては普段あまり意識したことはなかったのですが、多くの大企業が既に抱えているオンプレミスやレガシーなシステムをクラウドに移行する支援だったり、スタートアップが成長を加速するための支援だったり、多くのパートナー企業が存在しており、AWSが持つプラットフォーマーとしてのエコシステムの巨大さに改めて気付かされました。 KYN204: Keynote by Dr.Werner Vogels SREチームの西郷です。4日目に行われたこのKeynoteはAmazon社のCTOであるDr.Werner Vogelsが登場し、AWSのサービスを技術的に支える部分やAWSがこれまで行ってきたアーキテクチャへの投資についてがトピックでした。 仮想化の観点では、新しいハイパーバイザであるAWS Nitro Systemによるパフォーマンス向上の話がありました。Nitro Systemを搭載したC5系のインスタンスはベアメタルに近いパフォーマンスを発揮できるそうです。また、re:Inventの前半で発表された Nitro Enclaves はNitroを使用して、AWS KMSと統合したローカル接続のみに隔離された領域を提供します。つまり、EC2インスタンス内で個人情報等の機密性の高いデータを保護し、安全に処理できるようになります。 サーバーレスの観点では、FargateやLambdaの裏側で動いているFirecrackerの話がありました。Firecrackerは昨年のre:Inventで発表されたマイクロVMで、セキュリティはもちろん速度にもフォーカスして設計されています。今回のre:InventでFargate on EKSが発表されましたが、EC2と比較して軽量なFargateが使えるようになったことで高負荷時のオーバープロビジョニングが大幅に軽減されると説明がありました。 弊社サービスでもEC2やLambda、Fargateを採用しているため、使える技術の選択肢が増え、より安全で高パフォーマンスな環境になることは非常に嬉しく楽しみです。 また、新しいサービスとして The Amazon Builders' Library が発表されました。これまでAWSが自社のサービスを開発・運用する中で培ってきたナレッジのライブラリで、どれもシニアテクニカルリーダーが書いているそうです。より簡単にそういったテクニカルな情報にアクセスできるようになったことは開発者としてありがたく、早速目を通してみたいです。 最後に、本KeynoteでもAWSを使ってビジネスに変革を起こそうとしている企業の取り組みがいくつか紹介されたのですが、Volkswagen社のThe Volkswagen Industrial Cloud(工場のクラウド化プロジェクト)はその規模の大きさに目を惹かれました。グローバル全体で1つのアーキテクチャのクラウドにあらゆる生産工程を接続させる構想で、いずれは他の企業にも公開しOSSのエコシステムのようなものにしたいとのことでした。 全体的には、ゼロトラストネットワークの概念に法って開発されたAWS Nitro SystemやIndustry 4.0を意識したVolkswagen社の取り組みなど、クラウドの新たなフェーズを感じられるKeynoteでした。 Session SEC207-L: Leadership session: AWS identity 改めまして、SREチームの指原です。昨今、ゼロトラストネットワークというセキュリティの概念が広がってきており、個人情報の観点でもグローバルなサービスではGDPRなど法的に対応する必要性が出てきたりしています。そのようにインターネットやクラウドの世界でもセキュリティや情報保護に関して次々と大きな変化が起きつつあると感じています。このセッションではそのような変化に対応するためのAWSでのアクセス制御、リソース管理について新機能の紹介があるということで参加しました。 本セッションの内容をサマライズすると主に以下の4つについてでした。 AWS Organizationsを利用することで、複数のAWSアカウント全体の一元管理が可能 AWS SSOを利用することでユーザーアクセスの集中管理が可能 タグを利用することで請求の管理ができるだけでなく、セキュリティポリシーやアクセス制御の管理が可能 新サービスのIAM Access Analyzerを利用することで不必要なアクセス制御の許可を減らすことが可能 いくつか小さなアップデートと共に今回新しくリリースされた機能としては4つ目のIAM Access Analyzerが目玉として紹介されていました。IAM Access Analyzerは現在以下の5つのリソースに対応しており、これらのリソースに対して意図した通りのアクセス制御になっているかを分析ができます。 S3 IAM Role KMS Lambda SQS 特徴としては細かい設定をする必要がなく簡単に有効化でき、利用料金も必要ないという点です。また、IAM Access Analyzerはパターンマッチングなどに基づいて分析を行っているわけではなく、自動推論と呼ばれる数学的分析を使っているそうです。その結果、数千ポリシーを数秒で分析ができるパフォーマンスを発揮できるようです。 IAM Access Analyzerが対応している上記の5つのリソースはそれぞれ誤った設定をすると不要な外部公開をしてしまう可能性のあるサービスです。そのような誤った外部公開設定をしていないかどうかを重点的に確認できる機能がIAM Access Analyzerです。 弊社では数十のAWSアカウントを管理しており、GuardDuty等を利用して全アカウントを対象にしたセキュリティ的な脅威検知については担保しています。しかし各リソース単位での細かいセキュリティポリシーに関しては基本的には利用プロダクトに運用を一任しています。そのような事情から個別のリリースに対して外部公開設定されていないかどうをチェックすることが簡単にできるようになったことは非常に便利で、さっそく活用していきたいです。 CMY303: Using Amazon CloudFront, AWS WAF, and Lambda@Edge to keep spammers out 改めまして、SREチームの西郷です。このセッションは、あるWebサイトでユーザー登録数がスパイクしたケースにおけるCloudFront、WAF、Lambda@Edgeを使った対処法の紹介だったため、参考にできるものがあればと思い参加しました。 まとめると以下の内容でした。 前提 スパイクしたのは非営利団体のシステムで、ブログのようなもの(ユーザーがコンテンツを追加したりコンテンツにコメントしたりする) ユーザー登録のプロセスはメールアドレス認証。メールアドレスとキャプチャを入力、オプションで質問に対する答えを入力する 当初のインフラ構成はユーザーからの窓口としてCloudFront、後ろにサーバーのEC2とエラーページや静的ファイルが入ったS3バケット 起こったこと ユーザー登録数がスパイクした(通常は1日1回もしくは2、3日に1回の頻度だったが、1分に1回になった) ユーザー名とメールアドレスが普通じゃなかったので怪しいものだと気づいた 実際に行った対処 ほとんどがロシアまたはウクライナからのアクセスだったので、まずはCloudFrontで地域制限をかけた 地域制限で弾けなかったIPアドレスからの攻撃はWAFのセキュリティオートメーションで提供されているIP Reputation Listsで弾くようにする それでも弾けないものは怪しいIPアドレスかどうかをLambda@Edgeで判定し、怪しい場合は303を返してセキュリティオートメーションで提供されているBad Botのブラックリストに追加する その結果 2回目以降のアクセスはブロックされるようになり、スパイクは99%収束した Bad Botのブラックリストには1341個のIPアドレスが追加された(IP Reputation Listsで防げなかった数) このSessionで知見になったのは、AWS Solutionsで提供されている AWS WAF セキュリティオートメーション の存在です。これは一般的なWebベースの攻撃をフィルタリングするように設計されたWAFのルールセットを自動的にデプロイできるもので、CloudFormationで提供されています。今回のセッションで取り上げられた2つのコンポーネントの機能を調べたところ、以下のようなものでした。 IP Reputation Lists: CloudWatchをトリガーにしてLambdaがサードパーティのIP評価リストを定期的にパースし、WAFのIPリストを更新する Bad Bot: API GatewayとLambdaを使ったハニーポットで、Lambda FunctionがIPアドレスを検出するために検査を行いブロックするIPリストに追加する AWS WAF セキュリティオートメーションというソリューションが提供されていることや、AWS Solutionsというサービスがあることを知らなかったので参考になりました。弊社でもWAFを使用していますが、事前にブラックリスト等を設定しておく、ということはしていないため、ブロックしたいIPアドレスが発生する毎に手動で対応を行なっています。あらかじめブラックリストをルールに適用しておくと、より安全&運用コストの削減に繋がりそうだと感じたので、機会があれば活用していきたいです。 MOB317: Speed up native mobile development with AWS Amplify フロントエンドエンジニアの権守です。AWS Amplifyについては社内で導入を検討したことがなかったので、今後の選択肢の1つとして考えられるように使い方を学べるこのセッションに参加しました。 AWS Amplifyは、モバイルアプリケーション向けにバックエンドを提供するものです。API・認証・機械学習を使った予測などの機能から必要なものだけを選んでバックエンドを構築・利用できます。また、iOS・Android・Web・React Nativeアプリ向けにフレームワークを提供しており、UIコンポーネントや各機能を呼び出すインタフェースが用意されています。 セッションの内容はデモを中心としたもので、Amplify CLIを使ったGraphQL APIの構築や、iOS・AndroidのSDKを使った実装を見ることができました。具体的な実装としては、認証画面の呼び出し、機械学習による翻訳・ラベル付けを見ることができました。 こちらは画像から写っているものを特定して、ラベルとその信頼度を出力しているものです。 セッションを通して、利用のイメージが湧いたのでGraphQLを利用するプロトタイピングなどで試してみたいと思います。モバイル開発向けのAWS公式ブログにも AWS Amplifyの導入記事 が公開されたので興味の湧いた方には併せて読むことをお勧めします。 CMP323-R: [REPEAT] Optimize compute for performance and cost using AWS Compute Optimizer バックエンド・SREチームのテックリード児島です。ZOZOSUITS及びZOZOMATの計測システムのアプリケーション構築及びアーキテクチャ設計を担当しています。 re:Inventには昨年から参加し、今年で2年目となります。例年、EC2には様々なアップデートがあり、今ではその選択肢は270以上にもなるそうです。そのような状況下で、アプリケーションに最適なインスタンスを選定する知識は、年々複雑化しています。今年も開催の1か月前には、Savings Planの発表がありましたが、これに合わせ節約術のベストプラクティスもまた年々多様化していると言えます。re:Inventでは、その節約術提供のためCost Optimizingをテーマとしたセッションが毎年いくつか用意されており、現場で活用できる知見を深めることができます。昨年も、以下のセッションに参加しました。 「 Run Production Workloads on Spot, Save up to 90% (CMP306-R1) 」 「 Amazon EC2 T Instances – Burstable, Cost-Effective Performance (CMP209) 」 CMP209では、本番環境にも積極的にT系インスタンスを採用することを勧める内容で、とても興味深いものでした。 さて、CMP323-Rでは、Cost Explorerに今夏追加されたResource Optimization Recommendationsと比較して、新機能であるAWS Compute Optimizerが紹介されていました。Cost Explorerでは同一のインスタンスファミリアの中からしか最適なインスタンスタイプの推奨がなされませんでした。 これに対し、AWS Compute OptimizerではM/C/R/T/Xの5つのインスタンスファミリアを跨いで、140以上の組み合わせの中から最適なインスタンスタイプが推奨されるそうです。推奨にはMachine Learningが使われており、CPU使用率、ディスクI/O、ネットワークI/Oなどのメトリクスから、より精緻な個別の状況に応じた推奨がなされるそうです。現在は、東京リージョンでの使用は開始されていないようですが、今後のアップデートが待ち遠しいです。 実際の現場において、これまでも新しいインスタンスタイプが追加される度に、利用者がその採用によるパフォーマンスを評価することはとても大きな負担だったと思います。それにより導入が遅れたり、その評価に多大な時間が奪われるなど、大きな課題を生んでいたように思います。しかし、今回のAWS Compute Optimizerは、これらの問題を解決してくれるツールとなり得そうです。今後の益々の拡張に大きな期待をしたいと思います。 Workshop MGT305: Patch Compliance Using AWS Systems Manager And AWS Config 再び光野です。1エンジニアとしては、開発速度の底上げを図れるようなデプロイやガバナンスの仕組み作りに携わっています。re:Inventでは手を動かすWorkshop形式のセッションばかり回っており、ここで取り上げるMGT305もWorkshop形式です。課題の1つであるパッチの運用を解決すべく参加しました。常々パッチの管理と適用を自動化したいと考えており、糸口でも見つかれば、との気持ちでの参加でしたがまさにこのセッションが答えをくれました。Systems Manager(SSM)を組み合わせます。 EC2インスタンスにSSMエージェントをインストール 管理用タグを付ける SSM Inventoryにインスタンスの状態が蓄積される SSM State Managerでインスタンスとパッチの有無を関連付け SSM Patch Managerでインスタンスにパッチを適用 3と4のオプション項目で実行日時を設定(自動化) 残念ながら時間切れでAWS Configまでは進めませんでしたが、こちらでは各インスタンスが有効なSSMエージェントを持っているかを継続的に確認する仕組みが説明されていました。 SSMは、その中に多数の機能が含まれており敬遠しがちだったのですが、実際に触ってみると非常に強力なツールでした。今後は、もっと積極的に触っていこうと思っています。 なお、パッチ適用に際して、気をつけなければならないのが再起動です。パッチによってはインスタンスの再起動が発生します。サポートしてくれたSAに聞いたところ、事前の予告や状態遷移 1 は無いため、Patch Managerに指定する日時の前にサービスアウトしておく事が必要とのことです。大事なプロセスが強制終了されないよう、導入の際にはケアをしてあげてください。 AIM223-R6 - AWS DeepComposer: Get started with generative AI 改めまして、新規事業部の茨木です。日曜日のMidnight Madnessで発表されたDeepComposerを実際に体感してきました。このWorkshopは大変人気で、開始の3時間前から並んでやっと入ることができました。 今回体験したDeepComposerは、キーボードで入力したメロディに対して自動で伴奏を生成してくれるGenerative AIです。GANと呼ばれる教師なし学習を用いており、クラシックやジャズなどのジャンルに応じたモデルが予め用意されています。Workshopでは初めにDeepComposerの講義があり、DeepComposerを構成するインフラストラクチャや機械学習を学びました。 その後、実際にキーボードを用いてメロディを入力し、DeepComposerを使って様々なジャンルの曲を作りました。 メロディによってはあまり上手くいかない場合もありますが、ハーモニーやリズムはジャンルをよく表現している印象でした。実際に会場で作った曲をSoundCloudに上げたので、是非聴いてみてください。 参加者にはDeepComposerのキーボードが配布されました。 教師なし学習によるGenerative AIのアプローチはファッション分野でもコーディネート生成などで活かせるかもしれません。 まとめ 再びSREチームの西郷です。今回初めてAWS re:Inventに参加したのですが、AWSのビジネスの大きさや今後の可能性を体感できた貴重な1週間でした。 AWSがクラウド市場でシェアを獲得している理由はサービスのバリエーションやその開発スピードにあると思っていたのですが、KeynoteやSessionに参加する中で、ユーザーが本当に求めているサービスは何かが常に考えられていること、それが実際にちゃんと形になることも支持される理由なのだと感じました。 今回得た知見を弊社サービスの提供に活かし、ビジネスの成長に繋げていきたいです。 最後に、ZOZOテクノロジーズではサービスをより良くしてくれる仲間を大募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 https://tech.zozo.com/recruit/ tech.zozo.com Spot Instanceの終了予告、Auto ScalingのTerminating:Wait相当 ↩
こんにちは! 開発部基幹SREチームの廣瀬です。 2019年11月4日から8日にかけてシアトルで PASS Summit 2019 が開催され、参加してきました。 初めての海外カンファレンスで少し緊張しましたが、得るものはとても多かったため、その内容をレポートしたいと思います! PASS Summit 2019 PASSとは「Professional Association for SQL Server」の略です。 SQL Serverに限らず、Microsoftのデータプラットフォームを使っているプロフェッショナルのための世界的なコミュニティのことをいいます。 PASS Summitは、毎年秋に開催される、Microsoftのデータプラットフォーム分野における世界最大級のカンファレンスです。 MicrosoftのエンジニアやMicrosoft MVP、及びPASSのコミュニティメンバー等が登壇し、合計約260セッションの中から興味があるセッションを聴くことができます。 11月5日のレセプションパーティと、6日から8日の3日間が本カンファレンスなのですが、4日と5日にプレカンファレンスというものが同時開催されます。こちらは約8時間のセッションを1日1つ受講する、というものです。 1つのテーマについて、一般セッションよりも深い話を聞くことができます。そのため、私はプレカンファレンスも合わせて丸々5日間参加してきました。 公式ページ によると、PASSは1999年に設立された、約20年の歴史があるコミュニティみたいです。 そのためか、カンファレンスの運営がとてもしっかりしており、初参加の人にも優しいカンファレンスであると感じました。 PASS Summitのここが素敵 公式アプリが充実 全日程のイベントやセッションのスケジュール、会場の情報等がアプリに集約されており、自分の興味があるスケジュールだけをピックアップしたMy Schedule作成機能があります。 私は事前に気になるセッションをMy Scheduleへ複数登録しておき、直前にどのセッションを受講するか決めて参加していました。 関連セッションを複数に束ねて紹介する「 Learning Pathways 」 参加者の興味分野に応じて全部で10種類のラーニングパスが用意されていました。これは例えば「AIについて学びたいなら、この3セッションを順番に受講するといいよ」というガイドです。 今年から導入された制度のようですが、受講セッションの選択時にとても参考になりました。 参加者同士の交流を促す仕組み 参加者同士の交流を促す仕組みが豊富だと感じました。 例えば、カンファレンス初参加者が集まって交流する「Fisrt-Timer Event」では6人1組でクイズに挑戦しました。 他にはアジア圏やヨーロッパ圏など、同じ地域の参加者同士で交流できる「PASS Community Zone」が用意されていました。 気になったセッション 全部で14セッションに参加してきたので、その中で特に気になったセッションをピックアップし、レポートしたいと思います。 Session: Bigger Hardware or Better Code and Design? Microsoft SQL Server 2012 Internals の著者の一人であるJonathan Kehayias氏によるセッションでした。 SQL Server観点で最適化を実施するために気をつけるべき点や使えるツールについて、8時間ほどの充実したセッションでした。 ハードウェアのパワーで解決できない問題はたくさんある 冒頭でのこの問いかけは、自分の考えと一致し、一気に話に引き付けられました。 インデックス不足や、遅いクエリの書き方、DBの設定不備などがあるとハードウェアをスケールアップさせても解決できないか、解決できたとしても金額面で折り合いがつきません。 これはクラウドのPaaSでSQL Serverを使ったとしても生じる問題であり、SQL Serverレイヤーでの最適化が必要、という主張は大いに納得できるものでした。 「Matching the Design to the Problem」 これは本当に名言だと思いました。 この考えは、アーキテクティングのレイヤーでも言えることだと思いますし、SQL Serverのレイヤーでも言えることだと思います。 SQL Serverの強力な機能であるIn-memory OLTPやColumnstore Indexにも一長一短あります。 そのため適用したい箇所の性質を把握していないと成功しないよ、という主張には大いに納得しました。 RDBMSに限らず、「問題を正確に把握すること」は、問題解決における基本であり最も重要なことではないでしょうか。 RBAR Processing を避ける 行単位での読み込みが大量発生するような処理をRBAR(row-by-agonizing-row) Processingというそうです。 日本だと ミック本 における「ぐるぐる系」でおなじみの概念かと思います。 カーソルを使って1行ずつ処理する方式はRBAR Processingの代表格ですが、他にもScalar UDFや相関サブクエリもRBARにあたるそうです。 「カーソルを使わずに一括で更新処理を行うべき」という講演者の主張に対し、「それってブロッキング起きませんか?」という会場からの鋭い質問が。 回答は聞き取れませんでしたが、「ロックエスカレーションが起きない程度のバッチサイズ(500行など)でループして処理」がプロダクション環境における現実的な対応策かと思います。 sp_WhoIsActiveが人気 sp_WhoIsActiveとは、デフォルトで用意されているsp_whoやsp_who2といったプロシージャの上位互換といったイメージのもので、無償公開されています。 こちら に使い方が紹介されていますが、海外だとみんな使っているようです。 以前から知っていたものの使っていなかったのですが、コミュニティ内で信頼を得ていることが分かり、改めて使ってみようと思えました。 拡張イベント使用時の注意点 拡張イベントは軽量なことで有名ですが、query_post_execution_showplanというイベントだけは、プロダクション環境で収集すべきではないとのことでした。 秒間1万バッチ実行中の環境でquery_post_execution_showplanイベントを収集開始した途端、バッチ実行数が半減するデモは興味深かったです。 みんなオーバーヘッドを気にしている これは本セッションに限ったことではないのですが、情報取得に伴う負荷について気にする質問が多く飛び交っていました。 「overhead?」と何回聞いたか分かりません。このセッションでは、クエリストアと拡張イベントに関する話のときに負荷についての質問が飛んでいました。 プロダクション環境での情報取得の細かさとサーバー負荷はトレードオフの関係にあるため、ここは全DBAが気にするところなのだなと感じました。 ヒント句は基本使わず、オプティマイザに任せる スピーカーのこの主張は同意です。 skewed data selectivityが一定でない性質をもつカラムを「skewed data」というそうです。 例えば、とあるステータスを持ったカラムがあり、100万レコード中10レコードだけがステータス=1で、それ以外は2であるような場合を考えます。 この場合、where句でステータス=1で絞り込んだ時と2で絞り込んだ時では最適な実行プランが違ってきます。 このような状況は日本でもみんな把握しているとは思うのですが、用語としては存在していない印象です。「skewed data」だけで伝わるのは良いなと思いました。 Session:パフォーマンス関連 パフォーマンスに関するセッションを複数受けまして、内容に重複があったので1つにまとめてレポートします。 Query Optimization Statistics: Driving Force Behind Performance Azure SQL Database: Maximizing Cloud Performance and Availability Query Store In-Depth Performance Tuning Azure SQL Database SQL Server 2019の新機能 2019の新機能は、普段DBAが遭遇する問題をきちんと捉えているなと感じました。 Microsoftの開発チームは、そうした問題の起きない世界をつくろうというヴィジョンを持っているのだと思います。 Memory Grand Feedback(MGF) ワークスペースメモリ確保の過不足があったときに、初回実行のあとに同様のクエリに対してメモリ確保量を調整してくれる機能です。 特定のクエリが必要以上にワークスペースメモリを確保してしまい、そのクエリが複数同時実行されることでRESOURCE_SEMAPHORE待ちが大量発生するというのはDBAあるあるかと思います。 これを自動で防ぐ機能がMGFになります。これは素直に強力な新機能だなと思いました。 Batch Mode On Row Store 解析系クエリにおけるCPUバウンドなクエリで、(作成におけるオーバーヘッドが大きすぎる等の理由で)カラムストアインデックスを作れない場合に有効な手段だそうです。 OLTPと解析系クエリが混在するサーバーで有効な機能だと理解しました。 Interleaved Execution for MSTVFs 今まではMSTVFs(Multi-Statement Table-valued functions)がクエリに入っていると、必ず基数推定が100行となっていました。 そのため実際の行数との乖離が大きい場合に遅い実行プランが生成されてしまう場合がありました。 2019では、MSTVFsの正確な行数を最適化のときに使用するため、基数推定精度が改善しているそうです。 つまりは、あるべき実行プランへと近づきやすくなっているということです。 Batch Mode Adaptive Joins (AJ) skewed dataがあるときに、駆動表のselectivityが分かるまで、hash join/innner joinの決定を遅延できる仕組み。 この機能があることで、nested loop+大量のキー参照によってCPU高負荷な実行プランが生成される、という問題を未然に防ぐことが期待できるのではと感じました。 最適化には非常にわずかなCPUおよび時間しか使えないため、基数を推定するしかありません。 この「推定」と「実際」のズレによるパフォーマンスへの影響をどれだけ柔軟に吸収できるかという観点での進化を感じました。 クラウド(SQL Database)におけるパフォーマンスチューニング SQL ServerでもSQL Databaseでも、起こりうる問題と調査手法、特定した原因の解決方法については王道が整理されている印象を受けました。 複数セッションで同じようなことを話されていたので、業界の主流を知れてよかったです。 SQL ServerおよびSQL Databaseのパフォーマンスチューニングやトラブルシューティングに使える機能として以下の5つを紹介されていました。 DMV XEvents QueryStore AutomaticTuning IntelligentInsights クラウド、オンプレで手法はほとんど変わらないが、IntelligentInsightsはAzure限定とのことでした。 使うツールも大事ですが、普段からbaselineを取得しておくことも大事だと話されていて、基本的なことですが重要だなと改めて思いました。 sys.dm_exec_session_wait_statsというDMVがあり、セッション単位で待ち事象の内訳が取れるためとても便利だなと思いました。使いたい。。! セッション中に「performance tuning skills = immediate cost savings in the cloud」という名言が出ました。 クラウドを使えば容易にスケールアップやスケールアウトできるためチューニングの必要性が薄れるというイメージもあるかもしれませんが、まったくそんなことはありません。 むしろ、クライドを使っている環境においては、チューニングスキルはより直接的に金銭的なコストカットというメリットをもたらす武器になると感じました。 Query Store 2016で導入されたクエリストアですが、解析におけるデフォルトの選択肢になっているようです。 私自身は拡張イベントに頼りがちなのですが、クエリストアについてのスキルも上げていきたいなと思いました。 クエリストアと拡張イベント、どちらを使うべきかという質問に対しては、「状況による」という回答でした。 ただし、「実行プランがいつ変わったか」という問いに答えられるのはクエリストアだそうで、これは確かにと思いました。 クエリストアのlimitationとしては、 ログイン名、ホスト名、アプリケーション名は記録されない 集約された情報だけで、個別クエリの詳細は省かれる 実際の行数といった、実行時の情報は記録されない といった点があげられます。 クエリストアに限った話ではなく、DMVやXEventsにも一長一短があるため、状況に応じてうまく組み合わせて調査することが大事なのだなと理解しました。 そしてそのためには、各ツールの長所短所をもっと理解する必要があると感じました。 Session: Relational Data Modeling Trends for Transactional Applications Microsoft MVPのIke Ellis氏によるとても挑戦的なセッションでした。 データモデリングに関するセッションはほぼ無かったので、こちらのセッションを受講しました。 パフォーマンスチューニングの知識やその上位レイヤーのアーキテクティングの知識も大事ですが、その間に位置づけられるモデリングの知識も大事だなと最近感じています。 「EF Codd's ideas are bit outdated」 EF Coddの考えは今日では少し古くなりつつある、という発言にはっとしました。 スピーカーによると、EF Coddの考えは例えば「ディスクはとても高価である」という当時の状況に基づいています。 そのため、正規化を実施することでデータの重複を防ぎ、ディスク容量を抑える狙いもあった、とのことでした。 それが今日ではディスクはとても安価であり、状況が異なれば最適な設計もまた違ってくるということだそうです。 他にも、「DBは全アプリケーションの中心にあるもの」という当時主流だった思想にも影響を受けているそうです。 今日ではこの思想に基づく設計は、1つのアプリを変更した影響が他のアプリへと波及していまうというデメリットをはらんでいます。 最後に、「DBにデータの正確性の責任を持たせる」という思想もあったそうです。 この思想の問題点としては、ビジネスロジックがDBの中に入り込んでしまう点です。 SQL Serverの中にビジネスロジックが入り込んでしまうと、(ユーザ定義関数などで)その分リソースを使うため、結果的にライセンス料の高騰につながるという問題があります。 ビジネスロジックをストアドプロシージャやトリガーに入れ込んでしまうとテスト、リファクタリング、読解がしづらい状況となってしまいます。 代わりに、アプリ側でビジネスロジックを管理するほうが読みやすくテストもしやすいとの主張は、今日における主流の考え方なのかなと思いました。 こうしたEF Coddの時代と現代の状況を比較した上で、スピーカーはデータモデリングにおける3つの重要な要素を主張していました。 それは①データのクオリティを高める、②スキーマ変更のしやすさ、③microservices method(one application to one database/他アプリからの直接のデータアクセスは失礼という思想)というものです。 Optimize for reads EF Coddは(ディスクが高価であった状況等を踏まえて)書き込みに最適なモデリング手法を提唱しましたが、現在の大多数のアプリケーションはread heavyな性質を持っています。 そのため、読み取り最適化を意識することが重要と話されていました。 この思想だと、例えばRDBMSの顧客テーブルに「現在処理中の注文リストをもったJSONカラム」があっても良いじゃないか!という主張でした。 この主張は名著 SQLアンチパターン に掲載されているアンチパターンですが、スピーカーの思想に基づけば合理的な判断だなとも思いました。 データの一貫性については、DBの責務ではなく、アプリ側の責務という考え方も興味深かったです。 「注文合計金額」といったカラムの更新漏れチェックはアプリ側のテストに盛り込むべきと主張されていました。 Optimize for readsの思想にのっとって、非正規化を恐れない。そして、冗長なデータの更新責務はアプリとアプリ開発者が背負うべきとのことでした。 microserviceについては初心者向けの本を1冊読んだ程度でしたが、もっとこの思想について学ぶ必要があると痛感したセッションでした。 Optimize for network 「create schema for the network, not for the disk」という主張に拍手が沸き起こっていました。 昨今のモダンな設計におけるボトルネックはディスクよりもネットワークにあるため、ネットワーク最適化を意識したスキーマデザインが必要であるとのことでした。 ここも自分にとって新鮮すぎる考え方だったため勉強にはなったのですが、知識不足で具体的なイメージまでは湧きませんでした。 アツい質問 「モノリシックな大きいシステムをクラウドにもっていくのは金銭的なコストがかかるのでは?」という質問がでました。 「コストは問題じゃない。クラウドのほうがオンプレよりもはるかにテクノロジーの進歩が速く、それらを常に使える状態になるほうが経営上のメリットがある」との回答に拍手が起きました。 これは自分でも実感がありまして、オンプレ環境だと2019をプロダクション環境で使えるのは数年に1度のシステム更改時となります。 そのため、強力な機能が存在するのにバージョンの都合で使えないのはもったいないと感じました。(※弊社にはクラウド環境もオンプレ環境も存在しています。) Session: Into the Future with In-memory OLTP SQL Server界隈で超有名なKalen Delaney氏によるIn-memory OLTPに関するセッション。 30年以上SQL Serverに関わっているレジェンドらしいのですが、Microsoftで働き始めたのは半年ほど前から、という謎の経歴の持ち主でした。 セッションの詳細は割愛しますが、「プロダクション環境でIn-memory OLTPを使っている人?」という問いに対して挙手したのが会場全体の1%程度だったのがかなり印象的でした。 In-memory OLTPは、効果は絶大なものの制約が多く、使いどころが非常に難しいイメージがありました。 世界中から集まったDBA達の中でも、やはりプロダクション環境での使用例は少なく、実装のハードルは高いようです。 Session: Hash Match, the Operator Hash Matchの話題だけで2.5時間という超絶マニアックなセッションでした。PASS Summit 2019で唯一のLevel=500(最上級)ということもあり、せっかくなので受講してきました。 ドキュメント化されている情報ではなく、スピーカーが実験の結果たどり着いた結論だそうで、実験の複雑さに圧倒されました。 Call For Speakers 落選理由の分析 PASS Summit 2019に、私も「Trouble Shooting And Performance Tuning On High Traffic EC Site」というタイトルで応募したのですが、落選しました。 そもそも私の英語力ではセッション途中のQ&Aなどに対応することが難しく、現状では無理な挑戦であったなと感じています。 ただ、なぜ落選したのかについては、現地でセッションを受ける中でわりと明確になってきたので、書いておこうと思います。 理由1. PASS Summitの文脈を理解していなかった どういった内容のセッションが多いかは、カンファレンスごとに違うと思います。 PASS SummitはSQL Serverの特定機能についての洞察や、クエリチューニングに特化したセッションなど、1トピックに対して深堀するセッションが多いように感じました。 そして、それは受講者にとっても大変聴きごたえのあるものでした。 一方で、私が応募した内容は、SQL Serverの複数の機能を使って特定の問題を解決した、という事例紹介でした。 したがって、PASS Summitではウケの良くない内容だったなと感じました。 理由2. PASSコミュニティへの貢献がまったく無かった 応募の際は、最低でも3回の登壇経験を求められます。 PASSコミュニティでは、「 SQLSaturday 」という小規模な勉強会も定期開催されています。 SQLSaturdayの登壇経験を積んだ後、PASS Summitで話すという流れの人もいるようでした。 「PASS Summit is a large SQLSaturday」といわれており、コミュニティの小さなイベントとPASS Summitは一続きになっている印象を受けました。 積極的にSQLSaturdayで登壇している人は、PASSコミュニティでウケる内容も自然と理解でき、採択されやすかったのではと思います。 ということで、SQLSaturdayで早速登壇したいと思ったのですが、日本のローカルコミュニティが無いようです。。!自分で作るしか。。ないかな。。! 以上2つの理由が、私が落選した理由だと分析しました。 内容的には初心者向けのセッションもあり、「いかにコミュニティへ貢献できる人物と内容か」ということが大事なのかなと思います。 来年再挑戦するかは迷うところではありますが、いずれ登壇したい、という大きな目標が一つできたことは良かったです。 カンファレンス参加の意義 国内外に関係なく、自分の興味分野における最先端、最大規模のカンファレンスであれば、参加する価値は十分にあると思います。 コミュニティの中に自分を置き、熱量を肌で感じ取り、数日間インプットだけに集中できるのも、セッション動画を見るのではなく直接参加することのメリットだと感じました。 コミュニティのレベルが上がると、自分のレベルも引き上げられます。そして学んだことをコミュニティに還元することで、さらにコミュニティが強くなっていきます。 PASSはこの正のサイクルがうまく回っているコミュニティだと強く感じました。 今までは、Qiitaの記事や会社のテックブログ、勉強会での発信をなんとなく大事だとは思っていたので継続していました。 ただ、カンファレンスに参加したことで、アウトプットは巡り巡って自分のためにもなるという確信を持つことができました。 この変化が、自分にとっては一番大きな成果物だったかもしれません。とても良い経験をすることができました。 あわせて、普段と違う環境で大量のインプットをすることで、新しいことを学ぶだけでなく、新しい行動を起こしたくなるというのもメリットかと思います。 例えば、私の場合は「コミュニティ」が新しいキーワードとなりました。 ベンダに関係なく、RDBMSやデータストレージに関するエンジニア達がお互いの知識を共有し合い学び合えるコミュニティが日本にあればいいのに、と強く思いました。 思うだけでなく、実際に自分でつくるところまでつなげていきたいと思っています。 最後に カンファレンス参加に関わる渡航費・宿泊費などは全て会社に負担していただきました。 自分で希望すれば参加する機会をいただけるZOZOテクノロジーズという会社には本当に感謝です! エンジニアの成長を全力で応援してくれる姿勢を、この支援制度からも感じ取ることができました。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com