Cloud FormationでIoT Analyticsのワークフローの設定を行う方法

# Cloud FormationでIoT Analyticsのワークフローの設定を行う方法

AWS IoT Analytics（Analyticsと略す）の設定をCloud Formationで行う際にいろいろ躓いたポイントがあった為、備忘録として書いています。（2019/04/21現在）

この記事にたどり着いた方であれば各サービスの詳細は理解されていると思いますので、サービスの説明は行いません。

## 結論

この記事を必要としている人はすぐにでも結論が欲しいと思いますので、先に結論のYAMLデータを記載します。

``` CloudFormation-AnalyticsWorkflow.yaml
AWSTemplateFormatVersion: 2010-09-09
Resources:
  AnalyticsRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: iot.amazonaws.com
            Action: 'sts:AssumeRole'
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/CloudWatchFullAccess'
        - 'arn:aws:iam::aws:policy/CloudWatchLogsFullAccess'
      MaxSessionDuration: 3600
      Path: /
      RoleName: AnalyticsRole
    Metadata:
      'AWS::CloudFormation::Designer':
        id: 3a915d63-b53f-4014-8158-28ea71c94949
  MyLambdaRole:
    Type: 'AWS::IAM::Role'
    Properties:
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Principal:
              Service: lambda.amazonaws.com
            Action: 'sts:AssumeRole'
      ManagedPolicyArns:
        - 'arn:aws:iam::aws:policy/CloudWatchFullAccess'
        - 'arn:aws:iam::aws:policy/CloudWatchLogsFullAccess'
        - 'arn:aws:iam::aws:policy/AWSIoTAnalyticsFullAccess'
        - 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'
        - 'arn:aws:iam::aws:policy/AWSIoTFullAccess'
        - 'arn:aws:iam::aws:policy/service-role/AWSLambdaRole'
      MaxSessionDuration: 3600
      Path: /
      RoleName: MyLambdaRole
    Metadata:
      'AWS::CloudFormation::Designer':
        id: 99ea8723-ae9a-4478-b6ef-f0ef1df73a36
  MyChanel:
    Type: 'AWS::IoTAnalytics::Channel'
    Properties:
      ChannelName: my_chanel
      RetentionPeriod:
        Unlimited: true
    Metadata:
      'AWS::CloudFormation::Designer':
        id: accd8536-0d98-49ec-9cc3-43f6df5b3f79
  MyDatastore:
    Type: 'AWS::IoTAnalytics::Datastore'
    Properties:
      DatastoreName: my_datastore
      RetentionPeriod:
        Unlimited: true
    Metadata:
      'AWS::CloudFormation::Designer':
        id: 18e1feef-36e9-48d9-886a-4b651891b3d9
  MyFunction:
    Type: 'AWS::Lambda::Function'
    Properties:
      Code:
        ZipFile: |-
          exports.handler = async (event) => {
              console.log(event);
              return event;
          }
      FunctionName: MyFunction
      Handler: index.handler
      MemorySize: 128
      Role: !GetAtt 
        - MyLambdaRole
        - Arn
      Runtime: nodejs8.10
      Timeout: 3
    Metadata:
      'AWS::CloudFormation::Designer':
        id: cdf7bcde-8fb6-4c04-bee9-14dac73f45ac
  MyPipeline:
    Type: 'AWS::IoTAnalytics::Pipeline'
    Properties:
      PipelineName: my_pipeline
      PipelineActivities:
        - Channel:
            ChannelName: !Ref MyChanel
            Name: MyChanel
            Next: MyFunction
        - Lambda:
            Name: MyFunction
            Next: MyDatastore
            BatchSize: 1
            LambdaName: !Select 
              - '6'
              - !Split 
                - ':'
                - !GetAtt 
                  - MyFunction
                  - Arn
        - Datastore:
            DatastoreName: !Ref MyDatastore
            Name: MyDatastore
    Metadata:
      'AWS::CloudFormation::Designer':
        id: 31475aa9-b3ab-48d5-a1e7-9d9a5a627a05
  MyFunctionPermission:
    Type: 'AWS::Lambda::Permission'
    Properties:
      Action: 'lambda:InvokeFunction'
      FunctionName: !GetAtt 
        - MyFunction
        - Arn
      Principal: iotanalytics.amazonaws.com
    Metadata:
      'AWS::CloudFormation::Designer':
        id: 53ad796c-fbed-45ff-bb41-ab8286537cc4
  MyDataset:
    Type: 'AWS::IoTAnalytics::Dataset'
    Properties:
      DatasetName: my_dataset
      Actions:
        - ActionName: MyDataset
          QueryAction:
            SqlQuery: !Join 
              - ' '
              - - SELECT * FROM
                - !Ref MyDatastore
      RetentionPeriod:
        Unlimited: true
        NumberOfDays: 90
      Triggers:
        - Schedule:
            ScheduleExpression: cron(0/15 * * * ? *)
    Metadata:
      'AWS::CloudFormation::Designer':
        id: 5f53a313-5d81-42ef-b47d-655c6396191d
  MyTopicRule:
    Type: 'AWS::IoT::TopicRule'
    Properties:
      RuleName: MyTopicRule
      TopicRulePayload:
        Description: ''
        RuleDisabled: false
        AwsIotSqlVersion: 2016-03-23
        Sql: SELECT * FROM my/topic
        Actions:
          - IotAnalytics:
              ChannelName: !Ref MyChanel
              RoleArn: !GetAtt 
                - AnalyticsRole
                - Arn
    Metadata:
      'AWS::CloudFormation::Designer':
        id: a2fe4fdf-6c0c-455a-adb6-6c983e675d2f
Metadata:
  'AWS::CloudFormation::Designer':
    18e1feef-36e9-48d9-886a-4b651891b3d9:
      size:
        width: 60
        height: 60
      position:
        x: -210
        'y': 360
      z: 1
      embeds: []
    5f53a313-5d81-42ef-b47d-655c6396191d:
      size:
        width: 60
        height: 60
      position:
        x: -210
        'y': 260
      z: 1
      embeds: []
    accd8536-0d98-49ec-9cc3-43f6df5b3f79:
      size:
        width: 60
        height: 60
      position:
        x: -110
        'y': 260
      z: 1
      embeds: []
    99ea8723-ae9a-4478-b6ef-f0ef1df73a36:
      size:
        width: 60
        height: 60
      position:
        x: -10
        'y': 460
      z: 1
      embeds: []
    cdf7bcde-8fb6-4c04-bee9-14dac73f45ac:
      size:
        width: 60
        height: 60
      position:
        x: -110
        'y': 460
      z: 1
      embeds: []
    53ad796c-fbed-45ff-bb41-ab8286537cc4:
      size:
        width: 60
        height: 60
      position:
        x: -210
        'y': 460
      z: 1
      embeds: []
      isassociatedwith:
        - cdf7bcde-8fb6-4c04-bee9-14dac73f45ac
    31475aa9-b3ab-48d5-a1e7-9d9a5a627a05:
      size:
        width: 60
        height: 60
      position:
        x: -110
        'y': 360
      z: 1
      embeds: []
    3a915d63-b53f-4014-8158-28ea71c94949:
      size:
        width: 60
        height: 60
      position:
        x: -10
        'y': 360
      z: 1
      embeds: []
    a2fe4fdf-6c0c-455a-adb6-6c983e675d2f:
      size:
        width: 60
        height: 60
      position:
        x: -10
        'y': 260
      z: 1
      embeds: []
```

ここまでのコードで次図の環境を構築することができます。
![CloudFormation-AnalyticsWorkflow.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152941/f1dbac29-32bb-460c-3722-3ddf9d4d2532.png)
見える形で表示されると、なんかやる気でますよね。

## 作業手順

普通にAWS GUI上で作成を行った場合の手順は次の通りです。

1. IAM Roleを作成
2. IoT AnalyticsのChannelを作成
3. IoT AnalyticsのDatastoreを作成
4. IoT AnalyticsのPipeline用のLambdaを作成　※1
5. IoT AnalyticsのPipelineを作成　※2
6. IoT AnalyticsのPipelineがLambdaにPermissionを設定　※1
7. IoT AnalyticsのDatasetを作成
8. IoTのルールエンジンを作成

※1　Topicでデータを受けた後そのままDataStoreに格納する場合は不要
※2　Lambdaを利用しない、修正する必要がある

## 躓いたポイント

Cloud Formationで躓いたポイントは3つありました。

### WebのActivityとCloud FormationのActivityの不一致

最初にActivityをWeb上で設定するときの画面を見てみます。
![スクリーンショット 2019-04-20 23.31.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152941/0e7448d9-1f91-0bfa-6b7b-4a19dce966a8.png)

Web上では、ActivityはLambdaの設定しかありません。
実際の設定を覗いてもActivityは1つだけになってます。

![スクリーンショット 2019-04-20 23.43.45.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152941/e3c8ed19-cba6-955b-c621-b1d0cb5914ce.png)

しかしYAMLデータを見ると、Pipelineで登録する３つの作業はActivityとして登録されています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152941/2d898d45-7629-d7c0-46cb-79dca54f634e.png)
これが1個目の躓いたポイントでした。

公式リファレンスの[Activity](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-activity.html#cfn-iotanalytics-pipeline-activity-lambda)にはChannelとDatastoreは登場しているので、比較的納得のできる内容ではありましたが。。。

``` MyPipeline.yaml
  MyPipeline:
    Type: 'AWS::IoTAnalytics::Pipeline'
    Properties:
      PipelineName: my_pipeline
      PipelineActivities:
        - Channel:
            ChannelName: !Ref MyChanel
            Name: MyChanel
            Next: MyFunction
        - Lambda:
            Name: MyFunction
            Next: MyDatastore
            BatchSize: 1
            LambdaName: !Select 
              - '6'
              - !Split 
                - ':'
                - !GetAtt 
                  - MyFunction
                  - Arn
        - Datastore:
            DatastoreName: !Ref MyDatastore
            Name: MyDatastore
```

### PipelineのActivityには必須ではないパラメータは実質必須

次に、Channel、Lambda、Datastoreの共通パラメータを見てみましょう。
代表してChannelのAWS公式日本語リファレンス【[Pipeline.Properties.PipelineActivities.Channel](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-pipeline-channel.html)】をみてみましょう。

>
```
Name
    "チャネル" アクティビティの名前。
    必須: いいえ
    タイプ: 文字列
    更新に伴う要件: 中断はありません
Next
    パイプライン内の次のアクティビティ。
    必須: いいえ
    タイプ: 文字列
    更新に伴う要件: 中断はありません
```
>2019/04/21現在
>

配列で格納されているならば順番に呼び出されるだろうと勘違いをしていたため、この2つのプロパティに何の意味があるのかがわかりませんでした。
しかも2つとも必須ではないとされていたため余計に勘違いが加速してましたよね。。。

何なのかということですが、ただActivityを呼び出す順番の制御をしているだけだったのです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/152941/4d714da2-7935-3727-7f80-0ff95b146e37.png)
せめて、「`Name`と`Next`はActivityの呼び出す順番を制御するために利用している。次のActivityがない場合`Next`は記述しなくてもよい」ぐらいは書いてくれよ！！！一人逆ギレしていました。

### Analytics Datasetの必須ではないパラメータは実は必須

2つ目の躓きポイントで慣れたので躓いたわけじゃないです、少しリファレンスと違うので書いてます。

まずは、DatasetのAWS公式日本語リファレンス【[Dataset.Properties.RetentionPeriod](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-iotanalytics-dataset-retentionperiod.html)】を見てみましょう。

>
```
NumberOfDays
    メッセージデータが保持される日数。"unlimited" パラメータは false であることが必要です。
    必須: いいえ
    タイプ: 整数
    更新に伴う要件: 中断はありません
Unlimited
    true の場合、メッセージデータは無期限に保持されます。
    必須: いいえ
    タイプ: ブール値
    更新に伴う要件: 中断はありません
```
>2019/04/21現在
>

これはDatasetに登録されたデータの保存期限を決めるパラメータ群です。
無期限で保存したかったので`Unlimited: true`にして`NumberOfDays`は記述しませんでした。
そうしたらCloudFormationに「`NumberOfDays`は必須です」と突き返されてしまいました。。。


これには推測できていて、
このデフォルト値は90日期限となっています。
仮に無期限に設定したとしても、データとしてはデフォルトの期限情報が残っています。
なので、`NumberOfDays`パラメータは必須パラメータとして登録されているのだと思います。

## 最後に

1つ目の躓いたポイントは私の学習不足だったり、読む力といったものが影響して勝手に躓いたものでしたので、何も言えないです。しかし2つ目の躓いたポイントは避けようがない。。。
皆さんもリファレンスの落とし穴に嵌らないようにお気を付けください。

実は他のリファレンスと合わせて読むと解決するのではないかと思ったりしています。
私ではそれを見つけることができなかったので、リファレンスを見つけられた方がいましたら教えていただけると助かります。

実はCloud Formationを利用して4日目なので、説明が足りなかったり、ミスがありましたらコメントにてご指摘ください。
（ネストされたスタックとやらをつかうとCloud Formationの共通部品を括りだすことができるらしい？？？次はこれを調べてみて、記事にできればと思ってます。）