Ejercicio: refactorizar el archivo de Bicep

Completado

Tras revisar la plantilla con los compañeros, decidimos refactorizar el archivo para permitirles trabajar con él más fácilmente. En este ejercicio, pondremos en práctica los procedimientos recomendados aprendidos en las unidades anteriores.

Su tarea

Revisaremos la plantilla de Bicep que guardamos anteriormente. Tendremos presentes los consejos que hemos visto sobre cómo estructurar las plantillas. Intentaremos actualizar la plantilla para que sea más fácil de entender para los compañeros.

En las siguientes secciones, se señalan algunas partes específicas de la plantilla y se dan algunas pistas sobre las cosas que podríamos querer cambiar. Aquí se sugiere una solución, pero la plantilla que obtengamos podría tener un aspecto diferente, lo que es perfectamente válido.

Sugerencia

A medida que vayamos avanzando en el proceso de refactorización, conviene asegurarse de que el archivo de Bicep es válido y de que no hemos introducido errores inadvertidamente. La extensión de Bicep para Visual Studio Code nos servirá para esto. Esté atento por si aparecen líneas onduladas de color rojo o amarillo subrayando alguna parte del código, ya que señalan un error o una advertencia. Si quiere ver una lista de los problemas que hay en el archivo, seleccione Ver>Problemas.

Actualizar los parámetros

  1. Algunos parámetros de la plantilla no están claros. Fijémonos por ejemplo en estas funciones:

    @allowed([
      'F1'
      'D1'
      'B1'
      'B2'
      'B3'
      'S1'
      'S2'
      'S3'
      'P1'
      'P2'
      'P3'
      'P4'
    ])
    param skuName string = 'F1'
    
    @minValue(1)
    param skuCapacity int = 1
    

    ¿Para qué sirven?

    Sugerencia

    Si hay algún parámetro que estamos intentando comprender, Visual Studio Code es de ayuda. Seleccione y mantenga presionado (o haga clic con el botón derecho) el nombre de un parámetro en cualquier parte del archivo y seleccione Buscar todas las referencias.

    ¿Necesita la plantilla que especifiquemos la lista de valores permitidos para el parámetro skuName? ¿Qué recursos se ven afectados si elegimos otros valores para estos parámetros? ¿Podríamos denominar los parámetros de mejor forma?

    Sugerencia

    Cuando cambie el nombre de los identificadores, procure de hacerlo de modo coherente en todas las partes de la plantilla. Esto es especialmente importante en el caso de los parámetros, variables y recursos a los que se hace referencia en toda la plantilla.

    Visual Studio Code ofrece una forma muy cómoda de cambiar el nombre de los símbolos; solo hay que seleccionar el identificador que queremos cambiar de nombre, presionar F2, escribir el nuevo nombre y, por último, presionar Entrar:

    Captura de pantalla de Visual Studio Code en la que se muestra cómo cambiar el nombre de un símbolo.

    En estos pasos se cambia el nombre del identificador y se actualizan automáticamente todas las referencias a él.

  2. El parámetro managedIdentityName no tiene un valor predeterminado. ¿Podríamos arreglar esto? O, mejor aún, ¿podría crearse el nombre automáticamente en la plantilla?

  3. Echemos un vistazo a la definición del parámetro roleDefinitionId:

    param roleDefinitionId string = 'b24988ac-6180-42a0-ab88-20f7382dd24c'
    

    ¿Por qué el valor predeterminado es b24988ac-6180-42a0-ab88-20f7382dd24c? ¿Qué significa ese identificador tan largo? ¿Cómo podría otro usuario decidir si usar el valor predeterminado o reemplazarlo? ¿Qué podríamos hacer para mejorar el identificador? ¿Acaso tiene sentido tener esto como parámetro?

    Sugerencia

    Ese identificador es el identificador de definición del rol Colaborador de Azure. ¿Cómo podemos usar esta información para mejorar la plantilla?

  4. Cuando alguien implemente la plantilla, ¿cómo sabrá para qué sirve cada parámetro? ¿Podemos agregar alguna descripción para ayudar a los usuarios de la plantilla?

Agregar un conjunto de configuración

  1. Hablamos con nuestros compañeros y decidimos usar SKU específicas para cada recurso, según cuál sea el entorno que se implemente. Nos decantamos por estas SKU para cada uno de los recursos:

    Recurso SKU de producción SKU no de producción
    Plan de App Service S1, dos instancias F1, una instancia
    Cuenta de almacenamiento GRS LRS
    Base de datos SQL S1 Básico
  2. ¿Podemos usar un conjunto de configuración para simplificar las definiciones de parámetros?

Actualizar los nombres simbólicos

Echemos un vistazo a los nombres simbólicos de los recursos de la plantilla. ¿Qué podríamos hacer para mejorarlos?

  1. La plantilla de Bicep contiene recursos con diferentes usos de mayúsculas en los nombres simbólicos, como:

    • storageAccount y webSite, donde se usa una combinación de mayúsculas y minúsculas.
    • roleassignment y sqlserver, donde todo va en minúsculas.
    • sqlserverName_databaseName y AppInsights_webSiteName, donde se usan guiones bajos.

    ¿Podemos corregir esto y usar un solo estilo de forma coherente?

  2. Fijémonos en este recurso de asignación de roles:

    resource roleassignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
      name: guid(roleDefinitionId, resourceGroup().id)
    
      properties: {
        principalType: 'ServicePrincipal'
        roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId)
        principalId: msi.properties.principalId
      }
    }
    

    ¿Es el nombre simbólico lo suficientemente descriptivo como para otra persona sepa cómo trabajar con esta plantilla?

    Sugerencia

    La razón por la que la identidad necesita una asignación de roles es que la aplicación web usa su identidad administrada para conectarse al servidor de bases de datos. ¿Nos ayuda esto a explicarlo en la plantilla?

  3. Algunos recursos tienen nombres simbólicos que no reflejan los nombres actuales de los recursos de Azure:

    resource hostingPlan 'Microsoft.Web/serverfarms@2023-12-01' = {
      // ...
    }
    resource webSite 'Microsoft.Web/sites@2023-12-01' = {
      // ...
    }
    resource msi 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview' = {
      // ...
    }
    

    Antes, las identidades administradas se denominaban MSI; los planes de App Service, planes de hospedaje, y las aplicaciones de App Service, sitios web.

    ¿Sería posible actualizar estos nombres a los nombres más recientes para evitar confusiones en el futuro?

Simplificar las definiciones de contenedores de blobs

  1. Veamos cómo se definen los contenedores de blobs:

    resource container1 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-05-01' = {
      parent: storageAccount::blobServices
      name: container1Name
    }
    
    resource productmanuals 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-05-01' = {
      name: '${storageAccount.name}/default/${productmanualsName}'
    }
    

    Uno de ellos usa la propiedad parent; el otro no. ¿Podemos arreglarlo para que sean coherentes?

  2. Los nombres de los contenedores de blobs serán los mismos en todos los entornos. ¿Quizá debamos especificar los nombres mediante parámetros?

  3. Hay dos contenedores de blobs. ¿Podríamos implementarlos usando un bucle?

Actualizar los nombres de recurso

  1. Hay algunos parámetros que establecen expresamente los nombres de recurso:

    param managedIdentityName string
    param roleDefinitionId string = 'b24988ac-6180-42a0-ab88-20f7382dd24c'
    param webSiteName string = 'webSite${uniqueString(resourceGroup().id)}'
    param container1Name string = 'productspecs'
    param productmanualsName string = 'productmanuals'
    

    ¿Hay alguna otra forma de hacerlo?

    Precaución

    Recuerde que el nombre de los recursos no se puede cambiar una vez que se implementen. Al modificar plantillas que ya están en uso, hay que tener cuidado al cambiar la forma en que la plantilla crea nombres de recurso. Si la plantilla se volviera a implementar y el recurso tuviera otro nombre, Azure creará otro recurso. Podría incluso eliminar el recurso anterior si lo implementa en modo Completo.

    Aquí no tenemos que preocuparnos por esto porque es solo un ejemplo.

  2. El nombre de recurso del servidor lógico SQL se establece mediante una variable, aun cuando necesite un nombre único global:

    var sqlserverName = 'toywebsite${uniqueString(resourceGroup().id)}'
    

    ¿Cómo podríamos mejorarlo?

Actualizar las dependencias y los recursos secundarios

  1. Este es uno de nuestros recursos, que incluye una propiedad dependsOn. ¿Realmente lo necesitamos?

    resource sqlserverName_AllowAllAzureIPs 'Microsoft.Sql/servers/firewallRules@2023-08-01-preview' = {
      name: '${sqlserver.name}/AllowAllAzureIPs'
      properties: {
        endIpAddress: '0.0.0.0'
        startIpAddress: '0.0.0.0'
      }
      dependsOn: [
        sqlserver
      ]
    }
    
  2. Fíjese en cómo están declarados estos recursos secundarios en la plantilla:

    resource sqlserverName_databaseName 'Microsoft.Sql/servers/databases@2023-08-01-preview' = {
      name: '${sqlserver.name}/${databaseName}'
      location: location
      sku: {
        name: 'Basic'
      }
      properties: {
        collation: 'SQL_Latin1_General_CP1_CI_AS'
        maxSizeBytes: 1073741824
      }
    }
    
    resource sqlserverName_AllowAllAzureIPs 'Microsoft.Sql/servers/firewallRules@2023-08-01-preview' = {
      name: '${sqlserver.name}/AllowAllAzureIPs'
      properties: {
        endIpAddress: '0.0.0.0'
        startIpAddress: '0.0.0.0'
      }
      dependsOn: [
        sqlserver
      ]
    }
    

    ¿Cómo podríamos cambiar el modo en que estos recursos se declaran? ¿Existen otros recursos en la plantilla que también deban actualizarse?

Actualizar los valores de propiedades

  1. Echemos un vistazo a las propiedades del recurso de base de datos SQL:

    resource sqlserverName_databaseName 'Microsoft.Sql/servers/databases@2023-08-01-preview' = {
      name: '${sqlserver.name}/${databaseName}'
      location: location
      sku: {
        name: 'Basic'
      }
      properties: {
        collation: 'SQL_Latin1_General_CP1_CI_AS'
        maxSizeBytes: 1073741824
      }
    }
    

    ¿Tiene sentido codificar de forma rígida el valor de la propiedad name de la SKU? ¿Y qué son esos valores tan raros de las propiedades collation y maxSizeBytes?

    Sugerencia

    Las propiedades collation y maxSizeBytes están establecidas en sus valores predeterminados. Si no especificamos los valores nosotros mismos, se usarán los valores predeterminados. ¿Nos ayuda a la hora de decidir qué hacer con estos valores?

  2. ¿Podemos cambiar la forma en que la cadena de conexión del almacenamiento se establece para que la expresión compleja no se defina en línea con el recurso?

    resource webSite 'Microsoft.Web/sites@2023-12-01' = {
      name: webSiteName
      location: location
      properties: {
        serverFarmId: hostingPlan.id
        siteConfig: {
          appSettings: [
            {
              name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
              value: AppInsights_webSiteName.properties.InstrumentationKey
            }
            {
              name: 'StorageAccountConnectionString'
              value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(storageAccount.id, storageAccount.apiVersion).keys[0].value}'
            }
          ]
        }
      }
      identity: {
        type: 'UserAssigned'
        userAssignedIdentities: {
          '${msi.id}': {}
        }
      }
    }
    

Orden de los elementos

  1. ¿Estamos satisfechos con el orden de los elementos en el archivo? ¿Cómo podríamos mejorar la legibilidad del archivo moviendo los elementos?

  2. Vamos a detenernos en la variable databaseName. ¿Está donde debe estar?

    var databaseName = 'ToyCompanyWebsite'
    resource sqlserverName_databaseName 'Microsoft.Sql/servers/databases@2023-08-01-preview' = {
      name: '${sqlserver.name}/${databaseName}'
      location: location
      sku: {
        name: 'Basic'
      }
      properties: {
        collation: 'SQL_Latin1_General_CP1_CI_AS'
        maxSizeBytes: 1073741824
      }
    }
    
  3. ¿Se ha fijado en el recurso sin marca de comentario, webSiteConnectionStrings? ¿Cree que debe estar en el archivo?

Agregar comentarios, etiquetas y otros metadatos

Pensemos en cualquier elemento de la plantilla que podría no ser evidente o que requiera más explicación. ¿Podemos agregar comentarios para que sea más fácil de entender para otros usuarios que puedan abrir el archivo en el futuro?

  1. Echemos un vistazo a la propiedad identity del recurso webSite:

    resource webSite 'Microsoft.Web/sites@2023-12-01' = {
      name: webSiteName
      location: location
      properties: {
        serverFarmId: hostingPlan.id
        siteConfig: {
          appSettings: [
            {
              name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
              value: AppInsights_webSiteName.properties.InstrumentationKey
            }
            {
              name: 'StorageAccountConnectionString'
              value: 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(storageAccount.id, storageAccount.apiVersion).keys[0].value}'
            }
          ]
        }
      }
      identity: {
        type: 'UserAssigned'
        userAssignedIdentities: {
          '${msi.id}': {}
        }
      }
    }
    

    Qué sintaxis más rara, ¿no? ¿Quizá necesita un comentario que ayude a aclararla?

  2. Fijémonos en el recurso de asignación de roles:

    resource roleassignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
      name: guid(roleDefinitionId, resourceGroup().id)
    
      properties: {
        principalType: 'ServicePrincipal'
        roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', roleDefinitionId)
        principalId: msi.properties.principalId
      }
    }
    

    El nombre del recurso emplea la función guid(). ¿Ayudaría explicar por qué la usa?

  3. ¿Podemos incluir una descripción en la asignación de roles?

  4. ¿Podemos agregar un conjunto de etiquetas a cada recurso?

Solución propuesta

Este es un ejemplo de cómo podríamos refactorizar la plantilla. Es posible que su plantilla no tenga exactamente el mismo aspecto que esta, ya que su estilo podría ser diferente.

@description('The location into which your Azure resources should be deployed.')
param location string = resourceGroup().location

@description('Select the type of environment you want to provision. Allowed values are Production and Test.')
@allowed([
  'Production'
  'Test'
])
param environmentType string

@description('A unique suffix to add to resource names that need to be globally unique.')
@maxLength(13)
param resourceNameSuffix string = uniqueString(resourceGroup().id)

@description('The administrator login username for the SQL server.')
param sqlServerAdministratorLogin string

@secure()
@description('The administrator login password for the SQL server.')
param sqlServerAdministratorLoginPassword string

@description('The tags to apply to each resource.')
param tags object = {
  CostCenter: 'Marketing'
  DataClassification: 'Public'
  Owner: 'WebsiteTeam'
  Environment: 'Production'
}

// Define the names for resources.
var appServiceAppName = 'webSite${resourceNameSuffix}'
var appServicePlanName = 'AppServicePLan'
var sqlServerName = 'sqlserver${resourceNameSuffix}'
var sqlDatabaseName = 'ToyCompanyWebsite'
var managedIdentityName = 'WebSite'
var applicationInsightsName = 'AppInsights'
var storageAccountName = 'toywebsite${resourceNameSuffix}'
var blobContainerNames = [
  'productspecs'
  'productmanuals'
]

@description('Define the SKUs for each component based on the environment type.')
var environmentConfigurationMap = {
  Production: {
    appServicePlan: {
      sku: {
        name: 'S1'
        capacity: 2
      }
    }
    storageAccount: {
      sku: {
        name: 'Standard_GRS'
      }
    }
    sqlDatabase: {
      sku: {
        name: 'S1'
        tier: 'Standard'
      }
    }
  }
  Test: {
    appServicePlan: {
      sku: {
        name: 'F1'
        capacity: 1
      }
    }
    storageAccount: {
      sku: {
        name: 'Standard_LRS'
      }
    }
    sqlDatabase: {
      sku: {
        name: 'Basic'
      }
    }
  }
}

@description('The role definition ID of the built-in Azure \'Contributor\' role.')
var contributorRoleDefinitionId = 'b24988ac-6180-42a0-ab88-20f7382dd24c'
var storageAccountConnectionString = 'DefaultEndpointsProtocol=https;AccountName=${storageAccount.name};EndpointSuffix=${environment().suffixes.storage};AccountKey=${listKeys(storageAccount.id, storageAccount.apiVersion).keys[0].value}'

resource sqlServer 'Microsoft.Sql/servers@2023-08-01-preview' = {
  name: sqlServerName
  location: location
  tags: tags
  properties: {
    administratorLogin: sqlServerAdministratorLogin
    administratorLoginPassword: sqlServerAdministratorLoginPassword
    version: '12.0'
  }
}

resource sqlDatabase 'Microsoft.Sql/servers/databases@2023-08-01-preview' = {
  parent: sqlServer
  name: sqlDatabaseName
  location: location
  sku: environmentConfigurationMap[environmentType].sqlDatabase.sku
  tags: tags
}

resource sqlFirewallRuleAllowAllAzureIPs 'Microsoft.Sql/servers/firewallRules@2023-08-01-preview' = {
  parent: sqlServer
  name: 'AllowAllAzureIPs'
  properties: {
    endIpAddress: '0.0.0.0'
    startIpAddress: '0.0.0.0'
  }
}

resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = {
  name: appServicePlanName
  location: location
  sku: environmentConfigurationMap[environmentType].appServicePlan.sku
  tags: tags
}

resource appServiceApp 'Microsoft.Web/sites@2023-12-01' = {
  name: appServiceAppName
  location: location
  tags: tags
  properties: {
    serverFarmId: appServicePlan.id
    siteConfig: {
      appSettings: [
        {
          name: 'APPINSIGHTS_INSTRUMENTATIONKEY'
          value: applicationInsights.properties.InstrumentationKey
        }
        {
          name: 'StorageAccountConnectionString'
          value: storageAccountConnectionString
        }
      ]
    }
  }
  identity: {
    type: 'UserAssigned'
    userAssignedIdentities: {
      '${managedIdentity.id}': {} // This format is required when working with user-assigned managed identities.
    }
  }
}

resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = {
  name: storageAccountName
  location: location
  sku: environmentConfigurationMap[environmentType].storageAccount.sku
  kind: 'StorageV2'
  properties: {
    accessTier: 'Hot'
  }

  resource blobServices 'blobServices' existing = {
    name: 'default'

    resource containers 'containers' = [for blobContainerName in blobContainerNames: {
      name: blobContainerName
    }]
  }
}

@description('A user-assigned managed identity that is used by the App Service app to communicate with a storage account.')
resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-07-31-preview'= {
  name: managedIdentityName
  location: location
  tags: tags
}

@description('Grant the \'Contributor\' role to the user-assigned managed identity, at the scope of the resource group.')
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
  name: guid(contributorRoleDefinitionId, resourceGroup().id) // Create a GUID based on the role definition ID and scope (resource group ID). This will return the same GUID every time the template is deployed to the same resource group.
  properties: {
    principalType: 'ServicePrincipal'
    roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', contributorRoleDefinitionId)
    principalId: managedIdentity.properties.principalId
    description: 'Grant the "Contributor" role to the user-assigned managed identity so it can access the storage account.'
  }
}

resource applicationInsights 'Microsoft.Insights/components@2020-02-02' = {
  name: applicationInsightsName
  location: location
  kind: 'web'
  tags: tags
  properties: {
    Application_Type: 'web'
  }
}

Sugerencia

Si trabaja con sus compañeros mediante GitHub o Azure Repos, este sería un momento ideal de enviar una solicitud de incorporación de cambios para integrar los cambios en la rama principal. Enviar solicitudes de incorporación de cambios siempre viene bien después de realizar una tarea de refactorización.