[AWS CloudFormation][AWS SAM] FleetProvisioningテンプレート定義

# 構文

以下が[公式ドキュメント](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-iot-provisioningtemplate.html#cfn-iot-provisioningtemplate-preprovisioninghook)に記載されている構文

```yaml
Resources:
  ProvisioningTemplate: # 任意の名前
    Type: AWS::IoT::ProvisioningTemplate
    Properties: 
      Description: String # 説明
      Enabled: Boolean # フリートプロビジョニングテンプレート有効/無効
      PreProvisioningHook: # プロビジョニング時にフックするLambda
        ProvisioningHook
      ProvisioningRoleArn: String # [必須] デバイスをプロビジョニングする権限を持ったロールARN
      Tags: # フリートプロビジョニングテンプレートの管理に使用できるメタデータ
        Tags
      TemplateBody: String # [必須] フリートプロビジョニングテンプレートのJSON形式のコンテンツ(JSON)
      TemplateName: String # テンプレート名
```

以下、実際に値を入れたもの

```yaml
Resources:
  ProvisioningTemplate:
    Type: AWS::IoT::ProvisioningTemplate
    Properties:
      Enabled: true
      ProvisioningRoleArn: !GetAtt ProvisioningRole.Arn
      TemplateName: !Sub '${AWS::StackName}_Template'
      TemplateBody: |
        {
          "Parameters": {
            "SerialNumber": {
              "Type": "String"
            },
            "AWS::IoT::Certificate::Id": {
              "Type": "String"
            }
          },
          "Resources": {
            "certificate": {
              "Properties": {
                "CertificateId": {
                  "Ref": "AWS::IoT::Certificate::Id"
                },
                "Status": "Active"
              },
              "Type": "AWS::IoT::Certificate"
            },
            "policy": {
              "Properties": {
                "PolicyName": "xxxxxxxxxx5D8828xxxxxxxxxx52CExxxxxxxxxx"
              },
              "Type": "AWS::IoT::Policy"
            },
            "thing": {
              "OverrideSettings": {
                "AttributePayload": "MERGE",
                "ThingGroups": "DO_NOTHING",
                "ThingTypeName": "REPLACE"
              },
              "Properties": {
                "AttributePayload": {},
                "ThingGroups": [],
                "ThingName": {
                  "Fn::Join": [
                    "",
                    [
                      "fleet-provisioning-test-",
                      {
                        "Ref": "SerialNumber"
                      }
                    ]
                  ]
                },
                "ThingTypeName": "fleet-provisioning-test"
              },
              "Type": "AWS::IoT::Thing"
            }
          }
        }
```

`TemplateBody`のJSONはマネージメントコンソールでフリートプロビジョニングテンプレートを作成し、生成されたテンプレートのJSONをコピペしたもの

# プロビジョニングロールの定義

`ProvisioningRoleArn`で参照しているロールは以下のように定義

```yaml
  ProvisioningRole:
    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/service-role/AWSIoTThingsRegistration
```

`ManagedPolicyArns`でAWS管理ポリシーのARNを指定
マネージメントコンソールでフリートプロビジョニングテンプレートを作成した場合に、自動で設定されるAWS管理ポリシーのARNを設定

`Principal`で権限を与える対象としてiotを指定

# プロビジョニング前にフックするLambdaの設定

[公式ドキュメント](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-iot-provisioningtemplate-provisioninghook.html)より以下のような構文になる

```yaml
Resources:
  ProvisioningTemplate:
    Type: AWS::IoT::ProvisioningTemplate
    Properties: 
      PreProvisioningHook: # プロビジョニング時にフックするLambda
        TargetArn: String # フックするLambdaのARN
      ProvisioningRoleArn: String
      TemplateBody: String
```

以下、実際に値を入れたもの

```yaml
Resources:
  ProvisioningTemplate:
    Type: AWS::IoT::ProvisioningTemplate
    Properties:
      Enabled: true
      PreProvisioningHook: # 追加
        TargetArn: !GetAtt FleetProvisioningHook.Arn
      ProvisioningRoleArn: !GetAtt ProvisioningRole.Arn
      TemplateName: !Sub '${AWS::StackName}_Template'
      TemplateBody: |
        ...
       
  FleetProvisioningHook: # Lambda関数定義
    Type: AWS::Serverless::Function
      ...
```

上記に加えて、定義したLambda関数を実行できる権限をIoTに与える必要があるため以下を追加

```yaml
  LambdaAddPermission:
      Type: AWS::Lambda::Permission
      Properties:
        Action: lambda:InvokeFunction
        FunctionName: !GetAtt FleetProvisioningHook.Arn # LambdaのARN
        Principal: iot.amazonaws.com # iotからの実行を許可
```

# プロビジョニングテンプレート（JSON）の内容変更

## クレーム証明書の設定

(2020/11/05 追記)
下記`"SerialNumber"`、`"AWS::IoT::Certificate::Id"`はデバイスから受信する値のため`"Default"`は設定不要
(2020/11/05 追記終了)

以下のようにJSON内"AWS::IoT::Certificate::Id"`の`"Default"`に証明書IDを設定

```json
          "Parameters": {
            "SerialNumber": {
              "Type": "String"
            },
            "AWS::IoT::Certificate::Id": {
              "Type": "String",
              "Default": "xxxxxxxxxx8d192xxxxxxfb22xxxxxxxxx5b8f6d148cdaafb5506xxxxxxxxxx"
            }
          },
```

## プロビジョニングしたデバイスに付与するポリシーの設定

[公式ドキュメント](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-iot-policy.html)（CloudFormationのポリシー定義方法について）

1．既にAWSに作成済ポリシーを使用
      以下のようにポリシーのARNを指定

```json
            "policy": {
              "Properties": {
                "PolicyName": "xxxxxxxxxx5D8828xxxxxxxxxx52CExxxxxxxxxx"
              },
              "Type": "AWS::IoT::Policy"
            },

```

2．新規でポリシーを作成
        以下のように`PolicyDocument`でポリシーを定義する

```json
            "policy": {
              "Properties": {
                "PolicyDocument": {
                  "Version": "2012-10-17",
                  "Statement": [
                    {
                      "Effect": "Allow",
                      "Action": "iot:*",
                      "Resource": [
                        "arn:aws:iot:ap-northeast-1:36xxxxxxxxxx:client/*",
                        "arn:aws:iot:ap-northeast-1:36xxxxxxxxxx:topic/*",
                        "arn:aws:iot:ap-northeast-1:36xxxxxxxxxx:topicfilter/*"
                      ]
                    }
                  ]
                }
              },
              "Type": "AWS::IoT::Policy"
            },
```

### リージョン、アカウントIDに変数を使用してみる（失敗）

前述のポリシー定義ではリージョン（`ap-northeast-1`）、アカウントID（`36xxxxxxxxxx`）が変わった場合にテンプレートの変更が必要となるため
以下のように設定してみたが、PolicyDocumentの構文異常でデバイス登録時に失敗

```json
            "policy": {
              "Properties": {
                "PolicyDocument": {
                  "Version": "2012-10-17",
                  "Statement": [
                    {
                      "Effect": "Allow",
                      "Action": "iot:*",
                      "Resource": [
                        {"Fn::Sub" : "arn:aws:iot:${AWS::Region}:${AWS::AccountId}:client/*"},
                        {"Fn::Sub" : "arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topic/*"},
                        {"Fn::Sub" : "arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topicfilter/*"}
                      ]
                    }
                  ]
                }
              },

```

以下のように`Parameters`でリージョン、アカウントIDを定義した場合も`Parameters`には文字列以外設定できないためエラー

```json
          "Parameters": {
            "AccountID" : {
              "Type": "String",
              "Default": {"Fn::Sub" : "${AWS::AccountId}"}
            },
            "Region" : {
              "Type": "String",
              "Default": {"Fn::Sub" : "${AWS::Region}"}
            }
          },
          "Resources": {
            "policy": {
              "Properties": {
                "PolicyDocument": {
                  "Version": "2012-10-17",
                  "Statement": [
                    {
                      "Effect": "Allow",
                      "Action": "iot:*",
                      "Resource": [
                        {"Fn::Sub" : "arn:aws:iot:${Region}:${AccountID}:client/*"},
                        {"Fn::Sub" : "arn:aws:iot:${Region}:${AccountID}:topic/*"},
                        {"Fn::Sub" : "arn:aws:iot:${Region}:${AccountID}:topicfilter/*"}
                      ]
                    }
                  ]
                }
              },
              "Type": "AWS::IoT::Policy"
            }
          }
```

[公式ドキュメント](https://aws.amazon.com/jp/premiumsupport/knowledge-center/cloudformation-template-validation/)でも「組み込み関数の Fn::Sub が含まれています。この組み込み関数により、検証エラーが発生します。「すべての Default メンバーは文字列でなければなりません」。」と記載があり、この方法は不可


### リージョン、アカウントIDに変数を使用してみる（成功）(2020/11/03 追加)

プロビジョニングテンプレートの内容（`TemplateBody`）をJSON形式文字列として、`!Sub`関数を使って変数を埋め込むことで対応することができた
これによって、リージョン、アカウントIDが入力不要になった

```yaml
Resources:
  ProvisioningTemplate:
    Type: AWS::IoT::ProvisioningTemplate
    Properties:
      Enabled: true
      PreProvisioningHook:
        TargetArn: !GetAtt FleetProvisioningHook.Arn
      ProvisioningRoleArn: !GetAtt ProvisioningRole.Arn
      TemplateName: !Sub '${StackName}_ProvisioningTemplate'
      TemplateBody: !Sub |
        {
          "Parameters": {
            "SerialNumber": {
              "Type": "String"
            },
            "AWS::IoT::Certificate::Id": {
              "Type": "String"
            }
          },
          "Resources": {
            "certificate": {
              "Properties": {
                "CertificateId": {
                  "Ref": "AWS::IoT::Certificate::Id"
                },
                "Status": "Active"
              },
              "Type": "AWS::IoT::Certificate"
            },
            "policy": {
              "Properties": {
                "PolicyDocument": {
                  "Version": "2012-10-17",
                  "Statement": [
                    {
                      "Effect": "Allow",
                      "Action": "iot:*",
                      "Resource": [
                        "arn:aws:${AWS::Region}:${AWS::AccountId}:client/*",
                        "arn:aws:${AWS::Region}:${AWS::AccountId}:topic/*",
                        "arn:aws:${AWS::Region}:${AWS::AccountId}:topicfilter/*"
                      ]
                    }
                  ]
                }
              },
              "Type": "AWS::IoT::Policy"
            },
            ...

```

# クレーム証明書にポリシーをアタッチする（2020/11/05 追記）

クレーム証明書の生成と、IoT Coreへの登録は手動で行うとして、
クレーム証明書に必要なポリシーのアタッチをCloudFormationで行う

## 手動でポリシーをアタッチした場合

以下のようにプロビジョニングテンプレートのアクションから「アクセス許可を編集」をクリック
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695430/cda61e1e-d946-be9e-3d84-98cb471714e6.png)

以下のようにポリシーをアタッチする証明書を選択して、「ポリシーをアタッチ」をクリック
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/695430/6781ecc2-5521-9e00-973d-7761badaff55.png)

## CloudFormationでポリシーをアタッチした場合

以下が、アタッチするポリシーと、ポリシーをアタッチするリソース定義

```yaml
  CertificateAttachPolicy:
    Type: AWS::IoT::Policy
    Properties:
      PolicyName: !Sub '${StackName}_CertificateAttachPolicy'
      PolicyDocument: !Sub |
        {
          "Version": "2012-10-17",
          "Statement": [
            {
              "Effect": "Allow",
              "Action": [
                "iot:Connect"
              ],
              "Resource": [
                "*"
              ]
            },
            {
              "Effect": "Allow",
              "Action": [
                "iot:Publish",
                "iot:Receive"
              ],
              "Resource": [
                "arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topic/$aws/certificates/create/*",
                "arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topic/$aws/provisioning-templates/${StackName}_ProvisioningTemplate/provision/*"
              ]
            },
            {
              "Effect": "Allow",
              "Action": [
                "iot:Subscribe"
              ],
              "Resource": [
                "arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topicfilter/$aws/certificates/create/*",
                "arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topicfilter/$aws/provisioning-templates/${StackName}_ProvisioningTemplate/provision/*"
              ]
            }
          ]
        }

  PolicyAttachment:
    Type: AWS::IoT::PolicyPrincipalAttachment
    Properties: 
      PolicyName: !Ref CertificateAttachPolicy
      Principal: !Sub "arn:aws:iot:${AWS::Region}:${AWS::AccountId}:cert/${CertificateId}"
```

`CertificateAttachPolicy`がアタッチするポリシーリソース定義、
`PolicyAttachment`がAWS IoT ポリシーをプリンシパル (X.509 証明書または別の証明書) にアタッチするためのリソース定義
`PolicyAttachment`内で使用している`${CertificateId}`は別途`Parameters`セクションで定義が必要

CloudFormation（`AWS::IoT::PolicyPrincipalAttachment`）を使用してアタッチした場合、
対象のスタックを削除した場合、自動的にデタッチされる

ポリシーのみCloudFormationで定義し、手動でポリシーをアタッチした場合、
アタッチされたポリシーがCloudFormationから操作できなくなりスタックの更新、削除でエラーとなるため注意

# `PolicyDocument`に`${iot:Connection.Thing.ThingName}`を使用する（2020/11/05 追記）

[公式ドキュメント](https://docs.aws.amazon.com/iot/latest/developerguide/connect-and-pub.html)にあるように、以下のような`${iot:Connection.Thing.ThingName}`を使用したポリシー定義に変更する

```json

{
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Action":["iot:Publish"],
        "Resource": ["arn:aws:iot:us-east-1:123456789012:topic/${iot:Connection.Thing.ThingName}"]
      },
      {
        "Effect": "Allow",
        "Action": ["iot:Connect"],
        "Resource": ["arn:aws:iot:us-east-1:123456789012:client/${iot:Connection.Thing.ThingName}"]
      }
    ]
}
```

前述した以下のプロビジョニングテンプレート定義内の`"arn:aws:${AWS::Region}:${AWS::AccountId}:client/*"`部分を変更する

```yaml
      TemplateBody: !Sub |
        {
          "Parameters": {
            "SerialNumber": {
              "Type": "String"
            },
            "AWS::IoT::Certificate::Id": {
              "Type": "String"
            }
          },
          "Resources": {
            "certificate": {
              "Properties": {
                "CertificateId": {
                  "Ref": "AWS::IoT::Certificate::Id"
                },
                "Status": "Active"
              },
              "Type": "AWS::IoT::Certificate"
            },
            "policy": {
              "Properties": {
                "PolicyDocument": {
                  "Version": "2012-10-17",
                  "Statement": [
                    {
                      "Effect": "Allow",
                      "Action": "iot:*",
                      "Resource": [
                        "arn:aws:${AWS::Region}:${AWS::AccountId}:client/*", <= この"*"を変更する
                        "arn:aws:${AWS::Region}:${AWS::AccountId}:topic/*",
                        "arn:aws:${AWS::Region}:${AWS::AccountId}:topicfilter/*"
                      ]
                    }
                  ]
                }
              },
              "Type": "AWS::IoT::Policy"
            },
            ...
```

単純に
`"arn:aws:${AWS::Region}:${AWS::AccountId}:client/${iot:Connection.Thing.ThingName}"`
とした場合は、`!Sub`を使用してるため、`iot:Connection.Thing.ThingName`が見つからずエラーとなる

そのため、一旦以下のように`Parameters`で`${iot:Connection.Thing.ThingName}`を文字列として定義してから、
`"arn:aws:iot:${AWS::Region}:${AWS::AccountId}:client/${ConnectionThingName}"`とする

```yaml
Parameters:
  ConnectionThingName:
    Type: String
    Default: '${iot:Connection.Thing.ThingName}'

Resources:
  ProvisioningTemplate:
    Type: AWS::IoT::ProvisioningTemplate
    Properties:
      ...
      TemplateBody: !Sub |
        {
          "Parameters": {
            "SerialNumber": {
              "Type": "String"
            },
            "AWS::IoT::Certificate::Id": {
              "Type": "String"
            }
          },
          "Resources": {
            "certificate": {
              "Properties": {
                "CertificateId": {
                  "Ref": "AWS::IoT::Certificate::Id"
                },
                "Status": "Active"
              },
              "Type": "AWS::IoT::Certificate"
            },
            "policy": {
              "Properties": {
                "PolicyDocument": {
                  "Version": "2012-10-17",
                  "Statement": [
                    {
                      "Effect": "Allow",
                      "Action": "iot:*",
                      "Resource": [
                        "arn:aws:iot:${AWS::Region}:${AWS::AccountId}:client/${ConnectionThingName}",
                        "arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topic/*",
                        "arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topicfilter/*"
                      ]
                    }
                  ]
                }
              },
              "Type": "AWS::IoT::Policy"
            },
```

`"arn:aws:iot:${AWS::Region}:${AWS::AccountId}:client/${ConnectionThingName}"`
は、デプロイ後、以下のように変換される
`"arn:aws:iot:ap-northeast-1:xxxxxxxxxxxx:client/${ConnectionThingName}"`