Page updated Dec 7, 2023

Custom resources

Custom resources allow you to integrate any AWS service into an Amplify backend. You are responsible for ensuring that your custom resources are secure, adhere to best practices, and work with the resources that Amplify creates in your project.

Amplify (Gen 2) provides the ability to add custom AWS resources to an Amplify project using the AWS Cloud Development Kit (AWS CDK). The CDK is an open source software development framework to define your cloud application resources using familiar programming languages, such as TypeScript.

CDK can be used within an Amplify project to add custom resources and configurations beyond what Amplify supports out of the box. For example, a developer could use CDK to hook up a Redis cache, implement custom security rules, deploy containers on Fargate, or use any other AWS service.

The infrastructure defined via CDK code is deployed along with the Amplify project backend. This gives developers the simplicity of Amplify, combined with the flexibility of CDK for situations where they need more customization.

AWS CDK apps are composed of building blocks known as Constructs, which are composed together to form stacks and apps. You can learn more in the Concepts - AWS Cloud Development Kit (AWS CDK) v2 documentation.

With the Amplify Code-first DX, you can add existing or custom CDK Constructs to the backend of your Amplify project.

Adding an existing CDK Construct

The CDK comes with many existing Constructs that can be directly added to your Amplify backend. To get started, install the CDK library:

npm add --save-dev aws-cdk-lib constructs
1npm add --save-dev aws-cdk-lib constructs

Depending on the Node.js package manager you are using, you may encounter warnings where it is now unable to resolve the peer dependency version @aws-amplify/backend has on aws-cdk-lib. If you encounter a warning similar to the following, re-install the version specified in the warning text:

npm WARN Could not resolve dependency: npm WARN peer aws-cdk-lib@"~2.103.0" from @aws-amplify/backend@0.4.0 npm WARN node_modules/@aws-amplify/backend npm WARN dev @aws-amplify/backend@"^0.4.0" from the root project
1npm WARN Could not resolve dependency:
2npm WARN peer aws-cdk-lib@"~2.103.0" from @aws-amplify/backend@0.4.0
3npm WARN node_modules/@aws-amplify/backend
4npm WARN dev @aws-amplify/backend@"^0.4.0" from the root project

Using the sample warning text above, you would need to install aws-cdk-lib@2.103.0.

For example, to add an Amazon Simple Queue Service (SQS) queue and an Amazon Simple Notification Service (SNS) topic to your backend, you can add the following to your amplify/backend.ts file.

// amplify/backend.ts import * as sns from 'aws-cdk-lib/aws-sns'; import * as sqs from 'aws-cdk-lib/aws-sqs'; import { defineBackend } from '@aws-amplify/backend'; import { auth } from './auth/resource.js'; import { data } from './data/resource.js'; const backend = defineBackend({ auth, data }); const customResourceStack = backend.createStack('MyCustomResources'); new sqs.Queue(customResourceStack, 'CustomQueue'); new sns.Topic(customResourceStack, 'CustomTopic');
1// amplify/backend.ts
2import * as sns from 'aws-cdk-lib/aws-sns';
3import * as sqs from 'aws-cdk-lib/aws-sqs';
4import { defineBackend } from '@aws-amplify/backend';
5import { auth } from './auth/resource.js';
6import { data } from './data/resource.js';
7
8const backend = defineBackend({
9 auth,
10 data
11});
12
13const customResourceStack = backend.createStack('MyCustomResources');
14
15new sqs.Queue(customResourceStack, 'CustomQueue');
16new sns.Topic(customResourceStack, 'CustomTopic');

Note the use of backend.createStack(). This method instructs the backend to create a new CloudFormation Stack for your custom resources to live in. You can create multiple custom stacks and you can place multiple resources in any given stack.

Defining a CDK Construct

Constructs are the basic building blocks of AWS CDK apps. A construct represents a "cloud component" and encapsulates everything AWS CloudFormation needs to create the component. Read more

As shown above, you can use the existing CDK Constructs directly in an Amplify backend. However, you may find yourself repeating some patterns of common constructs. Custom constructs allow you to encapsulate common patterns into reusable components. This helps you implement best practices, accelerate development, and maintain consistency across applications.

A common use case is creating a custom notification construct that combines a Lambda functions with SNS and SES.

This AWS CDK construct implements a decoupled notification system using Amazon SNS and Lambda. It allows publishing notification messages to an SNS topic from one Lambda function, and processing those messages asynchronously using a separate Lambda subscribed to the topic.

The key components are:

  • An SNS topic to receive notification messages
  • A Lambda function to publish messages to the SNS topic
  • A second Lambda subscribed to the topic that processes the messages and sends emails via Amazon SES

The publisher Lambda allows publishing a message containing the email subject, body text, and recipient address. The emailer Lambda retrieves messages from the SNS topic and handles sending the actual emails.

The CustomNotifications custom CDK construct can be defined as follows:

import * as url from 'node:url'; import { Runtime } from 'aws-cdk-lib/aws-lambda'; import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs'; import * as sns from 'aws-cdk-lib/aws-sns'; import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions'; import { Construct } from 'constructs'; // message to publish export type Message = { subject: string; body: string; recipient: string; }; type CustomNotificationsProps = { /** * The source email address to use for sending emails */ sourceAddress: string; }; export class CustomNotifications extends Construct { constructor(scope: Construct, id: string, props: CustomNotificationsProps) { super(scope, id); const { sourceAddress } = props; // Create SNS topic const topic = new sns.Topic(this, 'NotificationTopic'); // Create Lambda to publish messages to SNS topic const publisher = new lambda.NodejsFunction(this, 'Publisher', { entry: url.fileURLToPath(new URL('publisher.ts', import.meta.url)), environment: { SNS_TOPIC_ARN: topic.topicArn }, runtime: Runtime.NODEJS_18_X }); // Create Lambda to process messages from SNS topic const emailer = new lambda.NodejsFunction(this, 'Emailer', { entry: url.fileURLToPath(new URL('emailer.ts', import.meta.url)), environment: { SOURCE_ADDRESS: sourceAddress }, runtime: Runtime.NODEJS_18_X }); // Subscribe emailer Lambda to SNS topic topic.addSubscription(new subscriptions.LambdaSubscription(emailer)); // Allow publisher to publish to SNS topic topic.grantPublish(publisher); } }
1import * as url from 'node:url';
2import { Runtime } from 'aws-cdk-lib/aws-lambda';
3import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs';
4import * as sns from 'aws-cdk-lib/aws-sns';
5import * as subscriptions from 'aws-cdk-lib/aws-sns-subscriptions';
6import { Construct } from 'constructs';
7
8// message to publish
9export type Message = {
10 subject: string;
11 body: string;
12 recipient: string;
13};
14
15type CustomNotificationsProps = {
16 /**
17 * The source email address to use for sending emails
18 */
19 sourceAddress: string;
20};
21
22export class CustomNotifications extends Construct {
23 constructor(scope: Construct, id: string, props: CustomNotificationsProps) {
24 super(scope, id);
25
26 const { sourceAddress } = props;
27
28 // Create SNS topic
29 const topic = new sns.Topic(this, 'NotificationTopic');
30
31 // Create Lambda to publish messages to SNS topic
32 const publisher = new lambda.NodejsFunction(this, 'Publisher', {
33 entry: url.fileURLToPath(new URL('publisher.ts', import.meta.url)),
34 environment: {
35 SNS_TOPIC_ARN: topic.topicArn
36 },
37 runtime: Runtime.NODEJS_18_X
38 });
39
40 // Create Lambda to process messages from SNS topic
41 const emailer = new lambda.NodejsFunction(this, 'Emailer', {
42 entry: url.fileURLToPath(new URL('emailer.ts', import.meta.url)),
43 environment: {
44 SOURCE_ADDRESS: sourceAddress
45 },
46 runtime: Runtime.NODEJS_18_X
47 });
48
49 // Subscribe emailer Lambda to SNS topic
50 topic.addSubscription(new subscriptions.LambdaSubscription(emailer));
51
52 // Allow publisher to publish to SNS topic
53 topic.grantPublish(publisher);
54 }
55}

The Lambda function code for the Publisher is:

// amplify/custom/CustomNotifications/publisher.ts import { PublishCommand, SNSClient } from '@aws-sdk/client-sns'; import type { Handler } from 'aws-lambda'; import type { Message } from './resource.js'; const client = new SNSClient({ region: process.env.AWS_REGION }); // define the handler that will publish messages to the SNS Topic export const handler: Handler<Message, void> = async (event) => { const { subject, body, recipient } = event; const command = new PublishCommand({ TopicArn: process.env.TOPIC_ARN, Message: JSON.stringify({ subject, body, recipient }) }); try { const response = await client.send(command); console.log('published', response); } catch (error) { console.log('failed to publish message', error); throw new Error('Failed to publish message', { cause: error }); } };
1// amplify/custom/CustomNotifications/publisher.ts
2import { PublishCommand, SNSClient } from '@aws-sdk/client-sns';
3import type { Handler } from 'aws-lambda';
4import type { Message } from './resource.js';
5
6const client = new SNSClient({ region: process.env.AWS_REGION });
7
8// define the handler that will publish messages to the SNS Topic
9export const handler: Handler<Message, void> = async (event) => {
10 const { subject, body, recipient } = event;
11 const command = new PublishCommand({
12 TopicArn: process.env.TOPIC_ARN,
13 Message: JSON.stringify({
14 subject,
15 body,
16 recipient
17 })
18 });
19 try {
20 const response = await client.send(command);
21 console.log('published', response);
22 } catch (error) {
23 console.log('failed to publish message', error);
24 throw new Error('Failed to publish message', { cause: error });
25 }
26};

The Lambda function code for the Emailer is:

// amplify/custom/CustomNotifications/emailer.ts import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses'; import type { SNSHandler } from 'aws-lambda'; import type { Message } from './resource'; const sesClient = new SESClient({ region: process.env.AWS_REGION }); // define the handler to process messages from the SNS topic and send via SES export const handler: SNSHandler = async (event) => { for (const record of event.Records) { const message: Message = JSON.parse(record.Sns.Message); // send the message via email await sendEmail(message); } }; const sendEmail = async (message: Message) => { const { recipient, subject, body } = message; const command = new SendEmailCommand({ Source: process.env.SOURCE_ADDRESS, Destination: { ToAddresses: [recipient] }, Message: { Body: { Text: { Data: body } }, Subject: { Data: subject } } }); try { const result = await sesClient.send(command); console.log(`Email sent to ${recipient}: ${result.MessageId}`); } catch (error) { console.error(`Error sending email to ${recipient}: ${error}`); throw new Error(`Failed to send email to ${recipient}`, { cause: error }); } };
1// amplify/custom/CustomNotifications/emailer.ts
2import { SESClient, SendEmailCommand } from '@aws-sdk/client-ses';
3import type { SNSHandler } from 'aws-lambda';
4import type { Message } from './resource';
5
6const sesClient = new SESClient({ region: process.env.AWS_REGION });
7
8// define the handler to process messages from the SNS topic and send via SES
9export const handler: SNSHandler = async (event) => {
10 for (const record of event.Records) {
11 const message: Message = JSON.parse(record.Sns.Message);
12
13 // send the message via email
14 await sendEmail(message);
15 }
16};
17
18const sendEmail = async (message: Message) => {
19 const { recipient, subject, body } = message;
20
21 const command = new SendEmailCommand({
22 Source: process.env.SOURCE_ADDRESS,
23 Destination: {
24 ToAddresses: [recipient]
25 },
26 Message: {
27 Body: {
28 Text: { Data: body }
29 },
30 Subject: { Data: subject }
31 }
32 });
33
34 try {
35 const result = await sesClient.send(command);
36 console.log(`Email sent to ${recipient}: ${result.MessageId}`);
37 } catch (error) {
38 console.error(`Error sending email to ${recipient}: ${error}`);
39 throw new Error(`Failed to send email to ${recipient}`, { cause: error });
40 }
41};

The CustomNotifications CDK construct can then be added to the Amplify backend one or more times, with different properties for each instance.

// amplify/backend.ts import { defineBackend } from '@aws-amplify/backend'; import { auth } from './auth/resource.js'; import { data } from './data/resource.js'; import { CustomNotifications } from './custom/CustomNotifications/resource'; const backend = defineBackend({ auth, data }); new CustomNotifications( backend.createStack('CustomNotifications'), 'CustomNotifications', { sourceAddress: 'sender@example.com' } );
1// amplify/backend.ts
2import { defineBackend } from '@aws-amplify/backend';
3import { auth } from './auth/resource.js';
4import { data } from './data/resource.js';
5import { CustomNotifications } from './custom/CustomNotifications/resource';
6
7const backend = defineBackend({
8 auth,
9 data
10});
11
12new CustomNotifications(
13 backend.createStack('CustomNotifications'),
14 'CustomNotifications',
15 { sourceAddress: 'sender@example.com' }
16);

Community CDK Resources

The Construct Hub is a community-driven catalog of reusable infrastructure components. It is a place for developers to discover and share reusable patterns for AWS CDK, maintained by AWS.

In addition, the Example projects using the AWS CDK repository contains a number of examples of reusable CDK constructs.

These resources can be leveraged to create custom CDK constructs that can be used in your Amplify project.