Cloudflare PagesでAWS Amplify SDK v6をつかおうとした時の覚書
この記事では、AWS Amplify SDKをCloudflare Pagesで利用する際の注意点やエラーについて紹介しています。エラーの原因や解決策、サーバーサイドでのSDK読み込み方法なども述べられています。2024年11月時点では、CloudflareはIDP as a Serviceを提供しておらず、AWS Amplifyや他のプロバイダーを利用するか、独自のIDPを構築する必要があります。しかし、実装にはトリッキーな側面や不具合の可能性があることに注意が必要です。
目次
この記事では、AWS Amplify SDKをCloudflare Pagesで利用したい場合に注意すべきポイントを簡単に紹介します。「Cloudflare Advent Calendar 2024」18日目の記事として作成しました。
Amplify SDKだけを利用する
RemixやNext.jsアプリなどでAWS AmplifyのSDKだけを利用することができます。SDKを利用することで、CognitoやAppSync / S3などへのアクセスと操作が簡単に行えるようになります。Amplify CLIも使わない場合は、Viteなどの環境変数を通して必要な値をAmplify.configureに渡しましょう。
import { Amplify } from 'aws-amplify'
export const awsAmplifyResourceConfig = {
  Auth: {
    Cognito: {
        identityPoolId: import.meta.env.VITE_PUBLIC_AMPLIFY_AUTH_IDP_ID,
        allowGuestAccess: true,
        userPoolId: import.meta.env.VITE_PUBLIC_AMPLIFY_AUTH_UP_ID,
        userPoolClientId: import.meta.env.VITE_PUBLIC_AMPLIFY_AUTH_UP_CLIENT_ID,
    },
  },
}
Amplify.configure(awsAmplifyResourceConfig)
Cloudflare Pagesでは、デプロイに失敗することがある
Vercelなどでは問題なかったのですが、Cloudflare Pagesに関しては次のエラーが出ることがあります。SDK内部で利用されている処理が原因のエラーで、ランタイム依存っぽいものなので、もしかすると将来のWorkerdアップデートで解決している・・・かもしれません。
✘ [ERROR] Deployment failed!
  Failed to publish your Function. Got error: Uncaught Error: Disallowed operation called within
  global scope. Asynchronous I/O (ex: fetch() or connect()), setting a timeout, and generating
  random values are not allowed within global scope. To fix this error, perform this operation
  within a handler. https://developers.cloudflare.com/workers/runtime-apis/handlers/
    at functionsWorker-0.5490692660383001.js:58815:5 in resetTimeout
    at functionsWorker-0.5490692660383001.js:58845:9 in detectFramework
    at functionsWorker-0.5490692660383001.js:58899:36 in getAmplifyUserAgentObject
    at functionsWorker-0.5490692660383001.js:58911:25 in getAmplifyUserAgent
    at functionsWorker-0.5490692660383001.js:58948:23 in
  ../node_modules/@aws-amplify/core/dist/esm/awsClients/cognitoIdentity/base.mjs
    at functionsWorker-0.5490692660383001.js:8:56 in __init
    at functionsWorker-0.5490692660383001.js:58977:5 in
  ../node_modules/@aws-amplify/core/dist/esm/awsClients/cognitoIdentity/getId.mjs
    at functionsWorker-0.5490692660383001.js:8:56 in __init
    at functionsWorker-0.5490692660383001.js:60149:5 in
  ../node_modules/@aws-amplify/core/dist/esm/index.mjs
    at functionsWorker-0.5490692660383001.js:8:56 in __init
🪵  Logs were written to "/Users/okamotohidetaka/.wrangler/logs/wrangler-2024-08-18_13-02-08_583.log"
error Command failed with exit code 1.
サーバー側でAmplify SDKを実行しないようにする
このエラー、どうやらサーバー側でAmplify SDKを読み込むと発生する様子です。そのため、Remixであればapp/entry.client.tsxにてAmplify.configureするとエラーを回避できました。
import { RemixBrowser } from "@remix-run/react";
import { Amplify } from 'aws-amplify'
import { startTransition, StrictMode } from "react";
import { hydrateRoot } from "react-dom/client";
import { awsAmplifyResourceConfig } from "./utils/aws-amplify.config";
import 'instantsearch.css/themes/satellite.css';
startTransition(() => {
  Amplify.configure(awsAmplifyResourceConfig)
  
  hydrateRoot(
    document,
    <StrictMode>
      <RemixBrowser />
    </StrictMode>
  );
});
SSRしたい場合は、await importで一応動く
もしSSRしたい場合、app/entry.serverl.tsxを次のような書き方にすることで、デプロイが成功することは確認できました。
export default async function handleRequest(
  request: Request,
  responseStatusCode: number,
  responseHeaders: Headers,
  remixContext: EntryContext,
  // This is ignored so we can keep it in the template for visibility.  Feel
  // free to delete this parameter in your app if you're not using it!
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
  loadContext: AppLoadContext
) {
  const { Amplify } = await import('aws-amplify')
  Amplify.configure(awsAmplifyResourceConfig)
  const body = await renderToReadableStream(
    <RemixServer context={remixContext} url={request.url} />,
    {
      signal: request.signal,
      onError(error: unknown) {
        // Log streaming rendering errors from inside the shell
        console.error(error);
        responseStatusCode = 500;
      },
    }
  );
  if (isbot(request.headers.get("user-agent") || "")) {
    await body.allReady;
  }
  responseHeaders.set("Content-Type", "text/html");
  return new Response(body, {
    headers: responseHeaders,
    status: responseStatusCode,
  });
}
この場合、loaderなどで呼び出す処理もawait importしないとダメかもしれません。
export async function loader({params, request, response}: LoaderFunctionArgs) {
  const { createAWSAmplifyServerContext } = await import ('~/utils/aws-amplify.server');
  const { getCurrentUser } = await import ('aws-amplify/auth/server');
  const amplifyServerContext = createAWSAmplifyServerContext({request, response})
  const message = await amplifyServerContext.runWithAmplifyServerContext(async (contextSpec) => {
    try {
      const { username } = await getCurrentUser(contextSpec);
      console.log(username)
      return `Welcome ${username}!`;
    } catch (e) {
      console.log(e)
      if (e instanceof Error) {
        if (e.name !== 'UserUnAuthenticatedException') {
          console.log(e)
        }
      } else {
        console.log(e)
      }
      return ''
    }
  })
  console.log({message})
おわりに
Cloudflareは2024/11月時点でIDP as a Serviceが提供されていません。そのため、AWS Amplify / Okta CIC ( Auth0 )やClerk / Supabaseなどを導入するか、D1などに独自にIDPを構築する必要があります。今回の方法では、AWSリソースを使いやすくなるメリットがありつつも、トリッキーな実装になることからの不具合が起きる可能性については御留意下さい。