Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions typescript/cloudfront-functions/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
!resources/**
!jest.config.js
*.d.ts
node_modules

# CDK asset staging directory
.cdk.staging
cdk.out
.DS_Store
6 changes: 6 additions & 0 deletions typescript/cloudfront-functions/.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
76 changes: 76 additions & 0 deletions typescript/cloudfront-functions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# demo-cloudfront-functions

This project will create a S3 bucket with simple `html` files that will serve as our website source code, a
CloudFront distribution to serve this content as our CDN and, two CloudFront functions that will work upon the request
and response.

> This project is intended to be just a sample demonstration. Please, do not use it in production.

## CDK Toolkit

The `cdk.json` file tells the CDK Toolkit how to execute your app.

To start working with the project, first you will need to install all dependencies as well as the cdk module (if not
installed already). In the project directory, run:

```bash
$ npm install -g aws-cdk
$ npm install
```

## Deploying the solution

To deploy the solution, we will need to request cdk to deploy the stack:

```shell
$ cdk deploy --all
```

> **Note** that after running the deploy command, you will be presented and Output in the console like bellow:\
> `DemoCloudfrontFunctionsStack.DistributionDomainName = xxxxxxxx.cloudfront.net`\
> We will use this URL to access and test the website.

## Testing the solution

To begin the tests, you must have the distribution's URL (returned by cdk execution with the
name `DistributionDomainName`), and web browser capable of analysing Network requests and responses (e.g. Google Chrome
with Developer Tools enabled) or similar tool (e.g. curl, wget).

### Testing the index url

1. Access the base distribution URL
2. A return code 200 is returned
3. The response body will contain the text `It works!`

### Testing the index url with query strings

1. Access the base distribution URL, appending `?foo=bar` at its end
2. A return code 200 is returned
3. The response body will contain the text `It works!`

### Testing the test route

1. Access the base distribution URL, appending `/test` or `/test.html` at its end
2. A return code 308 (permanent redirect) is returned
3. The url will have changed to `/subdir/test.html`
4. A return code 200 is returned
5. The response body will contain the text `This is a test file for you`

### Testing the invalid route

1. Access the base distribution URL, appending `/invalid` at its end
2. A return code 403 is returned
3. There won't be any response body

### Other test cases

You can explore the `/reources/functions/request-function.js` and `/reources/functions/response-function.js` for
more handling rules. Some of them are validated through _CloudWatch Logs_ and _X-Ray_ given their nature.

## Destroying the deployment

To destroy the provisioned infrastructure, you can simply run the following command:

```shell
$ cdk destroy --all
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env node
import 'source-map-support/register';
import * as cdk from 'aws-cdk-lib';
import {DemoCloudfrontFunctionsStack} from '../lib/demo-cloudfront-functions-stack';

const app = new cdk.App();

new DemoCloudfrontFunctionsStack(app, 'DemoCloudfrontFunctionsStack', {});
64 changes: 64 additions & 0 deletions typescript/cloudfront-functions/cdk.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"app": "npx ts-node --prefer-ts-exts bin/demo-cloudfront-functions.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,
"@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true,
"@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true,
"@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true,
"@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true
}
}
8 changes: 8 additions & 0 deletions typescript/cloudfront-functions/jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module.exports = {
testEnvironment: 'node',
roots: ['<rootDir>/test'],
testMatch: ['**/*.test.ts'],
transform: {
'^.+\\.tsx?$': 'ts-jest'
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import * as cdk from 'aws-cdk-lib';
import {RemovalPolicy} from 'aws-cdk-lib';
import {Construct} from 'constructs';
import {BlockPublicAccess, Bucket, BucketEncryption} from "aws-cdk-lib/aws-s3";
import {BucketDeployment, Source} from "aws-cdk-lib/aws-s3-deployment";
import {
AllowedMethods,
Distribution,
Function,
FunctionCode,
FunctionEventType,
FunctionRuntime,
OriginAccessIdentity
} from "aws-cdk-lib/aws-cloudfront";
import {BehaviorOptions} from "aws-cdk-lib/aws-cloudfront/lib/distribution";
import {S3Origin} from "aws-cdk-lib/aws-cloudfront-origins";

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

// create a bucket to deploy the website files
const bucket = new Bucket(this, 'WebsiteBucket', {
encryption: BucketEncryption.S3_MANAGED,
versioned: true,
enforceSSL: true,
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
removalPolicy: RemovalPolicy.DESTROY
});

// create a s3 bucket deployment to deploy the website directory's files to the website bucket
new BucketDeployment(this, 'WebsiteFiles', {
destinationBucket: bucket,
sources: [Source.asset('./website')],
contentType: 'text/html',
retainOnDelete: false,
});

// create an Origin Access Identity for CloudFront
const cloudfrontOAI = new OriginAccessIdentity(this, 'cloudfront-OAI', {
comment: `OAI for ${id}`
});

// grant read permissions on the bucket to the CloudFront's Origin Access Identity
bucket.grantRead(cloudfrontOAI);

// create a cloudFront function from the request-function.js file
const requestFunction = new Function(this, 'RequestFunction', {
functionName: 'RequestFunction',
runtime: FunctionRuntime.JS_2_0,
code: FunctionCode.fromFile({
filePath: './resources/functions/request-function.js'
})
});

// create a cloudFront function from the response-function.js file
const responseFunction = new Function(this, 'ResponseFunction', {
functionName: 'ResponseFunction',
runtime: FunctionRuntime.JS_2_0,
code: FunctionCode.fromFile({
filePath: './resources/functions/response-function.js'
})
});

// create a CloudFront behavior with origin of my website bucket and both request and response functions
const defaultBehavior: BehaviorOptions = {
origin: new S3Origin(bucket),
compress: true,
allowedMethods: AllowedMethods.ALLOW_ALL,
functionAssociations: [
{
function: requestFunction,
eventType: FunctionEventType.VIEWER_REQUEST,
},
{
function: responseFunction,
eventType: FunctionEventType.VIEWER_RESPONSE,
}
]
};

// create a CloudFront distribution with the behavior created
const distribution = new Distribution(this, 'SiteDistribution', {
comment: 'CloudFront Functions example',
defaultRootObject: 'index.html',
defaultBehavior: defaultBehavior,
});

// create an output with the CloudFront distribution URL
new cdk.CfnOutput(this, 'DistributionDomainName', {
value: distribution.domainName,
});

}
}
27 changes: 27 additions & 0 deletions typescript/cloudfront-functions/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "demo-cloudfront-functions",
"version": "0.1.0",
"bin": {
"demo-cloudfront-functions": "bin/demo-cloudfront-functions.js"
},
"scripts": {
"build": "tsc",
"watch": "tsc -w",
"test": "jest",
"cdk": "cdk"
},
"devDependencies": {
"@types/jest": "^29.5.11",
"@types/node": "20.11.6",
"jest": "^29.7.0",
"ts-jest": "^29.1.2",
"aws-cdk": "2.123.0",
"ts-node": "^10.9.2",
"typescript": "~5.3.3"
},
"dependencies": {
"aws-cdk-lib": "2.123.0",
"constructs": "^10.0.0",
"source-map-support": "^0.5.21"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
function handler(event) {
// you can log the entire event if you need
// console.log(event)

// query string validation and overwriting
if (event.request.uri === '/' && event.request.querystring !== {}) {
event.request.querystring = {};
}

// url redirects based on a list of URIs
const domain = event.request.headers.host.value;
const redirects = {
'/test.html': `https://${domain}/subdir/test.html`,
'/test': `https://${domain}/subdir/test.html`
}
const redirectUrl = redirects[event.request.uri];

if (redirectUrl) {
return {
statusCode: 308,
statusDescription: 'Permanent Redirect',
headers: {
'location': {value: redirectUrl}
}
};
}

// url validation and authorization
if (new RegExp('^/invalid').test(event.request.uri)) {
return {
statusCode: 403,
statusDescription: 'Forbidden',
};
}

// header validation and overwriting
if (event.request.headers['x-correlation-id'] && event.request.headers['x-correlation-id'].value === 'abcde') {
event.request.headers['x-correlation-id'].value = 'random-correlation-id';
}

// cookie validation and overwriting
if (event.request.cookies['foo'] && event.request.cookies['foo'].value === 'bar') {
event.request.cookies['should-cache'] = {value: 'true'}
}

return event.request
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
function handler(event) {
// you can log the entire event if you need
console.log(event)

// changing the cache ttl for individual objects
if (event.request.uri.endsWith('test.html')) {
event.response.headers['cache-control'] = {value: 'max-age=60'}
event.response.headers['x-test-header'] = {value: 'this is a test'}
}

return event.response
}
Loading
Loading