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' ;
44import { 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' ;
68import { getOriginShieldRegion } from './origin-shield' ;
7- import { deploySampleWebsite } from './sample-website' ;
89
910export 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