TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

938

こんにちはSRE部の川津です。ZOZOTOWNにおけるログ収集基盤の開発を進めています。開発を進めていく中でCloud Pub/Subのリージョン間費用を削減できる部分が見つかりました。 今回、OSSである fluent-plugin-gcloud-pubsub-custom にコントリビュートした結果、Cloud Pub/Subのリージョン間費用を削減できました。その事例を、ログ収集基盤開発の経緯と実装要件を踏まえて紹介します。 目次 目次 ログ収集基盤の紹介 開発経緯 フロントエンドのログしか取得できない BigQuery ExportはSLAを担保されていない リアルタイムにログを保存できない 実装要件 ログ送信側の環境に依存しない共通の仕組みで実装する 転送されるログの量に応じてオートスケールする構成にする 送られてくるログをロストしない リアルタイムにログが保存される インフラ構成 Fluentd バッファリングができる アプリケーションの実装環境に依らず利用できる Cloud Pub/Sub バッファリングを行ってくれる 自動でスケーリングされる ログの送信側と受信側を疎結合にできる Dataflow Streming形式でBigQueryにInsertできる 自動でスケーリングされる ログ収集基盤の問題点 Fluentd → Cloud Pub/Sub → Dataflow 解決策 Cloud Pub/SubのSubscribeする側とPublishする側を同じゾーンにする USリージョンに揃えた場合 東京リージョンに揃えた場合 BigQueryを東京リージョンに配置した場合 BigQueryをUSリージョンに配置した場合 FluentdのOutput PluginsでPub/Subのリージョンを指定できるようにする OSSへのコントリビュート内容 効果検証 まとめ 最後に ログ収集基盤の紹介 はじめに、ZOZOテクノロジーズにおけるログ収集基盤を紹介します。 開発経緯 ZOZOテクノロジーズではZOZOTOWNにおけるログを収集する基盤に課題があるため、新たな基盤の開発を進めています。 現在のログ収集は、サイト利用者のアクセスパターン等の分析をするために、Google Analyticsを利用しています。収集したデータはBigQuery Exportを介してBigQueryに保存されます。 しかし、Google Analyticsを利用する際、下記3点の問題がありました。 フロントエンドのログしか取得できない BigQuery ExportはSLAを担保されていない リアルタイムにログを保存できない フロントエンドのログしか取得できない Google Analyticsはフロントエンドのログしか取得できません。そのため、バックエンド側のログが必要な場合でもGoogle Analyticsでは検知されず収集できません。 バックエンド側のログをGoogle Analyticsで利用するためには、一度フロントエンド側にバックエンド側のログを送信する必要があります。 BigQuery ExportはSLAを担保されていない 現在、Google Analyticsで収集したログを利用する際はBigQuery Exportの機能を使いBigQueryへログを転送しています。しかし、下記の資料からGoogle AnalyticsのBigQuery Exportでは、SLAが担保されていないことがわかります。そのため、BigQuery Exportの機能に障害が発生した場合、復旧時間は保証されません。実際にBigQuery Exportで障害が発生し数時間分のログが欠損したケースが過去にありました。 support.google.com リアルタイムにログを保存できない 分析の結果は推薦や検索へ用いるため、ログをリアルタイムに利用したい要望があります。 Google AnalyticsはBigQueryのStreaming Exportを利用できます。しかし、内部ではBigQuery Exportを利用しています。そのため、BigQuery ExportはSLAが担保されておらず、障害が発生する場合を想定すると可用性に欠けます。 以上の問題を解消するため、バックエンドを含めZOZOTOWNにおいて発生するログを集約し、リアルタイムにログを収集できる基盤を構築することにしました。 実装要件 ログ収集基盤を構築するにあたり、以下の4つの要件があります。 ログ送信側の環境に依存しない共通の仕組みで実装する 転送されるログの量に応じてオートスケールする構成にする 送られてくるログをロストしない リアルタイムにログが保存される 各要件について説明します。 ログ送信側の環境に依存しない共通の仕組みで実装する ログを送信するサーバは様々な環境で動いています。例えばAWSやGCP、オンプレミス環境です。また環境によってOSや使用しているアプリケーションのバージョンや言語も異なるので、様々な環境で同じ挙動をする共通の仕組みが必要です。 転送されるログの量に応じてオートスケールする構成にする ZOZOTOWNのアクセス数は時期や時間帯によって大きく変化します。日中の時間帯はアクセス数が多いのに対し、深夜の時間帯のアクセス数は多くありません。また年末年始のセールや人気ブランド商品の発売等、急激にアクセス数が伸びる場合もあります。アクセスの増減に対してオートスケールしない構成の場合、料金が余分に発生してしまったり、収集するログを捌ききれず遅延が発生してしまいます。 このような問題を防ぐために、アクセス数が少ない場合はサーバ台数を自動的に減らし、逆にアクセス数が多い場合はサーバを自動的に増やすような構成にします。 送られてくるログをロストしない ログがロストする状況を考えると、ネットワークの影響でデータの送信が止まった際が考えられます。そのため、各インフラリソースを利用する際は、バッファリングを行いデータのロストを防ぐ必要があります。 リアルタイムにログが保存される ログ収集基盤で取り扱うログは、アイテムの検索や推薦で利用されることが想定されます。 検索や推薦で求められるリアルタイム性は最短で1分、最長で60分です。今回ログ収集基盤に送られるログのメッセージ数はおよそ20,000msg/sです。よって送られてくる20,000msg/sのデータを60分以内に保存できる仕組みが必要です。 インフラ構成 ログ収集基盤のインフラ構成は下記の図の通りです。 各リソースを採用した理由を順に説明します。 Fluentd Fluentdの利点は以下の点です。 バッファリングができる アプリケーションの実装環境に依らず利用できる バッファリングができる Fluentd側でバッファリングする目的はCloud Pub/Subに障害が起こった際や、ネットワークの影響でCloud Pub/Subへログが送れなかった際にロストする問題を防ぐためです。Cloud Pub/Subの可用性は月間99.95%以上です。詳細についてはGoogleの Pub/Sub Service Level Agreement に記載されています。 このSLAを基にダウンタイムを考えるとおよそ20分です。20分間のダウンタイムが発生した場合、バッファリング無しだとその20分間のログデータがロストしてしまいます。このロストは許容できないため、ログの送信側であるFluentdでバッファリングを行う必要があります。 アプリケーションの実装環境に依らず利用できる Fluentdはアプリケーションとして独立しており、多様な環境で利用可能です。そのため、ログを出力するアプリケーション側の言語に依らず導入できます。アプリケーション側はログをFluentd側に送るだけで、FluentdがCloud Pub/Subへ送信してくれます。 Cloud Pub/Sub Cloud Pub/Subの利点は以下の点です。 バッファリングを行ってくれる 自動でスケーリングされる ログの送信側と受信側を疎結合にできる バッファリングを行ってくれる Cloud Pub/Subでバッファリングする目的はログのロストを防ぐためです。 Cloud Pub/Subのバッファリングの性能は、 リソースの上限 を確認すると下記のように記載されています。 Retains unacknowledged messages in persistent storage for 7 days from the moment of publication. There is no limit on the number of retained messages. If subscribers don't use a subscription, the subscription expires. The default expiration period is 31 days. 確認応答されていないメッセージは7日間保存されます。そのため、Subscriber側のDataflowに障害が起こった場合でも7日間復旧の猶予ができます。また、保存されるメッセージ数に上限はないので大量のログによるバッファあふれでログをロストする心配もありません。 自動でスケーリングされる ログの受け口がオートスケール可能であることは、実装要件で挙げたようなセールや時間帯等によるログの送信数増減でリソースの枯渇と余剰を防ぐために必要です。Cloud Pub/Subでは、Cloud Pub/Sub側が定義した「負荷」によってリソースが可変します。詳しくはGoogleの公式ドキュメントの スケーラビリティ に記載されています。 オートスケールの上限はCloud Pub/Subの 割り当て上限 を確認すると、大規模リージョンと小規模リージョンによって上限が異なります。大規模リージョンに該当するリージョンはeurope-west1、us-central1、us-east1で、小規模リージョンはそれ以外の全てのリージョンです。 大規模リージョンの場合、Publisherのスループットは下記のように記載されています。 12,000,000 kB per minute (200 MB/s) in large regions Cloud Pub/SubへのPublishに関しては1リクエスト最大10MBで、1リクエスト1,000メッセージまでまとめて送れます。 大規模リージョンにおける最大メッセージ数は上記に記載されている通り200MB/sです。今回想定しているログのメッセージサイズは1メッセージ1kBなので200,000kB/sは200,000msg/sです。想定されるメッセージ数は20,000msg/sなので、上限に引っかかることはありません。 一方、小規模リージョンの場合、Publisherのスループットは下記のように記載されています。 3,000,000 kB per minute (50 MB/s) in small regions 上限が50MBなのでおよそ50,000msg/sを送ることが可能です。 小規模リージョンと大規模リージョンを比べると、大規模リージョンの方がより多くのメッセージを捌けることが明らかです。セールによってメッセージ数が数倍に増加する場合を考慮すると、今回は大規模リージョンが適していると言えます。 ログの送信側と受信側を疎結合にできる ログの送信側をステートレスにするため、Cloud Pub/Subを導入しログの送信側と受信側を疎結合にします。 もしCloud Pub/Subを利用しない場合は、FluentdからBigQueryへ直接Insertする方法があります。BigQueryのStreaming Insertを利用することで、リアルタイムにInsertできます。この構成にするとバッファリングはBigQuery側で行うことができないので、Fluentd側のみでバッファリングを行う必要があります。 ところが、Fluentd側でバッファリングを行った場合、アプリケーションのデプロイタイミングが難しくなります。バッファリングを行っている最中はログのデータを保持していますが、下記ドキュメントによると、Fluentdのバッファリングはメモリに保存する方法とファイルへ保存する方法があると書かれています。基本的にファイルへ保存する方法を利用するので、デプロイのタイミングでログがロストする心配はありません。しかし、デプロイのタイミングにはFluentd自体のログの送信が停止してしまいます。そのため、Fluentd側でバッファリングを行うとデプロイの最中にログ送信が止まってしまいます。影響範囲を少なくするために利用者が少ない時間帯にデプロイする必要が出てきます。 このような制約により、デプロイのタイミングを制限されてしまうと、アプリケーション側のリリースに影響が出るのでCloud Pub/Subを利用しています。 docs.fluentd.org Dataflow Dataflowの利点は下記の点です。 Streming形式でBigQueryにInsertできる 自動でスケーリングされる Streming形式でBigQueryにInsertできる DataflowではStreaming形式がサポートされているので遅延を抑えてBigQueryにログを保存できます。そのため、実装要件として求められているリアルタイム性を担保できます。 自動でスケーリングされる Dataflowにおいてスケーリングが必要となる理由は、Cloud Pub/Subでスケーリングが必要な理由と同様です。 Dataflowのオートスケール機能について、公式ドキュメントの オートスケーリング機能 を確認すると、ワーカーの負荷やリソースの使用率に応じてワーカーの数は変更されることがわかります。 同様にオートスケールの上限については、Dataflowの 割り当て上限 を確認すると下記のように記載されています。 Each Dataflow job can use a maximum of 1,000 Compute Engine instances. 上記の記載はありますが、1インスタンスあたりの性能についての記載は見当たらないので、検証を実施しました。 具体的には、Cloud Pub/SubにACK処理がされていないメッセージを溜め込んだ状態にし、Dataflowを起動してBigQueryにInsertを行いました。なお、Dataflowのワーカーは下記のスペックで1台に固定しました。 CPU数:4 メモリ:15GB ストレージ:430GB このスペックはDataflowのJobを作成する際にデフォルトで割り当てられるものです。なお、Dataflowで利用できるCPUやメモリの割り当ては、 Compute Engine の割り当て に記載のあるCompute Engineのマシンタイプを指定できます。 今回はn1-standard-4のマシンタイプを利用しています。CPUはデータを並列で処理したいので4つ割り当てており、メモリはCPUが4つの場合15GBと決まっているので15GBに設定しています。 Dataflowで扱うテンプレートはGoogleが提供している Pub/Sub Subscription to BigQuery テンプレート を利用しました。 以上の条件でCloud Pub/Subにデータサイズが1kBのメッセージを4,000,000件溜め込んだ状態で検証しました。 その結果、1インスタンスのスループットは約12,000msg/sでした。 今回、Cloud Pub/Subで想定されるメッセージ数は20,000msg/sです。その場合、約20,000msg/sのメッセージがDataflow側で処理されます。Dataflowでは1インスタンスで約12,000msg/s処理できるので、性能に関して問題ないことがわかりました。 よって、セール時に処理するメッセージ量が増加した場合でも処理できます。 ログ収集基盤の問題点 ログ収集基盤にはZOZOTOWNにおけるログが全て集約されます。そのため、Cloud Pub/SubにPublishするメッセージ数とBigQueryにInsertするデータ量も必然的に膨大なものになります。 データ量が大きくなる際に懸念すべき点が、各リソース間で発生するリージョン間通信です。リージョン間通信は現在のリージョンから別のリージョンへ通信が行われる際に発生する費用です。発生する料金は ネットワーク料金表 に記載されています。 今回のインフラ構成でリージョン間通信が発生するのは以下の通信です。 Fluentd → Cloud Pub/Sub → Dataflow Dataflow → BigQuery なお、「Dataflow → BigQuery」の通信はBigQueryをUSリージョンに配置する都合があるため、次の章で併わせて説明します。 Fluentd → Cloud Pub/Sub → Dataflow Cloud Pub/Subのリージョン間通信はこちらの 料金の説明 に記載されています。 The fees for internet egress and message delivery between Google Cloud regions are consistent with the Compute Engine network rates, with the following exceptions: Cloud Pub/Subのネットワーク料金はCompute Engineのネットワーク料金と同じ料金体系です。そのため、Cloud Pub/SubのメッセージをPublishする側とSubscribeする側のリージョンが別の場合、 ネットワーク料金表 の料金体系が適用されます。 課金される料金は下記のトラフィックの種類によって分類されています。 出典: ネットワーキングのすべての料金体系 | VPC | Google Cloud 内部IPかつ同じゾーンの場合は料金が発生せず、外部IPや異なるリージョン間の通信では料金が発生します。 今回利用するリージョンは、BigQueryの配置先が東京リージョンかUSリージョンになるので、必然的に東京リージョンかUSリージョンの2パターンです。料金が発生する条件はCloud Pub/SubのメッセージをPublishする側とSubscribeする側のリージョンが別の場合が条件です。つまり、Fluentd側とDataflow側を東京リージョンかUSリージョンのどちらかに統一しない場合に料金が発生します。例えばDataflow側をUSリージョン、Fluentd側を東京リージョンに配置する場合、下記の大陸間通信のトラフィックで料金が発生します。 出典: ネットワーキングのすべての料金体系 | VPC | Google Cloud 上記の通り$0.08/GBの料金が発生します。 今回仮定されるCloud Pub/Subへのメッセージサイズは、1メッセージ1kBです。Fluentdから送信されるメッセージ数を20,000msg/sと仮定すると1秒間で0.02GB転送されます。月で換算するとおよそ50,000GBのデータが転送されます。つまり、1か月に必要なリージョン間費用は$4,000です。金額換算すると無視できる額ではありません。 解決策 Cloud Pub/Subのリージョン間通信を抑えるために、下記2つの解決策を考えました。 Cloud Pub/SubのSubscribeする側とPublishする側を同じゾーンにする FluentdのOutput PluginsでPub/Subのリージョンを指定できるようにする Cloud Pub/SubのSubscribeする側とPublishする側を同じゾーンにする 同じゾーンにリソースを配置する際の選択肢は、Fluentdを動かすサーバとDataflowを動かすサーバをUSリージョンに置くパターンと東京リージョンに置くパターンです。それぞれのパターンを考えてみます。 USリージョンに揃えた場合 Cloud Pub/SubのSubscribeする側とPublishする側をUSリージョンに揃えた場合、リージョン間の料金は発生しません。 しかし、ZOZOTOWNの利用者は日本国内からのアクセスがほとんどです。そのため、USリージョンにインスタンスを配置すると大陸間の通信が発生します。その結果、東京リージョンへ配置する場合に比べ、距離的な問題で遅延が増加します。 下記の記事によると、アメリカ西海岸までの通信ではおよそ100msの往復遅延が発生します。 xtech.nikkei.com つまり、Fluentd側をUSリージョンに配置する場合、ログを送信するアプリケーションもUSリージョンに配置する必要があり、アプリケーション側で100msの遅延が発生します。100msの遅延はユーザ体験に影響が出るレベルであるため、この遅延は防ぐべきです。 東京リージョンに揃えた場合 東京リージョンに揃えた場合、前述のような距離に起因する大きな遅延は発生しません。 しかし、リージョン間の料金はBigQueryの配置が東京リージョンかUSリージョンかによって変わります。このBigQueryの配置先の選択肢を比較してみます。 BigQueryを東京リージョンに配置した場合 BigQueryを東京リージョンに配置する場合のメリットは、リージョン間通信が発生しないことです。 デメリットは、BigQueryの利用料金の単価がUSリージョンよりも高くなる点です。BigQueryの 料金表 で東京リージョンとUSリージョンを比較すると下記の金額差があります。 オンデマンド分析の料金(USD/TB) ストレージの料金(USD/GB) 東京 6.00 0.023 US 5.00 0.020 オンデマンド分析の料金やストレージの料金を比較するとUSリージョンの方が安いです。 BigQueryをUSリージョンに配置した場合 BigQueryをUSリージョンに配置するメリットは2つあります。 BigQueryの利用料金の単価が安い BigQueryの新機能を早期に使える BigQueryの新機能はUSリージョンから順にリリースされることが多いです。そのため、USリージョンに配置することで新機能の早期利用が可能です。 デメリットは、BigQueryをUSリージョンに配置した場合は「Dataflow → BigQuery」間でリージョン間通信が発生することです。発生するリージョン間費用は「東京 → US」間の通信なのでCloud Pub/Subのリージョン間費用と同等の料金が発生します。 その結果、BigQueryをUSリージョン、BigQuery以外のリソースを東京リージョンに揃えた場合は料金を削減できません。 以上の結果から、BigQueryの利用単価が安い点と新機能が早期に利用できる点でUSリージョンへ配置することにしました。 FluentdのOutput PluginsでPub/Subのリージョンを指定できるようにする Cloud Pub/Subのリージョン間費用をなくす方法として、Cloud Pub/SubにメッセージをPublishする場合にendpointを指定する方法があります。 下記のドキュメントより、Cloud Pub/Subのサービス自体はグローバルなサービスですが、リージョン毎にendpointが用意されていることがわかります。endpointを指定しない場合はglobal endpoint https://pubsub.googleapis.com へリクエストが送られます。このglobal endpointにリクエストが送られると、Cloud Pub/Sub側が自動的にリクエストを送った場所の近くのリージョンのendpointへルーティングします。その仕様を回避させるために、Cloud Pub/Subのメッセージを受け取る側のリージョンと同じリージョンのendpointを直接指定することでリージョン間の料金を発生させなくできます。 ただし、FluentdをGCP内のリソースに構築しendpointを指定した場合は別途料金が発生します。 今回利用するインフラ構成のFluentdを東京リージョンのインスタンス上に構築した場合を考えてみます。この状態でCloud Pub/Subのendpointを指定した場合、指定したendpointのリージョンへPublishされます。ここで「東京 → US」間の通信費用が発生します。このリージョン間の通信費用もCloud Pub/Subのリージョン間通信と同等の料金が発生します。そのため、endpointを指定してCloud Pub/Subのリージョン間費用をなくす方法はGCP内のネットワーク外からCloud Pub/SubへPublishする場合のみ有効です。 cloud.google.com 今回、FluentdでCloud Pub/SubにPublishする部分は fluent-plugin-gcloud-pubsub-custom を利用することにしました。しかし、このプラグインではCloud Pub/Subへログを送る際にパラメーターでendpointを指定できませんでした。 つまり、このプラグインでパラメータによりログを送信する際のendpointを指定できるようになれば、Cloud Pub/Subのリージョン間費用をなくすことができると言えます。 以上の検討結果より、OSSとして公開されているFluentdのプラグインであるfluent-plugin-gcloud-pubsub-customを修正することにしました。 OSSへのコントリビュート内容 実際に改修を加え、OSSへコントリビュートしていきます。 まず、fluent-plugin-gcloud-pubsub-customを利用する際に、Fluentdのconfigに対してパラメータでendpointを指定できるようにします。次に、内部で利用されているRubyのCloud Pub/Sub ClientからPublishする際にも、endpointを指定してPublishできるようにします。 なお、Cloud Pub/Subの ドキュメント から new でオブジェクトを生成する際にendpointを引数で渡すことが可能です。 そのため、Fluentdでendpointのパラメータを定義した後、newの引数に定義したendpointを渡すことで実現できます。 実際に改修を加えたPull Requestは下記の内容です。 github.com このPull Requestの内容を簡単に説明します。 Configuration Parameter Types にあるconfig_paramを利用することでFluentdのconfig内で扱えるパラメータを定義できます。これを利用し、今回は下記のように定義しました。 ruby config_param :endpoint, :string, :default => nil config_paramのData Typeは String なので、定義した値がインスタンス変数のendpointに格納されます。 あとは Google::Cloud::Pubsub.new をしている部分にendpointのパラメータを渡すだけです。 Google::Cloud::Pubsub.new をしている部分が下記のinitializeメソッドの部分なので、このClassを利用している部分に先程定義したインスタンス変数を渡すように修正しました。 github.com 以上の修正でFluentdからCloud Pub/SubへPublishする際にendpointを指定できるようになりました。 効果検証 Pull Reqestがmasterにマージされ、実際にリージョン間の費用が抑えられているかの検証を実施しました。 検証として2MBのメッセージをローカルで立てたFluentdから、Cloud Pub/Subへ2,048回送信しました。つまり、合計4GBのデータがCloud Pub/Subへ送信されます。 endpointを指定しない場合はリージョン間通信が発生するので1GBにつき$0.08発生します。合計で約$0.32の料金が課金されます。 一方、endpointを指定する場合はリージョン間通信が発生せず、同一リージョン間で通信が行われるので ネットワーク料金表 に記載のある下記の料金体系が適用されます。 出典: ネットワーキングのすべての料金体系 | VPC | Google Cloud 上記の記載から無料であることがわかります。 インフラ構成は前述のログ収集基盤と同じ構成で構築し、Fluentdのendpointを指定する場合と指定しない場合のリージョン間の費用を確認しました。 まず、endpointを指定しない場合の結果は以下の通りです。 次にendpointを指定する場合の結果は以下の通りです。 上記の結果より、endpointを指定しない場合の料金は Inter-region data delivery from Asia Pacific to North America の部分で料金が発生していることがわかります。価格はリージョン間費用が$0.08/GBなので、4GB送信されているので合計$0.32です。 一方、endpointを指定する場合はリージョン間の通信は発生しないので Inter-region data delivery from Asia Pacific to North America の部分の料金は発生していません。代わりに Intra-region data delivery に対して送信したデータ量が記載されています。こちらの項目は同一リージョン間の通信に発生する項目です。料金は$0なので4GBのデータ送信は特に料金が発生しません。 以上の結果より、Pull Requestで修正した内容により、コスト削減が実現されていることを確認できました。 まとめ FluentdのPluginであるfluent-plugin-gcloud-pubsub-customにendpointを追加で指定できるようにOSSを修正しました。また、実際に修正した機能を使ってリージョン間費用が発生しないことも確認できました。 その結果、プロジェクトにかかる費用を大きく抑えることができました。より低コストなログ収集基盤を提供できます。 OSSへのコントリビュートは初めてだったので良い経験になりました。OSSのコードを読むという点でも勉強になったので、今後も積極的にOSSへコントリビュートを行っていきたいと思います。 最後に ZOZOテクノロジーズではより良いサービスを提供するための基盤を開発したい仲間を募集中です。以下のリンクからご応募ください。 tech.zozo.com
アバター
こんにちは、SRE部の谷口( case-k )です。 本記事では、EC2 Image Builderを使いRedashの運用改善を行った事例をご紹介します。運用しているRedashについてご紹介し、その後、Redashの運用課題に対してEC2 Image Builderでどのように解決したかTipsも踏まえご紹介します。 余談ですが全国どこでも働けるようになったので沖縄に住めています(感謝!) https://press-tech.zozo.com/entry/20210118_zozotech press-tech.zozo.com 目次 目次 運用しているRedashの紹介 役割 インフラ構成 クエリ実行の流れ EC2インスタンス起動時の処理 Redashの運用課題 EC2 Image Builderによる課題解決 EC2 Image Builderの紹介 各リソースのTips 事前準備 コンポーネント レシピ インフラストラクチャ カスタムAMIの生成 イメージパイプライン EC2 Image Builderの利点・欠点 利点 カスタムAMIの手動運用が不要になる リソースをコードで管理できる EC2インスタンスの起動時間を短縮できる 欠点 エラーログの調査が大変 AMI生成までの時間が長い EC2 Image Builderが担う範囲の検討 まとめ さいごに 運用しているRedashの紹介 まず運用しているRedashの役割やインフラ構成、クエリ実行の流れ、起動時の処理についてご紹介します。 役割 ZOZOテクノロジーズでは配信基盤をインハウス化して自社で開発しています。メルマガやLINEなど複数のチャンネルに対して配信しています。 techblog.zozo.com Redashの主な役割としては「分析」と「監視」です。 分析では配信施策の状況のモニタリングや施策の効果検証をしています。また、Redashではクエリ実行結果に基づいた監視も可能なので配信状況などの異常検知やデータ連携遅延などの他サービスの監視も行っています。 インフラ構成 ZOZOテクノロジーズで運用しているRedashは、公式に提供されている Redash AMI をベースにしています。Redash AMIからインスタンスを起動すると、Web UIをホスティングするRedashサーバー、クエリの実行を担うRedashワーカーが立ち上がり、Redashを利用できるようになリます。 クエリの数が増えてもRedashワーカーがオートスケールできるよう、ALB配下にはRedash AMIから起動したEC2インスタンスを配置しています。可用性を高めるため、フルマネージドでマルチAZ構成可能なAWSのAurora(PostgreSQL)とElastiCache(Redis)を参照するようにしています。 Redashは社内ツールであり、他サービスの監視ツールとしても使われています。そのため、冗長構成により可用性の高い運用を行っています。 なお、CloudForamtionについては こちら にまとめました。 クエリ実行の流れ クエリ実行の流れを説明します。 まず、実行するクエリはWeb UIをホスティングしているRedashサーバーからElastiCache(Redis)に保存されます。 クエリの実行はRedashワーカーで行われるため、RedashワーカーはElastiCache(Redis)に保存されたクエリを取得し、BigQueryなどデータソースに対してクエリを実行します。 そして、クエリの実行結果はAurora(PostgreSQL)に書き込まれます。書き込まれたクエリの実行結果はRedashサーバーより読み出されWeb UIで確認できます。なお、クエリの実行は分散タスクキューの Celery によって非同期に行われます。 クエリ実行の流れは以下の記事が参考になりました。 speakerdeck.com EC2インスタンス起動時の処理 EC2インスタンスの起動時にはミドルウェアの接続先を変更するための処理をしています。 Redash AMIは起動時にDocker Composeでコンテナイメージをビルドします。コンテナイメージをビルドするとRedashサーバーやワーカー、ミドルウェアであるPostgreSQL、Redisコンテナが立ち上がります。その際に立ち上がるRedashサーバーやワーカーが参照するミドルウェアの接続先はの環境設定ファイルに定義されています。Redash AMIをそのまま使うと、Docker Composeで立ち上げたPostgreSQL、Redisの接続先は環境設定ファイルに定義されたものが使われます。なお、Redash AMIの起動時のビルド処理はユーザーデータには定義されておりません。 今回行いたいことはインフラ構成にて説明したような冗長構成です。EC2インスタンス2台の冗長構成にするため、ミドルウェアをEC2インスタンスの外で管理する必要があります。AWSのAurora(PostgreSQL)とElastiCache(Redis)を参照するようEC2インスタンスの起動時に環境設定ファイルを書き換えてコンテナイメージをビルドする必要があります。 そのため、ユーザーデータでAWSのCLIを使えるようライブラリをインストールし、CLIでAWSシークレットマネージャー管理下の秘密情報を取得します。秘密情報としてAuroraやElastiCacheのユーザー情報やデータソースの復号化に必要な「REDASH_COOKIE_SECRET」や「REDASH_SECRET_KEY」を管理しています。そして、取得した秘密情報とCloudFormationで作ったリソースに基づいて環境設定ファイルを生成し、コンテナイメージをビルドします。 Redash AMIには起動時にコンテナイメージのビルド処理が組み込まれています。この処理はユーザーデータで定義した処理より前に実行されるため、Redash AMIの古い環境設定ファイルに基づいてコンテナイメージをビルドします。古いコンテナイメージだとミドルウェアの接続先が正しくないため、ユーザーデータでは、Redash AMIで作られたコンテナを落としてから、新しい環境設定ファイルに基づいてビルドしています。 UserData : Fn::Base64 : !Sub | #!/bin/bash -e rm /opt/redash/env curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py" sudo rm /var/lib/dpkg/lock* sudo dpkg --configure -a sudo apt update sudo apt install python -y sudo python get-pip.py sudo pip install awscli sudo apt install jq -y RedashUsername=$(aws secretsmanager get-secret-value --region ${AWS::Region} --secret-id Redash/RDS/User | jq -r .SecretString) RedashPassword=$(aws secretsmanager get-secret-value --region ${AWS::Region} --secret-id Redash/RDS/Password | jq -r .SecretString) cat <<EOF > /opt/redash/env PYTHONUNBUFFERED=0 REDASH_LOG_LEVEL=INFO POSTGRES_PASSWORD=${!RedashPassword} REDASH_COOKIE_SECRET=$(aws secretsmanager get-secret-value --region ${AWS::Region} --secret-id Redash/CookieSecret | jq -r .SecretString) REDASH_SECRET_KEY=$(aws secretsmanager get-secret-value --region ${AWS::Region} --secret-id Redash/SecretKey | jq -r .SecretString) REDASH_REDIS_URL=redis://${ElasticacheClusterForRedash.RedisEndpoint.Address}:${ElasticacheClusterForRedash.RedisEndpoint.Port}/0 REDASH_DATABASE_URL=postgresql://${!RedashUsername}:${!RedashPassword}@${RDSDBClusterForRedash.Endpoint.Address}:${RDSDBClusterForRedash.Endpoint.Port}/${!RedashUsername} REDASH_FEATURE_EXTENDED_ALERT_OPTIONS= true EOF sudo docker-compose -f /opt/redash/docker-compose.yml down sudo docker-compose -f /opt/redash/docker-compose.yml up -d --build Redashの運用課題 EC2インスタンス起動時にRedash AMI側の古い環境設定ファイルに基づいたビルドと、ユーザーデータで定義した新しい環境設定ファイルに基づいたビルドをしています。2つのビルドが同時に実行されることでRedashワーカーが正常に動かず、クエリ実行結果が返ってこない事象が確認できました。そのため、Redashワーカーを正常に動かすために、EC2インスタンス初回起動時のみ手動でEC2インスタンスの再起動する運用をしていました。せっかくクエリの個数に応じてオートスケールできる仕組みにしたのに活用できずにいました。また、データ分析の他に配信状況やデータ連携の遅延などの監視にも使われているため、監視ツールとしての役割に不安がありました。 EC2 Image Builderによる課題解決 前述のRedashの運用課題はコンテナイメージのビルド処理を制御すれば改善できるため、事前にAMIを作ることで解決できます。 手動でカスタムAMIを作る場合、Redashのバージョンアップやその他リソースの変更の度にカスタムAMIを作る必要があります。加えて、運用における属人化を防ぐ意味でも全てCloudFormationで管理したい思いがありました。 そのため、CloudFormationで管理可能で、カスタムAMIの手動運用が不要なEC2 Image Builderを使うことにしました。 EC2 Image Builderの紹介 EC2 Image BuilderとはカスタムAMIの作成を自動化するサービスです。 CloudFormationによる表現も可能で、一連のAMI作成をコード管理できます。CloudFormation管理にすることで、CloudFormationで作られたリソースに基づいたAMIの生成が可能となり属人的な運用の回避にも繋がります。 ここではCloudFormationを使った各リソースのTipsをご紹介します。 各リソースのTips EC2 Image BuilderでカスタムAMIを作るときには4つの要素があります。 コンポーネント レシピ インフラストラクチャ イメージパイプライン 各リソースについてCloudFormationを使いながらご紹介します。なお、「イメージパイプライン」はRedashの運用改善では使っていません。 ここで紹介する、EC2 Image BuilderによるAMI生成の全体図は次の通りです。 Icons made by Freepik from www.flaticon.com 事前準備 事前準備としてソースイメージと、EC2 Image Builderに必要なIAMを定義します。 注意点はソースイメージに指定できるものはAWSが指定するマネージドなAMIもしくは、SSMがインストールされたカスタムAMIに限られている点です。そのため、公式に提供されているRedashのAMIをソースイメージに指定できなかったので、Ubuntuのイメージに必要なモジュールをインストールしました。 そして、EC2 Image Builderで必要なIAM権限は「EC2InstanceProfileForImageBuilder」と「AmazonSSMManagedInstanceCore」です。 また、EC2 Image Builderの内部処理としてSSMを呼び出しています。エラーについてもSSMのオートメーションページに出力されます。後述しますがSSMに出力されるエラーはデバッグが容易ではないので注意が必要です。 Icons made by Freepik from www.flaticon.com EC2ImageBuilderForRedash : Type : 'AWS::IAM::Role' Properties : AssumeRolePolicyDocument : Version : '2012-10-17' Statement : - Effect : Allow Principal : Service : - ec2.amazonaws.com Action : - 'sts:AssumeRole' Path : / ManagedPolicyArns : - 'arn:aws:iam::aws:policy/EC2InstanceProfileForImageBuilder' - 'arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore' InstanceProfile : Type : AWS::IAM::InstanceProfile Properties : InstanceProfileName : ImageBuilderInstanceProfile Roles : - !Ref EC2ImageBuilderForRedash docs.aws.amazon.com コンポーネント コンポーネントはカスタムAMI作成に必要な手順を定義したリソースです。 カスタムAMIに必要なモジュールを定義した手順に沿ってインストールします。更新したコンポーネントを反映したい場合はCloudFormation反映時に「Version: 1.0.0」の部分のバージョン番号を変更して適用します。 Icons made by Freepik from www.flaticon.com Component : Type : AWS::ImageBuilder::Component Properties Data : | name : InstallApache description : InstallApache schemaVersion : 1.0 phases : - name : build steps : - name : UpdateOS action : UpdateOS - name : RedashDir action : ExecuteBash inputs : commands : - mkdir /opt/redash - name : docker-install action : ExecuteBash inputs : commands : - sudo apt-get update - sudo apt-get install -y \ apt-transport-https \ ca-certificates \ curl \ gnupg-agent \ software-properties-common - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - - sudo apt-key fingerprint 0EBFCD88 - sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" - sudo apt-get update - sudo apt-get install -y docker-ce docker-ce-cli containerd.io - name : docker-compose-install action : ExecuteBash inputs : commands : - sudo apt-get update - sudo curl -L "https://github.com/docker/compose/releases/download/1.26.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose - sudo chmod +x /usr/local/bin/docker-compose - sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose - name : aws-cli-install action : ExecuteBash inputs : commands : - curl "https://bootstrap.pypa.io/get-pip.py" -o "get-pip.py" - sudo rm /var/lib/dpkg/lock* - sudo dpkg --configure -a - sudo apt update - sudo apt install python -y - sudo python get-pip.py - sudo pip install awscli - sudo apt install jq -y Name : redash-ami-component Platform : Linux # update version when fix the component Version : 1.0.0 docs.aws.amazon.com レシピ レシピはソースとなるイメージとコンポーネントを紐付けるリソースです。 先ほど述べましたが、ソースイメージとして指定できるのはAWSが指定するマネージドなAMI、もしくはSSMがインストールされたカスタムAMIです。 ここで定義したレシピに基づいてAMIが生成されます。なお、コンポーネントを変えた場合はレシピのバージョンも更新して反映します。 Icons made by Freepik from www.flaticon.com Recipe : Type : AWS::ImageBuilder::ImageRecipe Properties : Components : - ComponentArn : !Ref Component Name : redash-ami-recipe # parentImage only accept aws managed image or custom ami installed ssm. so can not use redash ami ParentImage : arn:aws:imagebuilder:ap-northeast-1:aws:image/ubuntu-server-18-lts-x86/2020.9.23 Version : 1.0.0 docs.aws.amazon.com インフラストラクチャ インフラストラクチャはイメージのビルドからテストまでの実行環境を定義するリソースです。 「TerminateInstanceOnFailure」を「false」に設定すると、処理の失敗時にインスタンスを終了せずに済みます。そのため、SSMのエラー内容が不十分な際に活用できます。 なお、AMIをビルドする環境はインターネットへ接続できる環境である必要があるので、サブネットを定義する際には注意が必要です。 Icons made by Freepik from www.flaticon.com InfrastructureConfiguration : Type : AWS::ImageBuilder::InfrastructureConfiguration Properties : InstanceProfileName : !Ref InstanceProfile InstanceTypes : [] Name : redash-ami-infrastructure-configuration SecurityGroupIds : [] TerminateInstanceOnFailure : True docs.aws.amazon.com カスタムAMIの生成 生成するRedashのイメージは次の通りです。 まず、レシピとイメージを定義しRedashのカスタムAMIを自動生成します。すると、ビルド用のインスタンスが起動、終了した後にテスト用のインスタンスが起動します。 所用時間として30〜60分ほどかかるので時間を短縮したい場合は「ImageTestsEnabled」を「false」に設定する手段もあります。 Icons made by Freepik from www.flaticon.com RedashAmiImage : Type : AWS::ImageBuilder::Image Properties : ImageRecipeArn : !Ref Recipe InfrastructureConfigurationArn : !Ref InfrastructureConfiguration ImageTestsConfiguration : ImageTestsEnabled : true TimeoutMinutes : 60 docs.aws.amazon.com イメージパイプライン イメージパイプラインはカスタムAMIの生成をスケジューリング実行するためのリソースです。 今回は使いませんでしたが、先ほど紹介したカスタムAMIの生成処理を定期実行する際に利用できます。他にもRedashの定期的なバージョンアップなどを自動化する際に活用できます。 Icons made by Freepik from www.flaticon.com Type : AWS::ImageBuilder::ImagePipeline Properties : Description : String DistributionConfigurationArn : String EnhancedImageMetadataEnabled : Boolean ImageRecipeArn : String ImageTestsConfiguration : ImageTestsConfiguration InfrastructureConfigurationArn : String Name : String Schedule : Schedule Status : String Tags : Key : Value docs.aws.amazon.com EC2 Image Builderの利点・欠点 EC2 Image Builderを実際に利用し、そこから得られた利点と欠点を紹介します。 利点 カスタムAMIの手動運用が不要になる カスタムAMIの手動運用が不要になったのは喜ばしい効果です。 特に頻繁にバージョンアップが必要なケースでは有益です。Redashもそうですが、バージョンアップ関連の処理に利用範囲を拡げていきたいです。 リソースをコードで管理できる カスタムAMIなどリソースがコード管理されてないと属人的な運用になってしまうので、CloudFormationを使いコードで管理できるのもメリットです。 Terraformでもサポート されています。 EC2インスタンスの起動時間を短縮できる 事前に必要なモジュールがインストール済みのAMIを使えるので、EC2インスタンスの起動が早くなります。 EC2インスタンスのユーザーデータでインストールするにはモジュールが多すぎる場合に有効活用できます。例えばEC2インスタンスで稼働させているDigdagのワーカーなどにも活用できます。 欠点 エラーログの調査が大変 エラーログを調査する際に、SSMのエラーログだけでは具体的にどこで落ちたのかわかりにくいです。 原因を特定するためにはインフラストラクチャで「TerminateInstanceOnFailure」を「false」に設定し、EC2インスタンス内からログの調査を実施する必要があります。 AMI生成までの時間が長い 上述の通り、ビルドからデプロイまで長い場合だと60分ほど時間がかかります。これは、再掲の内容ですが、ビルド用のインスタンスが起動、終了した後にテスト用のインスタンスが起動するためです。 合わせて欠点の1つ目に記載したエラーログ調査の難解さもあり、必然的に開発ライフサイクルが長くなります。 EC2 Image Builderが担う範囲の検討 前章の利点・欠点であげたように、RedashのカスタムAMIの手動運用が不要になり、運用課題を解決できました。 一方で、エラーログの調査方法とAMI生成までの時間に関しては懸念が残ります。失敗時のログの調査とAMI生成までの時間を考慮すると、リソースを変更する度にEC2 Image Builderの更新が必要になる運用は避けたいです。 そのため、EC2 Image Builderではライブラリのインストールのみ実施することにしました。環境設定ファイルやdocker-compose.ymlの生成、ビルドはEC2インスタンスのユーザーデータで行っています。 今後、Redashのバージョンアップを自動化する際には、EC2 Image Builderで動的に生成すべきですが、現時点の運用ではこのような役割分担にしました。 UserData : Fn::Base64 : !Sub | #!/bin/bash -e RedashUsername=$(aws secretsmanager get-secret-value --region ${AWS::Region} --secret-id Redash/RDS/User | jq -r .SecretString) RedashPassword=$(aws secretsmanager get-secret-value --region ${AWS::Region} --secret-id Redash/RDS/Password | jq -r .SecretString) cat <<EOF > /opt/redash/env PYTHONUNBUFFERED=0 REDASH_LOG_LEVEL=INFO POSTGRES_PASSWORD=${!RedashPassword} REDASH_COOKIE_SECRET=$(aws secretsmanager get-secret-value --region ${AWS::Region} --secret-id Redash/CookieSecret | jq -r .SecretString) REDASH_SECRET_KEY=$(aws secretsmanager get-secret-value --region ${AWS::Region} --secret-id Redash/SecretKey | jq -r .SecretString) REDASH_REDIS_URL=redis://${ElasticacheClusterForRedash.RedisEndpoint.Address}:${ElasticacheClusterForRedash.RedisEndpoint.Port}/0 REDASH_DATABASE_URL=postgresql://${!RedashUsername}:${!RedashPassword}@${RDSDBClusterForRedash.Endpoint.Address}:${RDSDBClusterForRedash.Endpoint.Port}/${!RedashUsername} REDASH_FEATURE_EXTENDED_ALERT_OPTIONS= true EOF cat <<EOF > /opt/redash/docker-compose.yml version : "2" x-redash-service : &redash-service image : redash/redash:8.0.0.b32245 env_file : /opt/redash/env restart : always services : server : <<: *redash-service command : server ports : - "5000:5000" environment : REDASH_WEB_WORKERS : 4 scheduler : <<: *redash-service command : scheduler environment : QUEUES : "celery" WORKERS_COUNT : 1 logging : driver : awslogs options : awslogs-region : ap-northeast-1 awslogs-group : redash_scheduler_logs awslogs-stream : redash_scheduler scheduled_worker : <<: *redash-service command : worker environment : QUEUES : "scheduled_queries,schemas" WORKERS_COUNT : 1 adhoc_worker : <<: *redash-service command : worker environment : QUEUES : "queries" WORKERS_COUNT : 2 nginx : image : redash/nginx:latest ports : - "80:80" depends_on : - server links : - server:redash restart : always EOF sudo docker-compose -f /opt/redash/docker-compose.yml up -d --build まとめ Redashは監視の役割も担っていたので、可用性の低い状態で他のサービスの監視をすることに不安がありました。その不安を払拭するためにも、EC2 Image Builderを導入したことで初回起動時に発生していたRedashの運用課題を解決でき、監視ツールとして可用性を高められました。 また、分析ツールとしてもクエリの実行数に応じてオートスケールが可能になりました。加えて、CloudFormationを使いコードとして管理できるので、カスタムAMIの運用負荷だけではなく属人的な運用を防ぐ意味でも役立ちそうです。 実際に試してみることでEC2 Image Builderの仕様も理解できました。同時に運用まで経験することでデバッグのやりにくさや、ビルドからデプロイまでの所要時間の課題感にも気づけました。 そこから、バージョンアップなど定期的に更新が必要な場合に相性が良い仕組みだという知見も得られました。 さいごに ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! https://tech.zozo.com/recruit/ tech.zozo.com
アバター
はじめに SRE部プラットフォームSREチームの川崎 @yokawasa です。 ZOZOTOWNではモノリシックなアーキテクチャーから、優先度と効果が高い機能から段階的にマイクロサービス化を進めています。本記事では、そのZOZOTOWNの段階的なマイクロサービス移行で実践しているカナリアリリースとサービス間通信の信頼性向上の取り組みについてご紹介します。 なお、ZOZOTOWNのリプレイス戦略ついてはこちらのスライドが参考になります。 speakerdeck.com さて、ZOZOTOWNマイクロサービスプラットフォーム(以下、プラットフォーム)はAWS上に構築しており、コンテナーアプリ基盤にマネージドKubernetesサービスであるEKSを採用しています。また、複数サービスを単一Kubernetesクラスターで稼働させる、いわゆるマルチテナントクラスター方式を採用しています。 下記イメージは、そのマルチテナントクラスター(以下、クラスター)に展開されているマイクロサービスとクライアントからマイクロサービスへのリクエストフローを表した概念図です。本記事ではこの中の青点線で囲んだ部分にフォーカスしてその取り組みをご紹介します。 ZOZO API Gatewayを軸にした段階的なマイクロサービスへの移行 本プラットフォームでは、クライアントが直接サービスと通信するのではなく、すべてのリクエストをZOZO API Gatewayと呼ばれるアプリケーションを経由してサービスにルーティングする API Gatewayパターン を採用しています。 ZOZO API GatewayはURIパスベースのルーティング機能を提供し、ルーティング先であるターゲットをまとめたターゲットグループという単位でカナリアリリースの機能を提供します。また、ターゲットへのルーティングにおいてリトライ制御、タイムアウトなど通信の信頼性を高める機能を提供します。 特定のマイクロサービス移行に際して、これらの機能のおかげで古いエンドポイントから新しいものへの切り替えに対しても、クライアントがURI変更の影響を受けることなく安定的かつ段階的な切り替えが可能になります。 下図は、 /search で始まるパスのリクエストをターゲットであるZOZO Search API PrimaryとCanaryにそれぞれ90対10で加重ルーティングするイメージです。 ZOZO API GatewayはGolangで独自実装しており、アルゴリズムや細かな動作制御パラメーター、可用性の機能などZOZOTOWNのさまざまな独自要件に対して柔軟に対応が可能です。まさに、ZOZOTOWNのマイクロサービスアーキテクチャーへの段階的な移行を支える中心的なコンポーネントと言えます。 ZOZO API Gatewayについては各機能や実装レベルの詳細が書かれた人気の記事があるので、是非ご覧ください。 techblog.zozo.com techblog.zozo.com ALB加重ルーティングによるAPI Gatewayのカナリアリリース ZOZO API Gatewayをカナリアリリースするための手法を紹介します。 ZOZO API Gatewayの前段にはApplication Load Balancer(以下、ALB)があり、クライアントからのすべてのリクエストはALBからZOZO API Gatewayにフォワードされます。ZOZO API GatewayのカナリアリリースはこのALBが持つ加重ルーティング機能を活用して実現します。そして、このALB加重ルーティング設定の自動化を実現するのが AWS Load Balancer Controller (以下、コントローラー)です。 このコントローラーをクラスターにデプロイすると、Ingressリソースに指定するパスベースのルーティングや接続ターゲットの情報に基づきALBが作成され、ALBのTargetGroupsとしてアプリケーションPodに直接ルーティングするよう、自動的にALBリスナールールを設定します。 以下、ZOZO API GatewayにおけるIngressマニフェストの設定例を紹介します。 TargetGroups部分にカナリアリリースにおける既存のサービスの zozo-api-gateway-primary と一部のリクエストを振り分けたい新しいサービスである zozo-api-gateway-canary を登録します。それぞれの比重を変更してクラスターに適用すると、Ingressリソースの更新イベントを常時モニタリングしているコントローラーにより自動的に指定された比重でALBリスナールールが更新され、ZOZO API Gatewayへのトラフィックの加重率が変更されます。 apiVersion : networking.k8s.io/v1beta1 kind : Ingress metadata : name : zozo-api-gateway-ingresss annotations : kubernetes.io/ingress.class : alb alb.ingress.kubernetes.io/target-type : ip alb.ingress.kubernetes.io/scheme : internet-facing alb.ingress.kubernetes.io/actions.forward-external-traffic : | { "Type" : "forward" , "ForwardConfig" :{ "TargetGroups" :[ { "ServiceName" : "zozo-api-gateway-primary" , "ServicePort" : "80" , "Weight" : 90 } , { "ServiceName" : "zozo-api-gateway-canary" , "ServicePort" : "80" "Weight" : 10 } ] } } spec : rules : - http : paths : - path : /* backend : serviceName : forward-external-traffic servicePort : use-annotation ALB Load Balancer Controllerのannnotation設定について詳しくは 公式リファレンス を参照ください。 Istioを活用したサービス間通信のトラフィック制御 Istio を活用したサービス間通信におけるトラフィック制御についてご紹介します。なお、本記事ではサービスメッシュの概要や、Istioそのものに関する説明はしません。 Istioサービスメッシュの導入背景について ZOZO API GatewayからマイクロサービスへのルーティングにおいてはZOZO API Gatewayのトラフィック制御機能が使えますが、マイクロサービスと他サービス(クラスター外のサービスを含む)間の通信に対しても一貫した機能を提供したいという思いがありました。 これを実現するために出てきた選択肢に以下の3つがありました。 マイクロサービス間の通信でもZOZO API Gatewayを介し、一貫したトラフィック制御機能を提供する タイムアウトやリトライ制御などの機能を提供する共通ライブラリを各アプリケーションに組み込む サービスメッシュを活用し、ソースコードを変更することなくアプリケーションPodにSidecarパターンでプロキシを注入して、透過的に機能を追加する 1については、ZOZO API Gateway独自に設定しているクライアント認証設定の手間と、ZOZO API Gatewayへの負荷を考慮すると現実的ではありませんでした。また2は、ZOZOTOWNのように利用言語やフレームワークが統一されていない多様な環境をサポートする必要がある状況下では難しさがありました。最終的に、3のサービスメッシュがもっとも現実的であるという結論に至りました。 そして、我々は次のような理由からIstioを選定して、2020年後半から検証を進めました。 サービスメッシュの管理ツールの中でも 比較的利用実績が多い 我々が分散トレーシングに利用しているDatadogが Istioとのインテグレーション をサポートしている 利用クラウド基盤に影響されず、同様のユーザー体験が実現できそうである ZOZO Aggregation APIにおける設定例 3月18日に ZOZOCOSMEやZOZOVILLAがリリース されましたが、この裏側で利用されているマイクロサービスではじめてIstioを導入しました。 このマイクロサービスはZOZO Aggregation APIと呼ばれ、いわゆるBackends for Frontends(BFF)層としての複数APIの結果を集約し、フロントエンドの仕様に特化したレスポンスを返却します。 ZOZO Aggregation APIでは、下図のようにSidecarプロキシでネットワーク接続されたサービスメッシュ内ネットワーク(以下、メッシュネットワーク)のサービス間の通信とメッシュネットワーク外にあるサービスとの通信の2パターンにおいてIstioによるトラフィック制御の設定をしています。 はじめに、メッシュネットワーク内のZOZO Aggregation APIと検索機能を提供するZOZO Search APIサービス間の通信の設定例を紹介します。 以下のサンプルは Virtual Service というルーティングの振る舞いを定義するカスタムリソースのHTTPルーティング部分ですが、ここでZOZO Search APIへの加重ルーティングの比重、タイムアウトやリトライ制御を設定します。今回の例では、上図のように新旧それぞれ90対10の加重ルーティングと、5秒タイムアウトで5xxや接続エラーに対して最大2回のリトライ制御を設定しています。なお、サービス間通信設定では他にも Destination Rule というIstioのカスタムリソースの定義が必要になりますが、ここでは省略しています。 http : - route : - destination : host : zozo-search-api.searchns.svc.cluster.local subset : zozo-search-api-primary weight : 90 - destination : host : zozo-search-api.searchns.svc.cluster.local subset : zozo-search-api-canary weight : 10 retries : attempts : 2 perTryTimeout : 4s retryOn : 5xx,connect-failure timeout : 5s 次に、メッシュネットワーク外にあるBackend APIサービスとの通信設定を紹介します。 以下のサンプルもメッシュネットワーク内サービス間通信と同じくVirtual ServiceのHTTPルーティング部分です。ここでは、6秒タイムアウトで5xxや接続エラーに対して最大2回のリトライ制御を設定しています。なお、メッシュネットワーク外とのサービス間通信設定では他にも Service Entry というカスタムリソースの定義が必要になりますが、ここでは省略しています。 http : - route : - destination : host : zozo-backend-api.zozo-sample-service.com retries : attempts : 2 perTryTimeout : 3s retryOn : 5xx,connect-failure timeout : 6s 分散トレーシング 上述の通り、本プラットフォームでは、ALBからZOZO API Gatewayへのルーティング、そこからマイクロサービスへのルーティングという通信連携があります。さらに、Istioを導入してからはサービスメッシュプロキシを通じてサービス間通信が透過的にルーティングされるため、より一層複雑性が増しています。 こういった中で、問題の発生箇所やパフォーマンスのボトルネック、信頼性の機構が期待通りに機能しているかなどをログやメトリクスのみから追うのは大変困難であることが容易に想像できます。 このような問題の解決策として本プラットフォームでは構築初期の頃から分散トレーシングを導入しており、バックエンドサービスとしてDatadog APMを活用しています。 ここでは、先日リリースしたZOZO Aggregation APIへのリクエストの処理状況を表すフレームグラフをご紹介します。ZOZO API GatewayからZOZO Aggregation APIにルーティングされ、そこから複数サービス間との通信で集約された結果がZOZO API Gatewayにより返されるまでの処理状況が一気通貫で確認可能です。 本プラットフォームにおけるDatadogを活用した可観測性の取り組みについて詳細はこちらの発表資料を参照ください。 speakerdeck.com 構成管理とCI/CD 本プラットフォームでは、インフラからアプリまでサービス環境の構成は可能な限りIaC化しており、その構築・更新はCI/CDパイプラインから行うことを基本としています。今回ご紹介した各所のカナリアリリースや、通信の信頼性のための設定についても当然ながら下図のようにCI/CDを起点としてサービス環境にロールアウトされる流れにしています。 なお、ZOZOTOWNマイクロサービスプラットフォームのCI/CD戦略に関しては、こちらの記事で解説していますので是非ご覧ください。 techblog.zozo.com ちなみに、Istioの構成管理ですが、 Istio Operator というKubernetes Operatorを利用して、IaC化とCI/CDを通じた自動ロールアウトを実現しています。IstioOperatorカスタムリソースに構成設定を定義してクラスターにデプロイすると、カスタムリソースの定義を元にインストールやアップグレード、Istio全体の設定やコンポーネントごとの設定を自動ロールアウトしてくれます。 まとめ ビックバンアプローチで全体を一気にマイクロサービスアーキテクチャーとしてリリースするケースがある一方、既存機能を動かしながら多様な環境状況を考慮しつつ段階的に移行するケースがあります。本記事では後者のケースにおいてそれを支えるためにZOZOTOWNで実践しているカナリアリリースとサービス間通信の信頼性向上の取り組みについてご紹介しました。 本記事では深く紹介できませんでしたが、ZOZO Aggregation APIやIstioについてはプロダクションリリース要件をクリアするまでにさまざまなチャレンジがありました。また、Istioは今後マイクロサービス全体にその利用広げていき、サーキットブレーカーをはじめとしたより高度な機能活用を行っていく予定です。これらについては別の記事にてその詳細をご紹介できればと思っております。 さいごに ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 tech.zozo.com
アバター
こんにちは、SRE部ZOZO-SREチームに2020年新卒入社した秋田です。普段はZOZOTOWNのオンプレミスとクラウドの運用・保守・構築に携わっています。 ZOZOTOWNのオンプレミスは17年の歴史があり、BIG-IP、FortiGate、vSphereなどの様々なベンダーの製品が稼働しています。さらに、ZOZOTOWNのサービスが大きくなるにつれてオンプレミスでの拡張も続けていました。 そのようなZOZOTOWNですが、VMware Cloud on AWSを活用することでオンプレミスとパブリッククラウドを掛け合わせた柔軟なインフラを構築しています。実際に、昨年から大規模なセール時に、オンプレミスを拡張するのではなくVMware Cloud on AWSを活用したサーバー増強をしています。 本記事では、ZOZOTOWNのオンプレミス環境の自動化で利用しているAWXについて、チーム内での活用方法や運用方法を実際のセールの事例を用いて紹介します。なお、その一環でOSSとして action-ansiblelint の公開も実現しています。 AWXとは まず初めにAWXについて紹介します。AWXは、AnsibleをWebアプリケーションで管理し、REST APIやタスクエンジンを提供しているOSSです。 また、AWXは商用版のRed Hat Ansible Tower(以下、Ansible Tower)のアップストリームプロジェクトです。 AWXとAnsible Towerの大きな違いとしては、専門的サポートが受けられる点や安定かつ安全に利用できる点です。AWX/Ansible Towerでは、日本語の ドキュメント も提供されており、日本のユーザーはインストールガイドやユーザーガイドなど様々な情報を容易に得ることができます。 github.com 私たちのチームでは、コスト面やAnsibleの利用経験が浅いことを考慮し、商用版のAnsible TowerではなくOSSのAWXを利用しています。 取り組み以前のAWXに関する課題 今回紹介する取り組み実施前は、AWXを導入した前任者が別チームに異動したこともあり、いくつかの課題がありました。 Ansible Playbookがローカル管理されていた 確立された運用方法がなかった 各課題についてどういったアプローチをとったか説明します。 課題1:Ansible Playbookがローカル管理されていた オンプレミスではAWX検証環境とAWX本番環境の2つの環境があり、それぞれでコードも違う状態でした。AWX検証環境とAWX本番環境のコードは同じ状態が望ましいため、AWX検証環境のコードをAWX本番環境のコードに統一することとしました。 そして、ローカルで管理されていたAnsible Playbookのコードは、GitHubへ移行することにしました。しかし、チームにおけるGitHubの利用ルールや使い方が定まっていなかったので、同時に策定する必要がありました。 まず、新規作成したリポジトリでAWX検証環境用のブランチ(例:dev)とAWX本番環境用のブランチ(例:prd)を用意し、それぞれGitHub上にコードを上げました。AWX本番環境用のブランチをAWX検証環境用のブランチにマージすることで、差分があった箇所はAWX本番環境のコードに統一できました。 また、 git-flow と GitHub Flow を参考にチームでGitHubの利用ルールを作成しました。利用ルールと同時にGitHubの使い方についてもまとめました。これは、今後、都度修正していく前提で作成しています。 利用ルールとして定めたものを一部紹介します。 mainブランチは常に本番稼働・実行できる状態にする 作業用ブランチをmainブランチから作成する 作業用ブランチは定期的にプッシュする プルリクエストによるレビューを必須とする プルリクエストでレビューが完了したらmainブランチへセルフマージする releaseブランチを用いて本番リリースする reset、cherry-pickはしない ルールをさらに具体化するために、使い方として以下のような流れをまとめました。 mainブランチから作業用ブランチを作成して、開発する 開発が完了したら、ローカル上で作業用ブランチに作業内容を日本語で簡潔に記載したコミットメッセージを添えてコミットし、GitHub上にプッシュする プルリクエストを作成する レビュー内容に応じて必要があればコードを修正して再度コミットとプッシュする 再度コードレビューをしてもらいアプルーブであれば、mainブランチにセルフマージする ルールや使い方をまとめておくことで、チームメンバーにGitやGitHubの経験に差があったとしても、GitHubを使うことやGit操作に対してのハードルを下げることができます。 課題2:確立された運用方法がなかった 前任者が書いた構築・運用に関するドキュメントがあったので、そのドキュメントを参考に以下の方針を立て、運用方針の策定とドキュメント化を実施しました。 検証・本番環境ごとのAWX作業手順 Ansible Playbookの記述方法 秘匿情報の扱い方 AWXのメンテナンス方法 各方針について、以下で説明していきます。 検証・本番環境ごとのAWX作業手順 これまではAnsible Playbookがローカル管理だったこともあり、AWX検証環境とAWX本番環境の作業手順は以下のようになっていました。 ローカルにProjectから参照できるAnsible Playbookを配置 AWX上でテンプレートの作成 テンプレートの実行、エラーがあれば修正 AWX本番環境では、AWX検証環境で作成したものをコピーして同様に作業していました。この方法では、AWX検証環境で検証はしているとはいえコードのAnsible Playbookで間違いがあった場合には、AWX本番環境でそれを実行して事故を起こしかねない状態でした。 Ansible Playbookの管理をGitHubへ移行すると同時に、作業手順と利用方法を見直しました。 まず初めに、GitHubをAWX検証環境とAWX本番環境に連携する設定をしました。具体的には、AWXのProjectのSource Code Management(以下、SCM)の機能を利用してGitHub上のAnsible PlaybookをProjectに反映させます。Projectの設定は、以下のドキュメントを参考にしています。 docs.ansible.com 見直したAWX検証環境とAWX本番環境での作業手順が以下の通りです。 AWX検証環境での作業手順 AWX検証環境用ブランチを最新の状態にする AWX検証環境用ブランチから作業用ブランチを作成する Ansible Playbookを作成し作業をする AWX検証環境で利用するためにコミットとプッシュを行う AWX検証環境でテンプレートを作成し、トライ&エラーを繰り返して開発を進める AWX検証環境で検証が完了後、検証環境用ブランチに対してプルリクエストを作成する プルリクエストをレビューしてもらい問題なければ検証環境用ブランチにセルフマージする プロジェクトを更新し、テンプレートのSCMブランチを削除する レビューを必須とすることでAnsible Playbookの確認をチームで行い、作業ミスを減らすことができます。また、AWX上ではSCMの機能を使ってテンプレートを以下のように設定し、5.のステップを作業用ブランチで実行できるようにしています。 AWX本番環境での作業手順 AWX検証環境用ブランチにマージされたタイミングで、GitHub ActionsによりAWX本番環境用ブランチへのプルリクエストが自動作成される 作成されたプルリクエストをセルフマージする AWX本番環境でテンプレートの作成、実行する 検証とレビューで問題ないことが保証されているため、セルフマージするようにしています。AWX本番環境では、AWX検証環境で作ったテンプレートをコピーするテンプレートを用意しており、同じものを作成する手間を省いたりしています。 Ansible Playbookの記述方法 これまでは、ローカル管理されていたこともあり、自由にAnsible Playbookが記述されていました。GitHub管理に移行したので、これを気にLinterを用いてAnsible Playbookの統一化することにしました。Ansibleでは、 ansible-lint がLinterとして提供されています。 GitHub Actionsでプルリクエストにある差分のAnsible Playbookに対してLinterを実行するようにしています。 reviewdog を用いることでプルリクエストにある差分のAnsible Playbookに対してLinterの実行が可能です。 reviewdogは、Linterの結果をプルリクエストのコメントに出力したりできます。詳しく知りたい方はREADMEや以下のドキュメントを参考にしてください。 haya14busa.com reviewdogのコミュニティでは、GitHub Actionsで利用できるように多くのエンジニアが、それぞれの言語向けのLinterのActionを作成しています。 そこで、ansible-lint用のAction、 action-ansiblelint を作成しました。 GitHub Actionのコードと実行結果を以下に示します。 name : Check Source Code on : [ pull_request ] jobs : ansible-lint : name : runner / ansible-lint runs-on : ubuntu-latest steps : - uses : actions/checkout@v2 - uses : actions/setup-python@v2 with : python-version : 3.6 - name : Execute Ansible Lint uses : reviewdog/action-ansiblelint@v1.2.1 with : github_token : ${{ secrets.github_token }} reporter : github-pr-review 構文に間違いがあるとプルリクエストにエラー箇所をコメントしてくれるように設定しています。 OSSの開発については、弊社では OSSポリシー があり、スムーズに開発・公開でき、とても良い経験ができました。action-ansiblelint公開後はOrganizationをreviewdogに移す対応を行い、メンテナンスを定期的に行っています。 techblog.zozo.com action-ansiblelintを作成したことで、誰でも簡単に利用できるようにする、かつコードの統一化といった最初の目的を果たすことができました。 秘匿情報の扱い方 当然のことですが、秘匿情報はGit上で管理せずAWXの認証情報で管理しています。認証情報ではSSHやネットワーク機器、クラウドのログイン情報などをサポートしています。 ローカル管理だった際には、認証情報でサポートされていない部分は直接Ansible Playbookに記述されていましたが、今回はその直接の記述を外す必要があります。 その際に、認証情報でサポートできない項目も出てきます。そのような時に便利なのがカスタム認証情報タイプです。 docs.ansible.com 入力設定のところでYAML形式かJSON形式で以下のような認証情報を作成できます。 fields : - id : username type : string label : Username - id : password type : string label : Password secret : true required : - username - password インジェクターの設定では以下のように記述します。 env : Sample_Password : '{{ password }}' Sample_Username : '{{ username }}' 設定すると以下のような表示になります。 作成した認証情報をAnsible Playbookで利用する場合は、Lookupプラグインを使うことで呼び出しが可能です。 - name: Set Fact set_fact: username: "{{ lookup('env', 'EXECUTION_USERNAME') }}" password: "{{ lookup('env', 'EXECUTION_PASSWORD') }}" このカスタム認証情報タイプを用いて、直接Ansible Playbookに記述していたユーザー名とパスワードを認証情報で管理できました。 AWXのメンテナンス方法 AWXのメンテナンスは、KerberosやDocker Composeを含んだ環境構築自動化のためのAnsible Playbookを用いて行います。そうすることで、新しいバージョンが出た際にも、別VMで環境を作成し検証できます。AWXの環境構築で使っているAnsible Playbookでは、インベントリーファイルの変数を使ってAWXなどのバージョンを切り替えられるようにしています。その他の部分も変数化しておくことで、柔軟な環境構築ができるようにしています。 localhost ansible_connection=local ansible_python_interpreter="/usr/bin/python3" [all:vars] # AWX version awx_version=17.0.1 # containerd.io containerd_io='https://download.docker.com/linux/centos/7/x86_64/stable/Packages/containerd.io-1.2.13-3.2.el7.x86_64.rpm' # Docker Compose Version docker_compose_version=1.26.0 # OS ## uname -s os_system=Linux ## uname -m os_architecture=x86_64 # 環境ごとに変更 server_env=dev 同時にバックアップ方法の確立も必要です。 AWX/Ansible Towerでは、APIを利用したCLIが提供されています。なお、AWXのCLIは、 pip install awxkit でインストールできます。 CLIの基本的な使い方は以下のドキュメントに記載されています。 docs.ansible.com このCLIを用いてバックアップスクリプトを作成していた際に、エクスポート機能にinventoryのhostが取得できないバグを発見しました。そこで、issueを書いて以下のバグレポート上げました。 github.com バージョン 15.0.0で対応され、エクスポート機能が利用できるようになりました。 このエクスポート機能を用いて、以下のようにエクスポートすることでバックアップを取得できます。 $ awx export > backup.json このコマンドを実行するスクリプトを作成し、Cronで毎日バックアップを取得するように実行しています。 インポートもエクスポート同様、以下のコマンドで実行できます。インポートする際の注意点は、パスワードなどの秘匿情報はエクスポートされていないのでインポートした際には、手動でパスワードなどを再設定する必要がある点です。 $ awx import < backup.json チーム内で勉強会を実施 上述した新しい運用方針を策定したので、それに伴って今までのAWXの利用方法に加えてGit/GitHubに関する勉強会を行いました。この勉強会の目的は、AWXの新しい作業手順を知ってもらいGit操作やGitHubに慣れてもらうことです。 勉強会内容は以下の内容です。 BIG-IPの操作が可能なPlaybookの作成方法 AWXのテンプレート、インベントリー、認証情報の作成方法 Git/GitHubの利用方法 AWXの検証環境と本番環境の利用方法 Red Hat社が定期的に開催しているワークショップに参加し、得られた知見はこの勉強会に反映しています。また、GitHub上にそのワークショップで利用するドキュメントが公開されているのでそちらの内容を参考にしています。 github.com 2021年の冬セール準備での実例 2021年の冬セールは、コロナ禍ということもあり、トラフィック量の予想がしづらい状況でした。そのため、去年の冬セールの2倍以上のサーバーをオンプレミスとVMware Cloud on AWSを組み合わせて準備することにしました。 新規サーバーの構築後、サービスインできる状態にするまでには、いくつかのステップが必要です。 新規作成したサーバーの設定ファイル変更 ファイル配布サーバーの設定変更 新規作成したサーバに対して現行サーバーからのコンテンツ同期 BIG-IPのノードにサーバーを追加 BIG-IPのプールにサーバーを追加 この5つのステップの中で、2.は自動化が難しい部分ですが、3.に関しては既に自動化されていました。今回の冬セールに向けて1.・4.・5.の自動化を実施することにしました。 1.の自動化は、各サーバーに決まった値を設定値として指定するものでした。そのため、設定する値のリストを用意し、AWXで各サーバーに設定しました。 一方で、4.と5.では bigip_node と bigip_pool_member モジュールを利用して新しいサーバーを追加できるようにしました。 これらの自動化により、約10時間以上かかっていた作業が約2時間ほどに短縮されました。 また、これら以外にも、AWXのインベントリーに数百台規模のホストを追加する作業があります。インベントリーの作成でクラウドプロバイダーから同期する方法もありますが、弊社の環境ではグループごとに変数などを設定していたことから既存のインベントリーに追加する必要がありました。 そこで、カスタムインベントリースクリプトを利用しました。カスタムインベントリースクリプトを利用することで独自のインベントリーソースが作成でき、特定のグループに追加ができます。 docs.ansible.com PythonやShellなどで記述し、スクリプトとして実行できます。出力をJSON形式にすることで数百台をインベントリーのホストに登録できます。ここで示すスクリプトのサンプルは以下の記事を参考に作成しました。 qiita.com スクリプトのサンプルと実行結果を以下に示します。なお、実行結果は一部省略しています。 #!/usr/bin/env python from collections import defaultdict import json class SampleInventory(object): def __init__(self): self.inventory = {} self.inventory = self.sample_inventory() print(json.dumps(self.inventory, indent=2)) def sample(self, number): # ホスト名 sample = "Sample" + str(number) # 追加する台数 sample_num = 200 samples = [] for n in range(1, sample_num+1): if len(str(n)) == 1: # Sample0001 ~ Sample0009 samplexxxx = sample + "00" + str(n) samples.append(samplexxxx) elif len(str(n)) == 2: # Sample0010 ~ Sample0099 samplexxxx = sample + "0" + str(n) samples.append(samplexxxx) else: # Sample0100 ~ Sample0200 samplexxxx = sample + str(n) samples.append(samplexxxx) return samples def sample_inventory(self): multi_dimension_dict = lambda: defaultdict(multi_dimension_dict) inventory = multi_dimension_dict() inventory["sample_group"]["hosts"] = self.sample(0) for sample in self.sample(0): inventory["_meta"]["hostvars"][sample] = {} return inventory SampleInventory() { " sample_group ": { " hosts ": [ " Sample0001 ", " Sample0002 ", " Sample0003 ", #省略 " Sample0199 ", " Sample0200 " ] } , " _meta ": { " hostvars ": { " Sample0001 ": {} , " Sample0002 ": {} , " Sample0003 ": {} , #省略 " Sample0199 ": {} , " Sample0200 ": {} } } } このスクリプトでは、ホスト名 Sample の部分が共通部分で、そこに適宜数値を結合し、その結果をJSON形式で出力するようにしています。今回のサンプルでは取り入れていませんが、vSphereのAPIを活用すればもっといい書き方ができるでしょう。 まとめ 本記事ではZOZOTOWNのオンプレミス環境の自動化で利用しているAWXについて、チーム内での活用方法や運用方法を実際のセールの事例を用いて紹介しました。 Ansible Playbookのローカル管理を、GitHubでの運用へ移行することでコードの管理が容易になりました。また、CI/CDの導入によりコードの統一も実現できました。 運用方法の策定では、新しく利用方法や運用方法に関するドキュメントを作成し、チーム内でドキュメントの展開とAWXの勉強会をすることでAWXの知識の浸透を実現させました。 最後に ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
アバター
はじめに こんにちは。気がつけば4月でZOZOTOWNに関わって9年目を迎えるSRE部の横田です。普段はSREとしてZOZOTOWNのリプレイスや運用に携わっています。 本記事ではGoogle Cloud PlatformでShared VPCを採用し全社共通ネットワークを構築した背景とその運用方法について説明します。 ZOZOTOWNとパブリッククラウド専用線 まずはZOZOTOWNとパブリッククラウドを接続する専用線について説明します。 数年前まではZOZOTOWNを支える基盤は、ほぼ全てがオンプレミス環境で稼働しており、以下の課題がありました。 システムが密結合であること アジリティの低さ これらを解決するためにパブリッククラウドを活用したマイクロサービス化が日々進んでいます。 現在パブリッククラウドはAmazon Web Services(以下、AWS)とGoogle Cloud Platform(以下、GCP)を主に利用しています。一方でオンプレミス環境でも様々な重要なシステムが稼働しています。それらの異なる環境で稼働するシステムが安定して相互通信を行えるネットワークはZOZOTOWNにとって重要な存在です。 そこで現在は各パブリッククラウドで以下の専用線接続サービスを利用しています。ここではバックアップ回線を含む利用状況を示しています。 AWS: Direct Connect 10Gbps × 6本 GCP: Dedicated Interconnect 10Gbps × 2本 どちらもオンプレミスとパブリッククラウドのネットワークを直接物理的に接続するサービスです。プライベート通信のためにインターネットを介したVPNでのネットワーク経路という選択肢もありますが、膨大なトラフィックに対する品質保証のためにAWSとGCPどちらも直接接続が可能なサービスを弊社では選択しています。オンプレミスとパブリッククラウド間の通信だけではなく、AWSとGCPなどパブリッククラウド同士の通信にも、これらのサービスとデータセンターを経由し、LANで実現させているケースが多々存在しています。 直面していた課題 ここでは、実際に直面していた2種類の課題を紹介します。 BGP設定に対する課題 上述した専用線接続サービスは対向側のオンプレミス環境で要件を満たすルーターにBGP設定を投入することで疎通可能になります。弊社ではBGP設定において以下の課題が存在していました。 BGP設定自体の複雑さ 複数チームで作業するため細かな連携が必要であること AWSの場合は導入当初こそ各VPC毎にVirtual Interfaceを払い出していましたが、現在は Direct Connect Gateway を活用しているケースが多いです。そのため、一度Direct Connect Gatewayを所有するVPCに対してVirtual Interfaceの作成とオンプレミス側でBGP設定を行えば以降は相乗りするVPCにはBGP設定は不要になります。 GCPの場合はこれまでのやり方だと新規プロジェクト作成の度にBGP設定を実施していました。さらに弊社の場合はプロダクション環境とステージング環境、開発環境など複数の環境が異なるGCPのプロジェクト上で稼働しています。そのため何か新しいプロジェクトを立ち上げる場合には複数の環境に対して設定する必要があります。 またオンプレミス環境の設定とクラウド環境の設定は異なるチームで連携して互いに作業する運用だったため、連携コストや作業ミスが発生した場合の切り分けや再設定にどうしても時間を要する課題が潜在的に存在していました。 Dedicated InterconnectのVLANアタッチ上限の課題 ある日、GCPのDedicated Interconnectで致命的な問題に直面しました。 GCPを利用する複数チームから同時期に複数の新規プロジェクトでDedicated Interconnectを利用する要望があり、設定パラメーターなどを連携して各チームが作業した際にある新規プロジェクトで以下のエラーが発生したのです。 Error: Error waiting to create InterconnectAttachment: Error waiting for Creating InterconnectAttachment: Quota 'Interconnect_ATTACHMENTS_ALL_REGIONS' exceeded. Limit: 16.0 globally 1つのDedicated Interconnectに関連付けることができるVLANアタッチメントの最大数の上限である「16」を超過しようとしたため発生したエラーでした。 公式ドキュメント でも上限の引き上げが不可能な項目となっています。 この上限に関して完全に盲点だったのは反省点です。この問題を解決しない限り新規のプロジェクトがDedicated Interconnectを利用できない状況となりました。 対応方針を模索 社内の有識者を集め対応方針を決めていきました。以下に検討した案とそれぞれのメリット、デメリットを紹介します。 Dedicated Interconnect自体を追加する案 シンプルに専用線を追加する案ですがデメリットの要素が多く選択肢からは早々に外れました。 メリット これまで通りの運用で追加が行える VPCのフルコントロール権を各プロジェクトに与えられる デメリット 敷設に様々なコストがかかる 帯域の利用状況としては余裕ある状況でのDedicated Interconnect追加はもったいない 1本追加すれば上限数が16増えるがプロジェクトの増加傾向を考えるとまた同じ悩みに直面する日は近い VPC Network Peeringを使った構成にする案 GCPの VPC Network Peering を使ってDedicated Interconnect設定済のVPCとVPC Network Peeringを行うことで専用線を利用する方法です。 こちらの構成の場合は以下の設定が必要になります。 HubとなるVPCからオンプレミス側へ各プロジェクトのVPC CIDRをアドバタイズ VPC Network Peeringを使いハブとなるVPCと各プロジェクトのVPCを接続 custom route設定でオンプレミス環境のネットワーク経路を各プロジェクトのVPCへ伝播 ハブとなるVPCではカスタム経路をexport 各プロジェクトのVPCではカスタム経路をimport メリット 既存のVPCをこの接続方法に切り替える場合もVPCをそのまま残せる VPCのフルコントロール権を各プロジェクトに与えることができる デメリット 多数のプロジェクトとVPC Network Peeringを行うようになると管理が煩雑になる 接続可能なVPCの 上限が25 こちらはDedicated Interconnectを追加する案よりも魅力的でしたが、接続可能なVPC上限を考慮すると心許ない値だったため、この構成は見送りました。 Shared VPCを使った構成にする案 Shared VPC は組織内の複数のプロジェクトのリソースを共通のVPCに接続する方法です。 メリット Dedicated Interconnect利用に関するBGP設定はホストプロジェクトのみ行えば良い Firewallなどネットワークポリシーの一元管理が可能 接続可能なサービスプロジェクト数が 初期値で1000 と弊社には十分な値 デメリット サービスプロジェクト側で任意のタイミングでVPCに紐づくリソースの変更ができない 既存プロジェクトをShared VPCに移行する場合はサブネットの作り直しになる システム停止など移行方法を検討する必要がある VPCのフルコントロール権限などがなくなってしまう課題はありましたが、ガバナンスを効かせる意味でも今の環境に適しているのはShared VPCという結論となり採用することになりました。 Shared VPCの管理と運用方法 ここからはShared VPCの管理と運用を解説していきます。 Shared VPCの利用が決定した際に以下の方針を立てました。 今後Dedicated Interconnectを利用する新規プロジェクトはShared VPCのサービスプロジェクトとする 既存プロジェクトの移行を現段階では強制しない Shared VPCの管理チームはチーム横断型 1つのネットワーク管理プロジェクトとしてShared VPCは専用のGitHubリポジトリで管理 Terraformを使った構成管理とGitHub ActionsでのCI/CDを行う サービスプロジェクト追加時はサービスプロジェクトのメンバーがPull Request作成 管理チーム 現在、4名の管理チームで運用しています。管理チームの主なタスクはネットワークの採番(予め払い出した巨大なCIDRから細分化して払い出し)とPull Request作成のコードレビューです。 ネットワークの採番では Google Kubernetes Engine (以下、GKE)を使うか使わないかで提供するIPレンジを調整します。基本的にGKEの利用が無い場合はプロダクション環境やステージング環境には/20のCIDRを割当てます。開発環境やQA環境に関してはプロダクション同等のサイジングが必要無いケースも多いため、半分の/21のCIDRを割り当てます。 GKEを利用するプロジェクトの場合は必要となるIPが多くなるため/18のCIDRを割り当てるようにしています。GKEのアドレス管理に関しては 公式ドキュメント にも記載されており参考にしました。 運用方法 Shared VPCの利用依頼からネットワークリソースの作成までを図示します。 Shared VPCの管理リポジトリは各プロジェクトのリポジトリとは完全に分離されており、Shared VPCのためのCI/CDで追加したネットワークリソースを各サービスプロジェクトで指定してCompute Engineなどのリソースを作成します。ネットワークの払い出し以外はコードで完結できるようになっています。 TerraformでShared VPC環境を定義 ここからはShared VPCを構築するTerraformのtfファイルで定義される内容について解説していきます。プロダクション環境やステージング環境など複数の環境に対してCDできるリポジトリ構成としています。 . ├── .github │ └── workflows └──terraform └── gcp ├── dev │ ├── backend.tf │ ├── locals.tf │ ├── main.tf -> ../main.tf │ ├── service1.tf -> ../service1.tf │ └── service2.tf -> ../service2.tf ├── prd # dev同様のファイル構成 ├── qa # dev同様のファイル構成 ├── stg # dev同様のファイル構成 ├── main.tf ├── service1.tf └── service2.tf それぞれのファイルの設定内容を解説していきます。 locals.tf プロダクションやステージングなどの複数環境に対してCDを行うために変数を定義しています。サービスプロジェクトを追加する場合は変数の追加が必要になりますが、そちらについては後述します。なお、値は仮のものですが以下の変数を定義しました。 locals { env = " dev " # 各環境の名称 host_project = " poject-dev " # ホストプロジェクトの名称 # Dedicated Interconnectに関する各種変数を指定 interconnect_region = " asia-northeast1 " # Dedicated Interconnectのregion interconnect_url_main = " https://www.googleapis.com/dev-main " # メイン回線のURL interconnect_url_bkup = " https://www.googleapis.com/dev-backup " # バックアップ回線のURL interconnect_attachment_bandwidth_capacity = " BPS_10G " # アタッチする帯域 router_google_asn = 64512 # GCP側のASN router_peer_asn = 65000 # オンプレミス側のASN interconnect_attachment_candidate_subnets_main = [ " 192.168.0.0/24 " ] # メイン回線のBGP IPで利用するCIDR interconnect_attachment_candidate_subnets_bkup = [ " 192.168.1.0/24 " ] # バックアップ回線のBGP IPで利用するCIDR interconnect_attachment_vlan_id = 100 # Cloud RouterにアタッチするVLAN # Cloud Routerを利用するregionを定義 nat_router_regions = [ " asia-northeast1 ", " asia-east1 ", ] # マネージドサービスやServerless環境など共通の環境で利用するサブネットを定義 cidr_google_managed_services = " 192.168.10.0/24 " # マネージドサービスに割り当てるサブネット cidr_vpc_serverless_access_connector = " 192.168.11.0/24 " # Serverless環境からVPCへのアクセスを中継するコネクタ用のサブネット cidr_proxy_only_subnet = { " asia-northeast1 " = " 192.168.12.0/24 " # 内部Load Balancer利用のためのプロキシ専用サブネット① " asia-east1 " = " 192.168.13.0/24 " # 内部Load Balancer利用のためのプロキシ専用サブネット② } main.tf main.tf ではホストプロジェクト側で管理すべき以下の内容を定義しています。 Shared VPCのホストプロジェクト、VPC定義 Dedicated Interconnectを利用するための定義 networkViewerの権限を付与するサービスプロジェクトのメンバーの定義 マネージドサービスやServerless環境利用のための定義 Firewall定義 プライベートIPでLoad Balancerを利用するためのプロキシ専用サブネットの定義 Cloud NATコントロールプレーンの定義 またCDのために各環境のディレクトリ配下に(prd/stg/dev/qa)にシンボリックリンクを作成します。 サンプルコードと共に各定義の内容を解説していきます。 Shared VPCのホストプロジェクト、VPC定義 Shared VPCで利用するプロジェクトやVPCを定義します。複数regionのサービスを利用するケースがあるためルーティングモードはグローバルで設定します。 provider " google " { project = local.host_project } provider " google-beta " { project = local.host_project } resource " google_compute_network " " shared_vpc " { name = " shared-vpc-${local.env} " auto_create_subnetworks = false routing_mode = " GLOBAL " } resource " google_compute_shared_vpc_host_project " " host " { project = local.host_project } Dedicated Interconnectを利用するための定義 メイン回線とバックアップ回線にそれぞれ定義します。 BGP IPが再作成される問題に対して社内ナレッジがあったためlifecycleを使って例外設定をします。ignore_changesを利用することでTerraform上の管理しているリソースと実際のリソースに差分がある状況の変更を無視できます。 # Cloud Routerを定義 resource " google_compute_router " " shared_vpc_main " { name = " shared-vpc-main-${local.env} " network = google_compute_network.shared_vpc.id bgp { asn = local.router_google_asn advertise_mode = " CUSTOM " advertised_groups = [ " ALL_SUBNETS " ] advertised_ip_ranges { range = local.cidr_google_managed_services } } region = local.interconnect_region } # Dedicated Interconnectの定義とVLANアタッチメント # 既存のDedicated Interconnectを指定する resource " google_compute_interconnect_attachment " " shared_vpc_main " { admin_enabled = true name = " shared-vpc-main-${local.env} " interconnect = local.interconnect_url_main router = google_compute_router.shared_vpc_main.id bandwidth = local.interconnect_attachment_bandwidth_capacity candidate_subnets = local.interconnect_attachment_candidate_subnets_main region = local.interconnect_region vlan_tag8021q = local.interconnect_attachment_vlan_id # Avoid force replacement lifecycle { ignore_changes = [ candidate_subnets, ] } } # BGP IPを定義 resource " google_compute_router_interface " " shared_vpc_main " { name = " router-interface-shared-vpc-main-${local.env} " router = google_compute_router.shared_vpc_main.name ip_range = google_compute_interconnect_attachment.shared_vpc_main.cloud_router_ip_address interconnect_attachment = google_compute_interconnect_attachment.shared_vpc_main.id region = local.interconnect_region lifecycle { ignore_changes = [ ip_range, ] } } # オンプレミス側ルーターとのBGPセッションを定義 resource " google_compute_router_peer " " shared_vpc_main " { name = " shared-vpc-${local.env} " router = google_compute_router.shared_vpc_main.name region = local.interconnect_region advertised_groups = [] advertised_route_priority = 0 peer_ip_address = replace ( google_compute_interconnect_attachment.shared_vpc_main.customer_router_ip_address, " /29 ", "" ) peer_asn = local.router_peer_asn interface = google_compute_router_interface.shared_vpc_main.name } networkViewerの権限を付与するサービスプロジェクトのメンバーの定義 サービスプロジェクトのメンバーにShared VPCのnetworkViewer権限を付与するための定義です。この権限を付与しないとShared VPCで作成したサブネットの情報を各サービスプロジェクトのメンバーが参照できません。 resource " google_project_iam_member " " network_viewer " { count = length ( local.all_service_projects_members ) project = local.host_project role = " roles/compute.networkViewer " member = element ( local.all_service_projects_members, count.index ) } マネージドサービスやServerless環境利用のための定義 Cloud SQLなどのマネージドサービスやServerless環境に対してプライベートIPでアクセスするための定義します。設定詳細については公式ドキュメントもご参照ください。 Private Service Access Serverless VPC Access # マネージドサービスとのVPC Network Peering設定 resource " google_compute_global_address " " private_ip_alloc_google_managed_service " { name = " google-managed-services-${google_compute_network.shared_vpc.name} " purpose = " VPC_PEERING " address_type = " INTERNAL " prefix_length = tonumber ( element ( split ( " / ", local.cidr_google_managed_services ) , 1 )) network = google_compute_network.shared_vpc.id address = element ( split ( " / ", local.cidr_google_managed_services ) , 0 ) } resource " google_service_networking_connection " " private_service_connection_google_managed_service " { network = google_compute_network.shared_vpc.id service = " servicenetworking.googleapis.com " reserved_peering_ranges = [ google_compute_global_address.private_ip_alloc_google_managed_service.name ] } resource " google_compute_network_peering_routes_config " " private_service_access_mysql " { peering = " cloudsql-mysql-googleapis-com " network = google_compute_network.shared_vpc.name import_custom_routes = false export_custom_routes = true } # Serverless環境とVPCを接続するためのコネクタ設定 resource " google_vpc_access_connector " " connector " { name = " vpc-access-connector " region = local.interconnect_region ip_cidr_range = local.cidr_vpc_serverless_access_connector network = google_compute_network.shared_vpc.name } Firewall定義 Shared VPCに対するFirewallを定義します。設定する際のポイントは以下の点です。 Identity-Aware Proxy のように全サービスプロジェクトが利用する仕組みに関しては予め許可設定にする Shared VPCからマネージドサービスに対する通信(Egress)は予め拒否設定にする 先ほども触れましたがCloud SQLなどのマネージドサービスとプライベートIPでの通信を行う要件がありPrivate Service Accessを利用しています。Private Service Accessは作成時にPrivate Service Access用のサブネットにCIDRを指定しますが、ユーザー管理下のVPCではなくGCP管理下のVPCに作成されます。作成されたPrivate Service AccessとShared VPCをVPC Network PeeringすることでプライベートIPでの接続が可能になります。 Cloud SQLのプライベートIPでの利用時には 1つのリージョンと1つのデータベースタイプにつき最小/24のサブネットが指定したCIDR内から割り当てられる要件 が存在します。Cloud SQL側のFirewallで承認済みネットワークにプライベートサブネットを指定することができないため、何も制御を行わない場合はShared VPC内のどのようなサービスプロジェクトのリソースでもCloud SQLにインスタンスレベルでのアクセスが可能になってしまいます。 この問題を回避するために送信元側で通信を制御します。 Firewallのコンポーネント のデフォルト設定ではEgressは全て許可設定のため、上述したようにShared VPC内からマネージドサービスのサブネットに対するDeny設定を投入しています。Shared VPC内各サービスプロジェクト毎に通信要件のあるCloud SQLに対しTagまたはService AccountへEgressの許可ルールをDenyよりも高い優先順位で作成することで制御を行うことにしました。 resource " google_compute_firewall " " allow_ssh_from_iap " { name = " allow-ssh-from-iap " network = google_compute_network.shared_vpc.name priority = 65534 allow { protocol = " tcp " ports = [ " 22 " ] } source_ranges = [ " 35.235.240.0/20 " ] } resource " google_compute_firewall " " deny_all_to_private_service_access " { name = " deny-all-to-private-service-access " network = google_compute_network.shared_vpc.name priority = 65532 direction = " EGRESS " deny { protocol = " tcp " ports = [ " 0-65535 " ] } destination_ranges = [ local.cidr_google_managed_services ] } プライベートIPでLoad Balancerを利用するためのプロキシ専用サブネットの定義 プライベートIPでLoad Balancerを利用するシーンも多いためプロキシ専用のサブネットをShared VPC内に定義します。プロキシ専用のサブネットはregion毎に1つしか作成できないため main.tf で管理します。 resource " google_compute_subnetwork " " proxy_only_subnet " { provider = google - beta for_each = local.cidr_proxy_only_subnet name = " proxy-only-subnet-${each.key} " ip_cidr_range = each.value region = each.key network = google_compute_network.shared_vpc.self_link purpose = " INTERNAL_HTTPS_LOAD_BALANCER " role = " ACTIVE " } Cloud NATコントロールプレーンの定義 プライベートサブネットからインターネットに接続するためにはCloud NATを利用します。 Cloud NATはSDNな分散マネージドサービスのため以下2つの要素から定義されます。 Cloud NATコントロールプレーン Cloud NATゲートウェイ 以下の理由から main.tf でコントロールプレーンを定義します。 ゲートウェイに割り当てるIPアドレスの数などは各プロジェクトの用途により変わるため各サービスプロジェクトの定義ファイルで管理したい Cloud NATコントロールプレーン(Cloud Router)は ネットワーク毎に1つのRegionあたり5つまでという上限 がある resource " google_compute_router " " nat-router " { for_each = toset ( local.nat_router_regions ) name = " nat-router-${each.value} " region = each.value network = google_compute_network.shared_vpc.self_link bgp { asn = local.router_google_asn } } ここまでホストプロジェクト側で予め準備してきたtfファイルを解説してきました。次は各サービスプロジェクトがShared VPCを利用する場合に作成、変更するファイルについて解説していきます。 サービスプロジェクトのメンバーにより作成・変更するファイル 新規にサービスプロジェクト側でShared VPCのサブネットを利用する場合は以下のtfファイルを作成、追加変更をします。 各サービスプロジェクトの設定項目を記載する サービスプロジェクト名.tf (新規作成) サンプルとしてservice1を記載 locals.tf (追記) 順に詳細を解説していきます。 サービスプロジェクト名.tf それぞれのサービスプロジェクトで管理するネットワークリソースを定義したtfファイルを作成します。CDのため各環境のディレクトリ(prd・stg・dev・qa)配下にシンボリックリンクを作成します。 # サービスプロジェクトを定義 resource " google_compute_shared_vpc_service_project " " service1 " { host_project = google_compute_shared_vpc_host_project.host.project service_project = local.service1 [ " service_project " ] } # サービスプロジェクト(service1)で利用するサブネットを定義 resource " google_compute_subnetwork " " service1_subnet " { name = " ${local.service1.service_project}-subnet " region = local.service1 [ " region " ] network = google_compute_network.shared_vpc.id ip_cidr_range = local.service1 [ " primary_cidr " ] private_ip_google_access = true } # 作成したサブネットに対して利用するサービスプロジェクトのメンバーへ操作権限を付与 resource " google_compute_subnetwork_iam_member " " service1 " { for_each = toset ( local.service1.service_project_members ) project = google_compute_shared_vpc_host_project.host.project region = google_compute_subnetwork.service1_subnet.region subnetwork = google_compute_subnetwork.service1_subnet.name role = " roles/compute.networkUser " member = each.value } # 組織ポリシーでサブネットとサービスプロジェクトを紐づける resource " google_project_organization_policy " " service1 " { project = local.service1 [ " service_project " ] constraint = " compute.restrictSharedVpcSubnetworks " list_policy { inherit_from_parent = false allow { values = [ " projects/${local.host_project}/regions/${google_compute_subnetwork.service1_subnet.region}/subnetworks/${google_compute_subnetwork.service1_subnet.name} " ] } } } # Cloud NATゲートウェイに割り当てるIPを定義 resource " google_compute_address " " service1_nat_ip " { name = " ${local.service1.service_project}-nat-ip " region = local.service1 [ " region " ] } # Cloud NATゲートウェイを定義 resource " google_compute_router_nat " " service1_nat_gateway " { name = " ${local.service1.service_project}-nat-gateway " router = google_compute_router.nat - router [ google_compute_subnetwork.service1_subnet.region ] .name region = local.service1 [ " region " ] nat_ip_allocate_option = " MANUAL_ONLY " min_ports_per_vm = 64 # 状況に応じて変更する nat_ips = [ google_compute_address.service1_nat_ip.self_link ] source_subnetwork_ip_ranges_to_nat = " LIST_OF_SUBNETWORKS " subnetwork { name = google_compute_subnetwork.service1_subnet.self_link source_ip_ranges_to_nat = [ " ALL_IP_RANGES " ] } log_config { enable = true filter = " ALL " } } ポイントは 組織ポリシーの制約 です。サブネットとサービスプロジェクトを紐づけることができます。この組織ポリシーで複数のサービスプロジェクトに対して権限を持つユーザーが誤って意図しないサービスプロジェクトにリソースを作成してしまうことを制御できます。 locals.tf への追記 先ほど解説した locals.tf に サービスプロジェクト.tf ファイルで利用する変数を追記していきます。 # 各サービスプロジェクトで利用する変数をlocals.tfに追記して定義する service1 = { service_project = " service1-${local.env} " service_project_id = " service1-${local.env} " region = " asia-northeast1 " primary_cidr = " 192.168.100.0/24 " service_project_members = [ " group:service1@example.com " ] } # 全サービスプロジェクトのメンバーを追記 all_service_projects_members = distinct ( concat ( local.service1.service_project_members )) } 状況によってはサービスプロジェクトのメンバーで main.tf を編集してPull Requestを作成することもあります。 Shared VPC環境のCI/CD 最後にShared VPCリポジトリのGitHub Actionsを利用したCI/CDについても簡単に解説していきます。 各ブランチでCI/CDが行われ、異なるGCP環境に対して処理が実行されるようになっています。各ブランチで段階的にCI/CDすることで誤った設定をした場合も開発環境やステージング環境への反映後に気がつき修正が可能なため、安全なリリースができる仕組みとなっています。 ZOZOTOWNのCI/CD戦略については弊社川崎の書いた記事で詳しく紹介されておりますので是非ご覧ください。 techblog.zozo.com Shared VPC管理リポジトリでのCI/CDにより作成されたサブネット上にリソースを構築することで各サービスプロジェクトはDedicated Interconnectが利用可能な状態になります。 まとめ Shared VPCを導入したことにより直面したVLANアタッチメントの上限数の問題を回避できました。またクラウド環境、オンプレミス環境と複数のチームでの設定が必要なことから潜在的に抱えていた課題もオンプレミス側のBGPルーターから広報するネットワークが増えない限りは基本的にはGCP側の作業のみで完結できるようになりました。 一方でGCP外の内部リソース(AWSなど)との通信制御については課題もあります。マネージドサービスなどは全サービスプロジェクトが共通のサブネットを利用していますが、IPレベルでの通信制御ができないため現状はどうしても制御が必要なシーンでは送信元でアクセス先を絞ることになります。 Cloud Armor がプライベートIPでの通信に向けて適用できるようになることを期待せずにはいられません。 謝辞 本プロジェクトの進行と環境構築、そして本ブログの執筆にあたり多大なる協力をいただいた弊社 shiozaki と civitaspo 、そして sonots へこの場を借りてお礼を申し上げさせていただきます。 最後に ZOZOテクノロジーズでは一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
アバター
はじめに デバイス管理に携わる全国の情シス担当の皆様、日々の業務お疲れ様です。コーポレートエンジニアリング部ファシリティチームの佐藤です。いわゆる”情シス”と呼ばれる役割のチームに所属し、社内インフラ(PCやネットワーク機器)の管理・運用に携わっています。 この記事では以前ご紹介した Windows 10ゼロタッチキッティング を活用しキオスク端末(マルチアプリキオスク)を導入した取り組みを紹介します。 目次 はじめに 目次 導入背景 シンクライアント端末の検討 課題1:キッティングとメンテナンス 課題2:ヘルプサポート 課題3:Windows デスクトップ クライアントのアップデート マルチアプリキオスク端末への移行 マルチアプリキオスクで実現可能なこと Windows AutoPilotで実現可能なこと マルチアプリキオスク端末の構築 デバイス登録の確認 グループの作成 アプリの作成 AutoPilotプロファイル作成と適用 登録ステータスページ作成と適用 レイアウトXMLの作成 アプリ情報の取得 AppIDの取得 Windows デスクトップ クライアント クイックアシスト Windows の設定 インストールパスの取得 Windows デスクトップ クライアント クイックアシスト 構成プロファイル(キオスク)の作成 許可アプリ構成 デバイスのセットアップ 構成プロファイルの追加 まとめ さいごに 導入背景 新型コロナウイルス感染症の感染拡大が懸念されていますが、社内ではファシリティチームもさることながら関係各所の涙ぐましい努力の甲斐もあり、テレワーク可能な業務が日々拡大されつつあります。 私が所属するファシリティチームではテレワーク推進のために、Microsoftが提供するDaaSサービス WVD(Windows Virtual Desktop) を採用しています。 採用の主な理由は以下の通りです。 社内で導入済みのAzure ADやIntuneなどMicrosoft製品の知識や経験が既にあるため 既に契約しているMicrosoftとのE5ライセンスを活用できるため セキュリティ対策が為されている既存の認証システムを利用できるため 弊社では「Windows Virtual Desktop(Spring 2020 リリース)」を利用しています。このバージョンからAzureのGUI上で構築が完結できます。そのため検証や環境構築は比較的スムーズに完了できました。 ところが、WVD導入に際し「 接続元のデバイス 」が大きな懸念点として浮上してきました。 弊社ではOSに関わらず、すべてのデバイスで以下の条件が必須事項となっています。 MDM(Intune)に登録されていること 会社標準のウイルス対策ソフトがインストールされていること デフォルトのポリシー準拠を満たしたPCであること 上記の条件を前提とし、WVDを利用する上で業務形態・業務環境にとらわれることなく機密性の高い情報の取り扱いに対応できるように以下の条件を追加しました。 デバイスで利用できる機能を制御する(スクリーンショットやファイル作成など) ローカルディスクにファイルなどを保存させない アプリケーションを自由にインストールさせない デバイスを操作できる範囲をWVD接続のみとする そして、上記の条件を満たすための WVD接続専用デバイス の選定に取り掛かかりました。 シンクライアント端末の検討 「ローカルディスクにファイルを残さない」となると、まず候補に挙がるのが、シンクライアント端末です。 シンクライアント端末は、端末の機能を最小限にしたもので、データの保存や処理の多くを仮想デスクトップマシン側で行います。弊社が当初選定したシンクライアント端末OSは、Windows 10 IoT Enterpriseです。統合書き込みフィルター処理 (UWF) 機能を標準搭載しており、設定不要でシンクライアント環境を実現できるものです。 シンクライアント端末を WVD接続専用デバイス として検証を進めた結果、以下の課題が出てきました。 課題1:キッティングとメンテナンス 一般ユーザーと管理者を交互にログインし、必要な設定の追加や有効/無効の作業を実施しました。しかし、ほぼ手作業が中心となるため、設定ミスや設定漏れのリスクがありました。 最終的に、今回検証したシンクライアント端末の仕様だと、メンテナンスをするためには管理者でのログインが必要となるため、テレワークで利用する場合の対応が困難だと判断しました。 課題2:ヘルプサポート ユーザーに代わって遠隔でPCを操作するために、弊社ではWindows 10標準のクイックアシストを利用しています。 しかし、シンクライアント端末のOSであるWindows 10 IoT Enterpriseにはクイックアシスト機能はついていないため、遠隔でのサポートができません。そのため、テレワーク利用を前提とした場合、ヘルプサポートのためのリモートデスクトップが利用できません。 課題3:Windows デスクトップ クライアントのアップデート WVDへの接続はWindows デスクトップ クライアントを採用しています。 その際の課題点はWindows デスクトップ クライアントの定期アップデートに対応できないことです。一般ユーザーではアップデートに必要なフォルダへアクセスできず「ファイルを読み取るときにエラーが発生しました」のエラーとなり、定期アップデートが実施できない結果となりました。 マルチアプリキオスク端末への移行 上述の通り、シンクライアント端末での課題を運用でカバーすることは困難と判断し、別の案を模索しました。 模索した中で今までの知見と経験を活かせる対応策として、 Windows AutoPilotでキッティング可能なマルチアプリキオスク端末 が候補に挙がりました。 マルチアプリキオスクで実現可能なこと 従来のキオスクモードは、単一のアプリのみ実行が可能となり、一般ユーザーはそれ以外のアプリを操作できません。 しかし、Windows 10 バージョン1709以降で対応している マルチアプリキオスク は、管理者が複数のアプリを実行できるようにキオスクを作成できます。 そのため一般ユーザーが操作可能なアプリを明示的に制限することで、特定の作業のみで使用するデバイスとしてPCをセットアップできます。 そして、マルチアプリキオスクをIntuneから展開および管理することで以下の課題を解決しました。 デバイス制御 Intune登録デバイスになるため、ログインなど多くのログの収集が可能 遠隔によるワイプが可能 管理者が許可しないアプリの制限が可能 情報漏洩のリスク Intuneによる構成プロファイルにて、BitLockerによる暗号化の強制が可能 ファイルのアップロードおよびダウンロードの制御が可能 ログインアカウントをAzure ADアカウントのみに制限が可能 ヘルプサポート 許可するアプリにクイックアシストを追加することで、デバイスの不具合時は遠隔サポートが可能 Intuneにより追加のアプリや設定が管理者側で容易に可能 Windows AutoPilotで実現可能なこと Windows AutoPilotとは自社環境に適したWindows 10デバイスの初期セットアップをクラウドを介して自動的に行うサービスです。 1台ずつのOSイメージ展開とは違い、デバイス情報を事前にIntuneへ登録しておくことで個別にセットアップする必要がなくなります。そして、一般ユーザーに端末を配送し、デバイスを初回起動したタイミングで自動で企業による構成や設定、必要なアプリケーションのインストールが実行されます。 冒頭でも紹介した Windows 10ゼロタッチキッティング と同様に、今回もOEMベンダーの協力のもと、デバイス情報をIntuneにタグ付きで登録します。 Windows AutoPilotによるキッティングの流れは以下の図の通りです。 マルチアプリキオスク端末の構築 次に、Windows AutoPilotを利用してマルチアプリキオスク端末をセットアップするまでの手順を順を追って説明します。 今回構築するキオスクの構成は以下の通りです。 デバイスOS:Windows 10 Pro(バージョン 2004) 許可するログイン種類:Azure AD ユーザー 使用を許可するアプリ Windows デスクトップ クライアント クイックアシスト Windows の設定 手順は以下の図の通りです。 最終的にユーザーがログインすると以下の図のデスクトップ画面となります。 デバイス登録の確認 キオスク端末とするデバイスを購入する際、OEMベンダーにIntuneへの登録と共に グループタグ を付与するように依頼します。Intuneのデバイス登録画面に正常にデバイスが登録されていることを確認します。 グループの作成 キオスク端末を利用するユーザーのための「ユーザーグループ」とキオスク端末とするデバイスが所属する「デバイスグループ」を作成します。 今回はWindows AutoPilot用プロファイルの適用を自動化するため、デバイスグループは動的グループとして作成しメンバー条件を「グループタグ:[ KIOSK ]」とします。 作成するグループは以下の通りです。 ユーザーグループ:静的(手動でキオスク端末を利用するユーザーを追加) デバイスグループ:動的メンバーシップルール アプリの作成 キオスク端末がWVDへ接続する際に使用する Windows デスクトップ クライアント をIntuneから展開します。 下記のサイトから Windows デスクトップ クライアント の最新版をダウンロードします。 docs.microsoft.com Intuneにサインインし、「アプリ」→「追加」をクリックし、アプリの種類は「基幹業務アプリ」を選択します。 「アプリ パッケージ ファイルの選択」から事前にダウンロードしたWindows デスクトップ クライアントのmsiファイルをアップロードします。 アプリの追加画面にてアプリ名などを任意で設定します。そして、コマンドライン引数は以下のものを利用します。 /qn ALLUSERS=2 MSIINSTALLPERUSER=1 割り当て先は、事前に作成したデバイスグループを指定します。 AutoPilotプロファイル作成と適用 キオスク端末用のAutoPilotプロファイルを以下の設定で作成します。 設定項目 設定値 配置モード 自己展開(プレビュー) Azure AD への参加の種類 Azure AD 参加済み 言語(リージョン) オペレーティング システムの既定値 プライバシーの設定 非表示 アカウントの変更オプションを非表示にする 非表示 ユーザー アカウントの種類 標準 割り当て先 事前に作成したキオスク端末が所属するデバイスグループ ここで、配置モードを「自己展開モード」とする点がポイントです。なお、自己展開モードはキオスク、デジタル看板デバイス、または共有デバイスとしてWindows 10デバイスを展開する場合の設定です。 ユーザー アカウントの種類は一般ユーザーである「標準」を選択します。 登録ステータスページ作成と適用 キオスク端末用の登録ステータスページを作成します。 登録ステータスページ(ESP)は、デバイスのキッティング時にプロビジョニングの進行状況を表示する設定です。 ここではWindows デスクトップ クライアントがインストールされるまでデバイスの利用をブロックするように指定します。割り当て先は、事前に作成したキオスク端末が所属するデバイスグループとします。 レイアウトXMLの作成 キオスク端末で表示するスタートメニューのレイアウトをXMLファイルとしてエクスポートします。 まず、通常のWindows 10のPCにて、「Windows の設定」から「タブレットモード」を「オン」にします。 次に、タブレットモードである状態で、スタートメニューに必要なアプリのアイコンを配置していきます。その際に、タイルの大きさやタイトル名は任意で設定します。 配置完了後、PowerShellを管理者権限で起動し、以下のコマンドを実行してレイアウトXMLファイルをエクスポートします。 Export-StartLayout -path C:¥Layout-KIOSKDevice.xml アプリ情報の取得 次に、アプリ情報として必要なAppIDとインストールパスを取得します。 AppIDの取得 前述のレイアウトXMLの作成で配置したアプリの情報をPowerShellにて取得します。 引き続き、先程の通常のWindows 10のPCにてPowerShellを起動し、以下のコマンドを実行します。 Windows デスクトップ クライアント get-StartApps | ?{$_.name -like "remote*"} クイックアシスト get-StartApps | ?{$_.name -like "クイック*"} Windows の設定 get-StartApps | ?{$_.name -like "設定*"} 以上のコマンドの実行結果に表示される AppID を記録しておきます。 インストールパスの取得 また、各アプリのインストール先のパスも同様に記録しておきます。 2つのアプリのインストールパスを例として挙げます。 Windows デスクトップ クライアント C:¥Program Files¥Remote Desktop¥msrdcw.exe クイックアシスト C:¥Windows¥System32¥quickassist.exe 構成プロファイル(キオスク)の作成 デバイスをキオスク端末とするための構成プロファイルをIntuneにて作成します。 Intuneにサインインし、「デバイス」→「構成プロファイル」→「プロファイルの作成」を選択します。 「プロファイルの作成」ペインで、プラットフォームを「Windows 10 以降」、プロファイルの種類を「テンプレート」し、一覧から「キオスク」を選択して下部にある「作成」を選択します。 すると、構成プロファイルの作成画面が開きます。 「基本」タブにて、プロファイルの名前を任意で入力し、「次へ」を選択します。 次に、「構成設定」タブにて、以下のように設定します。 設定項目 内容 キオスク モードを選択します マルチ アプリ キオスク S モード デバイスで Windows 10 を対象とする いいえ ユーザーのログオンの種類 事前に作成したユーザーグループを選択 許可アプリ構成 「ブラウザーとアプリケーション」の追加画面にて、「Win32 アプリの追加」ボタンを押して以下の通り設定を追加します。 許可するアプリ: Windows デスクトップ クライアント アプリケーション名:Remote Desktop アプリの実行可能ファイルのローカルパス:C:¥Program Files¥Remote Desktop¥msrdcw.exe ユーザーモデルID (AUMID):<コマンドで取得したAppID> タイルサイズ:任意 自動起動:オン 許可するアプリ: クイックアシスト アプリケーション名:Remote Desktop アプリの実行可能ファイルのローカルパス:C:¥Windows¥System32¥quickassist.exe ユーザーモデルID (AUMID):<コマンドで取得したAppID> タイルサイズ:任意 自動起動:オフ 「ブラウザーとアプリケーション」の追加画面にて、「AUMIDの指定によるアプリの追加」ボタンを押して設定を追加します。 許可するアプリ: 設定 アプリケーション名:設定 AUMID/パス:<コマンドで取得したAppID> 「スタート メニューのレイアウト」にて事前にエクスポートしたレイアウトXMLファイルをアップロードします。 「Windows タスクバー」と「ダウンロード フォルダーへのアクセスを許可する」「アプリの再起動のためのメンテナンス期間の指定」は任意で設定します。 「割り当て」では事前に作成したユーザーグループを指定します。 なお、ここでデバイスグループを割り当てにすると、Azure ADユーザーでログオンできないという問題が発生します。Microsoftによると、ユーザーログオンの種類で「Azure ADユーザーグループ」を指定する場合は、プロファイルの割り当て先もAzure ADユーザーグループとする必要があるとのことです。 以上でキオスクプロファイルの作成と適用が完了します。 デバイスのセットアップ 実際にデバイスをセットアップしていきます。 PCの電源をオンにし、ネットワークに接続するとWindows AutoPilotにより自動でセットアップが開始されます。ここでは「デバイスの準備」と「デバイスのセットアップ」が実施されます。 「デバイスの準備」と「デバイスのセットアップ」が完了すると、ユーザーのログイン画面が表示されます。キオスク用ユーザーグループ内のユーザーのアカウント情報を入力して、ログインを実施します。 すると、自動で「アカウントのセットアップ」が実施されます。 セットアップがすべて完了すると、Windows 10のデスクトップ画面が表示されます。その後、一度ログオフして再度ログインします。 デバイスにログインすると、それ以降はレイアウトXMLで指定したキオスク画面が表示されるようになります。 これにより、該当デバイスはキオスク端末となり、「Windows デスクトップ クライアント」「クイックアシスト」「設定」のみが操作可能な状態となります。 またプロファイルにて「Windows デスクトップ クライアント」の自動起動をオンの設定としたため、デバイス起動と同時にアプリが立ち上がります。ユーザーはデバイス起動と同時にWVDへ接続する画面を操作することが可能となります。 構成プロファイルの追加 Intuneによるマルチアプリキオスクを採用したことで、デバイスのセットアップ後もユーザーや組織のニーズに合わせて設定のカスタマイズが可能となっています。 ここでは、Intuneの 構成プロファイル を使用して「Windows の設定」画面に表示する項目を制御する例を紹介します。 Intuneにサインインし、「デバイス」→「構成プロファイル」→「プロファイルの作成」を選択します。 「プロファイルの作成」ペインにてプラットフォームを「Windows 10 以降」とします。プロファイルの種類を「テンプレート」とし、「デバイスの制限」を選択して、下部にある「作成」を選択します。 すると、構成プロファイルの作成画面が開きます。 「基本」タブにて、プロファイルの名前を任意で入力し、「次へ」を選択します。 次に、「構成設定」タブの「コントロール パネルと設定」にて「Windows の設定」画面で表示しない項目を「ブロック」にします。割り当て先はキオスクデバイスグループとして、プロファイル作成を完了させます。 デバイスに適用されると、「Windows の設定」で表示される項目が制限されていることが確認できます。 まとめ マルチアプリキオスクの採用でセットアップ後のカスタマイズやアプリアップデート配信など管理面において柔軟に対応ができます。また、一度構築されたセットアップ手順はIntuneによって自動化されるため、作業ミスや手順漏れがないため安定した展開環境を維持できます。Windows Virtual Desktopへの接続デバイスの1つの例として、本記事がお役に立てれば幸いです。 さいごに ZOZOテクノロジーズでは、社内の課題をITの力で解決する仲間を募集中です。WindowsとMacはもちろんiPhoneやiPadなど様々なデバイスを効率よく管理と制御することにどんどんチャレンジできます! ご興味のある方は、下記のリンクからぜひご応募ください! tech.zozo.com
アバター
はじめに BtoB開発部の増田です。 BtoB開発部は、主に Fulfillment by ZOZO (以下、FBZ)の開発を担当しているエンジニアチームです。FBZの初回ローンチから間もなく3年経過しますが、サービスの拡大、拡張とともに見直すべき課題も増えてきました。日々の運用負荷の増大や、それに伴う開発効率の低下の話しを耳にする機会も増えています。そこで、今期の開発計画では、運用改善のための開発も優先度を上げて取り組むこととしていました。 一方で、新型コロナウィルスの影響もありチーム全体がリモートワークに移行して1年が経過しました。リモートワークが浸透する過程にはさまざまなコミュニケーション課題があり、上記の運用改善の施策を進める上でもコミュニケーションの円滑化が急務でした。 そのようなコミュニケーション課題の対策のひとつとして1on1に力を入れているチームも多いでしょう。この記事では、1on1の実施がエンジニアチームの生産性やパフォーマンスにどのような影響を与えるか、BtoB開発部における実績をひとつの事例として紹介します。加えて、1on1を起点としてチーム内の「ガチ対話」を増やしていくために、どのような工夫が考えられるかをまとめました。 組織サーベイの結果で示されたチーム状況の変化 まず今回の取り組みで実現されたチーム状況の変化を示します。ある時期から顕著にポジティブな変化が表れるようになりました。その変化は、組織サーベイの結果から定量的に知ることができます。 ZOZOテクノロジーズでは3か月に一度のペースで組織サーベイを実施しています。匿名アンケートの回答結果に基づき、チーム単位でのエンゲージメントを定期的にモニタリングしています。今期は、2020年6月、9月、12月の計3回を実施しており、直近の9月、12月に実施したサーベイの結果に特徴的な変化が表れていました。 着目している重要指標 組織サーベイの結果は、全部で36項目の指標で示されます。そのうち、BtoB開発部では以下の9項目を重要指標と位置付けて注視しています。 今期は、FBZのサービス開発のなかで、エンジニアひとりひとりが成長を実感できる環境づくりに取り組んできました。リモートワークでもそのような環境が実現できているかを推し量るための指標として、下記の観点で重要指標9項目をピックアップしました。 日々の業務にやりがいを感じられているか(職務、理念戦略) エンジニアとしての成長実感を得られているか(自己成長) 成長のためのチャレンジを支援できているか(支援) リモートワークの中でも良好な人間関係を築けているか(人間関係) 成果に対して納得できる評価を感じられているか(承認) 重要指標の定量的変化 9月、12月の結果を比較すると、全36項目のうち32項目で改善が見られました。重要指標9項目に着目しても、漏れなくすべての項目で大幅に改善されており、9月から12月にかけてチーム状況がポジティブに変化したことがわかります。 変化の背景 BtoB開発部にとって、昨年9月は組織変更を行ったタイミングでした。この時期を境に、1on1をテンプレート化したり、チームごとの朝会・夕会を活発化するなどいくつかのコミュニケーション改善を実施してきました。特に1on1は、リモートワークで希薄になりやすいコミュニケーションを補強するために工夫をした部分です。次章では、この1on1に関してさらに説明していきます。 1on1の定期実施で意識したこと、わかったこと もともとBtoB開発部では、前身の子会社時代から数年に渡って1on1を実施してきました。しかし、リモートワークに適応するため、実施頻度ややり方を見直す必要がありました。昨年9月以降の実施要領は以下のとおりです。 最低でも隔週で実施する(時期や状況によっては週次での実施) 1回あたりの時間は30分 振り返りと傾向分析がしやすいように箇条書きレベルでログを残す 各回の対話のテーマはメンバー側から設定する 上記要領に沿って対話のテーマをメンバーが設定する際には、下記の5つのカテゴリから設定してもらっています。 質問をしたいです 共有をしたいです 雑談がしたいです モヤモヤしています ネタに困っています 期間中、私が10人のチームメンバーと行った1on1は合計80回で、テーマ数にして118個の対話をしました。テーマをカテゴリ別に集計すると、下記のグラフのような分布です。 この集計結果と組織サーベイの結果をもとに、BtoB開発部における1on1の影響を、チームメンバーへの影響、エンジニアリングへの影響、リーダー自身への影響の3つの観点で振り返ってみます。 チームメンバーへの影響 カテゴリ別に見ると、「質問をしたいです」の割合がもっとも多く、全体の40.7%を占めていました。なかでも、事業方針や、各々の役割や期待値に関する内容が多く、リーダー側からの説明不足を痛感しました。 事業方針のような大きなテーマだけでなく、タスクアサインの背景やミーティングでは質問できなかった疑問点など、直近の出来事に関する背景確認も頻出するテーマのひとつです。リモートワーク環境下で普段よりも共有や背景説明が薄くなりがちですが、1on1で早期に情報共有の不足を検知することで、素早くフォローできるようになりました。 次に、「共有をしたいです」「雑談がしたいです」がそれぞれ19.5%、18.6%と同程度の割合でした。共有については、メンバーからの直近の進捗共有や課題共有が大多数です。それと同じくらい1on1での雑談の割合が多かったのは、リモートワークが常態化して特に顕著になった傾向です。 チームミーティングで日々メンバーと会話する機会はありますが、どうしても業務中心の会話になってしまいます。1on1で対話を深めていくには、雑談や何気ない会話から育まれる信頼関係が重要です。以前は対面の偶発的コミュニケーションによってその下地が作られていましたが、それを意識的に実施するために、あえて1on1でも雑談をメインとするケースは増えました。 組織サーベイの結果のうち、「支援」「人間関係」「理念戦略」の部分が改善した背景には、実務的な支援だけでなく雑談を含めたコミュニケーションによる関係性の向上があったと考えています。 エンジニアリングへの影響 冒頭でも述べたように、FBZのサービス拡大、拡張とともに見直すべき課題も増えてきました。日々の運用負荷の増大や、それに伴う開発効率の低下が課題になっており、今期の開発計画では運用改善のための開発も優先度を上げていました。 1on1での課題抽出は計画検討のための情報収集として有効ですが、情報収集の要素以外にも、チームの改善意欲の向上という副次的な効果がありました。 課題について対話する過程で課題認識を持っていたメンバーほど改善意欲が高まり、リーダーシップを発揮してくれるようになりました。メンバー自らが中心となり計画を立案して実践し、その結果、直近でもっとも大きな課題となっていたノイズアラート対策が完了しました。チームの生産性を改善する大きな成果でした。具体的な内容は以下の記事で紹介しています。 techblog.zozo.com 計画の中では、若手メンバーのチャレンジや興味のあるサービスの試験利用など、課題解決のなかでメンバーが成長実感を得やすくなるような工夫も盛り込まれていました。チームのエンジニアリングを考え直すきっかけにもなり、このことが組織サーベイの結果の「自己成長」を高めることに繋がりました。 リーダー自身への影響 1on1は、基本的にはメンバーのための時間としています。対話のテーマをメンバー自身で設定してもらっているのも、上長ではなくメンバーが話したいテーマにフォーカスするためです。 一方で、メンバーの成長促進や内省支援を心がけたいと意識しつつも、対話を重ねれば重ねるほど結果的にリーダー自身も内省を深めていくことになります。 メンバーが体験した達成感から成長の着想を得たり、メンバーが感じている事業方針への違和感からサービスの改善点が浮き彫りになるなど、1on1はリーダーのアクションの原動力にもなります。 その意味で、1on1はメンバーだけではなくリーダーを含む相互成長のための場であり、1on1の頻度が増えることはリーダーの成長機会の増加にも繋がると言えます。 「ガチ対話」を目指すための工夫 昨年12月、ZOZOテクノロジーズに組織開発チームが立ち上がりました。マネジメントを強化するための専門の部署が発足したことはうれしい変化です。さっそく、組織開発チーム主導のもと、1月から週1回30分の1on1が全社で必須化されるなど、新たな変化が始まっています。 組織開発チームが掲げるテーマのひとつに、 創造性を解き放つために社内の「ガチ対話」を増やす というものがありました。この半年、自分自身がマネジメントのなかで意識してきたことのひとつでもあります。メンバーの主張、願望、反論など、内面の声を表面化しやすくするにはどのような工夫が考えられるか。いま、BtoB開発部で「ガチ対話」を増やしていくために意識しているポイントが2つあります。 全員が少しずつリーダーシップを意識していく 役職や役割に囚われず、リーダーシップを持ったメンバーが増えると、チームは強くなり成果が生まれやすくなります。前述の運用改善のケースでも、メンバー自らがリーダーシップを発揮した結果として成果が生まれました。 このような事例が今後も続くようにするには、メンバーひとりひとりが考えること、実践することを繰り返していく必要があります。そのために、1on1のテーマのひとつとして、「もしあなたがリーダーだったらいま何をしますか」という対話を徐々に増やしていっています。 このテーマで対話することにより、意識が個からチームへ広がります。意識が広がる分、理解すべきことやわからないことが増えます。それをクリアにするためには何らかのアクションが必要で、そのアクションによって経験が積み増しされます。1on1でこうした循環が生まれてくると、メンバーとリーダーの思考発話の過程が「ガチ対話」へと発展しやすくなります。 感情を共有する もうひとつ意識しているのが、感情の共有です。 ◯◯を達成してくれてありがとう! ◯◯を実践してくれたことが嬉しいです! ◯◯のアクションがとてもよかったね! ◯◯をとても不安に思っています ◯◯だったのがとても残念 ◯◯について共有不足で申し訳ない など、自分自身の感情はできるだけ背景を言語化して伝えるようにしています。 リーダーが1on1を考えるとき、「傾聴」や「コーチング」などのテクニックにフォーカスしすぎてしまうことがあります。自分自身を振り返ってみても、1on1をより1on1っぽいものにするため、テクニックに偏った1on1をやってしまった経験があります。とにかく聴くこと、考えさせることを意識しすぎたあまり、禅問答のような対話に陥り「ガチ対話」とはほど遠くなっていました。 1on1を、会議よりも有意義な対話にするためには、もっと自然体で臨む必要があります。メンバーの言葉を聴き、感じた印象はストレートに伝えます。とりわけ、お互いの感情を織り交ぜながら対話するほうが伝わりやすくなります。 リーダーとメンバーがお互いの感情を伝え合い、感情面での共感や反発があって「ガチ対話」が生まれやすくなります。テクニックを身に付けていくことはもちろん重要ですが、テクニックを活かす土台作りとして、感情を共有しあうことを意識しています。 まとめ 以上、1on1のひとつの事例として、BtoB開発における取り組み紹介しました。組織サーベイの結果が改善されたことは、1on1に限らずいくつかの要素が複合的に作用し合った結果です。ただ、1on1を繰り返す過程で、チーム状況が好転していく手応えを感じることができました。改善すべき点は多々ありますが、今後も「ガチ対話」を増やすことを目指しながら成長を追求していきたいと思います。 また、組織サーベイの結果だけでなく、エンジニアとしての施策の遂行にも繋げられたことも重要な成果です。引き続き、アウトプットにつながる変化を生み出していきます。 さいごに ZOZOテクノロジーズでは、BtoB事業の拡大に取り組んでいただけるエンジニアを絶賛募集中です。これまでの自社EC支援、物流支援に加えて、今後はブランドさまの実店舗との連携を強化するための開発を推進していく予定です。 ファッション業界のDX推進に関わる開発や、ブランドさまとの共同開発プロジェクトにご興味ある方は、 こちら からぜひご応募ください! tech.zozo.com
アバター
こんにちは。MSP技術推進部の手塚( @tzone99 )です。 この記事では、エンジニア向けのツールを周囲のエンジニア以外のチームにも導入し、チームを跨いだコミュニケーション上の課題を解決した事例をご紹介します。 普段エンジニアとしてプロダクトを開発する中でも、エンジニア同士のやり取りだけで業務が完結しないケースも多いかと思います。周囲のチームとやり取りする中でコミュニケーションのずれが発生した場合の対応として、今回の事例が参考になれば幸いです。 MSP技術推進部の活動について興味のある方はこちらの記事もぜひご覧ください。 techblog.zozo.com techblog.zozo.com techblog.zozo.com techblog.zozo.com 目次 目次 背景 コミュニケーション上の課題 業務要件のMarkdown/PlantUML化 運用の初期対応 自作のLinter導入、継続的メンテナンス 結果 おわりに 背景 私の所属するMSP技術推進部は服づくりに関する技術を開発する部門です。 ZOZOTOWN内で販売されるMS(マルチサイズ)アイテムの生産プラットフォームとして、アパレル商品の企画、設計、生産に関わる業務システムを開発し、ITを活用した新しい服づくりを追求しています。 MSアイテム は最大56サイズから自分の体型に合ったサイズを選択可能、という特徴を持っており既存のサイズに囚われずいろんな体型の方にファッションを楽しんでいただけるアイテムです。 一般的な場合と同様、このような多サイズのアイテムにおいても、生産の際はサイズ毎のパターンデータ(服の設計図)を用意する必要があります。従来SMLの3サイズ程度だったパターンデータを最大56サイズ分用意するためパターン設計の負荷が高いことは明らかです。 一般的なアパレルCADにはサイズ毎のパターンデータを生成(グレーディング)する機能が備わっているとはいえ、MSアイテムで想定しているような大小様々なサイズにそのまま機能を適用できる訳ではありません。現状、細かなパターン調整工数が大量に発生しており、事業のスケールを妨げる一因になっています。 このような背景からMSアイテムのような多サイズ展開に適したパターンデータの生成ツールを開発するプロジェクトが発足しました。 コミュニケーション上の課題 大枠としてアパレル専門職であるグレーディングチームがパターンデータの生成に必要な業務要件を数式や図で定義し、それをエンジニア側で実装するという流れで開発を進めていました。 パターン生成のためのルールは例えば以下のように数式や関数として表現されており、ルールが増えていくにつれて変更に伴うタイポ、計算ミスがどうしても防ぎきれなくなっていきました。 またルールによっては表ではなく図で管理したいものも多くありました。しかし、数式とこれらの図の整合性をとることもルールが増えるにつれ困難になっていきました。 このようなルール管理上のタイポや計算ミス、ドキュメント間の不整合から要件の確認工数がどんどん増えてしまい、開発のボトルネックになってしまいました。 そこで、ここまでの進行の振り返りを実施し、コミュニケーション上の課題を明文化しました。結果として以下のような課題がメンバー全員の共通認識となりました。 大量の計算式が業務要件として定義されているため、単純なタイポ、計算ミスが防ぎきれない 業務要件のレビューの負荷が特定のメンバーに集中してしまっている 内容の重複があり、1箇所の変更が影響範囲に網羅的に反映されず、ドキュメント間の不整合が発生している 変更されたルールのステータス(検討中/検討済み)が曖昧である 当初想定しきれなかったルール定義上の課題に対処するため、ルールの変更が想定以上に頻繁に発生している 業務要件のMarkdown/PlantUML化 上記の課題を仕組みで解消するために、ツールの運用、特にそれまで業務要件の定義で使っていたGoogleスライドやスプレッドシートの利用を見直しました。 これらのツールは確かにアイデアの可視化や共有には便利ですが、今回のように数式や関数で定義されたルールが頻繁に更新されるケースでは、より組織的な変更管理フローとそれに適したツールが必要と判断しました。挙がっていた課題の解決に必要な機能(タイポの防止/相互レビュー/頻繁な更新に耐え得る変更履歴と変更ステータス管理)を洗い出していくとソースコード管理に求められるものと同等でした。 したがってエンジニア向けのツールを要件定義にも導入するのが良さそうだという見通しの元、ツールを選定しました。利用するメンバーが非エンジニアであることもふまえて特に以下の点を意識しました。 学習・導入・運用コストが小さいもの GUIで操作できるもの ツールの使い方に関してWeb検索ですぐHitするもの ルールの記述はMarkdownをベースとし、図示した方が分かりやすいものはPlantUMLを用いて作図することで全てのルールをテキスト化しました。導入するツールとルールの定義(または変更)のフローを以下のように定め、運用を始めました。 運用の初期対応 エンジニアにとってはどれも馴染み深いツールですが、他のチームのメンバーにとっては初めて使うものばかり。円滑に運用を開始するためにハンズオンを実施しました。 ハンズオンでの情報過多による運用初期の混乱を抑えるため、必要最小限のトピックに絞って説明するにとどめました。約3時間で以下のトピックについて説明しました。 Visual Studio CodeでのMarkdown編集、拡張機能の追加 よく使う機能 Open folder(フォルダごと開く) Search(フォルダ内テキスト検索) 正規表現を使った検索 例えばアルファベット大文字4桁+数字4桁の変数を全て検索したい場合:[A-Z]{4}[0-9]{4} Source Control Diff(差分) Discard changes(変更を破棄) Command list(F1キーで開く) File: Compare Active File With(2つのファイルを比較) Markdown: Open preview(Markdownをプレビュー) PlantUML: Preview Current Diagram(PlantUMLをプレビュー) Preferences: Open User Setting(ユーザー設定を変更) Git: Stash(変更に名前をつけて一時退避) Git: Pop Stash(退避した変更を戻す) キーボードショートカット マルチカーソルと選択 Visual Studio Codeに拡張機能を追加 おすすめ拡張機能 Markdown all in one Markdownが見やすくなる。自動フォーマット機能あり。 Highlight settings.jsonに以下のテキストを追加すると全角のスペース、かっこ、英数字をハイライト表示できるのでエラー防止に。 " highlight.regexes ": { " ( ) ": [{ " backgroundColor ": " lightgray " }] , " ([0-9A-Z()]) ": [{ " color ": " red ", " backgroundColor ": " yellow " }] } GitHubの仕組みを説明 バージョン管理とは リモート/ローカルリポジトリ 変更履歴の統合 ブランチ GitHub基本操作のハンズオン(テスト用のリポジトリを作ってみんなでアクセス!) 基本のPull, Commit, Push コンフリクトの発生と解消 独自ブランチでの作業 ブランチのMerge 変更履歴の確認 プロジェクトでの運用フロー説明 運用フローを明文化し、各レビュアーの観点も明記して記載内容やレビュアーの重複がなくなるよう運用を設計しました。 もちろん上記の他にも運用していく中で様々な状況への対応が必要ですし、より良い運用のために知っておくべき操作も数多くあります。 しかし、それらは運用しながら適宜コメントを入れることで徐々に理解を深めてもらうようにしました。分からなくてもまずは使ってもらう、試してもらうというモチベーションで使ってもらいました。 導入したツールを使ったやりとりはメンバー全員が見えるオープンなものになるため、エンジニアメンバーの使い方を見ながら他の非エンジニアメンバーも使い方を学んでいけることを期待しました。 自作のLinter導入、継続的メンテナンス 上記の運用に加え、わざわざ人間がチェックする必要のない単純なルールの記載ミスを検出するLinterをPythonで自作しました。このLinterはMarkdown Table及びPlantUMLの構造解析と正規表現マッチングによるエラー検出を組み合わせた簡易的なものです。 例えば以下のコードでMarkdownファイル内のTableから特定の一列を配列として取得します。 # Markdownファイルに含まれるTableのデータを配列に格納 def get_table (path): with open (path) as f: lines = f.readlines() table = [] for line in lines: row = re.split( ' \\ |' , line) cells = [] for cell in row: cell = cell.strip() cell = cell.replace( ' \\ ' , '' ) cells.append(cell) table.append(cells) return table # Tableのデータ配列から任意の一列を取得 def get_column_contents (table, header): index = table[ 0 ].index(header) contents = [] for row in table: contents.append(row[index]) del contents[: 2 ] # table headerを除外 return contents # テスト実行 def test (self): FILEPATH = './filename.md' COLUMN_NAME = 'Target Column Name' table = get_table(FILEPATH) column_contents = get_column_contents(table, COLUMN_NAME) # 取得した列データに対してテストを実行 # . # . Markdown Parserは既に数多くのものが開発され公開されていますが、今回のMarkdownの用途はTableに特化していたため既存のParserを使うのはオーバースペックと判断しました。 以下のコードは数式から変数のリストを抽出するものです。業務要件として定義されている数式や変数には一定のルールがあるため、正規表現で十分抽出できると判断しました。 # 数式から変数のリストを抽出する def get_var_list_in_formula (formula): result = [] if formula != '' : var_list_row = re.split( r'\(|\)|\+|\-|\*|\/|\=|\,' , formula) for var in var_list_row: if var != '' and re.search( r'[0-9]{0,1}[a-zA-Z]+' , var): var = var.strip( ' ' ) if re.match( r'[0-9]{1}' , var): var = var[ 1 :] var = remove_landmark_prefix(var) var = remove_landmark_suffix(var) result.append(var) return result # 接頭語付き変数から接頭語を除外する def remove_landmark_prefix (s): if s.startswith(( 'm' , 'g' , 'r' , 'd' )): s = s[ 1 :] return s # 接尾語付き変数から接尾語を除外する def remove_landmark_suffix (s): if s.endswith( 'c' ): s = s[:- 1 ] return s このLinterで以下のようなケースを検出できるようになりました。 未定義の変数の使用 関数内で引数の型、引数の数が想定外 未使用の変数の検出 タイポにありがちな不正な書式 本格的な構文解析ツールではないのでルールの拡充に伴い継続的にメンテナンスが必要なものの、目視確認に比べてメンテナンスコストが大きく削減され、施策としては有効な結果でした。何より大量の数式に含まれ得るタイポの目視確認が不要となり、精神衛生上かなり快適になりました。 Linterはローカルで実行できる他、GitHubにルールをPushするとCircleCIにより実行されるよう設定しチェック忘れを防止しています。 結果 これらの施策により、エンジニア側にインプットされる業務要件が精度の高いものになり、コミュニケーション上の課題を解消できました。 自作したLinterで単純なタイポ、計算ミスを検知 レビュアーの役割を分散し明文化 Markdown化に伴い項目の重複が最小限になるようルール定義のフォーマットを更新 GitHub上での適切なブランチ運用によりルールのステータス(検討中/検討済み)を明確化 頻繁なルール変更に耐え得る変更フローを定義 運用開始からこれまで、パターン生成ルールの追加/更新に関する311件のCommitのうち、29件でLinterによるエラーが検出され、不正な要件がmainブランチに入るのを食い止められました。Linter単体でも9.3%の精度向上に寄与しています。 加えてLinterでチェックできないポイントはGitHub上で相互レビューを通して議論され、正しい要件がmainブランチへ入るようになりました。 またブランチ管理によりアイデアベースの要件整理や複数のトピックを混ぜることなく並行して議論できるようになりました。チームで共有し、頻繁に更新する類のドキュメントは多少作成コストや学習コストをかけてでもMarkdownで管理していくことの良さも実感しました。 私個人だけでなく、アパレル専門職のメンバーからもコミュニケーションが明確になったと好評をいただきました。 おわりに 当初はエンジニア向けのツールを他のチームに押し付けたくないという思いもありましたが、コミュニケーション上の課題を明文化し、共通認識が持てたことで自信を持って提案できました。またこの共通認識により周囲のメンバーも前向きに取り組んでくれたものと考えています。 今回ご紹介した運用はまだ始めたばかりで、ブラッシュアップする余地はまだまだあるので、今後も継続的に改善していきます。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
アバター
はじめに こんにちは。ECプラットフォーム部のAPI基盤チームに所属している籏野 @gold_kou と申します。普段は、GoでAPI GatewayやID基盤(認証マイクロサービス)の開発をしています。 先日、 【ZOZOTOWNマイクロサービス化】API Gatewayを自社開発したノウハウ大公開! を公開したところ、多くの方からご好評いただきました。ありがとうございます。まだ読まれていない方はぜひご覧ください。 techblog.zozo.com 今回はその記事の続きです。API Gatewayは単にリバースプロキシの役割を担うだけでなく、ZOZOTOWN全体の可用性を高める仕組みを用意しています。本記事では、それらの中でカナリアリリース機能・リトライ機能・タイムアウト機能に関して実装レベルの紹介をします。 マイクロサービスに興味ある方や、API Gatewayを自社開発する方の参考になれば幸いです。 なお、本記事における可用性の定義は こちら を参考にしており、 成功したリクエスト数 /(成功したリクエスト数 + 失敗したリクエスト数) で計算できるものとします。 はじめに カナリアリリース機能 前回記事のおさらい ターゲットとターゲットグループ ルーティング 加重ルーティング スケジューラの基本 重み付きランダムサンプリングでなく重み付きラウンドロビンを採用 SchedulerインタフェースとFetchメソッド 初期化 どちらのスケジューラになるか 重みのバリデーション 一般的なラウンドロビンのスケジューラ 構造体 Fetchメソッド 重み付きラウンドロビンのスケジューラ 構造体 重みの約分 Fetchメソッド Fetchメソッドの呼び出し ターゲットのスケジューリング ターゲットグループのスケジューリング リトライ機能 前回記事のおさらい ターゲットグループを跨いだリトライ 設定方法 リバースプロキシとリクエスト情報の準備 マイクロサービスへのリクエスト タイムアウト機能 前回記事のおさらい ターゲットとターゲットグループのタイムアウト 設定 実装 機能追加の展望 We are hiring カナリアリリース機能 ここでは前回の記事では触れなかったカナリアリリース機能の実装面について主に紹介します。 カナリアリリースとは、一部のリクエストのみ新系サービスにアクセスさせて、新系サービスに問題がなければ段階的に新系サービスへの比重を高めるデプロイ方法です。 例えば、まずは新系と旧系を1:9の比重でリクエスト分散します。仮にこの段階で、新系の変更部分が起因で問題が発生した場合は、被害は一部(ここでは1割のリクエスト)で済みます。もし新系に問題なければ比重を徐々にあげていき、最終的には新系と旧系を10:0にします。旧系はこの時点で削除します。 新系サービスにバグがあっても新系への加重率を低くしていれば失敗リクエスト数は減るため、可用性の低下を抑えられます。インフラコストは増えるものの、エンドユーザへの影響を最小限に抑えつつ、エンジニアのリリース時の心理的なハードルを下げられます。 前回記事のおさらい 前提知識として必要なものを簡単に説明します。 ターゲットとターゲットグループ API Gatewayにはターゲットとターゲットグループという概念があります。ターゲットは転送先の接続情報(ホストとポート)です。ターゲットグループは、転送先であるターゲットをまとめた単位です。 ターゲットとターゲットグループの設定には、 target_groups.yml という名前のYAMLファイルを用意します。以下は設定の具体例です。TargetGroupAというターゲットグループの中に、2つのターゲットを設定しています。 TargetGroupA : targets : - host : target1.example.com port : 8080 - host : target2.example.com port : 8081 ルーティング ルーティングの設定には、 routes.yml という名前のYAMLファイルを用意します。 以下は設定の具体例です。ルーティングする転送元と転送先の情報を定義します。HTTPリクエストのパスが正規表現で ^/sample/(.+)$ に一致した場合、転送先のパスをGoの regexp.ReplaceAllString を使って、 /$1 に置き換えます。正規表現マッチした部分がURLのリライトの対象となるため、例えば /sample/hoge というパスでリクエストがきた場合は、 /hoge に置き換えられます。 - from : path : ^/sample/(.+)$ to : destinations : - target_group : TargetGroupA path : /$1 加重ルーティング 加重ルーティングの設定には、 target_groups.yml および routes.yml の重みを指定します。 target_groups.yml で指定する重みはターゲットに対する重みで、 routes.yml で指定する重みはターゲットグループに対する重みです。ターゲットとターゲットグループで2段階の重み付けができます。転送先の比重をコントロールすることで、加重ルーティングおよびカナリアリリースを実現できます。 下記は target_groups.yml でTargetGroupA内のtarget1.example.comとtarget2.example.comに4:1の比重で振り分ける例です。 TargetGroupA : targets : - host : target1.example.com port : 8080 weight : 4 - host : target2.example.com port : 8081 weight : 1 下記は routes.yml でTargetGroupAとTargetGroupBに2:1の比重で振り分ける例です。 - from : path : ^/sample/(.+)$ to : destinations : - target_group : TargetGroupA path : /$1 weight : 2 - target_group : TargetGroupB path : /$1 weight : 1 スケジューラの基本 転送先を決定するスケジューラの基本について説明します。 重み付きランダムサンプリングでなく重み付きラウンドロビンを採用 ランダムサンプリングとは、複数の候補から無作為に対象を選択する方法です。一様ランダムサンプリングとも呼びます。重み付きランダムサンプリングとは、各対象が何かしらの選ばれやすさに関するパラメータを持っている場合のランダムサンプリングです。 スケジューラとして重み付きランダムサンプリングを採用する手段もありました。しかしながら、あるマイクロサービスの特殊な要件により、例えば重みの割合が9:1の場合では10回に1回は確実に片方へアクセスすることを保証する必要があったため、重み付きラウンドロビンを採用しています。 SchedulerインタフェースとFetchメソッド ターゲットおよびターゲットグループのどちらも同一の Scheduler インタフェースで処理が共通化されています。 Scheduler インタフェースは Fetch メソッドを持ちます。実装するラウンドロビンの種類によって Fetch メソッドの動作は異なりますが、いずれの種類においてもターゲットを決めるためのインデックスをint型の値で返します。 type Scheduler interface { Fetch() int } 初期化 NewScheduler 関数でスケジューラの初期化をします。一般的なラウンドロビンスケジューラ roundRobinScheduler あるいは重み付きラウンドロビンスケジューラ weightedRoundRobinScheduler を返します。どちらのスケジューラも Fetch メソッドを実装しているため Scheduler インタフェースを満たしています。 func NewScheduler(weights [] int ) (Scheduler, error ) { e := validateWeights(weights) if e != nil { return nil , e } one := weights[ 0 ] for _, w := range weights[ 1 :] { if one != w { return newWeightedRoundRobinScheduler(weights), nil } } return newRoundRobinScheduler( len (weights)), nil } どちらのスケジューラになるか どちらのスケジューラになるかは、設定ファイル( target_groups.yml あるいは routes.yml )の weight の設定次第です。均等に weight の値が設定されているか weight が設定されていなければ一般的なラウンドロビンです。それ以外は重み付きラウンドロビンです。 下記は target_groups.yml の一般的なラウンドロビンの例です。 weight が設定されていません。 TargetGroupA : targets : - host : target1.example.com port : 8080 - host : target2.example.com port : 8081 下記は target_groups.yml の重み付きラウンドロビンの例です。異なる weight の値が設定されています。 TargetGroupA : targets : - host : target1.example.com port : 8080 weight : 2 - host : target2.example.com port : 8081 weight : 1 重みのバリデーション 重みに負の値が設定されていないか、一部の対象にしか重みが設定されていないかをバリデーションします。 func validateWeights(weights [] int ) error { nonweightedCount := 0 for _, weight := range weights { if weight < 0 { return errors.New( "invalid weight" ) } if weight == 0 { nonweightedCount++ } } if nonweightedCount != 0 && nonweightedCount != len (weights) { return errors.New( "mixed weighted and nonweighted targets" ) } return nil } 下記は target_groups.yml で weight に負の値が設定されているNG例です。 TargetGroupA : targets : - host : target1.example.com port : 8080 weight : -10 - host : target2.example.com port : 8081 weight : -1 下記は routes.yml で weight プロパティが設定されているものと、されていないものが混在しているNG例です。 - from : path : ^/sample/(.+)$ to : destinations : - target_group : TargetGroupA path : /$1 weight : 2 - target_group : TargetGroupB path : /$1 一般的なラウンドロビンのスケジューラ 一般的なラウンドロビンのスケジューラ実装について説明します。 構造体 roundRobinScheduler 構造体は以下の変数を持ちます。 mutex 複数のリクエストを排他制御する current 現在のインデックス 初期値は0(つまりリストの先頭) count ラウンドロビン対象の総数 newRoundRobinScheduler 関数の引数で渡される type roundRobinScheduler struct { mutex *sync.Mutex current int count int } newRoundRobinScheduler 関数は NewScheduler 関数から呼び出されます。初期化して roundRobinScheduler のポインタ型の変数を返します。 func newRoundRobinScheduler(count int ) *roundRobinScheduler { return &roundRobinScheduler{mutex: &sync.Mutex{}, current: 0 , count: count} } Fetchメソッド roundRobinScheduler 構造体の Fetch メソッドの処理内容は以下の通りです。 排他制御ロックをかける 次の呼び出しに備えてインデックス current をインクリメントしておく リスト上に記載された次の対象を返すようにする もしリスト上で最後の場合は、最初のターゲットを返す 現在のインデックスを返す func (s *roundRobinScheduler) Fetch() int { s.mutex.Lock() defer s.mutex.Unlock() i := s.current s.current = (i + 1 ) % s.count return i } 例えば、下記の target_groups.yml があった場合に3回 Fetch メソッドを実行するときの挙動を見てみます。 TargetGroupA : targets : - host : target1.example.com port : 8080 - host : target2.example.com port : 8081 まず、 roundRobinScheduler 構造体の以下のフィールドは値が決定します。 count: 2 各実行回数とその時の最終的な値は以下の通りです。次回は前回の変数の値を引き継ぎます。 1回目 Fetch()の戻り値: 0(target1.example.comが選択される) current: 1 2回目 Fetch()の戻り値: 1(target2.example.comが選択される) current: 0 3回目 Fetch()の戻り値: 0(target1.example.comが選択される) current: 1 重み付きラウンドロビンのスケジューラ 重み付きラウンドロビンのスケジューラ実装について説明します。 構造体 weightedRoundRobinScheduler 構造体は以下の変数を持ちます。 mutex 複数のリクエストを排他制御する weights 対象全ての重み newWeightedRoundRobinScheduler 関数の引数で渡される maxWeight weights の中で最大の重み newWeightedRoundRobinScheduler 関数で決定される currentIndex 現在のインデックス Fetch メソッドで返される値 初期値は -1 currentWeight インデックスを返す際の基準となる重み currentWeight よりも重い weight を持つ currentIndex を Fetch メソッドで返す 初期値は0 type weightedRoundRobinScheduler struct { mutex *sync.Mutex weights [] int maxWeight int currentIndex int currentWeight int } newWeightedRoundRobinScheduler 関数は NewScheduler 関数から呼び出されます。 normalizeWeights 関数を実行して weightedRoundRobinScheduler のポインタ型の変数を返します。処理中に、引数で渡された重み weights を normalizeWeights 関数で約分します。 func newWeightedRoundRobinScheduler(weights [] int ) *weightedRoundRobinScheduler { normalizedWeights := normalizeWeights(weights) max := 0 for _, w := range normalizedWeights { if w > max { max = w } } return &weightedRoundRobinScheduler{weights: normalizedWeights, maxWeight: max, currentIndex: - 1 , currentWeight: 0 , mutex: &sync.Mutex{}} } 重みの約分 normalizeWeights 関数は gcd 関数により求めた最大公約数で weights の要素を全て約分した結果を返します。約分する理由は、 Fetch メソッド内で無駄なforループを無くすためです。 func normalizeWeights(weights [] int ) [] int { g := weights[ 0 ] for _, w := range weights[ 1 :] { g = gcd(g, w) } normalizedWeights := [] int {} for _, w := range weights { normalizedWeights = append (normalizedWeights, w/g) } return normalizedWeights } gcd 関数は標準パッケージの math/bigのGCD を利用して ユークリッドの互除法 により、最大公約数(greatest common divisor)を返します。 func gcd(m, n int ) int { x := new (big.Int) y := new (big.Int) z := new (big.Int) a := new (big.Int).SetUint64( uint64 (m)) b := new (big.Int).SetUint64( uint64 (n)) result := z.GCD(x, y, a, b) return int (result.Int64()) } Fetchメソッド はじめに、イメージを掴んでいただくために Fetch メソッドによる走査処理の概要から説明します。 例えば、 target1に3、target2に5、target3に1 といった重み付けがされたケースを考えます。この場合、以下の割り当て順にはなりません。 target1 →target1 →target1 →target2 →target2 →target2 →target2 →target3 下のような2次元配列のようにして上から順に、左から走査する形で割り当てます。 weight 5: target2 weight 4: target2 weight 3: target1 target2 weight 2: target1 target2 weight 1: target1 target2 target3 つまり、割り当て順は以下になります。 target2 →target2 →target1 →target2 →target1 →target2 →target1 →target2 →target3 それでは、実装を見ていきます。 weightedRoundRobinScheduler 構造体の Fetch メソッドの処理内容は以下の通りです。 排他制御ロックをかける currentIndex をインクリメントし、リストの最後の場合は先頭に戻る currentIndex がリストの先頭の場合は、 currentWeight をデクリメントする デクリメントした currentWeight が負の値であれば currentWeight に maxWeight を代入する currentIndex における weights の値が currentWeight 以上であればその currentIndex を返し、未満であれば2-4のステップを繰り返す func (s *weightedRoundRobinScheduler) Fetch() int { s.mutex.Lock() defer s.mutex.Unlock() for { s.currentIndex = (s.currentIndex + 1 ) % len (s.weights) if s.currentIndex == 0 { s.currentWeight = s.currentWeight - 1 if s.currentWeight <= 0 { s.currentWeight = s.maxWeight } } if s.weights[s.currentIndex] >= s.currentWeight { return s.currentIndex } } } 例えば、下記の target_groups.yml があった場合に3回 Fetch メソッドを実行するときの挙動を見てみます。 TargetGroupA : targets : - host : target1.example.com port : 8080 weight : 1 - host : target2.example.com port : 8081 weight : 2 まず、 weightedRoundRobinScheduler 構造体の以下のフィールドは値が決定します。 maxWeight: 2 weights: [1, 2] 各実行回数とその時の最終的な値は以下の通りです。次回は前回の変数の値を引き継ぎます。 1回目 Fetch()の戻り値: 1(target2.example.comが選択される) currentIndex: 1 currentWeight: 2 2回目 Fetch()の戻り値: 0(target1.example.comが選択される) currentIndex: 0 currentWeight: 1 3回目 Fetch()の戻り値: 1(target2.example.comが選択される) currentIndex: 1 currentWeight: 1 Fetchメソッドの呼び出し 上記で説明した Fetch メソッドの呼び出し側について説明します。ターゲットとターゲットグループのいずれのスケジューリングにも使用できます。 ターゲットのスケジューリング 下記のように target_groups.yml の targets 配下には複数のターゲットを指定可能です。 TargetGroupA : targets : - host : target1.example.com port : 8080 weight : 1 - host : target2.example.com port : 8081 weight : 2 TargetGroup 構造体は Scheduler インタフェース型の変数 scheduler をフィールドとして持っています。したがって、 TargetGroup 構造体のポインタ型をレシーバに持つ ScheduledTargets メソッド内で Fetch メソッドを呼び出せます。 ScheduledTargets メソッドは、複数の中から1つのターゲットを決定するために Fetch メソッドを呼び出します。 Fetch メソッドにより取得したインデックスで最初のターゲットを決定します。 type Target struct { Host string Port int Timeout timeout } type TargetGroup struct { // ... scheduler scheduler.Scheduler targets []*Target } func (targetGroup *TargetGroup) ScheduledTargets(length int ) []Target { targetIndex := targetGroup.scheduler.Fetch() roundRobinTargets := []Target{} target := targetGroup.targets[targetIndex] // ... return roundRobinTargets } ターゲットグループのスケジューリング 下記のように routes.yml の destinations 配下には複数のターゲットグループを指定可能です。 - from : path : ^/sample/(.+)$ to : destinations : - target_group : TargetGroupA path : /$1 - target_group : TargetGroupB path : /$1 routeTo 構造体は Scheduler インタフェース型の変数 scheduler をフィールドとして持っています。したがって、 routeTo 構造体のポインタ型をレシーバに持つ scheduledDestination メソッド内で Fetch メソッドを呼び出せます。複数の中から1つのターゲットグループを決定するために Fetch メソッドを使用します。 Fetch メソッドにより取得したインデックスでターゲットグループを選定します。 type routeTo struct { destinations []destination scheduler scheduler.Scheduler } type destination struct { targetGroup *targetgroup.TargetGroup path string } func (routeTo *routeTo) scheduledDestination() destination { destinationIndex := routeTo.scheduler.Fetch() return routeTo.destinations[destinationIndex] } リトライ機能 ここでは前回の記事では触れなかったリトライ機能の一部仕様を紹介します。 前回記事のおさらい 前提知識として必要なものを簡単に説明します。 リトライ機能は、リクエスト失敗時にAPI Gatewayとマイクロサービス間でリトライする機能です。どのようなシステムであっても、なんらかの原因でリクエストが失敗する可能性はあります。 例えば、転送先マイクロサービスの一時的なエラー、通信問題、タイムアウトなどです。その失敗をAPIクライアントへそのまま返さずに、API Gatewayとマイクロサービス間でリトライします。リトライによりリクエストが成功すれば、エンドユーザへエラーを返さずにすむため成功リクエスト数が増えることになり、可用性を高められます。 以下のように、リトライ回数やリトライ条件、リトライ先ターゲット、リトライ前のスリープを target_groups.yml から設定可能です。 TargetGroupAB : targets : - host : target-a-1.example.com port : 8080 retry_to : target-b-2.example.com - host : target-a-2.example.com port : 8080 retry_to : target-b-1.example.com - host : target-b-1.example.com port : 8080 retry_to : target-a-2.example.com - host : target-b-2.example.com port : 8080 retry_to : target-a-1.example.com max_try_count : 3 retry_cases : [ "server_error" , "timeout" ] retry_non_idempotent : true retry_base_interval : 50 retry_max_interval : 500 ターゲットグループを跨いだリトライ デフォルトでは、リトライ先のターゲットは同一ターゲットグループに属するもので限定されるため、ターゲットグループを跨ぎません。しかしながら、ターゲットグループを跨いだリトライでメリットが生まれるケースもあります。 例えば、TargetGroupAを新系、TargetGroupBを旧系のターゲットグループとします。TargetGroupAのTarget1で変更部分のバグによりリクエストが失敗した場合に、同じターゲットグループのTarget2へリトライしても同じ失敗になります。しかしながら、そのバグを含まないTargetGroupBのTarget3へリトライすればエンドユーザへの影響を最小限に抑えられます。エンドユーザへの若干のレスポンス速度の低下は発生しますが、エラーが返らずに済みます。また、マイクロサービスがターゲットグループ間で異なる場合は、下図のようにパスも異なる可能性があります。 設定方法 target_groups.yml の retry_to_target_group_id プロパティにターゲットグループIDを指定します。リトライ時は指定したターゲットグループのターゲットへのリトライになります。 下記は TargetGroupA の retry_to_target_group_id で TargetGroupB を指定している例です。 TargetGroupA : targets : - host : target1.example.com port : 8080 retry_to_target_group_id : TargetGroupB TargetGroupB : targets : - host : target2.example.com port : 8081 リバースプロキシとリクエスト情報の準備 HTTPリクエストをマイクロサービスへ転送する上で、リトライ情報を含めたリクエスト準備が必要です。ここでは、リクエスト情報を準備する上でどのように転送先のターゲット情報やリトライ情報を作成しているかの実装面を紹介します。 ServeHTTP メソッドは Route メソッドと transferRequest メソッドを実行します。 http.Handler のインタフェースのメソッドでもあります。 func (reverseProxy ReverseProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { // ... func () { // ... fetchedTargetGroup, routedPathMap, e := reverseProxy.router.Route(client, ip, r) // ... response, body, tryURLs, e = reverseProxy.transferRequest(r, fetchedTargetGroup, routedPathMap, traceID, client) // ... } } Route メソッドは fetchedTargetGroup と routedPathMap を返します。 fetchedTargetGroup は、内部の Fetch メソッドにより取得した最初の転送先ターゲットグループです。 routedPathMap はマッチしたルーティングに含まれる全てのターゲットグループとAPIパスの組み合わせを持つマップです。 func (router Router) Route(c client.Client, ip net.IP, req *http.Request) (fetchedTargetGroup *targetgroup.TargetGroup, routedPathMap map [*targetgroup.TargetGroup] string , e error ) { routedPathMap = map [*targetgroup.TargetGroup] string {} for _, route := range router.routes { // ... fetchedTargetGroup = route.to.scheduledDestination().targetGroup for _, d := range route.to.destinations { routedPathMap[d.targetGroup] = route.from.path.ReplaceAllString(req.URL.Path, d.path) } return } // ... return } func (routeTo *routeTo) scheduledDestination() destination { destinationIndex := routeTo.scheduler.Fetch() return routeTo.destinations[destinationIndex] } transferRequest メソッドはHTTPリクエストの転送処理をします。 Route メソッドの戻り値の fetchedTargetGroup と routedPathMap を引数に渡します。 func (reverseProxy ReverseProxy) transferRequest(r *http.Request, fetchedTargetGroup *targetgroup.TargetGroup, routedPathMap map [*targetgroup.TargetGroup] string , traceID string , client client.Client) (response *http.Response, body [] byte , tryURLs [] string , e error ) { // ... } transferRequest メソッド内では target_groups.yml に retry_to_target_group_id が指定されているかどうかで異なるリクエスト情報を作成します。 最大の試行回数 maxTryCount だけ、 targets 変数に以下のケースに基づいたターゲット情報を格納します。 retry_to_target_group_id が指定されていない場合 fetchedTargetGroup の ScheduledTargets の戻り値 retry_to_target_group_id が指定されている場合 最初のターゲットは fetchedTargetGroup の ScheduledTargets の戻り値の1つ目の要素 それ以降のターゲットはリトライ先のターゲットグループの ScheduledTargets の戻り値(要素数は1つ減らす) maxTryCount := fetchedTargetGroup.MaxTryCount() if !fetchedTargetGroup.RetryNonIdempotent() && (r.Method == http.MethodPost || r.Method == http.MethodPatch) { maxTryCount = 1 } var targets []targetgroup.Target if fetchedTargetGroup.RetryToTargetGroup() == nil { targets = fetchedTargetGroup.ScheduledTargets(maxTryCount) } else { firstTarget := fetchedTargetGroup.ScheduledTargets( 1 )[ 0 ] targets = append (targets, firstTarget) scheduledTargets := fetchedTargetGroup.RetryToTargetGroup().ScheduledTargets(maxTryCount - 1 ) targets = append (targets, scheduledTargets...) } ScheduledTargets メソッドのリトライ対象の決定に関する処理を補足します。 Fetch メソッドで先頭のターゲットIDを決定した後に、for文内では決定したターゲット情報をkeyに retryTargetMap フィールドからリトライ先情報を取得します。 retryTargetMap は、keyがターゲットIDでvalueにそのターゲットIDへのリクエストが失敗した場合の次の転送先のターゲットIDを持ちます。リトライ先のターゲットはラウンドロビンでなく target_groups.yml の retry_to に設定されたターゲットあるいは次のインデックスのターゲットを使用します。このようにして、引数のリトライ上限回数 length の数だけfor文でターゲット情報の集合 roundRobinTargets を生成して返します。 type TargetGroup struct { // ... retryTargetMap map [*Target]*Target } func (targetGroup *TargetGroup) ScheduledTargets(length int ) []Target { targetIndex := targetGroup.scheduler.Fetch() roundRobinTargets := []Target{} target := targetGroup.targets[targetIndex] for len (roundRobinTargets) < length { roundRobinTargets = append (roundRobinTargets, *target) target = targetGroup.retryTargetMap[target] } return roundRobinTargets } 作成した targets の数、つまり最大の試行回数だけ以下の情報を作成します。 url.Path 変数に以下のケースに基づいたパス情報を格納します。 retry_to_target_group_id が指定されていない場合 fetchedTargetGroup をkeyに routedPathMap から取得したもの retry_to_target_group_id が指定されている場合 リトライ先のターゲットグループをkeyに routedPathMap から取得したもの retryInfo 変数に以下のケースに基づいたリトライ情報を格納します。 retry_to_target_group_id が指定されていない場合 fetchedTargetGroup のリトライ情報 retry_to_target_group_id が指定されている場合 リトライ先のターゲットグループのリトライ情報 targetURLs := [] string {} // ... type retryInfo struct { cases []targetgroup.RetryCase baseInterval int maxInterval int } retryInfos := []retryInfo{} for i, target := range targets { url := *r.URL url.Scheme = "http" url.Host = fmt.Sprintf( "%v:%v" , target.Host, target.Port) var r retryInfo retryToTargetGroup := fetchedTargetGroup.RetryToTargetGroup() if i == 0 || retryToTargetGroup == nil { url.Path = routedPathMap[fetchedTargetGroup] r = retryInfo{ cases: fetchedTargetGroup.RetryCases(), baseInterval: fetchedTargetGroup.RetryBaseInterval(), maxInterval: fetchedTargetGroup.RetryMaxInterval(), } } else { url.Path = routedPathMap[retryToTargetGroup] r = retryInfo{ cases: retryToTargetGroup.RetryCases(), baseInterval: retryToTargetGroup.RetryBaseInterval(), maxInterval: retryToTargetGroup.RetryMaxInterval(), } } targetURLs = append (targetURLs, url.String()) // ... retryInfos = append (retryInfos, r) } マイクロサービスへのリクエスト 上述の通り、リトライ情報含めたリクエストに必要な情報を準備しました。後続の処理では transferRequestToHTTP 関数でマイクロサービスにHTTPリクエストします。こちらの関数内処理の詳細は本リトライの話から逸れますので割愛します。 for i, targetURL := range targetURLs { response, body, e = transferRequestToHTTP(r, requestBody, targetURL, httpClients[i], traceID, client) // ... } タイムアウト機能 ここでは前回の記事では触れなかったタイムアウト機能の一部仕様を紹介します。 前回記事のおさらい 前提知識として必要なものを簡単に説明します。 タイムアウトとは、その名の通り、一定の期間が経過したリクエストを打ち切ることです。実行が長引いているリクエストをタイムアウトさせて、後ろに詰まっているリクエストを正常に処理することで全体としてリクエスト成功数が増え、可用性を高められます。 API GatewayにはリバースプロキシするHTTPリクエストのタイムアウト設定が可能です。 設定項目は以下の通りです。 connect_timeout 1リクエストあたりのTCPコネクション確立までの間のタイムアウト値(ミリ秒単位) read_timeout 1リクエストあたりのリクエスト開始からレスポンスボディを読み込み終わるまでの間のタイムアウト値(ミリ秒単位) ターゲットとターゲットグループのタイムアウト ターゲットとターゲットグループの両方にタイムアウト設定が可能です。 例えば、マイクロサービス化した新しいターゲットはレスポンスが速いのでそちらだけタイムアウトを小さくしたいといったケースです。該当のターゲットにタイムアウト設定しつつ他のターゲットにはターゲットグループのタイムアウト設定をデフォルトとして適用させることが可能です。 設定 以下の target_groups.yml の通り、両方で connect_timeout と read_timeout の設定が可能です。 TargetGroupA : targets : - host : target1.example.com port : 8080 connect_timeout : 10 read_timeout : 5000 connect_timeout : 50 read_timeout : 3000 実装 TargetGroupConfig は target_groups.yml に対応する構造体です。タイムアウト関連のフィールドも持ちます。 type TargetGroupConfig struct { Targets [] struct { // ... ConnectTimeout int `yaml:"connect_timeout"` ReadTimeout int `yaml:"read_timeout"` // ... } `yaml:"targets"` ConnectTimeout int `yaml:"connect_timeout"` ReadTimeout int `yaml:"read_timeout"` // ... } newTargetGroup 関数は TargetGroup 型の変数を返します。その処理過程でターゲットグループとターゲットのタイムアウト設定を読み込みます。ターゲットのタイムアウト設定に関しては、 ターゲットの設定>ターゲットグループの設定>ハードコーディングによるデフォルト設定 の順で優先付けされています。 const defaultConnectTimeout = 1000 const defaultReadTimeout = 10000 const defaultRetryBaseInterval = 50 // ... func newTargetGroup(targetGroupConfig TargetGroupConfig) (TargetGroup, error ) { targets := []*Target{} // ターゲットグループにタイムアウト設定があればそれを使う。なければハードコーディングのデフォルト値とする。 if targetGroupConfig.Timeout < 0 || targetGroupConfig.ReadTimeout < 0 || targetGroupConfig.ConnectTimeout < 0 { return TargetGroup{}, errors.New( "invalid timeout" ) } targetGroupConnectTimeout := defaultConnectTimeout if targetGroupConfig.ConnectTimeout != 0 { targetGroupConnectTimeout = targetGroupConfig.ConnectTimeout } targetGroupReadTimeout := defaultReadTimeout if targetGroupConfig.ReadTimeout != 0 { targetGroupReadTimeout = targetGroupConfig.ReadTimeout } else if targetGroupConfig.Timeout != 0 { targetGroupReadTimeout = targetGroupConfig.Timeout } // ... // ターゲットのタイムアウト設定があればそれを使う。なければターゲットグループの値とする。 for i, t := range targetGroupConfig.Targets { targetConnectTimeout := targetGroupConnectTimeout targetReadTimeout := targetGroupReadTimeout if t.ConnectTimeout != 0 { targetConnectTimeout = t.ConnectTimeout } if t.ReadTimeout != 0 { targetReadTimeout = t.ReadTimeout } target := Target{ // ... Timeout: timeout{ Connect: targetConnectTimeout, Read: targetReadTimeout, }, } // ... } // ... } Target 構造体のタイムアウト値は、 http.Client の Timeout に Connect の値、 net.Dialer の Timeout に Read の値を使用します。 http.Client{ Transport: &http.Transport{ DialContext: (&net.Dialer{ Timeout: time.Duration(connectTimeout) * time.Millisecond, // ... }).DialContext, // ... }, // ... Timeout: time.Duration(readTimeout) * time.Millisecond, } 機能追加の展望 今後、さらに可用性を高める以下の機能追加を考えています。 スロットリング クライアントタイプごとにレートリミットで制限するような機能を想定 一部のクライアントタイプによる大量リクエストでシステム全体が停止するのを避け、可用性を高める サーキットブレーカー ある閾値以上の失敗が続いたら、そのマイクロサービスにはリクエストを送らずにAPI Gatewayが503エラーを返すような機能を想定 カスケード障害を防ぎ、可用性を高める We are hiring ZOZOTOWNのマイクロサービス化はまだ始まったばかりです。今後は、API GatewayやID基盤の追加開発に加えて、新たなマイクロサービスの開発も目白押しです。そのためのエンジニアが足りていない状況です。 ご興味のある方は、以下のリンクからぜひご応募ください。お待ちしております。 hrmos.co
アバター
こんにちは。ECプラットフォーム部の廣瀬です。 弊社ではサービスの一部にSQL Serverを使用しています。今回は2020年度に発生していたSQL Serverに関連する障害について、調査から対策実施までの流れを紹介したいと思います。これまでも弊社テックブログにて、SQL Serverに関するトラブルシューティングをいくつか紹介してきました。 techblog.zozo.com techblog.zozo.com techblog.zozo.com これらの記事と今回の記事の最大の相違点としては、「最後まで明確な原因の特定はできなかった」という点です。できる限り詳細な調査を実施しましたが、最後まで原因の特定には至りませんでした。そのような状況下において、どのようなフローで調査を実施し、最終的に障害が発生しない状況を作ることができたか紹介します。 SQL Server以外のデータストアを運用していて障害調査をすることがある方にも読んでいただけると幸いです。 障害の概要 弊社のサービスは、様々な要因でトラフィックが増大します。例えば、セールイベントや福袋発売イベントなどです。2020年の中頃から、ある機能を担っていたDBにおいて秒間のバッチ実行数が一定の閾値を超えると、アプリケーション側でタイムアウトエラーが多発するようになりました。尚、 バッチ とはSQL Serverにおける1つ以上のクエリのかたまりを指します。 上の図では、タイムアウトエラーの多発開始と同時に、秒間のバッチ実行数が急激に下がりDBサーバーのクエリ処理性能が落ちていることが分かります。また、バッチの総実行時間が急激に増えていることから、1クエリあたりの実行時間が大幅に伸びていることも分かります。 この事象を詳細に調査していきました。調査に用いた情報は、以下の3点です。 パフォーマンスモニタのSQL Serverに関連するメトリクス 拡張イベントの「blocked_process_report」 DMV(動的管理VIEW)の各種データを1分毎にテーブルへ保存しておいたもの 弊社のSQL Server障害調査フロー 弊社では、SQL Serverに関する障害が発生した際、以下のフローチャートに沿って調査を進めています。 以降では、このフローに沿って実際に障害を調査した事例を紹介します。 調査内容 1. 主要なメトリクスを確認 パフォーマンスモニタで取得したメトリクスのうち、主要なものを確認しました。フローチャートの①に該当します。 CPU使用率はピーク時で100%に近い値となっていました。Batch Resp Statisticsを確認すると、CPU使用時間が20msecから100msecのバッチがCPU使用率上昇の主な要因となっていました。その他、目立った変化のあったグラフを以下に列挙します。 これらのグラフから、以下のことが読み取れます。 ブロッキングの発生が確認できた SQL Server観点でもエラー発生が確認できた 待ち事象としては非ページラッチが障害発生の初期にスパイクしていた バッチ実行数の増加に伴ってコンパイル数も増加していた ワークスペースメモリを獲得したクエリの数が増大していた ワーカースレッド獲得待ちが多く発生しており、ワーカースレッドが枯渇していた可能性が高い これらの情報を見ただけでは、「何らかの理由でクエリが滞留し、最終的にワーカースレッドが枯渇してタイムアウトエラー多発につながった」ということしか分かりません。 クエリが滞留した理由としては、以下のようなものが考えられます。 CPU使用率が高騰したことでSOS_SCHEDULER_YIELD待ちが多発 非ページラッチの競合の発生 アクセスが集中する領域に長時間ロックがかかったことでブロッキングが多発 このような可能性に留意しつつ、エラーが起き始めた変化点にフォーカスし、より短い時間軸でメトリクスを確認しました。「エラーが起き始めたタイミングと同タイミングで変化し始めたメトリクス」が障害に関連している可能性が高いと考えたからです。 青い線がCPU使用率ですが、Attentionが発生したタイミングではCPU使用率が低下しているためCPU使用率が障害発生の原因とは考えにくい状況でした。エラー発生前後の最初の変化としては、まずPage Latchの発生が確認でき、次にNon-Page Latch、ブロッキングの順番で発生が確認できました。この結果から、Page Latch、Non-Page Latch、ブロッキングの発生は、エラーが発生し始めた要因と関連性がありそうです。 このような可能性に留意しつつ、保存しておいた拡張イベントとDMVの情報を後追いしていきました。 2. 拡張イベントでブロッキング状態を確認 こちら の方法で、「blocked_process_report」イベントをクエリベースで確認しました。フローチャートの②に該当します。 この結果から、以下のことが分かりました。 最初にブロッキングイベントが発生したのは、障害発生から15秒ほど経過した後 最初のブロッカーはbackgroundプロセスであり、自身も「ACCESS_METHODS_HOBT_VIRTUAL_ROOT」という種類のラッチで待たされていた 「ACCESS_METHODS_HOBT_VIRTUAL_ROOT」は ドキュメント によると、「内部Bツリーのルートページの抽象化へのアクセスを同期するために使用されます」と説明してありました。ブロッキングイベントが発生したのは障害発生から15秒ほど経過してからでしたが、パフォーマンスモニタ上では障害発生前からわずかにブロッキングが発生していました。拡張イベントのブロッキングイベントは、「〇秒以上ブロッキングが続いたら発生」というように一定の閾値を超えたものだけが記録されるため、イベントが記録されるまでにタイムラグが発生していたようです。また、拡張イベントに記録された最初のブロッカーがどのような処理をしていたかは分からず、原因特定には至りませんでした。 3. DMVの各種データを使ってドリルダウン フローチャートの③に該当します。 CPUのボトルネッククエリ調査 こちら の方法でCPUボトルネックとなっているクエリが無いかを調査しました。Batch Resp Statisticsの確認結果から、特にCPU使用時間が20msecから100msecのバッチを重点的に確認しました。しかし、今回はCPU負荷の観点でチューニングできそうなクエリは見つかりませんでした。 ラッチ こちら のクエリを使って障害発生中の各種ラッチの待ち時間を確認しました。 ラッチによる総待機時間の大きい順に並べた結果、ACCESS_METHODS_ACCESSOR_CACHEが最も大きく、次いでBUFFER、ACCESS_METHODS_HOBT_VIRTUAL_ROOTという順番になりました。 ACCESS_METHODS_HOBT_VIRTUAL_ROOTについては、平均の待機時間が大きいようです。この待ちはページ分割時などに発生すると理解しており、特定のテーブルへのLast Page Insert多発が原因の可能性として挙げられます。その場合は、テーブルのキーの再設計などによって該当のラッチを減らせるかもしれないと考えました。 ACCESS_METHODS_ACCESSOR_CACHEについては、コンパイルの発生に伴って値が上昇する傾向にあるようです。したがってコンパイル回数の抑制によって該当のラッチ待ち時間を低減できる可能性があると考えました。 待ち事象 こちら のクエリを使って、障害発生中の待ち事象について確認しました。 THREADPOOLは二次的な待ちのため無視するとして、非ページラッチ、ページラッチ、ロックなどが待ち事象として多くを占めていました。 実施した対応策 時間をかけて調査しましたが、根本的な原因の特定には至りませんでした。したがって次善の策として、取得した情報を使って「障害発生中に観測できた事象の中から、無くしたり減らすことのできるものを探す」という観点で対応策を決めて実施していきました。 1. 特定のストアドプロシージャのプランガイドの固定化 別の日に同様の障害が発生した際、拡張イベントの「blocked_process_report」におけるwaitresourceごとのイベント件数を抽出してみました。その結果、特定のストアドプロシージャのコンパイル待ちが原因でブロッキングが発生していることが分かりました。 したがって、これら4つのストアドプロシージャのプランガイドを固定化することにしました。これは根本的な原因を特定したうえでの対応策ではありませんが、「もし同様の事象が発生しても、コンパイル起因でのブロッキングの発生を0にする」という効果が期待できます。 2. コンパイルとリコンパイル発生を抑制する施策 バッチ実行数の上昇に伴ってコンパイル数も上昇したことがパフォーマンスモニタのメトリクスから確認できていました。コンパイルやリコンパイルによって効率の悪い実行プランが採用されてしまい、CPU使用率が上昇した可能性も考えられます。したがって、コンパイルとリコンパイルの発生回数を抑制する対応を実施しました。コンパイルとリコンパイルの発生要因として代表的なものは、以下の2つです。 関連テーブルの統計情報が更新された 実行プランがキャッシュアウトされた 1.については、統計情報の更新が今回の環境におけるリコンパイルの理由として本当に正しいのかをまず検証しました。拡張イベントで「sql_statement_recompile」イベントを取得することで、リコンパイルの理由を確認できます。 拡張イベントの作成後、以下のようにリコンパイルイベントの発生が確認できました。 リコンパイルの理由は「recompile_cause」に記載されており、図の中では「Statistics changed」となっています。つまり、統計情報の更新がリコンパイルの理由だと分かります。 リコンパイルの各理由の内訳を確認するために「recompile_cause」でグループ化してみると、下図のようにリコンパイルの原因の大多数は統計情報の更新であることが確認できました。 したがって、障害発生時のリコンパイルについても統計情報の自動更新が起因となったものが大多数であるはずだと判断しました。次に、 こちらのクエリ を使って障害が発生した時間帯で「統計情報が1回以上更新されたテーブル」をリストアップしました。この情報をもとに、トラフィック増が見込まれる時間帯だけ、リストアップしたテーブル群の統計情報の自動更新を一時的に無効化するようにしました。統計情報の自動更新を無効化するには、以下のクエリを実行すればOKです。 UPDATE STATISTICS tablename WITH NORECOMPUTE これによって、統計情報の更新が起因となったリコンパイルの発生を抑止できます。しかし、自動更新を常に無効化しておくのは、最適な実行プランが生成されなくなる確率を上昇させる高リスクな行為です。したがって、トラフィックが落ち着いてきたタイミングで自動更新を再度有効化するようにしました。自動更新の有効化は、以下のクエリのように「WITH NORECOMPUTE」無しで統計情報を更新すればOKです。 UPDATE STATISTICS tablename 2.のプランキャッシュアウトについては、SQL Serverがメモリの利用状況をコントロールしているため、こちらで制御できません。ベストエフォートな対応として、メモリに載っているデータキャッシュのうち、容量が大きいインデックスを圧縮することでメモリの利用効率アップを試みました。尚、メモリに載っているサイズが大きいインデックスを抽出するには こちらのクエリ を使用しました。 結果 各種対策を実施したことで、障害が発生しなくなりました。対策実施前と後での各メトリクスの変化を紹介します。 同程度のバッチ実行数の増加でも、クエリの滞留が起きなくなったため、対策実施後はコネクションの上昇が顕著に抑えられるようになりました。エラーが発生することもなくなり、明らかにパフォーマンスが改善しました。 CPU使用率はピーク時100%近くだったのを、ピーク時60%ほどまで抑えられるようになりました。 コンパイル数とリコンパイル数も顕著に発生回数を抑えられるようになりました。リコンパイルについては、統計情報の自動更新を一時的に無効化したことが顕著に効いています。コンパイルの発生回数が抑えられた要因については、仮説として「データ圧縮によりデータキャッシュのサイズが減少し、プランキャッシュが安定したことでキャッシュアウトしづらくなった」と考えられます。 対策実施前はクエリの総実行時間が障害時に大幅に増加していたのに対し、対策実施後は安定しました。トラフィック増により秒間のバッチ実行数が増えても、同時実行性が低下することなく、安定して処理できていることが分かります。 ロック競合の発生状況も大幅に改善しており、対策実施後は秒間のバッチ実行数が最大に達したタイミングでも、ロック競合は確認されませんでした。 このように、各種メトリクスに大幅な改善傾向がみられ、結果としてトラフィック増大時でも障害が発生しない状況をつくることができました。 まとめ 本記事では、2020年度に発生していたSQL Serverに関連する障害について、調査から対策実施までの流れを紹介しました。まずは特異な変化が起きているメトリクスを確認し、原因の仮説を立てながらDMVや拡張イベントを使ってドリルダウンしていきました。また、障害発生の前後数秒間で起きた変化を細かく見ていく調査も実施しました。いくつか障害原因の仮説は立てられたものの、特定にまでは至りませんでした。 こうした根本的な原因が特定できない状況において、「障害発生中に観測できた事象の中から、無くしたり減らすことのできるものを探す」という観点で対応策を決めて実施しました。その結果、最終的に障害が発生しない状況を作ることができました。原因が特定できない障害調査における対応策の実施について、今回紹介した考え方が参考になれば幸いです。 最後に ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
アバター
はじめに こんにちは。BtoB開発チームの中島です。 Fulfillment by ZOZO (以下、FBZ)で提供しているAPIシステムの開発・運用を担当しています。 FBZの運用では、エラーログ発生時にアラートを通知させ、エラー内容をチェックして対応要否を判断しています。しかし、アラート通知が多すぎると運用負荷が高くなったり、重要なアラートを見落とすリスクもあるため、適切な量で通知することが重要になってきます。 本記事では、FBZで実施した例を紹介しながらアラート通知の最適化について解説します。 FBZにおけるサービス監視 FBZでは、ログ解析によるサービス監視を実施しています。 AWS Lambda(以下、Lambda)から出力されたログを解析し、外部サービスのPagerDutyやDatadogに連携して監視しています。必要に応じてフィルタリングを行い、ログの通知量を都度調整しながら運用をしていました。 ログ解析によるサービス監視の詳細は、過去記事をご覧ください。 techblog.zozo.com サービス監視の課題 上記の過去記事でも紹介しているように、監視不要なログのフィルタリングを実施することでアラートを削減する効果がありました。 しかし、FBZサービスの機能追加などに伴い、再度アラートが増えてきました。デイリーで数十件程度アラートが通知されることも多く、通知が多いことでアラートがオオカミ少年化し始めていました。 そのため、フィルタリングだけでは対応が不十分と考え、監視方法を再検討することにしました。 通知されるアラートの現状分析 監視方法を再検討するために、まず現状の分析を実施しました。 現状は通知されるアラートを運用担当者が内容を確認して、1件1件対応要否を判断しています。運用を担当するメンバーから、日々運用をしている中で以下のLambdaのアラートが多く、ノイズになっている可能性が高いという意見が出てきました。 バッチ処理内でFBZ外と連携しているLambda(以下、外部連携バッチ) リトライで回復していることが多い FBZのデータを参照するAPIのLambda(以下、参照系API) 一時的なタイムアウトが多い ただし、あくまでも感覚的なものでしかなく、定量的なものではありません。仮説が間違っている場合、対策を実施しても改善につながらないため、アラートの発生状況を定量的にデータ分析することにしました。 アラート分析の観点 アラートを分析する観点を明確にします。今回は、「アラート発生件数」ではなく「アラート発生日数」を指標としました。 アラート発生件数を観点として考えてしまうと、他のアラートよりも大量に発生したアラートが1日だけある場合、その対策をしようとしてしまいます。しかし、それだけを対策しても運用改善に繋がりません。 定常的に発生するようなアラートに着眼するためにも、複数日にまたがって発生しているアラートの対策をすることで、運用改善につながると考えました。 高頻度でアラートが発生していたLambda アラート発生日数でFBZ内のLambdaを調査しました。過去100日程度の期間で調査し、週1回以上の頻度でアラート発生しているものを高頻度と定義して抽出したところ、以下のような結果になりました。 予想通り、「外部連携バッチ」と「参照系API」のアラートが多く、全体の半数以上を占めていることがわかりました。仮説が検証できたので、実際にこれらのアラート対策を実施しました。 外部連携バッチのアラート対策 まず、外部連携バッチで実施した具体的なアラート対策について説明します。 外部連携バッチのエラー内訳を調査したところ、99.5%がリトライで成功していることがわかりました。 FBZのバッチ処理はLambdaの非同期処理で実行しています。Lambdaは非同期で動作する場合、エラー発生時は自動的に3回リトライする仕様になっています。 エラー発生時のほとんどのケースは、このリトライが成功しており、自動回復していました。そのため、リトライでも自動回復していないケースに絞って監視をすることで、アラートの最適化が可能だと考えました。 具体的には、 Lambda Destinations(非同期呼び出しの宛先指定) を使った監視に変更しました。エラーログの監視ではリトライで成功しているものも検知してしまいますが、Lambda Destinationsを使うことでリトライをしても成功しなかったものだけを通知できます。そのため、外部連携バッチの監視は、エラーログの監視をやめ、Lambda Destinationsの通知の監視に切り替えました。 FBZでは Serverless Framework を使っているため、 Serverless.yml に以下のような設定を加えることでLambda Destinationsを設定できます。 functions : helloStarting : handler : handler.starting destinations : onFailure : arn:of:some:existing:resource onFailureにアラート通知用のLambdaを設定することにより、リトライでも成功しなかったもののみアラート通知する仕組みを実現しています。 参照系APIのアラート対策 次に、参照系APIでのアラート対策について説明します。 参照系APIで発生しているアラートの詳細を調査したところ、99.1%がネットワーク起因の一時的なエラーでした。 参照系のAPIということもあり、一時的に発生している点と再実行で取得可能となる点から1件1件の監視は不要であると判断しました。また、実際の運用時にエラーを検知した際にも、一時的なエラーの場合は対応不要と扱っていたことが多い点も監視不要とした理由です。 1件1件は監視不要ですが、短時間に頻発した場合は障害の可能性があります。そのため、頻度ベースでの監視をすることで、監視の最適化が可能だと考えました。 頻度ベースの監視は、 Datadog を利用しました。Datadogを利用することにした理由は以下の点です。 既にサービス監視のためにDatadogを利用していた点 API Gatewayのメトリクスで頻度ベースの監視ができる点 FBZ側に新規の作り込みが不要な点 既にサービス監視のためにDatadogを利用していました。しかし、ログ調査での利用がメインであり、監視での利用はほとんどしていませんでした。 今回、頻度ベースで監視したいと考えた際に、Datadogのモニター機能でアラートのしきい値設定を、固定値だけではなく過去データから自動設定できるため、Datadogを利用しました。モニター設定の詳細などは Datadogのマニュアル をご確認ください。 AWSとDatadogのインテグレーションを事前に設定しておく必要はありますが、FBZ側での新規開発なしで、頻度ベースの監視が実現できました。 アラート最適化の効果 今回、アラートの最適化として、以下の対策を実施しました。 外部連携バッチはLambda Destinationsでアラート通知したものを監視 参照系APIはDatadogでエラー頻度ベースでのアラート通知したものを監視 これらの対策を実施したことで、アラートの発生数が、1日数十件から数件程度に抑えられました。昨年にはアラートが0件という日もあり、アラートの発生頻度が下がったことで運用工数を月あたり約15時間削減できました。 まとめ アラートがオオカミ少年化すると、アラート対応が疎かになりサービスの安定的な運用ができなくなります。それを防ぐために、アラートの通知は必要最小限に抑える必要がありますが、単純なエラーログの監視では通知を減らすのにも限界があります。 今回はFBZで実施した監視手法として、Lambda Destinationsを利用した非同期実行Lambdaの監視と、Datadogを利用した頻度ベースでの監視を紹介しました。この対応によって、アラートを最適な量で通知できるようになりました。 今後は、分散トレーシングのために、Datadog APMの導入などを検討しています。 さいごに ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
アバター
こんにちは。ECプラットフォーム部マイグレーションチームの半澤です。 この記事では、Java + Spring Bootを使用したアプリケーション作成時にモブプログラミングを活用した事例をご紹介します。モブプログラミング未経験の方や、これから実施を考えている方の参考になれば幸いです。 目次 目次 はじめに モブプログラミングとは プロジェクトの概要 アーキテクチャ設計 基盤構築(モブプログラミングの実施) 時間の確保 エディタの設定 カメラとマイクはON タイピストとモブ タイピスト交代時の工夫 解決できない問題が発生した場合の対処法 API実装 モブプログラミングによる効果 技術力の底上げ 終わりのない議論がなくなる 一体感の醸成 安心して休める環境の構築 反省点 タイピストの順番変更 タイマーの導入 時間の確保 まとめ 最後に はじめに ZOZOTOWNのシステムにはレガシーな部分が多く存在しており、全体的なシステムリプレイスを進めています。その中でサーバーアプリケーションのリプレイスを行うために発足したのがマイグレーションチームです。 現在はキャッシュストアのリプレイスや、新規マイクロサービスの立ち上げ、立ち上げたサービスのJava未経験チームへの引き継ぎなどを行っています。 マイグレーションチームでは、プロジェクトごとに担当者をアサインするリソース効率を重視した働き方がこれまで主流でした。リソース効率を重視すると、メンバーはそれぞれ特定の業務のスキルに特化したエキスパートとなります。ある特定の業務の仕事量が増えた場合、その数少ないエキスパートだけでは仕事をこなせなくなり、遅れが生じ全体のボトルネックとなってしまいます。 チームとしては、チームワークや思いやりを大切にし、お互い高め合うような働き方をチームの目標として掲げています。しかし、実際は業務知識が各個人で分散しているため協力しにくく、プロジェクトの規模によっては特定のメンバーのみ残業が発生するという課題を抱えていました。 そんな中、この問題を一気に解決したのがモブプログラミングです。 モブプログラミングとは モブプログラミングは、リソース効率ではなくフロー効率を高めて早く機能をリリースすることを目標としています。フロー効率を高めることで、メンバー全員が業務知識を持ち、特定のメンバーへの依存度を下げることができます。その結果、負荷の分散だけでなく、特定のメンバーが突然稼働できなくなっても開発の継続が可能です。 これが実現できれば、上記のようなチームが抱えている課題を解決してくれそうです。 モブプログラミングの手法は、ペアプログラミングから発展したもので、3人以上の人々が1台のコンピュータの前に座って協力しながら問題を解決していきます。 モブプログラミングは「タイピスト」と「モブ」の役割があります。ひとりがタイピストとしてモブの指示を理解しタイピングを行い、その他はモブとしてタイピストへ指示を出します。タイピストは定められた時間がきたら次の人へ交代します。 詳細は、 モブプログラミング・ベストプラクティス がとても参考になるのでご興味のある方はぜひご一読ください。 このようなメリットから、今回のプロジェクトでは実験的にモブプログラミングを実施することになりました。 プロジェクトの概要 私たちのチームでは、主にJava + Spring Bootを使用したアプリケーションを開発しています。今回のような新規マイクロサービス立ち上げ時は、既存のアプリケーションで使用している基盤を元に作成していました。 しかし、その参考元となる基盤も構築から既に3年経過しており、改善の余地が出てきたため、この機会にアーキテクチャやツール・ライブラリの見直しを行うことになりました。 開発メンバーは私を含め4名です。また、初回作成するAPIは同じような処理を行うGET、POST、PUTのエンドポイント計11本です。 開発は下記の流れに沿って進み、モブプログラミングは基盤構築のフェーズで実施しました。 アーキテクチャ設計 基盤構築(モブプログラミングの実施) API実装 順を追って説明します。 アーキテクチャ設計 既存サービスの基盤 はレイヤードアーキテクチャを採用していました。詳細は以下の記事をご覧ください。 techblog.zozo.com しかし、今回の新しい設計ではDDD + オニオンアーキテクチャを採用しました。オニオンアーキテクチャを選択した理由は、インフラストラクチャ層がドメイン層に依存せず、DDDが推奨するアーキテクチャの中で一番理解しやすい形だからです。 今回作成するサービスは、ビジネスロジックが多く入りこみ、機能の特性や既存システムの縛りによって複数のデータベースを使い分けることとなります。複雑なビジネスロジックをレイヤードアーキテクチャのサービス層におくとサービス層が膨らみ、変更の際に影響範囲が分かりづらいという状況が予想されます。 また、将来データベースの切り替えも想定されるため、インフラストラクチャ層に依存することを避けたいという事情もありました。 DDDの経験者は1名で、他のメンバーは文献を読み知識はあるものの、実務レベルでは未経験です。チャレンジングではありましたが、変更に強いシステムを作るためにはオニオンアーキテクチャとDDDの実践が必要でした。 次にクラス図を作成し、各クラスでどのような処理を行うかソースコードを想像できるレベルまで言語ベースで認識を合わせました。 その後、メンバーごとにツール・ライブラリについて調査しました。調査の結果を元に全員で話し合い、ビルドツールやテストフレームワークなどを選定しました。半数以上が初めて使用するツール・ライブラリで決定しました。 ここではその一部をご紹介します。 ビルドツールはバージョン管理以外にも柔軟にスクリプトを書ける点から、MavenからGradleへ変更 テストフレームワークはJUnit4からSpockへ変更 Spockは、Groovyで動作するJava・Groovyアプリケーション向けのテストフレームワークですが、強力なアサートやテストパラメータのテーブル記法などを備えている点や、ツールのサポートやコミュニティの活発さはJUnit5が優勢ですが、テストの書きやすさからSpockを採用しました 他にも検討したツール・ライブラリはありますが、ここでは割愛させていただきます。 基盤構築(モブプログラミングの実施) このフェーズでモブプログラミングを実施しました。 アプリケーションの構成をクラス図通りに構築し、GET、POSTのAPIを1本ずつ作成します。この時点では各ツール・ライブラリについて調査結果の共有から全員が大枠の理解はあるものの、選定したメンバーが一番理解しており、レベルに差がある状態です。また、アーキテクチャやDDDにおいても同様です。 ここからは実施時に気を付けた点や、実施してみて気づいた点を紹介します。 時間の確保 まずはモブプログラミングを実施する時間をカレンダー上で確保します。Google Meetでタイピストの画面を共有しながら実施するため、Google Meetのリンクも忘れずに登録します。 時間はモブプログラミングを実施する期間中、毎日3時間を確保しました。3時間とした理由は、それより短い時間だと進捗が追いつかずに開発スケジュールに影響が出そうな点と、逆に長い時間だとミーティングなど他の予定により時間を確保するのが難しかったからです。 後になって振り返ると、モブプログラミングはかなり集中力を使い、疲れてしまう点と、別途調査の時間も必要だったため、3時間は適切だったと感じています。 もし開発スケジュールに余裕がある場合は、2時間程度から初めても良さそうです。 エディタの設定 全員がIntelliJ IDEA Ultimateを使用しており、デフォルトで行番号が表示されます。もしデフォルトで行番号が表示されないエディタを使用する場合は、ソースコードの何行目について話しているのか明確にするため、事前に行番号を表示しておきましょう。 また、事前にIntelliJ IDEA Ultimateへコードフォーマットの設定をインポートするだけでなく、GitHub Actionsでもpushされたソースコードを自動でフォーマットを修正するように設定しました。コードフォーマット論争に時間を割くのは勿体無いため、こちらも用意しておくことをおすすめします。基本的にはエディタの設定でフォーマットが整う想定ですが、設定されていないエディタでpushされたソースコードが追加された際にもフォーマットを維持するためにGitHub Actionsにも設定しています。 タイピストの使用するモニターによってはモブ側の画面で文字サイズがかなり小さくなってしまいます。必要に応じて文字サイズを拡大しましょう。 カメラとマイクはON リモートワーク下における少人数のモブプログラミングでは、カメラとマイクは常時ONにしておくことをおすすめします。これによりコミュニケーションコストの削減や、円滑なコミュニケーションによりソースコードの品質向上にもつながります。 typoやバグをスムーズに指摘することはもちろん、タイピストの手が止まっていれば、なぜ止まっているのか状況を察することもできます。困った表情や悩んだポーズをしていればフォローを入れ、猫が遊びにきていたら一緒に幸せなひとときを楽しみましょう。 また、モブの中でも疑問を持っていそうな表情のメンバーがいれば、意見を聞いてみることで後からの手戻りを防ぐことができます。 タイピストとモブ タイピストは30分で交代し、全員がタイピストを経験するようにしました。 私自身、他のメンバーよりJava歴が浅くて少し不安な気持ちもありました。しかし、タイピストは基本的にモブの指示に従って手を動かすため、実は自信のない人程タイピストに向いていると実感しました。 モブは積極的に意見を出し、モブプログラミングに貢献することが重要です。 優しくタイピストを導き、タイピストのレベルによってはtypoや大文字・小文字の指定などソースコードの表記に関する内容から、具体的な各行の処理の内容まで具体的に指示してあげましょう。 プログラミング初心者の場合、意見を言いづらいかもしれませんが、typoやより良い英単語の提案、過去に自分が他のモブから受けた指摘など、貢献できることはいろいろとあります。 また、一度も意見が出ていないモブのメンバーに気づいたら、何か意見がないか尋ねてみましょう。 タイピスト交代時の工夫 タイピストが交代するタイミングで、モブプログラミング用に用意したGitHubのブランチへソースコードをpushし、次のタイピストは同ブランチからpullしてその続きからコーディングを行いました。 また、交代の際にはGoogle Meetのカメラと音声をオフにして5分か10分の休憩を挟みます。時間が惜しい気持ちもありますが、集中しっぱなし・喋りっぱなしで疲れた脳では余裕もなくなり、議論やコードの質に悪影響を及ぼします。少しでもリフレッシュさせ、良い状態を維持することが大切です。 解決できない問題が発生した場合の対処法 モブプログラミングを行っていると、その場ですぐに解決できない問題が発生することもあります。 調査が必要な場合は、該当箇所以外の進められる部分を時間内に進め、期限と担当者を決めて別途調査しました。 期限内に解決が難しそうな場合は手が空いているメンバーがヘルプにつくという形式で取り組みました。最終的に全員が調査に加わった事もありました。 問題が他のメンバーによって解決された時は素直に感謝するのと同時に、自分事の様に嬉しく感じたのを覚えています。 API実装 最後にAPI実装のフェーズです。 土台となるAPIは上記のモブプログラミング形式で実装し、残りのAPIはメンバーで手分けして各自で実装しました。 全ての処理をひとりで実装するため、モブプログラミングで出てきたポイントをここで復習できました。 モブプログラミングの時間だけでは新しく導入したツール・ライブラリの全てを理解することは難しいので、ドキュメントを確認しながらの実装になります。しかし、根底の認識がモブプログラミングによって合っているため、実装に迷うことがなく、全員から上がってきたプルリクエストはある程度の統一感を保てていました。 些細な指摘はありましたが、プルリクエストのレビューで指摘をもらうことで品質を保つことができました。 モブプログラミングによる効果 実際にモブプログラミングを導入することで得られた効果を紹介します。 技術力の底上げ ツール・ライブラリの理解度だけではなく、それぞれのプログラミング知識を吸収できるなどの学びが多く、メンバー全員の技術力の底上げに繋がりました。 終わりのない議論がなくなる モブプログラミング実施中、終わりのない議論がありませんでした。 議論が長引いたことはありますが、意見が割れた場合、誰かが助け舟を出したり、より良い意見を出しあうことで解決に向かうことができました。 これは全員が共通の目標を持っており、相手の話に耳を傾け、理解しようとするマインドを持っていたためです。このようなマインドは心理的安全性を高め、忌憚のない意見も発言しやすく受け入れやすい環境を作るのでとても大切です。 一体感の醸成 全員が協力してモブプログラミングを行っていくうちに、不思議と一体感が生まれてきました。 実はコロナ禍のため実際に会った事も、あまり一緒に働いた事もないメンバーもいましたが、今まで知らなかったそれぞれの得意分野や、こだわり、これまで経験した苦労話など、その人を知ることにより、お互いリスペクトできる部分が生まれたのです。 一緒にコーディングをしただけで飲み会以上の効果があったのではと感じています。 安心して休める環境の構築 全員が全ての仕組みを理解している状態になったため、誰かが休むと困るということがなくなりました。 実際、全員が年末休暇を1週間ずつ交代で取得し、安心して家族と過ごすことができました。全てを他のメンバーに任せ、Slackを見る必要のない休暇からは本当の休息感を得られます。 反省点 次に活かしたい反省が3点あります。 タイピストの順番変更 タイピストの順番を変更しなかったため、2本のAPI実装で同じ人が同じレイヤーを2回とも実装することになってしまいました。 実際に手を動かすことで理解が深まり、見ているだけでは気づけないポイントもあるため、2回目は違うレイヤーに当たるよう順番を変えるべきでした。 タイマーの導入 つい熱中してしまい、気づいたら時間が超過していることがあったため、タイマーを設置するなど工夫が必要でした。前述の内容ですが、集中力維持や良い状態を維持するめには休憩を挟むことが重要です。 また、時間には上限があるため、一人のタイピストの時間が長くなることで、他のメンバーのタイピストの時間が短くなってしまいます。 時間の確保 先にも挙げましたが、ミーティングが多くまとまった時間の確保に苦労しました。 これは現在、定例などのミーティングを決まった曜日にまとめる動きがあるので、今後は時間を確保しやすくなりそうです。 まとめ ソースコードの質やチームワーク、有休消化率までアップさせるモブプログラミング。素晴らしいです。 今回はある程度Javaに慣れているチームで未知のものを作る際にモブプログラミングを活用した事例をご紹介しました。他にもこの経験からJava未経験チームへの引き継ぎにも活用したりと、活用の場を広げています。 最後に ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
アバター
こんにちは。ZOZO研究所の山﨑です。 ZOZO研究所では、検索クエリのサジェスト(以下、サジェスト)や検索後のアイテムの並び順といったZOZOTOWNでの検索改善にも取り組んでいます。 本記事では、ZOZOTOWNにおける実例を交えながら、サジェストの改善方針についてご説明します。 目次 目次 一般的なサジェストの概要 サジェストの分類 サジェストの評価指標 ZOZOTOWNでのサジェストの改善 サジェスト改善のサイクル 1. サジェスト改善方針の仮説 2. KPIの策定 3. サジェストの改善施策 4. ABテストの実施 まとめと今後の改善案 おわりに 一般的なサジェストの概要 はじめに、一般的なサジェストの分類や評価指標を説明します。 サジェストの分類 サジェストとは、検索窓にキーワードが入力された際に関連するクエリを表示する機能を指します。また、本記事ではサジェストに候補として表れるクエリをサジェストクエリと呼びます。 さらにサジェストは、 A Survey of Query Auto Completion in Information Retrieval では、Query Auto CompletionとQuery Suggestionに分類されています。 名称 イメージ図(画像は上記論文より引用) 入力 出力 Query Auto Completion クエリのprefix prefixから始まるN件のクエリのリスト Query Suggestion クエリ 入力クエリに関するN件のクエリのリスト Query Auto Completionは新たな検索クエリを提案することではなく、ユーザーの検索を完了させることを目的としています。Query Auto CompletionとQuery Suggestionのどちらを採用するべきかは、サービスの目的などによって異なります。 しかし、改善には同じ指標を使うことができます。 サジェストの評価指標 サジェストの良し悪しを測る指標は、以下のように色々なものが提案されています。 名称 概要説明 Impression Rate 入力クエリに対してサジェストが表示される割合です。 Click Through Rate(pSaved) サジェストクエリを選択した確率です。 オンラインのサジェストの評価指標としてよく用いられます。 Null Search Hit Rate(NSR) サジェストクリック後、商品数が0件となる検索結果に飛ばした割合です。 0件ヒットページに飛ばすことはユーザー体験に悪影響を及ぼすため、できるだけ下げることが望ましいとされている指標です。 Minimal Keystrokes(MKS) サジェストクエリを選択するまでの最小のキーストローク数です。 キーストローク数が少ない状態でサジェストをクリックする方がユーザビリティ向上に繋がったとみなせます。 eSaved CTRに「サジェストを選択することで省略することができたキーストローク数」ベースの重みをかけ合わせた指標です。 CTRとMKSを組み合わせたような指標で、多くの文字列を省略できて、かつそのクエリが多くクリックされることを良しとした指標です。 ランキングベース指標 実際に選択されたサジェストがサジェスト一覧の何番目に表示されていたかをベースに判断する指標で、順位の逆数を表すReciprocal Rank(RR)などがあります。 複数のクエリについてRRベースの結果を取得するときは平均値を取ったMRRやprefix毎に重みを変えたweighted MRRなどが存在します。 Diversity サジェストの多様性を示す指標です。 例えば、 Diversifying Query Suggestion Results のようにサジェスト選択後クリックされたページの異なり具合を測る指標などが存在します。 このように様々な指標が提案されているため、各サービスの課題とシステム制約の観点から使用可能な評価指標を選択する必要があります。 次章では、ZOZOTOWNのサジェストの課題解決のために、上記指標をどのように選択して改善したかについて説明します。 ZOZOTOWNでのサジェストの改善 本記事で説明するサジェストは、現在PCとスマートフォンのWeb版の ZOZOTOWN でのみ有効となっています。 サジェスト改善のサイクル 以下の4ステップのサイクルを何度か回して、サジェストを改善しました。 サジェスト改善方針の仮説をたてる 課題点や改善方針から目標とするKPIを策定する KPIが向上する改善を行い、定量評価と定性評価する ABテストでのKPIとGMVを計測し、改善の効果を確認する 次節からは各ステップの進め方を説明します。 1. サジェスト改善方針の仮説 サジェストはユーザーの意図しているクエリを補完することを目的としています。従来のZOZOTOWNでのサジェストは、ブランド名、ショップ名、カテゴリ名といった情報のみから作成されていました。そのためユーザーの意図したクエリの補完に対応できていないという課題感がありました。 この課題感を明確にするため、まずは、具体的にクエリの結果を眺めることで課題点を発見していきました。 クエリ例 2020/06/11時点のサジェストの結果 課題点 改善方針 計測指標 ブレザー サジェストクエリが表示されない サジェストがカバーするクエリを増やす Impression Rate サンダル 入力したクエリが、サジェストのどの部分に対応しているかが分かりにくい Query Auto Completionの対応を行う CTR パーカー 「在庫なし含む パーカー」などのクエリが検索結果が0件になる 検索結果が0件となるサジェストクエリを削除する NSR 2. KPIの策定 以上の調査結果とシステム制約などから、以下の2つをKPIとしてサジェスト改善の取り組みを進めました。 サジェストのクリック率(CTR)を増やす 0件ヒットするサジェストの数(NSR)を減らす サジェスト経由のCVR(商品購入率やお気に入り追加率など)も検討しましたが、サジェスト以外の影響を多分に受けるためKPIとしての採用は見送りました。 3. サジェストの改善施策 ZOZOTOWNでは、サジェストの検索システムとしてElasticsearchを採用しています。Elasticsearchの取り組みについては、 こちらの記事 をご覧ください。 techblog.zozo.com また、Elasticsearchのデフォルトのサジェスト機能は日本語との相性が悪いため、通常の検索クエリを使用して実装しました。こちらについては、 Elasticsearch公式のブログ記事 で詳しく言及されています。 www.elastic.co Elasticsearchでのクエリとドキュメントのマッチングに用いるデフォルトスコアは、検索クエリとドキュメントのBM25などをベースに計算されています。詳しくは Elasticsearchの公式の解説 をご覧ください。 www.elastic.co 今回デフォルトのスコアを使用せずに、過去のサジェストのクリック率やクリック後の商品の購入率などをベースにした重みを使用するように修正しました。 また、上述した「サンダル」の例のように、従来のサジェストでは入力クエリとサジェストクエリの対応が分かりにくいという課題がありました。その課題の改善策としてQuery Auto Completionを採用して、prefix matchするように変更しました。 さらに、過去に何度か検索結果が0件となる場合のサジェストクエリは結果から除去することでNSRの改善を目指しました。 まずはオフラインでの定量評価を以下の手順で行いました。 ある期間の過去ログを使用して、サジェストを作成する サジェスト作成で使用したログに後続する期間のログを使用したテストデータを作成する テストデータに含まれる検索キーワードの内、サジェストクエリと一致した割合を求める オフラインでのCTRとする 上記と同様のログを使用して、サジェストの遷移先のURLの内商品数が0件となる割合を求める オフラインでのNSRとする オフラインでの定量評価で改善が見込めることを確認した後に、オフラインでの定性評価を進めました。 専用のツールを作成し、上記の改善を施したサジェストと既存のサジェストの定性的な比較実験を行いました。 被験者は社内で募集した47人、485件のクエリに対して回答が得られ、改善を施したサジェストの方が約76%のクエリで良いという結果が得られました。 オフラインでの定量評価と定性評価の結果を踏まえて改修と評価を繰り返しました。 4. ABテストの実施 サジェストの改善の結果、CTRは約20%向上、NSRは約50%低下しました。KPIの他にも、ABテスト実施による売上の低下などGMVにも影響がないかをウォッチしつつ進めました。 予めテストを中断するための基準や、リリース実施可否を判断するための基準を設けておくことが大切です。 まとめと今後の改善案 本記事では一般的なサジェスト改善に用いられる指標の説明とZOZOTOWNでのサジェスト改善の取り組みについて説明しました。 サジェストの改善を進める上で、さらなる改善施策も見えてきました。例えば現状のサジェストでは多様性の計測は行っているものの、多様性を最適化する仕組みを取り込むことはできていません。今後は多様性を考慮したサジェストの改善も検討しています。 おわりに ZOZO研究所ではMLエンジニア、バックエンドエンジニアのメンバーを募集しております。今回紹介した検索技術に興味ある方はもちろん、幅広い分野で一緒に研究/開発を進めていけるメンバーも募集しています。 ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co hrmos.co
アバター
はじめに こんにちは、計測プラットフォーム部の歌代です。 普段はZOZOSUITやZOZOMATといった計測系プロダクトの「計測」に関わる部分の検証や、新規プロダクトに必要なデータ集め、また精度検証などサービス構築から、UI/UXの分析・評価など幅広く業務を行っています。 2020年2月にリリースした足計測ツール、ZOZOMATでは足のサイズをスマートフォンで測るという新しい体験をサービスとして提供開始しました。新たな体験にも関わらず、事前に工夫をすることで、使い方に関するユーザーからの問い合わせ件数を大きく抑えることに成功しました。 今回はZOZOMATリリースの際に、問い合わせ数削減につなげるために行った工夫や事前調査の内容を紹介します。 ZOZOMATとは 既に他のテックブログの記事でも紹介していますが、ZOZOMATは足のサイズを計測するために開発された足専用の計測マットです。 計測者自身のスマートフォンとZOZOMATを使って計測した情報を元に、靴の推奨サイズを提案するサービスを利用可能です。まだお試しいただけていない方、ご興味のある方は無料で配布中なのでぜひこちらをご確認ください。 zozo.jp 2021年1月末現在、ZOZOMATの計測件数は130万件以上あるのに対し、問い合わせ件数は数十件程度に留まっています。問い合わせ発生率としては極めて少なく、その中でもほとんど開発チームが関わる必要のある問い合わせがありませんでした。 ZOZOMATでは、どのようにして問い合わせ件数を抑え込むことができたのでしょうか。実施した内容は至ってシンプル、そしてやって当然な内容も多々ありますが、「事前にやっておいてよかった」と思うものがいくつもあります。ここではそれらの中からいくつかピックアップしてご紹介します。 ZOZOMAT開発時の工夫点 UI/UX改善を開発初期から行える体制を構築する 元々、私は品質管理部に在籍していました。そこでZOZOSUITの精度検証や端末疎通チェックをしていました。 ZOZOSUITを検証する際、すでに実装まで完了したUI/UXに対し、調査を実施し、その過程で「ここがわかりにくい」「ここが伝わりにくい」という情報を把握していました。社内で最も多くZOZOSUITの計測を経験したのは間違いなく私だと思います。 ZOZOSUITの精度検証の過程で気がついた点を、開発チームやUI/UXを担当するデザイナー、サービスチームにフィードバックしていたところ、現在の上司から「サービスを開発する段階からUI/UXについて意見をもらえないか」と声をかけられ、現在のチームに異動することになりました。 ZOZOSUITの計測を多く経験した人の声を、ZOZOMATのデザインや開発にいち早く反映させるために、チーム構成から変更されました。これにより、より良いUI/UXのためのアプローチをしやすい体制になりました。 ユーザビリティテストで問題点を見つける ユーザビリティテストは、読者の中にも実施されている方もいらっしゃるでしょう。一言で表すと「サービスやアプリの使い勝手の部分を評価するテスト」です。 「サービスを使って目的を果たせているか?」「チュートリアル動画はわかりやすいか?」「エラーメッセージでユーザーの不安を払拭できるか?」などを、社内スタッフや外部被験者を使って、評価・テストを行います。 ユーザビリティテストは、突き詰めていくとユーザビリティテストを専門に行う会社ができるほど、奥深いものです。しかし、簡易的なものでも十分効果があります。ユーザビリティテストだけではありませんが、テストはやった分だけ何かしらの成果・結果が伴ってきます。その中でも、リリース前のユーザビリティテストはその効果が大きいテストです。 ユーザビリティテストの目的は「使い勝手に影響を与える大きな問題(大勢の人間がよく間違う箇所)」を見つけることです。例えば10人テストして、10人がそれぞれ違うミスをしていた場合、それは被験者個別の問題であって、サービス・アプリの問題ではない可能性が高いです。しかし、逆に10人テストして、8人が同じ場所で同じミスをしていた場合、それはサービス・アプリのその該当箇所に問題があると言えます。 開発の試作段階のものを評価したい際や事業の方向性に関わる重要な決定する際には、サービスの詳細を知らされていない社内スタッフに依頼、ユーザビリティテストを実施し、スピーディーに問題点を洗い出し、サービスチームや開発、デザイナーにフィードバックします。すべて社内で完結させることにより、早いサイクルで実施できます。 ここでのポイントは、開発が完了したものに対して、ユーザビリティテストを実施するのではなく、開発途中の段階でユーザビリティテストを実施することです。開発終盤での修正は大きな手戻りが発生するため、リソース・時間的に追い込まれ、精神的な負担も大きくなります。その負担が発生しないよう、ZOZOMATプロジェクトでは開発と並行してユーザビリティテストを実施しました。開発やサービスの方向性の変更や決断を容易に行うことができ、サービス品質の向上に大きく寄与しました。 それでは実際にZOZOMATで行ったユーザビリティテストを例に、ポイントを説明します。 ユーザビリティテスト実施時のポイント UI/UXの工夫 実際にZOZOMATではユーザビリティテストを使った検証をいくつも実施しました。その中でも開発当初には「UI/UX」に関するユーザビリティテストを行いました。 ZOZOMATは開発当初、2種類のUI/UXデザイン案があり、どちらにするべきかサービスチームを中心に議論が行われていました。 開発担当、サービス担当、デザイン担当と議論した結果、2種のデザインはそれぞれに良さがあり、またそれぞれに弱点があるように思えました。 そこで社内と社外の両方から被験者を招き、テストの目的の詳細を説明しないまま、計測成功率を調査しました。 A案:6方向から足をスキャンする際、スマホ画面を計測するエリアの色に着色して、計測方向の指示を出すもの B案:スキャンの際、スマートフォンの画面上に、ZOZOMATの緑色の足形を表示させ、実際のZOZOMATの緑色の足形と重ねるもの 被験者は20〜30代、40〜50代、60代以上の3グループで男女5名ずつ、合計30人を集めました。そして、A案→B案の順でテストを実施するAチームと、B案→A案の順でテストを実施するBチームに分け、両方のUI/UXを試してもらい、成功率の比較とどちらがわかりやすかったかのインタビューをしました。 結果として、B案に比べA案の方が成功率が高く、またユーザーインタビューからもA案の方がわかりやすいという意見があり、ZOZOMATはA案のUI/UXを採用することになりました。 A案はあまり難しいことを考えずに、計測する足の周りを6方向からスキャンするという直感的でわかりやすいという特徴がありました。その結果、B案をブラッシュアップしてコストをかけるのではなく、A案にリソースを集中させることができました。開発の早い段階で、より効率の良い選択肢を選べました。その結果、リソースを有効活用してサービスの作り込みをすることにより、問い合わせ発生率を大幅に低く抑えることができました。 ユーザビリティテストは、問題点を見つけるだけでなく、サービスやUI/UXの方針を客観的に評価でき、リリース後のサービスの姿を先に垣間見ることができます。そのため、入念にユーザビリティテストを実施することは、非常に重要だと言えます。 Androidの複数端末疎通チェックによる動作確認 ZOZOTOWNのアプリはiOSとAndroidの両方で提供しています。そして、ZOZOMATはそのアプリ内で利用できます。 開発の流れは、まず初めに端末モデルに機能や仕様に差の少ないiOSのアプリを中心に組み立てられ、その次に同じ仕様をAndroidにコンバートしていきます。 iOSではほとんど端末モデル間に差分がありませんが、Androidは国内外の様々なメーカーで製造されているため、機種によるスペックの違いや設定の違いなどが顕著です。そのため、アプリに対して、どのような影響・違いがあるのかを確認するテストが必要となり、それをAndroid複数端末疎通チェックと呼んで実施しています。 ZOZOTOWNでよく利用されるAndroid端末を社内検証機として一部保有していますが、世の中の全種類を網羅することは現実味がありません。不足する部分は外部の検証機関の協力も得ながら、全アクセスの95〜99%をカバーできるよう、端末検証を実施しています。 ZOZOMATのテストを実施していると、一部の機種でカメラの挙動の影響により開発の仕様とは異なるスキャン結果が返され、うまく計測できないということがリリース前に発覚しました。このテストをリリース前に実施することで、想定通りの動作をしない端末が存在することを把握し、そのようなアプリが対応しきれない機種がある場合はヘルプやお知らせなどでユーザーに周知することができます。改修コスト次第では、テストの結果を受け、リリースまでに原因調査と改修をすることも可能です。 また、Androidはモデルにより端末のスペック差が大きく、計測はできても、足の計測にかかる計算時に想定以上の時間を要する場合もあります。そのような端末についても事前にテストをして把握しておくことで、ヘルプなどにその旨を記載できるほか、サポートデスクへ事前に情報共有をし、いざ問い合わせがあった場合でも、その情報を元に適切な対処法を案内できます。 このようなテストを時間を確保して実施した結果、リリース前に問題が発生しそうな端末に関する情報を得ることができ、リリース後に大きな混乱や慌ただしい追加調査が必要となることがありませんでした。 エラーメッセージの整理 ZOZOMATの開発時に、UI/UXに次いで議論したテーマが「音声メッセージ」「エラーメッセージ」の伝え方です。 ZOZOMATの計測は、ユーザーがスマートフォンを片手で持った状態で、計測する足の周囲を6箇所、膝の高さから、スマートフォンをやや斜めに向けてスキャンしてもらいます。 上記の説明を読んでもなかなか実際の光景をイメージすることは難しいかと思います。実際の計測時に、この「姿勢とスマートフォンの角度と距離」を正確に伝えることにとても苦戦していました。 ZOZOMATの注文画面に掲載しているイメージ映像や、アプリ起動後のチュートリアル動画で、実際の計測方法を動画で伝えています。 しかし、実際にユーザーが計測する際には以下の2つの想定と異なった計測をしてしまうことが想定されました。 スマートフォンを横向きに持ってスキャンしてしまう スマートフォンをマットに対して水平に持ってしまう ZOZOMATのUI/UXの特徴として、ZOZOSUITの計測時でも利用していた「音声案内による正しい計測姿勢への誘導」があげられます。「スキャン位置が遠すぎる/近すぎる」のようなエラーは検出も容易で、かつ音声メッセージでの修正が可能でした。 しかし、「横持ち」や「水平持ち」はシステムによるエラー検出が難しく、また音声案内で正しい持ち方を案内しても、テストをしてみると「ユーザーは案内されている通りの正しい持ち方をしてるつもりであり、どこが間違えているのかイメージが湧かない」という理由で、音声案内がより混乱を招くという事態に陥っていました。 そこでポップアップによる「正しい計測姿勢の案内」を特定の間違いを連続で検出した際に表示する仕組みを実験してみました。チュートリアル動画で案内した内容をもう一度、ポップアップ画面で案内するという仕様にしましたが、それでも「何が間違っているのかわからない」というテスト結果でした。人間は一度思い込むとなかなかそれを修正することが難しいということを実感させられました。 ポップアップでは、正しい計測姿勢や持ち方を案内した後、最もよく発生する「横持ち」と「水平持ち」ではなく、正しいスマートフォンの持ち方や角度を改めてユーザーに静止画で提示する仕様に切り替えました。これにより、足に対してスマートフォンは「縦向き」「少し斜めにしてスキャンする」ことが、ようやくほぼすべてのユーザーに伝わるようになりました。 エラーメッセージの伝え方はとても難しいものです。すべての発生し得る行動を予測し、丁寧に説明していると、過剰な説明によってユーザーに余計な負担をかける事になります。「アプリとしての機能や使い方を適切に伝えつつも、説明は最低限にとどめておき、直感的でわかりやすいUI/UX」が理想的です。 最後に 本記事で紹介した内容は、特別なスキルや経験を必要とするものではなく、どのサービスでも導入できるものです。もちろんスキルを身につけることによって精度が向上していきますが、第一歩として導入するだけでも効果が得られます。テストは、開発やシステム構築が完成した最後の工程と思われがちですが、開発と並行してテストを実施することで、様々なメリットがあることが今回のZOZOMATでの検証を通して体験できました。 目的とスピード感を持って検証に臨めば、開発やデザインの手戻りといった余計な工数を削減でき、同じ開発期間であってもさらに効率的・効果的にサービスを磨き上げることができます。 皆様も現在開発中の案件があれば、開発と並行したユーザビリティテストを実施してみてはいかがでしょうか。 ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
アバター
はじめに こんにちは、ZOZO研究所の平川( @china_syuke )です。 ZOZO研究所では今年度から、ファッションコーディネートアプリ「 WEAR (ウェア)」のデータを用いた調査リリースを執筆しています。一般的によく見るアンケート調査と違い、機械学習を用いてこれまで数値化されていない情報を調査しました。 この記事では、リリースした中でも面白いアプローチで調査した、第二弾「洋服の「丈」に関する流行の変化」に焦点を当てながら調査リリースの進め方・工夫したこと・課題に感じたことを紹介します。 https://press-tech.zozo.com/entry/20200820_WEAR_Research2 press-tech.zozo.com 目次 はじめに 目次 機械学習を用いた調査リリースの執筆工程 保有しているデータから仮説を立てる 統計処理のために数値化すべき項目を検討する 数値化するための機械学習の手法を検討し、仮説を立証するための筋道を立てる ターゲットの読者に伝わりやすい文章へ変換する 調査リリースの醍醐味 調査リリースの課題 短期間で調査するための道具の整備が必要 思い通りの結果にならない 仮説の質を上げる まとめ さいごに 機械学習を用いた調査リリースの執筆工程 最近では様々な企業が自社でのアンケートや蓄積されたデータを使用した調査リリースを出しています。サービスなどで蓄積されたデータを解析していくことは、消費者の動向や社会情勢を分析していく上でもとても意義のある調査です。また、自社サービスの宣伝や保有しているデータのアピールにも繋がります。 今回の記事では以下に注目して順を追って執筆の工程を説明していきます。 保有しているデータから仮説を立てる 統計処理のために数値化すべき項目を検討する 数値化するための機械学習の手法を検討し、仮説を立証するための筋道を立てる ターゲットの読者に伝わりやすい文章へ変換する 保有しているデータから仮説を立てる 通常の調査リリースでは、アンケートを調査対象として仮説を立てていくことが多いです。今回の調査リリースではアンケートは集計せずに、自社サービスであるWEARに投稿されたコーディネート画像を利用して仮説を立てていきます。 WEARに投稿されたコーディネート画像から近年どのようにファッションが変化しているか仮説を立てる 少量のサンプリングした画像データで仮説立てした傾向を確認していく 何か面白い変化が出そうであればもっと深堀していく もちろん立てた仮説が上手く立証できることは少なく、1と2の手順を繰り返し行いブラッシュアップしていきます。 今回の例で言うと、「最近はトップス短め/ボトムス長めの傾向がある」という仮説のもと小規模のデータでどのような傾向が出るか確認をしていきます。そして、何か面白いものが見えてきそうであれば、実際にデータの範囲を広げより細かな調査をしていきます。 統計処理のために数値化すべき項目を検討する アンケート調査の場合はすでに数値として結果が出ているため、統計データとして扱って執筆が行えます。画像データの場合はそのままでは数値として扱うことができません。統計による集計をするためにまずは「画像データから何を数値化したら調査を行えるのか」を検討します。 今回は「コーディネート画像上でのトップスとボトムスの比率」を知ることができれば良いので、それを得るための解き方を考えていきます。 数値化するための機械学習の手法を検討し、仮説を立証するための筋道を立てる 画像データを数値化するための機械学習の手法を検討していきます。今回の例で言うと、以下のような仮説立証への筋道が考えられます。 トップスとボトムスの画像上の比率を知る → 洋服の領域検出でトップスとボトムスの矩形を取得 画像上での被写体の身長を知る → 骨格検出で画像上の各部位の長さを定義 1,2より、画像上での身長に対するトップスとボトムスの比率を算出 ここで、「なぜ実際の商品サイズや検出された矩形の長さをそのまま扱うのではなく骨格検出などを用いて比率を出したのか?」という疑問が生じます。コーディネート画像から洋服のサイズを検出する際には以下のような状況が考えられます。 ポーズの影響を受ける タックインなどで本来の丈の長さより短く着こなしている場合がある 被写体の身長・撮影位置は一定ではないので検出された矩形だけでは比較できない これらを解決するために骨格検出を採用しました。 このように、1つの調査に対して様々な手法を組み合わせて問題を解いていきます。 ターゲットの読者に伝わりやすい文章へ変換する アンケート調査と違い機械学習を用いた調査は、調査工程が複雑になります。調査リリースの読者ターゲットは幅広く、弊社の場合はファッション関係のリリースのため技術系の読者でないことも想定されます。いかに前提知識のない読者に対して分かりやすく伝えるかが重要になってきます。 上図で示したように画像を用いて視覚的に分かりやすく説明するなど「可視化」を意識して記事を書くよう心がけました。 調査リリースの醍醐味 調査リリースの醍醐味は「目には見えないファッションの流行を数値化し、複雑な内容を分かりやすく読者に伝えられる」ことです。 今まで街角などで「こういうコーディネートや色が増えてきたなぁ」と感覚として感じていたものが実際に数値として出てファッションの動向も観測できました。機械学習をファッションと繋げてイメージしやすく伝えることで、難しく感じる機械学習という分野がより親しみやすくなり興味を持つきっかけになることを感じました。また、ファッションの動向は社会の動向にも密接に関係していると言われます。調査で出た数値が社会とどのような関係を持っているのかを読み解いていく過程も醍醐味の1つと言えます。 調査リリースの課題 これまでに調査リリースを3本出せましたが様々な課題が見つかりました。 短期間で調査するための道具の整備が必要 四半期に1回のリリースを目標としていたため、仮説立てからリリースまでのスケジュールが3か月程度でした。 調査リリースごとに仮説が異なるため、使用する機械学習の手法も異なります。多様な手法を取り入れることは、それぞれに対して調査が必要になるためスケジュールの遅延が発生しやすいです。そのため、様々な手法を短期間で扱えるよう機械学習のライブラリを準備しておく必要があります。また、画像の背景除去に使用するセマンティックセグメンテーションなどの前処理は非常に時間がかかります。そのような処理を高速化できる環境の整備をしていく必要があります。 思い通りの結果にならない これはどの調査リリースにも言えるのですが、仮説が必ずしも正しいとは限りません。 今回で言うと「最近はトップス短め/ボトムス長めの傾向がある」という仮説に対し、「トップス短め/長めの二極化している」という結果が得られました。もし得られた結果からうまくストーリーが立てられないと調査期間を延ばす、もしくは仮説自体を変更するという選択をしないといけません。 このようなリスクを回避するために初めから仮説を複数用意しておくか、下記の「 仮説の質を上げる 」ことが必要になってきます。 仮説の質を上げる 結論を言うと、調査リリースのキモは仮説の質を上げるに尽きます。 今回の調査で得られた、年毎のトップス丈の割合推移の図を紹介します。 図. トップスが短め/長めの二極化した結果を表すグラフ 年毎にグラフの山が左へ移動しトップス丈が短くなることを予想しましたが、実際はその山が徐々に二極化していくという面白い様子が観測できます。 当初は「トップス短め/ボトムス長めの傾向」という仮説でした。調査の工程で単純な商品サイズのデータではなく、画像から読み取れるコーディネートの比率に焦点を当て、コーディネートの傾向も考慮するように仮説の質を高めていきました。このように当初の仮説の質を上げることはストーリーの肉付けに繋がり、より面白いストーリーを構築できます。機械学習を用いた調査リリースは、調査を細分化する傾向があるので仮説の質を上げやすいと感じました。 仮説を細分化して質を上げると、仮説を複数検討しながら調査できるので、結果的に仮説の立て直しの手戻りを減らすことが期待できます。 まとめ 通常のアンケートベースの調査リリースと違い、機械学習を用いて数値化されていないデータを調査することは調査の難易度が上がります。機械学習を使用することで調査スケジュールは不安定になり、アンケートベース以上の課題が生まれました。しかし、数値のみのデータでは観測できないより深い調査が行えるため、仮説自体も質が高くなり面白いストーリーに仕上げることができると感じました。 この取り組みを通して研究所が何を行っているかをよりライトに発信できる仕組み作りができました。運用フローの課題を改善しつつ、ファッションの動向を素早く感知できるようなプラットフォームを開発して、より読者に興味を持ってもらえる調査リリースを出していきたいです。 今回取り上げた記事の他にも調査リリースを出しているので読んでみてください。 https://press-tech.zozo.com/entry/20200610_WEAR_Resarch1 press-tech.zozo.com https://press-tech.zozo.com/entry/20201218_WEARsearch3 press-tech.zozo.com さいごに ファッションの動向は社会・経済・様々なことに密接に関係し変化していきます。ZOZO研究所には様々な観点からファッションに関する謎を解明する環境が整っております。 ZOZO研究所ではMLエンジニア、バックエンドエンジニアのメンバーを募集しております。今回紹介した調査リリースもまだまだ始まったばかりの取り組みで、一緒に調査していただけるメンバーを募集しております。ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co https://hrmos.co/pages/zozo/jobs/0000029 hrmos.co
アバター
こんにちは。ECプラットフォーム部データエンジニアの遠藤です。現在、私は推薦基盤チームに所属して、データ集計基盤の運用やDMP・広告まわりのデータエンジニアリングなどに従事しています。 以前、私たちのチームではクエリ管理に Looker を導入することで、データガバナンスを効かせたデータ集計基盤を実現しました。詳細は、以前紹介したデータ集計基盤については以下の過去記事をご覧ください。 techblog.zozo.com 本記事では、データ集計基盤に「データバリデーション」の機能を加えて常に正確なデータ集計を行えるように改良する手段をお伝えします。 データバリデーションとは バリデーション導入後のデータ集計基盤 ジョブネット構築 テンプレートによる効率的なDAGの作成 DAG間の依存関係の設定方法 バリデーションDAGのタスク構成 まとめ データバリデーションとは データバリデーションとはデータの安全性・妥当性を確認・検証することです。データバリデーションは以下の視点から検証することが一般的です。 データ型:型(Int・Stringなど)として妥当なデータか データ形式:データの形式(「空値が許されるか?」「指定された範囲内の値であるか?」など)として妥当なデータか ビジネスロジック:開発時に定めた集計定義的に妥当なデータか 今回は上記に示された3つの観点から、絶えず流れてくるデータが集計仕様どおりになっているかを検証することでデータ集計における正確性向上へのさらなる改善を図ります。 RDBではテーブルの各カラムにおけるデータの型があらかじめ定められているので、「データ型」における妥当性はデータ集計開始時には既にクリアしています。一方、「データ形式」・「ビジネスロジック」における妥当性は依然不明であり、データ集計前に何らかの方法で検証することが必要です。 バリデーション導入後のデータ集計基盤 データマートの更新は、GCPのApache AirflowマネージドサービスであるCloud Composerを用いたデータ集計基盤を構築することで実現しています。Apache AirflowはPythonで定義したワークフローをスケジュール・モニタリングするためのプラットフォームです。 なお、Cloud Composer・Apache Airflowの詳しい説明はここでは省きます。 Cloud Composer公式サイト ・ Airflow公式サイト をそれぞれご覧ください。 まず、BigQueryへのデータ取り込み完了直後にCloud Pub/SubをKickして、Cloud Functions経由でデータ集計基盤を起動させます。 従来は起動後すぐにデータマートを更新していましたが、今回はデータマート更新で用いるデータ群が全て妥当なデータであることを確認してからデータマートを更新するようにします。以下の図は、データ取り込み完了後からCloud Composer内の処理までを、データバリデーション導入前後で比較したものです。 データバリデーション導入前後のフローを比較すると、導入後はジョブネット構築・バリデーションが主に追加されています。これらを次項で説明します。 ジョブネット構築 Cloud Composerでは、個々の処理をタスクと呼び、タスクに名前をつけ処理内容を記述します。そして、それらのタスクの依存関係をDAG(有向非巡回グラフ)で定義することによりワークフローを構築します。DAGの作成方法の詳細は Cloud Composer公式ページ内の「DAG(ワークフロー)の作成」 をご覧ください。 基本的にCloud Composerにおけるワークフローは1つのDAG内で定義します。しかし、ワークフローの規模が大きくなるにつれてタスクの依存関係が複雑になりDAGが肥大化するといった管理面でのデメリットが発生します。 DAGの肥大化を防ぐため、ワークフローを複数のDAGを組み合わせたジョブネットとして定義するように構築します。なお、ジョブネットとは一般的には実行順序を指定した1つ以上のジョブの集まりのことを指します。 ジョブネットでは2種類のDAGを作成します。 バリデーションDAG:バリデーションのタスクを行うDAG。バリデーション項目数と同じ数だけ作成される。 データマート更新DAG:データマート更新のタスクを行うDAG。依存するバリデーションDAG全てが正常終了しなければ起動しないようにする。更新するデータマート数と同じ数だけ作成される。 バリデーションDAGとデータマート更新DAG間の依存関係はワークフロー内で自動的に設定するようにします。ワークフロー全体の最初のステップとして、この依存関係を把握してジョブネットを構築します。 具体的に言うと、バリデーション・データマート更新でそれぞれ使用する全クエリを取得してクエリ構文を解析します。以下の図のように、データマート更新クエリから集計元テーブルを割り出し、その集計元テーブルと各バリデーションのクエリで用いるテーブルを一致させることで依存するDAG同士を結びつけていきます。 このように、クエリ解析で得られたDAG間の依存関係データはCloud Composer環境にカスタムプラグインとしてインストールすることで、実行順序が動的に変化するジョブネットを構築しました。 さて、Cloud Composerでジョブネットを構築するにあたり、以下の工夫した2点について解説します。 テンプレートによる効率的なDAGの作成 DAG間の依存関係の設定方法 テンプレートによる効率的なDAGの作成 バリデーションDAG・データマート更新DAGは関数でDAGのテンプレートをそれぞれ用意して、引数に任意の値を渡すことでDAGを作成します。例えば、バリデーションDAGにおけるコーディングは以下のようになります。 from airflow.models import DAG def validation_dag (validation_info): dag_id = "validation_" + validation_info[ 'validation_id' ] dag = DAG(dag_id, default_args=default_args, schedule_interval= None , catchup= False ) # 中略 return dag for validation_info in validation_config: validation_id = validation_info[ 'validation_id' ] globals ()[validation_id] = validation_dag(validation_info) バリデーションDAGのテンプレートを関数 validation_dag で作成します。なお、引数はバリデーションの詳細情報を格納した配列、返り値はDAGのオブジェクトです。 関数 validation_dag を呼び出した結果をグローバル変数に格納すれば、バリデーションDAGが効率的に作成されるようになります。 DAG間の依存関係の設定方法 Cloud ComposerではDAG内のタスクを定義する際にAirflowで用意されているOperatorを用います。異なるDAG同士の依存関係を設定するには、 ExternalTaskMarker・ExternalTaskSensor というOperatorを用います。 ExternalTaskMarker(Airflow Version 1.10.8以降に実装)は別のDAGのタスクを実行させるOperatorです。以下のコード例のように external_dag_id と external_task_id に後続の別のDAGのタスクを指定します。 from airflow.sensors import external_task_sensor start_following_dag_task = external_task_sensor.ExternalTaskMarker( task_id=f "start_following_dag_{update_datamart_dag_id}" , external_dag_id=f "{update_datamart_dag_id}" , external_task_id=f "start_update_{update_datamart_table_name}" , execution_date = "{{ execution_date }}" , dag=validation_dag,) 一方、ExternalTaskSensorは別のDAGのタスクのステータスを定期的に確認するOperatorです。以下のコード例のように external_dag_id と external_task_id に先行の別のDAGのタスクを指定します。 from airflow.sensors import external_task_sensor verify_leading_dag_task = external_task_sensor.ExternalTaskSensor( task_id=f "verify_leading_dag_{validation_dag_id}" , external_dag_id=f "{validation_dag_id}" , external_task_id=f "check_status_{validation_dag_id}" , timeout= 600 , allowed_states=[ 'success' ], failed_states=[ 'failed' , 'skipped' ], execution_date_fn= lambda dt:dt + timedelta( 0 ), mode= "reschedule" , dag=update_datamart_dag,) ExternalTaskSensorのタスクでは、指定した対象タスクのステータスを定期的に確認します。タスク verify_leading_dag_task のステータスはそのステータスが allowed_states と同じステータスにならなければ success になりません。これにより正常終了が必須である先行ジョブの依存関係が設定できます。 このように、以上に挙げた2つのOperatorを用いたタスクをDAGに組み込むことで複数のDAG間の依存関係を実装しています。 バリデーションDAGのタスク構成 バリデーションDAGは以下の図に示されるタスクのフローで構成されます。 先行の依存関係であるジョブネット構築DAGにおける最後のタスクのステータスが success になるまでバリデーションDAGの実行を待機します。 success になったことを確認したらバリデーションクエリを実行します。 バリデーションクエリの実行結果はCloud Pub/Subを用いてデータ転送します。これは今後Dataflowを用いてBigQueryテーブルに蓄積させたりすることでバリデーション結果を時系列で解析できるようにするためです。 次に、実行結果が閾値内であるかどうかを判定します。閾値内であれば依存する後続のデータマート更新DAGを実行させるようにして、逆に閾値内でなければエラー処理としてSlackにメッセージを送ることで不具合を通知します。 これにより、データマートへはデータバリデーションで妥当性が示されたデータのみ更新するようにします。逆に、データバリデーションでエラーが生じたデータを含むものは更新を全て意図的に止めることで、データマート更新前にクエリなどを修正するように促します。 ちなみに、Apache Airflowにはクエリ実行結果をチェックするタスクのOperatorである BigQueryValueCheckOperator が用意されています。しかし、今回はバリデーション結果の閾値判定を柔軟に処理できるPythonOperatorを使用しました。 まとめ データ集計基盤にデータの質を担保する処理「データバリデーション」を導入することで、集計結果への正確さの向上を図った取り組みを紹介しました。 データバリデーションのおかげで仕様変更などでデータに変化が生じた場合に適切なタイミングでアラートされるようになりました。そのアラートに対応することで、データの仕様が常に把握できている状態になり、仕様に即さない集計結果が出力されることはなくなりました。 また、Cloud Composer上で動的にジョブネットを構築する仕組みも提案しました。これにより、複雑で動的に変化する依存関係を伴うワークフローが手動で逐一設定することなく効率よく定義できるようになりました。 近年では膨大なデータを貯めておくことが容易になった反面、意図しない集計トラブルやコスト的に非効率な集計を起こしやすくなりました。 本記事が、膨大なデータに対する集計のクオリティ管理に関する問題を解決し、データガバナンス強化の足がかりを作る手助けになれば幸いです。 このように、推薦基盤チームではさまざまなシステムを支援するデータ集計基盤の開発・運用に取り組んでいます。チームメンバーを絶賛募集していますので、ご興味のある方は以下のリンクからぜひご応募ください! www.wantedly.com
アバター
こんにちは。SRE部BtoBチームの岩切です。普段はBtoB事業における自社ECサイトの運用保守・監視をしています。 今回2020年11月27日にオンラインで開催された AWS GameDay に参加しました。 本記事では、GameDayイベントで得た学びから実際の業務へどのような効果があったかを共有します。 AWS GameDayについて 今回のAWS GameDayでは、開催前の別日に事前勉強会が2時間、GameDay本番が5時間ほど開催されました。 合わせて17社31チームの123名が参加しており、参加人数の多さからGameDayへの注目度が伺えます。 GameDay概要 GameDayはAWS環境に触れた経験のある人向けです。障害時のトラブルシュートを学び、本番での対応方法を実践的に体験学習できます。以下が概要です。 障害対応の訓練。 本番同様の環境がAWSから提供される。 アプリケーションを稼働し続けスコアを獲得し、チーム対抗でスコアを競う。 内部及び外部の変更や脅威に対し得て柔軟に対応する。 参加レポート それでは、実際の開催2か月前から当日までの動きを紹介します。 参加申し込み 開催の約2か月前に社内向けのAWS GameDay参加者募集が開始されました。 社内でチームが調整され、私のチームは社内メンバー4名での参加となりました。AWS GameDay専用の Slack チャンネルで事前コミュニケーションを図ることもできました。 事前勉強会 AWSの担当者より事前勉強会として、GameDayの紹介及びサービスの紹介が実施されました。内容としては下記サービスの基本的機能の紹介がありました。 リージョンとアベイラビリティゾーン Amazon VPC セキュリティグループ vs. ネットワークACL AWS ELBの種類及び使い方 Amazon EC2インスタンス オートスケーリング Amazon ECS、Amazon EKS、Amazon ECR、AWS Fargate Amazon Lambda & Amazon API Gateway Amazon DynamoDB & Amazon RDS Amazon CloudWatch & AWS X-Ray Amazon CloudTrail 上記以外にもGameDayについての説明があり、適切なサービス運用の大切さと作業分担の大切さをGameDayでは重要視しており、日々の業務にも通じるとの説明がありました。 また疑問点や質問があった場合は適宜Slackで受け付けていただき、分かりやすい回答をいただくことができました。 GameDay当日 当日は休憩を含みながら5時間ほどのWorkshop形式で進行されました。 オープニングからフルオンラインで開催され、AWS側で用意されたオンライン会場に各自が参加しました。 オープニング後には各チームに分かれ、それぞれ好きなツールを使いコミュニケーションすることになります。私のチームでは引き続きSlackを利用し、通話機能でやり取りをしました。 異なる会社のメンバーでチームを組むことも考えられるため、あらかじめチャットツールが用意されているのは、参加の敷居を下げてくれていると感じました。 コミュニケーションについても活発に行うことができ、終始穏やかな雰囲気ですすめることができました。 また開会式では上位3チームに景品があるということも発表され、参加者全員が熱意に満ちていました。 予測できない脅威、変動、変更 全チーム共通で競技の目的を与えられ、とあるサービスのスコアを競いました。 サービスのデプロイだけではスコア上位を目指すことはできず、競技中はAWS側から与えられる予測できない脅威や変更に対応し続けなければいけません。 また予測できない脅威に対して、その場しのぎの対応をしても再度同じ脅威が襲ってくる場合もありました。こういった状況であったこともあり、「実際のサービスにおいても自動復旧を担保しないといけない」といった意識を持つことができました。 サービス運用と作業分担の大切さ 私のチームでは、あらかじめAWSの実務経験をヒアリングしておき、作業の向き不向きや、AWS知識のレベルを認識合わせしました。しかし、作業分担をあらかじめ明確に分けていなかったため、AWS側からの脅威に太刀打ちができませんでした。みんなでサービスのデプロイをするのではなく、各自がしっかりと役割を持っていた方が良かったです。これは、実際のサービスの運用でも同じことが言えるので、学びのひとつでした。 当日を振り返ると、少なくとも以下の作業分担をしっかりを明文化しておくべきだと感じました。 全体進行 デプロイ 障害対応 連携する他サービスの監視 途中からは役割分担ができるようになったのですが、もちろん担当者だけでは対応できない場合もありました。そのため、チームメンバーにどこで助けを求めるかを決めるのもチーム進行の大切な要素であると感じました。 またどうしても作業がうまくいかない場合や、チーム全体の知識で解決できなかった場合は、AWS側から助けをいただくこともできました。 Award & Review Session 競技後、表彰と競技の振り返りを行いました。 スコアボードを見ると上位層は僅差であり、上位3位のどのチームも作業分担を大切にしているとのことでした。 全体的に終始和やかな雰囲気で進み、競技終了後もSlackでは挨拶が盛んでした。 今回は開催されませんでしたが、本来であればWorkshop後にはGameDay前の各参加者の取り組みを紹介するLT大会もあるとのことでした。 GameDayに参加して得られたこと 業務効果 AWS GameDayでは、事前学習も含めてAWSに関する知識を普段以上に吸収できました。そこで学んだ内容が実際に実務で役に立つこともありました。 業務中に下記事象が発生し、GameDayでの経験から即時で復旧できました。 作業者がS3に設定ファイルをリリース。 設定ファイルのフォーマットエラーが発生。 サービスエラーが発生し作業者とは別の運用者が検知。 夜間に検知したため、いつ・誰が・反映したかの確認が難航。 「CloudTrail」で調査し作業者の特定。 作業者に復旧対応の依頼を行い、サービス復旧。 CloudTrailはサービスの存在は知っていたのですが、これまで触れることはありませんでした。AWS GameDayで初めてCloudTrailを経験していたからこそ、これを活用して問題点の即時特定できました。 カオスエンジニアリング AWS GameDayではトラブルシューティングの練習をしましたが、本番稼働しているサービスではシステムの耐障害性のテストが難しいでしょう。 AWSでは障害を意図的に起こすカオスエンジニアリングサービスの AWS Fault Injection Simulator が2021年に提供される予定です。 こちらは本番環境を「ダウンしないサービス」ではなく、「自動復旧が容易なサービス」へ切り替える手助けになるかもしれません。 私も興味があるため、機会があれば別記事でご紹介させていただきます。 AWS GameDayに参加して AWS GameDayは以前から行われているのは知っていてましたが、今回初参加となりました。 GameDayに参加したことで、運用しているサービスの設計や、連携している別サービスが自動復旧性を担保できているか確認する良い機会となりました。 GameDay以外にも障害訓練する機会はいくつかあるかと思います。それらの訓練ももちろん価値があります。しかし、GameDayでは具体的なAWSのサービスが登場した特別な訓練であり、AWSを活用したサービスの運営者にとって、この具体的なサービスを用いた訓練は非常に価値があるものと言えます。 みなさんも本番障害が発生した際の機会損失や社会的信用の低下を防ぐためにも、サービスの自動回復を今一度振り返ってみてはいかがでしょうか? さいごに ZOZOテクノロジーズでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
アバター
こんにちは、ZOZOテクノロジーズ CTO室の池田( @ikenyal )です。 ZOZOテクノロジーズでは、2/9に 第二回 AWSマルチアカウント事例祭り を開催しました。 zozotech-inc.connpass.com AWSを活用する複数社が集まり、事例に関してお話しする祭典が「AWSマルチアカウント事例祭り」です。専門性の高い、ここでしか聞けないコアなトークをお届けしました。特にAWSを使用している方、AWSのマルチアカウント運用を始めたい方、AWSのマルチアカウント運用に課題を感じている方に向けたイベントです。 登壇内容 まとめ ZOZOテクノロジーズ、ニフティ、Classiよりそれぞれ1名ずつ、合計3名が登壇しました。 マルチアカウントでのIAMユーザ把握と可視化 IAMユーザー棚卸しへの取り組み (株式会社ZOZOテクノロジーズ 光野 達朗 / @kotatsu360 ) AWS導入から3年 AWSマルチアカウント管理で変わらなかったこと変えていったこと (ニフティ株式会社 石川 貴之) セキュリティインシデントを乗り越えるために行ったマルチアカウントでの取り組みについて (Classi株式会社 大南 賢亮) 最後に ZOZOテクノロジーズでは、プロダクト開発以外にも、今回のようなイベントの開催など、外部への発信も積極的に取り組んでいます。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。 ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
アバター
ZOZO研究所 の森下( @IshyMore )です。本記事では、数式とソースコードを含む教材を用いてテレワーク環境下で輪講を実施した際に、スムーズに輪講を進められるよう工夫した点について紹介します。 目次 目次 輪講の目的 教材の選定理由 内容が基本的で、応用範囲が広い 数式に対応したソースコードが載っている 演習問題が大量にある 輪講の進め方 輪講運用のための仕組みづくり Jupyter Notebookによる統一 Jupyter Notebook用diffツールの採用 ライセンスの明記 演習問題ごとにファイルを新規作成 Pythonの環境構築 フォーマッタの検討 フォーマットチェックの自動化 コミュニティへの貢献 参加者への事後アンケート まとめ おわりに 輪講の目的 ZOZO研究所では日々の研究開発の傍ら、論文の輪読会、興味・関心事を紹介する勉強会、研究者を招待しての講演会など行っています。輪講も開催しており、メンバー同士で互いに議論しながら教材を読むことで、基礎知識の定着を図っていました。 ところが、昨今の新型コロナウィルスの感染拡大に伴い 弊社では在宅勤務が中心 になりました。それまで開催していた輪講は、オフィスに置いてある本を利用して同じ場に集って実施していましたが、新型コロナウィルスのためオフィスに出社することが困難となりました。 そこで、それまで開催していた輪講を中断し、新しい教材で輪講を開始することにしました。しかし、普段であれば同じ場所でホワイトボードを使いながらしていたような議論ができなくなり、新たな輪講の形式を模索する必要がありました。 教材の選定理由 まず輪講の教材として、 統計的機械学習の数理100問 with Python という本を選びました。この教材はタイトルの通り、機械学習の基本的な内容を全100問の問題を解きながら身につけようという内容になっています。 今回こちらの本を採用した理由は以下の3つです。 内容が基本的で、応用範囲が広い 数式に対応したソースコードが載っている 演習問題が大量にある 内容が基本的で、応用範囲が広い 今年度はZOZO研究所に新卒のMLエンジニアが配属されたため、機械学習の基本的な内容を学べる教材が適していました。また、ZOZO研究所には様々なバックグラウンドの研究者が集まっており、専門も機械学習に限らず数理最適化・制御理論・コンピュータグラフィクスなど幅広いです。それらの最新の知見は別途論文の輪読会や技術共有会などを開催して常にキャッチアップしています。 そのため、輪講ではより基本的なレベルでかつ多くのメンバーに役立つような内容を扱うよう意識しました。 数式に対応したソースコードが載っている ZOZO研究所は機械学習に関するシステムを開発することも多く、理論と実装の両方を学べる教材が望ましいです。採用した教材は、Bitbucketのリポジトリに本文中の登場するソースコードを全て公開しており、そのような用途に適していました。 演習問題が大量にある 演習問題を解くためには、自然と教材を読み込まざるを得ないので理解が捗ります。過去に別の輪講を主催していた際には演習問題がない教材を扱っていたのですが、流し読みで終わる人と、実際に手を動かす人では内容の理解度に差がついていました。 そこで、演習問題が大量にある教材を選び毎回1人1問をノルマに演習問題の解答を作成してもらうことで、曖昧な理解なままで終わるのを防止しました。 輪講の進め方 輪講は週1回90分の時間をとって開催しました。以下が1回の流れです。 事前準備 該当範囲の本文を読む 担当の演習問題を解いて、解答をGitHubにPull Requestとして提出 疑問点を社内Wikiに記載 輪講当日 各自が担当した問題の解答を画面共有しながら解説 社内Wikiに記載された疑問点を全員で議論 輪講後 その日の議論の内容を社内Wikiに記載 Pull Requestの解答に問題がなければマージ 上述の事前準備を参加者全員がすることで輪講をスムーズに進めることができました。作成した解答はGitHubリポジトリのmainブランチへ、Pull Requestとして提出してもらいますが、詳細は後述します。なお、輪講後のタスクは、幹事である私が担当していました。 以上の形式で進めると、議事録である社内Wikiには、我々がハマった箇所とそれに対する解決策が全て記載されることになります。これにより、輪講へ途中参加する際のキャッチアップが容易になるだけでなく、輪講終了後にも教材を独学する人の助けとなる資料が完成します。 また、今回上記の教材を読むにあたって、問題を解くと同時に本文中のソースコードのリファクタリングも行いました。特にコーディングスキルの高いメンバーはよりスマートな実装や、より速い実装を提案してくれます。それらを共有することで、ベテランから開発経験の浅いメンバーに対してうまく技術を伝達する機会を作ることができました。 輪講運用のための仕組みづくり テレワーク環境下では気軽にホワイトボードを使って議論ができません。今回採用した教材はソースコードを書くだけでなく数式を使って計算・証明をする演習問題も多く、それらをどのように参加者間でスムーズに共有するかという点に課題がありました。 そこで、以下に述べる工夫をしました。 Jupyter Notebookによる統一 数式の記述であればTeXファイルが望ましいですが、GitHub上でTeXファイルの数式は表示できません。 そこで、各自担当する演習問題の解答は、全てJupyter Notebookファイルで作成するようにしました。Jupyter Notebookファイル上でLaTeX記法を用いることにより、GitHub上で数式を表示できるためです。なお、本文中のソースコードは全てJupyter Notebookファイルで公開されていたことも理由に含みます。 Jupyter Notebook用diffツールの採用 Jupyter Notebookファイルの中身はJSON形式なので、リファクタリング前後の差分をGitHubのWeb画面上では綺麗に表示できません。 そこで、Jupyter Notebookファイルの差分を綺麗に表示できる、 nbdime を導入しました。 上図は、nbdimeでリファクタリング前を左半分に、リファクタリング後を右半分に表示したものです。 ライセンスの明記 公開されているソースコードのリファクタリングを試みる場合、複製・配布・改良の範囲はOSSライセンスによって決定されます。今回選んだ教材のソースコードには、ライセンスが未記載だったため、そのまま使用すると著作権侵害に該当する恐れがありました。 そこで、教材の著者に公開されているソースコードのライセンスを明記していただきました。これにより、社内のリポジトリへの複製が可能になり、ソースコードを改変できるようになりました。 演習問題ごとにファイルを新規作成 演習問題1問につき、1つのJupyter Notebookファイルを新規作成し、そこへ解答を記載するようにしました。教材の演習問題は100問と非常に多いので、章ごとにJupyter Notebookファイルを作成することも考えました。しかし、同じJupyter Notebookファイルを共同編集することでコンフリクトが発生しかねないので、このような方針としました。 また、公開されているソースコードは章ごとに1つのJupyter Notebookファイルでまとめられており、それを元にリファクタリングしたJupyter Notebookを新規作成しました。 Pythonの環境構築 Pythonのバージョンを指定し、必要なパッケージは requirements.txt を配布することで、参加者全員が同一の環境でコーディングできるようにしました。Pythonのバージョンを指定したのは、公開されているソースコードのJupyter Notebookファイルのメタデータに実行されたPythonのバージョンが記載されていたためです。 ここで考慮すべき点は、全ての公開されているソースコードが動作するような requirements.txt を作成することです。 公開されているソースコードはJupyter Notebookファイルのみで構成されており、その中で import が適宜記載されているスタイルでした。また、我々はnbdimeなど教材に記載されていないパッケージも利用していたため、 requirements.txt が自明ではありませんでした。 試しに、パッケージのバージョン指定をせずに requirements.txt を手動で作成したところ、依存関係でインストールエラーが発生しました。そこで、 Poetry で依存関係を解決し requirements.txt を作成しました。なお、 pandas などのバージョンが変わるとメソッドが変わってしまうパッケージはマイナーバージョンまで固定しました。 ちなみに、Pythonパッケージの依存関係を解決するだけでは、パッケージが問題なく動作するとは限りません。例えば、 LightGBM はインストールでエラーにはなりませんが、 import 実行時にエラーが発生します。この場合、事前に brew install で必要なパッケージをインストールする必要あったので、その旨をGitHubリポジトリに明記しました。 Poetryを利用したのはあくまでも、初めの段階でパッケージの依存関係を解決するためだけであり、参加者各自が環境構築する際は requirements.txt で行っていました。後になって考えると、Pythonのバージョンを明示的に指定できて、仮想環境も自動作成されるPoetryで環境構築した方が良かったのかもしれません。今後の改善ポイントです。 フォーマッタの検討 解答の作成やリファクタリングは、各メンバーが個別に行うため、コーディングスタイルに細かい差が出てしまいます。 そこで、Jupyter Notebookファイルのフォーマッタとして nb_black を導入することでコーディングスタイルの統一を目指しました。 nb_blackを使うには、それをインストールした上で、 %load_ext nb_black または %load_ext lab_black という文字列が入力されたセルを実行します。すると、後に実行されるセルに対して Black のフォーマットが適用されます。なお、上記nbdimeの使用例の図の右側は、nb_blackを実行済のソースコードです。 他の選択肢として Jupyter Black もありましたが、これはGUIで実行するものであり、後述するGitHub ActionsやGitHub Webhooksと相性が悪かったので見送りました。 フォーマットチェックの自動化 nb_blackが適用されていることをレビュアが毎回確認する手間を減らすため、GitHub Actionsを用いてフォーマットチェックを自動化しました。 実際に使用したワークフローを以下に示します。 Pull Requestが作成される度に、 %load_ext nb_black または %load_ext lab_black がJupyter Notebookファイルに含まれているかチェックしています。チェックを通過しないPull Requestはmainブランチへのマージができないようにしました。 name : Python nb_black on : push : branches : [ main ] pull_request : branches : [ main ] jobs : build : runs-on : ubuntu-latest steps : - uses : actions/checkout@v2 - name : Set up Python 3.7 uses : actions/setup-python@v2 with : python-version : 3.7 - name : Check code style with nb_black run : | count=0 for file in $(find . -not -path "*/\.*" -and -not -name "textbook_??.ipynb" -and -type f -name "*.ipynb" ); do chk_nb_black=$(cat $file | jq '.cells[] | select(.cell_type == "code").source[] | contains("%load_ext nb_black")' ) chk_lab_black=$(cat $file | jq '.cells[] | select(.cell_type == "code").source[] | contains("%load_ext lab_black")' ) if [[ $chk_nb_black == * true * ]] || [[ $chk_lab_black == * true * ]] ; then echo OK : $file elif [ -z "$chk_nb_black" ] ; then echo OK : $file else count=$(( count + 1 )) echo NG! : $file fi done if [ $count -gt 0 ] ; then exit 1 fi 実は、ここで行われている判定はあくまで文字列が存在するかどうかについてであり、実際にフォーマッタが実行されているかどうかは確認していません。 例えば、コーディングを全て終えてから %load_ext nb_black とセルに入力すると、Blackが実行されていないもののGitHub Actionsのチェックを通過してしまいます。Blackが実行されたかどうかを厳密にチェックすることも考えましたが、そこを厳格化するための費用対効果は低そうだったので、このような簡易的な確認で十分と判断しました。 なお、Pull Request時に自動判定する機能はGitHub Actionsの他にGitHub Webhooksもありますが、今回は実装が容易なGitHub Actionsを採用しています。 コミュニティへの貢献 他の参加者に解説できるくらい教材を丁寧に読み込んでいくなかで、細かい誤植を見つけることがありました。このような誤植は社内Wikiで逐次報告されるため、社内のメンバーに対しては誤植が原因で進まないという状況は減らせました。しかし、同じ教材を読み進めている社外の方たちは同じ問題に直面するはずです。 そこで、教材の著者が主催しているFacebookグループにも、逐次報告しました。すべての報告を著者自身に確認していただき、その都度、疑問を完全に解消していきました。 その結果、報告した内容は全て公式の正誤表に掲載され、新しく出版された英語翻訳版ではその箇所が修正されました。この貢献を認めていただき、英語翻訳版の謝辞には私の名前が掲載されています。 なお、弊社はOSS活動を推奨しており、OSS活動は職務として認められています。 techblog.zozo.com これは余談ですが、過去1か月以内にFacebookグループで最もエンゲージメントの高い投稿をした人に与えられる「Facebookの盛り上げ達人」という謎のバッジを獲得しました。また、著者が出版した続編の帯には「Facebookページが盛り上がっています」と記載されているのですが、この盛り上がりの一端を担っているのは我々ZOZO研究所のメンバーであろうと想像しています。 参加者への事後アンケート 輪講終了後、参加メンバーに対してアンケートを実施しました。「参加してよかったか」と「業務に(直接的でも間接的にでも)活かせそうか」を、5段階で評価してもらった結果が以下の図です。 多くの参加者が「参加してよかった」とポジティブに感じてくれたようです。実際に、議事録やGitHubを上手く使いこなすことで議論が白熱したり、詳しい人からの知識の伝授が発生したりとテレワーク環境下でも問題なく輪講が実施できました。 業務に活かせるかに関しては、控えめな結果となりました。教材の内容が機械学習の基本的な内容であるため、特定のタスクに特化した知識は得られないことと、機械学習の数理部分が機械学習システムの開発のごく一部でしかないことを反映しているのだと考えています。 まとめ テレワーク環境下で初めて輪講を行い、そこで得られた知見を本記事にまとめました。また、輪講のシステムのために幹事として準備したこともまとめ、結果として教材の謝辞に名前が載ることになりました。 そして、輪講参加メンバーに対するアンケートでも、概ね好意的な評価が得られました。読者の皆様がテレワーク環境下で輪講や勉強会を開催する際に、本記事の内容が少しでも参考になれたら幸いです。 おわりに ZOZOテクノロジーズでは、各種エンジニアを募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 tech.zozo.com
アバター
こんにちは、ZOZOテクノロジーズSREチームリーダー兼組織開発チーム所属の指原( @sashihara_jp )です。 この記事では2019年12月から全11回開催してきた「マネジメント勉強会」を通じて分かってきたZOZOテクノロジーズの組織課題と、これから取り組もうとしているその解決方法を紹介します。 ZOZOテクノロジーズの社員構成 マネジメント勉強会とは 立ち上げまでの道のり 運営メンバーの勧誘 経営層への企画提案 勉強会の命名 1年間で実施したテーマ 第1回 各チームで実施しているチームビルディング施策の共有 第2回 書籍「1on1マネジメント」を読んだ上で内容について議論 第9回 採用面接で質問している内容について意図と効果共有 マネジメント勉強会を通じて分かってきたZOZOテクノロジーズの現状 1.組織の急拡大による弊害 2.現場のコンフリクト 3.マネジメントと人材育成 組織開発チームの立ち上げについて 1.組織の急拡大による弊害への対応 2.現場のコンフリクトへの対応 3.マネジメントと人材育成への対応 今後の展望 マネジメント勉強会のリニューアル 組織開発チームが目指す組織 最後に ZOZOテクノロジーズの社員構成 まず、弊社について簡単に説明します。弊社は株式会社ZOZOの100%子会社であり、親会社のZOZOと子会社のZOZOテクノロジーズで主な役割が異なっています。ZOZOにはZOZOTOWNを運営するために必要なブランド営業・マーケティング・カスタマーサポート・物流まわりなどのスタッフが在籍しています。 一方、ZOZOテクノロジーズにはシステム開発をするために必要なスタッフ、エンジニア・デザイナー・リサーチャーなどが在籍しています。2020年2月時点ではZOZOテクノロジーズには約400名の従業員が在籍し、そのうちエンジニアは約350名ほどを占めています。 マネジメント勉強会とは そんなエンジニアの比率が高い会社の中で、マネジメント勉強会を立ち上げた経緯から説明します。 まず、ある法則を紹介します。米国の人事コンサルタント会社ロミンガー社の調査によると、ビジネスにおいて人は70%を仕事上の経験、20%を同僚からの助言やフィードバック、10%を研修などのトレーニングから学ぶと言われています。これは「7・2・1の法則」とも呼ばれ、企業研修などでもよく引用されています。 わたし自身、弊社に入社してから3年ほどSREチームのマネージャーとしてマネジメントをしてきてこの「7・2・1の法則」を実感してきました。仕事上の経験は当然積むのでそこからの学びはもっとも多く、マネジメント関連の書籍もたくさん読んで可能な限り自学してきました。ただ、それだけでは自分の成長速度に限界を感じていたのも事実でした。 そこで自分自身の成長速度を加速させるために上記法則の「同僚からの助言やフィードバック」を強化したいと考え、そのための仕組みを他のマネージャーたちと一緒に作りたいと思い立ちました。わたしが成長速度に物足りなさを感じていたのと同様に、弊社の他のマネージャーもそれぞれ悩みや、それに対する解決方法を持っていて、お互いに共有しあうことで成長したいと感じているのではと思ったからです。 また、マネジメント勉強会の立ち上げを検討し始めた2019年10月はZOZOグループが買収され前代表の前澤が退任した直後でした。強力なトップダウン型リーダーがいなくなったタイミングだったので、自分たちも変わらなければという意識が全社的に強まっていた時期でもあり、会社としてもマネージャー層のマネジメントスキル向上や横の情報共有を強化することが重要であるのではという考えもありました。 立ち上げまでの道のり そのような考えからマネジメント勉強会の立ち上げを思い立ち、下記のようなステップで初回の開催を計画していきました。 運営メンバーの勧誘 経営層への企画提案 勉強会の命名 運営メンバーの勧誘 まず、マネジメント勉強会を立ち上げるにあたって最初に考えたのが「1人で運営するべきではない」ということでした。複数人の運営メンバーを擁立することで下記のようなメリットがあります。 運営にかかる工数を役割分担することで分散できる 運営メンバーで相談しながら運営することで会のクオリティを向上できる 開催の継続力が高まる そこで以前から社内でマネジメントに関して関心が強かった荒井( @arara_jp )と鶴見( @_tsurumiii )に声をかけて、運営チームが確立されたことでマネジメント勉強会の立ち上げを決めました。 経営層への企画提案 次にマネジメント勉強会の立ち上げについて経営層に企画の共有をしました。 弊社はエンジニアが多い会社なので技術的な勉強会は社内で日常的に行われており、勉強会の開催自体はよくあることなので通常であれば経営層に許可を取る必要はありません。しかしマネジメント勉強会については組織横断型を想定しており、マネジメント層のスキル向上など経営課題にも関連する内容だったので企画について事前共有をしました。 結果、経営層からは応援するという前向きな意見をもらうと同時に、勉強会の中で出てきた「マネージャー層が感じている課題感や制度設計に関する要望」などについて教えてほしいという要望をもらい現在まで開催毎に経営層へのフィードバック会を実施しています。 勉強会の命名 この会は経営層から言われて業務命令で立ち上げたわけでもなく、人事が研修として公式な業務で行っているわけでもなく、一般のチームリーダーが自主的に必要性を感じて周囲に声をかけて始めた試みです。 そこでこの勉強会を立ち上げるに伴いもっとも意識したのは「ネーミングを間違えない」ということでした。具体的には「マネジメントのことを誰かに教えてやるという上から目線を感じない名前」にしようということです。これは設立理由にも記述したように、わたし自身が周りのマネージャー陣と一緒に「マネージャーとして成長していきたい」という想いから始まっているので運営メンバーは参加者を教育するような立場ではなく、あくまで参加者と同じ目線でいたいということです。ネーミングが上から目線になることで、多くのマネージャー陣の共感を得られず成果を挙げられないまますぐ終わるというようなことだけは避けたいと思っていました。 当初、勉強会の目的の1つにジョブマネジメントだけでなくピープルマネジメントの重要性を広めたいという気持ちもあったので「ピープルマネジメント研究会」みたいな案もありました。しかし、これだと一部の意識高い人だけが参加するというような印象を受けて参加ハードルが高いだろうと運営メンバーで話し合い、立ち上げ当初のネーミングとしては「新人マネージャー勉強会」に落ち着きました。 今の「マネジメント勉強会」とは異なる名前ですが、これは経験の浅いマネージャーも参加しやすいようにハードルを下げたいという意図がありました。また、運営メンバー陣もマネジメント歴がそれほど長いわけではないので「皆一緒なんですよ」というスタンスを強調したものでした。この名前は数回開催したあとに、逆に経験のあるマネージャーが参加しづらいのではという意見があり現在の「マネジメント勉強会」に変更しています。 1年間で実施したテーマ マネジメント勉強会は現在に至るまでの約1年間をかけ、合計11回開催してきました。各回の参加人数はテーマによってまちまちですが10名程度から最大で50名程度となっています。累計では全管理職の約8割がいずれかの回に参加しています。 これまでに下記のテーマを選定し開催してきました。 第1回 各チームで実施しているチームビルディング施策の共有 第2回 書籍「1on1マネジメント」を読んだ上で内容について議論 第3回 外部講師を招いての1on1勉強会 第4回 評価についての悩みや疑問を共有する会 第5回 他チームリーダーへの質問・相談会 第6回 マネージャーのキャリアプランについて考える会 第7回 外部講師を招いてのピープルマネジメント勉強会 第8回 コロナ禍でのチームマネジメント方法の工夫を共有する会 第9回 採用面接で質問している内容について意図と効果共有 第10回 新人事制度についての質問、フィードバック会 第11回 書籍「1on1ミーティング」を読んだ上での内容について議論 内容については各マネージャーが抱えている課題や悩みについて共有することで他のマネージャーから解決案のヒントを得るという形式や、会社の新制度導入タイミングに合わせた企画が多いです。また、全員で同じ書籍を読んで読書会のような方法をとったり、外部講師を呼んで講演会のような形式をとったりと参加メンバーが飽きないような工夫もしてきました。 いくつかテーマの紹介とそれぞれの効果を簡単に紹介します。 第1回 各チームで実施しているチームビルディング施策の共有 第1回では各チームで実施しているチームビルディング施策について紹介し合いました。たとえば下記のような施策が紹介されました。 人生(モチベーション)グラフ 過去のモチベーションが上がった出来事、下がった出来事を人生グラフにして開示しあうことでお互いのパーソナリティを知る。 チーム内ZOZOエール 会社で採用しているUniposというピアボーナス制度を活用し、日頃の感謝やバリューを体現した行動を賞賛しあう時間を毎週の定例の中に作る。 なお、チーム内ZOZOエールについては、以下の記事で紹介しています。 techblog.zozo.com 他にもたくさんの施策がありましたが、他のチームがやって成功している事例を取り入れることができ有意義な内容となりました。コロナ禍になる前でしたが、当時からリモートワークが制度として導入されていたこともあり、人間関係を円滑にするための工夫をそれぞれのチームが実施していることが印象的でした。 第2回 書籍「1on1マネジメント」を読んだ上で内容について議論 第2回はピープルマネジメントについての書籍「1on1マネジメント」を参加者で事前に読んできて内容について当日議論するという内容でした。事前に課題図書を作ることで参加ハードルは上がってしまうのですが、共通の書籍を読んでくることで目線のすり合わせができた状態で1つのテーマについて話すことができとても好評でした。 この書籍を参加者全員が読むことでピープルマネジメントの大切さについてインストールされたマネージャーが増えたことは会社として価値のあったことだと思います。 第9回 採用面接で質問している内容について意図と効果共有 第9回では採用をテーマにして議論しました。面接の際にどのような質問をすると候補者の本音が引き出せやすいかなどの知見の共有です。また、応募者が現在の採用ページを見るとこの部分で迷うのではないかという意見や、こんな面接官トレーニングをしたらいいのではという要望も出てきたので後日、採用人事へフィードバックする会も実施しました。 この会でも参加したマネージャーから多くの意見や悩みが出てきて、今まで会社として拾い上げることができていなかった声を拾えた意義ある会となりました。 マネジメント勉強会を通じて分かってきたZOZOテクノロジーズの現状 このようにマネジメント勉強会を開催していく中で、会社の局所的な課題やマネージャーが持つ個々の悩みについては横の情報共有をすることで、ある程度は解消することができました。しかし、マネジメント勉強会だけでは解決が難しい下記のような3つの大きな課題もZOZOテクノロジーズにはあることが分かってきました。 1.組織の急拡大による弊害 ZOZOテクノロジーズはこの3年間で社員数が200人から450人に急増してきました。この急激な規模拡大の影響からZOZOグループ全体で目指している上位戦略が現場に伝わりづらくなってしまい、やりがいや達成感を感じづらくなっているという問題が起きているようでした。また、規模が大きくなることで隣の部署やチームが何をしているのかよく分からないという関心の希薄化も生まれていました。 2.現場のコンフリクト ZOZOテクノロジーズはグループ内にあった7社が合併してできた会社であるため、さまざまな文化が混ざりあった状態です。それぞれの会社で大切にしていた価値観が時にぶつかり合うこともありました。また、文化が融合し、それまでのどの文化とも違う「新しいZOZOらしさ」が生まれましたが、その変化の速さに追いつけない社員も出てきました。 technote.zozo.com 3.マネジメントと人材育成 そして現場のマネージャー層がもっとも困っていたのは自身のマネジメントスキル向上についてでした。各々のチームの中で各リーダーがさまざまな工夫をしているものの、会社としてスキルや役割の標準化、マネージャー育成支援が十分にできているわけではなかったことからマネジメントスキルの属人化が進んでいました。また、現場の社員もキャリアパスや育成についての明確な指針がないことで不安を感じているという状態でした。 上記の3つの大きな課題はグループ会社が合併する前まではそれほど大きな問題にはなっていませんでした。それは会社の規模がまだ小さく、合併もしていない1つのみの会社だったので文化も単一、縦と横のつながりが強固で、やりがいと達成感を感じやすい環境だったからです。しかし、グループ会社が合併し、文化が混ざりあった状態で下図のような変化が起きてきました。 出典: 1on1ミーティング「対話の質」が組織の強さを決める この図では横軸が「上司・同僚との関わり具合」、縦軸が「ストレッチ経験の量」を表しています。どちらも高い状態だと「成長実感職場」となります。現在のZOZOテクノロジーズは上述のような歴史的背景から「上司・同僚の関わり具合」が徐々に薄れてきており、左上の「挑戦させすぎ職場」となっている可能性が高いです。 組織開発チームの立ち上げについて このような3つの大きな組織課題と「上司・同僚の関わり具合」が減っていることによる「挑戦させすぎ職場」になっている状態を脱却するため、新たに「組織開発」をテーマにした新チームを立ち上げることにしました。組織開発に詳しい専門家を他社からリーダーとしてスカウトし、わたし自身もメンバーとしてこのチームに参加しています。組織開発チームでは具体的に上記課題を以下のように解決していくことを目指しています。 1.組織の急拡大による弊害への対応 上位戦略の伝達のしづらさについては2020年10月から開始された新評価制度により全社目標、部門目標、個人目標の連携を強めています。自分の仕事がどのように上位戦略に結びついているか実感しやすくなることを目的としています。また、その連携を強めるための手法として週1回30分の1on1を必須として全社導入を始めています。隣のチームが何をしているか分からないという問題については階層別研修やコミュニケーション施策を強めることで解消を目指します。 2.現場のコンフリクトへの対応 新しいZOZOらしさへの適応についても2020年10月に制定された新バリューの浸透施策により共通の価値観を醸成すると共に、同じ目標に向かっていけるよう目標管理制度を導入しています。また、個別の組織課題についても組織開発チームとして支援をします。 3.マネジメントと人材育成への対応 マネジメントスキルについても会社として役割の明文化、スキルの標準化を目指し各種研修、支援を開始します。また、現場社員についても等級定義やキャリアパスを明示的に提示し、キャリア関連施策を実施します。 今後の展望 マネジメント勉強会のリニューアル これまでマネジメント勉強会は初期運営メンバーが、その都度必要だと思うテーマについて検討して実施してきました。今後は上述の会社全体の課題から逆算したゴール設定をし、戦略的に他の社内施策や勉強会とも連携調整しながら進めていく必要があると考えています。運営メンバーも追加募集を始めており、より会社に貢献するような会へと進化させていきます。 組織開発チームが目指す組織 発足した組織開発チームでは「創造性を解き放つ人と組織をつくる」をミッションとし、上記のような必要な施策や制度推進に取り組むことで、組織の課題解決をしていきたいと考えています。そしてグループのビジョンである「世界中をカッコよく、世界中に笑顔を。」の実現を目指します。 最後に まだまだたくさん課題のある会社ですが、スキル・マインド共に高い魅力的な社員が多く、組織課題をうまく解決していくことで飛躍するポテンシャルが非常に高い組織です。やるべき方向性は明確で進化の真っ最中です。 一緒にサービスを作り上げてくれる方はもちろん、さまざまな職種でZOZOテクノロジーズをいい組織にしてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! tech.zozo.com
アバター