TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

938

はじめに こんにちは。ブランドソリューション開発本部WEARバックエンド部SREブロックの山岡( @ymktmk )です。 2024年1月25日にFindy社によるオンラインイベント「Kubernetes活用の手引き 私たちの基盤構築・運用事例 Lunch LT」が開催されました。このイベントでは、株式会社メルカリさん、株式会社MIXIさん、LINEヤフー株式会社さんから一人ずつ、弊社からも私がLTをしてきましたので、こちらのブログでも報告いたします。 findy.connpass.com 登壇内容 今回のイベントでは、以下のような方をターゲットとして、各社からKubernetesに関する取り組みを紹介しました。 他社のKubernetes活用事例を知りたい方 Kubernetesを運用するための工夫点や検討すべき点を知りたい方 私からは、「Kubernetesを活用した開発者体験向上の取り組み」というタイトルで発表いたしました。 speakerdeck.com 発表内では、弊チームがこれまで取り組んできた開発者体験の向上施策のうち、「負荷試験基盤」と「Pull Request毎のPreview環境」の2つの事例を取り上げてお話ししました。以下は今回の発表の要約です。 負荷試験基盤 弊チームの負荷試験では各々が異なる負荷試験ツールを使用しており、統一されていませんでした。そのため、チームにおける負荷試験のノウハウの蓄積が難しく、特に新規メンバーは負荷試験の実施が容易ではありませんでした。 そこで、使用するツールをK6に統一し、 k6-operator とGitHub Actionsを活用した負荷試験基盤を作成しました。これにより、負荷試験の実施者は、テストシナリオをGitHubにPushし、実施したいタイミングでGitHub Actionsのワークフローを手動実行するだけで負荷試験が可能になりました。 導入後の効果としては、負荷試験を手軽に実施できるようになり、負荷試験のハードルが下がりました。また、+αの効果として、GitHubでテストシナリオを管理するようになったため、過去のシナリオを再利用できるようになり、負荷試験の準備やレビューが容易になりました。それにより、負荷試験の妥当性が向上し、心理的な負担も軽減しました。 Pull Request毎のPreview環境 私たちが担当する WEAR はローンチから10周年を迎えており、10年間の技術負債を解消すべく、Webフロントエンドをリプレイス中です。リプレイスにおいて、旧環境のUIと比較しながらレビューすることが多いため、負担が大きいことや、デザイナーを巻き込む場合にはStaging環境を使うため、他の機能のリリースを妨げていました。また、VercelやHerokuなどのモダンなPaaSに備わっているPreview Deployment機能を使った開発者体験を求める声が多く寄せられていました。 そこで、 Argo CD Pull Request Generator を活用し、Pull Request毎に Virtual Service 、Service、DeploymentのセットをKubernetesクラスターに適用することでPreview環境を実現しました。 導入後の効果としては、「Pull Requestのレビューがしやすくなりました」「開発スピードが爆上がりしました」などの嬉しいフィードバックをいただきました。現在では、フロントエンドチームが作成するPull Requestの半分以上はPreview環境が活用されています。 まとめ Kubernetesは高い拡張性を活かして様々な機能開発ができます。弊チームでは、k6-operatorを使って負荷試験基盤、Argo CD Pull Request Generatorを使ってPull Request毎のPreview環境を実現しています。Kubernetesと開発者体験の向上はとても相性が良いです。運用において発生した課題を解決するためOperatorの導入や開発に取り組んでみてはいかがでしょうか。 10分という短い時間でしたので詳細なことは話せませんでしたが、これらの事例の技術面にフォーカスした話は、弊社のTechBlogやイベントで発表していますので、ぜひご覧ください。 techblog.zozo.com techblog.zozo.com 登壇後の所感 登壇後のフィードバックやX( #k8s_findy )の実況を拝見すると、「k6-operator」や「Argo CD Pull Request Generator」といったKubernetes Operatorに対する関心が高いようでした。弊社での活用事例が参考になったとの声もいただき、大変嬉しく思います。 普段、社外からチームの活動に対する評価をいただく機会が少ない中、このような登壇の機会を頂けたことをとても感謝しています。今後も機会がありましたら、新たな情報発信ができればと思います。 謝辞 本イベントはFindy社の主催でしたので、Findyの方々には様々なサポートをしていただきました。そして、登壇に至るまでサポートしてくれた社内のDevRelチームや弊チームにもこの場を借りて感謝申し上げます。また、ご視聴いただいた皆様ありがとうございました。 おわりに ZOZOでは、プロダクト開発以外にも、今回のようなイベントの開催や登壇など、外部への発信も積極的に取り組んでいます。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com corp.zozo.com
アバター
初めまして。MLデータ部データ基盤ブロックの小泉です。 本記事ではGartner社から提唱されたActive Metadata Managementに着目し、BigQueryのCompute費用を削減した方法についてご紹介します。 目次 目次 Active Metadata Managementとは 結局どんなことを行なったのか、ざっくりまとめ Compute費用のpricing modelとReservationについて pricing model Reservation Metadataを使用して最安値のpricing modelを求める方法 マート集計クエリ実行時、pricing modelを切り替える方法 まとめ Active Metadata Managementとは Active Metadata Managementとは、Gartner社が提唱するメタデータ管理の新しい考え方です。 簡単に一言で説明すると、 システムが自らメタデータを収集、分析、洞察し、具体的なアクションを起こすこと です。この 一連のプロセスが継続的に行われること もポイントです。これにより、データの品質管理、セキュリティ、コストなどの最適化が期待される考え方になります。 詳細な解説がされているGartner社の記事はこちらです。 towardsdatascience.com 上記記事を要約すると、Active Metadata Managementには以下4つの特徴があります。 Active metadata platforms are always on. 人間が手作業でメタデータを入力することなく、常にあらゆるメタデータを自動収集する。 Active metadata platforms don’t just collect metadata. They create intelligence from metadata. メタデータを収集するだけでなく、収集したメタデータを分析して洞察する。加えて時間と共に洞察の精度を向上させる。 (例)クエリログからSQLコードを解析して自動的にカラムレベルのリネージを作成し、どのテーブルが最もクエリされているかを推察する。 Active metadata platforms don’t just stop at intelligence. They drive action. 収集したメタデータを分析して洞察した結果を、自ら活用する。 (例)過去のログを使用して、どのデータセットが最も利用されているかを分析。その分析結果をデータパイプラインシステムに送り、データパイプラインの実行スケジュールを自動的に最適化する。 Active metadata platforms are API-driven, enabling embedded collaboration. 外部ツールとAPI連携が可能。 (例)あるデータへのアクセスをリクエストされた際、データ所有者はSlack上でリクエストを受け取り、承認または拒否ができる。 今回私たちは、3つ目の特徴(Active metadata platforms don’t just stop at intelligence. They drive action.)に着目しCompute費用削減を行ないました。 3つ目の特徴についてもう少し詳しく説明します。 この特徴を言い換えると「メタデータを使用し、システムに対して自動的に何かをする」ということです。この「自動的に何かをする」というところが重要になります。先ほど挙げた例においては、「データパイプラインの実行スケジュールを自動的に最適化する」という部分が「自動的に何かをする」を指しています。また、データパイプラインの実行スケジュールが最適化されるまでに、人間が手を加える必要がないことも重要です。 つまり、 人間が介入することなく、システムが自動的にメタデータを分析し、アクションを起こすこと が3つ目の特徴となります。 以上がActive Metadata Managementの特徴です。本記事がActive Metadata Management活用のきっかけとなれば幸いです。 では、本題に入ります。 結局どんなことを行なったのか、ざっくりまとめ 費用削減までの工程をざっくりまとめると以下の通りです。 各工程における詳細な情報は後述します 。 各種pricing modelを設定したReservationを用意し、各々専用のprojectを割り当てる INFORMATION_SCHEMA.JOBS_BY_PROJECTから取得した過去の集計実績を元に、データマート毎に最安値のpricing modelを求める マート集計クエリ実行時、クエリを実行するprojectを最安値のpricing modelを設定しているprojectに切り替えて集計する 改めまして、ここからCompute費用の削減方法について詳しくご紹介していきます。 Compute費用のpricing modelとReservationについて 今回のCompute費用削減においては、Compute費用のpricing modelとReservationへの理解が重要になります。特に後述するReservationへprojectを割り当てる「Assignment」は、Compute費用削減に利用する機能です。それでは、Compute費用のpricing modelとReservationについて説明していきます。 pricing model まず、Compute費用のpricing modelについて簡単に説明します。 ※私たちはUSリージョンでBigQueryを使用しているので、USリージョン版単価での説明となります。 Compute費用のpricing modelを簡単な図にまとめました。 cloud.google.com 順を追って説明していきます。 まず、Compute費用のpricing modelは On-demand compute pricing と Capacity compute pricing の2種類に分類されます。違いは以下の通りです。 On-demand compute pricing:クエリ実行時にスキャンされたデータ量に対して課金される Capacity compute pricing:クエリ実行時に使用された計算リソース(Slot)に対して課金される それぞれのpricing modelについてもう少し詳しく解説していきます。 On-demand compute pricingは クエリ実行時にスキャンされたデータ量に対して1TBあたり6.25USD が課金されます。従って、クエリ実行時にスキャンされるデータ量が多いほど費用が高くなります。そしてCompute費用削減の観点からは、クエリ実行時にスキャンされるデータ量を少なくすることが重要になります。以下がOn-demand compute pricingを適用した方がお得になるクエリのイメージです。 (例)On-demand compute pricingを適用した方がお得になるクエリのイメージ SELECT Window関数等の複雑な計算 FROM 小さいテーブル_1 CROSS JOIN 小さいテーブル_2 CROSS JOIN 小さいテーブル_3 上記クエリは、複雑な計算を行なっているのでSlot使用量は多くなりますが、小さいテーブル_1,2,3のデータ量が少ないため、クエリ実行時にスキャンされるデータ量が少なくなります。従ってOn-demand compute pricingを適用した方がお得になります。 一方、Capacity compute pricingは クエリ実行時に使用された計算リソース、いわゆるSlot に対して課金されます。つまり、クエリ実行時に使用されるSlot数が多いほど費用が高くなります。Compute費用削減の観点からは、クエリ実行時に使用されるSlot数を少なくすることが重要になります。以下がCapacity compute pricingを適用した方がお得になるクエリのイメージです。 (例)Capacity compute pricingを適用した方がお得になるクエリのイメージ SELECT * FROM 巨大なテーブル LIMIT 10 上記クエリは、処理がシンプルなのでSlot使用量は少なくなりますが、巨大なテーブルのデータ量が多いため、クエリ実行時にスキャンされるデータ量が多くなります。従ってCapacity compute pricingを適用した方がお得になります。 また、Capacity compute pricingは、 BigQuery editions という、利用可能な機能・単価に違いがある3種類のplanから選択をする必要があります。以下がBigQuery editionsの一覧です。 pricing model 課金単位 単価 Standard Edition Slot(hour) 0.04USD Enterprise Edition Slot(hour) 0.06USD Enterprise Plus Edition Slot(hour) 0.1USD 料金の傾向として、Standard Editionsが最安値のpricing modelになります。 各Editionの違いに関する詳細は以下をご確認下さい。 cloud.google.com 各Editionで使用できる詳細な機能の違いについては上記ドキュメントをご確認いただければと思いますが、ここでは今回のCompute費用削減に関係する2点をご紹介します。 まず1点目は、3つのEditionでは費用の計算方法が異なることです。詳細は Reservation でご紹介しますが、以下が各Editionでの課金方法です。 Standard Edition:Autoscale Slot Enterprise Edition:Autoscale Slot + Baseline Slot Enterprise Plus Edition:Autoscale Slot + Baseline Slot 2点目は、 Fine-grained security controls の使用可否です。Fine-grained security controlsとは、BigQueryテーブルのカラムに対して、以下のようなことができる機能(他にもいくつかあります)です。 policy_tag というタグを付与することで特定カラムのデータへのアクセス権限を制限すること 特定カラムのデータをマスキングすること この機能についても3つのEditionで使用できるかどうかが異なります。今回、このFine-grained security controlsの使用可否は重要なポイントになりますので、念頭に置いておいていただけると幸いです。 Standard Edition:使用不可 Enterprise Edition:使用可能 Enterprise Plus Edition:使用可能 Fine-grained security controlsの詳細は以下をご確認下さい。 cloud.google.com ここからは私たちが採用している3つのpricing modelについてご紹介します。 Compute費用削減前の私たちは、全てのデータマートをEnterprise Editionのみで集計していましたが、今回新たにOn-demandとStandard Editionを導入しました。現在はこの3つのpricing modelからデータマート毎に最安値のpricing modelを求め、Compute費用を削減しています。 On-demand Standard Edition Enterprise Edition 以上がCompute費用のpricing modelについての説明です。続いてReservationについて説明します。 Reservation まず、Compute費用削減においてなぜReservationが関係するのか説明します。 現状(2024年2月時点)では クエリ実行時に使用するpricing modelをダイレクトに切り替える機能がありません 。そのため、今回のCompute費用削減ではReservationのAssignment機能を利用してpricing modelを切り替えることにしました。では、最初にReservationの解説に入ります。 Reservationとはクエリ実行に使用するSlot数をあらかじめ予約できるBigQueryの機能です。作成時は以下の設定をします。On-demand用のReservationに関しては、 Explicit On Demand Resources というReservationがデフォルトで用意されていますのでこちらを使用しています。 Reservation:Reservationの名前 Location:リージョン Editions:Reservationへ適用するBigQuery editions Max Reservation size:クエリ実行中、必要に応じて自動的に追加されるSlot(Autoscale Slot)の上限 Baseline Slot:常時Reservationへ割り当てるSlot数 以下がOn-demand用のReservation、Explicit On Demand Resourcesです。 詳細なReservationの設定方法は以下をご確認下さい。 cloud.google.com また pricing model でも触れましたが、Reservation作成時に選択した BigQuery editionsによって課金されるCompute費用の計算方法が異なります のでご注意ください。 Standard Editionを選択した場合は、以下の計算方法で課金されます。加えて設定できるMax Reservation sizeの上限は 1600Slot です。 クエリ実行中、自動的に追加されるSlot数(Autoscale Slot)のみ Enterprise EditionとEnterprise Plus Editionは以下の計算方法で課金されます。こちらはMax Reservation sizeの上限がありません。 Baseline Slot + クエリ実行中、自動的に追加されるSlot数(Autoscale Slot) 加えてこの2つのEditionは クエリ実行をしていない場合でも、設定したBaseline Slot分の料金が常に課金される ので注意してください。 以上がBigQuery editionsにおける費用の計算方法の違いです。Baseline SlotやAutoscale Slotの詳細については以下をご確認下さい。 cloud.google.com 続いてAssignmentについて説明します。AssignmentとはReservation作成後、設定したSlotを使用するため、project, folder, organizationのいずれかを割り当てる機能のことです。今回のCompute費用削減においては、projectをAssignmentするパターンを採用しているので、projectをAssignmentする方法について説明します。 まず、ReservationへprojectをAssignmentする効果は 設定したprojectで実行されるクエリがReservationのSlotを確保できる ということです。 そして最大のポイントは、クエリ実行する際にAssignmentしたprojectを選択すると、 Reservationで設定したpricing modelを使用して費用課金される ということです。今回はこのポイントを利用してCompute費用削減を行なっています。 具体的なAssignmentの方法ですが、BigQuery editionsを採用しているReservationの場合は、コンソールから操作が可能です。 注意点は、On-demand用Reservation(Explicit On Demand Resources)にはコンソールからprojectのAssignmentができないことです。On-demand用Reservation(Explicit On Demand Resources)へprojectをAssignmentする方法は2種類あります。 bqコマンドを実行する CREATE ASSIGNMENT DDLステートメントをコンソールから実行する 今回私たちは、bqコマンドを実行する方法を採用しました。 bq mk \ --location = LOCATION \ --reservation_assignment \ --reservation_id = none \ --job_type = QUERY \ --assignee_id = PROJECT_ID \ --assignee_type = PROJECT reservation_id=noneに設定する ことで、指定したprojectはOn-demand用Reservationを使用したクエリ実行が可能になります。加えて、On-demandはQUERYジョブ(job_type=QUERY)のみのサポートとなりますのでご注意下さい。 CREATE ASSIGNMENT DDLステートメントをコンソールから実行する方法を含む詳細なAssignment設定方法は以下をご確認下さい。 cloud.google.com ここからは私たちのReservation設定についてご紹介します。 1つのReservationに複数のprojectを割り当てることも可能ですが、今回私たちは各price modelを設定したReservationに対し、1つずつ専用projectを割り当てました。 以上がReservationについての説明です。続いて、Compute費用削減における各工程について説明します。 Metadataを使用して最安値のpricing modelを求める方法 まず、私たちが採用している3つのpricing modelを使用する際の注意点について説明します。 pricing model でご紹介した通り、今回のCompute費用の削減に伴い新たにOn-demandとStandard Editionを導入しました。3つのpricing modelからデータマート毎に最安値のpricing modelを求め、Compute費用削減をしています。 On-demand Standard Edition Enterprise Edition しかし、 pricing model でも触れた通り、Standard Editionは Fine-grained security controlsが使用できません 。つまり、policy_tagが付与されているテーブルを使用したクエリには適用不可能ということになります。従って、Standard Editionを適用するには以下の注意点を考慮する必要があります。 実行クエリ内でpolicy_tagが付与されているテーブルを参照していないこと この条件を判定するには policy_tagが付与されているテーブル情報 が必要になりますが、現状(2024年2月時点)では INFORMATION_SCHEMA からこの情報を取得できません。そのため、私たちはpolicy_tagが付与されているテーブルの一覧情報を管理するテーブルを自作しています。こちらの管理テーブルからpolicy_tagが付与されているテーブルの一覧を取得することで Standard Edition を適用する条件を満たしているかどうかを判定できます。 以下が最安値のpricing modelを求めるクエリです。このクエリを実行していただければ、全データマートの最安値pricing model情報を一括で求めることが可能です。 WITH -- policy_tagが付与されているテーブル一覧を全て取得 GetSensitive AS ( SELECT DISTINCT TableName FROM `プロジェクトID.データセットID.policy_tag管理テーブル` ), -- 集計されているデータマート情報を各3種類の料金プランが設定されているプロジェクトから取得 --(On-demand, Standard Edition, Enterprise Edition) JOBS_BY_PROJECT AS ( SELECT destination_table, total_bytes_processed, total_slot_ms, referenced_tables, creation_time, user_email, job_type FROM ` On -demand設定のプロジェクトID.region-us.INFORMATION_SCHEMA.JOBS_BY_PROJECT` UNION ALL SELECT destination_table, total_bytes_processed, total_slot_ms, referenced_tables, creation_time, user_email, job_type FROM `Standard Edition設定のプロジェクトID.region-us.INFORMATION_SCHEMA.JOBS_BY_PROJECT` UNION ALL SELECT destination_table, total_bytes_processed, total_slot_ms, referenced_tables, creation_time, user_email, job_type FROM `Enterprise Edition設定のプロジェクトID.region-us.INFORMATION_SCHEMA.JOBS_BY_PROJECT` ) --データマート毎に過去7間の平均価格を各3種類の料金プランで算出(On-demand, Standard Edition, Enterprise Edition) SELECT destination, ondemand_avg_price, enterprise_avg_price, standard_avg_price, policy_tag_flag, IF (policy_tag_flag , CASE WHEN enterprise_avg_price > ondemand_avg_price THEN ' On-demand設定のプロジェクトID ' ELSE ' Enterprise Edition設定のプロジェクトID ' END , CASE WHEN standard_avg_price > ondemand_avg_price THEN ' On-demand設定のプロジェクトID ' ELSE ' Standard Edition設定のプロジェクトID ' END ) AS lowest_project FROM ( SELECT CONCAT (destination_table.project_id, ' . ' ,destination_table.dataset_id, ' . ' ,destination_table.table_id) AS destination, SUM ((total_bytes_processed / 1024 / 1024 / 1024 / 1024 ) * 6 . 25 ) / 7 AS ondemand_avg_price, SUM ((total_slot_ms / 1000 / 60 / 60 ) * 0 . 06 ) / 7 AS enterprise_avg_price, SUM ((total_slot_ms / 1000 / 60 / 60 ) * 0 . 04 ) / 7 AS standard_avg_price, -- policy_tagが付与されているテーブルを参照しているかどうかを判定 LOGICAL_OR(ARRAY_LENGTH(ARRAY( SELECT referenced_table.table_id FROM UNNEST(referenced_tables) AS referenced_table INNER JOIN GetSensitive ON referenced_table.table_id = TableName)) > 0 ) AS policy_tag_flag FROM JOBS_BY_PROJECT WHERE DATE (creation_time, " Asia/Tokyo " ) BETWEEN DATE_SUB( CURRENT_DATE ( " Asia/Tokyo " ), INTERVAL 8 DAY) AND DATE_SUB( CURRENT_DATE ( " Asia/Tokyo " ), INTERVAL 1 DAY) AND user_email = ' データマート集計に使用するサービスアカウント ' AND job_type = ' QUERY ' GROUP BY destination ) クエリをご覧いただければわかる通り、データマート毎に過去7間の平均価格を各3種類の料金プランで算出及び比較をして最安値のpricing model( lowest_project カラム)を求めています。特に lowest_project カラムは、pricing modelを切り替える際に使用する情報ですのでご注目ください。 INFORMATION_SCHEMA.JOBS_BY_PROJECTの詳細については以下をご確認下さい。 cloud.google.com 以上がMetadataを使用して最安値のpricing modelを求める方法の説明です。 マート集計クエリ実行時、pricing modelを切り替える方法 続いて、マート集計クエリ実行時にpricing modelを切り替える方法について説明します。流れとしては、データマート集計クエリ実行時、取得した最安値のpricing modelへ切り替えをしてから、集計・更新するというものなっています。該当部分のコードを抜粋します。 from google.cloud import bigquery client = bigquery.Client(project= 'Enterprise Edition設定のプロジェクトID' ) query_job = client.query(project= '最安値のプロジェクトID' , query= 'データマート集計クエリ' , job_config= '集計結果を格納するテーブル設定など' ) 一番下の query_job に注目して下さい。ここで、 bigquery.Client のprojectパラメーターに先程の lowest_project カラム情報を指定しています。この設定をすることで、最安値のpricing modelを使用して集計ができます。つまり、以下3つのprojectの内、最安値のReservationにAssignmentされているprojectがパラメーターに設定されます。現状(2024年2月時点)では bigquery.Client にpricing modelを切り替えるパラメータが存在しないため、このような手法を採用しています。 また、 client にて bigquery.Client を定義する際、projectパラメータへ初期値として Enterprise Edition専用project を設定しています。これにより、最安値のpricing model情報が取得できなかったデータマートは、初期値に設定したproject(Enterprise Edition)で集計が行われます。 以上がマート集計クエリ実行時、pricing modelを切り替える方法です。 まとめ 本記事では、Active Metadata Managementに着目し、BigQueryのCompute費用を削減する方法についてご紹介しました。Compute費用の削減前は、全てEnterprise Editionでデータマート集計しておりましたが、この方法を採用した結果、以下のような割合でデータマート集計がされています。 On-demand:約20% Enterprise Edition:約30% Standard Edition:約50% Compute費用削減後、約50%のデータマートがStandard Editionを使用しています。Standard EditionはMax Reservation sizeの上限が1600Slotとなっています。そのため、集計遅延が発生する懸念をしておりましたが、Compute費用削減後も集計遅延は起きておらず削減前と変わらない速度で集計が完了しています。 そして本記事のタイトルにもなっていますが、トータルのCompute費用は約40%削減ができました。 比較的お手軽にCompute費用削減ができる方法ですので、是非お試しください。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
こんにちは。カート決済部カート決済基盤ブロックの斉藤とSRE部カート決済SREブロックの飯島です。普段はZOZOTOWNカート決済サービスのリプレイスに携わっています。 弊社はモノリスからマイクロサービスへのリプレイスを進めており、カート決済サービスもその一環としてリプレイスを進めています。 よろしければ、カートリプレイスPhase1としてカート投入リクエストのキャパシティコントロールを実現させた事例もご覧ください。 techblog.zozo.com 本記事ではリプレイスの中でも、カートリプレイスPhase2としてZOZOTOWNで扱う在庫データをクラウドリフトした事例を紹介します。 はじめに 背景・課題 解決へのアプローチ アプリケーションの説明 DynamoDB API その他、AWSリソース 在庫の同期・補正を行うバッチ リリースまでの道のり 負荷試験 リリース 1. 初期データの移行 DynamoDBのキャパシティ 整合性の担保 2. リリース 効果 他の施策との相乗効果 さいごに はじめに 本章ではまず、リプレイス前のZOZOTOWNの在庫データの概要を説明します。 ZOZOTOWNの在庫データはオンプレミス環境で稼働しているSQL ServerのDB・テーブルで管理されています。CartDBとFrontDBという2つのDBで管理しており、CartDBはユーザのカート操作に関わる在庫データ、FrontDBはユーザの注文確定に関わる在庫データを管理しています。CartDBとFrontDBの在庫データの概要を以下に示します。 CartDBの在庫テーブル 販売可能数を管理している メタデータとして販売種別や販売開始日、販売価格等も管理している カート投入操作で更新される CartDBのカートテーブル ユーザがカートに確保している販売可能数を管理している カート投入・カート削除操作で更新される FrontDBの在庫テーブル 販売可能数を管理している メタデータとして販売種別や販売開始日、販売価格等も管理している 注文確定で更新される 上記テーブルのデータの流れを以下の図に示します。 CartDBとFrontDBの在庫テーブルは過去1つのDBで管理されていました。しかしZOZOTOWNの成長に合わせてカート投入と注文確定という在庫データへの主要なユーザアクションによるDBへの負荷を分散させるために分離されました。在庫テーブル・カートテーブルはSQL Serverのトランザクションで更新することによって販売可能数の整合性を担保していました。 背景・課題 昨今のZOZOTOWNの成長やカートリプレイスPhase1でも触れていた特定商品へのカート投入リクエストの集中によって在庫データへのアクセスが増加しており、負荷が上昇していました。また、上述した通り在庫データの管理にオンプレミス環境で稼働しているSQL Serverを使用しているため、耐障害性やスケーラビリティに課題がありました。このような課題から現在の在庫データの構成だと数年後には限界を迎えると考えられます。 具体的な負荷上昇につながっているボトルネックについて説明します。 CartDBで管理している在庫テーブルはカート投入時に以下のようなクエリが実行されます。 update 在庫テーブル set 販売可能数 = 販売可能数 - 1 where PK = *** 上記のクエリが同一の特定商品へ大量に実行されると、在庫テーブルへの読み取り要求・更新要求の競合が発生し、クエリの実行時間の大幅な遅延やタイムアウトが発生します。最悪のケースでは、クエリの遅延によりワーカースレッドが枯渇しCartDB全体のスループットが著しく下がるという障害が発生することもあります。SQL Serverの読み取り要求・更新要求の競合については以下に詳しく記載されていますので気になる方はご覧ください。 techblog.zozo.com 解決へのアプローチ この問題を解決するためにCartDBで管理していた販売可能数をAmazon DynamoDB(以下、DynamoDB)へ移行することにしました。 DynamoDBはオンプレミス環境で稼働しているSQL Serverと比較して以下のようなメリットがあります。 耐障害性が高い スケーラビリティが高い 自動でキャパシティがスケールイン・スケールアウトされる また、カートリプレイスPhase1でも採用されているため、既に運用ノウハウがある点も採用に至った理由です。弊社の半澤がカートリプレイスプロジェクトでDynamoDBを採用した背景について説明している記事がAmazon Web Serviceブログに掲載されていますので、そちらもぜひご覧ください。 aws.amazon.com アプリケーションの説明 ここからカートリプレイスPhase2のシステム構成について説明します。 システム構成図は以下のようになっています。 DynamoDB SQL Serverから移行された在庫データを持ちます。テーブル定義の例を以下に示します。 AttributeName Type KeySchema Description stock_id N Hash 商品のサイズ・カラーを特定できるID goods_id N 商品を特定できるID stock_quantity N 販売可能数 created_at S 作成日時 updated_at S 更新日時 上記のようにZOZOTOWN上で更新頻度が高い販売可能数をDynamoDBへ移行しました。これによって同一の特定商品へカート投入リクエストが集中した場合でも負荷に耐えられることが可能となりました。 上記のテーブルが生まれたことにより、ZOZOTOWNの在庫データの流れは以下のようになりました。 在庫同期については後述します。 今回、読み取り/書き込みスループットの課金方法と管理方法についてはオンデマンドモードを採用しています。その理由としては、ZOZOTOWNはカート投入リクエスト数がイベントや時間帯によって大きく異なり、また人気商品の発売による突発的なリクエスト増加もあるためキャパシティを予測することが困難なためです。リクエストが少ない時はキャパシティが縮小されてコストが抑えられ、逆にリクエストが増加した際には自動で拡張されるため柔軟性と拡張性を担保できます。 バックアップについてはPoint-in-time recovery(以下、PITR)を採用しています。PITRとは自動で継続的な増分バックアップを作成し、直近5分より以前の特定の状態に復元できるDynamoDBの機能です。バックアップの保持は35日間であるためそれ以前の状態には復元できません。他にも定期的にフルバックアップを作成するオンデマンドバックアップ機能もありますが、今回は以下の理由からPITRを採用しました。 在庫データは常に変動するため、何日前の何時いつの状態にリカバリするといった要件がない 運用ミスでテーブルを削除してしまった時の念のためのバックアッププランがほしい 復旧時間を短縮するためにできるだけ直近のデータにリカバリしたい API 在庫データをDynamoDBに移行するとともに、在庫データを操作する機能をCartAPIという名称のAPIとして切り出しています。元々はオンプレミスのWebサーバで稼働していたClassic ASPをJavaにリプレイスし、Amazon EKS上(以下、EKS)でマイクロサービス化しました。 CartAPIが持つエンドポイントは以下のものがあります。 販売可能数を取得する参照系 販売可能数を減少、増加する更新系 またSQL ServerとDynamoDBの在庫同期や不整合を防ぐためのバッチから呼び出されるCartBatchAPIも誕生しています。CartAPIと同様にClassic ASPからJavaにリプレイスし、EKS上でマイクロサービス化しました。バッチについては後述します。 CartBatchAPIが持つエンドポイントは以下のものがあります。 販売可能数を取得する参照系 販売可能数を同期、補正する更新系 その他、AWSリソース DynamoDBの更新内容はAmazon Kinesis Data Streams(以下、KDS)を用いてキャプチャし、Amazon Aurora MySQL(以下、MySQL)に保存しています。KDSにキャプチャされたデータはKinesis Client Library (以下、KCL)アプリケーションによって取り出し、MySQLに書き込みます。このMySQLのデータは後述する在庫を補正するバッチで参照されます。KCLアプリケーションの仕様の詳細についてはカートリプレイスPhase1のテックブログで触れているのでご参照ください。 techblog.zozo.com 在庫の同期・補正を行うバッチ 今まではFrontDBの在庫データをCartDBに同期していましたが、リプレイス後はFrontDBの在庫データ内の販売可能数をDynamoDBに同期するように変更しました。 同期のフローは以下のとおりです。 上述のバッチが常に稼働しているためニアリアルタイムでSQL Serverの販売可能数の変更がDynamoDBに反映されます。 しかし、DynamoDBとSQL Serverの更新処理は別トランザクションであるため、ユーザのカート操作によって以下のような不整合が考えられます。 カート投入時 DynamoDBの更新が成功したがCartDB(カートテーブル)の更新に失敗する カート削除時 CartDB(カートテーブル)の更新が成功したがDynamoDBの更新に失敗する 上記のような不整合を防ぐために不整合を検知して補正するバッチを作成しました。 バッチの仕組みとしては以下の2つの履歴から在庫変動があったデータをターゲットとします。ターゲットとなる在庫に対して、オンプレミスの販売可能数とユーザのカートに確保されている販売可能数を用いてDynamoDBの販売可能数を補正するというものです。 ユーザのカート操作の履歴(SQL Server) DynamoDBの操作履歴(MySQL) 上記2つのバッチ処理によって常にDynamoDBの販売可能数はSQL Serverの販売可能数と一致するようになります。 リリースまでの道のり 負荷試験 在庫を同期するバッチや在庫を補正するバッチの負荷試験を行いました。在庫データの更新される件数をもとに同期の件数を処理し切れるか、また同期の処理時間がどの程度かを検証しました。検証結果に応じてチューニングを行い、バッチの並列度・処理間隔を決定しました。 リリース 本番環境におけるSQL ServerからDynamoDBへの在庫データの移行はN%リリースで行いました。以下がN%リリースの手順です。 初期データの移行 ZOZOTOWN内で扱う販売可能数のN%がDynamoDBの販売可能数を参照するように変更するリリース 各ステップの詳細を以下に説明します。 1. 初期データの移行 最初のステップとして、本番環境のSQL Serverの在庫データをDynamoDBに同期しました。SQL Serverの在庫データをCSV形式で出力し、AWSのブログで紹介されているソリューションを参考にしたスクリプトを使用してDynamoDBにインポートしました。 aws.amazon.com スクリプトではBoto3(AWS SDK for Python)というPythonからAWSリソースを操作できるライブラリを使用しています。DynamoDBへの書き込みは batch_writer を使用し、1回の書き込みリクエストの上限を気にすることなく、大量のデータを効率良く処理させています。 以下はスクリプトから一部抜粋、加工したもので、動作は保証しませんのでご了承ください。 import boto3 import csv s3 = boto3.resource( 's3' ) dynamodb = boto3.resource( 'dynamodb' ) def main (s3_bucket=s3_bucket, csv_file=csv_file, dynamodb_table=dynamodb_table): obj = s3.Object(s3_bucket, csv_file).get()[ 'Body' ] table = dynamodb.Table(dynamodb_table) batch_size = 100 batch = [] for row in csv.DictReader(codecs.getreader( 'utf-8' )(obj)): if len (batch) >= batch_size: write_to_dynamo(batch, table) batch.clear() batch.append(row) if batch: write_to_dynamo(batch, table) def write_to_dynamo (rows, table): with table.batch_writer() as batch: for i in range ( len (rows)): batch.put_item( Item=rows[i] ) 今回Lambdaには以下の制約があるためスクリプトの実行環境はEC2を選択しました。 Lambdaのタイムアウトが15分 15分で処理できる件数は約100万件 対象の在庫データの件数は1500万件を超えており、また日々増え続けていました。Lambdaのタイムアウト値や並列数の計算せずにシンプルに考えるためそのような制約がないEC2を選択しました。以下が手順と実行環境の全体図です。 CartDBから在庫データをCSV形式でエクスポート CSVをS3にアップロード EC2にS3のCSVをダウンロード EC2上でスクリプトを実行してCSVの中身をDynamoDBにインポート 次に、インポート時に発生した問題とその解決方法、またインポート後に整合性を確認するために行なった作業をご紹介します。 DynamoDBのキャパシティ 対象の在庫データは1500万件以上で、SQL Serverとの差分を小さくするためできる限り短時間での同期が望まれました。そのため複数台のEC2上でスクリプトを並列実行したところ、オンデマンドモードでの書き込みキャパシティの拡張が間に合わず、スロットリングエラーが多発しました。この問題に対して以下のように解決しました。 同期前にオンデマンドモードからプロビジョニングモードに切り替える 事前にキャパシティを手動で拡張する DynamoDBはオンデマンドモードからプロビジョニングモードに切り替えた場合、24時間経過しないと再度オンデマンドモードに戻せませんのでご注意ください。 整合性の担保 同期完了後に、SQL Serverから出力したCSVファイルとDynamoDBのデータにズレが生じていないか確認しました。スクリプトでログ出力やリトライ設定などは行っているので異常時には検知やリカバリができますが、より厳密に整合性を担保するためです。まずはデータの件数が一致しているかAWS CLIで確認しました。 $ aws dynamodb scan --table-name < table-name > --select COUNT --return-consumed-capacity TOTAL 補足ですが、上記コマンドでDynamoDBの読み込みキャパシティがある程度必要になります。そのため同期前に書き込みだけでなく読み込みのキャパシティも拡張しています。 次にSQL Serverから出力したCSVファイルを正として、データ1件1件をDynamoDBのデータと照らし合わせる作業を実施しました。先述の在庫同期と同じ環境下で、こちらもスクリプトを使用しました。同期完了後はすぐにDynamoDBのテーブルとSQL Serverとの差分を補正するためのバッチ処理を実行する必要がありました。そのため照合作業は以下のように非同期で行いました。 同期した直後のDynamoDBをPITRで復元する 復元元DynamoDBはバッチ処理の対象となりデータが更新されていくためバッチ処理開始前に復元する 復元先のDynamoDBに対してCSVと差分がないか確認する 同期後のデータの件数の一致と、データ内容に差分がないことを確認し、在庫同期において整合性を担保できました。 2. リリース 当然、在庫データはZOZOTOWNでも重要なデータなので影響範囲を限定するためにN%リリースを実施しました。その際、ZOZOTOWNで扱っている全在庫をN%に区切りながらリリースすることが難しかったため、以下のように決めました。 ZOZOTOWNで扱っている特定の1ショップだけクラウド対象にする ZOZOTOWNで扱っている特定の10ショップだけクラウド対象にする ZOZOTOWNで扱っているショップのN%をクラウド対象にする ZOZOTOWNで扱っているショップの100%をクラウド対象にする というように、段階的にクラウド対象を増やしていきました。最終的にはZOZOTOWNで扱っているすべてのショップをクラウド対象にしました。 効果 本リプレイスの効果について説明します。 リプレイス前は同一の特定商品へ大量のカート投入リクエストが実行されると、在庫テーブルにおいて読み取り要求・更新要求の競合が発生していました。その結果CartDBの負荷が上昇していました。しかしリプレイス後では競合の発生を防ぎ、CartDBの負荷を抑えられました。 下図はリプレイス前後で、同一の特定商品へ大量のカート投入リクエストが行われた某日を比較しています。リプレイス後はリプレイス前より5倍近いカート投入リクエストが実行されているにもかかわらず、競合は発生せずにCartDBの負荷も上昇していないことがわかります。 他の施策との相乗効果 カート決済チームではリプレイス以外にもカート機能の改善に取り組んでいます。本リプレイスと同時期にCartDBの負荷を下げる施策として、Istio Rate Limitを用いた商品単位でのカート投入リクエスト制限を導入しています。先述した本リプレイス後の結果はこの施策との相乗効果といえます。 Istio Rate Limitについては注文リクエストの制限に導入した事例が公開されているのでご覧ください。 techblog.zozo.com さいごに ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに ML・データ部推薦基盤ブロックの佐藤( @rayuron )です。私たちはZOZOTOWNのパーソナライズを実現するために、機械学習モデルとシステムを開発・運用しています。本記事ではクーポン推薦のための機械学習モデルとシステム改善に取り組んだ話を紹介します。 はじめに 背景 課題 1. 古い基盤でシステムが運用されている 2. KPIに改善の余地がある 3. 機械学習モデルの評価体制がない 課題解決のために 1. Vertex AI Pipelinesへの移行 2. Two-Stage Recommenderの導入 プロジェクトへの導入 Candidate Generation 1. 過去の実績 2. 人気ブランド 3. 興味を持っているブランドの類似ブランド 評価方法 Reranking 学習データの作成 アンダーサンプリング 特徴量エンジニアリング 学習 バリデーション 推論 3. 機械学習モデルの評価と体制整備 オフライン評価 メトリクスのオフライン評価 過去の施策を活用したオフライン評価 定性評価 実験管理の導入 結果 運用業務の簡易化 KPIの改善 リリースサイクルの改善 今後の展望 バイアスの考慮 評価体制の改善 評価指標の探求 定性評価の体制整備 おわりに 背景 ZOZOTOWNでは一部の提携するショップで使用可能なクーポンが発行されます。クーポンが発行されていることをユーザーに知らせるため、メールやPush通知のチャネルを通してクーポン対象のショップとアイテムの訴求を行なっています。以下はメール配信のイメージ画像です。 クーポン対象となるショップおよびアイテムが決まっている条件下において、メール配信の対象者と推薦ショップ、推薦アイテムを決定するために機械学習モデルとシステムを運用しています。以降、この機械学習モデルをクーポン推薦モデルと表現して紹介します。 課題 既存のクーポン推薦モデルとシステムには以下の課題がありました。 1. 古い基盤でシステムが運用されている 弊社の機械学習パイプラインはVertex AI Pipelinesを標準としているのですが、既存のクーポン推薦のパイプラインは標準化前のCloud Composerで運用されていました。Vertex AI Pipelinesは運用を簡易化するために内製の整備が進んでいる一方、Cloud Composerは整備されていないため運用業務が煩雑で時間を要します。さらにCloud Composerで機械学習パイプラインを作成している社員がほぼいないので属人性が高いという課題がありました。 2. KPIに改善の余地がある 前述の様にクーポン推薦モデルでは配信対象者の選定、ショップ推薦、アイテム推薦という3つの観点を考えます。 各観点に対する既存のアプローチは以下の通り大部分がルールベースのロジックで構成されているため、機械学習モデルに置き換えることでKPIを改善できると考えられます。 推薦の観点 既存のアプローチ 配信対象者の選定 Matrix Factorizationとルールベース ショップ推薦 ルールベース アイテム推薦 ルールベース 3. 機械学習モデルの評価体制がない 既存システムでも配信対象者の選定で機械学習モデルが利用されていますが、その当時は現在ほどMLOpsが整備されていなかったため、機械学習モデルを評価する体制がありませんでした。モデルやデータに変更がある際に精度の変化を確認できないためモデルの改善サイクルを回しにくく、意図せず低品質なモデルをデプロイしてしまう危険性がありました。 課題解決のために 上記の課題解決のために以下の3つに取り組みました。 Vertex AI Pipelinesへの移行 Two-Stage Recommenderの導入 機械学習モデルの評価と体制整備 1. Vertex AI Pipelinesへの移行 既存のシステムはCloud Composer上で機械学習パイプラインを構築していたため、現在弊社で機械学習基盤として利用されているVertex AI Pipelinesへ移行しました。移行に際し、 2021年に公開した「Kubeflow PipelinesからVertex Pipelinesへの移行による運用コスト削減」という記事 で今後の展望として挙げられていたパイプラインテンプレートを利用しています。パイプラインテンプレートは GitHubのテンプレートリポジトリ機能 を利用し、Vertex AI Pipelinesの実行・スケジュール登録・CI/CD・実行監視等をテンプレート化したものです。以下のテックブログでもVertex AI Pipelinesの導入事例についてご紹介しているので是非ご覧ください。 techblog.zozo.com techblog.zozo.com 2. Two-Stage Recommenderの導入 KPIを改善するために、推薦分野で広く利用されている Two-Stage Recommender という推薦手法を導入します。このモデルアーキテクチャは推薦に関するコンペティションの上位解法でよく使用されており、最近だとKaggleの H&M Personalized Fashion Recommendations などで使用されていました。その名の通りCandidate GenerationとRerankingという2つのステージで構成されています。ユーザーとアイテムの全ての組み合わせに対するスコア計算が困難な場合に、ユーザーとアイテムに対するスコアの計算を一部の組み合わせに限定できます。 Two-Stage Recommenderは以下の工程で推薦を実現します。 Candidate Generationステージで、ユーザーと関連度の高いアイテムの集合を取得する Rerankingステージで、取得したアイテムをユーザーの関心度に基づいて並べ替える プロジェクトへの導入 本プロジェクトでは前述の3つの推薦の観点を以下の様に変更します。 推薦の観点 変更前のアプローチ 変更後のアプローチ 配信対象者の選定 Matrix Factorizationとルールベース Two-Stage Recommender ショップ推薦 ルールベース Two-Stage Recommender アイテム推薦 ルールベース ショップ推薦を考慮したルールベース 配信対象者の選定とショップ推薦では同一のTwo-Stage Recommenderを使用しています。配信対象者の選定ではユーザー毎にTwo-Stage Recommenderのスコアの最大値を集計し、集計値が高い上位Nユーザーを配信対象とします。ショップ推薦ではTwo-Stage Recommenderのスコアが大きい順にショップを掲載します。アイテム推薦ではショップ推薦を考慮したルールベースのロジックを使用しますが、本記事では取り上げません。 ZOZOTOWNに出店しているショップは複数のブランドを取り扱うことがあるため、同じショップ内でもブランドによってユーザーの興味は異なるという仮説をモデルに反映します。以上を踏まえて、以下では本プロジェクトのTwo-Stage Recommenderの詳細について説明します。 Candidate Generation Candidate Generationステージではユーザー、クーポン対象ショップ、取り扱いブランド、日付の組み合わせを取得します。 以下の3つの軸を考慮してCandidate Generationを行いました。 過去の実績 人気ブランド 興味を持っているブランドの類似ブランド 1. 過去の実績 リターゲティング施策はコンバージョンにつながりやすいことが分かっているため、ユーザーのお気に入りブランドと過去N日間で閲覧したアイテムのブランドを推薦候補とします。 2. 人気ブランド トレンドを考慮した推薦をするため、性年代別の人気ブランドの上位N件を取得しユーザーに紐付けます。 3. 興味を持っているブランドの類似ブランド 多様性のある推薦をするため、ユーザーが閲覧したブランドを元に以下の手順で類似ブランドを取得します。 ユーザーのブランドに対する行動を学習データとしたMatrix Factorizationモデルを学習 学習の中間生成物であるブランドのEmbeddingを取得 Embeddingから全ブランド同士のコサイン類似度を計算 ライブラリは LightFM を使用しています。指定可能なLogistic、BPR、WARPのロスを変えた時の精度を比較し、最終的には次項で説明する評価指標が最も良かった WARPロス を使用しました。 評価方法 Candidate Generationの評価は後述する 全レコード数に対するRerankingモデルの正例の割合 を使用します。使用データの日数や人気ブランドの取得件数などのパラメータは正例の割合と推薦に必要な最低ユーザー数を考慮して決定します。 Reranking RerankingステージではCandidate Generationステージで取得した組み合わせを使用して、ユーザーが興味を持つ順番でクーポン対象ショップ、取り扱いブランドを並び替えます。 学習データの作成 ユーザーのブランドへの興味を数値化しランク付けをしたいのですが、興味を直接数値化することは困難です。そこで、ユーザーのブランドへの興味を アイテム閲覧データを用いて表現 します。閲覧アイテムに紐づくブランドを取得し、アイテム閲覧データをブランド閲覧データに変換します。これをCandidate Generationで取得したデータに紐付けることでZOZOTOWN会員が次の日にあるブランドを閲覧することを正例とした学習データを作成します。ブランド推薦をメール配信以外でも展開したいという要件があったため、クーポン施策に関わるユーザー行動を正例とするのではなくZOZOTOWN全体のユーザー行動を正例としています。 アンダーサンプリング 前述の通りに学習データを作成するとCandidate Generationで取得したレコードでは正例の割合が1〜2%程度となります。 学習時間の短縮と不均衡データへの対処を目的 とし、学習と検証データに負例のアンダーサンプリングを行いました。後述するオフライン評価指標が低下しないことを確認し、最終的に正例:負例 = 1:1を採用します。 特徴量エンジニアリング 特徴量としてユーザーとアイテムのマスタデータに加えてユーザーの行動データを用います。行動データとはZOZOTOWN上での注文、検索、カートイン、お気に入り、閲覧などのデータです。特徴量は全てBigQueryを使用して作成します。 ユーザー軸 デモグラフィック属性 価格に関わる指標 ブランド軸 ユーザー行動を集計した指標 価格に関わる指標 クーポン付与ポイント Candidate Generationで使用したモデルのEmbeddingを主成分分析した第N主成分 ユーザーとブランド軸 ユーザー行動を集計した指標 価格に関わる指標 ブランドロイヤルティを表す指標 特に特徴重要度が高かった特徴量は以下です。 価格に関わる特徴量 Embeddingを主成分分析した特徴量 学習 推薦モデルの学習には LightGBM を使用し、スコアが0〜1の範囲に収まる様に二値分類をします。これはスコアが閾値以上の時に配信対象者とすることで将来的にはクーポンに興味があるユーザーのみに配信を送りたいという要件のためです。 バリデーション Time Series Hold outによって検証データを作成します。最新のN日間のデータを取得し、約20〜30%が検証データとなります。アーリーストッピングを設定し検証データに対するロスが最善の時点で学習を終了します。 推論 Dataflow を使用して並列推論を行います。実験中に推論の実行時間に課題感を感じていましたが、導入後はテストデータと推薦対象データを対象に100GB超のデータに対して約20分で推論が完了します。 3. 機械学習モデルの評価と体制整備 上記の機械学習モデルを評価するためにオフライン評価の体制を整備します。具体的にはオフライン評価と実験管理の導入について紹介します。 オフライン評価 オフライン評価は定量評価と定性評価の2軸で評価をし、特に定量評価ではメトリクスの評価と過去の施策を活用したオフライン評価をします。 メトリクスのオフライン評価 テストデータに対するモデルメトリクスが向上していることを確認すると同時にvalidation lossやユーザーユニーク数などのメトリクスが異常ではないかを確認します。具体的には以下のメトリクスを確認します。 テストデータに対するprecision@k テストデータに対するrecall@k train loss validation loss 学習/推薦ユーザーのユニーク数 学習/推薦ブランドのユニーク数 過去の施策を活用したオフライン評価 過去の施策によって変更前、変更後のモデルに関わらないメール配信者やメール経由サイト流入者のデータが手に入っていました。このデータを活用することで以下の指標を擬似的に再現し、変更前のモデルと変更後のモデルを評価します。 N万位以内のユーザーの配信流入率 メール経由流入者がN万位以内である割合 定性評価 定量評価に加えて、定性評価をチームメンバーに行なってもらいます。具体的には各自のZOZOTOWNアカウントを用いて推薦結果に対するフィードバックをしてもらいます。以下の様なフィードバックをもらい、モデル改善の方向性を決定する際の参考にしました。 変更前の推薦は人気ブランドに偏っているが変更後の推薦はブランドのバリエーションが増えていて良い ライトユーザーは変更前の推薦を好むが、ミドル/ヘビーユーザーは変更後の推薦を好みそう 変更後の推薦は未知のブランドが多いが、その中に興味を持っているブランドがある時はかなり嬉しい 実験管理の導入 変更前の機械学習モデルは評価指標を保存する場所がなく他のモデルとの比較ができませんでした。対策として、オフライン評価指標の保存と参照を簡易化するために実験管理ツールである MLflow を導入します。パラメータはパイプラインの実行時に保存され、メトリクスは学習や評価の際にMLflowへ保存します。保存したデータは以下の画像のように確認できます。メトリクスにはダミーの値を入れています。 ▼ パラメーター ▼ メトリクス ▼ メトリクスの推移 結果 運用業務の簡易化 今回のシステム移行によってVertex AI Pipelinesを利用した他システムと同様の方法で 簡単に運用業務が行える ようになりました。また、Cloud Composerを使用した機械学習モデルの運用が全社的に終了し Vertex AI Pipelinesへと完全移行 できました。 KPIの改善 配信対象者の変更とショップ推薦の変更に対して実施したA/Bテストではcontrolと比較して KPIが以下の様に改善 しました。絶対数を表す指標は比率を、割合を表す指標は差を示しています。 KPI 配信対象者の変更 ショップ推薦変更 売上 124.69 % 104.02 % 1配信あたりの売上 123.81 % 104.40 % 注文者数 120.08 % 103.76 % 配信流入率 + 0.12 pt + 0.01 pt 配信注文率 + 0.004 pt + 0.001 pt リリースサイクルの改善 MLflowの導入によってモデルの精度比較が簡単にできるようになったため、 モデル改善のサイクルが早まりました 。また、モデルに関わらない変更をリリースする際も精度が低下していないことを確認できるため リリースの安全性が向上しました 。 今後の展望 バイアスの考慮 現在はアイテムを閲覧したブランドを学習データとしてショップ推薦をしています。このデータは推薦経由であっても記録されるため推薦モデルが自己の学習データに影響を与えることになります。推薦したブランドは再び推薦されやすくなるというバイアスを生み出しているため、このバイアスを考慮した設計をする必要があります。 評価体制の改善 評価指標の探求 現在の評価指標は推薦の精度に着目したものが多いです。一方で推薦には多様性、新規性、意外性等の精度以外にも考慮すべき指標が提案されています。弊社でも長期的な推薦の価値を捉えた指標を明らかにし、モニタリングする必要があると考えています。 定性評価の体制整備 今回のプロジェクトでは定量評価の体制を整備しましたが、定性評価がモデル改善の方針の参考になることが多かったため、定性評価の方法も標準化することでモデル改善を加速させたいと考えています。 おわりに 本記事ではクーポン推薦モデルとシステムの改善について紹介しました。現在ZOZOでは 「ワクワクできる『似合う』を届ける」 という経営戦略のもとパーソナライズを強化している最中です。推薦を含め機械学習に関わるエンジニアを募集しているので、ご興味がある方は是非以下のリンクからご応募ください。 hrmos.co hrmos.co
アバター
はじめに こんにちは。DevRelブロックの @wiroha です。1月23日に「 ZOZO Tech Meetup - Android 」を開催しました。ZOZOのモバイルアプリエンジニアがAndroidの開発話をするオンラインイベントです。 登壇内容まとめ 弊社から次の5名が登壇しました。 コンテンツ 登壇者 CoroutineExceptionHandlerと仲良くなる 愛川功樹 Compose Multiplatform for iOS開発でぶつかった壁 井上晃平( @nemo-855 ) Gradle超入門 財部彰太( @rabe_hamuyatti ) Jetpack Composeで様々な画面サイズに対応する 田中崇裕 Jetpack ComposeへのリファクタリングのTIPS: CustomViewの便利な使い方 山田尚吾( @yshogo87 ) 当日の発表はYouTubeのアーカイブでご覧ください。 www.youtube.com CoroutineExceptionHandlerと仲良くなる speakerdeck.com 愛川からはCoroutineExceptionHandlerについて紹介しました。Coroutinesの例外伝播やCoroutineExceptionHandlerの実体・呼ばれるまでの流れなどを説明しており、詳しい理解を助ける内容でした。Coroutineの実装を見ていくことで、普段コードを書く上でも内部のイメージをしやすくなったとのことです。 Compose Multiplatform for iOS開発でぶつかった壁 speakerdeck.com 井上はCompose Multiplatform for iOSを用いて、個人でiOSアプリとAndroidアプリを作成したそうです。開発する中でぶつかった課題とその解決法を紹介しました。TextField、Context、権限リクエストといったOS間の差異を吸収する工夫はとても良い知見だと感じました。 Gradle超入門 speakerdeck.com 財部からはGradle入門を発表しました。GradleはAndroid開発を行う上でほぼ必ず扱うことになるので、改めて理解を深める良い機会だと感じました。応用すると依存関係の問題の回避や、ビルド時間の短縮にも結びつくので、継続して学んでいきたいですね。 Jetpack Composeで様々な画面サイズに対応する speakerdeck.com 田中からは、さまざまな画面サイズへ対応するハードルを下げるべく、理解を深めていくための発表を行いました。ナビゲーションとペインの出し分けを実装することで、さまざまなサイズに対応します。具体的なコードが多く出てきて、対応時のイメージがわきやすくなっていました。 Jetpack ComposeへのリファクタリングのTIPS: CustomViewの便利な使い方 speakerdeck.com 山田からはリファクタリングを進めるためのTIPSを発表しました。複雑なレイアウトをいきなりJetpack Composeへリファクタリングするのではなく、少しずつ手順を踏みます。まずはレイアウトを分割し、分割したレイアウトのCustomViewを作り、CustomViewの中だけをJetpack Composeにリファクタリングします。これにより差分が小さくなるため細切れの時間を活用でき、不具合に気づきやすくなるメリットもあるとのことでした。 最後に 今回はAndroidに関するさまざまな知見を発表しました。みなさまご参加ありがとうございました。各発表の質疑応答の時間で多数の質問をいただきましたので、ぜひアーカイブをご覧ください。 ZOZOではAndroidを活用し、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は以下のリンクからぜひご応募ください。 hrmos.co
アバター
はじめに こんにちは、ZOZOTOWN開発2部Androidブロックの大江です。普段はZOZOTOWN Androidの開発を担当しています。 ZOZOTOWN Androidはリリースから10年以上経過し、現在のソースコードは9年近く開発されています。そのため、複数のアーキテクチャが混ざった状態になっていて、開発速度の向上を妨げる要因になっていました。 そこで今ZOZOTOWNにある3つのAndroidブロックから、それぞれテックリードなどの代表者を立て、アーキテクチャを統一するための座談会を週に1度開催しています。その座談会の成果としてまずはViewModel部分の実装方針を決めることができました。 本記事では座談会で決められたViewModelの実装方針と狙いをご紹介します。 目次 はじめに 目次 ViewModelの実装方針を決めるための要件 サンプルの説明 ViewModelの実装方針の紹介 UI StateとEffect Event ViewModelに実行してほしい処理の内容を表すようにEventを定義する案 UIの操作をそのまま表すようにEventを定義する案 Android ViewからJetpack Composeへの移行 まとめ ViewModelの実装方針を決めるための要件 ViewModelの詳細な実装方針を検討するにあたって、まずは現状のZOZOTOWN Androidが抱えている課題を整理し、それらを解決するための要件を決定しました。 具体的には以下の要件に基づいてViewModelの実装方針を決めることにしました。 UIの実装をシンプルにする 一般的にUIのテストを実装するコストは大きく、ZOZOTOWN AndroidでもUIのテストは十分に実装できていない UIの実装をシンプルにすることでテストが実装でき、それによって実装ミスを減らしたい 機能やレイアウトの改修を簡単に行える ZOZOTOWN Androidは大人数かつ複数のチームが同じ機能や画面を改修することがある その際のコンフリクトの発生や改修自体のコストを減らしたい UIのFrameworkに依存しない ZOZOTOWN AndroidはAndroid ViewとJetpack Composeが混在している Android ViewをJetpack Composeに置き換えていく予定だが、その際にViewModelは変更しなくて済むようにしたい サンプルの説明 今回のViewModelの実装方針を紹介するため、サンプルのお知らせ一覧画面を用意しました。 お知らせをタップしたらお知らせ詳細画面に遷移または外部ブラウザを起動するような仕様を想定します。 ViewModelの実装方針の紹介 ViewModelとUI双方向の処理の流れを明確にして、ViewModelの実装方針を決めました。 ViewModelからUIに向かう処理はUI StateとEffectとして実装する UIからViewModelに向かう処理はEventとして実装する それぞれについて解説します。 UI StateとEffect UI StateはUIに表示する内容を1つにまとめた値を表すdata classであり、StateFlowで管理します。 この実装は Android Developersのアプリ アーキテクチャ ガイドのUI レイヤのページ で紹介されている内容に則っています。 しかし、アプリ アーキテクチャ ガイドで紹介されている画面遷移などのイベントをUI Stateで管理するという部分は採用しませんでした。 ZOZOTOWN Androidでは画面遷移などのイベントはUI Stateとは別にEffectというsealed classを定義してSharedFlowで管理する方針を採用しました。 画面遷移などのイベントはSharedFlowで管理した方がシンプルな実装になるためです。 例えば、次のような場合にStateFlowを用いると値をクリアする必要がありますが、SharedFlowを用いると値をクリアする必要はありません。 同じイベントを連続で実行したい場合 画面の再生成時にイベントを再発火させたくない場合 お知らせの一覧を表示する画面のAndroid Viewを使ったサンプルコードを載せておきます。 data class InformationListUiState( val informationList: List <Information> ) sealed class InformationListEffect { data object ShowInformationDetail : InformationListEffect() data object OpenExternalBrowser : InformationListEffect() } class InformationListViewModel : ViewModel() { private val _uiState = MutableStateFlow(InformationListUiState(emptyList())) private val _effect = MutableSharedFlow<InformationListEffect>() val uiState = _uiState.asStateFlow() val effect = _effect.asSharedFlow() } class InformationListFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.uiState.collect { uiState -> // Adapterを更新してRecyclerViewでお知らせの一覧を表示する adapter.update(uiState.informationList) } } } viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.effect.collect { effect -> when (effect) { InformationListEffect.ShowInformationDetail -> { // 詳細画面に遷移する } InformationListEffect.OpenExternalBrowser -> { // 外部ブラウザを起動する } } } } } } } Event EventはUIの操作をViewModelに伝えるためのinterfaceです。 ViewModelにはEventを受け取るための dispatch() を定義します。 EventにはViewModelの拡張関数として consume() を定義して、 dispatch() の内部では consume() を実行するだけにしました。 class InformationListViewModel : ViewModel() { fun dispatch(event: Event) { event.run { this @InformationListViewModel.consume() } } interface Event { fun InformationListViewModel.consume() } data object Event1 : Event { override fun InformationListViewModel.consume() { // Event1がdispatchされたときに実行したい処理 } } data object Event2 : Event { override fun InformationListViewModel.consume() { // Event2がdispatchされたときに実行したい処理 } } } このような実装にすることでレイアウトや機能を改修する際には、そのレイアウトや機能に対応するEventのみを変更すればよく、他のEventに干渉することなく安全に作業できます。 次にEventの定義の粒度について説明します。 Eventをどのような粒度で定義するかについて、次の2つの案が挙がりました。 ViewModelに実行してほしい処理の内容を表す UIの操作をそのまま表す この2つの案について検討した結果、UIの実装をシンプルにするという観点からUIの操作をそのまま表す案を採用することにしました。 ViewModelに実行してほしい処理の内容を表すようにEventを定義する案 class InformationListViewModel : ViewModel() { fun dispatch(event: Event) { event.run { this @InformationListViewModel.consume() } } interface Event { fun InformationListViewModel.consume() } data class ShowInformationDetail( val informationID: InformationID) : Event { override fun InformationListViewModel.consume() { // お知らせ詳細画面に遷移する viewModelScope.launch { _effect.emit(InformationListEffect.ShowInformationDetail) } } } data class OpenBrowser( val url: String ) : Event { override fun InformationListViewModel.consume() { // 外部ブラウザを起動する viewModelScope.launch { _effect.emit(InformationListEffect.OpenExternalBrowser) } } } } class InformationListFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { adapter.setOnItemClickListener { index -> val information = _uiState.value.informationList[index] if (information.isExternalLink) { viewModel.dispatch(InformationListViewModel.OpenBrowser(information.url)) } else { viewModel.dispatch(InformationListViewModel.ShowInformationDetail(information.informationID)) } } } } UIの操作をそのまま表すようにEventを定義する案 class InformationListViewModel : ViewModel() { fun dispatch(event: Event) { event.run { this @InformationListViewModel.consume() } } interface Event { fun InformationListViewModel.consume() } data class OnItemClick( val index: Int ) : Event { override fun InformationListViewModel.consume() { val information = _uiState.value.informationList[index] if (information.isExternalLink) { // 外部ブラウザを起動する viewModelScope.launch { _effect.emit(InformationListEffect.OpenExternalBrowser) } } else { // お知らせ詳細画面に遷移する viewModelScope.launch { _effect.emit(InformationListEffect.ShowInformationDetail) } } } } } class InformationListFragment : Fragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { adapter.setOnItemClickListener { index -> viewModel.dispatch(InformationListViewModel.OnItemClick(index)) } } } それぞれの案の実装を比較してもらうと後者のUIの操作をそのまま表す案の方がFragmentの実装がシンプルになっていることがわかると思います。 アプリ アーキテクチャ ガイドではViewModelのfunの名前が前者の案であるViewModelに実行してほしい処理の内容を表しています。しかしZOZOTOWN AndroidではUIの実装をシンプルにするため、後者のUIの操作をそのまま表す案を採用しました。 Android ViewからJetpack Composeへの移行 Android ViewからJetpack Composeへ移行する場合は以下のようにUI部分を変更するだけです。 以下の理由でViewModelの実装を変更する必要はありません。 UI State、Effect、EventがAndroid ViewやJetpack Composeに関連するクラスを使用していない UI State、Effect、EventがAndroid ViewやJetpack Composeを連想させる命名になっていない @Composable fun InformationListScreen( viewModel: InformationListViewModel, ) { val uiState by viewModel.uiState.collectAsState() val lifecycleOwner = LocalLifecycleOwner.current LaunchedEffect(viewModel, lifecycleOwner) { viewModel.effect .flowWithLifecycle(lifecycleOwner.lifecycle) .onEach { effect -> when (effect) { InformationListEffect.ShowInformationDetail -> { // 詳細画面に遷移する } InformationListEffect.OpenExternalBrowser -> { // 外部ブラウザを起動する } } } .launchIn( this ) } InformationListContent( uiState = uiState, dispatch = viewModel :: dispatch, ) } @Composable fun InformationListContent( uiState: InformationListUiState, dispatch: (InformationListViewModel.Event) -> Unit , ) { // LazyColumnでお知らせの一覧を表示する LazyColumn { itemsIndexed(uiState.informationList) { index, information -> // お知らせの項目を表示する InformationItem( information = information, onClick = { dispatch(InformationListViewModel.OnItemClick(index)) } ) } } } まとめ 本記事ではZOZOTOWN Androidで採用することになったViewModelの実装方針を紹介しました。このViewModelの実装方針をチーム全体に浸透させることで開発速度の向上を期待しています。AndroidでViewModelの実装方針を検討している方がいれば、ぜひ参考にしてみてください。今後はUseCaseやRepositoryについても実装方針を決めていきたいと考えています。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
こんにちは、ZOZOTOWN開発1部iOSブロックの荻野です( @juginon )。 WWDC19でSwiftUIが発表されてから今年で5年になりますが、みなさんの携わっているプロジェクトではSwiftUIを使っていますか。ZOZOTOWN iOSチームでは、2023年11月にリリースしたアイテムレビュー機能 1 へ向けSwiftUIを積極的に導入する方針を定め、新規画面及びUI要素ではSwiftUIでの実装に取り組みました。 巨大なプロジェクトであるZOZOTOWN iOSにSwiftUIをどのように導入していったのか? 本記事では、実際にプロダクトコードに導入したことで見えた問題点、そこで得た知見を踏まえた上でのSwiftUIの導入方針についてご紹介します。 ZOZOTOWN iOSへのSwiftUI導入の背景 旧方針:限定的な範囲で小さく導入する UIViewで書いている部分はSwiftUIで実装する SwiftUIの標準APIで実現できないことがあるケースではUIKitを使用する 実際に導入した結果うまくいかなかった どう解決したか 新方針:小ささの粒度を柔軟に変えながら導入する まとめ ZOZOTOWN iOSへのSwiftUI導入の背景 ZOZOTOWN iOSへSwiftUIを導入した主な目的は、チーム全体でSwiftUIへの知見を増やすためです。今までもZOZOTOWN iOSにSwiftUIを導入した前例はありましたが、メンバー個人での挑戦に留まっていました。取り組んだメンバー同士で振り返ってみて、巨大で複雑なZOZOTOWN iOSのコードの上でSwiftUIの知見を増やすには、メンバー個人ではなくメンバー全員で取り組んだ方が良いと判断しました。そこで、ZOZOTOWN iOSにSwiftUIを導入するチームとしての方針を決めることにしました。SwiftUIへの挑戦や実際に発生した問題の共有をチームメンバー全員で取り組むことで、チームとして実践的な知見を増やせると期待しました。 また副次的な効果として、Previews in Xcodeを積極的に使うことでデザイン仕様の実装漏れを防止する期待もあります。機能要件の複雑さゆえにデザイン仕様の見落としが少なからずあるので、デザイン仕様から考えられる複数のユースケースをPreviewsで確認しながら実装できると仕様の見落としに気づきやすくなるでしょう。 これらの背景から、ZOZOTOWN iOSではSwiftUIの導入が検討されるようになりました。まずは導入方針を決めるにあたってどのような意思決定がされたのかについてご紹介します。 旧方針:限定的な範囲で小さく導入する 導入の方針を決める際は、画面単位で一気にSwiftUIを導入していくのか、コンポーネントにわけて小さく導入していくのか、大きく分けて2つの選択肢を考えました。チームの議論では以下のような意見が出ました。 一気に画面全体をSwiftUI化することはコードの規模や時間的に難しい ZOZOTOWN iOSでは複数の案件が同時並行で走っており、影響範囲が大きい改修を入れるのは現実的でない。 もし新規で画面を作る場合でも、問題が発生した際にUIKitの実装へフォールバックさせる判断をしたときのダメージが大きい。 ユーザーに価値を届けることを最優先にするが、技術的な挑戦もないがしろにするべきではない まずは小さく始めて導入に際しての懸念点を探し、チーム内で知見を増やすべき。 上記のように、最初は限定的な範囲で小さくSwiftUIを導入し、徐々にその適用範囲を広げていくことでチームのSwiftUIに対する理解と経験を速いサイクルで段階的に深めていこう、と決まりました。 具体的には次のアクションに沿ってSwiftUIをチームで導入することにしました。 UIViewで書いている部分はSwiftUIで実装する UIKitで実装することを考えたとき、UIViewとして書くであろう部分はSwiftUIのViewで実装します。具体的には以下のような例が考えられます。 UIViewController内で表示するビューをSwiftUIのViewで実装する UIViewController内で表示するビューはUIViewのままで、ビューを構成する各コンポーネントをSwiftUIのViewで実装する UIViewControllerの中でSwiftUIのViewを表示するときは UIHostingController を使用します。親のUIViewControllerにSwiftUIのViewを持つUIViewControllerを追加する形で表示します。 とはいえ、SwiftUIの標準APIだけでは実現できないケースもありえることはチームでも話に上がっていたため、次のアクションも合わせて決めました。 SwiftUIの標準APIで実現できないことがあるケースではUIKitを使用する 例として、リスト表示におけるSwiftUIとUIKitの実装を考えます。ZOZOTOWN iOSのUIデザインはSwiftUIのGridでは実装が難しく、UICollectionViewでないと表現しづらい複雑なレイアウトやデザインになっていることもあります。 この例に限らず、現状ZOZOTOWN iOSにおける様々なデザイン仕様に対してSwiftUIの標準APIだけで実現するには難しい状況です。 そのため、SwiftUIだけでは実現できない画面やコンポーネントに関してはUIViewを利用します。 実際に導入した結果うまくいかなかった アイテムレビュー機能では、上記の方針をもとに実際にいくつかの画面でSwiftUIのビューを作成しました。 最初に、新規作成されたレビュー詳細画面におけるSwiftUIでの実装予定と最終的なリリース時点での実装を比較してみましょう。図中の赤枠部分がSwiftUIでの実装です。 実装予定では4つのコンポーネントをSwiftUIで実装する計画を立てていましたが、実際に計画通り実装できたのは上2つの赤枠のコンポーネントのみとなりました。 実装予定 リリース時点での実装 計画通りにSwiftUIで実装できなかったコンポーネントは次の3つです。1つ目はユーザーの投稿を表示しているセル、2つ目はユーザーのリアクションを表示するビュー、3つ目は星評価のビューです。 技術的な課題の複雑さとプロジェクトのスケジュールの両方を考慮し、1つ目と2つ目についてはやむを得ずUIKitの実装へと戻しました。3つ目の星評価のビューについては不具合の発生条件があったため、原因を調査して対応した結果SwiftUIのまま実装できました。 この星評価のビューの対応について詳しくご紹介します。星評価のビューは以下の図の赤枠で使用されています。 赤枠のうち上から2つのビューは正しく表示されましたが、一番下の赤枠のみ星が表示されなくなる不具合を発見しました。 なぜ他の部分ではうまく表示できてこの箇所のみうまく表示できないのか、詳しく原因を調査することにしました。 左図: レビューセルの構成 右図: 不具合のイメージ この画面は UICollectionView がベースとなっており、そこに各セクションのビューを表示しています。 アイテムレビューのユーザー投稿は UICollectionViewCell で実装されています。セルの中で表示している星評価はSwiftUIのViewで作成したため、 UIHostingController を使用して表示していました。 このとき、以下の特定の状況において星評価のビュー自体が表示されませんでした。 レビューの投稿一覧のうち、上から2番目のレビューの星のみが表示されない iOS 14系、 iOS 16系では再現せず、iOS 15系でのみ再現が確認できる ファーストビューに表示されるセルの位置関係によって表示されない場合がある どう解決したか 不具合が起きているビューの特徴として、 UICollectionViewCell の内部で表示していることに着目しました。 同様の問題について言及している Apple Developer Forums を確認しました。公式の回答によると、 UIHostingController を UICollectionViewCell の内部での使用はサポートされてないようです。 Embedding a UIHostingController inside of cells is not officially supported, and you may run into various issues doing this. 本来であればこの段階でUIKitでの実装へ戻すアクションを取るべきです。 しかし、チーム内で調査を進めていった結果、今回の問題は safeAreaInsets がSwiftUIによって意図せず設定され、レイアウト崩れが発生していることが原因であるとわかりました。 ビュー自体は正常に追加されていたものの、 safeAreaInsets が設定されていたことで星を描画するサイズを確保できていなかったようです。 safeAreaInsets をゼロにすることで解決できました。 表示されない場合 表示される場合 iOS 16.4以降の場合、 UIHostingController の safeAreaRegions 2 を remove(.all) することでこの問題を解決できました。しかし、アイテムレビューリリース時のZOZOTOWN iOSはiOS 14以降をサポート対象OSとしていたため、iOS 16.4未満でも safeAreaInsets をゼロにする必要がありました。 iOS 16.4未満の場合には、独自で作成した SafeAreaRemovedHostingController を使用しました。 SafeAreaRemovedHostingController は safeAreaInsets をゼロにしたUIViewを持つ UIHostingController です。 import SwiftUI import UIKit final class SafeAreaRemovedHostingController < View : SwiftUI.View >: UIHostingController < View > { private class WrapperView : UIView { override var safeAreaInsets : UIEdgeInsets { .zero } } private let wrapperView = WrapperView() override func loadView () { super .loadView() view.backgroundColor = .clear view.translatesAutoresizingMaskIntoConstraints = false wrapperView.addSubview(view) NSLayoutConstraint.activate([ wrapperView.leadingAnchor.constraint(equalTo : view.leadingAnchor ), wrapperView.trailingAnchor.constraint(equalTo : view.trailingAnchor ), wrapperView.topAnchor.constraint(equalTo : view.topAnchor ), wrapperView.bottomAnchor.constraint(equalTo : view.bottomAnchor ) ]) view = wrapperView } } SafeAreaRemovedHostingController を用いて以下のようにOSバージョンによって出し分けました。これによってZOZOTOWN iOSでサポートしているすべてのOSバージョンで星評価のビューを問題なく表示できました。 let starView : UIView = { if #available(iOS 16.4 , * ) { let hostingController = UIHostingController(rootView : starRatingView ) hostingController.safeAreaRegions.remove(.all) return hostingController.view } else { return SafeAreaRemovedHostingController(rootView : starRatingView ).view } }() UICollectionView や UITableView の中でSwiftUIを表示する仕組みとしては、WWDC22で UIHostingConfiguration が紹介されています 3 。 UIHostingConfiguration の導入はiOS 16から可能となり、より簡潔に不具合なく実装できることが期待されます。ZOZOTOWN iOSのサポートバージョンがiOS 16以上へと上がったタイミングで上記の出し分けは消え、 UIHostingConfiguration へと移行する予定です。 新方針:小ささの粒度を柔軟に変えながら導入する 小さく導入するという旧方針に従って実際にSwiftUIで実装した結果、場合によって小ささの粒度を柔軟に変える必要がありました。これを踏まえて、ZOZOTOWN iOSにおけるSwiftUI導入のアクションは以下のように変更しました。 UIViewのサブビューにコンポーネントが複数ある場合、コンポーネントはSwiftUIかUIKitのどちらかに統一する UIHostingConfiguration を使えるようになるまでは UICollectionViewCell や UITableViewCell でSwiftUIを使わない SwiftUIで実装する際には、UIKitの実装へと戻す可能性も考えたリソース配分・スケジューリングを行う すでに上記のアクションを基に画面単位でSwiftUIへのリファクタリングをおこなった部分もあり、これからもこの方針でSwiftUIの導入を進めていきます。 まとめ 本記事ではZOZOTOWN iOSにSwiftUIを小さく導入するに至った経緯、実際に導入したことで見えてきた問題点、そこで得た知見を踏まえた上でのSwiftUIの導入方針をご紹介しました。 本記事で具体例として挙げたような問題は他にも存在する可能性がありますが、チームとしては都度調査し、SwiftUIとの共存を目指しています。今回のアイテムレビュー機能で小さく始めた経験は、チームにとって重要な知見を得る機会となりました。 個人開発なら時間をかけさえすれば解決できるような問題も、実際に仕事として進めていく中で直面すると臨機応変に対応しなければなりません。この経験はチームの文化としてSwiftUIを導入しようとしなければわからなかったことで、個人的には貴重な経験でした。 そういった短い時間での意思決定と技術的挑戦のバランス感を今後も養っていき、よりお客様に価値を提供できるといいなと思っています。 ZOZOTOWN iOSチームは今回の例に限らず、技術導入に積極的に挑戦しつつも、常にお客様に価値を提供することを最優先に考えています。 ZOZOTOWN iOSチームでは、そのような価値観を持っている方と一緒にサービスを作り上げたいと思っています。ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co https://corp.zozo.com/news/20231130-zozotown-itemreview/ ↩ https://developer.apple.com/documentation/swiftui/safearearegions ↩ https://developer.apple.com/videos/play/wwdc2022/10072/ ↩
アバター
こんにちは、XR × Fashion TechやXR × Beauty Techといった領域を推進している創造開発ブロックの @ikkou です。 2024年1月9日から12日の4日間にかけてラスベガスで開催された「CES 2024」に一般参加者として現地参加してきました。私個人としては通算5度目、ZOZO所属としては2020年、2023年に続き3度目の参加となります。 techblog.zozo.com techblog.zozo.com 前半はCESの概要と関連する情報、後半は私が注目したXR TechとFashion Tech、そしてBeauty Techについてお伝えします。CES 2024全体のトレンドについては、CES Daily Show Day 1 ・ Day 2 ・ Day 3 などをご覧ください。 CESとは 値上がりした参加費 会場の概要 Official Show Store 会場間の移動 徒歩 Vegas Loop Lyft Las Vegas Monorail XR Tech XR関連企業の出展動向 XRデバイスの動向 MetaのRay-BanスマートグラスとAmazonのEcho Frames CES 2024にあわせた発表 Apple Vision Pro XREAL Air 2 Ultra SonyのVR HMDを含む没入型空間コンテンツ制作システム ShiftallのMeganeX superlight Fashion TechとBeauty Tech Beauty Tech領域で強い存在感を示していたL'Oréal Group CES初出展の資生堂 キヤノンのスキャンソリューション おわりに CESとは 毎年Venetian Expoの2Fから1Fへ降りるエスカレーター付近に設置されているCESの吊り看板 CES はCTA(Consumer Technology Association)が主催する、毎年1月にラスベガスで開催される世界最大級と言える「テクノロジーのショーケース」です。 読み方は「せす」と呼ぶ方もいますが、正しくは「しーいーえす」です。また、CESは長らく「Consumer Electronics Show」という名称でしたが、現在この「Consumer Electronics Show」という表記は非推奨となっています。 CTAの100th Anniversaryを記念するモニュメント CESを主催しているCTAは、1924年にラジオ工業会として設立され、2024年は記念すべき100th Anniversaryの年でした。 CESには毎年テーマがあり、 2024/01/05に発表されたCTAのプレスリリース によると、今年のトップテーマは次の4つでした。 人工知能 / Artificial Intelligence 万人のための人間の安全保障 / Human Security for All モビリティー / Mobility 持続可能性 / Sustainability 毎年Venetian Expoの2Fに用意されている大きな看板 会場内の至るところに掲げられていた「ALL ON」の文字は「The Future is ALL ON」を意味し、「未来はすべてここにある」というCTAの強い意志を感じました。 INNOVATION AWARD SHOWCASEのエントランス 時勢もあり特に強く「AI推し」の側面が感じられ、会場の様々な場所で掲げられている各種ビジュアルも生成AIを利用して制作されているようでした。 ここ数年、CESは新型コロナウィルス感染症の影響を受け出展社数が減っていました。CES 2020の出展社数が4,400社以上だったところ、オフラインが完全復活を遂げた去年のCES 2023の出展社数が3,200社以上でした。しかし、 公式発表によれば今年のCES 2024の出展社数は4,300以上 と、概ねコロナ禍以前の水準に戻りつつあるように感じました。 値上がりした参加費 LVCC West Hallに設置されている#CES 2024のモニュメント CES 2024の参加登録は現地時刻2023/09/12にオープンし、チケットの価格は同12/05までの早期登録が$149(約¥22,000)、以降は$350(約¥52,000)でした。私は「貴重なCESの卒業生(英語表記は“valued CES alum”)」向けの特典によりRegistrationは$0で済みました。 ちなみに、CES 2023以前は早期登録が$100、通常登録が$300だったので、CES 2024から$50ほど値上がりしていることになります。USは何かと高騰が進んでいるので、これは仕方がないことかもしれません。 今回はCES参加5度目にして初めて公式のカンファレンスプログラムである「 AR/VR/XR 」を追加しました。オンサイトでXR領域に関する3つのセッションに参加できて$400(約¥60,000)でした。 このカンファレンスプログラムは参加費も含まれるため、特典を持っていない方は実質$50で3つのセッションに参加できるということです。 会場の概要 Image Source. https://www.ces.tech/exhibits/maps-and-locations.aspx CESの展示会場は例年通りTech East・Tech West・Tech Southという3つのエリアに大別されています。 一般の来場者向けの会期は4日間ありますが、1人で全ての会場・全てのブースを巡るのはあまりにも非現実的です。 XRエリアに近いCentral Hallの入口、初日の開場直前は多くの人が集まる。 私は例年、Tech EastのLas Vegas Convention(LVCC)のWest・North・Central HallとTech WestのVenetian Expoを中心に巡っています。LVCCにはSouth Hallもありますが、昨年に引き続き今年も改装中でした。 4日間ともその日の最初から最後までとにかく歩き回りました。見るべきものを絞って効率的に巡っているとはいえ、期間中の平均歩数は1日あたり25,000歩を超えていました。普段リモートワークであまり歩かない私にとってはかなりの運動量です。 Official Show Store オフィシャルCESストア 過去参加したCESでは見かけていませんでしたが、CES 2024ではCES公式のオフィシャルストアが会場内に5ヵ所設置されていました。CESの公式ウェブサイトには “We’re thrilled to bring an official store back to CES”と記載されている ので、過去にもあったのかもしれません。 CES 2024 Official Show Storeで購入したフーディー 私はフーディーを1着購入しましたが、このフーディーの図柄も生成AIを利用して制作されたもののように感じました。 会場間の移動 とにかく会場間の移動には時間がかかります。今回、会場間の移動には徒歩、Vegas Loop、Lyft、そしてLas Vegas Monorailを利用しました。 この他の選択肢として、会場間をつなぐ無料のシャトルバスや、有料の循環バス「DEUCE」、そして通常のタクシーなどが挙げられます。 徒歩 徒歩ルートを選ぶとVenetian Expoを出てすぐの歩道橋で話題のSphereを間近で見られる。 LVCCとVenetian Expoの間は何度か徒歩で移動しました。片道で30分程度かかります。 ライドシェアやシャトルバスの移動は乗車までに時間がかかることや、渋滞に巻き込まれることもあるため、実は徒歩の方が早いこともあります。日中であれば大通りの治安は悪くないので、時間と体力に余裕がある場合は徒歩での移動もおすすめです。 Vegas Loop Vegas LoopのLVCC WEST STATION Vegas Loop はイーロン・マスク氏が立ち上げたThe Boring Company社によるラスベガスの地下交通網です。ラスベガスの空港から北部のダウンタウンまで結ぶ計画が当局に承認されていますが、現在はLVCCのWest Hall・Central Hall・South Hallを結ぶ無料の路線(LVCC Loop)と、RESORT WORLDとLVCCを結ぶ有料の路線のみが運用されています。 Vegas LoopのRESORT WORLD路線の発着場 後者の有料の路線は、開通当時は1日乗り放題で$2.5でしたが、CES 2023時は1日乗り放題で$4.5に、そしてCES 2024では$5(約¥740)に値上がりしていました。ただし、この有料の路線はRESORT WORLD近辺の宿泊者以外がそう頻繁に利用することはないでしょう。私は確認するために1度だけ乗車しました。 CESの参加者はもっぱらLVCCの会場間を繋ぐ無料のLVCC Loopを利用します。LVCCのWest HallとCentral Hall間を徒歩で移動する場合は20分間程度かかりますが、LVCC Loopであればわずか2分間で移動でき、とても便利です。 Vegas Loopを走るファルコンウイングドアが特徴的なModel X 昨年同様、今年も何度か利用しました。車両はTeslaで統一され、車種はModel Yが多く、稀にファルコンウイングドアが特徴的なModel Xも走っていました。相乗りが前提になっていて、どの車両に乗れるかは運次第です。今回は運良く何度かModel Xに乗車できました。 The Boring Company社の公式Xによると、 将来的にはCybertruckが走る ようで、とても楽しみです。 Lyft ホテルと離れた会場間の移動にはライドシェアのLyftを多用しました。過去のCESではAptivと連携した「自動運転」のLyftが走っていましたが、今回は見かけませんでした。 ライドシェアの価格は変動相場制になっているので、同じ行き先でも時間帯によって価格が変わります。より安価に済ませたい場合はLyftとUberで比較することをおすすめします。 注意点として、まずライドシェアの乗り場は決まっています。乗り場から離れた場所にいる場合は移動しなければなりません。また、その日の閉場時刻になると非常に多くの人が乗ろうとするため混雑します。私はライドシェア乗り場で車両を手配してから40分待った末、車両が到着しなかったため結局徒歩で移動する羽目になりました。 Las Vegas Monorail LVCCとVenetianの移動に一度だけ Las Vegas Monorail を利用しました。区間距離は関係なく片道で$6(約¥890)です。1DAY PASSから7DAY PASSまで用意されているので、駅周辺に宿泊しているなどCESの期間中の移動をすべてLas Vegas Monorailで済ませる場合は$39(約¥5,800)の4-Day Passがおすすめです。なお、紙の切符ではなく、eTicketの場合は割引があります。 ライドシェアをはじめとする道路を走る交通手段と比較すると、目的地によっては駅を降りた後の徒歩移動が必要となりますが、渋滞に巻き込まれることもなく、安定した時間で移動できます。 XR Tech CESのProduct Categoriesに「AR/VR/XR」が存在するように、例年多くのXR関連企業が出展、あるいはCESにあわせて発表しています。また、HTCのように近くのホテルで独自に展示している企業もあります。 XR関連企業の出展動向 Image source. https://exhibitors.ces.tech/8_0/floorplan/?hallID=B LVCC Central Hallには「GAMING | METAVERSE | XR」とカテゴライズされた一画があります。CES 2020当時は「AR/VR & Gaming」でしたが、今年は昨年のCES 2023から変わらずこのカテゴリー名になっています。ただ、個人の感覚として、2024年においてもメタバースムーブメントが去年同様盛り上がっているかどうかについて疑義があります。実際のところこのエリアにメタバース推しのブースはほとんどありませんでした。 Venetian Expoの「Gaming/XR」エリアの地図 また、Venetian Expoのスタートアップ企業が集まるEureka Parkにも「Gaming/XR」エリアが設けられていましたが、7ブースしか出展していませんでした。参考としてCES 2023では「Gaming/Metaverse/XR」エリアとして16社が出展していました。2023年から2024年の変化としてはエリアとして「Metaverse」が削除され、「Gaming/XR」に取り組むスタートアップ企業が減ったということになります。 ただし、LVCC・Eureka Parkともに、このエリアに限らずVR HMDやAR Smart Glassesを含めたXR関連のブースは多くありました。例えばEureka Parkは国ごとのエリアが設けられているので、その中でXR関連のブースは「Gaming/XR」エリアの出展数以上にありました。 XRデバイスの動向 CES 2024で体験したXRデバイスの一部 今年もたくさんのXRデバイスを実際に試してきました。CES 2023では一旦落ち着きを見せていたXRデバイスですが、今年はCES 2020の頃のように盛り上がりを見せているように感じました。キヤノンのMREALやThinkのReality A3のように既に販売されているデバイスもありますが、まだ開発中のものも多く含まれています。 いわゆる眼鏡型のデバイスは、以前からあるスマートフォンに接続するものの他、Spatial Computerを謳う「 Nimo 1 」のようにデスクトップを拡張するものを増えてきました。これは後述するApple Vision Proと同じ流れと言えますし、CES 2024には出展していませんでしたが、 ImmersedのVisor も同様です。 MetaのRay-BanスマートグラスとAmazonのEcho Frames Metaは昨年の Meta Connect 2023 でMeta AIを組み込んだスマートグラスである「 Ray-Ban Metaスマートグラス 」を 発表しました 。また、昨年末にはこの Meta AIにマイクロソフトの「Bing」を組み込んだ物体認識機能が発表 されています。 Ray-Ban Metaスマートグラスを展示しているMetaブース CES 2024では、Metaブースでこの「Ray-Ban Metaスマートグラス」が展示され、多くの賑わいを見せていました。 Ray-Banショップで販売されていたRay-Ban Metaスマートグラス このスマートグラスは日本からの注目度が高いものの、現時点で日本からは注文できません。しかし、ラスベガスにはこの「Ray-Ban Metaスマートグラス」を取り扱うRay-Banの店舗やグラスショップが複数ありました。私の観測範囲内ではCESにあわせて渡米した日本からの参加者が一定の割合で購入していたようでした(私はマッチするフレームとレンズの組み合わせがなく、今回は見送りました)。 Amazonブースに展示されていたEcho Frames Amazonも昨年9月に第3世代にあたる「 Echo Frames 」を発表していて、AmazonのAlexa関連デバイスを紹介するブースで展示されていました。 Amazon.comで販売されていて、ブースではCES期間中の限定クーポンを配布していましたが、残念ながらこちらも現時点で日本からは注文できません。 CES 2024にあわせた発表 CES 2024にあわせてXR関連デバイスを発表した企業もあります。いくつか気になったものを紹介します。 Apple Vision Pro Appleは例年CESに“出展”していませんが、CESにあわせてOOHを掲出したり、CES 2020ではセッションに登壇したりしています。そのAppleはCES 2024の初日に重ねる形で「 Apple Vision Pro 」のUSにおける予約日と発売日を発表しました。 Apple CEOのTim Cook氏によるXのポスト を見かけたときは本当に興奮したことを今でも覚えています。 The era of spatial computing has arrived! Apple Vision Pro is available in the US on February 2. 本レポート記事の公開時点では、既にUSでの予約が始まっています。日本からの購入組も一定数いるように見えますが、いわゆる“技適”の関係で大手を振って国内で使えるようになるのは、日本での発売日を迎えてからになります。一刻も早く日本でも発売されることを願ってやみません。 XREAL Air 2 Ultra XREALブースの様子 いわゆる“ARグラス”の領域で特に勢いのあるXREAL社はCES 2024にあわせて「 XREAL Air 2 Ultra 」を発表、予約を開始しました。開発者が待ち望んだ「Nreal Light」の正統後継デバイスです。 赤いXの目立つブースでは、既に発売されているXREAL Air 2 / Air 2 Proの他、XREAL Air 2 Ultraの実機でハンドトラッキングのデモなどを体験できました。 6DoFのトラッキング性能を確認できるデモ 別途アポイントが必要となる個別のブースでは、6DoFのトラッキング性能を確認できるデモを体験できました。これはCES 2024の会期直前にメディア向け限定して開催される「CES Unveiled」で展示されていたものと同じです。自分だけしかいない落ち着いた個室での体験となり、Unveiledのように人が多くて混雑している状況よりもじっくりと体験できたと言えるかもしれません。 SonyのVR HMDを含む没入型空間コンテンツ制作システム CES 2024のプレスカンファレンスで突然登場したのが、SonyのVR HMDを含む没入型空間コンテンツ制作システムです。一般個人向けの製品ではなく、産業向けの製品として発表されました。 SonyのVR HMDを含む没入型空間コンテンツ制作システム Sonyブースには展示されていませんでしたが、 協業先となるSiemens のブースに実機が展示されていました。 www.youtube.com 残念ながら実機は体験できませんでしたが、YouTubeを見る限りでは、デジタル化が進んでいるファッション・アパレル領域でも使える可能性があると感じました。 ShiftallのMeganeX superlight Panasonicの100%子会社であるShiftallはMeganeXの軽量版にあたる「 MeganeX superlight 」、センサーを改良した「 HaritoraX ワイヤレス R 」、そして防音マイクmutalkの新モデル「 mutalk 2 」を発表しました。 MeganeXとMeganeX superlight CES 2020で発表された当時、その見た目から サイバーパンクやスチームパンクと話題になった VR HMDです。その後、改良を経て2023年夏に「MeganeX」として発売され、さらに実際の利用用途を考慮して軽量化されたのが「MeganeX superlight」です。 私は事前に日程をあわせた上でプライベートブースに伺い、現行品の「MeganeX」そして軽量版の「MeganeX superlight」をじっくりと試してきました。 Apple Vision Proの重さが600~650gと言われる中、約200gとされる「MeganeX superlight」は圧倒的に軽く感じました。将来的な方向性などもたくさんお話しいただき、とても有意義な時間を過ごすことができたとともに、今後が非常に楽しみになりました。 Fashion TechとBeauty Tech 昨年同様、XR領域と並行して、Fashion TechとBeauty Techも注目していました。 CES 2024のProduct Categories には記載されていませんが、Tech WestのVenetian Expo 2FにBeauty Techエリアが設けられていました。 Beauty Techエリアの地図 昨年はLife Styleエリアに配置されていたので、これはBeauty Techの注目度が高まっていることを示唆するのかもしれませんが、ブース数としては5社に留まっていました。 ただし、必ずしも関連するブースがこのBeauty Techエリアに配置されるわけではありません。昨年も賑わいを見せていた、肌に直接印刷するプリンターのPrinker社は別のエリアに配置されていたものの、今年も大盛況でした。 LG H&H社のIMPRINTUブース 肌に直接印刷するプリンター3種 また、同じジャンルでLG H&H社が IMPRINTU をCESにあわせて発表し、こちらも賑わっていました。ともにCES 2024で特に強い勢いを見せていた韓国企業です。 Beauty Tech領域で強い存在感を示していたL'Oréal Group Resort Worldに大きく展開されていたL'Oréal GroupのOOH 昨年のCES 2023では、『手や腕が不自由な人に向けたメイクアップアプリケーター「HAPTA」』と『世界初の家庭用電子アイブロウメイクアップアプリケーター「L'Oréal Brow Magic(現3D shu:brow)」』で CES 2023 Innovation Awardsを受賞していた L'Oréal Groupですが、今年もBeauty Tech領域で強い存在感を示していました。 Resort Worldに大きく展開されていたL'Oréal GroupのOOHもそのひとつですが、CES 2024ではBeauty系企業として初めてKeynoteを開催しました。 Image source. https://www.youtube.com/watch?v=pArGshMSoNo このKeynoteでは、L'Oréal GroupのCEOであるNicolas Hieronimus氏が登壇し、L'Oréal Groupのビジョンや、Beauty Tech領域における取り組みについて語りました。DE&IなBeauty Techの実現に向けて掲げていた「BEAUTY FOR EACH POWERED BY TECH」という表現が印象的でした。 また、L'Oréal Groupの持つ膨大なデータとLLMを利用したAIソリューションの「Beauty Genius」も発表されました。パーソナライズ・レコメンデーション・ARによるバーチャルトライオンなどを統合したソリューションです。 バーチャルトライオンついては2018年3月にModiFace社を買収し、その後さまざまな形で展開しているので、その一環だと考えられます。 https://www.loreal.com/ja-jp/japan/pages/group/our-purpose/discovering-modiface-jp/ www.loreal.com Image source. https://www.youtube.com/watch?v=pArGshMSoNo Beauty Geniusのデモとして、チャットUIでの会話・スマートフォンで自身の顔を撮影して送信・肌の状態を分析してスキンケア製品をレコメンドしてもらう一連の流れを披露していました。こういったソリューションが一般化することで、AIもよりいっそう自然なものになっていくのかもしれません。 L'Oréal Groupの取り組みに興味のある方はYouTubeに公開されているKeynoteのアーカイブ動画を参照してください。 www.youtube.com CES初出展の資生堂 CES初出展となった資生堂のブース 日本企業勢として、昨年のCES 2023ではKoséが初めてCESに出展していましたが、今年のCES 2024では資生堂が初めて出展していました。 資生堂がCESに出展していることに違和感を覚える方がいるかもしれませんが、近いところでは SXSW 2019でInvisible VRプロジェクト“caico”を出展 しています。他にもさまざまな形でBeayty Techに取り組んでいて、今回のCES 2024でもこの“caico”プロジェクトを引っ張っていた方が出展責任者を務めていました。 corp.shiseido.com 資生堂のブースでは、適切なスキンケア美容法の実践をサポートするアプリ「 Beauty AR Navigation 」と、2023年11月に発表された「 顔画像から将来の肌悩みを予測できるツール 」をベースにした「鼻の骨格から、“未来の肌悩み”を予測するツール」の2つを体験できました。 Beauty AR Navigationの様子 「Beauty AR Navigation」は、美容液などを肌に塗る際の資生堂が考える正しい動作を、DirectionとSpeedの2軸で評価するものです。ディスプレイの前に設置されているWebカメラに自身が映り、その動作をARで表示された手の動きと比較することで、正しい動作を学べます。私が体験したところ、Directionは良かったもののSpeedに難があり、結果は60点でした。 ゲーミフィケーション的な要素を取り入れることによって、正しい動作を習得するモチベーションを高められると感じました。Beauty TechにおけるARはバーチャルトライオンが目立ちがちですが、こうした正しい動作の習得を目的としたARの利用も、今後のBeauty Tech領域の発展に大きく貢献すると考えています。 鼻の骨格から、“未来の肌悩み”を予測するツール 「鼻の骨格から、“未来の肌悩み”を予測するツール」は、iPhoneのインカメラで顔を撮影するだけで、鼻の骨格から肌の状態や肌の特徴を推定するものです。この鼻骨格による肌分析は世界初とのことで、まさか鼻の形から将来的な肌予測に繋がるとは思いもせず、資生堂の研究開発力の高さを感じました。極めて簡単に診断できるので、今後の展開が非常に楽しみです。 キヤノンのスキャンソリューション キヤノンブース 技術カテゴリーとしてはImaging Technologyになりますが、キヤノンのブースではFashion Tech領域にも展開できるスキャンソリューションが初出展されていました。 デジタルツインなどの文脈で商品の3Dモデルを作成するソリューションは既に複数存在していますが、キヤノンのソリューションは1枚の画像からAIによってデプスを推定して立体に見せるものです。その仕組み上、360度すべてを見回すことはできませんが、ECにおいて必ずしも360度すべてを見せる必要があるわけではないので、十分に有用なソリューションだと感じました。サンプルとして服・靴・帽子が用意されていたのも非常にわかりやすかったです。 ちなみに、CES 2024では展示されていませんでしたが、同じカメラメーカーである ニコンもアバターファッションの文脈で服の3D化に取り組んでいます 。Fashion Tech領域において、カメラメーカーの持つ技術も活用されていくことは想像に難くなく、今後の動向が楽しみです。 おわりに 4日目の夜明けにホテルから見たSphere 例によって今回のCES視察は開発部門の福利厚生である「 セミナー・カンファレンス参加支援制度 」を利用しての参加となります。 今回はCES 2023時よりも1か月早くフライトとホテルを手配したことが功を奏したのか、CES 2023当時よりも15%ほど安価にフライトとホテルを手配できました。既にCES 2025の開催日程が発表されているので、参加意向のある方は、できるだけ早く手配すると良いかもしれません。特にホテルは多くの場合で一定期間まではキャンセルできます。 実際にはフライトとホテル以外にも一定の金銭的コストが発生していて、そのコストに対して得られたことの妥当性を説明するのは難しいでしょう。しかし、昨年も言っていますが、XR領域は「百聞は“一体験”に如かず」です。CESに関するニュース記事は日本でも多く目にしますが、現地に足を運び、その目その手で体験することに価値があると考えています。 また、CESはビジネスショーという性質上、個別に会話するプライベートブースが用意されています。いくつか参加しましたが、こういったオンサイトならでの対面コミュニケーションも、インターネットメディアの記事等からは得られない大きなメリットだと考えています。 最後までご覧いただきありがとうございました。帰国便の飛行機が大幅に遅延し、ひたすらロビーで待った末にフライトがキャンセルになり予定通り帰国できなくなるといったトラブルもありましたが、それはまた別の機会に。 ZOZOでは、各種エンジニアを採用中です。ご興味のある方は以下のリンクからご応募ください。 corp.zozo.com 現場からは以上です!
アバター
はじめに こんにちは、ML・データ部MLOpsブロックの 岡本 です。 MLOpsブロックでは機械学習モデルの実験基盤の作成、機械学習モデルを組み込んだAPI・Batchの開発・運用・保守を行なっています。APIを開発する際には負荷試験を実施し、本番環境で運用する際に求められるスループット・レイテンシを達成できるか確認します。 MLOpsブロックでの従来の負荷試験実施には人手を要する定型的な作業が複数ありました。また頻繁に行う作業でもありトイルとなっていました。 本記事ではMLOpsブロックで抱えていた負荷試験実施の課題と、解決のために開発したOSSのCLIツール、Gatling Commanderについて紹介します。Gatling Commanderが負荷試験の実施におけるトイル削減の一助になれば幸いです。 github.com 目次 はじめに 目次 背景・課題 従来の負荷試験実施の方法 分散負荷試験ツール(Gatling Operator)の説明 従来の実施方法 シェルスクリプトを使ったKubernetesクラスタの操作 手作業での負荷試験結果の記録 負荷試験実施の課題 設定値の変更の手間 実行状況の確認の手間 結果の記録の手間 Go製CLIツール(Gatling Commander)による負荷試験実施の自動化 Gatling Commanderの機能 負荷試験用コンテナイメージを自動作成 負荷試験用Gatlingオブジェクトを自動作成 過負荷時に負荷試験を自動停止 負荷試験結果・コンテナメトリクスを自動記録 負荷試験の完了を通知 Gatling Commanderの実装 利用技術 処理の概要 Kubernetesオブジェクトの操作の実装 Goroutinesによる並列処理の実装 Gatling Commanderの導入効果 負荷試験シナリオ切り替えの手間が不要になった 負荷試験実行の状況確認の手間が不要になった 負荷試験結果の記録の手間が不要になった 今後の展望 APIパフォーマンス劣化検知への利用 GitHub Actionsとの連携による負荷試験の定期実行 機能追加・改善 Kubernetes以外で稼働するAPIへの対応 Google Cloud以外のプロバイダへの対応 取得するメトリクスの拡充 まとめ 背景・課題 従来の負荷試験実施の方法 本節ではMLOpsブロックで行なっていた従来の負荷試験実施の方法について、利用するツールと実施方法を説明します。 分散負荷試験ツール(Gatling Operator)の説明 MLOpsブロックでは負荷試験の実施に、 Gatling で分散負荷試験を行う Kubernetes Operator の Gatling Operator を利用します。 GatlingはWebアプリケーション向けのOSS負荷試験フレームワークです。負荷試験シナリオに基づき負荷試験を行い、HTML形式の結果レポートを自動で生成します。 Kubernetes Operatorは、Kubernetes APIの拡張である Custom Resource と、その状態管理を担う Custom Controller によりKubernetesを拡張するための仕組みです。 Workloads の目的の状態を定義したCustom ResourceをKubernetesクラスタにデプロイすると、Custom Controllerは制御ループを通じてその目的の状態に近づくように制御します。WorkloadsはKubernetes上で稼動するアプリケーションであり、制御ループはあるシステムの状態を制御する終了状態のないループです。 Kubernetes Operatorを利用するとKubernetes上で稼動するアプリケーションであるWorkloadsのライフサイクル管理を自動化できます。 Gatling Operatorは分散負荷試験の内容を定義したCustom ResourceであるGatling Resourceを扱うKubernetes Operatorです。Gatling ResourceがKubernetesクラスタにデプロイされると、Gatling Resourceを監視するCustom ControllerであるGatling Controllerが目的の状態に近づくように制御します。Gatling Operatorの仕組みにより、一連の分散負荷試験のタスクを自動化できます。 Gatling Operatorの詳細は、TECH BLOGの「Gatlingによる分散負荷試験を自動化するKubernetesオペレーターGatling Operatorの紹介」をご参照ください。 techblog.zozo.com 従来の実施方法 MLOpsブロックでは従来、次の流れでGatling Operatorを使った負荷試験を実施していました。 Gatlingの負荷試験シナリオを作成 負荷試験シナリオを含むコンテナイメージのBuild&Push(アップロード) Gatling ResourceのKubernetesマニフェストを作成 Gatling ResourceをKubernetesクラスタへデプロイし、負荷試験を開始 負荷試験対象APIが稼働するコンテナのメトリクスを取得 負荷試験の実行状況を確認 負荷試験結果(Gatling Report)を Google Sheets へ記録 これらの作業は作業者のローカル環境で行っていました。 Gatling Operatorを使った従来の負荷試験では、各作業で利用できるシェルスクリプトはいくつかあるものの、作業者の手作業が多く必要でした。 シェルスクリプトを使ったKubernetesクラスタの操作 Gatling ResourceのデプロイなどKubernetesクラスタへの操作は、 kubectl コマンドを利用します。 kubectl コマンドはKubernetes APIを使用してKubernetesクラスタと通信するためのCLIツールです。 負荷試験の作業では、 kubectl コマンドを使ってKubernetesクラスタを操作し、Gatling Resourceのオブジェクト作成と負荷試験対象コンテナのメトリクスを取得します。 kubectl コマンドでの操作では操作対象クラスタの切り替えなどの作業も行います。 負荷試験でGatlingが自動生成するHTML形式の結果レポートは、Gatling ResourceのKubernetesマニフェストで指定したクラウドストレージにアップロードされます。一方で負荷試験結果は負荷試験を行うGatlingコンテナのログにも出力されます。このログを kubectl logs コマンドで取得し、ログから抽出することで、クラウドストレージを確認せずとも簡易的な形式で負荷試験結果を確認できます。 負荷試験対象コンテナのメトリクスは kubectl top コマンドで取得します。 kubectl top コマンドはKubernetesクラスタ内に Metrics Server をインストールすると利用できます。 kubectl top コマンドによりPod・Node・コンテナのCPU・メモリの使用量を取得できます。 このように負荷試験用のシェルスクリプトでは kubectl コマンドを使って、Gatling Resourceのオブジェクトを作成していました。またGatlingコンテナのログから結果の抽出と、負荷試験対象コンテナのメトリクスを取得し、これらの値をまとめた負荷試験結果を出力していました。 負荷試験時には作業者がこれらのシェルスクリプトを実行していました。 手作業での負荷試験結果の記録 MLOpsブロックでは負荷試験結果の記録先としてGoogle Sheetsを利用しています。Google Sheetsは複数の人が同時に閲覧・編集できるため、チームでの作業に便利です。 Gatlingは負荷試験ごとに結果レポートを自動生成します。このレポートは負荷試験ごとに作成されるため、実施したすべての負荷試験結果を一覧し、比較するには不便でした。 Google Sheetsであれば負荷試験結果を表形式でまとめることができ、Googleアカウントへの権限付与で共有できるためチーム内への展開も簡単です。そのため負荷試験結果の最終的な記録先としてGoogle Sheetsを利用しています。 作業者はシェルスクリプトが出力する負荷試験結果をGoogle Sheetsにコピー&ペーストして記録していました。 負荷試験実施の課題 従来の負荷試験実施の方法は作業者の手間が多く、負荷試験全体でかかる作業工数が課題でした。作業者は各作業を負荷試験シナリオごとに行います。個々は小さな手間でも積み重なると大きくなり、負荷試験全体で1〜2日程度の作業工数を要していました。 負荷試験でAPIのパフォーマンスを計測する際、様々な条件を試すために設定パラメータの値が異なる複数の負荷試験シナリオを用意します。負荷試験シナリオごとに、前節( 従来の実施方法 )で説明した 3.Gatling ResourceのKubernetesマニフェストを作成 から 7.負荷試験結果(Gatling Report)をGoogle Sheetsへ記録 までの作業を繰り返します。これらは定型作業ですが作業数が多く、負荷試験シナリオの数に比例して作業者の手間も増えます。 このように従来の負荷試験実施では、手作業の項目が多いことで作業者の工数を要していました。本節ではこれらの項目の中でも、特に作業者の大きな手間になっていた項目について説明します。 設定値の変更の手間 従来の負荷試験実施の方法では、負荷試験シナリオごとに作業者が設定パラメータの値を変更していました。 負荷試験シナリオの設定パラメータはGatlingのテストシナリオを記述したファイルであるSimulationで定義します。SimulationはGatlingのコンテナイメージに含めます。設定パラメータの値はGatling ResourceのKubernetesマニフェストで指定します。Gatlingはこの値をGatlingコンテナの環境変数から読み込みます。 負荷試験シナリオ間でSimulationが共通する場合、同一のコンテナイメージを利用できるため、シナリオごとにコンテナイメージのBuild&Push作業は不要です。一方で設定パラメータの値については負荷試験シナリオ間で異なるため、シナリオごとにGatling ResourceのKubernetesマニフェストで値の変更が必要です。 例えば負荷試験で頻繁にある、APIの最大スループットを計測する場合を考えます。この場合秒間リクエスト数の設定パラメータ値を変更した、複数のシナリオで負荷試験を実施します。 作業者は秒間リクエスト数を変更するために、負荷試験シナリオごとにGatling ResourceのKubernetesマニフェストを変更する必要がありました。負荷試験シナリオごとに実行結果を確認し、レイテンシに問題がなければ秒間リクエスト数の値を増やして次のシナリオを実行していました。 リクエスト数の変更を繰り返してAPIの最大スループットを測るため、作業者が負荷試験の設定パラメータの値を変更する作業回数は多くなります。特に新規開発したAPIでは、どの程度のスループットを捌けるのか事前にわからず、作業工数がかかっていました。 このように負荷試験において設定パラメータの値の変更は頻繁に行われるにもかかわらず、スクリプトでの自動化ができていなかったため作業者の手間になっていました。 実行状況の確認の手間 同一のAPIを対象に負荷試験を行う場合、前のシナリオで負荷試験が完了してから、次のシナリオでの負荷試験を実行します。従来の方法では、前の負荷試験の実行状況をシナリオごとに、作業者が確認していました。 Gatling Operatorによって分散負荷試験の実行は自動化されているため、シナリオごとの負荷試験の実行中に作業者による作業は必要ありません。この間に作業者は他のタスクを進められます。しかしシナリオあたりの負荷試験の実行時間が短いと、実行状況の確認のために他のタスクを頻繁に中断するため、作業者は効率的に作業できていませんでした。 このように実行状況の確認は、作業者にとって頻繁に行う必要のある割り込み作業であり、手間になっていました。 結果の記録の手間 従来の負荷試験実施の方法では、シナリオごとに作業者が結果を記録していました。 前節( 手作業での負荷試験結果の記録 )の通り、作業者はシェルスクリプトが出力した、簡易的な負荷試験結果の値をGoogle Sheetsへ記録します。作業者はGoogle Sheetsへ手作業でコピー&ペーストして結果を記録していました。 また前節( 設定値の変更の手間 )で例として挙げたAPIの最大スループットを測る場合では、結果の記録に加えて結果の値の確認も必要です。作業者は負荷試験結果を確認し、目標レイテンシを超えていないか、エラーが発生していないかなど、事前に決められた閾値を元に次のシナリオを実行するかどうかを判断していました。 このように負荷試験結果の確認およびGoogle Sheetsへの結果の記録も、シナリオごとに作業者が手作業で行い、作業者の手間となっていました。 Go製CLIツール(Gatling Commander)による負荷試験実施の自動化 MLOpsブロックでは負荷試験実施にかかる作業者の工数削減のために、CLIツールGatling Commanderを開発し、OSSとして公開しました。 Gatling Commanderは、Gatling Operatorを利用した負荷試験実施における一連の作業を自動化します。 前節( 従来の実施方法 )で説明した従来の負荷試験の流れのうち、 3.Gatling ResourceのKubernetesマニフェストを作成 から 7.負荷試験結果(Gatling Report)をGoogle Sheetsへ記録 までの作業はGatling Commanderにより自動化されます。 本節ではGatling Commanderについて機能と実装の概要、そして実際にMLOpsブロックで利用して感じた効果を説明します。 Gatling Commanderの機能 Gatling Commanderは次の機能を持ちます。 負荷試験用コンテナイメージをBuild&Push 負荷試験シナリオごとにGatlingオブジェクトを作成 過負荷時に負荷試験を自動停止 負荷試験結果、コンテナメトリクスを記録 負荷試験の実行状況を確認 負荷試験の完了を通知 これらの機能に加えて、Gatling Commanderの設定ファイルには複数の負荷試験シナリオを記述可能です。この設定ファイルをYAML形式で記述し、コマンドを実行するとGatling Commanderによりすべての負荷試験シナリオが自動実行されます。 Gatling Commanderは、従来の負荷試験実施の方法でシナリオの数に比例して作業者の手間が増加していた課題を解消し、負荷試験実施にかかる全体の作業工数を削減しています。本節ではGatling Commanderが提供する機能について説明します。 負荷試験用コンテナイメージを自動作成 Gatling Commanderは負荷試験用コンテナイメージのBuild&Push作業を自動化します。 Gatling CommanderはGatlingのSimulation等のファイルを含めて負荷試験用のコンテナイメージをBuildします。またGatling Commanderの設定ファイルで指定したコンテナレジストリに、BuildしたコンテナイメージをPushします。 Gatling CommanderはBuild&Pushしたコンテナイメージを、複数の負荷試験シナリオ間で共通のコンテナイメージとして利用します。 事前にBuild&Pushしたコンテナイメージを使う場合は、コマンド実行時にオプションを指定してBuild&Pushを省略可能です。Build&Pushを省略する場合、Gatling Commanderは設定ファイルで指定したBuild&Push済みのコンテナイメージを利用します。 負荷試験用Gatlingオブジェクトを自動作成 コンテナイメージのBuild&Push後、Gatling CommanderはKubernetesクラスタ内にGatling Resourceのオブジェクトを作成します。Kubernetesクラスタ内にGatlingオブジェクトが作成されると、Gatling OperatorはGatlingコンテナを稼働させるPodを作成し分散負荷試験を実施します。 Gatling Commanderは、設定ファイルで指定したベースとなるGatling ResourceのKubernetesマニフェストを、GoのGatling構造体のオブジェクトに読み込みます。次に負荷試験シナリオごとに値が異なるフィールドの値を、設定ファイルに指定された値で上書きします。 ベースとなるGatling ResourceのKubernetesマニフェストの例は次のとおりです。 apiVersion : gatling-operator.tech.zozo.com/v1alpha1 kind : Gatling metadata : name : <config.yaml overrides this field> # will be overrided by services[].name field value in config.yaml. ex: sample-service namespace : gatling spec : generateReport : true generateLocalReport : true notifyReport : false cleanupAfterJobDone : false podSpec : gatlingImage : <config.yaml overrides this field> # will be overrided by built Gatling Image URL or imageURL field value in config.yaml. ex: asia-docker.pkg.dev/project_id/foo/bar/gatlinge-image-name-prefix-YYYYMMDD rcloneImage : rclone/rclone resources : requests : cpu : "7000m" memory : "4G" limits : cpu : "7000m" memory : "4G" serviceAccountName : "gatling-operator-worker-service-account" affinity : nodeAffinity : requiredDuringSchedulingIgnoredDuringExecution : nodeSelectorTerms : - matchExpressions : - key : cloud.google.com/gke-nodepool operator : In values : - "gatling-operator-worker-pool" tolerations : - key : "dedicated" operator : "Equal" value : "gatling-operator-worker-pool" effect : "NoSchedule" cloudStorageSpec : provider : "gcp" bucket : "report-storage-bucket-name" notificationServiceSpec : provider : "slack" secretName : "gatling-notification-slack-secrets" testScenarioSpec : parallelism : <config.yaml overrides this field> # will be overrided by services[].scenarioSpecs[].testScenarioSpec.parallelism field value. ex: 1 simulationClass : <config.yaml overrides this field> # will be overrided by services[].scenarioSpecs[].testScenarioSpec.simulationClass field value. ex: SampleSimulation env : # will be overrided by services[].scenarioSpecs[].testScenarioSpec.env[] field value. ex: `env: [{name: ENV, value: "dev"}, {name: CONCURRENCY, value: "20"}]` - name : <config.yaml overrides this field> value : <config.yaml overrides this field> 上記のGatling ResourceのKubernetesマニフェストの例で、 <config.yaml overrides this field> と記述があるフィールドの値は、Gatling Commanderの設定ファイルの値で負荷試験シナリオごとに上書きされます。 例えば負荷試験コンテナイメージを指定する spec.podSpec.gatlingImage フィールド・GatlingのSimulationのクラス名や環境変数など、負荷試験の具体的な設定値を記述するフィールドを配下に持つ spec.testScenarioSpec フィールドの値は上書き対象です。これらのフィールドの値はGatling Commanderの設定ファイルの値で負荷試験シナリオごとに上書きされます。 Gatling Commanderの設定ファイルの記述例は次のとおりです。 gatlingContextName : gatling-cluster-context-name imageRepository : gatling-image-stored-repository-url imagePrefix : gatlinge-image-name-prefix imageURL : "" # (Optional) specify image url when using pre build gatling container image baseManifest : config/base_manifest.yaml gatlingDockerfileDir : gatling startupTimeoutSec : 1800 # 30min execTimeoutSec : 10800 # 3h slackConfig : webhookURL : slack-webhook-url mentionText : <@targetMemberID> services : - name : sample-service spreadsheetID : sample-sheets-id failFast : true targetPercentile : targetLatency : targetPodConfig : contextName : target-pod-context-name namespace : sample-namespace labelKey : run labelValue : sample-api containerName : sample-api scenarioSpecs : - name : case-1 subName : 10req/sec testScenarioSpec : simulationClass : SampleSimulation parallelism : 1 env : - name : CONCURRENCY value : "10" - name : ENV value : "dev" - name : DURATION value : "180" - name : case-1 subName : 20req/sec testScenarioSpec : simulationClass : SampleSimulation parallelism : 1 env : - name : CONCURRENCY value : "20" - name : ENV value : "dev" - name : DURATION value : "180" Gatling Commanderの設定ファイルでは、負荷試験シナリオごとに上書きするフィールド値を1つの設定ファイル内に記述できます。複数の負荷試験シナリオごとの設定値を1つの設定ファイルに集約することで、実行するシナリオを一覧しやすくなります。 上記の設定ファイルの例で services[].scenarioSpecs[] フィールドには2つの要素が記述されています。このフィールドの各要素は負荷試験シナリオごとに固有の設定値を持ちます。 Gatling Commanderはコマンド実行時に、設定ファイルから services[].scenarioSpecs[].testScenarioSpec フィールドの値を読み取ります。こうして読み取った値で負荷試験シナリオごとに、ベースとなるGatling ResourceのKubernetesマニフェストの対象フィールド値を上書きします。この値の上書きは、ベースとなるGatling ResourceのKubernetesマニフェストを読み込んだGatling構造体のオブジェクトに対して行われます。 負荷試験シナリオごとに固有な設定値をGatling Commanderの設定ファイルに指定することで、Gatling ResourceのKubernetesマニフェストをシナリオごとに用意する必要がなくなります。Gatling ResourceのKubernetesマニフェストについて、複数の負荷試験シナリオ間で同一のフィールド値の指定が共通化されるため、作業者はシナリオごとに固有の設定値のみGatling Commanderの設定ファイルへ記述すれば良くなります。フィールドが共通化されることにより、値を変更するフィールドが絞られ、設定値の変更漏れを防ぐことができます。 このようにGatling Commanderは、ベースとなるGatling ResourceのKubernetesマニフェスト・Gatling Commanderの設定ファイルから、負荷試験シナリオごとにGoのGatling構造体のオブジェクトを作成します。こうして作成したGatling構造体のオブジェクトを元に、Kubernetesクラスタ内に負荷試験シナリオごとのGatlingオブジェクトを作成します。 Gatling Commanderはこれらの処理により、負荷試験用Gatlingオブジェクトを自動作成します。 過負荷時に負荷試験を自動停止 Gatling Commanderは設定ファイルに停止条件を指定し、ある負荷試験シナリオの結果が条件に一致した時、後続のシナリオの実行を自動停止できます。事前に停止条件を指定することで、負荷試験シナリオを余計に実行することを防ぎます。 前節( 設定値の変更の手間 )で挙げた、複数のシナリオを切り替えて秒間リクエスト数を増やしながら負荷試験を繰り返し、APIの最大スループットを測る場合を考えます。 この場合、ある負荷試験シナリオで目標レイテンシを達成できないと、そのシナリオよりもリクエスト数が多い後続のシナリオでは更なるレイテンシの悪化が予想でき、追加実行はあまり意味がないです。こうした場合にGatling Commanderによる負荷試験の自動停止が活用できます。 負荷試験の自動停止の条件には次の項目が利用可能です。 Failしたリクエストがある 特定のパーセンタイル値で指定した目標レイテンシの閾値を超過する 設定ファイルに停止条件を指定すると、Gatling Commanderは各シナリオで負荷試験の実行が完了した際に、負荷試験結果の値を確認します。確認した値が指定したこの条件に一致する場合、Gatling Commanderは後続のシナリオの負荷試験を自動実行せずに停止します。 前節( Gatling Commanderの機能 )の通り、Gatling Commanderの設定ファイルには複数の負荷試験シナリオを記述できます。これらの負荷試験シナリオはserviceという単位で記述します。 service単位で記述することで、同一の負荷試験対象APIに対する複数のシナリオの設定値をグループ化できます。同一のservice配下に複数の負荷試験シナリオの設定値を記述する場合、これらはGatling Commanderにより設定ファイル内での記載順で実行されます。 前節( 負荷試験用Gatlingオブジェクトを自動作成 )で説明したGatling Commanderの設定ファイルの例で、serviceごとの設定値は次の箇所です。 services : - name : sample-service spreadsheetID : sample-sheets-id failFast : true targetPercentile : targetLatency : targetPodConfig : contextName : target-pod-context-name namespace : sample-namespace labelKey : run labelValue : sample-api containerName : sample-api scenarioSpecs : - name : case-1 subName : 10req/sec testScenarioSpec : simulationClass : SampleSimulation parallelism : 1 env : - name : CONCURRENCY value : "10" - name : ENV value : "dev" - name : DURATION value : "180" - name : case-1 subName : 20req/sec testScenarioSpec : simulationClass : SampleSimulation parallelism : 1 env : - name : CONCURRENCY value : "20" - name : ENV value : "dev" - name : DURATION value : "180" Galing Commanderの設定ファイルでは、 services[] フィールドの配下の要素にserviceごとの設定値を指定します。負荷試験の自動停止の条件は services[].failFast フィールドと services[].targetPercentile ・ services[].targetLatency フィールドで指定します。 この例では services[].name フィールドの値に負荷試験対象のservice名として sample-service を指定しています。このserviceの services[].scenarioSpecs[] フィールドには2つの負荷試験シナリオの設定値を指定しています。同一service内の負荷試験シナリオは順次実行されます。この例だとGatling Commanderは name: case-1 の subName: 10req/sec 、 subName: 20req/sec の順番でシナリオごとの負荷試験を実行します。 この例で services[].failFast フィールドの値は true です。そのため name: case-1 の subName: 10req/sec のシナリオの結果でFailしたリクエストがあると、後続の name: case-1 の subName: 20req/sec のシナリオの負荷試験は実行しません。 負荷試験の自動停止の例としてGatling Commanderを利用し、前節( 設定値の変更の手間 )で挙げたAPIの最大スループットを測る場合を考えます。次のYAMLはこの場合のGatling Commanderの設定ファイルの記述例です。 gatlingContextName : gatling-cluster-context-name imageRepository : gatling-image-stored-repository-url imagePrefix : gatlinge-image-name-prefix imageURL : "" # (Optional) specify image url when using pre build gatling container image baseManifest : config/base_manifest.yaml gatlingDockerfileDir : gatling startupTimeoutSec : 1800 # 30min execTimeoutSec : 10800 # 3h slackConfig : webhookURL : slack-webhook-url mentionText : <@targetMemberID> services : - name : sample-service spreadsheetID : sample-sheets-id failFast : true targetPercentile : 99 targetLatency : 500 targetPodConfig : contextName : target-pod-context-name namespace : sample-namespace labelKey : run labelValue : sample-api containerName : sample-api scenarioSpecs : - name : check-max-throughput subName : 50req/sec testScenarioSpec : simulationClass : SampleSimulation parallelism : 1 env : - name : CONCURRENCY value : "50" - name : ENV value : "dev" - name : DURATION value : "180" - name : check-max-throughput subName : 100req/sec testScenarioSpec : simulationClass : SampleSimulation parallelism : 1 env : - name : CONCURRENCY value : "100" - name : ENV value : "dev" - name : DURATION value : "180" - name : check-max-throughput subName : 125req/sec testScenarioSpec : simulationClass : SampleSimulation parallelism : 1 env : - name : CONCURRENCY value : "125" - name : ENV value : "dev" - name : DURATION value : "180" - name : check-max-throughput subName : 150req/sec testScenarioSpec : simulationClass : SampleSimulation parallelism : 1 env : - name : CONCURRENCY value : "150" - name : ENV value : "dev" - name : DURATION value : "180" - name : check-max-throughput subName : 175req/sec testScenarioSpec : simulationClass : SampleSimulation parallelism : 1 env : - name : CONCURRENCY value : "175" - name : ENV value : "dev" - name : DURATION value : "180" この例ではGatling Commanderの設定ファイルで停止条件として、 services[].targetPercentile に 99 、 services[].targetLatency に 500 を指定しています。これにより、あるシナリオの負荷試験結果で指定した目標レイテンシ(99%ile値で500ミリ秒)の閾値を超えると、後続のシナリオの負荷試験は実行されません。またGatling Commanderの設定ファイルの services[].scenarioSpecs[] フィールドの要素にはリクエスト数が50・100・125・150・175req/secに対応する複数の負荷試験シナリオの設定値を記述しています。 Gatling Commanderはこれらの負荷試験シナリオを設定ファイルの記載順で実行するため、小さいリクエスト数のシナリオを先に記述します。このように設定値を記述すると、Gatling Commanderはあるシナリオの負荷試験で目標レイテンシの閾値を超えるまで、順にリクエスト数を増やして負荷試験を実行します。もし125req/secのシナリオで負荷試験結果が目標レイテンシの閾値を超えると、Gatling Commanderは150req/sec以降のシナリオを実行せずに負荷試験を終了します。 このようにGatling Commanderでは事前に設定ファイルで停止条件を指定し、負荷試験結果が指定した条件と一致した場合に後続の負荷試験を自動停止できます。 負荷試験結果・コンテナメトリクスを自動記録 Gatling Commanderは設定ファイルで指定したGoogle Sheetsに、負荷試験結果と負荷試験対象コンテナのメトリクスを自動で記録します。 Gatlingコンテナが稼働するPodの実行が終了すると、Gatling OperatorはGatlingの結果レポートを指定したクラウドストレージにアップロードします。Gatling Commanderはこのクラウドストレージにアップロードされた結果レポートを取得します。 またGatling Commanderは負荷試験の実行中に、負荷試験対象コンテナのメトリクスを一定間隔で取得し続けます。このメトリクスはKubernetesクラスタのMetrics APIから取得します。そして負荷試験の実行完了後に、Gatling Commanderは取得したコンテナメトリクスの平均値を計算します。 続いてGatling Commanderは取得した結果レポートと、計算したコンテナメトリクスの平均値から負荷試験結果を生成します。生成した負荷試験結果を、Gatling Commanderの設定ファイルで指定されたGoogle Sheetsへ記録します。 次に示す画像が、Google Sheetsへの負荷試験結果の記録例です。 負荷試験結果の記録先となるGoogle SheetsはGatling Commanderの設定ファイルでserviceごとに指定できます。また同一service内では、 services[].scenarioSpecs[].name フィールドの値と実行日を基にしたシート名で、記録先シートを自動作成します。 負荷試験の完了を通知 Gatling Commanderでは設定ファイルに通知先を指定して、すべての負荷試験シナリオが完了したことを通知できます。通知先は Slack を利用でき、メンバーIDを指定することで特定メンバーへのメンションも可能です。 次に示す画像はSlack通知の例です。 この画像の例は、Gatling Commanderで実行した負荷試験が正常終了した場合の通知です。 Gatling Commanderは複数の負荷試験をまとめて実行可能であり、1回のコマンド実行あたりの負荷試験全体の実行時間は長くなります。このように事前にSlack通知を設定することで、作業者はGatling Commanderの実行完了時にSlackのメンションですぐに気が付けます。 Gatling Commanderの実装 本節ではGatling Commanderについて、利用技術、処理の概要、主な機能の実装として、Kubernetesオブジェクトの操作の実装・Goroutinesによる並列処理の実装を説明します。 利用技術 Gatling CommanderはGoで実装しています。 従来の負荷試験実施の方法では、負荷試験の際に必要な一部の操作をシェルスクリプトとして実装していました。そのため開発時に元々あったシェルスクリプトの機能を拡張し、課題を解決するアプローチも検討しました。しかし機能の拡張のしやすさを考えると、別の言語で新たに実装した方が良いと判断し、元々シェルスクリプトで行っていた処理も含めて新たにGoでCLIツールを実装しました。 Goは client-go ・ apimachinery ・ api などKubernetes周辺の開発で利用できるモジュールが充実しており、Kubernetes関連のツールの実装と相性が良いです。またGatling OperatorもGoで実装されており、Gatling Resourceに対応するGoの構造体はGatling Operatorのリポジトリで定義されています。これらの理由からGatling Commanderの開発言語にはGoを採用しました。 またGatling Commanderは、CLIを作成するためのインタフェースを提供するGoのモジュールである Cobra をベースに実装しています。Cobraを利用することでCLIインタフェースの実装に工数をかけずに開発ができました。 次にGoogle Sheetsへの結果の記録には Google Sheets API を利用しています。 Google Sheets APIへのリクエストには、Goのモジュールである Google APIs Client Library を使用しています。このモジュールは Google Cloud サービスへアクセスする機能を提供します。Gatling Commanderはこのモジュールの sheets パッケージを利用して、Google Sheets APIへリクエストを送り、Google Sheetsへ負荷試験結果を記録します。 処理の概要 Gatling Commanderは負荷試験実施の一連の作業を、次に示す各処理で実装しています。これらの一連の処理は次の流れで行います。 Gatling Commanderの設定ファイルの読み込み 負荷試験用コンテナイメージをBuild&Push 負荷試験シナリオに対応するGatling構造体のオブジェクトを作成 負荷試験対象APIが稼働するコンテナのリソース割り当ての値を取得 KubernetesクラスタにGatlingオブジェクトを作成 負荷試験の実行中に、負荷試験対象APIが稼働するコンテナのメトリクスを取得 負荷試験の実行後に、アップロードされた負荷試験の結果レポートをクラウドストレージから取得 負荷試験結果をGoogle Sheetsへ書き込み 次の負荷試験シナリオの負荷試験を実行 同一service内に複数のシナリオがある場合、 3.負荷試験シナリオに対応するGatling構造体のオブジェクトを作成 から 9.次の負荷試験シナリオの負荷試験を実行 までの処理をシナリオごとに繰り返します。またserviceごとの、 3.負荷試験シナリオに対応するGatling構造体のオブジェクトを作成 から 9.次の負荷試験シナリオの負荷試験を実行 までの処理は並列で実行します。 Kubernetesオブジェクトの操作の実装 Gatling CommanderはGoのモジュールを利用してKubernetesのオブジェクトを操作します。 具体的には次の操作をします。 Gatlingオブジェクトの取得・作成・削除 負荷試験対象APIが稼働するコンテナのメトリクスを取得 Kubernetesのオブジェクトを操作するためにはKubernetes APIへリクエストを送ります。GoでKubernetes APIへリクエストを送るために、各Kubernetes Resourceに対応するClientパッケージを利用します。Clientパッケージを利用することで、Goのオブジェクトのメソッドを通してAPIへのHTTPリクエストを送れます。 前述した client-go モジュールでは、PodやDeploymentなどKubernetesの標準Resourceを扱うClientを提供します。またKubernetesのControllerを開発する際に利用される controller-runtime モジュールでもClientを提供します。 Gatling Commanderでは controller-runtime の client パッケージでClientを初期化し、Kubernetesのオブジェクトを操作します。 client パッケージを用いて、Kubernetes Resourceを操作するには、操作対象のResourceのSchemeを追加する必要があります。SchemeはKubernetes Resourceについて、Kubernetes APIで扱うKubernetesオブジェクトと対応するGoの構造体をマッピングします。操作対象のKubernetes ResourceのSchemeを追加すると、 client パッケージで初期化したClientから対象のResourceを操作できます。 Gatling Commanderでは次の実装でClientを初期化します。簡略化のため一部処理の実装を省略しています。 import ( gatlingv1alpha1 "github.com/st-tech/gatling-operator/api/v1alpha1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" clientgoscheme "k8s.io/client-go/kubernetes/scheme" ctrlClient "sigs.k8s.io/controller-runtime/pkg/client" ctrlConfig "sigs.k8s.io/controller-runtime/pkg/client/config" ...省略 ) func InitClient(k8sCtxName string ) (ctrlClient.Client, error ) { scheme := runtime.NewScheme() utilruntime.Must(clientgoscheme.AddToScheme(scheme)) utilruntime.Must(gatlingv1alpha1.AddToScheme(scheme)) k8sConfig, err := ctrlConfig.GetConfigWithContext(k8sCtxName) if err != nil { return nil , err } cl, err := ctrlClient.New(k8sConfig, ctrlClient.Options{ Scheme: scheme, }) ...省略 } 各Kubernetes ResourceのSchemeは、各Resourceに対応する構造体を定義したGoのモジュールで提供されます。各モジュールが提供する AddToScheme() 関数にSchemeオブジェクトを渡すことで、Kubernetes ResourceごとのSchemeがSchemeオブジェクトに追加されます。 Clientは controller-runtime モジュールの client パッケージが提供する New() 関数で初期化できます。上記の実装では cl 変数が初期化したClientです。Schemeオブジェクトを New() 関数の引数に渡すことで、ClientはSchemeを追加したKubernete Resourceについて、メソッドを通してリクエストを送れるようになります。 初期化したClientの Create() メソッドを利用すると、次の実装でGatlingオブジェクトを作成できます。簡略化のため一部処理の実装を省略しています。 func CreateGatling(ctx context.Context, cl ctrlClient.Client, gatling *gatlingv1alpha1.Gatling) error { ...省略 if err := cl.Create(ctx, gatling); err != nil { return err } return nil } この実装の CreateGatling() 関数では引数 cl で、初期化したClientを受け取っています。引数 cl の Create() メソッドの引数には、Gatling構造体オブジェクトのポインタを渡して実行します。 Create() メソッドの実行によりKubernetes APIにリクエストを送り、KubernetesクラスタにGatlingオブジェクトを作成します。 このようにGoの controller-runtime 等のモジュールを活用して、簡単にKubernetes Resourceの操作を実装できました。 Goroutinesによる並列処理の実装 Gatling CommanderではGoが提供するGoroutinesを活用して、複数箇所で処理を並列実行しています。 GoroutinesはGoのランタイムにより管理される軽量なスレッドです。 go キーワードを関数の前につけることで、その関数を他のコードと別のスレッドで実行できます。 Gatling CommanderでのGoroutinesの主な利用用途は次の2つです。 serviceごとの負荷試験の並列実行 実行した負荷試験の終了の待機処理と、負荷試験対象コンテナのメトリクス取得処理の並列実行 Gatling Commanderでは次の実装でserviceごとの負荷試験を並列実行しています。簡略化のためここでは負荷試験の実行処理等の実装を省略しています。 wg := new (sync.WaitGroup) for _, service := range config.Services { wg.Add( 1 ) go func (ctx context.Context, s cfg.Service) { defer wg.Done() // 負荷試験の実行処理 ...省略 }(ctx, service) } wg.Wait() 上記の実装の中で config 変数はGatling Commanderの設定ファイルに対応する構造体のオブジェクトです。 config オブジェクトの Services フィールドでは各serviceの全ての負荷試験シナリオを含む設定値を持ちます。 for ブロックの中で go キーワードを無名関数の前につけて実行しています。この関数には go キーワードがついているため、serviceごとに呼び出し元のスレッドとは別のスレッドで実行されます。この関数の中では、Gatling Resourceオブジェクトの作成など負荷試験の根幹的な処理をします。 上記の実装では、呼び出し側のスレッドで実行したGoroutinesの完了を待つために wg 変数を作成しています。 wg 変数は sync パッケージの WaitGroup のポインタ型変数です。 WaitGroup 型が提供する Wait() メソッドを利用すると、実行したGoroutineの完了まで呼び出し側のスレッドで処理を待機できます。 Wait() メソッドは WaitGroup オブジェクトの持つカウンタが0になるまで処理をブロックします。また Add() メソッドはこのカウンタの値を増やし、 Done() メソッドはこの値を減らします。 上記の実装では、Goroutinesの起動時に呼び出し側で Add() メソッドを呼び、その後 Wait() メソッドを呼んでいます。それぞれのGoroutines内で defer キーワードをつけて、 Done() メソッドを呼んでいます。関数に defer キーワードをつけることで実行が遅延され、無名関数のブロックを抜ける前に必ず Done() メソッドが実行されます。 Wait() メソッドではカウンタが0になるまで処理をブロックするため、起動したすべてのGoroutines内で Done() メソッドが呼ばれるまで処理を待機します。 このようにGatling CommanderではGoroutinesを利用して、serviceごとの負荷試験を並列実行しています。 またGatling Commanderでは、実行した負荷試験の終了の待機処理と、負荷試験対象コンテナのメトリクス取得処理を並列で実行します。serviceごとの負荷試験の並列実行の実装と異なる点は、起動したGoroutinesを関数の呼び出し元のスレッドから終了する点です。 このようなGoroutines間の連携ではGoのChannelsを利用できます。Channelsを利用すると、異なるGoroutinesで実行している関数同士で、特定の型の値を送受信してやり取りできます。 実行した負荷試験の終了を待機する関数では、負荷試験の実行状況を一定間隔で確認し続けます。そして負荷試験の実行終了を確認すると、並列実行していた負荷試験対象コンテナのメトリクスを取得する関数に、負荷試験の実行終了を知らせます。 それぞれの関数は異なるGoroutinesとして実行されているため、関数間でのやり取りにChannelsを利用しています。 Gatling Commanderでは次の実装で、負荷試験の実行終了の待機と、負荷試験対象コンテナのメトリクス取得を並列実行しています。 wg := new (sync.WaitGroup) wg.Add( 1 ) informJobFinishCh := make ( chan bool , 1 ) metricsUsageCh := make ( chan kubeapiTools.MetricsField, 1 ) go kubeapiTools.FetchContainerMetricsMean(ctx, wg, metricsCl, metricsUsageCh, informJobFinishCh, targetPodConfig) err = gatlingTools.WaitGatlingJobRunning(ctx, k8sGatlingClient, gatling, waitExecTimeout, informJobFinishCh) if err != nil { return nil , fmt.Errorf( "failed to wait gatling job running, %v" , err) } close (informJobFinishCh) wg.Wait() close (metricsUsageCh) metricsUsageMean, ok := <-metricsUsageCh if !ok { fmt.Fprintf(os.Stderr, "metricsUsageCh value is empty, so each metricsUsage field value is 0" ) } この実装では、負荷試験対象コンテナのメトリクスを取得する関数が FetchContainerMetricsMean() 、実行した負荷試験の終了を待機する関数が WaitGatlingJobRunning() です。 FetchContainerMetricsMean() 関数の前には go キーワードがあり、 WaitGatlingJobRunning() 関数を実行するスレッドとは別のスレッドで実行されます。 WaitGatlingJobRunning() 関数を実行するスレッドから FetchContainerMetricsMean() 関数を実行するGoroutinesを終了するために利用するChannelsが informJobFinishCh 変数です。 FetchContainerMetricsMean() 関数と WaitGatlingJobRunning() 関数ではどちらも引数に informJobFinishCh 変数を受け取ります。 WaitGatlingJobRunning() 関数では負荷試験が終了した際にこの informJobFinishCh 変数へ値を送信します。一方で FetchContainerMetricsMean() 関数では informJobFinishCh 変数に書き込まれた値を受信します。 Gatling Commanderでは次の実装で、 WaitGatlingJobRunning() 関数内でChannelsへ値を送信します。簡略化のためここでは負荷試験の実行状況の確認処理を省略しています。 func WaitGatlingJobRunning( ctx context.Context, cl ctrlClient.Client, gatling *gatlingv1alpha1.Gatling, timeout int32 , jobFinishCh chan bool , ) error { defer func () { jobFinishCh <- true }() for { // 負荷試験の実行状況の確認処理 ...省略 } } WaitGatlingJobRunning() 関数では informJobFinishCh 変数の値を引数の jobFinishCh で受け取ります。そしてChannelsである jobFinishCh 引数へ値を送信します。 jobFinishCh 引数への値の送信は defer キーワードをつけた無名関数内で実行しています。 defer キーワードにより WaitGatlingJobRunning() 関数では、関数ブロックを抜ける前に必ず jobFinishCh 引数への値の書き込みが実行されます。 Gatling Commanderでは次の実装で、 FetchContainerMetricsMean() 関数内でChannelsから値を受信します。簡略化のためここではコンテナメトリクスの取得処理等を省略しています。 func FetchContainerMetricsMean( ctx context.Context, wg *sync.WaitGroup, cl metricsClientset.Interface, resultCh chan MetricsField, receiveGatlingFinishedCh chan bool , podConfig cfg.TargetPodConfig, ) { defer wg.Done() ...省略 for { select { case <-ctx.Done(): return case <-receiveGatlingFinishedCh: // 取得したコンテナメトリクスの平均値を計算 // 計算したコンテナメトリクスの値を呼び出し側に送信 ...省略 return default : // コンテナメトリクスの取得の処理 ...省略 } } } FetchContainerMetricsMean() 関数内で informJobFinishCh 変数の値を、引数の receiveGatlingFinishedCh で受け取ります。そしてChannelsである receiveGatlingFinishedCh 引数から値を受信します。 FetchContainerMetricsMean() 関数では、無限ループとなる for ブロックの中で select ブロックを使用して処理を条件分岐しています。 WaitGatlingJobRunning() 関数でChannelsに値が送信されると、 case <-receiveGatlingFinishedCh の条件に一致します。この条件に一致した場合、呼び出し側へ取得したコンテナメトリクスの値を渡して for ブロックを抜けます。 上記の実装では省略していますが、取得したコンテナメトリクスの値を呼び出し側に渡す際には、Channelsである引数の resultCh へ値を送信しています。 このようにGatling Commanderでは開発言語としてGoを採用したことで、GoroutinesとChannelsを活用し簡単に並列処理を実装できました。 Gatling Commanderの導入効果 Gatling Commanderの導入により、Gatling Operatorのみで行っていた従来の負荷試験において、作業者が手作業で実施していたほとんどの作業を自動化できました。 次に示すのが自動化できた作業の一覧です。 負荷試験シナリオを含むコンテナイメージのBuild&Push Gatling ResourceのKubernetesマニフェストを作成 負荷試験の開始(Gatling ResourceをKubernetesクラスタへデプロイ) 負荷試験対象APIが稼働するコンテナのメトリクスを取得 負荷試験の実行状況を確認 負荷試験結果(Gatling Report)をGoogle Sheetsへ記録 前節( 従来の実施方法 )で説明した作業の一覧と比較すると、 2.負荷試験シナリオを含むコンテナイメージのBuild&Push(アップロード) 以降の全ての作業が自動化できています。 またこれらの作業の中には作業者が負荷試験シナリオごとに設定を変えて繰り返す作業もありました。Gatling Commanderでは設定ファイルに複数の負荷試験シナリオに対応する値を指定できます。事前に複数のシナリオの値を指定することで、従来の負荷試験実施の方法で作業者が繰り返していた設定変更の作業は不要になりました。 このようにGatling Commanderを利用することで、負荷試験全体で作業者が要する工数を大幅に削減できました。作業者は一度Gatling Commanderのコマンドを実行すれば、全てのシナリオで負荷試験の実行が完了し、終了通知が来るまで負荷試験の実施を放置できるようになりました。 本節では、中でも特にGatling Commanderによる自動化の恩恵が大きかった部分について説明します。 負荷試験シナリオ切り替えの手間が不要になった 前節( 設定値の変更の手間 )の通り、従来の負荷試験実施ではシナリオごとの設定値の変更作業が作業者の大きな手間でした。 Gatling Commanderでは複数のシナリオごとの設定値を、1つの設定ファイルで記述できます。また負荷試験シナリオはservice単位でグループ化し、シナリオ間で共通する値の指定は共通化しています。serviceごとの負荷試験は並列で実行し、同一service内のシナリオごとの負荷試験は順次実行します。 事前に全ての負荷試験シナリオの設定値をGatling Commanderの設定ファイルに記述すると、Gatling Commanderは自動でシナリオごとの設定値を切り替えます。そのため従来発生していた作業者による設定値の変更作業は不要です。 負荷試験シナリオの切り替えの自動化により、作業者の工数を大きく削減できました。 負荷試験実行の状況確認の手間が不要になった 前節( 実行状況の確認の手間 )の通り、従来の負荷試験実施ではシナリオごとに作業者が実行状況を確認して、次のシナリオを実行していました。 Gatling Commanderは、作業者が確認していた次の点を自動で確認します。 前に実行した負荷試験が実行中か 実行した負荷試験の結果が閾値(目標レイテンシ、リクエストのFail数が0)を超えていないか 前節( Goroutinesによる並列処理の実装 )の通り、Gatling Commanderは実行中の負荷試験の終了を待機します。また前節( 過負荷時に負荷試験を自動停止 )の通り、事前に停止条件を指定することで過負荷時に負荷試験の順次実行を自動停止できます。Gatling Commanderが提供するこれらの機能により、従来の負荷試験で作業者が行っていた上記の点の確認作業は不要です。 例えばAPIの最大スループットを測る場合、従来の方法では作業者はAPIのレイテンシを見ながらその度にシナリオを変更して、負荷試験結果が閾値を超えないか確認していました。すべてのシナリオが完了するまでには最小でも 負荷試験の試行回数 x 負荷試験の実施時間 の時間が必要でした。試行回数が10回、実施時間が3分の場合、全ての負荷試験を合わせて最小でも30分かかります。実行状況の確認はこの間に頻繁に行う必要のある割り込み作業であり、作業者は全体で30分かかる待ち時間を効率的に使えていませんでした。 Gatling Commanderを利用すれば、従来の方法で作業者が行っていた実行状況の確認は不要になります。割り込み作業はなくなるため、作業者はすべての負荷試験シナリオが完了するまでの時間を最大限に活用し、他のタスクを進めることができます。 負荷試験の実行状況の確認作業の自動化により、作業者は複数のシナリオを持つ負荷試験の実行待ち時間を効率的に利用できるようになりました。 負荷試験結果の記録の手間が不要になった 前節( 結果の記録の手間 )の通り、従来の負荷試験実施ではシナリオごとに作業者が結果を記録していました。 Gatling Commanderは負荷試験の実行後に、負荷試験結果とコンテナメトリクスをGoogle Sheetsに自動記録します。この機能により、従来の負荷試験実施で作業者がシナリオごとに行なっていた記録作業は不要になります。 またGatling Commanderの負荷試験コマンド実行の終了後、作業者はすぐにシナリオごとの結果をGoogle Sheetsで一覧できるようになりました。 結果の記録作業は負荷試験実施の作業でも大きなトイルであったため、この点の自動化は作業者の工数削減に大きく貢献しました。 今後の展望 本節では今後の展望として、Gatling Commanderを応用したAPIパフォーマンスの劣化検知と、Gatling Commanderの機能追加・改善について説明します。 APIパフォーマンス劣化検知への利用 MLOpsブロックではGatling Commanderを利用したAPIパフォーマンスの劣化検知の仕組み作成を検討しています。 MLOpsブロックでは複数のAPIを本番運用していますが、短いスパンでの負荷試験実施はできていません。 新規開発や大きな機能変更の際には必ず負荷試験を実施し、日々の運用ではレスポンスタイムの監視も行なっています。しかしこれらの負荷試験の実施頻度は中・長期であり、依存ライブラリの更新など比較的小さな変更によるAPIパフォーマンスの変化は確認できていません。 劣化検知の仕組みを作成することで、APIのパフォーマンス悪化を早期に発見し、より安定して運用できるようにします。 劣化検知のためには負荷試験の定期的な実施が必要ですが、MLOpsブロックで運用するAPIは10以上あるため全てのAPIで負荷試験を実施する工数は大きいです。Gatling Commanderを利用することで、この工数を削減できるため、APIパフォーマンスの劣化検知の仕組みを作成できると考えています。 本節ではGatling Commanderを利用したAPIパフォーマンスの劣化検知の仕組みの構成案を簡単に説明します。 GitHub Actionsとの連携による負荷試験の定期実行 APIパフォーマンスの劣化検知の仕組みの構成案として、Gatling Commanderを利用した負荷試験を GitHub Actions でスケジュール実行することを考えています。 MLOpsブロックではCIツールとしてGitHub Actionsを利用しています。GitHub ActionsはGitHubが提供するCI/CDツールです。GitHub Actionsでは schedule イベントを利用することでスケジュールした時刻にGitHub Actionsのワークフローをトリガーできます。 Gatling CommanderはCLIツールであり、Goの実行環境があればインストール可能です。事前にGatlingのSimulation、Gatling Commanderの設定ファイルをリポジトリ内に用意します。GitHub Actionsのワークフローで、Gatling Commanderのコマンド実行時にこれらのファイルを指定して、負荷試験を実行できます。 このようにGatling CommanderのコマンドをCIでスケジュール実行することで、比較的簡単に定期的な負荷試験の実行が可能です。この構成案によりAPIパフォーマンスの劣化検知の仕組みを作成できると考えています。 機能追加・改善 本節では、Gatling Commanderで検討している今後の機能追加・改善について説明します。 Kubernetes以外で稼働するAPIへの対応 MLOpsブロックで運用するAPIはすべてKubernetes上で稼働しており、Gatling Commanderは負荷試験対象のAPIがKubernetes上で稼働する前提に実装されています。そのため本記事の公開時点でGatling Commanderは、Kubernetes以外の環境で動作するAPIの負荷試験に対応していません。 負荷試験ツールとしてより利用しやすくするために、任意のインフラで稼働するAPIを負荷試験対象にできるよう対応を検討中です。 Google Cloud以外のプロバイダへの対応 MLOpsブロックではGoogle Cloudのサービスを利用してインフラを構築しています。Gatling Commanderでは現状、Google Cloudが提供する Cloud Storage や Artifact Registry ・ Container Registry 以外のクラウドストレージ、レジストリサービスの利用に対応していません。そのため公開時点では Amazon Web Services (AWS)の S3 や Elastic Container Registry などGoogle Cloud以外のプロバイダで提供されるクラウドストレージ、レジストリサービスを利用できません。 今後の機能改善としてAWSなど他のプロバイダが提供するクラウドストレージ、レジストリサービスへの対応を考えています。 取得するメトリクスの拡充 現状Gatling Commanderが取得可能なメトリクスはコンテナのCPU・メモリ使用率のみです。自動で取得可能なメトリクスが増えることで、APIのパフォーマンス計測において作業者はより充実した情報を、工数をかけずに得ることができます。 既存のコンテナメトリクスに加えて、Google CloudのCloud MonitoringのようなリソースモニタリングやDatadogのようなアプリケーションのパフォーマンス監視の値を取得可能にする機能追加を検討中です。 まとめ 最後までお読みいただきありがとうございました。 本記事ではMLOpsブロックで抱えていた負荷試験実施の課題と、解決のために開発、公開したOSSのCLIツール、Gatling Commanderについてご紹介しました。 Gatling Commanderを利用して負荷試験を実施することで、従来の負荷試験で作業者が実施していた手作業を大幅に削減できました。本記事での事例の紹介が皆様のお役にたてば幸いです。またGatling CommanderではIssueやPull Requestを歓迎しています。ご興味のある方はぜひご利用ください。 最後になりますが、ZOZOでは一緒にサービスを作り上げてくれる方を募集中です。MLOpsブロックでも絶賛採用しているので、ご興味のある方は以下のリンクからぜひご応募ください。 hrmos.co corp.zozo.com
アバター
DevRelブロックの @ikkou です。もうすぐ2023年も終わりますね。皆さんは師走のイベント、「アドベントカレンダー」に参加しましたか? ZOZOは例年アドベントカレンダーに参加していて、2020年は計100本、2021年は計125本、2022年は175本と年々本数を増やしてきました。そして今年は昨年以上となる計225本の記事公開を“完走”しました。本記事ではその概要をお伝えします。 ZOZO Advent Calendar 2023 今年は計9個のカレンダーを完走し、12/1-25の期間中に合計225本の記事を公開しました。 ZOZO ADVENT CALENDAR 2023、無事完走しました! 今回はなんとシリーズ9まで、合計225件の記事を公開しました! 読んでくださったみなさま、ありがとうございました! https://t.co/2gxEcXIryx #zozo_engineer #Qiitaアドカレ #Qiita pic.twitter.com/4t9Njp4cEQ — ZOZO Developers (@zozotech) 2023年12月25日 qiita.com 実施概要 アドベントカレンダーは「任意参加」で実施しています。Slackチャンネルで実施と参加を呼びかけ、各自で空いている日に登録する運用となっています。 公開する先はZOZO TECH BLOGだけでなく、QiitaやZenn、noteや個人のブログなど自由です。ZOZO TECH BLOGよりも気軽に書けるという観点から、QiitaやZennに書く方が多いです。 1人で複数の記事を書くこともあるので、今年は225本の記事に対して123名が参加しました。昨年に続き100名以上が参加しています。ちなみにもっとも多くの記事を書いたのは @shiozaki で、全部で22本の記事を公開しています。 アドベントカレンダーはアウトプットの練習に適したイベントです。ZOZOではテックブログをアウトプットの主軸に置いていますが、「まだテックブログを書く自信が無い」「テックブログに書くにはネタが小粒」のような場合に、アドベントカレンダーは良い機会です。 2023年の振り返り ZOZOのアドベントカレンダーでは例年その年を振り返る記事を公開しています。 開発組織については昨年同様、執行役員 兼 CTOの @sonots が「振り返りと現状」記事を執筆しています。 qiita.com また、2021年からはコーポレート広報チームによる「ファッションテックハイライト」も公開しています。あわせてご覧ください。 technote.zozo.com 過去のアドベントカレンダー ZOZOでは、2018年から毎年アドベントカレンダーに参加しています。 ZOZO Advent Calendar 2022 qiita.com ZOZO Advent Calendar 2021 qiita.com ZOZOテクノロジーズ Advent Calendar 2020 qiita.com qiita.com qiita.com qiita.com ZOZOテクノロジーズ Advent Calendar 2019 qiita.com qiita.com qiita.com qiita.com qiita.com ZOZOテクノロジーズ Advent Calendar 2018 qiita.com qiita.com qiita.com 最後に ZOZOでは、プロダクト開発以外にも、アドベントカレンダーのような外部への発信も積極的に取り組んでいます。 一緒にサービスを作り上げてくれる方はもちろん、エンジニアの技術力向上や外部発信にも興味のある方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com
アバター
はじめに こんにちは! 24年度新卒内定者の石上です。11/21-22に開催された24年度新卒の内定者向け開発合宿に参加してきました! 内定者向けの開発合宿は4年ぶりの開催で、神奈川県の湯河原にて行われました。 今年は、24年度新卒エンジニア職内定者のうち参加を希望した学生と現役エンジニアの方が集まって開催されました。本記事では、開発合宿の様子と参加した感想についてレポートします! 集合 開催地の湯河原には、品川駅から踊り子号に乗って、約1時間で到着しました! 都心からのアクセスもよく、自然が豊かで温暖な気候なので、またプライベートでも訪れてみたいです。 宿 皆到着したら駅から送迎バスで宿に向かいます。丘の上にあるので、宿泊施設の中からきれいな湯河原の景色が見えました! 10分ほどで、今回宿泊するお宿、 レクトーレ湯河原 さんに到着です! 今回開催された宿は、研修施設として用いられており、研修室や大浴場などがありました。ロビーは吹き抜けになっており、開放感がありました。 自己紹介 人自 1 の門垣さんに開発合宿について説明してもらった後に、参加者の自己紹介をしました! みんな緊張しつつ開発合宿を楽しみにしていたようです。 みんなの自己紹介が終わったらお昼ごはんの時間です! 現場のエンジニアの方達だけでなく、全国から集まった内定者メンバーとも交流できました。 開発 こんにちは、24年度新卒内定者の岩崎です。 昼ごはんも食べ終わり、早速開発が始まりました。今回は「日常の課題を解決する」をテーマに、各々で事前に開発案を考えてきました。事前に開発を進めるのはNGだったので、各々0から開発を始めていきます。みなさんすごい切り替え&集中力で驚きました。 実装でわからない部分は内定者同士で相談し合ったり、先輩社員に尋ねたりしながら開発を進めていきました。 途中、人自の方から飲み物や軽食の差し入れも頂きました! ありがとうございました! ご飯 開発の合間に晩ご飯の時間が訪れました。とても豪華な食事が用意され、お酒も提供されました。 内定者と先輩社員がランダムに着席して、それぞれ色々な話をして盛り上がっていました。 開発の疲労を吹き飛ばすくらい美味しいご飯でした! 部屋 部屋の割り当ては、事前のアンケートをもとに1人部屋と4人部屋に分けられました。各部屋にはネット環境も整っているので開発もできます。 晩ご飯以降は自由時間だったので、誰かの部屋やフリースペースに集まって開発を続ける・温泉に入る・コンビニに行く・寝るなど、それぞれ自由に過ごしていました。 発表会 2日目は発表スライドを作り、すぐに発表会が始まりました。 1人5分程度で自分の成果物についてプレゼンします。発表順は1日目にくじ引きで決めました。 アプリやチャットBot、拡張機能など様々な方法で課題解決ができていてとても感心しました! 何よりみなさん短い時間にもかかわらず、しっかり動く物を創りきっていたのが本当に素晴らしいと思いました! 表彰式 こんにちは、24年度新卒内定者の松石です。 表彰は、内定者賞、先輩スタッフ賞、CPO賞、CTO賞がありました。各賞は以下の5項目をもとに選出されました! 成果物のアウトプット全体 課題解決できる手段になっているか 技術選定 ソウゾウのナナメウエ要素 挑戦しているか 発表を聞いて、誰が選ばれてもおかしくないなという印象でした。そのため、参加者はみんな緊張しつつもワクワクしている雰囲気がとてもよかったです! 受賞しなかったプロダクトもですが、受賞したプロダクトは、特に課題解決の手段としてわかりやすく、機械学習や今流行りの生成AIを用いたプロダクトが多い印象を受けました! 生成AIを活用したチャットアプリや、予算から1週間の献立を考える実用的なプロダクトなどがありました。特に、既存のデータから新しい価値を作り出しているプロダクトが表彰されていたように思います! 受賞者の5名からもコメントを頂いています! 内定者賞(桐島) 内定者賞の受賞ありがとうございます。多くの方に投票していただけて、とてもうれしいです! 今後も自分の好きなことをとことん追求し、がんばっていきたいと思います!! 先輩スタッフ賞1(Armin) このような機会また評価をいただき、本当に感謝しています。これからも皆さんと一緒に、人々の生活を少しでも良い方向に変えられるような開発をしていきたいと願っています。 先輩スタッフ賞2(田中) このような賞を頂けてとても光栄でした! これからも自分や、身近な人の課題を解決するような開発を続けていきたいです! CPO賞(畠中) 課題解決と世界観を重視して開発を行ったのでその点を評価いただき受賞出来たこと大変嬉しく思います! これからも日々進歩していきたいと思います! CTO賞(諸田) まさか賞をいただけると思っていなかったので、とてもとても嬉しいです! 企画していただきありがとうございました! 同期の発表を見て、技術の知識やアウトプットの量がまだまだ足りないなと感じたのでコツコツ経験を積んでいけたらなと思います! 懇親会 懇親会では、先輩社員と内定者を含めたグループに分かれて行われました! ここでは、ランチバイキングを楽しみながら開発合宿の感想を言い合ったり、自分がやっている研究や趣味などざっくばらんに話すことができました(疲れた体にビールが染みました!)。 解散 解散後は、バスで駅まで向かいましたが、バスの中で開発合宿の成果物や使用した技術などについて議論しながら帰っていました。駅到着後は、恒例のお土産物色を楽しみました! 感想 2日間とても楽しかったです! これまでも内定者と話す機会はありましたが、今回の開発合宿では、夜一緒に開発したり議論したりするメンバーが多かったです。そのため、これまでよりもコミュニケーションを多く取ることができ、内定者の仲が確実に深まりました! 自分は、賞を取れなかったため、どこかでリベンジしたいと思います! P.S. 2日後に内定者メンバーとフットサルをしに、京都から東京までいきました。全身筋肉痛。 お忙しい中、開発合宿を企画してくださった人自の方々をはじめ、来てくださったメンター社員の方々本当にありがとうございました! おわりに ZOZOでは、新卒エンジニアを募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com 当社では、「人事」のことを「人自」と表記します。「人事(ひとごと)」ではなく、スタッフ一人ひとりが他人の事も自分の事として考えられる会社にしたいという意味が込められています。 ↩
アバター
はじめに こんにちは。ブランドソリューション開発本部バックエンド部SREブロックの山岡( @ymktmk )です。 11/27〜12/1にラスベガスで開催されたAWS re:Invent 2023に、弊社から7名のエンジニアが参加しました。この記事では現地の様子とセッションについて紹介します! AWS re:Invent 2023とは 現地の様子 セッション紹介 おわりに AWS re:Invent 2023とは re:Invent はAmazon Web Services(AWS)が主催する大規模な技術カンファレンスです。 AWSが提供する様々なクラウドサービスに関する新サービスやアップデートが発表されます。参加者はセッションやワークショップを通じて、最新のクラウド技術に触れることができます。 内容は非常に多岐にわたり、コンピューティング、データベース、人工知能(AI)、機械学習(ML)、セキュリティ、開発ツールなど、さまざまな分野にわたるトピックが取り上げられます。 今年のre:Inventは、ChatGPTを中心とする生成AIが流行ったこともあり、その色がセッションや企業ブース、新サービス発表等に表れていました。 現地の様子 Keynoteでは、AWSの新サービスとしてAmazon Qが発表されました。 Amazon QはChatGPTのようなAIチャットボットで、AWSに関する質問をすると回答してくれるサービスです。まさに、生成AIと言わんばかりのサービスがリリースされましたね。 AWSの最新情報をいち早く知ることができるのも、re:Inventに参加する楽しみの1つだと思います。 ElastiCache Serverlessも今回の目玉アップデート。 会場内をAmazon S3 Express One Zone君が歩いていました。こちらも今回のアップデートの1つです。 こちらがExpoです。各企業のブースが設置されており、ノベルティを貰うことができます。国内ではまだまだ知られていないサービスを知る良い機会でした。 イベント期間中は朝食と昼食が提供されます。各会場で食事が用意されており、特に美味しい会場があるそうです。また、至る所に飲み物、軽食が提供されていました。 最終夜には「re:Play」というパーティーが開催されました。ライブステージやドッジボールなど音楽からアクティビティまで楽しめます。 re:Inventのオブジェと写真撮影できるコーナーがあったので集合写真を撮りました。 セッション紹介 ここからは各メンバーが気になったセッションなどを1つずつ紹介します。 Workshop Build a web-scale application with purpose-built databases & analytics (DAT403) Make applications highly resilient with AWS Fault Injection Service (FSI304) Chalk Talk A capability-oriented approach to defining your cloud architecture (ARC210) Breakout Session Dive deep into Amazon ECR (CON405) Gamified Learning AWS Jam: Security (sponsored by Fortinet and Palo Alto Networks) (GHJ205) Expo Firefly (Automatically turn any cloud into IaC) Workshop Build a web-scale application with purpose-built databases & analytics (DAT403) ブランドソリューション開発本部ZOZOMO部OMOバックエンドブロックの岡元です。 普段はZOZOTOWNとブランド実店舗をつなぐOMOプラットフォームである「ZOZOMO」のブランド実店舗の在庫確認・在庫取り置きサービスの開発、保守をしています。 このワークショップの目的は以下のとおりです。 The goal of this workshop is to understand that modern applications have a diverse set of access patterns, and the requirements imposed by those access patterns will determine which database can best serve those requirements. ワークショップではまず、完全に動作する書店アプリケーションに触れ、様々な機能が提供されていることを確認します。次に、不完全な書店アプリケーションにそれらの機能を実装していくことでAWSが提供する様々な目的別データベースについて学びます。 目的に適したデータベースを利用することでスケール、パフォーマンス、コストに関して妥協することなく機能を実現できます。ワークショップで提供されていた書店アプリケーションでは、以下の機能がそれぞれのデータベースで提供されていました。 機能 データベース 商品カタログ/ショッピングカート Amazon DynamoDB 注文処理 Amazon Aurora PostgreSQL 検索 Amazon Amazon OpenSearch Service 推薦 Amazon Neptune トップセラーリスト Amazon ElastiCache for Redis 個人的にはこれまでAmazon Neptuneに触る機会がなかったので、これに触ることができたのは新鮮でした。 また、我々が普段開発しているブランド実店舗の在庫確認・在庫取り置きサービスでも目的ごとにデータベースを使い分けています。アプリケーションへの更新リクエストを処理する記録のシステム(Systems of Record)にAmazon DynamoDBを、参照リクエストや集計リクエストを処理する導出データシステムにAmazon Auroraを利用しています。このワークショップを通して改めてAWSが提供している目的ごとのデータベースの豊富さを知り、今後のサービス拡充においては導出データシステムを構成するコンポーネントとしても幅広く活用できることを実感しました。 ワークショップの内容はこちらからも見ることができます。 catalog.workshops.aws また、ワークショップで使用したアプリケーションはGitHubでも公開されています。 github.com Make applications highly resilient with AWS Fault Injection Service (FSI304) ブランドソリューション開発本部ZOZOMO部OMOバックエンドブロックの木目沢です。 最近、カオスエンジニアリングという言葉を聞くことが多くなりました。 しかし、実践となると本番環境を壊す必要があるということで躊躇しているチームは多いと思います。 このワークショップはAWS Fault Injection Service(AWS FIS)を使って、事前に用意されたAPIを壊してみるというものです。そして結果を確認しながら実験テンプレート実施中でもAPIのリクエストが通るように環境を修正します。うまく修正できれば点数が加算され、分からず答えを見ると減点です。参加者全員で点数を競うというゲーミフィケーションが取り入れており、楽しいワークショップでした。 実験テンプレートは事前に用意されており、内容は以下のようなものでした。 1台しかないRDBのインスタンスを落とす あるAZへのALBの通信を落とす ECSのタスクを落とす AZの一部をすべて落とす それぞれの実験テンプレートを実行されながらもAPIが動作するようAWS環境を修正していきます。 今回実験テンプレートは用意されておりましたが、実際に自分のプロダクトで使う場合、実験テンプレートを作ること自体は難しくなさそうです。AWS Fault Injection Service(AWS FIS)を作成し、実行する流れは次の図のとおりです。 ただし、実際に使う場合は以下のような点を検討しておく必要があると感じました。 現在の構成を再確認し、どこをテストするのかを考え、実験テンプレートを作成する。 予期せぬことを検知するためにテストするのがカオスエンジニアリングですが、事前にできるだけ環境を壊しても問題ないよう対策を取っておく。 本番環境で実施する場合、サービスへの影響を最小限に抑えるための回復策を検討しておく。 検討事項は多いですが、AWSだからこそ容易に実施できるサービスですのでぜひ利用してみてください。 最後にre:Inventにおけるワークショップについて補足します。 私が参加したワークショップでは、資料・実施手順はすべてブラウザで確認できるものが用意されていましたのでGoogle翻訳などで確認しながら進めることが可能でした。講師に質問する場合など英語が必要な場面もありますが、ワークショップはYouTubeに公開されないという点もありますので、参加される方はぜひチャレンジしてみてください。 Chalk Talk A capability-oriented approach to defining your cloud architecture (ARC210) 技術本部SRE部ECプラットフォームサービスSREブロックの姫野です。 私はクラウドアーキテクチャに関するChalk Talkを紹介します。 Chalk Talkとは最大100名くらいが入れるre:Inventでは比較的小さいサイズの会場で講師2名が講義形式で行うセッションです。 前半はセッションのテーマに関するAWSサービス等についての概要をスライドを使って説明し、後半はホワイトボードを使ったりしながらより詳細な内容を深掘りしていきます。 このセッションの特徴は講師と参加者が活発にコミュニケーションを取ることです。 講師が説明した内容について、参加者が手を挙げて質問して答える、というやり取りが他のセッションに比べて頻繁に行われます(時には質問がたくさん出過ぎて講義が全く進まないことも…)。 当然、質問者と講師のやりとりは英語ですし、資料もないので、内容を理解するためにはある程度の英語力が必要になります。 私が紹介するChalk Talkのタイトルは「A capability-oriented approach to defining your cloud architecture」です。直訳すると「クラウドアーキテクチャを定義する能力指向アプローチ」です。 サービスに求められる能力(機能)の観点からAWSサービスの特徴にフォーカスしてアーキテクチャを考えようというアプローチです。 アーキテクチャを考える具体的な流れは以下の通りです。 サービスに求められる能力(機能)を明らかにする(例:ECサイトなら商品の検索機能・カート投入機能・決済機能等) それぞれの機能に求められる要件(特徴)を決める(例:パフォーマンス・スケーラビリティ・セキュリティ・コスト等) 候補となるAWSサービスの特徴を理解(抽出)して評価する 使用するAWSサービス(アーキテクチャ)を決める 実際にアーキテクチャを試して有効性を確認する ポイントとなるのは1と2で機能に求められる特徴を明確にして、3と4でその特徴に合致する特徴を持つAWSサービスを選ぶことです。 例えばECサイトの商品の検索機能を設計する際、この機能に求められる特徴は以下のようになったとします。 スケーラビリティ パフォーマンス 可用性 この機能における商品のメタデータを保管するデータストアを選ぶ際にはスケーラビリティとパフォーマンス、可用性を特徴とするDynamoDBを選ぶことが望ましいと言えます。なお、実際はこれ以外にも細かい要件があるはずなのでこんなに単純な話ではありません。 要は機能に優先的に必要な特徴とAWSサービスの特徴をよく理解してアーキテクチャを考えることが重要だと言っています。 当たり前と言えば当たり前な内容ですが、アーキテクチャの考え方を改めて言語化して整理できたセッションでした。 Chalk Talkの醍醐味は講師(そのセッションで扱うテーマやAWSサービスのプロフェッショナル)とのインタラクティブなコミュニケーションです。 良い質問がたくさん出ればその分内容も充実するため、参加者次第で満足度がより高くなる魅力的なセッションです。 内容を聞き取ったり質問するにはとにかく英語力は必要ですが、ぜひ一度トライしてみてください! Breakout Session Dive deep into Amazon ECR (CON405) 技術本部SRE部ECプラットフォーム基盤SREブロックの高塚です。 毎年re:Inventでは「Dive deep 〜」または「Deep dive 〜」のタイトルでAWSサービスの裏側を解説するセッションがたくさん開かれます。 私は今回 Amazon ECR の裏側について学んできました。 www.youtube.com ECR Registryは次のようなアーキテクチャです。 docker push などのコマンドをProxy Serviceが受け取り、内部用のAPI Callに変換します。イメージのタグやメタデータはDynamoDBに保存され、イメージのBlob(マニフェストや各レイヤー)はS3に保存されます。 youtu.be/PHuKrcsAaDw (5:53) また、すべての通信には認証・認可とスロットリングが適用されます。 youtu.be/PHuKrcsAaDw (7:24) PushとPullの両方について詳しい解説がありましたが、ここではPullのみ紹介します。Pullは全部で3ステップです。 youtu.be/PHuKrcsAaDw (18:51) クライアントは最初にマニフェストを要求します。タグ等を元にMetadata ServiceがDynamoDBを検索し、それを元にS3上のマニフェストを返します。 youtu.be/PHuKrcsAaDw (19:12) クライアントはマニフェストを見て必要なレイヤーをDigestで指定し要求します。ECRは先ほどと同様に、DynamoDBからレイヤーの保存場所を検索しますが、今度は 307 Redirect でS3の署名付きURLを返します。 youtu.be/PHuKrcsAaDw (20:01) あとはクライアントがレイヤーをS3から直接ダウンロードするだけです。 youtu.be/PHuKrcsAaDw (21:29) このほかに リージョン間の レプリケーション 脆弱性の スキャン 古いイメージを削除する ライフサイクルポリシー の3つの機能の仕組みが紹介されました。内部でLambdaやSQS、Inspectorなどが使われており、とても面白い内容でした! なお、アドベントカレンダーにてその他のセッションについてもレポートしています。よろしければお読みください! qiita.com Gamified Learning AWS Jam: Security (sponsored by Fortinet and Palo Alto Networks) (GHJ205) 技術本部SRE部ZOZOSREブロックの鈴木です。 普段はZOZOTOWNのSREエンジニアとして、ZOZOTOWNの裏で動いているオンプレミス環境の運用・保守をしつつAWSへの移行を進めています。また社内のAWS環境の管理者として全社にまたがるAWS環境の改善等々も行っています。 参加した「AWS Jam」について、体験できたことと得た学びについて紹介します。 Jamについて Jamの概要については下記リンクにて詳細が書かれていたため引用します。 AWS Jam とは、AWS re:Invent や AWS re:Inforce、AWS Summit などのグローバルで展開されているイベント、または AWS クラスルームトレーニング などで提供されている人気コンテンツの 1 つです。AWS のユースケースに沿って用意された様々なテーマの課題 (チャレンジと我々は呼んでいます) を解決していく実践型のイベントで、「AWS を楽しく学ぶ」ことができます。参加者はチームを組み、AWS やシステム開発の知識と経験を活用したりその場で調べたりしながら、与えられた複数のチャレンジを AWS マネジメントコンソールなどを利用してクリアしていきます。チャレンジごとに獲得点数やヒントが設定されており、時間内にクリアしたチャレンジと使用したヒントを総合して計算されたチームの得点を競い合います。 AWS Jam は、主に 3 つの目的で提供しています。 ・Play (遊ぶ): 得点を競うゲーミング形式のイベントを通じて、楽しみながら課題解決に挑戦します。チーム内でのコミュニケーションの促進にもつながります。 ・Learn (学ぶ): シナリオに沿った課題を解決することで、AWS サービスの知識やスキルを身につけていきます。普段扱っていない AWS のサービスや機能を新たに学んだり調べたりする機会にもなります。 ・Validate (検証する): 課題解決を通して、参加者自身の AWS サービスに対するスキルや理解度を確認できます。 2023 年 AWS Summit Tokyo で実施する AWS Jamのご案内 1チーム4人で与えられた課題を解いていく実践型のイベントです。各チームに与えられたAWS環境へアクセスして環境を修正、構築することで得点を得ることができます。 Jamにはテーマがあり、「DevOps」「GenAI」など様々中で、今回は「Security」がテーマのJamに参加しました。 4人でチームを組んでいくこともその場で出会ったメンバーと組むこともできます。日本人の参加者もちらほらおり、会場にて出会った日本人同士でチームを組んでいる方が多いように見えました。その場で会った海外エンジニアと組んで出ることもよい経験となりそうです。自分はちょうど知り合いがいたため日本人4人チームで参加しました。Jamのゲーム画面にはチーム内で連携をするためのチャット機能がついており、なんとこちらは自動翻訳がついていました。メンバーそれぞれの表示設定に合わせて翻訳されるためあまり言語の壁を感じる必要はなさそうです。 チャレンジの内容については今後参加される方が楽しめるように控えますが、様々なサービスに対するチャレンジが用意されていました。普段触ったことがあるサービスからそんなのあったのかというサービスまで用意されており、知っているものであれば復習に、未経験のものは新たに学ぶ機会となりました。 チャレンジによってはAWSのサービスのみならず、AWSのサービスと連携するsponsored by Fortinet and Palo Alto Networksなサービスを体験でき、新たに知ることができました。 解いた際の得点が減ってしまいますがClue(ヒント)が2つ用意されています。すべては見ていないですが1つ目は取っ掛かりを教えてくれ、2つ目は解法を教えてくれるような内容でした。解き方が分からず、詰まった際にはClueを見て手を動かすだけでも体験、学びがあったため、参加される際には恐れずにClueを使っていただきたいです。 AWS Security Jamでは一気に様々なサービスで検討すべきSecurityをGamifiedに楽しく知ることができました。AWSのサービスは日々増えており、キャッチアップだけでも大変ですがJamを通して検討、気をつけるべき箇所の手札を増やすことに繋がったことが大きな収穫でした。この規模であるかはわかりませんが、日本でJamが開催されることも、 AWS スキルビルダーページ からJamの体験もできるようなのでぜひ一度体験してみてください。 Expo Firefly (Automatically turn any cloud into IaC) 生産プラットフォーム開発本部生産プラットフォーム開発部生産開発ブロックの八代です。 私からはExpo内で気になったサービスがあったためご紹介します。Expoでは日本であまり見かけないサービスが多く展示されていました。特にAI技術を前面に押し出している企業を多く見かけました。 そんな中で私が特に注目したのは、「 Firefly.ai 」というサービスです。この記事では、Firefly.aiの特徴を紹介します。 Fireflyの概要 Fireflyは、AWSやGCPなどの主要クラウドプロバイダーでのインフラリソース管理を強化するSaaSサービスです。このサービスは、TerraformなどのIaCを使用してクラウドインフラのリソースを管理すると共に、GitHub連携を通じて、クラウド上で管理されていないリソースをコード化できます。また、TerraformやCloudFormationなどを管理画面上から実行しリソースを作成する機能も備えているため、このサービス上でリソース管理などを完結できます。 主な特徴と機能 リソースの可視化とコード化 クラウド上で見過ごされがちなリソースを可視化し、IaCに変換してくれます。 プルリクエストの自動生成 クラウド上で作成されたリソースとコード管理されているものに差異が発生した際に、対象リソースに対して自動的にIaC化したコードのプルリクエストを出してくれます。 マルチクラウド対応 AWSやGCPなど複数のクラウドプラットフォームに対応し、一元管理できます。 AIを活用したコード生成 AI技術を利用して、より効率的かつ精度の高いコード生成ができます。 ユーザーインタフェースと体験 製品デモを体験した際、サービスの導入から運用までのプロセスが非常にスムーズであることが印象的でした。管理画面も直感的でわかりやすいユーザーインタフェースだったので、初心者でも容易に扱える設計がされていました。 感想 Fireflyは、事業拡大に伴いインフラ管理が複雑化している企業に最適なサービスだと感じています。AIによるコード生成の精度に関しては導入してみないとわかりませんが、既存のリソースをコード化する「Terraformer」や「Former2」などのサービスと同等以上だと思います。まだ日本企業で導入している事例は見かけていませんが、その機能性と利便性から、今後大きな注目を集めるサービスになると思います。 おわりに re:Inventは私にとって初めての海外カンファレンスで、その経験は非常に特別でした。KeynoteやBreakout Sessionを通じて、世界の最先端技術を学ぶことで新たな視点やアイデアを得ることができました。 さらに、日本では経験できない規模のカンファレンスで、そのスケールに圧倒されました。現地のセッションの一部はYouTubeを通じてオンラインで視聴できますが、現地にいるからこそ感じる雰囲気や直接のコミュニケーションでしか得られない貴重な経験がたくさんあります。 今回の経験から得た知見を社内外に共有し、これからもAWSを使ってビジネスを拡大していきます。 ZOZOではAWSが大好きなエンジニアを募集しています。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com corp.zozo.com
アバター
はじめに こんにちは、MLデータ部データ基盤ブロックの仲地です。初めてのテックブログへの投稿になります。主に業務ではデータ基盤の開発・運用を担当しています。 データ基盤ブロックではELTツールである Airbyte を導入し、一部のデータ転送パイプラインをリプレイスしました。本記事ではそのAirbyteの構築方法と運用するにあたって工夫した点を紹介します。 目次 はじめに 目次 背景 Airbyte OSS Connectorの豊富さ ETLではなくEL(T) コミュニティが活発 GCP上でAirbyteを構築 全体構成 Terraform Kubernetesのマニフェスト KubernetesのSecret Kubernetesのデプロイ 工夫した点 GKE上での構築 Airflowによるスケジュール実行 MinIOを用いない PVCのAccessModeの変更 ServiceAccountの権限借用 導入後の課題点 Helm化 データ転送パイプラインの設定情報の反映 Cloud Loggingへのログ出力 まとめ 背景 現在、当社のデータ基盤には、自社運用の基幹DBからのデータ連携だけでなく、使用中のSaaSからのデータ連携も含まれています。基幹DBからのデータ連携やデータ基盤については、以前のブログに詳細があります。 techblog.zozo.com 基幹DBからのデータ連携以外にも、各業務で必要なSaaSにおけるデータを、データ基盤へ連携してほしいという要望がありました。SasSのデータ連携において、Airybte導入以前に用いていたDMP (Data Management Platform)は必要な機能以上のスペックが備わっていました。例えばそのDMPで行えるデータ転送において、任意のカラムによるフィルタやキーワードによる抽出条件の変更などができました。弊社におけるデータ基盤において、エンドユーザーである利用者が用いる分析基盤はBigQueryであり、そのDMPからBigQueryへのデータ転送が実施されていました。そしてこの転送プロセスにおいて抽出条件などを設けていたことで、日々のデータ転送時に欠損が生じたときに調査を困難にしていた課題などがありました。そのDMPは分析基盤として優秀ですが、BigQueryをデータ分析基盤と固めた今、その中間処理は複雑化する一因になっていました。またそのサービスを活かしきれてなく金銭的コストもかかっていました。不要な中間処理を省き、データ転送パイプラインの簡素化が求められていました。 Airbyte Airbyte はOSSなELTツールです。数多くあるELTツールの中でも、今回Airbyteを選択した理由を軸にAirbyteを紹介します。 OSS 選択の理由の1つとして、AirbyteがOSSであることです。OSSは柔軟で拡張可能なフレームワークやプラットフォームを提供しています。これにより、自身のニーズに合わせてソフトウェアをカスタマイズし拡張できます。またソースコードが公開されているため、OSSは透明性が高いです。バグや脆弱性を自身で発見し、コミュニティを通じて迅速に対応できます。そのほかにもOSSの採用は、ベンダーロックインから解放される手段となります。システムのアップデートやバグの対応、セキュリティパッチなどはベンダー依存になり、ベンダーロックインの懸念がありましたが、OSSであれば自身で対応できます。 Connectorの豊富さ 次の理由はConnectorの豊富さです。データ取得元であるデータソースが豊富であり、また転送先であるDWHやストレージなどが多いという特徴があります。実際のデータソースや転送先のサービス一覧は、下記のドキュメントをご覧ください。 airbyte.com 今回、移行前に連携していたSaaSが全てAirbyteのConnectorとして既に実装済みだったのも選択理由の1つでした。 ETLではなくEL(T) AirbyteはEL(T)ツールだと自身で説明しています。詳しくは下記の記事をご覧ください。 airbyte.com 従来よく使われていたデータ転送パイプラインは、データ取得元であるデータソースからデータを取得し、そのデータの中間処理を行い、宛先テーブルに注入するという流れでした。いわゆるETL(Extract, Transform, Load)です。しかし近年では、クラウド環境におけるデータウェアハウスの発展が著しく、データのサイズを懸念せずにデータを保持することが容易になりました。弊社では基幹DBからのデータ連携は一度、BigQueryに未加工のデータを取り込み、マシンパワーが強いBigQuery上でデータを加工しています。この方針にもAirbyteのEL(T)ツールは適していました。 コミュニティが活発 コミュニティが活発であることも選択理由の1つです。AirbyteのコミュニティはSlackやGitHubのIssueなどで活発に議論が行われています。また、Airbyteの開発チームもコミュニティに参加しており、Issueの対応やPull Requestのレビューなどを行っています。私自身もAirbyteのSlackに参加し、構築時に発生した問題の質問や、機能の追加要望などを行いました。その際には開発チームからの返答もあり、コミュニティの活発さを実感しました。AirbyteのGitHubのリポジトリを見ると、GitHubのスター数やコミット数なども多く、今後も機能の追加やバグの修正が期待できます。 github.com GCP上でAirbyteを構築 全体構成 Airbyteの構築にあたって、GCP上のリソースを下記図のように構築しました。 以下が各コンポーネントの概説です。 GKE : Airbyteのコンテナをデプロイするために使用しました。本記事ではGKEのAutopilotを使用しています。 IAP : GKE上のAirbyteのWeb UIへアクセスするために使用しました。ユーザーレベルの制御を行えます。 Cloud SQL : Airbyteのメタデータを保存するために使用しました。可能な限り、コンテナをステートレス化したかったため使用しました。 Cloud Storage : Airbyteのログを保存するために使用しました。Cloud SQLと同様に、コンテナをステートレス化するために使用しました。 Cloud Composer : Airbyteのデータ転送パイプラインのスケジューリングを行うために使用しました。本記事では構築方法は紹介しません。 次節より詳しい構築方法を紹介します。 Terraform GCP上のリソースを管理するために、Terraformを用いました。Terraformのコードを下記に紹介します。 Network関連 #--------------------------# # data #--------------------------# data "google_compute_network" "vpc" { name = "vpc-$ { local.project } " project = local.project } data "google_dns_managed_zone" "zone" { name = "zozo-zone" } #--------------------------# # Cloud NAT #--------------------------# resource "google_compute_router" "vpc_router" { name = "$ { local.project } -vpc-router" region = local.region network = data.google_compute_network.vpc.self_link } resource "google_compute_address" "nat_ip" { name = "$ { local.project } -nat-ip" region = local.region } resource "google_compute_router_nat" "nat_gateway" { name = "$ { local.project } -nat-gateway" router = google_compute_router.vpc_router.name region = local.region nat_ip_allocate_option = "MANUAL_ONLY" nat_ips = [ google_compute_address.nat_ip.self_link ] source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES" } #--------------------------# # Subnet #--------------------------# resource "google_compute_subnetwork" "airbyte" { name = "subnet-$ { local.project } -airbyte" project = local.project region = local.region network = data.google_compute_network.vpc.id private_ip_google_access = true ip_cidr_range = local.subnet_cidr_range_airbyte secondary_ip_range { range_name = "pods" ip_cidr_range = local.subnet_pods_secondary_cidr_range_airbyte } secondary_ip_range { range_name = "services" ip_cidr_range = local.subnet_services_secondary_cidr_range_airbyte } } resource "google_compute_subnetwork" "subnet_proxy_only" { name = "subnet-$ { local.project } -proxy-only" project = local.project region = "us-central1" network = google_compute_network.vpc.id ip_cidr_range = local.subnet_cidr_range_proxy_only purpose = "REGIONAL_MANAGED_PROXY" role = "ACTIVE" } #--------------------------# # Firewall #--------------------------# resource "google_compute_firewall" "firewall_proxy_connection" { name = "firewall-proxy-connection-$ { local.project } " project = local.project network = google_compute_network.vpc.name allow { protocol = "tcp" # Now, Internal Load Balancer is used in airbyte only. So, the following port is specified. ports = [ "8001" ] } source_ranges = [ local.subnet_cidr_range_proxy_only ] } 既に存在するVPCに対して、SubnetやFirewallの設定しています。また、GKE上で外部へ通信するために、Cloud NATを設定しています。連携するSaaSにおいてIPアドレスの制限があったため、Cloud NATのIPアドレスは固定IPアドレスを設けました。 GKE #--------------------------# # GKE Cluster(Autopilot) #--------------------------# resource "google_container_cluster" "airbyte" { name = "airbyte-cluster" enable_autopilot = true location = local.region network = data.google_compute_network.vpc.self_link subnetwork = google_compute_subnetwork.airbyte.self_link networking_mode = "VPC_NATIVE" ip_allocation_policy { cluster_secondary_range_name = "pods" services_secondary_range_name = "services" } private_cluster_config { enable_private_nodes = true enable_private_endpoint = false master_ipv4_cidr_block = local.gke_master_ipv4_cidr_block master_global_access_config { enabled = true } } # ref https://qiita.com/inductor/items/e60be2b1b33347dc0c21 maintenance_policy { recurring_window { start_time = "2020-05-05T20:00:00Z" # 05:00 JST end_time = "2020-05-06T00:00:00Z" # 09:00 JST recurrence = "FREQ=WEEKLY;BYDAY=MO,TU,WE,TH" } } release_channel { channel = "STABLE" } } GKEはAutopilotを使用しています。AutopilotはGKEのマネージドサービスであり、マスタノードの管理やノードプールの管理などをGCPが行ってくれます。また、GKEのバージョンアップなどもGCPが行ってくれます。GKEの管理にかかるコスト削減を図ります。 Cloud SQL関連 #--------------------------# # Cloud SQL #--------------------------# # private services access resource "google_compute_global_address" "private_ip_alloc_google_managed_service" { name = "google-managed-services-$ { data.google_compute_network.vpc.name } " purpose = "VPC_PEERING" address_type = "INTERNAL" prefix_length = tonumber ( element ( split ( "/" , local.cidr_google_managed_services), 1 )) network = data.google_compute_network.vpc.id address = element ( split ( "/" , local.cidr_google_managed_services), 0 ) } resource "google_service_networking_connection" "private_service_connection_google_managed_service" { network = data.google_compute_network.vpc.id service = "servicenetworking.googleapis.com" reserved_peering_ranges = [ google_compute_global_address.private_ip_alloc_google_managed_service.name ] } resource "google_sql_database_instance" "airbyte_primary" { name = "db-airbyte" region = local.region database_version = "POSTGRES_14" settings { tier = local.postgre_instance_type ip_configuration { ipv4_enabled = false private_network = data.google_compute_network.vpc.id } backup_configuration { point_in_time_recovery_enabled = true enabled = true start_time = "17:00" # JST:02:00 } availability_type = "REGIONAL" } depends_on = [ google_service_networking_connection.private_service_connection_google_managed_service ] } resource "google_sql_database" "airbyte" { name = "db-airbyte" instance = google_sql_database_instance.airbyte_primary.name } resource "google_sql_user" "airbyte_user" { name = "airbyte_k8s" instance = google_sql_database_instance.airbyte_primary.name # NOTE: ダミーのパスワードを設定しておき、後から手動で変更する password = "DummyPassword" lifecycle { ignore_changes = [ password, ] } } Airbyteの公式の構築手順では、Kubernetesのコンテナ内にPostgreSQLを構築をしていますが、本記事ではコンテナをステートレス化するためにCloud SQLを使用しました。Cloud SQLは管理が容易で、データベースのパフォーマンスの監視やメンテナンスがクラウドプロバイダによって自動的に行われます。またCloud SQLは定期的に自動バックアップを行い、必要に応じてこれを使用してデータベースを復元できます。これらのマネージドサービスのメリットを授かり、運用にかかるコスト削減を試みました。 また、GKE上からCloud SQLのプライベートIPアドレスで接続するために、プライベートサービスアクセスを構築しています。詳しくは下記のドキュメントをご覧ください。 cloud.google.com cloud.google.com Compute Address, Cloud DNS, ManagedCertificate #--------------------------# # Compute Address #--------------------------# resource "google_compute_global_address" "airbyte_webapp" { name = "$ { local.project } -airbyte-webapp" } resource "google_compute_address" "airbyte_server_internal_address" { name = "$ { local.project } -airbyte-server" subnetwork = google_compute_subnetwork.airbyte.id address_type = "INTERNAL" region = "us-central1" } #--------------------------# # Cloud DNS #--------------------------# resource "google_dns_record_set" "airbyte_webapp" { name = "airbyte.$ { data.google_dns_managed_zone.zone.dns_name } " managed_zone = data.google_dns_managed_zone.zone.name type = "A" ttl = 300 rrdatas = [ google_compute_global_address.airbyte_webapp.address ] } resource "google_dns_record_set" "airbyte_server_internal" { name = "internal.airbyte-server.$ { google_dns_managed_zone.zone.dns_name } " managed_zone = google_dns_managed_zone.zone.name type = "A" ttl = 300 rrdatas = [ google_compute_address.airbyte_server_internal_address.address ] } #--------------------------# # ManagedCertificate #--------------------------# resource "google_compute_managed_ssl_certificate" "airbyte_webapp_cert" { name = "$ { local.project } -airbyte-webapp-managed-cert" managed { domains = [ "airbyte.$ { data.google_dns_managed_zone.zone.dns_name } " ] } } Compute Address, Cloud DNS, ManagedCertificateを設定しています。Compute Addressは、GKE上のIngressで使用します。1つは、AirbyteのWeb UIへアクセスするために使用し、もう1つは、Cloud ComposerからAirbyteのAPIへアクセスするために静的内部IPアドレスを設定しています。詳しくは、 Airflowによるスケジュール実行 節にて紹介します。Cloud DNSは、Compute Addressの名前解決のために使用します。ManagedCertificateは、Compute Addressに対して証明書を発行するために使用します。 IAP #--------------------------# # IaP #--------------------------# resource "google_iap_brand" "iap_brand" { support_email = "{自身の適当なメールアドレス}" application_title = "Cloud IAP for $ { local.project } " } resource "google_iap_client" "airbyte_iap_client" { display_name = "Airbyte OAuth Client" brand = google_iap_brand.iap_brand.name } IAPは、AirbyteのWeb UIへアクセスするために使用します。ここで弊社が用いているVPNのIP制限によるアクセス制御も検討しましたが、ユーザーレベルの制御が行いやすい、GCPのIAPを用いました。IAPはGCPのリソースに対して認証する機能で、GCPの認証情報を持つユーザーのみがアクセスできるようになります。 Cloud Storage #--------------------------# # Cloud Storage #--------------------------# resource "google_storage_bucket" "airbyte_log" { name = "$ { local.project } -airbyte-log" location = "US" uniform_bucket_level_access = true } resource "google_storage_bucket" "airbyte_bq_staging" { name = "$ { local.project } -airbyte-bq-staging" location = "US" uniform_bucket_level_access = true } Airbyteのログを保存するために、Cloud Storageを使用しました。また、BigQueryへのデータ転送の際に一時的にデータを保存するために、Cloud Storageを追加しています。 Service Accountと権限関連 #--------------------------# # Service Account (IAM) #--------------------------# resource "google_service_account" "airbyte_app" { account_id = "airbyte-app" display_name = "Service Account for Airbyte Application" } resource "google_project_iam_member" "airbyte_app_bq_data_editor" { role = "roles/bigquery.dataEditor" member = "serviceAccount:$ { google_service_account.airbyte_app.email } " project = local.project } resource "google_project_iam_member" "airbyte_app_bq_user" { role = "roles/bigquery.user" member = "serviceAccount:$ { google_service_account.airbyte_app.email } " project = local.project } resource "google_storage_bucket_iam_member" "airbyte_app_storage_admin_log" { bucket = google_storage_bucket.airbyte_log.name role = "roles/storage.admin" member = "serviceAccount:$ { google_service_account.airbyte_app.email } " } resource "google_storage_bucket_iam_member" "airbyte_app_storage_admin_bg_staging" { bucket = google_storage_bucket.airbyte_bq_staging.name role = "roles/storage.admin" member = "serviceAccount:$ { google_service_account.airbyte_app.email } " } resource "google_iap_web_iam_member" "airbyte_app_access_service_account" { role = "roles/iap.httpsResourceAccessor" member = "serviceAccount:$ { google_service_account.airbyte_app.email } " } resource "google_iap_web_iam_member" "airbyte_app_access_members" { for_each = toset (local.airbyte_app_access_members) role = "roles/iap.httpsResourceAccessor" member = each.value } # Bind GSA to KSA resource "google_service_account_iam_member" "airbyte_app_k8s_iam_default" { service_account_id = google_service_account.airbyte_app.name role = "roles/iam.workloadIdentityUser" member = "serviceAccount:$ { local.project } .svc.id.goog[default/default]" } resource "google_service_account_iam_member" "airbyte_app_k8s_iam_airbyte_admin" { service_account_id = google_service_account.airbyte_app.name role = "roles/iam.workloadIdentityUser" member = "serviceAccount:$ { local.project } .svc.id.goog[default/airbyte-admin]" } AirbyteのコンテナからGCPのリソースへアクセスするために、Service Accountを追加と権限の借用の設定をしています。ここで、2つのKubernetes Service Account (以下、KSA)に対して、同一のGoogle Service Account (以下、GSA)の権限を借用する設定をしています。 ServiceAccountの権限借用 節で詳しく説明します。 また、AirbyteのWeb UIへアクセスするために、特定のGoogle Accountのみに対して iap.httpsResourceAccessor のロールを付与しています。同様にサービスアカウントにも同じロールを付与していますが、 データ転送パイプラインの設定情報の反映 節において紹介する認証トークンを取得するために追加しています。 次に各環境に依存する変数を定義した、 locals のみを定義したファイルを紹介します。 local.tf locals { project = "airbyte-project-prd" region = "us-central1" subnet_cidr_range_airbyte = "{SubnetのCIDR}" subnet_pods_secondary_cidr_range_airbyte = "{Pods SecondaryのCIDR}" subnet_services_secondary_cidr_range_airbyte = "{Services SecondaryのCIDR}" gke_master_ipv4_cidr_block = "{GKE MasterのCIDR}" cidr_google_managed_services = "{Google Managed ServicesのCIDR}" postgre_instance_type = "db-custom-1-3840" # AirbyteのWeb UIにアクセス可能なGoogle Accountのメールアドレス airbyte_app_access_members = [ "user:example@example.com" ] } Kubernetesのマニフェスト いくつかAirbyteオリジナルのKubernetesのKustomizeを変更したので、その設定例を紹介します。また詳しくは Helm化 節で紹介しますが、Kubernetesのデプロイは現在Helmを使った方法が推奨されています。本記事ではKustomizeを用いている、Airbyteの最終Versionである v0.40.32のkubeディレクトリ との差分を紹介します。 最初にディレクトリ構成の紹介します。 kube ├── overlays │   ├── dev │   │   ├── dev.yaml │   │   ├── kustomization.yaml │   │   └── set-resource-limits.yaml │   └── prd │      ├── kustomization.yaml │      ├── prd.yaml │      └── set-resource-limits.yaml └── resources ├── admin-service-account.yaml ├── bootloader.yaml ├── connector-builder-server.yaml ├── cron.yaml ├── default-service-account.yaml ├── kustomization.yaml ├── pod-sweeper.yaml ├── secret-gcs-log-creds.yaml ├── server.yaml ├── temporal.yaml ├── volume-configs.yaml ├── webapp.yaml └── worker.yaml 公式の stable-with-resource-limits を参考にしながら、各環境に合わせて変更を加えています。 最初に各環境共通であるresourcesディレクトリ内のマニフェストの差分設定を紹介します。 kustomization.yaml apiVersion : kustomize.config.k8s.io/v1beta1 kind : Kustomization resources : - bootloader.yaml - connector-builder-server.yaml - cron.yaml - pod-sweeper.yaml - admin-service-account.yaml - default-service-account.yaml - server.yaml - temporal.yaml - volume-configs.yaml - webapp.yaml - worker.yaml 公式のkustomization.yaml と比較するとDB関連とMinIOのマニフェストファイルを反映していません。DBはCloud SQLを使用するため、MinIOを本記事では使用しないためです。またSecretはマニフェストで管理せず、KubernetesのCLI Secretを用いているので除外しています。 webapp.yaml kind : Service metadata : name : airbyte-webapp-svc annotations : cloud.google.com/backend-config : '{"default": "airbyte-webapp-backend-config"}' ... # この間は変更ないので省略 ... --- apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : airbyte-webapp-ingress annotations : kubernetes.io/ingress.class : "gce" kubernetes.io/ingress.allow-http : "false" spec : defaultBackend : service : name : airbyte-webapp-svc port : number : 80 --- apiVersion : cloud.google.com/v1 kind : BackendConfig metadata : name : airbyte-webapp-backend-config spec : iap : enabled : true oauthclientCredentials : secretName : airbyte-iap-client-secrets timeoutSec : 300 特定のドメインを紐づけるために、Ingressを追加しました。また、IAPによる認証のために、 BackendConfig に iap の設定を追加しています。secretNameについては KubernetesのSecret 節にて紹介します。 cloud.google.com server.yaml kind : Service metadata : name : airbyte-server-svc annotations : cloud.google.com/neg : '{"ingress": true}' cloud.google.com/backend-config : '{"default": "airbyte-server-backend-config"}' ... # この間は変更ないので省略 ... --- apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : airbyte-server-ilb-ingress annotations : kubernetes.io/ingress.class : "gce-internal" spec : defaultBackend : service : name : airbyte-server-svc port : number : 8001 --- apiVersion : cloud.google.com/v1 kind : BackendConfig metadata : name : airbyte-server-backend-config spec : timeoutSec : 300 healthCheck : type : HTTP requestPath : /api/v1/health # NOTE : 変わる可能性がある port : 8001 --- 同一のVPCに存在するCloud ComposerからAirbyteのAPIを叩くために、内部Ingressを追加しました。導入の経緯については Airflowによるスケジュール実行 節にて紹介します。 cloud.google.com volume-configs.yaml apiVersion : storage.k8s.io/v1 kind : StorageClass metadata : name : airbyte-storage-class provisioner : filestore.csi.storage.gke.io volumeBindingMode : Immediate allowVolumeExpansion : false --- apiVersion : v1 kind : PersistentVolumeClaim metadata : name : airbyte-volume-configs labels : airbyte : volume-configs spec : accessModes : - ReadWriteMany storageClassName : airbyte-storage-class resources : requests : storage : 500Mi こちらのリソースの変更については、 PVCのAccessModeの変更 節にて説明します。 default-service-account.yaml apiVersion : v1 kind : ServiceAccount metadata : name : default こちらの追加リソースは、 ServiceAccountの権限借用 節にて説明します。 次に環境ごとに依存するoverlayディレクトリ内の差分設定を、prd環境を例に紹介します。 kusomization.yaml apiVersion : kustomize.config.k8s.io/v1beta1 kind : Kustomization namespace : default bases : - ../../resources images : - name : airbyte/db newTag : 0.40.32 - name : airbyte/bootloader newTag : 0.40.32 - name : airbyte/server newTag : 0.40.32 - name : airbyte/webapp newTag : 0.40.32 - name : airbyte/worker newTag : 0.40.32 - name : temporalio/auto-setup newTag : 1.13.0 - name : airbyte/cron newTag : 0.40.32 - name : airbyte/connector-builder-server newTag : 0.40.32 configMapGenerator : - name : airbyte-env env : .env patchesStrategicMerge : - set-resource-limits.yaml - prd.yaml 公式のkustomization.yaml と比較すると、Secretはマニフェストでコード管理せず、KubernetesのCLI Secretを用いているので secretGenerator を除外しています。また、 patchesStrategicMerge にて prd.yaml を追加しています。これはprd環境でのみ適用する設定を記述するためです。 prd.yaml apiVersion : v1 kind : ServiceAccount metadata : name : airbyte-admin annotations : iam.gke.io/gcp-service-account : airbyte-app@airbyte-project-prd.iam.gserviceaccount.com --- # Note:The following are deprecated on Airbyte. However, since this project uses a cluster for Airbyte only, we will also bind Google Service Account to the k8s default account. # https://github.com/airbytehq/airbyte/pull/11697 apiVersion : v1 kind : ServiceAccount metadata : name : default annotations : iam.gke.io/gcp-service-account : airbyte-app@airbyte-project-prd.iam.gserviceaccount.com --- apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : airbyte-webapp-ingress annotations : kubernetes.io/ingress.global-static-ip-name : airbyte-project-prd-airbyte-webapp ingress.gcp.kubernetes.io/pre-shared-cert : airbyte-project-prd-airbyte-webapp-managed-cert --- apiVersion : networking.k8s.io/v1 kind : Ingress metadata : name : airbyte-server-ilb-ingress annotations : kubernetes.io/ingress.regional-static-ip-name : airbyte-project-prd-airbyte-server --- apiVersion : storage.k8s.io/v1 kind : StorageClass metadata : name : airbyte-storage-class parameters : tier : standard network : vpc-airbyte-project-prd 実際の各環境に依存する設定を記述しています。 airbyte-admin と default のKSAに、GSAを権限借用するためにアノテーションを追加しています。 ServiceAccountの権限借用 節にて目的を説明します。 また、 airbyte-webapp-ingress では、下記の記事を参考にTerraformで作成したGlobal IPとManagedCertificateを紐付けています。 qiita.com airbyte-storage-class は、PersistentVolumeのStorageClassを指定しています。こちらの設定については PVCのAccessModeの変更 節にて説明します。 KubernetesのSecret 公式のKubernetesのマニフェストではSecretをマニフェストで管理していますが、秘密情報をコード管理することはセキュリティリスクがあるため、この方法は見送りました。代替案としてKubernetesのCLIを用いたので、その方法を紹介します。実際には以下の3つのSecretを作成しました。 airbyte-secrets 公式の kustomization.yaml 内の secretGenerator で生成されるSecretです。主にDBへ接続するための情報が含まれています。設定Keyが多いので下記コマンドでは省略していますが、 .secret で確認できます。 kubectl create secret generic airbyte-secrets --from-literal=DATABASE_USER=airbyte_k8s --from-literal=DATABASE_PASSWORD=airbyte --from-literal=... gcs-log-creds 公式の secret-gcs-log-creds.yaml に該当するSecretです。AirbyteのログをGCSに出力するための認証情報が含まれています。 kubectl create secret generic gcs-log-creds --from-literal=gcp.json='{認証情報のjsonファイルの中身}' airbyte-iap-client-secrets こちらはIAPによる認証のための認証情報が含まれています。 kubectl create secret generic airbyte-iap-client-secrets --from-literal=client_id={client_id} --from-literal=client_secret={client_secret} Kubernetesのデプロイ ここまででKubernetesのマニフェストとSecretの作成が完了したので、実際にデプロイを行います。 GKEの認証情報を取得後 に、下記コマンドを実行します。 kubectl apply -k kube/overlays/prd デプロイ後、DNSの設定で紐付けたドメインにアクセスすると、AirbyteのWeb UIが表示されます。 工夫した点 実際の運用にあたって直面した課題と、それを解決した工夫点をご紹介します。 GKE上での構築 本記事では、Airbyteの構築はKubernetesをベースとしたマネージドサービスであるGKE上に構築しました。AirbyteはDockerが動作する環境、例えばGCE上などに簡単に構築ができます。また内部にTemporalというワークフローエンジンがあり、タスクの実行時はDocker Composeの場合ならコンテナを、Kubernetes環境の場合ならPodのコンテナを並列に稼働させます。しかし単一ノードで稼働するDocker Composeではデータ転送タスクが増えるごとにマシンリソースを意識しなければならなく、運用するにあたってリソース管理が懸念されました。そこで本記事は、Kubernetesをマネージドに提供するGKE上で構築することで、インフラの管理コストの低減を図りました。 Airflowによるスケジュール実行 Airbyteのスケジュール実行には、Airbyteの機能を用いる方法と、Airflowなどのスケジューラーを用いる方法があります。本記事では、Airflowを用いる方法を選択しました。理由としては、弊社のデータマート基盤として既にCloud Composerを使用しており、運用の都合がよかったためです。またAirbyteによる転送後のデータ加工のためにAirflowの機能を用いることで、Airbyteの機能を用いるよりも柔軟にデータ加工を行えると考えました。Cloud Composerに関する情報は下記の記事をご覧ください。 techblog.zozo.com AirflowにおけるAirbyteのオペレーターを用いたタスク実行は、下記の公式のドキュメントを参考にしました。 docs.airbyte.com AirflowからAirbyteのスケジュールを行うには、Airbyte ServerのHost情報などを含む、Connectionの設定が必要です。構築初期の段階では、Web UIをホスティングしている webapp Podに対してのみ、外部Ingressを追加していました。しかし、AirbyteのWeb UIにはIAPによる認証を設定しており、リクエスト時にHeaderへ認証トークンを付与する必要があります。Connectionの設定ではHeaderへの認証トークンの付与ができないため、この方法は断念しました。 この問題に対して、内部Ingressを追加することで解決しました。Cloud ComposerとAirbyte Serverは同じVPC内に構築されており、Airbyte Serverの内部Ingressは静的内部IPアドレスを指定しています。この静的内部IPアドレスは、DNSでドメインに紐づいています。そのドメインをAirflowのConnectionのHostに指定することで、AirflowからAirbyte Serverへアクセスできるようにしました。 関連ページ内リンク TerraformによるDNSの設定 Kubernetesのserver.yamlマニフェスト Kubernetesのprd.yamlマニフェスト MinIOを用いない Airbyteはジョブ実行のログなどを、デフォルトのKubernetesのyamlを適用してしまうと MinIO というオブジェクトストレージに出力します。本記事では、MinIOを用いず、GCSにログを出力するようにしました。1つ目の理由は、MinIOのLICENCEがGNU AGPL v3だったためです。弊社では、オープンソースのライセンスのうち、AGPLの使用が禁止されています。2つ目の理由は、使い慣れているGCSを用いたかったためです。データ基盤チームの主要な使用クラウドサービスはGCPであり、GCSを用いることでログの管理コストを低減できると考えました。 PVCのAccessModeの変更 以下のIssueの対応です。 github.com airbyte-server と airbyte-cron のPodにおいて airbyte-volume-configs というPVCをマウントしています。このPodがクラスタ内の別々のノードにスケジュールされることがあり、その際にPVCのAccessModeが ReadWriteMany でないと、Podが起動できないという問題がありました。そのため、PVCのAccessModeを ReadWriteMany に変更しました。GKEで ReadWriteMany を用いるには、Filestore CSI Driverを使用する必要があります。下記の公式のドキュメントを参考にしました。 cloud.google.com 関連ページ内リンク Kubernetesのvolume-configs.yamlマニフェスト Kubernetesのprd.yamlマニフェスト ServiceAccountの権限借用 こちらの問題 1 は既に公式の対応によって解決されていますが、事例として紹介します。Airbyte上でBigQueryをDestinationとして登録する際、秘密情報であるService Account KeyをAirbyte上に登録する必要がありました。この時、GKE上にAirbyteを構築したため、KSAとGSAの権限借用を用いて、秘密情報を登録することなくBigQueryへのアクセス方法を検討しました。しかし、当時のAirbyteの転送ジョブの挙動として、転送用のPodを起動していました。そのPodは airbyte-admin Service Accountではなく default Service Accountで起動されていました。同じ課題を感じる方が Pull Request を出していたのですが、セキュリティの観点からRejectされていました。 上記の問題を解決するために、本記事では airbyte-admin と default のKSAに、GSAを権限借用する設定を追加しました。これにより、Airbyteの転送ジョブのPodは default KSAで起動されますが、GSAの権限を借用することで、BigQueryへのアクセスが可能になりました。また上記Pull Requestでは、同一クラスタ内に複数のサービスが存在する場合 default のKSAにGSAを権限借用する設定を追加すると、過剰な権限付与と指摘されています。本記事ではAirbyteのPodのみが存在するクラスタであるため、この設定で問題ないと判断しました。 関連ページ内リンク TerraformによるService Accountと権限関連 Kubernetesのdefault-service-account.yamlマニフェスト Kubernetesのprd.yamlマニフェスト 前述したように、この問題は解決済みのようです。最新のバージョンではApplication Default Credentials (ADC)による認証が可能になっています。 github.com 導入後の課題点 Helm化 現在、Airbyteの公式では下記リンクのHelmを用いたデプロイが推奨されています。Airbyte構築時点では、Kustomizeを用いたデプロイが推奨されていたので、そちらを選択しました。Airbyteのアップデートにも対応できるように、今後はHelmによるデプロイに移行する予定です。 docs.airbyte.com データ転送パイプラインの設定情報の反映 実際のデータ転送パイプラインの設定情報を反映する方法に課題があります。インフラのコード管理はTerraformとKubernetesのマニフェストで行えていますが、Airbyteのコネクタの設定はWeb UI上で行っています。Airbyteの公式が Octavia CLI という、コネクタの設定情報をCLIで管理するツールを提供しており、yaml形式で設定情報を管理できます。当初、こちらのCLIを用いて設定情報を管理しようと考えましたが、現在は octavia import all を用いた設定情報のバックアップのみを行っています。理由としては、connectionsのスキーマの変更に追従できないためです。sourcesとdestinationsの設定情報は一度設定すると、基本的に変更されることはありません。しかしconnectionsの設定情報はAPIのアップデートによってスキーマが変更された場合、その内容を取り込む必要があります。この自動化が構築初期の段階では難しかったため、現在は前述した設定情報のバックアップのみを行っています。 Octavia CLIをインストール後、下記手順で設定情報のバックアップを行いました。 GCPのAPI&Service画面のCredentialsから、IAPリソース追加時に作成されたOAuth ClientのClient IDの値を取得します。 次に初回時のみ、 octavia init コマンドを実行し、設定情報をバックアップするディレクトリを作成します。 下記のコマンドを実行し、設定情報をバックアップします。 octavia --airbyte-url {AirbyteのWeb UI URL} --api-http-header "Authorization" "Bearer $(gcloud auth print-identity-token airbyte-app@airbyte-project-prd.iam.gserviceaccount.com --audiences={1で取得したClient ID}" import all Cloud Loggingへのログ出力 本記事ではAirbyteのログをGCSへ出力するようにしましたが、実際のログを確認する際には該当するGCS内にあるファイルを確認する必要があります。現在、転送Jobの実行ログは、下記画像のようにAirbyteのWeb UI上で確認できます。しかしその他のWeb ServerやWorkerのログは、GCS内にあるファイルを確認する必要があります。 Cloud LoggingはGCSとは異なり、ログの検索やアラートの設定などが可能です。そのため、Cloud Loggingへのログ出力について調査しました。しかし、現在のAirbyteのログ出力は、GCSやS3、MinIOなどのオブジェクトストレージに対応していますが、Cloud Loggingに対応していません。そのため、Cloud Loggingへのログ出力については、今後の課題として残しています。 まとめ この記事では、ZOZOのデータ基盤におけるELTツールとしてAirbyteの導入と、それをGKE上で構築した経緯と詳細を紹介しました。Airbyteの選定理由、OSSである利点、豊富なConnectorの存在、EL(T)ツールとしての機能、そして活発なコミュニティサポートが主なポイントでした。 GKE上でAirbyteを構築する過程で、Kubernetesのマニフェストのカスタマイズ、Cloud SQLの使用、IAPによる認証、Airflowによるスケジュール実行の設定、そしてMinIOを用いず、GCSへのログ出力への切り替えなど、様々な工夫と調整が必要でした。これらの取り組みによって、Airbyteを構築する上で直面した課題を解決し、データ基盤の強化につながりました。 しかし、Helmによるデプロイへの移行や、データ転送パイプラインの設定情報の反映方法など、今後の課題も残っています。これらの課題に対応し、さらなるデータ基盤の強化を目指しています。 本記事が、Airbyteの導入を検討している方々や、GKE上でのELTツールの構築を考えている方々にとって、役立つ情報を提供できていれば幸いです。 ZOZOでは、一緒に楽しく働く仲間を募集中です。ご興味のある方は下記採用ページをご覧ください! corp.zozo.com https://discuss.airbyte.io/t/how-to-run-bigquery-without-service-account-key-json-on-gce-or-gke/1709 ↩
アバター
はじめに こんにちは、DevRelブロックの ikkou です。12月15日に「ZOZO Kubernetes Night」と題した、ZOZOのKubernetes活用事例をご紹介するオンラインイベントを開催しました。 zozotech-inc.connpass.com 目次 はじめに 目次 当日の登壇内容 WEAR のワークフロー実行基盤コストを何とかしたい WEARフロントエンドにおけるPull Request毎のPreview環境導入とその効果 ZOZOTOWNにおけるKubernetes Cluster Upgradeのこれまでとこれから 最後に 当日の登壇内容 ZOZOのSREチームに所属するエンジニア3名が以下の内容で登壇しました。 タイトル 登壇者 WEAR のワークフロー実行基盤コストを何とかしたい 小林 未来 ( @mirai_kobaaaaaa ) WEARフロントエンドにおけるPull Request毎のPreview環境導入とその効果 山岡 朋樹 ( @ymktmk ) ZOZOTOWNにおけるKubernetes Cluster Upgradeのこれまでとこれから 巣立 健太郎 ( @ksudate ) 今回のイベントではオフライン会場は設けず、YouTube Liveでのオンライン配信のみで実施しました。当日の発表はYouTubeのアーカイブでご覧ください。 www.youtube.com WEAR のワークフロー実行基盤コストを何とかしたい speakerdeck.com WEARのSREチームに所属する小林は、WEARワークフロー実行基盤のリプレイスにあたっての課題と解決へのアプローチ、リプレイス後の効果を紹介しました。 本発表は先日公開した記事「 ワークフロー実行基盤をFargateからEC2へ変更したらコストもパフォーマンスも改善できて幸せになった話 」でより詳しく紹介しています。Terraformのコードも含まれていますので、ご興味をお持ちの方はあわせてご覧ください。 techblog.zozo.com WEARフロントエンドにおけるPull Request毎のPreview環境導入とその効果 speakerdeck.com 小林と同じWEARのSREチームに所属する山岡は、WEAR Webのリプレイスに際して要望の挙がっていたPull Request毎にPreview環境を構築した手法とその効果についてご紹介しました。 山岡は、Kubernetesネイティブな負荷試験基盤の導入とその効果について説明する記事を公開しています。こちらもぜひご覧ください。 techblog.zozo.com ZOZOTOWNにおけるKubernetes Cluster Upgradeのこれまでとこれから speakerdeck.com ZOZOTOWNのSREチームに所属する巣立は、ZOZOTOWNのKubernetesクラスターのアップグレードについて、これまでの取り組みとこれからの取り組みについて紹介しました。 巣立は、拡大し続けるマイクロサービス基盤で直面したCI/CDの課題をどのように改善したのかを説明する記事を先日公開しています。こちらもぜひご覧ください。 techblog.zozo.com 最後に イベント当日にリアルタイムでご視聴いただいた方、そしてSlidoで質問をお寄せいただいた方、ありがとうございました。見逃した方はぜひYouTubeのアーカイブ動画をご覧ください。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co hrmos.co hrmos.co
アバター
はじめに こんにちは、マイグレーションブロックの寺嶋です。 11/29、ZOZOTOWNで購入した商品の口コミやレビューを投稿、閲覧する機能をリリースしました 1 (以下、アイテムレビューと記載)。ZOZOTOWNで購入をしたことがある方は投稿できますので、ぜひ使用感などの声を投稿してください。 なお、この記事は ZOZO Advent Calendar 2023 #1 の8日目の記事です。 目次 はじめに 目次 アイテムレビューとは データベース選定 スケーラビリティ優位性をあまり活かせなそう 利用できないデータベース機能がある 先行開発 ドメイン駆動設計(DDD) 設計フェーズ アーキテクチャの選定 仕様変更が入って実際どうだったか? テストデータ 基盤技術の共有 まとめ 最後に アイテムレビューとは まずは、アイテムレビューの概要について紹介します。 アイテムレビューという命名の通り、ご購入いただいた商品のレビューを投稿したり、他に購入した方のレビューを参照したりできるようになります。また、レビュー投稿の際には星による5段階評価が行われるので、ひと目で評価を判断できます。特徴的なのは、マックスくんスタンプによるリアクションが行えるところで他のECサイトにはないZOZOらしさを感じる機能かと思います。 次の図のように、商品詳細画面からレビューを見ることができるようになりました。すべてのレビューを見るリンクからレビューモーダルを表示でき、全レビューの参照や星評価での絞り込み、投稿日による並び替えができます。 トップレビューとレビューモーダルのイメージ画像です(実際の画面ではありません) レビューに対してスタンプでリアクションを取ることができます。 スタンプのイメージ画像です(実際の画面ではありません) 注文履歴のレビュータブで投稿可能な商品一覧が表示され、新規投稿・編集・削除ができます。商品カテゴリ毎にアンケートがあるので年代や性別、サイズや肌の悩みなど購入者の意見を参考にできます。 注文履歴のレビュータブ、投稿画面のイメージ画像です(実際の画面ではありません) 昨今のECサイトには必ずといっていいほど導入されていたアイテムレビュー機能がZOZOTOWNにも導入され、別サイトで口コミを確認するといった煩わしさが解消されます。ぜひ、多くの皆様にご活用いただけると幸いです。 そんなアイテムレビュー基盤を構築するにあたり行った工夫、発生した課題とその対策をご紹介します。 データベース選定 新規基盤マイクロサービスを作成するにあたり、いくつかの技術的課題がありました。 1つ目がデータベースの選定です。アイテムレビューはレビューの検索のような読み込み処理以外に、レビュー書き込み、スタンプによるリアクション、通報などといった書き込み処理が多く高負荷になることが予想されます。そのため、データベースの選定が重要であり、SREチームとも相談し次の候補で検討しました。 Amazon Aurora MySQL PingCAP TiDB Aurora MySQLはご存知の通り、AWSが提供するフルマネージドでMySQL互換のリレーショナルデータベースです。一方、TiDBはPingCAP社が開発した分散型データベースでRDBMSとNoSQLの機能を組み合わせたNewDBと呼ばれる新しいカテゴリのデータベースです。MySQL互換のSQL解析機能を持っているためアプリケーションからはMySQLと同等の利用が可能で、水平方向のスケーラビリティ・強力な一貫性・高可用性を兼ね備えたデータベースです。 SREチームと次の項目で比較検討しました。 項目 スケールアウト・イン(性能・サイズ) スケールアップ・ダウン マスタ障害 / フェイルオーバー データ(シャード)分割と結合 基本Read / Writeクエリ性能(ベンチマーク結果) ネットワーク連携(VPC) 構成管理(Iac) 監視・メトリクス・ログ バックアップ&リカバリ 監査ログ SQL Serverとの同期(ニアリアルタイム) 可用性 エコシステムとのインテグレーション サポート体制 コスト OLAP アプリケーション視点からの機能比較 アイテムレビューの特性を考慮し、次の理由から現時点ではTiDBはアイテムレビューにはマッチしないと判断しAurora MySQLを採用しました。 スケーラビリティ優位性をあまり活かせなそう 想定するWrite TPSとデータ量を考慮するとシャード分割によるスケールアウトを要するレベルではなく、インスタンスに十分収まりそうだと考えました。また、データ量的にもAuroraストレージ格納機能で補えるものと判断しました。TiDBの強力な自動リバランスによる書き込み水平スケール能力は残念ながらあまり活かすことはできなさそうでした。 利用できないデータベース機能がある MySQL互換とはいえまだ利用できない機能がありました(外部キー制約、auto incrementなど 2 )。その中でも外部キー制約を使えないのは大きな課題でした。ソフトウェアを安全に運用するためにはデータの整合性が不可欠と考えていたため、TiDBを選定するのは難しいと判断しました。ただ、最新のTiDBでは外部キー制約も解決されているので、更新系処理でスループットが出ないといった問題が起きた際は移行を検討してもいいかもしれません。 先行開発 新マイクロサービス基盤の設計・実装を先行して行い、構築が終わったころにWebフロントエンドやアプリ、BFF層が開発することになりました。基盤・後発に関わらず、開発が進めば想定外の事象が起きたり、辻褄が合わないなどで仕様変更が入ったりしやすくなります。もちろん、ソフトウェア開発で仕様変更が入ることは当然起こりえることです。 ただ、他チームが開発を始める頃にはアイテムレビュー基盤としては開発を終えているので、作り終えたあとに変更が入ることとなります。そうなった際に、いかに手早く修正し利用チームに対して機能を提供するか、そして品質を担保するのかが課題となります。言い換えると以下を意識することで課題解決ができると考えました。 変更容易性を高める 複雑なドメイン知識を集約し凝集度を高める そこでアイテムレビュー基盤開発では、ドメイン駆動設計(以降DDD)の手法を取り入れました。 ドメイン駆動設計(DDD) DDDとはエリック・エヴァンスが提唱したソフトウェア設計のプラクティスの1つです。DDDの細かい説明は他のサイトにまかせますが、以下がWikipediaからの抜粋 3 です。 ドメイン駆動設計(英語: domain-driven design、DDD)とは、ドメインの専門家からの入力に従ってドメインに一致するようにソフトウェアをモデル化することに焦点を当てるソフトウェア設計手法である。オブジェクト指向プログラミングに関しては、ソースコード(クラス名・クラスメソッド・クラス変数)の構造と名称がビジネスドメインと一致させる必要があることを意味する。 ドメイン駆動設計では、開発者は通常モデルを純粋で有用な構造として維持するために大量の分離とカプセル化を実装する必要があると批判されているが、ドメイン駆動型設計は保守性などの利点を提供する。 以上のことから、ドメイン(解決したい領域)の専門家の知識をソフトウェア設計にドメインモデルでそのまま反映させることを目的としています。正式なDDDを実践するためにはアイテムレビューを理解しているメンバーにドメイン知識を提供してもらい設計を進める必要がありますが、チームに専門家がいないためいわゆる戦術的DDDを取り組むこととなります。戦術的DDDとはドメイン駆動設計で語られる技術的要素のみを取り入れた手法です。課題となっている仕様変更を受け入れやすくする観点では、技術要素のみであっても十分にメリットがある戦略だと思います。 設計フェーズ 最初に行ったのがユースケースの整理による必要な操作の可視化でした。基盤側の実装だけではなくアプリやフロントエンドも含めた全体像の整理を目的としており、全チームの認識を合わせるために作成しました。ユースケース単位で各チーム工数見積りをしたり、機能や仕様調整もユースケース単位で行えたり、コミュニケーションを取るための共通認識となったり大変役に立ちました。また、利用者や行うアクションも記述することでよりイメージを膨らませることができました。 ユースケースで関係者との合意が取れたらユースケース単位でシーケンス図を作成しました。ユースケース単位でどのマイクロサービスが利用されるのか、今回開発するAPIやデータの粒度、API呼び出しの回数などの可視化をおこないました。各チームとのコミュニケーションツールとなりAPIが明確になることでAPIのI/F設計、負荷テストの際にどのような経路でどのようなAPIが利用されているのかが分かるようになりました。また、APIの経路を明確にすることで負荷試験時に必要となるデータや必要となる環境が可視化されるメリットにもつながりました。 マイグレーションブロックではドメインモデル図をモブプログラミング形式で作成する会を行い、ドメイン知識の共有を図りました。機能要求から制約を書き表したり、集約の区分け、データの関係性を可視化したりすることが目的で実装時にはドメインモデル図がとても重宝しました。メンバーとワイワイ話しながらドメインモデル図を書くというのもとても楽しく新鮮な経験となりました。 シーケンス図でAPIの種類まで認識合わせをしましたが、認識をあわせたAPIのI/Fを設計する必要があります。実装前であったのでドキュメントにHTTPメソッドやURL、パラメータやリクエスト・レスポンスを記載し利用チームにレビューを依頼する方法で行いました。その後、実装が進むとOpenAPI(Swagger)を介して仕様を調整していく形となり、よりスピーディーになったと思います。 アーキテクチャの選定 モブモデリングをしたドメインモデル図を活かすためのアーキテクチャ選定として、オニオンアーキテクチャを採用しました。オニオンアーキテクチャを採用した理由は次の通りです。 他マイクロサービスでも採用されており馴染みがある ドメイン層を中心として設計するのに一番理解しやすい DDDが推奨するアーキテクチャの思想は本質的に同じであるものの、複数のマイクロサービスを運営・管理するのに異なるアーキテクチャの採用はデメリットだと考えました。また、ドメインモデルに知識を集約させることを考えていたのでドメインを中心とするオニオンアーキテクチャが理解しやすく適していると判断しました。各層の目的・責務は次のようにしました。 domain層:ビジネスロジックの表現。集約単位で整合性を担保したデータ郡 infrastructure層:外部サービスへのアクセス(他層から直接参照はNG) presentation層:APIの公開やリクエスト・レスポンスへの変換 usecase層:操作(ユースケース)の組み立て、トランザクションの制御 仕様変更が入って実際どうだったか? それほど大きな変更は入らなかったものの、修正の際は責務が明確になっていることで対象箇所が限定的となり修正・ユニットテストが書きやすく、スピーディーな対応ができました。影響箇所の調査で疲弊することはなく、修正後のテストは断定的になりました。他チームの方々からも対応が早いといった感謝の声をいただくことができ、結果としてとても効果があったと感じています。 テストデータ 開発が中盤に差し掛かる頃には各マイクロサービスを結合させることが増えてきたり、テストケース毎にいろいろなパターンのレビューが必要となったりしてきます。我々基盤チームがデータを管理しているので各チームからレビュー作成や、状態変更などの依頼が来ます。簡単な依頼であれば直接SQLを実行したり対象のAPIで操作したりもできますが、星評価の希望した平均点や負荷試験で数百万件のレビューデータを作成する必要もあったためこれらの方法だと非効率でした。 そこでよくある方法ですが、作成する件数や回答パターンを指定できるMySQLストアドプロシージャを作成しました。正規手順によるデータ作成ではありませんが、ストアドプロシージャがあることで大量のデータや依頼に応じたデータを容易に作成できとても重宝しました。それでも数百万単位のレコードを作成するのに数日かかってしまったりしたので、まだ改善の余地はありそうです。 データベース選定時点ではテストデータ作成にMySQLストアドプロシージャを利用することは想定できていませんでしたが、TiDBでは残念ながらストアドプロシージャはサポートされていません。 4 別の方法を模索する必要があり、もっと複雑な方法で時間がかかったかもしれません。 基盤技術の共有 ZOZOTOWNのリプレイスはこれからも続き新たな基盤が作られていきます。アイテムレビューで培ったノウハウを次の基盤を作る際に活かすためSpring Bootとオニオンアーキテクチャをベースにしたテンプレートリポジトリを作成しました。今回アイテムレビュー基盤を作るにあたり議論した設計方針やユーティリティ、ユニットテスト環境などをテンプレートリポジトリにまとめたものです。 盛り込まれている内容を1部ご紹介します。 オニオンアーキテクチャのパッケージ構成とサンプル実装 DockerによるMySQLとSchemaSpyによるテーブル定義 ArchUnitによる依存関係の整合性テスト TestContainersによるユニットテスト 同じ設計思想で新たな基盤を作成するときはこのリポジトリをテンプレートとして作成すれば必要な環境が整った状態で着手できるようになり立ち上げ時の工数削減に役立つものと考えております。早速こちらのリポジトリを勉強の題材にしたり、新規基盤をこのリポジトリから作成したりと活用されています。これからも随時更新を続けていければと考えています。 まとめ 今回はアイテムレビュー基盤を構築する際の課題やアプローチについて紹介しました。データベース選定や設計・実装、テストデータ作成などの課題をチームで乗り越えることができ、変更容易性の高いマイクロサービスを構築できました。 今後もアイテムレビュー基盤構築の経験を活かし、よりよいサービスが提供できるマイクロサービス構築を目指していきます。新たな知見が得られた際はご紹介します。 最後に ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co https://corp.zozo.com/news/20231130-zozotown-itemreview/ ↩ 検討時の2022/10頃は未サポートでしたが最新バージョンではサポートされています。 外部キー制約 、 auto increment ↩ https://ja.wikipedia.org/wiki/%E3%83%89%E3%83%A1%E3%82%A4%E3%83%B3%E9%A7%86%E5%8B%95%E8%A8%AD%E8%A8%88 ↩ https://docs.pingcap.com/ja/tidb/stable/mysql-compatibility ↩
アバター
こんにちは。SRE部の巣立( @ksudate )です。 我々のチームでは、AWS上で多数のマイクロサービスを構築・運用しています。マイクロサービスが増えるにつれて、CI/CDの長期化やリリース手法の分散など様々な課題に直面しました。 本記事では、それらの課題をどのように解決したのかを紹介します。 目次 目次 はじめに CI/CDのこれまで Release PRによるリリース CI/CD実行時間の長期化 マイクロサービスごとのリリースが難しい リリーサーの制限ができない ドメイン単位の並行リリース リリース手法が分散する ブランチ間の同期が必要 パイプラインの増加 CI/CD実行時間の長期化 リリーサーを制限できない CI/CDの刷新 高速かつシンプルなCIパイプライン 変更差分を利用したCIパイプラインの実行 承認機能付きのCDパイプライン GitHub Environmentsによるリリース制御 GitHub Environmentsによるリリーサーの制限 結果 さいごに はじめに 我々のチームが管理するCI/CDでは、ZOZOTOWNマイクロサービス基盤の全てのインフラリソースを対象にリリースまで行います。 インフラリソースごとに管理ツールが異なっており、全てGitHubにコードとして管理されています。 CI/CDで実行される処理は以下のようになっています。 インフラリソース 管理ツール CI CD AWS CloudFormation aws cloudformation create-change-set aws cloudformation execute-change-set Kubernetes Fluxcd kubectl diff kubectl apply or flux push artifact Datadog Sentry PagerDuty Terraform terraform plan terraform apply CI/CDのこれまで これまでに2つのリリース手法を利用していました。 Release PRによるリリース ドメイン単位の並行リリース Release PRによるリリース マイクロサービス基盤の構築当初は、 release ブランチを利用してリリースしていました。 このリリース手法ではGitHubのPull Request(以下、PR)の作成・更新によってCIパイプラインが動作し、マージによってCDパイプラインが動作します。 開発用ブランチ(ここでは、featureブランチとする)からmasterブランチ宛のPRを作成すると、DEVELOP・STAGING環境のCIパイプラインが動作します。PRをmergeするとCDパイプラインによってDEVELOP環境・STAGING環境へデプロイされます。 masterブランチからreleaseブランチ宛のPRを作成すると、PRODUCTION環境のCIパイプラインが動作します。PRをmergeするとCDパイプラインによってPRODCUTION環境へデプロイされます。 PR CI/CDパイプラインの対象環境 feature -> master DEVELOP・STAGING master -> release PRODUCTION masterブランチからreleaseブランチへのPR(以下、Release PR)は自動で作成されます。既にRelease PRが存在する場合は、そのPRの変更内容に追加されます。 この手法のメリットは、リリース前の動作確認が可能でリリース手順も簡単という点です。 しかし、マイクロサービスが増えると以下の課題が生まれました。 CI/CD実行時間の長期化 マイクロサービスごとのリリースが難しい リリーサーの制限ができない CI/CD実行時間の長期化 CI/CDパイプラインでは、マイクロサービスの数だけ直列に処理を実行していました。 そのため、マイクロサービスが増えるにつれて、実行時間も増加していきました。 マイクロサービスごとのリリースが難しい 前述の通りreleaseブランチやRelease PRを全てのマイクロサービスで共有するため、マイクロサービスごとにリリースするのが難しいという問題がありました。 ロールバックする可能性のある変更は、他の変更と一緒にリリースしたくないことがあります。それを実現するには次の手順が必要です。 Release PRが存在するか確認。存在する場合、mergeする。 Release PRが存在するとその変更内容と同時にリリースされます。 他メンバーにPRのmergeを停止するように連絡する。 Release PRをmergeするまでに他メンバーがmasterブランチ宛に別のPRをmergeするとその変更も含まれてしまいます。 この調整が大きな負担で、リリースサイクルの低下を引き起こしていました。 リリーサーの制限ができない Release PRをmergeすることでCI/CDパイプラインによってPRODUCTION環境へリリースされます。 Release PRをmergeするには、1名以上のSREのレビューが必要です。 そのため、リリース可能なメンバー(以下、リリーサー)もSREに制限するのが理想です。 しかし、レビュー済みであればリポジトリへアクセスできる人は誰でもmergeできてしまいます。 上記の課題から新たなリリース手法を導入しました。 ドメイン単位の並行リリース この手法ではマイクロサービスを大まかな機能や担当チームごとにグループ(以下、ドメイン)に分けます。そしてそのドメインごとに main ブランチと release ブランチを使ってリリースします。 以下は検索ドメインの例を示しています。 検索ドメインでは、Search APIとSuggest APIの2つのマイクロサービスを持ちます。また、利用するブランチは zozo-search-main と zozo-search-release とします。 この手法のメリットは、ドメイン単位で並行にリリースできることです。 以前は複数チーム間でリリースの調整が必要でしたが、ドメイン内のマイクロサービスは1つのチームが管理しているので、調整なしでリリースできるようになりました。 また、CI/CDパイプラインではドメイン内のマイクロサービスに対してのみ処理が実行されます。そのため、以前の方法に比べて大幅な高速化を実現しています。 しかし、この手法でもいくつかの問題を抱えていました。 リリース手法が分散する ブランチ間の同期が必要 パイプラインの増加 CI/CD実行時間の長期化 リリーサーを制限できない リリース手法が分散する この手法ではマイクロサービスで利用するリソースのみをリリースの対象としました。 その他のリソース(ex. Cluster Autoscaler)は従来通り release ブランチを利用していました。 そのため、リリース手法が2つ存在することになり新規利用者を困惑させる原因となっていました。 また、変更内容によってはブランチを切り替えながら作業する必要がありました。 ブランチ間の同期が必要 複数のブランチから参照されるリソース、例えば作業用のスクリプトなどは、これまで通り master ブランチや release ブランチで管理していました。 各ブランチで最新の内容を参照するために定期的に各ブランチを同期していました。この作業は毎週SREが実施していました。 しかし、各ブランチを同期するには複雑な手順が必要になります。この作業手順を間違えるとブランチの変更内容が消えたり、最新の状態と異なるものをリリースする可能性があります。 そのため、SREの大きなトイルとなっていました。 パイプラインの増加 この手法では専用のブランチごとにパイプラインが実行されます。そのため、ブランチごとにパイプラインを作成する必要があります。 パイプラインの大部分は同じ内容になっています。しかし、全てのパイプラインで利用しているカスタムアクションのアップグレードにも複数のファイルを修正する必要がありました。 またパイプラインが増加したことでCI/CDの渋滞が発生するようになりました。一度に大量のパイプラインが起動すると新しいパイプラインは他のパイプラインが一定数に落ち着くまで待機状態となります。その結果、CI/CD完了までの時間も大幅に増加していました。 CI/CD実行時間の長期化 Release PRによるリリースに比べて実行時間の高速化は達成しました。 しかし、ドメイン内のマイクロサービスが増えるにつれ実行時間が増加するため根本的な問題解決には至っていませんでした。 また、 release ブランチで稼働するCI/CDパイプラインは高速化できていませんでした。 リリーサーを制限できない Release PRによるリリース同様にこの問題は解決していません。 CI/CDの刷新 これらの問題を全て解決するために、CI/CD基盤を新たにデザインすることにしました。 高速かつシンプルなCIパイプライン まずは、CIパイプラインについて説明します。 変更差分を利用したCIパイプラインの実行 新しいCIパイプラインでは、PRに変更のあるインフラリソースに対してのみ処理が実行されます。 directory-changes Jobでは、変更差分のあるインフラリソースのディレクトリ名を取得します。 変更差分の検知には、 changed-files を利用しました。このGitHubアクションを使うとPull Requestに変更のあったファイルやディレクトリを取得できます。 以下は、 cloudformation ディレクトリ配下に変更があった場合にそのディレクトリ名を返します。 cfn-directory-changes : outputs : cfn_changed_files : ${{ steps.directory_changes.outputs.cfn_all_changed_files }} runs-on : ubuntu-latest steps : - uses : actions/checkout@v4 - name : List modified directories id : directory_changes uses : tj-actions/changed-files@v40 with : json : true dir_names : true escape_json : false files_yaml : | cfn : - cloudformation/** dir_names_max_depth : 2 base_sha : ${{ github.event.pull_request.base.sha }} - name : Echo modified directories run : | echo "${{ steps.directory_changes.outputs.cfn_all_changed_files }}" ここで取得したディレクトリを後続のJobへ渡します。結果、Pull Requestに変更のあるインフラリソースに対してのみ処理が実行されます。 directory-changes Jobの導入によってマイクロサービスがどれだけ増えようとも実行時間が長期化することはなくなりました。 承認機能付きのCDパイプライン 続いて、CDパイプラインです。 CDパイプラインでもCIパイプライン同様に directory-changes を活用しています。 また、新たに release-approval と confirm-management-team の2つのJobを追加しました。 2つのJobによる新しい機能について説明します。 GitHub Environmentsによるリリース制御 これまで利用していた release や zozo-search-release などのブランチを廃止しました。新しいリリース手法では、 master ブランチのみを利用します。 しかし、リリース用のブランチが無くなるとリリース前の動作確認やリリースタイミングの制御ができません。 そこで、 GitHub Environments を使うことにしました。 GitHub EnvironmentsはGitHub ActionsのJobに設定できます。 release-approval : runs-on : ubuntu-latest environment : name : <ENVIRONMENT NAME> GitHub Environmentsにはいくつかの Protection Rule が存在します。 今回は、Required reviewersを付与しました。Required reviewersを設定すると指定のレビュアーからレビューがあるまでJobは実行されません。 以下の例では、 sre Environmentsの必須レビュアーに ksudate が設定されています。 上記の sre Environmentsを使用した例がこちらです。 ksudate から承認があるまで、 release-approval のJobは実行されません。また、needsに release-approval を指定している k8s-apply も実行されません。 name : Release Gate on : push : branches : - main jobs : release-approval : runs-on : ubuntu-latest environment : name : sre steps : - run : | echo "release approved" # CI STEP k8s-diff : runs-on : ubuntu-latest steps : - run : | echo "kubectl diff -f xxx" # CD STEP k8s-apply : runs-on : ubuntu-latest if : github.event.pull_request.merged == true needs : release-approval steps : - run : | echo "kubectl apply -f xxx" この機能によってPRODUCTION環境のリリース前にレビューを追加できました。 その結果、リリース前の動作確認が可能になり、リリースタイミングを指定のレビュアーが制御できます。 しかし、この方法はGitOpsを実現するKubernetesクラスタで問題があります。 マイクロサービス基盤のKubernetesクラスタはFluxcdでGitHubを参照してアプリケーションのデプロイを行なっています。 techblog.zozo.com そのため、 master へmergeしたタイミングでクラスタへ同期されます。 この対策として、Fluxcdの OCIRepository を利用しました。OCIRepositoryを利用することで、FluxcdはGitHubではなく任意のOCI Repositoryからマニフェストを取得します。 OCI RepositoryにはAmazon ECRを利用しています。このAmazon ECRにマニフェストが格納されるとFluxcdはその情報を元に同期を行います。 そこで、 release-approval の実行後にAmazon ECRへマニフェストを格納することでリリースのタイミングを制御できました。 他にもOCI Repositoryを利用するメリットはあります。 詳しくは「Kubernetes Meetup Tokyo #58」で発表した資料をご覧ください。 speakerdeck.com release-approval によって、以下の課題を解決しました。 マイクロサービスごとのリリースが難しい release-approval によってPRごとにリリースすることが可能になりました。 リリース手法が分散する 全てのインフラリソースを master ブランチを使ってリリース可能になりました。 ブランチ間の同期が必要 release ブランチは廃止されました。ドメインごとに release ブランチ、 main ブランチを管理する必要もありません。 パイプラインの増加 パイプラインのトリガーは master ブランチのみで今後増えることもありません。 GitHub Environmentsによるリリーサーの制限 残る課題は、リリーサーの制限についてです。 リリーサーはどのファイルを変更したかによって変わります。例えば、Search API変更時のリリーサーはSearch Teamになります。Cart API変更時のリリーサーはCart Teamになります。 そこで、ファイルごとに管理するチームを設定しました。 これには、 paths-filter を利用しました。 paths-filterを利用すると、事前に定義されたファイルの変更があったかどうかを知ることができます。 以下に例を示します。 confirm-management-team Jobはfiltersに定義された情報を元に変更を検知します。例えば、 cloudformation/search-api に変更があれば、 ${{ steps.filter.outputs.search-team }} がtrueを返します。 この情報を release-approval Jobに渡すことで特定のチームのリリースを必須にできます。 今回の例では、 release-approval はチームごとに作成しています。こうすることで、Search Teamが管理するファイルに変更があった場合に search-team-release-approval を起動して、Search Teamのレビューを必須にできます。 name : Release Gate on : push : branches : - main jobs : confirm-management-team : runs-on : ubuntu-latest outputs : search-team : ${{ steps.filter.outputs.search-team }} cart-team : ${{ steps.filter.outputs.cart-team }} steps : - uses : actions/checkout@v3 - uses : dorny/paths-filter@v2 id : filter with : filters : | search-team : - 'cloudformation/search-api' - 'kubernetes/search-api' - 'terraform/datadog/search-api' cart-team : - 'cloudformation/cart-api' - 'kubernetes/cart-api' - 'terraform/datadog/cart-api' search-team-release-approval : runs-on : ubuntu-latest if : ${{ needs.confirm-management-team.outputs.search-team == 'true' }} needs : confirm-management-team environment : name : search-team steps : - run : | echo "search-team release approved" cart-team-release-approval : runs-on : ubuntu-latest if : ${{ needs.confirm-management-team.outputs.cart-team == 'true' }} needs : confirm-management-team environment : name : cart-team steps : - run : | echo "cart-team release approved" # CI STEP k8s-diff : runs-on : ubuntu-latest steps : - run : | echo "kubectl diff -f xxx" # CD STEP k8s-apply : runs-on : ubuntu-latest if : ${{ ! ( failure() || cancelled() ) }} needs : - search-team-release-approval - cart-team-release-approval steps : - run : | echo "kubectl apply -f xxx" 結果 実行時間は、導入前に比べると大幅な削減を実現しました。 既存のワークフローから段階的に移行しているため、正確な比較は難しいです。しかし、導入前後で1か月間の平均などを見ると以下のようになっていました。 Before (min) After (min) Avg 9.45 2.00 Max 23.5 6.38 Min 0.37 0.28 Sum 1146 126 1か月のトータル実行時間は1/10程度に減少しており、平均も7分近く削減できました。 加えて、これまで抱えていた課題も全て解決できました。 さいごに この記事では、拡大し続けるマイクロサービス基盤で直面したCI/CDの課題をどのように改善したのかを説明しました。 現在、新しいモノレポCI/CDは問題なく稼働しています。引き続き、より良い開発体験を提供できるよう改善を進めていきます。 ZOZOTOWNでは一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com
アバター
はじめに こんにちは。DevRelブロックの @wiroha です。DevRelブロックはエンジニア組織の技術広報・技術ブランディングを担っており、TECH BLOGの運営や登壇支援、技術カンファレンスへの協賛などを行っています。本記事では登壇支援にフォーカスし、実施している取り組みや工夫を紹介します。 目次 はじめに 目次 背景 登壇機会の発見・創出 カンファレンスのCfPネタ出し会・レビュー会を開催 自社イベントの開催 社外イベントの登壇者募集の情報をSlack上で告知 登壇資料を作成する際のサポート 登壇資料デザインテンプレートを作成・改修 使いやすい素材情報を集約 DevRelと広報による登壇資料レビューを実施 登壇時のサポート 登壇リハーサルを開催 事前収録の実施 アフターサポート まとめ 背景 技術イベント・カンファレンスでエンジニアが登壇することは、自社の技術や組織を広く知ってもらうための重要な手段です。登壇者にとっても、説明するために深く調べること、伝わるように話すこと、フィードバックをもらうことは学びにつながります。交流によりモチベーションが向上することもあるでしょう。 登壇するのが良いことだと思ってはいても「何を話せばいいか思いつかない」「準備に時間がかかる」「書いた内容が適切か自信がない」といった方は多いかと思います。DevRelはそれらの障壁や不安をなるべく取り除くために、次の取り組みをしています。 登壇機会の発見・創出 登壇資料を作成する際のサポート 登壇時のサポート それぞれの具体的な実施内容を紹介していきます。 登壇機会の発見・創出 カンファレンスのCfPネタ出し会・レビュー会を開催 技術カンファレンスでセッションの募集がはじまったら、社内に告知しネタ出し会・レビュー会を開催します。DevRelではなくエンジニアの主導で開催することも多くあります。ひとつのGoogleドキュメントに各自のプロポーザルを下書きし、相互にレビューコメントを残しながらディスカッションしていきます。 複数の案を出してどれがよさそうか相談することや、他の人のプロポーザルを見ることで新しく話すネタを思いつくこともあります。技術顧問の方にレビューいただくなど自信を持って応募できるクオリティにブラッシュアップして、採択されるようみんなで取り組んでいます。 レビューの実例。アイディアを出し合っています。 自社イベントの開催 ZOZOではランチタイムに開催する「ZOZO Tech Talk」と、夜に開催する「ZOZO Tech Meetup」の2つのシリーズでイベントを運営しています。元々は夜の「ZOZO Tech Meetup」のみでしたが、短時間で気軽に参加できるオンラインのランチタイムイベントも2年ほど前からはじめました。育児などにより夜は時間を取りにくい方も、ランチタイムならば登壇できることがあります。夜の開催形態はオンライン・オフライン・ハイブリッドがあり、登壇者の希望をもとに決めています。 テーマは特定の技術分野に偏らず、さまざまなエンジニアが参加できるよう心がけています。言語だけではなく「物流システムリプレイス」といった珍しい切り口でのイベントは非常に盛り上がりました。 社外イベントの登壇者募集の情報をSlack上で告知 社内のSlackにおすすめの勉強会や登壇者の公募情報を流すチャンネルがあり、適宜情報を発信しています。情報を知りたいイベントのconnpassグループのメンバーになっておき、イベント公開のメール通知をトリガーにDevRelが手動で共有しています。自動で流す方法も検討したのですが、学生向けなど社員は参加できないイベントがあることと、情報が多すぎてもノイズになってしまうことから、精査して展開する方法にしました。 現在はconnpass以外のイベントは気付いた人が共有しており、その他プラットフォームでのイベント情報も把握・紹介し登壇機会を増やしていきたいと思っています。また新しいconnpassグループの見つけ方も今後の検討事項です。 登壇資料を作成する際のサポート 登壇資料デザインテンプレートを作成・改修 登壇資料を作成する上での手間を減らし品質を向上できるように、スライドのデザインテンプレートを用意し適宜更新しています。 このテンプレートを使うと次のメリットがあります。 一からテンプレートを作成する手間や、適したテンプレートを探す手間が省ける 統一したブランドイメージで発信できる 最新の正しい情報を手間なく掲載できる フォントサイズやレイアウトの推奨・提案に従い、読みやすいスライドを作成できる 書き方の例やノウハウもあわせて知ることができる スライド作成時のノウハウはたとえば次のものがあります。 黒背景はプロジェクター投影時に見づらくなる いらすとやさんの素材を商用利用する場合、無償で使えるのは1つの制作物につき20点までの制限がある フォント設定次第でSpeaker Deckへアップロードした際の崩れを防止できる Speaker Deck上で白抜きのように崩れてしまっているフォント(上)と崩れないフォント(下) 慣れていないと知らない知識ではないでしょうか。伝えたい内容がきちんと伝わるように、そして内容自体の検討にフォーカスできるようにテンプレートを進化させています。 使いやすい素材情報を集約 スライド作成時、適した画像を探すのに時間がかかってしまい、なかなか本題部分の作成が進まないことはよくあります。そこでスライド資料に使える画像素材の情報をまとめたページを作成しました。各プラットフォーム公式によるロゴ・アイコンの配布元情報や、商用利用が可能な素材情報を紹介しています。このまとめは非常に好評です。 登壇資料に使える画像素材のまとめページ DevRelと広報による登壇資料レビューを実施 業務で登壇するスライドについてはすべてレビューを実施しています。社外秘情報が含まれていないか、著作権上のルールを守れているか、誤字脱字・正式表記の誤りがないか、情報が古くなっていないかなどをチェックします。読みやすいフォントサイズ・コントラスト・レイアウトなど、伝わりやすさの観点でのレビューもします。レビューを通すことで「この内容で大丈夫だろうか」という不安を解消できます。 登壇時のサポート 登壇リハーサルを開催 カンファレンスでの登壇の際には、希望者を対象に登壇のリハーサルを実施しています。資料の可読性や内容の伝わりやすさ、スムーズにデモができるか、聞きやすい速度で話しているかなどをお互いに確認します。 私自身も登壇者としてリハーサルをしてもらった際は、初見の人にはわかりづらい点など自分では気付かなかった部分を数多く教えてもらえてとても助かりました。大見出しごとにかかった時間を記録してくれていたため、当日はその記録を頼りにペース配分をしてちょうど良い時間で終えることができました。緊張を和らげ、自信を持って登壇するためにもリハーサルは非常に有効です。 事前収録の実施 オンラインで自社イベントを行う際は、基本的に発表の事前収録を実施しています。失敗してもやり直せる、編集で直せるという安心感をもって発表に臨めます。ネットワークの調子が悪く途切れてしまったり、Slack通知により業務情報が露出してしまったりといったトラブルの対策にもなっています。 アフターサポート オフラインでの登壇の場合は良いカメラで撮影し、登壇者にお渡ししたり、レポートブログを書いたりして成果をしっかり残すようにしています。参加できなかった人に様子を伝えてより広められるだけでなく、成果を可視化することで登壇してやってよかったと感じてもらえるのではと考えています。 まとめ 本記事ではDevRelでの登壇支援の取り組みを「登壇機会の発見・創出」「登壇資料を作成する際のサポート」「登壇時のサポート」の3段階に分けて紹介しました。登壇する方・登壇を支援する方の参考になれば幸いです。今後もDevRelではさらなる支援の拡充と効率化をしていきたいと考えています。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。登壇をしたい方はDevRelがサポートします。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
アバター
はじめに こんにちは。ZOZO Researchの千代( @ryskchy )です。普段は主に数理最適化の技術を使った業務改善のための研究開発をしています。 ZOZOでは2022年から数理最適化の技術を使って最適な梱包資材を選ぶための取り組みを行なっています。本記事では梱包資材の選択のために解いている最適化モデルについて紹介します。 目次 はじめに 目次 背景・課題 梱包資材を選ぶアルゴリズム 直方体詰込み問題とは 直方体詰込み問題の解法 問題の定式化 回転や折りたたみを許さないモデル 定数 決定変数 目的関数 制約条件 全てのアイテムが梱包資材内に収まる制約 2つのアイテムが各軸の方向で重ならない制約 回転と折りたたみを許すモデル 追加の定数 決定変数の変更 追加の制約条件 荷姿を整えるための目的関数 まとめ 背景・課題 ZOZOTOWNで商品を注文すると、注文した商品に応じて様々な大きさの箱や袋(以下ではまとめて梱包資材と呼びます)で商品が届きます。注文した商品が全部入ればどの梱包資材でも配送はできますが、環境や物流リソースの観点からできるだけ小さい梱包資材を選びたいです。 注文された商品が1つだけであれば、その商品が入る最小の梱包資材を選ぶことは簡単ですが、複数の商品が同時に注文された場合には、全ての商品を梱包できる最小の資材を選ぶことはなかなか難しい問題です。 大き過ぎず小さ過ぎない最適な梱包資材を選ぶことで、配送費用や梱包資材の無駄が減少し、箱内の隙間が減ることで緩衝材の量や梱包品質も改善することが期待できます。また昨今問題となっている物流リソースの不足にも貢献できる非常に重要な課題です。 梱包資材を選ぶアルゴリズム 最小の梱包資材は以下の方法で選ぶことができます。 候補となる梱包資材を小さい順に並べ、順に以下を試す。 注文商品全てをはみ出すことなく梱包可能であればその資材を採用する。梱包できない場合は次の候補に移る。 ここで問題となるのが、候補となる梱包資材に注文商品が梱包可能なのかどうかを判定することです。これは、典型的な組合せ最適化問題 1 である(3次元)直方体詰込み問題を解くことで判定できます。 直方体詰込み問題とは (3次元)直方体詰込み問題は大きな直方体の容器に複数の大きさの異なる直方体(アイテム)を詰込む方法を考える問題の総称です。下記のようなバリエーションの問題が研究されています。 アイテムをなるべく少ない数の容器に詰込む3次元ビンパッキング問題 1辺の長さが可変な容器を仮定して、アイテムをなるべく小さい容器に詰込む3次元ストリップパッキング問題 今回解きたいのは容器を梱包資材、アイテムを注文商品として、梱包可否を判別する問題です。厳密には最適化問題ではありませんが、上記のバリエーションに容器の数や大きさを固定する制約条件を追加し、目的関数を削除した(定数として扱うことと同じ)最適化問題の一種として扱うことができます。 直方体詰込み問題の解法 直方体詰込み問題はNP困難な組合せ最適化問題の中でも計算コストが高い問題として知られています。例えば物流コンテナにダンボールを詰込む問題などアイテム数が数十〜数百規模になると、実用的な時間で厳密な最適解を求めることが難しく、ヒューリスティックな方法を採用することが多いです。 一方で、今回扱うのは注文商品を梱包資材に詰込む問題です。1度に注文される商品は10点以下であることがほとんどであるため、問題を定式化して最適化ソルバーで解くアプローチも現実的です。ZOZOの梱包資材の選択では最適化ソルバーに加えて、ルールベースやヒューリスティックなども含めた複合的なアルゴリズムをとっていますが、本記事では最適化ソルバーを利用した方法について紹介します。 問題の定式化 直方体詰込み問題を最適化ソルバーで解くために、直方体詰込み問題を 混合整数最適化問題 として定式化します。 OR学会機関誌の記事に2次元ストリップパッキングの定式化とプログラムがあるので、それを参考に3次元の詰込み可否判定問題を定式化します。 https://orsj.org/wp-content/corsj/or63-12/or63_12_762.pdf orsj.org 前提として、使用する梱包資材や梱包する商品は全て直方体としてみなすことができて、3辺の長さがあらかじめわかっているとします。 回転や折りたたみを許さないモデル まずは簡単のためにアイテムの回転や折りたたみは考慮しないモデルを考えます。 定数 , , : 梱包資材の3辺の長さ。 , , : アイテム の3辺の長さ。 決定変数 , , : アイテム の位置座標。 , , : アイテム がアイテム より各軸方向で原点側にある時1、そうでない時0をとる変数。 目的関数 前述の通りなし(定数)。 制約条件 全てのアイテムが梱包資材内に収まる制約 2つのアイテムが各軸の方向で重ならない制約 となるアイテム について制約がかかります。各制約はそれぞれ , , が1を取る時だけ有効になります。 また、2つのアイテムが3軸の前後関係の中でどれか1つは重ならない必要があるので、次の制約も必要です。 商品の回転や折りたたみを許さない場合は、以上の制約で梱包可否を判定するモデルとしての定式化は完了です。 回転と折りたたみを許すモデル 商品は梱包資材に対して様々な向きに回転して詰め込むことができます。また、Tシャツなど一部のカテゴリの商品は、標準の梱包状態からさらに2つ折りにして梱包することもできます。 回転や折りたたみといった形状変化はアイテムの寸法の変化として考えることができます。上記の定式化を以下のように変更することで寸法変化に対応できます。 追加の定数 , , : アイテム が形状 を採用する時の3辺の寸法。 決定変数の変更 : アイテム が形状 を取る時1、それ以外で0をとる変数を追加。 定数としていた , , を以下の式に変更する。 追加の制約条件 : アイテム はどれか1つの形状を採用するという制約 荷姿を整えるための目的関数 梱包資材とアイテムの寸法を用意して、上記の最適化モデルを最適化ソルバーに入力すると、モデルの実行可能性で梱包可否の判断が可能です。梱包可能な入力データを作成し、解いた結果を図示すると次のようになります。 目的関数が無いモデルは梱包可否の判定のためには十分ですが、計算結果を見るとアイテムが宙に浮いているなど現実的ではない荷姿が出てくることがあります。 荷姿の情報も参考情報として提示したい場合は、荷姿に関する目的関数を追加することで比較的現実的な計算結果が得られます。全商品の密度が同じだと仮定して、Z軸方向の重心位置を最小化する目的関数を設定すると次のような計算結果が得られます。 ソルバーが扱える目的関数には制限がありますが、シンプルな目的関数でもやや現実的な荷姿を得ることができました。 まとめ 本記事では梱包資材を選択するための数理最適化モデルについて紹介しました。 ZOZO NEXTでは、一緒に業務改善を見据えた研究開発をしてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co hrmos.co 組合せ最適化問題も含む、数理最適化問題とは何かについては 過去記事 で説明しています。 ↩
アバター
こんにちは、技術本部ML・データ部データ基盤ブロックの塩崎です。最近の気になる論文は、こちら 1 の動物病院での猫のストレスが音楽によって低減されるというものです。 さて、2年前にGCPの新米管理者になり色々と頑張っていますという内容のブログを公開しました。当時は対応が後手に回ってしまっていた内容でしたが、その後2年が経ったので、最近のGoogle Cloud管理者事情も紹介いたします。 この記事はGoogle Cloud Next'23 Tokyoの発表内容をブログにしたものです。イベント終了後にスライド公開が解禁されるため、終了し次第スライドも本記事に貼り付ける予定です。 前回のおさらい まずは、前回に公開した記事を軽く振り返ります。2年前に以下の記事を公開しました。幸いなことにSNSで多くの反応を頂き、弊社だけでなく多くの会社が管理業務に苦労している事がわかりました。 techblog.zozo.com どんな事件があったのかを軽く振り返ってみます。前回のブログを既に読んでいる方は次章まで読み飛ばしても問題ありません。 MyFisrt Projectが大量発生した事件 チュートリアル用途のプロジェクトであるMyFisrt Projectが大量に作られ、放置されていました。そのため、不要プロジェクトを削除した後にプロジェクト作成権限を限定的にする対応をとりました。 退職者の権限が残っていた事件 退職時のIAM権限削除が不十分であったため、退職者の権限が残っていました。Google Cloudのアカウント情報はMicrosoft Entra ID(旧Azure AD)とSAML連携されており、Entra ID側で無効化されていました。ですので、退職者がアクセスできていたわけではありませんが、望ましくない状況でした。kintoneに保存されている従業員マスタと突き合わせて権限を一括で削除するとともに、月次で棚卸しするようにしました。 Billing Accountが大量発生した事件 MyFisrt Projectを作成する時にBilling Accountも一緒に作成してしまうケースがありました。基本的にはMyFirst Projectと同様に対処しました。ですが、監査の都合によって消せないリソースやGoogle CloudのOrganization Admin権限でも操作不能なリソースがあったため、やや対応は煩雑でした。 教訓 この後手にまわった対応をした結果得られた教訓は、一度荒れてしまったら直すのは大変というものでした。そのために、荒れないようにするためのトラブルを未然に防ぐ活動も大事であると再認識しました。 トラブルの未然防止 ここからトラブルの未然防止のために行ったことを紹介します。まずは、Google Cloudに関する様々な情報をBigQueryに集約します。その後、トラブルに繋がりかねない「良くない臭い」をSQLで定義し、定期的にCloud Functionsからクエリを実行します。そして、臭いを検知したらSlackに通知する仕組みを作りました。 BigQueryに集約する理由 Google Cloudに関する情報をBigQueryに集約する理由について説明します。 連携機能が豊富 まず、BigQueryはGoogle Cloudの様々サービスと連携できるという点が挙げられます。この後に説明するサービスもBigQueryとの連携機能をデフォルトで持っている事が多いです。 パワフルな分析機能 次にパワフルな分析機能が挙げられます。BigQueryに集約しているデータ量は数TBにもなるため、ビッグデータ処理を念頭に作られてシステムでないと、分析できません。BigQueryは大量のコンピューティングリソースでパワフルな分析を行えます。 BigQueryが分析基盤の中核になっている 最後はBigQueryがGoogle Cloudにおける分析基盤の中核になっているという点です。BigQuery MLやBigQuery Studioなどの新サービス発表をみると、今後もBigQueryはますます便利になっていくであろうことが分かります。そのため、BigQueryにデータを集約することでBigQueryの成長に便乗できる可能性が高いです。 集約しているデータ紹介 どのようなデータをBigQueryに集約しているのかを紹介します。 Cloud Audit Logs Cloud Audit Logsから監査ログを取得できます。監査ログには「いつ」「どこで」「誰が」「何をしたのか」が記録されています。監査ログは主に以下の4つからなるため、それぞれを説明します。 Admin Activity audit logs リソースの構成やメタデータを変更する操作が記録されています。具体的にはGoogle Compute EngineでVMを作成したり、削除した時のログが一例です。 Data Access audit logs リソースの構成やメタデータを読み取る操作が記録されています。先程のログは「変更」操作でしたが、こちらのログは「読み取り」操作のログです。BigQueryテーブルへのSELECTやGoogle Cloud Storageからのファイルダウンロードなどが記録されます。 System Event audit logs Google Cloudのサービスによるリソース変更操作が記録されます。 Policy Denied audit logs セキュリティポリシー違反な操作を拒否したログが記録されます。 これら4つのうちで特に上2つのログを良く参照します。 cloud.google.com Cloud Asset Inventory Cloud Asset InventoryはGoogle Cloud内にあるアセットを検索・分析できるサービスです。先程のCloud Audit Logsがトランザクションデータだとするなら、Cloud Asset Inventoryはマスタデータに相当します。 アセットとは主に以下の3つを指します。これらの情報を検索・分析できます。 Resource Google Cloudリソースのメタデータです。例えば、どのようなVMが稼働しているのか、CPU・Memoryなどのスペックはどの程度なのかという情報です。 Policies リソースに対して設定されたポリシーのメタデータです。主にIAM Policyなどに関するデータが格納されていると考えれば分かりやすいです。 Runtime information OS Inventory Managementなどのランタイムに関するメタデータです。 cloud.google.com Cloud Billing Cloud BillingはGoogle Cloudの請求情報を管理しています。以下の3つの情報をBigQueryにエクスポートできますが、基本的には一番目の情報だけで十分なことが多いです。 Standard usage cost Project ID、サービス、SKU、使用量、費用などが含まれる請求情報です。 Detailed usage cost 上記の情報に加えてリソースレベルの情報が含まれた請求情報です。Google Compute EngineのVMレベルでの費用分析などを行う時に利用します。 Pricing SKUごとの単価情報です。 cloud.google.com kintone kintoneで管理しているマスタ情報があるため、これらの情報もBigQueryに集約しています。 従業員マスタ 従業員の氏名やメールアドレス(Google CloudのIDとしても使われる)などが格納されています。退職フラグが立っている従業員を抜き出すことで退職者情報を作成できます。 Google Cloud管理者マスタ Google Cloudのプロジェクトと管理者や管理部署などの情報が格納されています。Google Cloud全体の管理者とは別にプロジェクトごとにも管理者を立てる分割統治をしているため、プロジェクト管理者に連絡するために利用します。 集約する方法 先程のデータをどのようにしてBigQueryに集約しているのかを紹介します。 Cloud Audit Logs Cloud Audit Logsはログ情報をCloud Loggingに出力できるので、まずはそこに出力します。以下の設定でOrganization内の全ての監査ログがCloud Loggingに出力されます。 data "google_organization" "zozo-com" { domain = "zozo.com" } resource "google_organization_iam_audit_config" "zozo-com" { org_id = data.google_organization.zozo-com.org_id service = "allServices" audit_log_config { log_type = "ADMIN_READ" } audit_log_config { log_type = "DATA_READ" } audit_log_config { log_type = "DATA_WRITE" } } registry.terraform.io 次にCloud LoggingのLog Sink機能を使って、BigQueryにログを出力します。destinationには予め作成しておいたBigQueryのデータセットを指定します。 resource "google_logging_organization_sink" "audit_log_sink" { name = "audit_log_sink" org_id = data.google_organization.zozo-com.org_id destination = "bigquery.googleapis.com/$ { google_bigquery_dataset.audit_log.id } " include_children = true filter = "protoPayload.@type=\"type.googleapis.com/google.cloud.audit.AuditLog\"" } registry.terraform.io このログはデータ量が大きいため、古いログを定期的にGoogle Cloud Storage Archiveに移動させています。Cloud SchedulerからCloud Functionsを定期的に起動してデータを移動させています。このテクニックの詳細については以下のQiitaにもまとめています。 qiita.com この部分のアーキテクチャは以下のようになります。 Cloud Asset Inventory Cloud Asset Inventoryのデータは gcloud asset export コマンドでBigQueryにエクスポートできます。 cloud.google.com そのため、gcloudがインストールされたコンテナイメージを用意して、毎日Cloud SchedulerとCloud Runでコマンドを実行するようにしています。Cloud Run上で動いているコンテナはHTTPリクエストを受け取る必要があるので、goで簡単なHTTPサーバーを立てgcloudコマンドを起動しています。 このデータの容量は小さいので過去分のスナップショットを全て保存しています。 ソースコードの一部を抜粋します。 func main() { http.HandleFunc( "/export_asset_inventory" , scriptHandler) port := os.Getenv( "PORT" ) if port == "" { port = "8080" log.Printf( "Defaulting to port %s" , port) } // Start HTTP server. log.Printf( "Listening on port %s" , port) if err := http.ListenAndServe( ":" +port, nil ); err != nil { log.Fatal(err) } } func scriptHandler(w http.ResponseWriter, r *http.Request) { if r.Method == "POST" { cmd := exec.CommandContext(r.Context(), "/bin/bash" , "export_asset_inventory.sh" ) cmd.Stderr = os.Stderr out, err := cmd.Output() if err != nil { w.WriteHeader( 500 ) } w.Write(out) } } goから実行している export_asset_inventory.sh の内容も以下に示します。 #!/bin/bash set -eux # gcloud organization listコマンドで取得可能な12桁の数字 # ドメイン名ではないことに注意 organization_id = 123456789012 current_date = $( TZ = -9 date +%Y%m%d ) for content_type in resource iam-policy org-policy access-policy os-inventory do if [ $content_type == " resource " ]; then gcloud asset export --bigquery-dataset asset_inventory \ --bigquery-table " ${content_type} _ ${current_date} " \ --output-bigquery-force \ --organization $organization_id \ --content-type resource \ --snapshot-time " $( TZ = -9 date + " %Y-%m-%dT%H:%M:%SZ " ) " else gcloud asset export --bigquery-dataset asset_inventory \ --bigquery-table " ${content_type} _ ${current_date} " \ --output-bigquery-force \ --organization $organization_id \ --content-type $content_type \ --snapshot-time " $( TZ = -9 date + " %Y-%m-%dT%H:%M:%SZ " ) " \ --partition-key request_time fi done Cloud Billing Cloud BillingからBigQueryへのエクスポートはCLIやterraformでは設定できず、Webコンソールのみから設定できます。 cloud.google.com Cloud Bilingの管理画面からBilling exportメニューを開くと、設定画面があります。保存先のBigQueryのデータセットは予め作成しておく必要があります。 kintone kintoneからBigQueryへのデータエクスポートにはCloud FunctionsとBigQueryのRemote UDFを利用します。 詳しい方法は以下のテックブログ記事に記載しているので、こちらを参照下さい。 techblog.zozo.com 集約したデータを活用 ここからはBigQueryに集約したデータに対してどのようなクエリを実行しているのかを説明します。 BigQuery→Slack通知システム クエリ紹介の前にBigQueryからSlackに通知しているシステムを紹介します。 以下の記事に書かれているデータクオリティモニタリングシステムをSlack通知に活用しています。定期的にBigQueryへクエリを実行して、特定の条件(結果の行数が1行以上など)にマッチした時、Slackへ通知させています。元々は記事に書いてある通りデータクオリティモニタリング用途で開発されましたが、汎用性が高かったので流用しています。 techblog.zozo.com Google Cloud費用アラート 最初の活用事例として紹介するのはGoogle Cloud費用アラートです。Cloud Billingから出力されるデータを集計して通知しています。プロジェクト単位・サービス単位で日毎の費用をGROUP BYして、過去30日間の平均値を大きく上回った場合に通知しています。 クエリはシンプルなので省略し、代わりに通知の様子を紹介します。毎朝チェックが行われ、場合によっては以下のような通知がなされます。通知メッセージはテンプレートエンジンでカスタマイズ可能なので、柔軟な通知ができます。 ドメイン取得チェック 次にドメイン取得チェックについても紹介します。 ドメインのライフサイクルはサービスのそれよりも長くなることがあります。サービス終了後にドメインを更新せずに所有権を手放してしまうと問題になり、ネットニュースを賑わせることもあります。発生する問題の詳細についてはJPRSの注意喚起を参照下さい。 jprs.jp そのため、ZOZOではドメインを取得できる部署を一元化し、このような問題を防いでいます。Google Cloudでのドメイン取得は会社のルールで禁止しているために、ドメインが取得されていないかをチェックしています。取得されたドメイン情報はCloud Asset InventoryのResource情報の中に入っているので、以下のクエリでチェックできます。 select regexp_extract(name, r ' //domains.googleapis.com/projects/([a-zA-Z0-9-]+)/ ' ) as project_id, json_value( resource .data, ' $.domainName ' ) as domain_name, from < resource > asset_type = ' domains.googleapis.com/Registration ' and json_value( resource .data, ' $.state ' ) = ' ACTIVE ' なお、AWSでもドメイン取得を社内ルールで禁止しており、Service Control Policyを使ってより強固に禁止ルールを敷いています。 docs.aws.amazon.com Google CloudのDENY Policyでドメイン取得を禁止できるようになりましたら、DENY Policyに移行したいと考えています。 cloud.google.com 社外ユーザーの権限チェック Google Cloudは自組織以外が管理しているGoogle Workspaceユーザーにも権限付与ができ、組織間のコラボレーションが簡単にできます。しかし、無闇な他組織との共有は同時にガバナンスを低下させる原因にもなります。そのため、他組織と共有する際には事前申請制にしています。 権限情報はCloud Asset InventoryのIAM Policyに格納されています。以下のクエリで社外ユーザー(zozo.com以外のユーザー)に対して権限が付与されているかどうかをチェックしています。 -- 許可されたユーザー create temporary function is_exampt_member(member string) as ( member in ( ' user:hoge@example.com ' , ' user:fuga@example.com ' ) ); select distinct member from ( select binding.members from <iam-policy>, unnest(iam_policy.bindings) as binding ), unnest(members) as member where not is_exampt_member(member) and ( starts_with(member, " user: " ) and not ends_with(member, " @zozo.com " ) or starts_with(member, " group: " ) and not ends_with(member, " @zozo.com " ) or starts_with(member, " domain: " ) and not ends_with(member, " zozo.com " ) ) 以下のOrganization Policyを使うことでそもそも権限付与をさせないという、より強固な制限もできます。しかし、known issuesに書かれている運用が煩雑なため、権限付与の検知をしたら担当者に連絡をとって権限削除をしてもらう運用にしています。 cloud.google.com 社外Service Accountの権限チェック 先程の社外ユーザーの権限チェックのService Account版です。個人の権限は@の後ろ側のドメインだけで判断できるので簡単でしたが、Service Accountはドメインの単純な比較では判断できません。ドメインの一部にProject IDが含まれているので、そのIDが組織内に属しているかで判断します。 まずは、Cloud Asset InventoryのResource情報から自組織のProject ID一覧を作成します。 with project_ids_in_zozo as ( select json_value( resource .data, ' $.projectId ' ) as project_id from < resource > where asset_type = ' cloudresourcemanager.googleapis.com/Project ' ) 次にService Accountにはユーザー管理のものとGoogle管理のものがあるので、それらを判別する関数も作成します。 create temporary function serviceaccount_type(member string) as ( case when regexp_contains(member, r ' ^serviceAccount:.+\.svc\.id\.goog\[.+\]$ ' ) then " WorkloadIdentity " when regexp_contains(member, r ' ^serviceAccount:\d+(-compute)?@developer\.gserviceaccount\.com$ ' ) then " GCE default " when regexp_contains(member, r ' ^serviceAccount:.+@appspot\.gserviceaccount\.com$ ' ) then " AppEngine default " when regexp_contains(member, r ' ^serviceAccount:\d+@cloudbuild\.gserviceaccount\.com$ ' ) then " Cloud Build default " when regexp_contains(member, r ' ^serviceAccount:\d+@cloudservices\.gserviceaccount\.com$ ' ) then " Google API Service Agent " when regexp_contains(member, r ' ^serviceAccount:service-\d+@(cloud-ml|gae-api-prod)\.google.com\.iam\.gserviceaccount\.com$ ' ) then " Google API Service Agent " when regexp_contains(member, r ' ^serviceAccount:.+@gcp-sa-[a-z-]+\.iam\.gserviceaccount\.com$ ' ) then " Google API Service Agent " when regexp_contains(member, r ' ^serviceAccount:.+@bigquery-data-connectors\.iam\.gserviceaccount\.com$ ' ) then " Google API Service Agent " when regexp_contains(member, r ' ^serviceAccount:(project-|service-|service-org-)\d{9,}@[-a-z0-9]+\.iam\.gserviceaccount\.com$ ' ) then " Google API Service Agent " when regexp_contains(member, r ' ^serviceAccount:analytics-processing-dev@system\.gserviceaccount\.com$ ' ) then " Google Analytics Service Agent " when regexp_contains(member, r ' ^serviceAccount:firebase-.+@system\.gserviceaccount\.com$ ' ) then " Firebase Service Agent " when regexp_contains(member, r ' ^serviceAccount:.+@(crashlytics-bigquery-prod|fcm-bq-export-prod|performance-bq-export-prod|firebase-sa-management)\.iam\.gserviceaccount\.com$ ' ) then " Firebase Service Agent " when regexp_contains(member, r ' ^serviceAccount:backups@firebase-prod\.iam\.gserviceaccount\.com$ ' ) then " Firebase Service Agent " when regexp_contains(member, r ' ^serviceAccount:appsdev-apps-dev-script-auth@system\.gserviceaccount\.com$ ' ) then " AppScript Service Agent " when regexp_contains(member, r ' ^serviceAccount:billing-export-bigquery@system\.gserviceaccount\.com$ ' ) then " Billing export Service Agent " when regexp_contains(member, r ' ^serviceAccount:gapps-reports@system\.gserviceaccount\.com$ ' ) then " Google Workspace Service Agent " when regexp_contains(member, r ' ^serviceAccount:.+@[-a-z0-9]+\.iam\.gserviceaccount\.com$ ' ) then " User Managed " else " Unknown " end ); そして最後に、これらを組み合わせることで自組織外のService Account一覧を出力します。「自組織に所属していない」かつ「Google管理のService Accountではない」という条件で対象のService Accountを抽出しています。 with user_managed_service_accounts as ( select distinct member, regexp_extract(member, r ' ^serviceAccount:.+@([-a-z0-9]+)\.iam\.gserviceaccount\.com$ ' ) as project_id from ( select binding.members from <iam-policy>, unnest(iam_policy.bindings) as binding ), unnest(members) as member where starts_with(member, " serviceAccount: " ) and serviceaccount_type(member) = ' User Managed ' ) select member from user_managed_service_accounts where project_id not in ( select project_id from project_ids_in_zozo) and not exampted_service_accounts(member) -- 前節同様に許可されたService Accountかどうかを判断する関数を用意 order by member asc 退職者の権限チェック 退職者の権限が残っているかどうかをチェックするクエリを紹介します。 kintoneから取得した従業員マスタを使って退職フラグが立っている従業員を抽出し、退職者マスタを作成します。 with retired_employees as ( select json_value( row .mail_address) as mail_address, from unnest(( select json_query_array(<kintoneからデータ読み出しをするUDF>(従業員マスタのapp_id), " $ " ) )) as row where json_value( row .leaving_date) is not null ) 次にCloud Asset InventoryのIAM Policy情報とJOINすることで、退職者が持っているロール一覧を出します。 with iam_policies as ( select name, asset_type, role, regexp_extract(member, r ' ^.+:(.+?)(?:\?.+)?$ ' ) as member, from <iam_policy> unnest(iam_policy.bindings) as binding, unnest(binding.members) as member ), retired_employee_roles as ( select   i.* from iam_policies as i join retired_employees as r on i.member = r.mail_address ) この時点で必要最低限のものは完成しているのですが、このまま使用するとどのリソースに対して付与されているロールなのかが分かりづらいです。そのため、以下のようにCloud Asset InventoryのResource情報ともJOINしてリソース情報を補います。 with retired_employee_roles_full as ( select resolve_project_id(retired_employee_roles.name, retired_employee_roles.asset_type, resource . resource .data) as project_id, -- 関数の説明は後述 resolve_resource_name(retired_employee_roles.name, retired_employee_roles.asset_type, resource . resource .data) as resource_name, retired_employee_roles.name as full_resource_name, retired_employee_roles.asset_type, retired_employee_roles.role, retired_employee_roles.member, from retired_employee_roles left join < resource > as resource using (name) ) ここで注意が必要な点として、Resource情報の中のどこにProject IDやリソース名が格納されているのかは asset_type によるという点です。そのために、以下のようなヘルパー関数を用意して、 resource_data などからProject IDとリソース名を抽出できるようにしました。 create temporary function lookup_project_id_from_number(project_number any type ) as (( select json_value( resource .data, ' $.projectId ' ) from < resource > where name = concat ( ' //cloudresourcemanager.googleapis.com/projects/ ' , project_number) )); create temporary function resolve_project_id_or_number(full_name string, asset_type string, resource_data string) as ( case asset_type when ' cloudresourcemanager.googleapis.com/Organization ' then " <Organization> " when ' cloudbilling.googleapis.com/BillingAccount ' then ' <Billing Account> ' when ' cloudresourcemanager.googleapis.com/Project ' then json_value(resource_data, ' $.projectId ' ) when ' bigquery.googleapis.com/Dataset ' then regexp_extract(full_name, r ' //bigquery.googleapis.com/projects/(.+)/datasets/.+ ' ) when ' storage.googleapis.com/Bucket ' then json_value(resource_data, ' $.projectNumber ' ) when ' iap.googleapis.com/WebType ' then regexp_extract(full_name, r ' //iap.googleapis.com/projects/(.+)/iap_web/.+ ' ) when ' iam.googleapis.com/ServiceAccount ' then regexp_extract(full_name, r ' //iam.googleapis.com/projects/(.+)/serviceAccounts/.+ ' ) when ' compute.googleapis.com/Subnetwork ' then regexp_extract(full_name, r ' //compute.googleapis.com/projects/(.+)/regions/.+/subnetworks/.+ ' ) when ' cloudkms.googleapis.com/CryptoKey ' then regexp_extract(full_name, r ' //cloudkms.googleapis.com/projects/(.+)/locations/.+/keyRings/.+/cryptoKeys/.+ ' ) when ' bigquery.googleapis.com/Table ' then json_value(resource_data, ' $.tableReference.projectId ' ) when ' orgpolicy.googleapis.com/Policy ' then regexp_extract(full_name, r ' //orgpolicy.googleapis.com/projects/(.+)/policies/.+ ' ) when ' cloudresourcemanager.googleapis.com/Folder ' then " <Folder> " when ' iap.googleapis.com/Web ' then regexp_extract(full_name, r ' //iap.googleapis.com/projects/(.+)/.+ ' ) when ' iap.googleapis.com/WebService ' then regexp_extract(full_name, r ' //iap.googleapis.com/projects/(\d+)/.+ ' ) when ' iap.googleapis.com/TunnelInstance ' then regexp_extract(full_name, r ' //iap.googleapis.com/projects/(.+)/iap_tunnel/.+ ' ) when ' secretmanager.googleapis.com/Secret ' then regexp_extract(full_name, r ' //secretmanager.googleapis.com/projects/(.+)/secrets/.+ ' ) else " <Unknown asset_type> " end ); create temporary function resolve_project_id(full_name string, asset_type string, resource_data string) as ( if ( regexp_contains(resolve_project_id_or_number(full_name, asset_type, resource_data), r ' ^\d+$ ' ), lookup_project_id_from_number(resolve_project_id_or_number(full_name, asset_type, resource_data)), resolve_project_id_or_number(full_name, asset_type, resource_data) ) ); create temporary function resolve_resource_name(full_name string, asset_type string, resource_data string) as ( case asset_type when ' cloudresourcemanager.googleapis.com/Organization ' then json_value(resource_data, ' $.displayName ' ) when ' cloudbilling.googleapis.com/BillingAccount ' then json_value(resource_data, ' $.displayName ' ) when ' cloudresourcemanager.googleapis.com/Project ' then json_value(resource_data, ' $.projectId ' ) when ' bigquery.googleapis.com/Dataset ' then regexp_extract(full_name, r ' //bigquery.googleapis.com/projects/.+/datasets/(.+) ' ) when ' storage.googleapis.com/Bucket ' then regexp_extract(full_name, r ' //storage.googleapis.com/(.+) ' ) when ' iap.googleapis.com/WebType ' then regexp_extract(full_name, r ' //iap.googleapis.com/projects/.+/iap_web/(.+) ' ) when ' iam.googleapis.com/ServiceAccount ' then json_value(resource_data, ' $.email ' ) when ' compute.googleapis.com/Subnetwork ' then json_value(resource_data, ' $.name ' ) when ' cloudkms.googleapis.com/CryptoKey ' then regexp_extract(full_name, r ' //cloudkms.googleapis.com/projects/.+/locations/.+/keyRings/.+/cryptoKeys/(.+) ' ) when ' bigquery.googleapis.com/Table ' then concat (json_value(resource_data, ' $.tableReference.datasetId ' ), " . " , json_value(resource_data, ' $.tableReference.tableId ' )) when ' orgpolicy.googleapis.com/Policy ' then regexp_extract(full_name, r ' //orgpolicy.googleapis.com/projects/.+/policies/(.+) ' ) when ' cloudresourcemanager.googleapis.com/Folder ' then json_value(resource_data, ' $.displayName ' ) when ' iap.googleapis.com/Web ' then regexp_extract(full_name, r ' //iap.googleapis.com/projects/.+/(.+) ' ) when ' iap.googleapis.com/WebService ' then regexp_extract(full_name, r ' //iap.googleapis.com/projects/\d+/(.+) ' ) when ' iap.googleapis.com/TunnelInstance ' then regexp_extract(full_name, r ' //iap.googleapis.com/projects/.+/iap_tunnel/(.+) ' ) when ' secretmanager.googleapis.com/Secret ' then regexp_extract(full_name, r ' //secretmanager.googleapis.com/projects/.+/secrets/(.+) ' ) else " <Unknown asset_type> " end ); さらに、kintoneからGoogle Cloud管理者マスタも取得してプロジェクト毎の管理者への連絡に使用します。 with gcp_admin as ( select json_value( row .project_id) AS project_id, array( select json_value(elem.code) from unnest(json_query_array( row .administrator, ' $ ' )) as elem) as administrator, from unnest(( select json_query_array(<kintoneからデータ読み出しをするUDF>(Google Cloud管理者マスタのapp_id), " $ " ) )) as row ) select * except(administrator), array_to_string(administrator, " , " ) as administrator, from retired_employee_roles_full left join gcp_admin using (project_id) order by project_id, resource_name 上記のクエリを定期的に実行し、退職者の権限が検知された場合には以下のようなSlack通知がなされます。 BigQuery VIEWの参照状況 ここからはOrganizationの管理というよりもBigQueryの管理業務の話を紹介します。 テーブルの修正や削除する際の影響範囲を調査するために、 INFORMATION_SCHEMA.JOBS_BY_* を参照することはよくあります。 referenced_tables 列にそのジョブが参照しているテーブル情報が格納されています。しかし、この方法ではVIEWの参照状況を取得できません。 cloud.google.com その代わりにデータアクセス監査ログから参照されているVIEWの情報を取得できるので、そのためのクエリを紹介します。監査ログの収集に関する章で説明したように古いログはGCS Archiveに移動しています。そのため、このクエリを日次で実行した結果を積み上げたデータマートを作成しています。 select protopayload_auditlog.authenticationInfo.principalEmail as email, resource .labels.project_id, protopayload_auditlog.servicedata_v1_bigquery.jobCompletedEvent.job.jobName.jobId as job_id, timestamp , protopayload_auditlog.servicedata_v1_bigquery.jobCompletedEvent.job.jobStatistics.referencedViews as referenced_views, protopayload_auditlog.servicedata_v1_bigquery.jobCompletedEvent.job.jobConfiguration.query.query, from <cloudaudit_googleapis_com_data_access> where protopayload_auditlog.serviceName = " bigquery.googleapis.com " and protopayload_auditlog.methodName = " jobservice.jobcompleted " BigQueryを参照しているGoogle Sheets URLの調査 Connected Sheets機能を使うとGoogle SheetsからBigQueryに接続できます。この機能はとても便利ですが、クエリは発行されているけれども発行元のシートは不明という問題も起きやすいです。 cloud.google.com INFORMATION_SCHEMA.JOBS_BY_* の job_id 列を見ると、シートからのクエリであることは分かりますが、シートURLまでは不明です。そのため、監査ログからクエリの発行元シートURLを取得します。 select protopayload_auditlog.authenticationInfo.principalEmail as email, resource .labels.project_id, REGEXP_EXTRACT(protopayload_auditlog.resourceName, r ' ^projects/[\w-]+/jobs/([\w-]+)$ ' ) as job_id, timestamp , " https://docs.google.com/spreadsheets/d/ " || json_value(protopayload_auditlog.metadataJson, " $.firstPartyAppMetadata.sheetsMetadata.docId " ) as sheet_url, json_value(protopayload_auditlog.metadataJson, " $.jobInsertion.job.jobConfig.labels.sheets_trigger " ) as trigger , json_value(protopayload_auditlog.metadataJson, " $.jobInsertion.job.jobConfig.queryConfig.query " ) as query, from <cloudaudit_googleapis_com_data_access> where protopayload_auditlog.serviceName = " bigquery.googleapis.com " and json_value(protopayload_auditlog.metadataJson, " $.firstPartyAppMetadata.sheetsMetadata.docId " ) is not null BigQueryテーブルの作成者調査 BigQueryのテーブルメタデータには作成者に関する情報が含まれていません。ですが、不要テーブルの削除やテーブルに格納されたデータの問い合わせなどの業務で作成者の情報が必要になることもあります。 そのため、監査ログからテーブルの作成者を取得します。 select protopayload_auditlog.authenticationInfo.principalEmail as email, split(protopayload_auditlog.resourceName, " / " )[safe_offset( 1 )] as project_id, split(protopayload_auditlog.resourceName, " / " )[safe_offset( 3 )] as dataset_id, split(protopayload_auditlog.resourceName, " / " )[safe_offset( 5 )] as table_id, timestamp , from <cloudaudit_googleapis_com_activity> where protopayload_auditlog.serviceName = " bigquery.googleapis.com " and array_length(protopayload_auditlog.authorizationInfo) = 1 and protopayload_auditlog.authorizationInfo[safe_offset( 0 )].permission = " bigquery.tables.create " and regexp_contains(protopayload_auditlog.resourceName, r ' projects/.+/datasets/.+/tables/.+ ' ) 監査ログに対するクエリのコツ 監査ログはカラム数が約1000個もあり、構造体や配列が何重にも入れ子になっています。そのため、ちょっとしたクエリを書くだけでも一苦労です。そのような監査ログに対するクエリを書くコツを紹介します。 1. 配列に慣れる SQLの配列機能は独特です。Javaなどの言語に当たり前のようにあるforループがSQLには存在しないので、配列をイテレーションする方法が独特です。以下の公式ドキュメントに書かれているテクニックは基本的なものなので自然と暗記するまで通読しましょう。 cloud.google.com 2. CONTAINS_SUBSTR関数 この関数は第一引数にテーブルを渡すこともできる、少し特殊な関数です。その場合はテーブル内の全部の列が検索対象になります。構造体や配列の中身もトラバースして検索してくれるので、非構造化された監査ログ全体を対象にして検索できます。ただし、テーブル全体の検索は重い処理なので、探索的データ分析中の利用だけにとどめて定形バッチでは用いないほうが良いです。 cloud.google.com 3. JSON形式でダウンロードしてjqで開く 監査ログテーブルはNULLや空配列が多く格納されています。そのため、テーブル形式で見るとNULLが連続して見づらいことがあります。クエリ結果をJSON形式でダウンロードしてjqなどで開くと「比較的」読みやすくなります。 4. 自分でログを作る 調査対象の操作を自分自身で行い、どの操作でどのログが発行されるのかを調査すると、監査ログに対する理解が深まります。 5. 慣れ 監査ログに対するクエリを見ていると、 principalEmail などの頻繁に参照されるカラムが登場します。そのため、他人が書いたコードを解読し、頻発するイディオムに慣れましょう。 6. 挫折に負けない この記事で紹介しているクエリは完成形だけを見ると難解なものが多いです。どのクエリも一気に書いたものではなく、試行錯誤や挫折をしながら作り上げたものですので、根気強さが大事です。 他にも監査ログで色々とやっています この記事では紹介しきれませんでしたが、監査ログを使って他にも色々とチェックしています。いくつかの事例の概要だけ紹介します。 1. 未使用Service Accountチェック Cloud Asset InventoryのResource情報からService Accountマスタを作成して、監査ログとLEFT JOINします。これによってアクティビティのないService Accountを洗い出しています。同様のことをService AccountのJSON Keyに対しても行っています。 2. 古いSDKのチェック 監査ログにUserAgent情報が格納されているため、これを活用して古い言語・古いSDKが使われていないか確認しています。 3. JOBS_BY_ORGANIZATION にquery列を追加 INFORMATION_SCHEMA.JOBS_BY_ORGANIZATION にはquery列が存在しないため、扱いづらいです。そこで、監査ログから生成したJob IDとクエリの対応表をJOINすることで擬似的にquery列を追加しています。 まとめ ZOZOでは多くの社員がGoogle Cloudを利用しており、継続的な平和維持のための活動は必須です。一度荒れてしまうと元の状態に戻すことはとても大変です(経験談)。 BigQueryはサービス分析だけではなく、Google Cloudの利用状況を分析することにも使えます。そのためにGoogle Cloudに関する各種の情報をBigQueryに集約しました。そして、SQLでルールを定義し、違反があった場合にSlackへ通知するシステムを構築しました。 ZOZOでは、一緒に楽しく働く仲間を募集中です。ご興味のある方は下記採用ページをご覧ください! corp.zozo.com https://journals.sagepub.com/doi/full/10.1177/1098612X19828131 ↩
アバター
はじめに こんにちは、DevRelブロックの ikkou です。11月6日に「ZOZO Tech Meetup - Webフロントエンド」と題したWebフロントエンドに特化したオフラインイベントを開催しました。 zozotech-inc.connpass.com 目次 はじめに 目次 当日の登壇内容 アイスブレイク ZOZOTOWNにCSS in JS(Emotion)を導入して1年後の状況 React でコンポーネントを利用したテストをゴリゴリ書く ゼロから始めるアクセシビリティ啓蒙活動 現代のReactivityとSvelteの魔法 懇親会での取り組み 最後に 当日の登壇内容 ZOZOのWebフロントエンドエンジニア4名が以下の内容で登壇しました。 タイトル 登壇者 ZOZOTOWNにCSS in JS(Emotion)を導入して1年後の状況 菊地 宏之 ( @hiro0218 ) Reactでコンポーネントを利用したテストをゴリゴリ書く 渋谷 拓正 ( @bomb_phage ) ゼロから始めるアクセシビリティ啓蒙活動 田嶋 幸智子 ( @schktjm ) 現代のReactivityとSvelteの魔法 冨川 宗太郎 ( @ssssotaro ) 今回のイベントではオンライン配信を実施せず、オフライン会場でのみの開催となりました。登壇内容をまとめてお届けします。 アイスブレイク 多くのイベントでは本編の開始前に諸注意などを伝える時間が設けられていますが、今回はその中でSlidoの投票機能を利用したアンケートを実施しました。 今回のイベントはWebフロントエンドに特化していたので、「もっとも利用しているJavaScriptフレームワーク」や「最近CSSをどのように書いているか」をお聞きしました。 Slidoの投票機能はリアルタイムに回答結果が表示されるので、参加者の皆さんも楽しんでいただけたのではないでしょうか。 ZOZOTOWNにCSS in JS(Emotion)を導入して1年後の状況 speakerdeck.com ZOZOTOWNでWebフロントエンドエンジニアを務めている菊地は、1年前に公開した「 ZOZOTOWN Webフロントエンドリプレイスにおける CSS in JS の技術選定で Emotion を選定した話 」の続編として、その後の状況を紹介しました。 CSS in JSのライブラリは多くのものがありますが、ZOZOTOWNではWebフロントエンドリプレイスに際して「Emotion」を採用しています。このリプレイスをきっかけとしてCSS in JSに触れたメンバーも少なくなく、この1年を通してどのような変化が見られたか、チーム内におけるアンケート結果を紹介しました。また、これで終わりとせず、今後も引き続きCSS in JSの動向を追っていきたいという内容で締めました。 React でコンポーネントを利用したテストをゴリゴリ書く speakerdeck.com ZOZOTOWNでWebフロントエンドエンジニアを務めている渋谷は、3年前に公開した「 React Hooksでテストをゴリゴリ書きたい - react-reduxやaxiosが使われているような場合もゴリゴリテストを書きたい 」当時の考えから、今現在どのように考えているかを紹介しました。 テストの具体的な書き方を紹介するものではありませんが、コードを交えてわかりやすく解説し、最後には「楽しくテストを書いていきましょう」と締めました。 ゼロから始めるアクセシビリティ啓蒙活動 speakerdeck.com Webアクセシビリティについては、障害者差別解消法の改正に伴い「合理的な配慮」が義務化されることをご存知の方も多いのではないでしょうか。 ZOZOTOWNでWebフロントエンドエンジニアを務めている田嶋は、ZOZOのWebフロントエンド領域における「アクセシビリティ改善運動」の状況を紹介しました。まだこれからという状況ではありますが、今後も継続的に取り組んでいきたいという内容で締めました。 現代のReactivityとSvelteの魔法 speakerdeck.com WEARでWebフロントエンドエンジニアを務めている冨川は、その場でコードを書くライブデモを交えながら、タイトルの通り「現代のReactivityとSvelteの魔法」を紹介しました。 他3名の登壇者とは異なり、ライブデモを中心としていたため、公開資料だけでは伝わりにくい部分もあるかと思いますが、会場にいた方は楽しんでいただけたのではないでしょうか。 懇親会での取り組み 4人の登壇後には懇親会を開催しました。今回のイベントでは、受付時に名札を兼ねた自己紹介カードをお渡ししています。こういったものを用意することで、懇親会での会話はよりスムーズになったのではないでしょうか。オフラインイベントでは今後も取り入れていく予定です。 最後に 当日は多くの方にご参加いただき、懇親会もとても盛り上がりました。ご回答いただいたアンケートの結果をもとにして、今後もWebフロントエンドに特化したイベントを開催していきたいと考えています。 ZOZOでは、一緒にサービスを作り上げてくれる方をWebフロントエンドエンジニアを募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co hrmos.co hrmos.co
アバター