Classi開発者ブログ

教育プラットフォーム「Classi」を開発・運営するClassi株式会社の開発者ブログです。

monorepoでのWeb Components配信アーキテクチャ

コミュニケーションチームのid:NozomuMiyamotoです。

Classiでは、2021年12月のAngularJSのEOLに向けて、Web Componentsを利用した段階的AngularJS脱出作戦を進めてきました。これはAngular Elementsと呼ばれるAngularで作成したコンポーネントをWebComponentsに変換する技術を用い、部分的にシステムリプレースを行う作戦です。 今回は、そこでぶつかった課題と解決に用いたWebComponentsの配信アーキテクチャについて紹介します。

背景

Classiにはplatformと呼ばれる最初期から存在する巨大なリポジトリがあります。このリポジトリには様々な言語で書かれた複数のサービスが共存しており、全体像を把握することが非常に困難です。これを管理可能なレベルまで解体するとともに、EOLを迎えるシステムの置き換えを行うことが、コミュニケーションチームのミッションの1つです。前述の動画で紹介したAngular Elementsを用いた脱出作戦は、フロントエンドのスコープで、それを果たすための施策でした。

当初、AngularElementsのリポジトリは機能ごとに作られており、成果物を手動でplatformに移植する運用がなされていました。勿論、こうした運用は望ましくなく、オペミスによる障害も引き起こしかねません。ただ、こうした問題の改善をしようにも、リポジトリが分かれていては、その分だけメンテナンスに力がいります。こうした事情から、我々はNxを用いて機能別にリポジトリが散らばったAngular Elementsのモノレポ化を行い、アジリティと安全性を高めるためにデプロイフローの改善に踏み出しました。

問題

モノレポ化により開発体験の向上という恩恵が得られましたが、また別の課題に直面することにもなりました。それはバージョン管理と切り戻しに関する問題です。 全ての機能のAngularElementsを単一のパッケージでバージョン管理した場合、過去のリリースで障害が発生した際に関係ない機能まで切り戻す必要が生じてしまいます。

例えば、アンケート機能とカレンダー機能の2種類の機能がAngular Elementsで提供されており、同一バージョンのパッケージしか利用できないとしましょう。ここで以下のようなケースが考えられます。

  1. アンケートへの機能追加のためにAngular Elementsのパッケージをv1.1.0にアップデートする。
  2. カレンダー機能の修正のためにAngular Elementsのパッケージをv1.2.0にアップデートする。
  3. 1週間後、1. のリリースが原因で致命的な欠陥が見つかる。
  4. Angular Elementsのバージョンをv1.0.0まで切り戻す必要が生じる。

このケースでは、障害の原因はアンケートへの機能追加であるにも関わらず、原因と無関係なカレンダー機能の修正まで切り戻されてしまいます。

目的

この問題は、配信パッケージを分けるなどの方法で機能ごとにバージョンを選択できる状態にしてやれば簡単に解決します。ただ、背景で述べた通り、platformレポジトリはすでに複数のコードにより煩雑になっているため、これ以上、依存関係や開発環境を複雑にするのは望ましくありません。そこで、ここではplatformレポジトリにできる限り変更を加えず、機能ごとのバージョン指定ができるようにすることを目指します。

採用したアプローチ

結論から言えば、我々はURLベースでWebComponentsのバージョン管理を行う手法を採用しました。これは、次のようなscriptタグを用いて、指定したバージョンのWebComponentsを読み込む手法です。

<script src="https://cdn.classi.jp/elements/v1.0.0/questionnaire.js"></script>

(古くからあるCDN経由でJavaScriptのライブラリを読み込み方法ですね)

非常にシンプルな手法ですが、移植先の環境に殆ど変更を加える必要がないですし、npmなど、JSのパッケージマネージャーがない環境でも容易に扱えます。唯一、CDNの環境を整えることだけが少々手間ですが、この作戦の最後にはAngularアプリケーションとして独立したSPAをCDN経由で配信するためこれは将来への投資になると考えられます。

アーキテクチャ

具体的には、Conventional Commit + semantic-release + GitHub Actions +S3 + CloudFrontの構成で下記の流れで、機能ごとに指定したWebComponentsのパッケージを読み込みます。

  1. 開発者がConventional Commitに従ってコミットしてoriginにpushする。
  2. GitHub ActionsがSemantic Releaseを実行し自動バージョンニングを行う。
  3. GitHubActionsがビルドされた成果物をS3 Bucketにアップロードする。
  4. 開発者がplatformのscriptタグの向き先を任意のバージョンのURLに変更する。(※)

※ ここで次のようにバージョンを指定できるため、機能ごとのバージョンアップ(もしくは切り戻し)が可能となる。

<script src="https://cdn.classi.jp/elements/v1.1.0/questionnaire/main.js"></script>
<script src="https://cdn.classi.jp/elements/v1.0.0/calendar/main.js"></script>

採用しなかったアプローチ

もちろん、他にもいくつかのアプローチを検討しましたが、それらについて採用に至らなかった理由を以下で述べます。

npm + Nx Multi Packaging

Nxのマルチパッケージング機能を用い、機能ごとにAngularElmentsをバージョニング・パッケージングし、npmのprivate repositoryで配信する手法です。npm linkが利用できることは開発においても魅力的ですが、フロントエンジニア以外もplatformレポジトリを頻繁に触るため、アクセスキーの取得などが開発環境を複雑にすると考え採用に至りませんでした。

GitHub Release

GitHubのRelease機能を用いて機能ごとに各バージョンのビルドされたWebComponentsを保管し、GitHub Actionsからそれらを取得する手法です。GitHub Actions内で取得する各機能のバージョンを指定するために、何らかのバージョン管理手段を別途用意する必要があるため採用しませんでした。

Git Submodule

AngularElementsのモノレポをplatformのsubmoduleにし、サブモジュールごとビルドする手法。AngularElementsのモノレポ化の構想段階ではこの手法を用いる予定でしたが、今回の切り戻しの問題が懸念されたため採用しませんでした。

むすび

WebComponentsはその性質上、一般公開を前提とするため、URLに基づくバージョン管理で十分に要件を満たせると考えます。また、シンプルな構成で利用先への要求がscriptタグの利用以外ないことは大きなメリットです。Railsなどのnpmを用いていない環境下でのシステムリプレイスにも役に立つでしょう。

残された課題として、コンポーネントの粒度でも同じ問題が起こり得ることが挙げられます。これを防ぐためには、同様にコンポーネントの粒度でバージョン指定を可能にする必要がありますが、その場合、各コンポーネントに@angular/coreの一部がバンドルされることになるので全体のファイルサイズが膨れ上がります。それを避けるためには、共通モジュールのみを初期でロードし、各コンポーネントは必要に応じてLazyLoadできるような構成を取る必要があるでしょう。

WebComponentsを用いた取り組みはまだ事例が少なく、我々も手探りで進めている状態ではありますが、その分、この技術には大きな可能性を感じています。Classiでは現在も開発者を募集しておりますので、こうした取り組みにご興味のある方は、ぜひ一度ご連絡ください!

それではまた次回の投稿をお楽しみに!

© 2020 Classi Corp.