diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e69de29 diff --git a/README.MD b/README.MD new file mode 100644 index 0000000..9ee65f9 --- /dev/null +++ b/README.MD @@ -0,0 +1,72 @@ + +## Requirements + +| Name | Version | +|------|---------| +| [terraform](#requirement\_terraform) | >=0.15 | +| [google](#requirement\_google) | >= 3.89.0 | +| [random](#requirement\_random) | >= 3.1.0 | +| [vault](#requirement\_vault) | >= 2.17.0 | + +## Providers + +| Name | Version | +|------|---------| +| [google](#provider\_google) | >= 3.89.0 | +| [google-beta](#provider\_google-beta) | n/a | +| [random](#provider\_random) | >= 3.1.0 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +|------|------| +| [google-beta_google_secret_manager_secret.registry_sql_client_cert](https://registry.terraform.io/providers/hashicorp/google-beta/latest/docs/resources/google_secret_manager_secret) | resource | +| [google-beta_google_secret_manager_secret.registry_sql_user_password](https://registry.terraform.io/providers/hashicorp/google-beta/latest/docs/resources/google_secret_manager_secret) | resource | +| [google-beta_google_secret_manager_secret_version.registry_sql_client_cert](https://registry.terraform.io/providers/hashicorp/google-beta/latest/docs/resources/google_secret_manager_secret_version) | resource | +| [google-beta_google_secret_manager_secret_version.secret-version-basic](https://registry.terraform.io/providers/hashicorp/google-beta/latest/docs/resources/google_secret_manager_secret_version) | resource | +| [google_sql_database.cloudsql_database](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/sql_database) | resource | +| [google_sql_database_instance.cloudsql_instance](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/sql_database_instance) | resource | +| [google_sql_ssl_cert.client_cert](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/sql_ssl_cert) | resource | +| [google_sql_user.cloudsql_user](https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/sql_user) | resource | +| [random_password.cloudsql_user_password](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [authorized\_cidrs](#input\_authorized\_cidrs) | Authorized networks CIDRs to connect to Cloud SQL instance. | `map(string)` | `{}` | no | +| [backup\_configuration](#input\_backup\_configuration) | The backup\_configuration subblock for the database setings |
list(object({
enabled = bool
start_time = string
location = string
retained_backups = number
}))
| `[]` | no | +| [cert\_common\_name](#input\_cert\_common\_name) | Certificate Common Name. | `string` | n/a | yes | +| [database\_version](#input\_database\_version) | Database type and version. Supported values = {MYSQL\_5\_6, MYSQL\_5\_7, MYSQL\_8\_0, POSTGRES\_9\_6,POSTGRES\_10, POSTGRES\_11, POSTGRES\_12, POSTGRES\_13, ...}. See {Instance settings documentation} | `string` | n/a | yes | +| [db\_name](#input\_db\_name) | Full name of the SQL database. | `string` | n/a | yes | +| [deletion\_protection](#input\_deletion\_protection) | If set to true, you protect an instance from being deleted. | `bool` | `true` | no | +| [disk\_autoresize](#input\_disk\_autoresize) | Whether if the disk can grow when more space is needed. If disk\_autoresize=true do not set disk\_size as terraform apply would try to set the disk\_size | `bool` | `true` | no | +| [disk\_type](#input\_disk\_type) | Type of disk used on the CloudSQL instance VM | `string` | `"PD_SSD"` | no | +| [instance\_name](#input\_instance\_name) | Full name of the SQL instance. | `string` | n/a | yes | +| [maintenance\_window](#input\_maintenance\_window) | Maintenance window to update/patch the VM. It can be rebooted during this maintenance window |
list(object({
day = number
hour = number
update_track = string
}))
| `[]` | no | +| [module\_depends\_on](#input\_module\_depends\_on) | n/a | `any` | `null` | no | +| [network](#input\_network) | Self link of the VPC network | `string` | n/a | yes | +| [public\_ip](#input\_public\_ip) | assign a public IP to this CloudSQL instance. Attention: you need Security approval to give a public IP to your CloudSQL instance. | `bool` | `false` | no | +| [region](#input\_region) | The region to host the resources in. | `string` | `"europe-west1"` | no | +| [require\_ssl](#input\_require\_ssl) | If require SSL, the connection port will be 3307, otherwise 3306. | `bool` | `true` | no | +| [tier](#input\_tier) | CloudSQL instance machine type (service tier). See {Instance settings documentation} | `string` | n/a | yes | +| [user\_host](#input\_user\_host) | The host the user can connect from. This is only supported for MySQL instances. Don't set this field for PostgreSQL instances. Can be an IP address, or % to allow any host. Changing this forces a new resource to be created. | `string` | `null` | no | +| [user\_name](#input\_user\_name) | User name. | `string` | n/a | yes | + +## Outputs + +| Name | Description | +|------|-------------| +| [connection\_name](#output\_connection\_name) | Cloud SQL connection name | +| [database\_name](#output\_database\_name) | Cloud SQL Database name | +| [instance\_connection\_name](#output\_instance\_connection\_name) | The connection name of the instance to be used in connection strings. For example, when connecting with Cloud SQL Proxy. | +| [instance\_ip\_address](#output\_instance\_ip\_address) | The IPv4 address assigned to the database instance. | +| [instance\_ip\_address\_type](#output\_instance\_ip\_address\_type) | The type of IP address assigned to this instance. { PRIMARY \| OUTGOING \| PRIVATE }. This module handles with PRIVATE only | +| [instance\_name](#output\_instance\_name) | Cloud SQL Database instance name | +| [secret\_manager\_cloudsql\_client\_cert\_secret\_id](#output\_secret\_manager\_cloudsql\_client\_cert\_secret\_id) | Secret manager id path where the CloudSQL client certificate secret is stored | +| [secret\_manager\_cloudsql\_user\_secret\_id](#output\_secret\_manager\_cloudsql\_user\_secret\_id) | Secret manager id where the CloudSQL user name/password secret is stored | + \ No newline at end of file diff --git a/main.tf b/main.tf new file mode 100644 index 0000000..d511451 --- /dev/null +++ b/main.tf @@ -0,0 +1,169 @@ +resource "google_sql_database_instance" "cloudsql_instance" { + name = var.instance_name + region = var.region + database_version = var.database_version + + # settings section is optional unless clone is not set. We do not implement clone but insted we create a new instance. + # Thus, settings becomes mandatory in this implementation + # Some of the settigs set are the same as terraform default values but explicitly set + settings { + tier = var.tier + disk_autoresize = var.disk_autoresize + disk_type = var.disk_type + + # (backup_configuration is optional) + dynamic "backup_configuration" { + for_each = var.backup_configuration + + content { + enabled = backup_configuration.value.enabled + start_time = backup_configuration.value.start_time + location = backup_configuration.value.location + backup_retention_settings { + retained_backups = backup_configuration.value.retained_backups + # retention_unit = "COUNT" + } + } + } + + ip_configuration { + # should this cloud sql get assigned a public ip ? + ipv4_enabled = var.public_ip + # VPC network from which cloudsql instance is accessible for private IP. + private_network = var.network + require_ssl = var.require_ssl + + # Authorized networks can be configured if: + # - client application is connecting directly to a Cloud SQL instance on its public IP address + # - client application is connecting directly to a Cloud SQL instance on its private IP address, + # and your client's IP address is a non-RFC 1918 address + # ref: # https://cloud.google.com/sql/docs/mysql/authorize-networks + # + # Limitations: addresses that cannot be added as authorized networks + # RFC1918 private addresses (THEY ARE AUTOMATICALLY INCLUDED), + # RFC1122 Loopback, 172.17/16 docker bridge nw, + # RFC3330 null network, RFC3927/2373 link-local, RFC3330/3849 documentation nw, + # RFC3330 multicast networks, class-E address space (RFC1112) + # + # (authorized_networks is optional) + dynamic "authorized_networks" { + for_each = var.authorized_cidrs + iterator = ntw + + content { + name = ntw.key + value = ntw.value + } + } + } + + # maintenance window (for updates and needed restarts) - Time: UTC, Day: 1-7 (1==Monday,...) + # By default it does maintenance window any time, any day. + # (maintenance_window is optional) + dynamic "maintenance_window" { + for_each = var.maintenance_window + iterator = window + content { + day = window.value.day + hour = window.value.hour + update_track = window.value.update_track + } + } + + } + + deletion_protection = var.deletion_protection +} + +### User + +# Password - provide a strong password unless password already provided via variable + +# Provide a "Strong" password +# https://dev.mysql.com/doc/refman/8.0/en/validate-password.html +# Note that Password Policy STRONG => lenght>8, 1 numeric + 1 lowercase + 1 uppercase + 1 special, substring of len>=4 are not dictionary words +resource "random_password" "cloudsql_user_password" { + length = 20 + special = true + min_lower = 2 + min_upper = 2 + min_numeric = 2 + min_special = 2 + override_special = "_%@$#" +} + +resource "google_sql_user" "cloudsql_user" { + name = var.user_name + instance = google_sql_database_instance.cloudsql_instance.name + password = sensitive(random_password.cloudsql_user_password.result) + host = var.user_host +} + +### Database +resource "google_sql_database" "cloudsql_database" { + name = var.db_name + instance = google_sql_database_instance.cloudsql_instance.name +} + +### Certificate +resource "google_sql_ssl_cert" "client_cert" { + common_name = var.cert_common_name + instance = google_sql_database_instance.cloudsql_instance.name +} + +### TODO: Secret manager +resource "google_secret_manager_secret" "registry_sql_user_password" { + provider = google-beta + secret_id = "${var.instance_name}_sql_user_password" + + replication { + user_managed { + replicas { + location = var.region + } + } + } +} + +resource "google_secret_manager_secret_version" "secret-version-basic" { + provider = google-beta + secret = google_secret_manager_secret.registry_sql_user_password.id + + secret_data = sensitive( + jsonencode( + { + "cloudsql_instance_name" = google_sql_user.cloudsql_user.instance + "cloudsql_user_name" = google_sql_user.cloudsql_user.name + "cloudsql_user_password" = random_password.cloudsql_user_password.result + } + ) + ) +} + +resource "google_secret_manager_secret" "registry_sql_client_cert" { + provider = google-beta + secret_id = "${var.instance_name}_sql_client_cert" + + replication { + user_managed { + replicas { + location = var.region + } + } + } +} + +resource "google_secret_manager_secret_version" "registry_sql_client_cert" { + provider = google-beta + secret = google_secret_manager_secret.registry_sql_client_cert.id + + secret_data = sensitive( + jsonencode( + { + "cert" = base64encode(google_sql_ssl_cert.client_cert.cert) + "private_key" = base64encode(google_sql_ssl_cert.client_cert.private_key) + "server_ca_cert" = base64encode(google_sql_ssl_cert.client_cert.server_ca_cert) + } + ) + ) +} diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..9a86169 --- /dev/null +++ b/outputs.tf @@ -0,0 +1,40 @@ + +output "instance_name" { + value = google_sql_database_instance.cloudsql_instance.name + description = "Cloud SQL Database instance name" +} + +output "instance_connection_name" { + value = google_sql_database_instance.cloudsql_instance.connection_name + description = "The connection name of the instance to be used in connection strings. For example, when connecting with Cloud SQL Proxy." +} + +output "instance_ip_address" { + value = google_sql_database_instance.cloudsql_instance.ip_address + description = "The IPv4 address assigned to the database instance." +} + +output "instance_ip_address_type" { + value = google_sql_database_instance.cloudsql_instance.ip_address[0].type + description = "The type of IP address assigned to this instance. { PRIMARY | OUTGOING | PRIVATE }. This module handles with PRIVATE only" +} + +output "database_name" { + value = google_sql_database.cloudsql_database.name + description = "Cloud SQL Database name" +} + +output "connection_name" { + value = google_sql_database_instance.cloudsql_instance.connection_name + description = "Cloud SQL connection name" +} + +output "secret_manager_cloudsql_user_secret_id" { + value = google_secret_manager_secret.registry_sql_user_password.secret_id + description = "Secret manager id where the CloudSQL user name/password secret is stored" +} + +output "secret_manager_cloudsql_client_cert_secret_id" { + value = google_secret_manager_secret.registry_sql_client_cert.secret_id + description = "Secret manager id path where the CloudSQL client certificate secret is stored" +} \ No newline at end of file diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..3aed317 --- /dev/null +++ b/variables.tf @@ -0,0 +1,144 @@ + +############################################################### +# Default variables # +############################################################### + +##### GCP PROJECT +# +# variable "project_id" { +# type = string +# description = "The project ID to host resources." +# } + +variable "region" { + type = string + description = "The region to host the resources in." + default = "europe-west1" +} + +###### Cloud SQL + +# Cloud SQL configuration +# {Instance settings documentation} [https://cloud.google.com/sql/docs//instance-settings] +# {Instance locations documentation} [https://cloud.google.com/sql/docs//locations] + + +variable "instance_name" { + type = string + description = "Full name of the SQL instance." +} + +variable "db_name" { + type = string + description = "Full name of the SQL database." +} + +variable "cert_common_name" { + type = string + description = "Certificate Common Name." +} + +variable "user_name" { + type = string + description = "User name." +} + +variable "user_host" { + type = string + description = "The host the user can connect from. This is only supported for MySQL instances. Don't set this field for PostgreSQL instances. Can be an IP address, or % to allow any host. Changing this forces a new resource to be created." + default = null +} + +### Other parameters + +variable "deletion_protection" { + type = bool + description = "If set to true, you protect an instance from being deleted." + default = true +} + +# Settings + +variable "database_version" { + type = string + description = "Database type and version. Supported values = {MYSQL_5_6, MYSQL_5_7, MYSQL_8_0, POSTGRES_9_6,POSTGRES_10, POSTGRES_11, POSTGRES_12, POSTGRES_13, ...}. See {Instance settings documentation}" +} + +variable "tier" { + type = string + description = "CloudSQL instance machine type (service tier). See {Instance settings documentation}" +} + +variable "disk_autoresize" { + type = bool + description = "Whether if the disk can grow when more space is needed. If disk_autoresize=true do not set disk_size as terraform apply would try to set the disk_size" + default = true +} + +variable "disk_type" { + type = string + description = "Type of disk used on the CloudSQL instance VM" + default = "PD_SSD" +} + +# IP configuration +# Note that we do not allow the usage of public_ip but instead ise a private_network to +# reach the CloudSQL instance + +variable "public_ip" { + type = bool + description = "assign a public IP to this CloudSQL instance. Attention: you need Security approval to give a public IP to your CloudSQL instance." + default = false +} + +variable "network" { + type = string + description = "Self link of the VPC network" +} + +variable "require_ssl" { + type = bool + description = "If require SSL, the connection port will be 3307, otherwise 3306." + default = true +} + +variable "authorized_cidrs" { + type = map(string) + description = "Authorized networks CIDRs to connect to Cloud SQL instance." + default = {} +} + +# Backup +variable "backup_configuration" { + description = "The backup_configuration subblock for the database setings" + type = list(object({ + enabled = bool + start_time = string + location = string + retained_backups = number + })) + default = [] +} + +# OS updates +variable "maintenance_window" { + description = "Maintenance window to update/patch the VM. It can be rebooted during this maintenance window" + type = list(object({ + day = number + hour = number + update_track = string + })) + default = [] +} + + +############################################################### +# Custom variables # +############################################################### + +# Add here some custom / additional variables + +variable "module_depends_on" { + type = any + default = null +} \ No newline at end of file diff --git a/versions.tf b/versions.tf new file mode 100644 index 0000000..25054df --- /dev/null +++ b/versions.tf @@ -0,0 +1,20 @@ +terraform { + required_version = ">=0.15" + + required_providers { + google = { + source = "hashicorp/google" + version = ">= 3.89.0" + } + # For CloudSQL + random = { + source = "hashicorp/random" + version = ">= 3.1.0" + } + + vault = { + source = "hashicorp/vault" + version = ">= 2.17.0" + } + } +}