Forrest Brazeal in iot 7 minutes to read

Manage Your AWS Greengrass Lambda Functions with SAM

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-31
Description: Device Lambda
Parameters:
  GroupName:
    Default: my-group
    Type: String
  FunctionAlias:
    Default: prod
    Type: String
Resources:
  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.)

  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 is the Lambda function you want to run on Greengrass.

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

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:

  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'

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

import boto3
import json
import os
from urllib2 import build_opener, HTTPHandler, Request

client = 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)

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.yaml
sam 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.