Deploying Docker Compose with Greengrass!
This post shows how to deploy applications that use public or private ECR-hosted Docker images, built into AWS IoT Greengrass custom components!
- Docker images that can be privately hosted in Amazon Elastic Container Registry (ECR); and
- AWS IoT Greengrass components containing Docker Compose files.
- Ask the user to install Python, the
pip
dependencies needed to run the application, and give instructions on downloading and running the application; or - Ask the user to install Docker, then provide a single command to download and run your application.
1
git clone https://github.com/mikelikesrobots/greengrass-docker-compose.git
1
2
3
4
aws --version
gdk --version
jq --version
docker --version
1
aws sts get-caller-identity
1
sudo docker run hello-world
docker-compose
is currently used.1
2
3
4
5
# For the script version
docker-compose --version
# For the plugin version
docker compose --version
jq
-sudo apt install jq
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"ecr:GetAuthorizationToken",
"ecr:BatchGetImage",
"ecr:GetDownloadUrlForLayer"
],
"Resource": [
"*"
],
"Effect": "Allow"
}
]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"Version": "2012-10-17",
"Statement": [
{
"Action": [
"s3:GetObject"
],
"Resource": [
"*"
],
"Effect": "Allow"
}
]
}
python-hello-world:latest
. It expects an ECR repository of the same name to exist. We can create this by navigating to the ECR console and clicking "Create repository". Set the name to python-hello-world and keep the settings default otherwise, then click Create repository. This should create a new entry in the Repositories list:Copy the URI from the repo and strip off the
python-hello-world
ending to get the BASE_URI
. Then, back in your cloned repository, open the .env
file and replace the ECR_REPO
variable with your base URL. It should look like the following:1
ECR_REPO=012345678901.dkr.ecr.us-west-2.amazonaws.com
1
./build_all.sh
1
source ../../.env && ./build.sh
1
./publish_all.sh
1
sudo vim /greengrass/v2/logs/com.docker.PythonHelloWorld.log
1
2
3
4
5
2024-01-19T21:46:03.514Z [INFO] (Copier) com.docker.PythonHelloWorld: stdout. [36mpython-hello-world_1 |^[[0m Received new message on topic /topic/local/pubsub: Hello from local pubsub topic. {scriptName=services.com.docker.PythonHelloWorld.lifecycle.Run.Script, serviceName=com.docker.PythonHelloWorld, currentState=RUNNING}
2024-01-19T21:46:03.514Z [INFO] (Copier) com.docker.PythonHelloWorld: stdout. [36mpython-hello-world_1 |^[[0m Successfully published 999 message(s). {scriptName=services.com.docker.PythonHelloWorld.lifecycle.Run.Script, serviceName=com.docker.PythonHelloWorld, currentState=RUNNING}
2024-01-19T21:46:03.514Z [INFO] (Copier) com.docker.PythonHelloWorld: stdout. [36mpython-hello-world_1 |^[[0m Received new message on topic /topic/local/pubsub: Hello from local pubsub topic. {scriptName=services.com.docker.PythonHelloWorld.lifecycle.Run.Script, serviceName=com.docker.PythonHelloWorld, currentState=RUNNING}
2024-01-19T21:46:03.514Z [INFO] (Copier) com.docker.PythonHelloWorld: stdout. [36mpython-hello-world_1 |^[[0m Successfully published 1000 message(s). {scriptName=services.com.docker.PythonHelloWorld.lifecycle.Run.Script, serviceName=com.docker.PythonHelloWorld, currentState=RUNNING}
2024-01-19T21:46:05.306Z [INFO] (Copier) com.docker.PythonHelloWorld: stdout. [36mcomdockerpythonhelloworld_python-hello-world_1 exited with code 0. {scriptName=services.com.docker.PythonHelloWorld.lifecycle.Run.Script, serviceName=com.docker.PythonHelloWorld, currentState=RUNNING}
components
, docker
) and a couple of important scripts (build_all.sh
, publish_all.sh
).components
contains all of the Greengrass components, where each component goes in a separate folder and is built using GDK. We can see this from the folder inside, com.docker.PythonHelloWorld
.docker
contains all of the Docker images, where each image is in a separate folder and is built using Docker.build_all.sh
and publish_all.sh
, but if we take a look inside, we see that both scripts source the .env
file, then goes through all Docker folders followed by all Greengrass folders, for each one executing the build.sh
or publish.sh
script inside. The only exception is for publishing Greengrass components, where the standard gdk component publish
command is used directly instead of adding an extra file.1
2
topic = os.environ.get("MQTT_TOPIC", "example/topic")
message = os.environ.get("MQTT_MESSAGE", "Example Hello!")
python
, we add the application code into the app
directory, and then specify the entrypoint as the main.py
script.docker build
command with a default tag of the ECR repo. The publish step does slightly more work by logging in to the ECR repository with Docker before pushing the component. Note that both scripts use the ECR_REPO
variable set in the .env
file.python-hello-world
image. We can then update the image name in the build and publish scripts and change the application code and Dockerfile as required. A new ECR repo will also be required, matching the name given in the build and publish scripts.docker-compose.yml
that will be deployed using Greengrass.1
2
3
find . -maxdepth 1 -type f -not -name "*.sh" -exec sed -i "s/{ECR_REPO}/$ECR_REPO/g" {} \;
gdk component build
find . -maxdepth 1 -type f -not -name "*.sh" -exec sed -i "s/$ECR_REPO/{ECR_REPO}/g" {} \;
ECR_REPO
placeholder with the actual repo, then build the component with GDK, then replace that value back to the placeholder. As a result, the built files are modified, but the source files are changed to their original state.zip
. We could push only the Docker Compose file, but this method allows us to zip other files that support it if we want to extend the component. We also have the version tag, which needs to be incremented with new component versions.python-hello-world
Docker image built by docker/python-hello-world
by specifying the image name.latest
tag of python-hello-world. If you want your Greengrass component version to be meaningful, you should extend the build scripts to give a version number as the Docker image tag, so that each component version references a specific Docker image version.MQTT_TOPIC
and MQTT_MESSAGE
environment variables that need to be passed to the container. These can be overridden in the recipe.yaml
by Greengrass configuration, allowing us to pass configuration through to the Docker container.1
2
3
4
5
environment:
- SVCUID
- AWS_GG_NUCLEUS_DOMAIN_SOCKET_FILEPATH_FOR_COMPONENT
volumes:
- ${AWS_GG_NUCLEUS_DOMAIN_SOCKET_FILEPATH_FOR_COMPONENT}:${AWS_GG_NUCLEUS_DOMAIN_SOCKET_FILEPATH_FOR_COMPONENT}
python-hello-world
, and change our environment variables and image tags. Note that we don't need to reference images stored in ECR - we can also access public Docker images!1
2
3
4
ComponentConfiguration:
DefaultConfiguration:
Message: "Hello from local pubsub topic"
Topic: "/topic/local/pubsub"
1
- URI: "docker:{ECR_REPO}/python-hello-world:latest"
1
2
- URI: "s3://BUCKET_NAME/COMPONENT_NAME/COMPONENT_VERSION/com.docker.PythonHelloWorld.zip"
Unarchive: ZIP
{artifacts:decompressedPath}/com.docker.PythonHelloWorld/
path.1
2
3
4
5
6
7
Lifecycle:
Run:
RequiresPrivilege: True
Script: |
MQTT_TOPIC="{configuration:/Topic}" \
MQTT_MESSAGE="{configuration:/Message}" \
docker-compose -f {artifacts:decompressedPath}/com.docker.PythonHelloWorld/docker-compose.yml up
MQTT_TOPIC
and MQTT_MESSAGE
as environment variables to the docker-compose
command. With the up command, we tell the component to start the application in the Docker Compose file.