├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── architecture.png ├── bin └── image-optimization.ts ├── cdk.json ├── functions ├── image-processing │ └── index.mjs └── url-rewrite │ └── index.js ├── image-sample ├── 1.jpeg ├── 2.jpeg ├── 3.jpeg └── 4.jpeg ├── lib ├── image-optimization-stack.ts └── origin-shield.ts ├── package.json └── tsconfig.json /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Expected behavior** 14 | A clear and concise description of what you expected to happen. 15 | 16 | **Desktop (please complete the following information):** 17 | - OS & version: 18 | - Node version (`node -v`): 19 | - npm version (`npm -v`): 20 | 21 | **Additional context** 22 | Add any other context about the problem here. 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.js 2 | !/functions/url-rewrite/index.js 3 | !jest.config.js 4 | *.d.ts 5 | node_modules 6 | 7 | # CDK asset staging directory 8 | .cdk.staging 9 | cdk.out 10 | .DS_Store 11 | 12 | package-lock.json 13 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 10 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 11 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 12 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 13 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 14 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 15 | 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Image Optimization 2 | 3 | Images are usually the heaviest components of a web page, both in terms of bytes and number of HTTP requests. Optimizing images on your website is critical to improve your users’ experience, reduce delivery costs and enhance your position in search engine ranking. For example, Google’s Largest Contentful Paint metric in their search ranking algorithm is highly impacted by how much you optimize the images on your website. In the solution, we provide you with a simple and performant solution for image optimization using serverless components such as Amazon CloudFront, Amazon S3 and AWS Lambda. 4 | 5 | The proposed architecture is suitable for most common use cases. Image transformation is executed centrally in an AWS Region, only when the image hasn’t been already transformed and stored. The available transformations include resizing and formatting, but can be extended to more operations if needed. Both transformations can be requested by the front-end, with the possibility of automatic format selection done on server side. The architecture is based on S3 for storage, CloudFront for content delivery, and Lambda for image processing. The request flow is explained in the next diagram: 6 | 7 | 8 | 9 | 1. The user sends a HTTP request for an image with specific transformations, such as encoding and size. The transformations are encoded in the URL, more precisely as query parameters. An example URL would look like this: https://examples.com/images/cats/mycat.jpg?format=webp&width=200. 10 | 2. The request is processed by a nearby CloudFront edge location providing the best performance. Before passing the request upstream, a CloudFront Function is executed on viewer request event to rewrite the request URL. CloudFront Functions is a feature of CloudFront that allows you to write lightweight functions in JavaScript for high-scale, latency-sensitive CDN customizations. In our architecture, we rewrite the URL to validate the requested transformations and normalize the URL by ordering transformations and convert them to lower case to increase the cache hit ratio. When an automatic transformation is requested, the function also decides about the best one to apply. For example, if the user asks for the most optimized image format (JPEG, WebP, or AVIF) using the directive format=auto, CloudFront Function will select the best format based on the Accept header present in the request. 11 | 3. If the requested image is already cached in CloudFront then there will be a cache hit and the image is returned from CloudFront cache. To increase the cache hit ratio, we enable Origin shield, a feature of CloudFront that acts as an additional layer of caching before the origin, to further offload it from requests. If the Image is not in CloudFront cache, then the request will be forwarded to an S3 bucket, which is created to store the transformed images. If the requested image is already transformed and stored in S3, then it is simply served and cached in CloudFront. 12 | 4. Otherwise, S3 will respond with a 403 error code, which is detected by CloudFront’s Origin Failover. Thanks to this native feature, CloudFront retries the same URL but this time using the secondary origin based on Lambda function URL. When invoked, the Lambda function downloads the original image from another S3 bucket, where original images are stored, transforms it using Sharp library, stores the transformed image in S3, then serve it through CloudFront where it will be cached for future requests. 13 | 14 | Note the following: 15 | 16 | * The transformed image is stored in S3 with a lifecycle policy that deletes it after a certain duration (default of 90 days) to reduce the storage cost. Ideally, you’d set this value according to the duration after which the number of requests to a new image drops significantly. They are created with the same key as the original image in addition to a suffix based on the normalized image transformations. For example, the transformed image in response to /mycat.jpg?format=auto&width=200 would be stored with the key /mycat.jpg/format=webp,width=200 if the automatically detected format was webp. To remove all generated variants of the same image in S3, delete all files listed under the key of the original image /mycat.jpg/*. Transformed images are added to S3 with a Cache-Control header of 1 year. If you need to invalidate all cached variants of an image in CloudFront, use the following invalidation pattern: /mycat.jpg*. 17 | * To prevent from unauthorized invocations of the Lambda function, CloudFront is configured with OAC to sign requests using sigV4 before sending them to invoke the Lambda service. 18 | 19 | ## Deploy the solution using CDK 20 | 21 | > [!NOTE] 22 | > This solution is using [sharp](https://github.com/lovell/sharp) library for image processing. Your local development environment and the image processing Lambda function environment may be using different CPU and OS architectures - for example, when you are on an M1 Mac, trying to build code for a Linux-based, x86 Lambda runtime. If necessary, the solution will automatically perform a [cross-platform](https://sharp.pixelplumbing.com/install#cross-platform) installation of all required dependencies. **Ensure your local npm version is 10.4.0 or later**, to correctly leverage npm flags for native dependency management and take advantage of Lambda function size optimizations. 23 | 24 | ``` 25 | git clone https://github.com/aws-samples/image-optimization.git 26 | cd image-optimization 27 | npm install 28 | cdk bootstrap 29 | npm run build 30 | cdk deploy 31 | ``` 32 | 33 | When the deployment is completed within minutes, the CDK output will include the domain name of the CloudFront distribution created for image optimization (ImageDeliveryDomain =YOURDISTRIBUTION.cloudfront.net). The stack will include an S3 bucket with sample images (OriginalImagesS3Bucket = YourS3BucketWithOriginalImagesGeneratedName). To verify that it is working properly, test the following optimized image URL https:// YOURDISTRIBUTION.cloudfront.net/images/rio/1.jpeg?format=auto&width=300. 34 | 35 | The stack can be deployed with the following parameters. 36 | * **S3_IMAGE_BUCKET_NAME** Recommended for using an existing S3 bucket where your images are stored when deploying in production. Usage: cdk deploy -c S3_IMAGE_BUCKET_NAME=’YOUR_S3_BUCKET_NAME’. Without specifiying this parameter, the stack creates a new S3 bucket and sample images of Rio the dog ^^ 37 | * **STORE_TRANSFORMED_IMAGES** Allows you to avoid temporary storage of transformed images, every image request is sent for transformation using Lambda upon cache miss in CloudFront. Usage: cdk deploy -c STORE_TRANSFORMED_IMAGES=false. The default value of this paramter is true. 38 | * **S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION** When STORE_TRANSFORMED_IMAGES is set to true, this paramter allows you to set the expiration time in days, of the stored transfomed images in S3. After this expiration time, objects are deleted to save storage cost. Usage: cdk deploy -c S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION=10. The default value of this paramter is 90 days. 39 | * **S3_TRANSFORMED_IMAGE_CACHE_TTL** When STORE_TRANSFORMED_IMAGES is set to true, this paramter allows you to set a Cache-Control directive on transformed images. Usage: cdk deploy -c S3_TRANSFORMED_IMAGE_CACHE_TTL='max-age=3600'. The default value of this paramter is 'max-age=31622400'. 40 | * **CLOUDFRONT_ORIGIN_SHIELD_REGION** Specify this parameter when you do not want the stack to automatically choose the Origin Shield region for you. Usage: cdk deploy -c CLOUDFRONT_ORIGIN_SHIELD_REGION=us-east-1. Default value is automatically selected based on the region of the stack. 41 | * **CLOUDFRONT_CORS_ENABLED** Specify this parameter if you want to allow/disallow other domains to serve images from your image delivery Cloudfront distribution. Usage: cdk deploy -c CLOUDFRONT_CORS_ENABLED=false. Default value is set to true. 42 | * **LAMBDA_MEMORY** Speficy this parameter to tune the memory in MB of the Lambda function that processes images, with the goal of improving processing performance. Usage: cdk deploy -c LAMBDA_MEMORY=2000. Default value is 1500 MB. 43 | * **LAMBDA_TIMEOUT** Speficy this parameter to tune the timeout in seconds of the Lambda function that processes images. Usage: cdk deploy -c LAMBDA_TIMEOUT=10. Default value is 60 seconds. 44 | * **MAX_IMAGE_SIZE** Speficy this parameter to set a maximum request image size in bytes. If STORE_TRANSFORMED_IMAGES=false, requests resulting in images bigger than MAX_IMAGE_SIZE fail to 5xx error. Otherwise, Lambda transforms the image, uploads it to S3, then sends a redirect to the same image location on S3 to avoid hitting the Lambda output size limit. Usage: cdk deploy -c MAX_IMAGE_SIZE=200000. Default value is 4700000 bytes. 45 | * **DEPLOY_SAMPLE_WEBSITE** set this paramter to true if you want the stack to include another CloudFront distribution pointing to an S3 bucket, that you can use for static website hosting. This option is used in the initial solution [post](https://aws.amazon.com/blogs/networking-and-content-delivery/image-optimization-using-amazon-cloudfront-and-aws-lambda/) 46 | 47 | 48 | ## Clean up resources 49 | 50 | To remove cloud resources created for this solution, just execute the following command: 51 | 52 | ``` 53 | cdk destroy 54 | ``` 55 | 56 | ## License 57 | 58 | This library is licensed under the MIT-0 License. See the LICENSE file. 59 | 60 | -------------------------------------------------------------------------------- /architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/image-optimization/87ccc5d8ecb4ae6ba8d4ac542181a4ceb4c68ace/architecture.png -------------------------------------------------------------------------------- /bin/image-optimization.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import 'source-map-support/register'; 3 | import * as cdk from 'aws-cdk-lib'; 4 | import { ImageOptimizationStack } from '../lib/image-optimization-stack'; 5 | 6 | 7 | const app = new cdk.App(); 8 | new ImageOptimizationStack(app, 'ImgTransformationStack', { 9 | 10 | }); 11 | 12 | -------------------------------------------------------------------------------- /cdk.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": "npx ts-node --prefer-ts-exts bin/image-optimization.ts", 3 | "watch": { 4 | "include": [ 5 | "**" 6 | ], 7 | "exclude": [ 8 | "README.md", 9 | "cdk*.json", 10 | "**/*.d.ts", 11 | "**/*.js", 12 | "tsconfig.json", 13 | "package*.json", 14 | "yarn.lock", 15 | "node_modules", 16 | "test" 17 | ] 18 | }, 19 | "context": { 20 | "@aws-cdk/aws-apigateway:usagePlanKeyOrderInsensitiveId": true, 21 | "@aws-cdk/core:stackRelativeExports": true, 22 | "@aws-cdk/aws-rds:lowercaseDbIdentifier": true, 23 | "@aws-cdk/aws-lambda:recognizeVersionProps": true, 24 | "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, 25 | "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, 26 | "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, 27 | "@aws-cdk/core:checkSecretUsage": true, 28 | "@aws-cdk/aws-iam:minimizePolicies": true, 29 | "@aws-cdk/core:target-partitions": [ 30 | "aws", 31 | "aws-cn" 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /functions/image-processing/index.mjs: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | import { GetObjectCommand, PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; 5 | import Sharp from 'sharp'; 6 | 7 | const s3Client = new S3Client(); 8 | const S3_ORIGINAL_IMAGE_BUCKET = process.env.originalImageBucketName; 9 | const S3_TRANSFORMED_IMAGE_BUCKET = process.env.transformedImageBucketName; 10 | const TRANSFORMED_IMAGE_CACHE_TTL = process.env.transformedImageCacheTTL; 11 | const MAX_IMAGE_SIZE = parseInt(process.env.maxImageSize); 12 | 13 | export const handler = async (event) => { 14 | // Validate if this is a GET request 15 | if (!event.requestContext || !event.requestContext.http || !(event.requestContext.http.method === 'GET')) return sendError(400, 'Only GET method is supported', event); 16 | // An example of expected path is /images/rio/1.jpeg/format=auto,width=100 or /images/rio/1.jpeg/original where /images/rio/1.jpeg is the path of the original image 17 | var imagePathArray = event.requestContext.http.path.split('/'); 18 | // get the requested image operations 19 | var operationsPrefix = imagePathArray.pop(); 20 | // get the original image path images/rio/1.jpg 21 | imagePathArray.shift(); 22 | var originalImagePath = imagePathArray.join('/'); 23 | 24 | var startTime = performance.now(); 25 | // Downloading original image 26 | let originalImageBody; 27 | let contentType; 28 | try { 29 | const getOriginalImageCommand = new GetObjectCommand({ Bucket: S3_ORIGINAL_IMAGE_BUCKET, Key: originalImagePath }); 30 | const getOriginalImageCommandOutput = await s3Client.send(getOriginalImageCommand); 31 | console.log(`Got response from S3 for ${originalImagePath}`); 32 | 33 | originalImageBody = getOriginalImageCommandOutput.Body.transformToByteArray(); 34 | contentType = getOriginalImageCommandOutput.ContentType; 35 | } catch (error) { 36 | if (error.name === "NoSuchKey") { 37 | return sendError(404, "The requested image does not exist", error); 38 | } 39 | return sendError(500, 'Error downloading original image', error); 40 | } 41 | let transformedImage = Sharp(await originalImageBody, { failOn: 'none', animated: true }); 42 | // Get image orientation to rotate if needed 43 | const imageMetadata = await transformedImage.metadata(); 44 | // execute the requested operations 45 | const operationsJSON = Object.fromEntries(operationsPrefix.split(',').map(operation => operation.split('='))); 46 | // variable holding the server timing header value 47 | var timingLog = 'img-download;dur=' + parseInt(performance.now() - startTime); 48 | startTime = performance.now(); 49 | try { 50 | // check if resizing is requested 51 | var resizingOptions = {}; 52 | if (operationsJSON['width']) resizingOptions.width = parseInt(operationsJSON['width']); 53 | if (operationsJSON['height']) resizingOptions.height = parseInt(operationsJSON['height']); 54 | if (resizingOptions) transformedImage = transformedImage.resize(resizingOptions); 55 | // check if rotation is needed 56 | if (imageMetadata.orientation) transformedImage = transformedImage.rotate(); 57 | // check if formatting is requested 58 | if (operationsJSON['format']) { 59 | var isLossy = false; 60 | switch (operationsJSON['format']) { 61 | case 'jpeg': contentType = 'image/jpeg'; isLossy = true; break; 62 | case 'gif': contentType = 'image/gif'; break; 63 | case 'webp': contentType = 'image/webp'; isLossy = true; break; 64 | case 'png': contentType = 'image/png'; break; 65 | case 'avif': contentType = 'image/avif'; isLossy = true; break; 66 | default: contentType = 'image/jpeg'; isLossy = true; 67 | } 68 | if (operationsJSON['quality'] && isLossy) { 69 | transformedImage = transformedImage.toFormat(operationsJSON['format'], { 70 | quality: parseInt(operationsJSON['quality']), 71 | }); 72 | } else transformedImage = transformedImage.toFormat(operationsJSON['format']); 73 | } else { 74 | /// If not format is precised, Sharp converts svg to png by default https://github.com/aws-samples/image-optimization/issues/48 75 | if (contentType === 'image/svg+xml') contentType = 'image/png'; 76 | } 77 | transformedImage = await transformedImage.toBuffer(); 78 | } catch (error) { 79 | return sendError(500, 'error transforming image', error); 80 | } 81 | timingLog = timingLog + ',img-transform;dur=' + parseInt(performance.now() - startTime); 82 | 83 | // handle gracefully generated images bigger than a specified limit (e.g. Lambda output object limit) 84 | const imageTooBig = Buffer.byteLength(transformedImage) > MAX_IMAGE_SIZE; 85 | 86 | // upload transformed image back to S3 if required in the architecture 87 | if (S3_TRANSFORMED_IMAGE_BUCKET) { 88 | startTime = performance.now(); 89 | try { 90 | const putImageCommand = new PutObjectCommand({ 91 | Body: transformedImage, 92 | Bucket: S3_TRANSFORMED_IMAGE_BUCKET, 93 | Key: originalImagePath + '/' + operationsPrefix, 94 | ContentType: contentType, 95 | CacheControl: TRANSFORMED_IMAGE_CACHE_TTL, 96 | }) 97 | await s3Client.send(putImageCommand); 98 | timingLog = timingLog + ',img-upload;dur=' + parseInt(performance.now() - startTime); 99 | // If the generated image file is too big, send a redirection to the generated image on S3, instead of serving it synchronously from Lambda. 100 | if (imageTooBig) { 101 | return { 102 | statusCode: 302, 103 | headers: { 104 | 'Location': '/' + originalImagePath + '?' + operationsPrefix.replace(/,/g, "&"), 105 | 'Cache-Control': 'private,no-store', 106 | 'Server-Timing': timingLog 107 | } 108 | }; 109 | } 110 | } catch (error) { 111 | logError('Could not upload transformed image to S3', error); 112 | } 113 | } 114 | 115 | // Return error if the image is too big and a redirection to the generated image was not possible, else return transformed image 116 | if (imageTooBig) { 117 | return sendError(403, 'Requested transformed image is too big', ''); 118 | } else return { 119 | statusCode: 200, 120 | body: transformedImage.toString('base64'), 121 | isBase64Encoded: true, 122 | headers: { 123 | 'Content-Type': contentType, 124 | 'Cache-Control': TRANSFORMED_IMAGE_CACHE_TTL, 125 | 'Server-Timing': timingLog 126 | } 127 | }; 128 | }; 129 | 130 | function sendError(statusCode, body, error) { 131 | logError(body, error); 132 | return { statusCode, body }; 133 | } 134 | 135 | function logError(body, error) { 136 | console.log('APPLICATION ERROR', body); 137 | console.log(error); 138 | } 139 | -------------------------------------------------------------------------------- /functions/url-rewrite/index.js: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | function handler(event) { 5 | var request = event.request; 6 | var originalImagePath = request.uri; 7 | // validate, process and normalize the requested operations in query parameters 8 | var normalizedOperations = {}; 9 | if (request.querystring) { 10 | Object.keys(request.querystring).forEach(operation => { 11 | switch (operation.toLowerCase()) { 12 | case 'format': 13 | var SUPPORTED_FORMATS = ['auto', 'jpeg', 'webp', 'avif', 'png', 'svg', 'gif']; 14 | if (request.querystring[operation]['value'] && SUPPORTED_FORMATS.includes(request.querystring[operation]['value'].toLowerCase())) { 15 | var format = request.querystring[operation]['value'].toLowerCase(); // normalize to lowercase 16 | if (format === 'auto') { 17 | format = 'jpeg'; 18 | if (request.headers['accept']) { 19 | if (request.headers['accept'].value.includes("avif")) { 20 | format = 'avif'; 21 | } else if (request.headers['accept'].value.includes("webp")) { 22 | format = 'webp'; 23 | } 24 | } 25 | } 26 | normalizedOperations['format'] = format; 27 | } 28 | break; 29 | case 'width': 30 | if (request.querystring[operation]['value']) { 31 | var width = parseInt(request.querystring[operation]['value']); 32 | if (!isNaN(width) && (width > 0)) { 33 | // you can protect the Lambda function by setting a max value, e.g. if (width > 4000) width = 4000; 34 | normalizedOperations['width'] = width.toString(); 35 | } 36 | } 37 | break; 38 | case 'height': 39 | if (request.querystring[operation]['value']) { 40 | var height = parseInt(request.querystring[operation]['value']); 41 | if (!isNaN(height) && (height > 0)) { 42 | // you can protect the Lambda function by setting a max value, e.g. if (height > 4000) height = 4000; 43 | normalizedOperations['height'] = height.toString(); 44 | } 45 | } 46 | break; 47 | case 'quality': 48 | if (request.querystring[operation]['value']) { 49 | var quality = parseInt(request.querystring[operation]['value']); 50 | if (!isNaN(quality) && (quality > 0)) { 51 | if (quality > 100) quality = 100; 52 | normalizedOperations['quality'] = quality.toString(); 53 | } 54 | } 55 | break; 56 | default: break; 57 | } 58 | }); 59 | //rewrite the path to normalized version if valid operations are found 60 | if (Object.keys(normalizedOperations).length > 0) { 61 | // put them in order 62 | var normalizedOperationsArray = []; 63 | if (normalizedOperations.format) normalizedOperationsArray.push('format='+normalizedOperations.format); 64 | if (normalizedOperations.quality) normalizedOperationsArray.push('quality='+normalizedOperations.quality); 65 | if (normalizedOperations.width) normalizedOperationsArray.push('width='+normalizedOperations.width); 66 | if (normalizedOperations.height) normalizedOperationsArray.push('height='+normalizedOperations.height); 67 | request.uri = originalImagePath + '/' + normalizedOperationsArray.join(','); 68 | } else { 69 | // If no valid operation is found, flag the request with /original path suffix 70 | request.uri = originalImagePath + '/original'; 71 | } 72 | 73 | } else { 74 | // If no query strings are found, flag the request with /original path suffix 75 | request.uri = originalImagePath + '/original'; 76 | } 77 | // remove query strings 78 | request['querystring'] = {}; 79 | return request; 80 | } 81 | -------------------------------------------------------------------------------- /image-sample/1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/image-optimization/87ccc5d8ecb4ae6ba8d4ac542181a4ceb4c68ace/image-sample/1.jpeg -------------------------------------------------------------------------------- /image-sample/2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/image-optimization/87ccc5d8ecb4ae6ba8d4ac542181a4ceb4c68ace/image-sample/2.jpeg -------------------------------------------------------------------------------- /image-sample/3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/image-optimization/87ccc5d8ecb4ae6ba8d4ac542181a4ceb4c68ace/image-sample/3.jpeg -------------------------------------------------------------------------------- /image-sample/4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/image-optimization/87ccc5d8ecb4ae6ba8d4ac542181a4ceb4c68ace/image-sample/4.jpeg -------------------------------------------------------------------------------- /lib/image-optimization-stack.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | 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'; 5 | import { CfnDistribution } from "aws-cdk-lib/aws-cloudfront"; 6 | import { Construct } from 'constructs'; 7 | import { getOriginShieldRegion } from './origin-shield'; 8 | import { createHash } from 'crypto'; 9 | 10 | // Stack Parameters 11 | 12 | // related to architecture. If set to false, transformed images are not stored in S3, and all image requests land on Lambda 13 | var STORE_TRANSFORMED_IMAGES = 'true'; 14 | // Parameters of S3 bucket where original images are stored 15 | var S3_IMAGE_BUCKET_NAME: string; 16 | // CloudFront parameters 17 | var CLOUDFRONT_ORIGIN_SHIELD_REGION = getOriginShieldRegion(process.env.AWS_REGION || process.env.CDK_DEFAULT_REGION || 'us-east-1'); 18 | var CLOUDFRONT_CORS_ENABLED = 'true'; 19 | // Parameters of transformed images 20 | var S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION = '90'; 21 | var S3_TRANSFORMED_IMAGE_CACHE_TTL = 'max-age=31622400'; 22 | // Max image size in bytes. If generated images are stored on S3, bigger images are generated, stored on S3 23 | // and request is redirect to the generated image. Otherwise, an application error is sent. 24 | var MAX_IMAGE_SIZE = '4700000'; 25 | // Lambda Parameters 26 | var LAMBDA_MEMORY = '1500'; 27 | var LAMBDA_TIMEOUT = '60'; 28 | // 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/ 29 | var DEPLOY_SAMPLE_WEBSITE = 'false'; 30 | 31 | type ImageDeliveryCacheBehaviorConfig = { 32 | origin: any; 33 | compress: any; 34 | viewerProtocolPolicy: any; 35 | cachePolicy: any; 36 | functionAssociations: any; 37 | responseHeadersPolicy?: any; 38 | }; 39 | 40 | type LambdaEnv = { 41 | originalImageBucketName: string, 42 | transformedImageBucketName?: any; 43 | transformedImageCacheTTL: string, 44 | maxImageSize: string, 45 | } 46 | 47 | export class ImageOptimizationStack extends Stack { 48 | constructor(scope: Construct, id: string, props?: StackProps) { 49 | super(scope, id, props); 50 | 51 | // Change stack parameters based on provided context 52 | STORE_TRANSFORMED_IMAGES = this.node.tryGetContext('STORE_TRANSFORMED_IMAGES') || STORE_TRANSFORMED_IMAGES; 53 | S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION = this.node.tryGetContext('S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION') || S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION; 54 | S3_TRANSFORMED_IMAGE_CACHE_TTL = this.node.tryGetContext('S3_TRANSFORMED_IMAGE_CACHE_TTL') || S3_TRANSFORMED_IMAGE_CACHE_TTL; 55 | S3_IMAGE_BUCKET_NAME = this.node.tryGetContext('S3_IMAGE_BUCKET_NAME') || S3_IMAGE_BUCKET_NAME; 56 | CLOUDFRONT_ORIGIN_SHIELD_REGION = this.node.tryGetContext('CLOUDFRONT_ORIGIN_SHIELD_REGION') || CLOUDFRONT_ORIGIN_SHIELD_REGION; 57 | CLOUDFRONT_CORS_ENABLED = this.node.tryGetContext('CLOUDFRONT_CORS_ENABLED') || CLOUDFRONT_CORS_ENABLED; 58 | LAMBDA_MEMORY = this.node.tryGetContext('LAMBDA_MEMORY') || LAMBDA_MEMORY; 59 | LAMBDA_TIMEOUT = this.node.tryGetContext('LAMBDA_TIMEOUT') || LAMBDA_TIMEOUT; 60 | MAX_IMAGE_SIZE = this.node.tryGetContext('MAX_IMAGE_SIZE') || MAX_IMAGE_SIZE; 61 | DEPLOY_SAMPLE_WEBSITE = this.node.tryGetContext('DEPLOY_SAMPLE_WEBSITE') || DEPLOY_SAMPLE_WEBSITE; 62 | 63 | 64 | // deploy a sample website for testing if required 65 | if (DEPLOY_SAMPLE_WEBSITE === 'true') { 66 | var sampleWebsiteBucket = new s3.Bucket(this, 's3-sample-website-bucket', { 67 | removalPolicy: RemovalPolicy.DESTROY, 68 | blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, 69 | encryption: s3.BucketEncryption.S3_MANAGED, 70 | enforceSSL: true, 71 | autoDeleteObjects: true, 72 | }); 73 | 74 | var sampleWebsiteDelivery = new cloudfront.Distribution(this, 'websiteDeliveryDistribution', { 75 | comment: 'image optimization - sample website', 76 | defaultRootObject: 'index.html', 77 | defaultBehavior: { 78 | origin: new origins.S3Origin(sampleWebsiteBucket), 79 | viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 80 | } 81 | }); 82 | 83 | new CfnOutput(this, 'SampleWebsiteDomain', { 84 | description: 'Sample website domain', 85 | value: sampleWebsiteDelivery.distributionDomainName 86 | }); 87 | new CfnOutput(this, 'SampleWebsiteS3Bucket', { 88 | description: 'S3 bucket use by the sample website', 89 | value: sampleWebsiteBucket.bucketName 90 | }); 91 | } 92 | 93 | // For the bucket having original images, either use an external one, or create one with some samples photos. 94 | var originalImageBucket; 95 | var transformedImageBucket; 96 | 97 | if (S3_IMAGE_BUCKET_NAME) { 98 | originalImageBucket = s3.Bucket.fromBucketName(this, 'imported-original-image-bucket', S3_IMAGE_BUCKET_NAME); 99 | new CfnOutput(this, 'OriginalImagesS3Bucket', { 100 | description: 'S3 bucket where original images are stored', 101 | value: originalImageBucket.bucketName 102 | }); 103 | } else { 104 | originalImageBucket = new s3.Bucket(this, 's3-sample-original-image-bucket', { 105 | removalPolicy: RemovalPolicy.DESTROY, 106 | blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, 107 | encryption: s3.BucketEncryption.S3_MANAGED, 108 | enforceSSL: true, 109 | autoDeleteObjects: true, 110 | }); 111 | new s3deploy.BucketDeployment(this, 'DeployWebsite', { 112 | sources: [s3deploy.Source.asset('./image-sample')], 113 | destinationBucket: originalImageBucket, 114 | destinationKeyPrefix: 'images/rio/', 115 | }); 116 | new CfnOutput(this, 'OriginalImagesS3Bucket', { 117 | description: 'S3 bucket where original images are stored', 118 | value: originalImageBucket.bucketName 119 | }); 120 | } 121 | 122 | // create bucket for transformed images if enabled in the architecture 123 | if (STORE_TRANSFORMED_IMAGES === 'true') { 124 | transformedImageBucket = new s3.Bucket(this, 's3-transformed-image-bucket', { 125 | removalPolicy: RemovalPolicy.DESTROY, 126 | autoDeleteObjects: true, 127 | lifecycleRules: [ 128 | { 129 | expiration: Duration.days(parseInt(S3_TRANSFORMED_IMAGE_EXPIRATION_DURATION)), 130 | }, 131 | ], 132 | }); 133 | } 134 | 135 | // prepare env variable for Lambda 136 | var lambdaEnv: LambdaEnv = { 137 | originalImageBucketName: originalImageBucket.bucketName, 138 | transformedImageCacheTTL: S3_TRANSFORMED_IMAGE_CACHE_TTL, 139 | maxImageSize: MAX_IMAGE_SIZE, 140 | }; 141 | if (transformedImageBucket) lambdaEnv.transformedImageBucketName = transformedImageBucket.bucketName; 142 | 143 | // IAM policy to read from the S3 bucket containing the original images 144 | const s3ReadOriginalImagesPolicy = new iam.PolicyStatement({ 145 | actions: ['s3:GetObject'], 146 | resources: ['arn:aws:s3:::' + originalImageBucket.bucketName + '/*'], 147 | }); 148 | 149 | // statements of the IAM policy to attach to Lambda 150 | var iamPolicyStatements = [s3ReadOriginalImagesPolicy]; 151 | 152 | // Create Lambda for image processing 153 | var lambdaProps = { 154 | runtime: lambda.Runtime.NODEJS_20_X, 155 | handler: 'index.handler', 156 | code: lambda.Code.fromAsset('functions/image-processing'), 157 | timeout: Duration.seconds(parseInt(LAMBDA_TIMEOUT)), 158 | memorySize: parseInt(LAMBDA_MEMORY), 159 | environment: lambdaEnv, 160 | logRetention: logs.RetentionDays.ONE_DAY, 161 | }; 162 | var imageProcessing = new lambda.Function(this, 'image-optimization', lambdaProps); 163 | 164 | // Enable Lambda URL 165 | const imageProcessingURL = imageProcessing.addFunctionUrl(); 166 | 167 | // Leverage CDK Intrinsics to get the hostname of the Lambda URL 168 | const imageProcessingDomainName = Fn.parseDomainName(imageProcessingURL.url); 169 | 170 | // Create a CloudFront origin: S3 with fallback to Lambda when image needs to be transformed, otherwise with Lambda as sole origin 171 | var imageOrigin; 172 | 173 | if (transformedImageBucket) { 174 | imageOrigin = new origins.OriginGroup({ 175 | primaryOrigin: new origins.S3Origin(transformedImageBucket, { 176 | originShieldRegion: CLOUDFRONT_ORIGIN_SHIELD_REGION, 177 | }), 178 | fallbackOrigin: new origins.HttpOrigin(imageProcessingDomainName, { 179 | originShieldRegion: CLOUDFRONT_ORIGIN_SHIELD_REGION, 180 | }), 181 | fallbackStatusCodes: [403, 500, 503, 504], 182 | }); 183 | 184 | // write policy for Lambda on the s3 bucket for transformed images 185 | var s3WriteTransformedImagesPolicy = new iam.PolicyStatement({ 186 | actions: ['s3:PutObject'], 187 | resources: ['arn:aws:s3:::' + transformedImageBucket.bucketName + '/*'], 188 | }); 189 | iamPolicyStatements.push(s3WriteTransformedImagesPolicy); 190 | } else { 191 | imageOrigin = new origins.HttpOrigin(imageProcessingDomainName, { 192 | originShieldRegion: CLOUDFRONT_ORIGIN_SHIELD_REGION, 193 | }); 194 | } 195 | 196 | // attach iam policy to the role assumed by Lambda 197 | imageProcessing.role?.attachInlinePolicy( 198 | new iam.Policy(this, 'read-write-bucket-policy', { 199 | statements: iamPolicyStatements, 200 | }), 201 | ); 202 | 203 | // Create a CloudFront Function for url rewrites 204 | const urlRewriteFunction = new cloudfront.Function(this, 'urlRewrite', { 205 | code: cloudfront.FunctionCode.fromFile({ filePath: 'functions/url-rewrite/index.js', }), 206 | functionName: `urlRewriteFunction${this.node.addr}`, 207 | }); 208 | 209 | var imageDeliveryCacheBehaviorConfig: ImageDeliveryCacheBehaviorConfig = { 210 | origin: imageOrigin, 211 | viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, 212 | compress: false, 213 | cachePolicy: new cloudfront.CachePolicy(this, `ImageCachePolicy${this.node.addr}`, { 214 | defaultTtl: Duration.hours(24), 215 | maxTtl: Duration.days(365), 216 | minTtl: Duration.seconds(0) 217 | }), 218 | functionAssociations: [{ 219 | eventType: cloudfront.FunctionEventType.VIEWER_REQUEST, 220 | function: urlRewriteFunction, 221 | }], 222 | } 223 | 224 | if (CLOUDFRONT_CORS_ENABLED === 'true') { 225 | // Creating a custom response headers policy. CORS allowed for all origins. 226 | const imageResponseHeadersPolicy = new cloudfront.ResponseHeadersPolicy(this, `ResponseHeadersPolicy${this.node.addr}`, { 227 | responseHeadersPolicyName: `ImageResponsePolicy${this.node.addr}`, 228 | corsBehavior: { 229 | accessControlAllowCredentials: false, 230 | accessControlAllowHeaders: ['*'], 231 | accessControlAllowMethods: ['GET'], 232 | accessControlAllowOrigins: ['*'], 233 | accessControlMaxAge: Duration.seconds(600), 234 | originOverride: false, 235 | }, 236 | // recognizing image requests that were processed by this solution 237 | customHeadersBehavior: { 238 | customHeaders: [ 239 | { header: 'x-aws-image-optimization', value: 'v1.0', override: true }, 240 | { header: 'vary', value: 'accept', override: true }, 241 | ], 242 | } 243 | }); 244 | imageDeliveryCacheBehaviorConfig.responseHeadersPolicy = imageResponseHeadersPolicy; 245 | } 246 | const imageDelivery = new cloudfront.Distribution(this, 'imageDeliveryDistribution', { 247 | comment: 'image optimization - image delivery', 248 | defaultBehavior: imageDeliveryCacheBehaviorConfig 249 | }); 250 | 251 | // ADD OAC between CloudFront and LambdaURL 252 | const oac = new cloudfront.CfnOriginAccessControl(this, "OAC", { 253 | originAccessControlConfig: { 254 | name: `oac${this.node.addr}`, 255 | originAccessControlOriginType: "lambda", 256 | signingBehavior: "always", 257 | signingProtocol: "sigv4", 258 | }, 259 | }); 260 | 261 | const cfnImageDelivery = imageDelivery.node.defaultChild as CfnDistribution; 262 | cfnImageDelivery.addPropertyOverride(`DistributionConfig.Origins.${(STORE_TRANSFORMED_IMAGES === 'true')?"1":"0"}.OriginAccessControlId`, oac.getAtt("Id")); 263 | 264 | imageProcessing.addPermission("AllowCloudFrontServicePrincipal", { 265 | principal: new iam.ServicePrincipal("cloudfront.amazonaws.com"), 266 | action: "lambda:InvokeFunctionUrl", 267 | sourceArn: `arn:aws:cloudfront::${this.account}:distribution/${imageDelivery.distributionId}` 268 | }) 269 | 270 | new CfnOutput(this, 'ImageDeliveryDomain', { 271 | description: 'Domain name of image delivery', 272 | value: imageDelivery.distributionDomainName 273 | }); 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /lib/origin-shield.ts: -------------------------------------------------------------------------------- 1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | // SPDX-License-Identifier: MIT-0 3 | 4 | // All regions and their ordering are taken from 5 | // https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/origin-shield.html 6 | 7 | // Regions with Regional Edge Caches 8 | const REC_REGIONS = { 9 | US_EAST_2: "us-east-2", // 1. US East (Ohio) 10 | US_EAST_1: "us-east-1", // 2. US East (N. Virginia) 11 | US_WEST_2: "us-west-2", // 3. US West (Oregon) 12 | AP_SOUTH_1: "ap-south-1", // 4. Asia Pacific (Mumbai) 13 | AP_NORTHEAST_2: "ap-northeast-2", // 5. Asia Pacific (Seoul) 14 | AP_SOUTHEAST_1: "ap-southeast-1", // 6. Asia Pacific (Singapore) 15 | AP_SOUTHEAST_2: "ap-southeast-2", // 7. Asia Pacific (Sydney) 16 | AP_NORTHEAST_1: "ap-northeast-1", // 8. Asia Pacific (Tokyo) 17 | EU_CENTRAL_1: "eu-central-1", // 9. Europe (Frankfurt) 18 | EU_WEST_1: "eu-west-1", // 10. Europe (Ireland) 19 | EU_WEST_2: "eu-west-2", // 11. Europe (London) 20 | SA_EAST_1: "sa-east-1", // 12. South America (São Paulo) 21 | }; 22 | 23 | // Other supported regions 24 | const OTHER_REGIONS = { 25 | US_WEST_1: "us-west-1", // 13. US West (N. California) 26 | AF_SOUTH_1: "af-south-1", // 14. Africa (Cape Town) 27 | AP_EAST_1: "ap-east-1", // 15. Asia Pacific (Hong Kong) 28 | CA_CENTRAL_1: "ca-central-1", // 16. Canada (Central) 29 | EU_SOUTH_1: "eu-south-1", // 17. Europe (Milan) 30 | EU_WEST_3: "eu-west-3", // 18. Europe (Paris) 31 | EU_NORTH_1: "eu-north-1", // 19. Europe (Stockholm) 32 | ME_SOUTH_1: "me-south-1", // 20. Middle East (Bahrain) 33 | }; 34 | 35 | // Region to Origin Shield mappings based on latency. 36 | // To be updated when new Regions are available or new RECs are added to CloudFront. 37 | const REGION_TO_ORIGIN_SHIELD_MAPPINGS = new Map([ 38 | [REC_REGIONS.US_EAST_2, REC_REGIONS.US_EAST_2], // 1. 39 | [REC_REGIONS.US_EAST_1, REC_REGIONS.US_EAST_1], // 2. 40 | [REC_REGIONS.US_WEST_2, REC_REGIONS.US_WEST_2], // 3. 41 | [REC_REGIONS.AP_SOUTH_1, REC_REGIONS.AP_SOUTH_1], // 4. 42 | [REC_REGIONS.AP_NORTHEAST_2, REC_REGIONS.AP_NORTHEAST_2], // 5. 43 | [REC_REGIONS.AP_SOUTHEAST_1, REC_REGIONS.AP_SOUTHEAST_1], // 6. 44 | [REC_REGIONS.AP_SOUTHEAST_2, REC_REGIONS.AP_SOUTHEAST_2], // 7. 45 | [REC_REGIONS.AP_NORTHEAST_1, REC_REGIONS.AP_NORTHEAST_1], // 8. 46 | [REC_REGIONS.EU_CENTRAL_1, REC_REGIONS.EU_CENTRAL_1], // 9. 47 | [REC_REGIONS.EU_WEST_1, REC_REGIONS.EU_WEST_1], // 10. 48 | [REC_REGIONS.EU_WEST_2, REC_REGIONS.EU_WEST_2], // 11. 49 | [REC_REGIONS.SA_EAST_1, REC_REGIONS.SA_EAST_1], // 12. 50 | 51 | [OTHER_REGIONS.US_WEST_1, REC_REGIONS.US_WEST_2], // 13. 52 | [OTHER_REGIONS.AF_SOUTH_1, REC_REGIONS.EU_WEST_1], // 14. 53 | [OTHER_REGIONS.AP_EAST_1, REC_REGIONS.AP_SOUTHEAST_1], // 15. 54 | [OTHER_REGIONS.CA_CENTRAL_1, REC_REGIONS.US_EAST_1], // 16. 55 | [OTHER_REGIONS.EU_SOUTH_1, REC_REGIONS.EU_CENTRAL_1], // 17. 56 | [OTHER_REGIONS.EU_WEST_3, REC_REGIONS.EU_WEST_2], // 18. 57 | [OTHER_REGIONS.EU_NORTH_1, REC_REGIONS.EU_WEST_2], // 19. 58 | [OTHER_REGIONS.ME_SOUTH_1, REC_REGIONS.AP_SOUTH_1], // 20. 59 | ]); 60 | 61 | export const getOriginShieldRegion = (region: string) => { 62 | const originShieldRegion = REGION_TO_ORIGIN_SHIELD_MAPPINGS.get(region); 63 | if (originShieldRegion === undefined) throw new Error(`The specified region ${region} is not supported.`); 64 | 65 | return originShieldRegion; 66 | } 67 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "image-optimization", 3 | "version": "0.1.0", 4 | "bin": { 5 | "image-optimization": "bin/image-optimization.js" 6 | }, 7 | "scripts": { 8 | "prebuild": "npm install sharp --cpu=x64 --os=linux --libc=glibc --prefix functions/image-processing", 9 | "build": "tsc", 10 | "watch": "tsc -w", 11 | "test": "jest", 12 | "cdk": "cdk" 13 | }, 14 | "devDependencies": { 15 | "@types/jest": "^29.5.11", 16 | "@types/node": "20.11.2", 17 | "@types/prettier": "2.7.3", 18 | "aws-cdk": "2.121.1", 19 | "jest": "^29.7.0", 20 | "ts-jest": "^29.1.1", 21 | "ts-node": "^10.9.2", 22 | "typescript": "~5.3.3" 23 | }, 24 | "dependencies": { 25 | "aws-cdk-lib": "2.184.0", 26 | "constructs": "^10.3.0", 27 | "source-map-support": "^0.5.21" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "module": "commonjs", 5 | "lib": [ 6 | "es2018" 7 | ], 8 | "declaration": true, 9 | "strict": true, 10 | "noImplicitAny": true, 11 | "strictNullChecks": true, 12 | "noImplicitThis": true, 13 | "alwaysStrict": true, 14 | "noUnusedLocals": false, 15 | "noUnusedParameters": false, 16 | "noImplicitReturns": true, 17 | "noFallthroughCasesInSwitch": false, 18 | "inlineSourceMap": true, 19 | "inlineSources": true, 20 | "experimentalDecorators": true, 21 | "strictPropertyInitialization": false, 22 | "typeRoots": [ 23 | "./node_modules/@types" 24 | ] 25 | }, 26 | "exclude": [ 27 | "node_modules", 28 | "cdk.out" 29 | ] 30 | } 31 | --------------------------------------------------------------------------------