In this post
We will add some Cloudformation to a simple Serverless application that allows a Lambda within to be invoked by a principal in a different account. We'll also quickly talk about why it can be useful to do this! We will assume if you've found your way here that you have some practical experience in deploying Serverless applications and an environment that you'd like to apply this to.
Ok but why?
As your AWS estate grows, one of AWS' own recommendations is that you separate different workloads into separate accounts. Depending on how granular you want to get, you may end up with many accounts. When it becomes a good time to separate services across accounts is an entirely different conversation, however as this sort of work happens, inevitably there will end up a bit of functionality that is shared that we don't want to reimplement multiple times in multiple accounts. This is where the ability to invoke a Lambda function in a separate account comes in.
Serverless - The Problem
As a Cloud Engineer, I love infrastructure as code. I do however have a sordid history with Cloudformation, and try and avoid it wherever I can. I unfortunately am also a massive advocate of deploying serverless applications (where it makes sense), and one of the most prevalent tools for that is the Serverless framework which under the hood deploys using... ah.
We bugger on though.
Normally, if you're just manually deploying a Lambda, you could go in and edit it's resource policy manually and add something akin to the following:
{"Sid": "AllowInvokeFromSeparateAccount","Effect": "Allow","Principal": {"AWS": "*"},"Action": "lambda:InvokeFunction","Resource": "arn:aws:lambda:us-east-1:987654321:function:aws-node-rest-api-test-hello","Condition": {"StringEquals": {"aws:PrincipalOrgID": "your-org-id"}}}
This would allow any account within your AWS organisation to invoke the function. If you wanted to refine the permissions more to only allow from a specific account you could change the Principal block like so:
"Principal": {"AWS": "arn:aws:iam::123456789:root"}
You could even go down to a specific IAM role.
However, manually editing permissions sort of defeats the point of having IaC in the first place, and creates uncomfortable levels of risk if you need to re-deploy the application. So how do we add this using Serverless? We have to add a bit of custom Cloudformation under the resources
block that Serverless supports.
To demonstrate, I'm going to be using the node-rest-api starter example, with the two components being:
handler.js
"use strict";module.exports.hello = async (event) => {return {statusCode: 200,body: JSON.stringify({message: "Go Serverless v2.0! Your function executed successfully!",input: event,},null,2),};};
serverless.yaml
service: aws-node-rest-apiframeworkVersion: "3"provider:name: awsruntime: nodejs18.xlambdaHashingVersion: '20201221'functions:hello:handler: handler.helloevents:- http:path: /method: get
All we need to do to add the permission is modify serverless.yaml
to show the following:
service: aws-node-rest-apiframeworkVersion: "3"provider:name: awsruntime: nodejs18.xlambdaHashingVersion: '20201221'functions:hello:handler: handler.helloevents:- http:path: /method: getresources:Resources:permission:Type: AWS::Lambda::PermissionProperties:FunctionName:Fn::GetAtt: [HelloLambdaFunction, Arn]Action: lambda:InvokeFunctionPrincipal: "*"PrincipalOrgID: "your-org-id"
Naming Caveats
Serverless effectively translates everything to work nicely in Cloudformation and because of that, some translations can be a little... confusing. In our example above, you can see that though we called our Lambda hello
we have to reference it using the name HelloLambdaFunction
.
This can get more complex - for example where you might have called your function something like calculate_cost_of_pizza
, the Serverless Framework needs to make sure that becomes some unique identifier that Cloudformation can still read, and so you wouldn't necessarily be able to reference it the same as we have in the simple scenario above.
Honestly, without reading and understanding the documentation on how it translates everything, the easiest way to see what the end result identifier will be is to deploy the application without our new permissions block, and go see what the name becomes in the Cloudformation stack that is generated. Let's check that out with the calculate_cost_of_pizza
example and see what it-
Holy hell that's long
So, the end result for our serverless.yaml
would be:
service: aws-node-rest-apiframeworkVersion: "3"provider:name: awsruntime: nodejs18.xlambdaHashingVersion: '20201221'functions:hello:handler: handler.helloevents:- http:path: /method: getcalculate_cost_of_pizza:handler: handler.helloevents:- http:path: /calculate_cost_of_pizzamethod: getresources:Resources:hello_permission:Type: AWS::Lambda::PermissionProperties:FunctionName:Fn::GetAtt: [HelloLambdaFunction, Arn]Action: lambda:InvokeFunctionPrincipal: "*"PrincipalOrgID: "your-org-id"calculate_cost_permission:Type: AWS::Lambda::PermissionProperties:FunctionName:Fn::GetAtt: [CalculateUnderscorecostUnderscoreofUnderscorepizzaLambdaFunction, Arn]Action: lambda:InvokeFunctionPrincipal: "*"PrincipalOrgID: "your-org-id"
I hope this helped! Feel free to @ me with your ridiculously long function names.