Cross Account SNS Topic in CloudFormation

Pablo Perez
Pablo Perez
Published in
3 min readNov 13, 2018

When I think about what makes an infrastructure as a code tool valuable, I don’t just think on that it will allow me to deploy and track my environments, I think in how easy and clear will be to build and maintain my configurations.

Infrastructure as a code has to be simple providing the clarity of any declarative approach in order to have an easy maintenance in large environments.

When an infrastructure as a code tool needs from imperative procedures like for example using many custom resources in CloudFormation, maintenance of your defined infrastructure becomes complex and consumes engineers valuable time, furthermore it’s prone to errors and scares.

This apply also to configuration management tools, I have seen Chef cookbooks more complex than the Apollo 12 mission manuals, or Terraform and CloudFormation stacks with a crazy amount of code and convoluted designs. In my humble opinion that is not the goal.

Sometimes we know the imperative way is the only option available in order to work around limitations from the products however i wish to outline how important is to keep an eye on a daily basis to the products latest releases.

Little by little providers are filling all the gaps and keep up to date with the latest features released by other services.

We must gradually migrate and improve our imperative over-engineered stacks configurations accumulated over the years to simpler and clearer declarative ones, therefore more understandable and easier to maintain.

A tangible simple illustration is to substitute the custom and extra resources you had in your CloudFormation solutions to subscribe your lambdas cross region/cross account to SNS topics for simpler and shorter declarative configurations . Below we leverage new releases of new resources types like AWS::SNS::Subscription and AWS::SNS::TopicPolicy. The lack of them in the past years obliged us to go for imperative configurations and many people still is bonded to that.

1.- Stack A with SNS topic

Description: Stack that creates a SNS topic and a SNS topic policy to allow a lambda in account B to subscribe to the SNS topic.Parameters: 
OtherAccountNumber:
AllowedPattern: '[0-9]+'
Description: The 12 digit AWS account number to grant access to.
MaxLength: '12'
MinLength: '12'
Type: String
Default: 123456789101
Resources:
MySNSTopic:
Type: AWS::SNS::Topic
mysnspolicy:
Type: AWS::SNS::TopicPolicy
Properties:
Topics:
- !Ref MySNSTopic
PolicyDocument:
Statement:
- Action: 'sns:Publish'
Sid : '1'
Effect: Allow
Resource: !Sub "${MySNSTopic}"
Principal:
AWS: !Sub arn:aws:iam::${OtherAccountNumber}:root
- Action: 'sns:Subscribe'
Sid: '2'
Effect: Allow
Resource: !Sub "${MySNSTopic}"
Principal:
AWS: !Sub arn:aws:iam::${OtherAccountNumber}:root

2.- Stack B in another account with the lambda to subscribe to the above SNS topic.

Description: Stack that creates a Lambda function and subscribes it to a SNS topic in another account.
Parameters:
SNSTopicArn:
Type: String
Resources:
####below the lambda function to subscribe
LambdaFunction:
Type: "AWS::Lambda::Function"
Properties:
Runtime: python3.6
Description: >
Lambda function that pushes arns for the IAM Role and S3 bucket.
Handler: index.lambda_handler
Role: !Sub ${LambdaExecutionRole.Arn}
Code:
ZipFile: |
from __future__ import print_function
import json
print('Loading function')
def lambda_handler(event, context):
#print("Received event: " + json.dumps(event, indent=2))
message = event['Records'][0]['Sns']['Message']
print("From SNS: " + message)
return message
LambdaExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: BasicExecutionPolicy
PolicyDocument:
Version: 2012-10-17
Statement:
- Sid: CloudWatchLogsStatement
Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- logs:Describe*
Resource: arn:aws:logs:*:*:*
LambdaInvokePermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !GetAtt
- LambdaFunction
- Arn
Action: 'lambda:InvokeFunction'
Principal: sns.amazonaws.com
SourceArn: !Ref SNSTopicArn
LambdaExecutionCustomRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Principal:
Service:
- lambda.amazonaws.com
Action:
- sts:AssumeRole
Policies:
- PolicyName: BasicExecutionPolicy
PolicyDocument:
Version: 2012-10-17
Statement:
- Sid: CloudWatchLogsStatement
Effect: Allow
Action:
- logs:CreateLogGroup
- logs:CreateLogStream
- logs:PutLogEvents
- logs:Describe*
Resource: arn:aws:logs:*:*:*
- Sid: Permitsubscription
Effect: Allow
Action:
- sns:*
Resource: "*"
SnsLambdaSubscription:
Type: 'AWS::SNS::Subscription'
Properties:
TopicArn: !Ref 'SNSTopicArn'
Protocol: lambda
Endpoint: !GetAtt
- LambdaFunction
- Arn

--

--