Skip to main content
Aurora clusters incur AWS charges while running. The instance classes used in this example (db.r8g.2xlarge, db.r8g.large) are not free-tier eligible. Destroy the cluster when you are done to avoid unexpected costs.
1

Prerequisites

Before you begin, ensure you have the following:
  • Terraform >= 1.11.1 installed. Verify with:
    terraform version
    
  • AWS credentials configured in your environment. The IAM principal needs permissions to create VPC resources, RDS clusters, IAM roles, KMS keys, CloudWatch log groups, and security groups.
    aws configure
    # or set environment variables
    export AWS_ACCESS_KEY_ID="..."
    export AWS_SECRET_ACCESS_KEY="..."
    export AWS_REGION="eu-west-1"
    
  • AWS provider configured in your Terraform root module:
    terraform {
      required_version = ">= 1.11.1"
    
      required_providers {
        aws = {
          source  = "hashicorp/aws"
          version = ">= 6.28"
        }
      }
    }
    
    provider "aws" {
      region = "eu-west-1"
    }
    
2

Write the configuration

Create a directory for your configuration and add the following files. This example is taken directly from the module’s PostgreSQL example and provisions a three-instance Aurora PostgreSQL 17.5 cluster with a custom VPC, parameter groups, CloudWatch log exports, custom endpoints, and an activity stream.
The example sets skip_final_snapshot = true, which means no final snapshot is created when you destroy the cluster. This is appropriate for development and testing environments. Remove this argument or set it to false for production clusters.
provider "aws" {
  region = local.region
}

data "aws_availability_zones" "available" {
  # Exclude local zones
  filter {
    name   = "opt-in-status"
    values = ["opt-in-not-required"]
  }
}

locals {
  name   = "ex-postgresql"
  region = "eu-west-1"

  vpc_cidr = "10.0.0.0/16"
  azs      = slice(data.aws_availability_zones.available.names, 0, 3)

  tags = {
    Example    = local.name
    GithubRepo = "terraform-aws-rds-aurora"
    GithubOrg  = "terraform-aws-modules"
  }
}

################################################################################
# RDS Aurora Module
################################################################################

module "aurora" {
  source  = "terraform-aws-modules/rds-aurora/aws"

  name                        = local.name
  engine                      = "aurora-postgresql"
  engine_version              = "17.5"
  master_username             = "root"
  storage_type                = "aurora-iopt1"
  cluster_monitoring_interval = 30

  instances = {
    1 = {
      instance_class          = "db.r8g.2xlarge"
      publicly_accessible     = true
      db_parameter_group_name = "default.aurora-postgresql14"
    }
    2 = {
      identifier     = "static-member-1"
      instance_class = "db.r8g.2xlarge"
    }
    3 = {
      identifier     = "excluded-member-1"
      instance_class = "db.r8g.large"
      promotion_tier = 15
    }
  }

  endpoints = {
    static = {
      identifier     = "static-custom-endpt"
      type           = "ANY"
      static_members = ["static-member-1"]
      tags           = { Endpoint = "static-members" }
    }
    excluded = {
      identifier       = "excluded-custom-endpt"
      type             = "READER"
      excluded_members = ["excluded-member-1"]
      tags             = { Endpoint = "excluded-members" }
    }
  }

  vpc_id               = module.vpc.vpc_id
  db_subnet_group_name = module.vpc.database_subnet_group_name
  security_group_ingress_rules = {
    private-az1 = {
      cidr_ipv4 = element(module.vpc.private_subnets_cidr_blocks, 0)
    }
    private-az2 = {
      cidr_ipv4 = element(module.vpc.private_subnets_cidr_blocks, 1)
    }
    private-az3 = {
      cidr_ipv4 = element(module.vpc.private_subnets_cidr_blocks, 2)
    }
  }

  apply_immediately   = true
  skip_final_snapshot = true

  engine_lifecycle_support = "open-source-rds-extended-support-disabled"

  cluster_parameter_group = {
    name        = local.name
    family      = "aurora-postgresql17"
    description = "${local.name} example cluster parameter group"
    parameters = [
      {
        name         = "log_min_duration_statement"
        value        = 4000
        apply_method = "immediate"
      },
      {
        name         = "rds.force_ssl"
        value        = 1
        apply_method = "pending-reboot"
      }
    ]
  }

  db_parameter_group = {
    name        = local.name
    family      = "aurora-postgresql17"
    description = "${local.name} example DB parameter group"
    parameters = [
      {
        name         = "log_min_duration_statement"
        value        = 4000
        apply_method = "immediate"
      }
    ]
  }

  enabled_cloudwatch_logs_exports = ["postgresql"]
  create_cloudwatch_log_group     = true

  cluster_activity_stream = {
    kms_key_id = module.kms.key_id
    mode       = "async"
  }

  tags = local.tags
}

################################################################################
# Supporting Resources
################################################################################

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "~> 6.0"

  name = local.name
  cidr = local.vpc_cidr

  azs              = local.azs
  public_subnets   = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k)]
  private_subnets  = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 3)]
  database_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 6)]

  tags = local.tags
}

module "kms" {
  source  = "terraform-aws-modules/kms/aws"
  version = "~> 4.0"

  deletion_window_in_days = 7
  description             = "KMS key for ${local.name} cluster activity stream"
  enable_key_rotation     = true
  is_enabled              = true
  key_usage               = "ENCRYPT_DECRYPT"

  aliases = ["rds/${local.name}"]

  tags = local.tags
}
3

Initialize and apply

Initialize the working directory to download the module and provider, then review and apply the plan.
terraform init
Review the resources Terraform will create before applying:
terraform plan
Apply the configuration. Type yes when prompted to confirm:
terraform apply
Provisioning an Aurora cluster typically takes 10–15 minutes. Terraform will print output values when the apply completes.
4

Access the cluster endpoint

After a successful apply, retrieve the connection details from the Terraform outputs:
terraform output cluster_endpoint
terraform output cluster_reader_endpoint
terraform output cluster_port
terraform output cluster_master_user_secret
The cluster_endpoint output is the writer endpoint you use for read/write connections. The cluster_reader_endpoint is a load-balanced read-only endpoint across all reader instances.The master user password is managed by AWS Secrets Manager. Retrieve it with the AWS CLI using the secret ARN from the cluster_master_user_secret output:
aws secretsmanager get-secret-value \
  --secret-id "$(terraform output -raw cluster_master_user_secret | jq -r '.secret_arn')" \
  --query SecretString \
  --output text
Connect to the cluster using the writer endpoint:
psql -h "$(terraform output -raw cluster_endpoint)" \
     -U root \
     -p "$(terraform output -raw cluster_port)"  
5

Clean up

To remove all resources created by this example and stop incurring charges:
terraform destroy
Type yes when prompted. Because skip_final_snapshot = true is set in this example, no final snapshot will be created before the cluster is deleted.

Build docs developers (and LLMs) love