🚀

SvelteKit の Web サービスを Cloudflare Pages にデプロイする

2023/12/10に公開

この記事は LabBaseテックカレンダー Advent Calendar 2023 の 10 日目です。

https://qiita.com/advent-calendar/2023/labbase

はじめに

今回 SvelteKit 製の Web サービスを Cloudflare Pages にデプロイする方法をセットアップしたので、その手順と内容について解説します。

Cloudflare Pages は、 GitHub と連携させてリポジトリを更新したときに自動でデプロイされるような使い方が一般的だと思いますが、今回は GitHub とは連携せず、手元でスクリプトを実行してデプロイできるようにセットアップしています。

サンプルプロジェクトについて

https://github.com/hotwatermorning/cloudflare-test

これは SvelteKit 製の Web サービスを Cloudflare Pages でホスティングするサンプルプロジェクトです。データベースとして Cloudflare D1 を使用し、データベースのスキーマの定義とマイグレーションには Drizzle ORM を使用しています。

Web サービスはデータベースにアクセスする最小限のコードだけ実装されていて、ユーザー登録画面とユーザー一覧画面のみが存在します。


以下では、このプロジェクトのコードを元に使い方と仕組みについて解説します。

使い方

事前に Cloudflare のアカウント ID と API トークンを用意して、以下のように環境変数を設定しておきます。(この API トークン は Edit Cloudflare Workers テンプレートに D1 Database の Edit パーミッションを付加したものとして作成します。)

export CLOUDFLARE_ACCOUNT_ID="<Cloudflare のアカウント ID>"
export CLOUDFLARE_API_TOKEN="<生成した API トークン>"

その状態で

./deploy.sh local

のようにしてスクリプトを実行すると、以下の処理を行ってデプロイ処理を実行します。

  • Terraform で Cloudflare Pages と Cloudflare D1 の構成をセットアップ
  • SvelteKit プロジェクトのビルド
  • データベースに対して Drizzle ORM で生成したマイグレーションファイルを適用
  • ビルドした SvelteKit のプロジェクトを Cloudflare Pages へデプロイ

deploy.sh に渡す引数を preview や prd に変更すると、それぞれプレビュー用、プロダクション用にデプロイできます。

以下のスクリーンショットはプレビュー用にデプロイしたときのもので、デプロイが完了するとアクセス用の URL が発行されます。

deploy.sh の処理について

deploy.sh は以下のような内容になっています。

deploy.sh
#!/bin/bash

set -e -u -o pipefail

realpath() {
  [[ $1 = /* ]] && echo "$1" || echo "$PWD/${1#./}"
}

SCRIPT_NAME=$(basename "$(realpath "${BASH_SOURCE:-$0}")")
SCRIPT_DIR=$(dirname "$(realpath "${BASH_SOURCE:-$0}")")

if [ $# -ne 1 ]; then
  echo "Deploy mode is required." 1>&2
  echo "Usage: ${SCRIPT_NAME} <local|preview|prd>" 1>&2
  exit 1
fi

if ! command -v terraform; then
  echo "terraform is not found." 1>&2
  exit 1
fi

if ! command -v jq; then
  echo "jq is not found." 1>&2
  exit 1
fi

if ! command -v yarn; then
  echo "yarn is not found." 1>&2
  exit 1
fi

DEPLOY_MODE=$1

cd "${SCRIPT_DIR}/terraform"

# (1) Terraform を使った Cloudflare のセットアップ
export TF_VAR_cloudflare_account_id=${CLOUDFLARE_ACCOUNT_ID}
export TF_VAR_cloudflare_api_token=${CLOUDFLARE_API_TOKEN}
terraform init
terraform apply -auto-approve

# (2) セットアップしたリソース情報の取得
export CLOUDFLARE_PAGES_PROJECT_NAME=$(terraform output -json | jq -r ".app.value.name")
export CLOUDFLARE_ACCOUNT_ID=$(terraform output -json | jq -r ".account_id.value")
export CLOUDFLARE_DB_ID=$(terraform output -json | jq -r ".db.value.id")
export CLOUDFLARE_DB_NAME=$(terraform output -json | jq -r ".db.value.name")
export CLOUDFLARE_DB_PREVIEW_ID=$(terraform output -json | jq -r ".db_preview.value.id")
export CLOUDFLARE_DB_PREVIEW_NAME=$(terraform output -json | jq -r ".db_preview.value.name")

cd ..

# (3) リソース情報を含んだ wrangler.toml の生成
envsubst < wrangler.toml.in > wrangler.toml

# (4) プロジェクトのビルド
yarn
yarn build

# (5) DB のマイグレーションとデプロイの実行
if [ "${DEPLOY_MODE}" = "prd" ]; then
  echo "Deploy production"
  echo "y" | yarn wrangler d1 migrations apply ${CLOUDFLARE_DB_NAME}
  yarn wrangler pages deploy .svelte-kit/cloudflare --project-name my-pages-app --branch main
elif [ "${DEPLOY_MODE}" = "preview" ]; then
  echo "Deploy preview"
  echo "y" | yarn wrangler d1 migrations apply ${CLOUDFLARE_DB_PREVIEW_NAME}
  yarn wrangler pages deploy .svelte-kit/cloudflare --project-name my-pages-app --branch preview
else
  echo "Deploy local"
  echo "y" | yarn wrangler d1 migrations apply ${CLOUDFLARE_DB_NAME} --local
  yarn wrangler pages dev .svelte-kit/cloudflare
fi

(1) Terraform を使った Cloudflare のセットアップ

# (1) Terraform を使った Cloudflare のセットアップ
export TF_VAR_cloudflare_account_id=${CLOUDFLARE_ACCOUNT_ID}
export TF_VAR_cloudflare_api_token=${CLOUDFLARE_API_TOKEN}
terraform init
terraform apply -auto-approve

deploy.sh の (1) の箇所では Terraform を使って Cloudflare のリソースをセットアップしています。 Terraform の設定ファイルは以下のようになっています。

main.tf
resource "cloudflare_d1_database" "my_pages_app_db" {
  account_id = var.cloudflare_account_id
  name       = "my-pages-app-db"
  lifecycle {
    prevent_destroy = true
  }
}

resource "cloudflare_d1_database" "my_pages_app_db_preview" {
  account_id = var.cloudflare_account_id
  name       = "my-pages-app-db_preview"
  lifecycle {
    prevent_destroy = true
  }
}

resource "cloudflare_pages_project" "my_pages_app" {
  account_id = var.cloudflare_account_id
  name = "my-pages-app"
  production_branch = "main"

  deployment_configs {
    preview {
      d1_databases = {
        DB = cloudflare_d1_database.my_pages_app_db_preview.id
      }
    }
    production {
      d1_databases = {
        DB = cloudflare_d1_database.my_pages_app_db.id
      }
    }
  }
}

(2) セットアップしたリソース情報の取得

# (2) セットアップしたリソース情報の取得
export CLOUDFLARE_PAGES_PROJECT_NAME=$(terraform output -json | jq -r ".app.value.name")
export CLOUDFLARE_ACCOUNT_ID=$(terraform output -json | jq -r ".account_id.value")
export CLOUDFLARE_DB_ID=$(terraform output -json | jq -r ".db.value.id")
export CLOUDFLARE_DB_NAME=$(terraform output -json | jq -r ".db.value.name")
export CLOUDFLARE_DB_PREVIEW_ID=$(terraform output -json | jq -r ".db_preview.value.id")
export CLOUDFLARE_DB_PREVIEW_NAME=$(terraform output -json | jq -r ".db_preview.value.name")

Terraform で生成したリソースの情報は (2) の箇所で環境変数としてエクスポートしておき、 (3) の箇所で wrangler.toml を生成する際に使用しています。

(3) リソース情報を含んだ wrangler.toml の生成

# (3) リソース情報を含んだ wrangler.toml の生成
envsubst < wrangler.toml.in > wrangler.toml

(3) wrangler.toml.in というテンプレートファイルを元にして、Terraform で作成したリソースの ID を含んだ wrangler.toml を生成します。

wrangler.toml.in ファイルは以下のようになっています。

wrangler.toml.in
account_id = "${CLOUDFLARE_ACCOUNT_ID}"
compatibility_date = "2023-12-09"
send_metrics = false

[[d1_databases]]
binding = "DB" # i.e. available in your Worker on env.DB
database_id = "${CLOUDFLARE_DB_ID}"
preview_database_id = "${CLOUDFLARE_DB_PREVIEW_ID}"
database_name = "${CLOUDFLARE_DB_NAME}"
migrations_dir = "db/migrations"

# only for migration
[[d1_databases]]
binding = "DB_preview"
database_id = "${CLOUDFLARE_DB_PREVIEW_ID}"
database_name = "${CLOUDFLARE_DB_PREVIEW_NAME}"
migrations_dir = "db/migrations"

また、ここで設定した binding 名と同じ名前で、 src/app.d.ts にデータベースの型定義を追加してあります。

src/app.d.ts
// See https://kit.svelte.dev/docs/types#app
// for information about these interfaces
declare global {
  namespace App {
    // interface Error {}
    // interface Locals {}
    // interface PageData {}
    // interface Platform {}

    interface Platform {
      env: {
        DB: D1Database;
      };
      context: {
        waitUntil(promise: Promise<any>): void;
      };
    }
  }
}

export {};

(4) プロジェクトのビルド

# (4) プロジェクトのビルド
yarn
yarn build

(4) の箇所では SvelteKit 製の Web サービスのプロジェクトをビルドしています。

SvelteKit の設定ファイルで adapter-cloudflare という Adapter を使用すると、 Cloudflare Pages にデプロイするための構成でプロジェクトをビルドできます。

svelte.config.js
// Cloudflare 用の Adapter を使用する。
import adapter from "@sveltejs/adapter-cloudflare";
import { vitePreprocess } from "@sveltejs/kit/vite";

/** @type {import('@sveltejs/kit').Config} */
const config = {
  // Consult https://kit.svelte.dev/docs/integrations#preprocessors
  // for more information about preprocessors
  preprocess: vitePreprocess(),

  kit: {
    adapter: adapter(),
    alias: {
      "~": "src",
      $db: "db",
    },
  },
};

export default config;

(3) でデータベースと Binding するための設定をしてあるため、 SvelteKit の API Route では、以下のようにしてデータベースにアクセスできます。

src/routes/api/v1/users/+server.ts
import { drizzle } from 'drizzle-orm/d1';
import { users } from '$db/src/schema';
import { error, type RequestEvent, type RequestHandler } from '@sveltejs/kit';

export const GET: RequestHandler
  = async ({ request, url, platform }: RequestEvent) =>
{
  console.log(`${request.method} ${url.pathname}`);

  const DB = platform?.env?.DB; // (3) で設定した名前でデータベースを参照する。
  if( DB == null ) {
    throw error(503, "Invalid DB");
  }

  const db = drizzle(DB);
  const result = await db.select().from(users).all();
  return Response.json(result);
};

(5) DB のマイグレーションとデプロイの実行

# (5) 指定したデプロイ先に合わせた DB のマイグレーションとデプロイの実行
if [ "${DEPLOY_MODE}" = "prd" ]; then
  echo "Deploy production"
  echo "y" | yarn wrangler d1 migrations apply ${CLOUDFLARE_DB_NAME}
  yarn wrangler pages deploy .svelte-kit/cloudflare --project-name my-pages-app --branch main
elif [ "${DEPLOY_MODE}" = "preview" ]; then
  echo "Deploy preview"
  echo "y" | yarn wrangler d1 migrations apply ${CLOUDFLARE_DB_PREVIEW_NAME}
  yarn wrangler pages deploy .svelte-kit/cloudflare --project-name my-pages-app --branch preview
else
  echo "Deploy local"
  echo "y" | yarn wrangler d1 migrations apply ${CLOUDFLARE_DB_NAME} --local
  yarn wrangler pages dev .svelte-kit/cloudflare
fi

(5) の箇所では wranglerd1 コマンドを使って DB のマイグレーションと Cloudflare Pages へのデプロイを実行しています。

Cloudflare D1 のデータベースは SQLite ベースになっていて、今回のプロジェクトでは Drizzle ORM を使ってスキーマとマイグレーションファイルを管理しています。

schema.ts
/*
  DO NOT RENAME THIS FILE FOR DRIZZLE-ORM TO WORK
*/
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';

# SQLite 向けのテーブル定義
export const users = sqliteTable('users', {
  id: integer('id').primaryKey().notNull(),
  name: text('name').notNull(),
  tel: text('tel').notNull(),
});

その後、 wrangler の pages コマンドを使用してサービスをデプロイしています。 yarn build でビルドした成果物は .svelte-kit/cloudflare のパスに配置されるので、デプロイ時にそのパスを指定しています。

プロダクション用に公開する際はブランチ名を main に設定していますが、これは最初に Terraform で Cloudflare Pages のプロジェクトを作ったときに指定した production_branch と同じ名前にします。

まとめ

以上のようにセットアップすることで、 SvelteKit 製の Web サービスを Cloudflare Pages にデプロイできるようになりました。

Discussion