AWS Robotics Blog

Deploying ROS applications as Snaps with AWS IoT Greengrass 2.0

Introduction

AWS recently announced the release of AWS IoT Greengrass 2.0, an edge runtime that offers added flexibility in deploying containerized applications than its predecessor. In this blog, we’ll explore why this improvement is useful for the robotics community, and walk through how you package and deploy Robot Operating System (ROS) applications to devices using AWS IoT Greengrass 2.0.

ROS applications are typically a combination of numerous packages, dependencies and launch configurations that are highly dependent on their runtime environment for successful execution. This potpourri of software may work perfectly and consistently in one environment, say the IDE on a developer’s workstation, and quickly break when it is ported to a new one, like after being deployed to a robot’s on-board computer.

Between changing OS distributions, varying chip architectures and conflicting upstream dependencies, the deployment of ROS applications from development environments to physical robots in a reliable and repeatable manner is a non-trivial problem for roboticists.

To avoid breaking a build, one needs a robust app packaging mechanism together with a durable delivery pipeline for any robot software.

The Problem: Packaging and Delivery

Packaging an application creates a container that includes all the software, libraries and support files needed to run the app as a standalone unit. This is not to be confused with a ROS package, which is a term used interchangeably to denote a ROS application.

There are a few benefits to rolling up application software together with its dependencies. For one, it makes the application immune to breaking changes in dependencies upstream, thereby in creasing the reliability of the software. In addition, since dependencies do not need to be downloaded for every new installation on a robot, the deployment is much faster than traditional apt, yum or rosdep installations. Lastly, once deployed, the packaged application runs in an isolated sandbox with only mediated access to sensitive system resources. This enhances the security posture of the robot.

There are several popular formats for building containerized packages available today, such as Docker, Flatpak, Bundles, Appimage and Snaps. Aggregating software into a single independent asset with any of these tools essentially makes the application portable, thus readying it for distribution. The distribution itself is handled by a delivery pipeline, which is a mechanism that downloads the packaged asset over-the-air on to a robot and installs it in the right subspace within the operating system.

Some delivery pipelines available to roboticists include Flathub, Snap Store and AWS IoT Greengrass. These delivery mechanisms are often tied to and optimized for only one of the aforementioned packaging formats, like Snap Store for Snaps or AWS IoT Greengrass 1.0 for Bundles. That changes with the latest release of AWS IoT Greengrass, which now supports delivery of applications as bundles, snaps, Docker images, or almost any format you prefer.

This gives developers much needed flexibility to use a packaging scheme that they are familiar and comfortable with, and benefit from the advantages that a particular packaging method may bring to a customer’s specific use case.

The Solution: Snaps and AWS IoT Greengrass

Snap is a packaging tool developed by Canonical for Linux-based operating systems. Snapping a ROS application creates a containerized package that works seamlessly within any Linux environment. Developers can make their software available to even niche flavors of Linux without having to create and maintain separate packages for each distro, saving on time and reducing the complexity of their code base. This cross-platform compatibility is a big advantage of snaps.

AWS IoT Greengrass 2.0 allows users to connect a robot to their AWS account as a core device. A daemon running on the robot handles messages and data between the device and the AWS Cloud. AWS IoT Greengrass adopts the idea of modular components; pieces of software that are deployed on a core device and defined by a configuration file called a recipe. Recipes usually refer to one or more artifacts, which are the software assets prepared by the developer. In the demo following, our artifact is the ROS snap that we create.

Walkthrough

In this exercise, we will package a sample AWS RoboMaker application as a snap and deploy it with AWS IoT Greengrass 2.0 on to a Raspberry Pi-operated TurtleBot. We assume the Pi is set up with a 64-bit OS. The exact operating system distro does not matter as long as it is a distribution of Linux.

Prerequisites

For this walkthrough, you should have the following prerequisites:

  • An AWS account
  • Turtlebot3 with 64-bit Raspberry Pi 3B/4
  • Working knowledge of ROS and snaps

Step 1: Set up the robot

Log in to your robot, either via SSH or by hooking up a display to the Pi, and configure it with some relevant dependencies.

  1. Install deb packages.
sudo apt update
sudo apt install -y default-jdk awscli snapd
  1. Configure AWS CLI by following the guide here.
  2. Create a S3 bucket that will store all artifacts for the robot. Replace the <S3_BUCKET> tags with the name you would like for the bucket.
aws s3 mb <S3_BUCKET>

Step 2: Connect the robot to AWS IoT Greengrass

  1. Download and unzip the AWS IoT Greengrass 2.0 installer.
wget https://d2s8p88vqu9w66.cloudfront.net/releases/greengrass-nucleus-latest.zip && unzip greengrass-nucleus-latest.zip -d GreengrassCore
  1. Run the installer with your choice of names for the robot and its fleet.
sudo -E java -Droot="/greengrass/v2" -Dlog.store=FILE \
  -jar ./GreengrassCore/lib/Greengrass.jar \
  --aws-region $( aws( aws configure get region ) \
  --thing-name TurtleBot \
  --component-default-user ggc_user:ggc_group \
  --provision true \
  --setup-system-service true \
  --deploy-dev-tools true

Your robot should now appear listed as a core device in AWS IoT Greengrass console. Make sure you are in the same region as the one configured in your AWS command line profile.

Photo showing the Turtlebot connected to Greengrass

Step 3: Grant robot access to project bucket

In the previous step, when we connected the robot to AWS IoT Greengrass, the installer will set up an IAM role that allows the robot to communicate with AWS. This role, however, does not yet have access to the project’s Amazon S3 bucket where we will upload artifacts. We will now attach a policy to this role so that the robot can access the snap once a deployment is triggered.

  1. Create a file named policy.json and copy the following snippet into it.
{
  "Version": "2012-10-17",
  "Statement": [
    {
	  "Effect": "Allow",
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::<S3_BUCKET>/*"
    }
  ]
}

Replace the <S3_BUCKET> placeholder with the name of the bucket you created in Step 1.

  1. Create an IAM policy using the policy document.
aws iam create-policy \
  --policy-name RosSnapArtifactPolicy \
  --policy-document file://policy.json
  1. Attach the policy to the robot’s IAM role.
id=$( aws sts get-caller-identity --query Account --output text )
aws iam attach-role-policy
  --role-name GreengrassV2TokenExchangeRole
  --policy-arn arn:aws:iam::$id:policy/RosSnapArtifactPolicy

The Amazon S3 access policy will now be associated with the robot’s AWS IoT Greengrass role in IAM.

Photo depicting the Amazon S3 access policy associated with the robot’s AWS IoT Greengrass role in IAM

Note that since we did not specify a role name while setting up the robot, AWS IoT Greengrass used a default value for the role name, GreengrassV2TokenExchangeRole. Had we stated a preferred role name during installation, we would use that role name in the last command.

Step 4: Bring up AWS dev environment

We will use the AWS Cloud9 IDE for this demonstration, but the compilation and deployment process described in Steps 6-9 can be extended to any environment of your choice.

  1. Sign in to the AWS RoboMaker console.
  2. In the navigation pane, expand Development, choose Development environments, and then choose Create environment.

  1. On the Create AWS RoboMaker development environment page, enter a Name for your environment.
  2. For ROS Distribution, choose ROS Melodic.
  3. Select t2.micro for the instance type.
  4. Select a VPC. Choose the default VPC.
  5. Select a Subnet. Choose any of the public subnets shown in the dropdown.
  6. Choose Create to set up the AWS Cloud9 development environment.

Photo showing the "Create" selection in the AWS console

This will open up a new tab with an AWS Cloud9 set up in progress. Give it a few moments to finish and then the IDE is ready for use. You can always reconnect to this IDE again by going to Development environments in the AWS RoboMaker console.

Step 5: Download sample ROS application

We will use a sample Hello World project provided by AWS RoboMaker. The ROS application will rotate your TurtleBot in place at a constant rate.

  1. In the AWS Cloud9 environment, click on Resources from the top bar, expand Download Samples, and choose the Hello World application.

A terminal tab titled “Download Samples” will spawn at the bottom of the screen and show the ROS application being downloaded. Wait for this process to finish.

  1. Select the terminal tab titled bash from the bottom of the screen.
  2. Navigate to the application’s catkin workspace.
cd ~/environment/HelloWorld/robot_ws
  1. Install dependencies.
rosdep install --from-paths src --ignore-src -r -y
  1. Confirm application functionality (optional).
catkin_make
source devel/setup.bash
roslaunch hello_world_robot rotate.launch

The application should run without any error messages.

Step 6: Build the ROS snap

Your AWS Cloud9 environment is running on a x86_64 virtual machine, while the Turtlebot has an aarch64 chip. So, we need to cross-compile the ROS snap for the target architecture. This is one area, however, where snapcraft – the tool used for building snaps – runs into trouble. Snapcraft does not currently support cross-compilation. We will thus use a custom script that leverages the AWS Cloud to remotely build an aarch64 snap with a single command.

  1. Create files required for snapping under a directory named snap in your project’s root.
mkdir -p snap/local
touch snap/snapcraft.yaml snap/local/aarch64_compile.sh snap/local/aarch64_cfn.yaml
  1. Copy the following snippet in to the snapcraft.yaml file.
name: aws-hello-world
version: '0.1'
summary: AWS RoboMaker Hello World
description: |
  AWS RoboMaker application to rotate a Turtlebot3

base: core18
grade: devel 
confinement: devmode

parts:
  robot-ws:
    plugin: catkin
    source: .
    build-packages: [lsb-release]
    
apps:
  echo:
    command: rostopic echo /cmd_vel
    plugs: [network, network-bind]
    
  launch:
    command: roslaunch hello_world_robot deploy_rotate.launch
    plugs: [network, network-bind]

This YAML file defines configuration details for the snap. Refer to snapcraft docs to dive deeper into what these settings mean.

  1. Copy the following snippet in to the aarch64_compile.sh file.
#!/bin/bash
set -euf -o pipefail
# Fetch status tag of an ec2 instance
function get_status {
	echo $(aws ec2 describe-tags \
		--filters Name=resource-id,Values=$1 \
		Name=key,Values=Status \
		--query "Tags[0].Value" --output text)
}

# Delete s3 bucket and cfn stack
function cleanup {
	echo "- Cleaning up resources"
	aws s3 rb s3://$name --force
	aws ec2 delete-key-pair --key-name $name
	aws cloudformation delete-stack --stack-name $name
}

# Custom response to sigint/sigterm
function handle_sig {
    cleanup
    trap - SIGINT SIGTERM # clear the trap
    kill -- -$$ # Sends SIGTERM to child/sub processes
}

trap cleanup SIGINT SIGTERM

# Check for snapcraft file
if [[ ! -f $(pwd)/snap/snapcraft.yaml ]]; then
  echo "[ERROR] Snapcraft config file not found!"
  exit -1
fi

# Create s3 bucket
uuid=$(head -c 16 /proc/sys/kernel/random/uuid)
name=aarch64-snap-$uuid
echo "- Creating S3 bucket"
aws s3 mb s3://$name

# Upload code files to bucket
echo "- Uploading source code to bucket"
aws s3 cp $(pwd)/src/ s3://$name/src --recursive
aws s3 cp $(pwd)/snap/ s3://$name/snap --recursive

echo "- Creating EC2 key pair"
aws ec2 create-key-pair --key-name $name &> /dev/null
# Initiate cfn stack
echo "- Setting up AWS resources"
stack_arn=$(aws cloudformation create-stack \
	--stack-name $name \
	--template-body file://$(pwd)/snap/local/aarch64_cfn.yaml \
	--parameters ParameterKey=UniqueName,ParameterValue=$name \
	--capabilities CAPABILITY_IAM \
	--query "StackId" --output text)

echo -e "\t- Stack Name: $name"
echo -e "\t- Stack ARN: $stack_arn"

# Wait for ec2 instance to launch
echo -e "- Spinning up EC2 instance\c"
ec2_id='None'

while [ $ec2_id == 'None' ]; do
	sleep 1
	echo -e '.\c'
	ec2_id=$(aws cloudformation describe-stacks \
		--stack-name $name \
		--query "Stacks[0].Outputs[?OutputKey=='InstanceId'].OutputValue" \
		--output text)
done

echo -e "\n\t- Instance ID: $ec2_id"
echo -e "- Installing AWS tools\c"

# Wait for ec2 status tag to be created
while [ $(get_status $ec2_id) == 'None' ]; do
	echo -e '.\c'
	sleep 1
done

# Install snap tools on ec2
echo -e "\n- Configuring machine\c"
while [ $(get_status $ec2_id) == "CONFIGURING" ]; do
	echo -e '.\c'
	sleep 1
done

echo -e "\nThe next step will take several minutes to complete. \c"
echo -e "Perfect opportunity for a stretch break!"

# Snap source code
echo -e "- Building snap\c"
while [ $(get_status $ec2_id) == "SNAPPING" ]; do
	echo -e '.\c'
	sleep 1
done

# Download snap from bucket
if [ $(get_status $ec2_id) == "COMPLETE" ]; then
	echo -e "\n- Retrieving snap"
	aws s3 cp s3://$name/$(aws s3 ls s3://$name/ | awk '{print $4}' | grep -i .snap) .
else
	echo "[ERROR] Something went wrong!"
	cleanup
	exit -2
fi

cleanup
echo 'Finished successfully!'
  1. Copy the following snippet in to the aarch64_cfn.yaml file.
Parameters:
  UniqueName:
    Type: String

Resources:
  Aarch64InstanceProfile:
    Type: AWS::IAM::InstanceProfile
    Properties:
      Roles:
        - !Ref Aarch64AccessRole

  Aarch64AccessRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: ec2.amazonaws.com
            Action: sts:AssumeRole

  RolePolicies:
    Type: AWS::IAM::Policy
    DependsOn:
      - Aarch64Instance
    Properties:
      PolicyName: Aarch64AccessPolicy
      PolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: 's3:ListBucket'
            Resource: !Sub 'arn:aws:s3:::${UniqueName}'
          - Effect: Allow
            Action: 's3:*Object'
            Resource: !Sub 'arn:aws:s3:::${UniqueName}/*'
          - Effect: Allow
            Action: 'ec2:CreateTags'
            Resource: !Sub 'arn:aws:ec2:${AWS::Region}:${AWS::AccountId}:instance/*'
            Condition:
              StringEquals:
                ec2:ResourceTag/Name: !Ref UniqueName
      Roles:
        - !Ref Aarch64AccessRole


  SSHSecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Enable SSH access via port 22
      SecurityGroupIngress:
      - CidrIp: 0.0.0.0/0
        FromPort: 22
        IpProtocol: tcp
        ToPort: 22

  Aarch64Instance:
    Type: AWS::EC2::Instance
    Properties:
      KeyName: !Ref UniqueName
      InstanceType: t4g.micro
      ImageId: ami-087fa126bfdebc5c3  # Ubuntu 18.04 Aarch64
      IamInstanceProfile: !Ref Aarch64InstanceProfile
      SecurityGroups:
        - !Ref SSHSecurityGroup
      Tags:
        - Key: Name
          Value: !Ref UniqueName
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash -xe
          instance_id=$(curl -s http://169.254.169.254/latest/meta-data/instance-id)
          apt update
          apt install -y awscli

          aws ec2 create-tags --resources $instance_id --region ${AWS::Region} --tags Key=Status,Value=CONFIGURING
          apt remove lxd-client -y
          snap install lxd
          lxd init --auto
          snap install snapcraft --classic

          aws ec2 create-tags --resources $instance_id --region ${AWS::Region} --tags Key=Status,Value=SNAPPING
          mkdir -p /tmp/robomaker_snap
          cd /tmp/robomaker_snap
          aws s3 cp s3://${UniqueName}/src src/ --recursive
          aws s3 cp s3://${UniqueName}/snap snap/ --recursive
          snapcraft --use-lxd
          aws s3 cp *.snap s3://${UniqueName}/

          aws ec2 create-tags --resources $instance_id --region ${AWS::Region} --tags Key=Status,Value=COMPLETE
          echo 'Exiting user data script'

Outputs:
  InstanceId:
    Value: !Ref Aarch64Instance
  1. Give execution permissions to the script.
chmod +x snap/local/aarch64_compile.sh
  1. Initiate the remote snapping.
./snap/local/aarch64_compile.sh

The shell script will upload your code to a private Amazon S3 bucket, provision necessary AWS resources for snapping, build the snap and then download it to your project’s root directory. Thus, with a single command and zero configuration, you obtain a snap built for the target architecture right in your project directory.

The build process will take about 20 minutes to complete. The script will keep you posted on its progress. The finished snap will finally be downloaded to your catkin workspace by the script.

Step 7: Create AWS IoT Greengrass component

We will now upload the snap to an Amazon S3 bucket and have our recipe refer to it. As of the date of writing this post, AWS IoT Greengrass 2.0 is still a very new addition to the AWS CLI and so we will also have to update the command line tools included in our IDE.

  1. Update AWS CLI tools to the latest version.
curl https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip -o awscliv2.zip
unzip awscliv2.zip && rm awscliv2.zip
sudo ./aws/install && rm -rf aws
  1. Upload the snap to the Amazon S3 bucket we created in Step 1. Replace the <S3_BUCKET> tags with the name you would like for the bucket.
aws s3 cp aws-hello-world_0.1_aarch64.snap s3://<S3_BUCKET>/
  1. Create a file named recipe.yaml and copy the following snippet into it.
---
RecipeFormatVersion: '2020-01-25'
ComponentName: com.snap.robomaker.HelloWorld
ComponentVersion: 1.0.0
ComponentDescription: AWS RoboMaker application to rotate a Turtlebot3
ComponentPublisher: Amazon
Manifests:
  - Platform:
      os: linux
      architecture: aarch64
    Lifecycle:
      Install:
        RequiresPrivilege: true
        Script: sudo snap install --devmode {artifacts:path}/aws-hello-world_0.1_aarch64.snap
      Run:
        Script: /snap/bin/hello-world.launch
    Artifacts:
      - Uri: s3://<S3_BUCKET>/aws-hello-world_0.1_aarch64.snap

This recipe provides instructions to AWS IoT Greengrass on where to find the snap, how to install the software, and how to launch the application once it is deployed to the robot.

  1. Open the file downloaded to the recipes/ directory and replace the <S3_BUCKET> tag under Artifacts with the name of the bucket holding your snap.
  2. Create the AWS IoT Greengrass component.
aws greengrassv2 create-component-version --inline-recipe fileb://recipe.yaml

The component will now appear listed on the Components page of the AWS IoT Greengrass console.

Photo depicting the component in the AWS console

Step 8: Deploy snap to your robot

AWS IoT Greengrass is now ready to deploy, install and launch our snap to the robot.

  1. Initiate the deployment. A configuration will be created and an OTA deployment will be triggered by AWS IoT Greengrass.
id=$( aws sts get-caller-identity --query Account --output text )
region=$( aws configure get region )
aws greengrassv2 create-deployment \
  --deployment-name TurtlebotDeployment \
  --target-arn arn:aws:iot:$region:$id:thing/Turtlebot \
  --components '{"com.snap.robomaker.HelloWorld":{"componentVersion": "1.0.0"}}'
  1. Track progress of the deployment on the device’s page in the AWS IoT Greengrass console. Edit the linked URL if you chose a different name for your robot in Step 2. Once the deployment finishes, the page will show our component as Running on the robot.

  1. Go back to your robot and confirm that the snap is installed on your robot. Since our ROS app is continuously publishing to the /cmd_vel topic, we can listen to it and confirm that the snap has been successfully launched.
aws-hello-world.echo

The console prints out a stream of Twist messages as expected.

Cleaning up

Before we finish, let us delete the AWS resources created during this demo so you do not incur unnecessary charges on your account.

  1. Delete the core device associate with the robot.
aws greengrassv2 delete-core-device --core-device-thing-name TurtleBot
  1. Delete the component associated with the snap.
id=$( aws sts get-caller-identity --query Account --output text )
region=$( aws configure get region )
aws greengrassv2 delete-component \
  --arn arn:aws:greengrass:$region:$id:components:com.snap.robomaker.HelloWorld:versions:1.0.0
  1. Find the AWS Cloud9 environment’s ID under Development environments in the AWS RoboMaker console.

  1. Then issue the command to delete the IDE.
aws cloud9 delete-environment --environment-id <ID>

Conclusion

In this blog, we discussed the benefits of a robust software packaging and delivery mechanism for roboticists, and how otherwise complex over-the-air deployments are made painless by AWS IoT Greengrass 2.0. The flexibility offered by custom recipes in AWS IoT Greengrass 2.0 allows users to deploy applications in whichever format they prefer while maintaining complete control over how the software behaves once it is on the target system.

The hands-on exercise demonstrated how ROS snaps can deployed to a robot over-the-air using AWS IoT Greengrass. Observe how we did not have to install ROS on the Pi for the snap to work; the containerized package carried everything it needed to run on the system without relying on external dependencies. This snap will now work on any Linux system it is installed on.

Thus, coupling AWS IoT Greengrass with snaps gives developers a fast, secure and highly reliable method to build, install, and run software on virtually any Linux system without the overhead of maintaining multiple configurations, customizing upstream libraries, or managing network communications.

Check out developer guides for AWS RoboMaker, AWS IoT Greengrass, and Snapcraft to dive deeper into these topics.