Terraform Resource Module Specifications
# | ID | Title | Severity | Persona | Lifecycle |
---|---|---|---|---|---|
1 | SNFR8 | Module Owner(s) GitHub | |||
2 | SNFR20 | GitHub Teams Only | |||
3 | SNFR9 | AVM & PG Teams GitHub Repo Permissions | |||
4 | SNFR10 | MIT Licensing | |||
5 | SNFR11 | Issues Response Times | |||
6 | SNFR12 | Versions Supported | |||
7 | SNFR23 | GitHub Repo Labels | |||
8 | PMNFR4 | Missing Resource Module(s) | |||
9 | TFNFR3 | GitHub Repo Branch Protection |
See Specifications for this chapter
A module MUST have an owner that is defined and managed by a GitHub Team in the Azure GitHub organization.
Today this is only Microsoft FTEs, but everyone is welcome to contribute. The module just MUST be owned by a Microsoft FTE (today) so we can enforce and provide the long-term support required by this initiative.
The names for the GitHub teams for each approved module are already defined in the respective Module Indexes . These teams MUST be created (and used) for each module.
All GitHub repositories that AVM module are published from and hosted within MUST only assign GitHub repository permissions to GitHub teams only.
Each module MUST have separate GitHub teams assigned for module owners AND module contributors respectively. These GitHub teams MUST be created in the Azure organization in GitHub.
There MUST NOT be any GitHub repository permissions assigned to individual users.
The names for the GitHub teams for each approved module are already defined in the respective Module Indexes . These teams MUST be created (and used) for each module.
The
@Azure
prefix in the last column of the tables linked above represents the “Azure” GitHub organization all AVM-related repositories exist in. DO NOT include this segment in the team’s name!
Non-FTE / external contributors (subject matter experts that aren’t Microsoft employees) can’t be members of the teams described in this chapter, hence, they won’t gain any extra permissions on AVM repositories, therefore, they need to work in forks.
The naming convention for the GitHub teams MUST follow the below pattern:
<hyphenated module name>-module-owners-<bicep/tf>
- to be assigned as the GitHub repository’sModule Owners
team<hyphenated module name>-module-contributors-<bicep/tf>
- to be assigned as the GitHub repository’sModule Contributors
team
The naming convention for Bicep modules is slightly different than the naming convention for their respective GitHub teams.
Segments:
<hyphenated module name>
== the AVM Module’s name, with each segment separated by dashes, i.e.,avm-res-<resource provider>-<ARM resource type>
module-owners
ormodule-contributors
== the role the GitHub Team is assigned to<bicep/tf>
== the language the module is written in
Examples:
avm-res-compute-virtualmachine-module-owners-bicep
avm-res-compute-virtualmachine-module-contributors-tf
All officially documented module owner(s) MUST be added to the -module-owners-
team. The -module-owners-
team MUST NOT have any other members.
Any additional module contributors whom the module owner(s) agreed to work with MUST be added to the -module-contributors-
team.
Unless explicitly requested and agreed, members of the AVM core team or any PG teams MUST NOT be added to the -module-owners-
or -module-contributors-
teams as permissions for them are granted through the teams described in
SNFR9
.
In case of Bicep modules, permissions to the BRM repository (the repo of the Bicep Registry) are granted via assigning the-module-owners-
and-module-contributors-
teams to parent teams that already have the required level access configured. While it is the module owner’s responsibility to initiate the addition of their teams to the respective parents, only the AVM core team can approve this parent-child relationship.
Module owners MUST create their -module-owners-
and -module-contributors-
teams and as part of the provisioning process, they MUST request the addition of these teams to their respective parent teams (see the table below for details).
GitHub Team Name | Description | Permissions | Permissions granted through | Where to work? |
---|---|---|---|---|
<hyphenated module name>-module-owners-bicep | AVM Bicep Module Owners - <module name> | Write | Assignment to the avm-technical-reviewers-bicep parent team. | Need to work in a fork. |
<hyphenated module name>-module-contributors-bicep | AVM Bicep Module Contributors - <module name> | Triage | avm-module-contributors-bicep parent team. | Need to work in a fork. |
Examples - GitHub teams required for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network
):
avm-res-network-virtualnetwork-module-owners-bicep
–> assign to theavm-technical-reviewers-bicep
parent team.avm-res-network-virtualnetwork-module-contributors-bicep
–> assign to theavm-module-contributors-bicep
parent team.
Direct link to create a new GitHub team and assign it to its parent: Create new team
Fill in the values as follows:
- Team name: Following the naming convention described above, use the value defined in the module indexes.
- Description: Follow the guidance above (see the Description column in the table above).
- Parent team: Follow the guidance above (see the Permissions granted through column in the table above).
- Team visibility:
Visible
- Team notifications:
Enabled
As part of the “initial Pull Request” (that publishes the first version of the module), module owners MUST add an entry to the CODEOWNERS
file in the BRM repository (
here
).
Through this approach, the AVM core team will grant review permission to module owners as part of the standard PR review process.
Every CODEOWNERS
entry (line) MUST include the following segments separated by a single whitespace character:
- Path of the module, relative to the repo’s root, e.g.:
/avm/res/network/virtual-network/
- The
-module-owners-
team, with the@Azure/
prefix, e.g.,@Azure/avm-res-network-virtualnetwork-module-owners-bicep
- The GitHub team of the AVM Bicep reviewers, with the
@Azure/
prefix, i.e.,@Azure/avm-module-reviewers-bicep
Example - CODEOWNERS
entry for the Bicep resource module of Azure Virtual Network (avm/res/network/virtual-network
):
/avm/res/network/virtual-network/ @Azure/avm-res-network-virtualnetwork-module-owners-bicep @Azure/avm-module-reviewers-bicep
Module owners MUST assign the -module-owners-
and -module-contributors-
teams the necessary permissions on their Terraform module repository per the guidance below.
GitHub Team Name | Description | Permissions | Permissions granted through | Where to work? |
---|---|---|---|---|
<module name>-module-owners-tf | AVM Terraform Module Owners - <module name> | Admin | Direct assignment to repo | Module owner can decide whether they want to work in a branch local to the repo or in a fork. |
<module name>-module-contributors-tf | AVM Terraform Module Contributors - <module name> | Write | Direct assignment to repo | Need to work in a fork. |
Direct link to create a new GitHub team: Create new team
Fill in the values as follows:
- Team name: Following the naming convention described above, use the value defined in the module indexes.
- Description: Follow the guidance above (see the Description column in the table above).
- Parent team: Do not assign the team to any parent team.
- Team visibility:
Visible
- Team notifications:
Enabled
A module owner MUST make the following GitHub teams in the Azure GitHub organization admins on the GitHub repo of the module in question:
@Azure/avm-core-team-technical-bicep
= AVM Core Team@Azure/bicep-admins
= Bicep PG team
These required GitHub teams are already associated to the BRM repository and have the required permissions.
@Azure/avm-core-team-technical-terraform
= AVM Core Team@Azure/terraform-avm
= Terraform PG
Module owners MUST assign these GitHub teams as admins on the GitHub repo of the module in question.
For detailed steps, please follow this guidance .
A module MUST be published with the MIT License in the Azure GitHub organization.
A module owner MUST respond to logged issues within 3 business days. See Module Support for more information.
Only the latest released version of a module MUST be supported.
For example, if an AVM Resource Module is used in an AVM Pattern Module that was working but now is not. The first step by the AVM Pattern Module owner should be to upgrade to the latest version of the AVM Resource Module test and then if not fixed, troubleshoot and fix forward from the that latest version of the AVM Resource Module onwards.
This avoids AVM Module owners from having to maintain multiple major release versions.
GitHub repositories where modules are held MUST use the below labels and SHOULD not use any additional labels:
To help apply these to a module GitHub repository you can use the below PowerShell script:
An item MUST be logged onto as an issue on the
AVM Central Repo (Azure/Azure-Verified-Modules
)
if a Resource Module does not exist for resources deployed by the pattern module.
If the Resource Module adds no value, see Resource Module functional requirement ID: RMFR2 .
Module owners MUST set a branch protection policy on their GitHub Repositories for AVM modules against their default branch, typically main
, to do the following:
- Requires a Pull Request before merging
- Require approval of the most recent reviewable push
- Dismiss stale pull request approvals when new commits are pushed
- Require linear history
- Prevents force pushes
- Not allow deletions
- Require CODEOWNERS review
- Do not allow bypassing the above settings
- Above settings MUST also be enforced to administrators
If you use the template repository as mentioned in the contribution guide, the above will automatically be set.
# | ID | Title | Severity | Persona | Lifecycle |
---|---|---|---|---|---|
1 | SFR3 | Deployment/Usage Telemetry | |||
2 | SFR4 | Telemetry Enablement Flexibility | |||
3 | SNFR3 | AVM Compliance Tests |
See Specifications for this chapter
We will maintain a set of CSV files in the AVM Central Repo (
Azure/Azure-Verified-Modules
) with the required TelemetryId prefixes to enable checks to utilize this list to ensure the correct IDs are used. To see the formatted content of these CSV files with additional information, please visit the AVM Module Indexes page.These will also be provided as a comment on the module proposal, once accepted, from the AVM core team.
Modules MUST provide the capability to collect deployment/usage telemetry as detailed in Telemetry further.
To highlight that AVM modules use telemetry, an information notice MUST be included in the footer of each module’s README.md
file with the below content. (See more details on this requirement,
here
.)
The ARM deployment name used for the telemetry MUST follow the pattern and MUST be no longer than 64 characters in length: 46d3xbcp.<res/ptn>.<(short) module name>.<version>.<uniqueness>
<res/ptn>
== AVM Resource or Pattern Module<(short) module name>
== The AVM Module’s, possibly shortened, name including the resource provider and the resource type, without;- The prefixes:
avm-res-
- The prefixes:
avm-ptn-
- The prefixes:
<version>
== The AVM Module’s MAJOR.MINOR version (only) with.
(periods) replaced with-
(hyphens), to allow simpler splitting of the ARM deployment name<uniqueness>
== This section of the ARM deployment name is to be used to ensure uniqueness of the deployment name.- This is to cater for the following scenarios:
- The module is deployed multiple times to the same:
- Location/Region
- Scope (Tenant, Management Group,Subscription, Resource Group)
- The module is deployed multiple times to the same:
- This is to cater for the following scenarios:
Due to the 64-character length limit of Azure deployment names, the<(short) module name>
segment has a length limit of 36 characters, so if the module name is longer than that, it MUST be truncated to 36 characters. If any of the semantic version’s segments are longer than 1 character, it further restricts the number of characters that can be used for naming the module.
An example deployment name for the AVM Virtual Machine Resource Module would be: 46d3xbcp.res.compute-virtualmachine.1-2-3.eum3
An example deployment name for a shortened module name would be: 46d3xbcp.res.desktopvirtualization-appgroup.1-2-3.eum3
The telemetry enablement MUST be on/enabled by default, however this MUST be able to be disabled by a module consumer by setting the below parameter/variable value to false
:
- Bicep:
enableTelemetry
- Terraform:
enable_telemetry
Whenever a module references AVM modules that implement the telemetry parameter (e.g., a pattern module that uses AVM resource modules), the telemetry parameter value MUST be passed through to these modules. This is necessary to ensure a consumer can reliably enable & disable the telemetry feature for all used modules.
Modules MUST pass all tests that ensure compliance to AVM specifications. These tests MUST pass before a module version can be published.
Please note these are still under development at this time and will be published and available soon for module owners.
Module owners MUST request a manual GitHub Pull Request review, prior to their first release of version
0.1.0
of their module, from the related GitHub Team:@Azure/avm-core-team-technical-bicep
, OR@Azure/avm-core-team-technical-terraform
.
# | ID | Title | Severity | Persona | Lifecycle |
---|---|---|---|---|---|
1 | SFR1 | Preview Services | |||
2 | SFR2 | WAF Aligned | |||
3 | SFR5 | Availability Zones | |||
4 | SFR6 | Data Redundancy | |||
5 | SNFR25 | Resource Naming | |||
6 | RMFR1 | Single Resource Only | |||
7 | RMFR2 | No Resource Wrapper Modules | |||
8 | RMFR3 | Resource Groups | |||
9 | RMFR4 | AVM Consistent Feature & Extension Resources Value Add | |||
10 | RMFR5 | AVM Consistent Feature & Extension Resources Value Add Interfaces/Schemas | |||
11 | RMFR8 | Dependency on child and other resources | |||
12 | RMFR9 | End-of-life resource versions | |||
13 | RMNFR1 | Module Naming | |||
14 | RMNFR3 | RP Collaboration | |||
15 | TFFR1 | Cross-Referencing Modules | |||
16 | TFFR3 | Providers - Permitted Versions | |||
17 | TFNFR4 | Lower snake_casing |
See Specifications for this chapter
Modules MAY create/adopt public preview services and features at their discretion.
Preview API versions MAY be used when:
- The resource/service/feature is GA but the only API version available for the GA resource/service/feature is a preview version
- For example, Diagnostic Settings (
Microsoft.Insights/diagnosticSettings
) the latest version of the API available with GA features, like Category Groups etc., is2021-05-01-preview
- Otherwise the latest “non-preview” version of the API SHOULD be used
- For example, Diagnostic Settings (
Preview services and features, SHOULD NOT be promoted and exposed, unless they are supported by the respective PG, and it’s documented publicly.
However, they MAY be exposed at the module owners discretion, but the following rules MUST be followed:
- The description of each of the parameters/variables used for the preview service/feature MUST start with:
- “THIS IS A <PARAMETER/VARIABLE> USED FOR A PREVIEW SERVICE/FEATURE, MICROSOFT MAY NOT PROVIDE SUPPORT FOR THIS, PLEASE CHECK THE PRODUCT DOCS FOR CLARIFICATION”
Modules SHOULD set defaults in input parameters/variables to align to high priority/impact/severity recommendations, where appropriate and applicable, in the following frameworks and resources:
- Well-Architected Framework (WAF)
- Reliability Hub
- Azure Proactive Resiliency Library (APRL)
- Only Product Group (PG) verified
- Microsoft Defender for Cloud (MDFC)
They SHOULD NOT align to these recommendations when it requires an external dependency/resource to be deployed and configured and then associated to the resources in the module.
Alignment SHOULD prioritize best-practices and security over cost optimization, but MUST allow for these to be overridden by a module consumer easily, if desired.
Read the FAQ of What does AVM mean by “WAF Aligned”? for more detailed information and examples.
Modules that deploy zone-redundant resources MUST enable the spanning across as many zones as possible by default, typically all 3.
Modules that deploy zonal resources MUST provide the ability to specify a zone for the resources to be deployed/pinned to. However, they MUST NOT default to a particular zone by default, e.g. 1
in an effort to make the consumer aware of the zone they are selecting to suit their architecture requirements.
For both scenarios the modules MUST expose these configuration options via configurable parameters/variables.
For information on the differences between zonal and zone-redundant services, see Availability zone service and regional support
Modules that deploy resources or patterns that support data redundancy SHOULD enable this to the highest possible value by default, e.g. RA-GZRS
. When a resource or pattern doesn’t provide the ability to specify data redundancy as a simple property, e.g. GRS
etc., then the modules MUST provide the ability to enable data redundancy for the resources or pattern via parameters/variables.
For example, a Storage Account module can simply set the sku.name
property to Standard_RAGZRS
. Whereas a SQL DB or Cosmos DB module will need to expose more properties, via parameters/variables, to allow the specification of the regions to replicate data to as per the consumers requirements.
For information on the data redundancy options in Azure, see Cross-region replication in Azure
Module owners MUST set the default resource name prefix for child, extension, and interface resources to the associated abbreviation for the specific resource as documented in the following CAF article Abbreviation examples for Azure resources , if specified and documented. This reduces the amount of input values a module consumer MUST provide by default when using the module.
For example, a Private Endpoint that is being deployed as part of a resource module, via the mandatory interfaces, MUST set the Private Endpoint’s default name to begin with the prefix of pep-
.
Module owners MUST also provide the ability for these default names, including the prefixes, to be overridden via a parameter/variable if the consumer wishes to.
Furthermore, as per RMNFR2 , Resource Modules MUST not have a default value specified for the name of the primary resource and therefore the name MUST be provided and specified by the module consumer.
The name provided MAY be used by the module owner to generate the rest of the default name for child, extension, and interface resources if they wish to. For example, for the Private Endpoint mentioned above, the full default name that can be overridden by the consumer, MAY be pep-<primary-resource-name>
.
If the resource does not have a documented abbreviation in Abbreviation examples for Azure resources , then the module owner is free to use a sensible prefix instead.
A resource module MUST only deploy a single instance of the primary resource, e.g., one virtual machine per instance.
Multiple instances of the module MUST be used to scale out.
A resource module MUST add value by including additional features on top of the primary resource.
A resource module MUST NOT create a Resource Group for resources that require them.
In the case that a Resource Group is required, a module MUST have an input (scope or variable):
- In Bicep the
targetScope
MUST be set toresourceGroup
or not specified (which means default toresourceGroup
scope) - In Terraform the
variable
MUST be calledresource_group_name
Scopes will be covered further in the respective language specific specifications.
Resource modules support the following optional features/extension resources, as specified, if supported by the primary resource. The top-level variable/parameter names MUST be:
Optional Features/Extension Resources | Bicep Parameter Name | Terraform Variable Name | MUST/SHOULD |
---|---|---|---|
Diagnostic Settings | diagnosticSettings | diagnostic_settings | MUST |
Role Assignments | roleAssignments | role_assignments | MUST |
Resource Locks | lock | lock | MUST |
Tags | tags | tags | MUST |
Managed Identities (System / User Assigned) | managedIdentities | managed_identities | MUST |
Private Endpoints | privateEndpoints | private_endpoints | MUST |
Customer Managed Keys | customerManagedKey | customer_managed_key | MUST |
Azure Monitor Alerts | alerts | alerts | SHOULD |
Resource modules MUST NOT deploy required/dependent resources for the optional features/extension resources specified above. For example, for Diagnostic Settings the resource module MUST NOT deploy the Log Analytics Workspace, this is expected to be already in existence from the perspective of the resource module deployed via another method/module etc.
Please note that the implementation of Customer Managed Keys from an ARM API perspective is different across various RPs that implement Customer Managed Keys in their service. For that reason you may see differences between modules on how Customer Managed Keys are handled and implemented, but functionality will be as expected.
Module owners MAY choose to utilize cross repo dependencies for these “add-on” resources, or MAY chose to implement the code directly in their own repo/module. So long as the implementation and outputs are as per the specifications requirements, then this is acceptable.
ID: RMFR5 - Category: Composition - AVM Consistent Feature & Extension Resources Value Add Interfaces/Schemas
Resource modules MUST implement a common interface, e.g. the input’s data structures and properties within them (objects/arrays/dictionaries/maps), for the optional features/extension resources:
See:
A resource module MAY contain references to other resource modules, however MUST NOT contain references to non-AVM modules nor AVM pattern modules.
See BCPFR1 and TFFR1 for more information on this.
When a given version of an Azure resource used in a resource module reaches its end-of-life (EOL) and is no longer supported by Microsoft, the module owner SHOULD ensure that:
- The module is aligned with these changes and only includes supported versions of the resource. This is typically achieved through the allowed values in the parameter that specifies the resource SKU or type.
- The following notice is shown under the
Notes
section of the module’sreadme.md
. (If any related public announcement is available, it can also be linked to from the Notes section.):“Certain versions of this Azure resource reached their end of life. The latest version of this module only includes supported versions of the resource. All unsupported versions have been removed from the related parameters.”
- AND the related parameter’s description:
“Certain versions of this Azure resource reached their end of life. The latest version of this module only includes supported versions of the resource. All unsupported versions have been removed from this parameter.”
We will maintain a set of CSV files in the AVM Central Repo (
Azure/Azure-Verified-Modules
) with the correct singular names for all resource types to enable checks to utilize this list to ensure repos are named correctly. To see the formatted content of these CSV files with additional information, please visit the AVM Module Indexes page.This will be updated quarterly, or ad-hoc as new RPs/ Resources are created and highlighted via a check failure.
Resource modules MUST follow the below naming conventions (all lower case):
- Naming convention:
avm/res/<hyphenated resource provider name>/<hyphenated ARM resource type>
(module name for registry) - Example:
avm/res/compute/virtual-machine
oravm/res/managed-identity/user-assigned-identity
- Segments:
res
defines this is a resource module<hyphenated resource provider name>
is the resource provider’s name after theMicrosoft
part, with each word starting with a capital letter separated by dashes, e.g.,Microsoft.Compute
=compute
,Microsoft.ManagedIdentity
=managed-identity
.<hyphenated ARM resource type>
is the singular version of the word after the resource provider, with each word starting with a capital letter separated by dashes, e.g.,Microsoft.Compute/virtualMachines
=virtual-machine
, BUTMicrosoft.Network/trafficmanagerprofiles
=trafficmanagerprofile
- sincetrafficmanagerprofiles
is all lower case as per the ARM API definition.
- Naming convention:
avm-res-<resource provider>-<ARM resource type>
(module name for registry)terraform-<provider>-avm-res-<resource provider>-<ARM resource type>
(GitHub repository name to meet registry naming requirements)
- Example:
avm-res-compute-virtualmachine
oravm-res-managedidentity-userassignedidentity
- Segments:
<provider>
is the logical abstraction of various APIs used by Terraform. In most cases, this is going to beazurerm
orazuread
for resource modules.res
defines this is a resource module<resource provider>
is the resource provider’s name after theMicrosoft
part, e.g.,Microsoft.Compute
=compute
.<ARM resource type>
is the singular version of the word after the resource provider, e.g.,Microsoft.Compute/virtualMachines
=virtualmachine
Module owners (Microsoft FTEs) SHOULD reach out to the respective Resource Provider teams to build a partnership and collaboration on the modules creation, existence and long term maintenance.
Review this wiki page (Microsoft Internal) for more information.
Module owners MAY cross-references other modules to build either Resource or Pattern modules. However, they MUST be referenced only by a HashiCorp Terraform registry reference to a pinned version e.g.,
module "other-module" {
source = "Azure/xxx/azurerm"
version = "1.2.3"
}
They MUST NOT use git reference to a module.
module "other-module" {
source = "git::https://xxx.yyy/xxx.git"
}
module "other-module" {
source = "github.com/xxx/yyy"
}
Modules MUST NOT contain references to non-AVM modules.
See Module Sources for more information.
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 |
Authors MAY select either Azurerm, Azapi, or both providers in their module.
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
~>
.
That is to say that ~> 4.0
is equivalent to >= 4.0, < 5.0
.
terraform {
required_providers {
# Include one or both providers, as needed
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.0"
}
azapi = {
source = "Azure/azapi"
version = "~> 2.0"
}
}
}
Module owners MUST use lower snake_casing for naming the following:
- Locals
- Variables
- Outputs
- Resources (symbolic names)
- Modules (symbolic names)
For example: snake_casing_example
(every word in lowercase, with each word separated by an underscore _
)
# | ID | Title | Severity | Persona | Lifecycle |
---|---|---|---|---|---|
1 | TFNFR36 | Setting prevent_deletion_if_contains_resources | |||
2 | TFNFR6 | Resource & Data Order | |||
3 | TFNFR7 | Count & for_each Use | |||
4 | TFNFR8 | Resource & Data Block Orders | |||
5 | TFNFR9 | Module Block Order | |||
6 | TFNFR10 | No Double Quotes in ignore_changes | |||
7 | TFNFR11 | Null Comparison Toggle | |||
8 | TFNFR12 | Dynamic for Optional Nested Objects | |||
9 | TFNFR13 | Default Values with coalesce/try | |||
10 | TFNFR16 | Variable Naming Rules | |||
11 | TFNFR17 | Variables with Descriptions | |||
12 | TFNFR18 | Variables with Types | |||
13 | TFNFR19 | Sensitive Data Variables | |||
14 | TFNFR20 | Non-Nullable Defaults for collection values | |||
15 | TFNFR21 | Discourage Nullability by Default | |||
16 | TFNFR22 | Avoid sensitive = false | |||
17 | TFNFR23 | Sensitive Default Value Conditions | |||
18 | TFNFR24 | Handling Deprecated Variables | |||
19 | TFNFR25 | Verified Modules Requirements | |||
20 | TFNFR26 | Providers in required_providers | |||
21 | TFNFR27 | Provider Declarations in Modules | |||
23 | TFNFR30 | Handling Deprecated Outputs | |||
24 | TFNFR31 | locals.tf for Locals Only | |||
26 | TFNFR33 | Precise Local Types | |||
27 | TFNFR34 | Using Feature Toggles | |||
28 | TFNFR35 | Reviewing Potential Breaking Changes | |||
29 | TFNFR37 | Tool Usage by Module Owner |
See Specifications for this chapter
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.
Since we cannot guarantee our testing environment won’t be applied some
Azure Policy Remediation Tasks
in the future, for a robust testing environment, prevent_deletion_if_contains_resources
SHOULD be explicitly set to false
.
For the definition of resources in the same file, the resources be depended on SHOULD come first, after them are the resources depending on others.
Resources that have dependencies SHOULD be defined close to each other.
We can use count
and for_each
to deploy multiple resources, but the improper use of count
can lead to
anti pattern
.
You can use count
to create some kind of resources under certain conditions, for example:
resource "azurerm_network_security_group" "this" {
count = local.create_new_security_group ? 1 : 0
name = coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
resource_group_name = var.resource_group_name
location = local.location
tags = var.new_network_security_group_tags
}
The module’s owners MUST use map(xxx)
or set(xxx)
as resource’s for_each
collection, the map’s key or set’s element MUST be static literals.
Good example:
resource "azurerm_subnet" "pair" {
for_each = var.subnet_map // `map(string)`, when user call this module, it could be: `{ "subnet0": "subnet0" }`, or `{ "subnet0": azurerm_subnet.subnet0.name }`
name = "${each.value}"-pair
resource_group_name = azurerm_resource_group.example.name
virtual_network_name = azurerm_virtual_network.example.name
address_prefixes = ["10.0.1.0/24"]
}
Bad example:
resource "azurerm_subnet" "pair" {
for_each = var.subnet_name_set // `set(string)`, when user use `toset([azurerm_subnet.subnet0.name])`, it would cause an error.
name = "${each.value}"-pair
resource_group_name = azurerm_resource_group.example.name
virtual_network_name = azurerm_virtual_network.example.name
address_prefixes = ["10.0.1.0/24"]
}
There are 3 types of assignment statements in a resource
or data
block: argument, meta-argument and nested block. The argument assignment statement is a parameter followed by =
:
location = azurerm_resource_group.example.location
or:
tags = {
environment = "Production"
}
Nested block is a assignment statement of parameter followed by {}
block:
subnet {
name = "subnet1"
address_prefix = "10.0.1.0/24"
}
Meta-arguments are assignment statements can be declared by all resource
or data
blocks. They are:
count
depends_on
for_each
lifecycle
provider
The order of declarations within resource
or data
blocks is:
All the meta-arguments SHOULD be declared on the top of resource
or data
blocks in the following order:
provider
count
for_each
Then followed by:
- required arguments
- optional arguments
- required nested blocks
- optional nested blocks
All ranked in alphabetical order.
These meta-arguments SHOULD be declared at the bottom of a resource
block with the following order:
depends_on
lifecycle
The parameters of lifecycle
block SHOULD show up in the following order:
create_before_destroy
ignore_changes
prevent_destroy
parameters under depends_on
and ignore_changes
are ranked in alphabetical order.
Meta-arguments, arguments and nested blocked are separated by blank lines.
dynamic
nested blocks are ranked by the name comes after dynamic
, for example:
dynamic "linux_profile" {
for_each = var.admin_username == null ? [] : ["linux_profile"]
content {
admin_username = var.admin_username
ssh_key {
key_data = replace(coalesce(var.public_ssh_key, tls_private_key.ssh[0].public_key_openssh), "\n", "")
}
}
}
This dynamic
block will be ranked as a block named linux_profile
.
Code within a nested block will also be ranked following the rules above.
PS: You can use
avmfix
tool to reformat your code automatically.
The meta-arguments below SHOULD be declared on the top of a module
block with the following order:
source
version
count
for_each
blank lines will be used to separate them.
After them will be required arguments, optional arguments, all ranked in alphabetical order.
These meta-arguments below SHOULD be declared on the bottom of a resource
block in the following order:
depends_on
providers
Arguments and meta-arguments SHOULD be separated by blank lines.
The ignore_changes
attribute MUST NOT be enclosed in double quotes.
Good example:
lifecycle {
ignore_changes = [
tags,
]
}
Bad example:
lifecycle {
ignore_changes = [
"tags",
]
}
Sometimes we need to ensure that the resources created are compliant to some rules at a minimum extent, for example a subnet
has to be connected to at least one network_security_group
. The user SHOULD pass in a security_group_id
and ask us to make a connection to an existing security_group
, or want us to create a new security group.
Intuitively, we will define it like this:
variable "security_group_id" {
type = string
}
resource "azurerm_network_security_group" "this" {
count = var.security_group_id == null ? 1 : 0
name = coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
resource_group_name = var.resource_group_name
location = local.location
tags = var.new_network_security_group_tags
}
The disadvantage of this approach is if the user create a security group directly in the root module and use the id
as a variable
of the module, the expression which determines the value of count
will contain an attribute
from another resource
, the value of this very attribute
is “known after apply” at plan stage. Terraform core will not be able to get an exact plan of deployment during the “plan” stage.
You can’t do this:
resource "azurerm_network_security_group" "foo" {
name = "example-nsg"
resource_group_name = "example-rg"
location = "eastus"
}
module "bar" {
source = "xxxx"
...
security_group_id = azurerm_network_security_group.foo.id
}
For this kind of parameters, wrapping with object
type is RECOMMENDED:
variable "security_group" {
type = object({
id = string
})
default = null
}
The advantage of doing so is encapsulating the value which is “known after apply” in an object, and the object
itself can be easily found out if it’s null
or not. Since the id
of a resource
cannot be null
, this approach can avoid the situation we are facing in the first example, like the following:
resource "azurerm_network_security_group" "foo" {
name = "example-nsg"
resource_group_name = "example-rg"
location = "eastus"
}
module "bar" {
source = "xxxx"
...
security_group = {
id = azurerm_network_security_group.foo.id
}
}
This technique SHOULD be used under this use case only.
An example from the community:
resource "azurerm_kubernetes_cluster" "main" {
...
dynamic "identity" {
for_each = var.client_id == "" || var.client_secret == "" ? [1] : []
content {
type = var.identity_type
user_assigned_identity_id = var.user_assigned_identity_id
}
}
...
}
Please refer to the coding style in the example. Nested blocks under conditions, MUST be declared as:
for_each = <condition> ? [<some_item>] : []
The following example shows how "${var.subnet_name}-nsg"
SHOULD be used when var.new_network_security_group_name
is null
or ""
Good examples:
coalesce(var.new_network_security_group_name, "${var.subnet_name}-nsg")
try(coalesce(var.new_network_security_group.name, "${var.subnet_name}-nsg"), "${var.subnet_name}-nsg")
Bad examples:
var.new_network_security_group_name == null ? "${var.subnet_name}-nsg" : var.new_network_security_group_name)
The naming of a variable
SHOULD follow
HashiCorp’s naming rule
.
variable
used as feature switches SHOULD apply a positive statement, use xxx_enabled
instead of xxx_disabled
. Avoid double negatives like !xxx_disabled
.
Please use xxx_enabled
instead of xxx_disabled
as name of a variable
.
The target audience of description
is the module users.
For a newly created variable
(Eg. variable
for switching dynamic
block on-off), it’s description
SHOULD precisely describe the input parameter’s purpose and the expected data type. description
SHOULD NOT contain any information for module developers, this kind of information can only exist in code comments.
For object
type variable
, description
can be composed in HEREDOC format:
variable "kubernetes_cluster_key_management_service" {
type = object({
key_vault_key_id = string
key_vault_network_access = optional(string)
})
default = null
description = <<-EOT
- `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
}
type
MUST be defined for every variable
. type
SHOULD be as precise as possible, any
MAY only be defined with adequate reasons.
- Use
bool
instead ofstring
ornumber
fortrue/false
- Use
string
for text - Use concrete
object
instead ofmap(any)
If variable
’s type
is object
and contains one or more fields that would be assigned to a sensitive
argument, then this whole variable
SHOULD be declared as sensitive = true
, otherwise you SHOULD extract sensitive field into separated variable block with sensitive = true
.
Nullable SHOULD be set to false
for collection values (e.g. sets, maps, lists) when using them in loops. However for scalar values like string and number, a null value MAY have a semantic meaning and as such these values are allowed.
nullable = true
MUST be avoided.
sensitive = false
MUST be avoided.
A default value MUST NOT be set for a sensitive input - e.g., a default password.
Sometimes we will find names for some variable
are not suitable anymore, or a change SHOULD be made to the data type. We want to ensure forward compatibility within a major version, so direct changes are strictly forbidden. The right way to do this is move this variable
to an independent deprecated_variables.tf
file, then redefine the new parameter in variable.tf
and make sure it’s compatible everywhere else.
Deprecated variable
MUST be annotated as DEPRECATED
at the beginning of the description
, at the same time the replacement’s name SHOULD be declared. E.g.,
variable "enable_network_security_group" {
type = string
default = null
description = "DEPRECATED, use `network_security_group_enabled` instead; Whether to generate a network security group and assign it to the subnet. Changing this forces a new resource to be created."
}
A cleanup of deprecated_variables.tf
SHOULD be performed during a major version release.
The terraform.tf
file MUST only contain one terraform
block.
The first line of the terraform
block MUST define a required_version
property for the Terraform CLI.
The required_version
property MUST include a constraint on the minimum version of the Terraform CLI. Previous releases of the Terraform CLI can have unexpected behavior.
The required_version
property MUST include a constraint on the maximum major version of the Terraform CLI. Major version releases of the Terraform CLI can introduce breaking changes and MUST be tested.
The required_version
property constraint SHOULD use the ~> #.#
or the >= #.#.#, < #.#.#
format.
Note: You can read more about Terraform version constraints in the documentation .
Example terraform.tf
file:
terraform {
required_version = "~> 1.6"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.11"
}
}
}
The terraform
block in terraform.tf
MUST contain the required_providers
block.
Each provider used directly in the module MUST be specified with the source
and version
properties. Providers in the required_providers
block SHOULD be sorted in alphabetical order.
Do not add providers to the required_providers
block that are not directly required by this module. If submodules are used then each submodule SHOULD have its own versions.tf
file.
The source
property MUST be in the format of namespace/name
. If this is not explicitly specified, it can cause failure.
The version
property MUST include a constraint on the minimum version of the provider. Older provider versions may not work as expected.
The version
property MUST include a constraint on the maximum major version. A provider major version release may introduce breaking change, so updates to the major version constraint for a provider MUST be tested.
The version
property constraint SHOULD use the ~> #.#
or the >= #.#.#, < #.#.#
format.
Note: You can read more about Terraform version constraints in the documentation .
Good examples:
terraform {
required_version = "~> 1.6"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
}
}
}
terraform {
required_version = ">= 1.6.6, < 2.0.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">= 3.11.1, < 4.0.0"
}
}
}
terraform {
required_version = ">= 1.6, < 2.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">= 3.11, < 4.0"
}
}
}
Acceptable example (but not recommended):
terraform {
required_version = "1.6"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "3.11"
}
}
}
Bad example:
terraform {
required_version = ">= 1.6"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">= 3.11"
}
}
}
By rules
, in the module code provider
MUST NOT be declared. The only exception is when the module indeed need different instances of the same kind of provider
(Eg. manipulating resources across different location
s or accounts), you MUST declare configuration_aliases
in terraform.required_providers
. See details in this
document
.
provider
block declared in the module MUST only be used to differentiate instances used in resource
and data
. Declaration of fields other than alias
in provider
block is strictly forbidden. It could lead to module users unable to utilize count
, for_each
or depends_on
. Configurations of the provider
instance SHOULD be passed in by the module users.
Good examples:
In verified module:
terraform {
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 3.0"
configuration_aliases = [ azurerm.alternate ]
}
}
}
In the root module where we call this verified module:
provider "azurerm" {
features {}
}
provider "azurerm" {
alias = "alternate"
features {}
}
module "foo" {
source = "xxx"
providers = {
azurerm = azurerm
azurerm.alternate = azurerm.alternate
}
}
Bad example:
In verified module:
provider "azurerm" {
# Configuration options
features {}
}
Sometimes we notice that the name of certain output
is not appropriate anymore, however, since we have to ensure forward compatibility in the same major version, its name MUST NOT be changed directly. It MUST be moved to an independent deprecated_outputs.tf
file, then redefine a new output in output.tf
and make sure it’s compatible everywhere else in the module.
A cleanup SHOULD be performed to deprecated_outputs.tf
and other logics related to compatibility during a major version upgrade.
In locals.tf
, file we could declare multiple locals
blocks, but only locals
blocks are allowed.
You MAY declare locals
blocks next to a resource
block or data
block for some advanced scenarios, like making a fake module to execute some light-weight tests aimed at the expressions.
Precise local types SHOULD be used.
Good example:
{
name = "John"
age = 52
}
Bad example:
{
name = "John"
age = "52" # age should be number
}
A toggle variable MUST be used to allow users to avoid the creation of a new resource
block by default if it is added in a minor or patch version.
E.g., our previous release was v1.2.1
and next release would be v1.3.0
, now we’d like to submit a pull request which contains such new resource
:
resource "azurerm_route_table" "this" {
location = local.location
name = coalesce(var.new_route_table_name, "${var.subnet_name}-rt")
resource_group_name = var.resource_group_name
}
A user who’s just upgraded the module’s version would be surprised to see a new resource to be created in a newly generated plan file.
A better approach is adding a feature toggle to be turned off by default:
variable "create_route_table" {
type = bool
default = false
nullable = false
}
resource "azurerm_route_table" "this" {
count = var.create_route_table ? 1 : 0
location = local.location
name = coalesce(var.new_route_table_name, "${var.subnet_name}-rt")
resource_group_name = var.resource_group_name
}
Potential breaking(surprise) changes introduced by resource
block
- Adding a new
resource
withoutcount
orfor_each
for conditional creation, or creating by default - Adding a new argument assignment with a value other than the default value provided by the provider’s schema
- Adding a new nested block without making it
dynamic
or omitting it by default - Renaming a
resource
block without one or more correspondingmoved
blocks - Change
resource
’scount
tofor_each
, or vice versa
Terraform moved
block
could be your cure.
Potential breaking changes introduced by variable
and output
blocks
- Deleting(Renaming) a
variable
- Changing
type
in avariable
block - Changing the
default
value in avariable
block - Changing
variable
’snullable
tofalse
- Changing
variable
’ssensitive
fromfalse
totrue
- Adding a new
variable
withoutdefault
- Deleting an
output
- Changing an
output
’svalue
- Changing an
output
’ssensitive
value
These changes do not necessarily trigger breaking changes, but they are very likely to, they MUST be reviewed with caution.
newres
is a command-line tool that generates Terraform configuration files for a specified resource type. It automates the process of creating variables.tf
and main.tf
files, making it easier to get started with Terraform and reducing the time spent on manual configuration.
Module owners MAY use newres
when they’re trying to add new resource
block, attribute, or nested block. They MAY generate the whole block along with the corresponding variable
blocks in an empty folder, then copy-paste the parts they need with essential refactoring.
# | ID | Title | Severity | Persona | Lifecycle |
---|---|---|---|---|---|
1 | SNFR14 | Data Types | |||
2 | SNFR22 | Parameters/Variables for Resource IDs | |||
3 | RMFR6 | Parameter/Variable Naming | |||
4 | RMFR7 | Minimum Required Outputs | |||
5 | RMNFR2 | Parameter/Variable Naming | |||
6 | TFFR2 | Additional Terraform Outputs | |||
7 | TFNFR14 | Not allowed variables |
See Specifications for this chapter
A module SHOULD use either: simple data types. e.g., string, int, bool.
OR
Complex data types (objects, arrays, maps) when the language-compliant schema is defined.
A module parameter/variable that requires a full Azure Resource ID as an input value, e.g. /subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.KeyVault/vaults/{keyVaultName}
, MUST contain ResourceId/resource_id
in its parameter/variable name to assist users in knowing what value to provide at a glance of the parameter/variable name.
Example for the property workspaceId
for the Diagnostic Settings resource. In Bicep its parameter name should be workspaceResourceId
and the variable name in Terraform should be workspace_resource_id
.
workspaceId
is not descriptive enough and is ambiguous as to which ID is required to be input.
Parameters/variables that pertain to the primary resource MUST NOT use the resource type in the name.
e.g., use sku
, vs. virtualMachineSku
/virtualmachine_sku
Another example for where RPs contain some of their name within a property, leave the property unchanged. E.g. Key Vault has a property called keySize
, it is fine to leave as this and not remove the key
part from the property/parameter name.
Module owners MUST output the following outputs as a minimum in their modules:
Output | Bicep Output Name | Terraform Output Name |
---|---|---|
Resource Name | name | name |
Resource ID | resourceId | resource_id |
System Assigned Managed Identity Principal ID (if supported by module) | systemAssignedMIPrincipalId | system_assigned_mi_principal_id |
A resource module MUST use the following standard inputs:
name
(no default)location
(if supported by the resource and not a global resource, then use Resource Group location, if resource supports Resource Groups, otherwise no default)
Authors SHOULD NOT output entire resource objects as these may contain sensitive outputs and the schema can change with API or provider versions. Instead, authors SHOULD output the computed attributes of the resource as discreet outputs. This kind of pattern protects against provider schema changes and is known as an anti-corruption layer .
Remember, you SHOULD NOT output values that are already inputs (other than name
).
E.g.,
# Resource output, computed attribute.
output "foo" {
description = "MyResource foo attribute"
value = azurerm_resource_myresource.foo
}
# Resource output for resources that are deployed using `for_each`. Again only computed attributes.
output "childresource_foos" {
description = "MyResource children's foo attributes"
value = {
for key, value in azurerm_resource_mychildresource : key => value.foo
}
}
# Output of a sensitive attribute
output "bar" {
description = "MyResource bar attribute"
value = azurerm_resource_myresource.bar
sensitive = true
}
Since Terraform 0.13, count
, for_each
and depends_on
are introduced for modules, module development is significantly simplified. Module’s owners MUST NOT add variables like enabled
or module_depends_on
to control the entire module’s operation. Boolean feature toggles are acceptable however.
# | ID | Title | Severity | Persona | Lifecycle |
---|---|---|---|---|---|
1 | SNFR1 | Prescribed Tests | |||
2 | SNFR2 | E2E Testing | |||
3 | SNFR4 | Unit Tests | |||
4 | SNFR5 | Upgrade Tests | |||
5 | SNFR6 | Static Analysis/Linting Tests | |||
6 | SNFR7 | Idempotency Tests | |||
7 | SNFR24 | Testing Child, Extension & Interface Resources | |||
8 | TFNFR5 | Test Tooling | |||
9 | TFNFR15 | Variable Definition Order |
See Specifications for this chapter
Modules MUST use the prescribed tooling and testing frameworks defined in the language specific specs.
Modules MUST implement end-to-end (deployment) testing that create actual resources to validate that module deployments work. In Bicep tests are sourced from the directories in /tests/e2e
. In Terraform, these are in /examples
.
Each test MUST run and complete without user inputs successfully, for automation purposes.
Each test MUST also destroy/clean-up its resources and test dependencies following a run.
It is likely that to complete E2E tests, a number of resources will be required as dependencies to enable the tests to pass successfully. Some examples:
- When testing the Diagnostic Settings interface for a Resource Module, you will need an existing Log Analytics Workspace to be able to send the logs to as a destination.
- When testing the Private Endpoints interface for a Resource Module, you will need an existing Virtual Network, Subnet and Private DNS Zone to be able to complete the Private Endpoint deployment and configuration.
Module owners MUST:
- Create the required resources that their module depends upon in the test file/directory
- They MUST either use:
- Simple/native resource declarations/definitions in their respective IaC language,
OR - Another already published AVM Module that MUST be pinned to a specific published version.
- They MUST NOT use any local directory path references or local copies of AVM modules in their own modules test directory.
- Simple/native resource declarations/definitions in their respective IaC language,
- They MUST either use:
Modules SHOULD implement unit testing to ensure logic and conditions within parameters/variables/locals are performing correctly. These tests MUST pass before a module version can be published.
Unit Tests test specific module functionality, without deploying resources. Used on more complex modules. In Bicep and Terraform these live in tests/unit
.
Modules SHOULD implement upgrade testing to ensure new features are implemented in a non-breaking fashion on non-major releases.
Modules MUST use static analysis, e.g., linting, security scanning (PSRule, tflint, etc.). These tests MUST pass before a module version can be published.
There may be differences between languages in linting rules standards, but the AVM core team will try to close these and bring them into alignment over time.
Modules MUST implement idempotency end-to-end (deployment) testing. E.g. deploying the module twice over the top of itself.
Modules SHOULD pass the idempotency test, as we are aware that there are some exceptions where they may fail as a false-positive or legitimate cases where a resource cannot be idempotent.
For example, Virtual Machine Image names must be unique on each resource creation/update.
Module owners MUST test that child and extension resources and those Bicep or Terreform interface resources that are supported by their modules, are validated in E2E tests as per SNFR2 to ensure they deploy and are configured correctly.
These MAY be tested in a separate E2E test and DO NOT have to be tested in each E2E test.
Module owners MUST use the below tooling for unit/linting/static/security analysis tests. These are also used in the AVM Compliance Tests.
- Terraform test
terraform <validate/fmt/test>
- tflint (with azurerm ruleset)
- Go
- Some tests are provided as part of the AVM Compliance Tests, but you are free to also use Go for your own tests.
Input variables SHOULD follow this order:
- All required fields, in alphabetical order
- All optional fields, in alphabetical order
A variable
without default
value is a required field, otherwise it’s an optional one.
# | ID | Title | Severity | Persona | Lifecycle |
---|---|---|---|---|---|
1 | SNFR15 | Automatic Documentation Generation | |||
2 | SNFR16 | Examples/E2E | |||
3 | TFNFR1 | Descriptions | |||
4 | TFNFR2 | Module Documentation Generation |
See Specifications for this chapter
README documentation MUST be automatically/programmatically generated. MUST include the sections as defined in the language specific requirements BCPNFR2 , TFNFR2 .
An examples/e2e directory MUST exist to provide named scenarios for module deployment.
Where descriptions for variables and outputs spans multiple lines. The description MAY provide variable input examples for each variable using the HEREDOC format and embedded markdown.
Example:
variable "my_complex_input" {
type = map(object({
param1 = string
param2 = optional(number, null)
}))
description = <<DESCRIPTION
A complex input variable that is a map of objects.
Each object has two attributes:
- `param1`: A required string parameter.
- `param2`: (Optional) An optional number parameter.
Example Input:
```terraform
my_complex_input = {
"object1" = {
param1 = "value1"
param2 = 2
}
"object2" = {
param1 = "value2"
}
}
```
DESCRIPTION
}
Terraform modules documentation MUST be automatically generated via Terraform Docs .
A file called .terraform-docs.yml
MUST be present in the root of the module and have the following content:
---
### To generate the output file to partially incorporate in the README.md,
### Execute this command in the Terraform module's code folder:
# terraform-docs -c .terraform-docs.yml .
formatter: "markdown document" # this is required
version: "0.16.0"
header-from: "_header.md"
footer-from: "_footer.md"
recursive:
enabled: false
path: modules
sections:
hide: []
show: []
content: |-
{{ .Header }}
<!-- markdownlint-disable MD033 -->
{{ .Requirements }}
{{ .Providers }}
{{ .Resources }}
<!-- markdownlint-disable MD013 -->
{{ .Inputs }}
{{ .Outputs }}
{{ .Modules }}
{{ .Footer }}
output:
file: README.md
mode: replace
template: |-
<!-- BEGIN_TF_DOCS -->
{{ .Content }}
<!-- END_TF_DOCS -->
output-values:
enabled: false
from: ""
sort:
enabled: true
by: required
settings:
anchor: true
color: true
default: true
description: false
escape: true
hide-empty: false
html: true
indent: 2
lockfile: true
read-comments: true
required: true
sensitive: true
type: true
# | ID | Title | Severity | Persona | Lifecycle |
---|---|---|---|---|---|
1 | SNFR17 | Semantic Versioning | |||
2 | SNFR18 | Breaking Changes | |||
3 | SNFR19 | Registries Targeted | |||
4 | SNFR21 | Cross Language Collaboration |
See Specifications for this chapter
You cannot specify the patch version for Bicep modules in the public Bicep Registry, as this is automatically incremented by 1 each time a module is published. You can only set the Major and Minor versions.
See the Bicep Contribution Guide for more information.
Modules MUST use semantic versioning (aka semver) for their versions and releases in accordance with: Semantic Versioning 2.0.0
For example all modules should be released using a semantic version that matches this pattern: X.Y.Z
X
== Major VersionY
== Minor VersionZ
== Patch Version
Initially modules MUST be released as version
0.1.0
and incremented via Minor and Patch versions only until the AVM Core Team are confident the AVM specifications are mature enough and appropriate CI test coverage is in place, plus the module owner is happy the module has been “road tested” and is now stable enough for its first Major release of version1.0.0
.Releasing as version0.1.0
initially and only incrementing Minor and Patch versions allows the module owner to make breaking changes more easily and frequently as it’s still not an official Major/Stable release. 👍Until first Major version
1.0.0
is released, given a version numberX.Y.Z
:X
Major version MUST NOT be bumped.Y
Minor version MUST be bumped when introducing breaking changes (which would normally bump Major after1.0.0
release) or feature updates (same as it will be after1.0.0
release).Z
Patch version MUST be bumped when introducing non-breaking, backward compatible bug fixes (same as it will be after1.0.0
release).
A module SHOULD avoid breaking changes, e.g., deprecating inputs vs. removing. If you need to implement changes that cause a breaking change, the major version should be increased.
Modules that have not been released as1.0.0
may introduce breaking changes, as explained in the previous ID SNFR17 . That means that you have to introduce non-breaking and breaking changes with a minor version jump, as long as the module has not reached version1.0.0
.
There are, however, scenarios where you want to include breaking changes into a commit and not create a new major version. If you want to introduce breaking changes as part of a minor update, you can do so. In this case, it is essential to keep the change backward compatible, so that the existing code will continue to work. At a later point, another update can increase the major version and remove the code introduced for the backward compatibility.
See the language specific examples to find out how you can deal with deprecations in AVM modules.
Modules MUST be published to their respective language public registries.
- Bicep =
Bicep Public Module Registry
- Within the
avm
directory
- Within the
- Terraform = HashiCorp Terraform Registry
When the module owners of the same Resource or Pattern AVM module are not the same individual or team for all languages, each languages team SHOULD collaborate with their sibling language team for the same module to ensure consistency where possible.