The Internet of Things on AWS – Official Blog

Managing Docker container lifecycle with AWS IoT Greengrass

Introduction

In this post, we will be discussing how to manage Docker container lifecycle using an AWS IoT Greengrass custom component. There are five phases in a Docker container lifecycle: create, run, pause/unpause, stop, and kill. The custom component interacts with the Docker Engine via the Docker SDK for Python to manage processes based on your use case, such as user initiated commands or an application sending commands.

AWS IoT Greengrass is an open source Internet of Things (IoT) edge runtime and cloud service that helps you build, deploy and manage IoT applications on your devices. You can use AWS IoT Greengrass to build edge applications using pre-built software modules, called components, that can connect your edge devices to AWS services or third-party services.

AWS IoT Greengrass components can represent applications, runtime installers, libraries, or any code that you would run on a device. You can configure AWS IoT Greengrass components to run a Docker container from images stored in the following locations:

  • Public and private image repositories in Amazon Elastic Container Registry (Amazon ECR)
  • Public Docker Hub repository
  • Public Docker Trusted Registry
  • Amazon S3 bucket

While Greengrass components have lifecycles of their own that you may interact with, these lifecycles do not support containerized processes. To start, stop, pause and resume a Docker container running on AWS IoT Greengrass, you can use commands such as Docker pause and Docker unpause via a Greengrass component running on a Greengrass core device. The custom lifecycle component, which we will refer to as the ‘lifecycle component’, consists of a Python script that subscribes to an AWS IoT Core MQTT topic and interacts with the Docker Engine.

Solution Overview

Below is an example workflow and architecture for one possible implementation. With these building blocks in hand you can further expand to fit your specific use case.

  1. A user deploys a Docker container component and the lifecycle component to the Greengrass core device.
  2. The application publishes a MQTT message to an AWS IoT Core topic. The MQTT message specifies the container name and desired action to be performed. In this example, we send a start command to the container named env.
  3. The custom lifecycle component is subscribed to the topic.
  4. The lifecycle component receives the message and then interacts with the Docker Engine via the Docker SDK for Python and executes the desired command on the container name specified.
  5. Based on the command received from the lifecycle component, the Docker Engine will pause, unpause, start, or stop the specified container.

Solution Diagram

Solution Diagram

Implementation Instructions

Prerequisites

  1. The AWS CLI is installed and configured.
  2. AWS Greengrass Core installed on your device.
  3. Docker Engine is installed and running.

Deploy a Docker container to a Greengrass Core device

Follow the instructions on how to run a Docker container with AWS Greengrass or optionally create and run a container with the Docker Engine itself. Be sure to provide a name for the container, in this example we use the name of env.

Verify you have a running Docker container and that it has the desired name:

docker container ls

Create the custom lifecycle component

To create a Greengrass component we need to create the Python script that will contain our code and also a Greengrass recipe which will specify the deployment details when the component is deployed to a Greengrass Core device.

  1. Create an empty folder and script file named customlifecycle.py.
    mkdir -p ~/artifacts && touch ~/artifacts/customlifecycle.py
  2. In your favorite Integrated Development Environment (IDE), open customlifecycle.py and paste the following code. Be sure to save the file. Note: the code snippet below is under an MIT-0 license and is available on Github.
    #Imports
    import time
    import json
    import traceback
    import docker
    import subprocess
    import awsiot.greengrasscoreipc
    import awsiot.greengrasscoreipc.client as client
    from awsiot.greengrasscoreipc.model import (
        IoTCoreMessage,
        QOS,
        SubscribeToIoTCoreRequest
    )
    
    TIMEOUT = 10
    ipc_client = awsiot.greengrasscoreipc.connect()
    topic = "docker"
    qos = QOS.AT_MOST_ONCE
    
    #IPC Stream Handler
    class StreamHandler(client.SubscribeToIoTCoreStreamHandler):
        def __init__(self):
            super().__init__()
    
        def on_stream_event(self, event: IoTCoreMessage) -> None:
            message = json.loads(event.message.payload.decode())
            
            try:
                client = docker.from_env()
                name = message["name"]
                command = message["command"]
            
                if command == "start":
                    container = client.containers.get(name)
                    container.start()
                    print("Starting container: " + name)
            
                elif command == "pause":
                    container = client.containers.get(name)
                    result = json.loads(container.pause())
                    print(result)
                    print("Pausing container: " + name)
                    
                elif command == "unpause":
                    container = client.containers.get(name)
                    print(container.unpause())
                    print("Unpausing container: " + name)
                    
                elif command == "stop":
                    container = client.containers.get(name)
                    container.stop()
                    print("Stopping container: " + name)
                    
                else:
                    print("Error")
                
            except:
                with tempfile.TemporaryFile() as tmp:
                tmp.write("Docker Error")
                    
        def on_stream_error(self, error: Exception) -> bool:
            message_string = "Error!"
    
            return True
    
        def on_stream_closed(self) -> None:
            pass
            
    #Initiate Subscription
    request = SubscribeToIoTCoreRequest()
    request.topic_name = topic
    request.qos = qos
    handler = StreamHandler()
    operation = ipc_client.new_subscribe_to_iot_core(handler)
    future = operation.activate(request)
    future_response = operation.get_response()
    future_response.result(TIMEOUT)
    
    while True:
        time.sleep(1)
    
    operation.close()
    
  3. Create a bucket and retrieve your bucket name using the following command.
    EPOCH_TIME=$(date +"%s") && S3_BUCKET=lifecycle-component-$EPOCH_TIME && aws s3 mb s3://$S3_BUCKET
  4. Execute the following command to create a folder and a file to put the recipe into.
    mkdir -p ~/recipes && touch ~/recipes/customlifecycle-1.0.0.json
  5. Open the created recipe file customlifecycle-1.0.0.json and paste the following contents. Replace [YOUR BUCKET NAME] with the bucket name retrieved in the step 3.
    {
        "RecipeFormatVersion": "2020-01-25",
        "ComponentName": "Docker-lifecycle-component",
        "ComponentVersion": "1.0.0",
        "ComponentType": "aws.greengrass.generic",
        "ComponentDescription": "A component that interacts with Docker daemon.",
        "ComponentPublisher": "Amazon",
        "ComponentConfiguration": {
          "DefaultConfiguration": {
            "accessControl": {
              "aws.greengrass.ipc.mqttproxy": {
                "docker_lifecycle:mqttproxy:1": {
                  "policyDescription": "Allows access to subscribe to all topics.",
                  "operations": [
                    "aws.greengrass#SubscribeToIoTCore"
                  ],
                  "resources": [
                    "*"
                  ]
                }
              }
            }
          }
        },
        "Manifests": [
          {
            "Lifecycle": {
              "Install": "pip3 install awsiotsdk",
              "Run": "python3 -u {artifacts:path}/customlifecycle.py"
            },
            "Artifacts": [
              {
                "Uri": "s3://[YOUR BUCKET NAME]/customlifecycle.py"
              }
            ]
          }
        ]
      }
    
  6. Upload the component artifacts to Amazon Simple Storage Service.
    aws s3 cp --recursive ~/artifacts/ s3://$S3_BUCKET/
  7. Next, we will publish the Greengrass component by running the following command.
    cd ~/recipes && aws greengrassv2 create-component-version --inline-recipe fileb://customlifecycle-1.0.0.json
  8. You can now see this has been added to your AWS IoT Console -> Greengrass -> Components -> My Components.AWS Greengrass components

Deploy the custom lifecycle component

Now we will deploy the custom lifecycle component to your Greengrass Core device using the AWS CLI. Deployments may be applied to Things or Thing Groups. In this case, we will apply the deployment directly to the Greengrass Core thing entity.

  1. Create a deployment manifest folder and file using the command below.
    mkdir -p ~/deployments && touch ~/deployments/gg_deployment.json
  2. In your IDE, copy and paste the below into the gg_deployment.json file. Update the [targetARN] with your Thing ARN. You may retrieve your Thing ARN from the AWS IoT Core console. Be sure to save the file.
    {
      "targetArn": "[targetArn]",
      "deploymentName": "Deployment for Custom Docker Lifecycle",
      "components": {
        "Docker-lifecycle-component": {
          "componentVersion": "1.0.0"
        }
      }
    }
    
  3. Create the deployment with the following command.
    cd ~/deployments && aws greengrassv2 create-deployment --cli-input-json file://gg_deployment.json
  4. Verify that the component is now running on your Greengrass Core device. It may take several minutes for it to instantiate.
    sudo /greengrass/v2/bin/greengrass-cli component list

Test the Custom Lifecycle component

  1. Go to  AWS IoT Core console, select the MQTT test client.
  2. Select Publish to topic.
  3. In the Topic name, enter docker
  4. In the Message payload, copy in the message below. The command syntax will depend on the name and current state of your container.
    {
      "command":"start",
      "name":"env"
    }
  5. Verify that the state of your container has changed.
    docker container ls

Conclusion

In this blog post, we explored how to use AWS IoT Greengrass to control a Docker container’s lifecycle. This was achieved using a custom component that subscribes to an AWS IoT Core MQTT topic and uses the message contents to execute commands against the Docker daemon with the Docker SDK for Python.

To take a deeper dive with AWS IoT Greengrass, including building Greengrass components, check out our AWS IoT Greengrass V2 Workshop!