diff --git a/s3-lambda-resizing-dotnet/.gitignore b/s3-lambda-resizing-dotnet/.gitignore new file mode 100644 index 000000000..4507fc7d2 --- /dev/null +++ b/s3-lambda-resizing-dotnet/.gitignore @@ -0,0 +1,402 @@ +# Created by https://www.toptal.com/developers/gitignore/api/csharp +# Edit at https://www.toptal.com/developers/gitignore?templates=csharp + +### Csharp ### +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml + +# End of https://www.toptal.com/developers/gitignore/api/csharp \ No newline at end of file diff --git a/s3-lambda-resizing-dotnet/ImageResize.sln b/s3-lambda-resizing-dotnet/ImageResize.sln new file mode 100644 index 000000000..ffcd6c86c --- /dev/null +++ b/s3-lambda-resizing-dotnet/ImageResize.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.6.33815.320 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ImageResize", "ImageResize\ImageResize.csproj", "{FADD6CE5-EDF7-4BFE-B8F5-E84CD788D6DE}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {FADD6CE5-EDF7-4BFE-B8F5-E84CD788D6DE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FADD6CE5-EDF7-4BFE-B8F5-E84CD788D6DE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FADD6CE5-EDF7-4BFE-B8F5-E84CD788D6DE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FADD6CE5-EDF7-4BFE-B8F5-E84CD788D6DE}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {3B666636-4024-4B31-80A6-CB25C208D7F9} + EndGlobalSection +EndGlobal diff --git a/s3-lambda-resizing-dotnet/ImageResize/Function.cs b/s3-lambda-resizing-dotnet/ImageResize/Function.cs new file mode 100644 index 000000000..5724c587f --- /dev/null +++ b/s3-lambda-resizing-dotnet/ImageResize/Function.cs @@ -0,0 +1,128 @@ +using System.IO; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.PixelFormats; +using Amazon.S3.Model; +using SixLabors.ImageSharp.Formats.Jpeg; +using Amazon.Lambda.Core; +using Amazon.Lambda.S3Events; +using Amazon.S3; +using Amazon.S3.Util; + +// Assembly attribute to enable the Lambda function's JSON input to be converted into a .NET class. +[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))] + +namespace ImageResize; + +public class Function +{ + IAmazonS3 S3Client { get; set; } + + + /// + /// Default constructor. This constructor is used by Lambda to construct the instance. When invoked in a Lambda environment + /// the AWS credentials will come from the IAM role associated with the function and the AWS region will be set to the + /// region the Lambda function is executed in. + /// + public Function() + { + S3Client = new AmazonS3Client(); + } + + /// + /// Constructs an instance with a preconfigured S3 client. This can be used for testing outside of the Lambda environment. + /// + /// + public Function(IAmazonS3 s3Client) + { + this.S3Client = s3Client; + } + + public async Task FunctionHandler(S3Event evnt, ILambdaContext context) + { + string[] fileExtensions = new string[] { ".jpg", ".jpeg" }; + var s3Event = evnt.Records?[0].S3; + if (s3Event == null) + { + return null; + } + + try + { + foreach (var record in evnt.Records) + { + LambdaLogger.Log("----> File: " + record.S3.Object.Key); + if (!fileExtensions.Contains(Path.GetExtension(record.S3.Object.Key).ToLower())) + { + LambdaLogger.Log("File Extension is not supported - " + s3Event.Object.Key); + continue; + } + + string suffix = Path.GetExtension(record.S3.Object.Key).ToLower(); + Stream imageStream = new MemoryStream(); + using (var objectResponse = await S3Client.GetObjectAsync(record.S3.Bucket.Name, record.S3.Object.Key)) + { + using (Stream responseStream = objectResponse.ResponseStream) + { + using (var image = Image.Load(responseStream)) + { + // Create B&W thumbnail + image.Mutate(ctx => ctx.Grayscale().Resize(200, 200)); + image.Save(imageStream, new JpegEncoder()); + imageStream.Seek(0, SeekOrigin.Begin); + } + } + } + + // Creating a new S3 ObjectKey for the thumbnails + string thumbnailObjectKey = null; + int endSlash = record.S3.Object.Key.ToLower().LastIndexOf("/"); + if (endSlash > 0) + { + string S3ObjectName = record.S3.Object.Key.ToLower().Substring(endSlash + 1); + int beginSlash = 0; + if (endSlash > 0) + { + beginSlash = record.S3.Object.Key.ToLower().Substring(0, endSlash - 1).LastIndexOf("/"); + if (beginSlash > 0) + { + thumbnailObjectKey = + record.S3.Object.Key.ToLower().Substring(0, beginSlash) + + "thumbnails/" + + S3ObjectName; + } + else + { + thumbnailObjectKey = "thumbnails/" + S3ObjectName; + } + } + } + else + { + thumbnailObjectKey = "thumbnails/" + record.S3.Object.Key.ToLower(); + } + + LambdaLogger.Log("----> Thumbnail file Key: " + thumbnailObjectKey); + var destinationBucket = Environment.GetEnvironmentVariable("DESTINATION_BUCKET_NAME"); + await S3Client.PutObjectAsync(new PutObjectRequest + { + BucketName = destinationBucket, + Key = thumbnailObjectKey, + InputStream = imageStream + }); + } + + LambdaLogger.Log("Processed " + evnt.Records.Count.ToString()); + + return null; + } + catch (Exception e) + { + context.Logger.LogLine($"Error getting object {s3Event.Object.Key} from bucket {s3Event.Bucket.Name}"); + context.Logger.LogLine($"Make sure they exist and your bucket is in the same region as this function"); + context.Logger.LogLine(e.Message); + context.Logger.LogLine(e.StackTrace); + throw; + } + } +} \ No newline at end of file diff --git a/s3-lambda-resizing-dotnet/ImageResize/ImageResize.csproj b/s3-lambda-resizing-dotnet/ImageResize/ImageResize.csproj new file mode 100644 index 000000000..9dd47ae3e --- /dev/null +++ b/s3-lambda-resizing-dotnet/ImageResize/ImageResize.csproj @@ -0,0 +1,20 @@ + + + net6.0 + enable + enable + true + Lambda + + true + + true + + + + + + + + + \ No newline at end of file diff --git a/s3-lambda-resizing-dotnet/ImageResize/Properties/launchSettings.json b/s3-lambda-resizing-dotnet/ImageResize/Properties/launchSettings.json new file mode 100644 index 000000000..e6144efbb --- /dev/null +++ b/s3-lambda-resizing-dotnet/ImageResize/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Mock Lambda Test Tool": { + "commandName": "Executable", + "commandLineArgs": "--port 5050", + "workingDirectory": ".\\bin\\$(Configuration)\\net6.0", + "executablePath": "%USERPROFILE%\\.dotnet\\tools\\dotnet-lambda-test-tool-6.0.exe" + } + } +} \ No newline at end of file diff --git a/s3-lambda-resizing-dotnet/ImageResize/aws-lambda-tools-defaults.json b/s3-lambda-resizing-dotnet/ImageResize/aws-lambda-tools-defaults.json new file mode 100644 index 000000000..6ce1ffef1 --- /dev/null +++ b/s3-lambda-resizing-dotnet/ImageResize/aws-lambda-tools-defaults.json @@ -0,0 +1,26 @@ + +{ + "Information" : [ + "This file provides default values for the deployment wizard inside Visual Studio and the AWS Lambda commands added to the .NET Core CLI.", + "To learn more about the Lambda commands with the .NET Core CLI execute the following command at the command line in the project root directory.", + "dotnet lambda help", + "All the command line options for the Lambda command can be specified in this file." + ], + "profile" : "default", + "region" : "us-east-1", + "configuration" : "Release", + "function-runtime" : "dotnet6", + "function-memory-size" : 256, + "function-timeout" : 30, + "function-handler" : "ImageResize::ImageResize.Function::FunctionHandler", + "framework" : "net6.0", + "function-name" : "ImageResize", + "package-type" : "Zip", + "function-role" : "arn:aws:iam::595982400875:role/ImageResizeLambdaRole", + "function-architecture" : "x86_64", + "function-subnets" : "", + "function-security-groups" : "", + "tracing-mode" : "PassThrough", + "environment-variables" : "", + "image-tag" : "" +} \ No newline at end of file diff --git a/s3-lambda-resizing-dotnet/README.md b/s3-lambda-resizing-dotnet/README.md new file mode 100644 index 000000000..b267dd1c9 --- /dev/null +++ b/s3-lambda-resizing-dotnet/README.md @@ -0,0 +1,77 @@ +# AWS Amazon S3 to AWS Lambda - Create a Lambda function that resizes images uploaded to S3 + +The SAM template deploys a .NET 6 Lambda function, an S3 bucket and the IAM resources required to run the application. A Lambda function consumes ObjectCreated events from an Amazon S3 bucket. The function code checks the uploaded file is an image and creates a thumbnail version of the image in the same bucket. + +Learn more about this pattern at Serverless Land Patterns: [https://serverlessland.com/patterns/s3-lambda-dotnet](https://serverlessland.com/patterns/s3-lambda-dotnet) + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Requirements + +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +* [AWS Serverless Application Model](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) (AWS SAM) installed +* [.Net 6.0](https://dotnet.microsoft.com/en-us/download/dotnet/6.0) +* [Docker](https://docs.docker.com/get-docker/) installed and running + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + ``` + git clone https://github.com/aws-samples/serverless-patterns + ``` +1. Change directory to the pattern directory: + ``` + cd s3-lambda-dotnet + ``` +1. From the command line, use AWS SAM to build and deploy the AWS resources for the pattern as specified in the template.yml file: + ``` + sam build + sam deploy --guided + ``` +1. During the prompts: + * Enter a stack name + * Enter the desired AWS Region + * Allow SAM CLI to create IAM roles with the required permissions. + + Once you have run `sam deploy -guided` mode once and saved arguments to a configuration file (samconfig.toml), you can use `sam deploy` in future to use these defaults. + +1. Note the outputs from the SAM deployment process. These contain the resource names and/or ARNs which are used for testing. + +## How it works + +* Use the AWS CLI upload an image to S3 +* If the object is a .jpeg in the source bucket, the code creates a thumbnail and saves it to the destination bucket in a new folder, /thumbnails. +* The code assumes that the destination bucket exists and is defined in the `template.yaml` file + +============================================== + +## Testing + +Run the following S3 CLI command to upload an image to the S3 bucket. Note, you must edit the {SourceBucketName} placeholder with the name of the S3 Bucket. This is provided in the stack outputs. + +```bash +aws s3 cp './images/example.jpeg' s3://{BucketName}/example.jpeg +``` + +Run the following command to check that a new thumbnails folder has been created with a new version of the image. + +```bash +aws s3 ls s3://{BucketName}/thumbnails +``` + +## Cleanup + +1. Delete the stack + ```bash + aws cloudformation delete-stack --stack-name STACK_NAME + ``` +1. Confirm the stack has been deleted + ```bash + aws cloudformation list-stacks --query "StackSummaries[?contains(StackName,'STACK_NAME')].StackStatus" + ``` +---- +Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/s3-lambda-resizing-dotnet/event.json b/s3-lambda-resizing-dotnet/event.json new file mode 100644 index 000000000..f8486fe55 --- /dev/null +++ b/s3-lambda-resizing-dotnet/event.json @@ -0,0 +1,38 @@ +{ + "Records": [ + { + "eventVersion": "2.1", + "eventSource": "aws:s3", + "awsRegion": "us-east-1", + "eventTime": "2024-07-03T19:37:27.192Z", + "eventName": "ObjectCreated:Put", + "userIdentity": { + "principalId": "AWS:AIDAINPONIXQXHT3IKHL2" + }, + "requestParameters": { + "sourceIPAddress": "205.255.255.255" + }, + "responseElements": { + "x-amz-request-id": "D82B88E5F771F645", + "x-amz-id-2": "vlR7PnpV2Ce81l0PRw6jlUpck7Jo5ZsQjryTjKlc5aLWGVHPZLj5NeC6qMa0emYBDXOo6QBU0Wo=" + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "828aa6fc-f7b5-4305-8584-487c791949c1", + "bucket": { + "name": "", + "ownerIdentity": { + "principalId": "A3I5XTEXAMAI3E" + }, + "arn": "arn:aws:s3:::lambda-artifacts-deafc19498e3f2df" + }, + "object": { + "key": "", + "size": 1305107, + "eTag": "b21b84d653bb07b05b1e6b33684dc11b", + "sequencer": "0C0F6F405D6ED209E1" + } + } + } + ] +} diff --git a/s3-lambda-resizing-dotnet/images/example.jpeg b/s3-lambda-resizing-dotnet/images/example.jpeg new file mode 100644 index 000000000..031e7e132 Binary files /dev/null and b/s3-lambda-resizing-dotnet/images/example.jpeg differ diff --git a/s3-lambda-resizing-dotnet/template.yaml b/s3-lambda-resizing-dotnet/template.yaml new file mode 100644 index 000000000..93e4c2d02 --- /dev/null +++ b/s3-lambda-resizing-dotnet/template.yaml @@ -0,0 +1,70 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: > + (uksb-1tthgi812) (tag:s3-lambda-dotnet) + .NET Image resizing service + +# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst +Globals: + Function: + Tracing: PassThrough + Timeout: 10 + Runtime: dotnet6 + +Parameters: + SourceBucketName: + Type: String + # Pass your bucket name dynamically or change it here + Default: my-source-bucket-name + DestinationBucketName: + Type: String + # Pass your bucket name dynamically or change it here + Default: my-destination-bucket-name + +Resources: + SourceBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: !Ref SourceBucketName + + DestinationBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: !Ref DestinationBucketName + + ResizerFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: ImageResize/ + Handler: ImageResize::ImageResize.Function::FunctionHandler + MemorySize: 2048 + Environment: + Variables: + DESTINATION_BUCKET_NAME: !Ref DestinationBucketName + Policies: + - S3ReadPolicy: + BucketName: !Ref SourceBucketName + - S3CrudPolicy: + BucketName: !Ref DestinationBucketName + Events: + FileUpload: + Type: S3 + Properties: + Bucket: !Ref SourceBucket + Events: s3:ObjectCreated:* + Filter: + S3Key: + Rules: + - Name: suffix + Value: '.jpeg' + +Outputs: + SourceBucketName: + Value: !Ref SourceBucketName + Description: S3 Bucket for object storage + DestinationBucketName: + Value: !Ref DestinationBucketName + Description: S3 destination Bucket for object storage + FunctionArn: + Value: !Ref ResizerFunction + Description: ResizerFunction function Arn \ No newline at end of file diff --git a/s3-lambda-resizing-node/README.md b/s3-lambda-resizing-node/README.md new file mode 100644 index 000000000..8197d478d --- /dev/null +++ b/s3-lambda-resizing-node/README.md @@ -0,0 +1,75 @@ +# AWS Amazon S3 to AWS Lambda - Create a Lambda function that resizes images uploaded to S3 + +The SAM template deploys a Lambda function, an S3 bucket and the IAM resources required to run the application. A Lambda function consumes ObjectCreated events from an Amazon S3 bucket. The Lambda code checks the uploaded file is an image and creates a thumbnail version of the image in the same bucket. + +Learn more about this pattern at Serverless Land Patterns: [https://serverlessland.com/patterns/s3-lambda](https://serverlessland.com/patterns/s3-lambda) + +Important: this application uses various AWS services and there are costs associated with these services after the Free Tier usage - please see the [AWS Pricing page](https://aws.amazon.com/pricing/) for details. You are responsible for any AWS costs incurred. No warranty is implied in this example. + +## Requirements + +* [Create an AWS account](https://portal.aws.amazon.com/gp/aws/developer/registration/index.html) if you do not already have one and log in. The IAM user that you use must have sufficient permissions to make necessary AWS service calls and manage AWS resources. +* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured +* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git) +* [AWS Serverless Application Model](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) (AWS SAM) installed + +## Deployment Instructions + +1. Create a new directory, navigate to that directory in a terminal and clone the GitHub repository: + ``` + git clone https://github.com/aws-samples/serverless-patterns + ``` +1. Change directory to the pattern directory: + ``` + cd s3-lambda + ``` +1. From the command line, use AWS SAM to build and deploy the AWS resources for the pattern as specified in the template.yml file: + ``` + sam build + sam deploy --guided + ``` +1. During the prompts: + * Enter a stack name + * Enter the desired AWS Region + * Allow SAM CLI to create IAM roles with the required permissions. + + Once you have run `sam deploy -guided` mode once and saved arguments to a configuration file (samconfig.toml), you can use `sam deploy` in future to use these defaults. + +1. Note the outputs from the SAM deployment process. These contain the resource names and/or ARNs which are used for testing. + +## How it works + +* Use the AWS CLI upload an image to S3 +* If the object is a .jpg or a .png, the code creates a thumbnail and saves it to the target bucket. +* The code assumes that the destination bucket exists and its name is a concatenation of the source bucket name followed by the string -resized + +============================================== + +## Testing + +Run the following S3 CLI command to upload an image to the S3 bucket. Note, you must edit the {SourceBucketName} placeholder with the name of the S3 Bucket. This is provided in the stack outputs. + +```bash +aws s3 cp './events/example.jpg' s3://{SourceBucketName} +``` + +Run the following command to check that a new version of the image has been created in the destination bucket. + +```bash +aws s3 ls s3://{DestinationBucketName} +``` + +## Cleanup + +1. Delete the stack + ```bash + aws cloudformation delete-stack --stack-name STACK_NAME + ``` +1. Confirm the stack has been deleted + ```bash + aws cloudformation list-stacks --query "StackSummaries[?contains(StackName,'STACK_NAME')].StackStatus" + ``` +---- +Copyright 2021 Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: MIT-0 diff --git a/s3-lambda-resizing-node/events/event.json b/s3-lambda-resizing-node/events/event.json new file mode 100644 index 000000000..f8486fe55 --- /dev/null +++ b/s3-lambda-resizing-node/events/event.json @@ -0,0 +1,38 @@ +{ + "Records": [ + { + "eventVersion": "2.1", + "eventSource": "aws:s3", + "awsRegion": "us-east-1", + "eventTime": "2024-07-03T19:37:27.192Z", + "eventName": "ObjectCreated:Put", + "userIdentity": { + "principalId": "AWS:AIDAINPONIXQXHT3IKHL2" + }, + "requestParameters": { + "sourceIPAddress": "205.255.255.255" + }, + "responseElements": { + "x-amz-request-id": "D82B88E5F771F645", + "x-amz-id-2": "vlR7PnpV2Ce81l0PRw6jlUpck7Jo5ZsQjryTjKlc5aLWGVHPZLj5NeC6qMa0emYBDXOo6QBU0Wo=" + }, + "s3": { + "s3SchemaVersion": "1.0", + "configurationId": "828aa6fc-f7b5-4305-8584-487c791949c1", + "bucket": { + "name": "", + "ownerIdentity": { + "principalId": "A3I5XTEXAMAI3E" + }, + "arn": "arn:aws:s3:::lambda-artifacts-deafc19498e3f2df" + }, + "object": { + "key": "", + "size": 1305107, + "eTag": "b21b84d653bb07b05b1e6b33684dc11b", + "sequencer": "0C0F6F405D6ED209E1" + } + } + } + ] +} diff --git a/s3-lambda-resizing-node/events/example.jpg b/s3-lambda-resizing-node/events/example.jpg new file mode 100644 index 000000000..5a5e031e2 Binary files /dev/null and b/s3-lambda-resizing-node/events/example.jpg differ diff --git a/s3-lambda-resizing-node/events/inputFile.txt b/s3-lambda-resizing-node/events/inputFile.txt new file mode 100644 index 000000000..8dde042f4 --- /dev/null +++ b/s3-lambda-resizing-node/events/inputFile.txt @@ -0,0 +1,3 @@ +{ + "IsHelloWorldExample": "Hello" +} \ No newline at end of file diff --git a/s3-lambda-resizing-node/src/app.js b/s3-lambda-resizing-node/src/app.js new file mode 100644 index 000000000..e5420f479 --- /dev/null +++ b/s3-lambda-resizing-node/src/app.js @@ -0,0 +1,53 @@ +/*! Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: MIT-0 + */ + +const { GetObjectCommand, PutObjectCommand, S3Client } = require("@aws-sdk/client-s3"); +const s3 = new S3Client({ region: process.env.AWS_REGION }); + +const gm = require('gm') +const NEW_SIZE_PX = 480 + +// 1. Load the image from S3. +// 2. Transform the image. +// 3. Save the new image in another S3 bucket. + +exports.handler = async (event) => { + // console.log(JSON.stringify(event, null, 2)) + const Key = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, " ")) + + + // Read the object from S3 + const commandGet = new GetObjectCommand({ + Bucket: event.Records[0].s3.bucket.name, + Key + }); + const s3Object = await s3.send(commandGet); + + // Resize the image + const data = await resizeImage(s3Object.Body) + + // Write to S3 + const commandPut = new PutObjectCommand({ + Bucket: process.env.DESTINATION_BUCKETNAME, + Key, + ContentType: 'image/jpeg', + Body: data, + }); + const result = await s3.send(commandPut); + console.log('Write result: ', result) +} + + + +// Resize - takes and returns a image buffer +const resizeImage = async (buffer) => { + return new Promise((resolve, reject) => { + gm(buffer) + .resize(NEW_SIZE_PX, NEW_SIZE_PX) + .toBuffer('jpg', function (err, data) { + if (err) return reject(err) + resolve(data) + }) + }) +} \ No newline at end of file diff --git a/s3-lambda-resizing-node/src/package.json b/s3-lambda-resizing-node/src/package.json new file mode 100644 index 000000000..342766c18 --- /dev/null +++ b/s3-lambda-resizing-node/src/package.json @@ -0,0 +1,16 @@ +{ + "name": "resizer", + "version": "1.0.0", + "description": "", + "main": "app.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "James Beswick", + "license": "MIT-0", + "dependencies": { + "gm": "^1.25.0", + "@aws-sdk/client-s3": "^3.32.0" + } +} \ No newline at end of file diff --git a/s3-lambda-resizing-node/template.yaml b/s3-lambda-resizing-node/template.yaml new file mode 100644 index 000000000..b47762c9f --- /dev/null +++ b/s3-lambda-resizing-node/template.yaml @@ -0,0 +1,60 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Image resizing service (uksb-1tthgi812) (tag:s3-lambda) + +Parameters: + SourceBucketName: + Type: String + DestinationBucketName: + Type: String + +Resources: + ## S3 bucket + SourceBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: !Ref SourceBucketName + DestinationBucket: + Type: AWS::S3::Bucket + Properties: + BucketName: !Ref DestinationBucketName + + ## Lambda function + ResizerFunction: + Type: AWS::Serverless::Function + Properties: + CodeUri: src/ + Handler: app.handler + Runtime: nodejs18.x + MemorySize: 2048 + Layers: + - !Sub 'arn:aws:lambda:${AWS::Region}:175033217214:layer:graphicsmagick:2' + Policies: + - S3ReadPolicy: + BucketName: !Ref SourceBucketName + - S3CrudPolicy: + BucketName: !Ref DestinationBucketName + Environment: + Variables: + DESTINATION_BUCKETNAME: !Ref DestinationBucketName + Events: + FileUpload: + Type: S3 + Properties: + Bucket: !Ref SourceBucket + Events: s3:ObjectCreated:* + Filter: + S3Key: + Rules: + - Name: suffix + Value: '.jpg' +Outputs: + SourceBucketName: + Value: !Ref SourceBucketName + Description: S3 Bucket for object storage + DestinationBucketName: + Value: !Ref DestinationBucketName + Description: S3 destination Bucket for object storage + FunctionArn: + Value: !Ref ResizerFunction + Description: ResizerFunction function Arn \ No newline at end of file