Skip to content

Commit

Permalink
feat: support reserved instances (#233)
Browse files Browse the repository at this point in the history
* feat: support reserved instances

* fmt+readme

* add output

* ternary fix
  • Loading branch information
kevcube authored Sep 12, 2024
1 parent 71f0000 commit 9d1ecd6
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 1 deletion.
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ Available targets:
| [aws_rds_cluster_activity_stream.primary](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_cluster_activity_stream) | resource |
| [aws_rds_cluster_instance.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_cluster_instance) | resource |
| [aws_rds_cluster_parameter_group.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_cluster_parameter_group) | resource |
| [aws_rds_reserved_instance.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_reserved_instance) | resource |
| [aws_security_group.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource |
| [aws_security_group_rule.egress](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource |
| [aws_security_group_rule.egress_ipv6](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource |
Expand All @@ -364,6 +365,7 @@ Available targets:
| [random_pet.instance](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/pet) | resource |
| [aws_iam_policy_document.enhanced_monitoring](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source |
| [aws_rds_reserved_instance_offering.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/rds_reserved_instance_offering) | data source |

## Inputs

Expand Down Expand Up @@ -445,6 +447,8 @@ Available targets:
| <a name="input_publicly_accessible"></a> [publicly\_accessible](#input\_publicly\_accessible) | Set to true if you want your cluster to be publicly accessible (such as via QuickSight) | `bool` | `false` | no |
| <a name="input_rds_monitoring_interval"></a> [rds\_monitoring\_interval](#input\_rds\_monitoring\_interval) | The interval, in seconds, between points when enhanced monitoring metrics are collected for the DB instance. To disable collecting Enhanced Monitoring metrics, specify 0. The default is 0. Valid Values: 0, 1, 5, 10, 15, 30, 60 | `number` | `0` | no |
| <a name="input_rds_monitoring_role_arn"></a> [rds\_monitoring\_role\_arn](#input\_rds\_monitoring\_role\_arn) | The ARN for the IAM role that permits RDS to send enhanced monitoring metrics to CloudWatch Logs | `string` | `null` | no |
| <a name="input_rds_ri_duration"></a> [rds\_ri\_duration](#input\_rds\_ri\_duration) | The number of years to reserve the instance. Values can be 1 or 3 (or in seconds, 31536000 or 94608000) | `number` | `1` | no |
| <a name="input_rds_ri_offering_type"></a> [rds\_ri\_offering\_type](#input\_rds\_ri\_offering\_type) | Offering type of reserved DB instances. Valid values are 'No Upfront', 'Partial Upfront', 'All Upfront'. | `string` | `""` | no |
| <a name="input_reader_dns_name"></a> [reader\_dns\_name](#input\_reader\_dns\_name) | Name of the reader endpoint CNAME record to create in the parent DNS zone specified by `zone_id`. If left empty, the name will be auto-asigned using the format `replicas.var.name` | `string` | `""` | no |
| <a name="input_regex_replace_chars"></a> [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.<br>Characters matching the regex will be removed from the ID elements.<br>If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no |
| <a name="input_replication_source_identifier"></a> [replication\_source\_identifier](#input\_replication\_source\_identifier) | ARN of a source DB cluster or DB instance if this DB cluster is to be created as a Read Replica | `string` | `""` | no |
Expand All @@ -465,6 +469,7 @@ Available targets:
| <a name="input_tags"></a> [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).<br>Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no |
| <a name="input_tenant"></a> [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no |
| <a name="input_timeouts_configuration"></a> [timeouts\_configuration](#input\_timeouts\_configuration) | List of timeout values per action. Only valid actions are `create`, `update` and `delete` | <pre>list(object({<br> create = string<br> update = string<br> delete = string<br> }))</pre> | `[]` | no |
| <a name="input_use_reserved_instances"></a> [use\_reserved\_instances](#input\_use\_reserved\_instances) | WARNING: Observe your plans and applies carefully when using this feature.<br>It has potential to be very expensive if not used correctly.<br><br>Whether to use reserved instances. | `bool` | `false` | no |
| <a name="input_vpc_id"></a> [vpc\_id](#input\_vpc\_id) | VPC ID to create the cluster in (e.g. `vpc-a22222ee`) | `string` | n/a | yes |
| <a name="input_vpc_security_group_ids"></a> [vpc\_security\_group\_ids](#input\_vpc\_security\_group\_ids) | Additional security group IDs to apply to the cluster, in addition to the provisioned default security group with ingress traffic from existing CIDR blocks and existing security groups | `list(string)` | `[]` | no |
| <a name="input_zone_id"></a> [zone\_id](#input\_zone\_id) | Route53 DNS Zone ID as list of string (0 or 1 items). If empty, no custom DNS name will be published.<br>If the list contains a single Zone ID, a custom DNS name will be pulished in that zone.<br>Can also be a plain string, but that use is DEPRECATED because of Terraform issues. | `any` | `[]` | no |
Expand All @@ -486,6 +491,7 @@ Available targets:
| <a name="output_master_username"></a> [master\_username](#output\_master\_username) | Username for the master DB user |
| <a name="output_reader_endpoint"></a> [reader\_endpoint](#output\_reader\_endpoint) | A read-only endpoint for the Aurora cluster, automatically load-balanced across replicas |
| <a name="output_replicas_host"></a> [replicas\_host](#output\_replicas\_host) | Replicas hostname |
| <a name="output_reserved_instance"></a> [reserved\_instance](#output\_reserved\_instance) | All information about the reserved instance(s) if created. |
| <a name="output_security_group_arn"></a> [security\_group\_arn](#output\_security\_group\_arn) | Security Group ARN |
| <a name="output_security_group_id"></a> [security\_group\_id](#output\_security\_group\_id) | Security Group ID |
| <a name="output_security_group_name"></a> [security\_group\_name](#output\_security\_group\_name) | Security Group name |
Expand Down
6 changes: 6 additions & 0 deletions docs/terraform.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
| [aws_rds_cluster_activity_stream.primary](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_cluster_activity_stream) | resource |
| [aws_rds_cluster_instance.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_cluster_instance) | resource |
| [aws_rds_cluster_parameter_group.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_cluster_parameter_group) | resource |
| [aws_rds_reserved_instance.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/rds_reserved_instance) | resource |
| [aws_security_group.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group) | resource |
| [aws_security_group_rule.egress](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource |
| [aws_security_group_rule.egress_ipv6](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/security_group_rule) | resource |
Expand All @@ -50,6 +51,7 @@
| [random_pet.instance](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/pet) | resource |
| [aws_iam_policy_document.enhanced_monitoring](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source |
| [aws_partition.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/partition) | data source |
| [aws_rds_reserved_instance_offering.default](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/rds_reserved_instance_offering) | data source |

## Inputs

Expand Down Expand Up @@ -131,6 +133,8 @@
| <a name="input_publicly_accessible"></a> [publicly\_accessible](#input\_publicly\_accessible) | Set to true if you want your cluster to be publicly accessible (such as via QuickSight) | `bool` | `false` | no |
| <a name="input_rds_monitoring_interval"></a> [rds\_monitoring\_interval](#input\_rds\_monitoring\_interval) | The interval, in seconds, between points when enhanced monitoring metrics are collected for the DB instance. To disable collecting Enhanced Monitoring metrics, specify 0. The default is 0. Valid Values: 0, 1, 5, 10, 15, 30, 60 | `number` | `0` | no |
| <a name="input_rds_monitoring_role_arn"></a> [rds\_monitoring\_role\_arn](#input\_rds\_monitoring\_role\_arn) | The ARN for the IAM role that permits RDS to send enhanced monitoring metrics to CloudWatch Logs | `string` | `null` | no |
| <a name="input_rds_ri_duration"></a> [rds\_ri\_duration](#input\_rds\_ri\_duration) | The number of years to reserve the instance. Values can be 1 or 3 (or in seconds, 31536000 or 94608000) | `number` | `1` | no |
| <a name="input_rds_ri_offering_type"></a> [rds\_ri\_offering\_type](#input\_rds\_ri\_offering\_type) | Offering type of reserved DB instances. Valid values are 'No Upfront', 'Partial Upfront', 'All Upfront'. | `string` | `""` | no |
| <a name="input_reader_dns_name"></a> [reader\_dns\_name](#input\_reader\_dns\_name) | Name of the reader endpoint CNAME record to create in the parent DNS zone specified by `zone_id`. If left empty, the name will be auto-asigned using the format `replicas.var.name` | `string` | `""` | no |
| <a name="input_regex_replace_chars"></a> [regex\_replace\_chars](#input\_regex\_replace\_chars) | Terraform regular expression (regex) string.<br>Characters matching the regex will be removed from the ID elements.<br>If not set, `"/[^a-zA-Z0-9-]/"` is used to remove all characters other than hyphens, letters and digits. | `string` | `null` | no |
| <a name="input_replication_source_identifier"></a> [replication\_source\_identifier](#input\_replication\_source\_identifier) | ARN of a source DB cluster or DB instance if this DB cluster is to be created as a Read Replica | `string` | `""` | no |
Expand All @@ -151,6 +155,7 @@
| <a name="input_tags"></a> [tags](#input\_tags) | Additional tags (e.g. `{'BusinessUnit': 'XYZ'}`).<br>Neither the tag keys nor the tag values will be modified by this module. | `map(string)` | `{}` | no |
| <a name="input_tenant"></a> [tenant](#input\_tenant) | ID element \_(Rarely used, not included by default)\_. A customer identifier, indicating who this instance of a resource is for | `string` | `null` | no |
| <a name="input_timeouts_configuration"></a> [timeouts\_configuration](#input\_timeouts\_configuration) | List of timeout values per action. Only valid actions are `create`, `update` and `delete` | <pre>list(object({<br> create = string<br> update = string<br> delete = string<br> }))</pre> | `[]` | no |
| <a name="input_use_reserved_instances"></a> [use\_reserved\_instances](#input\_use\_reserved\_instances) | WARNING: Observe your plans and applies carefully when using this feature.<br>It has potential to be very expensive if not used correctly.<br><br>Whether to use reserved instances. | `bool` | `false` | no |
| <a name="input_vpc_id"></a> [vpc\_id](#input\_vpc\_id) | VPC ID to create the cluster in (e.g. `vpc-a22222ee`) | `string` | n/a | yes |
| <a name="input_vpc_security_group_ids"></a> [vpc\_security\_group\_ids](#input\_vpc\_security\_group\_ids) | Additional security group IDs to apply to the cluster, in addition to the provisioned default security group with ingress traffic from existing CIDR blocks and existing security groups | `list(string)` | `[]` | no |
| <a name="input_zone_id"></a> [zone\_id](#input\_zone\_id) | Route53 DNS Zone ID as list of string (0 or 1 items). If empty, no custom DNS name will be published.<br>If the list contains a single Zone ID, a custom DNS name will be pulished in that zone.<br>Can also be a plain string, but that use is DEPRECATED because of Terraform issues. | `any` | `[]` | no |
Expand All @@ -172,6 +177,7 @@
| <a name="output_master_username"></a> [master\_username](#output\_master\_username) | Username for the master DB user |
| <a name="output_reader_endpoint"></a> [reader\_endpoint](#output\_reader\_endpoint) | A read-only endpoint for the Aurora cluster, automatically load-balanced across replicas |
| <a name="output_replicas_host"></a> [replicas\_host](#output\_replicas\_host) | Replicas hostname |
| <a name="output_reserved_instance"></a> [reserved\_instance](#output\_reserved\_instance) | All information about the reserved instance(s) if created. |
| <a name="output_security_group_arn"></a> [security\_group\_arn](#output\_security\_group\_arn) | Security Group ARN |
| <a name="output_security_group_id"></a> [security\_group\_id](#output\_security\_group\_id) | Security Group ID |
| <a name="output_security_group_name"></a> [security\_group\_name](#output\_security\_group\_name) | Security Group name |
Expand Down
24 changes: 24 additions & 0 deletions main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ locals {
is_regional_cluster = var.cluster_type == "regional"
is_serverless = var.engine_mode == "serverless"
ignore_admin_credentials = var.replication_source_identifier != "" || var.snapshot_identifier != null
reserved_instance_engine = split("-", var.engine)[1]
use_reserved_instances = var.use_reserved_instances && !local.is_serverless && contains(["mysql", "postgresql"], local.reserved_instance_engine)
}

data "aws_partition" "current" {
Expand Down Expand Up @@ -88,6 +90,28 @@ resource "aws_security_group_rule" "egress_ipv6" {
security_group_id = join("", aws_security_group.default[*].id)
}

data "aws_rds_reserved_instance_offering" "default" {
count = local.use_reserved_instances ? 1 : 0
db_instance_class = var.db_cluster_instance_class
duration = var.rds_ri_duration
multi_az = local.is_regional_cluster
offering_type = var.rds_ri_offering_type
product_description = local.reserved_instance_engine
}

# Note: I'm not sure what will happen when the db reservation expires, and this is not easy to test.
# It will either be recreated or will require manual intervention to recreate.
resource "aws_rds_reserved_instance" "default" {
count = local.use_reserved_instances ? 1 : 0

offering_id = data.aws_rds_reserved_instance_offering.default[0].id
instance_count = local.cluster_instance_count
lifecycle {
# Once created, we want to avoid any case of accidentally re-creating.
prevent_destroy = true
}
}

# The name "primary" is poorly chosen. We actually mean standalone or regional.
# The primary cluster of a global database is actually created with the "secondary" cluster resource below.
resource "aws_rds_cluster" "primary" {
Expand Down
5 changes: 5 additions & 0 deletions outputs.tf
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,8 @@ output "activity_stream_name" {
value = join("", aws_rds_cluster_activity_stream.primary[*].kinesis_stream_name)
description = "Activity Stream Name"
}

output "reserved_instance" {
value = join("", aws_rds_reserved_instance.default[*])
description = "All information about the reserved instance(s) if created."
}
25 changes: 24 additions & 1 deletion variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -542,4 +542,27 @@ variable "network_type" {
type = string
default = "IPV4"
description = "The network type of the cluster. Valid values: IPV4, DUAL."
}
}

variable "use_reserved_instances" {
description = <<-EOT
WARNING: Observe your plans and applies carefully when using this feature.
It has potential to be very expensive if not used correctly.
Whether to use reserved instances.
EOT
type = bool
default = false
}

variable "rds_ri_offering_type" {
description = "Offering type of reserved DB instances. Valid values are 'No Upfront', 'Partial Upfront', 'All Upfront'."
type = string
default = ""
}

variable "rds_ri_duration" {
description = "The number of years to reserve the instance. Values can be 1 or 3 (or in seconds, 31536000 or 94608000)"
type = number
default = 1
}

0 comments on commit 9d1ecd6

Please sign in to comment.