1.はじめに
1.1.背景
AWS SAMとLambdaを使用して、マイクロサービスパターンの考え方を取り入れたサーバーレスToDoアプリを構築し、その理解を深めることを目的とします。
今回作成したGitHubへのリンク
1.2.マイクロサービスとは?
マイクロサービスとは、一つの大きなアプリケーションを小さなサービスの集合として構成するアーキテクチャパターンです。
このハンズオンでは、ToDoアプリの各機能(一覧取得、作成、更新など)をそれぞれ独立したLambda関数として実装し、マイクロサービスの考え方を適用していきます。
1.3.AWS SAM(Serverless Application Model)とは?
AWS上でサーバーレスアプリケーションを構築・デプロイするためのオープンソースフレームワークです。
CloudFormationを拡張したもので、YAML形式のテンプレートを使って、Lambda関数、API Gateway、DynamoDBテーブルといったリソースを簡潔に定義することが可能でサーバーレス開発を容易にします。
1.4.本ハンズオンの構成
- 以下のような、マイクロサービスにのっとったToDoアプリを構築していきます。
2.ハンズオン
2.1.前提
2.1.1.実行環境
- 本記事のバージョンはあくまで執筆時点のものです。
互換性の問題が発生した場合は、適宜読み替えて実行ください。
項目 | 設定 |
---|---|
環境 | AWS CloudShell |
リージョン | バージニア北部(us-east-1) |
AWS CLI | 2.27.8 |
Python | 3.9.21 |
2.1.2.ToDoアプリのファイル構成
/
├── template.yaml # SAM定義ファイル
├── dispatch/ # ディスパッチLambda
│ └── app.py
├── get-todos/ # 一覧取得Lambda
│ └── app.py
├── create-todo/ # 作成Lambda
│ └── app.py
├── get-todo-by-id/ # ID指定取得Lambda
│ └── app.py
├── update-todo/ # 更新Lambda
│ └── app.py
├── delete-todo/ # 削除Lambda
│ └── app.py
└── layer/ # 共通レイヤー
├── requirements.txt # Pythonパッケージ定義
└── todo_common/
└── __init__.py # 共通ユーティリティ
2.2.プロジェクト構造のセットアップ
- 最後まで実行して
2.1.2.ToDoアプリのファイル構成
と同様なことを確認する。
# メインフォルダ作成
mkdir todo-app && cd todo-app
# 各Lambda関数用ディレクトリ作成
mkdir -p dispatch
mkdir -p get-todos
mkdir -p create-todo
mkdir -p get-todo-by-id
mkdir -p update-todo
mkdir -p delete-todo
mkdir -p layer/todo_common
# テンプレートファイル作成
touch template.yaml
touch dispatch/app.py
touch get-todos/app.py
touch create-todo/app.py
touch get-todo-by-id/app.py
touch update-todo/app.py
touch delete-todo/app.py
touch layer/todo_common/__init__.py
touch layer/requirements.txt
# ディレクトリ構造確認
find . -type f | sort
2.3.SAMテンプレート作成
- サーバーレスリソースを宣言的にYAMLコードで定義することが可能な、CloudFormationを拡張したサーバーレスアプリケーションの構築に特化したテンプレートです。
- 多数の小さなLambda関数とそれを連携させるためのリソースが必要となるため、SAMを活用することで各マイクロサービス(Lambda関数)の定義を一箇所で管理する。
- 今回構築する TodoAPIのCORSについては「
AllowOrigin: "'*'"
」としており、全てのオリジンからのアクセスを許可する設定としています。
本番環境ではセキュリティリスクを考慮し、許可するオリジンを具体的に指定することを推奨。
cat > template.yaml << EOF
# CloudFormationのテンプレート形式のバージョンを指定
AWSTemplateFormatVersion: '2010-09-09'
# SAM(Serverless Application Model)変換を有効化
# これによりSAM特有の簡略化された構文が使えるようになる
Transform: AWS::Serverless-2016-10-31
# テンプレートの説明
Description: Python ToDo API
# グローバル設定 - すべてのLambda関数に適用される共通設定
Globals:
# すべてのLambda関数に適用される設定
Function:
# 関数のタイムアウト時間(秒単位)- 10秒でタイムアウトする
Timeout: 10
# 使用するランタイム - Python 3.9を使用
Runtime: python3.9
# CPU/メモリアーキテクチャ - x86_64を使用
Architectures:
- x86_64
# すべての関数に共通の環境変数
Environment:
Variables:
# DynamoDBテーブル名を環境変数として設定
# !Refを使用して別のリソース(TodosTable)を参照
TABLE_NAME: !Ref TodosTable
# テンプレート内で定義されるAWSリソース
Resources:
# 共通レイヤー - 複数のLambda関数で共有されるコード
CommonLayer:
# AWS::Serverless::LayerVersionはSAMの拡張リソースタイプ
Type: AWS::Serverless::LayerVersion
Properties:
# レイヤーの名前
LayerName: todo-common-layer
# レイヤーの説明
Description: Common dependencies for Todo functions
# レイヤーのコードの場所(ローカルパス)
ContentUri: layer/
# このレイヤーと互換性のあるランタイム
CompatibleRuntimes:
- python3.9
# レイヤーのビルド方法に関するメタデータ
Metadata:
BuildMethod: python3.9
# API Gateway - すべてのHTTPリクエストのエントリポイント
TodoApi:
# AWS::Serverless::ApiはSAMの拡張リソースタイプ
Type: AWS::Serverless::Api
Properties:
# APIのデプロイステージ名
StageName: Prod
# CORS(クロスオリジンリソース共有)の設定
Cors:
# 許可するオリジン - *は全てのドメインからのアクセスを許可
AllowOrigin: "'*'"
# 許可するHTTPヘッダー
AllowHeaders: "'Content-Type,Authorization'"
# 許可するHTTPメソッド
AllowMethods: "'GET,POST,PUT,DELETE,OPTIONS'"
# ディスパッチ(管理)用Lambda関数 - APIのルートパスを処理
DispatchFunction:
# AWS::Serverless::FunctionはSAMの拡張リソースタイプ
Type: AWS::Serverless::Function
Properties:
# 関数のコードの場所(ローカルパス)
CodeUri: dispatch/
# 関数のハンドラー(ファイル名.関数名)
Handler: app.lambda_handler
# 関数が使用するレイヤー
Layers:
# CommonLayerを参照
- !Ref CommonLayer
# 関数のトリガーとなるイベント
Events:
# APIルートパスのイベント
ApiRoot:
# APIイベントタイプ
Type: Api
Properties:
# 使用するAPI Gateway
RestApiId: !Ref TodoApi
# APIのパス
Path: /
# 対象のHTTPメソッド(ANYは全メソッド対応)
Method: ANY
# ToDo一覧取得用Lambda関数
GetTodosFunction:
Type: AWS::Serverless::Function
Properties:
# 関数のコードの場所
CodeUri: get-todos/
# 関数のハンドラー
Handler: app.lambda_handler
# 関数が使用するレイヤー
Layers:
- !Ref CommonLayer
# 関数のトリガーとなるイベント
Events:
# API /todos GETイベント
ApiEvent:
Type: Api
Properties:
RestApiId: !Ref TodoApi
# /todosパスを処理
Path: /todos
# GETメソッドのみを処理
Method: GET
# 関数に付与するIAMポリシー
Policies:
# DynamoDBの読み取り専用ポリシー
- DynamoDBReadPolicy:
# 対象のテーブル名
TableName: !Ref TodosTable
# ToDo作成用Lambda関数
CreateTodoFunction:
Type: AWS::Serverless::Function
Properties:
# 関数のコードの場所
CodeUri: create-todo/
# 関数のハンドラー
Handler: app.lambda_handler
# 関数が使用するレイヤー
Layers:
- !Ref CommonLayer
# 関数のトリガーとなるイベント
Events:
# API /todos POSTイベント
ApiEvent:
Type: Api
Properties:
RestApiId: !Ref TodoApi
# /todosパスを処理
Path: /todos
# POSTメソッドのみを処理
Method: POST
# 関数に付与するIAMポリシー
Policies:
# DynamoDBの書き込み専用ポリシー
- DynamoDBWritePolicy:
# 対象のテーブル名
TableName: !Ref TodosTable
# 個別ToDo取得用Lambda関数
GetTodoByIdFunction:
Type: AWS::Serverless::Function
Properties:
# 関数のコードの場所
CodeUri: get-todo-by-id/
# 関数のハンドラー
Handler: app.lambda_handler
# 関数が使用するレイヤー
Layers:
- !Ref CommonLayer
# 関数のトリガーとなるイベント
Events:
# API /todos/{id} GETイベント
ApiEvent:
Type: Api
Properties:
RestApiId: !Ref TodoApi
# /todos/{id}パスを処理 - {id}は動的パラメータ
Path: /todos/{id}
# GETメソッドのみを処理
Method: GET
# 関数に付与するIAMポリシー
Policies:
# DynamoDBの読み取り専用ポリシー
- DynamoDBReadPolicy:
# 対象のテーブル名
TableName: !Ref TodosTable
# ToDo更新用Lambda関数
UpdateTodoFunction:
Type: AWS::Serverless::Function
Properties:
# 関数のコードの場所
CodeUri: update-todo/
# 関数のハンドラー
Handler: app.lambda_handler
# 関数が使用するレイヤー
Layers:
- !Ref CommonLayer
# 関数のトリガーとなるイベント
Events:
# API /todos/{id} PUTイベント
ApiEvent:
Type: Api
Properties:
RestApiId: !Ref TodoApi
# /todos/{id}パスを処理
Path: /todos/{id}
# PUTメソッドのみを処理
Method: PUT
# 関数に付与するIAMポリシー
Policies:
# DynamoDBのCRUD(作成・読取・更新・削除)ポリシー
- DynamoDBCrudPolicy:
# 対象のテーブル名
TableName: !Ref TodosTable
# ToDo削除用Lambda関数
DeleteTodoFunction:
Type: AWS::Serverless::Function
Properties:
# 関数のコードの場所
CodeUri: delete-todo/
# 関数のハンドラー
Handler: app.lambda_handler
# 関数が使用するレイヤー
Layers:
- !Ref CommonLayer
# 関数のトリガーとなるイベント
Events:
# API /todos/{id} DELETEイベント
ApiEvent:
Type: Api
Properties:
RestApiId: !Ref TodoApi
# /todos/{id}パスを処理
Path: /todos/{id}
# DELETEメソッドのみを処理
Method: DELETE
# 関数に付与するIAMポリシー
Policies:
# DynamoDBのCRUDポリシー
- DynamoDBCrudPolicy:
# 対象のテーブル名
TableName: !Ref TodosTable
# DynamoDBテーブル - ToDoデータの永続化に使用
TodosTable:
# AWS::DynamoDB::TableはCloudFormationの標準リソースタイプ
Type: AWS::DynamoDB::Table
Properties:
# テーブルの名前
TableName: PythonTodos
# 課金モード - PAY_PER_REQUESTはオンデマンド課金
BillingMode: PAY_PER_REQUEST
# テーブルの属性(カラム)定義
AttributeDefinitions:
# id属性を定義
- AttributeName: id
# S = 文字列型
AttributeType: S
# テーブルのキースキーマ
KeySchema:
# idをハッシュキー(プライマリキー)として使用
- AttributeName: id
KeyType: HASH
# テンプレートの出力値
Outputs:
# API URLを出力値として定義
ApiUrl:
# 出力値の説明
Description: URL of the API endpoint
# APIのURL - !Subを使用して文字列を組み立て
# ${TodoApi}はAPI IDに、${AWS::Region}は現在のリージョンに置き換えられる
Value: !Sub "https://${TodoApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
EOF
2.4.共通レイヤの作成
2.4.1.共通レイヤとは?
- 複数のLambda関数間で共有される汎用的なコード
- 共有することで
コードの重複
やロジックの一貫性
、変更の一元管理
などのメリットがある。
■ 今回は以下の要素を共通レイヤとする
項番 | 項目名 | 詳細 |
---|---|---|
1 | 共通のユーティリティ関数 | 複数の場所で使われる汎用的な機能 |
2 | データベース接続のセットアップ | DynamoDBなどへの接続初期化 |
3 | レスポンス形式の標準化 | API応答の一貫したフォーマット |
4 | ビジネスロジックの共通部分 | アプリケーション固有の共有機能 |
2.4.2.ファイルの作成
# 共通ユーティリティの作成
cat > layer/todo_common/__init__.py << EOF
import os
import json
import boto3
import decimal
import uuid
from datetime import datetime
# デシマル型をJSONに変換するためのエンコーダ
class DecimalEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, decimal.Decimal):
return float(obj)
return super(DecimalEncoder, self).default(obj)
# DynamoDB接続
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ.get('TABLE_NAME', 'PythonTodos'))
# レスポンス作成ヘルパー
def create_response(status_code, body=None):
response = {
'statusCode': status_code,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type,Authorization'
}
}
if body is not None:
response['body'] = json.dumps(body, cls=DecimalEncoder)
return response
# 新しいToDo ID生成
def generate_todo_id():
return str(uuid.uuid4())
# 現在のタイムスタンプ取得
def get_timestamp():
return datetime.now().isoformat()
EOF
2.5.各Lambda関数の実装
2.5.1.ディスパッチ(管理用)関数
- リクエストの受付の役割を果たす関数
cat > dispatch/app.py << EOF
import json
def lambda_handler(event, context):
return {
'statusCode': 200,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps({
'message': 'Welcome to ToDo API',
'endpoints': {
'GET /todos': 'List all todos',
'POST /todos': 'Create a new todo',
'GET /todos/{id}': 'Get a specific todo',
'PUT /todos/{id}': 'Update a todo',
'DELETE /todos/{id}': 'Delete a todo'
}
})
}
EOF
2.5.2.ToDo一覧取得関数
- DynamoDBからToDoの一覧を取得する関数
table.scan()
については、テーブル全体をスキャン
するため大規模なテーブルではパフォーマンスやコストに影響を与える可能性があります。
実運用では、クエリやインデックスの利用を検討してください。
cat > get-todos/app.py << EOF
from todo_common import table, create_response
def lambda_handler(event, context):
try:
# DynamoDBからすべてのToDoを取得
response = table.scan()
items = response.get('Items', [])
return create_response(200, {'todos': items})
except Exception as e:
print(f"Error: {str(e)}")
return create_response(500, {'error': 'Error fetching todos'})
EOF
2.5.3.ToDo作成関数
- DynamoDBへToDoを保存する関数
cat > create-todo/app.py << EOF
import json
from todo_common import table, create_response, generate_todo_id, get_timestamp
def lambda_handler(event, context):
try:
# リクエストボディを取得
body = json.loads(event['body']) if event.get('body') else {}
# タイトルがなければエラー
if 'title' not in body:
return create_response(400, {'error': 'Title is required'})
# 新しいToDoアイテムを作成
todo_id = generate_todo_id()
new_todo = {
'id': todo_id,
'title': body['title'],
'completed': False,
'createdAt': get_timestamp()
}
# DynamoDBに保存
table.put_item(Item=new_todo)
return create_response(201, new_todo)
except Exception as e:
print(f"Error: {str(e)}")
return create_response(500, {'error': 'Error creating todo'})
EOF
2.5.4.ToDo個別取得関数
- 特定のToDoのみを取得する関数
cat > get-todo-by-id/app.py << EOF
from todo_common import table, create_response
def lambda_handler(event, context):
try:
# パスパラメータからIDを取得
todo_id = event['pathParameters']['id']
# DynamoDBから指定されたIDのToDoを取得
response = table.get_item(Key={'id': todo_id})
# アイテムが見つからない場合は404エラー
if 'Item' not in response:
return create_response(404, {'error': 'Todo not found'})
return create_response(200, response['Item'])
except Exception as e:
print(f"Error: {str(e)}")
return create_response(500, {'error': 'Error fetching todo'})
EOF
2.5.5.ToDo更新関数
-ToDoの更新をおこなう関数
cat > update-todo/app.py << EOF
import json
from todo_common import table, create_response, get_timestamp
def lambda_handler(event, context):
try:
# パスパラメータからIDを取得
todo_id = event['pathParameters']['id']
# リクエストボディを取得
body = json.loads(event['body']) if event.get('body') else {}
# 既存のToDoを確認
response = table.get_item(Key={'id': todo_id})
if 'Item' not in response:
return create_response(404, {'error': 'Todo not found'})
# 更新式の構築
update_expression = "SET "
expression_values = {}
if 'title' in body:
update_expression += "title = :title, "
expression_values[':title'] = body['title']
if 'completed' in body:
update_expression += "completed = :completed, "
expression_values[':completed'] = body['completed']
update_expression += "updatedAt = :updatedAt"
expression_values[':updatedAt'] = get_timestamp()
# DynamoDBを更新
response = table.update_item(
Key={'id': todo_id},
UpdateExpression=update_expression,
ExpressionAttributeValues=expression_values,
ReturnValues="ALL_NEW"
)
return create_response(200, response['Attributes'])
except Exception as e:
print(f"Error: {str(e)}")
return create_response(500, {'error': 'Error updating todo'})
EOF
2.5.6.ToDo削除関数
- ToDoを削除する関数
cat > delete-todo/app.py << EOF
from todo_common import table, create_response
def lambda_handler(event, context):
try:
# パスパラメータからIDを取得
todo_id = event['pathParameters']['id']
# DynamoDBから削除
table.delete_item(Key={'id': todo_id})
return create_response(204)
except Exception as e:
print(f"Error: {str(e)}")
return create_response(500, {'error': 'Error deleting todo'})
EOF
2.6.ビルドとデプロイ
2.6.1.SAMによるビルド
- ビルドが成功すると
Build Succeeded
が表示される
# samコマンドによるビルド
sam build
2.6.2.SAMによるデプロイ
- 以下コマンドを実施するとウィザード形式で質問される
- デプロイコマンド後
Successfully created/updated stack - todo-app in us-east-1
が表示される。
■ 質問への回答例
項番 | 項目 | 項目(日本語) | 回答内容 |
---|---|---|---|
1 | Stack Name [sam-app] | スタック名 | todo-app |
2 | AWS Region [us-east-1] | リージョン | us-east-1 |
3 | Confirm changes before deploy [y/N] | デプロイを実行する前に、変更内容を確認しますか? | y |
4 | Allow SAM CLI IAM role creation [Y/n] | SAMがIAMロール作成を許可するか? | y |
5 | Disable rollback [y/N] | ロールバックを無効にするか? | n |
6 | ${関数名} Function has no authentication. Is this okay? [y/N] | (※各関数で聞かれる)認証が設定されていませんが、これでよいですか? | y |
7 | Save arguments to configuration file [Y/n] | 設定ファイルに引数を保存しますか? | y |
8 | SAM configuration file [samconfig.toml] | SAM設定ファイルの名前 | ENTER(デフォルトの「samconfig.toml」) |
9 | SAM configuration environment [default] | ファイル内で異なる環境(開発環境、テスト環境、本番環境など)の設定 | ENTER(デフォルトの「default」) |
# samコマンドによるデプロイ
sam deploy --guided
2.6.2.1.デプロイして発生したエラー
- 問題:今回デプロイコマンドを実施した際、以下のようなエラー(S3バケットが存在しないエラー)が表示されました。
- 原因:SAM CLIがデプロイパッケージをアップロードするためのS3バケットを見つけられない、または自動作成できない。
- 解決策:マネージドS3バケットを手動作成
Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-XXXXXXXXXXXX
マネージドS3バケット: aws-sam-cli-managed-default-samclisourcebucket-XXXXXXXXXXXX
A different default S3 bucket can be set in samconfig.toml and auto resolution of buckets turned off by setting resolve_s3=False
samconfig.tomlで別のデフォルトS3バケットを設定できます。また、resolve_s3=Falseと設定することで、バケットの自動解決をオフにできます。
(中略)
S3 Bucket does not exist.
S3バケットが存在しません。
参考: Facing some issues with AWS EventBridge Application
2.6.2.2.解決に向けた実行コマンド
# マネージドS3バケット作成コマンド実施
aws s3 mb s3://aws-sam-cli-managed-default-samclisourcebucket-XXXXXXXXXXXX --region us-east-1
# 再度samコマンドによるデプロイ
sam deploy --guided
2.7.挙動の確認
- デプロイが完了すると SAMからAPI URLが表示されるため、以下コマンドのURL部分を適宜修正してテストを実施する。
- 本ハンズオンはAWS CloudShellでの実行を前提としているため、CloudShellには jq がプリインストールされています。
ローカル環境で実行する場合は、別途 jq をインストールしてください。
2.7.1.URL取得
# デプロイされたURLを設定 (実際のURLに置き換えてください)
export API_URL=https://xxxxxxxxxx.execute-api.us-east-1.amazonaws.com/Prod
# または以下のコマンドでURLを取得
export API_URL="https://$(aws cloudformation list-stack-resources --stack-name todo-app --query "StackResourceSummaries[?ResourceType=='AWS::ApiGateway::RestApi'].PhysicalResourceId" --output text).execute-api.us-east-1.amazonaws.com/Prod/"
echo $API_URL
2.7.2.一連の挙動確認
2.7.2.1.ToDoの作成
# ディスパッチ関数(ルート)をテスト
curl $API_URL
# 新しいToDoを作成し、そのIDを変数に保存
TODO_ID=$(curl -X POST $API_URL/todos \
-H "Content-Type: application/json" \
-d '{"title": "AWS SAMを使いこなす"}' | jq -r '.id')
# 参考レスポンス
{
"completed": false,
"createdAt": "2025-05-13T13:34:35.480359",
"id": "00653957-229a-49e8-99e0-6dc8baf11cac",
"title": "AWS SAMを使いこなす"
}
2.7.2.2.ToDoの更新
# ToDoを完了済みに更新
curl -X PUT $API_URL/todos/$TODO_ID \
-H "Content-Type: application/json" \
-d '{"completed": true}' | jq
# 参考レスポンス
{
"completed": true,
"createdAt": "2025-05-13T13:34:35.480359",
"id": "00653957-229a-49e8-99e0-6dc8baf11cac",
"updatedAt": "2025-05-13T13:36:55.682951",
"title": "AWS SAMを使いこなす"
}
2.7.2.3.ToDo一覧取得
# ToDo一覧取得
curl $API_URL/todos | jq
# 参考レスポンス
{
"todos": [
{
"completed": true,
"createdAt": "2025-05-13T13:34:35.480359",
"id": "00653957-229a-49e8-99e0-6dc8baf11cac",
"updatedAt": "2025-05-13T13:36:55.682951",
"title": "AWS SAMを使いこなす"
}
]
}
2.7.2.4.ToDo削除
# ToDo削除
curl -X DELETE $API_URL/todos/$TODO_ID
# 参考レスポンス
なし
# ToDo一覧取得
curl $API_URL/todos | jq
# 参考レスポンス
{
"todos": []
}
2.8.クリーンアップ
- 以下コマンドで、本ハンズオンで作成したリソースを削除します。
不要な課金を避けるために、ハンズオン終了後は実行することを推奨します。 - 削除の際に以下2点が聞かれるが、どちらも
y
と答えることを推奨する。
項番 | 項目 | 項目(日本語) | 回答内容 |
---|---|---|---|
1 | Are you sure you want to delete the stack todo-app in the region us-east-1 ? [y/N] | us-east-1リージョンのtodo-appスタックを削除してもよいか? | y |
2 | Are you sure you want to delete the folder todo-app in S3 which contains the artifacts? [y/N] | S3のtodo-appフォルダを削除してもよいか? | y |
# 削除コマンド
sam delete
3.おわりに
3.1.得られた知見
- マイクロサービスの基本的な考え方の理解。
- SAM (Serverless Application Model) を使用することで、多数のLambda関数や関連リソースを効率的に定義・管理・デプロイすることを体験。
3.2.今後の課題
- X-Rayなどを用いた分散トレーシングによるパフォーマンス監視とデバッグ。