CloudFormationでQuickSightにAthenaのデータセットを作成する

# はじめに

QuickSightがCloudFormationで操作できるので、AthenaのテーブルをQuickSightで使うには、という内容を試してみました。

# 参考

https://dev.classmethod.jp/articles/creating-tables-in-the-aws-glue-data-catalog-and-configuring-athena-with-cloudformation/

https://dev.classmethod.jp/articles/cloudformation-quicksight-dataset-s3/

# 構成図
語がたくさん出てきてわからなくなったので、図示してみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/40a9b363-4820-465e-4fe2-92a63af474fe.png)

# 構築
環境構築にあたって、２つの部分に分けました。

- 共通部分
    - 1回構築すればよい部分
    - 個別部分N個に対し、１つでよい部分
        - バケット
            - データ格納
            - 出力先
        - Athenaワークグループ
        - Glueデータベース
- 個別部分
    - 設定を変えて複数作成を想定する部分
        - Athenaテーブル
        - QuickSight DataSource
        - QuickSight DataSet

## 1.共通部分の構築
先の図のうち、以下の部分が最初に1回だけ構築する共通部分になります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/abf3dceb-f7c5-8c06-0f24-01b768b6818b.png)

CloudFormationは以下。パラメータ`Env`を別にすることで、複数の環境が作成できるはずです。
```yaml:common.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"
```

- RawDataBucket
    - データ格納バケット
    - S3のSSEを設定
    - パブリックアクセスブロックはON
- AthenaQueryResultBucket
    - Athenaの実行結果を格納するバケット
    - S3のSSEを設定
    - パブリックアクセスブロックはON
    - 7日でファイルを自動削除
- AthenaWorkGroup
    - 環境専用のAthenaのワークグループ
    - 出力先はAthenaQueryResultBucket
- GlueDatabase
    - 環境専用のAthenaテーブルを作成するデータベース


注意点ですが、Athenaワークグループ(AthenaWorkGroup)は、実行履歴があると削除時に以下のようなエラーになり、削除できませんでした。
```
"Invalid request provided: WorkGroup athena-work-group-xxxx is not empty (Service: Athena, Status Code: 400, Request ID: xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx, Extended Request ID: null)"
```
そのため、以下の設定を入れています。
```yaml
      RecursiveDeleteOption: true
```
参考のページは以下です。

https://dev.classmethod.jp/articles/how-recursivedeleteoption-works-when-deleting-workgroups/


## 2.QuickSightからのアクセス許可を設定
作成したバケットの両方に対して、QuickSightからアクセス許可を追加します。
またAthenaにアクセスできるようにもします。
QuickSightの管理画面で設定できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/255e788f-eba8-67e7-4a53-6b1e18d2b09d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/edef31b1-ebdf-f381-ae58-f6d3ef6cdc2c.png)

出力先バケットの場合は、「Athena　Workgroupの書き込みアクセス許可」にもチェックを入れてください。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/a6932643-c743-b5d7-07f5-33b8a94ebf88.png)

このあたりの部分もIaC化するのであれば、QuickSightで使用するロールに、自作のロールを使用することができそうです。以下の情報が参考になるかと思います。

https://dev.classmethod.jp/articles/quicksight-custome-iam-role/

## 3.データを格納
バケットに、Athenaテーブルで読ませたいデータを置きます。
今回は以下のファイルを置きました、社名を揃える用マスタというイメージです。

```csv:data\sampleData001.csv
source,destination
ＡＮＡ,全日本空輸
全日空,全日本空輸
全日本空輸,全日本空輸
```
```csv:data\sampleData002.csv
source,destination
ＪＲ東,東日本旅客鉄道株式会社
ＪＲ東日本,東日本旅客鉄道株式会社
東日本旅客鉄道株式会社,東日本旅客鉄道株式会社
```

## 4.個別部分
Athenaテーブルを作成して、QuickSightに読み込ませる部分です。

```yaml:each
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(*).

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}
```

- QuickSightAdminUser
    - DataSourceやDataSetを操作するQuickSightのユーザを指定します
    - "*"だと、エラーになってしまいました。
- GlueTable
    - Athenaのテーブル定義
    - Locationは、共通部分の出力を参照しています。
    - **注意**
        - **ColumnsのTypeは小文字**
            - 大文字でも作成はされますが、DataSetを作成する箇所で問題になりました。
- AthenaDataSource
    - AthenaのDataSourceの定義
    - 共通部分で出力したワークグループを参照
- RepDataSet
    - AthenaのDataSetの定義
    - 無加工であればPhysicalTableMapだけで定義可能
    - RelationalTable.Schemaには、（共通部分で出力した）データベースをセット
    - **注意**
        - **RelationalTable.InputColumnsのTypeは大文字**
            - 小文字だとエラーになります
            - （上で注意した）AthenaのColumnsのTypeを大文字にすると、以下のようなエラーになります。
```
An error has been thrown from the AWS Athena client. HIVE_METASTORE_ERROR: Error: type expected at the position 0 of 'STRING' but 'STRING' is found.
```

作成に成功すると、QuickSight上でデータセットが作成され、置いたCSVが確認できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/553680/6d1ebd27-63aa-6678-7922-c4b4dd020c75.png)


# 失敗談：削除時のやらかしについて
削除する際に、バケットを削除したあとにQuickSightのアクセス許可を外そうとすると、以下のようなちぐはぐな状態になっていました。
- QuickSightの管理画面ではアクセス許可の情報が表示されない
- IAMポリシーではアクセス許可が設定されている

このポリシーをQuickSight管理画面からではなく、直接修正するとQuickSightでは「外部で変更されたため、QuickSightからは編集できません」となり、以降QuickSightからはその部分の設定ができませんでした。
該当のポリシーを削除すれば、再度QuickSightから編集できます。ただし、削除前にどこのバケットに許可していたのかを控えておき、再度設定画面からチェックを入れる羽目になりました。

なので削除時は、操作の手順を正確に逆から行ってください。

# おわりに
今回はAthenaを作って、それをQuickSightから読み込ませることを（おおよそ）IaC化しました。
今までQuickSightはGUIでしかできないと思っていましたが、IaC化で標準化が簡単に出来るようになれば、利用シーンが増えるのではないでしょうか。