Skip to content

Working with SNS

Dennis Vriend edited this page Jan 4, 2018 · 1 revision

Working with SNS

The AWS Simple Notification Service is a fully managed pub/sub messaging service for coordinating the delivery of messages to subscribing endpoints and clients. SNS necessary notification ie. event driven systems. When something is worth notifying about ie. an notification event has occurred then you need the SNS service.

Seed project

The following seed project can be used as example on how to use SNS:

Libraries

Components use SNS need to add the following library to build.sbt:

libraryDependencies += "com.amazonaws" % "aws-java-sdk-sns" % "1.11.255"

Setting up SNS

Lambdas automatically have access to SNS when the execution policy allows it and the default client is used:

import com.amazonaws.services.sns.{AmazonSNS, AmazonSNSClientBuilder}
val sns: AmazonSNS = AmazonSNSClientBuilder.defaultClient()

Scoped SNS Topic names

Most resources in an AWS account have a flat namespace, this means that resources like SNS topics must have a unique name inside a single account. To be able to deploy multiple component in an account that use the same SNS topic name, the project uses a 'scope'.

A 'scope' works like a 'namespace' or 'package' in programming languages, it allows to reuse a name by prefixing it with a unique prefix.

sbt-sam manages resource names for us. It uses the sbt name setting and samStage setting to define a scope. All resources are prefixed with [name]-[stage]-[resource-name]. For example, a topic called 'person' would become name-stage-person.

A scoped topic name is useful, in the source code we can refer to the normal name 'person' and the topic will be automatically scoped by the settings of the project.

The setting name is obvious, it is the project name, the setting samStage defines a unique stage for the component. The stage can be an arbitrary text, and defines only a scope for the component. At first glance one would use the scope to define 'dev', 'acc', 'prod' environments. The stage is also useful when defining a single component, that can be composed together. For example, when defining a data-lake, consisting of multiple components, a component has the same project name, but the data lake consists of multiple 'stages', for example, 'part1', 'part2', 'part3' etc.

Resolving scoped topic names

The com.github.dnvriend.lambda.SamContext, a case class that is always available inside a Lambda, provides methods that can be used to determine the scoped topic name from its short name.

def handle(request: HttpRequest, ctx: SamContext): HttpResponse {
  val topicName = "HelloWorld"
  val topic: String = ctx.snsTopicArn(topicName)
  sns.publish(topic, "Hello World")
}

SNS Event Source

SNS is an event source in AWS serverless. This means that a Lambda can handle events generated by SNS:

@SNSConf(topic = "person-received")
class PersonReceivedPublished extends SNSEventHandler {
  override def handle(events: List[SNSEvent], ctx: SamContext): Unit = {
    val repo = new PersonRepository("people", ctx)
    events.foreach { event =>
      val person = event.messageAs[Person]
      val id = repo.id
      repo.put(id, person.copy(name = person.name + "-sns-" + person.id))
    }
  }
}

When the component defines a topic called 'person-received' in conf/sam.conf, then you can refer to the topic by just annotating a SNSEventHandler with @SNSConf and the topic name. The content of conf/sam.conf is:

topics {
    personReceived {
        name = "person-received"
        display-name = "person-received"
        export = false
    }
}

A topic can also be exported by another component by setting export = true in sam.conf. When you want to use a topic that is exported by another component, you need to use the following syntax to refer to that topic:

@SNSConf(topic = "import:component-name-export-topic:person-received")
class PersonReceivedPublished extends SNSEventHandler {
  override def handle(events: List[SNSEvent], ctx: SamContext): Unit = {
    val repo = new PersonRepository("people", ctx)
    events.foreach { event =>
      val person = event.messageAs[Person]
      val id = repo.id
      repo.put(id, person.copy(name = person.name + "-sns-" + person.id))
    }
  }
}

The import syntax consists of a component name that exports the topic, and the name of the topic. Also, you can only import resources from components from the same stage as your own component, so if you're component is deployed in the dev stage, you can only import resources from components that are also deployed in the dev stage. The scoped-resources naming convention assures this behavior.