☁️

Cloud Run × SQLite構成のクラウド運用 Litestream × Cloud Storageでレプリケーションを試す

に公開

概要

Cloud Run × Litestream を組み合わせ、コンテナ内の SQLite ファイルを Cloud Storage へリアルタイム複製できるかを検証しました。ローカルでは DevContainer でサクッと Hono + Kysely + SQLite の開発環境を構築し、MinIO を立てて Litestream の挙動を試し、動作確認用に ユーザー CRUD API を実装し、更新内容がちゃんとレプリケートされるかをチェックしています。最終的に GCP へ載せるため、Artifact RegistryCloud Storage バケットの準備・権限まわりもセットアップ。この記事では、そのやってみた過程とハマりどころをまとめました。

環境構築

ローカルで動く環境構築

以下の記事をベースに環境を構築していきたいと思います。

https://zenn.dev/slowhand/articles/a9e6a31b6215d2

Devcontainerを使用する構成に変更して .devcontainer ディレクトリを作成し、以下ファイルを作成します。

  • Dockerfile
FROM node:22.15.0-bullseye-slim

ARG username=vscode
ARG useruid=1000
ARG usergid=${useruid}

RUN set -ex \
    && apt-get update \
    && apt-get install -y \
        ca-certificates \
        sudo \
        sqlite3 \
        wget \
        --no-install-recommends \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/* \
    # Delete node user with uid=1000 and create vscode user with uid=1000
    && userdel -r node \
    && groupadd --gid ${usergid} ${username} \
    && useradd -s /bin/bash --uid ${useruid} --gid ${usergid} -m ${username} \
    && echo ${username} ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/${username} \
    && chmod 0440 /etc/sudoers.d/${username}

RUN wget https://github.com/benbjohnson/litestream/releases/download/v0.3.13/litestream-v0.3.13-linux-amd64.tar.gz \
    -O litestream.tar.gz \
    && tar -xzf litestream.tar.gz -C ./ \
    && mv litestream /usr/local/bin/ \
    && rm litestream.tar.gz \
    && chmod +x /usr/local/bin/litestream

USER ${username}
  • compose.yml
volumes:
  minio_data:
  modules_data:

name: cloudrun-litestream-example
services:
  app:
    build: .
    volumes:
      - ..:/usr/src
      - modules_data:/usr/src/node_modules
    command: tail -f /dev/null
    working_dir: /usr/src
  minio:
    image: minio/minio:RELEASE.2025-02-18T16-25-55Z
    volumes:
      - minio_data:/minio/data
    command: server --console-address ':9001' /minio/data
    ports:
      - 9000:9000
      - 9001:9001
  • devcontainer.json (拡張機能などはお好みで)
{
  "name": "CloudRun Litestream Example Remote Container Dev",
  "dockerComposeFile": ["compose.yaml"],
  "service": "app",
  "workspaceFolder": "/usr/src",
  "customizations": {
    "vscode": {
      "extensions": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"],
      "settings": {
        "editor.tabSize": 2,
        "editor.formatOnSave": true,
        "editor.codeActionsOnSave": {
          "source.fixAll.eslint": "always"
        },
        "files.insertFinalNewline": true,
        "files.trimFinalNewlines": true
      }
    }
  },
  "features": {
    "ghcr.io/devcontainers/features/git:1": {}
  },
  "postAttachCommand": ".devcontainer/postAttach.sh",
  "remoteUser": "vscode"
}
  • postAttach.sh
#!/bin/sh

cd `dirname $0`
cd ..
sudo chown -R vscode node_modules

ローカルで簡単なAPI実装

👇こちらをベースに Kysely×Hono×SQLite 構成で簡単なCRUD APIを実装してみます。

https://zenn.dev/slowhand/articles/6598214b1a738a

プロジェクト作成

まずは Hono で Nodejs用のプロジェクトを作成します。

$ yarn create hono .
yarn create v1.22.22
[1/4] Resolving packages...
[2/4] Fetching packages...
[3/4] Linking dependencies...
[4/4] Building fresh packages...

success Installed "create-hono@0.18.0" with binaries:
      - create-hono
create-hono version 0.18.0
✔ Using target directory … .
✔ Which template do you want to use? nodejs
✔ Directory not empty. Continue? Yes
✔ Do you want to install project dependencies? Yes
✔ Which package manager do you want to use? yarn
✔ Cloning the template
✔ Installing project dependencies
🎉 Copied project files

上記の記事と同様に .devcontainer/postAttach.sh の設定と .devcontainer/compose.yaml にportの設定を追加し http://localhost:3000 にアクセスして Hello Hono! が表示されていればOKです。

次に kysely-ctl を使ってテストテーブルを作成していきたいと思います。SQLiteのdialectとしてKyselyのドキュメントにある better-sqlite3 を同じように使って進めていきたいと思います。

早速必要なパッケージをインストールしていきます。

$ yarn add kysely better-sqlite3
$ yarn add -D kysely-ctl @types/better-sqlite3

次に Kysely の config ファイルを生成します。

yarn kysely init

最終的な .config/kysely.config.ts は以下の様に設定しました。

import Database from 'better-sqlite3';
import { SqliteDialect } from 'kysely';
import { defineConfig } from 'kysely-ctl';

export default defineConfig({
  dialect: new SqliteDialect({
    database: new Database('db.sqlite'),
  }),
  migrations: {
    migrationFolder: 'migrations',
  },
  //   plugins: [],
  //   seeds: {
  //     seedFolder: "seeds",
  //   }
});

Userテーブル作成

上記記事と同様に User テーブルを作成します。その際の migration ファイルはSQLite用に以下内容に修正し増田。

import { sql, type Kysely } from 'kysely';

export async function up(db: Kysely<any>): Promise<void> {
  await db.schema
    .createTable('users')
    .addColumn('id', 'integer', (col) => col.primaryKey())
    .addColumn('first_name', 'text', (col) => col.notNull())
    .addColumn('last_name', 'text')
    .addColumn('created_at', 'text', (col) =>
      col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
    )
    .addColumn('updated_at', 'text', (col) =>
      col.defaultTo(sql`CURRENT_TIMESTAMP`).notNull()
    )
    .execute();
}

export async function down(db: Kysely<any>): Promise<void> {
  await db.schema.dropTable('users').execute();
}

これでmigration実施し、Userテーブルが作成されていればOKです。

image1.png

シードデータ登録 & API実装

上記記事の「シードデータ登録」を参考にデータ登録を実施します。API実装も基本上記記事のままでSQLite用に少し修正が必要です。

src/types.tsUser モデルは better-sqlite3 がDateオブジェクトをサポートしていない為、以下の様に変更します。

export type User = {
  id?: number;
  first_name: string;
  last_name: string;
  created_at?: string; // Date -> string
  updated_at?: string; // Date -> string
};

以下SQLite用に修正した src/index.ts になります。

import { serve } from '@hono/node-server';
import SQLiteDatabase from 'better-sqlite3';
import { Hono } from 'hono';
import { Kysely, SqliteDialect } from 'kysely';
import type { Database } from './types.js';

const dialect = new SqliteDialect({
  database: new SQLiteDatabase('db.sqlite'),
});

export const db = new Kysely<Database>({
  dialect,
});

const app = new Hono();

app.get('/users', async (c) => {
  const users = await db.selectFrom('users').selectAll().execute();
  return c.json(users);
});

app.get('/users/:id', async (c) => {
  const id = c.req.param('id');
  const user = await db
    .selectFrom('users')
    .selectAll()
    .where('id', '=', Number(id))
    .executeTakeFirst();
  if (!user) {
    return c.notFound();
  }
  return c.json(user);
});

app.post('/users', async (c) => {
  const { first_name, last_name } = await c.req.json();
  const user = await db
    .insertInto('users')
    .values({ first_name, last_name })
    .returningAll()
    .executeTakeFirst();
  return c.json(user, 201);
});

app.put('/users/:id', async (c) => {
  const id = c.req.param('id');
  const { first_name, last_name } = await c.req.json();
  const user = await db
    .updateTable('users')
    // better-sqlite3はDateオブジェクトを渡せないので toISOString を使用
    .set({ first_name, last_name, updated_at: new Date().toISOString() })
    .where('id', '=', Number(id))
    .returningAll()
    .executeTakeFirst();
  if (!user) {
    return c.notFound();
  }
  return c.json(user);
});

app.delete('/users/:id', async (c) => {
  const id = c.req.param('id');
  const user = await db
    .deleteFrom('users')
    .where('id', '=', Number(id))
    .returningAll()
    .executeTakeFirst();
  if (!user) {
    return c.notFound();
  }
  return c.json(user);
});

serve(
  {
    fetch: app.fetch,
    port: 3000,
  },
  (info) => {
    console.log(`Server is running on http://localhost:${info.port}`);
  }
);

API叩いてちゃんとCRUDが動作していたらOKです。

ローカルでLitestream×MinIO構成で動作させる

次にローカル上でLitestreamを動作させ、MinIOへレプリケーションを実施する様にしてみたいと思います。

https://zenn.dev/slowhand/articles/a9e6a31b6215d2

基本上記の記事を元に進めて行き、litestream.yml はパスが異なる為以下の内容で作成しました。

dbs:
  - path: /usr/src/db.sqlite
    replicas:
      - type: s3
        bucket: litestream-bucket
        path: db.sqlite
        endpoint: http://minio:9000
        region: us-east-1
        access-key-id: xxxxxxxx
        secret-access-key: xxxxxxx
        force-path-style: true

ここまでできたら早速Litestreamを動かして動作確認してみたいと思います。継続的にレプリケーションを実施し、ちゃんと同期できているか確認します。

litestream replicate -config ./litestream.yml

上記コマンドを実施し継続的にレプリケーションする様にしときます。ちなみに現在のDBの内容は以下の様になっています。

image2.png

ここでデータを1件登録してみます。

$ curl -H "Content-Type: application/json" \
-X POST \
-d "{\"first_name\": \"Ichiro\", \"last_name\": \"Tanaka\"}" \
http://localhost:3000/users

次に litestreamCtrl+C で止めて別パスでdbをリストアし、中身を確認してみます。

$ litestream restore -o restore_db.sqlite -config ./litestream.yml /usr/src/db.sqlite
$ sqlite3 restore_db.sqlite
SQLite version 3.34.1 2021-01-20 14:10:07
Enter ".help" for usage hints.
sqlite> .table
_litestream_lock       kysely_migration       users
_litestream_seq        kysely_migration_lock
sqlite> SELECT * FROM users;
1|Taro|Yamada|2025-05-06 23:26:12|2025-05-06 23:26:12
2|Kenta|Fujimoto|2025-05-06 23:26:12|2025-05-06T23:38:30.072Z
3|Ichiro|Tanaka|2025-05-07 00:02:40|2025-05-07 00:02:40

ちゃんと同期できていそうです ✨

最後にコンテナ起動時にレプリケーションが走るように .devcontainer/postAttach.sh を以下に修正しときます。

yarn dev &

# start litestream
litestream replicate -config ./litestream.yml

デプロイ用の構成を追加

まずはレプリケーション先のCloudStorage、Artifact Registryの作成までを実施して行きます。

gcloud CLI を使って作業する為 .devcontainer/compose.yaml に以下serviceを追加しときます。

volumes:
  # ...
  # ↓追加
  gcloud_config:
  docker_config:

name: cloudrun-litestream-example
services:
  # ...
  # ↓追加
  infra:
    image: google/cloud-sdk:516.0.0-slim
    volumes:
      - ..:/usr/src
      - gcloud_config:/root/.config/gcloud
      - docker_config:/root/.docker
    working_dir: /usr/src
    command: tail -f /dev/null

今回はArtifact RegistryでのDockerイメージ作成の為、Docker in Dokcer構成が必要な為 .devcontainer/devcontainer.json を上書きした .devcontainer/devcontainer-infra.json を以下内容で作成します。

{
  "name": "CloudRun Litestream Infra Example Remote Container Dev",
  "dockerComposeFile": ["compose.yaml"],
  "service": "infra",
  "workspaceFolder": "/usr/src",
  "features": {
    "ghcr.io/devcontainers/features/git:1": {},
    "ghcr.io/devcontainers/features/docker-in-docker:2": {},
    "ghcr.io/devcontainers/features/node:1": {
      "version": "22.15.0"
    }
  }
}

👆 features/ghcr.io/devcontainers/features/docker-in-docker:2 で Docker in Dockerが使えるようにしています。

GUIからは devcontainer.json を上書きして起動はできなさそうな為、CLIを使って起動します。

# devcontainer cli をインストール
$ npm install -g @devcontainers/cli
# devcontainer-infra.json で上書きする形で起動
$ devcontainer up --workspace-folder . \
    --config .devcontainer/devcontainer.json \
    --override-config .devcontainer/devcontainer-infra.json

デプロイ用のDockerfileの作成

今回お試しなので手元で users テーブルやseedデータを登録済みの db.sqliteを用意しときます。

$ yarn kysely migrate latest
$ yarn kysely seed:run
$ db.sqlite
SQLite version 3.34.1 2021-01-20 14:10:07
Enter ".help" for usage hints.
sqlite> .tables
kysely_migration       kysely_migration_lock  users
sqlite> select * from users;
1|Taro|Yamada|2025-05-13 00:47:34|2025-05-13 00:47:34
2|Hanako|Suzuki|2025-05-13 00:47:34|2025-05-13 00:47:34
# パーミッションを変更しておく
$ chmod 777 db.sqlite

準備ができたので、以下内容の Dockerfile を作成しときます。

# ---------- 1) ビルドステージ ----------
FROM node:22.15.0-bullseye-slim AS build
WORKDIR /usr/src/app

RUN set -ex \
    && apt-get update \
    && apt-get install -y \
        ca-certificates \
        wget \
        --no-install-recommends \
    && apt-get clean \
    && rm -rf /var/lib/apt/lists/*

# 依存関係をインストール(開発依存は除外)
COPY package.json yarn.lock* ./
RUN --mount=type=cache,target=/usr/local/share/.cache/yarn \
    yarn install --frozen-lockfile

COPY tsconfig.json .
COPY src ./src
RUN yarn build

ARG LITESTREAM_VER=v0.3.13
RUN wget https://github.com/benbjohnson/litestream/releases/download/${LITESTREAM_VER}/litestream-${LITESTREAM_VER}-linux-amd64.tar.gz \
    -O litestream.tar.gz \
    && tar -xzf litestream.tar.gz -C ./ \
    && mv litestream /usr/local/bin/ \
    && rm litestream.tar.gz \
    && chmod +x /usr/local/bin/litestream

# ---------- 2) ランタイムステージ ----------
FROM node:22.15.0-bullseye-slim

ENV PORT=8080
EXPOSE 8080

WORKDIR /usr/src/app

COPY --from=build /usr/src/app/node_modules ./node_modules
COPY --from=build /usr/src/app/dist ./dist
COPY --from=build /usr/local/bin/litestream /usr/local/bin/litestream
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY litestream.yml .
COPY db.sqlite .

# ---------- エントリポイント ----------
# ・DB が無ければレプリカから restore
# ・その後 replicate -exec でアプリを子プロセスとして起動
ENTRYPOINT ["/bin/sh","-c", "\
  litestream replicate -config ./litestream.yml -exec 'node dist/index.js' \
"]

CloudStorageのバケット作成

上記の devcontainer up で立ち上げたコンテナ内に入り、まずは認証作業を実施して行きます。

gcloud auth login --no-launch-browser

👆のコマンドを実施し、表示されたURLにアクセスし許可すると verification code が取得できるのでコピペして認証を完了しときます。

# ちゃんと認証できているか↓のコマンドを実施して確認
$ gcloud projects list
# デフォルトの作業プロジェクトを設定しておくと便利
$ gcloud config set project <PROJECT_ID>
$ gcloud config list #=> プロジェクト設定確認

これで準備ができたので、早速レプリケーション先のバケットを作成しときます。

gcloud storage buckets create gs://litestream-example-bucket \
  --uniform-bucket-level-access \
  --location=asia-northeast1

Artifact Registryのリポジトリ作成

まずは Artifact Registry API が有効になっているか確認し、必要あれば有効にしときます。

$ gcloud services list --enabled | grep artifactregistry #=> 有効になっていれば表示される
$ gcloud services enable artifactregistry.googleapis.com # 必要あれば有効化

準備ができたのでリポジトリを作成しときます。

gcloud artifacts repositories create litestream-example-repository \
    --repository-format=docker \
    --location=asia-northeast1

最後に asia‑northeast1-docker.pkg.dev への docker push できる様に認証しときます。

gcloud auth configure-docker asia-northeast1-docker.pkg.dev

CloudRun用にPort指定と litestream.yml の修正

デプロイ環境で実行させる為に、以下を少し修正します。

  • src/index.ts
    • 環境変数のPORTで値を渡せるようにしておく
serve(
  {
    fetch: app.fetch,
    port: Number(process.env.PORT) || 3000, // ←修正
  },
  // ....
  • litestream.yml
    • 👇を参考にCloud Storageに向けた設定にしてきます

https://litestream.io/guides/gcs/

dbs:
  - path: /usr/src/app/db.sqlite
    replicas:
      - type: gcs
        bucket: litestream-example-bucket
        path: db.sqlite

デプロイ作業

Artifact RegistryのリポジトリへPush

Docker in Docker 環境で以下を実施し、作成したArtifact RegistryのリポジトリへPushします。

$ docker build --tag asia-northeast1-docker.pkg.dev/<PROJECT_ID>/litestream-example-repository/app:latest .
$ docker push asia-northeast1-docker.pkg.dev/<PROJECT_ID>/litestream-example-repository/app:latest

次にCloudRunからCloudStorageへアクセスできるサービスアカウントを作成しときます。

$ gcloud iam service-accounts create "litestream-example"
$ gcloud projects add-iam-policy-binding "<PROJECT_ID>" --member="serviceAccount:litestream-example@<PROJECT_ID>.iam.gserviceaccount.com" --role="roles/storage.admin"

CloudRunへデプロイ

最後にCloudRunへデプロイします。

$ gcloud run deploy "litestream-example" \
    --region="asia-northeast1" \
    --image="asia-northeast1-docker.pkg.dev/<PROJECT_ID>/litestream-example-repository/app:latest" \
    --port="8080" \
    --service-account="litestream-example@<PROJECT_ID>.iam.gserviceaccount.com" \
    --max-instances=1 \
    --allow-unauthenticated

動作確認

デプロイ後実際にアクセスしてみて users 一覧が取得できていたらOKです。

$ curl https://xxxxxxxxxxxxxx.run.app/users
[
  {
    "id": 1,
    "first_name": "Taro",
    "last_name": "Yamada",
    "created_at": "2025-05-13 00:47:34",
    "updated_at": "2025-05-13 00:47:34"
  },
  {
    "id": 2,
    "first_name": "Hanako",
    "last_name": "Suzuki",
    "created_at": "2025-05-13 00:47:34",
    "updated_at": "2025-05-13 00:47:34"
  }
]

CloudStorageのバケット内にも db.sqlite が作成されています。

image3.png

次にユーザーを追加してみます。

$ curl -H "Content-Type: application/json" -X POST -d \
  "{\"first_name\": \"Ichiro\", \"last_name\": \"Tanaka\"}" \
  https://xxxxxxxxxxxxxxxxxxxxx.run.app/users
# => {"id":3,"first_name":"Ichiro","last_name":"Tanaka","created_at":"2025-05-13 20:07:06","updated_at":"2025-05-13 20:07:06"}

再度ユーザー一覧を取得すると、

$ curl https://xxxxxxxxxxxxxx.run.app/users
[
  {
    "id": 1,
    "first_name": "Taro",
    "last_name": "Yamada",
    "created_at": "2025-05-13 00:47:34",
    "updated_at": "2025-05-13 00:47:34"
  },
  {
    "id": 2,
    "first_name": "Hanako",
    "last_name": "Suzuki",
    "created_at": "2025-05-13 00:47:34",
    "updated_at": "2025-05-13 00:47:34"
  },
  {
    "id": 3,
    "first_name": "Ichiro",
    "last_name": "Tanaka",
    "created_at": "2025-05-13 20:07:06",
    "updated_at": "2025-05-13 20:07:06"
  }
]

ちゃんと反映されています!

次はちゃんとレプリケーションされているかの確認の為、Dockerイメージを少し修正して再度CloudRunをデプロイしてみます。

👇 デプロイ時にローカルの db.sqlite ファイルを削除しCloudStorageからrestoreする様にしています。

ENTRYPOINT ["/bin/sh","-c", "\
  rm -f ./db.sqlite;\
  litestream restore -if-replica-exists -config ./litestream.yml ./db.sqlite;\
  litestream replicate -config ./litestream.yml -exec 'node dist/index.js' \
"]

再度デプロイして、「リビジョン」タブから👇の様に再デプロイできていればOKです

image4.png

再度 curl https://xxxxxxxxxxxxxx.run.app/users を実施し登録したデータが取得できていればOKです!

バッドノウハウ

CloudRun実行時に litestream shut down のログが出てCloudStorageに何も表示されない

自分の場合はシンプルに litestream.ymlpath が間違っていました…

CloudRun実行時に以下エラーが発生する 1

SqliteError: attempt to write a readonly database

自分の場合は非ルートユーザーで試した時や exec コマンドを使用して litestream replicate を実行していた時に db.sqlite ファイルへ書き込み権限がなく上記のエラーが出ていました。

※ 本当は非ルートユーザーで実施するのが良いのかと思うのですが、今回はお試しという事でルートユーザーで実行しています。

CloudRun実行時に以下エラーが発生する 2

tls: failed to verify certificate: x509: certificate signed by unknown authority

Dockerイメージ内に ca-certificates が存在していない為、 Litestream が GCS 証明書を検証できず発生している様です。修正方法としては ca-certificates をインストールするか上記のDockerfileの様にマルチステージングビルドの場合、ビルドステージから以下の様にCOPYしてやる方法があります。

COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

参考URL

https://qiita.com/faable01/items/ac7418d671c6db5b966f

https://ushumpei.hatenablog.com/entry/2023/03/16/172353

Discussion