9割を自宅で完結?Jamstack を学んで開発環境をイチから作ってみた話
2022年09月28日 水曜日
CONTENTS
イラストレーターの妻が「ポートフォリオサイト欲しい」と言い始めたので、面白そうだと思い開発を開始しました。
静的サイトなら Jamstack ってのがイイらしいと最近よく聞くものの、何かはわからないので勉強してみて、巷の Jamstack 構築環境をできる限り自分の家の中に作ってみます。
結論から言うと適材適所なのですが、皆さんもコーポレートサイトやブログなど「これって Jamstack がハマるんじゃないかな」と思える箇所があると思います。
是非、検討する材料としてこの記事をご覧ください。
Jamstack
Netlify 社が定義した JavaScript + API + Markup (JAM) の構成を Jamstack と呼ぶようです。
まずは Jamstack の何が嬉しいか、見ていきましょう。
従来の Web サイト
従来の構成でサーバサイドがレンダリングした Web サイトを表示するまでの(極端な)例を考えてみます。
- ブラウザからリクエストを送る
- ロードバランサがリクエストを Web サーバに
- Web サーバがアプリケーションサーバを call する
- アプリケーションサーバは DB などから情報を取得する
- アプリケーションサーバが取得した情報からレスポンスを生成する
- Web サーバがレスポンスをブラウザに橋渡しする
次に Jamstack が示すシステム構成がどんなものか、見てみます。
Jamstack 構成の Web サイト
従来の Web サイトを Jamstack 構成のサイトに置き換えた場合の(極端な)例を考えてみます。
- ブラウザから CDN にリクエストを送る
- CDN がファイルを返す
これだけです。
Jamstack とは
The core principles of pre-rendering, and decoupling, enable sites and applications to be delivered with greater confidence and resilience than ever before.
https://jamstack.org/what-is-jamstack/
what-is-jamstack 曰く、Jamstack は Pre-rendering 及び Decoupling を基本原則としているようです。
Pre-rendering とは「事前に HTML を生成しておくこと」を意味し、Jamstack では事前にできる限り最適化された静的なファイルでまとめることを目指します。
後述する SSG の機能を備えたフレームワークなどが、この原則を満たしやすい環境を作っています。
また、Decoupling は日本語にするなら「分離」でしょうか。
全てのコンテンツが Pre-rendering できるわけでもなく動的なコンテンツを取得しなければならないシーンも存在します。
そんな場合でもできるだけコンポーネントを分割し、API を用意して JavaScript でそれらを操作しようという思想ですね。
Jamstack を支える技術
と、前章では大げさに違いを誇張してしまいましたが、どうしてこんな構成で Web サイトが動くのでしょうか?
それは上述した SSG の機能を持つフレームワークの台頭に依る部分が大きいです。
SSG とは
SSG は Static Site Generator の略称で、日本語にするなら「静的サイトジェネレータ」でしょうか(ほとんど英語)。
Next.js, Gatsby, Nuxt といった Web アプリケーションフレームワークが備える機能の一つで、その名の通り静的なサイトを生成するものです。
SSG は、build 時に DB や API などに対するデータの Fetch を完了させ、json のようなもので足元に置いたり、レンダリング済み HTML を生成したりします。
これによって、build の成果物は Web サーバに置くだけで既に動く状態なる、というわけです。
メリット
完全に静的ファイルのみの場合ではデータ取得が発生しないため、サーバサイドでレンダリングした HTML を返すよりも高速になりますし、スケーリングのことも深く考える必要が無く、CDN に載せてやれば良いということになります。その際のデプロイ先の選択肢は、HTML をホスティングできる所ならどこでも良いという所も携帯性の点で優れています。
また、完全に静的なサイトでは関連するバックエンドのコンポーネントに対するセキュリティを考える必要がなくなります。
デメリット
逆に言うと、SSG に対応した Web サイトは build をする必要があるというデメリットがあります。
更新頻度が高い Web サイトだと、その都度 build を走らせる必要があるかもしれません。サイトの規模によっては build に時間がかかってしまうこともあります。
また、動的なコンテンツを動かすとなると、その部分は API への問い合わせ等を走らせるように作りこまなければならず、メリットが消えてしまう恐れもあります。
この部分では前述した通り、JavaScript による API 操作で対応しなければならない部分です。
向き不向き
つまり、動的でない、かつ更新頻度の高くないページで、かなりの効果を挙げるのが SSG と言えるでしょう。
厳密には SSG だけが Jamstack というわけでもないため、動的なページでも設計によっては Jamstack が生きてくることがあると思います。
Jamstack に関連するコンポーネント
では、Jamstack を実現する上でどのような機能が必要になるのでしょうか?
ホスティング
まずは、SSG で build した静的ファイルをどこに置くかというのが重要です。
基本的には build の成果物ディレクトリを DocumentRoot にして Web サーバを起動すれば動きます。
一方で、Jamstack 並びに SSG のフレームワークに特化して対応したホスティングサービスというのも存在します。
Netlify や Vercel, Cloudflare Pages といったサービスが有名だと思います。
GitHub と連携して Webhook などでイベントを受け、指定のブランチを build してから成果物をホスティングしてくれたりといった機能を備えており、Jamstack と親和性が高く作られています。
CMS
CMS は Contents Management System のことで、その名の通りコンテンツを管理するシステムです。WordPress などが有名でしょうか。
しかし、Jamstack では CMS を純粋なコンテンツ管理システムとして利用するので、フロントエンドとしての働きは不要です。
こんな時につかえるのが Headless CMS です。
ブログであれば記事データ、ポートフォリオサイトであれば画像データと言ったものを、定義したデータ構造を元に管理することができ、それらに対する API を自動的に公開してくれるのが、 Headless CMS です。
microCMS や Contentful といったサービスが有名です。また、 WordPress も Headless CMS のような利用方法ができます。
オープンソースの Headless CMS も存在しますが、後述します。
SCM
ソースコード管理も重要です。というのも、コンテンツが変更されたとき以外にソースが変更されたときも、build を走らせたりしたいからです。
基本的には GitHub + GitHub Actions を利用した SSG が流行しているようですが、上述した通りSSG の build 機能を有するホスティングサービスもありますので、その場合は Actions でなくホスティングサービス側での build になるでしょう。
一般的な Jamstack を実現する構成
世の人々は Jamstack をどのように開発/デプロイしているのでしょうか?
- microCMS を利用してコンテンツを作成
- Next.js で microCMS にアクセスしつつ画面を作成
- GitHub でソースを管理
- GitHub でのリリースや Headless CMS でのコンテンツ更新をホスティングサービスに通知
- Vercel が通知を受け、GitHub からソースを取得して SSG の build
- 成果物を Vercel 自身に自動で置く
こんなふうに回ってるんだそうです。
実際に Jamstack 環境を作ってみる
ここまでで Jamstack とは何ぞや、と言うものが見えてきたと思います。
今回はポートフォリオサイトという性質上、全部が静的ファイルとしてホスティング可能そうです。Jamstack を採用することにしてみます。
そうとなったら早速、上述の一般的な開発環境を真似てローカル Jamstack 環境を作っていきましょう。
タイトルにもある通り、今回は外部サービスを極力使わずに、なるべく自分の環境で立ち上げていきます。
まずは必要なコンポーネントのプロダクトや、使用する言語、フレームワークの選定をしましょう。
プロダクト選定
フレームワーク
今回は IIJ bootcamp (宣伝)を見て面白そうだと感じたので、 Svelte を利用した Web アプリケーションフレームワークの SvelteKit を使ってみます。
Headless CMS
OSS な Headless CMS の選択肢は本当に少ないです。
今回は strapi を利用することにしました。ほかに良い感じの self-hosted な Headless CMS でおすすめがあればおしえてください。
SCM
ソースコードの管理は Gitea を利用しました。
サクっと立ち上がるし良い感じです。GitLab ほどの高性能さは不要で、極力軽量なフットプリントのモノを選びます。
CI/CD
Woodpecker CI を選びました。これは Drone を fork した OSS の CI/CD プロダクトです。
Drone は社内の GitHub Enterprise でも利用しているため、慣れた使い勝手のモノを選んだ方が良いという選択です。
ホスティング
自宅内で完結できなかった残りの一割はここです。
契約している VPS に載せようかとも思いましたが、どうせならここは CDN を利用してみたいということで、今回は Cloudflare Pages を利用します。
選定理由は主にお金の面です。Cloudflare Pages は build 回数の制限こそあれ、手元で build して静的ファイルを配置する分にはとくにお金などかかりません。
Vercel や Netlify といったサービスに存在する帯域幅の制限もありません。
構想
ここまでで JavaScript(Svelte) + API(Strapi) + Markup(SvelteKit) と JAM に対応する部分とそれに付随するコンポーネントを選定しました。
ここからはこれらを組み合わせて実際に巷の Jamstack 実装環境のようなものを宅内に作ってみます。
目指すものとして自分は画面を開発し、利用者はコンテンツの管理のみに注力できるような環境です。
- 開発者の自分は手元に起動した開発環境で Headless CMS を参照しつつ開発する
- ソースを SCM に push し、merge をトリガに CI/CD がビルド/リリースを行う
- 利用者は Headless CMS の管理画面でコンテンツを書き換えるだけで本番環境が書き換わるようにする
- CMS の Webhook が SCM の API を叩いてリリースタグを打ち、それをトリガに CI/CD がビルド/リリースを行う
構築
Strapi/Gitea/Woodpecker は Docker で一つのマシン上に構築しました。
これらのコンテナは同じ Docker network に設定し、同じ network 上に nginx-proxy を置いています。
nginx-proxy は、各コンテナ起動時に与えた環境変数を読んで、その名前を持つ virtual host を自動で生やし、アクセスをさばいてくれるリバースプロキシです。
wildcard な証明書を食わせておけば、勝手に証明書も読み込んでくれます。作っててよかった wildcard 証明書。
それぞれの名前は、宅内のルータ(EdgeRouter X)で名前とマシン名を対応させており、宅内からであれば名前でアクセスが可能になっています。
作りこみ
Headless CMS の Webhook
今回利用した Headless CMS の Strapi は、コンテンツ更新時に Webhook を叩くことができます。
最初はこれを利用して Gitea のリリースタグを打つ API を叩いてやりたかったのですが、この Webhook 機能は POST にリクエストボディを指定することができません。
というわけで、CMS のサイドカーコンテナとして、パスで指定した名前の Repository にリリースタグを打ってくれるプログラムをサクッと書いて建てておきました。
docker-compose で横に生やしているので、 http://<service名>/<repository> を指定し Webhook を発行すれば、 Gitea の main ブランチを参照してリリースタグを打って CI/CD を走らせるようにしています。
SSG 時にコンテンツを含める
今回は CMS をグローバルに置くつもりはないので、画像を含むすべてのファイルを成果物として含むように build したいのですが、実はこの構成だとなかなかに難しいです。
画像の参照先を CMS にしている限り、img タグの src は外部のリソースになるため、sveltekit の SSG はそのまま URL を埋め込んで build してしまいます。
minista のような一部のフレームワークでは、外部リソースも build 時にダウンロードして URL を変更するような処理がありますが、著作権的な側面も相まってすべてのフレームワークに標準搭載されるべき機能だとも思いません。
というわけで、今回はまずソース側に、「開発環境であれば CMS を見る」「build 時であれは足元を見る」修正を入れます。
CMS は画像ディレクトリに Docker Volume を mount、CI/CD で build する際に別途同じ Docker Volume を mount する設定をして、build 時には足元に画像があるかのように振る舞う設定を施しました。
これにより、成果物には CMS に存在するすべての画像のうち、ソースから参照されているモノのみ含まれるようになりました。
この構成は作りこむのが一見面倒ですが、コンテンツを取得するためにグローバルに CMS を晒す必要が無く、従って Headless CMS を参照するためのクレデンシャル等も考慮しなくて良いのでセキュアです。
せっかく静的ファイル群が CDN に載せられていても、Headless CMS のコンテンツが CDN に載っていない場合はここがボトルネックになってしまう可能性もあります。この点でも作りこんで良かったポイントであると感じています。
Cloudflare Pages へのデプロイ
Cloudflare Pages へのデプロイは、ちょっと前まで Git ベースのモノだったらしいのですが、最近は wrangler という CLI で直接アップロードが可能なようです。
CI/CD はビルド完了後に成果物のディレクトリを丸ごと Cloudflare Pages にアップロード、更に API を叩いてキャッシュ自動で全パージするようにしておきました。
利用時の流れ
ここまでの作りこみを通じて、以下のような環境が達成されました。
- 開発者
- 足元で Sveltekit を起動して実装を進める
- コンテンツの取得先は Strapi の URL を指定する
- ソースを Gitea に push する
- Woodpecker で build が走る
- 成果物が cloudflare pages にデプロイされる
- 利用者
- コンテンツを Strapi で編集する
- 編集の完了時に Webhook が走り Gitea にリリースタグが打たれる
- タグ打ちを検知して Woodpecker で build が走る
- 成果物が cloudflare pages にデプロイされる
感想
開発環境を作りこむためのコストが高いものの、一度動き出せば非常に楽できるんじゃないかとおもいました。
また、 SSG を細かくコントロールするために、特定環境に向けた作りこみをしてしまったのが反省ポイントです。この辺りはプロダクト選定をうまくやれば回避できたかもしれません。
例えば「コーポレートサイト刷新したいけれど、Jamstack って言うのがイイっぽいな。でもコンテンツもソースも閉じた環境で管理したいなあ」という方々にとって参考になる資料となれば幸いです。