Serverless Stackでフルスタック開発 - サーバレス & フロントエンド開発
概要
本記事では、サーバレス開発を容易にするフレームワーク「Serverless Stack」(SST)を紹介します。SSTを使用することで、バックエンドのサーバレス開発だけでなく、フロントエンドの開発もスムーズに行えるようになります。
サンプルとして、シンプルなクリックカウントアプリを作成し、その構成と技術的要素を解説します。
Serverless Stack(SST)とは
Serverless Stack(SST)は、AWSのCloud Development Kit(CDK)を基盤として構築された、サーバレスアプリケーション開発を効率化するフレームワークです。SSTの採用により、以下のようなプロセスが簡素化され、開発者の作業負担が軽減されます。
- AWS CDKを使用したインフラの定義
- Live Lambda Development機能を使用したリアルタイムテスト
- 複数言語でのLambdaデプロイの簡易化
- バックエンドとフロントエンドのシームレスな連携
これらの特徴をサンプルアプリを通じて詳しく見ていきます。
サンプルアプリの概要
このサンプルアプリは、ユーザーのクリックをリアルタイムでカウントし、結果をダイナミックに表示する機能を備えています。このプロジェクトでは、以下のような技術スタックが採用されています。
- Serverless Stack: サーバレスとフロントエンドを連携しつつ、両方デプロイすることができるフレームワークです
- Lambda: バックエンドのコードを実行するサービスです
- DynamoDB: クリック数を保存しておくDBです
- API Gateway: バックエンドのエンドポイントです
- S3: フロントエンドのファイルを保存・配信するために使用します
- CloudFront: フロントエンドのエンドポイントとして使用します
- TypeScript: バックエンドとフロントエンドとServerless Stackの実装で使用します
- Next.js: フロントエンドのUIフレームワークです
サンプルアプリのすべてのコードは、以下のリポジトリに置いてあります。
ディレクトリ構成の詳細
サンプルプロジェクトのディレクトリ構成は、以下のように整理されています。この構成は、インフラストラクチャ・アズ・コード(IaC)、フロントエンド、バックエンドのコードを明確に分離し、プロジェクトの管理を容易にします。
.
├── package.json # プロジェクトの依存関係やスクリプトを管理
├── packages
│ ├── core # コア機能のコード
│ ├── frontend # フロントエンドのコード
│ └── functions # サーバーレス関数(バックエンド)のコード
├── pnpm-lock.yaml # pnpmによる依存関係のバージョンロック
├── pnpm-workspace.yaml # pnpmのワークスペース設定
├── sst.config.ts # SSTの設定
├── stacks
│ ├── ExampleStack.ts # SSTスタックの例
│ └── stacks.code-workspace # Visual Studio Codeのワークスペース設定
└── tsconfig.json # TypeScriptのコンパイラオプション設定
各ディレクトリの役割
- packages/core: このディレクトリは、アプリケーションのコア機能や共通のロジックを含むコードを格納します。これにより、コードの再利用性が高まり、メンテナンスが容易になります。
- packages/frontend: フロントエンドのコードはこのディレクトリに集約されています。Next.jsをベースにした構成で、通常のWebアプリケーション開発と同様の方法で開発を進めることができます。
- packages/functions: サーバーレス関数、つまりバックエンドのコードはここに配置されます。これにより、バックエンドのロジックが一箇所に集中し、管理がしやすくなります。
その他のファイル
- pnpm-lock.yaml と pnpm-workspace.yaml: これらのファイルは、pnpmパッケージマネージャを使用している場合に重要です。依存関係の管理やワークスペースの設定を効率的に行うことができます。
- sst.config.ts: SSTの設定ファイルで、プロジェクトのサーバレス環境に関する設定を管理します。
- stacks/ExampleStack.ts: このファイルは、SSTを使用して構築されたインフラストラクチャの例を示しています。ここで、Lambda関数、DynamoDBテーブル、API Gatewayなどのリソースを定義します。
このように、プロジェクトのディレクトリ構成は、開発の各フェーズ(フロントエンド、バックエンド、インフラストラクチャ)を明確に分離し、開発プロセスを整理しやすくすることを目的としています。
インフラ(IaC)の実装(stacks/ExampleStack.ts)
DynamoDB
DynamoDBの設定は、AWS CDKとほぼ変わらないため、学習コストは低いです。以下のようにDBを作成します。
// DynamoDBテーブルを作成
const table = new Table(stack, "counter", { // "counter"という名前のDynamoDBテーブルを作成します
fields: {
counter: "string", // "counter"フィールドを文字列型で定義します
},
primaryIndex: {partitionKey: "counter"}, // パーティションキーを"counter"に設定します
});
API
API GatewayとLambdaの設定は、CDKに比べて実装がシンプルで簡単になっています。
// API GatewayエンドポイントとLambdaを作成
const api = new Api(stack, "api", { // "api"という名前のAPI Gatewayエンドポイントを作成します
// 全Lambda関数の共通設定
defaults: {
function: {
bind: [table], // DynamoDBテーブルをバインドします
runtime: "nodejs20.x" // ランタイムを"nodejs20.x"に設定します
},
},
// APIエンドポイントの設定
routes: {
"GET /count": "packages/functions/src/get.main", // GETリクエストを"packages/functions/src/get.main"にルーティングします
"POST /count": "packages/functions/src/put.main", // POSTリクエストを"packages/functions/src/put.main"にルーティングします
},
});
フロントエンド
Next.jsのデプロイ設定は以下のとおりです。
// Next.jsのデプロイ設定
const site = new NextjsSite(stack, "NextSite", { // "NextSite"という名前のNext.jsサイトを作成します
bind: [siteBucket, api], // S3バケットとAPI Gatewayエンドポイントをバインドします
path: "packages/frontend", // ソースコードのパスを"packages/frontend"に設定します
environment: {
NEXT_PUBLIC_API_ENDPOINT: api.url, // 環境変数"NEXT_PUBLIC_API_ENDPOINT"にAPIのURLを設定します
},
});
フロントエンドの実装では、 NEXT_PUBLIC_API_ENDPOINT
を使用してAPIのエンドポイントを参照できます。これはCDKの設定によって連携されています。
バックエンド実装(packages/backend)
DynamoDBの接続
DynamoDBへの接続は、 Table.counter.tableName
を使用してテーブル名を参照できるように鳴っています。
import { Table } from "sst/node/table"; // sst/node/tableからTableをインポートします
export const updateCount = async (count: number) => { // updateCount関数を非同期で定義し、引数にcountを取ります
let params = { // パラメータを定義します
TableName: Table.counter.tableName, // テーブル名を指定します
Item: { // アイテムを指定します
counter: "clicks", // counterの値を"clicks"に設定します
count: count, // countの値を引数のcountに設定します
},
}
await put(params); // put関数を呼び出し、paramsを引数に渡します
};
Live Lambda Development
「Live Lambda Development」は、Serverless Stack(SST)が提供する機能で、AWS Lambda関数のローカルデバッグとテストを可能にするものです。
Live Lambda Develomentは、AWSリソースによってリモートで呼び出されている間に、Lambda関数をローカルでデバッグし、テストすることを可能にします。このプロセスは、AWSからのリクエストをローカルマシンにプロキシすることで実現されています。
pnpm run dev
コマンドを実行することでLive Lambda Development環境をAWS上にセットアップします。
SSTのドキュメントでは、VS Codeや他のIDEでのデバッグ設定、さまざまなランタイム環境を持つLambda関数の取り扱い、VPC内のLambda関数の取り扱いに関する詳細なガイダンスが提供されています。
Serverless Offline、SAM Accelerate、CDK Watchなどの他のサーバレスフレームワークやローカル開発ツールと比較して、Live Lambda Developmentは、速度と効率で優れているそうです。
フロントエンド実装(packages/frontend)
サンプルプログラムのフロントエンドは、Next.jsを使用して構築されています。この部分の重要なポイントは、SSTを使用してバックエンドとの連携が容易になっていることです。具体的には、API Gatewayのエンドポイントが NEXT_PUBLIC_API_ENDPOINT
という環境変数を通じてフロントエンドで参照できるように設定されています。これにより、フロントエンドからバックエンドのAPIを簡単に呼び出すことが可能になります。
import { ResponseCount } from "@click-count/core/types"; // "@click-count/core/types"からResponseCountをインポートします
const endpoint = () => `${process.env.NEXT_PUBLIC_API_ENDPOINT}/count`; // APIエンドポイントを定義します
export const getCount = async (): Promise<ResponseCount> => { // getCount関数を非同期で定義し、Promise<ResponseCount>を返します
const res = await fetch(endpoint(), { // APIエンドポイントからデータを取得します
method: 'GET', // HTTPメソッドはGETを使用します
mode: 'cors', // CORSモードを使用します
});
return res.json(); // 取得したデータをJSON形式で返します
};
export const updateCount = async (): Promise<ResponseCount> => { // updateCount関数を非同期で定義し、Promise<ResponseCount>を返します
const res = await fetch(endpoint(), { // APIエンドポイントからデータを取得します
method: 'POST', // HTTPメソッドはPOSTを使用します
mode: 'cors', // CORSモードを使用します
});
return res.json(); // 取得したデータをJSON形式で返します
};
一方で、バックエンドとの連携以外の部分は、フロントエンドの実装は通常のNext.jsアプリケーションと同様です。つまり、ディレクトリ構成やコンポーネントの作成、ページのルーティングなど、Next.jsの標準的な開発プラクティスに従って進めることができます。これにより、Next.jsに慣れている開発者は、SSTを使用しても、フロントエンドの開発において新たな学習校ストをほとんど負うこと無く、既存の知識と経験を活かして開発を進めることができます。
以下のコードは、フロントエンドの実装であり、ごく普通のNext.js(React.js)のコードとなっています。
'use client'; // クライアントを使用します
import { useEffect, useState } from 'react'; // reactからuseEffectとuseStateをインポートします
import styles from './Page.module.css'; // スタイルをインポートします
import { getCount, updateCount } from './api'; // apiからgetCountとupdateCountをインポートします
export default function Page() { // Page関数をエクスポートします
const [count, setCount] = useState(0); // countとsetCountのstateを設定します
const [isLoading, setIsLoading] = useState(false); // isLoadingとsetIsLoadingのstateを設定します
useEffect(() => { // useEffectを使用します
const fetchData = async () => { // fetchData関数を非同期で定義します
setIsLoading(true); // データ取得中にisLoadingをtrueに設定します
const data = await getCount(); // getCountからデータを取得します
setCount(data.count); // 取得したデータをsetCountで設定します
setIsLoading(false); // データ取得が完了したらisLoadingをfalseに設定します
};
fetchData(); // fetchData関数を呼び出します
}, []);
const onClick = async () => { // onClick関数を非同期で定義します
setIsLoading(true); // データ更新中にisLoadingをtrueに設定します
const data = await updateCount(); // updateCountからデータを取得します
setCount(data.count); // 取得したデータをsetCountで設定します
setIsLoading(false); // データ更新が完了したらisLoadingをfalseに設定します
};
return (
<div className={styles.App}>
<>
{count > 0 && <p className={styles.paragraph}>You clicked me {count} times.</p>}
<button className={styles.button} onClick={onClick} disabled={isLoading}>Click Me!</button>
</>
</div>
);
}
おわりに
Serverless Stack(SST)は、サーバレス開発を容易にし、フロントエンド開発もスムーズに行えるようにするフレームワークです。ぜひ、サーバレスバックエンドとSPAフロントエンドのフルスタック開発を快適に楽しんでください。
Discussion