Greengrass(V1)を実行するDockerコンテナを構築してみる

#はじめに
以下の記事では、AWSが提供しているDockerイメージを利用してGreengrass(V1)のコンテナを構築しました。
参考：[Dockerコンテナ上でGreengrass(V1)を実行する](https://qiita.com/kt-yt/items/b0145fd2c707dba94b08)

今回はDockerfileを自身で用意し、Greengrass(V1)のコンテナを構築しました。
そのうえで、動作検証を行います。

**2021/5/18 22:00 追記**

* Greengrassグループの証明書取得・格納をしないとコンテナ内のGreengrassが起動しないところ、証明書なしでGreengrassを起動する手順になっていたので、順番を大幅修正
* AmazonのルートCAをwgetで取得する際のオプションを -o から -O に修正（-oだとPEMファイルではなくTXTとして取得してしまう）
* 抜けていた手順として「1-3　Dockerホストの設定」を追加しました

#1　作業環境
今回はEC2上にDockerをインストールし、そこでGreengrassを動かします。
##1-1　EC2インスタンスの準備
詳細な手順は割愛しますが、今回は以下の通りEC2インスタンスおよび周辺の環境を用意しています。
参考：[AWS上にエッジ環境を構築してGreengrass(V1)の動きを確認する - 1　AWS上にエッジ環境を構築](https://qiita.com/kt-yt/items/ebdf00c30d399057b73b#1aws%E4%B8%8A%E3%81%AB%E3%82%A8%E3%83%83%E3%82%B8%E7%92%B0%E5%A2%83%E3%82%92%E6%A7%8B%E7%AF%89)
###ネットワーク設定
今回は東京リージョンのap-northeast-1aに作成しています。

| 作成するもの | 詳細 |
|:-|:-|
| VPC | CIDR：10.0.0.0/16 |
| サブネット | CIDR：10.0.0.0/24 |
| インターネットゲートウェイ | VPCにアタッチする |
| ルートテーブル | 10.0.0.0/16 -> local , 0.0.0.0/0 -> IGW |
###インスタンスの作成
セキュリティグループは以下の通り作成しました。

| タイプ | ポート範囲 | ソース | 備考 |
|:-|:-|:-|:-|
| SSH | 22 | マイIP | 自身の環境からEC2にSSH接続できるようにするため |
| カスタムTCP | 8883 | 任意の場所 | IoT Coreとの通信用 |
インスタンスは以下の通り作成しました。

| 設定項目 | 選択した内容 |
|:-|:-|
| AMI | Ubuntu Server 18.04 LTS (HVM), SSD Volume Type（64ビット(x86)） |
| インスタンスタイプ | t2.micro |
| ボリュームサイズ | 20GB |
| ネットワーク | ※作成したVPC |
| サブネット | ※作成したサブネット |
| 自動割り当てパブリックIP | 有効 |
| セキュリティグループ | ※作成したセキュリティグループ |
##1-2　Dockerのインストール
インスタンスを起動したらssh接続してDockerをインストールします。
手順2以降は以下を参考にインストールします。
参考：[Install Docker Engine on Ubuntu](https://docs.docker.com/engine/install/ubuntu/)

1.`sudo apt-get update` を `apt-get upgrade` を実行する

```bash
$ sudo apt-get update -y

$ sudo apt-get upgrade -y
```
2.古いバージョンのDockerを削除する
※インストールされていないはずだが、念のため実行

```bash
$ sudo apt-get remove docker docker-engine docker.io containerd runc
```
3.レポジトリの設定を行う

```bash
$ sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg \
    lsb-release -y

$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

#参考手順のページでは「x86_64/amd64」タブよりコピペ
$ echo \
  "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \
  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
```
4.Dockerをインストールする

```bash
$ sudo apt-get update

$ sudo apt-get install docker-ce docker-ce-cli containerd.io -y

$ apt-cache madison docker-ce
 docker-ce | 5:20.10.6~3-0~ubuntu-bionic | https://download.docker.com/linux/ubuntu bionic/stable amd64 Packages
 docker-ce | 5:20.10.5~3-0~ubuntu-bionic | https://download.docker.com/linux/ubuntu bionic/stable amd64 Packages
 (後略)

#上記コマンドの結果より任意のバージョン（2列目）で<VERSION_STRING>を置換して以下コマンドを実行
$ sudo apt-get install docker-ce=<VERSION_STRING> docker-ce-cli=<VERSION_STRING> containerd.io
```
5.動作確認をする

```bash
$ docker --version
Docker version 20.10.6, build 370c289
```
6.ログインユーザであるubuntuでもDockerを実行できるようにする

```bash
#デフォルトではrootユーザでないと以下のエラーが表示される
$ docker run hello-world
docker: Got permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock: Post http://%2Fvar%2Frun%2Fdocker.sock/v1.24/containers/create: dial unix /var/run/docker.sock: connect: permission denied.
See 'docker run --help'.

#以下のコマンドを実行した後、再接続する
$ sudo gpasswd -a ubuntu docker
Adding user ubuntu to group docker

$ exit

#再接続後、コマンドが実行できることを確認する
$ docker run hello-wolrd
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:5122f6204b6a3596e048758cabba3c46b1c937a46b5be6225b835d091b90e46c
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.
（後略）
```
##1-3　Dockerホストの設定
IPv4のフォワーディング有効化とシンボリックリンクとハードリンクの保護を有効化します。
参考：[Docker コンテナでの AWS IoT Greengrass の実行](https://docs.aws.amazon.com/ja_jp/greengrass/v1/developerguide/run-gg-in-docker-container.html#docker-pull-image)

1.シンボリックリンクとハードリンクの保護を有効にする

```bash
#rootユーザに切り替え（sudoで実行してもPermission Deniedになったため）
$ sudo su -

#/etc/sysctl.confに設定を追加
$ echo '# AWS Greengrass' >> /etc/sysctl.conf 
$ echo 'fs.protected_hardlinks = 1' >> /etc/sysctl.conf 
$ echo 'fs.protected_symlinks = 1' >> /etc/sysctl.conf
```
2.IPv4 ネットワーク転送を有効にする

```bash
#ファイル末尾に「net.ipv4.ip_forward = 1」を追記
vim /etc/sysctl.conf
```

3.設定を反映する

```
$ sysctl -p
fs.protected_hardlinks = 1
fs.protected_symlinks = 1
net.ipv4.ip_forward = 1
```
#2　GreengrassをDocker上で起動
以下からダウンロードできる「aws-greengrass-docker-1.11.0.tar.gz」を解凍し、フォルダ内のDockerfileを参考にして、Dockerfileを作成し、イメージのビルド、コンテナの構築を行います。
参考：[AWS IoT Greengrass とは - AWS IoT Greengrass Docker ソフトウェア](https://docs.aws.amazon.com/ja_jp/greengrass/v1/developerguide/what-is-gg.html#gg-docker-download)
##2-1　DockerfileからDockerイメージを作成
Dockerfileを作成し、イメージのビルドを行います。

1.Docker用のディレクトリを作成し、必要なファイルを作成する

```bash
$ pwd
/home/ubuntu

$ mkdir docker

$ cd docker

$ touch Dockerfile

#ENTRYPOINTとして指定するシェルスクリプト
$ touch greengrass-entrypoint.sh

$ chmod 744 greengrass-entrypoint.sh
```
2.ENTRYPOINTとしてシェルスクリプトを以下の通り編集する
※今回は「aws-greengrass-docker-1.11.0.tar.gz」のものを流用
　if文のみ `[[ 条件式 ]]` 表記でエラーになったため `[ 条件式 ]` に変更
※Lambda実行がGreengrassコンテナ内の場合は、Greengrass起動前に終了
※Greengrass起動後は、Greengrassのdaemonが起動しているかチェックするループがずっと走ってプロセスが維持される

```sh:greengrass-entryoint.sh
#!/bin/sh

set -e

# Initial check before starting greengrassd
# Check if default IsolationMode is set to GreengrassContainer or any user lambdas are configured in GreengrassContainer mode
if grep -q "GreengrassContainer" /greengrass/ggc/deployment/group/group.json; then
    echo "Default IsolationMode as GreengrassContainer or User Lambdas with GreengrassContainer mode aren't supported to run inside the GGC Docker Container. For troubleshooting, start a fresh deployment by following this guide: https://docs.aws.amazon.com/greengrass/latest/developerguide/run-gg-in-docker-container.html#docker-no-container. Finally, restart the GGC docker container after bind-mounting an empty deployment folder."
    exit 1;
fi

/greengrass/ggc/core/greengrassd start

daemon_pid=`cat /var/run/greengrassd.pid`
# block docker exit until daemon process dies.
while [ -d /proc/$daemon_pid ]
do
 # Sleep for 1s before checking that greengrass daemon is still alive
 daemon_cmdline=`cat /proc/$daemon_pid/cmdline`
 if [ $daemon_cmdline != ^/greengrass/ggc/packages/1.11.0/bin/daemon.* ]; then
  sleep 1;
 else
  break;
 fi;
done
```
3.Dockerfileを作成する
※今回はamd64/ubuntuをベースにする
※Greengrassの実行にJavaが必要だが、今回はAmazonが提供するOpenJDKであるAmazon Corretoを利用する
　参考：[Amazon Corretto](https://aws.amazon.com/jp/corretto/)

```dockerfile:Dockerfile
FROM amd64/ubuntu

ARG GREENGRASS_RELEASE_URL=https://d1onfpft10uf5o.cloudfront.net/greengrass-core/downloads/1.11.1/greengrass-linux-x86-64-1.11.1.tar.gz
ARG CORRETTO_KEY_URL=https://apt.corretto.aws/corretto.key

RUN apt update -y && \
    apt-get install wget gnupg software-properties-common -y && \
    #Greengrass Coreのソフトウェアをwgetで取得し解凍
    wget $GREENGRASS_RELEASE_URL && \
    GREENGRASS_RELEASE=$(basename $GREENGRASS_RELEASE_URL) && \
    tar zxvf $GREENGRASS_RELEASE -C / && \
    #不要なtar.gzファイルの削除
    rm $GREENGRASS_RELEASE && \
    #amazon corretto8のインストール
    wget -O- $CORRETTO_KEY_URL | apt-key add - && \
    add-apt-repository 'deb https://apt.corretto.aws stable main' && \
    add-apt-repository ppa:deadsnakes/ppa && \
    apt-get update && \
    apt-get install -y java-1.8.0-amazon-corretto-jdk python3.7 && \
    #Greengrass用のシステムユーザとシステムグループを作成
    useradd -r ggc_user && \
    groupadd -r ggc_group && \
    #タイムゾーンをAsia/Tokyoに設定
    ln -sf /usr/share/zoneinfo/Asia/Tokyo /etc/localtime && \
    #aptのキャッシュをクリーン
    apt clean && \
    rm -rf /var/lib/apt/lists/*

#作成したgreengrass-entrypoint.shをコンテナ内にコピー
COPY "greengrass-entrypoint.sh" /

EXPOSE 8883

CMD /greengrass-entrypoint.sh
```
4.イメージをビルドする

```bash
$ docker build -t ubuntu-greengrass .

$ docker image ls
REPOSITORY          TAG       IMAGE ID       CREATED       SIZE
ubuntu-greengrass   latest    ************   3 days ago    534MB
```
##2-2　Greengrassグループの作成
クラウド側でグループを作成し、Greengrassの起動に必要となる証明書等を取得します。

1.IoT Coreのマネジメントコンソールで Greengrass > クラシック(V1) > 「グループの作成」の順にクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/3c9ed628-4d64-b8be-2552-8de07e78a5ba.png)
2.「デフォルト作成を使用」をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/e27f18bf-f33e-0e02-7d8c-86796dfcc5d2.png)
3.任意のグループ名を入力して「次へ」をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/ba7faaa7-a219-372d-655f-7f943c215276.png)
4.任意のCoreの名前を入力して「次へ」をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/bbb21ce4-71d2-70f9-a0aa-5f44686a1951.png)
5.「グループとCoreの作成」をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/faeea97b-7e36-9839-0646-da40a099106d.png)
6.「これらのリソースはtar.gzとしてダウンロードしてください」をクリックしてセキュリティリソースを取得したら、「完了」をクリック
※ルートCAの取得はのちの手順で行う
※Greengrass Coreのソフトウェアはコンテナ内で取得するのでここでは不要
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/219aeeba-c042-af12-6758-4edd751972ee.png)
##2-3　コンテナの起動
dockerコマンドでコンテナを起動すると長くなるので、docker-composeで実行できるようにします。

1.docker-compose をインストールする

```bash
$ sudo apt install docker-compose -y

$ docker-compose --version
docker-compose version 1.17.1, build unknown
```
2.コンテナ起動時にマウントするconfigや証明書を格納するディレクトリを作成する

```bash
$ cd /home/ubuntu

$ mkdir config certs log
```
3.先ほど取得したtar.gzファイルをアップロードする
※私はFileZillaで実施した
4.tar.gzファイルを解凍する

```bash
$ pwd
/home/ubuntu

$ ls
xxxxxxxxxx-setup.tar.gz  certs  config  docker  log

#解凍すると、certsディレクトリ、configディレクトリ配下に各ファイルが格納される
$ tar zxvf xxxxxxxxxx-setup.tar.gz

$ ls certs
xxxxxxxxxx.cert.pem  xxxxxxxxxx.private.key  xxxxxxxxxx.public.key

$ ls config
config.json

#tar.gzファイルは不要なので削除
$ rm xxxxxxxxxx-setup.tar.gz
```
4.AmazonのルートCAを取得する

```bash
$ cd certs

#config.jsonがデフォルトでroot.ca.pemと記載しているため、その通りの名前で取得する
$ wget "https://www.amazontrust.com/repository/AmazonRootCA1.pem" -O root.ca.pem
```
5.docker-compose.yml ファイルを作成する

```yml:docker-compose.yml
version: "3"

services:
    greengrass-core:
        image: ubuntu-greengrass
        volumes:
            - /home/ubuntu/config:/greengrass/config
            - /home/ubuntu/certs:/greengrass/certs
            - /home/ubuntu/log:/tmp
        ports:
            - 8883:8883
        restart: always
```
6.コンテナが起動することを確認する

```bash
#docker-compose.yml ファイルを格納したディレクトリに移動
$ cd docker

$ docker-compose up -d

#コンテナが起動していることを確認
$ docker ps -a
CONTAINER ID   IMAGE               COMMAND                  CREATED         STATUS                  PORTS                                       NAMES
xxxxxxxxxxxx   ubuntu-greengrass   "/bin/sh -c /greengr…"   3 seconds ago   Up 2 seconds            0.0.0.0:8883->8883/tcp, :::8883->8883/tcp   docker_greengrass-core_1

#コンテナ内に入る
$ docker-compose exec greengrass-core /bin/bash

#greengrassが起動していることを確認する
$ ps aux | grep -E "greengrass.*daemon"
root        12  0.1  1.9 566608 19356 ?        Sl   21:21   0:00 /greengrass/ggc/packages/1.11.1/bin/daemon -core-dir /greengrass/ggc/packages/1.11.1 -greengrassdPid 9
root       296  0.0  0.0   3304   728 pts/0    S+   21:23   0:00 grep --color=auto -E greengrass.*daemon

#コンテナから抜ける
$ exit
```
##2-4　Greengrassのグループ作成・デプロイ
Greengrassグループがデプロイできることを確認します。

1.IoT Coreのマネジメントコンソールで Greengrass > クラシック(V1) > グループ > 作成したグループ名 の順にクリック
2.設定をクリック
3.ロールの追加 をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/6eaee6b5-4cca-28ae-0ec5-10d68e60e792.png)
4.「Greengrass_ServiceRole」を選択して「保存」をクリック
5.（同じく設定の画面で）デフォルトのLamnda関数コンテナ化を「コンテナなし」に変更して「デフォルトのLambda実行設定を更新する」をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1001693/fa4312c4-15dd-a3c0-5bbd-8b8ce03dac48.png)
6.グループ右上のアクション > デプロイ の順にクリック
##2-5　備忘録（エラーについて）
今回、デプロイ時に以下のエラーが発生しました。
エラーメッセージに対してそれぞれ取った対応を備忘録として残しておきます。

**デフォルトのLambda関数コンテナ化が「Greengrassコンテナ」になっていた**
Lambdaを紐づける前だったので、設定に手を加えず上記の状態でデプロイをしたところ、以下のエラーが発生しました。
>Deployment xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx of type NewDeployment for group xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx failed error: mounting tmpfs at /greengrass/ggc/packages/1.11.1/dns failed: failed to mount with args source="tmpfs" dest="/greengrass/ggc/packages/1.11.1/dns" fstype="tmpfs" flags="0" data="": operation not permitted

上記のメッセージから理解して設定を変更したというわけではないのですが、結果的にデフォルトのLamnda関数コンテナ化を変更することで解消しました。
※いずれにしろ、デフォルトのLamnda関数コンテナ化を「Greengrassコンテナ」にしているとgreengrass-entrypoint.shはエラーになるはず

**コンテナにAmazon Correto（Java）をインストールしていなかった**
当初、Amazon Correttoをインストールせずにコンテナを起動していました。
その状態（かつ、上記エラーを解消した状態）でデプロイをしたところ、以下のエラーが発生しました。
> Deployment xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx of type NewDeployment for group xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx failed error: worker with xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx failed to initialize with reason Unable to find java or java8 executables

こちらはエラーメッセージの通りです。
コンテナにAmazon Correttoをインストールすることで解消しました。
#3　動作検証
コンテナ内でGreengrassが動くことを確認できたので、IoT Coreとコンテナ間でMQTTメッセージのやりとりができることを確認します。
今回はコンテナ上のGreengrassにMQTTメッセージを受け取ったら起動するLambdaをデプロイし、その際にローカルへのログの書き出しとIoT Coreへのメッセージ通知をさせます。
##3-1　Lambdaの作成
今回は以下のLambdaを作成します。
※詳細な手順は以下を参照のこと
　参考：[Dockerコンテナ上でGreengrass(V1)を実行する - 5-2　Lambdaの作成](https://qiita.com/kt-yt/items/b0145fd2c707dba94b08#5-2lambda%E3%81%AE%E4%BD%9C%E6%88%90)

| 設定項目 | 内容 |
|:-|:-|
| ランタイム | Python3.7 |
| ハンドラ | docker_test.docker_test ※[ファイル名].[関数名] |
| 備考 | バージョンを発行し、エイリアスと紐づけている |


コードは以下の通りです。

```python:docker_test.py
import greengrasssdk
import logging
import sys
import json
import datetime

logger = logging.getLogger(__name__)
logging.basicConfig(stream=sys.stdout, level=logging.DEBUG)

client = greengrasssdk.client("iot-data")

logfile = "/tmp/docker_test.log"

def docker_test(event, context):
    date = datetime.datetime.now()
    timestamp = date.strftime("%Y-%m-%d %H:%M:%S")
    
    client.publish(
        topic="test/dockertest",
        payload=json.dumps(
            {
                "function": "docker_test", 
                "message": "This is Test",
                "version":  "1.0",
                "timestamp": timestamp
            }
        )
    )
    
    f = open(logfile, mode="a")
    f.write(timestamp + " success!\n")
    f.close()
```
##3-2　グループとLambdaの紐づけ
作成したLambdaをGreengrassグループに紐づけます。
なお、今回は「コンテナなし」でLambdaを実行するため、ローカルリソースアクセスの設定は不要です。

1.IoT Coreのマネジメントコンソールで Greengrass > クラシック(V1) > グループ の順にクリック
2.対象のグループ名 > Lambda > 「Lambdaの追加」の順にクリック
3.「既存のLambdaを使用」 > 作成したLambdaを選択して「次へ」 > 作成したエイリアスを選択して「完了」をクリック
4.追加されたLambdaの「…」 > 設定の編集 の順にクリック
5.設定の以下項目を設定して、「更新」をクリック

* コンテナ化：グループのデフォルトを使用（現在：コンテナなし）
* Lambdaのライフサイクル：オンデマンド関数

##3-3　サブスクリプションの設定
LambdaとIoT Core間でメッセージのpub/subができるようにサブスクリプションを追加します。

1.IoT Coreのマネジメントコンソールで Greengrass > クラシック(V1) > グループ の順にクリック
2.対象のグループ名 > サブスクリプション > 「サブスクリプションの追加」の順にクリック
3.以下のサブスクリプションを作成する

| ソース | ターゲット | topic |
|:-|:-|:-|
| Lambda | IoT Cloud | test/dockertest ※Lambda内で指定したtopic |
| IoT Cloud | Lambda | ※任意のtopic |

以上の設定が完了したら、改めでグループをデプロイします。
##3-4　検証と結果
IoT Coreからサブスクリプションで指定したtopic宛にメッセージを送ります。

1.IoT Coreのマネジメントコンソールで テスト > MQTTテストクライアント の順にクリック
2.「トピックをサブスクライブする」タブでトピックのフィルターに「test/dockertest」（Lambda内で指定したtopic）と入力し、「サブスクライブ」をクリック
3.「トピックに公開する」タブでトピック名にサブスクリプションで指定したIoT Cloud -> Lambdaのtopicを入力し、「発行」をクリック
4.以下を確認する

* MQTTテストクライアントの「トピックをサブスクライブする」にメッセージが届く
* Dockerホスト `/home/ubuntu/log` 配下にdocker_test.logが作成されている

#おわりに
（Dockerの勉強も兼ねて）Dockerfileの作成からGreengrassを実行するコンテナ環境の作成、動作検証まで行いました。

なお、今回はAmazonが提供しているということで、OpenJDK8の代わりにAmazon Corretto 8を利用しましたが、StreamManager（公式ドキュメントの手順でOpenJDK8をインストールしている）もこの環境で動くことを確認できました。
参考：[Greengrass(V1)でストリームマネージャーを利用してみる](https://qiita.com/kt-yt/items/04e16f6e9d5fc5f11e4b)

また、記事内ではIoT Core -> Greengrass -> IoT CoreのMQTT通信についてのみ記載していますが、前回の記事同様にして仮想デバイスからのメッセージ契機でコンテナ上のGreengrassのLambdaが起動することを確認できました。
参考：[Dockerコンテナ上でGreengrass(V1)を実行する](https://qiita.com/kt-yt/items/b0145fd2c707dba94b08)

このイメージをベースにエッジ推論用の環境も用意してみようと思います。
#参考文献（文中には登場していないもの）
* [さわって学ぶクラウドインフラ　docker基礎からのコンテナ構築](https://www.amazon.co.jp/dp/B089VZXX63/ref=docs-os-doi_0)
* [Debian ベース Linux、RPM ベース Linux、Alpine Linux ディストリビューションに Amazon Corretto 8 をインストールする手順](https://docs.aws.amazon.com/ja_jp/corretto/latest/corretto-8-ug/generic-linux-install.html)