TECH PLAY

株式会社ZOZO

株式会社ZOZO の技術ブログ

987

はじめに こんにちは、 ZOZOMO店舗在庫取り置き というサービスの開発を担当している、ZOZOMO部OMOブロックの木目沢です。 2024年4月17日から19日の3日間にかけて東京ビッグサイトで 「ファッション ワールド 東京 2024 春」 が開催され、このイベントにZOZOが出展しました。 ZOZOの出展ブースでは、私が開発を担当している 店舗在庫取り置き や FAANS 、 Fulfillment by ZOZO の3サービスが展示されました。 展示にあたっては各サービスのビジネスチームのメンバーが中心となり、準備から出展までを行いました。私たち開発チームからも数名が参加し、ビジネスチームと共に出展ブースにて多くの来場者の方々とお話しさせていただきました。 この記事では、前半で 「ファッションワールド東京 2024春」 の概要とZOZOの展示ブースの様子を紹介し、後半では開発チームがビジネスチームとともに活動する理由について述べます。 目次 はじめに 目次 ファッションワールド東京とは ZOZOが出展しました 店舗在庫取り置き Fulfillment by ZOZO FAANS 開発メンバーも参加しました 開発チームがビジネスチームと共に自事(※)をする意味 おわりに ファッションワールド東京とは ファッションワールド東京 は、毎年春と秋にRX Japanが主催するアパレル向けの展示会です。最新のサステナブルファッション、アパレル、生地、素材、ファッションDXを扱う企業が出展しています。今年の春は2024年4月17日から19日の3日間で、世界25カ国、800社が出展し、22910名が来場しました。当日の様子は 公式サイト でも公開されていましたので、ご参考までに紹介しておきます。 ファッションワールド東京 2024春 ZOZOが出展しました ZOZOの出展ブースでは、 店舗在庫取り置き や FAANS 、 Fulfillment by ZOZO のZOZOMO関連のサービスが展示されました。展示ブースには各サービスをご利用いただいているブランド様、サービスに興味を持たれているブランド様にもご来場いただき、サービスについて案内いたしました。 ZOZOブースの様子 ブース内では定期的に各サービスをより良くご利用いただくためのミニセミナーを開催しました。各回とも大変好評で、多くの方々にご来場いただき、サービスのアピールをさせていただく機会となりました。 FAANS/店舗在庫取り置き セミナーの様子 以下、ブース内に設置されていた各サービスの概要を説明するパネルと、各サービスに関連するテックブログの参考記事をご案内します。 店舗在庫取り置き AWSで実践するカオスエンジニアリング 〜ZOZOMOでの取り組み〜 ZOZOMO開発チームのユニットテスト戦略とテスト駆動開発 DynamoDBによるOutboxパターンとCDCを用いたCQRSアーキテクチャの実装〜ZOZOMOでの取り組み Chatworkさんと合同でCQRS Meetup【Chatwork × ZOZO】を開催しました #cqrsmeetup Fulfillment by ZOZO AWS CDKで構築するイベント駆動型アーキテクチャの実装戦略 FBZにおけるサーバーレス監視で実施したアラート通知の最適化 マルチAZ化から学んだ無停止でインフラを変更するために考慮すべき3点 物流支援サービスを支えるAWSサーバーレスアーキテクチャ戦略 FAANS 新規サービス「FAANS」における、立ち上げからReact+TypeScriptのSPA開発を2年間運用した際に取り組んだ組織的・技術的な課題 Kubernetesネイティブなワークフローエンジンとは!FAANSでArgo Workflowsを導入した話 FAANSにおけるCloud RunからGKE Autopilotへのリプレイス事例 Storybook × MSW × Chromaticを使ったUIの影響範囲を自動検知するための取り組み 開発メンバーも参加しました 今回の展示はビジネスチームが中心となり、出展準備を進めていきました。開発チームも運営に参加し、ご来場いただいた方々にサービスの内容や必要に応じて技術的な背景なども説明いたしました。 ファッションワールド東京のような展示会に出展し、多くの企業の方々にサービスをアピールすることは良いビジネス機会となります。また、既にサービスをご利用いただいているブランド様やサービスに興味のある企業の方々に直接お話しする機会の少ない開発チームのメンバーにとっては、ご意見やご感想を直接お伺いできるチャンスでもあります。 また、今回のようにビジネスチームと開発チームと共に活動することは、プロダクトにとっても必要なことです。以下はこの点についてご紹介したいと思います。 開発チームがビジネスチームと共に自事(※)をする意味 私たち開発チームはただサービスを作って終わりではなく、常に価値のあるソフトウェアを継続的に提供し、事業を成長させていくために活動しています。そのためには、ビジネスチームと常に一緒に自事(※)をし、状況の変化に合わせて開発を進めていく必要があります。 私たちは状況の変化に合わせて開発を続けていくために「アジャイル」であることを重要視しています。この考え方をまとめた アジャイルソフトウェア開発宣言 では、以下のように紹介されています。 私たちは、ソフトウェア開発の実践 あるいは実践を手助けをする活動を通じて、 よりよい開発方法を見つけだそうとしている。 この活動を通して、私たちは以下の価値に至った。 プロセスやツールよりも個人と対話を、 包括的なドキュメントよりも動くソフトウェアを、 契約交渉よりも顧客との協調を、 計画に従うことよりも変化への対応を、 価値とする。すなわち、左記のことがらに価値があることを 認めながらも、私たちは右記のことがらにより価値をおく。 (この宣言は、この注意書きも含めた形で全文を含めることを条件に自由にコピーしてよい。) まさに今回の展示会への参加は「顧客との協調」「個人と対話」を重視した結果とも言えます。さらに、このソフトウェア開発宣言には「 アジャイルソフトウェアの12の原則 」というページがあることはご存知でしょうか。そこには、アジャイルソフトウェア開発宣言にある「価値」に対応する「12の原則」が解説されています。 そのうちの1つに以下のような原則が示されています。 ビジネス側の人と開発者は、プロジェクトを通して 日々一緒に働かなければなりません。 顧客との協調していくことや状況によって変化していくことに対応するため、常にビジネス側と共に仕事をしていく必要があるということです。私たちもこの点について日々意識して活動しており、今回の展示への参加もその一環となります。 開発メンバーも参加しました ※ ZOZOでは仕事のことを自事と呼んでいます。「スタッフ一人ひとりが他人のことも自分のこととして考える」という意味と、「仕事=仕えること」ではなく「自事=自然なこと」だという意味が込められています。 おわりに 本記事では、 「ファッションワールド東京 2024春」 への出展の紹介と、開発チームがビジネスチームと共に自事をする意味を紹介しました。 今回の展示では私自身も開発メンバーとして3日間参加し、ご来場いただいた多くの方々にサービスを案内いたしました。また、サービスをご利用いただいているブランド様ともお話させていただく機会もありました。サービスについて高い評価をいただくこともあり、立ちっぱなしの3日間は大変疲れましたが、このようなお話をいただき疲れも吹き飛びました。 今後もビジネスチームと共に価値を提供し続けていきたいと考えています。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com 最後までご覧いただきありがとうございました!
はじめに こんにちは、計測プラットフォーム開発本部SREブロックの近藤です。普段はZOZOMATやZOZOGLASS、ZOZOFITなどの計測技術に関わるシステムの開発、運用に携わっています。 計測プラットフォーム開発本部では、複数のプロダクトを運用していますが並行して新しいプロダクトも開発しています。SREチームでは増え続けるプロダクトの運用負荷に対して改善は行っていますが、さらなるプロダクトの拡張に備えてZOZOFITの開発運用を別チームへ移管することになりました。移管作業の中でAWSリソースを別チームが管理するAWSアカウントへ移行する作業が発生することになりました。本記事では移行時に遭遇した課題と、その課題の解決に至るまでの取り組みをご紹介します。 目次 はじめに 目次 背景・課題 調査 ユーザ移行Lambdaの作成 簡易ダイアグラム フローチャート ユーザ移行Lambdaの処理 IAMの設定 移行元で用意するRole 移行先のユーザ移行Lambdaの実行Role 移行後 移行後に顕在化した問題 ユーザ移行Lambdaを経由した場合、ユーザ認証が大文字と小文字を区別するcase-sensitiveな判定になってしまう ユーザ移行Lambdaを経由したサインイン時にSMSが2通届いてしまう まとめ 背景・課題 まず、ZOZOFITを移管する上でAWSのリソースを別アカウントへ移行する事を検討しました。別チームの管理となるため管理上は別アカウントへ移行するのが適切な形です。ただし、クロスアカウントでのリソース移行は制約も多いため、慎重に検討する必要があります。このためまずはクロスアカウントでのデータ移行方法と影響範囲について調査しました。 ZOZOFITのシステム構成は以下の記事で詳細を記載していますが、データの移行対象となるのは、S3、RDS、Cognitoのユーザープール、の3つでした。前提として今回の移行ではサービス停止(ダウンタイムが発生する)が許容されていました。この中で、S3はレプリケーションを事前に設定し、RDSはスナップショットを利用してそれぞれデータを移行するため、クロスアカウント固有の事情で影響が大きくなることはないと判断しました。一方で、Cognitoのユーザプール移行に関しては未知の部分だったので調査から始まりました。 調査 Cognitoのユーザープールの移行方法について調査した結果、 AWS公式ドキュメント から、以下の2つの方法があるとわかりました。どちらの方法もクロスアカウント固有の制約はなく、この2つの移行方法についてそれぞれのPros/Consを整理して比較しました。なお、どちらの方法でもセッション情報が引き継がれず、ユーザーがサインアウト状態になる影響も判明しました。この点に関しては共通事項のため比較要素としては記載していません。 1 CSVファイルからユーザプールへのインポート 2 ユーザ移行Lambdaを利用した、イベントドリブンのユーザ移行 移行方法 Pros Cons CSVファイルからユーザプールへのインポート すべてのユーザの移行が一括で行える パスワードリセットが全ユーザに強制される ユーザ移行Lambdaを利用した、イベントドリブンのユーザ移行 サインインまたはパスワードをリセットすることで、データの移行が完了する ユーザがサインインまたはパスワードリセットを行った際にしか移行が行われないため、移行に長期間を要する Pros/Consを比較した結果、ユーザ影響の少ないユーザ移行Lambdaによるデータ移行を移行方法として選択する方針となりました。しかし、ここで1つ課題が見つかりました。移行対象のユーザープールではMFAの設定を必須にしており、移行時にMFAの有効化ができるか懸念があったためです。他社の事例を見ると回避手段がないように見えましたがAWSのテクニカルアカウントマネージャーに相談したところ、ユーザ移行Lambdaを利用してMFAの設定を有効化する方法を教えていただきました。懸念事項の回避手段が見つかり、実際にユーザ移行Lambdaを作成することになりました。 ユーザ移行Lambdaの作成 まずはユーザ移行Lambdaの挙動を整理するために簡易的なダイアグラムとフローチャートを用意しました。 簡易ダイアグラム フローチャート サインイン パスワードリセット 移行先アカウントのユーザープールに紐づけられたユーザ移行Lambdaが移行元のユーザープールのデータを取得する形となっています。ユーザ移行Lambdaは取得したユーザのデータの取得をレスポンスとして返すだけで、実際の登録処理は行いません。実際の移行先のユーザープールへのデータ登録はAWS側が行います。ここまででユーザ移行Lambdaの動きを簡単に説明しましたが、ユーザ移行Lambdaの実処理に関して実装時に注意したポイントを記載します。 ユーザ移行Lambdaの処理 ここからユーザ移行Lambdaのコードと実装時に注意したポイントを解説します。AWSで提供されているドキュメントを参考にしながらPythonで実装しました。処理の流れとしては、最初にユーザの存在を確認し、次にイベント情報をみて処理を分岐させます。サインインの場合は認証を処理した上でレスポンスを返し、パスワードリセットの場合は何もせずにレスポンスを返す形になっています。ここで注意したポイントは以下の2つです。 データ移行の観点から、レスポンスに含めるユーザ情報は移行元ユーザープールの情報を利用する レスポンスを受け取るのはAWS側であるため、 AWS公式ドキュメント に記載されているコードに沿って実装することを優先し、例外などもそのまま返す import boto3 from boto3.session import Session import json import os def lambda_handler (event, context): # setting src resource info SRC_ROLE_ARN = os.environ[ 'SRC_ROLE_ARN' ] SRC_USER_POOL_ID = os.environ[ 'SRC_USER_POOL_ID' ] SRC_USER_POOL_CLIENT_ID = os.environ[ 'SRC_USER_POOL_CLIENT_ID' ] SRC_AWS_REGION = os.environ[ 'SRC_AWS_REGION' ] # switch to src aws account sts_cli = boto3.client( 'sts' ) response = sts_cli.assume_role( RoleArn=SRC_ROLE_ARN, RoleSessionName= "switch_role_session" ) session = Session( aws_access_key_id=response[ 'Credentials' ][ 'AccessKeyId' ], aws_secret_access_key=response[ 'Credentials' ][ 'SecretAccessKey' ], aws_session_token=response[ 'Credentials' ][ 'SessionToken' ], region_name=SRC_AWS_REGION ) src_client = session.client( 'cognito-idp' ) # get user info from src cognito by input user info from event username = event[ 'userName' ] try : user = src_client.admin_get_user(UserPoolId=SRC_USER_POOL_ID, Username=username) except Exception as e: print (f "Unexpected {e=}, {type(e)=}" ) raise e # initiate auth from src cognito, if admin_initiate_auth is failed then is raised error if event[ 'triggerSource' ] == 'UserMigration_Authentication' : print ( 'UserMigration_Authentication' ) password = event[ 'request' ][ 'password' ] try : response = src_client.admin_initiate_auth( UserPoolId=SRC_USER_POOL_ID, ClientId=SRC_USER_POOL_CLIENT_ID, AuthFlow= "ADMIN_NO_SRP_AUTH" , AuthParameters={ 'USERNAME' : username, 'PASSWORD' : password } ) except Exception as e: print (f "Unexpected {e=}, {type(e)=}" ) raise e event[ 'response' ][ 'finalUserStatus' ] = 'CONFIRMED' event[ 'response' ][ 'enableSMSMFA' ] = True elif event[ 'triggerSource' ] == 'UserMigration_ForgotPassword' : print ( 'UserMigration_ForgotPassword' ) # common response sign-in/password reset # create new user on dst cognito by response data # just now, new user is active and verified for userattribute in user[ 'UserAttributes' ]: if userattribute[ 'Name' ] == 'phone_number' : phone_number = userattribute[ 'Value' ] if userattribute[ 'Name' ] == 'email' : email = userattribute[ 'Value' ] if userattribute[ 'Name' ] == 'email_verified' : email_verified = userattribute[ 'Value' ] if userattribute[ 'Name' ] == 'phone_number_verified' : phone_number_verified = userattribute[ 'Value' ] if userattribute[ 'Name' ] == 'custom:user_id' : user_id = userattribute[ 'Value' ] event[ 'response' ][ 'userAttributes' ] = { 'username' : username, 'email' : email, 'custom:user_id' : user_id, 'email_verified' : email_verified, 'phone_number' : phone_number, 'phone_number_verified' : phone_number_verified } event[ 'response' ][ 'messageAction' ] = 'SUPPRESS' # output migration user_id print (f "Migration: {user_id=}" ) return event IAMの設定 今回はクロスアカウントでの移行となるため、移行元と移行先でそれぞれRoleを作成しました。移行元では移行先のユーザ移行Lambdaが実行するRoleに権限を委譲する目的でRoleを用意しています。 移行元で用意するRole IAMRoleLambdaFunctionImportCognitoUserPool : Type : 'AWS::IAM::Role' Properties : RoleName : 'import-cognito-user-pool-role' AssumeRolePolicyDocument : Version : '2008-10-17' Statement : - Effect : 'Allow' Principal : AWS : !Sub 'arn:aws:iam::${DestinationAWSAccountID}:role/${DestinationAWSIamRole}' Action : 'sts:AssumeRole' Policies : - PolicyDocument : Version : 2012-10-17 Statement : - Effect : 'Allow' Action : - 'cognito-idp:AdminGetUser' - 'cognito-idp:AdminInitiateAuth' Resource : - !Ref CognitoUserPoolSrcArn PolicyName : 'import-cognito-user-pool-policy' 移行先のユーザ移行Lambdaの実行Role IAMRoleLambdaFunctionImportCognitoUserPool : Type : 'AWS::IAM::Role' Properties : RoleName : 'import-cognito-user-pool-lambda-function-role' AssumeRolePolicyDocument : Version : '2012-10-17' Statement : - Effect : 'Allow' Principal : Service : - 'lambda.amazonaws.com' Action : 'sts:AssumeRole' Policies : - PolicyDocument : Statement : - Effect : 'Allow' Action : - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:DescribeLogStreams' - 'logs:PutLogEvents' Resource : !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*' PolicyName : 'import-cognito-user-pool-lambda-function-policy' - PolicyDocument : Version : 2012-10-17 Statement : - Effect : 'Allow' Action : - 'sts:AssumeRole' Resource : !Sub 'arn:aws:iam::${SourceAWSAccountID}:role/${SourceAWSIamRole}' PolicyName : 'import-cognito-user-pool-assume-role-policy' 移行後 ユーザ移行Lambdaで発生した例外は準正常系のみであり、想定内の挙動に収まりました。また、移行後に全ユーザがサインアウト状態となるため、想定以上のサインインが行われた場合にCognitoのAPIのRateLimitに抵触することを懸念しましたが、無事想定内に収まりました。結果、大きな問題は起きませんでしたが、いくつか想定外の問題が発生しました。 移行後に顕在化した問題 ユーザ移行Lambdaを経由した場合、ユーザ認証が大文字と小文字を区別するcase-sensitiveな判定になってしまう 今回は移行処理のため、移行元のユーザのemailをレスポンスとして返却していましたが、この点で問題が発生しました。具体的にはLambdaが受け取ったusernameとLambdaから返されるレスポンスのemailが完全に一致していない場合はデータ移行がされないとわかりました。通常の処理では大文字と小文字を区別しないcase-insensitiveな判定をしているため、挙動が違う形となってしまいました。通常ケースとユーザ移行Lambdaを経由した場合の挙動の違いは以下の通りです。 ケース ユーザの入力値 DBに保存されている値 結果 通常のサインイン USER@example.com user@example.com 成功 ユーザ移行Lambda経由のサインイン USER@example.com user@example.com 失敗 通常のパスワードリセット USER@example.com user@example.com 成功 ユーザ移行Lambda経由のパスワードリセット USER@example.com user@example.com 失敗 テスト時に検証できていなかったケースで、移行後にAPIサーバ側でDBに保存されているユーザのemailを取得し、CognitoにはDBから取得したemailを受け渡す形に修正し、問題を解消しました。 ユーザ移行Lambdaを経由したサインイン時にSMSが2通届いてしまう サインイン時にユーザ移行Lambdaを経由した場合、移行先と移行元のCognitoからそれぞれSMSが届いてしまう問題が発生しました。こちらはテスト時にも発生していた問題でしたが、見落としてしまいました。切り替え後に移行元のCognitoでMFAを無効化することで対応しました。単純なテスト時の見落としですが、切り替え後に移行元のCognitoでMFAを無効化するべきだったこと、一時的にであれこの事象を許容できない場合はダウンタイムが不可避なこともわかりました。 まとめ いくつか移行後に問題が見つかりましたが、大きなトラブルなくユーザ負担も最小限に抑えて移行が完了しました。また、今回の移行作業は経験したことのない作業だったので多くの知見が得られました。 特に認証機構として利用しているCognitoについてデータ移行を想定していなかったこともあり、実際に調査してみないとわからない部分が多くありました。いざ移行するとなった時に初めて方法を検討する形となったのも反省点です。どのようなサービスであれ移行を考慮した上での技術選定は大切だなと改めて学びました。 計測プラットフォーム開発本部では、今回紹介したように、新規サービスの開発を活発に行いながら運用負荷を削減することでバランスを取って働いています。このような環境を楽しみ、サービスを一緒に盛り上げていける方を募集しています。少しでもご興味のある方は以下のリンクからぜひご応募ください。 corp.zozo.com CSVファイルのインポートについては、 AWS公式ドキュメント に詳細な仕様が記載されています。 ↩ ユーザ移行Lambdaについては、 AWS公式ドキュメント に詳細な仕様が記載されています。 ↩
こんにちは、MA部MA開発ブロックの @gachi-muchi-engineer です。 4/9-4/11に開催された Google Cloud Next '24 へ参加してきました。去年に続きオフライン開催で、今年はアメリカ・ラスベガスで開催されました。弊社からはMA部の @gachi-muchi-engineer ・ @da-sugi ・佐久間の3名が参加しました。去年参加した際の様子は以下のテックブログで紹介しています。 techblog.zozo.com 今年はどのようにAIを利用しているのか、利用していくのかを紹介したセッションが多かったように感じられました。本記事では、現地での様子と特に興味深かったセッションをピックアップして紹介します。 また、今回のテックブログで紹介できなかった内容などを含めてRecapのオンラインイベントを2024/5/13に開催予定です。このイベントでは、Google Cloud Japan合同会社の方にも登壇していただき今回のGoogle Cloud Next '24について詳しくお話いただきます。ぜひご参加ください。 zozotech-inc.connpass.com 現地での様子 Google Cloud Next '24の会場であるマンダレイ・ベイのコンベンションセンター 今回のイベントは、ラスベガスのマンダレイ・ベイで開催されました。会場はホテルに併設されているコンベンションセンターで、東京ドーム4個分ほどの広さだそうです。フルリモートで運動不足だった私達の足腰は、会場内を歩き回るだけでかなり鍛えられたと思います。 セッション会場の様子 各セッションは、センター内の大きな会議室で行われました。たくさんの会議室があり、それぞれ広々とした会場でした。 エキスポ会場内の地図 企業ブースは、とても大きな展示スペースで大変な数が出展されていました。お話した企業ブースの方が「この雰囲気はクレイジーだぜ」と言っていたのがとても印象深かったです。 それ以外にも企業ブースでは、Passport Program(いわゆるスタンプラリー)が行われていました。特定のブースを回ってQRコードを読み込むとCloud Nextのグッズがもらえるというものもありました。 エキスポ会場内の様子 以降で現地参加したメンバーが気になったセッションについて紹介します。 セッション紹介 @gachi-muchi-engineer です。 私は主にバックエンドの開発・運用に携わっています。今回参加したGoogle Cloud Next' 24の発表の中から今後の開発・運用が大きく変わっていくと感じた内容を紹介します。 Gemini for Google Cloud 今回のGoogle Cloud Next '24では、Google CloudのサービスにGeminiを導入した機能の紹介が多かったです。 まずGeminiについて簡単に紹介します。GeminiはGoogleが提供する最新のAIモデルです。テキストや画像、動画、音声などの複数の異なるデータを一度に理解できるマルチモーダルモデルの生成AIです。 詳しくは、以下のGoogleブログの記事をご覧ください。 blog.google GeminiをGoogle Cloudのサービスに導入することで、エンジニアをサポートする機能が追加されました。私のパートでは、Google CloudのサービスにGeminiが導入された機能を中心に紹介していきます。 What's next for Google Cloud databases in the gen AI era データベースに関して、"What's next for Google Cloud databases in the gen AI era"のセッションからいくつか紹介します。 Gemini in Databases データベースの開発・管理・運用を支援する3つの機能の紹介がありました。 1.Database Studio 「 What's next for Google Cloud databases in the gen AI era 」の27:32より引用 Cloud SQLにDatabase Studioが追加されました。BigQueryのWebコンソールと同じような操作性で、Webコンソールからクエリを実行し結果を確認できるようになります。 それに加えて、Geminiのサポートによりコンソールで自然言語を利用してクエリを生成できるようになります。他にも既存クエリを最適化してもらえるのはとても便利だと感じました。また、 Query Insights と合わせて利用することによりクエリの実行計画の可視化やクエリの実行時間の比較なども行えるので、開発者にとって非常に使いやすいと感じました。Database Studioを実際に利用してみましたが、とても操作性がよく利用しやすかったです。 今までは運用時に、 Cloud SQL Auth Proxy を利用したり、Cloud Shellからデータベースに接続したりしていました。ただ以下の課題を感じていました。 Cloud SQL Auth Proxyでは意図しないデータベースに接続してしまうなどのリスクがある Cloud Shellから接続する場合は、接続するのに時間がかかってしまう Database Studioを利用することで、これらの課題を解消し、より簡単に運用ができると感じました。 これだけでもとても便利ですが、これに加えてGeminiによるクエリのサポートを得られるようになります。Geminiに「こういうデータがほしい」と問いかけるだけでクエリを生成してくれるので、調査などの運用コストが下がると感じました。 2.Database Center 「 What's next for Google Cloud databases in the gen AI era 」の27:56より引用 Google Cloudのプロジェクトごとに運用しているデータベースを一元管理できるサービスです。自然言語で「セキュリティリスクを含んでいるデータベース」や「特定の状態になっているデータベース」などの検索が可能です。もちろん検索しただけではなく、そのデータベースに対してセキュリティアップデートの適応やスケールアップなどの操作も可能です。 3.Database Migration Service 「 What's next for Google Cloud databases in the gen AI era 」の35:22より引用 データベース移行を支援するサービスです。データベース移行は、データの整合性や移行後のパフォーマンスなど様々な課題がありますが、プロシージャーなどの移行や最適化もサポートされているので、簡単に行えると感じました。 紹介したサービスのAIによる支援は開発に関して大幅に生産性を向上させるだけでなく、セキュリティの課題やパフォーマンス向上に関するレコメンドなどは運用コストの大幅な削減に繋がると感じました。MA部では、月1回程度の頻度でアップデートの確認や対応をしています。地味に運用コストが結構掛かってしまったり対応漏れがあったりするので、Database Centerの活用でこういった課題の解決ができそうだと感じました。Database Migration Serviceは、データベース移行における課題やタスクをほとんど解消してくれるサービスなので、もし機会があればぜひ利用したいと思いました。 ベクトル検索に対応したデータベースプロダクト 「 What's next for Google Cloud databases in the gen AI era 」の5:04より引用 まずベクトル検索に関して簡単に紹介します。一般的なキーワード検索は、ある単語が文字列に含まれているものを検索する方法です。文章内に検索対象のキーワードが含まれていないと検索されません。一方でベクトル検索は、キーワードをベクトル(数値)に変換して、そのベクトルの類似性を利用した検索する方法になります。検索対象のキーワードが含まれていなくても検索されます。 これまでAlloyDB, Cloud SQL for PostgreSQLがベクトル検索に対応していましたが、これに加えて下記のデータベースプロダクトでもベクトル検索に対応予定です。 Cloud SQL for MySQL Spanner Firestore Bigtable Memorystore for Redis これにより、Google Cloudが提供するあらゆるデータベースプロダクトで最新データを利用してベクトル検索が可能になります。これによりリアルタイム性が求められるプロダクトでデータベースの選択肢が増えたと感じました。 MA部ではリアルタイムマーケティングシステムを運用しています。詳しくは以下のテックブログで紹介しています。 techblog.zozo.com 今回のベクトル検索の対応により、上記のテックブログで紹介したイベント検知とユーザー抽出の領域や最適化の領域において、将来的にベクトル検索を利用するアプローチが考えられ非常に興味深かったです。 このセッションでは、デモやユースケースの紹介もあり、どのように開発や運用に活用されるかを具体的な例を用いて非常にわかりやすく紹介されていました。運用に関しては、よくある社内でのケースを元にDatabase Centerがどのように活用できるのかが紹介されていました。特に自然言語からSQLを生成し実行するアプリケーションのデモは非常に興味深かったです。ぜひセッションのアーカイブ動画をご覧ください。 cloud.withgoogle.com Cloud Run: What's new "Cloud Run: What's new"のセッションからいくつか紹介します。 Volume Mounts 公開資料「 Cloud Run: What's new 」のP.9より引用 Cloud RunにおいてNFSとCloud StorageのVolume Mountsがサポートされます。これにより、Cloud Run上でのファイルの読み書きが可能になります。 これまでのCloud Runではファイルの読み書きが インメモリのファイルシステム だけでしたが、この機能が追加されることでメモリを気にせずファイルの読み書きが可能になります。この機能により、Cloud Run上で構築できるアプリケーションの幅が広がると考えられます。 Automatic Security Updates 公開資料「 Cloud Run: What's new 」のP.10より引用 デプロイされたimageのbase imageのセキュリティアップデートが自動で適用されるようになりました。 ダウンタイムはなく、リビルドの必要もありません。セキュリティインシデントに対して48時間以内にアップデートが自動で適用されるので、セキュリティのリスクを最小限に抑えられます。App EngineとCloud Functionsで運用されてきた機能のようで、Cloud Runでも利用できるようになります。 Gemini in Cloud Run Recommendations 公開資料「 Cloud Run: What's new 」のP.12より引用 Cloud Runのサービス一覧のページでGeminiがサポートされます。今までもレコメンドは表示されていましたがGeminiを利用することで、チャットを通してより積極的に利用してほしいと考えているようです。例えば、コストについての推奨事項があった場合に「より効果的なコストパフォーマンスを提供するための構成」などGeminiに質問ができるようになります。 Application canvas 公開資料「 Cloud Run: What's new 」のP.19より引用 Application canvasを使うことで、システムアーキテクチャをWebコンソール上で簡単に設計できるようになります。設計された各サービスの起動や接続に必要なロールの設定などを画面上で全て設定できるようになるサービスです。特にシステム構築の初期段階でアーキテクチャ図を作成する際にこちらを利用することで、そのままプロトタイプも開発できるようになるので非常に便利なサービスだと感じました。 これだけでも非常に便利なのですが、Geminiに自然言語で「こんなことができるアプリ」のように問いかけると適切なアーキテクチャを構築してくれます。セッションのデモで実際に自然言語で問いかけを行って、アーキテクチャの生成からアプリケーションのデプロイやアーキテクチャを修正する一連の流れが紹介されていました。 MA部では、現在運用しているマーケティングに関するシステムのリプレイスを行うZMPというプロジェクトを進めています。詳しくは以下のテックブログで詳しく紹介しています。 techblog.zozo.com その中で、Cloud RunはAPIや管理画面を構成する際に利用しています。Gemini in Cloud Run Recommendationsが利用できるようになると運用や改善点を発見するのに非常に役立ちそうだと感じました。私たちのチームでは、プロジェクトが進行していく中でリプレイスするシステムが増えていきます。Application canvasは、そこでシステム構築を検討する初期段階でアーキテクチャ図を作成する際に非常に便利だと感じました。自然言語で問いかけるだけでよいのも非常に便利です。 What's new with BigQuery 続いて"What's new with BigQuery"のセッションからいくつか紹介します。まず、このセッションでは様々な機能がBigQueryに統合され「AIに対応した単一のデータ分析プラットフォーム」であることが強調されていました。統合(unified)というキーワードがとても多く使われていたのが印象的で、新機能も様々なサービスとの連携強化やプラットフォームとしてより使いやすくなるような機能が多く紹介されていました。 公開資料「 What's new with BigQuery 」のP.12より引用 以下では、BigQueryの新機能の中から特に興味深かったものを紹介します。 Continuous real-time analytics in SQL 公開資料「 What's new with BigQuery 」のP.19より引用 この機能は、ストリーミングデータに対して継続的にSQLを実行できるようになるといったものです。 今まではDataflowを利用することが多かったですが、この機能によって選択肢が増えると思いました。BigQuery Studioなどから簡単に利用できるのであれば、Dataflowを構築する手間が省けるので非常に便利だと感じました。 BigQuery data canvas 公開資料「 What's new with BigQuery 」のP.27より引用 BigQuery data canvasは、自然言語を利用してデータ分析とビジュアライゼーションの作成が可能になります。 特に非エンジニアのデータ分析のハードルが大きく下がると感じました。 BigQuery data preparation 公開資料「 What's new with BigQuery 」のP.28より引用 BigQuery data preparationは、AIの支援を受けながらデータのクレンジングや変換が行えます。 AIがデータ変換に対してレコメンドしてくれる点が非常に便利そうだと感じました。大量にデータがあった場合に、意図しないデータや想定外のデータが含まれているときの警告を出したり変換を提案したりしてくれるようになります。以前データ分析をした際に、意図しないデータが入っていることで予想と違う結果になってしまい分析結果を検証しなければならないケースがありました。この機能を利用することで、そういった課題を解消できると考えられました。 BigQuery Workflows 公開資料「 What's new with BigQuery 」のP.25より引用 BigQuery Workflowsは、BigQuery Studioから簡単にワークフローを作成できる機能です。 Webコンソールからノーコーディングでワークフローを構築できます。構築されたワークフローはスケジュール実行ができるだけでなく、Cloud DataformやCloud Composer用にエクスポートできるようです。これはデータマートの作成や集計などで利用できそうなイメージが湧きました。BigQuery data preparationやBigQuery data canvasと組み合わせることで、データの前処理から分析、ビジュアライゼーションまでを一貫して行うことができると感じました。 What's next data analytics in the AI era こんにちは、MA部MA施策推進ブロックの @da-sugi です。私のパートでは、BigQueryで新たに発表があった機能を活用したデータ分析の進化について紹介します。 こちらのセッションでは、他セッションでも紹介されたBigQueryの新機能を前半で紹介しつつ、後半では、Geminiを活用したデータ分析のデモが行われました。AIに対応したBigQueryによって、スピーディーな分析と意思決定がいかに実現可能か、デモを交えて発表していました。会場の雰囲気としては1つ1つのプレビューの発表でも拍手が出るくらい、盛り上がっていました。 この章では、主にデモについて紹介します。新機能はデモに関連するものについてだけ紹介します。 BigQuery Studio (GA) 「 What’s next for data analytics in the AI era 」の24:09より引用 BigQuery Studioはデータの探索、分析、可視化、および共同作業を行うためのツールで、SQL、Python、自然言語などを使用してデータにアクセスできるデータ分析プラットフォームです。 わずか7 ~ 8か月前のGoogle Cloud Next '23でプレビューとして発表されたばかりですが、今回のGoogle Cloud Next '24で一般提供となりました。 BigQuery integration with Vertex AI for multimodal AI (GA) 「 What’s next for data analytics in the AI era 」の29:17よりスライド部分を強調して引用 BigQueryとVertex AIの統合によってマルチモーダルAIを実現できることが発表されました。 この統合により、BigQueryに保存されているデータを使用して、マルチモーダルデータを処理し、Vertex AIで機械学習モデルをトレーニングすることが可能になります。 Vector search in BigQuery (preview) 「 What’s next for data analytics in the AI era 」の31:59より引用 BigQueryにおける「ベクトル検索(Vector Search)」のプレビューが発表されました。 特定のカラムにベクトルデータが保存されており、そのデータをクエリして類似性の高いベクトルを持つレコードを見つけ、これにより画像検索など様々な分野で類似性検索を効率的に行うことが可能となります。 Gemini in BigQuery(preview) 直感的なインタフェースを使用してデータを探索し、インサイトを抽出できるGemini in BigQueryのプレビューが発表されました。Geminiを使用すると、SQLクエリを記述することなく、データセットの傾向やパターンを視覚的に理解できるようになります。またBigQuery Data Canvasを使うことで、インタラクティブな操作や自然言語でのデータ検索を共同で作業でき、その結果をビジュアル化して分析・共有できます。 デモ GeminiとLookerを使用して、オンラインファッションECサイトのデータを分析するデモが行われました。画像は、カスタムLookerアプリケーションで作成したもので、このリッチなレポートはGeminiとLookerで、なんとわずか数分で構築されたものだと言っていたのには驚きました。 「 What’s next for data analytics in the AI era 」の36:08より引用 実際にデモを通して、ビジュアライズに必要なデータをどのように構築されたのかを見ることができました。 まずデータ分析までの重要な3つのステップについて説明がされました。 「 What’s next for data analytics in the AI era 」の37:12より引用 ソーシャルメディアデータなどの複数のソースから取り込まれたデータを、全てまとめて分析できるようにGeminiとBigQueryを使用して変換 BigQuery Data CanvasとBigQueryのツールを使用して、トップトレンドの商品を発見・分析し、チャートを作成 BigQuery Vector Searchを使用して、トップトレンドの商品に似ている商品を見つける 1. GeminiとBigQueryを使用したデータのクリーニング クリーニング前のデータ 「 What’s next for data analytics in the AI era 」の38:14より引用 クリーニング作業 「 What’s next for data analytics in the AI era 」の38:25と38:49よりスライド部分を強調して引用 右側に表示されたAI生成コードの適用をクリックするだけで、瞬時に投稿日のフォーマットを整え、商品名のみが抽出されました。 クリーニング後のデータ 「 What’s next for data analytics in the AI era 」の38:54より引用 2. BigQuery Data Canvasを使用したトップトレンドの可視化 自然言語でのデータ検索と分析 Data Canvasを使用することで、自然言語でデータの検索ができ、即座にテーブルを作成できていました。 「 What’s next for data analytics in the AI era 」の39:43より引用 そのデータを元に、GeminiとBigQueryが自然言語からSQLを作成してデータ分析を可能にし、またVISUALISEから、わずか数秒でチャートだけでなくテキストのインサイトを作成していました。 「 What’s next for data analytics in the AI era 」の40:03より引用 「 What’s next for data analytics in the AI era 」の40:31より引用 3. BigQuery Vector Searchを使用した商品の類似性検索 Vector SearchはBigQuery内で、テキストから画像、動画からテキストなど、様々なデータ形式の類似性検索を行えます。これを使用して、デモではトップトレンドの商品に似た商品を検索していました。 「 What’s next for data analytics in the AI era 」の41:30より引用 4. MLの活用 また、BigQueryの機械学習を使用することで、トップ5の商品の今後1年間の販売予測もすぐに可能でした。これもGeminiとの対話によって実現されています。 「 What’s next for data analytics in the AI era 」の42:12より引用 セッション内のほとんどの作業が、単一のアプリケーション上で自然言語での対話によって実現可能であるのは非常に便利だと感じました。AIを活用したデータ分析がどれほど進んでいるのかを再認識できました。 ZOZOでは、BigQueryの新機能(GeminiとVector Search、Vertex AI)を組み合わせて、デモにあったように簡単な手順で様々な指標でのデータ分析が可能になり、次にどんな施策・配信をするのかなどの意思決定をよりスピーディーに行えるようになると思いました。 またMA部としては、前章(What's new with BigQuery)でも紹介があったSQLの継続的な実行(Continuous real-time analytics in SQL)と組み合わせ、最新のトレンド・販売予想などのデータを利用した配信も可能になるので、開発・運用しているマーケティング関連のシステムの改善や新規機能にも活用していけそうだと考えています。 What's new with IAM こんにちは、MA部MA開発ブロックの佐久間です。私からは "What's new with IAM - from least privilege to organization policies and AI-powered assistance"のセッション内容について紹介します。 私は普段バックエンドエンジニアとして開発・運用業務を行っていますが、IAMというセキュリティに関係する部分において、AI/MLがどのように機能していくのか興味がありました。管理者だけではなく、私のようなアプリケーション開発者の目線においても理解が深まる内容でした。 以降、IAMについての最新情報とともに、気になったトピックを紹介していきます。 Identity Provider こちらはGoogle CloudのIAM全体像です。 公開資料「 What’s new with IAM 」のP.6より引用 Identity Platformはその根底に位置し、多様なIdentityで構成されます。Identity Providerにはユーザーのものを使用できますが、以下の3通りの使い方があります。 【Cloud Identity】IdPをGoogleに同期して使用 【Identity Federation】Workforce Identity Federationを使用してGoogleに同期せず使用 【Mixed Mode】上記2つを合わせ、従業員の拡大や買収などで同期しきれない分にIdentity Federationを使用 今回、120以上の製品がIdentity Federation対象としてGAとなりました。一例ですが、Microsoft Power BIのEntra IDでBigQueryを利用できるようになっています。 Access Boundary 多層防御のアクセス管理として、IAMのGrant、Denyに加え、Access Boundaryが紹介されました。 公開資料「 What’s new with IAM 」のP.14より引用 こちらはアクセス可能なリソースの範囲、境界を制限するポリシーです。この境界には組織、フォルダー、プロジェクトといったレベルで定義できます。これらのポリシーはGrantを意味するのではなく、アクセス可能な最大の範囲を定義します。こちらは間もなくプレビュー版が公開されるそうです。 たとえ誤操作や誤認識で許可されてしまってもセーフティネットとして機能してくれそうです。 Privileged Access Manager Privileged Access Managerでは権限を資格として定義し、申請や承認の仕組みが利用できます。更新や承認が監査ログとして残る他、任意のEメールアドレスへ経過を送信できます。この度プレビューになりました。 公開資料「 What’s new with IAM 」のP.20より引用 Compute Adminなどの特権をそのまま付与するのではなく、資格として付与する権限や期間を定義できるため、一時的なトラブルシューティングのために1時間だけ払い出す、などの使い方ができます。そのため、作業後には意図しない権限が残り続けることはなく、不要な棚卸し作業から解放されそうです。 CIEM (Cloud Infrastructure Entitlement management) Google Cloud-AWS間でCIEMがプレビューになりました。年内にはMicrosoft Azureとの連携が予定されています。 公開資料「 What’s new with IAM 」のP.25より引用 CIEMではマルチクラウド環境でのアクセス権限管理を可能にし、IAMロールの最適化についてもレコメンドを受けることができます。また、Chronicle SOARというセキュリティプラットフォームに統合することで、発覚した過剰な権限がどのような脅威になるのかやその修復方法がわかり、JIRAチケットの自動発行なども可能になります。Chronicle SOARの詳細については以下のドキュメントをご覧ください。 cloud.google.com セッションでは過剰な権限付与がどれほど一般的に行われているかが説明され、最小権限の原則の重要性を強調されているようでした。 Resource Configuration 既に110以上のビルトインポリシーが、ガードレールのようにデフォルトで有効になっていますが、さらに組織レベルでのカスタムポリシー作成がGAとなりました。 公開資料「 What’s new with IAM 」のP.29より引用 例えば、GKEクラスターの作成にはバイナリ認証を有効にする必要がある、などを組織のポリシーとして定義できるようになります。 バイナリ認証が有効になっていると、検証環境で合格したイメージのみが本番環境にデプロイされることを担保できるなど、開発者にとってもうれしいポイントです。 Gemini Cloud Assist 最後に、Geminiによるアシストについての紹介です。こちらは間もなくプレビューになるそうです。 公開資料「 What’s new with IAM 」のP.37より引用 IAMやRoleに10,000もの推奨事項があるとして、必ずしも全てに対応する必要はないはずです。そこでGeminiが何から取りかかれば良いか、優先すべき事項を提案してくれます。また、あるサービスアカウントが最後に使われたのはいつだったのか、複雑なアクセスポリシーからどんな権限で拒否されているのかなどについても、自然言語で質問できるようになります。 やはり自然言語でどんな相談にも乗ってくれるのは、初学者や管理者などあらゆる立場の人にとって頼もしい存在に感じられました。ただセキュリティに関する部分なので、AI/MLが修復を推奨しなかったものはどのようなものなのか、なぜ推奨に至らなかったのかなどにも個人的には注意しようと思います。 まとめ 今年のGoogle Cloud Next '24は、去年同様にAIに関するセッションがとても多かったです。 去年はAIの可能性や今後についての観点でのセッションがメインだったと感じましたが、今年は進化したAIと実際の利用事例の紹介や今後Google CloudにどのようにAIが組み込まれ、進化していくかという内容が多かったと思います。 セッションだけでなく、世界中の企業が集まる企業ブースでAIの活用事例や現場のエンジニアとコミュニケーションを通して、本当にいろいろなところでAIが活用されているとを知ることができました。 この一年でAIが進化する速度の凄まじさを感じるとともに、次の一年でどこまで進化するのかが楽しみになりました。 紹介したセッション以外にもたくさんの興味深い発表がありました。全てのセッションは参加登録すれば公式サイトの Session Library から視聴できます。ぜひご覧ください。 最後に カンファレンス参加に伴う渡航費や宿泊費は 福利厚生 のひとつであるセミナー・カンファレンス参加支援制度によって全て会社負担です。 ZOZOでは一緒にプロダクトを開発してくれるエンジニアを募集しています。ご興味のある方は下記リンクからぜひご応募ください! corp.zozo.com
はじめに こんにちは。検索基盤部 検索技術ブロックの今井です。 検索基盤部では検索機能や検索精度を改善する中で検索クエリの意図解釈にも取り組んでいます。ZOZOTOWNで検索窓にクエリを入力して検索ボタンを押すと、クエリに応じて検索の絞り込み条件に変換するクエリ解釈機能の処理が動作します。 例えば、「ワンピース 白色」と検索した時、「ワンピース」を洋服のカテゴリー、「白色」を色のカテゴリーと解釈し、「白色のワンピース」を検索する絞り込み条件に変換します。 2024年5月現在ではスマートフォン向けWebサイト( https://zozo.jp/sp/xxx )とアプリのみ、クエリ解釈機能の処理が適用されています。クエリ解釈機能では意図解釈や検索の絞り込み条件に変換しています。 現在はシンプルな辞書ベースの手法を用いていますが、カバーしきれない課題も出てきており、改善のモチベーションが少しずつ上がってきています。本記事ではこれまでのクエリ解釈の取り組みについてシステム面も含めて紹介します。 目次 はじめに 目次 クエリ解釈について 従来のZOZOTOWNのクエリ解釈機能 導入背景 アーキテクチャ クエリ解釈機能のロジック 課題 クエリ解釈APIへのリプレイス 辞書生成バッチについて おわりに クエリ解釈について クエリ解釈は、検索の精度改善を目的として、検索者が入力したクエリの意図を解釈し、検索条件に変換します。クエリ解釈機能は以下のような処理フローで実現され、検索者の意図に沿った検索結果を返すことを目指しています。 ZOZOTOWNでは一部のみを導入し、全ての処理の導入までは至っていませんが、いずれは全て導入してクエリ解釈を強化したいと考えています。 以前弊社のテックブログ「 ZOZOTOWN検索の精度改善の取り組み紹介 」でも紹介している下記の外部記事で詳しく解説されていますのであわせてご参照ください。 Daniel Tunkelang: Query Understanding 検索体験を向上するQuery Understandingとは ※クエリ解釈は英語で query understanding (wikipedia) と呼ばれています。 従来のZOZOTOWNのクエリ解釈機能 ZOZOTOWNには10年ほど前からクエリ解釈機能が導入されていました。 導入背景 導入に至った背景としては以下2つの観点がありました。 検索の精度改善の観点 検索エンジンに対してキーワードマッチで検索するよりも、特定カテゴリのID等で検索結果を絞った方が検索精度の向上が期待できるため SEO観点 キーワード検索の検索結果ページに遷移するよりも、ブランドページやカテゴリページに遷移した方がSEO観点で良いとされるビジネス的な要件のため これらの観点が元となって導入された機能は以下です。 クエリ文字列から特定の絞り込み条件に変換する仕組み 特定ページへのリダイレクトURL構築(Web用) アーキテクチャ クエリ解釈機能が導入されたシステムアーキテクチャは以下です。この機能はClassic ASPで実装されています。 クエリ解釈機能のロジック 先述の通り、クエリ解釈機能で用いる手法はシンプルな辞書ベースの変換手法を用いています。以下は辞書のイメージです。 ターム 意図エンティティタイプ 意図エンティティ名 意図エンティティID ジャケット category "jacket" 1 パンツ category "pants" 2 zozo brand brand "ZOZO BRAND" 11 zozo shop shop "ZOZO SHOP" 21 zozo brandshop brand "ZOZO BRANDSHOP" 12 shop "ZOZO BRANDSHOP" 22 ... 検索条件の構築までの流れは以下です。タームによっては複数意図を持つタームも存在します。(e.g. 上記の表の"zozo brandshop") クエリ ジャケット を入力する 辞書内のターム一覧から ジャケット に完全一致でマッチした辞書エントリーを取得する 取得した辞書エントリーから カテゴリエンティティ(カテゴリ意図) であること、そのidが 1 であることを認識する 辞書エントリーによっては複数エンティティに紐づいていることがある 検索エンジンにリクエストする絞り込み条件として category_id に 1 をセットする 複数エンティティが存在する場合、優先意図の考慮や絞り込み条件同士の整合性が取れているかなどをチェックして一意に絞り込む 導入しているクエリ解釈機能の中には、以下3つの処理が含まれています。 処理 説明 クエリ分割(Query Segmentation) スペース区切りの文字列を意味のあるまとまりごとに扱うようにするために判定・分割する。 e.g. ・クエリ「ジャケット」→「ジャケット」としてセグメント化 ・クエリ「zozo brandshop 春服」→「zozo brandshop / 春服」として分割しセグメント化 クエリの属性の引当(Entity Recognition) セグメント化された各文字列がどの属性のタームなのかをエンティティとして識別する。 e.g. ・「ジャケット」→「カテゴリ:jacket(ID:1)」を属性として識別 ・「zozo brandshop 春服」→「ブランド:ZOZO BRANDSHOP(ID:12)」「ショップ:ZOZO BRANDSHOP(ID:22)」「キーワード:春服」を属性として識別 絞り込み検索条件の構築(Query Scoping) 識別されたタームを検索条件に変換する。検索エンジンに応じてクエリ要素へのマッピング内容が変わる。 e.g. ・「カテゴリ:jacket(ID:1)」→「カテゴリID:1」の絞り込み条件に変換 ・「ブランド:ZOZO BRANDSHOP(ID:12)」「ショップ:ZOZO BRANDSHOP(ID:22)」「キーワード:春服」→「ブランドID:12」AND「キーワード:春服」の絞り込み条件に変換("zozo brandshop"をショップではなくブランド条件として検索する場合) 課題 現行の辞書ベースの手法には、クエリ解釈精度面の課題があることが分かりました。特に、辞書にマッチしたタームが以下のケースに該当する場合は、変換を控える必要があります。 誤変換されるケース 複数の変換候補があるにもかかわらず、特定の候補に変換することがある この誤変換によって、他の変換候補の検索結果が表示されなくなる問題も発生 同音異義語が存在するケース ワンピース(カテゴリ)、ワンピース(漫画)は、カテゴリに変換するとワンピース(漫画)関連の商品が表示されなくなる 文字列長が短いケース 1文字、2文字など短いタームを変換してしまうため、検索者が意識せずたまたま入力したタームに対して誤ったまま変換してしまう可能性がある これらの問題が発生した場合は1件ずつアドホックに対応してきました。ただし、このままだとアドホック対応をいつまでも続ける必要があるため、根本解決する必要があります。 根本解決に向けて進めるために、まずはレガシーな実装の問題を解決するところから進めようと考えました。現行のクエリ解釈機能を保守運用する中で以下のアーキテクチャ面の課題が出てきました。 APIの管轄部署が検索基盤チーム以外であったため、改修や新機能の検証を気軽に出来なかった 実装がレガシー Classic ASPで実装されていたため、新規参画者に開発・保守運用の経験が少なく、学習コストが高かった モノリシックなレガシーAPIの1機能となっているため、改修による他影響を考える必要があった テストコードがないため、リグレッションに対して注視する必要があった アドホック対応 現状上記のような問題が発生したときに都度対応している ビジネス要件での変換依頼に対応することが時々ある 改善施策を施しABテストを実施するサイクルが回せていない これらのアーキテクチャ面の課題を解決すべく、まずはクエリ解釈APIのリプレイスを実施し、変換の課題を解決するための環境を整えました。次項では、そのリプレイス対応について説明します。 クエリ解釈APIへのリプレイス これらの課題解決の第一歩として、ZOZOTOWNのモノリシックなレガシーAPIの中からクエリ解釈の主機能を切り出し、クエリ解釈APIとしてリプレイスしました。このリプレイスではまず既存仕様を踏襲することとし、今後改善しやすいアーキテクチャを構築することを最優先としました。 このAPIは弊社技術スタックの推奨言語の1つであるGo言語で開発しました。採用理由としては、高速に動作することやGo言語開発の経験者による開発スピードの向上が期待できることなどが挙げられます。 リプレイス前後のシステムイメージは以下です。 ※ 辞書ファイルのレコード数は大規模ではないためKubernetesのPodに内包する形式を採用。そのため外部通信が発生せず安定かつ高速に動作。 クエリ解釈APIとして切り出すことで以下の恩恵が得られました。 機能追加、保守運用のし易さ リプレイス前は必要最低限の変更しか行われていなかったが、リプレイス後は問題の特定や影響範囲が分かりやすくなったこともあり定期的に改善が行われるようになった これにより「実装がレガシー」と「APIの管轄部署が検索基盤チーム以外であったため、改修や新機能の検証を気軽に出来なかった」の課題が解消された パフォーマンス向上 リプレイス前のAPIが高速でなかったこともあったが、リプレイス前後で比較すると10倍以上高速に動作 辞書登録する内容を一部見直し、リプレイス前は検索時に逐次DB問い合わせして取得していた情報をリプレイス後では予め辞書登録しておくようにしたことも効果的であった 今回のリプレイスにより以下の課題を解消できました。 実装がレガシー APIの管轄部署が検索基盤チーム以外であったため、改修や新機能の検証を気軽に出来なかった ただし、「アドホック対応」と「改善施策を施しABテストを実施するサイクルが回せていない」の課題の解消にはまだ至っていないため、引き続き解消に向けて取り組んでいきたいと思います。 辞書生成バッチについて 辞書ファイルは辞書生成バッチで生成していましたが、このバッチもClassic ASPで実装されていました。クエリ解釈APIのリプレイスに伴いバッチ側も同様にリプレイスを行いました。 具体的には、ワークフローエンジンに Vertex AI Pipelines を採用しPythonでバッチ処理を実装しました。 Classic ASPからVertex AI Pipelinesに乗り換えたことでワークフローエンジンとしての基本的な機能を得ることができ、保守運用を行いやすくなりました。以下は基本的な機能の例です。 バッチ処理内のタスクの依存関係を定義できる 処理途中で失敗したタスクからリトライできる backfillが容易になる データソースにはGoogle BigQueryとカスタマイズCSVファイルを利用します。Google BigQueryからはブランドやショップ、カテゴリなどの情報を取得します。これらのデータを用いて辞書ファイルを作成しGoogle Cloud Storageにアップロードします。 Google Cloud Storage上の辞書ファイルは、クエリ解釈APIのDockerコンテナイメージを作成するタイミングでダウンロードし内包しています。このDockerコンテナイメージからKubernetesのPodを作成・起動することで、外部通信が発生することなく安定かつ高速に動作します。 おわりに 本記事では、ZOZOTOWNでのこれまでのクエリ解釈の取り組みについて紹介しました。 クエリ解釈機能を従来のClassic ASPの実装からAPIとして切り出してGo言語でリプレイスしました。リプレイスではこれまでの仕様を踏襲したため依然として残っている課題も多々ありますが、切り出したことで改善しやすい状態に整えられました。 現在、クエリ解釈の次ステップの試みとして誤変換に対する課題を解決する手段などを検討・分析しています。この取り組みについても紹介できるようになり次第共有したいと思います。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは。SRE部フロントSREブロックの三品です。 3月19日から3月22日にかけてKubeCon + CloudNativeCon Europe 2024(以下、KubeCon EUと呼びます)が行われました。今回弊社からはZOZOTOWNのマイクロサービスや基盤に関わるエンジニア、推薦システムに関わるエンジニアの合わせて4人で参加しました。 本記事では現地の様子や弊社エンジニアが気になったセッションや現地の様子について紹介していきます。 目次 KubeConEU2024の概要 セッションの紹介 現地の様子 ブースについて 参加に向けてのTips 最後に KubeCon EU 2024の概要 昨年4月にオランダ アムステルダムで行われたKubeCon EUの様子については昨年の参加レポートをご覧ください。 techblog.zozo.com 今年のKubeCon EUはフランスのパリで開催されました。昨年まではコロナ禍の影響もありオフラインとオンラインのハイブリッドで開催されていました。 しかし今年はオフラインに統一され12,000人以上が現地で参加しており、今年もKubeCon EU史上最大の参加人数を更新しました。 キーノート会場の様子 KubeCon EUではキーノートやセッション、LTなどを通してKubernetesに関する最新のアップデートの紹介や実際にKubernetesを採用した企業の幅広い運用ノウハウを聞くことができます。 以降では参加してきた社員がそれぞれ気になったセッションや現地の様子について取り上げてご紹介します。 セッションの紹介 セッションタイトル Tutorial: Cloud Native WebAssembly and How to Use It - Brooks Townsend & Michael Yuan Cloud-Native LLM Deployments Made Easy Using LangChain Strategies for Efficient LLM Deployments in Any Cluster Why Is This so HARD? Conveying the Business Value of Open Source Kubernetes Maintainers Read Mean Comments To Infinity and Beyond: Seamless Autoscaling with in-Place Resource Resize for Kubernetes Pods Comparing Sidecar-Less Service Mesh from Cilium and Istio Future of Intelligent Cluster Ops: LLM-Azing Kubernetes Controllers Is Your Image Really Distroless? - Laurent Goderre, Docker Building Confidence in Kubernetes Controllers: Lessons Learned from Using E2e-Framework - Matteo Ruina, Datadog & Philippe Scorsolini, Upbound Tutorial: Cloud Native WebAssembly and How to Use It - Brooks Townsend & Michael Yuan ML・データ部MLOpsブロックの松岡です。 私の所属するMLOpsブロックではMLを使用した様々なサービスのプラットフォームを運用しています。MLにおいて近年話題になっているのがLLM(大規模言語モデル)とLLMを使った生成AIです。KubeCon EUでもLLMについてのセッションが多くありました。 その中でも特に興味深かったのがWasmを紹介した次のセッションです。 Wasmについての概要 LLMを実用化するにあたって重要な懸念点の1つがサービスがスケールする際のコストの問題です。 それを解決するためにユーザーのCPUやGPUで高速にプログラムを動かすことができる WebAssembly(Wasm) が注目されています。 このセッションでは、Wasmの概要から入り、実際にWasmを使ってLLMを動かしChatbotを構築する方法を紹介していました。説明が幅広く網羅的でかつ具体的でわかりやすかったです。 Wasmはブラウザ上で高速にプログラムを動かすために登場したバイナリコード形式のプログラミング言語です。 アセンブリと言う名前ですが、コーディングにあたってアセンブラ言語を記載する必要はなく、 C言語 や Rust など様々な高級言語からWasmバイナリにコンパイルできます。 コンパイル済みのコードは純粋な機械語ではなく抽象化された機械語となっており、Wasm Runtimeが各環境に応じた形へ変換し実行します。この仕組みによりJavaのように単一のコードで異なるOSやCPUアーキテクチャをサポートします。 当初この技術は3D処理などをブラウザ上で実行する用途として注目されていましたが、最近ではMLの推論をエッジで実行する用途にも注目されています。そしてWasmが小さなVMのようなものであることを応用しサーバーサイドにおけるDockerの置き換えとしても注目されているようです。 Wasm RuntimeによりCPUのアーキテクチャが抽象化される 「 Cloud Native Wasm And How To Use It 」より引用 WASIでWasmの利用用途が広がる このセッションではWASI(WebAssembly System Interface)0.2.0についても説明されていました。 WASIは、Wasmが直接サポートしていない標準入出力への読み書きや、ファイルシステム、ソケットへのアクセスなどアプリケーションに必要な機能をWasmから使用するのに使われています。これによりWasmはブラウザだけでなくサーバーサイドでの利用も可能になりました。 これらのインタフェースを標準化し、各Wasm Runtimeにてサポートすることで、実行環境が抽象化されコンテナ標準のように使用できるようになることがアナウンスされていました。 Wasmをコンポーネントとして扱い、コンポーネントを統合して新たなコンポーネントを作るアイデアが紹介されていました。それぞれのコンポーネントはGoやJavaなど様々な言語で記載可能です。コンパイラーがWasmのバイナリに変換します。コンポーネント間の入出力はWASIによるインタフェースで定義されます。 WASIは WIT(Wasm Interface Types) に基づく高度な型システムを利用できます。セッションではWITの型を一瞥できる wit-cheat-sheet が紹介されていました。 各コンポーネントはメモリー空間が独立しておりWASI以外には依存しないことで高い独立性を保ちます。そして、これらのコンポーネント間の呼び出しはナノ秒で行うことができるとのことです。WASIにより入出力が定義されているため、コンポーネント間は言語を揃える必要すらありません。Rustで書いたコンポーネントとGoで書いたコンポーネントを組み合わせて新しいコンポーネントを作るといったことが可能なようです。これはコードの再利用性を高め、システム構築の言語選定時に大きな自由度を与えてくれることになります。 異なる言語のコンポーネントがWASIでつながる 「 Cloud Native Wasm And How To Use It 」より引用 Wasm Edge Runtimeにより多くの環境でバイナリを高速に動かすことができる Wasm Edge Runtimeでの実行速度についての説明では、Wasmがネイティブの速度を超える事例について紹介していました。 sometimes faster than nativeについて 「 Cloud Native Wasm And How To Use It 」より引用 これは、特定の環境に最適化されていないネイティブバイナリーコードよりは、実行環境ごとに最適化されたWasmのほうが高速になりえるという話です。特定の環境に最適化されたネイティブバイナリーコードよりWasmのほうが速いというわけではないので注意してください。WasmはWasm Runtimeにより実行環境に応じて最適化されて実行できる利点がここでは主張されています。 また、プラグインにより機能を提供することで最小の機能を保ちながら必要な機能を追加していく事が可能であること、一例としてガベージコレクションをプラグインで提供したことが紹介されていました。 このガベージコレクションはKotlinからの強い要望があったそうですが、Kotlinの実行環境としてJVMだけでなくWasmという新しい選択肢が出てくることは大変興味深いです。 Cloud NativeにおけるWasiの有用性 WasmをKubernetes上で動かし、 Linuxコンテナーを置き換えるものとするアイディア も紹介されていました。 マイクロサービス化において、オーバーヘッドとなるLinux部分をWasmのモジュールへ置き換えることでコストの削減とスループットの向上につながる可能性があります。Wasmのモジュールはミリ秒で起動します。これはLinuxコンテナを起動するよりはるかに高速です。またWasmのモジュールが必要なメモリー量はLinuxコンテナよりも少ないです。そのため、Linuxコンテナの代わりにWasnのモジュールを用いることでコスト・運用ともに大幅な改善が期待できるとのことです。 また、Wasmはアーキテクチャに依存しないことも魅力的です。例えば私は普段Apple SiliconのMacBook Proで作業していますが、これにはArmをサポートするCPUとMetalをサポートするGPUが採用されています。ところがサーバーサイドでは一般的にx86をサポートするCPUとCUDAをサポートするGPUを使用することが多く、両環境でコンテナイメージを共有できませんでした。 Linuxコンテナイメージに変わって、Wasmコンポーネントを使用することでこの差は抽象化され両環境で一貫したバイナリを実行できるのはとても魅力的に感じます。フロントエンド寄りの技術だと思っていたWasmがCloud Nativeで大きな変革を起こす可能性があることは今回のセッションでの良い発見でした。 Wasmについて深く調べてみたくなりました。 LLMをエッジで動かすチュートリアル セッション後半では実際にWasmを使用して簡単な Hello World! を作成した後、より実践的なデモとして端末のGPUを使ってLLMモデルを動かしChatbotを実行するハンズオンが行われました。 LLMを使った生成AIは大量のコンピュータリソースを使用することがネックとなっています。WasmによりユーザーサイドのGPUを使用することでこの問題を解決できるかもしれません。このハンズオンのユースケースもまたWasmが今後大きく注目される部分だと思います。 Wasmは Cloud Native Wasm Day Hosted by CNCF においても詳しく紹介されているためそちらも合わせて見られることをおすすめします。 この他にもさまざまな魅力的なセッションがあり、全てを紹介できないので、いくつかをピックアップします。 Cloud-Native LLM Deployments Made Easy Using LangChain このセッションではLangChainを使用したクラウドネイティブなLLMのデプロイ方法について段階を追って説明されていました。モデルをデプロイするための手順として使用するモデルの定義、モデルの実行方法についての検討、LLMのパッキング、モデルのコンテナ化、複数モデルの統合の5段階で説明されていました。ML、特にLLMについてはモデルの作成だけでなく生成もコストが大きいこと、その結果に曖昧性が残ることなどから多くの項目を検討する必要があることを考えさせられました。 Strategies for Efficient LLM Deployments in Any Cluster 利用用途に応じたモデル選定について解説したセッションです。コストの大きなLLMsだけでなく必要に応じてSMLs(Small Language Models)=小規模言語モデルを選ぶメリットなどを紹介しています。 モデルの得手不得手を理解して、複数モデルの使い分けを意識したいと思います。 Why Is This so HARD? Conveying the Business Value of Open Source オープンソースプロジェクトの価値をいかに示すか解説したセッションです。 GitHub のIssueやPull Requestへラベルを付けて進捗を可視化する方法など、オープンソースに限らずプロジェクト管理で幅広く活用出来そうなアイデアが紹介されていました。 Kubernetes Maintainers Read Mean Comments KubernetesのIssueに投げられた意地悪なコメントについて紹介されていました。技術的な情報ではありませんが、コミュニティーがどのように機能しているかを理解し、円滑なコミュニケーションを推進する方法について示唆がありました。 To Infinity and Beyond: Seamless Autoscaling with in-Place Resource Resize for Kubernetes Pods KubernetesでPodsを運用するうえで悩みの種であるスケーリングについて、スケーリングがどのように機能するか、新しいin-Place Pod Resizingについて紹介されていました。 スケーリングにおける副作用についての学びが大きかったです。 Comparing Sidecar-Less Service Mesh from Cilium and Istio ML・データ部MLOpsブロックの 岡本 です。 Solo.ioのChristian Posta氏による、 Cilium と Istio Ambient Mesh を使ったサイドカーレスなサービスメッシュ構成の比較についてのセッションをご紹介します。本セッションのメインテーマでは、サイドカーレスなサービスメッシュについて、CiliumとIstio Ambient Meshを使ったそれぞれの構成を次の観点で比較していました。 コントロールプレーン データプレーン 相互認証 / mTLS 可観測性 トラフィック制御 サイドカーとは、Kubernetes Pod内のアプリケーションコンテナに対し補助的な役割を持つコンテナです。サービスメッシュを構成する場合に、従来はKubernetesの各Pod内でアプリケーションコンテナとEnvoyコンテナを稼働するサイドカーモデルが取られていました。 サイドカーモデルで構成するサービスメッシュには、サービスメッシュを透過的に構成できることや他のワークロードにリソースが占有されてしまう(ノイジーネイバー問題)を回避できるなどの利点がありました。 サイドカーコンテナの利点 「 Comparing Sidecar-less Service Mesh from Cilium and Istio 」より引用 一方でサイドカーモデルにはいくつかの課題もありました。例えば、アプリケーションコンテナとサイドカーコンテナの競合によるPodの起動・終了シーケンスの複雑化や、サイドカーのアップグレード時にアプリケーションの再起動が必要になることなどです。 サイドカーコンテナの課題 「 Comparing Sidecar-less Service Mesh from Cilium and Istio 」より引用 サイドカーレスなサービスメッシュはこのようなサイドカーモデルでの課題を解決するために提案されました。 次にセッションのメインテーマであるCiliumとIstio Ambient Meshを使ったサイドカーレスなサービスメッシュ構成の比較のうち、データプレーン部分の違いについて簡単にご紹介します。 サービスメッシュのアーキテクチャは大きくコントロールプレーンとデータプレーンに分かれています。データプレーンでは、主にPod間の通信を制御するプロキシの役割を担っており、コントロールプレーンではデータプレーンを管理しています。 データプレーンにおける構成の主な違いは、CiliumではOSI参照モデルのL4処理をeBPFで実装しているのに対し、Istio Ambient MeshではZtunnelで実装しているという点です。eBPFとはextended Berkeley Packet Filterの略であり、Linuxカーネルのコードを変更することなく、動的にカーネルの機能拡張を行う技術です。CiliumではeBPFを利用することでネットワーク処理を効率化しています。一方でZtunnelとはzero trust tunnelの略であり、Istio Ambient Meshのために実装されたNodeごとのプロキシです。Istio Ambient MeshではZtunnelを利用することでwaypoint proxy(L7処理を行うコンポーネント)へ効率的かつ安全にトラフィックを転送できます。 CiliumサービスメッシュのL4 「 Comparing Sidecar-less Service Mesh from Cilium and Istio 」より引用 Istio Ambient MeshサービスメッシュのL4 「 Comparing Sidecar-less Service Mesh from Cilium and Istio 」より引用 サービスメッシュの機能をどのネットワークレイヤで実現し、どこで実装しているのか、CiliumとIstio Ambient Meshでのそれぞれの構成の比較は次のスライドをご参照ください。 CiliumサービスメッシュでのL4・L7の分離 「 Comparing Sidecar-less Service Mesh from Cilium and Istio 」より引用 Istio Ambient MeshサービスメッシュでのL4・L7の分離 「 Comparing Sidecar-less Service Mesh from Cilium and Istio 」より引用 発表の最後の部分では、次の観点についてそれぞれのサービスメッシュ構成が適しているかそうでないかについて述べられていました。 リソースのオーバーヘッド 機能の分離 セキュリティの粒度 更新時の影響 これらの観点について各構成はそれぞれ次のスライドで評価されています。 Ciliumサービスメッシュの評価 「 Comparing Sidecar-less Service Mesh from Cilium and Istio 」より引用 Istio Ambient Meshサービスメッシュの評価 「 Comparing Sidecar-less Service Mesh from Cilium and Istio 」より引用 Ciliumでは特にリソースオーバーヘッドを小さくしたいケースに適しており、機能の分離という観点ではあまり適していないことがわかります。Istio Ambient Meshでは特に機能の分離という観点で適している一方で、リソースオーバーヘッドについてはCiliumでの構成にやや劣ることがわかります。このようにサイドカーレスなサービスメッシュにおいてはいくつかの構成が考えられます。またそれぞれの構成にはトレードオフがあるとわかりました。 サイドカーレスなサービスメッシュはサイドカーモデルでの課題解決のために提案されているとご紹介しましたが、サイドカーモデル自体についても日々改善されています。Kubernetesのv1.29では Sidecar Containers の機能がbetaで提供され、明示的にアプリケーションコンテナとサイドカーコンテナが区別できるようになります。これによりサイドカーモデルでのサービスメッシュでは、セッション内で挙げられていたいくつかの課題の改善が見込まれています。 今後のサービスメッシュにおいてはサイドカーモデルまたはサイドカーレス、CiliumまたはIstio Ambient Meshなどユースケースに応じてどの構成を取るべきか判断が必要だと感じました。 Future of Intelligent Cluster Ops: LLM-Azing Kubernetes Controllers SRE部ECプラットフォーム基盤SREブロックの織田です。 1日目のKyenoteは、すべてがMLやAI、LLMに関するものでした。今回紹介するセッションもLLMを利用してマニフェストに記述したコマンドや文章を理解し、効率的に実行する LLMNETES の紹介です。 LLMNETESは、OpenAIのGPT-3を含む、複数のLLMをサポートしておりローカルのモデルもデータセットとして使用できます。また、独自のインタフェースを実装することで独自のLLMも使用できます。 次にマニフェスト例とそれらをApplyすることでどのようなことができるのか4つほど紹介します。 1つ目は、ポート80を公開したnginx Podを3台作成する例です。 spec.inputに実行したいことを記述するとそれ通りにリソースの作成などを実施できます。複雑でマニフェストの作成に時間がかかりそうな場合やPodをすぐに用意したい場合などに役立ちそうに感じました。 apiVersion : llmnetes.dev/v1alpha1 kind : CommandExec metadata : name : command1 spec : input : Create 3 nginx pods that will serve traffic on port 80. 2つ目にデプロイされているPodとDeploymentのイメージをスキャンする例です。 spec.typeにScanImages、spec.resourcesにスキャンしたいリソースを記述すると対象のリソースのイメージをスキャンできます。 CIからツールを使って定期的にスキャンする場合は、CIでツールのセットアップなどを行う必要があるため、マニフェストをApplyするだけでスキャンできるのは便利そうに感じました。 apiVersion : llmnetes.dev/v1alpha1 kind : ClusterAudit metadata : name : cluster-audit-cves spec : type : ScanImages resources : - Pods - Deployments 3つ目にカオスエンジニアリングのシミュレーションをトリガーする例です。 spec.commandに実行したいことを記述します。以下は、default namespaceのPodを削除する例となります。 apiVersion : llmnetes.dev/v1alpha1 kind : ChaosSimulation metadata : name : chaos-simulation-cr spec : level : 10 command : break my cluster networking layer (or at least try to) 最後は、クラスタ内の非推奨APIを検出する例です。 spec.typeにClusterUpgradeCheckを指定します。現在、非推奨APIを検出する方法は多く存在しますが、マニフェストをApplyするだけで検出できるのはとても魅力的に感じました。 apiVersion : llmnetes.dev/v1alpha1 kind : ClusterUpgradeCheck metadata : name : cluster-upgrade-check spec : type : ClusterUpgradeCheck 1つのツールでイメージのスキャン、カオスエンジニアリング、非推奨APIの検出など様々なことができます。そのため、実施したいことごとにツールをインストールせずシンプルな構成になることで、アップグレードなどの運用コストが下がるというメリットがありそうに思いました。 LLMNETESは、開発中でありまだ本番で利用するまでには至っていませんが、検証などを行いながら導入を検討していきたいと考えています。 Is Your Image Really Distroless? - Laurent Goderre, Docker SRE部フロントSREブロックの三品です。 私が所属するSRE部フロントSREブロックでは、ZOZOTOWNが持つAPIの中でもクライアントに近い、frontendレイヤーのサービスを運用しており、言わばEmbedded SRE的な業務を行っています。 今回KubeCon EUに参加して私が業務の中で活かせそうだなと思ったセッションをいくつか紹介します。 このセッションでは、私たちが使っているDocker ImageはDistributionの影響を受けない状態にできていますか?と言うことを投げかけていました。 セッションの内容を説明すると、最初にDocker Imageを作成するのに必要なLinuxのDistributionやセキュリティとユーザビリティのジレンマについて説明していました。 what is a distro? 「 Is Your Image Really Distroless? - Laurent Goderre, Docker 」より引用 Usability-Security Dilemma 「 Is Your Image Really Distroless? - Laurent Goderre, Docker 」より引用 次にDistrolessなDocker Imageを生成するためDockerfileを作成しbuildするための方法やツールが説明されていました。 Multi-stage builds 「 Is Your Image Really Distroless? - Laurent Goderre, Docker 」より引用 buildKit 「 Is Your Image Really Distroless? - Laurent Goderre, Docker 」より引用 その後、bashなどがメインコンテナに含まれることによっての危険性を危惧し、initコンテナを利用し安全に依存関係のファイルをインストールする方法についてdemoを交えて説明していました。 init containers to the rescue! 「 Is Your Image Really Distroless? - Laurent Goderre, Docker 」より引用 私は今までDistrolessについてあまり意識を向けたことがありませんでした。 しかし、セッションを通してbashなどがメインコンテナにインストールされていることで実際にコンテナに侵入された場合の危険性について再考できました。 セッション内でinitコンテナの利便性について触れていましたが、initコンテナを利用してmainコンテナに何かを注入する処理を行うと、container起動までに追加の時間を必要とする可能性があります。セキュリティとのトレードオフになるため、initコンテナを利用する際にはこの点を考慮する必要があると感じました。 コンテナをセキュアに扱うと言う意味ですごく意味のあるものだと考えられますので、今回取り上げました。 Building Confidence in Kubernetes Controllers: Lessons Learned from Using E2e-Framework - Matteo Ruina, Datadog & Philippe Scorsolini, Upbound このセッションでは、Kubernetesコントローラーの信頼性を高める手法としてエンドツーエンドテスト(以降、E2Eテストと呼びます。)を取り上げ、E2Eテストの基礎理念から始まって特に他社2社の実践例について取り上げていました。 本記事ではその中でも自分が個人的に気になった部分を抜粋して紹介します。 本セッションではKubernetesのE2Eテスト( e2e-framework )をベースに説明されていました。 ※ e2e-frameworkとは、Kubernetesクラスター内で実行されるコンポーネントのエンドツーエンドテストを行うためのGo製フレームワークです。Kubernetesリポジトリ上にも多くのE2Eテストの例が公開されています。 その後E2Eテストのツールの特徴について述べられていました。 Goals 「 Building Confidence in Kubernetes Controllers: Lessons Learned from Using E2e-Framework 」より引用 日本語で翻訳すると以下のようになります。 採用に役立つ文書化されたフレームワーク 組み込みGoテストパッケージを使用する テストを構成するためのプログラム可能なAPIコンポーネントを提供する クライアントの機能を抽象化するヘルパー関数を提供する Kubernetesへの依存を避ける Go E2E Test Framework for Kubernetes でも同じようなことが書かれているのでこちらもご覧ください。 次にプログラムの動きについて説明をしていました。基本的なプログラムの動きについて説明します。 Example code 「 Building Confidence in Kubernetes Controllers: Lessons Learned from Using E2e-Framework 」より引用 環境ごとの設定情報を持つ 環境ごとの設定情報を元に実際のテストを紐づける テストの実行 DatadogやCrossplaneのE2Eテストの事例紹介ではE2Eテストフレームワークの比較や実装方法、問題点が紹介されていました。本記事ではその中でもフレームワークの比較について取り上げたいと思います。 Alternatives we considered 「 Building Confidence in Kubernetes Controllers: Lessons Learned from Using E2e-Framework 」より引用 セッションの中では、フレームワークがGoで書かれていること、そしてテストの時に環境のセットアップができると言う意味でe2e-frameworkが有効であると結論付けられていました。 セッション内では、実際にe2e-frameworkを実際に使ってみての課題感についても取り上げられていましたが、本記事ではその点については触れないので興味のある方は動画をご確認ください。 私は、Kubernetesのカスタムコントローラーを運用する業務は直接的には実施していませんが、Kubernetesの挙動を簡素化させたい時や機能をカスタムしたい時があります。そういった時にKubernetesのカスタムコントローラーは有効なので、引き続きKubernetesのE2Eテストの分野についてもウォッチしていきたいなと感じました。 現地の様子 今年のKubeCon EUはパリで開催され、日本とは全く違う景色を楽しむことができました。また、オリンピックを控えたこの時期には、オリンピックグッズを扱う店舗も見られ、オリンピックの雰囲気を肌で感じることができました。 パリの風景 会場のParis Expo Porte de Versaillesです。敷地内にはマクドナルドやコンビニも隣接されており、非常に行動しやすい会場でした。 会場入り口の様子 バッジピックアップの会場 会場の案内図 会場周囲にはトラムが走っており、交通の便がよく治安も良い場所のように感じました。地元のグループによる早朝パリ観光マラソンも開催されていました。 会場近くの風景 続いては、KubeCon EUのランチです。KubeCon EUのランチでは、肉、魚、ビーガン向けの食事などが提供され、参加者の多様な食事ニーズに対して細やかな配慮が施されていました。 会場で提供されたランチ(1日目) 会場で提供されたランチ(2日目) また、今回もCloud Native Community Japanさん主催のもと 日本人交流会 が実施され、各社の参加されているエンジニアの方々と意見交換ができとても楽しい時間を過ごすことができました。 日本人交流会の様子 ブースについて KubeCon EUはCloudNativeConと同時に開催されKubernetesをはじめとする様々なOSSプロジェクトのブースや企業ブースがあり、それらのプロジェクトに参加するメンテナーと身近に話せる機会でもあります。 実際に我々も社内で利用するOSSプロジェクトのブースに出向き機能要望や意見交換を実施できました。 KubeCon EUはOSSプロジェクトのメンテナーとの距離が近く議論できる機会になるなと感じました。 Fluxブースの様子 Istioブースの様子 参加に向けてのTips できるだけ早めに準備する 参加の可否をできるだけ早く決めて、一刻も早く動くことで自由度が広がります。 KubeCon EUのチケットは早い時期に買うほど大幅に割引が効くため、参加コストを大きく抑えられます。 例えば、2023年11月28日までにチケットを購入していた場合$1149で参加できました。しかし、2024年3月17日以降にチケットを購入すると$2229となっておりほぼ倍の金額となっていました。 とくに宿泊する宿については、時間が経つにつれて条件の良い宿が埋まっていくため早めに準備することで現地での移動を便利にできます。 時差ボケに備える フランスは日本と比べて8時間遅く時間が進みます。フランスの朝8時は日本では深夜のため、朝起きることは簡単でもセッションが進むにつれて眠たくなる状態でした。 そのため意識的に睡眠を取って体内時間をずらす必要がありました。 質問を用意しておく セッション終了後には質疑応答が用意されていることが多く、登壇者との交流の機会でもあるため、セッションの内容を予習しておき疑問点をまとめておくとより有意義な参加になります。 最後に 今年からKubeCon EUはオフライン形式で統一され、会場は活気にあふれていました。 今回はKubeCon経験者(NAも含む)からKubeCon初参加者まで、幅広いメンバーで参加しましたがオープンソースプロジェクトのメンテナーや発表者との距離が近かったため、Kubernetesおよび、その関連エコシステムについての深い学びを得ることができました。 ZOZOでは一緒に働くエンジニアを募集しています。また、KubernetesやCloudNativeが大好きなエンジニアも大歓迎です。ぜひご応募ください。 corp.zozo.com corp.zozo.com
こんにちは、カート決済SREブロックの飯島と、ECプラットフォーム基盤SREブロックの織田です。 本記事では複数チームで運用する共通のAWSアカウントとKubernetesにおけるコストの可視化についてご紹介します。 背景 コスト可視化に対する課題 課題解決へのアプローチ AWSリソースのコスト可視化 AWSコスト配分タグ タグの定義と運用ルール タグの付け方 AWS Cost Explorer AWSコスト配分タグの活用例 Kubernetesクラスタのコスト可視化 Kubecost 比較検討 カスタムバンドル採用の決め手 アーキテクチャ 可視化の仕組み ダッシュボード 効果 コスト可視化の活用事例 最後に 背景 現在、ZOZOTOWNはモノリスなサービスを機能ごとに分け、マイクロサービスに移行しながらモダンアーキテクチャへのリプレイスを実施しています。マイクロサービスの移行先としてクラウドプラットフォームはAWSを、アプリケーション実行基盤はKubernetesを選定しています。両者とも複数チームで共通アカウントを利用するマルチテナント構成となっています。 AWSアカウントやKubernetesをマルチテナント化することには、メリットとデメリットの両面があります。 マルチテナントのメリットとしては、1つのAWSアカウントや1つのKubernetesクラスタですべてのサービスを管理できるため、運用作業が簡素化されます。Kubernetesの場合、アップグレードの回数が減り、共通の設定を他のクラスタに展開する必要がないため、運用コストを削減できます。 しかしその一方で、マルチテナントにすることで課題も生じます。特に、コスト可視化の面での影響が大きく、1つのアカウントやクラスタを複数のチームやサービスが利用しているため、チームやサービス単位でのコスト分離が困難になり、コストの可視化が難しくなります。 このように、マルチテナントにすることで運用が簡素化される半面、コストの可視化や分離が難しくなるというトレードオフが発生しておりました。 コスト可視化に対する課題 上記で述べた通り、マルチテナントでは共有されたリソースを複数のチームやサービスが利用するため、個別のコスト把握が困難になる傾向があります。1つのAWSアカウントやKubernetesクラスタで請求やリソース使用量が集約されてしまうため、チームやサービス単位でコストを明確に分離・可視化できていませんでした。 そのため、各プロジェクトやチームのコスト負担を正確に見積もることが難しくなり、予算計画の精度が下がります。マルチテナントの利点を生かしつつ、コスト可視化を実現するためには、仕組みを構築する必要がありました。 次に紹介するアプローチを講じることで、マルチテナントでもチームやサービス単位でのコスト可視化を実現し、より適切な予算管理を行えるような取り組みを実施しました。 課題解決へのアプローチ 課題解決のアプローチとして、AWSコスト配分タグの活用と、KubernetesクラスタへのKubecostの導入を実施しました。次のセクションからはそれぞれの詳細について説明します。 AWSリソースのコスト可視化 AWSコスト配分タグ AWSコスト配分タグ とは、AWSリソースに割り当てることができるラベルです。このタグにより、リソースのコストをより詳細に追跡し、分析できます。 タグの定義と運用ルール AWSコスト配分タグとして以下の表に示すタグを新たに作成しました。 Key Value CostEnv AWS環境を表す文字列。「dev」や「prd」など。 CostService どのサービスで使用しているかを表す文字列。Namespace名に統一。 CostTeam どのチームが管理しているかを表す文字列。 各タグのValueの値には、命名規則を設けてフォーマットを統一しています。原則として、AWSリソースには「CostEnv」と「CostService」を付けることを必須としています。CostServiceが付いていれば管理しているチームは把握できるため、「CostTeam」は任意としています。 タグの付け方 基本的にAWSリソースはAWS CloudFormationで管理されています。そのため、スタックテンプレートを使用してタグを付けます。以下にサンプルのコードを示しますが、タグの書き方はリソースによって異なりますので、詳細は公式ドキュメントをご参照ください。 Resources : IAMUserSample : Type : "AWS::IAM::User" Properties : UserName : "IAMUserSample" Tags : - Key : CostEnv Value : "dev" - Key : CostService Value : "zozo-techblog-sample" 上記のサンプルのコードを実行すると、AWSコンソールから以下のようにタグ付けされていることが確認できます。 注意すべきポイントはタグ付けができないAWSリソースも存在することです。また、AWS CLIではタグ付けができてもAWS CloudFormationではできない場合もあります。以下の公式ドキュメントに情報がまとまっているので、合わせてご参照ください。 Resource types you can use with AWS Resource Groups and Tag Editor AWS Resource Groups Tagging API Reference AWS Cost Explorer AWS Cost Explorerとは、AWSのコストおよび利用状況を可視化し、分析するためのツールです。事前にAWS Billing and Cost Managementコンソールから タグの有効化 を実施することで、AWSコスト配分タグを利用した検索やフィルタリングが可能です。 AWSコスト配分タグの活用例 AWS Cost Explorerの操作方法について説明します。以下の図は、検索とフィルターを使用していない状態で、AWSアカウント全体のコストの合計が表示されています。 例えば、ディメンション機能を使用してTag:CostServiceを選択すると、サービスごとのコストの合計が表示されます。 また、ディメンション機能で「サービス」を選択し、フィルター機能を使用してTag:CostServiceから特定のサービスを選択すると、そのサービスのAWSリソースごとのコストが表示されます。 Kubernetesクラスタのコスト可視化 Kubecost Kubecost とは、Kubernetesクラスタ内のリソース利用とコストを追跡・管理するためのツールです。無料版とエンタープライズ版が提供されています。また、AWSが提供している、Amazon EKS上でKubecostを利用するのに最適化された カスタムバンドル もあります。弊社ではAmazon EKSでKubernetes環境を構築しているため、このカスタムバンドルを採用しました。 比較検討 どの種類を採用するか決めるにあたり、主に「データの閲覧可能な期間」と「利用料金」の2つの観点で検討しました。以下の表は各種類の特徴をまとめたものです。 Kubecostの種類 データの閲覧可能な期間 利用料金 カスタムバンドル 15日前までの制限あり 無料 無料版 15日前までの制限あり 無料 エンタープライズ版 制限なし 有料 カスタムバンドルと無料版では、データの閲覧可能な期間が15日前までに制限されており、それ以前のデータは閲覧できません。コストを見積もる際には、月や年単位の過去のデータが必要となるため、コスト可視化の用途には適していませんでした。利用料金についてはどちらも無料ですが、カスタムバンドル以上のメリットがない無料版は選択肢から外しました。 一方、エンタープライズ版ではデータの閲覧に制限がなく、無期限に閲覧できます。ただし有料であり、Amazon EKSクラスタで稼働するNode数に応じて料金が決まります。利用料金を算出した結果、エンタープライズ版の採用が即決できるものではありませんでした。 カスタムバンドル採用の決め手 Kubecostにはデータをエクスポートできる API が提供されています。詳細については後述しますが、このAPIを使用してデータをAmazon S3に保存することで、閲覧可能な期間の制限に縛られなくなります。このようにして、カスタムバンドルのネックが解消できたため、採用することにしました。 余談ですが、カスタムバンドルのメリットの1つに、AWSサポートでの問い合わせが可能ということが挙げられます。問題発生時の問い合わせ先がAWSに統一できることはユーザーとしてもメリットとなります。 アーキテクチャ 弊社で採用しているKubecostのアーキテクチャです。 公式のアーキテクチャ も合わせてご確認いただくと理解しやすいかと思います。 Kubecost内のコンポーネントごとの役割としては以下のようになっています。 Kubecost Cost-Analyzer Frontend: nginxを実行し、Prometheusへのルーティングを行う Cost model: コストの計算とメトリクスを提供する AWS Sig V4 proxy: Amazon Managed service for Prometheusからクエリするためのプロキシ。パスワードレス認証が可能になり、AWS認証情報が公開されるリスクを軽減できる Prometheus Prometheus Pod: Kubecost Cost-Analyzerが生成したデータをAmazon Managed service for Prometheusにリライトする Amazon Managed service for Prometheus: コストとメトリクスを格納する時系列データストアでKubecostが生成したデータを格納する Kubecostは、Kubernetes APIサーバーからNode、Pod、コンテナ、ボリュームなどのメトリクスを収集します。主要なクラウドプロバイダー(AWS、GCP、Azure、etc.)の価格データが組み込まれており、そのデータを使用して、リソース使用量に基づきコストを算出します。 また、収集したメトリクスと価格データを組み合わせて、リソースごとのコストをモデリングします。CPUとメモリの使用量、永続ボリュームの種類とサイズ、ネットワークの送受信データ量などから、それぞれのコストを計算します。 Namespace、Deployment、Service、Podなどのレベルでコストを集計しているため、特定のワークロードのコストを確認できます。Kubecostはリザーブドインスタンスやスポットインスタンスの利用、適切なインスタンスタイプ選択に関する最適化の提案をします。また継続的にメトリクスを収集することで、長期的なコストトレンドの分析や予測も可能になります。 Web UIやGrafanaを使用することで、メトリクスの可視化やアラートを設定できます。しかし弊社では、先述したようにカスタムバンドルでのデータ永続化に課題があったため、独自に可視化の仕組みを構築しています。詳細に関しては、次のセクションで説明します。 可視化の仕組み 先述したように、KubecostのAPIを使用してデータをエクスポートし、Amazon S3に保存しています。その処理と、Amazon S3のデータを可視化する仕組みについて説明します。アーキテクチャは以下の通りです。 まず、毎日定時に起動するGitHub ActionsのWorkflowからAPIを実行し、前日のNamespaceごとのコストをCSVファイルで取得します。その後、取得したCSVファイルをAmazon S3にアップロードします。APIは以下のcurlコマンドで実行しています。 curl " http://kubecost-cost-analyzer.kube-support.svc.cluster.local:9090/model/allocation " \ -d window =yesterday \ -d aggregate =namespace \ -d accumulate =true \ -d format =csv \ -G 次に、Amazon S3のイベント通知機能を使用し、アップロードしたCSVファイルをAmazon SQSのキューに通知します。最後に、それをSplunkの Add-on で取り込み、ダッシュボードで可視化しています。弊社でのSplunkの活用に関する情報はテックブログで公開されていますので、そちらもぜひご覧ください。 techblog.zozo.com techblog.zozo.com ダッシュボード ダッシュボードではデータを表示したい日付の範囲と、対象のAmazon EKSクラスタの環境が選択できます。選択した日付の範囲内の、Amazon EKSクラスタのコストの合計が表示されます。 また、日ごとのAmazon EKSクラスタ全体のコストとNamespaceごとのコストが表示され、選択した日付の範囲内で推移を確認できます。 先述したようにKubecostではNamespaceだけでなくDeploymentやServiceなどのコストも集計しています。またコストの分析や予測、監視といった機能も提供していますが、弊社ではチームやサービス単位でのコストの可視化を目的としたため、上記の仕組みを採用しました。 効果 これまでの課題解決へのアプローチにより、マルチテナントのAWSアカウントとKubernetesのコスト可視化が実現できました。その結果、コストを正確に見積もることができるようになり、適切な予算管理が可能になりました。またチームやサービス単位でコストが明確に分離・可視化できるようになりました。 次に、コスト可視化の活用事例をご紹介します。 コスト可視化の活用事例 ZOZOTOWNのカート投入処理では、Webサーバとデータベースの間にキューイングシステムを挟むことでリクエストのキャパシティコントロールを実現しています。このシステムで使われているAmazon Kinesis Data Streamsのシャード数を変更しました。 techblog.zozo.com 以下の図は、左側がシャード数変更前のコストを示し、右側がシャード変更後のコストを示しています。関連リソースのAmazon DynamoDBを含め、大幅なコストの削減が実現できました。 このようにコスト可視化によって効果が明確に見えるようになったことは、コスト最適化の業務に取り組むうえでのモチベーション向上に繋がりました。 最後に 本記事では、マルチテナントのAWSアカウントとKubernetesにおけるコスト可視化の課題に対し、AWSコスト配分タグの活用とKubecostの導入による解決事例を紹介しました。同様の課題を抱えている方がいれば、ぜひ参考にしてください。 SREにおいてコスト最適化は重要なミッションです。今回ご紹介したコスト可視化の仕組みを利用し、ZOZOTOWNの信頼性向上の一環として取り組んでいきます。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは。DevRelブロックの @wiroha です。2024年3月22日〜24日に「 try! Swift Tokyo 2024 」が開催されました。ZOZOはGOLDスポンサー・DIVERSITY & INCLUSIONスポンサーとして協賛し、ブースを出展しましたので現地のレポートをお届けします! 目次 はじめに 目次 会場 スポンサーブース アンケートの回答 印象に残ったセッション Swiftの型推論を学ぼう コード署名を楽しく乗り切る方法 Party、その他 アフターイベント LT1. SwiftPM マルチモジュール構成への第一歩 LT2. SwiftとC++を利用した画像処理プログラミング LT3. Introducing Pkl 感想戦 最後に 会場 会場は前回(2019年)と同じ、ベルサール渋谷ファーストです。try! Swiftのマスコットキャラクター「Riko」ちゃんのパネルやポスターが出迎えてくれます。 セッションルームはとても広く、たくさんの人が入れるようになっていました。朝から満員のようでした! 休憩ができるBreak Areaではかわいらしいお菓子やコーヒーなどが提供されていました。食べるのがもったいなくなります! こちらはスポンサーブースやAsk the Speakerコーナーのあるエリアです。休憩時、昼食時は特に大きな賑わいを見せ、皆さま交流を楽しんでいました。 スポンサーブース 各社さまざまな工夫を凝らしたブースが出展されていました。ZOZOのブースはたくさんのパネル・ノベルティにマネキンで存在感を出していました。 今回のメイン企画は「みんなの失敗」です。ZOZOには「#みんなの失敗」というSlackチャンネルがあり、失敗を共有することで共に学び、時に笑い、「日々進歩」しようという社内企画が行われています。その出張版として、ブースで皆さまの「開発の失敗」や「技術的負債」をお聞きしました。参加してくださった方には、失敗を水に流せるようにオリジナルトイレットペーパーのノベルティを差し上げました! トイレットペーパーの柄はZOZOのロゴをモチーフにしており、「ZOZO」の文字もこっそり隠れているんです! みなさんは気付きましたか? アンケートの回答 1日目のお題、「水に流したい開発の失敗」には最終的にパネルに貼りきれないほどの回答をいただきました! ヒヤリハットで済むものから、「これは深刻そう…」というものまで、さまざまな失敗が集まりました。なぜそうなったか、その後どうしたのかといった会話が弾みました。 2日目のお題は「水に流したい技術的負債」です。一度回答した方も「どんな回答が増えたか見に来ました」とたずねてくださる方が多数いました。これから勉強をはじめる方や新規開発中の方など、失敗や負債がまだない方には最近勉強していることを回答いただきました。 「英語」という回答には納得の声が出ました。try! Swiftは英語話者の参加が多いため、勉強しようというモチベーションが上がりますね。 「Apple Vision Proを買いましたか?」の質問は、悩んでいる方・次世代機を待っている方が多数ではあるものの、既に持っている・もうすぐ手に入るという方もいました! ZOZOでは会社でApple Vision Proを保有しており(技適特例申請済み)、ブースで体験できるようにしたところ一時は行列ができるほど盛況しました。 AirPlayでApple Vision Proで見ているものを共有しながらデモしている様子 印象に残ったセッション ここからは参加したエンジニアより、セッションの感想を紹介します。 Swiftの型推論を学ぼう speakerdeck.com 技術開発部のばんじゅんです。わたしはおもちメタルさんの「Swiftの型推論を学ぼう」をおすすめしたいと思います。コンパイラの理論的な部分はとっつきにくいものも多いですが、興味を持って調べるとおもしろい分野でもあります。このセッションでは、型推論ではまず押さえておきたい単純な例から始めて、まるで手で推論を計算するかのように学ぶことができます。一歩発展して、SwiftらしくOptionalの扱いの一部も計算できます。簡単に説明されていますが二度三度みてもよいくらい内容は詰まっていると感じます。より多くの人がコンパイラをつくる技術に興味をもつ入口のひとつになっていると良いなと思いながらセッションを聞きました。 登壇資料のスクリプト付きバージョン も公開されているので、内容をテキストで確認したいときはそちらが便利です。 コード署名を楽しく乗り切る方法 ZOZOTOWN開発1部iOSブロックの荻野です。このセッションでは、アプリをリリースするときに必ず通るコード署名について聞くことができました。発表の最初、「コード署名でつまずいたことがある方はいますか?」という問いかけに対して、会場にいたほとんどの人が手を挙げていました。自分と同じようにコード署名に苦い思い出のある人がこんなにいるんだと安心しました(笑)。 セッションの中では、コード署名において必要な要素を4つに分解し、それらをパズルのピースのように見立てていました。そうすることで起きているエラーのどこに問題があるのかが視覚的にわかりやすくなり、どう解決したらよいのかがすんなり理解できました。 発表の最後では、多くの参加者が「コード署名が楽しくなったと感じた」と手を挙げており、とても面白くためになる発表でした。発表資料は現時点では未公開のようで、公開されたらぜひ見ていただきたいです。 Party、その他 他にもSpeaker Dinner、スタンプラリー、Party、ワークショップなど企画が盛りだくさんのカンファレンスでした。 アフターイベント 開催翌週の3/27(水)、アフターイベントとして「 try! Swift Tokyo 2024 After Talk 」を開催しました。try! Swift Tokyo 2024のDIVERSITY & INCLUSIONスポンサーとして協賛しているピクシブ株式会社、株式会社ZOZO、STORES 株式会社による共催イベントです。モバイル開発に関するLTと、try! Swift Tokyo 2024の感想戦を行いました。当日の様子はYouTubeのアーカイブで公開しています。 www.youtube.com LT1. SwiftPM マルチモジュール構成への第一歩 speakerdeck.com ピクシブ株式会社 / 山本 小龍 @shoryu927 LT2. SwiftとC++を利用した画像処理プログラミング speakerdeck.com 株式会社ZOZO / 加藤 祥真 @shoma10170806 LT3. Introducing Pkl speakerdeck.com STORES 株式会社 / 榎本 健太 @enomotok_ 感想戦 感想戦はSTORES 株式会社の坂田( @huin )さま、ピクシブ株式会社の @FromAtom さま、STORES 株式会社の榎本さま、株式会社ZOZOの加藤で行いました。全体を振り返って楽しかった点や印象に残っているトーク、逆に難しかったので復習したいトーク、思い出に残った企画などを語り合いました。アフターイベントにご参加くださったみなさま、ありがとうございました! LT・感想戦の登壇者のみなさま 最後に try! Swift Tokyo 2024本編からアフターイベントまで、交流と学びを楽しませていただきました。オフラインカンファレンスは普段会えない方々とお話ができる貴重な機会です。このような場を提供くださったtry! Swift Tokyo 2024の運営の皆さま、本当にありがとうございました! ZOZOではカンファレンスに参加する方には参加費用の補助、登壇する方には登壇支援を行っています。一緒にサービスを作り上げてくれるiOSエンジニアを募集していますので、ご興味のある方は以下のリンクからぜひご応募ください。 hrmos.co カジュアル面談も実施していますので、「直近での選考応募は考えていないけども話を聞いてみたい」という方からもご応募お待ちしています! hrmos.co
はじめに こんにちは、SRE部 検索基盤SREブロックの花房です。普段は、ZOZOTOWNの検索関連マイクロサービスにおけるQCD改善やインフラ運用を担当しています。 以前まで、検索基盤を支えるチームではElastic Cloudの特権アカウントをメンバーで共用していました。本記事では、2023年4月にリリースされた Elastic CloudのRBAC(Role-Based Access Control)機能 を活用して、特権アカウントの共用から脱却した取り組みについて紹介します。さらに、既存機能と組み合わせることで実現した、効率的な権限管理についても紹介します。 同様の課題を抱えている読者の方には、下記の部分で参考になれば幸いです。 Elastic CloudにおけるSSOの活用 Elastic CloudのRBACによる権限管理の実例 Elastic Cloudアカウント情報を利用した、ElasticsearchのRole Mappingによる権限管理の実例 目次 はじめに 目次 背景・課題 Elastic Cloudの利用方法 何故、特権アカウントを共用していたか 権限管理における課題 解決策 Elastic Cloudの権限管理 Organization RBACの利用 SSOによるアカウント管理の省略 Elastic Cloudアカウントを利用したElasticsearchの権限管理 Cloud Roleに対するRole Mapping SSOアカウントに対するRole Mappingの追加 ElasticsearchのオリジナルRole 共通のRoleと、チームごとのRoleの作成 TerraformによるIaC化 結果 おわりに 背景・課題 Elastic Cloudの利用方法 ZOZOTOWNの検索基盤では、大規模な商品データを扱うためにElastic Cloud上のElasticsearch 1 を利用しています。Elastic Cloudは、ElasticsearchやKibanaなどのElastic製品をクラウド上で利用できるサービスです。検索基盤を支えるチームは、それぞれ担当業務が異なりますが、全チームがElastic Cloudにアクセスします。以下に業務の例を挙げます。 Elasticsearchを利用する検索機能の開発 ノード数やマシンスペックの増強 アラート発生時の調査 一般的に、クラウドサービスへのアクセスはメンバーそれぞれのアカウントを使用すべきです。さらに、そのアカウントには担当業務にあった権限セット(以降、 Role と表します)を付与します。このように管理することで、情報漏洩などのセキュリティ上のリスクを防ぐことができます。しかし、本記事の取り組みを実施するまで、私たちは特権アカウントを共用してElastic Cloudにログインしていました。その状態を下記の図に示します。 何故、特権アカウントを共用していたか Elastic Cloudの利用を開始した当初、登録できるアカウントは1つまでという制限があったため、やむを得ず特権アカウントを共有していました。 特権アカウントを使用すると、Elasticsearchクラスタの管理を担うDeploymentに対して全ての操作が可能です。そのため、特権アカウントの共用は誤操作による重大な事故を引き起こすリスクがありました。例えば、誤って本番環境のクラスタを削除してしまうことが考えられます。 そこで、操作頻度が高くリスクも大きいDeployment管理にはIaC化を施しました。IaC化により、管理画面からDeploymentを変更・削除する機会をなくすことで事故を防いでいました。こちらのIaC化の詳細については、以前の記事を参照ください。 techblog.zozo.com その後、 Elastic Cloudのアップデート により、Organizationへのアカウント追加が可能になりました。しかし、全てのアカウントに管理者権限が付与されてしまうため、細かい権限管理はできませんでした。以上の理由から特権アカウントの共用が続いていました。 権限管理における課題 これまでに説明した利用方法の問題を整理します。 アカウントの共有によるセキュリティ上のリスク 誤操作により重大な事故を引き起こす可能性について、対策を施していたが排除はできていない 上記の問題は、単純にメンバーごとにアカウントを分離すれば解決できそうに思います。しかし、アカウント分離について下記の課題があり、状況は変わりませんでした。 チームに合ったRoleを柔軟に設定できない メンバーごとのアカウントを新たに作成する場合、アカウント管理が大きなトイルになる 以降では、上記の問題・課題の解決方法と、改善後の権限管理について説明します。 解決策 Elastic Cloudの権限管理 2023年4月、 Elastic CloudのRBAC機能がリリース されました。この機能を利用すると、Organizationへ招待したメンバーに対して細かく権限を設定できます。 私たちは、この機能を利用することで特権アカウントの共用から脱却できました。 Organization RBACの利用 Elastic CloudのRBACでは、用意されたRole(以降、 Cloud Role 2 と表します)をメンバーに付与して権限を管理します。Cloud Roleには下記の5つが存在します。 Organization owner Billing admin Admin Editor Viewer 私たちは、Deployment操作の制限のためにAdminとViewerを使用しています。そのため、今回はその2種類のみ紹介します。その他のCloud Roleの説明は、 公式ドキュメント をご確認ください。 Cloud Role 説明 Admin Deploymentの作成や、プロパティ管理、セキュリティ権限の管理が可能 Viewer Deploymentの閲覧が可能 AdminとViewerは、Deployment単位で設定可能です。私たちは、SREチームに全DeploymentのAdmin、バックエンドチームやMLチームには全DeploymentのViewerを設定しています。このように設定している理由は、バックエンドやMLチームの誤操作による事故を防ぐためです。 Deployment管理と同様に、OrganizationのRBACもIaC化したいと考えていました。しかし、Organization機能がTerraformに対応していないため、今回は断念しました。現在は手動で権限を管理していますが、下記の理由から保守は難しくなく問題にはなっていません。 検索基盤を支えるチームは合計20人程度 設定するCloud Roleは、AdminまたはViewerの2種類のみ SSOによるアカウント管理の省略 RBACのためには、利用メンバーごとにアカウントを分ける必要があります。しかし、新たにElastic Cloudアカウントを作成すると、管理者側と利用者側の両方でアカウント管理がトイルになります。そこで、Elastic CloudへのSSOログインに会社管理のGoogleアカウントを用いるようにしました。 GoogleアカウントでのSSOにより、アカウント管理のトイルを削減できました。招待作業は残りますが、退職時の削除作業やパスワード変更は不要になります。退職時は会社側でGoogleアカウントが削除されるため、自動的にElastic Cloudにもログイン不可になります。また、利用者はElastic Cloud用にアカウントを使い分ける手間がなくなりました。会社管理のGoogleアカウントは、Elastic Cloud以外にも様々なサービスで普段利用しているため、統一することで利便性の向上にも繋がりました。 SSOを利用するために特別なことは必要ありません。通常のアカウントと同様に、Organizationに招待する際、Roleを付与した上でGoogleアカウントのメールアドレスに招待メールを送るだけです。利用者は招待メールからログイン画面に進み、GoogleアカウントでSSOログインするとOrganizationに参加できます。 ここまで説明した改善により、Elastic Cloudへのアクセス方法は下記の図のようになりました。 Elastic Cloudアカウントを利用したElasticsearchの権限管理 Elastic Cloudアカウントは、Elasticsearchへのアクセスにも利用可能です。直接アクセスするのではなく、クラスタに紐づくKibanaを通してGUIでアクセスします。Deploymentの管理画面からKibanaにアクセスすると自動的にログインされますが、下記のようなログイン画面が表示されることもあります。その場合は下側の Login in with Elastic Cloud を選択してください。 一方、Elastic CloudとElasticsearchは権限管理の仕組みが分かれているため、Cloud RoleではElasticsearchの権限を定義できません。それらを関連付けるため、 Elastic Cloudのアカウント情報を利用した設定 がElasticsearchのRole Mappingに標準で追加されました。 Role Mapping はElasticsearch内のRBACを実現する機能で、以前から存在しました。 その追加された設定では、Elastic Cloudアカウントでのアクセス時、Cloud Roleに対応するElasticsearchのRoleを付与します(以降、 Default Mapping 3 と表します)。この設定のように、Elastic Cloudのアカウント情報はElasticsearchから参照可能になっています。 以下では、Default Mappingと、私たちが追加したSSOアカウントに対するRole Mappingについて説明します。 Cloud Roleに対するRole Mapping Default Mappingが適用されると、Cloud Roleに対応するElasticsearch側のRole(以降、 Stack Role と表します)が付与されます。 Default Mappingにおいて、Stack Roleとして設定されるものは Elaticsearch標準のRole です。AdminとViewerについて、Cloud RoleとStack Roleのマッピング関係を下記に示します。 Cloud Role Stack Role Admin superuser Viewer viewer つまり、アクセスしたDeploymentについて、AdminのCloud Roleを持っていればStack Roleはsuperuserになります。ViewerのCloud Roleを持っていればStack Roleはviewerになります。 私たちは、バックエンドチームやMLチームにCloud RoleとしてViewerを付与しているため、Stack Roleとしてviewerが付与されます。このviewerでは操作が閲覧のみに制限されるため、そのまま利用するにはElasticsearchに関する権限が足りませんでした。バックエンドチームやMLチームは開発において、Index自体やIndex内のドキュメントを編集する必要があるためです。そこで、オリジナルのRoleを作成して適用することにしました。 先に、オリジナルのRoleをRole Mappingで適用する方法を説明します。その後、オリジナルのRoleについて説明します。 SSOアカウントに対するRole Mappingの追加 Elasticsearchにおいて、Elastic Cloudアカウントを対象としてオリジナルのRoleを付与するには、下記の画面のように設定します。 このUser IDを条件とする方法(以降、 Custom Mapping と表します)は、Elasticのサポートの方から教えていただきました。User IDはプロフィール画面やOrganization画面から確認できます。 Custom Mappingの説明のため、上記では画面をお見せしました。実際には、私たちはGUIではなくTerraformで管理しています。Elastic CloudのOrganizationはIaC化できませんでしたが、ElasticsearchのRole MappingはIaC化可能でした。公式のプロバイダー elastic/elasticstack を使用して、下記のように設定できます。 resource "elasticstack_elasticsearch_security_role_mapping" "zozo_default_role_mapping" { provider = elasticstack.cuslter_1 # Role Mappingを作成するクラスタを指定 name = "dev-zozo-default-role-mapping" enabled = true roles = [ # 付与するRoleを指定 elasticstack_elasticsearch_security_role.zozo_default_role.name ] rules = jsonencode ( { # Elastic CloudアカウントのUser IDが一致すればRoleを付与 any = [ for user_id in local.all_ec_user_ids : { field = { username = user_id } }] } ) } ここで説明したCustom Mappingの追加により、Elaticsearch内においてもアカウントを増やさず、チームごとの詳細な権限管理を実現しました。 ElasticsearchのオリジナルRole 以下では、Custom Mappingで付与するオリジナルのRoleと、そのIaC化について説明します。 共通のRoleと、チームごとのRoleの作成 オリジナルのRoleの権限設計は下記のように進めました。 各チームに、環境・Deployment・Indexごとに必要な操作をヒアリング ヒアリング結果の必要な操作と、 Cluster Privileges・Index Privileges との照らし合わせ 運用のしやすさを考慮したRole設計 権限設計の結果、オリジナルのRoleとして下記の2種類を作成しました。実際は本番・ステージング・開発の環境ごとに、さらに分かれています。 全員が最低限必要となる権限をまとめたRole チームごとに追加で必要となる権限をまとめたRole 1を作成した理由は、各チームが最低限必要となる権限がまとまっていたため、また、チームごとのRoleを冗長にせず管理しやすくするためです。2を作成した理由は、チームごとに権限が欲しいDeploymentやIndexが異なり、共通のRoleにはまとめられない権限が存在したためです。 TerraformによるIaC化 オリジナルのRoleもIaC化が可能であり、Role Mappingと合わせて下記のように設定しています。Indexに対する権限設定では、 dynamic blocks を使用して簡素化しています。 # 共通Role resource "elasticstack_elasticsearch_security_role" "zozo-default-role" { provider = elasticstack.cluster_1 name = "dev-zozo-default-role" cluster = local.zozo_default_role_cluster_privileges dynamic "indices" { for_each = local.zozo_default_role_index_privileges content { names = indices.value.names privileges = indices.value.privileges } } } # 共通RoleのRole Mapping resource "elasticstack_elasticsearch_security_role_mapping" "zozo_default_role_mapping" { provider = elasticstack.cluster_1 name = "dev-zozo-default-role-mapping" enabled = true roles = [ elasticstack_elasticsearch_security_role.zozo_default_role.name ] rules = jsonencode ( { # 全員のElastic CloudアカウントUser IDを条件に指定 any = [ for user_id in local.all_ec_user_ids : { field = { username = user_id } }] } ) } # バックエンドチーム用Role resource "elasticstack_elasticsearch_security_role" "backend_team_role" { provider = elasticstack.cluster_1 name = "dev-backend-team-role" cluster = local.backend_team_role_cluster_privileges dynamic "indices" { for_each = local.backend_team_role_index_privileges content { names = indices.value.names privileges = indices.value.privileges } } } # バックエンドチーム用RoleのRole Mapping resource "elasticstack_elasticsearch_security_role_mapping" "backend_team_role_mapping" { provider = elasticstack.cluster_1 name = "dev-backend-team-role-mapping" enabled = true roles = [ elasticstack_elasticsearch_security_role.backend_team_role.name ] rules = jsonencode ( { # XチームメンバーのElastic CloudアカウントUser IDを条件に指定 any = [ for user_id in local.backend_team_ec_user_ids : { field = { username = user_id } }] } ) } local.tf は下記サンプルのように設定しています。 # zozo_default_roleに関する部分のみ抜粋 locals { all_ec_user_ids = [ "1234567890" , "1234567891" , "1234567892" ] zozo_default_role_cluster_privileges = [ "monitor" , "monitor_snapshot" , "manage_index_templates" , "manage_ilm" , "manage_slm" ] zozo_default_role_index_privileges = [ { names = [ "sample-1-*" ] privileges = [ "all" ] } , { names = [ "sample-2-*" ] privileges = [ "create_snapshot" ] } , { names = [ "sample-3-*" ] privileges = [ "monitor" , "read" , "view_index_metadata" ] } ] } 結果 Elastic Cloudの権限管理は、最終的に下記の図のようになりました。SREチームは、Default MappingによりElasticsearch内でsuperuserが付与されるため、チーム用Roleの設定はありません。 今回の取組みにより、特権アカウント共用による下記の問題が解消されました。 アカウントの共有によるセキュリティ上のリスク 誤操作により重大な事故を引き起こす可能性 さらに、SSO対応・Role管理・IaC化を進めたことで下記の効果も得られました。 アカウント運用管理により発生するトイルの削減 Googleアカウントを用いたSSOログインによる利便性向上 RBAC利用による柔軟かつ統一された権限管理の実現 IaC化による権限管理の保守性の向上とヒューマンエラーの防止 おわりに 本記事では、Elastic CloudのRBAC機能を活用した、安全・効率的・柔軟な権限管理を紹介しました。 今回の取組みにより、特権アカウントの共用から脱却できました。しかし、下記のようにIaC化出来ていない箇所やリスクを排除できていない課題は残っているため、引き続き改善を進めていきます。 Elastic CloudのOrganization機能はTerraform未対応のためIaC化できていないこと マイクロサービスからElasticsearchへはsuperuserでアクセスしており、意図しない動作により関係のないIndexなどを削除してしまう可能性があること ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 hrmos.co Elasticsearchはバージョン8.2.3以降を利用しています。 ↩ Cloud Roleは正式名称ではありません。 公式ドキュメント では、 Elastic Cloud role と表現されています。本記事では、似た名称である Elastic Stack role が登場するため、読みやすさを考慮してそれぞれ Cloud Role 、 Stack Role と表現しています。 ↩ Default Mappingは正式名称ではありません。 公式ドキュメント では、説明の後、 the default mapping と表現されています。本記事では、独自に作成したRole Mappingと区別して読みやすくするため、それぞれ Default Mapping 、 Custom Mapping と表現しています。 ↩
こんにちは、カート決済部カート決済サービスブロックの林です。普段はZOZOTOWN内のカートや決済の機能開発、保守運用、リプレイスを担当しています。 弊社ではカートや決済機能のリプレイスを進めており、これまでにカート投入のキャパシティコントロールや在庫データのクラウドリフトを実現しています。 techblog.zozo.com techblog.zozo.com 本記事では新たにクレジットカード決済処理を非同期化したリプレイス事例を紹介します。 はじめに 背景・課題 非同期化のシステム構成 パターン1 - 完全非同期化パターン パターン2 - 非同期・同期切り替えパターン パターン3 - ポーリングパターン システム構成の決定 メッセージングサービスの選定 効果 今後の展望 まとめ さいごに はじめに 本章では、非同期化前のZOZOTOWNのクレジットカード決済を用いた注文処理の流れを説明します。 ZOZOTOWNの注文情報はオンプレミス環境で稼働しているSQL ServerのCartDBとFrontDBという2つのDBで管理しています。注文リクエストがくると、CartDBに注文データを作成します。この時に作成される注文データをZOZOTOWNでは仮注文と呼んでいます。仮注文の作成後、クレジットカード与信取得のためのリクエストを行います。与信取得後、仮注文の状態を与信完了に更新します。その後、CartDBからFrontDBへ注文データを移します。FrontDBの注文データを本注文と呼んでいます。 上記の処理の流れを以下の図に示します。 背景・課題 ZOZOTOWNでは、セールの終了時刻となる日曜から月曜にかけての日跨ぎ時に注文のピークを迎えます。この時の注文数が通常時の十倍前後となります。 次のグラフはあるセールの日の日跨ぎ時の注文数の推移です。 注文がスパイクしたタイミングで、レイテンシも上がっています。レイテンシが上がることで、ユーザーの待ち時間が長くなりUXの低下が発生します。以下が注文リクエストのレイテンシの推移です。 最初は、このレイテンシの悪化の原因がDBにあると考え、日跨ぎ時のDBのCPU使用率を調査しました。以下が同じ時間帯のDBのCPU使用率の推移です。 DB自体の負荷は余裕があり安定した状態となっていました。原因を深掘りしたところ、クレジットカードの与信リクエストの遅延でした。社外のAPIになるため、我々が直接レイテンシを改善することは困難でした。さらに、今後の成長を踏まえると、ユーザーの待ち時間がより長くなることで、UXの低下が予想されました。 そこで、UXの改善のための最初の対応としてクレジットカード決済処理の非同期化を進めることとしました。 非同期化のシステム構成 非同期化を行うにあたり以下の3パターンを検討しました。 与信処理から非同期化し、注文成立していない状態でも注文完了の画面を表示する 1のパターンを用意するが、同期処理にも切り替えられるようにする 与信処理から非同期化し、フロント側ではポーリングを行い注文成立したら注文完了の画面を表示する それぞれの詳細とメリット・デメリットを以下に記載します。 パターン1 - 完全非同期化パターン このパターンではユーザーとの注文完了コミュニケーションも非同期になります。処理フローは以下のとおりです。 Webサーバが注文リクエストを受け付ける 仮注文の作成 メッセージングサービスにエンキュー Webサーバは受付完了の画面を返す Workerがデキュー クレジットカードの与信確保のリクエスト CartDBに与信結果を保存 FrontDBに本注文を作成 5以降の処理が非同期になっています。ユーザーから見た場合、エンキューしたタイミングで画面が切り替わるため与信処理のレイテンシに影響されなくなります。 メリットとしては以下が挙げられます。 与信処理のレイテンシに影響されることなく注文リクエストが完了する キューとWorkerによるキャパシティコントロールができる デメリットとしては以下が挙げられます。 ユーザーは受付完了の画面に遷移できた場合でも与信NGで注文成立しない可能性がある 注文成立しない場合は支払い方法の変更通知などをメールで知らせる必要がある 支払い方法変更の猶予を持たせるために、在庫を確保しておく必要がある 注文数が少ない時間帯でも注文NGの場合に、ユーザーコミュニケーションが非同期になってしまう パターン2 - 非同期・同期切り替えパターン このパターンはパターン1の構成と既存の構成どちらも利用します。環境変数やアプリケーションロジックにより、既存パターンとパターン1の非同期化ロジックを切り替えられるようにします。 メリットとしては以下が挙げられます。 ピーク時はパターン1を利用できる 注文数が少ない時は、既存ロジックに切り替えることでピーク時以外はパターン1のデメリットをカバーする デメリットとしては以下が挙げられます。 同期と非同期どちらも必要になるので決済フローの改修コストが増える パターン3 - ポーリングパターン このパターンでは、パターン1と同様に与信処理は非同期化を行います。UXの低下は避けたいので、フロントからポーリングを行い、注文状態を確認するようにします。本注文が作成されたら、注文完了の画面に遷移します。与信等でエラーになった場合はカートトップに戻ります。処理フローは以下のとおりです。 Webサーバが注文リクエストを受け付ける 仮注文の作成 メッセージングサービスにエンキュー Webサーバはポーリング用のIDを返却 フロントはIDを用いてポーリング開始 Workerがデキュー クレジットカードの与信確保のリクエスト CartDBに与信結果を保存 FrontDBに本注文を作成 6以降の処理が非同期になっています。非同期で本注文が作成されると5でポーリングしているところで検知し、注文完了の画面に遷移します。ユーザーから見た場合は既存のUI/UXと変わりませんが、与信確保のリクエストはキャパシティコントロールされています。 メリットとしては以下が挙げられます。 キューとWorkerによるキャパシティコントロールができる 注文リクエスト自体は与信処理のレイテンシに影響されない 注文完了のユーザーコミュニケーションを非同期にする場合でも非同期部分のフローは変更なしで実現できる デメリットとしては以下が挙げられます。 ユーザーから見た時のUI/UXは変わらないので、注文リクエストをしてから注文完了の画面に行くまでに待ち時間がかかる システム構成の決定 3つのパターンの中から弊社ではパターン3を選択しました。理由は以下のとおりです。 現在のUXから低下させる部分を作りたくない パターン1ではピーク時以外のUXが低下してしまう 今後の改修コストの増加を極力避けたい パターン2では同期と非同期の二重開発になり改修コストが増える 注文完了のユーザーコミュニケーションを非同期にした場合、ビジネス要件の調整に時間がかかる 与信失敗した場合にその注文はどうするのか 支払い方法変更を受け付ける場合、どのくらいの期間受け付けるか 今後ピーク時だけ注文完了のユーザーコミュニケーションを非同期にすることも比較的容易に可能である パターン3でキャパシティコントロールを実現しつつ、並行してユーザーコミュニケーションの非同期化のためのビジネス要件の調整を進めることにしました。 メッセージングサービスの選定 データをキューイングするためのメッセージングサービスの検討もおこないました。今回候補に上がったAWSのサービスが以下の2つです。 Amazon Kinesis Data Streams (KDS) Amazon Simple Queue Service (SQS) それぞれの特徴やメリット、デメリットの詳細はカートリプレイスPhase1のテックブログで触れているのでご参照ください。 techblog.zozo.com クレジットカード決済の非同期化では以下4点を主な検討事項としました。 厳密な順序性はいらない スパイクにも耐えられる 1リクエストが詰まっても他に影響がでないようにしたい なるべくマネージドなサービスにしたい この中で採用の決め手となったのが、「1リクエストが詰まっても他に影響がでないようにしたい」でした。 与信処理では一部のリクエストだけが遅くなることがあります。そのため、特定のリクエストが遅延した場合でも他のリクエストへの影響を減らせる構成を検討していました。 KDSの場合シャード単位で読み出すため、特定のリクエストが遅延した場合の影響はシャード全体に渡ります。 SQSの場合は、特定のリクエストが遅延してもその他のWorkerがリクエストを処理できるので影響範囲を少なくできます。 上記の点から、本件ではSQSを採用することにしました。 効果 パターン3を実装した結果、仮注文作成までが注文リクエストの役割となったため、注文リクエスト自体のレイテンシは安定化し高速になりました。非同期化の実装前後での日跨ぎ時のレイテンシの比較は以下のとおりです。 また、SQSとWorkerでキャパシティコントロールができるようになった結果、与信リクエストの遅延がなくなりました。さらには、どれくらい与信キューに溜まっているかを可視化できるようになりました。以下は、日跨ぎ時に滞留しているキュー数の推移です。 滞留しているキュー数が明確になったことで、今後のZOZOTOWNの成長率との組み合わせでどれくらいキューに溜まり、注文完了までどれくらいかかるかの計算が可能になりました。 今後の展望 今回与信のキャパシティコントロールは実現したものの、UXの改善には至りませんでした。現状は注文完了の画面表示までのユーザーの待ち時間はリプレイス前と変わりません。今後ZOZOTOWNが成長していくと、必ず待ち時間は延びていきます。そのため、ユーザーコミュニケーションを非同期にしていく必要性があります。ユーザーコミュニケーションを非同期にした場合、以下の検討事項があります。 与信失敗時の注文データの取り扱い 既存では失敗時はカートに商品を戻している 再度注文を行なってもらうより支払い方法変更できることが望ましい 支払い方法を変更できる場合はどれくらいの期間在庫を確保するか 在庫の確保期間が長ければ売り上げ損失になる可能性がある 在庫の確保期間が短ければUXの低下を招く可能性がある これらの検討事項はシステムだけではなく、ビジネス要件とも大きく関係してきます。現時点ではまだ検討中で、ビジネス側と密に連携し、今後の方針を決めていきたいと考えています。 まとめ 本記事ではクレジットカード決済の非同期化のシステム構成を紹介しました。非同期化したことにより、与信処理のキャパシティコントロールを行うことができました。非同期化を検討している方がいれば、ぜひ参考にしてみてください。今後はユーザーとのコミュケーション部分も非同期で行えるようにし、UXの向上を目指していきたいと考えています。 さいごに ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、MA部の林( @hayash__p )です。 私達のチームでは、メール、LINE、Push通知、サイト内お知らせなどでユーザにZOZOTOWNのセールや新着商品を紹介するといった、マーケティングに関わるシステムを開発しています。これまで、配信チャネルや配信内容ごとに個別最適化したシステムを開発していましたが、それらを一新したマーケティングプラットフォームを作ることになりました。新しいマーケティングプラットフォームであるZOZO Marketing Platform(以下、ZMP)の概要については以下のテックブログをご覧ください。 techblog.zozo.com 本記事では、マーケティングプラットフォームのリプレイスにあたり、フロントエンドエンジニアとして取り組んだことを紹介します。 目次 はじめに 目次 背景 ZMPの管理画面モジュール MPマネージャー 技術選定 MPマネージャーと関連システムの構成について MPマネージャーでのフォルダ構成と役割について OpenAPI用のフォルダ フロントエンドAPI用のフォルダ 共通コンポーネント用のフォルダ 開発中に出てきたApp Router関連の課題 Serverからのfetchでは、リクエストオブジェクトにアクセスできない Server Actionsを後から導入したため、リファクタリングの余地がある エラー画面が描画された場合も、ステータスコードが200で固定になってしまう テストライブラリがServer Componentに未対応 まとめ ZMPの振り返り・今後 MA部フロントエンドとしての振り返り・今後 背景 ZMPでは、マーケターがキャンペーンの管理・運用をより手軽に行うため、新規に管理画面を作ることになりました。ZMPの初期リリース段階(以下、フェーズ1)での管理画面の要件は以下でした。 メール、Push通知による配信の設定ができること 拡張を前提とした画面であること(後々、LINE、サイト内お知らせなど、既存システムで利用している配信チャネル全てを網羅する想定のため) 画面には配信の関係者のみがアクセスできること 今まで、管理画面のないマーケティングシステムのバックエンド開発が中心だったこともあり、MA部にはフロントエンドに特化したチームがありませんでした。しかし、ZMPには複数のマーケティングシステムを集約するような画面が必要でした。そのため、フロントエンド、バックエンドそれぞれに主軸を持った開発者同士のチームで、ZMPの管理画面用Webアプリケーションを構築することになりました。 ZMPの管理画面モジュール MPマネージャー ZMP管理画面のフロントエンドモジュールを、Marketing Platform Managerを一部省略し、MPマネージャーと呼んでいます。ここからは、MPマネージャーで採用した技術、関連システムを含む構成、MPマネージャー自体の構成について紹介します。 技術選定 フロントエンドフレームワークは、ZOZOTOWNのWebホーム画面でも利用されているReact・Next.jsを採用しました 1 。また、Next.jsのルーティングシステムは、技術選定の時期にbetaとして登場したApp RouterをZOZO社内で初めて採用しました。当時はPage Routerがstableでしたが、Page Routerを選んだ場合、App Routerがstableとなった際に切り替えコストがかかってしまうことを考慮したためです。 私自身は当時の選定には携わっていなかったのですが、実際に手を動かして今回開発をしたところ、App Routerに関する課題がいくつか発生しました。具体的な例を挙げて後述します。 MPマネージャーと関連システムの構成について MPマネージャーとその関連システムの構成はこのようになっています。 内製のAPI(以降、バックエンドAPI)は配信設定のロジックはもちろん、DBやストレージ、外部APIとの疎通を担当しています。そんなバックエンドAPIから取得したデータを表示したり、フォームで入力したデータをバックエンドAPIに送信したりするのがMPマネージャーの担当領域となっています。 バックエンドAPIは同時並行で開発しており、バックエンドAPIとフロントエンドのインタフェースを共通化し相互連携を強化するために、OpenAPIで定義を作り共用しました。MPマネージャーではその定義を元に Prism でモックサーバーを立てて開発を進めました。 MPマネージャーでのフォルダ構成と役割について App Routerを使った場合のフォルダ構成の情報が少ないため、App Routerを使って開発する方の参考になればと思い、MPマネージャーでのフォルダ構成を紹介します。 MPマネージャーでは、 bulletproof をベースとしつつ、 Next.jsのルーティングルール も尊重して、フォルダ構成を設計しました。 下記は、MPマネージャーのフォルダ構成です。このうち、bulletproof・App Router・OpenAPIの組み合わせにより、特殊な実装・構成となった部分をピックアップして紹介します。 . ├── src/ │ ├── api/ │ ├── app/ │ │ ├── _api/ │ │ │ └── usersServer.ts │ │ ├── _components/ │ │ │ └── UserIcon.tsx │ │ ├── api/ │ │ │ └── users/ │ │ │ └── route.ts │ │ ├── users/ │ │ │ ├── _components/ │ │ │ │ └── UserForm.tsx │ │ │ └── page.ts │ │ ├── error.tsx │ │ ├── layout.tsx │ │ ├── loading.tsx │ │ └── page.tsx │ └── _components/ │ └── Button.tsx ... OpenAPI用のフォルダ まずは、 api に関連するフォルダについてです。 └── src/ └── api/ こちらは、 OpenAPI Generator を用いてOpenAPIの定義ファイルをTypeScript用に自動生成したファイルの置き場です。ルーティングに含みたくなかったこと、型定義のファイル群でありアプリケーションそのものと切り離したかったことから、 src/app 配下ではなく、 src 直下に専用のディレクトリを設けることにしました。 フロントエンドAPI用のフォルダ MPマネージャーでは、 Route Handlers を用いてフロントエンド側にもAPIを構築しました(以降、フロントエンドAPI)。ここからは、フロントエンドAPI用のフォルダについて紹介します。 └── src/ └── app/ └── api/ └── users/ └── route.ts フロントエンドAPIを用意したのは、Client ComponentとServer Component、両方からのアクセスとバックエンドAPIを繋ぐインタフェースを用意したかったためです。フロントエンドAPI( src/app/api/[モデル名]/route.ts )は、OpenAPIを利用してバックエンドAPIにアクセスする実装となっています。 // src/app/api/users/route.ts import { NextResponse, NextRequest } from 'next/server' ; import { UserApi } from '@/api' ; export const dynamic = 'force-dynamic' ; export async function GET ( request : NextRequest ) { try { const api = new UserApi(); const users = await api.getUsers(); return NextResponse.json(users); } catch (error) { // エラー処理 } } また、そのフロントエンドAPIを叩くためのfunctionをまとめたのが、 src/app/_api フォルダです。 └── src/ └── app/ └── _api/ └── usersServer.ts 'use server' ; import { fetchWithJwtIap } from '@/lib/typescript-fetch-on-server-component' ; export const getUserList = async () => { const res = await fetchWithJwtIap( // MEMO: NEXT_PUBLIC_HOST = フロントエンドのホスト ` ${ process .env.NEXT_PUBLIC_HOST } /api/users` , ); ... } ; page.tsx にて、下記のように呼び出して利用します。 // src/app/users/page.tsx import { getUserList } from '@/app/_api/usersServer' ; async function Page () { const users = await getUserList(); return < UserList users = {users} />; } ; 共通コンポーネント用のフォルダ MPマネージャーでは、以下のルールで共通コンポーネントのフォルダを3通り用意しました。 └── src/ └── _components/ └── Button.tsx bulletproofを参考にしたため、基本的にはこの src/_components に共通コンポーネントを配置しています。 └─ src/ └── app/ └── _components/ └── UserIcon.tsx こちらの src/app/_components は、OpenAPIに依存した共通コンポーネントの置き場です。先述の src/api から何かしらのファイルを読み込み、かつ複数箇所で使われる場合はこちらに配置します。 └── src/ └── app/ └── users/ └── _components/ └── UserForm.tsx 最後に、 src/app/[モデル名]/_components は特定のページでのみ使うコンポーネントの置き場です。使用するページが限定的であれば、近くに配置した方が管理しやすいため、このフォルダも用意しました。 このフォルダ構成にしたことで、OpenAPIひいてはバックエンドAPIに依存するコードかどうかや広範囲で使われているコンポーネントかどうかを意識しやすくなりました。 開発中に出てきたApp Router関連の課題 ここまでは、MPマネージャーを俯瞰的に解説してきました。ここからは、開発中に出てきたApp Router関連の課題とその解決法について説明します。 技術選定時はbetaで始まり、開発中にstableとなったApp Routerでしたが、stableとなった後でも利用事例が少なく、GitHubのコメントだけが頼りになることもしばしばありました。ここからは、開発中に詰まった点を実際の例を用いて紹介していきます。これからApp Routerでの本番アプリケーション開発を検討している方・すでに開発を始めている方の参考になれば幸いです。 Serverからのfetchでは、リクエストオブジェクトにアクセスできない MPマネージャーでは、「画面には配信の関係者のみがアクセスできる」ように、想定外のユーザーがアクセスできないよう IAPによる認証 を実施し、バックエンドAPIに認証情報を送っています。このIAPによる認証情報は、カスタムヘッダーに格納されています。 しかしApp Routerでは、Serverからのfetchではリクエストオブジェクトにアクセスできないという仕様により、カスタムヘッダーが取得できませんでした。 GitHub.com // src/app/users/page.tsx export const dynamic = 'force-dynamic' ; export default async function Page () { const users = await fetch ( ` ${ process .env.NEXT_PUBLIC_HOST } /api/users` , ); ... } // src/app/api/users/route.ts import { NextRequest } from 'next/server' ; export async function GET ( request : NextRequest ) { console .log(request. headers . get ( 'X-Goog-IAP-JWT-Assertion' )); // null } こちらは、 Discussionsのコメント を参考に、fetchの前に next/headers を読み込むことで解決しました。 // https://github.com/vercel/next.js/discussions/44270#discussioncomment-6064242 の対策のため、constで定義 const headers = import( 'next/headers' ); export const fetchWithJwtIap = async ( requestInfo : RequestInfo , init? : RequestInit | undefined , ) => { const headersList = ( await headers).headers(); const jwtIap = headersList. get ( 'X-Goog-IAP-JWT-Assertion' ); return fetch (requestInfo, { ...init, headers : { ...init?. headers , [ 'X-Goog-IAP-JWT-Assertion' ] : jwtIap, } , } ); } ; 今回はカスタムヘッダーを取得する実装になりましたが、Cookieなどの他のリクエストオブジェクトにアクセスしたい場合も、同じように next/headers を読み込むことで実装できそうです。 Server Actionsを後から導入したため、リファクタリングの余地がある MPマネージャーでは、フォームデータの送信はClient Component、初回表示用のデータ受信はServer Componentと、両Componentからfetchを使用しています。ただ、前述の通り、Client ComponentからのfetchとServer Componentからのfetchにて、認証・認可のためのコードに若干の違いが発生してしまいました。そこで開発初期は、ファイルを2つ用意してClient Componentでは [モデル名]Client.ts を、Server Componentでは [モデル名]Server.ts を呼び出す実装にしました。 Client Componentでのfetchの例は次のとおりです。 // src/app/_api/usersClient.ts export const postUser = async (...) => { const res = await fetch ( `/api/users` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' , } , body : ... } , ); ... } ; // src/app/users/new/page.tsx import { UserForm } from '@/app/users/_components/UserForm' ; async function Page () { return < UserForm />; } ; // src/app/users/_components/UserForm.tsx 'use client' ; import { postUser } from '@/app/_api/usersClient' ; async function UserForm () { return ( < form > ... < button type = "submit" onClick = {postUser (...) } /> </ form > ); } ; また、Server Componentでのfetchの例は次のとおりです。 // usersServer.ts 'use server' ; import { fetchWithJwtIap } from '@/lib/typescript-fetch-on-server-component' ; export const getUserList = async () => { const res = await fetchWithJwtIap( // MEMO: NEXT_PUBLIC_HOST = フロントエンドのホスト ` ${ process .env.NEXT_PUBLIC_HOST } /api/users` , ); ... } ; // src/app/users/page.tsx import { getUserList } from '@/app/_api/usersServer' ; async function Page () { const users = await getUserList(); return < UserList users = {users} />; } ; 後々、 Server Actions がリリースされ、Client ComponentでのfetchをServer側の処理として扱うことができるようになりました。その結果、Client Componentからも [モデル名]Server.ts が呼び出されるようになり、現在は [モデル名]Server.ts だけが残っています。 // usersServer.ts 'use server' ; import { fetchWithJwtIap } from '@/lib/typescript-fetch-on-server-component' ; export const getUserList = async () => { ... } export const postUser = async (...) => { const res = await fetchWithJwtIap( ` ${ process .env.NEXT_PUBLIC_HOST } /api/contents` , { method : 'POST' , headers : { 'Content-Type' : 'application/json' , } , body : ..., } ); } ; // src/app/users/new/page.tsx import { UserForm } from '@/app/users/_components/UserForm' ; async function Page () { return < UserForm />; } ; // src/app/users/_components/UserForm.tsx 'use client' ; import { useTransition } from 'react' ; import { postUser } from '@/app/_api/usersServer' ; async function UserForm () { const [ , startTransition ] = useTransition(); const handleClick = (...) => { startTransition( async () => { await postUser(...); } ); } ; return ( < form > ... < button type = "submit" onClick = {handleClick (...) } /> </ form > ); } ; 開発途中で追加された機能を導入したこともあり、試行錯誤を経てこの構成に辿りつきました。そのため、バックエンドAPIが求める形に入力値を加工する処理などがServer ActionsとRoute Handlersに点在してしまっています。バックエンドAPIにより近い位置で加工した方が良いと考えているため、将来的にはRoute Handlersに処理を集約できたらと考えています。 エラー画面が描画された場合も、ステータスコードが200で固定になってしまう Next.jsには本番環境ではServer Componentで起きたエラーはマスキングされてClient Componentに送られるという仕様があります。 nextjs.org そのため、Server Componentで throw Error をするとエラーメッセージの詳細が取得できなくなってしまいました。特にバリデーションエラー時に、何が原因でエラーになったのかをClient Componentで判別できず困りました。 MPマネージャーでは、バリデーションエラーなどの想定したエラーであれば、200系のステータスコードとエラー理由を含んだJSONをレスポンスとして返す実装にして乗り越えました。個人的には、エラー時にエラー系のステータスコードを返す実装が好ましいと思っているため、今後のアップデートを注視していきます。 また、MPマネージャーではSuspenseによるStreamingを利用していたのですが、これがステータスコードに想定外の影響を与えていました。Suspenseを使うと、データのfetch中にローディングマークを表示できます。ですが、画面の描画が始まっている = ルーティングは正常に完了していることから、fetch中にエラーが起こってエラーページが呼び出された場合も、200の正常系のステータスコードが返されていたのです。 nextjs.org この問題には有効な解決策を打てないままでした。しかし、画面の描画が始まった後のエラーはSentryでキャッチできます。また、MPマネージャーそのものが落ちていないかの死活監視は、ロードバランサのステータス監視で代用できます。以上の設定があれば、MPマネージャーが何かしらエラーで落ちてしまった場合も検知できると判断しました。 現在、GitHubの Discussion にて議論されているため、こちらもアップデートを注視していきます。 テストライブラリがServer Componentに未対応 MPマネージャーでは、テストライブラリとしてjestやtesting-libraryを採用しました。最初はClient Componentのユニットテストを中心に実装していたため、特に問題なく利用できました。ですが、Server Componentのテストには未対応でした( testing-libraryのIssue )。そのため、Server Componentのユニットテストが実装できずにいました。 Issueでは 回避策 としてE2Eテストが上げられていました。Next.jsでは playwrightのE2Eテストがサポートされている ことから、Server Componentが絡むコードは、E2Eテストで実装することとなりました。これにより、網羅的にテストコードを書くことができ、品質を担保できるようになりました。 まとめ ZMPの振り返り・今後 2024年1月から、ZMPを利用して設定したデータを元にメール、Push通知の配信ができるようになりました。 このように、画面上でメール、Push通知による配信の設定ができるようになっています。 ここまでで、フェーズ1に要件として上がっていた全ての項目が達成されています。 メール、Push通知による配信の設定ができること 拡張を前提とした画面であること(後々、LINE、サイト内お知らせなど、既存システムで利用している配信チャネル全てを網羅する想定のため) 画面には配信の関係者のみがアクセスできること 今後は、フェーズ2以降に予定されている機能の画面開発を進めていきます。 LINE、サイト内お知らせ用の配信機能 配信設定のレビュー機能 配信システムではない既存のシステムのリプレイス 参考: Digdagのワークフローをマーケターが実行するためのシステム 最終的に、ZMPには多種多様なマーケティングに関わるシステムが集約されることになるため、マーケターにとって操作しやすい管理画面が提供できるよう、開発を続けていきます。 MA部フロントエンドとしての振り返り・今後 MPマネージャーでは、App Routerという新しく利用例が少ない技術を採用しました。実際に開発を始めてみると、日本語ドキュメントが少ないこと、社内初の利用だったため知見が共有できなかったことから、MA部のフロントエンドとしては挑戦的な技術選定になってしまったように思います。具体的には、リクエストヘッダーを操作したり、テストを拡充したりなど、本番利用では欠かせない機能を開発している時に壁を感じることが多かったです。 しかし、今回出てきた問題点は、GitHubのIssueやDiscussionを参考に対応策を編み出せたため、リリースまで辿り着きました。また、認証など、周辺モジュールと連携する土台が完成しています。以上から、App Routerでの開発体制が軌道に乗り始めたところだと考えており、ZMPでは今後もApp Routerを使い続ける予定です。 今後は、既存システムのZMPへの移行が属人化しないよう、MAのフロントエンドのチーム体制を整えていきたいと考えています。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com ZOZOTOWNのWebホーム画面をNext.jsでリプレイスして得た知見 - ZOZO TECH BLOG ↩
はじめに こんにちは。WEARフロントエンド部Webチームの藤井です。私たちのチームでは、 WEAR のWebサイトのリプレイスと新規機能の開発を並行して進めています。これらの開発を推進する中で、Pull Requestのレビュー負荷を軽減し、開発生産性を向上させるための取り組みを行なってきました。本記事では、その中で効果的だった取り組みについてご紹介します。 目次 はじめに 目次 背景と課題 レビューの体制の薄さ スコープの広さ 仕様把握の負担 対応内容についての説明不足 処理の複雑性 仕様の抜け漏れ 動作確認の手間 課題解決に向けた取り組み レビュー体制の見直し Pull Requestを小さくする Issueを小さくする Pull Requestの粒度について明文化する 機械的なチェックの拡充 ESLintルールの拡充 Visual Regression Testの拡充 Pull Requestテンプレートの拡充 事前の仕様・設計相談 仕様・デザインのレビュー 実装前の設計相談 コーディングルールとしての蓄積 レビューしやすい環境の整備 Preview環境を使った動作確認 Storybookのレビュー活用 定期的な振り返り 取り組みの結果 サイクルタイム比較 Before After 主要スタッツ比較 Before After まとめ おわりに 背景と課題 WEARフロントエンド部では、開発速度の向上を目指して「Pull Requestのオープンからマージまでの平均時間 24時間以内」という目標を掲げています。また、月次のKPTや週次の1on1の中で、Pull Requestのレビューに対する負荷が高いという課題が浮上しました。レビュー負荷が高いと感じる理由や当時の傾向として、以下のような意見がありました。 レビューの体制の薄さ 社員3名、内定者アルバイト2名のメンバー構成で、基本的には社員2名のapproveでマージをするルールで運用しています。このルールで全てのPull Requestを運用するには社員1人あたりのレビュー数量的に負荷が高いという課題がありました。 スコープの広さ 1つのPull Requestに複数の対応内容やついでのリファクタリング、バグ修正などを含めていることがありました。その結果、Pull Requestのスコープが広がり、何の目的でどの部分のコードを修正しているかを把握するのに負荷がかかっていました。 仕様把握の負担 各メンバーそれぞれ違う画面のリプレイスや新規機能の開発を行なっているため、レビュアーが他の担当の画面仕様を把握するのに時間がかかってしまうことがありました。特に画面のリプレイスの場合には仕様書がないため、各レビュアーが既存画面や既存コードから調査するということが発生していました。 対応内容についての説明不足 Pull Requestの対象範囲がレビュアーに伝わらず、レビュイーが意図していない対応箇所を指摘することがありました。 処理の複雑性 コンポーネントや関数の設計が複雑になってしまい、レビュアーが処理を理解するのに時間がかかってしまうことがありました。また、レビュー時点で処理の複雑性に気づき、設計と実装の手戻りが発生することもありました。 仕様の抜け漏れ レビュー時に仕様の考慮漏れに気づいたり、仕様やデザイン面での相談が発生したりすることがあり、レビューが中断したり、実装の手戻りが発生してしまうことがありました。 動作確認の手間 ブランチをチェックアウトし、ローカル開発環境を立ち上げてUIのチェックや動作確認に時間がかかっていました。 課題解決に向けた取り組み 前述した課題に対してチーム内で改善策を話し合い、実践し、その効果を振り返りながら改善を続けました。その中で効果があったと考える取り組みを以下に紹介します。 レビュー体制の見直し レビューするメンバーが少ない中でも品質を担保しつつ効率的に運用していくためにレビュー体制を見直しました。例えば、画像差替えや文言修正など1名によるレビューでも品質が担保できる軽微な修正に関しては1名のapproveでマージするなど例外ルールを設けました。また、内定者アルバイトのメンバーにも少しずつレビューに入ってもらい、後進育成に取り組んでいます。 Pull Requestを小さくする Pull Requestの変更内容が大きいと、レビューに時間がかかります。例えば、変更内容の把握に時間がかかったり、まとまった時間がとれるまでレビュー着手できず遅れたりするということがありました。Pull Requestを小さくすることによって、隙間時間にもレビューができ、レビューに対する心理的な負担も軽減しました。Pull Requestを小さくするためには以下のような取り組みを行いました。 Issueを小さくする 1つのIssueに1つのPull Requestを紐づけることを基本とし、まずはIssueの単位を小さくするように心がけました。毎日の朝会でIssueを共有し、必要に応じて分割することで、Pull Requestを小さくなるように定着させていきました。 Pull Requestの粒度について明文化する IssueやPull Requestを小さくするといっても人それぞれ感覚が異なるので、Pull Requestの粒度について明文化するようにしました。例えば、以下のようなことを明示しました。 新規に作成するコンポーネントや関数は基本的に1つで1つのPull Requestとする ファイルの移動やリファクタリングは機能修正のPull Requestと分けて作成する IssueやPull Requestを小さくすると、仕様の全体像や背景がわかりづらいという問題がありました。その問題を回避するために、 タスクリスト 用のIssueを作成し、そこから細かいIssueを作成しています。よく使う汎用的なタスクリストについては Issueテンプレート を設定しておくことで、テンプレートから簡単にタスクリストを作成できます。 機械的なチェックの拡充 機械的にチェックできる項目はLinterや自動テストに任せることで、レビューの効率の向上を図りました。具体的には以下の取り組みを行いました。 ESLintルールの拡充 元々、ESLintのメインのルールセットに eslint-config-airbnb-typescript を採用していましたが、コードの複雑性もチェックするために以下のルールも追加しました。これにより実装段階で処理が複雑になることを回避でき、その結果、レビューの負担が軽減しました。 complexity max-statements max-statements-per-line max-depth max-lines max-lines-per-function max-params max-nested-callbacks また、最近ではアクセシビリティに関するチェックを行うプラグイン eslint-plugin-jsx-a11y も追加しました。こちらも実装段階で機械的にチェックできるので、レビューの負担軽減につながりました。 Visual Regression Testの拡充 以前から導入していたPlaywrightやChromaticによるVisual Regression Testにおいて、対象画面や細かいデータパターンなどテスト対象を拡充しました。UI面で意図しない変更が生じていないかのチェックを自分たちで比較する必要がなくなり、レビューコスト削減につながりました。また、機械的にチェックすることで人間の目による見落としがなくなり、品質向上にもつながりました。 Pull Requestテンプレートの拡充 仕様把握やUIチェックで時間がかかっていた課題を解決するためにPull Requestテンプレートを拡充しました。例えば、リプレイス前の画面URL、既存コードのGitHub URL、Figmaやリプレイス前の画面との比較スクリーンショットを提示するようにしました。そうすることで、各レビュアーが既存仕様の調査や画面を比較する手間がなくなり、レビュー負荷を軽減できました。現在は以下のようなPull Requestテンプレートで運用しています。 ## 対応内容 <!-- やったことを簡潔に --> ### Issue closes # <!-- このPRをマージすることで解決するIssue --> ref. # <!-- その他このPRに関係するIssue/PR --> ### 仕様書 <!-- Confluence、JIRA、リプレイス前のURLやsourceコード --> ### この PR でやらないこと ## 比較スクショ ## 影響範囲 <!-- この変更によって影響するページなど --> ## レビュー観点 <!-- このPRで特に注視して確認して欲しいこと --> 事前の仕様・設計相談 設計や実装の手戻りを減らすために、実装へ取り掛かる前に仕様や設計に関して相談するようにしました。具体的な取り組みは以下の通りです。 仕様・デザインのレビュー 実装に入る前段階で、チーム内で仕様やデザインをレビューするようにしました。特に画面のリプレイスの場合、既存の仕様が古くなっていたり、他の画面とのUIや仕様の一貫性が欠けていたりすることがありました。これらの問題を解消するために、実装前にPMやデザイナーと相談して仕様の見直しや利用率が極端に低い機能を削除しました。また、UIや仕様の統一を図り、共通コンポーネントを利用できるようにしました。これにより、実装段階やレビュー段階での仕様やデザインの見直しが減り、開発の効率化やレビュー負荷の軽減につながりました。 実装前の設計相談 毎朝行なっているタスク・進捗共有や毎夕行なっているレビュー会において、設計方針や実装中の悩みについても相談するようにしました。これにより、レビュー時に設計から見直すような手戻りが減り、実装やレビュー時間の短縮につながりました。 コーディングルールとしての蓄積 設計相談で決まった設計方針やレビュー中にでた指摘事項などで汎用化できるものについては、コーディングルールや命名規則としてGitHub Discussionsに記載し、ドキュメント化しました。方針が明文化されていることによって実装やレビューにおける迷いが軽減されました。 レビューしやすい環境の整備 以前はローカルにブランチをチェックアウトして動作確認を行なっていたのですが、Preview環境やStorybookを活用することによって、簡単に動作を確認できるようになりました。 Preview環境を使った動作確認 有難いことに、定期的に弊SREチームが各チームで困っていることを吸い上げて開発環境の改善を行なってくれており、そのうちの1つがPreview環境でした。Pull Request毎ごとにPreview環境を起動が可能になり、Preview環境内の対象画面のURLを共有するだけで容易に動作を確認できるようになりました。Preview環境の詳細についてはSREチームの山岡による以下の発表資料をご覧ください。 speakerdeck.com Storybookのレビュー活用 コンポーネント単位でのUI確認はStorybookを活用しています。Storybookを使用することで、ページに組み込む前のコンポーネントや、異なるデータや状態のパターンについてもそれぞれ確認できます。また、StorybookをChromatic上にデプロイしているので、ローカルでStorybookを起動せずともWeb上で閲覧でき、Chromaticにて変更点の差分も容易に確認できます。 定期的な振り返り 週に1回、Findy Team+を見て、オープンからマージまでの時間や平均の変更行数をチェックし、メンバー全員で振り返りをしました。数値が良い時、悪かった時それぞれ要因を考え、次の週へ向けて良かったことの継続や改善をしました。 取り組みの結果 前述した取り組みについては前々から行なっていたものもありますが、課題感をもってチーム全体として積極的に取り組んだのは2023年4月半ばでした。Findy Team+のレビュー分析を見ると2023年4月後半あたりにオープンからマージまでの平均時間が減り、グラフが安定してきている傾向が見られます。お休みや研修期間などもあり、マージまでの時間が少し増えている期間もありますが、2023年4月以前と比較すると時間も短く、安定しています。 サイクルタイム比較 以下のBefore、Afterは取り組みの効果が顕著に現れた2023年5月からの半年間とそれ以前の半年間のサイクルタイム分析の数値です。オープンからレビューまでの平均時間は少し伸びていますが、それ以外の時間と全体の合計値は改善し、合計で12時間も短縮できました。 Before After 主要スタッツ比較 オープンからマージまでの時間もおよそ4時間短縮でき、目標の24時間に近づくことができました。また、平均の変更行数も81行となり、Pull Requestが小さくなっています。 Before After まとめ チーム全体で課題解決に取り組み、さまざまな小さな改善を積み重ねることで、Pull Requestのレビュー負荷の軽減や開発プロセスの効率化につながったと実感しています。一時的な改善に終わらせないためにも、今後も継続的な取り組みが必要であり、長期的に持続可能な開発体制を築いていくことが大切だと思います。 おわりに ZOZOでは、一緒にサービスを開発する仲間を募集しています。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、検索基盤部の倉澤です。検索基盤部では、検索機能に必要なデータを生成するバッチシステムの開発や運用を担当しています。また、ユーザーのニーズやサービスの成長に合わせてリアーキテクチャを行うこともあります。今回は、リアーキテクチャを繰り返し行う中で見えてきたバッチシステムの内部設計の品質を高める・標準化するためのポイントを紹介します。 今回、バッチシステムの内部設計をソフトウェアのアーキテクチャ特性(品質特性とも呼ばれる)に基づいて説明します。 ソフトウェアのアーキテクチャ特性とは、非機能要件や品質特性と同じ意味を指しますが、 「ソフトウェアアーキテクトの基礎 (Fundamentals of Software Architecture)」 ではシステムが成功するために必要な運用と設計の基準として定義されています。アーキテクト特性には拡張性(Scalability)やメンテナンス容易性(Maintainability)、再利用性(Reusability)などがあり、イリティ(ility)とも呼ばれます。 リアーキテクチャによって得たバッチシステムの内部設計のポイント以下3点を紹介します。 ビジネスロジックはなるべく1つのコンポーネントに集約する データの抽出、変換、統合、削除などの各処理をなるべく小さなコンポーネントに分割する データリソースが増えた場合を考えデータ抽出処理と変換処理を独立させる 検索基盤部では、内部設計の品質を高めるためにDesign Docを活用しています。どのような項目に何を意識して記載しているかを最後に紹介したいと思います。 目次 はじめに 目次 背景 リアーキテクチャ前のバッチシステムの課題 良くない点1: ビジネスロジックが複数のコンポーネントに点在している なぜ良くないのか 課題1: メンテナンス容易性(Maintainability) 課題2: 拡張性(Scalability) どう解決したのか 良くない点2: 1つのコンポーネントに複数の責務を持つ処理が混在していた なぜ良くないのか 課題1: 再利用性(Reusability) 課題2: サポート容易性(Supportability) 課題3: テスト容易性(Testability) 課題4: 凝集度 どう解決したのか 良くない点3: データ抽出処理が独立していない なぜ良くないのか 課題1: 拡張性(Scalability) どう解決したのか Design Docの活用 なにを書くか Goal / Non-Goal 用語集 全体のシステム構成図 コンポーネントの定義 効果 まとめ おわりに 背景 ZOZOTOWNの検索機能の1つにユーザーが入力したクエリの意図を解釈する機能があります。この機能は、ユーザーが入力したクエリを解釈し、検索結果の品質を向上させることを目的としています。例えば、ユーザーが「ジャケット」というクエリで検索した場合、音楽に関連した情報ではなく洋服のカテゴリーであることを解釈し、検索結果の品質を向上させます。 この機能を実現させるために、ファッション用語を定義した辞書を作成するバッチシステムを開発しました。検索機能はバッチシステムが作成した辞書を元にルールベースでクエリの意図解釈を行います。この意図解釈を行う辞書は、社内外にある様々なデータソースからタームとその意味を抽出し、検索機能が利用できる形式に変換したり、ZOZOTOWN独自のビジネスロジックを適用したりすることで生成されます。 本文中の「ビジネスロジック」とは、辞書で定義しているタームの意味を恣意的に変更することを指しています。例えば、ZOZOTOWNで扱っているブランド名には一般名詞に該当するものがあり、ZOZOTOWNで扱う辞書を生成する際には該当のブランド名を一般名詞ではなく固有名詞として扱うことがあります。 今回紹介するバッチシステムは上記のようなドメイン特性がありますが、データ抽出・変換・統合・削除など一般的なバッチシステムの特性を持っています。そのため、本記事で紹介する内容は今後のバッチシステムの開発・運用においても参考になると考えています。 以下がリアーキテクチャ後のバッチシステムの全体像です。 上記のバッチシステムは主に2つに分かれています。 ビジネスロジックが介入していない辞書を作成するバッチシステム 各サービスのビジネスロジックが介入している辞書を作成するバッチシステム ビジネスロジックが介入していない共通処理をまとめたバッチシステムが前段にあることで、各サービスのバッチシステムはビジネスロジックの実装に集中できます。また、辞書を活用するサービスが増えた場合でも拡張しやすい構成となっています。 なぜ、上記のようなバッチシステムのアーキテクチャを採用したかを、次章のリアーキテクチャ前に存在していた課題を紹介することで説明します。 リアーキテクチャ前のバッチシステムの課題 リアーキテクチャのきっかけは、バッチシステムが生成する辞書をZOZOTOWN以外のサービスで使用したいという要望が発生したことでした。リアーキテクチャ前は、ZOZOTOWNの検索機能で利用することだけを考えた設計になっており他サービスはZOZOTOWNのビジネスロジックが介入した辞書を使うしかありませんでした。 以下がリアーキテクチャ前のバッチシステムの全体像です。 上記の構成図からわかるようにコンポーネントから出力される成果物は全てビジネスロジックが介入しています。ZOZOTOWNのビジネスロジックが介入している成果物を他のサービスで展開することは適していません。 これから紹介する課題はリアーキテクチャ前のバッチシステムが良くない設計だったというわけではなく、要求に応えるためのリアーキテクチャによって発生した課題です。当時の状況を考えるとリアーキテクチャ前のバッチシステムの構成はシンプルな設計だったと言えます。 良くない点1: ビジネスロジックが複数のコンポーネントに点在している なぜ良くないのか 課題1. ビジネスロジックの変更がしにくい・仕様の把握がしにくい 課題2. 他のサービスに成果物を展開できない 課題1: メンテナンス容易性(Maintainability) 「メンテナンス容易性(Maintainability)」とは、システムの変更や拡張がどれだけ簡単に行えるかを示す特性です。 コンポーネントのビジネスロジックに変更を加える際、他のコンポーネントのビジネスロジックに影響がないかを常に気にしないといけないため変更が難しくなります。また、各コンポーネントにビジネスロジックが点在していることで仕様を把握することが難しくなります。 課題2: 拡張性(Scalability) 「拡張性(Scalability)」とは、システムがどれだけ成長に対応できるかを示す特性です。 各コンポーネントにビジネスロジックが点在していることで、他のサービスに成果物を展開することが難しくなり、拡張性が損なわれることを示しています。 どう解決したのか ビジネスロジックをなるべく1つのコンポーネントに集約することで、仕様の把握から変更をしやすくし、メンテナンス容易性(Maintainability)を高める 各サービスで共通となる成果物を生成するバッチシステムを起点にして、そこから他のサービスに成果物を展開できる設計にすることで、拡張性(Scalability)を高める 良くない点2: 1つのコンポーネントに複数の責務を持つ処理が混在していた なぜ良くないのか 課題1. コンポーネントを再利用することが難しい 課題2. インシデントの原因究明に時間がかかる 課題3. コンポーネントのテストが難しい 課題4. コンポーネントの凝集度が低くなる 課題1: 再利用性(Reusability) 「再利用性(Reusability)」とは、システムの一部を他のシステムや他の部分で再利用できるかを示す特性です。 1つのコンポーネントにデータ抽出から変換処理など様々な責務を持つ処理が混在していると、特定のバッチシステムに特化したコンポーネントとなり、再利用性が損なわれます。 課題2: サポート容易性(Supportability) 「サポート容易性(Supportability)」とは、システムのエラー時に必要となる情報のログを整えられているかを示す特性です。 私たちはコンポーネント内の各処理で都度、成果物の状態を全てログとして残すのは現実的でないと判断したため、コンポーネントの出力結果をデータストレージに保存しています。そのため、コンポーネント内のどの処理でどのようなデータが生成されたのかをトレースすることが難しくなり、インシデントの原因究明に時間を要することがありました。 課題3: テスト容易性(Testability) 「テスト容易性(Testability)」とは、システムのテストがどれだけ簡単に行えるかを示す特性です。 テスト容易性は複数の要素から構成されます。課題3は「単純性(Simplicity)」と「理解容易性(Understandability)」に関わります。複数の責務を持つコンポーネントは、テスト対象となるコードの増加により単純性が損なわれます。さらに、テスト対象が増えることでテストケースの数も増え、テストの目的が理解しにくくなります。 課題4: 凝集度 課題4はコンポーネントの処理がどのくらい関連度のある塊になっているかを示す凝集度に関わる課題です。コンポーネント内に複数の処理があること自体は問題ありませんが、まとまり具合が問題となります。 リアーキテクチャ前の実装では、時間的凝集になっている箇所がありました。時間的凝集とは、同じようなタイミングで実行される処理をまとめ、実行順序を入れ替えても問題ない特徴があります。ここでは、ビジネスロジックと正規化処理を入れ替えても問題ないということがわかっていました。凝集度が低いコンポーネントは、課題3で説明したテスト容易性が損なわれることにも繋がります。 また、コンポーネント内の処理間に新しく追加したい処理が発生した場合、既存のコンポーネントへ実装することになり、さらに凝集度は低くなります。 どう解決したのか データの抽出、変換、統合、削除などの各処理をなるべく小さな単位でコンポーネント化し、再利用性(Reusability)やテスト容易性(Testability)、凝集度を高める 正規化処理の具体的な処理内容は外からコントロールできるようにし、再利用性(Reusability)を高める 小さなコンポーネント毎に成果物を出力することで状態をトレースしやすくし、サポート容易性(Supportability)を高める 削除処理を1つのコンポーネントに集約し、最後に配置することでインシデント対応時の情報源となるデータの欠損を防ぎ、サポート容易性(Supportability)を高める 良くない点3: データ抽出処理が独立していない なぜ良くないのか 課題1. 新しくデータリソースが増えた場合に対応しづらい 課題1: 拡張性(Scalability) バッチシステムのデータリソースは将来増えることが予想されていました。そのため、データ抽出処理とシステムの拡張性は密接に関連しており、データ抽出処理が独立していないとデータリソースが増えた場合に対応しづらく拡張性(Scalability)が損なわれます。 成果物のデータ形式にJSON Linesを採用しているため、各コンポーネントにはJSON Lines形式に変換する処理が含まれていました。そのため、データリソースが増えた場合はデータ抽出処理に加えて変換処理を都度実装する必要もありました。 どう解決したのか データ抽出処理を独立させることで、データリソースが増えた場合にも対応できるように拡張性(Scalability)を高める データ抽出以外のコンポーネントの入力形式をJSON Lines形式に統一し、変換処理の責務をデータ抽出処理に集約することで、拡張性(Scalability)を高める Design Docの活用 Design Docとは、システムの設計に関するドキュメントのことです。Design Docを用いて議論することで設計の品質を高めることを目的としています。今回のリアーキテクチャにおいても、Design Docを活用しました。 以下にDesign Docで共有した情報を紹介します。 なにを書くか 検索基盤部では、Design Docの雛形を各プロジェクトの要件に合わせてカスタマイズしています。今回のリアーキテクチャで共有した情報をいくつか紹介します。 Goal / Non-Goal プロジェクトの目的(Goal)と意図的に目指さないこと(Non-Goal)を記述します。リアーキテクチャの場合は、なぜ目的としているのかの背景や歴史的な経緯を記述し、途中から加わったメンバーにも目的を共有できるように意識しています。 用語集 プロジェクト内で用いる単語の定義を記述し、コミュニケーションの精度を高めることを目指しています。 全体のシステム構成図 システム全体が複数のコンポーネントから構成される場合、コンポーネント間の関係を示す図を作成します。この図によって、プロジェクトメンバーが全体像を把握しやすくなります。 議論段階では、考えられる設計案を複数提示しそれぞれのメリット・デメリットを記述するようにしています。リアーキテクチャの場合は現在のシステム構成図とリアーキテクチャ後に予定しているシステム構成図を比較しやすいようにしています。 コンポーネントの定義 システム全体を構成するコンポーネントの処理内容とインプットとアウトプットを記述します。さらに、インプットとアウトプットの例を記述し、処理のイメージを理解しやすくするように意識しています。 効果 今回Design Docを活用したことで、以下のような効果がありました。 リアーキテクチャ前のバッチシステムの課題を共有し、解決策を共有できた バッチシステムのドメイン知識やビジネスロジックを共有し関係者の理解を深めることができた Design Docの設計から実装がズレていないかをチェックできた 内部設計に関する知識や情報を共有できた まとめ 本記事では、バッチシステムの内部設計の品質を高めるためのポイントをリアーキテクチャの事例をもとに紹介しました。また、アーキテクチャ特性の観点で課題を整理し、それらの課題を解決するためのアプローチを紹介しました。 バッチシステムの外部設計に関するテックブログなどは散見されますが、内部設計に関する情報は多くありません。そこで、本記事がバッチシステムの開発や運用に携わるエンジニアの方々の参考になれば幸いです。 おわりに ZOZOでは一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com
はじめに こんにちは、MA部の中原です。 MA部ではメルマガやLINE、アプリプッシュ通知を配信するためのマーケティングオートメーションシステムを開発・運用しています。 2022年からこのマーケティングオートメーションシステムをリプレイスするためのプロジェクトをMA部で進めています。リプレイス後の新しいマーケティングプラットフォームを「ZOZO Marketing Platform(略称:ZMP)」と呼んでいます。ZMPの概要については以下のテックブログをご覧ください。 techblog.zozo.com 本記事では、マス配信バッチのリプレイスについてご紹介します。 目次 はじめに 目次 配信の種類 システム全体の課題 既存のマス配信のシステムと運用について 既存システム (1) 対象者抽出 (2) コンテンツの抽出 (3) 配信処理 運用 既存のシステムと運用の課題 マーケターの自由度が低い 簡易的なCMSの限界 配信処理が基盤ごとに分かれている リプレイス概要 1. キャンペーンごとのDigdagのワークフローの処理を共通化 2. 新規開発した管理画面からのパラメータを使う 新基盤の仕組み (1) キャンペーン設定 (2) 登録 (3) 配信データ取得 (4) 実行 (5) 実績データ登録 (6) 配信リクエスト 3. ABテストの導入 4. テスト配信の仕組みづくり データセットを分ける 配信リストを上書き 5. 配信処理を配信基盤に任せる リプレイスの課題 解決策 ジョブの導入 バッチ処理の流れ まとめ さいごに 配信の種類 ZOZOTOWNでは、マス配信とパーソナライズ配信の大きく2種類を配信しています。 マス配信 パーソナライズ配信 特徴 ・特定のセグメント(配信対象者)に対して特定のコンテンツ(届けたい情報)を特定日時に配信 ・ユーザの利用状況に応じて表示するアイテムや並びなどをパーソナライズする場合もあり ・ある条件をトリガーとして一人ひとりにリアルタイムで配信 ・ユーザの行動に応じて配信時間を最適化するものもあり 配信例 コスメに関心がありそうな世代・性別に対して、コスメに関するキャンペーン情報を配信 ・お気に入り登録したアイテムの在庫がわずかになった際の通知 ・閲覧したアイテムの値下げ通知 など マーケターの運用 キャンペーンの配信スケジュールやセグメント等を決め、CMSを使って運用 システムが特殊なつくりになっているため、設定を変更したい場合はマーケターからエンジニアに依頼 マス配信とパーソナライズ配信は基盤が分かれています。 システム全体の課題 システム全体で存在していた課題について以下のテックブログで詳しく紹介しています。 techblog.zozo.com 簡単にまとめると課題は大きく以下の3つがありました。 配信の種類ごとに使用技術や基盤が異なり、運用・保守コストが肥大化 運用・保守コストが大きいため、新規開発にリソースが割けない マーケターのみでキャンペーン実施作業が完結しない 既存のマス配信バッチにおいては特に3つ目の課題が顕著でした。次の章以降では、既存のマス配信バッチに焦点を当て、システム内容、運用、課題、リプレイス内容を説明します。 既存のマス配信のシステムと運用について 既存システム 既存のマス配信の仕組みでは Digdag を使用しています。Digdagとはオープンソースのワークフローエンジンで、複数のタスクをワークフローとして定義し、バッチ処理を行えるものです。キャンペーンの種類ごとにDigdagのワークフローが作成されており、クーポンメール用、タイムセールの告知メール用、アプリプッシュ通知用などがあります。これらのワークフローは、以下の大きく3つのステップで構成されています。 (1) 対象者抽出 対象者は基本的にアクティブな会員が対象です。BigQueryで作成したセグメントのビューを元に、特定の対象者に絞り込むこともあります。 (2) コンテンツの抽出 キャンペーンごとに様々な条件でコンテンツを抽出し、デザイン(お知らせ内容)を表示する上で必要なパラメータ(商品名や商品の値段など)を取得します。 (3) 配信処理 対象者とコンテンツのパラメータを組み合わせて配信リストを作成します。配信時にデザインのテンプレートにコンテンツ抽出時のパラメータが埋め込まれ、デザインが表示される状態で配信します。 運用 配信の設定は Google Sheets(スプレッドシート) と Google Apps Script(GAS) を利用した簡易的なCMSから行います。マーケターはこのCMSを使って各キャンペーンの配信日時やセグメント、Digdagのワークフローに渡すパラメータ等を設定し、運用しています。Digdagのワークフローに渡すパラメータには、掲載商品をセール商品のみにするかどうかのフラグやBigQueryで作成しているビューなどがあります。これらのパラメータはDigdagのワークフローの処理内で、対象者や掲載商品の絞り込みに使用されます。以下に運用時の設定例を示します。 簡易的なCMSの詳しい仕組みについては以下のテックブログをご覧ください。 techblog.zozo.com また、以下のようにDigdagのワークフローの作成や修正が必要な場合マーケターだけではできないのでエンジニアへ依頼します。 新規キャンペーンの追加 エンジニアにDigdagのワークフローの作成を依頼し、CMSからそのワークフローを指定する 生成するコンテンツの内容を変更 表示するおすすめ商品から特定の商品を除外する等 機能の追加 ABテストの機能を追加する等 既存のシステムと運用の課題 既存のマス配信バッチと運用には以下の課題がありました。 マーケターの自由度が低い 紹介したように既存の仕組みはエンジニアありきの運用だったため、マーケターが主体となってキャンペーンの実施ができない状態でした。 既存のマス配信バッチでは、新規キャンペーンの開発や条件の変更が必要な場合、エンジニアに依頼する必要がありました。そのため、キャンペーン実施までのリードタイムが長くなってしまいます。 また、既存のシステムでは機能不足より実現できないことがありました。例えば既存のマス配信バッチには、ABテストの機能が標準で備わっていませんでした。ABテストとは、複数のバリエーションを用意し、それぞれの効果を比較することで最適な配信をするための手法です。各キャンペーンの処理が共通化されていなかったため、エンジニアはマーケターからの依頼がある度にそのキャンペーンに対してABテストのロジックを実装する必要がありました。また、バリエーションの数やそれぞれに対象者を振り分ける比率は外部(CMS)から指定できない仕組みだったため、マーケターが自由に設定できずABテストの実施のハードルが高くなっていました。 簡易的なCMSの限界 簡易的なCMSはあるものの、スプレッドシートとGASでは操作性やバリデーション等のできることが限られているため、マーケターの運用ミスが起きやすい状態でした。 配信処理が基盤ごとに分かれている 先述の通り、マス配信とパーソナライズ配信では基盤が分かれています。メール配信の場合は外部のサービスにリクエストを送って配信します。それぞれの基盤から1つのメール配信サービスにリクエストを送るため、大量に配信すると流量制限ができずメール配信サービスに負荷をかけてしまい配信の遅延やエラーが時折発生していました。そのため、配信基盤を1つにしてメール配信サービスへのリクエスト流量をコントロールすることが求められていました。 リプレイス概要 マス配信バッチのリプレイス概要は以下の通りです。 キャンペーンごとのDigdagのワークフローの処理を共通化 新規開発した管理画面からのパラメータを使う ABテストの導入 テスト配信の仕組みづくり 1. キャンペーンごとのDigdagのワークフローの処理を共通化 ZMPの概念モデルに沿ってキャンペーンごとのDigdagのワークフローの処理を共通化・汎用化しました。概念モデルとは、ZMPにおけるデータの構造やデータの流れ、データの管理方法などを定義したものです。以下に概念モデルの一部を示します(初期検討時のもの)。 概念モデルの詳しい内容については こちら のテックブログをご覧ください。 以下はリプレイス前とリプレイス後のワークフローの実装イメージです。 既存のマス配信システムでは、「クーポンメール用」「タイムセールメール用」のようにキャンペーンの種類ごとにDigdagのワークフローが分かれていました。共通化することで1つのワークフローでチャネルやキャンペーンに関わらずすべての配信が可能になりました。また、マーケターの自由度の高い設定内容で柔軟に処理できるようにしました。 2. 新規開発した管理画面からのパラメータを使う 既存のマス配信バッチでは、スプレッドシートとGASを使って設定内容をBigQueryに登録し、その情報を使ってDigdagのワークフローを実行していました。新基盤では、スプレッドシートではなく、ZMPの管理画面から設定された情報を元にします。その他の仕組みは基本的に既存と同じにしました。 新基盤の仕組み 新基盤の仕組みは以下の通りです。 (1) キャンペーン設定 マーケターがZMPの管理画面でキャンペーンの設定情報を登録します。 (2) 登録 管理画面で登録された情報をBigQueryに登録します。登録する情報は既存のスプレッドシートに似た内容です。例えば、キャンペーン名、配信日時、セグメント、コンテンツの内容などです。 (3) 配信データ取得 3分おきに動いているDigdagのスターターというワークフローがBigQueryに登録されているデータを参照し、配信日時を迎えたキャンペーンの設定情報を取得します。スターターの仕組みについては こちら のテックブログで紹介しているものと同様です。 (4) 実行 (3)で取得した情報をパラメータとしてマス配信バッチ処理のDigdagのワークフローに渡して実行します。以下にパラメータ例を示します。 campaign : id : 1 name : "クーポンメール" segment : id : 1 query : "select * from `project.dataset.target_segment`" action : id : 1 channel : "mail" ab_test : settings : salt : 100 # ハッシュ生成のためのsalt control_group : percentage : 20 treatment_group : - contents_id : 1 percentage : 40 - contents_id : 2 percentage : 40 contents : - id : 1 name : "クーポンメール_パターンA" template : subject : "クーポンメール_パターンAの件名" body : "クーポンメール_パターンAの本文" - id : 2 name : "クーポンメール_パターンB" template : subject : "クーポンメール_パターンBの件名" body : "クーポンメール_パターンBの本文" jobs : - id : 1 project : "sample-project" # Digdagのプロジェクト名 workflow : "coupon_mail_job" # Digdagのワークフロー名 (5) 実績データ登録 BigQueryに実績データを登録します。実績データは配信したユーザの情報や配信したコンテンツの情報などです。既存のマス配信バッチでは細かい実績データは登録していませんでしたが、過去に配信したキャンペーンの分析や調査がしやすくなるように様々な実績を登録します。また、処理のステータス(実行前・実行中・成功・失敗)も書き込み、配信処理の状況を管理画面から確認できるようにします。 (6) 配信リクエスト 配信処理を行う配信基盤にリクエストして配信します。 3. ABテストの導入 既存のマス配信バッチで一部実装されていたABテストの仕組みを共通処理として導入しました。ABテストの設定がある場合、対象者を管理画面から指定された比率で振り分けます。具体的には、 FARM_FINGERPRINT でハッシュを生成し、 ABS で絶対値に変換し、 MOD で値が0から100に収まるように調整します。その値を使って、指定された比率でランダムに分割します。サンプルクエリを以下に示します。波括弧で囲まれた部分は管理画面から設定された値です。 WITH target_segment_with_seed AS ( SELECT unique_id,  -- ユニークID member_id,  -- 会員ID email_id,  -- メールID push_notification_uid,  -- アプリプッシュID mid,  -- LINE用のID CASE WHEN member_id IS NOT NULL THEN CAST (member_id AS STRING) ELSE COALESCE ( CAST (email_id AS STRING), '' ) || ' _ ' || COALESCE (push_notification_uid, '' ) || ' _ ' || COALESCE (mid, '' ) END AS seed FROM `project.dataset.target_segment` ), split_target_segment AS ( SELECT unique_id, member_id, email_id, push_notification_uid, mid, MOD ( ABS (FARM_FINGERPRINT( CAST (seed AS STRING) || CAST ( ' {{ salt }} ' AS STRING))), 100 ) AS treatment FROM target_segment_with_seed ) SELECT unique_id, member_id, email_id, push_notification_uid, mid FROM split_target_segment WHERE {{ rate_from }} <= treatment AND treatment < {{ rate_to }} ; 4. テスト配信の仕組みづくり 既存のマス配信バッチでは、テスト配信対象者にしか配信されない設定になっているQA環境で本配信同等の配信設定をしてテスト配信を行なっていました。新基盤では、本番環境のZMPの管理画面からテスト配信ができるようにしました。具体的な仕組みは以下の2つです。 データセットを分ける 配信リストを上書き データセットを分ける 本配信とテスト配信でBigQueryのデータセットを分けました。これにより、本番環境で本配信とテスト配信のデータが混ざることを防ぎます。マス配信のバッチ処理のDigdagのワークフローにテスト配信かどうかを判別するためのパラメータを追加し、データの書き込みや参照するデータセットを切り替えるようにしました。 配信リストを上書き テスト配信時は管理画面からテスト配信対象者を選択します。本配信と同じ処理を行い配信リストを作成した後、ランダムにデータを選び、テスト配信対象者のみになるように上書きします。この際、ユーザの名前やメールアドレスのような個人情報はテストアカウントのデータに置き換えます。 5. 配信処理を配信基盤に任せる 既存のマス配信バッチの中で行っていたメールの配信処理を配信基盤に任せます。先述の通り、メール配信サービスへのリクエスト流量をコントロールするためにどの基盤からも使えるメール配信基盤が必要になり作成したので、それを使うことにしました。ZMPではメールの配信基盤を新規で作成し、以下のテックブログで紹介しています。 techblog.zozo.com リプレイスの課題 前の章で述べた通り、キャンペーンの種類ごとに分かれていたDigdagのワークフローは、概念モデルに沿って抽象化することで共通化しました。これにより、1つのワークフローでチャネルやキャンペーンに関わらずすべての配信が可能になり、管理画面でワークフローを意識する必要がなくなりました。また、ABテストの導入やテスト配信の仕組みも組み込まれたことで、マーケターの自由度が高い設定内容で柔軟に処理できるようになりました。しかし、任意のタイミングで特殊な処理をしたい場合があります。例えば以下のような場合です。 特定のセグメントを抽出する前にセグメントのビューの中で参照しているテーブルが更新されるのを待ちたい コンテンツ抽出時にキャンペーンによって異なる方法で抽出したい 配信前に特定の外部サービスやZOZOTOWN本体にデータ連携やAPIリクエストをしたい 1つ目について例をあげて説明します。例えば、セグメントのビューの中で別のバッチによって更新されるテーブルを参照する場合、そのテーブルが更新された後に対象者を抽出しないと古いデータを参照してしまい、送りたい対象者に送ることができません。そのため、セグメント抽出前に「テーブルの更新を待つ」という処理が必要になります。 上記以外にも今後任意のタイミングでしたい処理が出てくる可能性があるため、別の概念を導入して補う必要がありました。 解決策 ジョブの導入 ZMPの概念モデルに初期検討時にはなかったジョブという概念を導入することで、必要に応じて任意のタイミングで特殊な処理ができるようになりました。ジョブとは指定されたDigdagのワークフローを実行できるようにするための概念です。特殊な処理が必要な場合、事前にジョブとしてその処理を行うDigdagのワークフローを作成しておきます。ジョブの情報はZMPの管理画面から設定され、指定されたタイミングでそのワークフローを実行することで、特殊な処理が必要な場合に補うことができます。 バッチ処理の流れ コンテンツ抽出時にジョブ(特定のセグメントを抽出する前にセグメントのビューの中で参照しているテーブルが更新されるのを待ちたい)を設定した場合を例に、バッチ処理の流れを以下に示します。 対象者抽出 管理画面から指定されたジョブを実行し、セグメントのビューの中で参照しているテーブルが更新されるのを待ちます。 ジョブ完了後、管理画面から指定されたセグメント(BigQueryのビュー)で対象者を抽出します。 コンテンツ抽出 管理画面から指定されたコンテンツ情報に基づいてコンテンツを抽出します。 対象者とコンテンツのパラメータを組み合わせて配信リストを作成します。 配信処理 配信リストを使って配信基盤にリクエストして配信します。 例としてあげた「特定のセグメントを抽出する前にセグメントのビューの中で参照しているテーブルが更新されるのを待ちたい」の場合、事前に対象の更新バッチを待つジョブを作成することになります。管理画面からはそのジョブを選択できます。また、ジョブはセグメント抽出の前か後かなど、実行するタイミングも指定できます。セグメント抽出の前処理として作成したジョブを指定することで、セグメント抽出前にジョブが実行され、セグメントのビューの中で参照しているテーブルが更新されるのを待つことができます。 まとめ マス配信バッチのリプレイスについてご紹介しました。任意のタイミングで特殊な処理をするための仕組みが必要でしたが、特殊な処理が必要な場合でもジョブという概念で補うことができました。汎用的で柔軟なシステムの概念モデルを構築できたと思います。汎用化したことでデータ管理が統一でき、統一した方法で管理画面でのキャンペーン登録ができるようになりました。本記事が皆様の参考になりましたら幸いです。 さいごに ZOZOでは一緒にプロダクトを開発してくれるエンジニアを募集しています。ご興味のある方は下記リンクからぜひご応募ください! hrmos.co hrmos.co
はじめに こんにちは、MA部の松岡( @pine0619 )です。MA部ではマーケティングオートメーションシステムの開発・運用に従事しています。 ZOZOTOWNでは、マーケティングオートメーションシステム(以下、MAシステム)を使い、メールやLINE、アプリプッシュ通知といったチャネルへのキャンペーンを配信しています。 MA部では、複数のMAシステムが存在しており、MAシステムそれぞれに各チャネルへの配信ロジックが記述されていました。これにより、現状の運用保守ならびに今後の改修コストが高いかつ、使用している外部サービスのレートリミットの一元管理が出来ていないなどの問題を抱えていました。そのため、外部サービスへのリクエスト部分をチャネルごとにモジュールとして切り出し、複数のMAシステムから共通で使える配信基盤を作成しました。 また、社内の他チームの持つシステムからのキャンペーン配信の要望があったため、全社共通で利用できる配信基盤を作成しました。 その中でも本記事ではメール配信基盤にフォーカスし、以下についてご紹介します。 システム構成 マーケティングメールを配信する際の考慮ポイント 複数の社内システムから利用される基盤作成の考慮ポイント メール配信基盤の監視について このメール配信基盤は注文完了などのトランザクションメールは対象外で、マーケティングメールのみを扱うシステムとなっています。 なお、アプリプッシュ通知やLINEの配信基盤については別の記事で紹介する予定です。 目次 はじめに 目次 メール配信基盤の作成の背景・目的 既存のマーケティングオートメーションシステムでの課題 他の社内システムからの利用希望 メール配信基盤のシステム構成 配信基盤へのリクエスト メール配信処理の流れ マーケティングメールを配信する際の考慮ポイント 配信の可能時間チェック 配信の有効期限チェック 配信の重複制御 メールアドレスの解決 配信対象者の分割 配信リストテーブルのテーブルサイズの取得 分割数の決定 パーティションを使ってテーブルを分割する 分割テーブルごとにファイルへエクスポートする メール配信サービスへのリクエストの流量制限 テスト配信 複数の社内システムから利用される基盤作成の考慮ポイント リクエスト元の識別およびデータ管理について メール配信基盤の監視について 今後の展望 まとめ メール配信基盤の作成の背景・目的 既存のマーケティングオートメーションシステムでの課題 導入部分でも触れたように、現在MA部では複数のMAシステムを使って配信しています。配信は特定のユーザーセグメント向けのマス配信と、個別のユーザーに最適化されたパーソナライズ配信の2種類が存在しています。この2種類の配信に加えて、チャネルによっては独立したシステムになっているという理由から複数のMAシステムの開発・運用をしています。 メール配信では外部のメール配信サービス(以下、メール配信サービス)を使用しています。このメール配信サービスを使った配信ロジックがパーソナライズ配信、マス配信の両方のMAシステムに入っていました。配信ロジックが2つのMAシステムに存在していることで、現状の運用保守ならびに今後の改修において工数がかかってしまうなどの問題がありました。 また、一般的に外部サービスにはリクエスト制限があります。このメール配信サービスにもリクエスト制限がありますが、現在のMAシステムでは、メール配信サービスへのリクエスト流量制限の一元化が出来ていませんでした。そのため、両方のシステムから同時に大量のメール配信をリクエストした際に、リクエスト制限を超過してしまい、配信遅延やエラーが発生するという問題もありました。 上記の課題を解決するため、配信基盤としてMAシステムから配信ロジックをモジュールとして切り出すことで、配信ロジックならびにリクエスト流量制限を一元化することにしました。 他の社内システムからの利用希望 上記で述べた課題の他に、社内の他チームの持つ社内システムからのキャンペーン配信の要望もありました。しかし、配信ロジックがMAシステム内に書かれていたため、そのままだと他の社内システムからの配信に対応出来ない状況でした。 この課題を解決するために、全社共通で利用できるメール配信基盤を作成することにしました。 メール配信基盤のシステム構成 今回作成したメール配信基盤についてご紹介します。 メール配信基盤では、ワークフローエンジンの Digdag を採用しています。既存のMAシステムでDigdagを採用しており、今回の配信基盤の開発でリソースを流用できるかつ、チームメンバーがDigdagに慣れており開発・運用コストを削減できるため採用を決めました。 以下がシステムの全体構成および処理の流れとなっています。 メールの配信内容は、メールデザインのテンプレートと、キャンペーンやユーザごとに動的な商品などのパラメータにより決まります。このテンプレートとパラメータを指定してメール配信サービスへ配信リクエストをすることで、配信時にテンプレートへパラメータが埋め込まれた状態でメール配信されます。 パラメータの詳細については以下のコンテンツパートを参照してください。 techblog.zozo.com 配信基盤へのリクエスト 配信基盤へ配信リクエストをする前に、社内システム側でメール配信サービスへのテンプレートのアップロードと、BigQueryの配信対象者テーブルを作成します。以下の図でいうと赤枠の部分になります。この配信対象者テーブルは、テンプレートのパラメータ、ユーザーID、メールアドレスに紐づいたIDを含みます。現状は社内システムから直接メール配信サービスへテンプレートをアップロードしていますが、こちらは後々、メール配信基盤を経由するように変更し、社内システムのメール配信サービスへの依存を無くす予定です。 その後、社内システムからの配信リクエストをAPIで受信します。以下の図でいうと赤枠の部分になります。この配信リクエストでは前述したテンプレートの情報や配信対象者テーブル名などを受け取ります。 次にリクエスト内容を保存するためのリクエストテーブルにリクエスト内容をINSERTして、社内システムにレスポンスを返します。その後、Digdagによるマイクロバッチでリクエストテーブルからリクエスト内容を取得し、配信処理しています。また、配信処理の他に配信履歴の確認や、分析目的で使用される配信実績テーブルへ実績を書き込みます。 ZOZOではBigQueryを全社共通のデータ基盤として利用しています。このデータ基盤上にメール内容の元となる商品データや会員データなどがあり、それらのデータを利用して配信します。また、メール配信基盤の配信実績データと、データ基盤上のデータを組み合わせて分析したいという要件もありました。以上の理由から、データ基盤とメール配信基盤間で円滑にデータを受け渡すために、メール配信基盤ではデータの管理にBigQueryを採用しています。 メール配信処理の流れ メール配信処理は以下の流れとなっています。 配信処理は、起動・配信前処理・配信処理を担う3つのワークフローから構成されています。各ワークフローの役割と実行タイミングについては以下となっています。 ワークフロー 役割 実行タイミング 起動  配信対象のリクエストの取得 配信の有効期限チェック 配信前処理ワークフローの起動 3分おきに定期実行 配信前処理 重複制御 メールアドレスの取得 配信リスト作成 起動ワークフローから起動される 配信処理 メール配信サービスへのリクエスト 配信実績の書き込み 5分おきに定期実行 配信前処理と配信処理を分けているのは、後述するメール配信サービスへのリクエストの流量を制限するためです。 マーケティングメールを配信する際の考慮ポイント マーケティングメールを配信する際に考慮すべき点と、メール配信基盤においてそれをどう実現したかについてご紹介します。 配信の可能時間チェック メール配信基盤では、一律な配信の可能時間を設定しています。メール配信基盤や外部サービスでの障害などにより配信遅延が発生し、深夜帯にメール配信される場合が考えられます。しかし、深夜帯は多くのユーザーが活動時間外ということもあり、場合によってはメルマガ購読停止やクレームなどに繋がる恐れがあります。 そのためメール配信基盤では、Digdagの スケジュール機能 を利用し、以下のようにスケジュール設定することで配信の可能時間を定めています。 schedule: cron>: '*/5 8-22 * * *' 配信の有効期限チェック 配信の可能時間チェックとは別に、社内システムからリクエストを受け取る際に配信の有効期限を受け取るようにしています。有効期限を受け取る理由は、配信リクエストを受け付けてから一定の時刻を過ぎた場合は配信しないようにするためです。 たとえばセール最終日のお知らせメールを送る場合について考えてみます。メール配信基盤や外部サービスでの障害などにより、セール終了時間までに配信完了しない場合が考えられます。その際にそのままメールを配信してしまうと、セールは終了しているため、事実と異なるメールがユーザーに届いてしまいます。それを防ぐために配信の有効期限をチェックし、有効期限切れの配信は送らないようにしています。 起動ワークフローでリクエストテーブルから配信対象を取得するクエリが以下です。このクエリで有効期限(expires_at)を過ぎていないかつ、statusが配信処理前を示すWAITINGになっているリクエストを取得することで、配信の有効期限チェックを実現しています。 SELECT * FROM `リクエストテーブル` WHERE expires_at > CURRENT_TIMESTAMP () AND status = ' WAITING ' ORDER BY expires_at ASC 前述の通り、メール配信基盤は必ず届かないと困るようなトランザクションメールではありません。そのため誤情報をユーザに送ってしまうことのほうが良くないため、有効時間を過ぎた場合は配信しないようにしています。 配信の重複制御 マーケティングメールにおいて重複配信は、会社への不信感や、メルマガ購読停止、クレームに繋がってしまう可能性があります。メール配信基盤では、配信リクエスト完了後の処理が失敗し処理全体をリトライした場合や、社内システムから同一リクエストを複数回受け取った場合に、重複配信の可能性がありました。そのため、重複制御の処理を入れることで配信の重複を防いでいます。 重複制御は以下の流れになっています。 配信対象者テーブルは社内システムから連携されるテーブルです。この配信対象者テーブルでdeduplication_idというメール1通単位ごとに一意となるIDを受け取ります。このdeduplication_idと重複制御用テーブルを使って重複制御を行います。重複制御用テーブルには、既に配信処理されたdedeplication_idを保存しています。重複除外のため、配信対象者テーブル内のdeduplication_idが重複制御用テーブルに存在しているかどうかをチェックします。存在している場合は、該当のdeduplication_idを持つレコードを除外した重複除外済みテーブルを作成します。その後、重複制御用テーブルを更新しています。後続の配信処理ではこの重複除外済みテーブルを配信対象としてメールを配信します。 メールアドレスの解決 メール配信では配信対象者のメールアドレスが必要となります。メールアドレスは個人情報に当たるため、本当に必要な場合を除いて、参照できる状態は好ましくありません。そのため、社内システムと配信基盤間ではメールアドレスに紐づいたemail_idを使って配信対象者を連携し、配信基盤側でemail_idに紐づいたメールアドレスを全社共通のデータ基盤から取得しています。これにより社内システムと配信基盤間で個人情報を受け渡す必要が無くなります。 会員メール情報テーブルのメールアドレスカラムに関してはアクセス制御されています。メール配信基盤で使用するサービスアカウントにはあらかじめメールアドレスカラムへのアクセス権限を付与しているため、email_idに紐づいたメールアドレスが取得できます。 配信対象者の分割 メール配信サービスへのリクエストでは、配信対象者のメールアドレスおよび、キャンペーンやユーザごとに必要なパラメータのリストを連携する必要があります。この配信リストの連携ではファイルサイズ上限が定められています。ZOZOTOWNのマーケティングメールでは1回のキャンペーンで1000万通程度のメールを配信することもあり、何も考えずに配信リストを作成・連携するとファイルサイズ上限を超過してしまいます。そのため、ファイルサイズ上限を超過しないように、配信リスト作成時に配信対象者を適切に分割する必要がありました。 配信対象者のリストはBigQueryに配置されると説明しました。配信時にはそのデータをCloud Storageにエクスポートし、そのファイルをメール配信サービスへ連携します。BigQueryからCloud Storageへのエクスポート時に、エクスポートするファイルのサイズを制限する方法は Google Cloudのドキュメント を参考にしました。 配信基盤では、以下のステップで配信対象者を分割し、配信リストを作成しています。 配信リストテーブルのテーブルサイズの取得 分割数の決定 パーティションを使ってテーブルを分割する 分割テーブルごとにファイルへエクスポートする 各ステップについてそれぞれご紹介します。 配信リストテーブルのテーブルサイズの取得 前述した重複制御・メールアドレスの解決の処理後、配信リストの元となる配信リストテーブルが作成されます。この配信リストテーブルを適切に分割し、ファイルへエクスポートするために、まずは配信リストテーブルのテーブルサイズを知る必要があります。テーブルサイズは INFORMATION_SCHEMA 内の パーティションビュー の TOTAL_LOGICAL_BYTES から取得可能で、以下のクエリにて取得しています。 SELECT total_logical_bytes FROM `{project}.{dataset}.INFORMATION_SCHEMA.PARTITIONS` WHERE table_name = 配信リストテーブル 分割数の決定 上記で取得した配信リストテーブルのテーブルサイズと、メール配信サービスのファイルサイズ上限を元に分割数を決定します。メール配信基盤ではDigdagでPythonスクリプトを呼び出しており、以下のコードで分割数を決定しています。 file_size_limit_mb = 900 # ファイルサイズ上限 table_size_bytes = 配信リストテーブルのテーブルサイズ table_size_mb = table_size_bytes / ( 1024 * 1024 ) partition_count = int (table_size_mb // file_size_limit_mb) + 1 パーティションを使ってテーブルを分割する 分割数が決定した後は、以下のクエリでパーティション用のidであるpartition_idを各レコードへランダムに割り振ります。 CREATE OR REPLACE TABLE `パーティションテーブル` PARTITION BY RANGE_BUCKET(partition_id, GENERATE_ARRAY( 0 , {partition_count}, 1 )) CLUSTER BY partition_id AS ( SELECT *, CAST ( FLOOR (n*RAND()) AS INT64) AS partition_id FROM `配信リストテーブル` ) その後、partition_idごとにテーブルを作成することで配信対象者を分割しています。 CREATE OR REPLACE TABLE `分割テーブル _ {partition_id}` AS ( SELECT * EXCEPT(partition_id) FROM `パーティションテーブル` WHERE partition_id = {partition_id} ) 分割テーブルごとにファイルへエクスポートする partition_idごとに分割したテーブルをCloud Storageにエクスポートします。メール配信基盤では、 EXPORT DATAステートメント ではなく、 クライアントライブラリ を使ってエクスポートしています。 EXPORT DATAステートメントでは、エクスポート先のCloud StorageのURIの指定で「単一のURI」が使用できず、「単一のワイルドカードURI」のみ対応しています。「単一のワイルドカードURI」の場合、以下のドキュメントの通り、複数のファイルに自動で分割され、ファイルサイズが一定ではなくなってしまいます。そのため、クライアントライブラリを使ってエクスポートしています。 エクスポートされるデータが最大値の 1 GB を超えそうな場合は、単一のワイルドカード URI を使用します。データは、指定したパターンに基づいて複数のファイルに分割されます。エクスポートされたファイルのサイズは一定ではありません。 引用: テーブルデータを Cloud Storage にエクスポートする  |  BigQuery  |  Google Cloud メール配信サービスへのリクエストの流量制限 メール配信基盤では、外部のメール配信サービスを利用してメールを配信しています。前述したようにメール配信サービスへのリクエスト流量を制限しない場合、メール配信サービスのパフォーマンス悪化により、配信遅延やエラー発生の懸念がありました。 そのため配信の前処理と実際にメール配信サービスへリクエストする配信処理との間に配信キューを挟み、配信リクエストの流量を制限することで上記の課題を解決しました。 ここでは Pub/Sub のようなメッセージングサービスではなく、BigQueryをキューとして使っています。これは、今後配信の優先度付けをする予定のため、条件でデータが取得できるデータベースの方が適していると判断しました。また、リクエストテーブルや配信対象者テーブルなどをBigQueryで管理しており、データの分散や運用対象リソースが増えるのを防ぎたくCloudSQLなど他のデータベースは利用しませんでした。 テスト配信 ユーザーへメールを送る前に、表示崩れや文字化けが無いかどうかを確認するため、施策担当者へテストメールを送りたいという要件がありました。そのため、テスト配信用のワークフローとユーザーテーブルを別途作成しました。 通常配信とテスト配信のワークフローを分けているのは、テスト配信では重複制御などの処理が不要なためです。テスト配信ワークフローでは、不要な処理を省略することで処理時間を短縮し、施策担当者がテストメールを受け取るまでの待ち時間を減らすことができました。 また、テストユーザーテーブルを用意しているのはテスト配信時に一般のユーザーへの誤配信を防ぐためです。通常の配信ではデータ基盤上の会員メール情報テーブルからメールアドレスを取得しますが、テスト配信ではテストユーザテーブルからメールアドレスを取得します。これによりテスト時は一般ユーザーの情報が入ったテーブルを参照する必要が無くなるため、誤配信を防ぐことができます。 複数の社内システムから利用される基盤作成の考慮ポイント 今回のメール配信基盤は、複数の社内システムからの利用を考慮する必要がありました。その際の考慮ポイントについてご紹介します。 リクエスト元の識別およびデータ管理について メール配信基盤が複数の社内システムから利用されるにあたって、どの社内システムからリクエストされたのかを識別する必要がありました。そのため配信基盤ではリクエスト元の識別にsourceという概念を導入しています。配信基盤ではsourceごとに作成したデータセット内へ配信実績テーブルやテストユーザテーブル、重複制御用テーブルを配置しています。以下の画像でいうと「ma_batch」がsourceに該当しています。 このようにsourceごとのデータセットに分けた理由は権限管理の容易さにあります。BigQueryではデータセット単位のアクセス制御が可能です。これによりテーブルごとの権限付与が不要となります。また、全sourceで共通なテーブルにしてしまうと、レコード数によってはクエリコストが増えてしまう可能性があることから、このような設計にしました。 メール配信基盤では Terraform によるインフラ管理をしており、以下のようにデータセットに対してviewer権限を付与しています。 resource "google_bigquery_dataset_iam_member" "bigquery_data_viewer_ma_batch" { for_each = toset (local.bigquery_data_viewer_ma_batch_members) project = local.project member = each.key dataset_id = "ma_batch" role = "roles/bigquery.dataViewer" } メール配信基盤の監視について メール配信基盤ではDigdagのワークフローが失敗した場合に、Slack通知および PagerDuty による電話通知をしており、オンコール当番が気付ける仕組みになっています。しかし、これだけだとシステム監視として不十分なため、以下の監視を導入しています。 監視内容 目的 有効期限切れにより配信されなかった配信の監視 未配信および配信遅延の検知 配信キュー内の未配信件数の監視 未配信検知および配信遅延の検知 重複配信の監視 重複配信の検知 配信成功率の監視 配信異常の確認 今後の展望 冒頭でも触れた通り、ZOZOTOWNのMAシステムには「マス配信」と「パーソナライズ配信」の2つがあります。現在MA部ではMAシステムのリプレイスを進めており、現在「マス配信」のリプレイスが完了し、「パーソナライズ配信」をリプレイスしています。MAシステムのリプレイスについて詳しくは以下の記事をご覧ください。 techblog.zozo.com 「パーソナライズ配信」の中にはリアルタイム性を求められるリアルタイム配信があります。現状の配信基盤では配信リクエスト順に処理しており、1000万通規模のメールを配信し終わるまでに時間がかかります。この1000万通のメール配信中にリアルタイム配信をリクエストされた場合、現状だとこの1000万通のメール配信が完了してからリアルタイム配信の配信が行われます。配信完了までのスピードがもっと早ければ現状の配信基盤でも対応できますが、メール配信サービスにはリクエスト上限があるため、これ以上の配信の速度向上は見込めません。 そこで配信の優先度を付け、優先度順に配信することでリアルタイム配信にも対応できるシステムにしていきたいと考えています。 また、今後の配信基盤の全体の展望として、他チャネルの拡充や改善を進め、ZOZOとお客様をつなぐコミュニケーションの窓口となる基盤を作り上げていきたいと考えています。 まとめ 本記事ではメール配信基盤について紹介しました。メール配信基盤の誕生により、メール配信サービスへの流量制限の一元化や、配信ロジックの集約による保守・運用コストの削減、複数の社内システムからの配信を実現できました。本記事が同じような状況・課題を持つ方、新たに配信基盤を作る方への参考になれば幸いです。 MA部では上記で挙げた理想の配信基盤を一緒に作り上げてくれる方を募集中です。ご興味のある方は、ぜひ以下のリンクからぜひご応募ください。 hrmos.co hrmos.co
はじめに こんにちは、技術本部SRE部フロントSREブロックの柳田です。オンプレミスとクラウドの構築・運用に携わっています。 ZOZOTOWNでは、既存システムのリプレイスプロジェクトを進行中です。リプレイス過渡期の現在、オンプレミスのネットワークとAWSのデータセンターを直接専用線で接続し、安定した高速通信を実現するDirect Connect(以降、DX)を利用しています。各サービスのマイクロサービス化に伴い、オンプレミスとクラウド間の通信量が増えた為、DX10Gの回線が逼迫する問題に直面しました。 本記事では、この回線逼迫の課題をどのように解決したかについて紹介します。 目次 はじめに 目次 回線逼迫の課題 ZOZOTOWNへのアクセスが困難 今後のリプレイスプロジェクトが遅延する可能性 DX10GからDX100Gへの移行 ステップ1:DX100Gの利用申請(クラウド) ステップ2:DX100G用のスイッチ導入(オンプレミス) ステップ3:AWS Transit Gatewayの導入(クラウド) Transit Gateway導入の経緯 TransitVIFとDXGWを用意 DXGWとTGWの紐付け IaCによるTGWの作成と管理 本番移行 移行前準備 移行実施 まとめ 回線逼迫の課題 DX10Gの回線逼迫により、以下の課題が出てきました。 ZOZOTOWNへのアクセスが困難 今後のリプレイスプロジェクトが遅延する可能性 ZOZOTOWNへのアクセスが困難 DX10Gが回線逼迫することで、性能低下、アクセスの難しさ、信頼性の低下が生じます。具体的には、データ転送の遅延、レイテンシーの増加によるユーザーエクスペリエンスの低下、サイトやサービスへのアクセス障害、予期せぬダウンタイムによるサービス信頼性等、様々な影響が考えられます。 これらの問題は、ZOZOTOWNだけではなく同様に回線を利用している弊社のサービスであるWEARにも発生しうるものでした。 今後のリプレイスプロジェクトが遅延する可能性 ZOZOTOWNはリプレイスプロジェクトが現在進行中であり、リプレイスが進むにつれて、オンプレミスとクラウド間の通信が増えていました。このままDX10Gを使用し続ける場合、回線の飽和が生じてしまいます。この状況はリプレイスプロジェクトに遅延をもたらす可能性が高いです。リプレイスプロジェクトが遅延すると、ZOZOTOWNの成長が遅れるとともに、サイトの進化にも悪影響を及ぼすことが考えられました。 DX10GからDX100Gへの移行 上記2つの課題を対処するために、DX10GからDX100Gへの移行を実施しました。 このアップグレードにより、性能低下やアクセス困難、信頼性の低下といった問題を解決し、ZOZOTOWNのサービス品質を大幅に向上できると考えられました。 ZOZOTOWNのDX10Gネットワーク構成を簡潔に示すと、以下になります。 DX100Gへの移行準備は、以下の3ステップにて実施しました。それぞれのステップについて説明します。 DX100Gの利用申請(クラウド) DX100G用のスイッチ導入(オンプレミス) AWS Transit Gatewayの導入(クラウド) ステップ1:DX100Gの利用申請(クラウド) AWSマネジメントコンソールからDX100Gの利用申請を行います。 まず、AWS Direct Connectサービスにアクセスし、必要なデータセンターを選択して100G回線を指定します。この過程で、Letter of Authorization (LOA) をダウンロードする必要があり、これは回線事業者への申請に必要な公式文書です。LOAを回線事業者へ提出することで、物理的な接続設定を開始します。この手続きは、AWS環境とオンプレミス環境間の高速接続を確立するための重要なステップです。 詳しくはこちらをご参照下さい。 docs.aws.amazon.com ステップ2:DX100G用のスイッチ導入(オンプレミス) オンプレミス側の必要な項目について以下にまとめます。 データセンター機器のレンタル 100G通信が可能なONUとデータセンター機器を、回線事業者およびデータセンターからレンタルしました。 レンタルに関する詳細情報や契約条件は、提携している回線事業者やデータセンターに直接お問い合わせいただくことをお勧めします。 スイッチ/ケーブルの準備 弊社のスイッチは10Gまでの通信しかサポートしていなかった為、10G以上の通信を実現するためには、QSFPに対応したSFPモジュールの準備が必要でした。 MPOケーブルの準備が必要です。これは、10Gを超える通信に適しており、複数のファイバーを効率的に扱えます。 マネージド機器とスイッチの結線 準備したONUとマネージドスイッチを、弊社の既存スイッチに結線します。 実際に用意したSFPモジュールとMPOケーブルを結線する際の緊張感は、とんでもないものでした。 この段階のネットワーク構成は以下の通りです。 ステップ3:AWS Transit Gatewayの導入(クラウド) DX100Gへの移行に伴い、Transit Gateway(TGW)を導入しました。 TGWにより、複数のVPCとオンプレミスネットワーク間の通信が簡素化され、管理が大幅に効率化されます。TGWの導入は、ネットワークの拡張性と柔軟性を向上させます。 このステップは、ZOZOTOWNのインフラを次のレベルへと引き上げる重要な進展でした。それぞれについて詳しく説明します。 Transit Gateway導入の経緯 リプレイスが進むにつれ、新規VPCを追加する度にDirect Connect Gateway(DXGW)とVirtual Private Gateway(VGW)を紐付ける作業に多大な労力を要していました。 この組み合わせでは、紐づけ可能なVPCの数が最大で10までと限定されており、さらにVPC間での直接通信が不可能であるという制約がありました。 これらの課題を解決するために、TGWへの移行を決定しました。TGWを採用することで、紐付け可能なVPCの数が5000まで増加し、異なるAWSアカウント間でのVPC間通信が可能になります。この移行はネットワーク管理の効率化と柔軟性の向上を実現しました。 TransitVIFとDXGWを用意 オンプレミスとクラウド間の通信にTGWを使用する為、Transit Virtual Interface (TransitVIF)の作成が必須です。 しかし、重要な注意点があります。TransitVIFは既存のDXGWには紐付けられません。そのため、新しいDXGWを準備し、そこにTransitVIFを紐付ける手順が必要になります。また、AS(Autonomous System)番号の重複にも注意してください。 DXGWとTGWの紐付け DXGWとTGWの紐付け作業はAWSマネジメントコンソールから実施します。 IaCによるTGWの作成と管理 Infrastructure as Code (IaC)を用いてTGWの作成、そのアタッチメント、およびアソシエーションとプロパゲーションを実施しました。 この作業により、TGWのデプロイメントが自動化され、複数のVPCやオンプレミスネットワーク間での統合通信が容易になりました。IaCの採用により、これらの作業をコードベースで管理し、迅速かつ一貫性のある環境構築を実現しました。 この段階のネットワーク構成は以下の通りです。 本番移行 オンプレミスとクラウド環境の準備が整ったため、次はDX100Gへの移行プロセスについて説明します。 移行前準備 DX10GからDX100Gへの移行において、通信断やサイト停止を最小限に抑えるため、移行前に可能な限り多くの準備作業を行いました。 特に、TGWアタッチメントのアソシエーションとプロパゲーションは、既存の通信に影響を与えず事前に実施できる作業として挙げられます。これらの作業を先行することで、移行プロセスの影響を最小化しました。 また、重要な点として、各VPCのルートテーブルをTGWに向ける変更をしなければ、既存の通信へ影響を及ぼすことはありません。 移行実施 いよいよ移行作業です。 ZOZOTOWNはその巨大なシステム規模から、関連する多くのシステムとの連携が必要であり、サイト停止の調整が不可欠でした。このため、DX100Gへの移行作業は数十分で完了予定だったものの、全体としては約4時間のサイト停止時間を確保する必要がありました。約4時間かかる理由として、以下の要素がありました。 ZOZOTOWNと関連システムの停止/開始作業 DX100G切り替え後、ZOZOTOWN全体の動作検証 移行失敗時の切り戻し時間(バッファ) 移行作業としては以下作業を実施しました。 ルートテーブルのターゲット切り替え(クラウド) 移行対象のVPCに関連するルートテーブルのターゲットをVGWからTGWに変更します。 ZOZOTOWNはネットワークもIaC管理の為、CI/CDパイプラインを通じてルートテーブルのターゲットをVGWからTGWに変更しました。 この自動化されたアプローチにより、ネットワーク設定の更新と展開を迅速かつ効率的に行うことができました。 ファイアウォール機器のスタティックルート切り替え(オンプレミス) オンプレミスとクラウドの通信にファイアウォール機器を使用しています。 ファイアウォール機器のスタティックルートがDX10G回線を指している為、DX100G回線へ向けてルーティングを更新します。 これにより、通信がDX100G回線を経由するようになり、高速で安定した接続が確保されます。 上記の作業を実施した結果、6個のVPCを20分程度でDX100Gに切り替えることができました。また、VGWの関連付け解除は、移行後の対応で問題ありません。 この過程で、継続的な接続確認のためにpingやtracertコマンドを活用し、新しい回線への正確な切り替わりを監視しました。また、より詳細なネットワークパフォーマンス分析のためにmtrコマンドも利用可能です。これらのツールを駆使することで、移行作業の安全性とスムーズな実行を確保しました。適切なモニタリング手法の選択は、環境に応じて異なりますが、移行プロセスの成功には欠かせません。 まとめ 本記事では回線逼迫を解決するために、DX10GからDX100Gへの移行方法を紹介しました。 DX10GからDX100Gへの移行は、オンプレミスからクラウドまで多岐にわたる作業が必要で、非常に手間がかかりました。 本作業は、幅広い技術と計画が求められ、チーム全体での協力が不可欠でした。特に、dev/stg環境での移行手順の事前検証や移行リハーサルが、スムーズな移行を実現するための大きな要因となりました。 結果として、この移行はZOZOTOWNのインフラを大幅に強化し、将来の拡張性とパフォーマンスの向上を実現させることができました。 DX100Gを検討している方がいれば、ぜひ参考にしてください。 ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください! corp.zozo.com
こんにちは。検索基盤部の橘です。検索基盤部では、ZOZOTOWNのおすすめ順検索の品質向上を目指し、機械学習モデル等を活用しフィルタリングやリランキングによる検索結果の並び順の改善に取り組んでいます。 ZOZOTOWN検索の並び順の精度改善の取り組みについては以下の記事をご参照ください。 techblog.zozo.com 検索基盤部では新しい改善や機能を導入する前にA/Bテストを行い効果を評価しています。A/Bテストの事前評価として、オフラインの定量評価と定性評価を実施しています。これらの評価によりA/Bテストの実施判断をしています。 おすすめ順検索のフィルタリング処理の効果検証として導入したオフライン定量評価の方法については以下の記事をご参照ください。 techblog.zozo.com 以前の記事で紹介したオフライン評価を日々運用する中で、幾つか課題点が見つかりました。本記事では、その課題点と改善方針について紹介します。 目次 目次 ZOZOTOWNのおすすめ順検索の商品ランキングロジックにおけるフィルタリング処理 フィルタリング処理における定量評価と定性評価 A/Bテストにおける定量評価と定性評価の位置付け 定量評価について 定性評価について オフライン評価で起こった問題 問題の原因分析アプローチ CVログ取得時と評価時の検索インデックス内容の差異 問題原因の分析 定量評価の改善 まとめ おわりに ZOZOTOWNのおすすめ順検索の商品ランキングロジックにおけるフィルタリング処理 ZOZOTOWNのおすすめ順検索の商品のランキングロジックは2つのフェーズに分けられます。 フィルタリング処理 : 再現率を高めることを目的にルールベースのロジックや軽量な機械学習モデルを用いて商品のフィルタリングを行います。 リランキング処理 : フィルタリング時のスコアトップN件に絞って「ランキング学習」と呼ばれる手法の機械学習モデルを用いた並び替え処理を行います。 フィルタリング処理における定量評価と定性評価 A/Bテストにおける定量評価と定性評価の位置付け 以下にA/Bテスト実施までのフローを示します。 オフラインの定量評価及び定性評価は、事前評価としてA/Bテストの前に実施します。 定量評価について 以前のテックブログ に記載している定量評価について抜粋してご紹介します。定量評価では、以下の評価指標により新ロジックの性能の良さを評価します。 評価指標 評価したい内容 評価結果の解釈 例 コンバージョンカバー率 コンバージョンの大幅な悪化がないこと 評価値が高いほどコンバージョンの悪化が少ない 新商品表示率 新ロジックが旧ロジックとは異なる商品を多く表示できるか 評価値が高いほど新ロジックは旧ロジックとは異なる商品を多く表示できる RBO(rank biased overlap) 新ロジックと旧ロジックの検索結果の並び順がどの程度類似しているか 評価値が低いほど新ロジックは旧ロジックの検索結果の並び順と異なる 0.8(0から1の値をとる) 定性評価について 次におすすめ順でのフィルタリング処理における定性評価について説明します。ZOZOでは社内で開発しているツールを用いて検索結果を定性評価しています。以下は、おすすめ順のフィルタリング処理の定性評価の流れです。 評価依頼者が評価対象の検索キーワードを評価者に割り振る 評価者がツールに評価対象の検索キーワードを入力する ツールが検索結果を新旧ロジックから取得し、それぞれの検索結果の差集合にあたる商品を画面に出力する 評価者は新旧ロジックそれぞれの結果のどちらが良いかを評価し、評価依頼者に結果を返却する 評価の流れのイメージは以下の通りです。 定性評価ツールの画面出力イメージは以下の通りです。 差集合にあたる商品のみを比較することで新旧ロジックの優劣を明確にします。 評価依頼者は複数の評価者による評価結果を集計し、新旧ロジックどちらが良いかの判定とA/Bテストに進むかを判定します。 現状の定量評価と定性評価は大幅に指標が上昇または下降していないかを確認する役割が大きいですが、理想的にはロジックの真の性能と定量評価の結果、定性評価の結果は相関することが望ましいです。もし性能の悪い新ロジックが定量評価で悪い評価結果になると、定性評価を実施する前に施策をストップします。 オフライン評価で起こった問題 前章のオフライン評価を用いたA/Bテストの事前評価を進めていくにあたり起こった問題について説明します。 定量評価の結果は良好であった新ロジックについて定性評価を引き続き実施したところ、定性評価の結果が悪いという結果になりました。以下の図に定量評価と定性評価の結果を示しています。 新ロジックでのみ抽出される商品(新ロジック抽出商品)のコンバージョン(CV)ログが存在することについては後ほど説明します。 定量評価では、コンバージョンカバー率と新商品表示率ともに高い値であり、新ロジックは良い結果となっています。 定性評価では、旧ロジックの方が良い検索結果となっている件数が多く、新ロジックは悪い結果となっています。 上に記載した通り本来は定量評価と定性評価の結果は相関するのが望ましいので上記は望ましくない結果であり、定量評価の方法もしくは定性評価の方法に改善すべき点があります。 双方の評価が相関しない原因を分析し、改善方針を検討しました。 問題の原因分析アプローチ この定量評価と定性評価の乖離の原因を分析するにあたり、どのようにアプローチしていけばよいでしょうか? 実際の分析アプローチとして、先ほどの図の定量評価の結果部分にあった『新ロジック抽出商品のCVログ』を活用しました。 この定量評価の指標を導入した当時は、新ロジック抽出商品のCVログは存在しない想定でした。検索結果に表示される商品は旧ロジックでのみ抽出される商品であるため、新ロジックでのみ抽出される商品のCVログは存在しないと想定していたためです。 しかし、実際にはCVログの中には新ロジック抽出商品のCVログが存在しました。この原因として、CVログ取得時と評価時の検索インデックス内容の差異の例をご紹介します。 CVログ取得時と評価時の検索インデックス内容の差異 以下の図のように、ZOZOTOWNの商品検索システムのインデックスは逐次更新されています。商品の売り切れや新商品の発売などに応じてインデックス内の商品が入れ替わります。よってCVログ取得時(ユーザーの検索行動時)は同じ旧ロジックを扱っていても検索結果は変化することがあります。 対して、評価時は旧ロジックによって表示された商品をログから集計せず特定のインデックスを使用し検索結果を抽出します。 このCVログ取得時と評価時のインデックスの違いが、CVログ取得時との検索結果の差分を発生させている原因です。 以上から、過去のインデックスから旧ロジックにより抽出された商品は、評価時のインデックスでは新ロジック抽出商品となりえることがわかりました。 問題原因の分析 定性評価の事後分析を実施し、先ほど説明したCVログを利用して新ロジック抽出商品ごとの過去のCV率を集計し、旧ロジック抽出商品のCV率と比較しました。新ロジック抽出商品と旧ロジック抽出商品のCV率のヒストグラムを作図すると以下のようになっていました(実際とは異なりますが、同様の傾向を持つデータです)。 この図から読み取れる新旧ロジックの特徴は以下の通りです。 新ロジック抽出商品はCV率のばらつきが比較的大きい。CV率が比較的高い商品を多く含むが、CV率が0の商品も多く含む。 旧ロジック抽出商品はCV率のばらつきが比較的小さい。 新ロジック抽出商品におけるCV率が0の商品を確認したところ、検索キーワードと無関係で検索意図に合わない商品となっていることがわかりました。 つまり、新ロジック抽出商品はユーザーの検索意図に合わない商品が多く含まれており、定性評価時にそれが原因で悪い評価結果になったことがわかりました。 ユーザーの検索意図に合わない商品が検索結果に多く含まれる場合、ユーザーは検索結果に対し満足せず再検索や検索から離脱する可能性があります。さらにはZOZOTOWNの検索機能やサービス自体に疑念を持つ場合も考えられます。 上記のリスクに関して、eコマースサイトの検索機能の改善ガイドラインを提供する Baymard Instituteのブログ でも、検索意図に合わない結果がサイトの離脱を招くと指摘されています。 以上より、検索意図に合わない商品数を確認できるよう定量評価を改善することにしました。 定量評価の改善 ある程度の量の新ロジック抽出商品のCVログが取得出来れば検索キーワードごとの新ロジック抽出商品のCV率が求められます。それを利用し、定量評価の時点でCV率が極端に低い(検索意図に合わない)商品数を新旧ロジックで比較することにしました。 この比較により検索意図に合わない商品の多さを定量評価の時点で把握でき、検索意図に合わない商品が多い傾向の新ロジックの検討を定量評価の時点で中止できます。 まとめ 本記事ではZOZOTOWNのおすすめ順検索の精度改善におけるオフライン評価を運用していた中で生じた課題について説明し、分析アプローチや評価方法のブラッシュアップの事例をご紹介しました。 引き続きオフライン評価を実施し評価方法のブラッシュアップを重ねていく予定です。 おわりに ZOZOでは、一緒にサービスを作り上げてくれる方を募集中です。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com
はじめに こんにちは、検索基盤部の広渡です。検索基盤部では、検索クエリのサジェスト(以下、サジェスト)の改善を行なっています。ここでサジェストは一般的に「Query Auto Completion」と呼ばれる、検索クエリを入力した際に入力の続きを補完したキーワードを提示する機能を指します。 ZOZOTOWNにおいては検索クエリを入力したとき、最大10件の検索クエリのサジェスト(以下、サジェストリスト)が表示されます(なお、ランキングを考慮しない場合はサジェスト集合と呼ぶこととします)。また、サジェストリストのランキングはユーザーの行動ログを用いて計算されたスコアによって決定されます。サジェストの具体的な説明や過去の改善事例は以下の記事を参照してください。 techblog.zozo.com techblog.zozo.com サジェストリストをチーム内で定性評価したところ、類似したサジェストが多く表示されることによる、多様性の乏しさを指摘する声が多くありました。多様性の乏しいサジェストリストの具体例は、「ゾゾ」「ZOZO」など表記揺れした同じ意味のサジェストや、「パンツ」「パンツ レディース」など性別のみ加わっただけのサジェストを含むものです。このような多様性の乏しいサジェストリストは、ユーザーに価値のあるサジェストが含まれず、結果的にユーザーの入力文字数が増えてしまい、検索体験の質を下げてしまう可能性があるため重要な課題です。 本記事では、サジェスト集合の多様性度を計測するdiversityと、サジェストリストの多様性度を計測するD#-nDCGについて紹介し、それらの指標とゴール指標との相関についての検証結果を解説します。ゴール指標は後ほど詳しく説明しますが、CTR(Click Through Rate)とCVR(Conversion Rate)です。多様性評価指標とゴール指標の相関を調査する目的は、多様性評価指標がZOZOTOWNのサジェスト改善に有効かを調べるためです。有効な指標を用いて新規サジェスト手法を計測することで、実際のユーザーの反応を見る前に改善が見込めるかどうかを判断でき、改善サイクルの効率化につなげられます。 目次 はじめに 目次 多様性評価指標 サジェスト集合の多様性度を表す指標 多様性を考慮したランキング指標 α-nDCG D#-nDCG ゴール指標と多様性評価指標の相関 測定方法 結果 まとめ おわりに 参考文献 多様性評価指標 ここでは、ランキングを考慮しないサジェスト集合の多様性度を表す指標について紹介し、その後、多様性を考慮したランキング指標について紹介します。 サジェスト集合の多様性度を表す指標 まず、 Ma, Hao et al, AAAI 2010 1 で提案されたdiversityと呼ばれるサジェスト集合の多様性度を測る指標について紹介します。 入力クエリ が与えられた際のサジェスト集合を とし、 番目のサジェストを とします。サジェスト集合 に対する多様性度は Ma, Hao et al, AAAI 2010 の定義に従うと、以下のように定義されます。 ここで であり、 は 番目のサジェスト と 番目のサジェスト との距離を表します。 このサジェスト間の距離 として、様々な定義が提案されています。 Ma, Hao et al, AAAI 2010 では、サジェスト経由で表示されたWebページのうち、クリックされた各ページに関連する検索クエリの集合のコサイン類似度を用いています。 Zhu, Xiaofei et al, WWW 2011 2 では、サジェストを経由して表示された上位10件のWebページのうち重複した割合で定義しています。 サジェスト間の距離が遠ければ遠いほど の値が大きくなり、サジェスト集合は多様であると判断できます。 多様性を考慮したランキング指標 diversityではサジェスト間の距離をもとに多様性度を測定していましたが、ランキングは考慮されていませんでした。ここでは多様性を考慮したランキング指標である、α-nDCGとD#-nDCGについて紹介します。ここからの説明は 情報アクセス評価方法論 3 を参考にしています。 α-nDCGとD#-nDCGは、サジェストリストがユーザーの多様な検索意図に適合するかを評価できるようにnDCG(normalized Discounted Cumulative Gain)を拡張したものです。ここでいう多様な検索意図とは、例えば"パンツ"を入力したユーザーはズボンを探している場合もあれば、下着を探している場合もあるなど、同一の検索クエリに対してユーザーの求める情報が異なることを指しています。 α-nDCG 本節では、 Clarke, Charles L.A. et al, SIGIR 2008 4 で提案されているα-nDCGについて紹介します。α-nDCGは、すでに検索された意図の利得を減衰させる特徴があります。 まず、 番目のサジェスト が 番目の意図 に適合するとき 、しないとき とします。 ここで、意図 に適合すると判断された 番目までのサジェストの個数を で表します。 このとき、 番目までサジェストリストを評価するための指標α-DCG(Discounted Cumulative Gain)は以下のように表されます。 ここで はパラメータであり、 情報アクセス評価方法論 によると通常0.5に設定されます。 そして理想的なランキングのα-DCGを求め、正規化したものがα-nDCGです。 α-nDCGによる評価は、 Zhu, Xiaofei et al, WWW 2011 , Cai, Fei et al, Foundations and Trends 2016 5 , Cai, Fei et al, ACM 2016 6 でも採用されています。 しかし、理論的には理想的なランキングを求める計算はNP完全であると Clarke, Charles L.A. et al, SIGIR 2008 で述べられています。 D#-nDCG α-nDCGにおけるNP完全を回避できる指標が、 Sakai, Tetsuya et al, SIGIR 2011 7 で提案されているD#-nDCGです。 D#-nDCGはサジェストリストを評価するD-nDCGと、検索意図の再現性を評価する意図再現率I-recの2つの要素から構成されています。まず、D-nDCGについて説明します。 検索窓に入力されたキーワード に対する意図 の確率を意図確率 とし、 番目のサジェスト の意図 に対する利得を とします。意図確率 のイメージとしては、"パンツ"が検索窓に入力されたとき、70パーセントがズボン、30パーセントが下着を意図しているといったものです。 このとき、 番目までのサジェストリストを評価するための指標D-DCGは以下のように表されます。 この式の分子 はグローバル利得と呼ばれます。 グローバル利得は、サジェスト が入力キーワード に適合する確率 を近似したものです。 このグローバル利得によりソートすることで理想的なランキングを求め、正規化したものがD-nDCGです。 次に、 Zhu, Xiaofei et al, WWW 2011 で提案されている意図再現率I-recについて説明します。 まず、 を、 番目までのサジェストリストの中で意図 に適合したものがあれば1、なければ0を表す変数とします。先ほどの を用いると、 で表されます。 このとき、 番目までのサジェストリストの意図再現率I-recは以下のように表されます。 ここまでで求めたD-nDCGと意図再現率I-recを線形結合させたものがD#-nDCGであり、以下のように表されます。 はパラメータであり0.5に設定されることが多いようです。 ゴール指標と多様性評価指標の相関 ここからは、上記で紹介した多様性評価指標であるdiversityとD#-nDCGが通常のnDCGと比べてゴール指標に相関するのか調査します。 ゴール指標であるCTR(Click Through Rate)とCVR(Conversion Rate)について説明します。CTRは(サジェストリストのクリック数/サジェストリストの表示数)で、CVRは(サジェストリストを経由して商品詳細に遷移した数/サジェストリストクリック数)としています。 α-nDCGではなくD#-nDCGを採用した理由は、先ほど説明にもある通りα-nDCGで生じるNP完全が回避できるためです。 本章では、各指標の計測方法を説明し、結果について述べます。 測定方法 まず、diversityの計測方法を説明します。 サジェスト間の距離 を計測するために、「サジェストを経由して遷移した商品の類似性」に着目しました。具体的な定義としては、遷移先商品の頻度をベクトル で表現し、そのコサイン類似度を用いました。 次に、D#-nDCGの計測方法を説明します。 はじめに、入力キーワードの意図 を定義する必要があります。α-nDCGを採用していた Cai, Fei et al, ACM 2016 では、クエリを通じてクリックされたURLを ODP(Open Directory Project) に基づき分類することで意図としていました。 ここから、ZOZOTOWNにおける商品URLも何らかの粒度で分類する必要がありました。クエリを通じてクリックされた商品URLをブランドとカテゴリーの組み合わせにより分類することで意図と定義しました。例えば、ユーザーがブランド「Hoge」のスニーカー商品をクリックするとURLは brand/hoge/shoes/sneakers/ になります。このURLからユーザーの検索意図は「Hoge スニーカー」と分類し定義しました。 この定義に基づき、意図確率 は、検索窓に文字列 を入力した後に検索された意図を集計することにより求めました。また、 番目のサジェスト の意図 に対する利得 は、サジェストを経由してクリックした商品が意図に適合していれば1、していなければ0としました。 最後に、nDCGの計測方法を説明します。 を 番目サジェストに対する利得としたとき、nDCGにおけるDCGは以下の式で表されます。 ここで利得 は、以下のようにサジェストを経由して商品詳細に遷移した回数によって定義しました。 利得=3: 100回以上 利得=2: 10回以上100回未満 利得=1: 1回以上10回未満 利得=0: 上記以外 ZOZOTOWNで実際にある1日で表示されたサジェストを使用して実験をしました。実験にあたっては、はずれ値の影響を抑えるために、以下に該当するサジェストリストのフィルタリング処理を事前に行いました。 CTRやCVRが0もしくは1 同じ商品を除き商品詳細に遷移した回数が3回未満 検索回数が5回未満 結果 以下の表にそれぞれの指標におけるCTR, CVRの相関係数を示します。有効数字3桁となるように四捨五入してあります。 nDCG diversity D#-nDCG CTR -0.00257 -0.153 0.253 CVR 0.177 -0.0353 0.219 D#-nDCGにおいては、弱い相関ではありますがnDCGよりも相関するという結果になりました。このことから、他の2指標と比較すると、D#-nDCGの値が高くなれば、ユーザーの検索体験の質が向上する傾向にあると言えます。 特に、CTRについてはnDCGとの差が大きいという結果になりました。これは、D#-nDCGはnDCGと比べ、クリックされた情報だけでなく、入力キーワードの意図をどの程度再現できるかを考慮している特徴が影響していると考察しました。 diversityがnDCGと比べ相関が弱くなった原因として、下記の影響が考えられます。 ZOZOTOWNではユーザーごとに異なる検索結果を表示しているため、いかなるサジェスト集合でも、サジェスト間の距離が大きく計測され多様性度も高くなる傾向にある。 入力キーワードによっては、多様なサジェストが表示される必要のない場合を考慮できない。例えば、入力キーワードが特定の商品名の場合は、遷移先の商品の多様性が乏しいためサジェスト集合の多様性度は低くなるが、CTRは低くならない。 まとめ 本記事では、サジェストにおける多様性評価指標と、それらの指標とCTR/CVRとの相関について紹介しました。実際に多様性評価指標を使うと、通常のnDCGに比べてCTR/CVRと相関することが分かりました。 引き続きサジェストの評価指標を調査し、さらにオフライン精度評価体制を整えていきます。 おわりに ZOZOでは検索エンジニア・MLエンジニアのメンバーを募集しています。ご興味のある方は、以下のリンクからぜひご応募ください。 corp.zozo.com 参考文献 Ma, Hao and Lyu, Michael and King, Irwin. (2010, January). Diversifying Query Suggestion Results. In Proceedings of the Twenty-Fourth AAAI Conference on Artificial Intelligence (pp. 1399-1404). ↩ Zhu, Xiaofei and Guo, Jiafeng and Cheng, Xueqi and Du, Pan and Shen, Hua-Wei. (2011, March). A Unified Framework for Recommending Diverse and Relevant Queries. In Proceedings of the 20th International Conference on World Wide Web (pp. 37–46). ↩ 酒井 哲也.(2015, 6月). 情報アクセス評価方法論ー検索エンジンの進歩のためにー. コロナ社 (pp. 73-82). ↩ Clarke, Charles L.A. and Kolla, Maheedhar and Cormack, Gordon V. and Vechtomova, Olga and Ashkan, Azin and Buttcher, Stefan and MacKinnon, Ian. (2008, July). Novelty and diversity in information retrieval evaluation. In Proceedings of the 31st annual international ACM SIGIR conference on Research and development in information retrieval (pp. 659–666). ↩ Cai, Fei and de Rijke, Maarten. (2016). A Survey of Query Auto Completion in Information Retrieval. In Foundations and Trends in Information Retrieval (pp. 1-92). ↩ Cai, Fei and Reinanda, Ridho and Rijke, Maarten De. (2016, September). Diversifying Query Auto-Completion. In ACM Transactions on Information Systems (pp. 1-33). ↩ Sakai, Tetsuya and Song, Ruihua. (2011, July). EvaLuating diversified search results using per-intent graded relevance. In Proceedings of the 34th international ACM SIGIR conference on Research and development in Information Retrieval (pp. 1043–1052). ↩
はじめに こんにちは、技術本部SRE部カート決済SREブロックの遠藤・金田です。 普段はSREとしてZOZOTOWNのカート決済機能のリプレイスや運用を担当しています。本記事では自作のコマンドラインツールをSlack + AWS Chatbot + AWS Lambdaを使用してChatOps化した事例をご紹介します。「日々の運用業務をコマンドラインツールを実装して効率化したものの今ひとつ広まらない」「非エンジニアにも使えるようにしたい」と考えている方の参考になれば幸いです。 目次 はじめに 目次 背景・課題 ChatOpsとは AWS ChatBotとは 構成 AWS ChatBot チャットツール側の設定 Slack Workflow Lambda 実装のポイント ChatBotのアクセス制御 User Roleの運用方法 ガードレールポリシー コマンドラインツールのLambda関数化 コマンドラインツールについて local環境での実行とPod起動コマンドの共通化 cgroupによる実行環境の判定 ツールのコードを再利用したLambda関数の実装 main関数の差し替え 実行結果の通知方法 標準出力をキャプチャする実装 ChatOps化の効果 環境構築や準備が不要 本番環境の権限が不要 実行履歴が残る さいごに 背景・課題 私たちの日々の運用業務の1つに過熱登録という作業があります。 福袋や限定品など、ユーザーから大量のアクセスが来る商品のことを弊社では「過熱商品」と呼んでいます。過熱商品は販売開始のタイミングで大量のアクセスが発生します。ZOZOTOWNのカート機能では過熱商品の販売によりアクセスが急増した際にも過熱商品ではない商品の購入に支障が出ないようにシステムを構築しています。 詳しくは以下のテックブログをご覧ください。 techblog.zozo.com 「過熱登録」とは、上記で紹介している、カート投入された商品が過熱商品であるかを判断するためのデータベース(以下:DB)に、その商品情報を登録する作業を指します。 このプロセスでは、商品ごとに一意のIDを用いてDB内に登録していきます。 過熱登録に使用するIDは商品のサイズや色毎に発行されるIDを使用しており、1つの商品に紐づく全てのIDをDBから抽出しそれらを個別に登録する必要があるため、手作業で行うと手間のかかる面倒な作業でした。 上記の課題を解決するため、DBから関連情報を抽出し一括で過熱登録するコマンドラインツールをGo言語で実装し運用していましたが、以下のような別の課題が見えてきました。 ツールを使用するための事前準備が必要 本番環境に接続する権限が必要 実行履歴が残らない コマンドラインツールのため非エンジニア向けではない これらの課題を解決する手法としてChatOpsの採用を決め実装しました。 ChatOpsとは ChatOpsとは、Slackなどのチームコラボレーションツールと自動化を組み合わせたもので、システム運用における運用業務を自動化するものです。ChatOpsはDevOps文化と密接に関わっており、作業の透明性や効率性を高めることができます。 AWS ChatBotとは AWS ChatBotとは、Slackなどのチャットツールと連携してAWSリソースを管理、監視、操作するためのAWSサービスです。 以下のような特徴が挙げられます。 通知とアラート:AWSのサービスやリソースに関するイベントやアラートをリアルタイムでチャットツールに通知できます。 アクションの実行:ChatBotを使用してAWSリソースを操作できます。awsコマンドで実施できることは一通り実行できます。 アクセス制御:IAMを使用して、特定のチャンネル、ユーザーにのみAWSリソースへのアクセスを許可します。これによりセキュリティを維持しながらチーム全体でのAWSリソースの管理が可能になります。 また、2024/2時点で以下の3つのチャットツールと連携が可能です。 Amazon Chime Slack Microsoft Teams 構成 ChatOpsのシステム構成は以下の通りです。 ChatOps用のSlackチャンネルからワークフローを実行し、対象の商品IDなど必要な情報を入力します。新しいスレッドが作成され、AWS ChatBotに対してコマンドが実行されます。ChatBot経由でAWS Lambdaを実行し、過熱登録ツールが実行されます。 AWS ChatBot ZOZOではチャットツールとしてSlackを使用しているため、本記事ではSlackとChatBotの連携方法について記載します。 はじめに、SlackとAWS ChatBotを連携する必要があり、Slackワークスペースに対して、AWS ChatBotアプリのアクセスを許可します。 その後、AWS ChatBotのリソースを作成します。 AWS ChatBotのCloudFormationは非常にシンプルです。WorkspaceIdとChannelIdを指定して、チャンネル単位で連携します。AWS ChatBotには後段のAWS Lambdaを実行するRoleを付与しています。 Resources : ZozoCartChatOpsChatbot : Type : AWS::Chatbot::SlackChannelConfiguration Properties : SlackWorkspaceId : !Ref TargetWorkspaceId SlackChannelId : !Ref TargetChannelId ConfigurationName : !Ref ConfiguredChannelName IamRoleArn : !GetAtt ZozoCartChatOpsChatbotRole.Arn LoggingLevel : ERROR ZozoCartChatOpsChatbotRole : Type : AWS::IAM::Role Properties : RoleName : zozo-chatops-chatbot AssumeRolePolicyDocument : Version : '2012-10-17' Statement : - Effect : Allow Principal : Service : - chatbot.amazonaws.com Action : - sts:AssumeRole ManagedPolicyArns : - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole Policies : - PolicyName : zozo-chatops-chatbot-policies PolicyDocument : Version : '2012-10-17' Statement : - Effect : Allow Action : - 'lambda:invokeAsync' - 'lambda:invokeFunction' Resource : - !GetAtt ZozoChatOpsLambda.Arn チャットツール側の設定 ChatBotリソースが作成されたら、ChatBotを利用したいSlackチャンネル上で以下のコマンドを実行しAWSアプリを追加します。 /invite @aws これでSlackからAWS ChatBotを利用する準備が整いました。 Slackの場合、パブリックチャンネル/プライベートチャンネルのどちらでも連携可能です。権限設定については後述しますが、管理をシンプルにするためプライベートチャンネルを採用し、作業者のみをチャンネルに招待して利用を開始しました。 Slack Workflow Slackにはワークフローという自動化機能があり、社内でも多く活用しています。作業をより簡単にするため、ChatOpsでもSlackワークフローを使って簡易化を検討しました。 自動化する作業ごとにワークフローを作成します。ワークフローを実行するとスレッド上でawsコマンドが投稿され、AWS ChatBotへ命令を渡すことができます。 aws lambda invoke コマンドを実行して任意のLambda関数を実行します。 Lambda ChatOpsから任意の処理を実行する基盤としてAWS Lambdaを採用しました。選定理由としては以下が挙げられます。 ChatBotから実行できワークフローで指定したパラメータを渡せる 今後様々な用途でChatOpsを利用することを考え、柔軟に実装できる コストが安い ChatOpsは運用者がコマンドを実行しない限り発動しないため、実行時間での課金であるAWS LambdaはChatOpsの実行基盤として相性が良いです。 ChatBotを経由して以下のコマンドを実行することでLambda関数を実行します。 @aws lambda invoke --payload '{JSON形式で過熱商品IDなどを渡す}' --function-name [Lamda関数名] --invocation-type Event --region ap-northeast-1 --payload オプションでLamdbaへ任意のパラメータを渡すことができるため、ワークフロー内で入力された情報をLambdaに渡すことができます。 実装のポイント ChatBotのアクセス制御 AWS ChatBotの権限設定には2つの設定範囲があります。 Channel Role User Role Channel Roleは、SlackチャンネルごとにIAM Roleを付与します。そのSlackチャンネルに参加するメンバーは同じ権限でChatBotにコマンドを送ることができます。 User Roleは、Slackユーザーごとに個別のIAM Roleを付与できます。チャンネル内のメンバーごとに、異なる権限を与えたい場合にはこちらが適しています。 理想としては、ChatOps用のSlackチャンネルをパブリックチャンネルにして、一部のメンバーのみが過熱登録できる状態が望ましいです。そのためUser Roleを使いたいと考えましたが、後述する問題がありSlackチャンネルはプライベートチャンネルにしてChannel Roleを使って運用しています。 User Roleの運用方法 ChatBotの設定でUser Roleを有効化することで、Slackユーザーごとに個別のIAMロールを付与できます。 しかし、User Roleの設定は各ユーザーがAWSのマネジメントコンソールにアクセスして、ChatBotの画面からSlackへ連携する必要があります。 この連携設定はCloudFormationで管理できません。AWSへの権限がある程度必要になるため、AWSへの権限を持っていないユーザーはそもそもこの設定ができません。 また、設定時に付与するIAM Roleを自分で設定する必要があり、統制を取ることが難しいと感じました。そのため、一旦プライベートチャンネルとChannel Roleでの運用としています。 ガードレールポリシー 上記のロール設定とは別にAWS ChatBotにはガードレールポリシーという設定があります。ガードレールポリシーで権限を制限することで、その範囲を超えて権限が付与されることを防ぎます。 ガードレールポリシーはデフォルトでAdministratorAccessとなっているため、制限がない状態です。ChatBotからアクセスする想定がないリソースはここで権限を制限しておくと安心です。 これにより、誤ってAWS ChatBotに強い権限が付与されてしまう事故を防ぐことができます。 コマンドラインツールのLambda関数化 コマンドラインツールについて 過熱登録用コマンドラインツール(以下、過熱登録ツールと記載します)についても簡単に触れておきたいと思います。 過熱登録の作業を効率化するために実装したGo言語製ツールで、機能毎にサブコマンドとして実装しています。 機能(サブコマンド) 処理内容 商品情報の取得 指定されたIDに紐づく商品情報をDBから取得して出力する。 過熱登録 1つの商品に紐づく全てのIDをDBから取得して過熱登録用のAPIサーバに登録リクエストを行う。 過熱商品の検知と自動登録 カート投入のログから過熱商品の発生を検知して自動で過熱登録する。ArgoWorkflowから定期的に実行する。 local環境での実行とPod起動コマンドの共通化 過熱登録ツールの各機能は主にDBサーバからの情報取得とEKS上で稼働しているAPIサーバへのリクエストで構成されます。 アクセス権限やDB接続情報などの秘密情報を管理する観点から、DBサーバやAPIサーバへのアクセスはlocal環境で実行する過熱登録ツールからは行わず、EKS上にPodを起動して行う構成としています。 Pod起動に使用するコンテナイメージは過熱登録ツールのバイナリを /usr/local/bin に配置したイメージをECRに登録して使用しています。 以下のDockerファイルでイメージをビルドしています。 FROM golang:1.21.4 WORKDIR /go/src/hotitem-tool COPY . . RUN go mod tidy # GOOSとGOARCHの設定以外はソースコードも含めlocal環境用のビルドと同一の設定でビルド RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /usr/local/bin/hotitem-tool . CMD [ "hotitem-tool" ] 過熱登録ツールはlocal環境で起動されたらPod起動の処理を行い、コンテナ環境で起動されたら各種サーバへのアクセスを行いビジネスロジックを実行する構成になっています。 local環境用/コンテナ環境用といったようにツールの実装を分けてしまうと管理が煩雑になってしまいます。そのため過熱登録ツール内部で起動された環境がlocal環境か/コンテナ環境かを判別して、Podを起動するか/ビジネスロジックを実行するかを分岐しています。 下記は商品情報の取得処理を実行する場合のツール起動コマンドとPod起動設定です。 local環境でのコマンド実行例です。 hotitem-tool get-goodsinfo --all 1234 上記コマンドで実行した場合、過熱登録ツールは以下のマニフェストに相当するspec設定で client-go を利用してPodを起動します。 apiVersion : v1 kind : Pod name : hotitem-tool namespace : <namespace> spec : containers : - name : hotitem-tool image : <ECRにpushした過熱登録ツールコンテナイメージのURI> command : - hotitem-tool - get-goodsinfo - --all - 1234 ツール起動時に指定されたサブコマンド/オプション/引数の値を全て引き継いでコンテナの起動コマンドを設定してPodを起動しています。Pod起動によってコンテナ環境で実行された過熱登録ツールは、commandに設定されたサブコマンドや引数に応じたビジネスロジックを実行します。 このような構成にすることで実装を分けずにツール単体でクライアント/サーバ型のアプリケーションと近い構成を実現しています。 cgroupによる実行環境の判定 ツールがどの環境で起動されたかの判定にはcgroupを利用しています。 cgroup(Control Group)はLinuxカーネルの機能の1つで、システムリソースを使用するプロセスをグループ化し、グループに対してのリソース使用量を制限できる機能です。cgroupはコンテナの実現に重要な役割を担っておりKubernetesにおいてもnodeリソースを管理するために利用されています。 /proc/<pid>/cgroup を確認すると <pid> のプロセスがどのコントロールグループに属しているかがわかるようになっています。 下記は開発環境のEKSで稼働しているコンテナのcgroupを実際に出力してみた結果です。コンテナのメインプロセスはpid:1で起動されるためpidは1を指定しています。 / # cat /proc/1/cgroup 11:cpuset:/kubepods.slice/kubepods-pod1f9bc0a5_a5bc_4707_b55e_f9accda568af.slice/cri-containerd-7e3aab58c075f4d2b7e00a7f91d5bc9ed827fb141f0cc2a95ab25083c71d0f6e.scope 10:hugetlb:/kubepods.slice/kubepods-pod1f9bc0a5_a5bc_4707_b55e_f9accda568af.slice/cri-containerd-7e3aab58c075f4d2b7e00a7f91d5bc9ed827fb141f0cc2a95ab25083c71d0f6e.scope 9:freezer:/kubepods.slice/kubepods-pod1f9bc0a5_a5bc_4707_b55e_f9accda568af.slice/cri-containerd-7e3aab58c075f4d2b7e00a7f91d5bc9ed827fb141f0cc2a95ab25083c71d0f6e.scope 8:pids:/kubepods.slice/kubepods-pod1f9bc0a5_a5bc_4707_b55e_f9accda568af.slice/cri-containerd-7e3aab58c075f4d2b7e00a7f91d5bc9ed827fb141f0cc2a95ab25083c71d0f6e.scope 7:perf_event:/kubepods.slice/kubepods-pod1f9bc0a5_a5bc_4707_b55e_f9accda568af.slice/cri-containerd-7e3aab58c075f4d2b7e00a7f91d5bc9ed827fb141f0cc2a95ab25083c71d0f6e.scope 6:net_cls,net_prio:/kubepods.slice/kubepods-pod1f9bc0a5_a5bc_4707_b55e_f9accda568af.slice/cri-containerd-7e3aab58c075f4d2b7e00a7f91d5bc9ed827fb141f0cc2a95ab25083c71d0f6e.scope 5:memory:/kubepods.slice/kubepods-pod1f9bc0a5_a5bc_4707_b55e_f9accda568af.slice/cri-containerd-7e3aab58c075f4d2b7e00a7f91d5bc9ed827fb141f0cc2a95ab25083c71d0f6e.scope 4:blkio:/kubepods.slice/kubepods-pod1f9bc0a5_a5bc_4707_b55e_f9accda568af.slice/cri-containerd-7e3aab58c075f4d2b7e00a7f91d5bc9ed827fb141f0cc2a95ab25083c71d0f6e.scope 3:devices:/kubepods.slice/kubepods-pod1f9bc0a5_a5bc_4707_b55e_f9accda568af.slice/cri-containerd-7e3aab58c075f4d2b7e00a7f91d5bc9ed827fb141f0cc2a95ab25083c71d0f6e.scope 2:cpu,cpuacct:/kubepods.slice/kubepods-pod1f9bc0a5_a5bc_4707_b55e_f9accda568af.slice/cri-containerd-7e3aab58c075f4d2b7e00a7f91d5bc9ed827fb141f0cc2a95ab25083c71d0f6e.scope 1:name = systemd:/kubepods.slice/kubepods-pod1f9bc0a5_a5bc_4707_b55e_f9accda568af.slice/cri-containerd-7e3aab58c075f4d2b7e00a7f91d5bc9ed827fb141f0cc2a95ab25083c71d0f6e.scope / # cgroupは階層構造を持っており、上記の出力でも確認できる通りKubernetesで起動されるコンテナのcgroupは必ず kubepods.slice/ というユニットに属します。 以上のことを利用し、過熱登録ツールでは以下の実装で実行環境を判定しています。 func IsInPods() bool { _, err := os.Stat( "/proc/1/cgroup" ) if err != nil { return false } return grep( "(kubepods)" , "/proc/1/cgroup" ) } func grep(pattern, filename string ) bool { file, err := os.Open(filename) if err != nil { return false } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() if match, _ := regexp.MatchString(pattern, line); match { return true } } if err := scanner.Err(); err != nil { return false } return false } ツールのコードを再利用したLambda関数の実装 過熱登録に必要な機能は、過熱登録ツールに実装済みであるため、実装済みの機能はChatOps用に再実装することなくそのまま活かしたい、ソースコードも同一リポジトリで管理したいと考えました。 Go言語でLambda関数を作成するには aws-lambda-go を使って以下のようなmain関数とハンドラ関数を実装します。 package main import ( "context" lambdago "github.com/aws/aws-lambda-go/lambda" ) type Option struct { Name string Value string } type Options []Option // 引数で受け取るペイロードは独自形式で定義できます type Payload struct { Command string Options Options Arguments string } func main() { lambdago.Start(handler) } func handler(ctx context.Context, event Payload) ( string , error ) { switch event.Command { case "get-goodsinfo" : return executeGetGoodsInfoCommand(event) case "regist" : return executeRegistCommand(event) } return "" , nil } 〜略〜 実装が完了したら以下の手順でLambda関数を作成します。 コンテナイメージをビルド ECRにpush Lambda関数の作成オプションでコンテナイメージを選択し、ECRにpushしたコンテナイメージのURIを設定 過熱登録ツールがlocal環境側で行う処理は、EKSにPodを起動して実行結果を受け取り出力することだけです。 Lambda関数化しても行う処理は同様で、EKSにPodを起動して実行結果を受け取り出力できれば良いです。そのためmain関数をコマンドラインツール用の実装からLambda関数用の実装に切り替えることができれば、既存実装をそのまま活かしてLambda関数化できます。 main関数の差し替え Go言語には実行対象の環境毎に実装を切り替えることができる ビルド制約 (ビルドタグとも呼ばれます)という機能があります。 ビルド制約は基本的にはOS/ARCHといったビルド時に設定する実行環境の情報を使ってビルドに含む/含まないといった制御ができる機能ですが、独自タグの定義による制御もできます。 本記事ではLambda関数用のビルドに含みたいファイルは、ファイルの先頭に //go:build lambda_handler というビルドタグを定義します。元のmain関数が実装してあるファイルの先頭にはLambda関数用のビルド時に含まれないように //go:build !lambda_handler を定義します。 そして go build コマンドのオプションに -tags lambda_handler を設定することで独自タグの定義が有効になりmain関数の差し替えを実現しています。 Lambda関数用のコンテナイメージをビルドするDockerfileは以下のように記述しています。 FROM golang:1.21.4 AS lambda-builder WORKDIR /go/src/hotitem-tool COPY . . RUN go mod tidy RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /usr/local/bin/hotitem-tool -tags lambda_handler . # Lambda関数実行時にawsコマンドでeks clusterの設定をするためaws-cliのイメージをベースにする FROM --platform=linux/amd64 amazon/aws-cli:2.14.1 AS lambda-handler WORKDIR /etc/ # awsコマンドでeks clusterのconfig設定をした後にハンドラ関数を実行するshellをentrypointに設定 COPY startup.sh . RUN chmod 755 startup.sh COPY --from=lambda-builder /usr/local/bin/hotitem-tool . ENTRYPOINT [ "/bin/sh" , "-c" , "./startup.sh" ] startup.sh の実装です。ハンドラ関数からPod起動ができるようにツール実行前にtokenを取得してKubeConfigを更新しています。 #!/bin/sh if [ -z " $AccountID " ] || [ -z " $ClusterName " ]; then echo " Error: environment variables are not set. " exit 1 fi export KUBECONFIG =/tmp/kubeconfig aws eks get-token --cluster-name " arn:aws:eks:ap-northeast-1: $AccountID :cluster/ $ClusterName " aws eks update-kubeconfig --name " $ClusterName " ./hotitem-tool " $@ " 過熱登録ツールは各機能をサブコマンドで実装しており、main関数を実装しているファイルはコマンドの定義だけです。そのためmain関数を実装しているファイルのみ差し替えることで既存実装に手を加えることなくLambda関数化できました。 実行結果の通知方法 過熱登録ツールは元々コマンドラインツールとして実装した関係で、実行結果の出力は標準出力で行っています。ChatOps化するにあたって実行結果はSlackに通知する必要がありました。 しかし、ChatBotの欠点として、タイムアウト値を10秒から変更できないことが挙げられます。 処理に10秒以上かかる場合、ChatBotによってレスポンスを返すことができないため、処理の中でWebhookによってレスポンスを通知するという方法をとりました。 そのため、AWS Lambdaの実行は -invocation-type オプションで非同期(Event)としています。 Lambda関数の標準出力は自動的にCloudWatchのロググループに /aws/lambda/<LambdaFuntionName> の形式で保存されます。 CloudWatchAlerm/AmazonSNS/AWS Chatbotを組み合わせてSlack通知する方法もありますが、本記事では以下の理由からツール側でSlack通知する方針にして実装しました。 元々ArgoWorkflowで実行した処理結果のSlack通知をツール側で実装済みだった Slack通知する際のフォーマットをツール側で定義・管理したい Slack通知する実装は既にツール側に存在していましたが、Lambda関数から実行した場合、実行結果を標準出力ではなくSlack通知へ切り替える実装をする必要があります。 真っ先に思いつく方法は各機能の引数にLambdaからの実行かどうかのフラグを持たせて標準出力とSlack通知を分岐させる方法です。しかしフラグを用いた方法だと既存実装に手を加える必要があり修正箇所も多くなるため、本記事では別の方法を採用しました。 具体的には以下のような標準出力をキャプチャする処理を実装し、ハンドラ関数のはじめにInit関数の呼び出しと終了時にPostする処理を呼び出すようにしています。 こうすることで各機能の実装に手を加えることなく標準出力をSlack通知に差し替えています。 標準出力をキャプチャする実装 //go:build lambda_handler package lambda import ( "bytes" "fmt" "io" "os" "regist-hotitems/slack" // Slackへのpostとフォーマットする関数を実装した独自モジュール ) type StdoutCapture struct { orgStdout *os.File bufChan chan string writer *os.File reader *os.File } func (s *StdoutCapture) Init() error { var err error s.orgStdout = os.Stdout s.reader, s.writer, err = os.Pipe() if err != nil { return err } os.Stdout = s.writer s.bufChan = make ( chan string ) go func () { var b bytes.Buffer io.Copy(&b, s.reader) s.bufChan <- b.String() }() return nil } func (s *StdoutCapture) Close() { s.writer.Close() s.reader.Close() os.Stdout = s.orgStdout } func (s *StdoutCapture) PostWebhookForGetGoodsInfo(title, url string ) error { s.writer.Close() s.reader.Close() os.Stdout = s.orgStdout // 標準出力をパースしてSlack通知用のフォーマットに変換する関数の呼び出し mes, err := slack.GenerateLambdaGetGoodsInfoNotificationMessage(title, <-s.bufChan) if err != nil { return fmt.Errorf( "failed to generate message: %w" , err) } if err := slack.PostWebhook(url, mes); err != nil { return fmt.Errorf( "failed to post webhook: %w" , err) } return nil } ハンドラ関数の先頭で標準出力をキャプチャする処理を初期化して、 defer で終了時にSlackへpostする処理を実行する。 //go:build lambda_handler package main import ( 〜略〜 "regist-hotitems/lambda" ) 〜略〜 func handler(ctx context.Context, event Payload) ( string , error ) { switch event.Command { case "get-goodsinfo" : return executeGetGoodsInfoCommand(event) case "regist" : return executeRegistCommand(event) } return "" , nil } func executeGetGoodsInfoCommand(event Payload) ( string , error ) { capture := lambda.StdoutCapture{} if err := capture.Init(); err != nil { return "" , fmt.Errorf( "failed to init stdout capture: %w" , err) } defer capture.PostWebhookForGetGoodsInfo( "get-goodsinfo result" , os.Getenv( "WebhookURL" )) 〜略〜 標準出力差し替えの詳細については ZOZO AdventCalender 2023 に投稿した記事で解説しているので、興味がある方はご覧ください。 qiita.com ChatOps化の効果 すでに本番環境の運用をChatOpsに切り替えており、ChatOps化したことであらゆるメリットを実感しています。 環境構築や準備が不要 今までは、運用業務を実施するメンバーがローカル環境を構築する必要がありました。運用ツールをアップデートするたびに、ツールをPullして再ビルドする必要がありましたがそれらが不要になりました。 本番環境の権限が不要 今までは、本番環境に対する権限を作業者に付与する必要がありましたが、AWS ChatBotを活用することで、本番環境の権限を直接払い出す必要がなくなりました。 また、作業に必要な権限が明確になる効果もあると感じました。 実行履歴が残る 誰がいつ何の作業をしたかを自動的に履歴として残すことができます。 また、そのままチャット上で会話に繋がるため、作業の共有やコミュニケーションが促される効果もあると感じました。 さいごに 本記事で紹介した過熱登録ツールを皮切りに、今後も様々な運用作業をChatOps化していこうと思います! ZOZOTOWNでは、SREとして自動化やインフラ構築に携わってくれるメンバーを募集しています! corp.zozo.com
こんにちは、MA部の齋藤( @kyoppii13 )です。 ZOZOTOWNでは、プッシュ通知やLINE、メールでのキャンペーン配信を実施しています。キャンペーン配信の例としては、お気に入り商品の在庫数が少なくなったときにプッシュ通知を送るといったものです。LINEやメールといった配信チャネル以外にも、キャンペーンごとにセグメントや実施タイミングも様々で、システムも配信キャンペーンの種類によって複数存在している状況でした。そのため運用保守のコストが大きくなっていました。また、キャンペーンの内容を変更するために開発側での工数が発生している状況でした。 そこでキャンペーン配信を効率的に実施するため社内向けのマーケティングプラットフォーム「ZOZO Marketing Platform(ZMP)」を開発しました。 本記事では、マーケティングプラットフォームの開発にあたって考慮した点とアーキテクチャについて紹介します。 ZOZOTOWNでのキャンペーン配信 セグメント コンテンツ 配信タイミング その他設定 これまでの運用フロー 課題 運用・保守コストの肥大化 新規開発にリソースが割けない キャンペーンを簡単かつ柔軟に追加・修正できない 新しいプラットフォームに求められること 開発フェーズと実現できること アーキテクチャと開発の進め方 既存システム バッチ配信 リアルタイムイベント配信 既存アーキテクチャ ZMPのアーキテクチャ アーキテクチャの考慮点 耐障害性 管理データの特性と処理特性 複数チームでの開発 拡張性 機能の統一 概念モデル セグメント コンテンツ オファー キャンペーン 各モジュールについて 管理画面 管理画面を使ったZMPの運用フロー MA基盤 MAモジュール MAマネージャー 配信基盤 開発の結果 開発を振り返って 今後の展望 まとめ 最後に ZOZOTOWNでのキャンペーン配信 ZOZOTOWNでは様々なキャンペーン配信を実施しています。キャンペーン配信をする際は以下の項目を検討する必要があります。 概要 説明 セグメント 誰に配信するか コンテンツ どのようなデザインでどのような情報を配信するか 配信タイミング どんなイベントをトリガーとするか その他設定 ABテスト、最適化などの設定 セグメント セグメントとは配信対象者のグループです。このセグメントは全ユーザーの場合もあれば、性別や年齢などのユーザー属性で絞り込む場合もあります。 コンテンツ コンテンツとはユーザーに配信する内容です。コンテンツは配信チャネルによって異なります。例えば、プッシュ通知の場合はタイトルと本文、画像を設定します。一方で、メールの場合はHTMLを設定します。 配信タイミング 配信タイミングとはキャンペーン発火のタイミングです。トリガーの種類によって、「バッチ配信」と「リアルタイムイベント配信」の2つに分かれます。バッチ配信は時間をトリガーとして配信するものです。例えば、新しいセールが始まるとき全ユーザーに通知するといったキャンペーンがあります。 リアルタイムイベント配信はユーザーの行動や商品在庫をトリガーとして配信するキャンペーンです。例えば、あるアイテムをお気に入り登録しているユーザーに対して、そのアイテムの在庫数が残り1つになったら通知する配信です。 バッチ配信は特定のユーザーセグメントに対して一括で送信するため「マス配信」とも呼んでいます。 その他設定 その他にはABテストを実施するか、配信時間を最適化するかなどを設定します。配信時の最適化は例えば配信する時間帯をユーザーごと最適なタイミングにする、配信数が増えすぎないように調整するといったものがあります。 これまでの運用フロー MA部では様々なキャンペーン特性に応じて複数のシステムを運用・管理していました。 バッチ配信ではワークフローエンジンのDigdagを利用したり、LINE Friendship Manager(LFM)やMail Banner Manager(MBM)といった社内ツールを自分たちで開発して利用しています。 リアルタイムイベント配信はリアルタイムマーケティングシステム(RTM)を利用しています。こちらも自分たちで開発したツールになります。 このように様々存在するキャンペーンを特性ごとに異なるシステムを利用して、担当者は以下のフローで運用していました。 各システムの詳しい説明については以下のテックブログを参照ください。 techblog.zozo.com techblog.zozo.com techblog.zozo.com techblog.zozo.com 課題 現状のキャンペーン配信では以下のような問題点がありました。 運用・保守コストの肥大化 新規開発にリソースが割けない キャンペーンを簡単かつ柔軟に追加・修正できない 運用・保守コストの肥大化 キャンペーンは様々な種類が存在しており、キャンペーン特性によってシステムも異なっていました。前の章でも説明したとおり、リアルタイムイベント配信はRTM、バッチ配信はワークフローエンジンのDigdagのように分かれていました。配信システムもチャネルや配信タイミングで分かれています。このようにシステムが分かれているのは、その時々で部分最適化しながら開発してきたためです。また、システムパフォーマンスを維持するための定期的なメンテナンスも必要です。中には古くから運用されているものもあり、属人化も顕著になっていました。 新規開発にリソースが割けない 運用・保守コストが大きいため、新規開発にリソースが割けない状況でした。また古くから運用されているシステムは特殊な技術スタックを採用しているものも多く、新規での人材採用が困難になり、新しいメンバーの育成にもコストがかかっていました。 また、古いシステムにはその仕様からテストやロールバックが困難なものもありました。そのため、新規の改修が難しく、開発工数も大きくなっていました。 キャンペーンを簡単かつ柔軟に追加・修正できない キャンペーンは新たに追加したり、既存キャンペーンの内容を修正したりもします。このキャンペーンの追加・修正作業は配信するキャンペーンの種類によっては管理画面が存在し、施策担当者のみで作業が完結するものもあります。一方で、管理画面が存在せずキャンペーン追加・修正時に開発側での工数が発生しているものもありました。例えば、RTMがその1つです。結果として、キャンペーンの企画から実施までを施策担当者だけで完結できず、キャンペーン実施までのリードタイムが長くなっていました。 また、前述の課題から新規開発にリソースが割けない状況であるため、最適化ロジックの追加修正もできず、最適なキャンペーンが実施できていない状況でした。 ここまで述べた課題を解決するものが必要でした。 新しいプラットフォームに求められること 前章で述べた課題を解決するために新規プラットフォームの構築を計画しました。プラットフォームに求められることは以下です。 システムがシンプルで、運用・保守や機能開発の工数を低減できる 既存システムと機能やパフォーマンスが変わらない 施策担当者(非エンジニア)だけでキャンペーンの追加・修正ができる 1つ目はシステムの運用・保守や新たな機能追加の工数を削減し、エンジニアの負荷を低減するためです。これによって、前述の運用保守コストの課題を解決し、結果として新規開発のリソースに人員を割けます。 2つ目は既存システムでのキャンペーン内容が変わったりやパフォーマンスが落ちたりしては、これまでのキャンペーンをユーザに提供できなくなってしまいます。そのため既存システムでできることはそのままである必要があります。 3つ目は施策担当者だけでキャンペーンを実施できるようにし、キャンペーン実施までのリードタイムを短くするためです。これによって、施策担当の部門が主体となってキャンペーンを実施できるようになり、キャンペーンの効果を高めたり、実施回数を増やしたりできます。 これらの要件を満たすプラットフォームを開発コストを抑えて実現するために、最初にマーケティングツールSaaSの導入を検討しました。しかしながらZOZOTOWNは会員数が1000万人を超え、多くの訴求をします。扱うデータも大規模であり、リアルタイムイベント配信ではリアルタイム性が求められます。キャンペーン経由での売上も非常に大きいため、安定的に稼働する必要もあります。 外部SaaSのみでの実現だけではなく、一部システムは自社開発しハイブリッドアーキテクチャとして開発した場合でも検討しました。しかし、ZOZOTOWNで必要な高いパフォーマンスと安定的な稼働が求められるシステムを実現できないとの結論になりました。 このような経緯から自社開発に至りました。そして作成したプラットフォームがZOZO Marketing Platform(ZMP)です。 開発フェーズと実現できること 自社開発にあたって、まず開発フェーズを設定しました。 フェーズ 実現できること 1 管理画面上からのバッチ配信キャンペーンの登録 2 管理画面上からのリアルタイムイベント配信キャンペーンの登録と配信最適化 3 他システムの統合 フェーズ1ではZMPを新規で開発し、画面上からバッチ配信キャンペーンを設定・配信できるようにします。 フェーズ2ではフェーズ1で作成したZMPに機能追加をする形でリアルタイムイベント配信と配信最適化ができるようにします。 フェーズ3では他社内ツールを統合し、統合的なプラットフォームとして利用できるようにします。 記事執筆の時点ではフェーズ1までが完了しています。 フェーズ1では、新規のシステム開発とキャンペーン処理の整理・共通化をします。ZMPをゼロから開発しバッチ配信ができるようにします。キャンペーン処理の整理・共通化では、キャンペーンごとに独自の処理を持っていたものを共通化し、キャンペーンの設定や配信を共通化できるようにします。 アーキテクチャと開発の進め方 ZMPのアーキテクチャについて説明する前に、既存システムとアーキテクチャについて説明します。 既存システム 既存のアーキテクチャは配信の種類によって異なり、大きく分けてバッチ配信とリアルタイムイベント配信があります。配信フローと配信特性が異なるため、システムも異なっています。 バッチ配信 バッチ配信は特定のユーザーセグメントに対して、決まった時間に送信するものです。以下のフローで配信されます。 順番 概要 1 時間トリガー発火 2 セグメント作成 3 コンテンツ作成 4 最適化処理 5 配信処理 設定した時間になったタイミングでトリガーが発火し、対象となるセグメントを作成します。 次にコンテンツを作成します。コンテンツは対象者全員で共通の場合もあれば、デザインのフォーマットは同じでも掲載される情報が異なる場合もあります。例えば、おすすめアイテムの通知はユーザーごとに掲載される商品が異なります。 次に最適化処理です。直近でキャンペーン配信が多ければ配信をしない通数最適化などをします。そして、最後に配信処理を実施します。 リアルタイムイベント配信 リアルタイムイベント配信は在庫状況の変動などをトリガーにする配信です。以下のフローで配信されます。 順番 概要 1 ユーザー行動や商品情報の変化を検知 2 キャンペーン判定 3 セグメント抽出 4 最適化処理 5 コンテンツ作成 6 配信処理 まずユーザーの行動や商品情報の変化を検知します。在庫が2つから1つに減少したなどの変化です。 次に変化のイベントをもとにキャンペーンの判定を実施します。例えば「在庫数が少なくなりました」とメッセージを配信するキャンペーンがあります。キャンペーンごとにトリガー条件が決まっており、これを判定するのがキャンペーン判定です。 次にセグメント抽出です。キャンペーン判定の結果、そのキャンペーンの対象となるユーザーを抽出します。在庫が少なくなった商品をお気に入り登録しているユーザーを抽出するといった処理です。 次に最適化処理です。開封されやすいチャネルを選択するチャネル最適化、開封されやすい時間に配信する時間最適化などの最適化をします。そして、最後に配信処理を実施します。 既存アーキテクチャ 既存システムで説明したように、配信フローや配信特性の違いから、既存アーキテクチャは以下のようになっています。 バッチ配信とリアルタイムイベント配信でシステムが異なっており、配信チャネルによってもシステムが分かれているような状態でした。また、配信に必要なデータを全社基盤から連携したり、連携したデータを元に配信データを作成したりするシステムも存在しています。 ZMPのアーキテクチャ 既存アーキテクチャをリプレイスするために考えたZMPのアーキテクチャは以下です。 フェーズ1だけでなくZMP全体としてのゴールを達成するためのアーキテクチャとなっています。 アーキテクチャは大きく分けて管理画面、MA基盤、配信基盤の3つのモジュールに分かれます。 管理画面モジュールは社内の施策担当者がキャンペーンの設定をするための画面です。 MA基盤モジュールは「MAマネージャー」と「MAモジュール」の大きく2つに分かれます。MAマネージャーは管理画面への管理画面のAPIを提供します。管理画面で必要なデータや設定されたキャンペーンパラメータは単一のDBに保存されます。MAモジュールは管理画面から設定されたキャンペーンを元に、配信に必要なデータの準備とキャンペーンを発火します。 配信基盤モジュールはMA基盤モジュールで生成されたデータを配信する部分です。 これら以外のモジュールとしてBigQuery上に構築された全社基盤からの変更データを連携するデータポンプが存在しています。データポンプはリアルタイムデータ連携システムで、必要なデータの変更を検知して、MAのシステムへリアルタイムで連携するシステムです。このシステムは既に開発済みでした。 データポンプの詳細については以下のテックブログをご覧ください。 techblog.zozo.com また、配信に必要なZOZOTOWNのデータはすべてBigQuery上に集約しています。ログデータもリアルタイムでBigQueryに連携しています。このBigQueryのデータを使って集計だけではなく、セグメントやコンテンツを作成します。 アーキテクチャの考慮点 このようなモジュール分割になった理由は以下です。 耐障害性 データ特性と処理特性 複数チームでの開発 拡張性 機能の統一 耐障害性 ZOZOTOWNでの配信では耐障害性が重要となります。非常に多くの配信をしているため、少しの間でも配信が止まってしまうと、売上影響が大きく機会損失となってしまいます。そこで障害が発生した場合に配信影響が最小となるようにしました。 まず、障害発生時に各モジュールで利用しているインフラやツールでの処理に影響があるかを検討しました。下記の処理について、障害発生時にどの処理で影響があるかを検討しました。 処理 モジュール 概要 配信設定 管理画面 管理画面からのキャンペーン設定 セグメント作成 MA基盤 セグメント作成処理 コンテンツ作成 MA基盤 コンテンツ作成処理 配信処理(メール) 配信基盤 メール配信処理 配信処理(プッシュ) 配信基盤 プッシュ配信処理 配信処理(LINE) 配信基盤 LINEの配信処理 管理画面で設定されたキャンペーンの情報はMA基盤のDBに保存され、トリガーによってセグメント作成とコンテンツ作成をし、配信処理を呼び出して配信します。 配信設定、セグメント作成、コンテンツ作成、配信処理が独立したシステムで存在しているので、前段のシステムで障害が発生して使用できなくなっても、後続のシステムまで処理が届いていれば処理が可能です。また、前段で処理されたデータが必要なのにシステムが使用できない場合、最終手段として手動でデータを用意して後続の処理を呼び出すなどの対応が可能となります。例えば、コンテンツ作成を手動でして、配信処理を手動で呼び出すなどの対応です。 管理データの特性と処理特性 管理画面から設定したキャンペーン情報は単一のDBに保存します。次のデータモデルで説明しますが、データには依存関係が多く存在しており、また、開発もMA部に閉じています。そのため一貫性を担保しやすくするのを優先し、マイクロサービス化はせずに単一のDBとしました。 MA基盤モジュール内ではバッチ配信とリアルタイムイベント配信の処理は別となっています。これは配信特性やフローが異なり、ボトルネックの箇所や要件が異なるためです。そのためここに関しては分割しています。 複数チームでの開発 MA部は複数の開発チームで構成しています。モジュールごとに責任を分け、各チームがモジュールを境界として開発を分担できるメリットがあります。障害試験や負荷試験もモジュールごとに実施ができます。 拡張性 将来的にMA基盤モジュール以外に独立させたいモジュールが必要でかつ、管理画面での管理が必要になったとします。その都度、管理画面を作ると部分最適化された管理画面が複数作られてしまいます。今回、管理画面を独立化させて、図のように管理画面を1つに統一できます。 機能の統一 既存のシステムでは、配信処理がアプリケーションごとに分かれており、処理が重複して存在していました。配信に利用する外部SaaSではクオータ制限もあり、配信処理を統一しないとフロー制御も難しい状況でした。また、配信基盤については、社内の他システムからも利用する可能性がありました。そのため、配信処理をモジュールとして独立させました。 概念モデル 今回、開発を進めるにあたって、最初に開発者全員で概念モデルを考えました。ここでいう概念モデルとは既存の配信フローを整理して考えたモデル構造です。概念モデルを最初に決定することで、共通認識を持って進められ、チームを分割しても開発を進められました。 既存の配信フローにおけるバッチ配信とリアルタイムイベント配信についてはそれぞれ前の章で述べました。2つの配信に共通するのは、「だれ」に「なに」を「どのように」送るかです。 このような共通点から考えたのが次の概念モデルです。 セグメント セグメントは「だれ」に相当する部分で、配信対象ユーザーグループです。既存の配信ではBigQueryのビューでセグメントを定義していましたが、将来的にGUIで作成する要件がありました。具体的にどのようなツールを使うかは現時点では未定ですが、例えばLookerの Explore を使ってGUIでの柔軟な条件指定でデータ抽出・可視化ができるようにするなどが考えられます。このように複数のセグメント定義方法に対応する必要がありました。そこでセグメントソースの概念を導入し、セグメント定義方法が増えても対応できるようにしました。 コンテンツ 次にコンテンツです。これは「なに」に相当する部分で、配信する内容です。コンテンツは配信チャネルごとに設定する項目が変わるので、コンテンツの下に各チャネルと対応するモデルがあります。 コンテンツにはデザインフォーマットは同じでも、内容となるデータをユーザーごとに変えたい場合があります。例えば対象セグメントのユーザーごとにお気に入りブランドの商品画像と価格を表示する場合です。このようなコンテンツは既存の配信においては、任意の記号(ここでは%%)で囲まれた文字列であるタグをコンテンツに記述し、配信時に実行するクエリで取得したデータを埋め込む形で対応していました。HTMLのメールコンテンツであれば、以下のようなHTMLをコンテンツに埋め込みます。 < p > GOODS_ID_1:%%GOODS_DETAIL_ID_1%% </ p > < p > GOODS_NAME_1:%%GOODS_NAME_1%% </ p > < p > PRICE_1:%%PRICE_1%% </ p > < p > GOODS_2:%%GOODS_DETAIL_ID_2%% </ p > < p > GOODS_NAME_2:%%GOODS_NAME_2%% </ p > < p > PRICE_2:%%PRICE_2%% </ p > このような共通して利用するデザインパーツをマージタグと呼ばれる概念で定義できるようにしました。動的パラメーターを含むHTMLなどのデータをその名前(マージタグ名)とともに定義します。上の例だと商品ID、商品名、価格を2つ表示するパーツになります。以下のようなyamlで定義します。nameがマージタグ名、descriptionがマージタグの説明、valueがマージタグの値です。 name : '{# goods_list #}' description : 商品リスト value : | <p>GOODS_ID_1:%%GOODS_DETAIL_ID_1%%</p> <p>GOODS_NAME_1:%%GOODS_NAME_1%%</p> <p>PRICE_1:%%PRICE_1%%</p> <p>GOODS_2:%%GOODS_DETAIL_ID_2%%</p> <p>GOODS_NAME_2:%%GOODS_NAME_2%%</p> <p>PRICE_2:%%PRICE_2%%</p> このマージタグ名をメールテンプレートなどのコンテンツに記述すると対応するvalueが動的に埋め込まれます。 マージタグでの値(例:GOODS_ID_1)をどのように取得するかはコンテンツマージタグパラメータで定義できるようにしています。 name : 商品リストのコンテンツマージタグパラメータ description : 商品リストのコンテンツマージタグパラメータ schema : name : goods_list key : email_id columns : - name : email_id type : INTEGER description : EMAIL ID example : 123456789 - name : GOODS_DETAIL_ID_1 type : INTEGER description : 商品詳細ID1 example : 1 - name : GOODS_NAME_1 type : STRING description : 商品名1 example : "かっこいいシャツ" - name : PRICE_1 type : STRING description : 価格 example : "10000" - name : GOODS_DETAIL_ID_2 type : INTEGER description : 商品詳細ID2 example : 2 - name : GOODS_NAME_2 type : STRING description : 商品名2 example : "かっこいいシャツ" - name : PRICE_2 type : STRING description : 価格 example : "10000" query : | SELECT a.email_id AS email_id, b.GOODS_DETAIL_ID_1 AS GOODS_DETAIL_ID_1, b.GOODS_NAME_1 AS GOODS_NAME_1, b.PRICE_1 AS PRICE_1, b.GOODS_DETAIL_ID_2 AS GOODS_DETAIL_ID_2, b.GOODS_NAME_2 AS GOODS_NAME_2, b.PRICE_2 AS PRICE_2, FROM GoodsListTagle また、マージタグやマージタグパラメータで定義された値の中にはキャンペーンによって変える値があります。例えば、タイムセールの終了日時や表示する商品の条件(表示する商品の件数や最低割引率)などです。先のHTMLの例だと、場合によっては件数を10件や20件にしたい場合があります。このような値を画面上で設定できるようにZMPプリセットの概念も導入しました。以下のような定義をあらかじめしておきます。 key : mail_min_goods_num_5_through_10 name : 最小商品数(メール用) description : | 対象商品の最小商品数を指定する項目です。 type : INTEGER input : from : offer type : select default : 5 options : - label : 5 value : 5 - label : 6 value : 6 - label : 8 value : 8 - label : 10 value : 10 マージタグには関連するZMPプリセットのキー名を定義します。そうすると、先のZMPプリセットの例の場合は管理画面で最低件数をセレクトボックスで選択できるようになります。 これらのコンテンツで使用するパラメータ(マージタグ、マージタグパラメータ、ZMPプリセット)はyamlファイルで定義します。このyamlファイルをCI/CDによってDBに反映できます。この仕組みによって、DBを直接操作せずにエンジニア以外でもコンテンツで使用するパラメータの定義ができるようになっています。 オファー 次にオファーです。これは、セグメントとコンテンツを組み合わせてそれを「どのように」配信するかを設定する部分です。定義したセグメントとコンテンツを指定し、いつ配信するのかのトリガーを指定します。トリガーはバッチ配信とリアルタイムイベント配信で設定項目が異なります。前の章でも説明したように、バッチ配信の場合は「時間」を基準にリアルタイムイベント配信の場合は「ユーザ行動や商品データの変更」がトリガーとなります。 施策によっては、複数チャネルへの配信や同一チャネルでのABテストを実施します。また、将来的には配信以外にもポイント付与やWeb接客といった施策もZMPで設定できるようにしたいです。そこで、オファーに直接コンテンツを紐づけるのではなく、アクションを間に導入しています。これにより、配信だけではない様々な施策をZMPでしたいとなったときに、その施策に対応するモデルを開発・導入してアクションに紐づけられます。 キャンペーン 最上位のキャンペーンはオファーをまとめるものです。キャンペーンによってはコンテンツを変えたり、対象者を変えたり内容を変えて配信します。そのように同一のキャンペーンであっても内容が異なる配信をまとめるためのものです。 次の章では各モジュールについて説明します。 各モジュールについて ZMPを構成する各モジュールについての詳細と責務について説明します。ここではフェーズ1までに完成している部分について主に説明します。 管理画面 管理画面モジュールは管理画面を提供するモジュールです。 管理画面を独立したモジュールとして開発した理由は2つあります。 1つ目の理由は、フロントエンドとバックエンドで開発の責務を分けてチーム分割をできるようにするためです。 2つ目の理由は、管理画面を伴う機能拡張の際に単一の管理画面アプリケーションで管理できるようにするためです。例えばフェーズ3で他の社内ツールをZMPに統合する際、先に管理画面だけをZMPに統合する手段が取れます。また、将来的にキャンペーン設定だけではなく、配信基盤のパラメータ設定もしたい場合でも、拡張性で説明したように管理画面アプリケーションを増やさずに対応できます。 管理画面を使ったZMPの運用フロー 管理画面ではフェーズ1終了時点で以下のようなフローで配信を実施します。 バッチ配信キャンペーンのお気に入りブランドの新着アイテムを配信する場合を考えます。 最初に最上位概念のキャンペーンを作成します。キャンペーン名と担当者を設定します。 次にセグメントを作成します。セグメントの設定項目は名前とセグメントソースです。セグメントソースとして今回はBigQueryのビューを使用します。BigQueryでビュー作成後にそれをセグメントに設定します。 次にコンテンツを作成します。今回はメールで配信する場合を想定します。また、内容として対象者ごとにお気に入りしているブランドの新着アイテムを10件表示します。メールコンテンツの設定項目は名前、タイトル、デザイン(HTML版・テキスト版)です。デザインはスマートフォン・PC向けのHTML版とフィーチャーフォン向けのテキスト版が必要です。このHTML版のデザイン作成に外部SaaSを利用します。対象者ごとに新着アイテムを出し分けるためにマージタグを利用します。 最後にオファー作成です。設定項目としては名前、セグメント、コンテンツ、トリガー条件です。セグメントとコンテンツはここまでで作成したものを設定します。トリガー条件はいつ配信するかです。今回はバッチ配信のため時間を設定します。 このようなフローで設定が可能です。作成したセグメントやコンテンツは使い回せるため、これまでの運用と比べて効率的に配信設定ができるようになります。 フェーズ2ではこの管理画面でリアルタイムトリガーの設定ができるようにします。 管理画面ではセグメント・コンテンツ・キャンペーンを設定しますが、管理画面の要件の1つに、GUIでのセグメント作成とメールコンテンツ作成がありました。 セグメント作成では、既存の運用においてはSQLを書く必要があります。しかし、担当者によってはSQLを書くための学習コストが発生したり、テーブル定義を理解したりする必要があります。これらを解消するために画面上から設定できる必要がありました。この理由からセグメント作成のツールの導入が必要でした。 メールコンテンツの作成では既存のフローでは施策内容を元にデザイナーがHTMLを書いて、それを施策担当者が確認し、適宜修正をして確定するフローでした。しかし、工数が多く発生していたので、これを解消するために施策担当者が自らデザインできるようにする必要がありました。この理由からコンテンツ作成ツールの導入が必要でした。 これらの機能は一般化されている部分であり、自分たちで開発するよりも既存のものを利用したほうが良いと判断し、外部SaaSを導入しました。一般化されている部分のため、導入したツールが利用できなくなっても他ツールで代用可能です。また自社開発よりも開発工数が削減出来ます。 今回導入したメールコンテンツ作成のSaaSではメールHTMLを部分パーツのブロックで定義・編集できる機能があります。例えば、フッターバナーのHTMLを1つのブロックで定義しておき、様々なテンプレートでの再利用を可能にします。この機能を応用し、運用効率の向上に繋がりました。施策担当者が掲載内容の一部をブロック単位で変更できるようになり、また、ブロックの組み合わせで新しいコンテンツを作成できるようになりました。SaaS導入にあたって、施策担当者とデザイナー共同で既存のHTMLメールデザインを全てSaaS上でパーツ化しました。 フェーズ1終了の現時点では、メールコンテンツ作成のみ外部SaaSの導入が完了しています。 MA基盤 MA基盤モジュールは管理画面のAPIの提供と配信に必要なデータの準備とキャンペーンを発火します。 MA基盤モジュールはその中にMAマネージャー、MAモジュールを持ちます。MAマネージャーは管理画面が使用するAPIを提供します。MAモジュールは管理画面から設定されたキャンペーン情報をもとにデータを準備し、配信処理をトリガーします。 MAモジュール MAモジュールの中には全社データ基盤からのデータ連携、キャンペーンごとに必要なセグメント・コンテンツ作成処理、チャネルごとのコンテンツ生成処理が含まれます。 バッチとリアルタイムイベントキャンペーンで処理を分けているのはキャンペーン特性や処理の違いがあって共通化が難しく、ボトルネックの箇所や要件が異なるためです。 配信の直前までの処理として、バッチ配信では時間のトリガーが発火した後にセグメント作成、コンテンツ作成となります。一方でリアルタイムイベント配信の場合はデータを連携し、条件が満たされた場合にセグメントとコンテンツを作成します。どちらも必要であれば最後に最適化を実施します。このように処理の流れが異なります。 また、配信のボトルネックの箇所や要件も異なります。バッチ配信は大量のユーザーに向けて1度に配信します。大量のユーザーに対してコンテンツを作成するのでボトルネックになりやすいです。リアルタイムイベント配信の場合は、データの変更を検知してすぐに配信する必要があるため、リアルタイム性が求められます。 このようなキャンペーン特性の違いからモジュールを分割しています。フェーズ1終了時点では、バッチ配信のモジュールであるMAバッチのみ作成が完了しています。 MAマネージャー MAマネージャーは管理画面のAPIを提供するアプリケーションとDBを持ちます。MAマネージャーは管理画面とMAモジュールの間に入りハブの役割を持ちます。 単一のDBにしているのは前で説明した通りデータの一貫性を保つためです。 MAマネージャーはMAモジュールへデータを一方向に流れるように設計しています。MAモジュールからは参照しないようにし、MAマネージャーで障害が起きても、配信処理に影響が出ないようにするためです。 配信基盤 配信基盤はMA基盤モジュールで生成されたデータを配信する部分です。 ここを独立させている理由は2つあります。 1つ目は他モジュールで障害が発生してもデータさえあれば配信できるようにするためです。管理画面やMAマネージャーが使えなくなった場合でも、すでに設定されているキャンペーンについては独自でトリガーを実行し配信処理はできるようにしています。 2つ目は将来的に全社向けの配信基盤とできるようにするためです。今はZMPだけでの利用となっていますが、将来的に他のシステムや部門からも利用できるようにするためです。 開発の結果 現時点ではフェーズ1のバッチ配信キャンペーンのみが設定・配信できる状態ですが、キャンペーン実施を施策担当者のみでできるようになり、これまで発生していた開発やデザイナーでの工数を削減できました。また、既存のDigdagなどにおける処理は、キャンペーンごとに定義された大きなワークフローとなっており、処理が重複し複雑になっていました。それをZMPへの構成に載せ替え、処理を共通化しシンプルな形でリプレイスも出来ました。 運用保守については、運用保守のコストが大きいRTMの移行ができていないため、大きなコスト改善はできていないものの、バッチ配信の部分についてはコスト削減ができました。 機能ごとにモジュール分けができたため、障害発生時でも原因追及がしやすくなり、チームごとに柔軟に開発ができるようになりました。 集計情報を施策担当者と整理したうえで1つのプラットフォームに載せ替えることで、集計情報をまとめられ、分析しやすい環境になりました。 開発を振り返って フェーズ1終了後に開発メンバーでKPTを実施しました。開発における進め方で良かった点や改善点を振り返りました。 良かった点としては、品質の担保ができたことと開発しやすさが挙げられました。 品質の担保については、障害試験・負荷試験の実施や、仕様やリリースフローについてCTO室レビューの実施で担保できたことが理由としてあります。 開発しやすかった理由としては、概念モデルを最初に決定し、大きな後戻りがなかったためです。現状の仕様や要件を調査し、実装フェーズへ入る前に開発メンバーと施策担当者で概念モデルについて念入りに認識合わせをし、決定出来たためです。 また、開発モジュールの分割でチームごとに実装を進められ、仕様決定までの経緯や開発フローについてドキュメントを残したことで途中参画メンバーでもスムーズに開発に入れたと意見もありました。 今後の展望 現在はフェーズ1の開発までが終了しており、2024年1月から運用開始しています。 現在はフェーズ2以降の開発に着手するための準備と、フェーズ1でやり残した細かいタスクやZMPユーザーからのフィードバックを受けて改善に取り組んでいます。フェーズ2までが完了すれば、リアルタイム配信システムを退役でき、運用保守コストの大きな改善が期待できます。フェーズ3までが完了すれば、他システムとの統合が完了し、全てのキャンペーンがZMPで管理できるようになります。 まとめ マーケティングプラットフォームの開発にあたって考慮した点とアーキテクチャについて紹介しました。このプラットフォームの作成によって、施策担当者やマーケティング担当者でキャンペーンの運用ができるようになりました。また、開発側においてもキャンペーン実施時に発生する開発コストやシステムの運用保守コストを削減できました。本記事が皆様の参考になりましたら幸いです。 最後に ZOZOでは一緒にプロダクトを開発してくれるエンジニアを募集しています。ご興味のある方は下記リンクからぜひご応募ください! corp.zozo.com
はじめに こんにちは、CISO部の 兵藤 です。日々ZOZOの安全のためにSOC対応を行なっています。 本記事ではサイバー脅威インテリジェンスプラットフォーム「OpenCTI」からMicrosoft Sentinelへの脅威インテリジェンスの取り込みについて紹介します。また、この内容については以下の「Azureで織りなすOpenCTI構築」に続く内容となっています。 techblog.zozo.com 目次 はじめに 目次 背景と概要 構築 連携に使用するプレイブック プレイブックを使用するための準備 Sentinel側のデータコネクタ SSLサーバー証明書 アクセス制御 インジケータの登録 Send to Security Graph API Read Stream- OpenCTI Indicators カスタムコネクタ OpenCTI-IndicatorsStream 運用 分析ルールの作成 取り込むインテリジェンスの選定 検知精度 まとめ おわりに 背景と概要 以前投稿したように、SOC対応を実施するにあたって各個人で調査収集していた情報を1つのプラットフォームで自動収集、管理し、ゆくゆくはSIEMと連携したいと考えていました。そのプラットフォームとしてOpenCTI( ver 5.11 )を導入しています。 また、ZOZOのSOCでは普段の検知対応においてSIEMを活用しています。具体的にはMicrosoftのSentinelです。この2つを連携し、自動で脅威インテリジェンスを用いた検知の仕組みを構築しました。 Sentinelにも「Microsoft Defender 脅威インテリジェンス」といったDefenderの脅威インテリジェンスを取り込むSentinelのデータコネクタは存在します。ですが、ZOZO独自で活用しているインテリジェンスやX(旧Twitter)にてリアルタイム公開される新鮮なフィッシング情報等インテリジェンスを用いる場合などでOpenCTIはとても便利です。 上記のようにアレンジした脅威インテリジェンスを用いた検知を自動で行うようにOpenCTIのインテリジェンスをSentinelに食わせました。 構築 連携に使用するプレイブック 連携のために、Microsoftが用意してくれているソリューションを利用します。執筆時点(2024年2月22日)ではSentinelのコンテンツハブから取得でき、プレイブックが4つあります。 また、OpenCTI公式のドキュメント 1 からもこのコンテンツがリンクされています。 4つのプレイブックは以下の通りです。 OpenCTIのインテリジェンスをSentinelに食わせるには Read Stream- OpenCTI Indicators と Send to Security Graph API - Batch Import (OpenCTI) の2つを使用します。 プレイブックを使用するための準備 Sentinel側のデータコネクタ このプレイブックは Microsoft Graph API を利用してインテリジェンスをテナントに作成します。このインテリジェンスをSentinel(Log Analytics)上の脅威インテリジェンスとして確認し検知ルールに組み込むためには、Sentinel上でデータコネクタを作成する必要があります。 このデータコネクタはコンテンツハブの「Threat Intelligence」から取得できます。 必要なデータコネクタはSentinel上で「脅威インテリジェンス プラットフォーム - 非推奨になっています (プレビュー)」といった表示になっており、Microsoftからは非推奨とされています。 ですが、公式ドキュメント 2 には以前の名称ではありますが、以下のようにこのデータコネクタを使用するように記載されています。 このデータコネクタを起動しておかないと、Microsoft Graph APIでのリクエストはエラーなく登録されますが、Sentinelでその情報を活用できない状態になります。 この状態に自分はとても悩まされました。このデータコネクタの後継である「Threat Intelligence Upload Indicators API (Preview)」を起動しただけではダメな状況でした。2つのデータコネクタを起動して取り込みに成功しています。 SSLサーバー証明書 プレイブックからOpenCTIへのリクエストはHTTPSなので、SSLサーバー証明書が必要です。以前のOpenCTIの構成ではAzure Load Balancer(以降ALB)を使用してAzure Key Vaultから配布される証明書を使用していました。 以前の構成ではSSLサーバー証明書の管理が面倒であり、かつSOC関係者やシステムしかアクセスしないOpenCTIに対してそこまで大規模なロードバランサーは必要ないと考えました。そのため、ALBを使用せずにMicrosoft側で管理されているSSLサーバー証明書を使用できるサービスにリプレースしました。 SSLサーバー証明書管理を省力化でき、小規模運用でき、アクセス制御が効くものとしてAzure Web App for Containers(以降Web App)でnginxを立てることを選択しました。Web AppはデフォルトでHTTPS通信のエンドポイントを提供してくれます。また、マシンスペックもごく小規模で運用でき、コンテナでのデプロイも容易です。仮想サブネットワーク統合でのリプレースもスムーズに進みました。 アクセス制御 アクセス元の制御については「受信トラフィックの構成」の項目から「アクセス制限付きで有効」を構成することによって実現できます。 Netwotk Security Group(以降NSG)と同様にサービスタグやIPアドレスを指定できます。これによってMicrosoft Entra ID(旧Azure Active Directory)からのSAML認証も許可できます。nginxの設定でもIP制限はできますが、Microsoft Entra IDからのアクセスをサービスタグで一括許可したい場合はこちらを構成しておくと便利です。 インジケータの登録 OpenCTIからのインテリジェンスを取り込むためには、OpenCTI側で取り込む対象となるSTIX形式のインジケータを登録する必要があります。以下のようにインジケータがあることを確認しておきます。 Send to Security Graph API 実際にプレイブックを利用してOpenCTIからインテリジェンスを取り込むLogic Appを組みます。 Send to Security Graph API - Batch Import (OpenCTI) プレイブックを最初に実行します。このプレイブックを利用して作成される OpenCTI-ImportToSentinel のLogic Appが以降の使用する Read Stream- OpenCTI Indicators で必要になるので、先にこちらを作成しておかなければなりません。 以下のようなLogic Appができるはずです。 また、このLogic Appにインジケータを作成する 3 ためのロール( ThreatIndicators.ReadWrite.OwnedBy )を付与する必要があります。 詳しい付与方法は公式の GitHub を参照してもらえればわかりますが、以下のようにテナント接続後にPowershellを用いてロールを付与します。 $MIGuid = "<Logic App の Managed ID>" $MI = Get-AzureADServicePrincipal -ObjectId $MIGuid $GraphApIAppId = "00000003-0000-0000-c000-000000000000" $PermissionName = "ThreatIndicators.ReadWrite.OwnedBy" $GrphAPIServicePrincipal = Get-AzureADServicePrincipal -Filter "appId eq ' $GraphApIAppId '" $AppRole = $GrphAPIServicePrincipal .AppRoles | Where-Object { $_ .Value -eq $PermissionName -and $_ .AllowedMemberTypes -contains "Application" } New-AzureAdServiceAppRoleAssignment -ObjectId $MI .ObjectId -PrincipalId $MI .ObjectId -ResourceId $GrphAPIServicePrincipal .ObjectId -Id $AppRole .Id Microsoft Entra IDの OpenCTI-ImportToSentinel 表記のエンタープライズアプリケーションを確認します。アクセス許可の項目で以下のようにロールが付与されていることを確認できれば成功です。 Read Stream- OpenCTI Indicators 次に Read Stream- OpenCTI Indicators プレイブックを利用してOpenCTIからインテリジェンスを取得するための2つ目のLogic Appを組みます。 カスタムコネクタ このプレイブックを利用するためにはカスタムコネクタを作成する必要があります。このカスタムコネクタはOpenCTIへGraphQLを利用してインテリジェンスを取得するためのものです。 以下のようにプレイブックの説明欄にリンクがあるので、そこからデプロイが可能です。 デプロイ後はカスタムコネクタのリソースとは別でAPI接続のリソースができます。このAPI接続にOpenCTIで利用するAPIキーを設定します。 ここで注意すべきは、APIキーの記載方法です。AzureのAPI接続にAPIキーを登録する場合は以下の注意書きがあり、 Bearer から記載する必要があります。 OpenCTI-IndicatorsStream プレイブックを実行すると、以下のようなLogic App(OpenCTI-IndicatorsStream)ができるはずです。 これでOpenCTIからインテリジェンスを取得するための大枠ができました。ですが、これだけでは動きません。 Until hasNextPage is false 内部の Parse JSON Indicators のアクションなど、最新のOpenCTIのGraphQLのレスポンスに対応していないため、スキーマを修正する必要があります。 具体的にはスキーマ内部の creators が creator になっていたり、この creators の形式を array に変更したり、 indicator_types の type 項目を編集したりと、レスポンスによって様々です。 レスポンスの例についてはOpenCTIの /graphql 階層で GraphQL Playground があるので、Logic Appのクエリ結果を以下のように確認できます。OpenCTIの ver 5.12 以上ではクエリの filters 等が異なりますので 公式ドキュメント を参照ください。 このGraphQL Playgroundを活用して、インジケータがOpenCTIから返されるレスポンスをサンプルとして取得すれば、Logic Appのスキーマの修正が容易です。以下の「サンプルのペイロードを使用してスキーマを作成する」から自動的にスキーマを生成してくれます。 上記のようにエラーが出るアクションごとにスキーマを修正し切れば、OpenCTIからのインテリジェンスを取得するためのLogic Appが完成します。このAppが最後に Send to Security Graph API へ送信することで、OpenCTIからのインテリジェンスをSentinelへ食わせることができます。 運用 分析ルールの作成 OpenCTIからのインテリジェンスを取り込むことができたら、次はそれを活用するための分析ルール(検知するためのルール)を作成します。このルールについては先述したコンテンツハブの「Threat Intelligence」から取得できます。 この分析ルールには大体の欲しいKQLの書き方が揃っているので独自で作成することは少ないですが、Microsoft製品以外のログ分析は当然自力で書くことになると思います。 例えばSWG製品を導入している場合は、そのログと取り込んだ脅威インテリジェンスを組み合わせて検知ルールを作成することになります。以下にIPv4アドレスのIOC情報と突合させるためのKQLの例を示します。 let dt_lookBack = 2h; let ioc_lookBack = 30d; let IP_Indicators = ThreatIntelligenceIndicator | where TimeGenerated >= ago(ioc_lookBack) | summarize LatestIndicatorTime = arg_max(TimeGenerated, *) by IndicatorId | where Active == true and ExpirationDateTime > now() | where isnotempty(NetworkIP) or isnotempty(EmailSourceIpAddress) or isnotempty(NetworkDestinationIP) or isnotempty(NetworkSourceIP) | extend TI_ipEntity = iff(isnotempty(NetworkIP), NetworkIP, NetworkDestinationIP) | extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(NetworkSourceIP), NetworkSourceIP, TI_ipEntity) | extend TI_ipEntity = iff(isempty(TI_ipEntity) and isnotempty(EmailSourceIpAddress), EmailSourceIpAddress, TI_ipEntity) | where ipv4_is_private(TI_ipEntity) == false and TI_ipEntity !startswith "fe80" and TI_ipEntity !startswith "::" and TI_ipEntity !startswith "127."; IP_Indicators | join kind=innerunique ( SWG_log() | where TimeGenerated >= ago(dt_lookBack) | extend SWG_TimeGenerated = TimeGenerated | where isnotempty(DstIpAddr) | where LogSummary !startswith "Block" ) on $left.TI_ipEntity == $right.DstIpAddr | where SWG_TimeGenerated < ExpirationDateTime | summarize SWG_TimeGenerated = arg_max(SWG_TimeGenerated, *) by IndicatorId, DstIpAddr | project SWG_TimeGenerated, Description, ActivityGroupNames, IndicatorId, ThreatType, DstIpAddr, User, Hostname, ExpirationDateTime, ConfidenceScore, Type | extend timestamp = SWG_TimeGenerated 上記KQLは例のためにSWGでのログをSentinelに以下のようなテーブル形式で取り込んでいると仮定しています。1例なので、実際の環境に合わせて修正する必要があります。 TimeGenerated LogSummary DstIpAddr DstPort User Hostname SrcIpAddr SrcPort 2024-02-22T09:10:30.8885957Z Block Risk Connection 192.0.2.1 443 user01 HOST01 192.0.2.2 4444 上記はIPでの分析例なのでURLやファイルハッシュなどのIOC情報を突合させる場合はそれぞれ別々の分析ルールを作成することになります。 これらの分析ルールによって自動で取得した脅威インテリジェンスを活用して検知できます。 取り込むインテリジェンスの選定 自分たちがOpenCTIで収集している脅威インテリジェンスは膨大にあります。作成されるインジケータをなんでも取り込んでしまうと過検知の量も膨大になります。人材も豊富で大規模なSOCであればそれでも対応できるかもしれませんが、自分たちのSOCではそれは難しいです。また、IPアドレスでのIOC情報はCDNのIPアドレスなども多く含まれているため、鮮度が落ちやすい情報も多いです。 ある程度の検知精度を出すためにインテリジェンスの信頼度、鮮度、作成元などの情報でSentinelに取り込むインテリジェンスを選定します。 選定する場合はOpenCTIに取り込む元々のインテリジェンスを制限するのではなくLogic Appでフィルタリングを行います。あくまでOpenCTIは情報の泉として活用したいからです。 Logic Appでのフィルタリングは以下のように Until hasNextPage is false 内部の Switch 2 アクションの各ケースに追加します。追加内容は制御アクションです。 制御アクションとして x_opencti_score の値によってインテリジェンスを取り込むかどうかを判断する「条件」(if文に相当)を設定します。以下の例だと信頼度が50未満の場合はIPv6のインテリジェンスを取り込まないように設定しています。 検知精度 セキュリティの観点から詳しいことをこの場で記載できませんが、検知精度としてはいい具合にZOZOの環境にチューニングできていると感じています。OpenCTIからのインテリジェンスを活用して検知することで、これまで検知できていなかった脅威を見つけることができています。 まとめ OpenCTIからのインテリジェンスをSentinelに食わせ、検知する仕組みや運用法を紹介しました。OpenCTIを用いた自動でのインテリジェンス検知運用の記事は事例が少ないので、参考になれば幸いです。 ZOZOではこれからも脅威インテリジェンスを逐次収集し、意思決定プロセスに必要なインテリジェンスの活用に努めていき、ZOZOの安全性の向上を図っていきたいと考えています。 おわりに ZOZOでは、一緒に安全なサービスを作り上げてくれる仲間を募集中です。ご興味のある方は、以下のリンクから是非ご応募ください! corp.zozo.com OpenCTI-Ecosystem ↩ 脅威インテリジェンスプラットフォームをMicrosoft-Sentinelに接続する ↩ 脅威インテリジェンスインジケータを作成する ↩