terraform_aurora_postgresql

Overview

In this module, we will configure Aurora PostrgreSQL with one node (reader and writer). Furthermore, we will configure PostgreSQL pg_logical extension to replicate to another PostgreSQL RDS. So let's get started.

Configure the publisher / provider

Firstly, we will start with the creation of the Aurora PostgreSQL, which will acts as a Publisher / Provider

Aurora PostgreSQL

resource "aws_rds_cluster_parameter_group" "pg14logical" {
  name        = "aurorapgsql14logicalcluster"
  family      = "aurora-postgresql14"
  description = "RDS cluster parameter group for logical replication"

  parameter {
    name  = "max_replication_slots"
    value = "10"
    apply_method = "pending-reboot"
  }

  parameter {
    name  = "max_wal_senders"
    value = "15"
    apply_method = "pending-reboot"
  }
  parameter {
    name  = "max_worker_processes"
    value = "10"
    apply_method = "pending-reboot"
  }
  parameter {
    name  = "rds.logical_replication"
    value = "1"
    apply_method = "pending-reboot"

  }
  parameter {
    name  = "shared_preload_libraries"
    value = "pg_stat_statements,pglogical"
    apply_method = "pending-reboot"
  }
}

locals {
  destination_endpoint = "${trim(module.cluster.cluster_endpoint,":5432")}"
  rds_cidr_blocks = [for s in data.aws_subnet.rds_subnet_array : s.cidr_block]
  all_cidr_blocks = concat(tolist([data.aws_subnet.ec2_subnet.cidr_block]), local.rds_cidr_blocks)
}

data "aws_subnets" "rds_subnets" {
  filter {
    name   = "tag:Name"
    values = ["${var.environment}-main-db*"]
  }
}

data "aws_subnet" "rds_subnet_array" {
  for_each = toset(data.aws_subnets.rds_subnets.ids)
  id       = each.value
}

data "aws_vpc" "main_vpc" {
  filter {
    name   = "tag:Name"
    values = ["${var.environment}-main"]
  }
}

resource "aws_key_pair" "deployer" {
  key_name   = "deployer-key"
  public_key = var.public_key
}

data "aws_subnet" "ec2_subnet" {
  id = data.aws_subnets.private_subnets.ids[0]
}

data "aws_subnets" "private_subnets" {
  filter {
    name   = "tag:Name"
    values = ["*main-private*"]
  }
}

resource "aws_security_group" "pgsql_allow" {
  name        = "allow_postgresql_ingress"
  description = "Allow PostgreSQL & SSH access"
  vpc_id      = data.aws_vpc.main_vpc.id

  tags = {
    Name = "allow_ssh_postgresql"
  }
}

data "aws_security_group" "selected_sg" {
  vpc_id = data.aws_vpc.main_vpc.id

  filter {
    name   = "tag:Name"
    values = ["allow_ssh_postgresql"]

  }
  depends_on = [aws_security_group.pgsql_allow]
}

data "aws_security_group" "default" {
  vpc_id = data.aws_vpc.main_vpc.id
  name = "default"
}

resource "aws_security_group_rule" "ingress_rules" {
  count             = length(var.rules)
  type              = "ingress"
  from_port         = var.rules[count.index].from_port
  to_port           = var.rules[count.index].to_port
  protocol          = var.rules[count.index].protocol
  cidr_blocks       = local.all_cidr_blocks
  description       = var.rules[count.index].description
  security_group_id = data.aws_security_group.selected_sg.id
}

resource "aws_security_group_rule" "egress_rules" {
  count             = length(var.rules)
  type              = "egress"
  from_port         = var.rules[count.index].from_port
  to_port           = var.rules[count.index].to_port
  protocol          = var.rules[count.index].protocol
  cidr_blocks       = local.all_cidr_blocks
  description       = var.rules[count.index].description
  security_group_id = data.aws_security_group.selected_sg.id
}

resource "local_file" "executable_file" {
  file_permission = "644"
  content = templatefile("${path.cwd}/source.tftpl", {
    source_endpoing         = "${module.cluster.cluster_endpoint}"
    database_name           = "${var.database_name}"
    database_admin_username = "${var.database_admin_username}"
    database_admin_password = "${var.database_admin_password}"
  })
  filename = "source.sql"
}

resource "aws_instance" "ec2_instance" {
  ami           = var.ami
  subnet_id     = data.aws_subnets.private_subnets.ids[0]
  instance_type = "t3.micro"
  key_name      = aws_key_pair.deployer.key_name
  vpc_security_group_ids = [data.aws_security_group.selected_sg.id, data.aws_security_group.default.id]

  tags = {
    Name        = "psqlclient"
    Environment = "Backup"
    Terraform   = "true"
  }
}

resource "null_resource" "database_configuration"{
  
    triggers = {
    always_run = timestamp()
    }
    provisioner "file" {
    source      = "${var.source_path}"
    destination = "/tmp/source.sql"

    connection {
      type     = "ssh"
      user     = "ec2-user"
      private_key = "${file(var.private_key_path)}"
      host     = aws_instance.ec2_instance.private_ip
    }
  }

    provisioner "remote-exec" {
    inline = ["sudo yum -y update",
    "sudo yum -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm",
    "sudo yum install -y postgresql14",
	  "sudo yum install -y telnet",
	  "sudo yum install -y unzip",
    "PGPASSWORD=\"${var.database_admin_password}\" psql -U${var.database_admin_username} -h ${local.destination_endpoint} -d ${var.database_name}  -f /tmp/source.sql"]

    connection {
      type     = "ssh"
      user     = "ec2-user"
      private_key = "${file(var.private_key_path)}"
      host     = aws_instance.ec2_instance.private_ip
    }
  }
  depends_on = [module.cluster]
}

module "cluster" {
  source                      = "terraform-aws-modules/rds-aurora/aws"
  name                        = "test-aurora-db-postgres145"
  engine                      = "aurora-postgresql"
  engine_version              = "14.5"
  manage_master_user_password = false
  master_username             = "${var.database_admin_username}"
  master_password             = "${var.database_admin_password}"
  instances = {
    one = {
      instance_class = "db.r5.large"
    }
  }

  vpc_id                 = data.aws_vpc.main_vpc.id
  db_cluster_parameter_group_name = aws_rds_cluster_parameter_group.pg14logical.name
  vpc_security_group_ids = [data.aws_security_group.selected_sg.id]
  db_subnet_group_name   = "${var.environment}-db-main"
  skip_final_snapshot    = true

  tags = {
    Environment = "dev"
    Terraform   = "true"
  }
}

To do


data "aws_vpc" "main_vpc" {
  filter {
    name   = "tag:Name"
    values = ["${var.environment}-main"]
  }
}

resource "aws_key_pair" "deployer" {
  key_name   = "deployer-key"
  public_key = "${var.public_key}"
}

data "aws_subnet" "ec2_subnet" {
	id = data.aws_subnets.private_subnets.ids[0]
}

data "aws_subnets" "private_subnets" {
	filter {
		name = "tag:Name"
		values = ["*main-private*"]
	}
}

resource "aws_security_group" "pgsql_allow" {
  name        = "allow_postgresql_ingress"
  description = "Allow PostgreSQL & SSH access"
  vpc_id      =  data.aws_vpc.main_vpc.id
  
  tags = {
    Name = "allow_ssh_postgresql"
  }
}

data "aws_security_group" "selected_sg" {
	vpc_id      =  data.aws_vpc.main_vpc.id
	
	filter {
		name = "tag:Name"
		values = ["allow_ssh_postgresql"]
	
	}
	depends_on = [aws_security_group.pgsql_allow]
}

resource "aws_security_group_rule" "ingress_rules" {
  count = length(var.ingress_rules)
  type              = "ingress"
  from_port         = var.ingress_rules[count.index].from_port
  to_port           = var.ingress_rules[count.index].to_port
  protocol          = var.ingress_rules[count.index].protocol
  cidr_blocks       = [data.aws_subnet.ec2_subnet.cidr_block]
  description       = var.ingress_rules[count.index].description
  security_group_id = data.aws_security_group.selected_sg.id
}

resource "aws_instance" "ec2_instance" {
    ami = var.ami
	count = 1
    subnet_id = data.aws_subnets.private_subnets.ids[0]
    instance_type = "t3.micro"
    key_name = aws_key_pair.deployer.key_name
	
	user_data=<<-EOF
	#!/bin/bash
	yum -y update
	yum -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm
	yum install -y postgresql14
	yum install -y barman
	yum install -y barman-cli
	yum install -y telnet
	yum install -y unzip
	curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
	unzip awscliv2.zip
	sudo ./aws/install
	EOF
	
	tags = {
		Name = "Barman"
		Environment = "Backup"
		Terraform = "true"
	}
}

module "cluster" {
  source  = "terraform-aws-modules/rds-aurora/aws"
  name           = "test-aurora-db-postgres145"
  engine         = "aurora-postgresql"
  engine_version = "14.5"
  manage_master_user_password = false
  master_username= "admin1"
  master_password= "Test1234#!"
  instances = {
    one = {
	  instance_class = "db.r5.large"
	}
    two = {
      instance_class = "db.r5.large"
	}
  }
  
  vpc_id               	 = data.aws_vpc.main_vpc.id
  vpc_security_group_ids = [data.aws_security_group.selected_sg.id]
  db_subnet_group_name   = "${var.environment}-db-main"
  skip_final_snapshot  	 = true

  tags = {
    Environment = "dev"
    Terraform   = "true"
  }
}

This code will create Aurora PostgreSQL with pg_logical extension enabled and furthermore, it will execute the SQL template below:

SQL Template Source

/* Setup */
\c ${database_name}
create extension pglogical;
select pglogical.drop_node('provider', true);

/* Create node */
select pglogical.create_node(node_name := 'provider', dsn := 'host=${source_endpoing} port=5432 user=${database_admin_username} dbname=${database_name} password=${database_admin_password}');

/* DR Setup */
select pglogical.drop_replication_set('b2b_subscription',true);
select pglogical.create_replication_set(set_name := 'b2b_subscription');
select pglogical.replication_set_add_all_tables(set_name  := 'dr', schema_names := ARRAY['a2a_transfers'], synchronize_data := true);
select pglogical.replication_set_add_all_tables(set_name  := 'dr', schema_names := ARRAY['account_service'], synchronize_data := true);


/* BI Setup */
select pglogical.drop_replication_set(set_name := 'bi_subscription');
select pglogical.create_replication_set(set_name := 'bi_subscription');
select pglogical.replication_set_add_table(set_name := 'bi_subscription', relation := 'bi.employees', synchronize_data := true, columns := '{id,salary}');
select pglogical.replication_set_add_table('bi_subscription', 'bi.departments');
select pglogical.replication_set_add_table('bi_subscription', 'bi.sales');


select pglogical.replicate_ddl_command('create schema bi;', '{ddl_sql}');
select pglogical.replicate_ddl_command('create table bi.employees(id int primary key, name text, salary int);', '{ddl_sql}');
select pglogical.replicate_ddl_command('create table bi.departments(id int primary key, name text);', '{ddl_sql}');

These are OF COURSE, test settings, BUT they will setup a pg_logical extension and start the node as publisher/provider Then, let's configure the Subscriber

Configure the Subscriber

The following code, will configure RDS PostgreSQL and it will create some Demo structure for the pg_logical:

Configure Subscriber

resource "aws_db_parameter_group" "rds_pg14logical" {
  name        = "pgsql14logicalrds-destination"
  family      = "postgres14"
  description = "RDS parameter group for logical replication"

  parameter {
    name  = "max_replication_slots"
    value = "10"
    apply_method = "pending-reboot"
  }
  parameter {
    name  = "max_wal_senders"
    value = "15"
    apply_method = "pending-reboot"
  }
  parameter {
    name  = "max_worker_processes"
    value = "10"
    apply_method = "pending-reboot"
  }
  parameter {
    name  = "rds.logical_replication"
    value = "1"
    apply_method = "pending-reboot"
  }
  parameter {
    name  = "shared_preload_libraries"
    value = "pg_stat_statements,pglogical"
    apply_method = "pending-reboot"
  }

  lifecycle {
    create_before_destroy = true
  }
}


locals {
  destination_endpoint = "${trim(module.db.db_instance_endpoint,":5432")}"
  rds_cidr_blocks = [for s in data.aws_subnet.rds_subnet_array : s.cidr_block]
  all_cidr_blocks = concat(tolist([data.aws_subnet.ec2_subnet.cidr_block]), local.rds_cidr_blocks)
}


data "aws_vpc" "main_vpc" {
  filter {
    name   = "tag:Name"
    values = ["${var.environment}-main"]
  }
}

resource "aws_key_pair" "deployer" {
  key_name   = "deployer-key-destination"
  public_key = var.public_key
}

data "aws_subnets" "private_subnets" {
  filter {
    name   = "tag:Name"
    values = ["*main-private*"]
  }
}

data "aws_subnet" "ec2_subnet" {
  id = data.aws_subnets.private_subnets.ids[0]
}

data "aws_subnets" "rds_subnets" {
  filter {
    name   = "tag:Name"
    values = ["${var.environment}-main-db*"]
  }
}

data "aws_subnet" "rds_subnet_array" {
  for_each = toset(data.aws_subnets.rds_subnets.ids)
  id       = each.value
}

resource "aws_security_group" "pgsql_allow" {
  name        = "allow_postgresql_ingress_destination"
  description = "Allow PostgreSQL & SSH access"
  vpc_id      = data.aws_vpc.main_vpc.id

  tags = {
    Name = "allow_ssh_postgresql_destination"
  }
}

data "aws_security_group" "selected_allow_ssh_postgresql" {
  vpc_id = data.aws_vpc.main_vpc.id

  filter {
    name   = "tag:Name"
    values = ["allow_ssh_postgresql_destination"]

  }
  depends_on = [aws_security_group.pgsql_allow]
}

data "aws_security_group" "default" {
  vpc_id = data.aws_vpc.main_vpc.id
  name = "default"
}

resource "aws_security_group_rule" "ingress_rules" {
  count             = length(var.rules)
  type              = "ingress"
  from_port         = var.rules[count.index].from_port
  to_port           = var.rules[count.index].to_port
  protocol          = var.rules[count.index].protocol
  cidr_blocks       = local.all_cidr_blocks
  description       = var.rules[count.index].description
  security_group_id = data.aws_security_group.selected_allow_ssh_postgresql.id
}

resource "aws_security_group_rule" "egress_rules" {
  count             = length(var.rules)
  type              = "egress"
  from_port         = var.rules[count.index].from_port
  to_port           = var.rules[count.index].to_port
  protocol          = var.rules[count.index].protocol
  cidr_blocks       = local.all_cidr_blocks
  description       = var.rules[count.index].description
  security_group_id = data.aws_security_group.selected_allow_ssh_postgresql.id
}

resource "local_file" "executable_file" {
  file_permission = "644"
  content = templatefile("${path.cwd}/destination.tftpl", {
    destination_endpoint    = "${local.destination_endpoint}"
    database_name           = "${var.database_name}"
    database_admin_username = "${var.database_admin_username}"
    database_admin_password = "${var.database_admin_password}"
    source_endpoint         = "${var.source_endpoint}"
  })
  filename = "destination.sql"
}

resource "aws_instance" "ec2_instance" {
  ami           = var.ami
  subnet_id     = data.aws_subnets.private_subnets.ids[0]
  instance_type = "t3.micro"
  key_name      = aws_key_pair.deployer.key_name
  vpc_security_group_ids = [data.aws_security_group.selected_allow_ssh_postgresql.id, data.aws_security_group.default.id]

  tags = {
    Name        = "ec2instance-dest"
    Environment = "pglogical"
    Terraform   = "true"
  }
}

resource "null_resource" "database_configuration"{

    triggers = {
    always_run = timestamp()
    }
  
    provisioner "file" {
    source      = "${var.destination_sql_path}"
    destination = "/tmp/destination.sql"

    connection {
      type     = "ssh"
      user     = "ec2-user"
      private_key = "${file(var.private_key_path)}"
      host     = aws_instance.ec2_instance.private_ip
    }
  }

    provisioner "remote-exec" {
    inline = ["sudo yum -y update",
    "sudo yum -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm",
    "sudo yum install -y postgresql14",
	  "sudo yum install -y telnet",
	  "sudo yum install -y unzip",
    "PGPASSWORD=\"${var.database_admin_password}\" psql -U${var.database_admin_username} -h ${local.destination_endpoint} -d postgres  -f /tmp/destination.sql"]

    connection {
      type     = "ssh"
      user     = "ec2-user"
      private_key = "${file(var.private_key_path)}"
      host     = aws_instance.ec2_instance.private_ip
    }
  }
  depends_on = [module.db]
}

module "db" {
  source = "terraform-aws-modules/rds/aws"
  engine                      = "postgres"
  engine_version              = "14.5"
  instance_class              = "db.m5.large"
  allocated_storage           = 5
  manage_master_user_password = false
  create_db_parameter_group   = false
  identifier                  = "postgres145destination"
  username                    = "${var.database_admin_username}"
  password                    = "${var.database_admin_password}"
  port                        = "5432"
  db_subnet_group_name        = "${var.environment}-db-main"
  vpc_security_group_ids      = [data.aws_security_group.selected_allow_ssh_postgresql.id]
  parameter_group_name        = aws_db_parameter_group.rds_pg14logical.name
  major_engine_version        = "14.5"
  
  tags = {
    Environment = "dev"
    Terraform   = "true"
  }
}

As before, the code will create RDS PostgreSQL with all the options for pg_logical. It will also execute the SQL template below:

SQL Template Destination

/* Clean up & Setup */
drop database ${database_name} with (FORCE);
create database ${database_name};
\c ${database_name}
create extension pglogical;
select pglogical.drop_node('subscriber', true);


/* Create node */
select pglogical.create_node(node_name := 'subscriber', dsn := 'host=${destination_endpoint} port=5432 user=${database_admin_username} dbname=${database_name} password=${database_admin_password}');

/* DR Setup */
select pglogical.drop_subscription('ddl_sql',true);
select pglogical.create_subscription(subscription_name := 'ddl_sql', provider_dsn := 'host=${source_endpoint} port=5432 dbname=${database_name} user=${database_admin_username} password=${database_admin_password}', replication_sets := array['ddl_sql'], synchronize_structure := true, synchronize_data := false);
select pglogical.create_subscription(subscription_name := 'b2b_subscription', provider_dsn := 'host=${source_endpoint} port=5432 dbname=${database_name} user=${database_admin_username} password=${database_admin_password}', replication_sets := array['b2b_subscription'], synchronize_structure := false, synchronize_data := true);

/* BI Setup */
select pglogical.drop_subscription('bi_subscription',true);
select pglogical.create_subscription(subscription_name := 'bi_subscription', provider_dsn := 'host=${source_endpoint} port=5432 dbname=${database_name} user=${database_admin_username} password=${database_admin_password}', replication_sets := array['bi_subscription'], synchronize_structure := false, synchronize_data := true);

Of course, that excludes all the meta configuration which you need to do for Terraform like: “provider.tf”, “terraform.tfvars” and “variables.tf”

That being said, as we need the security rules (ports, etc), here is extract from vars:

Variables

variable "region" {
  description = "Defines AWS region"
  type        = string
  default     = "eu-central-1"
}

variable "environment" {
  description = "Name of the environment"
  type        = string
}

variable "database_admin_password" {
  description = "The password for the database"
  type        = string

}

variable "database_admin_username"{
  description = "The username for the database"
  type        = string
}

variable "private_key_path" {
  description = "The path of the private key"
  type        = string
}

variable "public_key" {
  description = "Public key for the session"
  type        = string  
}

variable "ami" {
  description = "AMI for the EC2 image"
  type        = string
  default     = "ami-025d24108be0a614c"
}

variable "destination_sql_path" {
  description = "The path for the configuration of the source cluster"
  type        = string
}

variable "database_name" {
  description = "The name of the virtual database we will replicate"
  type        = string
}

variable "source_endpoint" {
  description = "The endpoint of the source database, provided manually for now"
  type        = string
}


variable "rules" {
  type = list(object({
    from_port   = number
    to_port     = number
    protocol    = string
    description = string
  }))
  default = [
    {
      from_port   = 22
      to_port     = 22
      protocol    = "tcp"
      description = "ssh access"
    },
    {
      from_port   = 5432
      to_port     = 5432
      protocol    = "tcp"
      description = "postgresql access"
    },
  ]
}

The “terraform.tfvars” are of course customized.

  • terraform_aurora_postgresql.txt
  • Last modified: 2023/09/10 15:05
  • by andonovj