この記事を2行で
- S3の署名付きURLと同じことが、Lambdaの関数URLにもできます
- Lambdaの署名付きURLを使えば、APIGatewayの29秒タイムアウトを超えてLambdaを実行できます
何が嬉しいの?
- AIのように時間のかかる処理を、Lambdaだけで処理できます
- 署名付きURLなので、Cognito + APIGatewayのような認証がそのまま使えます
どうやって?
- Lambdaの関数URLは
API Gatewayの29秒タイムアウト制限の影響を受けない
- S3の署名付きURLと同じ認証方法が、Lambdaの関数URLでも使える
- ※サービスはLambdaだけ、ライブラリもboto3の標準機能だけで実現できます
このあたりの事情を説明(APIGatewayのタイムアウト制限とは?)
AWSのLambdaは、15分かかる処理までは受け付けることができます。
ただ、APIGatewayは、レスポンスを返すのに29秒以上かかるリクエストを強制的に中断する仕様になっています。
Lambdaは何しろ安価で、運用も楽で、AIの推論を動かせるだけのスペックがあるのですが、サービスとして運用するときの認証回りはAPIGatewayに依存しています。APIGatewayの認証を使ったまま、簡単に29秒の壁を超えることはできないかと、色々な方法が検討されてきました。
※イメージ図
使う関数
from botocore.auth import SigV4QueryAuth
boto3が使える環境なら無条件で利用できます。
Googleで検索しても全く情報が出ないので、細かいところはコードを直接読んで調べる必要があります。
Lambdaの署名付きURLを発行する
前準備をする:リクエストを受け取るLambdaを作る
このようなLambdaを作成して、関数URLを発行しました。
import json
def lambda_handler(event, context):
user = event["queryStringParameters"]["user"]
return {
'statusCode': 200,
'body': json.dumps(f'Hello {user} from Lambda!')
}
認証タイプはAWS_IAM
です。
署名付きURLを発行するソース
以下の関数(presign_for_lambda)を定義して、署名付きURLを作成します。
関数URLは対象の関数URLに置き換えてください。
from botocore.auth import SigV4QueryAuth
from botocore.awsrequest import AWSRequest
from botocore.credentials import create_credential_resolver
from botocore.session import Session
def presign_for_lambda(url: str, body: str):
"""
Lambdaの署名付きURLを発行する
"""
# 定数(変更しても反映されない値、変更しないほうがいい値)
SERVICE_NAME = "lambda" # サービス名はLambdaで固定
METHOD = "GET" # bodyはクエリ文字列に変換されるので、GET固定でよい
EXPIRES = 300 # レスポンスを見ると、認証の有効期限は5分で固定される
# botocoreのセッションを取得する
session = Session()
# 今の環境に設定されている認証情報を取得する
resolver = create_credential_resolver(session)
credentials = resolver.load_credentials()
# リクエストに対して署名する
request = AWSRequest(method=METHOD, url=url, data=body)
SigV4QueryAuth(
credentials=credentials,
service_name=SERVICE_NAME,
region_name=session.get_config_variable("region"),
expires=EXPIRES,
).add_auth(request)
return request.url
### -----------------------
# 呼び出し部分
### -----------------------
import json
# Lambdaの関数URL(末尾のスラッシュは不要です)
URL = "https://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.lambda-url.ap-northeast-1.on.aws"
result = presign_for_lambda(
URL,
# Lambdaに送信するペイロード
json.dumps({"user": "Bjarne Stroustrup"}),
)
print(result)
上のファイルをapp.pyとして保存して、pythonで実行すると、長い署名付きURLが出力されると思います
$ python app.py
https://xxxxxxxxxxxxx...ずらずら長いURL...
curlに署名付きURLを渡してみると、Lambdaの実行結果が返ってきました。
$ curl `python app.py`
"Hello Bjarne Stroustrup from Lambda!"
渡したパラメータ(user)も正しく解析されています。
署名付きURLを調べてみる
発行された署名付きURLは以下のようになっています。
https://xxxxxxxxxxxxxxxxxxxxxxxxxx.lambda-url.ap-northeast-1.on.aws?
user=Bjarne%20Stroustrup&
X-Amz-Algorithm=AWS4-HMAC-SHA256&
X-Amz-Credential=AKIAxxxxxxxxxxxxxxxx%2F20230805%2Fap-northeast-1%2Flambda%2Faws4_request&
X-Amz-Date=20230805T042931Z&
X-Amz-Expires=300&
X-Amz-SignedHeaders=host&
X-Amz-Signature=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
リクエストの送信先はLambdaの関数URLです。
それはそうだろうとも言えますが、S3の署名付きURLと全く同じフォーマットです。
※JSON形式で渡したペイロードがクエリ文字列の中に入るので、そこだけ違います
検証:ペイロードを改ざんしてみる
署名付きURLのクエリ文字列を書き変えて、user=Bjarne%20Stroustrup
を、user=Alan%20Curtis%20Kay
に改ざんしてみます。リクエストを投げると、以下のようなレスポンスが返ってきます。
{"message":"The request signature we calculated does not match the signature you provided. Check your AWS Secret Access Key and signing method. Consult the service documentation for details."}
Signatureが、クエリ文字列、認証情報、送信先のホスト、日時を検証しています。一つでも改ざんされていると、リクエストは通らなくなります。
検証:署名付きURLに有効期限切れのリクエストを投げてみる
署名付きURLの有効期限は発行から5分です。
※SigV4QueryAuthのExpiresに何を設定しても反映されません。5分固定です。
{"message":"Signature expired: 20230805T041907Z is now earlier than 20230805T042511Z (20230805T043011Z - 5 min.)"}
# Signature expired: ${署名の発行時間} is now earlier than ${現在時間 の 5分前}
# 「署名された時間は、現在時間の5分前よりも古い」
# のメッセージで弾かれる。
#
# Expiresをどの数字(300にしても3600にしても12000000にしても)同じメッセージが返る
期限の切れたリクエストは通りません。
検証:認証なしでリクエストを投げてみる
認証情報をつけずにリクエストをすると、Forbiddenが返ります。
{"Message":"Forbidden"}
検証:POSTボディでJSONのままペイロードを渡せないの?
はい、SigV4QueryAuthを使う限りはGETのクエリ文字列でしかデータを渡せません。botocoreのソースに、data部(ペイロード)を全てクエリに置き換えて、元のdata部のペイロードを削除する処理が入っています。
SigV4QueryAuthではなくSigV4Authを使うとbodyのまま渡すこともできるのですが、認証情報がヘッダ部に入るので、若干面倒です。
operation_params = ''
if request.data:
# We also need to move the body params into the query string. To
# do this, we first have to convert it to a dict.
query_dict.update(_get_body_as_dict(request))
request.data = ''
本題:LambdaでLambdaの署名付きURLを発行する
ここからが本題です。
LambdaでLambdaの署名付きURLを発行して、リクエストさせます。
構成図は以下の通りです。
AWSの資料でよく見る構成図そのままですが、S3の場所がLambdaになります。
実装
署名付きURLを発行するLambdaを新規作成します。
※上で紹介したソースとほぼ同じソースですので、折り畳みで紹介します。
botocoreはLambdaに最初から入っているため、依存ライブラリは不要です。
Lambdaのソースコード
from botocore.auth import SigV4QueryAuth
from botocore.awsrequest import AWSRequest
from botocore.credentials import create_credential_resolver
from botocore.session import Session
import json
def presign_for_lambda(url: str, body: str):
"""
Lambdaの署名付きURLを発行する
"""
# 定数(変更しても反映されない値、変更しないほうがいい値)
SERVICE_NAME = "lambda" # サービス名はLambdaで固定
METHOD = "GET" # bodyはクエリ文字列に変換されるので、GET固定でよい
EXPIRES = 300 # レスポンスを見ると、認証の有効期限は5分で固定されている
# botocoreのセッションを取得する
session = Session()
# 今の環境に設定されている認証情報を取得する
resolver = create_credential_resolver(session)
credentials = resolver.load_credentials()
# リクエストに対して署名する
request = AWSRequest(method=METHOD, url=url, data=body)
SigV4QueryAuth(
credentials=credentials,
service_name=SERVICE_NAME,
region_name=session.get_config_variable("region"),
expires=EXPIRES,
).add_auth(request)
return request.url
URL = "https://xxxxxxxxxxxxxxxxxxxxxx.lambda-url.ap-northeast-1.on.aws"
def lambda_handler(event, context):
result = presign_for_lambda(
URL,
json.dumps({"user": "Bjarne Stroustrup"}),
)
return {
'statusCode': 200,
'body': json.dumps({"url": result})
}
Lambdaの実行ロールを編集、関数URLの実行権限をつける
Lambdaにlambda:InvokeFunctionUrl
の権限を付けます。
実行してみる
作ったLambdaを実行すると、以下のようなレスポンスが返ってきたと思います。
Lambdaのロールで発行するとX-Amz-Tokenがつくため、署名付きURLは長くなります。
{"statusCode": 200, "body": "{\\"url\\": \\"https://xxxxxxxxxxxxxxxxxxxxxxx.lambda-url.ap-northeast-1.on.aws?user=Bjarne%20Stroustrup&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=ASIXXXXXXXXXXXXX%2F20230805%2Fap-northeast-1%2Flambda%2Faws4_request&X-Amz-Date=20230805T054557Z&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Security-Token=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX&X-Amz-Signature=37cdf421c249c4b3610a9b9135823417ba2871fcb376080af4cb69d11997c0cb\\"}"}
Lambdaから受け取ったurlにリクエストを投げると、正しくレスポンスが返ってきます。
"Hello Bjarne Stroustrup from Lambda!"
29秒の壁を越えてみる
関数URLを発行しているLambdaを、以下の通り編集します
- タイムアウト時間を2分に変更します
- sleepを入れて、1分処理を待つようにします
実行結果
署名付きURLでcurlを投げると、1分後に正しく結果が返ってきました。
もちろんエラーはありません。
CloudWatch Logsのログを見ても、60002 ms(60秒)かかっていることが確認できます。
まとめ
Lambdaの関数URLは、S3の署名付きURLと同じフォーマットで署名ができます。仕組みも実装方法もS3の署名付きURLとほぼ同じです。署名付きURLを取得して、fetch("署名付きURL")をリクエストするだけでよいので、画面側の負担も少なくて済みます。
AIでリクエスト時間が延びる昨今、ぜひ関数URLを積極的に使っていただければと思います。
付録(説明とか)
Lambdaの関数URLとは?
APIGatewayを挟まずに、Lambdaを直接実行できるようにする機能です。
※2022年の4月に公開された機能です
署名付きURLとは?
権限を持っているユーザーや環境が、あらかじめ自身の権限でAWSリクエストに署名しておく機能です
署名されたAWSリクエストは、権限のないユーザーや環境が実行することができます
ただ、好きなような実行できるわけではなく、制限があります。
- 署名は時間が経つと失効する
- 送信する内容(URLクエリ、POSTで送るbody、ヘッダなど)と、データの送信先は、署名した時点から変更できない。※ただしファイルを送信するリクエストについては、ファイルの内容は送信時に決められる
委譲することで、タイミングと実行場所だけが動かせるようなイメージです。「署名付きURL=S3のサービス」だと思われがちですが、Cloudfront、Lambda、SQSなどでも署名付きURLを使うことができます。