<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://msftplayground.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://msftplayground.com/" rel="alternate" type="text/html" /><updated>2026-04-21T07:43:27+02:00</updated><id>https://msftplayground.com/feed.xml</id><title type="html">Microsoft Playground</title><subtitle>A blog about Architecture, Azure, Azure DevOps, Git, GitHub, and DevOps — by Microsoft MVP Maik van der Gaag</subtitle><author><name>Maik van der Gaag</name></author><entry><title type="html">Automatically onboard subscriptions with Azure Policy</title><link href="https://msftplayground.com/2026/03/automatically-onboard-subscriptions-with-azure-policy" rel="alternate" type="text/html" title="Automatically onboard subscriptions with Azure Policy" /><published>2026-03-09T01:00:00+01:00</published><updated>2026-03-09T01:00:00+01:00</updated><id>https://msftplayground.com/2026/03/automatically-onboard-subscriptions-with-azure-policy</id><content type="html" xml:base="https://msftplayground.com/2026/03/automatically-onboard-subscriptions-with-azure-policy"><![CDATA[<p>A lot of organizations manage multiple tenants. This can be the case for managed service providers but also for enterprises. For these cases, you would like to have delegated resource access to be able to manage those tenants.</p>

<p>This delegated resource access can be achieved by setting up Azure Lighthouse.</p>

<h2 id="what-is-azure-lighthouse">What is Azure Lighthouse</h2>

<p>Azure Lighthouse is a way to enable multitenant management with scalability, higher automation, and enhanced governance across resources. By using Azure Lighthouse, organizations can deliver services via the Azure platform. The main advantage is that the onboarded parties maintain control over who has access to their tenant, which resources they can access, and what actions they can take. As mentioned before, this is very useful in cases where organizations or MSPs have to manage multiple Azure Subscriptions that are located in multiple Entra Tenants.</p>

<p><img src="/assets/images/2026/lighthouse.png" alt="Lighthouse diagram Microsoft" /></p>

<h3 id="what-is-possible-with-azure-lighthouse">What is possible with Azure Lighthouse</h3>

<p>Azure Lighthouse offers you the following capabilities.</p>

<ul>
  <li>Manage onboarded Azure resources within your own tenant without switching context.</li>
  <li>It offers great insights into the tenants you manage.</li>
  <li>View cross-tenant information within the Azure Lighthouse blade.</li>
  <li>With deployment templates you can perform cross-tenant management tasks.</li>
</ul>

<h3 id="how-does-it-work">How does it work</h3>

<p>Azure Lighthouse configures permissions and capabilities for resources on a specified level. This can for example be a resource group or an Azure Subscription. This is all done by specifying this within a Template (ARM / Bicep).</p>

<p>To see more about this template you can check out the <a href="https://github.com/Azure/Azure-Lighthouse-samples">samples</a> repository for Azure Lighthouse on GitHub.</p>

<p>In the template for an Azure Lighthouse connection, this is done in the following steps:</p>

<ul>
  <li>Find out which role your identities / groups need to manage Azure resources in the other tenant. This is the role definition id.</li>
  <li>Onboard the resources/tenant by using a template. This could also be possible by using managed applications. This option is mainly used by service providers that sell management capabilities to their customers.</li>
  <li>Once the other tenant is onboarded, the specified identities/group of identities are able to manage the resources.</li>
</ul>

<p>A lot of information about Azure Lighthouse can be found on <a href="https://learn.microsoft.com/en-us/azure/lighthouse/overview">Microsoft Learn</a>.</p>

<h3 id="is-there-a-downside">Is there a downside</h3>

<p>Every capability normally also has a downside of using it. Personally speaking, one of the downsides I see for Azure Lighthouse is that the scopes that can be onboarded are subscriptions or resource groups. At the moment of writing this article, it isn’t possible to onboard a subscription automatically.</p>

<p>As a company that manages multiple customers (tenants), this can be a pretty tough situation. Normally, you would like to offer customers some kind of flexibility, meaning that you allow them to create Azure Subscriptions. When that is the case, you introduce the problem that you will not be able to manage those subscriptions for your customer.</p>

<p>That is where this article kicks in. There are options to automatically onboard new Azure Subscriptions.</p>

<h2 id="how-do-i-onboard-new-azure-subscriptions">How do I onboard new Azure Subscriptions</h2>

<p>You might be asking: how do we then onboard those new subscriptions?</p>

<p>This is where Azure Policy comes in. With Azure Policy, we are able to do deployments in certain specific situations. For this case, the situation would be a subscription that is not delegated for management in our management tenant. By deploying this policy to a Management Group, we make sure that every subscription that gets created and placed in this management group is automatically delegated for management by the other tenant.</p>

<p>With Azure Policy, we can use the ‘deployIfNotExists’ effect to achieve this. If you are interested in using this effect, you can check out <a href="https://learn.microsoft.com/en-us/azure/governance/policy/concepts/effect-deploy-if-not-exists?WT.mc_id=AZ-MVP-5004255">this</a> documentation from Microsoft. The effect of the policy checks a specific condition on a resource. If it does not find that condition, it performs a deployment that you specify.</p>

<h3 id="infrastructure-as-code">Infrastructure as Code</h3>

<p>The way to add the policy to the environment is by using infrastructure as code. In this example, we will use Bicep, but of course other languages such as Terraform can also be used. Adding a Policy to the Azure platform is done in two steps:</p>

<ul>
  <li><strong>Step 1:</strong> Adding the definition of the Azure Policy on a specific scope. The definition basically describes what the policy does and when it performs the specified action. Adding the definition is normally done at the highest possible point in your Azure Hierarchy. This way, you can use the definition in lower parts of the hierarchy.</li>
  <li><strong>Step 2:</strong> When the definition is added to the platform, you can make an assignment of the definition on a specific scope. This means basically activating the policy on a specific scope, such as a Management group. When making the assignment, you normally also have to add specific parameters for the policy.</li>
</ul>

<p>As mentioned, we will be using Bicep. The below snippet represents our main.bicep file.</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">targetScope = 'managementGroup'</span>

<span class="s">metadata info = {</span>
  <span class="s">title</span><span class="err">:</span> <span class="s1">'</span><span class="s">Azure</span><span class="nv"> </span><span class="s">Lighthouse</span><span class="nv"> </span><span class="s">Configuration'</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">1.1.0'</span>
  <span class="na">author</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Maik</span><span class="nv"> </span><span class="s">van</span><span class="nv"> </span><span class="s">der</span><span class="nv"> </span><span class="s">Gaag'</span>
<span class="err">}</span>
<span class="s">metadata description = '''</span>
<span class="s">IaC for the creation of a Azure Lighthouse on customer subscriptions.</span> 
<span class="s1">'</span><span class="s">'</span><span class="s1">'</span>

<span class="s">@description('The ID of the management group where the policy will be placed.')</span>
<span class="s">param policyScopeId string</span> 

<span class="err">@</span><span class="s">description('The IDs of the Management Group where the assignment will be done for the policy.')</span>
<span class="s">param assignmentScopeIds array</span>

<span class="err">@</span><span class="s">description('Specify an array of objects, containing tuples of Entra principalId, a Azure roleDefinitionId, and an optional principalIdDisplayName. The roleDefinition specified is granted to the principalId in the provider\'s Entra and the principalIdDisplayName is visible to customers.')</span>
<span class="s">param authorizations array</span>

<span class="err">@</span><span class="s">description('The customer name used in the description')</span>
<span class="s">param customer string</span>

<span class="err">@</span><span class="s">description('The location of the Policy Assignment(s)')</span>
<span class="s">param location string</span>

<span class="s">// Default value Parameters</span>
<span class="err">@</span><span class="s">description('Specify a unique name for your offer')</span>
<span class="s">param mspOfferName string = 'Azure Lighthouse Management'</span>

<span class="err">@</span><span class="s">description('Name of the Service Provider offering')</span>
<span class="s">param mspOfferDescription string = 'Lighthouse connection between ${customer} and the management tenant'</span>

<span class="err">@</span><span class="s">description('Specify the tenant id of the Managing party')</span>
<span class="s">param managedByTenantId string</span>

<span class="err">@</span><span class="s">description('date to append to the deployment name of modules')</span>
<span class="s">param deploymentDate string = utcNow('yyyyMMddTHHmm')</span>

<span class="err">@</span><span class="s">description('The description in the azure portal for the policy assignment')</span>
<span class="s">param assignmentDescription string = 'This policy assignment deploys azure lighthouse so the managing can get access to the Azure subscriptions'</span>

<span class="err">@</span><span class="s">description('The Display Name for the policy assignment')</span>
<span class="s">param assignmentDisplayname string = 'Azure Lighthouse Deployment'</span>

<span class="err">@</span><span class="s">description('The messages that describe why a resource is non-compliant with the policy.')</span>
<span class="s">param nonComplianceMessage string = 'Azure Lighthouse is not configured on this subscription. Please contact your managing party to remediate this issue.'</span>

<span class="err">@</span><span class="s">description('The name of the policy')</span>
<span class="err">@</span><span class="s">maxLength(10)</span>
<span class="s">param name string</span>

<span class="s">module policyDefinition '../modules/lighthouse-policy.bicep' = {</span>
  <span class="s">name</span><span class="err">:</span> <span class="s1">'</span><span class="s">lighthouse-policy-def-${deploymentDate}'</span>
  <span class="na">scope</span><span class="pi">:</span> <span class="s">managementGroup(policyScopeId)</span>
  <span class="na">params</span><span class="pi">:</span> <span class="pi">{</span>
    <span class="nv">authorizations</span><span class="pi">:</span> <span class="nv">authorizations</span>
    <span class="nv">name</span><span class="pi">:</span> <span class="nv">name</span>
    <span class="nv">managedByTenantId</span><span class="pi">:</span> <span class="nv">managedByTenantId</span>
    <span class="nv">mspOfferName</span><span class="pi">:</span> <span class="nv">mspOfferName</span>
    <span class="nv">mspOfferDescription</span><span class="pi">:</span> <span class="nv">mspOfferDescription</span>
  <span class="pi">}</span>
<span class="err">}</span>

<span class="s">module policyAssignment '../modules/policyassignment.bicep' = [for id in assignmentScopeIds</span><span class="err">:</span> <span class="pi">{</span>
  <span class="nv">name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">lighthouse-policy-assignment-${deploymentDate}'</span>
  <span class="nv">scope</span><span class="pi">:</span> <span class="nv">managementGroup(id)</span>
  <span class="nv">params</span><span class="pi">:</span> <span class="pi">{</span>
    <span class="nv">name</span><span class="pi">:</span> <span class="nv">name</span>
    <span class="nv">location</span><span class="pi">:</span> <span class="nv">location</span>
    <span class="nv">assignmentDescription</span><span class="pi">:</span> <span class="nv">assignmentDescription</span>
    <span class="nv">assignmentDisplayname</span><span class="pi">:</span> <span class="nv">assignmentDisplayname</span>
    <span class="nv">nonComplianceMessage</span><span class="pi">:</span> <span class="nv">nonComplianceMessage</span>
    <span class="nv">policyDefinitionId</span><span class="pi">:</span> <span class="nv">policyDefinition.outputs.policyDefinitionId</span>
  <span class="pi">}</span>
<span class="pi">}</span><span class="err">]</span>
</code></pre></div></div>

<p>As you can see in the snippet, it uses two modules. One module for the policy definition and another for the assignment. Let’s go over a few important things about this Bicep file:</p>

<ul>
  <li>The module ‘policyDefinition’ is deployed first. This makes sure we have the definition available. As you can see in the module specification, it will be deployed to a Management Group.</li>
  <li>The module ‘policyAssignment’ will be deployed second and will make the policy assignment on all management groups that are specified in the ‘assignmentScopeIds’ array.</li>
  <li>The parameter ‘authorizations’ is a specific tuple to specify which authorization will be made within the managing tenant. Take a look at the snippet below as an example. In the tuple, you specify which principal (this can be any type of identity, such as a group, user, or service principal) gets a specific role. The display name is optional and is what can be seen within the managing tenant:</li>
</ul>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">{</span>
  <span class="nv">principalIdDisplayName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Security</span><span class="nv"> </span><span class="s">-</span><span class="nv"> </span><span class="s">Azure</span><span class="nv"> </span><span class="s">Lighthouse</span><span class="nv"> </span><span class="s">Reader'</span>
  <span class="nv">principalId</span><span class="pi">:</span> <span class="s1">'</span><span class="s">91882300-3331-40dc-bc8a-14a665a2fc55'</span>
  <span class="nv">roleDefinitionId</span><span class="pi">:</span>  <span class="s1">'</span><span class="s">acdd72a7-3385-48ef-bd42-f606fba81ae7'</span>
<span class="pi">}</span>
<span class="pi">{</span> 
  <span class="nv">principalIdDisplayName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Security</span><span class="nv"> </span><span class="s">-</span><span class="nv"> </span><span class="s">Azure</span><span class="nv"> </span><span class="s">Lighthouse'</span>
  <span class="nv">principalId</span><span class="pi">:</span> <span class="s1">'</span><span class="s">91882300-3331-40dc-bc8a-14a665a2fc55'</span>
  <span class="nv">roleDefinitionId</span><span class="pi">:</span> <span class="s1">'</span><span class="s">cfd33db0-3dd1-45e3-aa9d-cdbdf3b6f24e'</span>
<span class="pi">}</span>
</code></pre></div></div>
<p>The rest of the parameters speak for themselves and also have a decent description. So let’s look into the modules.</p>

<h3 id="policy">Policy</h3>

<p>The policy definition is specified in a separate module. This module only deploys the definition on a management group scope based on the parameters that are supplied.</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">targetScope = 'managementGroup'</span>

<span class="s">metadata info = {</span>
  <span class="s">title</span><span class="err">:</span> <span class="s1">'</span><span class="s">Azure</span><span class="nv"> </span><span class="s">Lighthouse</span><span class="nv"> </span><span class="s">Policy'</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">1.1.0'</span>
  <span class="na">author</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Maik</span><span class="nv"> </span><span class="s">van</span><span class="nv"> </span><span class="s">der</span><span class="nv"> </span><span class="s">Gaag'</span>
<span class="err">}</span>

<span class="s">metadata description = '''</span>
<span class="s">Module for the creation of a Azure Lighthouse Policy on customer environments.</span>
<span class="s1">'</span><span class="s">'</span><span class="s1">'</span>

<span class="s">@description('The name of the Azure Policy')</span>
<span class="s">param name string</span>

<span class="err">@</span><span class="s">description('Add the tenant id provided by the MSP')</span>
<span class="s">param managedByTenantId string</span>

<span class="err">@</span><span class="s">description('the name of the MSP offering')</span>
<span class="s">param mspOfferName string</span>

<span class="err">@</span><span class="s">description('The description of the managing party offer')</span>
<span class="s">param mspOfferDescription string</span>

<span class="err">@</span><span class="s">description('Specify an array of objects, containing tuples of principalId, a roleDefinitionId, and an optional principalIdDisplayName. The roleDefinition specified is granted to the principalId in the provider\'s Entra and the principalIdDisplayName is visible to customers.')</span>
<span class="s">param authorizations array</span> 

<span class="err">@</span><span class="s">description('The ID of the Owner Role')</span>
<span class="s">var rbacOwner = '8e3af657-a8ff-443c-a75c-2fe8c4bcb635'</span>

<span class="s">resource policyDefinition 'Microsoft.Authorization/policyDefinitions@2023-04-01' = {</span>
  <span class="s">name</span><span class="err">:</span> <span class="s1">'</span><span class="s">def-${name}'</span>
  <span class="na">properties</span><span class="pi">:</span> <span class="pi">{</span>
    <span class="nv">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Policy</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">enforce</span><span class="nv"> </span><span class="s">Lighthouse</span><span class="nv"> </span><span class="s">on</span><span class="nv"> </span><span class="s">subscriptions,</span><span class="nv"> </span><span class="s">delegating</span><span class="nv"> </span><span class="s">mgmt</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">MSP'</span>
    <span class="nv">displayName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Enforce</span><span class="nv"> </span><span class="s">Lighthouse</span><span class="nv"> </span><span class="s">on</span><span class="nv"> </span><span class="s">subscriptions'</span>
    <span class="nv">mode</span><span class="pi">:</span> <span class="s1">'</span><span class="s">All'</span>
    <span class="nv">policyType</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Custom'</span>
    <span class="nv">parameters</span><span class="pi">:</span> <span class="pi">{</span>
      <span class="nv">managedByTenantId</span><span class="pi">:</span> <span class="pi">{</span>
        <span class="nv">type</span><span class="pi">:</span> <span class="s1">'</span><span class="s">string'</span>
        <span class="nv">defaultValue</span><span class="pi">:</span> <span class="nv">managedByTenantId</span>
        <span class="nv">metadata</span><span class="pi">:</span> <span class="pi">{</span>
          <span class="nv">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Add</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">tenant</span><span class="nv"> </span><span class="s">id</span><span class="nv"> </span><span class="s">provided</span><span class="nv"> </span><span class="s">by</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">MSP'</span>
        <span class="pi">}</span>
      <span class="pi">}</span>
      <span class="nv">mspOfferName</span><span class="pi">:</span> <span class="pi">{</span>
        <span class="nv">type</span><span class="pi">:</span> <span class="s1">'</span><span class="s">string'</span>
        <span class="nv">defaultValue</span><span class="pi">:</span> <span class="nv">mspOfferName</span>
        <span class="nv">metadata</span><span class="pi">:</span> <span class="pi">{</span>
          <span class="nv">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Add</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">tenant</span><span class="nv"> </span><span class="s">name</span><span class="nv"> </span><span class="s">of</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">provided</span><span class="nv"> </span><span class="s">MSP'</span>
        <span class="pi">}</span>
      <span class="pi">}</span>
      <span class="nv">mspOfferDescription</span><span class="pi">:</span> <span class="pi">{</span>
        <span class="nv">type</span><span class="pi">:</span> <span class="s1">'</span><span class="s">string'</span>
        <span class="nv">defaultValue</span><span class="pi">:</span> <span class="nv">mspOfferDescription</span>
        <span class="nv">metadata</span><span class="pi">:</span> <span class="pi">{</span>
          <span class="nv">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Add</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">description</span><span class="nv"> </span><span class="s">of</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">offer</span><span class="nv"> </span><span class="s">provided</span><span class="nv"> </span><span class="s">by</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">MSP'</span>
        <span class="pi">}</span>
      <span class="pi">}</span>
      <span class="nv">managedByAuthorizations</span><span class="pi">:</span> <span class="pi">{</span>
        <span class="nv">type</span><span class="pi">:</span> <span class="s1">'</span><span class="s">array'</span>
        <span class="nv">defaultValue</span><span class="pi">:</span> <span class="nv">authorizations</span>
        <span class="nv">metadata</span><span class="pi">:</span> <span class="pi">{</span>
          <span class="nv">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Add</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">authZ</span><span class="nv"> </span><span class="s">array</span><span class="nv"> </span><span class="s">provided</span><span class="nv"> </span><span class="s">by</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">MSP'</span>
        <span class="pi">}</span>
      <span class="pi">}</span>
    <span class="pi">}</span>
    <span class="nv">policyRule</span><span class="pi">:</span> <span class="pi">{</span>
      <span class="nv">if</span><span class="pi">:</span> <span class="pi">{</span>
        <span class="nv">allOf</span><span class="pi">:</span> <span class="pi">[</span>
          <span class="pi">{</span>
            <span class="nv">field</span><span class="pi">:</span> <span class="s1">'</span><span class="s">type'</span>
            <span class="nv">equals</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Microsoft.Resources/subscriptions'</span>
          <span class="pi">}</span>
        <span class="pi">]</span>
      <span class="pi">}</span>
      <span class="nv">then</span><span class="pi">:</span> <span class="pi">{</span>
        <span class="nv">effect</span><span class="pi">:</span> <span class="s1">'</span><span class="s">deployIfNotExists'</span>
        <span class="nv">details</span><span class="pi">:</span> <span class="pi">{</span>
          <span class="nv">type</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Microsoft.ManagedServices/registrationDefinitions'</span>
          <span class="nv">deploymentScope</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Subscription'</span>
          <span class="nv">existenceScope</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Subscription'</span>
          <span class="nv">roleDefinitionIds</span><span class="pi">:</span> <span class="pi">[</span>
            <span class="s1">'</span><span class="s">/providers/Microsoft.Authorization/roleDefinitions/${rbacOwner}'</span>
          <span class="pi">]</span>
          <span class="nv">existenceCondition</span><span class="pi">:</span> <span class="pi">{</span>
            <span class="nv">allOf</span><span class="pi">:</span> <span class="pi">[</span>
              <span class="pi">{</span>
                <span class="nv">field</span><span class="pi">:</span> <span class="s1">'</span><span class="s">type'</span>
                <span class="nv">equals</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Microsoft.ManagedServices/registrationDefinitions'</span>
              <span class="pi">}</span>
              <span class="pi">{</span>
                <span class="nv">field</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Microsoft.ManagedServices/registrationDefinitions/managedByTenantId'</span>
                <span class="nv">equals</span><span class="pi">:</span> <span class="s1">'</span><span class="s">[parameters(\'</span><span class="nv">managedByTenantId\')</span><span class="err">]</span><span class="s1">'</span>
              <span class="s">}</span>
            <span class="s">]</span>
          <span class="s">}</span>
          <span class="s">deployment:</span><span class="nv"> </span><span class="s">{</span>
            <span class="s">location:</span><span class="nv"> </span><span class="s">'</span><span class="nv">westeurope'</span>
            <span class="nv">properties</span><span class="pi">:</span> <span class="pi">{</span>
              <span class="nv">mode</span><span class="pi">:</span> <span class="s1">'</span><span class="s">incremental'</span>
              <span class="nv">parameters</span><span class="pi">:</span> <span class="pi">{</span>
                <span class="nv">managedByTenantId</span><span class="pi">:</span> <span class="pi">{</span>
                  <span class="nv">value</span><span class="pi">:</span> <span class="s1">'</span><span class="s">[parameters(\'</span><span class="nv">managedByTenantId\')</span><span class="err">]</span><span class="s1">'</span>
                <span class="s">}</span>
                <span class="s">mspOfferName:</span><span class="nv"> </span><span class="s">{</span>
                  <span class="s">value:</span><span class="nv"> </span><span class="s">'</span><span class="pi">[</span><span class="nv">parameters(\'mspOfferName\')</span><span class="pi">]</span><span class="s1">'</span>
                <span class="s">}</span>
                <span class="s">mspOfferDescription:</span><span class="nv"> </span><span class="s">{</span>
                  <span class="s">value:</span><span class="nv"> </span><span class="s">'</span><span class="pi">[</span><span class="nv">parameters(\'mspOfferDescription\')</span><span class="pi">]</span><span class="s1">'</span>
                <span class="s">}</span>
                <span class="s">managedByAuthorizations:</span><span class="nv"> </span><span class="s">{</span>
                  <span class="s">value:</span><span class="nv"> </span><span class="s">'</span><span class="pi">[</span><span class="nv">parameters(\'managedByAuthorizations\')</span><span class="pi">]</span><span class="s1">'</span>
                <span class="s">}</span>
              <span class="s">}</span>
              <span class="s">template:</span><span class="nv"> </span><span class="s">{</span>
                <span class="s">'</span><span class="nv">$schema'</span><span class="pi">:</span> <span class="s1">'</span><span class="s">https://schema.management.azure.com/2018-05-01/subscriptionDeploymentTemplate.json#'</span>
                <span class="nv">contentVersion</span><span class="pi">:</span> <span class="s1">'</span><span class="s">1.0.0.0'</span>
                <span class="nv">parameters</span><span class="pi">:</span> <span class="pi">{</span>
                  <span class="nv">managedByTenantId</span><span class="pi">:</span> <span class="pi">{</span>
                    <span class="nv">type</span><span class="pi">:</span> <span class="s1">'</span><span class="s">string'</span>
                  <span class="pi">}</span>
                  <span class="nv">mspOfferName</span><span class="pi">:</span> <span class="pi">{</span>
                    <span class="nv">type</span><span class="pi">:</span> <span class="s1">'</span><span class="s">string'</span>
                  <span class="pi">}</span>
                  <span class="nv">mspOfferDescription</span><span class="pi">:</span> <span class="pi">{</span>
                    <span class="nv">type</span><span class="pi">:</span> <span class="s1">'</span><span class="s">string'</span>
                  <span class="pi">}</span>
                  <span class="nv">managedByAuthorizations</span><span class="pi">:</span> <span class="pi">{</span>
                    <span class="nv">type</span><span class="pi">:</span> <span class="s1">'</span><span class="s">array'</span>
                  <span class="pi">}</span>
                <span class="pi">}</span>
                <span class="nv">variables</span><span class="pi">:</span> <span class="pi">{</span>
                  <span class="nv">managedByRegistrationName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">[guid(parameters(\'</span><span class="nv">mspOfferName\'))</span><span class="err">]</span><span class="s1">'</span>
                  <span class="s">managedByAssignmentName:</span><span class="nv"> </span><span class="s">'</span><span class="pi">[</span><span class="nv">guid(parameters(\'mspOfferName\'))</span><span class="pi">]</span><span class="s1">'</span>
                <span class="s">}</span>
                <span class="s">resources:</span><span class="nv"> </span><span class="s">[</span>
                  <span class="s">{</span>
                    <span class="s">type:</span><span class="nv"> </span><span class="s">'</span><span class="nv">Microsoft.ManagedServices/registrationDefinitions'</span>
                    <span class="nv">apiVersion</span><span class="pi">:</span> <span class="s1">'</span><span class="s">2019-06-01'</span>
                    <span class="nv">name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">[variables(\'</span><span class="nv">managedByRegistrationName\')</span><span class="err">]</span><span class="s1">'</span>
                    <span class="s">properties:</span><span class="nv"> </span><span class="s">{</span>
                      <span class="s">registrationDefinitionName:</span><span class="nv"> </span><span class="s">'</span><span class="pi">[</span><span class="nv">parameters(\'mspOfferName\')</span><span class="pi">]</span><span class="s1">'</span>
                      <span class="s">description:</span><span class="nv"> </span><span class="s">'</span><span class="pi">[</span><span class="nv">parameters(\'mspOfferDescription\')</span><span class="pi">]</span><span class="s1">'</span>
                      <span class="s">managedByTenantId:</span><span class="nv"> </span><span class="s">'</span><span class="pi">[</span><span class="nv">parameters(\'managedByTenantId\')</span><span class="pi">]</span><span class="s1">'</span>
                      <span class="s">authorizations:</span><span class="nv"> </span><span class="s">'</span><span class="pi">[</span><span class="nv">parameters(\'managedByAuthorizations\')</span><span class="pi">]</span><span class="s1">'</span>
                    <span class="s">}</span>
                  <span class="s">}</span>
                  <span class="s">{</span>
                    <span class="s">type:</span><span class="nv"> </span><span class="s">'</span><span class="nv">Microsoft.ManagedServices/registrationAssignments'</span>
                    <span class="nv">apiVersion</span><span class="pi">:</span> <span class="s1">'</span><span class="s">2019-06-01'</span>
                    <span class="nv">name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">[variables(\'</span><span class="nv">managedByAssignmentName\')</span><span class="err">]</span><span class="s1">'</span>
                    <span class="s">dependsOn:</span><span class="nv"> </span><span class="s">[</span>
                      <span class="s">'</span><span class="pi">[</span><span class="nv">resourceId(\'Microsoft.ManagedServices/registrationDefinitions/\'</span><span class="pi">,</span> <span class="nv">variables(\'managedByRegistrationName\'))</span><span class="pi">]</span><span class="s1">'</span>
                    <span class="s">]</span>
                    <span class="s">properties:</span><span class="nv"> </span><span class="s">{</span>
                      <span class="s">registrationDefinitionId:</span><span class="nv"> </span><span class="s">'</span><span class="pi">[</span><span class="nv">resourceId(\'Microsoft.ManagedServices/registrationDefinitions/\'</span><span class="pi">,</span><span class="nv">variables(\'managedByRegistrationName\'))</span><span class="pi">]</span><span class="s1">'</span>
                    <span class="s">}</span>
                  <span class="s">}</span>
                <span class="s">]</span>
              <span class="s">}</span>
            <span class="s">}</span>
          <span class="s">}</span>
        <span class="s">}</span>
      <span class="s">}</span>
    <span class="s">}</span>
  <span class="s">}</span>
<span class="s">}</span>

<span class="s">output</span><span class="nv"> </span><span class="s">mspOfferName</span><span class="nv"> </span><span class="s">string</span><span class="nv"> </span><span class="s">=</span><span class="nv"> </span><span class="s">'</span><span class="nv">Managed by $</span><span class="pi">{</span><span class="nv">mspOfferName</span><span class="pi">}</span><span class="s1">'</span>
<span class="s">output</span><span class="nv"> </span><span class="s">authorizations</span><span class="nv"> </span><span class="s">array</span><span class="nv"> </span><span class="s">=</span><span class="nv"> </span><span class="s">authorizations</span>
<span class="s">output</span><span class="nv"> </span><span class="s">policyDefinitionId</span><span class="nv"> </span><span class="s">string</span><span class="nv"> </span><span class="s">=</span><span class="nv"> </span><span class="s">policyDefinition.id</span>
</code></pre></div></div>

<p>In the code above, the policy definition is constructed where you can see that it has the ‘deployIfNotExist’ effect when a subscription does not have the managedByTenantId specified. The fact that it works for a subscription is set in the If condition of the policy.</p>

<p>Within the ‘deployment’ part of the policy, the ARM template is specified that will be deployed when the condition is not met. This means that it will deploy the Lighthouse connection to the subscription.</p>

<p>This module outputs the policyDefinitionId after the creation of the definition. This output value can then be used for the assignment of the policy.</p>

<h3 id="assignment">Assignment</h3>

<p>The next part to get this solution operational is the assignment. As mentioned in this article, this is the other module.</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">targetScope = 'managementGroup'</span>

<span class="s">metadata info = {</span>
  <span class="s">title</span><span class="err">:</span> <span class="s1">'</span><span class="s">Azure</span><span class="nv"> </span><span class="s">Policy</span><span class="nv"> </span><span class="s">Assignment'</span>
  <span class="na">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Module</span><span class="nv"> </span><span class="s">for</span><span class="nv"> </span><span class="s">creating</span><span class="nv"> </span><span class="s">policy</span><span class="nv"> </span><span class="s">assignments'</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">1.1.0'</span>
  <span class="na">author</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Maik</span><span class="nv"> </span><span class="s">van</span><span class="nv"> </span><span class="s">der</span><span class="nv"> </span><span class="s">Gaag'</span>
<span class="err">}</span>

<span class="s">metadata description = '''</span>
<span class="s">Module for creating policy assignments</span>
<span class="s1">'</span><span class="s">'</span><span class="s1">'</span>

<span class="s">@maxLength(10)</span>
<span class="s">@description('The name of the policyassignment')</span>
<span class="s">param name string</span>

<span class="err">@</span><span class="s">description('The description in the azure portal for the policy assignment')</span>
<span class="s">param assignmentDescription string</span>

<span class="err">@</span><span class="s">description('The location of the resources')</span>
<span class="s">param location string</span>

<span class="err">@</span><span class="s">description('The Display Name for the policy assignment')</span>
<span class="s">param assignmentDisplayname string</span>

<span class="err">@</span><span class="s">allowed([</span>
  <span class="s">'Default'</span>
  <span class="s">'DoNotEnforce'</span>
<span class="err">]</span><span class="s">)</span>
<span class="err">@</span><span class="s">description('The enforcement mode of the assignment, default is Default so the policy will get enforced. (default,DoNotEnforce )')</span>
<span class="s">param enforcementMode string = 'Default'</span>

<span class="err">@</span><span class="s">description('The messages that describe why a resource is non-compliant with the policy.')</span>
<span class="s">param nonComplianceMessage string</span>

<span class="err">@</span><span class="s">description('The ID of the policy definition or policy set definition being assigned.')</span>
<span class="s">param policyDefinitionId string</span>

<span class="err">@</span><span class="s">description('The ID of the Role that will be given to the managed identity')</span>
<span class="s">param roleDefinitionId string = '8e3af657-a8ff-443c-a75c-2fe8c4bcb635'</span>

<span class="err">@</span><span class="s">description('Deploy with identity')</span>
<span class="s">param identity bool = </span><span class="no">false</span>

<span class="err">@</span><span class="s">description('The list of resource IDs to be excluded from the policy assignment.')</span>
<span class="s">param exclusions array = []</span>

<span class="s">var identityValue = {</span>
  <span class="s">type</span><span class="err">:</span> <span class="s1">'</span><span class="s">SystemAssigned'</span>
<span class="err">}</span>

<span class="s">resource policyassignment 'Microsoft.Authorization/policyAssignments@2025-11-01' = {</span>
  <span class="s">name</span><span class="err">:</span> <span class="s1">'</span><span class="s">assignment-${name}'</span>
  <span class="na">location</span><span class="pi">:</span> <span class="s">location</span>
  <span class="na">identity</span><span class="pi">:</span> <span class="s">identityValue</span>
  <span class="na">properties</span><span class="pi">:</span> <span class="pi">{</span>
    <span class="nv">description</span><span class="pi">:</span> <span class="nv">assignmentDescription</span>
    <span class="nv">displayName</span><span class="pi">:</span> <span class="nv">assignmentDisplayname</span>
    <span class="nv">enforcementMode</span><span class="pi">:</span> <span class="nv">enforcementMode</span>
    <span class="nv">nonComplianceMessages</span><span class="pi">:</span> <span class="pi">[</span>
      <span class="pi">{</span>
        <span class="nv">message</span><span class="pi">:</span> <span class="nv">nonComplianceMessage</span>
      <span class="pi">}</span>
    <span class="pi">]</span>
    <span class="nv">policyDefinitionId</span><span class="pi">:</span> <span class="nv">policyDefinitionId</span>
    <span class="nv">notScopes</span><span class="pi">:</span> <span class="nv">exclusions</span>
  <span class="pi">}</span>
<span class="err">}</span>

<span class="s">resource roleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = if (identity) {</span>
  <span class="s">name</span><span class="err">:</span> <span class="s">roleDefinitionId</span>
<span class="err">}</span>

<span class="s">resource roleassignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = if (identity) {</span>
  <span class="s">name</span><span class="err">:</span> <span class="s">guid('policyassignment', policyassignment.name, roleDefinitionId)</span>
  <span class="s">properties</span><span class="err">:</span> <span class="pi">{</span>
    <span class="nv">principalId</span><span class="pi">:</span> <span class="nv">policyassignment.identity.principalId</span>
    <span class="nv">principalType</span><span class="pi">:</span> <span class="s1">'</span><span class="s">ServicePrincipal'</span>
    <span class="nv">roleDefinitionId</span><span class="pi">:</span> <span class="nv">roleDefinition.id</span>
  <span class="pi">}</span>
<span class="err">}</span>

<span class="s">output policyAssignmentIdentityId string = policyassignment.identity.principalId</span>

</code></pre></div></div>

<p>This module is not specific for setting up Lighthouse but can be used for assigning policies in general to the management scope in Azure. A few things to note here are:</p>

<ul>
  <li>The assignment can use an Identity. When it uses an Identity, it will use the SystemAssigned identity type. The role it needs is specified in the roleDefinitionId parameter.</li>
  <li>The policy that it will assign is specified in the parameter policyDefinitionId, and it will be assigned at the scope where this module is executed.</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>Managing Azure resources within multiple tenants is a challenging task. Using Azure Lighthouse simplifies this job in several ways. By using the solution described in this post, it may help you in situations where you are unsure if additional subscriptions will be added to the environment and where you do not want to lose control.</p>

<p>This way, you only have to onboard a tenant once and subscriptions will turn up when they are added.</p>]]></content><author><name>Maik van der Gaag</name></author><category term="Azure" /><category term="Tags" /><category term="Azure" /><category term="Policy" /><category term="Lighthouse" /><summary type="html"><![CDATA[A lot of organizations manage multiple tenants. This can be the case for managed service providers but also for enterprises. For these cases, you would like to have delegated resource access to be able to manage those tenants.]]></summary></entry><entry><title type="html">A Festive Tale - The Mystery of Hidden Tags</title><link href="https://msftplayground.com/2025/12/a-festive-tale-the-mystery-of-hidden-tags" rel="alternate" type="text/html" title="A Festive Tale - The Mystery of Hidden Tags" /><published>2025-12-25T01:00:00+01:00</published><updated>2025-12-25T01:00:00+01:00</updated><id>https://msftplayground.com/2025/12/a-festive-tale-the-mystery-of-hidden-tags</id><content type="html" xml:base="https://msftplayground.com/2025/12/a-festive-tale-the-mystery-of-hidden-tags"><![CDATA[<p>In the world of cloud management, every detail matters—but what happens when a crucial tag goes missing? Azure Tags are designed to bring order and clarity, yet sometimes they vanish into the shadows, leaving teams puzzled. This article uncovers the secrets behind hidden tags, why they matter, and how to ensure they never slip through the cracks.</p>

<p>It might not be the exact scenario, but it certainly sounds like an intriguing story. Before diving into the mystery, let’s start by exploring what Azure Tags are.</p>

<p>Tags in the Azure platform are key-value pairs that let you store descriptive information about your resources. For example, when deploying resources to Azure, you may want to save the resource owner.</p>

<p>If you check the snippet below, you’ll see that we add specific tags to a resource, such as owner, environment, purpose, version, and clean. This small snippet shows how this could be done via Bicep, but as you know, it is also possible through the Azure portal.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">@</span><span class="s">description('Solutions to add to workspace')</span>
<span class="s">param solutions array = []</span>

<span class="err">@</span><span class="s">description('Tags to apply to the resource')</span>
<span class="s">param tags object = {</span>
  <span class="s">owner</span><span class="err">:</span> <span class="s1">'</span><span class="s">maikvandergaag'</span>
  <span class="na">environment</span><span class="pi">:</span> <span class="s">env</span>
  <span class="na">purpose</span><span class="pi">:</span> <span class="s1">'</span><span class="s">monitoring'</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s1">'</span><span class="s">1.0.0'</span>
  <span class="na">clean</span><span class="pi">:</span> <span class="s1">'</span><span class="s">false'</span>
<span class="err">}</span>

<span class="s">resource logAnalyticsWorkspace 'Microsoft.OperationalInsights/workspaces@2025-02-01' = {</span>
  <span class="s">name</span><span class="err">:</span> <span class="s1">'</span><span class="s">la-${name}-${env}'</span>
  <span class="na">location</span><span class="pi">:</span> <span class="s">location</span>
  <span class="na">tags</span><span class="pi">:</span> <span class="s">tags</span>
  <span class="na">properties</span><span class="pi">:</span> <span class="pi">{</span>
    <span class="nv">sku</span><span class="pi">:</span> <span class="pi">{</span>
      <span class="nv">name</span><span class="pi">:</span> <span class="nv">sku</span>
    <span class="pi">}</span>
    <span class="nv">retentionInDays</span><span class="pi">:</span> <span class="nv">retentionInDays</span>
  <span class="pi">}</span>
<span class="err">}</span>
</code></pre></div></div>
<p>Tags that you want to apply can be applied to Azure resources, resource groups, and subscriptions. Management groups are not supported.</p>

<p>If you are looking for a strategy on how to deal with tagging, make sure to check out this <a href="https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/azure-best-practices/resource-naming?toc=%2Fazure%2Fazure-resource-manager%2Fmanagement%2Ftoc.json&amp;WT.mc_id=AZ-MVP-5004255">article</a>.</p>

<p>Managing tags in large environments manually can become quite intensive. That is why there is support for Azure Policies. With the help of Azure Policies, you can overcome most of the hassles. For example by using policies the eco system can help you with:</p>

<ul>
  <li>Inheriting tags from the resource group for the resources in the group.</li>
  <li>Automatically adding tags to resource groups or resources.</li>
  <li>Making sure that supported resources have a tag assigned.</li>
</ul>

<p>Information about the tagging policies can be found <a href="https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/tag-policies">here</a></p>

<h2 id="hidden-tags">Hidden Tags</h2>

<p>Now to the mystery of this festive tale. As you may know, all tags you add to Azure via the API, PowerShell, Bicep, or the Portal are displayed in the web interface.</p>

<p><img src="/assets/images/2025/tags-portal.png" alt="Tags Azure Portal" /></p>

<p>Try creating a new tag in the portal and prefix it with ‘hidden-‘. The tag will be made, but as the mystery reveals, it disappears from the portal.</p>

<p>With this capability, you can build tag-based automations and features that rely on tags but do not want to display those values in plain sight, so people are less likely to change them. You could use this in scenarios like:</p>

<ul>
  <li>Automation for starting and stopping VMs</li>
  <li>Cost analysis based on specific tag data</li>
</ul>

<p>Azure uses hidden tags itself, and for some people, this function remains a mystery. For example, add a tag called ‘hidden-title’ to a specific resource and see what happens. The screenshots below show you what happened in my environment:</p>

<p><em>Before</em>
<img src="/assets/images/2025/before.png" alt="Before hidden tag" /></p>

<p><em>During</em>
<img src="/assets/images/2025/during.png" alt="During hidden tag" /></p>

<p><em>After</em>
<img src="/assets/images/2025/after.png" alt="During hidden tag" /></p>

<p>Notice that basically nothing changed. We added the tag, and it isn’t displayed. But now move to the resources list view, for example, the resource group in which you created the tag for the resource.</p>

<p><em>Resource List</em>
<img src="/assets/images/2025/resource-list.png" alt="list view with display name" /></p>

<p>The title now appears in the resources list and lets you add a descriptive title to all your resources.</p>

<h2 id="automation">Automation</h2>
<p>The default Get-AzTag and Remove-AzTag do not retrieve the tags; how can you then use them in automation?</p>

<p>Using hidden tags for automation is done with the Get-AzResource command.</p>

<p>The snippet below, for example, gets all resources that have a tag ‘hidden-title’</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w"> </span><span class="n">Get-AzResource</span><span class="w"> </span><span class="nt">-TagName</span><span class="w"> </span><span class="s1">'hidden-title'</span><span class="w"> 
</span></code></pre></div></div>

<h2 id="removing-the-hidden-tag">Removing the hidden tag</h2>

<p>As you must have noticed in the portal, there is no way to delete the tag. But for the hidden title tag, you can remove the title by adding the same tag name with an empty value.</p>

<p>In this case, the added title will disappear, but the tag itself will remain.</p>

<p>To delete the tags completely, you need to use PowerShell or the Azure CLI. The example below shows how to delete the hidden tag using PowerShell.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$removeTags</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="s2">"hidden-title"</span><span class="o">=</span><span class="s2">""</span><span class="p">}</span><span class="w">                                                                                                                            
</span><span class="n">Update-AzTag</span><span class="w"> </span><span class="nt">-ResourceId</span><span class="w"> </span><span class="nv">$resource</span><span class="o">.</span><span class="nf">id</span><span class="w"> </span><span class="nt">-Tag</span><span class="w"> </span><span class="nv">$removeTags</span><span class="w"> </span><span class="nt">-Operation</span><span class="w"> </span><span class="nx">Delete</span><span class="w">     
</span></code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>

<p>Tags are a handy option for saving additional data with your resources. Using hidden tags can be effective, but it’s essential to know that further work is required to keep your environment clean and up to date.</p>]]></content><author><name>Maik van der Gaag</name></author><category term="Azure" /><category term="Tags" /><category term="Azure" /><summary type="html"><![CDATA[In the world of cloud management, every detail matters—but what happens when a crucial tag goes missing? Azure Tags are designed to bring order and clarity, yet sometimes they vanish into the shadows, leaving teams puzzled. This article uncovers the secrets behind hidden tags, why they matter, and how to ensure they never slip through the cracks.]]></summary></entry><entry><title type="html">Build a Custom Extension for Bicep</title><link href="https://msftplayground.com/2025/09/bicep-custom-extension" rel="alternate" type="text/html" title="Build a Custom Extension for Bicep" /><published>2025-10-08T02:00:00+02:00</published><updated>2025-10-08T02:00:00+02:00</updated><id>https://msftplayground.com/2025/09/bicep-custom-extension</id><content type="html" xml:base="https://msftplayground.com/2025/09/bicep-custom-extension"><![CDATA[<p>In a previous post about <a href="https://msftplayground.com/2025/09/bicep-extensions">Bicep Local</a>, I explained how this feature enables the creation of custom extensions that can be used directly within Bicep. Also that by using this option you are not limited to the server side implementation that Bicep now has.</p>

<p>If you’re interested in building your own extension, be sure to check out the documentation on <a href="https://github.com/Azure/bicep/blob/main/docs/experimental/local-deploy-dotnet-quickstart.md">Creating a Local Extension with .Net</a>. It’s a great starting point to help you get up and running.</p>

<h2 id="http-extension">HTTP Extension</h2>

<p>I’ve built an extension that enables performing HTTP requests directly from Bicep. With this capability, a number of powerful scenarios become possible, such as:</p>

<ul>
  <li>Updating your CMDB automatically when deploying resources to Azure.</li>
  <li>Calling external APIs to retrieve additional data required for deployments.</li>
  <li>Chaining API calls — for example, requesting a token from Microsoft Entra to authenticate a subsequent API call.</li>
</ul>

<p>The source code of the extension can be found here: <a href="https://github.com/maikvandergaag/bicep-ext-http">bicep-ext-http</a></p>

<p>The extension provides a new resource type HttpCall that can perform HTTP requests (GET, POST, PUT, DELETE, PATCH) and capture the response for use in your Bicep templates. This is useful for scenarios like I mentioned above.</p>

<h3 id="features">Features</h3>

<ul>
  <li>Multiple HTTP Methods: Supports GET, POST, PUT, DELETE, and PATCH requests</li>
  <li>Custom Headers: Add custom headers to your HTTP requests</li>
  <li>Request Body: Include JSON or other content in request bodies</li>
  <li>Response Capture: Access both the response body and HTTP status code</li>
  <li>Type Safety: Full IntelliSense support and type checking in Bicep</li>
</ul>

<h2 id="creating-your-own-extension">Creating your own extension</h2>

<p>When creating your own extension based on the article I shared earlier, there are a few important considerations and steps to keep in mind. To make this more practical, I’ll walk you through the process using the source code of my own extension as an example.</p>

<p>In my example, I followed specific coding guidelines, which is why my extension differs slightly from the one described in the article. One of the main things that I have changed is the separation of the code files.</p>

<h3 id="program">Program</h3>

<p>The core of the extension resides in ‘Program.cs’. Using dependency injection, the necessary components are loaded and the extension is assigned its name. As you can see in the snippet below I call my extension ‘bicep-ext-http’.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">Microsoft.AspNetCore.Builder</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Bicep.Local.Extension.Host.Extensions</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Microsoft.Extensions.DependencyInjection</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Bicep.Ext.Http.Handler</span><span class="p">;</span>

<span class="kt">var</span> <span class="n">builder</span> <span class="p">=</span> <span class="n">WebApplication</span><span class="p">.</span><span class="nf">CreateBuilder</span><span class="p">();</span>

<span class="n">builder</span><span class="p">.</span><span class="nf">AddBicepExtensionHost</span><span class="p">(</span><span class="n">args</span><span class="p">);</span>
<span class="n">builder</span><span class="p">.</span><span class="n">Services</span>
    <span class="p">.</span><span class="nf">AddBicepExtension</span><span class="p">(</span>
        <span class="n">name</span><span class="p">:</span> <span class="s">"bicep-ext-http"</span><span class="p">,</span>
        <span class="n">version</span><span class="p">:</span> <span class="s">"1.0.0"</span><span class="p">,</span>
        <span class="n">isSingleton</span><span class="p">:</span> <span class="k">true</span><span class="p">,</span>
        <span class="n">typeAssembly</span><span class="p">:</span> <span class="k">typeof</span><span class="p">(</span><span class="n">Program</span><span class="p">).</span><span class="n">Assembly</span><span class="p">)</span>
    <span class="p">.</span><span class="n">WithResourceHandler</span><span class="p">&lt;</span><span class="n">HttpCallHandler</span><span class="p">&gt;();</span>

<span class="kt">var</span> <span class="n">app</span> <span class="p">=</span> <span class="n">builder</span><span class="p">.</span><span class="nf">Build</span><span class="p">();</span>

<span class="n">app</span><span class="p">.</span><span class="nf">MapBicepExtension</span><span class="p">();</span>

<span class="k">await</span> <span class="n">app</span><span class="p">.</span><span class="nf">RunAsync</span><span class="p">();</span>
</code></pre></div></div>

<h3 id="deployment-identifier">Deployment Identifier</h3>

<p>To deploy a specific resource type, you first need to create an identifier for it. The snippet below shows the identifier I use for my HTTP calls, where I’ve chosen to define it with a simple name.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">namespace</span> <span class="nn">Bicep.Ext.Http.Model</span> <span class="p">{</span>
    <span class="k">public</span> <span class="k">class</span> <span class="nc">HttpCallIdentifiers</span> <span class="p">{</span>
        <span class="p">[</span><span class="nf">TypeProperty</span><span class="p">(</span><span class="s">"The Http Call Name"</span><span class="p">,</span> <span class="n">ObjectTypePropertyFlags</span><span class="p">.</span><span class="n">Identifier</span> <span class="p">|</span> <span class="n">ObjectTypePropertyFlags</span><span class="p">.</span><span class="n">Required</span><span class="p">)]</span>
        <span class="k">public</span> <span class="n">required</span> <span class="kt">string</span> <span class="n">Name</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="resource-type">Resource Type</h3>

<p>For the resource itself, you need to define a resource type. As you may know, in Bicep every deployment is carried out through a specific resource. The snippet below shows the ‘httpcall’ resource type that inherits from the Identifer.</p>

<p>This class contains a few properties that defined the resource type and are required to be able to perform a HTTP Call.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">namespace</span> <span class="nn">Bicep.Ext.Http.Model</span> <span class="p">{</span>
    <span class="p">[</span><span class="nf">ResourceType</span><span class="p">(</span><span class="s">"httpcall"</span><span class="p">)]</span>
    <span class="k">public</span> <span class="k">class</span> <span class="nc">HttpCall</span> <span class="p">:</span> <span class="n">HttpCallIdentifiers</span> <span class="p">{</span>

        <span class="p">[</span><span class="nf">TypeProperty</span><span class="p">(</span><span class="s">"The Http Call Url"</span><span class="p">,</span> <span class="n">ObjectTypePropertyFlags</span><span class="p">.</span><span class="n">Required</span><span class="p">)]</span>
        <span class="p">[</span><span class="nf">JsonPropertyName</span><span class="p">(</span><span class="s">"url"</span><span class="p">)]</span>
        <span class="k">public</span> <span class="n">required</span> <span class="kt">string</span> <span class="n">Url</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>

        <span class="p">[</span><span class="nf">TypeProperty</span><span class="p">(</span><span class="s">"The HTTP method to use"</span><span class="p">,</span> <span class="n">ObjectTypePropertyFlags</span><span class="p">.</span><span class="n">Required</span><span class="p">)]</span>
        <span class="p">[</span><span class="nf">JsonConverter</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">JsonStringEnumConverter</span><span class="p">))]</span>
        <span class="p">[</span><span class="nf">JsonPropertyName</span><span class="p">(</span><span class="s">"method"</span><span class="p">)]</span>
        <span class="k">public</span> <span class="n">Method</span><span class="p">?</span> <span class="n">Method</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>

        <span class="p">[</span><span class="nf">TypeProperty</span><span class="p">(</span><span class="s">"The body to include in the request"</span><span class="p">)]</span>
        <span class="p">[</span><span class="nf">JsonPropertyName</span><span class="p">(</span><span class="s">"body"</span><span class="p">)]</span>
        <span class="k">public</span> <span class="kt">string</span><span class="p">?</span> <span class="n">Body</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>

        <span class="p">[</span><span class="nf">TypeProperty</span><span class="p">(</span><span class="s">"The http call headers"</span><span class="p">)]</span>
        <span class="p">[</span><span class="nf">JsonPropertyName</span><span class="p">(</span><span class="s">"headers"</span><span class="p">)]</span>
        <span class="k">public</span> <span class="n">Header</span><span class="p">[]?</span> <span class="n">Headers</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>

        <span class="p">[</span><span class="nf">TypeProperty</span><span class="p">(</span><span class="s">"The http call result"</span><span class="p">)]</span>
        <span class="p">[</span><span class="nf">JsonPropertyName</span><span class="p">(</span><span class="s">"result"</span><span class="p">)]</span>
        <span class="k">public</span> <span class="kt">string</span><span class="p">?</span> <span class="n">Result</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>

        <span class="p">[</span><span class="nf">TypeProperty</span><span class="p">(</span><span class="s">"The http call status code"</span><span class="p">)]</span>
        <span class="p">[</span><span class="nf">JsonPropertyName</span><span class="p">(</span><span class="s">"statuscode"</span><span class="p">)]</span>
        <span class="k">public</span> <span class="kt">int</span> <span class="n">StatusCode</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>There are a few important details about this resource type. First, the Method property is defined as an enum (see below). This ensures that only the supported options can be used, preventing invalid inputs and adding the ability to choose a value when defining it in Bicep.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">namespace</span> <span class="nn">Bicep.Ext.Http.Model</span> <span class="p">{</span>
    <span class="k">public</span> <span class="k">enum</span> <span class="n">Method</span> <span class="p">{</span>
        <span class="n">Get</span><span class="p">,</span>
        <span class="n">Post</span><span class="p">,</span>
        <span class="n">Put</span><span class="p">,</span>
        <span class="n">Delete</span><span class="p">,</span>
        <span class="n">Patch</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Secondly, the ‘Headers’ property is not defined as a dictionary. Initially, I chose to use a dictionary, but I discovered that the extension model does not support dictionaries. To work around this, I created a custom type called Header and defined Headers as an array property instead. The code below shows this ‘Header’ type.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">namespace</span> <span class="nn">Bicep.Ext.Http.Model</span> <span class="p">{</span>
    <span class="k">public</span> <span class="k">class</span> <span class="nc">Header</span> <span class="p">{</span>
        <span class="p">[</span><span class="nf">TypeProperty</span><span class="p">(</span><span class="s">"The name."</span><span class="p">,</span> <span class="n">ObjectTypePropertyFlags</span><span class="p">.</span><span class="n">Required</span><span class="p">)]</span>
        <span class="k">public</span> <span class="kt">string</span><span class="p">?</span> <span class="n">Name</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>

        <span class="p">[</span><span class="nf">TypeProperty</span><span class="p">(</span><span class="s">"The value."</span><span class="p">,</span> <span class="n">ObjectTypePropertyFlags</span><span class="p">.</span><span class="n">Required</span><span class="p">)]</span>
        <span class="k">public</span> <span class="kt">string</span><span class="p">?</span> <span class="n">Value</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="this-is-were-the-magic-happens">This is were the magic happens</h2>

<p>The final component of your extension is the ‘Handler’. This is where the actions are executed. In my extension, the handler performs the API call and returns its response.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">namespace</span> <span class="nn">Bicep.Ext.Http.Handler</span> <span class="p">{</span>
    <span class="k">public</span> <span class="k">class</span> <span class="nc">HttpCallHandler</span> <span class="p">:</span> <span class="n">TypedResourceHandler</span><span class="p">&lt;</span><span class="n">HttpCall</span><span class="p">,</span> <span class="n">HttpCallIdentifiers</span><span class="p">&gt;</span> <span class="p">{</span>
        <span class="k">protected</span> <span class="k">override</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">ResourceResponse</span><span class="p">&gt;</span> <span class="nf">Preview</span><span class="p">(</span><span class="n">ResourceRequest</span> <span class="n">request</span><span class="p">,</span> <span class="n">CancellationToken</span> <span class="n">cancellationToken</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">await</span> <span class="n">Task</span><span class="p">.</span><span class="n">CompletedTask</span><span class="p">;</span>

            <span class="k">return</span> <span class="nf">GetResponse</span><span class="p">(</span><span class="n">request</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="k">protected</span> <span class="k">override</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">ResourceResponse</span><span class="p">&gt;</span> <span class="nf">CreateOrUpdate</span><span class="p">(</span><span class="n">ResourceRequest</span> <span class="n">request</span><span class="p">,</span> <span class="n">CancellationToken</span> <span class="n">cancellationToken</span><span class="p">)</span> <span class="p">{</span>
            <span class="k">await</span> <span class="n">Task</span><span class="p">.</span><span class="n">CompletedTask</span><span class="p">;</span>

            <span class="k">using</span> <span class="p">(</span><span class="kt">var</span> <span class="n">client</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">HttpClient</span><span class="p">())</span> <span class="p">{</span>
                <span class="kt">var</span> <span class="n">httpRequest</span> <span class="p">=</span> <span class="k">new</span> <span class="n">HttpRequestMessage</span> <span class="p">{</span>
                    <span class="n">Method</span> <span class="p">=</span> <span class="n">request</span><span class="p">.</span><span class="n">Properties</span><span class="p">.</span><span class="n">Method</span> <span class="k">switch</span> <span class="p">{</span>
                        <span class="n">Method</span><span class="p">.</span><span class="n">Get</span> <span class="p">=&gt;</span> <span class="n">HttpMethod</span><span class="p">.</span><span class="n">Get</span><span class="p">,</span>
                        <span class="n">Method</span><span class="p">.</span><span class="n">Post</span> <span class="p">=&gt;</span> <span class="n">HttpMethod</span><span class="p">.</span><span class="n">Post</span><span class="p">,</span>
                        <span class="n">Method</span><span class="p">.</span><span class="n">Delete</span> <span class="p">=&gt;</span> <span class="n">HttpMethod</span><span class="p">.</span><span class="n">Delete</span><span class="p">,</span>
                        <span class="n">Method</span><span class="p">.</span><span class="n">Put</span> <span class="p">=&gt;</span> <span class="n">HttpMethod</span><span class="p">.</span><span class="n">Put</span><span class="p">,</span>
                        <span class="n">Method</span><span class="p">.</span><span class="n">Patch</span> <span class="p">=&gt;</span> <span class="n">HttpMethod</span><span class="p">.</span><span class="n">Patch</span><span class="p">,</span>
                        <span class="n">_</span> <span class="p">=&gt;</span> <span class="k">throw</span> <span class="k">new</span> <span class="nf">InvalidOperationException</span><span class="p">(</span><span class="s">$"Unsupported HTTP method: </span><span class="p">{</span><span class="n">request</span><span class="p">.</span><span class="n">Properties</span><span class="p">.</span><span class="n">Method</span><span class="p">}</span><span class="s">"</span><span class="p">),</span>
                    <span class="p">},</span>
                    <span class="n">RequestUri</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Uri</span><span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="n">Properties</span><span class="p">.</span><span class="n">Url</span><span class="p">),</span>
                    <span class="n">Content</span> <span class="p">=</span> <span class="n">request</span><span class="p">.</span><span class="n">Properties</span><span class="p">.</span><span class="n">Body</span> <span class="p">!=</span> <span class="k">null</span> <span class="p">?</span> <span class="k">new</span> <span class="nf">StringContent</span><span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="n">Properties</span><span class="p">.</span><span class="n">Body</span><span class="p">)</span> <span class="p">:</span> <span class="k">null</span>
                <span class="p">};</span>

                <span class="k">if</span> <span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="n">Properties</span><span class="p">.</span><span class="n">Headers</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span>
                    <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">header</span> <span class="k">in</span> <span class="n">request</span><span class="p">.</span><span class="n">Properties</span><span class="p">.</span><span class="n">Headers</span><span class="p">)</span> <span class="p">{</span>
                        <span class="k">if</span> <span class="p">(</span><span class="n">header</span><span class="p">.</span><span class="n">Name</span> <span class="p">!=</span> <span class="k">null</span> <span class="p">&amp;&amp;</span> <span class="n">header</span><span class="p">.</span><span class="n">Value</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span>
                            <span class="k">if</span> <span class="p">(</span><span class="n">header</span><span class="p">.</span><span class="n">Name</span><span class="p">.</span><span class="nf">Equals</span><span class="p">(</span><span class="s">"Content-Type"</span><span class="p">,</span> <span class="n">StringComparison</span><span class="p">.</span><span class="n">OrdinalIgnoreCase</span><span class="p">)</span> <span class="p">&amp;&amp;</span> <span class="n">httpRequest</span><span class="p">.</span><span class="n">Content</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span> <span class="p">{</span>
                                <span class="n">httpRequest</span><span class="p">.</span><span class="n">Content</span><span class="p">.</span><span class="n">Headers</span><span class="p">.</span><span class="n">ContentType</span> <span class="p">=</span> <span class="k">new</span> <span class="n">System</span><span class="p">.</span><span class="n">Net</span><span class="p">.</span><span class="n">Http</span><span class="p">.</span><span class="n">Headers</span><span class="p">.</span><span class="nf">MediaTypeHeaderValue</span><span class="p">(</span><span class="n">header</span><span class="p">.</span><span class="n">Value</span><span class="p">);</span>
                            <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                                <span class="n">httpRequest</span><span class="p">.</span><span class="n">Headers</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">header</span><span class="p">.</span><span class="n">Name</span><span class="p">,</span> <span class="n">header</span><span class="p">.</span><span class="n">Value</span><span class="p">);</span>
                            <span class="p">}</span>
                        <span class="p">}</span>
                    <span class="p">}</span>
                <span class="p">}</span>

                <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">SendAsync</span><span class="p">(</span><span class="n">httpRequest</span><span class="p">,</span> <span class="n">cancellationToken</span><span class="p">);</span>
                <span class="n">response</span><span class="p">.</span><span class="nf">EnsureSuccessStatusCode</span><span class="p">();</span>

                <span class="n">request</span><span class="p">.</span><span class="n">Properties</span><span class="p">.</span><span class="n">StatusCode</span> <span class="p">=</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">response</span><span class="p">.</span><span class="n">StatusCode</span><span class="p">;</span>
                <span class="n">request</span><span class="p">.</span><span class="n">Properties</span><span class="p">.</span><span class="n">Result</span> <span class="p">=</span> <span class="k">await</span> <span class="n">response</span><span class="p">.</span><span class="n">Content</span><span class="p">.</span><span class="nf">ReadAsStringAsync</span><span class="p">(</span><span class="n">cancellationToken</span><span class="p">);</span>
            <span class="p">}</span>

            <span class="k">return</span> <span class="nf">GetResponse</span><span class="p">(</span><span class="n">request</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="k">protected</span> <span class="k">override</span> <span class="n">HttpCallIdentifiers</span> <span class="nf">GetIdentifiers</span><span class="p">(</span><span class="n">HttpCall</span> <span class="n">properties</span><span class="p">)</span>
            <span class="p">=&gt;</span> <span class="k">new</span><span class="p">()</span> <span class="p">{</span>
                <span class="n">Name</span> <span class="p">=</span> <span class="n">properties</span><span class="p">.</span><span class="n">Name</span><span class="p">,</span>
            <span class="p">};</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In my handler, there are no specific preview capabilities, which is why this method is left for a clear implementation. The ‘CreateOrUpdate’ method is the part that performs the API call and returns the ‘StatusCode’ as a output and the result of the call as a string.</p>

<h2 id="getting-it-to-work">Getting it to work</h2>

<p>With all the code in place, the extension is ready to be built. To do this, open a terminal and run the following commands:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet publish <span class="nt">--configuration</span> release <span class="nt">-r</span> osx-arm64 <span class="nb">.</span>
dotnet publish <span class="nt">--configuration</span> release <span class="nt">-r</span> linux-x64 <span class="nb">.</span>
dotnet publish <span class="nt">--configuration</span> release <span class="nt">-r</span> win-x64 <span class="nb">.</span>

bicep publish-extension <span class="nt">--bin-osx-arm64</span> ./bin/release/osx-arm64/publish/bicep-ext-http <span class="nt">--bin-linux-x64</span> ./bin/release/linux-x64/publish/bicep-ext-http <span class="nt">--bin-win-x64</span> ./bin/release/win-x64/publish/bicep-ext-http.exe <span class="nt">--target</span> ./bin/bicep-ext-http <span class="nt">--force</span>
</code></pre></div></div>

<p>Next step is to update the ‘bicepconfig.json’ file to be able to use the extension.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"experimentalFeaturesEnabled"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"localDeploy"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
    </span><span class="nl">"extensibility"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"extensions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"http"</span><span class="p">:</span><span class="w"> </span><span class="s2">"./extension-publish/bicep-ext-http"</span><span class="w">
  </span><span class="p">},</span><span class="w">
  </span><span class="nl">"implicitExtensions"</span><span class="p">:</span><span class="w"> </span><span class="p">[]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Now your defined resource type can be used in bicep.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">targetScope = 'local'</span>
<span class="s">extension http</span>

<span class="s">var callBody = loadTextContent('sample/body.json')</span>

<span class="s">resource getHttp 'httpcall' = {</span>
  <span class="s">name</span><span class="err">:</span> <span class="s1">'</span><span class="s">getcall'</span>
  <span class="na">url</span><span class="pi">:</span> <span class="s1">'</span><span class="s">https://bicep-local.free.beeceptor.com'</span>
  <span class="na">method</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Get'</span>
<span class="err">}</span>

<span class="s">resource postHttp 'httpcall' = {</span>
  <span class="s">name</span><span class="err">:</span> <span class="s1">'</span><span class="s">postcall'</span>
  <span class="na">url</span><span class="pi">:</span> <span class="s1">'</span><span class="s">https://bicep-local.free.beeceptor.com'</span>
  <span class="na">method</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Post'</span>
  <span class="s">headers:[</span>
    <span class="s">{name</span><span class="err">:</span> <span class="s1">'</span><span class="s">Content-Type'</span><span class="err">,</span> <span class="na">value</span><span class="pi">:</span> <span class="s1">'</span><span class="s">application/json'</span> <span class="err">}</span>
  <span class="err">]</span>
  <span class="na">body</span><span class="pi">:</span> <span class="s">callBody</span>
<span class="err">}</span>

<span class="s">resource deleteHttp 'httpcall' = {</span>
  <span class="s">name</span><span class="err">:</span> <span class="s1">'</span><span class="s">deletecall'</span>
  <span class="na">url</span><span class="pi">:</span> <span class="s1">'</span><span class="s">https://bicep-local.free.beeceptor.com/1'</span>
  <span class="na">method</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Delete'</span>
<span class="err">}</span>

<span class="s">output getResponse object = json(getHttp.result)</span>
<span class="s">output getStatusCode int = getHttp.statusCode</span>
<span class="s">output postResponse object = json(postHttp.result)</span>
<span class="s">output postStatusCode int = postHttp.statusCode</span>
<span class="s">output deleteStatusCode int = deleteHttp.statusCode</span>
</code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>

<p>Creating your own extension isn’t difficult and can add powerful capabilities to your infrastructure-as-code workflow.</p>

<p>It’s also worth noting that, at the time of writing, the Bicep team is working on a new deploy command. This will allow Bicep to perform deployments directly, without relying on the Azure CLI or PowerShell, and will also support local deployments. To stay updated on this development, check out this <a href="https://github.com/Azure/bicep/issues/17949">issue</a>.</p>

<p>If you’re looking for extensions, be sure to check out the community-maintained repository. If you’d like to contribute your own extension, don’t hesitate to submit a pull request.</p>

<ul>
  <li><a href="https://msftplayground.com/bicep-extensions">Bicep Extension Repository</a></li>
</ul>]]></content><author><name>Maik van der Gaag</name></author><category term="IaC" /><category term="Bicep" /><category term="Local" /><category term="Extensions" /><category term="Http" /><summary type="html"><![CDATA[In a previous post about Bicep Local, I explained how this feature enables the creation of custom extensions that can be used directly within Bicep. Also that by using this option you are not limited to the server side implementation that Bicep now has.]]></summary></entry><entry><title type="html">Bicep pattern parameter</title><link href="https://msftplayground.com/2025/09/bicep-pattern-parameter" rel="alternate" type="text/html" title="Bicep pattern parameter" /><published>2025-09-26T02:00:00+02:00</published><updated>2025-09-26T02:00:00+02:00</updated><id>https://msftplayground.com/2025/09/bicep-pattern</id><content type="html" xml:base="https://msftplayground.com/2025/09/bicep-pattern-parameter"><![CDATA[<p>On numerous occasions, I observed that this was not widely known; however, when working with the Bicep CLI, several commands (such as build or lint) support a –pattern parameter.</p>

<p>With this parameter, you can target multiple files by specifying a specific file-matching pattern. Some use cases to use this are:</p>

<ul>
  <li>Bulk building Bicep files.</li>
  <li>Validating all the Bicep files (lint) in a repository. (modules)</li>
</ul>

<p>As mentioned, the option is used to specify file-matching patterns, allowing you to target multiple files in one command without manually listing them all. The parameter accepts standard patterns (similar to those used in shells or file systems). This makes it very useful for automating builds or validations across a whole repository of Bicep templates.</p>

<h2 id="pattern">Pattern</h2>

<p>The pattern is a simple string-matching syntax used to select files and folders based on wildcards. In the Bicep CLI, patterns let you target multiple files without listing them individually.</p>

<ul>
  <li>&amp;ast; → matches any number of characters (except /).</li>
  <li>** → matches any number of subdirectories.</li>
</ul>

<h2 id="examples">Examples</h2>

<p>Validate all bicep files found in all subfolders.</p>

<pre><code class="language-bicep">bicep lint --pattern  "./**/*.bicep
</code></pre>

<p>Build all bicep files in the current directory.</p>

<pre><code class="language-bicep">bicep build --pattern  "./*.bicep
</code></pre>]]></content><author><name>Maik van der Gaag</name></author><category term="IaC" /><category term="Bicep" /><category term="Parameter" /><category term="pattern" /><summary type="html"><![CDATA[On numerous occasions, I observed that this was not widely known; however, when working with the Bicep CLI, several commands (such as build or lint) support a –pattern parameter.]]></summary></entry><entry><title type="html">How to Authenticate with the GitHub API Using a GitHub App</title><link href="https://msftplayground.com/2025/09/github-app-authentication" rel="alternate" type="text/html" title="How to Authenticate with the GitHub API Using a GitHub App" /><published>2025-09-15T02:00:00+02:00</published><updated>2025-09-15T02:00:00+02:00</updated><id>https://msftplayground.com/2025/09/github-app-authentication</id><content type="html" xml:base="https://msftplayground.com/2025/09/github-app-authentication"><![CDATA[<p>When working with GitHub and its APIs, authentication plays a crucial role in ensuring secure and controlled access to repositories, workflows, and organizational data.</p>

<p>While many developers are familiar with classic options like personal access tokens or OAuth apps, GitHub Apps introduce a more modern and scalable approach. Depending on your use case—whether it’s automating workflows, integrating with third-party services, or managing resources at scale—you can choose between several authentication methods. In this post, we’ll explore the available options for authenticating against GitHub’s APIs, with a focus on how GitHub App authentication works from C#.</p>

<h2 id="creating-an-app">Creating an App</h2>

<p>To get started with App authentication, you, of course, need a GitHub App. Take the following steps to create your App.</p>

<ol>
  <li>Go to your organization settings page within GitHub https://github.com/[organisation]/settings/</li>
  <li>Click on “GitHub Apps” and then on “New GitHub App”.</li>
</ol>

<p><img src="/assets/images/2025/github-app.png" alt="GitHub Apps" /></p>

<ol start="3">
  <li>On the “Register new GitHub App” page, fill in all the information that is required for your application.</li>
</ol>

<p>Next to the general information about the application, the GitHub App can have additional capabilities.</p>

<p><strong>Identifying and Authorizing users</strong></p>

<p>A capability to let your GitHub App perform actions on behalf of a user, like creating an issue, posting a comment, or making a deployment.</p>

<p><strong>Post Installation</strong></p>

<p>A GitHub App can have specific configurations based on the installation and can also redirect people to a particular page when you update the App within a Repository.</p>

<p><strong>Webhook</strong></p>

<p>Webhooks enable your GitHub App to receive real-time notifications when events happen on GitHub.</p>

<p>After providing information about the App and its capabilities, you also need to configure the permissions. In this section, you must clearly specify the permissions the App requires to perform its actions. For this article, we will create an App that registers issues, so we choose “Repository” - “Issues” - “Read and Write”.</p>

<p><img src="/assets/images/2025/github-app-permissions.png" alt="GitHub App Permissions" /></p>

<p>Also, make sure that you set the App access. If you want other people to be able to use your application, you have to set it to “Any Account”.</p>

<p>When done, click “Create GitHub App”</p>

<ol start="4">
  <li>When all the information is supplied correctly, you will be directed to the page of your GitHub App where you can provide additional information, like an image for your App. From this page, make sure to copy the App ID and generate a Private Key that we will use during the authentication.</li>
</ol>

<p><img src="/assets/images/2025/github-app-private-key.png" alt="GitHub App Private Key" /></p>

<p>Download this key, as it will be used for app authentication. As you may have noticed, this page also offers the capability to configure allowed IP addresses for using the App.</p>

<p>With the application in place, we can begin the authentication process. Authenticating with the App is done in several steps:</p>

<ol>
  <li>Generate a JWT App Token.</li>
  <li>Exchange the App Token for an Installation access token.</li>
  <li>Use the installation access token to call the API’s.</li>
</ol>

<h2 id="generate-a-jwt-token">Generate a JWT Token</h2>

<p>To generate the JWT token, we need the private key and the appId retrieved from the GitHub App page.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">void</span> <span class="nf">GenerateJwtToken</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">jwtSecurityTokenHandler</span> <span class="p">=</span> <span class="k">new</span> <span class="n">JwtSecurityTokenHandler</span> <span class="p">{</span> <span class="n">SetDefaultTimesOnTokenCreation</span> <span class="p">=</span> <span class="k">false</span> <span class="p">};</span>
    <span class="kt">var</span> <span class="n">rsa</span> <span class="p">=</span> <span class="n">RSA</span><span class="p">.</span><span class="nf">Create</span><span class="p">();</span>
    <span class="n">rsa</span><span class="p">.</span><span class="nf">ImportFromPem</span><span class="p">(</span><span class="n">_privateKeyPem</span><span class="p">.</span><span class="nf">ToCharArray</span><span class="p">());</span>
    <span class="kt">var</span> <span class="n">securityKey</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">RsaSecurityKey</span><span class="p">(</span><span class="n">rsa</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">credentials</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">SigningCredentials</span><span class="p">(</span><span class="n">securityKey</span><span class="p">,</span> <span class="n">SecurityAlgorithms</span><span class="p">.</span><span class="n">RsaSha256</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">now</span> <span class="p">=</span> <span class="n">DateTime</span><span class="p">.</span><span class="n">UtcNow</span><span class="p">.</span><span class="nf">AddSeconds</span><span class="p">(-</span><span class="m">60</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">token</span> <span class="p">=</span> <span class="n">jwtSecurityTokenHandler</span><span class="p">.</span><span class="nf">CreateToken</span><span class="p">(</span><span class="k">new</span> <span class="n">SecurityTokenDescriptor</span> <span class="p">{</span>
        <span class="n">Issuer</span> <span class="p">=</span> <span class="n">_appId</span><span class="p">,</span>
        <span class="n">Expires</span> <span class="p">=</span> <span class="n">now</span><span class="p">.</span><span class="nf">AddMinutes</span><span class="p">(</span><span class="m">10</span><span class="p">),</span>
        <span class="n">IssuedAt</span> <span class="p">=</span> <span class="n">now</span><span class="p">,</span>
        <span class="n">SigningCredentials</span> <span class="p">=</span> <span class="n">credentials</span>
    <span class="p">});</span>
    <span class="n">_jwt</span> <span class="p">=</span> <span class="n">jwtSecurityTokenHandler</span><span class="p">.</span><span class="nf">WriteToken</span><span class="p">(</span><span class="n">token</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>
<p>The above snippet shows a function that takes the Content of the private key as a string and generates a token that expires in 10 minutes.</p>

<p>This token can then be used to get a specific installation token.</p>

<h2 id="exchange-the-app-token-for-an-installation-access-token">Exchange the App Token for an Installation access token</h2>

<p>To exchange the App Token for an installation access token, call the following API: “https://api.github.com/app/installations”.</p>

<p>This API retrieves the installations of the applications, and contains information that is required to be abble to get a installation access token. The information that we need is the URL to request a access token. As you can see in the example below, this is the “access_token_url”.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="w">
  </span><span class="p">{</span><span class="w">
    </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">00000</span><span class="p">,</span><span class="w">
    </span><span class="nl">"client_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"-----"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"account"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"login"</span><span class="p">:</span><span class="w"> </span><span class="s2">"msftplayground"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">49311642</span><span class="p">,</span><span class="w">
      </span><span class="nl">"node_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"------"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"avatar_url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://avatars.githubusercontent.com/u/49311642?v=4"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"gravatar_id"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
      </span><span class="nl">"url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://api.github.com/users/msftplayground"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"html_url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://github.com/msftplayground"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"followers_url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://api.github.com/users/msftplayground/followers"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"following_url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://api.github.com/users/msftplayground/following{/other_user}"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"gists_url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://api.github.com/users/msftplayground/gists{/gist_id}"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"starred_url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://api.github.com/users/msftplayground/starred{/owner}{/repo}"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"subscriptions_url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://api.github.com/users/msftplayground/subscriptions"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"organizations_url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://api.github.com/users/msftplayground/orgs"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"repos_url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://api.github.com/users/msftplayground/repos"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"events_url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://api.github.com/users/msftplayground/events{/privacy}"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"received_events_url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://api.github.com/users/msftplayground/received_events"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Organization"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"user_view_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"public"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"site_admin"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"repository_selection"</span><span class="p">:</span><span class="w"> </span><span class="s2">"selected"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"access_tokens_url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://api.github.com/app/installations/83382606/access_tokens"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"repositories_url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://api.github.com/installation/repositories"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"html_url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://github.com/organizations/msftplayground/settings/installations/83382606"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"app_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">1864472</span><span class="p">,</span><span class="w">
    </span><span class="nl">"app_slug"</span><span class="p">:</span><span class="w"> </span><span class="s2">"msftplayground-issue-registration"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"target_id"</span><span class="p">:</span><span class="w"> </span><span class="mi">49311642</span><span class="p">,</span><span class="w">
    </span><span class="nl">"target_type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Organization"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"permissions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"issues"</span><span class="p">:</span><span class="w"> </span><span class="s2">"write"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"metadata"</span><span class="p">:</span><span class="w"> </span><span class="s2">"read"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"events"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
    </span><span class="nl">"created_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2025-08-29T15:41:16.000Z"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"updated_at"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2025-08-29T15:41:32.000Z"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"single_file_name"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
    </span><span class="nl">"has_multiple_single_files"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
    </span><span class="nl">"single_file_paths"</span><span class="p">:</span><span class="w"> </span><span class="p">[],</span><span class="w">
    </span><span class="nl">"suspended_by"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
    </span><span class="nl">"suspended_at"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">]</span><span class="w">
</span></code></pre></div></div>

<p>The URI shown above is the endpoint you call to obtain the application’s installation access token, which is required for performing actions as the application. In my implementation, I don’t hardcode this URI — instead, I retrieve the exact access_tokens_url by first querying the installation details for a given organization.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="nf">GetAccessTokenUrl</span><span class="p">(</span><span class="kt">string</span> <span class="n">organization</span><span class="p">,</span> <span class="kt">string</span> <span class="n">jwtToken</span><span class="p">)</span> <span class="p">{</span>

    <span class="kt">var</span> <span class="n">client</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">HttpClient</span><span class="p">();</span>
    <span class="n">client</span><span class="p">.</span><span class="n">DefaultRequestHeaders</span><span class="p">.</span><span class="n">Authorization</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">AuthenticationHeaderValue</span><span class="p">(</span><span class="s">"Bearer"</span><span class="p">,</span> <span class="n">jwtToken</span><span class="p">);</span>
    <span class="n">client</span><span class="p">.</span><span class="n">DefaultRequestHeaders</span><span class="p">.</span><span class="n">UserAgent</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="k">new</span> <span class="nf">ProductInfoHeaderValue</span><span class="p">(</span><span class="n">_appHeader</span><span class="p">,</span> <span class="s">"1.0"</span><span class="p">));</span>

    <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">GetAsync</span><span class="p">(</span>
        <span class="s">$"https://api.github.com/app/installations"</span><span class="p">);</span>

    <span class="n">response</span><span class="p">.</span><span class="nf">EnsureSuccessStatusCode</span><span class="p">();</span>
    <span class="kt">var</span> <span class="n">json</span> <span class="p">=</span> <span class="k">await</span> <span class="n">response</span><span class="p">.</span><span class="n">Content</span><span class="p">.</span><span class="nf">ReadAsStringAsync</span><span class="p">();</span>
    <span class="kt">var</span> <span class="n">doc</span> <span class="p">=</span> <span class="n">JsonDocument</span><span class="p">.</span><span class="nf">Parse</span><span class="p">(</span><span class="n">json</span><span class="p">);</span>

    <span class="kt">string</span> <span class="n">accessTokensUrl</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>

    <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">installation</span> <span class="k">in</span> <span class="n">doc</span><span class="p">.</span><span class="n">RootElement</span><span class="p">.</span><span class="nf">EnumerateArray</span><span class="p">())</span> <span class="p">{</span>

        <span class="kt">var</span> <span class="n">account</span> <span class="p">=</span> <span class="n">installation</span><span class="p">.</span><span class="nf">GetProperty</span><span class="p">(</span><span class="s">"account"</span><span class="p">);</span>
        <span class="kt">var</span> <span class="n">login</span> <span class="p">=</span> <span class="n">account</span><span class="p">.</span><span class="nf">GetProperty</span><span class="p">(</span><span class="s">"login"</span><span class="p">).</span><span class="nf">GetString</span><span class="p">();</span>
        <span class="kt">string</span> <span class="n">type</span> <span class="p">=</span> <span class="n">account</span><span class="p">.</span><span class="nf">GetProperty</span><span class="p">(</span><span class="s">"type"</span><span class="p">).</span><span class="nf">GetString</span><span class="p">();</span>

        <span class="k">if</span><span class="p">(</span><span class="n">login</span> <span class="p">==</span> <span class="n">organization</span> <span class="p">&amp;&amp;</span> <span class="n">type</span> <span class="p">==</span> <span class="s">"Organization"</span><span class="p">){</span>
            <span class="n">accessTokensUrl</span> <span class="p">=</span> <span class="n">installation</span><span class="p">.</span><span class="nf">GetProperty</span><span class="p">(</span><span class="s">"access_tokens_url"</span><span class="p">).</span><span class="nf">GetString</span><span class="p">();</span>
            <span class="k">break</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="n">accessTokensUrl</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now that we have the specific URI for the installation access token, we can proceed. The URI can be used in combination with the token to get the installation token.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="nf">GetInstallationTokenAsync</span><span class="p">(</span><span class="kt">string</span> <span class="n">accessTokenUrl</span><span class="p">,</span> <span class="kt">string</span> <span class="n">jwtToken</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">client</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">HttpClient</span><span class="p">();</span>
    <span class="n">client</span><span class="p">.</span><span class="n">DefaultRequestHeaders</span><span class="p">.</span><span class="n">Authorization</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">AuthenticationHeaderValue</span><span class="p">(</span><span class="s">"Bearer"</span><span class="p">,</span> <span class="n">jwtToken</span><span class="p">);</span>
    <span class="n">client</span><span class="p">.</span><span class="n">DefaultRequestHeaders</span><span class="p">.</span><span class="n">UserAgent</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="k">new</span> <span class="nf">ProductInfoHeaderValue</span><span class="p">(</span><span class="n">_appHeader</span><span class="p">,</span> <span class="s">"1.0"</span><span class="p">));</span>
    <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">PostAsync</span><span class="p">(</span><span class="n">accessTokenUrl</span><span class="p">,</span> <span class="k">null</span><span class="p">);</span>
    <span class="n">response</span><span class="p">.</span><span class="nf">EnsureSuccessStatusCode</span><span class="p">();</span>
    <span class="kt">var</span> <span class="n">json</span> <span class="p">=</span> <span class="k">await</span> <span class="n">response</span><span class="p">.</span><span class="n">Content</span><span class="p">.</span><span class="nf">ReadAsStringAsync</span><span class="p">();</span>
    <span class="kt">var</span> <span class="n">doc</span> <span class="p">=</span> <span class="n">JsonDocument</span><span class="p">.</span><span class="nf">Parse</span><span class="p">(</span><span class="n">json</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">doc</span><span class="p">.</span><span class="n">RootElement</span><span class="p">.</span><span class="nf">GetProperty</span><span class="p">(</span><span class="s">"token"</span><span class="p">).</span><span class="nf">GetString</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="use-the-installation-access-token-to-call-the-apis">Use the installation access token to call the API’s</h2>

<p>The token returned by the “GetInstallationTokenAsync” method can then be used to perform actions on the desired location. The snippet below creates an issue within the specified repository.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="nf">CreateIssueAsync</span><span class="p">(</span><span class="kt">string</span> <span class="n">title</span><span class="p">,</span> <span class="kt">string</span> <span class="n">body</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">string</span> <span class="n">retVal</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>

    <span class="kt">string</span> <span class="n">token</span> <span class="p">=</span> <span class="nf">GenerateJwtToken</span><span class="p">();</span>
    <span class="kt">var</span> <span class="n">url</span> <span class="p">=</span> <span class="k">await</span> <span class="nf">GetAccessTokenUrl</span><span class="p">(</span><span class="n">_owner</span><span class="p">,</span> <span class="n">token</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">installationToken</span> <span class="p">=</span> <span class="k">await</span> <span class="nf">GetInstallationTokenAsync</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">token</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">client</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">HttpClient</span><span class="p">();</span>
    <span class="n">client</span><span class="p">.</span><span class="n">DefaultRequestHeaders</span><span class="p">.</span><span class="n">Authorization</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">AuthenticationHeaderValue</span><span class="p">(</span><span class="s">"Bearer"</span><span class="p">,</span> <span class="n">installationToken</span><span class="p">);</span>
    <span class="n">client</span><span class="p">.</span><span class="n">DefaultRequestHeaders</span><span class="p">.</span><span class="n">UserAgent</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="k">new</span> <span class="nf">ProductInfoHeaderValue</span><span class="p">(</span><span class="n">_appHeader</span><span class="p">,</span> <span class="s">"1.0"</span><span class="p">));</span>

    <span class="kt">var</span> <span class="n">issue</span> <span class="p">=</span> <span class="k">new</span> <span class="p">{</span> <span class="n">title</span><span class="p">,</span> <span class="n">body</span> <span class="p">};</span>
    <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">PostAsJsonAsync</span><span class="p">(</span>
        <span class="s">$"https://api.github.com/repos/</span><span class="p">{</span><span class="n">_owner</span><span class="p">}</span><span class="s">/</span><span class="p">{</span><span class="n">_repo</span><span class="p">}</span><span class="s">/issues"</span><span class="p">,</span> <span class="n">issue</span><span class="p">);</span>

    <span class="kt">var</span> <span class="n">result</span> <span class="p">=</span> <span class="k">await</span> <span class="n">response</span><span class="p">.</span><span class="n">Content</span><span class="p">.</span><span class="nf">ReadAsStringAsync</span><span class="p">();</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">IsSuccessStatusCode</span><span class="p">)</span> <span class="p">{</span>
        <span class="kt">var</span> <span class="n">json</span> <span class="p">=</span> <span class="k">await</span> <span class="n">response</span><span class="p">.</span><span class="n">Content</span><span class="p">.</span><span class="nf">ReadAsStringAsync</span><span class="p">();</span>
        <span class="kt">var</span> <span class="n">doc</span> <span class="p">=</span> <span class="n">JsonDocument</span><span class="p">.</span><span class="nf">Parse</span><span class="p">(</span><span class="n">json</span><span class="p">);</span>
        <span class="k">return</span> <span class="n">doc</span><span class="p">.</span><span class="n">RootElement</span><span class="p">.</span><span class="nf">GetProperty</span><span class="p">(</span><span class="s">"id"</span><span class="p">).</span><span class="nf">ToString</span><span class="p">();</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="n">retVal</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>

<p>GitHub Apps represent a more secure and flexible way of authenticating to GitHub. Besides that, they can also act as a standalone entity. Based on the snippets above, the complete solution looks like this.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">Microsoft.IdentityModel.Tokens</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">System.IdentityModel.Tokens.Jwt</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">System.Net.Http.Headers</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">System.Net.Http.Json</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">System.Security.Cryptography</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">System.Text.Json</span><span class="p">;</span>

<span class="k">namespace</span> <span class="nn">App.Core.Services</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">class</span> <span class="nc">GitHubService</span>
    <span class="p">{</span>
        <span class="k">private</span> <span class="k">readonly</span> <span class="kt">string</span> <span class="n">_appId</span><span class="p">;</span>
        <span class="k">private</span> <span class="k">readonly</span> <span class="kt">string</span> <span class="n">_privateKeyPem</span><span class="p">;</span>
        <span class="k">private</span> <span class="k">readonly</span> <span class="kt">string</span> <span class="n">_owner</span><span class="p">;</span>
        <span class="k">private</span> <span class="k">readonly</span> <span class="kt">string</span> <span class="n">_repo</span><span class="p">;</span>
        <span class="k">private</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">_appHeader</span> <span class="p">=</span> <span class="s">"msftplayground-Issue-registration"</span><span class="p">;</span>

        <span class="k">public</span> <span class="nf">GitHubService</span><span class="p">(</span><span class="kt">string</span> <span class="n">appId</span><span class="p">,</span> <span class="kt">string</span> <span class="n">privateKeyPem</span><span class="p">,</span> <span class="kt">string</span> <span class="n">owner</span><span class="p">,</span> <span class="kt">string</span> <span class="n">repo</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="n">_appId</span> <span class="p">=</span> <span class="n">appId</span><span class="p">;</span>
            <span class="n">_owner</span> <span class="p">=</span> <span class="n">owner</span><span class="p">;</span>
            <span class="n">_repo</span> <span class="p">=</span> <span class="n">repo</span><span class="p">;</span>
            <span class="n">_privateKeyPem</span> <span class="p">=</span> <span class="n">privateKeyPem</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="k">public</span> <span class="kt">string</span> <span class="nf">GenerateJwtToken</span><span class="p">()</span>
        <span class="p">{</span>
            <span class="kt">var</span> <span class="n">jwtSecurityTokenHandler</span> <span class="p">=</span> <span class="k">new</span> <span class="n">JwtSecurityTokenHandler</span> <span class="p">{</span> <span class="n">SetDefaultTimesOnTokenCreation</span> <span class="p">=</span> <span class="k">false</span> <span class="p">};</span>

            <span class="kt">var</span> <span class="n">rsa</span> <span class="p">=</span> <span class="n">RSA</span><span class="p">.</span><span class="nf">Create</span><span class="p">();</span>
            <span class="n">rsa</span><span class="p">.</span><span class="nf">ImportFromPem</span><span class="p">(</span><span class="n">_privateKeyPem</span><span class="p">.</span><span class="nf">ToCharArray</span><span class="p">());</span>

            <span class="kt">var</span> <span class="n">securityKey</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">RsaSecurityKey</span><span class="p">(</span><span class="n">rsa</span><span class="p">);</span>
            <span class="kt">var</span> <span class="n">credentials</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">SigningCredentials</span><span class="p">(</span><span class="n">securityKey</span><span class="p">,</span> <span class="n">SecurityAlgorithms</span><span class="p">.</span><span class="n">RsaSha256</span><span class="p">);</span>

            <span class="kt">var</span> <span class="n">now</span> <span class="p">=</span> <span class="n">DateTime</span><span class="p">.</span><span class="n">UtcNow</span><span class="p">.</span><span class="nf">AddSeconds</span><span class="p">(-</span><span class="m">60</span><span class="p">);</span>
            <span class="kt">var</span> <span class="n">token</span> <span class="p">=</span> <span class="n">jwtSecurityTokenHandler</span><span class="p">.</span><span class="nf">CreateToken</span><span class="p">(</span><span class="k">new</span> <span class="n">SecurityTokenDescriptor</span> <span class="p">{</span>
                <span class="n">Issuer</span> <span class="p">=</span> <span class="n">_appId</span><span class="p">,</span>
                <span class="n">Expires</span> <span class="p">=</span> <span class="n">now</span><span class="p">.</span><span class="nf">AddMinutes</span><span class="p">(</span><span class="m">10</span><span class="p">),</span>
                <span class="n">IssuedAt</span> <span class="p">=</span> <span class="n">now</span><span class="p">,</span>
                <span class="n">SigningCredentials</span> <span class="p">=</span> <span class="n">credentials</span>
            <span class="p">});</span>

            <span class="k">return</span> <span class="n">jwtSecurityTokenHandler</span><span class="p">.</span><span class="nf">WriteToken</span><span class="p">(</span><span class="n">token</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="nf">GetAccessTokenUrl</span><span class="p">(</span><span class="kt">string</span> <span class="n">organization</span><span class="p">,</span> <span class="kt">string</span> <span class="n">jwtToken</span><span class="p">)</span> <span class="p">{</span>

            <span class="kt">var</span> <span class="n">client</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">HttpClient</span><span class="p">();</span>
            <span class="n">client</span><span class="p">.</span><span class="n">DefaultRequestHeaders</span><span class="p">.</span><span class="n">Authorization</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">AuthenticationHeaderValue</span><span class="p">(</span><span class="s">"Bearer"</span><span class="p">,</span> <span class="n">jwtToken</span><span class="p">);</span>
            <span class="n">client</span><span class="p">.</span><span class="n">DefaultRequestHeaders</span><span class="p">.</span><span class="n">UserAgent</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="k">new</span> <span class="nf">ProductInfoHeaderValue</span><span class="p">(</span><span class="n">_appHeader</span><span class="p">,</span> <span class="s">"1.0"</span><span class="p">));</span>

            <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">GetAsync</span><span class="p">(</span>
                <span class="s">$"https://api.github.com/app/installations"</span><span class="p">);</span>

            <span class="n">response</span><span class="p">.</span><span class="nf">EnsureSuccessStatusCode</span><span class="p">();</span>
            <span class="kt">var</span> <span class="n">json</span> <span class="p">=</span> <span class="k">await</span> <span class="n">response</span><span class="p">.</span><span class="n">Content</span><span class="p">.</span><span class="nf">ReadAsStringAsync</span><span class="p">();</span>
            <span class="kt">var</span> <span class="n">doc</span> <span class="p">=</span> <span class="n">JsonDocument</span><span class="p">.</span><span class="nf">Parse</span><span class="p">(</span><span class="n">json</span><span class="p">);</span>

            <span class="kt">string</span> <span class="n">accessTokensUrl</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>

            <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">installation</span> <span class="k">in</span> <span class="n">doc</span><span class="p">.</span><span class="n">RootElement</span><span class="p">.</span><span class="nf">EnumerateArray</span><span class="p">())</span> <span class="p">{</span>

                <span class="kt">var</span> <span class="n">account</span> <span class="p">=</span> <span class="n">installation</span><span class="p">.</span><span class="nf">GetProperty</span><span class="p">(</span><span class="s">"account"</span><span class="p">);</span>
                <span class="kt">var</span> <span class="n">login</span> <span class="p">=</span> <span class="n">account</span><span class="p">.</span><span class="nf">GetProperty</span><span class="p">(</span><span class="s">"login"</span><span class="p">).</span><span class="nf">GetString</span><span class="p">();</span>
                <span class="kt">string</span> <span class="n">type</span> <span class="p">=</span> <span class="n">account</span><span class="p">.</span><span class="nf">GetProperty</span><span class="p">(</span><span class="s">"type"</span><span class="p">).</span><span class="nf">GetString</span><span class="p">();</span>

                <span class="k">if</span><span class="p">(</span><span class="n">login</span> <span class="p">==</span> <span class="n">organization</span> <span class="p">&amp;&amp;</span> <span class="n">type</span> <span class="p">==</span> <span class="s">"Organization"</span><span class="p">){</span>
                    <span class="n">accessTokensUrl</span> <span class="p">=</span> <span class="n">installation</span><span class="p">.</span><span class="nf">GetProperty</span><span class="p">(</span><span class="s">"access_tokens_url"</span><span class="p">).</span><span class="nf">GetString</span><span class="p">();</span>
                    <span class="k">break</span><span class="p">;</span>
                <span class="p">}</span>
            <span class="p">}</span>

            <span class="k">return</span> <span class="n">accessTokensUrl</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="nf">GetInstallationTokenAsync</span><span class="p">(</span><span class="kt">string</span> <span class="n">accessTokenUrl</span><span class="p">,</span> <span class="kt">string</span> <span class="n">jwtToken</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="kt">var</span> <span class="n">client</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">HttpClient</span><span class="p">();</span>
            <span class="n">client</span><span class="p">.</span><span class="n">DefaultRequestHeaders</span><span class="p">.</span><span class="n">Authorization</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">AuthenticationHeaderValue</span><span class="p">(</span><span class="s">"Bearer"</span><span class="p">,</span> <span class="n">jwtToken</span><span class="p">);</span>
            <span class="n">client</span><span class="p">.</span><span class="n">DefaultRequestHeaders</span><span class="p">.</span><span class="n">UserAgent</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="k">new</span> <span class="nf">ProductInfoHeaderValue</span><span class="p">(</span><span class="n">_appHeader</span><span class="p">,</span> <span class="s">"1.0"</span><span class="p">));</span>

            <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">PostAsync</span><span class="p">(</span><span class="n">accessTokenUrl</span><span class="p">,</span> <span class="k">null</span><span class="p">);</span>

            <span class="n">response</span><span class="p">.</span><span class="nf">EnsureSuccessStatusCode</span><span class="p">();</span>
            <span class="kt">var</span> <span class="n">json</span> <span class="p">=</span> <span class="k">await</span> <span class="n">response</span><span class="p">.</span><span class="n">Content</span><span class="p">.</span><span class="nf">ReadAsStringAsync</span><span class="p">();</span>
            <span class="kt">var</span> <span class="n">doc</span> <span class="p">=</span> <span class="n">JsonDocument</span><span class="p">.</span><span class="nf">Parse</span><span class="p">(</span><span class="n">json</span><span class="p">);</span>
            <span class="k">return</span> <span class="n">doc</span><span class="p">.</span><span class="n">RootElement</span><span class="p">.</span><span class="nf">GetProperty</span><span class="p">(</span><span class="s">"token"</span><span class="p">).</span><span class="nf">GetString</span><span class="p">();</span>
        <span class="p">}</span>

        <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="nf">CreateIssueAsync</span><span class="p">(</span><span class="kt">string</span> <span class="n">title</span><span class="p">,</span> <span class="kt">string</span> <span class="n">body</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="kt">string</span> <span class="n">retVal</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>

            <span class="kt">string</span> <span class="n">token</span> <span class="p">=</span> <span class="nf">GenerateJwtToken</span><span class="p">();</span>
            <span class="kt">var</span> <span class="n">url</span> <span class="p">=</span> <span class="k">await</span> <span class="nf">GetAccessTokenUrl</span><span class="p">(</span><span class="n">_owner</span><span class="p">,</span> <span class="n">token</span><span class="p">);</span>
            <span class="kt">var</span> <span class="n">installationToken</span> <span class="p">=</span> <span class="k">await</span> <span class="nf">GetInstallationTokenAsync</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">token</span><span class="p">);</span>
            <span class="kt">var</span> <span class="n">client</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">HttpClient</span><span class="p">();</span>
            <span class="n">client</span><span class="p">.</span><span class="n">DefaultRequestHeaders</span><span class="p">.</span><span class="n">Authorization</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">AuthenticationHeaderValue</span><span class="p">(</span><span class="s">"Bearer"</span><span class="p">,</span> <span class="n">installationToken</span><span class="p">);</span>
            <span class="n">client</span><span class="p">.</span><span class="n">DefaultRequestHeaders</span><span class="p">.</span><span class="n">UserAgent</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="k">new</span> <span class="nf">ProductInfoHeaderValue</span><span class="p">(</span><span class="n">_appHeader</span><span class="p">,</span> <span class="s">"1.0"</span><span class="p">));</span>

            <span class="kt">var</span> <span class="n">issue</span> <span class="p">=</span> <span class="k">new</span> <span class="p">{</span> <span class="n">title</span><span class="p">,</span> <span class="n">body</span> <span class="p">};</span>
            <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">PostAsJsonAsync</span><span class="p">(</span>
                <span class="s">$"https://api.github.com/repos/</span><span class="p">{</span><span class="n">_owner</span><span class="p">}</span><span class="s">/</span><span class="p">{</span><span class="n">_repo</span><span class="p">}</span><span class="s">/issues"</span><span class="p">,</span> <span class="n">issue</span><span class="p">);</span>

            <span class="kt">var</span> <span class="n">result</span> <span class="p">=</span> <span class="k">await</span> <span class="n">response</span><span class="p">.</span><span class="n">Content</span><span class="p">.</span><span class="nf">ReadAsStringAsync</span><span class="p">();</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">IsSuccessStatusCode</span><span class="p">)</span> <span class="p">{</span>
                <span class="kt">var</span> <span class="n">json</span> <span class="p">=</span> <span class="k">await</span> <span class="n">response</span><span class="p">.</span><span class="n">Content</span><span class="p">.</span><span class="nf">ReadAsStringAsync</span><span class="p">();</span>
                <span class="kt">var</span> <span class="n">doc</span> <span class="p">=</span> <span class="n">JsonDocument</span><span class="p">.</span><span class="nf">Parse</span><span class="p">(</span><span class="n">json</span><span class="p">);</span>
                <span class="k">return</span> <span class="n">doc</span><span class="p">.</span><span class="n">RootElement</span><span class="p">.</span><span class="nf">GetProperty</span><span class="p">(</span><span class="s">"id"</span><span class="p">).</span><span class="nf">ToString</span><span class="p">();</span>
            <span class="p">}</span>

            <span class="k">return</span> <span class="n">retVal</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>For additional information, make sure to check out the documentation below.</p>

<ul>
  <li><a href="https://docs.github.com/en/rest/apps/installations?apiVersion=2022-11-28">REST API endpoints for GitHub App installations</a></li>
</ul>]]></content><author><name>Maik van der Gaag</name></author><category term="Azure" /><category term="GitHub" /><category term="App" /><category term="Authentication" /><summary type="html"><![CDATA[When working with GitHub and its APIs, authentication plays a crucial role in ensuring secure and controlled access to repositories, workflows, and organizational data.]]></summary></entry><entry><title type="html">Power Up Your Infrastructure with Bicep Extensions</title><link href="https://msftplayground.com/2025/09/bicep-extensions" rel="alternate" type="text/html" title="Power Up Your Infrastructure with Bicep Extensions" /><published>2025-09-01T02:00:00+02:00</published><updated>2025-09-01T02:00:00+02:00</updated><id>https://msftplayground.com/2025/09/bicep-extensions</id><content type="html" xml:base="https://msftplayground.com/2025/09/bicep-extensions"><![CDATA[<p>Bicep was initially developed to enhance ARM. Know it is also possible to reference resources beyond the scope of the Azure Resource Manager. As you may know, it is now possible to deploy resources within a Kubernetes platform and make use of the Microsoft Graph.</p>

<ul>
  <li><a href="https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/bicep-extension?WT.mc_id=AZ-MVP-5004255">Use Bicep Extensions</a></li>
  <li><a href="https://learn.microsoft.com/en-us/graph/templates/?WT.mc_id=AZ-MVP-5004255">Bicep Graph Extension</a></li>
  <li><a href="https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/bicep-kubernetes-extension?WT.mc_id=AZ-MVP-5004255">Bicep Kubernetes Extension</a></li>
</ul>

<p>These extensions are really great and make Bicep much more powerful than it already is. However, what if I tell you that the way this is built also has its shortcomings?</p>

<h2 id="disadvantage-bicep-extensions">Disadvantage Bicep Extensions</h2>

<p>At the time of writing this article, Bicep extensions have one main disadvantage. Let me explain, take a close look at the image below.</p>

<p><img src="/assets/images/2025/bicep-extensions.png" alt="Bicep Extension Process" /></p>

<p>As you may have seen in the image, the Bicep templates that you create locally are sent to the Azure Resource Manager API, where the deployment engine determines whether the Azure Resource Manager should perform its work or, in this example, the resources should be created by the Graph API. As this is all server-side (@Microsoft), there is no really good way for the community to jump in and help build extensions, unlike with Terraform, which makes the improvements and new additions to the extensions take a very long time.
But wait, there are some cool things in preview.</p>

<h2 id="bicep-local">Bicep Local</h2>

<p>Since a couple of months Bicep contains a preview feature called “localDeploy”. When activated, you can initiate the deployment from your local device and add extensions.</p>

<p>Currently, the community has already developed some nice extensions that you can use and try out:</p>

<ul>
  <li><a href="https://github.com/anthony-c-martin/bicep-ext-github">Bicep GitHub Extensions - Anthony Martin (Microsoft)</a></li>
  <li><a href="https://github.com/anthony-c-martin/bicep-ext-keyvault">Key Vault Extension - Anthony Martin (Microsoft)</a></li>
  <li><a href="https://github.com/Gijsreyn/bicep-ext-databricks">Azure Databricks extension - Gijs Reijn</a></li>
  <li><a href="https://github.com/maikvandergaag/bicep-ext-http">Http Extension - Maik van der Gaag</a></li>
</ul>

<p>As you can imagine, and as you may have seen on the list of extensions, you can create your own. Since version 0.37.4 of Bicep, there has been documentation released to get started creating your own extensions:</p>

<ul>
  <li><a href="https://github.com/Azure/bicep/blob/main/docs/experimental/local-deploy-dotnet-quickstart.md">Creating a Local Extension with .Net</a></li>
</ul>

<h2 id="start-testing-bicep-local-deployments">Start Testing Bicep Local deployments</h2>

<p>To get started deploying with local deployments, a few steps need to be taken. For these steps, we use the repository below as a reference.</p>

<ul>
  <li><a href="https://github.com/maikvandergaag/msft-bicep-local">Bicep Local Repository</a></li>
</ul>

<p>In that repository, you can find multiple samples of doing local deployments. With the steps and configurations below, you can get started using this extraordinary capability.</p>

<ol>
  <li>Make sure that you have one of the latest Bicep installations.</li>
  <li>Add the “localDeploy” experimental feature to your bicepconfig.json file.</li>
</ol>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">  </span><span class="nl">"experimentalFeaturesEnabled"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"localDeploy"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<ol start="3">
  <li>When using the CLI you can deploy locally by using the “bicep local” command.</li>
</ol>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Bicep <span class="nb">local </span>main.bicepparam
</code></pre></div></div>

<p>The deployments can also be run from Visual Studio Code. To do it from Visual Studio code open the deployments pane. You can do this while the bicepparam file is open.</p>

<p><img src="/assets/images/2025/deployment-pane-button.png" alt="Bicep Deployment Pane Button" /></p>

<p>From the pane that opens, deploy the Bicep file and see the output of your local deployment.</p>

<p><img src="/assets/images/2025/deploymentpane.png" alt="Bicep Deployment Pane" /></p>

<h3 id="tracing">Tracing</h3>

<p>When you have problems during your local deployments, which I have encountered occacionally you can enable tracing to get more insights. This is done by setting the Bicep Tracing environment variable.</p>

<h4 id="cmd">cmd</h4>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">set </span><span class="nv">BICEP_TRACING_ENABLED</span><span class="o">=</span><span class="nb">true</span>
</code></pre></div></div>

<h4 id="powershell">PowerShell</h4>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">BICEP_TRACING_ENABLED</span><span class="o">=</span><span class="bp">$true</span><span class="w">
</span></code></pre></div></div>

<h2 id="conclusion">Conclusion</h2>

<p>Local deploy is a great extension to Bicep that helps you get started with different platforms or enables more on the data plane. So let’s hope this functionality will be further developed and that it will become generally available.</p>

<p>Be sure to look out for my next blog post, where I’ll delve deeper into the extension I have developed.</p>]]></content><author><name>Maik van der Gaag</name></author><category term="Azure" /><category term="Bicep" /><category term="Extensions" /><category term="Local" /><category term="Preview" /><summary type="html"><![CDATA[Bicep was initially developed to enhance ARM. Know it is also possible to reference resources beyond the scope of the Azure Resource Manager. As you may know, it is now possible to deploy resources within a Kubernetes platform and make use of the Microsoft Graph.]]></summary></entry><entry><title type="html">Optimizing Budget Configurations for Enhanced Cost Management in Azure</title><link href="https://msftplayground.com/2025/03/optimizing-budget-cost-management-azure" rel="alternate" type="text/html" title="Optimizing Budget Configurations for Enhanced Cost Management in Azure" /><published>2025-03-04T01:00:00+01:00</published><updated>2025-03-04T01:00:00+01:00</updated><id>https://msftplayground.com/2025/03/optimizing-budget-cost-management-azure</id><content type="html" xml:base="https://msftplayground.com/2025/03/optimizing-budget-cost-management-azure"><![CDATA[<p>This is my involvement in Azure Spring Clean 2025, organized by Thomas Thornton, which I`m excited to be part of! For those unaware, the event promotes and encourages well-managed Azure tenants. It is held yearly in March, and over a couple of days, best practices and information are shared.
You can find more information <a href="https://www.azurespringclean.com/">here</a>.</p>

<p><img src="/assets/images/2025/spring-clean.png" alt="Azure Spring Clean" /></p>

<p>Cost management is crucial for maintaining a cost-effective cloud platform. Without proper cost management, you can face unexpected charges. Azure contains a couple of possibilities for efficient cost management.</p>

<p>Almost every scope within Azure (Management groups, subscriptions, and resources groups) has a ‘cost management’ section within the menu. All of these can also be managed from the ‘Cost Management’ blade within the Azure Portal.</p>

<p><img src="/assets/images/2025/cost-management.png" alt="Cost Management" /></p>

<p>When thinking about cost management in Azure, a few things are very important. One of them is the visibility of the costs within the platform. You should always be able to see the costs associated with your resources and applications. To get insights into this, you should use tags (especially when resources span resource groups or subscriptions).</p>

<blockquote>
  <p><strong>Note</strong>: Tags used for your resources can also be used to create specific budget alerts.</p>
</blockquote>

<p>In your environment, a person or team should be responsible for cost management. In many organizations, these responsibilities are also assigned within the DevOps teams.
Managing costs for the platform is an iterative process that should be handled by the people responsible for it. The process really spans optimizing, adjusting, and sharing insights.
Besides the responsibility, applications should be designed with costs in mind. When setting up a technical design for Azure, it is expected to include a subsection on costs and a description of why specific resources are used, which could also be based on costs. Even choosing the right location for a resource could be based on costs.</p>

<p>To help you make these decisions, the <a href="https://azure.microsoft.com/pricing/calculator/">Azure Cost Calculator</a> can help.</p>

<p>Insights regarding the costs within Azure are retrieved in a couple of ways, and Azure already offers an extensive one with the Cost Management Blade within the platform:</p>

<p><img src="/assets/images/2025/cost-management-blade.png" alt="Cost Management Blade" /></p>

<p>The rest of this article will discuss how to monitor your costs across the different scopes and set up budget and anomaly alerts.</p>

<h2 id="cost-anomaly-alert">Cost Anomaly Alert</h2>

<p>An anomaly is something that deviates from the expected or normal pattern. In the context of Azure and the associated costs, an anomaly typically refers to an unexpected spike or drop in cloud costs that is different based on historical information.</p>

<p>The Azure platform offers the capability to set up alerts for these anomalies, allowing you and, for example, your DevOps team to monitor their costs.</p>

<p>With an Azure Landing Zone implementation in place, it is a good option to include this with your application deployments and subscription vending process.</p>

<p>These alert rules can be created in the portal by performing the following steps:</p>

<ol>
  <li>Go to Azure Cost Management within the Azure Portal.</li>
  <li>In the monitoring section, click on ‘alert rules.’</li>
</ol>

<p><img src="/assets/images/2025/cost-management-alert-rules.png" alt="Cost Management Alert Rules" /></p>

<ol start="3">
  <li>Click on ‘Add’ to create a new alert rule.</li>
  <li>Set the alert type to ‘Anomaly’ and fill in the other information.</li>
</ol>

<p><img src="/assets/images/2025/alert-rule.png" alt="Alert rule" /></p>

<p>Save the alert rule and wait to see if you have strange behavior within the costs. Before saving, make sure the alert’s scope is the correct one.</p>

<blockquote>
  <p>Before saving alert rules, make sure that the alert is configured on the correct scope. This way, you can ensure that you are not over-alerting people.</p>
</blockquote>

<h3 id="bicep-anomaly-alert">Bicep Anomaly Alert</h3>

<p>Creating the alerts within the portal is nice, of course, but it would be much better if we could automate this process. So, let’s see if we can specify an anomaly alert within Bicep.</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">targetScope = 'subscription'</span>

<span class="err">@</span><span class="s">description('The name for the scheduled action. This name is used in the resource ID. Default</span><span class="err">:</span> <span class="s">AnomalyAlert.')</span>
<span class="s">param name string = 'AnomalyAlert'</span>

<span class="err">@</span><span class="s">description('The display name to show in the portal when viewing the list of alerts. Default</span><span class="err">:</span> <span class="s">(scheduled action name).')</span>
<span class="s">param displayName string = name</span>

<span class="err">@</span><span class="s">description('Email address of the person or team responsible for this scheduled action.')</span>
<span class="s">param notificationEmail string</span>

<span class="err">@</span><span class="s">description('List of email addresses that should receive emails.')</span>
<span class="s">param emailRecipients array</span>

<span class="err">@</span><span class="s">description('The body of the email. Default</span><span class="err">:</span> <span class="s">Anomaly detected in your subscription. Please review the Cost Management dashboard for more details.')</span>
<span class="err">@</span><span class="s">maxLength(250)</span>
<span class="s">param emailBody string = 'Anomaly detected in your subscription. Please review the Cost Management dashboard for more details.'</span>

<span class="err">@</span><span class="s">description('The subject of the email. Default</span><span class="err">:</span> <span class="na">(Anomaly alert</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">subscription displayname</span><span class="pi">]</span><span class="s">).')</span>
<span class="err">@</span><span class="s">maxLength(70)</span>
<span class="s">param emailSubject string = 'Anomaly alert</span><span class="err">:</span> <span class="s">${subscription().displayName}'</span>

<span class="err">@</span><span class="s">description('The first day the schedule should run. Default = Now.')</span>
<span class="s">param scheduleStartDate string = utcNow('yyyy-MM-ddTHH:00Z')</span>

<span class="err">@</span><span class="s">description('The last day the schedule should run. Default = 1 year from start date.')</span>
<span class="s">param scheduleEndDate string = dateTimeAdd(scheduleStartDate, 'P1Y')</span>

<span class="err">@</span><span class="s">description('The view ID to use for the scheduled action (should not be adjust but is defined as param to comply to best practices). Default</span><span class="err">:</span> <span class="s">DailyAnomalyByResourceGroup.')</span>
<span class="s">param viewId string = '${subscription().id}/providers/Microsoft.CostManagement/views/ms:DailyAnomalyByResourceGroup'</span>

<span class="s">resource sa 'Microsoft.CostManagement/scheduledActions@2024-08-01' = {</span>
  <span class="s">name</span><span class="err">:</span> <span class="s">name</span>
  <span class="s">kind</span><span class="err">:</span> <span class="s1">'</span><span class="s">InsightAlert'</span>
  <span class="na">properties</span><span class="pi">:</span> <span class="pi">{</span>
    <span class="nv">displayName</span><span class="pi">:</span> <span class="nv">displayName</span>
    <span class="nv">viewId</span><span class="pi">:</span> <span class="nv">viewId</span>
    <span class="nv">notificationEmail</span><span class="pi">:</span> <span class="nv">notificationEmail</span>
    <span class="nv">status</span><span class="pi">:</span> <span class="s1">'</span><span class="s">enabled'</span>
    <span class="nv">notification</span><span class="pi">:</span> <span class="pi">{</span>
      <span class="nv">subject</span><span class="pi">:</span> <span class="nv">emailSubject</span>
      <span class="nv">to</span><span class="pi">:</span> <span class="nv">emailRecipients</span>
      <span class="nv">message</span><span class="pi">:</span> <span class="nv">emailBody</span>
    <span class="pi">}</span>
    <span class="nv">schedule</span><span class="pi">:</span> <span class="pi">{</span>
      <span class="nv">frequency</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Daily'</span>
      <span class="nv">startDate</span><span class="pi">:</span> <span class="nv">scheduleStartDate</span>
      <span class="nv">endDate</span><span class="pi">:</span> <span class="nv">scheduleEndDate</span>
    <span class="pi">}</span>
  <span class="pi">}</span>
<span class="err">}</span>
</code></pre></div></div>

<p>The above template creates an anomaly alert on a subscription scope. The parameters allow you to adjust the naming and information in your mail.
Using this Bicep in your subscription vending process will help your team members spot any anomalies in the costs of their subscriptions.</p>

<h2 id="budget-alerting">Budget Alerting</h2>

<p>Next to anomaly alerts, there is also the option to configure budget alerts. This alert type allows you to set up alerts based on actual costs and forecasted costs.</p>

<blockquote>
  <p><strong>Note</strong>: The alerts you configure do not affect resources. Alerts are only an option to keep you informed.</p>
</blockquote>

<p>What is essential to know about the budgets is that cost and usage data is normally available within 8-24 hours, and budgets are evaluated against these costs every 24 hours. Usually, when an alert needs to be sent out, it will be sent out within the hour.</p>

<p>Budget alerts can be specified on many different scopes. The most well-known scopes are management groups, subscriptions, and resource groups, but there are also other scopes, such as EA accounts. More info about the scopes can be found here: <a href="https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/tutorial-acm-create-budgets?tabs=psbudget&amp;WT.mc_id=AZ-MVP-5004255#prerequisites">Microsoft Learn</a></p>

<p>Setting up these alerts is easy and can be done quickly. These steps can be performed from the specific scope but can also be started from the cost management blade.</p>

<p><img src="/assets/images/2025/budget-alert.png" alt="Budget Alert" /></p>

<ol>
  <li>Click on ‘Add’ to create a new budget alert.</li>
  <li>Specify the scope of your budget alert. Be sure to also look into the filter capabilities within the filter options. You can, for example, create budget alerts for all resources with a specific tag value.</li>
</ol>

<p><img src="/assets/images/2025/budget-scoping.png" alt="Budget Scoping" /></p>

<ol start="3">
  <li>Fill in the budget details and amount. The system will alert you against this amount. Also, watch the suggestions the platform gives you.</li>
</ol>

<p><img src="/assets/images/2025/budget-details.png" alt="Budget Details" /></p>

<ol start="4">
  <li>Now, the alert conditions can be specified. The alert conditions specify when alerts will be sent. Here you can select if you want to alert on actual costs or on forecasted costs. Alerts are sent based on a percentage of your costs on the specified scope. An action group can be selected for the rule as a last option. This gives the possibility to notify specific people on specific thresholds.</li>
</ol>

<blockquote>
  <p><strong>Note</strong>: You can notify people with a specific RBAC role on the scope using the action group capability. This way, you can directly inform people who can take action!</p>
</blockquote>

<p><img src="/assets/images/2025/budget-alert-config.png" alt="Budget Alert configuration" /></p>

<ol start="5">
  <li>Click Create to save the budget alert</li>
</ol>

<h3 id="bicep-budget-alert">Bicep Budget Alert</h3>

<p>As with the anomaly alerts, it is great that you can configure them via the portal; it is even better if you can automate this within your existing processes.</p>

<p>To automate this, we will dive into the Bicep code again.</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">targetScope = 'subscription'</span>

<span class="err">@</span><span class="s">description('Name of the budget.')</span>
<span class="s">param name string</span>

<span class="err">@</span><span class="s">description('The total amount of cost or usage to track with the budget')</span>
<span class="s">param amount int = </span><span class="m">1000</span>

<span class="err">@</span><span class="s">description('The time covered by a budget. Tracking of the amount will be reset based on the time grain.')</span>
<span class="err">@</span><span class="s">allowed([</span>
  <span class="s">'Monthly'</span>
  <span class="s">'Quarterly'</span>
  <span class="s">'Annually'</span>
<span class="err">]</span><span class="s">)</span>
<span class="s">param timeGrain string = 'Monthly'</span>

<span class="err">@</span><span class="s">description('The start date must be first of the month in YYYY-MM-DD format.')</span>
<span class="s">param startDate string = utcNow('yyyy-MM-ddTHH:00Z')</span>

<span class="err">@</span><span class="s">description('The last day the schedule should run. Default = 10 year from start date.')</span>
<span class="s">param endDate string = dateTimeAdd(startDate, 'P10Y')</span>

<span class="err">@</span><span class="s">description('The notifications associated with a budget.')</span>
<span class="s">param notifications object</span>

<span class="err">@</span><span class="s">description('The first threshold amount that triggers the budget notification.')</span>
<span class="s">param filter object</span>

<span class="s">resource budget 'Microsoft.Consumption/budgets@2024-08-01' = {</span>
  <span class="s">name</span><span class="err">:</span> <span class="s">name</span>
  <span class="s">properties</span><span class="err">:</span> <span class="pi">{</span>
    <span class="nv">timePeriod</span><span class="pi">:</span> <span class="pi">{</span>
      <span class="nv">startDate</span><span class="pi">:</span> <span class="nv">startDate</span>
      <span class="nv">endDate</span><span class="pi">:</span> <span class="nv">endDate</span>
    <span class="pi">}</span>
    <span class="nv">timeGrain</span><span class="pi">:</span> <span class="nv">timeGrain</span>
    <span class="nv">amount</span><span class="pi">:</span> <span class="nv">amount</span>
    <span class="nv">category</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Cost'</span>
    <span class="nv">notifications</span><span class="pi">:</span> <span class="nv">notifications</span>
    <span class="nv">filter</span><span class="pi">:</span><span class="nv">filter</span>
  <span class="pi">}</span>
<span class="err">}</span>
</code></pre></div></div>

<p>The bicep file itself is very simple. This template’s scope is also a subscription, but it can be any of the other allowed scopes.</p>

<p>Most of the logic for this template is placed into parameters so that the template itself can be reused. Using this option, it is kind of hard to let end users manage it, but technical people should be able to work with it.</p>

<p>Take a look at the following bicep parameters in the below snippet.</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">using './budget-alert.bicep'</span>

<span class="s">param name = 'Bicep Budget Alert 2'</span>
<span class="s">param amount = </span><span class="m">1000</span>
<span class="s">param timeGrain = 'Monthly'</span>
<span class="s">param notifications = {</span>
  <span class="s">budgetone</span><span class="err">:</span> <span class="pi">{</span>
    <span class="nv">enabled</span><span class="pi">:</span> <span class="nv">true</span>
    <span class="nv">operator</span><span class="pi">:</span> <span class="s1">'</span><span class="s">GreaterThan'</span>
    <span class="nv">threshold</span><span class="pi">:</span> <span class="nv">80</span>
    <span class="nv">contactEmails</span><span class="pi">:</span> <span class="pi">[</span>
      <span class="s1">'</span><span class="s">maik@familie-vandergaag.nl'</span>
    <span class="pi">]</span>
    <span class="nv">thresholdType</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Actual'</span>
  <span class="pi">}</span>
  <span class="na">budgettwo</span><span class="pi">:</span> <span class="pi">{</span>
    <span class="nv">enabled</span><span class="pi">:</span> <span class="nv">true</span>
    <span class="nv">operator</span><span class="pi">:</span> <span class="s1">'</span><span class="s">GreaterThan'</span>
    <span class="nv">threshold</span><span class="pi">:</span> <span class="nv">100</span>
    <span class="nv">contactEmails</span><span class="pi">:</span> <span class="pi">[</span>
      <span class="s1">'</span><span class="s">maik@familie-vandergaag.nl'</span>
    <span class="pi">]</span>
    <span class="nv">thresholdType</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Forecasted'</span>
  <span class="pi">}</span>
<span class="err">}</span>
<span class="s">param filter = {</span>
  <span class="s">and</span><span class="err">:</span> <span class="pi">[</span>
    <span class="pi">{</span>
      <span class="nv">dimensions</span><span class="pi">:</span> <span class="pi">{</span>
        <span class="nv">name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">ResourceGroupName'</span>
        <span class="nv">operator</span><span class="pi">:</span> <span class="s1">'</span><span class="s">In'</span>
        <span class="nv">values</span><span class="pi">:</span> <span class="pi">[</span>
          <span class="s1">'</span><span class="s">rg-'</span>
        <span class="pi">]</span>
      <span class="pi">}</span>
    <span class="pi">}</span>
    <span class="pi">{</span>

     <span class="nv">tags</span><span class="pi">:</span> <span class="pi">{</span>
        <span class="nv">name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">environment'</span>
        <span class="nv">operator</span><span class="pi">:</span> <span class="s1">'</span><span class="s">In'</span>
        <span class="nv">values</span><span class="pi">:</span> <span class="pi">[</span>
          <span class="s1">'</span><span class="s">prd'</span>
        <span class="pi">]</span>
      <span class="pi">}</span>
    <span class="pi">}</span>
  <span class="pi">]</span>
<span class="err">}</span>
</code></pre></div></div>

<p>In the bicep parameter files, the notifications and filters are configured.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Optimizing budget configurations is crucial for monitoring your cloud usage and costs.</p>

<p>As you have read in this article, you should start by using automation and utilizing Infrastructure as Code to achieve efficient cost Management.</p>

<p>For further reading about this subject and to check out the source code shared in this post, look at the links below.</p>

<ul>
  <li><a href="https://github.com/maikvandergaag/msft-bicep">Personal Bicep GitHub Repo</a></li>
  <li><a href="https://learn.microsoft.com/en-us/azure/cost-management-billing/costs/?WT.mc_id=AZ-MVP-5004255">Cost Management - Microsoft Learn</a></li>
</ul>]]></content><author><name>Maik van der Gaag</name></author><category term="Azure" /><category term="Cost Management" /><category term="IaC" /><category term="Bicep" /><category term="Azure" /><category term="GitHub" /><summary type="html"><![CDATA[This is my involvement in Azure Spring Clean 2025, organized by Thomas Thornton, which I`m excited to be part of! For those unaware, the event promotes and encourages well-managed Azure tenants. It is held yearly in March, and over a couple of days, best practices and information are shared. You can find more information here.]]></summary></entry><entry><title type="html">Bicep for graph resources</title><link href="https://msftplayground.com/2025/01/bicep-for-graph-resources" rel="alternate" type="text/html" title="Bicep for graph resources" /><published>2025-02-13T01:00:00+01:00</published><updated>2025-02-13T01:00:00+01:00</updated><id>https://msftplayground.com/2025/01/bicep-for-graph-resources</id><content type="html" xml:base="https://msftplayground.com/2025/01/bicep-for-graph-resources"><![CDATA[<p>For the Bicep language, an extensibility framework exists that adds capabilities to extend Infrastructure as Code deployments to other resources and providers. The extensibility framework is still an experimental feature, so it is subject to change and needs to be enabled separately.</p>

<p>Add the following snippet to your Bicep configuration file (bicepconfig.json) to enable this experimental feature.</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">    </span><span class="nl">"experimentalFeaturesEnabled"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"extensibility"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>At the moment of writing this blog post there is support for the Microsoft Graph and Kubernetes.</p>

<p>Using the Graph extension for Bicep allows you to deploy and change a limited set of Entra objects (at the moment of writing). The objects that can be authored at the moment of writing are:</p>

<ul>
  <li>Applications</li>
  <li>App role assignments</li>
  <li>Federated identity assignment</li>
  <li>Groups</li>
  <li>OAuth2 Permission grants</li>
  <li>Service Principals</li>
  <li>Users</li>
</ul>

<p><img src="/assets/images/2025/graph-extension.png" alt="Graph Extension" /></p>

<p>As you might expect, the extension would call the Graph API from the machine where the deployment is executed. But the Bicep extension works a little bit differently.</p>

<p>As with the regular Bicep files without the extensions, the file is built into a JSON-represented format (locally). As you can see in the above image, this file is then sent to the Azure Resource Manager. The deployment agent then decides whether to send the resources to the Azure Resource Manager or the Graph API for the Graph resources.</p>

<p>To use the graph resources, you need to do two things. The extension alias needs to be configured within the ‘bicepconfig.json.’</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w">    </span><span class="nl">"extensions"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"microsoftGraphV1"</span><span class="p">:</span><span class="w"> </span><span class="s2">"br:mcr.microsoft.com/bicep/extensions/microsoftgraph/v1.0:0.1.9-preview"</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>‘microsoftGraphV1’ is the alias that needs to be used and included in the template file. To use the resources within that file, include the following line.</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">extension microsoftGraphV1</span>
</code></pre></div></div>

<p>With all of this setup, the Graph objects can be specified. When using Visual Studio Code, the Bicep extension also has intelligence to get you started.</p>

<p><img src="/assets/images/2025/bicep-extension.png" alt="IntelliSense" /></p>

<p>For this blog post, we will create an Entra group, add members and owners, and give that group permission on an Azure resource.
Adding members and owners to a group normally uses the users’ IDs. However, as we want to parameterize the file so that other people can use it, we will have to retrieve the users’ IDs using their UPN.</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">param location string = resourceGroup().location</span>

<span class="s">param members array = []</span>

<span class="s">param owners array = []</span>

<span class="s">param name string = 'default'</span>

<span class="s">var readerRole = '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1'</span>

<span class="s">var roleAssignmentName = guid(resourceGroup().id, groupName, storageName)</span>

<span class="s">var groupName = 'sg-${name}'</span>

<span class="s">var storageName = 'stgraphtest${name}'</span>
</code></pre></div></div>

<p>This first section shows the parameters and variables we will use during the deployment. As you can see, there is an owners and members array, which specifies the users’ UPNs.
The other variables and parameters specify the name and role we will use during the deployment.</p>

<p>The second part is the logic that will be used to retrieve the IDs of the owners and members based on their UPN. As mentioned above, you need to supply their IDs to add members and owners to the group.</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">resource ownerList 'Microsoft.Graph/users@v1.0' existing = [for upn in owners</span><span class="err">:</span> <span class="pi">{</span>
  <span class="nv">userPrincipalName</span><span class="pi">:</span> <span class="nv">upn</span>
<span class="pi">}</span><span class="err">]</span>

<span class="s">resource memberList 'Microsoft.Graph/users@v1.0' existing = [for upn in members</span><span class="err">:</span> <span class="pi">{</span>
  <span class="nv">userPrincipalName</span><span class="pi">:</span> <span class="nv">upn</span>
<span class="pi">}</span><span class="err">]</span>
</code></pre></div></div>

<p>The group can be created with the users’ information. The snippet below shows that the owners and members have been added, and the group name has been specified.</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">resource readerGroup 'Microsoft.Graph/groups@v1.0' = {</span>
  <span class="s">displayName</span><span class="err">:</span> <span class="s">groupName</span>
  <span class="s">mailEnabled</span><span class="err">:</span> <span class="no">false</span>
  <span class="s">mailNickname</span><span class="err">:</span> <span class="s">uniqueString(groupName)</span>
  <span class="s">securityEnabled</span><span class="err">:</span> <span class="no">true</span>
  <span class="s">uniqueName</span><span class="err">:</span> <span class="s">groupName</span>
  <span class="s">owners</span><span class="err">:</span> <span class="pi">[</span><span class="nv">for i in range(0</span><span class="pi">,</span> <span class="nv">length(owners))</span><span class="pi">:</span> <span class="nv">ownerList</span><span class="pi">[</span><span class="nv">i</span><span class="pi">]</span><span class="nv">.id</span><span class="pi">]</span>
  <span class="na">members</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">for i in range(0</span><span class="pi">,</span> <span class="nv">length(members))</span><span class="pi">:</span> <span class="nv">memberList</span><span class="pi">[</span><span class="nv">i</span><span class="pi">]</span><span class="nv">.id</span><span class="pi">]</span>
<span class="err">}</span>
</code></pre></div></div>

<p>The other parts needed for this deployment are just regular Bicep / Azure deployment of resources.</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">resource storage 'Microsoft.Storage/storageAccounts@2023-05-01' = {</span>
  <span class="s">name</span><span class="err">:</span> <span class="s">storageName</span>
  <span class="s">location</span><span class="err">:</span> <span class="s">location</span>
  <span class="s">sku</span><span class="err">:</span> <span class="pi">{</span>
    <span class="nv">name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Standard_LRS'</span>
  <span class="pi">}</span>
  <span class="na">kind</span><span class="pi">:</span> <span class="s1">'</span><span class="s">StorageV2'</span>
  <span class="s">properties:{</span>
    <span class="s">minimumTlsVersion</span><span class="err">:</span> <span class="s1">'</span><span class="s">TLS1_2'</span>
  <span class="err">}</span>
<span class="err">}</span>

<span class="s">resource readersRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {</span>
  <span class="s">scope</span><span class="err">:</span> <span class="s">storage</span>
  <span class="s">name</span><span class="err">:</span> <span class="s">roleAssignmentName</span>
  <span class="s">properties</span><span class="err">:</span> <span class="pi">{</span>
    <span class="nv">principalId</span><span class="pi">:</span> <span class="nv">readerGroup.id</span>
    <span class="nv">roleDefinitionId</span><span class="pi">:</span> <span class="nv">resourceId('Microsoft.Authorization/roleDefinitions'</span><span class="pi">,</span> <span class="nv">readerRole)</span>
  <span class="pi">}</span>
<span class="err">}</span>
</code></pre></div></div>

<p>As you can see in the above snippet, a storage account is created, and a role is assigned to the security group we previously added.</p>

<p>All of this together creates the following deployment file.</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">extension microsoftGraphV1</span>

<span class="s">param location string = resourceGroup().location</span>

<span class="s">param owners array = []</span>
<span class="s">param members array = []</span>

<span class="s">param name string = 'default'</span>

<span class="s">var readerRole = '2a2b9908-6ea1-4ae2-8e65-a410df84e7d1'</span>

<span class="s">var roleAssignmentName = guid(resourceGroup().id, groupName, storageName)</span>

<span class="s">var groupName = 'sg-${name}'</span>

<span class="s">var storageName = 'stgraphtest${name}'</span>

<span class="s">resource ownerList 'Microsoft.Graph/users@v1.0' existing = [for upn in owners</span><span class="err">:</span> <span class="pi">{</span>
  <span class="nv">userPrincipalName</span><span class="pi">:</span> <span class="nv">upn</span>
<span class="pi">}</span><span class="err">]</span>

<span class="s">resource memberList 'Microsoft.Graph/users@v1.0' existing = [for upn in members</span><span class="err">:</span> <span class="pi">{</span>
  <span class="nv">userPrincipalName</span><span class="pi">:</span> <span class="nv">upn</span>
<span class="pi">}</span><span class="err">]</span>

<span class="s">resource storage 'Microsoft.Storage/storageAccounts@2023-05-01' = {</span>
  <span class="s">name</span><span class="err">:</span> <span class="s">storageName</span>
  <span class="s">location</span><span class="err">:</span> <span class="s">location</span>
  <span class="s">sku</span><span class="err">:</span> <span class="pi">{</span>
    <span class="nv">name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Standard_LRS'</span>
  <span class="pi">}</span>
  <span class="na">kind</span><span class="pi">:</span> <span class="s1">'</span><span class="s">StorageV2'</span>
  <span class="s">properties:{</span>
    <span class="s">minimumTlsVersion</span><span class="err">:</span> <span class="s1">'</span><span class="s">TLS1_2'</span>
  <span class="err">}</span>
<span class="err">}</span>

<span class="s">resource readerGroup 'Microsoft.Graph/groups@v1.0' = {</span>
  <span class="s">displayName</span><span class="err">:</span> <span class="s">groupName</span>
  <span class="s">mailEnabled</span><span class="err">:</span> <span class="no">false</span>
  <span class="s">mailNickname</span><span class="err">:</span> <span class="s">uniqueString(groupName)</span>
  <span class="s">securityEnabled</span><span class="err">:</span> <span class="no">true</span>
  <span class="s">uniqueName</span><span class="err">:</span> <span class="s">groupName</span>
  <span class="s">owners</span><span class="err">:</span> <span class="pi">[</span><span class="nv">for i in range(0</span><span class="pi">,</span> <span class="nv">length(owners))</span><span class="pi">:</span> <span class="nv">ownerList</span><span class="pi">[</span><span class="nv">i</span><span class="pi">]</span><span class="nv">.id</span><span class="pi">]</span>
  <span class="na">members</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">for i in range(0</span><span class="pi">,</span> <span class="nv">length(owners))</span><span class="pi">:</span> <span class="nv">memberList</span><span class="pi">[</span><span class="nv">i</span><span class="pi">]</span><span class="nv">.id</span><span class="pi">]</span>
<span class="err">}</span>

<span class="s">resource readersRoleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {</span>
  <span class="s">scope</span><span class="err">:</span> <span class="s">storage</span>
  <span class="s">name</span><span class="err">:</span> <span class="s">roleAssignmentName</span>
  <span class="s">properties</span><span class="err">:</span> <span class="pi">{</span>
    <span class="nv">principalId</span><span class="pi">:</span> <span class="nv">readerGroup.id</span>
    <span class="nv">roleDefinitionId</span><span class="pi">:</span> <span class="nv">resourceId('Microsoft.Authorization/roleDefinitions'</span><span class="pi">,</span> <span class="nv">readerRole)</span>
  <span class="pi">}</span>
<span class="err">}</span>
</code></pre></div></div>

<p>As you can see, the extension model makes it very easy to create more advanced deployments by including the capabilities for Entra objects. For the deployment above, the parameter file could look like this.</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">using './main.bicep'</span>

<span class="s">param owners = ['maik@msftplayground.info' ]</span>
<span class="s">param members = [ 'piet@msftplayground.info'</span>
 <span class="s">'charlotte@msftplayground.info'</span>
<span class="err">]</span>
<span class="s">param name = 'graph-group-name'</span>

</code></pre></div></div>

<p>It would be great if, over time, many other graph objects are added to the extension so that we could also author SharePoint sites or other M365 / Entra objects.</p>

<p>If you want to learn more about the Graph extension or Bicep, you can have a look at the following resources:</p>

<ul>
  <li><a href="https://github.com/maikvandergaag/msft-bicep">msft-bicep</a> - My personal bicep repository that contains a lot of bicep samples and also includes examples regarding the graph extension.</li>
  <li><a href="https://learn.microsoft.com/en-us/graph/templates/?WT.mc_id=AZ-MVP-5004255">Bicep templates for Microsoft Graph</a> - The official Microsoft learn documentation regarding the Graph extension for Bicep.</li>
</ul>]]></content><author><name>Maik van der Gaag</name></author><category term="Bicep" /><category term="Development" /><category term="IaC" /><category term="Bicep" /><category term="Azure" /><category term="Graph" /><category term="GitHub" /><summary type="html"><![CDATA[For the Bicep language, an extensibility framework exists that adds capabilities to extend Infrastructure as Code deployments to other resources and providers. The extensibility framework is still an experimental feature, so it is subject to change and needs to be enabled separately.]]></summary></entry><entry><title type="html">A Bicep linting action for GitHub</title><link href="https://msftplayground.com/2025/01/bicep-linting-action" rel="alternate" type="text/html" title="A Bicep linting action for GitHub" /><published>2025-01-14T01:00:00+01:00</published><updated>2025-01-14T01:00:00+01:00</updated><id>https://msftplayground.com/2025/01/bicep-linting-action</id><content type="html" xml:base="https://msftplayground.com/2025/01/bicep-linting-action"><![CDATA[<p>As Infrastructure as Code (IaC) practices continue to evolve, maintaining clean and error-free code is crucial for seamless deployments. For this process, I have been working on a composite action that I could use to scan my own Bicep files.</p>

<p>After having a first version and finding some shortcomings in the implementation:</p>

<ul>
  <li>It constantly scans all files within the repository.</li>
  <li>When setting the output for the bicep linter to SARIF, I get a SARIF file for each file I scan.</li>
</ul>

<p>This made me make a new version in which these shortcomings are fixed, and you can adjust the behavior based on input parameters. This ended in the composite action below.</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Bicep</span><span class="nv"> </span><span class="s">Linting"</span>
<span class="na">description</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Composite</span><span class="nv"> </span><span class="s">action</span><span class="nv"> </span><span class="s">for</span><span class="nv"> </span><span class="s">running</span><span class="nv"> </span><span class="s">Bicep</span><span class="nv"> </span><span class="s">Linting."</span>
<span class="na">branding</span><span class="pi">:</span>
  <span class="na">icon</span><span class="pi">:</span> <span class="s1">'</span><span class="s">code'</span>
  <span class="na">color</span><span class="pi">:</span> <span class="s1">'</span><span class="s">blue'</span>
<span class="na">inputs</span><span class="pi">:</span>
  <span class="na">allfiles</span><span class="pi">:</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">boolean</span>
    <span class="na">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Check</span><span class="nv"> </span><span class="s">all</span><span class="nv"> </span><span class="s">files</span><span class="nv"> </span><span class="s">or</span><span class="nv"> </span><span class="s">only</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">changed</span><span class="nv"> </span><span class="s">files'</span>
    <span class="na">default</span><span class="pi">:</span> <span class="no">false</span>
  <span class="na">create-sarif</span><span class="pi">:</span>
    <span class="na">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Create</span><span class="nv"> </span><span class="s">a</span><span class="nv"> </span><span class="s">combined</span><span class="nv"> </span><span class="s">SARIF</span><span class="nv"> </span><span class="s">file'</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">boolean</span>
    <span class="na">default</span><span class="pi">:</span> <span class="no">true</span>
  <span class="na">markdown-report</span><span class="pi">:</span>
    <span class="na">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Create</span><span class="nv"> </span><span class="s">a</span><span class="nv"> </span><span class="s">markdown</span><span class="nv"> </span><span class="s">report'</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">boolean</span>
    <span class="na">default</span><span class="pi">:</span> <span class="no">false</span>
  <span class="na">sarif-output-path</span><span class="pi">:</span>
    <span class="na">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">The</span><span class="nv"> </span><span class="s">file</span><span class="nv"> </span><span class="s">path</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">save</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">SARIF</span><span class="nv"> </span><span class="s">file'</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">default</span><span class="pi">:</span> <span class="s1">'</span><span class="s">bicep-lint.sarif'</span>
  <span class="na">markdown-output-path</span><span class="pi">:</span>
    <span class="na">description</span><span class="pi">:</span> <span class="s1">'</span><span class="s">The</span><span class="nv"> </span><span class="s">file</span><span class="nv"> </span><span class="s">path</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">save</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">markdown</span><span class="nv"> </span><span class="s">report'</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">default</span><span class="pi">:</span> <span class="s1">'</span><span class="s">bicep-lint.md'</span>
<span class="na">runs</span><span class="pi">:</span>
  <span class="na">using</span><span class="pi">:</span> <span class="s2">"</span><span class="s">composite"</span>
  <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">fetch-depth</span><span class="pi">:</span> <span class="m">2</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Get files</span>
        <span class="na">shell</span><span class="pi">:</span> <span class="s">pwsh</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">$allFiles = [System.Convert]::ToBoolean("$")</span>
          <span class="s">$\Get-BicepFiles.ps1 -AllFiles $allFiles</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Linting</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">bicep</span><span class="nv"> </span><span class="s">files"</span>
        <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">$sarif = [System.Convert]::ToBoolean("$")</span>
          <span class="s">$markdown = [System.Convert]::ToBoolean("$")</span>
          <span class="s">$\Get-BicepLintingResults.ps1 -BicepFilesJson $ `</span>
                     <span class="s">-CreateSarif $sarif `</span>
                     <span class="s">-MarkdownReport $markdown `</span>
                     <span class="s">-SarifOutputPath $ `</span>
                     <span class="s">-MarkdownOutputPath $</span>
        <span class="na">shell</span><span class="pi">:</span> <span class="s">pwsh</span>
</code></pre></div></div>

<p>The action makes sure that the fetch dept of the repository is 2. This way, the action is able to retrieve the changed files. This happens in the step ‘Get files. ‘ The files it finds in this step are saved within an environment variable. Based on the input parameters, it will save the changed file or all the bicep files it can find.</p>

<p>The second step, ‘Linting the bicep files,’ will iterate through all the files and perform the ‘bicep lint’ command for each one. The command can optionally output to SARIF when the ‘create-sarif’ input parameter is set. It will combine the files into one large SARIF file.</p>

<p>As this was a personal project and I was also interested in how actions end up within the GitHub Marketplace, I published the action to the GitHub Marketplace for everyone to use. The action can be found <a href="https://github.com/marketplace/actions/bicep-linting">here</a>.</p>

<h3 id="key-features">Key Features</h3>

<ol>
  <li>
    <p><strong>Scan Only Changed Files</strong>: This feature allows the action to lint only the modified files, saving time and resources by not re-linting unchanged files.</p>
  </li>
  <li>
    <p><strong>Combine SARIF Output</strong>: The action can combine the SARIF (Static Analysis Results Interchange Format) output from the Bicep linter into a single file. This makes integrating with GitHub’s code scanning alerts easier and provides a comprehensive view of all linting issues.</p>
  </li>
  <li>
    <p><strong>Markdown Reporting</strong>: One of the most exciting features is the ability to display the linting results in the GitHub Actions Step Summary. This provides a clear and concise report directly within the GitHub interface, making it easy to review and address issues.</p>
  </li>
</ol>

<h3 id="how-it-works">How It Works</h3>

<p>The Bicep Linting Action leverages the Bicep linter to analyze your Bicep files for potential issues. The results are then processed and presented in a user-friendly format. Here’s a brief overview of how to set up and use the action in your GitHub workflows:</p>

<ol>
  <li><strong>Setup the Action</strong>: Add the Bicep Linting Action to your workflow file.</li>
  <li><strong>Configure the Inputs</strong>: Specify the paths to your Bicep files and any additional options you want to use.</li>
  <li><strong>Run the Workflow</strong>: Trigger the workflow to lint your Bicep files and view the results in the GitHub Actions tab.</li>
</ol>

<h3 id="example-workflow">Example Workflow</h3>

<p>Below is an example of how to integrate the Bicep Linting Action into your GitHub workflow:</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Lint Bicep Files</span>

<span class="na">on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">push</span><span class="pi">,</span> <span class="nv">pull_request</span><span class="pi">]</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">lint</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Checkout repository</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v2</span>

      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Run Bicep Linting</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">maikvandergaag/bicep-linting-action@v1</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">files</span><span class="pi">:</span> <span class="s1">'</span><span class="s">**/*.bicep'</span>
          <span class="na">only-changed</span><span class="pi">:</span> <span class="no">true</span>
          <span class="na">sarif-output</span><span class="pi">:</span> <span class="s1">'</span><span class="s">bicep-lint-results.sarif'</span>
</code></pre></div></div>

<p>By using the GitHub events, you can also ensure that different types of scans (full / changed) are done on different events. The below example shows this by doing a full scan on a pull request when the target branch is the main branch. If this is not the case, it will only take the changes, for example, for simple commits.</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Bicep Linting</span>

<span class="na">on</span><span class="pi">:</span>
  <span class="na">push</span><span class="pi">:</span>
  <span class="na">pull_request</span><span class="pi">:</span>
  <span class="na">workflow_dispatch</span><span class="pi">:</span>


<span class="na">env</span><span class="pi">:</span>
 <span class="na">allFiles</span><span class="pi">:</span> <span class="s">${{ github.event_name == 'pull_request' || ( github.base_ref == 'main') }}</span>


<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">lint</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">maikvandergaag/action-biceplint@main</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">allfiles</span><span class="pi">:</span> <span class="s">${{ env.allFiles }}</span>
          <span class="na">create-sarif</span><span class="pi">:</span> <span class="no">true</span>
          <span class="na">markdown-report</span><span class="pi">:</span> <span class="no">false</span>
          <span class="na">sarif-output-path</span><span class="pi">:</span> <span class="s">bicep-lint.sarif</span>
      <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Upload SARIF file</span>
        <span class="na">if</span><span class="pi">:</span> <span class="s">always()</span>
        <span class="na">uses</span><span class="pi">:</span> <span class="s">github/codeql-action/upload-sarif@v3</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">sarif_file</span><span class="pi">:</span> <span class="s">bicep-lint.sarif</span>
          <span class="na">category</span><span class="pi">:</span> <span class="s">bicep-linting</span>
</code></pre></div></div>

<p>Doing full scans on pull requests can be a requirement, especially when you have a rule set ‘use-recent-api-versions’ to error, and you must check the files occasionally. So you could, for example, create a daily scan using this extension.</p>

<h2 id="conclusion">Conclusion</h2>

<p>The Bicep Linting GitHub Action is an incredibly personal project that can help maintain high-quality Bicep code. Focusing on changed files, combining SARIF outputs, and providing markdown reports streamlines the linting process and integrates seamlessly with your GitHub workflows. Try and experience the benefits of automated Bicep linting in your projects!</p>

<p>When you have feature requests or are having issues, please share these!</p>

<ul>
  <li><a href="https://github.com/marketplace/actions/bicep-linting">Github Bicep Linting Action</a></li>
  <li><a href="https://github.com/maikvandergaag/action-biceplint">Linting Action repository</a></li>
  <li><a href="https://github.com/maikvandergaag/action-biceplint/issues">Issues</a></li>
</ul>]]></content><author><name>Maik van der Gaag</name></author><category term="Bicep" /><category term="Development" /><category term="IaC" /><category term="Bicep" /><category term="Azure" /><category term="GitHub" /><summary type="html"><![CDATA[As Infrastructure as Code (IaC) practices continue to evolve, maintaining clean and error-free code is crucial for seamless deployments. For this process, I have been working on a composite action that I could use to scan my own Bicep files.]]></summary></entry><entry><title type="html">The Bicep deployer function</title><link href="https://msftplayground.com/2025/01/bicep-deployer-function" rel="alternate" type="text/html" title="The Bicep deployer function" /><published>2025-01-08T01:00:00+01:00</published><updated>2025-01-08T01:00:00+01:00</updated><id>https://msftplayground.com/2025/01/bicep-deployment-functions</id><content type="html" xml:base="https://msftplayground.com/2025/01/bicep-deployer-function"><![CDATA[<p>Bicep evolves weekly, and with every release, new functionality is added. With the release of <a href="https://github.com/Azure/bicep/releases/tag/v0.32.4">v0.32.4</a>, the last release of 2024, a cool new function was added.</p>

<h2 id="deployer-function">deployer function</h2>

<p>This newly added function helps get information about who is deploying the resources to the platform. This can, for example, help if there is a process within your organization to tag the resources with the person who deploys them.</p>

<p>Let’s look at an example by outputting the object retrieved by the deployer function.</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">output deployer object = deployer()</span>
</code></pre></div></div>

<p><img src="/assets/images/2025/deployer-output.png" alt="Bicep output deployer function" /></p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"objectId"</span><span class="p">:</span><span class="s2">"f275e010-27ad-4b3b-8c33-e9aac30fded4"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"tenantId"</span><span class="p">:</span><span class="s2">"324f7296-1869-4489-b11e-912351f38ead"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>As visible in the snippet above, this function returns the object id and the tenant id of the deploying principal. This information can then be used in other scenarios, like setting permissions or tagging resources. The snippet below shows the option of giving the deployer access to a resource group.</p>

<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">var principalId = deployer().objectId</span> 

<span class="s">resource contributorRoleDefinition 'Microsoft.Authorization/roleDefinitions@2022-04-01' existing = {</span>
  <span class="s">scope</span><span class="err">:</span> <span class="s">subscription()</span>
  <span class="s">name</span><span class="err">:</span> <span class="s1">'</span><span class="s">b24988ac-6180-42a0-ab88-20f7382dd24c'</span>
<span class="err">}</span>

<span class="s">resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {</span>
  <span class="s">name</span><span class="err">:</span> <span class="s">guid(resourceGroup().id, principalId, contributorRoleDefinition.id)</span>
  <span class="s">properties</span><span class="err">:</span> <span class="pi">{</span>
    <span class="nv">roleDefinitionId</span><span class="pi">:</span> <span class="nv">contributorRoleDefinition.id</span>
    <span class="nv">principalId</span><span class="pi">:</span> <span class="nv">principalId</span>
    <span class="nv">principalType</span><span class="pi">:</span> <span class="s1">'</span><span class="s">ServicePrincipal'</span>
  <span class="pi">}</span>
<span class="err">}</span>
</code></pre></div></div>

<p>Having the information available during deployment time and not having to retrieve it separately before the execution and supplying it as a parameter helps a lot and can help in situations where:</p>

<ul>
  <li>You need access to a newly deployed KeyVault with RBAC permissions configured, and also want to deploy secrets.</li>
  <li>Using the same identity for your deployment scripts.</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>The deployer function is a great new addition to the Bicep language and will help make advanced deployments much easier.</p>

<ul>
  <li><a href="https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/bicep-functions-deployment?WT.mc_id=AZ-MVP-5004255">Bicep deployment functions</a></li>
  <li><a href="https://github.com/maikvandergaag/msft-bicep/tree/main/examples/deployment">Bicep code in my GitHub Repository</a></li>
</ul>]]></content><author><name>Maik van der Gaag</name></author><category term="Bicep" /><category term="Development" /><category term="IaC" /><category term="Bicep" /><category term="Azure" /><summary type="html"><![CDATA[Bicep evolves weekly, and with every release, new functionality is added. With the release of v0.32.4, the last release of 2024, a cool new function was added.]]></summary></entry></feed>