👽

Terraform で ECR + Lambda + API Gateway 構築し、FastAPI をサーバーレスにデプロイ

に公開

はじめに

本記事は前回の記事「Terraform で Docker イメージを Lambda にデプロイする」 の続編です。

↓前回の記事
https://zenn.dev/fuuji/articles/547388be4ca9ce#ローカルで関数をテストする

今回は Terraform で API Gatewayを追加し、Lambda + API Gateway構成でFastAPIを公開するところまでをハンズオン形式で紹介します。

構成イメージ

  • API Gateway
    • インターネットからのリクエストを受け取り、Lambdaにリクエストをプロキシ
  • ECR・Lambda
    • 前回と同様にDockerイメージを使用した構成です

実際に使用したコードや構成ファイルは以下の GitHub リポジトリで公開しています。
https://github.com/anton-fuji/terraform_lambda_apigateway

FastAPIでエンドポイントを作成

まずは、REST API の実装部分となるPythonファイルを準備します。
今回は api ディレクトリ配下に以下のファイルを作成します。

api/
├── app.py             # FastAPI でエンドポイントを作成
├── Dockerfile         # Lambda ランタイムの Docker イメージを構築
└── requirements.txt   # 使用する Python ライブラリを指定

では順に解説していきます。

app.pyについて

以下は、FastAPI と Mangum を使用してLambda上で実行可能なAPIを定義しています。

app.py
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

    • 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です。

Dockerfile
FROM --platform=linux/amd64 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から依存パッケージをインストールすることです。
これにより、FastAPIMangumなど、アプリケーションに必要なパッケージを Lambda環境で利用できるようになります。

requirements.txtについて

このファイルには、使用するPythonライブラリを指定します。
今回はFastAPIMangumを記述しておきましょう。

requirements.txt
fastapi==0.95.2
Mangum==0.15.0

Terraform ファイルの作成

前回の api.tf に以下の内容を追加してください。

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セキュリティ認証認可 を意識しつつ、さらにブラッシュアップしていきたいと思います。

最後まで読んでいただき、ありがとうございます!

参考

https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/apigatewayv2_vpc_link

https://qiita.com/free-honda/items/8014807dafcab622c961

脚注
  1. ASGI :FastAPIやStarletteなどのPython Webサーバーやアプリケーションが非同期で通信するための標準インターフェース ↩︎

Discussion