使い方
ディレクトリ構成
serverless-backend-application-stack-python
├── .editorconfig
├── .gitignore
├── .vscode
│ ├── extensions.json
│ └── settings.json
├── build-app.sh # Lambdaのデプロイパッケージを作るシェルスクリプト
├── build-layer.sh # PostgreSQLクライアント用のLambdaレイヤーを作るシェルスクリプト
├── deploy
│ ├── cloudformation # 各CloudFormationテンプレート
│ │ ├── apigateway-lambda.yaml
│ │ ├── db.yaml
│ │ ├── s3-lambda.yaml
│ │ └── vpc.yaml
│ └── fixtures
│ └── psycopg3.zip # PostgreSQLのPython用クライアントであるpsycopg3のzipファイル
├── docker-compose.yml # ユニットテスト及びローカル開発に使用するDocker
├── poetry.lock
├── pyproject.toml
├── src
│ ├── .init_db.py # .から始まる以下のファイルはローカル実行用のファイル
│ ├── .init_minio.py
│ ├── .local.sample_api.get_users.py
│ ├── .local.sample_api.post_users.py
│ ├── .local.sample_event.import_users.py
│ ├── sample_api # API Gateway + Lambda + PostgreSQL構成のサンプルアプリケーション
│ │ ├── actions
│ │ │ ├── create_users_action.py
│ │ │ └── get_users_action.py
│ │ ├── daos
│ │ │ └── users_dao.py
│ │ ├── index.py
│ │ └── services
│ │ └── users_service.py
│ ├── sample_event # S3 + Lambda + PostgreSQL構成のサンプルアプリケーション
│ │ ├── actions
│ │ │ └── data_import_action.py
│ │ ├── aws
│ │ │ └── s3_client.py
│ │ ├── daos
│ │ │ └── users_dao.py
│ │ ├── index.py
│ │ └── services
│ │ └── data_import_service.py
│ └── sample_users.csv
└── tests
├── sample_api # API Gateway + Lambda + PostgreSQLアプリのユニットテスト
│ ├── conftest.py
│ ├── test_create_users.py
│ └── test_get_users.py
└── sample_event # S3 + Lambda + PostgreSQLアプリのユニットテスト
├── conftest.py
└── test_data_import.py
EC2インスタンスの立ち上げ
AWS MarketPlaceから本AMIを購入してください。その際AdministratorAcessのポリシーがアタッチされたIAMロールをインスタンスに付与してください
プロジェクトセットアップ
Pythonのパッケージ管理ツールであるpoetryを使ってserverless-backend-application-stack-pythonに登録済みのライブラリをインストールします
[ec2-user ~]cd serverless-backend-application-stack-python/
[ec2-user ~]poetry install
VPCデプロイ
まずはVPCをデプロイするためにaws-cliを使ってCloudFormationのデプロイを実行します。コマンド実行後AWSのマネジメントコンソールからデプロイの成功を待ちます。--stack-name、AppName、EnvNameはあなたの環境に合わせて好きな値を入れて下さい
[ec2-user ~]aws cloudformation create-stack \
--region us-east-1 \
--stack-name vpc \
--template-body file://deploy/cloudformation/vpc.yaml \
--parameters \
ParameterKey=AppName,ParameterValue=myapp \
ParameterKey=EnvName,ParameterValue=dev \
--capabilities CAPABILITY_NAMED_IAM
デプロイ完了後、マネジメントコンソールの出力タブを開いて以下の値を確認します。これらの値はその後のスタックのデプロイにて使用します。

データベースのデプロイ
Amazon Aurora PostgreSQLを先ほどデプロイしたVPC内にデプロイします。
[ec2-user ~]aws cloudformation create-stack \
--region us-east-1 \
--stack-name db \
--template-body file://deploy/cloudformation/db.yaml \
--parameters \
ParameterKey=AppName,ParameterValue=myapp \
ParameterKey=EnvName,ParameterValue=dev \
ParameterKey=DBUserName,ParameterValue=postgreAdmin \ # 好きな値を入れて下さい
ParameterKey=DBUserPassword,ParameterValue=SuperSecurePass123 \ # 8文字以上の好きな値を入れて下さい
ParameterKey=VpcId,ParameterValue=vpc-01187493c944a7ff0 \ # VPCの出力タブに出てきた値を入れます
ParameterKey=PrivateSubnet1,ParameterValue=subnet-0ba1ff7efe656281e \ # VPCの出力タブに出てきた値を入れます
ParameterKey=PrivateSubnet2,ParameterValue=subnet-0007df266992e3a06 \ # VPCの出力タブに出てきた値を入れます
--capabilities CAPABILITY_NAMED_IAM
DBのデプロイが完了したらマネジメントコンソールの出力タブよりAuroraクラスターのライターインスタンスのエンドポイントを確認します。

ローカルからデータベースに接続する
デプロイ済みのVPCにはSSMエージェントがインストール済みの踏み台サーバとVPCエンドポイントがデプロイされています。これらの仕組みを使ってプライベートサブネット内にあるDBとローカルのPCをポートフォワーディングして、ローカルから直接DBに接続できるようにします。
以下のコマンドでポートフォワーディングが可能になります。
[ローカルPC ~]aws ssm start-session \
--target i-034fbd3838d455db9 \ # VPCスタックのBastionInstanceIdの値
--document-name AWS-StartPortForwardingSessionToRemoteHost \
--region ap-northeast-1 \
--profile <your aws profile nmae> # ssmが実行できるAWSクレデンシャルが必要
--parameters '{"host":["db-postgresql-oqhxwoxfpbev.cluster-cy9ecx9quedk.ap-northeast-1.rds.amazonaws.com"],"portNumber":["5432"],"localPortNumber":["5432"]}'
# 先ほどデプロイしたDBスタックのライターインスタンスのエンドポイント
ローカルからのDB操作用にpgAdmin をダウンロードしてローカルPCにインストールします。 以下手順でpgAdminを作ってアプリが参照するデータベースとテーブルを作成します。
- サーバーを登録します

- Generalタブから好きなサーバー名を入力します

- ポートフォワーディングしているのでHostには127.0.0.1もしくはlocalhostを入力。CloudFormationのデプロイ時に入力したUsernameとPasswordを入力します。Maintenance databaseは既に入力されているpostgresのままで良いです。

- Databaseを作成します。

- Database名は好きなものを入れてもらって構いません。例ではmydbと入力してSaveをクリックするとDatabaseが出来上がります。

- 作成したmydbを選択してQuery Toolを開きます。そこからサンプルアプリで作るテーブルをつくります。

-
QueryからCreate Tableとinsertを実行してダミーのデータを作って下さい
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
country VARCHAR(100),
age INTEGER
);
INSERT INTO users (name, country, age) VALUES
('Alice', 'Japan', 30),
('Bob', 'USA', 25),
('Charlie', 'Germany', 35);
SELECT * FROM users;


以上で、ローカルからDBに接続して、アプリケーションから使用するDatabase, Tableの作成が完了です。
API Gateway + Lambdaのデプロイ
次にAPI Gateway + Lambdaから先ほど作成したデータベースを操作するアプリケーションをデプロイしていきます。
Lambdaレイヤー作成
まずはデータベースのクライアントとなるpsycopg3を使用するためにLambda layerを作ります。
以下のシェルスクリプトを実行するとlayerが作成されます。
[ec2-user ~]./build-layer.sh
シェルスクリプトを実行するとARNレスポンスが2つ返ってきますが以下のLayerVersionArnの値をCloudFormationのデプロイで使うためメモしておいて下さい
"LayerVersionArn": "arn:aws:lambda:us-east-1:<アカウントID>:layer:psycopg3-layer:1"
"LayerVersionArn": "aws:lambda:us-east-1:<アカウントID>:layer:python-lambda-application-template-lib-layer:1 "
デプロイ用バケット作成
LambdaのソースコードパッケージをS3バケットにアップロードしてCloudFormationから参照できるようにするため、専用のバケットを作成します。
[ec2-user ~]ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
[ec2-user ~]BUCKET_NAME="code-bucket-${ACCOUNT_ID}"
[ec2-user ~]aws s3api create-bucket \
--bucket $BUCKET_NAME \
--region us-east-1 \
--create-bucket-configuration LocationConstraint=<your region> #us-east-1以外に作成する場合はこのオプションを付けてください
デプロイパッケージとS3へのアップロード
Lambdaにデプロイするためのパッケージを作成します。既にアプリ側のサンプルソースは提供済みです。以下コマンドを実行してソースコードのパッケージを作成。その後、先程作ったS3バケットにzipファイルをアップロードします。
[ec2-user ~]./build-app.sh
[ec2-user ~]aws s3 cp .dist/app/sample_api/api.zip s3://$BUCKET_NAME
./build-app.shを実行すると.dist/app/sample_api/api.zipと.dist/app/sample_event/event.zip の2つのデプロイパッケージが生成されます。後者は後ほどのS3 + Lambdaのアプリケーションにて使用します。
API Gateway + LambdaのCloudFormationスタックのデプロイ
同様にaws-cliにてスタックのデプロイを行います。
[ec2-user ~]aws cloudformation create-stack \
--regio us-east-1 \
--stack-name apigateway-lambda \
--template-body file://deploy/cloudformation/apigateway-lambda.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--parameters \
ParameterKey=AppName,ParameterValue=myapp \
ParameterKey=EnvName,ParameterValue=dev \
ParameterKey=VpcId,ParameterValue=vpc-01187493c944a7ff0 \
ParameterKey=PrivateSubnet1,ParameterValue=subnet-0ba1ff7efe656281e \
ParameterKey=PrivateSubnet2,ParameterValue=subnet-0007df266992e3a06 \
ParameterKey=S3SourceCodeBucket,ParameterValue=$BUCKET_NAME \
ParameterKey=S3SourceCodeKey,ParameterValue=api.zip \
ParameterKey=DbHost,ParameterValue=db-postgresql-rqoopbocmcqq.cluster-chptakso6oan.us-east-1.rds.amazonaws.com \
ParameterKey=DbUser,ParameterValue=postgreAdmin \
ParameterKey=DbPassword,ParameterValue=SuperSecurePass123 \
ParameterKey=DbName,ParameterValue=mydb \
ParameterKey=AppLayerArn,ParameterValue=arn:aws:lambda:us-east-1:<アカウントID>:layer:python-lambda-application-template-lib-layer:1 \
ParameterKey=Psycopg3LayerArn,ParameterValue=arn:aws:lambda:us-east-1:<アカウントID>:layer:psycopg3-layer:1
デプロイが成功するとAPIエンドポイントが出力タブに表示されます。これを使って動作確認を行いましょう。

GETメソッドでhttps://<your API ID>.execute-api.us-east-1.amazonaws.com/dev/users?id=1 のようにユーザデータを取得するエンドポイントにPostmanなどでリクエストを実行しましょう。すると以下のようにデータベースに登録されたid=1のユーザーのデータが取得できることが分かります。(なお、実際のアプリでユーザIDにautoincrementされたIDを使うのはセキュリティ上避けましょう。攻撃者にIDが推測されやすくなるためです。アプリ側でuuidなどを生成するようにすることをお勧めします。)

次にユーザのデータをAPIから登録してみましょう。POSTメソッドでhttps://<your API ID>.execute-api.us-east-1.amazonaws.com/dev/users に以下のようなBodyパラメータを指定してリクエストを送って下さい。
{
"name": "horike",
"country": "Japan",
"age": "41"
}

pgAdminからSELECT文で確認すると正しくデータが出来ていることが分かります。

S3イベント + LambdaのCloudFormationスタックのデプロイ
このアプリケーションはユーザデータのCSVファイルをS3バケットにあげるとLambdaがトリガーされて、データベースに登録されるアプリです。本サンプルではsample_users.csvというファイル名でテスト用のCSVファイルを用意しています。
name,country,age
Taro,Japan,30
Alice,USA,28
Bob,UK,35
Maria,Germany,25
Li,China,32
デプロイ用のS3バケットへのパッケージのアップロード
APIを作るときに実行したbuild-package.shでこのアプリケーション用のソースコードパッケージも作成されています。同様にCloudFormationからアクセスできるように以下コマンドでパッケージをS3バケットにアップロードします。
[ec2-user ~]aws s3 cp .dist/app/sample_event/event.zip s3://$BUCKET_NAME
スタックのデプロイ
[ec2-user ~]aws cloudformation create-stack \
--region us-east-1 \
--stack-name s3-lambda \
--template-body file://deploy/cloudformation/s3-lambda.yaml \
--capabilities CAPABILITY_NAMED_IAM \
--parameters \
ParameterKey=AppName,ParameterValue=myapp \
ParameterKey=EnvName,ParameterValue=dev \
ParameterKey=VpcId,ParameterValue=vpc-01187493c944a7ff0 \
ParameterKey=PrivateSubnet1,ParameterValue=subnet-0ba1ff7efe656281e \
ParameterKey=PrivateSubnet2,ParameterValue=subnet-0007df266992e3a06 \
ParameterKey=S3SourceCodeBucket,ParameterValue=$BUCKET_NAME \
ParameterKey=S3SourceCodeKey,ParameterValue=event.zip \
ParameterKey=DbHost,ParameterValue=db-postgresql-rqoopbocmcqq.cluster-chptakso6oan.us-east-1.rds.amazonaws.com \
ParameterKey=DbUser,ParameterValue=postgreAdmin \
ParameterKey=DbPassword,ParameterValue=SuperSecurePass123 \
ParameterKey=DbName,ParameterValue=mydb \
ParameterKey=AppLayerArn,ParameterValue=arn:aws:lambda:us-east-1:<アカウントID>:layer:python-lambda-application-template-lib-layer:1 \
ParameterKey=Psycopg3LayerArn,ParameterValue=arn:aws:lambda:ap-northeast-1:<アカウントID>:layer:psycopg3-layer:1
デプロイが成功するとイベントがトリガーされるS3バケットが生成されます。ここにユーザデータのCSVをアップロードするとLambdaファンクションが起動してDBにデータが作成されます。

以下コマンドでテスト用のCSVをアップロードして、pgAdminで確認するとCSVのデータが正しくデータベースに格納されていることが確認できます。
[ec2-user ~]aws s3 cp ./src/sample_users.csv s3://myapp-dev-<アカウントID>-upload-csv

ローカル開発
ローカル開発は専用のpythonスクリプトと、PostgreSQLのコンテナ及びS3の互換ストレージであるminIOのコンテナを使って行います。まずはdocker-compose.yamlに定義されているこれらのコンテナを起動しましょう。
[ec2-user ~]docker compose up -d
Table作成
コンテナで起動したPostgreSQL内にデータベースとテーブルを作成します。以下のPythonスクリプトでデータベースとusersテーブルをコンテナ内に自動作成してくれます。
[ec2-user ~]poetry run python ./src/.init_db.py
psqlというPostgreSQLのコマンドラインツールもインストール済みなのでそれを使ってコンテナ内のテーブルの操作を行うことも可能です。ここでは3件データを作成しています。パスワードが求められますが、password と入力してください。
[ec2-user ~]psql -h localhost -U admin -d mydb
mydb=# INSERT INTO users (name, country, age) VALUES
('Alice', 'Japan', 30),
('Bob', 'USA', 25),
('Charlie', 'Germany', 35);
mydb=# exit #これで通常のターミナルに戻れます
APIのローカル実行
GETのAPI
.local.sample_api.get_users.py はGETメソッドでユーザデータを取得するAPIをシュミレートしたPythonスクリプトです。generate_http_api_request_payload という関数で擬似的にAPI Gatewayのイベントを生成しています。ファイル内の以下の箇所を変更するとAPIからクエリストリングでリクエストを送る内容を変えながら開発可能です。
QUERY_PARAMS = {
'id': '1'
}
[ec2-user ~]poetry run python ./src/.local.sample_api.get_users.py
API Execution Result: {"statusCode": 200, "body": "{\"statusCode\":200,\"body\":{\"name\":\"Alice\",\"country\":\"Japan\",\"age\":30}}", "isBase64Encoded": false, "headers": {"Content-Type": "application/json"}, "cookies": []}
実際にスクリプトを走らせるとAPIからのレスポンスが返ってきます。
POSTのAPI
.local.sample_api.post_users.py はPOSTメソッドでユーザを作成するAPIをシュミレートします。同様にgenerate_http_api_request_payload という関数で擬似的にAPI Gatewayのイベントを生成しています。今回は以下Bodyパラメータを操作しながら開発が可能です。
BODY = {
"name": "Shigeo",
"country": "Japan",
"age": 30
}
実際にスクリプトを実行すると正しくBodyに設定されたデータが作成されていることが分かります。
[ec2-user ~]poetry run python ./src/.local.sample_api.post_users.py
API Execution Result: {"statusCode": 200, "body": "{\"statusCode\":200,\"body\":{\"message\":\"The data created successfully.\"}}", "isBase64Encoded": false, "headers": {"Content-Type": "application/json"}, "cookies": []}
$ mydb=# SELECT * FROM users WHERE id=4;
id | name | country | age
----+--------+---------+-----
4 | Shigeo | Japan | 30
(1 row)
S3イベントのローカル実行
S3イベントではminIOというS3互換ストレージを使ってテストします。以下スクリプトを実行するとminIO上にtest-bucketというバケットを作成して、sample_users.csvをアップロードします。
[ec2-user ~]poetry run python ./src/.init_minio.py
minIOでは直接Lambdaをトリガーするところまでは再現できません。なので、.local.sample_event.import_users.py 内のgenerate_s3_request_payload でLambdaがトリガーされた時のイベントを生成し、ローカル実行を可能としています。スクリプトを実行するとsample_users.csvの内容がDB内に生成されていることが分かります。
[ec2-user ~]poetry run python ./src/.local.sample_event.import_users.py
mydb=# SELECT * FROM users;
id | name | country | age
----+---------+---------+-----
1 | Alice | Japan | 30
2 | Bob | USA | 25
3 | Charlie | Germany | 35
4 | Shigeo | Japan | 30
5 | Taro | Japan | 30
6 | Alice | USA | 28
7 | Bob | UK | 35
8 | Maria | Germany | 25
9 | Li | China | 32
ユニットテスト
ユニットテストもサンプルの実装を提供しています。pytestを使って、testsディレクトリの配下にテストコードが配置されています。ユニットテストも同様にPostgreSQLとminIOのコンテナを使って、バリデーションのパターンや正常系のパターンなど必要と思われるテストケースを実装しています。
テストは以下のコマンドで実行できます。
[ec2-user ~]poetry run pytest