Ever since the introduction of user-defined types in Azure Bicep, I have been experimenting with the best way to create strongly typed objects and parameters in order to improve the overall developer experience when using Bicep. I have settled on a concept that I think is worth sharing, which we will go over in this blog post.
User-defined types
Before we dive into the details, I will first give an introduction to user-defined types in Bicep.
From the beginning, Bicep did not have strongly typed objects for anything other than resource definitions.
Things have changed with a few key releases to the Bicep language, and with the introduction of export
and import
support for user-defined types, we can now share and reuse type definitions across modules, enabling much stronger type safety and consistency in our Bicep templates.
User-defined types are basically structured definitions that allow us to specify the exact shape and properties of objects in our Bicep templates. This means we can define complex parameter types, enforce required fields, and provide better validation and IntelliSense support when authoring infrastructure as code.
Below is an example of what a user-defined type looks like.
|
|
User-defined type parameters
The best use case I have found for user-defined types is for structuring parameter definitions. Typically, I find myself sharing resource naming conventions, configuration objects, and other common structures throughout an infrastructure project. When the project reaches a certain size, maintaining consistency and reusability becomes much easier by defining these shared shapes as user-defined types.
When I start a new infrastructure project nowadays, the first thing I do is create a shared configuration object. This consists of things I will use throughout my project - names of shared resources, configurations, and secret names. Below is a real-world example of how this looks in practice.
|
|
Let’s go over what these user-defined types and functions accomplish:
- naming: Centralizes all resource naming conventions, making it easy to update names in one place and maintain consistency across your Bicep modules.
- config: Defines the allowed SKUs and configuration options for shared resources, enforcing valid values and reducing the risk of typos or misconfiguration.
- secret: Provides a secure structure for sensitive values, leveraging Bicep’s
@secure()
decorator to ensure secrets are handled appropriately. - shared: Aggregates the above types into a single object, simplifying parameter passing and promoting reuse.
- defaultNaming: A function that generates standardized resource names based on the environment, further reducing duplication and manual errors.
With these in place, we can now use them across our modules, promoting reusability and structured parameters. An example of this looks like this:
|
|
Benefits of Strongly Typed Parameters
By leveraging user-defined types for parameters in Bicep, you gain several advantages:
- Improved Validation: Bicep will catch type mismatches and missing required properties at compile time, reducing deployment errors.
- Enhanced IntelliSense: Editors like VS Code provide better autocompletion and documentation, making infrastructure development faster and less error-prone.
- Reusability: Shared types and configuration objects can be imported across modules.
- Consistency: Centralizing naming conventions and configuration ensures all resources follow the same standards, making your infrastructure easier to manage and audit.
- Security: Using decorators like
@secure()
for secrets helps enforce best practices for sensitive data.
Conclusion
User-defined types in Bicep are a powerful feature that can significantly improve the maintainability, safety, and clarity of your infrastructure as code. By structuring your parameters and shared configuration using these types, you create a strong foundation for a robust infrastructure project.
If you haven’t already, try refactoring your Bicep templates to use user-defined types and see how it improves your workflow!