diff --git a/docs/wiki/The library - Module design.md b/docs/wiki/The library - Module design.md index 75e68a49cd..b68f7ab560 100644 --- a/docs/wiki/The library - Module design.md +++ b/docs/wiki/The library - Module design.md @@ -48,10 +48,12 @@ They can be deployed in different configurations just by changing the input para > **Example:** The VirtualNetworkPeering construct leverages the VirtualNetworkPeering module to deploy multiple virtual network peering connections at once. - Where the resource type in question supports it, the module should have support for: 1. **Diagnostic logs** and **metrics** (you can have them sent to one ore more of the following destination types: storage account, log analytics and event hub). - 2. Resource and child resource level **RBAC** (for example, providing data contributor access on a storage account; granting file share/blob container level access in a storage account) - 3. **Tags** (as objects) - 4. **Locks** - 5. **Private Endpoints** (if supported) + 1. Resource and child-resource level **RBAC** (for example, providing data contributor access on a storage account; granting file share/blob container level access in a storage account) + 1. **Tags** (as objects) + 1. **Locks** + 1. **Private Endpoints** (if supported) + 1. **User-defined Types** (if supported) + 1. **Customer-managed Keys** (if supported) --- @@ -70,13 +72,7 @@ A **CARML module** consists of A module usually represents a single resource or a set of closely related resources. For example, a storage account and the associated lock or virtual machine and network interfaces. Modules are located in the `modules` folder. -Also, each module should be implemented with all capabilities it and its children support. This includes - -- `Locks` -- `Role assignments (RBAC)` -- `Diagnostic Settings` -- `Managed identities` -- `Private Endpoints`. +Also, as described in the [General Guidelines](#general-guidelines), each module should be implemented with all capabilities it and its children support. ## Structure @@ -107,12 +103,13 @@ module server_databases 'databases/main.bicep' = [for (database, index) in datab Use the following naming standard for module files and folders: -- Module folders are in camelCase and their name reflects the main resource type of the Bicep module they are hosting (e.g., `storageAccounts`, `virtualMachines`). +- A module's 'Provider namespace' folder is lowercase, avoids the `Microsoft.` prefix and uses a `-` as a separator whenever the API reference would have an upper case (for example `'Microsoft.RecoveryServices'` would have a folder name `'recovery-service'`) +- A module's 'Resource Type' folder is lowercase, singular and uses a `-` as a separator whenever the API reference would have an upper case (for example, `'storageAccounts'` would be `'storage-account'`, or `'virtualMachines'` be `virtual-machine`). - Extension resource modules are placed in the `.bicep` subfolder and named `nested_.bicep` ```txt - Microsoft. - └─ + + └─ ├─ .bicep | ├─ nested_extensionResource1.bicep ├─ .test @@ -121,11 +118,11 @@ Use the following naming standard for module files and folders: └─ README.md ``` - > **Example**: `nested_roleAssignments.bicep` in the `Microsoft.Web\sites\.bicep` folder contains the `site` resource RBAC implementation. + > **Example**: `nested_roleAssignments.bicep` in the `web\site\.bicep` folder contains the `site` resource's RBAC implementation. > > ```txt - > Microsoft.Web - > └─ sites + > web + > └─ site > ├─ .bicep > | └─ nested_roleAssignments.bicep > ├─ .test @@ -197,8 +194,12 @@ param roleAssignments array = [] module _roleAssignments '.bicep/nested_roleAssignments.bicep' = [for (roleAssignment, index) in roleAssignments: { name: '${deployment().name}-rbac-${index}' params: { + description: contains(roleAssignment, 'description') ? roleAssignment.description : '' principalIds: roleAssignment.principalIds + principalType: contains(roleAssignment, 'principalType') ? roleAssignment.principalType : '' roleDefinitionIdOrName: roleAssignment.roleDefinitionIdOrName + condition: contains(roleAssignment, 'condition') ? roleAssignment.condition : '' + delegatedManagedIdentityResourceId: contains(roleAssignment, 'delegatedManagedIdentityResourceId') ? roleAssignment.delegatedManagedIdentityResourceId : '' resourceId: .id } }] @@ -215,11 +216,41 @@ The `builtInRoleNames` variable contains the list of applicable roles for the sp The element requires you to provide both the `principalIds` & `roleDefinitionOrIdName` to assign to the principal IDs. Also, the `resourceId` is target resource's resource ID that allows us to reference it as an `existing` resource. Note, the implementation of the `split` in the resource reference becomes longer the deeper you go in the child resource hierarchy. ```bicep +@sys.description('Required. The IDs of the principals to assign the role to.') param principalIds array -param principalType string = '' + +@sys.description('Required. The name of the role to assign. If it cannot be found you can specify the role definition ID instead.') param roleDefinitionIdOrName string + +@sys.description('Required. The resource ID of the resource to apply the role assignment to.') param resourceId string +@sys.description('Optional. The principal type of the assigned principal ID.') +@allowed([ + 'ServicePrincipal' + 'Group' + 'User' + 'ForeignGroup' + 'Device' + '' +]) +param principalType string = '' + +@sys.description('Optional. The description of the role assignment.') +param description string = '' + +@sys.description('Optional. The conditions on the role assignment. This limits the resources it can be assigned to. e.g.: @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:ContainerName] StringEqualsIgnoreCase "foo_storage_container".') +param condition string = '' + +@sys.description('Optional. Version of the condition.') +@allowed([ + '2.0' +]) +param conditionVersion string = '2.0' + +@sys.description('Optional. Id of the delegated managed identity resource.') +param delegatedManagedIdentityResourceId string = '' + var builtInRoleNames = { 'Owner': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8e3af657-a8ff-443c-a75c-2fe8c4bcb635') 'Contributor': subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'b24988ac-6180-42a0-ab88-20f7382dd24c') @@ -240,9 +271,13 @@ resource '/@.id, principalId, roleDefinitionIdOrName) properties: { + description: description roleDefinitionId: contains(builtInRoleNames, roleDefinitionIdOrName) ? builtInRoleNames[roleDefinitionIdOrName] : roleDefinitionIdOrName principalId: principalId principalType: !empty(principalType) ? any(principalType) : null + condition: !empty(condition) ? condition : null + conditionVersion: !empty(conditionVersion) && !empty(condition) ? conditionVersion : null + delegatedManagedIdentityResourceId: !empty(delegatedManagedIdentityResourceId) ? delegatedManagedIdentityResourceId : null } scope: }] @@ -277,8 +312,9 @@ param diagnosticEventHubAuthorizationRuleId string = '' @description('Optional. Name of the diagnostic event hub within the namespace to which logs are streamed. Without this, an event hub is created for each log category.') param diagnosticEventHubName string = '' -@description('Optional. The name of logs that will be streamed. "allLogs" includes all possible logs for the resource.') +@description('Optional. The name of logs that will be streamed. "allLogs" includes all possible logs for the resource. Set to \'\' to disable log collection.') @allowed([ + '' 'allLogs' ]) @@ -297,7 +333,7 @@ param diagnosticMetricsToEnable array = [ @description('Optional. The name of the diagnostic setting, if deployed. If left empty, it defaults to "-diagnosticSettings".') param diagnosticSettingsName string = '' -var diagnosticsLogs = [for category in diagnosticLogCategoriesToEnable: { +var diagnosticsLogsSpecified = [for category in filter(diagnosticLogCategoriesToEnable, item => item != 'allLogs' && item != ''): { category: category enabled: true retentionPolicy: { @@ -306,6 +342,17 @@ var diagnosticsLogs = [for category in diagnosticLogCategoriesToEnable: { } }] +var diagnosticsLogs = contains(diagnosticLogCategoriesToEnable, 'allLogs') ? [ + { + categoryGroup: 'allLogs' + enabled: true + retentionPolicy: { + enabled: true + days: diagnosticLogsRetentionInDays + } + } +] : contains(diagnosticLogCategoriesToEnable, '') ? [] : diagnosticsLogsSpecified + var diagnosticsMetrics = [for metric in diagnosticMetricsToEnable: { category: metric timeGrain: null @@ -347,7 +394,7 @@ The Private Endpoint deployment has 2 elements. A module that contains the imple @description('Optional. Configuration details for private endpoints. For security reasons, it is recommended to use private endpoints whenever possible.') param privateEndpoints array = [] -module _privateEndpoints 'https://github.com/Azure/ResourceModules/blob/main/Microsoft.Network/privateEndpoints/main.bicep' = [for (privateEndpoint, index) in privateEndpoints: { +module _privateEndpoints 'https://github.com/Azure/ResourceModules/blob/main/network/private-endpoint/main.bicep' = [for (privateEndpoint, index) in privateEndpoints: { name: '${uniqueString(deployment().name, location)}--PrivateEndpoint-${index}' params: { groupIds: [ @@ -364,9 +411,11 @@ module _privateEndpoints 'https://github.com/Azure/ResourceModules tags: contains(privateEndpoint, 'tags') ? privateEndpoint.tags : {} manualPrivateLinkServiceConnections: contains(privateEndpoint, 'manualPrivateLinkServiceConnections') ? privateEndpoint.manualPrivateLinkServiceConnections : [] customDnsConfigs: contains(privateEndpoint, 'customDnsConfigs') ? privateEndpoint.customDnsConfigs : [] + ipConfigurations: contains(privateEndpoint, 'ipConfigurations') ? privateEndpoint.ipConfigurations : [] + applicationSecurityGroups: contains(privateEndpoint, 'applicationSecurityGroups') ? privateEndpoint.applicationSecurityGroups : [] + customNetworkInterfaceName: contains(privateEndpoint, 'customNetworkInterfaceName') ? privateEndpoint.customNetworkInterfaceName : '' } }] - ``` @@ -391,7 +440,7 @@ Within a bicep file, use the following conventions: - `Conditional` - The parameter value can be optional or required based on a condition, mostly based on the value provided to other parameters. - `Optional` - The parameter value is not mandatory. The module provides a default value for the parameter. - `Generated` - The parameter value is generated within the module and should not be specified as input. -- Parameters mapping to resource properties should align with resource property names as much as possible and should not artifictially include a resource type's name prefix to avoid redundancy. +- Parameters mapping to resource properties should align with resource property names as much as possible and should not artificially include a resource type's name prefix to avoid redundancy. > For example, the input parameter of the Key Vault module which maps to the `name` resource property should be just `name` and not `keyVaultName`. The rationale is that the consumers know that the name is for the Key Vault if they deploy its module. - If a property value allows a single value only, there is no need to introduce a parameter for it. Instead it can be hardcoded into the deployment. > For example, the name of a Blob Container Immutability Policy resource can only be `default`. Hence we can implement its name property directly as `name: 'default'`. @@ -471,7 +520,7 @@ Within a bicep file, use the following conventions: - Module symbolic names are in camel_Snake_Case, following the schema `_` e.g., `storageAccount_fileServices`, `virtualMachine_nic`, `resourceGroup_roleAssignments`. - Modules enable you to reuse code from a Bicep file in other Bicep files. As such, they're normally leveraged for deploying child resources (e.g., file services in a storage account), cross referenced resources (e.g., network interface in a virtual machine) or extension resources (e.g., role assignment in a resource group). -- When a module requires to deploy a resource whose resource type is outside of the main module's provider namespace, the module of this additional resource is referenced locally. For example, when extending the Key Vault module with Private Endpoints, instead of including in the Key Vault module an ad hoc implementation of a Private Endpoint, the Key Vault directly references the Private Endpoint module (i.e., `module privateEndpoint 'https://github.com/Azure/ResourceModules/blob/main/Microsoft.Network/privateEndpoints/main.bicep'`). Major benefits of this implementation are less code duplication, more consistency throughout the module library and allowing the consumer to leverage the full interface provided by the referenced module. +- When a module requires to deploy a resource whose resource type is outside of the main module's provider namespace, the module of this additional resource is referenced locally. For example, when extending the Key Vault module with Private Endpoints, instead of including in the Key Vault module an ad hoc implementation of a Private Endpoint, the Key Vault directly references the Private Endpoint module (i.e., `module privateEndpoint 'https://github.com/Azure/ResourceModules/blob/main/network/private-endpoint/main.bicep'`). Major benefits of this implementation are less code duplication, more consistency throughout the module library and allowing the consumer to leverage the full interface provided by the referenced module. > **Note**: Cross-referencing modules from the local repository creates a dependency for the modules applying this technique on the referenced modules being part of the local repository. Reusing the example from above, the Key Vault module has a dependency on the referenced Private Endpoint module, meaning that the repository from which the Key Vault module is deployed also requires the Private Endpoint module to be present. For this reason, we provide a utility to check for any local module references in a given path. This can be useful to determine which module folders you'd need if you don't want to keep the entire library. For further information on how to use the tool, please refer to the tool-specific [documentation](./Getting%20started%20-%20Get%20module%20cross-references). ### Deployment names @@ -600,9 +649,9 @@ Dependency file (`dependencies.bicep`) guidelines: - A special case to point out is the implementation of Key Vaults that require purge protection (for example, for Customer Managed Keys). As this implies that we cannot fully clean up a test deployment, it is recommended to generate a new name for this resource upon each pipeline run using the output of the `utcNow()` function at the time. - > :scroll: [Example of test using purge protected Key Vault dependency](https://github.com/Azure/ResourceModules/tree/main/modules/Batch/batchAccounts/.test/encr) + > :scroll: [Example of test using purge protected Key Vault dependency](https://github.com/Azure/ResourceModules/tree/main/modules/batch/batch-account/.test/encr) - - If you need a Deployment Script to set additional non-template resources up (for example certificates/files, etc.), we recommend to store it as a file in the shared `modules/.shared/.scripts` folder and load it using the template function `loadTextContent()` (for example: `scriptContent: loadTextContent('../../../../.shared/.scripts/New-SSHKey.ps1')`). This approach makes it easier to test & validate the logic and further allows reusing the same logic accross multiple test cases. + - If you need a Deployment Script to set additional non-template resources up (for example certificates/files, etc.), we recommend to store it as a file in the shared `modules/.shared/.scripts` folder and load it using the template function `loadTextContent()` (for example: `scriptContent: loadTextContent('../../../../.shared/.scripts/New-SSHKey.ps1')`). This approach makes it easier to test & validate the logic and further allows reusing the same logic across multiple test cases. # Telemetry diff --git a/docs/wiki/The library - Module usage.md b/docs/wiki/The library - Module usage.md index b75ce04014..0a8b56ea31 100644 --- a/docs/wiki/The library - Module usage.md +++ b/docs/wiki/The library - Module usage.md @@ -35,9 +35,9 @@ $inputObject = @{ ResourceGroupName = 'ExampleGroup' TemplateParameterFile = 'parameters.json' # Using a local reference - TemplateFile = "$home\ResourceModules\modules\KeyVault\vault\main.bicep" + TemplateFile = "$home\ResourceModules\modules\key-vault\vault\main.bicep" # Using a remote reference - # TemplateUri = 'https://raw.githubusercontent.com/Azure/ResourceModules/main/modules/KeyVault/vaults/main.bicep' + # TemplateUri = 'https://raw.githubusercontent.com/Azure/ResourceModules/main/modules/key-vault/vault/main.bicep' } New-AzResourceGroupDeployment @inputObject ``` @@ -59,9 +59,9 @@ $inputObject = @{ TemplateParameterFile = 'parameters.json' Location = 'EastUS2' # Using a local reference - TemplateFile = "$home\ResourceModules\modules\Resources\resourceGroups\main.bicep" + TemplateFile = "$home\ResourceModules\modules\resources\resource-group\main.bicep" # Using a remote reference - # TemplateUri = 'https://raw.githubusercontent.com/Azure/ResourceModules/main/modules/Resources/resourceGroups/main.bicep' + # TemplateUri = 'https://raw.githubusercontent.com/Azure/ResourceModules/main/modules/resources/resource-group/main.bicep' } New-AzDeployment @inputObject ``` @@ -84,9 +84,9 @@ $inputObject = @{ Location = 'EastUS2' TemplateParameterFile = 'parameters.json' # Using a local reference - TemplateFile = "$home\ResourceModules\modules\Authorization\policyAssignments\managementGroup\main.bicep" + TemplateFile = "$home\ResourceModules\modules\authorization\policy-assignment\management-group\main.bicep" # Using a remote reference - # TemplateUri = 'https://raw.githubusercontent.com/Azure/ResourceModules/main/modules/Authorization/policyAssignments/managementGroup/main.bicep' + # TemplateUri = 'https://raw.githubusercontent.com/Azure/ResourceModules/main/modules/authorization/policy-assignments/management-group/main.bicep' } New-AzManagementGroupDeployment @inputObject ``` @@ -108,9 +108,9 @@ $inputObject = @{ TemplateParameterFile = 'parameters.json' Location = 'EastUS2' # Using a local reference - TemplateFile = "$home\ResourceModules\modules\Subscription\aliases\main.bicep" + TemplateFile = "$home\ResourceModules\modules\subscription\alias\main.bicep" # Using a remote reference - # TemplateUri = 'https://raw.githubusercontent.com/Azure/ResourceModules/main/modules/Subscription/aliases/main.bicep' + # TemplateUri = 'https://raw.githubusercontent.com/Azure/ResourceModules/main/modules/subscription/alias/main.bicep' } New-AzTenantDeployment @inputObject ``` @@ -135,9 +135,9 @@ $inputObject = @( '--resource-group', 'ExampleGroup', '--parameters', '@parameters.json', # Using a local reference - '--template-file', "$home\ResourceModules\modules\Storage\storageAccounts\main.bicep", + '--template-file', "$home\ResourceModules\modules\storage\storage-account\main.bicep", # Using a remote reference - # '--template-uri', 'https://raw.githubusercontent.com/Azure/ResourceModules/main/modules/Storage/storageAccounts/main.bicep' + # '--template-uri', 'https://raw.githubusercontent.com/Azure/ResourceModules/main/modules/storage/storage-account/main.bicep' ) az deployment group create @inputObject ``` @@ -159,9 +159,9 @@ $inputObject = @( '--parameters', '@parameters.json', '--location', 'EastUS2', # Using a local reference - '--template-file', "$home\ResourceModules\modules\Resources\resourceGroups\main.bicep" + '--template-file', "$home\ResourceModules\modules\resources\resource-group\main.bicep" # Using a remote reference - # '--template-uri', 'https://raw.githubusercontent.com/Azure/ResourceModules/main/modules/Resources/resourceGroups/main.bicep' + # '--template-uri', 'https://raw.githubusercontent.com/Azure/ResourceModules/main/modules/resources/resource-group/main.bicep' ) az deployment sub create @inputObject ``` @@ -184,9 +184,9 @@ $inputObject = @( '--location', 'EastUS2', '--management-group-id', 'myManagementGroup', # Using a local reference - '--template-file', "$home\ResourceModules\modules\Authorization\policyAssignments\managementGroup\main.bicep" + '--template-file', "$home\ResourceModules\modules\authorization\policy-assignment\management-group\main.bicep" # Using a remote reference - # '--template-uri', 'https://raw.githubusercontent.com/Azure/ResourceModules/main/modules/Authorization/policyAssignments/managementGroup/main.bicep' + # '--template-uri', 'https://raw.githubusercontent.com/Azure/ResourceModules/main/modules/authorization/policy-assignment/management-group/main.bicep' ) az deployment mg create @inputObject ``` @@ -208,9 +208,9 @@ $inputObject = @( '--parameters', '@parameters.json', '--location', 'EastUS2', # Using a local reference - '--template-file', "$home\ResourceModules\modules\Subscription\aliases\main.bicep" + '--template-file', "$home\ResourceModules\modules\subscription\alias\main.bicep" # Using a remote reference - # '--template-uri', 'https://raw.githubusercontent.com/Azure/ResourceModules/main/modules/Subscription/aliases/main.bicep' + # '--template-uri', 'https://raw.githubusercontent.com/Azure/ResourceModules/main/modules/subscription/alias/main.bicep' ) az deployment tenant create @inputObject ``` @@ -225,21 +225,21 @@ You can also reference modules in another template using the below syntax. To de ```bicep // Using local reference -module testDeployment 'ResourceModules/modules/KeyVaults/vaults/main.bicep' = { +module testDeployment 'ResourceModules/modules/key-vault/vault/main.bicep' = { scope: resourceGroup name: '${uniqueString(deployment().name)}-example' params: { ... } } // Using Template-Specs reference (with configuration file) -module testDeployment 'ts/modules:keyvaults.vaults:1.0.0' = { +module testDeployment 'ts/modules:key-vault.vault:1.0.0' = { scope: resourceGroup name: '${uniqueString(deployment().name)}-example' params: { ... } } // Using Bicep reference -module testDeployment 'br:.azurecr.io/bicep/modules/keyvaults.vaults:1.0.0' = { +module testDeployment 'br:.azurecr.io/bicep/modules/key-vault.vault:1.0.0' = { scope: resourceGroup name: '${uniqueString(deployment().name)}-example' params: { ... }