AWS QuickSight es un servicio de análisis de datos de AWS que, como ya se menciona en este post, lleva disponible desde 2016. Su objetivo inicial era permitir a perfiles no técnicos realizar análisis ad-hoc, así como la visualización de dichos análisis de forma rápida y sencilla. Lo cierto es que, con el tiempo, QuickSight se ha ido integrando con el resto de servicios tanto de almacenamiento como de análisis que existían y con muchos otros que han ido surgiendo en el ecosistema de AWS a lo largo de estos años. De esta forma, QuickSight ha mantenido su espíritu de herramienta con un interfaz amigable e intuitivo, a la vez que ha crecido en potencia y funcionalidad.

Para entender un poco en qué consiste QuickSight y los distintos elementos que hay que manejar para realizar un análisis, es recomendable recurrir a este otro artículo que explica en qué consiste un análisis, un visual y un dashboard. Así por ejemplo, un análisis es la base donde se definen, a partir de visuales, los datos, el origen de los mismos, las operaciones o el filtrado de dichos datos... Estos análisis se pueden publicar en forma de dashboards con un tema o presentación determinados.

Buenas prácticas de AWS… y en general

En la actualidad los proyectos de migración en la nube son cada vez más grandes y por ello hay que insistir en la necesidad de seguir las buenas prácticas que propone AWS para este tipo de migraciones. Una de estas propuestas es la de la segmentación en múltiples cuentas para una misma organización, así como el uso de roles para gestionar los permisos de los usuarios sobre estas cuentas.

Por último, una buena práctica generalizada (no solo en AWS sino en general para entornos productivos) es el uso de herramientas de infraestructura como código así como procedimientos de continuous delivery ya mencionados también en otros posts de este blog como este sobre pipelines de Jenkins.

De esta forma, una empresa podría tener una cuenta para cada entorno (desarrollo, preproducción, producción) y otra para la gestión económica y de los usuarios, en la que definir qué usuarios pueden asumir uno u otro rol de cada cuenta. Así por ejemplo, en desarrollo, todo el equipo puede prototipar y hacer pruebas del código, de análisis de datos, etc., pero en el resto de cuentas, las modificaciones de infraestructura y código deberían estar automatizadas, a fin de que los usuarios no necesiten tocar nada a mano en esas cuentas.

¿Cómo migrar un análisis de QuickSight?

Pero, ¿y si en nuestro caso lo que hay que migrar es un análisis de QuickSight? ¿Qué mecanismos existen para, sin perder la usabilidad de este servicio, automatizar el despliegue de ese análisis en forma de Dashboard en otras cuentas productivas?

Lo más inmediato podría ser utilizar alguna herramienta de infraestructura como código del tipo AWS CloudFormation o Terraform.

En el caso que se aborda en este post, existen varias formas de realizar esta migración, que dependen en gran medida de la forma de trabajar. Lo cierto es que AWS QuickSight, como la gran mayoría de servicios de AWS tiene soporte de CloudFormation. Podría parecer entonces que se puede definir un análisis, un tema y un dashboard usando plantillas de CloudFormation y luego desplegando dichas plantillas en la cuenta que se desee. Lo cierto es que si se presta atención a la definición de los parámetros que hay que incluir en CloudFormation, a fecha de hoy, la mayoría de ellos no están actualmente soportados. No se puede definir mediante Cloudformation ni los permisos del análisis, fuentes de datos o parámetros del mismo. A fecha de hoy, por lo tanto, esta opción no parece viable o al menos no recomendable.

Esta opción parece mucho más completa y plausible que Cloudformation, pues como se puede observar en la documentación del API, permite definir parámetros a la hora de definir un análisis. Para crear un análisis sería tan sencillo como incluir en tu script en Python el siguiente comando, y sustituir con los parámetros correspondientes (tema del análisis, fuentes de datos, permisos…). Dichos parámetros son, en muchos casos, ARN o identificadores de los recursos (tema, set de datos, usuarios…). Dichos recursos tienen que ser, necesariamente, creados, migrados o listados con boto3, ya que deben existir en la cuenta destino:

response = client.create_analysis(
    AwsAccountId='string',
    AnalysisId='string',
    Name='string',
    Parameters={
        'StringParameters': [
            {
                'Name': 'string',
                'Values': [
                    'string',
                ]
            },
        ],
        'IntegerParameters': [
            {
                'Name': 'string',
                'Values': [
                    123,
                ]
            },
        ],
        'DecimalParameters': [
            {
                'Name': 'string',
                'Values': [
                    123.0,
                ]
            },
        ],
        'DateTimeParameters': [
            {
                'Name': 'string',
                'Values': [
                    datetime(2015, 1, 1),
                ]
            },
        ]
    },
    Permissions=[
        {
            'Principal': 'string',
            'Actions': [
                'string',
            ]
        },
    ],
    SourceEntity={
        'SourceTemplate': {
            'DataSetReferences': [
                {
                    'DataSetPlaceholder': 'string',
                    'DataSetArn': 'string'
                },
            ],
            'Arn': 'string'
        }
    },
    ThemeArn='string',
    Tags=[
        {
            'Key': 'string',
            'Value': 'string'
        },
    ]
)

El inconveniente evidente de usar esta forma de trabajar es que resta a QuickSight su mayor potencial: el de permitir que esos análisis se realicen de forma fácil e intuitiva.

La solución que parece más recomendable es dejar al analista de datos que trabaje de forma cómoda y rápida en un entorno de desarrollo, y encontrar una forma de migrar el resultado de su trabajo entre cuentas sin que eso afecte a su forma de trabajar.

Para ello, existe un concepto relativamente nuevo en AWS QuickSight: el de plantilla o template. Una plantilla es simplemente una definición que se obtiene a partir de un análisis o de otra plantilla y que contiene los metadatos necesarios para crear un dashboard en una cuenta destino. Los pasos a seguir para realizar esta migración pueden hacerse tanto con el cliente de AWS como por Boto3; eso sí, no está disponible aún por la consola de AWS.

El proceso que hay que seguir es el siguiente :

  1. Crear una plantilla a partir del análisis

Para esto es necesario que el análisis exista (obvio), y conocer el nombre del mismo. También hace falta el Identificador de la cuenta de AWS donde se ha creado el análisis, así como tener configurado el cliente de AWS con un perfil válido y los permisos correspondientes de QuickSight. El comando que se utilizará para este paso es create_template:

quicksight = boto3.session.Session(profile_name=profile).client('quicksight')

response= quicksight.create_template(
       AwsAccountId = account_id,
       TemplateId = template_id,
       Name = template_name,
         Permissions =[
             {
                 'Principal': '*', 
                 'Actions': [
                     'quicksight:DescribeTemplate',
                 ]
             },
         ],
       SourceEntity={
               'SourceAnalysis': {
               'Arn': analysis_arn,
               'DataSetReferences': dataset_references
            } 
       }
        VersionDescription= 'v01’
   )

Antes de este comando, como se puede apreciar en el código, hay que determinar el valor del ARN del Análisis, así como el nombre y ARN de los data sets que necesita. Estos dos valores se obtienen a partir de una llamada a describe_analysis. Con el identificador que hay que obtener previamente ejecutando list_analyses a partir del nombre del análisis:

analyses= quicksight.list_analyses(
      AwsAccountId = account_id
   ).get('AnalysisSummaryList')

analysis_id=''

for analysis in analyses:
   if (analysis_name == analysis.get('Name')):
      analysis_id=analysis.get('AnalysisId')

response = quicksight.describe_analysis(
   AwsAccountId = account_id,
   AnalysisId = analysis_id
)
analysis_arn = response.get('Analysis').get('Arn')
datasets_arns = response.get('Analysis').get('DataSetArns')

dataset_references = list()

for dataset in analysis[1]:
   dataset_id = dataset.split("/")[-1]
   (dataset_arn, dataset_name)=describe_datasets(account_id, dataset_id,quicksight)
   dataset_references.append({"DataSetPlaceholder": dataset_name,"DataSetArn": dataset_arn})
  1. Migrar el tema del análisis entre cuentas

El primer paso para migrar el tema del análisis es, a partir del ARN de la plantilla original, obtener el identificador del tema, su nombre, si se ha basado en otro tema previo y la configuración del mismo. Esto se consigue mediante el método describe_template de QuickSight con el identificador del template (para conseguir el identificador del tema). Posteriormente con describe_theme y el identificador del tema, se obtienen los campos mencionados:

quicksight = boto3.session.Session(profile_name=origin_profile).client('quicksight')
   template_id=template_arn.split("/")[-1]

   template_info = quicksight.describe_template(
      AwsAccountId= origin_account_id,
      TemplateId= template_id
   )
   theme_arn = template_info.get('Template').get('Version').get('ThemeArn')
   theme_id = theme_arn.split("/")[-1]

   theme_info = quicksight.describe_theme(
      AwsAccountId = origin_account_id,
      ThemeId = theme_id
   )
   theme_name = theme_info.get('Theme').get('Name')
   base_theme_id = theme_info.get('Theme').get('Version').get('BaseThemeId')
   theme_configuration = theme_info.get('Theme').get('Version').get('Configuration')

Esta información es necesaria para, en la cuenta destino, crear un tema igual que el que existe en la cuenta origen (donde se encuentra el análisis). Para ello es preciso instanciar una nueva sesión de AWS QuickSight con el perfil de usuario de la cuenta destino:

quicksight = boto3.session.Session(profile_name=destination_profile).client('quicksight')

   response = quicksight.create_theme(
      AwsAccountId=destination_account_id,
      ThemeId=theme_id,
      Name=theme_name,
      BaseThemeId=base_theme_id,
      Configuration=theme_configuration,
   )

   new_theme_arn=response.get('Arn')

El ARN del nuevo tema es importante, pues es necesario en el paso de creación del dashboard final.

  1. Migrar los sets y fuentes de datos (AWS QuickSight data set y data sources) entre cuentas

A continuación es necesario migrar los data sets y data sources creados en AWS QuickSight a la cuenta destino. Hay que tener en cuenta que en la cuenta destino deben existir los orígenes de datos (s3, athena…) creados tal cual en la cuenta origen, si no, por mucho que se creen en AWS QuickSight, al acceder a los orígenes de datos, se obtendrá un error. Hay muchas formas de realizar dicha migración de los datos (definición mediante código, replicación entre buckets...), y depende mucho del origen de los mismos (si es S3, Athena…).

El siguiente paso consiste en la creación de la definición de set de datos y origen de datos en AWS QuickSight. Para ello, hay que configurar los permisos que deben tener en la cuenta destino. En este caso, se va a dar permisos a todos los usuarios de QuickSight de la cuenta destino como sigue (en este caso el perfil usado es el de la cuenta destino):

   quicksight_origin = boto3.session.Session(profile_name=destination_profile).client('quicksight')

   users=quicksight_destination.list_users(
      AwsAccountId=account_id,
      Namespace='default'
   )
   users_arn=[]

   for user in users.get('UserList'):
      users_arn.append(user.get('Arn'))

   permissions_datasource = []
   for quicksight_user_arn in users:
      permissions_datasource.append({"Principal": quicksight_user_arn, "Actions": [
         "quicksight:UpdateDataSourcePermissions",
         "quicksight:DescribeDataSource",
         "quicksight:DescribeDataSourcePermissions",
         "quicksight:PassDataSource",
         "quicksight:UpdateDataSource",
         "quicksight:DeleteDataSource"
      ]})

   permissions_dataset = []
   for quicksight_user_arn in users:
      permissions_dataset.append({"Principal": quicksight_user_arn, "Actions": [
         "quicksight:UpdateDataSetPermissions",
         "quicksight:DescribeDataSet",
         "quicksight:DescribeDataSetPermissions",
         "quicksight:PassDataSet",
         "quicksight:DescribeIngestion",
         "quicksight:ListIngestions",
         "quicksight:UpdateDataSet",
         "quicksight:DeleteDataSet",
         "quicksight:CreateIngestion",
         "quicksight:CancelIngestion"
      ]})

A continuación, a partir de la lista de ARNs de los data set que se obtuvieron con el comando describe_analysis en el paso 1 (variable datasets), hay que obtener para cada data set su identificador. A continuación, mediante el comando describe_data_set, se consigue la configuración del data set, entre la que figura el ARN del data source, su Identificador, el tipo y el nombre. Estos datos, junto con los permisos que se configuraron anteriormente, permitirán, con una sesión distinta de QuickSight (estanciada con el perfil de la cuenta destino) crear los data sources y los data set, iguales que los de la cuenta origen:

   quicksight_origin = boto3.session.Session(profile_name=origin_profile).client('quicksight')

   destination_dataset_references=[]
   for dataset in datasets:
      origin_dataset_id = dataset.split("/")[-1]

      describe_set = quicksight_origin.describe_data_set(
          AwsAccountId = account_id,
          DataSetId = origin_dataset_id
          )
      origin_dataset_configuration = describe_set.get('DataSet')
      origin_dataset_name = dataset_configuration.get('Name')
      origin_dataset_physical_table = dataset_configuration.get('PhysicalTableMap')

      physical_table_str = json.dumps(origin_dataset_physical_table)
      origin_datasource_arn = ''
      origin_datasource_name = ''
      origin_datasource_type = ''

      physical_table_str_splitted = physical_table_str.split("\"")
      for str_splitted in physical_table_str_splitted:
         if 'arn' in str_splitted:
            origin_datasource_arn = str_splitted

      origin_datasource_id = origin_datasource_arn.split("/")[-1]
      describe_source = quicksight_origin.describe_data_source(
          AwsAccountId = account_id,
          DataSourceId= origin_datasource_id
          )

      origin_datasource_name = describe_source.get('DataSource').get('Name')
      datasources = quicksight_origin.list_data_sources(
           AwsAccountId=account_id
       )

      for datasource in datasources.get('DataSources'):
         if (datasource.get('Name')==origin_datasource_name):
            origin_datasource_arn= datasource.get('Arn')
            origin_datasource_type= datasource.get('Type')

      quicksight_destination = boto3.session.Session(profile_name=destination_profile).client('quicksight')

      create_source = quicksight_destination.create_data_source(
         AwsAccountId=account_id,
         DataSourceId=origin_datasource_id,
         Name=origin_datasource_name,
         Type=origin_datasource_type,
         DataSourceParameters={
            'AthenaParameters':{
               'WorkGroup':'scripts'
            }
         },
         Permissions= permissions_datasource
      )

      destination_datasource_arn=create_source.get(‘Arn’)

      destination_physical_table_str = physical_table_str.replace(str(origin_account_id),str(destination_account_id)).replace(origin_region,str(destination_region)).replace(datasource_arn, destination_datasource_arn)

      destination_physical_table = json.loads(destination_physical_table_str)
      origin_dataset_import_mode = origin_dataset_configuration.get('ImportMode')

      response_create_set = quicksight_destination.create_data_set(
         AwsAccountId=destination_account_id,
         DataSetId=origin_dataset_id,
         Name=origin_dataset_name,
         PhysicalTableMap=destination_physical_table,
         ImportMode=origin_dataset_import_mode,
         Permissions=permissions_dataset
      )

      destination_dataset_arn= response_create_dataset.get('Arn')

      destination_dataset_references.append({"DataSetPlaceholder": destination_dataset_name,"DataSetArn": destination_dataset_arn})
  1. Migrar la plantilla a la cuenta destino.

Durante el primer paso de este procedimiento, se creó en la cuenta origen, a partir del análisis, una plantilla. Con ella, se puede publicar un dashboard en la cuenta original, pero también una plantilla a partir de ella en otra cuenta distinta. El objetivo de este punto es este: migrar dicha plantilla a otra cuenta destino, donde recrear con ella el dashboard.

Para ello, lo primero que hace falta es dar permisos a la plantilla original para poder acceder a ella desde la cuenta destino:

quicksight = boto3.session.Session(profile_name=origin_profile).client('quicksight')

template_id=template_arn.split("/")[-1]
principal='arn:aws:iam::'+ destination_account_id +':root' 

response =quicksight.update_template_permissions(
   AwsAccountId=origin_account_id,
   TemplateId=template_id,
   GrantPermissions=[
      {
         'Principal': principal,
         'Actions': [
             'quicksight:UpdateTemplatePermissions',
             'quicksight:DescribeTemplate'
         ]
      },         
   ]

A continuación se creará la plantilla en la cuenta destino como sigue (a partir del nombre y ARN de la plantilla original):

   quicksight = boto3.session.Session(profile_name=destination_profile).client('quicksight')

   template_id=template_arn.split("/")[-1]
   response= quicksight.create_template(
       AwsAccountId = destination_account_id,
       TemplateId = template_id,
       Name = template_name,
       SourceEntity={
           'SourceTemplate': {
               'Arn': template_arn,
            } 
       }
   )

      new_template_arn = response.get('Arn')
  1. Crear el dashboard en la cuenta destino a partir del templat

Recapitulando, en la cuenta destino están creados tanto el tema original, como los sets y fuentes de datos. Únicamente hace falta crear el dashboard en la cuenta destino, con los valores correctos de nombre, id, ARN de la plantilla migrada a la nueva cuenta y ARN del tema que se creó en la cuenta destino como sigue:

   quicksight = boto3.session.Session(profile_name=destination_profile).client('quicksight')

   dash_permissions = []
   for quicksight_user_arn in users:
      dash_permissions.append({"Principal": quicksight_user_arn, "Actions": [
         "quicksight:DescribeDashboard",
         "quicksight:ListDashboardVersions",
         "quicksight:UpdateDashboardPermissions",
         “quicksight:QueryDashboard",
         "quicksight:UpdateDashboard",
         "quicksight:DeleteDashboard",
         "quicksight:DescribeDashboardPermissions",
         "quicksight:UpdateDashboardPublishedVersion"
       ]})

   response= quicksight.create_dashboard(
    AwsAccountId = destination_destination_account_id,
    DashboardId = dashboard_id,
    Name = dashboard_name,
    Permissions = dash_permissions,
    SourceEntity={
             'SourceTemplate': {
         'Arn':destination_template_arn,
        'DataSetReferences': destination_dataset_references
        } 
},
    VersionDescription = 'dashboard test',
    DashboardPublishOptions ={
       'AdHocFilteringOption': {
       'AvailabilityStatus': 'ENABLED'
       },
       'ExportToCSVOption': {
           'AvailabilityStatus': 'ENABLED'
       },
       'SheetControlsOption': {
          'VisibilityState': 'EXPANDED'
       }
    },
           ThemeArn=new_theme_arn
    )

De esta forma, todos los usuarios de AWS QuickSight de la cuenta destino podrán visualizar el Dashboard resultado de esta migración.

Este proceso de migración puede ser en la misma cuenta o entre cuentas. La diferencia fundamental (como ya comentamos previamente) es que al migrar a una cuenta distinta, no existen ni el tema, sets de datos, fuente de datos, ni, por supuesto, la plantilla a partir de la cual crear el dashboard. Es decir, para crear un dashboard a partir de un análisis en la misma cuenta, solo habría que ejecutar los pasos de creación del template y de creación del dashboard (primero y último del procedimiento), mientras que si la migración es entre cuentas habría que ejecutar todos los pasos anteriormente mencionados.

Del mismo modo, si la migración es en la misma cuenta, no sería necesario, obviamente usar dos perfiles y números de cuenta, como sí se indica en el procedimiento descrito más arriba.

Otro punto que hay que dejar claro es que el proceso descrito previamente es de alto nivel y puede variar ligeramente dependiendo de los orígenes de los datos, configuraciones de las cuentas, etc… Por poner un ejemplo, el origen de datos en el ejemplo descrito es Athena y como tal, al crear el datasource se ha incluido el parámetro siguiente, que variaría en caso de ser otra fuente de datos:

      DataSourceParameters={
         'AthenaParameters':{
            'WorkGroup':'scripts'
         }
      },

Por otro lado, también sería recomendable incluir en el código la gestión de excepciones de los comandos de QuickSight para cuando, por ejemplo, los objetos que se crean en la cuenta destino ya existen. No se ha contemplado tampoco el versionado de los mismos (que sí está contemplado en la definición de plantillas de AWS QuickSight).

Por último, sería recomendable hacer una selección de qué usuarios deben tener acceso al Dashboard (no dar permisos a todos los usuarios de forma genérica), así como una selección más minuciosa de los permisos que deben tener cada uno de los objetos creados (template, data sources, temas…).

Volviendo al ciclo de vida…

Antes de terminar, queda hacer una reflexión sobre cómo podría integrarse este procedimiento de migración de forma que pudiera crearse, borrarse o actualizarse durante despliegue.

Como el lector habrá podido observar, este procedimiento no contempla la actualización continua de los elementos, sino su creación desde cero en una cuenta nueva. Para poder incluir la actualización de dicho dashboard, dataset y datasources dentro de un flujo de despliegue continuo, se recomienda usar una lambda que responda a un evento custom. Dicho custom resource se gestiona en la plantilla de cloudformation y cada vez que se produce un evento en la misma (creación, borrado, actualización), hace que se ejecute la función con un parámetro de entrada u otro (el evento, que es el parámetros de entrada de lambda será de tipo creación, borrado y actualización). De esta forma, se podría incluir en el ciclo de vida de los elementos, el borrado y actualización de los elementos en función del evento que envíe cloudformation a la lambda a través de dicho recurso custom.

¡Esperamos que este post sea de utilidad para tus futuras migraciones de QuickSight!

Cuéntanos qué te parece.

Los comentarios serán moderados. Serán visibles si aportan un argumento constructivo. Si no estás de acuerdo con algún punto, por favor, muestra tus opiniones de manera educada.

Suscríbete

Estamos comprometidos.

Tecnología, personas e impacto positivo.