diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..055049e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,40 @@
+# Local .terraform directories
+**/.terraform/*
+
+# .tfstate files
+*.tfstate
+*.tfstate.*
+
+# Crash log files
+crash.log
+crash.*.log
+
+# Exclude all .tfvars files, which are likely to contain sensitive data, such as
+# password, private keys, and other secrets. These should not be part of version
+# control as they are data points which are potentially sensitive and subject
+# to change depending on the environment.
+*.tfvars
+*.tfvars.json
+
+# Ignore override files as they are usually used to override resources locally and so
+# are not checked in
+override.tf
+override.tf.json
+*_override.tf
+*_override.tf.json
+
+# Include override files you do wish to add to version control using negated pattern
+# !example_override.tf
+
+# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
+# example: *tfplan*
+
+# Ignore CLI configuration files
+.terraformrc
+terraform.rc
+
+# Add vscode workspace file
+.vscode/*
+
+# Ignore test files
+**/.terraform.lock.hcl
\ No newline at end of file
diff --git a/README.md b/README.md
index fb92300..c8c9a97 100644
--- a/README.md
+++ b/README.md
@@ -10,7 +10,6 @@ No requirements.
| Name | Version |
|------|---------|
| [aws](#provider\_aws) | n/a |
-| [random](#provider\_random) | n/a |
## Modules
@@ -21,14 +20,14 @@ No modules.
| Name | Type |
|------|------|
| [aws_db_subnet_group.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/db_subnet_group) | resource |
+| [aws_iam_role.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource |
| [aws_rds_cluster.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_cluster) | resource |
| [aws_rds_cluster_instance.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_cluster_instance) | resource |
| [aws_secretsmanager_secret.connection_string](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret) | resource |
-| [aws_secretsmanager_secret.root_password](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret) | resource |
| [aws_secretsmanager_secret_version.connection_string](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret_version) | resource |
-| [aws_secretsmanager_secret_version.root_password](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/secretsmanager_secret_version) | resource |
| [aws_security_group.this](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource |
-| [random_password.password](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource |
+| [aws_iam_policy_document.rds_monitoring](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
+| [aws_secretsmanager_secret_version.root_password](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret_version) | data source |
| [aws_vpc.database_vpc](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/vpc) | data source |
## Inputs
@@ -37,12 +36,13 @@ No modules.
|------|-------------|------|---------|:--------:|
| [additional\_security\_groups](#input\_additional\_security\_groups) | Any additional security groups the cluster should be added to | `list(string)` | `[]` | no |
| [availability\_zones](#input\_availability\_zones) | Availability zones for the database | `list(string)` | n/a | yes |
-| [database\_name](#input\_database\_name) | Name of the default database to create | `string` | n/a | yes |
+| [database\_name](#input\_database\_name) | Name of the default database to create | `string` | `"main"` | no |
| [database\_subnets](#input\_database\_subnets) | Subnets for the database | `list(string)` | n/a | yes |
| [db\_cluster\_parameter\_group\_name](#input\_db\_cluster\_parameter\_group\_name) | parameter group | `string` | n/a | yes |
-| [instance\_class](#input\_instance\_class) | Instance class | `string` | n/a | yes |
+| [deletion\_protection](#input\_deletion\_protection) | Enable deletion protection. DO NOT DISABLE IN PRODUCTION, THIS IS ONLY FOR TESTING. | `bool` | `true` | no |
+| [instance\_class](#input\_instance\_class) | Instance class | `string` | `"db.t4g.medium"` | no |
| [instance\_count](#input\_instance\_count) | How many RDS instances to create | `number` | `1` | no |
-| [name](#input\_name) | Determines naming convention of assets. Generally follows DNS naming convention. | `string` | n/a | yes |
+| [name](#input\_name) | Determines naming convention of assets. Generally follows DNS naming convention. Service name or abbreviation. | `string` | n/a | yes |
| [tags](#input\_tags) | A mapping of tags to assign to the AWS resources. | `map(string)` | `{}` | no |
| [vpc\_id](#input\_vpc\_id) | The ID of the vpc the database belongs to | `string` | n/a | yes |
@@ -50,8 +50,9 @@ No modules.
| Name | Description |
|------|-------------|
-| [connection\_string\_arn](#output\_connection\_string\_arn) | n/a |
-| [db\_cluster\_id](#output\_db\_cluster\_id) | n/a |
-| [root\_password\_secret\_id](#output\_root\_password\_secret\_id) | n/a |
-| [security\_group\_id](#output\_security\_group\_id) | n/a |
+| [connection\_string\_arn](#output\_connection\_string\_arn) | The ARN of the secret that stores the connection string for the RDS cluster.
The secret stored inside is formatted as: postgresql://:@:/ |
+| [db\_cluster\_id](#output\_db\_cluster\_id) | The ID of the RDS cluster |
+| [root\_credentials](#output\_root\_credentials) | A map containing the username and password for the root user of the RDS cluster. Caution: This output will display the password in plain text. |
+| [root\_password\_id](#output\_root\_password\_id) | The ID of the secret that stores the root password for the RDS cluster |
+| [security\_group\_id](#output\_security\_group\_id) | The ID of the EC2 security group that controls access to the RDS cluster |
\ No newline at end of file
diff --git a/main.tf b/main.tf
index 8b508b0..584859e 100644
--- a/main.tf
+++ b/main.tf
@@ -1,4 +1,8 @@
resource "aws_rds_cluster" "this" {
+ # checkov:skip=CKV2_AWS_8: Using snapshots for backups
+ # checkov:skip=CKV2_AWS_27: Parameter group is passed in as a variable
+ # checkov:skip=CKV_AWS_327: We will use AWS managed keys because CMK are expensive and not necessary for our use case
+ # checkov:skip=CKV_AWS_162: IAM Authentication does not fit into our use cases
cluster_identifier_prefix = var.name
engine = "aurora-postgresql"
engine_version = "14.6"
@@ -6,7 +10,7 @@ resource "aws_rds_cluster" "this" {
skip_final_snapshot = false
final_snapshot_identifier = "${var.name}-final"
master_username = "root"
- master_password = aws_secretsmanager_secret_version.root_password.secret_string
+ manage_master_user_password = true
db_subnet_group_name = aws_db_subnet_group.this.name
storage_encrypted = true
availability_zones = var.availability_zones
@@ -15,47 +19,64 @@ resource "aws_rds_cluster" "this" {
vpc_security_group_ids = concat([aws_security_group.this.id], var.additional_security_groups)
tags = var.tags
db_cluster_parameter_group_name = var.db_cluster_parameter_group_name
- deletion_protection = true
-}
-
-resource "aws_secretsmanager_secret" "root_password" {
- name_prefix = "aurora-root-${var.name}"
- description = "Root password for the ${var.name} aurora cluster database"
- tags = var.tags
-}
-
-resource "aws_secretsmanager_secret_version" "root_password" {
- secret_id = aws_secretsmanager_secret.root_password.id
- secret_string = random_password.password.result
-}
+ deletion_protection = var.deletion_protection
+ copy_tags_to_snapshot = true
-resource "random_password" "password" {
- length = 32
- special = true
- min_special = 1
- override_special = "-._~" # URL-safe characters prevent parsing errors when using this password in a connection string
+ enabled_cloudwatch_logs_exports = [
+ "postgresql",
+ ]
}
resource "aws_secretsmanager_secret" "connection_string" {
+ # checkov:skip=CKV2_AWS_57: RDS connection strings cannot be rotated
+ # checkov:skip=CKV_AWS_149: We will use AWS managed keys because CMK are expensive and not necessary for our use case
name_prefix = "aurora-connectionstring-${var.name}"
description = "Connection String for the ${var.name} aurora cluster database"
tags = var.tags
}
+data "aws_secretsmanager_secret_version" "root_password" {
+ secret_id = aws_rds_cluster.this.master_user_secret[0].secret_arn
+}
+
resource "aws_secretsmanager_secret_version" "connection_string" {
secret_id = aws_secretsmanager_secret.connection_string.id
- secret_string = "postgresql://${aws_rds_cluster.this.master_username}:${aws_secretsmanager_secret_version.root_password.secret_string}@${aws_rds_cluster.this.endpoint}:${aws_rds_cluster.this.port}/${aws_rds_cluster.this.database_name}"
+ secret_string = "postgresql://${aws_rds_cluster.this.master_username}:${urlencode(jsondecode(data.aws_secretsmanager_secret_version.root_password.secret_string)["password"])}@${aws_rds_cluster.this.endpoint}:${aws_rds_cluster.this.port}/${aws_rds_cluster.this.database_name}"
+}
+
+data "aws_iam_policy_document" "rds_monitoring" {
+ statement {
+ actions = [
+ "sts:AssumeRole",
+ ]
+
+ principals {
+ type = "Service"
+ identifiers = ["monitoring.rds.amazonaws.com"]
+ }
+ }
+}
+
+resource "aws_iam_role" "this" {
+ name = "${var.name}-rds-monitoring-role"
+ assume_role_policy = data.aws_iam_policy_document.rds_monitoring.json
+ managed_policy_arns = ["arn:aws:iam::aws:policy/service-role/AmazonRDSEnhancedMonitoringRole"]
}
resource "aws_rds_cluster_instance" "this" {
- count = var.instance_count
- engine = "aurora-postgresql"
- engine_version = "14.6"
- identifier_prefix = "${var.name}-${count.index + 1}"
- cluster_identifier = aws_rds_cluster.this.id
- instance_class = var.instance_class
- db_subnet_group_name = aws_db_subnet_group.this.name
- tags = var.tags
+ # checkov:skip=CKV_AWS_354: We will use AWS managed keys because CMK are expensive and not necessary for our use case
+ count = var.instance_count
+ engine = "aurora-postgresql"
+ engine_version = "14.6"
+ identifier_prefix = "${var.name}-${count.index + 1}"
+ cluster_identifier = aws_rds_cluster.this.id
+ instance_class = var.instance_class
+ db_subnet_group_name = aws_db_subnet_group.this.name
+ tags = var.tags
+ auto_minor_version_upgrade = true
+ monitoring_interval = 5
+ monitoring_role_arn = aws_iam_role.this.arn
+ performance_insights_enabled = true
}
resource "aws_db_subnet_group" "this" {
@@ -78,29 +99,17 @@ resource "aws_security_group" "this" {
to_port = 5432
protocol = "tcp"
cidr_blocks = [data.aws_vpc.database_vpc.cidr_block]
+ description = "PostgreSQL traffic in"
}
egress {
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = [data.aws_vpc.database_vpc.cidr_block]
+ description = "PostgreSQL traffic out"
}
}
data "aws_vpc" "database_vpc" {
id = var.vpc_id
}
-
-output "db_cluster_id" {
- value = aws_rds_cluster.this.cluster_identifier
-}
-
-output "security_group_id" {
- value = aws_security_group.this.id
-}
-output "root_password_secret_id" {
- value = aws_secretsmanager_secret.root_password.id
-}
-output "connection_string_arn" {
- value = aws_secretsmanager_secret.connection_string.arn
-}
diff --git a/outputs.tf b/outputs.tf
new file mode 100644
index 0000000..3d3301d
--- /dev/null
+++ b/outputs.tf
@@ -0,0 +1,32 @@
+output "db_cluster_id" {
+ description = "The ID of the RDS cluster"
+ value = aws_rds_cluster.this.cluster_identifier
+}
+
+output "security_group_id" {
+ description = "The ID of the EC2 security group that controls access to the RDS cluster"
+ value = aws_security_group.this.id
+}
+
+output "root_password_id" {
+ description = "The ID of the secret that stores the root password for the RDS cluster"
+ value = data.aws_secretsmanager_secret_version.root_password.id
+}
+
+output "connection_string_arn" {
+ description = <:@:/
+EOT
+ value = aws_secretsmanager_secret.connection_string.arn
+}
+
+output "root_credentials" {
+ description = "A map containing the username and password for the root user of the RDS cluster. Caution: This output will display the password in plain text."
+ value = {
+ username = aws_rds_cluster.this.master_username
+ password = data.aws_secretsmanager_secret_version.root_password.secret_string
+ }
+
+ sensitive = true
+}
\ No newline at end of file
diff --git a/variables.tf b/variables.tf
index c31c86e..98524a1 100644
--- a/variables.tf
+++ b/variables.tf
@@ -1,11 +1,12 @@
variable "name" {
type = string
- description = "Determines naming convention of assets. Generally follows DNS naming convention."
+ description = "Determines naming convention of assets. Generally follows DNS naming convention. Service name or abbreviation."
}
variable "database_name" {
type = string
description = "Name of the default database to create"
+ default = "main"
}
variable "vpc_id" {
@@ -49,4 +50,11 @@ variable "db_cluster_parameter_group_name" {
variable "instance_class" {
type = string
description = "Instance class"
+ default = "db.t4g.medium"
}
+
+variable "deletion_protection" {
+ type = bool
+ description = "Enable deletion protection. DO NOT DISABLE IN PRODUCTION, THIS IS ONLY FOR TESTING."
+ default = true
+}
\ No newline at end of file