Skip to content

Commit af96158

Browse files
Restructure files, extract solution into a separate file
1 parent ed3c4d3 commit af96158

5 files changed

Lines changed: 219 additions & 175 deletions

lib/cdk-context-utils.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
1-
import { Stack } from "aws-cdk-lib";
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: MIT-0
3+
import { Node } from "constructs";
24

3-
export const getContextVariables = (stack: Stack) => {
4-
const { node } = stack;
5-
return {
6-
boolean: (key: string, defaultValue = false) => node.tryGetContext(key) ? Boolean(node.tryGetContext(key)) : defaultValue,
7-
number: (key: string, defaultValue: number) => node.tryGetContext(key) ? Number(node.tryGetContext(key)) : defaultValue,
8-
string: (key: string, defaultValue: string) => node.tryGetContext(key) ? String(node.tryGetContext(key)) : defaultValue,
9-
stringOrUndefined: (key: string) => node.tryGetContext(key) ? String(node.tryGetContext(key)) : undefined,
10-
}
11-
}
5+
export const readContext = (node: Node) => ({
6+
boolean: (key: string, defaultValue = false) => node.tryGetContext(key) ? Boolean(node.tryGetContext(key)) : defaultValue,
7+
number: (key: string, defaultValue: number) => node.tryGetContext(key) ? Number(node.tryGetContext(key)) : defaultValue,
8+
string: (key: string, defaultValue: string) => node.tryGetContext(key) ? String(node.tryGetContext(key)) : defaultValue,
9+
stringOrUndefined: (key: string) => node.tryGetContext(key) ? String(node.tryGetContext(key)) : undefined,
10+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: MIT-0
3+
import { RemovalPolicy, Stack } from 'aws-cdk-lib';
4+
import { BlockPublicAccess, Bucket, BucketEncryption } from 'aws-cdk-lib/aws-s3';
5+
import { Distribution, ViewerProtocolPolicy } from 'aws-cdk-lib/aws-cloudfront';
6+
import { S3BucketOrigin } from 'aws-cdk-lib/aws-cloudfront-origins';
7+
8+
export const sampleWebsite = (stack: Stack) => {
9+
const bucket = new Bucket(stack, 's3-sample-website-bucket', {
10+
autoDeleteObjects: true,
11+
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
12+
encryption: BucketEncryption.S3_MANAGED,
13+
enforceSSL: true,
14+
removalPolicy: RemovalPolicy.DESTROY
15+
});
16+
return new Distribution(stack, 'websiteDeliveryDistribution', {
17+
comment: 'Image Optimization - sample website',
18+
defaultBehavior: {
19+
origin: S3BucketOrigin.withOriginAccessControl(bucket),
20+
viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS
21+
},
22+
defaultRootObject: 'index.html'
23+
});
24+
}

lib/image-optimization-solution.ts

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: MIT-0
3+
import {
4+
aws_cloudfront as cloudfront,
5+
aws_cloudfront_origins as origins,
6+
aws_iam as iam,
7+
aws_lambda as lambda,
8+
aws_logs as logs,
9+
aws_s3 as s3,
10+
Duration, Fn, RemovalPolicy, Stack
11+
} from 'aws-cdk-lib';
12+
13+
type ImageProcessingProps = {
14+
corsEnabled: boolean,
15+
originalImageBucket: s3.IBucket,
16+
originShieldRegion: string,
17+
lambdaMemory: number,
18+
lambdaTimeout: Duration,
19+
maxImageSizeBytes: number,
20+
transformedImageCacheControl: string,
21+
transformedImageExpiration: Duration,
22+
storeTransformedImages: boolean,
23+
}
24+
25+
export const imageOptimizationSolution = (stack: Stack, props: ImageProcessingProps) => {
26+
const {
27+
corsEnabled,
28+
originalImageBucket,
29+
originShieldRegion,
30+
lambdaMemory,
31+
lambdaTimeout,
32+
maxImageSizeBytes,
33+
transformedImageCacheControl,
34+
transformedImageExpiration,
35+
storeTransformedImages,
36+
} = props;
37+
38+
// Create Lambda function for image processing
39+
const imageProcessing = new lambda.Function(stack, 'image-optimization', {
40+
code: lambda.Code.fromAsset('functions/image-processing'),
41+
environment: {
42+
maxImageSize: String(maxImageSizeBytes),
43+
originalImageBucketName: originalImageBucket.bucketName,
44+
transformedImageCacheTTL: transformedImageCacheControl,
45+
},
46+
handler: 'index.handler',
47+
// let downloads of original images from S3
48+
initialPolicy: [
49+
new iam.PolicyStatement({
50+
actions: ['s3:GetObject'],
51+
resources: [`arn:aws:s3:::${originalImageBucket.bucketName}/*`]
52+
}),
53+
],
54+
logRetention: logs.RetentionDays.ONE_DAY,
55+
memorySize: lambdaMemory,
56+
runtime: lambda.Runtime.NODEJS_20_X,
57+
timeout: lambdaTimeout,
58+
});
59+
60+
// Enable Lambda URL and create Amazon CloudFront origin
61+
const imageProcessingURL = imageProcessing.addFunctionUrl();
62+
const imageProcessingDomainName = Fn.parseDomainName(imageProcessingURL.url);
63+
const imageProcessingLambdaOrigin = new origins.HttpOrigin(imageProcessingDomainName, { originShieldRegion });
64+
65+
// Create custom response headers policy with CORS requests allowed for all origins
66+
const getCorsResponsePolicy = () => new cloudfront.ResponseHeadersPolicy(stack, 'cors-response-policy', {
67+
responseHeadersPolicyName: `CorsResponsePolicy${stack.node.addr}`,
68+
corsBehavior: {
69+
accessControlAllowCredentials: false,
70+
accessControlAllowHeaders: ['*'],
71+
accessControlAllowMethods: ['GET'],
72+
accessControlAllowOrigins: ['*'],
73+
accessControlMaxAge: Duration.seconds(600),
74+
originOverride: false,
75+
},
76+
// Recognize image requests that were processed by this solution
77+
customHeadersBehavior: {
78+
customHeaders: [
79+
{ header: 'x-aws-image-optimization', value: 'v1.0', override: true },
80+
{ header: 'vary', value: 'accept', override: true },
81+
]
82+
}
83+
});
84+
85+
// Create an S3 origin with fallback to Lambda
86+
const getS3OriginWithFallbackToLambda = () => {
87+
const transformedImageBucket = new s3.Bucket(stack, 's3-transformed-image-bucket', {
88+
autoDeleteObjects: true,
89+
lifecycleRules: [{ expiration: transformedImageExpiration }],
90+
removalPolicy: RemovalPolicy.DESTROY,
91+
});
92+
imageProcessing.addEnvironment('transformedImageBucketName', transformedImageBucket.bucketName);
93+
imageProcessing.role!.addToPrincipalPolicy(
94+
new iam.PolicyStatement({
95+
actions: ['s3:PutObject'],
96+
resources: [`arn:aws:s3:::${transformedImageBucket.bucketName}/*`]
97+
})
98+
);
99+
return new origins.OriginGroup({
100+
primaryOrigin: origins.S3BucketOrigin.withOriginAccessIdentity(transformedImageBucket, { originShieldRegion }),
101+
fallbackOrigin: imageProcessingLambdaOrigin,
102+
fallbackStatusCodes: [403, 500, 503, 504],
103+
});
104+
};
105+
106+
// Create content delivery distribution with Amazon CloudFront for optimized images
107+
const imageDelivery = new cloudfront.Distribution(stack, 'image-delivery-distribution', {
108+
comment: 'Image Optimization - image delivery',
109+
defaultBehavior: {
110+
cachePolicy: new cloudfront.CachePolicy(stack, `ImageCachePolicy${stack.node.addr}`, {
111+
defaultTtl: Duration.hours(24),
112+
maxTtl: Duration.days(365),
113+
minTtl: Duration.seconds(0),
114+
queryStringBehavior: cloudfront.CacheQueryStringBehavior.all()
115+
}),
116+
compress: false,
117+
functionAssociations: [{
118+
eventType: cloudfront.FunctionEventType.VIEWER_REQUEST,
119+
function: new cloudfront.Function(stack, 'urlRewrite', {
120+
code: cloudfront.FunctionCode.fromFile({ filePath: 'functions/url-rewrite/index.js' }),
121+
functionName: `urlRewriteFunction${stack.node.addr}`,
122+
})
123+
}],
124+
origin: storeTransformedImages ? getS3OriginWithFallbackToLambda() : imageProcessingLambdaOrigin,
125+
responseHeadersPolicy: corsEnabled ? getCorsResponsePolicy() : undefined,
126+
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
127+
}
128+
});
129+
130+
// Add OAC between CloudFront and LambdaURL
131+
const oac = new cloudfront.CfnOriginAccessControl(stack, 'origin-access-control', {
132+
originAccessControlConfig: {
133+
name: `oac${stack.node.addr}`,
134+
originAccessControlOriginType: 'lambda',
135+
signingBehavior: 'always',
136+
signingProtocol: 'sigv4',
137+
}
138+
});
139+
140+
const cfnImageDelivery = imageDelivery.node.defaultChild as cloudfront.CfnDistribution;
141+
cfnImageDelivery.addPropertyOverride(`DistributionConfig.Origins.${storeTransformedImages ? '1' : '0'}.OriginAccessControlId`, oac.getAtt('Id'));
142+
imageProcessing.addPermission('AllowCloudFrontServicePrincipal', {
143+
principal: new iam.ServicePrincipal("cloudfront.amazonaws.com"),
144+
action: 'lambda:InvokeFunctionUrl',
145+
sourceArn: `arn:aws:cloudfront::${stack.account}:distribution/${imageDelivery.distributionId}`
146+
});
147+
148+
return imageDelivery;
149+
}

lib/image-optimization-stack.ts

Lines changed: 37 additions & 133 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,44 @@
11
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: MIT-0
3-
import { aws_cloudfront as cloudfront, aws_cloudfront_origins as origins, aws_iam as iam, aws_lambda as lambda, aws_logs as logs, aws_s3 as s3, aws_s3_deployment as s3deploy, CfnOutput, Duration, Fn, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib';
3+
import { aws_s3 as s3, aws_s3_deployment as s3deploy, Duration, CfnOutput, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib';
44
import { Construct } from 'constructs';
5-
import { getContextVariables } from './cdk-context-utils';
5+
import { readContext } from './cdk-context-utils';
6+
import { sampleWebsite } from './image-optimization-sample-website';
7+
import { imageOptimizationSolution } from './image-optimization-solution';
68
import { getOriginShieldRegion } from './origin-shield';
7-
import { deploySampleWebsite } from './sample-website';
89

910
export class ImageOptimizationStack extends Stack {
1011
constructor(scope: Construct, id: string, props?: StackProps) {
1112
super(scope, id, props);
1213

1314
// Load stack parameters related to architecture from CDK context
14-
const getContext = getContextVariables(this);
15-
const CLOUDFRONT_CORS_ENABLED = getContext.boolean('CLOUDFRONT_CORS_ENABLED', true);
16-
const CLOUDFRONT_ORIGIN_SHIELD_REGION = getContext.string('CLOUDFRONT_ORIGIN_SHIELD_REGION', getOriginShieldRegion(process.env.AWS_REGION || process.env.CDK_DEFAULT_REGION || 'us-east-1'));
15+
const context = readContext(this.node);
1716

18-
const LAMBDA_MEMORY = getContext.number('LAMBDA_MEMORY', 1500);
19-
const LAMBDA_TIMEOUT_SECONDS = getContext.number('LAMBDA_TIMEOUT', 60);
20-
const MAX_IMAGE_SIZE = getContext.number('MAX_IMAGE_SIZE', 4700000);
17+
const CORS_ENABLED = context.boolean('CLOUDFRONT_CORS_ENABLED', true);
18+
const DEPLOY_SAMPLE_WEBSITE = context.boolean('DEPLOY_SAMPLE_WEBSITE');
19+
const LAMBDA_MEMORY = context.number('LAMBDA_MEMORY', 1500);
20+
const LAMBDA_TIMEOUT_SECONDS = context.number('LAMBDA_TIMEOUT', 60);
21+
const MAX_IMAGE_SIZE = context.number('MAX_IMAGE_SIZE', 4700000);
22+
const ORIGIN_SHIELD_REGION = context.string('CLOUDFRONT_ORIGIN_SHIELD_REGION', getOriginShieldRegion(process.env.AWS_REGION || process.env.CDK_DEFAULT_REGION || 'us-east-1'));
23+
const S3_ORIGINAL_IMAGE_BUCKET_NAME = context.stringOrUndefined('S3_IMAGE_BUCKET_NAME');
24+
const S3_TRANSFORMED_IMAGE_CACHE_CONTROL = context.string('S3_TRANSFORMED_IMAGE_CACHE_TTL', 'max-age=31622400');
25+
const S3_TRANSFORMED_IMAGE_EXPIRATION_DAYS = context.number('S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION', 90);
26+
const STORE_TRANSFORMED_IMAGES = context.boolean('STORE_TRANSFORMED_IMAGES', true);
2127

22-
const S3_IMAGE_BUCKET_NAME = getContext.stringOrUndefined('S3_IMAGE_BUCKET_NAME');
23-
const S3_TRANSFORMED_IMAGE_CACHE_TTL = getContext.string('S3_TRANSFORMED_IMAGE_CACHE_TTL', 'max-age=31622400');
24-
const S3_TRANSFORMED_IMAGE_EXPIRATION_DAYS = getContext.number('S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION', 90);
25-
const STORE_TRANSFORMED_IMAGES = getContext.boolean('STORE_TRANSFORMED_IMAGES', true);
26-
27-
// If DEPLOY_SAMPLE_WEBSITE is true, this stack will deploy an additional, sample website to showcase the solution
28+
// If true, this stack will deploy an additional, sample website to showcase the solution
2829
// Architecture of the sample website is described at https://aws.amazon.com/blogs/networking-and-content-delivery/image-optimization-using-amazon-cloudfront-and-aws-lambda/
29-
if (getContext.boolean('DEPLOY_SAMPLE_WEBSITE')) {
30-
deploySampleWebsite(this);
30+
if (DEPLOY_SAMPLE_WEBSITE) {
31+
const sampleWebsiteDelivery = sampleWebsite(this);
32+
new CfnOutput(this, 'SampleWebsiteDomain', {
33+
description: 'Sample website domain',
34+
value: sampleWebsiteDelivery.distributionDomainName
35+
});
3136
}
3237

33-
// ********************* Image Optimization Resources *********************
34-
3538
// For original images, use existing S3 bucket if provided, otherwise create a new one with sample images
3639
let originalImageBucket: s3.IBucket;
37-
if (S3_IMAGE_BUCKET_NAME) {
38-
originalImageBucket = s3.Bucket.fromBucketName(this, 'imported-original-image-bucket', S3_IMAGE_BUCKET_NAME);
40+
if (S3_ORIGINAL_IMAGE_BUCKET_NAME) {
41+
originalImageBucket = s3.Bucket.fromBucketName(this, 'imported-original-image-bucket', S3_ORIGINAL_IMAGE_BUCKET_NAME);
3942
} else {
4043
originalImageBucket = new s3.Bucket(this, 's3-sample-original-image-bucket', {
4144
removalPolicy: RemovalPolicy.DESTROY,
@@ -55,120 +58,21 @@ export class ImageOptimizationStack extends Stack {
5558
value: originalImageBucket.bucketName
5659
});
5760

58-
// Create Lambda function for image processing
59-
const imageProcessing = new lambda.Function(this, 'image-optimization', {
60-
code: lambda.Code.fromAsset('functions/image-processing'),
61-
environment: {
62-
maxImageSize: String(MAX_IMAGE_SIZE),
63-
originalImageBucketName: originalImageBucket.bucketName,
64-
transformedImageCacheTTL: S3_TRANSFORMED_IMAGE_CACHE_TTL,
65-
},
66-
handler: 'index.handler',
67-
// let downloads of original images from S3
68-
initialPolicy: [
69-
new iam.PolicyStatement({
70-
actions: ['s3:GetObject'],
71-
resources: [`arn:aws:s3:::${originalImageBucket.bucketName}/*`]
72-
}),
73-
],
74-
logRetention: logs.RetentionDays.ONE_DAY,
75-
memorySize: LAMBDA_MEMORY,
76-
runtime: lambda.Runtime.NODEJS_20_X,
77-
timeout: Duration.seconds(LAMBDA_TIMEOUT_SECONDS),
78-
});
79-
80-
// Enable Lambda URL and create Amazon CloudFront origin
81-
const imageProcessingURL = imageProcessing.addFunctionUrl();
82-
const imageProcessingDomainName = Fn.parseDomainName(imageProcessingURL.url);
83-
const originProps = { originShieldRegion: CLOUDFRONT_ORIGIN_SHIELD_REGION };
84-
const imageProcessingLambdaOrigin = new origins.HttpOrigin(imageProcessingDomainName, originProps);
85-
86-
// Create custom response headers policy with CORS requests allowed for all origins
87-
const getCorsResponsePolicy = () => new cloudfront.ResponseHeadersPolicy(this, 'cors-response-policy', {
88-
responseHeadersPolicyName: `CorsResponsePolicy${this.node.addr}`,
89-
corsBehavior: {
90-
accessControlAllowCredentials: false,
91-
accessControlAllowHeaders: ['*'],
92-
accessControlAllowMethods: ['GET'],
93-
accessControlAllowOrigins: ['*'],
94-
accessControlMaxAge: Duration.seconds(600),
95-
originOverride: false,
96-
},
97-
// Recognize image requests that were processed by this solution
98-
customHeadersBehavior: {
99-
customHeaders: [
100-
{ header: 'x-aws-image-optimization', value: 'v1.0', override: true },
101-
{ header: 'vary', value: 'accept', override: true },
102-
]
103-
}
61+
// Create Amazon CloudFront distribution to deliver optimized images
62+
const imageOptimization = imageOptimizationSolution(this, {
63+
corsEnabled: CORS_ENABLED,
64+
lambdaMemory: LAMBDA_MEMORY,
65+
lambdaTimeout: Duration.seconds(LAMBDA_TIMEOUT_SECONDS),
66+
maxImageSizeBytes: MAX_IMAGE_SIZE,
67+
originalImageBucket: originalImageBucket,
68+
originShieldRegion: ORIGIN_SHIELD_REGION,
69+
storeTransformedImages: STORE_TRANSFORMED_IMAGES,
70+
transformedImageCacheControl: S3_TRANSFORMED_IMAGE_CACHE_CONTROL,
71+
transformedImageExpiration: Duration.days(S3_TRANSFORMED_IMAGE_EXPIRATION_DAYS),
10472
});
105-
106-
// Create an S3 origin with fallback to Lambda
107-
const getS3OriginWithFallbackToLambda = () => {
108-
const transformedImageBucket = new s3.Bucket(this, 's3-transformed-image-bucket', {
109-
autoDeleteObjects: true,
110-
lifecycleRules: [{ expiration: Duration.days(S3_TRANSFORMED_IMAGE_EXPIRATION_DAYS) }],
111-
removalPolicy: RemovalPolicy.DESTROY,
112-
});
113-
imageProcessing.addEnvironment('transformedImageBucketName', transformedImageBucket.bucketName);
114-
imageProcessing.role!.addToPrincipalPolicy(
115-
new iam.PolicyStatement({
116-
actions: ['s3:PutObject'],
117-
resources: [`arn:aws:s3:::${transformedImageBucket.bucketName}/*`]
118-
})
119-
);
120-
return new origins.OriginGroup({
121-
primaryOrigin: origins.S3BucketOrigin.withOriginAccessIdentity(transformedImageBucket, originProps),
122-
fallbackOrigin: imageProcessingLambdaOrigin,
123-
fallbackStatusCodes: [403, 500, 503, 504],
124-
});
125-
};
126-
127-
// Create content delivery distribution with Amazon CloudFront for optimized images
128-
const imageDelivery = new cloudfront.Distribution(this, 'image-delivery-distribution', {
129-
comment: 'Image Optimization - image delivery',
130-
defaultBehavior: {
131-
cachePolicy: new cloudfront.CachePolicy(this, `ImageCachePolicy${this.node.addr}`, {
132-
defaultTtl: Duration.hours(24),
133-
maxTtl: Duration.days(365),
134-
minTtl: Duration.seconds(0),
135-
queryStringBehavior: cloudfront.CacheQueryStringBehavior.all()
136-
}),
137-
compress: false,
138-
functionAssociations: [{
139-
eventType: cloudfront.FunctionEventType.VIEWER_REQUEST,
140-
function: new cloudfront.Function(this, 'urlRewrite', {
141-
code: cloudfront.FunctionCode.fromFile({ filePath: 'functions/url-rewrite/index.js' }),
142-
functionName: `urlRewriteFunction${this.node.addr}`,
143-
})
144-
}],
145-
origin: STORE_TRANSFORMED_IMAGES ? getS3OriginWithFallbackToLambda() : imageProcessingLambdaOrigin,
146-
responseHeadersPolicy: CLOUDFRONT_CORS_ENABLED ? getCorsResponsePolicy() : undefined,
147-
viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
148-
}
149-
});
150-
151-
// Add OAC between CloudFront and LambdaURL
152-
const oac = new cloudfront.CfnOriginAccessControl(this, 'origin-access-control', {
153-
originAccessControlConfig: {
154-
name: `oac${this.node.addr}`,
155-
originAccessControlOriginType: 'lambda',
156-
signingBehavior: 'always',
157-
signingProtocol: 'sigv4',
158-
}
159-
});
160-
161-
const cfnImageDelivery = imageDelivery.node.defaultChild as cloudfront.CfnDistribution;
162-
cfnImageDelivery.addPropertyOverride(`DistributionConfig.Origins.${STORE_TRANSFORMED_IMAGES ? '1' : '0'}.OriginAccessControlId`, oac.getAtt('Id'));
163-
imageProcessing.addPermission('AllowCloudFrontServicePrincipal', {
164-
principal: new iam.ServicePrincipal("cloudfront.amazonaws.com"),
165-
action: 'lambda:InvokeFunctionUrl',
166-
sourceArn: `arn:aws:cloudfront::${this.account}:distribution/${imageDelivery.distributionId}`
167-
});
168-
16973
new CfnOutput(this, 'image-delivery-domain', {
170-
description: 'Domain name of image delivery',
171-
value: imageDelivery.distributionDomainName
74+
description: 'Image delivery domain',
75+
value: imageOptimization.distributionDomainName
17276
});
17377
}
17478
}

0 commit comments

Comments
 (0)