名前空間を用いたQuickSight上でのマルチテナントの実現

Work illustrations by Storyset

こんにちは、スタメンの滿本、若園、田中、近藤です。スクラムでのチーム名は、チームねぎまです。 (2022年冬からスクラム開発に移行しました)

本記事では、QuickSightの概要、マルチテナント構成とその運用方法について紹介します。

QuickSightとは

QuickSightは、AWSが提供しているクラウドBIサービスです。 本項では、QuickSightの主な構成要素であるユーザーとアセットについて紹介します。

ユーザー

QuickSightでは、下記の3種類のユーザー種別があります。

ロール 権限範囲 エンタープライズ版の費用 *1
管理者(ADMIN) 各種設定の変更, ダッシュボードの作成 24ドル/月
作成者(AUTHOR) ダッシュボードの作成 24ドル/月
閲覧者(READER) ダッシュボードの閲覧 最大5ドル/月

アセット

QuickSightでは、データ、分析、ダッシュボードの3つをアセットと呼びます。

データ

データソースとデータセット

グラフなどを用いた分析を行う前に、データソースへの接続もしくはアップロードを行い、データセットと呼ばれるデータ群を準備する必要があります。QuickSightは、様々なデータソースに対応しています。データセットは、データソース内の指定した任意のテーブルを元に作成することができ、テーブルの中身をそのまま使用したり、SQLを用いてカスタマイズした状態でデータセット化することができます。

※サンプルデータです。

分析

分析は、データセットを元に作成したグラフやピボットテーブルなどの要素群のことを指し、作成した分析をダッシュボードに公開することで分析結果をダッシュボード化することができます。

※サンプルデータです。

ダッシュボード

ダッシュボードは、複数シート化・Eメール送信・印刷・PDF出力ができ、APIを用いると埋め込み用のURLを生成することもできます。

※サンプルデータです。

QuickSightを用いたマルチテナント構成

QuickSightには、ユーザーをまとめる概念として、グループ名前空間(Namespace)の2つがあります。

グループを用いる場合、グループを作成しユーザーを所属させ、そのグループ単位でアセットへのアクセス権限を付与することできます。 この場合、QuickSightにデフォルトで存在している名前空間(名前空間名はdefault)上に、全ユーザー・アセットが存在している状態になります。 同一の名前空間上に存在しているため、アクセス権限付与のミスオペレーションなどでグループ間で意図しないアセットが共有されてしまう恐れがあり、マルチテナント構成には向いていないことが分かりました。

一方で名前空間(Namespace)を用いる場合、名前空間別に環境を分けることができるため、確実にユーザー・各アセットを隔離された空間に分離できます。これにより自分が所属している名前空間以外のユーザーとアセット共有することが出来なくなります。

今回の検証では下図のようなマルチテナント構成を準備しました。

AWS CLI *2によるユーザーとデータの作成

以下の手順でQuickSightのマルチテナント構成を構築しました。

  1. 名前空間の作成
  2. ユーザーの作成・登録
  3. データソースの作成
  4. データセットの作成

名前空間の作成

QuickSightの名前空間を作成します。

$ aws quicksight create-namespace \
  --aws-account-id {aws-account-id} \
  --namespace {namespace} \
  --identity-type QUICKSIGHT

ユーザーの作成、QuickSightへの登録

IAMでユーザーを作成し、QuickSightへ登録します。分析を作成する権限を持つユーザーは user-role をAUTHORに指定し、閲覧のみのユーザーはREADERとします。

$ aws quicksight register-user \
  --aws-account-id {aws-account-id} \
  --namespace {namespace} \
  --identity-type IAM \
  --email {email} \
  --user-role {role} \
  --iam-arn {iam_arn}

データソースの作成

今回はAthenaからデータソースを作成しました。permissionsでquicksight:UpdateDataSourcequicksight:DeleteDataSourceなどの必要な権限を付与しています。

$ aws quicksight create-data-source \
  --aws-account-id {aws-account-id} \
  --data-source-id {data_source_id} \
  --name {data_source_name} \
  --type ATHENA \
  --data-source-parameters AthenaParameters={WorkGroup=primary} \
  --permissions Principal={user_arn},Actions={permitted_actions}

データセットの作成

クエリが複数行にわたるため、cli-input-jsonオプションでjsonファイルを指定してデータセットを作成します。

$ aws quicksight create-data-set \
  --cli-input-json {cli_input_json_file_path}
// 読み込ませるjson
{
  "AwsAccountId": aws_account_id,
  "DataSetId": data_set_id,
  "Name": data_set_name,
  "PhysicalTableMap": {
    "AthenaPhysicalTable": {
      "CustomSql": {
        "DataSourceArn": data_source_arn,
        "Name": custom_sql_name,
        "SqlQuery": sql_query, // Athenaで実行するSQL
        "Columns": columns, // データセットとして利用するカラムの Name と Type を指定
      }
    }
  },
  "ImportMode": 'SPICE', // SPICE または DIRECT_QUERY
  "Permissions": [
    {
      "Actions": permitted_actions,
      "Principal": user_arn
    }
  ]
}

上記の手順でQuickSightコンソールから分析を作成できる環境を用意しました。

日々のSPICEインポート更新の設定と管理

QuicksightではSPICEと呼ばれる、高速なインメモリエンジンをデータ格納先として使用することができます 。 データセットの作成・編集においてSQLを用いる場合、「直接クエリ」と「SPICEへのインポート*3」という2種類のクエリモードのいずれかを選択できます。

下記の比較の通り、直接クエリと比較してメリットが大きかった為、SPICEを使用することが決まりました。

SPICEと直接クエリの比較

SPICEのメリット

①処理速度
SPICEにデータをインポートすることで、分析作成時やダッシュボード閲覧時において明らかな高速化が確認出来ました。

②コスト
直接クエリの場合、データセットを参照する毎に、データのクエリに対して課金されます。一方で、 SPICEのデータはインポートするデータの容量 *4で決まるため、一旦インポートすればコストは固定です。

SPICEのデメリット

容量管理
使用可能なSPICEの残容量は、QuickSightのコンソール上で確認することになります。容量は事前購入の必要があり、オートスケールさせることが出来ません。その為、容量が不足している場合は更新が失敗します。

SPICEデータの更新

SPICEの「スケジュールに基づいたデータセットの更新 *5」を利用することで、毎日の早朝に自動で最新のデータをSPICEにインポートし更新しています。更新のスケジュールは、データセットごとに設定することが可能です。またもし更新が失敗した場合は、メールで通知を受け取ることが出来ます。

リポジトリとスクリプトによるデータ管理

運用する中で、既存のデータセットを更新したいという要望が頻繁に発生しました。

また、都度QuickSightコンソール上でカスタムSQLを更新してしまうと、誰がどのような理由でデータセットのカスタムSQLを変更したのかが把握しづらくなるという課題がありました。

これらの問題を解決するために、カスタムSQLは下記のようにyamlファイルとして管理するようにしました。

ID: users
Name: ユーザー一覧
SqlQuery: |-
   SELECT
     name,
     age,
     ...,
   FROM users
     LEFT JOIN ...
   WHERE ....
   GROUP BY ...
Columns:
  - Name: ユーザー名
    Type: STRING
  - Name: 年齢
    Type: INTEGER
  - ...

また、update-data-setのコマンド引数に渡す--physical-table-mapを都度コマンド入力するのが大変だったので、スクリプト化することでより運用しやすくなりました。

# cmd/update_dataset_by_id
# 特定のデータセットを更新するスクリプト

#! /usr/bin/env ruby
# frozen_string_literal: true

require 'json'
require 'optparse'
require 'yaml'
require_relative '../src/aws/config'
require_relative '../src/aws/quicksight/update_data_set'

class UpdateDataSetById
  def initialize
    option = CmdOptionParser.new.execute

    @indicator_file_id = option.indicator_file_id
  end

  def execute
    puts '===== START Update Data Set ====='
    puts "=== Update Indicator ID: #{@indicator_file_id}"

    # 対象のyamlファイルの読み込み
    indicator = YAML.load(File.read("./indicators/#{@@indicator_file_id}.yml"))

    data_source_arn = "arn:aws:quicksight:ap-northeast-1:#{Aws::Config::AWS_ACCOUNT_ID}:datasource/#{@indicator_file_id}"

    Aws::Quicksight::UpdateDataSet.new.execute(indicator, data_source_arn)

    puts '===== FINISH Update Data Set ====='
  end
end

class CmdOptionParser
  Option = Struct.new(:indicator_file_id)

  def initialize
    @option = Option.new
  end

  def execute
    ::OptionParser.new do |o|
      o.on('-i', '--indicator-file-id [SQL_DEFINED_FILE_ID]', 'SQL Defined File ID (Required)') { |v| @option.indicator_file_id = v }
      o.on('-h', '--help', 'Show help.') do |_v|
        puts o
        exit
      end
      o.banner = 'Usage: update_data_set'
      o.parse!(ARGV)

      return @option if valid?

      warn 'indicator_files required.'
      puts o.help
      exit(1)
    end
  end

  private

  def valid?
    unless @option.indicator_file_id
      warn 'Argument indicator-file-id required'
      exit(1)
    end

    true
  end
end

UpdateDataSetById.new.execute
# src/aws/quicksight/update_data_set.rb
# aws cliを実行するクラス

require_relative '../../aws/config'
require_relative '../../utils'

module Aws
  module Quicksight
    class UpdateDataSet
      def initialize; end

      def execute(indicator, data_source_arn)
        File.open('tmp-update-data-set.json', 'w') do |file|
          JSON.dump(cli_input_json(indicator, data_source_arn), file)
        end

        `aws quicksight update-data-set \
         --cli-input-json file://tmp-update-data-set.json`

        `rm tmp-update-data-set.json`

        unless $?.success?
          warn '===== Command failed: aws quicksight update-data-set ====='
          exit(1)
        end
      end

      private

      def cli_input_json(indicator, data_source_arn)
        {
          "AwsAccountId": Aws::Config::AWS_ACCOUNT_ID,
          "DataSetId": "#{indicator['ID']}",
          "Name": "#{indicator['Name']}",
          "PhysicalTableMap": {
            "AthenaPhysicalTable": {
              "CustomSql": {
                "DataSourceArn": data_source_arn,
                "Name": 'CustomSQL',
                "SqlQuery": indicator['SqlQuery'],
                "Columns": indicator['Columns']
              }
            }
          },
          "ImportMode": 'SPICE'
        }
      end
    end
  end
end

参考

まとめ

以上が、QuickSightを用いたマルチテナント構成とその運用方法を効率化した内容となります。 BIツールとしてQuickSightの利用を検討している方、現在利用していて運用を効率化したいと考えている方にとって、少しでも参考になれば幸いです。 エンジニアとして、やりがいのある仕事がしたい!わくわくする仕事をしたい!という方がいらっしゃいましたらぜひ、弊社採用窓口までご連絡ください。

TUNAGの開発体制、使用技術については下記ブログ記事を参照ください。 TUNAGの技術と開発体制のすべてを紹介します!

また株式会社スタメンでは、オンラインサロン用プラットホームFANTSというサービスも開発・運営しております。 ご興味がある方はぜひ下記のリンクをご覧ください。 FANTSの開発技術・開発組織を紹介します!