CloudFormation で Lambda Web Functions を CloudFront OAC の背後に置いてみた

CloudFormation で Lambda Web Functions を CloudFront OAC の背後に置いてみた

AWS Lambda Web Functions を CloudFormation で作り、CloudFront の OAC を設定して試しました。IamAuth、OAC、リソースポリシーを組み合わせた結果、CloudFront 経由は 200、署名なしの直接アクセスは 403 でした。
2026.10.13

はじめに

2026年10月12日、Lambda Web Functions の GA と同時に、CloudFront のオリジンアクセスコントロール(OAC)が Lambda Web Functions に対応しました。

https://aws.amazon.com/jp/about-aws/whats-new/2026/10/cloudfront-lambda-web-functions-origin/

OAC を利用すると、特定の CloudFront ディストリビューションを経由しないアクセスを拒否できるため、Web Function を直接公開するリスクへの対策として有効です。

今回、Lambda Web Functions、CloudFront、OAC を CloudFormation による IaC で構築する機会があったので、その手順を紹介します。

構成と進め方

Web Function のコードは S3 に置いた .zip でしか渡せないため、テンプレートを 2 本に分け、間にコードの配置を挟んだ 3 手順にしました。

手順 内容 方法
1 コード置き場の S3 バケットの作成 CloudFormation
2 コードの .zip の配置 CLI
3 Web Function、OAC、Distribution、リソースポリシーの作成 CloudFormation

手順1: コード置き場の S3 バケットを作る

Web Function のコードを置くバケットは、バージョニングを有効にします。Lambda はオブジェクトのバージョンでリビジョンのコードを固定するため、バージョニングが必須です。

Lambda がバケットから .zip を読めるように、バケットポリシーで lambda.amazonaws.com に s3:GetObject と s3:GetObjectVersion を許可します。

- Sid: AllowLambdaWebFunctionCodeAccess
  Effect: Allow
  Principal:
    Service: lambda.amazonaws.com
  Action:
    - s3:GetObject
    - s3:GetObjectVersion
  Resource: !Sub ${CodeBucket.Arn}/*
  Condition:
    ArnLike:
      aws:SourceArn: !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:web-function/*

aws:SourceArn はワイルドカードにして、同じアカウント・リージョンのすべての Web Function からの読み取りを許可しています。特定の Web Function に限定したい場合は、web-function/<関数名>/revision/* のように関数名を指定してください。

https://docs.aws.amazon.com/lambda/latest/dg/web-functions-code-s3.html

step1-code-bucket.yaml(全文)
AWSTemplateFormatVersion: '2010-09-09'
Description: Step 1 - S3 bucket for Lambda Web Function code (versioning + bucket policy for Lambda)

Resources:
  CodeBucket:
    Type: AWS::S3::Bucket
    Properties:
      VersioningConfiguration:
        Status: Enabled
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

  CodeBucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref CodeBucket
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: AllowLambdaWebFunctionCodeAccess
            Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action:
              - s3:GetObject
              - s3:GetObjectVersion
            Resource: !Sub ${CodeBucket.Arn}/*
            Condition:
              ArnLike:
                aws:SourceArn: !Sub arn:${AWS::Partition}:lambda:${AWS::Region}:${AWS::AccountId}:web-function/*

Outputs:
  CodeBucketName:
    Value: !Ref CodeBucket

テンプレートを step1-code-bucket.yaml として保存し、スタックを作成します。スタックが CREATE_COMPLETE になったら、出力の CodeBucketName からバケット名を取得します。この値は手順2と手順3で使います。

aws cloudformation create-stack --stack-name hello-web-code --template-body file://step1-code-bucket.yaml
aws cloudformation describe-stacks --stack-name hello-web-code --query 'Stacks[0].Outputs'

手順2: コードを S3 に置く

アプリは、JSON を返すだけの Hello World です。index.js と package.json を用意します。index.js で import 構文(ES モジュール)を使うため、package.json に "type": "module" を指定しています。

index.js と package.json

index.js:

// Minimal AWS Lambda Web Function HTTP server.
//
// You can replace this with any code or framework that starts an HTTP server.
import http from 'node:http';

// Lambda sends requests to port 3000. Set LOCAL_PORT for local development.
const port = Number(process.env.LOCAL_PORT ?? 3000);

const server = http.createServer((req, res) => {
  res.writeHead(200, { 'content-type': 'application/json' });
  res.end(JSON.stringify({ message: 'Hello from Lambda Web!', path: req.url }));
});

server.listen(port, () => {
  console.log(`listening on port ${port}`);
});

package.json:

{
  "type": "module",
  "main": "index.js"
}

index.js が .zip のルートに置かれるように圧縮し、手順1のバケットへ app/hello-web.zip としてアップロードします。

zip -r hello-web.zip index.js package.json
aws s3 cp hello-web.zip s3://<CodeBucketName>/app/hello-web.zip

手順3: Web Function と CloudFront を作る

step3-web-function.yaml(全文)
AWSTemplateFormatVersion: '2010-09-09'
Description: Step 3 - Lambda Web Function (IamAuth) behind CloudFront with OAC

Parameters:
  FunctionName:
    Type: String
    Default: hello-web
  CodeBucket:
    Type: String
    Description: Bucket created in step 1
  CodeKey:
    Type: String
    Default: app/hello-web.zip
    Description: Key of the .zip uploaded in step 2

Resources:
  ExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: sts:AssumeRole
      ManagedPolicyArns:
        - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

  WebFunction:
    Type: AWS::Lambda::WebFunction
    Properties:
      FunctionName: !Ref FunctionName

  Revision:
    Type: AWS::Lambda::WebFunctionRevision
    Properties:
      FunctionName: !Ref WebFunction
      BuildConfig:
        CodeConfig:
          S3Object:
            Bucket: !Ref CodeBucket
            Key: !Ref CodeKey
        RuntimeConfig:
          Runtime: nodejs24.x
      ServiceConfig:
        ExecutionRoleArn: !GetAtt ExecutionRole.Arn
        TimeoutSeconds: 30

  Endpoint:
    Type: AWS::Lambda::WebFunctionEndpoint
    Properties:
      FunctionName: !Ref WebFunction
      EndpointName: main
      EndpointType: HomeRegion
      AuthType: IamAuth
      RevisionWeights:
        - RevisionId: !GetAtt Revision.RevisionId
          Weight: 100

  Oac:
    Type: AWS::CloudFront::OriginAccessControl
    Properties:
      OriginAccessControlConfig:
        Name: !Sub ${FunctionName}-oac
        OriginAccessControlOriginType: lambda
        SigningBehavior: always-amz-auth
        SigningProtocol: sigv4

  Distribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Enabled: true
        Comment: !Ref FunctionName
        Origins:
          - Id: web-function
            DomainName: !GetAtt Endpoint.DomainName
            OriginAccessControlId: !GetAtt Oac.Id
            CustomOriginConfig:
              OriginProtocolPolicy: https-only
        DefaultCacheBehavior:
          TargetOriginId: web-function
          ViewerProtocolPolicy: redirect-to-https
          AllowedMethods: [GET, HEAD]
          # Managed-CachingDisabled
          CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad
          # Managed-AllViewerExceptHostHeader
          OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac

  ResourcePolicy:
    Type: AWS::Lambda::ResourcePolicy
    Properties:
      ResourceArn: !GetAtt WebFunction.FunctionArn
      PolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Sid: AllowCloudFrontOACAccess
            Effect: Allow
            Principal:
              Service: cloudfront.amazonaws.com
            Action: lambda:InvokeWebFunctionEndpoint
            Resource: !GetAtt Endpoint.EndpointArn
            Condition:
              StringEquals:
                aws:SourceArn: !Sub arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:distribution/${Distribution}

Outputs:
  DistributionDomain:
    Value: !GetAtt Distribution.DomainName
  EndpointDomain:
    Value: !GetAtt Endpoint.DomainName

テンプレートを step3-web-function.yaml として保存し、スタックを作成します。CodeBucket には手順1で取得したバケット名を渡します。CodeKey は、既定値が手順2で置いたキー(app/hello-web.zip)と同じなので省略できます。IAM ロールを作成するため、--capabilities に CAPABILITY_IAM を指定します。

aws cloudformation create-stack \
  --stack-name hello-web \
  --template-body file://step3-web-function.yaml \
  --parameters ParameterKey=CodeBucket,ParameterValue=<CodeBucketName> \
  --capabilities CAPABILITY_IAM

実行ロール

信頼ポリシーで lambda.amazonaws.com にロールの引き受け(sts:AssumeRole)を許可し、CloudWatch Logs へログを書けるように AWSLambdaBasicExecutionRole を付けます。

AssumeRolePolicyDocument:
  Version: '2012-10-17'
  Statement:
    - Effect: Allow
      Principal:
        Service: lambda.amazonaws.com
      Action: sts:AssumeRole
ManagedPolicyArns:
  - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole

Revision と Endpoint

S3Object の VersionId を省略すると、リビジョン作成時点の最新バージョンのオブジェクトに固定されます。

BuildConfig:
  CodeConfig:
    S3Object:
      Bucket: !Ref CodeBucket
      Key: !Ref CodeKey
  RuntimeConfig:
    Runtime: nodejs24.x
ServiceConfig:
  ExecutionRoleArn: !GetAtt ExecutionRole.Arn
  TimeoutSeconds: 30

OAC を使うには IamAuth が必要なため、Endpoint の AuthType は IamAuth にします。

EndpointName: main
EndpointType: HomeRegion
AuthType: IamAuth
RevisionWeights:
  - RevisionId: !GetAtt Revision.RevisionId
    Weight: 100

OAC と Distribution

Web Function 向けの OAC では、OriginAccessControlOriginType を lambda、SigningBehavior を always-amz-auth にします。

OriginAccessControlConfig:
  OriginAccessControlOriginType: lambda
  SigningBehavior: always-amz-auth
  SigningProtocol: sigv4

オリジンリクエストポリシーには、Host ヘッダーをオリジンへ転送しないマネージドポリシーの AllViewerExceptHostHeader を使います。

Origins:
  - Id: web-function
    DomainName: !GetAtt Endpoint.DomainName
    OriginAccessControlId: !GetAtt Oac.Id
    CustomOriginConfig:
      OriginProtocolPolicy: https-only
DefaultCacheBehavior:
  # Managed-CachingDisabled
  CachePolicyId: 4135ea2d-6df8-44a3-9df3-4b5a84be39ad
  # Managed-AllViewerExceptHostHeader
  OriginRequestPolicyId: b689b0a8-53d0-40ab-baf2-68738e2966ac

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

リソースポリシー

AWS::Lambda::ResourcePolicy で、cloudfront.amazonaws.com に lambda:InvokeWebFunctionEndpoint を許可します。ResourceArn にはポリシーを付ける Web Function の ARN、Resource には呼び出しを許可するエンドポイントの ARN を指定します。

ResourceArn: !GetAtt WebFunction.FunctionArn
PolicyDocument:
  Statement:
    - Sid: AllowCloudFrontOACAccess
      Effect: Allow
      Principal:
        Service: cloudfront.amazonaws.com
      Action: lambda:InvokeWebFunctionEndpoint
      Resource: !GetAtt Endpoint.EndpointArn
      Condition:
        StringEquals:
          aws:SourceArn: !Sub arn:${AWS::Partition}:cloudfront::${AWS::AccountId}:distribution/${Distribution}

aws:SourceArn にこのスタックで作るディストリビューションの ARN を指定し、呼び出し元をそのディストリビューションに限定しています。

動作確認

スタックが CREATE_COMPLETE になったら、出力から CloudFront のドメイン(DistributionDomain)とエンドポイントのドメイン(EndpointDomain)を取得します。

aws cloudformation describe-stacks --stack-name hello-web --query 'Stacks[0].Outputs'

CloudFront の標準ドメインへ GET /hello?x=1 を送ると、200 と JSON の本文が返りました。

$ curl -sS -i 'https://dxxxxxxxxxxxxx.cloudfront.net/hello?x=1'
HTTP/1.1 200 OK
Content-Type: application/json
X-Cache: Miss from cloudfront

{"message":"Hello from Lambda Web!","path":"/hello?x=1"}

エンドポイントのドメインへ直接同じリクエストを送ると、403 が返りました。

$ curl -sS -i 'https://xxxxxxxxxxxxxxxx.xxx.lambda-web.ap-northeast-1.on.aws/hello?x=1'
HTTP/1.1 403 Forbidden

まとめ

Lambda Web Functions から CloudFront、OAC の設定まで、CloudFormation で構築できることを確認しました。

直接公開に伴うリスクを避けるため、Lambda Web Functions の公開には OAC の利用をおすすめします。

今回の検証環境では省略しましたが、Lambda Web Functions を継続的に公開する場合は、CloudFront に適切なキャッシュポリシーとアクセスログを設定し、必要に応じて WAF による保護を有効にすることもご検討ください。

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事