Un paramètre Bicep mal orthographié ne plante pas toujours ton déploiement. Écris otionalProperty au lieu de optionalProperty dans un objet de configuration, et par défaut, Bicep se contente d’un avertissement BCP089 — le déploiement continue, la propriété correcte reste vide, et tu découvres le problème en production plutôt qu’en revue de code. C’est exactement le genre de bug que les types définis par l’utilisateur, disponibles depuis la CLI Bicep 0.12, permettent d’éliminer, à condition de connaître les bons décorateurs.
Le problème que @allowed et les commentaires ne résolvent qu’à moitié
La façon classique de contraindre un paramètre Bicep, c’est le décorateur @allowed directement sur le paramètre :
@allowed([
'Standard_LRS'
'Standard_GRS'
])
param storageAccountSKU string = 'Standard_LRS'
Ça fonctionne pour un paramètre isolé. Mais dès que tu veux réutiliser cette contrainte dans plusieurs modules — le SKU d’un compte de stockage revient dans presque tous les modules d’une landing zone —, tu recopies le même bloc @allowed partout, avec le risque qu’une équipe l’oublie ou le fasse dériver légèrement d’un module à l’autre. Les types définis par l’utilisateur déplacent cette contrainte dans un type nommé, réutilisable et exportable.
Déclarer un type et le rendre solide
La syntaxe de base est courte, comme le montre la documentation officielle des types définis par l’utilisateur :
type storageAccountSkuType = 'Standard_LRS' | 'Standard_GRS'
type storageAccountConfigType = {
name: string
sku: storageAccountSkuType
}
param storageAccountConfig storageAccountConfigType
Ce qui rend ces types utiles en pratique, ce sont les décorateurs qu’on peut leur attacher. Ils vivent dans le namespace sys, donc en cas de conflit de nom tu peux toujours forcer sys.description() :
@minLength()/@maxLength()— bornes inclusives sur des chaînes ou des tableaux.@minValue()/@maxValue()— bornes inclusives sur des entiers.@secure()— masque la valeur dans l’historique de déploiement, applicable aux chaînes comme aux objets entiers.@description()— accepte du Markdown, utile pour la documentation générée automatiquement.@sealed()— transforme un avertissement de propriété inconnue en erreur bloquante. C’est le plus rentable des cinq, et il mérite sa propre section.
@sealed() : le décorateur qui aurait évité le bug d’introduction
Reprenons l’exemple du typo. Sans @sealed() :
type anObject = {
property: string
optionalProperty: string?
}
param aParameter anObject = {
property: 'value'
otionalProperty: 'value' // typo : simple avertissement BCP089, le déploiement continue
}
Avec @sealed() sur le type, ou directement sur le paramètre, la même faute de frappe devient une erreur de compilation, pas un warning noyé dans la sortie de az deployment :
@sealed()
type anObject = {
property: string
optionalProperty: string?
}
Sur un module partagé entre plusieurs équipes, ce n’est pas du perfectionnisme : c’est la différence entre un déploiement qui échoue tout de suite en pipeline et une ressource mal configurée qui passe une revue de code sans que personne ne le remarque. Il n’y a pas vraiment de cas où @sealed() coûte plus qu’il ne rapporte sur un type destiné à être réutilisé — mets-le par défaut, et retire-le seulement si un module a explicitement besoin de propriétés libres.
Unions discriminées : un seul paramètre, plusieurs formes possibles
Depuis la CLI 0.21, @discriminator() permet de typer un paramètre qui peut prendre plusieurs formes selon une propriété pivot. Pratique pour des modules qui exposent plusieurs modes de configuration sous un seul nom de paramètre :
type FooConfig = {
type: 'foo'
value: int
}
type BarConfig = {
type: 'bar'
value: bool
}
@discriminator('type')
type ServiceConfig = FooConfig | BarConfig
param serviceConfig ServiceConfig = { type: 'bar', value: true }
Bicep sait alors que si type vaut 'bar', value doit être un booléen, et le signale à la compilation si tu passes un entier par erreur, plutôt qu’à l’exécution côté Azure Resource Manager.
Types dérivés de ressources : la nouveauté qui évite de deviner le schéma
Fonctionnalité plus récente, CLI 0.34.1 minimum, les types dérivés de ressources extraient directement le schéma d’un type de ressource Azure, sans que tu aies à le recopier à la main :
param storageAccountProps resourceInput<'Microsoft.Storage/storageAccounts@2023-01-01'>.properties = {
accessTier: 'Hot'
minimumTlsVersion: 'TLS1_2'
allowBlobPublicAccess: false
supportsHttpsTrafficOnly: true
}
resource storageAccount 'Microsoft.Storage/storageAccounts@2025-06-01' = {
name: 'mystorageacct123'
location: resourceGroup().location
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
properties: storageAccountProps
}
resourceInput<...> expose les propriétés éditables, hors champs en lecture seule ; resourceOutput<...> fait l’inverse pour typer une sortie. Une nuance à garder en tête : cette vérification est faite à l’édition et à la compilation, pas imposée par le service ARM au moment du déploiement. Un @sealed() explicite reste donc le seul mécanisme qui bloque réellement un déploiement non conforme côté Azure.
Exporter des types pour arrêter de les redéfinir dans chaque module
Une fois qu’un type est stabilisé, @export() le rend disponible à l’import depuis d’autres fichiers Bicep :
@export()
type storageAccountSkuType = 'Standard_LRS' | 'Standard_GRS'
@export()
type storageAccountConfigType = {
name: string
sku: storageAccountSkuType
}
Si ton équipe a déjà investi dans les Azure Verified Modules pour standardiser ses landing zones, exporter les types utilisés par ces modules dans un fichier partagé règle exactement le problème que l’AVM tente de résoudre de son côté : deux équipes qui redéfinissent le même objet de configuration avec des contraintes légèrement différentes, jusqu’à ce qu’un déploiement échoue pour une raison que personne ne retrouve rapidement.
FAQ
Faut-il une version particulière de Bicep CLI pour utiliser tout ça ?
Les types de base fonctionnent dès la CLI 0.12.X. Les unions discriminées (@discriminator()) demandent la 0.21.X, et les types dérivés de ressources (resourceInput/resourceOutput) la 0.34.1 au minimum — vérifie ta version avec az bicep version avant de t’appuyer dessus dans un pipeline partagé.
Un type récursif est-il possible en Bicep ?
Oui, mais seulement si au moins un chemin vers l’appel récursif est optionnel. Un type où chaque niveau imbriqué référence obligatoirement le suivant, sans jamais de propriété optionnelle, ne compile pas.
Terraform a-t-il un équivalent direct ?
Pas vraiment sous cette forme : Terraform valide surtout via des blocs variable avec validation, sans système de types nommés et exportables aussi proche de TypeScript. Si le choix entre les deux outils se pose encore chez toi, on avait détaillé les critères dans notre comparatif Terraform vs Bicep.
Rien de tout ça ne demande de réécrire tes modules existants d’un coup. Commence par sceller (@sealed()) les objets de configuration les plus copiés-collés dans ton dépôt, et exporte-les au fur et à mesure que tu les stabilises. C’est le genre de changement qui ne se voit dans aucune démo, mais qui t’évite un ticket de support un vendredi soir.

Laisser un commentaire