ラズパイとAWS IoTを使った見守りシステム自作で学んだこと（８）template.yamlで定義するAWSリソースの説明とsam deploy

[（２）見守りProjectの設計（名前の設定）の最後に出てきた./template/template.yaml](https://qiita.com/nsaito9628/items/da402829316549853d0f#templatetemplateyaml)の説明です。

「[（４）cliでAWS IoTに新しいモノを登録し証明書を発行](https://qiita.com/nsaito9628/items/2ff6ab6d471537270e68)」で行うモノの登録と証明書の発行以外の全てAWSリソースをSAMで実装します。

<img width= 1200 src = https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565396/82f3ebce-0a2c-d7ca-6530-49d7e3846ec6.png>

前稿まで以上に理解が浅い部分で、見逃せない間違いがありましたらご指摘頂けると幸いです。検証を繰り返してブラッシュアップは継続します。

## 実行環境
今回はS3、CloudFront、Lambda、CloudWatchDashboard、IoTRule、IAM policy、IAM Roleをsam cliで実装しますが、IAM userが実行に[必要なポリシー](https://github.com/nsaito9628/Monitoring-system-with-Raspberry-Pi-and-AWS/tree/main/user%20policy)を[README](https://github.com/nsaito9628/Monitoring-system-with-Raspberry-Pi-and-AWS#preparation)に案内してあります。

随所でcliがーと言いつつ以下はAWS IAMのマネジメントコンソールよりIAM userにアタッチしました。

#### AWS管理ポリシー
IAM userにAWSIoTFullAccessをアクセス権限追加します。

#### ユーザー定義の管理ポリシー
/Monitoring-system-with-Raspberry-Pi-and-AWS/user policyに保存してある8つのポリシーテキストを使ってAWS IAMコンソールより自分カスタムのポリシーを作成してIAM userに権限を追加します。

ファイル中の{your accountID}の部分は自分のアカウントIDに置き換えます。

また、複数の試作で使い回しているため若干不要な権限が含まれている可能性があります。流用されることがありましたら自己責任でお願いいたします。
```sh
/home/user/Monitoring-system-with-Raspberry-Pi-and-AWS
・
┣ user policy
┃ ┣ SAM-Changeset-Create-Policy
┃ ┣ SAM-CloudWatch-Custom-Dashboard-Create-policy
┃ ┣ SAM-Cloudformation-Access-Policy
┃ ┣ SAM-Cloudfront-Create-Policy
┃ ┣ SAM-Lambda-Create-Policy
┃ ┣ SAM-List-Resource-Groups-Policy
┃ ┣ SAM-Loggroups-Create-Policy
┃ ┗ SAM-S3-Dev-Policy
┃ 
```
#### CloudFormationのCount Macroテンプレートのdeploy  
ラズパイの数に応じてIoT Topic Ruleを作成する為にCloudFormationのCountというMacro機能を利用するので[こちら(aws-cloudformation-templates/CloudFormation/MacrosExamples/Count)](https://github.com/aws-cloudformation/aws-cloudformation-templates/blob/main/CloudFormation/MacrosExamples/Count/README.md)を参考に予めdeployしておきます。

## AWS Serverless Application Model(SAM)
[AWS Serverless Application Model(SAM)](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/what-is-sam.html)という[Cloudformation](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/Welcome.html)の拡張機能を利用してAWSリソースをラズパイのコマンドラインから実装します。

SAMは[７種類のリソース](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-specification-resources-and-properties.html)を対象にしていますが、それ以外のCloudFrormation templateのbuildとdeployにも対応しているので一括実装に利用します。

## template.yaml

template.yamlはCloudFormationでAWSリソースを実装する為の定義ファイルとなります。以下にtemplateの各セクションを説明します。

### FormatVersion
AWS CloudFormation テンプレートのバージョンです。SAMのsample templateの標準設定を使用します。
```yaml
AWSTemplateFormatVersion: '2010-09-09'
```

### Transform
AWS サーバーレス アプリケーション モデル (AWS SAM) のバージョンと予めdeployしたCount macroを使うことを明示します。SAMのバージョンはsample templateの標準設定を使用します。
```yaml
Transform:
  - AWS::Serverless-2016-10-31
  - Count
```

### Globals
今回Lambda関数は一つだけですが、runtime等を変更するのにyamlの先頭部分に出しておいたほうが便利なのでFunctionから分離しました。

```yaml
Globals:
  Function:
    Runtime: python3.9
    Timeout: 15
    MemorySize: 128
    Architectures:
      - arm64
```

### Parameters
実行時 (スタックを作成または更新するとき) にテンプレートに渡す値です。リソースから参照することが出来ます。

[（２）見守りProjectの設計（名前の設定）で出力したtemplate_head.txt](https://qiita.com/nsaito9628/items/da402829316549853d0f#templatetemplateyaml)に相当します。この[リポジトリー](https://github.com/nsaito9628/Monitoring-system-with-Raspberry-Pi-and-AWS)を使って異なるProjectを作る際に**template.yamlの中で書き換えるのはこのParametersとDashboard**です。前述したとおりtemplate.yamlはパラメーターの対話入力後に自動出力されます。

（２）で例に示したラズパイ４台を設置するMyLivingというProjectのParametersを再掲します。
```yaml
####################################
Parameters:
############Customizable############
  ProjectName:
    Type: String
    Default: MyLiving
  Place:
    Type: String
    Default: CatBedNo1,CatBedNo2,CatBedNo3,CatBedNo4
  NumPlace:
    Type: Number
    Default: 4
  OrgBucketName:
    Type: String
    Default: neko-mimamori
  NameTag:
    Type: String
    Default: MyLiving
############# Fixed #############
  EventPrefix:
    Type: String
    Default: 'emr'
#################################
```
### Conditions  
**【2022.10.26削除】**  
IoT Topic RuleはCount macroを使ったループ処理、CloudWatch Dashboardはset_parameters.py中のfor文でのループ処理で、それぞれの定義文を作成する仕様に変更したためConditionsの定義が不要になりました。

![architecture_w_比較.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565396/d851ca68-f4a0-99f4-94e0-6e3b0aced66c.png)

~~リソース作成の制御、または特定のリソースプロパティに値を割り当てるかどうかを制御する条件を定義します。if文で条件分岐させるのと同様の使い方をしています。~~

~~Conditionsセクションを使った理由は、IoTRuleとDashBoardの二つのリソースの数をラズパイ台数に合わせて変える必要があるからです。~~

~~Parametersセクションのデフォルト値の有無を分岐条件に使い、IoTRuleとDashBoardの数をラズパイの数に合わせました。~~

~~IoTRuleの数を決める条件は以下です。IoTRule1からIoTRule4のResourcesを1つずつ個別に記述しておいて、ParameterのDefaultがブランクでなければConditionsでリソースを有効にします。~~

~~CloudWatchDashboardはIoTRuleとリソースの定義方法が異なり、筆者の理解(2022.08.18時点)ではラズパイ１台の時のDashboard、２台の時の・・・４台の時のDashBoardというような定義しか出来ませんでした。(count macroを使えば出来そうですが夏休みの宿題完成せず)~~

~~そこでConditionの与え方もラズパイ１台の時のDashboard・・・ラズパイ４台の時のDashboardという分岐をする為に以下のようになりました。~~


### Resources
#### OriginS3Bucket
イベント録画したmp4ファイルの保存先とCloudFrontのオリジンになります。
```yaml
  OriginS3Bucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Retain
    Properties: 
      BucketName: !Sub ${OrgBucketName}
      PublicAccessBlockConfiguration:
        BlockPublicAcls: True
        BlockPublicPolicy: True
        IgnorePublicAcls: True
        RestrictPublicBuckets: True
```
#### OriginS3BucketPolicy
CloudFrontのOriginAccessIdentity(OAI)だけGetObjectを許可するBucket policyです。この１０連投書いている途中で今後は[OAC](https://aws.amazon.com/jp/blogs/networking-and-content-delivery/amazon-cloudfront-introduces-origin-access-control-oac/)を使いましようと言うリリースがありました、今後はこちらを使いたいと思います。

```yaml
  OriginS3BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref OriginS3Bucket
      PolicyDocument:
        Statement:
          - Action: s3:GetObject
            Effect: Allow
            Resource: !Sub arn:aws:s3:::${OriginS3Bucket}/*
            Principal:
              AWS: !Sub arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ${CloudFrontOriginAccessIdentity}
```

#### CloudFrontDistribution
CloudFrontを使う目的はOAIを介したアクセス制限とhttpsリダイレクトです。見られて問題のあるデータは搬送しないのですが一応標準的な仕組みづくりの練習として設定してあります。目的がCDN利用ではないのでキャッシュを無効にしています。
```yaml
  CloudFrontDistribution:
    Type: AWS::CloudFront::Distribution
    Properties:
      DistributionConfig:
        Enabled: true
        HttpVersion: http2
        Origins:
        - Id: S3Origin
          DomainName: !Sub "${OriginS3Bucket}.s3.${AWS::Region}.amazonaws.com"
          S3OriginConfig:
            OriginAccessIdentity: !Sub origin-access-identity/cloudfront/${CloudFrontOriginAccessIdentity}
        DefaultRootObject: index.html
        Comment: !Sub ${AWS::StackName} distribution
        DefaultCacheBehavior:
          TargetOriginId: S3Origin
          ForwardedValues:
            QueryString: false
          ViewerProtocolPolicy: redirect-to-https
          AllowedMethods:
            - GET
            - HEAD
          DefaultTTL: 0
          MaxTTL: 0
          MinTTL: 0
        IPV6Enabled: false
```

#### CloudFrontOriginAccessIdentity

S3BucketがGetObjectを許可するリソースです。
```yaml
  CloudFrontOriginAccessIdentity:
    Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
    Properties:
      CloudFrontOriginAccessIdentityConfig:
        Comment: !Ref AWS::StackName
```

#### LambdaFunction
録画したmp4ファイルがS3にuploadされたら、mp4のファイル名を01.mp4に書き換えて「モノの名前＝ラズパイの設置場所」ごとのプレフィックスに移動します。CloudFrontが呼び出すindex.htmlではプレフィクスとファイル名を固定しているため、uploadしたファイルの名前をLambda関数でリネームしています。

Lambda関数(app.py)は[（９）S3にuploadした映像確認用web page](https://qiita.com/nsaito9628/items/73f8f2b175000db46ec2#lambda%E9%96%A2%E6%95%B0)で紹介します。
```yaml
  LambdaFunction:
    Type: AWS::Serverless::Function 
    Properties:
      CodeUri: function/
      FunctionName: !Sub ${ProjectName}Func
      Handler: app.lambda_handler
      Environment: 
        Variables: 
          ORG_BACKET: !Sub ${OrgBucketName}
      Events:
        S3Event:
          Type: S3
          Properties:
            Bucket: !Ref OriginS3Bucket 
            Events: s3:ObjectCreated:*
            Filter:
              S3Key:
                Rules:
                - Name: prefix
                  Value: !Sub ${EventPrefix} 
      Role: !GetAtt LambdaFunctionRole.Arn
      Tags:
        Name: !Sub ${NameTag}
```

#### LambdaFunctionLogGroup
Lambda関数のlog groupです。7日経ったら消します。
```yaml
  LambdaFunctionLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub /aws/lambda/${LambdaFunction}
      RetentionInDays: 7
```

#### LambdaFunctionRole
Lambda関数にS3 ObjectのGet、Put、Deleteとログの吐き出しを許可します。
```yaml
  LambdaFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Action: "sts:AssumeRole"
            Principal:
              Service: lambda.amazonaws.com
      Policies:
        - PolicyName: !Sub ${ProjectName}FuncPolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action:
                  - "logs:CreateLogGroup"
                  - "logs:CreateLogStream"
                  - "logs:PutLogEvents"
                Resource: 
                  - "arn:aws:logs:*:*:*"
              - Effect: "Allow"
                Action:
                  - "s3:GetObject"
                  - "s3:PutObject"
                  - "s3:DeleteObject"
                Resource: 
                  - !Sub "arn:aws:s3:::*"
```
#### IoTRuleActionRole
AWS IoTにcloudwatchにMetricDataを出力するアクションを許可します。
```yaml
  IoTRuleActionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: "Allow"
            Principal:
              Service:
                - "iot.amazonaws.com"
            Action:
              - "sts:AssumeRole"
      RoleName: !Sub ${ProjectName}IoTRuleActionRole
      Policies:
        - PolicyName: !Sub ${ProjectName}CustomMetricsPolicy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: "Allow"
                Action: "cloudwatch:PutMetricData"
                Resource: "*"
```

#### IoTRule  
**【2022.10.26修正】**
set_parameters.pyで入力されて予めParametersのNumPlaceに渡された数だけCountループを回してラズパイの数だけIoTRuleを定義します。  

場所名＝モノの名前はParametersのPlaceにラズパイの数だけ格納されるのでListとして参照します。

~~ラズパイを最大４台設置する為に予めIoTRule1からIoTRule4の4つのルールを定義しておきます。~~

~~ラズパイが１台ならIoTRule1だけを有効にして、ラズパイが２台ならIoTRule1とIoTRule2を有効にして、という具合にConditionsでリソースの有効/無効を決めます。~~
```yaml
  IoTRule:
    Type: AWS::IoT::TopicRule
    Properties:
      RuleName:  !Join ["", [!Select ["%d", !Split [",", !Sub ",${Place}"]], "IoTRule"]]
      TopicRulePayload:
        Actions:
          - CloudwatchMetric:
              MetricName: detect_count
              MetricNamespace: !Join ["", [!Select ["%d", !Split [",", !Sub ",${Place}"]], "/count"]]
              MetricUnit: None
              MetricValue: ${detect_count}
              RoleArn: !GetAtt IoTRuleActionRole.Arn
        AwsIotSqlVersion: "2016-03-23"
        Description: String
        RuleDisabled: false
        Sql: !Join ["", ["SELECT * FROM '", !Select ["%d", !Split [",", !Sub ",${Place}"]], "/count'"]]
      Tags:
        - Key: Name
          Value: !Sub ${NameTag}
    Count: !Ref NumPlace
```
#### CloudWatchDashboard  
**【2022.10.26修正】**
set_parameters.pyの中のforループでラズパイの数だけCloudWatch Dashboard定義文を出力する仕様に変更しました。Count macroを使ったyaml中でのループ処理を検討しましたが解決方法を見つけることが出来ませんでした。

~~DashboardBodyの定義がjsonになり(これ以外分からなかった)、ラズパイ１台の時、ラズパイ４台の時というリソースを単位化して使い回しが出来ない定義になりました。~~

~~本当はIoTルールのようにDashboard1、Dashboard2・・・と定義して連結する又はfor文の中でテンプレートを増殖させたかったので近いうちにmacro等に挑戦したいです。~~

~~DashboardBodyのjsonの生成方法は[「CloudFormationを利用してCloudWatchダッシュボードを設置してみた」](https://dev.classmethod.jp/articles/cloudwatch-dashboard-cfn/)等を参考にさせて頂きました。~~
```yaml
  CloudWatchDashboard1:
    Condition: OnePlace
    Type: AWS::CloudWatch::Dashboard
    Properties:
      DashboardName: !Sub ${ProjectName}Dashboard
      DashboardBody: !Sub 
                  '{
                      "widgets": [
                          {
                              "height": 6,
                              "width": 12,
                              "y": 0,
                              "x": 0,
                              "type": "metric",
                              "properties": {
                                  "metrics": [
                                      [ 
                                        "${Place1}/count", 
                                        "detect_count" 
                                      ]
                                  ],
                                  "view": "timeSeries",
                                  "stacked": false,
                                  "region": "ap-northeast-1",
                                  "title": "${Place1}",
                                  "period": 60,
                                  "stat": "Sum"
                              }
                          }
                      ]
                  }'
                         ・
                         ・
                         ・
  CloudWatchDashboard4:
    Condition: FourPlaces
    Type: AWS::CloudWatch::Dashboard
    Properties:
      DashboardName: !Sub ${ProjectName}Dashboard
      DashboardBody: !Sub |
                  {
                      "widgets": [
                        {
                              "height": 6,
                              "width": 12,
                              "y": 0,
                              "x": 0,
                              "type": "metric",
                              "properties": {
                                  "metrics": [
                                      [ 
                                        "${Place1}/count", 
                                        "detect_count" 
                                      ]
                                  ],
                                  "view": "timeSeries",
                                  "stacked": false,
                                  "region": "ap-northeast-1",
                                  "title": "${Place1}",
                                  "period": 60,
                                  "stat": "Sum"
                              }
                          },
                         ・
                         ・
                         ・
                          },
                          {
                              "height": 6,
                              "width": 12,
                              "y": 6,
                              "x": 12,
                              "type": "metric",
                              "properties": {
                                  "metrics": [
                                      [ 
                                        "${Place4}/count", 
                                        "detect_count" 
                                      ]
                                  ],
                                  "view": "timeSeries",
                                  "stacked": false,
                                  "region": "ap-northeast-1",
                                  "title": "${Place4}",
                                  "period": 60,
                                  "stat": "Sum"
                              }
                          }
                      ]
                  }
```
### Outputs
#### DomainName:
CloudFrontのドメイン名をcfnのマネコンまたはコマンドラインから参照できるようにOutputしました。
```yaml
Outputs:
  DomainName:
    Value: !GetAtt CloudFrontDistribution.DomainName
```

## sam build/deploy
ラズパイのコマンドラインでtemplate.yamlが保存されているdirectoryに移動して実行します。
```sh
cd ./template
```

buildを実行します。
```sh
sam build
```

このリポジトリーのtemplate.yamlではリソース名(例：FunctionName(Lambda関数名))を自分で指定してますのでdeployの際に[--capabilitiesオプションでCAPABILITY_NAMED_IAMを指定する必要](https://docs.aws.amazon.com/ja_jp/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-deploy.html)があります。

stack名もProject名と同じにして統一しておきます。
```sh
sam deploy --guided --capabilities CAPABILITY_NAMED_IAM --stack-name MyLiving
```
Parametersで事前に定義してある内容はそのままリターンします。
最後に[Y/N]で聞いてくる部分は全てYを入力します。
最後の２行はそのままリターンします。
> Setting default arguments for 'sam deploy' =========================================
Stack Name [MyLiving]: 
AWS Region [ap-northeast-1]:
Parameter ProjectName [MyLiving]: 
Parameter Place1 [CatBed1]:
Parameter Place2 [CatBed2]:
Parameter Place3 [CatBed3]:
Parameter Place4 [CatBed4]: 
Parameter OrgBucketName [neko-mimamori]: 
Parameter NameTag [MyLiving]: 
Parameter EventPrefix [emr]:
#Shows you resources changes to be deployed and require a 'Y' to initiate deploy Confirm changes before deploy [Y/n]: Y
#SAM needs permission to be able to create roles to connect to the resources in your template Allow SAM CLI IAM role creation [Y/n]: Y
#Preserves the state of previously provisioned resources when an operation fails Disable rollback [Y/n]: Y
Save arguments to configuration file [Y/n]: Y
SAM configuration file [samconfig.toml]:
SAM configuration environment [default]:


CloudFormationのマネジメントコンソールからStatus、リソース、Output等を確認します。
![cfn deploy completed.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565396/bc9ef9bc-5912-db48-2c16-16fa63747d54.png)

## 次回
[(9) S3にuploadした映像の確認用web page](https://qiita.com/nsaito9628/items/73f8f2b175000db46ec2)に続きます。


## 追記  

#### 修正　2022.10.26  
IoT Topic RuleはCount macroを使ったループ処理、CloudWatch Dashboardはset_parameters.py中のfor文でのループ処理で、それぞれの定義文を作成する仕様に変更しました。Conditionsの定義が不要になりました。