AWS LambdaをAmazon CloudFrontのオリジンに指定してみた

2022.04.15

先日のアップデートにより、AWS LambdaにHTTPSエンドポイントを設定できるようになり、Amazon API GatewayやApplication Load Balancerなどを挟まずにLambdaを直接APIとして呼び出せるようになりました。

LambdaをHTTPSで呼び出せるということは、Amazon CloudFrontのオリジンにLambdaを指定できることを意味し、CDNレイヤーでキャッシュしたりWAFを挟むことも可能です。

やってみた

実際に、Lambda を CloudFront から呼び出してみましょう。

オリジンに指定した Lambda から、リクエストのパス・クエリーストリングを取得できることを確認します。

1. Lambda関数を作成

まずはリクエストを処理する Lambda 関数を作成します。

利用者の多いリージョンに作成すると、CDNのキャッシュミス時のレイテンシーが小さくなります。

import json

def lambda_handler(event, context):
    print(event)

    path = event['rawPath']                        # /foo
    param = event.get('queryStringParameters', '') # {"bar" : "baz"}
    body = {
        'path' : path,
        'param' : param,
    }

    return {
        'statusCode': '200',
        'body': json.dumps(body),
        'headers': {
            'Content-Type': 'application/json',
        }
    }

リクエストのURIパス、クエリーストリングを返しているだけです。

2. エンドポイントURLを払い出す

Lambda 関数の Configuration -> Function URL からエンドポイントURLを払い出します。

CloudFrontから認証なしで呼び出せるよう、Auth Type : NONE にします。

3. Lambdaエンドポイントの動作確認

LambdaをURLから呼び出せることを確認します。

$ URL=https://xxx.lambda-url.ap-northeast-1.on.aws/

$ curl $URL
{"path": "/", "param": ""}

$ curl $URL/foo
{"path": "/foo", "param": ""}

$ curl $URL/foo\?bar=baz
{"path": "/foo", "param": {"bar": "baz"}}

4. Lambda用CloudFrontオリジンリクエストポリシーを作成

オリジンのLambdaでクエリーストリングを処理したいため、専用のオリジンリクエストポリシーを作成します。

Origin request settingsにおいて Query strings を All にします。

Cookieやリクエストヘッダーもオリジンに渡したい場合は、適宜修正してください。

なお、Lambda URLは Host リクエストヘッダーが Lambda のエンドポイントと異なると、 403 エラーを返します。

$ curl -I \
  -H "Host: dummy.example.com" \
  https://xxx.lambda-url.ap-northeast-1.on.aws/

HTTP/1.1 403 Forbidden
Date: Sun, 17 Apr 2022 09:49:23 GMT
Content-Type: application/json
Content-Length: 16
Connection: keep-alive
x-amzn-RequestId: 47afb71d-36cb-4e76-a36f-8bdf5d1eb510
x-amzn-ErrorType: AccessDeniedException

そのため、全てのリクエストヘッダーをオリジンに転送するマネージドポリシーの AllViewer を適用すると、CloudFront のホストヘッダーで Lambda にリクエストするため、同じ 403 Forbidden が発生します。

$ curl -I https://xxx.cloudfront.net
HTTP/2 403
content-type: application/json
content-length: 16
date: Sun, 17 Apr 2022 09:50:07 GMT
x-amzn-requestid: d6d3bd79-656a-4e66-afda-25ae79da0238
x-amzn-errortype: AccessDeniedException
x-cache: Error from cloudfront
via: 1.1 xxx.cloudfront.net (CloudFront)
x-amz-cf-pop: TXL50-P1
x-amz-cf-id: 1rQ7z8kkQwE0PWiKEeUo3tmTUAlFqpWvyfCmItzEqMxZcPGJ7Ri1oA==

リクエストヘッダーの転送が必要な場合、カスタムリクエストヘッダーの作成や Legacy cache settings でカスタマイズし、HOST は含めないでください。

5. CloudFrontディストリビューションを作成

CloudFront ディストリビューションを作成します。

Origin

Origin の設定画面において

  • Origin domain : Lambda の Function URL
  • Protocol : HTTPS only
  • Minimum origin SSL protocol : TLSv1.2
  • Name : Lambda

とします。

Default cache behavior

behavior 設定画面において

  • Origin request policy : lambda-endpoint(#4 で作成したポリシー)

を設定します。

6. CloudFront 経由の動作確認

最後に、CloudFront ディストリビューションのドメイン経由で Lambda を呼び出します。

$ curl -D - https://xxx.cloudfront.net/foo\?a\=b
HTTP/2 200
content-type: application/json
content-length: 37
date: Fri, 15 Apr 2022 08:31:36 GMT
x-amzn-requestid: 805d10c9-39ac-4753-a882-117b73aac1e3
x-amzn-trace-id: root=1-62592d68-2962e3481bf1f7f30994284b;sampled=0
x-cache: Miss from cloudfront
via: 1.1 xxx.cloudfront.net (CloudFront)
x-amz-cf-pop: DUS51-P1
x-amz-cf-id: 6ulq_7myYfNMkqeh71aNspQ8A26hkzY96nqXCzrDF3EByD_rPU2AUw==
server-timing: cdn-upstream-layer;desc="REC",cdn-upstream-dns;dur=0,cdn-upstream-connect;dur=676,cdn-upstream-fbl;dur=965,cdn-cache-miss,cdn-pop;desc="DUS51-P1",cdn-rid;desc="6ulq_7myYfNMkqeh71aNspQ8A26hkzY96nqXCzrDF3EByD_rPU2AUw=="

{"path": "/foo", "param": {"a": "b"}}


$ curl -D - https://xxx.cloudfront.net/foo\?a\=b
HTTP/2 200
content-type: application/json
content-length: 37
date: Fri, 15 Apr 2022 08:31:36 GMT
x-amzn-requestid: 805d10c9-39ac-4753-a882-117b73aac1e3
x-amzn-trace-id: root=1-62592d68-2962e3481bf1f7f30994284b;sampled=0
x-cache: Hit from cloudfront
via: 1.1 xxx.cloudfront.net (CloudFront)
x-amz-cf-pop: DUS51-P1
x-amz-cf-id: 9D3LpWjDbIkRkOmLe7yp1HYE4jbm2PwmweHb-e8QVM9EYG_9xz8O5g==
age: 3
server-timing: cdn-cache-hit,cdn-pop;desc="DUS51-P1",cdn-rid;desc="9D3LpWjDbIkRkOmLe7yp1HYE4jbm2PwmweHb-e8QVM9EYG_9xz8O5g==",cdn-hit-layer;desc="REC"

{"path": "/foo", "param": {"a": "b"}}

1回目の呼び出しではCDNがキャッシュミスし、2回目の呼び出しではCDNがキャッシュヒットしています。

API Gateway と CloudFront の使い分け

CloudFront に複数の Behavior を設定し、Path に応じて Lambda を切り分ければ、CloudFront を API Gateway のように使うことも可能です。

とはいえ、CloudFront はあくまでも簡易的に Lambda を保護・キャッシュするためだけに用い、複雑なルーティングをしたい場合は、素直に API Gateway を使ったほうが良いでしょう。

Lambda リクエストペイロードへの影響

LambdaをURLで呼び出したときのリクエストペイロードは以下の形をしています。

参考 https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html#urls-payloads

{
  "version": "2.0",
  "routeKey": "$default",
  "rawPath": "/my/path",
  "rawQueryString": "parameter1=value1&parameter1=value2&parameter2=value",
  "cookies": [
    "cookie1",
    "cookie2"
  ],
  "headers": {
    "header1": "value1",
    "header2": "value1,value2"
  },
  "queryStringParameters": {
    "parameter1": "value1,value2",
    "parameter2": "value"
  },
  "requestContext": {
    "accountId": "123456789012",
    "apiId": "<urlid>",
    "authentication": null,
    "authorizer": {
        "iam": {
                "accessKey": "AKIA...",
                "accountId": "111122223333",
                "callerId": "AIDA...",
                "cognitoIdentity": null,
                "principalOrgId": null,
                "userArn": "arn:aws:iam::111122223333:user/example-user",
                "userId": "AIDA..."
        }
    },
    "domainName": "<url-id>.lambda-url.us-west-2.on.aws",
    "domainPrefix": "<url-id>",
    "http": {
      "method": "POST",
      "path": "/my/path",
      "protocol": "HTTP/1.1",
      "sourceIp": "123.123.123.123",
      "userAgent": "agent"
    },
    "requestId": "id",
    "routeKey": "$default",
    "stage": "$default",
    "time": "12/Mar/2020:19:03:58 +0000",
    "timeEpoch": 1583348638390
  },
  "body": "Hello from client!",
  "pathParameters": null,
  "isBase64Encoded": false,
  "stageVariables": null
}

CloudFront 経由で呼び出されると、requestContext-> http にあるクライアント情報が CloudFront のものに置き換わります。

具体的には、以下の通りです。

    "http": {
      "method": "POST",
      "path": "/my/path",
      "protocol": "HTTP/1.1",
      "sourceIp": "Amazon CloudFrontのIPアドレス",
      "userAgent": "Amazon CloudFront"
    },

CloudFront アクセスログへの影響

CloudFrontのアクセスログは、ビューワー - CloudFront 間のリクエストが対象です。 オリジンは関係ありません。

最後に

HTTPS エンドポイント対応した Lambda を Amazon CloudFront から呼び出す方法を紹介しました。

CloudFront レイヤーでキャッシュやWAF連携することで、Lambda の呼び出し回数を抑えたり、Lambdaを保護することができます。

とはいえ、AWS Lambda の Function URLは 煩雑な API Gateway 連携をシンプルに解決するためのものです。 ルーティングやアクセスコントロールを CloudFront であれこれ頑張るのであれば、本当に Function URL を使うのが適切なのか、API Gateway で素直に構築できないか、アーキテクチャーを検討したほうが良いかもしれません。

それでは。

参考