From 66d5115613f7253d34782ee54da2906a09743176 Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Fri, 4 Oct 2024 14:44:34 +0100 Subject: [PATCH 01/28] Remove unused import --- lib/image-optimization-stack.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index e33228e..8fe6ca8 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -5,7 +5,6 @@ import { Fn, Stack, StackProps, RemovalPolicy, aws_s3 as s3, aws_s3_deployment a import { CfnDistribution } from "aws-cdk-lib/aws-cloudfront"; import { Construct } from 'constructs'; import { getOriginShieldRegion } from './origin-shield'; -import { createHash } from 'crypto'; // Stack Parameters From b9d2061c2f7acef18f368fa8d24cf005d5c78a5a Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Fri, 4 Oct 2024 14:52:42 +0100 Subject: [PATCH 02/28] Reorder context parameters --- lib/image-optimization-stack.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index 8fe6ca8..ea365a6 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -48,16 +48,16 @@ export class ImageOptimizationStack extends Stack { super(scope, id, props); // Change stack parameters based on provided context - STORE_TRANSFORMED_IMAGES = this.node.tryGetContext('STORE_TRANSFORMED_IMAGES') || STORE_TRANSFORMED_IMAGES; - S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION = this.node.tryGetContext('S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION') || S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION; - S3_TRANSFORMED_IMAGE_CACHE_TTL = this.node.tryGetContext('S3_TRANSFORMED_IMAGE_CACHE_TTL') || S3_TRANSFORMED_IMAGE_CACHE_TTL; - S3_IMAGE_BUCKET_NAME = this.node.tryGetContext('S3_IMAGE_BUCKET_NAME') || S3_IMAGE_BUCKET_NAME; CLOUDFRONT_ORIGIN_SHIELD_REGION = this.node.tryGetContext('CLOUDFRONT_ORIGIN_SHIELD_REGION') || CLOUDFRONT_ORIGIN_SHIELD_REGION; CLOUDFRONT_CORS_ENABLED = this.node.tryGetContext('CLOUDFRONT_CORS_ENABLED') || CLOUDFRONT_CORS_ENABLED; + DEPLOY_SAMPLE_WEBSITE = this.node.tryGetContext('DEPLOY_SAMPLE_WEBSITE') || DEPLOY_SAMPLE_WEBSITE; LAMBDA_MEMORY = this.node.tryGetContext('LAMBDA_MEMORY') || LAMBDA_MEMORY; LAMBDA_TIMEOUT = this.node.tryGetContext('LAMBDA_TIMEOUT') || LAMBDA_TIMEOUT; MAX_IMAGE_SIZE = this.node.tryGetContext('MAX_IMAGE_SIZE') || MAX_IMAGE_SIZE; - DEPLOY_SAMPLE_WEBSITE = this.node.tryGetContext('DEPLOY_SAMPLE_WEBSITE') || DEPLOY_SAMPLE_WEBSITE; + S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION = this.node.tryGetContext('S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION') || S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION; + S3_TRANSFORMED_IMAGE_CACHE_TTL = this.node.tryGetContext('S3_TRANSFORMED_IMAGE_CACHE_TTL') || S3_TRANSFORMED_IMAGE_CACHE_TTL; + S3_IMAGE_BUCKET_NAME = this.node.tryGetContext('S3_IMAGE_BUCKET_NAME') || S3_IMAGE_BUCKET_NAME; + STORE_TRANSFORMED_IMAGES = this.node.tryGetContext('STORE_TRANSFORMED_IMAGES') || STORE_TRANSFORMED_IMAGES; // deploy a sample website for testing if required From 47ae2fb2a145b16aa659e21befa4f096a8d57474 Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Fri, 4 Oct 2024 14:55:54 +0100 Subject: [PATCH 03/28] Lint indents and whitespaces --- lib/image-optimization-stack.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index ea365a6..b0fd3c4 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -58,7 +58,7 @@ export class ImageOptimizationStack extends Stack { S3_TRANSFORMED_IMAGE_CACHE_TTL = this.node.tryGetContext('S3_TRANSFORMED_IMAGE_CACHE_TTL') || S3_TRANSFORMED_IMAGE_CACHE_TTL; S3_IMAGE_BUCKET_NAME = this.node.tryGetContext('S3_IMAGE_BUCKET_NAME') || S3_IMAGE_BUCKET_NAME; STORE_TRANSFORMED_IMAGES = this.node.tryGetContext('STORE_TRANSFORMED_IMAGES') || STORE_TRANSFORMED_IMAGES; - + // deploy a sample website for testing if required if (DEPLOY_SAMPLE_WEBSITE === 'true') { @@ -92,7 +92,7 @@ export class ImageOptimizationStack extends Stack { // For the bucket having original images, either use an external one, or create one with some samples photos. var originalImageBucket; var transformedImageBucket; - + if (S3_IMAGE_BUCKET_NAME) { originalImageBucket = s3.Bucket.fromBucketName(this, 'imported-original-image-bucket', S3_IMAGE_BUCKET_NAME); new CfnOutput(this, 'OriginalImagesS3Bucket', { @@ -251,7 +251,7 @@ export class ImageOptimizationStack extends Stack { // ADD OAC between CloudFront and LambdaURL const oac = new cloudfront.CfnOriginAccessControl(this, "OAC", { originAccessControlConfig: { - name: `oac${this.node.addr}`, + name: `oac${this.node.addr}`, originAccessControlOriginType: "lambda", signingBehavior: "always", signingProtocol: "sigv4", @@ -259,7 +259,7 @@ export class ImageOptimizationStack extends Stack { }); const cfnImageDelivery = imageDelivery.node.defaultChild as CfnDistribution; - cfnImageDelivery.addPropertyOverride(`DistributionConfig.Origins.${(STORE_TRANSFORMED_IMAGES === 'true')?"1":"0"}.OriginAccessControlId`, oac.getAtt("Id")); + cfnImageDelivery.addPropertyOverride(`DistributionConfig.Origins.${(STORE_TRANSFORMED_IMAGES === 'true') ? "1" : "0"}.OriginAccessControlId`, oac.getAtt("Id")); imageProcessing.addPermission("AllowCloudFrontServicePrincipal", { principal: new iam.ServicePrincipal("cloudfront.amazonaws.com"), From c2aa1d1cc72eccc2eb42703889040ebe3164c9d5 Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Fri, 4 Oct 2024 14:57:13 +0100 Subject: [PATCH 04/28] Lint code comments --- lib/image-optimization-stack.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index b0fd3c4..e2c8a2c 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -47,7 +47,7 @@ export class ImageOptimizationStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); - // Change stack parameters based on provided context + // Update stack parameters based on provided context CLOUDFRONT_ORIGIN_SHIELD_REGION = this.node.tryGetContext('CLOUDFRONT_ORIGIN_SHIELD_REGION') || CLOUDFRONT_ORIGIN_SHIELD_REGION; CLOUDFRONT_CORS_ENABLED = this.node.tryGetContext('CLOUDFRONT_CORS_ENABLED') || CLOUDFRONT_CORS_ENABLED; DEPLOY_SAMPLE_WEBSITE = this.node.tryGetContext('DEPLOY_SAMPLE_WEBSITE') || DEPLOY_SAMPLE_WEBSITE; @@ -60,7 +60,7 @@ export class ImageOptimizationStack extends Stack { STORE_TRANSFORMED_IMAGES = this.node.tryGetContext('STORE_TRANSFORMED_IMAGES') || STORE_TRANSFORMED_IMAGES; - // deploy a sample website for testing if required + // Deploy a sample website for testing if required if (DEPLOY_SAMPLE_WEBSITE === 'true') { var sampleWebsiteBucket = new s3.Bucket(this, 's3-sample-website-bucket', { removalPolicy: RemovalPolicy.DESTROY, @@ -118,7 +118,7 @@ export class ImageOptimizationStack extends Stack { }); } - // create bucket for transformed images if enabled in the architecture + // Create bucket for transformed images if enabled in the architecture if (STORE_TRANSFORMED_IMAGES === 'true') { transformedImageBucket = new s3.Bucket(this, 's3-transformed-image-bucket', { removalPolicy: RemovalPolicy.DESTROY, @@ -131,7 +131,7 @@ export class ImageOptimizationStack extends Stack { }); } - // prepare env variable for Lambda + // Prepare env variable for Lambda var lambdaEnv: LambdaEnv = { originalImageBucketName: originalImageBucket.bucketName, transformedImageCacheTTL: S3_TRANSFORMED_IMAGE_CACHE_TTL, @@ -145,7 +145,7 @@ export class ImageOptimizationStack extends Stack { resources: ['arn:aws:s3:::' + originalImageBucket.bucketName + '/*'], }); - // statements of the IAM policy to attach to Lambda + // Statements of the IAM policy to attach to Lambda var iamPolicyStatements = [s3ReadOriginalImagesPolicy]; // Create Lambda for image processing @@ -180,7 +180,7 @@ export class ImageOptimizationStack extends Stack { fallbackStatusCodes: [403, 500, 503, 504], }); - // write policy for Lambda on the s3 bucket for transformed images + // Write policy for Lambda on the s3 bucket for transformed images var s3WriteTransformedImagesPolicy = new iam.PolicyStatement({ actions: ['s3:PutObject'], resources: ['arn:aws:s3:::' + transformedImageBucket.bucketName + '/*'], @@ -192,7 +192,7 @@ export class ImageOptimizationStack extends Stack { }); } - // attach iam policy to the role assumed by Lambda + // Attach iam policy to the role assumed by Lambda imageProcessing.role?.attachInlinePolicy( new iam.Policy(this, 'read-write-bucket-policy', { statements: iamPolicyStatements, @@ -222,7 +222,7 @@ export class ImageOptimizationStack extends Stack { } if (CLOUDFRONT_CORS_ENABLED === 'true') { - // Creating a custom response headers policy. CORS allowed for all origins. + // Create a custom response headers policy. CORS allowed for all origins. const imageResponseHeadersPolicy = new cloudfront.ResponseHeadersPolicy(this, `ResponseHeadersPolicy${this.node.addr}`, { responseHeadersPolicyName: `ImageResponsePolicy${this.node.addr}`, corsBehavior: { @@ -233,7 +233,7 @@ export class ImageOptimizationStack extends Stack { accessControlMaxAge: Duration.seconds(600), originOverride: false, }, - // recognizing image requests that were processed by this solution + // Recognize image requests that were processed by this solution customHeadersBehavior: { customHeaders: [ { header: 'x-aws-image-optimization', value: 'v1.0', override: true }, @@ -248,7 +248,7 @@ export class ImageOptimizationStack extends Stack { defaultBehavior: imageDeliveryCacheBehaviorConfig }); - // ADD OAC between CloudFront and LambdaURL + // Add OAC between CloudFront and LambdaURL const oac = new cloudfront.CfnOriginAccessControl(this, "OAC", { originAccessControlConfig: { name: `oac${this.node.addr}`, From 44bf155e8a770ee3549cbf7c3112910f35f9683c Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Fri, 4 Oct 2024 15:17:26 +0100 Subject: [PATCH 05/28] Bump deps --- lib/image-optimization-stack.ts | 2 +- package.json | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index e2c8a2c..4ecbe62 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -74,7 +74,7 @@ export class ImageOptimizationStack extends Stack { comment: 'image optimization - sample website', defaultRootObject: 'index.html', defaultBehavior: { - origin: new origins.S3Origin(sampleWebsiteBucket), + origin: origins.S3BucketOrigin.withOriginAccessIdentity(sampleWebsiteBucket), viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, } }); diff --git a/package.json b/package.json index 257fa82..7b0b36b 100644 --- a/package.json +++ b/package.json @@ -12,17 +12,17 @@ "cdk": "cdk" }, "devDependencies": { - "@types/jest": "^29.5.11", + "@types/jest": "^29.5.13", "@types/node": "20.11.2", "@types/prettier": "2.7.3", - "aws-cdk": "2.121.1", + "aws-cdk": "2.161.0", "jest": "^29.7.0", - "ts-jest": "^29.1.1", + "ts-jest": "^29.2.5", "ts-node": "^10.9.2", - "typescript": "~5.3.3" + "typescript": "~5.6.2" }, "dependencies": { - "aws-cdk-lib": "2.121.1", + "aws-cdk-lib": "2.161.0", "constructs": "^10.3.0", "source-map-support": "^0.5.21" } From 18fbe5c564ed43369137cc869ef2618f3f8b63e8 Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Fri, 4 Oct 2024 15:21:38 +0100 Subject: [PATCH 06/28] Create Lambda simpler --- lib/image-optimization-stack.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index 4ecbe62..b7e02ff 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -149,16 +149,15 @@ export class ImageOptimizationStack extends Stack { var iamPolicyStatements = [s3ReadOriginalImagesPolicy]; // Create Lambda for image processing - var lambdaProps = { - runtime: lambda.Runtime.NODEJS_20_X, - handler: 'index.handler', + const imageProcessing = new lambda.Function(this, 'image-optimization', { code: lambda.Code.fromAsset('functions/image-processing'), - timeout: Duration.seconds(parseInt(LAMBDA_TIMEOUT)), - memorySize: parseInt(LAMBDA_MEMORY), environment: lambdaEnv, + handler: 'index.handler', logRetention: logs.RetentionDays.ONE_DAY, - }; - var imageProcessing = new lambda.Function(this, 'image-optimization', lambdaProps); + memorySize: parseInt(LAMBDA_MEMORY), + runtime: lambda.Runtime.NODEJS_20_X, + timeout: Duration.seconds(parseInt(LAMBDA_TIMEOUT)), + }); // Enable Lambda URL const imageProcessingURL = imageProcessing.addFunctionUrl(); From 31cc61c89a8786f036e2d2e1ae3ef530b0e198e2 Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Fri, 4 Oct 2024 15:21:52 +0100 Subject: [PATCH 07/28] Wrap lifecycle rules --- lib/image-optimization-stack.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index b7e02ff..fec5b33 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -123,11 +123,7 @@ export class ImageOptimizationStack extends Stack { transformedImageBucket = new s3.Bucket(this, 's3-transformed-image-bucket', { removalPolicy: RemovalPolicy.DESTROY, autoDeleteObjects: true, - lifecycleRules: [ - { - expiration: Duration.days(parseInt(S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION)), - }, - ], + lifecycleRules: [{ expiration: Duration.days(parseInt(S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION)) }], }); } From b805fa134e675977fa98af8a6339ce0fc867f6e6 Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Fri, 4 Oct 2024 15:25:52 +0100 Subject: [PATCH 08/28] Update deprecated construct --- lib/image-optimization-stack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index fec5b33..2f6981e 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -166,7 +166,7 @@ export class ImageOptimizationStack extends Stack { if (transformedImageBucket) { imageOrigin = new origins.OriginGroup({ - primaryOrigin: new origins.S3Origin(transformedImageBucket, { + primaryOrigin: origins.S3BucketOrigin.withOriginAccessIdentity(transformedImageBucket, { originShieldRegion: CLOUDFRONT_ORIGIN_SHIELD_REGION, }), fallbackOrigin: new origins.HttpOrigin(imageProcessingDomainName, { From c1c64bc3e3ed8f28b05d67bbd6e07928a130459f Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Fri, 4 Oct 2024 15:36:38 +0100 Subject: [PATCH 09/28] Simplify origin shield usage --- lib/image-optimization-stack.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index 2f6981e..3c11d19 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -59,6 +59,7 @@ export class ImageOptimizationStack extends Stack { S3_IMAGE_BUCKET_NAME = this.node.tryGetContext('S3_IMAGE_BUCKET_NAME') || S3_IMAGE_BUCKET_NAME; STORE_TRANSFORMED_IMAGES = this.node.tryGetContext('STORE_TRANSFORMED_IMAGES') || STORE_TRANSFORMED_IMAGES; + const originShieldOriginProps = { originShieldRegion: CLOUDFRONT_ORIGIN_SHIELD_REGION }; // Deploy a sample website for testing if required if (DEPLOY_SAMPLE_WEBSITE === 'true') { @@ -166,12 +167,8 @@ export class ImageOptimizationStack extends Stack { if (transformedImageBucket) { imageOrigin = new origins.OriginGroup({ - primaryOrigin: origins.S3BucketOrigin.withOriginAccessIdentity(transformedImageBucket, { - originShieldRegion: CLOUDFRONT_ORIGIN_SHIELD_REGION, - }), - fallbackOrigin: new origins.HttpOrigin(imageProcessingDomainName, { - originShieldRegion: CLOUDFRONT_ORIGIN_SHIELD_REGION, - }), + primaryOrigin: origins.S3BucketOrigin.withOriginAccessIdentity(transformedImageBucket, originShieldOriginProps), + fallbackOrigin: new origins.HttpOrigin(imageProcessingDomainName, originShieldOriginProps), fallbackStatusCodes: [403, 500, 503, 504], }); @@ -182,9 +179,7 @@ export class ImageOptimizationStack extends Stack { }); iamPolicyStatements.push(s3WriteTransformedImagesPolicy); } else { - imageOrigin = new origins.HttpOrigin(imageProcessingDomainName, { - originShieldRegion: CLOUDFRONT_ORIGIN_SHIELD_REGION, - }); + imageOrigin = new origins.HttpOrigin(imageProcessingDomainName, originShieldOriginProps); } // Attach iam policy to the role assumed by Lambda From 269eaea374371b4aa002ab5ecb140bae58c7cd5d Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Fri, 4 Oct 2024 15:49:08 +0100 Subject: [PATCH 10/28] Refactor imageProcessingLambdaPolicyStatements --- lib/image-optimization-stack.ts | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index 3c11d19..811b7c3 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -136,14 +136,13 @@ export class ImageOptimizationStack extends Stack { }; if (transformedImageBucket) lambdaEnv.transformedImageBucketName = transformedImageBucket.bucketName; - // IAM policy to read from the S3 bucket containing the original images - const s3ReadOriginalImagesPolicy = new iam.PolicyStatement({ - actions: ['s3:GetObject'], - resources: ['arn:aws:s3:::' + originalImageBucket.bucketName + '/*'], - }); - // Statements of the IAM policy to attach to Lambda - var iamPolicyStatements = [s3ReadOriginalImagesPolicy]; + const imageProcessingLambdaPolicyStatements = [ + new iam.PolicyStatement({ + actions: ['s3:GetObject'], + resources: ['arn:aws:s3:::' + originalImageBucket.bucketName + '/*'] + }) + ]; // Create Lambda for image processing const imageProcessing = new lambda.Function(this, 'image-optimization', { @@ -172,12 +171,13 @@ export class ImageOptimizationStack extends Stack { fallbackStatusCodes: [403, 500, 503, 504], }); - // Write policy for Lambda on the s3 bucket for transformed images - var s3WriteTransformedImagesPolicy = new iam.PolicyStatement({ - actions: ['s3:PutObject'], - resources: ['arn:aws:s3:::' + transformedImageBucket.bucketName + '/*'], - }); - iamPolicyStatements.push(s3WriteTransformedImagesPolicy); + // Allow Lambda to push transformed images to S3 + imageProcessingLambdaPolicyStatements.push( + new iam.PolicyStatement({ + actions: ['s3:PutObject'], + resources: ['arn:aws:s3:::' + transformedImageBucket.bucketName + '/*'], + }) + ); } else { imageOrigin = new origins.HttpOrigin(imageProcessingDomainName, originShieldOriginProps); } @@ -185,7 +185,7 @@ export class ImageOptimizationStack extends Stack { // Attach iam policy to the role assumed by Lambda imageProcessing.role?.attachInlinePolicy( new iam.Policy(this, 'read-write-bucket-policy', { - statements: iamPolicyStatements, + statements: imageProcessingLambdaPolicyStatements, }), ); From 0d774776433dcec2a432f588f3bb3b6d0b5d59f7 Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Fri, 4 Oct 2024 16:11:18 +0100 Subject: [PATCH 11/28] Fix spelling --- lib/image-optimization-stack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index 811b7c3..cef0c12 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -128,7 +128,7 @@ export class ImageOptimizationStack extends Stack { }); } - // Prepare env variable for Lambda + // Prepare env variables for Lambda var lambdaEnv: LambdaEnv = { originalImageBucketName: originalImageBucket.bucketName, transformedImageCacheTTL: S3_TRANSFORMED_IMAGE_CACHE_TTL, From 7d8ad95a11b4b313213fba008b063bf43ead0fe1 Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Fri, 4 Oct 2024 16:48:29 +0100 Subject: [PATCH 12/28] Fix import style --- lib/image-optimization-stack.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index cef0c12..5056f08 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -2,7 +2,6 @@ // SPDX-License-Identifier: MIT-0 import { Fn, Stack, StackProps, RemovalPolicy, aws_s3 as s3, aws_s3_deployment as s3deploy, aws_cloudfront as cloudfront, aws_cloudfront_origins as origins, aws_lambda as lambda, aws_iam as iam, Duration, CfnOutput, aws_logs as logs } from 'aws-cdk-lib'; -import { CfnDistribution } from "aws-cdk-lib/aws-cloudfront"; import { Construct } from 'constructs'; import { getOriginShieldRegion } from './origin-shield'; @@ -248,7 +247,7 @@ export class ImageOptimizationStack extends Stack { }, }); - const cfnImageDelivery = imageDelivery.node.defaultChild as CfnDistribution; + const cfnImageDelivery = imageDelivery.node.defaultChild as cloudfront.CfnDistribution; cfnImageDelivery.addPropertyOverride(`DistributionConfig.Origins.${(STORE_TRANSFORMED_IMAGES === 'true') ? "1" : "0"}.OriginAccessControlId`, oac.getAtt("Id")); imageProcessing.addPermission("AllowCloudFrontServicePrincipal", { From 65adebab09916ec7f25fb8de3bdf9d4fc539043c Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Fri, 4 Oct 2024 17:38:34 +0100 Subject: [PATCH 13/28] Fix types --- lib/image-optimization-stack.ts | 95 ++++++++++++++------------------- lib/types.ts | 12 +++++ 2 files changed, 51 insertions(+), 56 deletions(-) create mode 100644 lib/types.ts diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index 5056f08..373fe4e 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -4,6 +4,7 @@ import { Fn, Stack, StackProps, RemovalPolicy, aws_s3 as s3, aws_s3_deployment as s3deploy, aws_cloudfront as cloudfront, aws_cloudfront_origins as origins, aws_lambda as lambda, aws_iam as iam, Duration, CfnOutput, aws_logs as logs } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { getOriginShieldRegion } from './origin-shield'; +import { ImageProcessingLambdaEnvironment } from './types'; // Stack Parameters @@ -26,22 +27,6 @@ var LAMBDA_TIMEOUT = '60'; // Whether to deploy a sample website referenced in https://aws.amazon.com/blogs/networking-and-content-delivery/image-optimization-using-amazon-cloudfront-and-aws-lambda/ var DEPLOY_SAMPLE_WEBSITE = 'false'; -type ImageDeliveryCacheBehaviorConfig = { - origin: any; - compress: any; - viewerProtocolPolicy: any; - cachePolicy: any; - functionAssociations: any; - responseHeadersPolicy?: any; -}; - -type LambdaEnv = { - originalImageBucketName: string, - transformedImageBucketName?: any; - transformedImageCacheTTL: string, - maxImageSize: string, -} - export class ImageOptimizationStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); @@ -128,11 +113,11 @@ export class ImageOptimizationStack extends Stack { } // Prepare env variables for Lambda - var lambdaEnv: LambdaEnv = { + const lambdaEnv = { originalImageBucketName: originalImageBucket.bucketName, transformedImageCacheTTL: S3_TRANSFORMED_IMAGE_CACHE_TTL, maxImageSize: MAX_IMAGE_SIZE, - }; + } as ImageProcessingLambdaEnvironment; if (transformedImageBucket) lambdaEnv.transformedImageBucketName = transformedImageBucket.bucketName; // Statements of the IAM policy to attach to Lambda @@ -194,47 +179,45 @@ export class ImageOptimizationStack extends Stack { functionName: `urlRewriteFunction${this.node.addr}`, }); - var imageDeliveryCacheBehaviorConfig: ImageDeliveryCacheBehaviorConfig = { - origin: imageOrigin, - viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, - compress: false, - cachePolicy: new cloudfront.CachePolicy(this, `ImageCachePolicy${this.node.addr}`, { - defaultTtl: Duration.hours(24), - maxTtl: Duration.days(365), - minTtl: Duration.seconds(0), - queryStringBehavior: cloudfront.CacheQueryStringBehavior.all() - }), - functionAssociations: [{ - eventType: cloudfront.FunctionEventType.VIEWER_REQUEST, - function: urlRewriteFunction, - }], - } + // Create custom response headers policy if necessary - CORS allowed for all origins + const getCorsResponsePolicy = () => new cloudfront.ResponseHeadersPolicy(this, 'CorsResponsePolicy', { + responseHeadersPolicyName: `CorsResponsePolicy${this.node.addr}`, + corsBehavior: { + accessControlAllowCredentials: false, + accessControlAllowHeaders: ['*'], + accessControlAllowMethods: ['GET'], + accessControlAllowOrigins: ['*'], + accessControlMaxAge: Duration.seconds(600), + originOverride: false, + }, + // Recognize image requests that were processed by this solution + customHeadersBehavior: { + customHeaders: [ + { header: 'x-aws-image-optimization', value: 'v1.0', override: true }, + { header: 'vary', value: 'accept', override: true }, + ], + } + }); - if (CLOUDFRONT_CORS_ENABLED === 'true') { - // Create a custom response headers policy. CORS allowed for all origins. - const imageResponseHeadersPolicy = new cloudfront.ResponseHeadersPolicy(this, `ResponseHeadersPolicy${this.node.addr}`, { - responseHeadersPolicyName: `ImageResponsePolicy${this.node.addr}`, - corsBehavior: { - accessControlAllowCredentials: false, - accessControlAllowHeaders: ['*'], - accessControlAllowMethods: ['GET'], - accessControlAllowOrigins: ['*'], - accessControlMaxAge: Duration.seconds(600), - originOverride: false, - }, - // Recognize image requests that were processed by this solution - customHeadersBehavior: { - customHeaders: [ - { header: 'x-aws-image-optimization', value: 'v1.0', override: true }, - { header: 'vary', value: 'accept', override: true }, - ], - } - }); - imageDeliveryCacheBehaviorConfig.responseHeadersPolicy = imageResponseHeadersPolicy; - } const imageDelivery = new cloudfront.Distribution(this, 'imageDeliveryDistribution', { comment: 'image optimization - image delivery', - defaultBehavior: imageDeliveryCacheBehaviorConfig + defaultBehavior: { + origin: imageOrigin, + viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + compress: false, + cachePolicy: new cloudfront.CachePolicy(this, `ImageCachePolicy${this.node.addr}`, { + defaultTtl: Duration.hours(24), + maxTtl: Duration.days(365), + minTtl: Duration.seconds(0), + queryStringBehavior: cloudfront.CacheQueryStringBehavior.all() + }), + functionAssociations: [{ + eventType: cloudfront.FunctionEventType.VIEWER_REQUEST, + function: urlRewriteFunction, + }], + // Set CORS headers if CORS enabled + responseHeadersPolicy: CLOUDFRONT_CORS_ENABLED === 'true' ? getCorsResponsePolicy() : undefined + } }); // Add OAC between CloudFront and LambdaURL diff --git a/lib/types.ts b/lib/types.ts new file mode 100644 index 0000000..ee2db0d --- /dev/null +++ b/lib/types.ts @@ -0,0 +1,12 @@ +export type ImageProcessingLambdaEnvironment = ImageProcessingLambdaEnvironmentBase & OptionalTransformedImageBucketName; + +type ImageProcessingLambdaEnvironmentBase = { + maxImageSize: string, + originalImageBucketName: string, + transformedImageCacheTTL: string, +}; + +type OptionalTransformedImageBucketName = Partial<{ + transformedImageBucketName: string +}>; + From 3da2340ceb2b8f560483be3e2bdc89f77aee367e Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Fri, 4 Oct 2024 17:46:21 +0100 Subject: [PATCH 14/28] Remove comma --- lib/image-optimization-stack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index 373fe4e..05e4eac 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -175,7 +175,7 @@ export class ImageOptimizationStack extends Stack { // Create a CloudFront Function for url rewrites const urlRewriteFunction = new cloudfront.Function(this, 'urlRewrite', { - code: cloudfront.FunctionCode.fromFile({ filePath: 'functions/url-rewrite/index.js', }), + code: cloudfront.FunctionCode.fromFile({ filePath: 'functions/url-rewrite/index.js' }), functionName: `urlRewriteFunction${this.node.addr}`, }); From 94935b8b39bc450f073c823824b06937a05fb832 Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Fri, 4 Oct 2024 17:51:00 +0100 Subject: [PATCH 15/28] Use string templates --- lib/image-optimization-stack.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index 05e4eac..773bc9c 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -124,7 +124,7 @@ export class ImageOptimizationStack extends Stack { const imageProcessingLambdaPolicyStatements = [ new iam.PolicyStatement({ actions: ['s3:GetObject'], - resources: ['arn:aws:s3:::' + originalImageBucket.bucketName + '/*'] + resources: [`arn:aws:s3:::${originalImageBucket.bucketName}/*`] }) ]; @@ -159,7 +159,7 @@ export class ImageOptimizationStack extends Stack { imageProcessingLambdaPolicyStatements.push( new iam.PolicyStatement({ actions: ['s3:PutObject'], - resources: ['arn:aws:s3:::' + transformedImageBucket.bucketName + '/*'], + resources: [`arn:aws:s3:::${transformedImageBucket.bucketName}/*`], }) ); } else { From 78345d2814f542018450d856510a39d5cd13581e Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Fri, 4 Oct 2024 17:52:58 +0100 Subject: [PATCH 16/28] Reuse lambda origin --- lib/image-optimization-stack.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index 773bc9c..b8bb965 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -139,11 +139,10 @@ export class ImageOptimizationStack extends Stack { timeout: Duration.seconds(parseInt(LAMBDA_TIMEOUT)), }); - // Enable Lambda URL + // Enable Lambda URL, get its the domain name and create CloudFront origin const imageProcessingURL = imageProcessing.addFunctionUrl(); - - // Leverage CDK Intrinsics to get the hostname of the Lambda URL const imageProcessingDomainName = Fn.parseDomainName(imageProcessingURL.url); + const imageProcessingLambdaOrigin = new origins.HttpOrigin(imageProcessingDomainName, originShieldOriginProps); // Create a CloudFront origin: S3 with fallback to Lambda when image needs to be transformed, otherwise with Lambda as sole origin var imageOrigin; @@ -151,7 +150,7 @@ export class ImageOptimizationStack extends Stack { if (transformedImageBucket) { imageOrigin = new origins.OriginGroup({ primaryOrigin: origins.S3BucketOrigin.withOriginAccessIdentity(transformedImageBucket, originShieldOriginProps), - fallbackOrigin: new origins.HttpOrigin(imageProcessingDomainName, originShieldOriginProps), + fallbackOrigin: imageProcessingLambdaOrigin, fallbackStatusCodes: [403, 500, 503, 504], }); @@ -163,7 +162,7 @@ export class ImageOptimizationStack extends Stack { }) ); } else { - imageOrigin = new origins.HttpOrigin(imageProcessingDomainName, originShieldOriginProps); + imageOrigin = imageProcessingLambdaOrigin; } // Attach iam policy to the role assumed by Lambda From cf45547ba5104cfb4b347213b2ed68bd6adee957 Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Sat, 5 Oct 2024 12:12:05 +0100 Subject: [PATCH 17/28] Reorder line alphabetically --- lib/image-optimization-stack.ts | 41 +++------------------------------ 1 file changed, 3 insertions(+), 38 deletions(-) diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index b8bb965..9513b8d 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -38,52 +38,17 @@ export class ImageOptimizationStack extends Stack { LAMBDA_MEMORY = this.node.tryGetContext('LAMBDA_MEMORY') || LAMBDA_MEMORY; LAMBDA_TIMEOUT = this.node.tryGetContext('LAMBDA_TIMEOUT') || LAMBDA_TIMEOUT; MAX_IMAGE_SIZE = this.node.tryGetContext('MAX_IMAGE_SIZE') || MAX_IMAGE_SIZE; - S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION = this.node.tryGetContext('S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION') || S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION; S3_TRANSFORMED_IMAGE_CACHE_TTL = this.node.tryGetContext('S3_TRANSFORMED_IMAGE_CACHE_TTL') || S3_TRANSFORMED_IMAGE_CACHE_TTL; + S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION = this.node.tryGetContext('S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION') || S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION; S3_IMAGE_BUCKET_NAME = this.node.tryGetContext('S3_IMAGE_BUCKET_NAME') || S3_IMAGE_BUCKET_NAME; STORE_TRANSFORMED_IMAGES = this.node.tryGetContext('STORE_TRANSFORMED_IMAGES') || STORE_TRANSFORMED_IMAGES; const originShieldOriginProps = { originShieldRegion: CLOUDFRONT_ORIGIN_SHIELD_REGION }; - // Deploy a sample website for testing if required - if (DEPLOY_SAMPLE_WEBSITE === 'true') { - var sampleWebsiteBucket = new s3.Bucket(this, 's3-sample-website-bucket', { - removalPolicy: RemovalPolicy.DESTROY, - blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, - encryption: s3.BucketEncryption.S3_MANAGED, - enforceSSL: true, - autoDeleteObjects: true, - }); - - var sampleWebsiteDelivery = new cloudfront.Distribution(this, 'websiteDeliveryDistribution', { - comment: 'image optimization - sample website', - defaultRootObject: 'index.html', - defaultBehavior: { - origin: origins.S3BucketOrigin.withOriginAccessIdentity(sampleWebsiteBucket), - viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, - } - }); - - new CfnOutput(this, 'SampleWebsiteDomain', { - description: 'Sample website domain', - value: sampleWebsiteDelivery.distributionDomainName - }); - new CfnOutput(this, 'SampleWebsiteS3Bucket', { - description: 'S3 bucket use by the sample website', - value: sampleWebsiteBucket.bucketName - }); - } - - // For the bucket having original images, either use an external one, or create one with some samples photos. - var originalImageBucket; - var transformedImageBucket; - + // For the bucket having original images, wse existing bucket if provided, otherwise create a new one with sample images + let originalImageBucket; if (S3_IMAGE_BUCKET_NAME) { originalImageBucket = s3.Bucket.fromBucketName(this, 'imported-original-image-bucket', S3_IMAGE_BUCKET_NAME); - new CfnOutput(this, 'OriginalImagesS3Bucket', { - description: 'S3 bucket where original images are stored', - value: originalImageBucket.bucketName - }); } else { originalImageBucket = new s3.Bucket(this, 's3-sample-original-image-bucket', { removalPolicy: RemovalPolicy.DESTROY, From d23c54826755356b843f82083020823dfd783c32 Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Sat, 5 Oct 2024 12:12:18 +0100 Subject: [PATCH 18/28] Reorder imports alphabetically --- lib/image-optimization-stack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index 9513b8d..7e9fe94 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -1,7 +1,7 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT-0 -import { Fn, Stack, StackProps, RemovalPolicy, aws_s3 as s3, aws_s3_deployment as s3deploy, aws_cloudfront as cloudfront, aws_cloudfront_origins as origins, aws_lambda as lambda, aws_iam as iam, Duration, CfnOutput, aws_logs as logs } from 'aws-cdk-lib'; +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'; import { Construct } from 'constructs'; import { getOriginShieldRegion } from './origin-shield'; import { ImageProcessingLambdaEnvironment } from './types'; From 6ad9aba04ce395b2cf1c4bb7e133b43ecb53b6e1 Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Sat, 5 Oct 2024 18:32:33 +0100 Subject: [PATCH 19/28] Finish code updates --- README.md | 1 - lib/cdk-context-utils.ts | 11 ++ lib/image-optimization-stack.ts | 197 +++++++++++++------------------- lib/sample-website.ts | 32 ++++++ lib/types.ts | 12 -- 5 files changed, 123 insertions(+), 130 deletions(-) create mode 100644 lib/cdk-context-utils.ts create mode 100644 lib/sample-website.ts delete mode 100644 lib/types.ts diff --git a/README.md b/README.md index 469563b..2ceeaa9 100644 --- a/README.md +++ b/README.md @@ -56,4 +56,3 @@ cdk destroy ## License This library is licensed under the MIT-0 License. See the LICENSE file. - diff --git a/lib/cdk-context-utils.ts b/lib/cdk-context-utils.ts new file mode 100644 index 0000000..c8b47bb --- /dev/null +++ b/lib/cdk-context-utils.ts @@ -0,0 +1,11 @@ +import { Stack } from "aws-cdk-lib"; + +export const getContextVariables = (stack: Stack) => { + const { node } = stack; + return { + boolean: (key: string, defaultValue = false) => node.tryGetContext(key) ? Boolean(node.tryGetContext(key)) : defaultValue, + number: (key: string, defaultValue: number) => node.tryGetContext(key) ? Number(node.tryGetContext(key)) : defaultValue, + string: (key: string, defaultValue: string) => node.tryGetContext(key) ? String(node.tryGetContext(key)) : defaultValue, + stringOrUndefined: (key: string) => node.tryGetContext(key) ? String(node.tryGetContext(key)) : undefined, + } +} \ No newline at end of file diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index 7e9fe94..ae8d329 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -1,52 +1,39 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT-0 - 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'; import { Construct } from 'constructs'; +import { getContextVariables } from './cdk-context-utils'; import { getOriginShieldRegion } from './origin-shield'; -import { ImageProcessingLambdaEnvironment } from './types'; - -// Stack Parameters - -// related to architecture. If set to false, transformed images are not stored in S3, and all image requests land on Lambda -var STORE_TRANSFORMED_IMAGES = 'true'; -// Parameters of S3 bucket where original images are stored -var S3_IMAGE_BUCKET_NAME: string; -// CloudFront parameters -var CLOUDFRONT_ORIGIN_SHIELD_REGION = getOriginShieldRegion(process.env.AWS_REGION || process.env.CDK_DEFAULT_REGION || 'us-east-1'); -var CLOUDFRONT_CORS_ENABLED = 'true'; -// Parameters of transformed images -var S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION = '90'; -var S3_TRANSFORMED_IMAGE_CACHE_TTL = 'max-age=31622400'; -// Max image size in bytes. If generated images are stored on S3, bigger images are generated, stored on S3 -// and request is redirect to the generated image. Otherwise, an application error is sent. -var MAX_IMAGE_SIZE = '4700000'; -// Lambda Parameters -var LAMBDA_MEMORY = '1500'; -var LAMBDA_TIMEOUT = '60'; -// Whether to deploy a sample website referenced in https://aws.amazon.com/blogs/networking-and-content-delivery/image-optimization-using-amazon-cloudfront-and-aws-lambda/ -var DEPLOY_SAMPLE_WEBSITE = 'false'; +import { deploySampleWebsite } from './sample-website'; export class ImageOptimizationStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); - // Update stack parameters based on provided context - CLOUDFRONT_ORIGIN_SHIELD_REGION = this.node.tryGetContext('CLOUDFRONT_ORIGIN_SHIELD_REGION') || CLOUDFRONT_ORIGIN_SHIELD_REGION; - CLOUDFRONT_CORS_ENABLED = this.node.tryGetContext('CLOUDFRONT_CORS_ENABLED') || CLOUDFRONT_CORS_ENABLED; - DEPLOY_SAMPLE_WEBSITE = this.node.tryGetContext('DEPLOY_SAMPLE_WEBSITE') || DEPLOY_SAMPLE_WEBSITE; - LAMBDA_MEMORY = this.node.tryGetContext('LAMBDA_MEMORY') || LAMBDA_MEMORY; - LAMBDA_TIMEOUT = this.node.tryGetContext('LAMBDA_TIMEOUT') || LAMBDA_TIMEOUT; - MAX_IMAGE_SIZE = this.node.tryGetContext('MAX_IMAGE_SIZE') || MAX_IMAGE_SIZE; - S3_TRANSFORMED_IMAGE_CACHE_TTL = this.node.tryGetContext('S3_TRANSFORMED_IMAGE_CACHE_TTL') || S3_TRANSFORMED_IMAGE_CACHE_TTL; - S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION = this.node.tryGetContext('S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION') || S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION; - S3_IMAGE_BUCKET_NAME = this.node.tryGetContext('S3_IMAGE_BUCKET_NAME') || S3_IMAGE_BUCKET_NAME; - STORE_TRANSFORMED_IMAGES = this.node.tryGetContext('STORE_TRANSFORMED_IMAGES') || STORE_TRANSFORMED_IMAGES; + // Load stack parameters related to architecture from CDK context + const getContext = getContextVariables(this); + const CLOUDFRONT_CORS_ENABLED = getContext.boolean('CLOUDFRONT_CORS_ENABLED', true); + const CLOUDFRONT_ORIGIN_SHIELD_REGION = getContext.string('CLOUDFRONT_ORIGIN_SHIELD_REGION', getOriginShieldRegion(process.env.AWS_REGION || process.env.CDK_DEFAULT_REGION || 'us-east-1')); + + const LAMBDA_MEMORY = getContext.number('LAMBDA_MEMORY', 1500); + const LAMBDA_TIMEOUT_SECONDS = getContext.number('LAMBDA_TIMEOUT', 60); + const MAX_IMAGE_SIZE = getContext.number('MAX_IMAGE_SIZE', 4700000); + + const S3_IMAGE_BUCKET_NAME = getContext.stringOrUndefined('S3_IMAGE_BUCKET_NAME'); + const S3_TRANSFORMED_IMAGE_CACHE_TTL = getContext.string('S3_TRANSFORMED_IMAGE_CACHE_TTL', 'max-age=31622400'); + const S3_TRANSFORMED_IMAGE_EXPIRATION_DAYS = getContext.number('S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION', 90); + const STORE_TRANSFORMED_IMAGES = getContext.boolean('STORE_TRANSFORMED_IMAGES', true); + + // If DEPLOY_SAMPLE_WEBSITE is true, this stack will deploy an additional, sample website to see th + if (getContext.boolean('DEPLOY_SAMPLE_WEBSITE')) { + // 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/ + deploySampleWebsite(this); + } - const originShieldOriginProps = { originShieldRegion: CLOUDFRONT_ORIGIN_SHIELD_REGION }; + // *********************** Image Optimization Stack *********************** - // For the bucket having original images, wse existing bucket if provided, otherwise create a new one with sample images - let originalImageBucket; + // For original images, use existing S3 bucket if provided, otherwise create a new one with sample images + let originalImageBucket: s3.IBucket; if (S3_IMAGE_BUCKET_NAME) { originalImageBucket = s3.Bucket.fromBucketName(this, 'imported-original-image-bucket', S3_IMAGE_BUCKET_NAME); } else { @@ -62,88 +49,41 @@ export class ImageOptimizationStack extends Stack { destinationBucket: originalImageBucket, destinationKeyPrefix: 'images/rio/', }); - new CfnOutput(this, 'OriginalImagesS3Bucket', { - description: 'S3 bucket where original images are stored', - value: originalImageBucket.bucketName - }); - } - - // Create bucket for transformed images if enabled in the architecture - if (STORE_TRANSFORMED_IMAGES === 'true') { - transformedImageBucket = new s3.Bucket(this, 's3-transformed-image-bucket', { - removalPolicy: RemovalPolicy.DESTROY, - autoDeleteObjects: true, - lifecycleRules: [{ expiration: Duration.days(parseInt(S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION)) }], - }); - } - - // Prepare env variables for Lambda - const lambdaEnv = { - originalImageBucketName: originalImageBucket.bucketName, - transformedImageCacheTTL: S3_TRANSFORMED_IMAGE_CACHE_TTL, - maxImageSize: MAX_IMAGE_SIZE, - } as ImageProcessingLambdaEnvironment; - if (transformedImageBucket) lambdaEnv.transformedImageBucketName = transformedImageBucket.bucketName; - - // Statements of the IAM policy to attach to Lambda - const imageProcessingLambdaPolicyStatements = [ - new iam.PolicyStatement({ - actions: ['s3:GetObject'], - resources: [`arn:aws:s3:::${originalImageBucket.bucketName}/*`] - }) - ]; + }; + new CfnOutput(this, 'OriginalImagesS3Bucket', { + description: 'S3 bucket storing original images', + value: originalImageBucket.bucketName + }); - // Create Lambda for image processing + // Create Lambda function for image processing const imageProcessing = new lambda.Function(this, 'image-optimization', { code: lambda.Code.fromAsset('functions/image-processing'), - environment: lambdaEnv, + environment: { + maxImageSize: String(MAX_IMAGE_SIZE), + originalImageBucketName: originalImageBucket.bucketName, + transformedImageCacheTTL: S3_TRANSFORMED_IMAGE_CACHE_TTL, + }, handler: 'index.handler', + // let downloads of original images from S3 + initialPolicy: [ + new iam.PolicyStatement({ + actions: ['s3:GetObject'], + resources: [`arn:aws:s3:::${originalImageBucket.bucketName}/*`] + }), + ], logRetention: logs.RetentionDays.ONE_DAY, - memorySize: parseInt(LAMBDA_MEMORY), + memorySize: LAMBDA_MEMORY, runtime: lambda.Runtime.NODEJS_20_X, - timeout: Duration.seconds(parseInt(LAMBDA_TIMEOUT)), + timeout: Duration.seconds(LAMBDA_TIMEOUT_SECONDS), }); - // Enable Lambda URL, get its the domain name and create CloudFront origin + // Enable Lambda URL and create Amazon CloudFront origin const imageProcessingURL = imageProcessing.addFunctionUrl(); const imageProcessingDomainName = Fn.parseDomainName(imageProcessingURL.url); - const imageProcessingLambdaOrigin = new origins.HttpOrigin(imageProcessingDomainName, originShieldOriginProps); + const originProps = { originShieldRegion: CLOUDFRONT_ORIGIN_SHIELD_REGION }; + const imageProcessingLambdaOrigin = new origins.HttpOrigin(imageProcessingDomainName, originProps); - // Create a CloudFront origin: S3 with fallback to Lambda when image needs to be transformed, otherwise with Lambda as sole origin - var imageOrigin; - - if (transformedImageBucket) { - imageOrigin = new origins.OriginGroup({ - primaryOrigin: origins.S3BucketOrigin.withOriginAccessIdentity(transformedImageBucket, originShieldOriginProps), - fallbackOrigin: imageProcessingLambdaOrigin, - fallbackStatusCodes: [403, 500, 503, 504], - }); - - // Allow Lambda to push transformed images to S3 - imageProcessingLambdaPolicyStatements.push( - new iam.PolicyStatement({ - actions: ['s3:PutObject'], - resources: [`arn:aws:s3:::${transformedImageBucket.bucketName}/*`], - }) - ); - } else { - imageOrigin = imageProcessingLambdaOrigin; - } - - // Attach iam policy to the role assumed by Lambda - imageProcessing.role?.attachInlinePolicy( - new iam.Policy(this, 'read-write-bucket-policy', { - statements: imageProcessingLambdaPolicyStatements, - }), - ); - - // Create a CloudFront Function for url rewrites - const urlRewriteFunction = new cloudfront.Function(this, 'urlRewrite', { - code: cloudfront.FunctionCode.fromFile({ filePath: 'functions/url-rewrite/index.js' }), - functionName: `urlRewriteFunction${this.node.addr}`, - }); - - // Create custom response headers policy if necessary - CORS allowed for all origins + // Create custom response headers policy with CORS requests allowed for all origins const getCorsResponsePolicy = () => new cloudfront.ResponseHeadersPolicy(this, 'CorsResponsePolicy', { responseHeadersPolicyName: `CorsResponsePolicy${this.node.addr}`, corsBehavior: { @@ -163,24 +103,48 @@ export class ImageOptimizationStack extends Stack { } }); + // Create an S3 origin with fallback to Lambda + const getS3OriginWithFallbackToLambda = () => { + const transformedImageBucket = new s3.Bucket(this, 's3-transformed-image-bucket', { + autoDeleteObjects: true, + lifecycleRules: [{ expiration: Duration.days(S3_TRANSFORMED_IMAGE_EXPIRATION_DAYS) }], + removalPolicy: RemovalPolicy.DESTROY, + }); + imageProcessing.addEnvironment('transformedImageBucketName', transformedImageBucket.bucketName); + imageProcessing.role!.addToPrincipalPolicy( + new iam.PolicyStatement({ + actions: ['s3:PutObject'], + resources: [`arn:aws:s3:::${transformedImageBucket.bucketName}/*`] + }) + ); + return new origins.OriginGroup({ + primaryOrigin: origins.S3BucketOrigin.withOriginAccessIdentity(transformedImageBucket, originProps), + fallbackOrigin: imageProcessingLambdaOrigin, + fallbackStatusCodes: [403, 500, 503, 504], + }); + }; + + // Create content delivery distribution with Amazon CloudFront for optimized images const imageDelivery = new cloudfront.Distribution(this, 'imageDeliveryDistribution', { - comment: 'image optimization - image delivery', + comment: 'Image Optimization - image delivery', defaultBehavior: { - origin: imageOrigin, - viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, - compress: false, cachePolicy: new cloudfront.CachePolicy(this, `ImageCachePolicy${this.node.addr}`, { defaultTtl: Duration.hours(24), maxTtl: Duration.days(365), minTtl: Duration.seconds(0), queryStringBehavior: cloudfront.CacheQueryStringBehavior.all() }), + compress: false, functionAssociations: [{ eventType: cloudfront.FunctionEventType.VIEWER_REQUEST, - function: urlRewriteFunction, + function: new cloudfront.Function(this, 'urlRewrite', { + code: cloudfront.FunctionCode.fromFile({ filePath: 'functions/url-rewrite/index.js' }), + functionName: `urlRewriteFunction${this.node.addr}`, + }), }], - // Set CORS headers if CORS enabled - responseHeadersPolicy: CLOUDFRONT_CORS_ENABLED === 'true' ? getCorsResponsePolicy() : undefined + origin: STORE_TRANSFORMED_IMAGES ? getS3OriginWithFallbackToLambda() : imageProcessingLambdaOrigin, + responseHeadersPolicy: CLOUDFRONT_CORS_ENABLED ? getCorsResponsePolicy() : undefined, + viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, } }); @@ -195,8 +159,7 @@ export class ImageOptimizationStack extends Stack { }); const cfnImageDelivery = imageDelivery.node.defaultChild as cloudfront.CfnDistribution; - cfnImageDelivery.addPropertyOverride(`DistributionConfig.Origins.${(STORE_TRANSFORMED_IMAGES === 'true') ? "1" : "0"}.OriginAccessControlId`, oac.getAtt("Id")); - + cfnImageDelivery.addPropertyOverride(`DistributionConfig.Origins.${STORE_TRANSFORMED_IMAGES ? "1" : "0"}.OriginAccessControlId`, oac.getAtt("Id")); imageProcessing.addPermission("AllowCloudFrontServicePrincipal", { principal: new iam.ServicePrincipal("cloudfront.amazonaws.com"), action: "lambda:InvokeFunctionUrl", diff --git a/lib/sample-website.ts b/lib/sample-website.ts new file mode 100644 index 0000000..8559fa3 --- /dev/null +++ b/lib/sample-website.ts @@ -0,0 +1,32 @@ +import { BlockPublicAccess, Bucket, BucketEncryption } from "aws-cdk-lib/aws-s3"; +import { ImageOptimizationStack } from "./image-optimization-stack"; +import { CfnOutput, RemovalPolicy } from "aws-cdk-lib"; +import { Distribution, ViewerProtocolPolicy } from "aws-cdk-lib/aws-cloudfront"; +import { S3BucketOrigin } from "aws-cdk-lib/aws-cloudfront-origins"; + +export const deploySampleWebsite = (stack: ImageOptimizationStack) => { + const sampleWebsiteBucket = new Bucket(stack, 's3-sample-website-bucket', { + removalPolicy: RemovalPolicy.DESTROY, + blockPublicAccess: BlockPublicAccess.BLOCK_ALL, + encryption: BucketEncryption.S3_MANAGED, + enforceSSL: true, + autoDeleteObjects: true, + }); + new CfnOutput(stack, 'SampleWebsiteS3Bucket', { + description: 'S3 bucket use by the sample website', + value: sampleWebsiteBucket.bucketName + }); + + const sampleWebsiteDelivery = new Distribution(stack, 'websiteDeliveryDistribution', { + comment: 'image optimization - sample website', + defaultRootObject: 'index.html', + defaultBehavior: { + origin: S3BucketOrigin.withOriginAccessIdentity(sampleWebsiteBucket), + viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + } + }); + new CfnOutput(stack, 'SampleWebsiteDomain', { + description: 'Sample website domain', + value: sampleWebsiteDelivery.distributionDomainName + }); +} diff --git a/lib/types.ts b/lib/types.ts deleted file mode 100644 index ee2db0d..0000000 --- a/lib/types.ts +++ /dev/null @@ -1,12 +0,0 @@ -export type ImageProcessingLambdaEnvironment = ImageProcessingLambdaEnvironmentBase & OptionalTransformedImageBucketName; - -type ImageProcessingLambdaEnvironmentBase = { - maxImageSize: string, - originalImageBucketName: string, - transformedImageCacheTTL: string, -}; - -type OptionalTransformedImageBucketName = Partial<{ - transformedImageBucketName: string -}>; - From a5753cc15cbfad0a9cf7c0e44a3ac7603b946dbf Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Sat, 5 Oct 2024 18:33:02 +0100 Subject: [PATCH 20/28] Bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7b0b36b..a949365 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "image-optimization", - "version": "0.1.0", + "version": "0.2.0", "bin": { "image-optimization": "bin/image-optimization.js" }, From b551921bec0e2cc4a9be62ac56b7c942bc519137 Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Sat, 5 Oct 2024 18:39:58 +0100 Subject: [PATCH 21/28] Standardize quote chars in stack --- lib/image-optimization-stack.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index ae8d329..22e4846 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -30,7 +30,7 @@ export class ImageOptimizationStack extends Stack { deploySampleWebsite(this); } - // *********************** Image Optimization Stack *********************** + // ****************** Image Optimization Stack Resources ****************** // For original images, use existing S3 bucket if provided, otherwise create a new one with sample images let originalImageBucket: s3.IBucket; @@ -44,13 +44,13 @@ export class ImageOptimizationStack extends Stack { enforceSSL: true, autoDeleteObjects: true, }); - new s3deploy.BucketDeployment(this, 'DeployWebsite', { + new s3deploy.BucketDeployment(this, 'deploy-website', { sources: [s3deploy.Source.asset('./image-sample')], destinationBucket: originalImageBucket, destinationKeyPrefix: 'images/rio/', }); }; - new CfnOutput(this, 'OriginalImagesS3Bucket', { + new CfnOutput(this, 'original-images-s3-bucket', { description: 'S3 bucket storing original images', value: originalImageBucket.bucketName }); @@ -84,7 +84,7 @@ export class ImageOptimizationStack extends Stack { const imageProcessingLambdaOrigin = new origins.HttpOrigin(imageProcessingDomainName, originProps); // Create custom response headers policy with CORS requests allowed for all origins - const getCorsResponsePolicy = () => new cloudfront.ResponseHeadersPolicy(this, 'CorsResponsePolicy', { + const getCorsResponsePolicy = () => new cloudfront.ResponseHeadersPolicy(this, 'cors-response-policy', { responseHeadersPolicyName: `CorsResponsePolicy${this.node.addr}`, corsBehavior: { accessControlAllowCredentials: false, @@ -125,7 +125,7 @@ export class ImageOptimizationStack extends Stack { }; // Create content delivery distribution with Amazon CloudFront for optimized images - const imageDelivery = new cloudfront.Distribution(this, 'imageDeliveryDistribution', { + const imageDelivery = new cloudfront.Distribution(this, 'image-delivery-distribution', { comment: 'Image Optimization - image delivery', defaultBehavior: { cachePolicy: new cloudfront.CachePolicy(this, `ImageCachePolicy${this.node.addr}`, { @@ -149,24 +149,24 @@ export class ImageOptimizationStack extends Stack { }); // Add OAC between CloudFront and LambdaURL - const oac = new cloudfront.CfnOriginAccessControl(this, "OAC", { + const oac = new cloudfront.CfnOriginAccessControl(this, 'origin-access-control', { originAccessControlConfig: { name: `oac${this.node.addr}`, - originAccessControlOriginType: "lambda", - signingBehavior: "always", - signingProtocol: "sigv4", + originAccessControlOriginType: 'lambda', + signingBehavior: 'always', + signingProtocol: 'sigv4', }, }); const cfnImageDelivery = imageDelivery.node.defaultChild as cloudfront.CfnDistribution; - cfnImageDelivery.addPropertyOverride(`DistributionConfig.Origins.${STORE_TRANSFORMED_IMAGES ? "1" : "0"}.OriginAccessControlId`, oac.getAtt("Id")); - imageProcessing.addPermission("AllowCloudFrontServicePrincipal", { + cfnImageDelivery.addPropertyOverride(`DistributionConfig.Origins.${STORE_TRANSFORMED_IMAGES ? '1' : '0'}.OriginAccessControlId`, oac.getAtt('Id')); + imageProcessing.addPermission('AllowCloudFrontServicePrincipal', { principal: new iam.ServicePrincipal("cloudfront.amazonaws.com"), - action: "lambda:InvokeFunctionUrl", + action: 'lambda:InvokeFunctionUrl', sourceArn: `arn:aws:cloudfront::${this.account}:distribution/${imageDelivery.distributionId}` }) - new CfnOutput(this, 'ImageDeliveryDomain', { + new CfnOutput(this, 'image-delivery-domain', { description: 'Domain name of image delivery', value: imageDelivery.distributionDomainName }); From 5b1dc99d1078f8e05f788a7eccba32fc90c3ae18 Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Sat, 5 Oct 2024 18:43:27 +0100 Subject: [PATCH 22/28] Standardize commas & comments --- lib/image-optimization-stack.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index 22e4846..a761f24 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -24,13 +24,13 @@ export class ImageOptimizationStack extends Stack { const S3_TRANSFORMED_IMAGE_EXPIRATION_DAYS = getContext.number('S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION', 90); const STORE_TRANSFORMED_IMAGES = getContext.boolean('STORE_TRANSFORMED_IMAGES', true); - // If DEPLOY_SAMPLE_WEBSITE is true, this stack will deploy an additional, sample website to see th + // If DEPLOY_SAMPLE_WEBSITE is true, this stack will deploy an additional, sample website to showcase the solution + // 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/ if (getContext.boolean('DEPLOY_SAMPLE_WEBSITE')) { - // 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/ deploySampleWebsite(this); } - // ****************** Image Optimization Stack Resources ****************** + // ********************* Image Optimization Resources ********************* // For original images, use existing S3 bucket if provided, otherwise create a new one with sample images let originalImageBucket: s3.IBucket; @@ -99,7 +99,7 @@ export class ImageOptimizationStack extends Stack { customHeaders: [ { header: 'x-aws-image-optimization', value: 'v1.0', override: true }, { header: 'vary', value: 'accept', override: true }, - ], + ] } }); @@ -140,7 +140,7 @@ export class ImageOptimizationStack extends Stack { function: new cloudfront.Function(this, 'urlRewrite', { code: cloudfront.FunctionCode.fromFile({ filePath: 'functions/url-rewrite/index.js' }), functionName: `urlRewriteFunction${this.node.addr}`, - }), + }) }], origin: STORE_TRANSFORMED_IMAGES ? getS3OriginWithFallbackToLambda() : imageProcessingLambdaOrigin, responseHeadersPolicy: CLOUDFRONT_CORS_ENABLED ? getCorsResponsePolicy() : undefined, @@ -155,7 +155,7 @@ export class ImageOptimizationStack extends Stack { originAccessControlOriginType: 'lambda', signingBehavior: 'always', signingProtocol: 'sigv4', - }, + } }); const cfnImageDelivery = imageDelivery.node.defaultChild as cloudfront.CfnDistribution; @@ -164,7 +164,7 @@ export class ImageOptimizationStack extends Stack { principal: new iam.ServicePrincipal("cloudfront.amazonaws.com"), action: 'lambda:InvokeFunctionUrl', sourceArn: `arn:aws:cloudfront::${this.account}:distribution/${imageDelivery.distributionId}` - }) + }); new CfnOutput(this, 'image-delivery-domain', { description: 'Domain name of image delivery', From 11779c0cb75ec1d1393d984bad4732c6afc594e2 Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Sat, 5 Oct 2024 20:00:18 +0100 Subject: [PATCH 23/28] Restructure files, extract solution into a separate file --- lib/cdk-context-utils.ts | 19 ++- lib/image-optimization-sample-website.ts | 24 ++++ lib/image-optimization-solution.ts | 149 ++++++++++++++++++++ lib/image-optimization-stack.ts | 170 +++++------------------ lib/sample-website.ts | 32 ----- 5 files changed, 219 insertions(+), 175 deletions(-) create mode 100644 lib/image-optimization-sample-website.ts create mode 100644 lib/image-optimization-solution.ts delete mode 100644 lib/sample-website.ts diff --git a/lib/cdk-context-utils.ts b/lib/cdk-context-utils.ts index c8b47bb..e1e0d58 100644 --- a/lib/cdk-context-utils.ts +++ b/lib/cdk-context-utils.ts @@ -1,11 +1,10 @@ -import { Stack } from "aws-cdk-lib"; +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +import { Node } from "constructs"; -export const getContextVariables = (stack: Stack) => { - const { node } = stack; - return { - boolean: (key: string, defaultValue = false) => node.tryGetContext(key) ? Boolean(node.tryGetContext(key)) : defaultValue, - number: (key: string, defaultValue: number) => node.tryGetContext(key) ? Number(node.tryGetContext(key)) : defaultValue, - string: (key: string, defaultValue: string) => node.tryGetContext(key) ? String(node.tryGetContext(key)) : defaultValue, - stringOrUndefined: (key: string) => node.tryGetContext(key) ? String(node.tryGetContext(key)) : undefined, - } -} \ No newline at end of file +export const readContext = (node: Node) => ({ + boolean: (key: string, defaultValue = false) => node.tryGetContext(key) ? Boolean(node.tryGetContext(key)) : defaultValue, + number: (key: string, defaultValue: number) => node.tryGetContext(key) ? Number(node.tryGetContext(key)) : defaultValue, + string: (key: string, defaultValue: string) => node.tryGetContext(key) ? String(node.tryGetContext(key)) : defaultValue, + stringOrUndefined: (key: string) => node.tryGetContext(key) ? String(node.tryGetContext(key)) : undefined, +}); diff --git a/lib/image-optimization-sample-website.ts b/lib/image-optimization-sample-website.ts new file mode 100644 index 0000000..7e78092 --- /dev/null +++ b/lib/image-optimization-sample-website.ts @@ -0,0 +1,24 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +import { RemovalPolicy, Stack } from 'aws-cdk-lib'; +import { BlockPublicAccess, Bucket, BucketEncryption } from 'aws-cdk-lib/aws-s3'; +import { Distribution, ViewerProtocolPolicy } from 'aws-cdk-lib/aws-cloudfront'; +import { S3BucketOrigin } from 'aws-cdk-lib/aws-cloudfront-origins'; + +export const sampleWebsite = (stack: Stack) => { + const bucket = new Bucket(stack, 's3-sample-website-bucket', { + autoDeleteObjects: true, + blockPublicAccess: BlockPublicAccess.BLOCK_ALL, + encryption: BucketEncryption.S3_MANAGED, + enforceSSL: true, + removalPolicy: RemovalPolicy.DESTROY + }); + return new Distribution(stack, 'websiteDeliveryDistribution', { + comment: 'Image Optimization - sample website', + defaultBehavior: { + origin: S3BucketOrigin.withOriginAccessControl(bucket), + viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS + }, + defaultRootObject: 'index.html' + }); +} diff --git a/lib/image-optimization-solution.ts b/lib/image-optimization-solution.ts new file mode 100644 index 0000000..e39798f --- /dev/null +++ b/lib/image-optimization-solution.ts @@ -0,0 +1,149 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 +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, + Duration, Fn, RemovalPolicy, Stack +} from 'aws-cdk-lib'; + +type ImageProcessingProps = { + corsEnabled: boolean, + originalImageBucket: s3.IBucket, + originShieldRegion: string, + lambdaMemory: number, + lambdaTimeout: Duration, + maxImageSizeBytes: number, + transformedImageCacheControl: string, + transformedImageExpiration: Duration, + storeTransformedImages: boolean, +} + +export const imageOptimizationSolution = (stack: Stack, props: ImageProcessingProps) => { + const { + corsEnabled, + originalImageBucket, + originShieldRegion, + lambdaMemory, + lambdaTimeout, + maxImageSizeBytes, + transformedImageCacheControl, + transformedImageExpiration, + storeTransformedImages, + } = props; + + // Create Lambda function for image processing + const imageProcessing = new lambda.Function(stack, 'image-optimization', { + code: lambda.Code.fromAsset('functions/image-processing'), + environment: { + maxImageSize: String(maxImageSizeBytes), + originalImageBucketName: originalImageBucket.bucketName, + transformedImageCacheTTL: transformedImageCacheControl, + }, + handler: 'index.handler', + // let downloads of original images from S3 + initialPolicy: [ + new iam.PolicyStatement({ + actions: ['s3:GetObject'], + resources: [`arn:aws:s3:::${originalImageBucket.bucketName}/*`] + }), + ], + logRetention: logs.RetentionDays.ONE_DAY, + memorySize: lambdaMemory, + runtime: lambda.Runtime.NODEJS_20_X, + timeout: lambdaTimeout, + }); + + // Enable Lambda URL and create Amazon CloudFront origin + const imageProcessingURL = imageProcessing.addFunctionUrl(); + const imageProcessingDomainName = Fn.parseDomainName(imageProcessingURL.url); + const imageProcessingLambdaOrigin = new origins.HttpOrigin(imageProcessingDomainName, { originShieldRegion }); + + // Create custom response headers policy with CORS requests allowed for all origins + const getCorsResponsePolicy = () => new cloudfront.ResponseHeadersPolicy(stack, 'cors-response-policy', { + responseHeadersPolicyName: `CorsResponsePolicy${stack.node.addr}`, + corsBehavior: { + accessControlAllowCredentials: false, + accessControlAllowHeaders: ['*'], + accessControlAllowMethods: ['GET'], + accessControlAllowOrigins: ['*'], + accessControlMaxAge: Duration.seconds(600), + originOverride: false, + }, + // Recognize image requests that were processed by this solution + customHeadersBehavior: { + customHeaders: [ + { header: 'x-aws-image-optimization', value: 'v1.0', override: true }, + { header: 'vary', value: 'accept', override: true }, + ] + } + }); + + // Create an S3 origin with fallback to Lambda + const getS3OriginWithFallbackToLambda = () => { + const transformedImageBucket = new s3.Bucket(stack, 's3-transformed-image-bucket', { + autoDeleteObjects: true, + lifecycleRules: [{ expiration: transformedImageExpiration }], + removalPolicy: RemovalPolicy.DESTROY, + }); + imageProcessing.addEnvironment('transformedImageBucketName', transformedImageBucket.bucketName); + imageProcessing.role!.addToPrincipalPolicy( + new iam.PolicyStatement({ + actions: ['s3:PutObject'], + resources: [`arn:aws:s3:::${transformedImageBucket.bucketName}/*`] + }) + ); + return new origins.OriginGroup({ + primaryOrigin: origins.S3BucketOrigin.withOriginAccessIdentity(transformedImageBucket, { originShieldRegion }), + fallbackOrigin: imageProcessingLambdaOrigin, + fallbackStatusCodes: [403, 500, 503, 504], + }); + }; + + // Create content delivery distribution with Amazon CloudFront for optimized images + const imageDelivery = new cloudfront.Distribution(stack, 'image-delivery-distribution', { + comment: 'Image Optimization - image delivery', + defaultBehavior: { + cachePolicy: new cloudfront.CachePolicy(stack, `ImageCachePolicy${stack.node.addr}`, { + defaultTtl: Duration.hours(24), + maxTtl: Duration.days(365), + minTtl: Duration.seconds(0), + queryStringBehavior: cloudfront.CacheQueryStringBehavior.all() + }), + compress: false, + functionAssociations: [{ + eventType: cloudfront.FunctionEventType.VIEWER_REQUEST, + function: new cloudfront.Function(stack, 'urlRewrite', { + code: cloudfront.FunctionCode.fromFile({ filePath: 'functions/url-rewrite/index.js' }), + functionName: `urlRewriteFunction${stack.node.addr}`, + }) + }], + origin: storeTransformedImages ? getS3OriginWithFallbackToLambda() : imageProcessingLambdaOrigin, + responseHeadersPolicy: corsEnabled ? getCorsResponsePolicy() : undefined, + viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + } + }); + + // Add OAC between CloudFront and LambdaURL + const oac = new cloudfront.CfnOriginAccessControl(stack, 'origin-access-control', { + originAccessControlConfig: { + name: `oac${stack.node.addr}`, + originAccessControlOriginType: 'lambda', + signingBehavior: 'always', + signingProtocol: 'sigv4', + } + }); + + const cfnImageDelivery = imageDelivery.node.defaultChild as cloudfront.CfnDistribution; + cfnImageDelivery.addPropertyOverride(`DistributionConfig.Origins.${storeTransformedImages ? '1' : '0'}.OriginAccessControlId`, oac.getAtt('Id')); + imageProcessing.addPermission('AllowCloudFrontServicePrincipal', { + principal: new iam.ServicePrincipal("cloudfront.amazonaws.com"), + action: 'lambda:InvokeFunctionUrl', + sourceArn: `arn:aws:cloudfront::${stack.account}:distribution/${imageDelivery.distributionId}` + }); + + return imageDelivery; +} diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index a761f24..fe587a4 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -1,41 +1,44 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT-0 -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'; +import { aws_s3 as s3, aws_s3_deployment as s3deploy, Duration, CfnOutput, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'; import { Construct } from 'constructs'; -import { getContextVariables } from './cdk-context-utils'; +import { readContext } from './cdk-context-utils'; +import { sampleWebsite } from './image-optimization-sample-website'; +import { imageOptimizationSolution } from './image-optimization-solution'; import { getOriginShieldRegion } from './origin-shield'; -import { deploySampleWebsite } from './sample-website'; export class ImageOptimizationStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props); // Load stack parameters related to architecture from CDK context - const getContext = getContextVariables(this); - const CLOUDFRONT_CORS_ENABLED = getContext.boolean('CLOUDFRONT_CORS_ENABLED', true); - const CLOUDFRONT_ORIGIN_SHIELD_REGION = getContext.string('CLOUDFRONT_ORIGIN_SHIELD_REGION', getOriginShieldRegion(process.env.AWS_REGION || process.env.CDK_DEFAULT_REGION || 'us-east-1')); + const context = readContext(this.node); - const LAMBDA_MEMORY = getContext.number('LAMBDA_MEMORY', 1500); - const LAMBDA_TIMEOUT_SECONDS = getContext.number('LAMBDA_TIMEOUT', 60); - const MAX_IMAGE_SIZE = getContext.number('MAX_IMAGE_SIZE', 4700000); + const CORS_ENABLED = context.boolean('CLOUDFRONT_CORS_ENABLED', true); + const DEPLOY_SAMPLE_WEBSITE = context.boolean('DEPLOY_SAMPLE_WEBSITE'); + const LAMBDA_MEMORY = context.number('LAMBDA_MEMORY', 1500); + const LAMBDA_TIMEOUT_SECONDS = context.number('LAMBDA_TIMEOUT', 60); + const MAX_IMAGE_SIZE = context.number('MAX_IMAGE_SIZE', 4700000); + const ORIGIN_SHIELD_REGION = context.string('CLOUDFRONT_ORIGIN_SHIELD_REGION', getOriginShieldRegion(process.env.AWS_REGION || process.env.CDK_DEFAULT_REGION || 'us-east-1')); + const S3_ORIGINAL_IMAGE_BUCKET_NAME = context.stringOrUndefined('S3_IMAGE_BUCKET_NAME'); + const S3_TRANSFORMED_IMAGE_CACHE_CONTROL = context.string('S3_TRANSFORMED_IMAGE_CACHE_TTL', 'max-age=31622400'); + const S3_TRANSFORMED_IMAGE_EXPIRATION_DAYS = context.number('S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION', 90); + const STORE_TRANSFORMED_IMAGES = context.boolean('STORE_TRANSFORMED_IMAGES', true); - const S3_IMAGE_BUCKET_NAME = getContext.stringOrUndefined('S3_IMAGE_BUCKET_NAME'); - const S3_TRANSFORMED_IMAGE_CACHE_TTL = getContext.string('S3_TRANSFORMED_IMAGE_CACHE_TTL', 'max-age=31622400'); - const S3_TRANSFORMED_IMAGE_EXPIRATION_DAYS = getContext.number('S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION', 90); - const STORE_TRANSFORMED_IMAGES = getContext.boolean('STORE_TRANSFORMED_IMAGES', true); - - // If DEPLOY_SAMPLE_WEBSITE is true, this stack will deploy an additional, sample website to showcase the solution + // If true, this stack will deploy an additional, sample website to showcase the solution // 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/ - if (getContext.boolean('DEPLOY_SAMPLE_WEBSITE')) { - deploySampleWebsite(this); + if (DEPLOY_SAMPLE_WEBSITE) { + const sampleWebsiteDelivery = sampleWebsite(this); + new CfnOutput(this, 'SampleWebsiteDomain', { + description: 'Sample website domain', + value: sampleWebsiteDelivery.distributionDomainName + }); } - // ********************* Image Optimization Resources ********************* - // For original images, use existing S3 bucket if provided, otherwise create a new one with sample images let originalImageBucket: s3.IBucket; - if (S3_IMAGE_BUCKET_NAME) { - originalImageBucket = s3.Bucket.fromBucketName(this, 'imported-original-image-bucket', S3_IMAGE_BUCKET_NAME); + if (S3_ORIGINAL_IMAGE_BUCKET_NAME) { + originalImageBucket = s3.Bucket.fromBucketName(this, 'imported-original-image-bucket', S3_ORIGINAL_IMAGE_BUCKET_NAME); } else { originalImageBucket = new s3.Bucket(this, 's3-sample-original-image-bucket', { removalPolicy: RemovalPolicy.DESTROY, @@ -55,120 +58,21 @@ export class ImageOptimizationStack extends Stack { value: originalImageBucket.bucketName }); - // Create Lambda function for image processing - const imageProcessing = new lambda.Function(this, 'image-optimization', { - code: lambda.Code.fromAsset('functions/image-processing'), - environment: { - maxImageSize: String(MAX_IMAGE_SIZE), - originalImageBucketName: originalImageBucket.bucketName, - transformedImageCacheTTL: S3_TRANSFORMED_IMAGE_CACHE_TTL, - }, - handler: 'index.handler', - // let downloads of original images from S3 - initialPolicy: [ - new iam.PolicyStatement({ - actions: ['s3:GetObject'], - resources: [`arn:aws:s3:::${originalImageBucket.bucketName}/*`] - }), - ], - logRetention: logs.RetentionDays.ONE_DAY, - memorySize: LAMBDA_MEMORY, - runtime: lambda.Runtime.NODEJS_20_X, - timeout: Duration.seconds(LAMBDA_TIMEOUT_SECONDS), - }); - - // Enable Lambda URL and create Amazon CloudFront origin - const imageProcessingURL = imageProcessing.addFunctionUrl(); - const imageProcessingDomainName = Fn.parseDomainName(imageProcessingURL.url); - const originProps = { originShieldRegion: CLOUDFRONT_ORIGIN_SHIELD_REGION }; - const imageProcessingLambdaOrigin = new origins.HttpOrigin(imageProcessingDomainName, originProps); - - // Create custom response headers policy with CORS requests allowed for all origins - const getCorsResponsePolicy = () => new cloudfront.ResponseHeadersPolicy(this, 'cors-response-policy', { - responseHeadersPolicyName: `CorsResponsePolicy${this.node.addr}`, - corsBehavior: { - accessControlAllowCredentials: false, - accessControlAllowHeaders: ['*'], - accessControlAllowMethods: ['GET'], - accessControlAllowOrigins: ['*'], - accessControlMaxAge: Duration.seconds(600), - originOverride: false, - }, - // Recognize image requests that were processed by this solution - customHeadersBehavior: { - customHeaders: [ - { header: 'x-aws-image-optimization', value: 'v1.0', override: true }, - { header: 'vary', value: 'accept', override: true }, - ] - } + // Create Amazon CloudFront distribution to deliver optimized images + const imageOptimization = imageOptimizationSolution(this, { + corsEnabled: CORS_ENABLED, + lambdaMemory: LAMBDA_MEMORY, + lambdaTimeout: Duration.seconds(LAMBDA_TIMEOUT_SECONDS), + maxImageSizeBytes: MAX_IMAGE_SIZE, + originalImageBucket: originalImageBucket, + originShieldRegion: ORIGIN_SHIELD_REGION, + storeTransformedImages: STORE_TRANSFORMED_IMAGES, + transformedImageCacheControl: S3_TRANSFORMED_IMAGE_CACHE_CONTROL, + transformedImageExpiration: Duration.days(S3_TRANSFORMED_IMAGE_EXPIRATION_DAYS), }); - - // Create an S3 origin with fallback to Lambda - const getS3OriginWithFallbackToLambda = () => { - const transformedImageBucket = new s3.Bucket(this, 's3-transformed-image-bucket', { - autoDeleteObjects: true, - lifecycleRules: [{ expiration: Duration.days(S3_TRANSFORMED_IMAGE_EXPIRATION_DAYS) }], - removalPolicy: RemovalPolicy.DESTROY, - }); - imageProcessing.addEnvironment('transformedImageBucketName', transformedImageBucket.bucketName); - imageProcessing.role!.addToPrincipalPolicy( - new iam.PolicyStatement({ - actions: ['s3:PutObject'], - resources: [`arn:aws:s3:::${transformedImageBucket.bucketName}/*`] - }) - ); - return new origins.OriginGroup({ - primaryOrigin: origins.S3BucketOrigin.withOriginAccessIdentity(transformedImageBucket, originProps), - fallbackOrigin: imageProcessingLambdaOrigin, - fallbackStatusCodes: [403, 500, 503, 504], - }); - }; - - // Create content delivery distribution with Amazon CloudFront for optimized images - const imageDelivery = new cloudfront.Distribution(this, 'image-delivery-distribution', { - comment: 'Image Optimization - image delivery', - defaultBehavior: { - cachePolicy: new cloudfront.CachePolicy(this, `ImageCachePolicy${this.node.addr}`, { - defaultTtl: Duration.hours(24), - maxTtl: Duration.days(365), - minTtl: Duration.seconds(0), - queryStringBehavior: cloudfront.CacheQueryStringBehavior.all() - }), - compress: false, - functionAssociations: [{ - eventType: cloudfront.FunctionEventType.VIEWER_REQUEST, - function: new cloudfront.Function(this, 'urlRewrite', { - code: cloudfront.FunctionCode.fromFile({ filePath: 'functions/url-rewrite/index.js' }), - functionName: `urlRewriteFunction${this.node.addr}`, - }) - }], - origin: STORE_TRANSFORMED_IMAGES ? getS3OriginWithFallbackToLambda() : imageProcessingLambdaOrigin, - responseHeadersPolicy: CLOUDFRONT_CORS_ENABLED ? getCorsResponsePolicy() : undefined, - viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, - } - }); - - // Add OAC between CloudFront and LambdaURL - const oac = new cloudfront.CfnOriginAccessControl(this, 'origin-access-control', { - originAccessControlConfig: { - name: `oac${this.node.addr}`, - originAccessControlOriginType: 'lambda', - signingBehavior: 'always', - signingProtocol: 'sigv4', - } - }); - - const cfnImageDelivery = imageDelivery.node.defaultChild as cloudfront.CfnDistribution; - cfnImageDelivery.addPropertyOverride(`DistributionConfig.Origins.${STORE_TRANSFORMED_IMAGES ? '1' : '0'}.OriginAccessControlId`, oac.getAtt('Id')); - imageProcessing.addPermission('AllowCloudFrontServicePrincipal', { - principal: new iam.ServicePrincipal("cloudfront.amazonaws.com"), - action: 'lambda:InvokeFunctionUrl', - sourceArn: `arn:aws:cloudfront::${this.account}:distribution/${imageDelivery.distributionId}` - }); - new CfnOutput(this, 'image-delivery-domain', { - description: 'Domain name of image delivery', - value: imageDelivery.distributionDomainName + description: 'Image delivery domain', + value: imageOptimization.distributionDomainName }); } } diff --git a/lib/sample-website.ts b/lib/sample-website.ts deleted file mode 100644 index 8559fa3..0000000 --- a/lib/sample-website.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { BlockPublicAccess, Bucket, BucketEncryption } from "aws-cdk-lib/aws-s3"; -import { ImageOptimizationStack } from "./image-optimization-stack"; -import { CfnOutput, RemovalPolicy } from "aws-cdk-lib"; -import { Distribution, ViewerProtocolPolicy } from "aws-cdk-lib/aws-cloudfront"; -import { S3BucketOrigin } from "aws-cdk-lib/aws-cloudfront-origins"; - -export const deploySampleWebsite = (stack: ImageOptimizationStack) => { - const sampleWebsiteBucket = new Bucket(stack, 's3-sample-website-bucket', { - removalPolicy: RemovalPolicy.DESTROY, - blockPublicAccess: BlockPublicAccess.BLOCK_ALL, - encryption: BucketEncryption.S3_MANAGED, - enforceSSL: true, - autoDeleteObjects: true, - }); - new CfnOutput(stack, 'SampleWebsiteS3Bucket', { - description: 'S3 bucket use by the sample website', - value: sampleWebsiteBucket.bucketName - }); - - const sampleWebsiteDelivery = new Distribution(stack, 'websiteDeliveryDistribution', { - comment: 'image optimization - sample website', - defaultRootObject: 'index.html', - defaultBehavior: { - origin: S3BucketOrigin.withOriginAccessIdentity(sampleWebsiteBucket), - viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, - } - }); - new CfnOutput(stack, 'SampleWebsiteDomain', { - description: 'Sample website domain', - value: sampleWebsiteDelivery.distributionDomainName - }); -} From dccd251932c83ab4cc72b01d5d40efeef48055dc Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Sat, 5 Oct 2024 20:30:10 +0100 Subject: [PATCH 24/28] Standardize resource id --- lib/image-optimization-sample-website.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/image-optimization-sample-website.ts b/lib/image-optimization-sample-website.ts index 7e78092..3ad891c 100644 --- a/lib/image-optimization-sample-website.ts +++ b/lib/image-optimization-sample-website.ts @@ -13,7 +13,7 @@ export const sampleWebsite = (stack: Stack) => { enforceSSL: true, removalPolicy: RemovalPolicy.DESTROY }); - return new Distribution(stack, 'websiteDeliveryDistribution', { + return new Distribution(stack, 'website-delivery-distribution', { comment: 'Image Optimization - sample website', defaultBehavior: { origin: S3BucketOrigin.withOriginAccessControl(bucket), From 777be8884f3c8cb6bb15b6183ef4c8a9cda2e83e Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Sat, 5 Oct 2024 20:49:16 +0100 Subject: [PATCH 25/28] Rename stuff --- lib/image-optimization-stack.ts | 8 ++++---- ...image-optimization-solution.ts => image-processing.ts} | 4 ++-- ...e-optimization-sample-website.ts => sample-website.ts} | 0 3 files changed, 6 insertions(+), 6 deletions(-) rename lib/{image-optimization-solution.ts => image-processing.ts} (98%) rename lib/{image-optimization-sample-website.ts => sample-website.ts} (100%) diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index fe587a4..e53345f 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -3,8 +3,8 @@ import { aws_s3 as s3, aws_s3_deployment as s3deploy, Duration, CfnOutput, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { readContext } from './cdk-context-utils'; -import { sampleWebsite } from './image-optimization-sample-website'; -import { imageOptimizationSolution } from './image-optimization-solution'; +import { sampleWebsite } from './sample-website'; +import { imageOptimizationSolution } from './image-processing'; import { getOriginShieldRegion } from './origin-shield'; export class ImageOptimizationStack extends Stack { @@ -59,7 +59,7 @@ export class ImageOptimizationStack extends Stack { }); // Create Amazon CloudFront distribution to deliver optimized images - const imageOptimization = imageOptimizationSolution(this, { + const imageDelivery = imageOptimizationSolution(this, { corsEnabled: CORS_ENABLED, lambdaMemory: LAMBDA_MEMORY, lambdaTimeout: Duration.seconds(LAMBDA_TIMEOUT_SECONDS), @@ -72,7 +72,7 @@ export class ImageOptimizationStack extends Stack { }); new CfnOutput(this, 'image-delivery-domain', { description: 'Image delivery domain', - value: imageOptimization.distributionDomainName + value: imageDelivery.distributionDomainName }); } } diff --git a/lib/image-optimization-solution.ts b/lib/image-processing.ts similarity index 98% rename from lib/image-optimization-solution.ts rename to lib/image-processing.ts index e39798f..e1e0782 100644 --- a/lib/image-optimization-solution.ts +++ b/lib/image-processing.ts @@ -10,7 +10,7 @@ import { Duration, Fn, RemovalPolicy, Stack } from 'aws-cdk-lib'; -type ImageProcessingProps = { +type ImageOptimizationProps = { corsEnabled: boolean, originalImageBucket: s3.IBucket, originShieldRegion: string, @@ -22,7 +22,7 @@ type ImageProcessingProps = { storeTransformedImages: boolean, } -export const imageOptimizationSolution = (stack: Stack, props: ImageProcessingProps) => { +export const imageOptimizationSolution = (stack: Stack, props: ImageOptimizationProps) => { const { corsEnabled, originalImageBucket, diff --git a/lib/image-optimization-sample-website.ts b/lib/sample-website.ts similarity index 100% rename from lib/image-optimization-sample-website.ts rename to lib/sample-website.ts From 602e49d4800d8c7d5e6bacb4d2d0384bee1ad79b Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Sat, 5 Oct 2024 22:05:26 +0100 Subject: [PATCH 26/28] Reorder alphabetically --- lib/image-optimization-stack.ts | 6 +++--- lib/image-processing.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index e53345f..827e912 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -41,16 +41,16 @@ export class ImageOptimizationStack extends Stack { originalImageBucket = s3.Bucket.fromBucketName(this, 'imported-original-image-bucket', S3_ORIGINAL_IMAGE_BUCKET_NAME); } else { originalImageBucket = new s3.Bucket(this, 's3-sample-original-image-bucket', { - removalPolicy: RemovalPolicy.DESTROY, + autoDeleteObjects: true, blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, encryption: s3.BucketEncryption.S3_MANAGED, enforceSSL: true, - autoDeleteObjects: true, + removalPolicy: RemovalPolicy.DESTROY, }); new s3deploy.BucketDeployment(this, 'deploy-website', { - sources: [s3deploy.Source.asset('./image-sample')], destinationBucket: originalImageBucket, destinationKeyPrefix: 'images/rio/', + sources: [s3deploy.Source.asset('./image-sample')], }); }; new CfnOutput(this, 'original-images-s3-bucket', { diff --git a/lib/image-processing.ts b/lib/image-processing.ts index e1e0782..a69a159 100644 --- a/lib/image-processing.ts +++ b/lib/image-processing.ts @@ -76,8 +76,8 @@ export const imageOptimizationSolution = (stack: Stack, props: ImageOptimization // Recognize image requests that were processed by this solution customHeadersBehavior: { customHeaders: [ - { header: 'x-aws-image-optimization', value: 'v1.0', override: true }, { header: 'vary', value: 'accept', override: true }, + { header: 'x-aws-image-optimization', value: 'v1.0', override: true }, ] } }); From fb6c8de9d7de382c9dfa70a224cca856e3d00282 Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Sun, 6 Oct 2024 20:38:31 +0100 Subject: [PATCH 27/28] Standardize style --- lib/image-processing.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lib/image-processing.ts b/lib/image-processing.ts index a69a159..f90041c 100644 --- a/lib/image-processing.ts +++ b/lib/image-processing.ts @@ -64,7 +64,7 @@ export const imageOptimizationSolution = (stack: Stack, props: ImageOptimization // Create custom response headers policy with CORS requests allowed for all origins const getCorsResponsePolicy = () => new cloudfront.ResponseHeadersPolicy(stack, 'cors-response-policy', { - responseHeadersPolicyName: `CorsResponsePolicy${stack.node.addr}`, + responseHeadersPolicyName: 'CorsResponsePolicy', corsBehavior: { accessControlAllowCredentials: false, accessControlAllowHeaders: ['*'], @@ -107,7 +107,7 @@ export const imageOptimizationSolution = (stack: Stack, props: ImageOptimization const imageDelivery = new cloudfront.Distribution(stack, 'image-delivery-distribution', { comment: 'Image Optimization - image delivery', defaultBehavior: { - cachePolicy: new cloudfront.CachePolicy(stack, `ImageCachePolicy${stack.node.addr}`, { + cachePolicy: new cloudfront.CachePolicy(stack, 'ImageCachePolicy', { defaultTtl: Duration.hours(24), maxTtl: Duration.days(365), minTtl: Duration.seconds(0), @@ -116,9 +116,9 @@ export const imageOptimizationSolution = (stack: Stack, props: ImageOptimization compress: false, functionAssociations: [{ eventType: cloudfront.FunctionEventType.VIEWER_REQUEST, - function: new cloudfront.Function(stack, 'urlRewrite', { + function: new cloudfront.Function(stack, 'url-rewrite', { code: cloudfront.FunctionCode.fromFile({ filePath: 'functions/url-rewrite/index.js' }), - functionName: `urlRewriteFunction${stack.node.addr}`, + functionName: 'urlRewriteFunction', }) }], origin: storeTransformedImages ? getS3OriginWithFallbackToLambda() : imageProcessingLambdaOrigin, @@ -130,7 +130,7 @@ export const imageOptimizationSolution = (stack: Stack, props: ImageOptimization // Add OAC between CloudFront and LambdaURL const oac = new cloudfront.CfnOriginAccessControl(stack, 'origin-access-control', { originAccessControlConfig: { - name: `oac${stack.node.addr}`, + name: 'lambda-oac', originAccessControlOriginType: 'lambda', signingBehavior: 'always', signingProtocol: 'sigv4', @@ -140,8 +140,8 @@ export const imageOptimizationSolution = (stack: Stack, props: ImageOptimization const cfnImageDelivery = imageDelivery.node.defaultChild as cloudfront.CfnDistribution; cfnImageDelivery.addPropertyOverride(`DistributionConfig.Origins.${storeTransformedImages ? '1' : '0'}.OriginAccessControlId`, oac.getAtt('Id')); imageProcessing.addPermission('AllowCloudFrontServicePrincipal', { - principal: new iam.ServicePrincipal("cloudfront.amazonaws.com"), action: 'lambda:InvokeFunctionUrl', + principal: new iam.ServicePrincipal('cloudfront.amazonaws.com'), sourceArn: `arn:aws:cloudfront::${stack.account}:distribution/${imageDelivery.distributionId}` }); From e8c819d7e0b4bf7da80fbe2e6b662c8b2c7b15c6 Mon Sep 17 00:00:00 2001 From: Piotrek Witkowski Date: Mon, 7 Oct 2024 20:55:31 +0100 Subject: [PATCH 28/28] Reorder imports alphabetically --- lib/image-optimization-stack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/image-optimization-stack.ts b/lib/image-optimization-stack.ts index 827e912..da6d28a 100644 --- a/lib/image-optimization-stack.ts +++ b/lib/image-optimization-stack.ts @@ -1,6 +1,6 @@ // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT-0 -import { aws_s3 as s3, aws_s3_deployment as s3deploy, Duration, CfnOutput, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'; +import { aws_s3 as s3, aws_s3_deployment as s3deploy, CfnOutput, Duration, RemovalPolicy, Stack, StackProps } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import { readContext } from './cdk-context-utils'; import { sampleWebsite } from './sample-website';