IoT
Exploring AWS IoT Services - Part 3
In the final installment of our AWS IoT journey, this post explores how to use IoT Jobs to update configuration information on a fleet of devices.
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?
In order to push out updates to the Lambda code in Greengrass, we need to:
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.
In the final installment of our AWS IoT journey, this post explores how to use IoT Jobs to update configuration information on a fleet of devices.