TECH PLAY

株式会社メルカリ

株式会社メルカリ の技術ブログ

267

こんにちは。Mercari DBRE(Database Reliability Engineer) チームのエンジニアの @takashi-kun です. 今回私達のチームでは複数の Cloud SQL for PostgreSQL インスタンス(以下 Cloud SQL インスタンス)の一部を一つのインスタンスに統合するという作業を行いました. 本記事ではその作業の詳細やダウンタイムを短くするために選択した方法などを紹介します. はじめに 今回の対象サービスのメルカリ Shops では microservice アーキテクチャを採用(ref: メルカリShopsはマイクロサービスとどう向き合っているか )しており, それぞれの microservice 毎にデータベースが存在します. スペックやデータサイズは大小様々ですが, おおよそ 50 程度のインスタンスが稼働しています. これらのインスタンスは運用効率化のためすべて Enterprise Plus Edition で稼働しています. 先のブログでも紹介されていますが, データベースが microservice 毎に別となっているため, 障害の局所化や他 microservice のデータベースを意識せずに開発できるなど, 開発からリリースまでを迅速に行えるという多くのメリットを享受しました. 一方でサービスの利用が拡大していき, それらをそのまま運用していくうえでいくつかの課題がありました. https://pixabay.com/photos/elephant-herd-animals-trunk-safari-8359382/ メルカリ Shops DB の課題 この章では現在メルカリ Shops において直面していた DB 運用の課題についていくつか紹介します. コスト まず1点目はコスト(サーバ費用)です. microservice 毎に DB が存在しているため, 利用が少なく最低スペックにも関わらずインスタンスを起動させ続けなければならない, という問題があります. インスタンス1台の費用は小さいですが, microservice の数が多いため “チリツモ” で費用が膨れていくにも関わらず, インスタンス Tier は最低で運用しているため CUD 以外のコスト削減の手段を取れていませんでした. 管理 microservice 毎にインスタンスがあるため, near-zero downtime といえどメンテナンスが大量に一度に通知されてしまいます. またIaC で管理されているとはいえ, 数が多いため現在のチーム体制ですべてを網羅的に管理運用することが難しくなっていました. 性質上, それぞれの microservice を廃止/統合するといったことは難しく, かといってインスタンスを減らすということはできず限られたメンバーで多くのインスタンスの対応が必要となっていました. 余剰ストレージ Cloud SQL にはストレージの自動拡張機能と PITR がありますが, メルカリ Shops ではかつてこの機能を全てのインスタンスで有効化していました. 特に更新(INSERT/UPDATE/DELETE)が多く行われる系統のインスタンスでは, PITR のために保存している WAL のサイズが多く, それが要因でインスタンスのストレージサイズを拡張し続ける, という問題が発覚しました. 止血対応として PITR を無効化したものの, Cloud SQL では 一度拡張したストレージサイズは縮小できない ため, 添付のよう余剰なストレージサイズに対して課金し続けていました. 4.5TB のディスクに対し実データは 250GB 程度 対策の手法 メルカリ Shops では上記のような運用/インスタンス費用の課題に対する対策として, Cloud SQL インスタンスの統合を行うことを決定しました. 単純にインスタンスを統合するにもいくつか方法があり, それぞれについて簡単に解説し, 今回メルカリ Shops がどのような手法で統合を行ったかについて説明していきます. 前提 サービス要件 まず, 統合作業時のサービス側の要件について紹介します. メルカリ Shops は利用者数が店舗・購入者ともに拡大しており, かつメルカリからの導線もあるため, 長時間(1時間以上)の停止メンテナンスを行うことはできませんでした. また作業時の停止についてもメルカリ Shops の DB は基本的に read heavy な構成のため, 書き込み(INSERT/UPDATE/DELETE)は停止しても読み込み(SELECT)については停止しないように進める必要がありました. 加えて統合対象の中には決済やレポート(店舗側に売上内容を表示する)機能といったサービス的にクリティカルな機能も含まれていたため, ダウンタイムをできるだけ短くしたい要望がありました. 一方, 今回の統合作業において重要な観点として 切り戻しの準備は不要 ということで合意をしました. つまり仮に切り替え後に何らかの問題があって切り戻した場合には, 切り戻し完了までに書き込まれたデータは欠損しても問題ない, ということです. これによって構成が少しシンプルになります. システム メルカリ Shops ではインスタンスはすべて各 microservice 毎に論理データベース, クレデンシャル(user/password), 権限(GRANT)が分離されています. また, 接続方法については各インスタンスの持っている Private IP に接続する方式となっており, Cloud SQL Auth Proxy や Cloud SQL Go Connector などを利用していませんでした. そのため, インスタンス統合後の切り替えのために Instance Connection Name( ${project}:${region}:${instance} )などを変更する必要がなく, アプリケーション側での作業が不要で切り替えを実施できます. 統合方法案 上記の前提を踏まえて, インスタンス統合で検討した方法と実際に行った方法を紹介します. https://wiki.postgresql.org/wiki/Logo DMS まず, Google Cloud 上で DB の移行が可能な managed service で Data Migration Service(DMS) があります. 詳細については省略しますが, これは Cloud SQL(source) を primary とする external replica の作成と切り替えをフォローする managed service です. DMS は現時点では統合はサポートしていないこと(destination のインスタンスに DB があると実行できない), 切り替えは DB の切り替えのみサポートしていてアプリケーションの変更は別途必要ということで今回は要件に見合いませんでした. export/import 当初やろうとしていた方法はこれで, source インスタンスで書き込みだけを停止し該当 DB を export , そしてそれを destination へと import し, 完了したらアプリケーションを destination へと変更する方法です. この方法は最も手順がシンプルかつ事前準備もほとんどいらないため, 最も楽な方法ではある一方, 書き込みの停止時間が長くなります. 特にメルカリ Shops では source インスタンスを参照している分析用バッチなどが多く動いていて, それらを import 後に切り替えるなどをするとどうしても 1 時間以上書き込み停止発生してしまい, その停止時間がサービスにクリティカルな機能の要件に見合わないということで, この方法も断念しました. Logical Replication(manual) 最後に検討した方法が PostgreSQL の Logical Replication を利用する方法です. Cloud SQL では Logical Replication をサポートしている ため, これらを利用してデータの同期と切り替えを行います. Logical Replication は, primary を source としないといけない, 同期レプリケーションはサポートしてない(設定できない), ConnectorEnforcement が有効化されている場合は機能しないなどの Cloud SQL としての制限 や, DDL は伝播しない, large object は伝播しないなど Logical Replication としての制限 がいくつかありますが, 今回のケースではいずれも大きな問題とはなりませんでした. 加えて export/import で問題になった分析用バッチなどもこの方法だと同期が完了したタイミングで参照先をアプリケーションより前に変更しておくなど, 切り替え方法を工夫することで停止時間を短くできるため, 今回はこの方法をとることにしました. 統合手順 Logical Replication を構成し統合するにはこのような流れで進めていきます: インスタンスで cloudsql.logical_decoding を有効化 schema dump & restore Logical Replication を設定 同期完了を待つ アプリケーションの接続を DNS ベースにする source 側で書き込み block(downtime 開始) ブロックする前までのデータ更新が追いついたことを確認 DNS 変更 source 側で既存の接続を kill 大きく分けて Logical Replication 準備(1-4), 切り替え前作業(5-7), 切り替え(8-9)の3つに分けて説明していきます. Logical Replication 準備 ここではインスタンスで Logical Replication を構成するための設定をします. 以下, 統合先インスタンスを destination, 統合元インスタンスを source とします. まず最初に source に対して cloudsql.logical_decoding を有効化する必要がありますが, これには再起動が必要となります. データ量や TPS などによって異なるかと思いますが, 大体 30s ~ 60s 程度で起動が完了しました. 続いて destination に source と同じ PostgreSQL user, database を作成します. user , database を作成したら destination に source の schema をリストアします. 実データは Logical Replication の COPY によって同期されていくため単に空のテーブルとアクセス権のみ設定するだけでよいです. インスタンスに接続するのは前述の Cloud SQL Auth Proxy 経由で接続しています(この後の Cloud SQL Auth Proxy の起動は省略). また, この後何度もインスタンスに接続をするので, このように /etc/hosts や ~/.pgpass を設定しておいて機械的にアクセスできるようにしておくと良いでしょう. ## /etc/hosts 127.0.0.1 source-001 127.0.0.2 destination-001 ## ~/.pgpass ### source-001, destination-001 に src user として src database に接続 source-001:5432:src:src:${PASSWORD} destination-001:5432:src:src:${PASSWORD} $ cloud-sql-proxy \ ${PROJECT}:${REGION}:source-001 \ --address $(grep source-001 /etc/hosts | cut -d' ' -f1) $ cloud-sql-proxy \ ${PROJECT}:${REGION}:destination-001 \ --address $(grep destination-001 /etc/hosts | cut -d' ' -f1) なお Logical Replication は DDL を伝播しないという制約があり, dump/restore やLogical Replication 中に誤って DDL を実行してしまわないようにスクリプト側でもブロックするなどしてそれに対応しました. ## dump $ pg_dump \ -U ${USER} \ -h source-001 \ --schema-only > src.$(date '+%Y%m%d-%H%M%S').sql ## restore $ psql -h destination-001 \ --user src \ --dbname src < src.xxxxxx.sql Logical Replication を構成するためには publication を source 側に, subscription を destination 側に作成する必要があります. それぞれ以下のような形で作成可能です: ## publication の作成 CREATE PUBLICATION pub FOR ALL TABLES; SELECT * FROM pg_publication_tables; subscription でつなぐユーザーは, 区別できるようにアプリケーションが利用しているものとは別のものを用意するとよいでしょう. ## subscription の作成 CREATE SUBSCRIPTION sub_src CONNECTION 'host=xxxxxxx port=5432 dbname=src user=replication password=xxxxxx' PUBLICATION pub; SELECT * FROM pg_stat_subscription; これで Logical Replication が開始し, COPY コマンドが source 側で実行され, データの初期同期(既存のデータのコピー)が行われます. COPY が終わったらデータの差分同期が行われるようになります. データサイズの大きいテーブルだとかなり時間がかかるため, COPY 作業が終わったかどうかは source 側で pg_stat_activity を見るか, destination 側で COPY 対象テーブルにロックが掛かってるかを見るとわかります: ## source SELECT * FROM pg_stat_activity WHERE query LIKE '%COPY%'; ## destination SELECT * FROM xxx LIMIT 1; COPY が終わり差分同期が始まったら source と destination の差はこのようにして求められます. 両方 0 だった場合は追いついています. ## source SELECT pg_wal_lsn_diff(sent_lsn,write_lsn) write_diff, pg_wal_lsn_diff(sent_lsn,flush_lsn) flush_diff, pg_wal_lsn_diff(sent_lsn,replay_lsn) replay_diff FROM pg_stat_replication; ## destination SELECT pg_wal_lsn_diff(received_lsn,latest_end_lsn) FROM pg_stat_subscription; COPY が終わりそれぞれの diff が 0 となっていたら差分同期も追いついているため, 切り替え前作業に進みます. 切り替え前作業 ここでは書き込みを停止して実際に切り替えを実行する前までの作業を説明します. 前述しましたがメルカリ Shops の DB 構成は DB への接続は直接 IP ベースで接続している write/read ユーザーを分けていない, cloudsqlsuperuser Role を持つ ということがあります. 書き込み停止を確実に行い, かつ短時間で済むようにするにはいくつかの工夫をする必要があります. 接続方法の変更 IP ベースでの接続となると, アプリケーションを統合先インスタンスへと向き先を変更する必要があり, そのためにはデプロイが必要となります. 書き込み停止時間中にデプロイをすると数十分かかるため, 事前にアプリケーションの接続を FQDN を使うように変更することにしました. 例えば DSN では postgres://src:xxxxxx@10.0.0.1:5432/src と 10.0.0.1 に向いていたものを src.db-consolidation.mercari.internal と FQDN に変更します: postgres://src:xxxxxx@src.db-consolidation.mercari-shops.internal.:5432/src こうすることによって書き込み停止した後に src.db-consolidation.mercari.internal を source から destination に変更することで, アプリケーションのデプロイを伴わずに切り替えが可能となります. 書き込み停止 書き込みの停止については write/read ユーザーが分かれておらず同一ユーザーでクエリが実行されていたため, 少し工夫が必要です. もしユーザーが分かれていれば write ユーザーの RENAME などでブロックすることも可能ですが, そうなってはいないためユーザーの権限を剥奪するかデータベースごと read only にするかの 2 通りの方法が考えられます. ユーザーは cloudsqlsuperuser Role を持っているため, REVOKE を実行して更新権限剥奪するよりも, 有効/無効が単純なデータベースを read only にすることにしました: ALTER DATABASE xxx SET default_transaction_read_only TO on; ただし, この ALTER 文は default を変更するのみで transaction で明示的に SET TRANSACTION などが実行されている場合は書き込み可能となってしまうため, それが利用されていないことを確認する必要があります. メルカリ Shops では DB 周りは https://github.com/ent/ent を利用しており, SET するには WithVar を実行するか, 直接 SET XXX を実行する必要がありますが, そのどちらも実行されていないため ALTER DATABASE を採用しました. 実際の流れですが, まずアプリケーションの DSN を FQDN(向き先は source-001) に変更しデプロイします. デプロイ完了し, 実際に FQDN が利用されていることが確認できたら書き込みを停止させるために source 側で ALTER 文を打ちます. この ALTER 文は Connection Pooling などすでに接続されているクライアントには有効ではないため, 一度すべての接続を切断して再接続を促します. ## 書き込みを停止 ALTER DATABASE src SET default_transaction_read_only TO on; ## 自分以外の接続をすべて KILL SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid <> pg_backend_pid() AND datname = current_database() AND usename = CURRENT_USER; このあとの接続はすべて default_transaction_read_only = ON となっているため, (明示的に指定しない限り)書き込みは失敗します. 切り替え 切り替えは FQDN の向き先を destination の IP に変更することで切り替え可能です. その前にデータが同期されているかなどを確認しておく必要があります. Cloud SQL での Logical Replication は同期レプリケーションではないため, 厳密にはすべての更新が伝播したかをチェックする必要がありますが, 前述の通りメルカリ Shops は read heavy なユースケースであって差分同期は十分早いため, シンプルな確認だけするようにしました. ## source SELECT pg_wal_lsn_diff(sent_lsn,write_lsn) write_diff, pg_wal_lsn_diff(sent_lsn,flush_lsn) flush_diff, pg_wal_lsn_diff(sent_lsn,replay_lsn) replay_diff FROM pg_stat_replication; ## destination SELECT pg_wal_lsn_diff(received_lsn,latest_end_lsn) FROM pg_stat_subscription; これらの source/destination での WAL の同期状況, 実行状況を何度か確認しそれぞれ 0 のままだった場合は切り替え可能として進めます. また, Logical Replication は仕様として sequence は同期されないため, もし利用している場合はそれもここで合わせておく必要があります: ## source SELECT * FROM pg_sequences; SELECT max_value FROM pg_sequences WHERE sequencename = '${SEQUENCE_NAME}'; ## destination SELECT setval('${SEQUENCE_NAME}', 99999); SELECT max_value FROM pg_sequences WHERE sequencename = '${SEQUENCE_NAME}'; これで source と destination のデータが揃っている状態となるため, FQDN の向き先を destination の Private IP に変更し, DNS が伝播した後に再度 source 側の接続を KILL します. これで source 側に残っている接続がすべて destination に切り替わります. $ dig +short src.db-consolidation.mercari-shops.internal. 10.0.0.2 ## source SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE pid <> pg_backend_pid() AND datname = current_database() AND usename = CURRENT_USER; ## destination SELECT * FROM pg_stat_activity WHERE pid <> pg_backend_pid() AND datname = current_database() AND usename = CURRENT_USER; FQDN を変更し source への接続を KILL しすべての接続が destination に向くことが確認できたら, downtime は終了となります. 統合した結果 上記の手順で統合作業を行うことで, 長くても 2~3 分程度の停止時間で切り替えが完了し, 実作業はメンテナンスなどを設けずにすべてオンラインで日中に完了させられました. エラーも切り替え作業前後の接続を KILL することによる再接続でエラーとなる程度で, 瞬断程度の問題で済みました. また統合による効果として コスト効果: 25%程度削減 余剰なインスタンス削除による運用負荷軽減 など, 当初の課題をいくつか解消することができました. メルカリ Shops では IP ベースでの接続をしていたためこのような手法で統合/切り替えを行ってきましたが, Cloud SQL Auth Proxy などではまた別のアプローチ( v2.15.0 の DNS ベースを利用するなど)を取る必要があるかと思います. また, より強い整合性が求められるケースや切り戻し用インスタンスを準備して切り戻しに備えるなど, 今回の統合手法よりも難しいケースも考えられます. 普段チームでよく運用している MySQL とは勝手が違うことが多くかなり勉強になりました. 最後に 現在, メルカリでは学生インターン/エンジニアを積極的に募集しています. ぜひ Job Description をご覧ください。
株式会社メルカリのPlatform Enablerチームで新卒エンジニアとして働く Tianchen Wang (@Amadeus) です。今回は、Large Language Model (LLM)を利用してフリマアプリ「メルカリ」の次世代インシデント対応を構築した事例を共有します。 今日の急速に進化する技術環境において、堅牢なオンコール体制を維持することは、サービスの継続性を確保するために重要です。インシデントは避けられないものですが、迅速に対応し解決する能力は、お客さまに安心・安全の体験を提供するために不可欠です。これは、メルカリのすべてのSite Reliability Engineer(SRE)と従業員が共有する目標です。 この記事では、Platform Enablerチームが生成AIを活用して開発したオンコールバディであるIBIS (Incident Buddy & Insight System) の紹介をします。IBISは、エンジニアのインシデント解決を迅速化し、MTTR(Mean Time to Recovery)を短縮することで、組織やエンジニアが負担するオンコール対応コストを削減することを目的として設計されています。 課題の認識と解決のモチベーション メルカリでは、お客さまが安心・安全に製品を利用できることがすべての従業員によって共有される優先の目標およびビジョンです。このために、異なる部門が協力し、オンコールチームを設立しました。毎週、オンコールメンバーには多くのアラートが発生し、その多くは実際にお客さまに影響を与えたインシデントとして扱われます。これらのインシデントはお客さま体験の悪化をもたらすため、インシデントが回復するまでの平均時間(MTTR)を短くすることが被害を最小化する上で重要です。 さらに、オンコールメンバーはこれらのインシデントに対処するために多大な時間を割かなければならず、新しい機能を開発するために利用できる時間が間接的に削減され、ビジネス目標の達成能力に影響を及ぼします。 結果として、 インシデント発生時にMTTRを短縮し、オンコールメンバーへの負担を軽減 することが、プラットフォームチームにとって重要な課題となっていますが、Large Language Model (LLM)の登場により、これらのインシデント対応を自動化することが可能な解決策として浮上しました。 深掘り:IBISのアーキテクチャ インシデント対応システム「IBIS」のアーキテクチャを詳しく見ていきましょう。 図1. IBISのアーキテクチャ 高レベルの視点から、過去のインシデントについての振り返りレポート情報をインシデント管理ツール Blameless から抽出します。これらのレポートには、暫定措置、根本原因、障害による損害などのデータが含まれています。これらのデータはクレンジング、翻訳、および要約のプロセスを受けます。その後、OpenAIの埋め込みモデルを使用して、これらのデータソースからベクターを作成します。 ユーザーが自然言語でSlackボットに質問を投げかけると、これらのクエリもベクターに変換されます。その後、会話コンポーネントが質問に関連するベクター埋め込みを検索し、関連する言語構造を整理してユーザーに応答を形成します。 アーキテクチャ全体を「データの前処理」と「会話機能」の2つの主要コンポーネントに分けて詳しく説明します。 データの前処理 以下はIBISがインシデントデータを前処理する方法です。 図2. IBISのデータ処理プロセス データ抽出 Blameless には各インシデントのプロセス詳細、インシデントSlackチャンネルからのチャットログ、振り返りおよびフォローアップアクションなど重要なインシデント関連情報が含まれています。Google Cloud Schedulerを活用し、Blamelessの外部APIから最新のインシデントレポートを定期的にGoogle Cloud Storageバケットにエクスポートします。このプロセスはサーバーレスの原則に基づいて設計され、Google Cloud Run Jobs内で実行されます。 データクレンジング Blamelessから取得したデータを無差別にLarge Language Model (LLM)に送信することはできません。それは、データに多数のテンプレートが含まれており、ベクター検索( コサイン類似度 )の精度に大きく影響を与える可能性があるだけでなく、膨大な量の 個人識別情報(PII) が含まれているためでもあります。潜在的な情報漏洩のリスクを軽減し、生成される結果の精度を高めるため、データクレンジングは必要なプロセスです。 データからテンプレートを除去するため、データがMarkdown形式であることを利用し、LangChainが提供する Markdown Splitter 機能を使って関連するセクションを抽出します。PIIに関しては、種類が多いため、 SpaCy NLPモデルを使用してトークン化し、語の種類に基づいて潜在的に存在するPIIを削除します。 データクレンジングコンポーネントはGoogle Cloud Run Functionsで実行されます。このステージ以降は、Google Cloud Workflowを使用してシステム全体を管理します。Google Cloud Storage Bucketに新しいファイルが追加されると、Eventarcが自動的に新しいワークフローをトリガーします。このワークフローはHTTPを使用してデータクレンジング用のCloud Run Functionを起動し、完了するとプロセスの次のステージに進みます。クラウドワークフローを導入することで、ETLプロセス全体のコードメンテナンスが容易になります。 翻訳、要約、エンベディング クリーンになったデータはプロセスの次の段階に進みます。データクレンジングのおかげで、LLMモデルを利用して、データをよりスマートに処理することができます。メルカリでは、インシデントレポートが日本語と英語で書かれているため、これらのレポートを英語に翻訳することは、検索精度を向上させるために重要なステップです。翻訳ステップをGPT-4oベースのLangChainに依頼しています。また、多くのレポートが長文であるため、内容の要約もベクター検索精度を向上させるために重要です。GPT-4oがデータの要約を支援します。最後に、翻訳された要約済みのデータはエンベディングを経て、ベクターデータベースに格納されます。 翻訳、要約およびエンベディングプロセスはGoogle Cloud Run Jobsで実行されます。データクレンジングが完了すると、Cloud WorkflowがCloud Run Jobを自動的にトリガーします。図2に示されているように、エンベディングされたデータはLangChainが提供する BigQueryベクターストア パッケージを使用して、BigQueryテーブルに格納されます。 会話機能 Slackベースの会話機能はIBISのコア機能です。私たちの設計では、ユーザーはSlackでボットに言及することで、自然言語でIBISに直接質問を投げかけることができます。この機能を実現するために、Slackからのリクエストを常時受信し、ベクターデータベースに基づいて応答を生成できるサーバーが必要です。 図3. IBISの会話システム 図3に示すように、このサーバーはGoogle Cloud Run Service上に構築されています。ベクターDBとして機能するBigQueryから関連情報を取得し、それをLLMモデルに送信して応答を生成します。 クエリの処理に加えて、会話コンポーネントは、短期記憶などの他の機能もサポートしており、インタラクティブな体験を向上させます。 短期記憶 エンジニアがインシデントの理解を時間とともに深めることを考慮し、同一スレッド内で記憶機能を取り入れることは、インシデントの解決策を提供するIBISの能力を強化するために重要です。図4に示されているように、LangChainのメモリ機能を使用して、同じチャンネルからのユーザーのクエリとLLMの応答を保存します。同じチャンネルで追加のクエリが投げかけられる場合、スレッド内の以前の会話がLLMに送信される入力の一部として付加されます。 図4. 短期記憶の設計 このストレージソリューションは、メモリをCloud Run Serviceインスタンスのメモリ内に配置するため、新しいバージョンのIBISを再デプロイしてCloud Run Serviceを更新すると、メモリが消失します。詳細については、 LangChainのメモリドキュメント を参照してください。 図5. 短期記憶のケース インスタンスをアクティブに保つ 短期記憶機能のメモリデータが現在インスタンスに保存されているため、コールドスタート時にメモリが失われないようにこのインスタンスをアクティブに保つ必要があります。これを達成するために、この ドキュメント のガイダンスに基づいた戦略を実施しました。Cloud Run Serviceインスタンスに定期的にアップタイムチェックを送信して、アクティブな状態を維持します。このアプローチはシンプルで、コストも最小限です。また、このサービスのスケールアップを制限し、インスタンスの最大数と最小数の両方を1に設定しました。 今後の展望 正確に ユーザーフィードバックを収集する ことが主要な目標の一つです。自動評価のためのヒューマン・イン・ザ・ループアプローチを採用し、ユーザーの調査応答をデータポイントとして収集し、IBISを継続的に改善する計画です。 従来の言及ベースのクエリ方法から Slackフォームベースの質問アプローチ に移行する予定です。この変更は、ユーザーのクエリを精緻化することにより、応答の精度を向上させることを目的としています。 社内ツールの継続更新を考慮し、会社のドキュメントに基づいて LLMモデルをfine-tuningする 計画です。これにより、モデルが最新で関連性のある回答を提供することを確実にします。 まとめ このプロジェクトは2024年12月末に初期バージョンをリリースしました。このブログを書いている時点までで(2024年1月)、IBISはメルカリのインシデント対応用のいくつかのslackチャンネルで使用可能になりました。このツールを利用するユーザーの数は増え続けているので、継続的にユーザーフィードバックを収集し、回復までの平均時間(MTTR)への影響を監視していきます。 さいごに 現在、株式会社メルカリでは学生インターン・新卒エンジニアを積極的に募集しています。ぜひ Job Description をご覧ください。
こんにちは、九州大学大学院1年の@masaと申します。 私は、2024年11月から12月末までの2ヶ月間、メルカリ ハロのフロントエンドエンジニアとして、インターンに参加しました。 左からインターンの@masa、メンターの@d–chanさん 今回は、その中で特に注力したインテグレーションテスト戦略と、メルカリでの学びについてお話しします。 なぜ「メルカリ ハロ」のインターンに参加したのか インターンに参加した主な目的は、大規模サービス、特にtoC向けのサービス開発を体験することでした。メルカリのサービスの中でもメルカリ ハロは、リリースして1年も経っていない新規のプロダクトであり、スピードと品質が求められる現場で、実践的な開発プロセスを学ぶ絶好の機会だと考えました。 また、メルカリという会社の雰囲気やカルチャーを直接体験して解像度を上げてみたいという興味も、参加の大きな動機の一つでした。 インテグレーションテストへの取り組み インターン期間中、大小様々なタスクに取り組みましたが、中でも注力したのが事業者向け画面のインテグレーションテストです。私がジョインした時点で、テックリードの@ryotahさん主導で技術選定と環境構築は完了しており、テストカバレッジ向上に取り組む段階でした。 メルカリ ハロでは、インテグレーションテストに関して、メルペイのフロントエンドテスト方針も踏襲して、仕様書(Spec)に準拠した、ページ単位のインテグレーションテストを実装しました。この過程で以下の2点に関して、改良に取り組みました。 冗長なコードの回避 バリデーションテストの最適化 冗長なコードの回避 Specに従ってテストを記述することで、チーム内でのテスト粒度や方針の一貫性を保てます。しかし、Specに厳密に従いすぎると、異なる画面で同じフォームコンポーネントを使う場合などに 検証内容が重複してしまい、コードが冗長になりがちです。 この問題に対し、以下の3つのアプローチを検討しました。 共通コンポーネントにテストを記述 メリット:テストコードの重複を解消できる。1つの共通コンポーネントに対するテストを集約できるため、同じ検証ロジックを何度も書く必要がなくなる。 デメリット:インテグレーションテストとしては「実際のアプリケーションに近い形でテストしたい」という方針とやや乖離する。複雑な部分をコンポーネント化してしまうと 「人によってテストの書き方が異なってしまう」 という懸念もある。 全ての画面でテストを記述: メリット:上記二つの中間的アプローチで、各ページでの実際のユーザー操作を想定したSpecに基づいたテストを忠実に書くことになるため、微妙に異なるユースケースやバグを見落としにくい。 デメリット:同様のテストロジックを大量に書くことになり、変更があった際の修正も多岐にわたってしまいメンテナンスが大変。 代表的な1画面でのみ共通コンポーネントのテストを記述 : メリット: 上記2つの中間的アプローチで、テストの冗長性をある程度抑えつつ、基本機能の担保が可能。 デメリット: 完全な網羅性はないものの、必要に応じて各ページごとに追加テストを柔軟に書くことで補える。 最終的には、 「代表的な1画面でのみ共通コンポーネントのテストを書く」 方式をベースに、 ページ固有のロジックがある場合だけテストを追記する という運用に落ち着きました。現状のチームリソースや開発速度を考慮すると、これが最も 現実的かつ柔軟 なアプローチだと判断しました。 バリデーションテストの最適化 フォームライブラリ(react-hook-form)の標準的なバリデーションはユニットテストでカバーし、インテグレーションテストでは、ユニットテストでは検証しにくいバリデーションに集中しました。 たとえば、以下のように submit時にエラーがあった場合にモーダルを表示 するロジックは、react-hook-formのschemaテストだけではカバーしにくいケースです。 const onSubmit = (value) => { // 入力項目に誤りがある場合 if (value.name !== 'hoge') { setShowModal(true) } // データの送信など } ここのような部分をPlaywrightを使ったインテグレーションテストで検証します。 // Playwright を使用したインテグレーションテストの例 test('入力項目に誤りがある場合にモーダルを表示する', async({page}) => { // 省略 // ... await page.getByLabel('名前').fill('foo'); await page.getByRole('button', {name: '送信'}).click(); await expect( page.getByRole('dialog', { name: '名前にはキーワードを含めてください' }).toBeVisible(); }); テストを書くコストとリターンのバランスを意識しつつ、後々の技術負債にならないよう、意味のあるテストコードを心がけました。 また、開発プロセスの透明性と効率性を高めるため、インテグレーションテスト用のSlackチャンネルを作成しました。背景としては、フロントエンド領域で気軽に技術的な相談ができる場が十分に整備されていなかったことや、別チームのエンジニアとコミュニケーションを取る機会が少なかったことが挙げられます。そこで、実装中に直面した課題や疑問点を具体的なケースとともに共有できるようにしたことで、 チーム全体で問題意識を共有することができ、より良いソリューションを見出す ことができました。 その他の活動と経験 インターン期間中に、生成AIを活用した業務効率化アイデアソンにも参加しました。 90分という限られた時間内で、チームビルディングからアイデア出し、プロトタイプ作成まで行うというかなり濃密なスケジュールでしたが、とても刺激的で楽しかったです。 アイデア選定時は「共感が得られるか」と「短時間で成果を出せるか」を重視しました。最終的には、「カレンダーたのんだ〜」という、参加者の空き状況や入れたい予定の性質をもとにGoogleカレンダーの予定調整を効率化するアイデアに取り組むことにしました。 チームの方々が優秀すぎて、自分の役割を見出すのに最初は戸惑いましたが、自分にできる貢献を考え、ワークフローの設計および実装を担当しました。Zapierを使用してカレンダー情報を取得する部分も実装したかったのですが、時間の制約で叶いませんでした。 そして結果はなんと、優勝することができました🎉 (チームメンバーのみなさんに感謝です🙇‍♂️) 英語でのコミュニケーションでの苦労 インターン選考時の面接では、所属予定のチームは英語の使用頻度が高くないので、英語が苦手でも大丈夫というお話を伺っていました。しかしチームの状況が変わり、私が参加した初週から、週に1回フロントエンドのMTGは英語になりました。英語でのコミュニケーションには正直不安があり、特に司会役を担当しながら英語で進行しなければいけない回は、大変苦労したことを覚えています。事前にチートシートを用意するなどして、なんとか乗り切りました…。 さらに、オフィスには多くの外国籍社員が在籍しており、社内イベントへ参加することで自然と英語を使う機会が増えました。また、Pull Requestのレビューも英語でやりとりするため、日常的に英語に触れられる環境だと実感しました。 予想以上に英語を使うシーンが多かったことで最初は戸惑いましたが、そのおかげで英語学習のモチベーションが格段に上がりました。技術的スキルだけでなく、グローバルなコミュニケーション能力も磨ける環境は、エンジニアとしての成長に大きな価値があると感じています。 さいごに 今回、メルカリ ハロのインターンを通して、大規模サービス開発の現場で多くの貴重な経験を積むことができました。特に、インテグレーションテストの実装を通じて、効率的で品質の高いテストコードを書く考え方や、チームコミュニケーションの大切さを深く学んだと感じています。 この2ヶ月間で得た知識や経験を、今後の学びやキャリアに活かしていきたいと思います。最後になりましたが、メンターのd–chanさんをはじめ、あたたかく迎えてくださった皆さまに心より感謝申し上げます。
はじめに こんにちは、メルカリでJapan RegionのCTOを担当している木村です。僭越ながら今年も最後のアドベントカレンダーの投稿を担当させていただきます。 昨年投稿した開発組織にとっての Engineering Roadmapの必要性 についての記事では、「開発スケジュールの期待値調整」が容易になったり、「将来を見越したアーキテクチャ」を作ることができたり、「Visionを組織に浸透させやすくなるメリット」があることなどをご紹介しました。しかし、昨年は実際にEngineering Roadmap(以下Roadmapと呼ぶ)にどのようなアイテムがあるのか、あるいはどのように運用されているのかといった具体的なご説明までには至ることができませんでした。本稿では、運用上難しかった話なども含めて、より実践的な内容をお話ししたいと思います。 昨年のRoadmapを振り返る まずは、前回のRoadmap作成時の狙いと実際に1年後どのような結果になったのかをご説明したいと思います(まだビジネス上オープンになっていないものもあるため、公表されているものに絞ってお話しします。Roadmapに関連するビジネス的なイベントとしては 2024年3月6日に新規事業であるメルカリ ハロをリリース。2024年8月29日に、台湾のお客さまがWeb版「メルカリ」を通じて日本で出品された商品を購入できる「 越境取引 」 の展開をスタートしたほか、 2024年9月10日には生成AIを活用した「AI出品サポート」の提供 を開始しました。 これらを技術的に実現させるため、23年12月の段階で、Roadmapの大きな方向性として以下のように定めていました。 ① 3つの領域の"開発コストの低下"と"Enabling"を実現する 中長期的なビジネスの拡張を実現するために、以下の3つの事項のバランスを保ちつつ、Biz Enablingと開発コストの低減を実現する。 既存サービス開発簡易化 新規事業展開簡易化 国際展開 メルカリグループではメルペイやメルコイン、今回のハロのように継続的に新規事業を提供しています。このような新規事業を立ち上げるたびにすべてを0から開発するのではなく、既存のプラットフォームを拡張・活用することによって、新規事業の展開をより高速かつ低コストで実現することをゴールに掲げました。この方針を応用することで、国内の新規事業の立ち上げのみではなく、多国展開もより効率的に実現することも目指しています。当然ながら新規のものだけでなく、既存サービスへの改善もあるので、既存サービス開発の効率化も同時に目標として掲げていました。 これらを実現するためにRoadmapの中にアクションアイテムとして定義していたものを一部抜粋してご紹介します。 Golden Path これまで、メルカリグループの組織としてどの技術を標準と位置付けるのかは特に明文化されていませんでした。もちろん言語やデータベースの選定などにおける暗黙的なコンセンサスは組織のなかにありましたが、基本的には各事業が必要な技術の選定をそれぞれで行ってきました。これらは柔軟性や自由度の高さという観点ではうまくワークすることもありましたが、一度使い始めると長期的なメンテナンスコストが発生したり、事業の立ち上げ時にゼロから投資を行う必要があったり、 選定のために同じような議論を繰り返すことになるなど、スピード面で課題がありました。 Golden Pathはグループ内での技術の標準化やフレームワークを作ることによって、開発と運用コストを落とすことと、同時に新規サービスを作る際の効率化を狙いました。アクションアイテムとしては以下のように設定していました。 技術標準をベースとしたアプリケーション構築領域(Web, Mobile, Backend)での Bootstrapping tool の開発を進め、Global Expansion への活用が可能な状態になっている。 これは言い換えると、アプリケーションを作る際に、メルカリの環境に適した効率的で標準化されたフレームワークを提供することを目的としています。1年後どのようになったかというと、残念ながらすべての領域でこれを実現することはできなかったのですが、WebについてはBootstrapping toolが完成されて、新規事業を作る際のWeb開発を大きく効率化することができました。また、改めてADRや Tech Radar の仕組みが整備され、BackendやMobile開発においても開発に使われるTech Stackを改めて標準化することによって、関係者にコンセンサスを取る手間が省けるようになり、技術選定のコストを低減させることができました。 また、新しい microservice や Webアプリケーション を本番環境で運用する前の基準を定めたチェックリストである Production Readiness Check (PRC) の効率化・短縮化 も実施しました。これまで2ヶ月以上かかってしまっていた PRC を自動化することによって効率化する試みです。 技術の標準化というものは、ごく当たり前のことに聞こえてしまうかもしれませんが、弊社でも継続的な新規事業の創出や技術的なトレンドの変化によって、全社でのコンセンサスを取ることが難しくなってきていました。ここで、この課題をそのままにせず、一度立ち止まり、全社で標準的に用いるTech Stackを再整理することで、改めて開発の高速化を狙う決断をしました。 IDP いわゆるアカウントIDに関するプラットフォームの改善です。IDはビジネス戦略に合わせて先行して技術基盤を用意しなければならず、Roadmapの中でも最重要な項目です。PassKeyに関する開発や普及に関してもこの項目に含まれます。こちらも、国際展開に向けて以下のようなアクションアイテムを設定していました。 新しくなったアカウント登録・ログインプロセスが、実際に日本以外のregionから利用される状態になっている。 計画的に開発を行い、こちらも1年後の現在、台湾で提供されているサービスでのアカウント作成に活用されています。上記にも述べましたように、IDはビジネス戦略の根幹になる技術と言っても過言ではありません。 昨年の記事 にも記載しましたように、お客さまへ新しい価値を提供するには、開発組織としてビジネスの方向性にアラインしつつ、先行してプラットフォームを用意する必要があります。まさに台湾での越境取引の件は昨年の時点で、他国へのビジネス展開が概ね決まっていたので、先行してそれを実現するためのアクションプランを計画的に実装し、提供することができました。また、これまでにもメルコインやメルカリ ハロを提供する中で、メルカリのIDとeKYCさえ完了していればとてもシームレスに新規サービスをご利用いただける仕組みができあがったのも、継続的にIDPが先を見越した開発ができていたことに起因しています。 AI出品サポート この時期に生成AIの活用の推進も強化しており、9月にリリースされた AI出品サポート のアクションアイテムも設定してありました。 AI出品サポート(出品補助) 生成AIのポテンシャルの大きさは明らかではありましたが、これをいち早くビジネスに導入して、特にメルカリにおける出品の利便性を向上させてお客さまに早く価値を提供したいと考えていました。この段階ではまだ生成AI社内でも検証段階でありましたが、早い段階でサービス活用の指針を定められていたことで、業界でも比較的早い段階で生成AIを実際にサービスに活用することができました。 Roadmapのアクションアイテムは実際にはより多くのものがあるのですが、雰囲気を掴んでいただくために公開できる範囲で一部のみ抜粋させていただきました。 この先にも述べますが、Roadmap作成の最大のメリットは、Visionを示すだけに留まらず、やること・やらないことを明確に意思表示できることだと感じています。特に大きなリアーキテクチャが伴うものについては「やらないといけないと思っていた」や「やろうと思っていた」ことは、なるべく早くに意思決定して、早く取り掛からなければ、後々解決するのが困難になってしまうことが多々あります。私たちは継続的に解決しなければならない課題について議論し、ビジネスとの方向性とアラインしながら技術的な投資の決定を継続的に行っています。 Next level of Scalability and Resiliency 今後のサービスの成長をより堅牢にするために、Infrastructureレベルでの改善もRoadmapに設定していました。これまでも私たちはInfrastructureのResiliencyやSalabilityの改善を行ってきましたが、今後の国際展開によるお客さまの増加や金融事業を提供しているメルペイのResiliencyを改善するためには抜本的な仕組みの改善が必要でした。 Scalabilityの改善に関して、特に大きな進歩は、大規模なコアなデータをMySQLの物理サーバに保存していたものを、 慎重な検証を重ねた上 で、TiDB Cloudへのmigrationを始めたことです。これによりSclabilityの改善と運用コストの大幅な低減の実現を狙っています。 そして、国際展開するための基盤の準備として、複数拠点でサービスを運用するためのMulti RegionでのInfrastructureの構築も進めています。こちらについては、現在も進行中であり、まとまった形で発表できる状態になったら再度詳しくご説明したいと思います。しかし、並行してInfrastructureのコストを最適化しつつも、Multi Regionでのサービス運用を実現するためにはコストの増加は避けられません。したがって、 FinOpsの文化醸成を継続的に行い 、具体的なコストの低減を全社の目標として共有しています。今年は、特にCUD採用率、Spot VM採用率を全社で上げていき、コンピュートリソースの最適化を実現することができました。 Roadmapの活用と運用 この作成したRoadmapをどのように活用、運用しているのかについてご説明します。 Visionの浸透に活用する Engineering組織で重要なことについてVisionの浸透があります。「私たちは今後何を実現したいのか」、そして「どのような過程を経てこれを実現させるのか」を一人一人のエンジニアに理解してもらうことが大事です。浸透について、私たちも特別なことをやっているわけではないのですが、Roadmapが完成したら、Engineerが全員参加するAll Handsで作成したRoadmapを使って説明し、Visionの浸透を図っています。まさに、年末の今も来年のRoadmapを作っているところであり、年明けに来年からの3ヵ年に実現したいVisionとRoadmapを全社で説明することになっています。なかなか一度の説明では浸透しないので、プレゼンテーション資料だけではなく、言語化されたRoadmapの文章をいつでも誰でも見られる状態にすることや、誰でもこれに対してFeedbackできる仕組みを築くことが重要です。それによって、一人でも多くのエンジニアがRoadmapを自分事として捉えて、Roadmapについて真剣に考えてくれることを目指しています。 OKRの設定に活用する 私たちはクォーターごと、つまり3ヶ月ごとにOKRを設定しています。OKRを3か月ごとに考えるのは計画性がないととても大変な作業ですし、OKRの設定に時間がかかってしまうと、設定と同時にすぐにまた次のOKRを考えなければならないといった悪循環となってしまいます。Roadmapで年間の計画が決まっていればOKRに設定しなければいけないオブジェクトの多くをRoadmapから転用することができるので、とてもスムーズに作成することができます。 運用について 当然ながら、Roadmapは掲げたままにしないこと、形骸化させないことが非常に重要です。そのために継続的に進捗を確認することやRoadmap自体をメンテナンスすることが重要です。これが完璧な運用方法というわけではないですし、将来変わることもあると思いますが、参考までにわたしたちの現在の運用方法をお話ししたいと思います。 基本的には以下のイテレーションでロードマップの作成とアップデートを行なっています。 12月にRoadmapのメジャーアップデートを行い(前年のRoadmapをリバイズして1年-3年の計画を作成する) その後はクォータ末に毎回マイナーアップデートを行う(3月、6月、9月) プログレスのチェックは月に1回各アクションアイテムのプログレスを確認しています。当然ながらプログレスの確認は必ずやったほうが良く、やりながら方向性をアジャストすることもできますし、この継続的な運用によってさらにVisionの浸透が強化されます。 Engineering Roadmapを運用する上で難しい点と工夫 ビジネスプライオリティの影響 1-3年間のRoadmapを立てて、着々と開発を進めても、ビジネス上のプライオリティが下がってしまったり、方向性が変わることはどうしても発生します。むしろ、そういう変化は受容できる仕組みにしなければ現実的な運用は厳しいと考えています。そのため、私たちは月1回のプログレスチェックでの方向性のアジャストや3か月ごとのマイナーアップデートを行って、なるべくフレキシブルにビジネスの要求に応えられる運用を目指しています。 アイテムが多くなりすぎる問題 どうしてもやりたいことを整理するとRoadmapに追加したい項目が多くなりがちになってしまいます。しかし、項目が多くなればなるほど、エンジニアをはじめとする現場のメンバーの理解を得ることが難しくなりますし、現実的には全てに手をつけられなくなってしまうリスクがあります。実際に私も「やらないことを決める」努力をして項目を減らす努力はしているのですが、まだまだ多い状態です。毎年Roadmapを洗練させていくなかで、少しずつ数を絞ってはいますが、実際に運用してみると「少し少ないかな」と思うくらいの方がいいと個人的には思います。 最後に 今回は少し具体的にRoadmapの内容や運用についてご説明させていただきました。本当はすべてのRoadmapを公開して、それぞれの狙いや振り返り、改善点などもお話することができるとよりイメージをお伝えしやすいのですが、まだまだ世に公開できていないものもありますので、それはまた来年末にとっておき、ご容赦いただけたら幸いです。Roadmapの設定と運用において、当たり前の内容ではあるのですが、せっかく作成したRoadmapを形骸化させないためには、議論を重ねて極力正確なものを作り、継続的に見直していくことがポイントとなります。そして、作成したものをそのままにせずに、いつでも誰でもアクセスできて、Feedbackを提供できる仕組みと雰囲気づくりをすることによって、血の通ったRoadmapを作成することができます。Roadmapは方向性を言語化することで、ビジネスとエンジニアリングの間の理解の差を埋めて、方向性の不確実性を減らすことができ、自信を持って開発し続けるために欠かせないツールだと考えています。ご一読くださったみなさんにとって、少しでも手助けになったら幸いです。
こんにちは。Mercari Corporate Products Teamのエンジニアの @yuki.watanabe です。 この記事は、 Mercari Advent Calendar 2024 の21日目の記事です。 はじめに 現在、内製の会計仕訳システムの開発に携わっています。このシステムには様々なバッチ処理が実装されているのですが、BigQueryへクエリしデータを抽出するためのバッチで誤検出の問題がありました。本記事ではこの問題に対して検討した複数のソリューションと結果的にどの方法を採用したのかについて紹介します。 バッチ処理の課題を解決する際の参考にしていただけると幸いです。 リコンサイルエラー検出のバッチについて 会計仕訳システムにおけるデータの流れ まず、会計仕訳システム(図のAccounting System)におけるデータの流れを紹介します。お客さまがメルカリやメルペイを使用した場合、取引内容に応じて様々なMicroservicesが処理を行い、金銭に関わるデータがある場合は、会計仕訳システムのPub/Subに送信します。会計仕訳システムではCloud Functionsでバリデーションを行い、Spannerのaccounting_dataテーブルへ登録します。 次に、各Microservicesは会計仕訳システムのPub/Subに送信済みのデータについて、会計仕訳システムのリコンサイル用APIへ送信します。このAPIは後述するリコンサイルと呼ばれる突合処理を行い、結果をSpannerのreconciliationテーブルへ登録するもので、Kubernetes上のgRPC ServerのAPIとして実装されています。 Spannerへ登録されたデータ(accounting_data、 reconciliation)は、Cloud ComposerとDataflowを用いて、BigQueryへ1日に1度差分を同期しています。 リコンサイルは会計データの確からしさを検証する仕組み リコンサイルとは、会計仕訳システムと会計データの送り元となるMicroservice間のデータの突合処理のことを指します。Microserviceはデータベースに登録した会計データをリクエストデータに含め、リコンサイル用APIへリクエストします。APIでは、リクエストデータと会計仕訳システムに登録された会計データ(accounting_data)を突合し、突合結果をreconciliationテーブルのstatusカラムに保持して登録します。このリコンサイルを通じて、Microservice側のデータと会計仕訳システム側のデータが一致していることを保証しています。 以下はreconciliationに登録されるstatusカラムの値のイメージです。 突合成功: status=’success’ 突合失敗: status=’failed’ リコンサイル検証用バッチでリコンサイルのエラーがないかを確認する 突合が失敗したデータについてはリコンサイルのエラーと考えられます。そこで、リコンサイルエラー検出用のバッチをCronJobを用いて実装しています。このCronJobでは1日に1度BigQueryへクエリし、リコンサイルエラーのデータを抽出します。エラーのデータが存在する場合は、Microservice Teamへ共有し、再度のリコンサイルAPIへのリクエストを依頼しています。 SpannerとBigQueryの同期タイムラグによる誤検出 しかし、上記のバッチには課題が存在しました。Spannerにはリアルタイムにリコンサイル結果が登録されていますが、バッチが参照しているBigQueryには1日に1度しか同期されません。このSpannerとBigQueryの同期タイムラグにより、バッチの実行結果には誤検出である偽陽性のデータが含まれていました。「Spannerには突合済みのデータが存在するが、BigQueryには未同期」のデータは本来は突合が成功していますが、バッチでは突合が失敗したデータとして検出されていました。 このため、バッチによってリコンサイルエラーとして抽出されたデータについて、エンジニアが「Spannerにクエリをして本当にリコンサイルのエラーがあるのかどうかを調べる」という手動の運用作業が発生していました。 会計仕訳システムでは、会計データを扱っているという特性上、毎月の月初に前月分のデータを確定する、いわゆる「締め」が必要になります。月末付近に発生したリコンサイルエラーは速やかに送り元であるMicroservice Teamにリコンサイル依頼をし、リコンサイルエラーを解消しなければなりません。しかし、上記の運用作業が発生する場合、リコンサイルエラーの検出から解消までに日をまたいでしまうこともあり、会計業務への影響が出てしまうこともありました。 これらの運用課題の解消のためには、「リコンサイルエラー検出のバッチの誤検出をゼロにする」ということが必要でした。 Spanner Data Boostの採用 検討したソリューション 運用課題の解消のため、複数のソリューションを検討しました。 1. StreamingでSpannerからBigQueryへ同期する方法 まず、Spannerに登録されたデータをStreamingでリアルタイムにBigQueryへ同期する方法を検討しました。Dataflowの Spanner change streams to BigQuery template などを利用し同期用のJobを作成することで、技術的には実現可能な方法ではあります。Streamingでリアルタイムに同期できると、上記以外の課題の解消にも役立てられるため、大きな恩恵を得られたでしょう。一方で、Stremingの同期用のJobを採用する場合は、同期の不具合がある場合にも備えなければなりません。例えば、同期用Jobが停止する、BigQueryへ二重でデータが登録される、BigQueryへの一部のデータの登録が失敗するなどが考えられます。こうした不具合が発生した場合には、手動運用でリカバリするか、もしくはリカバリ用のシステムの実装が必要になりますが、初期の実装とその後の運用まで含めた工数を考慮すると、既存の課題に対するソリューションとしては過大だと考え、採用を見送りました。 2. SpannerとBigQueryの同期頻度を増加する方法 次に、SpannerからBigQueryの同期頻度を増加する方法を検討しました。現状1日に1度行っている同期を2〜3回に増加させ、その後にリコンサイルエラー検出のバッチを実行する方法です。これまでの方法と比較し、偽陽性のデータを減らすことは可能だったかと思います。しかし、Spannerに登録されているBigQuery未連携のデータは多少なりとも存在するため、リコンサイルエラー検出のバッチの誤検出をゼロにすることには向かないと考え、採用を見送りました。 3. Spanner federated queriesとSpanner Data Boostを利用する方法 最終的に、Spannerの Data Boost を活用することにしました。上述の通り、リコンサイルエラー検出のバッチではBigQueryにクエリをしていますが、このクエリを修正し、BigQueryの Spanner federated queries の機能を使い、Spannerへのクエリもしています。Spannerへクエリする際に「Spannerには登録済みだがBigQueryには未同期のデータ」も併せて取得することで、BigQueryとSpannerのデータをどちらも考慮して、リコンサイルエラーのあるデータのみを抽出することができるようになっています。 重要な点としては、Spanner federated queriesを利用する際に、Data Boostを有効化するということです。Data BoostはSpannerのPrimary Instanceへ負荷を与えることなくSpannerへクエリすることができる非常に便利な機能です。リコンサイルエラー検出のバッチでは、Spannerへ登録された1日分のデータを取得しますが、1日分でもかなりのレコード数となるため、もしPrimary Instanceへクエリした場合はパフォーマンスへの影響は避けられません。そこで、Data Boostを有効化しこの問題を回避しています。 この方法を採用した理由は、「リコンサイルエラー検出のバッチの誤検出をゼロにする」という目的を達成可能であり、かつ実装工数の観点でも既存のクエリの改修の範囲で早急に実現できることが見込まれたためです。 BigQueryへのクエリ改修前後のサンプルコード リコンサイルエラー検出のバッチで使用しているBigQueryへのクエリの改修前後のサンプルコードを記載します。 改修前のクエリサンプル SELECT * FROM example_dataset.reconciliation WHERE status != 'success' まず、改修前のクエリではBigQueryのDatasetであるexample_datasetのreconciliationテーブルをFROM句に指定し、statusがsuccess以外のレコードを抽出していました。 改修後のクエリサンプル WITH spanner_reconciliation AS ( SELECT * FROM EXTERNAL_QUERY('spanner_connection_example', """ SELECT * FROM reconciliation WHERE created >= TIMESTAMP(CURRENT_DATE("Asia/Tokyo"), "Asia/Tokyo") """) ) SELECT * FROM example_dataset.reconciliation LEFT JOIN spanner_reconciliation ON reconciliation.id = spanner_reconciliation.id WHERE status != 'success' AND (spanner_reconciliation.id IS NULL OR spanner_reconciliation.status != 'success') 改修後のポイントは2点あります。1点目は、WITH句でEXTERNAL_QUERYの関数を利用している点です。第1引数にSpannerを指定したBigQueryのConnection IDを指定し、第2引数には、クエリ実行日に登録されたreconciliationテーブルのレコードを抽出するクエリを指定しています。 2点目は、メインのクエリのWHERE句の絞り込みです。FROM句にreconciliationテーブルを指定することは改修前と同様ですが、加えてWITH句で定義したspanner_reconciliationテーブルをLEFT JOINし、WHERE句で利用しています。これにより、「BigQueryのreconciliationテーブルのレコードでエラーが発生している」かつ「実行日に登録されたSpannerのreconciliationテーブルのレコードでエラーが発生している、またはレコードが存在しない」条件に該当したレコードのみを抽出できるようになっています。 まとめ 会計仕訳システムのリコンサイル検証用バッチには、SpannerとBigQueryの同期タイムラグによる誤検出の課題が存在しました。そこで、バッチで実行しているBigQueryへのクエリを改修し、Spanner federated queriesとData Boostを利用しSpannerへもクエリすることで、BigQueryへ未同期のデータも抽出するようにし、同期タイムラグによる課題を解決しました。 本記事執筆時点で、改修版のリリースから2ヶ月程度が経過しています。改修前と比較し、月に10件程度発生していたSpannerの手動クエリによる運用作業がほぼゼロになるなどの効果が出ています。 また、この経験が早速別の機会にも役立ちました。あるバッチでSpannerへクエリする処理が、とある変更をきっかけにインデックスが効かなくなり大幅にパフォーマンスが悪化してしまう問題があったのですが、Spanner federated queriesとData Boostを利用することで、これを解決することができました。 今後もSpannerとBigQueryをしばらく使い続けることが予想されるため、Spanner federated queriesとData Boostを利用したアプローチを様々な場面で活用できるかと思います。 明日の記事は kimras さんです。引き続きお楽しみください。 参考資料 Data Boost Overview | Spanner | Google Cloud Connect to Spanner | BigQuery | Google Cloud Spanner federated queries | BigQuery | Google Cloud データ指向アプリケーションデザイン ―信頼性、拡張性、保守性の高い分散システム設計の原理 データエンジニアリングの基礎 ―データプロジェクトで失敗しないために
こんにちは。メルペイ Engineering Managerの @masamichi です。 この記事は、 Merpay & Mercoin Advent Calendar 2024 の記事です。 メルペイのモバイルチームでは現在、メルカリアプリ内に存在するメルペイの数百画面をSwiftUI/Jetpack Composeに移行するプロジェクトを推進しています。 この記事では、プロジェクトの経緯とその進め方について紹介します。 メルペイのリリース メルペイが搭載されたメルカリアプリがリリースされたのは2019年2月です。初期の開発は主に2018年に進めていましたが、当時はSwiftUIやJetpackComposeは発表されておらず、メルペイを含むメルカリアプリはUIKit/Android Viewで開発していました。 その後、2019年内にiOS/Android共に宣言的UIフレームワークであるSwiftUI/Jetpack Composeが発表されました。 GroundUP Appプロジェクト 一方2020年ごろから、母体となるメルカリアプリは長年の開発で積み重なってきた課題を解決するために コードベースを刷新するGroundUP Appプロジェクトが立ち上がりました。 GroundUP AppプロジェクトではSwiftUI/Jetpack Composeが全面採用され、2022年にリリースすることができました。 プロジェクトの詳細についてはコアメンバーの記事を参照ください。 メルカリの事業とエコシステムをいかにサステナブルなものにするか?かつてない大型プロジェクト「GroundUp App」の道程 これからメルカリのエンジニアリングはもっと面白くなる──iOS&Androidのテックリードが振り返る、すべてがGo Boldだった「GroundUp App」 メルペイの各種機能はモジュール化してある程度疎結合な状態でメルカリアプリに組み込んでいたため、新しいアプリにも組み込まれた状態を実現し、GroundUP Appプロジェクトと並行しながら新機能の開発を続けていきました。 メルペイの移行についてはこちらの記事を参照ください。 メルカリアプリのコードベースを置き換える GroundUP App プロジェクトの話 【書き起こし】Merpay iOSのGroundUP Appへの移行 – kenmaz【Merpay & Mercoin Tech Fest 2023】 DesignSystem メルカリではDesignSystemを定義し画面デザインおよび開発を行っています。メルカリでは2019年ごろから段階的にアプリへの導入を進めてきました。 特にGroundUPプロジェクト後の新しいアプリではSwiftUI/Jetpack ComposeのUIコンポーネントに刷新され、DesignSystemの全面的な採用によって画面のUI/UXの統一、ダークモード対応やアクセシビリティの向上が実現しました。 一方で、メルペイは前述の通り初期から開発してきたモジュールをそのまま新しいアプリに統合しました。それらの画面はUIKit/Android Viewで作られており、DesignSystemについてもUIKit/Android Viewの旧バージョンの実装となっていました。それによって、UI/UXの差分、ダークモード非対応、UIフレームワークが違うことによるアーキテクチャの差分といった課題がありました。 GroundUPプロジェクトで得た効果を最大限活用するため、2023年よりメルペイの既存画面のマイグレーションを進めるプロジェクトを開始しました。 Engineering ProjectsとGolden Path メルペイの数百画面をマイグレーションしていくには、長期的な取り組みが必要となります。メルペイではこういった長期的なエンジニアリングへの投資を推進するためにEngineering Projectsという枠組みを構築してきました。 Engineering Projectsの詳細については、VP of Engineeringの@keigowさんの記事をご覧ください。 メルペイのエンジニアリングへの投資を推進する仕組み また、現在メルカリグループ全体で標準的な技術スタックをGolden Pathとして定義し、開発効率の向上や技術資産の再利用を目指しています。DesignSystemで採用されているSwiftUI/Jetpack ComposeはGolden Pathとして定義されており、メルペイのマイグレーションプロジェクトは社内ではわかりやすくDesignSystemマイグレーションプロジェクトと呼んでいます。 グローバル展開を推進する開発組織をつくる——Meet Mercari’s Leaders:木村俊也(CTO) 実際にマイグレーションを実施していくには工数が必要であり、優先度の議論も必要となります。本プロジェクトを立ち上げるにあたってプロジェクト計画書を作成し、背景やアクション、体制やマイルストーンを明確にしました。Golden Pathのような会社の長期的な方針やEngineering Projectsの取り組みもあり、本プロジェクトをEngineering Projectsの1つとして推進しています。 プロジェクト体制と進め方 体制 メルペイでは、プログラムという大きなドメイン毎にプロダクトマネージャーやエンジニアを含めたクロスファンクショナルなチーム体制で事業を推進しています。 Design System マイグレーションを進めるには全プログラムのモバイルチーム、デザイナーとの連携が必要不可欠です。モバイルチームのリーダーとデザイナーで隔週の定例ミーティングをセットし、進捗やブロッカーの共有、マイルストーンの設定を定期的に行っています。 プロジェクトの立ち上げ期は週次で集まって進め方の型を作っていくのが良いと思いますが、ある程度固まってくると隔週がちょうどいいと感じています。 プロジェクトの情報を全て集めたページを社内のConfluenceに作っています。ここでプロジェクト計画書や体制図、機能ごとのSlackのコミュニケーションチャンネル、デザインや開発のノウハウ、QAのテストケース、機能のリリース状況、定例のミーティング議事録などプロジェクトに必要な情報を俯瞰して見ることができるようにしています。 Table of Contentsの一部抜粋 マイグレーションを進めるには工数とタイミングが重要です。プロダクトの新規施策を導入するタイミングで同時にマイグレーションできれば、効率的に進めることができます。一方、それだけでは変化の少ない機能のマイグレーションが進められません。また、緊急度の高い開発に関してはスピードを優先して一旦既存の画面への開発を行うケースもあります。既存の機能をそのまま移行するケース、プロダクトの新規施策を導入するタイミングで同時に移行するケース、どちらもバランスよく進められるように、各プログラムのデザイナーおよびモバイルチームリーダーと密に連携をとりながら進めています。 Screen Listと進捗の追跡 画面のマイグレーションをしていくにも、まずどれくらいの機能および画面があるのかをできるだけ正確に把握する必要があります。 メルペイではプロジェクトを立ち上げる際に全ての画面一覧をスプレッドシートにまとめたスクリーンリストを作成しました。これによって画面数や画面パターンを正確に把握したり、機能のオーナーシップを持つチームや開発・デザイン担当者を一元化して把握することができるようになりました。全ての画面にIDを振ってチーム内で対象とする画面の認識齟齬がないようにもしています。 各画面には以下のような進捗ステータスも付けてグラフにすることで、全体の進捗を視覚的に追跡できるようにしています。 TODO Design In Progress Design In Review Design Done Dev in Progress In QA Done 隔週の定例ミーティングでマイグレーションに取り組んでいる機能の進捗状況を更新しています。 各画面の状況を正確に把握することで、Engineering Projectsの定例ミーティングでもCTO, VPoEに対して透明性高く正確な情報をレポートすることができています。 Screen Listのシート一部抜粋 Strategy Sharing メルペイでは四半期の後半に一度Strategy Sharingという、次の四半期の施策の優先順位の決定や戦略・ロードマップのレビューを行い全社的に共有するタイミングを設けています。その中で、Engineering Projectsとしても次の四半期にターゲットとする機能と進捗率を定義し、全社的にプロジェクトのマイルストーンを共有しています。これによってエンジニアリング部門以外の方々でも進捗を把握することができ、全社的な認知を得ることができています。 現在の進捗状況 これまで2023年から2024年にかけて約2年間プロジェクトを推進してきましたが、2024年12月現在、Androidは約65%、iOSが約60%のマイグレーションを完了してリリースできています。開発中のものも含めると70% ~ 80%のマイグレーションが進んでいます。 Android Progress iOS Progress 今後もメルペイのモバイルエンジニアリングをアップデートすべく、チーム一丸となって100%を目指してプロジェクトを推進していきます。 終わりに この記事では、メルカリアプリ内に存在するメルペイの数百画面をSwiftUI/Jetpack Composeに移行するプロジェクトプロジェクトの経緯とその進め方について紹介しました。 プロジェクト規模も大きく長期的な取り組みで困難なことも多いですが、テックカンパニーとしてこのような取り組みに挑戦できていることはメルカリグループのエンジニアリング組織としての強みだと思います。 SwiftUI/Jetpack Composeへ移行を検討しているチーム、移行を進めているチームの皆さまの参考になれば幸いです。 次の記事は @kimuras さんです。引き続きお楽しみください。
はじめに こんにちは! Microservices Platform Network チーム の hatappi です。 メルカリでは、2023年からCDNプロバイダーを Fastly から Cloudflare へと段階的に移行してきました。現在、ほぼすべての既存サービスのトラフィック移行が完了しており、新規サービスについては全て Cloudflare を使用しています。 この記事では、CDNプロバイダーの比較ではなく、移行プロセスに焦点を当て、スムーズに移行するために実施したアプローチを解説します。また、移行が私たちの最終的なゴールというわけではありません。その先の取り組みの一環として、社内向けの「CDN as a Service」についても紹介します。 背景 メルカリでは、これまでに開発環境および本番環境を合わせて数百のFastlyサービスが存在しており、これらは私たちNetworkチームによって管理されてきました(メルペイのサービスに関してはFintech SREチームが管理しています)。私たちのチームは、GCP VPCのようなクラウド・ネットワーキングやデータセンター・ネットワーキングも管理しています。そのため、限られた時間の中でスムーズに移行を進める方法を考える必要がありました。 移行ステップ 準備 Fastly と Cloudflare はどちらもCDNプロバイダーですが、全く同じ挙動をするわけではありません。たとえば、キャッシュの挙動について見ると、FastlyではオリジンのVaryヘッダーを考慮してキャッシュを分けますが、Cloudflareは現時点では画像に対してのみ対応しています。このように、移行対象のサービスがFastlyでどのような機能を使用しているか、そしてその機能をCloudflareではどのように実現するかを調査する必要がありました。 移行機能を検討する際に重視したのは、現状の挙動を大きく変更しないことです。移行を始めることで、改善点を加えたり新しい機能を試したくなることもあります。そのようなアプローチは、数サービスの移行であれば許容されるかもしれませんが、数百のサービスに対して行うと移行完了に途方もない時間が必要になります。そのため、移行範囲を広げすぎないというこの方針は、移行をスムーズに進めるために重要でした。また、この方針は後のステップでも効果を発揮します。 実装 Cloudflareの管理にはTerraformを採用し、公式から提供されている Terraformプロバイダー を使用しました。Terraformのリソースは、各サービスごとに個別に使用するのではなく、Terraformモジュールを作成し、そのモジュールに必要な機能を実装することで、今後のサービス移行時にも再利用できるようにしました。 Fastlyでは、自分たちが実装したロジックやFastlyが提供するロジックが最終的に一つのVCL(Varnish Configuration Language)としてまとめられます。移行の初期段階では、各VCLを個別に確認し、CloudflareのTerraformリソースへ手作業で実装していました。このため、少なくとも実装には30分以上かかっていました。 しかし、各サービスの移行が進むにつれて、VCLのロジックの中でも移行が必要なものと無視できるものがパターン化してきました。そこで移行の後半では、Go を用いて移行スクリプトを作成し、VCLを元にTerraformモジュールの設定を自動化できるようにしました。そして、自動で設定できなかったロジックは、移行検討が必要なものとして出力するようにしました。これにより、シンプルなサービスであれば、数分で実装が完了するようになりました。 テスト ほとんどのサービスには開発環境と本番環境があるため、まず開発環境でテストを行い、その後本番環境の移行を行います。しかし、トラフィックが多いサービスやミッションクリティカルな機能を提供するサービスの移行時には、事前に挙動をテストするためのコードを書きました。準備段階で述べたように、Fastlyと大きく挙動を変えていないため、Fastlyサービスの挙動を基準として比較するテストを書くことができました。これにより、自信を持ってトラフィックの移行を開始することができました。 トラフィックの移行 テストをどれだけ重ねても、本番のトラフィックを流す際には慎重に行う必要があります。特に、問題が発生した際には迅速にロールバックすることが求められます。 そこで私たちは、DNSレイヤーでこれらの要件を満たすアプローチを採用しました。メルカリでは Amazon Route 53 や Google Cloud DNS を使用しており、どちらもWeighted Routingをサポートしています。これにより、少しずつトラフィックをFastlyからCloudflareへ切り替えることができます。何か問題が発生した際には、CloudflareへのWeightを0%にするだけでロールバックが可能となり、手順もシンプルです。 移行中のモニタリングには Datadog を使用し、いくつかのメトリクスを確認しました。 まず、意図したトラフィック率になっているかを監視します。以下の画像は、FastlyとCloudflareのリクエスト比率から見たCloudflareのトラフィック率を示しています。 次に、以下の画像はCloudflareへの全リクエストから見た、2xxステータスコード以外のリクエスト比率を示しています。トラフィックの増加に伴い、これらの値が増えないかを確認することも重要な指標となります。 また、クライアント側から見たFastlyサービスとCloudflareの挙動には大きな変更がないはずなので、それぞれのキャッシュ率やリクエスト数や使用帯域の比較も行いました。 すべてのサービスの移行が完全に無障害で終わったわけではありませんが、これらのアプローチにより大規模な障害を回避し、問題が発生した際には影響範囲を最小限に抑えることができました。 CDN as a Service 移行の次のステップとして、Networkチームが集中管理していたCDNサービスの運用をセルフサービス化し、開発者自身が開発・運用できるようにする「CDN as a Service」を目指しています。 今回は、「CDN as a Service」に向けた2つの取り組みを紹介します。 CDN Kit 移行の際に触れたTerraformモジュールに私たちは「CDN Kit」という名前をつけています。開発者はCDN Kitを利用することで、1つ1つTerraformリソースを定義する必要がなく、自分が実現したいことを手軽に達成できます。また、私たちPlatformチームとしては、全体に提供したいベストプラクティスを各サービスごとに変更するのではなく、モジュール内に含めることで一箇所で提供できます。 例えば、オリジンへのアクセスをCloudflareを通じて行うというシンプルな要件であれば、開発者は以下のようにCDN Kitを使用するだけで済みます。 module "cdn_kit" { source = "..." company = "mercari" environment = "development" domain = "example.mercari.com" endpoints = { "@" = { backend = "example.com" } } } 開発者から見るとシンプルな定義ですが、CDN Kitを利用することで、さまざまなリソースが自動的に作成されます。以下はその一例です。 BigQuery へのログ送信 Cloudflareが提供するログをBigQueryに格納する際は、通常Cloud Functionsを使用します( ドキュメント )。しかし、これらを各サービスごとに作成するのは手間がかかるため、CDN Kit内で必要なリソースを自動的に作成しています。 Datadog モニターの作成 ドメインに応じた自動更新される SSL/TLS 証明書の発行 権限付与システム Cloudflareのダッシュボードは、インタラクティブにアクセス分析を行える強力なツールです。しかし、開発者にダッシュボードを公開するためには、以下の課題を解消する必要がありました。 退職者管理 権限付与の自動化 1つ目の退職者管理は、CloudflareのダッシュボードでSSOを有効にし、アイデンティティプロバイダーとしてOktaを利用することで解決しました( ドキュメント )。メルカリではOktaを使用しており、退職者の管理はITチームが担当しています。そのため、退職者処理の一環でOktaからアカウントが削除されると、Cloudflareのダッシュボードへのアクセスも自動的にできなくなります。このため、私たちNetworkチームは退職者管理を考慮する必要がありません。 2つ目の権限付与の自動化については、社内の既存のシステムと連携して動作する仕組みを実装しました。以下はその概要図です。 ※ Team Kitとは、開発者グループの管理を行うためのTerraformモジュールです。 開発者チームを管理するTerraformモジュールであるTeam Kit、およびCloudflareを管理するCDN Kitは、GitHubのリポジトリで管理されています。これらのモジュールの更新を自動的に検知するGitHub Actions Workflow を作成しました。このWorkflowは、更新を検知すると、以下に示すような権限管理用のマニフェストファイルを生成し、リポジトリにコミットします。 account_id: [Cloudflare Account ID] zone_id: [Cloudflare Zone ID] zone_name: [Cloudflare Zone Name] teams: - team_id: [ID of Team Kit] roles: - Domain Administrator Read Only users: - email: [email address] roles: - Domain Administrator Read Only 次にマニフェストファイルの変更を検知して、別のGitHub Actions Workflowが動作し、マニフェストをもとにCloudflareの各権限を設定します。 Team KitとCDN Kitの変更を検知して動作するGitHub Actions Workflowで、Cloudflareの権限を直接変更しない理由は、マニフェストファイルを保持することで宣言的にCloudflare の権限を管理できるようにするためです。これにより、例えば手動で権限が変更された場合であっても、いつでもマニフェストに基づいて正しい状態に戻すことが可能となります。 この権限付与システムによって、開発者はNetworkチームに権限を依頼する必要なくダッシュボードを見ることができるようになりました。すでに、開発者自らがダッシュボード上で問題を発見し、解決する事例も観測されており、「CDN as a Service」への取り組みがすでに効果を発揮していることを嬉しく思います。 おわりに この記事では、CDNプロバイダーの移行におけるアプローチを紹介し、その後のステップとして社内向けに提供する「CDN as a Service」の取り組みとしてCDN KitというTerraformモジュール、権限付与システムについて説明しました。
こんにちは。メルコインでバックエンドエンジニアをしているiwataです。 この記事は、 Merpay & Mercoin Advent Calendar 2024 の記事です。 tl;dr バッチ処理のSLO定義って難しい… そんな悩みを解決するSLO定義方法 BigQueryとSpanner External Datasetを活用した具体的な監視方法の紹介 メルコインの安定稼働を支える技術 最近ではビットコインやイーサリアムを 積み立てる機能 を開発していました。 積立の開発では積立日にバッチ(以下、積立バッチ)を起動することでビットコインなどの仮想通貨の購入処理を実行するようにしました。 積立バッチはお客さまの資産をあつかうとても重要なバッチです。設定された積立日に確実に処理を実行し終える必要があります。このようにシステムの信頼性を考える上で広く認識されている考え方がSLOです。それではバッチ処理におけるSLO定義とはなんでしょうか? バッチ処理においてSLOを定義することの難しさ 一般的にSLOで用いられるSLIとしてはAvailability(可用性)とLatencyがあります。 前者はエラーレートの逆数として算出可能であり、APIがエラーをどのくらいの割り合いで返しているかで監視することが多いです。 後者はAPIの応答時間を99パーセンタイルなどの統計値を基に監視します。 いずれの指標もAPIであればその定義も分かりやすく、監視方法も確立されています。 ではバッチ処理についてあてはめるとどうでしょうか? バッチのAvailabilityといった場合に、実行時の終了コードだけをみればよいのでしょうか? それともバッチで一括処理するデータひとつひとつのエラーレートをみればよいのでしょうか? またLatencyについてはバッチ処理の実行時間だけをみればよいのでしょうか? それともこちらもデータひとつひとつの処理時間をみればよいのでしょうか? 一方でSLOの設計においてはCUJ(Critical User Journey)に代表されるように、ユーザー視点で考えることが大切です。バッチ処理によってお客さまは何を期待しているのでしょうか。 これらのことを考える上で以下の資料がとても参考になりました。 バッチ処理のSLOをどう設計するか – Speaker Deck スライドにあるようにバッチ処理で担保したい信頼性をデータの「納期」(デッドライン)と「品質」という観点で整理しました。 積立においてお客さまが期待することは「積立日に積立処理が完了していること」となるはずです。 積立日=デッドライン 積立処理完了=データ品質 すなわち「積立日の23時59分59秒までにすべての注文処理が完了(残高不足などによる失敗も含む)」をSLOとして定義しました。積立においてはバッチ実行時にタイムアウトなど一時的なエラーが発生した場合に別プロセスで自動リトライする仕組みもあったりしますが、この定義を用いれば別プロセスであってもカバーできます。お客さまからみれば例え何回リトライしていようが、その日中に処理が完了していれば問題ないとみなせるからです。 以降では具体的な監視方法を紹介します。 BigQueryを使った監視 メルコインではデータベースとしてCloud Spanner(以下、Spanner)を使っています。 社内では分析用途で使うために、SpannerのデータをBigQueryに定期的に同期するパイプラインが用意されています。Spannerへの負荷を考慮しなくて済むように、監視クエリはBigQueryに対して実行します。 またBigQueryに対して定期的にクエリを実行し、その結果をDatadogから監視する仕組みも構築されているためこれを用いて実現しました。 詳細は省きますが下図のような仕組みが構築されています。 ロゴ出典: Slack, Datadog, GitHub, Goolge Cloud 簡単に説明すると、事前に定義しておいたクエリをBigQuery上で定期的に実行し、その結果をカステムメトリクスとしてDatadogに送信しています。クエリ実行した結果のレコード数がカスタムメトリクスとして送信されるので、Datadog上でメトリクスモニターを定義して監視できます。 例えば積立であれば未処理のレコードを返すクエリを定義し、デッドラインである23時59分59秒以降にカスタムメトリクスが1以上であればSLO違反に気づける、という具合です。実際には違反前に気づきたいので十分に余裕をもった時間で気づけるよう監視しています。 Spanner External Datasetの利用 単純な用途でこれまで紹介したツール郡を用いることで監視できていました。ところがSpannerに直接クエリを実行せず、BigQueryを使うことで以下のような問題があります。 SpannerからBigQueryへの同期がリアルタイムでない 同期処理がテーブル単位で実行される SpannerからBigQueryへの同期がリアルタイムでない 同期用のパイプラインは1時間に一回実行されており、リアルタイムにデータが同期されているわけではありません。これによって検知に数時間かかってしまいます。このタイムラグを許容できないケースも考えられます。 同期処理がテーブル単位で実行される 同期用のパイプラインはテーブル単位で設定し実行されます。したがって、任意のタイミングでBigQuery上の複数のテーブル間には整合性が担保されていません。 JOIN した結果を用いて監視をおこないたい場合にはこれは致命的です。 Spanner External Dataset これらの課題を解決するために一部のクエリでは Spanner External Dataset を使いました。External Datasetを使うことで以下のようなメリットがあります。 BigQueryへの同期は必要なく、Spannerに直接クエリできるのでタイムラグとテーブル間の不整合がなくなる Data Boost がつねに有効なのでSpannerへの負荷を考えなくてもよい また同じような機能として Spanner Federated Queries がありますが、 EXTERNAL_QUERY関数 が読みづらいなどの理由でExternal Datasetを採用しました。 External Datasetの利用方法 最後にTerraformを使った利用方法を載せておきます。 google_bigquery_dataset resource "google_bigquery_dataset" "spanner_external" { provider = google-beta project = {your-gcp_project_id} dataset_id = "spanner_external" location = "US" external_dataset_reference { external_source = "google-cloudspanner:/projects/{your-gcp_project_id}/instances/{your-spanner.google_spanner_instance_name}/databases/{your-database-name}" connection = "" } } 設定値は適宜置き換えてもらえばよいですが、 connection だけ オフィシャルドキュメント にあるように空文字で設定する必要があるので注意が必要です。 google_bigquery_dataset_access resource "google_bigquery_dataset_access" "access_spanner_external" { project = {your-gcp_project_id} dataset_id = google_bigquery_dataset.spanner_external.dataset_id role = "roles/bigquery.dataViewer" user_by_email = {your-google_service_account.email} } クエリを実行するService Accountに対して上記で作成したExternal Datasetへのアクセス権を付与します。 google_spanner_database_iam_member resource "google_spanner_database_iam_member" "monitor_can_read_database_with_data_boost" { project = {your-gcp_project_id} instance = {your-spanner.google_spanner_instance_name} database = {your-database-name} role = "roles/spanner.databaseReaderWithDataBoost" member = "serviceAccount:{your-google_service_account.email}" } External Dataset経由でSpannerにもアクセスするので対象のデータベースに対しての spanner.databaseReaderWithDataBoost ロールを付与します。ちなみにこのIAMロールは最近追加されました。Data Boostを使うにはこれまで別途カスタムロールの作成が必要だったり面倒だったのですが、いまではこのロールを割り当てるだけでよくなりました。 まとめ バッチのSLO定義について書きました。 バッチでは「デッドライン」と「データ品質」を基にSLOを定義することでうまく運用できています。 データ品質を監視する方法としてBigQueryに対して定期的にクエリを実行する手法を採用しています。BigQueryとSpannerとの連携についてはExternal Datasetが提供されるようになったことで課題が解消されています。 この記事が読んでいただいた方の運用の手助けになれば幸いです。 次の記事は masamichiさんです。引き続きお楽しみください。
こんにちは。メルカリハロのSRE TLの @naka です。 この記事は、 連載:メルカリ ハロ 開発の裏側 – Flutterと支える技術 –の7回目と、 Mercari Advent Calendar 2024 の18日目の記事です。 今回は、「メルカリ ハロ」のFlutter開発をSREとの関わりという観点から紹介します。日常の業務上はFlutter開発とSREの業務はそこまで密接な関わりはありませんが、Flutter開発の裏側ではSREがそれを支える場面もいくつかあり、そこからSREエンジニアとしての学びもありました。 開発・テスト環境改善やDeveloper Experience (DX) の向上に取り組んでいる具体的な内容を含めて紹介します。 概要 メルカリハロのFlutter開発を行っているメンバーは、高いオーナーシップを持ち、自ら理想的な開発環境を構築している理想的なチームです。普段はSREと直接関わることは少ないものの、今回はSREとしてFlutter開発に関連した具体的なサポート事例を紹介し、チーム間のコミュニケーションやSREとしての学びについても触れていきます。 QAとSRE: E2Eツール選定や自動化設定 E2Eテストの重要性 信頼性の高いアプリを迅速にリリースするためには、E2E(エンドツーエンド)テストの導入と自動化が欠かせません。メルカリハロでは、QAチームとの連携を通じて、E2Eテストのツール選定や自動化設定においてSREと連携する機会がありました。 ツール選定のサポート E2Eテストのツール選定時には、セキュリティ観点、メルカリ内で提供されているプラットフォームとの相性、ツールのコスト、ツールの特性などを総合的にみて決定する必要があります。QAメンバーが率先してツール選定をリードしていましたが、立ち上げ当初から環境構築をSREが担当してきたので、ツール選定の検討にも参加しました。 SREチームは、要件を満たすために、QAチームが適切なツールの評価と選定をできるよう支援しました。具体的には、以下の点を重視しました: セキュリティ: テストツールの仕様を読み、メルカリのセキュリティ基準を満たしているか 統合性: 既存の開発環境やプラットフォームとスムーズに統合できること。PoCを実施してから本導入する場合の段階的導入の具体的なステップ。 これらの情報をQAチームが統合して最終的なツール選定をスムーズに行うことが出来ました。 自動化設定のサポート ツール選定後、E2Eテストの自動化設定を行う際には、CI/CDパイプラインとの連携やNetworkの設定調整が必要となります。SREチームとしても、以下のサポートを提供しました。 E2Eテストが実行されるCI/CD環境からAPIサーバへアクセスする必要がありますが、ローカルでは動くけどCI/CD環境では動かないなどのケースでサポート依頼が来ました。 SREチームは、QAチームとともにE2Eテストの実行結果の確認や、E2Eテストのシナリオを確認してデバッグしたり、ネットワークの疎通環境の調整などを行ったりして、最終的に自動化が可能な状態になるよう支援しました。 これらの取り組みにより、E2Eテストの自動化が実現し、開発チームは信頼性の高いコードを迅速にリリースできるようになりました。 FlutterチームとSRE: CI/CD改善のSlack Bot メルカリハロでは、各チームのメンバーが自律してCI/CDのPipelineを整備しています。Flutterチームも、CI/CDの課題に積極的に取り組んでおり、リリース時のSlack botとの連携なども行いDX向上に努めてきました。 Slack BotによるCI/CD改善 最近、FlutterチームからGitHub Actionsの失敗時にSlack上から簡単にリトライ(Retry)できるようにしたいという要望がありました。これに応えるために、SREチームはFlutterメンバーとともに以下の取り組みを行いました。 Slack Appの設計とBootstrapingのサポート やりたいこととツールを相談して、実現可能な設計を一緒に行いました。 Slack AppからGitHub APIを使うためには、Security チームが管理・提供しているToken Serverの仕組みを使って、Installation Access Tokenを取得する必要があります。 第一回の Google CloudからGitHub PATと秘密鍵をなくす – Token ServerのGoogle Cloudへの拡張 の中で詳細が紹介されています。 Installation Access Tokenは、必要なScopeを事前に定義しておく必要があり、必要最低限の権限だけを付与することが可能です。また、今回はDX向上用のSlack Appなので、簡単にDeployができるようにCloud Runで構築することにしました。 このアプリが完成すると、失敗したGitHubActionsの再実行を直接Slack上から実行できるようになり、今まで必要だったGitHubのUIを開くひと手間をなくすことができDXの向上に貢献することができます。 FlutterメンバーはCloud Run自体の経験はなかったので、Cloud Runの初期設定として、SREで空のサービスを立ち上げて、Deployする方法を伝え、スピーディに開発に取り組める環境を構築しました。 このSlack Appは現在絶賛開発中ではありますが、すでにSREのサポートが不要な状態にあるので、あとはリリースされる日が来るのが楽しみです。 コミュニケーションといつでもサポートできる体制構築 メルカリ ハロの開発プロジェクトのなかで、SREチームとして、今まで業務上深く関わることが少なかったチームに対しても、間接的にFlutter開発をサポートする機会が生まれました。 このサポート体制がうまく機能した裏側には、FlutterチームとSREチームの日頃からの交流があったことも大きな理由の背景だったと思います。 SREチームは、Flutterチームとの信頼関係を築くために、定期的な業務関連の情報共有に加えて、懇親会への参加やチームビルディングランチの開催など日常的な交流を積極的に行っています。これにより、チームを跨いだメンバーとカジュアルにコミュニケーションが取りやすくなり、信頼関係を構築することができました。この信頼関係があることで、いざ何か問題が発生した際に、Flutterチームが気軽にSREチームに相談できる雰囲気が作れているのではと思います。 SREチームは、普段の業務のなかでの関わりが深いチームから、日常業務ではそこまで関わりの多くないチームまで幅広くサポートできるように、常に気軽に相談・質問しやすい雰囲気づくりを心がけています。 まとめ 今回は、SREとしてFlutter開発の裏側でのサポート事例として、QAチームと連携してE2Eテスト環境構築したことや、FlutterメンバーとともにDX向上に取り組んだことを紹介しました。 SREの活躍は表には出にくいながらも、裏側での支援の積み重ねによってプロダクトの品質や開発効率に大きく貢献できる場面はたくさんあると感じています。 だからこそ、普段業務上のやり取りが少ない他チームのメンバーとも積極的に情報交換しつつ、何か自分たちが貢献できる業務がないかを貪欲に探していくような姿勢が大切だと考えています。 これからも、他チームからさらに信頼されるSREエンジニアになるために、技術面・組織面の両面から一層高みを目指していきたいと思っています。 引き続き メルカリ ハロ 開発の裏側 – Flutterと支える技術 – シリーズを通じて、私たちの技術的知見や経験を共有していきますので、どうぞご期待ください。また、 Mercari Advent Calendar 2024 の他の記事もぜひチェックしてみてください。それでは、次回の記事でお会いしましょう!
こんにちは。メルペイ MoMの @abcdefuji です。 この記事は、 Merpay & Mercoin Advent Calendar 2024 の記事です。 はじめに 私たちPaymentPlatformは、メルカリグループ内のさまざまな価値循環、すなわち決済、返金、送金、入出金、精算などを実現しているチームです。現在、以下の図のように多様なサービスを支えています。 今回、PaymentPlatformがどのようにして各サービス毎の取引を識別し、会計システムと連携しているかについてお話ししたいと思います。また、本記事を作成するにあたり、AccountingチームのKENTYさんに多大なサポートをいただきました。 概要 – 会計ついて そもそも会計とは何のために行うのかを簡単に説明したいと思います。今回は財務会計に関して説明します。 財務会計は、企業の財務状況や経営成績を外部の利害関係者(株主、投資家、債権者、規制当局など)に報告するための会計手法です。主に、財務諸表を作成し、一定の会計基準に基づいてデータを整理・報告します。財務会計の主な成果物は、以下の3つの財務諸表です。 貸借対照表(バランスシート):特定の時点における企業の資産、負債、資本の構成を示すものです。資産は企業が所有するもの、負債は企業が負っている債務、資本は自己資本を表します。 損益計算書(インカムステートメント):一定期間における企業の収益と費用を記録し、最終的な利益または損失を示します。売上高、営業利益、税引前利益、当期純利益などが含まれます。 キャッシュフロー計算書:企業の現金の流出入を記録するもので、営業活動、投資活動、財務活動の各セクションに分かれています。これにより、現金の流れが明確になります。 つまり、システムからデータを集約し上記財務三表を作成するプロセスが必要になります。 しかしながら、データを集約させるだけでは実現することはできません。財務三表を実現するためにはデータを仕訳の形式に変換する必要があります。 複式簿記と仕訳 仕訳の形式に変換するために複式簿記の知識が必要になります。複式簿記とは、すべての取引を二重に記録する方法で、資産と負債、収益と費用の関係を明確にします。これにより、取引の正確性が高まり、財務諸表が信頼性を持つようになります。 仕訳 仕訳は、企業の日々の取引を会計上の勘定科目に振り分ける作業です。取引が行われる際には、「借方」と「貸方」に分けて記録します。 借方(Dr.):資産の増加、負債の減少、費用の発生を記録します。 貸方(Cr.):資産の減少、負債の増加、収益の発生を記録します。 例えば、商品を10,000円で販売した場合の仕分けは以下のようになります。 借方:現金 10,000円(資産が増加) 貸方:売上 10,000円(収益が増加) このように、各取引について対応する借方と貸方を設定することで、常に帳簿が総合的にバランスを保たれる仕組みが整います。これが複式簿記の基本的な考え方です。 勘定科目 企業や組織が財務活動を記録するために使用するカテゴリや項目のことを指します。これらは通常、貸借対照表や損益計算書といった財務諸表に表示されます。勘定科目は、それぞれの取引やイベントを記録し分類するための基本単位であり、以下のようなものが含まれます。 資産:現金、預金、受取手形、売掛金、在庫など 負債:買掛金、借入金、支払手形、未払費用など 資本(純資産):資本金、資本剰余金、利益剰余金など 収益:売上、受取利息など 費用:仕入原価、給料、広告宣伝費、租税公課など 上記勘定科目に従いデータを会計システムに蓄積されることが望まれます。 会計システムとPaymentPlatformの接続方法について 実際のシステムと会計システムの連携について具体的に説明します。以下のような決済シーケンスを想定します。 お客さまが購入処理を実行します。 次に、メルペイが決済リクエストを処理し、お客さまの残高を減少させます。 同時に、加盟店の売上が増加します。 最後に、PaymentPlatformから会計システムにデータを連携します。 上記の流れで、決済データが会計システムに連携されています。このように、決済データを会計システムに連携しています。 単一のユースケースから複数のユースケースへ では、PaymentPlatformが支えるユースケースが拡張し、さまざまなサービスで同じAPIが利用される状況になった場合、どのように取引を分類し、会計の観点から仕訳を行うことができるでしょうか。たとえば、メルカリグループ内の事業Aのサービスと事業Bのサービスから同じ決済APIが利用された場合であっても、商流やユーザーストーリーが異なる場合には、会計の観点からどのように取引を特定すべきでしょうか。 メルペイでは、この問題を解決する手段として仕訳IDを用意しています。 お客さまがメルペイでコード決済を実行します。 メルペイは決済リクエストを仕訳IDとともに処理し、お客さまの残高を減少させます。 加盟店の売上は増加させる。 PaymentPlatformから会計システムに対して仕訳IDを用いてデータを連携します。 このように、仕訳IDを上位から下位まで一貫して伝搬させ、会計システムにデータを届けています。これにより、複数のユースケースにおいても、会計の観点から決済データの正確な識別が可能となります。 開発プロセス 私たちの開発プロセスは、会計との密接な連携を重視しています。 開発の初期段階で事業の商流やユーザーストーリーを確認し、そこからお金の動きがどのように発生するかを分析します。この段階で、エンジニア、PdM(プロダクトマネージャー)、および経理の三者間で共通の認識を確立します。 その後、経理によって適切な仕訳IDの設計が行われ、発行された仕訳IDがエンジニアに提供されます。 最後に、エンジニアがその仕訳IDを会計システムまで正確に伝搬させるという流れになっています。 このプロセスでは、経理とエンジニアがお互いに歩み寄ることで、システム観点と会計観点の両方において最適な解決策を目指しています。 実現できた事 複雑なクエリからの解放 仕訳IDを活用することで、複雑な会計クエリからの脱却を実現しました。データを単一のシステムに集約し、仕訳単位での集計が容易になったため、関連システムからの複雑なクエリに頼ることなく、効率的な集計が可能になりました。また、会計システムにデータを集約してイミュータブルな状態で管理することで、冪等性が担保され、後日同じ手法で集計を行っても一貫した結果を得ることができます。 密な開発体制 体制としては、エンジニアが会計ドメインを理解しようとし、経理がエンジニアリングを理解しようとする歩み寄りの姿勢が育まれています。新しいユースケースが登場すると、必ずエンジニアから会計上の整理が正確であるかの確認が行われ、会計ドメインを意識しながら開発が進められています。 コスト削減 さらに、会計システム連携の共通化により、プロダクト開発コストの削減も達成しています。PaymentPlatformを利用することで、仕訳IDを介して会計システムまで一貫して連携できるため、新規事業や既存システムの拡張において、会計システムとの連携をプラットフォームとして取り込むことが可能です。 課題 / 今後に関して 仕訳IDの管理コストに関して、メルカリグループの事業拡大に伴い、PaymentPlatformがサポートするユースケースが大幅に増加しました。そのため、仕訳IDの増加に伴い現行の設計方針を維持できるか、あるいは将来的に維持が難しくなる可能性もあるため、継続的な検討が必要です。システム面でも、増加した仕訳IDに対してアドホックに対応したケースが負債として残っており、これを解消していくことが求められています。 開発プロセスにおいて、現状では会計要件を無視して開発を進めることは難しい状況です。さらに迅速な開発体験を実現するために、会計要件を気にせずに済む開発手法を模索しています。例えば、新規事業や既存の拡張から会計要件に落とし込み、仕訳を決定するまでのプロセスを簡略化したり、ある程度サービス側で整理した要件を出すフレームワークがあると効果的です。 オンボーディングのコストが高い点については、開発プロセスで会計ドメインに関するコミュニケーションが避けられません。特に新しいメンバーにとっては、会計システムと仕訳IDの設計を理解するまでのハードルが高い状況です。このため、キャッチアップのためのドキュメントを整備し、開発プロセスの簡略化を目指しています。 最後に、すべてのケースがPlatformとして吸収できるわけではありません。PaymentPlatformがカバーできるケースとカバーできないケースが存在し、事業やサービスによっては特殊なユースケースがあり、PaymentPlatformを経由せずに会計システムに連携している場合もあります。今後、PaymentPlatformのガバナンスを整理し、どこまでを吸収し、どこを対象外とするかを明確に管理する必要があります。 以上のような課題がある一方で、私たちはさらなる便利なPaymentPlatformの実現を目指し、日々精進を続けていきます。最後までお読みいただきありがとうございました。 参考資料 https://engineering.mercari.com/blog/entry/2019-09-19-113909/ https://careers.mercari.com/mercan/articles/40838/ https://engineering.mercari.com/blog/entry/2019-06-07-155849/ 次の記事は iwataさんです。引き続きお楽しみください。
この記事は Merpay & Mercoin Advent Calendar 2024 の記事です。 メルペイの Payment Core チームでバックエンドエンジニアをしている komatsu です。 普段はメルカリグループのさまざまなプロダクトに共通した決済機能を提供するための決済基盤の開発・運用をしています。 この記事では、私たちが直近開発した新しい決済手段であり、今年リリースされたスキマバイトサービス「メルカリ ハロ」や現在試験運用中のサービス (以降サービス A とします) に利用されている、“事業者請求払い” について紹介します。 事業者請求払いとは メルカリグループが提供するプロダクトは個人のお客さまに支えられていますが、同時にメルカリShops やメルペイなど、多くのパートナー企業 (加盟店、事業者) によっても支えられています。 そのため、個人のお客さまに提供する残高やあと払いといった決済スキーム以外にも、加盟店との資金の流れをシステムで管理するさまざまなユースケースが存在します。 メルカリとパートナー企業間における資金の流れは大きく分けて 2 種類あります。 メルカリ -> パートナー企業 メルペイ加盟店の売上から手数料などを差し引いた金額を、その加盟店に振り込むための資金の動きです。内部的には、締め日に応じて売上金の精算をし、メルカリが各加盟店に銀行振込をすることで実現されています。 パートナー企業 -> メルカリ 加盟店の売上をメルカリに移動することで、toB のサービス提供する場合の資金の動きです。例えばメルカリ ハロでは、お客さまが求人に応募をし、お仕事をすることでパートナー企業から給与が支払われます。ただし、パートナー企業から直接お客さまに支払われるわけではなく、メルカリ ハロを通してお客さまに給与の支払いが行われます。これはアルバイト完了後にすぐ給料が支払われる体験を提供するためや、パートナー企業から手数料を受け取ることを実現するためのものです。他にも試験運用中のサービス A では、加盟店がメルカリに代金を支払うことで、メルカリが加盟店に対してサービスを提供します。 このような、パートナー企業からメルカリに資金が移動する場合に利用されるのが事業者請求払いです。 事業者請求払いでは、次のような手順でサービスの提供が行われます。 メルカリはメルカリ ハロのようなプロダクトを、Payment Platform はメルカリグループにおけるあらゆる資金の移動に利用されている決済基盤を、加盟店は各プロダクトがサービスを提供しているパートナー企業を指します。 加盟店の与信審査依頼 事業者請求払いはサービスを提供してからその金額に応じた支払いを加盟店に請求するため、クレジットカードのようなあと払いの決済スキームです。 そのため、実際にサービスの提供を行う前に、貸し倒れリスクなどを考慮して各加盟店が支払い可能な金額分のサービスを提供する必要があります [1](現在加盟店の与信審査が必要な場合、私たちのチームから外部の会社に API 経由で依頼して実現しています)。 審査結果に基づくサービスの提供 与信審査が完了したら、サービスを提供できる状態になります。 メルカリ ハロであれば与信枠内で求人募集ができるようになります。 決済基盤の観点では、このタイミングで利用分を与信総額から都度減らし、利用分は未回収金の債権として管理します。 請求 月末などプロダクトが定める締め日をもって請求金額が計算され、加盟店にメルカリに返済するための請求書が送付されます。 請求書には支払う金額のほか、支払先の銀行口座やインボイス制度に基づく明細などが記載されます。 請求金額の支払い 加盟店は請求書に記載された金額を、支払期限までに支払います。 決済基盤は入金の通知を受け、債権の消し込みを行います。 ではなぜこのようなスキームが必要なのでしょうか? 事業者請求払いを利用しない最も簡単な方法は、サービス利用時に必要な金額をメルカリに入金することです。 ですが、これには事業者請求払いで解決できる、いくつかの問題点があります。 支払いが同月中に何度も発生するため、加盟店もメルカリも振込に関する管理が複雑化する。 振込入金に関するオペレーションは手動で行われることが多く、煩雑になります。 事業者請求払いでは月に 1 回程度の作業になるため、加盟店としてもメルカリとしても作業が簡易化されます。 加盟店のキャッシュフローが悪化する サービス利用時に入金する場合、加盟店の売上が立つ前に支払いを行うことになり、手持ちのキャッシュを利用する必要があります。 事業者請求払いではサービス提供の翌月以降に請求をすることで、売上やサービス利用による利益を返済に利用することができます。 適切な金額の請求ができないことがある 例えばメルカリ ハロの場合、残業が発生する場合など、サービス提供時 (= アルバイト募集の掲載時) と実際に加盟店が支払う金額には差分が生じることがあるため、前払いの形式では正しい金額を受け取ることが不可能です。 事業者請求払いではサービス提供後に請求が行われるため、実際のサービス利用料を算出したうえで正しい金額を請求することができます。 決済基盤の API 設計 事業者請求払いの概要を説明したところで、私たち Payment Core チームが既存の決済基盤にこの機能を追加するときの設計について紹介します。 パートナー間の決済を表現する PartnerTransfer 決済基盤マイクロサービスである Payment Service はさまざまな資金の動きや決済手段をサポートする API を持っています。 例えば、メルカリで商品を売買する場合、Escrow と呼ばれる API を利用して、買い手の残高やあと払いの枠、クレジットカードといった決済手段を消費し、その分のメルペイ残高を売り手に付与したり、手数料をメルカリ自身の売上として計上します。 他にもコード決済等で購入者から加盟店に資金を移動するための Charge や、キャンペーンのポイント付与等でメルカリからお客さまにポイントを扶養するための Transfer といった API があります。 Payment Service を利用するマイクロサービス、つまりプロダクト側のマイクロサービスはこれらの API を必要に応じて組み合わせながら、さまざまな決済体験をお客さまに提供します。 そして、さまざまある API の中で、パートナー間での資金の流れを表現する PartnerTransfer API があります。 ここでいうパートナーはコード決済を導入している加盟店や、メルカリShops に出店している加盟店、メルカリ ハロに求人を掲載している事業者などが含まれます。 それと同時に、メルカリグループの売上を管理するために、メルカリやメルペイ自身も含まれます。 既存の PartnerTransfer API のユースケースには以下のようなものがあります。 メルペイコード決済における加盟店の売上を加算する 決済には必ず原資があるため、PartnerTransfer 内部では、メルペイ自身の売上金を減らし、加盟店の売上金に追加する、といった処理が行われます。ここで、売上金に加算ということは、決済基盤が持っている加盟店残高管理用のマイクロサービスである Balance Service 上で加盟店の売上金 2 が増加することであり、実際に加盟店に振り込まれているわけではない状態です。プロダクトによって定められている精算のタイミングで売上金を加盟店の銀行口座に振り込むことで、現実世界で金銭が移動します。 加盟店の売上からメルカリShops の手数料を差し引く メルカリShops では売り手である加盟店から手数料を徴収するビジネスモデルです。そのため、手数料分を加盟店の売上からメルカリに移動する処理が必要となり、PartnerTransfer によって実行されます。 事業者請求払いは原資が加盟店の売上ではなく与信枠という違いはありますが、資金の流れは加盟店からメルカリ自身という点で、PartnerTransfer が想定するユースケースに当てはまります。 そのため、PartnerTransfer がサポートする 1 つの決済手段として事業者請求払いを組み込むことにしました。 与信管理や精算を柔軟にする設計 メルカリグループにはさまざまなプロダクトがあり、それぞれのプロダクトで千差万別の要件があります。 決済基盤チームはプロダクトの要望を受け入れつつも、なるべく一般化し、ロバストな設計を保つ必要があります。 グループ内のプロダクトごとに、それぞれが抱える加盟店の特徴や、決済基盤として求められるものが異なるケースがあります。例えば、メルカリ ハロではより多くの事業者がアルバイトを募集できるようにする一方で、サービス A のように与信情報がすでに分かっており、社会的な信用のある特定の事業者に対してのみ機能を提供する場合もあります。 言い換えれば、前者では大量の事業者に対して適切な与信審査をする必要があり、後者は審査をせずに大きな金額を与信として与えることができます。 そのため、私たち決済基盤では 2 通りの事業者請求払いのフローを構築しました。 1 つ目が外部の与信審査や請求を行う企業をバックエンドとして利用するパターンです。 メルカリではメルペイのあと払いなどで利用されている個人向けの与信管理の仕組みはありますが、パートナー向けのものはありませんでした。 そのため、企業与信の管理を行っている企業の API に別のマイクロサービス [3](Payment Service の責務は決済のトランザクション管理や価値交換のためのインターフェースの提供であり、外部サービスとの接続は、Payment Provider という別のマイクロサービスを開発して責務の分割をしています。Payment Service が Payment Provider を gRPC で呼び、Payment Provider が外部サービスを HTTP などのプロトコルで呼ぶようなフローになります。) を介して繋ぎ込みを行いました。 2 つ目は外部サービスを利用せず、メルペイが持つ既存の精算管理の仕組みを利用したパターンです。 こちらは企業与信を審査することはできないかわりに、精算や請求、入金確認までをすべてメルペイ内のシステムで完結して提供します。 これらの決済基盤の裏側の仕組みはユースケースに応じて選択できるものなので、API のインターフェースはなるべく一般化し、パラメータ 1 つで切り替えられる仕組みが必要でした。 そのため、Payment Service の RPC は以下のようになります。 // payment.proto service PaymentService { rpc CreatePartnerTransfer(CreatePartnerTransferRequest) returns (CreatePartnerTransferResponse) {} rpc CapturePartnerTransfer(CapturePartnerTransferRequest) returns (CapturePartnerTransferResponse) {} rpc CancelPartnerTransfer(CancelPartnerTransferRequest) returns (CancelPartnerTransferResponse) {} } message CreatePartnerTransferRequest { // 資金の移動元のパートナー uint64 from_partner_id = 1; // 資金の移動先のパートナー uint64 to_partner_id = 2; // from_partner_id が利用する決済手段 PaymentMethod payment_method = 3; } message CapturePartnerTransferRequest { string partner_trasnfer_id = 1; } message CancelPartnerTransferRequest { string partner_trasnfer_id = 1; } message PaymentMethod { enum Type { // Balance Service が管理するパートナーの売上金を利用する決済 PARTNER_SALES = 1; // 事業者請求払いの与信を利用する決済 PARTNER_INVOICE = 2; } PaymentMethod.Type type = 1; oneof details { PaymentMethodPartnerSales partner_sales = 1; PaymentMethodPartnerInvoice partner_invoice = 2; } } message PaymentMethodPartnerSales { uint64 amount = 1; } message PaymentMethodPartnerInvoice { // 請求明細の項目を表現する message message Detail { enum TaxType { UNKNOWN = 0; EIGHT_PERCENT = 1; TEN_PERCENT = 2; ANY = 3; } // 項目の名称 string name = 1; // 単価 int64 price = 2; // 量 int64 quantity = 3; // 税区分 TaxType tax_type = 4; } enum InvoicePaymentProvider { INVOICE_PAYMENT_PROVIDER_UNKNOWN = 0; INVOICE_PAYMENT_PROVIDER_XXX = 1; // 外部サービスを利用した事業者請求払い (XXX は仮の名前) INVOICE_PAYMENT_PROVIDER_INHOUSE = 2; // 内製の事業者請求払い } InvoicePaymentProvider invoice_payment_provider = 1; repeated Detail details = 2; } ここで、CreatePartnerTransfer, CapturePartnerTransfer, CancelPartnerTransfer はそれぞれ PartnerTransfer における決済のオーソリ、キャプチャ、キャンセルを表現します。 CreatePartnerTransfer は PaymentMethod を引数に取り、パートナーの残高を消費する決済手段か、事業者請求払いによる与信枠を消費する決済手段化かを選択できます。 事業者請求払いの場合、 PaymentMethodPartnerInvoice によって各明細の単価や量、インボイス制度に対応する税区分などを入力できます。 InvoicePaymentProvider によって、バックエンドで利用する事業者請求払いのプロバイダ (外部サービスなのか、メルペイ内製のものなのか) を切り替えることができます。 これによって、Payment Service を利用するプロダクト側はバックエンドのシステムをあまり知らない状態で、ユースケースに応じてパラメータを切り替えるだけで事業者請求払いの機能を一貫して利用することができます [4](実際には各バックエンドに依存する API なども存在しますが、決済のタイミングではこのフィールド以外を意識する必要がありません)。 決済の整合性担保 さまざまなマイクロサービスや外部サービスを跨いだ決済スキームである以上、整合性の担保が重要になります。 特に外部サービスとの突合は重要であり、プロダクトローンチ時から厳密な仕組みづくりが必要でした。 決済ごとの与信審査があるため、決済ステータスのライフサイクルは以下のようになります。 外部サービスとは毎日一度、前日のすべての取引の状態が CSV ファイルとして SFTP サーバ経由で送られてきます。 外部サービスに接続しているマイクロサービスである Payment Provider では、その時刻になったら CSV ファイルを取得し、決済基盤が持っている決済ステータスと差分がないかを突合します。 一見簡単にみえるこの処理において難しい点は、決済基盤が持っているデータは最新のものであるのに対し、CSV ファイルに含まれるのはあくまで前日終了時点でのステータスであるという点です。 例えば、 12/19 23:55 CreatePartnerTransfer によって外部サービスを利用した決済が発生し、オーソリが完了 12/20 00:05 CapturePartnerTransfer によって決済のキャプチャが完了 12/20 06:00 12/19 分の取引データが連携 (status: authorized) 12/20 07:00 12/19 分の突合処理を実行 このような時系列の場合、CSV に含まれるデータでは最後のステータスは authorized であるのに対し、私たちの決済基盤では captured になります。 これらを考慮するために、上記の状態遷移をコードで表現し、ステータスに差分が会ったとしても移り得るものなのかを判断し、柔軟に突合する仕組みを作りました。 一方で、1 日以上経ってもステータスが同じにならない場合、それは不整合として検知する必要があります。 例えば、 12/19 23:55 CreatePartnerTransfer によって外部サービスを利用した決済が発生し、オーソリが完了 12/20 00:05 CapturePartnerTransfer によって決済のキャプチャが完了 12/20 06:00 12/19 分の取引データが連携 (status: authorized) 12/20 07:00 12/19 分の突合処理を実行 12/21 06:00 12/20 分の取引データが連携 (status: authorized) 12/21 07:00 12/20 分の突合処理を実行 この例の場合、5 では取引データは captured になっていることを期待していますが authorized のままになっています。 状態遷移のみを考慮した場合では authorized から captured への遷移は想定されるため不整合と判別できません。 そのため、前回突合された際の決済ステータスを考慮に入れることで、より正確な整合状態を判別するようにしています。 このような突合の仕組みを利用して、より安定した決済基盤としての機能をプロダクトチームに提供しています。 おわりに この記事では、メルカリ ハロをはじめとするメルカリグループが近年注力しているプロダクトにおけるパートナーとの決済手段である、事業者請求払いについて解説しました。 実際にはもっと泥臭い処理が多く存在しており、さまざまなチームやマイクロサービスが関わって全体のフローが構成されていますが、今回は主に与信を利用した決済の部分にフォーカスをしました。 事業者請求払いによる決済スキームは、”あらゆる価値を循環させ、あらゆる人の可能性を広げる“ というメルカリグループのミッションを実現するうえで重要な役割を担っており、Payment Core チームは今後もこのような決済基盤の開発を通じて多くのプロダクトに貢献していきます。 次の記事は abcdefuji さんです。引き続きお楽しみください。
この記事は、 Mercari Advent Calendar 2024 の16日目の記事です。 メルカリでは多くの従業員の業務端末にMacbookを用いています。Security チームがmacOSのセキュリティ設定に関わる一連の作業品質・効率改善のため、設定内容の手動IaC化(Infrastructure as Code)を検討・試行した際の技術や課題に関わる所見について紹介します。 概要 この記事では、macOSのセキュリティ設定に関わる一部の作業を自動化し、作業効率を改善するための取り組みについてご紹介します。具体的には、macOS Security Compliance Project (以降mSCP)とJamf Proという2つのツールを組み合わせることで、macOSのセキュリティ設定をコード化しGitHub上で管理する手法となります。これは、将来的なGitOps化の前段階の位置づけです。 mSCPは、さまざまなコンプライアンスガイドラインに基づいたセキュリティ設定のベースラインを自動生成するツールです。一方、Jamf Proは、MDM(モバイルデバイス管理)ツールとして、macOS端末を一元的に管理します。これら2つのツールを連携させることで、以下のメリットが得られます。 設定情報をコード化しバージョン管理することで、設定変更のトレーサビリティを確保し、監査性を高めます。 設定変更作業の自動化により、人的ミスを減らし、複数環境への展開を容易にします。 GitHubのpull request機能を活用し、変更要求ごとにコードレビューと承認プロセスを設けることで、誤った設定変更のリスクを低減します。 また、Jamf APIを用いてmSCPの設定内容を含めたJamf Proの構成をコード化する方法とその考慮点についても触れています。これにより、よりシームレスなプロセス自動化を実現することができます。 本記事では、mSCPとJamf Proを組み合わせたmacOSセキュリティ設定の自動化の実例を交えながら、そのメリットや課題、今後の展望について説明しています。macOSのセキュリティ設定の管理に課題を抱えているセキュリティエンジニアやシステム管理者の参考となれば幸いです。 セキュリティ設定のライフサイクル macOSのセキュリティ設定は利用者端末への配布までさまざまな検討・作業が必要となります。まず、セキュリティ設定配布に関わる作業例について下表に紹介します。 表1:セキュリティ設定のフェージング例 No フェーズ 内容 1 設計 設定内容や配布スコープの定義。設定配布の目的・背景や配布に伴う利用者側への影響の試算 2 実装 設計に基づきセキュリティ設定のためのMDM管理ツール上の手順書等を作成 3 テスト 実装した設定値が有効かを検証機で検証 4 配布 実装した設定を本番の端末環境へ配布 5 監視運用 配布された設定が適切に適用されるかどうかをMDM管理ツール側及び端末側の双方で確認 そして、一連の作業は一過性のものでは無く、OSのバージョン・アップやOS開発元の仕様変更、業務・セキュリティ要件等に伴い、設定の見直しが一定頻度で発生するサイクリックな作業となります。 フェーズ毎の作業の重みは変更内容により変わりますが、作業品質を保ちながら各フェーズを全て手動で実施・管理していくことは人的な工数が多くかかる見込のため、一部自動化を試行し始めました。 macOS Security Compliance Project (mSCP) とは 表題の前の助走的な位置づけとして、mSCPの内容について紹介します。mSCPはmacOSのセキュリティ設定を自動作成するためのCLI(Command Line Interface)ツールであり、各種のコンプライアンスガイドライン(※1)に基づいたベースライン(プリセット)を作成可能です。GitHub上でオープンソースとして開発・配布されており、主には下記の機能を提供しています。 ※1: NIST 800-53、800-171、DiSA-STIG、CIS Benchmarks, CMMC, CNSSI等 YAML形式のテキストファイル編集によるベースラインや設定値のカスタマイズ セキュリティ設定用の構成プロファイルやスクリプトの作成。尚、スクリプトには構成プロファイル含む全対象設定のチェック機能(除外設定管理)も含まれる。 設定内容に関わるドキュメント生成(adoc, html, json, pdf, xls形式に対応) title: "タイトル" description: "概要" authors: "作成者・チーム名等" parent_values: "cis_lvl1" profile: - section: "auditing" rules: - audit_acls_files_configure - audit_acls_folders_configure - audit_auditd_enabled <中略> - section: "macos" rules: <中略> - section: "passwordpolicy" rules: <中略> - section: "systemsettings" rules: <後略> 2024年時点でmSCPでは約200項目のセキュリティ設定を構成プロファイルとスクリプトで適用可能です。mSCPを活用することにより、例えば、上表No.1, 2の設計・実装フェーズにおいては以下の様に変更可能です: mSCPで設定可能な全項目に関わるドキュメント生成(例:Microsoft Excelブック形式) 設計のベースとなるコンプライアンスガイドラインの内容をスプレッドシート化 No.1, 2で作成したシートを1つのシートに統合 統合シートで必要に応じて「現状の設定値」や「変更予定の設定値」「変更理由やその影響」列を追加し変更予定内容を関係者内で評価 確定した設定値でシートをフィルターしRuleID(mSCPにおける設定項目の識別名)をYAMLファイルへコピー&ペースト mSCPコマンドにより構成プロファイル・スクリプトの作成 この統合シートの利点としては、設定内容に付随する背景・外部のガイドラインとの差異を一元的に管理し、仮にmSCPにおける設定内容が更新されてもRuleIDやCCE(NISTのCommon Configuration Enumeration)等の識別子で再マッピングが比較的容易に可能なことです。もちろん、設定内容の説明先やその背景に応じてより要約・抽象化する必要はありますが、そのベースの資料としてはこちらのシート一つで収められます。 また、上記リストNo.6「mSCPコマンドにより構成プロファイル・スクリプトの作成」においてもmSCPがGitHubでバージョン管理されているため、GitHub Actions workflowへ組み込み、設定の自動作成を行う場合、mSCPの特定のコミットバージョンを指定して作成することが可能です。 以下はworkflow YAMLファイルの作成例となります。 <前略> jobs: build: runs-on: example-env # <適切なrunner環境名を指定> steps: - name: Checkout this repository uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 - name: Checkout mSCP repository uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4.1.4 with: repository: usnistgov/macos_security ref: '6b4330120592baf7f5a696764e67f2fbd0eaaa3a' # tested version path: 'macos_security' - uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0 with: python-version: '3.11' - name: Install pip run: python -m ensurepip --upgrade - name: Install Ruby and Bundler run: | sudo apt-get update sudo apt-get install -y ruby-full sudo gem install bundler - name: Install dependencies for mSCP run: | cd macos_security pip install -r requirements.txt bundle install --binstubs --path mscp_gems ls -la - name: Copy mSCP configuration files to cloned macos_security repository run: | cp -r ./macos/mSCP/baselines ./macos_security/build/ cp -r ./macos/mSCP/custom/rules/* ./macos_security/custom/rules/ cp ./macos/mSCP/logo_corporate.png ./macos_security/scripts/logo_corporate.png - name: Generate mSCP guidelines run: | cd macos_security ./scripts/generate_guidance.py -p build/baselines/sample_baseline.yaml -l logo_corporate.png -s -x <後略(作成したファイルの保管やrunner環境によっては自動テスト実行を必要に応じて追加)> また、上表No.3のテストフェーズにおける検証環境への設定配布に際しては、これまでMDM管理ツールなどのWeb UIベースでの手動設定から、mSCPで生成された設定ファイルのアップロードによって一部の設定項目を代替し、さらにチェックスクリプトを用いることで手動・目視での確認項目を削減することができます。 mSCPとJamf Proを組み合わせる利点 メルカリではMDM管理ツールとしてJamf Proを利用していますが、mSCPと組み合わせることで上表No.5 監視運用フェーズにおける利点についても紹介します。Jamf Proには管理対象のコンピュータ情報(名前、モデル名、シリアル番 等)について管理者側で新たな属性を追加できる、「拡張属性」機能があります。この拡張属性にはスクリプトの実行結果を設定できるため、mSCPのチェックスクリプトの実行結果を抽出・整形し下図の様に属性値としてJamf Pro上に登録することが可能です(※3)。 ※3: https://github.com/jordanburnette/mSCP_EAs 下図は対象端末の属性情報一覧の一例ですが、「Baseline – Failed Count」項目は構成プロファイルや設定スクリプトで適用できなかった設定項目数を記載しています。 図1:mSCPの適用結果を抽出する拡張属性(引用元: https://raw.githubusercontent.com/jordanburnette/mSCP_EAs/refs/heads/main/mSCP_EA_Sample.png ) Jamf Pro上ではスマートコンピュータグループの設定にて、拡張属性の値を対象端末を絞り込む検索クライテリアとして設定可能であり、例えば下図の2段目の式で「Baseline – Failed Count」> 0の端末の抽出が可能です。 図2:スマートコンピュータグループのクライテリア設定画面 端末側で設定が適用されない状態は、さまざまな要因が考えられますが、ここでは単純に、端末利用者が一時的にローカル管理者権限を借用の上、システム設定を変更し、変更後の戻し忘れと仮定しましょう。仮に対象の設定がmSCPスクリプトで設定可能であれば、Jamf Pro上のポリシーのスコープ設定にて上記のスマート・コンピュータグループを設定し、適当な「トリガー」や「実行頻度」を設定することで、自動で設定の再適用が可能です。 mSCPを利用する上での考慮点 mSCPを利用する利点がある一方で、mSCPが作成する各種の設定ファイルを適切に管理する必要があります。下記は約100項目のmSCP Ruleを設定したカスタムベースラインYAMLをもとに作成した設定ファイル一覧です。 build/sample_baseline ├── sample_baseline_compliance.sh <中略> ├── mobileconfigs │ ├── preferences │ │ ├── com.apple.MCX.plist │ │ ├── com.apple.Safari.plist │ │ ├── com.apple.SoftwareUpdate.plist │ │ ├── com.apple.SubmitDiagInfo.plist │ │ ├── com.apple.Terminal.plist │ │ ├── com.apple.applicationaccess.plist │ │ ├── com.apple.controlcenter.plist │ │ ├── com.apple.loginwindow.plist │ │ ├── com.apple.mDNSResponder.plist │ │ ├── com.apple.mobiledevice.passwordpolicy.plist │ │ ├── com.apple.preferences.sharing.SharingPrefsExtension.plist │ │ ├── com.apple.screensaver.plist │ │ ├── com.apple.security.firewall.plist │ │ ├── com.apple.systempolicy.control.plist │ │ └── com.apple.timed.plist │ └── unsigned │ ├── com.apple.MCX.mobileconfig │ ├── com.apple.ManagedClient.preferences.mobileconfig │ ├── com.apple.Safari.mobileconfig │ ├── com.apple.SoftwareUpdate.mobileconfig │ ├── com.apple.SubmitDiagInfo.mobileconfig │ ├── com.apple.Terminal.mobileconfig │ ├── com.apple.applicationaccess.mobileconfig │ ├── com.apple.controlcenter.mobileconfig │ ├── com.apple.loginwindow.mobileconfig │ ├── com.apple.mDNSResponder.mobileconfig │ ├── com.apple.mobiledevice.passwordpolicy.mobileconfig │ ├── com.apple.preferences.sharing.SharingPrefsExtension.mobileconfig │ ├── com.apple.screensaver.mobileconfig │ ├── com.apple.security.firewall.mobileconfig │ └── com.apple.systempolicy.control.mobileconfig └── preferences └── org.sample_baseline.audit.plist 構成プロファイル経由で設定する内容はmobileconfigsサブディレクトリ配下にありますが、 .plist、 .mobileconfigファイルは各々14、15ファイルに及びます。基本的にはplistかmobileconfigのどちら一方のファイル群を配布することで所定の設定が適用されますが、mSCPのバージョンや適用対象端末のOSバージョンの組み合わせによっては、mobileconfigかplistファイルの一方でのみ有効な設定があることを確認しています。 mSCPはApple社やJamf社の公式サイトで紹介されているツールではありますが(※4)、位置づけとしてはサードパーティツールであり、本番適用に際しては事前にmSCPを利用者側で検証する必要があります。 ※4 https://support.apple.com/ja-jp/guide/certifications/apc322685bb2/web https://www.jamf.com/blog/macos-security-compliance-project/ このことは、上表No.3 テストフェーズにおける実機検証において、検証パターンの増加に伴うJamf Proへのファイルアップロード回数の増加を意味します。その回数も適用するベースラインの数、適用対象テナント数、試行回数の乗算となり、加えて各ファイルのスコープ設定の調整を踏まえるとWeb UI上での操作数は相応の規模となり、操作数に比例しオペレーションリスクを増加させる要因にもなります。 Jamf APIを用いた構成のコード化 前述のようなリスクや手動での操作を低減し一連の配布作業・管理を効率化するためにJamf APIを用いてmSCP設定内容を含めたJamf Pro構成のコード化(YAML形式のテキストファイル)を試行しました。こちらの手法についてはJNUC 2021(※5)で紹介されたセッションを参考にしています。 ※5: https://www.jamf.com/blog/github-as-the-source-of-truth-for-configuration-in-jamf-pro/ Jamf社はClassic APIとJamf Pro APIの2種類のAPIを提供しておりmSCPに関連するコンポーネントを取得・更新するためのAPI概要は以下のとおりです。 表2:mSCPに関わる各Jamf ProコンポーネントのAPI種別 No コンポーネント名 API種別 データ形式 1 カテゴリー Jamf Pro JSON 2 コンピュータグループ Classic XML 3 拡張属性 Jamf Pro JSON 4 macOS構成プロファイル Classic XML 5 ポリシー Classic XML 6 スクリプト Jamf Pro JSON そして、2024年時点では公式が提供するTerraform providerやAPIラッパーライブラリーが存在しないことから、実装の投資対効果を踏まえて上記のコンポーネントの管理に特化したAPIラッパーや関連するサービスロジックを実装しました。以下はそのプログラムディレクトリ構造例です。 . <前略> ├── cmd │ └── main.go ├── go.mod ├── go.sum ├── internal │ ├── api │ │ ├── category.go │ │ ├── client.go │ │ ├── computer_group.go │ │ ├── configuration_profile.go │ │ ├── extension_attribute.go │ │ ├── policy.go │ │ └── script.go <中略> │ ├── service │ │ ├── category.go │ │ ├── computer_group.go │ │ ├── configuration_profile.go │ │ ├── extension_attribute.go │ │ ├── factory.go │ │ ├── policy.go │ │ ├── script.go │ │ └── service.go │ └── util │ ├── compare.go │ └── file.go <後略> また、上記のプログラムの構成としては下表のとおりです。 表3:各フォルダ配下のプログラム処理概要 No フォルダ名 処理概要 1 cmd プログラムのエントリーポイントであり、コマンドライン引数やクレデンシャル情報の処理や後述のserviceの生成・実行等 2 service ビジネスロジックの実装箇所。後述のapi、util内の関数等を用いてJamf Proコンポーネントのコード化や、コードとの差分確認、及びJamf Pro側へ適用等 3 api Jamf Classic / Pro API双方に対応したAPIラッパー。Jamf Pro上の構成情報を取得、更新等 4 util 各種コンポーネント横断で利用する処理を共通化した関数群 ・YAMLファイル化した構成定義とJamf Pro上の構成の差分確認を行う ・YAMLファイルの読み込みや書き出し処理 こちらのプログラムを用い、先ほどWeb UIで設定したスマート・コンピュータグループをYAMLファイルへエクスポートすると以下の様になります。(一部の値を掲載用に変更しています) id: 999 name: mSCP - sample_baseline - NonCompliant is_smart: true site: id: -1 name: NONE criteria: size: 2 criterion: - name: Computer Group and_or: and search_type: member of value: xxxxx_group - name: mSCP - Failed Results Count priority: 1 and_or: and search_type: more than value: "0" 上記の様な単純なクライテリア設定であればWebUIと作業工数はあまり変わらないですが、クライテリア条件の増加やグルーピングの修正等が発生する場合は、YAMLテキストファイル上で修正しAPI経由で更新する手法が効率的です。 Jamf ProにおけるWeb UI設定内容をAPI経由で取り扱う際の考慮点 コード化に伴い、設定作業者はWeb UIに代わり、YAMLファイルの編集に注力することが可能ですが、Jamf ProはWeb UI経由での設定した内容については、UIには現れない部分であるものの、API経由で設定を管理する際の考慮点があり、下表に検証・試行の中で確認した主な例を紹介します。 表4:API経由でJamf Proコンポーネントを管理する際の考慮点 No 考慮点 詳細 1 改行コードの取り扱い Web UIで設定した内容をAPI経由で取得する場合、改行コードが\r\n となる要素があります。(例:拡張属性やスクリプトのscriptContents等) 2 構成プロファイルにおけるペイロードの要素 mSCPにおいてWeb UI経由で初回アップロードした場合は一部の要素が追加・編集されます(例: PayloadRemovalDisallowed、PayloadIdentifier、PayloadUUID等) これらの内容はJamf ProーYAMLファイル間の差分確認や、mSCP設定内容をAPI経由で更新する際に特に考慮する必要があります。 macOSセキュリティ設定のIaC化による利点の考察 弊社の試行に際しては上記の様なWeb UI側との差分の考慮などを踏まえて、GitHub Actions workflowを用いた全コンポーネントの完全なIaC化やGitOps化は現時点では行っておらず、一部手動で構成管理・適用を行う手法を採っています。但し、仮にIaC化を進めて行くと以下の様な利点があります。 表5:IaC化による主な利点の考察 No IaC化のポイント 考えられる主な利点 1 構成情報のコード化とAPI経由での構成適用 • Web UIベースでの設定手順書の作成が不要となり、設定作業者はYAMLファイルの編集で設定が可能となる。 • YAMLファイルのコメント記法を活用することで構成内容の補足説明を追記できる。 • Jamf Pro テナントをまたいだ構成の移行が容易になる。(例:検証テナント→本番テナント) 2 GitHub上で構成情報のバージョン管理とWorkflowによる自動化 • Jamf Pro標準の履歴機能より詳細な変更差分を残すことができ、定期的なバックアップ取得や、YAMLファイル内容とJamf Pro上の設定内容を比較処理を行うことで、意図せぬ変更の検知する運用を自動化できる。 • 変更の背景起因をPull Request(以降PRと表記)上に残すことができる。 • Branch protection rulesやCODEOWNERSファイルの設定を組み合わせることで、簡易的なワークフロー(起票〜レビュー・承認)を作成できる。 • Jamf Proの編集権限がないユーザにおいても、リポジトリへのwrite権限があれば設定変更のPR起票ができる 、Jamf Pro上の編集権限者を必要最小限に絞ることが可能。 まとめ macOSのセキュリティ設定についてmSCPを活用した設定手法や多数の設定ファイルを管理する上で設定ファイルのコード化や、IaC化による利点などについて紹介しました。セキュリティ設定のライフサイクルを踏まえて、これらの要素を掛け合わせることで、プロセス全体の最適化に関わる考察を得られるよう努めました。 ここで記載された内容については全ての組織や環境に画一的に通じる方法では無く、またこの技術への投資における損益分岐点とその時期は組織ごとに異なります。実装寄りの記述も多数含む記載となりましたが、予め実装内容の解像度を上げることで、投資判断や技術負債化の予防の一助となれば幸いです。 メルカリのSecurity Teamでは、Jamf Proに限らずGoogle WorkspaceやOkta等の他のサービスのIaC化の実装へ投資し、設定自動化及び管理高度化を図っています。 Security Teamにおける採用情報については Mercari Career をご覧ください。
こんにちは。メルペイ 機械学習エンジニアの @rio です。 この記事は、 Merpay &amp; Mercoin Advent Calendar 2024 の記事です。 本記事では、メルペイの機械学習エンジニアチームで今年取り組んだ、MLOps の省力化および品質向上についてご紹介します。 目次 メルペイの機械学習システムの概要 1. 開発ブランチのマージ 問題点 解決策 2. 各種マスタデータの更新 問題点 解決策 3. 機械学習パイプラインの実行 問題点 解決策 まとめ メルペイの機械学習システムの概要 メルペイでは、毎月の与信枠更新ロジックの一部に機械学習システムを採用しています。 そのため、機械学習エンジニアチームでは毎月リリース作業が発生します。 本記事では、リリース作業のうち、以下の作業に関して品質を担保しながら省力化を目指した取り組みをご紹介します。 開発ブランチのマージ 各種マスタデータの更新 機械学習パイプラインの実行 1. 開発ブランチのマージ 開発ブランチでのさまざまな変更内容を main ブランチに反映させます。 その中でも、毎月必ず発生するのが config ファイルの更新です。 config ファイルでは、機械学習モデルの学習や推論、後処理などで必要な設定をしています。 以下は config で指定している項目の例です。 学習、評価データの期間 推論対象月 ハイパーパラメータ探索に関する設定 モデルやデータセットなどのバージョン 各種 I/O のパス など 問題点 config ファイルの更新時に、設定漏れや設定の不備など人為的ミスが起きてしまうことが問題でした。複数の機械学習モデルがあることや、各モデルごとの設定項目が多いことが要因としてあげられます。 解決策 config ファイルの設定の不備を機械的に検知してユーザに通知する仕組みを導入しました。 ワークフローは以下のとおりです。 <img src="https://storage.googleapis.com/prd-engineering-asset/2024/12/086d05b2--2024-12-19-14.20.51.png" alt="""> 図1. config ファイル自動チェックのワークフロー ロゴ出典: GitHub config の修正を以下のケースで分類しています。 モデルの推論のみを行うための修正 モデルの再学習を行うための修正 リリースに関係のない修正 3.については validatioin は実施しません。 1.2. に関しては、それぞれ一般的な型 validation に加えて、機械学習モデルの要件に紐づく以下のような validation を行っています。 作業月と推論対象月が矛盾していないか 先月リリース時の config と比較して矛盾がないか 作業月のNヶ月前の日付が指定されているか など この仕組みの導入により、config ファイル更新時に不備があった場合、PR のマージ前に低コストで気づけるようになりました。 2. 各種マスタデータの更新 メルペイの機械学習システムには、さまざまなマスタデータが存在します。 マスタデータの更新は、基本的に PdM や Biz の方が行うため、運用観点で現状は Google スプレッドシートでの管理に落ち着いています。 問題点 システム上の管理は GitHub と BigQuery で行っているため、スプレッドシートのデータを テキストファイルに変換する作業が必要になります。この作業で人為的ミスが起きることと、作業が非効率であることが問題でした。 解決策 ワークフローは以下のとおりです。 図2. マスタ更新自動化のワークフロー ロゴ出典:GitHub, Google スプレッドシート スプレッドシートの validation では、機械学習特有のものはありません。 データ型や入力値の範囲、ヘッダの数や名称など、一般的な項目をチェックしています。 複数のマスタデータや、その他のスプレッドシートで管理されているデータにおいて、品質担保および効率化できるよう、この仕組みを使いまわして運用しています。 3. 機械学習パイプラインの実行 毎月のリリース作業には約50個のタスクがあります。 そのうちのいくつかは、AirFlow の DAG を用いて実装されている機械学習パイプラインを実行するタスクです。 問題点 以下の問題がありました。 実行日とタスクが一覧になっているスプレッドシートでタスクを管理しているが、いつどの DAG を実行するかを確認する認知コストがかかる リリースタスクの数が多いこともあり、ミスや手戻りが発生し得る リリース作業のオンボーディングが非効率で、新規メンバーのキャッチアップに時間がかかる 解決策 リリース作業をアシストしてくれる Slack bot、”Release Ops Assistant”(以降 ROA)を導入しました。ワークフローは以下のとおりです。 図3. ROA のワークフロー ロゴ出典:Slack, Google スプレッドシート, Google Compute Engine, Apache Airflow ROA を実装する際に、下記の要件を満たしたいと考えていました。 Slack からの DAGトリガー要求にリアルタイムで対応できること 実装及び運用コストが低いこと 検討したアーキテクチャには、Cloud Functions や Cloud Run などありましたが、最終的には Socket Mode が使えるという理由で Google Compute Engine(以降 GCE)を採用しました。Socket Mode は、WebSocket を使用してリアルタイムでイベントを受信できる接続方式で、ファイアウォールを気にせず簡単にアプリ開発ができるという特徴があるため、上記の要件を満たすことができます。 また、ジョブスケジューラーも Cloud Scheduler や Cloud Tasks などを検討しましたが、最終的には Slack との親和性が最も高いという理由で Slack ワークフローを採用しました。 図3 のワークフロー内赤枠の部分では、Slack Bolt が Socket Mode で GCE のプログラムをトリガーし、GCE が Airflow REST API を叩いて DAG を実行しています。 ROA の導入により、いつどのタスクをやるべきか Bot が通知してくれるので認知コストが下がり、リアルタイムにタスクの実行やステータスの変更が可能となったためミスや手戻りも発生しづらくなりました。 図4. ROA の Slack 画面 まとめ 本番稼働中の機械学習モデルの運用について、品質を担保しながら省力化することで、生産性をあげる工夫をご紹介しました。 生産性向上はどうしても後回しにしてしまいがちですが、一度腰を据えてまとめて見直しができて非常に良かったと思います。 次の記事は @komatsu さんです。引き続きお楽しみください。
こんにちは。メルカリWorkチームQA Engineerの @um です。 この記事は、 連載:メルカリ ハロ 開発の裏側 – Flutterと支える技術 – の5回目と、 Mercari Advent Calendar 2024 の15日目の記事です。 今回は私達が開発している「メルカリ ハロ」のモバイルアプリのQAに焦点を当てて紹介します。 概要 メルカリ ハロのモバイルアプリは、クロスプラットフォームフレームワークであるFlutterを採用しています。Flutterによる開発は、単一のコードベースでiOSとAndroid両方のアプリを構築できるため、開発効率の向上に大きく貢献しています。QA活動においても、テスト効率の向上に貢献できるのですが、その特性を理解した上で適切なテスト計画を考える必要があります。この記事では、メルカリ ハロにおけるFlutterアプリQAのメリット・注意点、そして私たちが実践しているテストの進め方について解説します。 前提 モバイルアプリ開発には、「ネイティブアプリ開発」と「クロスプラットフォーム開発」という大きく二つのアプローチがあります。Flutterは後者のクロスプラットフォーム開発を可能にするフレームワークです。 ネイティブアプリ開発とは、iOSならSwift、AndroidならKotlinといった、プラットフォーム専用の言語とSDKを用いて開発する手法です。 対するクロスプラットフォーム開発とは、単一のコードベースで複数のプラットフォーム(メルカリ ハロではiOSとAndroid)に対応するアプリを開発する手法です。 この特徴はネイティブアプリの開発者だけでなく、QAエンジニアにとってもテスト効率の向上に大きく貢献します。具体例を以下に示します。 Flutterを用いたクロスプラットフォーム開発におけるQAのメリット 同時テストによる効率化と品質向上 iOSとAndroidアプリの実装が同時完了するため、両OSのビルドを並べての比較検証が可能になります。これにより、一つの観点で両OSを確認できるだけでなく、OS間のUIの差異や、それぞれ別でテストした場合に見逃しがちな細かな不具合の発見につながります。 また、私たちはスクラム開発の手法を採用しており、QAエンジニアもスクラムチームの一員として参加しています。スクラム開発を行う上で、1つのユーザーストーリーを1枚のストーリーチケットとして扱っており、開発はユーザーストーリー単位で実装されるため、テスト実行もユーザーストーリー単位で実行します。 そのプロセスにおいても、両OSのテストをチケットを分けることなく1枚のストーリーチケットでまとめて実施できるため、管理工数の削減にも貢献しています。 開発者とのコミュニケーションコスト削減 QAを行う上で開発者とのコミュニケーションは必要不可欠です。具体的には、スケジュールの調整から始まり、仕様の認識合わせ、開発視点で考慮が必要な観点、Acceptance Criteria(以下ACと記載します)の過不足のチェック、リリース手順の確認など多種多様なコミュニケーションが発生します。 機能をより早く、より安全にリリースするためには、こういったコミュニケーションの質や量を担保することが必要ですが、開発担当者が増えれば増えるほどコミュニケーションの難易度とコストは上がります。一般的にネイティブアプリ開発の場合はOSごとに開発者が分かれていることが多いため、例えばMTGの時間を合わせることが難しくやむをえず情報伝達に時間差が生じてしまったり、複数の担当者間での認識の齟齬が生まれたりすることなどが起こり得るかと思います。一方でFlutterのアプリケーション開発においては両方のOSを同一の開発者が担当するため、やり取りする担当者の数は最小限ですみます。これは単純ではあるものの大きな違いであり、これによりコミュニケーションの難易度とコストが抑えられるように感じます。 テストケース量の削減 テストケース数を少なくするためのアイディアとして、Flutterの単一コードベースの特徴を活用することができます。 具体的にはバックエンドAPI呼び出し部分やレスポンス表示部分などは片方のOSで正しく動作することを確認できれば、もう片方のOSでのテストを省略できる場合があります。 事前に開発者と実装内容や影響範囲を確認した上で、このような戦略的なテストケース削減を行うことで、リリースまでのリードタイム短縮につなげることができます。 Flutterを用いたクロスプラットフォーム開発におけるQAの注意点 メリットが多い一方で、Flutter特有の注意点も存在します。 両OSの不具合影響 Flutterが単一コードベースであるがゆえに、実装上の不具合が両OSに同時に影響を及ぼす可能性があります。このリスクを低減させるために適切な品質保証活動を行う必要があります。 具体的な活動については、QA Engineering ManagerのrinaがACについて記述した 本連載1回目の記事 をご参照いただければと思います。 OS固有の実装への対応 Flutterは単一コードベースを原則としますが、OSに依存した機能など特定の処理に関してはOSごとに異なる実装を行う必要があります。 メルカリ ハロでは、一例ですが以下のような機能でOSを意識した実装をしています。 ・応募したおしごとの日程をカレンダーアプリに登録する機能 ・おしごとの労働条件通知書ファイルを端末にダウンロードする機能 QAエンジニアは、OSごとの実装が入っているかどうかについて開発者とコミュニケーションをとり、適切なテスト設計をしたり、テスト実行を行うことが重要です。 まとめ Flutterはクロスプラットフォーム開発のメリットを享受できる一方、QAにおいてはその特性を理解した上で適切なアプローチを取ることが重要です。メルカリ ハロでは、今回ご紹介したメリット・注意点を踏まえ、効率的かつ効果的なQA活動を実践することで、あんしん・あんぜんなアプリを提供できるよう努めています。 この記事の内容が、みなさまのプロジェクトや技術的探求に貢献できたなら幸いです。引き続き「 メルカリ ハロ 開発の裏側 &#8211; Flutterと支える技術 &#8211; 」シリーズを通じて、私たちの技術的知見や経験を共有していきますので、どうぞご期待ください。また、 Mercari Advent Calendar 2024 の他の記事もぜひチェックしてみてください。それでは、次回の記事でお会いしましょう! 次回の記事は @howieさんです。引き続きお楽しみください。
はじめに こんにちは。メルカリEngineering Officeの @raven です。 この記事は、 Mercari Advent Calendar 2024 の14日目の記事です。 Engineering Officeはエンジニアリング領域における組織横断課題の解決に取り組んでいる部署です。エンジニアリング組織に対するナレッジマネジメントの改善も私たちの担当領域となります。 私は2024年4月にメルカリに入社しましたが、入社当初からメルカリでのナレッジの探しにくさを感じ、他の同僚に資料や情報の場所を聞いたりしながら必要な情報を探していました。実際に他部署のナレッジがどこにあり、どうやって調べて良いかもわからない状況でした。 そんな中、年次で行なっている全組織のエンジニアへのアンケートにおいて、最も満足度が低い領域の1位に輝いたのが社内のナレッジに関するものでした。アンケート結果に納得している私のもとに、幸運にもナレッジマネジメントの改善プロジェクトのアサイン依頼がやってきたのでした。 この記事に書かれている内容 この記事では、道半ばではありますが、私たちのチームがエンジニアのナレッジマネジメントに対する満足度を向上させるべくプロジェクトとして取り組んでいる内容を以下の2つのポイントで紹介したいと思います。 ・エンジニアが抱える課題に対してどのようなアプローチをとったのか? ・どのように組織横断でプロジェクトを推進したのか? 自社で同じようなナレッジに関する課題を抱えている方に、少しでも参考になれば幸いです。 エンジニアたちのナレッジに対する不満 ナレッジマネジメントを改善すると一言で言っても、簡単ではありません。これまで培ってきたドキュメンテーション文化の変更を、エンジニアに対してお願いする必要があります。単一の組織ですら改善が大変そうな取り組みですが、私たちの部署の担当範囲は、単一の事業部からインドを含む日本リージョンの全てのエンジニアリング組織へと大幅に拡大されたばかりでした。まさに組織横断課題の解決という、私たちの部署のミッションにぴったりの仕事です。そのため、このナレッジマネジメントプロジェクトは、私たちのミッションである「組織横断課題の解決」の真骨頂と言えるような、壮大な取り組みとなりました。 何はともあれ、まずはエンジニアたちがアンケートで回答したナレッジに関する不満の内容を分析することから始めました。エンジニアたちの大きな不満は主に以下のような内容でした。 ナレッジが複数のプラットフォームに分散しているので検索性や発見性が低い ナレッジプラットフォームが多いが、各組織が独自のルールでナレッジを構築しているのでナレッジが集中化および整理されていない ドキュメントが標準化されておらず、同じ資料でも記載内容や書きぶりが組織によって異なる ナレッジのメンテナンスがされておらず、重複や古い情報が多い ナレッジマネジメントに関するトレーニングやガイドラインなどが提供されていない 英語と日本語の資料が存在するが、言語の壁で情報共有がスムーズにできない これらのエンジニアからの不満は、他の会社でも共通する部分が多いのではないでしょうか? ナレッジマネジメントが適切に行われていないことで、私たちが失っているものは想像以上に多く、それは会社にとってもエンジニア達にとっても大きな損失となります。 私たちのエンジニアがストレスなく、言語の壁と組織の壁を超えて情報を共有したり取得したりできる。そんな世界観を目指してプロジェクトはスタートしました。 それぞれの課題にどうアプローチしていくのか? エンジニアからの課題をまとめると、以下のような課題を解決する必要がありそうでした。 複数のプラットフォームにナレッジが分散している ナレッジに対するルールがなくナレッジが整理されていない 上記の2つの課題により検索性や発見性が低い 言語の壁があり情報共有が進まない ドキュメントの標準化がされていない ナレッジが適切にメンテナンスされていない ナレッジに関するガイドラインやトレーニングが存在しない それぞれの課題に対する私たちのアプローチをご紹介します。 課題:複数のナレッジプラットフォームにナレッジが分散している 私たちがドキュメントを作成するツールは大まかに分類すると、以下となります。 Confluence Google Docs / Slide GitHub (ナレッジをまとめてWebページとして公開) どこにナレッジを集めるのか?という課題に対し、保有している既存資産からナレッジプラットフォームの選定を行うにあたり賛否両論がありました。ドラスティックなアプローチをとって、Confluenceのみにする、他のプラットフォームは利用させない等。しかし、プラットフォームの選定において製品を比較していくと、それぞれの製品に良さがあります。 プロダクト プロダクトの良い部分 Confluence 直感的なページ作成、ナレッジやナレッジの領域管理が容易 GitHub バージョン管理、レビューや承認機能などが充実 Google Workspace 様々なコラボレーションツールとシームレスに連携 色々と検討を重ねた結果、私たちは以下の方針でナレッジプラットフォームの構築を行うことにしました。 「Confluenceをナレッジプラットフォームの中心として、Confluenceに不足している機能を他のプラットフォームで補完する」 課題:ナレッジに対するルールがなくナレッジが整理されていない ナレッジプラットフォームはそれぞれのツールの良さを活かすため、柔軟性を持たせる設計としましたが、複数のツールを認めたからといって、情報をそのまま多くのツールに分散したままにしないよう、私たちはConfluence上で各組織ごとのナレッジ領域と、組織内の全てのチームのナレッジを実際に格納する専用のページを人事情報から自動作成する事にしました。各チームが持つコミュニケーションチャネルや、GitHubリポジトリ、設計書などの情報を標準化されたチームごとのテンプレートに情報を記載してもらうことで、まずは各組織のチームが保有している社内で共有する価値がある情報を、組織横断で同じテンプレートを利用してConfluenceへナレッジを集めることとしました。 なぜチームという組織カットのアプローチにしたのかというと、現在の組織構造と指揮命令系統を考慮し、ガバナンスを効かせたりプロジェクトの推進がしやすいというのが主な理由です。他の案としてはプロダクト単位、技術ドメイン単位という案もありましたが、まずナレッジマネジメントの改善の一歩を踏み出すにあたり、ナレッジにおける責任範囲を明確にした上でプロジェクトを推進するためには、このアプローチが最適と判断しました。 また、組織横断でチームごとに同じ粒度で情報を整理するこの取り組みは、お互いの組織の情報や保有するナレッジを相互理解するためにも重要な目的を持っていました。個人的な印象としては、地図のない世界にようやく手書きの粒度の粗い地図ができ、組織横断での見通しが良くなったと感じています。 課題:検索性や発見性が低い Confluenceに情報をリンクして、ある程度までは各組織が保有しているナレッジに対して導線ができて辿り着ける状態にはなりましたが、所詮リンクを張っただけでは検索性が劇的に向上するわけではありません。 前述したナレッジプラットフォームの図にもConfluence から伸びる矢印にLLM+RAGと記載されていましたが、私達はプロジェクト開始当初からLLM(Large Language Model)チームと連携し、Confluenceの情報をRAG(Retreval Augmented Generation)のソリューションを利用してエンジニアに関連するナレッジを検索できないかを検討していました。すでにLLMチームではGitHubなどの主要なエンジニアリングに関連する情報をRAGに取り込んでいたので、さらにConfluenceに組織横断で集めた情報からエンジニアに役立つ情報をRAGに取り込み、社内のLLMシステムからConfluenceに取り込んだナレッジを提供することにしました。 課題:言語の壁があり情報共有が進まない 日本語のドキュメントは日本語が苦手なエンジニアは読まない。英語のドキュメントは英語が苦手なエンジニアは読まない。当然と言えば当然ですが、ナレッジマネジメントではエンジニア間でナレッジをスムーズに共有できるように、言語の壁を取り除くことが重要です。 しかし、全てのドキュメントに日英のドキュメントを2種類用意するのはリソース面でも難しいですし、Confluenceの翻訳プラグインは翻訳量に応じた従量課金なので、Confluenceをナレッジマネジメントの中心とすることでコスト面のインパクトも気になります。 幸いなことに、私達はLLM+RAGというソリューションが既にあるため、日本語と英語で共有されるべきナレッジの言語の課題はLLM+RAGのソリューションで解決することとしました。英語で書かれているドキュメントの内容も、LLMのシステム上で日本語で質問すると日本語で回答が返ってくるので、ドキュメントにおける言語のバラツキが多い環境下においてもスムーズなナレッジの共有と、今まで閲覧することのなかった新たなナレッジの発見に貢献しそうです。 課題:ドキュメントの標準化がされていない 今まではほとんどのケースにおいて、各組織ごとに独自の標準化されたテンプレートを利用していました。さらに複雑な場合には、各組織の中でも複数のテンプレートが存在するという状況でした。 標準化されたドキュメントのテンプレートを利用することで、情報の粒度が均質化され、誰もが過不足なくドキュメントを作成できるようになり、また、読み手に対してもストレスなく情報を共有することができます。私たちはまずはエンジニアの間で作成頻度が高いドキュメントに対しては、標準化されたテンプレートを利用することを推奨することにしました。 課題:ナレッジが適切にメンテナンスされていない ナレッジを常に最新に保つために、私たちはナレッジマネジメントチームがConfluenceに対して実施しているドキュメントの健康診断チェックツールを強化しました。これによりナレッジの情報鮮度や、標準テンプレートの利用状況などのモニタリングと可視化を行い、定期的に点検をエンジニアに依頼することで、ナレッジの維持管理に努めています。 課題:ナレッジに関するガイドラインとトレーニングが存在しない ナレッジマネジメントの取り組みをエンジニアに理解してもらうために、私たちは、ドキュメンテーションツールの選択と、ドキュメントの標準化されたテンプレート利用に関するガイドラインをConfluence上で作成しました。ガイドラインは今後さらに拡張していく予定です。 ガイドラインを発行したからといって、全てのエンジニアがガイドラインを熟読し即座にガイドラインに沿った行動を取ってくれるわけではないので、社内のe-Learningシステムにナレッジマネジメントの基本的な考え方と、ガイドラインの内容を学べるトレーニングコースを作成し、必須トレーニングとしてトレーニングを受講してもらい、ガイドラインへの理解とナレッジマネジメントに対する意識改革を推進しています。 また、トレーニング以外においても、エンジニア向けの全体集会にてナレッジマネジメント関連の情報共有や、Open Doorセッションなどを定期的に開催し、ナレッジマネジメントの重要さを理解してもらう活動を行っています。 組織横断プロジェクトの推進 エンジニアが抱える課題に対して、どのようにアプローチするのかが決まっていても、全てをコミットし、デリバリーできなければ、いくら良い施策でも机上の空論で終わってしまいます。 ここでは、組織横断でプロジェクトを推進する際に特に気を付けていたポイントを書いてみます。 プロジェクトの設計 可視化 KM(Knowledge Management) コミッティーの組成 IO(Information Owner)のフォローアップ制度 アナウンスと浸透活動 プロジェクトの設計 プロジェクトを推進するにあたり、ナレッジマネジメント改善における取り組みの概要、スケジュール、詳細タスク、リスク分析、ナレッジの浸透計画、トレーニングや、ナレッジのモニタリング計画などを慎重に検討しました。 また、それらの計画や情報などはConfluence上でプロジェクト管理用のページを作成し、プロジェクトメンバーおよび、他の社員にもプロジェクトの取り組みを認知してもらえるように、情報を積極的に公開するように努めました。 可視化 計画や施策などに関しては、視覚的に理解しやすいイメージ図を作成して可視化を行い、プロジェクトのステークホルダーやメンバーが取り組みを理解しやすいように心がけました。会議においても、可視化した取り組みのイメージを活用することで参加者に誤解なく速やかに内容を理解してもらうことができ、組織横断での意思疎通もスムーズに行えました。 KM(Knowledge Management) コミッティーの組成 同じ会社といえども、組織ごとに異なるドキュメントの文化や慣習が存在します。 組織横断でプロジェクトを推進するために、まずは各組織からナレッジマネジメントの代表としてIO(Information Owner)を選出してもらい、KMコミッティーを組成しました。各組織から選出されたIOは20名程度になり、私たちはIOと共に、組織ごとに異なるドキュメンテーションの共有や組織横断としてのドキュメンテーションの方針、ガイドラインの検討やトレーニングコンテンツの検討などを行いました。また、ナレッジを組織のチームごとに集約する際もIOが自分の担当組織のマネージャーに更新を依頼し、トレーニングの受講を促したりと、ナレッジマネジメントの改善を一緒に推進することができました。 IO(Information Owner)のフォローアップ制度 IOはナレッジマネジメントのプロジェクトのみに注力できるわけではなく、基本的には業務が忙しい方達が担当しています。コミッティーに参加できないIOも存在しますので、プロジェクトメンバーであるナレッジマネジメントチームが担当IOを決めて、個別に1on1などを設定し、IOのフォローアップを行うことで、IO間での情報格差を最小限に抑えられました。 アナウンスと浸透活動 いざ、ガイドラインやトレーニングのデリバリーを行ったとしても、実際にエンジニアに届かなければ意味がありません。もちろん周知できるコミュニケーションチャネルにはアナウンスしていますが、すべてのエンジニアに認知してもらい、行動してもらうには、アナウンスだけでは十分ではないのです。私たちは、IOと協力して組織へナレッジマネジメントの活動を落とし込んでもらったり、エンジニアの全体集会やOpen Doorイベントなどでエンジニア向けのナレッジマネジメントの浸透活動を積極的に行いました。 最後に 私たちのエンジニアリング領域におけるナレッジマネジメント改善の挑戦に関する取り組みと、組織横断で推進するプロジェクトのポイントを書かせていただきました。 ナレッジマネジメントの活動は、プロジェクトが完了した後でも、定期的にユーザーからのフィードバックをガイドラインやトレーニングコンテンツに反映、標準化テンプレートの拡張と利用推進、LLMへのナレッジの取込みなどを継続して行う必要があります。私達は今後さらに、メルカリのエンジニアリング領域における持続的なナレッジ文化の向上を目指していきます。 また、エンジニアリング領域におけるナレッジ基盤が確立した後は、プロダクトやビジネスの領域にもナレッジマネジメントの取り組みを拡大し、全社レベルでの活動に取り組みを広げていきたいと考えてます。 この記事を読んでいただいた方に、私たちの経験を通じて少しでも参考になることがあれば幸いです。 最後までお読みいただき、ありがとうございました。
この記事は、 Merpay &amp; Mercoin Advent Calendar 2024 の記事です。 はじめに メルペイBalanceチームでバックエンドエンジニアをしている @kobaryo と申します。 皆さんは日々の開発の中で静的解析を利用していますか?静的解析を利用することで、コードが何かしらのルールに従っているということを保証することができます。プログラムの中にコンパイル時に検出できない何かしらのルールがあり、それに違反していることにプログラムを動かして初めて気付く、という事態になる前に違反を検出することができます。本記事でこれから示す事例は、静的解析で検出すべき良い事例なのではないかと思います。 メルペイの多くのチームでは、データベースとしてSpannerを採用しています。しかしながらSpannerを扱う上で、「 allow_commit_timestamp である列にアプリケーション側で生成した現在時刻を入れてしまう」というミスがしばしば見られます。これをしてしまうとSpannerへの列の挿入や更新が確率的に失敗してしまうため、テストやQAでこのミスを発見できず、インシデントに繋がってしまう恐れがあります。 テストやQAでこのミスに気付く可能性もあるのですが、確率的にエラーが発生する性質上、実際にプログラムを動かしてこのミスを検出するのには限界があります。 そこで、Goの time.Now() がSpannerのMutationに含まれてしまっているかをデータフロー解析で検知する静的解析ツール nowdet を作成しました。 本記事では、まずこのようなエラーが発生する背景から説明して、次にデータフロー解析の概要、nowdetの実装のコアな部分、最後に今後の展望について述べます。 背景 Spannerでは TIMESTAMP 型の列に allow_commit_timestamp オプションを付けることができます。このような列にプレースホルダ文字列 spanner.commit_timestamp() を挿入すると、その名の通りコミット時のタイムスタンプに置き換えられて保存されます。Spannerは external consistency という強い一貫性を持っており、これによりトランザクションの順序とタイムスタンプの順序が一致します。そのため、変更履歴といった順序が重要となる処理を、単にコミットタイムスタンプを参照することで実装できます。 この allow_commit_timestamp である列には spanner.commit_timestamp() だけでなくアプリケーション側で生成したタイムスタンプを挿入することもできるのですが、 過去のタイムスタンプでなければならない という制限があります。アプリケーションで現在時刻を生成しこの列に挿入しようとした場合、Spanner内のクロックとアプリケーションサーバーのクロックは一致していないために、アプリケーションで生成されたタイムスタンプがSpanner内部より未来のタイムスタンプになる可能性があります。この場合、 Cannot write timestamps in the future とエラーが発生してしまいます。 実際、私は当初この仕様を知らず、このオプションが付いている列に spanner.commit_timestamp() ではなく誤ってGoの time.Now() で生成した値を挿入してしまっていました(幸いなことにテストでミスが発覚しました)。また、社内のSlackで Cannot write timestamps in the future で検索すると、多数のメッセージがヒットすることから、自分と同じミスをしている開発者は多いことが分かります。 経験上、このミスをした場合に実際にエラーが発生する可能性はそこまで低くないのでミスに気づきやすい、また一度このミスを経験すると同じミスは犯しにくいように感じます。しかしながら、やはり確率的にエラーが出るという点で、静的解析によってこのミスを検知する価値があると私は考えています。 データフロー解析 静的解析の手法の1つであるデータフロー解析は、プログラムの実行経路に沿って発生するデータの流れに関する情報を求める手法の総称のことです。例えば、変数がその実行経路を通っても更新されない、といったことを静的に検知する際に利用します。以下のようなプログラムについて考えます。 func example(b bool) (int, int){ var x = 0 // may be changed var y = 0 // immutable if b { x = 1 } return x, y } この例の y の値は更新されていないので、 y は実際には定数として定義しても問題ありません。この例では、データフロー解析で各プログラムポイントで各変数がどのポイントで定義された値を保持しているか(到達定義)を求めることで、このことを検知することができます。まず、上記のプログラムを制御フローグラフ (CFG)で表します。 このとき、各プログラムポイントにおける到達定義は以下のようになります。 P5 の return 文後の y の到達定義が P1 のみであり、 y は P1 で定義されたものだったので、 y を定数として定義しても問題ないということが分かりました。実際にはプログラムにループや再帰が含まれていて、到達定義を一度求めた後に再計算しなければならない可能性があり、到達定義が収束するまでこの処理を繰り返します。 今回のような定数で定義できる変数の検出以外にもデータフロー解析を利用することができます。例えばあるbool型の変数がどの実行経路を通っても常にfalseであることを検知したり、あるポインタをdereferenceする際、そのポインタがnilであるような実行経路が存在することを検知したりできます。この記事を読んでいる皆さんも、IDEや静的解析ツールでこのような機能を見たことがあるかと思います。 nowdetの実装 今回作成した nowdet も上記のnilポインタを検知する例とほぼ同様の処理をして、SpannerのMutationに time.Now() が含まれる実行経路が存在するかどうかをチェックしています。 もう少し具体的な処理を例で示します。例えば、以下のような関数について考えます。 func insert(ctx context.Context, client *spanner.Client, isNow bool) error { var now time.Time if isNow { now = time.Now() } else { now = time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC) } _, err := client.Apply(ctx, []*spanner.Mutation{ spanner.Insert( &quot;Users&quot;, []string{&quot;name&quot;, &quot;created_at&quot;}, []interface{}{&quot;Alice&quot;, now}, ), }) return err } この関数では、引数の isNow に true が与えられるとアプリケーション側で生成した現在時刻がSpannerのテーブルに挿入されてしまいます。このプログラムをCFGで表します。nowdetでは解析対象のGoプログラムを静的単一代入形式 (SSA)に変換し、それをデータフロー解析しているので、実際は以下のようになります(赤字が time.Now() が関連している部分)。 基本的には、 time.Now() が代入された変数をマークし、マークされた変数が他の変数に代入される、または含まれた際にその変数にもマークを伝播させるという方針になっています。 例えば、関数を呼び出した際にその関数が time.Now() だった場合にマークをします。上記の例では、 P1 に t0 = time.Now() があるので、 t0 をマークします。その他にも、プログラムの実行経路によって変数の値が変わる場合、そのうちの一つがマークした変数から来たものであればマークします。上記の例の P3 の t1 = phi [1: t0, 3: t20] では、 t1 はプログラムの実行経路によって t0 か t20 になることを表しているので、 t1 もマークします。 このような処理を繰り返し、最終的に P20 で spanner.Insert の引数にマークされた変数 t13 が含まれるため、Mutationに time.Now() が含まれたと判断されて、アラートを返すという流れになっています。 かなり細かい話をすると、実はスライスが関わる処理では単純にマークをするのではなく、グラフを作っています。上で示した例だと、 t13 がマークされていれば正しく検知ができて、その t13 は P19 で t13 = slice t8[:] で定義されています。また、その t8 は P12 の t8 = new [2]interface{} (slicelit) で定義されています。 t8 が定義された時点で t8 がマークされていないと検知が成功しないのですが、実際には t8 が定義された後の P16 から P18 で、 t11 = &amp;t8[1:int] ・ t12 = make interface{} &lt;- time.Time (t1) ・ *t11 = t12 と、 t8 とマークされた変数 t1 が結びついています。そのため、プログラムを上から順に見ていくだけだと t8 をマークすることができず、うまく検知することができません。 この問題を解決するため、スライスが関わる処理では、スライスから辺を辿って要素に到達できるようなグラフを生成することで対策をしています。具体的には、 P16 の t11 = &amp;t8[1:int] と、 P18 の *t11 = t12 の処理で以下のようなグラフが作られます。 そして、 P19 の t13 = slice t8[:] で t8 がマークされているかをチェックする代わりに、このグラフを t8 から辿り、マークされている変数に到達すれば t13 をマークする、という処理に変更します。このグラフは t12 に到達し、 t12 は P17 の t12 = make interface{} &lt;- time.Time (t1) でマークされているため、 t13 をマークすることができます。 今後の展望 スキーマを取得し偽陽性を避ける 現在の実装では、実際に列が allow_commit_timestamp オプションを持っているかどうかにかかわらず time.Now() が挿入されうるかを検知しています。 allow_commit_timestamp オプションを利用せずアプリケーション側で生成した現在時刻を挿入するケースももちろんあるので、スキーマを取得し、 allow_commit_timestamp オプションを持っている列に関してのみ検知するようにしたいと考えています。 関数を跨いで time.Now() を検出する 現在の実装では、同じ関数内で time.Now() を呼び出し、それをMutationに入れている場合のみ検知します。異なる関数もしくはパッケージで呼び出した time.Now() がMutationに入る場合にも検知できるようにしたいと考えています。懸念としては計算時間の増加があるのですが、究極的には time.Now() からMutationまでのフローのみをチェックすればよく、無駄な基本ブロック(CFGの頂点)を解析対象から外す方針にしたいです。 ポインタの扱い スライスが絡んだ際にどのように検知しているかを上で述べたのですが、ポインタが関わるパターン一般に関してうまく動作するわけではありません。先述のグラフの例だと、 t8 と t12 は一方から他方に辿れるというよりは、イコールになるべきです。全ての場合に対応するのは直観的には難しい気がしているので、実用上耐えうるような制限を設け、全ての場合は検知できないが有用ではある、という状態を目指したいです。 おわりに 以上、Goの time.Now() がSpannerのMutationに含まれていないかを、nowdetがどのようにして検知しているかについて説明しました。Balanceチームでは(主に別の理由で) time.Now() がコードに含まれていないかをCIでgrepしてチェックしているのですが、誤検知が多いという問題があります。nowdetがこのgrepを置き換えられるよう、これからも開発を続けていきます。 次の記事は@orfeonさんです。引き続きお楽しみください。
はじめに こんにちは、Platform Securityのisoです。この記事は、 Mercari Advent Calendar 2024 の記事です。 本記事ではGitHubのbranch protection(protected branch)について、特にpull requestのマージに承認が必要とする制約をどうにかして突破できないかについて考察します。ぜひ最後までお読みいただけると嬉しいです。 メルカリにおけるGitHub メルカリではGitHubを使ってコードの管理をしています。アプリやバックエンドのコードだけではなく、TerraformやKubernetesなどインフラに関わるあらゆるファイルをGitHubを使って管理しておりGitHub上のデータは非常に重要な役割を担っています。 組織によって開発者に付与するGitHubの権限は様々だと思いますが、メルカリの開発者は基本的に(自チーム以外のリポジトリを含む)多くのリポジトリに書き込み権限を持っています。(もちろんリポジトリの内容を考慮し、限られた開発者のみがアクセスできるリポジトリもあります。)これにより他チームのリポジトリに新しくブランチを作成してpull request(以降、PR)を作成したり、TerraformやKubernetes関連のファイルが保存されているリポジトリにPRを作成してインフラを構成したりといったことが可能となっています。 色々なリポジトリに書き込み権限を持っていることは便利な一方で、そのリポジトリとは全く関係のない開発者がコードを勝手に書き換えられたり、重要なTerraformのファイルをレビューなしで変更できたりしてしまうのは好ましくありません。そこでbranch protection ruleあるいはbranch rulesetを使うことで、デフォルトブランチ(main/masterブランチ)への変更はPRの作成を必須化し、マージには承認を必要とするというセキュアな運用を実現できます。メルカリでは、プロダクションに関わるリポジトリにはすべてこの設定を導入しています。 (なお、GitHubにおいてブランチを保護する方法としてbranch protection ruleとbranch rulesetがありますが本記事が扱う内容においては2つに違いはないため、特に区別せずにbranch protectionと呼びます。) Branch Protectionへの攻撃方法 さて、このようにbranch protectionはリポジトリを守る上で重要な役割を担うわけですが、どのような設定をしたら良いのでしょうか。また本当にbranch protectionで大切なブランチを守り切れるのでしょうか。 前提条件 以下のシンプルな条件で考えてみます。 前提: リポジトリにアクセスできるすべての開発者がリポジトリに書き込み権限を持っている 要件: mainブランチの変更は最低1人からの承認を必須とする(mainブランチは1人では変更できてはいけない) これを満たすためにbranch protection ruleにおいて&quot;Required number of approvals before merging: 1&quot;が設定されているものとする 登場人物 攻撃方法を検討する上で2人の人物に登場してもらいます。 Alice ソフトウェアエンジニア。日々、コードを書いたりレビューしたりしている。レビューの際にはどんなに巧妙に隠された悪意あるコードも見つけ出すことができる鋭い嗅覚の持ち主。 Mallory 攻撃者。大きな野望を実現するため、とある方法でリポジトリへの書き込み権限を入手し、mainブランチのコードにバックドアを仕掛けようとしている。 Pull requestの役割の整理 実際の攻撃方法を考える前に、PRにおける役割を整理します。 PRはユーザーやbotなどによって作成されます。本記事ではPRの作成者を&quot;PR creator&quot;と呼びます。 PRのソースブランチ(マージ元)に最後にコミットをプッシュしたユーザーを&quot;last commit pusher&quot;と呼びます。多くの場合で &quot;PR creator&quot; == &quot;last commit pusher&quot; ですが必ずしもそうである必要はありません。 今回の条件下ではPRは最低1人から承認されている必要があります。PRを承認したユーザーを&quot;PR approver&quot;と呼びます。PRの作成者は自身が作成したPRを承認できないので &quot;PR creator&quot; != &quot;PR approver&quot; が常に成り立ちます。 PRは承認された後にマージされますが、マージはリポジトリに書き込み権限があれば誰でもでき、今回の攻撃方法の考察には関わってきません。 攻撃パターン0: MalloryがPRを作成しAliceにレビューしてもらう まずは最もシンプルにMalloryが悪意あるコードを含むPRを作成しAliceにレビューしてもらうことを考えてみます。 前述の通り、AliceはPRに含まれる悪意あるコードを持ち前の嗅覚で必ず見つけ出すのでこのPRは承認されず、攻撃は失敗に終わります。つまり、今回攻撃パターンを考える上ではAliceがPR approverとなるパターンは検討する必要がありません。 PR Creator Last Commit Pusher PR Approver Mallory Mallory Alice 攻撃パターン1: Aliceが作成したPRにMalloryがコミットをプッシュし承認する(PR Hijacking) この攻撃方法は次の記事で紹介されているPR hijackingと呼ばれる方法です。 https://www.legitsecurity.com/blog/bypassing-github-required-reviewers-to-submit-malicious-code PRは&quot;PR opener&quot;以外のリポジトリに書き込み権限があるユーザーなら誰でも承認ができるため、誰かが作ったPRに勝手にコミットを追加し、承認してマージすることが可能です。 Aliceは自分が作成したPRに勝手にコミットが追加され、マージされたことに気づく可能性はありますが、このPRがDependabotのようなbotによって作成されていた場合、このことに誰も気付けない可能性があります。 PR Creator Last Commit Pusher PR Approver Alice Mallory Mallory この攻撃は&quot;Require approval of the most recent reviewable push&quot;というオプションを有効化することで防ぐことができます。このオプションを有効化すると&quot;last commit pusher&quot; != &quot;PR approver&quot;という制約を追加することができるのでMalloryはPRを承認できなくなります。 攻撃パターン2: MalloryがPRを作成しGitHub Actionsで承認する リポジトリの設定によっては、GitHub Actionsのワークフローで 自動生成されるGITHUB_TOKEN を使ってPRを承認することができます。GitHub Actionsのワークフローはリポジトリの書き込み権限があれば誰でも作成・追加できるため、Malloryが自身が作成したPRを承認するようなワークフローを作成することも可能です。 GITHUB_TOKENを使ってPRを承認した場合、承認したユーザーは&quot;github-actions&quot;となりMalloryとは別のユーザーがPRを承認したものとして扱われます。 PR Creator Last Commit Pusher PR Approver Mallory Mallory github-actions この攻撃方法は Allow GitHub Actions to create and approve pull requests を無効化することで防ぐことができます。このオプションを無効化すると&quot;PR creator&quot; != github-actions &amp;&amp; &quot;PR approver&quot; != github-actionsという制約を加えることができます。 攻撃パターン3: GitHub ActionsでPRを作成しMalloryが承認する 攻撃パターン2の応用として、MalloryがGitHub ActionsのワークフローでPRの作成とコードの追加を行い、Mallory自身がPRを承認するという方法もあります。 PR Creator Last Commit Pusher PR Approver github-actions github-actions Mallory この攻撃方法も攻撃パターン2と同様に Allow GitHub Actions to create and approve pull requests を無効化することで防ぐことができます。 ここまでのまとめ これまで紹介した攻撃パターンとその他に考えうる攻撃パターンを表にまとめます。 なお、表内の対策1と対策2はそれぞれ次に対応します。 対策1: &quot;Require approval of the most recent reviewable push&quot;の有効化 対策2: &quot;Allow GitHub Actions to create and approve pull requests&quot;の無効化 Attack Pattern PR Creator Last Commit Pusher PR Approver 対策1で防げるか 対策2で防げるか 1 Alice Mallory Mallory ✅ Yes ❌ No 2 Mallory Mallory github-actions ❌ No ✅ Yes 3 github-actions github-actions Mallory ❌ No ✅ Yes 4 github-actions Mallory Mallory ✅ Yes ✅ Yes 5 Mallory github-actions github-actions ✅ Yes ✅ Yes 6 Alice Mallory github-actions ❌ No ✅ Yes 7 Alice github-actions Mallory ❌ No ❌ No 攻撃パターン7: Aliceが作成したPRにMalloryがGitHub Actionsでコミットを追加し、Mallory自身が承認する 表に記載の攻撃パターン1-6はGitHubのオプションを変更することで防ぐことができます。しかし、攻撃パターン7を防ぐ方法は(前提条件を変更しない限り)なさそうです。 具体的にはAliceが作成したPRにMalloryがGitHub Actionsを使って悪意あるコードを追加します。そしてMallory自身がPRを承認しマージします。(コードを追加するPRは必ずしもAliceが作成したPRである必要はなく、Dependabotのようなbotが作成したPRやオープンのまま忘れ去られているPRなどでも問題ありません。このようなPRが使われた場合、攻撃に気づくのは難しいでしょう) PR Creator Last Commit Pusher PR Approver Alice github-actions Mallory 攻撃パターン7を防ぐ方法 この攻撃はPR creator、last commit pusher、PR approverがすべて違うユーザーであり、これまで紹介したオプションを使用しても防ぐことができません。 GitHubが提供する方法でこの攻撃を防ぐにはPRのマージに必要な承認数(Required number of approvals before merging)を2以上に変更することです。しかしこの数を増やすことは開発者の生産性の低下につながり、あまり良い解決策とは言えません。 コードオーナーによるレビューを必須とするオプション(Require review from Code Owners)を使うことによりこの攻撃が行える可能性を減らすことは可能ですが、Malloryがコードオーナーであった場合は依然として攻撃が可能です。このオプションは攻撃の成功確率を下げることができるかもしれませんが、完璧な対策とはなり得ません。 現状、GitHubが提供する機能だけではこの攻撃を防ぐことはできないため、この攻撃パターンに対処したい場合はなんらかの仕組みを独自に開発する必要があります。例として以下のような方法が考えられます。 攻撃パターン7に合致するようなPRがマージされた場合にアラートを上げるような仕組みを作る PRのマージに必要な承認数を2にして攻撃パターン7に該当しない場合はbotがPRを承認し、botと人間1人による承認でPRがマージできるようにする なお、この件については今年5月ごろにGitHubに報告済みであり、意図した挙動であるという回答をもらっています。またこの件をブログに取り上げることについても承諾を得ています。 まとめ 本記事ではGitHubのbranch protectionについて、その回避方法と対策について考察しました。Branch protectionは重要なブランチを守る強力な機能である一方で、GitHub Actionsを利用すると場合によっては突破が可能であり、完全なものではないということもわかりました。本記事が各組織や個人がGitHubをよりセキュアに利用する一助になれば幸いです。
こんにちは。メルコインでソフトウェアエンジニアをしている @goro です。 この記事は、 Merpay &amp; Mercoin Advent Calendar 2024 の記事です。 本記事は自分の所属するチームが管理するマイクロサービスにおいて、ワークフローエンジンであるArgo Workflowsを導入し複数のバッチの制御を行ったので、その際に得た知見を共有します。 Argo Workflowsとは Argo Workflowsは、Kubernetes上で並列ジョブをオーケストレーションするためのオープンソースのコンテナネイティブなワークフローエンジンです。Argo WorkflowsはKubernetesのCRD(Custom Resource Definition)として実装されています。CRDはKubernetes APIを拡張して独自のリソースを定義するものです。 ArgoWorkflowを利用することで、各ワークフローをkubernetesのマニフェストで定義することができます。ワークフローはDAGとしてモデル化されているので、タスク間の複雑な依存関係を表現することができます。 また、ワークフローの各ステップはKubernetesのPodとして実行されます。そのため、ユーザーがArgo Workflowsに詳しくなくても、Kubernetesについての知識があれば比較的容易に構築できます。また通常のKubernetesのPodで特定のタスクを実行できるなら、それをワークフローステップでも同様に実行できるのです。 私たちの開発するシステムはバッチが多く、その依存関係が複雑で、Kubernetes CronJobでこれらのバッチを管理するのが難しくなってきました。そこでワークフローエンジンとしてArgo Workflowsを導入し各バッチを管理するようになりました。 Argo Workflowsのアーキテクチャ 次にArgo Workflowsの内部アーキテクチャについて簡単に説明します。 Argo WorkflowsはWorkflowなどのCRDとWorkflow Controller、Argo Serverという2つのDeploymentで構成されます。Workflow Controllerはリコンサイルを行い、Argo ServerはAPIを提供します。なお、Controllerは単独で使用することもできます。 Workflow Controllerと Argo Server は両方とも Argo Workflows namespaceで実行されます。ワークフローとそこから生成される Pod は別のnamespaceで実行されることが一般的です。 それぞれの役割は以下のようなものになっています Workflow Controllerの役割 Workflow ControllerはKubernetesのCRDを監視します。これにより、新しいワークフローが作成されたり、既存のワークフローが更新されたりしたときに適切に処理します。 ユーザーが定義したワークフローの各ステップをKubernetes上のポッドとしてスケジュールします。これには依存関係の解決や並列実行の管理も含まれます。 各タスクの実行状態をリアルタイムで更新し、ワークフロー全体の進行状況を把握します。成功、失敗、中断などの各状態を管理します。 タスクが失敗した場合の再試行ロジックや、失敗の際の回復手順を実行します。これにより、信頼性の高いワークフロー実行が可能になります。 Argo Serverの役割 ユーザーが操作できるAPIサーバーを提供します。 ワークフローの管理、ログのアクセス、ワークフローのステータス確認のためのユーザーインターフェースとAPIエンドポイントを提供します。 ユーザーとKubernetesのバックエンドをつなぐ役割を果たし、ワークフローの管理と可視化を容易にします。 Argo WorkflowsでWorkflowが実行されるまでの流れは以下のようなものになります。 Userがワークフローをkubectl applyなどを利用しSubmitします。 Argo Serverがワークフローのカスタムリソースを作成します。 Workflow Controllerがそのカスタムリソースを検出します。 Workflow Controllerは、Kubernetes APIを用いてポッドを作成し、ステータスを監視します。このプロセスは、ワークフローが完了するまでループします。Podの実行終了後は次のステップのPodを作成します。 Argo WorkflowsでWorkflowを定義する 今回、自分たちのサービスでArgo Workflowsを利用する上でWorkflowのカスタムリソースは利用していません。WorkflowTemplateというカスタムリソースでWorkflowを定義し、それをCronWorkflowというスケジュール実行したいワークフローを定義するリソースから呼び出す形でWorkflowを実行しています。 それぞれのリソースについては以下に簡単にまとめています。 WorkflowTemplateは、再利用可能なテンプレートとして複数のWorkflowやCronWorkflowで使用できるため、今回はWorkflowTemplateを利用しました。記載方法に関してはkindが異なるだけでWorkflowとWorflowTemplateは同じ内容になるため、WorkflowをWorflowTemplateにリネームするだけで簡単に再利用できるリソースを作成することができます。 Workflow Workflowは、複数のタスクやジョブを定義し、それらを一貫したシーケンスや依存関係に基づいて実行するためのフレームワークやプロセスです。具体的には、下記のような要素を含みます。 タスクの定義: 具体的な作業内容(例えば、データ処理やモデルのトレーニングなど) 依存関係: あるタスクが他のタスクに依存している場合、その順序を定義。 実行環境: タスクが実行される場所(クラウド、オンプレミスなど)。 実行管理: タスクのスケジューリング、リトライ、エラーハンドリングなど。 例: apiVersion: argoproj.io/v1alpha1 kind: Workflow metadata: generateName: hello-world- spec: entrypoint: whalesay templates: - name: whalesay container: image: docker/whalesay:latest command: [cowsay] args: [&quot;hello world&quot;] WorkflowTemplate WorkflowTemplateは、再利用可能なテンプレートとして複数のWorkflowで使用できる、Workflowの定義を含んでいます。具体的には、特定のタスクやシーケンスを標準化して、一貫した方法で繰り返し使用することができます。workflowTemplateRefでWorkflowやCronWorkflowから参照され利用されます。TemplateにはClusterTemplateWorkflowsという全てのnamescpaceで再利用するためのTemplateも存在します。 例: apiVersion: argoproj.io/v1alpha1 kind: WorkflowTemplate metadata: name: hello-world-template spec: templates: - name: whalesay-template container: image: docker/whalesay:latest command: [cowsay] args: [&quot;hello from template&quot;] CronWorkflow CronWorkflowは、Cronジョブのようにスケジュールに基づいて定期的に実行されるワークフローです。具体的には、特定の時間や間隔で自動的に実行されるように設定が可能です。 例: apiVersion: argoproj.io/v1alpha1 kind: CronWorkflow metadata: name: hello-world-cron spec: schedule: &quot;0 0 * * *&quot; # 毎日午前0時に実行 workflowSpec: workflowTemplateRef: name: hello-world-template # 先ほど定義したWorkflowTemplateの名前を指定 またワークフローの依存関係を定義する方法は以下のStepsとDAGの2つの方法があります。 STEPS STEPSでは一連のステップでタスクを定義することができます。テンプレートの構造は「リストのリスト」となっています。外側のリストは順次実行され、内側のリストは並行して実行されます。 以下の例ではA実行後にB-1とB-2がパラレルに実行されます。 - name: step-sample steps: - - name: A template: test - - name: B-1 template: test - name: B-2 template: test DAG DAGは、タスクを依存関係のグラフとして定義することができます。DAGでは、すべてのタスクをリストし、特定のタスクが開始される前に完了しなければならない他のタスクを設定します。依存関係のないタスクは即座に実行されます。 以下の例の場合はA-&gt;B-&gt;C-&gt;Dの順で実行されます。 - name: dag-sample dag: tasks: - name: A template: test - name: B dependencies: [A] template: test - name: C dependencies: [B] template: test - name: D dependencies: [C] template: test これらのCRDと依存関係の定義を利用することで、以下のような順番にタスクを実行するWorkflowを作成することができました。 こちらは実際のArgo Workflows Webコンソール上のスクリーンショットを掲載しています。WebコンソールではこちらのようなWorkflowの実行詳細で各Workflowの実行状況をリアルタイムで確認できます。また、コンソール上から簡単にリトライの実行やログの確認を行うこともできます。さらには定義されたWorkflowTemplateやCronWorkflowの一覧も確認することができ、それらを利用してWorkflowの実行をコンソール上からワンクリックで行うことが可能です。 今回Argo Workflowsを利用して感じたメリット 最後に今回Argo Workflowsの導入により、感じたいくつかのメリットを紹介します。 まず第一に、Argo WorkflowsがKubernetesネイティブなワークフローエンジンであり、各タスクをKubernetesのPodとして実行するため、他のKubernetesリソースと同様に、YAML形式のマニフェストを使ってワークフローを記述できる点が非常に便利でした。これにより開発者は既存のKubernetes知識を活用して比較的簡単にワークフローを構築できました。 さらに、ワークフローはコンソール上で直感的に可視化されるため、それぞれのタスクがどのように接続され、実行されているかを一目で把握できます。タスクの失敗時には容易にリトライが可能で、完了したタスクのログもすぐにアクセスできるため、迅速なトラブルシューティングが実現します。 また、このようなArgo Worksflowsのメリットは開発者以外の方にも大きな恩恵をもたらしました。例えば、QAチームはKubernetesの複雑なジョブ管理の詳細を知らなくても、kubctlコマンドなどを利用せずコンソール上からタスクを実行できるようになり、QAの効率化にも良い影響が発生しました。 まとめ 今回は、Argo Workflowsを利用して複数バッチの制御を行った事例について書きました。今後Argo Workflowsを導入される方の参考に少しでもなれば幸いです。次の記事は kobaryoさんです。引き続きお楽しみください。 参考文献 Argo Workflows Documentation
こんにちは。メルカリのVP of Engineeringの @motokiee です。この記事は、 Mercari Advent Calendar 2024 の12日目の記事です。 1. はじめに メルカリでは、Tech Radar の取り組みを2024年に開始しました。この記事では、メルカリTech Radar 導入の意図、定義の進め方、運用についてご紹介します。 2. Tech Radar とは Tech Radar は、企業が技術選定を効率的に行うためのフレームワークです。元々 ThoughtWorks社 が作成し、採用する技術のガイドラインとして用いられています。Tech Radar は、技術がどの段階にあるかを評価し、採用すべきか (Adopt)、試行すべきか (Trial)、評価途中であるか (Assess)、あるいは非推奨か (Hold) を示しています。 ThoughtWorks について ThoughtWorks は、技術コンサルティングとソフトウェア開発を専門とするグローバル企業で、Tech Radar を始めとする革新的なアイデアを業界に提供してきました。そのためTech Radar は、企業が技術的な選択をする際の考え方として世界的に知られています。 構成要素の説明 Tech Radar は通常、以下の4つの項目で構成されています。 Adopt(採用) : 十分に成熟し、すぐにでも採用する価値がある技術。 Trial(試行) : 有望で特定のプロジェクトで試行すべき技術。 Assess(評価) : 更なる調査と検証が必要な技術。 Hold(非推奨) : 現時点では推奨しない技術。 3. メルカリの Tech Radar について なぜ始めたか メルカリでは、これまでフリマ事業をはじめ多くの新規事業にチャレンジしてきており、同時に新たな技術への挑戦も続けています。数多くの新しいことにチャレンジできた一方でメンテナンス等での課題も発生してきました。 ご経験のある方もいると思いますが、新しい技術を採用したもののメンテナンスが特定の人に委ねられてしまうケースはよくあると思います。特定の人がチームにいる間は開発生産性が高い状態を維持できるのですが、その人が異動や退職をしてチームからいなくなってしまうと、チームからナレッジがなくなりトラブルシューティングやメンテナンスが困難になってしまいます。 こういったトラブルシューティングやメンテナンスは、できる限り Platform Engineering でカバーしていきたいと考えています。しかし、メルカリではエンジニアの大多数は Product Engineering に所属し、Platform Engineering 所属のエンジニアの割合は低いです。そのため、Platform Engineering で無制限にメンテナンスなどを引き受けることはできず、一定の選択が必要となります。 「An Elegant Puzzle: Systems of Engineering Management」や「Staff Engineer: Leadership beyond the management track」などの書籍を執筆した Will Larson さんのブログポストの Magnitudes of exploration を参照してみましょう。 冒頭にサマリとして “Mostly standardize, exploration should drive order of magnitude improvement, and limit concurrent explorations.” と書かれています。日本語訳をすると「主に標準化し、探索は桁違いの改善を促進すべきであり、同時に行う探索を制限する」となります。 意図して選択を行い、限られた持ち物を磨き込み、生産性を劇的に改善し得るものに絞って新しい取り組みを行うことであると解釈しています。Less is More に通ずる考え方で、Tech Radar は過去の取り組みの蓄積でありその証跡となる役割を持っていて、新しい取り組みを始める際の羅針盤となってほしいと考えています。 Tech Radar を導入し活用することで、スピード感をもって一貫性のある技術選定できるようになることを目指しています。 どのように定義したか メルカリのニーズや定義のしやすさに合わせて定義をカスタマイズしています。例えば、 メルカリ Tech Radar は Backend/Platform、 Mobile、 Web の3つのカテゴリに分かれています。この3つの領域に境界を分けて作業を進めるのが効率が良かったためです。 管理には原始的ですがスプレッドシートを利用しています。以下の図は Mobile 領域の languages-and-frameworks の Tech Radar に記載されている内容です。 現在使っている技術すべてに Architecture Decision Records (ADR) のような証跡があるわけではないので、実体としてどのように運用されているかをベースに Adopt などの定義を行いました。 なかには「会社として推奨を続けるべきか」曖昧なものもあり、ADR を作成し議論を行って曖昧な状態を解消したものもあります。 メルカリ Tech Radar で定義したものの大枠は https://engineering.mercari.com/technology-stack/ に反映されています。Adopt / Trial / Assess / Hold などの詳細な定義について現在外部公開する予定はないのですが、Hold \= つまり会社ですでに使うことを推奨していないものは含まれていません。 運用について Tech Radar は定期的に見直しを行い、最新の技術トレンドと社内のニーズに応じて更新していく必要があります。メルカリでは今年から運用を始めたため2025年に見直す予定で、年次の棚卸しのような行事にできると良いと考えています。 Tech Radar 自体の管理に多くの時間は使っていませんが、 Hold になっている、つまり非推奨のアイテムがどのような状態なのかはトラッキングを行っています。特に、非推奨である理由としてサービスのクローズなどの時間的制約があるものについては注意して関係するチームにマイグレーションのリマインドを行ったりしています。 4. おわりに メルカリでの Tech Radar の取り組みについて紹介しましたが、まだ始めたばかりのためこれから課題はまだまだ出てくると思います。 技術標準の設定を行うような制約を設ける取り組みでは「新しいことができなくなってしまう」という意見も出てきますが、Tech Radar に書かれているものしか使えない・使ってはいけないという状況もメルカリらしくないと考えています。 先ほど紹介した「主に標準化し、探索は桁違いの改善を促進すべきであり、同時に行う探索を制限する」が良い指針になると思っており、どの程度の割合で探索を行っていくかを考えることもこれからの宿題だと考えています。 メルカリとして蓄積してきた数々のナレッジや技術的な取り組みを最大限活用してほしい、見過ごしてほしくないと考えています。Tech Radar はメルカリのエンジニアリングとして何に投資をし全体最適化を行っていくかの判断のベースとなるものだと考えています。 Tech Radar で技術スタックの定義されているいくつかの領域では、選択的集中を行うことで、人や時間をより適切に投資することができ始めています。主にはツールの自動化で、開発スピードとエンジニアの満足度を向上させ、事業の Enable に成功しています。 Tech Radar に Adopt と定義されているものは、会社として継続的改善を行うという意思表明だと考えて運用していきたいと考えています。 以上、メルカリ Tech Radar の取り組みについてのご紹介でした。 明日の記事は iso さんです。引き続きお楽しみください。
はじめに こんにちは。メルペイの Growth Platform で Backend Engineer をしている @hiramekun です。 この記事は、 Merpay &amp; Mercoin Advent Calendar 2024 の記事です。 Growth Platformは組織としてはメルペイに所属していますが、メルペイに限定されないさまざまな取り組みを行っています。その一つとして、商品フィードシステムのリアーキテクチャに取り組みました。これにより得られた多くの学びを今回紹介します! 背景 商品フィードとは、オンラインストアや商品カタログの情報を一括して管理し、さまざまな販売チャネルや広告プラットフォームに配信するためのデータ形式や仕組みを指します。メルカリでは多用な商品フィードに商品データを連携し、商品が広告として表示されるようにしており、外部メディアに対する商品訴求において重要な役割を担っています。 例えば、Googleのショッピングタブでは多数のサイトの出品を一覧できますが、メルカリの商品も表示されます。 (出典: GoogleのShoppingタブ ) 課題 歴史的経緯から、連携先ごとに異なる商品フィードシステムが分散して作られ管理されており、このことが多くの課題を引き起こしていました。以下にいくつかの例を挙げます。 システムによって実装・メンテナンス担当チームが異なり、コミュニケーションコストが増加。 商品情報の取得や非表示商品のフィルタリングなど共通する処理があるにもかかわらず、連携先ごとに固有の実装が行われ、システムごとに異なる問題が発生。 システムごとにデータの取得元が異なり、商品の状態変更がリアルタイムにフィードに反映されない場合がある。 達成したい状態 こうした課題を解決するために、一つのシステムで全ての連携先に対する実装を提供することを目標として、商品フィード用のマイクロサービスを立ち上げることになりました。Growth Platormがオーナーシップを持っている他の既存のマイクロサービスに機能を追加するという選択肢もありました。しかし、今回は新たなマイクロサービスを立ち上げることに決め、その理由は以下の通りです: 既存のマイクロサービスの役割がすでに大きくなっており、その役割がさらに曖昧化することを避けるため。 各連携先サービスの特性に合わせてシステム設計を変更する際、他のシステムへの影響を最小限に抑えることができるため。 商品の更新イベントはRPSが高いため、システムの特性に応じてスケーリングが必要となる可能性があるため。 共通のフィルタリング設定や商品データの取得、商品のメタデータ付与などの処理は一つのシステムに統合することが必要です。これは、連携先に依存しない処理であり、修正が全サービスに適用されることを意図しています。 共通実装をまとめる一方、連携先に応じた固有実装は分離する必要があります。新しい連携先サービスを追加する際、必要最低限の差分で実装を完結させることが重要です。特に、外部APIへのリクエスト部分はエンドポイントやレート制限が異なるため、柔軟に変更できるようにしておく必要があります。 また、外部APIリクエストにはエラー対応が欠かせません。全てのエラーをこちらで制御できないので、常にエラーが発生する可能性を念頭に置き、リトライ可能な設計を採用しています。 技術的アプローチ アーキテクチャ 具体的なアーキテクチャを紹介します。大枠としては、共通処理を担当するworkerと、連携サービス固有のworker(Batch Requester)に処理を分け、これらをPub/Subでつなぐ設計としました。この設計により、次のような利点があります: 各workerのシステム特性に応じたスケーリングが可能。 他のマイクロサービスへのリクエストと外部APIへのリクエストを分離し、外部APIの予測困難な動作を切り離すことが可能。 新しいbatch requesterをPub/Subのsubscriberとして追加することで、共通実装部分を変更せずに新しい連携サービスを追加可能。 商品の状態更新イベントが急激に増えた際に、Pub/Sub Topicがメッセージキューとしてシステムの安定性を高めることができる。 大枠としては、共通処理部分のworkerと連携サービス固有のworker (Batch Requeseter) にworkerを分けて実装し、その二つをPub/Subで繋げるという設計にしました。こうすることで次のような利点があると考えました。 workerそれぞれのシステム特性に応じたスケーリングが可能になる 外部APIにおける不確実な挙動を他のマイクロサービスから切り離すことができる 新しいbatch requesterをPub/Sub Topicへのsubscriberとして追加することで、共通実装部分には手を加えずに連携サービスを追加することがきる 商品の状態更新イベント数スパイクした時にPub/Sub Topicがメッセージキューとしてシステムの安定性を高める ではそれぞれのworkerについてもう少し詳しく説明します。 共通処理部分のworker 共通処理部分のworkerは、商品の状態更新イベントとして別サービスからPub/Sub Topicに流れてくるデータを受け取ります。このTopicをsubscribeしてイベントをリアルタイムに受信し、他のマイクロサービスにリクエストを送ることで追加の商品情報を付与したり、フィルター設定を参照して不適切な商品を除外します。この結果、処理された商品情報をマイクロサービス内でのみ用いるPub/Sub Topicにpublishします。 このworkerにはHPA(Horizontal Pod Autoscaler)が設定されており、CPU使用率に基づいてPod数を動的に調整します。 サービス固有のworker (Batch Requester) 次に、その商品情報を受け取る側の実装です。フィード用にカスタマイズされた商品情報のPub/Sub Topicを、連携サービスごとにデプロイされた固有のbatch requesterがsubscribeします。 batch requesterは、外部APIへのリクエストを秒単位で継続的に実行する必要があります。そのため、Go言語で実装されたPodをCronJobではなくDeploymentとしてデプロイしています。Deploymentを使用することで、より細かい時間間隔でタスクを実行でき、必要に応じたスケーリングも柔軟に対応できます。 エラーハンドリングも重要です。外部APIの一時的なエラーやネットワークエラーでリクエストが失敗することがあるため、retry機能を実装しました。本システムではPub/Subのretry機構を活用し、以下のように機能します: batch requesterがPub/Subからメッセージを受け取り、インメモリにバッチとして保存。 一定間隔でそのバッチを外部APIに送信。 送信が成功した場合、そのバッチに含まれる全ての商品に対応するPub/Subメッセージをack。 送信が失敗した場合、全ての対応メッセージをnackし、Pub/Subがメッセージを再送。 商品の状態をなるべくリアルタイムでフィードに反映したいため、ある一定の回数retryに失敗した場合はDead-letter topicに転送し、後続のリクエストを優先させます。 SLOとしては、商品フィードに正しく反映されている商品の割合を確認しています。今のところこのSLOは達成できているので、Dead-letter topicに溜まっている商品を再試行するためのジョブは必要ありませんが、将来的にはそうしたジョブを作ることも検討しています。 最後に この商品フィードシステムを構築したことで、商品をよりリアルタイムに近い形でフィードに配信できるようになりました。また、共通の実装と各連携先の特有実装を分けることで、新しい連携先の追加がより簡単になりました。今後は新たな連携先の追加や、フィードデータのカスタマイズを進めていく予定です。 次の記事は@goroさんです。引き続きお楽しみください。