Services
About
CloudProse Blog
Serverless

Manage Your AWS Greengrass Lambda Functions with SAM

The AWS Serverless Application Model (SAM) makes deploying your Lambda code to Greengrass a snap.
Forrest Brazeal
Forrest Brazeal | Jun 12 2018

Tue, 12 Jun 2018

AWS Greengrass lets you run Lambda functions on your favorite edge device, such as a Raspberry Pi, while maintaining seamless integration with your resources in the AWS cloud. If that sounds complex to you, you’re not wrong. The official Greengrass Getting Started guide spans six modules and requires significant manual configuration both on your device and in the cloud.

A few tools are starting to emerge that take away some of the Greengrass deployment pain. A Cloud Guru has published AWS Greengrass: The Missing Manual and the associated greengo deployment tool. Our friends at IOPipe just released a Greengrass image for the Pi called Grassbian. AWS also has some Lambda deployment examples in their original Greengrass demo app.

But all these examples call the Lambda API. That’s not how we want to deploy our Lambda functions! We want to manage our Greengrass Lambdas the same way we handle any other serverless code, using a framework like AWS SAM (the Serverless Application Model).

So how can SAM help us push code to Greengrass?

Managing Greengrass Lambda Code With AWS SAM

Prerequisites

  • You have a Greengrass core and group created
  • You have configured your Lambda function in Greengrass according to the instructions in the Getting Started guide.

Steps

In order to push out updates to the Lambda code in Greengrass, we need to:

  1. Update the Lambda function
  2. Publish a new version of the Lambda function
  3. Associate that version with the Lambda alias used by our existing Greengrass group
  4. Create a new deployment of our Greengrass group

That’s quite a few clicks in the AWS console. Let’s see how AWS SAM makes this easier using the template below.

AWSTemplateFormatVersion: '2010-09-09'Transform: AWS::Serverless-2016-10-31Description: Device LambdaParameters: GroupName: Default: my-group Type: String FunctionAlias: Default: prod Type: StringResources: LambdaRole: Type: AWS::IAM::Role Properties: ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSGreengrassResourceAccessRolePolicy - arn:aws:iam::aws:policy/AWSGreengrassFullAccess AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: sts:AssumeRole DeviceCoreFunction: Type: AWS::Serverless::Function Properties: CodeUri: device/ Handler: device.function_handler Runtime: python2.7 Role: !GetAtt LambdaRole.Arn AutoPublishAlias: !Ref FunctionAlias CustomFunction: Type: AWS::Serverless::Function Properties: CodeUri: custom/ Handler: custom.function_handler Runtime: python2.7 Role: !GetAtt LambdaRole.Arn Environment: Variables: GROUP_NAME: !Ref GroupName CustomResource: Type: Custom::CustomResource DependsOn: DeviceCoreFunction Properties: ServiceToken: !GetAtt 'CustomFunction.Arn' ParameterOne: Parameter to pass into Custom Lambda Function

What’s going on in this SAM template? We’re creating two Lambda functions, an associated IAM role, and a custom resource. Let’s break down the sections individually.

The LambdaRole contains the managed policies your function on the edge device will need to interact with the AWS Greengrass service. (If your function needs to access other AWS resources, you can add those permissions to this role as well.)

<pre><code> LambdaRole: Type: AWS::IAM::Role Properties: ManagedPolicyArns: - arn:aws:iam::aws:policy/service-role/AWSGreengrassResourceAccessRolePolicy - arn:aws:iam::aws:policy/AWSGreengrassFullAccess AssumeRolePolicyDocument: Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: sts:AssumeRole</code></pre>

DeviceCoreFunction is the Lambda function you want to run on Greengrass.

<pre><code> DeviceCoreFunction: Type: AWS::Serverless::Function Properties: CodeUri: device/ Handler: device.function_handler Runtime: python2.7 Role: !GetAtt LambdaRole.Arn AutoPublishAlias: !Ref FunctionAlias</code></pre>

Assuming we have some code in device.py, the magic here is the AutoPublishAlias property. This amazing line of config singlehandedly creates an alias for the function, publishes a new version, points the alias to the version, and points all event sources to the alias, any time your function code changes. (In fact, this is just scratching the surface of SAM’s cool Lambda deployment powers.)

That takes care of steps 1 and 2 on our list of Greengrass deployment steps. Now we just need to update the Greengrass deployment itself. Unfortunately, Greengrass does not yet have CloudFormation support. Instead, we can use a Lambda-backed custom CloudFormation resource in our SAM template, here called CustomResource:

<pre><code> CustomFunction: Type: AWS::Serverless::Function Properties: CodeUri: custom/ Handler: custom.function_handler Runtime: python2.7 Role: !GetAtt LambdaRole.Arn Environment: Variables: GROUP_NAME: !Ref GroupName CustomResource: Type: Custom::CustomResource DependsOn: DeviceCoreFunction Properties: ServiceToken: !GetAtt 'CustomFunction.Arn'</code></pre>

The CustomFunction will run our Greengrass deployment code in custom.py:

<pre><code>import boto3import jsonimport osfrom urllib2 import build_opener, HTTPHandler, Requestclient = boto3.client('greengrass')def deploy_greengrass_group(group_name): group = [ group for group in client.list_groups()['Groups'] if group['Name'] == group_name ][0] client.create_deployment( DeploymentType='NewDeployment', GroupId=group['Id'], GroupVersionId=group['LatestVersion'] )def function_handler(event, context): if event['RequestType'] == 'Create' or event['RequestType'] == 'Update': deploy_greengrass_group(os.environ['GROUP_NAME']) sendResponse(event, context, "SUCCESS", { "Message": "Resource update successful!" }) else: sendResponse(event, context, "FAILED", { "Message": "Unexpected event received from CloudFormation" })def sendResponse(event, context, responseStatus, responseData): responseBody = json.dumps({ "Status": responseStatus, "Reason": "See the details in CloudWatch Log Stream: " + context.log_stream_name, "PhysicalResourceId": context.log_stream_name, "StackId": event['StackId'], "RequestId": event['RequestId'], "LogicalResourceId": event['LogicalResourceId'], "Data": responseData }) opener = build_opener(HTTPHandler) request = Request(event['ResponseURL'], data=responseBody) request.add_header('Content-Type', '') request.add_header('Content-Length', len(responseBody)) request.get_method = lambda: 'PUT' response = opener.open(request)</code></pre>

Most of the code above is boilerplate that takes care of sending the custom resource response back to CloudFormation. The function of interest is deploy_greengrass_group, which retrieves the group identifiers based on its name and then creates a new deployment using the Greengrass SDK for Python. (Note that urllib2 is used to avoid the extra step of packaging the requests module with our Lambda code.)

All that’s left now is to deploy the SAM template, replacing [YOUR_BUCKET] with an S3 bucket in your environment (SAM CLI installation instructions):

sam package --template-file device.template --s3-bucket [YOUR_BUCKET] --output-template-file packaged.yamlsam deploy --template-file ./packaged.yaml --stack-name gg-device --capabilities CAPABILITY_IAM

Now you can deploy the SAM template as many times as you change your code, and the code should automagically be pushed out to all the Lambda functions in your Greengrass group! When it comes to deployments, it looks like the grass really is greener on the SAM side of the fence.

Author
Forrest Brazeal
Forrest Brazeal