diff --git a/docs/content/specs-defs/includes/terraform/shared/functional/TFFR3.md b/docs/content/specs-defs/includes/terraform/shared/functional/TFFR3.md index 2efa5fe9e..84ced0a61 100644 --- a/docs/content/specs-defs/includes/terraform/shared/functional/TFFR3.md +++ b/docs/content/specs-defs/includes/terraform/shared/functional/TFFR3.md @@ -23,30 +23,30 @@ priority: 20030 Authors **MUST** only use the following Azure providers, and versions, in their modules: -| provider | min version | max version | -|----------|-------------|-------------| -| azapi | >= 2.0 | < 3.0 | -| azurerm | >= 4.0 | < 5.0 | - -{{% notice style="note" %}} -Authors **MAY** select either Azurerm, Azapi, or both providers in their module. -{{% /notice %}} +| provider | min version | max version | +|-----------------------|-------------|-------------| +| Azure/azapi | >= 2.0 | < 3.0 | + +> The AzureRM provider is permitted for module versions prior to v1.0.0, but **MUST NOT** be used in module versions v1.0.0 and later. +> Should your module use the AzureRM provider, you **MUST** use version 4.x of the provider, i.e., `~> 4.0`. You MAY also create an exclusion for the TFLint rule: +> +> ```hcl +> rule "provider_azurerm_disallowed" { +> enabled = false +> } +> ``` Authors **MUST** use the `required_providers` block in their module to enforce the provider versions. The following is an example. - In it we use the [pessimistic version constraint operator](https://developer.hashicorp.com/terraform/language/expressions/version-constraints#operators) `~>`. -- That is to say that `~> 4.0` is equivalent to `>= 4.0, < 5.0`. +- That is to say that `~> 2.0` is equivalent to `>= 2.0, < 3.0`. ```terraform terraform { required_providers { # Include one or both providers, as needed - azurerm = { - source = "hashicorp/azurerm" - version = "~> 4.0" - } azapi = { source = "Azure/azapi" version = "~> 2.0" diff --git a/docs/content/specs-defs/includes/terraform/shared/functional/TFFR4.md b/docs/content/specs-defs/includes/terraform/shared/functional/TFFR4.md new file mode 100644 index 000000000..1608dc775 --- /dev/null +++ b/docs/content/specs-defs/includes/terraform/shared/functional/TFFR4.md @@ -0,0 +1,65 @@ +--- +title: TFFR4 - AzAPI - response_export_values +description: Module Specification for the Azure Verified Modules (AVM) program +url: /spec/TFFR4 +type: default +tags: [ + Class-Resource, # MULTIPLE VALUES: this can be "Class-Resource" AND/OR "Class-Pattern" AND/OR "Class-Utility" + Class-Pattern, # MULTIPLE VALUES: this can be "Class-Resource" AND/OR "Class-Pattern" AND/OR "Class-Utility" + Class-Utility, # MULTIPLE VALUES: this can be "Class-Resource" AND/OR "Class-Pattern" AND/OR "Class-Utility" + Type-Functional, # SINGLE VALUE: this can be "Type-Functional" OR "Type-NonFunctional" + Category-Naming/Composition, # SINGLE VALUE: this can be "Category-Testing" OR "Category-Telemetry" OR "Category-Contribution/Support" OR "Category-Documentation" OR "Category-CodeStyle" OR "Category-Naming/Composition" OR "Category-Inputs/Outputs" OR "Category-Release/Publishing" + Language-Terraform, # MULTIPLE VALUES: this can be "Language-Bicep" AND/OR "Language-Terraform" + Severity-MUST, # SINGLE VALUE: this can be "Severity-MUST" OR "Severity-SHOULD" OR "Severity-MAY" + Persona-Owner, # MULTIPLE VALUES: this can be "Persona-Owner" AND/OR "Persona-Contributor" + Persona-Contributor, # MULTIPLE VALUES: this can be "Persona-Owner" AND/OR "Persona-Contributor" + Lifecycle-BAU, # SINGLE VALUE: this can be "Lifecycle-Initial" OR "Lifecycle-BAU" OR "Lifecycle-EOL" + Validation-TF/CI/Enforced # SINGLE VALUE: this can be "Validation-TF/Manual" OR "Validation-TF/CI/Informational" OR "Validation-TF/CI/Enforced" +] +priority: 20040 +--- + +## ID: TFFR4 - Category: Composition - AzAPI - response_export_values + +Authors **MUST** specify the `response_export_values` argument when using the AzAPI provider: + +```terraform +resource "azapi_resource" "example" { + type = "Microsoft.Example/resourceType@2021-01-01" + name = "example-resource" + location = "West US" + response_export_values = [] # must be specified, even if empty + body = { + properties = { + exampleProperty = "exampleValue" + } + } +} + +If you require read-only properties to be returned from the resource, you SHOULD include them as follows: + +```terraform +resource "azapi_resource" "example" { + type = "Microsoft.Example/resourceType@2021-01-01" + name = "example-resource" + location = "West US" + # Example as a list: + response_export_values = ["properties.readOnlyProperty"] + # Example as a map: + # response_export_values = { + # read_only_property = "properties.readOnlyProperty" + # } + body = { + properties = { + exampleProperty = "exampleValue" + } + } +} + +output "read_only_property" { + # Example if response_export_values is a list: + value = azapi_resource.example.output.properties.readOnlyProperty + # Example if response_export_values is a map: + # value = azapi_resource.example.output.read_only_property +} +``` diff --git a/docs/content/specs-defs/includes/terraform/shared/functional/TFFR5.md b/docs/content/specs-defs/includes/terraform/shared/functional/TFFR5.md new file mode 100644 index 000000000..a07cd2ca4 --- /dev/null +++ b/docs/content/specs-defs/includes/terraform/shared/functional/TFFR5.md @@ -0,0 +1,41 @@ +--- +title: TFFR5 - AzAPI - replace_triggers_refs +description: Module Specification for the Azure Verified Modules (AVM) program +url: /spec/TFFR5 +type: default +tags: [ + Class-Resource, # MULTIPLE VALUES: this can be "Class-Resource" AND/OR "Class-Pattern" AND/OR "Class-Utility" + Class-Pattern, # MULTIPLE VALUES: this can be "Class-Resource" AND/OR "Class-Pattern" AND/OR "Class-Utility" + Class-Utility, # MULTIPLE VALUES: this can be "Class-Resource" AND/OR "Class-Pattern" AND/OR "Class-Utility" + Type-Functional, # SINGLE VALUE: this can be "Type-Functional" OR "Type-NonFunctional" + Category-Naming/Composition, # SINGLE VALUE: this can be "Category-Testing" OR "Category-Telemetry" OR "Category-Contribution/Support" OR "Category-Documentation" OR "Category-CodeStyle" OR "Category-Naming/Composition" OR "Category-Inputs/Outputs" OR "Category-Release/Publishing" + Language-Terraform, # MULTIPLE VALUES: this can be "Language-Bicep" AND/OR "Language-Terraform" + Severity-MUST, # SINGLE VALUE: this can be "Severity-MUST" OR "Severity-SHOULD" OR "Severity-MAY" + Persona-Owner, # MULTIPLE VALUES: this can be "Persona-Owner" AND/OR "Persona-Contributor" + Persona-Contributor, # MULTIPLE VALUES: this can be "Persona-Owner" AND/OR "Persona-Contributor" + Lifecycle-BAU, # SINGLE VALUE: this can be "Lifecycle-Initial" OR "Lifecycle-BAU" OR "Lifecycle-EOL" + Validation-TF/CI/Enforced # SINGLE VALUE: this can be "Validation-TF/Manual" OR "Validation-TF/CI/Informational" OR "Validation-TF/CI/Enforced" +] +priority: 20050 +--- + +## ID: TFFR5 - Category: Composition - AzAPI - replace_triggers_refs + +Authors **MUST** specify the `replace_triggers_refs` argument when using the AzAPI provider. The values should contain the body paths that would cause the resource to be replaced when they change. + +This is to ensure that changes to properties that require replacement of the resource are handled correctly by Terraform. + +```terraform +resource "azapi_resource" "example" { + type = "Microsoft.Example/resourceType@2021-01-01" + name = "example-resource" + location = "West US" + replace_triggers_refs = [ + "properties.exampleProperty" + ] # must be specified, even if empty + body = { + properties = { + exampleProperty = "exampleValue" + } + } +} diff --git a/docs/content/specs-defs/includes/terraform/shared/non-functional/TFNFR17.md b/docs/content/specs-defs/includes/terraform/shared/non-functional/TFNFR17.md index 1a973ae24..2bf92025a 100644 --- a/docs/content/specs-defs/includes/terraform/shared/non-functional/TFNFR17.md +++ b/docs/content/specs-defs/includes/terraform/shared/non-functional/TFNFR17.md @@ -33,9 +33,9 @@ variable "kubernetes_cluster_key_management_service" { key_vault_network_access = optional(string) }) default = null - description = <<-EOT + description = <<-DESCRIPTION - `key_vault_key_id` - (Required) Identifier of Azure Key Vault key. See [key identifier format](https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#vault-name-and-object-name) for more details. When Azure Key Vault key management service is enabled, this field is required and must be a valid key identifier. When `enabled` is `false`, leave the field empty. - `key_vault_network_access` - (Optional) Network access of the key vault Network access of key vault. The possible values are `Public` and `Private`. `Public` means the key vault allows public access from all networks. `Private` means the key vault disables public access and enables private link. Defaults to `Public`. -EOT +DESCRIPTION } ``` diff --git a/docs/content/specs-defs/includes/terraform/shared/non-functional/TFNFR18.md b/docs/content/specs-defs/includes/terraform/shared/non-functional/TFNFR18.md index 3f3d28b65..34c2c8871 100644 --- a/docs/content/specs-defs/includes/terraform/shared/non-functional/TFNFR18.md +++ b/docs/content/specs-defs/includes/terraform/shared/non-functional/TFNFR18.md @@ -20,7 +20,7 @@ priority: 21180 ## ID: TFNFR18 - Category: Code Style - Variables with Types -`type` **MUST** be defined for every `variable`. `type` **SHOULD** be as precise as possible, `any` **MAY** only be defined with adequate reasons. +`type` **MUST** be defined for every `variable`. `type` **SHOULD** be as precise as possible. Authors **SHOULD NOT** use `any`. - Use `bool` instead of `string` or `number` for `true/false` - Use `string` for text diff --git a/docs/content/specs-defs/includes/terraform/shared/non-functional/TFNFR23.md b/docs/content/specs-defs/includes/terraform/shared/non-functional/TFNFR23.md index 09a3ad367..8d5d81b73 100644 --- a/docs/content/specs-defs/includes/terraform/shared/non-functional/TFNFR23.md +++ b/docs/content/specs-defs/includes/terraform/shared/non-functional/TFNFR23.md @@ -20,4 +20,26 @@ priority: 21230 ## ID: TFNFR23 - Category: Code Style - Sensitive Default Value Conditions -A default value **MUST NOT** be set for a sensitive input - e.g., a default password. +A default value **MUST NOT** be set for a sensitive input, unless it is an empty collection value. + +Good example: + +```hcl +variable "example_map" { + type = map(string) + default = {} + description = "An example map variable with an empty default value." + sensitive = true +} +``` + +Bad example: + +```hcl +variable "example_string" { + type = string + default = "sensitive_value" + description = "An example string variable with a sensitive default value." + sensitive = true +} +``` diff --git a/docs/content/specs-defs/includes/terraform/shared/non-functional/TFNFR25.md b/docs/content/specs-defs/includes/terraform/shared/non-functional/TFNFR25.md index 5028c2cde..f24ae3fac 100644 --- a/docs/content/specs-defs/includes/terraform/shared/non-functional/TFNFR25.md +++ b/docs/content/specs-defs/includes/terraform/shared/non-functional/TFNFR25.md @@ -39,8 +39,8 @@ terraform { required_version = "~> 1.6" required_providers { azurerm = { - source = "hashicorp/azurerm" - version = "~> 3.11" + source = "Azure/azapi" + version = "~> 2.5" } } } diff --git a/docs/content/specs-defs/includes/terraform/shared/non-functional/TFNFR36.md b/docs/content/specs-defs/includes/terraform/shared/non-functional/TFNFR36.md index fe72612bf..ff0c73282 100644 --- a/docs/content/specs-defs/includes/terraform/shared/non-functional/TFNFR36.md +++ b/docs/content/specs-defs/includes/terraform/shared/non-functional/TFNFR36.md @@ -18,7 +18,7 @@ tags: [ priority: 21360 --- -## ID: TFNFR36 - Category: Code Style - Setting prevent_deletion_if_contains_resources +## ID: TFNFR36 - Category: Code Style - Setting prevent_deletion_if_contains_resources (AzureRM only) From Terraform AzureRM 3.0, the default value of `prevent_deletion_if_contains_resources` in `provider` block is `true`. This will lead to an unstable test because the test subscription has some policies applied, and they will add some extra resources during the run, which can cause failures during destroy of resource groups. diff --git a/docs/content/specs-defs/includes/terraform/shared/non-functional/TFNFR5.md b/docs/content/specs-defs/includes/terraform/shared/non-functional/TFNFR5.md index 89aec0340..b688b7480 100644 --- a/docs/content/specs-defs/includes/terraform/shared/non-functional/TFNFR5.md +++ b/docs/content/specs-defs/includes/terraform/shared/non-functional/TFNFR5.md @@ -20,12 +20,6 @@ priority: 21050 ## ID: TFNFR5 - Category: Testing - Test Tooling -Module owners **MUST** use the below tooling for unit/linting/static/security analysis tests. These are also used in the AVM Compliance Tests. +Module owners **MUST** use the below test script for unit/linting/static/security analysis tests. -- [Terraform](https://www.terraform.io/) - - `terraform ` -- [terrafmt](https://github.com/katbyte/terrafmt) -- [Checkov](https://www.checkov.io/) -- [tflint (with azurerm ruleset)](https://github.com/terraform-linters/tflint-ruleset-azurerm) -- [Go](https://go.dev/) - - Some tests are provided as part of the AVM Compliance Tests, but you are free to also use Go for your own tests. +- `./avm pr-check` diff --git a/docs/static/includes/interfaces/tf/int.diag.schema.tf b/docs/static/includes/interfaces/tf/int.diag.schema.tf index 67b5c5a5b..3d2d880d2 100644 --- a/docs/static/includes/interfaces/tf/int.diag.schema.tf +++ b/docs/static/includes/interfaces/tf/int.diag.schema.tf @@ -4,7 +4,7 @@ variable "diagnostic_settings" { log_categories = optional(set(string), []) log_groups = optional(set(string), ["allLogs"]) metric_categories = optional(set(string), ["AllMetrics"]) - log_analytics_destination_type = optional(string, "Dedicated") + log_analytics_destination_type = optional(string) workspace_resource_id = optional(string, null) storage_account_resource_id = optional(string, null) event_hub_authorization_rule_resource_id = optional(string, null) @@ -28,7 +28,7 @@ variable "diagnostic_settings" { error_message = "At least one of `workspace_resource_id`, `storage_account_resource_id`, `marketplace_partner_resource_id`, or `event_hub_authorization_rule_resource_id`, must be set." } description = < v if var.private_endpoints_manage_dns_zone_group } - name = each.value.name != null ? each.value.name : "pep-${var.name}" - location = each.value.location != null ? each.value.location : var.location - resource_group_name = each.value.resource_group_name != null ? each.value.resource_group_name : var.resource_group_name - subnet_id = each.value.subnet_resource_id - custom_network_interface_name = each.value.network_interface_name - tags = each.value.tags - - private_service_connection { - name = each.value.private_service_connection_name != null ? each.value.private_service_connection_name : "pse-${var.name}" - private_connection_resource_id = azurerm_key_vault.this.id - is_manual_connection = false - subresource_names = ["MYSERVICE"] # map to each.value.subresource_name if there are multiple services. - } +module "avm_interfaces" { + source = "azure/avm-utl-interfaces/azure" + version = "0.5.0" # check latest version at the time of use - dynamic "private_dns_zone_group" { - for_each = length(each.value.private_dns_zone_resource_ids) > 0 ? ["this"] : [] + private_endpoints = var.private_endpoints + private_endpoints_scope = azapi_resource.this.id + role_assignment_definition_scope = azapi_resource.this.id +} - content { - name = each.value.private_dns_zone_group_name - private_dns_zone_ids = each.value.private_dns_zone_resource_ids - } - } +resource "azapi_resource" "private_endpoints" { + for_each = module.avm_interfaces.private_endpoints_azapi - dynamic "ip_configuration" { - for_each = each.value.ip_configurations + location = azapi_resource.this.location + name = each.value.name + parent_id = coalesce(var.private_endpoints[each.key].resource_group_resource_id, azapi_resource.this.parent_id) + type = each.value.type + body = each.value.body + retry = { + error_message_regex = ["ScopeLocked"] # This will retry if a lock is in place on the resource group, and has only just been removed + } - content { - name = ip_configuration.value.name - subresource_name = "MYSERVICE" # map to each.value.subresource_name if there are multiple services. - member_name = "MYSERVICE" # map to each.value.subresource_name if there are multiple services. - private_ip_address = ip_configuration.value.private_ip_address - } + timeouts { + delete = "5m" } } -# The PE resource when we are managing **not** the private_dns_zone_group block: -resource "azurerm_private_endpoint" "this_unmanaged_dns_zone_groups" { - for_each = { for k, v in var.private_endpoints : k => v if !var.private_endpoints_manage_dns_zone_group } +resource "azapi_resource" "private_endpoint_locks" { + for_each = module.avm_interfaces.lock_private_endpoint_azapi - # ... repeat configuration above - # **omitting the private_dns_zone_group block** - # then add the following lifecycle block to ignore changes to the private_dns_zone_group block + name = each.value.name + parent_id = azapi_resource.private_endpoints[each.value.pe_key].id + type = each.value.type + body = each.value.body - lifecycle { - ignore_changes = [private_dns_zone_group] - } + depends_on = [ + azapi_resource.private_dns_zone_groups, + azapi_resource.private_endpoint_role_assignments + ] } -# Private endpoint application security group associations. -# We merge the nested maps from private endpoints and application security group associations into a single map. -locals { - private_endpoint_application_security_group_associations = { for assoc in flatten([ - for pe_k, pe_v in var.private_endpoints : [ - for asg_k, asg_v in pe_v.application_security_group_associations : { - asg_key = asg_k - pe_key = pe_k - asg_resource_id = asg_v - } - ] - ]) : "${assoc.pe_key}-${assoc.asg_key}" => assoc } -} +resource "azapi_resource" "private_dns_zone_groups" { + for_each = module.avm_interfaces.private_dns_zone_groups_azapi + + name = each.value.name + parent_id = azapi_resource.private_endpoints[each.key].id + type = each.value.type + body = each.value.body + retry = { + error_message_regex = ["ScopeLocked"] # This will retry if a lock is in place on the resource group, and has only just been removed + interval_seconds = 15 + max_interval_seconds = 60 + } -resource "azurerm_private_endpoint_application_security_group_association" "this" { - for_each = local.private_endpoint_application_security_group_associations - private_endpoint_id = azurerm_private_endpoint.this[each.value.pe_key].id - application_security_group_id = each.value.asg_resource_id + timeouts { + delete = "5m" + } } -# You need an additional resource when not managing private_dns_zone_group with this module: +resource "azapi_resource" "private_endpoint_role_assignments" { + for_each = module.avm_interfaces.role_assignments_private_endpoint_azapi + + name = each.value.name + parent_id = azapi_resource.private_endpoints[each.value.pe_key].id + type = each.value.type + body = each.value.body + retry = { + error_message_regex = ["ScopeLocked"] + interval_seconds = 15 + max_interval_seconds = 60 + } -# In your output you need to select the correct resource based on the value of var.private_endpoints_manage_dns_zone_group: -output "private_endpoints" { - value = var.private_endpoints_manage_dns_zone_group ? azurerm_private_endpoint.this : azurerm_private_endpoint.this_unmanaged_dns_zone_groups - description = <