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