AWS CDKでCloudFrontのLambda Function URLsへのOACを設定してみた

CloudFormationとAWS CDKでも設定できます
2024.04.14

AWS CDKでもLambda Function URLsのOACを設定したい

こんにちは、のんピ(@non____97)です。

皆さんはAWS CDKでもLambda Function URLsへのOACを設定したいなと思ったことはありますか? 私はあります。

先日、CloudFrontがLambda Function URLsへのOACをサポートしました。これによりLambda Function URLsへのアクセス制限がやりやすくなりました。詳細は以下記事をご覧ください。

Lambda Function URLsをマネジメントコンソールから設定する際はCloudFront側でOACの設定をした後、表示されたAWS CLIのスクリプトを実行する必要があります。地味に手間です。

ふと、CloudFormationでOACのプロパティを眺めていると、Lambda Function URLsをもうサポートしていました。早いですね。

OriginAccessControlOriginType

The type of origin that this origin access control is for.

Required: Yes

Type: String

Pattern: ^(s3|mediastore|lambda|mediapackagev2)$

Update requires: No interruption

AWS::CloudFront::OriginAccessControl OriginAccessControlConfig - AWS CloudFormation

CloudFormationで設定できるということはAWS CDKでも設定できるということです。

実際にやってみます。

やってみた

AWS CDKのコードの紹介

AWS CDKのコードは以下リポジトリに保存しています。

OACの設定をするにあたって、やっていることは以下の2つです。

  • L1 ConstructでOACを作成する
  • CloudFront distributionのL2 Constructに対してEscape hatchesで、OACを指定する
  • Lambda関数にCloudFront distributionからのlambda:InvokeFunctionUrlを許可する

該当のコードは以下です。

./lib/construct/contents-delivery-construct.ts

    // Lambda Function
    const wasshoiLambda = new cdk.aws_lambda_nodejs.NodejsFunction(
      this,
      "WasshoiLambda",
      {
        entry: path.join(__dirname, "../src/lambda/wasshoi/index.ts"),
        runtime: cdk.aws_lambda.Runtime.NODEJS_20_X,
        bundling: {
          minify: true,
          tsconfig: path.join(__dirname, "../src/lambda/tsconfig.json"),
          format: cdk.aws_lambda_nodejs.OutputFormat.ESM,
        },
        architecture: cdk.aws_lambda.Architecture.ARM_64,
        loggingFormat: cdk.aws_lambda.LoggingFormat.JSON,
      }
    );

    // CloudFront Distribution
    this.distribution = new cdk.aws_cloudfront.Distribution(this, "Default", {
      defaultBehavior: {
        origin: new cdk.aws_cloudfront_origins.FunctionUrlOrigin(
          wasshoiLambda.addFunctionUrl({
            authType: cdk.aws_lambda.FunctionUrlAuthType.AWS_IAM,
          })
        ),
        allowedMethods: cdk.aws_cloudfront.AllowedMethods.ALLOW_GET_HEAD,
        cachedMethods: cdk.aws_cloudfront.CachedMethods.CACHE_GET_HEAD,
        cachePolicy: new cdk.aws_cloudfront.CachePolicy(this, "WasshoiCache", {
          minTtl: cdk.Duration.seconds(1),
          maxTtl: cdk.Duration.seconds(31536000),
          defaultTtl: cdk.Duration.seconds(86400),
          enableAcceptEncodingBrotli: true,
          enableAcceptEncodingGzip: true,
          queryStringBehavior:
            cdk.aws_cloudfront.CacheQueryStringBehavior.allowList("wasshoi"),
        }),
        viewerProtocolPolicy:
          cdk.aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        responseHeadersPolicy:
          cdk.aws_cloudfront.ResponseHeadersPolicy.SECURITY_HEADERS,
      },
      httpVersion: cdk.aws_cloudfront.HttpVersion.HTTP2_AND_3,
      priceClass: cdk.aws_cloudfront.PriceClass.PRICE_CLASS_200,
      domainNames: props.domainName ? [props.domainName] : undefined,
      certificate: props.domainName
        ? props.certificateConstruct?.certificate
        : undefined,
      logBucket: props.cloudFrontAccessLogBucketConstruct?.bucket,
      logFilePrefix: props.logFilePrefix,
    });

    // OAC
    const cfnOriginAccessControl =
      new cdk.aws_cloudfront.CfnOriginAccessControl(
        this,
        "OriginAccessControl",
        {
          originAccessControlConfig: {
            name: "Origin Access Control for Lambda Functions URL",
            originAccessControlOriginType: "lambda",
            signingBehavior: "always",
            signingProtocol: "sigv4",
          },
        }
      );

    const cfnDistribution = this.distribution.node
      .defaultChild as cdk.aws_cloudfront.CfnDistribution;

    // Set OAC
    cfnDistribution.addPropertyOverride(
      "DistributionConfig.Origins.0.OriginAccessControlId",
      cfnOriginAccessControl.attrId
    );

    // Add permission Lambda Function URLs 
    wasshoiLambda.addPermission("AllowCloudFrontServicePrincipal", {
      principal: new cdk.aws_iam.ServicePrincipal("cloudfront.amazonaws.com"),
      action: "lambda:InvokeFunctionUrl",
      sourceArn: `arn:aws:cloudfront::${
        cdk.Stack.of(this).account
      }:distribution/${this.distribution.distributionId}`,
    });

S3バケットのOAC設定をする場合は、問答無用で設定されるOAIを剥がすための記述がいくつか必要でしたが、Lambda Function URLsの場合はシンプルです。

Lambda関数で実行する処理はwasshoiというクエリで指定した値の数分だけわっしょい!!を出力するものです。

./lib/src/lambda/wasshoi/index.ts

import { Callback, LambdaFunctionURLEvent, Context } from "aws-lambda";

export const handler = async (
  event: LambdaFunctionURLEvent,
  context: Context,
  callback: Callback
) => {
  const wasshoi = Math.round(Number(event.queryStringParameters?.wasshoi || 0));

  const message =
    !Number.isInteger(wasshoi) || wasshoi <= 0
      ? "わっしょい! したくないのですか ... ?"
      : wasshoi >= 10
      ? "お静かに"
      : Array(wasshoi).fill("わっしょい!!").join(" ");

  return {
    statusCode: 200,
    headers: { "Content-Type": "text/plain" },
    body: JSON.stringify({
      message,
    }),
  };
};

デプロイ

実際にデプロイして試してみます。

設定は以下のようにしています。カスタムドメインで試してみたかったのでCloudFrontにlambda-url.non-97.netという名前でアクセスできるようにしています。

./parameter/index.ts

export const lambdaOacStackProperty: LambdaOacStackProperty = {
  env: {
    account: process.env.CDK_DEFAULT_ACCOUNT,
    region: process.env.CDK_DEFAULT_REGION,
  },
  props: {
    hostedZone: {
      zoneName: "lambda-url.non-97.net",
    },
    certificate: {
      certificateDomainName: "lambda-url.non-97.net",
    },
    contentsDelivery: {
      domainName: "lambda-url.non-97.net",
    },
    allowDeleteBucketAndObjects: true,
    cloudFrontAccessLog: {
      enableAccessLog: true,
      lifecycleRules: [{ expirationDays: 365 }],
    },
    logAnalytics: {
      createWorkGroup: true,
      enableLogAnalytics: ["cloudFrontAccessLog"],
    },
  },
};

大元のAWS CDKのコードは以下記事で紹介したものです。

動作確認

動作確認をします。

$ curl "https://lambda-url.non-97.net/?wasshoi=1"
{"message":"わっしょい!!"}

$ curl "https://lambda-url.non-97.net/?wasshoi=2"
{"message":"わっしょい!! わっしょい!!"}

$ curl "https://lambda-url.non-97.net/"
{"message":"わっしょい! したくないのですか ... ?"}

$ curl "https://lambda-url.non-97.net/?wasshoi=-1"
{"message":"わっしょい! したくないのですか ... ?"}

$ curl "https://lambda-url.non-97.net/?wasshoi=a"
{"message":"わっしょい! したくないのですか ... ?"}

$ curl "https://lambda-url.non-97.net/?wasshoi=8"
{"message":"わっしょい!! わっしょい!! わっしょい!! わっしょい!! わっしょい!! わっしょい!! わっしょい!! わっしょい!!"}

$ curl "https://lambda-url.non-97.net/?wasshoi=10"
{"message":"お静かに"}

$ curl "https://lambda-url.non-97.net/?wasshoi=5.1"
{"message":"わっしょい!! わっしょい!! わっしょい!! わっしょい!! わっしょい!!"}

$ curl "https://lambda-url.non-97.net/?wasshoi=5.5"
{"message":"わっしょい!! わっしょい!! わっしょい!! わっしょい!! わっしょい!! わっしょい!!"}

wasshoiで指定した値の数分だけ、わっしょいしていますね。

CloudFormationとAWS CDKでも設定できます

AWS CDKでCloudFrontとLambda Function URLsのOACを設定してみました。

API Gatewayがtoo muchである場合や、API Gatewayの統合リクエストタイムアウト29秒の制約が気になる場合に役立ちそうですね。

CloudFrontがLambda Function URLsへのOACをサポートしたことによるユースケースはwatany(@_watany)さんの以下記事も非常に参考になります。

この記事が誰かの助けになれば幸いです。

以上、AWS事業本部 コンサルティング部の のんピ(@non____97)でした!