Skip to main content
CESDK/CE.SDK/Cookbook

Running the Creative Engine on AWS Lambda with Node.js

Learn how to set up a scalable architecture on AWS to automatically generate images using the Creative Engine node package

If you are looking for a scalable way to implement creative automation in your application, you'll likely consider creating a micro-service to run the CE.SDK in headless mode.

Running CE.SDK’s Creative Engine on the server allows us to dynamically generate images or videos or create automation based on certain events in our app. At the same time, we can leverage the same templates we provide on the client server-side while staying consistent with the output on the client.

In this example, we’ll create a simple API on Amazon AWS that allows us to generate image variants from a CE.SDK template and store them in an S3 bucket. Concretely, we’ll create an image API implementing create and get endpoints. The former accepts POST requests with body parameters corresponding to text variables of a template and interpolates them inside an AWS lambda function and the latter allows a client to query for the creation status and URL of the rendered image.

Since it is generally good practice to background more computationally expensive tasks to keep client-facing APIs responsive and the AWS API Gateway allows a maximum timeout of 30 seconds, we’ll create two lambda functions. The first creates and queries the image resource, while the second performs the heavy lifting of initializing the Creative Engine, interpolating parameters and rendering the image.

We’ll achieve this by creating an entry in a DynamoDB images table upon the initial post request, the lambda function running the CE.SDK will then be triggered by this insertion into the images table.

Prerequisites#

Follow the steps in this AWS tutorial https://docs.aws.amazon.com/cdk/v2/guide/getting_started.html

We are going to use AWS CDK to provision the infrastructure (API Gateway, Lambda Function, S3 buckets, DynamoDB and IAM Role) we need. You should have a basic understanding of the AWS CLI and CloudFormation.

Create the AWS CDK App#

Create the AWS CDK app, for this example, we’ll create an app called “cesdk-aws-lambda”, but you’ll probably want to opt for a more descriptive name for your use case.

mdkir cesdk-aws-lambda
cd cesdk-aws-lambda
cdk init --language javascript

The main entry point for our app will be bin/cesdk_lambda_service and the stack script we’ll use to provision our resources will be located in lib/cesdk_lambda_service_stack.js.

Run the following command to test that everything is working correctly:

cdk synth

Since we haven’t added anything to our stack file this will synthesize an empty stack.

Create a CE.SDK service#

Create a CDK service file under lib/cesdk-service.js to configure our stack. We’ll create the necessary resources for the images API endpoint first; a REST API using APIGateway that integrates with a lambda handler that in turns creates and queries the DynamoDB table:

const cdk = require("aws-cdk-lib");
const { Construct } = require("constructs");
const apigateway = require("aws-cdk-lib/aws-apigateway");
const lambda = require("aws-cdk-lib/aws-lambda");
const s3 = require("aws-cdk-lib/aws-s3");
const dynamodb = require("aws-cdk-lib/aws-dynamodb");
const iam = require("aws-cdk-lib/aws-iam");
const eventsource = require("aws-cdk-lib/aws-lambda-event-sources");
class CESDKService extends Construct {
constructor(scope, id) {
super(scope, id);
const bucket = new s3.Bucket(this, "CESDKStore");
const tableName = "ImagesTable";
// lambda function running CE.SDK and rendering image
const cesdkHandler = new lambda.Function(this, "CESDKHandler", {
runtime: lambda.Runtime.NODEJS_14_X,
code: lambda.Code.fromAsset("src"),
handler: "cesdk-handler.main",
environment: {
BUCKET: bucket.bucketName,
TABLE_NAME: tableName,
TEMPLATE_URL:
"https://img.ly/showcases/cesdk/cases/headless-design/example.scene",
},
timeout: cdk.Duration.minutes(5),
memorySize: 2048,
});
// lambda function for images endpoint creating new images and returning images
const imagesHandler = new lambda.Function(this, "ImagesHandler", {
runtime: lambda.Runtime.NODEJS_14_X,
code: lambda.Code.fromAsset("src"),
handler: "images-handler.main",
environment: {
TABLE_NAME: tableName,
},
});
// Create dynamo db table for storing image objects
const imagesTable = new dynamodb.Table(this, "ImagesTable", {
tableName: "ImagesTable",
billingMode: dynamodb.BillingMode.PROVISIONED,
removalPolicy: cdk.RemovalPolicy.DESTROY,
partitionKey: { name: "id", type: dynamodb.AttributeType.STRING },
pointInTimeRecovery: true,
stream: dynamodb.StreamViewType.NEW_IMAGE,
});
// Configure lambda permissions for resources
bucket.grantReadWrite(cesdkHandler);
const imagesTablePermissionPolicy = new iam.PolicyStatement({
actions: [
"dynamodb:BatchGetItem",
"dynamodb:GetItem",
"dynamodb:PutItem",
"dynamodb:UpdateItem",
],
resources: [imagesTable.tableArn],
});
const imagesTablePermissions = new iam.Policy(
this,
`${this.appName}-ImagesTablePermissions`,
{
statements: [imagesTablePermissionPolicy],
}
);
imagesHandler.role?.attachInlinePolicy(imagesTablePermissions);
cesdkHandler.role?.attachInlinePolicy(imagesTablePermissions);
cesdkHandler.addEventSource(
new eventsource.DynamoEventSource(imagesTable, {
startingPosition: lambda.StartingPosition.LATEST,
})
);
// Set up REST api for images
const api = new apigateway.RestApi(this, "cesdk-api", {
restApiName: "CESDK Service",
description: "This service renders cesdk templates.",
});
const CESDKIntegration = new apigateway.LambdaIntegration(imagesHandler, {
requestTemplates: { "application/json": '{ "statusCode": "200" }' },
});
const imagesResource = api.root.addResource("images");
imagesResource.addMethod("POST", CESDKIntegration); // POST /images
const imageResource = imagesResource.addResource("{id}");
imageResource.addMethod("GET", CESDKIntegration); // GET /images/{id}
}
}
module.exports = { CESDKService };

This service has to be added to the stack definition in the lib/cesdk-aws-lambda-stack file.

const { Stack, Duration } = require("aws-cdk-lib");
const CESDKService = require("../lib/cesdk-service");
class CesdkAwsLambdaStack extends Stack {
/**
*
* @param {Construct} scope
* @param {string} id
* @param {StackProps=} props
*/
constructor(scope, id, props) {
super(scope, id, props);
// The code that defines your stack goes here
new CESDKService.CESDKService(this, "CESDK Service");
}
}
module.exports = { CesdkAwsLambdaStack };

Test that the app runs and synthesizes a stack cdk synth.

Create a Lambda Function to Create Images#

Create an src directory and initialize an images-handler.js file inside it.

This lambda function handles the POST and GET image requests.

In the first case, we create a unique image id and a file name derived from it, then we add a new image record in our DynamoDB table storing the parameter from the request that should be interpolated, the id and file name as well as a creation status (initially PENDING) of the image that the client can query.

The second case takes the image id from the request and simply returns the record from the database.

const AWS = require("aws-sdk");
const { v4: uuidv4 } = require("uuid");
const imagesDB = new AWS.DynamoDB.DocumentClient();
const tableName = process.env.TABLE_NAME;
exports.main = async function (event) {
const method = event.httpMethod;
const routeKey = `${event.httpMethod} ${event.resource}`;
try {
switch (routeKey) {
case "POST /images":
const id = uuidv4();
const filename = `inspirational-quote-${id}.png`;
const requestBody = JSON.parse(event.body);
const result = await imagesDB
.put({
TableName: tableName,
Item: {
id,
filename,
interpolationParams: JSON.stringify({
quote: requestBody.quote,
}),
creationStatus: "PENDING",
url: "",
},
})
.promise();
var body = { id };
break;
case "GET /images/{id}":
body = await imagesDB
.get({
TableName: tableName,
Key: {
id: event.pathParameters.id,
},
})
.promise();
break;
}
return {
statusCode: 200,
headers: {},
body: JSON.stringify(body),
};
} catch (error) {
var body = error.stack || JSON.stringify(error, null, 2);
return {
statusCode: 400,
headers: {},
body: JSON.stringify(body),
};
}
};

After saving the function, you can run cdk synth again as sanity check that we’re still synthesizing an emtpy stack.

Integrating the Creative Engine#

Now, we need another lambda function to perform the heavy lifting of running the Creative Engine and rendering the image.

We’ll install all dependencies inside src, since only code specified via code: lambda.Code.fromAsset("src") in our service file will be made available to our lambda function.

run npm init and install the @cesdk/node and node-fetch packages.

yarn add @cesdk/node node-fetch

Now add a cesdk-handler.js file in the src directory and create a lambda handler there.

We’ll initialize the CreativeEngine and load the template from the URL we’ll provide through an environment variable. Of course, in most use cases you will want to dynamically retrieve templates from file storage, but for illustration purposes we hardcode the template here.

const CreativeEngine = require("@cesdk/node");
const AWS = require("aws-sdk");
const S3 = new AWS.S3();
const imagesDB = new AWS.DynamoDB.DocumentClient();
const bucketName = process.env.BUCKET;
const templateURL = process.env.TEMPLATE_URL;
const tableName = process.env.TABLE_NAME;
const { MimeType } = CreativeEngine;
exports.main = async function (event) {
try {
const engine = await CreativeEngine.init();
// load scene from remote template file
await engine.scene.loadFromURL(templateURL);
for (const record of event.Records) {
const item = record.dynamodb.NewImage;
const filename = item.filename.S;
const id = item.id.S;
const interpolationParams = JSON.parse(item.interpolationParams.S);
// Interpolate text variable from request params
engine.variable.setString("quote", interpolationParams.quote);
const [page] = engine.block.findByType("page");
const renderedImage = await engine.block.export(page, MimeType.Png);
const imageBuffer = await renderedImage.arrayBuffer();
// Store rendered image in S3 bucket
await S3.putObject({
Bucket: bucketName,
Body: Buffer.from(imageBuffer),
ContentType: "image/png",
Key: filename,
}).promise();
// Retrieve image url
const signedUrl = await S3.getSignedUrlPromise("getObject", {
Bucket: bucketName,
Key: filename,
});
await imagesDB
.update({
TableName: tableName,
Key: { id },
AttributeUpdates: {
url: {
Action: "PUT",
Value: { S: signedUrl },
},
creationStatus: {
Action: "PUT",
Value: { S: "FINISHED" },
},
},
ReturnValues: "UPDATED_NEW",
})
.promise();
}
} catch (error) {
console.warn(error);
}
};
const CreativeEngine = require('@cesdk/node');
const AWS = require('aws-sdk');
const templateURL = process.env.TEMPLATE_URL;
const { DesignBlockType, MimeType } = CreativeEngine;
exports.main = async function(event) {
try {
const engine = await CreativeEngine.init();
// load scene from remote template file
await engine.scene.loadFromURL(templateURL);
...
} catch(error) {
console.warn(error);
}
}

Now to access the newly created image record, we need to set the cesdkHanlder as a lambda trigger for our DynamoDB table.

Inside the cesdk-service.js we’ll first define the lambda function increasing the memory available to the function as well as the timeout to allow for more computation-heavy renderings.

Then we’ll grant the required permissions to perform updates to the images table and finally, we’ll add a DynamoDB event stream to the cesdkHandler.

// lambda function running CE.SDK and rendering image
const cesdkHandler = new lambda.Function(this, 'CESDKHandler', {
runtime: lambda.Runtime.NODEJS_14_X,
code: lambda.Code.fromAsset('src'),
handler: 'cesdk-handler.main',
environment: {
BUCKET: bucket.bucketName,
TABLE_NAME: tableName,
TEMPLATE_URL:
'https://img.ly/showcases/cesdk/cases/headless-design/example.scene'
},
timeout: cdk.Duration.minutes(5),
memorySize: 2048
});
cesdkHandler.role?.attachInlinePolicy(imagesTablePermissions);
cesdkHandler.addEventSource(
new eventsource.DynamoEventSource(imagesTable, {
startingPosition: lambda.StartingPosition.LATEST
})
);

To receive only events of new image records having been added to the table, we need to add the following config option to the imagesTable definition:

stream: dynamodb.StreamViewType.NEW_IMAGEThe reason we are initializing the CE.SDK outside of our lambda handler is that this allows [resource sharing among lambda requests](https://docs.aws.amazon.com/lambda/latest/operatorguide/static-initialization.html) and decreases our response time.

Filling a Template and Generating an Image#

Now we can finally get to the meat of the matter and populate a CE.SDK template with data submitted via our API.

We receive the newly created image record via the event that is passed into the lambda handler, after interpolating the quote parameter and rendering the final image, we’ll store it in an S3 bucket and generate a signed URL to the image. Unfortunately, it is not possible to retrieve an unsigned URL directly using the S3 SDK, however, you can easily construct one from the bucket URL and the image’s file name.

Lastly, we update the image with the URL and set the creationStatus to “FINISHED”. The complete handler file now looks as follows:

const CreativeEngine = require("@cesdk/node");
const AWS = require("aws-sdk");
const S3 = new AWS.S3();
const imagesDB = new AWS.DynamoDB.DocumentClient();
const bucketName = process.env.BUCKET;
const templateURL = process.env.TEMPLATE_URL;
const tableName = process.env.TABLE_NAME;
const { MimeType } = CreativeEngine;
exports.main = async function (event) {
try {
const engine = await CreativeEngine.init();
// load scene from remote template file
await engine.scene.loadFromURL(templateURL);
for (const record of event.Records) {
const item = record.dynamodb.NewImage;
const filename = item.filename.S;
const id = item.id.S;
const interpolationParams = JSON.parse(item.interpolationParams.S);
// Interpolate text variable from request params
engine.variable.setString("quote", interpolationParams.quote);
const [page] = engine.block.findByType("page");
const renderedImage = await engine.block.export(page, MimeType.Png);
const imageBuffer = await renderedImage.arrayBuffer();
// Store rendered image in S3 bucket
await S3.putObject({
Bucket: bucketName,
Body: Buffer.from(imageBuffer),
ContentType: "image/png",
Key: filename,
}).promise();
// Retrieve image url
const signedUrl = await S3.getSignedUrlPromise("getObject", {
Bucket: bucketName,
Key: filename,
});
await imagesDB
.update({
TableName: tableName,
Key: { id },
AttributeUpdates: {
url: {
Action: "PUT",
Value: { S: signedUrl },
},
creationStatus: {
Action: "PUT",
Value: { S: "FINISHED" },
},
},
ReturnValues: "UPDATED_NEW",
})
.promise();
}
} catch (error) {
console.warn(error);
}
};

We now have a simple API endpoint that renders and stores a CE.SDK template from a set of input parameters.

It’s easy to extrapolate this setup to any number of design automation use cases. We could load the template dynamically based on the request, provide images to replace template placeholders and provide more detailed specifications such as font, size and color of the text.