From 8a476b784886c7ef10976af008175f501ab0a7da Mon Sep 17 00:00:00 2001 From: Uzair Haroon Date: Fri, 12 Jun 2026 14:10:07 +0100 Subject: [PATCH 1/5] Created inital RDS module --- infrastructure/modules/rds/README.md | 151 ++++++++++ infrastructure/modules/rds/context.tf | 374 ++++++++++++++++++++++++ infrastructure/modules/rds/main.tf | 148 ++++++++++ infrastructure/modules/rds/outputs.tf | 82 ++++++ infrastructure/modules/rds/variables.tf | 282 ++++++++++++++++++ infrastructure/modules/rds/versions.tf | 10 + 6 files changed, 1047 insertions(+) create mode 100644 infrastructure/modules/rds/README.md create mode 100644 infrastructure/modules/rds/context.tf create mode 100644 infrastructure/modules/rds/main.tf create mode 100644 infrastructure/modules/rds/outputs.tf create mode 100644 infrastructure/modules/rds/variables.tf create mode 100644 infrastructure/modules/rds/versions.tf diff --git a/infrastructure/modules/rds/README.md b/infrastructure/modules/rds/README.md new file mode 100644 index 00000000..73ca0294 --- /dev/null +++ b/infrastructure/modules/rds/README.md @@ -0,0 +1,151 @@ +# RDS Module + +Thin NHS wrapper around [`terraform-aws-modules/rds/aws`](https://registry.terraform.io/modules/terraform-aws-modules/rds/aws/latest) (v7.2.0). + +The module provisions an RDS DB instance together with its subnet group, parameter group, option group, and (optionally) an Enhanced Monitoring IAM role. It also creates a security group unless the caller supplies their own via `vpc_security_group_ids`. + +## Fixed controls + +These values are always enforced and cannot be overridden by callers. + +| Control | Value | Reason | +|---------|-------|--------| +| `publicly_accessible` | `false` | Databases must never be internet-facing | +| `storage_encrypted` | `true` | Encryption at rest is mandatory | +| `copy_tags_to_snapshot` | `true` | Snapshots must carry the same tags as the instance | +| `auto_minor_version_upgrade` | `false` | Teams keep instances in sync with the production engine version | +| `create_db_subnet_group` | `true` | Subnet group is always managed by this module | + +## Usage + +### Oracle with a fresh database + +```hcl +module "oracle_rds" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws//infrastructure/modules/rds?ref=vX.Y.Z" + + # context + service = var.service + environment = var.environment + workspace = terraform.workspace + + # identity + identifier = "${var.name_prefix}-oracle-${var.environment}-${terraform.workspace}" + + # engine + engine = "oracle-ee" + engine_version = "19" + license_model = "license-included" + major_engine_version = "19" + family = "oracle-ee-19" + character_set_name = "AL32UTF8" + + # sizing + instance_class = "db.m5.large" + allocated_storage = 100 + storage_type = "gp3" + + # database + db_name = "MYDB" + username = var.rds_master_username + port = 1521 + + # credentials (write-only — not stored in state) + password_wo = var.rds_master_password + password_wo_version = 1 + + # networking + subnet_ids = data.aws_subnets.private.ids + vpc_id = data.aws_vpc.selected.id + pi_port = 1529 + pi_cidr_block = ["10.0.0.0/8"] + + # options (Oracle S3 integration and timezone) + options = [ + { + option_name = "S3_INTEGRATION" + version = "1.0" + port = 0 + vpc_security_group_memberships = [] + option_settings = [] + }, + { + option_name = "Timezone" + port = 0 + vpc_security_group_memberships = [] + option_settings = [ + { name = "TIME_ZONE", value = "Europe/London" } + ] + } + ] + + # parameters + parameters = [ + { name = "_add_col_optim_enabled", value = "TRUE", apply_method = "immediate" } + ] + + # availability and backup + multi_az = var.environment == "prod" ? true : false + backup_retention_period = 7 + skip_final_snapshot = false + deletion_protection = var.environment == "prod" ? true : false +} +``` + +### Restore from snapshot + +```hcl +module "oracle_rds" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws//infrastructure/modules/rds?ref=vX.Y.Z" + + # ... (same as above) + + # When restoring from a snapshot, character_set_name must be null + character_set_name = null + snapshot_identifier = "rds:my-db-2024-01-01-06-00" +} +``` + +### RDS-managed password (no password in Terraform state) + +```hcl +module "oracle_rds" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws//infrastructure/modules/rds?ref=vX.Y.Z" + + # ... + + manage_master_user_password = true + # password_wo is not required when manage_master_user_password = true +} +``` + +The master password ARN is exposed via the `master_user_secret_arn` output. + +## Outputs + +| Name | Description | +|------|-------------| +| `instance_address` | Hostname of the RDS instance (without port) | +| `instance_port` | Port number | +| `instance_endpoint` | Connection endpoint in `host:port` format | +| `instance_id` | RDS instance identifier | +| `instance_arn` | ARN of the RDS instance | +| `instance_resource_id` | RDS resource ID (used for IAM authentication) | +| `master_user_secret_arn` | Secrets Manager ARN for the master password (when `manage_master_user_password = true`) | +| `rds_security_group` | Full security group object (`.id`, `.arn`, etc.) — null when `vpc_security_group_ids` is provided | +| `security_group_id` | Security group ID — null when `vpc_security_group_ids` is provided | +| `rds_subnet_group` | Object with `.id` and `.arn` for the DB subnet group | +| `db_subnet_group_id` | DB subnet group name/ID | +| `db_parameter_group_id` | DB parameter group ID | +| `db_option_group_id` | DB option group ID | +| `enhanced_monitoring_iam_role_arn` | Enhanced Monitoring IAM role ARN (empty when `monitoring_interval = 0`) | + +## Migration from the local bcss `rds` module + +When migrating from `../../modules/rds` in the bcss repo to this shared module, note: + +1. **Output names** — `instance_endpoint`, `instance_address`, `instance_port`, and `rds_security_group` are compatible. `rds_subnet_group` is compatible (both expose an object with `.id`). +2. **`snapshot_identifier`** — The local module used `""` as "no snapshot". This module uses `null`. Update the calling stack. +3. **`password_wo`** — The local module accepted `master_password` as a plain variable (stored in state). This module uses `password_wo` (write-only, not persisted in state). +4. **`deletion_protection`** — Defaults to `true` here (defaults to whatever `var.deletion_protection` was in the local module). Add `#checkov:skip=CKV_AWS_293` to the module call in non-production stacks. +5. **`ignore_changes` lifecycle** — The local module ignored `engine`, `engine_version`, `availability_zone`, `db_subnet_group_name`, and `storage_encrypted` on the `aws_db_instance`. These lifecycle rules are inside the community module and cannot be overridden from a wrapper. Raise this as a known limitation in the migration PR. diff --git a/infrastructure/modules/rds/context.tf b/infrastructure/modules/rds/context.tf new file mode 100644 index 00000000..39d9b945 --- /dev/null +++ b/infrastructure/modules/rds/context.tf @@ -0,0 +1,374 @@ +# +# ONLY EDIT THIS FILE IN github.com/NHSDigital/screening-terraform-modules-aws/infrastructure/modules/tags +# All other instances of this file should be a copy of that one +# +# +# Copy this file from https://github.com/NHSDigital/screening-terraform-modules-aws/blob/master/infrastructure/modules/tags/exports/context.tf +# and then place it in your Terraform module to automatically get +# tag module standard configuration inputs suitable for passing +# to other modules. +# +# curl -sL https://raw.githubusercontent.com/NHSDigital/screening-terraform-modules-aws/master/infrastructure/modules/tags/exports/context.tf -o context.tf +# +# Modules should access the whole context as `module.this.context` +# to get the input variables with nulls for defaults, +# for example `context = module.this.context`, +# and access individual variables as `module.this.`, +# with final values filled in. +# +# For example, when using defaults, `module.this.context.delimiter` +# will be null, and `module.this.delimiter` will be `-` (hyphen). +# + +module "this" { + source = "../tags" + + service = var.service + project = var.project + region = var.region + environment = var.environment + stack = var.stack + workspace = var.workspace + name = var.name + delimiter = var.delimiter + attributes = var.attributes + tags = var.tags + additional_tag_map = var.additional_tag_map + label_order = var.label_order + regex_replace_chars = var.regex_replace_chars + id_length_limit = var.id_length_limit + label_key_case = var.label_key_case + label_value_case = var.label_value_case + terraform_source = coalesce(var.terraform_source, path.module) + descriptor_formats = var.descriptor_formats + labels_as_tags = var.labels_as_tags + + context = var.context +} + +# Copy contents of screening-terraform-modules-aws/tags/variables.tf here +# tflint-ignore: terraform_unused_declarations +variable "aws_region" { + type = string + description = "The AWS region" + default = "eu-west-2" + validation { + condition = contains(["eu-west-1", "eu-west-2", "us-east-1"], var.aws_region) + error_message = "AWS Region must be one of eu-west-1, eu-west-2, us-east-1" + } +} + +variable "context" { + type = any + default = { + enabled = true + service = null + project = null + region = null + environment = null + stack = null + workspace = null + name = null + delimiter = null + attributes = [] + tags = {} + additional_tag_map = {} + regex_replace_chars = null + label_order = [] + id_length_limit = null + label_key_case = null + label_value_case = null + terraform_source = null + descriptor_formats = {} + # Note: we have to use [] instead of null for unset lists due to + # https://github.com/hashicorp/terraform/issues/28137 + # which was not fixed until Terraform 1.0.0, + # but we want the default to be all the labels in `label_order` + # and we want users to be able to prevent all tag generation + # by setting `labels_as_tags` to `[]`, so we need + # a different sentinel to indicate "default" + labels_as_tags = ["unset"] + } + description = <<-EOT + Single object for setting entire context at once. + See description of individual variables for details. + Leave string and numeric variables as `null` to use default value. + Individual variable settings (non-null) override settings in context object, + except for attributes, tags, and additional_tag_map, which are merged. + EOT + + validation { + condition = lookup(var.context, "label_key_case", null) == null ? true : contains(["lower", "title", "upper"], var.context["label_key_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`." + } + + validation { + condition = lookup(var.context, "label_value_case", null) == null ? true : contains(["lower", "title", "upper", "none"], var.context["label_value_case"]) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "terraform_source" { + type = string + default = null + description = "Source location to record in the Terraform_source tag. Defaults to this module path." +} + +variable "enabled" { + type = bool + default = null + description = "Set to false to prevent the module from creating any resources" +} + +variable "service" { + type = string + default = null + description = "ID element. Usually an abbreviation of your service directorate name, e.g. 'bcss' or 'csms', to help ensure generated IDs are globally unique" +} + +variable "region" { + type = string + default = null + description = "ID element _(Rarely used, not included by default)_. Usually an abbreviation of the selected AWS region e.g. 'uw2', 'ew2' or 'gbl' for resources like IAM roles that have no region" +} + +variable "project" { + type = string + default = null + description = "ID element. A project identifier, indicating the name or role of the project the resource is for, such as `website` or `api`" +} +variable "stack" { + type = string + default = null + description = "ID element. The name of the stack/component, e.g. `database`, `web`, `waf`, `eks`" +} +variable "workspace" { + type = string + default = null + description = "ID element. The Terraform workspace, to help ensure generated IDs are unique across workspaces" +} +variable "environment" { + type = string + default = null + description = "ID element. Usually used to indicate role, e.g. 'prd', 'dev', 'test', 'preprod', 'prod', 'uat'" +} + +variable "name" { + type = string + default = null + description = <<-EOT + ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'. + This is the only ID element not also included as a `tag`. + The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. + EOT +} + +variable "delimiter" { + type = string + default = null + description = <<-EOT + Delimiter to be used between ID elements. + Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. + EOT +} + +variable "attributes" { + type = list(string) + default = [] + description = <<-EOT + ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`, + in the order they appear in the list. New attributes are appended to the + end of the list. The elements of the list are joined by the `delimiter` + and treated as a single ID element. + EOT +} + +variable "labels_as_tags" { + type = set(string) + default = ["default"] + description = <<-EOT + Set of labels (ID elements) to include as tags in the `tags` output. + Default is to include all labels. + Tags with empty values will not be included in the `tags` output. + Set to `[]` to suppress all generated tags. + **Notes:** + The value of the `name` tag, if included, will be the `id`, not the `name`. + Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be + changed in later chained modules. Attempts to change it will be silently ignored. + EOT +} + +variable "tags" { + type = map(string) + default = {} + description = <<-EOT + Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`). + Neither the tag keys nor the tag values will be modified by this module. + EOT +} + +variable "additional_tag_map" { + type = map(string) + default = {} + description = <<-EOT + Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`. + This is for some rare cases where resources want additional configuration of tags + and therefore take a list of maps with tag key, value, and additional configuration. + EOT +} + +variable "label_order" { + type = list(string) + default = null + description = <<-EOT + The order in which the labels (ID elements) appear in the `id`. + Defaults to ["namespace", "environment", "stage", "name", "attributes"]. + You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. + EOT +} + +variable "regex_replace_chars" { + type = string + default = null + description = <<-EOT + Terraform regular expression (regex) string. + Characters matching the regex will be removed from the ID elements. + If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. + EOT +} + +variable "id_length_limit" { + type = number + default = null + description = <<-EOT + Limit `id` to this many characters (minimum 6). + Set to `0` for unlimited length. + Set to `null` for keep the existing setting, which defaults to `0`. + Does not affect `id_full`. + EOT + validation { + condition = var.id_length_limit == null ? true : var.id_length_limit >= 6 || var.id_length_limit == 0 + error_message = "The id_length_limit must be >= 6 if supplied (not null), or 0 for unlimited length." + } +} + +variable "label_key_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of the `tags` keys (label names) for tags generated by this module. + Does not affect keys of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper`. + Default value: `title`. + EOT + + validation { + condition = var.label_key_case == null ? true : contains(["lower", "title", "upper"], var.label_key_case) + error_message = "Allowed values: `lower`, `title`, `upper`." + } +} + +variable "label_value_case" { + type = string + default = null + description = <<-EOT + Controls the letter case of ID elements (labels) as included in `id`, + set as tag values, and output by this module individually. + Does not affect values of tags passed in via the `tags` input. + Possible values: `lower`, `title`, `upper` and `none` (no transformation). + Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs. + Default value: `lower`. + EOT + + validation { + condition = var.label_value_case == null ? true : contains(["lower", "title", "upper", "none"], var.label_value_case) + error_message = "Allowed values: `lower`, `title`, `upper`, `none`." + } +} + +variable "descriptor_formats" { + type = any + default = {} + description = <<-EOT + Describe additional descriptors to be output in the `descriptors` output map. + Map of maps. Keys are names of descriptors. Values are maps of the form + `{ + format = string + labels = list(string) + }` + (Type is `any` so the map values can later be enhanced to provide additional options.) + `format` is a Terraform format string to be passed to the `format()` function. + `labels` is a list of labels, in order, to pass to `format()` function. + Label values will be normalized before being passed to `format()` so they will be + identical to how they appear in `id`. + Default is `{}` (`descriptors` output will be empty). + EOT +} + +variable "owner" { + type = string + description = "The name and or NHS.net email address of the service owner" + default = "None" +} + +variable "tag_version" { + type = string + description = "Used to identify the tagging version in use" + default = "1.0" +} + +variable "data_classification" { + type = string + description = "Used to identify the data classification of the resource, e.g 1-5" + default = "n/a" + validation { + condition = contains(["n/a", "1", "2", "3", "4", "5"], var.data_classification) + error_message = "Data Classification must be \"n/a\" or between 1-5" + } +} + +variable "data_type" { + type = string + description = "The tag data_type" + default = "None" + validation { + condition = contains(["None", "PCD", "PID", "Anonymised", "UserAccount", "Audit"], var.data_type) + error_message = "Data Type must be one of None, PCD, PID, Anonymised, UserAccount, Audit" + } +} + + +variable "public_facing" { + type = bool + description = "Whether this resource is public facing" + default = false +} + +variable "service_category" { + type = string + description = "The tag service_category" + default = "n/a" + validation { + condition = contains(["n/a", "Bronze", "Silver", "Gold", "Platinum"], var.service_category) + error_message = "The Service Category must be one of n/a, Bronze, Silver, Gold, Platinum" + } +} +variable "on_off_pattern" { + type = string + description = "Used to turn resources on and off based on a time pattern" + default = "n/a" +} + +variable "application_role" { + type = string + description = "The role the application is performing" + default = "General" +} + +variable "tool" { + type = string + description = "The tool used to deploy the resource" + default = "Terraform" +} + +#### End of copy of screening-terraform-modules-aws/tags/variables.tf diff --git a/infrastructure/modules/rds/main.tf b/infrastructure/modules/rds/main.tf new file mode 100644 index 00000000..d4af04c9 --- /dev/null +++ b/infrastructure/modules/rds/main.tf @@ -0,0 +1,148 @@ +data "aws_vpc" "selected" { + id = var.vpc_id +} + +locals { + create_security_group = length(var.vpc_security_group_ids) == 0 + effective_ingress_cidr_blocks = length(var.ingress_cidr_blocks) > 0 ? var.ingress_cidr_blocks : [data.aws_vpc.selected.cidr_block] +} + +# ---------------------------------------------------------------------------- +# Security group for the RDS instance. +# +# Only created when vpc_security_group_ids is not provided. The security group +# allows inbound traffic on the DB port (and optionally the Performance Insights +# agent port) from the VPC CIDR or caller-supplied CIDR blocks, and restricts +# outbound traffic to HTTPS only. +# ---------------------------------------------------------------------------- + +# tflint-ignore: terraform_required_providers +resource "aws_security_group" "this" { + # checkov:skip=CKV2_AWS_5: SG is attached to the RDS instance via vpc_security_group_ids in the community module below + count = local.create_security_group ? 1 : 0 + + name_prefix = "${module.this.id}-rds-" + description = "Allow VPC traffic to ${var.engine} RDS instance on port ${var.port}" + vpc_id = var.vpc_id + + ingress { + description = "DB port from VPC" + from_port = var.port + to_port = var.port + protocol = "tcp" + cidr_blocks = local.effective_ingress_cidr_blocks + } + + dynamic "ingress" { + for_each = var.pi_port != null ? [1] : [] + content { + description = "Performance Insights agent port" + from_port = var.pi_port + to_port = var.pi_port + protocol = "tcp" + cidr_blocks = length(var.pi_cidr_block) > 0 ? var.pi_cidr_block : local.effective_ingress_cidr_blocks + } + } + + egress { + description = "HTTPS egress for AWS service communication" + from_port = 443 + to_port = 443 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + ipv6_cidr_blocks = ["::/0"] + } + + tags = merge(module.this.tags, { Name = "${module.this.id}-rds" }) + + lifecycle { + create_before_destroy = true + } +} + +# ---------------------------------------------------------------------------- +# RDS instance +# +# Wraps terraform-aws-modules/rds/aws v7.2.0. +# +# Fixed controls (not exposed as variables): +# - publicly_accessible = false (databases must never be internet-facing) +# - storage_encrypted = true (encryption at rest is mandatory) +# - copy_tags_to_snapshot = true (snapshots must carry the same tags) +# - auto_minor_version_upgrade = false (teams keep instances in sync with prod) +# - create_db_subnet_group = true (subnet group always managed by this module) +# ---------------------------------------------------------------------------- +module "rds" { + source = "terraform-aws-modules/rds/aws" + version = "7.2.0" + + identifier = var.identifier + + # Engine + engine = var.engine + engine_version = var.engine_version + license_model = var.license_model + character_set_name = var.character_set_name + + # Instance sizing + instance_class = var.instance_class + allocated_storage = var.allocated_storage + max_allocated_storage = var.max_allocated_storage + storage_type = var.storage_type + iops = var.iops + storage_encrypted = true + kms_key_id = var.kms_key_id + + # Database + db_name = var.db_name + username = var.username + port = var.port + + # Credentials + manage_master_user_password = var.manage_master_user_password + password_wo = var.password_wo + password_wo_version = var.password_wo_version + + # Networking + publicly_accessible = false + vpc_security_group_ids = local.create_security_group ? [aws_security_group.this[0].id] : var.vpc_security_group_ids + + # Subnet group (always managed by this module) + create_db_subnet_group = true + subnet_ids = var.subnet_ids + + # Parameter group + family = var.family + parameters = var.parameters + + # Option group + major_engine_version = var.major_engine_version + options = var.options + + # Monitoring + monitoring_interval = var.monitoring_interval + create_monitoring_role = var.monitoring_interval > 0 + + # Availability and backup + multi_az = var.multi_az + backup_retention_period = var.backup_retention_period + backup_window = var.backup_window + maintenance_window = var.maintenance_window + skip_final_snapshot = var.skip_final_snapshot + snapshot_identifier = var.snapshot_identifier + apply_immediately = var.apply_immediately + copy_tags_to_snapshot = true + auto_minor_version_upgrade = false + + # Performance Insights + performance_insights_enabled = var.performance_insights_enabled + performance_insights_retention_period = var.performance_insights_enabled ? var.performance_insights_retention_period : null + performance_insights_kms_key_id = var.performance_insights_kms_key_id + + # Lifecycle + deletion_protection = var.deletion_protection + + timeouts = var.timeouts + + tags = module.this.tags +} diff --git a/infrastructure/modules/rds/outputs.tf b/infrastructure/modules/rds/outputs.tf new file mode 100644 index 00000000..60bf8117 --- /dev/null +++ b/infrastructure/modules/rds/outputs.tf @@ -0,0 +1,82 @@ +# Instance connection details + +output "instance_address" { + description = "Hostname of the RDS instance (without port)" + value = module.rds.db_instance_address +} + +output "instance_port" { + description = "Port on which the RDS instance accepts connections" + value = module.rds.db_instance_port +} + +output "instance_endpoint" { + description = "Connection endpoint for the RDS instance in host:port format" + value = module.rds.db_instance_endpoint +} + +output "instance_id" { + description = "Identifier of the RDS instance" + value = module.rds.db_instance_identifier +} + +output "instance_arn" { + description = "ARN of the RDS instance" + value = module.rds.db_instance_arn +} + +output "instance_resource_id" { + description = "The RDS resource ID (used for IAM authentication and tagging)" + value = module.rds.db_instance_resource_id +} + +output "master_user_secret_arn" { + description = "ARN of the Secrets Manager secret for the master user password. Only populated when manage_master_user_password is true" + value = module.rds.db_instance_master_user_secret_arn +} + +# Security group + +output "rds_security_group" { + description = "The security group created for the RDS instance. Null when vpc_security_group_ids was provided by the caller" + value = local.create_security_group ? aws_security_group.this[0] : null +} + +output "security_group_id" { + description = "ID of the RDS security group. Null when vpc_security_group_ids was provided by the caller" + value = local.create_security_group ? aws_security_group.this[0].id : null +} + +# Subnet group + +output "rds_subnet_group" { + description = "The DB subnet group used by the RDS instance, with id and arn attributes" + value = { + id = module.rds.db_subnet_group_id + arn = module.rds.db_subnet_group_arn + } +} + +output "db_subnet_group_id" { + description = "Name/ID of the DB subnet group" + value = module.rds.db_subnet_group_id +} + +# Parameter and option groups + +output "db_parameter_group_id" { + description = "ID of the DB parameter group" + value = module.rds.db_parameter_group_id +} + +output "db_option_group_id" { + description = "ID of the DB option group" + value = module.rds.db_option_group_id +} + +# Monitoring + +output "enhanced_monitoring_iam_role_arn" { + description = "ARN of the Enhanced Monitoring IAM role. Empty when monitoring_interval is 0" + value = module.rds.enhanced_monitoring_iam_role_arn +} diff --git a/infrastructure/modules/rds/variables.tf b/infrastructure/modules/rds/variables.tf new file mode 100644 index 00000000..e5bb8191 --- /dev/null +++ b/infrastructure/modules/rds/variables.tf @@ -0,0 +1,282 @@ +# ---------------------------------------------------------------------------- +# Instance identity +# ---------------------------------------------------------------------------- + +variable "identifier" { + description = "The name of the RDS instance" + type = string +} + +# ---------------------------------------------------------------------------- +# Engine +# ---------------------------------------------------------------------------- + +variable "engine" { + description = "The database engine to use (e.g. 'oracle-ee', 'postgres', 'mysql')" + type = string +} + +variable "engine_version" { + description = "The engine version to use" + type = string +} + +variable "license_model" { + description = "License model for the DB instance. Required for some engines (e.g. Oracle SE1 requires 'license-included')" + type = string + default = null +} + +variable "character_set_name" { + description = "Oracle character set name. Cannot be changed after creation. Must be null when restoring from a snapshot" + type = string + default = null +} + +# ---------------------------------------------------------------------------- +# Instance sizing +# ---------------------------------------------------------------------------- + +variable "instance_class" { + description = "The instance type of the RDS instance (e.g. 'db.m5.large')" + type = string +} + +variable "allocated_storage" { + description = "The allocated storage in gibibytes" + type = number +} + +variable "max_allocated_storage" { + description = "Upper limit for storage autoscaling in gibibytes. Set to 0 to disable autoscaling" + type = number + default = 0 +} + +variable "storage_type" { + description = "One of 'standard', 'gp2', 'gp3', 'io1', or 'io2'. Defaults to 'io1' when iops is set, otherwise 'gp2'" + type = string + default = null +} + +variable "iops" { + description = "Provisioned IOPS. Required when storage_type is 'io1' or 'io2'" + type = number + default = null +} + +variable "kms_key_id" { + description = "ARN of the KMS key for storage encryption. If omitted, the default account KMS key is used" + type = string + default = null +} + +# ---------------------------------------------------------------------------- +# Credentials +# ---------------------------------------------------------------------------- + +variable "username" { + description = "Username for the master DB user" + type = string + sensitive = true +} + +variable "password_wo" { + description = "Write-only password for the master DB user. Required when manage_master_user_password is false and snapshot_identifier is not set" + type = string + default = null + sensitive = true +} + +variable "password_wo_version" { + description = "Increment this value to trigger a password rotation when password_wo changes" + type = number + default = 1 +} + +variable "manage_master_user_password" { + description = "When true, RDS manages the master password in Secrets Manager. When false, password_wo must be provided" + type = bool + default = false +} + +# ---------------------------------------------------------------------------- +# Database +# ---------------------------------------------------------------------------- + +variable "db_name" { + description = "The name of the database to create. Omit to skip initial database creation" + type = string + default = null +} + +variable "port" { + description = "The port on which the DB accepts connections" + type = number +} + +# ---------------------------------------------------------------------------- +# Networking +# ---------------------------------------------------------------------------- + +variable "subnet_ids" { + description = "List of VPC subnet IDs for the DB subnet group" + type = list(string) +} + +variable "vpc_id" { + description = "VPC ID. Used to derive the VPC CIDR for security group ingress when ingress_cidr_blocks is not set" + type = string +} + +variable "vpc_security_group_ids" { + description = "List of existing security group IDs to associate with the instance. When provided, no security group is created by this module" + type = list(string) + default = [] +} + +variable "ingress_cidr_blocks" { + description = "CIDR blocks allowed to connect on the DB port. Defaults to the VPC CIDR when empty" + type = list(string) + default = [] +} + +variable "pi_port" { + description = "Performance Insights agent port. When set, an additional ingress rule is added to the security group" + type = number + default = null +} + +variable "pi_cidr_block" { + description = "CIDR blocks allowed to connect on the Performance Insights port. Defaults to ingress_cidr_blocks when empty" + type = list(string) + default = [] +} + +# ---------------------------------------------------------------------------- +# Parameter group +# ---------------------------------------------------------------------------- + +variable "family" { + description = "DB parameter group family (e.g. 'oracle-ee-19', 'postgres16', 'mysql8.0')" + type = string +} + +variable "parameters" { + description = "List of DB parameters to apply to the parameter group" + type = list(object({ + name = string + value = string + apply_method = optional(string) + })) + default = [] +} + +# ---------------------------------------------------------------------------- +# Option group +# ---------------------------------------------------------------------------- + +variable "major_engine_version" { + description = "Major engine version for the option group (e.g. '19' for Oracle 19c)" + type = string +} + +variable "options" { + description = "List of option group options to apply. See the community module documentation for the full object shape" + type = any + default = [] +} + +# ---------------------------------------------------------------------------- +# Availability and backup +# ---------------------------------------------------------------------------- + +variable "multi_az" { + description = "Specifies if the RDS instance is Multi-AZ" + type = bool + default = false +} + +variable "backup_retention_period" { + description = "Number of days to retain automated backups. Must be between 0 and 35" + type = number + default = 7 +} + +variable "backup_window" { + description = "Daily UTC time range for automated backups (e.g. '23:00-23:30'). Must not overlap with maintenance_window" + type = string + default = "23:00-23:30" +} + +variable "maintenance_window" { + description = "Weekly maintenance window (e.g. 'Sun:00:00-Sun:03:00')" + type = string + default = "Sun:00:00-Sun:03:00" +} + +variable "skip_final_snapshot" { + description = "If true, no final snapshot is created on deletion. Should be false in production" + type = bool + default = false +} + +variable "snapshot_identifier" { + description = "Snapshot ID to restore the instance from. When set, character_set_name must be null" + type = string + default = null +} + +variable "apply_immediately" { + description = "Apply modifications immediately rather than deferring to the next maintenance window" + type = bool + default = false +} + +# ---------------------------------------------------------------------------- +# Monitoring and performance +# ---------------------------------------------------------------------------- + +variable "monitoring_interval" { + description = "Interval in seconds between Enhanced Monitoring data points. Valid values: 0, 1, 5, 10, 15, 30, 60. Set to 0 to disable" + type = number + default = 5 +} + +variable "performance_insights_enabled" { + description = "Enable Performance Insights" + type = bool + default = true +} + +variable "performance_insights_retention_period" { + description = "Retention period for Performance Insights data in days. Valid values: 7, 731, or a multiple of 31" + type = number + default = 7 +} + +variable "performance_insights_kms_key_id" { + description = "ARN of the KMS key used to encrypt Performance Insights data. If omitted, the default KMS key is used" + type = string + default = null +} + +# ---------------------------------------------------------------------------- +# Lifecycle +# ---------------------------------------------------------------------------- + +variable "deletion_protection" { + description = "Prevents the DB instance from being deleted when true. Should be true in production" + type = bool + default = true +} + +variable "timeouts" { + description = "Terraform resource management timeouts for the DB instance" + type = object({ + create = optional(string) + update = optional(string) + delete = optional(string) + }) + default = null +} diff --git a/infrastructure/modules/rds/versions.tf b/infrastructure/modules/rds/versions.tf new file mode 100644 index 00000000..aaa5e443 --- /dev/null +++ b/infrastructure/modules/rds/versions.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">= 1.11.1" + + required_providers { + aws = { + source = "hashicorp/aws" + version = ">= 6.28" + } + } +} From 323f3e9d6d47b8964a847c2ddfd00f14b7341783 Mon Sep 17 00:00:00 2001 From: Uzair Haroon Date: Wed, 17 Jun 2026 17:04:38 +0100 Subject: [PATCH 2/5] Made PR improvements --- infrastructure/modules/rds/README.md | 12 ++--- infrastructure/modules/rds/main.tf | 64 +------------------------ infrastructure/modules/rds/outputs.tf | 12 ----- infrastructure/modules/rds/variables.tf | 25 +--------- 4 files changed, 6 insertions(+), 107 deletions(-) diff --git a/infrastructure/modules/rds/README.md b/infrastructure/modules/rds/README.md index 73ca0294..1c010544 100644 --- a/infrastructure/modules/rds/README.md +++ b/infrastructure/modules/rds/README.md @@ -2,7 +2,7 @@ Thin NHS wrapper around [`terraform-aws-modules/rds/aws`](https://registry.terraform.io/modules/terraform-aws-modules/rds/aws/latest) (v7.2.0). -The module provisions an RDS DB instance together with its subnet group, parameter group, option group, and (optionally) an Enhanced Monitoring IAM role. It also creates a security group unless the caller supplies their own via `vpc_security_group_ids`. +The module provisions an RDS DB instance together with its subnet group, parameter group, option group, and (optionally) an Enhanced Monitoring IAM role. The caller is responsible for creating a security group (use the dedicated security group module) and passing its ID via `vpc_security_group_ids`. ## Fixed controls @@ -55,10 +55,8 @@ module "oracle_rds" { password_wo_version = 1 # networking - subnet_ids = data.aws_subnets.private.ids - vpc_id = data.aws_vpc.selected.id - pi_port = 1529 - pi_cidr_block = ["10.0.0.0/8"] + subnet_ids = data.aws_subnets.private.ids + vpc_security_group_ids = [module.rds_security_group.security_group_id] # options (Oracle S3 integration and timezone) options = [ @@ -132,8 +130,6 @@ The master password ARN is exposed via the `master_user_secret_arn` output. | `instance_arn` | ARN of the RDS instance | | `instance_resource_id` | RDS resource ID (used for IAM authentication) | | `master_user_secret_arn` | Secrets Manager ARN for the master password (when `manage_master_user_password = true`) | -| `rds_security_group` | Full security group object (`.id`, `.arn`, etc.) — null when `vpc_security_group_ids` is provided | -| `security_group_id` | Security group ID — null when `vpc_security_group_ids` is provided | | `rds_subnet_group` | Object with `.id` and `.arn` for the DB subnet group | | `db_subnet_group_id` | DB subnet group name/ID | | `db_parameter_group_id` | DB parameter group ID | @@ -144,7 +140,7 @@ The master password ARN is exposed via the `master_user_secret_arn` output. When migrating from `../../modules/rds` in the bcss repo to this shared module, note: -1. **Output names** — `instance_endpoint`, `instance_address`, `instance_port`, and `rds_security_group` are compatible. `rds_subnet_group` is compatible (both expose an object with `.id`). +1. **Output names** — `instance_endpoint`, `instance_address`, `instance_port`, and `rds_subnet_group` are compatible with the local module. `rds_security_group` is no longer an output — the caller now owns the security group resource. 2. **`snapshot_identifier`** — The local module used `""` as "no snapshot". This module uses `null`. Update the calling stack. 3. **`password_wo`** — The local module accepted `master_password` as a plain variable (stored in state). This module uses `password_wo` (write-only, not persisted in state). 4. **`deletion_protection`** — Defaults to `true` here (defaults to whatever `var.deletion_protection` was in the local module). Add `#checkov:skip=CKV_AWS_293` to the module call in non-production stacks. diff --git a/infrastructure/modules/rds/main.tf b/infrastructure/modules/rds/main.tf index d4af04c9..45b45d53 100644 --- a/infrastructure/modules/rds/main.tf +++ b/infrastructure/modules/rds/main.tf @@ -1,65 +1,3 @@ -data "aws_vpc" "selected" { - id = var.vpc_id -} - -locals { - create_security_group = length(var.vpc_security_group_ids) == 0 - effective_ingress_cidr_blocks = length(var.ingress_cidr_blocks) > 0 ? var.ingress_cidr_blocks : [data.aws_vpc.selected.cidr_block] -} - -# ---------------------------------------------------------------------------- -# Security group for the RDS instance. -# -# Only created when vpc_security_group_ids is not provided. The security group -# allows inbound traffic on the DB port (and optionally the Performance Insights -# agent port) from the VPC CIDR or caller-supplied CIDR blocks, and restricts -# outbound traffic to HTTPS only. -# ---------------------------------------------------------------------------- - -# tflint-ignore: terraform_required_providers -resource "aws_security_group" "this" { - # checkov:skip=CKV2_AWS_5: SG is attached to the RDS instance via vpc_security_group_ids in the community module below - count = local.create_security_group ? 1 : 0 - - name_prefix = "${module.this.id}-rds-" - description = "Allow VPC traffic to ${var.engine} RDS instance on port ${var.port}" - vpc_id = var.vpc_id - - ingress { - description = "DB port from VPC" - from_port = var.port - to_port = var.port - protocol = "tcp" - cidr_blocks = local.effective_ingress_cidr_blocks - } - - dynamic "ingress" { - for_each = var.pi_port != null ? [1] : [] - content { - description = "Performance Insights agent port" - from_port = var.pi_port - to_port = var.pi_port - protocol = "tcp" - cidr_blocks = length(var.pi_cidr_block) > 0 ? var.pi_cidr_block : local.effective_ingress_cidr_blocks - } - } - - egress { - description = "HTTPS egress for AWS service communication" - from_port = 443 - to_port = 443 - protocol = "tcp" - cidr_blocks = ["0.0.0.0/0"] - ipv6_cidr_blocks = ["::/0"] - } - - tags = merge(module.this.tags, { Name = "${module.this.id}-rds" }) - - lifecycle { - create_before_destroy = true - } -} - # ---------------------------------------------------------------------------- # RDS instance # @@ -105,7 +43,7 @@ module "rds" { # Networking publicly_accessible = false - vpc_security_group_ids = local.create_security_group ? [aws_security_group.this[0].id] : var.vpc_security_group_ids + vpc_security_group_ids = var.vpc_security_group_ids # Subnet group (always managed by this module) create_db_subnet_group = true diff --git a/infrastructure/modules/rds/outputs.tf b/infrastructure/modules/rds/outputs.tf index 60bf8117..7d642c75 100644 --- a/infrastructure/modules/rds/outputs.tf +++ b/infrastructure/modules/rds/outputs.tf @@ -35,18 +35,6 @@ output "master_user_secret_arn" { value = module.rds.db_instance_master_user_secret_arn } -# Security group - -output "rds_security_group" { - description = "The security group created for the RDS instance. Null when vpc_security_group_ids was provided by the caller" - value = local.create_security_group ? aws_security_group.this[0] : null -} - -output "security_group_id" { - description = "ID of the RDS security group. Null when vpc_security_group_ids was provided by the caller" - value = local.create_security_group ? aws_security_group.this[0].id : null -} - # Subnet group output "rds_subnet_group" { diff --git a/infrastructure/modules/rds/variables.tf b/infrastructure/modules/rds/variables.tf index e5bb8191..d5fb751f 100644 --- a/infrastructure/modules/rds/variables.tf +++ b/infrastructure/modules/rds/variables.tf @@ -124,31 +124,8 @@ variable "subnet_ids" { type = list(string) } -variable "vpc_id" { - description = "VPC ID. Used to derive the VPC CIDR for security group ingress when ingress_cidr_blocks is not set" - type = string -} - variable "vpc_security_group_ids" { - description = "List of existing security group IDs to associate with the instance. When provided, no security group is created by this module" - type = list(string) - default = [] -} - -variable "ingress_cidr_blocks" { - description = "CIDR blocks allowed to connect on the DB port. Defaults to the VPC CIDR when empty" - type = list(string) - default = [] -} - -variable "pi_port" { - description = "Performance Insights agent port. When set, an additional ingress rule is added to the security group" - type = number - default = null -} - -variable "pi_cidr_block" { - description = "CIDR blocks allowed to connect on the Performance Insights port. Defaults to ingress_cidr_blocks when empty" + description = "List of security group IDs to associate with the instance. Create the security group using the dedicated security group module and pass its ID here" type = list(string) default = [] } From 198446951e768544634e831d9788c9e7f9fd9773 Mon Sep 17 00:00:00 2001 From: Oliver Slater Date: Fri, 19 Jun 2026 15:13:20 +0100 Subject: [PATCH 3/5] feat(rds): enhance module functionality and documentation - Added support for a new RDS module path in dependabot.yaml. - Created .terraform.lock.hcl for the new RDS module. - Updated README.md to include usage examples for PostgreSQL and Oracle RDS instances, along with security group and secrets manager integration. - Modified context.tf to enable module creation based on the 'enabled' variable. - Introduced locals.tf to define rds_identifier based on provided names. - Updated main.tf to conditionally create RDS resources based on the 'enabled' variable. - Enhanced variables.tf to include custom_name variable for explicit RDS instance naming. - Updated versions.tf to require Terraform version >= 1.13 and AWS provider version >= 6.42. --- .github/dependabot.yaml | 1 + .../modules/rds/.terraform.lock.hcl | 55 ++++ infrastructure/modules/rds/README.md | 305 ++++++++++++++++-- infrastructure/modules/rds/context.tf | 2 + infrastructure/modules/rds/locals.tf | 4 + infrastructure/modules/rds/main.tf | 12 +- infrastructure/modules/rds/variables.tf | 9 +- infrastructure/modules/rds/versions.tf | 4 +- 8 files changed, 356 insertions(+), 36 deletions(-) create mode 100644 infrastructure/modules/rds/.terraform.lock.hcl create mode 100644 infrastructure/modules/rds/locals.tf diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index ed8d5c8a..d3abac03 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -61,6 +61,7 @@ updates: - "infrastructure/modules/rds-gateway-ecs-task" - "infrastructure/modules/rds-instance" - "infrastructure/modules/rds-users" + - "infrastructure/modules/rds" - "infrastructure/modules/s3-bucket" - "infrastructure/modules/s3" - "infrastructure/modules/secrets-manager" diff --git a/infrastructure/modules/rds/.terraform.lock.hcl b/infrastructure/modules/rds/.terraform.lock.hcl new file mode 100644 index 00000000..61bf386e --- /dev/null +++ b/infrastructure/modules/rds/.terraform.lock.hcl @@ -0,0 +1,55 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "6.51.0" + constraints = ">= 6.14.0, >= 6.28.0, >= 6.42.0" + hashes = [ + "h1:017ISHZZBI+yeqA4AAtgLQJC7Lhd4wYM7tEKYmlk/7Y=", + "h1:4c8zjgtGH0QgP+p/cF1UqdqkvD7V5i0ZxqslieZLTbc=", + "h1:QWxF+1ePJ4qFCHEc6PyHNeXc865wLvrWVl71d/nABa8=", + "h1:aPBmqoiYqfrIgCGwzuemljkOXuGCYQRTXo91nQxrE+s=", + "h1:bclp+xS1fYeOCil0XZO6mKvEeHFESt5K/XotVSZND54=", + "zh:03fcea0a1ea2ca81d62d4d2e2961181bef9068b1c701f2cddc4aa5fac105818a", + "zh:1213944cd623143974ea5c9b70b22ae1ccca33d743924c149ed089d34b8e08b4", + "zh:190a46da0c69082b74da48238ce134d2fc9893e09122ac249c5689f88eab7e13", + "zh:1b312a4b53fa3cf731f95e674c033865feea5455f163b86136f2614424637293", + "zh:2b319814806222c5aba196b1a78756a6b36dc5c91f85edda349234d8a2f20a6a", + "zh:2bddf92c8efc6ad445a2eb8a0e5f88742a0596392c3a4ebc350ebb4105a4a96d", + "zh:3bef0c4f675c09034ff017cf899977b1765b2c0b3d1e489bcb06a5fcac316e2d", + "zh:47c46b5aa22199638fed5c93b195bbfd1182a1408edad4e5c39d4a73a04493f6", + "zh:5f808699650f6db961964466c77f5a581eab142a91c2e54810bb09b6f2fcd3f2", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:ada97e6be10164f452e278c23412b8597698a9c95ffb68fe83629d63d85906f3", + "zh:c4d73a91810d8dbcf9abbd431d41fcceebb48f8b6fd3c28a84bb3c6ed08be2e9", + "zh:c63ec875d38fc557b16b0b2b0ab1c7635852799453113240e21a52409de94a71", + "zh:cdd0209a755fc3aa14855aa013dae4b166a2fc7f6d3cbb673f7ff2142f5b63a2", + "zh:e5e665a27290391fd1bffc093ab68b596f6c507785be2e3f0949fab4fd6aec1b", + "zh:f6c42046a31d65eff2793737656b38931f90318b53661046bb84326cd4cb558f", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.9.0" + constraints = ">= 3.1.0" + hashes = [ + "h1:OO+IuvQJSPmWdN8AyyIEvPJbLvDQpgX/zbktoa9KsJE=", + "h1:UlBuNVuCGJ39tTv2c5gz2NRZnQbXfbIWbTzWcth5o74=", + "h1:lVDv+0AjDjrLfpmaJbWqUmIw/k3/AHXLc3N4m55SNdo=", + "h1:o0s5Mk9NXMP60nlheO1r0LsDGGratFb3oL0t7bD2QnM=", + "h1:q/uaUTBdKgAmZESrwsoeDQff9uUA/cI/N5ZKNgVwa9c=", + "zh:161ad0bd9a75768c82f53fb6e7172a9d8be2d4889b012645a34795031aaf1bf1", + "zh:19dc9a5b17729725ccfc4f45b0500af0ee5bc6b6b160c7adb8f2bf617d2c80ea", + "zh:269eda8fe42daa7974d5a34d166c3ba9defe80cde86c01e4dadcfdf2e1f05e5f", + "zh:373f7c65566f8f2cc7f45d698654feb9d988996957e1266a69ca00c52d6d16d0", + "zh:5599d16804c41c83009ec621b6d6b6f74e102f5827678a4750f8809055546b61", + "zh:583be0440469a22bff70dcfa56593b01566860b29607437264adb51060cf46fc", + "zh:5f211d8ec3f2e1f414870d9584bfe26e6995560ef81c748f8447a48164767398", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:7b547fd16216761ef86efc3ed516ac5ac0c5c42b7c7eb24a08cef2d93f69ed5e", + "zh:7e7c0679daf2a382151d05068c8c3f0dae6b7b7dccf818827b73dd08638df2ef", + "zh:8089dec888a8038b9b4fb23b3df7e1057293dbc5b60b42cc47ff690d69d4b61b", + "zh:c51f15a031edfd6f23ce8ced3446ca7f8d8d647e2499890d7d5d10d5016d7257", + "zh:c94784f005708890dc6895afd53636ec00ec1e430b15d41e5aebfb1d4b39bd04", + ] +} diff --git a/infrastructure/modules/rds/README.md b/infrastructure/modules/rds/README.md index 1c010544..437988f3 100644 --- a/infrastructure/modules/rds/README.md +++ b/infrastructure/modules/rds/README.md @@ -2,22 +2,53 @@ Thin NHS wrapper around [`terraform-aws-modules/rds/aws`](https://registry.terraform.io/modules/terraform-aws-modules/rds/aws/latest) (v7.2.0). -The module provisions an RDS DB instance together with its subnet group, parameter group, option group, and (optionally) an Enhanced Monitoring IAM role. The caller is responsible for creating a security group (use the dedicated security group module) and passing its ID via `vpc_security_group_ids`. +The module provisions an RDS DB instance together with its subnet group, parameter group, option group, and (optionally) an Enhanced Monitoring iam role. The caller is responsible for creating a security group (use the dedicated security group module) and passing its ID via `vpc_security_group_ids`. -## Fixed controls +## What this module enforces -These values are always enforced and cannot be overridden by callers. - -| Control | Value | Reason | -|---------|-------|--------| -| `publicly_accessible` | `false` | Databases must never be internet-facing | -| `storage_encrypted` | `true` | Encryption at rest is mandatory | -| `copy_tags_to_snapshot` | `true` | Snapshots must carry the same tags as the instance | -| `auto_minor_version_upgrade` | `false` | Teams keep instances in sync with the production engine version | -| `create_db_subnet_group` | `true` | Subnet group is always managed by this module | +|Control|Value|Reason| +|-|-|-| +|`publicly_accessible`|`false`|Databases must never be internet-facing| +|`storage_encrypted`|`true`|Encryption at rest is mandatory| +|`copy_tags_to_snapshot`|`true`|Snapshots must carry the same tags as the instance| +|`auto_minor_version_upgrade`|`false`|Teams keep instances in sync with the production engine version| +|`create_db_subnet_group`|`true`|subnet group is always managed by this module| +|Creation gate|`module.this.enabled`|Prevents all managed RDS resources when disabled| ## Usage +### Minimal PostgreSQL instance + +```hcl +module "postgres_rds" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws//infrastructure/modules/rds?ref=vX.Y.Z" + + service = "bcss" + project = "screening" + environment = "dev" + stack = "database" + workspace = terraform.workspace + + engine = "postgres" + engine_version = "16" + major_engine_version = "16" + family = "postgres16" + + instance_class = "db.t4g.medium" + allocated_storage = 100 + + db_name = "screening" + username = var.rds_master_username + port = 5432 + + password_wo = var.rds_master_password + password_wo_version = 1 + + subnet_ids = data.aws_subnets.private.ids + vpc_security_group_ids = [module.rds_security_group.security_group_id] +} +``` + ### Oracle with a fresh database ```hcl @@ -29,8 +60,9 @@ module "oracle_rds" { environment = var.environment workspace = terraform.workspace - # identity - identifier = "${var.name_prefix}-oracle-${var.environment}-${terraform.workspace}" + # identity (optional) + # If omitted, the module derives a name from context labels. + custom_name = "${var.name_prefix}-oracle-${var.environment}-${terraform.workspace}" # engine engine = "oracle-ee" @@ -117,24 +149,121 @@ module "oracle_rds" { } ``` -The master password ARN is exposed via the `master_user_secret_arn` output. +### With security-group and secrets-manager modules -## Outputs +This example shows the recommended pattern for production use: integrating with the dedicated security-group and secrets-manager modules. -| Name | Description | -|------|-------------| -| `instance_address` | Hostname of the RDS instance (without port) | -| `instance_port` | Port number | -| `instance_endpoint` | Connection endpoint in `host:port` format | -| `instance_id` | RDS instance identifier | -| `instance_arn` | ARN of the RDS instance | -| `instance_resource_id` | RDS resource ID (used for IAM authentication) | -| `master_user_secret_arn` | Secrets Manager ARN for the master password (when `manage_master_user_password = true`) | -| `rds_subnet_group` | Object with `.id` and `.arn` for the DB subnet group | -| `db_subnet_group_id` | DB subnet group name/ID | -| `db_parameter_group_id` | DB parameter group ID | -| `db_option_group_id` | DB option group ID | -| `enhanced_monitoring_iam_role_arn` | Enhanced Monitoring IAM role ARN (empty when `monitoring_interval = 0`) | +```hcl +# Create a security group using the NHS security-group wrapper module +module "rds_security_group" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws//infrastructure/modules/security-group?ref=vX.Y.Z"" + + service = "bcss" + project = "screening" + environment = var.environment + workspace = terraform.workspace + + vpc_id = data.aws_vpc.private.id + description = "Security group for RDS database" + + # Allow inbound on Oracle port from application security group + ingress_rules = [ + { + from_port = 1521 + to_port = 1521 + protocol = "tcp" + security_groups = [module.app_security_group.security_group_id] + description = "Oracle from application" + } + ] +} + +# Store the master password in Secrets Manager +module "rds_secret" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws//infrastructure/modules/secrets-manager?ref=vX.Y.Z" + + service = "bcss" + project = "screening" + environment = var.environment + workspace = terraform.workspace + name = "rds-master-password" + + secret_string = var.rds_master_password + # Optionally: managed rotation, encryption key, tags, etc. +} + +# Create the RDS instance with the security group and managed password +module "oracle_rds" { + source = "git::https://github.com/NHSDigital/screening-terraform-modules-aws//infrastructure/modules/rds?ref=vX.Y.Z" + + service = "bcss" + project = "screening" + environment = var.environment + workspace = terraform.workspace + stack = "database" + + engine = "oracle-ee" + engine_version = "19" + license_model = "license-included" + major_engine_version = "19" + family = "oracle-ee-19" + character_set_name = "AL32UTF8" + + instance_class = "db.m5.large" + allocated_storage = 500 + storage_type = "gp3" + + db_name = "MYDB" + username = var.rds_master_username + port = 1521 + + # Use RDS Secrets Manager integration for password management + manage_master_user_password = true + # The password is automatically stored in Secrets Manager by RDS + # Retrieve it via module output: module.oracle_rds.master_user_secret_arn + + subnet_ids = data.aws_subnets.private.ids + # Pass the security group created above + vpc_security_group_ids = [module.rds_security_group.security_group_id] + + multi_az = var.environment == "prod" + backup_retention_period = 7 + skip_final_snapshot = false + deletion_protection = var.environment == "prod" + + depends_on = [module.rds_security_group] +} + +# Output the secret arn for application connection strings +output "rds_secret_arn" { + value = module.rds_secret.secret_arn + description = "arn of the RDS master password secret" +} + +output "rds_endpoint" { + value = module.oracle_rds.instance_endpoint + description = "RDS instance endpoint (host:port)" +} +``` + +The master password arn is exposed via the `master_user_secret_arn` output. + +## Conventions + +- Naming and tagging come from shared `context.tf` via `module.this`. +- Identifier resolution order is `custom_name`, then `identifier`, then `module.this.id`. +- Security groups are intentionally caller-managed. This module associates IDs passed via `vpc_security_group_ids`. +- Resource creation is gated by `module.this.enabled`. +- Snapshot tagging is always enabled via `copy_tags_to_snapshot = true`. +- Resource arn values (e.g., `instance_arn`) are exposed as output attributes. +- iam authentication and resource tagging require the instance resource ID. + +## What this module does NOT do + +- Create or manage security groups. +- Allow public internet access to the database instance. +- Disable encryption at rest. +- Enable automatic minor engine upgrades. ## Migration from the local bcss `rds` module @@ -145,3 +274,121 @@ When migrating from `../../modules/rds` in the bcss repo to this shared module, 3. **`password_wo`** — The local module accepted `master_password` as a plain variable (stored in state). This module uses `password_wo` (write-only, not persisted in state). 4. **`deletion_protection`** — Defaults to `true` here (defaults to whatever `var.deletion_protection` was in the local module). Add `#checkov:skip=CKV_AWS_293` to the module call in non-production stacks. 5. **`ignore_changes` lifecycle** — The local module ignored `engine`, `engine_version`, `availability_zone`, `db_subnet_group_name`, and `storage_encrypted` on the `aws_db_instance`. These lifecycle rules are inside the community module and cannot be overridden from a wrapper. Raise this as a known limitation in the migration PR. + + + + +## Requirements + +| Name | Version | +| ---- | ------- | +| [terraform](#requirement\_terraform) | >= 1.13 | +| [aws](#requirement\_aws) | >= 6.42 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +| ---- | ------ | ------- | +| [rds](#module\_rds) | terraform-aws-modules/rds/aws | 7.2.0 | +| [this](#module\_this) | ../tags | n/a | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +| ---- | ----------- | ---- | ------- | :------: | +| [additional\_tag\_map](#input\_additional\_tag\_map) | Additional key-value pairs to add to each map in `tags_as_list_of_maps`. Not added to `tags` or `id`.
This is for some rare cases where resources want additional configuration of tags
and therefore take a list of maps with tag key, value, and additional configuration. | `map(string)` | `{}` | no | +| [allocated\_storage](#input\_allocated\_storage) | The allocated storage in gibibytes | `number` | n/a | yes | +| [application\_role](#input\_application\_role) | The role the application is performing | `string` | `"General"` | no | +| [apply\_immediately](#input\_apply\_immediately) | Apply modifications immediately rather than deferring to the next maintenance window | `bool` | `false` | no | +| [attributes](#input\_attributes) | ID element. Additional attributes (e.g. `workers` or `cluster`) to add to `id`,
in the order they appear in the list. New attributes are appended to the
end of the list. The elements of the list are joined by the `delimiter`
and treated as a single ID element. | `list(string)` | `[]` | no | +| [aws\_region](#input\_aws\_region) | The AWS region | `string` | `"eu-west-2"` | no | +| [backup\_retention\_period](#input\_backup\_retention\_period) | Number of days to retain automated backups. Must be between 0 and 35 | `number` | `7` | no | +| [backup\_window](#input\_backup\_window) | Daily UTC time range for automated backups (e.g. '23:00-23:30'). Must not overlap with maintenance\_window | `string` | `"23:00-23:30"` | no | +| [character\_set\_name](#input\_character\_set\_name) | Oracle character set name. Cannot be changed after creation. Must be null when restoring from a snapshot | `string` | `null` | no | +| [context](#input\_context) | Single object for setting entire context at once.
See description of individual variables for details.
Leave string and numeric variables as `null` to use default value.
Individual variable settings (non-null) override settings in context object,
except for attributes, tags, and additional\_tag\_map, which are merged. | `any` |
{
"additional_tag_map": {},
"attributes": [],
"delimiter": null,
"descriptor_formats": {},
"enabled": true,
"environment": null,
"id_length_limit": null,
"label_key_case": null,
"label_order": [],
"label_value_case": null,
"labels_as_tags": [
"unset"
],
"name": null,
"project": null,
"regex_replace_chars": null,
"region": null,
"service": null,
"stack": null,
"tags": {},
"terraform_source": null,
"workspace": null
}
| no | +| [custom\_name](#input\_custom\_name) | Optional override name for the RDS instance. Takes precedence over identifier when set. | `string` | `null` | no | +| [data\_classification](#input\_data\_classification) | Used to identify the data classification of the resource, e.g 1-5 | `string` | `"n/a"` | no | +| [data\_type](#input\_data\_type) | The tag data\_type | `string` | `"None"` | no | +| [db\_name](#input\_db\_name) | The name of the database to create. Omit to skip initial database creation | `string` | `null` | no | +| [deletion\_protection](#input\_deletion\_protection) | Prevents the DB instance from being deleted when true. Should be true in production | `bool` | `true` | no | +| [delimiter](#input\_delimiter) | Delimiter to be used between ID elements.
Defaults to `-` (hyphen). Set to `""` to use no delimiter at all. | `string` | `null` | no | +| [descriptor\_formats](#input\_descriptor\_formats) | Describe additional descriptors to be output in the `descriptors` output map.
Map of maps. Keys are names of descriptors. Values are maps of the form
`{
format = string
labels = list(string)
}`
(Type is `any` so the map values can later be enhanced to provide additional options.)
`format` is a Terraform format string to be passed to the `format()` function.
`labels` is a list of labels, in order, to pass to `format()` function.
Label values will be normalized before being passed to `format()` so they will be
identical to how they appear in `id`.
Default is `{}` (`descriptors` output will be empty). | `any` | `{}` | no | +| [enabled](#input\_enabled) | Set to false to prevent the module from creating any resources | `bool` | `null` | no | +| [engine](#input\_engine) | The database engine to use (e.g. 'oracle-ee', 'postgres', 'mysql') | `string` | n/a | yes | +| [engine\_version](#input\_engine\_version) | The engine version to use | `string` | n/a | yes | +| [environment](#input\_environment) | ID element. Usually used to indicate role, e.g. 'prd', 'dev', 'test', 'preprod', 'prod', 'uat' | `string` | `null` | no | +| [family](#input\_family) | DB parameter group family (e.g. 'oracle-ee-19', 'postgres16', 'mysql8.0') | `string` | n/a | yes | +| [id\_length\_limit](#input\_id\_length\_limit) | Limit `id` to this many characters (minimum 6).
Set to `0` for unlimited length.
Set to `null` for keep the existing setting, which defaults to `0`.
Does not affect `id_full`. | `number` | `null` | no | +| [identifier](#input\_identifier) | Explicit name for the RDS instance. If null, this module derives the name from context labels. | `string` | `null` | no | +| [instance\_class](#input\_instance\_class) | The instance type of the RDS instance (e.g. 'db.m5.large') | `string` | n/a | yes | +| [iops](#input\_iops) | Provisioned IOPS. Required when storage\_type is 'io1' or 'io2' | `number` | `null` | no | +| [kms\_key\_id](#input\_kms\_key\_id) | ARN of the KMS key for storage encryption. If omitted, the default account KMS key is used | `string` | `null` | no | +| [label\_key\_case](#input\_label\_key\_case) | Controls the letter case of the `tags` keys (label names) for tags generated by this module.
Does not affect keys of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper`.
Default value: `title`. | `string` | `null` | no | +| [label\_order](#input\_label\_order) | The order in which the labels (ID elements) appear in the `id`.
Defaults to ["namespace", "environment", "stage", "name", "attributes"].
You can omit any of the 6 labels ("tenant" is the 6th), but at least one must be present. | `list(string)` | `null` | no | +| [label\_value\_case](#input\_label\_value\_case) | Controls the letter case of ID elements (labels) as included in `id`,
set as tag values, and output by this module individually.
Does not affect values of tags passed in via the `tags` input.
Possible values: `lower`, `title`, `upper` and `none` (no transformation).
Set this to `title` and set `delimiter` to `""` to yield Pascal Case IDs.
Default value: `lower`. | `string` | `null` | no | +| [labels\_as\_tags](#input\_labels\_as\_tags) | Set of labels (ID elements) to include as tags in the `tags` output.
Default is to include all labels.
Tags with empty values will not be included in the `tags` output.
Set to `[]` to suppress all generated tags.
**Notes:**
The value of the `name` tag, if included, will be the `id`, not the `name`.
Unlike other `null-label` inputs, the initial setting of `labels_as_tags` cannot be
changed in later chained modules. Attempts to change it will be silently ignored. | `set(string)` |
[
"default"
]
| no | +| [license\_model](#input\_license\_model) | License model for the DB instance. Required for some engines (e.g. Oracle SE1 requires 'license-included') | `string` | `null` | no | +| [maintenance\_window](#input\_maintenance\_window) | Weekly maintenance window (e.g. 'Sun:00:00-Sun:03:00') | `string` | `"Sun:00:00-Sun:03:00"` | no | +| [major\_engine\_version](#input\_major\_engine\_version) | Major engine version for the option group (e.g. '19' for Oracle 19c) | `string` | n/a | yes | +| [manage\_master\_user\_password](#input\_manage\_master\_user\_password) | When true, RDS manages the master password in Secrets Manager. When false, password\_wo must be provided | `bool` | `false` | no | +| [max\_allocated\_storage](#input\_max\_allocated\_storage) | Upper limit for storage autoscaling in gibibytes. Set to 0 to disable autoscaling | `number` | `0` | no | +| [monitoring\_interval](#input\_monitoring\_interval) | Interval in seconds between Enhanced Monitoring data points. Valid values: 0, 1, 5, 10, 15, 30, 60. Set to 0 to disable | `number` | `5` | no | +| [multi\_az](#input\_multi\_az) | Specifies if the RDS instance is Multi-AZ | `bool` | `false` | no | +| [name](#input\_name) | ID element. Usually the component or solution name, e.g. 'app' or 'jenkins'.
This is the only ID element not also included as a `tag`.
The "name" tag is set to the full `id` string. There is no tag with the value of the `name` input. | `string` | `null` | no | +| [on\_off\_pattern](#input\_on\_off\_pattern) | Used to turn resources on and off based on a time pattern | `string` | `"n/a"` | no | +| [options](#input\_options) | List of option group options to apply. See the community module documentation for the full object shape | `any` | `[]` | no | +| [owner](#input\_owner) | The name and or NHS.net email address of the service owner | `string` | `"None"` | no | +| [parameters](#input\_parameters) | List of DB parameters to apply to the parameter group |
list(object({
name = string
value = string
apply_method = optional(string)
}))
| `[]` | no | +| [password\_wo](#input\_password\_wo) | Write-only password for the master DB user. Required when manage\_master\_user\_password is false and snapshot\_identifier is not set | `string` | `null` | no | +| [password\_wo\_version](#input\_password\_wo\_version) | Increment this value to trigger a password rotation when password\_wo changes | `number` | `1` | no | +| [performance\_insights\_enabled](#input\_performance\_insights\_enabled) | Enable Performance Insights | `bool` | `true` | no | +| [performance\_insights\_kms\_key\_id](#input\_performance\_insights\_kms\_key\_id) | ARN of the KMS key used to encrypt Performance Insights data. If omitted, the default KMS key is used | `string` | `null` | no | +| [performance\_insights\_retention\_period](#input\_performance\_insights\_retention\_period) | Retention period for Performance Insights data in days. Valid values: 7, 731, or a multiple of 31 | `number` | `7` | no | +| [port](#input\_port) | The port on which the DB accepts connections | `number` | n/a | yes | +| [project](#input\_project) | ID element. A project identifier, indicating the name or role of the project the resource is for, such as `website` or `api` | `string` | `null` | no | +| [public\_facing](#input\_public\_facing) | Whether this resource is public facing | `bool` | `false` | no | +| [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.
Characters matching the regex will be removed from the ID elements.
If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no | +| [region](#input\_region) | ID element \_(Rarely used, not included by default)\_. Usually an abbreviation of the selected AWS region e.g. 'uw2', 'ew2' or 'gbl' for resources like IAM roles that have no region | `string` | `null` | no | +| [service](#input\_service) | ID element. Usually an abbreviation of your service directorate name, e.g. 'bcss' or 'csms', to help ensure generated IDs are globally unique | `string` | `null` | no | +| [service\_category](#input\_service\_category) | The tag service\_category | `string` | `"n/a"` | no | +| [skip\_final\_snapshot](#input\_skip\_final\_snapshot) | If true, no final snapshot is created on deletion. Should be false in production | `bool` | `false` | no | +| [snapshot\_identifier](#input\_snapshot\_identifier) | Snapshot ID to restore the instance from. When set, character\_set\_name must be null | `string` | `null` | no | +| [stack](#input\_stack) | ID element. The name of the stack/component, e.g. `database`, `web`, `waf`, `eks` | `string` | `null` | no | +| [storage\_type](#input\_storage\_type) | One of 'standard', 'gp2', 'gp3', 'io1', or 'io2'. Defaults to 'io1' when iops is set, otherwise 'gp2' | `string` | `null` | no | +| [subnet\_ids](#input\_subnet\_ids) | List of VPC subnet IDs for the DB subnet group | `list(string)` | n/a | yes | +| [tag\_version](#input\_tag\_version) | Used to identify the tagging version in use | `string` | `"1.0"` | no | +| [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).
Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no | +| [terraform\_source](#input\_terraform\_source) | Source location to record in the Terraform\_source tag. Defaults to this module path. | `string` | `null` | no | +| [timeouts](#input\_timeouts) | Terraform resource management timeouts for the DB instance |
object({
create = optional(string)
update = optional(string)
delete = optional(string)
})
| `null` | no | +| [tool](#input\_tool) | The tool used to deploy the resource | `string` | `"Terraform"` | no | +| [username](#input\_username) | Username for the master DB user | `string` | n/a | yes | +| [vpc\_security\_group\_ids](#input\_vpc\_security\_group\_ids) | List of security group IDs to associate with the instance. Create the security group using the dedicated security group module and pass its ID here | `list(string)` | `[]` | no | +| [workspace](#input\_workspace) | ID element. The Terraform workspace, to help ensure generated IDs are unique across workspaces | `string` | `null` | no | + +## Outputs + +| Name | Description | +| ---- | ----------- | +| [db\_option\_group\_id](#output\_db\_option\_group\_id) | ID of the DB option group | +| [db\_parameter\_group\_id](#output\_db\_parameter\_group\_id) | ID of the DB parameter group | +| [db\_subnet\_group\_id](#output\_db\_subnet\_group\_id) | Name/ID of the DB subnet group | +| [enhanced\_monitoring\_iam\_role\_arn](#output\_enhanced\_monitoring\_iam\_role\_arn) | ARN of the Enhanced Monitoring IAM role. Empty when monitoring\_interval is 0 | +| [instance\_address](#output\_instance\_address) | Hostname of the RDS instance (without port) | +| [instance\_arn](#output\_instance\_arn) | ARN of the RDS instance | +| [instance\_endpoint](#output\_instance\_endpoint) | Connection endpoint for the RDS instance in host:port format | +| [instance\_id](#output\_instance\_id) | Identifier of the RDS instance | +| [instance\_port](#output\_instance\_port) | Port on which the RDS instance accepts connections | +| [instance\_resource\_id](#output\_instance\_resource\_id) | The RDS resource ID (used for IAM authentication and tagging) | +| [master\_user\_secret\_arn](#output\_master\_user\_secret\_arn) | ARN of the Secrets Manager secret for the master user password. Only populated when manage\_master\_user\_password is true | +| [rds\_subnet\_group](#output\_rds\_subnet\_group) | The DB subnet group used by the RDS instance, with id and arn attributes | + + + diff --git a/infrastructure/modules/rds/context.tf b/infrastructure/modules/rds/context.tf index 39d9b945..62befcb0 100644 --- a/infrastructure/modules/rds/context.tf +++ b/infrastructure/modules/rds/context.tf @@ -1,3 +1,4 @@ +# tflint-ignore-file: terraform_standard_module_structure, terraform_unused_declarations # # ONLY EDIT THIS FILE IN github.com/NHSDigital/screening-terraform-modules-aws/infrastructure/modules/tags # All other instances of this file should be a copy of that one @@ -23,6 +24,7 @@ module "this" { source = "../tags" + enabled = var.enabled service = var.service project = var.project region = var.region diff --git a/infrastructure/modules/rds/locals.tf b/infrastructure/modules/rds/locals.tf new file mode 100644 index 00000000..0f350376 --- /dev/null +++ b/infrastructure/modules/rds/locals.tf @@ -0,0 +1,4 @@ +locals { + # Prefer explicit caller names when provided, otherwise derive from context labels. + rds_identifier = coalesce(var.custom_name, var.identifier, module.this.id) +} diff --git a/infrastructure/modules/rds/main.tf b/infrastructure/modules/rds/main.tf index 45b45d53..32c8cfce 100644 --- a/infrastructure/modules/rds/main.tf +++ b/infrastructure/modules/rds/main.tf @@ -14,7 +14,12 @@ module "rds" { source = "terraform-aws-modules/rds/aws" version = "7.2.0" - identifier = var.identifier + create_db_instance = module.this.enabled + create_db_subnet_group = module.this.enabled + create_db_parameter_group = module.this.enabled + create_db_option_group = module.this.enabled + + identifier = local.rds_identifier # Engine engine = var.engine @@ -46,8 +51,7 @@ module "rds" { vpc_security_group_ids = var.vpc_security_group_ids # Subnet group (always managed by this module) - create_db_subnet_group = true - subnet_ids = var.subnet_ids + subnet_ids = var.subnet_ids # Parameter group family = var.family @@ -59,7 +63,7 @@ module "rds" { # Monitoring monitoring_interval = var.monitoring_interval - create_monitoring_role = var.monitoring_interval > 0 + create_monitoring_role = module.this.enabled && var.monitoring_interval > 0 # Availability and backup multi_az = var.multi_az diff --git a/infrastructure/modules/rds/variables.tf b/infrastructure/modules/rds/variables.tf index d5fb751f..83bee1b3 100644 --- a/infrastructure/modules/rds/variables.tf +++ b/infrastructure/modules/rds/variables.tf @@ -3,8 +3,15 @@ # ---------------------------------------------------------------------------- variable "identifier" { - description = "The name of the RDS instance" + description = "Explicit name for the RDS instance. If null, this module derives the name from context labels." type = string + default = null +} + +variable "custom_name" { + description = "Optional override name for the RDS instance. Takes precedence over identifier when set." + type = string + default = null } # ---------------------------------------------------------------------------- diff --git a/infrastructure/modules/rds/versions.tf b/infrastructure/modules/rds/versions.tf index aaa5e443..cb30fe5c 100644 --- a/infrastructure/modules/rds/versions.tf +++ b/infrastructure/modules/rds/versions.tf @@ -1,10 +1,10 @@ terraform { - required_version = ">= 1.11.1" + required_version = ">= 1.13" required_providers { aws = { source = "hashicorp/aws" - version = ">= 6.28" + version = ">= 6.42" } } } From 7011cafbee0e29fa93c3c50dcf6b56f5f871fc23 Mon Sep 17 00:00:00 2001 From: Uzair Haroon Date: Tue, 23 Jun 2026 17:26:51 +0100 Subject: [PATCH 4/5] Added doc format fixes --- infrastructure/modules/rds/README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/infrastructure/modules/rds/README.md b/infrastructure/modules/rds/README.md index 437988f3..0c3aed31 100644 --- a/infrastructure/modules/rds/README.md +++ b/infrastructure/modules/rds/README.md @@ -2,12 +2,12 @@ Thin NHS wrapper around [`terraform-aws-modules/rds/aws`](https://registry.terraform.io/modules/terraform-aws-modules/rds/aws/latest) (v7.2.0). -The module provisions an RDS DB instance together with its subnet group, parameter group, option group, and (optionally) an Enhanced Monitoring iam role. The caller is responsible for creating a security group (use the dedicated security group module) and passing its ID via `vpc_security_group_ids`. +The module provisions an RDS DB instance together with its subnet group, parameter group, option group, and (optionally) an Enhanced Monitoring IAM role. The caller is responsible for creating a security group (use the dedicated security group module) and passing its ID via `vpc_security_group_ids`. ## What this module enforces |Control|Value|Reason| -|-|-|-| +|---|---|---| |`publicly_accessible`|`false`|Databases must never be internet-facing| |`storage_encrypted`|`true`|Encryption at rest is mandatory| |`copy_tags_to_snapshot`|`true`|Snapshots must carry the same tags as the instance| @@ -256,7 +256,7 @@ The master password arn is exposed via the `master_user_secret_arn` output. - Resource creation is gated by `module.this.enabled`. - Snapshot tagging is always enabled via `copy_tags_to_snapshot = true`. - Resource arn values (e.g., `instance_arn`) are exposed as output attributes. -- iam authentication and resource tagging require the instance resource ID. +- IAM authentication and resource tagging require the instance resource ID. ## What this module does NOT do From d1fba95df7ae64ae6ebccdb4737f83425810554b Mon Sep 17 00:00:00 2001 From: Oliver Slater Date: Wed, 24 Jun 2026 11:41:08 +0100 Subject: [PATCH 5/5] docs(README.md): update README and module metadata for rds --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 4ff20c4a..4ec23177 100644 --- a/README.md +++ b/README.md @@ -323,6 +323,7 @@ Rules: | `parameter_store` | — | SSM Parameter Store configuration | | `r53` | terraform-aws-modules/route53/aws | Route 53 DNS Zones, Records, Resolver and Resolver Firewall | | `r53-healthcheck` | — | Route 53 health checks | +| `rds` | terraform-aws-modules/rds/aws | RDS database instance | | `rds-database` | — | RDS database (logical) | | `rds-gateway-ecs-task` | — | RDS gateway ECS task definition | | `rds-instance` | — | RDS instance |