Skip to content

Commit

Permalink
Merge pull request #2468 from samudurand/cloudfront-lambda-url-iam-cd…
Browse files Browse the repository at this point in the history
…k-ts

New serverless pattern - cloudfront-lambda-url-iam-cdk-ts
  • Loading branch information
julianwood authored Jan 6, 2025
2 parents a8d042a + aca64f7 commit 5e40898
Show file tree
Hide file tree
Showing 13 changed files with 532 additions and 0 deletions.
64 changes: 64 additions & 0 deletions cloudfront-lambda-url-iam-cdk-ts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# CloudFront to Lambda URL

This pattern demonstrates how to setup Amazon CloudFront to proxy and cache traffic to AWS Lambda Function URLs, secured via IAM and Lambda@Edge.

A function URL is a dedicated endpoint for your Lambda function. When you create a function URL, Lambda automatically generates a unique URL endpoint for you. Unlike API Gateway endpoints, using this URL does not incur any additional charges, beyond the usual data transfer costs.

By configuring CloudFront in front of the Lambda Function URL endpoint you can use custom domain names, Cognito authentication via Lambda@Edge, AWS Web Application Firewall (WAF) and AWS Shield Advanced to protect your endpoint from attacks.

Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/cloudfront-lambda-url-iam-cdk-ts.

Note: the Lambda@Edge uses pure Javascript instead of Typescript due to the limitations of the [Lambda@Edge CDK construct](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudfront.experimental.EdgeFunction.html), which does not offer a native Typescript packaging option.

Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example.

## Requirements

* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources.
* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured with your account's credentials
* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed
* [NodeJS](https://nodejs.org/en/download) installed

## Deployment Instructions

1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository:
```bash
git clone https://github.com/aws-samples/serverless-patterns
```
1. Modify the `cdk/bin/cdk.ts` and `cdk/lib/lambda-edge/authEdge.js` with your prefered region (the default is `eu-central-1`)
1. Change directory to the pattern directory:
```bash
cd cloudfront-lambda-url-iam-cdk-ts/cdk
```
1. From the command line, install the Node.js dependencies, bootstrap CDK in your AWS account, and finally deploy the pattern:
```bash
npm i
npx cdk bootstrap
npx cdk deploy CdkStack
```
1. Note the outputs from the CDK deployment. The `CdkStack.CloudFrontDistributionURL` contains the URL of the cloudfront distribution that you can use to test the deployment.

## How it works

An Amazon CloudFront distribution is created that forwards requests to the domain name of the deployed AWS Lambda function URL. Amazon CloudFront also caches responses from the Lambda function. The Lambda URL is protected via IAM and can only be called via the CloudFront distribution which includes a Lambda@Edge adding the necessary IAM credentials.

## Testing

Copy the url of the CloudFront distribution that you can find in the `cdk deploy` command output, called `CdkStack.CloudFrontDistributionURL`. Paste this URL in a browser and you will get a JSON response.

```bash
{"message":"Hello, world!"}
```

## Cleanup

Delete all deployed resources

```bash
npx cdk destroy
```

----
Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.

SPDX-License-Identifier: MIT-0
9 changes: 9 additions & 0 deletions cloudfront-lambda-url-iam-cdk-ts/cdk/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
*.js
!lambda-edge/*.js
!jest.config.js
*.d.ts
node_modules

# CDK asset staging directory
.cdk.staging
cdk.out
6 changes: 6 additions & 0 deletions cloudfront-lambda-url-iam-cdk-ts/cdk/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
*.ts
!*.d.ts

# CDK asset staging directory
.cdk.staging
cdk.out
11 changes: 11 additions & 0 deletions cloudfront-lambda-url-iam-cdk-ts/cdk/bin/cdk.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import { CdkStack } from '../lib/cdk-stack';

const app = new cdk.App();
new CdkStack(app, 'CdkStack', {
env: {
region: 'eu-central-1' // Modify to fit your own region
}
});
60 changes: 60 additions & 0 deletions cloudfront-lambda-url-iam-cdk-ts/cdk/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"app": "npx ts-node --prefer-ts-exts bin/cdk.ts",
"watch": {
"include": [
"**"
],
"exclude": [
"README.md",
"cdk*.json",
"**/*.d.ts",
"**/*.js",
"tsconfig.json",
"package*.json",
"yarn.lock",
"node_modules",
"test"
]
},
"context": {
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
"@aws-cdk/core:checkSecretUsage": true,
"@aws-cdk/core:target-partitions": [
"aws",
"aws-cn"
],
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
"@aws-cdk/aws-iam:minimizePolicies": true,
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
"@aws-cdk/core:enablePartitionLiterals": true,
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
"@aws-cdk/aws-iam:standardizedServicePrincipals": true,
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
"@aws-cdk/aws-route53-patters:useCertificate": true,
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
"@aws-cdk/aws-redshift:columnId": true,
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
"@aws-cdk/aws-kms:aliasNameRef": true,
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
"@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
"@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true
}
}
65 changes: 65 additions & 0 deletions cloudfront-lambda-url-iam-cdk-ts/cdk/lambda-edge/authEdge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const axios = require('axios');
const { SignatureV4 } = require('@aws-sdk/signature-v4');
const { Sha256 } = require('@aws-crypto/sha256-js');

const {
AWS_ACCESS_KEY_ID,
AWS_SECRET_ACCESS_KEY,
AWS_SESSION_TOKEN,
} = process.env;

const sigv4 = new SignatureV4({
service: 'lambda',
region: 'eu-central-1', // Modify to fit your own region
credentials: {
accessKeyId: AWS_ACCESS_KEY_ID,
secretAccessKey: AWS_SECRET_ACCESS_KEY,
sessionToken: AWS_SESSION_TOKEN,
},
sha256: Sha256,
});

module.exports.handler = async (event) => {
const cfRequest = event.Records[0].cf.request;
const headers = cfRequest.headers;

const apiUrl = new URL(`https://${cfRequest.origin.custom.domainName}${cfRequest.uri}`);

const signV4Options = {
method: cfRequest.method,
hostname: apiUrl.host,
path: apiUrl.pathname + (cfRequest.querystring ? `?${cfRequest.querystring}` : ''),
protocol: apiUrl.protocol,
query: cfRequest.querystring,
headers: {
host: apiUrl.hostname
},
};

try {
return await signAndForwardRequest(signV4Options, apiUrl);
} catch (error) {
console.error('An error occurred', error);
return {
status: '500',
statusDescription: 'Internal Server Error',
body: 'Internal Server Error',
};
}
};

async function signAndForwardRequest(signV4Options, apiUrl) {
const signed = await sigv4.sign(signV4Options);
const result = await axios({
...signed,
url: apiUrl.href,
timeout: 5000
});

return {
status: '200',
statusDescription: 'OK',
body: JSON.stringify(result.data),
};
}

15 changes: 15 additions & 0 deletions cloudfront-lambda-url-iam-cdk-ts/cdk/lambda-edge/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"name": "auth-lambda-edge",
"version": "0.1.0",
"devDependencies": {
"@types/aws-lambda": "^8.10.119",
"aws-sdk": "^2.1452.0"
},
"dependencies": {
"@aws-crypto/sha256-js": "^5.2.0",
"@aws-sdk/credential-providers": "^3.624.0",
"@aws-sdk/protocol-http": "^3.374.0",
"@aws-sdk/signature-v4": "^3.374.0",
"axios": "^1.7.3"
}
}
6 changes: 6 additions & 0 deletions cloudfront-lambda-url-iam-cdk-ts/cdk/lambda/handler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export const handler = async () => {
return {
statusCode: 200,
body: JSON.stringify({ message: 'Hello, world!' }),
};
};
96 changes: 96 additions & 0 deletions cloudfront-lambda-url-iam-cdk-ts/cdk/lib/cdk-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import * as lambdaNode from 'aws-cdk-lib/aws-lambda-nodejs';
import path = require('path');
import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam';

export class CdkStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);

// Create the Lambda
const simpleLambda = new lambdaNode.NodejsFunction(this, 'simpleLambda', {
entry: 'lambda/handler.ts',
handler: 'handler',
runtime: lambda.Runtime.NODEJS_18_X,
functionName: 'simpleLambda'
});

const authFunction = this.createAuthEdgeFunction(simpleLambda.functionArn);

// Configure the Lambda URL
const lambdaUrl = simpleLambda.addFunctionUrl({
authType: lambda.FunctionUrlAuthType.AWS_IAM,
});

// Create the CloudFront distribution redirecting calls to the Lambda URL
const cfDistribution = new cloudfront.CloudFrontWebDistribution(this, 'CFDistribution', {
originConfigs: [
{
customOriginSource: {
domainName: this.getURLDomain(lambdaUrl),
originProtocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY,
},
behaviors: [{
isDefaultBehavior: true,
allowedMethods: cloudfront.CloudFrontAllowedMethods.ALL,
lambdaFunctionAssociations: [{
eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST,
lambdaFunction: authFunction.currentVersion,
includeBody: true
}],
}],
}
],
});

new cdk.CfnOutput(this, 'CloudFrontDistributionURL', {
value: cfDistribution.distributionDomainName,
});

}

/**
* Extracts the domain from a Lambda URL
*
* Example: https://my-lambda.execute-api.us-east-1.amazonaws.com/ -> my-lambda.execute-api.us-east-1.amazonaws.com
*/
getURLDomain(lambdaUrl: lambda.FunctionUrl) {
return cdk.Fn.select(2, cdk.Fn.split('/', lambdaUrl.url));
}

private createAuthEdgeFunction(functionArn: string) {
const authFunction = new cloudfront.experimental.EdgeFunction(this, 'AuthLambdaEdge', {
handler: 'authEdge.handler',
runtime: lambda.Runtime.NODEJS_16_X,
code: lambda.Code.fromAsset(path.join(__dirname, '../lambda-edge'), {
bundling: {
command: [
"bash",
"-c",
"npm install && cp -rT /asset-input/ /asset-output/",
],
image: lambda.Runtime.NODEJS_16_X.bundlingImage,
user: "root",
},
}),
currentVersionOptions: {
removalPolicy: cdk.RemovalPolicy.DESTROY
},
timeout: cdk.Duration.seconds(7),
});

authFunction.addToRolePolicy(new PolicyStatement({
sid: 'AllowInvokeFunctionUrl',
effect: Effect.ALLOW,
actions: ['lambda:InvokeFunctionUrl'],
resources: [functionArn],
conditions: {
"StringEquals": { "lambda:FunctionUrlAuthType": "AWS_IAM" }
}
}));
return authFunction;
}
}
26 changes: 26 additions & 0 deletions cloudfront-lambda-url-iam-cdk-ts/cdk/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"name": "cdk",
"version": "0.1.0",
"bin": {
"cdk": "bin/cdk.js"
},
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"cdk": "cdk",
"postinstall": "cd lambda-edge && npm i"
},
"devDependencies": {
"@types/jest": "^29.5.4",
"@types/node": "20.5.9",
"jest": "^29.6.4",
"aws-cdk": "2.96.2",
"ts-node": "^10.9.1",
"typescript": "~5.2.2"
},
"dependencies": {
"aws-cdk-lib": "2.96.2",
"constructs": "^10.0.0",
"source-map-support": "^0.5.21"
}
}
Loading

0 comments on commit 5e40898

Please sign in to comment.