<?xml version="1.0" encoding="utf-8" standalone="no"?><feed xmlns="http://www.w3.org/2005/Atom"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://blog.tyang.org/feed.xml" rel="self" type="application/atom+xml"/><link href="https://blog.tyang.org/" rel="alternate" type="text/html"/><updated>2026-04-05T21:26:23+10:00</updated><id>https://blog.tyang.org/feed.xml</id><title type="html">Managing Cloud and Datacenter by Tao Yang</title><subtitle>My thoughts on various Microsoft technologies</subtitle><author><name>Tao Yang</name></author><xhtml:meta content="noindex" name="robots" xmlns:xhtml="http://www.w3.org/1999/xhtml"/><entry><title type="html">Policy Integration Testing Framework in AzPolicyFactory</title><link href="https://blog.tyang.org/2026/04/05/policy-int-test-in-azpolicyfactory" rel="alternate" title="Policy Integration Testing Framework in AzPolicyFactory" type="text/html"/><published>2026-04-05T10:00:00+10:00</published><updated>2026-04-05T10:00:00+10:00</updated><id>https://blog.tyang.org/2026/04/05/policy-int-test-in-azpolicyfactory</id><content type="html" xml:base="https://blog.tyang.org/2026/04/05/policy-int-test-in-azpolicyfactory"><![CDATA[<p>One of the most challenging and time-consuming aspects of managing Azure Policy at scale is ensuring the policy does what it’s supposed to do without causing unexpected issues in the environment. This is especially important in large organizations with complex environments, where a single misconfiguration can have significant consequences.</p>

<p>In the past, I have seen customers doing this manually and capturing the evidence in a very ad-hoc way. Since the policy resources are constantly evolving in any Azure environments, the test effort should be continuous to ensure (1) new or updated policies are working as expected, and (2) the existing policies are still effective after any policy changes.</p>

<p>The Policy Integration Test is a framework that I have spent over 8 months developing to address this challenge. It provides an automated way to test policy resources in a real environment, and programmatically capture the evidence using standard testing framework <code class="language-plaintext highlighter-rouge">Pester</code>. The test cases can be automatically triggered when a PR is raised in your Policy IaC repository, or manually executed on demand.</p>

<p>The Policy Integration Test is an important part of the AzPolicyFactory solution, it is in fact the value proposition of the solution. It provides a comprehensive set of tests and validations at different stages of the CI/CD pipelines to ensure the quality and correctness of the Azure Policy resources being deployed.</p>

<p>Today, I have finished the migration of the existing AzPolicyFactory solution to the new repository in the Azure GitHub organization, together with the addition of the Policy Integration Test framework. You can find the new repository at <a href="https://aka.ms/AzPolicyFactory">aka.ms/AzPolicyFactory</a>. I have included very comprehensive documentation in the repository to help you get started with the Policy Integration Test framework.</p>

<p>The Policy Integration Test solution supports both Azure DevOps pipelines and GitHub Actions workflows.</p>

<p>It supports both Azure Bicep and Terraform in the test cases.</p>

<p>Here are some screenshots of the Policy Integration Test framework in action:</p>

<p><strong>Azure DevOps:</strong></p>

<p><img src="./../../../assets/images/2026/04/pol-int-test-ado-01.jpg" alt="1" /></p>

<p><img src="./../../../assets/images/2026/04/pol-int-test-ado-02.jpg" alt="2" /></p>

<p><strong>GitHub Action Workflow:</strong></p>

<p><img src="./../../../assets/images/2026/04/pol-int-test-gh-01.jpg" alt="3" /></p>

<p><img src="./../../../assets/images/2026/04/pol-int-test-gh-02.jpg" alt="4" /></p>

<p>You can find detailed documentation about Policy Integration Test or AzPolicyFactory solution as a whole in the GitHub repository.</p>

<p>P.S. Since the release of AzPolicyFactory solution last month, I have received a few requests for me to document the feature comparison between AzPolicyFactory and other popular open source solutions.</p>

<p>My focus has been and always will be building the AzPolicyFactory solution. I will <strong>not</strong> document such comparison. If you have any specific questions or are requesting that I produce detailed documentation on a specific topic related to AzPolicyFactory, please raise an issue in the GitHub repository.</p>

<p>You can also use your favourite AI model to summarize the differences for you as long as the solutions you want to compare are well-documented.</p>

<p>Hopefully as you can see, the focus on AzPolicyFactory is <strong>testing</strong> and <strong>validation</strong>. Deploying Azure Policy or any Azure resources is not hard, there are many ways to achieve this.</p>

<p>Looking back and seeing how much time I have spent on all the features in AzPolicyFactory (and the upcoming release of a brand new solution that I have planned), to me the policy resource deployment part of the solution is something I have spent maybe 5% of the entire time on.</p>

<p>Lastly, please feel free to raise issues in the GitHub repository if you have questions or run into issues.</p>

<p>Looking forward to seeing how the <code class="language-plaintext highlighter-rouge">AzPolicyFactory</code> solution can help you in managing Azure Policy at scale in your organization!</p>]]></content><author><name>Tao Yang</name></author><category term="Azure"/><category term="Azure"/><category term="Azure Policy"/><category term="DevOps"/><category term="Infrastructure as Code"/><summary type="html"><![CDATA[One of the most challenging and time-consuming aspects of managing Azure Policy at scale is ensuring the policy does what it’s supposed to do without causing unexpected issues in the environment. This is especially important in large organizations with complex environments, where a single misconfiguration can have significant consequences.]]></summary></entry><entry><title type="html">Deploying Azure Policy with Infrastructure as Code (IaC)</title><link href="https://blog.tyang.org/2026/03/08/deploying-azure-policy-iac" rel="alternate" title="Deploying Azure Policy with Infrastructure as Code (IaC)" type="text/html"/><published>2026-03-08T10:00:00+11:00</published><updated>2026-03-08T10:00:00+11:00</updated><id>https://blog.tyang.org/2026/03/08/azure-policy-iac</id><content type="html" xml:base="https://blog.tyang.org/2026/03/08/deploying-azure-policy-iac"><![CDATA[<h2 id="background">Background</h2>

<p>I have developed several Azure Policy-related solutions for various customers over the last 4-5 years. Some of these patterns have kept evolving over time.</p>

<p>I have decided to open source these solutions and share with the community. At the moment, there are at least 3 solutions that I have in mind.</p>

<p>This is considered the first installment of the series, which is a pattern I have developed for deploying Azure Policy with Infrastructure as Code (IaC) using Azure DevOps or GitHub Actions.</p>

<p><strong>AzPolicyFactory</strong> is a pattern I started building around 2021 and has gone through many iterations based on the feedback from customers and the changes in Azure Policy itself. I believe it has reached a mature level and provides a safe and efficient way to manage Azure Policy at scale, especially in large organizations with complex environments.</p>

<p>The next 2 patterns I’m planning to open source will work well with this pattern, and I will share more details about them in the future. Stay tuned!</p>

<h2 id="azpolicyfactory-introduction">AzPolicyFactory Introduction</h2>

<p><img src="./../../../assets/images/2026/03/azpolicyfactory-banner.png" alt="AzPolicyFactory" /></p>

<p><strong>AzPolicyFactory</strong> provides a comprehensive set of IaC solutions for testing, deploying and managing Azure Policy resources at scale.</p>

<p>By leveraging these IaC templates and pipelines, organizations can automate the deployment and management of Azure Policy resources, ensuring consistent governance across their Azure environments while reducing manual effort and the risk of misconfigurations.</p>

<p><strong>AzPolicyFactory</strong> ships with 4 separate but interconnected Azure DevOps pipelines and GitHub Actions workflows that cover the entire lifecycle of Azure Policy resources, including:</p>

<ul>
  <li><strong>Policy Definitions</strong></li>
  <li><strong>Policy Initiatives</strong></li>
  <li><strong>Policy Assignments</strong></li>
  <li><strong>Policy Exemptions</strong></li>
</ul>

<p>The solution automates the entire lifecycle of Azure Policy resources — from code commit through testing and validation to production deployment — ensuring quality and correctness at every stage.</p>

<p><img src="./../../../assets/images/2026/03/azpolicyfactory-high-level-process.png" alt="High-Level-Process" /></p>

<p>The Azure Policy IaC solution in this repository includes the following key features:</p>

<ul>
  <li>Supports both <strong>Azure DevOps pipelines</strong> and <strong>GitHub Actions workflows</strong> for maximum flexibility and compatibility with different CI/CD platforms.</li>
  <li>Comprehensive set of Bicep modules and templates for deploying Azure Policy resources, following best practices for modularity, reusability, and maintainability.</li>
  <li>Comprehensive set of tests and validation at different stages of the CI/CD pipelines to ensure the quality and correctness of the Azure Policy resources being deployed.</li>
  <li>Follows industry best practices for Azure Policy management, safe deployment, code scan, and PR validation to ensure that the Azure Policy resources are deployed in a secure and compliant manner.</li>
  <li>Unit tests for every policy resource being deployed.</li>
  <li>Policy Integration Test (coming soon) to validate the functionality and effectiveness of the deployed Azure Policy resources in enforcing the desired governance and compliance requirements.</li>
</ul>

<h2 id="learn-more">Learn More</h2>

<p>You can find the AzPolicyFactory solution in the following GitHub repository: <a href="https://aka.ms/AzPolicyFactory">AzPolicyFactory</a>. I have included very comprehensive documentation in the repository to help you get started.</p>

<h2 id="whats-next">What’s Next?</h2>

<p><del>Since I only joined Microsoft a little over a month ago, I am still finding my way around. My plan is to try to move this repository to the Azure GitHub organization. If I manage to do that, I will update this post with the new link. In the meantime, you can check out the repository in my personal GitHub account.</del></p>

<p><strong>2026-04-05 Update</strong>: I have successfully moved the repository to the Azure GitHub organization. You can now find it at <a href="https://aka.ms/AzPolicyFactory">aka.ms/AzPolicyFactory</a>. Unfortunately, it’s not move, but initialize a new repo and copy the code over. So the stars and forks are not moved. If you have already starred or forked the original repo, please update to the new one. I will archive the original repo soon.</p>

<p>Next, I’m planning to release a solution I spent over 8 months developing, which I completed about 18 months ago — a framework for Azure Policy integration testing. I’m in the process of finding a good place to release it within Microsoft. Hopefully it won’t be too long before I can share it with the community. Stay tuned!</p>]]></content><author><name>Tao Yang</name></author><category term="Azure"/><category term="Azure"/><category term="Azure Policy"/><category term="DevOps"/><category term="Infrastructure as Code"/><summary type="html"><![CDATA[Background]]></summary></entry><entry><title type="html">My New GitHub Account</title><link href="https://blog.tyang.org/2026/01/26/my-new-github-account/" rel="alternate" title="My New GitHub Account" type="text/html"/><published>2026-01-26T00:00:00+11:00</published><updated>2026-01-26T00:00:00+11:00</updated><id>https://blog.tyang.org/2026/01/26/my-new-github-account</id><content type="html" xml:base="https://blog.tyang.org/2026/01/26/my-new-github-account/"><![CDATA[<p>Since I have recently started a new role as a full-time Microsoft employee, my existing GitHub account <code class="language-plaintext highlighter-rouge">tyconsulting</code> that I have been using for 10+ years doesn’t sound appropriate since it was originally created for my consulting business.</p>

<p>I have decided to create a new GitHub account that better reflects my personal identity rather than continue using the old one. My new GitHub account is <a href="https://github.com/taoyangcloud"><strong>taoyangcloud</strong></a>. I have also created a new GitHub organization called <a href="https://github.com/taoyang-cloud"><strong>TaoYang-Cloud</strong></a> to host my open-source projects going forward.</p>

<p>I have transferred all of the repositories from my old account <code class="language-plaintext highlighter-rouge">tyconsulting</code> to the new organization <a href="https://github.com/taoyang-cloud">TaoYang-Cloud</a>. If you have bookmarked or stared any of my repos in the past, the link should still work as GitHub automatically redirects to the new location. However, if it doesn’t work, please go look for the repos under the new organization.</p>

<p>Sadly I will lose all the followers and achievements associated with my old account, but I believe this is a necessary step to align with my new career path.</p>

<p>I have also updated the links in all my blog posts that referenced my old GitHub account to point to the new account and organization.</p>

<p>Feel free to follow my new GitHub account and organization to stay updated with my latest open-source projects and contributions!</p>]]></content><author><name>Tao Yang</name></author><category term="GitHub"/><category term="GitHub"/><summary type="html"><![CDATA[Since I have recently started a new role as a full-time Microsoft employee, my existing GitHub account tyconsulting that I have been using for 10+ years doesn’t sound appropriate since it was originally created for my consulting business.]]></summary></entry><entry><title type="html">Career Update and MVP Retirement</title><link href="https://blog.tyang.org/2026/01/22/mvp-retirement" rel="alternate" title="Career Update and MVP Retirement" type="text/html"/><published>2026-01-22T10:00:00+11:00</published><updated>2026-01-22T10:00:00+11:00</updated><id>https://blog.tyang.org/2026/01/22/mvp-retirement</id><content type="html" xml:base="https://blog.tyang.org/2026/01/22/mvp-retirement"><![CDATA[<p>12 years is a long time. It’s been an absolute honor to be recognized and a part of the awesome Microsoft MVP program for such a long period. I have learned so much from my fellow MVPs, Microsoft product groups, and the broader tech community. The journey has been incredibly rewarding, and I am forever grateful for all the opportunities and experiences that came with being an MVP. More importantly, I have made so many good friends along the way and it’s likely we will stay connected for life!</p>

<p>12 years ago, after spending 4 years blogging and contributing to the Microsoft tech community, I started wondering where could this lead me. I started having this though in my head that “maybe I can become a Microsoft MVP one day just like the guys I looked up to. Maybe I can reach out to the MVPs I admire and ask them for advice on how to become a part of their elite group.” Funny enough, before I had the courage to reach out to them, one morning, I received an email from Microsoft informing me that I had been nominated by a MVP that I deeply admire. I felt extremely honoured and excited. long story short, after I completed the nomination process and waited for approx. 3 months, I got accepted! Since then, I have been re-awarded every year for 12 consecutive years.</p>

<p>Initially, I set a personal goal to maintain the MVP status for at least 5 years. then after 5 years, I made another goal to maintain in the program for as long as the MVP trophy itself has more space for the annual renewal discs.</p>

<p><img src="../../../../assets/images/2026/01/mvp-retire-01.jpg" alt="01" /></p>

<p>Right now, I am proud to say that I have accomplished my goal and filled all the available space on this trophy.</p>

<p>During my time, I have had the privilege to contribute to the community in various ways, including speaking at conferences and user groups, writing articles, YouTube videos, books and providing feedback to product groups, etc.. The MVP program has provided me with a platform to share my knowledge and passion for technology, and I am grateful for the recognition and support from Microsoft.</p>

<p>I also enjoyed the opportunities to meet the fellow MVPs and Microsoft product groups in person during MVP summits. The wonderful time we had in Seattle would always be cherished memories.</p>

<p>Every journey has its end. Back in November last year, I have accepted a full-time employee position at Microsoft Australia. After the Christmas holidays, I started the new role at Microsoft this week.</p>

<p>As a full-time Microsoft employee, I am no longer eligible to be an MVP. Therefore, with a mix of emotions, I have notified the MVP support team that I will be retiring from the MVP program.</p>

<p>I want to take this opportunity to express my heartfelt gratitude to the entire MVP community. Thank you for the support, friendship, and inspiration over the years.</p>

<p>Last but not least, I am still going to be active in the tech community, just in a different capacity now. I look forward to continuing to contribute and give back to the community in new ways as a Microsoft employee.</p>]]></content><author><name>Tao Yang</name></author><category term="Career"/><category term="MvP"/><category term="Career"/><category term="MVP"/><summary type="html"><![CDATA[12 years is a long time. It’s been an absolute honor to be recognized and a part of the awesome Microsoft MVP program for such a long period. I have learned so much from my fellow MVPs, Microsoft product groups, and the broader tech community. The journey has been incredibly rewarding, and I am forever grateful for all the opportunities and experiences that came with being an MVP. More importantly, I have made so many good friends along the way and it’s likely we will stay connected for life!]]></summary></entry><entry><title type="html">Using Hidden Tags For Managing Azure Bicep Modules</title><link href="https://blog.tyang.org/2025/08/19/using-hidden-tags-for-managing-azure-bicep-modules" rel="alternate" title="Using Hidden Tags For Managing Azure Bicep Modules" type="text/html"/><published>2025-08-19T00:00:00+10:00</published><updated>2025-08-19T00:00:00+10:00</updated><id>https://blog.tyang.org/2025/08/19/using-hidden-tags-for-managing-azure-bicep-modules</id><content type="html" xml:base="https://blog.tyang.org/2025/08/19/using-hidden-tags-for-managing-azure-bicep-modules"><![CDATA[<h2 id="background">Background</h2>
<p>Many customers have gone down the route of developing, publishing and sharing internally developed Azure IaC modules within the organization. The modules can be written in Bicep, or Terraform or other IaC languages.</p>

<p>The process is typically as follows:</p>

<ol>
  <li>Develop the module locally.</li>
  <li>Publish a “beta” or preview version of the module into a registry.</li>
  <li>Test the module in a staging environment before promoting it to production.</li>
  <li>Bump the version number and publish the final module.</li>
</ol>

<p>We have implemented internal Bicep module libraries leveraging existing now retired <a href="https://aka.ms/carml">Azure CARML</a> and it’s successor <a href="https://azure.github.io/Azure-Verified-Modules/">Azure Verified Modules</a> (AVM) library for several customers over the last few years.</p>

<p>Some common questions asked by the customers include:</p>

<ol>
  <li><strong>Module usage tracking</strong> - How do we know where the modules are being used? if we update or retire a module, we need to know who’s going to be impacted.</li>
  <li><strong>Module versioning control</strong> - How do we ensure only production-ready versions are being used in production environments? In another word, how do we prevent the use of <code class="language-plaintext highlighter-rouge">beta</code> or <code class="language-plaintext highlighter-rouge">pre-release</code> versions in production?</li>
</ol>

<p>To answer these questions and address the concern, we have come up with a pattern that involves the use of <code class="language-plaintext highlighter-rouge">hidden-</code> tags in the Bicep modules.</p>

<p>As you may know, if you create a tag with the <code class="language-plaintext highlighter-rouge">hidden-</code> prefix, the Azure Portal hides the tag (but it is still viewable via the ARM REST API). For example, this Storage Account has 2 hidden tags as you can see in the resource JSON view</p>

<p><img src="../../../../assets/images/2025/08/bicep-hidden-tags-01.jpg" alt="01" /></p>

<p>But they are hidden from the portal view:</p>

<p><img src="../../../../assets/images/2025/08/bicep-hidden-tags-02.jpg" alt="02" /></p>

<p>We created two hidden tags for every resource module. The <code class="language-plaintext highlighter-rouge">hidden-module_name</code> tag indicates the name of the module and the <code class="language-plaintext highlighter-rouge">hidden-module_version</code> tag indicates the complete semantic version of the module (major.minor.patch). If the module consumers don’t look close enough, they won’t notice these tags because they are hidden from the portal view.</p>

<p>To implement this in the bicep modules, we added the following code to the module (using storage account as an example):</p>

<div class="language-terraform highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">//tags parameter</span>
<span class="err">@</span><span class="nx">description</span><span class="err">(</span><span class="s1">'Optional. Tags of the resource.'</span><span class="err">)</span>
<span class="nx">param</span> <span class="nx">tags</span> <span class="nx">object</span><span class="err">?</span>

<span class="c1">//external version.json file that stores the version number (as a common pattern in AVM and CARML)</span>
<span class="kd">var</span> <span class="nx">moduleVersion</span> <span class="err">=</span> <span class="nx">loadJsonContent</span><span class="err">(</span><span class="s1">'./version.json'</span><span class="err">).</span><span class="nx">version</span>

<span class="c1">//combine the existing tags and hidden tags</span>
<span class="kd">var</span> <span class="nx">mergedTags</span> <span class="err">=</span> <span class="nx">union</span><span class="err">(</span><span class="nx">tags</span><span class="err">,</span> <span class="p">{</span>
    <span class="s1">'hidden-module_name'</span><span class="err">:</span> <span class="s1">'storage/storage-account'</span>
    <span class="s1">'hidden-module_version'</span><span class="err">:</span> <span class="nx">moduleVersion</span>
  <span class="p">}</span><span class="err">)</span>

<span class="c1">//pass the combined tags to the resource</span>
<span class="k">resource</span> <span class="nx">storageAccount</span> <span class="s1">'Microsoft.Storage/storageAccounts@2025-01-01'</span> <span class="err">=</span> <span class="p">{</span>
  <span class="nx">name</span><span class="err">:</span> <span class="s1">'mystorageaccount'</span>
  <span class="nx">location</span><span class="err">:</span> <span class="s1">'eastus'</span>
  <span class="nx">tags</span><span class="err">:</span> <span class="nx">mergedTags</span>
  <span class="p">...</span>
  <span class="p">...</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="module-usage-tracking">Module Usage Tracking</h2>

<p>After these hidden tags are embedded in each module, we can start tracking the usage of these modules across your organization using Azure Resource Graph.</p>

<p>Here are some sample queries you can use:</p>

<p><strong>Get all module usage</strong></p>

<div class="language-ocl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">resources</span>
<span class="o">|</span> <span class="n">where</span> <span class="n">tags</span><span class="p">[</span><span class="s1">'hidden-module_name'</span><span class="p">]</span> <span class="n">matches</span> <span class="n">regex</span> <span class="s1">'.'</span>
<span class="o">|</span> <span class="n">extend</span> <span class="n">module_name</span> <span class="o">=</span> <span class="n">tostring</span><span class="p">(</span><span class="n">tags</span><span class="p">[</span><span class="s1">'hidden-module_name'</span><span class="p">])</span>
<span class="o">|</span> <span class="n">extend</span> <span class="n">module_version</span> <span class="o">=</span> <span class="n">tostring</span><span class="p">(</span><span class="n">tags</span><span class="p">[</span><span class="s1">'hidden-module_version'</span><span class="p">])</span>
<span class="o">|</span> <span class="n">summarize</span> <span class="n">resource_count</span> <span class="o">=</span> <span class="nf">count</span><span class="p">()</span> <span class="n">by</span> <span class="n">type</span><span class="p">,</span> <span class="n">module_name</span><span class="p">,</span> <span class="n">module_version</span>
</code></pre></div></div>

<p><img src="../../../../assets/images/2025/08/bicep-hidden-tags-03.jpg" alt="03" /></p>

<blockquote>
  <p>Note: repeat this query for other Azure Resource Graph tables because not everything is stored in the <code class="language-plaintext highlighter-rouge">resources</code> table. Refer to <a href="https://learn.microsoft.com/en-us/azure/governance/resource-graph/reference/supported-tables-resources">this article</a> for the details on ARG tables.</p>
</blockquote>

<p><strong>List all storage accounts deployed by the storage module with module version, <code class="language-plaintext highlighter-rouge">owner</code> and <code class="language-plaintext highlighter-rouge">environment</code> tag values</strong></p>

<div class="language-ocl highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">resources</span>
<span class="o">|</span> <span class="n">where</span> <span class="n">type</span> <span class="o">=~</span> <span class="err">"</span><span class="n">microsoft</span><span class="p">.</span><span class="n">storage</span><span class="o">/</span><span class="n">storageAccounts</span><span class="err">"</span>
<span class="o">|</span> <span class="n">where</span> <span class="n">tags</span><span class="p">[</span><span class="s1">'hidden-module_name'</span><span class="p">]</span> <span class="n">contains</span> <span class="s1">'storage'</span>
<span class="o">|</span> <span class="n">extend</span> <span class="n">module_name</span> <span class="o">=</span> <span class="n">tostring</span><span class="p">(</span><span class="n">tags</span><span class="p">[</span><span class="s1">'hidden-module_name'</span><span class="p">])</span>
<span class="o">|</span> <span class="n">extend</span> <span class="n">owner</span><span class="o">=</span><span class="n">tostring</span><span class="p">(</span><span class="n">tags</span><span class="p">[</span><span class="s1">'owner'</span><span class="p">])</span>
<span class="o">|</span> <span class="n">extend</span> <span class="n">environment</span><span class="o">=</span><span class="n">tostring</span><span class="p">(</span><span class="n">tags</span><span class="p">[</span><span class="s1">'environment'</span><span class="p">])</span>
<span class="o">|</span> <span class="n">project</span> <span class="n">name</span><span class="p">,</span> <span class="n">tags</span><span class="p">,</span> <span class="n">module_name</span><span class="p">,</span> <span class="n">environment</span><span class="p">,</span> <span class="n">owner</span>
<span class="o">|</span> <span class="n">mvexpand</span> <span class="n">tags</span>
<span class="o">|</span> <span class="n">extend</span> <span class="n">tagKey</span> <span class="o">=</span> <span class="n">tostring</span><span class="p">(</span><span class="n">bag_keys</span><span class="p">(</span><span class="n">tags</span><span class="p">)[</span><span class="mi">0</span><span class="p">])</span>
<span class="o">|</span> <span class="n">extend</span> <span class="n">tagValue</span> <span class="o">=</span> <span class="n">tostring</span><span class="p">(</span><span class="n">tags</span><span class="p">[</span><span class="n">tagKey</span><span class="p">])</span>
<span class="o">|</span> <span class="n">distinct</span> <span class="n">name</span><span class="p">,</span> <span class="n">tagKey</span><span class="p">,</span> <span class="n">tagValue</span><span class="p">,</span> <span class="n">module_name</span><span class="p">,</span> <span class="n">owner</span><span class="p">,</span> <span class="n">environment</span>
<span class="o">|</span> <span class="n">where</span> <span class="n">tagKey</span> <span class="o">=~</span> <span class="err">"</span><span class="n">hidden</span><span class="o">-</span><span class="n">module_version</span><span class="err">"</span>
<span class="o">|</span> <span class="n">project</span> <span class="n">resourceName</span> <span class="o">=</span> <span class="n">name</span><span class="p">,</span> <span class="n">module_name</span><span class="p">,</span> <span class="n">module_version</span> <span class="o">=</span> <span class="n">tagValue</span><span class="p">,</span> <span class="n">owner</span><span class="p">,</span> <span class="n">environment</span>
</code></pre></div></div>

<p><img src="../../../../assets/images/2025/08/bicep-hidden-tags-04.jpg" alt="04" /></p>

<h2 id="module-versioning-control">Module Versioning Control</h2>

<p>If the AVM / CARML pattern is being used, we need to firstly understand how the module version numbers are constructed.</p>

<p>In AVM / CARML, each module has a <code class="language-plaintext highlighter-rouge">version.json</code> file in the same directory of the module bicep file which contains the <code class="language-plaintext highlighter-rouge">major.minor</code> version number.</p>

<p>The patch version is generated by the pipeline at the module publish time. The pipeline then combines the <code class="language-plaintext highlighter-rouge">major.minor</code> version from the <code class="language-plaintext highlighter-rouge">version.json</code> file with the patch number it generated. When a module is published from the <code class="language-plaintext highlighter-rouge">main</code> or <code class="language-plaintext highlighter-rouge">master</code> branches, the final version number is the one this combined version number.</p>

<p>When the module is published from another branch (i.e. a feature branch for adding new features or a bugfix branch for fixing a bug), the pipeline appends <code class="language-plaintext highlighter-rouge">-prerelease</code> to the version number.</p>

<p>The purpose of the <code class="language-plaintext highlighter-rouge">-prerelease</code> versions are for you to conduct integration testing in staging environments. These version have not gone through the code review and PR process, the code has not been merged to the main branch, therefore they are not production ready.</p>

<p>To block the use of <code class="language-plaintext highlighter-rouge">-prerelease</code> versions in production, We have created an Azure Policy definition and assigned it to the management group represents the top of the hierarchy for the production environment. The policy simply blocks any resources that has <code class="language-plaintext highlighter-rouge">hidden-module_version</code> tag with the value that matches the pattern <code class="language-plaintext highlighter-rouge">*-prerelease</code>:</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">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pol-restrict-prerelease-overlay-module-versions"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"properties"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Restrict resources to be deployed using prerelease overlay module versions"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Prerelease module versions are published for testing purposes only. They are not intended for production use and they have not gone through code review and validation. This policy restricts resources from being deployed using prerelease overlay module versions."</span><span class="p">,</span><span class="w">
    </span><span class="nl">"metadata"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"category"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Code Vulnerability"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"preview"</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">"deprecated"</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">"mode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Indexed"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"parameters"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"effect"</span><span class="p">:</span><span class="w"> </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">"String"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"metadata"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Effect"</span><span class="p">,</span><span class="w">
          </span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Enable or disable the execution of the policy"</span><span class="w">
        </span><span class="p">},</span><span class="w">
        </span><span class="nl">"allowedValues"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
          </span><span class="s2">"Audit"</span><span class="p">,</span><span class="w">
          </span><span class="s2">"Deny"</span><span class="p">,</span><span class="w">
          </span><span class="s2">"Disabled"</span><span class="w">
        </span><span class="p">],</span><span class="w">
        </span><span class="nl">"defaultValue"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Deny"</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"policyRule"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"if"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"allOf"</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><span class="nl">"field"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tags[hidden-module_version]"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"exists"</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="p">{</span><span class="w">
            </span><span class="nl">"field"</span><span class="p">:</span><span class="w"> </span><span class="s2">"tags[hidden-module_version]"</span><span class="p">,</span><span class="w">
            </span><span class="nl">"like"</span><span class="p">:</span><span class="w"> </span><span class="s2">"*-prerelease"</span><span class="w">
          </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><span class="nl">"then"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"effect"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[parameters('effect')]"</span><span class="w">
      </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><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<blockquote>
  <p>Note: This policy works very well for the resources that support tags. Obviously, not all resources in Azure support tags, and this is a limitation.</p>
</blockquote>

<p>Our hidden tag is important because Bicep modules can be consumed in two ways:</p>

<ol>
  <li>
    <p>When a module is pulled from a registry (either public or private Azure Container Registry), the Bicep template compiles it as a “nested deployment” with all module contents embedded into the ARM payload. In this scenario the module’s version is not discoverable.</p>
  </li>
  <li>
    <p>When a module is used as a TemplateSpec, the compiled template uses a linked reference that points to the TemplateSpec version. However, Azure Policy does not evaluate <code class="language-plaintext highlighter-rouge">Microsoft.Resources/deployments</code> resource used by nested or linked deployments. Azure Policy skips anything coming from the Microsoft.Resources resource provider (except for subscriptions and resource groups).</p>
  </li>
</ol>

<p>Because of both these issues, we decided to “mark” our modules by storing a hidden version tag in the ARM payload.</p>

<h2 id="update-module-pipelines">Update Module Pipelines</h2>

<p>If you are using the CARML / AVM pipeline patterns for your internal Bicep modules, there is one update to the Pipeline code that you need to be aware of.</p>

<p>As I have shown above, the module version number is retrieved from the <code class="language-plaintext highlighter-rouge">version.json</code> file. However this file only contains the <code class="language-plaintext highlighter-rouge">major.minor</code> version numbers.</p>

<p>We need to update the pipeline to write back the full version number to this file before the module publish task.</p>

<p>This is what we have done:</p>

<p>Firstly updated the <a href="https://github.com/Azure/ResourceModules/blob/main/utilities/pipelines/resourcePublish/Get-ModulesToPublish.ps1">Get-ModulesToPublish.ps1</a> and added a new function and placed it before the <code class="language-plaintext highlighter-rouge">Get-ModulesToPublish</code> function::</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">function</span><span class="w"> </span><span class="nf">Set-ModuleVersionFile</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="p">[</span><span class="n">CmdletBinding</span><span class="p">()]</span><span class="w">
    </span><span class="kr">param</span><span class="w"> </span><span class="p">(</span><span class="w">
        </span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="p">)]</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="w"> </span><span class="nv">$TemplateFilePath</span><span class="p">,</span><span class="w">

        </span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="p">)]</span><span class="w">
        </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="w"> </span><span class="nv">$Version</span><span class="w">
    </span><span class="p">)</span><span class="w">

    </span><span class="nv">$ModuleFolder</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Split-Path</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$TemplateFilePath</span><span class="w"> </span><span class="nt">-Parent</span><span class="w">
    </span><span class="nv">$VersionFilePath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Join-Path</span><span class="w"> </span><span class="nv">$ModuleFolder</span><span class="w"> </span><span class="s1">'version.json'</span><span class="w">
    </span><span class="nv">$VersionFileContent</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-ChildItem</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$VersionFilePath</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Get-Content</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertFrom-Json</span><span class="w">
    </span><span class="nv">$VersionFileContent</span><span class="o">.</span><span class="nf">version</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Version</span><span class="w">
    </span><span class="nv">$VersionFileContent</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertTo-Json</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Set-Content</span><span class="w"> </span><span class="nv">$VersionFilePath</span><span class="w">
    </span><span class="n">Write-Verbose</span><span class="w"> </span><span class="s2">"Updated module '</span><span class="si">$(</span><span class="nv">$ModuleFolder</span><span class="o">.</span><span class="nf">Replace</span><span class="p">(</span><span class="s1">'\'</span><span class="p">,</span><span class="s1">'/'</span><span class="p">)</span><span class="o">.</span><span class="nf">Split</span><span class="p">(</span><span class="s1">'modules'</span><span class="p">)[</span><span class="nt">-1</span><span class="p">]</span><span class="si">)</span><span class="s2">' version metadata content: </span><span class="se">`n</span><span class="si">$(</span><span class="nv">$VersionFileContent</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Out-String</span><span class="p">)</span><span class="s2">"-Verbose
}

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

<p>Then added a step to call this function and update the <code class="language-plaintext highlighter-rouge">version.json</code> file before returning the <code class="language-plaintext highlighter-rouge">$modulesToPublish</code> variable in the <code class="language-plaintext highlighter-rouge">Get-ModulesToPublish</code> function at the end of the file:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$TemplateFileToPublish</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$TemplateFilesToPublish</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$ModuleVersion</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-NewModuleVersion</span><span class="w"> </span><span class="nt">-TemplateFilePath</span><span class="w"> </span><span class="nv">$TemplateFileToPublish</span><span class="o">.</span><span class="nf">FullName</span><span class="w"> </span><span class="nt">-Verbose</span><span class="w">
    </span><span class="n">Set-ModuleVersionFile</span><span class="w"> </span><span class="nt">-TemplateFilePath</span><span class="w"> </span><span class="nv">$TemplateFileToPublish</span><span class="w"> </span><span class="nt">-Version</span><span class="w"> </span><span class="nv">$ModuleVersion</span><span class="w"> </span><span class="nt">-Verbose</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p><img src="../../../../assets/images/2025/08/bicep-hidden-tags-05.jpg" alt="05" /></p>

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

<p>Over time I’ve raised a <a href="https://github.com/Azure/bicep-registry-modules/issues/2503">feature request</a> so that the AVM team might support similar functionality natively. It has been a while since I raised the request and the AVM team have not committed to implementing this.</p>

<p>Therefore I have decided to document this pattern so that if we fork the AVM or CARML modules for our internal use, we can “inject” our own hidden tags (both the module name and version) into the ARM payload.</p>

<p>Although the sample code above is specific to Bicep modules and our AVM/CARML fork, the same concept can be applied – for example, in Terraform modules when you need to track which version of a module was deployed.</p>]]></content><author><name>Tao Yang</name></author><category term="Azure"/><category term="Azure"/><category term="Azure Bicep"/><summary type="html"><![CDATA[Background Many customers have gone down the route of developing, publishing and sharing internally developed Azure IaC modules within the organization. The modules can be written in Bicep, or Terraform or other IaC languages.]]></summary></entry><entry><title type="html">AzPolicyTest Module Updated to 2.8.0</title><link href="https://blog.tyang.org/2025/08/10/azpolicytest-module-updated-to-2-8-0" rel="alternate" title="AzPolicyTest Module Updated to 2.8.0" type="text/html"/><published>2025-08-10T21:00:00+10:00</published><updated>2025-08-10T21:00:00+10:00</updated><id>https://blog.tyang.org/2025/08/10/azpolicytest-module-updated-to-2-8-0</id><content type="html" xml:base="https://blog.tyang.org/2025/08/10/azpolicytest-module-updated-to-2-8-0"><![CDATA[<p>I have updated the AzPolicyTest PowerShell module (<a href="https://github.com/TaoYang-cloud/AzPolicyTest">GitHub</a>, <a href="https://www.powershellgallery.com/packages/AzPolicyTest/">PowerShell Gallery</a>) to <code class="language-plaintext highlighter-rouge">v2.8.0</code>. This release includes the following updates:</p>

<h2 id="new-test-to-check-if-any-referenced-resource-types-are-excluded-from-policy-evaluation">New test to check if any referenced resource types are excluded from policy evaluation</h2>

<p>Azure policy excludes several resource types when evaluating resources. these types are defined here:</p>

<ul>
  <li><a href="https://github.com/azure/azure-policy?tab=readme-ov-file#resources-that-are-exempt-from-policy-evaluation">Resources that are exempt from policy evaluation</a></li>
  <li><a href="https://github.com/azure/azure-policy?tab=readme-ov-file#provider-pass-through-to-non-azure-resource-manager-resources">Provider pass-through to non Azure Resource Manager resources</a></li>
</ul>

<p>Added a new test to check if any referenced resource types are from the lists above (using regex)</p>

<h2 id="updated-tests-for-audit--deny-interchangeable-effects-to-exclude-auto-generated-bypassed-properties">Updated tests for Audit / Deny interchangeable effects to exclude auto-generated bypassed properties</h2>

<p>Some properties are not available at resource creation time (not in the request payload). any policies targeting these properties cannot use <code class="language-plaintext highlighter-rouge">Deny</code> effect.(<a href="https://github.com/azure/azure-policy?tab=readme-ov-file#optional-or-auto-generated-resource-property-that-bypasses-policy-evaluation">Optional or auto-generated resource property that bypasses policy evaluation</a>)</p>

<ul>
  <li>Updated the existing tests for Audit / Deny interchangeable effects to exclude policies that are referencing these properties (using regex)</li>
  <li>Added new test to ensure any policies that are referencing these properties do have Deny as one of the allowed values for the policy effect.</li>
</ul>]]></content><author><name>Tao Yang</name></author><category term="Azure"/><category term="Azure"/><category term="Azure Policy"/><category term="PowerShell"/><category term="Pester"/><summary type="html"><![CDATA[I have updated the AzPolicyTest PowerShell module (GitHub, PowerShell Gallery) to v2.8.0. This release includes the following updates:]]></summary></entry><entry><title type="html">Configure PowerShell extension in VSCode on macOS</title><link href="https://blog.tyang.org/2025/04/02/2025-04-02-configure-powershell-vscode-on-macos" rel="alternate" title="Configure PowerShell extension in VSCode on macOS" type="text/html"/><published>2025-04-02T12:00:00+11:00</published><updated>2025-04-02T12:00:00+11:00</updated><id>https://blog.tyang.org/2025/04/02/configure-powershell-vscode-on-macos</id><content type="html" xml:base="https://blog.tyang.org/2025/04/02/2025-04-02-configure-powershell-vscode-on-macos"><![CDATA[<p>I have 3 Mac computers running on the latest version of MacOS, and PowerShell is installed on all of them using Homebrew:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>brew <span class="nb">install </span>powershell/tap/powershell
</code></pre></div></div>
<p>I also have VSCode and the PowerShell extension installed on all of them.</p>

<p>I noticed the other day that I wasn’t able to get vscode to format PowerShell script on one of the Mac computers. I then tried on the other two and none of them worked. Looks like it’s a common issue across all my Mac computers.</p>

<p>When I tried to format the script, I got the prompt to search and install a formatter.</p>

<p><img src="../../../../assets/images/2025/04/pwsh-vscode-mac-config-01.jpg" alt="01" /></p>

<p>To Troubleshoot this, I opened the integrated terminal in VSCode checked the output from PowerShell extension. I found the following error message:</p>

<p><img src="../../../../assets/images/2025/04/pwsh-vscode-mac-config-02.jpg" alt="02" /></p>

<p>It seems that VScode doesn’t know the path to the PowerShell executable.</p>

<p>To fix it, I found the location in a terminal window using the <code class="language-plaintext highlighter-rouge">where</code> command:</p>

<p><img src="../../../../assets/images/2025/04/pwsh-vscode-mac-config-03.jpg" alt="03" /></p>

<p>Then configured the <code class="language-plaintext highlighter-rouge">powershell.powerShellAdditionalExePaths</code> setting in VSCode as the error message suggested and pointed it to the location of the PowerShell executable. I also followed <a href="https://learn.microsoft.com/en-us/powershell/scripting/dev-cross-plat/vscode/using-vscode?view=powershell-7.5#adding-your-own-powershell-paths-to-the-session-menu">this instruction</a> and added <code class="language-plaintext highlighter-rouge">powershell.powerShellDefaultVersion</code> setting in the <code class="language-plaintext highlighter-rouge">settings.json</code> file.</p>

<p><img src="../../../../assets/images/2025/04/pwsh-vscode-mac-config-04.jpg" alt="04" /></p>

<p>After this, I restarted VSCode and opened the PowerShell script again. This time, I was able to format the script without any issues. The output from the integrated terminal also showed that the PowerShell extension was able to find the PowerShell executable.
<img src="../../../../assets/images/2025/04/pwsh-vscode-mac-config-05.jpg" alt="05" /></p>

<p>Since I also have to use Windows and Ubuntu (WSL) for work, I don’t want these settings to be sync’d to my Windows laptop because the PowerShell path would be different on Ubuntu and Windows. So I have configured the settings sync to ignore these settings in the <code class="language-plaintext highlighter-rouge">settings.json</code> file. I added the following lines to the <code class="language-plaintext highlighter-rouge">settings.json</code> file:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"settingsSync.ignoredSettings"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
  </span><span class="s2">"powershell.powerShellAdditionalExePaths"</span><span class="p">,</span><span class="w">
  </span><span class="s2">"powershell.powerShellDefaultVersion"</span><span class="w">
</span><span class="p">]</span><span class="err">,</span><span class="w">
</span></code></pre></div></div>]]></content><author><name>Tao Yang</name></author><category term="MacOS"/><category term="PowerShell"/><category term="VSCode"/><category term="MacOS"/><category term="PowerShell"/><category term="VSCode"/><summary type="html"><![CDATA[I have 3 Mac computers running on the latest version of MacOS, and PowerShell is installed on all of them using Homebrew:]]></summary></entry><entry><title type="html">Azure Policy Limitation for SQL MI Databases</title><link href="https://blog.tyang.org/2025/03/08/azure-policy-limitation-for-sql-mi-databases" rel="alternate" title="Azure Policy Limitation for SQL MI Databases" type="text/html"/><published>2025-03-08T18:00:00+11:00</published><updated>2025-03-08T18:00:00+11:00</updated><id>https://blog.tyang.org/2025/03/08/azure-policy-limitation-for-sql-mi-databases</id><content type="html" xml:base="https://blog.tyang.org/2025/03/08/azure-policy-limitation-for-sql-mi-databases"><![CDATA[<p>Once a SQL Managed Instance (SQL MI) is created, you can connect to the managed instance using SQL Server Management Studio (SSMS). There are 2 ways a database can be created on the SQL MI instance:</p>

<ul>
  <li>via Azure Resource Manager API (Azure PowerShell, Azure CLI, Bicep templates or Azure Portal)</li>
  <li>directly on the SQL MI instance using SSMS</li>
</ul>

<p>If the database is created via the Azure Resource Manager API, any Azure Policies that you have assigned for the SQL MI databases will apply. However, if the database is created directly on the SQL MI instance using SSMS, the Azure Policies will not apply at all.</p>

<p>I have discovered this behaviour around 18 months ago and reported it to the Azure SQL product team and it was acknowledged. Unfortunately to date, this limitation has not been addressed.</p>

<p>To demonstrate this behaviour, I created two databases on a SQL MI instance, one via the Azure Portal and the other via SSMS. We can see the DB created via ARM API on the SSMS console and vice versa.</p>

<p>SSMS view:</p>

<p><img src="../../../../assets/images/2025/03/policy-for-sql-mi-db-01.jpg" alt="01" /></p>

<p>Azure Portal view:</p>

<p><img src="../../../../assets/images/2025/03/policy-for-sql-mi-db-02.jpg" alt="02" /></p>

<p>I have a <code class="language-plaintext highlighter-rouge">DeployIfNotExists</code> policy to configure Diagnostic Settings for SQL MI databases. As you can see, the policy is applied to the DB created via ARM API, but not the one created via SSMS.</p>

<p><img src="../../../../assets/images/2025/03/policy-for-sql-mi-db-03.jpg" alt="03" /></p>

<p><img src="../../../../assets/images/2025/03/policy-for-sql-mi-db-04.jpg" alt="04" /></p>

<p>This limitation applies to all policy effects, not just <code class="language-plaintext highlighter-rouge">DeployIfNotExists</code> policies. If you have any <code class="language-plaintext highlighter-rouge">Deny</code> policies targeting SQL MI databases, they will not apply to databases created via SSMS at creation time, since the database is then visiable via a <code class="language-plaintext highlighter-rouge">GET</code> request in ARM API, the compliance scan of Azure Policy will eventually pick it up and mark it as non-compliant.</p>

<p>This is a security and operational risk. If your organisation is using SQL MI and would use SSMS to create databases, you should be aware of this limitation and consider other methods to enforce your policies. In the past, I have created an Azure Function App to scan the SQL MI databases and enforce the policies. This is a workaround, not ideal, since if a <code class="language-plaintext highlighter-rouge">Deny</code> policy cannot be enforced at creation time, Policy compliance scan will not fix the non-compliant resources automatically.</p>

<p>P.S. The Azure Policy GitHub repo has a list of known issues documented <a href="https://github.com/azure/azure-policy?tab=readme-ov-file#known-issues">here</a>. This behaviour with SQL MI is not listed there.</p>]]></content><author><name>Tao Yang</name></author><category term="Azure"/><category term="Azure"/><category term="Azure Policy"/><summary type="html"><![CDATA[Once a SQL Managed Instance (SQL MI) is created, you can connect to the managed instance using SQL Server Management Studio (SSMS). There are 2 ways a database can be created on the SQL MI instance:]]></summary></entry><entry><title type="html">Azure Policies for Azure Monitor Action Groups</title><link href="https://blog.tyang.org/2025/03/08/azure-policies-for-azure-monitor-action-groups" rel="alternate" title="Azure Policies for Azure Monitor Action Groups" type="text/html"/><published>2025-03-08T15:00:00+11:00</published><updated>2025-03-08T15:00:00+11:00</updated><id>https://blog.tyang.org/2025/03/08/azure-policies-for-azure-monitor-action-groups</id><content type="html" xml:base="https://blog.tyang.org/2025/03/08/azure-policies-for-azure-monitor-action-groups"><![CDATA[<p>I am currently working on implementing some monitoring solutions in a customer’s Azure environment. I only realised yesterday that Azure does not offer any built-in Azure Policy definitions for managing Azure Monitor Action Groups.</p>

<p><img src="../../../../assets/images/2025/03/action-group-policies-01.jpg" alt="01" /></p>

<p>When creating an action group, you can configure zero or more notifications as well as actions.</p>

<p>We need to make sure when alerts are triggered, The following controls should be put in place to prevent data exfiltration and enhance network transport security.</p>

<p><strong>Emails are only sent to authorised email addresses.</strong></p>

<p>Only email domains managed by your organisation should be used. Personal email addresses or emails of another organisation should be prohibited.</p>

<p><strong>SMS messages are only sent to authorised mobile phone numbers.</strong>
The phone numbers used to receive SMS messages should belong to appropriate personnel. Phone numbers external to your organisation or numbers of other countries should be restricted.</p>

<p><strong>Actions that trigger another Azure resource (Azure Automation Runbook, Event Hub Namspace, Azure Function App and Azure Logic App) are only triggering resources within the same subscription or been explicitly added to the allowed list of targets.</strong></p>

<p>When the organisation does not separate Azure environments into different Entra ID tenants, this will ensure we are not potentially sending production alert data to non-production environments.</p>

<p>It will also allow the organisation to control which azure resources can be used to receive alert data if they are indeed located in different subscriptions of the action group.</p>

<p><strong>Only allowed Webhooks can be used to receive alert data</strong></p>

<p>The Webhook URL can belong to anyone and located anywhere in the world. It should be controlled so only allowed Webhook URLs can be used in action groups.</p>

<p><strong>Only allow Webhooks that use HTTPS</strong></p>

<p>Unencrypted data using <code class="language-plaintext highlighter-rouge">HTTP</code> protocol should be prohibited.</p>

<p>Fortunately the Azure Policy aliases for these properties exist already. I was able to leverage them and created the following policies and placed them in <a href="https://github.com/TaoYang-cloud/azurepolicy/tree/master/policy-definitions/action-groups">my azure policy GitHub repo</a>:</p>

<ul>
  <li><a href="https://github.com/TaoYang-cloud/azurepolicy/blob/master/policy-definitions/action-groups/pol-ag-deny-external-email-notification.json">Restrict Azure Monitor Action Group Send Email Notification to External Email Addresses</a></li>
  <li><a href="https://github.com/TaoYang-cloud/azurepolicy/blob/master/policy-definitions/action-groups/pol-ag-deny-unauthorized-sms-notification-recipients.json">Restrict Azure Monitor Action Group Send SMS Notification to Unauthorized Phone Numbers</a></li>
  <li><a href="https://github.com/TaoYang-cloud/azurepolicy/blob/master/policy-definitions/action-groups/pol-ag-deny-unauthorized-azure-automation-actions.json">Restrict Azure Monitor Action Group Trigger Actions to Cross-Subscription Azure Automation or not on the Allowed List</a></li>
  <li><a href="https://github.com/TaoYang-cloud/azurepolicy/blob/master/policy-definitions/action-groups/pol-ag-deny-unauthorized-event-hub-actions.json">Restrict Azure Monitor Action Group Trigger Actions to Cross-Subscription Event Hubs or not on the Allowed List</a></li>
  <li><a href="https://github.com/TaoYang-cloud/azurepolicy/blob/master/policy-definitions/action-groups/pol-ag-deny-unauthorized-function-app-actions.json">Restrict Azure Monitor Action Group Trigger Actions to Cross-Subscription Function Apps or not on the Allowed List</a></li>
  <li><a href="https://github.com/TaoYang-cloud/azurepolicy/blob/master/policy-definitions/action-groups/pol-ag-deny-unauthorized-logic-app-actions.json">Restrict Azure Monitor Action Group Trigger Actions to Cross-Subscription Logic Apps or not on the Allowed List</a></li>
  <li><a href="https://github.com/TaoYang-cloud/azurepolicy/blob/master/policy-definitions/action-groups/pol-ag-deny-unauthorized-webhooks.json">Restrict Azure Monitor Action Group Trigger Actions to Webhooks that are not on the Allowed List</a></li>
  <li><a href="https://github.com/TaoYang-cloud/azurepolicy/blob/master/policy-definitions/action-groups/pol-ag-deny-http-webhooks.json">Restrict Azure Monitor Action Group Trigger Actions to Webhooks that are not using HTTPS</a></li>
</ul>

<p>With the policies for restricting cross-subscription function app, logic app, event hub and automation runbooks, the definitions provide an array parameter to allow a list of cross-subscription resources. You can use the <code class="language-plaintext highlighter-rouge">allowedAutomationAccounts</code>, <code class="language-plaintext highlighter-rouge">allowedEventHubNamespaces</code>, <code class="language-plaintext highlighter-rouge">allowedFunctionApps</code> and <code class="language-plaintext highlighter-rouge">allowedLogicApps</code> parameters in its respective policy to approve the use of any of these resources that are located in other subscriptions.</p>

<p>In the portal UI, you can also see the ITSM connection and Secure Webhook as available actions. I did not bother to develop policies for ITSM connections because according to the <a href="https://learn.microsoft.com/azure/azure-monitor/alerts/itsmc-overview#itsm-integration-workflow">documentation</a>, it is already deprecated.</p>

<p>With regards to Secure Webhook, the ARM JSON configuration is the same as normal webhook, the only difference is that you will need to specify an EntraID application’s object ID and set <code class="language-plaintext highlighter-rouge">useAadAuth</code> to true. Therefore the policies for the Webhook Actions also apply to Secure Webhooks.</p>]]></content><author><name>Tao Yang</name></author><category term="Azure"/><category term="Azure"/><category term="Azure Monitor"/><category term="Azure Policy"/><summary type="html"><![CDATA[I am currently working on implementing some monitoring solutions in a customer’s Azure environment. I only realised yesterday that Azure does not offer any built-in Azure Policy definitions for managing Azure Monitor Action Groups.]]></summary></entry><entry><title type="html">Log Analytics Queries for Billable Data per Subscription</title><link href="https://blog.tyang.org/2025/02/24/og-analytics-queries-for-billable-data-per-sub" rel="alternate" title="Log Analytics Queries for Billable Data per Subscription" type="text/html"/><published>2025-02-24T12:00:00+11:00</published><updated>2025-02-24T12:00:00+11:00</updated><id>https://blog.tyang.org/2025/02/24/log-analytics-queries-for-billable-data-per-sub</id><content type="html" xml:base="https://blog.tyang.org/2025/02/24/og-analytics-queries-for-billable-data-per-sub"><![CDATA[<p>When we deploy Azure Enterprise Scale Landing Zones, We often advise our customers to use a centralised Log Analytics workspace for all their Azure resources and configure the workspace to use the <a href="https://learn.microsoft.com/en-us/azure/azure-monitor/logs/manage-access?tabs=portal#access-mode">Resource-context Access Mode</a>. With this pattern, normally the cloud administrators and security teams would have been granted access on the Log Analytics workspace level. The application teams who consume the Azure resources do not need to be granted any roles to the Log Analytics workspace.</p>

<p>There are many benefits of using this pattern:</p>

<ul>
  <li>Log data, table retention can be centrally managed.</li>
  <li>Simply the management of AMPLS (Azure Monitor Private Link Scope).</li>
  <li>Simply the management of Log Analytics workspace access.</li>
  <li>Able to apply for discounted price via <a href="https://learn.microsoft.com/en-us/azure/azure-monitor/logs/cost-logs#commitment-tiers">Commitment Tier</a> (There is a minimum volume requirement for commitment tier, which small workspaces will not be able to qualify).</li>
  <li>Simplify the log access, monitor and alerting configurations since all the logs are in the same location.</li>
</ul>

<p>However, one of the challenges of using a centralised Log Analytics workspace is how to calculate and charge back the data usage to different subscriptions. In most of the billing solutions, customers normally have nominated a common tag that’s associated to the internal cost centre which is used for consumption charge back.</p>

<p>Few days ago, I worked with my good friend <a href="https://x.com/AlexVerkinderen">Alex Verkinderen</a> and came up with the following queries to calculate the total billable data per subscription. The queries are based on the assumption that the <code class="language-plaintext highlighter-rouge">SolutionID</code> tag is used to identify the cost centre for each subscription. The queries are designed to be used in any Log Analytics workspace and leverage the capability of <a href="https://techcommunity.microsoft.com/blog/azuregovernanceandmanagementblog/azure-monitor-availability-alerts-using-resource-graph-queries/4096469">cross querying Azure Resource Graph</a>. By cross querying Azure Resource Graph, we can get the subscription name and any tags of interest from the Azure Resource Graph and join the data with the Log Analytics data.</p>

<p>Here are the 2 queries we have developed:</p>

<p><strong>Total ingested volume over last month per subscription</strong></p>

<pre><code class="language-OQL">arg("").ResourceContainers
| where type =~ 'microsoft.resources/subscriptions'
| project SubscriptionName = name, subscriptionId, SolutionId = tags['SolutionID']
| join kind=inner hint.remote=right (
    find where TimeGenerated between(startofday(ago(32d))..startofday(now())) project _BilledSize, _IsBillable,  _SubscriptionId
    | where _IsBillable == true
    | summarize BillableDataBytes = sum(_BilledSize) by _SubscriptionId
    | extend DataIngestedInGB = format_bytes(BillableDataBytes, 3, "GB")
    | extend subscriptionId = _SubscriptionId
) on subscriptionId
| sort by BillableDataBytes
| project SubscriptionName, subscriptionId, SolutionId, DataIngestedInGB

</code></pre>

<p><img src="../../../../assets/images/2025/02/law-queries-01.jpg" alt="01" /></p>

<p><strong>Daily ingestion volume over last month for a specific subscription</strong></p>

<pre><code class="language-OQL">arg("").ResourceContainers
| where type =~ 'microsoft.resources/subscriptions'
| project SubscriptionName = name, subscriptionId, SolutionId = tags['SolutionID']
| join kind=inner hint.remote=right (
    find where TimeGenerated between(startofday(ago(32d))..startofday(now())) project _BilledSize, _IsBillable,  _SubscriptionId, TimeGenerated
    | where _IsBillable == true
    | summarize BillableDataBytes = sum(_BilledSize) by _SubscriptionId, bin(TimeGenerated, 1d)
    | extend DataIngestedInGB = format_bytes(BillableDataBytes, 3, "GB")
    | extend IngestionDate = bin(TimeGenerated, 1d)
    | extend subscriptionId = _SubscriptionId
) on subscriptionId
| where SubscriptionName =~ 'The-Big-MVP-Sub-2'
| sort by IngestionDate asc
| project SubscriptionName, subscriptionId, SolutionId, DataIngestedInGB, IngestionDate
</code></pre>

<p><img src="../../../../assets/images/2025/02/law-queries-02.jpg" alt="02" /></p>

<p>The billable price can be then easily calculated based on the ingested volume and price per GB.</p>]]></content><author><name>Tao Yang</name></author><category term="Azure"/><category term="Azure"/><category term="Azure Monitor"/><category term="Log Analytics"/><category term="Kusto Query Language"/><summary type="html"><![CDATA[When we deploy Azure Enterprise Scale Landing Zones, We often advise our customers to use a centralised Log Analytics workspace for all their Azure resources and configure the workspace to use the Resource-context Access Mode. With this pattern, normally the cloud administrators and security teams would have been granted access on the Log Analytics workspace level. The application teams who consume the Azure resources do not need to be granted any roles to the Log Analytics workspace.]]></summary></entry></feed>