TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

987

はじめに こんにちは。ブランドソリューション開発本部バックエンド部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
はじめに こんにちは、計測プラットフォーム開発本部SREブロックの近藤です。普段はZOZOMATやZOZOGLASS、ZOZOFITなどの計測技術に関わるプロダクトの開発、運用に携わっています。計測プラットフォーム開発本部では、以前プロダクト単位でSLO(Service Level Objective) 1 を定めましたが、うまく活用できず、再度SLOについて運用方法を考え直すことになりました。本記事では、SLOの再導入から運用に向かう中で見つかった課題と、課題に対する対応策についてご紹介します。 目次 はじめに 目次 背景 要因分析 Problem Try Action Actionの実行 SLO設定時の段階分け 例:ZOZOMATの段階分け 課題の洗い出し 例:SLOがない事による課題(SRE視点) 目的の明確化 信頼性とはそもそも何か 一般的な信頼性 計測プロダクト UJの整理 SLOの定期的な見直しと改善 SLOダッシュボード まとめ 背景 前述したように、各プロダクト毎にSLOを導入しましたが、運用できているとは言えない状況でした。具体的には、SLOを活用した改善が進められておらず、また他チームやビジネスサイドに浸透できていませんでした。これらの課題について要因分析し、対応策を検討しました。 要因分析 要因分析としてSREチーム内でKPTAフレームワークを使った振り返りを実施しました。この場で挙げられたProblem、Try、Actionを以下に記載します。 Problem SLOの作成目的の抽象度が高く、解決したい課題が明確になっていなかった 過去に設定した目的:ステークホルダーがどの作業を優先すべきかを決定するため SLOの作成が優先され、何故作るのかが深掘りできていなかった SLOの啓蒙を十分に行わずに他のチームへ展開してしまった SLOが安定しており、課題が出てこない Try SLOによって何を得たいか具体化する 現状の課題とSLOの目的を明確にする 以下に例を載せるが、SLOがないことによる課題をあげ、SLOが運用されることでどういった状態を目指すか考える 例: 課題:サービスが信頼性を担保できているのかわからない 具体例:レイテンシが悪化傾向にあったとして、対応すべき問題かわからない 目指す状態:SLOがあることにより、サービスの信頼性について判断でき、信頼性の低下時に対応方法の議論/改善が行える 初期段階から他チームやビジネスチームを巻き込むのは難易度が高い SREだけでなく、他チームとSLOについて認識を合わせる 段階を分け、各段階毎に課題と目的を明確にする 上記で他チームと記載しましたが、ここで計測プラットフォーム開発本部の組織体制図を紹介します。体制図からわかる通り、職能毎にチームが分かれている状態となっています。ビジネスサイドはさらに別の組織となっているため、ここでは割愛します。 ※引用元スライド: https://speakerdeck.com/zozodevelopers/company-deck Action SLO設定時の段階分け 各段階の課題と目的を明確化 SLOの定期的な見直しと改善 Actionの実行 SLO設定時の段階分け 段階の分け方としては、計測システム部、計測プラットフォーム開発本部、と巻き込む範囲をまずは段階の要素として考え、次にSLOの成熟度も段階の要素として考えました。この2つの要素を縦軸を成熟度、横軸を巻き込む範囲としてグラフ化し、プロダクトのフェーズや状態によって調整しようと考えました。実際には、SLOの計測、可視化、目標の調整、目標値を下回った際の対応など細かいフェーズがありますが、図では簡略化しています。 実際に行った段階分けについてはプロダクト毎によって異なりますが、ここではZOZOMATを例として記載します。 縦軸がSLAの箇所に行くにはビジネスサイドを巻き込む必要があり必然的に横軸を進める必要がありますが、ZOZOMATはToB向けのサービスではないためSLOの運用までとし、横軸で段階分けを行いました。 例:ZOZOMATの段階分け 段階 状態 1 SREチーム内でSLOの運用が開始されている 2 計測システム部内でSLOの運用が開始されている 3 POを含め、SLOの運用が開始されている 課題の洗い出し まずはSLOがないことによる課題を洗い出すところから始めました。ここでは、段階1のSREチーム内で洗い出した課題を例として記載します。 例:SLOがない事による課題(SRE視点) サービスが信頼性を担保できているのかわからない 具体例:メトリクスに変化があっても問題か判断できない 仮に信頼性が低下傾向にあるとわかっても、信頼性を向上させることを他の事項よりも優先すべきか判断できない 具体例:システム振り返りの調査タスクが積もりがち、プランニングで優先度を判断できない 上記で記載したシステム振り返りは、以下のような形で行っています。 開催頻度:週次 参加者:計測システム部所属メンバー 対象システム:ZOZOMAT、ZOZOGLASS、ZOZOFIT 確認する項目:AWSコスト、Datadogコスト、アラート通知履歴、プロダクト毎のDatadogのダッシュボード 進め方:確認項目で気になる点を各自が記載した後、同期的に議論し、改善や調査の必要があればタスクを起票する 目的の明確化 課題について深掘りし、目的を明確化しました。ここでは、段階1のSREチーム内での目的を例として記載します。 信頼性とはそもそも何か 課題に記載した信頼性について言語化ができていない状態だったので、信頼性については定義することから始めました。信頼性が高い状態=ユーザに価値を継続して提供できている状態と考え、それはどういった状態かチーム内で議論しました。まずは、一般的な信頼性について考え、次に計測プロダクトに当てはめた場合について考えてみました。 一般的な信頼性 ユーザに価値を提供できていない状態とはどんな状態か ユーザが体験を損なわない速度で応答を返せているか 計測プロダクト 計測に時間がかかりすぎて、こんなに待てないとなっていないか ユーザが体験を損なわない成功率を出せているか 計測の成功率が低く、繰り返しの計測が求められ、離脱に繋がっていないか ここで計測プロダクトの特性を加味した信頼性について考えることで、SLIを立てる際にユーザージャーニー(以下、UJ)を整理して調整することになりました。 UJの整理 このように課題を洗い出して深堀りした上で、最初の目的は「SREチームがサービスの信頼性について判断できる状態にし、信頼性の低下を防止できるようになる」としました。 同じ流れで各段階毎の課題と目的を明確化し、最終的にZOZOMATについては以下のように整理されました。 段階 目標 目的 課題が解決された状態 1 SREチーム内でSLOの運用が開始されている SREチームがサービスの信頼性について判断できる状態にし、信頼性の低下を防止できるようになる プランニングで信頼性の向上施策について、優先度を判断できる 2 計測システム部内でSLOの運用が開始されている 計測システム部がサービスの信頼性について判断できる状態にし、信頼性の低下を防止できるようになる システム振り返りで調査対応すべきタスクを定量的な指標で判断できるようになる 3 POを含め、SLOの運用が開始されている POがサービスの信頼性について判断できる状態にし、信頼性の低下を防止できるようになる 信頼性が低下した場合に、他プロダクトの案件と比較して、優先度を判断できる SLOの定期的な見直しと改善 SLOの定期的な見直しと改善に関しては、チームでスクラムを採用している関係もあり、スクラムイベントの中に組み込みました。具体的には、SLOの確認はリファインメントで行い、課題が見つかった場合もその場で改善タスクを起票すると言った形です。当初は定期的な振り返りを想定していましたが、結果としてMTGの時間を増やす事なく、SLOの評価と改善が行えるようになりました。 SREチームのスクラムについてはブログ記事が公開されているので、気になる方はこちらの記事をご参照ください。 techblog.zozo.com SLOダッシュボード SLOの定期的な見直しと改善をするにあたってダッシュボードを整備しました。工夫したポイントとしては、1つのダッシュボードで全てのプロダクトのSLOが確認できる点と、SLOで問題があるものだけが表示される点です。情報量が多くなると見落としがちになるため、必要な情報のみ出すことで議論すべきSLOが明確になりました。なお、ZOZOFITのSLOは今後導入を予定しています。 まとめ 本記事では、SLOを導入し運用を進める中での課題と対応策について紹介しました。現状はSREチーム内での運用が開始され、次の段階である計測システム部での運用を目指している状態です。導入し運用に向かう中で見つかった課題と向き合うことで、本来解決したかった課題と目的を改めて考える機会を得られ、結果としてSLOの設計から見直すことができ、運用フローもチームに適した形となりました。SLOの作成や導入を急ぐと陥りがちな状況かもしれませんが、これからSLOの導入を進める方、運用に課題を感じている方の参考になれば幸いです。 余談になりますが、稼働中のPODに対して変更する上でエラーは発生するもののSLOに問題ない=サービスとして許容可能なリスクであると判断し、変更方法を決める際の後押しにもなりました。信頼できるSLOがあることで攻めの姿勢を選択しやすくなりました。これは予期せぬ副産物でした。 ZOZOでは一緒にサービスをより良い方向に改善して頂ける方を募集中です。計測プラットフォーム開発本部としては、既存サービスの改善を日々行いつつ、新規サービスの開発も行っています。このような環境を楽しみ、サービスを一緒に盛り上げていける方を募集しています。少しでもご興味のある方は以下のリンクからぜひご応募ください。 corp.zozo.com (引用: SLO:サービスの信頼性の目標レベル) ) ↩
はじめに こんにちは、ML・データ部MLOpsブロックの松岡です。 本記事では Cloud Composer のワークフローにおいて、GPUを使うタスクで発生した Google Cloud のGPU枯渇問題と、その解決のために行った対策を紹介します。 ZOZOが運営する ZOZOTOWN ・ WEAR では、特定の商品やコーディネート画像に含まれるアイテムの類似商品を検索する 類似アイテム検索機能 があります。本記事ではこの機能を画像検索と呼びます。 画像検索では類似商品の検索を高速に行うため、画像特徴量の近傍探索Indexを事前に作成しています。近傍探索Indexはワークフローを日次実行して作成しています。 このワークフローでは大きく次のように処理を行っています。 当日追加された商品の情報を取得し、商品情報をもとに商品画像を取得する。 物体検出器で商品画像から商品が存在する座標とカテゴリーを検出する。 検出した座標で、商品画像を切り抜き、画像の特徴量を抽出する。 特徴量からカテゴリーごとに、 Spotify が開発したPython製の近傍探索ライブラリである Annoy を使って近似最近傍探索Indexを作成する。 我々はこのワークフローをCloud Composer上に構築しています。Cloud ComposerとはGoogle Cloudにおける Apache Airflow のマネージドサービスです。Cloud Composerには大きく Cloud Composer 1 と Cloud Composer 2 があり、画像検索のワークフローはより新しいCloud Composer 2に移行済みです。 Apache Airflowでは個別の処理をタスクとして記載し、タスク同士の依存関係を有向非巡回グラフ( Directed Acyclic Graph : DAG)として記載することでワークフローを定義します。本記事の中でもワークフローのことはDAGと記載します。 画像検索の近傍探索Indexを作成するDAGのなかで、物体検出と特徴量抽出のタスクはMLモデルを利用しており高速化のためにGPUを使用しています。 Annoyにつきましては、弊社テックブログの 近傍探索ライブラリ「Annoy」のコード詳解 もご参照ください。 今回はこのDAGを運用する中で発生した、Google Cloud内部のGPUリソース枯渇による課題と、その解決のために行った対策について説明します。 目次 はじめに 目次 GPUを使用する構成 GPUを割り当てたNodeが起動しないことによるタスクの失敗 タスクが失敗した原因の調査 サポートチームの回答と提案された暫定対応 Google Cloud内部のGPUリソース枯渇への対策 対応1 GKEクラスタのロケーションタイプをゾーンからリージョンに変更する 対応2 Composerタスクのリトライ間隔を最適化する 対応3 各タスクが使用するGPU数を最適な値に調整する 対応4 同時に実行するタスク数を制御し必要なGPU数を更に下げる 対応5 前日のDAGが完了するまで翌日のDAG実行を遅延する 対応6 前日のDAGが正常に完了していることを確認する まとめ 終わりに GPUを使用する構成 まずDAGのタスクでGPUを使用する際のCloud Composer 2の環境構成について説明します。 Cloud Composer 2の環境は Google Kubernetes Engine (GKE)の Autopilot モードの VPCネイティブ クラスタを利用して構築されます。 Airflowはタスクのスケジュールを行うスケジューラ、管理画面を提供するウェブサーバー、各タスクを実行するワーカーのコンポーネントにより構成されます。 各コンポーネントはCloud Composerが動作するクラスタ内のPodでコンテナとして実行されます。ワークロードの構成でCPU、メモリ、ストレージ、スケーリング台数を指定可能ですが、GPUの指定はできません。このように2023年9月現在Cloud Composer 2は GPUの利用をサポートしていません。 Cloud Composer 2環境の詳細については次の公式ドキュメントを参照してください。 Cloud Composer 環境のアーキテクチャ Cloud Composer 環境を作成する そのためCloud Composer 2でGPUを使ったタスクを実行するには、ワーカーとは別にGPUが利用可能なインスタンスを用意する必要があります。画像検索では別途構築したクラスタにGPUが利用可能なNode poolを用意して、GPUが必要な処理をオフロードしています。 Cloud Composer 2の GKEStartPodOperator を使用することで、Cloud Composer 2からGKEクラスタのPodを起動できます。ワーカーはPodを起動、監視しGPUが必要な処理はワーカーが起動したPodにて行います。 Podを起動する先として、Composerが動作するGKEクラスタとは異なるGKEクラスタも指定可能です。画像検索においても、Cloud Composer2が動作するGKEクラスタとは異なるGKEクラスタを指定しています。 GPUを割り当てたNodeが起動しないことによるタスクの失敗 ここからは画像検索のDAGを運用する中で発生した具体的な問題について述べます。 プロダクション環境のDAGにおいて、物体検出のタスクが失敗し、DAGが正常に終了しない問題が発生しました。物体検出は上述したGPUを使用するタスクの1つです。本節ではこのタスクの失敗について、実際に行った原因調査と暫定対応についてご説明します。 タスクが失敗した原因の調査 まず失敗の原因が環境起因か切り分けるために、プロダクション環境以外でも同様の問題が発生していないか調査しました。画像検索ではプロダクション環境の他に開発環境とステージング環境が存在します。ステージング環境ではプロダクション環境と同じDAGを日次実行しています。ステージング環境を調べると同様の問題がプロダクションと近い時刻に発生していることがわかりました。また並列化した全カテゴリーで同様にタスクが失敗していることがわかりました。 AirflowのWeb UI上から失敗を起こした物体検出のタスクが出力したログを確認すると次のエラーログが確認できました。 {pod_manager.py:310} WARNING - Pod not yet started: detect-object-pants-v3-830zvw9r {pod_manager.py:310} WARNING - Pod not yet started: detect-object-pants-v3-830zvw9r {pod_manager.py:310} WARNING - Pod not yet started: detect-object-pants-v3-830zvw9r {pod.py:716} INFO - Deleting pod: detect-object-pants-v3-830zvw9r {taskinstance.py:1770} ERROR - Task failed with exception Traceback (most recent call last): File "/opt/python3.8/lib/python3.8/site-packages/airflow/providers/cncf/kubernetes/operators/pod.py", line 548, in execute_sync self.await_pod_start(pod=self.pod) File "/opt/python3.8/lib/python3.8/site-packages/airflow/providers/cncf/kubernetes/operators/pod.py", line 510, in await_pod_start self.pod_manager.await_pod_start(pod=pod, startup_timeout=self.startup_timeout_seconds) File "/opt/python3.8/lib/python3.8/site-packages/airflow/providers/cncf/kubernetes/utils/pod_manager.py", line 317, in await_pod_start raise PodLaunchFailedException(msg) airflow.providers.cncf.kubernetes.utils.pod_manager.PodLaunchFailedException: Pod took longer than 1200 seconds to start. Check the pod events in kubernetes to determine why. Pod took longer than 1200 seconds to start. のログからPodが起動時にタイムアウトしたことにより、タスクが失敗となった事がわかります。 PodのタイムアウトについてはCloud Composer 2の公式ドキュメントで KubernetesPodOperator のトラブルシューティングガイド に記載があります。 GKEStartPodOperator のPodが起動するデフォルトタイムアウト時間は120秒ですが、物体検出タスクではモデルのロードに時間がかかるため次のように1200秒を設定しています。 GKEStartPodOperator( startup_timeout_seconds= 1200 , ... ) Podが起動しなかった原因について、再現性があるものなのか確認するためタスクをリトライしました。 通常であればPodが起動するのに十分な時間が経過しても、 {pod_manager.py:310} WARNING - Pod not yet started: のログが出力され続けていました。 物体検出の処理を行うオフロード先のGKEクラスタに対して Kubectl get pods を実行してPodのステータスを確認したところ、次の結果が出力されました。 NAME READY STATUS RESTARTS AGE detect-tops 0/1 Pending 0 60m detect-pants 0/1 Pending 0 60m ... NodeのNodeAffinity、Toleration、コンテナに割り当てるResourceなど Pending の原因になりそうな項目は確認しましたが、問題ありませんでした。 Podのステータスについての詳細はKubernetes公式ドキュメントの Pod Lifecycle をご参照ください。 上記でステータスが Pending となっているPodの1つに対して kubectl describe pod コマンドを実行し、Podの詳細な情報を確認しました。実行中のPodのデバッグについては 実行中のPodのデバッグ をご参照ください。 kubectl describe pods の出力結果のうち Events フィールドに次のEventが確認できました。 Events フィールドにはPodの直近の Event が表示されています。Kubernetesの Event はスケジューラによって行われた決定や、PodがNodeからEvictされた原因など、クラスタ内部で起こっている情報を提供します。 Node scale up in zones asia-east1-a associated with this pod failed: GCE out of resources. Pod is at risk of not being scheduled. Node scale up in zones asia-east1-a associated with this pod failed: GCE out of resources, GCE quota exceeded. Pod is at risk of not being scheduled. 0/2 nodes are available: 2 node(s) didn't match Pod's node affinity/selector. GCE out of resources. との出力から、 Google Compute Engine (GCE)のリソースを超えているとわかります。続く Pod is at risk of not being scheduled. の出力から、Podはスケジュールされていない可能性があるとわかります。 続くログに GCE quota exceeded と出力されています。メッセージからはGCEの Quota (割り当て)を超過していると受け取れます。 さらに 2 node(s) didn't match Pod's node affinity/selector. が出力されています。このため、Podで設定した affinity/selector と一致するNodeのうち割り当て可能なものがなかったとわかります。 GCEでは割り当てによりGoogle Cloudプロジェクト単位で割り当てるリソースの上限が設定されています。割り当ての上限を超えるとGoogle Cloudプロジェクトへそれ以上のリソースが割り当てられなくなります。プロジェクトに割り当てられた割り当ての上限と、現在の使用量はナビゲーションメニューから IAMと管理 を選び 割り当て から確認可能です。 上述のログから割り当ての上限に達したことを想定していましたが、割り当ての状態を確認するとリソースの使用量は上限値まで余裕がある状態でした。またPodがデプロイされる Node pool のマシンタイプ等を変更したわけでもないため、Podはこれまで正常に動いていたスペックと同じスペックのNodeで実行されるはずです。 続いてGoogle CluodのWebコンソールからGKEのプロダクトページを開き、対象のPodが動作するクラスタの詳細を確認すると次の表示が出ていました。 表示の詳細を開くと表示の詳細を開くと次のメッセージを確認できました。 また詳細からログのリンク先に移動すると、 Cloud Logging のLogs Explorerから次のエラーメッセージを確認できました。 これらの調査から今回の問題の原因は、DAGの実装や構築したインフラの設定など開発者側の問題ではない可能性が高いと判断し、 Cloudカスタマーケア にてサポートケースを作成しました。 サポートチームの回答と提案された暫定対応 Google Cloudのサポートチームからの回答により、事象が発生していた時間帯において、オフロード先のGKEクラスタが存在する asia-east1-a ゾーン全体で nvidia-tesla-t4 インスタンスのリソースが一時的に枯渇していたことがわかりました。 対応方法として別の時間帯でタスクをリトライし、正常に動作するか確認することを提案されました。 提案に従ってリトライを繰り返すと、問題の発生から4時間ほど経って一部のカテゴリーで nvidia-tesla-t4 インスタンスを割り当てたNodeが起動しました。その後Podがスケジュールされステータスも Running となりました。 一方で引き続きPodがスケジュールされず Pending のままになっているカテゴリーもありました。このことから、 nvidia-tesla-t4 インスタンスのリソースはまだ完全に充実していないことが推測できます。 そこで同時に使用するGPUのリソース量を少しでも減らすため、先に Running となったタスクが完了してPodが終了するのを待ってから、 Pending となっているタスクをリトライしました。するとPodがスケジュールされ、 Running になりました。 タスクの状態をAirflowのWeb UIで監視し続け、タスクが完了するたびに失敗となっているカテゴリーのタスクをリトライすることで、全カテゴリーの物体検出を完了させることができました。 ところがGPUの枯渇はその後も頻発しました。上記の方法により、GPUが枯渇するたびに人力でタスクの状態を監視してリトライを行うのは運用コストが大きすぎます。また人力での対応では復旧までの時間もかかってしまいます。そこで割り当て可能なGPUが枯渇し物体検出タスクを実行できない問題について、恒久的な対応を実施しました。 次章では実施した恒久対応について説明します。 Google Cloud内部のGPUリソース枯渇への対策 GKEPodOperator はGPUが必要なタスクを実行するときに動的にNodeのリソースを確保します。タスクが終わるとNodeも終了するため確保されたリソースが開放されます。 一方で Compute Engine ゾーンリソースの予約 を利用することでリソースを確保し続けることもできます。 しかしながら日次で実行されるDAGにおいて物体検出は30分から2時間程度で完了します。また、商品カテゴリー単位で物体検出のタスクを並列に実行しているため、同時に多くのGPUを必要とします。画像検索では現在対応カテゴリーが13カテゴリーあり、各カテゴリーごとに4GPUを割り当てており計52GPUを使用しています。 もし、52GPUを一日中確保し続けると、タスクが実行されていないほとんどの時間帯ではGPUが使用されないため、無駄なコストが発生してしまいます。そこでリソースの予約は行わずGPUリソース枯渇問題に対応する必要がありました。 対応1 GKEクラスタのロケーションタイプをゾーンからリージョンに変更する GKEクラスタが動いているCompute Engineのリソースは世界中のロケーションごとにリージョンという単位で配置されています。リージョンにはゾーンというリソースの論理グループが3つから4つ用意されています、リージョン内のそれぞれのゾーンは広帯域のネットワークで接続されています。ゾーンごとに冷却インフラなどをグループ化することで、各ゾーンの障害が異なるゾーンに影響しづらくなるよう設計されています。 GKEクラスタのロケーションタイプには、リージョンタイプとゾーンタイプの2タイプがあります。ロケーションタイプにリージョンを指定することでクラスタは特定のゾーンに制限されなくなります。これによりCompute Engineはリージョン内の複数ゾーン間でリソースを適切に割り当てることができるようになります。 以前のバージョンである Cloud Composer 1 の GKEPodOperator ではロケーションにゾーンしか指定できませんでした。Cloud Composer 2になり、ロケーションにリージョンも指定できるようになりました。画像検索は構築時に Cloud Composer 1 を使用していたため、これまでオフロード先にゾーンタイプのGKEクラスタを使用していました。画像検索は昨年Cloud Composer 2へのバージョンアップを行ったことで、リージョンタイプのGKEクラスタも使用可能となっています。そこで、オフロード先のGKEクラスタをリージョンタイプに切り替えます。 GKEクラスタのロケーションは一度構築すると変更できません。そのためリージョンタイプのロケーションを持つGKEクラスタを用意するにはGKEクラスタの再作成が必要です。 GKEクラスタを作成時にリージョンを指定することで、ロケーションタイプがリージョンのGKEクラスタを作成できます。 GPUに関しては全ゾーンにすべてのGPUプラットフォームのリソースが存在するわけではなく 利用できるゾーンが限定されている ため注意が必要です。今回は NVIDIA Tesla T4 を使用するため デフォルトのノードのロケーション に asia-east1-a と asia-east1-c を指定します。 これによりオフロード先のGKEクラスタをリージョンタイプで再作成できました。 Cloud Composer 2からリージョンタイプのGKEクラスタへオフロードするには GKEStartPodOperator の location パラメータへゾーン名に変わってリージョン名を指定します。 GKEStartPodOperator( location= "asia-east1" , ... ) この対応により複数ゾーンのGPUを利用できるようになり、 asia-east1-a のGPUインスタンスリソースが枯渇しているときでも asia-east1-c のリソースを使用可能になりました。 対応2 Composerタスクのリトライ間隔を最適化する Airflowはタスクが失敗すると、一定期間をおいて自動でタスクをリトライさせます。今回の物体検出タスクが失敗した際にも、Airflowは自動的にタスクをリトライしていました。 しかしGPUの枯渇が長時間に及んだことで、リトライ回数の上限を超えてしまいタスクが失敗していました。失敗とマークされたタスクは、それ以降は自動的にリトライされなくなります。その場合手作業でタスクをリトライしなければなりません。 GPUの枯渇と回復は前触れなく発生するため、手作業での対応は手間と時間がかかり望ましくありません。タスクが失敗していることに気が付かなければリトライされない危険もあります。GPUの枯渇は週末に発生することが多く、タスクの失敗に気が付きにくいため問題はより深刻でした。 そこで、リソースが回復されるまでより幅広い時間に渡ってリトライするよう設定の見直しを行いました。それにはリトライ回数を増やすか、リトライ間隔を広げる方法が有効です。ただしこれらの対応にはそれぞれ弊害もあります。 リトライ回数を増やすと問題発生時、余計なコストを発生させる場合があります。一般的にタスクの失敗はリトライのみで解決するとは限りません。今回の原因以外にも入力データの不備により失敗することもあります。この場合データを整備して原因を解消してからリトライしなければ、タスクは失敗とリトライを繰り返してしまいGPUリソースを長時間使用し続けることになります。 リトライ間隔を広げるとタスクが失敗した時の復旧に時間がかかるようになります。一時的な問題が原因でタスクが失敗した場合など、直後にリトライすることで解決する問題も多くあります。長すぎるリトライ間隔はこのような場合に復旧が遅れる原因となってしまいます。 そのためまずは短い間隔でリトライし、リトライ時にもタスクが失敗した場合はリトライ間隔を広げていく Exponential backoff を行います。 タスクのリトライで Exponential backoff を有効にするには、Airflowの retry_exponential_backoff を設定します。 retry_exponential_backoff に True を設定すると、リトライ回数に応じてリトライ間隔が指数関数的に増加します。例えばリトライ間隔に60秒を設定した場合、最初のリトライは60秒、2回目は120秒、3回目は240秒となります。これにより、直後にリトライすれば解決する偶発的な問題では最初に短い間隔でリトライが行われます。最初のリトライで解決しなかった場合はリトライ間隔が広がることで、リトライ回数を増やすことなく、長期間に渡ってリトライが行われます。 retry_exponential_backoff の指定はDAGを定義するスクリプトに記述します。 retry_delay を60秒とし、 retry_exponential_backoff を True に設定するには次のように記述します。 dag = models.DAG( "YOUR_DAG_NAME" , default_args={ "retry_delay" : 60 , "retry_exponential_backoff" : True , ... ) 注意すべき点としてリトライ間隔が指数関数的に増加するため、リトライ回数が増えると間隔が必要以上に広がりすぎてしまうことがあります。例えばリトライ間隔を60秒に設定している場合、10回目のリトライではリトライ間隔が8時間以上まで広がってしまいます。これではリソースが空いてもすぐにリトライが行われず、再度割り当て可能なリソースがなくなってしまう恐れもあります。 このような事態を避けるために、 retry_exponential_backoff に合わせて max_retry_delay を設定します。これによりリトライ間隔に上限を設定できます。 dag = models.DAG( "YOUR_DAG_NAME" , default_args={ "retry_delay" : datetime.timedelta(minutes= 1 ), "max_retry_delay" : datetime.timedelta(minutes= 30 ), "retry_exponential_backoff" : True , ... ) 今回は max_retry_delay に30分を設定したことで6回目のリトライ以降はリトライ間隔が広がらず30分ごとにリトライされます。 これによりリトライ回数を大きく増やすことなく、リトライを幅広い時間帯で行えるようになり、GPUのリソースが枯渇した場合にもリトライで復旧できる可能性が高まります。 対応3 各タスクが使用するGPU数を最適な値に調整する タスク自体を見直し、同時に必要なGPU数を減らすことでGPUの枯渇が発生した場合の影響を低減します。 前述のとおり画像検索では13カテゴリーそれぞれで4GPUを使用し物体検出のタスクを行っており、一度に52GPUをリクエストしていました。このように大量のGPUを使用していたのは、インスタンスの使用コストがリソースのGPU数だけでなく使用時間にも比例するためです。1GPUで52時間かけて処理を行うのも、52GPUで1時間かけて処理を行うのも、どちらも利用するGPUのリソース量は52時間となります。一方で処理時間は52時間から1時間に短縮できます。このためリソースが充分に使用できる場合においては一度に大量のGPUを使用するのは妥当な方法と言えます。 しかしNodeに割り当て可能なGPUが枯渇した状態では、大量のGPUをリクエストするとタスク自体が動作しません。そのため上記の方法を見直し、同時に使用するGPU数を削減しました。 単純にGPUの数を減らすと、処理時間が犠牲になってしまいます。処理時間を犠牲にしないためカテゴリーごとの商品数に着目しました。ZOZOTOWNで取り扱う商品数はカテゴリーごとに差があり、必要な処理時間も異なります。商品数が少ないカテゴリーは物体検出が早く終わり、商品数が多いカテゴリーの物体検出が終わるまで待ち状態となっていました。 そこで商品数が少ないカテゴリーの物体検出で使用するGPU数を減らします。 直近の商品数を調べるとトップスカテゴリーの商品数が1番多く、続くパンツはその半分より少なく、それ以外の各カテゴリーはパンツのさらに半分以下でした。 そのためトップスにはこれまで通り4GPUを割り当て、パンツはその半分の2GPU、それ以外のカテゴリーは1GPUを割り当てることにします。カテゴリーによってはこれまでの4倍の時間がかかりますが、元々商品数が多いカテゴリーの物体検出が終わるまでは待ち状態となっていたため、トータルの処理時間は変わりません。 これにより全体の処理時間に影響なく、同時に割り当てるリソースを最大52GPUから17GPUにまで減少させることができました。 対応4 同時に実行するタスク数を制御し必要なGPU数を更に下げる GPU枯渇後の復旧時には全カテゴリーのタスクを同時に実行できず、一部カテゴリーのPodは Pending となっていました。このため先に実行されているタスクの終了を待機し、終了したらPodが Pending となっているタスクをリトライする必要がありました。この方法はGPU枯渇の対策に効果的でしたが、人力の作業であったため運用面での問題となっていました。 この問題を解決するため全カテゴリーのGPU使用量に応じて、自動的にタスクを実行する仕組みを構築します。対策3により使用するGPU数はカテゴリーごとで異なっているため、それも考慮して同時に実行するタスクを制限します。 タスクの並列数を自動的に監視し、制御するために、Airflowの Pool を利用します。PoolはAirflowのスケジューラがタスクの実行を管理するために使用する仮想的なリソースです。 Airflow全体で使用できるPoolとPoolが持つSlot数を設定し、タスクには使用するPoolと消費するSlot数を指定します。タスクが開始されると、指定されたPoolのSlotが消費されます。タスクが終了するとPoolのSlotが復活します。 スケジューラはタスクの消費するSlotがPoolに残っていない場合にはタスクの開始を待機します。その場合でもタスクは失敗となりません、そのためリトライ間隔が広がったり、リトライ回数を超えることはありません。他のタスクが終了し待機していたタスクの消費するSlotがPoolに確保されるとタスクは開始されます。 これにより17GPU未満しか割り当たらない場合でも、他タスクのGPU使用量を考慮してタスクをスケジュールできます。 今回は gpu_pool というPoolを設定し、同時に割り当て可能なGPU数をPoolのSlot数として設定します。 AirflowのWeb UIからメニューのAdminを選びプルダウンのPoolsを選ぶと、Poolsの設定画面が表示されます。 Composer 2の標準ではdefault_poolというPoolが設定されており、Slotsに100000が設定されています。+ボタンをクリックすると新しいPoolを追加できます。 PoolにPool名、Slotsに使用可能なSlot数を指定します。今回はgpu_poolというPoolを追加し、割り当て可能なGPU数をSlot数として指定します。 次に GKEStartPodOperator のパラメータ pool に使用するPool名を指定し、 pool_slots に使用するGPU数(トップスは4、パンツは2、それ以外は1)を指定します。 GKEStartPodOperator( ... pool= "gpu_pool" , pool_slots= 1 ) カテゴリーごとに使用するGPUの数に合わせて消費するSlot数を指定することで、タスクで使用するGPU数に応じて同時実行数が調整されます。 例えばSlotに4が指定されているときは、トップスの物体検出中は他カテゴリの処理は開始されません。トップスの処理が終わると、パンツとその他カテゴリが2カテゴリー同時に実行されます。 これにより人力による監視を必要とせず、使用可能なGPUを有効に使えるようタスクを順番にスケジュールし、全タスクを完了できるようになりました。 対応5 前日のDAGが完了するまで翌日のDAG実行を遅延する GPUがNodeへ割り当てられるのに時間がかかるようになったことで、DAGの総実行時間が1日を超える日が出てきました。 画像検索では前日のDAGが作成した結果のデータを利用して、翌日のDAGでは新たに追加された商品のみを差分で計算することで、処理量を減らす差分実行の仕組みを導入しています。そのため前日のDAGがデータを作成する前に翌日のDAGが実行されると、翌日のDAGは前日のDAGが作成したデータを利用できなくなってしまいます。この問題を防ぐために、前日のDAGが完走するまで、翌日のDAGが実行されないようにします。 これを実現するためにAirflowの max_active_runs_per_dag を使用します。 max_active_runs_per_dag は同一のDAGが同時に実行できる数を制限します。これに1を指定すると、同一のDAGが複数起動しなくなるため、前日のDAGが完了するまで翌日のDAGは開始されなくなります。 max_active_runs_per_dag を設定するにはコンソールのナビゲーションメニューから Composer を開き、名前を選択して Airflow構成のオーバーライド タブで編集ボタンを押下します。 AIRFLOW構成のオーバーライドを追加 を押下し、セクションで Core 、キーには max_active_runs_per_dag を選んで値を 1 に設定します。 これにより前日のDAGが完了する前に、翌日のDAGが実行されてしまう問題を防止できるようになりました。 対応6 前日のDAGが正常に完了していることを確認する max_active_runs_per_dag は同時に実行するDAGの数を制限するだけで、DAGの成否について考慮しないことに注意が必要です。もし前日のタスクが失敗し、DAGが Failed で終了しても、他に実行しているDAGがなければ翌日のDAGは実行されてしまいます。 これでは前日のデータが存在しない状態で翌日のDAGが動いてしまう問題を完全には防止できません。そこでDAGの冒頭で前日のDAGが Success で完了していることを確認するタスクを追加します。 DagRun.find() を使用することで他のDAGの状態を取得できます。自身のDAG_IDと一致するDAGの状態を取得し、実行日の降順でソート、今回の実行日より前に開始された最初のDAGを調べることで、前回実行されたDAGのステータスを取得できます。前回実行されたDAGのステータスが Success でない場合にはタスクを失敗としDAGの処理がそれ以上実行されないようにします。 def check_yesterday_state (**context): ds = datetime.datetime.strptime((context[ "ds" ]), "%Y-%m-%d" ).astimezone(datetime.timezone.utc) dagruns = DagRun.find(dag_id=<DAG_ID>) dagruns.sort(key= lambda x: x.execution_date, reverse= True ) for dagrun in dagruns: if dagrun.execution_date < ds: if dagrun.state == State.SUCCESS: return else : raise Exception ( "Previously ( Usually yesterday ) started DAG has not completed yet." ) これにより前日のDAGが失敗したときに翌日のDAGが実行されるのを防ぐことができました。 まとめ 本記事ではCloud Composer 2でGPUを使うタスクで発生したGPU枯渇問題とその対策について紹介しました。 リージョンタイプのGKEクラスタへの移行と、各タスクが使用するGPU数を最適な値に調整したことにより、GPU枯渇を大きく減らすことができました。 本番環境において発生したGPU枯渇は、対策前の30日間で24回発生していたのに対し、対策後の直近30日では6回に抑えられています。またその6回いずれの場合においても自動的にリトライが行われ手作業を必要とせずDAGを正常終了できています。 ステージング環境でDAGが正しく動作しなかった際には、翌日のDAGが実行前に停止しており、差分データを取得できない問題も未然に防ぐことができました。 今後も、さらなる改善により低コストで安定したシステムを構築していきたいと考えています。 終わりに 最後までお読みいただきありがとうございました。 ZOZOでは一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、次のリンクからぜひご応募ください。 hrmos.co corp.zozo.com
はじめに こんにちは! ZOZOTOWN開発本部のAndroid開発チームです。 2023年9月14日から16日にかけて「 DroidKaigi 2023 」が開催されましたね! 今年ZOZOはPLATINUM SPONSORSとして協賛し、オフライン会場にてスポンサーブースの出展をしました。 technote.zozo.com 今年は昨年を上回る多彩な企画や取り組みを展開しました。 クイズやアンケートなど、皆様にご参加いただく企画を実施しました。これらの集計結果や、ブース準備の様子など、ここでしか読めない裏話をご用意しました! ぜひ、最後までお楽しみに! 目次 はじめに 目次 公式アプリへのコントリビューション ZOZOエンジニアが1名登壇しました スポンサーブース運営までの裏側:DroidKaigi 2023の取り組み ブースコンセプトの策定 ブースを支える体制の紹介 情報共有(コミュニケーション) マインドマップ トークスクリプト ブース当番のオペレーションシート ノベルティの選定 ブース運営準備の振り返り アンケート結果 Androidでおすすめの開発便利ツールはありますか? 服やコスメの購入時に重要視するポイントは? DroidKaigiのブースで欲しいノベルティはどれですか? クイズの結果 来場者からの反応 今年も開催しました! After DroidKaigi セミナー・カンファレンス参加支援制度 まとめ 公式アプリへのコントリビューション 今年はAndroid版に6名、iOS版に1名と公式アプリへのコントリビューションを積極的に行いました。こちらに関しては 別記事 にてご紹介しておりますので、合わせてご覧ください! ZOZOエンジニアが1名登壇しました DevRelブロックの @wiroha が、「 よく見るあのUIをJetpack Composeで実装する方法〇選 」というタイトルで登壇しました! 「よく見るあのUIをJetpack Composeで実装するとどのくらいの時間がかかるのだろうか?」というお悩みを解決すべく、よく見るUIの実装方法についてアニメーション動画やコードを交えて詳しく解説しています。 docs.google.com このセッションでは初心者から経験者まで、幅広い層に対してわかりやすく知見が解説されました。資料もアップロードされているので、Jetpack ComposeでのUI実装を予定している方は是非、ご覧ください! スポンサーブース運営までの裏側:DroidKaigi 2023の取り組み 協賛ブース運営の全体進行を務めました高田です。ここからは協賛ブース運営を支えた体制や、得られた経験、そしてリードする立場での全体進行についてお伝えします。 ブースコンセプトの策定 まずはブースのコンセプトを策定しました。来場者の皆様に対して「何を届けたいのか?」というブース出展の根幹について今一度考えて整えることで、提供する価値の最大化を図りました。 本年度は、下記の3つを届けたい! という思いを、ブースのコンセプトとして策定しました。 ZOZOのサービス 社内制度 コスメ販売への取り組み ブースを支える体制の紹介 毎年、ZOZOではAndroidエンジニアが主だってブース準備を進めています。今年は3名で臨み、分業による効率化を図るために役割を分けました。その役割は下記のとおりです。 旗振り担当:全体の進行や方針決定、連携を主に担当 ブース担当:ブースの企画や、運営方法、展示物の検討を主に担当 ノベルティ担当:ノベルティ見積もりや発注を主に担当 また、Androidエンジニアだけでなく、様々な部署に協力を仰いで臨みました。今回一緒に協力してくれた部署は下記のとおりです。 技術戦略部、DevRel:DroidKaigi運営との連携、PR活動、イベント出展に関する物事全般(資材搬入、法務確認など)の調整を担当。 デザインチーム:ブースでのビジュアルコンテンツの制作、ノベルティのデザインを担当。 このようにたくさんの人達によって、今年のブース運営は支えられておりました! 情報共有(コミュニケーション) 弊社のエンジニアは基本的にリモートワークです。当日のオペレーションはどのように回すのか? ノベルティやコンテンツの制作はどこまで進んだか? など、共有すべき事項はたくさんあります。リアルなアウトプットが絡むイベントですから、いつものアプリ開発と同じコミュニケーションだけでは中々思うように進みません。 イベントの成功のためには、あらゆるコミュニケーションに工夫が必要でした。その一部をご紹介します。 マインドマップ イベントに対する認識は人によって異なります。ZOZOのブースを運営するためにはまず、やらなければいけないことを網羅する必要がありました。今回はその手段としてMiroを用いてマインドマップを作成しました。 ※かなり大きい図になってしまったため、一部のみのご紹介といたします。 複数人という体制でマインドマップを用いることで、短期間で網羅性の高い情報を書き出すことができました。そのおかげで、計画し損なった作業による手戻りは一切発生しませんでした。 もちろん一人でも有効な手段ではあるのですが、「最初のうちに複数人でやる」ということに大きなアドバンテージを感じました。 トークスクリプト イベント当日のブースは準備メンバー3名に加えて、当日参加するAndroidチームメンバー約10名が当番制で運営にあたります。例年、ブースに訪れた方々は弊社のサービスや開発に対する質問をしてくださいますが、全員が同じクオリティの回答をできておりませんでした。せっかく来ていただいたのに、詳しいお話をご提供できないという事態を問題として捉えました。そこで、ブース当日の会話を想定したZOZOのプロダクト、ノベルティやブース内容に関しての回答をまとめたトークスクリプトを用意しました。 このドキュメントを全メンバーが携えつつブース当番にあたることで、ご提供するお話のクオリティを一定以上の水準に保つことができました。 ブース当番のオペレーションシート 今年は例年と比較して多くの取り組みを企画したので、準備メンバー以外のAndroidチームメンバーが混乱することを予見していました。皆さんにお渡しするノベルティはいつも5種類くらいですが、今年は10種類。加えてそれぞれ異なるコンテンツでお渡しするというルール。コンテンツもそれぞれ濃い内容で3種類あります。 そこでトークスクリプトと同様の考え方で、ブース当番のやることが一目で分かるオペレーションシートをMiroで用意しました。 イベント当日、最初は混乱するメンバーもいましたが、こちらのオペレーションシートを使うことでスムーズにブース対応ができていました。予見していた事態を無事避けることができてよかったです! ノベルティの選定 今回は様々な趣向を凝らしたノベルティをご用意させていただきました。例年お渡ししているペンや付箋、ZOZOMAT、ZOZOGLASSに加えて以下のラインナップを追加しました。 一期一会という四字熟語をもじった「一合一会」をコンセプトにしたお米一合 ZOZOのコスメへの取り組みを象徴したハンドミラー ZOZOが本社を構える西千葉のコーヒー店「Eureka Coffee Roasters」とコラボしたブレンドコーヒー LINEスタンプとしても配信中の「箱猫マックスVol.6(エンジニア編)」をそのまま使ったステッカー ZOZOTOWNのロゴがプリントされたショッパー 来年もお楽しみに! ブース運営準備の振り返り DevRelやデザイン部、技術戦略部など、多岐にわたる部署のご協力のもと無事にブース運営を終えることができました。事前のリハーサルやプランの練り直しも含め、実際の協賛ブース運営のノウハウを学ぶことができました。また、外部への発信だけでなく社内の一体感を深める良い機会となりました。今回得られた経験・知識を今後に活かして参ります。 アンケート結果 ZOZOTOWNアプリ部Android1ブロックの池田です。ZOZOのスポンサーブースでは、時間別に3つのアンケートを実施しました。会場のみなさんに回答していただいたアンケート内容を見ていきたいと思います。 Androidでおすすめの開発便利ツールはありますか? Androidアプリを開発していく上でどのような便利ツールをみなさんが利用しているのかアンケートしました。やはり先日、日本でも利用可能となったStudio BotやChat GPT、GitHub CopilotなどのAI関連のツールに注目が集まっていますね。他には、HyperionやKotlin Fill ClassといったAndroid開発ならではのツールを回答してくれた方もいらっしゃいました。初めて聞いたツールもあったので非常に参考になりました。 アンケート「Androidでおすすめの開発便利ツールはありますか?」の回答結果 服やコスメの購入時に重要視するポイントは? 普段、服やコスメを買うときに何を重要視しているかエンジニアや人事の方、学生の方など多くの方に回答していただきました。「素材」や「価格」を重要視している方が多いという結果になりました。普段、みなさんが何を重要視していて、何に困っているのかといった洋服やコスメに関するお話をたくさんできました。 アンケート「服やコスメの購入時に重要視するポイントは?」の回答結果 DroidKaigiのブースで欲しいノベルティはどれですか? エコバッグやミニタオルが欲しいという意見が多かったです。企業ブースでのノベルティやDroidKaigiのオリジナルグッズがたくさんもらえることもあり、それらを持ち運べるエコバッグが欲しいという声が多かったです。今後のZOZOのノベルティ制作の参考にさせていただきますので楽しみにお待ちください! アンケート「DroidKaigiのブースで欲しいノベルティはどれですか?」の回答結果 クイズの結果 ZOZOのスポンサーブースでは、ZOZOについてより知ってもらおうというコンセプトで5問のクイズを実施しました。クイズは2日間で約200人の方に参加していただき、その中で全問正解した方が20人いらっしゃいました! 参加していただきありがとうございました! ここでは、正答率の一番高かったクイズと低かったクイズを見ていきたいと思います。 正答率の一番高かったクイズは「コーディングチャレンジ」の問題で、71.7%でした! ZOZOでは社内LT大会が行われており、そのときに題材となったコードをクイズとして出題しました。こちらのクイズではブースの前に設置したボードからコードを読み込み、議論をしながら何が描画されるか推察していただく方が多かった印象です。このコードでは drawB が花火の1つ1つの花びらを表しており、それが円状に配置されていることから正解は3番の花火になります。コードチャレンジの問題の正答率が一番高い結果となり、参加していただいた方々の技術力の高さを感じることができました。 一方で一番正答率の低かったクイズは「ネーミングセンスを鍛える会」の問題で、43.4%となりました! このクイズはZOZOTOWN Androidチームで行なっている取り組みをブースに来た方にも体験していただきたいといった目的で出題しました。このクイズのポイントは、お題の最後に書かれている「入力リストが空の場合、エラーメッセージを出力します」といった条件があり、戻り値はResult型を返す必要がありました。そのためこのクイズは2番が正解となります。 ただし、この問題は必ずしも一意に定まるものではないので、正解が1つにならなければいけないクイズとしては考え直す必要がありそうでした。取り組み自体は好評でしたので、今後はクイズではなく来場した皆様と議論できるようなコンテンツとして、再検討する予定です。 来場者からの反応 DroidKaigi 2023のZOZOブースに多くの来場者が訪れ、特に以下の点が好評でした。 Jetpack Composeのクイズ:Androidエンジニアの方々から、業務に関連する質問が盛り上がったり、社内LTトークやネーミングセンスを鍛える会の紹介を興味深く受け取ってもらえました。 ノベルティの選定:「一期一会」のお米や、ZOZOCOSMEを意識した手鏡、千葉のドリップコーヒーなどのノベルティが参加者から大変好評でした。特にお米への感想や、コンセプトに共感してくださる方が多かったです。 ARメイク体験:現地でのARメイク体験は、特に参加者からの反響が大きく、ZOZOの最先端の技術を間近で体験してもらえた点が好評でした。 これらのフィードバックをもとに、次回のイベントやブースの運営に活かしていきたいと考えています。 今年も開催しました! After DroidKaigi 昨年に引き続き今年もLINE、ヤフー、ZOZOの3社合同でAfter DroidKaigiをオンラインで開催しました! 詳細につきましては 別記事 にて紹介しておりますので、こちらもチェックをお願いいたします! zozotech-inc.connpass.com セミナー・カンファレンス参加支援制度 ZOZOにはエンジニアのイベント参加をサポートする制度があります。 ZOZOはPLATINUM SPONSORとして協賛しているのでスポンサーチケットがありますが、チケット費用をはじめとし、以下の補助が受けられます。 希望するエンジニアは業務として参加 チケット費用、交通費は経費精算、遠方の場合は出張扱い可 (イベントが休日開催の場合)休日出勤扱いで参加 これらの制度があることで、イベントに参加しやすい環境が整っています。我々開発メンバーとしても、今回のイベント参加ハードルは非常に低かったです。 まとめ オフラインで参加された方も、オンラインで参加された方も、お疲れさまでした! 今年も楽しかったですね! また、DroidKaigi 2023を運営してくださったスタッフの皆様のおかげで、我々も安心してイベントに参加できました。細かなフォローや丁寧な説明でサポートしていただきありがとうございました。これからもAndroidアプリ開発を盛り上げてまいりましょう! 最後に、ZOZOではAndroidエンジニアをはじめ、一緒に楽しく働く仲間を募集中です。ご興味のある方は下記採用ページをご覧ください! hrmos.co
はじめに こんにちは、生産プラットフォーム開発本部の stakme です。 本稿では、スプレッドシートの作業に「手続き的なアプローチ」と「宣言的なアプローチ」という観点を持ち込み、ふたつを対比しながら紹介します。Google Sheetsの多彩な関数を駆使して、日常的な問題に効率的に対応するための具体的なテクニックやヒントを提供します。また注意点やリスクを指摘し、スプレッドシートをより強力に活用するための知識を提供します。 目次 はじめに 目次 背景・課題 本稿の目的 規則的な処理を繰り返すケース 手続き的に構築された例 宣言的に記述された例 SEQUENCE ARRAYFORMULA 関数の組み合わせ なぜ「宣言的」なのか データが徐々に増えるケース 手続き的に構築された例 宣言的に記述された例 別の見せ方でデータを表示したいケース 手続き的に構築された例 宣言的に記述された例 やりすぎのケース 手続き的に構築された例 宣言的に記述された例 まとめ 背景・課題 筆者は生産プラットフォーム開発本部に所属するソフトウェアエンジニアです。弊本部は「 Made by ZOZO 」を支えるシステム開発に従事しています。筆者の役割は、BigQueryを中心とする事業データ利活用の基盤を整えることを通じて、目標達成に向かうビジネス上の意思決定を支援することです。 直近の案件において、「人間による入力」に対する「システムによる評価フィードバック」をすばやく得ることで意思決定を効率化したいというケースがありました。入力インタフェース(手での入力やコピーペースト)の強力さ、フィードバック反映のスピードなどから判断すると、Google Sheetsが最適と考えられるケースでした。 Google Sheetsは非常に強力なスプレッドシート製品であり、データの入力、蓄積、加工、表示という一連の流れを単体で処理できます。ただし今回のように不定件数の入力データを加工するときには、若干の「慣れ」が必要となります。素朴に関数をコピーペーストして実装すると、データ増加に伴って関数がコピーされていないセルが生まれ、処理が壊れてしまうのです。 振り返ってみると、ここで筆者が苦しんだ原因は「何を実現したいのか」ではなく「どのセルでどのような演算をするか」という抽象度の低い内容をそのまま記述してしまった点にあると思います。プログラミングでいうと、for文を使わずに何千回もメモリを直接操作しているような状態でしょうか。そう喩えてみると、いかにも壊れやすそうですね。プログラミングでは避けられることがスプレッドシートでは上手く回避できない、という部分にこの「慣れ」の要素があると思います。 本稿の目的 本稿は、そのようなスプレッドシートにおける「慣れ」の実体を叙述するために、プログラミングの領域からふたつの概念を借用します。 抽象度が低く、人間の管理に依存する、手続き的に構築されたスプレッドシート 抽象度が高く、データ構造に依存する、宣言的に記述されたスプレッドシート 両アプローチを比較しながら、具体的なGoogle Sheetsの利用テクニックを紹介します。この紹介を通じて、読者はGoogle Sheetsにおける実装の特徴について言語化する術を獲得し、場面によって適切なアプローチを意識的に採用できるようになるでしょう。 この目的を達成するため、手元で試して学べるサンプルを多く含めるようにしました。実際にGoogle Sheetsの動作を試しつつ、読み進めることをおすすめします! 規則的な処理を繰り返すケース 例えば以下のように、「A1の日付( 2023/01/01 )を基準として、その0日後、7日後、14日後… という日付を6回だけ生成して表示したい」とします。 手続き的に構築された例 このシートのB列は、下記のような関数で表現できるでしょう。 =A1 =B1+7 =B2+7 =B3+7 =B4+7 =B5+7 7日ずつ日付を進めたいので、 +7 を繰り返し記述しています。これを6回だけ行いたいので、6個のセルで関数を実行しています。とてもシンプルですが、誤って途中のセルを削除すると次の図のようになります。 削除したB3セルが空欄になるだけでなく、それ以降の日付は1900年1月になってしまいました 1 。あきらかに意図と異なる値ですが、日付データそのものは生成されてしまっており、問題に気づきにくい状態です。 宣言的に記述された例 最初に具体例を示します。B1セルにこのように入力してください。 =ARRAYFORMULA(A1 + SEQUENCE(6, 1, 0, 7)) このときB2からB6セルは空欄としてください。そうすると、B列に先ほどと同じ日付が表示されます。 この例に含まれている関数について、ひとつずつ説明します。 SEQUENCE まずは SEQUENCE(6, 1, 0, 7) という部分です。 SEQUENCE 関数 は、連続する数値を生成します。 SEQUENCE(6, 1, 0, 7) は、縦に1列で 0, 7, 14, 21, 28, 35 という6つの数値を生成します(下図のA列)。 設定を変えると、ゼロではない数値から始めたり、複数の列にわたる数値も生成できます。たとえば SEQUENCE(6, 3, 700, 7) を実行すると、下図のC列からE列のようになります。実際にさまざまな設定を試してみると、仕組みがよく分かると思います。 ARRAYFORMULA 次に ARRAYFORMULA です。 ARRAYFORMULA 関数 を理解するために、まずは下記の3つの例を確認してください。 ={10; 20; 30} =100 + {10; 20; 30} =ARRAYFORMULA(100 + {10; 20; 30}) {10; 20; 30} という記述は「縦に並んだ10、20、30という数値の組み合わせ」を表しています(1つのセルにこれを記述すると、そこから下に3つの数値が表示されます)。このような構造を配列(Array)といいます。配列と数値は異なるものであり、 100 + {10; 20; 30} というような足し算はできません。配列という特別な構造を踏まえつつ、その中身をひとつずつ処理するには ARRAYFORMULA 関数を利用します。 こちらも言葉で説明すると複雑ですが、実際の動作を比べると ARRAYFORMULA 関数の役割が分かると思います。 関数の組み合わせ このふたつを組み合わせた =ARRAYFORMULA(A1 + SEQUENCE(6, 1, 0, 7)) は、結局こんな処理を意味しています。 ①最初の状態 ARRAYFORMULA(A1 + SEQUENCE(6, 1, 0, 7)) ②SEQUENCE関数により、縦に並んだ6つの数値を生成 =ARRAYFORMULA(A1 + {0; 7; 14; 21; 28; 35}) ③ARRAYFORMULA関数により、縦並びの構造を保ちながらA1との足し算を実行 ={A1+0; A1+7; A1+14; A1+21; A1+28; A1+35} ④結果を表示 ちなみに、 MAP 関数 という別の関数を利用して =MAP(SEQUENCE(6, 1, 0, 7), LAMBDA(i, A1 + i)) などと書くこともできます(結果の見た目は同じです)。 なぜ「宣言的」なのか もともとこのケースは、「A1の日付を基準として、その0日後、7日後、14日後… という日付を6回だけ生成して表示したい」という明確な意図があると想定していました。手作業で =B2 + 7 と書いたり消したりしたのは、この意図をすばやく実現するための手続きを進めていたにすぎません。人間のミスは、このような手続きにおいて発生するものです。 だとすると、人間が手続きをやめれば、手続きのミスは問題しなくなるはずです。 人間は「こういう処理をしたい」と宣言だけして、あとの処理はスプレッドシートに任せてしまう。そうすれば、手続きでミスを起こして壊れる余地はなくなります。また、スプレッドシート作成者の意図がほかの人々にも理解しやすくなるでしょう。そこで本稿はこうしたアプローチを「宣言的な記述」と呼び、人間がデータ処理の流れをひとつずつ実装する「手続き的な構築」と対比することにしました。 データが徐々に増えるケース データが増えるたび、なんらかの処理を行いたいケースを考えます。 例えば、以下のように「処理済みのID」と「チェック対象のID」それぞれのリストがあります。あるIDが処理済みリストに含まれるかを VLOOKUP 関数 で調べたいとします。「チェック対象のID」はどんどん増えると想定します。 手続き的に構築された例 データが増えるたびにVLOOKUPをコピー&ペーストすることで対応できます。 =VLOOKUP(C2, $A$2:$A$4, 1, false) =VLOOKUP(C3, $A$2:$A$4, 1, false) =VLOOKUP(C4, $A$2:$A$4, 1, false) ... 宣言的に記述された例 手続き的なアプローチは、対象のIDが増えるたびに手作業が必要です。ただし、毎回まったく新しい作業をするわけでもありません。 C2, C3, C4, ... という部分だけ入れ替え、同じことを繰り返しています。 ということは、 {C2; C3; C4; ...} という無限に続く配列があれば、 ARRAYFORMULA 関数を利用して一気に処理できるはずです。Google Sheetsでは、このような配列を C2:C と表現できます。 C2:C は「C2からC列の最後まで」という意味です。ここでは、以下のように記述できます。 =ARRAYFORMULA(VLOOKUP(C2:C, $A$2:$A$4, 1, false)) これを実行すると、下図E列のようになります。 今はまだチェック対象のIDが存在しない行も含めて、C2より下にあるすべてのIDをチェックできています。 より読みやすい表示を実現するのであれば、 IF 関数 、 ISNA 関数 、 NOT 関数 を利用して改良できます。 =ARRAYFORMULA(IF(C2:C, NOT(ISNA(VLOOKUP(C2:C, $A$2:$A$4, 1, false))), "")) 別の見せ方でデータを表示したいケース すでにあるデータを、別の見せ方で表示したいことがあります。例えば、作成済みのテーブルの列を入れ替え、名前も変えて表示したいとします。 手続き的に構築された例 値をコピーして、手で入れ替え、「会員番号」「名前」と入力するだけです。今後のデータ更新を考慮しない場合であれば、この方法で十分です。 宣言的に記述された例 常に最新のデータを異なる見せ方で表示したい場合は、単純なコピーでは対応できません。 QUERY 関数 を利用することで「同じデータを、違う順番で、異なるラベルをつけて表示する」という処理を宣言的に記述します。 =QUERY(A1:B, "select B, A label A '名前', B '会員番号'") これをD1セル(D2ではないことに注意してください)に記述し、E1セルを空欄にすると、下図のようになります。 QUERY 関数を利用すれば、必要な列だけを再利用したり、特定条件のある行だけに絞り込んだり、データを加工したうえで表示したりできます。ただし、絞り込みや加工の自由度はあまり高くありません。 FILTER 関数 や SORT 関数 などの関数と使い分けるとよいでしょう。場合によっては、現在のデータをそのまま表示する ={A1:A} のような書き方が有効というケースもあるかもしれません。 なお QUERY 関数の生成する結果はそのまま ARRAYFORMULA 関数に渡すことができます。この組み合わせに慣れると、さまざまな処理を宣言的に記述できるようになります。 =ARRAYFORMULA(QUERY(A1:B, "select B where A = 'Alice'", 0) + 100) やりすぎのケース すでに明らかかもしれませんが、単純なコピー&ペーストで済む場面であれば、あえて複雑に実装する必要はありません。実装時のコストと必要性を天秤にかけて、適切な方法を選択することが重要です。その点に注意を促すため、実装・読解コストが高いと思われる例を紹介します。 数値の合計値を行ごとに求めて、右側のF列に表示したい場合を想定してください。 手続き的に構築された例 スプレッドシートに慣れた人であれば、迷うことはないと思います。 =sum(A1:E1) =sum(A2:E2) =sum(A3:E3) 意図が明瞭であり、非常にシンプルです。 宣言的に記述された例 宣言的に記述するなら、 このような書き方ができるでしょう。 ①SUMIFパターン =ARRAYFORMULA(SUMIF(IF(COLUMN(A1:E1), ROW(A1:A1000)), ROW(A1:A1000), A1:E1000)) ②MMULTパターン =ARRAYFORMULA(MMULT(A1:E3, SEQUENCE(COLUMNS(A1:E3), 1, 1, 0))) どちらも手続き的な記述より複雑です。なにより大きな問題は、実装の都合により SUM 関数が使われていないことです。 SUM 関数が存在しないため、「行の合計値を求める」という人間の意図が曖昧になっています。少なくともSUMIFパターンは設定すべきパラメータも多く、本当に必要なケースでしか利用すべきではありません。 このようなケースでは、シンプルさを重視して手続き的に構築することも合理的な選択肢です。宣言的に記述するとしても、MMULTパターンを 名前を付けた関数 として定義し、処理の意図をあきらかにしたうえで利用することをおすすめします(たとえば SumByEachRow など)。 まとめ 本記事では、Google Sheetsにおける処理アプローチを言語化するために「手続き的」「宣言的」というふたつの概念を持ち込み、それぞれの具体例や関数のリファレンスを示しました。これらが読者の皆さんの選択肢に加わり、スプレッドシートの利用がより効率的になることを願っています 2 。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。Made by ZOZOを支えるソフトウェアエンジニアのポジションも募集しています (Go, TypeScript)。ご興味のある方は、以下のリンクからぜひご応募ください! hrmos.co hrmos.co Google Sheetsが内部的に「1899年12月30日=ゼロ」として管理していること、空白セルを数値として扱うとゼロになることが原因のようです。前者については、 Lotus 1-2-3互換に関連する挙動であるという説明 もあります。 ↩ 本稿における「手続き的・宣言的」という単語借用のアイデアについては、VisiCalcに対するテッド・ネルソンのコメントから刺激を受けました。“Where conventional programming was thought of as a sequence of steps, this new thing was no longer sequential in effect” ( Nelson, T. (1989). In S. Brand (Ed.), Whole Earth Software Catalog for 1986 (p. 66). Quantum Press/Doubleday. ) ↩
はじめに こんにちは。ブランドソリューション開発本部FAANSバックエンドブロックの佐野です。普段はサーバーサイドエンジニアとして、FAANSのバックエンドシステムを開発しています。 FAANSとは、弊社が2022年8月に正式ローンチした、アパレル店舗で働くショップスタッフの販売サポートツールです。例えば、コーディネート投稿機能や成果確認機能などを備えています。投稿されたコーディネートはZOZOTOWNやWEAR、Yahoo!ショッピング、ブランド様のECサイトへの連携が可能です。成果確認機能では、投稿されたコーディネート経由のEC売上やコーディネート閲覧数などの成果を可視化しています。 本記事では、成果データの集計処理におけるBigQueryのクエリ実行処理のユニットテストをGoで実装した取り組みと、その際の工夫についてご紹介します。 目次 はじめに 目次 成果データの集計処理とは 抱えていた課題 バグが発生しやすい 動作確認が煩雑になる 正しい動作を判断しづらい なぜSQLのテストをGoで書いたのか テストの実装 フィクスチャ テストケースの分割 悪い例 良い例 テストケースごとにテーブルを作成 Goのtemplateを使って接続先の差し替えを容易に 結果 開発効率が上がった バグが発生しにくくなった レビュアーの負担が減った QAの負担が減った 今後の展望 ボイラープレートの自動生成 QA用テストデータ作成の環境づくり さいごに 成果データの集計処理とは 本題へ入る前に、FAANSの成果データの集計処理について簡単にご説明します。 全社のデータ基盤のBigQueryには、例えばWEARやZOZOTOWNのユーザーが「コーディネート画像を閲覧した」といった様々な種類のビジネスイベントのデータが格納されています。それらのイベントデータから、コーディネート画像がどれくらい閲覧されたかや、コーディネート画像経由でどの商品がいくつ購入されたかといった様々な種類の成果のデータをバッチ処理で集計しています。 抱えていた課題 前述の通り、FAANSはZOZOTOWN・WEAR・Yahoo!ショッピング・ブランド様のECサイトと連携しているという特性上、複数のデータソースからデータを抽出する必要があります。 特に日次のバッチ処理で行っている成果データの集計処理では、複雑な条件によりSQLのクエリが長くなる傾向にあります。長いものだと10テーブルをJOINした上、WHERE句で8つほどの条件を指定しているため、100行以上になることもあります。 そのような複雑なロジックを持つクエリに関して、以下のような課題がありました。 バグが発生しやすい 長いクエリは読み解くのにコストがかかります。WHERE句ひとつとっても、背景を把握していないとなぜこの一文が必要なのかが伝わらない場合もあります。そのようなクエリに対して追加修正が必要となった際に、ロジックを読み違えて意図しない変更を加えてしまう可能性がありました。 動作確認が煩雑になる 動作確認の際にはその都度手作業でデータを用意しなくてはならず、また複数の条件に合致するデータを用意するだけでも確認事項が多いため、そこに多くの時間が取られてしまっていました。その結果、開発効率が落ちてしまったり属人化してしまうという状況にありました。 正しい動作を判断しづらい 100行にもわたるSQLのクエリは、どのような動きが正しいのかを判断しづらく、レビューコストが高いという課題もありました。SQL内のコメントで基本的なロジックについての説明ができても、様々なパターンのデータに対してどのように動作するかを詳細に伝えるのは困難でした。 なぜSQLのテストをGoで書いたのか FAANSでは、Web APIサーバーやバッチ処理といったバックエンドシステムの全てをGoで実装しています。また、今後も様々な分析データを提供するために、複雑なクエリを用いたデータ抽出処理は増えていくと考えられます。それらの理由から、チームの学習コストや開発生産性を考慮して、他言語のツールを導入するのではなくGoで書くことが適切と判断しました。 テストの実装 では、どのようにテストを書いたか説明していきます。 フィクスチャ 今回は、SQLを組み立ててBigQueryでクエリを実行する処理のユニットテストを実装しました。テストデータの投入はメルカリ社の記事( Goでテストのフィクスチャをいい感じに書く )を参考に用意したフィクスチャを使用しました。 テストケースの分割 テストケースの分割では、網羅性と凝集度を高めることを意識しました。例えば、売上の種類が3つ存在し、それぞれの合計額を取得する処理のテストを書くとします。 悪い例 func TestCalculateSalesAmount(t *testing.T) { f := fixture.Build(t, fixture.Sales( func (s *model.Sales) { s.Type = "A" s.Amount = 1000 }), fixture.Sales( func (s *model.Sales) { s.Type = "A" s.Amount = 2000 }), fixture.Sales( func (s *model.Sales) { s.Type = "B" s.Amount = 5000 }), fixture.Sales( func (s *model.Sales) { s.Type = "C" s.Amount = 3000 }), fixture.Sales... ) ... f.Setup(t) t.Run( "タイプA・タイプB・タイプCの売上データが取得でき、それぞれの合計額は3000円・5000円・10000円である" , func (t *testing.T) { ... if len (result) != 3 { t.Errorf( "売上データの件数に過不足がある" ) } if result[ 0 ].Type != "A" { t.Errorf( "タイプAの売上データが取得できていない" ) } if result[ 0 ].SalesAmount != 3000 { t.Errorf( "タイプAの売上データの合計額に誤りがある" ) } if result[ 1 ].Type != "B" { t.Errorf( "タイプBの売上データが取得できていない" ) } if ... }) } このように複数のパターンを一度にテストしようとすると、準備するべきデータや判定ロジックが増えてしまいます。すると、凝集度が低く見通しの悪いものとなってしまうため、ひとつのテストケースで確認する必要がないものは、以下のように分割しました。 良い例 func TestCalculateSalesAmount_TypeA(t *testing.T) { f := fixture.Build(t, fixture.Sales( func (s *model.Sales) { s.Type = "A" s.Amount = 1000 }), fixture.Sales( func (s *model.Sales) { s.Type = "A" s.Amount = 2000 }), fixture.Sales( func (s *model.Sales) { s.Type = "B" // 合算の対象にならないことを確認するために作成 s.Amount = 5000 }), ) ... f.Setup(t) t.Run( "タイプAの売上データが取得でき、合計額は3000円である" , func (t *testing.T) { ... if result[ 0 ].Type != "A" { t.Errorf( "タイプAの売上データが取得できていない" ) } if result[ 0 ].SalesAmount != 3000 { t.Errorf( "タイプAの売上データの合計額に誤りがある" ) } }) } 分割したことで準備するデータや確認項目も減り、テストケースの意図が明確になりました。このように分けることで、 TestXXX_TypeB 、 TestXXX_TypeC とテストコードが増えることにはなりますが、テストケースの凝集度を高めると、結果的にメンテナンス性も向上します。 テストケースごとにテーブルを作成 前項で示したように、複数のテストケースが存在していると、別のテストケースで用意したデータを参照してしまい、意図した結果とならないという問題が起きます。テストケース同士の関連やデータの競合について考慮しながらメンテナンスしていくのは難しいため、テスト用のヘルパーを用意してテストケースごとにテーブルを作成・削除するという方法をとりました。 まず、作成したいテーブルのスキーマをコピーして、テスト用のBigQueryのデータセットにテーブルを作成します。テーブル名はテストケース間で重複しないように、本来の名前のsuffixにランダムなIDを追加したものとしました。なお、必要なデータを投入してテストを実行した後、作成したテーブルを削除します。 テストケースごとにテーブルを分けたことにより、並列実行が可能になるというメリットもありました。実行時間の短縮のためにも、Goの標準パッケージである testing パッケージの t.Parallel() メソッドを使って、それぞれのテストケースを並列で動かすようにしました。 Goのtemplateを使って接続先の差し替えを容易に テスト対象の処理では、SQLはファイルに切り出し、Goの標準パッケージである text/template パッケージを用いて組み立てるようにしました。そうすることで、環境ごとに接続先のBigQueryのテーブルを切り替えることが可能になりました。また、指定するテーブルの情報はメソッドの外から渡すようにして、テスト時も差し替えがしやすい作りを意識しました。 SELECT ... FROM {{.project_id}}.{{.dataset_id}}.{{.table_id}} WHERE ... // templateを使ったSQLの組み立て func BuildSQL(path string , params any) ( string , error ) { body := & bytes.Buffer {} t, err := template.ParseFS(templates, path) if err != nil { ... } if err := t.Execute(body, params); err != nil { ... } return body.String(), nil } // 呼び出し側 sql, err := query.BuildSQL( "hoge.sql" , map [ string ] string { "project_id" : "hoge" , // GCPのプロジェクトID "dataset_id" : "fuga" , // BigQueryのデータセットID "table_id" : "piyo" , // BigQueryのテーブルID }) 結果 実際にテストを書いてみたところ、1つのクエリに対するテストで検証する項目が最大20パターン存在しました。これら全ての動作確認を手作業でデータを用意して行うのは非現実的です。また、テスト対象は一度しか実行されないクエリではなく定常的に実行されるもので、今後プロダクトが成長するにつれて、新たなカラムが追加されたり仕様が変わったりする可能性も大いにあります。そのような処理に対するテストの実装をしたことで、以下のような効果がありました。 開発効率が上がった フィクスチャの実装によって手動でデータを投入しなくてよくなり、煩雑な作業を無くすことができました。また、SQLに修正を加えてロジックが変更されても、テストデータの変更が容易になったため、すぐに動作確認ができました。テストを動かすための仕組みができるまでには一定のコストがかかりましたが、一度作ってしまえばその後の開発は進めやすくなるということを体感しました。 バグが発生しにくくなった 境界値のテストが簡単にできるようになり、細部までテストを書くことでバグの発生リスクを下げることができました。変更を加えた際に予期せぬ影響があっても、テストの失敗でそれに気付くことができ、心理的なハードルも下がりました。実際に、最近リリースした案件で新たに実装した成果データの集計処理では、QA(品質保証)でSQLの実装が原因のバグは見つかりませんでした。 レビュアーの負担が減った テストを書くまでは、レビュアーは仕様書と実際の処理を見比べながら、複雑なクエリを読み解く必要がありました。しかし、テストケース名から仕様の概要を把握できるようになり、効率よくレビューできるようになりました。テストケースの不足から考慮漏れに気付きやすくなり、レビューの質も向上しました。 QAの負担が減った 最大20パターンの検証項目があると、QAの際に必要となるデータのパターンも多岐に渡ります。ユニットテストでエッジケースを担保できるようになったため、QAの工数の削減に繋がりました。 今後の展望 今回の取り組みにより一定の効果が得られた一方で、技術的負債の影響でテストが書けていない箇所もまだ存在します。動作を確認しているとはいえ、テストを書けていない機能のリリースには不安が残るため、負債を解消しながらこの取り組みを継続的に行っていきたいと考えています。また、今後は以下の観点でも改善していきたいです。 ボイラープレートの自動生成 現状では、作成するテーブルのスキーマやフィクスチャのボイラープレートを手動で書いています。ある程度テストを書いてパターンが見えてきたため、今後はこれらを自動生成して、テスト作成の効率を上げていきたいです。 QA用テストデータ作成の環境づくり ユニットテストがあるとはいえ、QAではシナリオテストを実施したいと考えています。その場合、必要なデータの準備にはまだまだ改善の余地があります。データ投入を簡単に行えるツールを作成するなど、より良い方法を模索していければと考えています。 さいごに ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
こんにちは、ブランドソリューション開発本部フロントエンド部の田中です。 普段はFAANSのWebフロントエンドの開発を行なっています。 FAANSとは「Fashion Advisors are Neighbors」がサービス名の由来で、ショップスタッフの効率的な販売をサポートするショップスタッフ専用ツールです。 ショップスタッフ向けにコーデ投稿・成果確認などの機能が存在し、2022年8月に正式ローンチしました。詳しくは以下のプレスリリースをご覧ください。 corp.zozo.com 現在FAANSは立ち上げから2年経過し、Webフロントエンドの開発現場において様々な組織的・技術的課題がありました。 今回はその課題と取り組みについて紹介したいと思います。 目次 目次 前提 FAANSの組織の特徴 FAANSのWebのプロダクトの特徴 変化の多い環境下で遭遇し続ける課題 組織的・技術的課題とその取り組み 課題1: UIコンポーネントの作成に時間がかかっていた 取り組み1: UIコンポーネントライブラリのChakra UIを導入した 課題2: FAANSのWebを開発しているメンバーが1人となり案件をさばけるような体制ではなかった 取り組み2: FAANS内の他職種の人に協力してもらう 課題3: 開発ドキュメントが少なく属人化していた 取り組み3: 開発ドキュメントを作成し、フロードキュメントとストックドキュメントを分けて運用 課題4: FAANSのWebにおいて何の課題を優先して取り組むべきか分からなかった 取り組み4: FAANSのWebチームで抱えている課題をJIRAで管理するようにした 課題5: 権限やフラグによってUIの表示や機能が異なり把握しづらかった 取り組み5: Storybookを使って多様なUIを管理した 課題6: フロントエンドとバックエンドの差異を吸収する層が存在していなかった 取り組み6: フロントエンドとバックエンドの差異を吸収するPresenters層を設けた 課題7: FAANSで使っていたCreate React Appにおいてメンテナンス状況に不安があった 取り組み7: Viteへ移行 課題8: OpenAPIのymlを手動でコピーして運用していた 取り組み8: submoduleを使ってOpenAPIのymlを参照し、自動でAPI Clientを生成するようにした 終わりに 前提 まず前提としてFAANSの組織とWebのプロダクトの特徴について紹介したいと思います。 FAANSの組織の特徴 FAANSはWeb、iOS、Androidのプラットフォームが存在します。立ち上がって2年ほどで、スタートアップのような小規模なチーム(チーム全体で約15名)で開発をしています。 FAANSのWebのプロダクトの特徴 FAANSは導入が決まった企業のみが利用でき、検索エンジンには載らないログイン必須な業務ツールです。 したがって、SEOは意識せず、ページ遷移やユーザーによる操作などのインタラクションが多いのが特徴です。その特徴のもとユーザービリティの高い体験を提供するために、以下のようにクライアントサイドレンダリングをベースにして開発しています。 アプリケーションのコードをViteを使ってビルドし、生成されたHTML,CSS,JSなどのファイルをFirebase Hostingからブラウザへ配信する構成になっています。 配信後クライアントサイドレンダリングでページ遷移し、データが必要な場合はバックエンドのAPIを叩いてデータを取得するシングルページアプリケーションです。 初回表示はFAANSの全体のフロントエンドのファイルをブラウザに配信するため時間がかかりますが、その後のページ遷移に関してはクライアント側でレンダリングするため速くなります。FAANSはSEOを意識しない業務ツールで頻繁にページ遷移が発生することから、初回表示よりもその後のページ遷移の速度を重視してこのような構成になっています。 また現時点(2023年10月時点)で採用している主な技術スタックは以下の通りです。 TypeScript React Vite Chakra UI React Hook Form Storybook 変化の多い環境下で遭遇し続ける課題 慎重にプロダクトに合わせたチームを形成・技術選定がされていたものの、運用していくにつれていくつか課題に遭遇しました。 後に紹介しますが、Webフロントエンドの開発チームが1人になり案件をさばける体制でない、立ち上げ当初は推奨されていた技術がメンテナンスに不安があるといった組織的・技術的にも取り組まないといけない課題がありました。 そのような変化がある環境下で、企業様が必要とする機能開発とバランスをとりながら、プロダクトチームと話し合い、優先度をつけてそれらの課題に取り組む必要がありました。 組織的・技術的課題とその取り組み 以降遭遇した組織的・技術的な課題とその取り組みについて時系順に紹介したいと思います。同じような課題感を持つ方の参考になれば幸いです。 課題1: UIコンポーネントの作成に時間がかかっていた 開発当初のFAANSのWebフロントエンドチームは2名体制で、styled-componentsを採用して1からCSSやJSを書いてUIコンポーネントを作成していました。 UIコンポーネントライブラリをFAANS用のUIにカスタマイズする案もありましたが、そのUIに引っ張られてカスタマイズに時間がかかる懸念から、当時は1から書く手法をとっていました。 しかし、モーダルのような複雑なUIコンポーネントを作成するときに時間がかかって、思うようにスピード感が出せてないと感じていました。少ないチームメンバーでスケジュール通りに機能を届けるためにも、そのスピード感を上げる必要がありました。 取り組み1: UIコンポーネントライブラリのChakra UIを導入した そこでCSSライブラリの変更の舵を切るのに不安がありましたが、プロダクトやチームの特性を考えて途中でChakra UIを導入することにしました。主な理由は以下の通りです。 機能豊富なコンポーネントが備わっていて、かつ、アクセシビリティが考慮されており、その土台がある状態からUIコンポーネントを作成できる。 UIの個性が強くなく簡単にスタイルを上書きできるのでFAANSのUIにカスタマイズしやすかった。 自作していたstyled-componentsのUIコンポーネントに対して、marginTopやfontSizeなどのスタイリングをするために、Styled Systemを使って拡張していた。Chakra UIもStyled Systemを参考にしているため、スタイリングの際のpropsのインタフェースがほとんど同じで移行しやすかった。 実際に運用してみるとstyled-componentsからの移行もスムーズで、UIコンポーネントの作成にかかる時間を短縮できました。これは少ないチームで開発をする上で大きなメリットとなりました。 また選定時は意識していなかったのですが、Inputのようなフォームのコンポーネントを非制御コンポーネントとして扱えるのもFAANSのWebにとってメリットがありました。 FAANSのWebでは店舗登録やスタッフ登録画面などのフォームが多く、フォームの管理のためReact Hook Formを採用しています。主な採用理由は非制御コンポーネントに対しても扱うことができ、その場合にフォームのデータをDOM自身が管理するため、再レンダリングの回数を減らせるからです。 以下のようにReact Hook Formのregisterを使って、フォームのコンポーネントに対してrefを渡すことで、データが更新されたとしても再レンダリングされないようにできます。 制御コンポーネントを扱うControllerを使うのに比べてregisterでの登録は記載が短くすみ、可読性の向上に繋がりました。 import { forwardRef , Input , InputProps } from '@chakra-ui/react' ; export const StyledInput = forwardRef < InputProps , 'input' >(( props , ref ) => ( < Input height = "42px" fontSize = "13px" borderColor = "gray.CCCCCC" borderRadius = "4px" _placeholder = {{ color: 'gray.999999' , }} ref = { ref } { ...props } / > )) < StyledInput { ...register ( 'externalUrl' ) } placeholder = "例:https://faans.jp" / > 課題2: FAANSのWebを開発しているメンバーが1人となり案件をさばけるような体制ではなかった FAANSのWebフロントエンドの開発は2人で開発していたのですが、一時的に1人のメンバーが開発から離れ、自分1人になった時期がありました。 その時期にとある企業様のFAANSの導入を進めるにあたって、スケジュール優先で案件を着地させる必要がありました。 メンバーが自分1人になる前、FAANSのWebフロントエンドの採用へ繋げるための土台づくり(テックブログを書く・登壇する)をしたものの、エンジニアの採用は難しく採用へ繋げることができませんでした。 そこで機能のスコープを優先度が高いものに絞ったり、開発効率を向上するために自動化などの施策を試みたものの、メンバー1人では案件をさばける状況ではありませんでした。 余裕をもった上で案件を着地させるためにも、この状況下で適切な手段を考える必要がありました。 取り組み2: FAANS内の他職種の人に協力してもらう そこでチームで解決策について話し合った結果、FAANSチームの他の職種からもWebフロントエンドの開発に協力してもらう手段を選択しました。というのも、Webフロントエンドの経験があったり、Webの最近の動向を知って自分の開発に活かしたいと興味のあるメンバーがいたからです。 実際にそのとき余力があったバックエンドのエンジニア2名とiOSのエンジニア1名に協力してもらい、自分を含む計4人体制で開発を進めました。 お願いする際にこの協力は評価されるかという点を気にしていましたが、会社の評価制度も柔軟で評価対象だと分かり安心してお願いできました。 協力の際にはJIRAでタスクを細かく切って、ガントチャートで進捗を可視化しつつ、その方のフロントエンドの経験度に合わせてタスクをお願いしました。 また新しく開発に関わる人にはどのディレクトリにどのファイルを置けば良いか、どういった作法でコードを書けば良いのか分からないという問題がありました。そこはHygenを使って指定したディレクトリにテンプレートのファイルを自動生成することで、開発時の迷いを軽減させるようにしました。 この協力体制のもと案件を切り抜けられました。この手法のメリットとして、再度Webのフロントエンドの開発の人手が足りない時に一度協力してもらった方にお願いできる余地ができました。 課題3: 開発ドキュメントが少なく属人化していた 取り組み2でFAANSの他職種の人にWebフロントエンドの開発の協力をしてもらい4人体制で開発を進めましたが、開発ドキュメントが充実していませんでした。質問の度に口頭で設計や運用ルールなどのWebフロントエンドの開発に必要な説明をしていて時間がかかっていました。 取り組み3: 開発ドキュメントを作成し、フロードキュメントとストックドキュメントを分けて運用 取り組み2の案件が終わった後、メンバーが1人仲間に加わり、FAANSのWebフロントエンドの開発メンバーが2人になりました。 そのタイミングで開発ドキュメントを作成し、それを見ればFAANSのWebフロントエンドの開発に必要な情報が分かるようにしました。 この結果、属人化が抑えられ開発の説明にかかる時間が短縮されました。 ただ、次の懸念としてそのドキュメントがメンテナンスされ続けるかという点を気にしていました。開発ドキュメントは一度作成して終わるというわけではなく、プロダクトの成長に伴ってドキュメントを追加したり更新する必要があります。 各々がバラバラにドキュメントを配置してしまったり、どのドキュメントを信頼すべきか分からなくなる可能性があり、それを避ける必要がありました。 そこでドキュメントを以下のように分けて運用しました。 ストックドキュメント: アーキテクチャーなど定期的にメンテナンスする必要があるドキュメント フロードキュメント: メンテナンスする必要がないメモのような一時的なドキュメント メモ書きでも良いのでチームとしてドキュメントは残す方針にし、一度フロードキュメントに入れてその中で良い内容はストックドキュメントにも記載、適切なタイミングで階層構造を整備する運用にしました。こうすることで、ドキュメントを残す文化が醸成し、ストックドキュメントにはメンテナンスされていて信頼できる情報が残るようになりました。 課題4: FAANSのWebにおいて何の課題を優先して取り組むべきか分からなかった 取り組み3で開発ドキュメントを作成したことによって、開発の説明にかかる時間は短縮されました。そのタイミングで一時的に離れていたメンバーも戻ってきて、FAANSのWebチームも3人になりました。ただ中途で入ったばかりの人や久しくFAANSの開発に携わる人にとって、案件とは別に現状どの課題が存在しているのか、何の課題を優先して解決すべきか分からないという声がありました。その結果優先度が低い課題に着手してしまう可能性がありました。 取り組み4: FAANSのWebチームで抱えている課題をJIRAで管理するようにした FAANSのWebチームの抱えている課題をJIRAのチケットとして作成して、管理するようにしました。その課題に対しては「FAANS_WEB_IMPROVEMENT」のラベルを貼るようにし、抱えている課題のチケットに絞って一覧で表示できるようにしました。それを元にチーム内で話し合い、課題に対してHigh、Medium、Lowなどの優先度付けをしました。これによって、メンバー全員に課題感の共有ができ、優先度が高い課題に取り組む体制となりました。 課題5: 権限やフラグによってUIの表示や機能が異なり把握しづらかった FAANSはショップスタッフ、ショップを管理する人、企業を管理する人などの権限や企業が持つ自社のECとの連携状況などのフラグが存在しています。そして、その権限やフラグによって表示されるUIや機能が異なるのもFAANSの特徴の1つです。 そのため各権限やフラグにおいて表示されるUIや機能が正しいか確認するために、アカウントを切り替えながら開発していて手間だと感じていました。 また、ある権限やフラグのアカウントで修正したときに他の条件のアカウントに影響が出ていないか自動的に担保する仕組みがありませんでした。 取り組み5: Storybookを使って多様なUIを管理した そこで解決策としてStorybookを導入し、権限やフラグの状態をMock Service Workerなどを使ってモックして、それぞれのページにおけるStoryを一覧で見られるようにしました。これでアカウントを都度切り替えずとも各UIのパターンを把握しながら開発できるようになりました。一例を挙げると以下のように自社EC連携の有無によって自社ECのカードが表示されるか確認できます。 また、StorybookではInteraction testsの機能が備わっており、表示されたUIに対して期待値と一致しているかのテストができます。以下では自社ECを連携している場合に自社ECのカードが表示されるかをテストしています。そのテストをCIに組み込むことができ、リグレッションが起きていないか自動的にテストできました。 export const IsLinkedOwnedEc: Story = { name: '自社EC連携をしている場合' , parameters: { msw: { handlers: { mockGetStaffMember: rest. get( ` ${ MSW_ORIGIN } /v1/staff_member` , ( req , res , ctx ) => { return res ( ctx.json ( produce ( mockGetStaffMemberBaseResponse , ( draftState ) => { draftState.company.is_linked_with_owned_ec = true ; } ), ), ); } , ), } , } , } , play: async ( { canvasElement , step } ) => { await step ( '自社ECのカードが表示されていること' , async () => { const title = '自社EC' ; await within ( canvasElement ) .findByRole ( 'region' , { name: title , } ); } ); } , } ; また、FAANSは業務ツールということもありユーザーによる操作などインタラクションが多いのも特徴です。例えばフォームの入力、ボタンのクリックなどの操作です。インタラクション後のUIも期待値通りか自動的に担保したく、それが可能で見やすい形でデバッグができるInteraction Testsの機能はFAANSのプロダクトの特性に合った選択肢でした。 Storybookの運用にあたって、Storybookの作成が目的化して、メンテナンスするためのモチベーションの低下を懸念していました。 しかしUIコンポーネントの管理の他に、以下のようにStorybookをメンテナンスするための目的を持たせたり、開発フローに取り込む事でモチベーション低下を防ぎました。 多様なUIパターンを把握しながら開発する その多様なUIやインタラクション後のUIも含めて自動テストをし、不具合を検知できるようにする 次の課題として、表示されたUIに対して担保したい項目が多く、その分テストコードも増えてメンテナンスコストが高くなっています。カバー率の高いビジュアルテストと組み合わせて、コストを下げることを検討しています。( 参考 ) 課題6: フロントエンドとバックエンドの差異を吸収する層が存在していなかった フロントエンドとバックエンドの差異を吸収する層が存在しないため、APIの変更の影響を受けやすいViewの実装がありました。 一例をあげると以下のフォームのように、APIのリクエストのあるパラメーターの型がbooleanなので、onChangeの際にbooleanへ変換するAPIを意識したViewの実装です。 この実装をするとAPIのリクエストのパラメーターの変更によって、Viewのコードの変更が必要になります。またこの変換や加工といった吸収処理をどこに書くかルールが決まっておらず、開発者によって書く場所が異なり、レビューコストが上がってしまいました。 // フォームのViewの実装例。ここではReact Hook FormのControllerでフォームのデータを管理。 < Controller control = { control } name = "parameter_name" render = { ( { field } ) => { return ( < RadioGroup onChange = { ( e ) => { field.onChange ( e === 'true' );   //APIのリクエストのパラメーターの型に合わせるために、stringをbooleanに変換している。このようにViewで書く開発者もいれば、onSubmit時に書く開発者もいて統一されていなかった。 }} value = { field.value ? 'true' : 'false' } > < Radio value = "false" > false < /Radio > < Radio value = "true" > true < /Radio > < /RadioGroup > ); }} / > 取り組み6: フロントエンドとバックエンドの差異を吸収するPresenters層を設けた フロントエンドとバックエンドの差異を吸収するPresenters層を設けてそこで吸収させることにしました。 これによって、APIのインタフェースの変更の影響範囲はそのPresenters層に限定でき、Viewにその影響を与えないようにしました。 またViewに書かれていた変換や加工といった吸収処理がPresenters層に統一されたことで、開発者がどこに書くか迷うことがなくなったり、Viewのコードもシンプルになりました。結果としてレビューコストも下がるようになりました。 // フォーム送信時に呼び出すPresenters層の関数。 export const xxxPresenter = async ( formData: FormData ) : Promise < Response > => { // formDataをAPIのインターフェースに合わせて変換・加工する。(例、stringをbooleanへと変換。) // 変換後にAPIを呼ぶ。 } ; // フォームのViewの実装例。ここではReact Hook FormのControllerでフォームのデータを管理。 < Controller control = { control } name = "name" render = { ( { field } ) => { return ( < RadioGroup //stringのまま管理。APIは意識せずにViewのコードを書ける。 onChange = { field.onChange } value = { field.value } > < Radio value = "false" > false < /Radio > < Radio value = "true" > true < /Radio > < /RadioGroup > ); }} / > 課題7: FAANSで使っていたCreate React Appにおいてメンテナンス状況に不安があった JIRAで管理している課題チケットの中にDependabotから通知されるセキュリティーアラートの数が多いという問題がありました。 そこでそのセキュリティアラートで指摘されているパッケージを見ると利用していたv4のCreate React App(以下CRA)が依存しているパッケージ起因であることが分かりました。 それを機にCRAの現在について調べてみると最近のv5へのアップデートが1年前だったり、メンテナンス状況に不安が残るissueがいくつかありました。またチーム内から開発サーバーの立ち上がりに時間がかかるといった声を聞くようになりました。 旧Reactの公式ドキュメントではSPA開発に CRAがお勧め されていて、その選定には違和感なく開発していたのですが、最近ではこのような状況になりフロントエンドの変化のスピードに驚きました。 取り組み7: Viteへ移行 そこで月1回開催されるフロントエンド勉強会で技術顧問やZOZOTOWNやWEARなどの他のチームの方に相談して、CRAに感謝しつつViteへの移行を決めました。 Next.jsでSPAを作るという候補も上がっていましたが、以下の理由でViteへ移行しました。 FAANSのWebがReact Routerに依存しておりファイルシステムベースのルーティングへの移行コストがかかりそう。 サーバーサイドレンダリングの予定がない。 移行作業もスムーズに行き、規模が大きいところでいくと環境変数の参照を process.env から import.meta.env へ変更することでした。なので環境変数を利用しているページに影響するとみて、そのページを中心にQAして頂いてリリースしました。 結果として抱えていた課題は解消され、以下のように開発サーバーの立ち上げや本番ビルドなどの速度が上がりました。 比較内容 CRA4 Vite dev cold start 約3分15秒 約13秒 dev warm start 約27秒 約2秒 hot reload 約2秒 保存直後 production build 約10分 約3分 課題8: OpenAPIのymlを手動でコピーして運用していた FAANSのWebではOpenAPI Generatorを使って、バックエンドのリポジトリに保存されているOpenAPIのymlファイルをフロントエンドのリポジトリに手動でコピーして、API Clientを生成していました。 定期的にこの作業は発生し、手動のためオペレーションミスを引き起こす可能性がありました。 取り組み8: submoduleを使ってOpenAPIのymlを参照し、自動でAPI Clientを生成するようにした そこでフロントエンドのリポジトリにsubmoduleを登録し、そこからバックエンドのリポジトリに保存されているOpenAPIのymlファイルを参照するようにしました。また、OpenAPIのymlの更新を検知して自動でAPI Clientを生成し、プルリクエストを作成するワークフローを組みました。この作業を自動化することで、オペレーションミスを防いだり、他のクライアントのチームにも展開できるようになりました。 終わりに 以上が組織的・技術的な課題とそれに対する取り組みになります。 全体的にまず課題の共有から始めて、WebフロントエンドのチームのみならずFAANSの他職種の方と会話を重ねるのも大事でした。 広い視点で、そのときの状況やプロダクトの特性に適した解決策が見つかったり、今何をやるべきかといった優先度も洗練されていくからです。 課題2のようにWebのフロントエンドの開発者が1人になり案件がさばける体制でない状況下で、他の職種の方に協力してもらう解決策はその1つの例でした。 当時は自動化して開発効率を上げる技術的なアプローチで解決することを考えていましたが、このように組織的なアプローチで解決に繋がるとは思わなかったです。 そうして話し合いながら、優先度が高い課題に対処して改善されていく日々を目の当たりにすることは、その課題の解消の効用以上にチームの雰囲気に良い影響を与えました。チームの雰囲気が良くなれば、チーム内で意見が言いやすくなったり、次なる課題に対して取り組みやすくなると思います。 この「課題の共有」→「プロダクトチームと会話」→「優先度づけして取り組む」→「チームの雰囲気が良くなる」→「次なる課題の共有」→(以降繰り返し)というポジティブなサイクルは、この変化の多い環境下で課題を解決し続けていく上で大事だと感じました。 現状FAANSのWebに関して以下のような課題を抱えており、これからも案件とバランスをとりながら取り組み続けていきたいと思っています。 Findyで開発生産性の可視化した上で施策が打ち続けられるチームづくり 開発効率を上げることを目的としたデザインシステムの作成 ZOZOではこのように課題を前向きに改善してくれるエンジニアを募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co corp.zozo.com
こんにちは、技術本部SRE部ZOZOSREチームの斉藤です。普段はZOZOTOWNのオンプレミスとクラウドの構築・運用に携わっています。またDBREとしてZOZOTOWNのデータベース全般の運用・保守も兼務しております。 ZOZOTOWNではSQL Serverインスタンスが複数存在しており、サービスのメインデータベースとして稼働しています。その中で、1つのインスタンスを共用し、2つのデータベースが相乗りしている状態で運用されている環境が存在します。相乗りしているデータベースを検討したシステム構成の制限内で分離するには詳細な調査が必要でした。後述しておりますが、調査の過程で課題が見つかり、容易に分離はできませんでした。課題を解決し、分離を実現させるために日々邁進しております。 本記事では、SQL Serverインスタンスを共用し、2つのデータベースが相乗りしている環境からデータベースを分離させるための取り組みを紹介します。データベース分離に関して構成やスペック検討、課題として顕在化する項目の一例としてご参考になればと思います。 目次 目次 データベース分離を検討した背景と課題 構成検討 ソリューション選定 可用性 SQL Serverのエディション 必要なスペックを調査する CPUコア数を見積もる 並列処理の最大限度(MAXDOP)を検証する バッファキャッシュサイズを見積もる 必要スペックの調査結果と改善案 まとめ おわりに データベース分離を検討した背景と課題 現在、1つのインスタンス上にショッピングカート関連の機能を持ったデータベース(以下、カートDB)と履歴関連のデータを持つデータベース(以下、履歴DB)が共存している状態です。どちらかのデータベースが起因で障害が発生した場合、もう一方のデータベースにも影響が波及し、障害の範囲が広がってしまいます。特にカートDBが影響を受け、ショッピングカート機能に影響を受けるとZOZOのサービス継続に多大なインパクトを発生させてしまいます。障害の影響範囲を限定的にするため、カートDBと履歴DBを分離したいと考えました。ワークロードやサービスの継続性を考慮し履歴DBをインスタンスから分離させることにしました。 構成検討 履歴DBは、ZOZOTOWN内の一部機能やサイト表示に影響があり、社内の業務システムでも使用されていることから、可用性を担保したまま移行できることを必須の条件としました。他にも、クラウドとオンプレの比較、費用や機能面、アプリケーション改修コストを主な検討項目としました。 ソリューション選定 3つのデータベースソリューションを検討しました。コストは各ソリューションを5年間運用した場合を想定して比較しました。結論としては、アプリケーションの改修コストとライセンスコストを押さえられるオンプレミスのSQL Serverを選定しました。クラウドはコストメリットが出ませんでした。ハードウェアの運用については、ノウハウが蓄積されているので運用コストの大幅な増加は無いと判断しました。 製品 アプリケーション改修コスト 運用費用(5年) ハードウェアの運用 SQL Server(オンプレミス) 低 低 有 Amazon RDS for SQL Server 低 高 無 Amazon Aurora MySQL 高 中 無 可用性 Windows Serverの標準機能として利用できるWindows Server Failover Clustering(以下、WSFC)でクラスター化し、可用性を担保することにしました。WSFCは既存環境で採用されており、安定して運用できることが選定理由です。 WSFCについての情報は Windows Server フェールオーバー クラスタリングの概要 を参照してください。 概要図 SQL Serverのエディション 既存のインスタンスはEnterprise Editionで稼働しています。データベースが独立するので、サーバスペックを縮小できると想定しました。Enterprise EditionとStandard Editionではコストが大幅に変わります。可能ならば、分離先のインスタンスはStandard Editionで稼働させたいと考えましたが、エディション毎に機能制限があります。Enterprise EditionとStandard Editionの主な違いは以下の通りです。 ライセンス価格 Enterprise EditionはStandard Editionに比べ、約4倍の価格。 Enterprise Edition:USD$13,748 Standard Edition:USD$3,586 ライセンス価格についての情報は SQL Server 2019 の価格 を参照してください。 オンライン操作 Enterprise Editionはインデックスをオンラインで作成、再構築が可能だが、Standard Editionでは不可能。 リソース制限 Enterprise Editionはサーバに搭載されているリソースを最大限に使用できるが、Standard Editionでは制約がある。 CPU:4ソケットまたは、24コアのどちらか小さいほうに制限 バッファプール:最大サイズが128GB エディション間の機能差についての詳細情報は SQL Server 2019 の各エディションとサポートされている機能 を参照してください。 ライセンス価格の安いStandard Editionを選びたいところですが、各種制限の範囲で履歴DBを移行できるか懸念があります。次のステップとしてこれらの制限が移行にどの程度影響していくかを調査します。 必要なスペックを調査する データベースを分離するにあたって難しいと感じたのが、スペック検討です。共有しているサーバリソースから履歴DB部分で使用しているリソースのみを抜き出す必要があります。メトリクスがサーバやインスタンス単位で取得されており、データベース単位で数値化しなければなりません。更にStandard Editionのリソース制限内で運用が可能かという観点でも調査が必要でした。 調査項目 CPUコア数を見積もる 並列処理の最大限度(MAXDOP)を検証する バッファキャッシュサイズを見積もる CPUコア数を見積もる 必要なCPUコア数を見積もります。まずDBのCPU使用率が24時間中で最も高かった1時間に絞りました。弊社では動的管理ビュー(Dynamic Management View:以下、DMV)の情報をロギングしています。対象時間帯で実行されている全クエリから履歴DBのCPU時間を抽出し、以下の式に当てはめて必要なコア数を算出しました。DMVのロギングについては以前のテックブログで紹介しています。 techblog.zozo.com 計算式 (履歴DBのCPU時間 ÷ 全体のCPU時間) × 全体のCPU使用率 × サーバの論理コア数 結果 (279 ÷ 557) × 0.5 × 64 = 16.02 以上の結果から、必要なコア数は約16となり、Standard Editionの制限内である24コアで稼働できる見込みとなりました。SQL Server以外のOSなどが使用するCPUも考慮し、32コアのハードウェアスペックがあれば問題ないと見積もりました。 並列処理の最大限度(MAXDOP)を検証する SQL Serverは1つのステートメントで使用できるプロセッサの最大数を決めることができます。既存のインスタンスはMAXDOPが8で設定されており、Standard EditionでもMAXDOPを8に設定できます。しかし使用できるCPUコア数が24のため、MAXDOPが8のクエリが3セッションで実行されると、CPU使用率が100%に達してしまう可能性があります。Standard Editionの並列処理度を考慮するとMAXDOPを4程度に減らすことを検討する必要があります。並列処理の最大限度(MAXDOP)が半減した場合にどの程度の性能劣化が生じるのか検証しました。 並列処理の最大限度(MAXDOP)についての詳細情報は データベース エンジンの構成 -MAXDOP ページ を参照してください。 以下の調査クエリで履歴DBからMAXDOP8で実行されているクエリを抽出します。結果が比較しやすくなるので、調査クエリの結果から実行時間の長いクエリをピックアップしました。 select top 1000 last_execution_time ,execution_count ,last_elapsed_time ,last_logical_reads ,last_physical_reads ,max_dop ,text from sys.dm_exec_query_stats outer apply sys.dm_exec_query_plan (plan_handle) as qp outer apply sys.dm_exec_sql_text (sql_handle) as sql where qp.dbid = db_id( ' 履歴DB ' ) and max_dop = 8 MAXDOP4に設定したStandard Editionの環境でピックアップしたクエリを実行し、結果を確認します。 実行結果 実行時間:20秒 先行読み取り数:0 論理読み取り数:8112285(100%キャッシュに載っている状態) 実行したクエリのMAXDOPを確認します。 実行結果 MAXDOP4で実行されていることを確認 同様のクエリに「option (maxdop 8)」を指定して再度実行します。「option (maxdop 8)」を指定すると強制的にMAXDOP8で実行されます。 実行結果 実行時間:10秒 先行読み取り数:0 論理読み取り数:8112285(100%キャッシュに載っている状態) 以上の検証結果から同時実行性を考慮し、MAXDOPを4にした場合、MAXDOP8で実行されているクエリは性能劣化が想定される結果となりました。業務要件を精査してどうしても性能劣化が許容できないクエリにのみ「option (maxdop 8)」を指定するなどの対策が必要という結論に至りました。 バッファキャッシュサイズを見積もる 必要なバッファサイズを見積もります。現状の各データベース毎のバッファキャッシュサイズを調査しました。 各データベース毎のバッファサイズ調査クエリ select count (*)* 8 / 1024 / 1024 as buffer_cache_size , case database_id when 32767 then ' ResourceDb ' else db_name(database_id) end as database_name from sys.dm_os_buffer_descriptors with (nolock) group by db_name(database_id) ,database_id order by buffer_cache_size desc ; 各データベース毎のバッファサイズ buffer_cache_size database_name 496GB 履歴DB 149GB カートDB 履歴DBで使用されているバッファキャッシュのサイズは496GBでした。履歴データを保持しているテーブルはデータ量が多く、バッファキャッシュを想像以上に使用しており、Standard Editionの制限である128GBの約4倍のサイズでした。バッファキャッシュに載っているデータの中でアクセスの無い余剰なデータがあると考え、調査する方法を検討しました。 テスト環境を作成する 本番環境と同等のスペックを持つ周辺システムの構築とワークロードを再現することは現実的には難しい 本番環境のバッファキャッシュサイズに使用上限を設定して影響調査する 履歴DBのバッファキャッシュに限定して使用上限を設定できず、インスタンス全体に影響が波及してしまう インスタンスを共用しているデメリットが現れてしまいました。残念ながらバッファキャッシュ内の余剰なデータを調査できませんでした。しかし、下記の結果の通りディスクを読み込んでいる処理が存在するのを確認できました。現状、問題になっているわけではありませんが、現時点でキャッシュアウトが発生していることが想定され、余剰なデータはバッファキャッシュに載っていないと判断しました。 物理読み込み発生有無の調査クエリ select top 1000 last_logical_reads ,last_physical_reads ,max_dop ,text from sys.dm_exec_query_stats outer apply sys.dm_exec_query_plan (plan_handle) as qp outer apply sys.dm_exec_sql_text (sql_handle) as sql where qp.dbid = db_id( ' 履歴DB ' ) and last_physical_reads > 0 order by max_physical_reads desc 実行結果 last_physical_reads値が0ではないので、物理読み込みが発生していることが想定できる キャッシュアウトが増加した場合に備え、どの程度の性能劣化が起きるのか検証することにしました。まずはメモリを大量に使っているクエリを調査します。 メモリを大量に使用している調査クエリ select top 1 last_execution_time ,execution_count ,last_elapsed_time ,last_logical_reads ,last_physical_reads ,max_dop ,text from sys.dm_exec_query_stats outer apply sys.dm_exec_query_plan (plan_handle) as qp outer apply sys.dm_exec_sql_text (sql_handle) as sql where qp.dbid = db_id( ' 履歴DB ' ) order by max_logical_reads desc 該当したクエリ 実行時間:11秒 last_logical_reads:58GBをデータキャッシュから読み込んでいる 7494590ページx8KB=59956720KB=58GB last_physical_reads:ディスク読み込みは発生していない 該当クエリをテスト環境で実行し性能差を比較します。キャッシュをクリアし、該当クエリを実行します。 dbcc dropcleanbuffers 実行結果 実行時間:4分22秒 先行読み取り数:8112285 論理読み取り数:8112285 SQL Serverの仕様上physical_readsの値には反映されず、先行と論理の読み取り数が同じ場合、物理I/Oが発生していると判断します。キャッシュクリアをしたので、物理ディスクからの読み込みになっており想定通り実行時間が増加しました。 同様のクエリを再度実行します。 実行結果 実行時間:9秒 先行読み取り数:0 論理読み取り数:8112285 今回は100%キャッシュに載っている状態で実行され、本番と同じパフォーマンスで実行されました。バッファキャッシュに載っていればStandard Editionでも性能が出ることを確認できました。 以上の調査結果からStandard Editionの制限内でバッファキャッシュを128GBにした場合、キャッシュアウトの増加が予想されます。クエリの性能劣化が想定されるため、メモリを大量に使用しているクエリの改善や業務要件の見直し、性能劣化の許容範囲などを調整していく必要があるという結論に至りました。 必要スペックの調査結果と改善案 現状のまま履歴DBをStandard Editionに分離した場合、著しい性能劣化が想定される結果になりました。分離する前にデータベースを最適化する必要があることを確認し、改善案を検討しました。 CPUコア数 履歴DBのCPU使用率が高い時間帯のCPU時間から計算し、16コアが必要となりました。Standard Editionの制限内に収まりますが、なるべくCPU使用を抑える施策をしておくのがよさそうです。 改善案 CPUを非効率に使用しているクエリのチューニング 並列処理の最大限度(MAXDOP) Standard Editionは使用できるCPUコア数が24のため、MAXDOPを4程度に減らすことを検討する必要があります。クエリの性能劣化は避けられないので対策案を出しました。 対策案 MAXDOPを中間値の6に設定できるか検討する 業務要件を精査して性能劣化が許容できないクエリをピックアップ 対象のクエリにのみ「option (maxdop 8)」を指定する バッファキャッシュサイズ Standard Editionは使用できるバッファキャッシュサイズが128GBです。現状使用しているバッファキャッシュサイズの1/4程度になるので、キャッシュアウトが増加し、クエリの性能劣化が想定されます。メモリを大量に使用しているクエリを改善する必要があります。 改善案 大量のデータを処理している処理の改修 日時で全件selectをしている処理を改修する メモリ使用が多いクエリのチューニング 列ストアインデックスの検討 データを圧縮することで、ストレージの使用量が削減され、データの読み取り性能を向上させる 大量のデータを一括で処理するバッチ処理の性能が向上する 列ストアインデックスについての情報は 列ストア インデックス: 概要 を参照してください。 改善の余地があるので、上記の各改善案をもとに現状の状態で改善をし、Standard Editionの制限内で分離が実現できるか取り組んでいきたいと思います。 まとめ データベースの分離を検討した場合、スペックを縮小するのは難しい場合があります。コストや制限を考慮し、設けられた範囲内で分離するために施策を講じる必要があります。リソースを効率的に使用するために、1つのインスタンスに複数のデータベースを構築する場面は珍しくありません。しかし、分離の必要性が出てきた際にスペックを詳細に算出するのが難しいという課題があります。SQL Serverに関してはエディションによって制限があり、限られたリソースの範囲で分離を検討しなければならず、考慮するポイントが多いと感じました。「リソースに空きがある」という理由だけで1つのインスタンスを共用するのはリスクが高いと感じます。長期的な目線でデータベースの運用や障害リスクを考慮し、慎重に検討していくことが必要です。上記の通り、弊社では分離の前にリソースを最適化するという課題が現れました。今回考えた改善や改修案の実施結果については、また別の機会にお話できればと考えています。 おわりに ZOZOでは、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com
はじめに こんにちは。DevRelブロックの @wiroha です。9月26日に「 Ask the Masters - 評価制度や組織設計 」と題して、ZOZO CTOの瀬尾とタイミー VPoTの山口さまによる対談イベントを開催しました! イベント内容 本イベントはオフライン・オンラインのハイブリッドで開催しました。オフライン会場にはタイミーさまのイベントスペースをお借りしました。 タイミーさまの広いイベントスペース 今回の企画は、タイミーの山口さまとZOZOの瀬尾がDeNA出身という共通点を持ち、親交があるという背景から実現しました。山口さまは2023年5月に株式会社タイミー執行役員VPoTに就任し、瀬尾は2023年6月に株式会社ZOZO執行役員CTOに就任しています。そこで、組織の作り方や技術戦略といった共通の関心事をディスカッションする場として設けることにしました。 パネルディスカッション「評価制度や組織設計」 ZOZOの瀬尾(左)とタイミーの山口さま(右) 「評価制度や組織設計」をテーマにいくつか質問を用意し、ディスカッションを行いました。当日の内容はYouTubeのアーカイブで視聴できます。 www.youtube.com 機材トラブルのため序盤はパネリストの音声が小さく聞き取りづらい状態ですが、 23:51 頃から解消しております。 用意した質問を一部抜粋します。回答が気になる方はぜひYouTubeをご覧ください! Q. EMは会社によって定義が微妙に違うので難しいロールだと感じておりますが、お二人が考えるEMの定義について教えてください Q. 開発組織作りにおいて参考にしている会社や書籍はありますか? また独自で工夫や意識していることを教えてください Q. お二人とも組織の急成長を経験されていると思いますが、開発組織のスケールのペインとそれをどう乗り越えたかを教えてください Q. 今まで多くの意思決定をされてきたと思いますが、組織レベルの意思決定で今振り返るとあの決断をして特に良かったと思う意思決定はありますか? Q&Aセッション Slidoを見ながら質問に回答 Q&Aセッションでは、Slidoで寄せられた質問に対して回答していきました。パネルディスカッションで話された内容の深掘りや、具体的な指標を教えてほしいといった質問が寄せられました。 「いつ頃から開発組織作りに興味を持っていたか」という質問に関して、おふたりともDeNA在職時はスペシャリスト寄りで、組織作りにはあまり意識を向けていなかったと話していました。そのような背景から徐々に組織作りへの関心が高まっていく変化は興味深いと感じました。 瀬尾がチームを持ちたいと思ったきっかけについて「ひとつの大きな成果を出そうと思うとひとりだけで全部やるのは無理がある。もっと会社に貢献していくために、チームをつくって同じミッションに向かって動かすことが必要だと思った」と語っていたのが印象的でした。 最後に 終了後、タイミーさまのマスコットキャラクターの前で撮影 Q&Aセッション終了後には懇親会を行いました。組織や評価に関心を持つ方々が集まっており、それぞれの持つ課題や今後やりたいことなどを話し合う場となっていました。会社の規模やカルチャーによって組織制度が異なるため、相互のコミュニケーションにより新しいヒントを得ることができたかと思います。今後もさまざまなイベントを開催していきますのでぜひご参加ください! ZOZOでは一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは。DevRelブロックの @wiroha です。9月25日に After DroidKaigi 2023 を開催しました。9月14日〜16日に開催されたDroidKaigi 2023の協賛企業である株式会社ZOZO、ヤフー株式会社、LINE株式会社の3社合同での振り返りイベントです。オフラインとオンラインのハイブリッドで実施しました。 登壇内容まとめ 3社の社員による発表と、パネルディスカッションを行いました! コンテンツ 登壇者 ZOZOTOWNアプリでのJetpack Compose取り組み事例 内山 雅由 / 株式会社ZOZO Modifier.composedがプロダクトに与えている影響「Modifier.Nodeを使いましょう」を踏まえて 長濱 伶 / ヤフー株式会社 Code Review Challenge An example of a solution DroidKaigi 2023: コードレビューチャレンジの問題解説 安藤 祐貴 / LINE株式会社 パネルディスカッション 高田 真壽, 森 洋之, 玉木 英嗣 当日の発表はYouTubeのアーカイブでご覧ください。 ZOZOTOWNアプリでのJetpack Compose取り組み事例 内山 雅由 / 株式会社ZOZO speakerdeck.com 弊社ZOZOの内山からZOZOTOWNアプリのプロダクトコードにJetpack Composeを導入した事例を紹介しました。リニューアル前後でコードメトリクスの計測をしており、比較可能にしているのは良い取り組みだと感じました。 Modifier.composedがプロダクトに与えている影響「Modifier.Nodeを使いましょう」を踏まえて 長濱 伶 / ヤフー株式会社 speakerdeck.com 長濱さまからは、DroidKaigi 2023で発表されたセッション「 Modifier.Nodeを使いましょう 」を踏まえて、Recompositionについての詳細を解説しました。参考にされた「Modifier.Nodeを使いましょう」の発表動画はDroidKaigiの公式YouTubeで公開されています。ぜひ見てみてください! www.youtube.com Code Review Challenge An example of a solution DroidKaigi 2023: コードレビューチャレンジの問題解説 安藤 祐貴 / LINE株式会社 speakerdeck.com 安藤さまからはDroidKaigi 2023のブースで出題したコードレビューチャレンジの問題解説を行いました。例外のハンドリング漏れや複雑な関数など、アンチパターンがよくわかる発表でした。 パネルディスカッション パネルディスカッションの様子 パネルディスカッションでは今年最もホットだったトピックスは何だったか、おもしろかったセッション、やってみたいと思った事例などをそれぞれ語り合いました。Jetpack Composeに注目している方が多く、やってみたいといった話も多く出ました。 セッションについてだけではなく、各社ブースでどんな企画をしたかも紹介しました。今回のLTでも紹介されていたCode Review Challengeや、アプリの体験、クイズ、アンケートなど各社の個性が出ていました。 最後に オフライン会場ではノベルティの配布も実施しました パネルディスカッションの後、オフライン会場では交流会を実施しました。各社のノベルティを配布するお楽しみスペースを設け、DroidKaigi当日にもらって嬉しかったものなどの話が盛り上がりました。みなさまご参加ありがとうございました! ZOZOでは一緒にサービスを作り上げてくれるAndroidエンジニアを募集中です。ご興味のある方は以下のリンクからぜひご応募ください。 hrmos.co
はじめに こんにちは、ML・データ部MLOpsブロックの 岡本 です。 MLOpsブロックでは日々複数の Google Cloudプロジェクト を管理しています。これらのプロジェクトでは、データサイエンティストやプロジェクトマネージャーなど別チームのメンバーが作業することもあり、必要に応じてメンバーの Googleアカウント へ権限を付与しています。 権限の付与はプロジェクトの管理者であるMLOpsブロックメンバーが行いますが、これは頻繁に発生する作業でありトイルとなっていました。 また権限付与後はこれらを継続的に管理し、定期的に棚卸しすることで不要になった権限を削除する必要があります。しかし当初の運用だと権限の棚卸しの対応コストが大きく、これが実施されずに不要な権限が残り続けるという課題もありました。 本記事ではMLOpsブロックで抱えていたGoogle Cloudプロジェクト内での権限管理における課題と、解決に至るまでの取り組みについて、実際の対応手順と運用後の所感を交えてご紹介します。手順の中には一部地道な手作業もありますが、組織のクラウド利用における権限管理に関する事例として参考にしていただければ幸いです。 目次 はじめに 目次 背景・課題 背景 課題と解決方針 既存権限のTerraform管理への移行 tfファイルの作成 既存IAMロールの一覧化 継続的な権限管理の方針とdriftctlによる省力化 継続的な権限管理の方針 driftctlの導入 driftctlの定期実行と自動化 権限付与・変更依頼の運用フローの見直し 運用後の所感 終わりに 背景・課題 背景 前提として、まずはZOZOにおける全社的な Google Cloud 利用の管理体制についてご説明します。 ZOZOでは多くのチームでGoogle Cloudを使用しており、これらの全社的な管理はワーキンググループという形でいくつかのチームから有志のメンバーが集まって行なっています。以下ではこちらのワーキンググループをGCP Adminと呼びます。 Google Cloudのリソース階層の中にはプロジェクトの上位リソースとしてフォルダ、組織という2つの階層が存在し、GCP Adminは組織レベルの権限を持っています。またGCP Adminでは組織配下にそれぞれのチームごとのフォルダを作成しています。プロジェクトの作成はGCP Adminが各チームから依頼を受けて対応し、プロジェクトはプロジェクト管理者が所属するチームのフォルダ配下に配置しています。 cloud.google.com この体制を取ることで、GCP Adminでは各Google Cloudプロジェクトがどのチームにより管理されているのか把握しやすくしています。加えて、GCP Adminでは組織レベルで主にセキュリティ・コスト観点について複数項目の監視・制限を作成することでガードレールを設け、ガバナンスを効かせています。 これらの取り組みの上で、プロジェクトレベルのリソースの管理については基本的に各チームの管理者に委ねており、ある程度自由度を持ってGoogle Cloudを利用できるようにしています。 ZOZOにおける全社的なGoogle Cloud管理の詳細については、TECH BLOGの「GCPの秩序を取り戻すための試み 〜新米GCP管理者の奮闘記〜」をご参照ください。 techblog.zozo.com MLOpsブロックでは、ZOZOTOWNの推薦・検索といった案件単位でGoogle Cloudプロジェクトを作成しています。またそれぞれの案件ごとにdev(開発環境)・stg(検証環境)・qa(本番環境と類似のテスト環境)・prd(本番環境)を作成しています。これらを合計すると、2023年9月時点では約40のGoogle CloudプロジェクトがMLOpsブロックの管理対象として存在しています。 前述の通りこれらのプロジェクトでは自チームのメンバーだけでなく、データサイエンティストやプロジェクトマネージャーなど他チームのメンバーが閲覧・分析・実験などの作業でリソースを操作します。 こういった中で、MLOpsブロックメンバーは自チームのGoogle Cloudプロジェクト内に存在するリソースを把握し、主にセキュリティ・コスト観点において管理する必要があります。 課題と解決方針 MLOpsブロックでは、 Terraform を利用してほぼ全てのGoogle Cloudリソースの作成・変更をコード化し、これらは全て Git ・ GitHub で管理されています。これにより手動作業によるミスの低減やリソースの変更点の追跡容易性といった恩恵を受けています。 Terraformとは、 HashiCorp, Inc. から提供されているツールであり、インフラリソースの構築をコードで行うInfrastructure as Code(IaC)を実現できます。 またGoogle Cloudでは、アクセス制御を管理する仕組みとして Identity and Access Management(IAM) システムが提供されています。リソースにアクセスするための複数の権限は IAMロール にまとめて、Googleアカウント・ サービスアカウント などのプリンシパルに付与します。 MLOpsブロックで権限付与の依頼に対応する際は、この仕組みで付与対象のGoogleアカウントへ必要なIAMロールを付与します。 しかし歴史的な経緯によりMLOpsブロックでは、Googleアカウントに付与するIAMロールが例外としてTerraform管理されていませんでした。そのため権限付与時はMLOpsブロックメンバーがGoogle CloudコンソールからIAMロールを付与・変更する運用が取られていました。 特に他チームのメンバーへの権限付与では次の定型作業が頻繁に発生していました。 Slack でMLOpsブロック宛に権限付与の依頼が来る 必要に応じて付与するIAMロール・付与先・用途についてヒアリングする MLOpsブロックメンバーがGoogle Cloudコンソールから手動でIAMロールを付与する こちらの運用における課題は主に次の2点です。 権限付与の履歴(誰が誰に何の権限を付与したのか・誰が承認したのか)を確認しづらく、セキュリティ観点での管理コストになる 権限付与・削除のたびにMLOpsブロックメンバーの工数が発生し、運用観点での対応コストになる これらの課題を解決するために、次の対応方針を立てました。 GoogleアカウントのIAMロールをTerraform管理に移行すること 権限付与の依頼・承認・付与の一連の流れをGitHubのPull Request上で行うようにすること 既存の運用に対して見込まれる改善点は次の2点です。 Gitのログや過去のPull Requestを追跡して権限付与の一連の流れを容易に確認できるため、管理コストが削減される MLOpsブロックメンバーの作業はPull RequestのレビューとApprove・Mergeのみになるため、対応コストが削減される MLOpsブロックで利用するTerraformのコードの変更はGitHubのPull Requestを通して行われ、Approve・MergeはMLOpsブロックメンバーのみが行えます。 GoogleアカウントのIAMロールについてもTerraformのコードとして定義することでGit・GitHubにより変更履歴を管理し、権限付与の一連の流れを容易に追跡可能にします。 またTerraformによるリソースの作成はCIにより自動化しています。IAMロールの変更反映時にMLOpsブロックメンバーの手動作業は発生せず、対応コストはPull RequestのレビューとApprove・Mergeのみと軽微なものになります。 次節では上記の方針をもとに具体的に行なった対応内容とつまづいたポイント、最後にしばらく運用を行なった所感をご説明します。 既存権限のTerraform管理への移行 既存権限をTerraform管理へ移行するにあたり、まずはプロジェクトに存在するIAMロールをTerraformのコードで定義し直しました。 MLOpsブロックでは、Googleアカウントに対して基本的に プロジェクトレベルのIAMロール を付与しています。今回はこれらを対象に作業しました。 次に作業手順の詳細についてご説明します。 tfファイルの作成 GoogleアカウントのIAMロールをTerraformのコードで定義するために、まずテンプレートファイル(.tf)を作成します。 ファイル構成については特に次の2点を意識しました。 付与対象のGoogleアカウントをIAMロールごとに一覧できる 利用用途(主にチーム単位)ごとにGoogleアカウントをまとめ、IAMロールの変更漏れを防止する 中心的なファイルは、IAMロールのリソースを定義する iam.tf 、対象のGoogleアカウントを配列にまとめてIAMロールと紐付ける role-binding.tf です。補助として利用用途でGoogleアカウントを配列にまとめているのが members.tf です。 ファイル構成は次の通りです。 . ├── iam.tf ├── members.tf └── role-binding.tf iam.tf は次のように記述します。説明のためプロバイダの指定は省いています。 resource " google_project_iam_member " " viewer " { project = " example-project " for_each = toset ( local.project_viewer_users ) role = " roles/viewer " member = each.value } resource " google_project_iam_member " " bigquery_jobuser " { project = " example-project " for_each = toset ( local.bigquery_jobuser_users ) role = " roles/bigquery.jobUser " member = each.value } role-binding.tf は次のように記述します。 locals { project_viewer_users = concat ( local.analysis_members, local.project_management_members, [ " user:user1@gmail.com ", ] ) bigquery_jobuser_users = local.analysis_members, } members.tf は次のように記述します。 locals { analysis_members = [ " user:analysis-member1@gmail.com ", " user:analysis-member2@gmail.com ", " user:analysis-member3@gmail.com ", ] project_management_members = [ " user:pm-member1@gmail.com ", " user:pm-member2@gmail.com ", ] } 上記のファイル構成をとることで次のメリットがあります。 例として、分析チームの新メンバーへ分析作業に必要な権限を付与する場合を考えます。この際 members.tf のanalysis_members配列に新メンバーのGoogleアカウントを追加することで、必要なIAMロールを一括で付与できます。 特に新メンバーが複数人いる場合や複数のIAMロールを付与する場合、コンソールでの手動作業はIAMロールの付与漏れが発生しやすくなります。 members.tf でGoogleアカウントをグループ化することで作業回数が減り、このようなケースでの作業漏れを低減できます。 また、 role-binding.tf ではIAMロールごとに付与の対象であるGoogleアカウントがグループ化されています。そのためMLOpsブロックメンバーは role-binding.tf を見ることで誰にどのIAMロールが付与されているのか容易に確認できます。 一方、こちらの構成では付与するIAMロールごとに iam.tf ・ role-binding.tf の記述が増えるという懸念があります。 しかしMLOpsブロック管理のプロジェクトでGoogleアカウントに付与するIAMロールは、 roles/viewer ・ roles/editor など基本のロールがほとんどです。その他のIAMロールについては必要に応じてアドホックに付与することが多く、量としては少ないため iam.tf ・ role-binding.tf の記述量は現状特に問題になっていません。 次節では、上記のテンプレートファイルを元に既存権限をTerraform管理化するため、プロジェクト内のIAMロールを一覧化する手順をご説明します。 既存IAMロールの一覧化 プロジェクト内のIAMロールをTerraformのコードで定義し直すには、MLOpsブロックが管理する約40のGoogle Cloudプロジェクトで、既存のIAMロールを一覧化する必要がありました。 プロジェクトのIAMロール一覧を取得するために gcloud CLI でCloud Asset Inventoryの search-all-iam-policies コマンド を利用しました。Cloud Asset Inventoryを用いるとプロジェクト・フォルダ・組織内のIAMポリシーを検索できます。 Cloud Asset Inventoryの詳細については次の公式ドキュメントを参照してください。 cloud.google.com コマンド実行時のscope引数に folder_number を指定することで、特定のフォルダ配下のプロジェクトにあるIAMロールの一覧を取得できます。 folder_number = 999999999 gcloud asset search-all-iam-policies --scope= " folders/ $folder_number " --query= " memberTypes:user " --asset-types= " cloudresourcemanager.googleapis.com/Project " --format= " json(resource,policy) " > iam_policies.json 上記コマンドを実行することで次の出力結果が得られます。 [ { " policy ": { " bindings ": [ { " members ": [ " user:user1@gmail.com ", " user:user2@gmail.com ", " serviceAccount:example@example-project.iam.gserviceaccount.com ", " group:example-group@gmail.com " ] , " role ": " roles/viewer " } , ] }, " resource ": " //cloudresourcemanager.googleapis.com/projects/example-project " }, { " policy ": { " bindings ": [ { " members ": [ " user:user3@gmail.com " ] , " role ": " roles/editor " } , ] }, " resource ": " //cloudresourcemanager.googleapis.com/projects/example-project-2 " } ] gcloudコマンドの出力では、 members フィールドにサービスアカウント・ Googleグループ が含まれます。フォルダ配下の全てのプロジェクトのIAMロールは出力内で配列の要素として数千行に渡り一覧化されています。 コマンドの出力をより見やすくするため、次のPythonスクリプトを作成しました。 サービスアカウントのIAMロールは既にTerraform管理されているためフィルタリングします。またIAMロールごとに付与対象のGoogleアカウントとGoogleグループをグループ化します。その後でプロジェクトごとにjsonファイルを作成して出力を分割しました。 import json import typer from pathlib import Path from typing import Optional def format_json (path: Optional[Path] = typer.Option( None )): output_dir_name = "outputs" Path(output_dir_name).mkdir(exist_ok= True ) with open (path, encoding= "utf-8" ) as f: folder_iam_list = json.load(f) for project_iam in folder_iam_list: project_id = project_iam[ "resource" ].split( '/' )[- 1 ] members_of_role = {} for role_binding in project_iam[ "policy" ][ "bindings" ]: role = role_binding[ "role" ] for member in role_binding[ "members" ]: if not (member.startswith( "user:" ) or member.startswith( "group:" )): continue members_of_role.setdefault(role, []) members_of_role[role].append(member) with open (f '{output_dir_name}/{project_id}.json' , encoding= "utf-8" , mode= "w" ) as f: json.dump(members_of_role, f) if __name__ == "__main__" : typer.run(format_json) 次のシェルスクリプトは上記のgcloudコマンドおよびPythonスクリプトを一度に実行します。 ./run.sh 999999999 のように対象の folder_number を指定してスクリプトを実行すると、 outputs ディレクトリ内にファイルが作成されます。これらはプロジェクトIDをファイル名として持ち、 folder_number で指定したGoogle Cloudフォルダ内のプロジェクトごとに example-project.json の形式で作成されます。それぞれのファイルにはプロジェクト内のIAMロールの一覧が出力されます。 #!/bin/bash set -eu folder_number = $1 gcloud asset search-all-iam-policies --scope= " folders/ $folder_number " --query= " memberTypes:user " --asset-types= " cloudresourcemanager.googleapis.com/Project " --format= " json(resource,policy) " > iam_policies.json poetry run python main.py --path iam_policies.json rm iam_policies.json outputs/example-project.json の出力例は次の通りです。IAMロールごとにGoogleアカウント・Googleグループを一覧化した結果を得られています。 { " roles/viewer ": [ " user:user1@gmail.com ", " user:user2@gmail.com ", " group:example-group@gmail.com " ] } 次にプロジェクトごとに生成されたjsonファイルを元に、既存のIAMロールをTerraformのコードとして定義し直しました。こちらの転記は地道に手作業で進めました。手作業による移行漏れのリスクについては、後述する差分検知の仕組みによりカバーが可能なため、ここでは問題としていません。 また、 role-binding.tf で共通の権限を持つGoogleアカウントについてはTerraformの配列としてまとめて members.tf に定義しています。 role-binding.tf では members.tf にまとめた配列を対象にIAMロールを紐付けました。 本節の手順によりプロジェクトのIAMロールをTerraformのコードとして定義し直し、既存の権限をTerraform管理下に置くことができました。 次節では、これらを継続的に管理するための方針と、ツールを導入することによる管理コストの省力化についてご説明します。 継続的な権限管理の方針とdriftctlによる省力化 継続的な権限管理の方針 上記の作業により、一時的にプロジェクト内のIAMロールがTerraform管理できている状態を作れましたが、それだけでは継続的にこの状態を維持できません。 IAMロールを変更できるロールを持つGoogleアカウントは、GitHubのPull Request上での権限付与の一連の流れを無視して、コンソール・CLIで既存のIAMロールを変更できてしまいます。 こうなるとTerraformのコードで管理されているIAMロールと実際にプロジェクト内に存在するIAMロールの間に差分が生じます。これでは権限付与の履歴を確認できない当初の課題が再発しています。 これはプロジェクトの権限管理の運用ルールとして、IAMロールを変更できるロールはCIのサービスアカウントのみに付与し、Googleアカウントへ付与しないことで一応回避できます。しかしこれでは緊急時にすぐ権限を付与できず、障害対応に支障が出ます。 MLOpsブロック管理のプロジェクトではセキュリティ・事故防止の観点からGoogleアカウントに対しdev環境を除いてはリソースの変更が可能なIAMロールを付与していません。特に本番環境では、プロジェクトの管理者である1人以外には基本的に閲覧以外のIAMロールを付与していません。 しかし本番稼働するAPI・バッチを運用・保守するMLOpsブロックメンバーについては、障害発生時に緊急対応のため手動でリソースを変更したいケースが発生し得ます。 このように緊急度の高いケースにおいては、Pull Requestを作成しての権限付与フローでは障害対応が遅れ、致命的な損失につながる可能性があります。これを避けるために、MLOpsブロックメンバーには本番環境においてプロジェクトのIAMロールを変更できるロールを例外的に付与しています。 そこでMLOpsブロックでは管理方針として、Terraform管理されているIAMロールとプロジェクト内に存在するIAMロールの差分を定期的に確認し、適宜修正するようにしました。 これにより全てのGoogleアカウントのIAMロールがTerraform管理された状態を継続的に維持できます。 この方針では定期的に差分の確認作業が発生しますが、ツールにより差分の確認を自動化することでMLOpsブロックメンバーの対応コストを最小限に抑えました。 次節ではこちらのツールと自動化による差分検知の仕組みについてご説明します。 driftctlの導入 Terraform管理されたIAMロールと、実際にプロジェクト内に存在するIAMロールの差分を確認するため、 driftctl というツールを採用しました。driftctlはApache-2.0 licenseで利用できますが、現在Beta版での提供となっている点にご注意ください。 driftctlは Snyk Ltd. により開発されたGo製のCLIツールで、IaCのコードが実際に存在するリソースをどの程度カバーできているか測定できます。またdriftctlではコンソール・CLIなど、Terraform以外の方法でGoogleアカウントに対してIAMロールが付与された場合の差分も検知できます。 Terraform管理されたリソースのみの差分検知であれば terraform plan コマンドで引数に -detailed-exitcode を指定し、コマンド終了時のexit codeにより差分の有無を判別することで対応できます。 一方でこの方法は、Terraform以外の方法でGoogleアカウントに対してIAMロールが付与された場合の差分は検知できません。 こちらの差分も検知できるという点で、driftctlはTerraform管理されているIAMロールとプロジェクト内に存在するIAMロールを完全一致させたいという今回の要件にマッチしました。 次にdriftctlの使い方を簡単にご説明します。 CLIは次のようにcurlやbrewでインストールできます。 # Linux # x64 $ curl -L https://github.com/snyk/driftctl/releases/latest/download/driftctl_linux_amd64 -o driftctl # macOS $ curl -L https://github.com/snyk/driftctl/releases/latest/download/driftctl_darwin_amd64 -o driftctl $ brew install driftctl # Windows # x64 $ curl https://github.com/snyk/driftctl/releases/latest/download/driftctl_windows_amd64.exe -o driftctl.exe Terraformと実際のリソースの差分は driftctl scan コマンドで確認でき、必要な環境変数を渡して実行することで差分を出力できます。 コマンドの詳細については次の公式ドキュメントを参照してください。 docs.driftctl.com Google Cloudを対象にする場合は Cloud Asset API の有効化と認証するアカウントに対してプロジェクトの roles/cloudasset.viewer ・ roles/viewer のIAMロールの付与が必要です。 Google Cloudでの認証の詳細については次の公式ドキュメントを参照してください。 docs.driftctl.com driftctl scan の実行時に参照するTerraformのStateは --from 引数で指定できます。特に指定しない場合はカレントディレクトリ配下のHCLを自動的に読み取り、使用するtfstateファイルを探します。 driftctl scan の実行と出力結果の例は次の通りです。 GOOGLE_APPLICATION_CREDENTIALS =your-creds.json \ CLOUDSDK_CORE_PROJECT =example-project \ driftctl scan --to gcp+tf { " options " : { " deep " : false , " only_managed " : false , " only_unmanaged " : false } , " summary " : { " total_resources " : 6 , " total_changed " : 0 , " total_unmanaged " : 0 , " total_missing " : 0 , " total_managed " : 6 , " total_iac_source_count " : 1 } , " managed " : [ { " id " : " example-project/roles/viewer/user:user1@gmail.com " , " type " : " google_project_iam_member " , " source " : { " source " : " tfstate+gs://bucket/terraform/terraform.tfstate " , " namespace " : "" , " internal_name " : " resourcemanager_projectiamadmin " } } , { " id " : " example-project/roles/bigquery.jobUser/user:analysis-member1@gmail.com " , " type " : " google_project_iam_member " , " source " : { " source " : " tfstate+gs://bucket/terraform/terraform.tfstate " , " namespace " : "" , " internal_name " : " bigquery_jobuser " } } , ... 省略 ] , " unmanaged " : null, " missing " : null, " differences " : null, " coverage " : 100 , " alerts " : null, " provider_name " : " gcp+tf " , " provider_version " : " 4.80.0 " , " scan_duration " : 1 , " date " : " 2023-09-14T00:00:00.000000+00:00 " } またdriftctlではコマンド実行時に --filter 引数を指定して、差分として検知する対象の絞り込み・除外ができます。 引数なしの場合、プロジェクト内のすべてのGoogle Cloudリソースを対象にTerraform管理されたリソースとの差分を出力します。今回差分を出力したいGoogle CloudリソースはプロジェクトのIAMロールのみです。そのため対象をプロジェクトのIAMロールに絞り込みました。加えてプロジェクトに存在するIAMロールにはサービスアカウントのロールも含まれるため、合わせて検知対象から除外しました。 上記の絞り込み・除外を考慮した driftctl scan の実行コマンドは次の通りです。 GOOGLE_APPLICATION_CREDENTIALS =your-creds.json \ CLOUDSDK_CORE_PROJECT =example-project \ driftctl scan --to gcp+tf --filter $' (Type== \' google_project_iam_member \' && contains(Id, \' user: \' )) ' 上記により、Googleアカウントに付与されたIAMロールを対象にTerraform管理されたリソースと実際のリソースの差分を出力できます。 しかし上記のコマンドではGoogle CloudのAPI呼び出しのRate Limitにより、 driftctl scan の実行時に次のエラーが発生しました。 rpc error: code = ResourceExhausted desc = Resource has been exhausted ( e.g. check quota ) . こちらについては 同様のエラーについてのIssue が報告されており、 コメントで提案されていた方法 を参考に対応しました。 Rate Limitに引っかかった原因はGoogle Cloud上のリソース数が多く、リソース取得のためにGoogle CloudのAPIを呼び出す回数が多くなっていることでした。これは driftctl scan で取得する対象のリソースを絞り込むことで解決できました。 この絞り込みには .driftignore を利用しました。 .driftignore に除外したいリソースを記述することで、 driftctl scan の取得対象から除外できます。前述の --filter 引数での除外は複雑な除外の用途で使用し、単に一連のリソースを除外するのみであれば .driftignore を使用することを公式ドキュメントでは推奨しています。 docs.driftctl.com 次の記述を .driftignore に追加し、 google_project_iam_member 以外のリソースを driftctl scan の対象から除外しました。 * はワイルドカードでの指定となるため、すべてのリソースを対象から除外し、その上で google_project_iam_member を除外対象から外すという記述になります。 # Ignore all drifts except for google_project_iam_member * !google_project_iam_member これにより、必要のないリソースが driftctl scan の取得対象となることを回避し、上述のRate Limitによるエラーを解消できました。また driftctl scan の実行時間も大幅に短縮されました。 次節ではdriftctlコマンドをCIで自動実行する手順についてご説明します。 driftctlの定期実行と自動化 MLOpsブロックではCIとして GitHub Actions を利用しています。 差分の確認を定期実行するため、GitHub Actionsの Scheduleトリガーイベント によりdriftctlコマンドを定期実行するワークフローを作成しました。このワークフローはTerraform管理されたリソースと実際のリソースに差分を確認するとSlackに通知します。 ワークフローを作成した後で気がつきましたが、GitHub Actionsでのdriftctlの実行については開発元であるSnyk Ltd.から driftctl-action が提供されています。詳細については次の公式ドキュメントを参照ください。 docs.driftctl.com ワークフローの大まかな流れは次の通りです。 GitHub Actionsで利用するGoogle Cloudのサービスアカウントを認証して、 driftctl scan 実行時に必要な認証情報を環境変数にセットする driftctlをインストールし、 driftctl scan コマンドにより差分を確認して結果を出力する 出力された結果の coverage の値を確認し、差分が見つかった場合は exit 1 で終了する 差分が見つかった場合のみSlackで通知する 上記の流れの2・3(driftctlのインストールから差分の確認まで)の実装は、driftctl-actionで代替できます。 以下では独自実装のワークフローについてご説明します。 ワークフローの定義は次の通りです。 name : check_user_account_project_iam_member_role_drift on : schedule : - cron : '0 1 * * 4' permissions : contents : 'read' id-token : 'write' jobs : check-user-account-project-iam-member-role-drift : runs-on : ubuntu-20.04 strategy : fail-fast : false matrix : cfg : - DIR : terraform_dir PROJECT_NUMBER : 202020202020 defaults : run : working-directory : ${{ matrix.cfg.DIR }} outputs : coverage : ${{ steps.run_driftctl.outputs.coverage }} steps : - uses : actions/checkout@v3.5.2 - name : authenticate to gcp uses : 'google-github-actions/auth@v1.1.1' with : workload_identity_provider : projects/${{ matrix.cfg.PROJECT_NUMBER }}/locations/global/workloadIdentityPools/example-pool/providers/github-actions service_account : ci-service-account@example-project.iam.gserviceaccount.com create_credentials_file : true export_environment_variables : true - name : setup driftctl run : | curl -L https://github.com/snyk/driftctl/releases/latest/download/driftctl_linux_amd64 -o driftctl chmod +x driftctl mv driftctl /usr/local/bin/ - name : run driftctl id : run_driftctl run : | output=$(driftctl scan --quiet --to gcp+tf --filter $'(Type==\'google_project_iam_member\' && contains(Id, \'user:\'))' --output json://stdout | jq 'del(.managed)' ) echo "coverage=$(echo " $output" | jq .coverage)" >> $GITHUB_OUTPUT echo $output | jq . - name : check coverage if : ${{ steps.run_driftctl.outputs.coverage != 100 }} run : | echo "The driftctl result coverage value is not 100. Please check run driftctl step output." && exit 1 slack-notice : needs : [ check-user-account-project-iam-member-role-drift ] runs-on : ubuntu-20.04 if : ${{ failure() }} steps : - uses : slackapi/slack-github-action@v1 with : payload : | { "blocks" : [ { "type" : "section" , "text" : { "type" : "mrkdwn" , "text" : "<!channel>" } } , { "type" : "section" , "text" : { "type" : "mrkdwn" , "text" : ":rotating_light: ユーザーアカウントのロールとterraformの構成に差分があります:rotating_light: \n 以下のURLからrun driftctlステップの差分を確認し、差分の解消を行って下さい。 \n\n *GitHub Actions URL*: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} \n\n *差分検知後の対応フロー*: 対応ドキュメントのリンク" } } ] } env : SLACK_WEBHOOK_URL : ${{ secrets.SLACK_WEBHOOK_URL_NOTICE }} SLACK_WEBHOOK_TYPE : INCOMING_WEBHOOK 続いてワークフローに含まれるJobごとの処理の流れをご説明します。 初めに check-user-account-project-iam-member-role-drift Jobについてご説明します。 認証処理の記述は次の箇所です。 まず、 authenticate to gcp StepではCIで利用するGoogle Cloudのサービスアカウントの認証を行なっています。 MLOpsブロックでは、サービスアカウントの権限を利用するすべてのリポジトリで認証に Workload Identity連携 を利用しています。ここでは google-github-actions/auth を利用しています。 前述の通り、 driftctl scan の実行時には GOOGLE_APPLICATION_CREDENTIALS (認証情報)と CLOUDSDK_CORE_PROJECT (プロジェクト名)を指定する必要があります。認証時の引数に create_credentials_file: true と export_environment_variables: true を指定し、環境変数として参照できるようにしています。 - name : authenticate to gcp uses : 'google-github-actions/auth@v1.1.1' with : create_credentials_file : true workload_identity_provider : projects/${{ matrix.cfg.PROJECT_NUMBER }}/locations/global/workloadIdentityPools/example-pool/providers/github-actions service_account : ci-service-account@example-project-${{ matrix.cfg.ENV }}.iam.gserviceaccount.com export_environment_variables : true driftctlコマンドの実行処理の記述は次の箇所です。 setup driftctl Stepでは前述した手順に従ってdriftctl CLIをインストールして実行権限を付与しています。続く run driftctl Stepでは実際に driftctl scan を実行しています。 ここでJobにデフォルトで指定している working-directory には各IAMロールを定義したtfファイルが配置されているディレクトリを指定します。指定したディレクトリ配下には取得対象から除外するリソースを記載した .driftignore ファイルを置いています。 また --quiet 引数や jq によって出力結果を整形することで、差分検知のアラートがなった際にノイズとなる情報を取り除いています。 次に check coverage Stepで driftctl scan の出力結果を確認しています。 出力のうち coverage キーの値にはTerraform管理されたリソースが実際のリソースをどの程度カバーできているかを測定した値が入っています。こちらの値が100であれば、実際に存在するリソースがすべてTerraform管理された状態です。 if: ${{ steps.run_driftctl.outputs.coverage != 100}} により値を確認し、差分があった場合はログを出力して exit 1 で終了しています。 - name : setup driftctl run : | curl -L https://github.com/snyk/driftctl/releases/latest/download/driftctl_linux_amd64 -o driftctl chmod +x driftctl mv driftctl /usr/local/bin/ - name : run driftctl id : run_driftctl run : | output=$(driftctl scan --quiet --to gcp+tf --filter $'(Type==\'google_project_iam_member\' && contains(Id, \'user:\'))' --output json://stdout | jq 'del(.managed)' ) echo "coverage=$(echo " $output" | jq .coverage)" >> $GITHUB_OUTPUT echo $output | jq . - name : check coverage if : ${{ steps.run_driftctl.outputs.coverage != 100 }} run : | echo "The driftctl result coverage value is not 100. Please check run driftctl step output." && exit 1 通知処理の記述は次の箇所です。 slack-notice Jobでは差分が検知された場合にのみSlack通知を送ります。差分があると前段の check coverage Stepは失敗します。この時 slack-notice Jobでは if: ${{ failure() }} を指定しているためJobがトリガされ、Slack通知処理が走ります。Slack通知処理には slackapi/slack-github-action を利用しています。 また送信するメッセージにはGitHub ActionsのRunのURL及び、差分解消の対応フローを記述したドキュメントのリンクを含めています。これによりMLOpsブロックメンバーであれば誰でも差分解消の作業ができるようにしています。 slack-notice : needs : [ check-user-account-project-iam-member-role-drift ] runs-on : ubuntu-20.04 if : ${{ failure() }} steps : - uses : slackapi/slack-github-action@v1 with : payload : | { "blocks" : [ { "type" : "section" , "text" : { "type" : "mrkdwn" , "text" : "<!channel>" } } , { "type" : "section" , "text" : { "type" : "mrkdwn" , "text" : ":rotating_light: ユーザーアカウントのロールとterraformの構成に差分があります:rotating_light: \n 以下のURLからrun driftctlステップの差分を確認し、差分の解消を行って下さい。 \n\n *GitHub Actions URL*: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} \n\n *差分検知後の対応フロー*: 対応ドキュメントのリンク" } } ] } env : SLACK_WEBHOOK_URL : ${{ secrets.SLACK_WEBHOOK_URL_NOTICE }} SLACK_WEBHOOK_TYPE : INCOMING_WEBHOOK 実際に差分が検知された場合のSlack通知は次の通りです。 通知があった場合は、メッセージに記述されたGitHub ActionsのRunのリンクに飛ぶことで検知された差分の内容を確認できます。 本節ではTerraform管理されているIAMロールと実際にプロジェクト内に存在するIAMロールについて、自動で差分を検知する仕組みを作成しました。これによりMLOpsメンバーの対応工数を抑えつつ、プロジェクト内に存在するGoogleアカウントのIAMロールをすべてTerraform管理できている状態を維持できるようになりました。 次節では、システム面以外での運用フローの改善についてご説明します。 権限付与・変更依頼の運用フローの見直し 既存の運用フローについて、Slackメッセージベースの依頼のフローから、GitHubのPull Requestベースでの依頼フローへの見直しについてご紹介します。 既存の運用フローでは権限の付与・変更時に、MLOpsブロックメンバーが主体となって作業する部分がほとんどでした。一方でGitHubのPull Requestベースのフローでは主な作業は依頼者が行なうようにしています。 依頼者は権限付与・変更のPull Requestを作成し、レビュアにMLOpsブロックの Team をアサインします。MLOpsブロックメンバーはPull Requestをレビューし、問題なければApprove・Mergeします。Terraformによるリソースの作成は元々CIにより自動化されているため、MLOpsブロックメンバーの作業は基本的にPull RequestのレビューとMergeのみになります。 実際の権限付与の依頼例は次の通りです。 権限付与が必要な背景はPull RequestのDescriptionに記載されており、MLOpsブロックメンバーはGit・GitHubの履歴を辿ることで、権限付与の経緯などを後でも確認できます。 一方で、GitHubのPull Requestベースのフローには次の2つの懸念点もあります。 Terraform経験が少ない依頼者にTerraformコードの記述を強制する必要がある 依頼者の対応工数が増える 1つ目について、権限付与の依頼者はほとんどがエンジニアですが、データサイエンティストなど普段Terraformを使わない方も多いです。そういった方に権限付与の依頼のためにわざわざTerraformの文法の履修を強制することは総工数を増やすだけでなく、依頼のハードルを上げることにもなります。 こちらについてはドキュメントを充実させ、Terraformの知識がない方でもコピー&ペーストで作業ができるようにすることで負荷を軽減しました。ドキュメントには権限の付与・変更・削除など想定される依頼パターンごとのPull Request作成例を記載するだけでなく、プロジェクトごとにリソースを定義したリポジトリの対応表など作業で必要な情報をまとめています。 これによりプログラミング経験、Gitの利用経験があればどなたでも作業可能な状態を作っています。 2つ目は、依頼者にとって権限付与の依頼は頻繁に発生する作業ではない点・基本コピー&ペーストで済む作業であれば対応工数も少なく済む点から許容できると判断しました。 最終的に、GitHubのPull Requestベースの新しい運用フローについて手順書を作成し、作成した手順書と運用フローの変更について関係者に周知しました。既存の運用では依頼時に利用する固定のSlackチャンネルはなかったため、MLOpsブロックのチャンネルで関連チーム向けにアナウンスし、同チャンネルにメッセージをピン留めする方法を取りました。 MLOpsブロックで抱えていたGoogle Cloudプロジェクト内でのGoogleアカウントの権限管理における課題について、解決のために行なった取り組みはこれで全てとなります。 運用後の所感 MLOpsブロックが管理するプロジェクト内での権限管理における課題解決、継続的な権限管理の仕組み導入と依頼時の運用フロー見直しについて、実際に約4か月運用した現在の所感をお話しします。 まずGoogleアカウントへのIAMロールの付与・変更をTerraform管理に移行した点では、権限の一覧性向上・コード上でGoogleアカウントをグループ化できる点で非常に管理しやすくなりました。特に後者については、Googleアカウントをチームごとに配列でまとめることで、チームメンバーが増えた際の権限付与や退職者の権限削除にかかる工数の削減と対応漏れの防止につながるというメリットがありました。 余談ですが、Googleアカウントをグループ化してIAMロールを付与することはGoogleグループでも対応可能です。ただしこの方法のデメリットとして、ユーザーは独自にグループへのメンバー追加ができるという点があります。これはMLOpsブロックメンバーが把握できないところで権限が付与されてしまうといった問題や、誰にどの権限が付与されているのかが分かりにくいといった権限管理上の問題を引き起こします。 こういった理由から、ZOZOのGoogle Cloud利用においてはGoogleグループへのIAMロールの付与を廃止し、個別のGoogleアカウントへの付与に移行しました。また、Googleグループに対するIAMロールの付与があればGCP Adminで検知できる監視の仕組みを導入しています。 次に、driftctlによる差分検知の導入についてです。dev・stg環境での軽微な作業の際にコンソールから一時的に付与したIAMロールの消し忘れなど、普段の運用の中での見落としを防ぐことにも非常に役立っています。また、差分検知の仕組みの運用・保守のコストは、Rate Limitエラーが発生した以外にほとんど発生していません。ワークフロー自体も非常にシンプルであるため、運用・保守のコストに対してチームのパフォーマンス改善に大きく貢献できています。 また、権限付与の運用フローを見直した点では、依頼者が権限付与のPull Requestを作成する運用にしたことで、MLOpsブロックメンバーの対応コストは大幅に低減されたと感じています。それ以外にも次のメリットを感じています。 変更がCIにより反映されるようになったことで、権限の付与など対応漏れの防止になる 依頼を見落としている場合に、GitHubのPull Requestレビューのリマインダーにより気がつける 一方で運用後に残っている課題として、権限付与の新しい運用フローが依頼者に浸透しきっていないという点があります。上述のように利用者への周知は行いましたが、たびたびSlackメッセージでの依頼は届いており、その都度MLOpsブロックメンバーがアナウンス時のメッセージのリンクを共有して再度周知しています。 また依頼者がTerraformのコードを書いてPull Requestを書くハードルについても、許容はできますが課題として残っています。 今後はこういった依頼時の認知・作業負荷の削減を考えており、 Slackワークフロー を利用したPull Request作成の自動化の仕組みによりこれを実現することを検討しています。 終わりに 最後までお読みいただきありがとうございました。 本記事ではMLOpsブロックが抱えていたGoogle Cloudプロジェクトの権限管理における課題とその解決方法について、実際の対応手順と運用後の所感を交えてご説明しました。本記事での事例の紹介が皆様のお役に立てば幸いです。 最後になりますが、ZOZOでは一緒にサービスを作り上げてくれる方を募集中です。MLOpsブロックでも絶賛採用しているので、ご興味のある方は以下のリンクからぜひご応募ください。 hrmos.co corp.zozo.com
はじめに こんにちは。DevRelブロックの @wiroha です。9月20日に ZOZO Tech Talk #8 - Go を開催しました。ZOZOのエンジニアがGoを利用した開発事例を紹介する、ランチタイムのイベントです。 登壇内容まとめ 弊社から次の2名が登壇しました。 コンテンツ 登壇者 UseCaseの凝集度を高めるGoのpackage戦略 ブランドソリューション開発本部 バックエンド部 / 田村誠基 事業成長を加速させるGoのコード品質改善の取り組み ブランドソリューション開発本部 バックエンド部 / 田島太一 当日の発表はYouTubeのアーカイブでご覧ください。 www.youtube.com UseCaseの凝集度を高めるGoのpackage戦略 ブランドソリューション開発本部 バックエンド部 / 田村誠基 speakerdeck.com 田村からはショップスタッフの販売サポートツール「FAANS」における改善を発表しました。これまでの構成は依存関係が増加しているという問題や、似た命名のstructがあることで見通しにくくなっているという課題がありました。パッケージを細かく分割することでこれらの問題点が解決されたそうです。実際試して良かったところ、気になるところも紹介しました。 事業成長を加速させるGoのコード品質改善の取り組み ブランドソリューション開発本部 バックエンド部 / 田島太一 speakerdeck.com 田島からはGoのコード品質改善のために取り組んだことを5つ紹介しました。徐々に厳しくするLinter設定、スタイルガイド「Google Go Style Guide」の導入、エラーハンドリングの改善、凝集度を高める実装パターン、ボーイスカウトルールによる既存コードの継続的改善の5つです。詳細はYouTubeやスライド資料をご覧ください。 最後に 質疑応答の様子 それぞれの発表の後には質疑応答も行いました。多くのご質問ありがとうございました! 皆さまの開発のヒントになっていれば幸いです。 ZOZOではGoを活用し、一緒にサービスを作り上げてくれる仲間を募集中です。ご興味のある方は以下のリンクからぜひご応募ください。 hrmos.co