Building DevSecOps solutions using AWS, Terraform and Kubernetes

Lambda SNS SQS Example

  • 20th September 2022

Introduction

A reasonably common pattern in AWS is subscribing your SQS queue to your SNS topic.

It is possible to directly use a Lambda to add to the SQS queue, but we will be using SNS for the other benefits it brings. For example, SNS could allow us to send out an immediate acknowledgement e-mail using SES, while also adding the message to a queue for processing later.

During this example exercise we will look at creating a simple Lambda, publishing the data to SNS and then into SQS for later processing.

Lambda to SNS to SQS

Example Golang Lambda

First let’s create our Lambda that will publish messages to an SNS topic.

The SNS topic will be defined later as an environment variable call TopicArn.

The SNS message will be definted as a Json input event while calling the lambda, for example:

{
  "SnsMessage": "My demo SNS message"
}

To get started create a file called main.go:

package main

import (
	"context"

	"github.com/aws/aws-lambda-go/lambda"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/sns"

	"log"
	"os"
)

type JsonInput struct {
	SnsMessage string `json:"SnsMessage"`
}

func handler(ctx context.Context, event JsonInput) {
	sess := session.New(&aws.Config{})
	svc := sns.New(sess)

	log.Println("Attempting to publish SNS message:", event.SnsMessage)
	result, err := svc.Publish(&sns.PublishInput{
		Message:  aws.String(event.SnsMessage),
		TopicArn: aws.String(os.Getenv("TopicArn")),
	})
	if err != nil {
		log.Println("Unable to publish to SNS topic", err.Error())
		log.Fatal(err.Error())
	}

	log.Println("SNS Message published. Message ID:", *result.MessageId)
}

func main() {
	lambda.Start(handler)
}

Then we will run these CLI commands to create our .zip file:

# Initialise our golang project
go mod init example.com/demo
go get github.com/aws/aws-lambda-go/lambda
go get github.com/aws/aws-sdk-go/aws/session
go get github.com/aws/aws-sdk-go/service/sns

# If you are on a mac, let Go know you want linux
export GOOS=linux
export GOARCH=amd64
export CGO_ENABLED=0

# Build our lambda
go build -o hello

# Zip up our binary ready for terraform
zip -r function.zip hello

If this worked then you should now have a file created called function.zip that can be used to create your lambda in the next step.

Terraform

Now for the infrastructure! This terraform snippet will configure the Lambda, SNS and SQS.

Create a file called main.tf and add this content:

terraform {
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 3.0"
    }
  }
}

# Configure the AWS Provider
provider "aws" {
  region = "eu-west-1"
}

# Create out lambda, using our locally sourced zip file
resource "aws_lambda_function" "demo_lambda_to_sns" {
  function_name = "demo-lambda-to-sns"
  role          = aws_iam_role.demo_lambda_role.arn
  package_type  = "Zip"
  handler       = "hello"
  runtime       = "go1.x"

  filename         = "function.zip"
  source_code_hash = filebase64sha256("function.zip")

  depends_on = [
    aws_iam_role.demo_lambda_role
  ]

  environment {
    variables = {
      TopicArn = aws_sns_topic.demo_sns_topic.arn
    }
  }

  tags = {
    Name = "Demo Lambda to SNS"
  }
}

# Lambda IAM Role
data "aws_caller_identity" "current" {}
data "aws_region" "current" {}

locals {
  account_id = data.aws_caller_identity.current.account_id
}

# Lambda IAM Role
resource "aws_iam_role" "demo_lambda_role" {
  name = "demo-lambda-role"

  assume_role_policy = jsonencode({
    "Version" : "2012-10-17",
    "Statement" : [
      {
        "Action" : "sts:AssumeRole",
        "Principal" : {
          "Service" : "lambda.amazonaws.com"
        },
        "Effect" : "Allow"
      }
    ]
  })

  inline_policy {
    name = "demo-lambda-policies"
    policy = jsonencode({
      "Version" : "2012-10-17",
      "Statement" : [
        {
          "Effect" : "Allow",
          "Action" : "logs:CreateLogGroup",
          "Resource" : "arn:aws:logs:${data.aws_region.current.name}:${local.account_id}:*"
        },
        {
          "Effect" : "Allow",
          "Action" : [
            "logs:CreateLogStream",
            "logs:PutLogEvents"
          ],
          "Resource" : [
            "arn:aws:logs:${data.aws_region.current.name}:${local.account_id}:log-group:/aws/lambda/*:*"
          ]
        },
        {
          "Effect" : "Allow",
          "Action" : "sns:Publish",
          "Resource" : aws_sns_topic.demo_sns_topic.arn
        }
      ]
    })
  }
}

# SQS
resource "aws_sqs_queue" "demo_queue" {
  name                    = "demo-queue"
  sqs_managed_sse_enabled = true
}

# SNS
resource "aws_sns_topic" "demo_sns_topic" {
  name = "user-updates-topic"
}

# Subscribe SQS to SNS
resource "aws_sns_topic_subscription" "demo_queue_target" {
  topic_arn = aws_sns_topic.demo_sns_topic.arn
  protocol  = "sqs"
  endpoint  = aws_sqs_queue.demo_queue.arn
}

# SQS Queue Policy
resource "aws_sqs_queue_policy" "demo_queue_policy" {
  queue_url = aws_sqs_queue.demo_queue.id

  policy = jsonencode({
    "Version": "2012-10-17",
    "Id": "sqspolicy",
    "Statement": [
      {
        "Sid": "First",
        "Effect": "Allow",
        "Principal": "*",
        "Action": "sqs:SendMessage",
        "Resource": aws_sqs_queue.demo_queue.arn,
        "Condition": {
          "ArnEquals": {
            "aws:SourceArn": aws_sns_topic.demo_sns_topic.arn
          }
        }
      }
    ]
  })
}

NOTE: This example doesn’t use encryption on the SNS Topic, but if you care about Data Protection then there is a good chance you will want to add that too.

Deploy Terraform

First, run

terraform plan

Now, once we’re confident we can run:

terraform apply

And that’s it! You should now have a Lambda -> SNS Topic -> SQS Queue deployed.

Testing Lambda

This lambda requires one event input parameter, for example:

{
  "SnsMessage": "Here is a sample message."
}

You can either execute this from the CLI, or find the lambda in the AWS console and create a test event:

How to invoke lambda with event

Click “Test” and you should now see a success message!

Summary

And that’s it! The infrastructure is ready, and you should now be able to publish SNS messages from your Lambda. These messages will then flow through to your SQS queue ready to be processed later.

While it would be possible to write directly to SQS from the lambda, there are many benefits of using SNS as an intermediary step which we will explore in our next article.

Cleanup

If you were just experimenting, remember you can destroy the resources when you’re done by running:

terraform destroy

While these serverless components are relatively cheap, it is always good to keep your account clean to avoid any unnecessary billing.

Rhuaridh

Please get in touch through my socials if you would like to ask any questions - I am always happy to speak tech!