背景・目的
前、Redshift ServerlessをCDKで構築し、テストデータのロードまで試しました。
今回は、そのRedshiftをデータソースとしてQuickSightに接続し、SPICEへのインポートからビジュアル作成・データ更新の反映までを一通り試します。
まとめ
下記に特徴をまとめます
| 特徴 | 説明 |
|---|---|
| 正式名称 | SPICE (Super-fast, Parallel, In-memory Calculation Engine) |
| 役割 | QuickSightのインメモリエンジン。データソースからインポートしたデータを保持し、分析クエリを高速処理 |
| モード選択 | データセット作成時に「SPICE」か「直接クエリ」を選択 |
| メリット | クエリ高速化、データソースへの都度アクセス不要、インポート済みデータは追加コストなしで再利用可能 |
| 暗号化 | Enterprise Editionでは保管時暗号化に対応 |
| 容量 | リージョンごとに割り当て。同一アカウント・リージョン内の全ユーザーで共有。管理者が追加購入・解放可能 |
| データ更新 | 手動更新、スケジュール更新、増分更新に対応 |
| サイズ計算 | 論理サイズで算出。数値・日付: 12B/セル、文字列: 24B + UTF-8長/セル |
概要
SPICEへのデータのインポート
下記を基に整理します
-
SPICE (Super-fast, Parallel, In-memory Calculation Engine) は、QuickSightのインメモリエンジン
-
データソースからデータをインポート(取り込み)して保持し、分析クエリを高速に処理します。直接クエリと異なり、データソースへの都度アクセスが不要なため、速度とコストの両面でメリットがある
-
Enterprise Editionでは保管時暗号化にも対応
-
データの更新は手動またはスケジュールで行う
-
データセット作成時に「SPICE」か「直接クエリ」を選択できる。SPICEを選ぶメリットは下記の3つ
- 分析クエリの処理が高速
- 直接クエリの待ち時間が不要
- インポート済みデータは追加コストなしで何度でも再利用可能(直接クエリはクエリごとに課金)
-
SPICE容量はリージョンごとに割り当てられ、同一アカウント・リージョン内の全ユーザーで共有される
-
デフォルトではホームリージョンにのみ容量が割り当てられ、他リージョンは別途購入が必要
-
管理者は容量の確認・追加購入・解放が可能
SPICE データセットのサイズの推定
- SPICEのデータセットサイズは「論理サイズ」で計算され、ソーステーブルのサイズとは異なる
- データ型変換・計算列の定義後にマテリアライズされた結果が対象
- サイズの計算式は以下の通り
- 数値・日付: 12バイト/セル
- 文字列: 24バイト + UTF-8テキスト長/セル(インデックス作成のため数値より大きい)
SPICE専用機能
下記を基に整理します
Amazon Quick Sight の SPICE (Super-fast, Parallel, In-memory Calculation Engine) では、計算負荷の高い特定のデータ準備機能を使用できます。これらの変換は、クエリ時に実行されるのではなく、最適なパフォーマンスのために SPICE でマテリアライズされます。
一部の加工機能(Append, Aggregate, Pivot, Unpivot, Divergence)も持っています。
実践
Redshift環境の整備
- 下記のSQLを作成します
-- スキーマ作成
CREATE SCHEMA IF NOT EXISTS dm;
-- 月別カテゴリ売上サマリ
CREATE TABLE IF NOT EXISTS dm.monthly_sales_summary (
sales_month INTEGER NOT NULL,
category VARCHAR(50) NOT NULL,
region VARCHAR(50) NOT NULL,
quantity INTEGER NOT NULL,
revenue NUMERIC(12,2) NOT NULL
)
DISTSTYLE AUTO SORTKEY AUTO;
-- サンプルデータ
INSERT INTO dm.monthly_sales_summary VALUES
(202601, 'Electronics', 'Tokyo', 120, 1800000.00),
(202601, 'Electronics', 'Osaka', 85, 1275000.00),
(202601, 'Clothing', 'Tokyo', 200, 600000.00),
(202601, 'Clothing', 'Osaka', 150, 450000.00),
(202601, 'Food', 'Tokyo', 500, 250000.00),
(202601, 'Food', 'Osaka', 420, 210000.00),
(202602, 'Electronics', 'Tokyo', 130, 1950000.00),
(202602, 'Electronics', 'Osaka', 90, 1350000.00),
(202602, 'Clothing', 'Tokyo', 180, 540000.00),
(202602, 'Clothing', 'Osaka', 160, 480000.00),
(202602, 'Food', 'Tokyo', 480, 240000.00),
(202602, 'Food', 'Osaka', 450, 225000.00),
(202603, 'Electronics', 'Tokyo', 140, 2100000.00),
(202603, 'Electronics', 'Osaka', 95, 1425000.00),
(202603, 'Clothing', 'Tokyo', 250, 750000.00),
(202603, 'Clothing', 'Osaka', 170, 510000.00),
(202603, 'Food', 'Tokyo', 520, 260000.00),
(202603, 'Food', 'Osaka', 400, 200000.00);
テーブルを作成
1.実行前のクエリを確認します。DMスキーマは0件です
aws redshift-data execute-statement \
--workgroup-name <workgroup-name> \
--database <database-name> \
--secret-arn <secret-arn> \
--sql "SELECT schemaname, tablename FROM pg_tables WHERE schemaname = 'dm' ORDER BY tablename;" \
--region ap-northeast-1
TotalNumRows: 0
2.テーブルを作成し、データを登録します
aws redshift-data execute-statement \
--workgroup-name <workgroup-name> \
--database <database-name> \
--secret-arn <secret-arn> \
--sql "$(cat 01_setup.sql)" \
--region ap-northeast-1
Status: FINISHED
3.テーブル作成後の確認をします
schemaname | tablename
dm | monthly_sales_summary
TotalNumRows: 1
4.データの確認をします
sales_month | category | region | quantity | revenue
202601 | Clothing | Osaka | 150 | 450000.00
202601 | Clothing | Tokyo | 200 | 600000.00
202601 | Electronics | Osaka | 85 | 1275000.00
202601 | Electronics | Tokyo | 120 | 1800000.00
202601 | Food | Osaka | 420 | 210000.00
202601 | Food | Tokyo | 500 | 250000.00
202602 | Clothing | Osaka | 160 | 480000.00
202602 | Clothing | Tokyo | 180 | 540000.00
202602 | Electronics | Osaka | 90 | 1350000.00
202602 | Electronics | Tokyo | 130 | 1950000.00
202602 | Food | Osaka | 450 | 225000.00
202602 | Food | Tokyo | 480 | 240000.00
202603 | Clothing | Osaka | 170 | 510000.00
202603 | Clothing | Tokyo | 250 | 750000.00
202603 | Electronics | Osaka | 95 | 1425000.00
202603 | Electronics | Tokyo | 140 | 2100000.00
202603 | Food | Osaka | 400 | 200000.00
202603 | Food | Tokyo | 520 | 260000.00
TotalNumRows: 18
Quick環境の整備
サインアップ
1.Quickに遷移します
2.「Amazon Quickにサインアップ」をクリックします

3.下記の内容を入力し、「アカウントを作成」をクリックします
- アカウント名:任意
- アカウント通知用メールアドレス:自分のアドレス
- リージョン:任意
- 認証方法:パスワードベースまたはシングルサインオン
- 暗号化:AWS管理のKMS

4.「Amazon Quickに移動」をクリックします

5.ホーム画面が表示されました
VPC接続環境を構築
初期化
1.cdk initで初期化します
npx cdk init app --language typescript
実装とテスト
1.VPCConnectionのオブジェクトを作ります
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as quicksight from 'aws-cdk-lib/aws-quicksight';
import { NagSuppressions } from 'cdk-nag';
import { Construct } from 'constructs';
export class QsVpcConnectionStack extends cdk.Stack {
constructor(scope: Construct, id: string, props?: cdk.StackProps) {
super(scope, id, props);
const vpcId = this.node.getContext('vpcId');
const redshiftSgId = this.node.getContext('redshiftSgId');
// 既存VPC・SGをインポート
const vpc = ec2.Vpc.fromLookup(this, 'Vpc', { vpcId });
const redshiftSg = ec2.SecurityGroup.fromSecurityGroupId(this, 'RedshiftSg', redshiftSgId);
// QS VPC接続用SG
const qsSg = new ec2.SecurityGroup(this, 'QsSg', {
vpc,
securityGroupName: 'qs-vpc-connection-sg',
description: 'SG for QuickSight VPC Connection to Redshift',
});
// Redshift SGにQS SGからのインバウンドを追加
redshiftSg.addIngressRule(qsSg, ec2.Port.tcp(5439), 'From QuickSight VPC Connection');
// QSサービスロール(service-role/パス付きARN)
const qsServiceRoleArn = `arn:aws:iam::${this.account}:role/service-role/aws-quicksight-service-role-v0`;
const qsServiceRole = iam.Role.fromRoleArn(this, 'QsServiceRole', qsServiceRoleArn);
qsServiceRole.addToPrincipalPolicy(new iam.PolicyStatement({
actions: [
'ec2:CreateNetworkInterface',
'ec2:ModifyNetworkInterfaceAttribute',
'ec2:DeleteNetworkInterface',
'ec2:DescribeSubnets',
'ec2:DescribeSecurityGroups',
],
resources: ['*'],
}));
// QuickSight VPC接続
const vpcConnection = new quicksight.CfnVPCConnection(this, 'QsVpcConnection', {
awsAccountId: this.account,
vpcConnectionId: 'qs-redshift-vpc-conn',
name: 'QS to Redshift VPC Connection',
subnetIds: vpc.isolatedSubnets.map(s => s.subnetId),
securityGroupIds: [qsSg.securityGroupId],
roleArn: qsServiceRoleArn,
});
// cdk-nag suppressions
NagSuppressions.addResourceSuppressions(this, [
{
id: 'AwsSolutions-IAM5',
reason: 'EC2 ENI operations do not support resource-level permissions. Wildcard is required for QuickSight VPC Connection.',
},
], true);
}
}
2.テストを書きます
import { Match, Template } from 'aws-cdk-lib/assertions';
import * as cdk from 'aws-cdk-lib';
import { QsVpcConnectionStack } from '../lib/qs-vpc-connection-stack';
const app = new cdk.App({
context: {
vpcId: 'vpc-dummy',
redshiftSgId: 'sg-dummy',
},
});
const stack = new QsVpcConnectionStack(app, 'TestStack', {
env: { account: '123456789012', region: 'ap-northeast-1' },
});
const template = Template.fromStack(stack);
test('QS用SGが作成される', () => {
template.hasResourceProperties('AWS::EC2::SecurityGroup', {
GroupName: 'qs-vpc-connection-sg',
});
});
test('Redshift SGにQS SGからの5439インバウンドが追加される', () => {
template.hasResourceProperties('AWS::EC2::SecurityGroupIngress', {
GroupId: 'sg-dummy',
IpProtocol: 'tcp',
FromPort: 5439,
ToPort: 5439,
});
});
test('QSサービスロールにENI権限が追加される', () => {
template.hasResourceProperties('AWS::IAM::Policy', {
PolicyDocument: Match.objectLike({
Statement: Match.arrayWith([
Match.objectLike({
Action: Match.arrayWith(['ec2:CreateNetworkInterface']),
Effect: 'Allow',
}),
]),
}),
});
});
test('QuickSight VPC接続がservice-role付きARNで作成される', () => {
template.hasResourceProperties('AWS::QuickSight::VPCConnection', {
VPCConnectionId: 'qs-redshift-vpc-conn',
RoleArn: 'arn:aws:iam::123456789012:role/service-role/aws-quicksight-service-role-v0',
});
});
3.テストを実行します
npx jest
・・・
Test Suites: 1 passed, 1 total
Tests: 4 passed, 4 total
Snapshots: 0 total
Time: 6.991 s, estimated 7 s
Ran all test suites.
デプロイ
1.synthを実行し、テンプレートを生成します
npx cdk synth \
-c vpcId=vpc-XXXXXXXXXX \
-c redshiftSgId=sg-XXXXXXXXXX \
2.deployします
CDK_DEFAULT_ACCOUNT=XXXXX CDK_DEFAULT_REGION=ap-northeast-1 \
npx cdk deploy \
-c vpcId=vpc-XXXXX \
-c redshiftSgId=sg-XXXXX \
--require-approval broadening
リソースの確認
- 上記で作成した、VPC接続の管理をみます
- Quickでプロファイルを開き自分の名前をクリックします
- 左ナビゲーションペインで、「VPC接続を管理」をクリックします
- VPC接続名などが確認できます
データソースの作成
セキュリティとアクセス許可
CLIで作成
aws quicksight create-data-source \
--aws-account-id XXXXXXXX \
--data-source-id redshift-dev \
--name "Redshift Dev" \
--type REDSHIFT \
--data-source-parameters '{"RedshiftParameters":{"Database":"<database-name>","Host":"<workgroup-endpoint>","Port":5439}}' \
--credentials '{"SecretArn":"<secret-arn>"}' \
--vpc-connection-properties '{"VpcConnectionArn":"arn:aws:quicksight:<region>:<account-id>:vpcConnection/qs-redshift-vpc-conn"}' \
--region ap-northeast-1 \
--output json
データソースを確認
データセット作成 + SPICEインポート
データセットの作成
1.下記のコマンドでデータセットを作成します
aws quicksight create-data-set \
--aws-account-id XXXXXXX \
--data-set-id monthly-sales-summary \
--name "Monthly Sales Summary" \
--import-mode SPICE \
--physical-table-map '{
"salesTable": {
"RelationalTable": {
"DataSourceArn": "arn:aws:quicksight:ap-northeast-1:XXXXXXX:datasource/redshift-dev",
"Schema": "dm",
"Name": "monthly_sales_summary",
"InputColumns": [
{"Name": "sales_month", "Type": "INTEGER"},
{"Name": "category", "Type": "STRING"},
{"Name": "region", "Type": "STRING"},
{"Name": "quantity", "Type": "INTEGER"},
{"Name": "revenue", "Type": "DECIMAL"}
]
}
}
}' \
--permissions '[{
"Principal": "arn:aws:quicksight:ap-northeast-1:XXXXXXX:user/default/admin/XXXXXXX",
"Actions": [
"quicksight:DescribeDataSet",
"quicksight:DescribeDataSetPermissions",
"quicksight:PassDataSet",
"quicksight:DescribeIngestion",
"quicksight:ListIngestions",
"quicksight:UpdateDataSet",
"quicksight:DeleteDataSet",
"quicksight:CreateIngestion",
"quicksight:CancelIngestion",
"quicksight:UpdateDataSetPermissions"
]
}]' \
--region ap-northeast-1 \
--output json
{
"Status": 201,
"Arn": "arn:aws:quicksight:ap-northeast-1:XXXXX:dataset/monthly-sales-summary",
"DataSetId": "monthly-sales-summary",
"IngestionArn": "arn:aws:quicksight:ap-northeast-1:XXXXX:dataset/monthly-sales-summary/ingestion/XXXXX",
"IngestionId": "XXXXX",
"RequestId": "XXXXX"
}
2.SPICEへのインジェスションも開始されています。確認します。18行が取り込まれました
aws quicksight describe-ingestion \
--aws-account-id XXXXXX \
--data-set-id monthly-sales-summary \
--ingestion-id XXXXXX \
--region ap-northeast-1 \
--query 'Ingestion.{Status:IngestionStatus,RowsIngested:RowInfo.RowsIngested}' \
--output json 2>&1
{
"Status": "COMPLETED",
"RowsIngested": 18
}
確認
ビジュアル作成
-
下記を選択します。できました
- Y軸:category
- 値:revenue(合計)
- グループ/色:sales_month
Redshiftのデータを更新し、分析画面までの反映
データマートを更新し、Quickの画面が変化するまでを確認します
データマートの更新
1.データマートにレコードを追加します。202604のElectronicsのデータが追加されました
# 202604(4月)のデータを追加。既存は202601-03なので明確に区別できる
aws redshift-data execute-statement \
--workgroup-name <workgroup-name> \
--database dev-redshift-db \
--secret-arn "arn:aws:secretsmanager:ap-northeast-1:XXXXXX:secret:redshift!XXXXXX" \
--sql "INSERT INTO dm.monthly_sales_summary VALUES (202604, 'Electronics', 'Tokyo', 999, 9999999.00), (202604, 'Electronics', 'Osaka', 888, 8888888.00);" \
--region ap-northeast-1 \
--query 'Id' --output text
2.確認します
aws redshift-data describe-statement --id XXXXXX --region ap-northeast-1 --query 'Status' --output text
SPICEを更新
1.SPICEを更新します
aws quicksight create-ingestion \
--aws-account-id XXXXXX \
--data-set-id monthly-sales-summary \
--ingestion-id refresh-$(date +%Y%m%d%H%M%S) \
--region ap-northeast-1 \
--output json
2.確認します。20行になりました
aws quicksight describe-ingestion \
--aws-account-id XXXXXX \
--data-set-id monthly-sales-summary \
--ingestion-id refresh-XXXXXX \
--region ap-northeast-1 \
--query 'Ingestion.{Status:IngestionStatus,RowsIngested:RowInfo.RowsIngested}' \
--output json
{
"Status": "COMPLETED",
"RowsIngested": 20
}
分析を確認
考察
今回、Redshiftにデータマートを作成し、QuickのSPICEへロードし、画面に反映するまでを試しました。
SPICEの更新タイミングについては、SPICEはスナップショット方式のため、Redshiftのデータを更新してもQSの表示は自動では変わらない。本番運用では日次バッチ後にスケジュール更新を設定するか、Step Functionsから create-ingestion APIを呼んでSPICE更新をトリガーする設計が必要になります。
aws quicksight create-ingestion \
--data-set-id monthly-sales-summary \
--ingestion-id refresh-$(date +%Y%m%d%H%M%S) \
今後も継続して試してみます。
参考









