もしかして App Runner 上 Next.js の SSR で直接 DynamoDB 呼べますか?
TL;DR - YES!
はい、IAM Role を設定すればできます。Next.js と データソースの間に API を挟まなくてもよいです。便利ですね。ブログ記事をDynamoDBから取り出す想定で試しました。以下の手順でいけます。
- 記事を保管するDynamoDBを作成する(AWS CDK)
- DynamoDB へのアクセス権限をもった IAM ロールを作成する(AWS CDK)
- ブログ用 Next.js アプリの
getServerSideProps
で AWS SDK 経由での DynamoDB 操作を書く - AWS App Runner(以下 App Runner)にて ② で作成した IAM を割り当てつつサービスを作成する(手作業)
経緯など
前に書いたこの記事で Next.js の App Runner へのデプロイを試したのですが、SSRの可能性を試したくなりました。サーバー上で動くのなら getServerSideProps
でAWS SDK が使えるのかどうかという点です。もしそれができるならば、これまで例えば
- ブラウザ => React SPA => API Gateway => Lambda Function => DynamoDB
としていたところが、
- ブラウザ => Next.js(App Runner) => DynamoDB
というようにかなりショートカットできます。Fargateと違いVPCも考慮する必要がありません。何らかのブログ記事が DynamoDB に保存されているという想定のもと、早速ためしてみました。
バージョン情報
利用ツール | バージョン |
---|---|
Next.js | 10.2.3 |
aws-sdk-js | 2.918.0 |
順番
- DynamoDB と IAM を AWS CDK で作成する
- blog-starter-typescript を修正する
- App Runner をデプロイして試す
という感じで進めます。
DynamoDB と IAM を AWS CDK で作成する
AWS CDK は TypeScriptなどの プログラミング言語でAWSリソースを定義・デプロイするためのツールです。手でポチポチ作成するよりも素早く再現デプロイできるのでおすすめです。気になる方は DevelopersIO のタグを覗いてみてください。
AWS CDK でリソース定義&デプロイ
DynamoDB と IAM Role をデプロイするための記述例です:
import * as cdk from '@aws-cdk/core';
import { RemovalPolicy, Stack } from '@aws-cdk/core';
import * as dynamo from '@aws-cdk/aws-dynamodb';
import { AttributeType, ProjectionType } from '@aws-cdk/aws-dynamodb';
import * as iam from '@aws-cdk/aws-iam';
import { GlobalProps } from './global-props';
export async function blogServiceApplicationStack(
scope: cdk.Construct,
id: string,
global: GlobalProps,
): Promise<Stack> {
const stack = new cdk.Stack(scope, id, {
stackName: global.getStackName(id),
});
// ①
const articleTable = new dynamo.Table(stack, 'ArticleTable', {
tableName: global.getTableName('Article'),
partitionKey: { name: 'id', type: AttributeType.STRING },
removalPolicy: RemovalPolicy.DESTROY,
});
articleTable.addGlobalSecondaryIndex({
indexName: 'slug-index',
partitionKey: { name: 'slug', type: AttributeType.STRING },
projectionType: ProjectionType.ALL,
});
// ②
new iam.Role(stack, 'BlogInstanceRole', {
roleName: global.getRoleName('BlogInstance'),
assumedBy: new iam.ServicePrincipal('tasks.apprunner.amazonaws.com'),
managedPolicies: [
iam.ManagedPolicy.fromAwsManagedPolicyName(
'AmazonDynamoDBFullAccess',
),
],
});
// App Runner does not support CDK yet, please handmade:)
return stack;
}
- 記事用のテーブルを作成しています。
getServerSideProps
で使うことを考えるとおそらく slug からコンテンツを読み出す展開になるので、Global Secondary Index を貼って slug から特定できるようにしました。 - DynamoDB にアクセスできるロールを作成しています。
これを使ってデプロイします。CDKコマンド例です。
cdk deploy --app 'npx ts-node bin/blog.ts' --require-approval never BlogServiceStack
dev-xxx-Article-table
dev-xxx-BlogInstance-role
というふたつのリソースがAWS上に作成されます。
記事データを作成
Article テーブルに記事データを作成しておきましょう。ここは、手作業でいきます。
AWS Console > DynamoDB > Items: dev-greeting-service-Article-table > Item editor
図のような感じでコンテンツを埋めました。大事なのは slug
です。getServerSideProps
にてこれをもとに記事を探すようにするので、控えておきます。
blog-starter-typescript を修正する
またお世話になります。作成したいディレクトリに移動し、yarn create
します。
yarn create next-app --example blog-starter-typescript next-server
素の状態では Static Site Generation するコードなので、改変します。ターゲットファイルは next-server/pages/posts/[slug].tsx
です。以下のように改変しました。
import { useRouter } from "next/router";
import ErrorPage from "next/error";
import Container from "../../components/container";
import PostBody from "../../components/post-body";
import Header from "../../components/header";
import PostHeader from "../../components/post-header";
import Layout from "../../components/layout";
import PostTitle from "../../components/post-title";
import Head from "next/head";
import { CMS_NAME } from "../../lib/constants";
import markdownToHtml from "../../lib/markdownToHtml";
import PostType from "../../types/post";
import { GetServerSideProps } from "next";
import * as aws from "aws-sdk";
import Author from "../../types/author";
const Region = process.env.REGION!;
const BlogTableName = process.env.BLOG_TABLE_NAME!;
const dynamodb = new aws.DynamoDB.DocumentClient({
apiVersion: "2012-08-10",
region: Region,
signatureVersion: "v4",
});
type Props = {
post: PostType;
morePosts: PostType[];
preview?: boolean;
};
const Post = ({ post, morePosts, preview }: Props) => {
console.log("post", post);
const router = useRouter();
if (!router.isFallback && !post?.slug) {
return <ErrorPage statusCode={404} />;
}
return (
<Layout preview={preview}>
<Container>
<Header />
{router.isFallback ? (
<PostTitle>Loading…</PostTitle>
) : (
<>
<article className="mb-32">
<Head>
<title>
{post.title} | Next.js Blog Example with {CMS_NAME}
</title>
<meta property="og:image" content={post.ogImage.url} />
</Head>
<PostHeader
title={post.title}
coverImage={post.coverImage}
date={post.date}
author={post.author}
/>
<PostBody content={post.content} />
</article>
</>
)}
</Container>
</Layout>
);
};
export default Post;
type Params = {
params: {
slug: string;
};
};
type Article = {
slug: string;
title: string;
date: string;
coverImage: string;
author: Author;
excerpt: string;
ogImage: {
url: string;
};
content: string;
createAt: string;
};
export const getServerSideProps: GetServerSideProps<Props> = async ({
params,
}) => {
console.log("params", params);
const result = await dynamodb
.query({
TableName: BlogTableName,
IndexName: "slug-index",
Limit: 1,
KeyConditionExpression: "#slug = :slug",
ExpressionAttributeNames: {
"#slug": "slug",
},
ExpressionAttributeValues: {
":slug": (params?.slug as string) ?? "",
},
})
.promise();
const article: Article = result.Items![0] as Article;
console.log(article);
const content = await markdownToHtml(article.content);
return {
props: {
post: {
...article,
content,
},
morePosts: [],
},
};
};
少し多いので分割して見ていきます。
DynamoDB クライアント生成部分
import * as aws from "aws-sdk";
const Region = process.env.REGION!;
const BlogTableName = process.env.BLOG_TABLE_NAME!;
const dynamodb = new aws.DynamoDB.DocumentClient({
apiVersion: "2012-08-10",
region: Region,
signatureVersion: "v4",
});
環境変数からテーブル名を読み、AWS SDKで、DynamoDB のクライアントを生成しています。
getServerSideProps
部分
export const getServerSideProps: GetServerSideProps<Props> = async ({
params,
}) => {
console.log("params", params);
// ①
const result = await dynamodb
.query({
TableName: BlogTableName,
IndexName: "slug-index",
Limit: 1,
KeyConditionExpression: "#slug = :slug",
ExpressionAttributeNames: {
"#slug": "slug",
},
ExpressionAttributeValues: {
":slug": (params?.slug as string) ?? "",
},
})
.promise();
const article: Article = result.Items![0] as Article;
console.log(article);
// ②
const content = await markdownToHtml(article.content);
return {
props: {
post: {
...article,
content,
},
morePosts: [],
},
};
};
- 今回のテーマです。DynamoDB クライアント をつかって slug から記事を探しています。つまり、URLで
/posts/app-runner-nextjs
を開くとapp-runner-nextjs
という slug で検索をかけることになります。これが実行できるかどうかが成否をわけます。 - DynamoDB Item には 記事の中身が含まれているので、それをHTMLに変換しています。SSG の場合ここは Markdown ファイルから読み出す動きでしたが、今回は DynamoDB に Markdown テキストが入っています。
あとは props として return しています。props を受け取って JSX を返す部分は変えていません。変更点を GitHub にPUSHしてください。次は App Runner にデプロイしていきましょう。
App Runner にデプロイ
GitHub に PUSH したら、そのリポジトリをソースにして App Runner にソースコードデプロイします。AWS App Runner にSSRありの Next.js アプリをデプロイする(ソースコードデプロイ)も参照ください。今回違うのは、環境変数を設定するのと、作成した IAM Role を割り当てる点です。どちらもAWSコンソールで設定できます。
環境変数と IAM Role の設定
- Environment variables: Next.js で DynamoDB クライアントを生成するのに必要なので、環境変数をふたつ設定します
- Instance role: 先程 CDK で作成した IAM Role が指定できるはずなので、選びます
あとはサービスを作成でOKです。しばらく待ちます。
/posts/app-runner-nextjs
にアクセスしてみる
デプロイが終わったら DynamoDB に存在する slug でアクセスしてみます。SSRできていれば、DynamoDBと接続し、slug: app-runner-nextjs
に該当するコンテンツを検索してくれるはずです。
/posts/app-runner-nextjs
へのアクセスに対して、ブログページが正常に表示されていることがわかります。IAM Role を設定すれば、Next.js のSSRで直接 DynamoDB へアクセスできることがわかりました。
おわりに
今回試した結果より、IAMさえ設定すればあらゆるAWSのパブリックサービスに App Runner からアクセスできるということを意味します。Next.js はページごとにSSR/ISR/SSGを切り替えられるので、がっつりAWSリソースにアクセスするページはSSRで、ビルド時にS3から取得できれば良いページはSSGで…など、AWSのエコシステムとの相性が良さそうです。Lambda Function ではなかなか制限が大きく、FargateではVPCとの往来を考慮する必要があるというもどかしい状況でしたが、App Runner のおかげで一気に視界が開けたと感じます。Next.js と App Runner の組み合わせは他にも可能性を秘めている気がするので、今後もいろいろ試していきたいと思います。
ソースコード
モノレポでやっていますので一部yarn
コマンドが変わっています。yarn next-dev
など。フォークして試す場合は注意してください。
関連記事
Discussion