I am trying to deploy an Azure Policy Assignment with Bicep.
resource policy_assignment 'Microsoft.Authorization/policyAssignments#2021-06-01' = {
name: 'my_policy'
location: 'westus'
scope: subscriptionResourceId('Microsoft.Resources/resourceGroups', resourceGroup().name)
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'/subscriptions/xxxxxxx-xxxxxx-xxxx-xxx/resourceGroups/my-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/mymi': {}
}
}
properties: {
parameters: {
MyParamKey: '/subscriptions/xxxxx-xxx-xxxx-xxx-xxx/resourcegroups/my-rg2/providers/microsoft.network/virtualnetworks/vnetmy/subnets/default'
}
policyDefinitionId: '/subscriptions/xxxxx-xxx-xxxx-xxx-xxx//providers/Microsoft.Authorization/policyDefinitions/my-policy-def'
}
}
When I check it with az bicep build --file .\policy_assignment.bicep , I get the error below:
C:$Path.bicep(4,10) : Error BCP036: The property "scope" expected a value of type "resource | tenant" but the provided value is of type "string".
C:$Path.bicep(13,32) : Warning BCP036: The property "MyParamKey" expected a value of type "ParameterValuesValue" but the provided value is of type "'/subscriptions/xxxxx-xxx/resourcegroups/my-rg2/providers/microsoft.network/virtualnetworks/vnetmy/subnets/default'".
I have two problems:
Definition of the scope of the policy assignment.
Definition of the parameter of the assignment
I couldn't find much example on the internet. The documentation of the Policy Assignment for Bicep is here.
Do you have any idea how can I correct these errors?
This resource type most probably expects parameter values to be wrapped in objects with a value like :
parameters: {
MyParamKey: {
value: '/subscriptions/xxxxx-xxx-xxxx-xxx-xxx/resourcegroups/my-rg2/providers/microsoft.network/virtualnetworks/vnetmy/subnets/default'
}
}
There are some other use cases like this one.
EDIT : As stated by #Thomas, the scope should be referred as scope: resourceGroup() since this is dynamically retrieved by your client with the right type Bicep is waiting for.
Related
I am trying to create two reusable bicep modules to allow reading specific secrets in chosen key vaults. To do this, I first declare the role definition:
targetScope = 'subscription'
param subscriptionId string
param resourceGroupName string
param keyVaultName string
param allowedSecrets array
param managementGroupRoot string
var keyVaultScope = '/subscriptions/${subscriptionId}/resourcegroups/${resourceGroupName}/providers/Microsoft.KeyVault/vaults/${keyVaultName}'
var assignableScopes = [for secretName in allowedSecrets: '${keyVaultScope}/secrets/${secretName}']
var roleName = 'Limitied ${keyVaultName} secret reader ${managementGroupRoot}'
// Permissions based on Key Vault Secrets User
// https://www.azadvertizer.net/azrolesadvertizer/4633458b-17de-408a-b874-0445c86b69e6.html
resource key_vault_secrets_user_role_definition 'Microsoft.Authorization/roleDefinitions#2018-01-01-preview' existing = {
name: '4633458b-17de-408a-b874-0445c86b69e6'
}
resource role_definition 'Microsoft.Authorization/roleDefinitions#2018-01-01-preview' = {
name: guid(roleName)
properties: {
roleName: roleName
description: 'Allows reading specific secrets in the ${keyVaultName} key vault in ${managementGroupRoot}'
assignableScopes: assignableScopes
permissions: key_vault_secrets_user_role_definition.properties.permissions
}
}
output roleDefinitionId string = role_definition.id
The role definition creation works well, and it results in this role definition:
{
"assignableScopes": [
"/subscriptions/subscriptionId/resourcegroups/resourceGroupName/providers/Microsoft.KeyVault/vaults/keyVaultName/secrets/secretName",
"/subscriptions/subscriptionId/resourcegroups/resourceGroupName/providers/Microsoft.KeyVault/vaults/keyVaultName/secrets/anotherSecret"
],
"description": "Allows reading specific secrets in the xxx} key vault in xxx",
"id": "/subscriptions/xxx/providers/Microsoft.Authorization/roleDefinitions/xxx",
"name": "c64aa8eb-479d-5c2d-8f25-b1acb151c0af",
"permissions": [
{
"actions": [],
"dataActions": [
"Microsoft.KeyVault/vaults/secrets/getSecret/action",
"Microsoft.KeyVault/vaults/secrets/readMetadata/action"
],
"notActions": [],
"notDataActions": []
}
],
"roleName": "Limitied key vault secret reader xxx",
"roleType": "CustomRole",
"type": "Microsoft.Authorization/roleDefinitions"
}
Next, I want to assign this role to a service principal. Here's where I'm not entirely clear on the details, but since I want this principal to be able to read n number of individual secrets, I made the assmuption that I would need to iterate on the assignable scopes.
To do that, I have a main file:
targetScope = 'managementGroup'
resource roleDefinition 'Microsoft.Authorization/roleDefinitions#2018-01-01-preview' existing = {
name: roleDefinitionId
}
module example 'module.bicep' = {
name: 'example-${managementGroup().name}'
scope: resourceGroup(keyVaultSubscriptionId, keyVaultResourceGroupName)
params: {
roleDefinitionId: roleDefinitionId
assignableScopes: roleDefinition.properties.assignableScopes
managementGroupName: managementGroup().name
keyVaultName: keyVaultName
}
}
The module then looks like this:
targetScope = 'resourceGroup'
param roleDefinitionId string
param assignableScopes array = []
param managementGroupName string
param keyVaultName string
param principalId string
// Full scope looks like this:
// '/subscriptions/<sub>/resourcegroups/<rg>/providers/Microsoft.KeyVault/vaults/<vault>/<secret>'
// Hence 8 is the secret name
// Also verifies that the secrets exist
var secretNames = [for scope in assignableScopes: split(scope, '/')[8]]
resource secretResources 'Microsoft.KeyVault/vaults/secrets#2021-11-01-preview' existing = [for secret in secretNames: {
name: '${keyVaultName}/${secret}'
}]
// Iterating the secretResources array is not supported, so we iterate the scope which they are based
resource regressionTestKeyVaultReaderAssignment 'Microsoft.Authorization/roleAssignments#2020-04-01-preview' = [for (scope, index) in assignableScopes: {
name: guid(managementGroupName, principalId, scope)
scope: secretResources[index] // Access by index and apply this role assignment to all assignable scopes
properties: {
principalId: principalId
roleDefinitionId: roleDefinitionId
}
}]
However, this fails with the following error:
ERROR: ***"code": "InvalidTemplate", "message": "Deployment template validation failed: 'The template resource 'exmaple-xxx' at line '97' and column '5' is not valid: Unable to evaluate template language function 'extensionResourceId': function requires exactly two multi-segmented arguments. The first must be the parent resource id while the second must be resource type including resource provider namespace. Current function arguments '/providers/Microsoft.Management/managementGroups/ESD,Microsoft.Authorization/roleDefinitions,/subscriptions/***/providers/Microsoft.Authorization/roleDefinitions/xxx'. Please see https://aka.ms/arm-template-expressions/#extensionresourceid for usage details.. Please see https://aka.ms/arm-template-expressions for usage details.'.", "additionalInfo": [***"type": "TemplateViolation", "info": ***"lineNumber": 97, "linePosition": 5, "path": "properties.template.resources[6]"***]***
I am using az to deploy in a GitHub pipeline so I tried to access the request and response, to no avail:
$deployment = az deployment mg create | ConvertFrom-Json // additional params
Write-Host "Request: $(ConvertTo-Json -InputObject $deployment.request)" // Request: null
Write-Host "Response: $(ConvertTo-Json -InputObject $deployment.response)" // Response: null
The error is very cryptic to me and I don't really know what is going on as I'm not even using that utility method that is being referenced. I'm guessing the conversion to ARM does something in the background. vscode says everything is fine and dandy.
What am I doing wrong? My only guess is the scope part of the assignment, but I have no ideas on how to correct it.
Any help would be greatly appreciated.
Update
Some additional information that I found while trying to solve this. The validation of the template fails and the deployment doesn't even start. I built both the main and the module bicep files to see if that would give some additional context. The module looks fine but main has an error on the module resource:
So this is in the main file with targetScope = 'managementGroup', and the module with targetScope = 'resourceGroup' shows no validation errors when built.
Update 2
When compiled to ARM, I see the following value is passed from main to the module:
"assignableScopes": {
"value": "[reference(extensionResourceId(managementGroup().id, 'Microsoft.Authorization/roleDefinitions', parameters('secretReaderRoleDefinitionId')), '2018-01-01-preview').assignableScopes]"
},
AFAICT this is 3 arguments, and the error I get in the GitHub pipeline says:
Unable to evaluate template language function 'extensionResourceId': function requires exactly two multi-segmented arguments.
That doesn't seem to be true when reading the docs about that function.
Update 3
The error is produced in a GitHub pipeline where I'm running on ubuntu-latest. I'm going to replicate the same command locally and see If I can get it to work here in case of a runner issue.
Update 4
Exact same error reproduced outside of the GitHub pipeline.
A couple thoughts...
Creating a custom roleDef with limited assignable scopes doesn't have a ton of value from a security perspective, because the built-in roleDef has the same permissions has a broader scope - and the principal that assigns one would be able to assign the other.
If your goal is to simply iterate over the secrets and assign the role to those secrets all you need is the resourceId of those secrets. It looks like you're trying to pull that list from the roleDefinition (instead of passing to the template) which is possible but seems somewhat complex. That would mean that any time you want to "adjust" this deployment you have to define a new role or modify the existing, both have some downstream consequences. There are a finite number of custom roles that can be defined in a tenant and as you change them you could break existing assignments unintentionally (either remove access or inadvertently give access to new ones).
That said, I don't see that specific error in your code but perhaps a few others - try this:
main.bicep
targetScope = 'managementGroup'
param roleDefinitionId string
param keyVaultSubscriptionId string
param keyVaultResourceGroupName string
param keyVaultName string
param principalId string
resource roleDefinition 'Microsoft.Authorization/roleDefinitions#2018-01-01-preview' existing = {
scope: subscription(keyVaultSubscriptionId)
name: roleDefinitionId
}
module example 'module.bicep' = {
name: 'example-${managementGroup().name}'
scope: resourceGroup(keyVaultSubscriptionId, keyVaultResourceGroupName)
params: {
roleDefinitionId: roleDefinitionId
assignableScopes: roleDefinition.properties.assignableScopes
keyVaultName: keyVaultName
principalId: principalId
}
}
module.bicep
targetScope = 'resourceGroup'
param roleDefinitionId string
param assignableScopes array
param keyVaultName string
param principalId string
var secretNames = [for scope in assignableScopes: last(split(scope, '/'))]
resource secretResources 'Microsoft.KeyVault/vaults/secrets#2021-11-01-preview' existing = [for secret in secretNames: {
name: '${keyVaultName}/${secret}'
}]
resource roleDef 'Microsoft.Authorization/roleDefinitions#2022-04-01' existing = {
name: roleDefinitionId
}
resource regressionTestKeyVaultReaderAssignment 'Microsoft.Authorization/roleAssignments#2020-04-01-preview' = [for (scope, index) in assignableScopes: {
name: guid(roleDef.id, principalId, scope)
scope: secretResources[index]
properties: {
principalId: principalId
roleDefinitionId: roleDef.id
}
}]
Suppose I have two files/modules in Azure Bicep, both are called in a 'main.bicep'. One is called 'storage.bicep' and contains, among others, the following code to create a storageAccount:
resource storageAccountTemp 'Microsoft.Storage/storageAccounts#2021-08-01' = {
name: 'tmpst4dnbnlp'
location: location
sku: {
name: storageAccountSku
}
kind: 'StorageV2'
properties: {
allowBlobPublicAccess: false
accessTier: 'Hot'
}
}
Another file contains some LogicApp definitions and is called 'orchestration.bicep'. Now in this file, there is a part where I want to reference the 'storageAccountTemp' resource in module 'storage.bicep', as to provide the LogicApp system managed identity access the contributor role for the:
resource logicAppStorageAccountRoleAssignment 'Microsoft.Authorization/roleAssignments#2020-10-01-preview' = {
scope: 'xxx'
name: guid('ra-logicapp-${roleDefinitionId}')
properties: {
principalType: 'ServicePrincipal'
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId)
principalId: logicAppTest.identity.principalId
}
}
Where I need to specify the scope (that now says 'xxx'). I can't say resourceGroup() since the storage is in a different resource group. Instead, I want to reference the storageAccountTemp object. This seems impossible to do when the object is in a different module (I tried outputting the name and id and using these but this was not accepted by Bicep.
Is there any way I can actually reference the original storageAccountTemp object from 'storage.bicep' in the 'orchestration.bicep' file?
You need to use an existing resource declaration. So you'll have something like:
resource storageAccountTemp 'Microsoft.Storage/storageAccounts#2021-08-01' existing = {
scope: resourceGroup('blah')
name: 'tmpst4dnbnlp
}
And then you can use that for the scope property on the roleAssignment. How you get blah (the resourceGroup name) and the storageAccount name to the roleAssignment module depends... if the two modules are peers in the orchestrator, then usually those params are known and can be passed to both modules. Failing that you can use outputs from the storage module and pass those in as params to the roleAssignment.
That help?
From my "main" bicep module, I would like to reference an existing function that is created by a module called from the same "main" bicep. So used the following code:
resource functionApp 'Microsoft.Web/sites#2021-02-01' existing = {
name: functionAppName
scope: resourceGroup(subscriptionId, 'rg-365response-${env}-001')
}
I am then able to use properties from the "functionApp" resource variable to obtain the function key and store as a key vault secret as follows:
resource funcSecret 'Microsoft.KeyVault/vaults/secrets#2021-04-01-preview' = {
name: '${kvName}/funcAppKey'
properties: {
value: listKeys('${functionApp.id}/host/default', functionApp.apiVersion).functionKeys.default
}
}
However, when I run a resource group deployment and see the following error:
The Resource 'Microsoft.Web/sites/func-365response-int-001' under
resource group 'rg-365response-int-001' was not found
This is some kind of timing issue, I guess it's checking for the function app before the call to the module that creates it has had chance to complete.
If I run the "main" bicep module a second time, everything works okay.
It seems it's not possible to use the "dependsOn" syntax for a resource that is "existing".
Is there an alternative?
DependOns can only be used for resources defined in the same bicep file (ARM template).
When you use the existing keyword, it will compiled to a resourceId() or reference() by Bicep
You could create a module to create secret:
// key-vault-secret.bicep
param kvName string
param secretName string
#secure()
param secretValue string
resource kvSecret 'Microsoft.KeyVault/vaults/secrets#2021-04-01-preview' = {
name: '${kvName}/${secretName}'
properties: {
value: secretValue
}
}
Then from where you are creating your function, you could invoke it like that:
resource functionApp 'Microsoft.Web/sites#2021-03-01' = {
name: functionAppName
location: location
kind: 'functionapp'
...
}
// Add secret to KV
module functionKey 'key-vault-secret.bicep' = {
name: 'function-default-host-key'
scope: resourceGroup()
params:{
kvName: kvName
secretName: 'funcAppKey'
secretValue: listKeys('${functionApp.id}/host/default', functionApp.apiVersion).functionKeys.default
}
}
I think you are correct in that the listKeys() is called too early, you can't fix it with dependsOn unfortunately. There is a bit more explanation here: https://bmoore-msft.blog/2020/07/26/resource-not-found-dependson-is-not-working/
The only fix for this is to put the listKeys and the function into different modules and make sure you have dependsOs if the second module doesn't consume an input from the first.
The part that's not adding up for me is that you have an existing keyword on the resource in the sample above but you say you're creating it. The symptoms you describe also suggest you're creating it in the same deployment. If you are, they you don't need the `existing' keyword.
If all else fails - post all the code.
I've created a Bicep template. In it I create a user-assigned identity and reference it in other resources like this
var identityName = 'mid-dd-test'
var roleName = 'TestRole'
var roleDescription = 'Some test'
var roleScopes = [
resourceGroup().id
]
var resolvedActions = [
'Microsoft.Resources/subscriptions/resourcegroups/*'
'Microsoft.Compute/sshPublicKeys/*'
]
var permittedDataActions = []
resource userId 'Microsoft.ManagedIdentity/userAssignedIdentities#2018-11-30' = {
name: identityName
location: resourceGroup().location
}
resource roleDef 'Microsoft.Authorization/roleDefinitions#2018-01-01-preview' = {
name: guid(subscription().id, 'bicep', 'dsadsd')
properties: {
roleName: roleName
description: roleDescription
type: 'customRole'
assignableScopes: roleScopes
permissions: [
{
actions: resolvedActions
dataActions: permittedDataActions
}
]
}
}
resource roles 'Microsoft.Authorization/roleAssignments#2018-09-01-preview' = {
name: guid(subscription().id, 'bicep-roleassignments', 'dsddsd')
properties: {
principalId: userId.properties.principalId
roleDefinitionId: roleDef.id
}
}
Whenever I deploy this I need 2 runs. The first run ends in the error message:
Principal XXX does not exist in the directory YYY
where XXX would be a principal id the user-assigned identity has and YYY is my tenant id. If I now look into the portal the identity is created and XXX is the correct id.
So when I now simply re-run the deployment it works.
I consider it a bug in dependsOn which should relate to ARM templates and not Bicep. I could not find any place where I can report ARM template issues to Microsoft.
I'm asking to assure that I do not miss something else here.
Edit: Added complete working sample which shows the bug. To use it, copy the script content into a test.bicep locally. Then create a resource group (lets call it "rg-test"), ensure that your local POSH context is set correctly and execute the following line in the folder where you stored the bicep in:
New-AzResourceGroupDeployment -Name deploy -Mode Incremental -TemplateFile .\test.bicep -ResourceGroupName rg-test
In the role assignment, you need to specify the principalType to ServicePrincipal and also use an api version greater or equal than: 2018-09-01-preview.
When you create a service principal, it is created in an Azure AD. It takes some time for the service principal to be replicated globally. By setting the principalType to ServicePrincipal, it tells the ARM API t0 wait for the replication.
resource roles 'Microsoft.Authorization/roleAssignments#2018-09-01-preview' = {
name: guid(subscription().id, 'bicep-roleassignments', 'dsddsd')
properties: {
principalId: userId.properties.principalId
roleDefinitionId: roleDef.id
principalType: 'ServicePrincipal'
}
}
You need to reference a newly created identity inside identity property of the target resource. dependsOn is redundant because bicep creates resources in the correct order based on actual usage:
resource userId 'Microsoft.ManagedIdentity/userAssignedIdentities#2018-11-30' = {
name: 'myidentity'
location: resourceGroup().location
}
resource appService 'Microsoft.Web/sites#2021-02-01' = {
name: 'appserviceName'
location: resourceGroup().location
properties: {
//...
}
identity: {
type: 'UserAssigned'
userAssignedIdentities: {
'/subscriptions/{your_subscription_id}/resourceGroups/${resourceGroup().name}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/${userId.name}': {}
}
}
}
The documentation doesn't recommend to use dependsOn without as strong reason:
In most cases, you can use a symbolic name to imply the
dependency between resources. If you find yourself setting explicit
dependencies, you should consider if there's a way to remove it.
So bicep does not require the dependsOn segment if referencing the property correctly.
Need to reference the properties.principalId of the userId in the resource block.
So would look like:
userId.properties.principalId
Here's a quickstart that calls out in a working example how this would work.
We have a release pipeline using the Terraform 0.12.3 series of tasks to create a VM, part of which involves creating a policy for automatically tagging the resources. Here's the relevant Terraform code that's giving an error:
# Assign tagging policy
resource "azurerm_policy_assignment" "tag_policy" {
count = length(var.tagNames)
name = "Apply${var.tagNames[count.index]}Tag"
scope = azurerm_resource_group.rsg.id
policy_definition_id = "/providers/Microsoft.Authorization/policyDefinitions/2a0e14a6-b0a6-4fab-991a-187a4f81c498"
description = "Assign policy for Tag '${var.tagNames[count.index]}' Value '${var.tagValues[count.index]}'"
display_name = "Apply${var.tagNames[count.index]}Tag"
parameters = <<PARAMETERS
{
"tagName": {
"value": "${var.tagNames[count.index]}"
},
"tagValue": {
"value": "${var.tagValues[count.index]}"
}
}
PARAMETERS
}
(the "count" parts of the code are due to having 3 Tags to apply, which are defined in variables.tf)
The error we receive is:
2020-04-17T15:40:29.5769619Z Error: cannot parse "policy_definition_id" as a Policy Definition ID: unable to parse Policy Definition ID "/providers/Microsoft.Authorization/policyDefinitions/2a0e14a6-b0a6-4fab-991a-187a4f81c498": unable to parse Remediation Scope ID: ID is empty
2020-04-17T15:40:29.5771388Z
2020-04-17T15:40:29.5771960Z on main.tf line 62, in resource "azurerm_policy_assignment" "tag_policy":
2020-04-17T15:40:29.5772430Z 62: resource "azurerm_policy_assignment" "tag_policy" {
I've double checked the policy definition ID is correct:
Name: Append a tag and its value to resources
Definition ID: /providers/Microsoft.Authorization/policyDefinitions/2a0e14a6-b0a6-4fab-991a-187a4f81c498
The guide at Hashicorp for the azurerm_policy_assignment makes no mention of requiring a Remediation Scope ID, or setting a flag to ignore it.
For this issue, I think the introduction for the property policy_definition_id of the resource azurerm_policy_assignment misleading you in the wrong way. You can see it quote the azurerm_policy_definition.example.id for the property, but when you output that id, you would know that it's the resource Id of the policy definition, not the Id you provided. So the policy_definition_id looks like this:
/subscriptions/xxxxxxxx/providers/Microsoft.Authorization/policyDefiniti
ons/my-policy-definition