🔒

CloudFront OAC+Lambda関数URLをTerraformで実装する

に公開

この記事は、Finatext Advent Calendar 2025の 6 日目の記事です。
株式会社 Finatext でバックエンドエンジニアをしている阿部です。

先日業務で AWS Lambda で作った API (Go + Echo) を CloudFront で公開する機会がありました。
Terraform で IaC 化するにあたりいくつかハマったポイントもあったのでこの記事でまとめます。

説明すること / 説明しないこと

説明すること

  • Lambda Function URLs (Lambda 関数 URL)、CloudFront OAC とはなにか
  • Terraform のコード例
  • ハマったポイント

説明しないこと

前提知識・背景

Lambda Function URLs とは

Lambda Function URLs (Lambda 関数 URL)は、Lambda 関数に HTTP(S) エンドポイントを追加できる機能です。
関数 URL を作成すると、以下のような一意の URL エンドポイントが生成されます。

https://<url-id>.lambda-url.<region>.on.aws

認証タイプとして AWS_IAM(lambda:InvokeFunctionUrllambda:InvokeFunctionを持つ Principal のみアクセス可能)かNONE(パブリックにアクセス可能)が選択できます。

認証タイプにAWS_IAMが設定されている場合、各 HTTP リクエストにはAWS Signature Version 4 (SigV4) による署名が必要となります。
CloudFront 経由で関数 URL にアクセスする場合、後述する CloudFront OAC の設定によってリクエストに署名させることができます。

CloudFront OAC (Origin Access Control) とは

OAC は CloudFront がオリジンにアクセスする際の認証/認可を設定できる機能です。
従来の OAI(Origin Access Identity)の後継機能として、より細かな設定が行えるようになりました。
2024 年 4 月のアップデートで、Lambda Function URLs の実行がサポートされるようになりました。

CloudFront OAC + Lambda Function URLs で実現できること

以下の 2 つによって、Lambda Function URLs の直接アクセスを防ぎ、CloudFront 経由のみに制限することができます。

  • CloudFront ディストリビューションの「認証」
    CloudFront が Lambda Function URLs にアクセスする際、CloudFront が各リクエストに対して SigV4 で署名を行い、Lambda 側で検証することで、「このリクエストは信頼できる CloudFront から来ている」と認証できる
  • Lambda Function URLs への「認可」
    Lambda のポリシーに OAC を設定することで、「認証タイプがAWS_IAMかつ指定した CloudFront ディストリビューションからの Lambda 実行のみを許可する」ことができる

アーキテクチャ概要

フロー:

  1. クライアントが CloudFront にリクエスト
  2. CloudFront が OAC を使用して AWS Signature V4 で署名
  3. Lambda Function URL が署名を検証
  4. 検証成功時のみ Lambda 関数を実行

Terraform による実装

以下は Terraform のコード例です。

Lambda Function の作成

まず、Lambda 関数と Function URL を作成します。
ポイントは authorization_type = "AWS_IAM" を指定することです。

# Lambda 関数 (コンテナイメージ使用)
resource "aws_lambda_function" "example" {
  architectures = ["arm64"]
  function_name = "example-function"
  image_uri     = "<account-id>.dkr.ecr.<region>.amazonaws.com/<ecr-name>:<tag>"
  memory_size   = 128
  role          = aws_iam_role.lambda_exec.arn
}

# Lambda Function URL
resource "aws_lambda_function_url" "example" {
  function_name      = aws_lambda_function.example.function_name
  authorization_type = "AWS_IAM"  # IAM 認証を有効化
}

# Lambda 実行ロール
resource "aws_iam_role" "lambda_exec" {
  name = "lambda-exec-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Action = "sts:AssumeRole"
      Effect = "Allow"
      Principal = {
        Service = "lambda.amazonaws.com"
      }
    }]
  })
}

resource "aws_iam_role_policy_attachments_exclusive" "lambda_policy" {
  role       = aws_iam_role.lambda_exec.name
  policy_arns = ["arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"]
}

CloudFront Distribution の設定

次に CloudFront Distribution の作成 と OAC を設定を行います。

OAC の設定

resource "aws_cloudfront_origin_access_control" "example" {
  name                              = "example-oac"
  description                       = "OAC for Lambda Function URL origin"
  origin_access_control_origin_type = "lambda"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"  # SigV4による署名
}

CloudFront Distribution の作成

Origin 設定で Function URL を指定します。
ここで注意が必要なのは、Function URL からプロトコル部分 (https://) と末尾の/を除いたドメイン名を指定することです。

resource "aws_cloudfront_distribution" "example" {
  enabled      = true
  price_class  = "PriceClass_All"
  http_version = "http2and3"

  origin {
    domain_name = replace(replace(aws_lambda_function_url.example.function_url, "https://", ""), "/", "")  # プロトコル部と末尾の/を削除
    origin_id   =  aws_lambda_function_url.example.url_id

    origin_access_control_id = aws_cloudfront_origin_access_control.example.id

    custom_origin_config {
      http_port              = 80
      https_port             = 443
      origin_protocol_policy = "https-only"
      origin_ssl_protocols   = ["TLSv1.2"]
    }
  }

  default_cache_behavior {
    allowed_methods        = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
    cached_methods         = ["GET", "HEAD"]
    target_origin_id       = aws_lambda_function_url.example.url_id
    viewer_protocol_policy = "redirect-to-https"
    cache_policy_id          = data.aws_cloudfront_cache_policy.managed_cachingdisabled.id
    origin_request_policy_id = data.aws_cloudfront_origin_request_policy.managed_allviewerexcepthost.id
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }
}

data "aws_cloudfront_cache_policy" "managed_cachingdisabled" {
  name = "Managed-CachingDisabled"
}

data "aws_cloudfront_origin_request_policy" "managed_allviewerexcepthost" {
  name = "Managed-AllViewerExceptHostHeader"
}

Lambda のリソースポリシー設定

最後に Lambda に対してリソースポリシーの設定を行います。
CloudFront からの Lambda 呼び出しを許可するために、lambda:InvokeFunctionUrllambda:InvokeFunction の両方の権限が必要です。

resource "aws_lambda_permission" "cloudfront" {
  statement_id           = "AllowCloudFrontInvoke"
  action                 = "lambda:InvokeFunctionUrl"
  function_name          = aws_lambda_function.example.function_name
  function_url_auth_type = aws_lambda_function_url.example.authorization_type
  principal              = "cloudfront.amazonaws.com"
  source_arn             = aws_cloudfront_distribution.example.arn
}

resource "aws_lambda_permission" "cloudfront_invoke" {
  statement_id           = "AllowCloudFrontInvokeFunction"
  action                 = "lambda:InvokeFunction"
  function_name          = aws_lambda_function.example.function_name
  principal              = "cloudfront.amazonaws.com"
  source_arn             = aws_cloudfront_distribution.example.arn
}

terraform apply 後、Lambda のアクセス権限 > リソースベースのポリシーステートメントでポリシーを表示すると以下のようになります。

{
  "Version": "2012-10-17",
  "Id": "default",
  "Statement": [
    {
      "Sid": "AllowCloudFrontInvoke",
      "Effect": "Allow",
      "Principal": {
        "Service": "cloudfront.amazonaws.com"
      },
      "Action": "lambda:InvokeFunctionUrl",
      "Resource": "arn:aws:lambda:<region>:<account-id>:function:<function-name>",
      "Condition": {
        "StringEquals": {
          "lambda:FunctionUrlAuthType": "AWS_IAM"
        },
        "ArnLike": {
          "AWS:SourceArn": "arn:aws:cloudfront::<account-id>:distribution/<distribution-id>"
        }
      }
    },
    {
      "Sid": "AllowCloudFrontInvokeFunction",
      "Effect": "Allow",
      "Principal": {
        "Service": "cloudfront.amazonaws.com"
      },
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:<region>:<account-id>:function:<function-name>",
      "Condition": {
        "ArnLike": {
          "AWS:SourceArn": "arn:aws:cloudfront::<account-id>:distribution/<distribution-id>"
        }
      }
    }
  ]
}

ハマったポイント

1. Lambda リソースポリシーの権限設定

InvokeFunction だけではだめ

初め、以下のように lambda:InvokeFunction だけを設定していました。

# ❌ これだけでは動かない
resource "aws_lambda_permission" "cloudfront" {
  statement_id           = "AllowCloudFrontInvoke"
  action                 = "lambda:InvokeFunctionUrl"
  function_name          = aws_lambda_function.example.function_name
  function_url_auth_type = aws_lambda_function_url.example.authorization_type
  principal              = "cloudfront.amazonaws.com"
  source_arn             = aws_cloudfront_distribution.example.arn
}

しかし、CloudFront 経由でアクセスするとエラーが発生します。

公式のドキュメントを参照すると、2025 年 10 月以降は lambda:InvokeFunctionUrllambda:InvokeFunctionの両方を設定する必要があると記載があります。
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/urls-auth.html

両方設定したところ、問題なくアクセスできるようになりました。

2. CloudFront Origin の設定

domain_name の指定方法

Function URL を Origin の domain_name に設定する際、最初は以下のように設定しましたが、アクセスできませんでした。

# ❌ これではダメ
origin {
  domain_name = aws_lambda_function_url.example.function_url
  origin_id   = "lambda-function-url"
  # ...
}

原因

aws_lambda_function_url.example.function_urlhttps:// を含む完全な URL を返します。
https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function_url

しかし、CloudFront の domain_name パラメータにはドメイン名のみを指定する必要があります。

解決策

replace 関数を使ってプロトコル部(https://)と末尾の/ を削除したものを domain_name に設定する必要があります。

# ✅ 正しい設定
origin {
  domain_name = replace(replace(aws_lambda_function_url.example.function_url, "https://", ""), "/", "")
}

3. CloudFront ビヘイビアの設定

オリジンリクエストポリシー

オリジンリクエストポリシーに Managed Policy を設定する際、AllViewerは Terraform からは apply できますが、コンソールからは設定できないようになっています。
alt text

基本は Lambda Functions URLs 向けの推奨設定にするのが良さそうです。

  • キャッシュポリシー: CachingDisabled
  • オリジンリクエストポリシー: AllViewerExceptHostHeader

4. その他の注意点

PUT メソッドと POST メソッド

PUT メソッドや POST メソッドを使用する際、リクエストボディのハッシュ値を SHA256 で計算したものをx-amz-content-sha256ヘッダーにセットする必要があります。

% curl -X POST \
  -H 'x-amz-content-sha256: 0cf5950903541013d83c2f79171a7f4ef1868f6001fef37018a855bf2425cf21' \
  -H 'Content-Type: application/json' \
  -d '{"key": "value}' \
  https://${CLOUDFRONT_DOMAIN}/example   # 例: CLOUDFRONT_DOMAIN=xxxxxxxxx.cloudfront.net

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

動作確認

CloudFront URL 経由でのアクセス(成功)

# アクセステスト
% curl -w "%{http_code}" https://${CLOUDFRONT_DOMAIN}/health  # 例: CLOUDFRONT_DOMAIN=xxxxxxxxx.cloudfront.net

# 出力例
200

Lambda Function URL への直接アクセス(失敗)

# 直接アクセスを試行
% curl https://<url-id>.lambda-url.<region>.on.aws

{"Message":"Forbidden"}

このようにして、Function URL への直接アクセスは拒否され、CloudFront 経由でのみアクセスできることが確認できました。

まとめ

CloudFront OAC を使って Lambda Function URLs へのアクセスを CloudFront に制限する方法を、Terraform を使った実装とともに解説しました。

今回の実装をベースに、以下のような拡張をするのも良いと思います!

  • AWS WAF の追加:CloudFront に WAF を関連付けて、DDoS 対策や IP 制限を設定する
  • カスタムドメインの使用:Route 53 を使った独自ドメイン設定
  • CloudFront Functions の活用x-amz-content-sha256にボディのハッシュを自動的にセット

Finatext グループでは、一緒に働いてくれる仲間を募集中です!
https://herp.careers/v1/finatexthd/iRI8okPMACP_

カジュアル面談も実施しておりますので、少しでも興味を持っていただけた方はお気軽にご応募ください!
https://herp.careers/v1/finatexthd/vZWzSlI_B-qk

Finatext Tech Blog

Discussion