最新ウェブ アーキテクチャの「ジグソーパズル」で作業を簡単に

Google Cloud Japan Team
※この投稿は米国時間 2021 年 4 月 30 日に、Google Cloud blog に投稿されたものの抄訳です。
今や、ウェブベースのソフトウェアの配信は、LAMP サーバーへの SSH の実行や vi による php ファイルの編集のような、単純なものではなくなっています。当然、多くの人々が、複雑化や最新のニーズに対処するための実践方法を考案しテクノロジーを取り入れています。このほど、さまざまなテクノロジーと実践方法の「ジグソーパズル」を組み立てました。これにより、コンテナベースのポータビリティ、インフラストラクチャ自動化、継続的インテグレーション / 継続的デリバリーを活用しながら、エッジ キャッシングされグローバルに分散したスケールオンデマンドのウェブアプリのデプロイが可能になります。
パズルの主要ピースとなるのは、Java 仮想マシン(JVM)、Scala、sbt、Docker、GraalVM、Cloud Native Buildpack、bash、git、GitHub Actions、Google Cloud Run、Google Cloud Build、Google Cloud CDN、Google Container Registry、Google Domains などです。
実に多くのピースがあります。まず、私が解決したユースケースを見ていきましょう。
グローバルに分散したスケールオンデマンドの JavaDocs
JVM エコシステムのライブラリ(Java、Kotlin、Scala などで作成)は一般に、Maven Central と呼ばれるリポジトリに公開されます。現在そこには 600 万を超えるアーティファクト(あるライブラリの各バージョン)があります。ライブラリの作者は通常、ライブラリを公開するときに、バージョニングしたドキュメント(つまり JavaDoc)が含まれるアーティファクトを同梱します。これらのアーティファクトは基本的に ZIP ファイルであり、生成された HTML が含まれています。ライブラリを使用すると、通常、IDE 内または公開先ウェブページ上の JavaDoc が参照されます。
面白い実験として、Maven Central から JavaDocs を pull してウェブページに表示するウェブサイトを作成してみました。Java ライブラリに精通していて何か思い当たる方は、当方のウェブサイトをご参照ください。
たとえば、gRPC Kotlin スタブ JavaDocs を確認してください。
https://javadocs.dev/io.grpc/grpc-kotlin-stub/latest
サイトは、どこからでも非常に速く読み込まれたはずです。それは、大規模なエッジ キャッシングによってグローバルに分散したスケールオンデマンドのウェブアプリが作成されているからです。ランタイム アーキテクチャがどのようになるか、以下に示します。
1. io.grpc:grpc-kotlin-stub の最新の JavaDocs を入手


2. JavaDoc の index.html を入手
io.grpc:grpc-kotlin-stub:1.0.0


何よりも好都合なのは、わずかな行を使って Cloud Build を構成すると、マージ時にシステム全体がメインに継続的に配信されることです。種明かしとして、同じようにエッジキャッシュされグローバルに分散したスケールオンデマンドのウェブアプリを作成するのに必要なビルド構成の全文を、以下に示します。
操作をこれほど簡易化してアプリの起動を高速化するためには、数多くの異なるピースを接ぎ合わせる作業が必要でした。順を追って全体をご説明します。
デベロッパー エクスペリエンスを損なわない超高速起動
「JavaDoc Central」ウェブアプリは、Maven Central のメタデータとアーティファクトのプロキシです。リポジトリからメタデータをクエリする必要があります。たとえば、「最新」というバージョンを実際の最新バージョンに変換するなどです。ある特定のアーティファクトの JavaDoc をユーザーからリクエストされたら、その関連 JavaDoc を Maven Central から pull して抽出し、コンテンツを提供する必要があります。
従来のウェブアプリ ホスティングは、リクエスト到着時にサーバーでの処理準備ができている、オーバープロビジョニングに依存していました。スケールオンデマンドのアプローチはもっと効率的です。つまり、基盤となるリソースは、リクエストが来ると動的に割り振られますが、リクエストの数が減少すると自動的に割り振りが解除されます。これは自動スケーリングまたはサーバーレスとも呼ばれます。スケールオンデマンドで有利なのは、サーバーが十分に活用されず無駄になってしまうことがないという点です。ただし、このアプローチにも課題はあります。利用可能な供給(基盤となるサーバー)を上回る需要(リクエスト数)があったときには、新しいサーバーを起動して過剰な需要を処理できるようにする必要があるため、アプリケーションをきわめて速く起動する必要があるという点です。これは「コールド スタート」と呼ばれ、プログラミング プラットフォーム、アプリケーションのサイズ、キャッシュ ハイドレーションの必要性、接続プーリングなどの多くの変数に応じて、さまざまな影響を及ぼします。
コールド スタートは、サーバーがない状態からスケールアップするときだけでなく、需要が供給を上回るときにはいつでも発生します。
コールド スタートの問題に簡単に対処する手段として、起動オーバーヘッドがあまり大きくないプログラミング プラットフォームを使用する方法があります。JVM ベースのアプリケーションは一般に、JVM に起動オーバーヘッドがあること、JAR の読み込みに時間がかかること、依存性注入のためのクラスパスのスキャンが遅くなる可能性があることなどの理由から、起動に数秒以上かかります。そのため、スケールオンデマンドのアプローチでは、Node.js、Go、Rust などのテクノロジーが広く使われるようになっています。
それでも、私は好んで JVM を使用しています。これには、豊富なライブラリとツール エコシステムや、最新の高度なプログラミング言語(Kotlin と Scala)など、さまざまな理由があります。JVM での生産効率は驚くほど高く、スケールオンデマンドへの対応性を改善するだけのためにその効率を手放したくはありません。詳細については、当方のブログ The Modern Java Platform - 2021 Edition をお読みください。
幸い、二兎を追って二兎とも得られる方法があります。GraalVM ネイティブ イメージは、JVM ベースのアプリケーションを、JVM で実行するのではなく、ネイティブ アプリケーションに事前(AOT)コンパイルします。しかし、その処理には時間がかかります(数秒単位ではなく数分単位)。私としては、開発サイクル中にそれを待つ時間を費やしたくはありません。ありがたいことに、JVM ベースのアプリケーションは、JVM とネイティブ イメージで実行できます。私が JavaDoc Central コードで行っていることが、まさにそれなのです。私の開発ワークフローをご紹介します。


GraalVM でのネイティブ イメージを作成するために、ビルドツール プラグインを使用しました。私は Scala と sbt ビルドツールを使用しているため sbt-native-packager プラグインを使用しましたが、Maven と Gradle 用にも類似のプラグインがあります。これによって、継続的デリバリー システムでコマンドを実行し、JVM ベースのアプリケーションから AOT ネイティブの実行ファイルを作成できます。
GraalVM ネイティブ イメージでは、オプションでネイティブ イメージを静的にリンクし、オペレーティング システムがなくても実行できるようにすることさえ可能です。その結果、静的にリンクされた JavaDoc ウェブアプリのコンテナ イメージは全体でわずか 15 MB で、起動に要する時間は 1 秒を優に下回ります。オンデマンド スケーリングに最適です。
マルチリージョン デプロイの自動化
私が初めて javadocs.dev サイトをデプロイしたときには、15 MB のコンテナ イメージを実行する Cloud Run でのサービスを手作業で作成しました。しかし、Cloud Run サービスはリージョンベースであるため、ユーザーのいる場所によって到達までのレイテンシが異なります(地球を 1 周する TCP トラフィックには光の速度はきわめて遅いことがわかります)。Cloud Run は 24 の Google Cloud リージョンすべてで利用できますが、それらすべてのサービスと、ルーティングを処理する関連ネットワーキング インフラストラクチャを、手作業で作成するのは回避したいと考えました。Cloud Run に関しては、複数のリージョンからのトラフィックの処理という素晴らしいドキュメントがあります。n 個の Cloud Run サービスの前に Google Cloud ロードバランサを作成するための全手順が解説されています。それでも、私はそのすべてを自動化したいと考えたため、課題をさらに複雑化させる方向に歩みだしました。しかしその結果、グローバルなデプロイ、ネットワーク構成、グローバルな負荷分散を自動化する、現在使用中の素晴らしいツールが得られました。
インフラストラクチャのセットアップを自動化するには、Google Cloud の Terraform サポート など、数種類の方法があります。しかし私が必要としていたのは、単にいくつかの gcloud コマンドを実行してくれるコンテナ イメージでした。これらのコマンドを書くのは非常に簡単ですが、それをコンテナ化して、自動化されたデプロイで容易に再利用できるようにしたいと考えました。
このようなものをコンテナ化する場合、コンテナで実行可能なデータをソースから生成するために必要な手順を、Dockerfile を使用して定義するのが一般的です。しかし、Dockerfile はコピーと貼り付けでしか再利用できず、その結果、セキュリティとメンテナンスのコストが生じます。そしてそのコストは、最初から明確になっているわけではありません。そこで、gcloud 自動化用コンテナを作成するために誰でも再利用できる gcloud スクリプト用の Cloud Native Buildpack をビルドしようと決めました。Buildpack では、ソースをコンテナ内で実行可能なデータに変換するロジックを再利用することが可能です。
1 時間ほどかけて Buildpack の作成方法を学び、gcloud-buildpack を完成させました。Buildpack はソースをコンテナ イメージに変換する処理を省いてくれるため、知っておかないと困るような情報は少ししかないのですが、内部の様子を理解するために、詳しくご説明します。
Buildpack 実行イメージ
Buildpack は、「実行イメージ」に Docker レイヤを追加します。そこで、Buildpack ではそれらのいずれかが必要になります。私の gcloud-buildpack には、gcloud コマンドを含む実行イメージが必要です。そこで、gcloud のベースイメージをベースにし、Buildpack 向けに必要な 2 つのラベル(Docker メタデータ)を持った、新しい実行イメージを作成しました。
また、実行イメージを自動的に作成してコンテナ レジストリに保存できるようにするとともに、変更があるたびにコンテナ イメージが更新されるようにするための、セットアップ自動化も必要でした。ビルドの実行には GitHub Actions を使用し、コンテナ イメージの保存には GitHub Container Registry を使用することにしました。Actions の YAML は次のとおりです。
以上です。実行イメージが利用可能になり、継続的にデプロイされます。
gcloud Buildpack
Buildpack は、Cloud Native Buildpack ライフサイクルに参加しており、少なくとも 2 つのフェーズ(検出とビルド)を実装する必要があります。複数の Buildpack を組み合わせると、次のようなことを実行できます。
そして、ビルダー イメージ内のすべての Buildpack に対し、特定の対象のビルド方法を把握しているか確認が行われます。Google Cloud Buildpacks は、Java、Go、Node.js、Python、.NET のアプリケーションのビルド方法を把握しています。私の gcloud Buildpack の場合、ビルダー イメージに追加するつもりはなかったため、検出結果が常に正と出るようにしました(つまり、Buildpack はどのような場合も実行されます)。それを実現するために、私の検出スクリプトはエラーなしで単純に終了します。注: Buildpack は、Docker 内のビルドイメージ内部で実行されるため、任意のテクノロジーで作成できます。私は、訳あって Bash で書くことにしました。
私の gcloud Buildpack の次のフェーズは、ソースの「ビルド」です。しかし、Buildpack はシェル スクリプトを実行イメージに追加するため、必要なのは、スクリプトを適切な場所にコピーして、それが実行可能 / 起動可能であることを Buildpack ライフサイクルに伝えることだけです。ビルドコードをご確認ください。
Buildpack はコンテナ イメージ経由で使用できるので、gcloud Buildpack をビルドしてコンテナ レジストリに公開する必要があります。ここでも GitHub Actions を使用しました。
gcloud Buildpack が導入され、グローバルに負荷分散したサービスを Cloud Run で容易にデプロイできるようにするコンテナ イメージの作成準備が整いました。
簡単な Cloud Run
マルチリージョン Cloud Run アプリのセットアップ手順を自動化して、すべて CI / CD パイプラインの一部として実施できるようにする bash スクリプトを作成しました。興味のある方は、ソースを参照してください。新しい gcloud-buildpack を使用して、GitHub Actions でコマンドをコンテナ イメージにパッケージ化できました。
これで、誰でも ghcr.io/jamesward/easycloudrun コンテナ イメージを 6 つの環境変数とともに使用して、グローバル ロードバランサのセットアップとマルチリージョンのデプロイを自動化できるようになりました。これを javadoccentral リポジトリについて実行する場合、次のようになります。
ネットワーキングとロードバランサの構成は(既存のものがない場合)すべて自動的に作成され、Cloud Run サービスは、ロードバランサ以外とは通信できないように --ingress=internal-and-cloud-load-balancing オプションを付けてデプロイされます。ロードバランサでは http から https へのリダイレクトも作成されます。Google Cloud Console 内でのロードバランサとネットワーク エンドポイント グループは次のようになります。


24 の Google Cloud リージョンによってサポートされるサーバーレスのグローバル分散アプリケーションのセットアップはすべて、CI / CD パイプラインの一部として約 1 分間で実施されます。
Cloud Build CI / CD
これをすべて 1 つにまとめて、javadocs.dev アプリケーションのテスト、GraalVM ネイティブ イメージ コンテナの作成、マルチリージョンのデプロイを行うパイプラインを実現しましょう。私は Cloud Build を使用しました。Cloud Build では、GitHub 統合があり、ビルドの権限の管理にサービス アカウントが使用される(それによって Cloud Run のデプロイ、ネットワーク構成のセットアップなどの実現が容易になる)からです。Cloud Build の定義(GitHub 上のソース):
1 つ目のステップで、アプリケーションのテストを実行します。2 つ目のステップで、GraalVM ネイティブ イメージを使用してアプリケーションをビルドします。3 つ目のステップで、コンテナ イメージを Google Cloud Container Registry に push します。最後に 4 つ目のステップで、ロードバランサ / ネットワークのセットアップを実施し、アプリケーションを 24 のリージョンすべてにデプロイします。GraalVM ネイティブ イメージは多量のリソースを使用するためビルドに大規模マシンを使用していることに注目してください。その CI / CD パイプラインでのカスタム値は、ロードバランサのセットアップに必要な DOMAINS のみです。それ以外はすべてボイラープレートです。
パズルが完成
ピース数の多い、高難度のジグソーパズルでした。すべてのピースが組み立てられた今、皆様は簡単に、Google Cloud でサーバーレスのグローバル分散アプリケーションを独自に作成してeasycloudrun でデプロイできるようになったはずです。または、gcloud Buildpack を使用して独自の自動化を作成するのもよいでしょう。どちらの場合でも、何かお困りのことがありましたらお知らせください。
-デベロッパー アドボケイト James Ward