SNS to AppSync subscription

How to send a subscription event to clients for an SNS message

Author's image
Tamás Sallai
1 min

SNS is a pub-sub service that can fan-out notifications in AWS. Many services can publish messages to it, and you can add implement logic in your own applications to do the same. Then SNS manages subscriptions and notifies the interested parties of these events.

In a different part of a cloud-based architecture, clients can subscribe to the real-time channel AppSync provides. Then when something interesting happens in the API, they can get an event via that channel.

In this article, we'll look into how to integrate the two worlds: whenever a message is published to an SNS topic, AppSync will send a notification to interested clients.

Overview

Let's start with the basics! There is an SNS topic that some process publishes messages. This can be EC2 lifecycle events from EventBridge, billing data from a separate system, DynamoDB changes via streams, or anything else.

SNS topic

Then on the other end, AppSync allows clients to subscribe via GraphQL requests. A subscription event is triggered by a GraphQL mutation, so we need one for that.

type Mutation {
	receivedNotification(message: String!): String
}

type Subscription {
	message: String
		@aws_subscribe(mutations: ["receivedNotification"])
}

The SNS message needs to call this mutation to trigger the subscription event. Since that is not possible directly, we'll implement a Lambda function as a middleman.

Implementation

First, trigger the Lambda from the SNS topic:

resource "aws_lambda_permission" "sns" {
	action        = "lambda:InvokeFunction"
	function_name = aws_lambda_function.lambda.arn
	principal     = "sns.amazonaws.com"
	source_arn = aws_sns_topic.topic.arn
}

resource "aws_sns_topic_subscription" "lambda" {
	topic_arn = aws_sns_topic.topic.arn
	protocol = "lambda"
	endpoint = aws_lambda_function.lambda.arn
}

Also, define the Lambda function:

resource "aws_lambda_function" "lambda" {
	function_name = "${random_id.id.hex}-function"

	filename         = data.archive_file.lambda_zip.output_path
	source_code_hash = data.archive_file.lambda_zip.output_base64sha256
	environment {
		variables = {
			APIURL = aws_appsync_graphql_api.appsync.uris["GRAPHQL"]
			apiRegion = data.aws_arn.appsync.region
			NODE_NO_WARNINGS = "1"
		}
	}

	timeout = 30
	handler = "index.handler"
	runtime = "nodejs18.x"
	role    = aws_iam_role.lambda_exec.arn
}

The function needs to know the AppSync API and also its region.

Then the function code reads the records it receives and calls the AppSync API:

export const handler = async (event) => {
	const {APIURL, apiRegion} = process.env;
	return Promise.all(event.Records.map(async (record) => {
		const message = record.Sns.Message;
		const body = {
			query: `mutation trigger($message: String!) {
	receivedNotification(message: $message)
}`,
			operationName: "trigger",
			variables: {message},
			authMode: "AWS_IAM",
		};
		// ...sign and call the API
	});
};

Testing

To test the implementation, first subscribe with a client to the AppSync subscription:

Subscribe to the AppSync query

Second, publish a message to the SNS topic:

Publish to the topic

An event is received via the AppSync WebSocket channel:

A subscription event is sent to the clients
December 15, 2022
In this article