Skip to content

Instantly share code, notes, and snippets.

@YasharHND
Last active February 18, 2025 15:33
Show Gist options
  • Save YasharHND/927ad30c240299ade0b2c162159d432e to your computer and use it in GitHub Desktop.
Save YasharHND/927ad30c240299ade0b2c162159d432e to your computer and use it in GitHub Desktop.
CDK QuickSight Construct With Permissions (for QS Group) & CfnAnalysis
import { NestedStack, type NestedStackProps } from 'aws-cdk-lib';
import {
CfnFolder,
CfnDataSet,
CfnDataSource,
CfnTemplate,
CfnDashboard,
CfnAnalysis,
} from 'aws-cdk-lib/aws-quicksight';
import type { Construct } from 'constructs';
import { readFileSync } from 'fs';
import { join } from 'path';
interface QuickSightStackProps extends NestedStackProps {
environment: string;
}
export class QuickSightStack extends NestedStack {
constructor(scope: Construct, id: string, props: QuickSightStackProps) {
super(scope, id, props);
const requiredEnvVars = [
'QS_DASHBOARD_VIEWERS_GROUP_ARN',
'QS_DATASOURCE_VPC_CONNECTION_ARN',
'DB_HOST',
'DB_SCHEMA',
'DB_USERNAME_READONLY',
'DB_PASSWORD_READONLY',
];
const missingEnvVars = requiredEnvVars.filter((envVar) => !process.env[envVar]);
if (missingEnvVars.length > 0) {
throw new Error(`Missing required environment variables: ${missingEnvVars.join(', ')}`);
}
this.createQuickSightDashboards(props.environment, process.env.QS_DASHBOARD_VIEWERS_GROUP_ARN!);
}
private createQuickSightDashboards(env: string, viewersGroupArn: string): void {
const mainFolder = this.createQuickSightFolder(env, undefined, undefined, viewersGroupArn);
const datasetsFolder = this.createQuickSightFolder(env, 'datasets', mainFolder.attrArn, viewersGroupArn);
const dashboardsFolder = this.createQuickSightFolder(env, 'dashboards', mainFolder.attrArn, viewersGroupArn);
const dataSource = this.createDataSource(env, viewersGroupArn);
this.createAntiPatternDashboard(
env,
datasetsFolder.attrArn,
dashboardsFolder.attrArn,
dataSource.attrArn,
viewersGroupArn,
);
}
private createDataSource(env: string, viewersGroupArn: string): CfnDataSource {
const dataSourceId = `timeback-platform-${env}`;
return new CfnDataSource(this, `QuickSightDataSource-${env}`, {
awsAccountId: this.account,
dataSourceId,
name: dataSourceId,
type: 'POSTGRESQL',
dataSourceParameters: {
postgreSqlParameters: {
host: process.env.DB_HOST!,
database: process.env.DB_SCHEMA!,
port: 5432,
},
},
credentials: {
credentialPair: {
username: process.env.DB_USERNAME_READONLY!,
password: process.env.DB_PASSWORD_READONLY!,
},
},
vpcConnectionProperties: {
vpcConnectionArn: process.env.QS_DATASOURCE_VPC_CONNECTION_ARN!,
},
permissions: [
{
principal: viewersGroupArn,
actions: [
'quicksight:DescribeDataSource',
'quicksight:DescribeDataSourcePermissions',
'quicksight:PassDataSource',
'quicksight:UpdateDataSource',
'quicksight:DeleteDataSource',
'quicksight:UpdateDataSourcePermissions',
],
},
],
});
}
// TODO :: move this & the underlying methods to separate files per each actual dashboard
private createAntiPatternDashboard(
env: string,
datasetsFolderArn: string,
dashboardsFolderArn: string,
dataSourceArn: string,
viewersGroupArn: string,
): void {
const dataset = this.createAntiPatternDataset(env, datasetsFolderArn, viewersGroupArn, dataSourceArn);
const template = this.createAntiPatternTemplate(env, viewersGroupArn);
const dashboardName = `timeback-platform-${env}-anti-pattern-dashboard`;
new CfnDashboard(this, `QuickSightDashboard-${env}`, {
awsAccountId: this.account,
dashboardId: dashboardName,
name: dashboardName,
folderArns: [dashboardsFolderArn],
sourceEntity: {
sourceTemplate: {
arn: template.attrArn,
dataSetReferences: [
{
dataSetArn: dataset.attrArn,
dataSetPlaceholder: 'anti_pattern_dataset',
},
],
},
},
permissions: [
{
principal: viewersGroupArn,
actions: ['quicksight:DescribeDashboard', 'quicksight:ListDashboardVersions', 'quicksight:QueryDashboard'],
},
],
tags: [
{
key: 'Version',
value: `1-${new Date().toISOString()}`,
},
],
});
}
private createAntiPatternDataset(
env: string,
folderArn: string,
viewersGroupArn: string,
dataSourceArn: string,
): CfnDataSet {
const datasetName = `timeback-platform-${env}-anti-pattern`;
// CAUTION ::
// 1. Do NOT put semicolon at the end of the file otherwise the query in the dataset will break
// 2. Do NOT have extra lines at the end of the file (we're trimming the content but still)
const sqlQuery = readFileSync(join(__dirname, 'queries', 'anti-pattern.sql'), 'utf-8').trim();
return new CfnDataSet(this, `QuickSightDataSet-${env}`, {
awsAccountId: this.account,
dataSetId: datasetName,
name: datasetName,
importMode: 'DIRECT_QUERY',
folderArns: [folderArn],
physicalTableMap: {
antiPatternTable: {
customSql: {
dataSourceArn,
name: 'anti_pattern_query',
sqlQuery,
columns: [
{ name: 'anti_pattern_id', type: 'STRING' },
{ name: 'anti_pattern_type', type: 'STRING' },
{ name: 'started_at_time', type: 'DATETIME' },
{ name: 'ended_at_time', type: 'DATETIME' },
{ name: 'duration_seconds', type: 'DECIMAL' },
{ name: 'person_name', type: 'STRING' },
{ name: 'course_name', type: 'STRING' },
{ name: 'subject_name', type: 'STRING' },
{ name: 'software_application_name', type: 'STRING' },
],
},
},
},
permissions: [
{
principal: viewersGroupArn,
actions: [
'quicksight:DescribeDataSet',
'quicksight:DescribeDataSetPermissions',
'quicksight:DescribeDataSetRefreshProperties',
'quicksight:PassDataSet',
'quicksight:DescribeIngestion',
'quicksight:ListIngestions',
],
},
],
});
}
private createAntiPatternTemplate(env: string, viewersGroupArn: string): CfnTemplate {
return new CfnTemplate(this, `QuickSightTemplate-${env}`, {
awsAccountId: this.account,
templateId: `timeback-platform-${env}-anti-pattern-template`,
name: `timeback-platform-${env}-anti-pattern-template`,
// Need to use tag to enforce the update on the stack
tags: [
{
key: 'Version',
value: `1-${new Date().toISOString()}`,
},
],
definition: {
dataSetConfigurations: [
{
placeholder: 'anti_pattern_dataset',
dataSetSchema: {
columnSchemaList: [
{ name: 'anti_pattern_id', dataType: 'STRING' },
{ name: 'anti_pattern_type', dataType: 'STRING' },
{ name: 'started_at_time', dataType: 'DATETIME' },
{ name: 'ended_at_time', dataType: 'DATETIME' },
{ name: 'duration_seconds', dataType: 'DECIMAL' },
{ name: 'person_name', dataType: 'STRING' },
{ name: 'course_name', dataType: 'STRING' },
{ name: 'subject_name', dataType: 'STRING' },
{ name: 'software_application_name', dataType: 'STRING' },
],
},
},
],
sheets: [
{
sheetId: 'sheet1',
name: 'Anti-Pattern Overview',
visuals: [
{
tableVisual: {
visualId: 'table1',
title: {
formatText: {
plainText: 'Anti-Pattern Data',
},
},
chartConfiguration: {
fieldWells: {
tableAggregatedFieldWells: {
groupBy: [
{
categoricalDimensionField: {
fieldId: 'person_name',
column: {
dataSetIdentifier: 'anti_pattern_dataset',
columnName: 'person_name',
},
},
},
{
categoricalDimensionField: {
fieldId: 'subject_name',
column: {
dataSetIdentifier: 'anti_pattern_dataset',
columnName: 'subject_name',
},
},
},
{
categoricalDimensionField: {
fieldId: 'course_name',
column: {
dataSetIdentifier: 'anti_pattern_dataset',
columnName: 'course_name',
},
},
},
{
categoricalDimensionField: {
fieldId: 'anti_pattern_type',
column: {
dataSetIdentifier: 'anti_pattern_dataset',
columnName: 'anti_pattern_type',
},
},
},
],
values: [
{
numericalMeasureField: {
fieldId: 'duration_seconds',
column: {
dataSetIdentifier: 'anti_pattern_dataset',
columnName: 'duration_seconds',
},
aggregationFunction: {
simpleNumericalAggregation: 'SUM',
},
},
},
],
},
},
},
},
},
],
},
],
},
permissions: [
{
principal: viewersGroupArn,
actions: ['quicksight:DescribeTemplate'],
},
],
});
}
// Keeping the code in case we need Analysis int the future
private createAntiPatternAnalysis(
env: string,
dataSetArn: string,
templateArn: string,
viewersGroupArn: string,
): CfnAnalysis {
const analysisName = `timeback-platform-${env}-anti-pattern-analysis`;
return new CfnAnalysis(this, `QuickSightAnalysis-${env}`, {
awsAccountId: this.account,
analysisId: analysisName,
name: analysisName,
sourceEntity: {
sourceTemplate: {
arn: templateArn,
dataSetReferences: [
{
dataSetArn,
dataSetPlaceholder: 'anti_pattern_dataset',
},
],
},
},
permissions: [
{
principal: viewersGroupArn,
actions: [
'quicksight:DescribeAnalysis',
'quicksight:DescribeAnalysisPermissions',
'quicksight:QueryAnalysis',
'quicksight:RestoreAnalysis',
'quicksight:UpdateAnalysis',
'quicksight:UpdateAnalysisPermissions',
'quicksight:DeleteAnalysis',
],
},
],
});
}
private createQuickSightFolder(
env: string,
subPath?: string,
parentFolderArn?: string,
viewersGroupArn?: string,
): CfnFolder {
const baseName = `timeback-platform-${env}`;
const folderName = subPath ? `${baseName}-${subPath}` : baseName;
const folderId = folderName.toLowerCase().replace(/[^a-z0-9-]/g, '-');
return new CfnFolder(this, `QuickSightFolder-${folderName}`, {
awsAccountId: this.account,
folderId,
name: folderName,
folderType: 'SHARED',
parentFolderArn,
permissions: [
{
principal: viewersGroupArn!,
actions: ['quicksight:DescribeFolder'],
},
],
});
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment