メインコンテンツに移動
デベロッパーのためのクラウド活用方法

AWS Lambda 関数の動きを楽に確認しちゃおう ! ~ AWS CDK で試して学ぶ サーバーレス開発の達人の技  #2 ~

2025-01-06 | Author : 高野 賢司

はじめに

みなさんこんにちは ! AWS の 高野 賢司 です。最近、子どものために買った電子ピアノで自分も練習しはじめました。2025 年の目標は「毎日 15 分でいいから練習を継続する」です。なお音楽センスはまったくないので、弾けるようになることはつゆほども期待していません。

シリーズ『AWS CDK で試して学ぶ サーバーレス開発の達人の技』では、インフラからアプリまでさまざまな領域の「達人」にインタビューして、開発高速化のための「達人の技」を AWS CDK を使って試していきます。

ではさっそく、今回のトピックを紹介します。

💭 AWS Lambda 関数の動きを確認するのに、デプロイするのがちょっと面倒・・・

前回の記事 では、AWS CDK を使用して Lambda 関数をサクッとデプロイしました。ホットスワップ機能を利用すれば数秒から数十秒で関数をデプロイできますし、 cdk watch コマンドで変更されたファイルの監視やログの確認もできます。
でも、Lambda 関数の中身のロジックを 1 行変更しただけで動作確認に数秒待たされると思うと、ちょっと長すぎますよね。

ということで今回は、新しいアプリをゴリゴリ生み出す達人、AWS Prototyping Engineer の 工藤 朋哉 さんに「最短で Lambda 関数の動きを確認する技」を教えてもらいましょう !

本記事は、アプリケーションコードをあまり書いたことがないインフラエンジニアやサーバーレス開発の初心者、テスト自動化に馴染みがないアプリエンジニアを対象とした内容となっています。テスト自動化へのモチベーションを高めるため、導入部分ではベストプラクティスに沿わない部分があることをご容赦ください。


X ポスト » | Facebook シェア » | はてブ »

builders.flash メールメンバー登録

builders.flash メールメンバー登録で、毎月の最新アップデート情報とともに、AWS を無料でお試しいただけるクレジットコードを受け取ることができます。 
今すぐ登録 »

Lambda 関数の検証時間はどうすれば短くなる ?

高野「工藤さん、Lambda 関数を何度もデプロイしていたら、待ち時間がつらくなってきました。どうしたら短くできるか教えてください !」

工藤「デプロイ回数が多いと辛いですよね。どんな検証をしているんですか ?」

高野admin ロールをもつユーザーを返す関数を Python で作っているんですが、取得漏れや余分なユーザーが含まれるバグがないか気になっていて・・・。」

backend/src/app.py ... Lambda ハンドラーのコード

コード

python
USERS = [
    {"username": "foo", "roles": []},
    {"username": "bar", "roles": ["admin"]},
    {"username": "baz", "roles": ["audit"]},
    {"username": "qux", "roles": ["admin", "audit"]},
]


def handler(event, context):
    # admin ロールを保持しているユーザーだけを返す
    users = [user for user in USERS if "admin" in user["roles"]]
    return {"statusCode": 200, "body": users}

bin/cdk.ts ... CDK のコード

コード

typescript
new Function(this, "ListUsersFunction", {
    runtime: Runtime.PYTHON_3_13,
    handler: "src/app.handler",
    code: Code.fromAsset("backend"),
});

Lambda 関数のコードはローカルで直接実行可能

高野「これを cdk watch でデプロイして、aws lambda invoke コマンドで動かしています。」

工藤「なるほど。確認したいのはビジネスロジックなのに、デプロイで余計な待ち時間が発生しているんですね。じゃあ、直接 app.py を実行しちゃうのはどうでしょうか。」

高野「えっ、Lambda 関数のコードってローカルで直接実行できるんですか?クラウドにデプロイするか、エミュレータみたいなものが必要だと思っていました。それにほら、 event とか context という謎の引数が渡されていますよ。」

工藤Lambda 関数のコードはただの Python コードなので実行できますよ。実際に Lambda サービスの中でも handler() のように呼び出しているだけで、特別なことはしていません。ハンドラーの引数は、いまは関数の中で参照していないですよね。必要になったら必要な分だけ渡せばいいんです。とりあえず動かしてみましょう。」

Note : Lambda の中でハンドラー関数がどのように実行されているか詳しく知りたい方は、こちらの記事 の bootstrap ファイルを見てみることをおすすめします。

達人の技 1 / ローカル実行用のスクリプトで動作確認する

✨️技の効果:検証時間を短くする

Lambda ハンドラーのローカル実行スクリプトを作成

Python と TypeScript で、Lambda ハンドラーのローカル実行スクリプトを書いてみましょう。

Python

main.py ... Lambda ハンドラーを直接実行するためのスクリプト

from pprint import pprint  # レスポンスの整形表示用
from app import handler


def main():
    response = handler({}, {})
    pprint(response)


if __name__ == "__main__":
    main()

実行方法

python main.py

TypeScript

⚠️ ts-node のインストールが必要です。 npm i -D ts-node

⚠️ any 型の使用に抵抗がある方は続きをお読みください !

app.ts ... Lambda ハンドラーのコード

type User = {
  username: string;
  email: string;
  roles: string[];
};

const Users: User[] = [
  { username: "foo", roles: [] },
  { username: "bar", roles: ["admin"] },
  { username: "baz", roles: ["audit"] },
  { username: "qux", roles: ["admin", "audit"] },
];

export const handler = async function (event: any, context: any) {
  const users = Users.filter((user) => user.roles.includes("admin"));
  return { statusCode: 200, body: JSON.stringify(users) };
};

main.ts ... Lambda ハンドラーを直接実行するためのスクリプト

import { handler } from "./app";

async function main() {
  const response = await handler({}, {});
  console.log(response);
}

main();

実行方法

npx ts-node main.ts

ハンドラー関数の引数は ?

高野「ローカルで Lambda ハンドラーが実行できるのは便利ですね ! 時間短縮になりました。でもやっぱり、ハンドラーの引数で実際にどんな値が渡されるのか気になります。TypeScript で any 型を使うのも気が引けますし・・・。」

工藤eventcontext の内容が重要なときは、実際にクラウド上で動かしてイベントをキャプチャするか、生成 AI に聞くのがおすすめです。」

達人の技 2 / 生成 AI に引数を補完してもらう

✨️技の効果:コードを時間を短くする。複雑な型が必要なときに特に有効

ハンドラーにどんな引数が渡されるかわからない場合は、生成 AI にサンプルコードを書いてもらうのが有効です。静的型付けの言語でのエラー解決だけでなく、値まで書いてくれるのがメリットです。

Lambda ハンドラーの定義

例えば、TypeScript で Amazon API Gateway のプロキシ統合を使用した Lambda ハンドラー はこのように定義できます。

typescript
export const handler: APIGatewayProxyHandler = async function (event, context) {
  const users = Users.filter((user) => user.roles.includes("admin"));
  return { statusCode: 200, body: JSON.stringify(users) };
};

npm パッケージをインストール

handler に指定されている型の定義は @types/aws-lambda パッケージで提供されています。npm パッケージをインストールするには以下のコマンドを実行します。詳細は「TypeScript の Lambda 関数ハンドラーの定義」を参照してください。

typescript
npm i -D @types/aws-lambda

ハンドラー関数の型情報

IDE でマウスオーバーしてハンドラー関数の型情報をみてみると、APIGatewayProxyEvent などを引数に取ることがわかります。

Screenshot showing a TypeScript code example for AWS Lambda APIGatewayProxyHandler, which works with Lambda Proxy Integration for Rest API or HTTP API integration using Payload Format version 1.0. The image includes code and a reference to AWS documentation.

Amaozn Q Developer のインラインチャット

コーディング中に生成 AI を使う方法はいくつかあります。チャット形式で「これは AWS Lambda のハンドラー関数です。ローカルの Node.js で呼べるようにするコードを書いてください」と聞いてみるのもいいでしょう。

エンジニアであれば、IDE の中でコードを直接修正してもらったほうが便利ですよね。Amaozn Q Developer のインラインチャット を使えば、ピンポイントで APIGatewayProxyEvent Context 型のオブジェクトを生成してくれます。次のようにコンパイルエラーになっている状態で Control + I (macOS の場合は Command + I) を押して次のようなプロンプトを入力します。

Fix type errors. Fill required fields in each interface with sample values
A screenshot of Visual Studio Code displaying a TypeScript file for an AWS Lambda function with type errors highlighted. The image shows missing properties errors for API Gateway event, context, and callback types, along with coding suggestions in the editor.

サンプル値を入力

すると、インターフェースを充足するようにフィールドを追加し、それっぽいサンプル値を入れてくれます。便利ですね !

💡 Tips : AWS の認証情報があれば API 呼び出しも可能

ローカルでのスクリプト実行時に AWS の認証情報を環境変数などにセットしていれば、AWS SDK を使用したコードの実行も可能です。ベストプラクティスとして、永続的な IAM ユーザーのアクセスキーではなく、AWS IAM Identity Center で認証情報を取得することをおすすめします。詳細は AWS CLI のドキュメント を参照してください。

ただし、持っている権限によってはデータやリソースの変更ができてしまうため、副作用に注意して使用してください。

Screenshot of a TypeScript code editor showing an error where a block-scoped variable 'event' cannot be redeclared in an AWS Lambda function using APIGatewayProxyEvent. The error message is highlighted in red, indicating that the same variable name is being used twice in the same scope.

テストを自動化してもっと楽をしよう

高野「Lambda 関数の呼び出しって、こんなに簡単にできたんですね。でも、結果を目視で確認したりするのはまだ面倒です・・・。」

工藤「ローカル実行はあくまで、とりあえず動かすための手段です。今回でいえばユーザーの抽出ロジックが肝ですが、 狙ったところだけ、すばやく、何回も結果を確認するには、テストを自動化しちゃいましょう。」

高野「でも、テスト自動化って難しいんでしょう? テストピラミッドとか、モックとか言われてもよくわかりません !」

工藤「自動化されたテストでやることはさっきのローカル実行とあまり変わりませんよ。コードを実行して、結果を検証する部分を自動にするだけです。つまり、 ただの便利スクリプトの一種です。まずはユニットテストといって、純粋な Python コードだけで完結する部分のテストを書いてみましょう。」

達人の技 3 / 便利スクリプトとしてのユニットテストを書こう

✨️技の効果:検証時間を短くする。コードを変更したとき、意図しない部分が壊れていないか確認してエンジニアの心を安らげる

Python のテストフレームワークである pytest をインストールするために、Python の仮想環境を作成します。

Note: pytest は Lambda の デプロイパッケージ (ソースコードや依存パッケージを zip にまとめたもの) には必要ないため、通常の requirements.txt ではなく requirements-dev.txt に保存しています。

Note : TypeScript で Lambda 関数のコードを記述する場合、テストフレームワークには Jest がよく使用されます。Lambdaのデプロイパッケージを作成するには、NodejsFunction コンストラクトを使用すると便利です。NodejsFunction はコードを自動的に esbuild でバンドルして不要な依存関係やコードを取り除いてくれるため、CDK アプリと同じ package.json で依存関係管理できます。

Python の仮想環境を作成

コマンド

bash
cd backend
python3 -m venv .venv
source .venv/bin/activate
echo pytest >> requirements-dev.txt
pip3 install -r requirements-dev.txt

コードを配置

テストコードをディレクトリに分けて配置するために、次のようにコードを配置します。

backend/
├── .venv/
├── requirements.txt
├── src/
│ ├── init.py
│ ├── app.py # Lambda 関数のコード
│ └── main.py # ローカル実行用のスクリプト
└── tests/
└── test_app.py # テストコード

メインのロジックを新しい関数に分離

src/app.py をテストをしやすくするために、ハンドラー関数からメインのロジックを新しい関数に分離します。これで filter_users_by_role 関数だけをテストすればよくなります。

python
def filter_users_by_role(users, role):
    return [user for user in users if role in user["roles"]]


def handler(event, context):
    # admin ロールを保持しているユーザーだけを返す
    users = filter_users_by_role(USERS, "admin")
    return {"statusCode": 200, "body": users}

テストコードを作成

tests/test_app.py としてテストコードを書きます。

python
from src.app import filter_users_by_role


def test_filter_users_by_role():
    USERS = [
        {"username": "foo", "roles": []},
        {"username": "bar", "roles": ["admin"]},
        {"username": "baz", "roles": ["audit"]},
        {"username": "qux", "roles": ["admin", "audit"]},
    ]
    filtered_users = filter_users_by_role(USERS, "admin")
    assert len(filtered_users) == 2
    assert filtered_users[0]["username"] == "bar"
    assert filtered_users[1]["username"] == "qux"

テストを実行

次のようにテストを実行します。仮想環境が有効になっていることに注意してください。

bash
python -m pytest tests/

==================================================== test session starts ====================================================
platform darwin -- Python 3.13.0, pytest-8.3.4, pluggy-1.5.0
rootdir: /Users/konoken/cdk-serverless-articles/packages/02-first-unit-test/api/python
collected 1 item                                                                                                            

tests/test_app.py .                                                                                                   [100%]

===================================================== 1 passed in 0.00s =====================================================

CDK を含めた全体のファイル構成

テストが成功し、ビジネスロジックが正しいことが確認できました ! これでロジックを修正しても再実行が簡単になりますね。

CDK を含めた全体のファイル構成は次のようになります。CDK ではアプリケーションコードも一緒に管理することが一般的です。また、CDK のコードと Lambda 関数のコードが異なる言語で書かれていても問題ありません。Lambda のデプロイパッケージを作成するコマンドや除外するファイルなどは細かくカスタマイズできます。詳細は Blackbelt Online Seminar - CDK #3 を参照してください。

./
├── backend/
│ ├── .venv/
│ ├── requirements.txt
│ ├── src/
│ │ ├── init.py
│ │ ├── app.py # Lambda 関数のコード
│ │ └── main.py # ローカル実行用のスクリプト
│ └── tests/
│ └── test_app.py # テストコード
├── bin/
│ └── cdk.ts # CDK のコード
├── cdk.json
├── package.json
└── tsconfig.json

exclude を追加

Lambda のデプロイパッケージに開発用のファイルが含まれないように、exclude を追加することをおすすめします。

python
new Function(this, "ListUsersFunction", {
    runtime: Runtime.PYTHON_3_13,
    handler: "src/app.handler",
    code: Code.fromAsset("backend", {
     exclude: [".venv", "__pycache__", ".pytest_cache", "src/main.py", "tests"],
    }),
});

「何をテストしようとしているか」を意識しよう

高野「ユニットテストが書けるとサクサク開発が進みますね。 Amazon Q Developer にユニットテストのコードを生成してもらう のも便利です。Lambda 関数の開発がぐっと身近になりました !」

工藤「Lambda サービスにデプロイしないとわからないことももちろんありますが、素早く何度も実行できるローカルテストを取り入れると開発が速くなりますよ。テスト自動化にはさまざまなテクニックや考え方がありますが、まずはユニットテストを便利スクリプトの一種とみて試してみるのはおすすめです。」

前回、サーバーレス開発の「速さ」を分解して考えてみました。ローカル実行やユニットテストは、繰り返しの多い検証をなるべく高速なローカルテストに寄せるアプローチだと言えるでしょう。

工藤「ここでぜひ覚えておいてほしいことは『何をテストしようとしているか』を常に意識することです。Lambda 関数の場合、テストの関心事はざっくり 3 つに分けることができます。」

  • ビジネスロジックのテストは、「admin ロールをもつユーザーを返すこと」のように純粋な Python のコードの振る舞いを確認します。ビジネスロジックは最も頻繁に変更されるので、ユニットテストを書いて検証時間を短くすることに大きなメリットがあります。

  • ハンドラー関数のテストは、「リクエストパラメータからロール ID を取得してビジネスロジックを呼び出すこと」などを確認します。main.py を用意してローカルでテストすることもできますが、ハンドラー関数に渡されるイベントやコンテキストには多くの情報が含まれるので、テストの信頼性を保つには実際のイベントとの一致が不可欠になります。ビジネスロジックのテストが十分なら、ハンドラー関数のテストパターンを最小限にできます。その場合、デプロイしてクラウドで動かしたほうが早いこともあります。

  • Lambda 関数そのもの (AWSリソース) の構成テストは、「Lambda 関数が正しく起動され、Amazon CloudWatch Logs にログが出力されること」のように、実際に AWS にデプロイしてリソースの振る舞いを確認します。デプロイパッケージの作成コマンドを間違えてディレクトリ構成が違っていたり、異なる CPU アーキテクチャ向けのバイナリが含まれていたり、AWS IAM 権限が足りなかったりする可能性があるためです。あまり多くのテストパターンは必要ないので、統合テスト (Integration Test) として実施するのがおすすめです。

高野「自分がテストしたかった部分をすっきり整理することができました。工藤さん、ありがとうございました !」



次回予告

今回は純粋な Python コードでしたが、実際の Lambda 関数では Amazon DynamoDB からユーザーを取得したり、Amazon S3 からファイルを読み取ったりと AWS サービスと連携することがほとんどです。そんなとき、どうやってテストをすればよいでしょうか ?

おや、次の「サーバーレス開発の達人」が来てくれたようです。

??? 「外部のサービスと連携したコードでも、すばやくテストする方法があるぞ !」

次回もお楽しみに !

参考コンテンツ集

  • 2021 年版、サーバーレスのテスト手法を考える(セッション動画資料
  • CI/CD なのだからちゃんとテストを書いてみよう ~ 分散環境のためのコンシューマ駆動契約を添えて ~ (セッション動画資料
  • AWS CDK における単体テストの使い所を学ぶ ... 今回は Lambda 関数のコード (ビジネスロジック) のユニットテストについて考えましたが、CDK のコードでもユニットテストを書いた方が良いケースがたくさんあります。
  • 筆者プロフィール

    工藤 朋哉 (左側 / Kudo, Tomoya)
    アマゾン ウェブ サービス ジャパン合同会社
    Prototyping Engineer

    技術的な課題を抱えている案件のプロトタイプを作成することで、お客様のプロジェクトを加速させるお手伝いをしています。プロトタイプ開発では主に AWS CDK を使ってお客様の環境でスムーズにデプロイが行えるように実装します。


    高野 賢司 (右側 / Kono, Kenji / @konokenj)
    アマゾン ウェブ サービス ジャパン合同会社
    シニア ソリューション アーキテクト

    名古屋在住のソリューションアーキテクトとして、主に東海地方の製造業のお客様を技術面でサポートしています。 Infrastructure as Code (IaC) のスペシャリストとして、イベント登壇や Baseline Environment on AWS (BLEA) の開発も行っています。

    Two men smiling and holding a large yellow box labeled 'FRAGILE' and 'MADE IN BELGIUM' in an indoor setting.