Azure / bicep

Bicep is a declarative language for describing and deploying Azure resources
MIT License
3.21k stars 745 forks source link

Proposal - simplifying resource referencing (part 2) #2246

Open anthony-c-martin opened 3 years ago

anthony-c-martin commented 3 years ago

Proposal - simplifying resource referencing (part 2)

Part 1 / Part 2

Problem statement

Passing around / obtaining references to resources in a type-safe manner is overly complex. Rather than inventing non-type-safe mechanisms to refer to resources or resource properties, we should provide a first-class syntax for doing so, with full type-safety and editor support.

Resources as params and outputs

A new type of resource will be accepted in param and output declarations to permit passing a reference to a resource as an input or output for a module. Supplying the type string for the resource would be optional, but functionality would be greatly reduced without it.

At compile-time, Bicep will type check for reference equality - it will ensure a valid resource reference is passed to a generic resource param, and it will ensure that a valid resource reference matching the expected type string is passed to a typed resource param.

Examples

Generic

// we haven't specified a resource type here
param lockableResource resource

resource lockResource 'Microsoft.Authorization/locks@2016-09-01' = {
  scope: lockableResource
  name: 'DontDelete'
  ...
}

Input/Output

// input a resource reference
param storageAcc resource 'Microsoft.Storage/storageAccounts@2021-01-01'

var myContainer = storageAcc.child('blobServices', 'default').child('containers', 'myContainer')

// output a resource reference - note the resource type can be omitted
output myContainer resource = myContainer

Property access

param storageAcc resource 'Microsoft.Storage/storageAccounts@2021-01-01'

// list keys
var myKey = listKeys(storageAcc.id, storageAcc.apiVersion).keys[0].value

// property access
output accountTags object = storageAcc.tags

Notes

  1. If the param does not specify a resource type string, functionality will be greatly reduced - limited to using the resource as a scope for an extension resource, and accessing the resource id property. We want to encourage module authors to be specific about the type they accept to provide optimal type safety.
  2. API versions do not need to match across module params and outputs, but types must match if the param has specified a type.
  3. We will need to be careful when passing param references to resources at a different scope to the module, as they cannot be used for certain purposes (deploying children/extensions, for example).

Out of scope

  1. This proposal requires both inputs and outputs to accept a resource reference, and there is no conversion between resourceId string and resource reference. The following would not be permitted:
    module myMod './module.bicep' = {
      name: 'myMod'
      params: {
        // resourceReference is a param of type 'resource'
        // resourceId is a string containing a resourceId
        resourceReference: resourceId
      }
    } 

Codegen

The most straightforward option for JSON codegen is to generate a string parameter or output in the template JSON, with some associated metadata.

alex-frankel commented 3 years ago

Will we accept a literal resource ID string for foo.params.res here?

foo.bicep

param res resource

main.bicep

module foo './foo.bicep' = {
  name: ...
  params: {
    res: '/subscriptions/.../resourceGroups/.../microsoft.rp/type/...'
  }
}
majastrz commented 3 years ago

My expectation would be for the string to be rejected because it's not a symbolic name. However, I think we do need some sort of interop gesture to bridge templates passing resource ID strings around with this way of passing parameters.

majastrz commented 3 years ago

@anthony-c-martin During our last discussion, we realized that API version match/mismatch semantics differ on inputs and outputs. I think it'd be worthwhile to explain more about for the community at large to offer feedback (if any).

In the generic resource case (note number 1), would we allow name and type property access as well? Just wondering... we can start small and add more later, of course.

Regarding code gen, we should consider moving some of the type safety down to the runtime so all tempaltes can leverage it. We could still compile down to a fully qualified resource ID but we could introduce a new parameter type in the JSON with some additional settings.

alex-frankel commented 3 years ago

My expectation would be for the string to be rejected because it's not a symbolic name.

Right, but then this would have to work I guess? So if you need to pass in a resource ID from an external source, you would expose that as a param of type resource.

params.json

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "foo": {
      "value": "/subscriptions/.../resourceGroups/.../microsoft.rp/type/..."
    }
  }
}

main.bicep

param foo resource

module foo './foo.bicep' = {
  name: ...
  params: {
    res: foo
  }
}
az deployment group create -f ./main.bicep -p params.json
miqm commented 3 years ago

My expectation would be for the string to be rejected because it's not a symbolic name.

Right, but then this would have to work I guess? So if you need to pass in a resource ID from an external source, you would expose that as a param of type resource.

params.json

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "foo": {
      "value": "/subscriptions/.../resourceGroups/.../microsoft.rp/type/..."
    }
  }
}

main.bicep

param foo resource

module foo './foo.bicep' = {
  name: ...
  params: {
    res: foo
  }
}
az deployment group create -f ./main.bicep -p params.json

in some other issue I've suggested using as keyword that could be used to cast string that we expect to be a resourceid to a biceps resource. perhaps worth revisiting that?

majastrz commented 3 years ago

@alex-frankel Yeah passing it through parameters would have to work. We do have to answer the question whether we need the ability to turn a string ID into a symbolic name within a Bicep file without the use of parameters. One scenario I can think of comes up with referencing a JSON file as a module (when we have the feature) when the template outputs a string resource ID.

@miqm Yeah as could be an option although we have so far resisted adding type casting so far. All the type conversions currently are done through converter functions like any() or string(). Another option would be a separate function or a resource() overload that accepts a string ID

anthony-c-martin commented 3 years ago

My expectation would be for the string to be rejected because it's not a symbolic name.

Right, but then this would have to work I guess? So if you need to pass in a resource ID from an external source, you would expose that as a param of type resource.

This is definitely a scenario we'll need to handle, and I think we have the options of either (with some rough examples):

  1. Exposing as a string parameter in the JSON - simple, but potentially error-prone if people format the id incorrectly.
    "parameters": {
      "vmResource": {
        "type": "string"
      }
    }
  2. Creating a new parameter type in JSON with enhanced validation (on server side, but possibly also client-side with psh/cli)
    "parameters": {
      "vmResource": {
        "type": "resource"
      }
    }
  3. (sort of an in-between) Exposing as a string parameter in the JSON, with metadata that newer CLI utilities are able to understand (so as to not make it a breaking change, and allow better validation to be added gracefully).
    "parameters": {
      "vmResource": {
        "type": "string",
        "metadata": {
          "_typeinfo": {
            "resourceType": "Microsoft.Compute/virtualMachines"
          }
        }
      }
    }
anthony-c-martin commented 3 years ago

@alex-frankel Yeah passing it through parameters would have to work. We do have to answer the question whether we need the ability to turn a string ID into a symbolic name within a Bicep file without the use of parameters. One scenario I can think of comes up with referencing a JSON file as a module (when we have the feature) when the template outputs a string resource ID.

It makes sense to think about this, but for the purposes of this specific proposal, I'd like to treat turning a resourceId in a string into a symbolic reference out-of-scope. Would you be OK with me creating another issue specifically for that, and adding a note to this proposal to mention as such?

majastrz commented 3 years ago

Should be ok to include it in part 3.

miqm commented 3 years ago

I like Passing type in metadata rather than introducing new type. In addition we could do object or array typing in similar way.

anthony-c-martin commented 3 years ago

Will we accept a literal resource ID string for foo.params.res here?

@alex-frankel, @majastrz - FYI, I added a note to explicitly mention this scenario is not covered by this proposal.

jamesongithub commented 3 years ago

Will this proposal cover passing a resource directly as a parameter of another resource or just for module input/outputs

something like


resource disk 'disks' {
   ...
}

resource vm 'vms' {
  osDisk: disk
}
anthony-c-martin commented 3 years ago

Will this proposal cover passing a resource directly as a parameter of another resource or just for module input/outputs

This proposal is just covering module inputs/outputs.

stan-sz commented 3 years ago

As in #2163 the output of a module can contain secrets (e.g. output of a list*) function to store it's value in a KV. Is it planed to introduce secureString or secureObject (or other way to hide secrets) from module output?

rynowak commented 3 years ago

@anthony-c-martin - any ideas about how this would work for a resource with a discriminator field? (likely kind) property?

SeidChr commented 2 years ago

Just another suggestion: I can access my module outputs by <modulename>.outputs.<outputname>

I would love to access my module resources (fully type-safe of cause) by <modulename>.resources.<outputname>

The Idea brings up a question: Should all top level module elements be accessible in this way? Wouldn't it be nice to access <modulename>.modules.<modulename>.resources.<resourcename>.properties.<propertyname>?

ptemmer commented 2 years ago

Apologies as this is not the most appropriate place to ask, however this proposal would be a possible solution to my question, nonetheless as this feature hasn't been released yet, I'm wondering what the current most appropriate way is to achieve the following:

Say: main.bicep:

module rAppservice 'appService.bicep' = {
  name: appServiceName
  params: {
    appServiceName: appServiceName
}

resource vnetintegration 'Microsoft.Web/sites/networkConfig@2021-02-01' = {
  name: 'vnetintegration'
  parent: XXXXXXX
}

I cannot specify the module symbolic name as a parent as that is not a resource. Currently what I am doing is the following, however this feels wrong.

module rAppservice 'appService.bicep' = {
  name: appServiceName
  params: {
    appServiceName: appServiceName
  }
}

resource existingAppService 'Microsoft.Web/sites@2021-02-01' existing = {
  name: appServiceName
}

resource vnetintegration 'Microsoft.Web/sites/networkConfig@2021-02-01' = {
  name: 'vnetintegration'
  parent: existingAppService 
}
anthony-c-martin commented 2 years ago

@ptemmer your workaround looks like the best option that's currently available. The only thing to be careful with is to order the dependencies so that the networkConfig resource gets deployed after the module:

module rAppservice 'appService.bicep' = {
  name: appServiceName
  params: {
    appServiceName: appServiceName
  }
}

resource existingAppService 'Microsoft.Web/sites@2021-02-01' existing = {
  name: appServiceName
}

resource vnetintegration 'Microsoft.Web/sites/networkConfig@2021-02-01' = {
  name: 'vnetintegration'
  parent: existingAppService
  dependsOn: [
    rAppservice
  ]
}
ptemmer commented 2 years ago

Thanks @anthony-c-martin for the super quick reply. Good to know that workaround is ok, as I honestly had the feeling I wasn't using Bicep as it should be. I guess the current proposal will solve this altogether.

Off-topic: I'm starting out with Bicep so I due regularly run into these kind of issues/doubts. What is the best place to ask questions? This repo? Or is there a Slack bicep community? (wasn't able to find one).

Thanks again.

anthony-c-martin commented 2 years ago

@ptemmer - this repo is the best place to go. https://github.com/Azure/bicep/discussions is a good place to ask questions such as "What's the best way to do XYZ?" or for help with specific scenarios.

thealanagrace commented 2 years ago

Is there an ETA on this functionality? It would really help me make my deployment scripts so much sleeker!

alex-frankel commented 2 years ago

We are trying to get an initial implementation done in the next few months.

rouke-broersma commented 2 years ago

@alex-frankel

Parameter resources cannot be used with the .parent or .scope properties because it would allow you to bypass scope validation easily. The set of cases that we could actually provide validation for these use cases are really limited.

This is one of the use cases we would very much want to use this for, especially scope because of Authorization on resources. Is there a different issue for this use case or is this never going to be solved? I personally wouldn't consider this fixed until we can at least use resource params for scope.

rynowak commented 2 years ago

The first step of this (all the disruptive changes) are in but hidden behind an experimental flag. I plan to continue making progress on all of the scenarios described here and will keep this issue open until we're totally unblocked.

johndowns commented 2 years ago

@rynowak Awesome - is it possible to give some instructions on how to enable the experimental flag if we want to try some of this out now?

rynowak commented 2 years ago

Set the environment variable BICEP_RESOURCE_TYPED_PARAMS_AND_OUTPUTS_EXPERIMENTAL=true

I'd recommend also reading the PR description before trying this out https://github.com/Azure/bicep/pull/4971

Some of the scenarios described in this issue are explicitly blocked by the compiler at this stage. We've also run into limitations enforced by the deployment engine using resources as module outputs. That will require changes in Azure to unblock.

bgawale commented 2 years ago

Very much looking forward for this feature to be available, deployments would be much more simpler.

xavierjohn commented 2 years ago

If I had a module named west2 and it had two resources name myStorage and myEventHubNamespace, would using a . format be more natural? As a programmer in several languages, the natural format for me would be to use west2.mtStorage and west2.myEventHubNamespace without having to deal with module output.

ds-evo commented 2 years ago

Hello,

i just tried to set the resource param to null.

But this didn't work.

To reduce duplicate code i tried this:

param app_res resource 'Microsoft.Web/sites@2021-03-01' = any(null)
param app_slot_res resource 'Microsoft.Web/sites/slots@2021-03-01' = any(null)

param appIsSlot bool = app_res == null

var _res = (!appIsSlot) ? app_res : app_slot_res

@secure()
param cfg_conn object

@secure()
param cfg_app object

resource appConnStrs 'Microsoft.Web/sites/config@2021-01-15' = {
    parent: _res
    name: 'connectionstrings'
    properties: cfg_conn
}

resource appSettings 'Microsoft.Web/sites/config@2021-01-15' = {
    parent: _res
    name: 'appsettings'
    properties: cfg_app
}

But then i get this error:

Error BCP036: The property "parent" expected a value of type "Microsoft.Web/sites" but the provided value is of type "Microsoft.Web/sites/slots@2021-03-01 | Microsoft.Web/sites@2021-03-01"

There are so many cases, where i think i need to use a template language to auto-generate bicep files, only to get a generic solution.

And, i start to think, that the Rest-Api with a good ORM is more flexible and easier then bicep or ARM

ds-evo commented 2 years ago

UPDATE:

The above code has logic error - so rewrite it to this.

param app_res resource 'Microsoft.Web/sites@2021-03-01' = any(null)
param app_slot_res resource 'Microsoft.Web/sites/slots@2021-03-01' = any(null)

param appIsSlot bool = app_res != null

@secure()
param cfg_conn object

@secure()
param cfg_app object

resource appSlotConnStrs 'Microsoft.Web/sites/slots/config@2021-01-15' = if (appIsSlot) {
    parent: app_slot_res
    name: 'connectionstrings'
    properties: cfg_conn
}

resource appSlotSettings 'Microsoft.Web/sites/slots/config@2021-01-15' = if (appIsSlot) {
    parent: app_slot_res
    name: 'appsettings'
    properties: cfg_app
}

resource appConnStrs 'Microsoft.Web/sites/config@2021-01-15' = if (!appIsSlot) {
    parent: app_res
    name: 'connectionstrings'
    properties: cfg_conn
}

resource appSettings 'Microsoft.Web/sites/config@2021-01-15' = if (!appIsSlot) {
    parent: app_res
    name: 'appsettings'
    properties: cfg_app
}

Now i get: Error BCP229: The parameter "app_slot_res" cannot be used as a resource scope or parent. Resources passed as parameters cannot be used as a scope or parent of a resource.

This is my Solution for this problem. I ended with two modules, with same parameters and nearly same code....

module appCfgReal '../../../app/app_cfg_sets.real.bicep' = if (!dryRun && !appIsSlot) {
  name: '${appName}-appCfg-real-mod'
  params: {
    app: app
    cfg_conn: cfg_conn
    cfg_app: cfg_app    
  }
}

module appCfgSlotReal '../../../app/app_cfg_sets.real.slots.bicep' = if (!dryRun && appIsSlot) {
  name: '${appName}-appCfg-real-slot-mod'
  params: {
    app: app
    cfg_conn: cfg_conn
    cfg_app: cfg_app    
  }
}

PS: And it would be nice, if string interpolation in resource types would work.

Devvox93 commented 2 years ago

We've also run into limitations enforced by the deployment engine using resources as module outputs. That will require changes in Azure to unblock.

Is there any update or roadmap on when passing resources as module outputs would be supported by Azure? Or is this something that will remain unsupported?

Very eager to use this as, like others have mentioned, it would greatly simplify authoring modules :)

stan-sz commented 2 years ago

How about (re)using the existing syntax for params:

param resource storageAcc 'Microsoft.Storage/storageAccounts@2021-01-01' existing

or

param storageAcc resource 'Microsoft.Storage/storageAccounts@2021-01-01' existing

In the first snippet, the resource and storageAcc are flipped to preserve the resource syntax. The second snippet follows the param <symbol name> <type>. Semantically this would behave the same as resource reference, because a resource passed into a module needs to exist, so param would just be an indicator that it's a parameter.

dazinator commented 1 year ago

Just tried the experimental feature. It wasn't useful for my case, as it doesn't support declaring resource paramaters of any type, and setting the scope property for a resource e.g this won't work

param scope resource // can't pass an untyped resurce which is what `scope` needs below.

var roleAssignmentName = 'some-name'
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: roleAssignmentName
  scope: resource // can't set this
  properties: {
    roleDefinitionId: roleDefinitionResourceId
    principalId: userAssignedIdentity.properties.principalId
    principalType: 'ServicePrincipal'
    description: empty(roleAssignmentDescription) ? null : ''
  }
}

as such I am unable to modularise this code, however role assignments are quite common accross our deployments, and there is value in ensuring things like the unique name of the assignment uses a guid based on principal id, scope id, and role definition id etc.

jangelfdez commented 1 year ago

I just hit the same situation that @dazinator mentions in his reply. Trying to modularize roleassignments into an independent module to be reused with different type of assignment levels.

Being able to pass a generic resource would be needed in this case to cover all the different scenarios.

asdkant-bf commented 1 year ago

After reading all the comments here, there's something I'm still not clear on:

Assuming I have a module that creates one or more resources, will I be able to pass those resources as outputs?

alex-frankel commented 1 year ago

After reading all the comments here, there's something I'm still not clear on:

Assuming I have a module that creates one or more resources, will I be able to pass those resources as outputs?

Yes, you will be able to write

resource foo '...' = { ... }
output myResource resource = foo
housten commented 1 year ago

I am also really wanting to be able to have a generic role assignment module. I want to be able to pass multiple roles so I was trying to put the creation of the roles in a module and while breaking it out it makes sense to let it fulfill multiple purposes so I can lower the published module maintenance overhead.

For my part, this need could be met by being able to set the scope of a module to the resource it should attach to. I am not so interested in being able to receive these as outputs.

@description('Name of the queue')
param QueueName string

@description('A list of rbac settings to create')
param Rbac array

resource queue 'Microsoft.ServiceBus/namespaces/queues@2021-06-01-preview' = {
  name: QueueName
  properties: {
    deadLetteringOnMessageExpiration: false
    enableBatchedOperations: true
    enablePartitioning: false
  }
}

module roleAssignment 'br:mybicepregistry.azurecr.io/role:1.0' = [for rbac in Rbac: {
  scope: queue
  name: rbac.principleId
  params: {
    roles: rbac.roles
    principalId: rbac.principalId
    principalType: rbac.principalType
    targetRoleId: queue.id  // for naming
  }
} ]

This was the first thing I tried when I broke the role out to a module but got the error that scope for a module has to be a resource group or a subscription. Syntactically I like this because it matches the syntax for creating the resource at this level (ie without a module).

The ARM json work around #5805 was a nice idea, but then each repo would have to have its own json as I am using a bicep registry for all the modules, which I assume can't hold a json template file. It presents a nightmare maintenance-wise having lots of json templates to have to fix and a technical understanding of the file that I am trying encapsulate away for the users (or blind acceptance that a file has to be in their repo and shouldn't be fiddled with).

BartDecker commented 1 year ago

@housten scanning your post quickly I think you might want to look at the examples given in: https://github.com/Azure/bicep/issues/7621

housten commented 1 year ago

@housten scanning your post quickly I think you might want to look at the examples given in: #7621

Thanks @BartDecker. In my last paragraph I mention the ARM template work around and why I didn't think it was a good fit. To avoid having to have arm templates in every repository, I would rather have specific modules for each type of role target. Unless there is a way to put arm templates into the bicep registry?

cloudlene commented 4 months ago

I was trying to build a generic role assignment module.

I don't understand why we need provider & the API Version when the resource passed must be a symbolic name in parent modules.

  1. If we are declaring it without provider & version, restrict the property access to top level properties (such as name, id and not 'properties'). This way we can pass it around to child modules or assign them to parent & scope properties.
  2. If resource declaration must include the provider & apiVersion, at the least we should have a overload of resource function that would accept a resource Id.
  3. Another option could be to make the reference function return a 'resource' type that makes it assignable to 'scope' property.

Option #1: role-assignments.bicep

param principalId string
param principalType string
param roleNames string[]
param targetResourceId string

// Runs a shell script that creates a query from the roleNames passed: az role definition list --query "roleName == 'Key Vault Reader' || roleName == 'Key Vault Secrets Officer'"
module lookupRoles 'lookup-roles.bicep' = {
  name: 'lookupRoles'
  params: {
    location: location
    roleNames: roleNames
    nameSuffixShort: nameSuffixShort
  }
}

var roleIds = lookupRoles.outputs.roleIds

resource roleDefinitions 'Microsoft.Authorization/roleDefinitions@2022-05-01-preview' existing = [for roleId in roleIds : {
  scope: resource(targetResourceId) // Or Make reference(targetResourceId) return a resource type instead of object
  name: roleId
}]

resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for i in range(0, length(roleIds)):{
  name: guid(string(i), roleDefinitions[i].id)
  properties: {
    principalId: principalId
    roleDefinitionId: roleDefinitions[i].id
    principalType: principalType
  }
}]

// Option 2: role-assignments.bicep

param principalId string
param principalType string
param roleNames string[]
// Notice no need for type or version
param targetResource resource

// Runs a shell script that creates a query from the roleNames passed: az role definition list --query "roleName == 'Key Vault Reader' || roleName == 'Key Vault Secrets Officer'"
module lookupRoles 'lookup-roles.bicep' = {
  name: 'lookupRoles'
  params: {
    location: location
    roleNames: roleNames
    nameSuffixShort: nameSuffixShort
  }
}

var roleIds = lookupRoles.outputs.roleIds

resource roleDefinitions 'Microsoft.Authorization/roleDefinitions@2022-05-01-preview' existing = [for roleId in roleIds : {
  scope: targetResource 
  name: roleId
}]

resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = [for i in range(0, length(roleIds)):{
  name: guid(string(i), roleDefinitions[i].id)
  properties: {
    principalId: principalId
    roleDefinitionId: roleDefinitions[i].id
    principalType: principalType
  }
}]

When it's already allowed that any resource can be passed to the scope property of the roleDefinitions, it's only logical to expect a parameter that can accept ANY resource not just ones that are statically typed to a specific provider & version.