TerraformでLambda Function URLをCloudFront OACに設定する

# はじめに
CloudFrontのOrigin Access ControlでLambda Function URLを保護できるようになりました。

https://aws.amazon.com/jp/about-aws/whats-new/2024/04/amazon-cloudfront-oac-lambda-function-url-origins/

Lambda Function URLのアクセス制御は、PublicもしくはIAMによる認証しか選択できないため、この変更によりCloudFront + WAFでより細かくアクセス制御・保護が可能になります。

本記事ではこの設定をTerraformで作成します。

# できたもの
```terraform
variable "stage" { type = string }
variable "prefix" { type = string }
variable "generate_lambda_name" { type = string }
variable "generate_lambda_url" { type = string }

resource "aws_cloudfront_cache_policy" "lambda_origin" {
  name        = "${var.prefix}-cache-policy-${var.stage}"
  min_ttl     = 1
  max_ttl     = 100
  default_ttl = 50
  parameters_in_cache_key_and_forwarded_to_origin {
    cookies_config {
      cookie_behavior = "none"
    }
    headers_config {
      header_behavior = "none"
    }
    query_strings_config {
      query_string_behavior = "all"
    }
  }
}

resource "aws_cloudfront_origin_access_control" "lambda_origin" {
  name                              = "${var.prefix}-oac-${var.stage}"
  signing_protocol                  = "sigv4"
  signing_behavior                  = "always"
  origin_access_control_origin_type = "lambda"
}

resource "aws_cloudfront_distribution" "lambda_origin" {
  origin {
    domain_name              = replace(replace(var.lambda_function_url, "https://", ""), "/", "")
    origin_id                = "LambdaOrigin"
    origin_access_control_id = aws_cloudfront_origin_access_control.lambda_origin.id
    custom_origin_config {
      http_port                = 80
      https_port               = 443
      origin_protocol_policy   = "https-only"
      origin_ssl_protocols     = ["TLSv1.2"]
    }
  }

  enabled         = true
  is_ipv6_enabled = true

  default_cache_behavior {
    target_origin_id       = "LambdaOrigin"
    viewer_protocol_policy = "allow-all"
    allowed_methods        = ["HEAD", "DELETE", "POST", "GET", "OPTIONS", "PUT", "PATCH"]
    cached_methods         = ["HEAD", "GET", "OPTIONS"]
    cache_policy_id        = aws_cloudfront_cache_policy.lambda_origin.id
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }
}

resource "aws_lambda_permission" "allow_cloudfront" {
  statement_id  = "AllowCloudFrontServicePrincipal"
  action        = "lambda:InvokeFunctionUrl"
  function_name = var.lambda_name
  principal     = "cloudfront.amazonaws.com"
  source_arn    = aws_cloudfront_distribution.lambda_origin.arn
}
```

# ポイントなど
### `domain_name`について
Lambda Function URLは

`https://xxxxxxxxxxxxxxxxxxxxx.lambda-url.ap-northeast-1.on.aws/`

の形式で取得できるので、`domain_name`にはこれを整形して、

`xxxxxxxxxxxxxxxxxxxxx.lambda-url.ap-northeast-1.on.aws`

を渡します。

### PUT, POSTメソッドについて
Lambda Function URLでPUT, POSTリクエストを行う際は、`x-amz-content-sha256`ヘッダーにペイロードのsha256ハッシュ値を含める必要があります。

https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-lambda.html

（IAM認証情報を使った署名は必要ありません：[参考](https://repost.aws/ja/questions/QUbHCI9AfyRdaUPCCo_3XKMQ/lambda-function-url-behind-cloudfront-invalidsignatureexception-only-on-post?sc_ichannel=ha&sc_ilang=en&sc_isite=repost&sc_iplace=hp&sc_icontent=QUbHCI9AfyRdaUPCCo_3XKMQ&sc_ipos=4)）
今回のようにCloudFront経由でリクエストを行う際も、CloudFrontへのリクエストに同様のヘッダーを含める必要があります。

これがないと以下のように怒られます。
```
{
    "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."
}
```

### API Gatewayと最大timeoutについて
今回このCloudFront + Lambda Function URLの構成を検討したきっかけとして、Lambdaの処理時間が長い場合にAPI Gatewayのtimeout制限を手っ取り早く回避したいというものがありました。
最大timeoutはそれぞれ

- [Lambda](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/gettingstarted-limits.html)：900秒
- [API Gateway](https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/limits.html)：30秒
- [CloudFront](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/cloudfront-limits.html)：60秒（リクエストで180秒まで引き上げ可能）

です。
CloudFrontのクォータはコンソールの「Service Quotas」からは引き上げリクエストできないので、サポートに問い合わせる必要があります。
CloudFrontのクォータ引き上げ時の最大timeoutの180秒について、これを明記しているドキュメントは見当たらなかったのですが、引き上げリクエスト時にサポートに聞いたら180秒がhard limitであると回答をもらいました。