<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://wragg.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://wragg.io/" rel="alternate" type="text/html" /><updated>2026-03-24T10:46:39+00:00</updated><id>https://wragg.io/feed.xml</id><title type="html">Mark Wragg - Blog | wragg.io</title><subtitle>The personal IT blog of Mark Wragg, an Azure DevOps Engineer in the UK.</subtitle><author><name>Mark Wragg</name><email>mark@wragg.io</email></author><entry><title type="html">Automating SSL/TLS certificate renewal in Azure</title><link href="https://wragg.io/automating-ssl-certificate-renewal-in-azure/" rel="alternate" type="text/html" title="Automating SSL/TLS certificate renewal in Azure" /><published>2026-03-07T12:00:00+00:00</published><updated>2026-03-07T12:00:00+00:00</updated><id>https://wragg.io/automating-ssl-certificate-renewal-in-azure</id><content type="html" xml:base="https://wragg.io/automating-ssl-certificate-renewal-in-azure/"><![CDATA[<blockquote>
  <p>This blog post is part of the <a href="https://www.azurespringclean.com/">Azure Spring Clean 2026</a> virtual community event, promoting well-managed Azure tenants.</p>
</blockquote>

<p><strong>The end (of annual SSL/TLS Certificate renewals) is nigh!</strong> The maximum validity of SSL/TLS certificates is being <a href="https://www.digicert.com/blog/tls-certificate-lifetimes-will-officially-reduce-to-47-days">significantly reduced over the next few years</a>, so there’s never been a better time to automate your certificate management. In Azure TLS certificates can be used in lots of different places, some of which are easier to manage than others, and for some services you may already have a degree of automation in place. But by 2029 TLS certificates will have a maximum lifetime of 47 days, so automation will be essential to ensure you can continue to keep services running without disruption and significant overhead on the team (or teams) that maintain your infrastructure.</p>

<p>In this blog post we’ll explore some of the different areas of Azure that utilise TLS certificates and how you might consider automating them.</p>

<p>The reduction of the maximum valid lifetime of TLS certificates is being implemented in the following phases:</p>

<ul>
  <li><strong>Until March 14th 2026:</strong> Max 398 days (This is/was effectively 13 months, and has been the standard since September 2020)</li>
  <li><strong>From March 15th 2026:</strong> Max 200 days (6 months + 1/2 of a 30-day month + 1 day)</li>
  <li><strong>From March 15th 2027:</strong> Max 100 days (3 months + 1/4 of a 30-day month + 1 day)</li>
  <li><strong>From March 15th 2029:</strong> Max 47 days (31 days + 1/2 of a 30-day month + 1 day)</li>
</ul>

<p>This means that by 2029 TLS certificates will need to be renewed nearly <em>8 times per year</em> (or if you don’t want to leave it until the last minute, more than likely: monthly). What’s also worth noting is that if you renew your TLS certificates before the March 14th 2026 deadline (and get the maximum 398 days), there will be no value in renewing those certificates again until after October 2026, as you’ll otherwise get a certificate with a shorter validity than you already have.</p>

<p>If this isn’t something you’ve thought much about up to now, a good first step would be to document everywhere in your infrastructure that currently uses TLS certificates. Some of the resource types to consider include:</p>

<ul>
  <li>App Service</li>
  <li>Application Gateway</li>
  <li>Front Door</li>
  <li>AKS Ingress</li>
  <li>API Management</li>
  <li>Virtual Machines (VMs) or ScaleSets (VMSS) running webserver technologies such as IIS, NGINX or Apache Tomcat</li>
  <li>Service Fabric</li>
  <li>CDN</li>
  <li>Traffic Manager</li>
  <li>Container Apps / Instances</li>
  <li>Event Grid</li>
</ul>

<blockquote>
  <p>Note: This is not an exhaustive list.</p>
</blockquote>

<p>Automating the generation of your certificates will depend on your certificate provider. There is a standard protocol for certificate renewal automation called <a href="https://en.wikipedia.org/wiki/Automatic_Certificate_Management_Environment">ACME</a> (Automated Certificate Management Environment) and a number of tools that can be implemented to automate the process with a variety of providers (including <a href="https://letsencrypt.org/">Let’s Encrypt</a>, which provides free public TLS certificates). Some certificate providers are offering their own automation platforms, but you could also consider one or more of these tools:</p>

<ul>
  <li><a href="https://poshac.me/docs/v4/">Posh-ACME</a>: A PowerShell module and ACME client to create publicly trusted SSL/TLS certificates from an ACME capable certificate authority such as Let’s Encrypt.</li>
  <li><a href="https://www.win-acme.com/">win-acme</a>: A Windows-focused ACME client (WACS) that can be scripted to manage certificate renewal, particularly for IIS and Azure-hosted workloads.</li>
  <li><a href="https://azacme.dev/">az-acme</a>: A specialized CLI designed to integrate ACME issuers (like Let’s Encrypt) directly with Azure Key Vault, storing certificates and enabling auto-rotation.</li>
  <li><a href="https://www.keytos.io/azure-pki">EZCA</a>: A dedicated Azure-native tool for automating certificate renewal across various Azure services, including support for Key Vault auto-rotation.</li>
  <li><a href="https://certbot.eff.org/">Certbot</a>: While general-purpose, Certbot can be used with hook scripts to renew certificates and push them to Azure resources.</li>
</ul>

<p>These tools not only automate the request and retrieval of a certificate, but can automate the process of domain validation, such as adding a token value as a TXT DNS entry to the domain to prove ownership. They can also automate some of the deployment and configuration scenarios, such as updating the certificate binding in IIS. Most of these tools are free and open source, so well worth consideration before you inadvertently reinvent the wheel.</p>

<p>Another tool worth considering is <a href="https://www.haveibeenexpired.com/">haveibeenexpired.com</a> which you can use to monitor your publicly accessible domains for expiring certificates. When you automate a process, you should also monitor it (trust, but verify).</p>

<p><img src="/content/images/2026/haveibeenexpired.png" alt="Have I Been Expired example output" class="align-center" /></p>

<p>While the certificate automation tools listed above offer a variety of features, you don’t necessarily need them. For most Azure resources, you just need to hook up to a Key Vault, and then have a scheduled process in place to renew the certificate within that, which can be relatively simple.</p>

<h3 id="azure-key-vault">Azure Key Vault</h3>

<p>Azure Key Vault is the recommended resource to use in Azure for provisioning, managing and deploying TLS certificates. Many other resources in Azure are designed to consume certificates directly from Key Vault, and a single Key Vault can be used to provide certificates to multiple resources.</p>

<p>You can deploy a Key Vault via Bicep as follows:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">param</span><span class="w"> </span><span class="n">location</span><span class="w"> </span><span class="nx">string</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">resourceGroup</span><span class="p">()</span><span class="o">.</span><span class="nf">location</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="n">keyVaultName</span><span class="w"> </span><span class="nx">string</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'kv-example'</span><span class="w">

</span><span class="n">resource</span><span class="w"> </span><span class="nx">keyVault</span><span class="w"> </span><span class="s1">'Microsoft.KeyVault/vaults@2023-02-01'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">name:</span><span class="w"> </span><span class="nx">keyVaultName</span><span class="w">
  </span><span class="n">location:</span><span class="w"> </span><span class="nx">location</span><span class="w">
  </span><span class="n">properties:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">tenantId:</span><span class="w"> </span><span class="nx">subscription</span><span class="p">()</span><span class="o">.</span><span class="nf">tenantId</span><span class="w">
    </span><span class="n">sku:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="n">family:</span><span class="w"> </span><span class="s1">'A'</span><span class="w">
      </span><span class="n">name:</span><span class="w"> </span><span class="s1">'standard'</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="n">enableRbacAuthorization:</span><span class="w"> </span><span class="nx">true</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>There are a few ways to <a href="https://learn.microsoft.com/en-us/azure/key-vault/certificates/certificate-scenarios">get your certificates in to Key Vault</a>:</p>

<ol>
  <li>Import a TLS certificate you’ve purchased from an external provider</li>
  <li>Generate a CSR (Certificate Signing Request) via Azure Key Vault which you can then use to complete a certificate request via an external provider</li>
  <li>Use one of the partnered CA providers (Digicert or GlobalSign) to generate and import the certificate directly. This requires having an account with either Digicert or Globalsign and providing your credentials for these within the Key Vault.</li>
</ol>

<p>When you upload a certificate into Key Vault, it imports various properties including the valid from and valid to dates, which you can then use to monitor the validity of your certificates (such as by configuring alerts in Azure Monitor). When a certificate is generated by Key Vault, you configure an email notification to be sent when a certain percentage of the certificates lifetime has been consumed. You can setup the same for imported certificates, by configuring “Certificate Contacts” on the Key Vault, and going to the Issuance Policy settings of the certificate to configure the percentage you want to be notified at. By default this is 80%.</p>

<p>When you use one of the partnered CA (Certificate Authority) providers, Key Vault refers to this as using an <a href="https://learn.microsoft.com/en-us/azure/key-vault/certificates/how-to-integrate-certificate-authority">Integrated CA</a> and you can configure automatic renewal. If you’re not using an Integrated CA, then you’ll need to automate the process of retrieving a new certificate, and then upload that into Key Vault.</p>

<p>You can do the latter pretty easily via PowerShell:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Variables</span><span class="w">
</span><span class="nv">$kv</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="s2">"my-keyvault"</span><span class="w">
</span><span class="nv">$name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"my-certificate"</span><span class="w">
</span><span class="nv">$pfx</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"C:\certs\mycert.pfx"</span><span class="w">
</span><span class="nv">$pw</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="n">ConvertTo-SecureString</span><span class="w"> </span><span class="s2">"PfxPasswordHere"</span><span class="w"> </span><span class="nt">-AsPlainText</span><span class="w"> </span><span class="nt">-Force</span><span class="w">

</span><span class="c"># Import certificate</span><span class="w">
</span><span class="n">Import-AzKeyVaultCertificate</span><span class="w"> </span><span class="nt">-VaultName</span><span class="w"> </span><span class="nv">$kv</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nv">$name</span><span class="w"> </span><span class="nt">-FilePath</span><span class="w"> </span><span class="nv">$pfx</span><span class="w"> </span><span class="nt">-Password</span><span class="w"> </span><span class="nv">$pw</span><span class="w">
</span></code></pre></div></div>

<p>Ensure you use the same <code class="language-plaintext highlighter-rouge">Name</code> value when renewing the certificate and a new version will be uploaded to the existing entry in the Key Vault.</p>

<h3 id="app-service">App Service</h3>

<p>Azure App Service allows you to create a <a href="https://learn.microsoft.com/en-us/azure/app-service/configure-ssl-certificate">free managed TLS certificate</a> (provided by Digicert), which is then fully managed by the App Service and automatically renewed prior to expiry. If you instead provide your own TLS certificate, you can either upload this directly into App Service, or import it from a Key Vault. The latter is recommended, as its then easier (per the above) to monitor and manage your certificate. When you update the certificate entry in Key Vault, App Service automatically syncs the new version within 24 hours and without downtime. You can also trigger a manual sync in App Service.</p>

<blockquote>
  <p>Note when targetting the certificate in Key Vault you must use the non-version specific URL. Doing so will ensure it always pulls the latest certificate.</p>
</blockquote>

<p>If you upload the certificate directly to App Service, you will need to modify the certificate in the App Service config, either via the Portal, or via infrastructure code such as ARM or Bicep. Managing the certificate in Key Vault decouples the management of the certificate from the configuration of the resource.</p>

<p>Here’s an example of how you might configure an App Service to use a Key Vault certificate via Bicep:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">param</span><span class="w"> </span><span class="n">location</span><span class="w"> </span><span class="nx">string</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">resourceGroup</span><span class="p">()</span><span class="o">.</span><span class="nf">location</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="n">appName</span><span class="w"> </span><span class="nx">string</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'myapp-${uniqueString(resourceGroup().id)}'</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="n">kvName</span><span class="w"> </span><span class="nx">string</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'kv-example'</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="n">certName</span><span class="w"> </span><span class="nx">string</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'mySslCert'</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="n">customHostname</span><span class="w"> </span><span class="nx">string</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'www.mycustomdomain.com'</span><span class="w">

</span><span class="n">resource</span><span class="w"> </span><span class="nx">appServicePlan</span><span class="w"> </span><span class="s1">'Microsoft.Web/serverfarms@2023-01-01'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">name:</span><span class="w"> </span><span class="s1">'asp-${appName}'</span><span class="w">
  </span><span class="n">location:</span><span class="w"> </span><span class="nx">location</span><span class="w">
  </span><span class="n">sku:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">name:</span><span class="w"> </span><span class="s1">'P1v2'</span><span class="w">
    </span><span class="n">tier:</span><span class="w"> </span><span class="s1">'PremiumV2'</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">resource</span><span class="w"> </span><span class="nx">kv</span><span class="w"> </span><span class="s1">'Microsoft.KeyVault/vaults@2023-02-01'</span><span class="w"> </span><span class="nx">existing</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">name:</span><span class="w"> </span><span class="nx">kvName</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">resource</span><span class="w"> </span><span class="nx">webApp</span><span class="w"> </span><span class="s1">'Microsoft.Web/sites@2023-01-01'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">name:</span><span class="w"> </span><span class="nx">appName</span><span class="w">
  </span><span class="n">location:</span><span class="w"> </span><span class="nx">location</span><span class="w">
  </span><span class="n">identity:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">type</span><span class="p">:</span><span class="w"> </span><span class="s1">'SystemAssigned'</span><span class="w">
  </span><span class="p">}</span><span class="w">
  </span><span class="n">properties:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">serverFarmId:</span><span class="w"> </span><span class="nx">appServicePlan.id</span><span class="w">
    </span><span class="n">httpsOnly:</span><span class="w"> </span><span class="nx">true</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">resource</span><span class="w"> </span><span class="nx">certificate</span><span class="w"> </span><span class="s1">'Microsoft.Web/certificates@2023-01-01'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">name:</span><span class="w"> </span><span class="nx">certName</span><span class="w">
  </span><span class="n">location:</span><span class="w"> </span><span class="nx">location</span><span class="w">
  </span><span class="n">properties:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">keyVaultId:</span><span class="w"> </span><span class="nx">kv.id</span><span class="w">
    </span><span class="n">keyVaultSecretName:</span><span class="w"> </span><span class="nx">certName</span><span class="w">
    </span><span class="n">serverFarmId:</span><span class="w"> </span><span class="nx">appServicePlan.id</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>If you reference the certificate from Key Vault, you need to grant the App Service permission to read the certificate via the <code class="language-plaintext highlighter-rouge">Key Vault Secrets User</code> role. The simplest way to grant this is to configure the App Service to have a System Assigned Identity and then grant this Identity this permission on the Key Vault.</p>

<h3 id="application-gateway">Application Gateway</h3>

<p>Similar to App Service, you can implement a TLS certificate in Application Gateway by either configuring it directly on the resource, or via a Key Vault secret. As before, I recommend using a Key Vault to decouple management of the certificate from configuration of the resource.</p>

<p>Here’s a partial example of how you might configure an Application Gateway to use a certificate from a Key Vault via Bicep:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">param</span><span class="w"> </span><span class="n">location</span><span class="w"> </span><span class="nx">string</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">resourceGroup</span><span class="p">()</span><span class="o">.</span><span class="nf">location</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="n">appGwName</span><span class="w"> </span><span class="nx">string</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'agw-example'</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="n">keyVaultName</span><span class="w"> </span><span class="nx">string</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'kv-example'</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="n">certSecretName</span><span class="w"> </span><span class="nx">string</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'appgw-cert'</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="n">vnetName</span><span class="w"> </span><span class="nx">string</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'agw-vnet'</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="n">subnetName</span><span class="w"> </span><span class="nx">string</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'agw-subnet'</span><span class="w">

</span><span class="n">resource</span><span class="w"> </span><span class="nx">keyVault</span><span class="w"> </span><span class="s1">'Microsoft.KeyVault/vaults@2023-02-01'</span><span class="w"> </span><span class="nx">existing</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">name:</span><span class="w"> </span><span class="nx">keyVaultName</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">resource</span><span class="w"> </span><span class="nx">appGateway</span><span class="w"> </span><span class="s1">'Microsoft.Network/applicationGateways@2023-05-01'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">name:</span><span class="w"> </span><span class="nx">appGwName</span><span class="w">
  </span><span class="n">location:</span><span class="w"> </span><span class="nx">location</span><span class="w">
  </span><span class="n">identity:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">type</span><span class="p">:</span><span class="w"> </span><span class="s1">'UserAssigned'</span><span class="w">
  </span><span class="p">}</span><span class="w">

  </span><span class="n">properties:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">sku:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="n">name:</span><span class="w"> </span><span class="s1">'Standard_v2'</span><span class="w">
      </span><span class="n">tier:</span><span class="w"> </span><span class="s1">'Standard_v2'</span><span class="w">
      </span><span class="n">capacity:</span><span class="w"> </span><span class="nx">2</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="n">gatewayIPConfigurations:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="p">{</span><span class="w">
        </span><span class="n">name:</span><span class="w"> </span><span class="s1">'gwIpConfig'</span><span class="w">
        </span><span class="n">properties:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="n">subnet:</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">id:</span><span class="w"> </span><span class="nx">resourceId</span><span class="p">(</span><span class="s1">'Microsoft.Network/virtualNetworks/subnets'</span><span class="p">,</span><span class="w"> </span><span class="n">vnetName</span><span class="p">,</span><span class="w"> </span><span class="nx">subnetName</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><span class="p">]</span><span class="w">

    </span><span class="n">frontendIPConfigurations:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="p">{</span><span class="w">
        </span><span class="n">name:</span><span class="w"> </span><span class="s1">'frontendIp'</span><span class="w">
        </span><span class="n">properties:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="n">publicIPAddress:</span><span class="w"> </span><span class="p">{</span><span class="w">
            </span><span class="n">id:</span><span class="w"> </span><span class="nx">resourceId</span><span class="p">(</span><span class="s1">'Microsoft.Network/publicIPAddresses'</span><span class="p">,</span><span class="w"> </span><span class="s1">'agw-pip'</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><span class="p">]</span><span class="w">

    </span><span class="n">frontendPorts:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="p">{</span><span class="w">
        </span><span class="n">name:</span><span class="w"> </span><span class="s1">'httpsPort'</span><span class="w">
        </span><span class="n">properties:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="n">port:</span><span class="w"> </span><span class="nx">443</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="n">sslCertificates:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="p">{</span><span class="w">
        </span><span class="n">name:</span><span class="w"> </span><span class="s1">'sslCertFromKV'</span><span class="w">
        </span><span class="n">properties:</span><span class="w"> </span><span class="p">{</span><span class="w">
          </span><span class="n">keyVaultSecretId:</span><span class="w"> </span><span class="s1">'https://${kvName}.vault.azure.net/secrets/${certSecretName}'</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="n">httpListeners:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="o">..</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>

<h3 id="front-door">Front Door</h3>

<p>The pattern for Azure Front Door is very similar to App Service and App Gateway when referencing a certificate from a Key Vault. As before, the System Identity of the Front Door will require the <code class="language-plaintext highlighter-rouge">Key Vault Secrets User</code> role on the Key Vault.</p>

<p>Here’s a Bicep example for configuring Azure Front Door with a certificate from Key Vault:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">param</span><span class="w"> </span><span class="n">profileName</span><span class="w"> </span><span class="nx">string</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'afd-profile'</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="n">keyVaultName</span><span class="w"> </span><span class="nx">string</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'kv-prod-network'</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="n">certName</span><span class="w"> </span><span class="nx">string</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'site-cert'</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="n">secretName</span><span class="w"> </span><span class="nx">string</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'site-cert-secret'</span><span class="w">

</span><span class="n">resource</span><span class="w"> </span><span class="nx">kv</span><span class="w"> </span><span class="s1">'Microsoft.KeyVault/vaults@2023-02-01'</span><span class="w"> </span><span class="nx">existing</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">name:</span><span class="w"> </span><span class="nx">keyVaultName</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">resource</span><span class="w"> </span><span class="nx">afdProfile</span><span class="w"> </span><span class="s1">'Microsoft.Cdn/profiles@2023-05-01'</span><span class="w"> </span><span class="nx">existing</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">name:</span><span class="w"> </span><span class="nx">profileName</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">resource</span><span class="w"> </span><span class="nx">afdSecret</span><span class="w"> </span><span class="s1">'Microsoft.Cdn/profiles/secrets@2023-05-01'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">name:</span><span class="w"> </span><span class="s1">'${afdProfile.name}/${secretName}'</span><span class="w">
  </span><span class="n">properties:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">parameters:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="kr">type</span><span class="p">:</span><span class="w"> </span><span class="s1">'CustomerCertificate'</span><span class="w">
      </span><span class="n">secretSource:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">id:</span><span class="w"> </span><span class="nx">kv.id</span><span class="w">
      </span><span class="p">}</span><span class="w">
      </span><span class="n">secretVersion:</span><span class="w"> </span><span class="s1">''</span><span class="w">
      </span><span class="n">secretName:</span><span class="w"> </span><span class="nx">certName</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="n">resource</span><span class="w"> </span><span class="nx">customDomain</span><span class="w"> </span><span class="s1">'Microsoft.Cdn/profiles/customDomains@2023-05-01'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">name:</span><span class="w"> </span><span class="s1">'${afdProfile.name}/mydomain'</span><span class="w">
  </span><span class="n">properties:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">hostName:</span><span class="w"> </span><span class="s1">'www.example.com'</span><span class="w">
    </span><span class="n">tlsSettings:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="n">certificateType:</span><span class="w"> </span><span class="s1">'CustomerCertificate'</span><span class="w">
      </span><span class="n">minimumTlsVersion:</span><span class="w"> </span><span class="s1">'TLS12'</span><span class="w">
      </span><span class="n">secret:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">id:</span><span class="w"> </span><span class="nx">afdSecret.id</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>

<h3 id="aks-ingress">AKS Ingress</h3>

<p>For Azure Kubernetes Service, you can also reference a certificate from a Key Vault. However there’s slight additional complexity here in that AKS cannot read from the vault directly. Instead you need to install the CSI Driver in Kubernetes, so that it can then read from the Key Vault and implement the certificate as a Kubernetes TLS secret. Once again using the System Assigned Identity of AKS is recommended for granting access to the Key Vault certificate via the <code class="language-plaintext highlighter-rouge">Key Vault Secrets User</code> role.</p>

<p>The CSI Driver can be installed via Helm:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>helm repo add csi-secrets-store-provider-azure https://azure.github.io/secrets-store-csi-driver-provider-azure/charts
helm <span class="nb">install </span>csi csi-secrets-store-provider-azure/csi-secrets-store-provider-azure
</code></pre></div></div>

<p>And then within the Kubernetes config you define a SecretProviderClass:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">secrets-store.csi.x-k8s.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">SecretProviderClass</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">tls-cert-provider</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">provider</span><span class="pi">:</span> <span class="s">azure</span>
  <span class="na">parameters</span><span class="pi">:</span>
    <span class="na">keyvaultName</span><span class="pi">:</span> <span class="s">kv-prod-network</span>
    <span class="na">objects</span><span class="pi">:</span> <span class="pi">|</span>
      <span class="s">array:</span>
        <span class="s">- |</span>
          <span class="s">objectName: site-cert</span>
          <span class="s">objectType: secret</span>
    <span class="na">tenantId</span><span class="pi">:</span> <span class="s">&lt;tenant-id&gt;</span>
</code></pre></div></div>

<p>And sync the secret to Kubernetes:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">secretObjects</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">secretName</span><span class="pi">:</span> <span class="s">ingress-tls</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">kubernetes.io/tls</span>
  <span class="na">data</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">objectName</span><span class="pi">:</span> <span class="s">site-cert</span>
    <span class="na">key</span><span class="pi">:</span> <span class="s">tls.key</span>
  <span class="pi">-</span> <span class="na">objectName</span><span class="pi">:</span> <span class="s">site-cert</span>
    <span class="na">key</span><span class="pi">:</span> <span class="s">tls.crt</span>
</code></pre></div></div>

<p>Finally you configure the Ingress:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">networking.k8s.io/v1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Ingress</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">app-ingress</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">tls</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">hosts</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">www.example.com</span>
    <span class="na">secretName</span><span class="pi">:</span> <span class="s">ingress-tls</span>
  <span class="na">rules</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">host</span><span class="pi">:</span> <span class="s">www.example.com</span>
    <span class="na">http</span><span class="pi">:</span>
      <span class="na">paths</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">path</span><span class="pi">:</span> <span class="s">/</span>
        <span class="na">pathType</span><span class="pi">:</span> <span class="s">Prefix</span>
        <span class="na">backend</span><span class="pi">:</span>
          <span class="na">service</span><span class="pi">:</span>
            <span class="na">name</span><span class="pi">:</span> <span class="s">webapp</span>
            <span class="na">port</span><span class="pi">:</span>
              <span class="na">number</span><span class="pi">:</span> <span class="m">80</span>
</code></pre></div></div>

<h3 id="api-management">API Management</h3>

<p>API Management is similar to the other services in that you can upload a TLS certificate to it directly, or reference one in an Azure Key Vault. Once again the latter is the recommendation, for the same reasons as previously stated: you separate the maintenance of the TLS certificate from the configuration of the resource. Again the API Management resource needs permission to read the certificate from the Key Vault, which can be implemented by configuring it with a System Assigned Identity and then giving that the <code class="language-plaintext highlighter-rouge">KeyVault Secrets Reader</code> role on the Key Vault.</p>

<p>When you update the certificate in the KeyVault API Management should detect the change within 4 hours and then apply it within 20 minutes. As with the other resources, it is important to reference the Key Vault secret for the certificate without a specific version, so that it always retrieves the latest.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">param</span><span class="w"> </span><span class="n">apimName</span><span class="w"> </span><span class="nx">string</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="n">location</span><span class="w"> </span><span class="nx">string</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">resourceGroup</span><span class="p">()</span><span class="o">.</span><span class="nf">location</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="n">gatewayHostname</span><span class="w"> </span><span class="nx">string</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="n">keyVaultSecretId</span><span class="w"> </span><span class="nx">string</span><span class="w">

</span><span class="n">resource</span><span class="w"> </span><span class="nx">apim</span><span class="w"> </span><span class="s1">'Microsoft.ApiManagement/service@2023-05-01-preview'</span><span class="w"> </span><span class="nx">existing</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">name:</span><span class="w"> </span><span class="nx">apimName</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">resource</span><span class="w"> </span><span class="nx">apimCustomDomain</span><span class="w"> </span><span class="s1">'Microsoft.ApiManagement/service@2023-05-01-preview'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">name:</span><span class="w"> </span><span class="nx">apim.name</span><span class="w">
  </span><span class="n">location:</span><span class="w"> </span><span class="nx">location</span><span class="w">

  </span><span class="n">properties:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">hostnameConfigurations:</span><span class="w"> </span><span class="p">[</span><span class="w">
      </span><span class="p">{</span><span class="w">
        </span><span class="kr">type</span><span class="p">:</span><span class="w"> </span><span class="s1">'Proxy'</span><span class="w">
        </span><span class="n">hostName:</span><span class="w"> </span><span class="nx">gatewayHostname</span><span class="w">

        </span><span class="n">keyVaultId:</span><span class="w"> </span><span class="nx">keyVaultSecretId</span><span class="w">

        </span><span class="n">defaultSslBinding:</span><span class="w"> </span><span class="nx">false</span><span class="w">
        </span><span class="n">negotiateClientCertificate:</span><span class="w"> </span><span class="nx">false</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>

<h3 id="virtual-machines-or-scalesets">Virtual Machines or ScaleSets</h3>

<p>You can often avoid having TLS certificates on VMs at all, by terminating TLS earlier in the network such as at an Application Gateway, Front Door or Load Balancer. However if you have a need to configure a TLS certificate on a Virtual Machine or ScaleSet there are a couple of good options.</p>

<p>One approach is to use the <a href="https://learn.microsoft.com/en-us/azure/virtual-machines/extensions/key-vault-windows">Azure Key Vault VM Extension</a>. This gives you a very similar pattern as with the other resources mentioned earlier. You reference the non-version specific URL of the certificate’s secret. The extension polls at a specified interval for changes to this secret, and when a new version is detected it downloads the certificate and installs it into the certificate store location of your choice. Here’s a Bicep example for a Windows VM (a similar configuration can be used for <a href="https://learn.microsoft.com/en-us/azure/virtual-machines/extensions/key-vault-linux">Linux VMs</a>):</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">resource</span><span class="w"> </span><span class="nx">kvExtension</span><span class="w"> </span><span class="s1">'Microsoft.Compute/virtualMachines/extensions@2023-09-01'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">name:</span><span class="w"> </span><span class="s1">'${vm.name}/KeyVaultForWindows'</span><span class="w">
  </span><span class="n">location:</span><span class="w"> </span><span class="nx">resourceGroup</span><span class="p">()</span><span class="o">.</span><span class="nf">location</span><span class="w">

  </span><span class="n">properties:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">publisher:</span><span class="w"> </span><span class="s1">'Microsoft.Azure.KeyVault'</span><span class="w">
    </span><span class="kr">type</span><span class="p">:</span><span class="w"> </span><span class="s1">'KeyVaultForWindows'</span><span class="w">
    </span><span class="n">typeHandlerVersion:</span><span class="w"> </span><span class="s1">'3.0'</span><span class="w">
    </span><span class="n">autoUpgradeMinorVersion:</span><span class="w"> </span><span class="nx">true</span><span class="w">

    </span><span class="n">settings:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="n">secretsManagementSettings:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">pollingIntervalInS:</span><span class="w"> </span><span class="s1">'3600'</span><span class="w">
        </span><span class="n">requireInitialSync:</span><span class="w"> </span><span class="nx">true</span><span class="w">
        </span><span class="n">observedCertificates:</span><span class="w"> </span><span class="p">[</span><span class="w">
          </span><span class="p">{</span><span class="w">
            </span><span class="n">url:</span><span class="w"> </span><span class="s1">'https://my-keyvault.vault.azure.net/secrets/my-cert'</span><span class="w">
            </span><span class="n">certificateStoreName:</span><span class="w"> </span><span class="s1">'My'</span><span class="w">
            </span><span class="n">certificateStoreLocation:</span><span class="w"> </span><span class="s1">'LocalMachine'</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><span class="p">}</span><span class="w">

  </span><span class="n">dependsOn:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="n">vm</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>If you’re using IIS, it can be configured to automatically rebind when a new certificate is detected.</p>

<p>Another approach is to use the <a href="https://certbot.eff.org/">certbot</a> tool mentioned earlier. This can be installed via <code class="language-plaintext highlighter-rouge">apt</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>apt update
<span class="nb">sudo </span>apt <span class="nb">install </span>certbot python3-certbot-nginx <span class="nt">-y</span>
</code></pre></div></div>

<p>And then called as follows to perform an initial retrieval of the cert:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>certbot <span class="nt">--nginx</span> <span class="nt">-d</span> app.example.com
</code></pre></div></div>

<p>This:</p>

<ul>
  <li>Validates domain ownership</li>
  <li>Generates a certificate</li>
  <li>Updates Nginx configuration automatically.</li>
</ul>

<p>Then you can setup a cron job to perform automatic renewal:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"0 0,12 * * * root /opt/certbot/bin/python -c 'import random; import time; time.sleep(random.random() * 3600)' &amp;&amp; sudo certbot renew -q"</span> | <span class="nb">sudo tee</span> <span class="nt">-a</span> /etc/crontab <span class="o">&gt;</span> /dev/null
</code></pre></div></div>

<p>There are more detailed <a href="https://certbot.eff.org/instructions">instructions on the official website</a> for a variety of Operating Systems.</p>

<h2 id="summary">Summary</h2>

<p>If you’re reading this in 2026, there’s still a good amount of time to get a handle on your certificate management before the activity becomes a monthly task. When considering how to manage certificates in Azure, I recommend the following:</p>

<ul>
  <li>Audit your estate and ensure you have a good understanding of everywhere certificates are used.</li>
  <li>Use the fully managed certificate services and automatic renewal where possible.</li>
  <li>Centralise certificate management via Key Vault, and consider minimising the number of Key Vaults used to store certificates, which reduces the number of places certificates need to be updated (if you are not able to use auto-renew). You might consider a KeyVault per environment stage for example (Dev -&gt; Staging -&gt; Production) that is then shared by each of the resources in that stage.</li>
  <li>Implement monitoring of certificates to ensure you catch expiring certificates before they become an outage.</li>
</ul>]]></content><author><name>Mark Wragg</name><email>mark@wragg.io</email></author><category term="azure" /><category term="certificates" /><category term="automation" /><category term="deployment" /><category term="bicep" /><summary type="html"><![CDATA[The maximum validity of SSL/TLS certificates is being significantly reduced over the next few years, so there’s never been a better time to automate your certificate management. In Azure TLS certificates can be used in lots of different places, some of which are easier to manage than others. By 2029 TLS certificates will have a maximum lifetime of 47 days, so automation will be essential to ensure you can continue to keep services running without disruption.]]></summary></entry><entry><title type="html">Learning Python via PowerShell</title><link href="https://wragg.io/learning-python-via-powershell/" rel="alternate" type="text/html" title="Learning Python via PowerShell" /><published>2025-05-17T09:00:00+00:00</published><updated>2025-05-17T09:00:00+00:00</updated><id>https://wragg.io/learning-python-via-powershell</id><content type="html" xml:base="https://wragg.io/learning-python-via-powershell/"><![CDATA[<style type="text/css">
  td { vertical-align: top; }

  #top_left_col {
    float:left;
    width:72%;
    padding-right: 30px;
  }
  #top_right_col {
    float:right;
    width:28%;
  }
</style>

<div id="top_left_col">

  <p>PowerShell and Python are powerful programming languages with many similarities. While PowerShell is technically a shell scripting language (like Bash), functionally it has a lot more in common with Python, and can be used to generate scripts of equal complexity.</p>

  <p>As someone with a strong familiarity with PowerShell, I’m finding it useful as I learn Python to reference the equivalent concepts in PowerShell.</p>

  <p><a href="https://blog.ironmansoftware.com/powershell-vs-python/">Adam Driscoll did this previously in 2020 and his page is incredibly helpful</a>. Below I’ve followed a similar approach, and have used the topics covered by the <a href="https://www.w3schools.com/python/default.asp">W3Schools Python tutorial</a> as a guide. For each concept I’ve provided a side by side comparison with the closest equivalent from PowerShell.</p>

  <blockquote>
    <p>The examples given below have been tested as working in PowerShell 7.4.7 and Python 3.13.3.</p>
  </blockquote>

</div>
<div id="top_right_col">
  <aside class="sidebar__right">
<nav class="toc">
      <header><h4 class="nav__title"><i class="fas fa-code"></i> Contents</h4></header>
<ul class="toc__menu" id="markdown-toc">
  <li><a href="#get-started" id="markdown-toc-get-started">Get started</a></li>
  <li><a href="#variables" id="markdown-toc-variables">Variables</a></li>
  <li><a href="#data-types" id="markdown-toc-data-types">Data Types</a></li>
  <li><a href="#strings" id="markdown-toc-strings">Strings</a></li>
  <li><a href="#arrays" id="markdown-toc-arrays">Arrays</a></li>
  <li><a href="#dictionaries" id="markdown-toc-dictionaries">Dictionaries</a></li>
  <li><a href="#dates" id="markdown-toc-dates">Dates</a></li>
  <li><a href="#files" id="markdown-toc-files">Files</a></li>
  <li><a href="#json" id="markdown-toc-json">JSON</a></li>
  <li><a href="#operators" id="markdown-toc-operators">Operators</a></li>
  <li><a href="#conditionals" id="markdown-toc-conditionals">Conditionals</a></li>
  <li><a href="#iteration" id="markdown-toc-iteration">Iteration</a></li>
  <li><a href="#functions" id="markdown-toc-functions">Functions</a></li>
  <li><a href="#classes" id="markdown-toc-classes">Classes</a></li>
  <li><a href="#modules" id="markdown-toc-modules">Modules</a></li>
  <li><a href="#exceptions" id="markdown-toc-exceptions">Exceptions</a></li>
</ul>

    </nav>
</aside>
</div>

<table>
<tr><td colspan="3"><div>
        <h3 id="get-started">Get started</h3>

        <ul>
          <li>The latest version of PowerShell is cross-platform and can be <a href="https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell?view=powershell-7.5">installed on Windows, MacOS and Linux</a>.</li>
          <li>Python can also be <a href="https://www.python.org/downloads/">installed on a variety of platforms including Windows, MacOS and Linux</a>.</li>
          <li>Indentation is important in Python and is used to associate certain blocks of code, similar to how curly braces are used in PowerShell. The number of spaces you use is up to you, but must be consistent for each indented block.</li>
        </ul>

      </div></td></tr>
<tr><th width="20%">Concept</th><th width="40%">PowerShell</th><th width="40%">Python</th></tr>

<tr>
<td>Check installed version</td>
<td>
<div>

        <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="bp">$PSVersionTable</span><span class="p">)</span><span class="o">.</span><span class="nf">PSVersion</span><span class="w">
</span></code></pre></div>        </div>

        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>7.4.7
</code></pre></div>        </div>

      </div>
</td>
<td>
<div>

        <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">python</span> <span class="o">--</span><span class="n">version</span>
</code></pre></div>        </div>

        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Python 3.13.3
</code></pre></div>        </div>

      </div>
</td>
</tr>

<tr>
<td>Hello World</td>
<td>
<div>

        <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Hello, World!"</span><span class="w">
</span></code></pre></div>        </div>

        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Hello, World!
</code></pre></div>        </div>

        <p>Alternatively use <code class="language-plaintext highlighter-rouge">Write-Output</code> to return a PowerShell object.</p>

      </div>
</td>
<td>
<div>

        <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">print</span><span class="p">(</span><span class="s">"Hello, World!"</span><span class="p">)</span>
</code></pre></div>        </div>

        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Hello, World!
</code></pre></div>        </div>

      </div>
</td>
</tr>

<tr>
<td>Comments</td>
<td>
<div>

        <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#This is a comment</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Hello, World!"</span><span class="w"> </span><span class="c">#This too</span><span class="w">

</span><span class="cm">&lt;# This is a
multiline comment #&gt;</span><span class="w">
</span></code></pre></div>        </div>

      </div>
</td>
<td>
<div>

        <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">#This is a comment
</span><span class="k">print</span><span class="p">(</span><span class="s">"Hello, World!"</span><span class="p">)</span> <span class="c1">#This too
</span>
<span class="c1"># This is a
# multiline comment
</span></code></pre></div>        </div>

        <p>Unofficially, you can also use <code class="language-plaintext highlighter-rouge">'''</code> for multiline comments, which are ignored unless used as <a href="https://www.geeksforgeeks.org/python-docstrings/">docstrings</a>.</p>

      </div>
</td>
</tr>

<tr><td colspan="3"><div>
        <h3 id="variables">Variables</h3>

        <ul>
          <li>In both languages, a variable is created the moment you first assign a value to it.</li>
          <li>Variables are scoped based on where they are declared: <a href="https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_scopes?view=powershell-7.5">PowerShell</a> | <a href="https://www.w3schools.com/python/python_scope.asp">Python</a>.</li>
          <li>Variables do not need to be declared with a specific type, and can change type after being set. You can however use casting to specify the data type.</li>
        </ul>
      </div></td></tr>
<tr width="100%"><th width="20%">Concept</th><th width="40%">PowerShell</th><th width="40%">Python</th></tr>

<tr>
<td>Creating variables</td>
<td>
<div>

        <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$myVariable</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">5</span><span class="w">
</span><span class="nv">$text</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'sometext'</span><span class="w">
</span></code></pre></div>        </div>

        <p>PowerShell variable names are case-insensitive, <code class="language-plaintext highlighter-rouge">$text</code> is the same as <code class="language-plaintext highlighter-rouge">$Text</code>.</p>

      </div>
</td>
<td>
<div>

        <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">myVariable</span> <span class="o">=</span> <span class="mi">5</span>
<span class="n">text</span> <span class="o">=</span> <span class="s">'sometext'</span>
</code></pre></div>        </div>

        <p>Python variable names are case-sensitive <code class="language-plaintext highlighter-rouge">text</code> is <strong>not</strong> the same as <code class="language-plaintext highlighter-rouge">Text</code>.</p>

      </div>
</td>
</tr>

<tr>
<td>Casting</td>
<td>
<div>

        <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="mi">3</span><span class="w">   </span><span class="c"># '3'</span><span class="w">
</span><span class="nv">$y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">int</span><span class="p">]</span><span class="mi">3</span><span class="w">
</span><span class="nv">$z</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">double</span><span class="p">]</span><span class="mf">3.14</span><span class="w">
</span></code></pre></div>        </div>

        <p><code class="language-plaintext highlighter-rouge">float</code> in PowerShell maps to <code class="language-plaintext highlighter-rouge">System.Single</code> which is a 32 bit floating-point number. <code class="language-plaintext highlighter-rouge">double</code> is used in the example above to be equivalent to the Python <code class="language-plaintext highlighter-rouge">float</code> which uses a 64 bit integer.</p>

      </div>
</td>
<td>
<div>

        <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">x</span> <span class="o">=</span> <span class="nb">str</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span>   <span class="c1"># '3'
</span><span class="n">y</span> <span class="o">=</span> <span class="nb">int</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span>
<span class="n">z</span> <span class="o">=</span> <span class="nb">float</span><span class="p">(</span><span class="mf">3.14</span><span class="p">)</span>
</code></pre></div>        </div>

        <p><code class="language-plaintext highlighter-rouge">float</code> in Python is a 64 bit floating-point number.</p>

      </div>
</td>
</tr>

<tr>
<td>Assign multiple values</td>
<td>
<div>

        <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$x</span><span class="p">,</span><span class="nv">$y</span><span class="p">,</span><span class="nv">$z</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">3</span><span class="w">
</span></code></pre></div>        </div>

      </div>
</td>
<td>
<div>

        <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="p">,</span><span class="n">z</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">3</span>
</code></pre></div>        </div>

      </div>
</td>
</tr>

<tr><td colspan="3"><div>
        <h3 id="data-types">Data Types</h3>

        <ul>
          <li>Lists and tuples are standard Python data types that store values in a sequence. Sets are another standard Python data type that also store values. The major difference is that sets, unlike lists or tuples, cannot have multiple occurrences of the same element and store unordered values.</li>
        </ul>

      </div></td></tr>
<tr width="100%"><th width="20%">Concept</th><th width="40%">PowerShell</th><th width="40%">Python</th></tr>

<tr>
<td>Built-in Data Types</td>
<td>
<div>

        <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"some string"</span><span class="w">             </span><span class="c">#string</span><span class="w">
</span><span class="nv">$x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">20</span><span class="w">                        </span><span class="c">#int</span><span class="w">
</span><span class="nv">$x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mf">20.5</span><span class="w">                      </span><span class="c">#double</span><span class="w">
</span><span class="nv">$x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"ben"</span><span class="p">,</span><span class="s2">"max"</span><span class="p">,</span><span class="s2">"kim"</span><span class="w">         </span><span class="c">#array</span><span class="w">

</span><span class="nv">$x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="o">..</span><span class="mi">5</span><span class="w">                      </span><span class="c">#range</span><span class="w">
</span><span class="nv">$x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="nx">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"ben"</span><span class="p">;</span><span class="w"> </span><span class="nx">age</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">36</span><span class="p">}</span><span class="w"> </span><span class="c">#hashtable</span><span class="w">

</span><span class="nv">$x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">                     </span><span class="c">#bool</span><span class="w">
</span></code></pre></div>        </div>

      </div>
</td>
<td>
<div>

        <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">x</span> <span class="o">=</span> <span class="s">"some string"</span>                <span class="c1">#str
</span><span class="n">x</span> <span class="o">=</span> <span class="mi">20</span>                           <span class="c1">#int
</span><span class="n">x</span> <span class="o">=</span> <span class="mf">20.5</span>                         <span class="c1">#float
</span><span class="n">x</span> <span class="o">=</span> <span class="p">[</span><span class="s">"ben"</span><span class="p">,</span><span class="s">"max"</span><span class="p">,</span><span class="s">"kim"</span><span class="p">]</span>          <span class="c1">#list
</span><span class="n">x</span> <span class="o">=</span> <span class="p">(</span><span class="s">"ben"</span><span class="p">,</span><span class="s">"max"</span><span class="p">,</span><span class="s">"kim"</span><span class="p">)</span>          <span class="c1">#tuple
</span><span class="n">x</span> <span class="o">=</span> <span class="nb">range</span><span class="p">(</span><span class="mi">6</span><span class="p">)</span>                     <span class="c1">#range
</span><span class="n">x</span> <span class="o">=</span> <span class="p">{</span><span class="s">"name"</span> <span class="p">:</span> <span class="s">"ben"</span><span class="p">,</span> <span class="s">"age"</span> <span class="p">:</span> <span class="mi">36</span><span class="p">}</span> <span class="c1">#dict
</span><span class="n">x</span> <span class="o">=</span> <span class="p">{</span><span class="s">"ben"</span><span class="p">,</span> <span class="s">"max"</span><span class="p">,</span> <span class="s">"kim"</span><span class="p">}</span>        <span class="c1">#set
</span><span class="n">x</span> <span class="o">=</span> <span class="bp">True</span>                         <span class="c1">#bool
</span></code></pre></div>        </div>

      </div>
</td>
</tr>

<tr>
<td>Assign multiple values</td>
<td>
<div>

        <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$x</span><span class="p">,</span><span class="nv">$y</span><span class="p">,</span><span class="nv">$z</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">3</span><span class="w">
</span></code></pre></div>        </div>

      </div>
</td>
<td>
<div>

        <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">x</span><span class="p">,</span><span class="n">y</span><span class="p">,</span><span class="n">z</span> <span class="o">=</span> <span class="mi">1</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">3</span>
</code></pre></div>        </div>

      </div>
</td>
</tr>

<tr><td colspan="3"><div>
        <h3 id="strings">Strings</h3>

        <ul>
          <li>In both languages, strings can be specified via single or double quotes.</li>
          <li>Both languages have a number of built-in methods you can use on strings, but they differ. For a full list of methods see these pages: <a href="http://xahlee.info/powershell/powershell_string_methods.html">PowerShell</a> | <a href="https://www.w3schools.com/python/python_ref_string.asp">Python</a></li>
        </ul>

      </div></td></tr>
<tr width="100%"><th width="20%">Concept</th><th width="40%">PowerShell</th><th width="40%">Python</th></tr>

<tr>
<td>Quotation marks</td>
<td>
<div>

        <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$str1</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'this is a string'</span><span class="w">
</span><span class="nv">$str2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"this is also a string"</span><span class="w">
</span></code></pre></div>        </div>

        <p>In PowerShell you can interpolate variables inside a double quoted string:</p>

        <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'Mark'</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"my name is </span><span class="nv">$name</span><span class="s2">"</span><span class="w">
</span><span class="n">Write-Host</span><span class="w"> </span><span class="s1">'my name is '</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nv">$name</span><span class="w">
</span></code></pre></div>        </div>

        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>my name is Mark
my name is Mark
</code></pre></div>        </div>

      </div>
</td>
<td>
<div>

        <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">str1</span> <span class="o">=</span> <span class="s">'this is a string'</span>
<span class="n">str2</span> <span class="o">=</span> <span class="s">"this is also a string"</span>
</code></pre></div>        </div>

        <p>In Python you can interpolate variables in any string. The below uses the <a href="https://www.geeksforgeeks.org/formatted-string-literals-f-strings-python/">f-string method</a>:</p>

        <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">name</span> <span class="o">=</span> <span class="s">'Mark'</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"my name is </span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">'my name is </span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s">'</span><span class="p">)</span>
</code></pre></div>        </div>

        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>my name is Mark
my name is Mark
</code></pre></div>        </div>

      </div>
</td>
</tr>

<tr>
<td>Multiline strings</td>
<td>
<div>

        <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$str1</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="sh">@"
This is
a multiline
string
"@</span><span class="w">

</span><span class="nv">$str2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="sh">@'
This is also
a multiline
string
'@</span><span class="w">
</span></code></pre></div>        </div>

      </div>
</td>
<td>
<div>

        <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">str1</span> <span class="o">=</span> <span class="s">"""
This is
a multiline
string
"""</span>

<span class="n">str2</span> <span class="o">=</span> <span class="s">'''
This is also
a multiline
string
'''</span>
</code></pre></div>        </div>

      </div>
</td>
</tr>

<tr>
<td>String length</td>
<td>
<div>

        <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"This is a string"</span><span class="o">.</span><span class="nf">length</span><span class="w">
</span></code></pre></div>        </div>

        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>16
</code></pre></div>        </div>

      </div>
</td>
<td>
<div>

        <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">print</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="s">"This is a string"</span><span class="p">))</span>
</code></pre></div>        </div>

        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>16
</code></pre></div>        </div>

      </div>
</td>
</tr>

<tr>
<td>String methods</td>
<td>
<div>

        <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$str</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'heLLo'</span><span class="w">

</span><span class="nv">$str</span><span class="o">.</span><span class="nf">padleft</span><span class="p">(</span><span class="nx">8</span><span class="p">)</span><span class="w">           </span><span class="c"># --&gt; '   heLLo'</span><span class="w">
</span><span class="nv">$str</span><span class="o">.</span><span class="nf">padright</span><span class="p">(</span><span class="nx">8</span><span class="p">)</span><span class="w">          </span><span class="c"># --&gt; 'heLLo   '</span><span class="w">
</span><span class="nv">$str</span><span class="o">.</span><span class="nf">tolower</span><span class="p">()</span><span class="w">            </span><span class="c"># --&gt; 'hello'</span><span class="w">
</span><span class="nv">$str</span><span class="o">.</span><span class="nf">toupper</span><span class="p">()</span><span class="w">            </span><span class="c"># --&gt; 'HELLO'</span><span class="w">
</span><span class="nv">$str</span><span class="o">.</span><span class="nf">replace</span><span class="p">(</span><span class="s1">'LL'</span><span class="p">,</span><span class="s1">'YY'</span><span class="p">)</span><span class="w">   </span><span class="c"># --&gt; 'heYYo'</span><span class="w">
</span></code></pre></div>        </div>

      </div>
</td>
<td>
<div>

        <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">str</span> <span class="o">=</span> <span class="s">'heLLo'</span>

<span class="nb">str</span><span class="p">.</span><span class="n">rjust</span><span class="p">(</span><span class="mi">8</span><span class="p">)</span>             <span class="c1"># --&gt; '   heLLo'
</span><span class="nb">str</span><span class="p">.</span><span class="n">ljust</span><span class="p">(</span><span class="mi">8</span><span class="p">)</span>             <span class="c1"># --&gt; 'heLLo   '
</span><span class="nb">str</span><span class="p">.</span><span class="n">lower</span><span class="p">()</span>              <span class="c1"># --&gt; 'hello'
</span><span class="nb">str</span><span class="p">.</span><span class="n">upper</span><span class="p">()</span>              <span class="c1"># --&gt; 'hello'
</span><span class="nb">str</span><span class="p">.</span><span class="n">replace</span><span class="p">(</span><span class="s">'LL'</span><span class="p">,</span><span class="s">'YY'</span><span class="p">)</span>   <span class="c1"># --&gt; 'heYYo'
</span></code></pre></div>        </div>

      </div>
</td>
</tr>

<tr><td colspan="3"><div>
        <h3 id="arrays">Arrays</h3>

        <ul>
          <li>Python does not have built-in support for arrays, but Python Lists can be used instead, as demonstrated in the below examples. You can get <a href="https://www.w3schools.com/python/numpy/numpy_creating_arrays.asp">arrays via the NumPy module</a>.</li>
          <li>PowerShell has arrays, but once defined they are a fixed length. To append a new item PowerShell recreates the entire array with the new element. You can alternatively use <code class="language-plaintext highlighter-rouge">[System.Collections.ArrayList]</code> which is a dynamic array that behaves more like Python Lists.</li>
        </ul>

      </div></td></tr>
<tr width="100%"><th width="20%">Concept</th><th width="40%">PowerShell</th><th width="40%">Python</th></tr>

<tr>
<td>Create an array</td>
<td>
<div>

        <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$basket</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="s2">"apple"</span><span class="p">,</span><span class="s2">"pear"</span><span class="p">,</span><span class="s2">"plum"</span><span class="p">)</span><span class="w">
</span><span class="nv">$basket</span><span class="w">

</span></code></pre></div>        </div>
        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apple
pear
plum
</code></pre></div>        </div>

      </div>
</td>
<td>
<div>

        <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">basket</span> <span class="o">=</span> <span class="p">[</span><span class="s">"apple"</span><span class="p">,</span><span class="s">"pear"</span><span class="p">,</span><span class="s">"plum"</span><span class="p">]</span>
<span class="k">for</span> <span class="n">fruit</span> <span class="ow">in</span> <span class="n">basket</span><span class="p">:</span>
  <span class="k">print</span><span class="p">(</span><span class="n">fruit</span><span class="p">)</span>
</code></pre></div>        </div>
        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apple
pear
plum
</code></pre></div>        </div>

      </div>
</td>
</tr>

<tr>
<td>Array length (number of elements)</td>
<td>
<div>

        <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$basket</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="s2">"apple"</span><span class="p">,</span><span class="s2">"pear"</span><span class="p">,</span><span class="s2">"plum"</span><span class="p">)</span><span class="w">
</span><span class="nv">$basket</span><span class="o">.</span><span class="nf">count</span><span class="w">
</span></code></pre></div>        </div>
        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>3
</code></pre></div>        </div>

      </div>
</td>
<td>
<div>

        <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">basket</span> <span class="o">=</span> <span class="p">[</span><span class="s">"apple"</span><span class="p">,</span><span class="s">"pear"</span><span class="p">,</span><span class="s">"plum"</span><span class="p">]</span>
<span class="k">print</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">basket</span><span class="p">))</span>
</code></pre></div>        </div>
        <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>3
</code></pre></div>        </div>

        <tr>
<td>Add an item to an array</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$basket</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="s2">"apple"</span><span class="p">,</span><span class="s2">"pear"</span><span class="p">,</span><span class="s2">"plum"</span><span class="p">)</span><span class="w">
</span><span class="nv">$basket</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="s2">"banana"</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">basket</span> <span class="o">=</span> <span class="p">[</span><span class="s">"apple"</span><span class="p">,</span><span class="s">"pear"</span><span class="p">,</span><span class="s">"plum"</span><span class="p">]</span>
<span class="n">basket</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="s">"banana"</span><span class="p">)</span>
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Remove an item from an array</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$basket</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="s2">"apple"</span><span class="p">,</span><span class="s2">"pear"</span><span class="p">,</span><span class="s2">"plum"</span><span class="p">)</span><span class="w">
</span><span class="nv">$basket</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$basket</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">where</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="s2">"banana"</span><span class="w"> </span><span class="p">}</span><span class="w">
</span></code></pre></div>              </div>
              <p>Alternatively you could define a <code class="language-plaintext highlighter-rouge">[System.Collections.ArrayList]</code> object which has <code class="language-plaintext highlighter-rouge">add</code> and <code class="language-plaintext highlighter-rouge">remove</code> methods.</p>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">basket</span> <span class="o">=</span> <span class="p">[</span><span class="s">"apple"</span><span class="p">,</span><span class="s">"pear"</span><span class="p">,</span><span class="s">"plum"</span><span class="p">]</span>
<span class="n">basket</span><span class="p">.</span><span class="n">remove</span><span class="p">(</span><span class="s">"banana"</span><span class="p">)</span>
</code></pre></div>              </div>
              <p>This will only remove the first occurrence of the specified value.</p>

            </div>
</td>
</tr>

        <tr>
<td>Sort an array</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$basket</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@(</span><span class="s2">"apple"</span><span class="p">,</span><span class="s2">"pear"</span><span class="p">,</span><span class="s2">"plum"</span><span class="p">,</span><span class="s2">"banana"</span><span class="p">)</span><span class="w">
</span><span class="nv">$basket</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Sort-Object</span><span class="w">
</span><span class="nv">$basket</span><span class="w">

</span></code></pre></div>              </div>
              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apple
banana
pear
plum
</code></pre></div>              </div>
              <p>Alternatively you could define a <code class="language-plaintext highlighter-rouge">[System.Collections.ArrayList]</code> object which has a <code class="language-plaintext highlighter-rouge">sort</code> method.</p>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">basket</span> <span class="o">=</span> <span class="p">[</span><span class="s">"apple"</span><span class="p">,</span><span class="s">"pear"</span><span class="p">,</span><span class="s">"plum"</span><span class="p">,</span><span class="s">"banana"</span><span class="p">]</span>
<span class="n">basket</span><span class="p">.</span><span class="n">sort</span><span class="p">()</span>
<span class="k">for</span> <span class="n">fruit</span> <span class="ow">in</span> <span class="n">basket</span><span class="p">:</span>
  <span class="k">print</span><span class="p">(</span><span class="n">fruit</span><span class="p">)</span>
</code></pre></div>              </div>
              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>apple
banana
pear
plum
</code></pre></div>              </div>
              <p>Sorts the list ascending by default. Use <code class="language-plaintext highlighter-rouge">sort(reverse=True)</code> to specify descending.</p>

            </div>
</td>
</tr>

        <tr><td colspan="3"><div>
              <h3 id="dictionaries">Dictionaries</h3>

              <ul>
                <li>Dictionaries are a collection of key value pairs. In PowerShell a dictionary is referred to as a <a href="https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_hash_tables?view=powershell-7.5">hashtable</a>.</li>
                <li>In both languages, dictionaries (hashtables) are changeable: you can modify/add/remove items after creation. Dictionary keys must be unique.</li>
              </ul>

            </div></td></tr>
        <tr width="100%"><th width="20%">Concept</th><th width="40%">PowerShell</th><th width="40%">Python</th></tr>

        <tr>
<td>Creation</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$car</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
  </span><span class="nx">brand</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Audi"</span><span class="w">
  </span><span class="nx">model</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Q7"</span><span class="w">
  </span><span class="nx">year</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">2019</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>              </div>

              <p>You can specify an ordered hashtable by adding the <code class="language-plaintext highlighter-rouge">[ordered]</code> type accelerator.</p>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">car</span> <span class="o">=</span> <span class="p">{</span>
  <span class="s">"brand"</span><span class="p">:</span> <span class="s">"Audi"</span><span class="p">,</span>
  <span class="s">"model"</span><span class="p">:</span> <span class="s">"Q7"</span><span class="p">,</span>
  <span class="s">"year"</span><span class="p">:</span> <span class="mi">2019</span>
<span class="p">}</span>
</code></pre></div>              </div>

              <p>As of Python 3.7, dictionaries are ordered by default. Prior to 3.7 they are unordered.</p>

            </div>
</td>
</tr>

        <tr>
<td>Return an item</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$car</span><span class="p">[</span><span class="s2">"brand"</span><span class="p">]</span><span class="w">
</span></code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Audi
</code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">print</span><span class="p">(</span><span class="n">car</span><span class="p">[</span><span class="s">"brand"</span><span class="p">])</span>
</code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Audi
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>List keys</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$car</span><span class="o">.</span><span class="nf">keys</span><span class="w">
</span></code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>year
brand
model
</code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">print</span><span class="p">(</span><span class="n">car</span><span class="p">.</span><span class="n">keys</span><span class="p">())</span>
</code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dict_keys(['brand', 'model', 'year'])
</code></pre></div>              </div>

              <p>The list of the values is a view of the dictionary. Any changes to the dictionary will be reflected in the list.</p>

            </div>
</td>
</tr>

        <tr>
<td>List items</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$car</span><span class="o">.</span><span class="nf">values</span><span class="w">
</span></code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2019
Audi
Q7
</code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">print</span><span class="p">(</span><span class="n">car</span><span class="p">.</span><span class="n">items</span><span class="p">())</span>
</code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dict_items([('brand', 'Audi'),
  ('model', 'Q7'),
  ('year', 2019)])
</code></pre></div>              </div>

              <p>The list of the values is a view of the dictionary. Any changes to the dictionary will be reflected in the list.</p>

            </div>
</td>
</tr>

        <tr>
<td>Check if a key exists</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="s1">'model'</span><span class="w"> </span><span class="nt">-in</span><span class="w"> </span><span class="nv">$car</span><span class="o">.</span><span class="nf">Keys</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="bp">$true</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="s">"model"</span> <span class="ow">in</span> <span class="n">car</span><span class="p">:</span>
  <span class="k">print</span><span class="p">(</span><span class="bp">True</span><span class="p">)</span>

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

            </div>
</td>
</tr>

        <tr>
<td>Add or update an item</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$car</span><span class="p">[</span><span class="s2">"color"</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Blue"</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">car</span><span class="p">[</span><span class="s">"color"</span><span class="p">]</span> <span class="o">=</span> <span class="s">"Blue"</span>
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Remove an item</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$car</span><span class="o">.</span><span class="nf">remove</span><span class="p">(</span><span class="s2">"color"</span><span class="p">)</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">car</span><span class="p">.</span><span class="n">pop</span><span class="p">(</span><span class="s">"color"</span><span class="p">)</span>
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Remove all items</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$car</span><span class="o">.</span><span class="nf">clear</span><span class="p">()</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">car</span><span class="p">.</span><span class="n">clear</span><span class="p">()</span>
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Dictionary length</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$car</span><span class="o">.</span><span class="nf">count</span><span class="w">
</span></code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>3
</code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">print</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="n">car</span><span class="p">))</span>
</code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>3
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr><td colspan="3"><div>
              <h3 id="dates">Dates</h3>

              <ul>
                <li>To work with dates in Python you need to import the built-in <code class="language-plaintext highlighter-rouge">datetime</code> module.</li>
              </ul>

            </div></td></tr>
        <tr width="100%"><th width="20%">Concept</th><th width="40%">PowerShell</th><th width="40%">Python</th></tr>

        <tr>
<td>Return the current date/time</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$now</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Date</span><span class="w">


</span><span class="nv">$now</span><span class="w">
</span><span class="nv">$now</span><span class="o">.</span><span class="nf">day</span><span class="w">
</span><span class="nv">$now</span><span class="o">.</span><span class="nf">month</span><span class="w">
</span><span class="nv">$now</span><span class="o">.</span><span class="nf">year</span><span class="w">
</span></code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>17 May 2025 13:27:16
17
5
2025
</code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">datetime</span>
<span class="n">now</span> <span class="o">=</span> <span class="n">datetime</span><span class="p">.</span><span class="n">datetime</span><span class="p">.</span><span class="n">now</span><span class="p">()</span>

<span class="k">print</span><span class="p">(</span><span class="n">now</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">now</span><span class="p">.</span><span class="n">day</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">now</span><span class="p">.</span><span class="n">month</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">now</span><span class="p">.</span><span class="n">year</span><span class="p">)</span>
</code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2025-05-17 12:28:25.903551
17
5
2025
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Create a date object</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Date</span><span class="w"> </span><span class="nt">-Day</span><span class="w"> </span><span class="nx">17</span><span class="w"> </span><span class="nt">-Month</span><span class="w"> </span><span class="nx">5</span><span class="w"> </span><span class="nt">-Year</span><span class="w"> </span><span class="nx">2025</span><span class="w">

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

              <p>Time parameters can also be specified, their default is the current time.</p>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">datetime</span>
<span class="n">x</span> <span class="o">=</span> <span class="n">datetime</span><span class="p">.</span><span class="n">datetime</span><span class="p">(</span><span class="mi">2025</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">17</span><span class="p">)</span>
</code></pre></div>              </div>

              <p>Time parameters can also be specified, their default is 0.</p>

            </div>
</td>
</tr>

        <tr>
<td>Formatting dates</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-Date</span><span class="w"> </span><span class="nt">-Day</span><span class="w"> </span><span class="nx">17</span><span class="w"> </span><span class="nt">-Month</span><span class="w"> </span><span class="nx">5</span><span class="w"> </span><span class="nt">-Year</span><span class="w"> </span><span class="nx">2025</span><span class="w">


</span><span class="n">Get-Date</span><span class="w"> </span><span class="nv">$x</span><span class="w"> </span><span class="nt">-UFormat</span><span class="w"> </span><span class="s1">'%a'</span><span class="w"> </span><span class="c"># --&gt; Sat</span><span class="w">
</span><span class="n">Get-Date</span><span class="w"> </span><span class="nv">$x</span><span class="w"> </span><span class="nt">-UFormat</span><span class="w"> </span><span class="s1">'%A'</span><span class="w"> </span><span class="c"># --&gt; Saturday</span><span class="w">
</span><span class="n">Get-Date</span><span class="w"> </span><span class="nv">$x</span><span class="w"> </span><span class="nt">-UFormat</span><span class="w"> </span><span class="s1">'%w'</span><span class="w"> </span><span class="c"># --&gt; 6</span><span class="w">
</span><span class="n">Get-Date</span><span class="w"> </span><span class="nv">$x</span><span class="w"> </span><span class="nt">-UFormat</span><span class="w"> </span><span class="s1">'%d'</span><span class="w"> </span><span class="c"># --&gt; 17</span><span class="w">
</span><span class="n">Get-Date</span><span class="w"> </span><span class="nv">$x</span><span class="w"> </span><span class="nt">-UFormat</span><span class="w"> </span><span class="s1">'%b'</span><span class="w"> </span><span class="c"># --&gt; May</span><span class="w">
</span><span class="n">Get-Date</span><span class="w"> </span><span class="nv">$x</span><span class="w"> </span><span class="nt">-UFormat</span><span class="w"> </span><span class="s1">'%m'</span><span class="w"> </span><span class="c"># --&gt; 05</span><span class="w">
</span><span class="n">Get-Date</span><span class="w"> </span><span class="nv">$x</span><span class="w"> </span><span class="nt">-UFormat</span><span class="w"> </span><span class="s1">'%y'</span><span class="w"> </span><span class="c"># --&gt; 25</span><span class="w">
</span><span class="n">Get-Date</span><span class="w"> </span><span class="nv">$x</span><span class="w"> </span><span class="nt">-UFormat</span><span class="w"> </span><span class="s1">'%Y'</span><span class="w"> </span><span class="c"># --&gt; 2025</span><span class="w">
</span></code></pre></div>              </div>

              <p>See here for the full list of supported <a href="https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/get-date?view=powershell-7.5#notes">Python datetime formatting codes</a>.</p>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">datetime</span>
<span class="n">x</span> <span class="o">=</span> <span class="n">datetime</span><span class="p">.</span><span class="n">datetime</span><span class="p">(</span><span class="mi">2025</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">17</span><span class="p">)</span>

<span class="n">x</span><span class="p">.</span><span class="n">strftime</span><span class="p">(</span><span class="s">"%a"</span><span class="p">)</span> <span class="c1"># --&gt; Sat
</span><span class="n">x</span><span class="p">.</span><span class="n">strftime</span><span class="p">(</span><span class="s">"%A"</span><span class="p">)</span> <span class="c1"># --&gt; Saturday
</span><span class="n">x</span><span class="p">.</span><span class="n">strftime</span><span class="p">(</span><span class="s">"%w"</span><span class="p">)</span> <span class="c1"># --&gt; 6
</span><span class="n">x</span><span class="p">.</span><span class="n">strftime</span><span class="p">(</span><span class="s">"%d"</span><span class="p">)</span> <span class="c1"># --&gt; 17
</span><span class="n">x</span><span class="p">.</span><span class="n">strftime</span><span class="p">(</span><span class="s">"%b"</span><span class="p">)</span> <span class="c1"># --&gt; May
</span><span class="n">x</span><span class="p">.</span><span class="n">strftime</span><span class="p">(</span><span class="s">"%m"</span><span class="p">)</span> <span class="c1"># --&gt; 05
</span><span class="n">x</span><span class="p">.</span><span class="n">strftime</span><span class="p">(</span><span class="s">"%y"</span><span class="p">)</span> <span class="c1"># --&gt; 25
</span><span class="n">x</span><span class="p">.</span><span class="n">strftime</span><span class="p">(</span><span class="s">"%Y"</span><span class="p">)</span> <span class="c1"># --&gt; 2025
</span></code></pre></div>              </div>

              <p>See here for the full list of supported <a href="https://docs.python.org/3/library/datetime.html#format-codes">PowerShell datetime formatting codes</a>.</p>

            </div>
</td>
</tr>

        <tr><td colspan="3"><div>
              <h3 id="files">Files</h3>

              <ul>
                <li>PowerShell has built-in cmdlets for working with files.</li>
                <li>Python has built-in functions for creating, reading and writing to files. File deletion requires the <code class="language-plaintext highlighter-rouge">os</code> module.</li>
              </ul>

            </div></td></tr>
        <tr width="100%"><th width="20%">Concept</th><th width="40%">PowerShell</th><th width="40%">Python</th></tr>

        <tr>
<td>Read from a file</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$f</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="s2">"file.txt"</span><span class="w">
</span><span class="nv">$f</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">f</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="s">"file.txt"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">f</span><span class="p">.</span><span class="n">read</span><span class="p">())</span>
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Write to a file</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Use Add-Content to append a file</span><span class="w">
</span><span class="s1">'Some content'</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Add-Content</span><span class="w"> </span><span class="s2">"file.txt"</span><span class="w">


</span><span class="c"># Use Set-Content to overwrite a file</span><span class="w">
</span><span class="s1">'New content'</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="s2">"file.txt"</span><span class="w">

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

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Use "a" to append a file
</span><span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s">"file.txt"</span><span class="p">,</span> <span class="s">"a"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
  <span class="n">f</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="s">"Some content"</span><span class="p">)</span>

<span class="c1"># Use "w" to overwrite a file
</span><span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s">"file.txt"</span><span class="p">,</span> <span class="s">"w"</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
  <span class="n">f</span><span class="p">.</span><span class="n">write</span><span class="p">(</span><span class="s">"New content"</span><span class="p">)</span>
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>New file</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Use New-Item to create a file</span><span class="w">
</span><span class="n">New-Item</span><span class="w"> </span><span class="s2">"file.txt"</span><span class="w"> </span><span class="nt">-ItemType</span><span class="w"> </span><span class="s2">"File"</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Use "x" to create a file
</span><span class="nb">open</span><span class="p">(</span><span class="s">"file.txt"</span><span class="p">,</span> <span class="s">"x"</span><span class="p">)</span>
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Delete files</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Use Remove-Item to delete a file</span><span class="w">
</span><span class="n">Remove-Item</span><span class="w"> </span><span class="s2">"file.txt"</span><span class="w">

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

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># File delete requires os module remove()
</span><span class="kn">import</span> <span class="nn">os</span>
<span class="n">os</span><span class="p">.</span><span class="n">remove</span><span class="p">(</span><span class="s">"file.txt"</span><span class="p">)</span>
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Check if a file exists</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Use Test-Path to check exists</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="n">Test-Path</span><span class="w"> </span><span class="s2">"file.txt"</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="c">#..</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># File exists requires os module exists()
</span><span class="kn">import</span> <span class="nn">os</span>
<span class="k">if</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="n">exists</span><span class="p">(</span><span class="s">"file.txt"</span><span class="p">):</span>
  <span class="c1">#..
</span></code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr><td colspan="3"><div>
              <h3 id="json">JSON</h3>

              <ul>
                <li>To work with JSON in Python you need to import the built-in <code class="language-plaintext highlighter-rouge">json</code> module.</li>
              </ul>

            </div></td></tr>
        <tr width="100%"><th width="20%">Concept</th><th width="40%">PowerShell</th><th width="40%">Python</th></tr>

        <tr>
<td>Convert from JSON</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># ConvertFrom-Json is built-in</span><span class="w">
</span><span class="nv">$x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'{"name":"Bob","age":30,"city":"Bath"}'</span><span class="w">
</span><span class="nv">$y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$x</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">$y</span><span class="o">.</span><span class="nf">name</span><span class="w">
</span><span class="nv">$y</span><span class="o">.</span><span class="nf">age</span><span class="w">
</span><span class="nv">$y</span><span class="o">.</span><span class="nf">city</span><span class="w">
</span></code></pre></div>              </div>

              <p>PowerShell converts JSON to a PSCustomObject.</p>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Bob
30
Bath
</code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">json</span>
<span class="n">x</span> <span class="o">=</span> <span class="s">'{ "name":"Bob","age":30,"city":"Bath"}'</span>
<span class="n">y</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="n">loads</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">y</span><span class="p">[</span><span class="s">"name"</span><span class="p">])</span>
<span class="k">print</span><span class="p">(</span><span class="n">y</span><span class="p">[</span><span class="s">"age"</span><span class="p">])</span>
<span class="k">print</span><span class="p">(</span><span class="n">y</span><span class="p">[</span><span class="s">"city"</span><span class="p">])</span>
</code></pre></div>              </div>

              <p>Python converts JSON to a dictionary object.</p>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Bob
30
Bath
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Convert to JSON</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># ConvertTo-Json is built-in</span><span class="w">
</span><span class="nv">$x</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
  </span><span class="nx">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Bob"</span><span class="w">
  </span><span class="nx">age</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="mi">30</span><span class="w">
  </span><span class="nx">city</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Bath"</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="nv">$y</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$x</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="nv">$y</span><span class="w">
</span></code></pre></div>              </div>

              <p>The ConvertTo-Json cmdlet converts any .NET object to JSON. The properties become the name / values, and the methods are removed.</p>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{
  "age": 30,
  "name": "Bob",
  "city": "Bath"
}
</code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">json</span>
<span class="n">x</span> <span class="o">=</span> <span class="p">{</span>
  <span class="s">"name"</span><span class="p">:</span> <span class="s">"Bob"</span><span class="p">,</span>
  <span class="s">"age"</span><span class="p">:</span> <span class="mi">30</span><span class="p">,</span>
  <span class="s">"city"</span><span class="p">:</span> <span class="s">"Bath"</span>
<span class="p">}</span>

<span class="n">y</span> <span class="o">=</span> <span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">y</span><span class="p">)</span>
</code></pre></div>              </div>

              <p>Python objects are converted into the JSON equivalent: <code class="language-plaintext highlighter-rouge">dict</code> –&gt; object, <code class="language-plaintext highlighter-rouge">list</code>, <code class="language-plaintext highlighter-rouge">tuple</code> –&gt; array, <code class="language-plaintext highlighter-rouge">str</code> –&gt; string, <code class="language-plaintext highlighter-rouge">int</code>, <code class="language-plaintext highlighter-rouge">float</code> –&gt; number, <code class="language-plaintext highlighter-rouge">True</code>/<code class="language-plaintext highlighter-rouge">False</code> –&gt; true/false, <code class="language-plaintext highlighter-rouge">None</code> –&gt; null.</p>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{"name":"John","age": 30,"city":"New York"}
</code></pre></div>              </div>

              <p>You can further customise the indentation, separators and sorting as follows (these can also be combined):</p>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">indent</span><span class="o">=</span><span class="mi">4</span><span class="p">)</span>
<span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">separators</span><span class="o">=</span><span class="p">(</span><span class="s">". "</span><span class="p">,</span> <span class="s">" = "</span><span class="p">))</span>
<span class="n">json</span><span class="p">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">x</span><span class="p">,</span> <span class="n">sort_keys</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr><td colspan="3"><div>
              <h3 id="operators">Operators</h3>

              <ul>
                <li>For a full list of operators see these pages: <a href="https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_operators?view=powershell-7.5">PowerShell</a> | <a href="https://www.w3schools.com/python/python_operators.asp">Python</a>.</li>
              </ul>

            </div></td></tr>
        <tr width="100%"><th width="20%">Concept</th><th width="40%">PowerShell</th><th width="40%">Python</th></tr>

        <tr>
<td>Arithmetic</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$a</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="nv">$b</span><span class="w">                  </span><span class="c"># Addition</span><span class="w">
</span><span class="nv">$a</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nv">$b</span><span class="w">                  </span><span class="c"># Subtraction</span><span class="w">
</span><span class="nv">$a</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nv">$b</span><span class="w">                  </span><span class="c"># Multiplication</span><span class="w">
</span><span class="nv">$a</span><span class="w"> </span><span class="n">/</span><span class="w"> </span><span class="nv">$b</span><span class="w">                  </span><span class="c"># Division</span><span class="w">
</span><span class="nv">$a</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="nv">$b</span><span class="w">                  </span><span class="c"># Modulus</span><span class="w">
</span><span class="p">[</span><span class="n">Math</span><span class="p">]::</span><span class="n">Pow</span><span class="p">(</span><span class="nv">$a</span><span class="p">,</span><span class="w"> </span><span class="nv">$b</span><span class="p">)</span><span class="w">      </span><span class="c"># Exponentiation</span><span class="w">
</span><span class="p">[</span><span class="n">math</span><span class="p">]::</span><span class="n">floor</span><span class="p">(</span><span class="nv">$a</span><span class="w"> </span><span class="n">/</span><span class="w"> </span><span class="nv">$b</span><span class="p">)</span><span class="w">   </span><span class="c"># Floor division</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">a</span> <span class="o">+</span> <span class="n">b</span>    <span class="c1"># Addition
</span><span class="n">a</span> <span class="o">-</span> <span class="n">b</span>    <span class="c1"># Subtraction
</span><span class="n">a</span> <span class="o">*</span> <span class="n">b</span>    <span class="c1"># Multiplication
</span><span class="n">a</span> <span class="o">/</span> <span class="n">b</span>    <span class="c1"># Division
</span><span class="n">a</span> <span class="o">%</span> <span class="n">b</span>    <span class="c1"># Modulus
</span><span class="n">a</span> <span class="o">**</span> <span class="n">b</span>   <span class="c1"># Exponentiation
</span><span class="n">a</span> <span class="o">//</span> <span class="n">b</span>   <span class="c1"># Floor division
</span></code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Assignment</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$x</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="mi">3</span><span class="w">   </span><span class="c"># --&gt; x = x + 3 (add)</span><span class="w">
</span><span class="nv">$x</span><span class="w"> </span><span class="o">-=</span><span class="w"> </span><span class="mi">3</span><span class="w">   </span><span class="c"># --&gt; x = x - 3 (subtract)</span><span class="w">
</span><span class="nv">$x</span><span class="w"> </span><span class="o">*=</span><span class="w"> </span><span class="mi">3</span><span class="w">   </span><span class="c"># --&gt; x = x * 3 (multiply)</span><span class="w">
</span><span class="nv">$x</span><span class="w"> </span><span class="n">/</span><span class="o">=</span><span class="w"> </span><span class="mi">3</span><span class="w">   </span><span class="c"># --&gt; x = x / 3 (divide)</span><span class="w">
</span><span class="nv">$x</span><span class="w"> </span><span class="o">%=</span><span class="w"> </span><span class="mi">3</span><span class="w">   </span><span class="c"># --&gt; x = x % 3 (modulus)</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">x</span> <span class="o">+=</span> <span class="mi">3</span>    <span class="c1"># --&gt; x = x + 3 (add)
</span><span class="n">x</span> <span class="o">-=</span> <span class="mi">3</span>    <span class="c1"># --&gt; x = x - 3 (subtract)
</span><span class="n">x</span> <span class="o">*=</span> <span class="mi">3</span>    <span class="c1"># --&gt; x = x * 3 (multiply)
</span><span class="n">x</span> <span class="o">/=</span> <span class="mi">3</span>    <span class="c1"># --&gt; x = x / 3 (divide)
</span><span class="n">x</span> <span class="o">%=</span> <span class="mi">3</span>    <span class="c1"># --&gt; x = x % 3 (modulus)
</span><span class="n">x</span> <span class="o">**=</span> <span class="mi">3</span>   <span class="c1"># --&gt; x = x ** 3 (exponentiation)
</span><span class="n">x</span> <span class="o">//=</span> <span class="mi">3</span>   <span class="c1"># --&gt; x = x // 3 (floor divide)
</span></code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Comparison</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$a</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="nv">$b</span><span class="w">   </span><span class="c"># Equals</span><span class="w">
</span><span class="nv">$a</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="nv">$b</span><span class="w">   </span><span class="c"># Not equals</span><span class="w">
</span><span class="nv">$a</span><span class="w"> </span><span class="o">-lt</span><span class="w"> </span><span class="nv">$b</span><span class="w">   </span><span class="c"># Less than</span><span class="w">
</span><span class="nv">$a</span><span class="w"> </span><span class="o">-le</span><span class="w"> </span><span class="nv">$b</span><span class="w">   </span><span class="c"># Less than or equal to</span><span class="w">
</span><span class="nv">$a</span><span class="w"> </span><span class="o">-gt</span><span class="w"> </span><span class="nv">$b</span><span class="w">   </span><span class="c"># Greater than</span><span class="w">
</span><span class="nv">$a</span><span class="w"> </span><span class="o">-ge</span><span class="w"> </span><span class="nv">$b</span><span class="w">   </span><span class="c"># Greater than or equal to</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">a</span> <span class="o">==</span> <span class="n">b</span>   <span class="c1"># Equals
</span><span class="n">a</span> <span class="o">!=</span> <span class="n">b</span>   <span class="c1"># Not equals
</span><span class="n">a</span> <span class="o">&lt;</span> <span class="n">b</span>    <span class="c1"># Less than
</span><span class="n">a</span> <span class="o">&lt;=</span> <span class="n">b</span>   <span class="c1"># Less than or equal to
</span><span class="n">a</span> <span class="o">&gt;</span> <span class="n">b</span>    <span class="c1"># Greater than
</span><span class="n">a</span> <span class="o">&gt;=</span> <span class="n">b</span>   <span class="c1"># Greater than or equal to
</span></code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Escape characters</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s2">"This is </span><span class="se">`"</span><span class="s2">how</span><span class="se">`"</span><span class="s2"> to escape"</span><span class="w"> </span><span class="c"># Escape next</span><span class="w">
</span><span class="s2">"This is </span><span class="se">`n</span><span class="s2"> on two lines"</span><span class="w">   </span><span class="c"># new line</span><span class="w">
</span><span class="s2">"</span><span class="se">`t</span><span class="s2">This has a tab space"</span><span class="w">    </span><span class="c"># tab space</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"This is </span><span class="se">\"</span><span class="s">how</span><span class="se">\"</span><span class="s"> to escape"</span> <span class="c1"># Escape next
</span><span class="s">"This is </span><span class="se">\n</span><span class="s"> on two lines"</span>   <span class="c1"># new line
</span><span class="s">"</span><span class="se">\t</span><span class="s">This has a tab space"</span>    <span class="c1"># tab space
</span></code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Logical</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nv">$a</span><span class="w"> </span><span class="o">-gt</span><span class="w"> </span><span class="mi">5</span><span class="p">)</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="p">(</span><span class="nv">$b</span><span class="w"> </span><span class="o">-lt</span><span class="w"> </span><span class="mi">10</span><span class="p">)</span><span class="w">
</span><span class="p">(</span><span class="nv">$a</span><span class="w"> </span><span class="o">-gt</span><span class="w"> </span><span class="mi">5</span><span class="p">)</span><span class="w"> </span><span class="o">-or</span><span class="w"> </span><span class="p">(</span><span class="nv">$b</span><span class="w"> </span><span class="o">-lt</span><span class="w"> </span><span class="mi">10</span><span class="p">)</span><span class="w">
</span><span class="o">-not</span><span class="w"> </span><span class="p">(</span><span class="nv">$a</span><span class="w"> </span><span class="o">-gt</span><span class="w"> </span><span class="mi">5</span><span class="w"> </span><span class="o">-and</span><span class="w"> </span><span class="nv">$b</span><span class="w"> </span><span class="o">-lt</span><span class="w"> </span><span class="mi">10</span><span class="p">)</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="n">a</span> <span class="o">&gt;</span> <span class="mi">5</span><span class="p">)</span> <span class="ow">and</span> <span class="p">(</span><span class="n">b</span> <span class="o">&lt;</span> <span class="mi">10</span><span class="p">)</span>
<span class="p">(</span><span class="n">a</span> <span class="o">&gt;</span> <span class="mi">5</span><span class="p">)</span> <span class="ow">or</span> <span class="p">(</span><span class="n">b</span> <span class="o">&lt;</span> <span class="mi">10</span><span class="p">)</span>
<span class="ow">not</span><span class="p">(</span><span class="n">a</span> <span class="o">&gt;</span> <span class="mi">5</span> <span class="ow">and</span> <span class="n">b</span> <span class="o">&lt;</span> <span class="mi">10</span><span class="p">)</span>
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Membership</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$a</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">3</span><span class="w">
</span><span class="mi">3</span><span class="w"> </span><span class="nt">-in</span><span class="w"> </span><span class="nv">$a</span><span class="w">       </span><span class="c"># True</span><span class="w">
</span><span class="mi">4</span><span class="w"> </span><span class="nt">-in</span><span class="w"> </span><span class="nv">$a</span><span class="w">       </span><span class="c"># False</span><span class="w">
</span><span class="mi">3</span><span class="w"> </span><span class="nt">-notin</span><span class="w"> </span><span class="nv">$a</span><span class="w">    </span><span class="c"># False</span><span class="w">
</span></code></pre></div>              </div>

              <p>PowerShell also has the <code class="language-plaintext highlighter-rouge">-contains</code> operator.</p>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">a</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span><span class="mi">2</span><span class="p">,</span><span class="mi">3</span><span class="p">)</span>
<span class="mi">3</span> <span class="ow">in</span> <span class="n">a</span>     <span class="c1"># True
</span><span class="mi">4</span> <span class="ow">in</span> <span class="n">a</span>     <span class="c1"># False
</span><span class="mi">3</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">a</span> <span class="c1"># False
</span></code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr><td colspan="3"><div>
              <h3 id="conditionals">Conditionals</h3>

              <ul>
                <li>Python uses indentation to define the code that belongs to a conditional statement. PowerShell uses curly braces.</li>
              </ul>

            </div></td></tr>
        <tr width="100%"><th width="20%">Concept</th><th width="40%">PowerShell</th><th width="40%">Python</th></tr>

        <tr>
<td>if / else-if / else</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$b</span><span class="w"> </span><span class="o">-gt</span><span class="w"> </span><span class="nv">$a</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"b is greater than a"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">elseif</span><span class="w"> </span><span class="p">(</span><span class="nv">$a</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="nv">$b</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"a and b are equal"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"a is greater than b"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="n">b</span> <span class="o">&gt;</span> <span class="n">a</span><span class="p">:</span>
  <span class="k">print</span><span class="p">(</span><span class="s">"b is greater than a"</span><span class="p">)</span>

<span class="k">elif</span> <span class="n">a</span> <span class="o">==</span> <span class="n">b</span><span class="p">:</span>
  <span class="k">print</span><span class="p">(</span><span class="s">"a and b are equal"</span><span class="p">)</span>

<span class="k">else</span><span class="p">:</span>
  <span class="k">print</span><span class="p">(</span><span class="s">"a is greater than b"</span><span class="p">)</span>

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

            </div>
</td>
</tr>

        <tr>
<td>switch / match</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">switch</span><span class="w"> </span><span class="p">(</span><span class="nv">$day</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="mi">1</span><span class="o">..</span><span class="mi">5</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"weekday"</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="mi">6</span><span class="o">..</span><span class="mi">7</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"weekend"</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="n">default</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="kr">throw</span><span class="w"> </span><span class="s2">"day out of range"</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>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">match</span> <span class="n">day</span><span class="p">:</span>
  <span class="n">case</span> <span class="mi">1</span> <span class="o">|</span> <span class="mi">2</span> <span class="o">|</span> <span class="mi">3</span> <span class="o">|</span> <span class="mi">4</span> <span class="o">|</span> <span class="mi">5</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"weekday"</span><span class="p">)</span>

  <span class="n">case</span> <span class="mi">6</span> <span class="o">|</span> <span class="mi">7</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"weekend"</span><span class="p">)</span>

  <span class="n">case</span> <span class="n">_</span><span class="p">:</span>
    <span class="k">raise</span> <span class="nb">Exception</span><span class="p">(</span><span class="s">"day out of range"</span><span class="p">)</span>


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

            </div>
</td>
</tr>

        <tr><td colspan="3"><div>
              <h3 id="iteration">Iteration</h3>

              <ul>
                <li>Python doesn’t have a <code class="language-plaintext highlighter-rouge">do</code>..<code class="language-plaintext highlighter-rouge">until</code> loop, but you can achieve the same result by negating the condition of a <code class="language-plaintext highlighter-rouge">while</code> loop.</li>
              </ul>

            </div></td></tr>
        <tr width="100%"><th width="20%">Concept</th><th width="40%">PowerShell</th><th width="40%">Python</th></tr>

        <tr>
<td>While Loops</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="w">

</span><span class="kr">while</span><span class="w"> </span><span class="p">(</span><span class="nv">$i</span><span class="w"> </span><span class="o">-le</span><span class="w"> </span><span class="mi">5</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nv">$i</span><span class="w">
  </span><span class="nv">$i</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="mi">1</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">i</span> <span class="o">=</span> <span class="mi">1</span>

<span class="k">while</span> <span class="n">i</span> <span class="o">&lt;=</span> <span class="mi">5</span><span class="p">:</span>
  <span class="k">print</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>
  <span class="n">i</span> <span class="o">+=</span> <span class="mi">1</span>

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

            </div>
</td>
</tr>

        <tr>
<td>For Loops</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">for</span><span class="w"> </span><span class="p">(</span><span class="nv">$i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w"> </span><span class="nv">$i</span><span class="w"> </span><span class="o">-lt</span><span class="w"> </span><span class="mi">21</span><span class="p">;</span><span class="w"> </span><span class="nv">$i</span><span class="o">++</span><span class="p">){</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="nv">$i</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>              </div>

              <p>Iterate a set number of times at a custom increment (2 in this example):</p>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">for</span><span class="w"> </span><span class="p">(</span><span class="nv">$i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w"> </span><span class="nv">$i</span><span class="w"> </span><span class="o">-lt</span><span class="w"> </span><span class="mi">21</span><span class="p">;</span><span class="w"> </span><span class="nv">$i</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="mi">2</span><span class="p">){</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="nv">$i</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">21</span><span class="p">):</span>
    <span class="k">print</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>

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

              <p>Iterate a set number of times at a custom increment (2 in this example):</p>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">21</span><span class="p">,</span> <span class="mi">2</span><span class="p">):</span>
    <span class="k">print</span><span class="p">(</span><span class="n">i</span><span class="p">)</span>

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

            </div>
</td>
</tr>

        <tr>
<td>ForEach Loops</td>
<td>
<div>

              <p>Iterate over a collection with a <code class="language-plaintext highlighter-rouge">foreach</code> loop:</p>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$fruits</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"apple"</span><span class="p">,</span><span class="w"> </span><span class="s2">"banana"</span><span class="p">,</span><span class="w"> </span><span class="s2">"cherry"</span><span class="w">

</span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$fruit</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$fruits</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">Write-Host</span><span class="w"> </span><span class="nv">$fruit</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>              </div>

              <p>PowerShell also has <code class="language-plaintext highlighter-rouge">foreach-object</code> for iterating objects via the pipeline.</p>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$fruits</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">foreach-object</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">Write-Host</span><span class="w"> </span><span class="bp">$_</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <p>Iterate over a list with a <code class="language-plaintext highlighter-rouge">for</code> loop:</p>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fruits</span> <span class="o">=</span> <span class="p">[</span><span class="s">"apple"</span><span class="p">,</span> <span class="s">"banana"</span><span class="p">,</span> <span class="s">"cherry"</span><span class="p">]</span>

<span class="k">for</span> <span class="n">fruit</span> <span class="ow">in</span> <span class="n">fruits</span><span class="p">:</span>
  <span class="k">print</span><span class="p">(</span><span class="n">fruit</span><span class="p">)</span>

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

            </div>
</td>
</tr>

        <tr><td colspan="3"><div>
              <h3 id="functions">Functions</h3>

              <ul>
                <li>Python uses indentation to define the code that belongs to a function. PowerShell uses curly braces.</li>
                <li>In PowerShell a <code class="language-plaintext highlighter-rouge">param()</code> block can also be used to define function input parameters (arguments), with additional validation.</li>
              </ul>

            </div></td></tr>
        <tr width="100%"><th width="20%">Concept</th><th width="40%">PowerShell</th><th width="40%">Python</th></tr>

        <tr>
<td>Defining and calling a function</td>
<td>
<div>

              <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">sayHello</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Hello"</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">sayHello</span><span class="w">
</span></code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Hello
</code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">sayHello</span><span class="p">():</span>
  <span class="k">print</span><span class="p">(</span><span class="s">"Hello"</span><span class="p">)</span>


<span class="n">sayHello</span><span class="p">()</span>
</code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Hello
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Function arguments / parameters</td>
<td>
<div>

              <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">sayHello</span><span class="p">(</span><span class="nv">$name</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Hello </span><span class="nv">$name</span><span class="s2">"</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">sayHello</span><span class="w"> </span><span class="s2">"Sarah"</span><span class="w">
</span></code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Hello Sarah
</code></pre></div>              </div>

              <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">sayHello</span><span class="p">(</span><span class="nv">$fname</span><span class="p">,</span><span class="w"> </span><span class="nv">$lname</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Hello </span><span class="nv">$fname</span><span class="s2"> </span><span class="nv">$lname</span><span class="s2">"</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">sayHello</span><span class="w"> </span><span class="s2">"Bob"</span><span class="w"> </span><span class="s2">"Bilby"</span><span class="w">
</span></code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Hello Bob Bilby
</code></pre></div>              </div>

              <p>Parameters can be invoked by name:</p>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sayHello</span> <span class="o">-</span><span class="n">fname</span> <span class="s">"Bob"</span> <span class="o">-</span><span class="n">lname</span> <span class="s">"Bilby"</span>
</code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">sayHello</span><span class="p">(</span><span class="n">name</span><span class="p">):</span>
  <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Hello </span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>


<span class="n">sayHello</span><span class="p">(</span><span class="s">"Sarah"</span><span class="p">)</span>
</code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Hello Sarah
</code></pre></div>              </div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">sayHello</span><span class="p">(</span><span class="n">fname</span><span class="p">,</span><span class="n">lname</span><span class="p">):</span>
  <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Hello </span><span class="si">{</span><span class="n">fname</span><span class="si">}</span><span class="s"> </span><span class="si">{</span><span class="n">lname</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>


<span class="n">sayHello</span><span class="p">(</span><span class="s">"Bob"</span><span class="p">,</span> <span class="s">"Bilby"</span><span class="p">)</span>
</code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Hello Bob Bilby
</code></pre></div>              </div>

              <p>Parameters can be invoked by name:</p>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">sayHello</span><span class="p">(</span><span class="n">fname</span> <span class="o">=</span> <span class="s">"Bob"</span><span class="p">,</span> <span class="n">lname</span> <span class="o">=</span> <span class="s">"Bilby"</span><span class="p">)</span>
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Argument / parameter default values</td>
<td>
<div>

              <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">sayHello</span><span class="p">(</span><span class="nv">$name</span><span class="p">,</span><span class="w"> </span><span class="nv">$co</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"BobCo"</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Hello </span><span class="nv">$name</span><span class="s2"> from </span><span class="nv">$co</span><span class="s2">"</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">sayHello</span><span class="w"> </span><span class="s2">"Jeff"</span><span class="w">
</span></code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Hello Jeff from BobCo
</code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">sayHello</span><span class="p">(</span><span class="n">name</span><span class="p">,</span><span class="n">co</span> <span class="o">=</span> <span class="s">"BobCo"</span><span class="p">):</span>
  <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Hello </span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s"> from </span><span class="si">{</span><span class="n">co</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>


<span class="n">sayHello</span><span class="p">(</span><span class="s">"Jeff"</span><span class="p">)</span>
</code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Hello Jeff from BobCo
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Arbitrary arguments *args</td>
<td>
<div>

              <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">sayHello</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$name</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="bp">$args</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Hello </span><span class="nv">$name</span><span class="s2">"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">sayHello</span><span class="w"> </span><span class="s2">"Jeff"</span><span class="w"> </span><span class="s2">"Bob"</span><span class="w"> </span><span class="s2">"Sarah"</span><span class="w">
</span></code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Hello Jeff
Hello Bob
Hello Sarah
</code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">sayHello</span><span class="p">(</span><span class="o">*</span><span class="n">names</span><span class="p">):</span>
  <span class="k">for</span> <span class="n">name</span> <span class="ow">in</span> <span class="n">names</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Hello </span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>



<span class="n">sayHello</span><span class="p">(</span><span class="s">"Jeff"</span><span class="p">,</span> <span class="s">"Bob"</span><span class="p">,</span> <span class="s">"Sarah"</span><span class="p">)</span>
</code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Hello Jeff
Hello Bob
Hello Sarah
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Arbitrary keyword arguments **kwargs</td>
<td>
<div>

              <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">personDetails</span><span class="p">(</span><span class="nv">$person</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$p</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$person</span><span class="o">.</span><span class="nf">getenumerator</span><span class="p">())</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"</span><span class="si">$(</span><span class="nv">$p</span><span class="o">.</span><span class="nf">key</span><span class="si">)</span><span class="se">`:</span><span class="s2"> </span><span class="si">$(</span><span class="nv">$p</span><span class="o">.</span><span class="nf">value</span><span class="si">)</span><span class="s2">"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">personDetails</span><span class="w"> </span><span class="p">@{</span><span class="nx">fn</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Bob"</span><span class="p">;</span><span class="w"> </span><span class="nx">ln</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"Bilby"</span><span class="p">}</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">personDetails</span><span class="p">(</span><span class="o">**</span><span class="n">person</span><span class="p">):</span>
  <span class="k">for</span> <span class="n">key</span> <span class="ow">in</span> <span class="n">person</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">key</span><span class="si">}</span><span class="s">: </span><span class="si">{</span><span class="n">person</span><span class="p">[</span><span class="n">key</span><span class="p">]</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>


<span class="n">personDetails</span><span class="p">(</span><span class="n">fn</span> <span class="o">=</span> <span class="s">"Bob"</span><span class="p">,</span> <span class="n">ln</span> <span class="o">=</span> <span class="s">"Bilby"</span><span class="p">)</span>
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Return values</td>
<td>
<div>

              <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">multiplyBy5</span><span class="w"> </span><span class="p">(</span><span class="nv">$x</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="kr">return</span><span class="w"> </span><span class="p">(</span><span class="nv">$x</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="mi">5</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="n">multiplyBy5</span><span class="w"> </span><span class="nx">5</span><span class="w">
</span></code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>25
</code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">multiplyBy5</span> <span class="p">(</span><span class="n">x</span><span class="p">):</span>
    <span class="k">return</span> <span class="p">(</span><span class="n">x</span> <span class="o">*</span> <span class="mi">5</span><span class="p">)</span>


<span class="k">print</span><span class="p">(</span><span class="n">multiplyBy5</span><span class="p">(</span><span class="mi">5</span><span class="p">))</span>
</code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>25
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Empty function</td>
<td>
<div>

              <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">emptyFunction</span><span class="w"> </span><span class="p">{}</span><span class="w">

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

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">emptyFunction</span><span class="p">:</span>
  <span class="k">pass</span>
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr><td colspan="3"><div>
              <h3 id="classes">Classes</h3>

              <ul>
                <li>Both languages are object oriented. Almost everything is an object with properties and methods.</li>
                <li>A Class is a way to define a custom object’s structure.</li>
              </ul>

            </div></td></tr>
        <tr width="100%"><th width="20%">Concept</th><th width="40%">PowerShell</th><th width="40%">Python</th></tr>

        <tr>
<td>Class with a fixed property</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">Class</span><span class="w"> </span><span class="nc">Customer</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nv">$Bank</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"GlobalBank"</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Example usage:</span><span class="w">
</span><span class="nv">$c</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">Customer</span><span class="p">]::</span><span class="n">new</span><span class="p">()</span><span class="w">
</span><span class="nv">$c</span><span class="o">.</span><span class="nf">Bank</span><span class="w">
</span></code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GlobalBank
</code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Customer</span><span class="p">:</span>
  <span class="n">Bank</span> <span class="o">=</span> <span class="s">"GlobalBank"</span>

<span class="c1"># Example usage:
</span><span class="n">c</span> <span class="o">=</span> <span class="n">Customer</span><span class="p">()</span>
<span class="k">print</span><span class="p">(</span><span class="n">c</span><span class="p">.</span><span class="n">Bank</span><span class="p">)</span>
</code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GlobalBank
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Class with assignable properties</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">Class</span><span class="w"> </span><span class="nc">Customer</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$name</span><span class="w">
  </span><span class="p">[</span><span class="n">decimal</span><span class="p">]</span><span class="nv">$balance</span><span class="w">

  </span><span class="nv">$Bank</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"GlobalBank"</span><span class="w">

  </span><span class="n">Customer</span><span class="p">(</span><span class="nv">$name</span><span class="p">,</span><span class="w"> </span><span class="nv">$balance</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="bp">$this</span><span class="o">.</span><span class="nf">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$name</span><span class="w">
    </span><span class="bp">$this</span><span class="o">.</span><span class="nf">balance</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$balance</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Example usage:</span><span class="w">
</span><span class="nv">$c</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">Customer</span><span class="p">]::</span><span class="n">new</span><span class="p">(</span><span class="s2">"Alice"</span><span class="p">,</span><span class="mi">100</span><span class="p">)</span><span class="w">
</span><span class="nv">$c</span><span class="o">.</span><span class="nf">name</span><span class="w">
</span><span class="nv">$c</span><span class="o">.</span><span class="nf">balance</span><span class="w">
</span></code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Alice
100
</code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Customer</span><span class="p">:</span>
  <span class="n">bank</span> <span class="o">=</span> <span class="s">"GlobalBank"</span>

  <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="n">balance</span><span class="o">=</span><span class="mi">0</span><span class="p">):</span>
    <span class="bp">self</span><span class="p">.</span><span class="n">name</span> <span class="o">=</span> <span class="n">name</span>
    <span class="bp">self</span><span class="p">.</span><span class="n">balance</span> <span class="o">=</span> <span class="n">balance</span>

<span class="c1"># Example usage:
</span><span class="n">c</span> <span class="o">=</span> <span class="n">Customer</span><span class="p">(</span><span class="s">"Alice"</span><span class="p">,</span><span class="mi">100</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">c</span><span class="p">.</span><span class="n">name</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">c</span><span class="p">.</span><span class="n">balance</span><span class="p">)</span>
</code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Alice
100
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Class with properties and methods</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">Class</span><span class="w"> </span><span class="nc">Customer</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="nv">$name</span><span class="w">
  </span><span class="p">[</span><span class="n">decimal</span><span class="p">]</span><span class="nv">$balance</span><span class="w">

  </span><span class="nv">$Bank</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"GlobalBank"</span><span class="w">

  </span><span class="n">Customer</span><span class="p">(</span><span class="nv">$name</span><span class="p">,</span><span class="w"> </span><span class="nv">$balance</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="bp">$this</span><span class="o">.</span><span class="nf">name</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$name</span><span class="w">
    </span><span class="bp">$this</span><span class="o">.</span><span class="nf">balance</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$balance</span><span class="w">
  </span><span class="p">}</span><span class="w">

  </span><span class="p">[</span><span class="n">void</span><span class="p">]</span><span class="n">Deposit</span><span class="p">(</span><span class="nv">$amount</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$amount</span><span class="w"> </span><span class="o">-le</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="kr">throw</span><span class="w"> </span><span class="s2">"Negative deposit."</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="bp">$this</span><span class="o">.</span><span class="nf">balance</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="nv">$amount</span><span class="w">
  </span><span class="p">}</span><span class="w">

  </span><span class="p">[</span><span class="n">void</span><span class="p">]</span><span class="n">Withdraw</span><span class="p">(</span><span class="nv">$amount</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$amount</span><span class="w"> </span><span class="o">-gt</span><span class="w"> </span><span class="bp">$this</span><span class="o">.</span><span class="nf">balance</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="kr">throw</span><span class="w"> </span><span class="s2">"Insufficient funds."</span><span class="w">
    </span><span class="p">}</span><span class="w">
    </span><span class="bp">$this</span><span class="o">.</span><span class="nf">balance</span><span class="w"> </span><span class="o">-=</span><span class="w"> </span><span class="nv">$amount</span><span class="w">
  </span><span class="p">}</span><span class="w">

  </span><span class="p">[</span><span class="n">string</span><span class="p">]</span><span class="n">ToString</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$fbalance</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"£{0:N2}"</span><span class="w"> </span><span class="nt">-f</span><span class="w"> </span><span class="bp">$this</span><span class="o">.</span><span class="nf">balance</span><span class="w">
    </span><span class="kr">return</span><span class="w"> </span><span class="s2">"</span><span class="si">$(</span><span class="bp">$this</span><span class="o">.</span><span class="nf">name</span><span class="si">)</span><span class="s2">: </span><span class="nv">$fbalance</span><span class="s2">"</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Example usage:</span><span class="w">
</span><span class="nv">$c</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">Customer</span><span class="p">]::</span><span class="n">new</span><span class="p">(</span><span class="s2">"Alice"</span><span class="p">,</span><span class="w"> </span><span class="mi">100</span><span class="p">)</span><span class="w">
</span><span class="nv">$c</span><span class="o">.</span><span class="nf">Deposit</span><span class="p">(</span><span class="nx">50</span><span class="p">)</span><span class="w">
</span><span class="nv">$c</span><span class="o">.</span><span class="nf">Withdraw</span><span class="p">(</span><span class="nx">30</span><span class="p">)</span><span class="w">
</span><span class="nv">$c</span><span class="o">.</span><span class="nf">ToString</span><span class="p">()</span><span class="w">
</span></code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Alice: £120.00
</code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">Customer</span><span class="p">:</span>

  <span class="n">bank</span> <span class="o">=</span> <span class="s">"GlobalBank"</span>

  <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">name</span><span class="p">,</span> <span class="n">balance</span><span class="o">=</span><span class="mi">0</span><span class="p">):</span>
    <span class="bp">self</span><span class="p">.</span><span class="n">name</span> <span class="o">=</span> <span class="n">name</span>
    <span class="bp">self</span><span class="p">.</span><span class="n">balance</span> <span class="o">=</span> <span class="n">balance</span>

  <span class="k">def</span> <span class="nf">deposit</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">amount</span><span class="p">):</span>
    <span class="k">if</span> <span class="n">amount</span> <span class="o">&lt;=</span> <span class="mi">0</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nb">ValueError</span><span class="p">(</span><span class="s">"Negative deposit."</span><span class="p">)</span>

    <span class="bp">self</span><span class="p">.</span><span class="n">balance</span> <span class="o">+=</span> <span class="n">amount</span>

  <span class="k">def</span> <span class="nf">withdraw</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">amount</span><span class="p">):</span>
    <span class="k">if</span> <span class="n">amount</span> <span class="o">&gt;</span> <span class="bp">self</span><span class="p">.</span><span class="n">balance</span><span class="p">:</span>
        <span class="k">raise</span> <span class="nb">ValueError</span><span class="p">(</span><span class="s">"Insufficient funds."</span><span class="p">)</span>

    <span class="bp">self</span><span class="p">.</span><span class="n">balance</span> <span class="o">-=</span> <span class="n">amount</span>

  <span class="k">def</span> <span class="nf">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
    <span class="n">fbalance</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"£</span><span class="si">{</span><span class="bp">self</span><span class="p">.</span><span class="n">balance</span><span class="si">:</span><span class="p">.</span><span class="mi">2</span><span class="n">f</span><span class="si">}</span><span class="s">"</span>
    <span class="k">return</span> <span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="bp">self</span><span class="p">.</span><span class="n">name</span><span class="si">}</span><span class="s">: </span><span class="si">{</span><span class="n">fbalance</span><span class="si">}</span><span class="s">"</span>


<span class="c1"># Example usage:
</span><span class="n">c</span> <span class="o">=</span> <span class="n">Customer</span><span class="p">(</span><span class="s">"Alice"</span><span class="p">,</span> <span class="mi">100</span><span class="p">)</span>
<span class="n">c</span><span class="p">.</span><span class="n">deposit</span><span class="p">(</span><span class="mi">50</span><span class="p">)</span>
<span class="n">c</span><span class="p">.</span><span class="n">withdraw</span><span class="p">(</span><span class="mi">30</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="n">c</span><span class="p">)</span>
</code></pre></div>              </div>

              <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Alice: £120.00
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr><td colspan="3"><div>
              <h3 id="modules">Modules</h3>

              <ul>
                <li>In both languages, a module is simply a collection of functions that you want to include in another script or application.</li>
                <li>
                  <p>In Python modules are discovered via the <a href="https://docs.python.org/3/tutorial/modules.html#the-module-search-path">module search path</a>:</p>

                  <blockquote>
                    <p>When a module named <code class="language-plaintext highlighter-rouge">spam</code> is imported, the interpreter first searches for a built-in module with that name. These module names are listed in <code class="language-plaintext highlighter-rouge">sys.builtin_module_names</code>. If not found, it then searches for a file named <code class="language-plaintext highlighter-rouge">spam.py</code> in a list of directories given by the variable <code class="language-plaintext highlighter-rouge">sys.path</code>.</p>
                  </blockquote>
                </li>
              </ul>

            </div></td></tr>
        <tr width="100%"><th width="20%">Concept</th><th width="40%">PowerShell</th><th width="40%">Python</th></tr>

        <tr>
<td>Use a module</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Save this in a file called MyModule.ps1</span><span class="w">
</span><span class="kr">function</span><span class="w"> </span><span class="nf">greeting</span><span class="p">(</span><span class="nv">$name</span><span class="p">):</span><span class="w">
  </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Hello, </span><span class="nv">$name</span><span class="s2">"</span><span class="w">

</span><span class="c"># In your main file</span><span class="w">
</span><span class="n">Import-Module</span><span class="w"> </span><span class="nx">MyModule.ps1</span><span class="w">

</span><span class="n">greeting</span><span class="w"> </span><span class="s2">"Mark"</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Save this in a file called MyModule.py
</span><span class="k">def</span> <span class="nf">greeting</span><span class="p">(</span><span class="n">name</span><span class="p">):</span>
  <span class="k">print</span><span class="p">(</span><span class="s">"Hello, {name}"</span><span class="p">)</span>

<span class="c1"># In your main file
</span><span class="kn">import</span> <span class="nn">MyModule</span>

<span class="n">MyModule</span><span class="p">.</span><span class="n">greeting</span><span class="p">(</span><span class="s">"Mark"</span><span class="p">)</span>
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>List the functions in a module</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Import-Module</span><span class="w"> </span><span class="nx">MyModule.ps1</span><span class="w">

</span><span class="p">(</span><span class="n">Get-Command</span><span class="w"> </span><span class="nt">-Module</span><span class="w"> </span><span class="nx">MyModule</span><span class="p">)</span><span class="o">.</span><span class="nf">Name</span><span class="w">

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

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">MyModule</span>

<span class="k">for</span> <span class="n">name</span> <span class="ow">in</span> <span class="nb">dir</span><span class="p">(</span><span class="n">MyModule</span><span class="p">):</span>
  <span class="k">print</span><span class="p">(</span><span class="n">name</span><span class="p">)</span>
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Import a specific function</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Import-Module</span><span class="w"> </span><span class="nx">MyModule</span><span class="w"> </span><span class="nt">-Function</span><span class="w"> </span><span class="s2">"greeting"</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">MyModule</span> <span class="kn">import</span> <span class="n">greeting</span>
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Install a module</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Install-Module</span><span class="w"> </span><span class="nx">SomeModule</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">pip</span> <span class="n">install</span> <span class="n">SomeModule</span>
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Uninstall a module</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Uninstall-Module</span><span class="w"> </span><span class="nx">SomeModule</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">pip</span> <span class="n">uninstall</span> <span class="n">SomeModule</span>
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>List installed modules</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># List modules installed via PowerShellGet</span><span class="w">
</span><span class="n">Get-InstalledModule</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># List modules installed via pip
</span><span class="n">pip</span> <span class="nb">list</span>
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr><td colspan="3"><div>
              <h3 id="exceptions">Exceptions</h3>

              <ul>
                <li>PowerShell and Python have similar concepts for handling excpetions, with <code class="language-plaintext highlighter-rouge">try..catch</code> and <code class="language-plaintext highlighter-rouge">try..except</code> being functionally similar. Both support catching multiple specific exception types and a <code class="language-plaintext highlighter-rouge">finally</code> block for performing post-exception clean up tasks, such as closing connections or removing temporary files.</li>
              </ul>

            </div></td></tr>
        <tr width="100%"><th width="20%">Concept</th><th width="40%">PowerShell</th><th width="40%">Python</th></tr>

        <tr>
<td>Raise an exception</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">throw</span><span class="w"> </span><span class="s1">'An exception occurred'</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">raise</span> <span class="nb">Exception</span><span class="p">(</span><span class="s">"An exception occurred"</span><span class="p">)</span>
</code></pre></div>              </div>

            </div>
</td>
</tr>

        <tr>
<td>Catch exceptions</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">Write-Host</span><span class="w"> </span><span class="nv">$x</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="nx">Stop</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">Write-Error</span><span class="w"> </span><span class="p">(</span><span class="s2">"An exception occurred"</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span><span class="p">:</span>
  <span class="k">print</span><span class="p">(</span><span class="n">x</span><span class="p">)</span>

<span class="k">except</span><span class="p">:</span>
  <span class="k">print</span><span class="p">(</span><span class="s">"An exception occurred"</span><span class="p">)</span>

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

            </div>
</td>
</tr>

        <tr>
<td>Catch multiple exceptions</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">Get-Content</span><span class="w"> </span><span class="o">.</span><span class="nx">\file.txt</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="nx">Stop</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">[</span><span class="n">System.IO.IOException</span><span class="p">]</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">Write-Error</span><span class="w"> </span><span class="s2">"A file error occurred"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">Write-Error</span><span class="w"> </span><span class="s2">"A different error occurred"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span><span class="p">:</span>
  <span class="n">f</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="s">'file.txt'</span><span class="p">,</span> <span class="s">'w'</span><span class="p">)</span>

<span class="k">except</span> <span class="nb">OSError</span><span class="p">:</span>
  <span class="k">print</span><span class="p">(</span><span class="s">"A file error occurred"</span><span class="p">)</span>

<span class="k">except</span><span class="p">:</span>
  <span class="k">print</span><span class="p">(</span><span class="s">"A different error occurred"</span><span class="p">)</span>

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

            </div>
</td>
</tr>

        <tr>
<td>Execute code after an exception</td>
<td>
<div>

              <div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nv">$f</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">System.IO.File</span><span class="p">]::</span><span class="n">Open</span><span class="p">(</span><span class="s2">"file.txt"</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="n">Write-Error</span><span class="w"> </span><span class="s2">"Failed to open"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">finally</span><span class="w"> </span><span class="p">{</span><span class="w">
  </span><span class="nv">$f</span><span class="o">.</span><span class="nf">close</span><span class="p">()</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div>              </div>

            </div>
</td>
<td>
<div>

              <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span><span class="p">:</span>
  <span class="n">f</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="s">'file.txt'</span><span class="p">,</span> <span class="s">'w'</span><span class="p">)</span>

<span class="k">except</span><span class="p">:</span>
  <span class="k">print</span><span class="p">(</span><span class="s">"Failed to open"</span><span class="p">)</span>

<span class="k">finally</span><span class="p">:</span>
  <span class="n">f</span><span class="p">.</span><span class="n">close</span><span class="p">()</span>

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

            </div>
</td>
</tr>
      </div></td></tr></table>]]></content><author><name>Mark Wragg</name><email>mark@wragg.io</email></author><category term="powershell" /><category term="python" /><category term="automation" /><summary type="html"><![CDATA[PowerShell and Python are powerful programming languages with many similarities. While PowerShell is technically a shell scripting language (like Bash), functionally it has a lot more in common with Python, and can be used to generate scripts of equal complexity. As someone with a strong familiarity with PowerShell, I'm finding it useful as I learn Python to reference the equivalent concepts in PowerShell.]]></summary></entry><entry><title type="html">Configuring a Logic App managed identity as an Azure SQL user via an Azure DevOps pipeline</title><link href="https://wragg.io/azure-sql-managed-identity/" rel="alternate" type="text/html" title="Configuring a Logic App managed identity as an Azure SQL user via an Azure DevOps pipeline" /><published>2025-04-06T10:00:00+00:00</published><updated>2025-04-06T10:00:00+00:00</updated><id>https://wragg.io/azure-sql-managed-identity</id><content type="html" xml:base="https://wragg.io/azure-sql-managed-identity/"><![CDATA[<p>I was recently tasked with enabling connectivity to an Azure SQL database via the managed identity of a Logic App and it was surprisingly complicated to setup. In addition we wanted this configuration to be enabled through our automation pipelines, which added a further complexity. This blog post details the steps we had to take.</p>

<p>Azure provides managed identities as a secure and convenient way to manage access to resources. By configuring a managed identity you no longer need to store secrets, keys or credentials. For SQL server, the traditional way to access a database would be via a connection string that would include a username and password. However managed identities are password-less, so eliminate the need to provide a password directly in the connection string.</p>

<p>The complexity of this task comes from the following issues:</p>

<ul>
  <li>To configure a managed identity as a user within Azure SQL the user must be configured by another Entra ID account</li>
  <li>The SQL Server itself must be able to validate the Entra ID user you are configuring</li>
</ul>

<p>Getting this setup to execute via a pipeline can be done via the following three tasks:</p>

<h2 id="1-configure-an-ad-administrator-for-the-sql-server">1. Configure an AD Administrator for the SQL server</h2>

<blockquote>
  <p>To configure a managed identity as a user within Azure SQL the user must be configured by another Entra ID account</p>
</blockquote>

<p>At first this might seem like a <a href="https://en.wikipedia.org/wiki/Chicken_or_the_egg">chicken and egg scenario</a>, because how can you configure an Entra ID account as a user without already having an Entra ID account as a user? The answer is to configure an Entra ID account as the SQL Server AD Administrator. If you connect using local SQL credentials (even those with system administrator permissions) and attempt to create a SQL login for an Entra ID user you’ll get an error.</p>

<p>Because we wanted to be able to configure the Logic App managed identity with permissions to SQL via the pipeline, I made the service principal of the pipeline the Azure AD Administrator. I did this via PowerShell as follows (this script ran in the pipeline that deploys Azure SQL, which has new input variables to specify the name <code class="language-plaintext highlighter-rouge">$sqlAdAdminName</code> and object ID <code class="language-plaintext highlighter-rouge">$sqlAdAdminObjectId</code> of the pipeline service principal):</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Get existing Sql server configuration</span><span class="w">
</span><span class="nv">$sqlServer</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">az</span><span class="w"> </span><span class="nx">sql</span><span class="w"> </span><span class="nx">server</span><span class="w"> </span><span class="nx">show</span><span class="w"> </span><span class="nt">--name</span><span class="w"> </span><span class="nv">$sqlServerName</span><span class="w"> </span><span class="nt">--resource-group</span><span class="w"> </span><span class="nv">$resourceGroup</span><span class="w"> </span><span class="nt">--output</span><span class="w"> </span><span class="nx">json</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="c"># Check if AD admin already exists</span><span class="w">
</span><span class="nv">$existingAdAdministrator</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$sqlServer</span><span class="o">.</span><span class="nf">administrators</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">administratorType</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'ActiveDirectory'</span><span class="w"> </span><span class="p">}</span><span class="w">

</span><span class="c"># Configure AD admin if not already set to the required SQL Admin name</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$existingAdAdministrator</span><span class="o">.</span><span class="nf">login</span><span class="w"> </span><span class="o">-ne</span><span class="w"> </span><span class="nv">$sqlAdAdminName</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">

    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Setting SQL AD Admin.."</span><span class="w">
    </span><span class="n">az</span><span class="w"> </span><span class="nx">sql</span><span class="w"> </span><span class="nx">server</span><span class="w"> </span><span class="nx">ad-admin</span><span class="w"> </span><span class="nx">create</span><span class="w"> </span><span class="nt">--resource-group</span><span class="w"> </span><span class="nv">$resourceGroup</span><span class="w"> </span><span class="nt">--server</span><span class="w"> </span><span class="nv">$sqlServerName</span><span class="w"> </span><span class="nt">--display-name</span><span class="w"> </span><span class="nv">$sqlAdAdminName</span><span class="w"> </span><span class="nt">--object-id</span><span class="w"> </span><span class="nv">$sqlAdAdminObjectId</span><span class="w">

    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="bp">$?</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="bp">$false</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="n">Write-Error</span><span class="w"> </span><span class="s2">"Error setting SQL AD Admin </span><span class="nv">$sqlAdAdminName</span><span class="s2"> with objectId </span><span class="nv">$sqlAdAdminObjectId</span><span class="s2">."</span><span class="w">
    </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"SQL AD Admin already set to </span><span class="nv">$sqlAdAdminName</span><span class="s2">."</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>If you didn’t want to use the pipeline’s service principal as the AD administrator, you could configure another Entra ID user or group and then provide the credentials for that user during your pipeline execution. Obviously using the pipeline’s service principal negates the need to store any credentials, but it does mean anything executed via the pipeline’s service principal in the future has AD Administrator access to the SQL Server, so would have to be carefully secured. It’s up to you to decide whether this is appropriate for your specific database/scenario.</p>

<h2 id="2-enable-a-system-assigned-or-user-assigned-managed-identity-on-the-sql-server">2. Enable a system-assigned (or user-assigned) managed identity on the SQL server</h2>

<blockquote>
  <p>The SQL Server itself must be able to validate the Entra ID user you are configuring</p>
</blockquote>

<p>You might think (as I did) that the Entra ID user you’re now connecting to the SQL server with to execute your commands to create the new Entra ID login would have enough access by itself to complete the task. But actually, when you add a directory user to SQL server, it seems the SQL server itself has to do a bit of validation against the directory. To do this, the Azure SQL Server needs either a System Assigned identity, or User Assigned identity configured with specific permissions to read the Entra ID directory.</p>

<p>You can enable a System Assigned identity via PowerShell as follows:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Check if existing System Assigned Identity is set</span><span class="w">
</span><span class="nv">$existingSAIdentity</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$sqlServer</span><span class="o">.</span><span class="nf">identity</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">type</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="s1">'SystemAssigned'</span><span class="w"> </span><span class="p">}</span><span class="w">

</span><span class="c"># Configure System Assigned Identity if not already set</span><span class="w">
</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">-not</span><span class="w"> </span><span class="nv">$existingSAIdentity</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">

    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"Setting System Assigned Identity.."</span><span class="w">
    </span><span class="n">az</span><span class="w"> </span><span class="nx">sql</span><span class="w"> </span><span class="nx">server</span><span class="w"> </span><span class="nx">update</span><span class="w"> </span><span class="nt">--resource-group</span><span class="w"> </span><span class="nv">$resourceGroup</span><span class="w"> </span><span class="nt">--name</span><span class="w"> </span><span class="nv">$sqlServerName</span><span class="w"> </span><span class="nt">--assign_identity</span><span class="w"> </span><span class="nt">--identity-type</span><span class="w"> </span><span class="nx">SystemAssigned</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Host</span><span class="w"> </span><span class="s2">"System Assigned Identity already set."</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Once you’ve configured a System Assigned identity you need to <a href="https://learn.microsoft.com/en-us/azure/azure-sql/database/authentication-aad-directory-readers-role?view=azuresql">grant it the Directory Readers role</a>. This has to be done by an Entra ID account with Privileged Role Administrator or higher permissions. We did this step manually, but it could be automated. Alternatively if you use a user-assigned managed identity, you can configure specific Microsoft Graph permissions for it via PowerShell as described in <a href="https://learn.microsoft.com/en-us/azure/azure-sql/database/authentication-azure-ad-user-assigned-managed-identity?view=azuresql">this Microsoft article</a>.</p>

<h2 id="3-execute-your-sql-scripts-to-configure-the-managed-identity-permissions-and-roles">3. Execute your SQL scripts to configure the managed identity permissions and roles</h2>

<p>Having completed the above, we could now configure the Logic App managed identity as a user in the SQL server as follows (this script runs in another pipeline where the specific logic app is deployed that we wanted to grant access):</p>

<blockquote>
  <p>Because we configured the pipeline user as a AD Admin, the below uses Az CLI to get the current users access token to authenticate to SQL. If you configured another user as SQL Server AD Admin you’ll need to authenticate to SQL with those credentials.</p>
</blockquote>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Get the access token of the current pipeline user session</span><span class="w">
</span><span class="nv">$accessToken</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">az</span><span class="w"> </span><span class="nx">account</span><span class="w"> </span><span class="nx">get-access-token</span><span class="w"> </span><span class="nt">--resource</span><span class="w"> </span><span class="nx">https://database.windows.net/</span><span class="w"> </span><span class="nt">--query</span><span class="w"> </span><span class="nx">accessToken</span><span class="w"> </span><span class="nt">--output</span><span class="w"> </span><span class="nx">tsv</span><span class="w">

</span><span class="nv">$invokeSqlcmd</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
    </span><span class="nx">ServerInstance</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"</span><span class="nv">${sqlServerName}</span><span class="s2">.database.windows.net"</span><span class="w">
    </span><span class="nx">AccessToken</span><span class="w">     </span><span class="o">=</span><span class="w"> </span><span class="nv">$accessToken</span><span class="w">
    </span><span class="nx">OutputSqlErrors</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="c"># Specify the name of the Logic App resource, which has system assigned identity enabled</span><span class="w">
</span><span class="nv">$laName</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"your-logic-app-resource-name"</span><span class="w">

</span><span class="c"># Check if the DB user is already configured</span><span class="w">
</span><span class="kr">try</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$dbuserAlreadyExists</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Invoke-Sqlcmd</span><span class="w"> </span><span class="err">@</span><span class="nx">invokeSqlcmd</span><span class="w"> </span><span class="nt">-Query</span><span class="w"> </span><span class="s2">"SELECT name FROM sys.sysusers WHERE name='</span><span class="nv">$laName</span><span class="s2">'"</span><span class="w"> </span><span class="nt">-Database</span><span class="w"> </span><span class="nv">$dbName</span><span class="w"> </span><span class="nt">-ErrorAction</span><span class="w"> </span><span class="nx">Stop</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">catch</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Error</span><span class="w"> </span><span class="bp">$_</span><span class="w">
    </span><span class="kr">throw</span><span class="w"> </span><span class="s2">"Failed to check for existing login for id </span><span class="nv">$laName</span><span class="s2"> in database </span><span class="nv">$dbname</span><span class="s2">."</span><span class="w">
</span><span class="p">}</span><span class="w">

</span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">-not</span><span class="w"> </span><span class="nv">$dbuserAlreadyExists</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">

    </span><span class="n">Write-Verbose</span><span class="w"> </span><span class="s2">"Creating user </span><span class="nv">$laName</span><span class="s2"> in database </span><span class="nv">$dbName</span><span class="s2"> from external provider.."</span><span class="w">
    </span><span class="n">Invoke-Sqlcmd</span><span class="w"> </span><span class="err">@</span><span class="nx">invokeSqlcmd</span><span class="w"> </span><span class="nt">-Database</span><span class="w"> </span><span class="nv">$dbName</span><span class="w"> </span><span class="nt">-Query</span><span class="w"> </span><span class="s2">"CREATE USER [</span><span class="nv">$laName</span><span class="s2">] FROM EXTERNAL PROVIDER"</span><span class="w">

    </span><span class="n">Write-Verbose</span><span class="w"> </span><span class="s2">"Adding role db_datareader to user </span><span class="nv">$laName</span><span class="s2">.."</span><span class="w">
    </span><span class="n">Invoke-Sqlcmd</span><span class="w"> </span><span class="err">@</span><span class="nx">invokeSqlcmd</span><span class="w"> </span><span class="nt">-Database</span><span class="w"> </span><span class="nv">$dbName</span><span class="w"> </span><span class="nt">-Query</span><span class="w"> </span><span class="s2">"ALTER ROLE [db_datareader] ADD MEMBER [</span><span class="nv">$laName</span><span class="s2">];"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="kr">else</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="n">Write-Verbose</span><span class="w"> </span><span class="s2">"User </span><span class="nv">$laName</span><span class="s2"> already exists."</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>For more information on the above (and to ensure you’re following the latest Microsoft guidance) check out the official documentation for <a href="https://learn.microsoft.com/en-us/azure/azure-sql/database/authentication-aad-configure">configuring Microsoft Entra authentication with Azure SQL</a>.</p>

<p>The page on <a href="https://learn.microsoft.com/en-us/azure/azure-sql/database/authentication-aad-service-principal?view=azuresql">Microsoft Entra service principals with Azure SQL</a> was also useful.</p>]]></content><author><name>Mark Wragg</name><email>mark@wragg.io</email></author><category term="azure" /><category term="sql" /><category term="azuredevops" /><category term="pipelines" /><category term="powershell" /><summary type="html"><![CDATA[I was recently tasked with enabling connectivity to an Azure SQL database via the managed identity of a Logic App and it was surprisingly complicated to setup. In addition we wanted this configuration to be enabled through our automation pipelines, which added a further complexity. This blog post details the steps we had to take.]]></summary></entry><entry><title type="html">Well-architect your Azure Terraform with PSRule</title><link href="https://wragg.io/well-architect-terraform-with-psrule/" rel="alternate" type="text/html" title="Well-architect your Azure Terraform with PSRule" /><published>2025-03-03T17:00:00+00:00</published><updated>2025-03-03T17:00:00+00:00</updated><id>https://wragg.io/well-architect-terraform-with-psrule</id><content type="html" xml:base="https://wragg.io/well-architect-terraform-with-psrule/"><![CDATA[<p>This blog post is part of the <a href="https://www.azurespringclean.com/">Azure Spring Clean 2025</a> virtual community event, promoting well-managed Azure tenants. In last year’s Azure Spring Clean, <a href="https://rios.engineer/azure-spring-clean-azure-best-practice-for-bicep-with-psrule/">Dan Rios blogged about using PSRule for Bicep code</a>. The focus of this blog post is on how you can use PSRule to validate Azure resources deployed via <a href="https://www.terraform.io/">Terraform by HashiCorp</a>.</p>

<blockquote>
  <p>Many DevOps tools were gifted to the Engineers, who above all else, desired automation. For within these tools was bound the strength and will to govern the Cloud.
But they were all of them deceived, for another tool was made.
In the land of Microsoft, in the fires of Mount Azure, the Architect <a href="https://www.linkedin.com/in/bernie-white/">Bernie White</a> forged, in open source, a tool to validate all others.
And into this tool he poured his creativity, his mastery and his will to improve all infrastructure.</p>

  <p>One tool to <a href="https://microsoft.github.io/PSRule/v2/">PSRule</a> them all.</p>
</blockquote>

<p>I suspect it was something like that anyway.</p>

<p><a href="https://github.com/microsoft/PSRule">PSrule can be found in GitHub</a> and has been in development since December 2018. The current stable version is v2.9.0 (v3 is in development). The <a href="https://microsoft.github.io/PSRule/v2/">official website</a> has lots of guidance on getting started, and is officially described as:</p>

<blockquote>
  <p>“A rules engine geared towards testing Infrastructure as Code (IaC). Rules you write or import perform static analysis on IaC artifacts such as: templates, manifests, pipelines, and workflows.”</p>
</blockquote>

<p>The idea is that you (or Microsoft, the community, the Council of Elrond) define rules for how your infrastructure should be configured and PSRule (executed as part of your development process) confirms those constraints are being followed. Infrastructure code is often complex, and can be developed by multiple individuals or teams over time. PSRule can act as a guard rail to ensure the infrastructure requirements of your organisation are being followed, or used to evaluated the quality of existing infrastructure.</p>

<p>Obviously developing these rules is itself a timely endeavour, but PSRule has done the heavy lifting for you by providing various pre-built rules based on best practice guidance such as the <a href="https://learn.microsoft.com/en-us/azure/well-architected/">Azure Well-architected Framework</a>. PSRule is extensible, so you choose which existing rules you want to use, and customise them (and/or develop your own rules) to meet your requirements.</p>

<h3 id="begin-the-journey">Begin the journey</h3>

<blockquote>
  <p><em>“It’s a dangerous business, Frodo, going out your door.”</em> — Bilbo Baggins</p>
</blockquote>

<p><a href="https://azure.github.io/PSRule.Rules.Azure/install/">Installing PSRule for Azure</a> requires no difficult journey to Mount Doom. There is a <a href="https://azure.github.io/PSRule.Rules.Azure/">dedicated site for this module</a>, with its own <a href="https://azure.github.io/PSRule.Rules.Azure/about/">Getting started</a> page. You can install it directly, as follows:</p>

<ul>
  <li><a href="https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell">Install PowerShell 7</a> (if you haven’t already)</li>
  <li>Install PSRule (with the pre-built Azure rules) from the PowerShell Gallery:</li>
</ul>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Install-Module</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="s1">'PSRule.Rules.Azure'</span><span class="w"> </span><span class="nt">-Repository</span><span class="w"> </span><span class="nx">PSGallery</span><span class="w"> </span><span class="nt">-Scope</span><span class="w"> </span><span class="nx">CurrentUser</span><span class="w">
</span></code></pre></div></div>

<p>PSRule for Azure is officially described as:</p>

<blockquote>
  <p>“A pre-built set of tests and documentation to help you configure Azure solutions. These tests allow you to check your Infrastructure as Code (IaC) before or after deployment to Azure. PSRule for Azure includes unit tests that check how Azure resources defined in <strong>ARM templates or Bicep code</strong> are configured.”</p>
</blockquote>

<p>As it turns out, one does not <em>simply</em> test Terraform with PSRule.</p>

<p><img src="/content/images/2025/one-does-not-simply-test-terraform.jpg" alt="One does not simply test Terraform meme" class="align-center" /></p>

<p>But it’s not <em>that</em> complicated. At the moment you can’t perform static analysis of your Terraform files (or Terraform plan output), but you can <a href="https://github.com/Azure/PSRule.Rules.Azure/issues/1193">indicate your support for the feature here</a>. For now, you need to deploy resources to Azure, run a command to export the configuration of those resources to a JSON file, which PSRule can then analyse. While this doesn’t make PSRule as useful as it is for analysing Bicep (for which there is also a <a href="https://marketplace.visualstudio.com/items?itemName=bewhite.psrule-vscode">VSCode extension</a> so you can get feedback while developing), if you deploy your infrastructure through a series of environments for testing (or even if you don’t) I can see PSRule still forming a valuable part of that pipeline.</p>

<p>To summarise, here’s the steps to take to analyse Terraform resources:</p>

<ol>
  <li>Deploy your Terraform code to Azure (ideally to a non-production environment).</li>
  <li>Ensure you have authenticated to Azure via <code class="language-plaintext highlighter-rouge">Connect-AzAccount</code> and have the relevant subscription selected.</li>
  <li>Export your Azure resources to a specified directory (and ensure the directory exists):</li>
</ol>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">New-Item</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nx">out</span><span class="w"> </span><span class="nt">-ItemType</span><span class="w"> </span><span class="nx">Directory</span><span class="w">
</span><span class="n">Export-AzRuleData</span><span class="w"> </span><span class="nt">-OutputPath</span><span class="w"> </span><span class="s2">"</span><span class="nv">$pwd</span><span class="s2">/out"</span><span class="w">
</span></code></pre></div></div>

<p>By default this will export all of the resources for the currently selected subscription. You can use <code class="language-plaintext highlighter-rouge">-Subscription</code> to specify one or more subscriptions, or <code class="language-plaintext highlighter-rouge">-ResourceGroupName</code> to specify one or more resource groups. You can also use <code class="language-plaintext highlighter-rouge">-Tag</code> to export resources with one or more specified tags.</p>

<p>If you want to go big (rather than return to the shire), you can use <code class="language-plaintext highlighter-rouge">-All</code> to export resources from all the subscriptions your current context can access.</p>

<p>The output file/s are named with the guid of the subscription. This is true even if you filter to specific resources by Resource Group or Tag, and any existing file with the same name will be overwritten.</p>

<h3 id="face-your-demons">Face your demons</h3>

<blockquote>
  <p><em>“It is the small things, everyday deeds of ordinary folk that keep the darkness at bay.”</em> — Gandalf</p>
</blockquote>

<p>It’s now time to analyse the output. You do this with <code class="language-plaintext highlighter-rouge">Invoke-PSRule</code> which you need to point at your export file/s and the <code class="language-plaintext highlighter-rouge">PSRule.Rules.Azure</code> module:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Invoke-PsRule</span><span class="w"> </span><span class="nt">-InputPath</span><span class="w"> </span><span class="s2">"</span><span class="nv">$pwd</span><span class="s2">/out/"</span><span class="w"> </span><span class="nt">-Module</span><span class="w"> </span><span class="s1">'PSRule.Rules.Azure'</span><span class="w">
</span></code></pre></div></div>

<p>Depending on your resources, you’ll probably get quite a lot of output because by default the tool returns all results (pass, fail and error). If you want to just see failed results, execute:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Invoke-PSRule</span><span class="w"> </span><span class="nt">-InputPath</span><span class="w"> </span><span class="s2">"</span><span class="nv">$pwd</span><span class="s2">/out/"</span><span class="w"> </span><span class="nt">-Module</span><span class="w"> </span><span class="s1">'PSRule.Rules.Azure'</span><span class="w"> </span><span class="nt">-Outcome</span><span class="w"> </span><span class="nx">Fail</span><span class="w">
</span></code></pre></div></div>

<p><img src="/content/images/2025/psrule-output.png" alt="PSRule for Azure output example" class="align-center" /></p>

<p>Results from PSRule are returned as PowerShell objects, so it’s worthwhile returning them to a variable as there are more properties returned than you see in the default view above. But what you do get by default is the rule outcomes grouped by resource, with the name of the rule and a further description of how to address it in the Recommendation.</p>

<p>Further properties in the object that are useful include:</p>

<ul>
  <li>Ref — This is the unique reference number for the rule. You can use this or the rule name to get to the relevant <a href="https://azure.github.io/PSRule.Rules.Azure/en/rules/">reference page</a> which has more detail explaining the rule and sometimes examples of how to address it.</li>
  <li>Reason — This is useful to get more specific detail of the violation, particularly if it relates to one or more sub-resources.</li>
</ul>

<p>You also might want to export the results for some further/offline analysis and PSRule makes this easy with it’s <code class="language-plaintext highlighter-rouge">-OutputPath</code> parameter, along with <code class="language-plaintext highlighter-rouge">-OutputFormat</code> that can be used to output to various formats such as CSV, JSON, Markdown, YAML and NUnit3.</p>

<p>There are some other switches on the command that are worth exploring, one of which is <code class="language-plaintext highlighter-rouge">-As Summary</code> which simply returns a table showing for each rule how many resources passed and failed:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>RuleName                            Pass  Fail  Outcome
--------                            ----  ----  -------
Azure.AppService.PlanInstanceCount  0     4     Fail
Azure.AppService.MinTLS             20    0     Pass
Azure.KeyVault.Logs                 19    0     Pass
Azure.KeyVault.Name                 19    0     Pass
Azure.Resource.UseTags              153   0     Pass
Azure.Resource.AllowedRegions       154   0     Pass
Azure.ResourceGroup.Name            20    0     Pass
Azure.ServiceBus.Usage              4     0     Pass
Azure.SQL.FirewallRuleCount         1     1     Fail
Azure.SQL.AllowAzureAccess          0     2     Fail
Azure.Storage.UseReplication        23    6     Fail
Azure.Storage.SoftDelete            2     12    Fail
Azure.RBAC.UseRGDelegation          20    0     Pass
Azure.VM.PublicIPAttached           5     1     Fail
Azure.VNET.UseNSGs                  0     4     Fail
...
</code></pre></div></div>

<h3 id="make-the-hard-choices-or-easy-ones">Make the hard choices (or easy ones)</h3>

<blockquote>
  <p><em>“This task was appointed to you. And if you do not find a way, no one will.”</em> — Galadriel</p>
</blockquote>

<p>Once you have the output there’s obviously two ways forward (three if you count doing nothing):</p>

<ol>
  <li>Address the failure by modifying your Terraform code</li>
  <li>Exclude the rule</li>
</ol>

<p>If there are issues that you can and want to fix, then simply make the required changes in your Terraform code (using the guidance provided by the recommendation). Redeploy your infrastructure and re-run the export of the rule data and analysis to confirm the test now passes.</p>

<p>There may be issues that you don’t want to fix, because the supposed misconfiguration is appropriate for your infrastructure (for example, you might have a Storage account that you need to be publicly accessible) or there may be entire rulesets that you don’t consider in scope for your infrastructure (you may have no need for tags for example). PSRule allows you to control these exceptions by <a href="https://azure.github.io/PSRule.Rules.Azure/concepts/suppression/">configuring exclusion or suppression</a>.</p>

<p>These customisations are configured by creating a file called <code class="language-plaintext highlighter-rouge">ps-rule.yaml</code>.</p>

<p>You can exclude a rule as follows:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">rule</span><span class="pi">:</span>
  <span class="na">exclude</span><span class="pi">:</span>
    <span class="c1"># Ignore the following rules for all resources</span>
    <span class="pi">-</span> <span class="s">Azure.Resource.UseTags</span>
</code></pre></div></div>

<p>And you can suppress a rule for specific resources like this:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">suppression</span><span class="pi">:</span>
  <span class="na">Azure.Storage.BlobPublicAccess</span><span class="pi">:</span>
    <span class="c1"># Ignore blob public access on the following storage account</span>
    <span class="pi">-</span> <span class="s">mypublicstorageaccount</span>
</code></pre></div></div>

<p>These rules will no longer pass or fail.</p>

<p><img src="/content/images/2025/you-shall-not-pass.jpg" alt="You shall not pass or fail rule is excluded meme" class="align-center" /></p>

<p>There’s some further cleverness you can do to avoid having to manually populate resources in these files, by instead creating logic that sets up exclusions based on the value of fields such as names or tags. These are called <a href="https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_SuppressionGroups/">suppression groups</a>.</p>

<p>The built-in rules are routinely updated (usually monthly). As the rules are updated, the output you get may change. You can manage this by <a href="https://azure.github.io/PSRule.Rules.Azure/working-with-baselines/">running PSRule against a specified baseline</a> which are published quarterly. This keeps your testing consistent, until you decide to move to a new baseline. There are also pillar specific baselines, for example if you’re only interested in using PSRule to evaluate cost optimisation you can use the <code class="language-plaintext highlighter-rouge">Azure.Pillar.CostOptimization</code> baseline. You can also <a href="https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Baseline/">create your own custom baselines</a>.</p>

<p><a href="https://azure.github.io/PSRule.Rules.Azure/en/baselines/">The built-in baselines are listed here</a>. To run PSRule against a specified baseline you use the <code class="language-plaintext highlighter-rouge">-Baseline</code> parameter:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Invoke-PSRule</span><span class="w"> </span><span class="nt">-InputPath</span><span class="w"> </span><span class="s2">"</span><span class="nv">$pwd</span><span class="s2">/out/"</span><span class="w"> </span><span class="nt">-Module</span><span class="w"> </span><span class="s1">'PSRule.Rules.Azure'</span><span class="w"> </span><span class="nt">-Outcome</span><span class="w"> </span><span class="nx">Fail</span><span class="w"> </span><span class="nt">-Baseline</span><span class="w"> </span><span class="s1">'Azure.GA_2024_09'</span><span class="w">
</span></code></pre></div></div>

<p>Note: you will see a warning if you use a baseline that is outdated (i.e, not the latest).</p>

<h3 id="dont-go-it-alone">Don’t go it alone</h3>

<blockquote>
  <p><em>“Help me bear this burden.”</em> — Frodo</p>
</blockquote>

<p>While it might be helpful to have a band of <a href="https://en.wiktionary.org/wiki/hobbitses">hobbitses</a> to assist with this work, a more DevOpses approach might be to implement a pipeline. Once you’ve done the above: established your baseline and excluded or suppressed rules that do not apply to your infrastructure, it may make sense to include a run of PSRule as part of your CI pipeline, and perhaps as a gate for infrastructure Pull Requests. <a href="https://azure.github.io/PSRule.Rules.Azure/creating-your-pipeline">PSRule provides guidance on this</a> but it is once again geared towards testing Bicep or ARM static files. For Terraform we need to do an in-flight analysis post-deployment.</p>

<p>Here’s how you might do that in Azure DevOps:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">trigger</span><span class="pi">:</span>
<span class="pi">-</span> <span class="s">main</span>

<span class="na">pool</span><span class="pi">:</span>
  <span class="na">vmImage</span><span class="pi">:</span> <span class="s1">'</span><span class="s">ubuntu-latest'</span>

<span class="na">stages</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">stage</span><span class="pi">:</span> <span class="s">Plan</span>
  <span class="na">displayName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Terraform</span><span class="nv"> </span><span class="s">Plan'</span>
  <span class="na">jobs</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">job</span><span class="pi">:</span> <span class="s">TerraformPlan</span>
    <span class="na">displayName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Terraform</span><span class="nv"> </span><span class="s">Plan'</span>
    <span class="na">steps</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">TerraformInstaller@0</span>
      <span class="na">inputs</span><span class="pi">:</span>
        <span class="na">terraformVersion</span><span class="pi">:</span> <span class="s1">'</span><span class="s">latest'</span>

    <span class="pi">-</span> <span class="na">script</span><span class="pi">:</span> <span class="pi">|</span>
        <span class="s">terraform --version</span>
        <span class="s">terraform init -backend-config="storage_account_name=$(storageAccountName)" -backend-config="container_name=$(containerName)" -backend-config="key=$(key)" -backend-config="access_key=$(accessKey)"</span>
        <span class="s">terraform plan -out=tfplan</span>
      <span class="na">displayName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Terraform</span><span class="nv"> </span><span class="s">Init</span><span class="nv"> </span><span class="s">and</span><span class="nv"> </span><span class="s">Plan'</span>

<span class="pi">-</span> <span class="na">stage</span><span class="pi">:</span> <span class="s">Deploy</span>
  <span class="na">displayName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Terraform</span><span class="nv"> </span><span class="s">Apply'</span>
  <span class="na">jobs</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">job</span><span class="pi">:</span> <span class="s">TerraformApply</span>
    <span class="na">displayName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Terraform</span><span class="nv"> </span><span class="s">Apply'</span>
    <span class="na">steps</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">TerraformInstaller@0</span>
      <span class="na">inputs</span><span class="pi">:</span>
        <span class="na">terraformVersion</span><span class="pi">:</span> <span class="s1">'</span><span class="s">latest'</span>

    <span class="pi">-</span> <span class="na">script</span><span class="pi">:</span> <span class="pi">|</span>
        <span class="s">terraform --version</span>
        <span class="s">terraform init -backend-config="storage_account_name=$(storageAccountName)" -backend-config="container_name=$(containerName)" -backend-config="key=$(key)" -backend-config="access_key=$(accessKey)"</span>
        <span class="s">terraform apply -auto-approve tfplan</span>
      <span class="na">displayName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Terraform</span><span class="nv"> </span><span class="s">Init</span><span class="nv"> </span><span class="s">and</span><span class="nv"> </span><span class="s">Apply'</span>

<span class="pi">-</span> <span class="na">stage</span><span class="pi">:</span> <span class="s">PSRule</span>
  <span class="na">displayName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Execute</span><span class="nv"> </span><span class="s">PSRule'</span>
  <span class="na">jobs</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">job</span><span class="pi">:</span> <span class="s">PSRule</span>
    <span class="na">displayName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Execute</span><span class="nv"> </span><span class="s">PSRule'</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">AzurePowerShell@5</span>
            <span class="s">displayName</span><span class="err">:</span> <span class="s">Export PSRule Data and Execute Rules</span>
            <span class="s">inputs</span><span class="err">:</span>
              <span class="na">azureSubscription</span><span class="pi">:</span> <span class="s">$(azureSubscription)</span>
              <span class="na">scriptType</span><span class="pi">:</span> <span class="s">InlineScript</span>
              <span class="na">azurePowerShellVersion</span><span class="pi">:</span> <span class="s">LatestVersion</span>
              <span class="na">Inline</span><span class="pi">:</span> <span class="pi">|</span>
                <span class="s">Install-Module -Name 'PSRule.Rules.Azure' -Scope CurrentUser -Force -ErrorAction Stop</span>

                <span class="s">New-Item -Path out -ItemType Directory -Force</span>
                <span class="s">Export-AzRuleData -OutputPath 'out'</span>

                <span class="s">Assert-PSRule -InputPath 'out' -Module 'PSRule.Rules.Azure' -Outcome Fail -Baseline 'Azure.GA_2024_12'</span>
</code></pre></div></div>

<p>Note the usage of <code class="language-plaintext highlighter-rouge">Assert-PSRule</code> instead of <code class="language-plaintext highlighter-rouge">Invoke-PSRule</code>. This command returns formatted text instead of PowerShell objects which is easier to read in the output of a pipeline job.</p>

<p>The pipeline job will fail if the tests fail, and the output will look something like this:</p>

<p><img src="/content/images/2025/psrule-pipeline-output.png" alt="PSRule for Azure pipeline output example" class="align-center" /></p>

<h3 id="journeying-onward">Journeying onward</h3>

<blockquote>
  <p><em>“Don’t adventures ever have an end? I suppose not. Someone else always has to carry on the story.”</em> — Bilbo Baggins</p>
</blockquote>

<p>If you’ve conquered all of the above, well done! But what comes after? Well you could give some consideration to authoring your own <a href="https://azure.github.io/PSRule.Rules.Azure/customization/using-custom-rules/">custom rules</a>. To do this:</p>

<ol>
  <li>Create a <code class="language-plaintext highlighter-rouge">.ps-rule</code> directory in your repository.</li>
  <li>Create a new custom rule ps1 file. The rule name is up to you, but must be unique. For example: <code class="language-plaintext highlighter-rouge">Org.Azure.Rule.ps1</code>.</li>
  <li>In the file define your rule. For example, here’s how you might configure a rule for custom tags:</li>
</ol>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Synopsis: Resource Groups must have all mandatory tags defined.</span><span class="w">
</span><span class="n">Rule</span><span class="w"> </span><span class="s1">'Org.Azure.RG.Tags'</span><span class="w"> </span><span class="nt">-Type</span><span class="w"> </span><span class="s1">'Microsoft.Resources/resourceGroups'</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$hasTags</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Assert</span><span class="o">.</span><span class="nf">HasField</span><span class="p">(</span><span class="nv">$TargetObject</span><span class="p">,</span><span class="w"> </span><span class="s1">'Tags'</span><span class="p">)</span><span class="w">
    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nv">$hasTags</span><span class="o">.</span><span class="nf">Result</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="kr">return</span><span class="w"> </span><span class="nv">$hasTags</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c"># Require tags be case-sensitive</span><span class="w">
    </span><span class="nv">$Assert</span><span class="o">.</span><span class="nf">HasField</span><span class="p">(</span><span class="nv">$TargetObject</span><span class="o">.</span><span class="nf">tags</span><span class="p">,</span><span class="w"> </span><span class="s1">'costCentre'</span><span class="p">,</span><span class="w"> </span><span class="nv">$True</span><span class="p">)</span><span class="w">
    </span><span class="nv">$Assert</span><span class="o">.</span><span class="nf">HasField</span><span class="p">(</span><span class="nv">$TargetObject</span><span class="o">.</span><span class="nf">tags</span><span class="p">,</span><span class="w"> </span><span class="s1">'env'</span><span class="p">,</span><span class="w"> </span><span class="nv">$True</span><span class="p">)</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>In the example above the <code class="language-plaintext highlighter-rouge">HasField</code> assertion takes 3 inputs, the value being evaluated, the expected value and a boolean indicating whether the comparison should be case-sensitive. There are plenty of <a href="https://microsoft.github.io/PSRule/v2/concepts/PSRule/en-US/about_PSRule_Assert/">other assertions</a> you can use.</p>

<p>Before your rule will work, you will also need to add the following to the <code class="language-plaintext highlighter-rouge">ps-rule.yaml</code> file we created earlier:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Configure binding options</span>
<span class="na">binding</span><span class="pi">:</span>
  <span class="na">targetType</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s1">'</span><span class="s">resourceType'</span>
    <span class="pi">-</span> <span class="s1">'</span><span class="s">type'</span>
</code></pre></div></div>

<p>This binding allows custom rules to use the <code class="language-plaintext highlighter-rouge">-Type</code> parameter. The in-built rules detect this automatically, but custom rules check the <code class="language-plaintext highlighter-rouge">ps-rule.yaml</code> file for their type binding configuration.</p>

<p>You can test your custom rule by executing <code class="language-plaintext highlighter-rouge">Invoke-PSRule</code> with a <code class="language-plaintext highlighter-rouge">-Path</code> parameter:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Invoke-PSRule</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="s2">"</span><span class="nv">$pwd</span><span class="s2">/.ps-rule/"</span><span class="w"> </span><span class="nt">-InputPath</span><span class="w"> </span><span class="s2">"</span><span class="nv">$pwd</span><span class="s2">/out/"</span><span class="w">
</span></code></pre></div></div>

<p>The output will look something like this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>RuleName                            Outcome    Recommendation
--------                            -------    --------------
Org.Azure.RG.Tags                   Fail

   TargetName: sometargetresource
</code></pre></div></div>

<p>You’ll notice there’s no Recommendation returned. We can add this by adding the <code class="language-plaintext highlighter-rouge">Recommend</code> keyword into your rule:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Rule</span><span class="w"> </span><span class="s1">'Org.Azure.RG.Tags'</span><span class="w"> </span><span class="nt">-Type</span><span class="w"> </span><span class="s1">'Microsoft.Resources/resourceGroups'</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nv">$hasTags</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$Assert</span><span class="o">.</span><span class="nf">HasField</span><span class="p">(</span><span class="nv">$TargetObject</span><span class="p">,</span><span class="w"> </span><span class="s1">'Tags'</span><span class="p">)</span><span class="w">
    </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="nv">$hasTags</span><span class="o">.</span><span class="nf">Result</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="kr">return</span><span class="w"> </span><span class="nv">$hasTags</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="c"># Require tags be case-sensitive</span><span class="w">
    </span><span class="nv">$Assert</span><span class="o">.</span><span class="nf">HasField</span><span class="p">(</span><span class="nv">$TargetObject</span><span class="o">.</span><span class="nf">tags</span><span class="p">,</span><span class="w"> </span><span class="s1">'costCentre'</span><span class="p">,</span><span class="w"> </span><span class="nv">$True</span><span class="p">)</span><span class="w">
    </span><span class="nv">$Assert</span><span class="o">.</span><span class="nf">HasField</span><span class="p">(</span><span class="nv">$TargetObject</span><span class="o">.</span><span class="nf">tags</span><span class="p">,</span><span class="w"> </span><span class="s1">'env'</span><span class="p">,</span><span class="w"> </span><span class="nv">$True</span><span class="p">)</span><span class="w">

    </span><span class="n">Recommend</span><span class="w"> </span><span class="s2">"The following tags are mandatory: 'costCentre','env'"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>And now we can see the message:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>RuleName                            Outcome    Recommendation
--------                            -------    --------------
Org.Azure.RG.Tags                   Fail       The following tags are mandatory: 'costCentre','env'

   TargetName: sometargetresource
</code></pre></div></div>

<p>I hope you’ve found this a useful introduction to PSRule. While the focus of this blog was on Terraform, most of what is detailed above can be used to test infrastructure deployed via any mechanism (even resources created manually, but please don’t :) ).</p>

<p>For other great content in this year’s Azure Spring Clean, check out the <a href="https://www.azurespringclean.com/">website</a> or follow the <a href="https://bsky.app/profile/wragg.io/lists/3ljhoyojxk42m">Azure Spring Clean list on BlueSky</a>.</p>

<p><img src="/content/images/2025/lotr-fellowship.jpg" alt="Lord of the Rings fellowship" class="align-center" /></p>

<p><em>Did you know that in the books, 17 years pass between Frodo receiving the ring and heading off on his quest? Once he finally got going he <a href="https://www.theonering.net/torwp/2014/05/31/89705-compare-the-time-and-distance-travelled-in-the-hobbit-and-the-lord-of-the-rings/">travelled about 1800 miles, from Bag End to Mount Doom, in about 6 months</a>. It would likely have been quicker by eagle.</em></p>]]></content><author><name>Mark Wragg</name><email>mark@wragg.io</email></author><category term="azure" /><category term="psrule" /><category term="terraform" /><category term="azuredevops" /><category term="pipelines" /><category term="powershell" /><summary type="html"><![CDATA[This blog post is part of the Azure Spring Clean 2025 virtual community event, promoting well-managed Azure tenants. In last year’s Azure Spring Clean, Dan Rios blogged about using PSRule for Bicep code. The focus of this blog post is on how you can use PSRule to validate Azure resources deployed via Terraform by HashiCorp.]]></summary></entry><entry><title type="html">How I became a (PowerShell) millionaire</title><link href="https://wragg.io/how-i-became-a-powershell-millionaire/" rel="alternate" type="text/html" title="How I became a (PowerShell) millionaire" /><published>2024-08-13T10:00:00+00:00</published><updated>2024-08-13T10:00:00+00:00</updated><id>https://wragg.io/how-i-became-a-powershell-millionaire</id><content type="html" xml:base="https://wragg.io/how-i-became-a-powershell-millionaire/"><![CDATA[<p>There was a recent trend on Twitter/X where people were sharing whether they spent more time tweeting or coding, which you can discover for yourself through this <a href="https://shiptalkers.dev/compare">tool</a>. Apparently I spent 61% more time tweeting than coding (in public anyway), and while I was pondering whether or not that was a good thing, <a href="https://x.com/dfinke">Doug Finke</a> came to this excellent conclusion:</p>

<blockquote class="twitter-tweet" data-align="center" data-theme="dark"><p lang="en" dir="ltr">If no one knows what you&#39;re doing, what&#39;s the point? <a href="https://t.co/JdNcIDpzzU">pic.twitter.com/JdNcIDpzzU</a></p>&mdash; Doug Finke (@dfinke) <a href="https://twitter.com/dfinke/status/1798785849238360370?ref_src=twsrc%5Etfw">June 6, 2024</a></blockquote>
<script async="" src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

<p>Which got me thinking about the modules I’ve published in <a href="https://github.com/markwragg">GitHub</a> and the <a href="https://www.powershellgallery.com/profiles/markwragg">PowerShell Gallery</a> over the last few years (almost 10 in fact). So I’ve decided to revisit them, why they exist, and whether they are still useful, in case it inspires anyone else to go on their own decade long journey of publishing things to the gallery.</p>

<blockquote>
  <p>If you’ve never published a module to the PowerShell Gallery and are interested in how to get started, there’s a detailed guide in the <a href="https://learn.microsoft.com/en-us/powershell/gallery/how-to/publishing-packages/publishing-a-package?view=powershellget-3.x">official documentation here</a>.</p>
</blockquote>

<p>I thought it might be interesting to do that in order of popularity, and (if you’ve used a consistent author name) it’s actually quite easy (although not entirely quick as I think it has to parse every module) to get your modules from the gallery along with their download counts by using <code class="language-plaintext highlighter-rouge">Find-Module</code>. For example:</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">Find-ModuleByAuthor</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="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="nv">$Author</span><span class="w">
  </span><span class="p">)</span><span class="w">

  </span><span class="n">Write-Verbose</span><span class="w"> </span><span class="s2">"Finding all modules by </span><span class="nv">$Author</span><span class="s2">.."</span><span class="w">
  </span><span class="nv">$Modules</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Find-Module</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Where-Object</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Author</span><span class="w"> </span><span class="o">-eq</span><span class="w"> </span><span class="nv">$Author</span><span class="w"> </span><span class="p">}</span><span class="w">

  </span><span class="kr">foreach</span><span class="w"> </span><span class="p">(</span><span class="nv">$Module</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$Modules</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">

    </span><span class="n">Write-Verbose</span><span class="w"> </span><span class="s2">"Finding all versions of </span><span class="si">$(</span><span class="nv">$Module</span><span class="o">.</span><span class="nf">Name</span><span class="si">)</span><span class="s2">.."</span><span class="w">
    </span><span class="nv">$ModuleVersions</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Find-Module</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nv">$Module</span><span class="o">.</span><span class="nf">Name</span><span class="w"> </span><span class="nt">-AllVersions</span><span class="w">

    </span><span class="nv">$FirstPublishedDate</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nv">$ModuleVersions</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Sort</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Version</span><span class="w"> </span><span class="o">-as</span><span class="w"> </span><span class="p">[</span><span class="n">version</span><span class="p">]</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="o">|</span><span class="w"> 
      </span><span class="n">Select</span><span class="w"> </span><span class="nt">-First</span><span class="w"> </span><span class="nx">1</span><span class="p">)</span><span class="o">.</span><span class="nf">PublishedDate</span><span class="w">
    </span><span class="nv">$DownloadCount</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">int</span><span class="p">](</span><span class="nv">$ModuleVersions</span><span class="o">.</span><span class="nf">AdditionalMetadata</span><span class="o">.</span><span class="nf">versionDownloadCount</span><span class="w"> </span><span class="o">|</span><span class="w"> 
      </span><span class="n">Measure-Object</span><span class="w"> </span><span class="nt">-Sum</span><span class="p">)</span><span class="o">.</span><span class="nf">Sum</span><span class="w">

    </span><span class="p">[</span><span class="n">PSCustomObject</span><span class="p">]@{</span><span class="w">
      </span><span class="nx">Name</span><span class="w">               </span><span class="o">=</span><span class="w"> </span><span class="nv">$Module</span><span class="err">.</span><span class="nx">Name</span><span class="w">
      </span><span class="nx">Downloads</span><span class="w">          </span><span class="o">=</span><span class="w"> </span><span class="nv">$DownloadCount</span><span class="w">
      </span><span class="nx">Description</span><span class="w">        </span><span class="o">=</span><span class="w"> </span><span class="nv">$Module</span><span class="err">.</span><span class="nx">Description</span><span class="w">
      </span><span class="nx">Link</span><span class="w">               </span><span class="o">=</span><span class="w"> </span><span class="nv">$Module</span><span class="err">.</span><span class="nx">ProjectUri</span><span class="w">
      </span><span class="nx">Published</span><span class="w">          </span><span class="o">=</span><span class="w"> </span><span class="nv">$FirstPublishedDate</span><span class="w">
      </span><span class="nx">LastUpdated</span><span class="w">        </span><span class="o">=</span><span class="w"> </span><span class="nv">$Module</span><span class="err">.</span><span class="nx">PublishedDate</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>

<p>Here’s where I discovered a horrifying statistic:</p>

<h1 id="modules-ive-published-in-the-powershell-gallery-have-been-downloaded-in-total-almost-a-million-times">Modules I’ve published in the PowerShell Gallery have been downloaded (in total) <em>almost a million times</em>.</h1>

<p>(well.. 963,850 times to be exact):</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$Modules</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Find-ModuleByAuthor</span><span class="w"> </span><span class="nt">-Author</span><span class="w"> </span><span class="s1">'Mark Wragg'</span><span class="w">
</span><span class="nv">$Modules</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select</span><span class="w"> </span><span class="nx">Downloads</span><span class="p">,</span><span class="nx">Published</span><span class="p">,</span><span class="nx">Name</span><span class="p">,</span><span class="nx">Link</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Sort</span><span class="w"> </span><span class="nx">Downloads</span><span class="w">
</span></code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Downloads</th>
      <th>Published</th>
      <th>Name</th>
      <th>Link</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>359</td>
      <td>14/01/2024 15:34:23</td>
      <td>AzCostTools</td>
      <td><a href="https://github.com/markwragg/PowerShell-AzCostTools">https://github.com/markwragg/PowerShell-AzCostTools</a></td>
    </tr>
    <tr>
      <td>1221</td>
      <td>02/10/2018 11:28:45</td>
      <td>MacNotify</td>
      <td><a href="https://github.com/markwragg/PowerShell-MacNotify">https://github.com/markwragg/PowerShell-MacNotify</a></td>
    </tr>
    <tr>
      <td>1283</td>
      <td>25/01/2017 11:30:48</td>
      <td>PSHipChat</td>
      <td><a href="https://github.com/markwragg/Powershell-Hipchat">https://github.com/markwragg/Powershell-Hipchat</a></td>
    </tr>
    <tr>
      <td>1584</td>
      <td>19/01/2017 15:21:44</td>
      <td>XKCD</td>
      <td><a href="https://github.com/markwragg/Powershell-XKCD">https://github.com/markwragg/Powershell-XKCD</a></td>
    </tr>
    <tr>
      <td>1666</td>
      <td>25/05/2017 14:43:28</td>
      <td>Remedy</td>
      <td><a href="https://github.com/markwragg/Powershell-Remedy">https://github.com/markwragg/Powershell-Remedy</a></td>
    </tr>
    <tr>
      <td>1674</td>
      <td>25/01/2017 13:50:21</td>
      <td>SlackBot</td>
      <td><a href="https://github.com/markwragg/Powershell-SlackBot">https://github.com/markwragg/Powershell-SlackBot</a></td>
    </tr>
    <tr>
      <td>1675</td>
      <td>07/02/2024 23:48:58</td>
      <td>CurrencyConverter</td>
      <td><a href="https://github.com/markwragg/PowerShell-CurrencyConverter">https://github.com/markwragg/PowerShell-CurrencyConverter</a></td>
    </tr>
    <tr>
      <td>4395</td>
      <td>07/08/2019 13:52:53</td>
      <td>Lumos</td>
      <td><a href="https://github.com/markwragg/powershell-lumos">https://github.com/markwragg/powershell-lumos</a></td>
    </tr>
    <tr>
      <td>9800</td>
      <td>07/06/2016 11:43:38</td>
      <td>ADAudit</td>
      <td><a href="https://github.com/markwragg/Test-ActiveDirectory">https://github.com/markwragg/Test-ActiveDirectory</a></td>
    </tr>
    <tr>
      <td>14898</td>
      <td>19/03/2018 09:08:11</td>
      <td>Watch</td>
      <td><a href="https://github.com/markwragg/Powershell-Watch">https://github.com/markwragg/Powershell-Watch</a></td>
    </tr>
    <tr>
      <td>24316</td>
      <td>06/08/2018 09:25:13</td>
      <td>HashCopy</td>
      <td><a href="https://github.com/markwragg/Powershell-HashCopy">https://github.com/markwragg/Powershell-HashCopy</a></td>
    </tr>
    <tr>
      <td>432312</td>
      <td>11/04/2019 10:52:29</td>
      <td>Subnet</td>
      <td><a href="https://github.com/markwragg/PowerShell-Subnet">https://github.com/markwragg/PowerShell-Subnet</a></td>
    </tr>
    <tr>
      <td>468667</td>
      <td>31/12/2017 09:41:33</td>
      <td>Influx</td>
      <td><a href="https://github.com/markwragg/Powershell-Influx">https://github.com/markwragg/Powershell-Influx</a></td>
    </tr>
  </tbody>
</table>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="nv">$modules</span><span class="o">.</span><span class="nf">downloadCount</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Measure</span><span class="w"> </span><span class="nt">-Sum</span><span class="p">)</span><span class="o">.</span><span class="nf">Sum</span><span class="w">

</span><span class="mi">963850</span><span class="w">
</span></code></pre></div></div>

<p>So this is a story all about how (my life got flipped-turned upside down, and I’d like to take a minute just sit right there while I tell you how) I’m about to become a PowerShell Gallery millionaire.</p>

<h3 id="azcosttools">AzCostTools</h3>

<ul>
  <li><strong>First published:</strong> 14 Jan 2024</li>
  <li><strong>Last updated:</strong> 02 Jun 2024</li>
  <li><strong>Download count:</strong> 359</li>
</ul>

<p>AzCostTools is one of my newest projects. I introduced it in <a href="https://wragg.io/monitor-and-manage-your-azure-cloud-costs-with-powershell/">this blog post</a> which was written for <a href="https://www.azurespringclean.com/">Azure Spring Clean</a>. It allows you to report on monthly costs across multiple Azure Subscriptions and compare those costs to one or more previous months. It also generates some simple charts using another module called <a href="https://github.com/endowdly/PSparklines">PSparklines</a>. The idea is to have a single command you can run once a month (or schedule) to generate insight into how your costs are changing in Azure.</p>

<p><strong>Should you use it?</strong> Yes! (if you’re using Azure and want to better understand or report on your costs), although currently it doesn’t work for CSP subscriptions (where billing is done elsewhere). However I am working on a solution to this, as you can usually schedule CSP costs to be exported to a storage account and then AzCostTools will be able to import those exports.</p>

<h3 id="macnotify">MacNotify</h3>

<ul>
  <li><strong>First published:</strong> 02 Oct 2018</li>
  <li><strong>Last updated:</strong> 17 Mar 2024</li>
  <li><strong>Download count:</strong> 1221</li>
</ul>

<p>My second least popular module is an apparently little known tool called <a href="https://github.com/markwragg/PowerShell-MacNotify">MacNotify</a> that I published in 2018. This is for MacOS users of PowerShell, made possible since the introduction of PowerShell Core. MacNotify allows you to generate your own custom MacOS alerts (those little pop up boxes that appear in the top right). If you’ve ever heard of <a href="https://github.com/Windos/BurntToast">BurntToast</a> by <a href="https://x.com/WindosNZ">Josh King</a> (which has only been downloaded 19 million times), MacNotify is the Mac equivalent. In fact Josh went on to create a cross-platform module called <a href="https://github.com/Windos/PoshNotify">PoshNotify</a> which wraps BurntToast, MacNotify and <a href="https://github.com/TylerLeonhardt/PSNotifySend">PSNotifySend</a> (for generating notifications under Linux) so that you can use consistent cmdlet names to generate notifications on any of those operating systems.</p>

<p><strong>Should you use it?</strong> Yes! In fact I made some updates to it a few months ago as I was clearing down old issues on my GitHub repos and as part of that I made sure it still works on the current version of MacOS (which it does!) so if you’re running PowerShell on MacOS and want an easy way to surface events MacNotify could be what you’re looking for. Or use PoshNotify and make your script OS notifications platform agnostic.</p>

<h3 id="pshipchat">PSHipChat</h3>

<ul>
  <li><strong>First published:</strong> 25 Jan 2017</li>
  <li><strong>Last updated:</strong> 09 Sep 2019</li>
  <li><strong>Download count:</strong> 1283</li>
</ul>

<p>I first uploaded <a href="https://github.com/markwragg/Powershell-Hipchat">PSHipChat</a> to GitHub in March of 2016. HipChat is (was?) Atlassian’s answers to Slack. I think they bundled it free with some of their other products (such as Confluence) as I worked for a few companies where it was used before Slack became more ubiquitous and now (for me at least) Teams. The PSHipChat module provides some PowerShell cmdlets for sending notifications into HipChat from your scripts by invoking its API (spoiler alert: wrapping an API is the theme of a lot of my modules, FYI). This allowed my team at the time to get visibility of when certain automated tasks were running, by just adding a few lines to those scripts to trigger notifications in HipChat.</p>

<p>The original blog for it is <a href="https://wragg.io/send-notifications-to-hipchat-with-powershell/">here</a>.</p>

<p><strong>Should you use it?</strong> I have no idea tbh. I haven’t worked anywhere with HipChat for a number of years, so I’m not sure if it still works. The project is actually now maintained under the <a href="https://github.com/AtlassianPS">AtlassianPS</a> GitHub organisation, but its not getting a lot of love there either.</p>

<h3 id="xkcd">XKCD</h3>

<ul>
  <li><strong>First published:</strong> 19 Jan 2017</li>
  <li><strong>Last updated:</strong> 25 Feb 2020</li>
  <li><strong>Download count:</strong> 1584</li>
</ul>

<p><a href="https://xkcd.com/">XKCD</a> is a webcomic of romance, sarcasm, math, and language that’s been going since September 2005. The <a href="https://github.com/markwragg/Powershell-XKCD">XKCD module</a> just wraps some PowerShell friendly cmdlets around the XKCD API which requires no key/registration and returns JSON. This was mostly for fun, and an excuse to get my head around <a href="https://wragg.io/create-dynamic-powershell-functions-with-parameter-sets/">creating advanced functions with Parameter Sets</a>.</p>

<p><strong>Should you use it?</strong> I don’t see why not! It still works. The most useful thing about it is its ability to search for a specific comic by keyword, and it builds a local cache of the comics inside the Module so subsequent searches require no calls to the API. So if you’re looking for a comic on a specific topic, it’s as good as Google:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Find-Xkcd -Query covid | FT

month  num link year news safe_title              transcript alt
-----  --- ---- ---- ---- ----------              ---------- ---
7     2333      2020      COVID Risk Chart                   First prize is a free ticket to the kissing booth.
8     2346      2020      COVID Risk Comfort Zone            I'm like a vampire, except I'm not crossing that threshold even if you invite me.
9     2355      2020      University COVID Model             I admit this is an exaggeration, since I can think of at least three parties I attended while doing my degree, and I'm probably forgetting several more.
12    2395      2020      Covid Precaution Level             It's frustrating to calibrate your precautions when there's only one kind of really definitive feedback you can get, you can only get it once, and when you do it's too late.
</code></pre></div></div>

<p>Or you can open a random comic by running <code class="language-plaintext highlighter-rouge">Get-Xkcd -Random -Open</code>.</p>

<p><a href="https://xkcd.com/428/" class="align-center"><img src="/content/images/2024/xkcd-blog.png" alt="xkcd starwatching comic #428" /></a></p>

<h3 id="remedy">Remedy</h3>

<ul>
  <li><strong>First published:</strong> 25 May 2017</li>
  <li><strong>Last updated:</strong> 10 Sep 2019</li>
  <li><strong>Download count:</strong> 1666</li>
</ul>

<p>My <a href="https://github.com/markwragg/Powershell-Remedy">Remedy</a> module is another API wrapper, this time for the ITSM tool BMC Remedy. At the time I wrote this module I was working somewhere that used BMC Remedy and the interface for it back then was pretty clunky. Being able to pull data out of it via PowerShell allowed me to automate a lot of reporting that I needed to generate, as well as hook it up to the next module in this list (SlackBot) to allow my team to query tickets / changes / assets as part of a Slack conversation.</p>

<p><strong>Should you use it?</strong> This is another one where I’m not sure. I haven’t worked somewhere with BMC Remedy for a while. If you’re using the version of Remedy I had when I wrote it (which I think was v6) then I suspect it will still work, but I doubt it’s compatible with any subsequent versions.</p>

<h3 id="slackbot">SlackBot</h3>

<ul>
  <li><strong>First published:</strong> 25 Jan 2017</li>
  <li><strong>Last updated:</strong> 10 Sep 2019</li>
  <li><strong>Download count:</strong> 1674</li>
</ul>

<p>The purpose of my <a href="https://github.com/markwragg/Powershell-SlackBot">SlackBot</a> module was to provide a basic demonstration of <a href="https://wragg.io/powershell-slack-bot-using-the-real-time-messaging-api/">how to setup a bot for Slack using their Real-time messaging API</a>. This kind of Bot essentially monitors one or more chat channels for messages (as if it were a user) and can then respond to certain messages directed at it. Part of the motivation for sharing this code was because it was really fiddly to get working at the time (and I wanted to save others the pain).</p>

<p>The version in the PowerShell Gallery (and on GitHub) is just a basic framework that you need to extend with whatever functionality you want the Bot to do. At the company I was working for at the time our internal version of it had lots of additional functionality so you could use to query for assets / tickets / changes etc (as mentioned above) and it would then make calls to other systems to retrieve that data.</p>

<p><strong>Should you use it?</strong> Maybe. If you’d rather build something up yourself, I think its still a good starting point, although I am also not a big Slack user these days so I don’t know entirely if it’s still compatible with the API. There is a more featured alternative I can recommend though (that I suspect is also more up to date) and easier to customise/extend with additional functionality: <a href="https://github.com/poshbotio/PoshBot">PoshBot</a> by <a href="https://github.com/devblackops">Brandon Olin</a>.</p>

<h3 id="currencyconverter">CurrencyConverter</h3>

<ul>
  <li><strong>First published:</strong> 07 Feb 2024</li>
  <li><strong>Last updated:</strong> 18 May 2024</li>
  <li><strong>Download count:</strong> 1675</li>
</ul>

<p><a href="https://github.com/markwragg/PowerShell-CurrencyConverter">CurrencyConverter</a> is the newest module in this list. It actually began life as part of AzCostTools, where I wanted to be able to convert Azure costs into an alternative currency (as sometimes they’re billed in a currency that is not your own). I realised this was probably <a href="https://wragg.io/Perform-currency-conversion-with-PowerShell/">useful functionality in its own right</a> and there didn’t seem to be a recent module in the gallery that could do the same. One of the appealing things about my module is that it uses a backend service that has an open API, so there’s no need to register an account / generate a key. This caused some confusion on Reddit when I shared it as people thought I’d just hard coded my own key into the tool but this is not the case.</p>

<p><strong>Should you use it?</strong> Yes! It should work fine as long as the API it wraps continues to exist. I also added crypto currency conversion recently at the request of someone who raised an issue on it’s GitHub project. Beware though that for traditional currencies the exchange rates only update daily.</p>

<h3 id="lumos">Lumos</h3>

<ul>
  <li><strong>First published:</strong> 07 Aug 2019</li>
  <li><strong>Last updated:</strong> 07 Dec 2021</li>
  <li><strong>Download count:</strong> 4395</li>
</ul>

<p>The year was 2019. Windows 10 had just got a dark mode feature, as had MacOS, but neither offered the ability to switch between light and dark mode automatically. Hence <a href="https://github.com/markwragg/PowerShell-Lumos">Lumos</a> was born, and the world was saved. I think both OSes do allow you to configure automatic switching now, so Lumos is kind of redundant. That being said it does allow you to switch Windows to dark mode, but keep apps as light mode (or vice versa) and it also allows you to configure a specific wallpaper for each mode that it can then switch for you at the same time (can Windows do that? Maybe! I haven’t checked). Plus it’s fun to type <code class="language-plaintext highlighter-rouge">Invoke-Lumos</code> and feel like (you’re) a Wizard (Harry).</p>

<p><strong>Should you use it?</strong> Yes, be magical.</p>

<h3 id="adaudit">ADAudit</h3>

<ul>
  <li><strong>First published:</strong> 07 Jun 2016</li>
  <li><strong>Last updated:</strong> 25 Feb 2020</li>
  <li><strong>Download count:</strong> 9800</li>
</ul>

<p>I always thought XKCD was the first module I ever published, but apparently the title belongs to <a href="https://github.com/markwragg/Test-ActiveDirectory">ADAudit</a>. This module actually just built on some excellent work by <a href="https://x.com/IrwinStrachan">Irwin Strachan</a>, who was very supportive and continues to be a wonderful and generous member of the PowerShell community. ADAudit is just a set of Pester tests which validate whether an Active Directory forest is in good health. The idea is that (assuming your AD is currently healthy) you export a “gold snapshot” of its current state and then can run the tests routinely to see if anything has changed / is out of order.</p>

<p>If you want to read more about it the original blog is <a href="https://wragg.io/testing-active-directory-with-pester-and-powershell/">here</a>.</p>

<p><strong>Should you use it?</strong> I would say probably not, unless you fancy bringing it up to date. I haven’t updated it for several years and so it’s currently designed for use with Pester v3 (which is pretty old). A good alternative looks to be <a href="https://github.com/EvotecIT/Testimo">Testimo</a> by EvotecIT, although I haven’t personally used it, it looks to be more featured and recently maintained.</p>

<h3 id="watch">Watch</h3>

<ul>
  <li><strong>First published:</strong> 19 Mar 2018</li>
  <li><strong>Last updated:</strong> 08 Mar 2023</li>
  <li><strong>Download count:</strong> 14898</li>
</ul>

<p>Linux has a Watch command and Windows doesn’t which was frankly unacceptable. <a href="https://github.com/markwragg/PowerShell-Watch">Watch</a> allows you to <a href="https://wragg.io/watch-for-changes-with-powershell/">run a command (or script block) continuously until the output changes</a>. It’s useful if you’re waiting for something to occur, or want to catch something in the act (like a process starting, or terminating). Ultimately all we’re really doing here is wrapping some code in a <code class="language-plaintext highlighter-rouge">while</code> loop and comparing it’s output to the previous iteration. One of the coolest things about <code class="language-plaintext highlighter-rouge">Watch-Command</code> is that you can just pipe a string of commands to it. It then gets the full pipeline that proceeded it from <code class="language-plaintext highlighter-rouge">$MyInvocation</code> so it can loop those commands and watch for changes. You can also send it a script block, but you don’t have to. This makes it pretty convenient to use.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-Service</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select</span><span class="w"> </span><span class="nx">Name</span><span class="p">,</span><span class="nx">Status</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Watch-Command</span><span class="w">
</span></code></pre></div></div>

<p><strong>Should you use it?</strong> Yes! It still works as far as I know. I don’t use it in earnest but every now and again I have some need to quickly monitor something for change and I remember it exists. And 14,865 other people apparently did too.</p>

<h3 id="hashcopy">HashCopy</h3>

<ul>
  <li><strong>First published:</strong> 06 Aug 2018</li>
  <li><strong>Last updated:</strong> 02 Jul 2023</li>
  <li><strong>Download count:</strong> 24316</li>
</ul>

<p>24.3K downloads (you can tell we’re getting serious now, because I’m using “K”). <a href="https://github.com/markwragg/PowerShell-HashCopy">HashCopy</a> came into existence because I needed to know if some files in one folder matched some files in another folder, but I couldn’t trust the modified dates to tell me (long story short: Git). I discovered PowerShell has a <code class="language-plaintext highlighter-rouge">Get-FileHash</code> command, which you can use to compute the hash value of a file. So by computing the hash values of the files I wanted to compare, I could quickly determine if they were different.</p>

<p><strong>Should you use it?</strong> Yes! I guess I’m not the only person to face this problem based on the downloads, and I don’t think much has changed for <code class="language-plaintext highlighter-rouge">Get-FileHash</code> so it should still work. You can read the original blog post <a href="https://wragg.io/a-powershell-cmdlet-to-copy-files-based-on-hash-difference/">here</a>.</p>

<h3 id="subnet">Subnet</h3>

<ul>
  <li><strong>First published:</strong> 11 Apr 2019</li>
  <li><strong>Last updated:</strong> 10 Sep 2019</li>
  <li><strong>Download count:</strong> 432312</li>
</ul>

<p>Now we take a giant leap to the last two modules that do most of the heavy lifting for this two-thumbed millionaire. If I’m being honest here, I suspect much of the success of my <a href="https://github.com/markwragg/PowerShell-Subnet">Subnet</a> module is because I noticed no one had nabbed the name “Subnet”. It’s possibly an oversight (and a bit of a security flaw) of the PowerShell Gallery that anyone can take any available name.</p>

<p>The Subnet module has 3 commands, <code class="language-plaintext highlighter-rouge">Get-Subnet</code> calculates details of a specified subnet (such as it’s subnet mask, IP range and the host addresses), <code class="language-plaintext highlighter-rouge">Get-NetworkClass</code> tells you the network class of a specified IP and <code class="language-plaintext highlighter-rouge">Test-PrivateIP</code> gives you a true/false result for public/private IPs.</p>

<p><strong>Should you use it?</strong> Yes! I personally use it pretty frequently as it’s usually more convenient than using a subnet calculator website. It’s also a great tool if you need to generate subnet details for a list of IP addresses.</p>

<h3 id="influx">Influx</h3>

<ul>
  <li><strong>First published:</strong> 31 Dec 2017</li>
  <li><strong>Last updated:</strong> 06 May 2023</li>
  <li><strong>Download count:</strong> 468667</li>
</ul>

<p>And finally, with a horrifying 468,659 downloads and counting is <a href="https://github.com/markwragg/PowerShell-Influx">Influx</a>. Guess what? I had a need to write some metrics into Influx (which is a time-series database that I heartily recommend, particularly if you pair it with Grafana), using the API directly was a little clunky, and so I did something I’d ~never~ done before and turned to PowerShell. The Influx module makes writing statistics into an Influx database pretty simple. And I guess lots of other people thought so too.</p>

<p>Read the original blog post <a href="https://wragg.io/windows-based-grafana-analytics-platform-via-influxdb-and-powershell/">here</a>.</p>

<p><strong>Should you use it?</strong> I mean, I guess?! One regret I have is that I bundled into the module various other commands that I wrote for retrieving values from things like VMWare and 3PAR and then writing those into Influx and I think that kind of bloats the module now, but for all I know 100k people think those things are really useful.</p>

<hr />

<p>If you’ve made it this far, thanks for indulging me through this walk down memory lane. You’ve perhaps noticed there’s a consistent theme to the modules above, which is that most of them started life as a way to solve some specific problem for me, that I realised could probably also help someone else. None of the modules above are perfect, but I suspect they’ve saved (perhaps a lot) of people (hopefully a lot) of time, and that’s worth a million. Here’s to the next.</p>

<p><img src="/content/images/2024/Leonardo-Dicaprio-Cheers.jpg" alt="Leonardo DiCaprio Cheers" class="align-center" /></p>]]></content><author><name>Mark Wragg</name><email>mark@wragg.io</email></author><category term="powershell" /><category term="module" /><category term="github" /><summary type="html"><![CDATA[There was a recent trend on Twitter/X where people were sharing whether they spent more time tweeting or coding, which you can discover for yourself through this tool. Apparently I spent 61% more time tweeting than coding (in public anyway), and while I was pondering whether or not that was a good thing, Doug Finke came to this excellent conclusion:]]></summary></entry><entry><title type="html">Converting Azure DevOps Classic Release deployment pipelines to YAML</title><link href="https://wragg.io/converting-azure-devops-classic-release-pipelines-to-yaml/" rel="alternate" type="text/html" title="Converting Azure DevOps Classic Release deployment pipelines to YAML" /><published>2024-04-02T12:00:00+00:00</published><updated>2024-04-02T12:00:00+00:00</updated><id>https://wragg.io/converting-azure-devops-classic-release-pipelines-to-yaml</id><content type="html" xml:base="https://wragg.io/converting-azure-devops-classic-release-pipelines-to-yaml/"><![CDATA[<p>I recently migrated some Azure DevOps Classic Release deployment pipelines to YAML. There’s obvious benefits to storing your pipelines as code: they become an artifact in source control that can evolve and change as the code they build or deploy does, and you have the benefits of version history and maintaining the pipelines via pull requests. However I also found that I could use logic and expressions to make the pipelines more efficient and easier to maintain and that through templating could easily connect the pipelines together to form what I humorously dubbed the “super pipeline” (but then the name stuck). In this blog post I will explain the approach I took and the advantages/disadvantages I discovered.</p>

<blockquote>
  <p>Maybe the real treasure was the automation improvements we made along the way.</p>
</blockquote>

<p>Originally Azure DevOps featured two sections for building pipeline automation, named Builds and Releases. Several years ago Microsoft renamed the Builds section to just “Pipelines” to better reflect that they can be used to create automation for any purpose (including builds or releases), or if you’re doing CI/CD, a single pipeline that both builds and releases. Pipelines that you create under “Pipelines” can still be built via the UI, or they can be stored as one or more YAML files in source control. In contrast, pipelines built under “Releases” can only be configured via the Azure DevOps UI. These pipelines do feature a version history, but its only visible within Azure DevOps.</p>

<h2 id="getting-started">Getting started</h2>

<p>Getting started with YAML pipelines can be a little intimidating. If you go to Pipelines (underneath Pipelines..) and click the “New Pipeline” button in the top right, Azure DevOps will take you through a sort of setup wizard (because Microsoft 😝). You’ll first need to select where your code is, which can be an Azure DevOps repository, Github repository, bitbucket cloud or several other option (it’s nice that you can use Azure DevOps to build or automate code that lives elsewhere). Once you’ve picked a location, its going to ask you if you have an existing pipeline definition (i.e a YAML file that is already in the repository) or if you want to create a new one. You can choose “starter pipeline” to have a very basic scaffolding at the top, or choose from a number of templates for different build purposes (it will suggest templates based on the kind of code it finds in the repository you selected). But all of this is very build focussed, and there’s nothing overly useful here for someone looking to create a deployment pipeline.</p>

<blockquote>
  <p>If you’re converting a Classic Build pipeline to YAML, the process is relatively simple as you can export the whole pipeline as YAML via one step. See the <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/migrate/from-classic-pipelines?view=azure-devops">official guidance here</a></p>

  <p>Unfortunately there’s not a built-in way to do the same for Classic Release pipelines. This third party tool can apparently do it, but I didn’t personally try it so use at your own risk:</p>

  <ul>
    <li><a href="https://github.com/f2calv/yamlizr">https://github.com/f2calv/yamlizr</a></li>
  </ul>
</blockquote>

<p>Personally I started from scratch, so I suggest finding a suitable location in whatever repository you want to store your pipeline, and create a new file there called (for example) <code class="language-plaintext highlighter-rouge">Deployment.yml</code> (or whatever you want to name it).</p>

<blockquote>
  <p>Working with YAML pipelines can be fiddly, because YAML requires things be indented correctly. I recommend using Visual Studio Code, and installing the <a href="https://marketplace.visualstudio.com/items?itemName=ms-azure-devops.azure-pipelines">Azure Pipelines Extension</a> as well as an extension to help with formatting YAML, such as <a href="https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode">Prettier</a></p>

  <p>If you’re new to YAML pipelines I also recommend reading the <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/?view=azure-devops">Azure DevOps pipelines documentation</a>, to familiarise yourself with things such as the <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/get-started/key-pipelines-concepts?view=azure-devops">key concepts</a>.</p>
</blockquote>

<p>The Classic Release pipelines I migrated were separated into stages, with a single stage per environment. We would then create a Release, which would consume the artifacts from several builds that contained the built version of the product being deployed, and manually trigger each environment stage whenever we were ready to deploy. There’s lots of different ways to implement this as YAML, I could create a pipeline per environment, or have pipelines that deploy to multiple environments, but what I decided to do was use <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/templates?view=azure-devops&amp;pivots=templates-includes">Templates</a>, so that the pipeline could be defined once, but have an environment parameter in it to specify which environment was being deployed.</p>

<h2 id="parameters">Parameters</h2>

<p>To specify parameters, your YAML file needs a parameters section, under which you create one or more parameters by specifying a name, type and default value. If you want the parameter to be a drop-down selection, you can specify a list of accepted values:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">parameters</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Environment</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">string</span>
    <span class="na">values</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">Test"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">Staging"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">UAT"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">Production"</span>
    <span class="na">default</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Test"</span>
</code></pre></div></div>

<p>When the pipeline is executed, the environment selection needs to make sure that we’re using variables specific to that environment. Under the Classic Release pipeline, if you took the approach of having a stage per environment, you could then have variables scoped to that environment (stage). To achieve the same result for the YAML pipeline, we can create Variable Groups, which we link to the template.</p>

<h2 id="variable-groups">Variable Groups</h2>

<p>To create Variable Groups, in Azure DevOps go to Pipelines &gt; Library. You might want to create a variable group called “All environments”, which has default values, or values that apply to all environments. On the Classic Release pipelines the equivalent of this would be variables that were Release scoped. You might also want to create variable groups that apply to more than one of your environments, for values that are the same for all environments of that type, for example: “Non-production environments” and “Production environments”. Finally you want to create a variable group for each specific environment, for example: “Test”, “Staging”, “UAT” etc. Within each of your variable groups, create the various environment specific variables that are required for deployment.</p>

<blockquote>
  <p>Note that with the Classic Release pipelines, the variables you created on a pipeline would be cloned when you created a Release, and then any changes you made to the variables on that Release would affect it only. With variable groups, the variables are read from the group at time of execution, so bear in mind that any changes you make to the variables that reference your groups will affect old and new deployments alike.</p>
</blockquote>

<h2 id="variables">Variables</h2>

<p>Having created your variable groups, you can now reference them in the YAML template, via a <code class="language-plaintext highlighter-rouge">variables</code> section. The order in which you specify each group is important, if the same named variable is in multiple groups the last one defined will be used. So define them in priority order. For example:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">variables</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">group</span><span class="pi">:</span> <span class="s2">"</span><span class="s">All</span><span class="nv"> </span><span class="s">environments"</span>

  <span class="pi">-</span> <span class="s">${{ if in(parameters.Environment, 'Test','Staging') }}</span><span class="err">:</span>
      <span class="pi">-</span> <span class="na">group</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Non-production</span><span class="nv"> </span><span class="s">environments"</span>

  <span class="pi">-</span> <span class="s">${{ if in(parameters.Environment, 'UAT', 'Prod') }}</span><span class="err">:</span>
      <span class="pi">-</span> <span class="na">group</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Production</span><span class="nv"> </span><span class="s">environments"</span>

  <span class="pi">-</span> <span class="na">group</span><span class="pi">:</span> <span class="s">${{ parameters.Environment }}</span>
</code></pre></div></div>

<p>In the above I’ve used a <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/expressions?view=azure-devops#conditional-insertion">conditional expression</a> to define the inclusion of the prod/non-prod groups based on the Environment parameter matching one or more specified names. And then I just use the Environment parameter value itself to include the variable group that I created for the specified environment. The <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/template-expressions?view=azure-devops">template expression syntax</a> <code class="language-plaintext highlighter-rouge">${{ }}</code> is used to include these bits of logic. These get processed when the pipeline is initialised.</p>

<h2 id="resources">Resources</h2>

<p>Because we have separate build pipelines (that generate our deployment artifacts such as infrastructure templates and a compiled release of the software) we next define a <code class="language-plaintext highlighter-rouge">resources</code> section. <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/resources?view=azure-devops&amp;tabs=schema">Resources</a> are anything used by a pipeline that live outside the pipeline. This can include other Azure DevOps pipelines as well as builds from other external CI systems.</p>

<p>In my case, I need to consume some other Azure DevOps pipelines, for example one that builds my infrastructure artifacts (such as ARM templates and deployment scripts) and one that builds the product itself. Because these are Azure DevOps pipelines, we consume them by specifying <code class="language-plaintext highlighter-rouge">pipelines</code>. The value given for <code class="language-plaintext highlighter-rouge">pipeline</code> is the alias I want to refer to them via later in the code, <code class="language-plaintext highlighter-rouge">project</code> is the name of the Azure DevOps project they exist in, <code class="language-plaintext highlighter-rouge">source</code> is the actual name of the pipeline, and <code class="language-plaintext highlighter-rouge">version</code> is the specific build version I want to consume.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">resources</span><span class="pi">:</span>
  <span class="na">pipelines</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">pipeline</span><span class="pi">:</span> <span class="s2">"</span><span class="s">InfraBuild"</span>
      <span class="na">project</span><span class="pi">:</span> <span class="s2">"</span><span class="s">MyProject"</span>
      <span class="na">source</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Build_Our_Infrastructure"</span>
      <span class="na">version</span><span class="pi">:</span> <span class="s">1.0.123</span>

    <span class="pi">-</span> <span class="na">pipeline</span><span class="pi">:</span> <span class="s2">"</span><span class="s">ProductBuild"</span>
      <span class="na">project</span><span class="pi">:</span> <span class="s2">"</span><span class="s">MyProject"</span>
      <span class="na">source</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Build_Our_Product"</span>
      <span class="na">version</span><span class="pi">:</span> <span class="s">1.0.456</span>
</code></pre></div></div>

<h2 id="trigger">Trigger</h2>

<p>The next thing I define in my YAML pipeline is the <code class="language-plaintext highlighter-rouge">trigger</code>. By default pipelines trigger whenever there is a commit to any branch in their respective repository, but because this is a deployment pipeline that I want to trigger manually, we need to set trigger to none.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">trigger</span><span class="pi">:</span> <span class="s">none</span>
</code></pre></div></div>

<h2 id="stages">Stages</h2>

<p>We’re now ready to start defining the actual tasks the deployment pipeline will perform. You don’t have to use stages as part of this, but by grouping the different associated sets of tasks into stages you can then easily enable or disable them at the point where you run the deployment. For example you might have a pipeline that deploys databases, configuration and then the application/s. It might make sense to split these into stages if you might have a run in the future where you only want to execute a subset of them.</p>

<p>To define stages we need to specify <code class="language-plaintext highlighter-rouge">stages:</code> and then under that each stage with a name. Within the stage we can define <code class="language-plaintext highlighter-rouge">displayName</code> to have a more descriptive title appear in the UI when the pipeline is run. You use <code class="language-plaintext highlighter-rouge">pool</code> to define which of your Azure DevOps deployment pools will execute this task. You can define this at the top level if all of your tasks will run under the same pool, but for my pipeline I wanted some tasks to run via a self hosted agent and some tasks to run via Microsoft-Hosted agents, so it was necessary to define it at the stage level.</p>

<p>Within each Stage you can have one or more <code class="language-plaintext highlighter-rouge">jobs</code>. We use a special job type here called <code class="language-plaintext highlighter-rouge">deployment</code> to indicate to Azure DevOps that this is a <a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/process/deployment-jobs?view=azure-devops">deployment job</a> we’re running. Under deployment you specify <code class="language-plaintext highlighter-rouge">environment</code> which then allows you to view your deployments per environment under the Pipelines &gt; Environments section of Azure DevOps. In the example below i’m using the <code class="language-plaintext highlighter-rouge">Environments</code> parameter value to populate this via the expression syntax. We then need to specify <code class="language-plaintext highlighter-rouge">strategy</code> because this is a deployment job. For my purpose this needs to be <code class="language-plaintext highlighter-rouge">runOnce</code> because I’m just deploying to a single environment and I just want each phase of the deployment to run once. You can split your tasks under different phases (predeploy, deploy, routeTraffic and postRouteTraffic) but I personally just have all my tasks under deploy. Finally you use <code class="language-plaintext highlighter-rouge">steps:</code> under which you then define each of the tasks to be performed.</p>

<p>Here’s an example first stage, with some tasks for unzipping and deploying a database DACPAC:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">stages</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">stage</span><span class="pi">:</span> <span class="s">DatabaseDeployment</span>
    <span class="na">displayName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Deploy</span><span class="nv"> </span><span class="s">Database"</span>
    <span class="na">pool</span><span class="pi">:</span> <span class="s2">"</span><span class="s">My</span><span class="nv"> </span><span class="s">Deployment</span><span class="nv"> </span><span class="s">Pool"</span>
    <span class="na">jobs</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">deployment</span><span class="pi">:</span> <span class="s">DatabaseDeployment</span>
        <span class="na">environment</span><span class="pi">:</span> <span class="s">${{ parameters.Environment }}</span>
        <span class="na">strategy</span><span class="pi">:</span>
          <span class="na">runOnce</span><span class="pi">:</span>
            <span class="na">deploy</span><span class="pi">:</span>
              <span class="na">steps</span><span class="pi">:</span>
                <span class="pi">-</span> <span class="na">download</span><span class="pi">:</span> <span class="s">ProductBuild</span>
                  <span class="na">artifact</span><span class="pi">:</span> <span class="s">Database</span>
                  <span class="na">displayName</span><span class="pi">:</span> <span class="s">Download Database Release</span>

                <span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">ExtractFiles@1</span>
                  <span class="na">displayName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Extract</span><span class="nv"> </span><span class="s">Database</span><span class="nv"> </span><span class="s">zip</span><span class="nv"> </span><span class="s">file"</span>
                  <span class="na">inputs</span><span class="pi">:</span>
                    <span class="na">archiveFilePatterns</span><span class="pi">:</span> <span class="s1">'</span><span class="s">ProductBuild\Database\Database.zip'</span>
                    <span class="na">destinationFolder</span><span class="pi">:</span> <span class="s1">'</span><span class="s">ProductBuild\Database'</span>
                    <span class="na">cleanDestinationFolder</span><span class="pi">:</span> <span class="no">false</span>

                <span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">AzureKeyVault@1</span>
                  <span class="na">displayName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Get</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">Database</span><span class="nv"> </span><span class="s">password</span><span class="nv"> </span><span class="s">from</span><span class="nv"> </span><span class="s">KeyVault"</span>
                  <span class="na">inputs</span><span class="pi">:</span>
                    <span class="na">azureSubscription</span><span class="pi">:</span> <span class="s2">"</span><span class="s">$(azureSubscription)"</span>
                    <span class="na">KeyVaultName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">$(KeyVaultName)"</span>
                    <span class="na">SecretsFilter</span><span class="pi">:</span> <span class="s">DatabasePassword</span>

                <span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">SqlAzureDacpacDeployment@1</span>
                  <span class="na">displayName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Deploy</span><span class="nv"> </span><span class="s">Product</span><span class="nv"> </span><span class="s">Database"</span>
                  <span class="na">inputs</span><span class="pi">:</span>
                    <span class="na">azureSubscription</span><span class="pi">:</span> <span class="s2">"</span><span class="s">$(azureSubscription)"</span>
                    <span class="na">ServerName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">productdb-$(EnvironmentName).database.windows.net"</span>
                    <span class="na">DatabaseName</span><span class="pi">:</span> <span class="s">ProductDatabase</span>
                    <span class="na">SqlUsername</span><span class="pi">:</span> <span class="s">databaseadmin</span>
                    <span class="na">SqlPassword</span><span class="pi">:</span> <span class="s2">"</span><span class="s">$(DatabasePassword)"</span>
                    <span class="na">DacpacFile</span><span class="pi">:</span> <span class="s2">"</span><span class="s">ProductBuild/Database/ProductDatabase.dacpac"</span>

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

<p>Notice that the first task here is downloading the required artifact from one of the resources I specified in <code class="language-plaintext highlighter-rouge">resources:</code>. By default this task would download all artifacts from the resource, but if you know for a specific stage that you only need a subset of them (and assuming your build outputs multiple resources), then you can specify just the ones you need to speed things up.</p>

<p>If you’re converting a Classic Release pipeline to YAML, you can get the YAML for your individual tasks by going to your Classic Release pipeline and the Tasks view. Then for each individual task there is a “View YAML” link in the top right. You can copy this and paste it into your YAML file, ensuring you indent it appropriately. If the task references a different resource name alias than the one you configured in the <code class="language-plaintext highlighter-rouge">resources:</code> section you’ll need to update that. It will also add comments to the top of the YAML it produces warning you of any variables that have been used that you’d then need to either define on the pipeline directly (under <code class="language-plaintext highlighter-rouge">variables:</code>) or via the variable groups I suggested earlier.</p>

<p>You can see in my example tasks above that I reference a <code class="language-plaintext highlighter-rouge">$(azureSubscription)</code> variable, that is the name of my Azure Subscription. This would be the sort of variable that would exist in my Non-production environments and Production environments variable groups, as I’d have a separate subscription for each. And then i’ve got <code class="language-plaintext highlighter-rouge">$(KeyVaultName)</code> and <code class="language-plaintext highlighter-rouge">$(EnvironmentName)</code> variables, these would be environment specific values that would exist in each of my environment variable groups. The <code class="language-plaintext highlighter-rouge">$()</code> syntax is the same variable substitution syntax that is used on the Classic Release pipelines. Note that variables using this syntax are populated when the pipeline runs.</p>

<h2 id="dependencies">Dependencies</h2>

<p>The stages / jobs / tasks that you specify in a YAML pipeline will execute in the order that you list them, but you can use the <code class="language-plaintext highlighter-rouge">dependsOn</code> setting to specify that certain stages depend on the execution of one or more previous ones. This is also how you can specify certain stages to execute in parallel. For example, imagine I had my DatabaseDeployment stage above, and then I had stages called ConfigurationDeployment and ApplicationDeployment. Lets say my configuration gets written into the database, and my application will just keep polling for it if until it exists. So to speed up my deployment i’m happy for my configuration and application to deploy in parallel, so long as the database has been deployed. By using <code class="language-plaintext highlighter-rouge">dependsOn</code> these two stages will run in parallel:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="pi">-</span> <span class="na">stage</span><span class="pi">:</span> <span class="s">ConfigurationDeployment</span>
    <span class="na">displayName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Deploy</span><span class="nv"> </span><span class="s">Configuration"</span>
    <span class="na">pool</span><span class="pi">:</span> <span class="s2">"</span><span class="s">My</span><span class="nv"> </span><span class="s">Deployment</span><span class="nv"> </span><span class="s">Pool"</span>
    <span class="na">dependsOn</span><span class="pi">:</span> <span class="s">DatabaseDeployment</span>
    <span class="na">jobs</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">deployment</span><span class="pi">:</span> <span class="s">ConfigurationDeployment</span>
        <span class="na">environment</span><span class="pi">:</span> <span class="s">${{ parameters.Environment }}</span>
        <span class="na">strategy</span><span class="pi">:</span>
          <span class="na">runOnce</span><span class="pi">:</span>
            <span class="na">deploy</span><span class="pi">:</span>
              <span class="na">steps</span><span class="pi">:</span>
                <span class="pi">-</span> <span class="s">...</span>

  <span class="pi">-</span> <span class="na">stage</span><span class="pi">:</span> <span class="s">ApplicationDeployment</span>
    <span class="na">displayName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Deploy</span><span class="nv"> </span><span class="s">Application"</span>
    <span class="na">pool</span><span class="pi">:</span> <span class="s2">"</span><span class="s">My</span><span class="nv"> </span><span class="s">Deployment</span><span class="nv"> </span><span class="s">Pool"</span>
    <span class="na">dependsOn</span><span class="pi">:</span> <span class="s">DatabaseDeployment</span>
    <span class="na">jobs</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">deployment</span><span class="pi">:</span> <span class="s">ApplicationDeployment</span>
        <span class="na">environment</span><span class="pi">:</span> <span class="s">${{ parameters.Environment }}</span>
        <span class="na">strategy</span><span class="pi">:</span>
          <span class="na">runOnce</span><span class="pi">:</span>
            <span class="na">deploy</span><span class="pi">:</span>
              <span class="na">steps</span><span class="pi">:</span>
                <span class="pi">-</span> <span class="s">...</span>
</code></pre></div></div>

<p>For the tasks to execute in parallel you need to be using a deployment pool that supports more than 1 parallel job. If you’re using the free tier Microsoft-hosted agents and a private project you only get 1 job by default. If you purchase parallel jobs you have to purchase at least 2 (once you have a paid job the free one no longer applies).</p>

<p>If you want some tasks to execute in parallel at the beginning of your pipeline, you can specify <code class="language-plaintext highlighter-rouge">dependsOn: []</code>.</p>

<h2 id="repetition">Repetition</h2>

<p>If you have a number of repetitive tasks in your YAML pipeline, you can use the <code class="language-plaintext highlighter-rouge">each</code> expression to repeat them so you only have to specify them (and therefore maintain them) once. For example, if you need to deploy 3 applications to Azure App Service, you could do something like the following:</p>

<p>First you need a parameter defined to specify the list of items (in this case applications) to deploy:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">parameters</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">AppsToDeploy</span>
    <span class="na">type</span><span class="pi">:</span> <span class="s">object</span>
    <span class="na">values</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">App1"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">App2"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">App3"</span>
</code></pre></div></div>

<p>Then for your task (or for a set of tasks, or you could do it at the stage level) you specify the <code class="language-plaintext highlighter-rouge">each</code> expression:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="s">${{ each app in parameters.AppsToDeploy }}</span><span class="err">:</span>
  <span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">AzureRmWebAppDeployment@3</span>
    <span class="na">displayName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Deploy</span><span class="nv"> </span><span class="s">${{</span><span class="nv"> </span><span class="s">app</span><span class="nv"> </span><span class="s">}}</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">App</span><span class="nv"> </span><span class="s">Service"</span>
    <span class="na">inputs</span><span class="pi">:</span>
      <span class="na">azureSubscription</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(azureSubscription)'</span>
      <span class="na">WebAppName</span><span class="pi">:</span> <span class="s">${{ app }}</span>
      <span class="na">Package</span><span class="pi">:</span> <span class="s1">'</span><span class="s">/ProductBuild/${{</span><span class="nv"> </span><span class="s">app</span><span class="nv"> </span><span class="s">}}/${{</span><span class="nv"> </span><span class="s">app</span><span class="nv"> </span><span class="s">}}.zip'</span>
      <span class="na">SetParametersFile</span><span class="pi">:</span> <span class="s1">'</span><span class="s">/ProductBuild/${{</span><span class="nv"> </span><span class="s">app</span><span class="nv"> </span><span class="s">}}/${{</span><span class="nv"> </span><span class="s">app</span><span class="nv"> </span><span class="s">}}.SetParameters.xml'</span>
</code></pre></div></div>

<p>If you do this at the stage level, then when you select Run Pipeline in Azure DevOps, the pipeline will be initialised and these stages setup, which if you then select “Stages to run” under Advanced options, you’ll see them (and if you only wanted to deploy one of the applications, you could turn one or more of the stages off).</p>

<p>Having the input values as a parameter would mean that you would see (and could modify them) at the the time of triggering the pipeline. However we can hide them away by making the application deployment stages a separate template.</p>

<p>To do this, you would create a new YAML file called something like <code class="language-plaintext highlighter-rouge">ApplicationDeployment-Stages.yml</code>. This would need to have all the necessary parameters for the applicaiton deployment tasks (for example the <code class="language-plaintext highlighter-rouge">Environment</code> one, as well as the <code class="language-plaintext highlighter-rouge">AppsToDeploy</code> one above). But in this template you don’t then specify the <code class="language-plaintext highlighter-rouge">resources:</code>, <code class="language-plaintext highlighter-rouge">variables:</code> or <code class="language-plaintext highlighter-rouge">trigger:</code> sections. After the parameters, just specify the various stages, jobs and tasks that apply to App Deployment.</p>

<p>Then back in our <code class="language-plaintext highlighter-rouge">Deployment.yml</code> template, we can remove all of the application deployment tasks and instead reference the template, along with any input parameters we want to pass along, for example:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">stages</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">template</span><span class="pi">:</span> <span class="s">ApplicationDeployment-Stages.yml</span>
    <span class="na">parameters</span><span class="pi">:</span>
      <span class="na">Environment</span><span class="pi">:</span> <span class="s">${{ parameters.Environment }}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">AppsToDeploy</code> parameter no longer needs to be present on the <code class="language-plaintext highlighter-rouge">Deployment.yml</code> file, as within <code class="language-plaintext highlighter-rouge">ApplicationDeployment-Stages.yml</code> its default value is then always used.</p>

<p>By separating off groups of related stages into separate templates, you can build different combinations of the templates for different purposes. And this where I ended up creating a “super pipeline”, as once I had a template for infrastructure deployment, application deployment etc. I could then have a pipeline that referenced each of them to have a single pipeline to do a full deployment (such as when standing up an environment for testing). While still having separate definitions for when I just needed to do either infrastructure or application deployments.</p>

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

<p>There’s a lot to love about YAML pipelines. As I said in the beginning, having everything defined as code (including your pipelines), is powerful in itself because you get all the benefits of source control when maintaining those code artifacts, and (if you implement templates well) pipelines that are a lot less hassle to maintain. Being able to use the <code class="language-plaintext highlighter-rouge">if</code> and <code class="language-plaintext highlighter-rouge">each</code> expressions allows you to build pipelines that are more versatile and easier to execute. With the Classic Release pipelines running a subset of tasks would usually mean editing a release to enable/disable certain tasks. But with the YAML pipelines well defined parameters or stages allow you to implement this flexibility as part of the design. And although you can also do parallel tasks in the Classic Release pipelines its a lot less effort to implement via YAML.</p>

<p>That being said, there’s a few downsides as well. If you used the approach of having stages per environment in your Classic Release pipelines, then it was very easy to see which environment was deployed to which release, and you could take a kind of left to right deployment approach. Although there is the Environments pane within Azure DevOps that gets populated when you use a <code class="language-plaintext highlighter-rouge">Deployment</code> type job, its not as easy to see the current status of your environments against each other. One way in which I made this a little more transparent was to implement the <code class="language-plaintext highlighter-rouge">name:</code> setting in my pipelines, to customise the run name to include the environment and the version of the product being deployed. For example:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Application Deployment - ${{ parameters.Environment }} - 1.0.123 - Run $(Rev:r)</span>
</code></pre></div></div>

<p>The other downside is with the variables. As I noted earlier, the variable groups are very similar to how you’d configure variables on the release pipelines, but there’s no concept of creating a release that then stamps a version of those variables. In a way this is a positive as well, because maintaining variables on the Classic Release pipelines can be a bit of a nightmare when they’re copied all over the place, but if you make some changes to your variable groups for the next release, and those changes weren’t appropriate for the previous release then you’ve broken some backwards compatibility. There’s ways to workaround this but it requires thought and careful consideration.</p>

<p>Personally I found the positives outweighed the negatives and ended up with some easy to maintain pipelines that reduced the deployment time by multiple hours, and allowed me to build the “super pipeline” so I can one click deploy test environments. I also built an environment removal pipeline to have a one click tear down. Which means costs saved, by making it easy to run and remove environments only when required.</p>]]></content><author><name>Mark Wragg</name><email>mark@wragg.io</email></author><category term="powershell" /><category term="azure" /><category term="azuredevops" /><category term="pipelines" /><category term="YAML" /><summary type="html"><![CDATA[I recently migrated some Azure DevOps Classic Release deployment pipelines to YAML. There’s obvious benefits to storing your pipelines as code: they become an artifact in source control that can evolve and change as the code they build or deploy does, and you have the benefits of version history and maintaining the pipelines via pull requests. However I also found that I could use logic and expressions to make the pipelines more efficient and easier to maintain and that through templating could easily connect the pipelines together to form what I humorously dubbed the “super pipeline” (but then the name stuck). In this blog post I will explain the approach I took and the advantages/disadvantages I discovered.]]></summary></entry><entry><title type="html">Monitor and manage your Azure cloud costs with a little help from PowerShell</title><link href="https://wragg.io/monitor-and-manage-your-azure-cloud-costs-with-powershell/" rel="alternate" type="text/html" title="Monitor and manage your Azure cloud costs with a little help from PowerShell" /><published>2024-03-02T09:00:00+00:00</published><updated>2024-03-02T09:00:00+00:00</updated><id>https://wragg.io/monitor-and-manage-your-azure-cloud-costs-with-powershell</id><content type="html" xml:base="https://wragg.io/monitor-and-manage-your-azure-cloud-costs-with-powershell/"><![CDATA[<p><strong>Should cloud computing be illegal?</strong> <em>Probably not</em>, but it is incredibly easy to get started, equally difficult to stop, and before you know it you could be selling your grandmother just to afford one more month of that delicious compute. Hopefully your circumstances never get that dire, but I’ve seen plenty of companies entrench themselves into the highly addictive world of automated, scalable infrastructure, but then struggle to understand the often-astronomical monthly bill.</p>

<p>I found myself in this situation some time ago (not selling my grandmother.. but trying to understand high bills). After some wrangling, I managed to cut a client’s cloud bill by 60%, saving approximately £500,000 over 2 years, and that was despite <a href="https://news.microsoft.com/europe/2023/01/05/consistent-global-pricing-for-the-microsoft-cloud/">Microsoft increasing their prices by 11% last April</a>.</p>

<p>Rather than a 12-step program, I believe cloud addiction can be treated in just 6:</p>

<ol>
  <li>Understand your costs (use the built-in tools)</li>
  <li>Take ownership of the costs and perform regular reviews</li>
  <li>Configure budgets and billing alerts</li>
  <li>Automate the creation and destruction of your resources (and get good at it)</li>
  <li>Ensure the purpose/ownership of all resources is understood</li>
  <li>Purchase reservations</li>
</ol>

<p><a href="https://mpfe.uk/blog/2023-03-31-azure-cost-management/">I blogged about these topics last year on my company website</a>. However in this blog post (and for the purposes of <a href="https://www.azurespringclean.com/">Azure Spring Clean</a>) I will be focussing on #2: take ownership / perform regular reviews, by way of introducing a PowerShell module I’ve recently published to help do just that.</p>

<blockquote>
  <p>You may want to review your costs more frequently than monthly, but unless you have a very static environment, I think it’s a good minimum guideline as it’s how Azure usage is billed. Per step #3, it’s important to also have budgets and billing alerts configured (and with thresholds that are close to your typical costs) so that if your usage spikes unexpectedly during the month you are made aware and can intervene if appropriate.</p>
</blockquote>

<p>The AzCostTools module helps perform monthly reviews by:</p>

<ul>
  <li>Extracting your cost data via the <code class="language-plaintext highlighter-rouge">Get-AzConsumptionUsage</code> cmdlet for one or more months</li>
  <li>Comparing those months to the previous ones (or to a previous month of your choice by configuring an offset).</li>
  <li>Calculating the cost difference and change percentage</li>
  <li>Charting the daily cost, so you have a basic view of how costs fluctuate</li>
  <li>Retrieves any budgets you have configured and indicates where you’ve been within/over budget</li>
  <li>Identifying the most expensive services, by type</li>
</ul>

<p>Per step #1 (Understand your costs - use the built-in tools), AzCostTools is not intended as a replacement for the existing cost management tools that are in the Azure Portal. I strongly advocate their use, and there’s several default views that can give you quick and valuable insight. My personal favourite is the daily cost view, granulated by Resource Group so that I can drill into where the most expensive resources are. But if you’re responsible for more than one tenant and/or multiple subscriptions, you may find AzCostTools works well to automate the reporting of those costs (both individually and in total). Another weakness of the built-in Cost Management interface is it only seems to be able to show you the data from the last 12 months. If you wanted to compare your costs for the last few months to those same months from a year prior (which might be a reasonable thing to do if your costs fluctuate seasonally), it’s not very helpful. However the <code class="language-plaintext highlighter-rouge">Get-AzConsumptionUsage</code> cmdlet does return data from more than 12 months ago, and so AzCostTools can do this comparison for you.</p>

<h3 id="getting-started">Getting started</h3>

<p>That’s probably enough sales talk (it’s free btw..), here’s how to use it:</p>

<p>The module is in GitHub, so you can have a look at the source code here:</p>

<ul>
  <li><a href="https://github.com/markwragg/PowerShell-AzCostTools/">https://github.com/markwragg/PowerShell-AzCostTools/</a></li>
</ul>

<p>You can install the module from the <a href="https://www.powershellgallery.com/packages/AzCostTools/">PowerShell Gallery</a> so to get started, open a PowerShell window and execute:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Install-Module</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nx">AzCostTools</span><span class="w">
</span></code></pre></div></div>

<p>You’ll need to also ensure you have the <a href="https://learn.microsoft.com/en-us/powershell/azure/install-azure-powershell?view=azps-11.3.0">Azure PowerShell modules</a> (so that you have <code class="language-plaintext highlighter-rouge">Get-AzConsumptionUsage</code> and some other cmdlets that the module uses). Also, if you want to generate charts, you need to install a module called <a href="https://github.com/endowdly/PSparklines">PSparklines</a>. AzCostTools will work without it, but the charts are vaguely informative and fun.</p>

<p>To install these prerequisites, execute:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Install-Module</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nx">Az</span><span class="w">
</span><span class="n">Install-Module</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="nx">PSparklines</span><span class="w">
</span></code></pre></div></div>

<p>Finally, you of course need to make sure you’ve authenticated to Azure via the Az module, and for the tenant/s that you want to query costs. To login to Azure execute:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Connect-AzAccount</span><span class="w">
</span></code></pre></div></div>

<h3 id="subscription-costs">Subscription costs</h3>

<p>You are now ready to start querying your costs. So brace yourselves, this might hurt.</p>

<p>A good place to start is by executing <code class="language-plaintext highlighter-rouge">Get-SubscriptionCost</code>, which like all great cmdlets can be executed without any supplied parameters. This will retrieve the current month’s cost data for each subscription in your current context. Because you’ll probably want to dig into the data, I recommend returning it to a variable, which in the below example is <code class="language-plaintext highlighter-rouge">$Cost</code>.</p>

<blockquote>
  <p>Note, errors may be returned for any subscriptions where the cost data is inaccessible, e.g you are not authorised to access costs, or the subscription is of a type where costs are managed externally (such as a CSP).</p>
</blockquote>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$Cost</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-SubscriptionCost</span><span class="w">
</span></code></pre></div></div>

<blockquote>
  <p>Depending on the number of subscriptions and/or previous months of data you wish to query the <code class="language-plaintext highlighter-rouge">Get-SubscriptionCost</code> cmdlet can take a few minutes to run.
If you want to see the numbers roll in while also saving the result to a variable, you could instead do <code class="language-plaintext highlighter-rouge">Get-SubscriptionCost -OutVariable Cost</code>.</p>
</blockquote>

<p>The cmdlet returns the most pertinent fields by default and as table output, but there’s a lot more properties returned, which you can view via <code class="language-plaintext highlighter-rouge">$Cost | Format-List</code>.</p>

<p><img src="/content/images/2024/Get-SubscriptionCost.png" alt="Get-SubscriptionCost returns current costs for all subscriptions in the current context" /></p>

<p>The <code class="language-plaintext highlighter-rouge">Cost</code> property is the total cost for the specified billing period (in this case, the current month). If you want to view the costs for a different month, you can specify that by using the <code class="language-plaintext highlighter-rouge">-BillingMonth</code> parameter:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$Cost</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Get-SubscriptionCost</span><span class="w"> </span><span class="nt">-BillingMonth</span><span class="w"> </span><span class="nx">12/2023</span><span class="w">
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">Currency</code> property lets you know the currency you’re being billed in. If you’ve installed the <code class="language-plaintext highlighter-rouge">PSparklines</code> module, you get a sparkline chart showing how the costs fluctuate per day (fun, right?). The actual daily cost values are available in the <code class="language-plaintext highlighter-rouge">DailyCost</code> property. Then there’s a breakdown of your cost by service type, available under <code class="language-plaintext highlighter-rouge">CostPerService</code>, and if you have any budgets configured (and active) these are detailed in the <code class="language-plaintext highlighter-rouge">ActiveBudgets</code> property. The other properties perform some useful calculations on your costs, including the average daily cost, max and minimum daily cost, the dates that were most and least expensive and finally the service types that were most and least expensive (and how much they cost).</p>

<p>By default the cmdlet discards the raw data that’s returned from the <code class="language-plaintext highlighter-rouge">Get-AzConsumptionUsage</code> cmdlet, but if you want to keep it (for your own analysis), you can add the <code class="language-plaintext highlighter-rouge">-Raw</code> parameter. The raw data will then be available under a property named <code class="language-plaintext highlighter-rouge">Consumption_Raw</code>.</p>

<p>You can of course specify one or more subscriptions to query, just use the <code class="language-plaintext highlighter-rouge">-SubscriptionName</code> parameter, with an array of one or more subscription names. You can also customise how large the sparkline graphs are (they work up to a height of 10 lines) via the <code class="language-plaintext highlighter-rouge">-SparkLineSize</code> parameter.</p>

<p>Now this is all well and good, but for me cost management is only really worth doing if there’s an element of competition, and so I like to know if I’m doing better than I did last month. It keeps me focussed through the month on minimising costs (such as by keeping test environments deployed for as little time as possible). Obviously not every month is going to be cheaper (unless you’re going out of business I guess..) but you probably know the patterns of your own business (or hopefully you will do), and when there’s likely to be higher or lower demand (such as if you’re ramping up testing at the end of a development cycle, or ramping up infrastructure for peak seasonal demand such as Black Friday / Christmas). So you can come to expect when you should be driving costs down and when they’re likely to increase.</p>

<p>To enable this “competitive mode”, you can execute <code class="language-plaintext highlighter-rouge">Get-SubscriptionCost</code> with the <code class="language-plaintext highlighter-rouge">-ComparePrevious</code> parameter. You can combine this with <code class="language-plaintext highlighter-rouge">-BillingMonth</code> obviously to specify the month you want to look at costs for, but it will also then get the previous months costs for comparison (and do some of that comparison for you). For example (note in the below I’ve also made the sparkline graphs bigger):</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-SubscriptionCost</span><span class="w"> </span><span class="nt">-SubscriptionName</span><span class="w"> </span><span class="s1">'AdventureWorks Cycles'</span><span class="w"> </span><span class="nt">-ComparePrevious</span><span class="w"> </span><span class="nt">-SparkLineSize</span><span class="w"> </span><span class="nx">3</span><span class="w">
</span></code></pre></div></div>

<p><img src="/content/images/2024/Get-SubscriptionCost-ComparePrev.png" alt="Get-SubscriptionCost returns costs for a specified subscription and compares them to the previous month with sparkline charts that are 3 rows in height" /></p>

<p>Per the screenshot above, there is again a table view returned by default, with cost, the previous month’s cost and sparkline daily cost graphs for both. If you once again do <code class="language-plaintext highlighter-rouge">$Cost | Format-List</code> you’ll see there’s more properties available. These include all of the same properties that were returned without using <code class="language-plaintext highlighter-rouge">-ComparePrev</code>, and then properties that represent the same data for the previous month (all prefixed with <code class="language-plaintext highlighter-rouge">Prev</code>), but where you get additional value by using this parameter is that you also get a <code class="language-plaintext highlighter-rouge">CostChange</code> property, which is the calculated difference between the previous month and current one, that change represented as a percentage, and then the cost change also broken down per day under a property named <code class="language-plaintext highlighter-rouge">DailyCostChange</code>. When I’m reporting costs I typically show the current cost for the month I’m reporting on, how much more or less that is than last month and that change as a percentage, which I think gives a very quick and easy to understand view of whether your costs are going up or down and by how much. Once again if you want the raw data for both months, using <code class="language-plaintext highlighter-rouge">-Raw</code> ensures these are included.</p>

<p>If you want to look at costs for more than a single month, you could of course make several repeated calls to <code class="language-plaintext highlighter-rouge">Get-SubscriptionCost</code> with different billing periods, but it has a parameter that is more convenient. If you use <code class="language-plaintext highlighter-rouge">-PreviousMonths</code> it will return cost data for however many previous months you specify. There’s a slight performance advantage to doing this if you’re also using <code class="language-plaintext highlighter-rouge">-ComparePrevious</code> because the tool will know it already retrieved the previous months cost and will just reuse it. For example:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-SubscriptionCost</span><span class="w"> </span><span class="nt">-PreviousMonths</span><span class="w"> </span><span class="nx">5</span><span class="w"> </span><span class="nt">-ComparePrevious</span><span class="w">
</span></code></pre></div></div>

<p><img src="/content/images/2024/Cost-MultipleSubscription-PrevMonths-ComparePrev.png" alt="Get-SubscriptionCost returns current costs for all subscriptions in the current context and the previous 5 months of costs" /></p>

<p>The default behaviour of comparing your costs to the previous month might not be the right choice for you. If you work somewhere with specific patterns of work (as talked about earlier, development cycles of a fixed cadence or infrastructure that is ramped up seasonally) it might be more insightful to compare to the same month from the previous period (be that last quarter, or last year etc.). To modify which month is used for comparison when using <code class="language-plaintext highlighter-rouge">-ComparePrevious</code> you can specify <code class="language-plaintext highlighter-rouge">-ComparePreviousOffset</code>.  This will compare each month of cost data returned to X month/s prior as specified. You can continue to combine this with the other parameters (for example the <code class="language-plaintext highlighter-rouge">-PreviousMonths</code> one) so for example, if you wanted to compare costs for the last 6 months against the same 6 months from the year prior, you could execute:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-SubscriptionCost</span><span class="w"> </span><span class="nt">-PreviousMonths</span><span class="w"> </span><span class="nx">6</span><span class="w"> </span><span class="nt">-ComparePrevious</span><span class="w"> </span><span class="nt">-ComparePreviousOffset</span><span class="w"> </span><span class="nx">12</span><span class="w">
</span></code></pre></div></div>
<p><img src="/content/images/2024/Cost-MultipleSubscription-PrevMonths-ComparePrev-Offset12.png" alt="Get-SubscriptionCost returns current costs for all subscriptions in the current context and the previous 6 months of cost, comparing each to the equivalent month 12 months prior" /></p>

<h3 id="cost-analysis">Cost analysis</h3>

<p>I expect most people (assuming anyone has made it this far) will want to take the output of this tool and do something further with it. Perhaps bring it into Excel and generate some charts (I certainly do). But I wanted to see what other useful ways I could implement <code class="language-plaintext highlighter-rouge">PSparklines</code>, and while we have all this data in a console window it’s potentially very quick and easy to generate some further insight there too. So without any further rambling, let me introduce <code class="language-plaintext highlighter-rouge">Show-CostAnalysis</code>.</p>

<p>Having retrieved a set of cost data for one or more subscriptions, you can pipe that data to <code class="language-plaintext highlighter-rouge">Show-CostAnalysis</code> to generate some charts and tables analysing the costs:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$Cost</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Show-CostAnalysis</span><span class="w">
</span></code></pre></div></div>

<p>This is best experienced with <code class="language-plaintext highlighter-rouge">PSparklines</code> installed of course, but again it works without it. It generates a daily cost chart with a slightly more useful default height, but if your cost data returned one or more budgets, we convert one of those into a daily average and then use that to colour the daily cost chart to show where you were over budget (in red) and under budget (in green). If your subscription doesn’t have a budget, the chart is white. It also shows a little summary for each Subscription detailing the budget used, the daily budget calculation it generated, your peak daily cost, the most/least expensive date and total cost. If there’s a budget configured, the cost is in green if you’re within that overall budget and red if it’s over.</p>

<p>The tool then breaks down the costs by service type, creating a chart with different colours for each type, and again summarising the most/least expensive services and their cost. It will do this for up to 15 service types (after which we sort of ran out of console colours).</p>

<p>If more than one subscription is in the cost data, the cmdlet will end with a total of cost for all subscriptions and another chart showing most to least expensive. This is a little slow to produce (its largely down to the sparkline charts which take a few seconds each to generate):</p>

<p><img src="/content/images/2024/Show-CostAnalysis.gif" alt="Show-CostAnalysis generates charts and tables for a set of returned cost data" /></p>

<p>There’s a couple of parameters on <code class="language-plaintext highlighter-rouge">Show-CostAnalysis</code>: you can again customise the size of the charts returned by specifying <code class="language-plaintext highlighter-rouge">-SparkLineSize</code>. The default is 3. But perhaps more usefully, you can also specify <code class="language-plaintext highlighter-rouge">-ConvertToCurrency</code> with a 3-letter currency code if you’d like the cost values returned to be converted to a different currency. Sometimes Azure costs are billed in a currency that is not your own and it may be more informative to view them in your local currency. For example:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$Cost</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Show-CostAnalysis</span><span class="w"> </span><span class="nt">-ConvertToCurrency</span><span class="w"> </span><span class="nx">GBP</span><span class="w">
</span></code></pre></div></div>

<blockquote>
  <p>Note that this uses a free/open API for currency conversion that only refreshes the exchange rates once a day. I thought this functionality was so useful I spun it off into its own module which you can read about here:</p>

  <ul>
    <li><a href="https://wragg.io/Perform-currency-conversion-with-PowerShell/">https://wragg.io/Perform-currency-conversion-with-PowerShell/</a></li>
  </ul>

  <p>AzCostTools doesn’t need this separate module installed, it has the functionality built in.</p>
</blockquote>

<p>If you used <code class="language-plaintext highlighter-rouge">-ComparePrevious</code> when executing <code class="language-plaintext highlighter-rouge">Get-SubscriptionCost</code> you can also specify <code class="language-plaintext highlighter-rouge">-ComparePrevious</code> for <code class="language-plaintext highlighter-rouge">Show-CostAnalysis</code> to generate further tables and charts for the previous cost data. This might be most useful when using <code class="language-plaintext highlighter-rouge">-ComparePreviousOffset</code> so that you can see the charts side by side of the current and previous costs. For example:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$Cost</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Show-CostAnalysis</span><span class="w"> </span><span class="nt">-ComparePrevious</span><span class="w">
</span></code></pre></div></div>
<p><img src="/content/images/2024/Show-CostAnalysis-ComparePrev.png" alt="Show-CostAnalysis generates charts and tables for a set of returned cost data and shows charts for the previous cost data" /></p>

<p>Bear in mind the output of <code class="language-plaintext highlighter-rouge">Show-CostAnalysis</code> is not objects, so there’s nothing here you can easily export or pipe into another cmdlet, but most of the calculations it uses were already performed via the <code class="language-plaintext highlighter-rouge">Get-SubscriptionCost</code> cmdlet, so the data is there.</p>

<h3 id="storage-costs">Storage costs</h3>

<p>I plan to continue to expand AzCostTools with other useful cost-related functionality, hence the slightly generic name. One such expansion is to start to dig into the cost of Storage. Returning to my original tongue-in-cheek premise, I think it’s not outrageous to think of cloud storage as like the gateway drug of cloud computing, because it is incredibly cheap. But sometimes cloud costs can be death by a thousand cuts, and over time you can end up with hundreds of small storage accounts littered through your subscriptions that individually aren’t expensive, but still represent a waste that cumulatively can become significant.</p>

<p>To help you understand your storage costs specifically, you can execute:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-StorageCost</span><span class="w">
</span></code></pre></div></div>

<p>It works in a very similar to way to <code class="language-plaintext highlighter-rouge">Get-SubscriptionCost</code> (mostly because I started by just copy/pasting that function). By default it will query all available storage accounts in your current context, or you can specify one or more storage accounts via the <code class="language-plaintext highlighter-rouge">-AccountName</code> parameter. And then it has the other parameters you’ll find familiar: <code class="language-plaintext highlighter-rouge">-BillingMonth</code>, <code class="language-plaintext highlighter-rouge">-ComparePrevious</code>, <code class="language-plaintext highlighter-rouge">-ComparePreviousOffset</code>, <code class="language-plaintext highlighter-rouge">-PreviousMonths</code> and <code class="language-plaintext highlighter-rouge">-SparkLineSize</code>. It’s particularly useful for seeing which storage accounts are the most expensive, and which storage accounts are experiencing active change vs being static.</p>

<h3 id="in-summary">In summary</h3>

<p>In summary, Drugs = bad. Cloud = good. Reducing cloud costs = great. If I were dispensing any specific advice, I would say don’t be afraid to take responsibility for managing your costs, particularly if no one else does. If not for any other reason, saving companies money looks great on your CV. Of course work within the bounds of your organisation, and with suitable permission to make changes.</p>

<p>A final caveat, this AzCostTools module was slightly cobbled together, so please let me know if you experience any bugs, and please don’t sue me if you get any erroneous results. But more than anything, please let me know if you have any ideas for improvements. You can log those here:</p>

<ul>
  <li><a href="https://github.com/markwragg/PowerShell-AzCostTools/issues">https://github.com/markwragg/PowerShell-AzCostTools/issues</a></li>
</ul>

<p>Thanks for taking the time to read this blog post, I hope you found it useful. Enjoy more great content from Azure Spring Clean 2024 via the website here:</p>

<ul>
  <li><a href="https://www.azurespringclean.com/">https://www.azurespringclean.com/</a></li>
</ul>

<p>or by following <a href="https://www.linkedin.com/search/results/all/?keywords=%23AzureSpringClean&amp;origin=GLOBAL_SEARCH_HEADER&amp;sid=_pt">#AzureSpringClean on LinkedIn</a> or the <a href="https://twitter.com/search?q=%23AzureSpringClean&amp;src=typed_query&amp;f=live">website formerly known as Twitter</a>.</p>]]></content><author><name>Mark Wragg</name><email>mark@wragg.io</email></author><category term="powershell" /><category term="azure" /><category term="azurespringclean" /><category term="module" /><category term="costmanagement" /><category term="cloudcomputing" /><summary type="html"><![CDATA[Should cloud computing be illegal? Probably not, but it is incredibly easy to get started, equally difficult to stop, and before you know it you could be selling your grandmother just to afford one more month of that delicious compute. Hopefully your circumstances never get that dire, but I’ve seen plenty of companies entrench themselves into the highly addictive world of automated, scalable infrastructure, but then struggle to understand the often-astronomical monthly bill.]]></summary></entry><entry><title type="html">Perform Currency Conversions with PowerShell</title><link href="https://wragg.io/Perform-currency-conversion-with-PowerShell/" rel="alternate" type="text/html" title="Perform Currency Conversions with PowerShell" /><published>2024-02-08T13:30:00+00:00</published><updated>2024-02-08T13:30:00+00:00</updated><id>https://wragg.io/Perform-currency-conversion-with-PowerShell</id><content type="html" xml:base="https://wragg.io/Perform-currency-conversion-with-PowerShell/"><![CDATA[<p>I’ve recently been working on a <a href="https://wragg.io/monitor-and-manage-your-azure-cloud-costs-with-powershell/">PowerShell module for exploring Azure costs</a> and while doing so added some functionality to allow the costs to be converted between different currencies. It occurred to me that this functionality would be useful as a module of its own, and when I searched around I didn’t find too many recent examples for the same. As such I’ve now developed and published a module in the PowerShell Gallery and on GitHub called <a href="https://github.com/markwragg/PowerShell-CurrencyConverter/tree/main">CurrencyConverter</a>.</p>

<blockquote>
  <p>https://github.com/markwragg/PowerShell-CurrencyConverter/</p>
</blockquote>

<p>The module is essentially a PowerShell wrapper for the currency conversion API that is provided by <a href="https://www.exchangerate-api.com/">ExchangeRate-API</a>. They provide an open API, which requires no registration or API key to use. This was helpful for my other module as I didn’t want the user to have to take a dependency on registering for a service for it to work. By default the Currency Converter module uses the open API, but if you have registered and wish to use an API key, you can provide one via the <code class="language-plaintext highlighter-rouge">-APIKey</code> parameters of <code class="language-plaintext highlighter-rouge">Convert-Currency</code> and <code class="language-plaintext highlighter-rouge">Get-ExchangeRate</code>. Note that with the open API, the rates refresh once every 24 hours. This is also true for the free tier of the registered API, but you are less likely to be rate limited. If you take one of their paid plans then the currency conversion rates refresh more frequently.</p>

<p>The Currency Converter module is published in the <a href="https://www.powershellgallery.com/packages/CurrencyConverter/">PowerShell Gallery</a>, so can be installed by running:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Install-Module</span><span class="w"> </span><span class="nx">CurrencyConverter</span><span class="w">
</span></code></pre></div></div>

<h2 id="usage">Usage</h2>

<p>This module provides the following cmdlets:</p>

<table>
  <thead>
    <tr>
      <th>Cmdlet</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Convert-Currency</td>
      <td>Converts a decimal value between two specified currencies.</td>
    </tr>
    <tr>
      <td>Format-Currency</td>
      <td>Formats a value as a string with the currency symbol for a specified country.</td>
    </tr>
    <tr>
      <td>Get-Currency</td>
      <td>Returns the list of supported currency codes along with their currency name and country code.</td>
    </tr>
    <tr>
      <td>Get-ExchangeRate</td>
      <td>Returns all exchange rates for a specified currency, or a specified exchange rate between two currencies.</td>
    </tr>
  </tbody>
</table>

<h3 id="convert-currency">Convert-Currency</h3>

<p>To perform a simple currency conversion, execute:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Convert-Currency</span><span class="w"> </span><span class="nt">-Value</span><span class="w"> </span><span class="nx">100</span><span class="w"> </span><span class="nt">-From</span><span class="w"> </span><span class="nx">USD</span><span class="w"> </span><span class="nt">-To</span><span class="w"> </span><span class="nx">GBP</span><span class="w">
</span></code></pre></div></div>

<p>To use the registered API, provide your key via the <code class="language-plaintext highlighter-rouge">-APIKey</code> parameter:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Convert-Currency</span><span class="w"> </span><span class="nt">-Value</span><span class="w"> </span><span class="nx">100</span><span class="w"> </span><span class="nt">-From</span><span class="w"> </span><span class="nx">USD</span><span class="w"> </span><span class="nt">-To</span><span class="w"> </span><span class="nx">GBP</span><span class="w"> </span><span class="nt">-ApiKey</span><span class="w"> </span><span class="nx">yourapikeystringhere</span><span class="w">
</span></code></pre></div></div>

<blockquote>
  <p>See here for a list of supported currencies: https://www.exchangerate-api.com/docs/supported-currencies. Alternatively you can use the <code class="language-plaintext highlighter-rouge">Get-Currency</code> cmdlet.</p>
</blockquote>

<p>After querying the API, the response is saved within the module directory, as a JSON file. If you query the API for the same currency again, the file will be read. It contains a date field for when the values next refresh. If the current date is earlier than this value, then the file is used. If it is after this date, the API is queried again and the file updated. As a result this module will only query the API once a day, per currency.</p>

<p>You can alternatively provide the value to be converted via the pipeline:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="mi">100</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Convert-Currency</span><span class="w"> </span><span class="nt">-From</span><span class="w"> </span><span class="nx">EUR</span><span class="w"> </span><span class="nt">-To</span><span class="w"> </span><span class="nx">CAD</span><span class="w">
</span></code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>79.201100
</code></pre></div></div>

<p>And this means that you can pipe multiple values in to perform a series of conversions:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="mi">100</span><span class="p">,</span><span class="mi">200</span><span class="p">,</span><span class="mi">250</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Convert-Currency</span><span class="w"> </span><span class="nt">-From</span><span class="w"> </span><span class="nx">USD</span><span class="w"> </span><span class="nt">-To</span><span class="w"> </span><span class="nx">JPY</span><span class="w">
</span></code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>14805.315400
29610.630800
37013.288500
</code></pre></div></div>

<h3 id="format-currency">Format-Currency</h3>

<p>The value returned by <code class="language-plaintext highlighter-rouge">Convert-Currency</code> is always a decimal, to ensure we keep the result as precise as possible, and in case you want to do further calculations. If you want to present the result (such as in a report) as a currency value, then you can use the <code class="language-plaintext highlighter-rouge">Format-Currency</code> cmdlet. This returns a string value, rounded to two decimal places (by default) and with the corresponding currency symbol added. Obviously you can’t then treat this result as a decimal value, so you probably want to ensure this cmdlet is used last in your pipeline.</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="mi">100</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Format-Currency</span><span class="w"> </span><span class="nt">-Currency</span><span class="w"> </span><span class="nx">USD</span><span class="w">
</span></code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$100.00
</code></pre></div></div>

<p>You can specify a different number of decimal places to round to, via the <code class="language-plaintext highlighter-rouge">-Decimals</code> parameter:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Format-Currency</span><span class="w"> </span><span class="nt">-Value</span><span class="w"> </span><span class="nx">123.4567890</span><span class="w"> </span><span class="nt">-Currency</span><span class="w"> </span><span class="nx">GBP</span><span class="w"> </span><span class="nt">-Decimals</span><span class="w"> </span><span class="nx">4</span><span class="w">
</span></code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>£123.4568
</code></pre></div></div>

<p>You can also perform a currency conversion at the same time as performing the format:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Format-Currency</span><span class="w"> </span><span class="nt">-Value</span><span class="w"> </span><span class="nx">100</span><span class="w"> </span><span class="nt">-Currency</span><span class="w"> </span><span class="nx">GBP</span><span class="w"> </span><span class="nt">-ConvertTo</span><span class="w"> </span><span class="nx">USD</span><span class="w">
</span></code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$125.85
</code></pre></div></div>

<p>For some currencies it may be convention to put the currency symbol after the value. This can be achieved by specifying the <code class="language-plaintext highlighter-rouge">-SymbolAtEnd</code> switch parameter:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Format-Currency</span><span class="w"> </span><span class="nt">-Value</span><span class="w"> </span><span class="nx">100</span><span class="w"> </span><span class="nt">-Currency</span><span class="w"> </span><span class="nx">GBP</span><span class="w"> </span><span class="nt">-SymbolAtEnd</span><span class="w">
</span></code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>125.85£
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Format-Currency</code> also supports pipeline input, so you can simply pipe the result of <code class="language-plaintext highlighter-rouge">Convert-Currency</code> to <code class="language-plaintext highlighter-rouge">Format-Currency</code>:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Convert-Currency</span><span class="w"> </span><span class="nt">-Value</span><span class="w"> </span><span class="nx">100</span><span class="w"> </span><span class="nt">-From</span><span class="w"> </span><span class="nx">USD</span><span class="w"> </span><span class="nt">-To</span><span class="w"> </span><span class="nx">EUR</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Format-Currency</span><span class="w"> </span><span class="nt">-Currency</span><span class="w"> </span><span class="nx">EUR</span><span class="w">
</span></code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>€93.04
</code></pre></div></div>

<h3 id="get-currency">Get-Currency</h3>

<p>The currency code inputs for the previous two cmdlets have a <code class="language-plaintext highlighter-rouge">validateset</code> attribute, so if you try to use an unsupported code an error is returned with the list of codes supported. If you’re not sure what the code is for a particular currency or country, you can explore them with the <code class="language-plaintext highlighter-rouge">Get-Currency</code> cmdlet. Executing this on its own returns all of the supported currency codes along with their currency name and country:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-Currency</span><span class="w">
</span></code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Code Name                          Country
---- ----                          -------
AED  UAE Dirham                    United Arab Emirates
AFN  Afghan Afghani                Afghanistan
ALL  Albanian Lek                  Albania
AMD  Armenian Dram                 Armenia
ANG  Netherlands Antillian Guilder Netherlands Antilles
AOA  Angolan Kwanza                Angola
ARS  Argentine Peso                Argentina
AUD  Australian Dollar             Australia
AWG  Aruban Florin                 Aruba
AZN  Azerbaijani Manat             Azerbaijan
BAM  Bosnia and Herzegovina Mark   Bosnia and Herzegovina
...
</code></pre></div></div>

<blockquote>
  <p>Note that for all European Countries that use the Euro, “European Union” is listed as the country. I appreciate this isn’t entirely accurate and possibly politically controversial :).</p>
</blockquote>

<p>With <code class="language-plaintext highlighter-rouge">Get-Currency</code> you can also specify a specific code (this requires an exact match):</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-Currency</span><span class="w"> </span><span class="nt">-Currency</span><span class="w"> </span><span class="nx">GBP</span><span class="w">
</span></code></pre></div></div>

<p>Or you can filter by currency or country name (this will return results for partial matches):</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-Currency</span><span class="w"> </span><span class="nt">-Name</span><span class="w"> </span><span class="s1">'Pound'</span><span class="w">
</span></code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Code Name                   Country
---- ----                   -------
EGP  Egyptian Pound         Egypt
FKP  Falkland Islands Pound Falkland Islands
GBP  Pound Sterling         United Kingdom
GGP  Guernsey Pound         Guernsey
GIP  Gibraltar Pound        Gibraltar
IMP  Manx Pound             Isle of Man
JEP  Jersey Pound           Jersey
LBP  Lebanese Pound         Lebanon
SDG  Sudanese Pound         Sudan
SHP  Saint Helena Pound     Saint Helena
SSP  South Sudanese Pound   South Sudan
SYP  Syrian Pound           Syria
</code></pre></div></div>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-Currency</span><span class="w"> </span><span class="nt">-Country</span><span class="w"> </span><span class="s1">'United'</span><span class="w">
</span></code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Code Name                 Country
---- ----                 -------
AED  UAE Dirham           United Arab Emirates
GBP  Pound Sterling       United Kingdom
USD  United States Dollar United States
</code></pre></div></div>

<h3 id="get-exchangerate">Get-ExchangeRate</h3>

<p>Finally there is the <code class="language-plaintext highlighter-rouge">Get-ExchangeRate</code> cmdlet. You can use this to return the full result from the API as a PowerShell object, which includes the fields that show when the next update will be available and all of the supported rates for a specified currency:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-ExchangeRate</span><span class="w"> </span><span class="nt">-Currency</span><span class="w"> </span><span class="nx">USD</span><span class="w">
</span></code></pre></div></div>

<p>To use the registered API, provide your key via the <code class="language-plaintext highlighter-rouge">-APIKey</code> parameter:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-ExchangeRate</span><span class="w"> </span><span class="nt">-Currency</span><span class="w"> </span><span class="nx">GBP</span><span class="w"> </span><span class="nt">-ApiKey</span><span class="w"> </span><span class="nx">yourapikeystringhere</span><span class="w">
</span></code></pre></div></div>

<p>When an API key is provided, the v6 API is invoked. This returns the rates as a property called ‘conversion_rates’. The Open API returns the property as ‘rates’.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>result                : success
provider              : https://www.exchangerate-api.com
documentation         : https://www.exchangerate-api.com/docs/free
terms_of_use          : https://www.exchangerate-api.com/terms
time_last_update_unix : 1707350551
time_last_update_utc  : Thu, 08 Feb 2024 00:02:31 +0000
time_next_update_unix : 1707437661
time_next_update_utc  : Fri, 09 Feb 2024 00:14:21 +0000
time_eol_unix         : 0
base_code             : USD
rates                 : @{USD=1; AED=3.6725; AFN=73.755496; ALL=96.818553; AMD=405.868756; ANG=1.79; AOA=839.476403; ARS=830.15;
                        AUD=1.533245; AWG=1.79; AZN=1.699659; BAM=1.81591; BBD=2; BDT=109.768633; BGN=1.81581; BHD=0.376;
                        BIF=2850.388516; BMD=1; BND=1.343391; BOB=6.934197; BRL=4.962119; BSD=1; BTN=83.001945; BWP=13.679849;
                        BYN=3.245101; BZD=2; CAD=1.346345; CDF=2728.475301; CHF=0.873457; CLP=947.575519; CNY=7.19952;
                        COP=3954.758146; CRC=517.23293; CUP=24; CVE=102.376655; CZK=23.176359; DJF=177.721; DKK=6.92457;
                        DOP=58.566572; DZD=134.658699; EGP=30.904297; ERN=15; ETB=56.700184; EUR=0.928449; FJD=2.247047;
                        FKP=0.792018; FOK=6.92461; GBP=0.792011; GEL=2.659816; GGP=0.792018; GHS=12.422748; GIP=0.792018;
                        GMD=66.562902; GNF=8588.188018; GTQ=7.814905; GYD=209.536717; HKD=7.820255; HNL=24.66876; HRK=6.995483;
                        HTG=131.989675; HUF=360.172059; IDR=15650.579046; ILS=3.652044; IMP=0.792018; INR=83.001968;
                        IQD=1310.842088; IRR=42080.433475; ISK=137.663905; JEP=0.792018; JMD=156.245808; JOD=0.709;
                        JPY=148.053154; KES=160.352692; KGS=89.414602; KHR=4091.328437; KID=1.533294; KMF=456.772434;
                        KRW=1326.84486; KWD=0.307938; KYD=0.833333; KZT=452.274884; LAK=20736.087575; LBP=15000; LKR=312.628935;
                        LRD=192.145847; LSL=18.897808; LYD=4.844127; MAD=10.061229; MDL=17.827961; MGA=4536.616958;
                        MKD=57.312368; MMK=2099.744651; MNT=3412.288092; MOP=8.054862; MRU=39.584855; MUR=45.384067;
                        MVR=15.453447; MWK=1689.264206; MXN=17.05608; MYR=4.761416; MZN=63.917225; NAD=18.897808;
                        NGN=1407.528785; NIO=36.672231; NOK=10.577722; NPR=132.803112; NZD=1.636807; OMR=0.384497; PAB=1;
                        PEN=3.861833; PGK=3.749532; PHP=56.01365; PKR=279.042023; PLN=4.033029; PYG=7296.21721; QAR=3.64;
                        RON=4.620779; RSD=108.801814; RUB=91.346239; RWF=1284.744312; SAR=3.75; SBD=8.488406; SCR=13.291546;
                        SDG=544.79719; SEK=10.475851; SGD=1.343391; SHP=0.792018; SLE=22.587694; SLL=22587.694431;
                        SOS=571.785619; SRD=36.611265; SSP=1102.449099; STN=22.747273; SYP=12950.775091; SZL=18.897808;
                        THB=35.595367; TJS=10.947348; TMT=3.500938; TND=3.129049; TOP=2.371485; TRY=30.62091; TTD=6.777386;
                        TVD=1.533294; TWD=31.341578; TZS=2541.212285; UAH=37.600041; UGX=3817.518963; UYU=39.148736;
                        UZS=12479.514661; VES=36.2375; VND=24413.241561; VUV=120.508933; WST=2.745778; XAF=609.029912; XCD=2.7;
                        XDR=0.754059; XOF=609.029912; XPF=110.795003; YER=250.331823; ZAR=18.897825; ZMW=27.029301;
                        ZWL=10811.587531}
</code></pre></div></div>

<p>If you want to return just the exchange rates, you can execute <code class="language-plaintext highlighter-rouge">Get-ExchangeRate</code> and specify the <code class="language-plaintext highlighter-rouge">-Rates</code> parameter:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-ExchangeRate</span><span class="w"> </span><span class="nt">-Currency</span><span class="w"> </span><span class="nx">GBP</span><span class="w"> </span><span class="nt">-Rates</span><span class="w">
</span></code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GBP : 1
AED : 4.636888
AFN : 92.99538
ALL : 122.070196
AMD : 510.608906
ANG : 2.260049
AOA : 1067.044213
ARS : 1048.144979
AUD : 1.935836
AWG : 2.260049
AZN : 2.145246
BAM : 2.292681
BBD : 2.525194
BDT : 138.549806
...
</code></pre></div></div>

<p>If you just want to return a specific exchange rate, you can execute <code class="language-plaintext highlighter-rouge">Get-ExchangeRate</code> and specify <code class="language-plaintext highlighter-rouge">-From</code> and <code class="language-plaintext highlighter-rouge">-To</code>. This will get you the rate of exchange, instead of performing a conversion:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Get-ExchangeRate</span><span class="w"> </span><span class="nt">-From</span><span class="w"> </span><span class="nx">GBP</span><span class="w"> </span><span class="nt">-To</span><span class="w"> </span><span class="nx">USD</span><span class="w">
</span></code></pre></div></div>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1.262609
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Get-ExchangeRate</code> also supports pipeline input, so if you want to get the exchange rate details for a set of currencies you can execute:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s1">'USD'</span><span class="p">,</span><span class="s1">'GBP'</span><span class="p">,</span><span class="s1">'EUR'</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Get-ExchangeRate</span><span class="w">
</span></code></pre></div></div>

<p>Finally, if for some reason you wanted to query the API for every currency (resulting in the module caching every currencies exchange rate to disk), you could do the following (although beware this might trigger rate limiting for the API, as it will make 161 queries in quick succession):</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="n">Get-Currency</span><span class="p">)</span><span class="o">.</span><span class="nf">Code</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Get-ExchangeRate</span><span class="w">
</span></code></pre></div></div>

<p>I hope you found this useful. If you experience any issues or can think of improvements that could be made, please see here for how to request or make a contribution:</p>

<ul>
  <li>https://github.com/markwragg/PowerShell-CurrencyConverter/blob/main/CONTRIBUTING.md</li>
</ul>]]></content><author><name>Mark Wragg</name><email>mark@wragg.io</email></author><category term="powershell" /><category term="api" /><category term="module" /><category term="currency" /><summary type="html"><![CDATA[I’ve recently been working on a PowerShell module for exploring Azure costs and while doing so added some functionality to allow the costs to be converted between different currencies. It occurred to me that this functionality would be useful as a module of its own, and when I searched around I didn’t find too many recent examples for the same. As such I’ve now developed and published a module in the PowerShell Gallery and on GitHub called CurrencyConverter.]]></summary></entry><entry><title type="html">Charting Azure Application Gateway Logs by Response Code</title><link href="https://wragg.io/charting-azure-application-gateway-access-logs-by-response-code/" rel="alternate" type="text/html" title="Charting Azure Application Gateway Logs by Response Code" /><published>2023-07-02T12:00:00+00:00</published><updated>2023-07-02T12:00:00+00:00</updated><id>https://wragg.io/charting-azure-application-gateway-access-logs-by-response-code</id><content type="html" xml:base="https://wragg.io/charting-azure-application-gateway-access-logs-by-response-code/"><![CDATA[<p>I recently implemented a new Azure Application Gateway and was interested to chart the connections through it per hour, grouped by HTTP Response Code. 
My initial attempt was this query:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">AzureDiagnostics</span>
<span class="o">|</span> <span class="k">where</span> <span class="n">OperationName</span> <span class="o">==</span> <span class="s1">'ApplicationGatewayAccess'</span>
<span class="o">|</span> <span class="n">summarize</span> <span class="k">count</span><span class="p">()</span> <span class="k">by</span> <span class="n">bin</span><span class="p">(</span><span class="n">TimeGenerated</span><span class="p">,</span><span class="mi">1</span><span class="n">h</span><span class="p">),</span> <span class="n">httpStatus_d</span>
<span class="o">|</span> <span class="n">render</span> <span class="n">columnchart</span>
</code></pre></div></div>

<p>But this didn’t generate the chart I expected, instead creating one that looked this this (with two values, <code class="language-plaintext highlighter-rouge">count_</code> and <code class="language-plaintext highlighter-rouge">httpStatus_d</code>):</p>

<p><img src="/content/images/2023/07/loganalytics-httpstatus1.png" alt="log analytics azure app gateway chart grouped by http status not working" /></p>

<p>I eventually realised the issue was due to the <code class="language-plaintext highlighter-rouge">httpStatus_d</code> field being a numeric value, which seems to confuse the <code class="language-plaintext highlighter-rouge">summarize</code> operator. To work around this you need to use the <code class="language-plaintext highlighter-rouge">tostring()</code> function to convert it to a string:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">AzureDiagnostics</span>
<span class="o">|</span> <span class="k">where</span> <span class="n">OperationName</span> <span class="o">==</span> <span class="s1">'ApplicationGatewayAccess'</span>
<span class="o">|</span> <span class="n">summarize</span> <span class="k">count</span><span class="p">()</span> <span class="k">by</span> <span class="n">bin</span><span class="p">(</span><span class="n">TimeGenerated</span><span class="p">,</span><span class="mi">1</span><span class="n">h</span><span class="p">),</span> <span class="n">tostring</span><span class="p">(</span><span class="n">httpStatus_d</span><span class="p">)</span>
<span class="o">|</span> <span class="n">render</span> <span class="n">columnchart</span>
</code></pre></div></div>

<p><img src="/content/images/2023/07/loganalytics-httpstatus2.png" alt="log analytics azure app gateway chart grouped by http status working but displaying decimals" /></p>

<p>This generated the chart I wanted, but because <code class="language-plaintext highlighter-rouge">httpstatus_d</code> has a data type of <code class="language-plaintext highlighter-rouge">real</code> it treats it as a decimal value and the HTTP status codes (once converted to strings) subsequently have a <code class="language-plaintext highlighter-rouge">.0</code> at the end.</p>

<p>If you want to get it to convert <code class="language-plaintext highlighter-rouge">httpstatus_d</code> as integers, you can use <code class="language-plaintext highlighter-rouge">toint()</code> on them first, as follows:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">AzureDiagnostics</span>
<span class="o">|</span> <span class="k">where</span> <span class="n">OperationName</span> <span class="o">==</span> <span class="s1">'ApplicationGatewayAccess'</span>
<span class="o">|</span> <span class="n">summarize</span> <span class="k">count</span><span class="p">()</span> <span class="k">by</span> <span class="n">bin</span><span class="p">(</span><span class="n">TimeGenerated</span><span class="p">,</span><span class="mi">1</span><span class="n">h</span><span class="p">),</span> <span class="n">tostring</span><span class="p">(</span><span class="n">toint</span><span class="p">(</span><span class="n">httpStatus_d</span><span class="p">))</span>
<span class="o">|</span> <span class="n">render</span> <span class="n">columnchart</span>
</code></pre></div></div>

<p><img src="/content/images/2023/07/loganalytics-httpstatus3.png" alt="log analytics azure app gateway chart grouped by http status working with corrected integer values" /></p>

<p>And voila, an Azure Application Gateway traffic graph displaying connections by status code, per hour.</p>]]></content><author><name>Mark Wragg</name><email>mark@wragg.io</email></author><category term="azure" /><category term="loganalytics" /><category term="appgateway" /><summary type="html"><![CDATA[I recently implemented a new Azure Application Gateway and was interested to chart the connections through it per hour, grouped by HTTP Response Code. My initial attempt was this query:]]></summary></entry><entry><title type="html">Changelog Driven Deployments</title><link href="https://wragg.io/changelog-driven-deployments/" rel="alternate" type="text/html" title="Changelog Driven Deployments" /><published>2022-02-20T14:00:00+00:00</published><updated>2022-02-20T14:00:00+00:00</updated><id>https://wragg.io/changelog-driven-deployments</id><content type="html" xml:base="https://wragg.io/changelog-driven-deployments/"><![CDATA[<p>A <a href="https://keepachangelog.com/en/1.0.0/">changelog</a> is a useful addition to any project, as it provides users and contributors with a summary of notable changes between each release. One way to ensure you always update your changelog as part of any new release is by making it part of the the automated deployment process. This blog post describes how I’ve implemented changelog driven deployments for the PowerShell modules I maintain in GitHub.</p>

<p>My PowerShell modules are built and deployed using a set of scripts that perform the following tasks whenever a PR is raised or a commit is made to the Master branch:</p>

<ol>
  <li>
    <p>Combine the individual PowerShell functions into a single module file. This improves performance when the module is loaded.</p>
  </li>
  <li>
    <p>Execute the Pester tests to validate the code is functioning correctly and to update the <code class="language-plaintext highlighter-rouge">README.md</code> with the current code coverage percentage.</p>
  </li>
  <li>
    <p>Generate automatic PowerShell Help documentation for each of the module’s commands into the /Documentation folder of the repo. This gives users the option to browse help online.</p>
  </li>
  <li>
    <p>Deploy the module to the PowerShell Gallery, but only <em>if</em> the branch is Master and the <code class="language-plaintext highlighter-rouge">CHANGELOG.md</code> file includes <code class="language-plaintext highlighter-rouge">## !Deploy</code>. This line of the ChangeLog is then modified during this task to include the version number of the module being deployed and the date of the deployment.</p>
  </li>
  <li>
    <p>Finally the changes that have been made to the source files (the updates to the README.md, CHANGELOG.md and any changes under /Documentation) are committed back to source control.</p>
  </li>
</ol>

<p>You can look at these tasks in more detail by looking at the /Build folder under any of <a href="https://github.com/markwragg/PowerShell-Influx/blob/master/Build">my PowerShell projects in GitHub</a>.</p>

<blockquote>
  <p>Note: The basis of my tasks/scripts have been lifted and modified from other PowerShell community members, but unfortunately I can’t recall who specifically to give them credit.</p>
</blockquote>

<p>Making the <code class="language-plaintext highlighter-rouge">CHANGELOG.md</code> part of the deployment logic has been achieved by adding the following to the Deploy task:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="n">Get-Item</span><span class="w"> </span><span class="s2">"</span><span class="nv">$ProjectRoot</span><span class="s2">/CHANGELOG.md"</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
        
  </span><span class="nv">$ChangeLog</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="s2">"</span><span class="nv">$ProjectRoot</span><span class="s2">/CHANGELOG.md"</span><span class="w">

  </span><span class="kr">if</span><span class="w"> </span><span class="p">(</span><span class="nv">$ChangeLog</span><span class="w"> </span><span class="o">-contains</span><span class="w"> </span><span class="s1">'## !Deploy'</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">

    </span><span class="nv">$Params</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">@{</span><span class="w">
      </span><span class="nx">Path</span><span class="w">    </span><span class="o">=</span><span class="w"> </span><span class="s2">"</span><span class="nv">$ProjectRoot</span><span class="s2">/Build/deploy.psdeploy.ps1"</span><span class="w">
      </span><span class="nx">Force</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="bp">$true</span><span class="w">
      </span><span class="nx">Recurse</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="bp">$false</span><span class="w">
    </span><span class="p">}</span><span class="w">

    </span><span class="n">Invoke-PSDeploy</span><span class="w"> </span><span class="err">@</span><span class="nx">Verbose</span><span class="w"> </span><span class="err">@</span><span class="nx">Params</span><span class="w">

    </span><span class="c"># Update ChangeLog with deployment version and date</span><span class="w">
    </span><span class="nv">$ChangeLog</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$ChangeLog</span><span class="w"> </span><span class="o">-replace</span><span class="w"> </span><span class="s1">'## !Deploy'</span><span class="p">,</span><span class="w"> </span><span class="s2">"## [</span><span class="nv">$Version</span><span class="s2">] - </span><span class="si">$(</span><span class="n">Get-Date</span><span class="w"> </span><span class="nt">-F</span><span class="w"> </span><span class="s1">'yyyy-MM-dd'</span><span class="p">)</span><span class="s2">"
    Set-Content -Path "</span><span class="nv">$ProjectRoot</span><span class="n">/CHANGELOG.md</span><span class="s2">" -Value </span><span class="nv">$ChangeLog</span><span class="s2">
  }
  else {
      Write-Host 'CHANGELOG.md did not contain ## !Deploy. Skipping deployment.'
  }
}
else {
  Write-Host "</span><span class="nv">$ProjectRoot</span><span class="nx">/CHANGELOG.md</span><span class="w"> </span><span class="nx">not</span><span class="w"> </span><span class="nx">found.</span><span class="w"> </span><span class="nx">Skipping</span><span class="w"> </span><span class="nx">deployment.</span><span class="s2">"
}
</span></code></pre></div></div>

<p>You can see that this is also where the <code class="language-plaintext highlighter-rouge">CHANGELOG.md</code> file is updated with the release version and date.</p>

<p>To allow the source file changes to be committed back to the repo you need to grant Azure DevOps permissions to write to your code repositories. This can be done by using <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/repos/github?view=azure-devops&amp;tabs=yaml#github-app-authentication">GitHub App authentication</a>. In my build pipeline committing the changes back to the repo is completed in the <strong>Commit</strong> build task as follows:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Set-Location</span><span class="w"> </span><span class="nv">$ProjectRoot</span><span class="w">
</span><span class="nv">$Module</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">BHProjectName</span><span class="w">

</span><span class="n">git</span><span class="w"> </span><span class="nt">--version</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="nt">--global</span><span class="w"> </span><span class="nx">user.email</span><span class="w"> </span><span class="s2">"build@azuredevops.com"</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">config</span><span class="w"> </span><span class="nt">--global</span><span class="w"> </span><span class="nx">user.name</span><span class="w"> </span><span class="s2">"AzureDevOps"</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">checkout</span><span class="w"> </span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">BUILD_SOURCEBRANCHNAME</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">add</span><span class="w"> </span><span class="nx">Documentation/</span><span class="o">*.</span><span class="nf">md</span><span class="w">
</span><span class="nx">git</span><span class="w"> </span><span class="nx">add</span><span class="w"> </span><span class="nx">README.md</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">add</span><span class="w"> </span><span class="nx">CHANGELOG.md</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">commit</span><span class="w"> </span><span class="nt">-m</span><span class="w"> </span><span class="s2">"[skip ci] AzureDevOps Build </span><span class="si">$(</span><span class="nv">$</span><span class="nn">env</span><span class="p">:</span><span class="nv">BUILD_BUILDID</span><span class="si">)</span><span class="s2">"</span><span class="w">
</span><span class="n">git</span><span class="w"> </span><span class="nx">push</span><span class="w">
</span></code></pre></div></div>

<p>It is important to include <code class="language-plaintext highlighter-rouge">[skip ci]</code> in the commit message otherwise you risk creating an infinite loop of builds and check ins. The <code class="language-plaintext highlighter-rouge">[skip ci]</code> tag is a built in way to tell Azure DevOps not to build something you’ve committed.</p>

<p>Here’s an example of the ChangeLog being updated for a recent deployment of my PowerShell-Influx module:</p>

<p><img src="/content/images/2022/02/changelog-deploy-example.png" alt="changelog deployment example commit" /></p>

<p>Here’s the CI pipeline updating the <code class="language-plaintext highlighter-rouge">CHANGELOG.md</code> to include the new version number and date as the title:</p>

<p><img src="/content/images/2022/02/changelog-deploy-example-2.png" alt="changelog deployment example commit" /></p>

<p>And here’s how the final ChangeLog looks:</p>

<p><img src="/content/images/2022/02/changelog-deploy-example-3.png" alt="changelog deployment example commit" /></p>

<p>If you decide to implement this technique it’s a good idea to also include a <code class="language-plaintext highlighter-rouge">CONTRIBUTING.md</code> file in your project to let people know that updating the ChangeLog is a mandatory part of the deployment process:</p>

<p><img src="/content/images/2022/02/contributing-example.png" alt="changelog deployment example commit" /></p>

<p>– https://github.com/markwragg/PowerShell-Influx/blob/master/CONTRIBUTING.md</p>]]></content><author><name>Mark Wragg</name><email>mark@wragg.io</email></author><category term="powershell" /><category term="azuredevops" /><category term="cicd" /><category term="github" /><summary type="html"><![CDATA[A changelog is a useful addition to any project, as it provides users and contributors with a summary of notable changes between each release. One way to ensure you always update your changelog as part of any new release is by making it part of the the automated deployment process. This blog post describes how I’ve implemented changelog driven deployments for the PowerShell modules I maintain in GitHub.]]></summary></entry></feed>