CloudFormationでQuickSightを複製する

# はじめに
前回、QuickSightをCloudFormationで操作した内容を記事にしました。

https://qiita.com/a_b_/items/f9e7588348547c952d55

"分析"や"ダッシュボード"に関しても試してみたので記事にします。

# 概要
- CloudFormationでは、"分析"や"ダッシュボード"をゼロから構築することはできない。
- GUIで作成した"分析"をテンプレートにして、それを基に"分析"と"ダッシュボード"を作成することは、CloudFormationで出来る。
    - "分析"のテンプレートから、"分析"と"ダッシュボード"の両方が複製できます。
- そのため今回は「複製する」というテーマで、複製元でテンプレートを作って複製先に配布する、という内容。

# 参考

https://dev.classmethod.jp/articles/cloudformation-quicksight-dashboard-analysis/

# 構築



## 複製元
### 1.共通部分作成
まずは複製元で環境を作っていきます。
前の記事と同様に、共通部分から作ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/9dd50737-20b5-44e8-67a5-46fc71f6adda.png)

コードは前回とほぼ同じものですので説明は省きます。

```yaml:00_createCommonResource.yaml
AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  Env:
    Type: String

Resources:
  RawDataBucket:
    Type: AWS::S3::Bucket
    Properties: 
      BucketName: !Sub raw-data-${AWS::AccountId}-${AWS::Region}-${Env}
      BucketEncryption: 
          ServerSideEncryptionConfiguration: 
            - 
              ServerSideEncryptionByDefault: 
                  SSEAlgorithm: 'AES256'
              BucketKeyEnabled: false
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true

  AthenaQueryResultBucket:
    Type: AWS::S3::Bucket
    Properties: 
      BucketName: !Sub query-result-${AWS::AccountId}-${AWS::Region}-${Env}
      BucketEncryption: 
          ServerSideEncryptionConfiguration: 
            - 
              ServerSideEncryptionByDefault: 
                  SSEAlgorithm: 'AES256'
              BucketKeyEnabled: false
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      LifecycleConfiguration: 
        Rules: 
          - 
            Id: 'auto-delete'
            Status: 'Enabled'
            ExpirationInDays: 7

  AthenaWorkGroup:
    Type: AWS::Athena::WorkGroup
    Properties:
      Name: !Sub athena-work-group-${Env}
      RecursiveDeleteOption: true
      WorkGroupConfiguration:
        ResultConfiguration:
          OutputLocation: !Sub s3://${AthenaQueryResultBucket}/data
          EncryptionConfiguration: 
            EncryptionOption: 'SSE_S3'
        EnforceWorkGroupConfiguration: true
        PublishCloudWatchMetricsEnabled: true

  GlueDatabase:
    Type: AWS::Glue::Database
    Properties: 
      CatalogId: !Ref AWS::AccountId  
      DatabaseInput:
        Name: !Sub glue-database-${Env}

Outputs:
  RawDataBucket:
    Value: !Ref RawDataBucket
    Export:
      Name: !Sub "${Env}-RawDataBucket-Name"
  AthenaQueryResultBucket:
    Value: !Ref RawDataBucket
    Export:
      Name: !Sub "${Env}-AthenaQueryResultBucket-Name"
  AthenaWorkGroup:
    Value: !Ref AthenaWorkGroup
    Export:
      Name: !Sub "${Env}-AthenaWorkGroup-Name"
  GlueDatabase:
    Value: !Ref GlueDatabase
    Export:
      Name: !Sub "${Env}-GlueDatabase-Name"
```

### 2.QuickSightからのアクセス許可を設定
やり方は[前回](https://qiita.com/a_b_/items/f9e7588348547c952d55#2quicksight%E3%81%8B%E3%82%89%E3%81%AE%E3%82%A2%E3%82%AF%E3%82%BB%E3%82%B9%E8%A8%B1%E5%8F%AF%E3%82%92%E8%A8%AD%E5%AE%9A)と同じです。

### 3.データを格納
[前回](https://qiita.com/a_b_/items/f9e7588348547c952d55#3%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E6%A0%BC%E7%B4%8D)と同じデータを使っています。

### 4.個別部分作成
Athenaテーブルや、DataSetを作っていきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/74f567a1-a483-3046-4b4d-8be6d2f96c04.png)

コードは前回を手直ししています。
- 複製元でも複製先でも使えるようにしています。
    - 複製元の場合：DataSet作成まで
    - 複製先の場合：DataSetを作成し、"分析"と"ダッシュボード"も作成
        - パラメータ`SourceTemplateArn`の指定の有無で、作成する/しないを制御

`Analysis`や`Dashboard`についてパラメータ化が中途半端ですがご容赦ください。「どういうDataSetを、どういう風に参照するか」等の設定はパラメータ化するのが難しいので、直接コードを変更してください。

`DataSetPlaceholder`は、のちの分析テンプレート作成時と同じ文字列を指定してください。（詳細はテンプレート作成コードのところで）

```yaml:01_createDataSet.yaml
AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  Env:
    Type: String
  TableName:
    Type: String
    Description: Table name must not contain uppercase characters.
  QuickSightAdminUser:
    Type: String
    Description: user must not be asterisk(*).
  SourceTemplateArn:
    Type: String
    Default: ''

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      -
        Label:
          default: User Setting.
        Parameters:
          - Env
          - TableName
          - QuickSightAdminUser
          - SourceTemplateArn

# SourceTemplateArnが空文字なら作らない
Conditions:
  CreateAnalysis:
    !Not [!Equals [ !Ref SourceTemplateArn , '']]

Resources:
  # Athena Table
  GlueTable:
    Type: AWS::Glue::Table
    Properties:
      CatalogId: !Ref AWS::AccountId
      DatabaseName:
        Fn::ImportValue:
          !Sub "${Env}-GlueDatabase-Name"
      TableInput:
        Name: !Ref TableName
        TableType: EXTERNAL_TABLE
        Parameters:
          skip.header.line.count: 1
          has_encrypted_data: false
          serialization.encoding: utf-8
          EXTERNAL: true
        StorageDescriptor:
          OutputFormat: org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat
          Columns:
            - Name: source
              Type: string
            - Name: destination
              Type: string
          InputFormat: org.apache.hadoop.mapred.TextInputFormat
          Location:
            Fn::Join:
              - ''
              - - 's3://'
                - Fn::ImportValue: !Sub "${Env}-RawDataBucket-Name"
                - '/data' 
          SerdeInfo:
            Parameters:
              field.delim: ","
              serialization.format: ","
            SerializationLibrary: org.apache.hadoop.hive.serde2.lazy.LazySimpleSerDe

  # QuickSight DataSource
  AthenaDataSource:
    Type: AWS::QuickSight::DataSource
    Properties: 
      AwsAccountId: !Ref AWS::AccountId
      Name: !Sub "${Env}-Athena"
      DataSourceId: !Sub "${Env}-Athena-Id"
      Type: ATHENA
      DataSourceParameters:
        AthenaParameters:
          WorkGroup:
            Fn::ImportValue:
              !Sub "${Env}-AthenaWorkGroup-Name"
      Permissions:
        - Actions:
            - quicksight:UpdateDataSourcePermissions
            - quicksight:DescribeDataSource
            - quicksight:DescribeDataSourcePermissions
            - quicksight:PassDataSource
            - quicksight:UpdateDataSource
            - quicksight:DeleteDataSource
          Principal: !Sub arn:aws:quicksight:${AWS::Region}:${AWS::AccountId}:user/default/${QuickSightAdminUser}

  # QuickSight DataSet
  RepDataSet:
    Type: AWS::QuickSight::DataSet
    Properties:
      AwsAccountId: !Ref AWS::AccountId
      Name: !Sub "Select-${GlueTable}"
      DataSetId: !Sub "Select-${GlueTable}-Id"
      ImportMode: SPICE
      PhysicalTableMap:
        rep-physical:
          RelationalTable:
            DataSourceArn: !GetAtt AthenaDataSource.Arn
            # Set Database Name
            Schema: 
              Fn::ImportValue:
                !Sub "${Env}-GlueDatabase-Name"
            # Set Table Name
            Name: !Ref GlueTable
            InputColumns:
              - Name: source
                Type: STRING
              - Name: destination
                Type: STRING
      Permissions:
        - Actions:
            - quicksight:UpdateDataSetPermissions
            - quicksight:DescribeDataSet
            - quicksight:DescribeDataSetPermissions
            - quicksight:PassDataSet
            - quicksight:DescribeIngestion
            - quicksight:ListIngestions
            - quicksight:UpdateDataSet
            - quicksight:DeleteDataSet
            - quicksight:CreateIngestion
            - quicksight:CancelIngestion
          Principal: !Sub arn:aws:quicksight:${AWS::Region}:${AWS::AccountId}:user/default/${QuickSightAdminUser}

  Analysis:
    Type: AWS::QuickSight::Analysis
    Condition: CreateAnalysis
    Properties:
      AwsAccountId: !Ref AWS::AccountId
      AnalysisId: 'Sample-Analysis-From-Temp-Id'
      Name: 'Sample-Analysis-From-Temp'
      SourceEntity:
        SourceTemplate:
          Arn: !Ref SourceTemplateArn
          DataSetReferences:
            - 
              DataSetPlaceholder: 'DataSet001Placeholder'
              DataSetArn: !GetAtt RepDataSet.Arn
      Permissions:
        - 
          Principal: !Sub arn:aws:quicksight:${AWS::Region}:${AWS::AccountId}:user/default/${QuickSightAdminUser}
          Actions:
            - quicksight:RestoreAnalysis
            - quicksight:UpdateAnalysisPermissions
            - quicksight:DeleteAnalysis
            - quicksight:DescribeAnalysisPermissions
            - quicksight:QueryAnalysis
            - quicksight:DescribeAnalysis
            - quicksight:UpdateAnalysis

  Dashboard:
    Type: AWS::QuickSight::Dashboard
    Condition: CreateAnalysis
    Properties: 
      AwsAccountId: !Ref AWS::AccountId
      DashboardId: 'Sample-Dashboard-From-Temp-Id'
      Name: 'Sample-Dashboard-From-Temp'
      DashboardPublishOptions: 
        AdHocFilteringOption: 
          AvailabilityStatus: ENABLED
        ExportToCSVOption: 
          AvailabilityStatus: DISABLED
        SheetControlsOption: 
          VisibilityState: EXPANDED
      SourceEntity: 
        SourceTemplate: 
          Arn: !Ref SourceTemplateArn
          DataSetReferences: 
            - 
              DataSetPlaceholder: 'DataSet001Placeholder'
              DataSetArn: !GetAtt RepDataSet.Arn
      Permissions: 
        - 
          Principal: !Sub arn:aws:quicksight:${AWS::Region}:${AWS::AccountId}:user/default/${QuickSightAdminUser}
          Actions: 
            - quicksight:DescribeDashboard
            - quicksight:ListDashboardVersions
            - quicksight:UpdateDashboardPermissions
            - quicksight:QueryDashboard
            - quicksight:UpdateDashboard
            - quicksight:DeleteDashboard
            - quicksight:DescribeDashboardPermissions
            - quicksight:UpdateDashboardPublishedVersion
```

### 5."分析"の作成
QuickSightにアクセスして、見せたいビジュアルを作成してください。
今回は名称変換的なデータなので、サンキーダイアグラムで表してみました。（グラフ的には無価値です）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/844ea0a8-2ddc-4bfd-b0c4-b3df10401b66.png)

### 6.分析テンプレートの作成
分析テンプレートはCloudFormationから作ります。

```yaml:02_createTemplate.yaml
AWSTemplateFormatVersion: '2010-09-09'

Parameters:
  TemplateId:
    Type: String
  DataSet001Arn:
    Type: String
  TargetAnalysisArn:
    Type: String

Metadata:
  AWS::CloudFormation::Interface:
    ParameterGroups:
      -
        Label:
          default: User Setting.
        Parameters:
          - TemplateId
          - DataSet001Arn
          - TargetAnalysisArn

Resources:
  AnalysisTemplate:
    Type: AWS::QuickSight::Template
    Properties:
      AwsAccountId: !Ref AWS::AccountId
      Name: !Sub "${TemplateId}-Analysis-Template"
      TemplateId: !Sub "${TemplateId}-Analysis-Template-Id"
      SourceEntity:
        SourceAnalysis:
          Arn: !Ref TargetAnalysisArn
          DataSetReferences:
            - 
              DataSetPlaceholder: 'DataSet001Placeholder'
              DataSetArn: !Ref DataSet001Arn
      # ここは手作成
      Permissions:
        - 
          Principal: 'arn:aws:iam::123456789012:root'
          Actions: 
            - quicksight:DescribeTemplate

Outputs:
  AnalysisTemplate:
    Value: !GetAtt AnalysisTemplate.Arn
    Export:
      Name: !Sub "${TemplateId}-Analysis-Template-Arn"
```
パラメータについては以下を指定してください。
- TemplateId
    - 任意の文字列
- DataSet001Arn
    - "分析"で使用しているデータセットのリソースネームを指定
- TargetAnalysisArn
    - "分析"のリソースネームを指定

リソースネームを取得するコマンドはそれぞれ以下になります。
```
# データセットのリソースネームを取得
aws quicksight list-data-sets --aws-account-id 123456789012

# "分析"のリソースネームを取得
aws quicksight list-analyses --aws-account-id 123456789012

# 作成したテンプレートを確認
aws quicksight list-templates --aws-account-id 123456789012
```
`Permissions`は、同アカウント内に複製する場合は設定する必要ありません。
別アカウントで複製する場合は参照できるよう権限を設定する必要があります。


##### QuickSightの分析テンプレートで定義する`DataSetPlaceholder`について
- 分析で用いるデータセットを指定するKeyのように用いられているようです
- 用いるデータセットが2つ以上ある場合はわかりやすい名前を付けるのがいいと思います
    - `01_createDataSet.yaml`で、”同じ名前と、同じ使われ方をするデータセット”のペア、を指定する必要があります
    - プレースホルダー名の例
        - purchaseData / customerMaster
        - cmData / programData / companyData
- 複製先で`DataSetPlaceholder`の与え方を間違えると、以下のようなエラーに
```
Invalid request provided: Given placeholders [HogeHogePlaceholder] are not part of template (Service: QuickSight, Status Code: 400, Request ID: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)
```


## 複製先

### 7.共通部分作成～データを格納
複製先のアカウントで、[1.共通部分作成](#1共通部分作成)、[2.QuickSightからのアクセス許可を設定](#2quicksightからのアクセス許可を設定)、[3.データを格納](#3データを格納)を行ってください。

### 8.個別部分、"分析"と"ダッシュボード"の作成
[4.個別部分作成](#4個別部分作成)と同じコードで、今回はパラメータに[6.分析テンプレートの作成](#6分析テンプレートの作成)で作成された分析テンプレートのARNを指定して実行してください。
"分析"と"ダッシュボード"に、同じサンキーダイアグラムができているはずです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/a356c560-9c27-e276-c7ef-91dfca86dedd.png)

# 削除
作成順と逆順で削除されます。"分析"と"ダッシュボード"もちゃんと消えてくれます。

# おわりに
今回はビジュアルもIaCで作成してみました。
「ベースのダッシュボードをテンプレート化しておき、他アカウントに複製した後、個別のカスタマイズをする」などの用途で使えるのではないでしょうか。