AWS QuickSight Boto3【Python】

# はじめに

この記事は、[Boto 3 Docs 1.9.185 documentation]
(https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/quicksight.html) の内容をベースとしています。
<img src = "https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/696827/bc2b28ef-4a39-f370-b525-602e16a0c1c9.png" width = 496px height = 184px>

AWS外部のWebアプリケーションで組織やユーザが登録された場合にPython Boto3, API Gateway, Lambda, Step Functionsから成るAPIを利用してQuickSightでも同様の組織やユーザを作成するケースがあり、本記事ではその際に利用したPython Boto3 QuickSight Clientの関数を記載しています。

## Preparation

```python
import boto3
import logging



logger = logging.getLogger()
logger.setLevel(logging.DEBUG)

# set Variables
REGION_NAME = os.getenv("REGION_NAME")
AWS_ACCOUNT_ID = os.getenv("AWS_ACCOUNT_ID")

NAME_SPACE = 'default'

"""
QuickSightへの必要に応じた権限を付与されているRoleをAssume RoleとしてLambdaで利用することにより、
aws_access_key_id, aws_secret_access_keyをパラメータに設定する必要がなくセキュア。
"""
qs = boto3.client('quicksight', region_name = REGION_NAME)

"""
ダッシュボードをqs.create_dashboardで作成するにはテンプレートが必要なのであらかじめ作成。
QuickSightコンソール上で作成しておいた分析のIDをANALYSIS_IDに設定。
SourceEntityには分析および分析に利用しているデータセットのリソースネームを設定。
qs.create_templateで設定されるDataSetPlaceholderはqs.create_dashboardでも利用。
"""
ANALYSIS_ID = 'analysis-id'
qs.create_template(
    AwsAccountId = AWS_ACCOUNT_ID, 
    Name = 'Dashboard Template', 
    Permissions = [
        {
            'Principal': 'arn:aws:quicksight:{}:{}:user/{}/<QuickSightUserName>'.format(REGION, AWS_ACCOUNT_ID, NAME_SPACE), 
            'Actions': [
                'quicksight:UpdateTemplatePermissions', 
                'quicksight:DescribeTemplate',
            ]
        },
    ], 
    TemplateId = 'Dashboard-Template',
    SourceEntity = {
        'SourceAnalysis': {
            'Arn': 'arn:aws:quicksight:{}:{}:analysis/{}'.format(REGION, AWS_ACCOUNT_ID, ANALYSIS_ID), 
            'DataSetReferences': [
                {
                    'DataSetPlaceholder': 'DataSetPlaceHolder1',
                    'DataSetArn': 'arn:aws:quicksight:{}:{}:dataset/<DataSetId1>'.format(REGION, AWS_ACCOUNT_ID)
                },
                {
                        'DataSetPlaceholder': 'DataSetPlaceHolder2',
                        'DataSetArn': 'arn:aws:quicksight:{}:{}:dataset/<DataSetId2>'.format(REGION, AWS_ACCOUNT_ID)
                },
                ]
            }
        }, 
    Tags=[
        {
            'Key': 'name', 
            'Value': 'Dashboard Template'
        }, 
    ], 
    # VersionDescription = '1'
)
```

## Organization

```python
# create Group
"""
default Namespaceに組織IDと等しいGroupNameのグループを作成。
"""
def CreateGroup(qs, AwsAccountId, Namespace, GroupName):
    qs.create_group(
        GroupName = GroupName,
        AwsAccountId = AwsAccountId,
        Namespace = Namespace
    )
    
# create Dataset
"""
ダッシュボードに表示するデータをデータセットとして作成。
ImportMode = 'DIRECT_QUERY'としてDBからクエリしたデータをQuickSightのデータセットとして利用。
DBのデータをQuickSightのデータセットとして利用するには対象のDBをデータソースとしてQuickSightに登録しておく必要があり、
DataSourceArnにはQuickSightに登録してあるデータソースのリソースネームを指定。
データセットとして読み込むデータとしてクエリデータのカラム情報をColumnsに指定。
QuickSightに作成してあるユーザやグループをPermissionsに設定することでデータセットへの権限の付与が可能。
"""
def CreateDataset(qs, AwsAccountId, Region, Namespace, DataSetId, DataSetName, TableName, DataSourceArn, SqlQuery, Columns):
    qs.create_data_set(
        AwsAccountId = AwsAccountId, 
        DataSetId = DataSetId, 
        Name = DataSetName, 
        PhysicalTableMap = {
            TableName: {
                'CustomSql': {
                    'DataSourceArn': DataSourceArn,
                    'Name': 'Query',
                    'SqlQuery': SqlQuery,
                    'Columns': Columns
                },
            }
        },
        ImportMode = 'DIRECT_QUERY',
        Permissions = [
            {
                'Principal': 'arn:aws:quicksight:{}:{}:user/{}/<QuickSightUserName>'.format(Region, AwsAccountId, Namespace),
                'Actions': [
                    "quicksight:DescribeDataSet",
                    "quicksight:DescribeDataSetPermissions",
                    "quicksight:PassDataSet",
                    "quicksight:DescribeIngestion",
                    "quicksight:ListIngestions",
                    "quicksight:UpdateDataSet",
                    "quicksight:DeleteDataSet",
                    "quicksight:CreateIngestion",
                    "quicksight:CancelIngestion",
                    "quicksight:UpdateDataSetPermissions"
                ]
            },
        ]
    )

# create Dashboard
"""
組織名は変更されることが想定されるため、組織IDをDashboardIdとして利用してダッシュボードを作成。。
Permissionsにグループを設定することで、設定されたグループに属しているユーザにダッシュボード閲覧権の付与が可能。
SourceEntityではダッシュボードに読み込むデータセットおよびグラフビジュアルを作成する基となるテンプレートのリソースネームを設定。
DataSetIdとDataSetPlaceholderに1と2が存在しており、ダッシュボードに2つのデータセットを読み込むことを想定。
"""
def CreateDashboard(qs, AwsAccountId, Region, Namespace, DashboardId, DashboardName, GroupName,
                    DataSetId1, DataSetId2, DataSetPlaceholder1, DataSetPlaceholder2,
                    TemplateID):
    qs.create_dashboard(
        AwsAccountId = AwsAccountId,
        DashboardId = DashboardId,
        Name = DashboardName,
        Permissions = [
            {
                'Principal': 'arn:aws:quicksight:{}:{}:user/{}/<QuickSightUserName>'.format(Region, AwsAccountId, Namespace),
                'Actions': [
                    'quicksight:DescribeDashboard',
                    'quicksight:ListDashboardVersions',
                    'quicksight:UpdateDashboardPermissions',
                    'quicksight:QueryDashboard',
                    'quicksight:UpdateDashboard',
                    'quicksight:DeleteDashboard',
                    'quicksight:DescribeDashboardPermissions',
                    'quicksight:UpdateDashboardPublishedVersion'
                ]
            },
            {
                'Principal': 'arn:aws:quicksight:{}:{}:group/{}/{}'.format(Region, AwsAccountId, Namespace, GroupName),
                'Actions': [
                    "quicksight:DescribeDashboard",
                    "quicksight:QueryDashboard",
                    "quicksight:ListDashboardVersions"
                ]
            }
        ],
        SourceEntity = {
            'SourceTemplate': {
                'DataSetReferences': [
                    {
                        'DataSetPlaceholder': DataSetPlaceholder1,
                        'DataSetArn': 'arn:aws:quicksight:{}:{}:dataset/{}'.format(Region, AwsAccountId, DataSetId1)
                    },
                    {
                        'DataSetPlaceholder': DataSetPlaceholder2,
                        'DataSetArn': 'arn:aws:quicksight:{}:{}:dataset/{}'.format(Region, AwsAccountId, DataSetId2)
                    },
                ],
                'Arn': 'arn:aws:quicksight:{}:{}:template/{}'.format(Region, AwsAccountId, TemplateID)
            }
        },
        Tags = [
            {
                'Key': 'name', 
                'Value': DashboardId
            },
        ],
        # VersionDescription = '1'
    )

# delete Group
def DeleteGroup(qs, AwsAccountId, Namespace, GroupName):
    qs.delete_group(
        GroupName = GroupName,
        AwsAccountId = AwsAccountId,
        Namespace = Namespace
    )

# delete Dataset
def DeleteDataset(qs, AwsAccountId, DataSetId):
    qs.delete_data_set(
        AwsAccountId = AwsAccountId,
        DataSetId = DataSetId
    )

# delete Dashboard
def DeleteDashboard(qs, AwsAccountId, DashboardId):
    qs.delete_dashboard(
        AwsAccountId = AwsAccountId, 
        DashboardId = DashboardId
    )

# update Dashboard
"""
Permissionsを除き、qs.create_dashboardと同じ引数を利用。
引数に対応する値はqs.create_dashboardの際に指定した引数の値に等しい。
Nameのみを変更することでダッシュボード名を変更することが可能。
"""
def UpdateDashboardName(
        qs, AwsAccountId, DashboardId,
        DataSetId1, DataSetPlaceholder1, DataSetId2, DataSetPlaceholder2, NewDashboardName, TemplateId
        ):
        update_dashboard_info =  qs.update_dashboard(
            AwsAccountId = AwsAccountId,
            DashboardId = DashboardId,
            Name = NewDashboardName,
            SourceEntity = {
                'SourceTemplate': {
                    'DataSetReferences': [
                        {
                            'DataSetPlaceholder': DataSetPlaceholder1,
                            'DataSetArn': 'arn:aws:quicksight:{}:{}:dataset/{}'.format(Region, AwsAccountId, DataSetId1)
                        },
                        {
                            'DataSetPlaceholder': DataSetPlaceholder2,
                            'DataSetArn': 'arn:aws:quicksight:{}:{}:dataset/{}'.format(Region, AwsAccountId, DataSetId2)
                        },
                    ],
                    'Arn': 'arn:aws:quicksight:{}:{}:template/{}'.format(Region, AwsAccountId, TemplateId)
                }
            },
            # VersionDescription = '15',
        )
        return update_dashboard_info

# update Dashboard Published Version
"""
qs.update_dashboardでのダッシュボード名の変更を表示に反映。
ダッシュボード名が変更されたダッシュボードのバージョンナンバーをVersionNumberに設定。
qs.update_dashboardの返り値に含まれるバージョンナンバーを
VersionArn = update_dashboard_info['VersionArn']
target = '/version/'
idx = VersionArn.find(target)
version_str = VersionArn[idx + len(target):]
VersionNumber = int(version_str)
のように取得することが可能。
"""
def UpdateDashboardPublishedVersion (AwsAccountId, DashboardId, VersionNumber):
    qs.update_dashboard_published_version(
        AwsAccountId = AwsAccountId,
        DashboardId = DashboardId,
        VersionNumber = VersionNumber
    )
```
## User

```python
# register User
"""
IdentityType = 'IAM'として登録されたユーザはパスワードを入力せずにEmbed URLからダッシュボードやコンソールのページを表示することが可能。
QuickSightに登録されるユーザ名はIamArnとSessionNameから成る。
IamArn = 'arn:aws:iam::{}:role/<Role>'.format(AWS_ACCOUNT_ID), SessionName = 'TestUser'として設定した場合の
QuickSightに登録されるユーザ名は'<Role>/TestUser'。
登録するユーザが属している組織のIDをOrganizationListを指定することで、組織IDに等しいグループ名のグループがQuickSightに存在していれば
グループに作成したユーザを追加。
"""
def RegisterUser(qs, AwsAccountId, Email, IamArn, RegisterUserName, Namespace, qsUserName, UserList, OrganizationList):
    if qsUserName not in UserList:
        result = qs.register_user(
            IdentityType = 'IAM',
            Email = Email,
            UserRole = 'READER',
            IamArn = IamArn, 
            SessionName = RegisterUserName, 
            AwsAccountId = AwsAccountId, 
            Namespace = Namespace, 
            CustomPermissionsName = 'DataExplorer'
        )
        if result['Status'] == 201:
            if OrganizationList != []:
                for GroupName in OrganizationList:
                    qs.create_group_membership(
                        MemberName = qsUserName, 
                        GroupName = GroupName, 
                        AwsAccountId = AwsAccountId, 
                        Namespace = Namespace
                    )
            else:
                logger.debug('The organization that {} belongs to does not exist in QuickSight.'.format(qsUserName)) # log
        else:
            logger.debug('User {} creation failed'.format(qsUserName)) # log
    else:
        logger.debug('User {} already exists'.format(qsUserName)) # log

# delete User
def DeleteUser(qs, AwsAccountId, qsUserName, Namespace):
    qs.delete_user(
        UserName = qsUserName, 
        AwsAccountId = AwsAccountId, 
        Namespace = Namespace
    )

# update User permission for dashboard
def CreateGroupMembership(qs, AwsAccountId, qsUserName, GroupName, Namespace):
    qs.create_group_membership(
        MemberName = qsUserName, 
        GroupName = GroupName, 
        AwsAccountId = AwsAccountId, 
        Namespace = Namespace
    )

# update User permission for dashboard
def DeleteGroupMembership(qs, AwsAccountId, qsUserName, GroupName, Namespace):
    qs.delete_group_membership(
        MemberName = qsUserName, 
        GroupName = GroupName, 
        AwsAccountId = AwsAccountId, 
        Namespace = Namespace
    )
```

# まとめ
実際に開発を行う場合にはAPI仕様書や詳細設計書、シーケンス図の作成、DB設計などを行い、AWSであれば必要に応じてセキュリティグループやサブネットを設定、デプロイはserverless frameworkを利用しGitHubでソースコードのバージョン管理を行うなどの流れがありますが、ここではQuickSightに対して処理を行うPythonの関数を載せてみました。
認識の間違いなどありましたらご指摘のほどよろしくお願いいたします。