Skip to content

Commit 5e40898

Browse files
authored
Merge pull request #2468 from samudurand/cloudfront-lambda-url-iam-cdk-ts
New serverless pattern - cloudfront-lambda-url-iam-cdk-ts
2 parents a8d042a + aca64f7 commit 5e40898

File tree

13 files changed

+532
-0
lines changed

13 files changed

+532
-0
lines changed
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# CloudFront to Lambda URL
2+
3+
This pattern demonstrates how to setup Amazon CloudFront to proxy and cache traffic to AWS Lambda Function URLs, secured via IAM and Lambda@Edge.
4+
5+
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.
6+
7+
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.
8+
9+
Learn more about this pattern at Serverless Land Patterns: https://serverlessland.com/patterns/cloudfront-lambda-url-iam-cdk-ts.
10+
11+
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.
12+
13+
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.
14+
15+
## Requirements
16+
17+
* [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.
18+
* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured with your account's credentials
19+
* [Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) installed
20+
* [NodeJS](https://nodejs.org/en/download) installed
21+
22+
## Deployment Instructions
23+
24+
1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository:
25+
```bash
26+
git clone https://github.com/aws-samples/serverless-patterns
27+
```
28+
1. Modify the `cdk/bin/cdk.ts` and `cdk/lib/lambda-edge/authEdge.js` with your prefered region (the default is `eu-central-1`)
29+
1. Change directory to the pattern directory:
30+
```bash
31+
cd cloudfront-lambda-url-iam-cdk-ts/cdk
32+
```
33+
1. From the command line, install the Node.js dependencies, bootstrap CDK in your AWS account, and finally deploy the pattern:
34+
```bash
35+
npm i
36+
npx cdk bootstrap
37+
npx cdk deploy CdkStack
38+
```
39+
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.
40+
41+
## How it works
42+
43+
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.
44+
45+
## Testing
46+
47+
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.
48+
49+
```bash
50+
{"message":"Hello, world!"}
51+
```
52+
53+
## Cleanup
54+
55+
Delete all deployed resources
56+
57+
```bash
58+
npx cdk destroy
59+
```
60+
61+
----
62+
Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved.
63+
64+
SPDX-License-Identifier: MIT-0
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
*.js
2+
!lambda-edge/*.js
3+
!jest.config.js
4+
*.d.ts
5+
node_modules
6+
7+
# CDK asset staging directory
8+
.cdk.staging
9+
cdk.out
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
*.ts
2+
!*.d.ts
3+
4+
# CDK asset staging directory
5+
.cdk.staging
6+
cdk.out
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
#!/usr/bin/env node
2+
import 'source-map-support/register';
3+
import * as cdk from 'aws-cdk-lib';
4+
import { CdkStack } from '../lib/cdk-stack';
5+
6+
const app = new cdk.App();
7+
new CdkStack(app, 'CdkStack', {
8+
env: {
9+
region: 'eu-central-1' // Modify to fit your own region
10+
}
11+
});
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"app": "npx ts-node --prefer-ts-exts bin/cdk.ts",
3+
"watch": {
4+
"include": [
5+
"**"
6+
],
7+
"exclude": [
8+
"README.md",
9+
"cdk*.json",
10+
"**/*.d.ts",
11+
"**/*.js",
12+
"tsconfig.json",
13+
"package*.json",
14+
"yarn.lock",
15+
"node_modules",
16+
"test"
17+
]
18+
},
19+
"context": {
20+
"@aws-cdk/aws-lambda:recognizeLayerVersion": true,
21+
"@aws-cdk/core:checkSecretUsage": true,
22+
"@aws-cdk/core:target-partitions": [
23+
"aws",
24+
"aws-cn"
25+
],
26+
"@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true,
27+
"@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true,
28+
"@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true,
29+
"@aws-cdk/aws-iam:minimizePolicies": true,
30+
"@aws-cdk/core:validateSnapshotRemovalPolicy": true,
31+
"@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true,
32+
"@aws-cdk/aws-s3:createDefaultLoggingPolicy": true,
33+
"@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true,
34+
"@aws-cdk/aws-apigateway:disableCloudWatchRole": true,
35+
"@aws-cdk/core:enablePartitionLiterals": true,
36+
"@aws-cdk/aws-events:eventsTargetQueueSameAccount": true,
37+
"@aws-cdk/aws-iam:standardizedServicePrincipals": true,
38+
"@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true,
39+
"@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true,
40+
"@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true,
41+
"@aws-cdk/aws-route53-patters:useCertificate": true,
42+
"@aws-cdk/customresources:installLatestAwsSdkDefault": false,
43+
"@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true,
44+
"@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true,
45+
"@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true,
46+
"@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true,
47+
"@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true,
48+
"@aws-cdk/aws-redshift:columnId": true,
49+
"@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true,
50+
"@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true,
51+
"@aws-cdk/aws-apigateway:requestValidatorUniqueId": true,
52+
"@aws-cdk/aws-kms:aliasNameRef": true,
53+
"@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true,
54+
"@aws-cdk/core:includePrefixInUniqueNameGeneration": true,
55+
"@aws-cdk/aws-efs:denyAnonymousAccess": true,
56+
"@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true,
57+
"@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true,
58+
"@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true
59+
}
60+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
const axios = require('axios');
2+
const { SignatureV4 } = require('@aws-sdk/signature-v4');
3+
const { Sha256 } = require('@aws-crypto/sha256-js');
4+
5+
const {
6+
AWS_ACCESS_KEY_ID,
7+
AWS_SECRET_ACCESS_KEY,
8+
AWS_SESSION_TOKEN,
9+
} = process.env;
10+
11+
const sigv4 = new SignatureV4({
12+
service: 'lambda',
13+
region: 'eu-central-1', // Modify to fit your own region
14+
credentials: {
15+
accessKeyId: AWS_ACCESS_KEY_ID,
16+
secretAccessKey: AWS_SECRET_ACCESS_KEY,
17+
sessionToken: AWS_SESSION_TOKEN,
18+
},
19+
sha256: Sha256,
20+
});
21+
22+
module.exports.handler = async (event) => {
23+
const cfRequest = event.Records[0].cf.request;
24+
const headers = cfRequest.headers;
25+
26+
const apiUrl = new URL(`https://${cfRequest.origin.custom.domainName}${cfRequest.uri}`);
27+
28+
const signV4Options = {
29+
method: cfRequest.method,
30+
hostname: apiUrl.host,
31+
path: apiUrl.pathname + (cfRequest.querystring ? `?${cfRequest.querystring}` : ''),
32+
protocol: apiUrl.protocol,
33+
query: cfRequest.querystring,
34+
headers: {
35+
host: apiUrl.hostname
36+
},
37+
};
38+
39+
try {
40+
return await signAndForwardRequest(signV4Options, apiUrl);
41+
} catch (error) {
42+
console.error('An error occurred', error);
43+
return {
44+
status: '500',
45+
statusDescription: 'Internal Server Error',
46+
body: 'Internal Server Error',
47+
};
48+
}
49+
};
50+
51+
async function signAndForwardRequest(signV4Options, apiUrl) {
52+
const signed = await sigv4.sign(signV4Options);
53+
const result = await axios({
54+
...signed,
55+
url: apiUrl.href,
56+
timeout: 5000
57+
});
58+
59+
return {
60+
status: '200',
61+
statusDescription: 'OK',
62+
body: JSON.stringify(result.data),
63+
};
64+
}
65+
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"name": "auth-lambda-edge",
3+
"version": "0.1.0",
4+
"devDependencies": {
5+
"@types/aws-lambda": "^8.10.119",
6+
"aws-sdk": "^2.1452.0"
7+
},
8+
"dependencies": {
9+
"@aws-crypto/sha256-js": "^5.2.0",
10+
"@aws-sdk/credential-providers": "^3.624.0",
11+
"@aws-sdk/protocol-http": "^3.374.0",
12+
"@aws-sdk/signature-v4": "^3.374.0",
13+
"axios": "^1.7.3"
14+
}
15+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export const handler = async () => {
2+
return {
3+
statusCode: 200,
4+
body: JSON.stringify({ message: 'Hello, world!' }),
5+
};
6+
};
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import * as cdk from 'aws-cdk-lib';
2+
import { Construct } from 'constructs';
3+
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
4+
import * as lambda from 'aws-cdk-lib/aws-lambda';
5+
import * as lambdaNode from 'aws-cdk-lib/aws-lambda-nodejs';
6+
import path = require('path');
7+
import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam';
8+
9+
export class CdkStack extends cdk.Stack {
10+
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
11+
super(scope, id, props);
12+
13+
// Create the Lambda
14+
const simpleLambda = new lambdaNode.NodejsFunction(this, 'simpleLambda', {
15+
entry: 'lambda/handler.ts',
16+
handler: 'handler',
17+
runtime: lambda.Runtime.NODEJS_18_X,
18+
functionName: 'simpleLambda'
19+
});
20+
21+
const authFunction = this.createAuthEdgeFunction(simpleLambda.functionArn);
22+
23+
// Configure the Lambda URL
24+
const lambdaUrl = simpleLambda.addFunctionUrl({
25+
authType: lambda.FunctionUrlAuthType.AWS_IAM,
26+
});
27+
28+
// Create the CloudFront distribution redirecting calls to the Lambda URL
29+
const cfDistribution = new cloudfront.CloudFrontWebDistribution(this, 'CFDistribution', {
30+
originConfigs: [
31+
{
32+
customOriginSource: {
33+
domainName: this.getURLDomain(lambdaUrl),
34+
originProtocolPolicy: cloudfront.OriginProtocolPolicy.HTTPS_ONLY,
35+
},
36+
behaviors: [{
37+
isDefaultBehavior: true,
38+
allowedMethods: cloudfront.CloudFrontAllowedMethods.ALL,
39+
lambdaFunctionAssociations: [{
40+
eventType: cloudfront.LambdaEdgeEventType.ORIGIN_REQUEST,
41+
lambdaFunction: authFunction.currentVersion,
42+
includeBody: true
43+
}],
44+
}],
45+
}
46+
],
47+
});
48+
49+
new cdk.CfnOutput(this, 'CloudFrontDistributionURL', {
50+
value: cfDistribution.distributionDomainName,
51+
});
52+
53+
}
54+
55+
/**
56+
* Extracts the domain from a Lambda URL
57+
*
58+
* Example: https://my-lambda.execute-api.us-east-1.amazonaws.com/ -> my-lambda.execute-api.us-east-1.amazonaws.com
59+
*/
60+
getURLDomain(lambdaUrl: lambda.FunctionUrl) {
61+
return cdk.Fn.select(2, cdk.Fn.split('/', lambdaUrl.url));
62+
}
63+
64+
private createAuthEdgeFunction(functionArn: string) {
65+
const authFunction = new cloudfront.experimental.EdgeFunction(this, 'AuthLambdaEdge', {
66+
handler: 'authEdge.handler',
67+
runtime: lambda.Runtime.NODEJS_16_X,
68+
code: lambda.Code.fromAsset(path.join(__dirname, '../lambda-edge'), {
69+
bundling: {
70+
command: [
71+
"bash",
72+
"-c",
73+
"npm install && cp -rT /asset-input/ /asset-output/",
74+
],
75+
image: lambda.Runtime.NODEJS_16_X.bundlingImage,
76+
user: "root",
77+
},
78+
}),
79+
currentVersionOptions: {
80+
removalPolicy: cdk.RemovalPolicy.DESTROY
81+
},
82+
timeout: cdk.Duration.seconds(7),
83+
});
84+
85+
authFunction.addToRolePolicy(new PolicyStatement({
86+
sid: 'AllowInvokeFunctionUrl',
87+
effect: Effect.ALLOW,
88+
actions: ['lambda:InvokeFunctionUrl'],
89+
resources: [functionArn],
90+
conditions: {
91+
"StringEquals": { "lambda:FunctionUrlAuthType": "AWS_IAM" }
92+
}
93+
}));
94+
return authFunction;
95+
}
96+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "cdk",
3+
"version": "0.1.0",
4+
"bin": {
5+
"cdk": "bin/cdk.js"
6+
},
7+
"scripts": {
8+
"build": "tsc",
9+
"watch": "tsc -w",
10+
"cdk": "cdk",
11+
"postinstall": "cd lambda-edge && npm i"
12+
},
13+
"devDependencies": {
14+
"@types/jest": "^29.5.4",
15+
"@types/node": "20.5.9",
16+
"jest": "^29.6.4",
17+
"aws-cdk": "2.96.2",
18+
"ts-node": "^10.9.1",
19+
"typescript": "~5.2.2"
20+
},
21+
"dependencies": {
22+
"aws-cdk-lib": "2.96.2",
23+
"constructs": "^10.0.0",
24+
"source-map-support": "^0.5.21"
25+
}
26+
}

0 commit comments

Comments
 (0)