この広告は、90日以上更新していないブログに表示しています。
おいおいタイトル長すぎだろ……常識的に考えて。
今回はだいぶ実用性重視の作例を紹介しようと思って、言いたいことを全部タイトルに盛り込もうとしたらちょっと長くなっちゃった。
AI にタイトルつけてもらえばよかったのに。
それな。
だいぶ前の話ですが、AWS Lambda 単体で WebAPI が作れてしまう関数URLという機能が登場しました。
この機能を有効化すると、下図に示すように HTTP(S) リクエストを契機に直接 Lambda 関数を実行できるようになります(APIGateway が不要になります)。
リクエストがAPIGateway を通らないため、「レスポンスタイムが29秒を超えてもタイムアウトにならない」という大きなメリットを享受できます。
AI による推論のように、時間のかかる処理をAPI 化したい場合には有効と言えるでしょう。
一方で、関数URL を有効化した Lambda 関数は、APIGateway のような高度なオーソライザを具備しているわけではありません。 せいぜい、IAMロールによる認可を設定できるくらいです。
そのため、「API を保護する手段が少ない」という切実なデメリットとも向き合う必要があります。
どういうこと?
まぁ要するに、「ログイン中のユーザだけがアクセスできるようにしたい」みたいな要件を素直に満たせないんだよ。
なるほどそれは不便だね。
ただ、このあと示す手法を使えば、この課題をテクニカルに解決できるのだ。
まず結論から言うと、下図のように「❶ 一時権限を払い出すAPI」と「❷ 関数URLを有効化したLambda」の2階建てを作ります。
そして、❷の Lambda 関数は、❶のAPI で払い出した一時権限がない限りアクセスできないよう、IAM ロールによる認可制にします。
❶ は、前段にAPIGateway があるため、オーソライザやAPI キーなどを用いてAPI を保護できます。
この方法を使えば、29秒を超えるAPI をセキュアに呼び出す仕組みを実現できるでしょう。
理屈は解ったけど、作るべきリソースと依存関係がややこしいね。
そうだね。
手動でポチポチ作ると、どこかで手順を間違えたときに悲惨なことになるので、IaC を使って構築することにしよう。
AWS SAMだね。
うん。
これでタイトルの伏線は全回収だ。
まずは最も簡単な、「関数 URL を有効化した Lambda」をAWS SAM で作ります。 下図の赤枠相当のリソースになります。
template.yaml
AWSTemplateFormatVersion:'2010-09-09'Transform: AWS::Serverless-2016-10-31Description: > Sample SAM TemplateGlobals:Function:Timeout:3Resources:HelloWorldFunction:Type: AWS::Serverless::FunctionProperties:CodeUri: hello_world/Handler: app.lambda_handlerRuntime: python3.12Architectures:-x86_64FunctionUrlConfig: # ★追加 ── 関数URLを有効化AuthType: NONE # ★追加 ── 認証なしOutputs:HelloWorldFunctionUrl:Value:!GetAtt HelloWorldFunctionUrl.FunctionUrl # ★追加 ── Lambdaの関数URL
次に、Lambda 関数本体のコードを示します。
こちらは、単に 200 応答とHello World メッセージを返すだけのごく簡単な関数です。
AWS SAMCLI で生成される一番簡単な Lambda と同じものですので、特に説明は不要でしょう。
hello_world/app.py
import jsondeflambda_handler(event, context):return {"statusCode":200,"body": json.dumps({"message":"hello world", }), }
続いて、今作った Lambda を、CORS 対応させます。 template.yaml を以下のように修正しましょう(app.py の修正は不要です)。
template.yaml(第2版)
AWSTemplateFormatVersion:'2010-09-09'Transform: AWS::Serverless-2016-10-31Description: > Sample SAM TemplateGlobals:Function:Timeout:3Resources:HelloWorldFunction:Type: AWS::Serverless::FunctionProperties:CodeUri: hello_world/Handler: app.lambda_handlerRuntime: python3.12Architectures:-x86_64FunctionUrlConfig:AuthType: NONECors: # ★追加 ── CORS対応AllowOrigins: # ★追加 ── 〃-"*" # ★追加 ── 〃AllowCredentials:true # ★追加 ── 〃AllowMethods: # ★追加 ── 〃-"*" # ★追加 ── 〃AllowHeaders: # ★追加 ── 〃-"*" # ★追加 ── 〃Outputs:HelloWorldFunctionUrl:Value:!GetAtt HelloWorldFunctionUrl.FunctionUrl
これで、オリジンの異なるクライアントからAPI 呼び出しができるようになりました。
このままでは、関数URLを知ってさえいれば、世界中の誰でもその Lambda を呼び出せてしまいます。
そのため、まずは実行権限を持たないクライアントから Lambda が呼び出せないよう、IAM ロール認可を設定しましょう。
また、Lambda の呼び出しに必要となる IAMロールも併せて作成します。 このロールは、どこかの誰かに対して恒久的に権限を与える使い方ではなく、一時的に権限を貸し出す使い方(AssumeRole)をします。
ただし、誰でも AssumeRole ができてしまうと、(結局、誰もがアクセス権を得ることになってしまい)事実上、認証の意味が無くなってしまいますので、権限の貸し出し先を絞るために信頼関係ポリシーを設定します。
下図の赤枠部に相当する箇所となります。
template.yaml(第3版)
AWSTemplateFormatVersion:'2010-09-09'Transform: AWS::Serverless-2016-10-31Description: > Sample SAM TemplateGlobals:Function:Timeout:3Resources:HelloWorldFunction: # ─────────────────────────────────┐Type: AWS::Serverless::Function # │Properties: # │CodeUri: hello_world/ # │Handler: app.lambda_handler # │Runtime: python3.12 # │Architectures: # │-x86_64 # │FunctionUrlConfig: # │AuthType: AWS_IAM # ★修正 ── IAM認可を有効化 │Cors: # │AllowOrigins: # │-"*" # │AllowCredentials:true # │AllowMethods: # │-"*" # │AllowHeaders: # │-"*" # │# │# ★ 追加ここから ★ │# │# ① Lambdaを関数URLで実行するIAMポリシー │LambdaFunctionInvokePolicy: # │Type: AWS::IAM::Policy # │Properties: # │PolicyName: hello-world-invoke-policy # │PolicyDocument: # │Version:'2012-10-17' # │Statement: # │-Effect: Allow # │Action: lambda:InvokeFunctionUrl # │Resource:!GetAtt HelloWorldFunction.Arn # ←─┘Roles:-!Ref LambdaFunctionInvokeRole # ─┐# │# ② 上記①のポリシーをアタッチしたIAMロール │# 信頼関係ポリシーを設定しているため、 |# 一時的にこのロールを引き渡すことができる │LambdaFunctionInvokeRole: # ←────────────┘Type: AWS::IAM::RoleProperties:AssumeRolePolicyDocument:Version:'2012-10-17'Statement:-Effect: AllowPrincipal:AWS:!Sub"arn:aws:iam::${AWS::AccountId}:root"Action: sts:AssumeRole## ★ 追加ここまで ★#Outputs:HelloWorldFunctionUrl:Value:!GetAtt HelloWorldFunctionUrl.FunctionUrl
いよいよバックエンドの実装も大詰めです。 最後に、一時権限を払い出す Lambda を作っていきましょう。
まずは template.yaml に関数の定義と実行契機を記述していきます。 また、CORS 対応を行うための記述も必要となります。
template.yaml(第4版)
AWSTemplateFormatVersion:'2010-09-09'Transform: AWS::Serverless-2016-10-31Description: > Sample SAM TemplateGlobals:Function:Timeout:3Api: # ★追加 ── API GatewayのCORS設定Cors: # ★追加 ── 〃AllowOrigin:"'*'" # ★追加 ── 〃AllowCredentials:"true" # ★追加 ── 〃AllowMethods:"'POST'" # ★追加 ── 〃AllowHeaders:"'*'" # ★追加 ── 〃Resources:HelloWorldFunction:Type: AWS::Serverless::FunctionProperties:CodeUri: hello_world/Handler: app.lambda_handlerRuntime: python3.12Architectures:-x86_64FunctionUrlConfig:AuthType: AWS_IAMCors:AllowOrigins:-"*"AllowCredentials:trueAllowMethods:-"*"AllowHeaders:-"*"## ★ 追加ここから ★#TemporaryPermissionGrantFunction:Type: AWS::Serverless::FunctionProperties:CodeUri: temporary_permission_grant/Handler: app.lambda_handlerRuntime: python3.12Architectures:-x86_64Policies:-Statement:-Effect: AllowAction:-sts:AssumeRoleResource:!GetAtt LambdaFunctionInvokeRole.Arn # ←───┐Environment: # │Variables: # │REGION:!Ref AWS::Region # │FUNC_URL:!GetAtt HelloWorldFunctionUrl.FunctionUrl # ←──┐ROLE_ARN:!GetAtt LambdaFunctionInvokeRole.Arn # ←──┐│ │Events: # ││ │ApiGateway: # ││ │Type: Api # ││ │Properties: # ││ │Path: /grant # ││ │Method: post # ││ │# ││ │# ★ 追加ここまで ★ ││ │# ││ │LambdaFunctionInvokePolicy: # ││ │Type: AWS::IAM::Policy # ││ │Properties: # ││ │PolicyName: hello-world-invoke-policy # ││ │PolicyDocument: # ││ │Version:'2012-10-17' # ││ │Statement: # ││ │-Effect: Allow # ││ │Action: lambda:InvokeFunctionUrl # ││ │Resource:!GetAtt HelloWorldFunction.Arn # ││ │Roles: # ││ │-!Ref LambdaFunctionInvokeRole # ││ │# ││ │LambdaFunctionInvokeRole: # ──────────────────────────────────┘┘ │Type: AWS::IAM::Role # │Properties: # │AssumeRolePolicyDocument: # │Version:'2012-10-17' # │Statement: # │-Effect: Allow # │Principal: # │AWS:!Sub"arn:aws:iam::${AWS::AccountId}:root" # │Action: sts:AssumeRole # │# │Outputs: # │HelloWorldFunctionUrl: # │Value:!GetAtt HelloWorldFunctionUrl.FunctionUrl # ────────────┘GrantFunctionApiUrl:Value:!Sub"https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/grant/"
続いて、関数本体の実装です。
関数 URL から Lambda を実行する権限を持った IAM ロールを AssumeRole して、クレデンシャルと呼ばれる情報を呼び出し元に返却します。
ついでに、次に呼ぶべき Lambda 関数のURL 情報と、後述する SigV4 の計算に利用するリージョンも併せて呼び出し元に返すことにします。
temporary_permission_grant/app.py
import jsonimport boto3import osdeflambda_handler(event, context): sts_client = boto3.client('sts')# 環境変数からIAMロールのArn、関数URL、リージョンを取得 role_arn = os.environ['ROLE_ARN'] func_url = os.environ['FUNC_URL'] region = os.environ['REGION']# HelloWorldFunctionを実行するための一時的な権限を取得 assumed_role = sts_client.assume_role( RoleArn=role_arn, RoleSessionName='InvokeHelloWorldFunction' ) credentials = assumed_role['Credentials']return {'statusCode':200,'headers': {"Access-Control-Allow-Origin":"*","Access-Control-Allow-Credentials":"true","Access-Control-Allow-Methods":"POST","Access-Control-Allow-Headers":"*", },'body': json.dumps({'FuncUrl': func_url,'Region': region,'AccessKeyId': credentials['AccessKeyId'],'SecretAccessKey': credentials['SecretAccessKey'],'SessionToken': credentials['SessionToken'],'Expiration': credentials['Expiration'].isoformat() }) }
以上でバックエンドの実装は完了です。 お疲れ様でした。
最後の仕上げに、API を呼び出す側(クライアントサイド)を実装していきます。
初めに、一時権限を払い出すAPI にリクエストを送り、Lambda を実行するための一時権限を取得します。
続いて、この取得した情報を用いてSigV4と呼ばれる電子署名を作成し、Lambda 関数を呼び出します。
リクエストを2回行うことになるためラウンドトリップは増えますが、セキュアな形でAPI アクセスができます。
SigV4 によって、「権限を持った人からのリクエストであること」「リクエストが改竄されていないこと」が保証されるようになるからです。
ただし、SigV4 の計算は非常に繁雑ですので、AWS-SDK などのライブラリを利用することがAWS 公式によって強く推奨されています。
以下のサンプルは、ボタンを押すと「一時権限の払い出し → それを用いて Lambda 関数 URL にリクエストを送り、レスポンスの内容をアラートに表示する」という一連の動きを確認できる HTML です。
コード中の変数apiUrl
には、sam deploy
した際に表示されるAPIGateway のエンドポイント URL を指定しましょう。
index.html
<!DOCTYPE html><html><head><title>Function URL Invoker</title><scriptsrc="https://cdn.jsdelivr.net/npm/aws-sdk@latest/dist/aws-sdk.min.js"></script></head><body><buttonid="button">button</button><script>document.getElementById('button').addEventListener('click',async()=>{// 一時権限払い出しAPIのエンドポイントURLconst apiUrl='https://██████████.execute-api.ap-northeast-1.amazonaws.com/Prod/grant/';// AWSから一時的な権限を取得するconst getCredentials=async()=>{const response=awaitfetch(apiUrl,{method:'POST'});return response.json();// JSON形式のレスポンスを返す}// Lambda Function URLを呼び出すconst invokeFunctionUrl=async(credentials)=>{// AWSクレデンシャルとリージョンの設定const AWS=window.AWS; AWS.config.credentials={accessKeyId: credentials.AccessKeyId,secretAccessKey: credentials.SecretAccessKey,sessionToken: credentials.SessionToken,}; AWS.config.region= credentials.Region;// リクエストの設定const endpoint=newAWS.Endpoint(credentials.FuncUrl);const request=newAWS.HttpRequest(endpoint, AWS.config.region); request.method='POST'; request.headers['Host'] = endpoint.host; request.headers['Content-Type'] ='application/json';// 送信するデータ request.body=JSON.stringify({/* 必要に応じて Body を設定してください */});// リクエストにSigV4署名を付与const signer=newAWS.Signers.V4(request,'lambda'); signer.addAuthorization(AWS.config.credentials, AWS.util.date.getDate());// Lambda Function URLへリクエストを送信const response=awaitfetch(endpoint.href,{method: request.method,headers: request.headers,body: request.body});return response.json();// JSON形式のレスポンスを返す}// 主な処理フローtry{const credentials=awaitgetCredentials();console.log('取得したクレデンシャル:', credentials);const result=awaitinvokeFunctionUrl(credentials);console.log('Function URLのレスポンス:', result);// アラートに表示window.alert(JSON.stringify(result));}catch(error){console.error('エラー:', error);}});</script></body></html>
Copyright (c) 2012 @tercel_s, @iTercel, @pi_cro_s.
引用をストックしました
引用するにはまずログインしてください
引用をストックできませんでした。再度お試しください
限定公開記事のため引用できません。