QuickSightのアクセスパターンとダッシュボードの埋め込みについて

Amazon QuickSightはAWSが提供するフルマネージドでサーバレスなBIサービスです。私はBIの分野には明るくないので他のBIサービスと比較してどうこうは分からないのですが、AWS内のデータソースとの統合のしやすさやMLとの統合など活発な機能追加が行われていて、とても魅力的なサービスだなと感じています。

そんなQuickSightを今回始めて導入しようとしたわけですが、アクセス方法がいくつもあり選択で迷ったので、まとめておきたいと思います。また後半ではQuickSightのダッシュボードをWebアプリケーションに埋め込む方法についても説明したいと思います。

ユーザ管理の概念

まずはじめにQuickSightが管理するユーザの概念について整理します。

ユーザ

ユーザはQuickSightのサービスにアクセスする主体(人)を管理する概念です。QuickSightが管理するユーザには2つのタイプがあります。1つはIAM(ユーザやロール)と紐づくユーザ、もう1つはQuickSightが独自に管理するユーザです。

IdentityType IAM User 説明
IAM YES IAM(ユーザやロール)に紐づくユーザです。IAMロールの場合はセッション名まで含めて一意なユーザとして管理されます。
QUICKSIGHT NO IAMに紐付かない、QuickSightが独自に管理するユーザです。Emailアドレスで一意に管理されます。

※ この他にActive Directory連携やSAML連携などもありますが、これらがどちらのIdentityTypeになるのかは未確認です。

ロール

ロールはユーザがQuickSightで実行できる操作の権限を管理する概念です。全てのユーザには必ず1つのロールが紐付きます。

Role 説明
ADMIN すべての操作ができる
AUTHOR 分析とダッシュボードの作成ができる
READER 共有されたダッシュボードの閲覧のみできる

グループ

グループはユーザを束ねる概念で、分析やダッシュボードの共有に使われます。分析やダッシュボードは、作った人以外が編集・閲覧できるようにするには、特定のユーザやグループに対して共有を行う必要があります。共有をユーザ単位に設定すると管理が煩雑になりますので、あらかじめ共有したいユーザをグループに追加しておき、グループに対して共有を行うのが良いです。

なお現時点(2020/08/19)では、グループの作成や管理はAWS CLIからのみ行うことができます。

QuickSightのアクセスパターン

QuickSightコンソール

QuickSightコンソールへログインして分析やダッシュボードにアクセスする方法です。

アクセス方法 IdentityType 説明 Edition
パスワードログイン QUICKSIGHT QuickSightコンソールのログイン画面から、アカウント名・ユーザ名・パスワードを使用してサインインします。 Standard/Enterprise
AWS Management ConsoleからのSSO IAM AWS Management Consoleにログインしている状態でQuickSightコンソールへアクセスすると、SSOによりサインインします。 Standard/Enterprise
SAML2.0やOIDCによるSSO 未確認 SAML2.0のIDフェデレーションやOIDCを設定することにより、IDプロバイダーのログインセッションを利用してQuickSightコンソールへサインインします。 Standard/Enterprise
Active DirectoryによるSSO 未確認 Active Directoryのグループに対してQuickSightへのアクセス権限を与えることでActive Directoryのユーザでサインインします。 Enterprise

ダッシュボードの埋め込み

こちらの機能はEnterprise Edition限定です。

QuickSightにはダッシュボードを既存のWebアプリケーションに埋め込むための機能が提供されています。埋め込みのダッシュボードは読み取り専用(フィルタなどは可能)ですので、READERに提供するためのアクセスパターンとなります。

この方法の良いところは、ダッシュボードへのアクセスの認証・認可を既存のWebアプリケーションに任せられることです。IAMユーザやロールを持っている社員であったり、SAMLやActive Directoryなどが統合されている場合はQuickSightコンソールを利用すれば良いと思うのですが、そうでない場合にREADER一人ひとりにIAMユーザやQuickSight独自のユーザを発行するのは、利用者にも管理者にも負担に鳴ります。ダッシュボードを普段使っているWebアプリケーションに埋め込めば、利用者はQuickSightへのアクセスを意識する必要がありません。

QuickSightコンソールの埋め込み

(追記: 2020/08/25) 情報提供をいただきましたので追記しました。

こちらの機能はEnterprise Edition限定です。

「ダッシュボードの埋め込み」では特定のダッシュボードだけをWebアプリケーションに埋め込むことができますが、「QuickSightコンソールの埋め込み」を使うとデータセット・分析・ダッシュボードなどを含むQuickSightコンソールをWebアプリケーションに埋め込むことができます。また、公開したい範囲や操作を一部制限することもできるそうです。

https://docs.aws.amazon.com/quicksight/latest/user/embedding-the-quicksight-console.html

※まだ日本語のページは存在しません。

ダッシュボードの埋め込み手順

大まかな手順としては以下のようになります。

  1. ダッシュボードとグループを作成する。ダッシュボードをグループに共有する。
  2. ダッシュボードを埋め込むWebアプリケーションのドメインを登録(CORS対応)する。
  3. QuickSightからEmbedUrlを取得するためのIAMロールを作成する。
  4. サーバサイドでEmbedUrlを取得してWebアプリケーションに返すAPIを用意する。
  5. WebアプリケーションでEmbedUrlを発行するAPIを叩き、Amazon QuickSight 埋め込み SDKに使用してダッシュボードを表示する。

4以外はドキュメント通りなので問題ないかと思います。ちょっとややこしいのが4の部分なのですが、ここでやりたいことは、Webアプリケーションの認証情報を検証して所有者を特定し、所有者の名のもとにQuickSightのユーザとしてEmbedUrlを発行するということです。

Webアプリケーション側でSTSを使って一時クレデンシャルを取得し、EmbedUrlを取得してもいいのでは?と思うかもしれませんが、現時点(2020/08/20)で、Webアプリケーションからの GetDashboardEmbedUrl の実行は許可されていません

Currently, you can use GetDashboardEmbedURL only from the server, not from the user's browser.

また、QuickSightのユーザをWebアプリケーションのすべての利用者で共有する方法も考えられますが、同時に複数の端末からアクセスされるとセッションが切れるので基本的には1端末1ユーザとしてアクセスします。

実装例

今回の導入では、Auth0を認証プロバイダとするWebアプリケーションにダッシュボードの埋め込みを行いました。Auth0はOIDCプロバイダとして機能しますので、OIDCプロバイダを使った場合の説明します。Webアプリケーションの認証方法としては、他にも独自認証やOAuth2.0、Cognitoを使っている場合などがありますが基本的な考えは先に述べたとおりです。

構成

OIDCプロバイダを使った構成例です。Auth0の部分はOIDCプロバイダであれば何でも良いです。

IAMロール

AWS CDKで作る例です。

const providerName = 'IAMに登録したプロバイダ名'
const audience = 'WebアプリケーションのclientId'

new iam.Role(this, 'QuicksightDashboardReaderRole', {
  roleName: 'QuicksightDashboardReaderRole',
  inlinePolicies: {
    QuicksightDashboardReaderPolicy: new iam.PolicyDocument({
      statements: [
        new iam.PolicyStatement({
          effect: iam.Effect.ALLOW,
          actions: ['quicksight:RegisterUser', 'quicksight:CreateGroupMembership'],
          resources: ['*']
        }),
        new iam.PolicyStatement({
          effect: iam.Effect.ALLOW,
          actions: ['quicksight:GetDashboardEmbedUrl'],
          resources: [`arn:aws:quicksight:${props?.env?.region}:${props?.env?.account}:dashboard/*`]
            })
          ]
        })
      },
      assumedBy: new iam.FederatedPrincipal(`arn:aws:iam::${props?.env?.account}:oidc-provider/${providerName}`, {
        'StringEquals': {
          [`${providerName}:aud`]: audience
        },
      }, 'sts:AssumeRoleWithWebIdentity'),
    })

AssumeRoleWithWebIdentityの実行時に権限がない( AccessDenied: Not authorized to perform sts:AssumeRoleWithWebIdentity )のようなエラーになる場合、Principalの設定が誤っている可能性があります。設定すれば必ず通りますので設定をよく確認してみてください。Lambda側のロールのポリシーは AWSLambdaBasicExecutionRole 管理ポリシーだけで大丈夫です。

Lambdaのソースコード

サーバサイドの実装例です。例としてLambdaを利用していますが、EC2などのサーバアプリケーションでも問題ありません。

import { Context, APIGatewayProxyEvent } from 'aws-lambda'
import AWS from 'aws-sdk'
import jwt from 'jsonwebtoken'

const dashboardId = process.env.DASHBOARD_ID // ダッシュボードIDは環境変数で受け取ります
const roleName = 'QuicksightDashboardReaderRole'

export const handler = async (event: APIGatewayProxyEvent, context: Context) => {
  try {
    const awsAccountId = context.invokedFunctionArn.match(/\d{3,}/)[0]
    const roleArn = `arn:aws:iam::${awsAccountId}:role/${roleName}`
    const idToken = event.headers['Authorization'] // Bearer部分は除外する
    const decoded = jwt.decode(idToken) // VerifyはassumeRoleWithWebIdentityで行う
    const email = decoded['email'] // 'email' claimを取得するにはprofile scopeを指定する必要があります
    const sessionName = email // sessionNameもemailと同じにします

    AWS.config.region = process.env.AWS_REGION

    const stsClient = new AWS.STS()
    const stsResponse = await stsClient.assumeRoleWithWebIdentity({
      RoleArn: roleArn,
      RoleSessionName: sessionName,
      WebIdentityToken: idToken
    }).promise()

    const quicksight = new AWS.QuickSight({
      apiVersion: '2018-04-01',
      credentials: {
        accessKeyId: stsResponse.Credentials.AccessKeyId,
        secretAccessKey: stsResponse.Credentials.SecretAccessKey,
        sessionToken: stsResponse.Credentials.SessionToken
      }
    })

    let describeUserResponse: AWS.QuickSight.DescribeUserResponse | undefined

    // ユーザが既に登録されている場合はユーザ登録処理をスキップする
    try {
      describeUserResponse = await quicksight.describeUser({
        AwsAccountId: awsAccountId,
        Namespace: 'default',
        UserName: `${roleName}/${email}`
      }).promise()
    } catch (error) {
      // ユーザが未登録の場合はerror.codeがResourceNotFoundExceptionとなります
      if (error.code !== 'ResourceNotFoundException') {
        throw error
      }
    }

    if (!describeUserResponse) {
      // ユーザを登録
      const registerUserResponse = await quicksight.registerUser({
        AwsAccountId: awsAccountId,
        Namespace: 'default',
        IdentityType: 'IAM',
        UserRole: 'READER',
        Email: email,
        IamArn: roleArn,
        SessionName: sessionName
      }).promise()

      // ユーザをグループに追加
      await quicksight.createGroupMembership({
        AwsAccountId: awsAccountId,
        Namespace: 'default',
        GroupName: 'reader-group', // グループは予め用意されているものとします
        MemberName: registerUserResponse.User.UserName
      }).promise()
    }

    // Embed Urlを取得
    const getDashboardEmbedUrlResponse = await quicksight.getDashboardEmbedUrl({
      AwsAccountId: awsAccountId,
      DashboardId: dashboardId,
      IdentityType: 'IAM',
      ResetDisabled: true,
      SessionLifetimeInMinutes: 100,
      UndoRedoDisabled: false
    }).promise()

    context.succeed({
      embedUrl: getDashboardEmbedUrlResponse.EmbedUrl
    })
  }
  catch (err) {
    console.error(err)
    context.fail(err)
  }
}

参考