📚 この記事は「AWS 完全攻略シリーズ」の Episode 12 です。
AWS Lambdaは、サーバーを管理することなくコードを実行できるサーバーレスコンピューティングサービスです。SESエンジニアにとって、Lambda を理解していることは市場価値を大きく高める武器になります。
本記事では、AWS Lambdaの基礎概念から実際のSES現場で役立つ実装パターン、API Gateway連携、コスト最適化まで、実践的なノウハウを解説します。

AWS Lambdaとは
サーバーレスコンピューティングの基本
AWS Lambdaは、イベント駆動型のサーバーレスコンピューティングサービスです。従来のサーバーベースのアプリケーションとの違いを整理します。
| 項目 | EC2(サーバーベース) | Lambda(サーバーレス) |
|---|---|---|
| サーバー管理 | 自分で管理 | AWSが管理 |
| スケーリング | 手動 or Auto Scaling | 自動(同時実行数に応じて) |
| 課金方式 | 稼働時間ベース | 実行回数+実行時間ベース |
| 起動時間 | 数分 | ミリ秒〜数秒 |
| メンテナンス | OS・ランタイムのパッチ適用 | 不要 |
| 最大実行時間 | 無制限 | 15分 |
Lambdaの最大の利点は、実行した分だけ課金されることです。リクエストがない時間帯はコストがゼロ。SES案件でのバッチ処理やAPI開発に最適です。
Lambdaが活躍するユースケース
- APIバックエンド: API Gateway + Lambda でRESTful APIを構築
- ファイル処理: S3にアップロードされた画像のリサイズ・変換
- データ変換: DynamoDBストリームのトリガーでデータを加工
- 定期バッチ: EventBridgeで定期実行するバッチ処理
- Webhook受信: 外部サービスからのWebhookを処理
- 認証処理: Cognitoと連携したカスタム認証
Lambda関数の作成:ハンズオン
Node.js での基本的なLambda関数
// index.mjs
export const handler = async (event) => {
console.log('Received event:', JSON.stringify(event, null, 2));
const name = event.queryStringParameters?.name || 'World';
return {
statusCode: 200,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: JSON.stringify({
message: `Hello, ${name}!`,
timestamp: new Date().toISOString(),
}),
};
};
Python での基本的なLambda関数
import json
from datetime import datetime
def handler(event, context):
print(f"Received event: {json.dumps(event)}")
name = event.get('queryStringParameters', {}).get('name', 'World')
return {
'statusCode': 200,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
'body': json.dumps({
'message': f'Hello, {name}!',
'timestamp': datetime.now().isoformat(),
}),
}
AWS CLIでのデプロイ
# 関数コードをZIPにパッケージ
zip function.zip index.mjs
# Lambda関数の作成
aws lambda create-function \
--function-name hello-world \
--runtime nodejs22.x \
--handler index.handler \
--role arn:aws:iam::123456789012:role/lambda-execution-role \
--zip-file fileb://function.zip
# 関数のテスト実行
aws lambda invoke \
--function-name hello-world \
--payload '{"queryStringParameters": {"name": "SES Engineer"}}' \
output.json
cat output.json
API Gateway + Lambda でREST API構築
基本的なCRUD APIの構築
SES案件で最も需要が高いのが、API Gateway + Lambda の組み合わせによるREST API構築です。
// api/users.mjs
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
import { DynamoDBDocumentClient, GetCommand, PutCommand, ScanCommand, DeleteCommand } from '@aws-sdk/lib-dynamodb';
const client = new DynamoDBClient({});
const dynamo = DynamoDBDocumentClient.from(client);
const TABLE_NAME = process.env.TABLE_NAME || 'Users';
export const handler = async (event) => {
const { httpMethod, pathParameters, body } = event;
try {
switch (httpMethod) {
case 'GET':
if (pathParameters?.id) {
// 単一ユーザー取得
const result = await dynamo.send(new GetCommand({
TableName: TABLE_NAME,
Key: { id: pathParameters.id },
}));
if (!result.Item) {
return response(404, { error: 'User not found' });
}
return response(200, result.Item);
}
// ユーザー一覧取得
const scanResult = await dynamo.send(new ScanCommand({
TableName: TABLE_NAME,
}));
return response(200, scanResult.Items);
case 'POST': {
const userData = JSON.parse(body);
const newUser = {
id: crypto.randomUUID(),
...userData,
createdAt: new Date().toISOString(),
};
await dynamo.send(new PutCommand({
TableName: TABLE_NAME,
Item: newUser,
}));
return response(201, newUser);
}
case 'DELETE': {
await dynamo.send(new DeleteCommand({
TableName: TABLE_NAME,
Key: { id: pathParameters.id },
}));
return response(204, null);
}
default:
return response(405, { error: 'Method not allowed' });
}
} catch (error) {
console.error('Error:', error);
return response(500, { error: 'Internal server error' });
}
};
function response(statusCode, body) {
return {
statusCode,
headers: {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
},
body: body ? JSON.stringify(body) : '',
};
}
SAMテンプレートでのインフラ定義
# template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Globals:
Function:
Timeout: 30
Runtime: nodejs22.x
MemorySize: 256
Resources:
UsersFunction:
Type: AWS::Serverless::Function
Properties:
Handler: api/users.handler
Environment:
Variables:
TABLE_NAME: !Ref UsersTable
Policies:
- DynamoDBCrudPolicy:
TableName: !Ref UsersTable
Events:
GetUsers:
Type: Api
Properties:
Path: /users
Method: get
GetUser:
Type: Api
Properties:
Path: /users/{id}
Method: get
CreateUser:
Type: Api
Properties:
Path: /users
Method: post
DeleteUser:
Type: Api
Properties:
Path: /users/{id}
Method: delete
UsersTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: Users
BillingMode: PAY_PER_REQUEST
AttributeDefinitions:
- AttributeName: id
AttributeType: S
KeySchema:
- AttributeName: id
KeyType: HASH
# SAM でビルド&デプロイ
sam build
sam deploy --guided
Lambda関数のベストプラクティス
1. コールドスタートの最適化
Lambda関数が一定時間呼び出されないと、実行環境が破棄されます(コールドスタート)。再起動時にはレイテンシが増加します。
// ❌ ハンドラ内で毎回初期化
export const handler = async (event) => {
const client = new DynamoDBClient({}); // 毎回作成される
// ...
};
// ✅ ハンドラ外で初期化(再利用される)
const client = new DynamoDBClient({});
const dynamo = DynamoDBDocumentClient.from(client);
export const handler = async (event) => {
// clientは再利用される
// ...
};
2. 環境変数の活用
// ハードコーディングしない
const TABLE_NAME = process.env.TABLE_NAME;
const STAGE = process.env.STAGE || 'dev';
const LOG_LEVEL = process.env.LOG_LEVEL || 'info';
3. エラーハンドリング
export const handler = async (event) => {
try {
const result = await processEvent(event);
return {
statusCode: 200,
body: JSON.stringify(result),
};
} catch (error) {
if (error instanceof ValidationError) {
return { statusCode: 400, body: JSON.stringify({ error: error.message }) };
}
if (error instanceof NotFoundError) {
return { statusCode: 404, body: JSON.stringify({ error: error.message }) };
}
console.error('Unexpected error:', error);
return { statusCode: 500, body: JSON.stringify({ error: 'Internal server error' }) };
}
};
4. ログ出力の構造化
// 構造化ログ
function log(level, message, data = {}) {
console.log(JSON.stringify({
level,
message,
...data,
timestamp: new Date().toISOString(),
requestId: process.env.AWS_REQUEST_ID,
}));
}
export const handler = async (event) => {
log('info', 'Request received', { path: event.path, method: event.httpMethod });
// ...
log('info', 'Request completed', { statusCode: 200, duration: elapsed });
};
Lambda のコスト最適化
料金体系の理解
Lambda の料金は以下の2つの要素で決まります。
- リクエスト数: 100万リクエストあたり $0.20
- 実行時間: GB-秒あたり $0.0000166667
メモリ設定の最適化
メモリを増やすと CPU パワーも比例して増加するため、メモリを増やした方が安くなる場合があります。
# AWS Lambda Power Tuning で最適なメモリを測定
# https://github.com/alexcasalboni/aws-lambda-power-tuning
# 128MB: 実行時間 3000ms → コスト 0.0000063
# 256MB: 実行時間 1500ms → コスト 0.0000063
# 512MB: 実行時間 800ms → コスト 0.0000067
# 1024MB: 実行時間 400ms → コスト 0.0000067
この例では128MBと256MBのコストが同じですが、レスポンスタイムは256MBの方が速いため、256MBが最適です。
Provisioned Concurrencyの判断基準
コールドスタートが許容できないAPIには、Provisioned Concurrencyを検討します。ただし、常時コストが発生するため、トラフィックパターンを分析してから導入しましょう。
# SAM テンプレートでの設定
AutoPublishAlias: live
ProvisionedConcurrencyConfig:
ProvisionedConcurrentExecutions: 5
SES現場でのLambda活用パターン
パターン1:画像処理パイプライン
// S3トリガーで画像をリサイズ
import sharp from 'sharp';
import { S3Client, GetObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
const s3 = new S3Client({});
export const handler = async (event) => {
const bucket = event.Records[0].s3.bucket.name;
const key = event.Records[0].s3.object.key;
// 元画像を取得
const original = await s3.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
const imageBuffer = Buffer.from(await original.Body.transformToByteArray());
// サムネイル生成
const thumbnail = await sharp(imageBuffer)
.resize(300, 300, { fit: 'cover' })
.webp({ quality: 80 })
.toBuffer();
// サムネイルを保存
await s3.send(new PutObjectCommand({
Bucket: bucket,
Key: `thumbnails/${key.replace(/\.[^.]+$/, '.webp')}`,
Body: thumbnail,
ContentType: 'image/webp',
}));
return { statusCode: 200, body: 'Thumbnail generated' };
};
パターン2:バッチデータ処理
import json
import boto3
from datetime import datetime, timedelta
dynamodb = boto3.resource('dynamodb')
ses = boto3.client('ses')
def handler(event, context):
"""毎日実行:期限切れデータの通知"""
table = dynamodb.Table('Contracts')
# 30日以内に期限切れになる契約を取得
threshold = (datetime.now() + timedelta(days=30)).isoformat()
response = table.scan(
FilterExpression='expiryDate <= :threshold AND #s = :active',
ExpressionAttributeNames={'#s': 'status'},
ExpressionAttributeValues={
':threshold': threshold,
':active': 'active',
},
)
expiring = response['Items']
if expiring:
# メール通知
ses.send_email(
Source='[email protected]',
Destination={'ToAddresses': ['[email protected]']},
Message={
'Subject': {'Data': f'契約期限アラート: {len(expiring)}件'},
'Body': {'Text': {'Data': format_report(expiring)}},
},
)
return {'processed': len(expiring)}
パターン3:Webhook受信処理
// Stripe Webhook処理
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
export const handler = async (event) => {
const sig = event.headers['stripe-signature'];
try {
const stripeEvent = stripe.webhooks.constructEvent(
event.body,
sig,
process.env.STRIPE_WEBHOOK_SECRET
);
switch (stripeEvent.type) {
case 'payment_intent.succeeded':
await handlePaymentSuccess(stripeEvent.data.object);
break;
case 'payment_intent.payment_failed':
await handlePaymentFailure(stripeEvent.data.object);
break;
}
return { statusCode: 200, body: 'OK' };
} catch (err) {
console.error('Webhook error:', err);
return { statusCode: 400, body: `Webhook Error: ${err.message}` };
}
};
ローカル開発環境の構築
SAM CLI でのローカルテスト
# ローカルでAPI起動
sam local start-api
# 単一関数のテスト
sam local invoke UsersFunction --event events/get-users.json
# テストイベントの生成
sam local generate-event apigateway aws-proxy > events/get-users.json
Docker を使ったテスト
# Lambda互換のDockerイメージでテスト
docker run --rm -v $(pwd):/var/task:ro \
public.ecr.aws/lambda/nodejs:22 \
index.handler '{"httpMethod":"GET"}'
Lambda のセキュリティベストプラクティス
IAMロールの最小権限原則
# ❌ 広すぎる権限
Policies:
- AmazonDynamoDBFullAccess
# ✅ 必要最小限の権限
Policies:
- Statement:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:Query
Resource: !GetAtt UsersTable.Arn
機密情報の管理
// ❌ 環境変数に直接書かない
const API_KEY = process.env.API_KEY; // プレーンテキスト
// ✅ Secrets Manager から取得
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
const sm = new SecretsManagerClient({});
let cachedSecret;
async function getSecret() {
if (!cachedSecret) {
const result = await sm.send(new GetSecretValueCommand({
SecretId: process.env.SECRET_ARN,
}));
cachedSecret = JSON.parse(result.SecretString);
}
return cachedSecret;
}
SESエンジニアとしてのLambdaスキルの価値
SES市場でのサーバーレス需要
2026年のSES市場では、サーバーレスアーキテクチャを扱えるエンジニアの需要が急増しています。
- 単価アップ: Lambda経験者は月単価が5-10万円高い傾向
- 案件の幅: バックエンドだけでなくインフラ・DevOps案件にも参画可能
- キャリアパス: クラウドアーキテクトへのステップアップ
関連AWS認定資格
Lambdaの知識は以下のAWS認定資格で特に重要です。
- Solutions Architect Associate: サーバーレスアーキテクチャの設計
- Developer Associate: Lambda関数の実装とデバッグ
- DevOps Engineer Professional: CI/CDパイプラインとの統合
まとめ
AWS Lambdaは、SESエンジニアが次のキャリアステップに進むための最も費用対効果の高いスキルの一つです。
学習ロードマップ
- 基礎: Lambda関数の作成・デプロイ(本記事)
- API構築: API Gateway + Lambda でREST API
- データ連携: DynamoDB・S3との統合
- 運用: CloudWatch Logs・X-Rayでの監視
- IaC: SAM / CDK でのインフラコード化
関連記事
AWS 完全攻略シリーズ一覧はこちら