0

I have previously created working bicep templates that create a function app and later use the keys but only where the function app is created as a resource directly within the main template - rather than within a module.

This time, I have a "main" bicep template that calls a bicep module to create a function app. Later in the main template, I need to pass the function app's key to another module that stores as a secret apim named value.

ChatGPT suggested the following:

// Create the Function App
module functionApp 'br:azcontreg.azurecr.io/bicep/modules/functionapp:v23.01.17.01' = {
  name: 'functionApp'
  params: {
    functionAppName: functionAppName
    appServicePlanName: appServicePlanName
    appServicePlanResourceGroup: appServicePlanResourceGroup
    storageAccountName: storageAccountName
    storageAccountResourceGroup: storageAccountResourceGroup
  }
}

// Retrieve the Function App keys
output functionAppKeys object = listKeys(functionApp.outputs.id, '2019-08-01')

// Create the API Management named value
module apimNamedValue 'br:azcontreg.azurecr.io/bicep/modules/apimnamedvalue:v23.01.17.01' = {
  name: 'apimNamedValue'
  scope: resourceGroup('rg-apim-${subscription().displayName}')
  dependsOn: [
    functionApp
    keyVault
  ]
  params: {
    name: '${functionApp.outputs.appServiceName}-key'
    secret: true
    value: functionAppKeys.keys[0].value
    environment: environment
  }
}

Problem is, this gives the following compile error:

This expression is being used in an argument of the function "listKeys", which requires a value that can be calculated at the start of the deployment. Properties of functionApp which can be calculated at the start include "name".bicep(BCP181)

The module that provisions the function app contains the following:

resource appService 'Microsoft.Web/sites@2021-03-01' = {
    name: name
    kind: appKind
    location: location
    tags: tags
    identity: {
        type: !empty(identityName) ? 'SystemAssigned, UserAssigned' : 'SystemAssigned'
        userAssignedIdentities: !empty(identityName) ? { 
            '${appServiceIdentity.id}': {}
        } : null
    }
    properties: {
        httpsOnly: true
        reserved: false
        serverFarmId: appServicePlan.id
        virtualNetworkSubnetId: !empty(vnetName) && !empty(vnetSubnetName) ? vnetSubnet.id : null
    }

    resource appServiceAppSettings 'config' = {
        name: 'appsettings'
        properties: appSettingsInternal
    }

    resource appServiceSlotConfigNames 'config' = {
        name: 'slotConfigNames'
        properties: {
            appSettingNames: deploymentSlotSettingNames
        }
    }

    resource appServiceWeb 'config' = {
        name: 'web'
        properties: {
            alwaysOn: alwaysOn
            ftpsState: 'FtpsOnly'
            healthCheckPath: healthCheckPath
            ipSecurityRestrictions: ipSecurityRestrictions
            use32BitWorkerProcess: use32BitProcess
            vnetRouteAllEnabled: vnetRouteAll
        }
    }
}
output appServiceId string = appService.id

This is called from the main.bicep with the following:

module functionApp 'br:crbicepregistryprod001.azurecr.io/bicep/modules/appservice:v23.04.13.01' = {
  name: 'functionApp'
  params: {
    name: functionAppName
    appServicePlanScope: functionAppServicePlanEnv.rg
    appServicePlanName: functionAppServicePlanEnv.name
    appKind: 'functionapp'
    vnetName: 'vnet-${environment}-uksouth'
    vnetSubnetName: functionAppServicePlanEnv.subnet
    ipSecurityRestrictions: functionAppIpSecurityRestrictions
    appSettings: functionAppSettings
    enableSlot: false
    alwaysOn: true
    healthCheckPath: '/health'
  }
  dependsOn: [
    appInsights
    keyVault
  ]
}

Main.bicep later calls the new module with the following:

module apimNamedValue 'br:crbicepregistryprod001.azurecr.io/bicep/modules/apimnamedvalueforlistkeys:v23.06.13.02' = {
  name: 'apimNamedValue'
  scope: resourceGroup('rg-apim-${subscription().displayName}')
  dependsOn: [
    functionApp
    keyVault
  ]
  params: {
    name: '${functionApp.outputs.appServiceName}-key'
    secret: true
    resourceId: functionApp.outputs.appServiceId
    environment: environment
  }
}

The new module to obtain the key using listKeys is as follows:

@description('Name of the NamedValue to deploy')
param name string

@description('Resource Id to be used in the listkeys function')
param resourceId string

@description('Used in listkeys function')
param apiVersion string = '2022-09-01'

@description('Specify if the value is secret. Defaults to false')
param secret bool = false

@description('Specify if the value is a key vault reference. Also implies value is secret. Defaults to false')
param keyVaultReference bool = false

@description('APIM environment to deploy to')
@allowed([
    'dev'
    'int'
    'act'
    'prod'
])
param environment string = 'dev'

var apimName = {
    dev: 'apim-dev-002'
    int: 'apim-int-001'
    act: 'apim-act-001'
    prod: 'apim-prod-002'
}[environment]

var value = listKeys(resourceId, apiVersion)
resource apim 'Microsoft.ApiManagement/service@2021-08-01' existing = {
    name: apimName

    resource apimNamedValue 'namedValues' = {
        name: name
        properties: {
            displayName: name
            secret: secret || keyVaultReference
            keyVault: keyVaultReference ? {
                secretIdentifier: value
            } : null
            value: !keyVaultReference ? value : null
        }
    }
}

output namedValueId string = apim::apimNamedValue.id
output namedValueName string = apim::apimNamedValue.name
output namedValueNameFormatted string = '{{${apim::apimNamedValue.name}}}'

If I drill into the resource group deployment in the Azure portal I see:

enter image description here

If I then click the error details link, I see:

{
  "code": "BadRequest",
  "message": ""
}

I guess the error state is “Not Found” because the module is running in the scope of the apim’s resource group but it’s trying to access the key of the function app which is in a different resource group?

Rob Bowman
  • 7,632
  • 22
  • 93
  • 200

2 Answers2

1

Passing around keys/secrets can be tricky, especially when everything modularised.

I'd be tempted to create a new module variant br:azcontreg.azurecr.io/bicep/modules/apimnamedvaluefromkey that takes an extra 2 parameters for the resourceid and api version. Then you can move the listkeys into that module and it won't suffer the name calculation error as it's abstracted.

GordonBy
  • 3,099
  • 6
  • 31
  • 53
  • Many thanks for your answer Gordon. That seems like progress but I now get a NotFound error against the function app when the apinnamevaluefromkey bicep module runs. I guess this is because the module is being run before the function app creation has completed? If so, what's the best work-around for this? Maybe there's a sample that creates a function app then uses its key you could point me at? – Rob Bowman Jun 13 '23 at 15:44
  • if you're using `functionApp.outputs.id` then there shouldn't be a dependency problem. – GordonBy Jun 13 '23 at 17:16
  • ok, that's strange. I have now updated the question with details of the changes I've made and also the error being reported on deployment. – Rob Bowman Jun 13 '23 at 18:25
1

Solved by storing the function app key in a key vault of the same resource group. The apim named value then referenced the key vault secret.

Below is the call to the initial apim named value module.

module apimNamedValue 'br:regname.azurecr.io/bicep/modules/apimnamedvalue:v23.05.04.01' = {
  name: 'apimNamedValue'
  scope: resourceGroup('rg-apim-${subscription().displayName}')
  dependsOn: [
    functionApp
    keyVault
  ]
  params: {
    name: '${functionApp.outputs.appServiceName}-key'
    secret: true
    keyVaultReference: true
    value: '${keyVault.outputs.keyVaultId}/secrets/${secretName}/'
    environment: environment
  }
}
Rob Bowman
  • 7,632
  • 22
  • 93
  • 200