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リソースを使いやすくなるメリットがありつつも、トリッキーな実装になることからの不具合が起きる可能性については御留意下さい。