AWS Lambdaで始めるサーバーレス開発:実務で必要な基礎知識

本記事では、AWS Lambdaを使ったサーバーレスアーキテクチャの基本から、実際のプロジェクトで即活用できる実装パターンまでを解説します。初心者がつまずきやすいポイントと解決策も含めた、実用的なガイドです。

サーバーレスアーキテクチャとは何か

サーバーレスアーキテクチャは、インフラストラクチャの管理をクラウドプロバイダに完全に委譲し、開発者はビジネスロジックに集中する設計パターンです。名前に「サーバーレス」とありますが、実際にはサーバーは存在します。ただし、その管理と運用がプロバイダ側の責任になるため、開発チームはサーバーの構築・スケーリング・保守から解放されます。

実務では、サーバーレスを選ぶ主な理由は以下の通りです:

  • コスト削減:実行時間に応じた従量課金。アイドル時間が不要。
  • 自動スケーリング:トラフィック変動に自動対応。コード記述だけで対応完了。
  • デプロイ簡素化:関数コードをアップロードするだけ。インフラ管理が不要。
  • 運用負荷低減:パッチ管理、セキュリティ更新などはAWS側が処理。

一方、使うべきでない場面もあります。長時間実行されるバッチ処理や、常時稼働が必要な高トラフィックシステム、複雑なステートフルな処理には不向きです。

AWS Lambdaの基本概念

Lambdaとは

AWS Lambdaはイベント駆動型のサーバーレスコンピューティングサービスです。以下のトリガーに応じて自動的に関数が実行されます:

  • APIリクエスト(API Gateway経由)
  • S3ファイルアップロード
  • DynamoDBストリーム変更
  • CloudWatch Events(スケジュール実行)
  • SNS、SQSメッセージ受信

関数の実行モデル

Lambdaは「実行環境」と呼ばれるコンテナ内で関数を実行します。初回実行時は環境の起動に遅延(コールドスタート)が生じます。その後、一定時間(通常15分)は環境が保持されるため、次のリクエストは高速に実行されます。


flowchart TD
    A[イベント発火] --> B{実行環境は存在?}
    B -->|No| C[コンテナ起動
100-1000ms] B -->|Yes| D[ウォーム実行] C --> E[関数コード実行] D --> E E --> F[レスポンス返却] F --> G[環境アイドル
15分保持] G --> H{新たなイベント?} H -->|Yes| D H -->|No| I[環境終了]

初めてのLambda関数を作成する

開発環境のセットアップ

以下の環境で動作確認済みです:

  • AWS アカウント(無料枠で試験可能)
  • Node.js 18.x 以上
  • AWS CLI v2
  • IAM権限:lambda:CreateFunction、lambda:InvokeFunction

AWS CLIをインストール後、認証情報を設定します:

aws configure
# 対話形式でアクセスキーID、シークレットアクセスキー、リージョン(例:ap-northeast-1)を入力

シンプルなHello World関数

まず、最小限の関数を作成して動作を確認します。

// index.js
exports.handler = async (event) => {
    console.log('受け取ったイベント:', JSON.stringify(event));
    
    return {
        statusCode: 200,
        body: JSON.stringify({
            message: 'Hello from Lambda!',
            timestamp: new Date().toISOString(),
            input: event
        })
    };
};

このコードの重要なポイント:

  • handler関数がエントリーポイント。Lambdaコンソールで「index.handler」として指定。
  • eventパラメータにトリガーからのデータが渡される。
  • async/awaitでI/O処理を効率的に扱える。
  • 戻り値は自動的にJSON形式で返却される。

AWSコンソールでのデプロイ

以下の手順でLambda関数を作成します:

  1. AWSマネジメントコンソール → Lambda → 「関数の作成」
  2. 関数名:HelloWorld、ランタイム:Node.js 18.xを選択
  3. 実行ロール:「新しいロールを作成」選択
  4. コードエディタに上記コードを貼り付け
  5. 「Deploy」をクリック
  6. 「Test」タブで、テストイベント名をMyTestとして、次のJSONを入力:
{
  "queryStringParameters": {
    "name": "World"
  }
}

「Invoke」をクリックすると、関数が実行され、結果が表示されます。

実務で使える5つの実装パターン

パターン1:REST APIのバックエンド

API Gatewayと組み合わせることで、APIサーバーを実装できます。

// APIリクエストを処理する関数
exports.handler = async (event) => {
    const httpMethod = event.httpMethod;
    const path = event.path;
    
    console.log(`[${httpMethod}] ${path}`);
    
    // ルーティング処理
    if (path === '/api/users' && httpMethod === 'GET') {
        return {
            statusCode: 200,
            body: JSON.stringify([
                { id: 1, name: 'User 1' },
                { id: 2, name: 'User 2' }
            ])
        };
    }
    
    if (path === '/api/users' && httpMethod === 'POST') {
        const body = JSON.parse(event.body);
        // DynamoDBに保存するロジック
        return {
            statusCode: 201,
            body: JSON.stringify({ id: 3, ...body })
        };
    }
    
    return {
        statusCode: 404,
        body: JSON.stringify({ error: 'Not Found' })
    };
};

パターン2:スケジュール実行(バッチ処理)

CloudWatch Eventsで定期実行をトリガーします。毎日の集計処理やキャッシュ更新に使用できます。

// 毎日午前0時に実行される関数
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient();

exports.handler = async (event) => {
    console.log('バッチ処理開始:', new Date().toISOString());
    
    try {
        // DynamoDBからデータ取得
        const params = {
            TableName: 'DailyLogs',
            FilterExpression: 'createdDate = :today',
            ExpressionAttributeValues: {
                ':today': new Date().toISOString().split('T')[0]
            }
        };
        
        const result = await dynamodb.scan(params).promise();
        
        // 集計処理
        const summary = {
            totalRecords: result.Items.length,
            timestamp: new Date().toISOString()
        };
        
        console.log('集計結果:', summary);
        
        return {
            statusCode: 200,
            body: JSON.stringify(summary)
        };
    } catch (error) {
        console.error('エラー:', error);
        return {
            statusCode: 500,
            body: JSON.stringify({ error: 'Batch processing failed' })
        };
    }
};

CloudWatch EventsのルールはTerraformで定義すると管理しやすいです:

resource "aws_cloudwatch_event_rule" "daily_batch" {
  name                = "daily-batch-rule"
  schedule_expression = "cron(0 0 * * ? *)"  # 毎日UTC 0時
}

resource "aws_cloudwatch_event_target" "lambda" {
  rule      = aws_cloudwatch_event_rule.daily_batch.name
  target_id = "DailyBatchLambda"
  arn       = aws_lambda_function.batch.arn
}

パターン3:S3イベント処理(ファイルアップロード検知)

ユーザーが画像をアップロードした時点で、自動的に画像の加工や検証を実行できます。

// S3にファイルがアップロードされたときのトリガー
const AWS = require('aws-sdk');
const s3 = new AWS.S3();

exports.handler = async (event) => {
    console.log('S3イベント受取:', JSON.stringify(event));
    
    // S3イベントからバケット名とキーを抽出
    const bucket = event.Records[0].s3.bucket.name;
    const key = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, ' '));
    
    try {
        // S3からファイルを取得
        const params = {
            Bucket: bucket,
            Key: key
        };
        
        const data = await s3.getObject(params).promise();
        console.log(`ファイル取得成功: ${key}, サイズ: ${data.ContentLength} bytes`);
        
        // ここでファイル処理(例:画像リサイズ、テキスト抽出など)
        const result = {
            bucket: bucket,
            key: key,
            size: data.ContentLength,
            contentType: data.ContentType,
            processedAt: new Date().toISOString()
        };
        
        return {
            statusCode: 200,
            body: JSON.stringify(result)
        };
    } catch (error) {
        console.error('エラー:', error);
        return {
            statusCode: 500,
            body: JSON.stringify({ error: 'File processing failed' })
        };
    }
};

パターン4:非同期タスクキュー処理(SQS連携)

重い処理を別のLambdaに非同期で委譲し、レスポンス時間を短縮します。

// メインAPI:タスクをSQSキューに投入
const AWS = require('aws-sdk');
const sqs = new AWS.SQS();

exports.handler = async (event) => {
    const body = JSON.parse(event.body);
    
    const params = {
        QueueUrl: process.env.QUEUE_URL,
        MessageBody: JSON.stringify({
            userId: body.userId,
            action: 'send-email',
            timestamp: new Date().toISOString()
        })
    };
    
    try {
        await sqs.sendMessage(params).promise();
        return {
            statusCode: 202,
            body: JSON.stringify({ message: 'Task queued', taskId: body.userId })
        };
    } catch (error) {
        console.error('Queue error:', error);
        return {
            statusCode: 500,
            body: JSON.stringify({ error: 'Failed to queue task' })
        };
    }
};
// ワーカーLambda:SQSメッセージを処理
const AWS = require('aws-sdk');
const ses = new AWS.SES();

exports.handler = async (event) => {
    for (const record of event.Records) {
        const message = JSON.parse(record.body);
        
        console.log('処理中:', message);
        
        // メール送信処理(重い処理の例)
        const emailParams = {
            Source: 'noreply@example.com',
            Destination: { ToAddresses: [`user${message.userId}@example.com`] },
            Message: {
                Subject: { Data: 'Your task completed' },
                Body: { Text: { Data: 'Processing finished!' } }
            }
        };
        
        try {
            await ses.sendEmail(emailParams).promise();
            console.log(`Email sent for user ${message.userId}`);
        } catch (error) {
            console.error('Email error:', error);
        }
    }
    
    return { batchItemFailures: [] };
};

パターン5:DynamoDBストリーム処理(リアルタイムトリガー)

データベースの変更をリアルタイムで検知し、他のシステムへの同期やログ記録を実施できます。

// DynamoDBテーブルの変更を検知して処理
exports.handler = async (event) => {
    console.log('DynamoDBストリームイベント受取');
    
    for (const record of event.Records) {
        const eventName = record.eventName; // INSERT, MODIFY, REMOVE
        const dynamodb = record.dynamodb;
        
        if (eventName === 'INSERT') {
            console.log('新規作成:', JSON.stringify(dynamodb.NewImage));
            // 外部APIに通知、キャッシュ更新など
        } else if (eventName === 'MODIFY') {
            console.log('更新:', JSON.stringify(dynamodb.NewImage));
            // 差分抽出、検証ロジックなど
        } else if (eventName === 'REMOVE') {
            console.log('削除:', JSON.stringify(dynamodb.OldImage));
            // クリーンアップ処理など
        }
    }
    
    return { statusCode: 200 };
};

よくあるハマりポイントと解決策

コールドスタートの遅延が問題になる場合

マイクロ秒が重要な取引システムでは、コールドスタートの1〜2秒の遅延が許容できない場合があります。以下の対策があります:

  • Provisioned Concurrency:事前に実行環境を確保。コスト増加が課題。
  • メモリ増加:メモリを大きく設定すると、CPU配分も増加し、コールドスタートが短縮。
  • コード最適化:外部ライブラリの遅延ロード、Node.jsのrequire()を関数内で実行。

タイムアウトエラーが発生する

デフォルトのタイムアウトは3秒。長時間実行される処理は以下対応が必要です:

  • Lambda設定の「一般設定」でタイムアウト時間を延長(最大15分)。
  • 外部API呼び出しがハング状態の場合、Axios等のHTTPクライアントで明示的にtimeoutを設定:
const axios = require('axios');

const instance = axios.create({
  timeout: 5000  // 5秒でタイムアウト
});

try {
  const response = await instance.get('https://api.example.com/data');
} catch (error) {
  if (error.code === 'ECONNABORTED') {
    console.error('リクエストタイムアウト');
  }
}

IAM権限不足で動作しない

Lambdaは実行ロール(IAM Role)に付与された権限のみ使用できます。DynamoDB、S3、SESなどへのアクセスが拒否される場合、実行ロールにポリシーを追加する必要があります。

筆者の実務経験上、以下ポリシーを最小限構成として推奨します:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents"
      ],
      "Resource": "arn:aws:logs:*:*:*"
    },
    {
      "Effect": "Allow",
      "Action": [
        "dynamodb:GetItem",
        "dynamodb:PutItem",
        "dynamodb:Query"
      ],
      "Resource": "arn:aws:dynamodb:ap-northeast-1:123456789012:table/MyTable"
    },
    {
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject"
      ],
      "Resource": "arn:aws:s3:::my-bucket/*"
    }
  ]
}

環境変数の管理が煩雑

Lambdaコンソールで環境変数を設定できますが、大規模環境では管理が困難になります。AWS Secrets Managerを使用する方法を推奨します:

const AWS = require('aws-sdk');
const secretsManager = new AWS.SecretsManager();

let cachedSecret = null;

exports.handler = async (event) => {
    // 2回目以降はキャッシュから取得
    if (!cachedSecret) {
        const params = {
            SecretId: 'prod/database/password'
        };
        const result = await secretsManager.getSecretValue(params).promise();
        cachedSecret = JSON.parse(result.SecretString);
    }
    
    const dbPassword = cachedSecret.password;
    // その後の処理...
};

ローカル開発とテストが困難

AWS SAM(Serverless Application Model)を使うことで、ローカル環境でLambdaと同じ環境を再現できます:

pip install aws-sam-cli
sam init  # テンプレート生成
sam build
sam local start-api  # ローカルAPIサーバー起動

または、AWS Toolkit for Visual Studio Codeを使うと、IDEから直接Lambdaのデバッグが可能です。

パフォーマンスとコストの最適化

メモリ設定とコストの関係

Lambdaの料金はメモリ設定と実行時間で決まります。高いメモリを設定すると、CPUも同時に増加し、実行時間が短縮される傾向があります。


graph TD
    A[メモリ設定: 128MB] -->|実行時間: 5秒| B[料金: 低]
    C[メモリ設定: 1024MB] -->|実行時間: 0.5秒| D[料金: 変動あり]
    
    E["推奨: 実装→テスト→最適なメモリを探索"]
  

CloudWatch Logsで実行時間を確認し、Cost Calculatorで料金試算をしながら、最適なメモリサイズを見つけることが重要です。

コードの効率化

以下は実務で有効な最適化手法です:

  • 接続の再利用:グローバルスコープでDB接続やHTTPクライアントを定義。複数実行間で再利用される。
  • バンドル最小化:不要なライブラリを除去。esbuildなどでツリーシェーキングを実施。
  • 非同期処理:複数の並列処理をPromise.all()で統合。
// グローバル接続の再利用例
const AWS = require('aws-sdk');
const dynamodb = new AWS.DynamoDB.DocumentClient();  // 関数外で定義

exports.handler = async (event) => {
    // 複数回の呼び出しで同じコネクションを再利用
    const result = await dynamodb.get({
        TableName: 'Users',
        Key: { userId: event.userId }
    }).promise();
    
    return result.Item;
};

デプロイメント戦略

Infrastructure as Code(IaC)での管理

AWS CDK、TerraformServerless Frameworkなど、複数の選択肢があります。筆者の推奨はTerraformです。理由は、AWSだけでなく複数クラウドに対応でき、チーム全体で学習資産が共有できるためです。

resource "aws_lambda_function" "api_handler" {
  filename      = "lambda.zip"
  function_name = "api-handler"
  role          = aws_iam_role.lambda_role.arn
  handler       = "index.handler"
  runtime       = "nodejs18.x"
  
  timeout = 30
  memory_size = 512
  
  environment {
    variables = {
      TABLE_NAME = aws_dynamodb_table.users.name
      QUEUE_URL  = aws_sqs_queue.tasks.url
    }
  }
}

ステージング環境の構築

本番環境への影響を最小化するため、ステージング環境を用意します。Terraformでvariable.tfを分けることで、環境ごとの設定を管理できます。

variable "environment" {
  type = string
  default = "staging"
}

resource "aws_lambda_function" "api_handler" {
  function_name = "${var.environment}-api-handler"
  # その他の設定...
}

監視・ログ・アラート

CloudWatch Logsの活用

すべてのLambda実行ログは自動的にCloudWatch Logsに送信されます。console.log()の出力がリアルタイムで閲覧可能です。

本番環境ではStructured Loggingを推奨します:

// JSON形式でログを出力。検索・フィルタリングが容易
const log = (level, message, metadata = {}) => {
    console.log(JSON.stringify({
        timestamp: new Date().toISOString(),
        level: level,
        message: message,
        ...metadata
    }));
};

exports.handler = async (event) => {
    try {
        log('INFO', 'Request received', { userId: event.userId });
        // 処理...
        log('INFO', 'Request completed', { statusCode: 200 });
    } catch (error) {
        log('ERROR', 'Processing failed', { error: error.message, stack: error.stack });
    }
};

CloudWatch Alarmの設定

エラー率やタイムアウト増加を自動検知し、SNS経由でアラート送信できます:

resource "aws_cloudwatch_metric_alarm" "lambda_errors" {
  alarm_name          = "api-handler-errors"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 1
  metric_name         = "Errors"
  namespace           = "AWS/Lambda"
  period              = 300
  statistic           = "Sum"
  threshold           = 5
  alarm_actions       = [aws_sns_topic.alerts.arn]
  
  dimensions = {
    FunctionName = aws_lambda_function.api_handler.function_name
  }
}

X-Ray による分散トレース

複数のAWSサービス間でのレイテンシボトルネック分析に活用できます:

const AWSXRay = require('aws-xray-sdk-core');
const AWS = require('aws-sdk');

// DynamoDBクライアントをX-Rayでラップ
const dynamodb = AWSXRay.captureClientExceptions(
  new AWS.DynamoDB.DocumentClient()
);

exports.handler = async (event) => {
    const segment = AWSXRay.getSegment();
    const subsegment = segment.addNewSubsegment('get-user');
    
    try {
        const result = await dynamodb.get({
            TableName: 'Users',
            Key: { userId: event.userId }
        }).promise();
        
        subsegment.close();
        return result.Item;
    } catch (error) {
        subsegment.addError(error);
        throw error;
    }
};

セキュリティベストプラクティス

最小権限の原則

IAMロールには、その関数が必要とする最小限の権限のみ付与します。「*」でのワイルドカードはセキュリティリスク。特定のリソースARNを明記してください。

環境変数と機密情報の保護

APIキーやDB認証情報は環境変数ではなくAWS Secrets Managerに保存。環境変数として公開されると、ログやCI/CDパイプラインで露出するリスクがあります。

VPC設定

Lambda関数をVPC内で実行することで、RDSなどプライベートリソースへのアクセスが可能になります。ただし、VPC外のインターネットアクセスにはNAT Gatewayが必要になり、コスト増加につながります。

resource "aws_lambda_function" "api_handler" {
  vpc_config {
    subnet_ids         = [aws_subnet.private.id]
    security_group_ids = [aws_security_group.lambda.id]
  }
}

よくある質問

A:最大15分(900秒)です。それ以上の長時間実行処理はECS、EC2、Batchなどを使用してください。

K
AWS・Python・生成AIを専門とするソフトウェアエンジニア。AI・クラウド・開発ワークフローの実践ガイドを執筆しています。詳しく見る →