From d806336734c27aa9d2ba83c6546c522b3cc650a3 Mon Sep 17 00:00:00 2001
From: Matthew Baggett <matthew@baggett.me>
Date: Thu, 26 Dec 2024 21:56:31 +0100
Subject: [PATCH] Now available in non-serverless flavour!

---
 cloud/aws/rds/aws.tf             |  5 +++
 cloud/aws/rds/debug.tf           | 61 ++++++++++++++++++++++++++++
 cloud/aws/rds/inputs.tf          | 68 ++++++++++++++++++++++++++++++++
 cloud/aws/rds/outputs.tf         | 16 ++++++++
 cloud/aws/rds/rds.certificate.tf | 13 ++++++
 cloud/aws/rds/rds.tf             | 68 ++++++++++++++++++++++++++++++++
 cloud/aws/rds/security-groups.tf | 24 +++++++++++
 cloud/aws/rds/subnets.tf         | 22 +++++++++++
 cloud/aws/rds/tenants.tf         | 12 ++++++
 cloud/aws/rds/terraform.tf       | 26 ++++++++++++
 10 files changed, 315 insertions(+)
 create mode 100644 cloud/aws/rds/aws.tf
 create mode 100644 cloud/aws/rds/debug.tf
 create mode 100644 cloud/aws/rds/inputs.tf
 create mode 100644 cloud/aws/rds/outputs.tf
 create mode 100644 cloud/aws/rds/rds.certificate.tf
 create mode 100644 cloud/aws/rds/rds.tf
 create mode 100644 cloud/aws/rds/security-groups.tf
 create mode 100644 cloud/aws/rds/subnets.tf
 create mode 100644 cloud/aws/rds/tenants.tf
 create mode 100644 cloud/aws/rds/terraform.tf

diff --git a/cloud/aws/rds/aws.tf b/cloud/aws/rds/aws.tf
new file mode 100644
index 0000000..3be5cfc
--- /dev/null
+++ b/cloud/aws/rds/aws.tf
@@ -0,0 +1,5 @@
+data "aws_region" "current" {}
+data "aws_caller_identity" "current" {}
+data "aws_vpc" "current" {
+  id = data.aws_security_group.source.vpc_id
+}
\ No newline at end of file
diff --git a/cloud/aws/rds/debug.tf b/cloud/aws/rds/debug.tf
new file mode 100644
index 0000000..b53e3cc
--- /dev/null
+++ b/cloud/aws/rds/debug.tf
@@ -0,0 +1,61 @@
+resource "local_file" "debug" {
+  content = nonsensitive(jsonencode({
+    rds = {
+      instance_name    = var.instance_name,
+      sanitised_name   = local.sanitised_name
+      tennants         = var.tenants,
+      application_arn  = try(var.application.arn, null),
+      application_name = try(var.application.name, null),
+      engine = {
+        requested = {
+          engine  = var.engine,
+          version = var.engine_version
+        }
+        resolved = {
+          engine  = data.aws_rds_engine_version.latest.engine,
+          version = data.aws_rds_engine_version.latest.version,
+          match   = data.aws_rds_engine_version.latest,
+        }
+      }
+      #endpoints = {
+      #  write = aws_rds_cluster_endpoint.endpoint["write"].endpoint,
+      #  read  = aws_rds_cluster_endpoint.endpoint["read"].endpoint
+      #}
+      admin = {
+        username = module.admin_identity.username
+        password = nonsensitive(module.admin_identity.password)
+      }
+    }
+    tenants = var.tenants
+  }))
+  filename        = "${path.root}/.debug/aws/rds/${var.instance_name}.provided.json"
+  file_permission = "0600"
+}
+resource "local_file" "debug_result" {
+  content = nonsensitive(jsonencode({
+    rds = {
+      instance_name    = var.instance_name,
+      tennants         = var.tenants,
+      application_arn  = try(var.application.arn, null),
+      application_name = try(var.application.name, null),
+      engine = {
+        requested = {
+          engine  = var.engine,
+          version = var.engine_version
+        }
+        resolved = {
+          engine  = data.aws_rds_engine_version.latest.engine,
+          version = data.aws_rds_engine_version.latest.version,
+          match   = data.aws_rds_engine_version.latest,
+        }
+      }
+      endpoints = aws_db_instance.instance.endpoint
+    }
+    tenants = merge({ admin = {
+      username = module.admin_identity.username
+      password = nonsensitive(module.admin_identity.password)
+    } }, local.output_tenants)
+  }))
+  filename        = "${path.root}/.debug/aws/rds/${var.instance_name}.result.json"
+  file_permission = "0600"
+}
\ No newline at end of file
diff --git a/cloud/aws/rds/inputs.tf b/cloud/aws/rds/inputs.tf
new file mode 100644
index 0000000..c41cc25
--- /dev/null
+++ b/cloud/aws/rds/inputs.tf
@@ -0,0 +1,68 @@
+variable "instance_name" {
+  type        = string
+  description = "The name of the RDS serverless instance"
+  default     = "serverless-multitennant"
+}
+locals {
+  sanitised_name = replace(replace(lower(var.instance_name), "[^a-z0-9A-Z-]", "-"), " ", "-")
+}
+variable "tenants" {
+  type = map(object({
+    username = string
+    database = string
+    active   = optional(bool, true)
+  }))
+  default = null
+}
+variable "application" {
+  description = "The AWS myApplication to be associated with this cluster"
+  type = object({
+    arn             = string
+    name            = string
+    description     = string
+    application_tag = map(string)
+  })
+  default = null
+}
+variable "engine" {
+  type        = string
+  description = "The database engine to use. This must be either aurora-mysql or aurora-postgresql"
+  default     = "mysql"
+  validation {
+    error_message = "Must be either ${join(" or ", local.supported_engines)}."
+    condition     = contains(local.supported_engines, var.engine)
+  }
+}
+locals {
+  supported_engines = ["mysql", "postgresql", "mariadb", ]
+  is_mysql          = var.engine == "mysql"
+  is_postgres       = var.engine == "postgresql"
+  is_mariadb        = var.engine == "mariadb"
+}
+variable "engine_version" {
+  type    = string
+  default = null
+}
+variable "backup_window" {
+  type        = string
+  description = "The daily time range during which automated backups are created if automated backups are enabled."
+  default     = "03:00-05:00"
+}
+variable "backup_retention_period_days" {
+  type    = number
+  default = 30
+  validation {
+    error_message = "backup_retention_period_days must be between 1 and 35."
+    condition     = var.backup_retention_period_days >= 1 && var.backup_retention_period_days <= 35
+  }
+}
+variable "skip_final_snapshot" {
+  type        = bool
+  description = "Determines whether a final DB snapshot is created before the DB cluster is deleted."
+  default     = false
+}
+variable "enable_performance_insights" {
+  type    = bool
+  default = false
+}
+
diff --git a/cloud/aws/rds/outputs.tf b/cloud/aws/rds/outputs.tf
new file mode 100644
index 0000000..2148d2a
--- /dev/null
+++ b/cloud/aws/rds/outputs.tf
@@ -0,0 +1,16 @@
+locals {
+  output_tenants = {
+    #for key, tenant in module.tenants : key => {
+    #  username          = tenant.username
+    #  database          = tenant.database
+    #  password          = tenant.password
+    #  connection_string = tenant.connection_string
+    #}
+  }
+}
+output "tenants" {
+  value = local.output_tenants
+}
+output "admin" {
+  value = module.admin_identity
+}
\ No newline at end of file
diff --git a/cloud/aws/rds/rds.certificate.tf b/cloud/aws/rds/rds.certificate.tf
new file mode 100644
index 0000000..35fb4f5
--- /dev/null
+++ b/cloud/aws/rds/rds.certificate.tf
@@ -0,0 +1,13 @@
+data "aws_rds_certificate" "default" {
+  id                = aws_db_instance.instance.ca_cert_identifier
+  latest_valid_till = true
+}
+data "http" "cert_data" {
+  url = "https://truststore.pki.rds.amazonaws.com/global/global-bundle.pem"
+}
+output "cert" {
+  value = data.aws_rds_certificate.default
+}
+output "cert_data" {
+  value = data.http.cert_data.response_body
+}
\ No newline at end of file
diff --git a/cloud/aws/rds/rds.tf b/cloud/aws/rds/rds.tf
new file mode 100644
index 0000000..e5cfd66
--- /dev/null
+++ b/cloud/aws/rds/rds.tf
@@ -0,0 +1,68 @@
+data "aws_rds_engine_version" "latest" {
+  engine  = var.engine
+  version = var.engine_version
+  latest  = true
+}
+resource "aws_kms_key" "db_key" {
+  description = "RDS ${var.instance_name} Encryption Key"
+  tags = merge(
+    try(var.application.application_tag, {}),
+    {
+      TerraformSecretType = "RDSMasterEncryptionKey"
+    }
+  )
+}
+variable "instance_class" {
+  type        = string
+  description = "The instance class to use for the RDS instance"
+  default     = "db.t4g.small"
+}
+module "admin_identity" {
+  source         = "../../../utils/identity"
+  username_words = 2
+}
+variable "allocated_storage_gb" {
+  type        = number
+  default     = 5
+  description = "The storage size for the RDS instance, measured in GB."
+}
+variable "max_allocated_storage_gb" {
+  type        = number
+  default     = 100
+  description = "The maximum storage size for the RDS instance, measured in GB."
+}
+resource "aws_db_instance" "instance" {
+  identifier_prefix       = "${local.sanitised_name}-"
+  instance_class          = var.instance_class
+  engine                  = data.aws_rds_engine_version.latest.engine
+  engine_version          = data.aws_rds_engine_version.latest.version
+  storage_encrypted       = true
+  kms_key_id              = aws_kms_key.db_key.arn
+  publicly_accessible     = false
+  apply_immediately       = true
+  db_subnet_group_name    = aws_db_subnet_group.sg.name
+  vpc_security_group_ids  = [aws_security_group.rds.id]
+  skip_final_snapshot     = var.skip_final_snapshot
+  username                = module.admin_identity.username
+  password                = module.admin_identity.password
+  backup_window           = var.backup_window
+  backup_retention_period = var.backup_retention_period_days
+  allocated_storage       = var.allocated_storage_gb
+  max_allocated_storage   = var.max_allocated_storage_gb
+
+  performance_insights_enabled          = var.enable_performance_insights
+  performance_insights_retention_period = var.enable_performance_insights ? 7 : null
+  performance_insights_kms_key_id       = var.enable_performance_insights ? aws_kms_key.db_key.arn : null
+
+  lifecycle {
+    create_before_destroy = false
+  }
+
+  tags = merge(
+    try(var.application.application_tag, {}),
+    {}
+  )
+}
+output "endpoints" {
+  value = aws_db_instance.instance.endpoint
+}
diff --git a/cloud/aws/rds/security-groups.tf b/cloud/aws/rds/security-groups.tf
new file mode 100644
index 0000000..4f58f7e
--- /dev/null
+++ b/cloud/aws/rds/security-groups.tf
@@ -0,0 +1,24 @@
+resource "aws_security_group" "rds" {
+  name        = join("-", [var.instance_name, "rds"])
+  description = "RDS Security Group for ${var.instance_name}"
+  vpc_id      = data.aws_vpc.current.id
+  tags = merge(
+    try(var.application.application_tag, {}),
+    {}
+  )
+}
+variable "source_security_group_id" {
+  type        = string
+  description = "The security group ID to allow access to the RDS instance"
+}
+data "aws_security_group" "source" {
+  id = var.source_security_group_id
+}
+resource "aws_security_group_rule" "sgr" {
+  security_group_id        = aws_security_group.rds.id
+  type                     = "ingress"
+  protocol                 = "tcp"
+  from_port                = var.engine == "aurora-postgres" ? 5432 : 3306
+  to_port                  = var.engine == "aurora-postgres" ? 5432 : 3306
+  source_security_group_id = var.source_security_group_id
+}
\ No newline at end of file
diff --git a/cloud/aws/rds/subnets.tf b/cloud/aws/rds/subnets.tf
new file mode 100644
index 0000000..08cc9f8
--- /dev/null
+++ b/cloud/aws/rds/subnets.tf
@@ -0,0 +1,22 @@
+variable "aws_subnets" {
+  description = "Pass an aws_subnets data object to the module"
+  type = object({
+    ids = list(string)
+  })
+}
+data "aws_subnets" "subnets" {
+  filter {
+    name   = "subnet-id"
+    values = var.aws_subnets.ids
+  }
+}
+resource "aws_db_subnet_group" "sg" {
+  name       = lower(join("-", [var.instance_name, "subnet-group"]))
+  subnet_ids = data.aws_subnets.subnets.ids
+  tags = merge(
+    try(var.application.application_tag, {}),
+    {
+      Name = "${var.instance_name} Subnet Group"
+    }
+  )
+}
\ No newline at end of file
diff --git a/cloud/aws/rds/tenants.tf b/cloud/aws/rds/tenants.tf
new file mode 100644
index 0000000..a773132
--- /dev/null
+++ b/cloud/aws/rds/tenants.tf
@@ -0,0 +1,12 @@
+/*module "tenants" {
+  depends_on     = [aws_db_instance.instance]
+  for_each       = var.tenants
+  source         = "./tenant"
+  username       = each.value.username
+  database       = each.value.database
+  vpc_id         = data.aws_vpc.current.id
+  cluster_id     = aws_rds_cluster.cluster.id
+  engine         = data.aws_rds_engine_version.latest.engine
+  admin_username = module.admin_identity.username
+  admin_password = module.admin_identity.password
+}*/
\ No newline at end of file
diff --git a/cloud/aws/rds/terraform.tf b/cloud/aws/rds/terraform.tf
new file mode 100644
index 0000000..287a7c2
--- /dev/null
+++ b/cloud/aws/rds/terraform.tf
@@ -0,0 +1,26 @@
+terraform {
+  required_version = "~> 1.6"
+
+  required_providers {
+    aws = {
+      source  = "hashicorp/aws"
+      version = "~> 5.0"
+    }
+    random = {
+      source  = "hashicorp/random"
+      version = "~> 3.0"
+    }
+    local = {
+      source  = "hashicorp/local"
+      version = "~> 2.0"
+    }
+    null = {
+      source  = "hashicorp/null"
+      version = "~> 3.0"
+    }
+    ssh = {
+      source  = "matthewbaggett/ssh"
+      version = "~> 0.1"
+    }
+  }
+}