diff --git a/modules/azure/storage-account/backplane/README.md b/modules/azure/storage-account/backplane/README.md index 1f7a1fd1..283078cd 100644 --- a/modules/azure/storage-account/backplane/README.md +++ b/modules/azure/storage-account/backplane/README.md @@ -108,8 +108,8 @@ module "storage_account_backplane" { | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0 | -| [azuread](#requirement\_azuread) | ~> 3.7.0 | -| [azurerm](#requirement\_azurerm) | 3.116.0 | +| [azuread](#requirement\_azuread) | ~> 3.8 | +| [azurerm](#requirement\_azurerm) | ~> 4.64 | ## Modules @@ -123,9 +123,9 @@ No modules. | [azuread_application_federated_identity_credential.buildingblock_deploy](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application_federated_identity_credential) | resource | | [azuread_application_password.buildingblock_deploy](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application_password) | resource | | [azuread_service_principal.buildingblock_deploy](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/service_principal) | resource | -| [azurerm_role_assignment.created_principal](https://registry.terraform.io/providers/hashicorp/azurerm/3.116.0/docs/resources/role_assignment) | resource | -| [azurerm_role_assignment.existing_principals](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_role_assignment.created_principal](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_role_assignment.existing_principals](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_role_definition.buildingblock_deploy](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_definition) | resource | ## Inputs diff --git a/modules/azure/storage-account/backplane/main.tf b/modules/azure/storage-account/backplane/main.tf index 44e93d18..cf3533f8 100644 --- a/modules/azure/storage-account/backplane/main.tf +++ b/modules/azure/storage-account/backplane/main.tf @@ -14,11 +14,15 @@ resource "azuread_service_principal" "buildingblock_deploy" { # Create federated identity credentials (one per subject) +# Use a map with static numeric string keys so that for_each keys are known at plan time, +# even when subject values contain apply-time unknowns (e.g. building block definition UUIDs). resource "azuread_application_federated_identity_credential" "buildingblock_deploy" { - for_each = var.create_service_principal_name != null && var.workload_identity_federation != null ? toset(var.workload_identity_federation.subjects) : toset([]) + for_each = var.create_service_principal_name != null && var.workload_identity_federation != null ? { + for i, s in var.workload_identity_federation.subjects : tostring(i) => s + } : {} application_id = azuread_application.buildingblock_deploy[0].id - display_name = reverse(split(":", each.value))[0] + display_name = "subject-${each.key}" audiences = ["api://AzureADTokenExchange"] issuer = var.workload_identity_federation.issuer subject = each.value diff --git a/modules/azure/storage-account/backplane/versions.tf b/modules/azure/storage-account/backplane/versions.tf index a6b8fd3c..630c0652 100644 --- a/modules/azure/storage-account/backplane/versions.tf +++ b/modules/azure/storage-account/backplane/versions.tf @@ -4,11 +4,11 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = "3.116.0" + version = "~> 4.64" } azuread = { source = "hashicorp/azuread" - version = "~> 3.7.0" + version = "~> 3.8" } } } diff --git a/modules/azure/storage-account/buildingblock/README.md b/modules/azure/storage-account/buildingblock/README.md index 880c40ef..3a5bf4d2 100644 --- a/modules/azure/storage-account/buildingblock/README.md +++ b/modules/azure/storage-account/buildingblock/README.md @@ -37,9 +37,9 @@ provider "azurerm" { | Name | Version | |------|---------| -| [azuread](#requirement\_azuread) | 3.1.0 | -| [azurerm](#requirement\_azurerm) | 4.18.0 | -| [random](#requirement\_random) | 3.6.3 | +| [azuread](#requirement\_azuread) | ~> 3.8 | +| [azurerm](#requirement\_azurerm) | ~> 4.64 | +| [random](#requirement\_random) | ~> 3.8 | ## Modules @@ -49,11 +49,11 @@ No modules. | Name | Type | |------|------| -| [azurerm_resource_group.storage_account_rg](https://registry.terraform.io/providers/hashicorp/azurerm/4.18.0/docs/resources/resource_group) | resource | -| [azurerm_storage_account.storage_account](https://registry.terraform.io/providers/hashicorp/azurerm/4.18.0/docs/resources/storage_account) | resource | -| [random_string.resource_code](https://registry.terraform.io/providers/hashicorp/random/3.6.3/docs/resources/string) | resource | -| [azurerm_client_config.current](https://registry.terraform.io/providers/hashicorp/azurerm/4.18.0/docs/data-sources/client_config) | data source | -| [azurerm_subscription.current](https://registry.terraform.io/providers/hashicorp/azurerm/4.18.0/docs/data-sources/subscription) | data source | +| [azurerm_resource_group.storage_account_rg](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group) | resource | +| [azurerm_storage_account.storage_account](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account) | resource | +| [random_string.resource_code](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string) | resource | +| [azurerm_client_config.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/client_config) | data source | +| [azurerm_subscription.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/subscription) | data source | ## Inputs @@ -61,7 +61,6 @@ No modules. |------|-------------|------|---------|:--------:| | [location](#input\_location) | The location/region where the storage account is created. | `string` | n/a | yes | | [storage\_account\_name](#input\_storage\_account\_name) | The name of the storage account. Must be unique across entire Azure Region, not just within a Subscription. | `string` | n/a | yes | -| [storage\_account\_resource\_group\_name](#input\_storage\_account\_resource\_group\_name) | The name of the resource group containing the storage account. | `string` | n/a | yes | ## Outputs diff --git a/modules/azure/storage-account/buildingblock/main.tf b/modules/azure/storage-account/buildingblock/main.tf index 7773a0b7..938cf847 100644 --- a/modules/azure/storage-account/buildingblock/main.tf +++ b/modules/azure/storage-account/buildingblock/main.tf @@ -9,7 +9,7 @@ resource "random_string" "resource_code" { } resource "azurerm_resource_group" "storage_account_rg" { - name = var.storage_account_resource_group_name + name = "rg-${var.storage_account_name}" location = var.location } diff --git a/modules/azure/storage-account/buildingblock/variables.tf b/modules/azure/storage-account/buildingblock/variables.tf index 128cf455..a64caa25 100644 --- a/modules/azure/storage-account/buildingblock/variables.tf +++ b/modules/azure/storage-account/buildingblock/variables.tf @@ -4,12 +4,6 @@ variable "storage_account_name" { description = "The name of the storage account. Must be unique across entire Azure Region, not just within a Subscription." } -variable "storage_account_resource_group_name" { - type = string - nullable = false - description = "The name of the resource group containing the storage account." -} - variable "location" { type = string description = "The location/region where the storage account is created." diff --git a/modules/azure/storage-account/buildingblock/versions.tf b/modules/azure/storage-account/buildingblock/versions.tf index b44089e0..9441fdee 100644 --- a/modules/azure/storage-account/buildingblock/versions.tf +++ b/modules/azure/storage-account/buildingblock/versions.tf @@ -2,15 +2,15 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = "4.18.0" + version = "~> 4.64" } azuread = { source = "hashicorp/azuread" - version = "3.1.0" + version = "~> 3.8" } random = { source = "hashicorp/random" - version = "3.6.3" + version = "~> 3.8" } } } diff --git a/modules/azure/storage-account/defintion/definition.json b/modules/azure/storage-account/defintion/definition.json deleted file mode 100644 index 77cb0918..00000000 --- a/modules/azure/storage-account/defintion/definition.json +++ /dev/null @@ -1,69 +0,0 @@ -{ - "apiVersion": "v1", - "metadata": { - "uuid": null, - "markedForDeletionOn": null, - "markedForDeletionBy": null, - "tags": { - "environment": ["dev"], - "team": ["devops"] - }, - "ownedByWorkspace": "workspace-123" - }, - "spec": { - "displayName": "Example Building Block", - "symbol": "EBB", - "description": "This is an example building block definition.", - "supportedPlatforms": ["aws", "azure"], - "useInLandingZonesOnly": false, - "supportUrl": "https://example.com/support", - "documentationUrl": "https://example.com/docs", - "latestVersion": { - "number": 1, - "onlyApplyOncePerTenant": true, - "deletionMode": "DELETE", - "implementation": { - "terraform": { - "terraformVersion": "0.14.7", - "repositoryUrl": "https://github.com/example/terraform-repo", - "async": false, - "repositoryPath": "path/to/module", - "sshPrivateKey": "private-key", - "refName": "main", - "knownHost": null, - "useMeshHttpBackendFallback": false - } - }, - "inputs": [ - { - "inputKey": "example-input", - "displayName": "Example Input", - "type": "STRING", - "assignmentType": "USER_INPUT", - "argument": null, - "isEnvironment": false, - "isSensitive": false, - "updateableByConsumer": true, - "selectableValues": ["value1", "value2"], - "defaultValue": "value1", - "description": "An example input.", - "inputValueValidationRegex": "^[a-zA-Z0-9]+$", - "validationRegexErrorMessage": "Only alphanumeric characters are allowed." - } - ], - "outputs": [ - { - "outputKey": "example-output", - "displayName": "Example Output", - "type": "STRING", - "assignmentType": "NONE" - } - ], - "state": "DRAFT", - "dependencies": ["dependency1", "dependency2"] - }, - "notificationSubscriber": ["user1", "user2"] - }, - "kind": "MeshBuildingBlockDefinition", - "meaningfulIdentifier": "meshBuildingBlockDefinition[Example Building Block (123e4567-e89b-12d3-a456-426614174000)]" -} diff --git a/modules/azure/storage-account/meshstack_integration.tf b/modules/azure/storage-account/meshstack_integration.tf new file mode 100644 index 00000000..5863679e --- /dev/null +++ b/modules/azure/storage-account/meshstack_integration.tf @@ -0,0 +1,222 @@ +variable "hub" { + type = object({ + git_ref = optional(string, "main") + bbd_draft = optional(bool, true) + }) + default = {} + 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 +} + +variable "meshstack" { + type = object({ + owning_workspace_identifier = string + }) + description = "`owning_workspace_identifier`: Identifier of the workspace that owns the building block." +} + +# Retrieves the workload identity federation configuration from meshStack. +# The building block runners share the same OIDC issuer and namespace prefix as meshStack integrations. +# For self-hosted runners running outside our cluster, this does not hold true. +data "meshstack_integrations" "integrations" {} + +variable "azure" { + type = object({ + tenant_id = string + subscription_id = string + scope = string + location = optional(string, "germanywestcentral") + }) + description = <<-EOT + `tenant_id`: Azure Entra tenant ID where the storage accounts will be deployed. + `subscription_id`: Azure subscription ID where storage accounts will be deployed. + `scope`: Azure management group or subscription ID used as the scope for the backplane role definition and assignment. + `location`: Default Azure region where storage accounts will be created (e.g. 'germanywestcentral'). + EOT +} + +variable "backplane_name" { + type = string + default = "azure-storage-account" + description = "Name for the backplane resources (service principal, 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." +} + +module "backplane" { + source = "github.com/meshcloud/meshstack-hub//modules/azure/storage-account/backplane?ref=${var.hub.git_ref}" + + name = var.backplane_name + scope = var.azure.scope + + create_service_principal_name = var.backplane_name + + 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 + } + + spec = { + display_name = "Azure Storage Account" + description = "Provisions an Azure Storage Account as a highly scalable, durable, and secure container in the target Azure subscription." + support_url = "mailto:support@meshcloud.io" + documentation_url = "https://hub.meshcloud.io/platforms/azure/definitions/azure-storage-account" + notification_subscribers = var.notification_subscribers + symbol = "https://raw.githubusercontent.com/meshcloud/meshstack-hub/main/modules/azure/storage-account/buildingblock/logo.png" + target_type = "WORKSPACE_LEVEL" + + readme = chomp(<<-EOT + ## Azure Storage Account + + This building block provisions an **Azure Storage Account** in your Azure subscription, providing scalable and durable cloud storage for blobs, files, queues, and tables. + + ## When to use it? + + Use this building block when you need a managed Azure Storage Account with consistent naming, resource group organisation, and a pre-configured lifecycle policy. + + ## Shared Responsibilities + + | Responsibility | Platform Team | Application Team | + | ------------------------------------------- | :-----------: | :--------------: | + | Provision and configure storage account | ✅ | ❌ | + | Manage storage account lifecycle | ✅ | ❌ | + | Choose storage account name and region | ❌ | ✅ | + | Manage data stored in the storage account | ❌ | ✅ | + | Define access policies for stored data | ❌ | ✅ | + 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/storage-account/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 service principal used to authenticate with Azure." + assignment_type = "STATIC" + is_environment = true + argument = jsonencode(module.backplane.created_service_principal.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 storage account 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") + } + storage_account_name = { + type = "STRING" + display_name = "Storage Account Name" + description = "A name prefix for the storage account. A random 5-character suffix will be appended to ensure uniqueness (e.g. 'myapp' becomes 'myappx7k2q'). Only lowercase letters and numbers, 3–19 characters." + assignment_type = "USER_INPUT" + value_validation_regex = "^[a-z0-9]{3,19}$" + validation_regex_error_message = "Only lowercase letters and numbers are allowed, between 3 and 19 characters (a 5-character suffix will be appended, keeping the final name within Azure's 24-character limit)." + } + location = { + type = "STRING" + display_name = "Location" + description = "The Azure region where the storage account will be created." + assignment_type = "STATIC" + argument = jsonencode(var.azure.location) + } + } + + outputs = { + storage_account_id = { + type = "STRING" + display_name = "Storage Account ID" + description = "The Azure resource ID of the created storage account." + assignment_type = "NONE" + } + storage_account_name = { + type = "STRING" + display_name = "Storage Account Name" + description = "The name of the created storage account." + assignment_type = "NONE" + } + storage_account_resource_group = { + type = "STRING" + display_name = "Resource Group" + description = "The name of the resource group containing the storage account." + assignment_type = "NONE" + } + } + } +} + +output "building_block_definition_version_uuid" { + description = "UUID of the latest version. In draft mode returns the latest draft; otherwise returns the latest release." + value = var.hub.bbd_draft ? meshstack_building_block_definition.this.version_latest.uuid : meshstack_building_block_definition.this.version_latest_release.uuid +} + +terraform { + required_version = ">= 1.11.0" + + required_providers { + meshstack = { + source = "meshcloud/meshstack" + version = "~> 0.19.3" + } + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.64" + } + azuread = { + source = "hashicorp/azuread" + version = "~> 3.8" + } + } +}