AWS CFn(CDK)でRobomakerを使ってROSをデプロイするときのハマりポイント

# はじめに

AWS Robomakerを用いてROSプロジェクトのデプロイ管理は非常に便利ですが、その仕組みについて完全に理解していないといくつかハマりポイントが存在します。わたしのプロジェクトではCDKを用いて各デバイスごとにCFnのスタックを作成し、それらが依存するスタックでRobomakerのリソースを作成してデプロイまで行っています。今回その中でドキュメントを読んでもなかなか理解できずにハマってしまったポイントをまとめました。

# GreengrassとRobomaker関連

## Greengrass Groupに設定するRoleについて

Greengrass Groupに設定する[Role](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-greengrass-group.html#cfn-greengrass-group-rolearn
)は、Lambda関数とコネクタがAWSサービスとやり取りするために必要な権限であり、今回Robomakerで必要な権限は、[これ](https://docs.aws.amazon.com/robomaker/latest/dg/create-robot.html)となります。

【2020/08/11】
ドキュメントには記載がありませんので、必要な方はAWSサポートにもご確認ください。

## Greengrassのログ設定について

Greengrassに設定する[Logger](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-greengrass-loggerdefinitionversion-logger.html#cfn-greengrass-loggerdefinitionversion-logger-component)は、`GreengrassSystem`と`Lambda`の2種類あり、これらは[TYPE](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-greengrass-loggerdefinitionversion-logger.html#cfn-greengrass-loggerdefinitionversion-logger-type)であるとおり、`FileSystem`か`AWSCloudWatch`を指定します。

AWSのガイドなどでマネージメントコンソールからリソースを作成した場合、ログが自動的にFileSystemにはかれていたため、設定しないとログがでないと気が付きにくいですが、設定しないと下記のようなWARNが発生します。

```localwatch.log
[2020-06-11T17:47:37.219+09:00][DEBUG]-will keep the log files for the following lambdas {"readingPath": "/home/ggc_user/greengrass/ggc/var/log/user", "lambdas": "map[]"}
[2020-06-11T17:47:37.219+09:00][WARN]-failed to list the user log directory {"path": "/home/ggc_user/greengrass/ggc/var/log/user"}

[2020-06-11T18:04:55.47+09:00][DEBUG]-will keep the log files for the following lambdas {"readingPath": "/home/ggc_user/greengrass/ggc/var/log/user/ap-northeast-1/*************", "lambdas": "map[aws-robomaker-deployment-function-x86_DO_NOT_DELETE:true]"}
[2020-06-11T18:04:55.47+09:00][WARN]-failed to list the user log directory {"path": "/home/ggc_user/greengrass/ggc/var/log/user/ap-northeast-1/*************"}
```

また、これらのログ出力先を`CloudWatch`にする場合は、前述のGreengrassのRoleに対して権限を与える必要があります。今回はCDKで実装しているため、最終的な実装は下記のようになりました。

``` robomaker-deploy-role.ts
this.robo_role = new iam.Role(scope, 'RoboRole', {
    roleName: 'RoboRole',
    assumedBy: new iam.CompositePrincipal(
        new iam.ServicePrincipal("lambda.amazonaws.com"),
        new iam.ServicePrincipal("greengrass.amazonaws.com")
    )
});

const s3_bucket_policy = new iam.PolicyStatement({
    effect: iam.Effect.ALLOW,
    actions: [
        "s3:List*",
        "s3:Get*"
    ],
    resources: [
        "arn:aws:s3:::mybucket/ros_bundle/*"
    ]
});

const robomaker_deploy_policy = new iam.PolicyStatement({
    effect: iam.Effect.ALLOW,
    actions: [
        "robomaker:UpdateRobotDeployment"
    ],
    resources: [
        "*"
    ]
});

const cloudwatch_log_policy = new iam.PolicyStatement({
    effect: iam.Effect.ALLOW,
    actions: [
        "logs:CreateLogGroup",
        "logs:CreateLogStream",
        "logs:PutLogEvents",
        "logs:DescribeLogStreams"
    ],
    resources: [
        this.getServiceResourceArn("logs", "log-group:", "aws/greengrass/GreengrassSystem/*"),
        this.getServiceResourceArn("logs", "log-group:", "aws/greengrass/Lambda/*")
    ]
});

const robo_policy = new iam.Policy(scope, "RoboPolicy");
robo_policy.addStatements(s3_bucket_policy);
robo_policy.addStatements(robomaker_deploy_policy);
robo_policy.addStatements(cloudwatch_log_policy);
this.robo_role.attachInlinePolicy(robo_policy);
```

※現地点での`AWSLambdaBasicExecutionRole`には、`DescribeLogStreams`がなくエラーが出てしまったためポリシーステートメントを作成しています。

```AWSLambdaBasicExecutionRole.json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "*"
        }
    ]
}
```

## Robomakerでデプロイ時のLambdaの実行権限をstackからは変更できない

Greengrass FunctionDefinitionVersionの`DefaultConfig`である[RunAs](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-greengrass-functiondefinitionversion-execution.html#cfn-greengrass-functiondefinitionversion-execution-runas)にデフォルトである`ggc_user`や`ggc_group`の代わりに実行させるための設定、今回はroot権限(UID/GID=0)を設定しましたが、Robomakerからデプロイを行うとこの設定は無効になります。

【2020/08/11】
ドキュメントには記載がありませんので、必要な方はAWSサポートにもご確認ください。

マネージメントコンソールから設定した場合、CFnのstackでRobomakerのデプロイを変えた場合やGreengrassの設定を変更すると再設定が必要となり、100+のような台数を管理する場合には非常に面倒です。そのため、今回は`/etc/passwd`の`ggc_user`に対して下記の設定をしています。

```
ggc_user:x:0:0::/home/ggc_user:/bin/false
```


## RobomakerのROSデプロイ前に実行するファイルのインストールモジュールについて

[こちら](https://docs.aws.amazon.com/robomaker/latest/dg/create-deployment-job.html)の9項に下記の設定をするとありますが、

```CMakeLists.txt
install(FILES deploymentScripts/post_launch_script.sh
  DESTINATION ${CATKIN_GLOBAL_SHARE_DESTINATION}
)
```

この設定をすべきROSのモジュールは、launchファイルが存在するモジュールと同じでなければいけません。ドキュメントを読んだ限り同一モジュールという指定がなかったためlaunchモジュールと別であっても、ROSのPATHが通っているため検索を行うかと勘違いしていましたが、launchで指定したモジュール内のみを使用するため注意が必要です。

またここでCMakeのinstall時のパーミッションですが、[こちら](https://cmake.org/cmake/help/v3.18/command/install.html)を確認すると、設定しない場合`644`となりますが、Robomakerが実行前に`chmod +x`を実行するため、特に設定する必要はないです。しかしシェルの内部でまた別のシェルを呼び出して実行する場合、そのシェルに対して権限を設定する必要があります。

## RobomakerのPrelaunch fileの実行時のPATHについて

開発時にUbuntuなどを用いている場合のデフォルトPATHは下記のようになります。

```
$ cat /etc/environment 
PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games"
```

しかしRobomakerのLambdaから実行されるシェルにおいてPATHは、下記のようになります。

```
[2020-06-17T10:56:56.616+09:00][INFO]-PATH=/home/ggc_user/roboMakerDeploymentPackage/******************/opt/ros/melodic/bin:/opt/ros/melodic/bin:/home/ggc_user/roboMakerDeploymentPackage/******************/usr/sbin:/home/ggc_user/roboMakerDeploymentPackage/******************/usr/bin:/home/ggc_user/roboMakerDeploymentPackage/******************/usr/local/bin:/home/ggc_user/roboMakerDeploymentPackage/******************/sbin:/home/ggc_user/roboMakerDeploymentPackage/******************/bin:/usr/bin:/usr/local/bin:/bin:/home/ggc_user/greengrass/ggc/deployment/lambda/arn.aws.lambda.ap-northeast-1.***********.function.aws-robomaker-deployment-function-x86_DO_NOT_DELETE.3
```

通常の実行では問題になりませんが、例えばこのシェルでROSで使用するカーネルモジュールのロードを行いたいときに`insmod`や`rmmod`を使用しますが、これらのコマンドは、`/sbin`にあり

```
$ which insmod
/sbin/insmod
```

デフォルトで設定されているrobomakerのlambdaのPATHには、設定されていません。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/559128/73aaf4c3-e6b0-d1f3-333b-105bc659b178.png)

そのため、必要なPATHはシェルの内部で自分で設定を追記する必要があります。

```
export PATH="${PATH}:/sbin"
```

このような原因でローカルでの実行では問題ないが、なぜかRobomaker経由だと動かないということがありました。

## CFnの1度の更新でRobotに紐づくFleetの変更ができない

これは非常に気が付きにくいですが、[Fleet](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-robomaker-robot.html#cfn-robomaker-robot-fleet)の変更時の更新方法は、`Replacement`のため、CFnはまず新しいフリートにおいてRobotリソースを作成しようとしますが、ここに設定している`GreengrassGroupId`は、同時に複数のRobotに紐づくことを許可していないため、下記のエラーが発生します。

```
 2/5 | 4:57:17 PM | UPDATE_FAILED        | AWS::RoboMaker::Robot            |
Greengrass GroupId is already in use by: ******** (Service: AWSRoboMaker; Status Code: 400; Error Code: ResourceAlreadyExistsException;)
```

そのため、Fleet変更には必ずRobotの削除という前ステップが必要となります。またこのときに削除されたRobotでデプロイされていたROSアプリケーションがAWSリソースが削除されたときにどのような挙動になるかは、公開されていません。

【2020/08/11】
ドキュメントには記載がありませんので、必要な方はAWSサポートにもご確認ください。

## IAM PolicyとIoT PolicyではCFnでの更新方法が異なる

IAM Policyの[PolicyDocument](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iam-policy.html#cfn-iam-policy-policydocument)の更新方法は、`No interruption`ですが、IoT Policyの[PolicyDocument](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-policy.html#cfn-iot-policy-policydocument)の更新方法は、`Replacement`です。つまりIoT Policyの権限が変わるような場合、PolicyNameも同じタイミングで変更しなければ、[こちら](https://aws.amazon.com/jp/premiumsupport/knowledge-center/cloudformation-custom-name/)のエラーが発生してしまいます。同じPolicyでもそれぞれ挙動が異なるため、ドキュメントは正確に読む必要があります。

## Greengrassのコアが認識されない

こちらのマネージメントコンソールからデプロイしている場合に気が付きにくいですが、証明書とエンドポイントだけ設定すれば自動で認識されるわけではありません。[こちら](https://docs.aws.amazon.com/ja_jp/greengrass/latest/developerguide/configs-core.html)の4項にあるようにコアの検索を定義する必要があります。CDKで行う場合には、Greengrassの[Function](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-greengrass-functiondefinitionversion-function.html)に[こちら](https://docs.aws.amazon.com/ja_jp/greengrass/latest/developerguide/gg-core.html)の7項で説明がある設定をします。

CDKで行う場合には下記のようになります。

```greengrass-function.ts
new CfnFunctionDefinitionVersion(scope, 'GGFuncDefVer', {
    functionDefinitionId: func.ref,
    functions: [
        {
            functionArn: "arn:aws:lambda:::function:GGIPDetector:1",
            functionConfiguration: {
                pinned: true,
                memorySize: 32768,
                timeout: 3
            },
            id: 'GGLambda'
        }
    ],
})
```

# その他について

## Stack TagsとResource Tagsについて

CDKの[stacks](https://docs.aws.amazon.com/cdk/latest/guide/stacks.html)には、

> stack.tags – Returns a TagManager that you can use to add or remove stack-level tags. This tag manager tags all resources within the stack, and also tags the stack itself when it's created through AWS CloudFormation.

とありますが、これはスタックタグの上にリソースタグが上書きされるのか、リソースタグの上にスタックタグが上書きされるのか明示的に書かれておらずソースを読んで確認する必要があります。

今回使用しているバージョンはv1.32.2なので、下記のリポジトリより

https://github.com/aws/aws-cdk


stack tagsとresource tagsの設定している部分を探す。

https://github.com/aws/aws-cdk/blob/v1.32.2/packages/%40aws-cdk/core/lib/cfn-resource.ts#L320


そこではstack tagsに対してresource tagsがオーバライドされていることが確認できます。

https://github.com/aws/aws-cdk/blob/v1.32.2/packages/@aws-cdk/core/lib/cfn-resource.ts#L402

そもそもCDKのタグの実装はec2のeipであれば、

```
/**
 * `AWS::EC2::EIP.Tags`
 * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-eip.html#cfn-ec2-eip-tags
 */
readonly tags?: cdk.CfnTag[];
```

greengrassのgroupの場合は、下記の注意書きのようにanyであり

> この Json プロパティタイプは、キーと値のペアのマップとして処理されます。次の形式を使用します。これは、AWS CloudFormation テンプレートのほとんどの Tags 実装とは異なります。

```
/**
 * `AWS::Greengrass::Group.Tags`
 * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-greengrass-group.html#cfn-greengrass-group-tags
 */
readonly tags?: any;
```

robomakerのfleetについては、特に特記事項がありませんがanyです。

```
/**
 * `AWS::RoboMaker::Fleet.Tags`
 * @see http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-robomaker-fleet.html#cfn-robomaker-fleet-tags
 */
readonly tags?: any;
```

これがCFnではどうやって扱われているかというと

通常の[JSONフォーマット](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-resource-tags.html)は下記であり

```
"Tags" : [
   {
      "Key" : "keyname1",
      "Value" : "value1"
   },
   {
      "Key" : "keyname2",
      "Value" : "value2"
   }
]
```

greengrass系の[Tags](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-greengrass-group.html#cfn-greengrass-group-tags
)は、下記のようになります。

```
"Tags": {
    "KeyName0": "value1",
    "KeyName1": "value2",
    "KeyName2": "value3"
}
```

## CDKでまとめてデプロイするときに依存関係が自動的に解決されない

下記のissuesで議論中です。依存関係のあるスタックの更新については、手動で実行をする必要があります。

https://github.com/aws/aws-cdk/issues/7602



# まとめ

* CDKはいま絶賛開発中であり、ドキュメントや事例も少なくソースコードやissueを読まなければわからないことも多いです
* AWSのマネージメントコンソールからCDKを用いたCFnへの移行は結構なコストが必要であり、今回はAWSサポートに問い合わせをしながら2ヶ月近く実装に時間がかかりました
* AWSリソースのプロビジョニングを自動でできるようになり、Greengrass Coreのプロビジョニングをrc.localに設定したスクリプトによって実行することによって、完全に自動化されたロボットのプロビジョニングができるようになりました

CDKやGreengrass、Robomakerなどはやってみた系の記事が多いですが、いざ実際に本番環境で動かすことを想定して準備するとわからないことがたくさんあり大変でした。これでインフラまわりが完全に自動化されたことでロボットのスケールなども簡単に管理できるようになりました。