diff --git a/cdk/lib/constructs/cf-lambda-furl-service/service.ts b/cdk/lib/constructs/cf-lambda-furl-service/service.ts index b1cbb6a..39fc7ae 100644 --- a/cdk/lib/constructs/cf-lambda-furl-service/service.ts +++ b/cdk/lib/constructs/cf-lambda-furl-service/service.ts @@ -1,6 +1,6 @@ import { Construct } from 'constructs'; -import { Duration } from 'aws-cdk-lib'; -import { FunctionUrlAuthType, Function, InvokeMode } from 'aws-cdk-lib/aws-lambda'; +import { Aws, Duration } from 'aws-cdk-lib'; +import { FunctionUrlAuthType, Function, InvokeMode, CfnPermission } from 'aws-cdk-lib/aws-lambda'; import { AllowedMethods, CacheCookieBehavior, @@ -120,6 +120,18 @@ export class CloudFrontLambdaFunctionUrlService extends Construct { minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2021, }); + // Starting October 2025, new function URLs require both lambda:InvokeFunctionUrl + // and lambda:InvokeFunction permissions for CloudFront OAC. + // CDK's FunctionUrlOrigin.withOriginAccessControl only adds lambda:InvokeFunctionUrl, + // so we explicitly add lambda:InvokeFunction here. + // See: https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html + new CfnPermission(this, 'InvokeFunctionPermission', { + action: 'lambda:InvokeFunction', + functionName: handler.functionArn, + principal: 'cloudfront.amazonaws.com', + sourceArn: `arn:${Aws.PARTITION}:cloudfront::${Aws.ACCOUNT_ID}:distribution/${distribution.distributionId}`, + }); + if (hostedZone) { new ARecord(this, 'Record', { zone: hostedZone, diff --git a/cdk/lib/main-stack.ts b/cdk/lib/main-stack.ts index ac5db01..ef42305 100644 --- a/cdk/lib/main-stack.ts +++ b/cdk/lib/main-stack.ts @@ -4,7 +4,7 @@ import { Construct } from 'constructs'; import { AsyncJob } from './constructs/async-job'; import { Auth } from './constructs/auth/'; import { Database } from './constructs/database'; -import { InstanceClass, InstanceSize, InstanceType, NatProvider, Vpc } from 'aws-cdk-lib/aws-ec2'; +import { InstanceClass, InstanceSize, InstanceType, NatProvider, UserData, Vpc } from 'aws-cdk-lib/aws-ec2'; import { HostedZone } from 'aws-cdk-lib/aws-route53'; import { ICertificate } from 'aws-cdk-lib/aws-certificatemanager'; import { Webapp } from './constructs/webapp'; @@ -56,12 +56,31 @@ export class MainStack extends Stack { autoDeleteObjects: true, }); + // Custom user data for NAT instance to support Amazon Linux 2023. + // CDK's default user data uses `route` command which requires net-tools package, + // but AL2023 doesn't have net-tools pre-installed. We use `ip route` instead. + // Retry yum install to handle RPM lock conflicts during boot. + const natUserData = UserData.forLinux(); + natUserData.addCommands( + // Retry yum install up to 5 times with 10 second intervals + 'for i in {1..5}; do yum install iptables-services -y && break || sleep 10; done', + 'systemctl enable iptables', + 'systemctl start iptables', + 'echo "net.ipv4.ip_forward=1" > /etc/sysctl.d/custom-ip-forwarding.conf', + 'sysctl -p /etc/sysctl.d/custom-ip-forwarding.conf', + "IFACE=$(ip route show default | awk '{print $5}')", + '/sbin/iptables -t nat -A POSTROUTING -o $IFACE -j MASQUERADE', + '/sbin/iptables -F FORWARD', + 'service iptables save', + ); + const vpc = new Vpc(this, `Vpc`, { ...(useNatInstance ? { natGatewayProvider: NatProvider.instanceV2({ instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.NANO), associatePublicIpAddress: true, + userData: natUserData, }), natGateways: 1, } diff --git a/cdk/test/__snapshots__/serverless-fullstack-webapp-starter-kit-without-domain.test.ts.snap b/cdk/test/__snapshots__/serverless-fullstack-webapp-starter-kit-without-domain.test.ts.snap index 4c6b442..7b58331 100644 --- a/cdk/test/__snapshots__/serverless-fullstack-webapp-starter-kit-without-domain.test.ts.snap +++ b/cdk/test/__snapshots__/serverless-fullstack-webapp-starter-kit-without-domain.test.ts.snap @@ -2712,14 +2712,15 @@ exports.handler = async function (event, context) { ], "UserData": { "Fn::Base64": "#!/bin/bash -yum install iptables-services -y +for i in {1..5}; do yum install iptables-services -y && break || sleep 10; done systemctl enable iptables systemctl start iptables echo "net.ipv4.ip_forward=1" > /etc/sysctl.d/custom-ip-forwarding.conf -sudo sysctl -p /etc/sysctl.d/custom-ip-forwarding.conf -sudo /sbin/iptables -t nat -A POSTROUTING -o $(route | awk '/^default/{print $NF}') -j MASQUERADE -sudo /sbin/iptables -F FORWARD -sudo service iptables save", +sysctl -p /etc/sysctl.d/custom-ip-forwarding.conf +IFACE=$(ip route show default | awk '{print $5}') +/sbin/iptables -t nat -A POSTROUTING -o $IFACE -j MASQUERADE +/sbin/iptables -F FORWARD +service iptables save", }, }, "Type": "AWS::EC2::Instance", @@ -3684,6 +3685,38 @@ sudo service iptables save", }, "Type": "AWS::IAM::Policy", }, + "WebappInvokeFunctionPermission8F3F2610": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "WebappHandler8DD158A3", + "Arn", + ], + }, + "Principal": "cloudfront.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":cloudfront::", + { + "Ref": "AWS::AccountId", + }, + ":distribution/", + { + "Ref": "Webapp107041BD", + }, + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, "WebappMigrationRunnerAC67C012": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED", diff --git a/cdk/test/__snapshots__/serverless-fullstack-webapp-starter-kit.test.ts.snap b/cdk/test/__snapshots__/serverless-fullstack-webapp-starter-kit.test.ts.snap index 4cb5db9..d02589b 100644 --- a/cdk/test/__snapshots__/serverless-fullstack-webapp-starter-kit.test.ts.snap +++ b/cdk/test/__snapshots__/serverless-fullstack-webapp-starter-kit.test.ts.snap @@ -2544,14 +2544,15 @@ exports[`Snapshot test 2`] = ` ], "UserData": { "Fn::Base64": "#!/bin/bash -yum install iptables-services -y +for i in {1..5}; do yum install iptables-services -y && break || sleep 10; done systemctl enable iptables systemctl start iptables echo "net.ipv4.ip_forward=1" > /etc/sysctl.d/custom-ip-forwarding.conf -sudo sysctl -p /etc/sysctl.d/custom-ip-forwarding.conf -sudo /sbin/iptables -t nat -A POSTROUTING -o $(route | awk '/^default/{print $NF}') -j MASQUERADE -sudo /sbin/iptables -F FORWARD -sudo service iptables save", +sysctl -p /etc/sysctl.d/custom-ip-forwarding.conf +IFACE=$(ip route show default | awk '{print $5}') +/sbin/iptables -t nat -A POSTROUTING -o $IFACE -j MASQUERADE +/sbin/iptables -F FORWARD +service iptables save", }, }, "Type": "AWS::EC2::Instance", @@ -3490,6 +3491,38 @@ sudo service iptables save", }, "Type": "AWS::IAM::Policy", }, + "WebappInvokeFunctionPermission8F3F2610": { + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "WebappHandler8DD158A3", + "Arn", + ], + }, + "Principal": "cloudfront.amazonaws.com", + "SourceArn": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition", + }, + ":cloudfront::", + { + "Ref": "AWS::AccountId", + }, + ":distribution/", + { + "Ref": "Webapp107041BD", + }, + ], + ], + }, + }, + "Type": "AWS::Lambda::Permission", + }, "WebappMigrationRunnerAC67C012": { "DependsOn": [ "VpcPrivateSubnet1DefaultRouteBE02A9ED",