Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

自己紹介 ● HarrisonEagle ○ 本業はSRE ○ 副業や趣味などでフロントエンドのコード書いたりする ■ 最近はServer ActionとRSCを遊んでる ○ 好きなプログラミング言語: TypeScript, Go, Rust, Kotlin ○ ゴルフ、筋トレ、バードウォッチングが趣味 ○ GitHub: https://github.com/HarrisonEagle

Slide 3

Slide 3 text

これから話す内容 ● 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

Slide 4

Slide 4 text

AWS Amplify ● 簡単に言うとAWS版のVercel ○ GitHubリポジトリと連携するだけデプロイまで自動化できる ○ 認証機能を提供するCognito、ストレージ機能を提供する S3などのAWS機能との連携が 簡単にできる

Slide 5

Slide 5 text

今までのAmplify+DynamoDB+Cognitoの構成 これはRESTの場合だが、GraphQLの場合はAPI GatewayがAppSyncになる

Slide 6

Slide 6 text

今回作ったアプリの構成

Slide 7

Slide 7 text

Amplifyの中身 SSRの部分はフルマネージドになり、AmplifyでのSSRとServer ActionはLambdaに当たる部分で実行される模様

Slide 8

Slide 8 text

これは何が嬉しいか ● フロントエンドとバックエンドのロジックは両方とも同じリポジトリで管理できるので、管理しやす いし自動デプロイの恩恵が両方得られる ○ API GatewayとLambdaを構築する手間を省ける ○ 特にLambdaのコード管理とデプロイの自動化はめんどくなりがち ● CORS設定も必要ない ● ローカルでの検証も楽 ● これによってよりフロントエンド側に注力できる ただし、SSRとServer Actionの実行時間に制限があるかは不明

Slide 9

Slide 9 text

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 });

Slide 10

Slide 10 text

Cognitoのロールベースアクセスコントロール ● APIへの認証をつけない代わりに、 DynamoDBへのアクセスを認証済みのユー ザーだけできるようにする ● 認証で使用するCognitoのIdentity Poolの ユーザーアクセスに、ユーザーが認証された 場合のAWSサービスへのアクセスをロールと して設定できる ○ このロールに、認証されたユーザーが使用で きるDynamoDBへのアクセス権限を付与す る

Slide 11

Slide 11 text

バックエンド認証と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でユーザー情報を取得できる

Slide 12

Slide 12 text

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("/"); };

Slide 13

Slide 13 text

ただし... ● 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に渡すことで対策できる

Slide 14

Slide 14 text

トラブルが発生する可能性がある実装 "use client"; import { deleteNote } from "@/actions"; type Props = { note: Note; }; export const NoteCard = ({ note }: Props) => { return (   ... Delete ); };

Slide 15

Slide 15 text

対策 "use client"; type Props = { note: Note; deleteNote: (data: FormData) => Promise // Server Componentから渡す }; export const NoteCard = ({ note, deleteNote }: Props) => { return (   ... Delete ); };

Slide 16

Slide 16 text

まとめと考察 ● 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からインポートすると 動かないことがあるので要注意

Slide 17

Slide 17 text

ご清聴ありがとうございました!