Sunday, March 1, 2026

Deploying Azure Infrastructure… and Turning on the Light


If something has an API and a meaningful desired state, it can be modeled declaratively. That idea feels obvious in the cloud. We describe virtual machines, networks, load balancers, and container apps in code, and the platform converges reality toward that definition. We call it Infrastructure as Code.

But what happens if you apply that same thinking to something in the physical world?

I decided to find out. Using Bicep’s experimental local extension capability, I built a custom resource provider that treats a Home Assistant light as a first-class Infrastructure-as-Code resource. Running az bicep local-deploy now physically changes the state of a Zigbee light in my office, declaratively and idempotently. Not through a script. Not through an automation. Through a resource definition.

Let’s start by seeing it in action in the video below!

Why This Wasn’t Just a Gimmick

Home automation APIs are almost always imperative. You tell them to “turn on,” “turn off,” or “toggle.” That works perfectly for dashboards and mobile apps, but it clashes with Infrastructure as Code principles. IaC is about convergence. You don’t toggle a VM. You declare its desired configuration. You don’t flip a load balancer. You define its final state and let the system move toward it. So my first rule of this extension was simple: no toggle support. A deployment must explicitly declare “on” or “off”. Running the same deployment twice must result in the same outcome. Anything else would undermine the model.

The Real Hardware Behind It

This isn’t simulated. It’s running on actual hardware. Home Assistant is running locally in a VM on Windows 11. For Zigbee connectivity, I’m using the Sonoff ZBDongle-E as the Zigbee 3.0 coordinator. The light itself is an Aqara T2 Light Bulb, capable of RGB and color temperature modes.

The signal path looks like this: Laptop → Home Assistant VM → Zigbee dongle → Aqara bulb

The extension never talks to Zigbee directly. It communicates with Home Assistant via its REST API. Home Assistant handles the radio communication and device state management. That separation is important. The Bicep extension doesn’t care whether the device is Zigbee, Thread, Wi-Fi, or something else. It only cares that the system exposes a stable API and that the resource has a meaningful desired state. Once you abstract at that level, the pattern becomes clean.

The Enabler: Bicep Local Extensions

The experimental local extension model in Bicep allows you to register a custom executable as a resource provider. Instead of deploying to Azure Resource Manager, az bicep local-deploy invokes your handler locally. The execution flow looks like this:

Bicep CLI parses the template. It discovers the registered extension. It invokes the resource handler. The handler performs the operation locally. Outputs are returned to Bicep.

In this case, the handler calls the Home Assistant REST API, applies the declared state, reads back the entity to confirm convergence, and returns outputs. Everything runs locally. Nothing touches Azure. That’s what makes this interesting. Bicep becomes a declarative engine, not just an Azure deployment tool.

Modeling the Light as Infrastructure

Here’s what the resource definition in main.Bicep looks like:

targetScope = 'local'
extension homeassist
@secure()
@description('Home Assistant long-lived access token')
param accessToken string
@description('Home Assistant instance URL')
param homeAssistantUrl string = 'http://192.168.68.70:8123'
@description('The entity ID of the light to control')
param lightEntityId string = 'light.aqara_lumi_light_agl003'
@description('Desired state of the light')
@allowed(['on', 'off'])
param lightState string = 'on'
@description('Brightness level (0-255)')
@minValue(0)
@maxValue(255)
param brightness int = 100
@description('Color temperature mireds (153-500, 0 to skip)')
@minValue(0)
@maxValue(500)
param colorTemp int = 0
@description('Hue 0-360 (-1 to skip)')
@minValue(-1)
@maxValue(360)
param hue int = -1
@description('Saturation 0-100 (-1 to skip)')
@minValue(-1)
@maxValue(100)
param saturation int = -1
resource aqaraLight 'Light' = {
entityId: lightEntityId
homeAssistantUrl: homeAssistantUrl
accessToken: accessToken
state: lightState
brightness: brightness
colorTemp: colorTemp
hue: hue
saturation: saturation
}
output currentState string = aqaraLight.currentState
output friendlyName string = aqaraLight.friendlyName
output entityId string = aqaraLight.entityId

This is the main.bicepparam file, I left out the access token for obvious reasons.

using 'main.bicep'
param accessToken = 'XXX'
param homeAssistantUrl = 'http://192.168.68.72:8123'
param lightEntityId = 'light.aqara_lumi_light_agl003'
param lightState = 'off'
param brightness = 100
param colorTemp = 10
param hue = -1
param saturation = -1

And to be able to import the HomeAssist extension, the following is configured in the bicepconfig.json file. It contains the references to the bin folder where the homeassist extension is located, as well as enabling the localDeploy experimental feature.

{
"experimentalFeaturesEnabled": {
"localDeploy": true
},
"extensions": {
"homeassist": "./bin/bicep-ext-homeassist"
}
}

To kick off the Bicep local deploy run:

az bicep local-deploy main.bicepparam

The Extension Itself

Under the hood, the extension is a .NET 9 resource host that registers a Light resource type and implements CreateOrUpdate semantics. Instead of imperative commands, it computes a desired end state from the template properties and applies exactly one color mode per deployment. It deliberately avoids toggle behavior and enforces idempotency at the handler level. The executable is packaged using az bicep publish-extension and invoked locally through the experimental local-deploy runtime, effectively acting as a custom resource provider, just one that happens to manage a Zigbee bulb instead of a cloud resource.

The Color Mode Lesson

One of the more subtle issues during development was color handling. Home Assistant does not allow color_temp and hs_color in the same service call. Sending both results in an HTTP 400. The extension now enforces exclusivity: if colorTemp is set, it uses color_temp. Otherwise, if hue and saturation are set, it uses hs_color. Never both. It’s a small detail, but it reinforces an important principle: declarative modeling only works when you respect the semantics of the underlying API.

Building and Publishing the Extension

Because this relies on the experimental local extension model, the build and registration steps are important. Here’s the exact build process I use on Windows:

# Restore dependencies
dotnet restore HomeAssistExtension.csproj
# Build
dotnet build HomeAssistExtension.csproj --configuration Release
# Publish self-contained binary
dotnet publish HomeAssistExtension.csproj --configuration Release -r win-x64
# Register with Bicep
az bicep publish-extension `
--bin-win-x64 .\bin\Release\net9.0\win-x64\publish\bicep-ext-homeassist.exe `
--target .\bin\bicep-ext-homeassist `
--force

Dotnet publish produces the executable that implements the resource handlers. az bicep publish-extension packages and registers it locally so Bicep can discover and invoke it during local-deploy. After that, any Bicep file declaring the extension homeassist can use the custom Light resource. It’s Bicep acting as a declarative runtime locally.

Outputs

One of the things I wanted from the beginning was proper deployment feedback — not just “it worked” or “it failed,” but meaningful outputs that reflect the observed state of the device. The Bicep template defines explicit outputs:

output currentState string = aqaraLight.currentState
output friendlyName string = aqaraLight.friendlyName
output entityId string = aqaraLight.entityId

They are returned by the extension itself after it completes the convergence logic. Inside the handler, after calling the appropriate Home Assistant service (light.turn_on or light.turn_off), the extension reads the actual entity state from Home Assistant and maps relevant fields back into the resource outputs.

  • currentState reflects the actual final state reported by Home Assistant.

Surfacing real outputs transforms this from “a script that flips a light” into a proper resource model.

The extension applies the declared desired state, reads back the actual state from the source system, and returns that state as outputs to the Bicep runtime.

It’s being declared, converged, and observed.

And It Doesn’t Stop There

The light was just the smallest possible proof. Through Home Assistant, I already have dishwashers, washing machines, printers, motion sensors, smart plugs, and other appliances integrated behind the same API surface. They all expose the state. They all expose actions. They all have configuration that could be expressed declaratively. More fun challenges ahead!

The extension currently supports lights. But there’s nothing fundamentally preventing it from expanding to switches, scenes, or anything else. Once the extension model exists, the boundary moves.

We’ve been thinking about Infrastructure as Code in terms of where it runs: the cloud, the data center, the platform. Maybe we should start thinking about it in terms of what can converge.

Watching a Bicep deployment turn on a light is fun. Realizing that the same pattern could declaratively manage an entire physical environment is the interesting part.

What’s the most unexpected thing you’d model declaratively?

 

Saturday, June 7, 2025

From the Stage at Microsoft Build 2025: My Story & What’s Coming Next

I had the absolute honor and pleasure of presenting a session at the Microsoft Build 2025 conference in Seattle last month. If you've been following me here, it won’t be a surprise that the topic was Bicep Infrastructure as Code for Azure.

I love a challenge, and this session certainly was one. It was a live demo session, which meant I had just 15 minutes, theater-style, to deliver a clear message, teach something valuable, and tell a compelling story.

Full disclosure: I had some experience with this session format from previous Build and Ignite events. The key is to trim the content to the bare essentials of your message. There’s no time for an extended introduction; demos must be live, fast-paced, visually engaging, and still easy to follow for both in-person attendees and those watching later. Zooming and drawing on your screen effectively can make a huge difference. I've been using ZoomIt for that for years and highly recommend it.

I chose to focus on the reusability aspect of Bicep code. I covered the three types of modules: stored locally, in Azure Container Registries, and Azure Verified Modules, using practical examples to highlight the differences.

Then, I took it a step further by showing how to enhance reusability with the export() decorator for custom types and functions, as well as resource-derived types.

Along the way, I showcased new Bicep features like the onlyIfNotExists() decorator and secure() decorators for module outputs. Whenever presenting Bicep at any event, I always like to demonstrate the Bicep Visualizer and the power of the Bicep extension to improve the overall authoring experience in VS Code.

I always try to build a narrative and begin with something unexpected, usually a personal story, to connect with the audience and capture their attention. So, how did I start my session this time? Watch the video and find out!

https://build.microsoft.com/en-US/sessions/DEM539

I also gave away a few copies of my book Getting started with Bicep: Infrastructure as code on Azure after the session.

Getting started with Bicep: Infrastructure as code on Azure

I had the honor of handing out these Bicep pins on behalf of the Bicep Product team, which were a huge hit!

Article content
Bicep pins to hand out as swag

And lastly, I had a great time continuing the conversation on Bicep at the Azure Infrastructure booth in the Hub, where I spent a whole morning answering questions, performing demos, and having great conversations.

Article content
Azure Infrastructure booth in the Hub area.

This photo was taken just two minutes before showtime. Check out the sign on the middle-left that says “ARM”, unintentionally hilarious for a session all about Bicep! :) And don’t miss the guy photo-bombing in the back — one of the fantastic A/V crew members who recorded the session. We had a great laugh about it together afterwards!

Article content
Photo taken 2 min before showtime.

Statistics shared by the event indicated 142 in-person attendees for my session! I’m truly honored. It’s incredibly rewarding to know that the content resonated with so many people, and I’m grateful for the opportunity to contribute to Build this year.

I'm looking back at a great week in the Seattle Convention Center!

And finally, it doesn't stop there; I will deliver an extended version of this session to upcoming community conferences. More news on those soon!

Monday, December 16, 2024

Bicep Christmas gift: the deployer() function

The Bicep language for Azure Infrastructure as Code (IaC) continues to evolve rapidly, reshaping how developers and cloud architects design, deploy, and manage Azure resources.

In this post, we’ll explore an exciting new function introduced in Bicep version 0.32, highlighting how it enhances the language’s capabilities for Azure Infrastructure as Code (IaC). These additions reflect Bicep’s continuous evolution, making it even more powerful and versatile for developers working with Azure.

A common challenge in Azure Infrastructure as Code (IaC) is identifying and reusing the identity executing a deployment. This capability would enable assigning roles or configuring access dynamically without hardcoding identity details or managing them externally.

Currently, Bicep provides functions like subscription(), retrieving metadata about the subscription in which the deployment is running. However, there wasn’t a built-in way to access details about the principal executing the deployment.

With Bicep 0.32 you now can! The function deployer() is introduced, which allows you to retrieve the object of the principal that is deploying the Bicep file!

The function deployer() allows you to retrieve the objectId of the principal that is deploying the Bicep file

The ability to natively access the deploying principal within Bicep would eliminate the need for additional automation scripts or manual intervention, making deployments more streamlined and robust.

One practical use case for this functionality is generating unique GUIDs for scenarios such as creating role assignments. By leveraging the unique attributes of the deploying identity — such as its Object ID or Tenant ID — along with other contextual details, you can create deterministic and consistent GUIDs.

generating unique GUIDs

The deployer() function also proves valuable in scenarios involving Azure Entra ID. For instance, when creating an Entra group using the new Graph extension, you can seamlessly assign the current user (or deploying identity) to that group. This dynamic capability simplifies group membership management and eliminates the need for workarounds.

Upgrade to 0.32 to get access to the deployer() function!

As we wrap up, it’s exciting to see how features like the deployer() function are expanding the capabilities of Bicep, making Azure Infrastructure as Code more powerful and intuitive.

With the holiday season upon us, it’s the perfect time to reflect. Whether you’re deploying in the cloud or just enjoying some downtime, here’s wishing you a joyous holiday season and a fantastic start to the new year! Looking forward to 2025!

***

Originally posted here: https://www.linkedin.com/pulse/bicep-christmas-gift-deployer-function-freek-berson-a8hye