Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

🟧 AWS Redshift Terraform Module

A secure-by-default Amazon Redshift data warehouse in one of two selectable shapes — a provisioned cluster or a Redshift Serverless namespace + workgroup — encrypted, private, and logging-enabled out of the box. Built for the AWS provider v6.x.

Terraform aws module type resources


🧩 Overview

  • 🏢 Provisions a complete Amazon Redshift warehouse from a single module call — choose the shape with one variable, deployment_mode.
  • 🅰️ provisioned mode builds an aws_redshift_cluster plus its subnet group, an optional parameter group, and optional audit logging.
  • 🅱️ serverless mode builds an aws_redshiftserverless_namespace + aws_redshiftserverless_workgroup with RPU-based compute.
  • 🔒 Encrypted at rest by default (AWS-managed KMS, or a customer-managed CMK via kms_key_arn).
  • 🚫 Private by defaultpublicly_accessible = false, enhanced VPC routing on, placed only in caller-supplied subnets.
  • 🔑 No plaintext admin password — Secrets Manager generates and manages it by default (manage_admin_password = true).
  • 🧾 Audit logging to an S3 bucket or CloudWatch Logs, plus automated snapshots and a final snapshot on destroy.
  • 🏷️ Universal tagging — every taggable resource carries var.tags; the merged tags_all is surfaced as an output.

💡 Why it matters: a Redshift warehouse often holds the most concentrated PII in an regulatory-regulated estate; this module makes the encrypted, private, logged posture the default and forces an explicit, documented opt-out to weaken it.


❤️ Support this project

If these Terraform modules have been helpful to you or your organization, I'd appreciate your support in any of the following ways:

Whether it's a star, a professional connection, or a coffee, every gesture helps keep these modules actively maintained and continually improving. Thank you for being part of the community!


🗺️ Where this fits in the family

flowchart LR
 vpc["terraform-aws-vpc<br/>subnet_ids"]
 sg["terraform-aws-security-group<br/>vpc_security_group_ids"]
 kms["terraform-aws-kms<br/>kms_key_arn"]
 iam["terraform-aws-iam-role<br/>iam_roles (COPY/UNLOAD)"]
 s3["terraform-aws-s3-bucket<br/>audit-log bucket"]
 sm["terraform-aws-secrets-manager<br/>admin password"]
 rs["terraform-aws-redshift"]
 bi["BI / app clients<br/>endpoint:port"]

 vpc --> rs
 sg --> rs
 kms --> rs
 iam --> rs
 s3 --> rs
 sm -.-> rs
 rs --> bi

 style rs fill:#FF9900,color:#fff
Loading

The warehouse sits downstream of the networking, encryption, and identity foundations: it consumes subnet_ids from terraform-aws-vpc, vpc_security_group_ids from terraform-aws-security-group, a CMK ARN from terraform-aws-kms, COPY/UNLOAD role ARNs from terraform-aws-iam-role, and an audit-log bucket from terraform-aws-s3-bucket. Admin credentials flow back out to Secrets Manager rather than into a Terraform variable.


🧬 What this module builds

flowchart TB
 subgraph mod["terraform-aws-redshift (deployment_mode selector)"]
 direction TB
 subgraph prov["provisioned mode"]
 sng["aws_redshift_subnet_group.this"]
 pg["aws_redshift_parameter_group.this (optional)"]
 cl["aws_redshift_cluster.this — KEYSTONE A"]
 log["aws_redshift_logging.this (optional audit log)"]
 sng --> cl
 pg --> cl
 cl --> log
 end
 subgraph srv["serverless mode"]
 ns["aws_redshiftserverless_namespace.this — KEYSTONE B"]
 wg["aws_redshiftserverless_workgroup.this"]
 ns --> wg
 end
 end

 style cl fill:#FF9900,color:#fff
 style ns fill:#FF9900,color:#fff
Loading

The two modes are mutually exclusive, gated by var.deployment_mode. Each resource is created via for_each over a single-key map so that only the resources for the selected mode are instantiated; the other mode renders zero resources.

Resource Mode Role
aws_redshift_cluster.this provisioned Keystone A — the cluster
aws_redshift_subnet_group.this provisioned Placement across caller subnets
aws_redshift_parameter_group.this provisioned Cluster parameters (only when parameter_group is set)
aws_redshift_logging.this provisioned Audit logging (only when logging is set)
aws_redshiftserverless_namespace.this serverless Keystone B — database + admin + encryption
aws_redshiftserverless_workgroup.this serverless Compute (RPU) + networking

✅ Provider / Versions

Requirement Version
Terraform >= 1.12.0
hashicorp/aws >= 6.0, < 7.0

ℹ️ This module declares only required_providers — it contains no provider {} block. Region and credentials are inherited from the caller's provider configuration.


🔑 Required IAM Permissions

The Terraform identity needs the following least-privilege actions. Provisioned-only and serverless-only actions are marked; grant only those for the mode you deploy.

Action Required for Notes
redshift:CreateCluster, redshift:DeleteCluster, redshift:ModifyCluster, redshift:DescribeClusters Provisioned cluster lifecycle provisioned mode
redshift:CreateClusterSubnetGroup, redshift:DeleteClusterSubnetGroup, redshift:ModifyClusterSubnetGroup, redshift:DescribeClusterSubnetGroups Subnet group provisioned mode
redshift:CreateClusterParameterGroup, redshift:DeleteClusterParameterGroup, redshift:ModifyClusterParameterGroup, redshift:DescribeClusterParameterGroups Parameter group only when parameter_group is set
redshift:EnableLogging, redshift:DisableLogging, redshift:DescribeLoggingStatus Audit logging only when logging is set
redshift-serverless:CreateNamespace, redshift-serverless:DeleteNamespace, redshift-serverless:UpdateNamespace, redshift-serverless:GetNamespace Serverless namespace serverless mode
redshift-serverless:CreateWorkgroup, redshift-serverless:DeleteWorkgroup, redshift-serverless:UpdateWorkgroup, redshift-serverless:GetWorkgroup Serverless workgroup serverless mode
redshift:CreateTags, redshift:DeleteTags, redshift:DescribeTags Tagging resource-level tags
iam:PassRole Attach COPY/UNLOAD roles to the cluster/namespace scope to the specific role ARNs in iam_roles; the role must trust redshift.amazonaws.com
iam:CreateServiceLinkedRole Auto-create AWSServiceRoleForRedshift only on first use in the account; harmless if it already exists
kms:DescribeKey, kms:CreateGrant, kms:RetireGrant CMK-encrypted warehouse only when kms_key_arn (or admin_password_secret_kms_key_id) is supplied; the key policy must also allow Redshift / Secrets Manager
secretsmanager:CreateSecret, secretsmanager:TagResource Managed admin password only when manage_admin_password = true; the secret is created by the Redshift service
s3:PutObject, s3:GetBucketAcl S3 audit-log delivery granted to the log-delivery service on the bucket policy, not on the Terraform identity
ec2:DescribeSubnets, ec2:DescribeSecurityGroups, ec2:DescribeVpcs Resolve networking inputs read-only

ℹ️ The COPY/UNLOAD roles themselves are created by terraform-aws-iam-role and only referenced here by ARN. iam:PassRole is what lets Terraform attach them.


📋 AWS Prerequisites

  • Service-linked role: AWSServiceRoleForRedshift is used by the service to manage ENIs in your VPC. AWS auto-creates it on first cluster/workgroup launch (requires iam:CreateServiceLinkedRole).
  • Subnet group (provisioned): the cluster requires a subnet group spanning the target AZs — this module creates it from subnet_ids. Place those subnets in private address space.
  • Customer-managed KMS (optional): when supplying kms_key_arn, the CMK's key policy must grant the Redshift service (and, for the managed-password secret, Secrets Manager) the necessary kms:* actions.
  • Audit-logging bucket: the S3 bucket named in logging.bucket_name must grant the Redshift log-delivery service principal s3:PutObject and s3:GetBucketAcl via its bucket policy. Buckets in opt-in Regions have additional account requirements.
  • COPY/UNLOAD roles: any role in iam_roles must have a trust policy allowing redshift.amazonaws.com to assume it.
  • Node type / capacity availability: the provisioned node_type (e.g. ra3.xlplus) or the Serverless base RPU must be offered in the target Region/AZ. multi_az and availability_zone_relocation_enabled require the RA3 family.
  • Region: Redshift is a regional service — no us-east-1 global constraint applies. Region is inherited from the caller's provider.
  • Quotas: default soft limits cap clusters and nodes per Region (raisable via Service Quotas). Serverless has default RPU and namespace/workgroup limits per Region.

📁 Module Structure

terraform-aws-redshift/
├── providers.tf # required_providers (aws >= 6.0, < 7.0); no provider block
├── variables.tf # deeply-typed inputs, secure defaults, validation
├── main.tf # subnet/parameter group, cluster, logging, namespace, workgroup
├── outputs.tf # id + arn + endpoint + tags_all + mode-specific outputs
├── README.md # this file
└── SCOPE.md # in/out-of-scope, IAM, prerequisites, gotchas

⚙️ Quick Start

A minimal provisioned warehouse, wired from upstream foundation modules:

module "warehouse" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-redshift?ref=v1.0.0"

  name      = "casey-analytics"
  node_type = "ra3.xlplus"

  # networking (private subnets only)
  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.redshift_sg.id]

  # encryption with a customer-managed CMK
  kms_key_arn = module.kms.arn

  tags = {
    Environment = "prod"
    DataClass   = "PII"
  }
}

Encryption (encrypted = true), private access (publicly_accessible = false), enhanced VPC routing, a Secrets Manager-managed admin password, automated snapshots, and a final snapshot on destroy are all on by default — the call above is already hardened.


🔌 Cross-Module Contract

Consumes

Input Type Source module
subnet_ids list(string) terraform-aws-vpc
vpc_security_group_ids list(string) terraform-aws-security-group
kms_key_arn string (KMS key ARN) terraform-aws-kms
admin_password_secret_kms_key_id string (KMS key id) terraform-aws-kms
iam_roles / default_iam_role_arn list(string) / string (role ARNs) terraform-aws-iam-role
logging.bucket_name string (S3 bucket id) terraform-aws-s3-bucket
master_password string (sensitive) terraform-aws-secrets-manager (only if manage_admin_password = false)

Emits

Output Description Consumed by
id Cluster identifier (provisioned) or namespace id (serverless) references / CLI
arn Cluster ARN (provisioned) or namespace ARN (serverless) — cross-resource reference type IAM policies, monitoring, KMS grants
endpoint Connection host (provisioned) or workgroup endpoint address (serverless) BI tools, application config
port Warehouse port (default 5439) application config, security-group rules
database_name Initial database name client connection strings
master_password_secret_arn Secrets Manager secret ARN for the admin password (when managed) app secret retrieval
namespace_arn / workgroup_arn Serverless ARNs references
tags_all Merged tags incl. provider default_tags governance / audit

📚 Example Library

1 · Minimal provisioned cluster
module "warehouse" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-redshift?ref=v1.0.0"

  name                   = "casey-analytics"
  node_type              = "ra3.xlplus"
  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.redshift_sg.id]
}
2 · Multi-node provisioned cluster
module "warehouse" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-redshift?ref=v1.0.0"

  name            = "casey-warehouse"
  node_type       = "ra3.4xlarge"
  number_of_nodes = 4 # >1 derives cluster_type = "multi-node"
  database_name   = "analytics"

  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.redshift_sg.id]
}
3 · Customer-managed KMS (CMK) encryption
module "warehouse" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-redshift?ref=v1.0.0"

  name      = "casey-secure"
  node_type = "ra3.xlplus"

  encrypted   = true           # default
  kms_key_arn = module.kms.arn # CMK from terraform-aws-kms; key policy must allow Redshift

  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.redshift_sg.id]
}
4 · Tags (merge with provider default_tags)
# Provider default_tags set by the caller, e.g.:
# provider "aws" { default_tags { tags = { Owner = "platform", CostCenter = "1234" } } }

module "warehouse" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-redshift?ref=v1.0.0"

  name      = "casey-tagged"
  node_type = "ra3.xlplus"

  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.redshift_sg.id]

  tags = {
    Environment = "prod"
    DataClass   = "PII"
    Owner       = "data-eng" # resource tag WINS over default_tags Owner = "platform"
  }
}

# module.warehouse.tags_all => Owner=data-eng, CostCenter=1234, Environment=prod, DataClass=PII
5 · Caller-supplied admin password from Secrets Manager (opt-out of managed password)
module "warehouse" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-redshift?ref=v1.0.0"

  name      = "casey-byo-pw"
  node_type = "ra3.xlplus"

  manage_admin_password = false # opt out of Secrets Manager management
  master_username       = "caseyadmin"
  master_password       = data.aws_secretsmanager_secret_version.redshift.secret_string

  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.redshift_sg.id]
}

Prefer the default manage_admin_password = true — it keeps the password out of state and plan output entirely.

6 · Audit logging to an S3 bucket
module "warehouse" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-redshift?ref=v1.0.0"

  name      = "casey-logged"
  node_type = "ra3.xlplus"

  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.redshift_sg.id]

  logging = {
    log_destination_type = "s3"
    bucket_name          = module.audit_logs.id # bucket policy must allow the Redshift log-delivery service
    s3_key_prefix        = "redshift/casey-logged/"
  }
}
7 · Audit logging to CloudWatch Logs
module "warehouse" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-redshift?ref=v1.0.0"

  name      = "casey-cw-logged"
  node_type = "ra3.xlplus"

  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.redshift_sg.id]

  logging = {
    log_destination_type = "cloudwatch"
    log_exports          = ["connectionlog", "useractivitylog", "userlog"]
  }
}
8 · Dedicated parameter group with require_ssl
module "warehouse" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-redshift?ref=v1.0.0"

  name      = "casey-tls"
  node_type = "ra3.xlplus"

  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.redshift_sg.id]

  parameter_group = {
    family = "redshift-1.0"
    parameters = {
      require_ssl                  = "true"
      enable_user_activity_logging = "true"
    }
  }
}
9 · COPY/UNLOAD IAM roles
module "warehouse" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-redshift?ref=v1.0.0"

  name      = "casey-etl"
  node_type = "ra3.xlplus"

  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.redshift_sg.id]

  iam_roles            = [module.copy_role.arn, module.unload_role.arn] # must trust redshift.amazonaws.com
  default_iam_role_arn = module.copy_role.arn
}
10 · RA3 Multi-AZ high availability
module "warehouse" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-redshift?ref=v1.0.0"

  name            = "casey-ha"
  node_type       = "ra3.4xlarge"
  number_of_nodes = 2
  multi_az        = true # RA3 only

  subnet_ids             = module.vpc.private_subnet_ids # span >= 2 AZs
  vpc_security_group_ids = [module.redshift_sg.id]
}
11 · Restore a provisioned cluster from a snapshot
module "warehouse" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-redshift?ref=v1.0.0"

  name      = "casey-restored"
  node_type = "ra3.xlplus"

  snapshot_identifier = "casey-analytics-2026-06-20"
  # owner_account = "111122223333" # only if restoring a snapshot you do not own

  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.redshift_sg.id]
}
12 · Redshift Serverless namespace + workgroup
module "warehouse" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-redshift?ref=v1.0.0"

  name            = "casey-serverless"
  deployment_mode = "serverless"

  base_capacity = 32  # RPUs, multiples of 8
  max_capacity  = 256 # cap burst cost
  database_name = "analytics"

  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.redshift_sg.id]

  kms_key_arn            = module.kms.arn
  serverless_log_exports = ["connectionlog", "useractivitylog", "userlog"]
}
13 · Serverless with AI price-performance target
module "warehouse" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-redshift?ref=v1.0.0"

  name            = "casey-autoscale"
  deployment_mode = "serverless"
  base_capacity   = 16

  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.redshift_sg.id]

  price_performance_target = {
    enabled = true
    level   = 50 # 1 = favor cost, 100 = favor speed
  }

  config_parameters = {
    require_ssl = "true"
  }
}
14 · ⚠️ Secure-by-default opt-out (documented exception)
# Use ONLY with a documented, risk-accepted exception. Each line below WEAKENS the baseline.
module "warehouse" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-redshift?ref=v1.0.0"

  name      = "casey-lab"
  node_type = "dc2.large"

  encrypted            = false # ⚠️ at-rest encryption OFF — never for PII
  publicly_accessible  = true  # ⚠️ public endpoint + Elastic IP
  enhanced_vpc_routing = false # ⚠️ COPY/UNLOAD can traverse the public internet
  skip_final_snapshot  = true  # ⚠️ data permanently discarded on destroy

  subnet_ids             = module.vpc.public_subnet_ids
  vpc_security_group_ids = [module.redshift_sg.id]
}
15 · 🏁 End-to-end composition (foundations → warehouse)
module "kms" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-kms?ref=v1.0.0"
  name   = "casey-redshift"
}

module "vpc" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-vpc?ref=v1.0.0"
  name   = "casey-data"
  #...cidr, subnets...
}

module "redshift_sg" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-security-group?ref=v1.0.0"
  name   = "casey-redshift"
  vpc_id = module.vpc.id

  ingress_rules = {
    redshift = {
      from_port                = 5439
      to_port                  = 5439
      protocol                 = "tcp"
      source_security_group_id = module.app_sg.id # never 0.0.0.0/0
    }
  }
}

module "audit_logs" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-s3-bucket?ref=v1.0.0"
  name   = "casey-redshift-audit-logs"
  #...attach a bucket policy allowing the Redshift log-delivery service...
}

module "copy_role" {
  source             = "git::https://github.com/microsoftexpert/terraform-aws-iam-role?ref=v1.0.0"
  name               = "casey-redshift-copy"
  assume_role_policy = data.aws_iam_policy_document.redshift_trust.json # trusts redshift.amazonaws.com
}

module "warehouse" {
  source = "git::https://github.com/microsoftexpert/terraform-aws-redshift?ref=v1.0.0"

  name      = "casey-analytics"
  node_type = "ra3.4xlarge"

  subnet_ids             = module.vpc.private_subnet_ids
  vpc_security_group_ids = [module.redshift_sg.id]
  kms_key_arn            = module.kms.arn

  iam_roles            = [module.copy_role.arn]
  default_iam_role_arn = module.copy_role.arn

  logging = {
    log_destination_type = "s3"
    bucket_name          = module.audit_logs.id
    s3_key_prefix        = "redshift/casey-analytics/"
  }

  tags = {
    Environment = "prod"
    DataClass   = "PII"
  }
}

output "warehouse_endpoint" {
  value = module.warehouse.endpoint
}

📥 Inputs (high-level)

  • Core / identity: name, deployment_mode.
  • Networking: subnet_ids, vpc_security_group_ids, subnet_group_description, port, enhanced_vpc_routing, publicly_accessible, elastic_ip.
  • Provisioned compute: node_type, number_of_nodes, cluster_type, cluster_version, database_name, availability_zone, availability_zone_relocation_enabled, multi_az.
  • Admin credentials: master_username, manage_admin_password, master_password (sensitive), admin_password_secret_kms_key_id.
  • Encryption: encrypted, kms_key_arn.
  • IAM roles: iam_roles, default_iam_role_arn.
  • Maintenance: allow_version_upgrade, apply_immediately, preferred_maintenance_window, maintenance_track_name.
  • Snapshots & recovery: automated_snapshot_retention_period, manual_snapshot_retention_period, skip_final_snapshot, final_snapshot_identifier, snapshot_identifier, snapshot_arn, snapshot_cluster_identifier, owner_account.
  • Parameter group / logging (provisioned): parameter_group_name, parameter_group, logging.
  • Serverless: namespace_name, serverless_log_exports, workgroup_name, base_capacity, max_capacity, track_name, config_parameters, price_performance_target.
  • Universal: tags, timeouts.

🧾 Outputs

  • Primary: id, arn, name, deployment_mode, tags_all.
  • Connectivity: endpoint, port, database_name, dns_name (provisioned only — null in serverless).
  • Provisioned cluster: cluster_identifier, cluster_namespace_arn, cluster_revision_number (all null in serverless).
  • Admin secret: master_password_secret_arnnull unless manage_admin_password = true.
  • Serverless: namespace_arn, namespace_id, namespace_name, workgroup_arn, workgroup_id, workgroup_name (all null in provisioned).
  • Subnet/parameter groups: subnet_group_name, subnet_group_arn, parameter_group_name, parameter_group_arn.

Conditionally-present outputs use try(..., null) and resolve to null in the mode that does not create the resource. No secret value is emitted — only the Secrets Manager secret ARN.


🧠 Architecture Notes

  • ARN / ID formats:
  • Provisioned cluster ARN — arn:aws:redshift:<region>:<account>:cluster:<cluster_identifier>; id is the cluster identifier.
  • Serverless namespace ARN — arn:aws:redshift-serverless:<region>:<account>:namespace/<uuid>; workgroup ARN — arn:aws:redshift-serverless:<region>:<account>:workgroup/<uuid>. id is the namespace id.
  • arn is the cross-resource reference type — IAM resource policies, KMS grant principals, and monitoring all key on ARNs.
  • FORCE-NEW / immutable fields: cluster_identifier (name), database_name, master_username, cluster_subnet_group_name, and the parameter group family recreate the resource on change. Enabling/disabling encrypted or changing kms_key_id triggers a recreate or a long encryption migration. Changing deployment_mode destroys one resource set and creates the other — never an in-place edit.
  • Resize vs recreate: changing node_type or number_of_nodes is a Redshift resize operation, not a destroy — but it can be disruptive and lengthy.
  • tagstags_alldefault_tags: var.tags flows to every taggable resource. tags_all is the computed merge of resource tags over provider default_tags, with resource tags winning on key conflict. default_tags is the caller's provider-block concern and is never set inside this module.
  • Eventual consistency: a newly created subnet group / parameter group / IAM role may not be immediately visible to the cluster API; the resource graph orders creation, but transient InvalidParameterValue on first apply can occur and succeed on retry.
  • Destroy ordering: Redshift manages ENIs in your subnets via the service-linked role; these are torn down when the cluster/workgroup is deleted. If the cluster's security group or subnets are managed in the same root and destroyed concurrently, deletion can stall on lingering ENIs — let Terraform's dependency graph delete the warehouse first. A skip_final_snapshot = false cluster also takes a final snapshot on destroy, which extends destroy time.
  • No us-east-1 global constraint: Redshift is regional; nothing here must live in us-east-1.

🧱 Design Principles

Secure-by-default posture, with the variable that relaxes each control:

Hardened default Variable Opt-out
Encryption at rest on (AWS-managed KMS; CMK via kms_key_arn) encrypted encrypted = false (discouraged; never for PII)
Private access (no public endpoint) publicly_accessible publicly_accessible = true (documented exception)
Enhanced VPC routing on enhanced_vpc_routing enhanced_vpc_routing = false
Admin password in Secrets Manager (never in state) manage_admin_password manage_admin_password = false + master_password
Final snapshot taken on destroy skip_final_snapshot skip_final_snapshot = true (discouraged)
7-day automated snapshot retention automated_snapshot_retention_period set to 0 to disable (discouraged)
Audit logging available logging / serverless_log_exports leave null/empty to disable

Additional principles: placement in private subnets only; security groups must restrict ingress to the warehouse port from application SGs (never 0.0.0.0/0); one composite covers both deployment shapes behind a single selector; COPY/UNLOAD roles and the log bucket are referenced by ARN/name to keep the module's blast radius to warehouse resources.


🚀 Runbook

terraform init -backend=false
terraform validate
terraform fmt -check

# plan/apply require valid AWS credentials (profile / SSO / OIDC) and a Region
terraform plan
terraform apply
terraform output

⚠️ Always pin the module source with ?ref=v1.0.0 — never track a branch.


🧪 Testing

  • terraform init -backend=false then terraform validate — structural validation, no credentials needed.
  • terraform fmt -check — formatting gate.
  • terraform plan against a sandbox account to confirm the selected deployment_mode renders the expected resource set and that secure defaults are present in the plan.
  • Verify tags_all reflects the merge of var.tags over provider default_tags.
  • Confirm master_password never appears in plan output (it is sensitive) and that no password is emitted as an output.

💬 Example Output

Apply complete! Resources: 3 added, 0 changed, 0 destroyed.

Outputs:

arn = "arn:aws:redshift:us-east-1:111122223333:cluster:casey-analytics"
database_name = "analytics"
endpoint = "casey-analytics.abc123.us-east-1.redshift.amazonaws.com:5439"
id = "casey-analytics"
master_password_secret_arn = "arn:aws:secretsmanager:us-east-1:111122223333:secret:redshift!casey-analytics-AbCdEf"
port = 5439
tags_all = tomap({ "DataClass" = "PII", "Environment" = "prod" })

🔍 Troubleshooting

  • Tag drift / perpetual diff on tags. A key set in both var.tags and provider default_tags shows churn if expectations differ — resource tags win in tags_all. Align the two or move the key to one place. Use provider ignore_tags for tags applied out-of-band.
  • Credential-chain failures (NoCredentialProviders, ExpiredToken). validate/fmt work offline; plan/apply need a resolved provider — set AWS_PROFILE, refresh SSO, or confirm the OIDC role assumption in CI.
  • InvalidParameterValue / role-not-found on first apply. Eventual consistency between a just-created IAM role / subnet group and the cluster API — re-run apply; it typically succeeds on retry.
  • AccessDenied creating the cluster. Check the IAM table above — common gaps are iam:PassRole for iam_roles, iam:CreateServiceLinkedRole for AWSServiceRoleForRedshift, and kms:CreateGrant when a CMK is supplied.
  • Audit logging fails to enable. The S3 bucket policy must grant the Redshift log-delivery service s3:PutObject and s3:GetBucketAcl; a bucket in a different Region or account, or one with Block Public Access misconfigured for the service principal, will reject delivery.
  • Destroy stalls on ENIs / NAT / dependencies. Redshift ENIs are removed when the warehouse is deleted; don't destroy the warehouse's subnets/security group in the same operation ahead of it — let the dependency graph delete the cluster first.
  • Destroy blocked by missing final-snapshot name. With skip_final_snapshot = false, a final_snapshot_identifier is required; the module derives <name>-final, but a name collision with an existing snapshot will fail — supply a unique identifier.
  • Switching deployment_mode recreates everything. This is expected — the modes are mutually exclusive. Plan for data migration (snapshot/restore or UNLOAD/COPY) before flipping the selector.
  • Public-access surprise. If the warehouse is unexpectedly reachable, confirm publicly_accessible = false (default) and that subnets are private with no 0.0.0.0/0 ingress on the security group.

🔗 Related Docs

  • Terraform Registry — aws_redshift_cluster, aws_redshift_subnet_group, aws_redshift_parameter_group, aws_redshift_logging
  • Terraform Registry — aws_redshiftserverless_namespace, aws_redshiftserverless_workgroup
  • Amazon Redshift Management Guide — provisioned clusters overview, database encryption, database audit logging
  • Amazon Redshift — comparing Redshift Serverless to a provisioned data warehouse
  • Amazon Redshift — security overview and IAM access-control overview
  • Sibling modules — terraform-aws-vpc, terraform-aws-security-group, terraform-aws-kms, terraform-aws-iam-role, terraform-aws-s3-bucket, terraform-aws-secrets-manager

🧡 "Infrastructure as Code should be standardized, consistent, and secure."