0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Lambdaのローカル環境構築と開発体験・実行環境の分離設計:前編

0
Last updated at Posted at 2026-05-04

はじめに

AWSのサーバーレスアーキテクチャを中心に設計・構築を担当するエンジニアです。これまで「ECS上でコンテナを動かす」スタイルで開発体験を最適化してきましたが、Lambda中心のアーキテクチャに移行する際、多くの開発者が「ローカルでいかにLambda環境を再現するか」という問いに嵌まりがちです。

私自身も当初は「ホットリロードが効かない」「イベントの再現が面倒」といった課題に対し、ローカル再現に心血を注いできました。しかし、本検証を通じて至った結論は、「ローカルでの再現」を目指すこと自体がアンチパターンであるということです。

この記事では、サーバーレス開発における「開発・検証・本番」の境界をどう設計し、それぞれのフェーズに最適な手段をどう割り当てるべきか、という問いに対する一つの回答を提示します。

レポジトリ:

後編

対象読者

  • AWS LambdaやAPI Gatewayを使ったサーバーレス開発に携わっているエンジニア
  • ローカルでのLambda再現ツール(SAM CLI、LocalStack、serverless-offline など)の導入を検討している方
  • サーバーレス開発の「DXをどう改善するか」という問いに向き合っているチームリード・スタッフエンジニア

用いた技術スタック

本記事の検証は以下の構成で実施しました。

  • 言語: Python 3.12
  • 開発用フレームワーク: Flask 3.x
  • テスト: pytest + moto(AWSサービスのモック)
  • デプロイ/検証ツール: AWS SAM CLI、Serverless Framework v3
  • IaC: SAM template.yaml
  • ローカルAWSエミュレーション: LocalStack(補助利用)

サーバーレス開発の核心:環境の「再現」ではなく「分離」

サーバーレス開発において重要なのは、Lambdaの実行環境をローカルで再現することではありません。開発・検証・本番を明確に分離し、それぞれに最適な手段を割り当てることです。

  1. 開発(Development): Flaskによる高速な試行錯誤。ビジネスロジックの構築に集中し、フィードバックループを最短化する。
  2. 検証(Validation): eventベースのテスト(pytest)。Lambdaハンドラに対する入力(Event)と出力(Response)の整合性を型とテストで担保する。
  3. 本番(Production): クラウド環境での実行確認。IaC(SAM/Terraform)を通じたデプロイと、実際のリソース間連携の確認。

本検証では、この構成が最も合理的であるという結論に至りました。

調査の目的と仮説

今回検証するのは以下の3点です。

  1. 試行錯誤の高速化: 開発フェーズにおいて、Lambdaの制約(デプロイ待ちやコンテナ起動待ち)に縛られず、ECS開発のような即時フィードバックを得られるか。
  2. 整合性の設計: 開発用のFlask実行と、本番のLambda(イベント駆動)の境界をどう設計すれば、ロジックの共通化と挙動の一致が成立するか。
  3. 検証コストの最適化: どのフェーズでどのレベルのテストを行うのが、トータルでの品質向上とスピードに寄与するか。

スタッフエンジニアが向き合うべき問題は「どのツールが本番に近いか」ではなく「どこで何を担保するか」という責務の割り当てです。

評価対象:3つのアプローチ

同一のビジネスロジックに対して、役割の異なる3つの環境を比較・検証しました。

1. Flaskベース(開発特化)

Flask + pytest を組み合わせ、API Gatewayのイベント構造をPython dictで模倣してhandlerに渡す構成です。**「開発」**フェーズにおける圧倒的な生産性を担います。

Flaskはあくまで「handlerをHTTP経由でキックするための薄いアダプタ」として機能します。api/adapters/http_flask.py がイベント変換を担い、ビジネスロジックはhandlerおよびusecase層に閉じ込めることで、Flaskに依存しないコードを実現します。

# api/adapters/http_flask.py(概念)
@app.route("/calc", methods=["POST"])
def calc():
    event = build_apigw_event(request)   # FlaskのRequestをLambdaイベントへ変換
    result = handler.lambda_handler(event, None)
    return response_mapper.to_flask_response(result)

2. AWS SAM(検証・デプロイ)

Dockerコンテナ上でLambdaをエミュレートします。**「本番直前の検証」**およびデプロイツールとして機能します。sam local invoke によりコンテナを起動してhandlerを実行しますが、起動オーバーヘッドが約400ms〜数秒あるため、開発サイクルには向きません。IaCとしての template.yaml 管理と、マネージドサービスの権限設定(IAMロール等)を一元管理できる点が強みです。

3. Serverless Framework(代替案)

serverless-offline を用いた環境。Node.jsエコシステムでの開発における選択肢として比較対象としました。起動は約30msとSAMより軽快ですが、Pythonプロジェクトではランタイムの再現性がやや劣り、設定の複雑度が増す傾向があります。

対象アーキテクチャ

現代的なサーバーレスアプリケーションで頻出する、結合度の高いパターンを検証対象としました。

  • API Gateway → Lambda → DB(CRUD)
  • API Gateway → Lambda → S3(ファイル操作)
  • SNS / SQS コンシューマ(非同期連携)
  • Lambda-to-Lambda(同期呼び出し)

評価結果:フェーズごとの最適解

フェーズ 推奨手段 理由 役割
開発 Flask (Local) ホットリロードが「極速」。思考を止めない。 ロジックの実装、DB連携の試行錯誤
検証 eventテスト スキーマの一致を自動で担保。副作用の検査。 品質保証、リグレッション防止
本番 AWS実環境 認証・認可、ネットワーク、権限の最終確認。 最終的な動作確認

詳細比較

評価軸 Flask (Local) SAM Serverless (v3)
開発体験(スピード) 優 (1ms) 劣 (~400ms) 良 (~30ms)
クラウド再現性
テスト容易性

実験結果:なぜ「分離」がうまくいくのか

すべての環境を通して同一のテストを実行した結果、103/103 passed という結果を得ました。

注目すべきは、実行速度の圧倒的な差です。Flaskは実行オーバーヘッドがほぼゼロであり、開発者のフィードバックループをミリ秒単位で回せます。一方、SAMは実行のたびにコンテナ起動を伴うため、実装初期の細かな修正には向きません。

「クラウド再現性が低いFlaskで開発して大丈夫か?」という懸念に対し、本検証は**「境界設計さえ正しければ、開発は最速の手段でよい」**という答えを出しました。

テスト戦略の詳細

pytestのテストは、実際のAWSイベント構造を模したJSONフィクスチャを用いてhandlerを直接呼び出します。これにより、FlaskやHTTPレイヤーを一切介さずにhandlerの振る舞いを検証できます。

# api/tests/test_calc.py(例)
import json, pytest
from api.functions.calc.handler import lambda_handler

@pytest.fixture
def apigw_event():
    with open("api/tests/events/apigw/calc.json") as f:
        return json.load(f)

def test_calc_success(apigw_event):
    response = lambda_handler(apigw_event, None)
    assert response["statusCode"] == 200
    body = json.loads(response["body"])
    assert "result" in body

フィクスチャとなるJSONは api/tests/events/ 以下にイベントソース別に格納しています。

api/tests/events/
├── apigw/         # API Gateway プロキシイベント
│   ├── calc.json
│   └── sqs_enqueue.json
├── sns/           # SNS通知イベント
│   └── notification.json
└── sqs/           # SQSメッセージイベント
    └── sample.json

この構成により、「どのイベント構造でhandlerが呼ばれるか」をコードとして明示し、仕様変更時の影響範囲を一目で把握できます。

AWSサービスのモック(moto)

DynamoDB、S3、SQS、SNSなどのAWSサービスはmotoライブラリでモックします。これによりLocalStackやAWS実環境への依存なしに、高速・安定したテストが実現できます。

import boto3
from moto import mock_aws

@mock_aws
def test_s3_upload(apigw_event):
    # motoがS3の仮想バケットを提供する
    s3 = boto3.client("s3", region_name="us-east-1")
    s3.create_bucket(Bucket="test-bucket")

    response = lambda_handler(apigw_event, None)
    assert response["statusCode"] == 200

分析:成功の鍵は「境界の設計」

「開発・検証・本番」を分離可能にするのは、Lambdaハンドラを「薄いディスパッチャー」に保つという設計原則です。

# 1. 開発:Flaskが「イベント」を作成してハンドラを呼ぶ
# 2. 検証:pytestが「イベントJSON」を読み込んでハンドラを呼ぶ
# 3. 本番:AWSが「リアルなイベント」をハンドラに送る

ビジネスロジックを execute(event) やusecase層に完全に分離することで、呼び出し元がFlaskであれAWSであれ、コアの動作は同一になります。Flaskはあくまで「handlerをHTTP経由でキックするための開発用アダプタ」であり、そこには一切のロジックを含めないことが重要です。

ハンドラの実装例

実際のhandlerは以下のような構造になります。ビジネスロジックは execute 関数に集約し、handlerはeventの受け渡しのみを行います。

# api/functions/calc/handler.py
from __future__ import annotations
import json

def execute(event: dict) -> dict:
    body = json.loads(event.get("body") or "{}")
    a = int(body["a"])
    b = int(body["b"])
    return {"result": a + b}

def lambda_handler(event: dict, context) -> dict:
    try:
        data = execute(event)
        return {"statusCode": 200, "body": json.dumps(data)}
    except (KeyError, ValueError) as e:
        return {"statusCode": 400, "body": json.dumps({"error": str(e)})}

lambda_handler はeventを受け取り execute に委譲するだけです。この設計により、テストは execute 単体でも lambda_handler 全体でも書けます。

ディレクトリ構成と依存の方向

api/
├── main.py                   # Flaskアプリのエントリポイント
├── adapters/
│   ├── event_builder.py      # FlaskのRequestをLambdaイベントへ変換
│   ├── http_flask.py         # FlaskのルーティングとhandlerのDI(ここだけFlaskを知る)
│   └── response_mapper.py    # LambdaレスポンスをFlask Responseへ変換
└── functions/
    └── calc/
        └── handler.py        # ビジネスロジック(Flaskを一切知らない)

依存の方向は adapters → functions の一方向のみです。functions 配下のコードはFlaskもHTTPも知りません。これにより、ローカル開発用のFlaskアダプタをすべて取り除いても、handlerのコードは無変更でAWS上で動作します。

陥りがちなアンチパターン

本検証を通じて見えてきた、サーバーレス開発でよくある失敗パターンを整理します。

アンチパターン1:handlerにビジネスロジックを書く

# 悪い例:handler内にロジックが混在している
def lambda_handler(event, context):
    body = json.loads(event["body"])
    # ここに複雑な計算ロジックが直接書かれている
    result = complex_calculation(body["data"])
    conn = get_db_connection()
    conn.execute("INSERT INTO ...")
    return {"statusCode": 200, "body": json.dumps(result)}

このパターンでは、handlerを呼び出さずにロジックを単体テストできません。execute や usecase関数へのロジック分離が必須です。

アンチパターン2:FlaskのRouteにロジックを書く

# 悪い例:Flaskルートにビジネスロジックが漏れている
@app.route("/calc", methods=["POST"])
def calc():
    data = request.json
    result = data["a"] + data["b"]   # ←ロジックがFlask側に
    return jsonify({"result": result})

これではFlaskを起動しないとテストできず、Lambda環境との乖離が生まれます。FlaskルートはhandlerのDIに徹し、ロジックは持たないことが原則です。

アンチパターン3:「本番に近いローカル環境」を追い求める

LocalStackのすべてのサービスを立ち上げ、SAMのコンテナを常時起動し、環境変数を細かく合わせる…この努力は「環境の再現」に消費される工数です。motoとeventフィクスチャによる検証で代替できる部分が多く、本番固有の挙動確認(IAM、VPC、サービス間の実権限)はクラウド上で確認するほうが速く確実です。

結論:サーバーレス開発の「標準構成」

本検証により、以下の構成が最も合理的であるという結論に至りました。

  1. 開発はFlaskで「速さ」を追求する: ローカルDB統合と極速リロードを享受し、ビジネスロジックを完成させる。
  2. 検証はEventテストで「正しさ」を担保する: 実際のAWSイベントを模したJSON(Fixture)によるテストを徹底し、境界の整合性を自動化する。
  3. 本番はクラウドで「稼働」を確認する: インフラ構成やIAM権限など、マネージドサービス特有の挙動は本物の環境で最終確認する。

「ローカルでLambdaを完コピしようとする努力」を、「各フェーズの責務分離と境界設計」へとシフトすること。これこそが、サーバーレス開発における生産性と品質を両立させる正解です。

本記事のまとめ

問い 答え
ローカルでLambdaを完全再現すべきか? No。 再現性の追求は投資対効果が低い
開発中はどの環境を使うべきか? Flask。 速度最優先、ロジックだけに集中する
テストはどう書くべきか? eventドリブン。 JSONフィクスチャ+motoで境界を担保する
本番固有の挙動はどこで確認するか? クラウド本番(またはステージング)環境で確認する
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?