AWS Lambda入門 - サーバーレス関数の実装

中級 | 30分 で読める | 2025.01.10

公式ドキュメント

AWS Lambdaとは

AWS Lambdaは、サーバー管理なしでコードを実行できるサーバーレスコンピューティングサービスです。リクエスト数と実行時間に基づく従量課金制です。

セットアップ

実践メモ: AWS SAM CLIを使うとローカルでLambda関数をテスト・デバッグできます。本番デプロイ前に必ずローカルテストしましょう

AWS SAM CLI

# SAM CLIインストール
brew install aws-sam-cli

# プロジェクト初期化
sam init --runtime nodejs20.x --name my-lambda

# ディレクトリ構造
my-lambda/
├── template.yaml
├── src/
│ └── handlers/
│ └── hello.ts
├── package.json
└── tsconfig.json

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Globals:
 Function:
 Timeout: 30
 Runtime: nodejs20.x
 MemorySize: 256
 Environment:
 Variables:
 NODE_ENV: production

Resources:
 HelloFunction:
 Type: AWS::Serverless::Function
 Properties:
 Handler: dist/handlers/hello.handler
 Events:
 Api:
 Type: Api
 Properties:
 Path: /hello
 Method: get
 Metadata:
 BuildMethod: esbuild
 BuildProperties:
 Minify: true
 Target: es2020

基本的なハンドラー

// src/handlers/hello.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';

export const handler = async (
 event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
 try {
 const name = event.queryStringParameters?.name || 'World';

 return {
 statusCode: 200,
 headers: {
 'Content-Type': 'application/json',
 },
 body: JSON.stringify({
 message: `Hello, ${name}!`,
 timestamp: new Date().toISOString(),
 }),
 };
 } catch (error) {
 console.error('Error:', error);

 return {
 statusCode: 500,
 body: JSON.stringify({ error: 'Internal Server Error' }),
 };
 }
};

template.yamlはLambdaのインフラ定義そのものなので慎重に管理してください。

DynamoDB連携

// src/handlers/users.ts
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import {
 DynamoDBDocumentClient,
 GetCommand,
 PutCommand,
 QueryCommand,
} from '@aws-sdk/lib-dynamodb';

const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);
const TABLE_NAME = process.env.USERS_TABLE!;

// ユーザー取得
export const getUser = async (event: APIGatewayProxyEvent) => {
 const userId = event.pathParameters?.id;

 const { Item } = await docClient.send(new GetCommand({
 TableName: TABLE_NAME,
 Key: { userId },
 }));

 if (!Item) {
 return { statusCode: 404, body: JSON.stringify({ error: 'Not found' }) };
 }

 return { statusCode: 200, body: JSON.stringify(Item) };
};

// ユーザー作成
export const createUser = async (event: APIGatewayProxyEvent) => {
 const body = JSON.parse(event.body || '{}');
 const userId = crypto.randomUUID();

 await docClient.send(new PutCommand({
 TableName: TABLE_NAME,
 Item: {
 userId,
 ...body,
 createdAt: new Date().toISOString(),
 },
 }));

 return {
 statusCode: 201,
 body: JSON.stringify({ userId }),
 };
};

template.yaml (DynamoDB)

Resources:
 UsersTable:
 Type: AWS::DynamoDB::Table
 Properties:
 TableName: users
 BillingMode: PAY_PER_REQUEST
 AttributeDefinitions:
 - AttributeName: userId
 AttributeType: S
 KeySchema:
 - AttributeName: userId
 KeyType: HASH

 UsersFunction:
 Type: AWS::Serverless::Function
 Properties:
 Handler: dist/handlers/users.getUser
 Environment:
 Variables:
 USERS_TABLE: !Ref UsersTable
 Policies:
 - DynamoDBCrudPolicy:
 TableName: !Ref UsersTable

ポイント: DynamoDBDocumentClientを使うとJavaScriptオブジェクトをそのまま読み書きでき、型変換の手間が省けます

ミドルウェアパターン(middy)

// src/handlers/protected.ts
import middy from '@middy/core';
import httpJsonBodyParser from '@middy/http-json-body-parser';
import httpErrorHandler from '@middy/http-error-handler';
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';

const baseHandler = async (
 event: APIGatewayProxyEvent & { body: any }
): Promise<APIGatewayProxyResult> => {
 const { name, email } = event.body;

 // ビジネスロジック
 const result = await processUser({ name, email });

 return {
 statusCode: 200,
 body: JSON.stringify(result),
 };
};

// ミドルウェアをチェーン
export const handler = middy(baseHandler)
 .use(httpJsonBodyParser())
 .use(httpErrorHandler());

実践メモ: middyを使うとJSONパース、エラーハンドリングなどの横断的関心事をミドルウェアとして分離できます

認証(Cognito連携)

// src/middleware/auth.ts
import { CognitoJwtVerifier } from 'aws-jwt-verify';

const verifier = CognitoJwtVerifier.create({
 userPoolId: process.env.USER_POOL_ID!,
 tokenUse: 'access',
 clientId: process.env.CLIENT_ID!,
});

export const authMiddleware = () => ({
 before: async (request: any) => {
 const token = request.event.headers.authorization?.replace('Bearer ', '');

 if (!token) {
 throw { statusCode: 401, message: 'Unauthorized' };
 }

 try {
 const payload = await verifier.verify(token);
 request.event.user = payload;
 } catch (error) {
 throw { statusCode: 401, message: 'Invalid token' };
 }
 },
});

// 使用
export const handler = middy(baseHandler)
 .use(authMiddleware())
 .use(httpErrorHandler());

注意: トークン検証に失敗した場合は必ず401を返しましょう。エラーメッセージに内部情報を含めないよう注意してください。

コールドスタート対策

ポイント: DB接続などの重い初期化処理はハンドラー関数の外に置くことで、コールドスタートの影響を最小化できます。

// 接続の再利用
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';

// ハンドラーの外で初期化(再利用される)
const client = new DynamoDBClient({});

export const handler = async (event) => {
 // clientは再利用される
 // ...
};
# Provisioned Concurrency(コールドスタート回避)
Resources:
 MyFunction:
 Type: AWS::Serverless::Function
 Properties:
 AutoPublishAlias: live
 ProvisionedConcurrencyConfig:
 ProvisionedConcurrentExecutions: 5

Provisioned Concurrencyを有効にすると常時ウォームな関数を保てますが、コストが増加します。

デプロイ

注意: sam deploy --guidedは初回のみ使用し、2回目以降はsamconfig.tomlが自動参照されます。

# ビルド
sam build

# ローカルテスト
sam local invoke HelloFunction --event events/hello.json
sam local start-api

# デプロイ
sam deploy --guided

# CI/CDでのデプロイ
sam deploy \
 --stack-name my-app-prod \
 --s3-bucket my-deploy-bucket \
 --capabilities CAPABILITY_IAM \
 --no-confirm-changeset

関連記事

まとめ

AWS Lambdaは、スケーラブルなサーバーレスアプリケーションを構築できます。DynamoDB、API Gateway、Cognitoと組み合わせることで、フルスタックアプリを実現できます。

参考リソース

← 一覧に戻る
PR
PR
PR
PR