Quick take — A reusable hashicorp/aws ~> 5.0 Terraform module for Amazon QuickSight: Enterprise account subscription, a QUICKSIGHT-backed namespace, RBAC groups, a shared folder, and an Athena/S3 data source with scoped permissions — production defaults baked in. New here? Jump to the Quickstart below to deploy it in minutes; read on for how it works and when to reach for it.
Quickstart (copy-paste)
Minimal, runnable configuration — drop this in a .tf file and fill in the "..." placeholders (each required input is commented):
provider "aws" {
region = "us-east-1"
}
module "quicksight" {
source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-quicksight?ref=v1.0.0"
account_name = "..." # Globally-unique QuickSight account name (shown at sign-in)…
notification_email = "..." # Email QuickSight uses for account/subscription notifications.
}
Then terraform init && terraform apply. Every other input has a sensible default — see Inputs below to override behaviour.
Heads-up:
aws_quicksight_account_subscriptionis account-wide and effectively one-time — there is one QuickSight subscription per AWS account. Treat the account-subscription part of this module as a foundational, rarely-changed stack, and read Best practices below before runningdestroy.
What this module is
Amazon QuickSight is AWS’s managed, serverless business-intelligence service: dashboards, paginated reports, and ML-powered insights over data in Athena, S3, Redshift, RDS, and more. Unlike most AWS services, QuickSight is account-scoped — you subscribe the account to an edition once, and everything else (namespaces, users, groups, folders, data sources, datasets, analyses, dashboards) lives under that single subscription.
That account-scoped model is exactly where the footguns hide. aws_quicksight_account_subscription is not idempotent the way a normal resource is: there is one subscription per account, the DescribeAccountSettings API does not return every argument, and importing or re-creating it can trigger a planned replacement — which, on a destroy, can tear down BI for the entire account. The remaining building blocks are individually simple but easy to get inconsistent: namespaces default to the QUICKSIGHT identity store but are silent about it, groups land in the wrong namespace, folders default to SHARED but are created without permissions, and data sources are routinely shipped with no permission block at all — leaving them invisible to the very analysts who need them.
This module wires the whole foundation together with safe defaults: an ENTERPRISE edition subscription with IAM_AND_QUICKSIGHT authentication, a QUICKSIGHT-backed namespace, a set of RBAC groups, a SHARED folder with explicit permissions, and an Athena (or S3) data source whose permission blocks grant exactly the right groups read/use access. The account-subscription resource is guarded with lifecycle settings and clear documentation so a stray terraform destroy does not nuke your analytics estate.
When to use it
- You are bootstrapping QuickSight in a fresh AWS account and want the Enterprise subscription, namespace, RBAC groups, and a governed shared folder created as one reviewed, versioned stack.
- You manage BI access through groups, not individual users, and want consistent
aws_quicksight_groupdefinitions plus aSHAREDfolder that the right groups can read and contribute to. - You connect QuickSight to a data lake via Athena (or directly to S3 manifests) and want each data source to ship with scoped
permissionblocks so the correct groups can use it on day one. - You want the inherently dangerous account-subscription resource owned by a small, audited platform stack rather than copy-pasted into application repos where a
destroycould remove BI for everyone.
Reach for a thinner setup — just aws_quicksight_user/aws_quicksight_group — when the account is already subscribed and you only need to manage access. Use this full module when you own the QuickSight foundation end to end. For embedded analytics in a customer-facing app, layer namespaces and capacity on top of what this module establishes.
Module structure
terraform-module-aws-quicksight/
├── versions.tf # provider + Terraform version pins
├── main.tf # subscription, namespace, groups, folder, data source
├── variables.tf # var-driven inputs with validations
└── outputs.tf # subscription status, namespace arn, group/folder/data source ids
versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
main.tf
data "aws_caller_identity" "current" {}
data "aws_partition" "current" {}
data "aws_region" "current" {}
locals {
account_id = data.aws_caller_identity.current.account_id
tags = merge(
{
"ManagedBy" = "terraform"
"Module" = "terraform-module-aws-quicksight"
},
var.tags,
)
# ARN of a group, used to grant folder/data-source permissions.
group_arn = { for g in var.groups : g => "arn:${data.aws_partition.current.partition}:quicksight:${data.aws_region.current.name}:${local.account_id}:group/${var.namespace}/${g}" }
}
# Account-wide, effectively one-time subscription. See best practices before
# destroying — replacement/deletion affects QuickSight for the whole account.
resource "aws_quicksight_account_subscription" "this" {
count = var.create_account_subscription ? 1 : 0
account_name = var.account_name
authentication_method = var.authentication_method
edition = var.edition
notification_email = var.notification_email
# The Describe API does not return every argument, so suppress spurious diffs
# that would otherwise plan a destructive replacement on the next apply.
lifecycle {
ignore_changes = [
authentication_method,
]
}
}
# A QUICKSIGHT-backed namespace isolates users/groups/assets for a tenant or
# environment. The default "default" namespace always exists; this adds another.
resource "aws_quicksight_namespace" "this" {
count = var.create_namespace ? 1 : 0
namespace = var.namespace
identity_store = var.identity_store
tags = local.tags
depends_on = [aws_quicksight_account_subscription.this]
}
# RBAC groups — manage BI access by group, never per-user.
resource "aws_quicksight_group" "this" {
for_each = toset(var.groups)
group_name = each.value
namespace = var.namespace
description = "Managed by terraform (${each.value})"
depends_on = [aws_quicksight_namespace.this]
}
# A SHARED folder organizes dashboards/analyses and carries group permissions.
resource "aws_quicksight_folder" "this" {
count = var.create_folder ? 1 : 0
folder_id = var.folder_id
name = var.folder_name
folder_type = "SHARED"
dynamic "permissions" {
for_each = var.folder_reader_groups
content {
principal = local.group_arn[permissions.value]
actions = [
"quicksight:DescribeFolder",
"quicksight:CreateFolderMembership",
"quicksight:DeleteFolderMembership",
"quicksight:DescribeFolderPermissions",
]
}
}
tags = local.tags
depends_on = [aws_quicksight_group.this]
}
# An Athena (or S3) data source feeding analyses/datasets, with scoped perms.
resource "aws_quicksight_data_source" "this" {
count = var.create_data_source ? 1 : 0
data_source_id = var.data_source_id
name = var.data_source_name
type = var.data_source_type
parameters {
dynamic "athena" {
for_each = var.data_source_type == "ATHENA" ? [1] : []
content {
work_group = var.athena_work_group
}
}
dynamic "s3" {
for_each = var.data_source_type == "S3" ? [1] : []
content {
manifest_file_location {
bucket = var.s3_manifest_bucket
key = var.s3_manifest_key
}
}
}
}
dynamic "permission" {
for_each = var.data_source_user_groups
content {
principal = local.group_arn[permission.value]
actions = [
"quicksight:DescribeDataSource",
"quicksight:DescribeDataSourcePermissions",
"quicksight:PassDataSource",
]
}
}
tags = local.tags
depends_on = [aws_quicksight_group.this]
}
variables.tf
variable "create_account_subscription" {
description = "Whether to create the account-wide QuickSight subscription. Keep false if the account is already subscribed."
type = bool
default = true
}
variable "account_name" {
description = "Globally-unique QuickSight account name (shown when users sign in)."
type = string
validation {
condition = can(regex("^[A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9]$", var.account_name))
error_message = "account_name must be 2-63 chars: alphanumeric and hyphens, not starting/ending with a hyphen."
}
}
variable "authentication_method" {
description = "Authentication method: IAM_AND_QUICKSIGHT, IAM_ONLY, IAM_IDENTITY_CENTER, or ACTIVE_DIRECTORY."
type = string
default = "IAM_AND_QUICKSIGHT"
validation {
condition = contains(["IAM_AND_QUICKSIGHT", "IAM_ONLY", "IAM_IDENTITY_CENTER", "ACTIVE_DIRECTORY"], var.authentication_method)
error_message = "authentication_method must be IAM_AND_QUICKSIGHT, IAM_ONLY, IAM_IDENTITY_CENTER, or ACTIVE_DIRECTORY."
}
}
variable "edition" {
description = "QuickSight edition: STANDARD, ENTERPRISE, or ENTERPRISE_AND_Q."
type = string
default = "ENTERPRISE"
validation {
condition = contains(["STANDARD", "ENTERPRISE", "ENTERPRISE_AND_Q"], var.edition)
error_message = "edition must be STANDARD, ENTERPRISE, or ENTERPRISE_AND_Q."
}
}
variable "notification_email" {
description = "Email QuickSight uses for account/subscription notifications."
type = string
validation {
condition = can(regex("^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$", var.notification_email))
error_message = "notification_email must be a valid email address."
}
}
variable "create_namespace" {
description = "Whether to create a non-default namespace."
type = bool
default = false
}
variable "namespace" {
description = "Namespace for groups/folders/data sources. Use 'default' unless isolating a tenant/environment."
type = string
default = "default"
}
variable "identity_store" {
description = "Namespace identity store. Currently only QUICKSIGHT is valid."
type = string
default = "QUICKSIGHT"
validation {
condition = var.identity_store == "QUICKSIGHT"
error_message = "identity_store must be QUICKSIGHT (the only supported value)."
}
}
variable "groups" {
description = "QuickSight group names to create for RBAC, e.g. ['bi-admins','bi-authors','bi-readers']."
type = list(string)
default = []
}
variable "create_folder" {
description = "Whether to create a SHARED folder."
type = bool
default = false
}
variable "folder_id" {
description = "Identifier for the SHARED folder."
type = string
default = null
}
variable "folder_name" {
description = "Display name for the SHARED folder."
type = string
default = null
}
variable "folder_reader_groups" {
description = "Group names (subset of var.groups) granted permissions on the folder."
type = list(string)
default = []
}
variable "create_data_source" {
description = "Whether to create a data source."
type = bool
default = false
}
variable "data_source_id" {
description = "Identifier for the data source."
type = string
default = null
}
variable "data_source_name" {
description = "Display name for the data source (max 128 chars)."
type = string
default = null
}
variable "data_source_type" {
description = "Data source type: ATHENA or S3."
type = string
default = "ATHENA"
validation {
condition = contains(["ATHENA", "S3"], var.data_source_type)
error_message = "data_source_type must be ATHENA or S3 (extend the module for other engines)."
}
}
variable "athena_work_group" {
description = "Athena workgroup to connect to (used when data_source_type = ATHENA)."
type = string
default = "primary"
}
variable "s3_manifest_bucket" {
description = "S3 bucket holding the manifest file (used when data_source_type = S3)."
type = string
default = null
}
variable "s3_manifest_key" {
description = "S3 key of the manifest file (used when data_source_type = S3)."
type = string
default = null
}
variable "data_source_user_groups" {
description = "Group names (subset of var.groups) granted use/describe on the data source."
type = list(string)
default = []
}
variable "tags" {
description = "Tags merged onto namespace, folder, and data source."
type = map(string)
default = {}
}
outputs.tf
output "account_subscription_status" {
description = "Status of the QuickSight account subscription, or null when not managed here."
value = try(aws_quicksight_account_subscription.this[0].account_subscription_status, null)
}
output "namespace_arn" {
description = "ARN of the created namespace, or null when not managed here."
value = try(aws_quicksight_namespace.this[0].arn, null)
}
output "group_arns" {
description = "Map of group name => group ARN."
value = { for k, g in aws_quicksight_group.this : k => g.arn }
}
output "folder_arn" {
description = "ARN of the SHARED folder, or null when not created."
value = try(aws_quicksight_folder.this[0].arn, null)
}
output "folder_id" {
description = "ID of the SHARED folder, or null when not created."
value = try(aws_quicksight_folder.this[0].folder_id, null)
}
output "data_source_arn" {
description = "ARN of the data source, or null when not created."
value = try(aws_quicksight_data_source.this[0].arn, null)
}
output "data_source_id" {
description = "ID of the data source, or null when not created."
value = try(aws_quicksight_data_source.this[0].data_source_id, null)
}
How to use it
module "quicksight" {
source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-quicksight?ref=v1.0.0"
# Account foundation (run once per AWS account).
create_account_subscription = true
account_name = "kloudvin-analytics"
authentication_method = "IAM_AND_QUICKSIGHT"
edition = "ENTERPRISE"
notification_email = "bi-ops@kloudvin.com"
# RBAC groups in the default namespace.
groups = ["bi-admins", "bi-authors", "bi-readers"]
# A governed shared folder readers/authors can use.
create_folder = true
folder_id = "finance-dashboards"
folder_name = "Finance Dashboards"
folder_reader_groups = ["bi-authors", "bi-readers"]
# An Athena data source over the analytics workgroup, usable by authors.
create_data_source = true
data_source_id = "lakehouse-athena"
data_source_name = "Lakehouse (Athena)"
data_source_type = "ATHENA"
athena_work_group = aws_athena_workgroup.analytics.name
data_source_user_groups = ["bi-admins", "bi-authors"]
tags = {
Environment = "prod"
Team = "data-platform"
CostCenter = "BI-1180"
}
}
# Downstream: assign an existing IAM user into the readers group via membership.
resource "aws_quicksight_group_membership" "analyst" {
group_name = "bi-readers"
member_name = "jane.doe"
depends_on = [module.quicksight]
}
Pin the module with
?ref=<tag>. Keepcreate_account_subscription = falsein every stack except the single platform-owned one that bootstraps the account — there is only one subscription per account, and managing it from multiple stacks invites a destructive replacement.
With Terragrunt
Terragrunt keeps this module DRY across environments — define the backend and provider once in a root config, then a thin terragrunt.hcl per environment supplies only the inputs that differ.
1. Root config — live/terragrunt.hcl (inherited by every module):
remote_state {
backend = "s3"
generate = { path = "backend.tf", if_exists = "overwrite" }
config = {
# ...s3 state bucket/container + key per path...
}
}
2. Module config — live/prod/quicksight/terragrunt.hcl:
include "root" {
path = find_in_parent_folders()
}
terraform {
source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-aws-quicksight?ref=v1.0.0"
}
inputs = {
account_name = "..."
notification_email = "..."
}
3. Deploy one environment, or roll out all modules together:
cd live/prod/quicksight && terragrunt apply # this module
terragrunt run-all apply # every module under live/prod
Why Terragrunt here: the backend and provider live in one place instead of being copy-pasted into every module; inputs is overridden per environment (dev / stage / prod) without forking the module; and run-all orchestrates dependencies across modules. Reach for it once you have more than one environment or more than a handful of modules — for a single stack, the plain Quickstart above is enough.
Inputs
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
account_name |
string |
— | Yes | Globally-unique QuickSight account name (shown at sign-in). |
notification_email |
string |
— | Yes | Email for account/subscription notifications. |
create_account_subscription |
bool |
true |
No | Create the account-wide subscription (false if already subscribed). |
authentication_method |
string |
IAM_AND_QUICKSIGHT |
No | IAM_AND_QUICKSIGHT, IAM_ONLY, IAM_IDENTITY_CENTER, or ACTIVE_DIRECTORY. |
edition |
string |
ENTERPRISE |
No | STANDARD, ENTERPRISE, or ENTERPRISE_AND_Q. |
create_namespace |
bool |
false |
No | Create a non-default namespace. |
namespace |
string |
default |
No | Namespace for groups/folders/data sources. |
identity_store |
string |
QUICKSIGHT |
No | Namespace identity store (QUICKSIGHT only). |
groups |
list(string) |
[] |
No | Group names to create for RBAC. |
create_folder |
bool |
false |
No | Create a SHARED folder. |
folder_id |
string |
null |
No | Identifier for the SHARED folder. |
folder_name |
string |
null |
No | Display name for the SHARED folder. |
folder_reader_groups |
list(string) |
[] |
No | Groups granted permissions on the folder. |
create_data_source |
bool |
false |
No | Create a data source. |
data_source_id |
string |
null |
No | Identifier for the data source. |
data_source_name |
string |
null |
No | Display name for the data source. |
data_source_type |
string |
ATHENA |
No | ATHENA or S3. |
athena_work_group |
string |
primary |
No | Athena workgroup (ATHENA type). |
s3_manifest_bucket |
string |
null |
No | S3 bucket holding the manifest (S3 type). |
s3_manifest_key |
string |
null |
No | S3 key of the manifest (S3 type). |
data_source_user_groups |
list(string) |
[] |
No | Groups granted use/describe on the data source. |
tags |
map(string) |
{} |
No | Tags merged onto namespace, folder, and data source. |
Outputs
| Name | Description |
|---|---|
account_subscription_status |
Status of the account subscription, or null. |
namespace_arn |
ARN of the created namespace, or null. |
group_arns |
Map of group name → group ARN. |
folder_arn |
ARN of the SHARED folder, or null. |
folder_id |
ID of the SHARED folder, or null. |
data_source_arn |
ARN of the data source, or null. |
data_source_id |
ID of the data source, or null. |
Enterprise scenario
A finance organization standardizes BI on QuickSight Enterprise. The platform team owns a single Terraform stack that calls this module with create_account_subscription = true to subscribe the analytics account once, then defines bi-admins, bi-authors, and bi-readers groups, a Finance Dashboards SHARED folder, and a Lakehouse (Athena) data source pointed at the curated-data workgroup. Analysts are added to groups via aws_quicksight_group_membership in lightweight per-team stacks that set create_account_subscription = false, so no application repo can accidentally manage — or destroy — the account subscription. Because the data source ships with permission blocks granting only bi-admins and bi-authors use access, readers see published dashboards but cannot wire new analyses to raw lakehouse tables, and a quarterly access review reads cleanly off the module’s group_arns and data_source_arn outputs.
Best practices
- Own the account subscription in exactly one stack. There is a single QuickSight subscription per AWS account. Set
create_account_subscription = trueonly in a platform-owned foundation stack; everywhere else usefalse. Managing it from multiple stacks risks a planned replacement that disrupts BI account-wide. - Be deliberate about destroy.
terraform destroyon the subscription tears down QuickSight for the whole account. Apply a deletion safeguard (state separation, restrictive IAM on the stack’s role, and review gates), and prefer disabling features over destroying the subscription resource. - Manage access by group, not user. Define
bi-admins/bi-authors/bi-readersonce and attach folder and data-source permissions to those group ARNs; add people withaws_quicksight_group_membership. This keeps audits simple and avoids per-user permission sprawl. - Always set permissions on folders and data sources. A
SHAREDfolder or data source with nopermissionblock is invisible to analysts. This module wires group-scoped permissions so the right teams can describe and use each asset on day one — least privilege, but actually usable. - Use namespaces only when you need isolation. The
defaultnamespace is correct for a single internal BI estate; create a separateQUICKSIGHT-backed namespace per tenant/environment when you need hard isolation (e.g. embedded multi-tenant analytics), and keep groups/folders consistent within each. - Pin engine choices and tag for chargeback. Fix
editionandauthentication_methoddeliberately (the Describe API does not round-trip every field, so the module ignores spurious diffs), connect data sources to a governed Athena workgroup, and carryEnvironment/Team/CostCentertags so QuickSight spend and ownership are attributable.
Part of the KloudVin Terraform module library. Pair this with the Athena Workgroup, Glue Catalog, and IAM modules — they supply the workgroup, catalog, and identities this module grants QuickSight access to.