<?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-08-12T22:13:50+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">Azure Policy Definition to Enforce IaC and Restrict Click Ops</title><link href="https://blog.tyang.org/2026/08/12/policy-enforce-iac-restrict-click-ops" rel="alternate" title="Azure Policy Definition to Enforce IaC and Restrict Click Ops" type="text/html"/><published>2026-08-12T12:00:00+10:00</published><updated>2026-08-12T12:00:00+10:00</updated><id>https://blog.tyang.org/2026/08/12/policy-enforce-iac-restrict-click-ops</id><content type="html" xml:base="https://blog.tyang.org/2026/08/12/policy-enforce-iac-restrict-click-ops"><![CDATA[<h2 id="background">Background</h2>

<p>Infrastructure as Code (IaC) is the preferred way for almost every mature organisation to deploy and manage resources on cloud platforms. However, managing configuration drift has always been challenging when someone with privileged access changes resources directly without updating the IaC code.</p>

<p>To minimise this risk, organisations can adopt strict rules when assigning privileged IAM roles to users. However, this approach can be hit or miss. I have often found configuration drift between IaC code and actual resource configurations in customer environments.</p>

<p>A few days ago, I was discussing the <a href="https://learn.microsoft.com/en-us/azure/governance/policy/how-to/using-request-context-identity"><code class="language-plaintext highlighter-rouge">requestContext().identity</code></a> function in Azure Policy with a colleague. He jokingly said that we could develop a <code class="language-plaintext highlighter-rouge">Deny</code> policy for requests where the identity is my user account.</p>

<p>Although it was a joke, it made me realise that this could be a good way to enforce IaC and restrict ClickOps. It would prevent users from making changes even with privileged access unless they sign in to Azure using a service principal.</p>

<h2 id="policy-definition">Policy Definition</h2>

<p>I managed to develop this policy definition: <a href="https://github.com/Azure/AzPolicyFactory/blob/main/policyDefinitions/general/pol-restrict-user-arm-api-requests.json">Restrict Manual ARM API Requests by User Identity</a>.</p>

<p>You need to specify the resource types for which you want to restrict manual operations using the <code class="language-plaintext highlighter-rouge">resourceTypePrefix</code> parameter. The policy appends a wildcard (<code class="language-plaintext highlighter-rouge">*</code>) to the end of this parameter, so you can use a prefix such as <code class="language-plaintext highlighter-rouge">Microsoft.Compute</code> to apply the policy to the entire resource provider, or <code class="language-plaintext highlighter-rouge">Microsoft.Compute/virtualMachines</code> to apply it to virtual machines and all their child resources.</p>

<p>To put this into action, I added the definition to the <a href="https://github.com/Azure/AzPolicyFactory/blob/main/policyInitiatives/polset-nsg.json#L178-L191">Azure Network Security Groups Policy Initiative</a> and set the <code class="language-plaintext highlighter-rouge">resourceTypePrefix</code> parameter to <code class="language-plaintext highlighter-rouge">Microsoft.Network/networkSecurityGroups</code>. Once the initiative is assigned, any manual operations on network security groups and their child resources are denied, and users must use IaC to make changes.</p>

<p>To test the policy, I assigned the initiative and tried to change the destination of an existing NSG rule through the Azure portal using my user account, which had the <code class="language-plaintext highlighter-rouge">Owner</code> role. The operation was denied as expected.</p>

<p><img src="/assets/images/2026/08/restrict-click-op-policy-01.jpg" alt="01" /></p>

<p>I then updated the IaC code with the same change and ran the Azure DevOps pipeline. The deployment succeeded, and the change was reflected in the NSG rule. This is because the ADO pipeline uses a service principal to deploy the IaC code, which is allowed by the policy.</p>

<p><img src="/assets/images/2026/08/restrict-click-op-policy-02.jpg" alt="02" /></p>

<h2 id="considerations">Considerations</h2>

<p>When using this policy, please consider the following:</p>

<ol>
  <li>Make sure you test this thoroughly in a non-production environment before applying it to production. You don’t want to accidentally lock yourself out of making necessary changes.</li>
  <li>Azure Policy can only manage resources at the subscription scope and below. You cannot use this policy to restrict operations on resources at the management group or tenant scope, such as policy resources, role definitions, and role assignments deployed at those scopes.</li>
  <li>Some resources are exempt from Azure Policy evaluation. Refer to the <a href="https://github.com/azure/azure-policy#resources-that-are-exempt-from-policy-evaluation">Azure Policy documentation</a> for details. These resources are not affected by this policy, so users can still make manual changes to them.</li>
  <li>This policy is not a replacement for proper IAM role management. It provides an additional layer of control to enforce IaC and restrict ClickOps. You should still follow best practices for IAM role assignments and apply the principle of least privilege when managing access to your Azure resources.</li>
</ol>]]></content><author><name>Tao Yang</name></author><category term="Azure"/><category term="Azure"/><category term="Azure Policy"/><summary type="html"><![CDATA[Background]]></summary></entry><entry><title type="html">Various updates for AzPolicyFactory</title><link href="https://blog.tyang.org/2026/08/09/azpolicyfactory-various-updates" rel="alternate" title="Various updates for AzPolicyFactory" type="text/html"/><published>2026-08-09T12:00:00+10:00</published><updated>2026-08-09T12:00:00+10:00</updated><id>https://blog.tyang.org/2026/08/09/azpolicyfactory-various-updates</id><content type="html" xml:base="https://blog.tyang.org/2026/08/09/azpolicyfactory-various-updates"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>I have made various updates to the AzPolicyFactory solution over the past few months. The updates include:</p>

<ul>
  <li>New policies for Microsoft Defender for Cloud (MDC)</li>
  <li>New policies for AI Landing Zone components (AI Foundry, Cosmos DB and Azure AI Search)</li>
  <li>New policies for Azure Kubernetes Service (AKS)</li>
  <li>New policies for allowing or denying resource providers</li>
  <li>An updated policy for restricting resource creation using <code class="language-plaintext highlighter-rouge">-preview</code> ARM API versions</li>
</ul>

<h2 id="new-microsoft-defender-for-cloud-mdc-policies">New Microsoft Defender for Cloud (MDC) Policies</h2>

<p>I noticed that the built-in policies for Microsoft Defender for Cloud (MDC) are outdated. I have produced a new set of MDC policies that use more recent API versions and provide updated plan coverage and configuration options. They are located at:</p>

<ul>
  <li><strong>Policy Definitions</strong>: <a href="https://github.com/Azure/AzPolicyFactory/tree/main/policyDefinitions/mdc">policyDefinitions/mdc</a></li>
  <li><strong>Policy Initiatives</strong>: <a href="https://github.com/Azure/AzPolicyFactory/blob/main/policyInitiatives/polset-mdc.json">policyInitiatives/polset-mdc.json</a></li>
</ul>

<h2 id="new-ai-landing-zone-policies">New AI Landing Zone Policies</h2>

<p>I have produced a new set of policies for AI Landing Zone components, including AI Foundry, Cosmos DB and Azure AI Search. They are located at:</p>

<h3 id="ai-foundry-cognitive-services">AI Foundry (Cognitive Services)</h3>

<ul>
  <li><strong>Policy Definitions</strong>: <a href="https://github.com/Azure/AzPolicyFactory/tree/main/policyDefinitions/cognitive-service">policyDefinitions/cognitive-service</a></li>
  <li><strong>Policy Initiatives</strong>: <a href="https://github.com/Azure/AzPolicyFactory/blob/main/policyInitiatives/polset-cognitive-service.json">policyInitiatives/polset-cognitive-service.json</a></li>
</ul>

<p>The following policies are included for AI Foundry accounts:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Cognitive Services accounts should restrict public network access</code>: Compared with the built-in policy, this policy has one additional condition: <code class="language-plaintext highlighter-rouge">Microsoft.CognitiveServices/accounts/networkAcls.defaultAction</code> must also be set to <code class="language-plaintext highlighter-rouge">Deny</code>.</li>
  <li><code class="language-plaintext highlighter-rouge">Cognitive Services accounts should have local authentication methods disabled</code>: This policy uses the <code class="language-plaintext highlighter-rouge">Deny</code> effect, whereas the built-in policy uses the <code class="language-plaintext highlighter-rouge">Modify</code> effect.</li>
  <li><code class="language-plaintext highlighter-rouge">Cognitive Services accounts should only allow permitted model formats</code>: This policy ensures that only allowed LLM model formats (such as <code class="language-plaintext highlighter-rouge">OpenAI</code>, <code class="language-plaintext highlighter-rouge">xAI</code> and <code class="language-plaintext highlighter-rouge">Anthropic</code>) are deployed to AI Foundry accounts.</li>
  <li><code class="language-plaintext highlighter-rouge">Cognitive Services accounts should only allow permitted model names</code>: This policy ensures that only allowed LLM model names (such as <code class="language-plaintext highlighter-rouge">gpt-5</code>, <code class="language-plaintext highlighter-rouge">claude-opus-4-8</code> and <code class="language-plaintext highlighter-rouge">grok-4</code>) for a given format are deployed to AI Foundry accounts.</li>
</ul>

<p>The <code class="language-plaintext highlighter-rouge">polset-cognitive-service.json</code> policy initiative uses several instances of the permitted model format and model name policies. These instances ensure that only allowed model formats (vendors) and model names (including versions) can be deployed to AI Foundry accounts. You can customise this initiative to include your own allowed model formats and names.</p>

<h3 id="cosmos-db">Cosmos DB</h3>

<ul>
  <li><strong>Policy Definitions</strong>: <a href="https://github.com/Azure/AzPolicyFactory/tree/main/policyDefinitions/cosmos-db">policyDefinitions/cosmos-db</a></li>
  <li><strong>Policy Initiatives</strong>: <a href="https://github.com/Azure/AzPolicyFactory/blob/main/policyInitiatives/polset-cosmos-db.json">policyInitiatives/polset-cosmos-db.json</a></li>
</ul>

<p>The following policies are included for Cosmos DB:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">Cosmos DB database accounts should have local authentication methods disabled</code>: We encountered issues while testing the built-in policy when <code class="language-plaintext highlighter-rouge">Microsoft.DocumentDB/databaseAccounts/capabilities[*]</code> was empty. This policy handles that scenario and also blocks deployment when no capabilities are defined for the Cosmos DB account.</li>
  <li><code class="language-plaintext highlighter-rouge">Azure Cosmos DB accounts should have a minimum TLS version</code>: This policy enforces the minimum TLS version for Cosmos DB accounts.</li>
  <li><code class="language-plaintext highlighter-rouge">Azure Cosmos DB key-based metadata write access should be disabled</code>: This policy uses the <code class="language-plaintext highlighter-rouge">Audit</code> or <code class="language-plaintext highlighter-rouge">Deny</code> effect, whereas the built-in policy uses <code class="language-plaintext highlighter-rouge">Append</code>. The built-in policy also hardcodes the effect rather than parameterising it.</li>
</ul>

<h3 id="azure-ai-search">Azure AI Search</h3>

<ul>
  <li><strong>Policy Initiatives</strong>: <a href="https://github.com/Azure/AzPolicyFactory/blob/main/policyInitiatives/polset-search.json">policyInitiatives/polset-search.json</a></li>
</ul>

<p>This is a sample reference policy initiative for Azure AI Search.</p>

<h2 id="new-aks-policies">New AKS Policies</h2>

<ul>
  <li><strong>AKS Control Plane Policy Definitions</strong>: <a href="https://github.com/Azure/AzPolicyFactory/tree/main/policyDefinitions/aks">policyDefinitions/aks</a></li>
  <li><strong>AKS Control Plane Policy Initiatives</strong>: <a href="https://github.com/Azure/AzPolicyFactory/blob/main/policyInitiatives/polset-aks-control.json">policyInitiatives/polset-aks-control.json</a></li>
  <li><strong>AKS Data Plane Policy Initiatives</strong>: <a href="https://github.com/Azure/AzPolicyFactory/blob/main/policyInitiatives/polset-aks-data.json">policyInitiatives/polset-aks-data.json</a></li>
</ul>

<p>These custom policy definitions and initiatives were developed for previous customer engagements. They are all referenced in the control plane policy initiative.</p>

<p>To keep the initiatives simple and optimise the performance of policy integration tests, we have separated the control plane and data plane policies into two initiatives. The data plane policy initiative contains all the built-in policies for AKS that use <code class="language-plaintext highlighter-rouge">Microsoft.ContainerService.Data</code> mode.</p>

<h2 id="new-policies---allowed-resource-providers-and-deny-resource-providers">New Policies - Allowed Resource Providers and Deny Resource Providers</h2>

<ul>
  <li><strong>Deny Resource Providers</strong>: <a href="https://github.com/Azure/AzPolicyFactory/blob/main/policyDefinitions/general/pol-deny-resource-provider.json">policyDefinitions/general/pol-deny-resource-provider.json</a></li>
  <li><strong>Allow Resource Providers</strong>: <a href="https://github.com/Azure/AzPolicyFactory/blob/main/policyDefinitions/general/pol-allow-resource-provider.json">policyDefinitions/general/pol-allow-resource-provider.json</a></li>
</ul>

<p>These two policies can be used to prevent resources from specified resource providers from being deployed. You can choose between an allowlist and a denylist based on your organisation’s requirements.</p>

<p>For example, if your organisation is new to Azure, the Allowed Resource Providers policy is probably the better approach. You can start with a small set of allowed resource providers and gradually add more as your organisation grows. If your organisation has been using Azure for a while and you want to restrict certain resource providers, you can use the Deny Resource Providers policy.</p>

<h2 id="updated-policy-for-restricting-resource-creation-using--preview-arm-api-versions">Updated Policy for restricting resource creation using <code class="language-plaintext highlighter-rouge">-preview</code> ARM API versions</h2>

<ul>
  <li><strong>Control usage of preview versions of Azure Resource Manager REST APIs</strong>: <a href="https://github.com/Azure/AzPolicyFactory/blob/main/policyDefinitions/general/pol-restrict-arm-preview-api-versions.json">policyDefinitions/general/pol-restrict-arm-preview-api-versions.json</a></li>
</ul>

<p>This is an updated version of <a href="https://github.com/TaoYang-Cloud/azurepolicy/blob/master/policy-definitions/arm-api-versions/control-preview-api/azurepolicy.json">my previously published policy definition</a>.</p>

<p>The original version uses an <code class="language-plaintext highlighter-rouge">array</code> parameter to define the list of resource types. I am not a fan of this approach because it is not possible to create an exemption for a specific resource type.</p>

<p>The updated version uses a <code class="language-plaintext highlighter-rouge">string</code> parameter to define a specific resource type. The policy can be included in any policy initiative, and you can define multiple instances for different resource types. You can also create an exemption for a specific resource type.</p>

<p>The original version also had an issue with subsequent compliance scans. Because Azure Policy always evaluates resources using the latest API version, the policy always reports a resource as <code class="language-plaintext highlighter-rouge">non-compliant</code> when the latest version is a <code class="language-plaintext highlighter-rouge">-preview</code> version. In practice, this policy should be evaluated only when a resource is created or updated. The updated version fixes this issue by adding the following condition:</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">"value"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[tryGet(requestContext().identity, 'idtyp')]"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"in"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="s2">"user"</span><span class="p">,</span><span class="w"> </span><span class="s2">"app"</span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>This condition ensures that the policy is evaluated only when an identity (<code class="language-plaintext highlighter-rouge">user</code> or <code class="language-plaintext highlighter-rouge">app</code>) initiates the request. During a compliance scan, the <code class="language-plaintext highlighter-rouge">idtyp</code> value returned by the <code class="language-plaintext highlighter-rouge">requestContext().identity</code> function is <code class="language-plaintext highlighter-rouge">null</code>, so the policy is not evaluated. This ensures that the policy is evaluated only when a resource is created or updated.</p>]]></content><author><name>Tao Yang</name></author><category term="Azure"/><category term="Azure"/><category term="Azure Policy"/><summary type="html"><![CDATA[Introduction]]></summary></entry><entry><title type="html">AzPolicyLens - Automated Azure Policy Insights and Reporting</title><link href="https://blog.tyang.org/2026/05/03/azpolicylens" rel="alternate" title="AzPolicyLens - Automated Azure Policy Insights and Reporting" type="text/html"/><published>2026-05-03T10:00:00+10:00</published><updated>2026-05-03T10:00:00+10:00</updated><id>https://blog.tyang.org/2026/05/03/azpolicylens</id><content type="html" xml:base="https://blog.tyang.org/2026/05/03/azpolicylens"><![CDATA[<p><img src="/assets/images/2026/05/azpolicylens-banner.jpg" alt="banner" /></p>

<h2 id="introduction">Introduction</h2>

<p>AzPolicyLens is a solution that automatically generates insights and reports based on Azure Policy data. It provides a comprehensive view of the policy compliance status across your Azure environment, helping you identify areas of non-compliance and take corrective actions.</p>

<p>This solution has been in active development for the last 12 months, and I am excited to share that I have finally released it to the public. You can find the project at <a href="https://aka.ms/AzPolicyLens">aka.ms/AzPolicyLens</a>.</p>

<p>In this blog post, I will provide an overview of AzPolicyLens, its features, and how it can benefit your organization.</p>

<p><strong>12/05/2026 - Update</strong>: I have published a video introducing AzPolicyLens on <a href="https://www.youtube.com/@AzureTar">AzureTar YouTube channel</a>. You can watch the video below:</p>

<iframe src="//www.youtube.com/embed/g2fRcfxRkBA" height="375" width="640" allowfullscreen="" frameborder="0"></iframe>

<h2 id="problem-statement">Problem Statement</h2>

<p>While working in many customer environments over the last few years, I have observed the following challenges related to Azure Policy:</p>

<ul>
  <li>Lack of visibility</li>
  <li>Lack of policy compliance reporting and monitoring</li>
  <li>Inconsistent, overlapping, incorrect policy definitions</li>
  <li>Lack of documentation for application teams, i.e.:
    <ul>
      <li>How should we configure our Azure resources</li>
      <li>What is the service offering from the Azure landing zone?</li>
    </ul>
  </li>
  <li>Lack of security control mapping</li>
  <li>Lack of compliance reporting dimensions</li>
</ul>

<p>With these challenges in mind, I set out to create a solution that would address these issues and provide a comprehensive view of Azure Policy compliance across an organization.</p>

<h2 id="features-of-azpolicylens">Features of AzPolicyLens</h2>

<p>AzPolicyLens is an automated solution that generates insights and reports based on Azure Policy data.</p>

<p>It supports the two most commonly used DevOps tools, Azure DevOps and GitHub. It provides an Azure DevOps pipeline and a GitHub Actions workflow to generate Markdown wiki content for both Azure DevOps code wiki and GitHub wiki.</p>

<p>The target audience for the generated wiki includes:</p>

<ul>
  <li>Azure platform engineers and administrators</li>
  <li>Azure cloud architects</li>
  <li>Security Architects and Security Operations teams</li>
  <li>Application developers and architects</li>
  <li>Compliance and audit teams</li>
</ul>

<h2 id="offering">Offering</h2>

<p>The solution offers:</p>

<ul>
  <li>Detailed wiki for cloud engineering and security teams</li>
  <li>Security control catalog for native built-in and custom controls</li>
  <li>Ability to integrate internal security controls into the wiki (treated the same as built-in security controls)</li>
  <li>Tailored wiki for each application team focusing on their own data</li>
  <li>Integration with existing CI/CD platforms (supports Azure DevOps and GitHub Actions)</li>
</ul>

<p>The detailed features can be found in the <a href="https://github.com/Azure/AzPolicyLens/blob/main/docs/README.md#feature-comparison">Feature Comparison</a> section of the solution documentation.</p>

<h2 id="image-gallery">Image Gallery</h2>

<p>:memo: Here are some screenshots (Click to enlarge) of the generated wiki content from AzPolicyLens (for comprehensive sample wiki sites, please refer to the <a href="https://github.com/Azure/AzPolicyLens#demo">demo wiki sites</a>).</p>

<figure class="third ">
  
    
      <a href="/assets/images/2026/05/azpolicylens-gallery-01.jpg" title="Detailed Wiki Main Page">
          <img src="/assets/images/2026/05/azpolicylens-gallery-01.jpg" alt="Main page of the generated detailed wiki" />
      </a>
    
  
    
      <a href="/assets/images/2026/05/azpolicylens-gallery-02.jpg" title="Management Group Summary">
          <img src="/assets/images/2026/05/azpolicylens-gallery-02.jpg" alt="Management Group Summary on the detailed wiki main page" />
      </a>
    
  
    
      <a href="/assets/images/2026/05/azpolicylens-gallery-03.jpg" title="Policy Definition Syntax Validation Report">
          <img src="/assets/images/2026/05/azpolicylens-gallery-03.jpg" alt="Policy Definition Syntax Validation Report from the Policy Definition page in the detailed wiki" />
      </a>
    
  
    
      <a href="/assets/images/2026/05/azpolicylens-gallery-04.jpg" title="Assignment Compliance Summary">
          <img src="/assets/images/2026/05/azpolicylens-gallery-04.jpg" alt="Assignment Compliance Summary from the Policy Assignment page" />
      </a>
    
  
    
      <a href="/assets/images/2026/05/azpolicylens-gallery-05.jpg" title="Overall Compliance Summary of a Security Framework">
          <img src="/assets/images/2026/05/azpolicylens-gallery-05.jpg" alt="Overall compliance summary of a built-in security framework (ISO 27001 in this case) on the Security Control Catalog page" />
      </a>
    
  
    
      <a href="/assets/images/2026/05/azpolicylens-gallery-06.jpg" title="Overall Compliance Summary of a Custom Security Control Framework">
          <img src="/assets/images/2026/05/azpolicylens-gallery-06.jpg" alt="Overall compliance summary of a custom security control framework (organization's internal framework in this case) on the Security Control Catalog page" />
      </a>
    
  
    
      <a href="/assets/images/2026/05/azpolicylens-gallery-07.jpg" title="Recommendations from the Analysis page">
          <img src="/assets/images/2026/05/azpolicylens-gallery-07.jpg" alt="Recommendations from the Analysis page in the detailed wiki" />
      </a>
    
  
    
      <a href="/assets/images/2026/05/azpolicylens-gallery-08.jpg" title="Raw JSON Definition for the Policy Initiative">
          <img src="/assets/images/2026/05/azpolicylens-gallery-08.jpg" alt="Raw JSON definition for the policy initiative" />
      </a>
    
  
    
      <a href="/assets/images/2026/05/azpolicylens-gallery-09.jpg" title="Policy Category Summary">
          <img src="/assets/images/2026/05/azpolicylens-gallery-09.jpg" alt="Policy category summary in the detailed wiki" />
      </a>
    
  
  
    <figcaption>Click any image to view it in full size.
</figcaption>
  
</figure>

<h2 id="wiki-pipelines">Wiki Pipelines</h2>

<p>The pipelines are designed to run either manually or on a schedule, with security in mind. No privileged roles are required to gather the data (only the <code class="language-plaintext highlighter-rouge">Reader</code> role is required in Azure). The pipelines perform a single discovery and use the same discovery data to generate one or more copies of the wiki for different personas and teams.</p>

<p>The discovery data does not contain any sensitive information and can be configured to be encrypted at rest with customer-managed keys.</p>

<p><img src="/assets/images/2026/05/azpolicylens-pipeline-flow.jpg" alt="banner" /></p>

<h2 id="benefits">Benefits</h2>

<p>By using AzPolicyLens, organizations can gain the following benefits:</p>

<ul>
  <li>Visibility into policy compliance and security posture</li>
  <li>Inclusion in the landing zone service offering for application teams (customers)</li>
  <li>Integration of in-house security controls with policy reporting</li>
  <li>A library of external and internal security frameworks and controls for reference and use in policy development</li>
  <li>Compliance reporting across multiple dimensions:
    <ul>
      <li>In the overall specified scope</li>
      <li>By individual subscriptions</li>
      <li>By policy assignments</li>
      <li><strong>By security frameworks</strong></li>
      <li><strong>By custom security controls</strong></li>
      <li><strong>By specific security controls</strong></li>
      <li><strong>By policy categories</strong></li>
    </ul>
  </li>
  <li>Insights into areas for improvement and actionable recommendations</li>
  <li>Assistance with ongoing policy development and maintenance</li>
  <li>For Azure cloud consultants and partner solution providers, this solution can be used as a tool for an initial assessment of a customer’s existing Azure environment, as well as ongoing monitoring and reporting.</li>
</ul>

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

<p>Whether you are responsible for your organization’s Azure environment or you are an Azure consultant working with a customer’s Azure environment, I strongly recommend giving this solution a try, as I believe it solves many common challenges related to Azure Policy management and compliance reporting.</p>

<h2 id="credits">Credits</h2>

<p>Over the last 12 months, from an initial prototype based on a customer’s request to a fully released solution, I have received a lot of feedback and suggestions from many people. I want to take this opportunity to thank everyone who has contributed to the development of this solution, including:</p>

<ul>
  <li>Thanks to <a href="https://www.linkedin.com/in/petezerger/">Pete Zerger</a> and <a href="https://www.linkedin.com/in/ningning/">NingNing Zhao</a> - my favourite CISSPs - for their valuable feedback and suggestions from the perspective of security practitioners at different levels, which was crucial in designing a solution that meets the needs of different personas in the security and compliance space.</li>
  <li>Thanks to <a href="https://www.linkedin.com/in/alexandreverkinderen/">Alex Verkinderen</a> and <a href="https://www.linkedin.com/in/ahmadabdalla/">Ahmad Abdalla</a> for their feedback and suggestions from an Azure platform engineering perspective, which was crucial in designing a solution that meets the needs of Azure platform engineers and architects.</li>
  <li>Thanks to <a href="https://www.linkedin.com/in/johandahlbom/">Johan Dahlbom</a>, <a href="https://www.linkedin.com/in/alessandrocardoso/">Alessandro Cardoso</a>, <a href="https://www.linkedin.com/in/andreaswasita/">Andreas Washita</a>, <a href="https://www.linkedin.com/in/sgraef/">Sebastian Gräf</a>, and many other colleagues within Microsoft for the guidance and encouragement that helped me navigate the internal process of releasing this as an open-source solution under the Microsoft banner.</li>
  <li>Thanks to the project team from the customer (whom I cannot name here) for the initial request and for the time and support to develop the first prototype in the customer’s environment, which became the foundation of this solution.</li>
  <li>Lastly, thanks to Ahmad and Sebastian for accompanying me on this project and becoming co-owners (a Microsoft requirement for this type of project).</li>
</ul>]]></content><author><name>Tao Yang</name></author><category term="Azure"/><category term="Azure"/><category term="Azure Policy"/><category term="DevOps"/><summary type="html"/></entry><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></feed>