This module provisions all required Azure resources for Terraform remote backend and Terraform Git Flow Automation pipeline configurations
Fully modular and flexible (see examples)
- Everything optional
- Any quantity
- Resources automatically linked (eg key vault ref in app config)
- Attributes fully customizable (eg. sku_name, public_access, etc.)
It provisions on Azure:
- Resource group
- App Configuration (tfvars)
- Storage container (remote backend)
- DevOps Pool (agents)
- Service Connection with WorkloadIdentityFederation (OIDC)
- Scope specific RBAC role assignments for above resources (service principal)
The following requirements are needed by this module:
The following resources are used by this module:
- modtm_telemetry.telemetry (resource)
- random_uuid.telemetry (resource)
- azapi_client_config.current (data source)
- azapi_client_config.telemetry (data source)
- modtm_module_source.telemetry (data source)
The following input variables are required:
Description: This variable helps connect the pool to the devops organization.
Type: string
Description: Name of the resource group.
Type: string
The following input variables are optional (have default values):
Description: A map of app configurations to create for tfvars. The map key is arbitrary; the value supports the following attributes. Defaults to {} (no app configurations).
-
name- (Required) The name of the Container which should be created within the Storage Account. Changing this forces a new resource to be created. -
location- (Optional) The Azure location where the resources will be deployed. Defaults to the location of the storage account. -
resource_group_name- (Optional) The resource group where the resources will be deployed. Defaults to the resource group of the storage account. -
public_network_access_enabled(Optional) Whether to enable public network access, default isfalse. -
sku- (Optional) The SKU of the resource. Valid values are free, developer, standard, and premium. Setsoft_delete_retention_daystonullfor free sku. Default isdeveloper -
purge_protection_enabled- (Optional) Whether to enable purge protection, default isfalse. -
soft_delete_retention_days- (Optional) The number of days that items are retained before being permanently deleted. Default isnull. -
vault_references- (Optional) Map of objects for vault references.secret_keyrefers to the key value from thekeysobject withinkeyvaultsinput variable. -
key_values- (Optional) A map a key-value pairs to prefill the configuration with. -
role_assignments- (Optional) A map of role assignments to create on the container. Defaults to{}. Seevar.role_assignmentsfor the attribute schema.
Example Input:
appconfigurations = {
tf_tfvars = {
name = module.naming.app_configuration.name_unique
purge_protection_enabled = false
soft_delete_retention_days = null
vault_references = {
key_ref_1 = {
name = "vault_ref1"
secret_key = "secret_1"
}
}
}
}Type:
map(object({
name = string
location = optional(string, null)
resource_group_name = optional(string, null)
public_network_access_enabled = optional(bool, false)
sku = optional(string, "developer")
purge_protection_enabled = optional(bool, false)
soft_delete_retention_days = optional(number, null)
vault_references = optional(map(object({
name = string
secret_key = string
})), {})
key_values = optional(map(object({
key = string
value = string
content_type = optional(string, null)
label = optional(string, null)
tags = optional(map(string), null)
})), {})
role_assignments = optional(map(object({
role_definition_id_or_name = string
principal_id = string
principal_type = optional(string, null)
description = optional(string, null)
skip_service_principal_aad_check = optional(bool, false)
condition = optional(string, null)
condition_version = optional(string, null)
delegated_managed_identity_resource_id = optional(string, null)
})), {})
}))Default: {}
Description: A map of dev centers to create for a pool. The map key is arbitrary; the value supports the following attributes. Defaults to {} (no dev centers).
name- (Required) The name of the Container which should be created within the Storage Account. Changing this forces a new resource to be created.location- (Optional) The Azure location where the resources will be deployed. Defaults to the location of the storage account.resource_group_name- (Optional) The resource group where the resources will be deployed. Defaults to the resource group of the storage account.projects- (Optional) A map of project objects. Defaults to{}.role_assignments- (Optional) A map of role assignments to create on the container. Defaults to{}. Seevar.role_assignmentsfor the attribute schema.
Example Input:
devcenters = {
center_1 = {
name = module.naming.app_configuration.name_unique
projects = {
project_1 = {
name = module.naming.app_configuration.name_unique
pools = {
pool1 = {
name = module.naming.app_configuration.name_unique
storage_account_type = "Standard"
maximum_concurrency = 1
profile_images = [
{
"aliases" : [
"ubuntu-24.04-g2",
"git-automation",
],
"well_known_image_name" : "ubuntu-24.04-g2/latest"
}
]
}
pool2 = {
name = module.naming.app_configuration.name_unique
maximum_concurrency = 1
}
}
}
}
}
}Type:
map(object({
name = string
location = optional(string, null)
resource_group_name = optional(string, null)
projects = optional(map(object({
name = string
location = optional(string, null)
resource_group_name = optional(string, null)
# center_key = string
pools = optional(map(object({
name = string
# project_key = string
maximum_concurrency = number
storage_account_type = optional(string, "Standard")
sku_name = optional(string, "Standard_D2ads_v5")
enable_telemetry = optional(bool, false)
prediction_profile_automatice = optional(string, "MostCostEffective")
# image_alias = optional(string, "az-pipeline")
# well_known_image_name = optional(string, "ubuntu-24.04-g2/latest")
profile_images = optional(list(object({
resource_id = optional(string)
well_known_image_name = optional(string)
buffer = optional(string, "*")
aliases = optional(list(string))
})), [
{
"aliases" : [
"ubuntu-24.04-g2",
"az-pipeline",
],
"well_known_image_name" : "ubuntu-24.04-g2/latest"
}
])
})), {})
role_assignments = optional(map(object({
role_definition_id_or_name = string
principal_id = string
principal_type = optional(string, null)
description = optional(string, null)
skip_service_principal_aad_check = optional(bool, false)
condition = optional(string, null)
condition_version = optional(string, null)
delegated_managed_identity_resource_id = optional(string, null)
})), {})
})), {})
role_assignments = optional(map(object({
role_definition_id_or_name = string
principal_id = string
principal_type = optional(string, null)
description = optional(string, null)
skip_service_principal_aad_check = optional(bool, false)
condition = optional(string, null)
condition_version = optional(string, null)
delegated_managed_identity_resource_id = optional(string, null)
})), {})
}))Default: {}
Description: ID of the Azure DevOps project.
Type: string
Default: null
Description: This variable controls whether or not telemetry is enabled for the module.
For more information see https://aka.ms/avm/telemetryinfo.
If it is set to false, then no telemetry will be collected.
Type: bool
Default: true
Description: Name of the environment to manage.
Type: string
Default: "todo"
Description: Optional. A map of key vaults to create which can be linked to an app config. The map key is arbitrary; the value supports the following attributes. Defaults to {} (no key vaults).
name- (Required) The name of the Container which should be created within the Storage Account. Changing this forces a new resource to be created.location- (Optional) The Azure location where the resources will be deployed. Defaults to the location of the storage account.resource_group_name- (Optional) The resource group where the resources will be deployed. Defaults to the resource group of the storage account.role_assignments- (Optional) A map of role assignments to create on the container. Defaults to{}. Seevar.role_assignmentsfor the attribute schema.network_acls- (Optional) The network ACL configuration for the Key Vault. If not specified then the Key Vault will be created with a firewall that blocks access. Specifynullto create the Key Vault with no firewall. -bypass- (Optional) Should Azure Services bypass the ACL. Possible values areAzureServicesandNone. Defaults toNone. -default_action- (Optional) The default action when no rule matches. Possible values areAllowandDeny. Defaults toDeny. -ip_rules- (Optional) A list of IP rules in CIDR format. Defaults to[]. -virtual_network_subnet_ids- (Optional) When using with Service Endpoints, a list of subnet IDs to associate with the Key Vault. Defaults to[].public_network_access_enabled- (Optional) Specifies whether public access is permitted. Defaultfalsesku- (Optional) The SKU name of the Key Vault. Default isstandard. Possible values arestandardandpremium.purge_protection_enabled- (Optional) Specifies whether protection against purge is enabled for this Key Vault. Note once enabled this cannot be disabled.soft_delete_retention_days- (Optional) The number of days that items should be retained for once soft-deleted. This value can be between 7 and 90 (the default) days.keys- (Optional) A map of keys to create on the Key Vault. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time. -name- The name of the key. -key_type- The type of the key. Possible values areECandRSA. -key_opts- A list of key options. Possible values aredecrypt,encrypt,sign,unwrapKey,verify, andwrapKey. -key_size- The size of the key. Required forRSAkeys. -curve- The curve of the key. Required forECkeys. Possible values areP-256,P-256K,P-384, andP-521. The API will default toP-256if nothing is specified. -not_before_date- The not before date of the key. -expiration_date- The expiration date of the key. -tags- A mapping of tags to assign to the key. -rotation_policy- The rotation policy of the key. -automatic- The automatic rotation policy of the key. -time_after_creation- The time after creation of the key before it is automatically rotated. -time_before_expiry- The time before expiry of the key before it is automatically rotated. -expire_after- The time after which the key expires. -notify_before_expiry- The time before expiry of the key when notification emails will be sent. Supply role assignments in the same way as forvar.role_assignments.secrets- (Optional) A map of secrets to create on the Key Vault. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time. -name- The name of the secret. -content_type- The content type of the secret. -tags- A mapping of tags to assign to the secret. -not_before_date- The not before date of the secret. -expiration_date- The expiration date of the secret. Supply role assignments in the same way as forvar.role_assignments. > Note: thevalueof the secret is supplied via thevar.secrets_valuevariable. Make sure to use the same map key.secrets_value- (Optional) A map of secret keys to values. The map key is the supplied input tovar.secrets. The map value is the secret value. This is a separate variable tovar.secretsbecause it is sensitive and therefore cannot be used in afor_eachloop.
Example Input:
keyvaults = {
key_vault_1 = {
name = module.naming.key_vault.name_unique
keys = {
secret_1 = {
name = module.naming.key_vault_key.name_unique
key_type = "RSA"
key_size = 2048
key_opts = [
"decrypt",
"encrypt",
"sign",
"unwrapKey",
"verify",
"wrapKey"
]
enabled = true
}
}
secrets = {
secret_1 = {
name = module.naming.key_vault_secret.name_unique
}
}
secrets_value = {
secret_1 = "supersecretpassword123"
}
}
}Type:
map(object({
name = string
location = optional(string, null)
resource_group_name = optional(string, null)
network_acls = optional(object({
bypass = optional(string, "None")
default_action = optional(string, "Deny")
ip_rules = optional(list(string), [])
virtual_network_subnet_ids = optional(list(string), [])
})
, null)
public_network_access_enabled = optional(bool, false)
sku = optional(string, "standard")
purge_protection_enabled = optional(bool, false)
soft_delete_retention_days = optional(number, null)
keys = optional(map(object({
name = string
key_type = string
key_opts = optional(list(string), ["sign", "verify"])
key_size = optional(number, null)
curve = optional(string, null)
not_before_date = optional(string, null)
expiration_date = optional(string, null)
tags = optional(map(any), null)
role_assignments = optional(map(object({
role_definition_id_or_name = string
principal_id = string
description = optional(string, null)
skip_service_principal_aad_check = optional(bool, false)
condition = optional(string, null)
condition_version = optional(string, null)
delegated_managed_identity_resource_id = optional(string, null)
principal_type = optional(string, null)
})), {})
rotation_policy = optional(object({
automatic = optional(object({
time_after_creation = optional(string, null)
time_before_expiry = optional(string, null)
}), null)
expire_after = optional(string, null)
notify_before_expiry = optional(string, null)
}), null)
})), {})
secrets = optional(map(object({
name = string
content_type = optional(string, null)
tags = optional(map(any), null)
not_before_date = optional(string, null)
expiration_date = optional(string, null)
role_assignments = optional(map(object({
role_definition_id_or_name = string
principal_id = string
description = optional(string, null)
skip_service_principal_aad_check = optional(bool, false)
condition = optional(string, null)
condition_version = optional(string, null)
delegated_managed_identity_resource_id = optional(string, null)
principal_type = optional(string, null)
})), {})
})), {})
secrets_value = optional(map(string), null)
role_assignments = optional(map(object({
role_definition_id_or_name = string
principal_id = string
principal_type = optional(string, null)
description = optional(string, null)
skip_service_principal_aad_check = optional(bool, false)
condition = optional(string, null)
condition_version = optional(string, null)
delegated_managed_identity_resource_id = optional(string, null)
})), {})
}))Default: {}
Description: Azure region where the resource should be deployed.
Type: string
Default: "West Europe"
Description: Controls the Resource Lock configuration for this resource. The following properties can be specified:
kind- (Required) The type of lock. Possible values are\"CanNotDelete\"and\"ReadOnly\".name- (Optional) The name of the lock. If not specified, a name will be generated based on thekindvalue. Changing this forces the creation of a new resource.
Type:
object({
kind = string
name = optional(string, null)
})Default: null
Description: This variable controls whether or not purge_protection is enabled for the module.
Type: bool
Default: true
Description: Override the AzAPI <provider>/<resource>@<api-version> strings used by this module. Each key defaults to a tested value; supply only the keys you want to override. Useful when targeting a sovereign cloud with older API versions, or when opting into a newer preview API.
devcenter- The devcenter, used by the project.project- The project, used by the pool.lock- Management lock applied to the storage account (and to private endpoints when configured).
Type:
object({
resourcegroup = optional(string, "Microsoft.Resources/resourceGroups@2025-04-01")
devcenter = optional(string, "Microsoft.DevCenter/devCenters@2025-02-01")
project = optional(string, "Microsoft.DevCenter/projects@2025-02-01")
lock = optional(string, "Microsoft.Authorization/locks@2020-05-01")
})Default: {}
Description: A map of role assignments to create on this resource. The map key is deliberately arbitrary to avoid issues where map keys maybe unknown at plan time.
role_definition_id_or_name- The ID or name of the role definition to assign to the principal.principal_id- The ID of the principal to assign the role to.description- The description of the role assignment.skip_service_principal_aad_check- If set to true, skips the Azure Active Directory check for the service principal in the tenant. Defaults to false.condition- The condition which will be used to scope the role assignment.condition_version- The version of the condition syntax. Valid values are '2.0'.delegated_managed_identity_resource_id- The delegated Azure Resource Id which contains a Managed Identity. Changing this forces a new resource to be created.principal_type- The type of the principal_id. Possible values areUser,GroupandServicePrincipal. Changing this forces a new resource to be created. It is necessary to explicitly set this attribute when creating role assignments if the principal creating the assignment is constrained by ABAC rules that filters on the PrincipalType attribute.
Note: only set
skip_service_principal_aad_checkto true if you are assigning a role to a service principal.
Type:
map(object({
role_definition_id_or_name = string
principal_id = string
description = optional(string, null)
skip_service_principal_aad_check = optional(bool, false)
condition = optional(string, null)
condition_version = optional(string, null)
delegated_managed_identity_resource_id = optional(string, null)
principal_type = optional(string, null)
}))Default: {}
Description: A reference to the service connection object and used to assign roles to a service principal. Required if serviceconnections is defined.
Example Input:
serviceconnections = {
oidc_wip = {
name = "Managed Terraform Git Automation Service Connection"
...
}
}
service_connection_key = "oidc_wip"Type: string
Default: null
Description: A map of service connections to create on the parent subscription. The map key is arbitrary; the value supports the following attributes. Defaults to {} (no service connections).
name- (Required) The Service Endpoint Name.devops_project_name- (Required) The Project Name.app_registration_name- (Optional) The display name for the application.service_endpoint_authentication_scheme(WIP) Specifies the type of Azure Resource Manager Service Endpoint. Possible values are WorkloadIdentityFederation, ManagedServiceIdentity or ServicePrincipal. Defaults to ServicePrincipal for backwards compatibility.
Example Input:
serviceconnections = {
oidc_wip = {
name = "Managed Terraform Git Automation Service Connection"
devops_project_name = local.project_name
application_name = "Managed Terraform Git Automation Application"
}
}
service_connection_key = "oidc_wip"Type:
map(object({
name = string
application_name = optional(string, null)
service_endpoint_authentication_scheme = optional(string, "ServicePrincipal")
}))Default: {}
Description: The number of days that items are retained before being permanently deleted. Default is 7. Set to null for free sku.
Type: number
Default: 7
Description: A map of to create on the parent resource group. The map key is arbitrary; the value supports the following attributes. Defaults to {} (no storage containers).
name- (Required) The name of the Container which should be created within the Storage Account. Changing this forces a new resource to be created.location- (Optional) The Azure location where the resources will be deployed. Defaults to the location of the storage account.resource_group_name- (Optional) The resource group where the resources will be deployed. Defaults to the resource group of the storage account.account_sku_name(Optional) Explicit storage account SKU name (e.g.Standard_LRS,Premium_ZRS,PremiumV2_LRS,StandardV2_GZRS). When set, this value is sent to Azure verbatim and overrides the SKU derived fromaccount_tier,account_replication_typeandprovisioned_billing_model_version- those variables are only honoured whenaccount_sku_nameis explicitly set tonull. Defaults toStandard_ZRS. Note: the*V2_*SKUs (e.g.StandardV2_ZRS,PremiumV2_ZRS) requireaccount_kind = "FileStorage". Default: "Standard_ZRS"public_network_access_enabled(Optional) Whether the public network access is enabled? Defaults tofalse.network_rules(Optional) Network rules restricting access to the storage account. Defaults to{}, which applies the object's own per-attribute defaults (effectivelydefault_action = "Deny"withbypass = ["AzureServices"]). > Note: the default value blocks all public access to the storage account. If you want to disable all network rules, set this value tonull. -bypass- (Optional) Specifies whether traffic is bypassed for Logging/Metrics/AzureServices. Valid options are any combination ofLogging,Metrics,AzureServices, orNone. Defaults to["AzureServices"]. -default_action- (Optional) Specifies the default action of allow or deny when no other rules match. Valid options areDenyorAllow. Defaults toDeny. -ip_rules- (Optional) List of public IP or IP ranges in CIDR format. Only IPv4 addresses are allowed. Private IP address ranges (as defined in RFC 1918) are not allowed. Defaults to[]. -virtual_network_subnet_ids- (Optional) A set of virtual network subnet IDs to secure the storage account. Defaults to[]. -private_link_access- (Optional) A list of private link access rules. Defaults tonull. Each entry supports: -endpoint_resource_id- (Required) The resource ID of the resource access rule to be granted access. -endpoint_tenant_id- (Optional) The tenant ID of the resource of the resource access rule to be granted access. Defaults to the current tenant ID. -timeouts- (Optional) Per-operation timeouts for the network rules resource. Defaults tonull(uses provider defaults). Supports: -create- (Optional) Timeout for create operations. -delete- (Optional) Timeout for delete operations. -read- (Optional) Timeout for read operations. -update- (Optional) Timeout for update operations. Default {}role_assignments- (Optional) A map of role assignments to create on the container. Defaults to{}. Seevar.role_assignmentsfor the attribute schema.
Example Input:
storageaccounts = {
tf_state_account = {
name = module.naming.storage_account.name_unique
containers = {
tf_state_container = {
name = "tfstate"
}
}
}
}Type:
map(object({
name = string
location = optional(string, null)
resource_group_name = optional(string, null)
account_sku_name = optional(string, "Standard_LRS")
shared_access_key_enabled = optional(bool, false)
default_to_oauth_authentication = optional(bool, true)
public_network_access_enabled = optional(bool, false)
network_rules = optional(object({
bypass = optional(set(string), ["AzureServices"])
default_action = optional(string, "Deny")
ip_rules = optional(set(string), [])
virtual_network_subnet_ids = optional(set(string), [])
private_link_access = optional(list(object({
endpoint_resource_id = string
endpoint_tenant_id = optional(string)
})))
timeouts = optional(object({
create = optional(string)
delete = optional(string)
read = optional(string)
update = optional(string)
}))
}), null)
containers = optional(map(object({
name = optional(string, "tfstate")
container_access_type = optional(string, "private")
rbac_authorization_enabled = optional(bool, true)
role_assignments = optional(map(object({
role_definition_id_or_name = string
principal_id = string
principal_type = optional(string, null)
description = optional(string, null)
skip_service_principal_aad_check = optional(bool, false)
condition = optional(string, null)
condition_version = optional(string, null)
delegated_managed_identity_resource_id = optional(string, null)
})), {})
})), {})
}))Default: {}
Description: (Optional) Tags of the resource.
Type: map(string)
Default: null
The following outputs are exported:
Description: A map of all created app config endpoints
Description: A map of all created storage account names
Description: A map of all created storage container names
Description: The resource group.
Description: The azapi_client_config tenant ID
Description: The service principal client ID
Description: The service principal client ID
The following Modules are called:
Source: Azure/avm-res-appconfiguration-configurationstore/azure
Version: 0.5.1
Source: Azure/avm-res-resources-resourcegroup/azurerm
Version: 0.4.0
Source: ./modules/devcenter
Version:
Source: Azure/avm-res-keyvault-vault/azurerm
Version: 0.10.2
Source: Azure/avm-res-devopsinfrastructure-pool/azurerm
Version: 0.3.1
Source: ./modules/project
Version:
Source: ./modules/serviceconnection
Version:
Source: Azure/avm-res-storage-storageaccount/azurerm
Version: 0.7.2
Azure DevOps pipeline integration (see pipelines folder):
- Generate Terraform pipeline plan on PR creation
- Apply Terraform pipeline plan on PR merge
- Agnostic multi-environment (eg. development, staging, prod1, prod2)
- Connected with the service connection
- DevOps Pool integration
- App config integration
- Simple one-time setup using Variable(s) (groups)
It will connect everything automatically:
- TFVARS pre-filled in the app config (with key vault compatibility)
- Example backend config pre-filled (for state migration)
- DevOps pools using alias
Azure DevOps Marketplace extension:
The following pipelines are used by this module:
Description: The Plan stage in this pipeline can be configured to trigger on the creation of a PR to generate a Terraform plan for those changes. Will be used in conjunction with the service connection that was provisioned by the module.
Jobs
Generate- Job for generating and posting a plan based on the Terraform config changes in the PR.
Variables:
environment_name- (Required) The source folder for the Terraform configuration, used to make the pipeline multi-environment.trigger- (Optional) The trigger is configured through branch policies. Defaults tonone.pool- (Optional) The agent pool where the pipeline will be deployed. Defaults to$(environment_name)-motte-pipeline.
Steps:
Configure git authentication header- Uses Git CLI to set OIDC WIP Authentication HeadersExtract App Config as File- Uses Azure CLI to fetch tfvars from App Configuration using theapp-config-endpointpipeline variable.Terraform Init- Uses Terraform Extension task to initialize Terraform.Terraform Plan- Uses Terraform Extension task to generate a plan and convert it to a text format.Publish Artifact- Uses Azure task to publish the plan as artifactPost Comment- Uses Azure CLI to post the plan as a comment to the connected PR.
Description: This pipeline can be configured to trigger when changes are merged to a branch that manages Terraform configurations. Will be used in conjunction with the service connection that was provisioned by the module.
Jobs:
Apply- Job for applying a Terraform plan after the PR is approved and merged.Comment- Job for retrieving and posting apply output from applying a Terraform Plan.
Variables:
environment_name- (Required) The source folder for the Terraform configuration, used to make the pipeline multi-environment.trigger- (Optional) The trigger is configured on branch merge and can be overridden for secondary staging environments. Defaults tomaster.pool- (Optional) The agent pool where the pipeline will be deployed. Defaults to$(environment_name)-motte-pipeline.
Steps:
Configure git authentication header- Uses Git CLI to set OIDC WIP Authentication HeadersDownload Artifact- Download the Artifact from the Plan pipeline.Terraform Init- Uses Terraform Extension task to initialize Terraform.Terraform Apply- Uses Terraform Extension task to apply a plan and convert the output to a text format.Get PR ID- Uses Azure CLI to retrieve the PR ID.Post Comment- Uses Azure CLI to post the output as a comment to the connected PR.
The software may collect information about you and your use of the software and send it to Microsoft. Microsoft may use this information to provide services and improve our products and services. You may turn off the telemetry as described in the repository. There are also some features in the software that may enable you and Microsoft to collect data from users of your applications. If you use these features, you must comply with applicable law, including providing appropriate notices to users of your applications together with a copy of Microsoft’s privacy statement. Our privacy statement is located at https://go.microsoft.com/fwlink/?LinkID=824704. You can learn more about data collection and use in the help documentation and our privacy statement. Your use of the software operates as your consent to these practices.