From 64b9bcbb8bd99a0576b1ed44f844573baade889b Mon Sep 17 00:00:00 2001 From: Thomas Felix Date: Thu, 9 Jul 2026 14:37:58 +0200 Subject: [PATCH] feat(azure/postgresql): modernize module to pass scorecard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bring azure/postgresql up to current conventions (scorecard 53% → 100%), addressing the gaps surfaced on PR #221. - buildingblock: migrate from the retired Azure PostgreSQL Single Server (azurerm_postgresql_server) to Flexible Server (azurerm_postgresql_flexible_server); self-contained resource group and globally-unique server name with random suffix - backplane: replace the principal_ids role-assignment pattern with the UAMI + WIF convention (user-assigned managed identity, federated identity credential, identity output); role definition targets flexibleServers and keeps */register/action for fresh subscriptions - add meshstack_integration.tf wiring the BBD, backplane and OIDC auth - add e2e/ smoke test modeled on azure/storage-account - drop legacy bb_description.md (superseded by inline BBD readme) Co-Authored-By: Claude Opus 4.8 (1M context) --- modules/azure/postgresql/backplane/README.md | 32 ++- modules/azure/postgresql/backplane/main.tf | 59 ++++- modules/azure/postgresql/backplane/outputs.tf | 26 +- .../azure/postgresql/backplane/variables.tf | 27 +- .../azure/postgresql/backplane/versions.tf | 7 +- modules/azure/postgresql/bb_description.md | 30 --- .../azure/postgresql/buildingblock/README.md | 50 ++-- .../azure/postgresql/buildingblock/main.tf | 38 +-- .../azure/postgresql/buildingblock/outputs.tf | 27 +- .../postgresql/buildingblock/variables.tf | 61 ++--- .../postgresql/buildingblock/versions.tf | 6 +- modules/azure/postgresql/e2e/main.tf | 68 +++++ modules/azure/postgresql/e2e/provider.tf | 6 + modules/azure/postgresql/e2e/terraform.tf | 12 + .../e2e/tests/azure_postgresql_hub.tftest.hcl | 29 +++ .../azure/postgresql/meshstack_integration.tf | 238 ++++++++++++++++++ 16 files changed, 535 insertions(+), 181 deletions(-) delete mode 100644 modules/azure/postgresql/bb_description.md create mode 100644 modules/azure/postgresql/e2e/main.tf create mode 100644 modules/azure/postgresql/e2e/provider.tf create mode 100644 modules/azure/postgresql/e2e/terraform.tf create mode 100644 modules/azure/postgresql/e2e/tests/azure_postgresql_hub.tftest.hcl create mode 100644 modules/azure/postgresql/meshstack_integration.tf diff --git a/modules/azure/postgresql/backplane/README.md b/modules/azure/postgresql/backplane/README.md index ef7521ab..1cf49dca 100644 --- a/modules/azure/postgresql/backplane/README.md +++ b/modules/azure/postgresql/backplane/README.md @@ -4,12 +4,16 @@ This documentation is intended as a reference for platform engineers deploying t ## Overview -The backplane provisions the automation identity and permissions required to deploy Azure Database for PostgreSQL on behalf of application teams. +The backplane provisions the automation identity and permissions required to deploy Azure Database +for PostgreSQL Flexible Server on behalf of application teams. It creates a +**User-Assigned Managed Identity (UAMI)** with a **federated identity credential** so meshStack can +authenticate via workload identity federation (WIF) — no service principals or client secrets. ## Permissions -This is a simple building block backplane that grants the automation principal access to PostgreSQL -across all subscriptions underneath a management group (typically the top-level management group for landing zones). +The UAMI is granted a custom role definition scoped to `var.scope` (typically the top-level +management group for landing zones) that allows managing PostgreSQL Flexible Servers and their +resource groups, plus registering the required resource providers on freshly created subscriptions. ## Requirements @@ -17,8 +21,7 @@ across all subscriptions underneath a management group (typically the top-level | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0 | -| [azuread](#requirement\_azuread) | 3.1.0 | -| [azurerm](#requirement\_azurerm) | 3.116.0 | +| [azurerm](#requirement\_azurerm) | >= 4.64 | ## Modules @@ -28,24 +31,27 @@ No modules. | Name | Type | |------|------| -| [azurerm_role_assignment.buildingblock_deploy](https://registry.terraform.io/providers/hashicorp/azurerm/3.116.0/docs/resources/role_assignment) | resource | -| [azurerm_role_definition.buildingblock_deploy](https://registry.terraform.io/providers/hashicorp/azurerm/3.116.0/docs/resources/role_definition) | resource | +| [azurerm_federated_identity_credential.backplane](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/federated_identity_credential) | resource | +| [azurerm_resource_group.backplane](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group) | resource | +| [azurerm_role_assignment.backplane](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_role_definition.backplane](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_definition) | resource | +| [azurerm_user_assigned_identity.backplane](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/user_assigned_identity) | resource | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [name](#input\_name) | n/a | `string` | `"postgresql"` | no | -| [principal\_ids](#input\_principal\_ids) | set of principal ids that will be granted permissions to deploy the building block | `set(string)` | n/a | yes | -| [scope](#input\_scope) | Scope where the building block should be deployable, typically the parent of all Landing Zones. | `string` | n/a | yes | +| [location](#input\_location) | Azure region for the UAMI resource. | `string` | n/a | yes | +| [name](#input\_name) | Name for the building block identity and role definition. | `string` | n/a | yes | +| [scope](#input\_scope) | Scope for role assignment (management group or subscription ID). | `string` | n/a | yes | +| [workload\_identity\_federation](#input\_workload\_identity\_federation) | WIF issuer and subjects for federated authentication. |
object({
issuer = string
subjects = list(string)
})
| n/a | yes | ## Outputs | Name | Description | |------|-------------| -| [role\_assignment\_ids](#output\_role\_assignment\_ids) | The IDs of the role assignments for the service principals. | -| [role\_assignment\_principal\_ids](#output\_role\_assignment\_principal\_ids) | The principal IDs of the service principals that have been assigned the role. | +| [identity](#output\_identity) | The managed identity used as the automation principal for this building block. | | [role\_definition\_id](#output\_role\_definition\_id) | The ID of the role definition that enables deployment of the building block to subscriptions. | | [role\_definition\_name](#output\_role\_definition\_name) | The name of the role definition that enables deployment of the building block to subscriptions. | -| [scope](#output\_scope) | The scope where the role definition and role assignments are applied. | +| [scope](#output\_scope) | The scope where the role definition and role assignment are applied. | diff --git a/modules/azure/postgresql/backplane/main.tf b/modules/azure/postgresql/backplane/main.tf index 9bda2841..60f605eb 100644 --- a/modules/azure/postgresql/backplane/main.tf +++ b/modules/azure/postgresql/backplane/main.tf @@ -1,24 +1,57 @@ -resource "azurerm_role_definition" "buildingblock_deploy" { +resource "azurerm_resource_group" "backplane" { + name = var.name + location = var.location +} + +resource "azurerm_user_assigned_identity" "backplane" { + name = var.name + location = var.location + resource_group_name = azurerm_resource_group.backplane.name +} + +resource "azurerm_federated_identity_credential" "backplane" { + for_each = { for i, s in var.workload_identity_federation.subjects : tostring(i) => s } + + name = "subject-${each.key}" + resource_group_name = azurerm_resource_group.backplane.name + parent_id = azurerm_user_assigned_identity.backplane.id + audience = ["api://AzureADTokenExchange"] + issuer = var.workload_identity_federation.issuer + subject = each.value +} + +resource "azurerm_role_definition" "backplane" { name = "${var.name}-deploy" description = "Enables deployment of the ${var.name} building block to subscriptions" scope = var.scope permissions { actions = [ - "Microsoft.DBforPostgreSQL/servers/write", - "Microsoft.DBforPostgreSQL/servers/read", - "Microsoft.DBforPostgreSQL/servers/delete", - "Microsoft.DBforPostgreSQL/servers/firewallRules/read", - "Microsoft.DBforPostgreSQL/servers/firewallRules/write", - "Microsoft.DBforPostgreSQL/servers/databases/read", - "Microsoft.DBforPostgreSQL/servers/databases/write" + # resource provider registration — required on freshly created subscriptions + "*/register/action", + + # resource groups + "Microsoft.Resources/subscriptions/resourcegroups/read", + "Microsoft.Resources/subscriptions/resourcegroups/write", + "Microsoft.Resources/subscriptions/resourcegroups/delete", + + # postgresql flexible servers + "Microsoft.DBforPostgreSQL/flexibleServers/read", + "Microsoft.DBforPostgreSQL/flexibleServers/write", + "Microsoft.DBforPostgreSQL/flexibleServers/delete", + "Microsoft.DBforPostgreSQL/flexibleServers/firewallRules/read", + "Microsoft.DBforPostgreSQL/flexibleServers/firewallRules/write", + "Microsoft.DBforPostgreSQL/flexibleServers/firewallRules/delete", + "Microsoft.DBforPostgreSQL/flexibleServers/databases/read", + "Microsoft.DBforPostgreSQL/flexibleServers/databases/write", + "Microsoft.DBforPostgreSQL/flexibleServers/databases/delete", + "Microsoft.DBforPostgreSQL/flexibleServers/configurations/read", + "Microsoft.DBforPostgreSQL/flexibleServers/configurations/write", ] } } -resource "azurerm_role_assignment" "buildingblock_deploy" { - for_each = var.principal_ids - - role_definition_id = azurerm_role_definition.buildingblock_deploy.role_definition_resource_id - principal_id = each.value +resource "azurerm_role_assignment" "backplane" { scope = var.scope + role_definition_id = azurerm_role_definition.backplane.role_definition_resource_id + principal_id = azurerm_user_assigned_identity.backplane.principal_id } diff --git a/modules/azure/postgresql/backplane/outputs.tf b/modules/azure/postgresql/backplane/outputs.tf index 983bf40f..cfa15002 100644 --- a/modules/azure/postgresql/backplane/outputs.tf +++ b/modules/azure/postgresql/backplane/outputs.tf @@ -1,25 +1,23 @@ +output "identity" { + value = { + client_id = azurerm_user_assigned_identity.backplane.client_id + principal_id = azurerm_user_assigned_identity.backplane.principal_id + tenant_id = azurerm_user_assigned_identity.backplane.tenant_id + } + description = "The managed identity used as the automation principal for this building block." +} + output "role_definition_id" { - value = azurerm_role_definition.buildingblock_deploy.id + value = azurerm_role_definition.backplane.id description = "The ID of the role definition that enables deployment of the building block to subscriptions." } output "role_definition_name" { - value = azurerm_role_definition.buildingblock_deploy.name + value = azurerm_role_definition.backplane.name description = "The name of the role definition that enables deployment of the building block to subscriptions." } -output "role_assignment_ids" { - value = [for id in azurerm_role_assignment.buildingblock_deploy : id.id] - description = "The IDs of the role assignments for the service principals." -} - -output "role_assignment_principal_ids" { - value = [for id in azurerm_role_assignment.buildingblock_deploy : id.principal_id] - description = "The principal IDs of the service principals that have been assigned the role." -} - output "scope" { value = var.scope - description = "The scope where the role definition and role assignments are applied." + description = "The scope where the role definition and role assignment are applied." } - diff --git a/modules/azure/postgresql/backplane/variables.tf b/modules/azure/postgresql/backplane/variables.tf index 21da05bb..f33ee46b 100644 --- a/modules/azure/postgresql/backplane/variables.tf +++ b/modules/azure/postgresql/backplane/variables.tf @@ -1,17 +1,30 @@ variable "name" { - type = string - nullable = false - default = "postgresql" + type = string + nullable = false + description = "Name for the building block identity and role definition." + validation { + condition = can(regex("^[-a-z0-9]+$", var.name)) + error_message = "Only alphanumeric lowercase characters and dashes are allowed" + } } variable "scope" { type = string nullable = false - description = "Scope where the building block should be deployable, typically the parent of all Landing Zones." + description = "Scope for role assignment (management group or subscription ID)." +} + +variable "location" { + type = string + nullable = false + description = "Azure region for the UAMI resource." } -variable "principal_ids" { - type = set(string) +variable "workload_identity_federation" { + type = object({ + issuer = string + subjects = list(string) + }) nullable = false - description = "set of principal ids that will be granted permissions to deploy the building block" + description = "WIF issuer and subjects for federated authentication." } diff --git a/modules/azure/postgresql/backplane/versions.tf b/modules/azure/postgresql/backplane/versions.tf index ecfa871a..70e0537f 100644 --- a/modules/azure/postgresql/backplane/versions.tf +++ b/modules/azure/postgresql/backplane/versions.tf @@ -4,12 +4,7 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = "3.116.0" - } - azuread = { - source = "hashicorp/azuread" - version = "3.1.0" + version = ">= 4.64" } } } - diff --git a/modules/azure/postgresql/bb_description.md b/modules/azure/postgresql/bb_description.md deleted file mode 100644 index 16e0e065..00000000 --- a/modules/azure/postgresql/bb_description.md +++ /dev/null @@ -1,30 +0,0 @@ -# Azure PostgreSQL - -## Description -This building block provides an Azure Database for PostgreSQL instance, offering a fully managed, scalable, and secure relational database service. It supports enterprise-grade PostgreSQL workloads with automated maintenance, high availability, and built-in security features. - -## Usage Motivation -This building block is for application teams that need a reliable and managed PostgreSQL database without the operational overhead of self-hosting. It is ideal for applications that require structured data storage, transaction consistency, and seamless integration with Azure services. - -## Usage Examples -- A web application stores user data and transactional records in an Azure PostgreSQL database. -- A data analytics team uses Azure PostgreSQL as a backend for reporting and business intelligence queries. - -## Shared Responsibility - -| Responsibility | Platform Team | Application Team | -|------------------------|--------------|----------------| -| Provisioning and configuring the PostgreSQL instance | ✅ | ❌ | -| Enforcing security policies (e.g., encryption, firewall rules) | ✅ | ❌ | -| Managing database backups and disaster recovery | ✅ | ❌ | -| Monitoring database performance and availability | ✅ | ❌ | -| Creating and managing database schemas and tables | ❌ | ✅ | -| Managing application-level database performance tuning | ❌ | ✅ | -| Handling database queries and indexing optimization | ❌ | ✅ | - -## Recommendations for Secure and Efficient PostgreSQL Usage -- **Use private endpoints**: Restrict access to authorized Azure services via VNET integration. -- **Enable automated backups**: Configure point-in-time recovery (PITR) for data protection. -- **Enforce role-based access control**: Manage database permissions using PostgreSQL roles. -- **Optimize performance**: Use Query Performance Insights and connection pooling. -- **Choose the right pricing tier**: Select between Single Server, Flexible Server, or Hyperscale based on workload needs. diff --git a/modules/azure/postgresql/buildingblock/README.md b/modules/azure/postgresql/buildingblock/README.md index 385a3b25..0c8f52fe 100644 --- a/modules/azure/postgresql/buildingblock/README.md +++ b/modules/azure/postgresql/buildingblock/README.md @@ -3,12 +3,12 @@ name: Azure PostgreSQL Deployment supportedPlatforms: - azure description: | - Provides a managed Azure PostgreSQL database with scalability, security, and high availability. + Provides a managed Azure Database for PostgreSQL Flexible Server with scalability, security, and high availability. --- # Azure PostgreSQL Deployment -This Terraform project deploys a cost-effective Azure PostgreSQL database with minimal resources and security-conscious configuration. +This Terraform project deploys a managed Azure Database for PostgreSQL Flexible Server into a dedicated resource group, with sensible defaults for SKU, version, storage, and backups and a security-conscious configuration. ## 🛠 Configuration @@ -18,9 +18,8 @@ This Terraform project deploys a cost-effective Azure PostgreSQL database with m | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.5.0 | -| [azurerm](#requirement\_azurerm) | >= 4.22.0 | -| [random](#requirement\_random) | >= 3.7.1 | +| [azurerm](#requirement\_azurerm) | >= 4.64 | +| [random](#requirement\_random) | >= 3.8 | ## Modules @@ -30,37 +29,34 @@ No modules. | Name | Type | |------|------| -| [azurerm_postgresql_server.example](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/postgresql_server) | resource | -| [azurerm_resource_group.example](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group) | resource | +| [azurerm_postgresql_flexible_server.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/postgresql_flexible_server) | resource | +| [azurerm_resource_group.postgresql](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group) | resource | | [random_password.psql_admin_password](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/password) | resource | +| [random_string.resource_code](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [administrator\_login](#input\_administrator\_login) | Administrator username for PostgreSQL | `string` | `"psqladmin"` | no | -| [auto\_grow\_enabled](#input\_auto\_grow\_enabled) | Enable auto-grow for storage | `bool` | `false` | no | -| [backup\_retention\_days](#input\_backup\_retention\_days) | Backup retention in days | `number` | `7` | no | -| [geo\_redundant\_backup\_enabled](#input\_geo\_redundant\_backup\_enabled) | Enable geo-redundant backups | `bool` | `false` | no | -| [location](#input\_location) | Azure region | `string` | `"West Europe"` | no | -| [postgresql\_server\_name](#input\_postgresql\_server\_name) | Name of the PostgreSQL server | `string` | n/a | yes | -| [postgresql\_version](#input\_postgresql\_version) | PostgreSQL version | `string` | `"11"` | no | -| [public\_network\_access\_enabled](#input\_public\_network\_access\_enabled) | Enable public network access | `bool` | `false` | no | -| [resource\_group\_name](#input\_resource\_group\_name) | Name of the Azure resource group | `string` | n/a | yes | -| [sku\_name](#input\_sku\_name) | The SKU name for the PostgreSQL server | `string` | `"B_Gen5_1"` | no | -| [ssl\_enforcement\_enabled](#input\_ssl\_enforcement\_enabled) | Enforce SSL connection | `bool` | `true` | no | -| [ssl\_minimal\_tls\_version\_enforced](#input\_ssl\_minimal\_tls\_version\_enforced) | Minimum TLS version | `string` | `"TLS1_2"` | no | -| [storage\_mb](#input\_storage\_mb) | Storage size in MB | `number` | `5120` | no | -| [subscription\_id](#input\_subscription\_id) | the Azure subscription id | `string` | n/a | yes | +| [administrator\_login](#input\_administrator\_login) | Administrator username for the PostgreSQL Flexible Server. | `string` | `"psqladmin"` | no | +| [backup\_retention\_days](#input\_backup\_retention\_days) | Backup retention in days (7-35). | `number` | `7` | no | +| [geo\_redundant\_backup\_enabled](#input\_geo\_redundant\_backup\_enabled) | Enable geo-redundant backups. | `bool` | `false` | no | +| [location](#input\_location) | Azure region where the PostgreSQL Flexible Server is created. | `string` | `"germanywestcentral"` | no | +| [postgresql\_server\_name](#input\_postgresql\_server\_name) | Name prefix for the PostgreSQL Flexible Server. A random 5-character suffix is appended to ensure global uniqueness. Only lowercase letters, numbers and hyphens are allowed. | `string` | n/a | yes | +| [postgresql\_version](#input\_postgresql\_version) | PostgreSQL major version. | `string` | `"16"` | no | +| [public\_network\_access\_enabled](#input\_public\_network\_access\_enabled) | Enable public network access. Disabling requires VNet integration (delegated subnet), which is out of scope for this building block. | `bool` | `true` | no | +| [sku\_name](#input\_sku\_name) | The SKU name for the PostgreSQL Flexible Server (tier + size, e.g. B\_Standard\_B1ms, GP\_Standard\_D2s\_v3). | `string` | `"B_Standard_B1ms"` | no | +| [storage\_mb](#input\_storage\_mb) | Storage size in MB. Must be one of the sizes supported by Azure Database for PostgreSQL Flexible Server (e.g. 32768, 65536, 131072). | `number` | `32768` | no | ## Outputs | Name | Description | |------|-------------| -| [postgresql\_admin\_username](#output\_postgresql\_admin\_username) | The administrator username for PostgreSQL | -| [postgresql\_fqdn](#output\_postgresql\_fqdn) | The fully qualified domain name of the PostgreSQL server | -| [postgresql\_server\_name](#output\_postgresql\_server\_name) | The name of the PostgreSQL server | -| [postgresql\_version](#output\_postgresql\_version) | The PostgreSQL version | -| [psql\_admin\_password](#output\_psql\_admin\_password) | The administrator password for PostgreSQL | -| [resource\_group\_name](#output\_resource\_group\_name) | The name of the resource group in which the PostgreSQL database is created | +| [postgresql\_admin\_username](#output\_postgresql\_admin\_username) | The administrator username for the PostgreSQL Flexible Server. | +| [postgresql\_fqdn](#output\_postgresql\_fqdn) | The fully qualified domain name of the PostgreSQL Flexible Server. | +| [postgresql\_server\_id](#output\_postgresql\_server\_id) | The Azure resource ID of the PostgreSQL Flexible Server. | +| [postgresql\_server\_name](#output\_postgresql\_server\_name) | The name of the PostgreSQL Flexible Server. | +| [postgresql\_version](#output\_postgresql\_version) | The PostgreSQL major version. | +| [psql\_admin\_password](#output\_psql\_admin\_password) | The administrator password for the PostgreSQL Flexible Server. | +| [resource\_group\_name](#output\_resource\_group\_name) | The name of the resource group in which the PostgreSQL Flexible Server is created. | diff --git a/modules/azure/postgresql/buildingblock/main.tf b/modules/azure/postgresql/buildingblock/main.tf index 4223cf68..3afe93d9 100644 --- a/modules/azure/postgresql/buildingblock/main.tf +++ b/modules/azure/postgresql/buildingblock/main.tf @@ -1,20 +1,27 @@ +resource "random_string" "resource_code" { + length = 5 + special = false + upper = false +} + resource "random_password" "psql_admin_password" { - length = 16 - special = true + length = 20 + special = true + override_special = "!#$%&*()-_=+[]{}" } -resource "azurerm_resource_group" "example" { - name = var.resource_group_name +resource "azurerm_resource_group" "postgresql" { + name = "rg-${var.postgresql_server_name}" location = var.location } -resource "azurerm_postgresql_server" "example" { - name = var.postgresql_server_name - location = azurerm_resource_group.example.location - resource_group_name = azurerm_resource_group.example.name +resource "azurerm_postgresql_flexible_server" "this" { + name = "${var.postgresql_server_name}-${random_string.resource_code.result}" + resource_group_name = azurerm_resource_group.postgresql.name + location = azurerm_resource_group.postgresql.location - administrator_login = var.administrator_login - administrator_login_password = random_password.psql_admin_password.result + administrator_login = var.administrator_login + administrator_password = random_password.psql_admin_password.result sku_name = var.sku_name version = var.postgresql_version @@ -22,9 +29,12 @@ resource "azurerm_postgresql_server" "example" { backup_retention_days = var.backup_retention_days geo_redundant_backup_enabled = var.geo_redundant_backup_enabled - auto_grow_enabled = var.auto_grow_enabled - public_network_access_enabled = var.public_network_access_enabled - ssl_enforcement_enabled = var.ssl_enforcement_enabled - ssl_minimal_tls_version_enforced = var.ssl_minimal_tls_version_enforced + public_network_access_enabled = var.public_network_access_enabled + + lifecycle { + # Availability zone is picked by Azure at creation time when not specified; + # ignore it so subsequent applies don't try to force the server into a new zone. + ignore_changes = [zone] + } } diff --git a/modules/azure/postgresql/buildingblock/outputs.tf b/modules/azure/postgresql/buildingblock/outputs.tf index f6478258..5f8fda4b 100644 --- a/modules/azure/postgresql/buildingblock/outputs.tf +++ b/modules/azure/postgresql/buildingblock/outputs.tf @@ -1,30 +1,35 @@ +output "postgresql_server_id" { + description = "The Azure resource ID of the PostgreSQL Flexible Server." + value = azurerm_postgresql_flexible_server.this.id +} + output "postgresql_server_name" { - description = "The name of the PostgreSQL server" - value = azurerm_postgresql_server.example.name + description = "The name of the PostgreSQL Flexible Server." + value = azurerm_postgresql_flexible_server.this.name } output "postgresql_fqdn" { - description = "The fully qualified domain name of the PostgreSQL server" - value = azurerm_postgresql_server.example.fqdn + description = "The fully qualified domain name of the PostgreSQL Flexible Server." + value = azurerm_postgresql_flexible_server.this.fqdn } output "postgresql_admin_username" { - description = "The administrator username for PostgreSQL" - value = azurerm_postgresql_server.example.administrator_login + description = "The administrator username for the PostgreSQL Flexible Server." + value = azurerm_postgresql_flexible_server.this.administrator_login } output "postgresql_version" { - description = "The PostgreSQL version" - value = azurerm_postgresql_server.example.version + description = "The PostgreSQL major version." + value = azurerm_postgresql_flexible_server.this.version } output "psql_admin_password" { - description = "The administrator password for PostgreSQL" + description = "The administrator password for the PostgreSQL Flexible Server." value = random_password.psql_admin_password.result sensitive = true } output "resource_group_name" { - description = "The name of the resource group in which the PostgreSQL database is created" - value = azurerm_resource_group.example.name + description = "The name of the resource group in which the PostgreSQL Flexible Server is created." + value = azurerm_resource_group.postgresql.name } diff --git a/modules/azure/postgresql/buildingblock/variables.tf b/modules/azure/postgresql/buildingblock/variables.tf index 15fc772a..4d2f517d 100644 --- a/modules/azure/postgresql/buildingblock/variables.tf +++ b/modules/azure/postgresql/buildingblock/variables.tf @@ -1,80 +1,57 @@ -variable "subscription_id" { - description = "the Azure subscription id" - type = string -} - -variable "resource_group_name" { - description = "Name of the Azure resource group" +variable "postgresql_server_name" { + description = "Name prefix for the PostgreSQL Flexible Server. A random 5-character suffix is appended to ensure global uniqueness. Only lowercase letters, numbers and hyphens are allowed." type = string + nullable = false + validation { + condition = can(regex("^[a-z0-9-]{3,57}$", var.postgresql_server_name)) + error_message = "Only lowercase letters, numbers and hyphens are allowed, between 3 and 57 characters (a 6-character suffix is appended, keeping the final name within Azure's 63-character limit)." + } } variable "location" { - description = "Azure region" - type = string - default = "West Europe" -} - -variable "postgresql_server_name" { - description = "Name of the PostgreSQL server" + description = "Azure region where the PostgreSQL Flexible Server is created." type = string + default = "germanywestcentral" } variable "administrator_login" { - description = "Administrator username for PostgreSQL" + description = "Administrator username for the PostgreSQL Flexible Server." type = string default = "psqladmin" } variable "sku_name" { - description = "The SKU name for the PostgreSQL server" + description = "The SKU name for the PostgreSQL Flexible Server (tier + size, e.g. B_Standard_B1ms, GP_Standard_D2s_v3)." type = string - default = "B_Gen5_1" + default = "B_Standard_B1ms" } variable "postgresql_version" { - description = "PostgreSQL version" + description = "PostgreSQL major version." type = string - default = "11" + default = "16" } variable "storage_mb" { - description = "Storage size in MB" + description = "Storage size in MB. Must be one of the sizes supported by Azure Database for PostgreSQL Flexible Server (e.g. 32768, 65536, 131072)." type = number - default = 5120 + default = 32768 } variable "backup_retention_days" { - description = "Backup retention in days" + description = "Backup retention in days (7-35)." type = number default = 7 } variable "geo_redundant_backup_enabled" { - description = "Enable geo-redundant backups" - type = bool - default = false -} - -variable "auto_grow_enabled" { - description = "Enable auto-grow for storage" + description = "Enable geo-redundant backups." type = bool default = false } variable "public_network_access_enabled" { - description = "Enable public network access" - type = bool - default = false -} - -variable "ssl_enforcement_enabled" { - description = "Enforce SSL connection" + description = "Enable public network access. Disabling requires VNet integration (delegated subnet), which is out of scope for this building block." type = bool default = true } - -variable "ssl_minimal_tls_version_enforced" { - description = "Minimum TLS version" - type = string - default = "TLS1_2" -} diff --git a/modules/azure/postgresql/buildingblock/versions.tf b/modules/azure/postgresql/buildingblock/versions.tf index 753b01db..e873ef95 100644 --- a/modules/azure/postgresql/buildingblock/versions.tf +++ b/modules/azure/postgresql/buildingblock/versions.tf @@ -1,14 +1,12 @@ terraform { - required_version = ">= 1.5.0" - required_providers { azurerm = { source = "hashicorp/azurerm" - version = ">= 4.22.0" + version = ">= 4.64" } random = { source = "hashicorp/random" - version = ">= 3.7.1" + version = ">= 3.8" } } } diff --git a/modules/azure/postgresql/e2e/main.tf b/modules/azure/postgresql/e2e/main.tf new file mode 100644 index 00000000..c669d369 --- /dev/null +++ b/modules/azure/postgresql/e2e/main.tf @@ -0,0 +1,68 @@ +variable "test_context" { + type = object({ + hub_git_ref = string + workspace = string + name_suffix = string + + fixtures = object({ + azure = object({ + subscription_uuid = string + entra_tenant_id = string + }) + }) + }) + + nullable = false +} + +locals { + # Derive the scope from the subscription ID — role definitions are scoped to the subscription. + azure_scope = "/subscriptions/${var.test_context.fixtures.azure.subscription_uuid}" + + # postgresql_server_name must match ^[a-z0-9-]{3,57}$ — the building block appends a 6-char suffix. + # name_suffix is "YYYYMMDDhhmmss" (14 digits), so "pg-" + 14 digits = 17 chars total. + postgresql_server_name = "pg-${var.test_context.name_suffix}" +} + +module "postgresql" { + source = "../" + + meshstack = { + owning_workspace_identifier = var.test_context.workspace + tags = {} + } + + hub = { + git_ref = var.test_context.hub_git_ref + bbd_draft = true + } + + azure_tenant_id = var.test_context.fixtures.azure.entra_tenant_id + azure_subscription_id = var.test_context.fixtures.azure.subscription_uuid + azure_scope = local.azure_scope + + # Unique backplane name per test run so role definitions don't clash across concurrent/retried runs. + backplane_name = "hub-e2e-pg-${var.test_context.name_suffix}" +} + +resource "meshstack_building_block" "this" { + # depend on the entire backplane to force correct resource ordering at the module boundary, + # not just individual resources in the backplane + depends_on = [module.postgresql] + + wait_for_completion = true + + spec = { + building_block_definition_version_ref = module.postgresql.building_block_definition.version_ref + + display_name = "smoke-test-postgresql-${var.test_context.name_suffix}" + target_ref = { + kind = "meshWorkspace" + name = var.test_context.workspace + } + + inputs = { + postgresql_server_name = { value = jsonencode(local.postgresql_server_name) } + } + } +} diff --git a/modules/azure/postgresql/e2e/provider.tf b/modules/azure/postgresql/e2e/provider.tf new file mode 100644 index 00000000..69325904 --- /dev/null +++ b/modules/azure/postgresql/e2e/provider.tf @@ -0,0 +1,6 @@ +provider "azurerm" { + subscription_id = var.test_context.fixtures.azure.subscription_uuid + tenant_id = var.test_context.fixtures.azure.entra_tenant_id + + features {} +} diff --git a/modules/azure/postgresql/e2e/terraform.tf b/modules/azure/postgresql/e2e/terraform.tf new file mode 100644 index 00000000..39e32bf6 --- /dev/null +++ b/modules/azure/postgresql/e2e/terraform.tf @@ -0,0 +1,12 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + meshstack = { + source = "meshcloud/meshstack" + } + azurerm = { + source = "hashicorp/azurerm" + } + } +} diff --git a/modules/azure/postgresql/e2e/tests/azure_postgresql_hub.tftest.hcl b/modules/azure/postgresql/e2e/tests/azure_postgresql_hub.tftest.hcl new file mode 100644 index 00000000..3fa57ebd --- /dev/null +++ b/modules/azure/postgresql/e2e/tests/azure_postgresql_hub.tftest.hcl @@ -0,0 +1,29 @@ +run "azure_postgresql_hub" { + assert { + condition = meshstack_building_block.this.status.status == "SUCCEEDED" + error_message = "azure/postgresql hub building block expected SUCCEEDED, got ${meshstack_building_block.this.status.status}" + } + + assert { + condition = can(regex( + "^/subscriptions/[^/]+/resourceGroups/[^/]+/providers/Microsoft\\.DBforPostgreSQL/flexibleServers/", + jsondecode(meshstack_building_block.this.status.outputs["postgresql_server_id"].value) + )) + error_message = "expected postgresql_server_id to be a valid Azure PostgreSQL Flexible Server resource ID, got ${jsondecode(meshstack_building_block.this.status.outputs["postgresql_server_id"].value)}" + } + + assert { + condition = startswith(jsondecode(meshstack_building_block.this.status.outputs["postgresql_server_name"].value), "pg-") + error_message = "expected postgresql_server_name to start with 'pg-', got ${jsondecode(meshstack_building_block.this.status.outputs["postgresql_server_name"].value)}" + } + + assert { + condition = endswith(jsondecode(meshstack_building_block.this.status.outputs["postgresql_fqdn"].value), ".postgres.database.azure.com") + error_message = "expected postgresql_fqdn to end with '.postgres.database.azure.com', got ${jsondecode(meshstack_building_block.this.status.outputs["postgresql_fqdn"].value)}" + } + + assert { + condition = startswith(jsondecode(meshstack_building_block.this.status.outputs["resource_group_name"].value), "rg-pg-") + error_message = "expected resource_group_name to start with 'rg-pg-', got ${jsondecode(meshstack_building_block.this.status.outputs["resource_group_name"].value)}" + } +} diff --git a/modules/azure/postgresql/meshstack_integration.tf b/modules/azure/postgresql/meshstack_integration.tf new file mode 100644 index 00000000..c17d526a --- /dev/null +++ b/modules/azure/postgresql/meshstack_integration.tf @@ -0,0 +1,238 @@ +variable "azure_tenant_id" { + type = string + description = "Azure Entra tenant ID where the PostgreSQL Flexible Server will be deployed." +} + +variable "azure_subscription_id" { + type = string + description = "Azure subscription ID where the PostgreSQL Flexible Server will be deployed." +} + +variable "azure_scope" { + type = string + description = "Azure management group or subscription ID used for backplane role scope." +} + +variable "azure_location" { + type = string + default = "germanywestcentral" + description = "Default Azure region where the PostgreSQL Flexible Server will be created." +} + +variable "backplane_name" { + type = string + default = "azure-postgresql" + description = "Name for the backplane resources (managed identity, role definition). Must match pattern ^[-a-z0-9]+$." +} + +variable "notification_subscribers" { + type = list(string) + default = [] + description = "List of email addresses to notify on building block lifecycle events." +} + +variable "meshstack" { + type = object({ + owning_workspace_identifier = string + tags = optional(map(list(string)), {}) + }) + description = "Shared meshStack context. Tags are optional and propagated to building block definition metadata." +} + +variable "hub" { + type = object({ + git_ref = optional(string, "main") + bbd_draft = optional(bool, true) + }) + const = true + default = { + git_ref = "main" + bbd_draft = true + } + description = <<-EOT + `git_ref`: Hub release reference. Set to a tag (e.g. 'v1.2.3') or branch or commit sha of the meshstack-hub repo. + `bbd_draft`: If true, the building block definition version is kept in draft mode, which allows changing it (useful during development in LCF/ICF). + EOT +} + +output "building_block_definition" { + description = "BBD is consumed in building block compositions." + value = { + uuid = meshstack_building_block_definition.this.metadata.uuid + version_ref = var.hub.bbd_draft ? meshstack_building_block_definition.this.version_latest : meshstack_building_block_definition.this.version_latest_release + } +} + +data "meshstack_integrations" "integrations" {} + +module "backplane" { + source = "github.com/meshcloud/meshstack-hub//modules/azure/postgresql/backplane?ref=${var.hub.git_ref}" + + name = var.backplane_name + scope = var.azure_scope + location = var.azure_location + + workload_identity_federation = { + issuer = data.meshstack_integrations.integrations.workload_identity_federation.replicator.issuer + subjects = [ + "${trimsuffix(data.meshstack_integrations.integrations.workload_identity_federation.replicator.subject, ":replicator")}:workspace.${var.meshstack.owning_workspace_identifier}.buildingblockdefinition.${meshstack_building_block_definition.this.metadata.uuid}" + ] + } +} + +resource "meshstack_building_block_definition" "this" { + metadata = { + owned_by_workspace = var.meshstack.owning_workspace_identifier + tags = var.meshstack.tags + } + + spec = { + display_name = "Azure PostgreSQL" + description = "Provisions a managed Azure Database for PostgreSQL Flexible Server in the target Azure subscription." + support_url = "mailto:support@meshcloud.io" + documentation_url = "https://hub.meshcloud.io/platforms/azure/definitions/azure-postgresql" + notification_subscribers = var.notification_subscribers + symbol = "https://raw.githubusercontent.com/meshcloud/meshstack-hub/main/modules/azure/postgresql/buildingblock/logo.png" + target_type = "WORKSPACE_LEVEL" + + readme = chomp(<<-EOT + This building block provisions a managed **Azure Database for PostgreSQL Flexible Server** in your Azure subscription, providing a fully managed, scalable, and secure relational database service. + + ## 🎯 When to use it + + Use this building block when your application needs a reliable, managed PostgreSQL database without the operational overhead of self-hosting — for structured data storage, transactional workloads, or as a backend for reporting and analytics. + + ## 📝 What you get + + A dedicated resource group and a PostgreSQL Flexible Server with a generated administrator password (exposed as a sensitive output), sensible defaults for SKU, version, storage, and backups, and a globally-unique server name. + + ## Shared Responsibilities + + | Responsibility | Platform Team | Application Team | + | ----------------------------------------------------- | :-----------: | :--------------: | + | Provision and configure the PostgreSQL server | ✅ | ❌ | + | Enforce security policies (encryption, TLS, backups) | ✅ | ❌ | + | Manage database backups and disaster recovery | ✅ | ❌ | + | Choose the server name and region | ❌ | ✅ | + | Create and manage database schemas and tables | ❌ | ✅ | + | Application-level performance tuning and query design | ❌ | ✅ | + EOT + ) + } + + version_spec = { + draft = var.hub.bbd_draft + + deletion_mode = "DELETE" + + implementation = { + terraform = { + terraform_version = "1.9.0" + repository_url = "https://github.com/meshcloud/meshstack-hub.git" + repository_path = "modules/azure/postgresql/buildingblock" + ref_name = var.hub.git_ref + use_mesh_http_backend_fallback = true + } + } + + inputs = { + ARM_CLIENT_ID = { + type = "STRING" + display_name = "ARM Client ID" + description = "Client ID of the managed identity used to authenticate with Azure." + assignment_type = "STATIC" + is_environment = true + argument = jsonencode(module.backplane.identity.client_id) + } + ARM_TENANT_ID = { + type = "STRING" + display_name = "ARM Tenant ID" + description = "Azure Entra tenant ID for authentication." + assignment_type = "STATIC" + is_environment = true + argument = jsonencode(var.azure_tenant_id) + } + ARM_SUBSCRIPTION_ID = { + type = "STRING" + display_name = "Azure Subscription ID" + description = "The Azure subscription ID where the PostgreSQL server will be deployed." + assignment_type = "STATIC" + is_environment = true + argument = jsonencode(var.azure_subscription_id) + } + ARM_USE_OIDC = { + type = "STRING" + display_name = "ARM Use OIDC" + description = "Enables OIDC-based workload identity federation for the Azure provider." + assignment_type = "STATIC" + is_environment = true + argument = jsonencode("true") + } + ARM_OIDC_TOKEN_FILE_PATH = { + type = "STRING" + display_name = "ARM OIDC Token File Path" + description = "Path to the OIDC token file used for workload identity federation authentication." + assignment_type = "STATIC" + is_environment = true + argument = jsonencode("/var/run/secrets/workload-identity/azure/token") + } + postgresql_server_name = { + type = "STRING" + display_name = "PostgreSQL Server Name" + description = "A name prefix for the PostgreSQL Flexible Server. A random 5-character suffix will be appended to ensure global uniqueness. Only lowercase letters, numbers and hyphens, 3–57 characters." + assignment_type = "USER_INPUT" + value_validation_regex = "^[a-z0-9-]{3,57}$" + validation_regex_error_message = "Only lowercase letters, numbers and hyphens are allowed, between 3 and 57 characters (a 6-character suffix will be appended, keeping the final name within Azure's 63-character limit)." + } + location = { + type = "STRING" + display_name = "Location" + description = "The Azure region where the PostgreSQL server will be created." + assignment_type = "STATIC" + argument = jsonencode(var.azure_location) + } + } + + outputs = { + postgresql_server_id = { + type = "STRING" + display_name = "PostgreSQL Server ID" + description = "The Azure resource ID of the created PostgreSQL Flexible Server." + assignment_type = "NONE" + } + postgresql_server_name = { + type = "STRING" + display_name = "PostgreSQL Server Name" + description = "The name of the created PostgreSQL Flexible Server." + assignment_type = "NONE" + } + postgresql_fqdn = { + type = "STRING" + display_name = "PostgreSQL FQDN" + description = "The fully qualified domain name of the PostgreSQL Flexible Server." + assignment_type = "NONE" + } + resource_group_name = { + type = "STRING" + display_name = "Resource Group" + description = "The name of the resource group containing the PostgreSQL server." + assignment_type = "NONE" + } + } + } +} + +terraform { + required_version = ">= 1.12.0" + + required_providers { + meshstack = { + source = "meshcloud/meshstack" + version = ">= 0.21.0" + } + azurerm = { + source = "hashicorp/azurerm" + version = ">= 4.64" + } + } +}