Terraform で ECR + Lambda + API Gateway 構築し、FastAPI をサーバーレスにデプロイ
はじめに
本記事は前回の記事「Terraform で Docker イメージを Lambda にデプロイする」 の続編です。
↓前回の記事
今回は Terraform
で API Gatewayを追加し、Lambda + API Gateway構成でFastAPIを公開するところまでをハンズオン形式で紹介します。
構成イメージ
- API Gateway
- インターネットからのリクエストを受け取り、Lambdaにリクエストをプロキシ
- ECR・Lambda
- 前回と同様にDockerイメージを使用した構成です
実際に使用したコードや構成ファイルは以下の GitHub リポジトリで公開しています。
FastAPIでエンドポイントを作成
まずは、REST API の実装部分となるPythonファイルを準備します。
今回は api
ディレクトリ配下に以下のファイルを作成します。
api/
├── app.py # FastAPI でエンドポイントを作成
├── Dockerfile # Lambda ランタイムの Docker イメージを構築
└── requirements.txt # 使用する Python ライブラリを指定
では順に解説していきます。
app.pyについて
以下は、FastAPI と Mangum
を使用してLambda上で実行可能なAPIを定義しています。
from fastapi import FastAPI
from mangum import Mangum
app = FastAPI()
@app.get("/")
async def home():
return {"message": "Hello, Lambda!"}
@app.get("/testing")
async def testing():
return {"message": "Testing, Lambda!"}
handler = Mangum(app)
-
/
- ホームエンドポイント
-
{"message": "Hello, Lambda!"}
を返します
-
/testing
- テスト用のエンドポイント
-
{"message": "Testing, Lambda!"}
を返します
-
-
Mangum
は、ASGI[1]ベースのAPI(FastAPI)を Lambda + API Gateway を中継してくれるアダプタです - FastAPI製APIを手軽にLambda + API Gateway で公開できます
- 最終行の
handler = Mangum(app)
を加えることで、Mangum
がFastAPIのインスタンスをAPI Gatewayと統合できるLambdaのhandlerに変換してくれます
-
Dockerfileについて
以下は、Lambdaランタイムをベースとした Dockerfileです。
FROM public.ecr.aws/lambda/python:3.13
COPY requirements.txt ${LAMBDA_TASK_ROOT}
RUN pip install --no-cache-dir -r requirements.txt
COPY . ${LAMBDA_TASK_ROOT}
CMD [ "app.handler" ]
前回の Dockerfileとの明確な違いは requirements.txtから依存パッケージをインストールすることです。
これにより、FastAPI
や Mangum
など、アプリケーションに必要なパッケージを Lambda環境で利用できるようになります。
requirements.txtについて
このファイルには、使用するPythonライブラリを指定します。
今回はFastAPI
と Mangum
を記述しておきましょう。
fastapi==0.95.2
Mangum==0.15.0
Terraform ファイルの作成
前回の api.tf
に以下の内容を追加してください。
## 続き、、
resource "aws_apigatewayv2_api" "lambda" {
name = "lambda-api-gateway"
protocol_type = "HTTP"
}
resource "aws_apigatewayv2_stage" "lambda" {
api_id = aws_apigatewayv2_api.lambda.id
name = "$default"
auto_deploy = true
access_log_settings {
destination_arn = aws_cloudwatch_log_group.api_gw.arn
format = jsonencode({
requestId = "$context.requestId"
sourceIp = "$context.identity.sourceIp"
requestTime = "$context.requestTime"
protocol = "$context.protocol"
httpMethod = "$context.httpMethod"
resourcePath = "$context.resourcePath"
routeKey = "$context.routeKey"
status = "$context.status"
responseLength = "$context.responseLength"
integrationErrorMessage = "$context.integrationErrorMessage"
}
)
}
}
resource "aws_cloudwatch_log_group" "api_gw" {
name = "/aws/api_gw/${aws_apigatewayv2_api.lambda.name}"
retention_in_days = 30
}
resource "aws_apigatewayv2_integration" "lambda" {
api_id = aws_apigatewayv2_api.lambda.id
integration_uri = module.lambda_function.lambda_function_arn
integration_type = "AWS_PROXY"
payload_format_version = "2.0"
}
resource "aws_apigatewayv2_route" "lambda_root" {
api_id = aws_apigatewayv2_api.lambda.id
route_key = "ANY /"
target = "integrations/${aws_apigatewayv2_integration.lambda.id}"
}
resource "aws_apigatewayv2_route" "lambda_proxy" {
api_id = aws_apigatewayv2_api.lambda.id
route_key = "ANY /{proxy+}"
target = "integrations/${aws_apigatewayv2_integration.lambda.id}"
}
resource "aws_lambda_permission" "api_gw" {
statement_id = "AllowExecutionFromAPIGateway"
action = "lambda:InvokeFunction"
function_name = module.lambda_function.lambda_function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_apigatewayv2_api.lambda.execution_arn}/*/*"
}
output "api_gateway_url" {
value = aws_apigatewayv2_stage.lambda.invoke_url
}
API Gateway の各リソースについてまとめていきます。
HTTP API
resource "aws_apigatewayv2_api" "lambda" {
name = "lambda-api-gateway"
protocol_type = "HTTP"
}
- Lambda 関数をインターネット経由で公開するためのエントリーポイント
-
name
- API Gatewayの名称を指定できます
- 今回は
lambda-api-gateway
ステージ設定
resource "aws_apigatewayv2_stage" "lambda" {
api_id = aws_apigatewayv2_api.lambda.id
name = "$default"
auto_deploy = true
access_log_settings {
destination_arn = aws_cloudwatch_log_group.api_gw.arn
format = jsonencode({
requestId = "$context.requestId"
sourceIp = "$context.identity.sourceIp"
requestTime = "$context.requestTime"
protocol = "$context.protocol"
httpMethod = "$context.httpMethod"
resourcePath = "$context.resourcePath"
routeKey = "$context.routeKey"
status = "$context.status"
responseLength = "$context.responseLength"
integrationErrorMessage = "$context.integrationErrorMessage"
}
)
}
}
-
$default
- API Gateway はデフォルトステージで動作し、設定変更時は自動デプロイ
-
access_log_settings
- 今回はCloudWatchにロググループを作成し、APIリクエスト/レスポンスの詳細ログを記録します
CloudWatch Log Group
resource "aws_cloudwatch_log_group" "api_gw" {
name = "/aws/api_gw/${aws_apigatewayv2_api.lambda.name}"
retention_in_days = 30
}
- API GatewayのログをCloudWatch上で
name
で指定したパスに30日間保存するように設定 - ここに保存されているログを元に作業を進めることで効率的なデバッグが可能です
Lambda 統合
resource "aws_apigatewayv2_integration" "lambda" {
api_id = aws_apigatewayv2_api.lambda.id
integration_uri = module.lambda_function.lambda_function_arn
integration_type = "AWS_PROXY"
payload_format_version = "2.0"
}
-
integration_type = "AWS_PROXY"
- Lambda に直接リクエストをプロキシ(API Gateway → Lambda)
-
integration_uri
- ここで指定している module.lambda_function.lambda_function_arn は、既に定義済みのLambda関数のARNを指定します
ルート (ANY /, ANY /{proxy+})
resource "aws_apigatewayv2_route" "lambda_proxy" {
api_id = aws_apigatewayv2_api.lambda.id
route_key = "ANY /{proxy+}"
target = "integrations/${aws_apigatewayv2_integration.lambda.id}"
}
-
route_key = "ANY /{proxy+}"
- プロキシパターンで任意のパスをLambdaに転送
Lambda Permission
resource "aws_lambda_permission" "api_gw" {
statement_id = "AllowExecutionFromAPIGateway"
action = "lambda:InvokeFunction"
function_name = module.lambda_function.lambda_function_name
principal = "apigateway.amazonaws.com"
source_arn = "${aws_apigatewayv2_api.lambda.execution_arn}/*/*"
}
- API Gateway が Lambda を実行できる権限を付与
- apigateway.amazonaws.com からのリクエストのみ許可
- source_arn で API Gateway のリクエストに限定
API Gateway URL
output "api_gateway_url" {
value = aws_apigatewayv2_stage.lambda.invoke_url
}
- API Gatewayの公開URLを出力し、デプロイ後にすぐ確認できます
デプロイしていきましょう!
以下のコマンドでデプロイ作業を進めてください。
terraform init
terraform apply --auto-approve
Applyが完了すると以下のURLが出力されるので、ブラウザからアクセスしてみましょう。
Apply complete! Resources: 14 added, 0 changed, 0 destroyed.
Outputs:
api_gateway_url = "https://t9qfbmy874.execute-api.ap-northeast-1.amazonaws.com/"
まずは、https://t9qfbmy874.execute-api.ap-northeast-1.amazonaws.com/
にアクセスし、{"message": "Hello, Lambda!"}
が表示されたらOKです!
次は、https://t9qfbmy874.execute-api.ap-northeast-1.amazonaws.com/testing
にアクセスし、{"message": "Testing, Lambda!"}
表示されたら検証終了です!
最後に、全てのリソースを削除しておきましょう。
terraform destroy --auto-approve
# こちらが出力されればOKです
Destroy complete! Resources: 14 destroyed.
さいごに
今回は、ECRからDockerイメージをLambdaがプルし、それをAPI Gatewayを通じてHTTPリクエストを受け取り、レスポンスを返すREST APIを提供する構成を構築しました。
この構成はシンプルながらも柔軟で、ハッカソンや個人開発で手軽に活用できると感じています。
ただ、実際に運用する場合はAPIセキュリティ・認証・認可 を意識しつつ、さらにブラッシュアップしていきたいと思います。
最後まで読んでいただき、ありがとうございます!
参考
Discussion