Adding cross account invoke permissions to a Lambda function with the Serverless Framework | Man Yells at Cloud<!-- --> | <!-- -->Man Yells at Cloud<!-- --> <!-- -->

Man Yells at Cloud

Adding cross account invoke permissions to a Lambda function with the Serverless Framework

January 2, 2024

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-api
frameworkVersion: "3"
provider:
name: aws
runtime: nodejs18.x
lambdaHashingVersion: '20201221'
functions:
hello:
handler: handler.hello
events:
- 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-api
frameworkVersion: "3"
provider:
name: aws
runtime: nodejs18.x
lambdaHashingVersion: '20201221'
functions:
hello:
handler: handler.hello
events:
- http:
path: /
method: get
resources:
Resources:
permission:
Type: AWS::Lambda::Permission
Properties:
FunctionName:
Fn::GetAtt: [HelloLambdaFunction, Arn]
Action: lambda:InvokeFunction
Principal: "*"
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-api
frameworkVersion: "3"
provider:
name: aws
runtime: nodejs18.x
lambdaHashingVersion: '20201221'
functions:
hello:
handler: handler.hello
events:
- http:
path: /
method: get
calculate_cost_of_pizza:
handler: handler.hello
events:
- http:
path: /calculate_cost_of_pizza
method: get
resources:
Resources:
hello_permission:
Type: AWS::Lambda::Permission
Properties:
FunctionName:
Fn::GetAtt: [HelloLambdaFunction, Arn]
Action: lambda:InvokeFunction
Principal: "*"
PrincipalOrgID: "your-org-id"
calculate_cost_permission:
Type: AWS::Lambda::Permission
Properties:
FunctionName:
Fn::GetAtt: [CalculateUnderscorecostUnderscoreofUnderscorepizzaLambdaFunction, Arn]
Action: lambda:InvokeFunction
Principal: "*"
PrincipalOrgID: "your-org-id"

I hope this helped! Feel free to @ me with your ridiculously long function names.