Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Next.js 14+Cognito +DynamoDB+Amplifyで認証付きのCRUDア...

Next.js 14+Cognito +DynamoDB+Amplifyで認証付きのCRUDアプリを構築してみる

YingZhi "Harrison" Huang

January 29, 2024
Tweet

Transcript

  1. 自己紹介 • HarrisonEagle ◦ 本業はSRE ◦ 副業や趣味などでフロントエンドのコード書いたりする ▪ 最近はServer ActionとRSCを遊んでる

    ◦ 好きなプログラミング言語: TypeScript, Go, Rust, Kotlin ◦ ゴルフ、筋トレ、バードウォッチングが趣味 ◦ GitHub: https://github.com/HarrisonEagle
  2. これから話す内容 • Next.js 14 + AWS Amplify + AWS DynamoDB

    + AWS Cognitoで認証つきの Webアプリを構築する方法について紹介します ◦ バックエンドの処理はAmplify SSRとServer Actionで完結させます。LambdaやAPI Gatewayなど他のAPIを作って繋ぎこむことはありません ◦ 最新のAmplify SDK v6の使い方についても紹介します ◦ サンプルアプリとして簡単な CRUDアプリをデプロイしました: ▪ https://deployment.ddprvnig7qphd.amplifyapp.com ◦ リポジトリ:https://github.com/HarrisonEagle/amplify-dynamodb-ssr-sample
  3. Amplify SDKの初期化 • Amplify SDKの機能の多くとAmplify Authはク ライアントサイドで実行するので、 Amplify SDK の初期化はクライアントサイドで行う

    ◦ Amplify SDK v5以前ではここでAuth.configure でAuthの初期化もする必要があったが、 v6か らAuthの初期化もAmplify .configureでまとめ られるようになった • 明示的にAmplify側でSSRを使用するに設定す る • TokenなどをCookieで管理するように設定(後 述) “use client” import { CookieStorage, parseAmplifyConfig } from "aws-amplify/utils"; import { cognitoUserPoolsTokenProvider } from "aws-amplify/auth/cognito"; import awsmobile from "../aws-exports"; const amplifyConfig = parseAmplifyConfig(awsmobile); cognitoUserPoolsTokenProvider.setKeyValueStorage(new CookieStorage()); Amplify.configure(amplifyConfig, { ssr: true });
  4. バックエンド認証とDynamoDBへの接続 "use server"; import awsmobile from "../aws-exports"; import { createServerRunner

    } from "@aws-amplify/adapter-nextjs"; import { fetchAuthSession, getCurrentUser } from "aws-amplify/auth/server"; import { DynamoDBClient } from "@aws-sdk/client-dynamodb"; import { cookies } from "next/headers"; const { runWithAmplifyServerContext } = createServerRunner({ config: awsmobile, }); const getCurrentSessionFromServer = async () => await runWithAmplifyServerContext({ nextServerContext: { cookies }, operation: async (contextSpec) => fetchAuthSession(contextSpec), }); export const getDynamoDBClient = async () => { const session = await getCurrentSessionFromServer(); return await new DynamoDBClient({ credentials: session.credentials, region: "ap-northeast-1", }); }; • AWS Amplify SDK v6とNext.js Adapterを利 用すると結構便利 • next/headersでリクエストヘッダー内に含まれ てるCookieを抽出し、その中に入っている認証 情報を使用してセッションを取得する • 有効なセッション情報内には DynamoDBやS3 など、SDKの認証に使用できるCredentialsを 抽出できる ◦ 認証されたユーザーのみ DynamoDBに接続 できるような構成になる • fetchAuthSessionによるSessionチェックは middleware.tsでも行う ◦ 今回の場合は、ログインページ以外全部認証 する必要あるので、 middlewareでセッションが 無効だったらログイン画面にリダイレクトする ように • getCurrentUserでユーザー情報を取得できる
  5. Server ActionからのDynamoDBへの操作 • AWS Amplify SDK v6とNext.js Adapterで DynamoDBクライアントの初期化とユーザー情 報の取得行う共通メソッドを予め用意する

    • Server ActionsでそのままDynamoDBクライア ントとユーザー情報を取得し、それを利用して DBへのCRUD操作を行う • Server Action自体はClient Componentと Server Component両方インポートして実行で きるので非常に便利。 ◦ バックエンドとフロントエンドとの連携は、 Component側からServer Actionをインポート するだけで済むので API GatewayとLambdaを 構築する手間を省ける "use server" import { Note } from "@/entities"; import { v4 as uuidv4 } from "uuid"; import { getCurrentUserFromServer, getDynamoDBClient } from "@/utils"; import { QueryCommand, QueryCommandInput, PutItemCommand, PutItemCommandInput, DeleteItemCommand, DeleteItemCommandInput, } from "@aws-sdk/client-dynamodb"; import { revalidatePath } from "next/cache"; export const putNote = async (data: FormData) => { const note_name = data.get("note_name") as string; const note_content = data.get("note_content") as string; let note_id = data.get("note_id") as string; if (!note_id) { note_id = uuidv4(); } const client = await getDynamoDBClient(); const user = await getCurrentUserFromServer(); const putItemRequest: PutItemCommandInput = { TableName: tableName, Item: { note_id: { S: note_id }, user_id: { S: user.userId }, note_name: { S: note_name }, note_content: { S: note_content }, }, }; await client.send(new PutItemCommand(putItemRequest)); revalidatePath("/"); };
  6. ただし... • SDKを使った処理をServer Actionで記述し、それをそのまま Client Componentからインポート する実装にしていたが、Devモードで動くもののProduction向けのビルドではWebpack build が失敗してしまう ◦

    Next.js公式の例だとそれで動くはずだった ... ◦ https://github.com/vercel/next.js/discussions/57535 ▪ 上記のDiscussionはまだ完全にResolveされてない模様(2024.1.28現在) ◦ 元々DynamoDBに繋げるAWS SDKもそうだったが、SDKのアップデートとNext.js 14の 更新に伴って現在は修正済み ◦ このエラーに遭遇した場合、 Client ComponentからServer Actionを直接インポートする のではなく、Server ComponentからServer Actionをインポートし、Props経由でClient Componentに渡すことで対策できる
  7. トラブルが発生する可能性がある実装 "use client"; import { deleteNote } from "@/actions"; type

    Props = { note: Note; }; export const NoteCard = ({ note }: Props) => { return ( <Card mt="3" w="lg">   ... <ButtonGroup spacing="2"> <form action={deleteNote}> <input hidden name="note_id" value={note.note_id} /> <Button type="submit" variant="ghost" colorScheme="red"> Delete </Button> </form> </ButtonGroup> </Card> ); };
  8. 対策 "use client"; type Props = { note: Note; deleteNote:

    (data: FormData) => Promise<void> // Server Componentから渡す }; export const NoteCard = ({ note, deleteNote }: Props) => { return ( <Card mt="3" w="lg">   ... <ButtonGroup spacing="2"> <form action={deleteNote}> <input hidden name="note_id" value={note.note_id} /> <Button type="submit" variant="ghost" colorScheme="red"> Delete </Button> </form> </ButtonGroup> </Card> ); };
  9. まとめと考察 • Server ActionとRSCは色々言われているが、うまく使いこなせると APIを(手動で)作らずにバッ クエンドに繋げられるので割と便利 ◦ 特にAmplify SSRとの相性は良かった •

    Amplify SSRを活用することによって、API Gateway + LambdaでREST APIを構築してフロント 側に繋ぎこむ手間をかなり省けた ◦ 何かWebアプリを爆速で作って AWSで動かして検証したい !って場合は向いてそう ◦ ただしタイムアウト時間については未知なので、重い処理は Lambdaに載せて、サーバーサイドで連 携させる方が無難 • RSCとServer ActionでもSDKが動かないものもあったりするのでプロダクションとして使う際に は注意が必要 ◦ Amplify SDKとFirebase SDKの多くの機能はクライアント前提だったりするので ◦ SDKを使った処理をServer Actionで記述し、それをそのまま Client Componentからインポートすると 動かないことがあるので要注意