Invoking Remote Lambda Functions with Custom Resources in AWS CloudFormation Templates

One under-appreciated feature of Amazon Web Services’ CloudFormation templates is the ability to make custom resources. Inspired by my previous post on how to update Infoblox DNS records using curl, I’ll now take that idea one step further and give a quick taste of how you could use CloudFormation custom resources to automatically update DNS records via some Python code in an AWS Lambda function (hint: adapt those curl commands in Python using “requests“). You may not have this exact use case, but the principles are the same no matter what you want CloudFormation to interact with.

Once you’ve created your Lambda function, you’ll need to invoke it somehow. The most direct way to do this is via a Lambda-backed custom resource. Here’s an example:

 "InfobloxDNSRecords": {
   "Type": "Custom::InfoBloxEntry",
   "DependsOn": "Instance",
   "Properties": {
     "ServiceToken": { "Fn::Join": [ ":", [
 "arn:aws:lambda",
 { "Ref": "AWS::Region" },
 { "Ref": "AWS::AccountId" },
 "function:CloudFormation-DNS-Infoblox"
 ] ] },
     "RecordName": { "Ref": "SystemName" },
     "IPv4Address": { "Fn::GetAtt" : [ "Instance", "PrivateIp" ] },
     "Arecordcomment": { "Fn::Join": [ "", [ "AWS ", { "Ref": "AWS::Region" }, ", ", { "Ref": "AWS::StackName" }, ", ", { "Ref": "TagApplication" }, ", ", { "Ref": "TagUseCase" } ] ] },
     "PTRrecordcomment": { "Fn::Join": [ "", [ "AWS ", { "Ref": "AWS::Region" }, ", ", { "Ref": "AWS::StackName" }, ", ", { "Ref": "TagApplication" }, ", ", { "Ref": "TagUseCase" } ] ] }
    } 
 }

You would put this code in the “Resources” section in CloudFormation. Notice that the “ServiceToken” section is referencing the ARN of a Lambda function you’ve already created, but since you may not want to hard-code the function’s ARN, you can “Join” various pieces that make up the ARN together: the beginning part that’s common to all Lambda functions (“arn:aws:lambda”), the region you’re in, the Account ID you’re in, and the name of the function. Again, this is just to illustrate an idea – you could just hard-code the ARN in there instead.

Below that, you’ll see a section where I’m defining any additional info I want CloudFormation to pass along to my Lambda function. In this example, it’s the name of the instance I’m building, its IPv4 address, and some comments I’ll reference as variables in my function’s Python script that will put them in the DNS records’ comment field in Infoblox. These particular comments join together the letters “AWS” (to differentiate these DNS records from non-AWS records), the region the instance is in, the CF’s stack name, and two custom tags assigned to the instance, which describe its application and use case. Of course, you can pass along whatever info you want to about these instances.

Of course, you can’t have CloudFormation in one account/region work with a Lambda function in a different account/region, and you may not want to (or be able to) create that Lambda function in every region/account you want to run it from, so you could instead create an SNS topic in each account/region and then subscribe your single Lambda function to the SNS topic in each location. You’d then have CloudFormation send its info to the local SNS topic which will pass it along to Lambda in the other account/region, instead of trying to get CF to talk directly to Lambda, which it can’t do across regions/accounts.

(Note: While you can subscribe an SNS topic to a Lambda function across regions within the same account via the Console/GUI, you can not do it automatically via CloudFormation at the moment. AWS tech support tells me they’ve submitted a “feature request” to have this changed, but for now, the only way to subscribe a topic to a function in another region is via the console or using the CLI. The only way to subscribe a topic to a Lambda function across accounts is using the CLI, via a confusing but quick procedure which I go into in my next post.)

So, to send your CloudFormation data to a Lambda function in another region/account, here’s the code you’ll want to drop in the “Resources” section of your template to talk to the SNS topic you’ve created:

 "InfobloxDNSRecords": {
   "Type": "Custom::InfoBloxEntry",
   "DependsOn": "Instance",
   "Properties": {
     "ServiceToken": { "Fn::Join": [ "", [ "arn:aws:sns:", { "Ref": "AWS::Region" }, ":", { "Ref": "AWS::AccountId" }, ":SNSInfblox" ] ] },
     "RecordName": { "Ref": "SystemName" },
     "IPv4Address": { "Fn::GetAtt" : [ "Instance", "PrivateIp" ] },
     "Arecordcomment": { "Fn::Join": [ "", [ "AWS ", { "Ref": "AWS::Region" }, ", ", { "Ref": "AWS::StackName" }, ", ", { "Ref": "TagApplication" }, ", ", { "Ref": "TagUseCase" } ] ] },
     "PTRrecordcomment": { "Fn::Join": [ "", [ "AWS ", { "Ref": "AWS::Region" }, ", ", { "Ref": "AWS::StackName" }, ", ", { "Ref": "TagApplication" }, ", ", { "Ref": "TagUseCase" } ] ] }
   }
 }

Notice, the only thing I really changed from the example above is the “ServiceToken” which now references “arn:aws:sns” since it’s an SNS topic.

On the Lambda side, it will receive the same data as before, but when it comes via SNS it is all a long JSON string in a line called “Message” in the SNS payload, so you’ll have to add a bit of code to tease it out. Something like this will do it:

incoming = json.loads(event['Records'][0]['Sns']['Message'])

This is far from exhaustive, but hopefully these quick examples will help get you on your way to using custom resources in your CloudFormation templates so you can more fully take advantage of more advanced AWS functionality.

Happy coding,

Steve