AWS IoT Greengrass 2.0 で Docker コンテナを動かしてみた

こちらは [AWS Containers Advent Calendar 2020](https://qiita.com/advent-calendar/2020/aws-containers) の23日目の記事です。

re:Invent 2020 で [AWS IoT Greengrass 2.0](https://aws.amazon.com/jp/blogs/news/announcing-aws-iot-greengrass-2-0-with-an-open-source-edge-runtime-and-new-developer-capabilities/) が発表されました。
v1 がそうであったように、v2 でも [Docker コンテナ](https://docs.aws.amazon.com/greengrass/v2/developerguide/run-docker-container.html) の動作がサポートされています。

さっそくやってみました。

# やったこと
- EC2 インスタンス上に AWS IoT Greengrass 2.0 をインストールする
- Docker コンテナを動かす(ここでは nginx コンテナを動かしました）


# AWS IoT Greengrass 2.0 のインストール on EC2
手元にラズパイはあるのですが、別の用途で使っているため、今回は EC2 インスタンスをたてて、そのうえで検証しました。
(t2.micro, x86, Amazon Linux2 の EC2 インスタンスを使っていますが、EC2インスタンスの作成手順は省略しています）

## EC2 へ必要なソフトウェアのインストール


## Core Device の作成
AWS IoT の管理コンソールで、Core device の作成を行います（画像右側のオレンジボタンです）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329408/8d98e8ec-8ba0-031e-213f-000c85cf7ba0.png)

Core Device名 と Things group名を入力します。
ここでは、それぞれ ```greengrass-v2-qiita-core``` と ```greengrass-v2-qiita-group``` としています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329408/11a3213c-57fa-3263-1fd3-df3c2c7aa603.png)


ページをスクロールするとインストール手順が表示されるため、1つ1つやって行きましょう。


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329408/1da21a00-f4eb-189c-3c71-0854ba240e78.png)

### Step0: 追加手順
私はこれはやらずに次に進んだため、ハマってしまいました。

```sudo visudo``` で以下のように root の設定を少しいじります。

```
root ALL=(ALL) ALL
↓
root ALL=(ALL:ALL) ALL
```

### Step1: Install Java on the device
[こちら](https://docs.aws.amazon.com/corretto/latest/corretto-8-ug/amazon-linux-install.html#amazon-linux-install-instruct)を参考に Java 8(Corretto 8) を入れます。

```
sudo amazon-linux-extras enable corretto8
sudo yum install -y java-1.8.0-amazon-corretto
```

### Step2: Configure AWS credentials on the device
Greengrass のインストール(インストール後の AWS IoT への登録など)には、AWSの認証情報が必要になります。
必要な Policy だったり、環境変数を使った設定方法がドキュメントに記載されています。
- https://docs.aws.amazon.com/greengrass/v2/developerguide/install-greengrass-core-v2.html?icmpid=docs_gg_console
- https://docs.aws.amazon.com/greengrass/v2/developerguide/install-greengrass-core-v2.html#provision-minimal-iam-policy

ただ、この検証では、EC2 インスタンスを使っていますので、IAM Role を EC2 インスタンスにアタッチすることで、完了しています。手順は省略
検証なので、アタッチした IAM Role には Administratorの権限が紐付いています（本番だと絶対にやってはいけないですね）


### Step3: Run the installer

表示されたコマンドをコピペして、立ち上げた EC2 インスタンスにSSHで入って、コマンドを実行して、Greengrass をインストールします。

```
curl -s https://d2s8p88vqu9w66.cloudfront.net/releases/greengrass-nucleus-latest.zip > greengrass-nucleus-latest.zip && unzip greengrass-nucleus-latest.zip -d GreengrassCore

Archive:  greengrass-nucleus-latest.zip
  inflating: GreengrassCore/bin/greengrass.service.template  
  inflating: GreengrassCore/bin/loader  
  inflating: GreengrassCore/conf/nucleus-build.properties  
  inflating: GreengrassCore/lib/Greengrass.jar  
```

```
sudo -E java -Droot="/greengrass/v2" -Dlog.store=FILE -jar ./GreengrassCore/lib/Greengrass.jar --aws-region us-east-1 --thing-name greengrass-v2-qiita-core --thing-group-name greengrass-v2-qiita-group --component-default-user ggc_user:ggc_group --provision true --setup-system-service true --deploy-dev-tools true

Creating user ggc_user 
ggc_user created 
Creating group ggc_group 
ggc_group created 
Added ggc_user to ggc_group 
Provisioning AWS IoT resources for the device with IoT Thing Name: [greengrass-v2-qiita-core]...
Creating new IoT policy "GreengrassV2IoTThingPolicy"
Creating keys and certificate...
Attaching policy to certificate...
Creating IoT Thing "greengrass-v2-qiita-core"...
Attaching certificate to IoT thing...
Successfully provisioned AWS IoT resources for the device with IoT Thing Name: [greengrass-v2-qiita-core]!
Adding IoT Thing [greengrass-v2-qiita-core] into Thing Group: [greengrass-v2-qiita-group]...
Successfully added Thing into Thing Group: [greengrass-v2-qiita-group]
Setting up resources for aws.greengrass.TokenExchangeService ... 
TES role alias "GreengrassV2TokenExchangeRoleAlias" does not exist, creating new alias...
IoT role policy "GreengrassTESCertificatePolicyGreengrassV2TokenExchangeRoleAlias" for TES Role alias not exist, creating policy...
Attaching TES role policy to IoT thing...
IAM policy named "GreengrassV2TokenExchangeRoleAccess" already exists. Please attach it to the IAM role if not already
Configuring Nucleus with provisioned resource details...
Downloading Root CA from "https://www.amazontrust.com/repository/AmazonRootCA1.pem"
Created device configuration
Successfully configured Nucleus with provisioned resource details!
Creating a deployment for Greengrass first party components to the thing group
Configured Nucleus to deploy aws.greengrass.Cli component
Successfully set up Nucleus as a system service

```


### AWS IoTの管理コンソールを見て、正常にインストールされていることを確認します
正常にインストールされていれば、図のように Core Device名 が表示されているはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329408/ab50c61a-ce54-2940-7649-868a96c6b165.png)



# Docker コンテナを動かす
## 前提条件を満たすためにいろいろする
### Docker のインストールと権限設定
Docker をインストールして、ggc_user に docker　を実行できる権限を付加します。
(greengrass 経由で実行されるdocker コマンドはこの ggc_user で実行されるためです）


```
sudo yum install -y docker
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -a -G docker ggc_user
sudo usermod -a -G docker ec2-user   #このコマンドは必須ではないですが、いちいち sudo をつけるのがめんどくさいので実行しています
```
usermod が有効になるように、一度 EC2インスタンスに入り直します。

## ローカルでの動作確認
ここでは local 環境でデプロイを行って、動作することを確認します
### docker image の用意
```
mkdir -p ~/GreengrassCore/artifacts/com.example.MyDockerComponent/1.0.0

docker pull public.ecr.aws/nginx/nginx
docker save public.ecr.aws/nginx/nginx > ~/GreengrassCore/artifacts/com.example.MyDockerComponent/1.0.0/nginx.tar
docker image rm public.ecr.aws/nginx/nginx   #デプロイ時にローカルの image を使わないように削除しておく
```

### recipe の用意
```
mkdir -p ~/GreengrassCore/recipes
touch ~/GreengrassCore/recipes/com.example.MyDockerComponent-1.0.0.yaml
```

```com.example.MyDockerComponent-1.0.0.yaml
---
RecipeFormatVersion: '2020-01-25'
ComponentName: com.example.MyDockerComponent
ComponentVersion: '1.0.0'
ComponentDescription: A component that runs a Docker container.
ComponentPublisher: Amazon
Manifests:
  - Platform:
       os: linux
    Lifecycle:
       Install:
         Script: docker load -i {artifacts:path}/nginx.tar
       Run:
         Script: docker run --rm -p 8080:80 public.ecr.aws/nginx/nginx
```

### ローカルでのデプロイ
```
sudo /greengrass/v2/bin/greengrass-cli deployment create \
  --recipeDir ~/GreengrassCore/recipes \
  --artifactDir ~/GreengrassCore/artifacts \
  --merge "com.example.MyDockerComponent=1.0.0"

# output
Dec 23, 2020 4:22:52 AM software.amazon.awssdk.eventstreamrpc.EventStreamRPCConnection$1 onConnectionSetup
INFO: Socket connection /greengrass/v2/ipc.socket:8033 to server result [AWS_ERROR_SUCCESS]
Dec 23, 2020 4:22:52 AM software.amazon.awssdk.eventstreamrpc.EventStreamRPCConnection$1 onProtocolMessage
INFO: Connection established with event stream RPC server
Local deployment submitted! Deployment Id: f86c969b-ce30-4f62-9cf4-fddb85987bc1
```

### ローカルデプロイの確認
ログを確認します(なんとかく docker run されていそうなのが分かります）


```
[ec2-user@ip-10-0-12-139 recipes]$ sudo tail -f  /greengrass/v2/logs/com.example.MyDockerComponent.log 
...
2020-12-23T05:43:26.313Z [INFO] (pool-2-thread-33) com.example.MyDockerComponent: shell-runner-start. {scriptName=services.com.example.MyDockerComponent.lifecycle.Install.Script, serviceName=com.example.MyDockerComponent, currentState=NEW, command=["docker load -i /greengrass/v2/packages/artifacts/com.example.MyDockerComponent..."]}
2020-12-23T05:43:32.796Z [INFO] (Copier) com.example.MyDockerComponent: stdout. Loaded image: public.ecr.aws/nginx/nginx:latest. {scriptName=services.com.example.MyDockerComponent.lifecycle.Install.Script, serviceName=com.example.MyDockerComponent, currentState=NEW}
2020-12-23T05:43:32.849Z [INFO] (pool-2-thread-33) com.example.MyDockerComponent: shell-runner-start. {scriptName=services.com.example.MyDockerComponent.lifecycle.Run.Script, serviceName=com.example.MyDockerComponent, currentState=STARTING, command=["docker run --rm -p 8080:80 public.ecr.aws/nginx/nginx"]}
2020-12-23T05:43:34.532Z [INFO] (Copier) com.example.MyDockerComponent: stdout. /docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration. {scriptName=services.com.example.MyDockerComponent.lifecycle.Run.Script, serviceName=com.example.MyDockerComponent, currentState=RUNNING}
2020-12-23T05:43:34.532Z [INFO] (Copier) com.example.MyDockerComponent: stdout. /docker-entrypoint.sh: Looking for shell scripts in /docker-entrypoint.d/. {scriptName=services.com.example.MyDockerComponent.lifecycle.Run.Script, serviceName=com.example.MyDockerComponent, currentState=RUNNING}
2020-12-23T05:43:34.544Z [INFO] (Copier) com.example.MyDockerComponent: stdout. /docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh. {scriptName=services.com.example.MyDockerComponent.lifecycle.Run.Script, serviceName=com.example.MyDockerComponent, currentState=RUNNING}
2020-12-23T05:43:34.569Z [INFO] (Copier) com.example.MyDockerComponent: stdout. 10-listen-on-ipv6-by-default.sh: info: Getting the checksum of /etc/nginx/conf.d/default.conf. {scriptName=services.com.example.MyDockerComponent.lifecycle.Run.Script, serviceName=com.example.MyDockerComponent, currentState=RUNNING}
2020-12-23T05:43:34.605Z [INFO] (Copier) com.example.MyDockerComponent: stdout. 10-listen-on-ipv6-by-default.sh: info: Enabled listen on IPv6 in /etc/nginx/conf.d/default.conf. {scriptName=services.com.example.MyDockerComponent.lifecycle.Run.Script, serviceName=com.example.MyDockerComponent, currentState=RUNNING}
2020-12-23T05:43:34.605Z [INFO] (Copier) com.example.MyDockerComponent: stdout. /docker-entrypoint.sh: Launching /docker-entrypoint.d/20-envsubst-on-templates.sh. {scriptName=services.com.example.MyDockerComponent.lifecycle.Run.Script, serviceName=com.example.MyDockerComponent, currentState=RUNNING}
2020-12-23T05:43:34.615Z [INFO] (Copier) com.example.MyDockerComponent: stdout. /docker-entrypoint.sh: Configuration complete; ready for start up. {scriptName=services.com.example.MyDockerComponent.lifecycle.Run.Script, serviceName=com.example.MyDockerComponent, currentState=RUNNING}

```

コンテナが動いていることも確認できますね！

```
docker ps

CONTAINER ID        IMAGE                        COMMAND                  CREATED             STATUS              PORTS                  NAMES
5f5281942e5b        public.ecr.aws/nginx/nginx   "/docker-entrypoint.…"   3 minutes ago       Up 3 minutes        0.0.0.0:8080->80/tcp   heuristic_sanderson
```

nginx も動いていることが確認できますね！

```
curl localhost:8080

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
```

さて、これでローカルで動くことが確認できたので、実際にマネジメントコンソールからデプロイを行いましょう

# デプロイ実施（準備編)
## ローカルのデプロイメントを削除します
これで起動している docker コンテナが消えるはずです

```
sudo /greengrass/v2/bin/greengrass-cli deployment create --remove="com.example.MyDockerComponent"
```

## tar の Docker イメージ を S3 にアップロードします
S3の場所は各自でお好きな場所を指定してください
この場合は、```s3://xxxxxx/greengrassv2/docker/nginx.tar``` に保存

```
aws s3 cp ~/GreengrassCore/artifacts/com.example.MyDockerComponent/1.0.0/nginx.tar s3://xxxxxx/greengrassv2/docker/
```

# デプロイ実施（デプロイ編)
デプロイの方法は、AWS CLI, SDK などいろいろあるようですが、ここでは マネジメントコンソールからデプロイを行いました。
## Component の作成
create component をクリック
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329408/c3d2f76d-e68b-f5f0-1e57-494e60915b9d.png)

Yamlを入力(ローカルデプロイのyamlと似ていますが、Artifacts という項目を追加しています）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329408/0cfe892f-cb10-3bd0-3461-a0624e232e39.png)

```yaml
---
RecipeFormatVersion: '2020-01-25'
ComponentName: com.example.MyDockerComponent
ComponentVersion: '1.0.0'
ComponentDescription: A component that runs a Docker container.
ComponentPublisher: Amazon
Manifests:
  - Platform:
       os: linux
    Lifecycle:
       Install:
         Script: docker load -i {artifacts:path}/nginx.tar
       Run:
         Script: docker run --rm -p 8080:80 public.ecr.aws/nginx/nginx
    Artifacts:
     - Uri: s3://xxxxx/greengrassv2/docker/nginx.tar
```

yaml を保存したら、Deploy をクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329408/e83661c0-038e-bf7d-3a14-7491caac229f.png)

初回インストールに作成されたデプロイメントを指定します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329408/0b4efb40-e678-5a43-d9d5-083ead305438.png)

作成した Components を作成します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329408/7c2175f2-a5cb-cd9e-dddf-72fe12452d2c.png)

特にオプションは設定せずに、Deploy を選択します
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/329408/9a368f5c-6232-b77f-2fb0-5ccfe0b95f6b.png)

これでデプロイされるはずので、動作確認をしてみましょう。

# 動作確認
docker コマンドで確認したり、localhostでアクセスしたりしてみます。


```
docker ps
CONTAINER ID        IMAGE                        COMMAND                  CREATED              STATUS              PORTS                  NAMES
4c3181250b11        public.ecr.aws/nginx/nginx   "/docker-entrypoint.…"   About a minute ago   Up About a minute   0.0.0.0:8080->80/tcp   recursing_montalcini
```

```
curl localhost:8080

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
    body {
        width: 35em;
        margin: 0 auto;
        font-family: Tahoma, Verdana, Arial, sans-serif;
    }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>
```

docker コンテナも動いてて、nginx の動作も確認できました！

# まとめ
発表された Greengrass 2.0 を使って、デバイス(今回はEC2インスタンス)に docker コンテナをデプロイしました。
今回は、EC2 インスタンスなので、有り難みはあまり感じないですが、これが IoT機器で、しかも、それが大量にあるという環境だとかなり便利に使えるのではないでしょうか？
現時点では、データの取得元(artifacts)は、S3 のみのため、docker image を tar にして S3 にアップロードするというのはやや残念ではありますが、まだリリースされたばかりなので今後のアップデートに期待ですね。