Six Tips For Developing with AWS Greengrass and CloudFormation: Part 1

AWS Greengrass

These might just help you get a jump over some of the hurdles you may encounter using these services.

First Things First

Some Context

With the rise of IaaS (Infrastructure as a Service), web/cloud services are in huge fashion these days. If you’re a young Software Engineer like me, it is likely you have been or will at some point be handed down a solution design (or architect one yourself!) for a project that makes use of many of these different cloud services, be them hosted by AWSAzure or GCP.

A lot of these services are, honestly put, awesome to work with, as you feel like you’re fiddling with the forefront of technology (seldom stuff you can simply explain to common folk). Well, something you will invariably find along the line is that for the multiplicity of tools you are presented with, in your use cases they will not always play as well with each other as you’d like for one reason or another, and if you’re anything like me (my hunch is most devs are), you’re more fond of actually writing code than “wasting” time with configuration.

Enter…This Article’s Goal

I was recently tasked with developing the IoT part of one of Concentrix Catalyst’s projects. Given that we have an AWS partnership, the solution was designed using AWS services (mainly GreengrassCloudFormation and Lambda for managing the IoT device, the cloud resources, and the actual function code).

Rather than explaining the whole use case and how the IoT piece fit into our architecture (worthy of another article, no doubt), the goal of this article is to share with you a combination of practical solutions for challenges I faced throughout the project while using Greengrass and CloudFormation.

  1. Deploying multiple Greengrass groups from a single CloudFormation template;
  2. Using Lambda functions from a different (Serverless) stack in a Greengrass group;
  3. Including the Greengrass group deployment in the development pipeline.

In order not to make this article too lengthy, I will deliver only the above three tips as a Part I, and in case you’re interested you can move along to Part II, which goes through:

  1. Handling IoT rules and rule permissions from CloudFormation;
  2. Streamlining the test of code changes in a physical Greengrass IoT edge device;
  3. Accessing local Greengrass IoT edge device files for simple operations.

For each of these, I will explain how and why the difficulty came about in my experience, and what learnings I needed to resolve the matter. Let’s jump in.

The Tips

Note: for brevity, I’ll refer to Greengrass as GG and to CloudFormation as CFN throughout.

Deploying Multiple Greengrass Groups From a Single CloudFormation Template

For all GG resource types in CFN, for example: AWS::Greengrass::CoreDefinition or AWS::Greengrass::FunctionDefinition, the logical ID (i.e. custom name you give to the resource) needs to be unique within the template, but not between different groups. The actual physical ID (Name property for some resources and Id property for others) is what actually needs to differ from group to group and resource to resource:

logical vs physical code

What does this mean in practice? It means that you can use the exact same template for the various groups, as long as you keep the Name (or Id where applicable) properties of your definition resources dynamic (and unique), which you can do with the help of CFN Parameters. Suppose that your group only has one IoT thing which is one core. The part of your CFN template that defines it will look something like this:

Notice how, although our resources’ logical IDs are simply namedCoreThingCoreDefinition and CoreDefinitionVersion (and will be for every group), the ThingName property of the CoreThing, the Name property of the CoreDefinition as well as the Id property of the Core in the CoreDefinitionVersion get their values by referring to two variables: pEnvName for the environment, the group is to be deployed to (usually something likedevuat or prod); and pCoreId for the core ID (identifier for each core). These would be passed to the template as:

You would do something similar to define your functions (see code below). Again, notice the Name property of the FunctionDefinition resource as well as the Id property of the Function resource inside the FunctionDefinitionVersion. This particular GGIPDetector Lambda function is provided by AWS and actually allows the GG group to have its core connectivity set as automatically detectable by default.

What you’d do from here is just guarantee that the pair pEnvName+pCoreId you specify when deploying the CFN template is never repeated for different groups and you’d be good to go with multiple GG groups.

First takeaway: to deploy multiple GG groups from a single CFN template, use that one template with the same logical IDs but using parameters to create unique physical IDs.

Using Lambda Functions From a Different (Serverless) Stack in a Greengrass Group

Our CFN template was a little more complicated than mentioned in tip 1. To keep things consistent with the backend system we were developing in parallel, our Lambda functions destined for the IoT device were managed through a Serverless framework stack.

The problem is that you need to provide the functions in the CFN AWS::Greengrass::FunctionDefinitionVersion resource through the FunctionArn property:

Problem: how to get the qualified ARNs of our custom Serverless Lambda functions into this stack? We could hardcode them, but what happens when we deploy a new function version and want this new version to be deployed to the GG core instead of the old one? Should we update the template manually again and again with the new ARN? That’s not very handy.

This is a common problem with CFN — how to export Lambda qualified ARNs to a different stack. The usual pattern using the ImportValue intrinsic function does not work well because that is intended for static values. Try it with a qualified Lambda ARN and it will fail on update.

Once again, our solution was to resort to CFN Parameters. Our development pipeline ran a set of scripts that would, in summary:

  • First, deploy the latest Serverless stack (i.e. with the most up-to-date Lambda functions) through the sls deploy command;
  • Second, make use of the Serverless CLI’s sls info -v command and some manipulation with the jq tool to output the Lambda ARNs into a JSON file;
  • Lastly, deploy the CFN GG groups by feeding in as Parameters the Lambda ARNs read from that JSON file.

I totally understand if this sounds a bit too abstract, however it’s really as far as I can go with a conceptual idea explanation. Hit me up personally for more implementation details if you need them, or please share if you have found a better way to share dynamic values across stacks!

Second takeaway: to use Lambda functions from a different stack in a GG group, make your pipeline output their qualified ARNs to a file and then feed them into the group template as parameters.

Including the Greengrass Group Deployment in the Development Pipeline

For this tip it is important to first clarify the difference between deploying a CFN stack that defines the GG resources and actually deploying the GG group to physical devices. The first simply creates/updates (I like the database-originated term upsert) the GG group, its functions, subscriptions, etc., while the second one “installs” that updated group onto the GG core and/or any other IoT edge devices. To differentiate them I will refer to them correspondingly as “cloud deployment” and “physical deployment”.

If you’ve been working with GG for a little while, you might have noticed the little “annoying” fact (which to be fair does make sense) that you cannot perform a group cloud deployment if that group is currently in a successful physical deployment state.

Our development pipeline was not prepared for this and I’ll be honest: it triggered me. I didn’t want to be manually resetting the physical deployments in the AWS Console every time I wanted to deploy a new GG group version to the cloud. The solution: automate the reset of the physical deployments on our pipeline script.

The AWS CLI provides a method to reset group deployments (the physical kind). To use it, I noticed that pretty much all I needed was to somehow get each cloud-deployed group’s ID. Thus the first step was to work with CFN Outputs to (redundancy aside) output that ID from the CFN template for the GG group, like this:

GG Group template definition example. Notice how we can use Fn::ImportValue for the GG group role ARN (which we couldn’t in tip 2. for qualified Lambda ARNs) because its value never changes.

Then in my pipeline script, I was able to use the command describe-stacks with a query to extract that group ID from the CFN stack output and then use the reset-deployments command to force physical deployment reset:

Things like $AWS_REGION are Bitbucket pipeline variables.

This step is performed each time before CFN deployment, ensuring that, if the GG group is already deployed to the cloud, its physical deployments will not get in the way of an update.

Something worth mentioning was that we decided to keep the physical deployment post cloud deployment as a manual step. This was intentional because

  • We might want to update the GG cloud group multiple times before physical deployment;
  • The edge device might not be ready;
  • There might be extra safety reasons to consider.

However we could have automated it as well if we wanted to with the create-deployment command. Furthermore, the group ID output proves useful when you have many devices in production and want to perform bulk physical deployment. If that’s something you need, I’ll point you to this AWS documentation, which you can combine with this tip.

Third takeaway: to include the GG group deployment in the development pipeline, output the GG group ID from the CFN template and grab it in the pipeline to reset physical deployments before cloud deployment.

The first part of my development tips ends now. Tune in to the sequel at Part II to uncover even more dev tips, if you liked this one and/or if you didn’t find all the answers you were looking for.