このページで分かること

  • 3つの保存先(R2バインディング、S3互換、ローカル)が向いている場合と、データベースのバックアップにはメディアのファイルが含まれないこと
  • R2バインディングの設定と公開用のドメイン、S3互換ストレージの設定(AWS SDKのインストール、S3_* の環境変数)、ローカルのファイルシステムの設定
  • 環境ごとに保存先を分ける方法と、署名付きアップロードに対応する保存先
難易度
実践
読む時間
5分
このページの目次

アップロードされたメディアの保存には、ストレージアダプターを1つ選びます。データベースのバックアップに含まれるのはメディアのメタデータで、保存されたファイルそのものは含まれません。そのため、ストレージのバックエンドは別にバックアップします。

概要

ストレージ 使う場面 署名付きアップロード
R2バインディング サイトをCloudflare Workersで動かしている 非対応
S3 Node.jsのサイトで、AWS S3、R2のS3 API、MinIO、またはその互換ストレージを使う 対応
ローカル Node.jsのサイトに、書き込み可能な永続ボリュームが1つある 非対応
本サイトの補足 やさしい解説

WordPressでは、アップロードした画像はサーバーのフォルダーに保存され、記事などのデータはデータベースに入ります。EmDashも同じく、メディアのファイルと、その情報(メタデータ)を別々の場所に保存します。保存先は、Cloudflare Workersで動かすならR2バインディング、Node.jsで動かすならS3互換のストレージかローカルのディスクから選びます。データベースのバックアップだけではメディアのファイルは戻らないため、保存先のバックアップも別に取ります。

Cloudflare R2バインディング

Cloudflare Workersでは、R2バインディングのアダプターを使います。実行時にバインディングがアクセス手段を提供するため、サイトにR2のアクセスキーは必要ありません。

astro.config.mjs
import emdash from "emdash/astro";
import { r2 } from "@emdash-cms/cloudflare";

export default defineConfig({
	integrations: [
		emdash({
			storage: r2({ binding: "MEDIA" }),
		}),
	],
});

設定項目

オプション 説明
binding string wrangler.jsonc に書いたR2バインディングの名前
publicUrl string バケットの公開URL(任意)

セットアップ

Wranglerの設定にR2バインディングを追加します。

wrangler.jsonc

{
  "r2_buckets": [
    {
      "binding": "MEDIA",
      "bucket_name": "emdash-media"
    }
  ]
}

wrangler.toml

[[r2_buckets]]
binding = "MEDIA"
bucket_name = "emdash-media"

公開アクセス

公開バケットからメディアを配信するには、Cloudflare APIでカスタムドメインを接続し、そのオリジンを publicUrl に設定します。Cloudflareの r2.dev の開発用URLにはレート制限があり、本番のトラフィックを想定したものではありません。

storage: r2({
	binding: "MEDIA",
	publicUrl: "https://media.example.com",
});

同じバケットに自動のJSONバックアップも保存している場合、公開バケットのオリジンから backups/ 配下のオブジェクトが見えてしまうことがあります。非公開のバケットとEmDashのメディア用ルートを使うか、公開オリジンをメディアのオブジェクトだけに制限します。詳しくはバックアップを参照してください。

S3互換ストレージ

S3アダプターは、Node.js上で、Cloudflare R2のS3 API、AWS S3、MinIO、およびその互換サービスと組み合わせて動作します。

次の設定では、Node.jsのプロセスの起動時に、エンドポイント、バケット、資格情報、リージョン、任意の公開URLを S3_* の変数から読み込みます。

astro.config.mjs
import emdash, { s3 } from "emdash/astro";

export default defineConfig({
	integrations: [
		emdash({
			storage: s3(),
		}),
	],
});

設定項目

オプション 必須 説明
endpoint string はい S3のエンドポイントURL
bucket string はい バケット名
accessKeyId string いいえ* アクセスキー
secretAccessKey string いいえ* シークレットキー
region string いいえ リージョン(デフォルト:"auto"
publicUrl string いいえ CDNまたは公開URL(任意)

* accessKeyIdsecretAccessKey は、両方を指定するか、両方を省略します。

環境変数からのS3設定の読み込み

s3({...}) で省略したフィールドは、プロセスの起動時に、対応する S3_* の環境変数から読み込まれます。これにより、コンテナーイメージを一度ビルドし、再ビルドせずに起動時に資格情報を注入できます。s3({...}) に明示した値は、常に環境変数より優先されます。

環境変数 フィールド 備考
S3_ENDPOINT endpoint 有効な httphttps のURLであること
S3_BUCKET bucket
S3_ACCESS_KEY_ID accessKeyId
S3_SECRET_ACCESS_KEY secretAccessKey
S3_REGION region デフォルトは "auto"
S3_PUBLIC_URL publicUrl 任意のCDNのプレフィックス

環境変数は、プロセスの起動時に process.env から読み込まれます。これはNode.js専用の機能です。

s3() を引数なしで呼び出すと、すべてのフィールドを S3_* の環境変数から読み込みます。

astro.config.mjs — runtime environment variable example
import emdash, { s3 } from "emdash/astro";

export default defineConfig({
	integrations: [
		emdash({
			// s3() with no args: all fields from S3_* environment variables
			storage: s3(),

			// Or mix: override one field, rest from environment
			// storage: s3({ publicUrl: "https://cdn.example.com" }),
		}),
	],
});

S3 API経由のR2

R2への署名付きの直接アップロードが必要な場合は、Node.js上でS3アダプターを使います。Cloudflare APIまたはCLIで、範囲を限定したR2のAPI資格情報を作成し、次の実行時の変数を設定します。

.env.example
S3_ENDPOINT=https://<account-id>.r2.cloudflarestorage.com
S3_BUCKET=emdash-media
S3_ACCESS_KEY_ID=<r2-access-key-id>
S3_SECRET_ACCESS_KEY=<r2-secret-access-key>
S3_REGION=auto
S3_PUBLIC_URL=https://media.example.com

実際の値は、Node.jsのホスティング環境のシークレット管理機能に保存します。公開URLは任意で、アップロードに使うS3 APIのエンドポイントの代わりにはなりません。

MinIO

同じ実行時の変数をMinIOに向けます。S3_ENDPOINT にはMinIOのAPIのオリジン、S3_BUCKET にはバケット名、2つの資格情報の変数には範囲を限定したMinIOのアクセスキーを設定します。S3_PUBLIC_URL は、そのオリジンがバケットのオブジェクトを公開で配信する場合だけ設定します。

ローカルのファイルシステム

ローカルストレージは、開発時か、永続ディスクを持つ1台のNode.jsサーバーで使います。ファイルは、そのディスク上のディレクトリに保存されます。

astro.config.mjs
import emdash, { local } from "emdash/astro";

export default defineConfig({
	integrations: [
		emdash({
			storage: local({
				directory: "./uploads",
				baseUrl: "/_emdash/api/media/file",
			}),
		}),
	],
});

設定項目

オプション 説明
directory string ファイルを保存するディレクトリのパス
baseUrl string ファイルを配信するときのベースURL

独自の静的ファイルサーバーを設定する場合を除き、baseUrl はEmDashのメディアファイルのエンドポイント(/_emdash/api/media/file)に合わせます。

環境ごとのストレージの使い分け

次の設定では、開発中はローカルのディレクトリを使い、Cloudflareの本番ビルドではR2を使います。

astro.config.mjs
import emdash, { local } from "emdash/astro";
import { r2 } from "@emdash-cms/cloudflare";

const storage = import.meta.env.PROD
	? r2({ binding: "MEDIA" })
	: local({
			directory: "./uploads",
			baseUrl: "/_emdash/api/media/file",
		});

export default defineConfig({
	integrations: [emdash({ storage })],
});

署名付きアップロード

S3アダプターは署名付きアップロードURLに対応しています。これにより、クライアントはサーバーを経由せずに、ストレージへ直接アップロードできます。大きなファイルのアップロードが速くなります。

S3アダプターを使う場合、署名付きアップロードは自動で使われます。管理画面は、使える場合に署名付きアップロードを使います。

署名付きアップロードに対応するアダプター:

  • S3(S3 API経由のR2を含む)

署名付きアップロードに対応しないアダプター:

  • R2バインディング(代わりにR2の資格情報を使ってS3アダプターを使います)
  • ローカル
本サイトの補足 やさしい解説

WordPressでは、管理画面から画像をアップロードすると、ファイルはいったんWebサーバーが受け取ります。EmDashのR2バインディングとローカルストレージも同じで、ファイルはサーバーを経由して保存されます。S3アダプターだけは「署名付きアップロードURL」という一時的な許可を発行し、ブラウザーからストレージへ直接ファイルを送れます。サーバーを経由しないため、大きなファイルのアップロードが速くなります。Cloudflare Workersで動かす場合はR2バインディングを使うため、この仕組みは使われません。

ストレージのインターフェース

すべてのストレージアダプターは、同じインターフェースを実装しています。

interface Storage {
	upload(options: {
		key: string;
		body: Buffer | Uint8Array | ReadableStream;
		contentType: string;
	}): Promise<UploadResult>;

	download(key: string): Promise<DownloadResult>;
	delete(key: string): Promise<void>;
	exists(key: string): Promise<boolean>;
	list(options?: ListOptions): Promise<ListResult>;
	getSignedUploadUrl(options: SignedUploadOptions): Promise<SignedUploadUrl>;
	getPublicUrl(key: string): string;
}

インターフェースが共通なので、アプリケーションのコードを変えずにストレージのバックエンドを切り替えられます。