AWS Lambda@edge with S3 when calling image

Jason Choi
3 min readApr 30, 2021

Conclusion only

Your time is important. So, I will only write the conclusion here.

Problem

Getting image from S3 Cloudfront took too long, so it harmed user experience.

Reason

Images displayed on my app were uploaded by users, and the sizes of images were often larger than expected: more than 1MB, and some images were larger than 5MB.

Three alternatives

  1. Resize image when uploading: Original image is lost.
  2. Save resized images to AWS S3, along with original images: costs more storages
  3. Resize image when image URL is called using AWS Lambda: the way I will show now

Flow

  1. Configure IAM role, since Lambda accesses to S3 via authorization by IAM.
  2. Initialize Lambda function.
  3. Write resizing code.
  4. Apply the code to Lambda function initialized in stage 2.

Manual

Refer to

(page is written in Korean)

Problem that can occur

Image can be rotated after resizing; then use this way.

2.1 .withMetadata()

S3.getObject({Bucket: BUCKET, Key: originalKey}).promise()
.then(data => Sharp(data.Body)
.resize(width, height)
.withMetadata() // add this line here
.toBuffer()
)

2.2 .rotate()

Sharp(data.Body)
.rotate()
.resize(width, height)
.toBuffer()

https://velog.io/%40nawnoes/AWS-lambda-nodejs-sharp-%EC%9D%B4%EB%AF%B8%EC%A7%80-%EB%A6%AC%EC%82%AC%EC%9D%B4%EC%A6%88-%EC%8B%9C-%EC%82%AC%EC%A7%84-%ED%9A%8C%EC%A0%84%ED%95%98%EB%8A%94-%ED%98%84%EC%83%81

My Code on Cloud9

‘use strict’;const querystring = require(‘querystring’); // Don’t install.
const AWS = require(‘aws-sdk’); // Don’t install.
const Sharp = require(‘sharp’);
const S3 = new AWS.S3({
// region: ‘Asia Pacific (Seoul) ap-northeast-2’
});
const BUCKET = ‘my bucket name';
exports.handler = async (event, context, callback) => {
const { request, response } = event.Records[0].cf;
// Parameters are w, h, f, q and indicate width, height, format and quality.
const params = querystring.parse(request.querystring);
// Required width or height value.
if (!params.w && !params.h) {
return callback(null, response);
}


// Extract name and format.
const { uri } = request;

const [, imageName, extension] = uri.match(/\/?(.*)\.(.*)/);

// Exception ‘.gif’ image.
if (extension === ‘gif’) {
console.log(‘GIF image requested!’);
return callback(null, response);
}
// Init variables
let width;
let height;
let format;
let quality; // Sharp는 이미지 포맷에 따라서 품질(quality)의 기본값이 다릅니다.
let s3Object;
let resizedImage;
// Init sizes.
width = parseInt(params.w, 10) ? parseInt(params.w, 10) : null;
height = parseInt(params.h, 10) ? parseInt(params.h, 10) : null;
// Init quality.
if (parseInt(params.q, 10)) {
quality = parseInt(params.q, 10);
}
// Init format.
format = params.f ? params.f : extension;
format = format === ‘jpg’ ? ‘jpeg’ : format;
// For AWS CloudWatch.
console.log(`params: ${JSON.stringify(params)}`); // Cannot convert object to primitive value.
console.log(`name: ${imageName}.${extension}`); // Favicon error, if name is `favicon.ico`.
try {
s3Object = await S3.getObject({
Bucket: BUCKET,
Key: decodeURI(imageName + ‘.’ + extension)
}).promise();
} catch (error) {
console.log(‘S3.getObject: ‘, error);
return callback(error);
}
try {
resizedImage = await Sharp(s3Object.Body)
.resize(width, height)
.toFormat(format, {
quality
})
.withMetadata() // add this line here to prevent image rotating
.toBuffer();
} catch (error) {
console.log(‘Sharp: ‘, error);
return callback(error);
}
const resizedImageByteLength = Buffer.byteLength(resizedImage, ‘base64’);
console.log(‘byteLength: ‘, resizedImageByteLength);
// `response.body`가 변경된 경우 1MB까지만 허용됩니다.
if (resizedImageByteLength >= 1 * 1024 * 1024) {
return callback(null, response);
}
response.status = 200;
response.body = resizedImage.toString(‘base64’);
response.bodyEncoding = ‘base64’;
response.headers[‘content-type’] = [
{
key: ‘Content-Type’,
value: `image/${format}`
}
];
return callback(null, response);
};

--

--