新規投稿のお知らせを受信されたい方は、サブスクリプションをご登録ください:

Cloudflare Pagesを使用したフルスタックアプリケーションの構築

2021/11/17

6 分で読了
Building a full-stack application with Cloudflare Pages

これは大々的に披露するしかないと思っていたため、私たちはCloudflare Pagesでのフルスタックアプリケーションのサポートを発表できることに興奮していました。Cloudflare Workersの支援を受けてサーバーレス関数をPages内から追加する方法を示すための、実演用の画像共有プラットフォームを構築しました。プロジェクトに1つのファイルを新たに追加するだけで、ダイナミックレンダリング、他のAPIとのやりとり、KVおよびDurable Objectsを使用したpersistデータを追加することができます。フルスタックアプリケーションの可能性とPagesの高速開発サイクルおよび無制限プレビュー環境を組み合わせることで、ほとんどすべてのアプリケーションを作成する能力が得られます。

本日は、画面共有プラットフォームの例について説明します。一部の画像は秘密にしたまま、画像を友人たちと共有できるようにしたいと思います。関数を使用してJSON APIを構築(KVおよびDurable Objects上にデータを格納する)して、Cloudflare ImagesおよびCloudflare Accessと統合し、フロントエンドとしてReactを使用します。

良質のものをすぐにでも使用したい場合は、こちらにあるデモインスタンス、およびGitHub上のコードを利用できますが、ここではゆっくりしたアプローチに拘ります。

Cloudflare Pagesを使用したサーバーレス関数の構築

ファイル-ベースのルーティング

まだ慣れていない場合、Cloudflare Pagesはgitプロバイダーと接続(GitHubおよびGitLab)して、Cloudflareのネットワークに対する静的サイトのデプロイを自動化します。関数を使用するとこれらのアプリを動的データに散りばめて拡張することができます。まだお済みでない場合は、こちらから加入することができます

私たちのプロジェクトで、新しい関数を作成してみましょう。

// ./functions/time.js


export const onRequest = () => {
  return new Response(new Date().toISOString())
}

git commitと、このファイルのプッシュにより最初のPages関数の構築とデプロイがトリガーされます。/timeに対するすべてのリクエストはこの関数によって処理され、他のすべてのリクエストはユーザーのプロジェクトのスタティックアセットに戻されます。ディレクトリ内へのFunctionsファイルの配置は予想どおり動作します。./functions/api/time.jsは/api/time、./functions/some_directory/index.jsは/some_directoryで利用可能です。

また、TypeScript (./functions/time.tsが同様に動作する)、およびパラメータ化されたファイルもサポートされます

  • ./functions/todos/[id].jsのように単一角括弧を使用すると/todos/123などのすべてのリクエストと一致させられます。
  • そして./functions/todos/[[path]].jsのように二重角括弧を使用すると、任意の数のパスセグメント(例:/todos/123/subtasks)と一致させられます。

PagesFunctionの型は@cloudflare/workers-typesライブラリで宣言しますが、これは関数の型のチェックのために使用することができます。

動的データ

では、私たちの画像共有アプリに戻り、いくつかの画像をアップロード済みであると仮定し、それらをホームページ上に表示したいと思います。そのフロントエンドが呼び出すことができる、これらの画像のリストを返すエンドポイントが必要です。

// ./functions/api/images.ts

export const jsonResponse = (value: any, init: ResponseInit = {}) =>
  new Response(JSON.stringify(value), {
    headers: { "Content-Type": "application/json", ...init.headers },
    ...init,
  });

const generatePreviewURL = ({
  previewURLBase,
  imagesKey,
  isPrivate,
}: {
  previewURLBase: string;
  imagesKey: string;
  isPrivate: boolean;
}) => {
  // If isPrivate, generates a signed URL for the 'preview' variant
  // Else, returns the 'blurred' variant URL which never requires signed URLs
  // https://developers.cloudflare.com/images/cloudflare-images/serve-images/serve-private-images-using-signed-url-tokens

  return "SIGNED_URL";
};

export const onRequestGet: PagesFunction<{
  IMAGES: KVNamespace;
}> = async ({ env }) => {
  const { imagesKey } = (await env.IMAGES.get("setup", "json")) as Setup;

  const kvImagesList = await env.IMAGES.list<ImageMetadata>({
    prefix: `image:uploaded:`,
  });

  const images = kvImagesList.keys
    .map((kvImage) => {
      try {
        const { id, previewURLBase, name, alt, uploaded, isPrivate } =
          kvImage.metadata as ImageMetadata;

        const previewURL = generatePreviewURL({
          previewURLBase,
          imagesKey,
          isPrivate,
        });

        return {
          id,
          previewURL,
          name,
          alt,
          uploaded,
          isPrivate,
        };
      } catch {
        return undefined;
      }
    })
    .filter((image) => image !== undefined);

  return jsonResponse({ images });
};

観察力の優れた読者であればonRequestGetをエクスポートしていることに気が付くでしょう。これによりGETリクエストのみに応答することができます。

また、KV名前空間(env.IMAGESでアクセス)を使用して、アップロード済みの画像に関する情報を格納します。Pagesプロジェクト内にバインディングを作成するには、[Settings(設定)] タブに移動します。

他のAPIとのインターフェース

Cloudflare Imagesは画像のホスティングおよび変換のための、低価格で、ハイパフォーマンス、高機能のサービスです。複数のバリアントを作成して、様々な方法で画像をレンダリングして署名付きURLでアクセスを制御することができます。このサービスのAPIを使用するインターフェースに以下の関数を追加して、受信ファイルをCloudflare Imagesにアップロードします。

// ./functions/api/admin/upload.ts

export const onRequestPost: PagesFunction<{
  IMAGES: KVNamespace;
}> = async ({ request, env }) => {
  const { apiToken, accountId } = (await env.IMAGES.get(
    "setup",
    "json"
  )) as Setup;

  // Prepare the Cloudflare Images API request body
  const formData = await request.formData();
  formData.set("requireSignedURLs", "true");
  const alt = formData.get("alt") as string;
  formData.delete("alt");
  const isPrivate = formData.get("isPrivate") === "on";
  formData.delete("isPrivate");

  // Upload the image to Cloudflare Images
  const response = await fetch(
    `https://api.cloudflare.com/client/v4/accounts/${accountId}/images/v1`,
    {
      method: "POST",
      body: formData,
      headers: {
        Authorization: `Bearer ${apiToken}`,
      },
    }
  );

  // Store the image metadata in KV
  const {
    result: {
      id,
      filename: name,
      uploaded,
      variants: [url],
    },
  } = await response.json<{
    result: {
      id: string;
      filename: string;
      uploaded: string;
      requireSignedURLs: boolean;
      variants: string[];
    };
  }>();

  const metadata: ImageMetadata = {
    id,
    previewURLBase: url.split("/").slice(0, -1).join("/"),
    name,
    alt,
    uploaded,
    isPrivate,
  };

  await env.IMAGES.put(
    `image:uploaded:${uploaded}`,
    "Values stored in metadata.",
    { metadata }
  );
  await env.IMAGES.put(`image:${id}`, JSON.stringify(metadata));

  return jsonResponse(true);
};

永続性データ

頻繁に読み取られてもまれにしか書き込まれない情報を格納するため、既にKVを使用しています。もう少し同機が必要な機能の場合はどうでしょうか。

各画像にダウンロードカウンターを追加しましょう。highresバリアントをCloudflare Images内に作成すると、ユーザーがリンクをリクエストするたびにカウンターをインクリメントすることができます。これにはもう少しセットアップが必要ですが、プロジェクト内のDurable Objectsの機能を解き放つだけの価値が絶対にあります。

このダウンロードカウントを保持できる、以下のDurable Objectクラスを作成して公開する必要があります。

// ./durable_objects/downloadCounter.js
ts#example---counter

export class DownloadCounter {
  constructor(state) {
    this.state = state;
    // `blockConcurrencyWhile()` ensures no requests are delivered until initialization completes.
    this.state.blockConcurrencyWhile(async () => {
      let stored = await this.state.storage.get("value");
      this.value = stored || 0;
    });
  }

  async fetch(request) {
    const url = new URL(request.url);
    let currentValue = this.value;

    if (url.pathname === "/increment") {
      currentValue = ++this.value;
      await this.state.storage.put("value", currentValue);
    }

    return jsonResponse(currentValue);
  }
}

ミドルウェア

関数を実行する前に一部のコード(認証またはロギングなど)を実行する必要がある場合のために、Pagesには使いやすいミドルウェアが用意されており、これはファイルベースのルーティングの任意のレベルで適用することができます。ディレクトリに_middleware.tsファイルを作成することで、このファイルが最初に実行され、next()が呼び出されるときにユーザーの関数を実行することがわかります。

私たちのアプリケーションでは、未認証ユーザーが画像をアップロード(/api/admin/upload)または画像を削除(/api/admin/delete)できないようにしたいと思います。Cloudflare Accessではすべてまたは一部のアプリケーションに対するロールベースのアクセス制御を適用することができ、ユーザーが必要なのはサーバーレス関数に統合する単一ファイルだけです。私たちは./functions/api/admin/_middleware.tsを作成し、これは/api/admin/*の受信リクエストすべてに適用されます:

// ./functions/api/admin/_middleware.ts

const validateJWT = async (jwtAssertion: string | null, aud: string) => {
  // If the JWT is valid, return the JWT payload
  // Else, return false
  // https://developers.cloudflare.com/cloudflare-one/identity/users/validating-json

  return jwtPayload;
};

const cloudflareAccessMiddleware: PagesFunction<{ IMAGES: KVNamespace }> =
  async ({ request, env, next, data }) => {
    const { aud } = (await env.IMAGES.get("setup", "json")) as Setup;

    const jwtPayload = await validateJWT(
      request.headers.get("CF-Access-JWT-Assertion"),
      aud
    );

    if (jwtPayload === false)
      return new Response("Access denied.", { status: 403 });

    // We could also use the data object to pass information between middlewares
    data.user = jwtPayload.email;

    return await next();
  };

export const onRequest = [cloudflareAccessMiddleware];

ミドルウェアは自由に使える強力なツールで、Cloudflare Accessを使用するアプリケーションのパーツを容易に保護、またはHoneycombおよびSentryなどの可観測性およびエラーロギングプラットフォームと素早く統合することができます。

Jamstackとしての統合

"Jamstack"の"Jam"はJavaScript、API、Markupを表します。Cloudflare Pagesは以前からこの「J」と「M」を提供していましたが、中間にWorkersがあると、本当にフルスタックのJamstackを使用することができます。

取り組みやすい例として、Create Reactアプリでこの画像共有プラットフォームのフロントエンドを構築しましたが、Cloudflare Pagesは増加の一途をたどるフレームワークと自然に統合し(現在は23)、いつでも独自の完全なカスタムビルドコマンドを構成することができます

ユーザーのフロントエンドは私たちが構成済みの関数を呼び出して、そのデータをレンダリングするだけです。私たちはSWRを使用して物事を単純化しますが、ユーザーが望めば、全くありきたりなJavaScriptの fetchを使用してこれを実行することができます。

// ./src/components/ImageGrid.tsx

export const ImageGrid = () => {
  const { data, error } = useSWR<{ images: Image[] }>("/api/images");

  if (error || data === undefined) {
    return <div>An unexpected error has occurred when fetching the list of images. Please try again.</div>;
  }


  return (
    <div>
      {data.images.map((image) => (
        <ImageCard image={image} key={image.id} />
      ))}
    </div>
  );

}

ローカルデプロイ

動作状況をテストするためにすべての変更をプッシュする必要がある場合、それが高速であるとしても、このようなプロジェクト上の反復は手間がかかります。関数、Workers、機密情報、環境変数、KVに対する完全なサポートを含む、Pagesプロジェクトのローカルデプロイのための、Wranglerとの素晴らしい統合がリリースされました。Durable Objectsのサポートは間もなく開始されます。

npmからのインストール:

npm install wrangler@beta

静的アセットのフォルダの提供をするか、既存のツールをプロキシします:

# Serve a directory
npx wrangler pages dev ./public

# or integrate with your other tools
npx wrangler pages dev -- npx react-scripts start

前進、そして作成!

子犬がお好きでしたら、こちらに画像共有アプリケーションを展開してあります。コードがお好きでしたら、GitHub上に用意されています。ご自分で自由にフォークしてデプロイしてください! 5分間のセットアップウィザードがありますが、Cloudflare Images、Access、Workers、Durable Objectsが必要になります。

私たちはPagesプラットフォームの将来に胸を高鳴らせており、あなたが構築しているものについて聞きたいです!当社のDiscordサーバー上の#what-i-builtチャンネルであなたのフルスタックアプリケーションを公開するか、#pages-helpチャンネルでサポートを受けてください。

Cloudflareは企業ネットワーク全体を保護し、お客様がインターネット規模のアプリケーションを効率的に構築し、あらゆるWebサイトやインターネットアプリケーションを高速化し、DDoS攻撃を退けハッカーの侵入を防ぎゼロトラスト導入を推進できるようお手伝いしています。

ご使用のデバイスから1.1.1.1 にアクセスし、インターネットを高速化し安全性を高めるCloudflareの無料アプリをご利用ください。

より良いインターネットの構築支援という当社の使命について、詳しくはこちらをご覧ください。新たなキャリアの方向性を模索中の方は、当社の求人情報をご覧ください。
Full Stack Week (JP)Cloudflare Workers (JP)Cloudflare Access (JP)Serverless (JP)日本語Product News (JP)

Xでフォロー

Greg Brimble|@GregBrimble
Obinna Ekwuno|@Obinnaspeaks
Cloudflare|@cloudflare

関連ブログ投稿

2021年11月20日 13:59

ネットワークパフォーマンスの最新情報:Full Stack Week

2か月と少し前に、世界中のラストワンマイルネットワークの広範なベンチマーク結果を共有しました。さまざまなテスト (TCP接続時間、最初の1バイトを受信するまでの時間(TTFB)、最後の1バイトを受信するまでの時間(TTLB))、また、さまざ...

2021年11月19日 14:00

Cloudflare Workersで、StripeのJavaScript SDKをネイティブサポート

アプリの中で支払いを処理することは、オンラインビジネスを構築する上で非常に重要です。多くの開発者が決済のプラットフォームに選ぶのがStripeです。私が初めてStripeに出会ったのは約7年前ですが、このサービスは単純な支払い処理をはるかに超えて進化しています。...

2021年11月19日 13:59

Cloudflareで次の動画アプリケーションを構築

歴史的に、動画アプリケーションの構築は非常に難しいものでした。動画の録画、エンコード、そして再生の裏には多くの複雑な技術があります。幸いなことに、Cloudflare Streamではすべての難しい部分を取り除くことで、カスタム動画やストリーミングアプリケーションを簡単に構築できます...

2021年11月19日 13:59

CloudflareスタックでのオープンソースCMS:紹介記事

Cloudflareのドキュメントは、コンセプトを学習したり、APIの使用上の注意を確認したりする場合や、APIやコンセプトを説明するための簡潔なスニペットが必要な場合に役立つ資料です。しかし、その資料が網羅的であるとしても、Cloudflare Workersプラットフォームの新規ユーザーは、...