PLAID Engineer Blog

PLAID Engineer Blog


KARTEを提供する株式会社プレイドのエンジニアブログです。プレイドのエンジニアのユニークなパーソナリティを知ってもらうため、エンジニアメンバーたちが各々執筆しています。

PLAID Engineer Blog

KARTE Blocksを支える技術

Ryosuke SuzukiRyosuke Suzuki

こんにちは、エンジニアの鈴木(@RyosukeCla)です。
僕は KARTE Blocks の保守、運用、開発、計画と幅広く携わっています。
無事、2021年9月14日にプロダクトを正式リリースして、現在はプロダクトの改善に取り組んでいます。
本稿では、KARTE Blocks をどのように開発しているのか、その裏側、特に技術に関して紹介していきます。

この記事は「KARTE Blocksリリースの裏側」という連載シリーズの1日目の記事です。全10回を予定しています。
これから毎日記事を更新していくため、更新をチェックしたい方は@KARTE_BlocksのTwitterアカウントをフォローしてください!

連載では次のような記事をはじめとした、さまざまなBlocksの裏側を公開していく予定です。

  1. KARTE Blocksを支える技術 ← イマココ
  2. インクリメンタルに新しい技術を取り入れる方法
  3. セカンドパーティコンテンツをもつサードパーティスクリプトの作り方
  4. AWSが落ちてもGCPに逃がすことで落ちないシステムを作る技術
  5. ユーザーが自ら理解・学習するためのテックタッチなアプローチ
  6. 爆速で価値あるプロダクトをリリースするためのチームビルディング
  7. CSS in JSとしてVanilla-Extractを選んだ話と技術選定の記録の残し方
  8. 0→1のフェーズで複数のユーザー体験をつなぐUIデザインを考える
  9. リリース後に落ちないように、新規サービスで備えておいたこと
  10. KARTE Blocksにおけるポジショニングの考え方とその狙い
  11. 「KARTE Blocksリリースの裏側」の裏側 - 複数人で連載記事を書く方法

Blocks概論 - KARTE Blocks とは

KARTE Blocks とは、ウェブサイトの更新、評価、改善を、「エンジニアリングの知識無しに」行えることを目指したプロダクトです。

プログラムを書かない Web の運用担当者がサイトを更新する場合、エンジニアや協力会社に依頼するフローが発生します。これは関係者が増えるほどステップ数が増えていき、場合によっては膨大な承認フローや確認フローが発生し、多くの待ち時間が発生します。

現状、これらの問題によって、ウェブサイトの運営の現場では責任をもちたいはずの人間が、自主性を持ってウェブサイトの改善できていません。もし他者もしくは他社への依頼、上司からの承認を待つ必要なくサイトの変更ができたら、今まで待っていた時間を改善の試行錯誤に利用できるはずです。

KARTE Blocks は、この問題をいかに解決するか、という発想で開発が始まりました。開発側とマーケター側間のギャップを解消し、マーケター側が開発側と同等のことを行うために、我々は何を提供すればいいか?

つまり、次の機能が必要だと考えました。

こう言ってしまうのは簡単ですが、実現するのは簡単ではありません。そのためにさまざまな工夫をしています。

しかし、「エンジニアリングスキルを必要とせずにウェブサイトの管理ができる」プロダクトは一筋縄では作れません。 ローマは一日にしてならず、という格言があるように、KARTE Blocks も一日にして成らず、というわけです。

本稿では、このKARTE Blocks が解決したい課題をどのような技術を使って解決していったのかについて紹介します。

プロダクトの機能

ウェブサイトでの運用、管理、改善をするために必要なことを構想した結果、次の4つに辿り着きました。

これらをひとつずつ解説します。

「ウェブサイトの変更を自然に反映させる」仕組み

サードパーティスクリプト

KARTE Blocks は KARTE Blocks がソースコードを管理していないウェブサイトを更新できるプロダクトです。そのため、 KARTE Blocksの管理画面で設定した内容を利用者のウェブサイトへ反映させる仕組みが必要です。

「ウェブサイトの変更を自然に反映させる」ためのメインアイディアは、ウェブサイト本来の DOM が表示される前に同期的に割り込んで、それを置換するアプローチです。

これを実現する疑似コードは次のようになります。

// DOMの変更設定
const DOM_REWRITES = [{ selector: 'body > div', html: '<div>hello</div>' }];

// DOMがロードされたときに、DOMを変更する
document.addEventListener('DOMContentLoaded', () => {
  DOM_REWRITES.forEach(({ selector, html }) => {
    const element = document.querySelector(selector);
    if (!element) return;
    element.outerHTML = html;
  });
});

これを同期的なサードパーティスクリプトとして実行することによって、ブラウザ上で本来のコンテンツが表示される前に DOM を書き換えます。書き換える前の要素は表示されないため、最初からDOMを書き換えた後の要素が表示され、「書き換える前の要素が見えて画面がちらつく」、というエンドユーザーの体験が悪化するのを避けています。

書き換える設定はスクリプトに含ませ、DOM がレンダリングされる前に変更を反映できます。これによってウェブサイトの変更をスムーズに、自然に反映させることができます。

そのほかにも、動的なSingle Page Application(SPA)への対応などさまざまなウェブサイトでも自然に動作させるための仕組みを持っています。
詳細については、「セカンドパーティコンテンツをもつサードパーティスクリプトの作り方」の記事にて紹介します。

サードパーティスクリプトの配信の仕組み

DOM 変更の設定をスクリプトに含ませるため、スクリプトをプロジェクト毎(ここではウェブサイト単位と捉えて問題ありません)に生成し Content Delivery Network(CDN) で配信しています。
そのため、DOM 変更の設定が更新される度に、スクリプトを生成し CDN へ配信し直すことが必要です。

プロジェクト毎のサードパーティスクリプトの生成と配信を担うシステムは、次のような図になります。

builder-server

次のような流れで、プロジェクト毎の設定を含むサードパーティスクリプトを生成しています。

  1. Blocks の管理画面で変更DOMの設定を保存
  2. 保存の Hook によって、 Builder Server へサードパーティスクリプトの生成と配信のリクエストを送信
  3. Builder Server は、webpack[1] を用い サードパーティスクリプトを生成し、Stroage にアップロード
  4. CDN が Storage からスクリプトを取得して配信

お客さんのウェブサイトには、次のような1行のタグでプロジェクト毎のスクリプトを読み込むだけで、管理画面で設定した内容をもとにサイトの表示を書き換えます。

<script src="https://cdn-blocks.karte.io/{PROJECT_ID}/builder.js"></script>

また、CDN を使い配信するということは、CDN側でスクリプトの Cache が行われることになります。
もし、新しいバージョンのスクリプトをアップロードしたときに、古いバージョンのスクリプトが Cache に残ってしまうこともあります。
しかし、CDN 側の Cache Invalidation には limitation[2] があり、スクリプト数の増加に対してスケールしません。
そのため、Cache Invalidation の代わりに Cache-Controlヘッダ の max-age の値を短く設定することで、スクリプト数の増加に対してのロバストネスを確保しています。

さらに、Builder Server でスクリプトを配信する際には、 Atomicity を確保する必要もあります。

これは、新しいバージョンのスクリプトが常に配信されるために必要な性質です。
そのため、Builder Server では、プロジェクト毎にプロジェクト一意となる Key を使用し、排他制御をしています。

それだけでなく、Database(DB) への保存を hook にした、 API Server から Builder Server へのリクエストの traffic control も同時にやっています。
もし、traffic control をしないと、保存したタイミングで Builder Server には多くのリクエストが来ることになり、そのリクエストそれぞれで webpack build が走ってしまいます。
そのため、安定的な配信のための仕組みとして、分散システムで動作する debounce を使用し traffic control を行っています。

この分散システムで動作する debounce には、Redisを利用しており、疑似コードで書くと次のとおりです。

function distributedDebouce(key, wait, callback) {
  ticket = reids.MULTI()
    .INCR(key).
    .EXPRE(key, wait)
    .EXEC();

  SLEEP(wait);

  currentTicket = GET(key);
  if (currentTicket != ticket) {
    return;
  }

  IF SET(key_lock, '', NX, PX, 100) != OK {
    return;
  }

  callback();
}

実際にBuilder Serverで利用しているコードは、ライブラリとして公開しています。

サードパーティスクリプトのリスク

KARTE Blocksを利用するウェブサイトは、scriptタグで同期的にサードパーティスクリプトをロードします。

この同期的なサードパーティスクリプトには、さまざまなリスクが付き物です。
たとえば、CDN に障害があったとき、サードパーティスクリプトに致命的なバグがあったとき、DOM 変更の設定の大きさが肥大化していったときなどです。

これらのリスクをヘッジをすることは大事なことで、さまざまな工夫でリスクを軽減したり移転できるようにしています。
たとえば、AWSとGCP を使った Multi cloud CDN の構築、サードパーティスクリプトの多重読み込み防止、CDN レイヤーでの gzip 化によるサードパーティスクリプトの最適化などです。

他にも、多くのリスクとヘッジがありますが、詳しくは後日公開する予定の「AWSが落ちてもGCPに逃がすことで落ちないシステムを作る技術」の記事にて紹介します。

「ウェブサイトの変更を直感的にできる」仕組み

KARTE Blocks ではウェブサイトの変更を直感的にできることをプロダクトの軸として置いています。
そのようなプロダクトを開発するために、次のような制約を置いています。

まず、外部のウェブサイト(お客さんのウェブサイト)をKARTE Blocksの管理画面内で変更するために、いくつかの課題があります。
ウェブにおいては、Same Origin Policy[3]などで異なるOriginのリソースにアクセスできる方法を制限することで、セキュリティを担保しています。
そのため、管理画面と外部のウェブサイトは異なるOriginとなるため、通常の方法では管理画面から外部のウェブサイトはコントロールできません。

管理画面上での外部ウェブサイトのコントロールするためには、主に次の3つの方法が考えられます。
最終的にKARTE Blocksでは、この中のChrome Extensionを使った方法を利用しています。

「サードパーティスクリプト自体に管理画面用の機能を含める」方法は、お客さんのウェブサイトで読み込んでいる KARTE Blocks のスクリプトにある種のデバッグ機能をつけるということを意味しています。
このデバッグ機能は、お客さんのウェブサイトを訪問するエンドユーザーにとっては不要な機能で、またバグなどがあるとセキュリティリスクとなります。そのため、この方法は利用しませんでした。

次の「Proxyサーバーを通したスクリプトの injection」にもいくつかの問題が知られています。

そのため、KARTE Blocks では Chrome Extension と iframe を使い、管理画面上に外部のウェブサイトをシームレスに組み込む方法を採用しています。

Chrome Extension を使った仕組みは、次の図のようになります。

chrome-extensionのmessaging

通常の場合は、管理画面と外部のウェブサイト間で直接やり取りできませんが、間にChrome Extensionを使うことで中継しています。

具体的には、Chrome Extensionを使ったメッセージングと、background.js の http request/response の Proxy を用い、管理画面と外部のウェブサイトを統合しています。
これによって、アプリケーション内でログインが必要なウェブページを iframe で表示し、iframe 内にプレビュー用のスクリプトを inject することが可能になります。
また、この一連の仕組みは、KARTE Blocksの管理画面の特定のiframeだけに対して有効化することで、拡張を入れた場合に他のページへの不用意な影響が出ないなどの対策も含まれています。

KARTE Blocksのエディタの動作

KARTE Blocksではこの GIF画像 のように、管理画面上に入力した内容をウェブサイトへリアルタイムに反映しています。
これは、次のような処理をChrome Extensionを利用して行なっています。

  1. Chrome Extension で proxy する
  2. 外部サイトを iframe で開く
  3. プレビュー用のスクリプトを iframe に inject する
  4. HTML を Abstract Syntax Tree(AST) に変換する
  5. AST を Graphical User Interface(GUI) で操作する
  6. 変更された AST を HTML に戻し、 iframe 内の DOM を変更する

このように、管理画面内で外部サイトを表示し、リアルタイムに DOM の変更を実現しています。
また、HTML の知識をできる限り隠蔽するため、HTML を AST に変換し、AST を操作するための GUI を実装しています。
たとえば、Textノードはテキストエリアとして入力したり、Img要素のsrc属性はアップロードした画像から選択できます。

これらの技術によって、HTML のノーコードエディティングを実現しています。

ノーコードエディターとなると、フロントエンドのコードは複雑化します。
複雑化してくるとメンテナンスが大変になります。
メンテナンスが大変になると、プロダクトの改善、機能開発が困難になります。
そのため、プロダクトの改善、機能開発のスピードを落とさないような技術選定がキーとなってきます。

KARTE Blocks では、TypeScript, React, vanilla-extractを採用し、 style に関しても型を付けていく方針で開発をしています。
元々、KARTE Blocks は Vue, JavaScript, TypeScript で開発していたので、実際のところ、Vue も使用しています。
しかし、 React 上で Vue を動かすことによって、Vue と React の integration を保ちつつ、徐々に React, TypeScript にコードを寄せていくということをしています。

ReactとVueを共存させて徐々に移行する仕組みについては、「インクリメンタルに新しい技術を取り入れる方法」という記事で紹介します。

「ウェブサイトの変更による効果が視覚的にわかる」ための仕組み

ウェブサイトの変更をしたら、PDCAを回すには、変更による結果がどうであったかが気になりますね。
仮説検証には結果がセットです。

KARTE Blocks には、仮説検証をするための機能があります。
ウェブサイトの変更前と変更後を比較し、クリック率、コンバージョン率の差分があることを二項検定でテストします。
テストするために、ウェブサイト上の変更したブロックの impression, click をトラッキングする必要があります。

KARTE Blocks は、KARTE Blocks のお客さんのサイトのユーザー数分だけリクエストが発生することになります。
突発的なリクエストの増加も起きるため、スケーラビリティが高く、可用性が担保されたシステムが必要です。
そのため、トラッキングデータの収集と解析を分けたアーキテクチャになりました。

KARTE Blocks では、ログの収集をするLog Collecting Serverとログの解析をするLog Aggregating Serverに分けています。

Log Collecting Server

Log Collecting Server はエンドユーザーのブラウザーから送られてきたトラッキングデータを収集し、 BigQuery[4] に流すためのシステムです。
このシステムはこれ以上のことはやらず、急なスパイクやユーザー数の増加にも耐えられるように設計されます。

膨大な数の http request を漏らさずに BigQuery へデータを流し込むため、Google App Engine(GAE)[5] と Dataflow[6] を採用しました。
GAE を選んだのはスケーラビリティの観点で、急なスパイクでもトラッキングデータをロスしないことが、テストの結果、得られたからです。
また、Dataflow も同様の理由で、retry 機構のある pubsub[7] をベースにしたデータ処理ができるからです。

log-collector

Log Aggregating Server

Log Aggregating Server は BigQuery に保存されたデータを解析し、結果を Cache Database に保存することを目的としたシステムです。
データ解析は時間がかかる作業であるのと、データの収集とは責務が違うので、データの収集システムとは分けました。

仮に、データ解析時にバグがあり、システムがダウンしたとしても、データの収集には影響はありません。
そのため、エンドユーザーに近いシステムの可用性を担保できるように設計されています。

log-aggregator

「ウェブサイトの変更による最適化ができる」仕組み

最適化の部分は、統計的仮説検定[8] を元にした仕組みが実装されています。
詳細についてはスペースが足りなくなってしまうため、割愛させていただきます。

プロダクトをアジャイルにリリースするために

ウェブサイトでの運用、管理、改善をするためのKARTE Blocksの概要を紹介しましたが、SaaS の改善は常に続きます。
プロダクトは日常的に改善され、新機能も増えていきます。
その裏側には、リリース、ロールバックがいつでも簡単に早くできる仕組みが必要です。

ウェブアプリケーションのリリースフロー

KARTE Blocks ではアプリケーションはマイクロサービスアーキテクチャに則りつつ、オペレーションとして Kubernetes(k8s) と GitOps を採用しています。
さらに、KARTE Blocks はアプリケーションレポジトリ、オペレーションレポジトリ、クロームエクステンションレポジトリの3つで管理されています。

リリースフローの流れをまとめた図

まず、管理画面などのアプリケーションは1つのレポジトリで管理されていて、 GitHub flow[9] で開発されています。
GitHub flow は、ブランチを切り PR を作り main branch にマージする、という流れです。
KARTE Blocks は GitHub flow で開発されていますが、k8s や GAE へのデプロイは別のオペレーション用のレポジトリで管理されています。

まず、アプリケーションレポジトリでのフローです。アプリケーションレポジトリでは 生成物として Container Image を生成しています。

  1. アプリケーションレポジトリの main branch の変更を hook し、cloudbuild[10] が走る
  2. cloudbuild で Container Image を build し、次に artifact registry[11] に Container Image[12] を push する
  3. cloudbuild で GAE に log collector server をデプロイする

KARTE Blocks は k8s のみで構成されているわけではなく、ログ収集サーバーには GAE を使用しています。
そのため、Container Image の build and push だけでなく、 GAE へのデプロイも含めています。
ただし、GAE にデプロイしても、外部ネットワークからのトラフィックはデプロイされた GAE service には来ません。

次に、Container Imageを扱うオペレーションレポジトリでのフローです。

  1. オペレーションレポジトリ内の Container Image to GAE service の version を最新にし、main branch を更新する
  2. オペレーションレポジトリの main branch を Argo CD が pull し、 k8s の状態を sync する
  3. cloudbuild が main branch の変更を hook し、GAE service の traffic を変更する

最後に、Chrome Extension のリリースについてです。
Chrome Extension のリリースは Chrome Web Store への申請と承認が必要なので、僕たちのコントロール下にありません。
そのため、リリースフローとレポジトリを分けています。

Chrome Extension を公開する際のリリースフローは自動化されています。
社内で利用するBeta版、お客さんが利用するProduction版は、それぞれ別のChrome Extensionに分けています。

Beta用のリリースフローは次のとおりです。

  1. main branch を更新する
  2. GCS に拡張パッケージを upload する
  3. Chrome Web Store に申請する

Production用のリリースフローはとおりです。

  1. レポジトリにある versioning file を最新にし main branch を更新する
  2. GCS から特定の version の拡張パッケージを fetch する
  3. Chrome Web Store に申請する

また、Chrome Web Store への申請には、次のようにアップロードと申請するスクリプトを使い、自動化されています。


main();

async function main() {
  await buildPackage();
  const accessToken = await refreshAccessToken();
  await applyPackageToChromeWebStore(accessToken);
}

await function refreshAccessToken(): Promise<string> {
  const formData = new FormData();
  formData.append('refresh_token', rocess.env.REFRESH_TOKEN);
  formData.append('client_id', rocess.env.CLIENT_ID);
  formData.append('client_secret', rocess.env.CLIENT_SECRET);
  formData.append('grant_type', 'refresh_token');
  const response = await fetch(
    'https://www.googleapis.com/oauth2/v4/token',
    {
      method: 'POST',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      body: formData,
    }
  );
  const data = response.json();
  return data['access_token'];
}

async function applyPackageToChromeWebStore(accessToken: string) {
  await fetch(
    `https://www.googleapis.com/chromewebstore/v1.1/items/${process.env.APP_ID}/publish?publishTarget=default`,
    {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${accessToken}`,
        'x-goog-api-version': '2'
      }
    }
  );
}

プロダクトの品質を担保するために

モニタリング

プロダクトの品質を担保するのに、モニタリングは必須です。
システムがきちんと動作しているか、エンドユーザーにサードパーティスクリプトがきちんと配信されているか、お客さんがアプリケーションを問題なく触れているかどうかを監視することで、プロダクトの品質を担保します。

プロダクトの品質の担保にはレベルを設けています。
優先度が高い順に、次のようになっています。

  1. サードパーティスクリプトが配信されていること
  2. サードパーティスクリプトが配信できること
  3. アプリケーションが動作していること

最初にサードパーティスクリプトが配信されているかをどのように担保しているかについてです。
CDN の監視は特に大事です。サードパーティスクリプトが配信される CDN はエンドユーザーに直接的に影響を及ぼします。
もし、CDN に障害があった場合、KARTE Blocks のお客さんのお客さんにまで影響が及び、被害は広がります。

CDNを監視するために、Datadog Synthetic Monitoring で CDN からサードパーティスクリプトを問題なく fetch できるかどうかを定期的に監視しています。
具体的には 500ms 以内かつ status code が 200 で帰ってくるかどうかを1分間隔でテストしています。

次に、サードパーティスクリプトが配信できることの担保についてです。
先ほど説明したとおり、ウェブサイトの変更内容の保存に伴って、サードパーティスクリプトを配信します。
その、サードパーティスクリプトの生成と配信が常に成功していることを監視するため、Datadog Metric Monitor で一定時間内での失敗率の監視をしています。
ある単位時間内での配信失敗率が閾値を超えたとき、slack に通知が飛ぶようになっています。

最後に、アプリケーション(管理画面など)が正常に動作していることの監視についてです。
Datadog RUM と bugsnag を用いて、フロントエンドがきちんと動作しているか、エラーが発生していないか、またパフォーマンスが落ちてないかをチェックしています。
さらに、Datadog APMを使い、バックエンドのパフォーマンスも同時に確認しています。
たとえば、API の response time の監視して、response time の遅い API には改善の余地があるかを調査したりしています。
もちろん、Blocks のバックエンドはほとんどが k8s 上で動いているため、k8s 標準の liveness/readiness prove による health check は前提として行っています。

今までみてきたように、Blocks ではプロダクトの品質を担保するために優先度をつけ、監視をするようにしています。
エンドユーザーへの影響度が高くなるにつれ、エラーへの対処法が緊急になりうように、優先度は設計されています。

社内 dog fooding による品質テストと改善

プロダクトの機能を開発を単に開発し、リリースするだけでは、プロダクトの品質は担保できません。
まずは、自分達で触ってみて、開発した機能をお客さんにリリースできるかを判断します。
もし、品質が低ければ機能を改善し、バグがあれば直します。
これによって、できるだけプロダクトが安定的に動作するようにしています。

また、KARTE BlocksのウェブサイトでKARTE Blocksを利用してウェブサイトを改善するといった自己利用での改善も行なっています。

今後の展望

KARTE Blocks はまだまだリリースされたばかりの赤ちゃんプロダクトです。
KARTE Blocks で効率化できる業務のカバー範囲を広げたいし、より使い勝手も良くしていきたいです。
また、技術的負債の解消もしていきたい。

具体的にはまだまだやりたいことがたくさんあります!

ぜひ、KARTE Blocks Team に来てください!待ってます!

あとがき

本稿では、KARTE Blocks をどのように開発しているのか、その裏側、特に技術に関して紹介していきました。

最後まで読んでいただきありがとうございます!

また、この記事は「KARTE Blocksリリースの裏側」という連載の1日目の記事です。全10回を予定しています。
これから毎日記事を更新していくため、更新をチェックしたい方は@KARTE_BlocksのTwitterアカウントをフォローしてください!

この連載では、ソフトウェア的な技術だけではなく、プロダクト改善、デザイン開発、リスク管理などの考え方などさまざまなテーマの記事を公開していきます。
またこの連載は、メルカリさんの連載:「メルカリShops」プレオープンまでの開発の裏側を参考にさせていただきました。

この「KARTE Blocksリリースの裏側」という連載では、KARTE Blocksやそれを構成する技術などについて詳しく書いていくため、是非楽しんでいってください!

最後に、KARTE Blocks自体の開発に興味がある!というエンジニア(インターンも!)を募集しています!
詳しくは弊社エンジニア採用ページ採用スライドをご覧ください!


  1. https://webpack.js.org/ ↩︎

  2. https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/Invalidation.html ↩︎

  3. https://developer.mozilla.org/ja/docs/Web/Security/Same-origin_policy ↩︎

  4. https://cloud.google.com/bigquery/ ↩︎

  5. https://cloud.google.com/appengine/ ↩︎

  6. https://cloud.google.com/dataflow/ ↩︎

  7. https://cloud.google.com/pubsub/ ↩︎

  8. Neiman-Pearson Hypothesis Testing の話をするのがめんどくさかったです!詳しくは、論文や書籍を参照してください! ↩︎

  9. https://gist.github.com/Gab-km/3705015 ↩︎

  10. https://cloud.google.com/build/ ↩︎

  11. https://cloud.google.com/artifact-registry ↩︎

  12. https://github.com/opencontainers/image-spec ↩︎

Comments