AWS Amplifyを使ってNext.jsアプリをデプロイしてみよう
はじめに
最近ベースを習い出したtanesanです。本記事では、Next.jsアプリケーションのAWSリソースを使ったデプロイの選択肢としてAWS Amplifyを使用する方法をご紹介しようと思います。ハンズオン形式で進めていきますので、是非手元で実際にお試しいただき、Amplifyの開発者体験の良さを感じていただければと思います。
AWS Amplifyとは
AWS Amplifyは、Webおよびモバイルアプリケーションの開発・デプロイを簡単にできる仕組みを提供してくれる開発フレームワークです。AmplifyにはGen1と昨年発表されたGen2があり、Gen2ではAmplifyフォルダ内のresourceファイルを使用することで、ワークロードに必要な認証の構成や、DBのモデル等をコードファーストで定義することが可能です。Gen2は現時点ではまだプレビュー版で、一部Gen1で利用できた機能が使えませんが、今後のアップデートで対応されるものと思います。なので、今回は素振りもかねてGen2を利用し、Amplify Gen2の正式リリースに一緒に備えていきましょう!
Next.jsとは
Next.jsはVercel社によって開発されたReactアプリケーションのためのフレームワークです。サーバサイドレンダリング(SSR)や静的サイト生成(SSG)を簡単に実装することができます。また、バージョン13以降ではApp Routerを使用したコンポーネント単位でのSSRが可能になりました。AWS AmplifyはApp Routerを使用したアプリケーションのデプロイにも対応しています。今回のハンズオンでもApp Routerを使用したNext.jsアプリを作っていきます。
ハンズオン
ここからは実際にNext.jsを使ったアプリケーションをAWS Amplifyでデプロイするまでの作業をハンズオン形式で紹介していきます。前提として下記の環境を想定しています。
- Node.js v20.0.11
- ローカルでAWS CLIが実行可能(AmplifyBackendDeployFullAccessが付与されていること)
Next.jsアプリ作成
下記コマンドでNext.jsプロジェクトを作成します。
npx create-next-app@latest
いくつか設問がでるので、下記のように回答します(プロジェクト名は適当でOKです。)。
✔ What is your project named? … amplify-sample
✔ Would you like to use TypeScript? … No / Yes
✔ Would you like to use ESLint? … No / Yes
✔ Would you like to use Tailwind CSS? … No / Yes
✔ Would you like to use `src/` directory? … No / Yes
✔ Would you like to use App Router? (recommended) … No / Yes
✔ Would you like to customize the default import alias (@/*)? … No / Yes
一度下記コマンドで起動してみましょう。Next.jsのサンプル画面が表示されればOKです。
npm run dev
Amplifyリソースファイルを追加
Amplifyのリソースファイルを追加します。下記のコマンドを実行します。
npm create amplify@latest
コマンドを実行すると、プロジェクトルートにAmplifyフォルダが作成されます。Amplifyフォルダの中には、設定ファイルを除くとauthフォルダ・dataフォルダ・backend.tsが含まれています。それぞれの役割を説明します。
authフォルダ
authフォルダ内のresource.tsはAWS Cognitoの設定を記述します。このファイルで定義した設定をもとにAmplifyがCognitoのリソースを作成してくれます。Cognitoのユーザプールに設定する属性やMFAの設定等もこのresource.tsファイル内で行うことができます。
dataフォルダ
dataフォルダ内のresource.tsはDynamoDBのスキーマやデータに対する認可の設定を定義することができます。このファイルで記述したファイルをもとにAmplifyがDynamoDBのテーブルを作成してくれます。モデルを複数定義することができ、モデル間のリレーションも表現できます。モデルを複数定義した場合は、モデルごとにテーブルが作成されます。
backend.tsファイル
auth・dataフォルダ内のresource.tsで定義したリソースをbackendとして定義します。また、auth・dataで定義した以外のAWSリソースを作成したい場合はAWS CDKで定義することができます。注意点として、AWS CDKでリソースを定義した場合は、後述するサンドボックス環境の管理対象にならず、削除を手動で行う必要があります。
Sandboxを使ってみる
Amplifyの目玉機能にSandboxという機能があります。Sandboxを使うことで、開発者ごとにAWSリソースを作成することができるので、ローカルでの動作検証のためにDocker composeファイルの作成やログイン機能の動作の切り替え等を設定する必要がなくなります。Sandbox環境を作成するために下記のコマンドを実行します。
npx amplify sandbox
上記を実行することで、サンドボックス環境のAWSリソースと作成されたリソースへの接続上を持つ設定ファイル(amplifyconfiguration.json)が生成されます。また、コマンドを実行するとwatchモードになり、amplifyフォルダ内のファイルに変更があると自動的に Sandbox環境のAWSリソースに反映してくれます。
ログイン画面の作成
Sandbox環境で作成したCognitoを使用するログイン画面を作ってみます。今回はAmplifyが提供するUIを使用したいと思います。まずは下記コマンドでAmplify UIを追加します。
npm install @aws-amplify/ui-react @aws-amplify/adapter-nextjs
下記のコードを追加します。
// components/ConfigureAmplify.tsx
"use client";
import { Amplify } from "aws-amplify";
import config from "@/amplifyconfiguration.json";
Amplify.configure(config, { ssr: true });
export default function ConfigureAmplifyClientSide() {
return null;
}
// app/layout.tsx
import "@aws-amplify/ui-react/styles.css";
import type { Metadata } from "next";
import { Inter } from "next/font/google";
import "./globals.css";
import ConfigureAmplifyClientSide from "@/components/ConfigureAmplify";
const inter = Inter({ subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body className={inter.className}>
<ConfigureAmplifyClientSide />
{children}
</body>
</html>
);
}
// utils/amplify-utils.ts
import { cookies } from "next/headers";
import { createServerRunner } from "@aws-amplify/adapter-nextjs";
import { generateServerClientUsingCookies } from "@aws-amplify/adapter-nextjs/api";
import { getCurrentUser } from "aws-amplify/auth/server";
import { type Schema } from "@/amplify/data/resource";
import config from "@/amplifyconfiguration.json";
export const { runWithAmplifyServerContext } = createServerRunner({
config,
});
export const cookiesClient = generateServerClientUsingCookies<Schema>({
config,
cookies,
});
export async function AuthGetCurrentUserServer() {
try {
const currentUser = await runWithAmplifyServerContext({
nextServerContext: { cookies },
operation: (contextSpec) => getCurrentUser(contextSpec),
});
return currentUser;
} catch (error) {
console.error(error);
}
}
ログインのコンポーネントを追加します。
// components/Login.tsx
"use client";
import { withAuthenticator } from "@aws-amplify/ui-react";
import { AuthUser } from "aws-amplify/auth";
import { redirect } from "next/navigation";
import { useEffect } from "react";
function Login({ user }: { user?: AuthUser }) {
useEffect(() => {
if (user) {
redirect("/");
}
}, [user]);
return null;
}
export default withAuthenticator(Login);
ログインページを追加します。
// app/login/page.tsx
import Login from '@/components/Login';
export default function LoginPage() {
return <Login />;
}
ログアウトのコンポーネントを追加します。
// components/Logout.tsx
"use client";
import { signOut } from "aws-amplify/auth";
import { useRouter } from "next/navigation";
export default function Logout() {
const router = useRouter();
return (
<button
onClick={async () => {
await signOut();
router.push("/login");
}}
className="px-2 bg-white text-black"
>
Sign out
</button>
);
}
ミドルウェアを設定します。
※ミドルウェア:Next.jsではmddleware.tsファイル作成することでリクエスト前に処理を差し込むことができる。
import { NextRequest, NextResponse } from "next/server";
import { fetchAuthSession } from "aws-amplify/auth/server";
import { runWithAmplifyServerContext } from "@/utils/amplify-utils";
export async function middleware(request: NextRequest) {
const response = NextResponse.next();
const authenticated = await runWithAmplifyServerContext({
nextServerContext: { request, response },
operation: async (contextSpec) => {
try {
const session = await fetchAuthSession(contextSpec, {});
return session.tokens !== undefined;
} catch (error) {
console.log(error);
return false;
}
},
});
if (authenticated) {
return response;
}
return NextResponse.redirect(new URL("/login", request.url));
}
export const config = {
matcher: [
"/((?!api|_next/static|_next/image|favicon.ico|login).*)",
],
};
ここまでの追加できたらもう一度下記コマンドでアプリケーションを立ち上げてアクセスしてみましょう。
npm run dev
エラーがなければ下記のようなログイン画面が表示されているはずです。
次のステップのために、ここで適当なアカウントを作成しておいてください。
Todo新規作成フォームの追加
認証を実際に確認したので、次はSandbox環境のDynamoDBを使ってデータの取得・追加をできるようにしてみましょう。
まずは、モデルのアクセス権をオーナーのみに変更します。
// amplify/data/resource.ts
const schema = a.schema({
Todo: a
.model({
content: a.string(),
})
.authorization([a.allow.owner()]),
});
データアクセスの認可方法をAPI KeyからUserPoolに変更します。
// amplify/data/resource.ts
export const data = defineData({
schema,
authorizationModes: {
defaultAuthorizationMode: 'userPool'
}
});
ホーム画面に一覧と新規作成のフォームを追加します。
import { revalidatePath } from 'next/cache';
import { AuthGetCurrentUserServer, cookiesClient } from '@/utils/amplify-utils';
import Logout from './login/Logout';
async function App() {
const user = await AuthGetCurrentUserServer();
const { data: todos } = await cookiesClient.models.Todo.list();
async function addTodo(data: FormData) {
'use server';
const title = data.get('title') as string;
await cookiesClient.models.Todo.create({
content: title,
});
revalidatePath('/');
}
return (
<>
<h1>Hello, Amplify 👋</h1>
{user && <Logout />}
<form action={addTodo}>
<input type="text" name="title" />
<button type="submit">Add Todo</button>
</form>
<ul>
{todos && todos.map((todo) => <li key={todo.id}>{todo.content}</li>)}
</ul>
</>
);
}
export default App;
ここまでの追加でエラーがなければ↓のような画面が表示されているはずです!
MFAの追加
MFAの追加もauthフォルダのresource.tsに設定を追加することで、簡単に実装できます。今回はワンタイムパスワードによるMFAをログイン時に強制するように下記の設定を加えます。
// amplify/auth/resource.tsexport
const auth = defineAuth({
/*****/
multifactor: {
mode: 'REQUIRED',
totp: true,
sms: false,
},
});
アプリケーション画面に戻り、もう一度サインインすると、下記のようにワンタイムパスワードを設定するための二次元コードが表示されるはずです!
デプロイ
さて、ここまでで一通りローカルで動作確認をすることができましたので、いよいよデプロイをしていきたいと思います。まずは、適当なGithubリポジトリを作成しコードをpushしてください。
リポジトリの作成が終わったらAWSのコンソールに移動し、Amplifyのホームからアプリを作成を選択します。
続いてソースコードプロバイダーを選択の画面にになるので、オプション2を選択→Githubを選択→次への順で選択してください。
リポジトリをAmplifyと連携していきます。Update Github permissionsを選択して、先ほど作成したリポジトリとAmplifyの連携を許可します。
連携が完了したらブランチを選択して次へを選択してください。
アプリケーションの設定画面になるので、詳細設定でビルドイメージとNext.js、Node.jsのバージョンを下記の通り変更します。
設定を確認して問題なければ保存してデプロイを選択します。今回の例だと5分程度待つとデプロイが完了します。
デプロイされたURLにアクセスしてログイン画面が表示されていればOKです!お疲れさまでした!
(補足)環境変数の設定
Amplifyではコンソール上から下記のように環境変数を設定することができます。
Next.jsアプリケーションの場合は、コンソール上で環境変数を設定するだけでは反映されず、ビルドの設定にあるamplify.ymlを変更する必要があります。環境変数を使用する場合は、下記のように明示的に.envファイルを作成するコマンドを追加してください。amplify.ymlはリポジトリにも含めることができます。その場合はリポジトリ上のamplify.ymlが優先されることに注意してください。
// amplify.yml
version: 1
backend:
phases:
build:
commands:
- nvm use 18
- npm ci
- npx amplify pipeline-deploy --branch $AWS_BRANCH --app-id $AWS_APP_ID
frontend:
phases:
preBuild:
commands:
- npm ci
build:
commands:
- env | grep -e AUTH_GITHUB >> .env // ←を追加
- npm run build
artifacts:
baseDirectory: .next
files:
- '**/*'
cache:
paths:
- node_modules/**/*
おわりに
本記事では、AWS Amplifyを使ったNext.jsアプリケーションのデプロイをハンズオン形式で行いました。Amplifyを使用することでフルスタックなアプリケーションを素早く開発できることを少しでも体感いただけていたら嬉しいです。今回は紹介しきれませんでしたが、Amplifyを使用することで、ブランチ単位のデプロイであったり、PRに応じたプレビュー環境のデプロイ(Gen2はまだ未対応)など、レビューの品質をグッと向上させるといったことも期待できます。プロジェクトの要件やワークロードが合えば是非Amplifyを使ってみてはいかがでしょうか。それでは!
参考
- https://docs.amplify.aws/gen2/how-amplify-works/
- https://nextjs.org/docs
- https://docs.amplify.aws/gen2/reference/iam-policy/#pageMain
- https://docs.amplify.aws/gen2/build-a-backend/data/data-modeling/relationships/
- https://docs.amplify.aws/gen2/deploy-and-host/sandbox-environments/features/#pageMain
- https://docs.amplify.aws/gen2/start/quickstart/nextjs-app-router-server-components/#build-ui
- https://docs.amplify.aws/gen2/deploy-and-host/fullstack-branching/secrets-and-vars/#pageMain