SST(Serverless Stack)完全ガイド — AWSサーバーレス開発の最適解
SSTとは
SST(Serverless Stack)は、AWSサーバーレスアプリケーション開発のためのフレームワークです。
特徴
- Live Lambda: ローカル開発でクラウドのLambdaをライブ更新
- 型安全: TypeScriptファーストで完全な型推論
- Next.js/Astro統合: フロントエンドフレームワークと簡単に統合
- Infrastructure as Code: AWS CDKベース
- 開発体験: ホットリロード、ローカルデバッグ
- 本番環境対応: マルチステージ、CI/CD
2026年現在、AWSサーバーレス開発の最有力フレームワークです。
なぜSSTなのか
従来のAWS開発の問題
SAM(Serverless Application Model)
- YAMLが冗長
- ローカル開発が遅い
- 型安全性がない
Serverless Framework
- 設定が複雑
- AWS以外にも対応しすぎて中途半端
- パフォーマンスが悪い
AWS CDK
- インフラ定義は良いが、開発体験が悪い
- ローカル開発が面倒
SSTの解決策
- Live Lambda: コード変更が即座にクラウドに反映
- 型安全: リソース参照が型推論される
- 統合開発: フロントエンド + バックエンド統一管理
- 高速: ビルドが高速、デプロイも高速
インストール
新規プロジェクト作成
npx create-sst@latest my-sst-app
cd my-sst-app
テンプレート選択:
- Minimal
- GraphQL API
- API with Postgres
- Next.js with API
- Astro with API
既存プロジェクトに追加
npm install sst
npx sst init
プロジェクト構造
my-sst-app/
├── sst.config.ts # SSTの設定
├── packages/
│ ├── functions/ # Lambda関数
│ │ └── src/
│ ├── core/ # 共通コード
│ └── web/ # フロントエンド(Next.js等)
└── .sst/ # SSTの内部ファイル
基本的な使い方
sst.config.ts
import { SSTConfig } from 'sst'
import { API } from './stacks/API'
export default {
config(_input) {
return {
name: 'my-sst-app',
region: 'ap-northeast-1',
}
},
stacks(app) {
app.stack(API)
},
} satisfies SSTConfig
スタック定義
stacks/API.ts:
import { StackContext, Api } from 'sst/constructs'
export function API({ stack }: StackContext) {
const api = new Api(stack, 'api', {
routes: {
'GET /': 'packages/functions/src/lambda.handler',
'GET /users': 'packages/functions/src/users.list',
'POST /users': 'packages/functions/src/users.create',
'GET /users/{id}': 'packages/functions/src/users.get',
},
})
stack.addOutputs({
ApiEndpoint: api.url,
})
}
Lambda関数
packages/functions/src/lambda.ts:
import { APIGatewayProxyHandlerV2 } from 'aws-lambda'
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
return {
statusCode: 200,
body: JSON.stringify({
message: 'Hello from SST!',
path: event.rawPath,
}),
}
}
ローカル開発
npm run dev
# または
npx sst dev
これでLive Lambdaが起動し、コード変更が即座にクラウドに反映されます。
API Gateway
RESTful API
import { StackContext, Api } from 'sst/constructs'
export function API({ stack }: StackContext) {
const api = new Api(stack, 'api', {
routes: {
'GET /posts': 'packages/functions/src/posts.list',
'POST /posts': 'packages/functions/src/posts.create',
'GET /posts/{id}': 'packages/functions/src/posts.get',
'PUT /posts/{id}': 'packages/functions/src/posts.update',
'DELETE /posts/{id}': 'packages/functions/src/posts.delete',
},
cors: {
allowMethods: ['GET', 'POST', 'PUT', 'DELETE'],
allowOrigins: ['http://localhost:3000'],
},
})
stack.addOutputs({
ApiEndpoint: api.url,
})
return api
}
パスパラメータ取得
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
const id = event.pathParameters?.id
return {
statusCode: 200,
body: JSON.stringify({ id }),
}
}
クエリパラメータ
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
const page = event.queryStringParameters?.page || '1'
const limit = event.queryStringParameters?.limit || '10'
return {
statusCode: 200,
body: JSON.stringify({ page, limit }),
}
}
リクエストボディ
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
const body = JSON.parse(event.body || '{}')
return {
statusCode: 201,
body: JSON.stringify({ created: body }),
}
}
認証(JWT)
import { StackContext, Api } from 'sst/constructs'
export function API({ stack }: StackContext) {
const api = new Api(stack, 'api', {
authorizers: {
jwt: {
type: 'jwt',
jwt: {
issuer: 'https://myapp.auth0.com/',
audience: ['https://api.myapp.com'],
},
},
},
routes: {
'GET /public': 'packages/functions/src/public.handler',
'GET /private': {
function: 'packages/functions/src/private.handler',
authorizer: 'jwt',
},
},
})
return api
}
DynamoDB
テーブル作成
import { StackContext, Table } from 'sst/constructs'
export function Database({ stack }: StackContext) {
const table = new Table(stack, 'Users', {
fields: {
userId: 'string',
email: 'string',
},
primaryIndex: { partitionKey: 'userId' },
globalIndexes: {
emailIndex: { partitionKey: 'email' },
},
})
return table
}
Lambda + DynamoDB
import { StackContext, Api, Table } from 'sst/constructs'
export function API({ stack }: StackContext) {
const table = new Table(stack, 'Users', {
fields: {
userId: 'string',
email: 'string',
},
primaryIndex: { partitionKey: 'userId' },
})
const api = new Api(stack, 'api', {
defaults: {
function: {
bind: [table],
},
},
routes: {
'GET /users': 'packages/functions/src/users.list',
'POST /users': 'packages/functions/src/users.create',
},
})
return { api, table }
}
DynamoDB操作
packages/functions/src/users.ts:
import { DynamoDBClient } from '@aws-sdk/client-dynamodb'
import { DynamoDBDocumentClient, PutCommand, ScanCommand } from '@aws-sdk/lib-dynamodb'
import { Table } from 'sst/node/table'
import { APIGatewayProxyHandlerV2 } from 'aws-lambda'
const client = new DynamoDBClient({})
const docClient = DynamoDBDocumentClient.from(client)
export const list: APIGatewayProxyHandlerV2 = async () => {
const result = await docClient.send(
new ScanCommand({
TableName: Table.Users.tableName,
})
)
return {
statusCode: 200,
body: JSON.stringify(result.Items),
}
}
export const create: APIGatewayProxyHandlerV2 = async (event) => {
const body = JSON.parse(event.body || '{}')
await docClient.send(
new PutCommand({
TableName: Table.Users.tableName,
Item: {
userId: crypto.randomUUID(),
email: body.email,
name: body.name,
createdAt: new Date().toISOString(),
},
})
)
return {
statusCode: 201,
body: JSON.stringify({ success: true }),
}
}
型安全なリソース参照
SSTは自動的に型を生成します:
import { Table } from 'sst/node/table'
// Table.Users.tableName が型推論される
const tableName = Table.Users.tableName
S3バケット
バケット作成
import { StackContext, Bucket } from 'sst/constructs'
export function Storage({ stack }: StackContext) {
const bucket = new Bucket(stack, 'Uploads', {
cors: [
{
allowedMethods: ['GET', 'PUT', 'POST', 'DELETE'],
allowedOrigins: ['*'],
},
],
})
return bucket
}
ファイルアップロード
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3'
import { Bucket } from 'sst/node/bucket'
const s3 = new S3Client({})
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
const body = JSON.parse(event.body || '{}')
await s3.send(
new PutObjectCommand({
Bucket: Bucket.Uploads.bucketName,
Key: `uploads/${Date.now()}-${body.filename}`,
Body: Buffer.from(body.content, 'base64'),
ContentType: body.contentType,
})
)
return {
statusCode: 200,
body: JSON.stringify({ success: true }),
}
}
RDS(PostgreSQL)
RDS作成
import { StackContext, RDS } from 'sst/constructs'
export function Database({ stack }: StackContext) {
const rds = new RDS(stack, 'Database', {
engine: 'postgresql13.9',
defaultDatabaseName: 'myapp',
migrations: 'packages/functions/migrations',
})
return rds
}
マイグレーション
packages/functions/migrations/1_create_users.mjs:
export async function up(db) {
await db.schema
.createTable('users')
.addColumn('id', 'serial', (col) => col.primaryKey())
.addColumn('email', 'varchar', (col) => col.notNull().unique())
.addColumn('name', 'varchar')
.addColumn('created_at', 'timestamp', (col) => col.defaultTo(db.fn.now()))
.execute()
}
export async function down(db) {
await db.schema.dropTable('users').execute()
}
Lambda + RDS
import { Kysely } from 'kysely'
import { DataApiDialect } from 'kysely-data-api'
import { RDSDataClient } from '@aws-sdk/client-rds-data'
import { RDS } from 'sst/node/rds'
interface Database {
users: {
id: number
email: string
name: string | null
created_at: Date
}
}
const db = new Kysely<Database>({
dialect: new DataApiDialect({
mode: 'postgres',
driver: {
client: new RDSDataClient({}),
database: RDS.Database.defaultDatabaseName,
secretArn: RDS.Database.secretArn,
resourceArn: RDS.Database.clusterArn,
},
}),
})
export const handler: APIGatewayProxyHandlerV2 = async () => {
const users = await db.selectFrom('users').selectAll().execute()
return {
statusCode: 200,
body: JSON.stringify(users),
}
}
Next.js統合
Next.jsサイト追加
import { StackContext, NextjsSite } from 'sst/constructs'
export function Web({ stack }: StackContext) {
const api = use(API)
const site = new NextjsSite(stack, 'site', {
path: 'packages/web',
environment: {
NEXT_PUBLIC_API_URL: api.url,
},
})
stack.addOutputs({
SiteUrl: site.url,
})
}
Next.jsからAPI呼び出し
packages/web/app/page.tsx:
export default async function Home() {
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/posts`)
const posts = await res.json()
return (
<div>
<h1>Posts</h1>
<ul>
{posts.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
</div>
)
}
Server Actions対応
'use server'
import { Resource } from 'sst'
export async function createPost(formData: FormData) {
const title = formData.get('title')
await fetch(`${Resource.Api.url}/posts`, {
method: 'POST',
body: JSON.stringify({ title }),
})
}
Astro統合
Astroサイト追加
import { StackContext, AstroSite } from 'sst/constructs'
export function Web({ stack }: StackContext) {
const api = use(API)
const site = new AstroSite(stack, 'site', {
path: 'packages/web',
environment: {
PUBLIC_API_URL: api.url,
},
})
stack.addOutputs({
SiteUrl: site.url,
})
}
SSR対応
packages/web/src/pages/posts.astro:
---
const res = await fetch(`${import.meta.env.PUBLIC_API_URL}/posts`)
const posts = await res.json()
---
<html>
<body>
<h1>Posts</h1>
<ul>
{posts.map((post: any) => (
<li>{post.title}</li>
))}
</ul>
</body>
</html>
Cron(スケジュール実行)
import { StackContext, Cron } from 'sst/constructs'
export function Scheduler({ stack }: StackContext) {
new Cron(stack, 'DailyReport', {
schedule: 'cron(0 9 * * ? *)', // 毎日9時
job: 'packages/functions/src/report.handler',
})
new Cron(stack, 'HourlySync', {
schedule: 'rate(1 hour)',
job: 'packages/functions/src/sync.handler',
})
}
SQS(キュー)
import { StackContext, Queue, Api } from 'sst/constructs'
export function Jobs({ stack }: StackContext) {
const queue = new Queue(stack, 'EmailQueue', {
consumer: 'packages/functions/src/email.handler',
})
const api = new Api(stack, 'api', {
defaults: {
function: {
bind: [queue],
},
},
routes: {
'POST /send-email': 'packages/functions/src/enqueue.handler',
},
})
return { queue, api }
}
メッセージ送信:
import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs'
import { Queue } from 'sst/node/queue'
const sqs = new SQSClient({})
export const handler: APIGatewayProxyHandlerV2 = async (event) => {
const body = JSON.parse(event.body || '{}')
await sqs.send(
new SendMessageCommand({
QueueUrl: Queue.EmailQueue.queueUrl,
MessageBody: JSON.stringify({
to: body.to,
subject: body.subject,
body: body.body,
}),
})
)
return {
statusCode: 200,
body: JSON.stringify({ queued: true }),
}
}
環境変数とシークレット
環境変数
import { StackContext, Api } from 'sst/constructs'
export function API({ stack }: StackContext) {
const api = new Api(stack, 'api', {
defaults: {
function: {
environment: {
STAGE: stack.stage,
REGION: stack.region,
},
},
},
routes: {
'GET /': 'packages/functions/src/lambda.handler',
},
})
}
シークレット(SSM Parameter Store)
import { StackContext, Config, Api } from 'sst/constructs'
export function API({ stack }: StackContext) {
const API_KEY = new Config.Secret(stack, 'API_KEY')
const api = new Api(stack, 'api', {
defaults: {
function: {
bind: [API_KEY],
},
},
routes: {
'GET /': 'packages/functions/src/lambda.handler',
},
})
}
シークレット設定:
npx sst secrets set API_KEY my-secret-value
Lambda内で使用:
import { Config } from 'sst/node/config'
export const handler = async () => {
const apiKey = Config.API_KEY
return {
statusCode: 200,
body: JSON.stringify({ configured: !!apiKey }),
}
}
マルチステージ
ステージ切り替え
# 開発環境
npx sst dev --stage dev
# 本番環境
npx sst deploy --stage prod
ステージごとの設定
export default {
config(_input) {
return {
name: 'my-app',
region: 'ap-northeast-1',
}
},
stacks(app) {
app.setDefaultFunctionProps({
runtime: 'nodejs20.x',
timeout: app.stage === 'prod' ? 30 : 10,
})
app.stack(API)
},
} satisfies SSTConfig
デプロイ
初回デプロイ
npx sst deploy --stage prod
CI/CD(GitHub Actions)
.github/workflows/deploy.yml:
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: 20
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ap-northeast-1
- name: Install dependencies
run: npm ci
- name: Deploy
run: npx sst deploy --stage prod
実践例: ブログAPI
完全な実装例:
stacks/index.ts:
import { SSTConfig } from 'sst'
import { API } from './API'
import { Web } from './Web'
export default {
config(_input) {
return {
name: 'blog-app',
region: 'ap-northeast-1',
}
},
stacks(app) {
app.stack(API).stack(Web)
},
} satisfies SSTConfig
stacks/API.ts:
import { StackContext, Api, Table } from 'sst/constructs'
export function API({ stack }: StackContext) {
const table = new Table(stack, 'Posts', {
fields: {
id: 'string',
userId: 'string',
createdAt: 'number',
},
primaryIndex: { partitionKey: 'id' },
globalIndexes: {
byUser: { partitionKey: 'userId', sortKey: 'createdAt' },
},
})
const api = new Api(stack, 'api', {
defaults: {
function: {
bind: [table],
},
},
routes: {
'GET /posts': 'packages/functions/src/posts.list',
'POST /posts': 'packages/functions/src/posts.create',
'GET /posts/{id}': 'packages/functions/src/posts.get',
'PUT /posts/{id}': 'packages/functions/src/posts.update',
'DELETE /posts/{id}': 'packages/functions/src/posts.delete',
},
})
stack.addOutputs({
ApiEndpoint: api.url,
})
return api
}
まとめ
SSTは2026年現在、AWSサーバーレス開発の最適解です。
メリット
- 開発速度: Live Lambdaで爆速開発
- 型安全: リソース参照が完全に型推論
- 統合開発: フロントエンド + バックエンド統一
- 本番対応: マルチステージ、CI/CD完備
- コスパ: サーバーレスで運用コストが低い
ユースケース
- REST API / GraphQL API
- Webアプリケーション(Next.js/Astro)
- バックグラウンドジョブ
- データパイプライン
- スタートアップのMVP
SAM、Serverless Framework、AWS CDKから移行する価値は十分にあります。