SSTを使ってAWSにNextjsのフルスタックアプリ作る
はじめに
AWSのIaCの選択はたくさんありますが、今回はSST(Serverless Stack)を使ってフルスタックアプリを作ります。
感想
AWS CDKとの比較ですが
- シンプル
- デプロイが早い
- 開発しやすい
- 気軽にNextjsが使える
構成
このような構成とします。
github
NextjsのAWSデプロイについて
今までNextjsのAWSデプロイ先として選定されていたのはECSでした。というのもNextjsはSSRやISRなどサーバー側で動作する機能が多くあり、それらを機能させるためにはS3+Cloudfrontでは不可能でした。かといってAmplifyでは問題がいくつもありました。
↑勝手にリンク貼らせていただいています💦
結果的にAWSでサーバーレスというとSPAを選択する結論が多かった気がしています。
open-next
2年前(2023年ごろ)現たのがOpenNextです。OpenNextとはNext.jsアプリをVercel以外のプラットフォームでホスティングできるようにするオープンソースの取り組みです。現在CloudflareとSSTの開発メンバーがバックアップしてるようです。
結論、SSTではデフォルトでこのOpenNextを使うことで簡単にデプロイできるようになっています。静的ファイルについてはS3、動的なものはLambdaでデプロイし、CloudFrontから参照するような形で自動的にデプロイされます。
はじめる
こちらからSSTのモノレポの雛形を落としてきます。
SSTの公式にあるように、自分のプロジェクト名に変えます(MY_APPのところ)。
npx replace-in-file /monorepo-template/g MY_APP **/*.* --verbose
パッケージをインストールします
npm install
ローカルにAWS認証情報がある状態であれば、この状態でSSTのデプロイが可能です。
npx sst dev
モノレポサンプルはS3とLambdaだけの構成なのでこれから修正していきます。
インフラ
sst.config.tsを構成通りになるよう修正します。
- 全体サービス名:nextjsapp
- Nextjs関連名(cloudfront,s3,lambda):MyWeb
- API関連名(APIGateway,lambda):MyApi
- DynamoDB関連名(DynamoDB):MyTable
//sst.config.ts
/// <reference path="./.sst/platform/config.d.ts" />
export default $config({
app(input) {
return {
name: "nextjsapp",
removal: input?.stage === "production" ? "retain" : "remove",
protect: ["production"].includes(input?.stage),
home: "aws",
};
},
async run() {
const table = new sst.aws.Dynamo("MyTable", {
fields: {
userId: "string",
noteId: "string",
},
primaryIndex: { hashKey: "userId", rangeKey: "noteId" },
});
const api = new sst.aws.ApiGatewayV2("MyApi", {
cors: {
allowMethods: ["GET"],
allowOrigins: ["*"],
},
link: [table],
});
api.route("GET /", "packages/functions/src/api.handler");
new sst.aws.Nextjs("MyWeb", {
path: "packages/web",
link: [api],
});
return {
MyApi: api,
MyTable: table,
};
},
});
SSTではlinkオプションで権限を付与します。CDKではDynamoDBへのRead権限をLambdaに付与することでデータを読み取ることができるようになりますが、SSTはこれだけです。
functionsとcoreのロジックも載せておきます。
// packages/functions/src/api.ts
import { Handler } from "aws-lambda";
import { Example } from "@nextjsapp/core/example";
import { User } from "../../core/src/user";
import { docClient } from "@nextjsapp/core/dynamodb";
export const handler: Handler = async (_event) => {
console.log(_event);
// DynamoDBのテーブル内容(userId=1)をそのまま返すだけ
return {
statusCode: 200,
body: `${Example.hello()} : ${
JSON.stringify(
await User.getUser(docClient, {
userId: "1",
}),
)
}.`,
};
};
// packages/core/src/user/index.ts
import { DynamoDBDocumentClient, QueryCommand } from "@aws-sdk/lib-dynamodb";
import { Resource } from "sst";
export interface UserType {
userId: string;
noteId: string;
}
export namespace User {
export async function getUser(
docClient: DynamoDBDocumentClient,
params: { userId: string },
): Promise<UserType[]> {
const res = await docClient.send(
new QueryCommand({
TableName: Resource.MyTable.name,
KeyConditionExpression: "userId = :userId",
ExpressionAttributeValues: {
":userId": params.userId,
},
}),
);
return res.Items as UserType[];
}
}
// packages/core/src/dynamoDb/index.ts
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
const client = new DynamoDBClient();
export const docClient = DynamoDBDocumentClient.from(client);
Nextjsはこんな感じ(Nextjsの初期化は省略します)
// web/src/app/page.tsx
import { Resource } from 'sst';
export default async function Home() {
const data = await fetch(Resource.MyApi.url);
console.log(Resource);
return <div>{await data.text()}</div>;
}
ESLint
eslint-config-serverless-stack
を導入します
npm i -D eslint-config-serverless-stack
// package.json
{
・・・
"devDependencies": {
・・・
"eslint-config-serverless-stack": "^0.69.7",
・・・
},
・・・
"eslintConfig": {
"extends": [
"serverless-stack"
]
}
・・・
}
それ以外はモノレポ用のeslint,prettierのルート設定をします。
Debug
.vscode/launch.jsonを以下のように記述します
これで実Lambdaへのリモートデバッグができるようになります!
{
"version": "0.2.0",
"configurations": [
{
"name": "Debug SST Dev",
"type": "node",
"request": "launch",
"runtimeExecutable": "${workspaceRoot}/node_modules/.bin/sst",
"runtimeArgs": ["dev"],
"console": "integratedTerminal",
"skipFiles": ["<node_internals>/**"]
}
]
}
最後に感想
sstにビルドのみのオプションが欲しいですね。
リソース情報を参照できるsst-env.d.tsがデプロイ後じゃないと
生成されません。つまり単独でNextjsのビルドをしてもTypeエラーとなってしまいます。
このあたりなんとかしてくれると非常にありがたいですね〜。
補足*1 Lambda個別にPermissionを与えるには
//sst.config.ts
・・・
const func = new sst.aws.Function("MyFunction", {
url: false,
handler: "packages/functions/src/api.handler",
permissions: [
{
actions: ["dynamodb:Query"],
resources: [table.arn],
},
],
link: [table],
});
const api = new sst.aws.ApiGatewayV2("MyApi", {
cors: {
allowMethods: ["GET"],
allowOrigins: ["*"],
},
});
api.route("GET /", func.arn);
・・・
Discussion