はじめに
こちらはQiita Advent Calendar AWS Lambda と Serverless 21日目の記事です。
サーバレスで外部からAPIを呼び出したい時、API Gateway + Lambdaは鉄板構成です。
ただ、Lambdaの処理時間が長いとAPI Gatewayのタイムアウトの最大である29秒(RESTの場合)を超えてしまい、APIのリクエストが失敗してしまいます。
そのため、API側でLambdaを突き放して実行する構成が取られますが、今回それを実現するAWS Serverless Application Model(AWS SAM)のテンプレートについて主に説明します。
管理コンソールから上記実現する方法はググればたくさん見つかります。
SAMについては情報が少なかったため、記載することにしました。
実現したかったこと
当時(2年前ぐらい?)、比較的小規模の会社様の請求システムの構築のプロジェクトに参画しました。
お客様からの要望は、以下のようなものでした。
- 請求書の作成と請求先へのメール送信を自動化したい
- メール送信まで終わったら通知してほしい
AWSでシステムを構築することはすでに決まっており、運用コストの上限もなぜかすでに決まっていました。
このような状況下でシステム設計を担当し、結果として以下の構成でシステムを構築することに決めました。
色々省略している構成図ですが、API Gateway + Lambda、かつ、Lambdaは非同期で実行させます。
作ったSAMテンプレート
早速ですが、非同期でLambdaをAPI Gatewayから実行するSAMテンプレートは以下です。
重要な箇所、詰まった箇所についてこの後説明します。
AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Resources:
SampleLambda:
Type: AWS::Serverless::Function
Properties:
PackageType: Zip
Runtime: python3.11
CodeUri: src/sampleLambda
Handler: app.lambda_handler
Timeout: 300
Architectures:
- x86_64
FunctionName: sampleLambdaFunction
Policies:
- AWSLambdaBasicExecutionRole
Events:
sampleLambda:
Type: Api
Properties:
Path: /sampleLambda
Method: get
RestApiId: !Ref SampleApi
Metadata:
Dockerfile: Dockerfile
DockerContext: ./src/sampleLambda
DockerTag: prod
SampleApi:
Type: AWS::Serverless::Api
Properties:
EndpointConfiguration:
Type: REGIONAL
Name: sampleApi
StageName: Prod
DefinitionBody:
swagger: "2.0"
schemes:
- "https"
paths:
/sampleLambda:
get:
responses:
"202":
description: 202 response
schema:
$ref: "#/definitions/Empty"
x-amazon-apigateway-request-validator: all
x-amazon-apigateway-integration:
requestParameters:
"integration.request.header.X-Amz-Invocation-Type": "'Event'"
httpMethod: POST
type: aws
passthroughBehavior: "when_no_match"
responses:
default:
statusCode: "202"
uri:
!Join [
"",
[
"arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/",
!GetAtt SampleLambda.Arn,
"/invocations",
],
]
StageName: Prod
x-amazon-apigateway-policy:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal: "*"
Action: execute-api:Invoke
Resource:
- "execute-api:/*"
definitions:
Empty:
type: object
title: Empty Schema
x-amazon-apigateway-integration.requestParameters
「Amazon API Gateway API から非同期に Lambda 関数を呼び出すにはどうすればよいですか?」の記載の通り、API Gatewayから非同期でLambdaを呼び出すには統合リクエストを選択し、HTTPヘッダーX-Amz-Invocation-Type
を追加し、値に'Event'
を指定すればよいです。
SAMでの統合リクエストの設定はOpenAPIの拡張x-amazon-apigateway-integration
に定義します。
SAMのプロパティであるDefinitionBodyを使用しているため、上記OpenAPI仕様が使用できるわけです。
x-amazon-apigateway-integration.requestParametersで、統合リクエストのヘッダーやクエリがプロパティとして定義でき、ここに非同期起動を指示するHTTPヘッダーを指定します。
x-amazon-apigateway-integration:
# ↓ここ↓
requestParameters:
"integration.request.header.X-Amz-Invocation-Type": "'Event'"
x-amazon-apigateway-integration.httpMethod
詰まった箇所について紹介します。
APIをリクエストするとAccessDeniedException: Unable to determine service/operation name to be authorized
というエラーが発生し、Lambda関数が起動できないという事象が発生しました。
調べてみるとStackoverflowに対処方法を書いてくださっている方がいました。
I think you are using "GET" for your Lambda function endpoint on your GET method as well. Please change it to use "POST" for the Lambda integration HTTP method.
Lambda関数を起動させるためには統合リクエストのメソッドをPOST
にしなければならないようです。
AWS公式ドキュメントにも同様のことが記載されていました。
当時、統合リクエストのメソッドが何であるのか特に気にせずAPIのHTTPメソッドと同じGET
にしていたため、本事象が発生してしまいました。
統合リクエストについて、AWS公式ドキュメントから抜粋します。
統合リクエストは、API Gateway がバックエンドに送る HTTP リクエストであり、クライアントから送信されたリクエストデータを渡し、必要に応じて変換します。
統合リクエストの HTTP メソッド (つまり動詞) と URI は、バックエンド (つまり統合エンドポイント) が指定します。
HTTP メソッドと URI はそれぞれ、メソッドリクエストのものと同じ場合もあれば異なる場合もあります。
統合リクエストのメソッドはバックエンドのAWSサービスを呼び出す際に使われるものであり、APIのHTTPメソッドと異なってもよいものということです。
今回バックエンドはLambdaを呼び出します。
LambdaのAPIリファレンスを確認したところ、HTTPメソッドはPOST
であり、非同期呼び出しの場合、X-Amz-Invocation-Type
ヘッダーにEvent
を指定しろと記載されていました。
これら設定値が統合リクエストから渡されるため、Lambdaが非同期で呼び出されるのだなと理解することができました。
x-amazon-apigateway-integration:
requestParameters:
"integration.request.header.X-Amz-Invocation-Type": "'Event'"
# ↓ここ↓
httpMethod: POST
実際に試す
SAMテンプレートについて説明は終わりです。実際にリソースを構築し動かしてみます。
なお、実行するLambda関数を以下に示します。処理が開始して、30秒経ったら終了するものです。
import json
import time
def lambda_handler(event, context):
print("処理を開始")
time.sleep(30)
print("30秒経過しました。")
return {
'statusCode': 200,
'body': json.dumps('OK')
}
API Gatewayのテスト機能からAPIを実行します。
ログの時間に注目してください。1秒もかからずレスポンスが返ってきていることがわかります。
Lambda関数の実行結果をCloudWatch Logsから確認します。
こちらも時間に注目していただくと、ちゃんと30秒経過してから処理を終了しています。
APIはLambdaの処理時間に引きずられずレスポンスを返却できていることがわかりました。
おわりに
最後まで読んでいただきありがとうございました。
テーマがLambdaとサーバレスなのにAPI Gatewayが中心の内容となってしまったことに執筆途中で気がついてしまいました。(モウシワケナイ)
ただ、私にとって今回取り上げたシステムは本格的にAWSを触り始めた時のものだったので、今回書く機会ができてよかったです。
この記事が誰かのお役に立てば嬉しいです。