AWS Lambda Function URLs のリクエストデータを CDK Watch しながら操作する

2022-06-03
AWS コミュニティ通信

吉田 真吾 (AWS Serverless HERO)

AWS Serverless Hero 吉田です。

ひさしぶりに AWS Lambda に大型アップデートがやってきました !
2022 年 4 月 6 日、Lambda 関数を HTTPS で実行できる「AWS Lambda 関数 URL (Function URLs)」機能が追加されました。

HTTPS エンドポイントが追加されることで関数 URL が生成されます。CORS を有効にすることができるため、オリジン配信元から当該関数 URL にイベントデータを POST することもできますし、呼び出し時に認証なしあるいは IAM 認証を設定することもできます。

これにより簡易的な API を Lambda のみで実現できるようになったため、Webhook やアクセスカウンターなどの小さな Web アプリ機能を素早く簡単に実装可能です。

詳細は こちらのブログ を確認してください。

ご注意

本記事で紹介する AWS サービスを起動する際には、料金がかかります。builders.flash メールメンバー特典の、クラウドレシピ向けクレジットコードプレゼントの入手をお勧めします。

*ハンズオン記事およびソースコードにおける免責事項 »

このクラウドレシピ (ハンズオン記事) を無料でお試しいただけます »

毎月提供されるクラウドレシピのアップデート情報とともに、クレジットコードを受け取ることができます。 


1. 事前準備

今回は AWS CDK v2 を初めて使う人を想定してステップバイステップで環境を構築します。ローカル環境に以下の準備をしてください。TypeScript で CDK プロジェクトを作り、Postman でリクエストをポストします。すでに環境が整っているものについては読み飛ばして大丈夫です。


2. AWS CDK で Lambda 関数 URL をセットアップする

1. プロジェクトディレクトリの作成

任意の場所にプロジェクトのディレクトリを作成します。

$ mkdir cdk-lambda-url && cd cdk-lambda-url

2. cdk init

新しい TypeScript の CDK プロジェクトを作成します。

$ cdk init app --language typescript
:
:
Executing npm install...
✅ All done!

3. TypeScript コードのコンパイル

1、2とは別の新しいターミナルセッションを起動し、watch モードで TypeScript のコンパイルを開始します。

#新しいターミナルセッション
$ cd cdk-lambda-url
$ npm run watch
:
:
File change detected. Starting incremental compilation...

4. cdk.json にスタックを構築するリージョン指定のための context と、CDK Watch コマンドで変更監視する Lambda コードのディレクトリを指定します。

cdk.json

{
  "app": "npx ts-node --prefer-ts-exts bin/cdk-lambda-url.ts",
  "watch": {
    "include": [
      "**",
    ],
    "exclude": [
      "README.md",
      "cdk*.json",
      "**/*.d.ts",
      "tsconfig.json",
      "package*.json",
      "yarn.lock",
      "node_modules",
      "test"
    ]
  },
  "context": {
    "region": "ap-northeast-1",
    (中略)
  }
}

5. アプリケーション環境設定をします。

アプリのスタックを読み込んでインスタンス化する bin/cdk-lambda-url.ts において cdk.json の context に指定したリージョンを設定します。

bin/cdk-lambda-url.ts

#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { CdkLambdaUrlStack } from '../lib/cdk-lambda-url-stack';

const app = new cdk.App();
const stack_region = app.node.tryGetContext("region");
new CdkLambdaUrlStack(app, 'CdkLambdaUrlStack', {
  env: {
    region: stack_region
  }
});

6. リソース定義をします。

メインのスタック lib/cdk-lambda-url-stack.ts でアプリケーションのリソースを定義します。

lib/cdk-lambda-url-stack.ts

import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as lambda from 'aws-cdk-lib/aws-lambda';

export class CdkLambdaUrlStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    const hello = new lambda.Function(this, 'HelloHandler', {
      runtime: lambda.Runtime.NODEJS_14_X,
      code: lambda.Code.fromAsset('lambda'),
      handler: 'hello.handler'
    });

    const fnUrl = hello.addFunctionUrl({
      authType: lambda.FunctionUrlAuthType.NONE,
      cors: {
        allowedMethods: [lambda.HttpMethod.ALL],
        allowedOrigins: ["*"],
      },
    });

    new CfnOutput(this, 'FunctionUrl', {
      value: fnUrl.url
    });
  }
}

7. Lambda コードを作成します。

プロジェクトフォルダ直下に lambda ディレクトリを作成し、Lambda コードを lambda/hello.js に作成します。

ターミナルセッション

$ mkdir lambda && touch lambda/hello.js

lambda/hello.js

exports.handler = async function(event) {
    console.log("request:", JSON.stringify(event, undefined, 2));
  
    return {
      statusCode: 200,
      headers: { "Content-Type": "text/plain" },
      body: `Hello, Serverless!`
    };
};

8. アプリを合成 (Synthesize) します。

2 のターミナルに戻り、ここまでの定義を合成 (Synthesize) して CloudFormation テンプレートを生成します。

$ cdk synth

すると以下のように、実際に AWS にデプロイされる、合成された CloudFormation テンプレートが出力されます。

Resources:
  HelloHandlerServiceRole11EF7C63:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Statement:
          - Action: sts:AssumeRole
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
        Version: "2012-10-17"
      ManagedPolicyArns:
        - Fn::Join:
            - ""
            - - "arn:"
              - Ref: AWS::Partition
              - :iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
    Metadata:
      aws:cdk:path: CdkLambdaUrlStack/HelloHandler/ServiceRole/Resource
  HelloHandler2E4FBA4D:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket:
          Fn::Sub: cdk-hnb659fds-assets-${AWS::AccountId}-ap-northeast-1
        S3Key: 00252453104da6998ffc2d29815360f4109325daed492886098e64b5013dc21b.zip
      Role:
        Fn::GetAtt:
          - HelloHandlerServiceRole11EF7C63
          - Arn
      Handler: hello.handler
      Runtime: nodejs14.x
    DependsOn:
      - HelloHandlerServiceRole11EF7C63
    Metadata:
      aws:cdk:path: CdkLambdaUrlStack/HelloHandler/Resource
      aws:asset:path: asset.00252453104da6998ffc2d29815360f4109325daed492886098e64b5013dc21b
      aws:asset:is-bundled: false
      aws:asset:property: Code
  HelloHandlerFunctionUrl0BA407B8:
    Type: AWS::Lambda::Url
    Properties:
      AuthType: NONE
      TargetFunctionArn:
        Fn::GetAtt:
          - HelloHandler2E4FBA4D
          - Arn
      Cors:
        AllowMethods:
          - "*"
        AllowOrigins:
          - "*"
    Metadata:
      aws:cdk:path: CdkLambdaUrlStack/HelloHandler/FunctionUrl/Resource
  HelloHandlerinvokefunctionurl81495083:
    Type: AWS::Lambda::Permission
    Properties:
      Action: lambda:InvokeFunctionUrl
      FunctionName:
        Fn::GetAtt:
          - HelloHandler2E4FBA4D
          - Arn
      Principal: "*"
      FunctionUrlAuthType: NONE
    Metadata:
      aws:cdk:path: CdkLambdaUrlStack/HelloHandler/invoke-function-url
  CDKMetadata:
    Type: AWS::CDK::Metadata
    Properties:
      Analytics: v2:deflate64:H4sIAAAAAAAA/z2OSw+CMBCEf4v3siIk3sXEqwbjmZRSyUIfptvqoeG/2+Lj9M3O7CRTQVVDueEvKsQwFwp7iFfPxcyS1UXFdT9wiKdghEdr2PFu/vonbk5l/4uLdBqJkr8w5Bpia5XMQebCqO44kfQEh4x0QxPELH3DKcWrmQaMaMZcOgf/CH6tS7LBifRj7CBhou1zt4ddmdZPhFi4YDxqCe2Hb6ECIyfZAAAA
    Metadata:
      aws:cdk:path: CdkLambdaUrlStack/CDKMetadata/Default
Outputs:
  FunctionUrl:
    Value:
      Fn::GetAtt:
        - HelloHandlerFunctionUrl0BA407B8
        - FunctionUrl
Parameters:
  BootstrapVersion:
    Type: AWS::SSM::Parameter::Value<String>
    Default: /cdk-bootstrap/hnb659fds/version
    Description: Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]
Rules:
  CheckBootstrapVersion:
    Assertions:
      - Assert:
          Fn::Not:
            - Fn::Contains:
                - - "1"
                  - "2"
                  - "3"
                  - "4"
                  - "5"
                - Ref: BootstrapVersion
        AssertDescription: CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.

9. CDK Toolkit をデプロイする。

cdk bootstrap を実行し、デプロイ先の AWS 環境において CloudFormation スタックをオーケストレートするための CDKToolkit スタックをデプロイします。

$ cdk bootstrap
:
:
✅  Environment aws://<AWSアカウント>/ap-northeast-1 bootstrapped (no changes).

※すでにブートストラップ済みの場合は、no change (変更なし) が表示されます。

10. アプリケーションをデプロイします。

それでは cdk deploy を実行し、アプリをデプロイしましょう。

$ cdk deploy
:
:
Outputs:
CdkLambdaUrlStack.FunctionUrl = https://3dmzvqsb6fjzmnzdkqjqmetb4a0hreyn.lambda-url.ap-northeast-1.on.aws/
:
:
✨  Total time: 71.23s

11. 関数 URL を確認します。

スタック作成時の Outputs に、作成された関数 URL (CdkLambdaUrlStack.FunctionUrl) が表示されていることを確認します。

ブラウザで関数 URL にアクセスし、Lambda から応答が返ってくることを確認します。


3. CDK Watch しながら関数 URL へのリクエストデータを操作する

12. cdk watch コマンドを実施します。

Lambda を複数回変更してデプロイするにあたり、cdk のサブコマンド cdk watch を実行することで、ローカルの変更を自動ですばやく環境にデプロイすることができます。cdk watch は、ローカルの変更監視をおこないながら、変更を検知したときに cdk diff と cdk deploy を自動で実行します。

加えて、CloudFormation テンプレートには変更なく、Lambda コードのみ変更がある場合は hotswap モードに切り替えてデプロイすることで素早い開発体験を得ることができます。

※CDK Watchは開発時のみの利用が推奨されています。

$ cdk watch

13. 関数 URL へのリクエストペイロードを出力してみる

Lambda 関数 URL ではペイロードのフォーマットとして「API Gateway ペイロードフォーマット バージョン 2.0」に対応しています。

Lambda で受け取ったリクエストをレスポンスとしてダンプするように、hello.js に以下のようにコードを追加します。

lambda/hello.js

exports.handler = async function(event) {
    console.log("request:", JSON.stringify(event, undefined, 2));
  
    return {
      statusCode: 200,
      headers: { "Content-Type": "text/plain" },
      body: `Hello, Serverless!
        \nrequest payload:\n${JSON.stringify(event, undefined, 2)}`
    };
};

お気づきでしょうか ? CDK Watch が Lambda コードの変更を検知し、数秒で Lambda コードを自動でデプロイしてくれました。

lambda/hello.js

#cdk watchしているターミナルセッション
Detected change to 'lambda/hello.js' (type: change). Triggering 'cdk deploy'
:
✨  Total time: 5.72s

再度ブラウザで関数 URL にアクセスすると、リクエストペイロードが出力されることを確認できます。

lambda/hello.js

Hello, Serverless!
      
request payload:
{
  "version": "2.0",
  "routeKey": "$default",
  "rawPath": "/",
  "rawQueryString": "",
  "headers": {
    "sec-fetch-mode": "navigate",
    "sec-fetch-site": "cross-site",
    "accept-language": "ja,en-US;q=0.9,en;q=0.8,hu;q=0.7",
    "x-forwarded-proto": "https",
    "x-forwarded-port": "443",
    "x-forwarded-for": "xxx.xxx.xxx.xxx",
    "sec-fetch-user": "?1",
    "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
    "sec-ch-ua": "\" Not A;Brand\";v=\"99\", \"Chromium\";v=\"100\", \"Google Chrome\";v=\"100\"",
    "sec-ch-ua-mobile": "?0",
    "x-amzn-trace-id": "Root=1-6273c271-12e1f1fe5aba4371516b744e",
    "sec-ch-ua-platform": "\"macOS\"",
    "host": "3dmzvqsb6fjzmnzdkqjqmetb4a0hreyn.lambda-url.ap-northeast-1.on.aws",
    "upgrade-insecure-requests": "1",
    "accept-encoding": "gzip, deflate, br",
    "sec-fetch-dest": "document",
    "user-agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36"
  },
  "requestContext": {
    "accountId": "anonymous",
    "apiId": "3dmzvqsb6fjzmnzdkqjqmetb4a0hreyn",
    "domainName": "3dmzvqsb6fjzmnzdkqjqmetb4a0hreyn.lambda-url.ap-northeast-1.on.aws",
    "domainPrefix": "3dmzvqsb6fjzmnzdkqjqmetb4a0hreyn",
    "http": {
      "method": "GET",
      "path": "/",
      "protocol": "HTTP/1.1",
      "sourceIp": "39.110.219.221",
      "userAgent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/100.0.4896.127 Safari/537.36"
    },
    "requestId": "960ac841-7b8b-4feb-b7fc-f8c29e0cdf2d",
    "routeKey": "$default",
    "stage": "$default",
    "time": "05/May/2022:12:26:25 +0000",
    "timeEpoch": 1651753585437
  },
  "isBase64Encoded": false
}

14. 個別のパラメータを取り出してみる

引き続き Lambda コードに以下のように追記して、個別のリクエストパラメータを取り出してみましょう。hello.js を以下のように修正します。

lambda/hello.js

exports.handler = async function(event) {
    console.log("request:", JSON.stringify(event, undefined, 2));
  
    return {
      statusCode: 200,
      headers: { "Content-Type": "text/plain" },
      body: `Hello, Serverless!
        \npath: ${event.requestContext.http.path}
        \nfrom: ${event.requestContext.http.sourceIp}
        \ncookies: ${event.cookies}
        \nbody: ${event.isBase64Encoded ? Buffer.from(event.body, 'base64').toString() : event.body}
        \nrequest payload:\n${JSON.stringify(event, undefined, 2)}`
    };
};

cdk watch でまた自動的にデプロイされましたね。

15. Postman でリクエストする。

リクエストペイロードに form データや cookie データを載せるために Postman でリクエストをおこないます。

Postman の Cookies 画面でドメイン「on.aws」を指定して、以下を cookie の先頭に追加して「Save」します。

SESSION=xxxxxxxx; Path=/; Domain=on.aws;

クリックすると拡大します

Postman の (1) リクエスト URL に関数 URL と (2) 適当なパスを入力し、(3) メソッドに「POST」を指定し、(4) リクエスト Body の form-data に KEY/VALUE を入力します。

クリックすると拡大します

Send」をクリックして関数 URL にリクエストデータを POST します。

クリックすると拡大します

リクエストデータから、「リクエストしたパス」「送信元 IP アドレス」「cookie データ」「base64 エンコードされた body 部のデータ (をデコードしたもの)」が取り出せました。

Hello, Serverless!
        
path: /tenant-a/user-a
        
from: xxx.xxx.xxx.xxx
        
cookies: SESSION=xxxxxxxx
        
body: ----------------------------002623005989495685913338
Content-Disposition: form-data; name="whoami"

yoshidashingo
----------------------------002623005989495685913338--

        
request payload:
{
  "version": "2.0",
  "routeKey": "$default",
  "rawPath": "/tenant-a/user-a",
  "rawQueryString": "",
  "cookies": [
    "SESSION=xxxxxxxx"
  ],
  "headers": {
    "x-amzn-trace-id": "Root=1-6273c75d-063bc336309f79ac73770d04",
    "cookie": "SESSION=xxxxxxxx",
    "x-forwarded-proto": "https",
    "postman-token": "b2ee7f00-51f1-4fb5-b6b9-80163ef2ac3a",
    "host": "5gavmkcouhzi3uv7bkqytxv4u40ivtss.lambda-url.ap-northeast-1.on.aws",
    "x-forwarded-port": "443",
    "content-type": "multipart/form-data; boundary=--------------------------002623005989495685913338",
    "x-forwarded-for": "xxx.xxx.xxx.xxx",
    "accept-encoding": "gzip, deflate, br",
    "accept": "*/*",
    "user-agent": "PostmanRuntime/7.29.0"
  },
  "requestContext": {
    "accountId": "anonymous",
    "apiId": "5gavmkcouhzi3uv7bkqytxv4u40ivtss",
    "domainName": "5gavmkcouhzi3uv7bkqytxv4u40ivtss.lambda-url.ap-northeast-1.on.aws",
    "domainPrefix": "5gavmkcouhzi3uv7bkqytxv4u40ivtss",
    "http": {
      "method": "POST",
      "path": "/tenant-a/user-a",
      "protocol": "HTTP/1.1",
      "sourceIp": "xxx.xxx.xxx.xxx",
      "userAgent": "PostmanRuntime/7.29.0"
    },
    "requestId": "59671e53-7ef1-4662-9c13-932ae75699d0",
    "routeKey": "$default",
    "stage": "$default",
    "time": "05/May/2022:12:47:25 +0000",
    "timeEpoch": 1651754845241
  },
  "body": "LS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLTAwMjYyMzAwNTk4OTQ5NTY4NTkxMzMzOA0KQ29udGVudC1EaXNwb3NpdGlvbjogZm9ybS1kYXRhOyBuYW1lPSJ3aG9hbWkiDQoNCnlvc2hpZGFzaGluZ28NCi0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0wMDI2MjMwMDU5ODk0OTU2ODU5MTMzMzgtLQ0K",
  "isBase64Encoded": true
}

4. まとめ

いかがでしたでしょうか ? この記事では、AWS Lambda 関数 URLs が CDK で簡単に定義して構築できること、CDK Watch で手間なく自動的にスタックのコードがデプロイできること、関数 URLs のリクエストデータが API Gateway ペイロードフォーマット バージョン 2.0 に対応しており、簡易的な Web アプリの構築に便利そうであることの 3 点が確認できたと思います。

実際の Web フロント部分も、上記の CDK スタックに S3 ホスティングや CloudFront を定義することで、手間なく発展させることができます。ぜひ挑戦してみてください。


builders.flash メールメンバーへ登録することで
AWS のベストプラクティスを毎月無料でお試しいただけます

筆者プロフィール

吉田 真吾
株式会社サイダス 取締役CTO
AWS Serverless Hero

証券システム構築からクラウドネイティブなシステム構築・運用などを経て現職。かたわらで JAWS-UG や Serverless Community(JP) の運営、また各種記事執筆を通じて、日本におけるサーバーレスの普及を促進。

AWS を無料でお試しいただけます

AWS 無料利用枠の詳細はこちら ≫
5 ステップでアカウント作成できます
無料サインアップ ≫
ご不明な点がおありですか?
日本担当チームへ相談する