𝕏 f B! L
案件・求人数 12,345
案件を探す(準備中) エージェントを探す(準備中) お役立ち情報 ログイン
案件・求人数 12,345
AWS Lambda入門|サーバーレスでSESエンジニアの価値を上げる

AWS Lambda入門|サーバーレスでSESエンジニアの価値を上げる

AWSLambdaサーバーレスSESAWS Lambda 入門
目次

📚 この記事は「AWS 完全攻略シリーズ」の Episode 12 です。

AWS Lambdaは、サーバーを管理することなくコードを実行できるサーバーレスコンピューティングサービスです。SESエンジニアにとって、Lambda を理解していることは市場価値を大きく高める武器になります。

本記事では、AWS Lambdaの基礎概念から実際のSES現場で役立つ実装パターン、API Gateway連携、コスト最適化まで、実践的なノウハウを解説します。

AWS Lambdaサーバーレスアーキテクチャの全体像

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つの要素で決まります。

  1. リクエスト数: 100万リクエストあたり $0.20
  2. 実行時間: 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エンジニアが次のキャリアステップに進むための最も費用対効果の高いスキルの一つです。

学習ロードマップ

  1. 基礎: Lambda関数の作成・デプロイ(本記事)
  2. API構築: API Gateway + Lambda でREST API
  3. データ連携: DynamoDB・S3との統合
  4. 運用: CloudWatch Logs・X-Rayでの監視
  5. IaC: SAM / CDK でのインフラコード化

関連記事


AWS 完全攻略シリーズ一覧はこちら

SES案件をお探しですか?

SES記事をもっと読む →
🏗️

SES BASE 編集長

SES業界歴10年以上のメンバーが在籍する編集チーム。SES企業での営業・エンジニア経験、フリーランス独立経験を持つメンバーが、業界のリアルな情報をお届けします。

📊 業界データに基づく記事制作 🔍 IPA・経済産業省データ参照 💼 SES実務経験者が執筆・監修