TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

938

はじめに こんにちは。検索基盤部の倉澤です。 検索機能におけるtypo(誤字脱字や綴り間違いなど)は難しい問題 1 とされています。typoの扱い方によってはユーザーに悪い検索体験を提供してしまう恐れがあります。例えば、typoを含む検索クエリを入力された時にユーザーが意図している検索結果を得ることができないといった問題があります。 例に漏れず、ZOZOTOWNでもtypoを含む検索クエリが入力された場合に検索結果が表示されないといった問題が発生しています。以下、「レディース」と入力するつもりが「レデース」と入力してしまった場合の検索結果です。 今回は日本語におけるtypoの一般的な解決策を調査・検証し、その結果・課題点を紹介します。手法の検証が容易であることを優先し、以下の2つの方法について検証しました。 Elasticsearchを用いてtypoを含む検索クエリでも検索結果を得る方法 ユーザーの検索クエリログを用いてtypoを正しいクエリに修正する方法 目次 はじめに 目次 Elasticsearchを用いた検索時のtypoの扱い方 Fuzzy match Synonym token filter ユーザーの検索クエリログを用いたスペルコレクション Peter Norvig, "How to Write a Spelling Corrector"を元にしたスペルコレクション 再検索クエリログを用いたスペルコレクション さいごに Elasticsearchを用いた検索時のtypoの扱い方 本章では、Elasticsearchを用いてtypoを含む検索クエリでも検索結果を得る方法について説明します。1つ目はFuzzy match(あいまい検索)と呼ばれる手法で、ユーザーが完全な検索クエリを入力しなくても、関連する結果を得られるようにします。2つ目はSynonym token filterを用いた手法で、typoクエリを修正クエリへ変換し検索結果を得られるようにします。 以下の表に、それぞれの手法の概要と、精度と運用コストの観点からの評価をまとめました。 手法 概要 精度 運用コスト Fuzzy match ElasticsearchのQuery DSLで用意されている Fuzzy query を用いたあいまい検索により検索結果を返す Not Good 入力されたクエリの文字数に対するfuzzinessの設定など検索結果をコントロールするのが難しい Good ElasticsearchのVersionによるメンテナンス程度で負担が小さい Synonym token filter Elasticsearchの Synonym token filter によりtypoクエリから正しいクエリへ展開し検索結果を返す Good Synonym token filterで定義しているクエリによってヒットする結果をコントロールすることができる Not Good 新しいtypoクエリが発見された場合にSynonym token filterの定義に追加するコストが発生し続ける Fuzzy match Elasticsearchには、あいまい検索を実現するために Fuzzy query が用意されています。Fuzzy queryは、レーベンシュタイン距離(以下、編集距離と呼ぶ)によりtypoを含む検索クエリでも検索結果を返すことができます。 編集距離は、ある文字列を別の文字列へ変換するために必要な操作回数を表します。具体的には、以下の4つの操作を用いて文字列を変換します。 文字の挿入 文字の削除 文字の置換 文字の転置 例えば、「レデース」という文字列を「レディース」に変換する場合、文字の挿入を1回行えば変換ができます。この場合の編集距離は1となります。 Fuzzy queryでは、この編集距離を fuzziness というパラメータを用いて指定でき、設定した値を最大値として範囲内に収まるキーワードをマッチさせることが可能です。 一方、Fuzzy queryには検索結果をコントロールするのが難しいという問題点があります。指定の編集距離内であれば、どのようなクエリでも検索結果を返してしまうため、関連しない結果がヒットしてしまう可能性があります。特に文字数が少ないクエリの場合は、指定の編集距離内に収まるクエリが多くなり、この問題が顕著になります。 例えば、ユーザーがズボンに関連する商品を探しており「ズボン」と入力するつもりが「ザボン」と入力してしまった場合について考えます。 GET kurasawa_test_index/_search { " query ": { " match ": { " keyword ": { " query ": " ザボン ", " fuzziness ": " AUTO " } } } } 上記の検索クエリを実行すると、以下のような結果が返ってきます。 ... " hits " : [ { " _index " : " kurasawa_test_index ", " _id " : " xxx ", " _score " : xxx , " _source " : { " keyword " : " ズボン ", ... } } , { " _index ": " kurasawa_test_index ", " _id " : " xxx ", " _score " : xxx , " _source " : { " keyword " : " リボン ", ... } } , { " _index ": " kurasawa_test_index ", " _id " : " xxx ", " _score " : xxx , " _source " : { " keyword " : " おぼん ", ... } } , ] 上記の検索結果を見てみると、ユーザーが意図している「ズボン」がヒットしていることがわかります。しかし、「リボン」や「おぼん」もヒットしていることがわかります。これらは「ザボン」に対する編集距離が1であることからヒットしており、ユーザーが意図していない結果もヒットしてしまっています。 このようにFuzzy queryではユーザーが意図していない結果もヒットしてしまうため、検索結果をコントロールするのが難しいという問題点があります。 過去の検索クエリのログからtypoのクエリとそれに対応する修正クエリが傾向として把握できている場合は、次章のSynonym token filterを用いることで検索結果をコントロールできます。 Synonym token filter 検索時のAnalyzerに Synonym token filter を追加することで、typoクエリから修正クエリへ変換し検索結果を返すことができます。Fuzzy queryとは異なり、予め定義した正しいクエリへと変換できるため、検索結果をコントロールできます。 以下に、Synonym token filterによる検証手順を説明します。前項の例を元に「ザボン」を「ズボン」に変換するようにSynonym token filterを定義します。 # インデックスの作成 PUT /kurasawa_test_synonym # アナライザーの定義 PUT kurasawa_test_synonym/_settings { " analysis ": { " filter ": { " my_synonym_filter ": { " type ": " synonym ", " synonyms ": [ " ザボン=>ズボン " ] } } , " analyzer ": { " my_index_analyzer ": { " type ": " custom ", " tokenizer ": " kuromoji_tokenizer " } , " my_search_analyzer ": { " type ":" custom ", " tokenizer ": " kuromoji_tokenizer ", " filter ": [ " my_synonym_filter " ] } } } } # マッピングの定義 PUT kurasawa_test_synonym/_mapping { " properties ": { " keyword ": { " type ":" text ", " analyzer ": " my_index_analyzer ", " search_analyzer ":" my_search_analyzer " } } } # ドキュメントの登録 POST /kurasawa_test_synonym/_bulk { " index ": { " _id ": 1 } } { " keyword ": " ズボン " } { " index ": { " _id ": 2 } } { " keyword ": " リボン " } { " index ": { " _id ": 3 } } { " keyword ": " おぼん " } 上記で定義したインデックスに対して、以下の検索クエリを実行します。 GET kurasawa_test_synonym/_search { " query ": { " match ": { " keyword ": " ザボン " } } } 以下の検索結果から、ユーザーが意図している「ズボン」のみがヒットしていることがわかります。 { ... " hits " : [ { " _index " : " kurasawa_test_synonym ", " _id " : " xxx ", " _score " : xxx , " _source " : { " keyword " : " ズボン " } } ] } } このようにSynonym token filterを用いることで、typoクエリから修正クエリへ変換し検索結果をコントロールできます。 ただし、Synonym token filterを用いる場合は、予めtypoクエリと正しいクエリの対応を把握しておく必要があります。さらに、新しくtypoと思われるクエリが発見された場合は都度filterに定義を追加するなどの運用コストがかかります。そのため、Synonym token filterを用いるかどうかは、検索結果に対する精度と運用コストのバランスから判断する必要があります。 ユーザーの検索クエリログを用いたスペルコレクション 本章では検索する前にtypoクエリを修正することを想定したスペルコレクションの手法を紹介します。また、今回紹介する手法はユーザーの検索クエリのログを用いた手法です。 手法 概要 精度 Peter Norvig, "How to Write a Spelling Corrector" を元にしたスペルコレクション Peter Norvig, "How to Write a Spelling Corrector" の提案手法を日本語に適用するために改良した方法。typoクエリに対する修正候補を編集距離を用いて生成し、文章中の出現確率が最大となるクエリを出力する ZOZOTOWNのデータを用いて検証した結果、正しく修正できたクエリの割合: 69% 再検索クエリログを用いたスペルコレクション 各ユーザーの連続する検索クエリログから再検索されたクエリを修正候補のクエリとみなし、出現確率が最大となるクエリを出力する ZOZOTOWNのデータを用いて検証した結果、正しく修正できたクエリの割合: 14% Peter Norvig, "How to Write a Spelling Corrector"を元にしたスペルコレクション Peter Norvig, "How to Write a Spelling Corrector" の提案手法がスペルを修正する手順を簡単に説明します。具体的な手法については、文献を参照してください。 文章中の各クエリの出現頻度を算出する typoクエリに対する編集距離1の文字列を生成し、手順1の文章中に存在するクエリを修正候補のクエリとする typoクエリに対する編集距離2の文字列を生成し、手順1の文章中に存在するクエリを修正候補のクエリとする 修正クエリを以下の優先順位で決定する 4.1. 編集距離0の文字列が手順1の文章中に存在すれば、入力されたクエリを修正クエリとする 4.2. 手順2の修正候補クエリが存在すれば、その中から出現頻度が一番高いクエリを修正クエリとする 4.3. 手順3の修正候補クエリが存在すれば、その中から出現頻度が一番高いクエリを修正クエリとする 4.4. 入力されたクエリを修正クエリとする この提案手法を日本語に適用しZOZOTOWNのデータを用いて検証するためには、以下の点が課題になりました。この課題に対する原因と解決策を記載します。 課題 原因 解決策 ファッション用語に特化したコーパスの準備 ファッション用語に特化した外部公開されたコーパスが存在しない ZOZOTOWNのユーザーの検索クエリのログを用いる 日本語で編集距離1または2の文字列を生成する際の計算量が膨大 漢字・カタカナ・ひらがなの文字数を2000文字とすると、長さnの文字列に対する編集距離1の文字列を生成するための操作回数は、削除(n回)と置換(2000n回)、転置(n-1回)、挿入(2000(n+1)回)の合計4002n+1999回が必要となる 編集距離1または2の文字列の中から既知のクエリの集合を取得する処理をElasticsearchのFuzzy matchを用いる ElasticsearchのFuzzy matchを用いることで、編集距離1または2の文字列の中から修正候補となる既知のクエリの集合を取得する処理の高速化がのぞめます。 Peter Norvig, "How to Write a Spelling Corrector" の実装を一部変更し、ElasticsearchのFuzzy matchを用いたスペルコレクションを実装しました。 # 各クエリの出現頻度を保持する辞書 TERM_FREQ = { "ズボン" : 10 , "リボン" : 5 , "おぼん" : 2 } # Fuzzy queryを実行するためのクエリ ES_QUERY = { "query" : { "fuzzy" : { "keyword" : { "value" : "" , "fuzziness" : "2" } } }, "size" : 20 } # Elasticsearchのインデックス名 ES_INDEX_NAME = "kurasawa_test_index" # Fuzzy matchを用いて編集距離1または2の文字列の中から修正候補となる既知のクエリの集合を取得する関数 def edit (es_client, word): ES_QUERY[ "query" ][ "fuzzy" ][ "keyword" ][ "value" ] = word result = es_client.search(index=ES_INDEX_NAME, body=ES_QUERY) return set (hit[ "_source" ][ "keyword" ] for hit in result[ "hits" ][ "hits" ]) def known (word): return set (w for w in word if w in TERM_FREQ) def correct (es_client, word): candidates = known([word]) or edit(es_client, word) or [word] return max (candidates, key= lambda w: TERM_FREQ.get(w, 0 )) correct(es_client, "ザボン" ) >> "ズボン" 今回、上記の解決策を用いてスペルコレクションを実装した結果の評価結果を以下に記載します。評価方法は、 Peter Norvig, "How to Write a Spelling Corrector" の「Evaluation」の章で紹介されている方法を用いて、独自に準備した正解データ約50件に対して、修正されたクエリが正解データと一致しているかどうかを確認しました。 検索クエリログ 評価結果 過去1週間 かつ 検索回数が100回以上の検索クエリログ 正しく修正できたクエリの割合: 69% 約70%のクエリを正しく修正できていますが、以下のような課題も残っています。 出現回数を保持する辞書に存在しないが、typoではないクエリを誤変換してしまう可能性がある 正しいクエリよりも出現回数が多いtypoクエリを修正候補として返してしまう可能性がある 再検索クエリログを用いたスペルコレクション 各ユーザーの連続する検索クエリのログから起点になるクエリとその直後に発生する再検索クエリを1つのペアとします。このクエリ同士の編集距離と検索ログ内でのペアの発生確率から修正クエリを見つけることを考えます。 例えば以下のような検索クエリログがあったとします。 ユーザーID クエリ 再検索クエリ A ザボン ズボン B ザボン ズボン C ザボン リボン 「クエリ」と「再検索クエリ」をペアとして、発生確率と編集距離を計算します。 (クエリ, 再検索クエリ) 発生確率 編集距離 (ザボン, ズボン) 2/3 1 (ザボン, リボン) 1/3 1 編集距離2以下のペアに限定し、発生確率が最大となる再検索クエリを修正クエリとします。つまり、上記の例では「ザボン」を「ズボン」に修正します。 これらの簡易的な手順を実装すると以下のようになります。 import pandas as pd import Levenshtein def correct (df, word): result = {} for record in df[df[ "query" ]==word].values: query = record[ 0 ] next_query = record[ 1 ] ratio = record[ 2 ] distance = Levenshtein.distance(query, next_query) if distance < 3 : result[next_query] = ratio return max (result, key=result.get) df = pd.DataFrame( data={ "query" : [ "ザボン" , "ザボン" ], "next_query" : [ "ズボン" , "リボン" ], "ratio" : [ 0.6666 , 0.3333 ] } ) word= "ザボン" correct(df, word) >> "ズボン" こちらの手法も、前章で紹介した手法と同様に評価しました。また、正解データも同様のものを用いました。 検索クエリログ 評価結果 過去1週間 かつ 検索回数が100回以上の検索クエリログ 正しく修正できたクエリの割合: 14% 以下の課題により、前章で紹介した手法よりも精度が低くなってしまいました。 過去の検索クエリのログにない未知なtypoクエリを修正することが出来ない ユーザーがtypoに気づかず再検索クエリのログに正しいクエリが存在しない場合に修正することが出来ない さいごに 今回はElasticsearchを用いた検索時のtypoへの対応方法とtypoを修正するためのスペルコレクションについて紹介しました。 検索におけるtypoは、検索結果の精度やユーザー体験を低下させる要因となります。一方で、日本語は漢字、カタカナ、ひらがなといった複数の文字で構成されていることや同音異義語、多様な表記揺れにより、一般的にtypoの修正は難しいとされています。 ZOZOTOWNの検索機能でもtypoへの対応にはまだまだ課題があります。引き続き検索精度の向上に取り組んでいきます。 ZOZOでは一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com 書籍「 Query Understanding for Search Engines 」にも同様な言及がなされています。 ↩
アバター
はじめに こんにちは。基幹システム本部・物流開発部の作田です。現在、ZOZO社内で使用している基幹システムのリプレイスを担当しています。 現在行っているリプレイスでは、既存の基幹システムから発送機能を切り出し、マイクロサービスに移行しています。リプレイスの詳細については、ZOZOBASEを支える発送システムリプレイスの取り組みをご覧ください。 techblog.zozo.com マイクロサービスは発送業務の各作業が完了したことを基幹システムに連携しており、この連携を実現するために Amazon Managed Streaming for Apache Kafka (以降、Amazon MSK)を採用しました。今回は、サービス間のデータ連携にAmazon MSKを採用した理由やAmazon MSKでの実装例と考慮点について紹介します。MySQLなどのリレーショナルデータベースに対してAmazon MSKを用いて非同期でデータを連携する事例として参考になれば幸いです。 目次 はじめに 目次 システム要件と設計 システム要件 発送マイクロサービスが独立して動作可能 更新したデータの即時連携 システム設計 技術選定 選定理由 非機能要件の検証 信頼性と可用性の検証 オフセット情報の消失 Kafka BrokerやMSK Connectの停止 検証によって分かったデメリット 実装例 MSK Connect Event Consumer consume処理 例外発生時の処理 おわりに システム要件と設計 システム要件とそれを満たすために考えた設計を順番に説明します。 システム要件 発送マイクロサービスから基幹システムにデータを連携させるにあたって2つの要件がありました。 発送マイクロサービスが独立して動作可能 更新したデータの即時連携 この2つのシステム要件について簡単に説明します。 発送マイクロサービスが独立して動作可能 発送マイクロサービスは基幹システムに障害が発生しても発送業務を遂行できることを目指しています。そのため、発送マイクロサービスが独立して動作できるシステムを構築する必要がありました。 更新したデータの即時連携 発送マイクロサービスは発送業務の各作業(ピック作業や梱包作業)が完了したことを基幹システムに連携する必要があります。連携されたデータは実績の確認などに使用され、一時的に発送マイクロサービスとデータが異なることは許容できますが結果整合性を担保する必要があります。また、実績の乖離による作業者の混乱を防ぐためデータの連携には即時性が求められました。 これらの要件を満たすには、サービス間を疎結合にして非同期でデータを連携する必要があります。要件を満たすためのシステム概要図は以下の通りです。基幹システムは発送業務に必要なデータを非同期で発送マイクロサービスに連携し、発送マイクロサービスはデータの変更を非同期で基幹システムに連携します。今回は発送マイクロサービスから基幹システムへのデータ連携に焦点を当てます。 システム設計 非同期でデータを連携させる上で、データベースの更新とメッセージングシステムへの書き込みに整合性が必要となります。データベースとメッセージングシステムへそれぞれ書き込みを行った場合、どちらかの処理が失敗するとデータの不整合が発生してしまいます。この問題を解決するために、 Outboxパターン を採用しました。 発送マイクロサービスでのOutboxパターンの実装例を説明します。前提として、データベースには社内で広く使われているAurora MySQLを選定しています。Outboxパターンの考えに基づいて、集約の状態更新(itemsテーブルの更新)とドメインイベントの発行(item_eventsテーブルへの書き込み)を同一トランザクションで行います。そして、メッセージリレーはイベントテーブルに書き込まれた内容をメッセージブローカーに送信します。これにより、データベースの更新とメッセージングシステムへの書き込みの整合性を担保できます。 技術選定 Outboxパターンを実現するために、 Apache Kafka のフルマネージドサービスであるAmazon MSKを採用しました。次の節で、Amazon MSKを採用した理由について説明します。 選定理由 Amazon MSKを採用した理由は、Debeziumコネクタが提供されているApache Kafkaをフルマージドで運用可能だからです。なぜDebeziumやApache Kafkaが必要なのかを順番に説明します。 データベースにMySQLを選定しているため、Outboxパターンの実現にはMySQLのデータ変更を検知してメッセージブローカーにデータを送信する仕組みが必要です。データがいつどのように変更されたかを検出する仕組みのことをChange Data Capture(以降、CDC)と言います。今回はCDCを実現するために Debezium を採用することにしました。Debeziumは様々なデータベースに対してCDCを実現でき、MySQLではbinlogを読み取りコミットされた変更を検知しています。 DebeziumはApache Kafka Connectを使用してデプロイされることが一般的 で、Kafka Connectを使用することでデータソースやデータターゲットへの接続が容易になります。そのため、メッセージリレーにApache Kafka Connect、メッセージブローカーに Apache Kafka を採用しました。Apache Kafkaはストリーミングデータを処理するために必要なアプリケーションの実行や構築ができるプラットフォームです。 そして、Apache Kafkaの運用のコストをできるだけ下げるために、フルマネージドサービスであるAmazon MSKを採用しました。Amazon MSKには、Kafka Brokerを持つMSK ClusterとDebeziumをカスタムプラグインとしたMSK Connectが含まれています。MSK Connectではイベントテーブルの変更を検知して、MSK Clusterにメッセージを送信しています。Event Consumerは、MSK Clusterに対して100ミリ秒間隔でメッセージのポーリングを行い、データベースを更新しています。Amazon MSKによって、できるだけ早く別のサービスにデータ連携するという要件をフルマネージドなサービスで実現できました。 非機能要件の検証 Amazon MSKは社内での導入実績がなかったため、SRE担当者と協力し以下の非機能要件について検証しました。 性能・拡張性 信頼性 可用性 性能・拡張性に関しては、複数のシナリオを考慮した負荷試験を行い、本番相当のデータをinsertし続けてもMSK ConnectがMSK Clusterにデータを送れることを確認しました。信頼性と可用性に関しては次の節で詳しく説明します。 信頼性と可用性の検証 信頼性と可用性を高めるために以下の点について考慮しました。 オフセット情報の消失 Kafka Brokerの停止 MSK Connectの停止 オフセット情報の消失はアプリケーション側で考慮しており、Kafka BrokerやMSK Connectの停止はインフラ側で考慮しているのでそれぞれ説明します。 オフセット情報の消失 Event ConsumerがPartitionのどこまでメッセージを読み込んだかのオフセットはKafka Broker内のトピックで管理しています。このオフセット情報が何かしらの理由で失われてしまった場合を考慮して、 ConsumerConfig.AUTO_OFFSET_RESET_CONFIG に earliest を設定しています。earliestに設定すると、オフセット情報が存在しない場合はPartitionの最初からメッセージを読み直します。これにより全てのメッセージが1回以上読み出されるため、Event Consumerは冪等性を考慮した実装が求められます。本システムでは、イベントテーブルにシーケンス用のカラムを用意し、イベントの発行順序が分かるようにしています。このシーケンス番号を元に処理済みのメッセージかどうかを判断し、重複処理をしないようにしています。 また、MSK Connectもどこまでメッセージを送信したかのオフセットを指定したトピックに保存しています。このオフセット情報が消失した場合、binlogの最初から再度メッセージが送信されますが、Event Consumer側で重複処理をしないように実装しているため問題ありません。 Kafka BrokerやMSK Connectの停止 Kafka BrokerやMSK Connectが何かしらの理由で停止してしまった場合を考慮して、AZ障害を模擬した障害試験を実施しました。MSK Clusterでは3つのBrokerを用いて冗長化し、Partitionごとにデータをレプリしているため、Kafka Brokerの1つが停止してもデータが欠損しません。実際に1つを停止してみましたが、発送マイクロサービスから別のサービスへのデータ連携が滞りなく行われることを確認しました。 MSK ConnectではDebeziumを使用しているため、 1つのタスクしかサポートされておらず 冗長化できません。また、何らかの理由でfailed(=produce処理ができない)の状態で止まってしまった場合は再起動できず、再作成をする必要があります。再作成に15分程度かかりますが、再作成が完了すればデータの欠損なく連携できることを確認しています。頻繁にMSK Connectの再作成が必要であれば、 Airbyte などの他のツールへの乗り換えを検討していきたいです。 検証によって分かったデメリット 検証によって、MSK Connectがfailedの状態で止まった場合や設定を変更した場合に再作成しなくてはならないことが分かりました。MSK Connectの再作成時に発生するデメリットは以下の通りです。 再作成が完了するまでの15分程度はメッセージの送信ができない 再作成時にテーブルに対して読み取りロックが発生する(100万件で1分程度) 今回は、これらのデメリットを容認できたため、Amazon MSKでデータ連携することを決めました。参考としてご覧いただけると幸いです。 実装例 ここからはMSK ConnectとEvent Consumerの実装について紹介します。 MSK Connect Producerの実装は行わず、MSK ConnectにDebeziumを採用することでマネージドにCDCを実現しました。MSK Connectはイベントテーブルのみを監視するように設定し、イベントテーブルごとにトピックを用意しています。これにより、イベントテーブルAの変更はトピックA、イベントテーブルBの変更はトピックBにメッセージが送信されます。 Kafkaは 同一のPartitionでのみ順序を保証している ため、順序保証が必要であれば同一のPartitionにメッセージを入れる必要があります。デフォルトでは監視しているテーブルの主キーのハッシュに基づいて各Partitionにメッセージを振り分けます。今回のケースでは、同じ集約IDのイベントに対して順序保証をしたいため、イベントテーブルの主キーではなく集約IDのカラムに基づいて各Partitionに振り分ける設定を入れています。現時点では、バージョン2.1のDebeziumを使用しているため ComputePartition で設定していますが、廃止予定なので PartitionRouting で設定できるように対応する予定です。 Event Consumer 次に、Event Consumerの実装例を紹介します。使用している言語のバージョンは以下の通りです。 種類 バージョン Java 17 Spring Boot 2.7.1 Spring Boot 2.7.xでKafkaを使用するため、 build.gradle に依存関係を追加します。 implementation 'org.apache.kafka:kafka-clients:3.2.3' implementation 'org.springframework.kafka:spring-kafka:2.9.3' コードの関係は以下のようになっています。今回は EventConsumer.java と KafkaConsumerConfig.java について説明していきます。 app/ |-- EventConsumer.java `-- config |-- KafkaConsumerConfig.java `-- KafkaConsumerSettings.java consume処理 EventConsumer.java では、 @KafkaListenerアノテーション を使用してメッセージのconsume処理を実装しています。consumeメソッドはメッセージが受信できたときに呼び出され、どのパーティションからメッセージが来たのかなどを把握できます。このコード例では受け取ったメッセージをログに出力しています。後続の処理では、メッセージの値をJavaのクラスに変換してデータベースを更新していますが、consume処理とは直接関係ないため割愛します。 @Slf4j public class EventConsumer { @KafkaListener (topics = "${kafka.consumer.topic}" ) public void consume(ConsumerRecord<String, String> record) { log.info( String.format( "Consumed event from %s topic, partition %d : key = %s, value = %s" , record.topic(), record.partition(), record.key(), record.value())); // record.value()に対して処理を行う } } @KafkaListener アノテーションを使用するためには、 @Configuration と @EnableKafka アノテーションが付与されたクラスを作成し、リスナーコンテナーファクトリを用意する必要があります。コード例に記載されている KafkaConsumerSettings クラスでは application.yaml から読み取った環境変数の値を保持しており、 KafkaConsumerConfig クラスではリスナーコンテナーファクトリを提供しています。 KafkaConsumerConfig クラスの consumerFactory メソッドではConsumerに関する設定を定義しており、 getDefaultErrorHandler メソッドでは例外発生時の処理方法を指定しています。 @Slf4j @EnableKafka @Configuration @RequiredArgsConstructor @EnableConfigurationProperties ({KafkaConsumerSettings. class }) public class KafkaConsumerConfig { private final KafkaConsumerSettings settings; @Bean KafkaListenerContainerFactory<ConcurrentMessageListenerContainer<String, String>> kafkaListenerContainerFactory() { final ConcurrentKafkaListenerContainerFactory<String, String> factory = new ConcurrentKafkaListenerContainerFactory<>(); factory.setConsumerFactory(consumerFactory()); factory.getContainerProperties().setPollTimeout( 3000 ); factory.setCommonErrorHandler(getDefaultErrorHandler()); return factory; } private ConsumerFactory<String, String> consumerFactory() { final HashMap<String, Object> props = new HashMap<>(); props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, settings.getBootstrapServers()); props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, settings.getAutoOffsetReset()); props.put(ConsumerConfig.GROUP_ID_CONFIG, settings.getGroupId()); props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer. class ); props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, StringDeserializer. class ); return new DefaultKafkaConsumerFactory<>(props); } private DefaultErrorHandler getDefaultErrorHandler() { // 3秒から始まり2倍ずつ増えていく。最大値は30秒 // ex) 3, 6, 12, 24, 30, 30, 30, ... final var backOff = new ExponentialBackOff( 3000L , 2 ); // 合計で1分経過したら再試行を停止する backOff.setMaxElapsedTime( 60000L ); final var defaultErrorHandler = new DefaultErrorHandler( (consumerRecord, exception) -> { log.error(exception); // 処理できなかったメッセージをデータベースに保存する }, backOff); // SQL周りで発生した例外だけリトライする defaultErrorHandler.addNotRetryableExceptions(Exception. class ); defaultErrorHandler.addRetryableExceptions(SQLException. class ); return defaultErrorHandler; } } 例外発生時の処理 getDefaultErrorHandler メソッドでは、SQL周りで例外が発生した場合のみ処理をリトライするように設定しています。リトライ時は、エクスポネンシャルバックオフを指定し、書き込み先のデータベースに過度な負荷がかからないようにしています。リトライ試行期間が合計1分を超えたら DefaultErrorHandler で定義した処理が実行されます。DefaultErrorHandlerで定義した処理にも失敗した場合は、オフセットがコミットされないので再び同じメッセージをポーリングします。この仕組みによって、書き込み先のデータベースがダウンしているときは、永遠に同じメッセージを処理し続けるようにしてデータの連携が止まるようにしています。 メッセージが処理できなかった場合は、受け取ったメッセージをそのままデータベースに保存するようにDefaultErrorHandlerで設定しています。 dead-letter用のトピックにメッセージを送信する方法 が一般的で、デットレタリングされたメッセージを元のトピックに戻すなどのリカバリー処理が容易です。しかし、処理できなかったメッセージを直接確認するためにはトピックからメッセージを受け取る仕組みを作成する必要があり、データベースと比べて手間がかかります。初期リリース時は処理できなかったメッセージの確認をすぐに行いたかったため、データベースにメッセージを保存するという選択をしました。 おわりに 発送マイクロサービスの初期リリースが完了し、現在は既存の基幹システムから段階的に移行しています。この過程では、引き続き既存の基幹システムを使用しながら、発送マイクロサービスで少しずつ発送処理をしています。現時点では、データ処理量が限定的なのでデータの連携は数秒ほどで完了しています。負荷試験において、本番環境と同量のデータが処理できることを確認しましたが、既存システムと同量のデータを持続的に処理したわけではありません。処理するデータ量が増えたときに問題点が見つかれば改善していきたいです。 本記事では、マネージドサービスであるAmazon MSKを用いてMySQLに対してCDCを実現する事例を紹介しました。Amazon MSKはOutboxパターンでドメインイベントを伝える仕組みと相性が良いため、イベント駆動型アーキテクチャでの導入を検討してみてはいかがでしょうか。 ZOZOの基幹システムの開発・リプレイスに興味を持ってくださった方は、以下のリンクからぜひご応募ください。 https://hrmos.co/pages/zozo/jobs/0000150 hrmos.co
アバター
はじめまして、ZOZOTOWNアプリ部Android1ブロックの池田一成です。普段はZOZOTOWN Androidアプリ開発を担当しています。 ZOZOTOWNアプリは歴史の長いアプリのため、レガシーなコードがいくつか残っています。そのため、Android Lintで検出されるビルドワーニングが複数放置されたままの状態になっていました。これらのビルドワーニングは潜在的なバグを生み出す可能性やメンテナンスコストを増加させる可能性があります。ZOZOTOWNアプリにおいても機能改修をした際に新たに発生したビルドワーニングを検知できず、リリース後不具合に繋がってしまったことがありました。本記事では、JetBrains製の Qodana という静的解析ツールを用いた既存のビルドワーニングの可視化と新規のビルドワーニングを発生させない仕組みづくりについての取り組みをご紹介します。 Qodanaとは 導入背景 Qodanaを導入する 構成ファイルの準備 GitHub Actionsへの導入 計測結果の可視化 Qodanaを活用した取り組み 重大度の高いビルドワーニングへの対応 新規のビルドワーニングの検知 まとめ Qodanaとは Qodanaは各種CIツールと連携可能なコード品質プラットフォームで、Java、JavaScript、PHP、Kotlin、Go、C# など60以上の言語で記述されたコードの問題を検出、解析、および解決できます。 公式ブログ より抜粋したQodanaに委任できるタスクの一覧を以下に引用します。 コードの問題を早期にキャッチ。コードが実際に本番環境にプッシュされる前に問題を解決できます。 後で見つかった問題を解決するのはコストが高くつきます。 異常なコードを検出。プロジェクトには一般的でない方法で記述されたコード箇所は、プロジェクトのセキュリティリスクになる可能性があります。 コードレビューを自動化。未使用のインポート、複製、スペルや書式の問題など、複数のチェックを自動化し、フィードバックループにかかる時間を短縮できます。 デッドコードを除去。これにより、無関係な処理の実行をなくし、プログラムの実行時間を短縮できます。 コンプライアンスリスクを低減。Qodana のライセンス監査でプロジェクトが使用している依存関係を追跡できます。 追跡することで、ライセンス要件の準拠を維持しやすくなります。 コード構造を改善。コードを読みやすく、メンテナンス性の高いものにします。 Qodana なら、インデント、名前付けスキーム、行の長さ制限など、コードの一貫性を確保できます。 コーディングのベストプラクティスを導入。プロジェクトやビジネスの要件に基づき、コードを独自のコードポリシー(特定のプログラミングスタイルガイドなど)に準拠させることができます。 導入背景 ZOZOTOWN Androidアプリには既にたくさんのビルドワーニングが存在しています。そのため、Android Lintではアプリの改修等で新しく発生した問題の検知が難しく、アプリ全体のビルドワーニングの可視化と新規のビルドワーニングを検知する仕組みが必要でした。Qodanaの機能の1つにビルドワーニングの種別 1 をサンバースト図として表示する機能があります。この機能を活用することで、前述した課題を解決し、下記の目的を達成できると判断したためQodanaを導入することにしました。 アプリ全体のビルドワーニングの数や種類の可視化 新規のビルドワーニングの発生を検知 また、ZOZOTOWN Androidチームでは静的解析ツールに ktlint と Android Lint を導入しています。ktlintはKotlinコードのスタイルガイドに従ってコードを検証し、一貫性のあるスタイルを保つのに役立ちます。主にコードフォーマットやスタイルに関連する問題を検出するために導入しています。Android LintはAndroidプロジェクト内で一般的なバグや問題を検出するためのツールとして、非推奨なAPIの使用、リソースの問題、潜在的なメモリリークなどを検出するために導入しています。ktlintとAndroid Lintは、それぞれコードのスタイルやAndroidアプリの品質向上のための優れたツールですが、下記の観点から並行してQodanaを導入することにしました。 JetBrainsが設計した独自の検査により総合的な静的解析を提供するため、Android Lintとktlintでは見逃されるかもしれない幅広い問題やバグを検出できる Qodanaはカスタマイズ性が豊富であり、プロジェクトのニーズに合わせてチェックを調整できる。そのため既存のktlintやAndroid Lintルールと組み合わせて、さらに厳格な検証ができる Qodanaを導入する ZOZOTOWN AndroidチームではGitHub Actionsを利用しているため、その導入手順を紹介します。 公式ドキュメント が非常に整っていたため、簡単に導入できました。 構成ファイルの準備 プロジェクトのroot配下にqodana.ymlを作成します。このファイルをカスタマイズすることで解析の条件や使用するリンターを設定できます。 version : "1.0" linter : jetbrains/qodana-jvm-android:2023.2 profile : name : qodana.recommended projectJDK : 11 今回はAndroidのプロジェクトを解析するため、リンターには qodana-jvm-android を設定します。このリンターを設定することでJavaやKotlin、Gradleなど複数の言語の解析ができます。その他にはPHPやPython、Goなどのリンターが用意されています。リンターの詳細な内容については公式ドキュメントの 「Linters」ページ をご参照ください。 profileには starter と recommended を設定できます。 starter は重要なチェックのみ使用され、プロジェクトの初回スキャンに最適です。 recommended はほとんどのプロジェクトに幅広く適した事前選択済みのインスペクション一式を有効にする設定と説明されています。ZOZOTOWNではより詳細な解析をするため recommended を利用しています。profileの詳細な内容については公式ドキュメントの 「Inspection profiles」ページ をご参照ください。 GitHub Actionsへの導入 Qodanaには過去の結果と比較できる ベースライン機能 があります。これにより、新しい問題、変更されていない問題、解決された問題を確認できます。ZOZOTOWN Androidチームではこのベースラインを利用して運用しています。そのJobは下記のようになります。 qodana : runs-on : ubuntu-latest steps : - uses : actions/checkout@v3 - name : calculate previous run number env : NUM : ${{ github.run_number }} run : | echo "GITHUB_PREVIOUS_RUN_NUMBER=$(($NUM - 1))" >> $GITHUB_ENV - name : Download artifact id : download-artifact uses : dawidd6/action-download-artifact@v2 with : run_number : ${{ env.GITHUB_PREVIOUS_RUN_NUMBER }} if_no_artifact_found : ignore - uses : JetBrains/qodana-action@v2023.2 with : args : --baseline,./qodana-report/results/qodana.sarif.json upload-result : true - name : Deploy to GitHub Pages uses : peaceiris/actions-gh-pages@v3 with : github_token : ${{ secrets.GITHUB_TOKEN }} publish_dir : ${{ runner.temp }}/qodana/results/report destination_dir : ./docs/qodana 本来、ベースライン機能を利用する場合、比較対象となる qodana.sarif.json 2 をプロジェクトのroot配下に置いてQodanaを実行する必要があります。しかし、そのような運用をGitHub Actionsで行うとGitのリポジトリサイズが増加したり、定期的なjsonファイルの更新が必要になります。そのため、他のWorkflowやJobのArtifactsの取得を可能にする action-download-artifact というライブラリを利用することにしました。これにより、CI上でArtifactsにアップロードされた qodana.sarif.json を参照できるようにしました。 このJobでは現在実行している run_number から前回の実行時の run_number を計算し、前回のArtifactsを取得することでベースライン機能を利用しています。これにより前回実行時との差分を新規のビルドワーニングと既存のビルドワーニングとしてダッシュボードに出力できます。ダッシュボードについては後述します。 計測結果の可視化 Qodanaで計測されたデータをGitHub ActionsのArtifactsに保存し、GitHub Pagesにホストすることで計測結果を可視化しました。サンプルプロジェクトでの計測結果を用いて簡単にダッシュボードの使い方を紹介します。 ACTUAL PROBLEMS:この計測でQodanaが検出したビルドワーニングを表示します BASELINE:以前の実行からそのまま残っているビルドワーニングを表示します タブを切り替えることで、この計測において新たに検知したビルドワーニングと前回の計測からそのまま残っているビルドワーニングを分けて確認できます。また、ビルドワーニングの種類や言語、重大度によってビルドワーニングをフィルターして表示できます。 画像のように、ビルドワーニングの発生しているファイル名や内容、行数を確認できるためプロジェクト全体のビルドワーニングの把握がしやすくなり、スムーズに修正に取り掛かることができます。 Qodanaを活用した取り組み ここからは、ZOZOTOWN Androidチームにおけるビルドワーニングへの取り組みを紹介します。 重大度の高いビルドワーニングへの対応 プロジェクト内に存在する多くのビルドワーニングに対応するため、QodanaのSeverity(重大度)による分類をもとに優先順位をつけて修正しました。 具体的には、優先度の高いビルドワーニング対応のチケットをファイル単位で作成し、そのファイルに存在するビルドワーニングを優先度に関わらずすべて修正する方針で進めました。このように進めることで修正による影響範囲を抑えつつ、優先度の高いビルドワーニングを着実に減らしながら、全体のビルドワーニング数も効率的に減らせるようにしました。 新規のビルドワーニングの検知 ZOZOTOWN Androidチームでは2つの方法でQodanaを実行するように仕組み化しています。 1つ目は機能実装するfeatureブランチのベースブランチであるdevelopブランチに変更がプッシュされたタイミングで実行しています。これは、機能実装の間で発生したビルドワーニングの変化を確認したいためです。これをしておくことで、後述するfeatureブランチで実行する仕組みと合わせて差分が確認できるようになります。 2つ目は、Pull Requestに特定のラベルを貼ったタイミングで実行しています。これはfeatureブランチでQodanaを実行したい時に行います。このJobでは1つ目で実行されたQodanaの計測結果を取得しベースライン機能を活用して、developブランチとfeatureブランチ間の計測結果の差分を表示できるようにしています。こうすることにより、featureブランチで発生したビルドワーニングをダッシュボードで確認できるようになります。また、Qodana実行の際の引数に post-pr-comment を渡すことで下記のような計測結果のサマリーをコメントしてくれるためレビュー時の見落としが減るかと思います。PRが作成されたタイミングやプッシュされたタイミングでQodanaを実行する方法も検討しましたが、実行時間が長いため、ラベルで実行を選択できるようにしました。 MeasureWarning : name : Measure the warning if : github.event.label.name == 'ワーニング計測' runs-on : ubuntu-latest steps : - uses : actions/checkout@v3 - name : Download artifact id : download-artifact uses : dawidd6/action-download-artifact@v2 with : workflow : code_quality.yml - name : Qodana uses : JetBrains/qodana-action@v2023.2 with : args : --baseline,./qodana-report/results/qodana.sarif.json upload-result : true - name : Deploy to GitHub Pages uses : peaceiris/actions-gh-pages@v3 with : github_token : ${{ secrets.GITHUB_TOKEN }} publish_dir : ${{ runner.temp }}/qodana/results/report destination_dir : ./docs/qodana まとめ 本記事では、ZOZOTOWN AndroidにおけるQodanaを用いたビルドワーニングへの取り組みを紹介しました。Qodanaの導入によりビルドワーニングの可視化と定期的な計測ができるようになりました。ZOZOTOWNアプリ内のビルドワーニングの数は導入前と比較すると減少しているものの、まだまだたくさん残っています。そのため、今後はビルドワーニングを0件にする運用の検討を進めていきたいと考えています。Qodanaの導入を検討している方がいれば、ぜひ参考にしてみてください。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co 本記事のビルドワーニングはAndroid Studio内の問題のあるコードが黄色でハイライト、重大な問題の場合は、コードに赤色の下線が引かれるポップアップテキストを示します。 ↩ Qodana実行時に出力されるSARIF仕様に従ってフォーマットされたJSONファイル形式のレポート ↩
アバター
はじめに こんにちは、ブランドソリューション開発本部バックエンド部SREブロックの小林( @mirai_kobaaaaaa )です。普段は WEAR や FAANS というサービスのSREとして開発、運用に携わっています。 WEARではAmazon Elastic Kubernetes Service(以下、EKSと呼ぶ)を用いて複数システムのインフラ基盤を構築・運用しています。その中の1つとして、ワークフロー処理の実行基盤が存在しています。 本記事では、そのワークフロー実行基盤が抱えていた課題と、それらをどのように解決したのかを紹介します。また、付随して得られたメリットについても紹介いたします。 目次 はじめに 目次 WEARにおけるワークフロー ワークフロー処理内容 ワークフロー実行基盤の構成 ワークフロー実行基盤の課題 コスト内訳の調査 過剰なPodスペック Fargate実行時間の増大 ワークフロー実行基盤の改修方針 EKS on EC2へのリプレイス ワーカーノードのプロビジョニング方法 各Podのノード配置戦略 ワーカーノードのインスタンスタイプ選定 ワーカーノードのスケーリングプロダクト選定 スケーリングプロダクトの概要 選定ポイントと選定結果 リプレイス作業 EKSクラスターの作成 Operator系Podの配置 Karpenterの設定 切り替え 結果 その他影響 終わりに WEARにおけるワークフロー まずは、WEARにおけるワークフローとは何か、どのような構成だったのかを紹介します。 ワークフロー処理内容 WEARで運用しているワークフロー実行基盤は、例えば以下のような処理を行っています。 コーディネート情報の更新 アイテム情報の更新、紐付け ユーザー情報の更新 これらは決まった時間に実行されるスケジュールワークフローとして稼働しています。処理内容によって数分で終わるものから数時間かかるものが存在しており、1日あたり約1500件が実行されています。 ワークフロー実行基盤の構成 前述の通り、ワークフロー実行基盤はEKSを用いて構築されていました。また、Pod実行基盤としてはAWS Fargate(以下、Fargateと呼ぶ)を採用していました。Fargateで実行されるPodは自動的にワーカーノードがプロビジョニングされるため、運用負荷を減らせると考えたからです。 ワークフロー実行時には子タスクとして1つ以上のJobが起動します。Job実行時には単一のPodがプロビジョニングされます。ワークフローの内容によっては複数のJobが実行されることもあります。 また、このEKSにはワークフローシステム以外に各種Kubernetes Operator(以下、Operatorと呼ぶ)が存在していました。GitOpsで利用するArgo CDやAWSのElastic Load Balancingを管理するためのAWS Load Balancer Controller等です。 ワークフロー実行基盤の課題 ワークフロー実行基盤が抱える最たる課題はコストでした。ワークフロー数や実行回数が増えるにつれ、EKS内でプロビジョニングされるPodも単純増加し、比例してコストも増加していきます。 WEARにおけるコーディネート情報更新数やアイテム処理件数の増加はサービス拡大と同意義です。しかし、サービス拡大とサービスを支える基盤コストが単純比例してしまう構成では、サービス成長を鈍化させる要因の1つとなってしまいます。 これらの理由から、コスト効率性の高い基盤を構築することが急務と考え、ワークフロー実行基盤の改修を決定しました。 コスト内訳の調査 ワークフロー実行基盤において、まずは何がコストセンターになっているかを把握する必要がありました。そのため、AWS Cost Explorerや各種ログ、メトリクスを用いてコスト内訳を調査しました。 その結果、下記要因によりコスト増加を引き起こしていることがわかりました。 過剰なPodスペック Fargate実行時間の増大 Fargateコストについても触れながら、それぞれの要因について説明していきます。 過剰なPodスペック Fargateは、割り当てられたスペック、実行時間、Pod数に応じて課金されます。つまり、Podに割り当てられたスペックが大きいほど、コストも増加していくことになります。 CloudWatchやDatadogを確認したところ、Podに割り当てられたスペックと実際に消費されるメトリクスに乖離がありました。これにより不要なコストが発生していました。 この要因については、各Podに対して適切なリソースを割り当て直すことで早々に改善が見込めると考えました。 Fargate実行時間の増大 続いて、Fargate実行時間の増大についてです。 先に述べた通り、Fargateコストは実行時間にも比例します。実行時間はイメージのダウンロードを開始した時間からPodが終了するまでの時間を指します。 Kubernetesログを確認したところ、1Pod起動にあたり25秒程度のイメージダウンロード時間がかかっていました。1日に起動されるPod数は1500以上であるため、1日あたりのイメージダウンロード時間は約10時間にも及びます。 Events: Type Reason Age From Message ---- ------ ---- ---- ------- Normal Pulling 27s kubelet Pulling image "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/workflow:latest" Normal Pulled 2s kubelet Successfully pulled image "xxxxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/workflow:latest" in 25.246397319s Normal Created 2s kubelet Created container workflow Normal Started 2s kubelet Started container workflow この問題を解決するため、まずはイメージサイズの削減を検討しました。WEARはモノリシックなアプリケーションとして構成されており、イメージ自体が大きくなりがちでした。そのため、このアプローチが有効であると考えました。 イメージサイズの削減方法としてはzstdによるイメージ圧縮方法の変更を検討しました。zstdはMeta社によって開発された圧縮アルゴリズムであり、使用していたgzip圧縮より圧縮率と解凍速度の向上が見込めたからです。 zstd圧縮テストの結果、イメージサイズを削減できました。しかし、私たちのケースではイメージダウンロード時間へ与える影響は極小規模に留まったため、別のアプローチを検討することにしました。 余談ですが、zstd圧縮アプローチについてはAWS公式よりブログが公開されています。イメージサイズにお困りの方は是非ご覧ください。 aws.amazon.com 最終的に選ばれたのは、EKS on FargateからEKS on EC2へのリプレイスでした。ワーカーノードとしてEC2を使用することで、イメージキャッシュを用いたイメージダウンロード時間の削減が期待できます。 また、ワークフローの性質上、Podは頻繁に入れ替わることが予想されます。そのため、ピーク時の同時実行数をもとにEC2サイズを決定することで、Fargateと比べてより少ないワーカーノードでワークフローを処理でき、ワーカーノードのコスト削減が可能だと考えました。 ワークフロー実行基盤の改修方針 各要因へのアプローチ方法を検討した結果、下記方針で進めることにしました。 Podに割り当てられたスペックの再調整 EKS on FargateからEKS on EC2へリプレイス まずは、1つ目のPodに割り当てられたスペックを再調整しました。 計測された実績値に基づいて各Podに割り当てられたスペックを調整します。テスト等を挟みましたが、作業自体は数日で完了しました。 2つ目のEKS on FargateからEKS on EC2へのリプレイスについては、より詳細に設計しました。 EKS on EC2へのリプレイス EKS on EC2へリプレイスするにあたり、下記を決定しました。 ワーカーノードのプロビジョニング方法 各Podのノード配置戦略 ワーカーノードのインスタンスタイプ選定 ワーカーノードのスケーリングプロダクト選定 ワーカーノードのプロビジョニング方法 最初にワーカーノードをどのようにプロビジョニングするかを考えました。EKSでEC2をノードとして利用する場合、大きく分けてセルフマネージドノードとマネージドノードの2種類があります。セルフマネージドノードは自身で作成したEC2インスタンスをノードとして利用し、必要に応じてAmazon EC2 Auto Scalingを作成、管理します。一方マネージドノードは、AWSによってEC2インスタンスが自動作成され、それに紐づくAmazon EC2 Auto Scalingもプロビジョニングされます。 今回はセルフマネージドノードを使用するほどのカスタマイズ性が必要ないこと、既に別クラスターで運用経験があったことからマネージドノードを採用しました。また、ワークフローの途中停止リスクを下げたいため、スポットインスタンスではなくオンデマンドインスタンスを使用することにしました。 各Podのノード配置戦略 2つ目は、どのPodをどのワーカーノードに配置すべきかという問題です。先に述べた通り、ワークフロー実行基盤のEKSクラスター内にはArgo CD等のOperatorも存在しています。Operatorと同様のワーカーノードにPodを配置することで、ノード数を削減し、コストがより圧縮できます。しかし、Operator系Podによるノードレベルの影響を避けたいと考え、ワークフロー実行PodはOperatorとは別のワーカーノードに配置しました。 ワーカーノードのインスタンスタイプ選定 続いて3つ目に記載したワーカーノードのインスタンスタイプ選定です。調査でも述べましたが、ワークフローの性質上、常に一定のPodが実行されているわけではありません。そのため、ピーク時の同時実行数や負荷傾向をもとにインスタンスタイプを決定しました。 ちなみに、現在は後述するKarpenterの採用によってインスタンスタイプを細かく管理していません。ワークロードの負荷傾向から最適となるようインスタンスファミリーを選択するのみに留まっています。 ワーカーノードのスケーリングプロダクト選定 最後に、ワーカーノードのスケーリングプロダクト選定です。Fargateは1つのPodに対して1つのワーカーノードが自動でプロビジョニングされます。つまり、Horizontal Pod Autoscaler等を用いてPod数を増加させることでワーカーノードを含めた水平スケーリングが可能です。 しかし、EC2をワーカーノードとして使用する場合、Podのスケーリングだけでなくワーカーノードのスケーリング方法も考慮する必要があります。今回はEKSで利用できるスケーリングプロダクトを調査し、比較の上選定しました。 スケーリングプロダクトの概要 EKSでは下記2つのスケーリングプロダクトをサポート 1 しています。 Cluster Autoscaler Karpenter Cluster AutoscalerはKubernetesが公式で用意しているスケーリングプロダクトです。EKSにおける動作としてはマネージドノードグループ及びAuto Scaling Groupを書き換えることで、EC2インスタンスを追加起動し、起動プロセス完了後にEKSへワーカーノード登録をします。 一方で、KarpenterはOSSとして公開されているスケーリングプロダクトであり、Cluster Autoscalerとは異なる方法でスケーリングを行います。Karpenterは待機中Podのリソースリクエストに応じて必要なワーカーノードのサイズを計算し、必要に応じてワーカーノードを追加、削除します。この時、Auto Scaling Groupは利用しません。ワーカーノード追加時には、待機中Podの情報をもとにした必要な容量の計算、要求を満たすEC2インスタンスの選択と起動、EKSへのワーカーノード登録をします。 ( https://karpenter.sh/ より引用) 選定ポイントと選定結果 選定のポイントとして、スケーリング速度に重点をおきました。ワークフロー実行基盤において、スケーリング速度は非常に重要です。スケーリングに時間がかかってしまえば、ワークフロースケジュール全体、ひいてはサービスへの悪影響が発生してしまいます。 下記はPodがスタートするまでの比較結果 2 です。 イメージキャッシュなし はワーカーノード追加と読み替えてください。Fargateはワーカーノード追加も同時に行うため、 イメージキャッシュなし としています 3 。 スケーリングプロダクト イメージキャッシュなし イメージキャッシュあり Cluster Autoscaler 70s 3s Karpenter 60s 4s Fargate 57s - EC2インスタンスをワーカーノードとして登録する工程が発生するため、Fargateが優位だろうと推測していました。しかし、意外にもKarpenterも十分な速度を有していました。また、イメージキャッシュが存在する場合のPod起動速度はやはりEC2が非常に高速です。 検討を重ねた結果、Karpenterを採用しました。決め手となったのは、必要なリソースリクエストに応じて柔軟にEC2インスタンスを選択する機能を有していたからです。これは今回の課題に対して非常に魅力的でした。 リプレイス作業 最終的な構成はこのようになりました。 Operator系Podはcommonというマネージドノードグループに配置します。CPU使用率の低いPodがほとんどであり、極稀に負荷が上がるもののバーストクレジットで対応可能と考えT系インスタンスを使用しています。 Karpenterは普段から一定の処理をしており、かつ本構成の心臓部分です。そのため、安定したワークロードを実行できるようにM系インスタンスと独立したマネージドノードグループで構成しています。 実際にワークフローが実行されるワーカーノードの管理はKarpenterに委ねています。スケーリングプロダクト選定でも述べた通り、Karpenterは待機中Podのリソース要求をもとに最適なインスタンスタイプを計算、決定し起動します。 EKSクラスターの作成 既存環境への影響を考慮し、Blue/Greenデプロイメントを利用してクラスターを切り替えることにしました。そのため、新しいクラスターを作成する必要があります。以下は terraform-aws-modules/eks/aws を用いたクラスター設定例です。Submoduleの eks-managed-node-group を使用し、マネージドノード設定も記述しています。 # main.tf module "workflow_cluster" { source = "terraform-aws-modules/eks/aws" version = "19.6.0" cluster_name = "workflow-cluster" cluster_version = 1 . 24 vpc_id = var.vpc_id subnet_ids = var.subnet_ids enable_irsa = true # Karpenterがこのタグを見てノードを立ち上げる node_security_group_tags = { "karpenter.sh/discovery" = "workflow-cluster" } # Submoduleのeks-managed-node-groupを利用 eks_managed_node_groups = { # Operatorを設置するマネージドノードグループ common = { name = "common" desired_size = 2 min_size = 2 max_size = 6 instance_types = [ "t3.large" ] disk_size = 20 capacity_type = "ON_DEMAND" iam_role_additional_policies = { AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" } labels = { NodeGroupName = "common" } } # Karpenter本体を設置するマネージドノードグループ karpenter = { name = "karpenter" desired_size = 2 min_size = 2 max_size = 6 instance_types = [ "m5.large" ] disk_size = 20 capacity_type = "ON_DEMAND" iam_role_additional_policies = { AmazonSSMManagedInstanceCore = "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" } labels = { NodeGroupName = "karpenter" } } } } Operator系Podの配置 構成図の通り、マネージドノードグループが2種類存在するため、適したPodを適したノードグループに配置する必要があります。そのため、各OperatorのmanifestにNode Affinityを設定し、Podを配置するノードを宣言的に選択します。私たちはArgo CD経由でHelm Chartを使用していたため、helm valuesに設定を追加します。 helm : values : | affinity : nodeAffinity : requiredDuringSchedulingIgnoredDuringExecution : nodeSelectorTerms : - matchExpressions : - key : NodeGroupName operator : In values : - common この設定により、NodeGroupNameというLabelにcommonという値が設定されたノードにのみPodが配置されます。 Karpenterの設定 続いてKarpenterの設定です。KarpenterがEC2を起動できるようIRSAを作成します。 # main.tf # Karpenter用のIRSAを作成 module "karpenter_irsa" { source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks" version = "5.14.0" role_name = "workflow-cluster-karpenter-controller" attach_karpenter_controller_policy = true karpenter_tag_key = "karpenter.sh/discovery" karpenter_controller_cluster_id = module.workflow_cluster.cluster_name karpenter_controller_node_iam_role_arns = [ module.workflow_cluster.eks_managed_node_groups [ "karpenter" ] .iam_role_arn ] oidc_providers = { ex = { provider_arn = module.workflow_cluster.oidc_provider_arn namespace_service_accounts = [ "karpenter:karpenter" ] } } } # Karpenterが起動するEC2インスタンスにアタッチするインスタンスプロフィール resource "aws_iam_instance_profile" "karpenter" { name = "workflow-cluster-KarpenterNodeInstanceProfile" role = module.workflow_cluster.eks_managed_node_groups [ "karpenter" ] .iam_role_name } Karpenter本体をArgo CDのApplicationとしてデプロイします。 # karpenter.yaml apiVersion : argoproj.io/v1alpha1 kind : Application metadata : name : karpenter namespace : argocd finalizers : - resources-finalizer.argocd.argoproj.io spec : destination : namespace : karpenter server : https://kubernetes.default.svc project : default source : chart : karpenter repoURL : public.ecr.aws/karpenter targetRevision : v0.27.0 helm : releaseName : karpenter parameters : - name : 'settings.aws.clusterName' value : 'workflow-cluster' - name : 'settings.aws.defaultInstanceProfile' value : 'workflow-cluster-KarpenterNodeInstanceProfile' - name : 'settings.aws.clusterEndpoint' value : 'https://xxxxxxxxxxxxxxxxxxxxxx.gr7.eu-west-1.eks.amazonaws.com' - name : 'serviceAccount.annotations.eks\.amazonaws\.com/role-arn' value : workflow-cluster-karpenter-controller values : | affinity : nodeAffinity : requiredDuringSchedulingIgnoredDuringExecution : nodeSelectorTerms : - matchExpressions : - key : karpenter.sh/provisioner-name operator : DoesNotExist - matchExpressions : - key : NodeGroupName operator : In values : - karpenter syncPolicy : syncOptions : - CreateNamespace= true automated : prune : true KarpenterはProvisionerというCustom Resourceを用いてワーカーノードの管理をします。インスタンス構成やネットワーク設定を指定することで、起動するインスタンスの制御が可能です。 # provisioner.yaml # KarpenterがAWSに立ち上げるEC2インスタンスの設定 apiVersion : karpenter.sh/v1alpha5 kind : Provisioner metadata : name : workflow-provisioner spec : # 起動するワーカーノードにworkflowというlabelを設定 labels : NodeGroupName : workflow requirements : - key : karpenter.k8s.aws/instance-category operator : In values : [ m, r ] - key : karpenter.sh/capacity-type operator : In values : [ "on-demand" ] - key : kubernetes.io/os operator : In values : - linux - key : kubernetes.io/arch operator : In values : - amd64 provider : # EC2を起動するサブネットやセキュリティグループ、タグを指定 subnetSelector : karpenter.sh/discovery : workflow-cluster securityGroupSelector : karpenter.sh/discovery : workflow-cluster tags : karpenter.sh/discovery : workflow-cluster # DaemonSet以外のPodが存在しない状態が30秒続くとワーカーノードを削除する ttlSecondsAfterEmpty : 30 最後に、ワークフロー実行PodがKarpenter経由で起動したワーカーノードに配置されるようNode Affinityを設定します。 # workflow.yaml spec : affinity : nodeAffinity : requiredDuringSchedulingIgnoredDuringExecution : nodeSelectorTerms : - matchExpressions : - key : NodeGroupName operator : In values : - workflow Node Affinityを設定することで、ワークフロー実行PodはworkflowというLabelが設定されたノードにのみ配置されるようになります。また、ワーカーノードのリソースが枯渇している場合はKarpenterが自動でEC2インスタンス起動、EKSへワーカーノードを登録します。 切り替え 先ほど述べた通り、Blue/Greenデプロイメントを用いた切り替えを行いました。 旧環境のワークフローを停止 新環境のワークフローを開始 停止期間中のワークフローを再実行(必要に応じて) 各種管理コンソールのドメイン切り替え 事前に疎通確認は取れていたこと、ほとんどのワークフローが後続実行でリカバリーできることから、ユーザーへの影響は発生しませんでした。 結果 これらの改修により、ワークフロー実行基盤のコストを大幅に削減できました。青い部分が旧環境の想定コスト、赤い部分が実際のコストです。 特に効果的だったのは、EC2インスタンスへPodを集約したことでした。これにより、効果的にリソースを使用でき、30%程度のコスト削減を実現できました。 その他影響 今回の改修は主にコスト削減を目的としていましたが、パフォーマンスにも影響がありました。特にイメージキャッシュの効果は大きく、連続して複数Podが実行されるようなワークフローでは、実行時間を20%程度高速化できています。 また、ワークロードに最適なEC2インスタンス 4 を割り当てることで処理も高速化され、7時間程度かかっていたワークフローを5時間程度まで高速化できました。 これらはWEARのサービス性質上、非常に喜ばしい結果でした。 終わりに 以上のように、WEARのワークフロー実行基盤を改修する取り組みを進めました。 ワークフロー実行基盤の構築当初は、Fargateを使用することで運用負荷を抑制でき、その時間を他の開発に割り当てできました。しかし、「今のWEAR」から見つめ直した時、違うアプローチを取るべきだと判断しました。このように、実装当時は最適であったものが、サービスの成長や環境変化と共に常に変わることを再認識できました。 現在、ワークフロー実行基盤は安定して稼働しています。引き続きサービスをより良いものにできるようコストとパフォーマンスの両軸で改善を進めていきます。 WEARでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! https://hrmos.co/pages/zozo/jobs/0000021 hrmos.co 2023年8月16日時点の情報です。今後変更される可能性があります。 ↩ 測定結果は環境によって異なります。あくまで参考値としてご覧ください。 ↩ 厳密にいえばFargateは純粋なスケーリングプロダクトではありません。今回は比較のために記載しています。 ↩ EC2インスタンスはインスタンスタイプによってプロセッサやアーキテクチャが異なります。そのため、ワークロードによっては同一vCPU、Memoryのインスタンスであってもパフォーマンス差異が発生します。参考: https://aws.amazon.com/jp/ec2/instance-types/ ↩
アバター
はじめに こんにちは。ブランドソリューション開発本部バックエンド部SREの山岡( @ymktmk )です。普段はファッションコーディネートアプリ「 WEAR 」のSREとしてクラウドの運用やリプレイスをおこなっています。 昨年から、私たちのチームでは分散した技術スタックをKubernetesへ統一するリプレイスプロジェクトを開始し、先月ついにKubernetesへの移行が完了しました。 techblog.zozo.com また、Kubernetesへの段階的な移行と並行して、Kubernetesの柔軟性を活かした運用改善や開発者体験の向上に取り組んできました。その一環として、k6-operatorを活用した負荷試験基盤を作成しました。 本記事ではWEARにKubernetesネイティブな負荷試験基盤を導入した背景とその効果についてご紹介します。Kubernetes環境における負荷試験基盤の導入を検討している方の参考になれば幸いです。 目次 はじめに 目次 導入の背景 負荷試験基盤の設計 要件 負荷試験ツールの選定 選定対象の負荷試験ツールの概要と比較 k6の選定理由 負荷試験基盤のアーキテクチャ GitHub Actionsワークフローを介した負荷試験の実行 負荷試験結果レポートのDatadogダッシュボードの活用 負荷試験の実施フローとその仕組み テストシナリオの作成 テストシナリオのクラスターへの自動適用 負荷試験の実行 負荷試験の結果 導入後の効果 まとめ おわりに 導入の背景 私たちのチームでは、これまで負荷試験に関して、各々が異なる負荷試験ツールを使用しており、統一されていませんでした。そのため、チームにおける負荷試験のノウハウの蓄積が難しく、特に新規メンバーは負荷試験の実施が容易ではありませんでした。テストシナリオも手探りで書く必要があり、開発効率が悪くなっていました。 このような課題を解決し、負荷試験の容易な実施とノウハウの蓄積を可能にするため、負荷試験基盤を導入しました。 負荷試験基盤の設計 要件 負荷試験基盤を構築するにあたって、3つの要件を定めました。 1. 簡素で馴染みのある言語でテストシナリオを記述できること 導入の背景で述べた通り、私たちのチームでは各々が異なる負荷試験ツールを使用しており、チーム内でのノウハウが蓄積されていませんでした。そのため、負荷試験ツールの統一が必要でした。 負荷試験ツールの統一にあたり、負荷試験を実施する開発者が簡素で馴染みのある言語でテストシナリオを記述できることはテストシナリオの作成・共有が容易になり、開発効率の向上が期待できます。 2. 分散負荷試験が可能であること 単一のマシン上で行う負荷試験では、そのマシンの性能の限界に達するまでの負荷しかかけることができません。分散負荷試験は、想定される大きな負荷を再現し、アプリケーションの振る舞いを再現できます。 3. 負荷試験結果をレポートとして残せること 負荷試験結果をレポートとして残すことで、チームメンバーとの負荷試験結果の共有を容易にし、レビュー時においてメンバーが理解しやすくなります。また、過去の負荷試験結果と比較できるため、パフォーマンスの劣化・向上とその要因を調査しやすくなります。 これらの要件を満たすことで、チームの課題を解決できると考えました。そして、適切な負荷試験ツールの選定とインフラ設計をしました。 負荷試験ツールの選定 選定対象の負荷試験ツールの概要と比較 はじめに述べた通り、私たちのチームは全ての基盤をKubernetesに移行することを決定しました。こうした背景から、Kubernetesの特徴である柔軟性を活かして負荷試験基盤を作成しようと考えていました。 そのため、Kubernetes上で実現可能な負荷試験ツールを調査しました。その結果、以下のツールが選定対象として挙げられました。 Gatling Locust k6 それぞれのツールの特徴を見ていきます。 GatlingはテストシナリオをScala(Gatling 3.7からはJavaやKotlinもサポート)のDSLで記述します。社内でも利用実績が豊富であり、GatlingをベースとしたKubernetes Operatorである gatling-operator を開発しています。 techblog.zozo.com LocustはテストシナリオをPythonで記述します。Google Cloud Platformでは、「Google Kubernetes Engineを使用した負荷分散テスト」というテーマでKubernetes環境におけるlocustを使用した分散の負荷試験を紹介しています。 cloud.google.com k6はテストシナリオをJavaScriptで記述します。k6にはKubernetes Operatorである k6-operator が公式から提供されています。 これら3つのツールを要件に基づいて比較した結果が以下です。 言語の馴染み深さ 分散負荷試験 レポート出力 Gatling × ○ ○ Locust ○ ○ ○ k6 ○ ○ ○ k6の選定理由 Gatlingは社内で多くの利用実績がありましたが、テストシナリオをScalaで記述する必要があり、普段Rubyで開発しているWEARのエンジニアには馴染みがありませんでした。そのため、選択肢から外れました。 選定候補として残ったのがLocustとk6の2つでした。どちらも最低限の要件を満たしていましたが、最終的にk6を選択しました。k6を選んだ理由は、私たちのチームでは保守・運用性を重視し、Kubernetes Operatorであるk6-operatorが公式から提供されているk6を選択しました。さらに、チーム内で既にk6の利用実績があったことも選定の決め手となりました。 負荷試験基盤のアーキテクチャ 要件を考慮した上で、負荷試験基盤のアーキテクチャは以下のようになりました。 負荷試験の開始から終了までは以下の流れになります。 GitHub Actionsワークフローを介してk6 Custom Resourceをクラスターに適用 k6-operatorがk6 Custom ResourceをWatchし、Podを起動して負荷試験を開始 Slackに負荷試験結果レポートのDatadogダッシュボードを送信 GitHub Actionsワークフローを介した負荷試験の実行 負荷試験の実行には、k6-operatorが実行されているクラスターにk6 Custom Resourceを適用する必要があります。ただし、k6 Custom Resourceをローカルから手動適用するのは手間がかかります。そのため、GitHub Actionsのワークフローを実行し、k6 Custom Resourceを適用するようにしました。GitHub Actionsを利用することで負荷試験を容易に実施でき、利便性が高いと考えました。 負荷試験結果レポートのDatadogダッシュボードの活用 負荷試験結果レポートはk6が Datadogのインテグレーション に対応していることから、Datadogダッシュボードを利用することにしました。また、私たちのチームでは以前からアプリケーションやDBのモニタリングにDatadogを利用していたため、Datadogダッシュボードを使って負荷試験結果レポートを確認できることは好都合でした。 次に実際に負荷試験を実施する際のフローとその仕組みを説明します。 負荷試験の実施フローとその仕組み テストシナリオの作成 負荷試験基盤の実施者は、テストシナリオを記述します。以下はテストシナリオの簡単な例です。 1人の仮想ユーザーが100秒間、1秒につき1回 https://test.k6.io に対してGETリクエストを送信し、レスポンスのHTTPステータスコードが200であることを確認する負荷試験です。 import http from 'k6/http' ; import check from 'k6' ; export const options = { vus: 1, duration: '100s' , rps: 1, } ; export default function () { const response = http.get( 'https://test.k6.io' ); check(response, { "status is 200" : (r) => r. status === 200 } ); } テストシナリオのクラスターへの自動適用 k6-operatorを使用して負荷試験を実施するには、テストシナリオを記述するだけでは不十分です。実際に負荷試験を行うには、k6-operatorが実行されているクラスターに以下のようなk6 Custom Resourceを適用する必要があります。 apiVersion : k6.io/v1alpha1 kind : K6 metadata : name : k6 namespace : k6-operator-system spec : parallelism : 1 arguments : --out statsd script : configMap : name : scenario file : test01.js runner : env : - name : K6_STATSD_ENABLE_TAGS value : "true" - name : K6_STATSD_ADDR value : "datadog-agent.datadog.svc.cluster.local:8125" k6 Custom ResourceはConfigMapと、それに格納しているテストシナリオのJavaScriptファイルを指定する必要があります。したがって、記述したテストシナリオを元に負荷試験を行うためには、その都度テストシナリオのJavaScriptファイルからConfigMapを作成しなければなりません。 そこで、 kustomize のconfigMapGeneratorを使用してテストシナリオのJavaScriptファイルからConfigMapを動的に生成するようにしました。 apiVersion : kustomize.config.k8s.io/v1beta1 kind : Kustomization configMapGenerator : - name : scenario namespace : k6-operator-system files : - scenario/test01.js - scenario/test02.js - scenario/test03.js - scenario/test04.js generatorOptions : annotations : argocd.argoproj.io/compare-options : IgnoreExtraneous 私たちのチームでは、 Argo CD を使ってKubernetesマニフェストのデプロイを実現しています。そのため、テストシナリオを負荷試験の専用のGitHubリポジトリへPushするだけで、テストシナリオのJavaScriptファイルからConfigMapを動的に生成し、クラスターに適用されます。 負荷試験の実行 GitHub Actionsの workflow_dispatch トリガーを使ってWeb UIからワークフローを実行し、k6 Custom Resourceを適用します。引数を設定する画面にて、テストシナリオであるJavaScriptファイルをプルダウンから選択することで、容易に負荷試験を実施できます。また、負荷試験の実行時に、vus、duration、rps、parallelismなどのパラメータを容易にオーバーライドできるよう実装しています。 ワークフローを実行すると、k6 Custom Resourceがクラスターに適用され、k6-operatorがPodを起動し、負荷試験を行います。この際の負荷試験のメトリクスは、Datadog Agentを介してDatadogに送信されます。 負荷試験の結果 負荷試験が終了すると、負荷試験用のSlackチャンネルに負荷試験結果レポートである、DatadogダッシュボードのURLが送信されます。 URLをブラウザで開くと、Datadogダッシュボード上で負荷試験結果レポートが閲覧できます。 導入後の効果 今回作成した負荷試験基盤の導入により、今年入社した新卒エンジニアなどの新規メンバーでも手軽に負荷試験を実施でき、負荷試験を実施するハードルを下げることができました。 また、負荷試験ツールの統一により、チーム内での負荷試験のノウハウが蓄積され、開発者の開発効率が向上しました。 さらに、今回の取り組みにより、専用のGitHubリポジトリを使用して負荷試験を管理できるようになったことが非常に良かったです。テストシナリオの作成から負荷試験の実行、結果の確認まで、チームレビューが行き届くようになりました。これにより、以前に個人で行っていた時と比べて、負荷試験の妥当性が向上し、心理的な負担も軽減しました。 まとめ 今回は、WEARにおけるKubernetesネイティブな負荷試験基盤の導入とその効果についてご紹介しました。この負荷試験基盤の導入により、負荷試験が手軽に実施できるようになり、負荷試験へのハードルが下がりました。 また、これを機に従来の負荷試験についても見直すことができました。今後も負荷試験基盤の利用者がより使いやすくなるよう継続的に改善していきます。 さらに、今後は負荷試験基盤だけではなく、Kubernetesの柔軟性を活かして運用改善や開発者体験の向上に取り組んでいきます。 おわりに WEARでは一緒にサービスを改善してくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co
アバター
はじめに こんにちは、DevRelブロックの @wiroha です。DevRelの3名による連載「ZOZO TECH BLOGを支える技術」の3本目、最終回として Looker Studio (旧データポータル)を使ったBLOGの数字分析についてご紹介します。 Looker Studioは、データを視覚的にわかりやすいグラフやチャートにして表示するツールです。ZOZO内ではCSレポートの作成や、計測プロダクトのデータ分析などで活用しており、これまでに記事も公開しています。 techblog.zozo.com techblog.zozo.com ZOZO TECH BLOGの数字分析において実際に運用しているレポート画面は下図のとおりです。この画面の作成に至るまでの経緯と作成方法を解説していきます。 現在運用しているレポート画面 目次 はじめに 目次 背景・課題 導入検討 レポート作成の前準備 レポートの作成手順 1. レポートを作成する 2. レポートにデータを追加する 3. コンポーネントを追加する 4. フィールド・フィルタを追加する 5. 共有する 応用編:はてなブックマークの件数を表示する レポートの活用 まとめ 背景・課題 以前までは数字分析にGoogle アナリティクスを導入していたものの、把握したい数字だけを見やすくしたレポート化・可視化までは行っていませんでした。そのため、記事を書いてもどれだけ見られているかの反響を拾いにくいという課題がありました。また、Google アナリティクスは権限を持つ一部の人しか閲覧できないという課題もありました。今年2月に私がDevRel専任として入社し、これらを解決すべく取り組むことにしました。 導入検討 アクセス数・SNSシェア数・SNSコメント内容など把握したい項目を洗い出し、優先度をつけて実現の可否を調査しました。X(Twitter)上でのRT・コメントはリアルタイム性が高いため専用のSlackチャンネルへ流すことにし、任意のタイミングで見たい数字面をレポート化することにしました。レポート化の手段は次の理由でLooker Studioを採用しました。 グラフや表を使ってデータをわかりやすく視覚化できる レポートエディタのUIが優れておりストレスなく作成できる 複数のデータソースに接続してデータを統合できる ZOZOはGoogle Workspaceを使用しているため、作成したレポートを組織内のユーザに共有しやすい 特に複数のデータソースからデータを統合できる点は魅力的です。Google アナリティクス、Google スプレッドシート(以下、スプレッドシートとする)、BigQueryなどさまざまなデータソースからデータを取得し、1つの表に結合して表示できます。利用用途とも合致しており、スムーズに導入が決まりました。 接続可能なデータソース(一部) レポート作成の前準備 レポートの作成には元となるデータソースが必要です。ZOZO TECH BLOGは既にGoogle アナリティクスを導入していました。もし導入していない場合は設定します。はてなブログであれば、はてなブログ ヘルプの「 Google Analyticsを導入する 」ページに詳細の解説があります。 追加でカスタムディメンションの設定も行いました。これにより記事の投稿日やリンククリックの計測などができるようになります。はてな開発ブログの「 はてなブログで Google アナリティクス 4の設定が可能になりました 」を参考にpost_date, link_url, link_textといったディメンションを追加しました。これで前準備は完了です。 追加したカスタムディメンションの一覧 レポートの作成手順 基本的な流れは次のとおりです。 Looker Studio にログインし、レポートを作成する レポートにデータを追加する コンポーネント(グラフやコントロール)を追加する フィールドやフィルタの追加により表示したい内容に更新する 共有する それぞれ画像を含めて詳細を解説します。 1. レポートを作成する Looker Studio にログインすると、空のレポートを作成するか、テンプレートを元にした新規作成ができます。テンプレートから作成しカスタムしていくと手間が少なくおすすめです。 「テンプレートを使って開始」画面 2. レポートにデータを追加する テンプレートを元に作成する場合、上部の「自分のデータを使用」からデータソースを指定し追加できます。 「データのレポートへの追加」画面 3. コンポーネントを追加する 上部の「グラフを追加」を選択すると表・スコアカード・グラフなどのコンポーネントを追加できます。 「グラフを追加」で種類を選択できる Google アナリティクスの数字はディメンションと指標の組み合わせで構成されており、表示内容は画面右側の「グラフ」パネルで設定します。「グラフ」内のディメンション・指標は右端の「データ」パネルにあるフィールドから選択します。 「グラフ」のディメンション・指標で設定 上記画像で「大陸」「地域」のプルダウンになっている部分が「コントロール」と呼ばれるコンポーネントです。デバイスごとに見比べたり、期間を1週間・1か月など切り替えて見たいときに活用できます。 4. フィールド・フィルタを追加する 既存のフィールドや関数を組み合わせて独自のフィールドを追加できます。活用例を見てみましょう。次の図は記事ごとのイベント数を表にしたものです。 このままでも良いのですが、ページタイトルの末尾にすべて「 - ZOZO TECH BLOG」がついており、繰り返しになっています。社内で見る分には自明な情報なのでカットすると次のようになります。 スッキリとして見やすくなりました。これは次の図のようにページタイトルをREPLACE関数で加工したものを新しいフィールドとして保存し、ディメンションに設定することで実現しています。 使用できる関数はLooker Studioのヘルプページ「 関数リスト 」にまとめられています。特にURLへのリンクを付与する HYPERLINK 関数は便利です。 また先ほどの表ではフィルタ機能も使っています。投稿日でソートするために、下図のような設定をしてpost_dateがfalseのデータを除外しました。トップページやカテゴリー画面など投稿日が存在しないページではfalseが入る仕様のようです。グラフパネルの「フィルタを追加」から行います。 グラフパネル設定下部の「フィルタを追加」を選択 「フィルタの編集」で除外条件・一致条件を設定できる フィルタの条件には正規表現も使用できます。こうしてコンポーネント・フィールド・フィルタを追加して見たい内容を作っていけばレポートの作成は完了です。 5. 共有する Google ドキュメントやGoogle スプレッドシートを使ったことがある方なら馴染みのある画面で共有の設定ができます。リンクを知っている組織内のユーザーであれば誰でも閲覧できるようにすると、複雑なユーザ管理を考えなくて済みます。もちろん秘匿性の高い情報のレポートを作成する場合はきちんと制限できます。 アクセス権の管理とリンク設定を行える 応用編:はてなブックマークの件数を表示する 応用編として2つのデータソースを1つの表に結合している例を紹介します。レポート上の「最近の記事」部分のはてなブックマーク数はスプレッドシートから、PV数はGoogle アナリティクスからデータを取得しています。 レポートの「最近の記事」部分 データソースのスプレッドシート スプレッドシート側の数値は はてなブックマーク件数取得API を利用して取得しています。タスク自動化ツールの「 Zapier 」から定期的にAPIを呼び出し、結果を書き込む仕組みです。きちんと実装するのであればプログラムを書いてcronで実行し、APIで取得した結果はデータベースに保存するところですが、なるべくノーコードで実現して保守性を高める方向性にしました。Zapierの使い方を説明すると長くなってしまうため、ここでは割愛します。 スプレッドシートの作成後、Looker Studio上部の「データを追加」から「Google スプレッドシート」を選択して追加します。追加できたら画面上部メニューの「リソース」から「統合を管理」を選択します。「統合を追加」を選択すると、結合の設定画面が開きます。 結合の設定画面 結合条件を設定し、Google アナリティクスとGoogle スプレッドシートのデータを結合します。データソース名をつけて保存すると「混合データ」として追加され、これを元に表やグラフを作成できるようになりました。 「混合データ」が追加されている 応用編を含め、今回紹介したノウハウを駆使すれば冒頭で紹介したレポートが作成できます。同じような課題を抱いている方の参考になれば幸いです。 レポートの活用 Looker Studioの導入によって数字状況を追いやすくなり、反響の多かった記事から派生してイベントを開催するなど次のアウトプットにもつながるようになりました。 レポートを活用し、執筆者へフィードバックする試みも新たに始めました。PV数・はてなブックマーク数・記事に対するコメントなどを共有することで「書いて良かった」「また書きたい」と感じてもらいたいと考えています。 ただし気をつけたいのは、短期的なPV数だけにとらわれないことです。特に執筆者は「他の人に比べて見られていないのではないか」とかえって不安になるかもしれません。分析を通して行いたいのは、記事の魅力をより伝わりやすくしたり、まだ外に出ていない知見を見つけ出したり、執筆者のモチベーションにつなげたりする改善をまわすことです。これらを損なわないよう分析だけでなくコミュニケーションも大切にしながら、継続的なアウトプットの支援を続けていきたいと思います。 まとめ 本記事ではLooker Studioを使ったTECH BLOGの数字分析について紹介しました。レポートによって可視化することで日々の確認や、他のアウトプットへの展開、執筆者へのフィードバックがしやすくなりました。今後はカテゴリーごとの偏りや読了率といった高度な分析もしていきたいと考えています。 現在DevRelブロックメンバーの募集は行なっていませんが、一緒にZOZOのサービスを作り上げてくれる方を募集中です。TECH BLOGなどのアウトプットが好きな方はDevRelがサポートします。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは、CTO/DevRelブロックの堀江( @Horie1024 )です。本記事はZOZO DevRelチームによる連載「ZOZO TECH BLOGを支える技術」の2本目の記事です。 前回の記事ではZOZO TECH BLOGの概要とその運用について紹介しました。今回の記事ではTECH BLOGの運用プロセスのうち記事の執筆に焦点を当て、執筆とそのレビュー体制を支えるCI/CDフローの整備について紹介します。 目次 はじめに 目次 ZOZO TECH BLOGでのCI/CDの活用 記事の静的解析と文章校正 記事のプレビュー環境へのデプロイ CI/CDフローの構築 CI/CDフローの概要 文章校正 プレビュー環境へのデプロイ フォーマット・画像のアップロード プレビューへの記事の反映 公開済みの記事一覧を取得 記事の新規投稿または更新 事例紹介 文章校正 textlint-disableの利用 校正ルールの運用 記事のプレビュー 導入成果 レビューパフォーマンス CI/CDフロー整備の影響 今後の展望 まとめ さいごに ZOZO TECH BLOGでのCI/CDの活用 CI/CDは、Continuous Integration(継続的インテグレーション)およびContinuous Delivery(継続的デリバリー)の略です。ソフトウェア開発プロセスの自動化による品質向上を目的とした手法です。CIでは、開発者がソースコードをリポジトリへプッシュするたびにビルドプロセスを実行しコードを統合します。そしてCDでは、CIの成果物を任意の環境へ自動的にデプロイします。これにより、開発者はより迅速かつ信頼性の高いソフトウェアを提供できるようになります。 ZOZO TECH BLOGを支える技術 #1 これまでとこれから で記事公開までのおおまかなプロセスを紹介しています。このうち、記事の執筆に関わるプロセスは次のとおりです。 記事の執筆とテストページへのデプロイ 記事のレビュー 指摘箇所の修正と再レビュー この3つのプロセスを記事が完成するまで繰り返します。これらのプロセスはソフトウェア開発と変わらないと言っても違和感はないでしょう。ZOZO TECH BLOGの記事は全てソースコードと同様にGitHubリポジトリで管理しており、記事のレビューもPull Requestを介して行います。執筆のプロセスにもCI/CDの手法を適用することで執筆者の執筆をサポートし、ZOZO TECH BLOGとしてより良い記事を公開することに繋がります。 記事の静的解析と文章校正 投稿する記事には正確で分かりやすい文章であることが求められます。文章の質の担保はレビューによって行いますが、人によるレビューの実施前に自動化された文章校正を実施する事でレビューの効率化を図れます。 CIによって反復的に実行されるプロセスには一般的に次のようなものがあります 1 。 ソースコードのコンパイル 分析(静的解析、動的解析等) テスト ZOZO TECH BLOGでは、これらのプロセスのうち分析にあたる静的解析を記事に対して行い文章校正をします。 具体的には、文章がリポジトリにプッシュされることをトリガーに textlint を実行し、その結果をPull Requestにコメントとして書き込みます。これにより、記事の執筆者は文章校正の結果を確認できます。 記事のプレビュー環境へのデプロイ 記事が読者にどう表示されるかを確認することは読みやすい記事を執筆する上で重要です。意図しない改行や文字化けが発生していないか、記事中のコードブロックが正しく表示されているかや画像のサイズが適切かなど実際に記事を表示して確認することが望ましいです。 ZOZO TECH BLOGでは、記事の執筆はMarkdownで行っており、それをはてなブログに投稿することで公開しています。このため、実際に記事がどのように表示されるかを確認できるプレビュー環境を非公開のはてなブログとして整備しています。 文章がリポジトリにプッシュされることをトリガーに、はてなブログのAPIを利用して執筆中の記事がプレビュー環境にデプロイされます。このプレビュー環境は記事の執筆者が記事のプレビューを確認するためだけに利用します。また、2023年8月時点では本番環境への公開はDevRelチームが手動で行っています。 CI/CDフローの構築 CI/CDフローを整備することで実現したことは次の2点です。 執筆中の記事の文章校正 執筆中の記事のプレビュー環境へのデプロイ これらを実現するCI/CDフローがどのように構成されているかを紹介します。 CI/CDフローの概要 文章校正とプレビュー環境へのデプロイを行うCI/CDフローの概要を図に示します 2 。CIサービスとしては GitHub Actions を用いています 3 。文章校正とプレビュー環境へのデプロイ共にPull Requestの作成またはコミットのプッシュをトリガーにワークフローを実行します。 CI/CDフローの概要 文章校正 文章校正は次のようなプロセスで実現します。 差分の検出 textlintの実行 実行結果のフィードバック これらのプロセスをPull Requestの作成またはコミットのプッシュをトリガーに実行します。textlintによる文章校正の結果はPull Requestにコメントとして書き込まれ執筆者にフィードバックされます。この一連の流れを実行するワークフロー定義は次のとおりです。 name : "textlint & reviewdog" on : pull_request : paths-ignore : - '**/README.md' env : REVIEWDOG_GITHUB_API_TOKEN : ${{ secrets.GITHUB_TOKEN }} jobs : linter : runs-on : ubuntu-latest steps : - name : "Checkout" uses : actions/checkout@v3 with : fetch-depth : 0 - name : "Setup nodejs" uses : actions/setup-node@v3 with : node-version : 16 cache : 'yarn' - name : "Setup reviewdog" uses : reviewdog/action-setup@v1 with : reviewdog_version : latest - name : "Install node dependencies" run : yarn install - name : textlint and reviewdog if : ${{ (github.event_name == 'pull_request' ) }} run : | DIFF_FILES=`git diff --name-status origin/master --diff-filter=MA | grep -E ".*.md" | cut -f2` if [ -z "${DIFF_FILES}" ] ; then exit 0; fi $(yarn bin)/textlint --ignore-path config/.textlintignore -c config/.textlintrc -f checkstyle $(echo ${DIFF_FILES}) | reviewdog -f=checkstyle -name="textlint" -reporter=github-pr-review --fail-on-error= true -filter-mode=added textlint and reviewdog stepで文章校正とPull Requestへのフィードバックをします。具体的な処理内容は次のとおりです。 差分の検出 textlintの実行 reviewdogでのPull Requestへのコメント追加 textlintの実行時には次のオプションを指定しています 4 。 オプション名 指定内容 オプションの概要 --ignore-path config/.textlintignore textlintの対象から除外するファイルを.textlintignoreに記載し指定 -c config/.textlintrc textlintの設定を.textlintrcに記載し指定 -f checkstyle 出力形式にcheckstyleを指定 また、textlintの実行結果をPull Requestにコメントとしてフィードバックするために reviewdog を使用しています。reviewdogはtextlintの実行結果を受け取り、その結果をPull Requestにコメントとして書き込みます。reviewdogの実行時には次のオプションを指定しています 5 。 オプション名 指定内容 オプションの概要 -f checkstyle 入力形式にcheckstyleを指定 -reporter github-pr-review 結果の出力にPull Requestへのコメントを指定 -fail-on-error true 1つでもエラーが発生した場合にジョブを失敗させるよう指定 -filter-mode file 追加されたファイル単位で結果をフィルタリングするよう指定 プレビュー環境へのデプロイ 記事のプレビュー環境へのデプロイは次のようなプロセスで実現します。 フォーマット・画像のアップロード プレビューへの記事の反映 これらのプロセスを文章校正と同様にPull Requestの作成またはコミットのプッシュをトリガーに実行します。 フォーマット・画像のアップロード プレビュー環境へのデプロイにおいて画像の扱いを考慮する必要があります。ZOZO TECH BLOGの標準的な記事のディレクトリ構成は次のとおりです。 articles └── sample_article ├── entry.md └── images └── sample.png entry.mdから画像を参照する場合は次のように記述します。 ![ sample ]( ./images/sample.png ) ここで、entry.mdをプレビュー環境へデプロイする際、 ./images/sample.png をはてなフォトライフへアップロードします。そして、 ./images/ のパスを https://cdn-ak.f.st-hatena.com/images/fotolife/ に変換しentry.mdの該当箇所を書き換えます。この一連の処理をフォーマットと呼んでいます。フォーマットが完了するとentry.mdは次のようになります。 ![ sample ]( https://cdn-ak.f.st-hatena.com/images/fotolife/<USERNAME>/<UPLOADED_IMAGE_PATH> ) GitHub Actionsでこのフォーマット処理を行うワークフロー定義は次のとおりです。entry.mdの差分を検出し、差分のあるファイルに対してフォーマットを行います。 name : "format & post to hatenablog" on : pull_request : paths : - '**/entry.md' env : HATENA_ACCESS_TOKEN : ${{ secrets.HATENA_ACCESS_TOKEN }} HATENA_ACCESS_TOKEN_SECRET : ${{ secrets.HATENA_ACCESS_TOKEN_SECRET }} HATENA_CONSUMER_KEY : ${{ secrets.HATENA_CONSUMER_KEY }} HATENA_CONSUMER_SECRET : ${{ secrets.HATENA_CONSUMER_SECRET }} jobs : format : runs-on : ubuntu-latest steps : - name : "Checkout" uses : actions/checkout@v3 with : fetch-depth : 0 - name : "Upload Image" run : | ARTICLE_PATH=`pwd`/`git diff --name-status origin/master --diff-filter=MA | grep -E "articles/.*.md" | cut -f2` cd scripts bundle bundle exec ruby format_article.rb $ARTICLE_PATH ../entry.md - name : "mv formatted article" run : mkdir /tmp/workspace && cp entry.md /tmp/workspace/formatted_entry.md - name : "Upload formatted_entry.md for job: post_to_hatenablog" uses : actions/upload-artifact@v3 with : name : formatted_entry path : /tmp/workspace/formatted_entry.md このワークフロー定義では、 Upload Image stepで format_article.rb を実行することで画像のアップロードと記事のフォーマットを行います。ここではてなフォトライフへのアップロードには rlho/hatena_fotolife を使用しています。 次に mv formatted article stepでフォーマット済みの記事を /tmp/workspace/formatted_entry.md に移動します。最後に Upload formatted_entry.md for job: post_to_hatenablog stepでフォーマット済みの記事を成果物としてアップロードします。フォーマット済みの記事は後続の post_to_hatenablog jobでプレビュー環境へデプロイします。 プレビューへの記事の反映 プレビュー環境は非公開のはてなブログです。成果物としてアップロードされたフォーマット済みの記事をプレビュー環境へ反映するには、はてなブログへの投稿が必要になります。 フォーマット済みの記事を受け取りはてなブログへの投稿するワークフロー定義は次のとおりです。 env : BLOG_USERNAME : ${{ secrets.BLOG_USERNAME }} BLOG_DOMAIN : ${{ secrets.BLOG_DOMAIN }} BLOG_API_KEY : ${{ secrets.BLOG_API_KEY }} jobs : format : ... post_to_hatenablog : needs : format runs-on : ubuntu-latest steps : - name : "Checkout" uses : actions/checkout@v3 with : fetch-depth : 0 - name : "Setup hatenablog CLI" uses : x-motemen/blogsync@v0 - name : "Download formatted_entry.md from job: format" uses : actions/download-artifact@v3 with : name : formatted_entry path : /tmp/workspace - name : "cp formatted_entry.md" run : cp /tmp/workspace/formatted_entry.md . - name : "Add blogsync config" run : | echo -e "${BLOG_DOMAIN} \" : \"\n username \" : \" ${BLOG_USERNAME} \n password \" : \" ${BLOG_API_KEY} \n default \" : \"\n local_root \" : \" ./ \n omit_domain: true" | tr -d \" >> blogsync.yaml - name : "Create/Update article" run : | blogsync pull $BLOG_DOMAIN BRANCH=${{ github.head_ref }} ARTICLE_PATH=`git diff --name-status origin/master --diff-filter=MA | grep -E "articles/.*.md" | cut -f2` # 整形したものに置き換え mv -f formatted_entry.md $ARTICLE_PATH if grep '^URL:' $ARTICLE_PATH; then # ブログが投稿されている場合は投稿されている記事を更新 blogsync push $ARTICLE_PATH else # ブログが一度も投稿されてない場合は投稿 blogsync post --title=$BRANCH --custom-path=$BRANCH $BLOG_DOMAIN < $ARTICLE_PATH # 記事メタデータの付与 BLOGSYNC_PATH="entry/${BRANCH}.md" cp -r $ARTICLE_PATH $ARTICLE_PATH.old head -8 $BLOGSYNC_PATH | cat - $ARTICLE_PATH.old > $ARTICLE_PATH fi # 画像のアップロード・メタデータが付与されていればコミットしてpush if git status -s | grep articles; then git config --global user.email ${BOT_EMAIL} git config --global user.name 'TechBlog Bot' git checkout $BRANCH git add $ARTICLE_PATH git commit -m "Add hatena blog meta data" git push ${TECH_BLOG_REPO_URL} $BRANCH fi - name : "Print blog URL" run : | ARTICLE_PATH=`git diff --name-status origin/master --diff-filter=MA | grep -E ".*.md" | cut -f2` echo `grep -e "^URL:" $ARTICLE_PATH | awk '{print $2}' ` Download formatted_entry.md from job: format stepでフォーマット済みの記事を成果物としてダウンロードします。はてなブログへの投稿には x-motemen/blogsync を使用し、 Add blogsync config stepでblogsyncの設定ファイルを作成します。そして、 Create/Update article stepでは次のことを行います。 公開済みの記事一覧を取得 記事の新規投稿または更新 公開済みの記事一覧を取得 blogsync pull コマンドで公開済みの記事一覧を取得します 6 。 blogsync pull $BLOG_DOMAIN 記事の新規投稿または更新 記事がプレビュー環境に投稿されている場合は更新し、投稿されていない場合は新規投稿します。 blogsync push コマンドで記事を更新します 7 。titleとcustom-pathはブランチ名、投稿内容は標準入力で指定します。 blogsync post --title = $BRANCH --custom-path = $BRANCH $BLOG_DOMAIN < $ARTICLE_PATH 投稿後、記事は自動的にダウンロードされ entry/${BRANCH}.md に保存されます。この時に、記事の先頭には記事のURL等のメタデータが付与されています 8 。例えば、本記事の場合次のようになります。 Title: ZOZO TECH BLOGを支える技術 #2 執筆をサポートするCI/CD Date: 2023-08-08T09:55:22+09:00 URL: https:// < BLOG _DOMAIN> /entry/techblog-writing-support-by-ci-cd EditURL: https://blog.hatena.ne.jp/ < BLOG _USERNAME> / < BLOG _DOMAIN> /atom/entry/... CustomPath: techblog-writing-support-by-ci-cd このメタデータのURLが記事のプレビューURLとなりますが、メタデータはZOZO TECH BLOGのリポジトリでは管理されていません。リポジトリ管理下にあるのは記事の本文のみです。そのため、記事の本文にメタデータを付与しZOZO TECH BLOGのリポジトリにコミットします。 記事の本文へのメタデータの付与は次のように行います。 BLOGSYNC_PATH = " entry/ ${BRANCH} .md " cp -r $ARTICLE_PATH $ARTICLE_PATH .old head -8 $BLOGSYNC_PATH | cat - $ARTICLE_PATH .old > $ARTICLE_PATH そして、ZOZO TECH BLOGのリポジトリにプッシュします。 git config --global user.email ${BOT_EMAIL} git config --global user.name ' TechBlog Bot ' git checkout $BRANCH git add $ARTICLE_PATH git commit -m " Add hatena blog meta data " git push ${TECH_BLOG_REPO_URL} $BRANCH 事例紹介 今回紹介したCI/CDフローが実際にどう活用されているか、ZOZO TECH BLOGでの事例を紹介します。 文章校正 「GitHub Copilotの全社導入とその効果」の執筆中に指摘があった例を紹介します。 techblog.zozo.com 丸かっこの指摘 冗長な表現の指摘 文字数の指摘 textlint-disableの利用 文章校正の結果を無視したい場合もあります。例えば、次の例ではアンケートの選択肢の部分でtextlintの指摘が出てしまいます。 アンケートの選択肢についてのtetlintの指摘 この場合、次のように textlint-disable を利用することで、指摘を無視できます 9 。 <!-- textlint-disable --> 文章校正を無効にしたい文章 <!-- textlint-enable --> 校正ルールの運用 技術文書向けのtextlintルールプリセットである textlint-rule-preset-ja-technical-writing をはじめとする複数のルールを導入しています。このプリセットには、文章中の漢字の連続文字数を制限する textlint-jatextlint-rule-max-kanji-continuous-len が含まれています。このルールは、次のように漢字が連続していることを指摘します。 漢字の連続文字数の指摘 しかし、 平均削減金額 のように指摘を無視したい場合があります。 textlint-disable を利用しても良いですが、恒久的な対応として例外に登録可能です。 漢字の連続文字数の例外登録 記事のプレビュー プレビュー環境は記事に付与されたメタデータからアクセスできます。例えば、本記事のプレビューは次のようになります。 プレビュー表示 導入成果 CI/CDフローの導入によって、執筆者とレビュアの双方で記事の体裁の指摘が減少しています。また、自動的にプレビュー環境が生成されるため表示確認も容易になっています。DevRelチームとしても記事の体裁の指摘が減少しているため、記事の内容にフォーカスしてレビューできています。 レビューパフォーマンス レビューのパフォーマンスを定量的に評価することは難しいですが、レビュー開始からアプルーブまでの時間を指標として見てみます。 Findy Team+ の「レビュー分析」でZOZO TECH BLOGリポジトリのレビュー開始からアプルーブまでの時間を表示しました。集計期間は過去1年間です。ZOZO TECH BLOGの場合、レビュー開始からアプルーブまで平均で225.5時間かかっています。 過去1年間のレビューからアプルーブまでの平均時間 2023年2月からリードタイムが大きく改善しています。DevRelチームの発足によるレビュー体制強化が大きな要因として考えられます。 次は、DevRelチームが発足した2023年2月1日から2023年8月14日までで集計した結果です。この期間の平均リードタイムは、レビュー開始からアプルーブまで平均で145時間です。 2023年2月以降のレビューからアプルーブまでの平均時間 ここで、ZOZO TECH BLOGの月別の記事公開数は次のようになります。 公開月 公開本数 2023年2月 7 2023年3月 16 2023年4月 4 2023年5月 12 2023年6月 15 2023年7月 8 2023年8月 2 3月、5月、6月は公開記事数の増加によりレビューが重なりました。しかし、DeRelチームですべて捌ききることができ、レビュー開始からアプルーブまでの時間の増加もチームとしての許容範囲内に収められています。レビューの負荷が高い状況でも大きくパフォーマンスは悪化していません。 CI/CDフロー整備の影響 CI/CDフローの整備は1年以上前に行われていますので、レビューパフォーマンス改善の主要因ではありません。しかし、CI/CDフローの整備によってレビュアがレビューする際の効率化が図られているため、レビュー体制を強化した効果がより発揮されていると考えることはできそうです。 CI/CDフローの整備を進めていく上で「レビュー開始からアプルーブまでの時間」のような具体的な指標を追うことは、整備の方向性を模索する上で重要な検討材料となります。また、記事のアイデアを出す段階から公開されるまでの流れを見直すことで、記事の公開までの時間をより短縮できるかもしれません。 今後の展望 整備したCI/CDフローのサポートによって記事の執筆およびレビューの効率化が実現されています。しかし、まだまだ改善の余地があります。例えば、アップロードする画像サイズの最適化は著者にその実施を委ねていますが、CI/CDフローの中で実施できた方が執筆者の負担は減るでしょう。また、AIの活用についても検討を重ねていきたいです。文章校正やレビューについてAIを活用したり、記事のテーマや構成案の検討にも活用できそうです。 まとめ 本記事では、ZOZO TECH BLOGの執筆をサポートするCI/CDフローについて紹介しました。ZOZOではDevRelチームを中心に執筆をサポートする体制を整えています。今後も引き続き執筆環境を改善することで執筆者が少しでも書きやすい環境になり、それにより記事の執筆者が増え、社内の取り組みをより多く社外に発信できればと思います。 さいごに ZOZOでは一緒に楽しく働くエンジニアを絶賛募集中です。ご興味のある方は下記リンクからぜひご応募ください。 corp.zozo.com ビルドという言葉はコンパイルと同じ意味で使われる場合もありますが、CIの文脈ではコンパイルだけでなく、テストや分析まで含まれることがあります。 ↩ GitHub Actionsのアイコンは 公式サイト から引用しています。 ↩ 当初はCIサービスとしてCircleCIを利用していましたがGitHub Actionsに移行しました。 ↩ textlintのオプションは textlintリポジトリのREADME.md から確認できます。 ↩ reviewdogのオプションは reviewdogリポジトリのREADME.md から確認できます。 ↩ https://github.com/x-motemen/blogsync#%E3%82%A8%E3%83%B3%E3%83%88%E3%83%AA%E3%82%92%E3%83%80%E3%82%A6%E3%83%B3%E3%83%AD%E3%83%BC%E3%83%89%E3%81%99%E3%82%8Bblogsync-pull ↩ https://github.com/x-motemen/blogsync#%E3%82%A8%E3%83%B3%E3%83%88%E3%83%AA%E3%82%92%E6%9B%B4%E6%96%B0%E3%81%99%E3%82%8Bblogsync-push ↩ https://github.com/x-motemen/blogsync#%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%81%AE%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88 ↩ textlint-enable で再度有効にする必要があります。 ↩
アバター
はじめに こんにちは。検索基盤部の岩崎です。検索基盤部ではZOZOTOWNの検索機能の改善に日々取り組んでいます。ZOZOTOWNのおすすめ順検索のプロジェクトでは、機械学習モデルを活用した検索結果の並び順の改善に取り組んでおり、全ての施策はA/Bテストで検証しています。なお、最近の並び順精度改善の取り組みについては以下の記事をご参照ください。 techblog.zozo.com 本記事におけるA/Bテストとは、特定期間中ランダムに振り分けたユーザーに対してそれぞれ別の施策を提示し、その成果の差を検定するテストのことを指します。A/Bテストは施策の効果を検証するための優れた手段として広く知られており、おすすめ順検索改善のリリース判断には欠かせない存在となっています。ZOZOではA/Bテスト基盤の整備を進めており、おすすめ順検索以外にもさまざまな施策でA/Bテスト基盤を用いた運用がされています。 しかし、おすすめ順検索のプロジェクトではA/Bテストを重ねていくうちにデータ分析の工程が徐々に複雑化してしまい、その手動作業が分析者の負担となっていました。ダッシュボードに表示する指標もどんどん増え、修正作業の難しい状態となっていました。本記事ではA/Bテストのデータ分析工程に関して、従来の課題とその改善策について焦点をあて、取り組みを紹介していきます。A/Bテストのデータ分析工程に関して類似した課題を抱えている方の一助となれば幸いです。 目次 はじめに 目次 従来の運用 改善1. 集計のワークフローをVertex AI Pipelinesで自動化する 改善2. 指標の再整理 まとめ おわりに 従来の運用 A/Bテスト導入当初はスプレッドシートの カスタムクエリ の機能を用いて集計処理が行われていました。以下は実際に当初運用していたクエリの編集画面になります。なお具体的なクエリの内容については加工処理をしています。 この集計結果をもとに、A/Bテスト期間中の各施策の指標を比較できるダッシュボードをスプレッドシート内に作成していました。A/Bテスト期間中は定期的にカスタムクエリを実行し、指標の速報値をプロジェクトメンバー内で共有し、もし指標値の急激な悪化が見られた場合は早期に切り戻しする、といった運用をしていました。A/Bテスト期間後に発生する後続の分析調査についても同じスプレッドシート内に追加していくことで運用されていました。 そして、A/Bテストが行われるたびに上記の集計クエリやダッシュボードが格納されたスプレッドシートを複製し、クエリ編集画面にあるパラメータやクエリ内容を次のA/Bテストの設定に修正していました。以上が従来の運用方法になります。 この運用は、比較的軽量な作業で運用可能な状態を作り上げられるというメリットがあります。一方で、コードの管理体制が十分でないことやスプレッドシート自体の制約により、運用を継続していく上で多くの課題が生じました。以降にその課題と改善の取り組みを説明していきます。 改善1. 集計のワークフローをVertex AI Pipelinesで自動化する 従来の運用では、集計クエリのコードがスプレッドシート内に存在しており、その内容を直接バージョン管理できていませんでした。レビュー体制も構築できておらず、クエリ修正作業の属人化が進行していました。また、集計クエリがダッシュボードに依存しているという課題もありました。これによりA/Bテストが行われるたびに集計クエリが複製されるため、さらに差分を追いづらい状態になっていました。ダッシュボードを他のツールに移行することも難しく、ツール再選定の余地がない状態になっていました。 加えて、カスタムクエリのタイムアウトは通常のクエリのタイムアウトよりも短く設定されていることが一般的です。このため、カスタムクエリの集計期間によってはタイムアウトになることもありました。これを防ぐために、通常のクエリ実行で集計した結果を一時的なテーブルとして保存しておき、カスタムクエリでその結果を取得する、といった暫定対応なども必要になりました。こういった手動作業の複雑化についても課題となっていました。 これらの課題により、A/Bテストのたびに行う分析者の手動作業が多く、分析者のリソースが逼迫していました。人的ミスの入り込む可能性も高く、クエリ修正作業の属人化も進行していました。これらの課題の解決のために、集計クエリの実行の自動化に取り組みました。 集計クエリの自動化には、Google Cloud Platform(GCP)のサービスである Vertex AI Pipelines を利用しました。選定理由は以下の3点です。 1点目の選定理由は、条件分岐やループをもつワークフローを簡単に記述できる点です。これにより、「A/Bテスト期間中のみ速報値を日次で出力したい」といった要件や「複数のA/Bテストが同時期に行われることも想定したい」といった要件に対しても柔軟に対応できます。 2点目の選定理由は、Vertex AI Pipelinesがおすすめ順検索の開発者にとって馴染みのあるサービスであるという点です。Vertex AI Pipelinesは一般的には機械学習のワークフローを管理するサービスであり、おすすめ順検索の機械学習モデルの開発時にも活用しています。そのため、習得すべき技術項目を増やすことなく分析工程の改善にも取り組むことができます。なお、おすすめ順検索の機械学習モデルの開発からデプロイまでのワークフローの自動化については以下の記事をご覧ください。 techblog.zozo.com 3点目の選定理由は、社内にVertex AI PipelinesのCI/CD、監視、スクリプト類のまとまったテンプレートリポジトリがある点です。このリポジトリは Cloud Scheduler と Cloud Functions によるVertex AI Pipelinesの定期実行も簡単に実装できるようになっています。こちらを活用することにより、今回目指している集計クエリの自動化がより容易に実現できます。以下の記事でVertex AI Pipelinesの導入事例やテンプレートリポジトリを紹介しているので併せてご参照ください。 techblog.zozo.com 以上の理由から、Vertex AI Pipelinesを採用して、集計クエリの自動化を行いました。 実際に運用している集計のワークフローを以下に示します。この図はVertex AI Pipelinesのコンソール画面から確認できるワークフロー全体像となっています。 簡単にこのワークフローを説明すると、以下のとおりです。このワークフローが日次で実行されることにより、集計の自動化が実現できています。 集計の基準日を取得する(デフォルトではワークフロー実行日となる) 前段で取得した基準日に行われているA/Bテストがいくつなのか、A/Bテスト名などの情報とあわせて取得する 前段で取得したA/Bテストそれぞれについて、指標算出のための集計クエリを実行し、結果をBigQueryに出力する なお、移行にあたっては、特定のA/Bテストについて従来の運用と新規の運用を並走させて同一の結果を出力することを確認する、という方針で安全に行いました。 この取り組みにより、集計クエリとダッシュボードとの依存関係を軽減でき、ダッシュボードのツールを再選定しやすい状態になりました。現状のダッシュボードは習熟コストの低さを優先してスプレッドシートを引き続き採用していますが、今後のツール選定の余地を残したまま運用できています。 また、GitHub上で管理されたSQLファイルを直接実行する仕組みとなったため、取得する指標の変更に応じてレビューが適切に行われるようになりました。加えて、SQLのLinterである SQLFluff をGitHub Actionsに導入することでコードの品質も保つことができるようになりました。 改善2. 指標の再整理 従来の運用では、これまでのA/Bテストで作成された指標がそのまま次のA/Bテストのダッシュボードに引き継がれていく仕組みとなっていました。A/Bテスト期間後の事後分析用のクエリも引き継いでいるため、徐々にダッシュボードが複雑になっていき、「集計しているが数値の確認はしていない」といった状態の指標が発生していました。またレビュー体制が整っていないこともあり、「A/Bテストの判断に必要な指標」と「事後分析などの追加調査のための指標」という別の目的をもった集計が単一クエリに混在している状態でした。以上のことから、クエリやダッシュボードも必要以上の複雑さになっており、修正作業の属人化が進行していました。 この状態の解決のために、現在算出している指標とその算出に必要なカラムを全て洗い出し、指標を分類する取り組みを行いました。関係者と議論して「A/Bテストの判断に必要な指標」とそうでない指標を分類し、前者のみを自動化の対象とし、その算出に必要なカラムのみを抽出するように集計クエリを作り直しました。 これらの対処の結果、追加調査は別クエリとして切り出されるようになり、集計クエリのコードの肥大化防止につながりました。追加調査もより気軽に、他の集計を気にせず行える仕組みとなりました。指標数やクエリのコード量は従来の1/5程度に抑えられ、クエリ修正作業の属人化の解消にもつながりました。ダッシュボードもシンプルになり、KGIやKPI、A/Bテストが正しく行われているかの指標を信頼区間つきで端的に示されることで、リリース可否の判断もしやすくなりました。 以下は改善後のダッシュボード例になります。数値はダミーの値に加工しています。 この図から、新規施策が多くの指標で上回っており、そのうち2つの指標についてその差が統計的に有意であることが読み取れます。 まとめ 今回の取り組みにより手動作業の多くは自動化され、分析者はA/Bテスト期間中でも他の分析へ注力できるようになりました。クエリの簡単化とレビュー体制の構築により、属人化の問題を解消できました。さらに、これらの仕組み改善のおかげで、Sample Ratio Mismatchの検知や信頼区間の導入など、リリース判断にとって重要な取り組みを実施できるようになりました。 今回の取り組みによって分析工程の自動化をある程度進めることができた一方、A/Bテストの実施工程の全体を見つめ直すとまだまだ自動化できる要素がたくさん残っています。現状はリリースフロー上関係部署と多くのやり取りをする必要があるため、協調して仕組みを自動化できればと考えております。日々の運用を改善していき、大量のA/Bテストを負担なく実行できるような体制につなげられればと思います。 おわりに ZOZOでは一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com
アバター
はじめに こんにちは、エンジニア組織の技術広報を担うDevRelブロックの @ikkou です。ARやVRをはじめとするXRといった技術領域を担う創造開発ブロックと、技術戦略の策定やエンジニア組織の強化を担うCTOブロックも兼任しています。 これから3つの記事にかけて、皆さんが見ているこの「ZOZO TECH BLOGを支える技術」を紹介していきます。1本目となる本記事では、これまでの取り組みと今後の展望を紹介します。 目次 はじめに 目次 はじめに DevRelブロックについて ZOZO TECH BLOGについて ZOZO TECH BLOGを支える技術 これまで これまでの仕組みと課題、その解決 スケジューリング 概要のレビュー デザイナーチームに画像の作成依頼 記事の執筆とテストページへのデプロイ 記事のレビュー 予約投稿と公開 X(Twitter)にシェア 今後の展望 AI導入による効率化 反響の可視化 まとめ はじめに まずはじめに、DevRelブロックとZOZO TECH BLOGについて紹介します。 DevRelブロックについて DevRelブロックは、ZOZOにおけるエンジニア組織の「技術広報」を担うチームです。この「ZOZO TECH BLOG」の運用・執筆支援をはじめとして、技術カンファレンスや勉強会での登壇支援や開催など、エンジニア組織の技術発信とブランディングを支援しています。 これまではCTOブロックがその役割を担ってきましたが、今年の春から専門チームとして新設されました。私のように他ブロックを兼任しているメンバーと、DevRelブロック専任のメンバーで構成されています。 技術広報を担う組織の場合、人事やコーポレート広報の経験者が担当または兼任しているケースもありますが、ZOZOの場合は全員エンジニアです。それぞれ素地となる専門領域は異なりますが、エンジニアとしての視点を持って取り組んでいます。 ZOZO TECH BLOGについて まさに今皆さんが読んでいる、このZOZO TECH BLOGは、今年で13年目となり、現在までに様々な技術領域の記事やイベントレポートなどが約650本投稿されています。歴史を紐解くと、まず 2011年5月9日に「株式会社VASILY」の「VASILY Tech Blog」として始まりました 。2018年4月に複数社で「株式会社スタートトゥデイテクノロジーズ」として合併、 名称が「スタートトゥデイテクノロジーズ テックブログ」に変わりました 。そして同年10月に商号が「株式会社ZOZOテクノロジーズ」に変わり、あわせて名称も「ZOZO Technologies TECH BLOG」に変わりました。その後、 2021年秋の吸収分割で「株式会社ZOZO」と「株式会社ZOZO NEXT」に分かれ、現在の「ZOZO TECH BLOG」になりました 。 このように何度かの組織改編や名称変更を経てきましたが、テックブログに対する思いや熱量は変わることなく、現在はZOZO所属のエンジニアとZOZO NEXT所属のエンジニアが記事を執筆しています。 ZOZO TECH BLOGを支える技術 早速、本記事のテーマとなる「これまで」と「これから」を紹介していきます。 これまで 前述の通り、これまではCTOブロックが技術広報の役割を担ってきました。そのため、このZOZO TECH BLOGの運用も同様にCTOブロックが担っていました。テックブログにかける思いや、その運用方法については、前任の担当者が2020年末に記事として公開しています。今でも十二分に通用するテックブログの運営・運用ノウハウが詰まっているので、ぜひご一読ください。 techblog.zozo.com これまでの仕組みと課題、その解決 一部、先の記事にも記載されていますが、記事公開までおおまかには次のプロセスを経ていました。 各部署から執筆予定をヒアリング スケジューリング 概要のレビュー デザイナーチームに画像の作成依頼 記事の執筆とテストページへのデプロイ 記事のレビュー 指摘箇所の修正 予約投稿と公開 X(Twitter)にシェア 長年の運用によって培われてきたこのプロセスですが、一部の運用に改善の余地がありました。引き継いだタイミングと、DevRelブロックが組成されたタイミングで、それらの課題を解決するためのアプローチを考えました。 スケジューリング 当初はConfluenceで運用していましたが、見やすさを重視するためGoogle スプレッドシートに移行しました。また、それぞれのステータスを追いやすい形式に変更しました。 テックブログ予定表の一例 行を固定している#0は凡例です。スケジューリングの前工程として、各部署から上期・下期ごとの執筆予定の記事数をヒアリングするとともに、この予定表に記載しています。執筆者には、執筆予定の記事について、概要を記載してもらい、後工程に続きます。それぞれの工程では、依頼前・依頼済み・確認済みなどのステータスを変更することで、公開までの進捗を把握できるようにしています。 概要のレビュー まず、執筆者の上長による概要のレビューを経て、DevRelブロックでも同様にレビューしています。このレビューは、検閲のようなものではなく、記事の内容を把握するためのものです。また、執筆者が記事を書く際に、概要を書くことで記事の方向性を明確にできます。 デザイナーチームに画像の作成依頼 ZOZO TECH BLOGの記事冒頭にあるOGP画像は、すべてデザイナーが作成しています。Slackに「TECH BLOG 画像作成依頼ワークフロー」を用意し、公開日・タイトル・概要・確認ページのURL・使用可能な画像アセットを記した上で作成を依頼します。デザイナーは、このワークフローを参照して画像を作成しています。この画像は、記事の内容を表現するだけでなく、SNSでシェアした際にも目を引くように工夫しています。 画像の作成依頼は公開日の14営業日前までに依頼するようにしています。デザイナーのマンパワーにも限りがあるため、記事は公開できる状態になっているが、画像が作成できていないという状況が時々発生してしまいます。対策として、昨今流行りの画像生成AIの利用や、記事タイトルのテキストを画像化するなども検討していますが、現時点ではクオリティの観点から見送っています。今後の課題のひとつです。 記事の執筆とテストページへのデプロイ 概要のレビューが済んだ後、各エンジニアは専用のリポジトリに記事を作成します。記事の執筆には、以前から変わらずMarkdownを採用しています。textlintを導入しているため、執筆中に必要最低限のチェックを行なえます。そしてPushすることで非公開のテストページに自動的にデプロイされ、表示を確認できます。その際にtextlintも実行されるので、手元でのチェックから漏れたエラーを確認の上、修正できます。 このCI/CDはもともとCircleCIによって実現していましたが、現在はGitHub Actionsに移行しています。この移行の詳細は後続の記事で紹介します。 記事のレビュー 一定まで執筆した、あるいはすべて書き終えた記事は、まずチーム内でレビューされます。チーム内レビューでは、記事内の技術的な内容の正しさを担保しています。ZOZOには一部の技術領域においてテックリードという制度があるので、ときにはチームの垣根を越えてテックリードがレビューすることもあります。 チーム内・テックリードの後、DevRelブロックでもレビューします。画像の作成依頼と同じように、Slackに用意してある「TECH BLOG レビュー依頼ワークフロー」からレビューを依頼してもらいます。DevRelブロックのレビューでは、誤字脱字や表記揺れ、表記誤りなどを修正するとともに、記事の方向性が概要と一致しているかを確認します。DevRelブロックが組成されるまで、このレビューパートは1人で行なっていたため、レビューのタイミングが遅くなるといった課題がありました。何より属人的になることで、可用性が低い運用となっていました。そういった背景もあり、現在のDevRelブロックが組成されたことで、マンパワーの増強によりできることが増え、その結果、これまで培ってきたことの改善と今後に向けた施策も考えられるようになりました。 マンパワーが増強されたとはいえ、期末など記事の公開が集中しがちなタイミングでは、レビューが追いつかないこともあります。画像の作成同様にAIを活用することも検討していますが、現時点ではまだ実現していません。これもまた今後の課題のひとつです。 予約投稿と公開 画像が用意され、各レビューが完了した記事は、執筆者自身の手で本番環境に下書き保存しています。下書き保存の完了後、スケジュールを確認しながらDevRelブロック公開日時を決めて予約投稿を設定しています。公開日の大原則は以前から変わらず、「1日に複数の記事は公開しない」と「午前11時に公開する」としています。 X(Twitter)にシェア 記事公開に先立ち、執筆者には記事の概要文を用意してもらっています。この概要文を記事公開にあわせてX(Twitter)のZOZO Developersアカウント( @zozotech )でシェアしています。これまでの経緯からこのシェアは人事組織が担っていましたが、現在はDevRelブロックが担当しています。 今後の展望 一連の流れは紹介した通りです。画像作成やレビューといったマンパワーに起因する課題はありますが、DevRelブロック組成以前に比べて可用性が高まっていると感じています。これからは、この可用性を維持しつつ、さらに改善していくことを目指しています。その一部を紹介します。 AI導入による効率化 前述の通り、画像作成とレビューにおけるAIの導入は今後の課題です。前者の画像作成については、例えばこれまでのOGP画像を学習データとして与えることで、ZOZO TECH BLOGらしい画像を生成できるかもしれませんが、まだ公開できるフェーズではありません。後者のレビューも同様ですが、textlintを活用することで、誤字脱字や表記揺れ、表記誤りなどの多くは自動的に修正できるようになっています。それ以上の言い回しや表現方法をAIが判断できるのか、今はまだ引き続き検証が必要な段階です。 別の観点では、記事執筆にGitHub Copilotが使える可能性も感じています。ちなみに、ZOZOは先日GitHub Copilotを全社導入しました。詳細はぜひ以下の記事をご覧ください。 techblog.zozo.com GitHub Copilotは多くの場合、コードの補完に利用されていますが、テックブログのような文章を書く場面でも活用できます。実際にこの記事も一部でGitHub Copilotの恩恵を受けています。記事すべてをGitHub Copilot然りAIに完全に任せるのはまだ早いと考えていますが、補完のような形であれ、AIを活用することで執筆者の負担を軽減できるとも考えています。 反響の可視化 改善のひとつとして執筆者に記事公開後の反響を伝えようと考えています。実はこれまで、記事公開後に記事のPV数やシェア数を執筆者に伝えることはしていませんでした。現在は「DevRelブロック通信」という社内報で前月に公開された記事と、ブックマーク数が目立った記事を紹介していますが、記事公開後のフォローが不十分だと感じています。そこで、PV数・シェア数・はてなブックマーク数といった情報の他、記事に対するコメントや質問を共有することで、執筆者が記事を書くモチベーションにつながると考えています。 この記事公開後の反響の可視化については、もともと導入していたGA4とLooker Studioを組み合わせることで実現できます。このLooker Studioに関する詳細も後続の記事で紹介します。 まとめ 本記事ではDevRelブロックと、ZOZO TECH BLOGのこれまでとこれからを紹介しました。ZOZO TECH BLOGについては、後続の2つ目と3つ目の記事も楽しみにしてもらえると嬉しいです。 現在、DevRelブロックメンバーの募集は行なっていませんが、DevRelブロックが技術広報という形で支援するのはエンジニアの皆さんです。一緒にZOZOのサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
こんにちは。ZOZO Researchの研究員の古澤・北岸・平川です。2023年7月25日(火)から7月28日(金)にかけて画像の認識・理解シンポジウムMIRU2023に参加しました。この記事では、MIRU2023でのZOZO Researchのメンバーの取り組みやMIRU2023の様子について報告します。 目次 目次 MIRU2023 企業展示 全体の動向 若手プログラム インタラクティブセッション [IS3-46] 着用者の体型を考慮したファッションコーディネート推薦 [IS3-87] ファッショントレンドの検出と予測:SNS投稿データのクラスタリングと時系列解析 気になった研究発表 [OS3B-L2] Instruct 3D-to-3D: Text Instruction Guided 3D-to-3D conversion [OS4A-L2] 数式ドリブン教師あり学習によるセマンティックセグメンテーション [OS1A-L2] アテンションはアノテーションの代わりになるか?:テキスト−画像生成モデルの注意機構を利用した領域分割の弱教師あり学習 [IS2-16] セグメンテーションマスクを利用した動画からの静的なNeRF表現の学習 まとめ 最後に おまけ MIRU2023 MIRUとは、Meeting on Image Recognition and Understandingという画像の認識・理解についてのシンポジウムです。2023年の今回はアクトシティ浜松においてオフラインとオンラインのハイブリッド形式で開催されました。昨年に引き続き今年もハイブリッド開催ということで、過去最多の1513名(オンライン参加含む)もの方々が参加されたそうです。ZOZO NEXTは、このMIRU2023にゴールドスポンサーとして協賛させていただきました。 cvim.ipsj.or.jp 昨年のMIRU2022に参加した際のレポートは以下の記事をご覧ください。 techblog.zozo.com 企業展示 企業展示ブースでは、ZOZO NEXTにおける取り組みについてポスターを用いて紹介させていただきました。ZOZOが提供する多角的なファッションサービスとそこから生み出される多様なデータを活用した研究事例について、最新の研究成果も交えて紹介させていただきました。大変喜ばしいことに多くの方々に興味を持っていただき、お話をさせていただくことができました。ブースまで足を運んでくださった皆さま、誠にありがとうございました。展示していたポスターはこちらです。 全体の動向 今回のMIRUでは、生成モデルと3Dモデルについての研究が目立ちました。特に生成モデルに関しては、昨年は敵対的生成ネットワークを使用する研究が多かったと記憶していましたが、今年はStable Diffusionなどで注目を集めている拡散モデルを用いた研究へとシフトが見られました。3Dモデルに関しては、昨年に引き続きNeural Radiance Fields (NeRF)に関する研究が行われており、新たにNeRFと拡散モデルを組み合わせる研究も見受けられました。 また、近年の生成モデルの発展を受け、「ニューラルデジタルヒューマン合成の最先端」と「大規模言語モデル時代のHuman-in-the-Loop機械学習」の2つのチュートリアル講演が行われました。前者の講演では、人間の3Dモデル生成について、敵対的生成ネットワークを用いた研究から近年の拡散モデルを使用した研究までレビューされており非常に興味深かったです。後者の講演では、クラウドソーシングによる大規模教師データの作成や安全性・公平性・多様性を担保するための人間からのフィードバックの活用について紹介がありました。 特別講義では、「画像と言語の基盤モデルの現状とこれから」と「見立てて見て取るための視覚的表現とインタラクティビティ」の2つの講演が行われました。前者の講演では、大規模モデルのサステナビリティや今後の発展の方向性などについても議論が行われ、今後の研究を考える上で大変参考になるお話を多く聞くことができました。また、後者の講演では、文脈などの見えないものを「見立てて見て取る」ヒトの能力を、コンピュータで解明するにあたっての課題と展望について伺いました。人が情報を認識するときに、情報そのものだけでなく、誰が言っているかなどの文脈を考慮して認識しているというご指摘が興味深かったです。 若手プログラム 弊社の研究員である後藤が、MIRU2023若手プログラムに参加しました。本プログラムは画像認識の分野で研究をしている若手を対象に、研究・開発を進める上で役立つ知見を学び合い、参加者同士で交流をする企画です。今年は「データをMIRU」をテーマに、9つのグループに分かれて画像認識に深く関わりのある様々な領域のサーベイを行いました。 後藤は、「データとプライバシー」に関するグループサーベイを行いました。データ駆動のビジネスモデルが勢いを強める中、学習データや出力に関するプライバシーや著作権に関する訴訟が増加しています。作成した資料では、公開データセットの学習に関するプライバシーの問題点を明らかにし、データセット・法律・アルゴリズムによる解決法を議論しています。 若手の会グループサーベイ資料は以下のページから見ることができます。 sites.google.com インタラクティブセッション ZOZO Researchからはインタラクティブセッション2件を発表しました。各研究の要約は以下の通りです。 [IS3-46] 着用者の体型を考慮したファッションコーディネート推薦 平川優伎, 斎藤侑輝 (ZOZO Research) パーソナライズされたファッションコーディネート推薦を実現する上で着用者の体型情報は重要な情報です。一方で、先行研究としてドレスなど単一のアイテムと体型情報を考慮した研究が知られていますが、複数のアイテムから成るコーディネートを考慮する場合は議論の余地があります。そこで、本研究では集合マッチングに基づくコーディネート推薦モデルを拡張し、着用者の体型とコーディネートを構成する複数のアイテムの相性を推定する深層学習モデル及び学習手法を提案しました。さらに、コーディネートを構成するアイテム画像と着用者の体型データから成る独自データセットを用いた性能評価実験により、コーディネート補完問題における提案手法の優位性を示しました。 [IS3-87] ファッショントレンドの検出と予測:SNS投稿データのクラスタリングと時系列解析 北岸毅一, Sai Htaung Kham, 後藤亮介 (ZOZO Research) ファッショントレンドの形成は多様な要素が絡み合うものであり、その分析は専門的知識と高度な労力を必要とします。これにより、トレンドの分析が頻繁に更新されることは少なく、微細なトレンドは見落とされる傾向にあります。本研究では、5年間のファッションSNSの投稿データから、タグデータの潜在トピックを抽出し、スナップ画像からのスタイルクラスタを構築しました。さらにGranger causality検定を用い、スタイルに影響を与えるトピックを検出しました。これにより、月単位でのファッションスタイルの変遷と、その発生要因の検出を自動化するための手法を確立しました。 気になった研究発表 私たちが個人的に興味を持った研究について紹介します。 [OS3B-L2] Instruct 3D-to-3D: Text Instruction Guided 3D-to-3D conversion Hiromichi Kamata, Yuiko Sakuma, Akio Hayakawa, Masato Ishii, Takuya Narihira (Sony Group) この研究では、テキストの指示に従って画像を編集する拡散モデルであるInstructPix2Pixの拡張として、テキスト指示による3Dモデルの編集に取り組んでいます。ただし、単純に学習方法を3次元に拡張するのではなく、NeRFとInstructPix2Pixを組み合わせることで3Dモデルの編集を実現しています。具体的には、まず、元の3Dモデルを作成するsourceモデルとそのコピーとしてtargetモデルを準備します。次に、元の3Dモデルから取得した2D画像に対しノイズを加え、InstructPix2Pixでテキストに基づきノイズを推論します。最後に、ノイズの推論結果からScore Distilation Samplingと呼ばれる方法でtargetモデルの勾配を取得し、targetモデルの重みを更新していきます。これにより編集後の3Dモデルを作成するモデルを得ることができるとのことです。また、この研究では、Dynamic Scalingという提案手法により編集強度の制御を可能にしているそうです。結果は こちらのプロジェクトページ からも見ることができるそうなので、興味のある方はぜひ見てみてください。 [OS4A-L2] 数式ドリブン教師あり学習によるセマンティックセグメンテーション 篠田理沙 (AIST, 京大), 速水亮 (AIST, 東京電機大), 中嶋航大 (AIST), 井上中順, 横田理央 (東工大), 片岡裕雄 (AIST) データセットの作成の際には、各データに正解ラベルを付与するのには膨大な人的・時間的コストが掛かってしまうという課題や不適切なデータが混入してしまう危険性があります。この研究では、特にセマンティックセグメンテーションというタスクに着目し、様々な中空の多角形を数式に基づいて生成することで、実画像や人手のアノテーションを必要としないデータセットを作成されていました。さらに、提案データセットとCOCO-Stuff-164kという実画像データセットのそれぞれについて事前学習を行なったモデルの、ファインチューン後の精度を比較されていました。結果としては、ADE-20k・Cityscapesという実画像データセットに対するファインチューンについて、提案データセットを用いたモデルの方がより良い結果を出したとのことです。個人的には、数式に基づいてデータを生成しそれを学習することで、実画像よりも良い事前学習ができてしまうというのはとても面白くかつ強力な手法であると思いました。企業で研究している身としては、商用利用が可能な点も非常に嬉しい点です。また、画像データだけではなく、音声や言語といったデータにも拡張できるのかといった点も気になりました。 [OS1A-L2] アテンションはアノテーションの代わりになるか?:テキスト−画像生成モデルの注意機構を利用した領域分割の弱教師あり学習 吉橋亮太, 大塚雄也, 土井賢治, 田中智大 (ヤフー) 近年、Semantic Segmentation、Poanoptic Segmentation、Instance Segmentationなど様々な領域分割タスクに関する研究が盛んに行われています。一般に、領域分割モデルの学習には大量のセグメンテーションマスク付きの画像が必要になりますが、1枚あたりのアノテーションコストが非常に高いため、データセットサイズがスケールしないという課題があります。本研究では拡散モデルベースの画像生成モジュールと領域分割モジュールを組み合わせることで、テキストと画像のペアからセグメンテーションマスク付き画像をEnd-to-Endに生成する手法を提案されています。このアプローチでは、セグメンテーションマスクのアノテーション付き画像が生成されるため、領域分割モデルの学習データとして使用する場合に、アノテーションが原理的に不要になります。PASCAL VOCデータセットを用いた実験では提案手法は生成データのみを用いてmIoU 50%程度と比較的良好な結果を達成できたそうです。ファッションの領域においても、スナップ画像の認識タスクなどを高精細に実行するためには、領域分割の技術が必要不可欠です。本研究のようなアプローチを用いて生成した大量のラベル付き画像を用いて半自動的に学習した領域分割モデルを、実データの分析や認識モデルの性能向上に活かすことができるのか気になります。 [IS2-16] セグメンテーションマスクを利用した動画からの静的なNeRF表現の学習 大隣嵩 (東大), 池畑諭 (NII, 東工大, 東大), 相澤清晴 (東大) この研究は、動く物体を含む動画からのNeRF表現の学習に関する研究です。NeRFは、特定の物体を様々な角度から撮影し、その結果をもとに高品質な3Dモデルを生成する技術です。複数の角度から撮影された画像を動画で代用する研究はよく知られていますが、撮影中に物体が動いてしまうと3Dモデルの作成が難しくなるという課題が存在します。この問題を解決するため、SegFormerから取得したマスクをタスクに合わせて拡張し、動的な物体を除外して3Dモデルを生成するという提案がされていました。具体的には、Test-time augmentation(複数の拡張結果を融合させることでエラーを軽減する)や、Gradual mask dilation(動く物体を覆う領域を広げて動く物体をもれなく学習から除外する)という方法を用いたとのことです。ファッション分野においても、このような手法の進化により、より容易にリアルな質感の3Dモデルが作成できるようになる可能性があると感じました。 まとめ 本記事では、MIRU2023の参加レポートをお伝えしました。昨年に引き続き今年のMIRUも現地で楽しむことができ、研究のトレンドや技術進展を学び、多くの方々と意見交換を行う良い機会となりました。MIRU2023で得られた知見や経験を今後の研究に取り入れ、より良い研究開発を行なっていきたいと思います。 最後に ZOZO NEXTでは次々に登場する新しい技術を使いこなし、プロダクトの品質を改善できるエンジニアを募集しています。ご興味のある方は、以下のリンクからぜひご応募ください。 zozonext.com おまけ 学会の合間にさわやかのハンバーグを食べに行きました。さわやかはTV・YouTube等で何度も取り上げられている有名なハンバーグ店で、新鮮な牛肉を使ったハンバーグを実際に味わうことができたのは非常に嬉しかったです。
アバター
目次 目次 はじめに マイページ画面リプレイスに伴う課題 使用したライブラリ Objective-Cでリファレンス、Swiftでテスト リファレンス画像のファイルサイズを小さく デバイスも言語も一気にテスト 複数言語のテスト自動化 複数デバイスを一気にテストする方法 いにしえVCのためのスタブデータの用意 おわりに はじめに みなさん、こんにちは! 松井です。普段はWEAR iOSアプリ開発で、コードを書く筋肉をパンパンに鍛えています。WEARアプリは、長い歴史を持っており、まだまだObjective-Cで書かれたレガシーなコードも居座っているんです。そんな中、私たちは地道にリファクタリングを進めています。そうしたObjective-CからSwiftへのリプレイス戦略において、スナップショットテストを活用したお話をしたいと思います。 スナップショットテストと聞くと、一般的にはコードの修正前後でUIが変わってしまっていないかをチェックするためのテスト手法という印象が強いでしょう。しかし今回は単なる修正ではなく、大胆なリプレイスにおいて旧と新のViewController(以下、VC)を比較した際のUIリグレッション確認のためにスナップショットテストを活用しました。スナップショットテストの力を借りて、リプレイスは無事に完了! 0.5ptの微妙なズレや、他にもいくつかリグレッションを見つけることができ大助かりでした。 マイページ画面リプレイスに伴う課題 私たちが着手したのは、投稿したコーディネートや動画、持っているアイテムやお気に入りしたコンテンツを一元管理するマイページ画面です。Objective-C製のコードで動いており、テストが書かれておらず、そもそもテストを書きづらい設計になっていました。しかもこの画面、各項目をタブで仕分けしており、1つの画面内でタブを切り替えるごとにUIがガラリと変わります。ユーザのプロフィールも表示しているので、プロフィール情報によってもUIが変化するんです。確認パターンが多すぎて、まさに開発者の強敵! 確認に工数がかかり、人的なミスも誘発しやすいです。ならばここは、スナップショットテストで対抗しましょう! スナップショットテストの使い方としては、まずObjective-Cによって書かれた旧VCでリファレンス画像を撮ります。それを元にSwiftへ生まれ変わった新VCが旧VCの見た目を再現できているかをテストすることで、解決を図ります。 使用したライブラリ ライブラリはPoint-Freeの swift-snapshot-testing を選択しました。このライブラリは、デバイスごとのプリセットが用意されているという点が優れています。後述しますが、これによりデバイスのサイズを指定しなくても良くなり、複数のデバイスのテストを一度に行うことができるんです。 Objective-Cでリファレンス、Swiftでテスト さて、Objective-CとSwiftでの見た目の違いを検証する際の、具体的なコードをお見せします。 @MainActor final class MypageViewControllerTest : XCTestCase { // リファレンス画像撮影モードを切り替える private let isRecord : Bool = true func test コーディネートタブ_投稿 1 件() async throws { let vc : UIViewController if isRecord { // VCを作成して返すメソッドは別途作成する必要があります。 vc = makeOldMypageViewController(tabType : .coordinate, dataCount : 1 ) } else { vc = makeNewMypageViewController(tabType : .coordinate, dataCount : 1 ) } snapshot(vc : vc ) } private func snapshot (vc : UIViewController ) { // テスト対象のViewControllerを表示 UIApplication.shared.firstKeyWindow?.rootViewController = vc // スナップショットテスト実行部分 // .imageHEICに関しては後述します。 assertSnapshots(matching : vc , as : [ .imageHEIC ] , record : isRecord , testName : "testコーディネートタブ_投稿1件" ) } } isRecord変数を定義し、このフラグを使うことでObjective-CとSwift、どちらのVCをテスト対象とするかを切り替えています。実際にスナップショットテストを実行するコードは、 snapshot(vc: UIViewController) メソッドの中に閉じ込めています。 上記のテストコードを実行すると、isRecordがtrueの時は旧VCのマイページ画面でリファレンス画像が生成されます。isRecordをfalseにして実行すると、リファレンス画像と新VCのマイページ画面を比較するテストが走ります。リファレンス画像どおりの見た目になっていたらテスト成功となります。失敗した場合は、失敗画像が生成されるので、リファレンス画像との差分を見比べて新VCのコードを修正します。 普通にテストを実行するだけならここまでの話で十分なのですが、swift-snapshot-testingには、強力な機能がほかにも備わっています。次のセクションからは、私たちが実際に使ってみて「これは便利だな」と感じたTipsをいくつか紹介します。 リファレンス画像のファイルサイズを小さく ここでは前節で省略した、 assertSnapshots の引数に指定した .imageHEIC について説明します。 リファレンス画像が増えたり、サムネイル画像が含まれていたりすると、どうしてもファイルサイズが大きくなってしまいます。しかもGitで管理したい場合、なおさらファイルサイズが気掛かりですね。ライブラリのREADMEを見ると多数の拡張ライブラリが紹介されており、その中に SnapshotTestingHEIC というHEIC形式でリファレンス画像を出力してくれる拡張ライブラリを発見しました。swift-snapshot-testingはPNG形式でリファレンス画像を出力しますが、PNGよりHEICのほうがファイルサイズを小さくできます。こちらを採用した結果、ファイルサイズを1/4〜1/3くらいまで落とすことができました。 上のコードに出てきた .imageHEIC というキーワードは、このSnapshotTestingHEICが提供している機能だったというわけです。拡張ライブラリは、他にもいくつか紹介されています。プロジェクトのニーズに合う拡張機能がないか確認してみると良いかもしれません。 デバイスも言語も一気にテスト WEARでは、日本語、英語、中国語(簡体字・繁体字)と複数の言語をサポートしています。そのため、言語が変わった時の表示と、複数のデバイスでの表示も確認しておきたいですね。スナップショットテストを使えばこうした色んな条件でのテストも楽チンです。言語の自動化はTestPlan、デバイスはswift-snapshot-testingが提供している各端末サイズのプリセットを使えば可能です。 複数言語のテスト自動化 まず、XcodeでTestPlanを新規作成します。TestPlanの Configurations > Application Language でテスト対象の言語を設定するだけです。あとはXcodeが自動的に、選択した言語環境でテストを回してくれます。 複数デバイスを一気にテストする方法 次に複数のデバイスを1度にテストするための、管理と実施方法について説明します。今回swift-snapshot-testingを採用した決め手となった、プリセットの出番です! まずは、テストしたいデバイスをまとめておくためのSnapshotConfigというenumを作ります。これには ViewImageConfig を使います。これこそがswift-snapshot-testingが提供する便利な機能で、それぞれのデバイスに対するプリセット情報を持っています。 import Foundation import SnapshotTesting enum SnapshotConfig : CaseIterable { case iPhone8 case iPhone13 case iPhone13ProMax case iPad9_7 func device () -> ViewImageConfig { switch self { case .iPhone8 : return ViewImageConfig.iPhone8 case .iPhone13 : return ViewImageConfig.iPhone13 case .iPhone13ProMax : return ViewImageConfig.iPhone13ProMax case .iPad9_7 : return ViewImageConfig.iPad9_7 } } } これで準備完了です。あとはテストケースで SnapshotConfig.allCases を回すだけ。一度のテスト実行で、複数のデバイスのテストが可能です。 @MainActor final class MypageViewControllerTest : XCTestCase { private let isRecord : Bool = true func test コーディネートタブ_投稿 1 件() async throws { let stubUser = makeStubUser() WRAccountManager.sharedInstance()?.setValue(stubUser, forKey : "user" ) SnapshotConfig.allCases.forEach { let vc : UIViewController if isRecord { vc = makeOldMypageViewController(tabType : .coordinate, dataCount : 1 ) } else { vc = makeNewMypageViewController(tabType : .coordinate, dataCount : 1 ) } snapshot(vc : vc , config : $0 ) } } private func snapshot (vc : UIViewController , config : SnapshotConfig ) { UIApplication.shared.firstKeyWindow?.rootViewController = vc let suffix = " \( config ) - \( Locale.preferredLanguages.first ?? "" ) " assertSnapshots(matching : vc , as : [ .imageHEIC ( on: config.device )] , record : isRecord , testName : "testコーディネートタブ_投稿1件" + suffix) } } ここで紹介した実装方法は、 メルペイiOSチームのスナップショットテストを効率化した話 を参考にしました。 いにしえVCのためのスタブデータの用意 さいごに、あなたのいにしえVCでも使えるかもしれないスタブデータの用意の仕方をご紹介します。 スタブデータの作成、今回これがなかなかの難敵でした。マイページ画面はユーザ情報によりUIが変化します。テストケースごとにこの情報を変えたいのですが、お相手は、Objective-Cで書かれたダシの効いたコード。ユーザ情報はAccountManagerクラスでuserプロパティとして保持されており、userプロパティはreadonlyになっていることから、スタブデータのセットが難しい状況でした。AccountManagerクラス自体の改修は、今回のリプレイスに関係のないコードにまで連鎖的に影響を及ぼす可能性があり、そう簡単には手出しできません。そんな時、大活躍したのが Key-Value Coding (KVC)を使う方法です。KVCはNSObjectが標準で提供している機能で、これを使えば、例えプロパティがreadonlyだったとしても、Objective-Cの変数の値をいじったり取り出したりできます。 具体的な使い方は以下のような感じです。 func test ヘッダー_性別のみ表示() async throws { let stubUser : User = makeStubUser() stubUser.sexName = "WOMAN" stubUser.height = nil AccountManager.sharedInstance()?.setValue(stubuser, forKey : "user" ) // 以下、スナップショットテスト実行コード } まずスタブのユーザ情報を持ったstubUserを作成します。上記の例では性別を女性、身長を未設定にしています。このstubUserをAccountManagerのuserプロパティにsetValueメソッドを使ってセットすることで、テストケースごとにユーザ情報をコントロールすることが可能になります。とても便利な機能ですが、あくまでNSObjectが標準で提供するものですので型チェックをスルーしてしまうところが落とし穴です。プロダクトコードでの使用は避けたほうが良いですが、今回はテストコードだったため使用しています。 おわりに Swiftに生まれ変わったマイページ画面がリリースされ、Objective-C製のマイページにさよならするとき、リグレッションの役目を終えたスナップショットテストも削除しました。役目を全うしたスナップショットテスト、おつかれさまでした! 今後も大掛かりなリプレイスをする際に活躍してくれることでしょう。この記事が、みなさんのプロジェクトでのリプレイス戦略にも何かのヒントになれば嬉しい限りです。より良いソフトウェア開発をこれからも進めていきましょう! ZOZOでは一緒に楽しく働くエンジニアを絶賛募集中です。ご興味のある方は下記リンクからぜひご応募ください。 corp.zozo.com
アバター
こんにちは。カート決済部カート決済基盤ブロックの長沼です。先日Javaアプリケーションをリリースしたのですが、リリース後にOld領域のメモリ使用量がわずかに増加し続ける現象が発生しました。本記事ではこの現象の調査にて得られた知見を共有します。 本記事で共有すること 前提知識 JVM(Java Virtual Machine)とは ヒープ領域とは GC(ガベージ・コレクション)とは GCのアルゴリズム G1GC(ガベージファースト・ガベージ・コレクタ)とは G1GCのメモリ管理 ヒープ領域の概念図 世代別領域に対するGCの基本処理 Young GC 混合GC Full GC ソフト参照 Javaの参照 ソフト参照とGC まとめ 最後に 本記事で共有すること GCの概要を理解するための前提知識を説明した後、G1GC(ガベージファースト・ガベージ・コレクタ)がメモリをどのように扱うか説明します。最後にメモリ使用量増加の原因となっていた ソフト参照 について説明します。 前提知識 まず、G1GCのメモリ管理方法を理解する際に前提となる事柄を説明します。 JVM(Java Virtual Machine)とは Javaバイトコードを実行するための仮想環境です。JDK(Javaのプログラムを開発するためのソフトウェアパッケージ)に含まれています。 ヒープ領域とは Javaプログラム実行時に生成したオブジェクトを割り当てるためのメモリ領域のことです。JVMのメモリ管理システムによって管理されます。 GC(ガベージ・コレクション)とは プログラムが使用し終わったメモリ領域を探し、割り当てていたメモリを解放し他のプログラムで利用できるようにする機能を GC(ガベージ・コレクション) と呼びます。 GCのアルゴリズム GCのアルゴリズムにはいくつか種類があり、アプリケーション起動引数により切り替えできます。 OpenJDK 17 64Bit Server VM の場合はG1GCの他に シリアル・コレクタ 、 パラレル・コレクタ 、 Zガベージ・コレクタ が選択できます。アプリケーションの特性やマシンリソースにより、最適なアルゴリズムは異なります。 G1GC(ガベージファースト・ガベージ・コレクタ)とは 大量のヒープ領域を最低限の停止時間で処理することを目標としたアルゴリズムです。マルチプロセッサ・マシンで動作することを前提として、アプリケーションの実行と同時に処理の一部を行うことでGCによる停止時間を短くします。 G1GCのメモリ管理 ヒープ領域の概念図 G1GCは世代別ガベージ・コレクションです。下の図はヒープ領域の概念図 1 です。 ヒープ領域は Young世代 と Old世代 に分類されます。Young世代はさらに Eden領域 と Survivor領域 に分類されます。Old世代は Old領域 のみとなります。 世代別領域に対するGCの基本処理 G1GCアルゴリズムでは3種類のGCが発生します。 YoungGC(Young-Only Collections) 混合GC(Mixed Collections) Full GC(Full Garbage Collections 2 ) 以降、順に処理の流れを説明します。 Young GC Young GCは主にYoung世代のYoung領域・Eden領域のメモリを管理します。 まず、オブジェクトがEden領域に配置されます。 Eden領域がいっぱいになるとYoungGCが発生します。この時、未使用オブジェクトは解放されます。使用中オブジェクトはSurvivor領域へ移されます。 空になったEden領域がいっぱいになると、再びYoungGCが発生します。この時、Survivor領域のオブジェクトが使用されていなければ解放されます。 Survivor領域がいっぱいになっていてEden領域から移動できない場合は直接Old領域へ移されます。 7〜15回 3 のYoungGCを経てもSurvivor領域から解放されなかったオブジェクトはOld領域へ移されます。 混合GC 混合GCは主にOld領域のメモリを管理します。 ヒープ領域が逼迫してくると 混合GC が発生します。混合GCによりOld領域内の未使用オブジェクトが解放されます。 なお、混合GCの前には必ずYoungGCが実行されます。上の図はYoungGC後に混合GCが発生してOld領域を解放する様子を表しています。 Full GC Full GCは混合GCが間に合わず、Old領域にて割り当て可能なメモリが不足した時に発生します。 Full GCを行っている間、アプリケーションは完全に停止します。長時間停止してしまうことがあるためこの現象は STW(Stop the world) と呼ばれ忌避されています。 ソフト参照 前半では世代別領域に対するGCの基本処理の流れを見てきました。ここからはメモリ使用量増加の原因となっていた ソフト参照 について説明します。 Javaの参照 通常の方法でオブジェクトのインスタンスを生成した場合の参照は 強い参照 と呼ばれます。これまで説明してきたのは 強い参照 に対するGCの挙動でした。 Javaの参照は他に、 ソフト参照 、 弱い参照 、 ファントム参照 があります。これらの参照を使用している場合、 強い参照 の時とはGCの挙動が異なります。 ソフト参照とGC java.lang.ref.SoftReferenceクラスを使用すると、 ソフト参照 の仕組みを利用できます。ソフト参照はヒープ領域が逼迫されるとGC対象となります。キャッシュを実装するために使用されることが多いです。 強い参照の場合、未使用であることがメモリ解放の対象となる条件でした。ソフト参照は最長不使用(LRU)アルゴリズムにより解放対象を決定します。具体的には下記の式 4 に該当するものを解放対象とします。 現在時刻(ミリ秒) - 最終使用時刻(ミリ秒) > ヒープ内空きメモリ(MB) * 1000 なお、ソフト参照のメモリ解放には複数回混合GCが必要になります。 まとめ ソフト参照はメモリ解放となる条件が強い参照と異なるため、アプリケーションの挙動を確認するためには長時間シナリオのテストが必要です。本記事の調査では開発環境にて70時間の負荷試験を行い、複数回の混合GC発生後にOld領域使用量が減少することを確認しました。 リリース後に慌てることがないよう、長時間シナリオテストを実施しましょう。 最後に カート決済部では、仲間を募集しています。ご興味のある方は、こちらからご応募ください。 hrmos.co 実際のヒープ領域は均等のサイズに細かく区切られたリージョンから構成されていて、どの世代の管理を行なうかはリージョンごとに割り当てられます。 Garbage-First (G1) Garbage Collector ↩ Webで情報を検索しやすいよう、括弧内は英語の正式名称を記載しています。 Full Garbage Collections のみ Garbage が含まれていますが、これが正式名称です。 HotSpot Virtual Machine Garbage Collection Tuning Guide ↩ 閾値は初期値が7回、最大15回です。 -XX:InitialTenuringThreshold や -XX:MaxTenuringThreshold オプションで変更できます。 Java HotSpot VM Options ↩ 参考文献: Scott Oaks(著), アクロクエストテクノロジー株式会社(監修), 寺田 佳央(監修), 牧野 聡(翻訳), “Javaパフォーマンス”, オライリー・ジャパン, 2015/4/11, 227頁. 1000はSoftRefLRUPolicyMSPerMBのデフォルト値です。 -XX:SoftRefLRUPolicyMSPerMB オプションで変更できます。 左辺と右辺で単位が異なりますが、そういうアルゴリズムです。 ↩
アバター
はじめまして。2023年4月に新卒として株式会社ZOZO(以下ZOZO)に入社しました、 財部彰太(たからべ しょうた) と申します。 この記事では、現在私が所属しているZOZOTOWN開発本部ZOZOTOWNアプリ部にて2022年5月から2023年3月までの期間で参加した内定者アルバイトについての話をさせていただきます。参加した理由、結果、そしてアルバイトを通して知った会社・チームの素敵な点を紹介させていただきます。ZOZOに興味がある人、内定後の動きに不安がある人、そもそも新卒でエンジニアとして働くか迷っている人、色々な人に読んでいただけたら幸いです。 内定者アルバイトとは 私が内定者アルバイトに参加した目的 ZOZOでの内定者アルバイトとは 実際にアルバイトをした部署、チームの話 なぜZOZOTOWN Androidチームにしたのか ZOZOTOWN Androidチームはどんなチームなのか 素敵な文化(1) チームとして新しい知識を取得するための時間を業務時間中に多く設けている 毎日の朝会後の読書会 週に1度のAndroid技術活動会 月に1度のデザイナーと共に行うMaterial Design勉強会 & 実装会 素敵な文化(2) お互いのいいところを積極的に見つけていく文化がある フルリモートワークでも問題なかったか 朝、夕に行う進捗、困りごと共有 週2回の1on1 実際に行ったタスク Android AnnotationsをZOZOTOWN Androidアプリから剥がす どのように進めたか 結果 内定者アルバイトに参加した目的は達成されたのか 余談 最後に 内定者アルバイトとは 内定者アルバイトとは、内定承諾から入社までの期間を利用して、アルバイトとして内定先で就業できる仕組みのことです。 私が内定者アルバイトに参加した目的 内定者アルバイトを行うメリットは以下の点があると考えております。 実際の業務に参加することで、自分の興味があることや得意なことを入社前に見極めることができる 内定者時代から入社後の業務や環境に慣れることができる 部署の雰囲気を知ることで、配属後のミスマッチを減らすことができる 上記のメリットを踏まえて、入社後に好調なスタートを切るために、私は内定者アルバイトに参加しました。 ZOZOでの内定者アルバイトとは ZOZOでは、就活の選考時に志望している職種以外のポジションにアルバイトとして応募、参加が可能です *1 。 入社後の活躍イメージが想像できず不安、今後の主軸としていく職種が定まっていない、配属後の業務内容の解像度を上げたい、入社までにスキルアップしたいなどの悩みや希望がある方も多いかと思います。内定者アルバイトの機会を上手く活用することで、不安解消に繋がり安心して入社できるかもしれません。 ZOZOの開発部門はフルフレックス制度を導入しており、自分にあった形で働くことができます。アルバイトについてはシフト制のためフルフレックス制度ほどの自由度はありませんが、基本的に希望に沿ったシフトが組めますので、学業に支障のない範囲での勤務が可能です。 実際にアルバイトをした部署、チームの話 私は2022年2月に内定をいただき、同年の5月から翌年2023年3月の大学卒業までの11か月間ZOZOTOWNのAndroidアプリを開発するチームで内定者アルバイトを実施しました。 なぜZOZOTOWN Androidチームにしたのか 私がZOZOで働きたいと思ったきっかけは、昔からZOZOTOWNアプリが好きだったからでした。使いやすく、デザインもとても可愛い。そして内定後、アルバイトの募集一覧を見たときにZOZOTOWNアプリの募集があり、ここしかないと思い応募しました。ZOZOTOWNアプリに関する募集はiOS/Androidともにありました。その中から、元々私がAndroidユーザーかつ、Androidエンジニアとして学習を行なっていた経緯から、Androidチームを希望しました。 ZOZOTOWN Androidチームはどんなチームなのか 2023年7月時点で、ZOZOTOWN Androidチームは10名程度の規模で構成されています。 チームのコミュニケーションはSlack、Google Meet、Miro等で行われています。チームには沢山の文化があります。全てを紹介するのは無理なので、私が特に素敵だと感じた文化を2点紹介します。 チームとして新しい知識を取得するための時間を業務時間中に多く設けている お互いのいいところを積極的に見つけていく文化がある 素敵な文化(1) チームとして新しい知識を取得するための時間を業務時間中に多く設けている 現在ZOZOTOWN Androidチーム内で行なっている勉強会は多数ありますが、この場では3つ紹介させてください。 毎日の朝会後の読書会 この会では、テックリードが中心となって本を読んでいきます。30分×週3回の頻度で行なっていて、本の長さにもよりますが、だいたい2か月で1冊読み終えているペースです。ただ読むだけではなく、テックリードによる解説や、メンバーの経験談の共有、質問の受け答えの時間が設けられており、双方向のやり取りで理解を深められる形になっています。読書会の中での疑問点や、会話の内容はMiroを使用してまとめています。 これまでに、『 読みやすいコードのガイドライン 』、『 かんたんUML入門 』、『 Clean Architecture 』といった本を読書会の中で読んできました。 週に1度のAndroid技術活動会 この会は、週に1度、1時間の頻度で行なっています。 AndroidDagashi や Android Developers Blog の新着記事、JetpackのライブラリやAndroid Studioの新しいリリース内容といった最新情報を追ったりしています。他にも、困り事の相談や、チーム内のガイドラインの更新があった時に共有する時間にもなっています。こちらもテックリードが主導して行なっています。 月に1度のデザイナーと共に行うMaterial Design勉強会 & 実装会 Material DesignはGoogleが提唱しているデザインシステムであり、Androidプラットフォームではこちらのデザインシステムに準拠することが推奨されています。こちらの会はそのMaterial Designをデザイナーと共に学習し、プロダクトに反映させていくというものです。詳細は別記事の『 AndroidアプリをMaterial Designのガイドラインに準拠させるための取り組み 』で紹介しています。 こうした時間を業務内に確保してくれることで、なかなか業務外だと本を読んだり記事を読んだりする時間が取れない人でも、会を通じて着実にインプットできます。 素敵な文化(2) お互いのいいところを積極的に見つけていく文化がある ZOZOTOWN Androidチームでは週1度振り返りが行われています。振り返りの中ではKPTを行なったり、次の1週間のタスクの計画を立てたりします。 その中で、私が特に素敵だと思った文化が、「今週のいいね」です。今週のいいねでは、この1週間でいいねと思ったことを下記のようにMiroで作られたフォーマットに書き出して発表します。 いくつか抜粋して紹介します。 積極的でいいねといった内容だったり、ちょっとした小話的な内容もあったり、この新しい取り組みがいいねと言った内容もあります。最後の1枚はネーミングセンスを鍛える取り組みについての内容ですが、こちらは同チームのテックリードによるものです。興味がある方は、別の記事でまとめられている「 ネーミングセンスを鍛える会の取り組み 」を覗いてみてください。 毎週の振り返りでは、こういった「いいね」が20個、多いときは40個集まる時もあります。褒められて嬉しいし、褒めても明るい気持ちになれるし、とっても素敵な文化だと感じています!「今週のいいね」でいい体験をして、次の週ではさらに同僚のいいところを見つけていこう、自分もいいところを作っていこうとしていけるので皆が幸せになれますね。 こういったお互いのいいねを送り合うことは、部署を越えて行なったりもしています。 フルリモートワークでも問題なかったか 内定者アルバイト期間、私はフルリモートで働きました。ZOZOのエンジニアはリモートワークをするか、オフィス出社をするか選択できます。ZOZOTOWN Androidチームはリモートワークの方が多いです。 リモートで働き始めると、場合によっては困難に直面することもあると思います。環境構築のつまずき、孤独感、人によって様々なストレスを感じるでしょう。新卒や学生のような経験が浅い場合だと尚更です。しかしZOZOTOWN Androidチームは以下のような形でアルバイトに参加でき上記の困難に遭遇せずに働くことができました。 朝、夕に行う進捗、困りごと共有 アルバイトとして入社して3か月の間、朝と夕にメンターと上長の3人で進捗や困りごとを共有する時間をいただいていました。人によっては多く感じる頻度かもしれませんが、私にとってはとてもありがたかったです。技術的な相談はもちろん、壁打ちの時間としても有効活用できました。チームに参加してすぐのタイミングだと、自分でも気づかないうちに間違った方針で進めてしまうこともあると思います。朝、夕の頻度で共有の時間をいただけて方針の修正ができてとても助かりました。 週2回の1on1 ZOZOでは社員の成長を支援するために、上長と1on1をする制度があります *2 。 こちらは正社員向けの制度ではありますが、ZOZOTOWN Androidチームではアルバイトの期間でも行なっていただきました。私のチームでは上長、テックリードとそれぞれ週1度1on1を実施しています。1on1では困りごとの共有、この先数年単位でどんなことをしていきたいか、キャリアにしていきたいかといった話や、気軽な雑談も行われます。キャリアや仕事の話ももちろんありがたかったですが、私にとっては気軽な雑談を行えるのがとても嬉しかったです。ZOZOに来るまでにもリモートワークを少しだけ経験しましたが、どうしても気軽な雑談が減ってしまい、同僚の人となりや趣味といった情報がなく、少しだけ無機質に感じることがありました。同じように感じる人も多いと思います。ZOZOでは1on1を通じて、より同僚を身近に感じることができるようになりました。結果的により何でも話せるようになり信頼が深まったように感じます。1on1のおかげで孤独感を感じることなく働くことができました。 実際に行ったタスク 約1年間の内定者アルバイトの中で、色々なタスクを行いましたが、今回は最初にいただいたタスクについて紹介しようと思います。 Android AnnotationsをZOZOTOWN Androidアプリから剥がす タスクの提案をいただくにあたって、アルバイト配属前の面談で以下のような希望を出していました。 ZOZOTOWN Androidアプリの広い範囲をよく知りたい エンジニアとしての能力を向上したい それに対して提案をいただいたのが、この「Android AnnotationsをZOZOTOWN Androidアプリから剥がす」というタスクになります。Android Annotationsはアプリの広い範囲で使われていて、改修する中でアプリの広い範囲に触れることができ、1つ目の希望と合っています。またAndroid Annotationsは古くから存在する画面で多く使われていたため、改修する中で現在チームが推進しているアーキテクチャに刷新していく必要もありました。チームが推進したいアーキテクチャを理解できるという点で、2つ目の希望にも合っています。 このタスクの背景としては、ZOZOTOWN Androidチームがビルド時間増加を課題と認識していたことが挙げられます。ビルド時間を短くするための手段として、 kaptをKSPへ移行する ことが有効です。このKSPへの移行のためには、KSPに対応していないライブラリをアプリから剥がす必要があり、Android Annotationsは剥がす必要のあるライブラリの1つでした。Android Annotationsを剥がすタスクは、自分の希望に沿うのみでなく、プロダクトの課題解決にもつながるとわかり、より意欲が高まりました。 どのように進めたか 私は当時、週に3日間(月曜日から水曜日)働いており、1日8時間の作業時間を確保していました。毎週月曜日にその週のタスクを計画し、水曜日までに完了しなかったタスクはメンターに引き継ぎました。実装後は、メンターと2人のチームメンバーによるレビューを受けて修正作業を行いました。タスクはクラス単位で分割され、修正が完了するたびにプルリクエストを提出しました。 結果 最終的に全体の5割のタスクを任せてもらい、およそ1か月半でリリースまで持っていくことができました。KSPへの移行はほかのライブラリが原因でできてはいませんが、1つ大きな障害を取り除くことはできました。 もともとの希望も叶えることができました。1つ目の「ZOZOTOWN Androidアプリの広い範囲をよく知りたい」という希望に対しては、リニューアルが行われていたホームタブ以外のタブは全てこのタスクの中で読むことができました。結果、「〇〇の画面に機能を追加する」といった話があった時に、おおよその改修箇所の目星をつけられるようになりました。 2つ目の「エンジニアとしての能力を向上したい」という希望に対しても、古い画面にチームが現在推奨しているアーキテクチャを適用していく中で、アーキテクチャへの理解を深めることができました。また非同期通信をKotlin Coroutinesに刷新する必要がある時に、理解が浅かった自分のためにメンターの方が1時間以上かけて説明をしてくださるなど、自分が予想していたよりも遥かに多くの学びの機会を頂く事ができました。 レビューでも自分では気がつけない多くの改善点を教えてくださり、こちらでも多くの学びを得ました。メンターの方、レビューをしてくださった方、本当にありがとうございました。 タスクを通して、自分が触ってきたアプリの裏側を見ることができて純粋に嬉しかったことを覚えています。またレガシー部分を刷新していく楽しさも強く感じました。アーキテクチャへの理解を深められることはもちろん、これまでユーザーに使ってもらえてきた画面が改修によってより長い期間存続できるようになると思うと、とても嬉しいです。 内定者アルバイトに参加した目的は達成されたのか 冒頭では、私が内定者アルバイトに参加する目的は入社後に好調なスタートを切ることとお話ししました。こちらの目的は達成できたと考えております。最終的に私は、社員としての本配属もZOZOTOWN Androidチームに希望を出し、そのまま配属させていただいて、自分が望む環境とのミスマッチがなく働けていると感じています *3 。入社後大きなトラブルなくタスクの着手ができ、今後どのようなことをしていきたいかといった数年単位の話も具体的に考えることができました。アルバイト期間の経験があってのものだと思います。 余談 ZOZOは2019年からDroidKaigiに協賛しており、今年のDroidKaigi 2023にもブースを出展いたします! ぜひお越しください。 そんな2022年のDroidKaigi 2022では、チケット代を負担していただき、ブースにも参加しました。DroidKaigi 2022は自分にとって、オンラインオフライン問わずに初めてのカンファレンスでした。いろんな会社のブースで、普段聞けないお話を聞けるなど、オフライン参加ならではの体験が出来たためとても感謝しています。 最後に ZOZOでは新卒、中途問わずAndroidエンジニア、そしてインターンシップを募集しています。ご興味のある方は下記リンクからぜひご応募ください。 hrmos.co 2025新卒_就業型インターン(ZOZOTOWN/Androidエンジニア): https://public.n-ats.hrmos.co/zozo/jobs/1852303323965423616 *1 : 2023年7月現在 *2 : 福利厚生について https://corp.zozo.com/recruit/welfare/ *3 : 配属方針は年度ごとに異なる可能性があります
アバター
こんにちは、ブランドソリューション開発本部フロントエンド部の田中です。普段はWebのフロントエンドエンジニアとして、FAANSのWebの開発を行なっています。 FAANSとは「Fashion Advisors are Neighbors」がサービス名の由来で、ショップスタッフの効率的な販売をサポートするショップスタッフ専用ツールです。 ショップスタッフ向けにコーデ投稿・成果確認などの機能が存在し、2022年8月に正式ローンチしました。詳しくは以下のプレスリリースをご覧ください。 corp.zozo.com 弊社の福利厚生の中に海外カンファレンスを含む勉強会やセミナーの参加費用を会社で補助してくれる制度があります。今回その制度を利用して、オランダのアムステルダムまで渡航し、JSNationとReact Summitに参加致しました。 corp.zozo.com オンラインからの参加も可能で、プレゼンテーションのライブ配信やDiscordを利用したコミュニケーションを取れたり楽しいイベントとなっています。今回の記事では現地参加ならではの経験、特に会場内の雰囲気を中心に紹介していきたいと思います! JSNationとReact Summitとは? 2023年6月1日 - JSNation 質問ルーム ブース ランチタイム 休憩スペース 景品抽選タイム OSS Awards 2023年6月2日 - React Summit Reactの誕生日 After Party 2023年6月3日 - Boat Tour & Walking Tour 気になったセッションについて Modern Web Debugging Accessible Component System Through Customization 初めて海外カンファレンスに行ってみての感想 さいごに JSNationとReact Summitとは? JSNation と React Summit は GitNation が主催するカンファレンスです。JSNationはJavaScriptに関して、React SummitはReactに関連する発表やイベントを扱っていて、Next.jsやQwikなど関連した技術に関する発表もあります。また、アムステルダムの街を探索するイベントなどエンジニア同士で交流するイベントも用意されています。 現地参加の各カンファレンスのスケジュールは以下の通りです。今回参加できなかったのですが、別日にスピーカーの人から学べるワークショップも開催されています。 日付 時間帯(CEST) イベント 場所 2023/6/1 9AM - 5PM JSNation アムステルダム 2023/6/2 9AM - 10PM React Summit アムステルダム 2023/6/3 10AM - 1PM Hangout Day アムステルダム 以降、参加したそれぞれのイベントについて詳しく紹介していきます。 2023年6月1日 - JSNation JSNationの会場内は上の画像のような雰囲気でした。 プレゼンテーションは2会場で行われ、目星をつけていたセッションを中心に見ました。各国から著名な方が登壇し会場内は盛り上がっていました。 特に印象的だったのがプレゼンテーション中にデモが動かなく、発表者がChromeのConsoleのデバッグを始めた時に、会場内で拍手がわき起こったことです。 APIからのデータ取得時にエラーが発生し、その原因はWi-Fiが接続されていなかったためでした。会場内のWi-Fiに接続しデータの取得に成功したところ、再度拍手が湧き起こり、非常に暖かい雰囲気が広がりました。予想外のトラブルも楽しめて素晴らしかったです。 質問ルーム 質問ルームでは、プレゼンテーション終了直後に発表者に対して質問が可能です。 ブース 会場内にはスポンサーの展示ブースが設置されており、FAANSのWebでも利用しているChromaticとSentryのブースを訪れて、働いているエンジニアと直接話す機会を得ました。 Storybookに関して今後進めたいこと(VRTのローカル環境での実行といったテスト機能の追加、Storybookの導入とその記述コストの軽減など)やChromatic社の働き方を話しました。 Storybookのイベントでよく見かけるYann Bragaさんにも会場内でバッタリ会い、FAANSにおけるStorybookの活用方法についてお話ししました。ロゴ付きのステッカーを頂いたり、会話後に記念写真を撮ったり一生の思い出になりました。 Sentryについては最初にFAANSのWebでどう活用しているかヒアリングがあり、その後に知らなかった機能を紹介していただけました。その中でクライアント側のコードで不具合が発生した前後のユーザーの画面を再生できる機能をデモンストレーションしていただきました。FAANSのWebでも活用してみたいと思いました。 ランチタイム チュニジア人とイタリア人のエンジニアと、現在進行中のプロジェクトや使用している技術について話しながらランチを食べました。カンファレンス参加にあたって英語に対する不安があったのですが、出会ったエンジニアは私の英語が少し不自由だとしても理解しようと努力してくれました。そして、聞き取れなかった場合はGoogle翻訳を使って日本語訳を見せてくれ、その配慮に感動しました。 休憩スペース セッション外の場所では飲み物や軽食を受け取れるスペースがあり、休憩したり他の参加者と交流したりできます。 景品抽選タイム ブースを出展している企業から抽選で景品がもらえるイベントもありました。景品を引き当てた人は大いに喜んでいました。 OSS Awards オープンソースの授賞式もあり、各部門(例:生産性への貢献やもっともコミュニティーに影響与えた)ごとに賞が授与されました。使ったことがないライブラリや拡張機能があってとても参考になりました。特にTypeScriptのエラーを見やすく表示できるVisual Studio Codeの拡張機能のPretty TypeScript Errorsは使ってみたいと思いました。 2023年6月2日 - React Summit 2日目はReact Summitです。JSNationと同じ会場で行われたのですが、雰囲気は一気に変わり全体的に緑を基調とした空間でした。 Reactの誕生日 Reactが10周年を迎えるということでそのお祝いが行われました。会場内でケーキが配布されました。また、JSNationの時と同様にプレゼンテーション、ブースの出展、OS Awardsのイベントも行われていました。 After Party カンファレンスが終わった後にアフターパーティーが行われ、JSで作られたデジタルアートと音楽が流れ、盛り上がっていました。 2023年6月3日 - Boat Tour & Walking Tour 最終日にHangout Dayにてボートと散歩のツアーがありました。どちらのツアーもガイドの案内があり、アムステルダムについて知ることができました。 気になったセッションについて 最後に、特に印象に残ったプレゼンテーションを2つ紹介します。これらのプレゼンテーションはGitNationのウェブサイトでも視聴可能なので、是非確認してみてください。 portal.gitnation.org Modern Web Debugging portal.gitnation.org 最初に注目したのは、Chrome DevToolsのメンバーで、DevToolsの機能を頻繁に動画で紹介してくれるJecelyn Yeenさんの発表でした。設定した条件を満たした時点で停止する 「Conditional Breakpoints」の機能 や、 ネットワークパネルにおけるレスポンスヘッダーを上書きしてデバッグできる機能 などが紹介されました。 私は普段の開発の中でconsole.logを使ったりしますが、それと似た機能である 「Logpoints」を使ったデバッグ などの存在も初めて知りました。これからは開発者ツールのデバッグ機能をもっと活用していきたいと思いました。 いくつかの新たなデバッグ体験が紹介され、観客の間から拍手が湧き上がりました。隣にいたエンジニアと雑談し「新たなデバッグの機能を知ることができてよかった」と感想をシェアしました。 また、質問パートにおいて本番環境でminifyされたコードをデバッグする方法について質問していた方がいて興味深かったです。回答としては、そのminifyされたコードに対するSource Mapsをプライベートな場所に保存するという内容でした。 FAANSのWebはエラーログの収集ツールとしてSentryを使っていますが、ログがminifyされた状態で表示されていて追いづらい課題を抱えていました。 そこでSentryにSource MapsをアップロードするSDKが用意されており、minifyされたコードから開発コードへ復元された状態でログを追えることが分かりました。 FAANSのWebでは開発速度の向上を目的にCreate React AppからViteに移行し、 ViteでSource MapsをアップロードするSDK が用意されておりその一環で対応しました。 対応の内容として、ビルドして生成した本番コードとそれに紐づくSource MapsをSentryへアップロードするが、ユーザーへは公開しない対応を取りました。 これによりセキュリティーリスクを回避しつつログが追いやすい体制を整えることができました。 カンファレンスで得た内容が実プロダクトに活かされてよかったです。 Accessible Component System Through Customization portal.gitnation.org 他社においてコンポーネントライブラリがどのような過程で構築されるのかに興味がありプレゼンテーションを視聴しました。コンポーネントライブラリは全体のデザインシステムを定義した後の実装部分であるとし、それを構築する際は適切なデザインシステム(スタイルガイドやタイポグラフィーなど)の設計が必要であると述べていました。 また全ての状況をカバーできるような万能なコンポーネントライブラリを作ることはできず、作成する際にはその目的や対象を明確にする必要があると指摘していました。そこで、彼女の自身のプロジェクトであるStorefront UIはEコマース専用のコンポーネントライブラリに特化し、それを元にアクセシビリティーなどの要件を満たすように構築していると述べていました。 また、質問パートで既存のライブラリ(例えばRadix UIのようなもの)をカスタマイズするのではなく、ゼロからコンポーネントライブラリを作成する理由について質問していた方がいて興味深かったです。 一概に決められるものではなく状況によると述べていました。Microsoftのような大企業で働いている場合、内部の様々なチームが再利用可能なものを自前で作る必要があるとのことです。また、コードが成長するにつれて外部のライブラリを使用することは、バグ修正、保守性、テストなどの観点から問題を引き起こす可能性があるから自作で構築していると説明していました。 この話を聞いてプロジェクトや状況に応じた技術選定が大事で、改めてFAANSのWebにおけるコンポーネントライブラリの選定を見直すきっかけになりました。 FAANSのWebはChakra UIを使っていて外部のライブラリに依存しております。ただFigmaが提供されていたり、少しのカスタマイズで複雑なUIをスピード感もって実装できることから、少数な弊チームにあった選択肢だと再確認できました。 初めて海外カンファレンスに行ってみての感想 日本から遠いオランダでしかも初めての海外カンファレンスで不安がありましたが、参加してみて結果的には大満足でした。初対面の人々と会話するのに少々緊張していましたが、周りの人は私の話を理解しようとし、それによって技術についての話題で盛り上がることができてとても楽しかったです。その経験により私の不安は消え去り、開発者同士の繋がりを実感できました。また、Source MapsのSentryへのアップロード対応のようにカンファレンスで得た知識を実プロダクトに活かせて良かったです。著名なエンジニアからプレゼンテーションや会話を通して新たな知識や良い影響を受けたり、日々のフロントエンド開発のモチベーションにもなりました。更に深いレベルの情報共有をしたいと思うようになり、今後の英語学習のモチベーションに繋がりました。 さいごに 最後に、ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは。DevRelブロックの @wiroha です。7月11日に ZOZO Tech Meetup - iOS/Android を開催しました。ZOZOのiOSエンジニアとAndroidエンジニアがそれぞれの技術領域にフォーカスして紹介するイベントです。オンラインとオフラインのハイブリッドで開催しました。 オープニングの様子 登壇内容まとめ 今回は弊社から9人とたくさんのエンジニアが登壇しました。 コンテンツ 登壇者 [Android] JankStats LibraryでJankを検出しよう 高橋啓太 [Android] Compose for iOS for ZOZOTOWN 井上晃平・ 財部彰太 [iOS] Visionフレームワークを使って非接触でMacを操作しよう 中岡黎 [Android] ComposeでWidgetを実装できるライブラリ「Glance」がbetaになったので触ってみる 野々村樹 [iOS] Pathを活用してSwift Chartsの限界を超えろ 山田楓也 [Android] よく書く同じようなコードはAndroid StudioのLiveTemplateを使ってテンプレート化しよう 山田尚吾 [iOS] FAANS iOSアプリでGitHub Copilotを使い倒してみた 中島頌太 [iOS] 10年以上の歴史を持つプロダクトのRosettaフリー 〜その中で遭遇した問題と解決法を公開します〜 小松悟 当日の発表はYouTubeのアーカイブでご覧下さい。 www.youtube.com [Android] JankStats LibraryでJankを検出しよう 高橋啓太 / ZOZOTOWN開発本部 ZOZOTOWNアプリ部 Android2ブロック speakerdeck.com 高橋からは、Jank検出ツールであるJankStats Libraryについて発表しました。Jankとはフレームがスキップされてアプリがスムーズでなくなることを指し、その検出・修正は快適な操作感を維持するために重要です。JankStats Libraryは本番環境で発生しているJankの原因が分析可能になる点が優れているそうです。導入方法・使用方法・実装時に詰まりそうな点が紹介され導入しやすそうでした。 [Android] Compose for iOS for ZOZOTOWN 前半担当:財部彰太 @hamuyatti / ZOZOTOWN開発本部 ZOZOTOWNアプリ部 Android1ブロック 後半担当:井上晃平 @nemo-855 / ZOZOTOWN開発本部 ZOZOTOWNアプリ部 Android2ブロック speakerdeck.com こちらは2人からの発表です。前半は財部よりCompose for iOSの基礎知識について、後半は井上よりZOZOTOWNの画面を作ってみたというお話をしました。ComposeのコードでiOSの画面も作れるのはAndroidエンジニアにとっては嬉しいですね。まだalpha版とのことですが、カルーセルやGridのスライダーもしっかり動いていました! [iOS] Visionフレームワークを使って非接触でMacを操作しよう 中岡黎 @rei_nakaoka / 計測プラットフォーム開発本部 計測アプリ部 iOSブロック speakerdeck.com 中岡の発表は非接触でMacを操作するとはどういうことか紹介するデモ動画からはじまりました。手を上下に動かして画面のスクロールをしたり、指でつまむような動作をしてマウスポインタを動かしたりと、Macを操作できていました。Visionフレームワークを使って手のポーズを認識し、それに応じてCGEventを発生させることで実現できるそうです。 中岡はiOSDC Japan 2023でも登壇します。こちらも聞きに来ていただけると嬉しいです! fortee.jp [Android] ComposeでWidgetを実装できるライブラリ「Glance」がbetaになったので触ってみる 野々村樹 @nono_develop / ブランドソリューション開発本部 フロントエンド部 WEARAndroidブロック speakerdeck.com 野々村からはComposeでWidgetを実装できるライブラリ「Glance」を触ってみたという発表です。Composable関数を書くことでWidgetを作成できていました。ただしGlance専用のAPIを使用するためまだ多少制限があるそうです。betaでGlanceThemeというThemeが追加されるなど便利になっているので、今後が楽しみですね! [iOS] Pathを活用してSwift Chartsの限界を超えろ 山田楓也 @gamegamega_329 / ブランドソリューション開発本部 フロントエンド部 WEARiOSブロック speakerdeck.com 次は山田楓也からSwift ChartsではサポートされていないグラフもPathを活用すれば描画ができるという発表です。Pathを使ってアニメーション付きの円グラフが綺麗に描画されていました。レーダーチャート、メーター、多重ドーナツ型のグラフも作成できていました。Swift Chartsの限界を超えていました! [Android] よく書く同じようなコードはAndroid StudioのLiveTemplateを使ってテンプレート化しよう 山田尚吾 @yshogo87 / ブランドソリューション開発本部 フロントエンド部 FAANSブロック speakerdeck.com 続いて山田尚吾より、Android StudioのLiveTemplateについて発表しました。略語でテンプレートを呼び出せる機能で、とても便利そうだとTwitterでも盛り上がっていました。Gitでの管理もできるためチームの実装を楽にするためにも、設計を強制するためにも使えます。すぐに取り入れて活用できそうな内容でした! [iOS] FAANS iOSアプリでGitHub Copilotを使い倒してみた 中島頌太 @burita083 / ブランドソリューション開発本部 フロントエンド部 FAANSブロック speakerdeck.com 中島からは、いまみなさまの関心が高いGitHub Copilotの導入事例について発表しました。導入箇所を検討し試したところ、同じようなコードを複数書く場面ではうまく活用できていました。新規にコードを書く場面ではまだ改善の余地があるそうです。 GitHub Copilotの全社導入とその効果については先日堀江( @Horie1024 )が記事を公開し、多くの反響をいただいています。ぜひこちらもご覧ください! techblog.zozo.com [iOS] 10年以上の歴史を持つプロダクトのRosettaフリー 〜その中で遭遇した問題と解決法を公開します〜 小松悟 @tosh_3 / ZOZOTOWN開発本部 ZOZOTOWNアプリ部 iOS1ブロック speakerdeck.com 小松からはRosettaの説明、課題、その解決方法について熱のこもった発表がありました。RosettaはApple silicon上でx86_64用のバイナリを動かす翻訳プロセスです。いつまでサポートがあるかわからないためRosettaフリーを進めました。課題は多く大変な道のりのようでしたがひとつずつやり切り、副次的効果としてビルド時間の高速化・新規メンバーのセットアップ簡易化などのメリットも得られていました。 小松もiOSDC Japan 2023に登壇しますので、ぜひ聞きに来てください! fortee.jp 最後に 司会・登壇者のみなさま LT終了後にオフライン会場では懇親会を行いました。食事と飲み物を片手に交流を楽しんでおり賑やかな雰囲気でした。iOSエンジニアの方が「Androidも開発してみたくなった」と言ってくださったり、私自身も元AndroidエンジニアながらiOSの知識が広がったのを感じたり、相互に関心を持つ良い機会となりました。みなさまご参加ありがとうございました! ZOZOでは今後もiOSやAndroidのイベントを開催していきます。そして一緒にサービスを作り上げてくれるiOS/Androidエンジニアを募集中です。ご興味のある方は以下のリンクからぜひご応募ください。 hrmos.co hrmos.co
アバター
はじめに こんにちは、CTO/DevRelブロックの堀江( @Horie1024 )です。ZOZOでは GitHub Copilot を全社へ導入しました。本投稿では、GitHub Copilotの導入に際して検討した課題とその課題の解決策としてどのようなアプローチを取ったのかを紹介します。 目次 はじめに 目次 GitHub Copilotとは何か? GitHub Copilot導入の背景と目的 導入する上での課題 セキュリティ上の懸念 ライセンス侵害のリスク GitHub Copilot for Businessの利用 導入による費用対効果 試験導入による費用対効果の見積もり 試験導入の実施 対象者の選出 アンケートの設計 試験導入の実施 アンケート結果の集計 アンケート結果の考察 費用対効果の見積もり 全社導入の判断 導入決定後のGitHub Copilot利用環境の整備 社内LT会 おまけ まとめ さいごに GitHub Copilotとは何か? GitHub CopilotはGitHub社が提供するAIを活用したコード補完ツールです。開発者はコードを書き始めるか、そのコードに何をさせたいのかをコメントに記述することでGitHub Copilotのサポートを得られ、コードをよりすばやく記述できるようになります 1 。また、GitHub Copilotの能力を強化するGitHub Copilot Xのリリースも予定されています。AIとのチャットによる既存コードの分析や修正提案、Pull Requestの作成など、開発のライフサイクル全体を通してAIがサポートすることでよりスムーズな開発者体験の実現が期待されます 2 。 GitHub Copilot導入の背景と目的 ZOZOでは、 ZOZOTOWN や WEAR 、 FAANS 、 ZOZOFIT といった複数の事業でこれから実現していきたいことが多くあり、ユーザーへより多くの価値を素早く届ける体制作りを進めています。リリースまでのリードタイムのうち開発のプロセスが全てを占める訳ではありませんが、開発効率を向上させることは重要です。そして、開発効率の向上を目的とする施策の一環として、GitHub Copilotを導入することにしました。 GitHub Copilotが開発効率に与える影響について、GitHub社により行われた興味深い調査があります。この調査では、2,000名を超える開発者へのアンケート、GitHub Copilotの使用有無でタスクの完了までの時間に差が発生するかの対照実験を95名の開発者を対象に行なっています。 github.blog この調査では、抜粋すると次のような結果が報告されています。 GitHub Copilotを使用した開発者の60-75%が開発の満足度が向上したと回答 GitHub Copilotを使用した73%の開発者が集中して作業ができ、87%の開発者が繰り返し作業中の精神的疲労を軽減できたと回答 3 GitHub Copilotを使用した開発者群が55%早くタスクを完了 この調査結果を踏まえると、GitHub Copilotを導入することで開発効率を向上させる効果が期待できます。 導入する上での課題 GitHub Copilotを全社へ導入する上での課題として次の3点があげられます。 セキュリティ上の懸念 ライセンス侵害のリスク 導入による費用対効果 セキュリティ上の懸念 セキュリティ上の懸念として次の2点があります。 プロダクトのコードが学習に使用されることでの社外への流出 提案されたコードへの脆弱性の混入 私達が様々なサービスを利用する際に入力したデータは、機械学習モデルの学習データとして活用される場合があり、特に業務利用では機密情報の入力に気をつける必要があります。GitHub Copilotにおいても入力となるプロダクトのコードが学習に使用される可能性が懸念点としてあがりました。加えて、GitHub Copilotが提案するコードに脆弱性が含まれていた場合、それをそのまま受け入れることでプロダクトに脆弱性を作り込んでしまう懸念もあります。 ライセンス侵害のリスク 普段の開発でOSSを利用しない場面は無く、その利用には各OSSが採用するライセンスを遵守する必要があります。弊社でもOSSの利用ガイドラインを策定してライセンスを侵害しないよう努めています。ライセンスの種類によりますが、侵害した場合OSSを利用したプロダクトのコードの公開要求や賠償請求のリスクがあります。 GitHub Copilotが使用するOpenAIのCodexモデルの学習データにはGituHub上で公開されているOSSのソースコードも含まれます。GitHub Copilotが生成するコードは学習元のコードをそのままコピーするわけではありませんが、特定のコードスニペットがライセンス違反を問われる可能性は存在します。 GitHub Copilot for Businessの利用 GitHub Copilotには、個人での利用を目的としたfor Individualsとビジネスでの利用を目的としたfor Businessの2つのプランがあります。そして、セキュリティ上の懸念およびライセンス侵害のリスクは、GitHub Copilot for Businessを利用することで低減できます。 docs.github.com GitHub Copilot for Businessでは、プロダクトのコードはGitHub Copilotの学習に使用されません 4 。また、GitHub Copilot for Businessを利用し、脆弱性を含むコードをチェックし提案から除外するフィルタを利用した場合でもGitHub Copilotが提案するコードに脆弱性が混入する可能性はあります 5 。しかし、脆弱性を作り込まないための対策はGitHub Copilotの使用有無で変わりません。開発者は自身の書いたコードの内容をGitHub Copilotの提案を含め理解した上でPull Requestを作成し、レビュアーによるコードレビューを必ず実施することが脆弱性を防ぐ上で重要です。 また、ライセンス侵害のリスクは、公開されたコードがGitHub Copilotの提案として出力されそれをそのまま受け入れてしまった場合に発生します。GitHub Copilotの設定には「Suggestions matching public code」という項目があります。こちらをBlockedにすることで公開されたコードと一致する提案をブロックし、ライセンス侵害のリスクを低減します。加えてGitHub Copilot for BusinessではOrganization全体にこの設定を強制可能です。 docs.github.com このような対策を講じた上でライセンス侵害が問題になった場合どうすれば良いのでしょうか。この場合、 GitHub Copilot Product Specific Terms の第4項 6 および GitHub Customer Agreement の第6項、第7項が適用されます。それにより第三者からの賠償請求に対してGitHub社から無制限の補償を受けられます 7 。 導入による費用対効果 GitHub Copilotを全社へ導入する上でその費用を賄う予算をどう確保するかは課題となります。GitHub Copilotを導入することで開発効率を向上させる効果が期待できますが、予算を計上するには費用対効果を意識する必要があります。ここで、導入に係る費用はGitHub Copilotの利用料金であり、その効果は開発効率を向上させたことでどの程度コストを削減できたかです。GitHub Copilotの導入にコストメリットがあると示すことは予算の承認を受ける上で重要です。 試験導入による費用対効果の見積もり 弊社では、費用対効果を見積もるために、GitHub Copilotの試験導入を計画しました。試験導入によって実際の開発業務でどの程度コスト削減できたかを可視化し、GitHub Copilotの導入にコストメリットがあると確認することで全社への導入判断に役立てます。 試験導入の実施 試験導入では、社内の様々な開発者を対象者として選出します。対象者には試験導入の終了後にアンケートを実施し、その集計結果から費用対効果を見積もります。そして、その結果を踏まえGitHub Copilotの全社への導入を判断します。 試験導入の開始から終了までのプロセスは次のとおりです。 試験対象者の選出 アンケートの設計 試験導入の実施 アンケート結果の集計 アンケート結果の考察 費用対効果の見積もり 対象者の選出 試験対象者の選出には、様々な属性の対象者に対して満遍なく試験ができるよう次のような観点を設けました。 業務内容や使用するプログラミング言語、開発環境が異なるよう選出すること また、望ましい対象者のパーソナリティとして次の点をあげています。 新しいツールを試すのが得意 導入時にチーム内で有効な活用方法を教示できる これらの条件を元に社内の各マネージャーに対して開発効率の向上を狙いたいチームから各1名を目安に選出を依頼し、最終的に56名を対象者に選出しました。 対象者の一日の仕事の中でコーディングに費やす時間、GitHub Copilotと共に使用したプログラミング言語、開発に使用したエディタ・IDEは次のとおりです。 一日の仕事の中でコーディングに費やす時間 アンケート回答者がGitHub Copilotと共に使用したプログラミング言語 アンケート回答者が開発に使用したエディタ・IDE アンケートの設計 アンケートの設計は、 GitHub社の調査 を参考にZOZO独自の設問を追加する形で行いました。 次は実際のアンケートの設問内容です。 設問 選択肢 開発でGitHub Copilotと共に使用したプログラミング言語を教えてください。 Bash C++ Go Java JavaScript Kotlin Objective-C PHP PowerShell Python Ruby Scala Swift TypeScript VBScript その他 前問でその他を選択されえた方はこちらにプログラミング言語名をご入力ください。 記述式 あなたの職種を教えてください。 SRE バックエンドエンジニア フロントエンドエンジニア iOSエンジニア Androidエンジニア 検索/推薦/機械学習エンジニア データエンジニア QA/テストエンジニア その他 開発に使用したエディタ・IDEを教えてください。 記述式 GitHub Copilotを使用することでより生産的になった。※必須 全くそう思わない そう思わない どちらでもない そう思う とてもそう思う 一日の仕事の中でコーディングに費やす時間はどれくらいですか? 30%以下 30%-60% 60%以上 GitHub Copilotを使用するとコーディング時のイライラが軽減される。 全くそう思わない そう思わない どちらでもない そう思う とてもそう思う GitHub Copilotを使うことで仕事に充実感を感じられるようになった。 全くそう思わない そう思わない どちらでもない そう思う とてもそう思う GitHub Copilotを使用することで繰り返し作業をより迅速にこなすことができるようになった。 全くそう思わない そう思わない どちらでもない そう思う とてもそう思う GitHub Copilotを使うことでより満足度の高い仕事に集中できるようになった。 全くそう思わない そう思わない どちらでもない そう思う とてもそう思う GitHub Copilotを使用することでより早くタスクを完了できるようになった。※必須 全くそう思わない そう思わない どちらでもない そう思う とてもそう思う GitHub Copilotを使用することでフロー状態(作業に没頭し集中できる状態)に入りやすい。 全くそう思わない そう思わない どちらでもない そう思う とてもそう思う GitHub Copilotを使用することで検索にかかる時間が短縮された。※必須 全くそう思わない そう思わない どちらでもない そう思う とてもそう思う GitHub Copilotを使用すると繰り返し作業に費やす精神的な負担が軽減された。 全くそう思わない そう思わない どちらでもない そう思う とてもそう思う 1日あたり、GitHub Copilotを使用することでおおよそどれくらいの時間を節約できましたか?※必須 0-0.5時間 0.5-1時間 1-2時間 2-3時間 3-4時間 4-5時間 5-6時間 7-8時間 8時間以上 GitHub Copilotについて同僚に共有したいこと(tips等をまとめたページやSlackのコメントなど)があれば教えてください。 記述式 GitHub CopilotについてGitHubへのフィードバックがあれば教えてください。 記述式 テンプレートとの差分ですが、回答者の細かい属性を取得するために次の設問を追加しています。 「開発でGitHub Copilotと共に使用したプログラミング言語を教えてください」 「前問でその他を選択された方はこちらにプログラミング言語名をご入力ください」 「あなたの職種を教えてください」 「開発に使用したエディタ・IDEを教えてください」 加えて、テンプレートに存在した設問「GitHub Copilotでの経験について同僚に知っておいてほしいことはありますか?」については次のように変更しています。 「GitHub Copilotについて同僚に共有したいこと(tips等をまとめたページやSlackのコメントなど)があれば教えてください」 また、「1日あたり、GitHub Copilotを使用することでおおよそどれくらいの時間を節約できましたか?」の設問については、回答の選択肢として選べる時間区分をより細分化しました。 試験導入の実施 試験導入は期間を2週間として実施し、試験対象者への連絡は専用のSlackチャンネルにて行いました。 試験導入の開始アナウンス 試験期間中には、GitHub Copilotを実際に使用して得た知見を試験への参加者同士で共有することを推奨しました。これはGitHub Copilotが活発に利用され、より良い試験導入とすることを意図しています。 試験参加者の間で共有された知見の例 試験導入は予定通り2週間で終了し、参加者にはGoogleフォームで作成したアンケートに回答頂きました。 アンケート終了のアナウンスと回答へのお礼 アンケート結果の集計 アンケート結果の集計結果は次のとおりです 8 。 アンケート結果の考察 設問「GitHub Copilotを使用することでより生産的になった」への回答では、78.9%がGitHub Copilotを使用することでより生産的になったと回答しています。その他の設問でも「とてもそう思う」「そう思う」の合計は、ほとんどの設問で過半数を超えています。また、設問「1日あたり、GitHub Copilotを使用することでおおよそどれくらいの時間を節約できましたか?」への回答では、58%が1日あたり30分以上時間を節約できたと回答しています。 また、アンケートの次の設問は、SPACEフレームワーク 9 の「満足と幸福」に対応しています 10 。 GitHub Copilotを使用するとコーディング時のイライラが軽減される GitHub Copilotを使うことで仕事に充実感を感じられるようになった GitHub Copilotを使うことでより満足度の高い仕事に集中できるようになった 回答結果を見ると、これらどの質問に対しても40%から50%の回答者が「とてもそう思う」「そう思う」と答えています。 加えて、次の設問はSPACEフレームワークの「効率とフロー」に対応しています。 GitHub Copilotを使用することで繰り返し作業をより迅速にこなすことができるようになった GitHub Copilotを使用することでより早くタスクを完了できるようになった GitHub Copilotを使用することでフロー状態(作業に没頭し集中できる状態)に入りやすい GitHub Copilotを使用することで検索にかかる時間が短縮された GitHub Copilotを使用すると繰り返し作業に費やす精神的な負担が軽減された 設問「GitHub Copilotを使用することでフロー状態(作業に没頭し集中できる状態)に入りやすい」への回答は「とてもそう思う」「そう思う」の合計が29.9%です。しかし、それ以外の設問では60%を超えています。 これらの結果から、ZOZOの場合においてもGitHub Copilotを利用することで開発者の「満足度と幸福度」「効率とフロー」に良い影響を与えられると言えるでしょう。 78.9%がGitHub Copilotを使用することでより生産的になったと回答する一方、「そう思わない」「全くそう思わない」という回答も存在します。この原因を探るため「GitHub Copilotを使用することでより生産的になった」への回答を職種別に集計しました。 次は「GitHub Copilotを使用することでより生産的になった」への回答の職種別の集計結果です。iOSエンジニアにおいて「そう思わない」「全くそう思わない」と回答する割合が増えています。 今回の試験ではiOSエンジニアが開発に使用した開発環境はXcodeのみでしたので、プログラミング言語別、開発環境別での集計もSwift、Xcodeが同様の結果となります。 Xcodeは、GitHub Copilotが公式にサポートしているエディタに含まれていません 11 。XcodeからGitHub Copilotを利用するには非公式のプラグインを利用する必要があり、このような結果となった一因であると考えられます。iOSアプリ開発においてもGitHub Copilotを活用しやすい環境を整えることは導入後の課題となります。 費用対効果の見積もり アンケートの集計結果を元に費用対効果を見積もります。アンケート「1日あたり、GitHub Copilotを使っておおよそどれくらいの時間を節約できましたか?」の回答結果を参照し、月あたりのコスト削減金額を計算します。そして、GitHub Copilotの利用料と比較することで費用対効果を見積もります。 まず、人件費、年間休日数、1日あたりの労働時間を次のように仮定します。 人件費 年間休日数 1日あたりの労働時間 800万/年 12 120日 8時間 この場合時給は4,082円となり、1日あたりの節約時間より計算すると平均削減金額は、1日あたり最大で4,917円/人、最小で2,372円/人になります。月(20営業日)で換算すると、月あたりの平均削減金額は次のようになります。 最大 最小 98,340円/人 47,440円/人 また、GitHub Copilotの月あたりの利用料は次のとおりです。 $19/月/人 ≒ 2,736円/人(為替レート144円 13 /ドルで計算) したがって、「GitHub Copilot導入による人件費の平均削減金額」と比較して、次の金額だけコストメリットがあると見積もりました。 最大 最小 95,604円/人 44,704円/人 全社導入の判断 前述の通り、GitHub Copilotの導入には3つの課題がありました。 セキュリティ上の懸念 ライセンス侵害のリスク 導入による費用対効果 セキュリティ上の懸念およびライセンス侵害のリスクについては、GitHub Copilot for Businessを利用することでその懸念とリスクを低減できます。また、導入による費用対効果についても十分な効果とコストメリットがあると判断したことから、GitHub Copilotの全社導入を決定しました。 導入決定後のGitHub Copilot利用環境の整備 GitHub Copilotの導入決定後、全社でGitHub Copilotを利用できる環境の整備を進めました。 まず、全社に対してGitHub Copilot利用希望者のリストアップを依頼しました。その後、コーポレートエンジニアリングチームと連携しながら総利用金額の見積りと予算確認の後、希望者全員に対してGitHub Copilotを有効化しました。また、GitHub Copilotの利用に関する社内承認フローを整備し、運用ルールについても整備しています。 社内LT会 GitHub Copilotを活用していく上で重要なのは、まずどのような使い方ができるかを開発者が知ることです。執行役員CTO瀬尾( @sonots )のアイデアもあり、GitHub Copilotのtipsについて発表するLT大会を開催しました。登壇者は試験導入への参加者の中から8名に依頼しています。 LTのタイトルは次のとおりです。 タイトル GitHub Copilot × 言語 GitHub CopilotとAndroid開発 GitHub Copilot × Kotlin/Java PowerShellでGitHub Copilot利用 GitHub Copilot × PowerShell How to use Copilot Successfully with jQuery GitHub Copilot × jQuery Terraform, YAMLでの利用例 GitHub Copilot × Terraform/YAML GitHub Copilotを使ってみた感想 GitHub Copilot × Java iOSアプリ開発におけるGitHub Copilotの活用方法 GitHub Copilot × Swift Copilot使ってみてLT GitHub Copilot × TypeScript GitHub Copilot × VBScript GitHub Copilot × VBScript LT会には約200名が参加し非常に盛り上がりました。様々な言語・フレームワークとGitHub Copilotについての事例をまとめて聞けたので個人的にも勉強になりました。社外秘の情報が含まれるので登壇資料の全てをお見せすることはできませんが、一部を抜粋して紹介しようと思います。 GitHub Copilotを使ってみた感想やまとめを見ると、当然ながら得意とする作業と苦手な作業があります。ペアプロの体験になぞらえた体験例も面白いですね。また、GitHub Copilot × Swiftの事例では、GitHub CopilotとXcodeとの相性やVSCodeを使ったコーディングについて触れられていました。どのようにすればiOSアプリ開発でGitHub Copilotをより活用できるかを探る必要があります。iOSチームでは全社導入後にもiOSアプリ開発でのGitHub Copilotの活用方法について勉強会を開いており、他の領域でも同様な勉強会が開催できればと考えています。 GitHub CopilotとAndroid開発 :GitHub Copilot × Kotlin/Java Terraform, YAMLでの利用例 : GitHub Copilot × Terraform/YAML GitHub Copilotを使ってみた感想 : GitHub Copilot × Java iOSアプリ開発におけるGitHub Copilotの活用方法 : GitHub Copilot × Swift 型定義をGitHub Copilotに与えること提案の精度が上がることもLTで報告されており、GitHub Copilotを活用していくヒントとなりました。 Copilot使ってみてLT : GitHub Copilot × TypeScript 巨大なjQueryで書かれたコードに対して、GitHub CopilotにJSDocで型定義を書いてもらうことでコードリーディングを容易にする事例もありました。初見のコードに対してそのコードが何をしているかを理解する上でもGitHub Copilotは有効活用できそうです。 How to use Copilot Successfully with jQuery : GitHub Copilot × jQuery ZOZOTOWNのコードベースではJavaへのリプレイスが進んでいますが、今でもシステムの大部分をVBScriptが支えています 14 。今回の試験導入で、VBScriptでの開発についてもGitHub Copilotが有効に活用できる事例を得られたことは、既存機能の改修とリプレイスを行う際にもプラスになると考えています。 GitHub Copilot × VBScript 事例 GitHub Copilot × VBScript まとめ おまけ GitHub Copilotが提案したコメントなのですが、謎のクリスマス事件として盛り上がりました。 まとめ 現在ZOZOでは社内のほぼ全ての開発者がGitHub Copilotを活用して開発業務を行なっています。GitHub CopilotはGitHub Copilot Xとして今後も継続的に機能の追加が予定されています。それにより開発効率を向上させユーザーへより多くの価値を素早く届けるための手助けとなってくれることを期待しています。また、GitHub NextのページではGitHub Copilotに関する興味深いプロジェクトを確認できます。これらのプロジェクトによってどのような開発者体験が実現されるのか楽しみです。 本記事では、ZOZOでのGitHub Copilotの導入にあたって課題となる点や、課題にどのように対応したのかを紹介しました。本記事がGitHub Copilotの導入の一助となれば幸いです。 さいごに ZOZOでは一緒に楽しく働くエンジニアを絶賛募集中です。ご興味のある方は下記リンクからぜひご応募ください。 corp.zozo.com https://docs.github.com/ja/copilot/overview-of-github-copilot/about-github-copilot-for-individuals ↩ https://github.blog/jp/2023-03-23-github-copilot-x-the-ai-powered-developer-experience/ ↩ 原文では精神エネルギーの節約とありましたが、ここでは精神的疲労を軽減と表現しました。 ↩ GitHub Copilot for Business Privacy Statement でGitHub Copilot for Businessが収集するデータを確認しました。その結果、GitHub Copilot for Businessではコードスニペットデータは保持されず、プロダクトのコードは学習に使用されないと判断できました。また、GitHub for Individualsの設定項目に「Allow GitHub to use my code snippets for product improvements」があります。GitHub for Individualsではこの項目の設定値によって学習への使用有無を制御可能です。 ↩ GitHub Copilotには、リアルタイムに安全でないコーディングパターンをブロックするAIベースの脆弱性防止システムが導入されています。詳細はこちらの記事「 GitHub Copilot now has a better AI model and new capabilities 」をご覧ください。 ↩ 「Suggestions matching public code」をBlockedに設定することが契約に基づいて補償する条件として明記されています。 ↩ 「GitHub Copilot Product Specific Terms」および「GitHub Customer Agreement」は2023/6/22時点のものを参照し確認しています。また、弊社が締結した契約では無制限の補償を受けられることをGitHub社に確認済みですが、保証内容は契約しているプラン、契約時期によって異なります。エンタープライズ契約でも主契約次第で補償内容が異なる可能性もありますので、必ずGitHub社にご確認をお願いいたします。 ↩ 記述式での回答結果の集計は除いています。 ↩ https://queue.acm.org/detail.cfm?id=3454124 ↩ Albert Ziegler, et al. "Productivity Assessment of Neural Code Completion" arXiv preprint arXiv: 2205.06537. 2022, p.12. ↩ GitHub Copilotがサポートするエディタは2023年6月時点でVisual Studio Code、Neovim、JetBrains系IDE、Visual Studioです。 ↩ こちらは仮の人件費です。弊社実績とは関係ありません。 ↩ 2023年6月末の為替レートを参照しています。 ↩ https://technote.zozo.com/n/ndf768964d62e ↩
アバター
はじめに こんにちは、計測プラットフォーム開発本部SREの纐纈です。 2023年6月23日にFindy社によるオンラインイベント「CI/CD最前線〜今開発現場が直面している課題とは? Lunch LT」が開催されました。このイベントでは、株式会社アンドパッドさん、株式会社サイバーエージェントさん、エムスリー株式会社さんから一人ずつ、弊社からも私がLTをしてきましたので、こちらのブログでも報告させていただきます。 findy.connpass.com 今回のイベントでは、CI/CDを社内で導入・推進されている、もしくはCI/CDの取り組みの具体的な方法や事例を知りたい方が参加者の対象となっていました。そのため、ここ最近CI/CDの改善に努めていた弊チームの取り組みがちょうど良く紹介できると思い、発表者として手を上げさせてもらいました。 今回の発表に使ったスライドはこちらです。 speakerdeck.com 登壇内容まとめ 今回の私の発表は、今までのチームの取り組みまとめということで、そのほとんどはブログ内ですでに公開しているものです。これらをLTの時間内に伝わるようにまとめて、改善効果が伝わりやすくなるように努めました。 要約すると、元は週2〜3時間、2人でやっていた定期リリース作業を、PRをマージするだけでリリースできるように安全性を担保しつつ自動化するまでという内容です。視聴者にとって全てが刺さることはないと思いますが、一部だけでも参考になる箇所があれば良いなと思いました。 これまで公開したブログは次のとおりです。詳しく知りたい方はぜひこちらをご覧下さい。 techblog.zozo.com techblog.zozo.com techblog.zozo.com techblog.zozo.com techblog.zozo.com 登壇後の所感 登壇後のフィードバックやTwitter( #CICD_findy )実況を見たところ、ポジティブな反応が多くあり、安心しました。特に負荷試験まで自動化されていることに関心を持たれた人が多かったです。普段社外からチームでやっていることに対して評価をもらうことはないので、新鮮な気持ちになりました。今後も機会があればこのような発表ができたらと思います。 謝辞 今回のイベントはFindy社の主催でしたので、Findyの方々には様々なサポートをしていただきました。また、登壇までサポートしてくれた社内のDevRelチームや弊チームにもこの場を借りて感謝申し上げたいと思います。イベントに参加して視聴してくださった皆様も、ありがとうございました。 最後に ZOZOでは、プロダクト開発以外にも、今回のようなイベントの開催や登壇など、外部への発信も積極的に取り組んでいます。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com
アバター
はじめに こんにちは、CISO部の 兵藤 です。日々ZOZOの安全のためにSOC対応を行なっています。 本記事ではサイバー脅威インテリジェンス(CTI)のプラットフォームの1つであるOpenCTIをAzure上に構築した事例を紹介します。また、CISO部ではその他にもZOZOを守るための取り組みを行っています。詳細については以下の「フィッシングハントの始め方」をご覧いただければと思います。 techblog.zozo.com 目次 はじめに 目次 背景と概要 OpenCTIとは 構築 概要 Azure VM でのOpenCTI実装 Keyコンテナでの管理 Azure Application Gatewayを用いたHTTPS化 Azure ADと連携しSAML認証を実装 OpenCTIのコンフィグ変更 Azure ADの設定 SAML認証フローの確認 Azure Network Security Group(NSG)でアクセス制限 Azure File Storageを用いたデータのバックアップ 構築結果 まとめ おわりに 背景と概要 そもそもサイバー脅威インテリジェンスとは何かですが、「脅威情報を意思決定のプロセスに必要な状態へ分析、解釈、集約、補強されたもの」で、インテリジェンスのレベル感によって大きく3段階に分かれます。 Strategic level Operational level Tactical level SOCでは悪意のあるドメイン名やマルウェアのハッシュ値など、Tactical levelでのインテリジェンス(IOCなど)を日々扱うことが多いです。詳しくはサイバー脅威インテリジェンスについて調べていただければと思いますが、これらのインテリジェンスは活用スパンが短く、自動運用を進めることが望ましいです。ですが、ZOZO内では上記Tactical levelでのインテリジェンスを各個人が扱い、SOC対応に活かしている状況でした。 SOC対応を実施するにあたって各個人で調査収集していた情報を1つのプラットフォームで自動収集、管理し、ゆくゆくはSIEMと連携したいと考えていました。そこで、CTIのプラットフォームのOSSであるOpenCTIを検討しました。 OpenCTIとは OpenCTIについては先ほど少し紹介した、CTIのプラットフォームのOSSであり、Filigranが提供しているものです。公式サイトは こちら をご参照ください。 ReactやGraphQL、Elasticsearchなどを使用しており、モダンでカッコいい見た目がたまらなくセキュリティエンジニアをしている感が出ているプラットフォームです。 ※以下画像は公式 1 から引用。 日本語での詳しい解説は「にのせき」さんの記事 2 が参考になります。 このOSSはDockerを用いて構築が可能で、初期構築が素早くできます。また、OpenCTIの機能を拡張するための「Connectors」についてもDockerイメージが提供されており、容易に機能拡張が可能です。アプリケーションの構築の経験が少ない方でも、Dockerを用いて簡単に構築できるのでおすすめのOSSです。 OpenCTIの機能を拡張するための「Connectors」の紹介 構築 概要 ZOZOではAzure上にOpenCTIを構築しました。以下が概要図です。 Azure VM上にDockerを用いてOpenCTIを構築 環境変数などはKeyコンテナで管理 Azure Application Gatewayを用いてHTTPS化 Azure ADと連携しSAML認証を実装 Azure Network Security Group(NSG)でアクセス制限 Azure File Storageを用いてデータのバックアップを実施 Azure VM でのOpenCTI実装 OpenCTIは先ほども述べたようにDockerでの構築が可能です。要するにコンテナサービスを提供しているクラウドサービスであれば構築可能です。例えばAzureでのコンテナサービスは以下のようなものがあります。 Azure Container Instances Azure App Service Azure Container Apps Azure Kubernetes Service (AKS) 上記のうちコンテナを複数起動する場合は後半の2つ、Container AppsまたはAKSを検討することになると思います。筆者も最初はこの2つを検討し、Kubernetesで構築するほどでもなく簡易的に複数コンテナを起動できれば良かったのでContainer Appsでの構築を目指しました。では何故Azure VMでの構築になったのでしょうか? 答えは簡単です。Container Appsでの構築を進めていると、インスタンスの起動の段階で壁にぶち当たります。Elasticsearchなどのコンテナがスペック不足で落ちてしまうのです。Microsoft公式の ドキュメント によると従量課金プランだと記事執筆の時点(2023年6月15日)ではメモリ4Gがリミットとなってます。ですので、これ以上スペックを上げることがContainer Appsではできそうもありませんでした。ここら辺のお話や知見はSlackのコミュニティでも議論されていて、今回の私の知見も共有しています。このようにSlackではOpenCTIに関する情報が多く共有されているので、構築する際には参考になると思います。 SlackコミュニティにおけるAzure Containerの見解 これらの背景により、今回はAzure VM上にDockerを用いてOpenCTIを構築しました。 ここまでAzureにこだわっている理由は後ほど説明します。 Keyコンテナでの管理 OpenCTIを構築する際には、環境変数を設定しなければなりません。Connectorsの種類によっては、APIキーなどの秘匿情報を設定する必要があります。このような秘匿情報をハードコーディングすることはよろしくないので、Keyコンテナを用意し、そこで環境変数を管理するようにしました。 この変数についてですが、コンテナを起動する際に必要なので、Keyコンテナから値を読み込むOSSツールの vaultenv を利用しています。 github.com 以下のフォーマットのように記述すれば環境変数を置き換えてくれるので便利です。 APIKEY={{ kv "https://keyvault-name.vault.azure.net/secrets/example-key" }} Azure Application Gatewayを用いたHTTPS化 Application GatewayでSSL/TLSオフロードをしています。OpenCTIはデフォルトでは8080のportで起動するのでこのportへ443からの通信をマッピングすることでHTTPS化を実現しています。特段変わったことはしていません。 Azure ADと連携しSAML認証を実装 OpenCTIはデフォルトでID、Password認証です。これをAzure ADと連携しSAML認証に変更しました。この実装については文章化されているものが少ないので詳細を記載しようと思います。公式のドキュメントは こちら のリンクを参照してください。 OpenCTIのコンフィグ変更 OpenCTIのデフォルトのdocker-compose.ymlは以下のようになっています。 opencti: image: opencti/platform:5.7.4 environment: - NODE_OPTIONS=--max-old-space-size=8096 - APP__PORT=8080 - APP__BASE_URL=${OPENCTI_BASE_URL} - APP__ADMIN__EMAIL=${OPENCTI_ADMIN_EMAIL} - APP__ADMIN__PASSWORD=${OPENCTI_ADMIN_PASSWORD} - APP__ADMIN__TOKEN=${OPENCTI_ADMIN_TOKEN} - APP__APP_LOGS__LOGS_LEVEL=error - REDIS__HOSTNAME=redis - REDIS__PORT=6379 - ELASTICSEARCH__URL=http://elasticsearch:9200 - MINIO__ENDPOINT=minio - MINIO__PORT=9000 - MINIO__USE_SSL=false - MINIO__ACCESS_KEY=${MINIO_ROOT_USER} - MINIO__SECRET_KEY=${MINIO_ROOT_PASSWORD} - RABBITMQ__HOSTNAME=rabbitmq - RABBITMQ__PORT=5672 - RABBITMQ__PORT_MANAGEMENT=15672 - RABBITMQ__MANAGEMENT_SSL=false - RABBITMQ__USERNAME=${RABBITMQ_DEFAULT_USER} - RABBITMQ__PASSWORD=${RABBITMQ_DEFAULT_PASS} - SMTP__HOSTNAME=${SMTP_HOSTNAME} - SMTP__PORT=25 - PROVIDERS__LOCAL__STRATEGY=LocalStrategy ports: - "8080:8080" depends_on: - redis - elasticsearch - minio - rabbitmq restart: always ローカル認証に関する環境変数は PROVIDERS__LOCAL__STRATEGY=LocalStrategy です。ここに追加で環境変数を設定していき、SAML認証のみを行うようにコンフィグを変更できます。以下のように追加します。 # docker-compose.yml ...省略 - SMTP__PORT=25 - PROVIDERS__LOCAL__STRATEGY=LocalStrategy - PROVIDERS__LOCAL__CONFIG__DISABLED= true # ここから追加した環境変数 - PROVIDERS__SAML__STRATEGY=SamlStrategy - "PROVIDERS__SAML__CONFIG__LABEL=Login with SAML" # ログイン画面のボタン名 - PROVIDERS__SAML__CONFIG__ISSUER=${PROVIDERS_SAML_CONFIG_ISSUER} # Issuer名 - PROVIDERS__SAML__CONFIG__ENTRY_POINT=${PROVIDERS_SAML_CONFIG_ENTRY_POINT} #IdPのエンドポイント - PROVIDERS__SAML__CONFIG__SAML_CALLBACK_URL=${PROVIDERS_SAML_CONFIG_SAML_CALLBACK_URL} # SAML認証後のリダイレクト先 - PROVIDERS__SAML__CONFIG__CERT=${PROVIDERS_SAML_CONFIG_SAML_CERT} # IdPの証明書 ports : - "8080:8080" ...省略 これらの設定でSAML認証のみを行うようになります。実際に認証画面を覗きにいくと以下のようにSAML認証のみ可能になっています。各種環境変数の詳細な設定値については後ほどADの設定の際に確認します。 SAML認証のみ有効な様子 Azure ADの設定 OpenCTIのSAML認証を有効化するためにAzure ADでアプリを登録し、SAML認証の各種設定をしなければなりません。まずはAzure ADでアプリを登録します。アプリの登録は公式ドキュメントの「 独自のアプリケーションの作成 」を参考にしてください。 続いて、SAML認証を設定します。Azure ADのアプリの設定画面から「シングル サインオン」を選択し、「SAML」を選択します。 ここから具体的なSAMLの設定に移っていくのですが、その前にSAML認証でAzure AD、OpenCTIで設定が必要な各種項目について整理しておきます。 項目 設定箇所 説明 識別子(Entity ID) Azure AD SPを一意に識別するためのID。URL形式をとり、IdPとSPで一致させる必要がある。別名「Issuer」。 応答URL(Assertion Consumer Service URL) Azure AD SAML Responseの宛先URL。 Issuer OpenCTI IdPを一意に識別するためのID。URL形式をとり、IdPとSPで一致させる必要がある。別名「Entity ID」。 Entry Point OpenCTI IdPのエンドポイント。SAML Requestの宛先URL。 SAML Callback URL OpenCTI SAML Responseの受信URL。 CERT OpenCTI SAML Responseに対して署名を検証するための証明書。 上記「識別子(Entity ID)」と「Issuer」はIdP(Azure AD)とSP(OpenCTI)で一致させないといけない点に注意して設定します。「応答URL(Assertion Consumer Service URL)」については「SAML Callback URL」で設定したURL(ここでは「https[:]//opencti-domain/autth/saml/callback」とします)を記載します。このURLの階層はそのまま変えずにドメインを変更すればいいでしょう。「CERT」については以下の赤枠の「ダウンロード」をクリックすると証明書がダウンロードできます。 最後に「Entry Point」については以下の「ログインURL」の項目をOpenCTIに設定すれば良いです。 以上でAzure AD(とOpenCTI)の設定は完了です。 SAML認証フローの確認 OpenCTIのSAML認証がどのように動作しているか確認をしていこうと思います。SAML認証のフローには大きく「SP-initiated」と「IdP-initiated」の2つがあります。OpenCTIの認証前の緑色のボタンは「SP-initiated」のフローを開始するボタンなので、今回はこちらの方を確認していきます。 通信のフローを確認するためにChromeの拡張機能「 SAML-tracer 」をインストールしておきます。この拡張機能を用いるとSAML認証の通信のフロー(SAML Request, SAML Response)を簡単に確認できます。以下が「SP-initiated」のSAML Requestです。 < samlp : AuthnRequest xmlns : samlp = "urn:oasis:names:tc:SAML:2.0:protocol" ID = "_80a2e36b3cc742a5b5b7" Version = "2.0" IssueInstant = "2023-06-06T04:29:02.704Z" ProtocolBinding = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Destination = "https://login.microsoftonline.com/XXXXXXX-XXXXXX-XXXXXX-XXXXXX/saml2" AssertionConsumerServiceURL = "https://opencti-domain/auth/saml/callback" > < saml : Issuer xmlns : saml = "urn:oasis:names:tc:SAML:2.0:assertion" > http://opencti/entity-id </ saml : Issuer> < samlp : NameIDPolicy xmlns : samlp = "urn:oasis:names:tc:SAML:2.0:protocol" Format = "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress" AllowCreate = "true" /> < samlp : RequestedAuthnContext xmlns : samlp = "urn:oasis:names:tc:SAML:2.0:protocol" Comparison = "exact" > < saml : AuthnContextClassRef xmlns : saml = "urn:oasis:names:tc:SAML:2.0:assertion" > urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport </ saml : AuthnContextClassRef> </ samlp : RequestedAuthnContext> </ samlp : AuthnRequest> Requestの中身を確認すると、 NameIDPolicy の Format 項目で emailAddress を要求しているのでメールアドレスの単位でOpenCTIのユーザを一意に認識していると考えられます。また、 AllowCreate の項目が true となっているのでOpenCTI側でユーザを作成していなくてもIdP(Azure AD)がOpenCTIにユーザを作成し初期化してくれます。ProtocolとしてはHTTP-POSTバインディングで返信するように要求しています。認証が成功すると <NameID> の項目で emailAddress がSAML Responseで返ってきます。 HTTPベースでこれらの情報をやり取りするのでHTTPSでの実装をしていることが前提になります。 Azure Network Security Group(NSG)でアクセス制限 SAML認証でもアクセス制御を行っていますが、それ以上にNSGで接続元を制限していたりと多層防御を実施しています。このように設計していた場合、Azure ADを用いたSAML認証の認証フローを許可するのが面倒かと思われるかもしれません。 ですが、Azureで実装していればその点の面倒さは解消されます。Azureにこだわっていたのはこのためです。以下のようにソースを「Service Tag」、ソースサービスタグを「AzureActiveDirectory」に設定しておけば煩わしい設定が一気に簡単になります。 受信セキュリティ規則で設定を行う一例 Azure File Storageを用いたデータのバックアップ コンテナの難点にデータの永続化が挙げられると思います。OpenCTIではDocker volumeを利用して永続化を行なっていますが、本設計だとVMを丸ごとスナップショットとして保存しておかないといけなくなります(それでもいいのですけど)。ですので、VM内のコンテナのファイルをAzure File Storageにマウントし、OpenCTIのMinIOのデータを外部記憶しています。 docker-compose.ymlのvolumesの部分を以下のように変更することで実現しています。 ...省略 volumes : esdata : s3data :  # ここから変更 driver : local driver_opts : type : cifs o : "mfsymlinks,vers=3.0,username=${AFS_NAME},password=${AFS_KEY},addr=${AFS_NAME}.file.core.windows.net" device : "//${AFS_NAME}.file.core.windows.net/${AFS_CONTAINER}" 正常にマウントされていれば以下のようにAzure File Storageにデータが保存されていると思います。 基本的にこのFile StorageはOpenCTIからしかアクセスしないのでNW制御でVnetからしかアクセスできないようにしておくのがおすすめです。また、アクセスキーについては定期的に更新しておきましょう。 構築結果 上記画像のようにAzure上にOpenCTIを構築できました。これにより自動でインテリジェンスを取り込み、管理できるようになりました。Search欄から気になるIOCなど検索できて便利です。 また、日本語対応もしていて設定がとてもしやすいと感じました。開発してくださったNFLabs 3 の皆様ありがとうございます。 まとめ サイバー脅威インテリジェンスのプラットフォームの1つであるOpenCTIをAzure上に構築する取り組みについて紹介しました。意外とAzureでのOpenCTI構築やSAML認証の実装事例が少ないので、OpenCTIをこれから構築する方達のご参考になれば幸いです。 ZOZOではこれからも脅威情報を逐次収集し、意思決定プロセスに必要なインテリジェンスの活用に努めていき、ZOZOの安全性の向上を図っていきたいと考えています。続いてのフェーズではIOCのトリアージを詳細に詰めていこうと目論んでいます。 おわりに ZOZOでは、一緒に安全なサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクから是非ご応募ください! corp.zozo.com OpenCTIの Github ↩ OpenCTI入門 ↩ OpenCTIの日本語化にあたって ↩
アバター
はじめに こんにちは。DevRelブロックの @wiroha です。6月27日に「 WWDC23 報告会 at LINE, ZOZO, ヤフー 」を開催しました。WWDCに参加した各社のエンジニアが新しく発表された技術や得た知見・情報などを共有するイベントです。今年はオンラインと一部オフラインのハイブリッドで開催しました。 登壇内容まとめ 3社の社員によるLTとパネルディスカッションを行い、その後は交流会で盛り上がりました。なお本イベントはAppleがNDAを締結した開発者にのみ公表している情報を取り扱っており、参加はApple Developer Programに加入している方に限定させていただきました。本レポートもLTの詳細は割愛し、雰囲気をお伝えできればと思います。 コンテンツ 登壇者 What’s new in image processing たなたつ (田中 達也) ◆ヤフー Hello Object Capture for iOS! 森口 友也 ◆ZOZO Animate SF Symbols 羽柴 彩月 ◆LINE Whatʼs new in privacy 2023 大塚 達也 ◆ヤフー The New Potential of Widgets 山田 楓也 ◆ZOZO What’s new in Swift 5.9 Hiraki ◆LINE Panel Discussion 早石 明浩 ◆LINE 冨田 悠斗 ◆ヤフー 永井 崇大 ◆ZOZO まつじ (松本淳之介) ◆LINE 交流会 発表風景 ヤフーの田中さま ZOZOの森口 LINEの羽柴さま ヤフーの大塚さま ZOZOの山田 LINEのHirakiさま Panel Discussion 挙手をしながらパネルディスカッションを進行 乾杯をしてカジュアルな雰囲気でパネルディスカッションがはじまりました。随時質問を挟み会場の皆さんに手をあげていただき、双方向のコミュニケーションを取りながら楽しく進行しました。 最も印象に残った発表はApple Vision Pro 最も印象に残った発表は「Apple Vision Pro」で満場一致でした。久しぶりの「One more thing...」に現地会場は大興奮で一体感があったそうです。 興味があったセッションはUIKitやARKitなど各々の好きな分野を熱く語っていました。試したい機能やプロジェクトで実装したいことについては、普段の業務知識や関心があるからこその発想があげられていました。 前日の現地での様子を紹介 パネリストの3人より前日・当日のタイムラインを写真で共有いただきました。現地に参加した方は前日からノベルティの受取や著名な方との写真撮影をしており、イベントを楽しむ様子が伝わってきます。日本にいた方はヤフー紀尾井町オフィスのLODGEにて行われたリアルタイム視聴イベント「 Extended Tokyo - WWDC 2023 」に参加していました。こちらの様子はレポート記事がありますのでこちらもぜひご覧ください! techblog.zozo.com 交流会 パネルディスカッションの後、オフライン会場にて交流会を実施しました。Apple JapanからTechnology Evangelistの豊田さまにご参加いただき、参加者からのさまざまな質問に回答いただきました。登壇したみなさまもリラックスした様子で交流を楽しんでいました。 Apple Japanの豊田さま 最後に みなさまご参加ありがとうございました。WWDCの報告会は毎年の恒例イベントです。また来年も開催できることを楽しみにしています! ZOZOでは一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は以下のリンクからぜひご応募ください。 hrmos.co
アバター
はじめに こんにちは、ECプラットフォーム基盤SREブロックの織田と、カート決済SREブロックの遠藤です。 本記事では、Istio Rate Limitの概要とZOZOTOWNでの導入事例を2つご紹介します。 目次 はじめに 目次 現在のZOZOTOWN Istio Rate Limitの概要 Istio Rate Limitとは? 仕組み どのような制限ができるのか? Circuit Breakerとの違い 導入事例 事例1. マイクロサービスから外部APIへのリクエスト制限 構成 設定と計測のポイント 1. レイテンシの計測 2. 1つのAPIに対して2重の制限を設定 事例2. オンプレミス環境のリクエスト制限 構成 設定と計測のポイント 1. 商品別の注文リクエスト数制限 2. 閾値決定のための負荷試験 今後の展望 感想 最後に 現在のZOZOTOWN ZOZOTOWNではオンプレミス環境からクラウドへの大規模なリプレイスを行っており、クラウド移行と共にモノリシックな開発からマイクロサービス開発への移行を推進しています。クラウド、マイクロサービスへの移行では、ストアドプロシージャからの脱却やマイクロサービスごとにDBを分離するなどの対応を実施しています。 また、サービスメッシュの導入も並行して進めており、マイクロサービスの基盤にはKubernetes、サービスメッシュにはIstioを採用しています。 マイクロサービス化への取り組みについては、以前イベントを行っており、そのまとめとしてTECH BLOGを投稿しています。ご興味のある方はご覧ください。 techblog.zozo.com Istioに関する取り組みについてもTECH BLOGを投稿しています。 techblog.zozo.com techblog.zozo.com Istio Rate Limitの概要 Istio Rate Limitとは? Istio Rate Limitは、Istioの機能の一部で特定のエンドポイントやAPIなどに対するリクエスト数を制限できる機能です。 リクエスト数を制限することで急激なトラフィックの増加によるアプリケーションやバックエンドサービスの過負荷を防ぎ、サービスの安定性の向上が実現できます。Rate LimitはIstio独自のものではなく、ネットワークトラフィックを制限することを意味する単語として利用されていることが多いです。 仕組み Envoyのネイティブレートリミットを利用し、EnvoyFilterで動的にトラフィックを制限します。ここでは、外部リクエストと内部リクエストに対するRate Limitの仕組みを簡単に説明します。 まずは、外部リクエストについてです。外部からのリクエストに対してIstio Rate Limitがどのような通信をしてトラフィックを制御するか説明します。 外部からのリクエストがIngress Gateways(istio-proxy Container)に届く ingressgatewayからRate Limitにリクエストを行う Rate LimitがRedisに書き込みを行い、ingressgatewayにレスポンスを返す 設定ファイル(ConfigMap)に定義されている制限に引っかかっていればingressgatewayが429を返し、制限に引っかかっていない場合はアップストリームにトラフィックが流れる 次に内部リクエストについて説明します。外部リクエストと基本的に同じですが、リクエスト元やRate Limitに問い合わせするリソースが異なります。 Service Pod内のService Containerからサイドカーであるistio-proxy Containerにリクエストが届く istio-proxyからRate Limit Podにリクエストを行う Rate Limit PodがRedisに書き込みを行い、istio-proxyにレスポンスを返す 設定ファイル(ConfigMap)に定義されている制限に引っかかっていればistio-proxyが429を返し、制限に引っかかっていない場合はアップストリームにトラフィックが流れる Redisに書き込まれるkeyとvalueは、 <domain>_<key>_<value>_<unix-time> とリクエストカウントの組み合わせとなります。 domain、key、valueは、Rate Limitの設定ファイルに定義している値が使われます。Redisに書き込まれるkeyは、 GenerateCacheKey で生成され、以下のように設定ファイルを定義している場合、 global-ratelimit_generic_key_zozotown_1687238460 となります。 domain : global-ratelimit descriptors : - key : generic_key value : zozotown rate_limit : unit : second requests_per_unit : 1 どのような制限ができるのか? メッシュ内の全トラフィックに対して制限可能です。例えば、内部サービスAから内部サービスBへのリクエストや内部サービスCから外部サービスDへのトラフィックなど様々なケースに対して制限を設定できます。 ただ、外部へのリクエストに対して制限を設ける場合は、 Service Entry を作成する必要があります。Service Entryを追加することで外部サービスを論理的にメッシュ内に取り込み、メッシュ内サービス同様にアクセスやルーティングできるようになります。 制限を設定できる範囲としては、 Global Rate Limit と Local Rate Limit があります。両者の違いは、以下のようになります。 Global Rate Limitは、メッシュ全体へ制限を適用します。外部のインメモリーデータストアにデータを格納し、各ワークロード(Pod)から書き込みや参照が行われます。 Local Rate Limitは、ワークロード(Pod)毎に制限を適用します。サイドカーとしてinjectされたistio-proxyごとにインメモリーでデータを持ちます。ワークロードごとに制限がされているため、ワークロードが増える度にアップストリームへの制限は緩くなります。 Rate Limitの評価は、Local Rate Limit、Global Rate Limitの順で実行されます。そのため、両者を組み合わせて利用することでGlobal Rate Limitの負荷を軽減できます。 制限可能な単位は、 Rate limit definition に明記されていて1秒、1分、1時間、1日です。例えば、1分間に100リクエストまで通常通り処理し、101リクエスト目以降は固定のレスポンスコードを返すというようなことができます。 1秒間で5リクエストの制限を設定できますが、2秒間で10リクエスト、3秒間で15リクエストなどの制限は設定できません。1秒間で5リクエストの設定にしておけば、2秒間で10リクエスト、3秒間で15リクエストの制限になりそうですが、実際の挙動は異なるので細かな設定が必要な場合は注意が必要です。 ただ、設定できる単位などについてはユーザの需要に基づいて機能が追加されていくとのことなので、issueを立てたりPRを作成してみると良いかもしれません。 このように柔軟性が高いため、マイクロサービス環境でのトラフィック制限を簡単かつ効果的に実現可能です。 Circuit Breakerとの違い Rate Limitはトラフィック制限、Circuit Breakerは障害回避とタイムアウト制御のためのもので、両者ともに異なる問題に対するアプローチです。 Circuit Breakerは、あるサービスの障害を検知した場合に通信を遮断し、サービスの復旧を検知すると通信を復旧させる仕組みです。特定マイクロサービスの障害を検知した場合にそのマイクロサービスへの通信を遮断することによって、1つの障害が連鎖的な障害となるカスケード障害を避けることができます。 ZOZOTOWNでは、Istio Circuit Breakerについても採用しています。 techblog.zozo.com 導入事例 ここからはシステムへの導入事例を2つ紹介します。両事例ではメッシュ全体に制限を適用させ、ワークロードの増減に影響されずアップストリームへのリクエスト制限を一定にしたかったため、Global Rate Limitを採用しました。 マイクロサービスから外部APIへのリクエスト制限 オンプレミス環境のリクエスト制限 事例1. マイクロサービスから外部APIへのリクエスト制限 事例1では、マイクロサービスから外部APIへのリクエスト制限を導入した事例について紹介します。 2023年5月頃に利用者の年齢に応じたマーケティング施策を実施可能にするため、会員情報を扱うマイクロサービス(以降、会員基盤)で年齢認証の機能をリリースしました。実際に2023年6月1日から6月30日の間で、年齢認証を利用したマーケティング施策を実施しています。 年齢認証の機能では、本人確認を実施するために外部サービス LIQUID eKYC を利用しており、LIQUID社からプレスリリースも出されています。 liquidinc.asia LIQUID eKYCは、AI審査で本人確認業務を自動化するサービスで、撮影画像の品質をチェックする画像処理技術や本人確認書類の文字を読み取るOCR技術などの精度の高さが特徴です。 会員基盤からLIQUID eKYCへのリクエスト数に制限を設けるため、Rate Limitを導入しました。制限を設けずに年齢認証が必要な現在行われている施策などを実施した場合、LIQUIDへのリクエストがスパイクし過負荷になってしまう可能性があるためです。 構成 会員基盤周辺の構成図は、以下のようになっています。 会員基盤からLIQUIDへは、Service Entryでメッシュ内のサービスがLIQUIDにアクセスやルーティングできるようにし、 Virtual Service でルーティングの設定を行い、 Destination Rule でルーティングが発生した際にトラフィックに適用されるポリシーを設定します。EnvoyfilterとRate Limit ConfigMapは、どのようなリクエストに対してどのような制限をするか設定します。 Rate Limitサービスはマイクロサービスと同じEKSにデプロイし、RedisはAWS ElastiCache for Redisを利用しています。現在、Rate Limitを利用しているマイクロサービスは多くないため、Rate LimitサービスとRedisはKubernetes Clusterごとに共通のものを使用しています。 設定と計測のポイント 会員基盤へ導入するにあたって実施したことを2つほど紹介します。 1. レイテンシの計測 Rate Limitを導入することでレイテンシが悪化してしまうというような懸念も考えられます。私達は普段からAPIの性質に応じて目標レイテンシを設定しているため、今回のような外部APIをコールするAPIに対しても目標を設定しRate Limitの有無でどのような変化があるか確認しました。 実際に計測した方法は、Rate Limitの有無がAPIのレイテンシにどのように影響を与えるかというシンプルな方法です。EnvoyFilterの作成、削除でRate Limitの有無を設定できるため、とても容易に計測できました。 計測結果としては、Rate Limitを設定していた場合に約30msほどレイテンシが高くなるという結果となりました。マイクロサービスやインフラの構成によって差異が発生するため参考程度にしていただければと思います。 2. 1つのAPIに対して2重の制限を設定 LIQUID APIへのリクエストに対して1秒と1分単位の制限を2重で設定しています。ここでは、EnvoyFilterとRate Limit configの設定例を紹介します。 まずはEnvoyFilterです。特定のマイクロサービスからZOZOTOWNへのリクエストに対してRate Limitで制限するための設定になります。 apiVersion : networking.istio.io/v1alpha3 kind : EnvoyFilter metadata : name : zozotown spec : configPatches : ... - applyTo : HTTP_ROUTE match : context : SIDECAR_OUTBOUND routeConfiguration : vhost : name : zozo.jp:443 route : name : zozotown patch : operation : MERGE value : route : rate_limits : - actions : - generic_key : descriptor_value : zozotown-per-second - actions : - generic_key : descriptor_value : zozotown-per-minute 設定項目の説明をすると ApplyTo は、パッチを適用する対象を指定します。 Match(EnvoyConfigObjectMatch) は、パッチを適用する条件を指定します。SIDECARからHTTP_ROUTEの vhost(zozo.jp:443) に定義されている route(zozotown) に対するOUTBOUNDトラフィックに適用されます。SIDECARに設定されているルートコンフィグの確認は、 istioctl proxy-config route <pod name> で確認可能です。 Patch は、対象の扱い方を指定します。マッチしたリクエストに対してRate Limitで制御するためにdescriptorを付与しますが、どのような値を付与するかは descriptor_value に定義します。1秒間の制御に利用する zozotown-per-second と1分間の制御に利用する zozotown-per-minute を付与する設定となっています。 次にRate Limitの設定ファイルです。 domain : global-ratelimit descriptors : - key : generic_key value : zozotown-per-second rate_limit : unit : second requests_per_unit : 10 - key : generic_key value : zozotown-per-minute rate_limit : unit : minute requests_per_unit : 100 Rate Limitが制御する際に参照する設定ファイルの descriptors は、上記のように定義します。この例ではEnvoyFilterで定義した descriptor_value (設定ファイル内では、value)を利用し、1秒間に10回、1分間に100回という制限を設定しています。 このようにすることで特定のマイクロサービスからZOZOTOWNへのリクエストに対して1秒間に10回、1分間に100回という制限を設けることができます。 事例2. オンプレミス環境のリクエスト制限 次に、オンプレミス環境の注文処理に対してRate Limitを導入した事例をご紹介します。 2023年6月現在、注文処理はオンプレミス環境上のレガシーシステム(Classic ASP)で稼働していますが、高負荷時のボトルネックとして顕著になってきたため、リプレイスを待たずに流量制限の仕組みを導入することになりました。 構成 この事例の特徴は、Istio Rate Limitを使って、サービスメッシュ外のオンプレミス環境で稼働するアプリケーションの処理を制御している点です。 オンプレミス環境の注文処理アプリケーションにリクエストがあると、Rate Limit用エンドポイントにリクエストを送ります。 そのエンドポイントに対してRate Limitが設定されており、閾値を超えた場合はHTTPステータス429を返し、閾値を超えない場合はDummy Web ServerがHTTPステータス200を返します。 注文処理アプリケーションはそのHTTPステータスに従い、429の場合はカート画面にリダイレクトします。それ以外の場合は後続処理へ進み、SQLServerに接続します。 Rate Limit用エンドポイントであるDummy Web Serverは、サービスメッシュ内の空のAPIであり、常にHTTPステータス200を返します。 設定と計測のポイント 1. 商品別の注文リクエスト数制限 ZOZOTOWNの注文確定時の処理に対して、2段階の制限をかけています。 注文処理の全リクエスト数に対する制限 商品ID単位のリクエスト数に対する制限 同一商品へのリクエストが集中することで、データベースの同一ページに対するラッチ競合が発生します。 これを防ぐために商品ID単位での制限値も設ける必要がありました。 仕組みとしては、クライアント側でリクエストヘッダーに商品ID(以下goods-id)を付与し、Istio Rate Limitでヘッダーの値ごとにカウントすることで実現しました。基本的な設定内容は事例1と同じですが、 request_headers でヘッダー名を指定することで、ヘッダーの値ごとにカウントできます。 apiVersion : networking.istio.io/v1alpha3 kind : EnvoyFilter metadata : name : zozotown spec : configPatches : ... - applyTo : HTTP_ROUTE match : context : ANY routeConfiguration : vhost : name : <FQDN>:80 route : name : zozotown patch : operation : MERGE value : route : rate_limits : - actions : - generic_key : descriptor_value : zozotown-order-count-by-goodsid - request_headers : header_name : goods-id descriptor_key : goods-id 2. 閾値決定のための負荷試験 以下のように設定することで、全体数の閾値とgoods-idごとの閾値を設定できました。 descriptors : - key : generic_key value : zozotown-order-count rate_limit : unit : second requests_per_unit : 200 - key : generic_key value : zozotown-order-count-by-goodsid descriptors : - key : goods-id rate_limit : unit : second requests_per_unit : 100 これらの閾値を決めるために負荷試験を実施しました。 まずは単体負荷試験として、Rate Limit Podの処理性能を確認しました。Rate Limit Podは今後複数の機能で共通利用していく想定のため、キャパシティ計画を立てやすいように1Podあたりが処理できるリクエスト数を明確にしました。 次に、Rate Limitが発動しないように閾値を非常に大きくした状態で、注文処理の結合負荷試験を実施しました。 徐々に注文処理のリクエスト数を増やし、データベースでページラッチが発生してパフォーマンス影響が出たところを限界値とし、それよりも小さい閾値でRate Limitの閾値を設定しました。 余談ですが、Rate Limitを発動させたくない場合は、 shadow_mode を有効化することで透過モードとなり、Rate Limitの閾値を超えても常にOKと判定されるようになります。 パフォーマンスについても、Rate Limit Pod単体でのレイテンシは5ms程度で、上記の構成によるオンプレミス環境側へのレスポンスも20ms~30ms程度と短く、注文機能として許容できるものでした。 今後の展望 リプレイスが進む中でシステムのキャパシティは変わっていくため、今後もRate Limitの閾値を調整していくことが考えられます。 現時点で基本的な監視は導入済みで、各Podの負荷状況、Rate Limit用エンドポイントへの総リクエスト数、Limitを超過したリクエスト数を監視しています。 課題として、ヘッダーのgoods-id別でカウント数を可視化する必要があり、監視の改善を進めています。 envoy proxyのドキュメントによると、 detailed_metric を有効化することで、今回のようにヘッダー値を指定しない場合でもヘッダー値をmetricsに含めることができるそうです。 最終的には他のIstioのメトリクスと同じようにDatadogで可視化できないかを検証中です。 また、現在カート投入のリクエストに対しても同じ対応を進めており、Istio Rate Limitの機能を横展開する予定です。 感想 EnvoyFilterの利用が初めてで普段読み慣れているIstio公式ドキュメントの EnvoyFilter には詳細な情報があまりなかったため、 Envoy のドキュメントを読みながら試行錯誤するのが大変でとても苦労しました。 Istio Rate Limitは柔軟で詳細な設定ができ、様々なケースに対応できそうだと感じました。また、パフォーマンスや安定性は非常に高く、満足のいく構成となりました。 最後に 現在、私たちと共にサービスを支える方を募集しています。少しでもご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co
アバター