TECH PLAY

株式会社メルカリ

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

261

はじめに こんにちは! 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さんです。引き続きお楽しみください。
アバター
こんにちは。メルカリのソフトウェアエンジニアの @sintario_2nd です。 この記事は、連載: メルカリ ハロ 開発の裏側 &#8211; Flutterと支える技術 &#8211; の4回目と、 Mercari Advent Calendar 2024 の10日目の記事です。 この記事について メルカリ ハロは 2024年3月6日にサービスを開始しました。サービスローンチ後は開発チームの体制が変わり、わたしはGrowth Hackチームに配属されて Customer Relationship Management (CRM) のツールのインテグレーションに関わってきました。いろいろと一筋縄にはいかないこともあり試行錯誤したなかで、今回はプッシュ通知周りで遭遇した課題について、どのような調査を経てどのように解決したか、をご紹介したいと思います。 メルカリ ハロのプッシュ通知の当初構成 メルカリは各サービスへの通知を管理する microservice (Notification Service と呼ぶことにします) をすでに持っており、メルカリ ハロのバックエンド (Hallo Backend) から Notification Service 経由で Firebase Cloud Messaging (FCM) によるプッシュ通知が送られるという構成になっています。Hallo Backend からのメッセージがアプリの載っている端末にプッシュ通知として届くまでの図式としては下図のようになります。 サービスローンチまでは非常に短い時間であったということもあり、まずは最低限の機能を実現するべく、 Flutter で実装されたアプリ側は FlutterFire を使用して FlutterFire のガイドどおりに素直に実装されていました。 Braze を組み込む サービスローンチ後は一般的にどんなサービスでも利用拡大を目指していくものかと思います。そのため、一定のターゲット属性に当てはまる利用者群に向けてキャンペーンのメッセージをお届けする、といった CRM 施策がよく行われます。メルカリ ハロでは、CRMの分野ではよく使われている Braze を採用しています。 Braze は各種のメッセージ手段でキャンペーンを実施する仕組みを持っていて、 In-App Messaging / Content Cards / e-mail / プッシュ通知 などに対応しています。この分野では老舗ということもあり、 Flutter向け Braze SDK の組み込みガイド も提供されていたので書かれた通りに作業すればすんなり動くものと高をくくっていました。実際 IAM などは軽作業のみですんなり使えて拍子抜けしましたが、プッシュ通知については我々がもともと実装済みだった構成との兼ね合いで、想定よりも苦戦することになりました。以下では Android にフォーカスしてご紹介したいと思います。 Braze は Android のプッシュ通知には FCM を使う(※)ので、Braze がサービスに組み込まれると下図のように FCM の送信をトリガーする経路が2系統になります。アプリから見るとプッシュ通知は一律 FCM が送ってくることになるのでただ受信するだけなら単純なわけですが、実際にはそれぞれのプッシュ通知の着信率や開封率を知りたかったり、通知タップ後に何かしら特別なアクションがしたかったりしますよね。そのため受信したプッシュ通知が Hallo Backend 由来か Braze 由来かはアプリ側で通知の中身を見分け、仕分ける必要があります。 ※ iOS については Braze は APNs を直接扱うので少し異なるデータフローになりますが、本題からそれるので今回は説明省略します。 実際、なにも考えずに組み立て終わってレビューと機能検証をしていると、エンジニアの同僚やQAから「 プッシュ通知の2重受信が起きることがあるみたいだ 」と指摘されました。Braze SDK側は Braze 由来のプッシュ通知かどうかを見分けられるものの、もともとあった Hallo Backend 由来のプッシュを扱う実装が Braze からのプッシュ通知を見分けることができておらず、 Braze 由来のプッシュ通知が Hallo Backend 由来のプッシュ通知用のハンドラにも処理される2重ハンドリングが起きていました。 Braze からするとこういった FCM 発行者が複数になるパターンはよくある事案として想定されているようで、 Braze と関係ないメッセージを fallback で扱うための FirebaseMessagingService をAndroid プロジェクト側で指定できる ようになっています。braze.xml というリソースファイルに下記のように fallback の有効化と fallback サービス名を指定することになります。 &lt;bool name=&quot;com_braze_fallback_firebase_cloud_messaging_service_enabled&quot;&gt;true&lt;/bool&gt; &lt;string name=&quot;com_braze_fallback_firebase_cloud_messaging_service_classpath&quot;&gt;com.company.OurFirebaseMessagingService&lt;/string&gt; またこれを前提にしているからか、Dart 向けの braze_plugin.dart では RemoteMessage が Braze 由来のものかを判別するメソッドを提供していないようです。 ということは、FlutterFire が Dart 層までメッセージを流し込む役目をしている FirebaseMessagingService の実装クラスをこの fallback サービスに指定すればいいのかな、、、となるわけですが、 アプリの AndroidManifest.xml をみてもそんなクラスはどこにも見当たらず、 🤔どうしたものか、となったわけです。 FlutterFire を読む 読者の皆さんはもうお気づきでしょうが、私たちは今 Flutter (Dart の世界)の範疇では解決できない領域に足を踏み入れました。ライブラリの Dart の実装部分だけを読んでいてもどうにもなりません。ネイティブアプリの開発知識を駆使して向き合う必要があります。 プッシュ通知のようなネイティブの機能と密接に関わるものはライブラリにもネイティブ実装部分があります。Android の場合は FCM のハンドリングをするサービスを AndroidManifest.xml に宣言する必要があり、アプリ側の manifest ではなくライブラリ側にも AndroidManifest.xml があってビルド時にマージされるので、つまりは FlutterFire のリポジトリにある AndroidManifest.xml を調べてみるのが良いだろう、と当たりをつけたわけです。 ありました。 https://github.com/firebase/flutterfire/blob/_flutterfire_internals-v1.3.35/packages/firebase_messaging/firebase_messaging/android/src/main/AndroidManifest.xml &lt;manifest xmlns:android=&quot;http://schemas.android.com/apk/res/android&quot; package=&quot;io.flutter.plugins.firebase.messaging&quot;&gt; &lt;uses-permission android:name=&quot;android.permission.INTERNET&quot;/&gt; &lt;uses-permission android:name=&quot;android.permission.WAKE_LOCK&quot;/&gt; &lt;uses-permission android:name=&quot;android.permission.ACCESS_NETWORK_STATE&quot;/&gt; &lt;!-- Permissions options for the `notification` group --&gt; &lt;uses-permission android:name=&quot;android.permission.POST_NOTIFICATIONS&quot;/&gt; &lt;application&gt; &lt;service android:name=&quot;.FlutterFirebaseMessagingBackgroundService&quot; android:permission=&quot;android.permission.BIND_JOB_SERVICE&quot; android:exported=&quot;false&quot;/&gt; &lt;service android:name=&quot;.FlutterFirebaseMessagingService&quot; android:exported=&quot;false&quot;&gt; &lt;intent-filter&gt; &lt;action android:name=&quot;com.google.firebase.MESSAGING_EVENT&quot;/&gt; &lt;/intent-filter&gt; &lt;/service&gt; &lt;receiver android:name=&quot;.FlutterFirebaseMessagingReceiver&quot; android:exported=&quot;true&quot; android:permission=&quot;com.google.android.c2dm.permission.SEND&quot;&gt; &lt;intent-filter&gt; &lt;action android:name=&quot;com.google.android.c2dm.intent.RECEIVE&quot; /&gt; &lt;/intent-filter&gt; &lt;/receiver&gt; &lt;service android:name=&quot;com.google.firebase.components.ComponentDiscoveryService&quot;&gt; &lt;meta-data android:name=&quot;com.google.firebase.components:io.flutter.plugins.firebase.messaging.FlutterFirebaseAppRegistrar&quot; android:value=&quot;com.google.firebase.components.ComponentRegistrar&quot; /&gt; &lt;/service&gt; &lt;provider android:name=&quot;.FlutterFirebaseMessagingInitProvider&quot; android:authorities=&quot;${applicationId}.flutterfirebasemessaginginitprovider&quot; android:exported=&quot;false&quot; android:initOrder=&quot;99&quot; /&gt; &lt;!-- Firebase = 100, using 99 to run after Firebase initialises (highest first) --&gt; &lt;/application&gt; &lt;/manifest&gt; FlutterFirebaseMessagingService というのがそれなのかな?と思って開いてみると、これ自身は FCM token のリフレッシュを Dart の世界に引っ張り込む入口程度の薄いクラスなのがわかります。 public class FlutterFirebaseMessagingService extends FirebaseMessagingService { @Override public void onNewToken(@NonNull String token) { FlutterFirebaseTokenLiveData.getInstance().postToken(token); } @Override public void onMessageReceived(@NonNull RemoteMessage remoteMessage) { // Added for commenting purposes; // We don&#039;t handle the message here as we already handle it in the receiver and don&#039;t want to duplicate. } } We don&#039;t handle the message here as we already handle it in the receiver and don&#039;t want to duplicate. というコメントの通り、 RemoteMessage としてやってきているはずの通知メッセージの実処理は、実は FlutterFirebaseMessagingReceiver のほうが担っています。 なお、 &lt;receiver android:name=&quot;.FlutterFirebaseMessagingReceiver&quot; android:exported=&quot;true&quot; android:permission=&quot;com.google.android.c2dm.permission.SEND&quot;&gt; &lt;intent-filter&gt; &lt;action android:name=&quot;com.google.android.c2dm.intent.RECEIVE&quot; /&gt; &lt;/intent-filter&gt; &lt;/receiver&gt; という記述を見て Android 開発のキャリアの長い方々はお気づきでしょうが、これは FCM ではなくそれ以前にプッシュ通知に使われていた GCM という仕組みの BroadcastReceiver の実装になります。現在は直接的にアプリや一般のライブラリがこれを使用するのは非サポートとなっているので、ご注意を。 FlutterFire の AndroidManifest に登場していた他のクラスについても少し触れておくと FlutterFirebaseMessagingBackgroundService FlutterFirebaseMessagingReceiver がバックグラウンドでプッシュ通知を受け取った場合に内部的にこのサービスを enqueue して、Dart の世界に通知を取り次ぐ役割をします。 BroadcastReceiver が長時間かかる処理をしないようにこのような構成になっているものと思われます。 FlutterFirebaseMessagingReceiver を使う限りは AndroidManifest に宣言しておかないといけないもの、ということになります。 こうして以下のことがわかったわけです。 FlutterFire は FirebaseMessagingService の実装クラスを持っていたが、困ったことに onMessageReceived(RemoteMessage) の実装が空なので Braze からの fallback にはそのまま使えない。 また FlutterFirebaseMessagingReceiver を単純に組み込むとすべてのメッセージが Dart の世界まで引き込まれてしまう。フィルターアウトしたいメッセージの選別する手段を差し込む口が提供されていない。 さてどうしよう。。。となりました。 Braze SDK も読んでみる 困ったときは実装を読み込むしかないな、ということで Braze SDK の中も見てみます。 Flutter 向けには braze_plugin として提供されていますが、こちらも各OS向けのネイティブライブラリに依存していて、 Android については braze-android-sdk の android-sdk-ui モジュールにプッシュ通知周りの実装が入っているのがわかりました。 このあたり です implementation &quot;com.braze:android-sdk-ui:30.4.0&quot; Braze 自身は標準的な FCM の仕組みに則っていて、 Kotlin で実装された open class の BrazeFirebaseMessagingService により FCM Token と RemoteMessge がハンドリングされるようになっています。このクラスは必ず必要ということになります。うーん。。。ただ onMessageReceived はすごく薄い実装になっていて override fun onMessageReceived(remoteMessage: RemoteMessage) { super.onMessageReceived(remoteMessage) handleBrazeRemoteMessage(this, remoteMessage) } handleBrazeRemoteMessage(this, … という記述から Java や Kotlin に詳しい方にはお察しいただける通り、このクラスは Java の static method (Kotlin の companion object の method)としてメッセージのハンドリング実体を提供する /** * Consumes an incoming [RemoteMessage] if it originated from Braze. If the [RemoteMessage] did * not originate from Braze, then this method does nothing and returns false. * * @param remoteMessage The [RemoteMessage] from Firebase. * @return true iff the [RemoteMessage] originated from Braze and was consumed. Returns false * if the [RemoteMessage] did not originate from Braze or otherwise could not be handled by Braze. */ @JvmStatic fun handleBrazeRemoteMessage(context: Context, remoteMessage: RemoteMessage): Boolean { if (!isBrazePushNotification(remoteMessage)) { およびその冒頭でちらっと見えていますが Braze 由来の RemoteMessage かを判別する static method の /** * Determines if the Firebase [RemoteMessage] originated from Braze and should be * forwarded to [BrazeFirebaseMessagingService.handleBrazeRemoteMessage]. * * @param remoteMessage The [RemoteMessage] from [FirebaseMessagingService.onMessageReceived] * @return true iff this [RemoteMessage] originated from Braze or otherwise * should be passed to [BrazeFirebaseMessagingService.handleBrazeRemoteMessage]. */ @JvmStatic fun isBrazePushNotification(remoteMessage: RemoteMessage): Boolean { を発見できました&#8230;!! これでなんとかなりそうです。 解決編 いろいろやり方はあると思いますが、以下のような方法をとりました。 Step1: FlutterFire の FlutterFirebaseMessagingReceiver を tools:node=”remove” を使って除去 FlutterFirebaseMessagingReceiver が直接 RemoteMessage を拾ってしまう限りは Braze 由来のメッセージを誤ってハンドリングするのを防げないので、 Managing manifest files にしたがって以下の記述をアプリ側の AndroidManifest.xml に追加し、 FlutterFire によって宣言されてしまう分を除去させます。 &lt;receiver android:name=&quot;io.flutter.plugins.firebase.messaging.FlutterFirebaseMessagingReceiver&quot; xmlns:tools=&quot;http://schemas.android.com/tools&quot; tools:node=&quot;remove&quot; android:exported=&quot;true&quot; android:permission=&quot;com.google.android.c2dm.permission.SEND&quot;&gt; &lt;intent-filter&gt; &lt;action android:name=&quot;com.google.android.c2dm.intent.RECEIVE&quot; /&gt; &lt;/intent-filter&gt; &lt;/receiver&gt; Step2: BrazeFirebaseMessagingService の派生クラスとして HalloFirebaseMessagingService を実装 BrazeFirebaseMessagingService 相当の機能はすべて残さないといけないし、一方で FirebaseMessagingService の実装クラスが複数いて読解に苦慮するのもなと思ったので、以下のような薄いクラスを書きました。 /** * We are using both FlutterFire and Braze to handle push notifications. * Unfortunately we found that Braze notifications could be handled twice * when integrating both of them following their official ways simply, * therefore we resolved the issue by having our own [FirebaseMessagingService]. * * # About Token refresh * * This class inherits from [BrazeFirebaseMessagingService] intentionally, * since [BrazeFirebaseMessagingService.onNewToken] is needed to register a new FCM token to Braze. * Note that our notification service will also receive a new FCM token in dart layer * via [FlutterFirebaseMessagingService.onNewToken]. */ class HalloFirebaseMessagingService: BrazeFirebaseMessagingService() { /** * FCM from Hallo backend through notification service should be handled by * [FlutterFirebaseMessagingReceiver] which will redirect messages to dart layer. * But braze_plugin provided by Braze for Flutter users doesn&#039;t provide any measure * to filter Braze messages, therefore we need to filter out Braze messages in native layer before * [FlutterFirebaseMessagingReceiver] works. * * [BrazeFirebaseMessagingService] can have a fallback service to handle messages from other than * Braze, but we don&#039;t use the mechanism to not have multiple [FirebaseMessagingService] * implementations. * * Fortunately [BrazeFirebaseMessagingService.handleBrazeRemoteMessage] is provided publicly * to construct own FCM handling. If it doesn&#039;t consume FCM then we should delegate it to * [FlutterFirebaseMessagingReceiver.onReceive]. * * Also we removes [FlutterFirebaseMessagingReceiver] from AndroidManifest * to prevent it from handing FCM directly. */ override fun onMessageReceived(remoteMessage: RemoteMessage) { if (handleBrazeRemoteMessage(this, remoteMessage)) { return } FlutterFirebaseMessagingReceiver().onReceive(this, remoteMessage.toIntent()) } } handleBrazeRemoteMessage が Braze 由来のメッセージでないとみなしてハンドルしなかった場合は FlutterFirebaseMessagingReceiver のインスタンスを直接作って FlutterFire の処理に乗せ直す、ということをやっています。 AndroidManifest には FlutterFirebaseMessagingReceiver がもう宣言されていない状態で、この場で必要なときだけ明示的に仕事を渡す、というかたちにしました。 Step3: アプリの AndroidManifest.xml に、 BrazeFirebaseMessagingService のかわりに HalloFirebaseMessagingService を宣言 &lt;!-- &lt;service android:name=&quot;com.braze.push.BrazeFirebaseMessagingService&quot; android:exported=&quot;false&quot;&gt; &lt;intent-filter&gt; &lt;action android:name=&quot;com.google.firebase.MESSAGING_EVENT&quot; /&gt; &lt;/intent-filter&gt; &lt;/service&gt; --&gt; &lt;service android:name=&quot;.HalloFirebaseMessagingService&quot; android:exported=&quot;false&quot;&gt; &lt;intent-filter&gt; &lt;action android:name=&quot;com.google.firebase.MESSAGING_EVENT&quot; /&gt; &lt;/intent-filter&gt; &lt;/service&gt; これでめでたく Braze 由来のメッセージは2重ハンドルされることがなくなり、 Hallo Backend 由来のメッセージも従来通り動作する状態を実現することができました。 結び メルカリ ハロは Flutter アプリです。画面や機能の大部分は1ソースで Android / iOS 両方を実現しており、開発コストの圧縮と十分な性能の両立に Flutter という技術選択が寄与しました。それでも、 Flutter の範疇で解決できない問題に直面するケースはまれにあります。プッシュ通知のようなネイティブの処理に強く影響を受ける個別性の高い機能を扱うなかで、外部提供のSDKやライブラリの組み合わせによって発生した課題を、ライブラリのネイティブ実装部分を解読し工夫することによって乗り切った事例をご紹介させていただきました。 この話は iOS 編も実はありまして、method swizzling 満載の Objective-C を読むもう少し大変な冒険譚があるのですが、今回は Android だけで盛りだくさんになってしまいましたので、またの機会があればお話したいと思います。 明日の記事は danny さんと simon さんです。引き続きお楽しみください。
アバター
はじめに こんにちは。メルカリMarketplace SRE Tech Leadの @mshibuya です。 この記事は、 Mercari Advent Calendar 2024 の9日目の記事です。 自身が所属するMarketplace SREチームは、メルカリグループ全体としてのPlatformを提供するPlatform Divisionに所属しています。この記事では、サービスの信頼性を支えるProduction Readiness Checkと呼んでいるプロセスに対して行った改善と、その結果もたらされた開発者体験について取り上げます。 サービスが必要十分な信頼性を持つことの重要性は広く認識されていると考えます。が、そのための取り組みは地道かつ手間がかかるものであり、ともすればそのためのプロセスの存在によって開発スピードを落としてしまうという結果につながりかねません。このたび行われたProduction Readiness Checkプロセスへの改善について、プロセスのどのような側面に着目して改善を行ったか、その結果どのような開発者体験へつながることを目指したかをご紹介します。同種の取り組みを行う皆様の参考となれば幸いです。 Production Readiness Checkとは メルカリには、Production Readiness Check(略してPRC)と呼ばれるプロセスがあります。これは新しく開発されたプロダクトやマイクロサービスが満たすべき一連の基準を集めたチェックリストで、これに合格しないと本番環境において運用開始することはできません。 過去のブログ記事において 概要の紹介 があるほか、チェック項目そのものについても最新のものではありませんが GitHub上にて公開 されています。 メルカリにおいては広くマイクロサービスアーキテクチャを採用しています。フリマアプリ「メルカリ」や、スマホ決済サービス「メルペイ」といったすでに大きな規模となったサービスにおいては多くの機能追加がマイクロサービスの新規開発という形で進みますし、「 メルコイン 」や「 メルカリ ハロ 」といった新たに立ち上がるプロダクトについても「メルカリ」・「メルペイ」同様のマイクロサービス基盤における1マイクロサービスとしての形を取ります。したがって、マイクロサービスの新規立ち上げというのは日常的に発生します。またDevOpsにおけるYou build it, you run itの原則に従い、それらが本番での運用に耐える信頼性を持つよう担保していく責任も個々のマイクロサービス開発チームにあります。 マイクロサービス開発チームは必ずしもこうした新規のサービス立ち上げや、そのために必要な信頼性の担保に精通しているとは限りません。開発チームがマイクロサービスの立ち上げを自律的に行いつつも必要な信頼性を担保することがProduction Readiness Checkプロセスの狙いです。 解決したい課題 Production Readiness Checkは、メルカリにおいて開発されるサービスがお客さまからの実トラフィックを受け稼働していくために十分な信頼性を持っている(i.e. Production Readyである)ことを担保するために欠かせない役割を果たしてきました。その一方で、次第にこのチェックプロセス自体の運用が開発者にとって重荷となりつつあったことは否定できません。 メルカリにおけるProduction Readiness Checkプロセスは、チェックリストを含むissueを作成することで開始され、issueのcloseとともに終了となります。このissueのopen-closeまでの全期間で実作業が発生しているわけではないので参考値とはなりますが、約5年間のデータでは平均35.5日を要していました。 また、Platform Divisionで行っている開発者インタビューにおいても、Production Readiness Checkプロセスへの不満が多く寄せられている状況でした。 社内開発者からのコメントの例: Did PRC as well, lots of “copy this, paste this, take a screenshot of this…” Overall straightforward, just PRC was a pain PRC, takes about 4 weeks Takes a lot of time Personal opinion is that 1-2 sprints could be cut by simplifying the PRC process Too many things to check, some things are hard to understand how to verify 最もやりたくない仕事のひとつ。必要なのはわかる メルカリグループでは、新規プロダクト立ち上げや既存プロダクトへの機能追加においてスピードがこれまで以上に重視されるようになっており、このProduction Readiness Checkプロセスを高速化し、デリバリーにかかる時間を短縮し省力化することは喫緊の課題とみなされるようになりました。 既存のプロセスにおける開発者体験 ここでは新規プロダクトの立ち上げを例に取り、改善前のProduction Readiness Checkプロセスにおける典型的な体験を示します。エピソードはすべて架空のものなので、最悪のケースでは開発者がこんな体験をしていた可能性がある…という程度のものとしてお読みください。 メルカリグループではとある新規のプロダクトを立ち上げることとなりました。このプロダクトはフリマアプリとしてのメルカリとのシステム連携を含むものとなっており、お客さまがスムーズにプロダクトを使っていただくのに十分な信頼性を有している必要があります。 早速開発チームが立ち上げられ、6ヶ月間でのサービス提供開始を目指して怒涛の開発がスタートします。まずチームはプロダクト上の要件を明らかにし、それをシステム上の実装に落とし込むための設計を行い、Design Docの形でまとめます。出来上がった設計を元に実際のアプリケーションコードの実装が順調に進んでいき、リリースも間近の5ヶ月目に大半の機能の実装を終えることができました。 さて、チームは実際にプロダクトのリリース準備に入ります。本番で利用するインフラ環境構築を進めていくのですが、このあたりでチームはProduction Readiness Checkプロセスの存在に気づくのです。このチームは入社してまもないメンバーや既存サービスの定常的な開発に関わってきたメンバーが多かったため、Checkプロセスの必要性を見落としていたのです。これを全部満たすことがプロダクトのリリースに必須と聞いたチームは全力で対応するのですが、対応が必要な項目数自体が多いこともあり、もともと設計上の考慮に含まれていなかった要件なども判明し難航します。 結果、チームはProduction Readiness Checkを完了するのに2ヶ月を要してしまい、その分プロダクトのリリースを延期せざるを得ませんでした。必要な信頼性を満たせていなかった以上仕方ないのですが、その分プロダクトを早く世に出しお客さまからフィードバックをいただくチャンスを失ってしまいました。 解決策 チェックの自動化 プロセスに労力がかかる要因としてまず挙げられるのが、チェックが必要な項目数そのものが多いこと、またそれが増加するトレンドにあることです。 この種のチェックリスト全般に言えることではありますが、時間経過とともにチェック項目は増加する傾向にあります。なにかトラブルが発生した際の原因としてとある設定を持っていなかったことが指摘され、その再発防止策がチェックリストに追加されるという流れが必然的に発生するためです。 こうした対応がその場しのぎの安易な追加とならないよう抑制的に運用されていたとは理解していますが、それでも典型的なサービスにとってのチェック項目数は公開版の時点では62項目であったものが最新の内部版では71項目となり、約3年間で15%近く増加しています。 また、チェックリストに含まれる項目には「どのような状態を満たす必要があるのか」の定義はあっても、「どのような手段が使われていれば満たされていると見なせるのか」についての言及が十分にはされていないものもあります。このこと自体はマイクロサービスアーキテクチャにおける個々のサービスでの技術選定の柔軟性に配慮した結果ではあると考えられますが、「この確認ができればOK」という基準が明確に示されない状態は実際にチェックリストの対応を行う担当者としても、それをレビューする側としても、都度の検討が必要で負担の大きなものとなっていました。 こうした状況を緩和し労力を削減するため、Production Readiness Checkプロセスにおけるチェックの部分的な自動化を推進しました。プロセスにおけるチェック対象はアプリケーションソースコードやインフラ設定など多岐にわたりますが、自動化によるチェック実装が容易に行えるものから実装を始め、現在はチェック項目の半数近く、45%ほどの項目においてなんらかの自動化チェックが行われるようになっています。 この結果、少なくとも自動化済みの部分に対しては開発者が容易にチェックを行うことができ、求められる状態とのギャップを埋めるためのアクションを取れる状態となっています。自動化チェック自体が「どのような手段が使われていれば満たされていると見なせるのか」の基準を内包しているため、レビュワーにとっても迷いなく判断を行えるようになりました。 既存のコンポーネントによるProduction Readiness Check対応の拡充 過去にも様々な機会で発信されているように 、メルカリにおいてはPlatform Engineeringの考え方が、プラクティスとして広く実践されています。セルフサービスを重視したPlatformによって開発者生産性を高めていく思想のもと、Platform Divisionはこれまでも多くのコンポーネントを構築し、開発者に提供しています。 Production Readiness Checkプロセスの負荷が高い原因を探る過程において、我々はそれによって求められている要件と、実際にPlatformとして提供しているコンポーネント群が持つ機能にギャップが存在していることを認識しました。 メルカリのPlatformにおいては、SDLCの全領域において、開発者が効率的に必要な目的を達成できるよう様々なコンポーネントを提供しています。それらのコンポーネントが十分にカバーできていない領域ながら、Production Readiness Checkプロセスにおいて大きな労力を要している部分を特定し、そのギャップを満たすためのコンポーネントの機能拡充を行いました。 またより重要かつ費用対効果の高い改善として、それらのコンポーネントによって満たされるProduction Readiness Check上の要求を明確化するようドキュメントの改善を行っています。 今回の取り組みを通じた気づきとして、開発者がマイクロサービスを構築していく上で避けて通れないProduction Readiness Checkプロセスに対し、こういったコンポーネントをいかに統合し全体としての開発者体験を作れるかが重要という点があります。単にコンポーネントを提供するだけではなく、その先にあるチェックプロセスそのものを改善したことで、相互のフィードバックループが機能する状態を作れたのではないかと考えています。 &quot;Shift-Left&quot;によるアプローチ ここでいう&quot;Shift-Left&quot;はソフトウェアテストやセキュリティの文脈でよく使われる考え方で、例えばテスト実施のような何らかの活動をより早い段階(=タイムライン図における左側)に動かすことを指しています。 先にあげた新規プロダクト立ち上げの失敗例においては、チームはプロダクトをリリースする直前、短期間にProduction Readiness Checkプロセスを行うことを試み、その多大な労力ゆえに困難に直面しています。個人的にはこの手の状況を「夏休みの宿題ギリギリ問題」と呼ぶのですが、これはチームが怠惰だからそうなってしまったのではなく、構造的な問題があると考えます。新たなプロダクトを立ち上げるにあたっては数多のチャレンジ・困難が存在しており、それらを解決していくなかで、重要と知りつつも喫緊の必要がないものは先送りせざるを得ないからです。 この問題に対処するためには、仕組みレベルでの改善が必要と考えました。前項の自動化が達成された今、チームは少なくとも自動化されたチェック項目に対しては最小限の労力で何度もチェックを行い、漸進的に要件を満たしていくことが可能となっています。また、既存のコンポーネントによるProduction Readiness Check対応も拡充され、早い段階よりそうしたコンポーネントの採用を進めておくことによって、労せずに必要な要件をあらかじめ満たしておけるようになりました。仕上げとして、これらの施策の存在をチームが開発初期の段階から認識できる状態をつくることで、リリース直前の短期間に作業が集中することを防げると考えました。 とはいえ、単にそうした新プロセスや解決策の存在を周知したところで、声が届く範囲には限界があります。そこで、既に確立された、開発初期に必ず発生する別のプロセスの中に入れ込むことで、開発初期のチームがその存在を漏れなく認識できるよう工夫しています。 メルカリでは新規システムの設計にあたってはDesign Docを作成し関係するチームのレビューを受ける文化が根付いています。そのDesign Docを作成する元となるテンプレートにおいて、Production Readiness Checkの存在そのものや、その中でも早期からの考慮が必要な項目について記載し注意を促すこととしました。 これら一連の&quot;Shift-Left&quot;によるアプローチの結果として、開発者が実際の開発やインフラ環境の構築に着手するずっと前、設計の段階からそれらの要件を知り、Production Readiness Checkプロセスへ向けより早期に意味のあるアクションが取れるようになりました。 新たなプロセスによる開発者体験 プロセス改善、そして自動化を取り入れたProduction Readiness Checkプロセスによって、先の架空の新規プロダクト立ち上げにおける開発者体験がどのように変わるかを見ていきましょう。 まずは、Shift-Leftの結果として、チームは6ヶ月の開発期間における最初の段階、設計を行いDesign Docを作る際にProduction Readiness Checkプロセスの存在を知ります。そこで注目すべき要件を理解し、設計段階から考慮しておくことで、必要条件を満たすための根本的なアクション(例えばプロダクト上の要件変更についてステークホルダーと相談するなど)に踏み込むことも可能になります。 5ヶ月目、いよいよプロダクトのリリースが近づき、チームはProduction Readiness Checkプロセスの準備を始めます。事前の想定に基づき要求を満たすために適切なPlatformのコンポーネントを選定済みなので、要求を満たすために新たに行う必要のある変更・作業は最小限に抑えられています。 Production Readiness Checkにおけるチェック項目が満たせているかどうかの確認は、自動化によって大幅に労力が削減されました。おかげでチームは1ヶ月間でProduction Readiness Checkプロセスを完了することができ、お客さまに早く価値を届けフィードバックを得ることで更にプロダクトを磨き上げていくことができるようになりました。 今後の展望 このようにProduction Readiness Checkプロセスは一定の改善を受けており、実際のマイクロサービスリリース前におけるチェックにも利用されはじめています。一方、既存のコンポーネントによるProduction Readiness Check要求項目の対応状況や、自動化における対応可能ケースの拡充にはまだまだ改善の余地があり、より良い開発者体験を目指し当面の間注力していくべき領域になると想定しています。 それらの改善を推し進めていった先には何があるでしょう? 筆者個人としては、「チェックを行う」という考え方自体が不要になることが理想的だと考えています。Platformが提供する機能・コンポーネントによって標準でほぼ全ての必要条件が満たされており、開発者はなにも考えずとも、信頼性の高いサービスを構築・運用していける世界観です。 存在を意識する必要がない、当たり前に必要なことが満たされている理想のPlatformを目指して、道のりは遠いながらもいかにそこに近づいていけるかを考えたいです。 まとめ この記事では、メルカリでのProduction Readiness Checkプロセスの概要について説明し、そのプロセスに対してどのような改善を行ったか、その結果としてどのような開発者体験を作ることができたかをご紹介しました。 明日の記事はsintario_2ndさんです。引き続きお楽しみください。
アバター
こんにちは。メルペイVPoEの@jorakuです。 この記事は、 Merpay &amp; Mercoin Advent Calendar 2024 の記事です。 メルペイに入社して半年が経ちました。この期間で改めて個人的に実感した「メルカリらしさ」についてお話ししたいと思います。メルカリといえば、Go Bold / All for One / Be a ProといったValueが知られていますが、メルカリグループの魅力はそれだけにとどまりません。多様性を尊重し、挑戦を恐れず、学び続けるこの環境は、成果を最大化させる基礎を築き、自分自身やチームの成長にも大きく貢献しています。 1. Go Global / 多様性を包摂する文化 メルカリには55カ国からプロフェッショナル人材が集まっています。特にエンジニアの半数以上は外国籍の優秀なエンジニアであり、異なる文化や考え方に触れることで、自分の視野が広がるのを実感します。この「多様性」は単なるスローガンではなく、日常の中で感じられるリアルなものです。 特にメルカリはグローバルに展開し、かつ国内外問わず多様なモノや価値の循環を行っているため、異なる視点やアイデアを生み出すことに貢献しています。 参考: メルカリIMPACT REPORT GOTサポートで言語の壁を越える 多様な背景を持つメンバーが活躍する中で、GOT (Global Operations Team) のサポートは非常に心強い存在です。同時通訳者の組織が整備されており、必要に応じていつでも誰でも活用できます。これにより、言語の壁を感じることなくスムーズに仕事を進められます。私も活用することがありますが、GOTの皆さんの技術用語の理解の深さには毎回驚かされます。 参考: GOTの取り組み 言語サポートの仕組み やさしい日本語とやさしい英語 さらに、お互いの理解を深めるために「やさしい日本語」や「やさしい英語」を使う文化も根付いています。「伝わりにくいことは歩み寄る姿勢」が自然と浸透しているのは、メルカリらしいポイントだと感じます。 参考: やさしい日本語・英語の活用 2. Scrap and Buildの精神 メルペイに入って感じたもう一つの素晴らしい文化が、技術的負債の解消やシステムのリアーキテクチャといった「Scrap and Build」の精神です。技術だけではなく組織や人材まで及びます。現状に満足することなく、常に改善を目指して行動する姿勢が、エンジニアとしてのやりがいを大きくしています。過去の話ですがアプリをゼロから作り直した話はとてもGo Boldな取り組みでした。そのような大胆な意思決定が可能であり、それを最後までやり切る力がメルカリの強みです。 現在も、多くのマイグレーションやリアーキテクチャが行われ、常に進化し続ける土台を作っています。12/6の@hibagon-sanのアドベントカレンダーにおいてもMerpay API Rearchitectureについて触れられています。 参考: 大型プロジェクト「GroundUp App」の道程 iOS&Androidのテックリードが振り返る、すべてがGo Boldだった「GroundUp App」 メルカリ新卒1年目のエンジニアが最初の7ヶ月間でやったこと 循環する組織 また、半年ごとに役割が変わるほどの柔軟な組織体制も魅力です。これにより、新しいチャレンジが可能となり常に新しい知識や経験を重ねることができます。もちろん専門性を深掘りしたい人はその意思も反映されます。 一度メルカリを離れたとしても、再びメルカリに戻ってくる人材も少なくありません。このような柔軟な文化が、組織としての強さを支えています。 社外に応募している職種はそのまま社内公募(Bold Choice)にも出されるため、自分のキャリアを後押しすることも可能です。 参考: Talent Development(Bold Choice) 3. ドキュメント文化 メルカリで働く上で特に印象的なのが「ドキュメント文化」です。この文化は、メルカリが持つ透明性と効率性を象徴するものといえます。 会議前の「読む読むタイム」 会議によっては事前に議題に関するドキュメントが作成され、参加者が「読む読むタイム」を持ちます。この時間を通じて、全員が同じ情報を共有した状態で議論を始めることができるため、深い議論や迅速な意思決定が可能になります。さらに、意思決定の背景や議論の経緯もドキュメントに残されるため、後から過去の決定やプロセスを追跡することができます。 図よりも文字で表現 提案資料や説明の場では、Power PointやSlideを使うのではなく、文字で詳細に表現するのが特徴です。これは、図では曖昧になりやすい部分も正確に伝えることができるためです。この文化は、Amazonの「ナラティブ文化」に似ていますが、メルカリ独自の進化を遂げています。 VisionならびにRoadmapの言語化 特に興味深いのは、VisionやRoadmapの作成が全社員に求められる点です。多くの企業では、方向性の策定はDirectorやその上の役職者が主に担当しますが、メルカリではプロジェクトをリードする社員にはVisionやRoadmapを作成することが奨励されています。このプロセスにより自分やチームの進むべき方向性や戦略を明らかにし、ステークホルダの共通認識や期待値の調整が容易になります。長期的な視点をもったアーキテクチャに取り組むこともできます。これが、個人の成長とメルカリ全体の方向性をリンクさせる鍵となっています。 参考: メルカリエンジニアリングのロードマップ作成 4. 成長し続ける環境 最後に、学びのサポートについてです。成長自体は業務における成果によってもたらされますし、成長するかしないかは全ては個人の問題だと捉えています。ただ、メルカリではエンジニアの成長を後押しする制度が整っています。お互いが学び合うOpen Doorは頻繁に開催され、プリンシパルエンジニアによるAsk me anything など。また、書籍購入支援や、O&#8217;Reilly Safari Books Onlineの提供、海外カンファレンスへの参加など、個人の成長を強力に後押ししてくれます。このような支援のおかげで、自分のスキルを高めることができ、より高いパフォーマンスを発揮できるようになっています。 参考: Fintech Tech Talk at Office Week を開催したよ! Talent Development(Learning Support) 参考: メルペイエンジニアが参加した/予定の海外カンファレンス(一部) Conference Name Technology Area Droidcon Berlin/Lisbon/London/San Francisco/NYC Android GopherCon UK 2024 Backend QCon 2024 Shanghai Backend SREcon24 Europe/Middle East/Africa , Americas SRE Pragma Conference 2024 iOS SwiftLeeds 2024 iOS KubeCon + CloudNativeCon North America SRE Do iOS 2024 iOS Agile Testing Days 2024 QA/Testing AppDevCon 2025 Mobile KotlinConf 2024 Android Google I/O 2024 Android EuroSTAR Conference 2024 QA/Testing WWDC24 iOS React Advanced London Frontend 終わりに メルペイに入社して半年、メルカリのエンジニアリングカルチャーの強みを日々実感しています。これまで多くの事を学び取り入れてきたつもりですが、この短い期間で、全く異なる変化を生み出せている事を感じています。それは、メルカリの文化がGo Globalな挑戦を後押しし、常に進化し続ける環境を提供してくれるからだと思います。引き続きこれまでの強みを活かした組織を作っていければと思います。 また、メルカリグループのミッションである「あらゆる価値を循環させ、あらゆる人の可能性を広げる」、そしてメルペイのミッションである「信用を創造して、なめらかな社会を創る」は、特に今の時代において、その意義と重要性を増していると感じます。いき過ぎた資本主義や、社会や経済が目まぐるしく変化する中で、これらのミッションは多くの人々にとって新しい価値を提供し、持続可能な未来を形作るための力になると確信しています。このミッションへの思いはまた別途述べる事にします。 この記事を通じて、少しでもメルカリのエンジニアリングカルチャーについてお伝えできていれば幸いです。 次の記事は cyanさんです。引き続きお楽しみください。
アバター
こんにちは。メルカリ ハロのモバイルチームのEMの @atsumo です。 この記事は、連載: メルカリ ハロ 開発の裏側 &#8211; Flutterと支える技術 &#8211; の3回目と、 Mercari Advent Calendar 2024 の7日目の記事です。 はじめに メルカリ ハロは2024年3月にリリースされた、Flutterを用いて開発されたアプリケーションです。本記事では、デザインシステムの導入によって実現した開発効率の向上と、その具体的な運用方法について共有いたします。 目次 デザインシステムの概要 メルカリ ハロのデザインシステム Componentのご紹介 FigmaからFlutterへの実装について Componentを使った画面実装 1年経過して見えてきた課題 今後の展望 まとめ それでは、順を追って説明していきます。 デザインシステムの概要 デザインシステムについて触れたいと思います。 私たちはデザインシステムを『サービスにおけるUXやUIの一貫性を保つための要素と仕組み』と捉えています。デザインシステムを活用することで、個別のコンポーネントや色やタイポグラフィの設定だけではなく、サービス全体におけるそれらの一貫性を担保しながら、開発体験および生産性を向上させることができます。 デザインシステムによってデザイナーの創造性を制限してしまうのではないか?など自由度が下がってしまうのではないかと懸念されることがあるかも知れませんが、私がデザインシステム導入を通じて感じたことは、制限があることで創造性が損なわれるのではなく、むしろ明確なルールと基盤があるおかげで、本来解決すべき課題に集中できるということです。今回メルカリ ハロでデザインシステムを導入したことで、デザイナーとエンジニア間でとても効率的に開発を進めることができたと感じています。 参考: Figma Blog &#8211; デザインシステムの基本: デザインシステムとは? メルカリ ハロのデザインシステム Componentのご紹介 Design Token Components Specific Components などのように大きく3つの構成要素で分かれています。 Design Token Design Tokenとして定義しているのは5つあります。 Color Space Radius Shadow Typography Colorに関してはDark Mode / Light Modeを定義しておりvariablesとして管理されています。 Figmaのデザインデータでは、Spacing、Radius、Typographyなども画面サイズに応じて変数で管理されています。ただし、ハロアプリはタブレットなどの大きな画面に対応していないため、これらは反映されていません。 Components Componentsではドメインに依存しないようなUI Componentを定義しています。 Button / AppBar / Checkbox / Divider / Toggle / Badge / Tab / ProgressBar / Indicator / Snackbar / Tooltip など他のデザインシステムにもあるような一般的なものや Input / Section Title / State Message / Callout / Notesなど、少しハロ独自で使っているがドメインに依存しすぎないようなComponentを用意しています。 Specific Components Specific Componentsとしてハロのドメインや仕様に依存していて、複数画面で使用されるようなComponentも定義しています。 Specific Components自体も基本的にはComponentsの組み合わせでできており、それぞれのComponentが持っているプロパティはSpecific Componentでも適用することが可能です。 こちらのSpecific Componentsのケースで言うと内部でCalloutという上記のComponentとして定義したものを使用しているため、CalloutのComponentのプロパティで変更できる部分に関してはこちらでも変更することが可能です。 FigmaからFlutterへの実装について Listのアイテムを例に説明していきます。 ハロ内ではListItemでも複数のパターンのitemを用意しています。 これを全て1つのComponentとしてプロパティを切り替えで定義してしまうと、プロパティがたくさん増え、いろんな組み合わせでいろんな表現ができる一方で、わかりにくいComponentになってしまいます。 ハロでは下記の様にそれぞれ別のComponentとして取り扱っています。 Figma上でコンポーネントのプロパティを✏️で示してもらうことで、変更可能な値がわかりやすくなり、実装時にはそのプロパティを基にしてコンポーネントを作成することで、デザインデータとコードの認識を一致させることができました。 Show Divider: Top / Show Divider: Bottomなど Figmaのデザイン上は便利な一方でだが実際に実装時にComponentには入れない部分などもありますが、おおよそ同じようなプロパティ構成になっています。 実際のComponentのコード class ListItemWithValue extends ConsumerWidget { ListItemWithValue({ required this.label, required this.value, required this.horizontalPadding, super.key, this.note, this.onTap, this.align = ListItemValueAlign.end, ... }) : assert( !((note?.isNotEmpty ?? false) &amp;&amp; align == ListItemValueAlign.start), &#039;Value align can only be start if note is not null&#039;, ); final String label; final String value; final String? note; final GestureTapCallback? onTap; final ListItemValueAlign align; ... } Figmaのデータを元に実装する際にはFigmaのDev Modeをよく利用しています。Dev Modeを使用することでCSSやiOSのUIKit / SiwftUI、AndroidのXML / Composeなどのコードに変換された状態で確認することができます。残念ながら、現在Flutterは公式サポートされていないですが、Community pluginでFlutterのコードに変換するようなPluginがいくつかあり、そちらを使うことで実装を容易にすることができます。 Figma to Code (HTML, Tailwind, Flutter, SwiftUI) などのpluginを使って、変換されたコードを見ながら、Widgetの構成を検討することがあります。変換されたコードは高さや幅が固定値になってしまっていたり、無駄なWidgetがあったりするためそのまま使うのは難しいですが、実装時の参考になることは多いです。 Componentを確認する 前回 メルカリ ハロ アプリの技術スタックの紹介 でも Widgetbook についても軽く触れていますが、Componentを確認するためにWidgetbookを使用しています。 ※1: Figma上のプロパティ ※2: コード上のプロパティ 上記のように Figma上の ✏️ 部分(※1)とコードのプロパティ(※2)の認識を合わせることで、Componentで変更できる値がわかりやすくなり、コミュニケーションがとてもスムーズに行えるようになっています。 画面実装前のComponentを量産する時期には、新規画面実装の際のカタログとしてWidgetbookが大いに活躍していました。Webでよく使われている Storybook が持っているようなさまざまな機能などが今後Widgetbookにも追加されていけば、画面実装時のモックUI実装にも大いに役立つ可能性があり、今後もWidgetbookに期待しています。 Componentを使った画面実装 メルカリ ハロのデザインシステムを活用して、具体的な画面実装を行う方法について説明します。ここでは、設定画面を例に、どのようにデザインシステムのComponentを使用して画面を構築するかを紹介します。 設定画面の実装 設定画面では、以下のようにComponentを使用しています。 NavigationBar &#8211; 画面のタイトルと戻るボタンを表示 Notes &#8211; 設定に関する説明文を表示 ListItem &#8211; 現在の設定状態を表示 Button &#8211; 設定変更用のボタン class SettingsScreen extends HookConsumerWidget { const SettingsScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final theme = ref.watch(appThemeProvider); return Scaffold( // 1. Navigation Bar - 画面のタイトルと戻るボタンを表示 appBar: const HalloNavigationBar.normal( title: Text(&#039;設定画面タイトル&#039;), ), body: SafeArea( child: Padding( padding: EdgeInsets.only( left: theme.spacing.size4, right: theme.spacing.size4, top: theme.spacing.size8, bottom: theme.spacing.size10, ), child: Column( children: [ // 2. Notes - 設定に関する説明文を表示 const HalloNotes( content: &#039;設定に関する説明文をここに記載します&#039;, ), Gap(theme.spacing.size8), // 3. List Item - 現在の設定状態を表示 HalloListItem.value( label: &#039;設定項目名A&#039;, value: &#039;設定状態&#039;, ), const HalloDivider(), HalloListItem.leadingWidget( label: &#039;設定項目名B&#039;, leading: const Icon(Icons.info_outline_rounded), note: &#039;設定項目に関するメモなどを記載する&#039;, doubleLine: true, ), const HalloDivider(), HalloListItem.trailingWidget( label: &#039;設定項目名C&#039;, trailing: HalloSwitchButton( value: true, onChanged: (value) { // Switchのオンオフによって実行される処理 }, ), note: &#039;オンオフを設定することが可能&#039;, ), const HalloDivider(), const Spacer(), // 4. Button - 設定変更用のボタン HalloButton.filled( label: &#039;設定を変更する&#039;, onPressed: () { // 設定画面を開く処理 }, ), ], ), ), ), ); } } このように、デザインシステムのコンポーネントを使用することで、統一感のあるUIを簡単に実装することができます。各コンポーネントは再利用可能であり、デザインの一貫性を保ちながら効率的に開発を進めることができます。 リリースから半年後に見えてきた課題 メルカリ ハロは今年の3月にリリースされています。去年の今頃は上記のComponentを用いて各Component自体はWidgetbookで確認することができ、それらのComponentを使用して新規の画面を実装するフェーズでした。Componentの活用により、生産性高く画面実装を行うことができたと感じています。 最近は新しい画面を作ることもありますが、今まであった画面の機能の改善や機能を追加していくことも増えています。その中で既存のコンポーネントでは対応しきれないケースや、当初の設計では考慮されていなかった部分が少しずつ明らかになってきました。 例えばComponentに内包されているPaddingやMarginとは違うものを使った方が、単体の画面構成としては収まりが良いなど、既存のものとは少しだけ異なる亜種のComponentが生まれ始めています。ルールに縛られすぎてUXを阻害するものになってはいけないと思いつつも、デザインシステムは、ユーザー体験だけでなく、開発者やデザイナーの体験も高めることで、その効果を最大限に発揮できると考えています。 現在はデザイナーとコミュニケーションを取り、基本的にはデザインシステムで定義されているものを使用するというルールを改めて確認するようにしています。一方で、デザインシステムで定義されているものだけではユーザー体験を損ねる可能性があるケースにし関しては、デザインシステム自体の改善であったり、Specific Componentsを定義するような流れをとっています。 今後の展望 メルカリ ハロのデザインシステムは、さらなる開発の効率化と品質向上を目指して進化を続けています。この目的のために、いくつかの新しい可能性を模索しています。 まだアイデアの段階ではありますが、Figmaなどのデザインツールにおいて、Componentが正しく使用されていない箇所を自動的に検知できるlintの仕組みを導入することを検討しています。また、新たなチームメンバーが加わる際に、デザインシステムを円滑に理解し活用できるよう、勉強会やドキュメントの整備にも力を入れています。全員が共通の知識とスキルを持てる環境を作り、プロジェクトに貢献できるよう取り組んでいきます。 さらに Figma AI はじめ v0 や bolt.new など生成AIを使ったデザインおよび実装のアプローチに興味を持っており、これらを参考にメルカリ ハロのデザインシステムに基づいたデザインから実装の効率化の方法を模索しています。これによって、デザインを試行錯誤するプロセスが、デザイナーやエンジニアだけでなく、他の職種の方々にも手の届きやすいものになればと考えています。 まとめ この記事では、メルカリ ハロのデザインシステム導入による開発効率向上とその具体的な運用方法についてご紹介しました。デザインシステムは、ビジュアルの一貫性を保つだけでなく、デザイナーとエンジニアが円滑に連携し、生産性を高めるための基盤となっています。リリースから半年以上が経過し、新たな課題も浮上していますが、引き続きデザイナーと協力してデザインシステムの改善を続けています。 今後は生成AIの活用を視野に入れつつ、さらなる効率化とデザインの品質向上を進め、より良いユーザー体験を提供できるよう努めていきます。 明日の記事はmshibuyaさんです。引き続きお楽しみください。
アバター
こんにちは。メルコイン エンジニアのpoohです。 この記事は、 Merpay &amp; Mercoin Advent Calendar 2024 の記事です。 目的 LLM(大規模言語モデル)による情報取得能力が向上している今日、以下で紹介する検索テクニックが不要になる時代が目前に迫っているかもしれません。しかし、まだしばらくは役立つ場面が多いと思いますので、ぜひ参考にしてください。 検索技術の中には「知っている人にとっては当たり前」でも、「知らない人にとっては驚き」につながるものがあります。今回ご紹介するのは、Slackで効率的に情報を検索するための基本的なテクニックです。 説明の前に 今回の投稿のためにSlackのドキュメントを読み、知らなかった検索クエリを知ることができ、いい学びとなりました。 新たに on:today は頻繁に使うようになりました。 自分の仕事の振り返りと情報共有を目的として個人的に日報を書いています。その際に、「from:@pooh on:Today」を早速使っています。 ダブルクォーテーションで括る 最初に紹介するのは、「ダブルクォーテーションで括る」方法です。 Slackでは通常、検索クエリに基づいて曖昧検索が行われます。たとえば、以下のような結果が得られます。 「探してね」で検索 → 「探し」「探す」がヒット 「見つかった」で検索 → 「見つか」「見つから」「見つかり」「見つかる」がヒット この曖昧検索は便利な場合もありますが、正確に特定の単語を検索したい場合には不便です。その際には、検索語句をダブルクオーテーション(例: &quot;見つかった&quot;)で括ることで、完全一致する結果だけを表示させることができます。 特定キーワードの除外(-) 次に紹介するのは、「-(マイナス)」を使った方法です。特定のキーワードを含むメッセージを検索結果から除外したい場合に便利です。 例: KPI -FY24Q1 → 「FY24Q1」を含むメッセージを除外した結果が表示されます。 特定ユーザーの発言を検索(from:@) 特定のユーザーの発言のみを検索したい場合は、from:@を使用します。 例: from:@example → Slack名が@exampleの人の発言のみが検索されます。 さらに、複数のユーザーを対象とする場合は以下のように記述します。 from:@example1 from:@example2 → @example1と@example2の両方の発言が表示されます。 チャネル内の発言を検索(in:) 特定のチャネル内での発言を検索する場合には、in:を使用します。 例: in:#all-random → #all-randomチャネル内の発言が検索されます。 また、特定のDMを検索する場合には以下のように記述します。 in:@example1 → @example1とのDMを対象に検索。 複数のチャネルやDMを対象にしたい場合は以下のように記述できます。 in:#all-random in:example1,example2 特定ユーザーとのメッセージを検索(with:) with:を使うと、特定のユーザーとのやり取りを簡単に検索できます。 例: with:@example1 → 以下を含む発言が対象になります。 @example1とのDM @example1が参加しているスレッド内のメッセージ 日付を指定して検索(before: / after: / on:) 検索範囲を特定の日付に絞りたい場合は、before: / after: / on:を使用します。 例: on:2024-12-01 → 2024年12月1日の発言だけが表示されます。 before:2024-12-09 → 2024年12月8日以前の発言が表示されます。 after:2024-11-30 → 2024年12月1日以降の発言が表示されます。 日付は年月日のみならず、月単位でも指定可能です。 例: on:2024-10 → 2024年10月の発言 before:2024-10 → 2024年9月以前の発言 まとめ ここで紹介した検索クエリは組み合わせて使用することで、さらに効率的な検索が可能になります。 例: on:2024-12-08 from:@example1 &quot;連絡して&quot; -FY24Q1 → 「2024年12月8日」、「@example1」、「&quot;連絡して&quot;を含む」、「FY24Q1を除外した」発言が検索結果として表示されます。 適切な検索クエリを駆使することで、必要な情報を迅速かつ正確に見つけることが可能です。ぜひ試してみてください!
アバター
この記事は、 Merpay &amp; Mercoin Advent Calendar 2024 の記事です。 はじめに こんにちは。メルペイ Frontend の @togami です。 私たちのチームでは Engagement Platform、通称 EGP という内製マーケティングツールの開発をしています。ポイントやクーポンなどのインセンティブの配布、LP の作成と公開、キャンペーンの作成など CRM 関連のことをマーケターや PM がコーディングの知識なしで行えるようにするための社内ツールです。EGP はメルカリ US を除く全てのプロダクトで使われている会社全体の共通基盤となっています。 本記事ではこの中でも LP 作成機能、通称 EGP Pages について紹介します。また、 EGP Pages の拡張版であり Server Driven UI の実装である EGP Cards について紹介します。 EGP Pages とは EGP Pages は WYSIWYG コンポーネントエディタです。LP の作成に特化しています。 Text Image Layout Lottie Entry Button など全部で 28 種類のコンポーネントが用意されており、それらを組み合わせて LP を作成します。このツールを使って公開された LP は月間で 5000 万 PV ほどあり、月によっては 100 を超える LP が新たに公開されています。 EGP Pages の使い方 どのように機能するのか簡単なサンプルの作成を通してみていきます。 このサンプルでは Web ブラウザで閲覧している時は QR コードとテキストを、Mobile で閲覧している時はテキストカスタムボタンを表示するようにします。 ①まずメニューから Layout コンポーネントを選択します(図1.1)。 図1.1 コンポーネントメニュー 左側の Tree View に Layout が追加されました。 ②この Layout は Web 向けのコンテンツのコンテナとして使用するため、右側のタブの設定から”Web Content”と名付けます(図1.2)。 図1.2 Layout へのネーミング ③続いて QR コードのコンポーネント、Text のコンポーネントを挿入します。Text には&quot;Please install app&quot;というテキストを入れます(図1.3)。 図1.3 QRとTextの追加と設定 ④次にアプリで閲覧している時のためのコンテンツを保持するために別の Layout コンポーネントを追加します。”Mobile Content”と名付け、その中にテキストとボタンを追加します(図1.4)。 図1.4 Mobile 用のコンテンツ追加 ⑤全てのコンテンツを作成した後、When コンポーネントを使用して Layout をラップします。このコンポーネントは表示条件を設定でき、その条件が True の時のみ表示されるものです。 ⑥When で作成したコンテンツをラップして表示する条件を追加します。JavaScriptで条件を追加が、多用する条件についてはテンプレートが存在するので利用します。ここでは Mobile でのみ表示したいので、”Is using mobile apps”を選択すると条件が設定されます(図1.5)。 図1.5 Whenによる条件分岐の設定 ⑦同様に Web 向けにもうひとつ When コンポーネントを追加し、⑧条件を設定します(図1.6)。 図1.6 Web用コンテンツの分岐 このエディタにはデバイスの設定(Web/iOS/Android)やログイン状態、エントリー状態の有無など、状態をエミュレートする機能が備わっています。⑨iOS を想定して描画結果を確認してみると、このように Mobile Content の When ブロック内の要素のみが描画されているのがわかります(図1.7)。 図1.7 エディタによるiOS環境のエミュレート ⓾同様に Web を想定したものです。Mobile のブロックは描画されておらず、Web のコンテンツのみが表示されています(図1.8)。 このように形でコンディショナルレンダリングをし、Web と Mobile で要素を出しわけたページを作成ができます。 図1.8 エディタによるWeb環境のエミュレート 簡単なサンプルを通して基本的な使い方を説明しました。今回は触れませんが、これ以外にも LOG の設定や API のモック、開発中のモック機能や Dark モード/Light モードへの対応、日本語以外の英語や台湾華語への設定なども行うことができます。 アークテクチャ EGP Pages は以下のようなアーキテクチャで構成されています(図2.1, 図2.2)。 図2.1 EGP Pages の全体アーキテクチャ 図2.2 EGP pages エディタ部分のアーキテクチャ DB は Cloud Firestore を使用しており、ユーザーが作成したコンポーネントやページの情報を保存しています。ユーザーが作成したコンポーネントやページの情報は全て JSON 形式で保存されており、それを元にページの描画しています。 先ほどの Firestore からロードしたスキーマを元にEGPのエディタの state の初期化 と 編集中の LP を React で描画するための transform を行います。 そして 描画された結果は iframe を通じてエディタに表示されます。 エディタの右側に各種設定項目があり、変更すると Redux の state が更新されて、ます。その結果がリアルタイムで反映されます。 エディタから LP を公開する時には Cloud Run functions 上で LP を SSG することでで HTML と CSS を生成します。生成された静的なページを CDN 経由で配信しています。 こうして配信された HTML が Mobile なら Webview 経由で、Web ならそのまま描画されます。 最終的には静的な HTML を配信しているだけなのでパフォーマンスについても概ね良好です。 エディタの仕組み エディタの仕組みについて、もう少し詳しく説明します。 一見複雑そうに見えますが、実際には各要素は決められたスキーマに沿った単純な JSON データです。先ほどのサンプルを例に挙げて説明します。 まず、Text コンポーネントですがこのエレメントは tagName: &quot;Text&quot; を持ち、 props として value や className を持っています。これらの value や className は、エディタの右側にあるパネルから変更できます。 具体的な JSON データは以下のようになります。 { &quot;tlVV3bzegT-Pi59b3sJgQ&quot;: { &quot;id&quot;: &quot;tlVV3bzegT-Pi59b3sJgQ&quot;, &quot;name&quot;: null, &quot;tagName&quot;: &quot;Text&quot;, &quot;props&quot;: { &quot;value&quot;: &quot;Please install the app!&quot;, &quot;className&quot;: &quot;text-center text-[0.8125rem] text-[#000000]&quot; }, &quot;meta&quot;: null } } 次に、 QR コード コンポーネントも同様の構造を持っています。ただし、QR コード特有の url や表示に関するプロパティを持っています。 以下がその JSON データです。 { &quot;SsUPPIaLOkuh7zUzgIn-S&quot;: { &quot;id&quot;: &quot;SsUPPIaLOkuh7zUzgIn-S&quot;, &quot;name&quot;: null, &quot;tagName&quot;: &quot;QrCode&quot;, &quot;props&quot;: { &quot;url&quot;: &quot;https://jp.mercari.com&quot;, &quot;margin&quot;: &quot;4&quot;, &quot;scale&quot;: &quot;4&quot;, &quot;darkColor&quot;: &quot;#000000&quot;, &quot;lightColor&quot;: &quot;#ffffff&quot;, &quot;className&quot;: &quot;self-center&quot; }, &quot;meta&quot;: null } } これらの各エレメントは、それぞれ親子関係を持って構成されています。具体的には、 Layout コンポーネントがコンテナとなり、その中に Text と QR コード のコンポーネントを子要素として持っています。 以下がその Layout コンポーネントの JSON データです。 { &quot;id&quot;: &quot;W682tXEHtC1eWgvLMvyYx&quot;, &quot;name&quot;: &quot;Web Content&quot;, &quot;tagName&quot;: &quot;Layout&quot;, &quot;props&quot;: { &quot;className&quot;: &quot;flex flex-col&quot;, &quot;children&quot;: [ &quot;:=element.SsUPPIaLOkuh7zUzgIn-S&quot;, &quot;:=element.tlVV3bzegT-Pi59b3sJgQ&quot; ] }, &quot;meta&quot;: null } children には、このレイアウトの子要素として含まれるコンポーネントの参照が記述されています。 &quot;:=element.SsUPPIaLOkuh7zUzgIn-S&quot; は先ほどの QR コードコンポーネントを、 &quot;:=element.tlVV3bzegT-Pi59b3sJgQ&quot; は Text コンポーネントを指しています。 つまり、この Layout コンポーネントは先ほど定義した QR コードと Text のコンポーネントを子要素として持ち、それらを含むコンテナとして機能しています。 これらの JSON データを元に、エディタ内部では React のコンポーネントを再帰的に組み立てていきます。具体的には、 tagName に対応する React のコンポーネントを生成し、props の情報をそれぞれに渡します。 こうして生成されたコンポーネントツリーが、最終的に画面上に描画されます。 図2.2(再掲) EGP pages エディタ部分のアーキテクチャ ユーザーがエディタでコンポーネントの設定を変更すると、その変更は直ちに対応する JSON データに反映されます。そして、その更新された JSON データを元に React コンポーネントが再レンダリングされ、プレビュー画面にリアルタイムで反映されます。 LP のスタイリング LP のスタイリングの裏側では Tailwind CSS を使っています。 CSS をあまり知らなくてもスタイリングが行えるように UIウィジェットを作成し、そこから Tailwind CSS の className を付与することでスタイリングしています(図2.3)。 図2.3 UIウィジェット また最終手段になりますが個々のエレメントに任意の className が付与できるので、細かい調整や少し複雑な表現も Tailwind CSS でできることは全て実現ができるようになっています。 Native Bridge LP の要件によっては Webview と Mobile 間での通信が必要です。例えばボタンをクリックした時にアプリ内の特定の画面を開いたり、特定のアクションを実行するといった動作です。 Webview と Mobile 間で通信するための方法はいくつかあります。その一つとして例えばカスタム URL スキームがあります。しかし、この方法では単方向でしか通信できなかったりセキュリティリスクがあります。 そこでより安全に Webview と Mobile 間で双方向通信を行うために、私たちは Onix という内製の Native Bridge を作成しました。これは Channel Messaging API を利用した双方向通信をサポートしています。 加えて端末の OS やバージョンによる Webview webview の API の差分のハンドリングや Channel Messaging API をサポートしていないバージョンの場合 Deep link へ fallback なども行います。 この Native Bridge は現在 iOS と Android のみ対応しているため、今後は Flutter の Webview でも対応する予定です。 技術的な課題 これまで一通りエディタについて見てきましたが、もちろん課題もあります。その 1 つとして このエディタは複数人によるリアルタイム編集ができません。 そのため、予期せぬ上書きが発生してしまうことがありました。 詳しくは こちらのブログをご覧ください 。 この問題に対しては yjs を使ってリアルタイム編集を実現しようと検証中です。このライブラリは CRDT(Conflict-free replicated data type) と呼ばれるデータ構造の JavaScript による実装で多くの同時編集機能があるアプリケーションで使われています。 運用上の課題 EGP Pages は あくまで LP というドメインに特化したエディタ です。そのため、大量の API コールや条件分岐が存在したり、特別なイベントハンドラが必要な 複雑なものを作ろうとすると限界があります。 LSP や Linter、Formatter、Syntax Highlight、コードジャンプや検索がない VSCode でいわゆる Web Application チックなものを開発しているのを想像してみてください。エンジニアの方には伝わると思いますが、このような制限された環境では複雑な機能を実装したり大規模な物を管理したりすることが非常に困難だということは想像に難くないでしょう。 加えて、 通常のテキストプログラミングと違ってテストを書くことも難しく基本マニュアルで QA しなければなりません。 変更によって予期せぬ問題が発生するリスクが常につきまとっているので、小さな修正でさえ慎重に作業を進める必要があります。 また、これはローコードプラットフォームを作っていると直面することが多い問題ですが、 できることが増えれば増えるほど、より複雑なデザインや機能要件が求められてしまいます。 個々の要件をエンジニアなしで済むような UI や仕組みとして加えていくと、表面上は綺麗でもどんどんコードが複雑化していきます。そのため、どこまでをエディタの機能として提供し、どこからはエンジニアによるカスタマイズが必要かを常に考えていく必要があります。 当初 EGP もエンジニアなしで完結する ノーコード/ローコードプラットフォームのような方向性もありました。しかし今は エンジニアと非エンジニアが効果的に共同作業できるツールを目指すという方向性になっています。 そのため、引き続き 非エンジニア向けに UI ウィジェットを改善していくのはもちろんエンジニア向けにコンポーネントの Encapsulation や KV pair のコンフィグなどの新しいメカニズムを導入してエンジニアと非エンジニアがよりシームレスに協働できる環境を整え複雑な要件にも対応できるようにしていきます。 Server Driven UI 最後に Server Driven UI の実装であり、現在開発中の EGP Cards について解説します。 Server Driven UI とはサーバーからデータを一緒に UI の構造も返却してクライアント(Web / Mobile)でレンダリングする手法です。 2021 年に AirBnB が出した A Deep Dive into Airbnb’s Server-Driven UI System という記事で有名です。 A Deep Dive into Airbnb’s Server-Driven UI System この手法には次のようなメリットがあります。 サーバー側の変更で UI を更新できるため、アプリのアップデートを待たずにリリースできる クライアント側の実装を簡素化できる クロスプラットフォームの一貫性を保ちやすい Native で描画するため Webview と比べてパフォーマンスが良い 採用背景 これまで、マーケターは主に別のツールを用いてパーソナライズされたコンテンツを配信していました。しかし、利用可能なコンテンツカードは 3 種類 のみで、そのカスタマイズ性も限定的でした。また、独自コンポーネントを使用しているため実際に配信されるまでコンテンツのプレビューができず テストが非効率 でした。 これに加えて、 各プラットフォームでの開発工数の増大 が課題となっていました。現在メルカリでは、プロダクトの増加に伴って使用しているプラットフォームが多岐に渡っています。 Mercari アプリ:Swift/Kotlin Mercari Shops、はたらくタブ :アプリ内 WebView Mercari Hallo:Flutter Mercari Web:Web 当初、この課題に対して EGP Pages の活用を検討しました。しかし、WebView を用いた実装では モバイルアプリのパフォーマンスに影響を及ぼす可能性があり 、高パフォーマンスを求める要件には適さないという問題がありました。このような状況下で、EGP Pages の利点を活かしつつ課題を克服するために Server Driven UI の採用を決定しました。 EGP Cards 私たちは、Server Driven UI の実装として EGP Cards と名付けたシステムを開発しました。 まず Mobile と Web 間共用の UI を記述するために Card UI Protocol という JSON ベースのプロトコルを定義しました。 この Protocol を用いてクロスプラットフォーム UI を記述します。 一例として、以下のような UI を考えます。 上下中央揃え、縦横 200px の Box Box 中に&quot;This is test&quot;という Text が中央揃えで配置 これを Card UI Protocol で記述すると以下のようになります。 { &quot;id&quot;: &quot;root&quot;, &quot;type&quot;: &quot;Layout&quot;, // UIの構造を定義 &quot;styles&quot;: { &quot;direction&quot;: &quot;row&quot;, &quot;wrap&quot;: false, &quot;mainAxis&quot;: &quot;center&quot;, &quot;crossAxis&quot;: &quot;center&quot;, &quot;width&quot;: { &quot;preferred&quot;: { &quot;v&quot;: 200, &quot;u&quot;: &quot;px&quot; } }, &quot;height&quot;: { &quot;preferred&quot;: { &quot;v&quot;: 200, &quot;u&quot;: &quot;px&quot; } }, &quot;background&quot;: { &quot;light&quot;: &quot;#ffffff&quot;, &quot;dark&quot;: &quot;#222222&quot; } }, &quot;children&quot;: [ { &quot;id&quot;: &quot;SCaWRbQdLtwLoiCuEjh8h&quot;, &quot;type&quot;: &quot;Text&quot;, // UIの構造を定義 &quot;styles&quot;: { &quot;fontSize&quot;: { &quot;v&quot;: 13, &quot;u&quot;: &quot;px&quot; }, &quot;textAlign&quot;: &quot;center&quot; }, &quot;props&quot;: { &quot;value&quot;: &quot;This is Test&quot; }, &quot;children&quot;: [] } ] } この JSON をもとにレンダリングエンジンで描画すると以下のような UI になります(図3.1)。 図3.1 Card UI Protocolで記述したコンポーネントの描画結果 EGP Cards Editor このプロトコルで UI を簡単に作成できるように、EGP Pages のエディタを拡張しました。エディタ上で スタイリングされたコンポーネントを Card UI Protocol に変換して保存できるようにしています(図3.2)。 図3.2 エディタの Cards 向け拡張 EGP Pages では HTML ファイルと CSS ファイルを生成していましたが、EGP Cards ではコンポーネントを Card UI Protocol の形式に変換し最終的には JSON ファイルがデータベースに保存されます。 この JSON ファイルをクライアントに送信し、 各プラットフォームでネイティブの UI として描画します。 レンダリングエンジンは各プラットフォームの言語(Swift/Kotlin/Flutter/JavaScript)で実装されています。以下のように、単一の JSON ファイルから各プラットフォームで共通の UI を描画します(図3.3)。 図3.3 単一のJSONファイルをもとにしたクロスプラットフォームレンダリング さらに、この EGP Cards をセグメンテーションサービスと組み合わせることで、ユーザーごとにパーソナライズされたコンテンツの配信や、A/B テスト の実施などユースケースの拡大を図っています(図3.4)。 図3.4 セグメンテーションサービスと連携した例 まとめ 本記事では内製マーケティングツール Engagement Platform(EGP) と、その中でも特に EGP Pages および Server Driven UI の実装の EGP Cards について紹介しました。 今後も技術的・運用上の課題に取り組みつつ、EGP を進化させていくことでマーケティング活動の効率化と効果向上を目指していきます。 次の記事は @poohさん です。引き続きお楽しみください。
アバター