diff --git a/README.md b/README.md index 2df86ea..5aa25e4 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,4 @@ -# terraform-azurerm-template -Template file for creating public terraform modules which can be pushed to the Terraform registry +# SQL Server + +Creates an Azure SQL Server with databases. +By default, local authentication and public network access is disabled. \ No newline at end of file diff --git a/main.tf b/main.tf index 139597f..85ab71d 100644 --- a/main.tf +++ b/main.tf @@ -1,2 +1,135 @@ +locals { + name_prefix = data.azurerm_subscription.current.display_name + resource_group_name = var.create_resource_group == true ? azurerm_resource_group.sql[0].name : data.azurerm_resource_group.rg[0].name + unique = var.unique == null ? random_string.unique[0].result : var.unique + enable_local_auth = var.azuread_administrator[0].azuread_authentication_only == true ? false : true + server_name = var.server_name != null ? var.server_name : "${local.name_prefix}-sql${local.unique}-sqlsvr" +} +data "azurerm_subscription" "current" {} + +resource "random_password" "password" { + count = local.enable_local_auth == true ? 1 : 0 + length = var.password_length + special = true + override_special = "_%@" +} + +data "azurerm_resource_group" "rg" { + count = var.create_resource_group == false ? 1 : 0 + name = var.resource_group_name +} + +resource "random_string" "unique" { + count = var.unique == null ? 1 : 0 + length = 6 + special = false + upper = false + numeric = true +} + +resource "azurerm_resource_group" "sql" { + count = var.create_resource_group == true ? 1 : 0 + name = "${local.name_prefix}-sql" + location = var.location + lifecycle { + ignore_changes = [tags] + } +} + +resource "azurerm_mssql_server" "sqlsrv" { + administrator_login = local.enable_local_auth ? var.admin_username : null + administrator_login_password = local.enable_local_auth ? random_password.password[0].result : null + location = var.location + name = local.server_name + resource_group_name = local.resource_group_name + minimum_tls_version = var.minimum_tls_version + version = "12.0" + transparent_data_encryption_key_vault_key_id = var.transparent_data_encryption_key_vault_key_id != null ? var.transparent_data_encryption_key_vault_key_id : null + + public_network_access_enabled = var.publicly_available + + dynamic "azuread_administrator" { + for_each = var.azuread_administrator[0].login_username != "" ? [1] : [] + # Only 1 or 0 of this block is supported. Always use index 0 of azuread_administrator block if supplied + content { + azuread_authentication_only = var.azuread_administrator[0].azuread_authentication_only + login_username = var.azuread_administrator[0].login_username + object_id = var.azuread_administrator[0].object_id + tenant_id = var.azuread_administrator[0].tenant_id + } + } + + dynamic "identity" { + for_each = var.create_managed_identity == true ? [1] : [] + content { + type = "SystemAssigned" + } + } +} + +resource "azurerm_mssql_database" "db" { + for_each = var.databases + + # Since sku_names now determine server type, we need to compute the type here. + # Serverless will always have GP_S_xx, and we can therefore deduce this from splitting by underscore. + # License type not allowed for serverless databases + name = each.key + server_id = azurerm_mssql_server.sqlsrv.id + sku_name = each.value.sku_name != null ? each.value.sku_name : "GP_S_Gen5_1" + min_capacity = !startswith(each.value.sku_name, "GP_S") ? 0 : try(each.value.min_capacity, 0.5) + auto_pause_delay_in_minutes = !startswith(each.value.sku_name, "GP_S") ? 0 : try(each.value.auto_pause_delay_in_minutes, 60) + storage_account_type = each.value.storage_account_type != null ? each.value.storage_account_type : "Local" + license_type = each.value.capacity_unit == "Provisioned" && each.value.license_type != null ? each.value.license_type : null + collation = each.value.collation != null ? each.value.collation : "Danish_Norwegian_CI_AS" + max_size_gb = !startswith(each.value.sku_name, "GP_S") ? try(each.value.max_size_gb, 32) : try(each.value.max_size_gb, 50) + create_mode = each.value.create_mode + creation_source_database_id = each.value.create_mode != "Default" && each.value.creation_source_database_id != null ? each.value.creation_source_database_id : null + + restore_point_in_time = each.value.create_mode == "PointInTimeRestore" && each.value.restore_point_in_time != null ? each.value.restore_point_in_time : null + dynamic "long_term_retention_policy" { + # Long term retention policy not allowed for serverless databases with auto-pause enabled. + # Therefore the "hacky" determination of enabling LTR or not. + # This logic will enable LTR by default if supported. + for_each = each.value.capacity_unit == "Provisioned" || each.value.auto_pause_delay_in_minutes == -1 ? ["true"] : [] + content { + monthly_retention = lookup(long_term_retention_policy, "monthly_retention", "P6M") + week_of_year = lookup(long_term_retention_policy, "week_of_year", 1) + weekly_retention = lookup(long_term_retention_policy, "weekly_retention", "P1M") + yearly_retention = lookup(long_term_retention_policy, "yearly_retention", "P5Y") + } + } + + short_term_retention_policy { + retention_days = each.value.short_term_retention_policy == null ? 7 : each.value.short_term_retention_policy.retention_days + backup_interval_in_hours = each.value.short_term_retention_policy == null ? 12 : each.value.short_term_retention_policy.backup_interval_in_hours + } +} + +resource "azurerm_private_endpoint" "sqlsrv_pe" { + count = var.create_private_endpoint == true ? 1 : 0 + location = azurerm_mssql_server.sqlsrv.location + name = "${azurerm_mssql_server.sqlsrv.name}-pe" + resource_group_name = local.resource_group_name + subnet_id = var.subnet_id + private_service_connection { + is_manual_connection = false + name = "${azurerm_mssql_server.sqlsrv.name}-pe" + private_connection_resource_id = azurerm_mssql_server.sqlsrv.id + subresource_names = ["sqlServer"] + } +} + +resource "azurerm_private_dns_a_record" "sqlsrv_pe_dns" { + count = var.create_private_endpoint == true ? 1 : 0 + name = azurerm_mssql_server.sqlsrv.name + records = [ + azurerm_private_endpoint.sqlsrv_pe[0].private_service_connection[0].private_ip_address + ] + resource_group_name = var.dns_resource_group_name + ttl = 600 + zone_name = "privatelink.database.windows.net" + + provider = azurerm.p-dns +} diff --git a/outputs.tf b/outputs.tf index e69de29..6d72ae7 100644 --- a/outputs.tf +++ b/outputs.tf @@ -0,0 +1,9 @@ +output "server" { + description = "The SQL Server resource" + value = azurerm_mssql_server.sqlsrv +} + +output "private_ip" { + description = "The database private IP if created." + value = var.create_private_endpoint == true ? azurerm_private_endpoint.sqlsrv_pe[0].private_service_connection[0].private_ip_address : "" +} diff --git a/providers.tf b/providers.tf new file mode 100644 index 0000000..ba9a5bf --- /dev/null +++ b/providers.tf @@ -0,0 +1,14 @@ +terraform { + required_version = "~> 1.5" + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 3.0" + configuration_aliases = [azurerm.p-dns] + } + random = { + source = "hashicorp/random" + version = "~> 3.5" + } + } +} diff --git a/variables.tf b/variables.tf index e69de29..f3ccc88 100644 --- a/variables.tf +++ b/variables.tf @@ -0,0 +1,153 @@ +variable "resource_group_name" { + type = string + description = "Resource Group Name where resources should be placed. Defaults to auto-generated name for creating rg." + default = null +} + +variable "dns_resource_group_name" { + type = string + description = "Resource Group Name where DNS zone is located." + default = "p-dns-pri" +} + +variable "server_name" { + type = string + description = "Name of the SQL server. Defaults to auto-generated name." + default = null +} + +variable "create_resource_group" { + type = bool + description = "Create resource group? Defaults to true." + default = true +} + +variable "subnet_id" { + type = string + description = "Virtual Network subnet id where private endpoints should be created." +} + +variable "create_private_endpoint" { + type = bool + description = "Create private endpoint for the SQL server? Defaults to true." + default = true +} + +variable "location" { + type = string + description = "Location for all resources involved." + default = "norwayeast" +} + +variable "publicly_available" { + type = bool + description = "Should SQL server be publicly available? Defaults to false." + default = false +} + +variable "admin_username" { + type = string + description = "Admin username for SQL server. Defaults to 'sqlserveradmin'." + default = "sqlserveradmin" +} + +variable "minimum_tls_version" { + type = string + description = "Minimum TLS version the SQL server supports. Valid values 1.0, 1.1, 1.2. Defaults to 1.2 (preferred)." + default = "1.2" + + validation { + condition = (contains(["1.0", "1.1", "1.2"], var.minimum_tls_version)) + error_message = "Valid values are '1.0', '1.1', or '1.2'." + } +} + +variable "create_managed_identity" { + type = bool + description = "Create system assigned managed identity for SQL server? Defaults to false." + default = false +} + +variable "transparent_data_encryption_key_vault_key_id" { + type = string + description = "The Key Vault Key ID to use for Transparent Data Encryption. Defaults to null." + default = null +} + +variable "azuread_administrator" { + type = list(object({ + azuread_authentication_only = optional(bool, true) + login_username = optional(string, "MDIR SQL Admins PIM") + object_id = optional(string, "0820ef72-b3ef-4b39-aebd-1d1912ef0df9") + tenant_id = optional(string, "f999e2e9-5aa8-467f-9eca-df0d6c4eaf13") + })) + default = [{ + azuread_authentication_only = true + login_username = "MDIR SQL Admins PIM" + object_id = "0820ef72-b3ef-4b39-aebd-1d1912ef0df9" + tenant_id = "f999e2e9-5aa8-467f-9eca-df0d6c4eaf13" + }] +} + +variable "unique" { + type = string + description = "Provide a unique string if you want to use an already generated one." + default = null + + validation { + condition = length(var.unique == null ? "123456" : var.unique) == 6 + error_message = "Unique string must be exactly 6 chars long." + } +} + +variable "databases" { + type = map(object({ + sku_name = optional(string), # Sku name for database. Many possibilities .Defaults to "GP_S_Gen5_1" which means serverless 1 vcore. + min_capacity = optional(number), # Minimum capacity for serverless type capacity. Defaults to 0.5. + auto_pause_delay_in_minutes = optional(number), # Time in minutes after which database is automatically paused. A value of -1 means that automatic pause is disabled. Defaults to 60. + storage_account_type = optional(string), # Storage account type for database backup. Possible values are Geo, GeoZone, Local and Zone. Defaults to Local. + license_type = optional(string), # License type for hybrid benefit. LicenseIncluded (regular) or BasePrice(Hybrid benefit). Defaults to LicenseIncluded. + collation = optional(string), # Collation for database. Defaults to "Danish_Norwegian_CI_AS". + max_size_gb = optional(number), # Number of gigabytes database size. Defaults to 50. + capacity_unit = optional(string), # The capacity unit for database. Either Serverless or Provisioned. Only applicable if using vCore server type. Defaults to Serverless. + creation_source_database_id = optional(string), # The resource ID of the source database if create_mode is not Default. Defaults to null. + create_mode = optional(string, "Default") # The creation mode of the database. Defaults to Default. + restore_point_in_time = optional(string), # The point in time to restore from if create_mode is PointInTimeRestore. Defaults to null. + long_term_retention_policy = optional(object({ + monthly_retention = optional(string) # See own comment below + week_of_year = optional(number) # See own comment below + weekly_retention = optional(string) # See own comment below + yearly_retention = optional(string) # See own comment below + })) + short_term_retention_policy = optional(object({ + backup_interval_in_hours = optional(number) # See own comment below + retention_days = optional(number) # See own comment below + })) + }) + ) + description = "Map of objects containing information on databases to be created." + default = { + defaultdb = {} + } +} + +variable "password_length" { + type = number + description = "Length of password for SQL server. Defaults to 16." + default = 16 +} + +############################################################################## +# Retention policies # +############################################################################## +# If you don't provide backup info, a best practice will be enforced for you.# +############################################################################## +# A long_term_retention_policy block supports the following: +# weekly_retention - (Optional) The weekly retention policy for an LTR backup in an ISO 8601 format. Valid value is between 1 to 520 weeks. e.g. P1Y, P1M, P1W or P7D. +# monthly_retention - (Optional) The monthly retention policy for an LTR backup in an ISO 8601 format. Valid value is between 1 to 120 months. e.g. P1Y, P1M, P4W or P30D. +# yearly_retention - (Optional) The yearly retention policy for an LTR backup in an ISO 8601 format. Valid value is between 1 to 10 years. e.g. P1Y, P12M, P52W or P365D. +# week_of_year - (Required) The week of year to take the yearly backup. Value has to be between 1 and 52. + +# A short_term_retention_policy block supports the following: +# retention_days - (Required) Point In Time Restore configuration. Value has to be between 7 and 35. +# backup_interval_in_hours - (Optional) The hours between each differential backup. This is only applicable to live databases but not dropped databases. Value has to be 12 or 24. Defaults to 12 hours.