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"
+ }
+ }
+}