Jekyll2021-07-26T09:03:49+10:00https://mscloud.be/feed.xmlCloud management at your fingertipsAzure Cloud management at your fingertipsAlexandre VerkinderenCreate an Automatic Service Principal Azure RM Service Connection in Azure DevOps via Azure CLI2020-07-10T00:00:00+10:002020-07-10T00:00:00+10:00https://mscloud.be/azure/Create-Automatic-Service-Principal-AzureRM-Service-Connection-with-AzureCLI<p>With more and more of our development and infrastructure projects being built and released via Azure DevOps, I find myself creating a few DevOps projects which, at creation time, share identical configs like service connections, permissions, repository names etc. Therefore, this week I have been trying to automate the creation of Azure DevOps projects. Many of the configs are easily configurable with AzureCLI and the Devops extension of it, but one thing I was struggling with was the creation of the service connections to our Azure subscriptions the way we do it <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/library/connect-to-azure?view=azure-devops#create-an-azure-resource-manager-service-connection-using-automated-security">from the GUI.</a> We are using the Automatic Option when setting up the service connections for each one of our Azure subscriptions.</p>
<p><img src="https://mscloud.be/assets/images/2020-07-11-AutomaticGUI.PNG" alt="Automatic Service Connector GUI" /></p>
<p>When you select the Automatic way the following is happening as part of the automatic creation process:</p>
<ul>
<li>Azure Devops will connect to the AzureAD tenant</li>
<li>Creates an Azure AD application on behalf of the user</li>
<li>Assigns the new AzureAD application contributor permissions on the subscription</li>
<li>Creates a Service Connection in Azure DevOps using the AzureAD application details</li>
</ul>
<p>This is the preferred option as we don’t have to make certificates and keys or manually create the Azure AD Application etc.</p>
<h2 id="issue">Issue</h2>
<p>I wanted to be able to do the same thing with AzureCLI but if you look at the documentation there doesn’t seem to be a command for this. The <a href="https://docs.microsoft.com/en-us/cli/azure/ext/azure-devops/devops/service-endpoint/azurerm?view=azure-cli-latest#ext-azure-devops-az-devops-service-endpoint-azurerm-create">‘az devops service-endpoint azurerm create’</a> would seem to be the one we have to use but there is no automatic flag and its parameters are essentially what you see in the manual option on the GUI.</p>
<p><img src="https://mscloud.be/assets/images/2020-07-11-ManualGUI.PNG" alt="Manual Service Connector GUI" /></p>
<p>So it was looking like it was not going to be possible to automate that part until I started looking at the general <a href="https://docs.microsoft.com/en-us/cli/azure/ext/azure-devops/devops/service-endpoint?view=azure-cli-latest#ext-azure-devops-az-devops-service-endpoint-create">‘az devops service-endpoint create’</a> commands which requires a config file.</p>
<h2 id="solution">Solution</h2>
<p>While doing some research online, I found various examples for creating Service Connections to numerous external and third party products but nothing to AzureRM. Using another project as a template I started using the <a href="https://docs.microsoft.com/en-us/cli/azure/ext/azure-devops/devops/service-endpoint?view=azure-cli-latest#ext-azure-devops-az-devops-service-endpoint-list">‘az devops service-endpoint list’</a> and <a href="https://docs.microsoft.com/en-us/cli/azure/ext/azure-devops/devops/service-endpoint?view=azure-cli-latest#ext-azure-devops-az-devops-service-endpoint-show">‘az devops service-endpoint show’</a> commands to get the config of the existing service connections in an effort to recreate the config and hopefully get it to create the automatic service principal connection we wanted. After some trial and error I got a config that successfully creates the service connection.</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"data"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"creationMode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Automatic"</span><span class="p">,</span><span class="w">
</span><span class="nl">"environment"</span><span class="p">:</span><span class="w"> </span><span class="s2">"AzureCloud"</span><span class="p">,</span><span class="w">
</span><span class="nl">"managementGroupId"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
</span><span class="nl">"managementGroupName"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
</span><span class="nl">"mlWorkspaceLocation"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
</span><span class="nl">"mlWorkspaceName"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
</span><span class="nl">"oboAuthorization"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
</span><span class="nl">"resourceGroupName"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
</span><span class="nl">"resourceId"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
</span><span class="nl">"scopeLevel"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Subscription"</span><span class="p">,</span><span class="w">
</span><span class="nl">"subscriptionId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"</span><span class="p">,</span><span class="w">
</span><span class="nl">"subscriptionName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"DEV"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">,</span><span class="w">
</span><span class="nl">"groupScopeId"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"isReady"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"isShared"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="p">,</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"SC-DEV"</span><span class="p">,</span><span class="w">
</span><span class="nl">"authorization"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"parameters"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"authenticationType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"spnKey"</span><span class="p">,</span><span class="w">
</span><span class="nl">"tenantId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"scheme"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ServicePrincipal"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"owner"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Library"</span><span class="p">,</span><span class="w">
</span><span class="nl">"readersGroup"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"azurerm"</span><span class="p">,</span><span class="w">
</span><span class="nl">"url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://management.azure.com/"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<blockquote>
<p>Note: We tried to have this step run as part of a YAML Build Pipeline but the service connection is unable to create the SPN in AzureAD. In the GUI it looks like the Service Connection is created which confused us at first. But if you use the ‘az devops service-endpoint list’ for that project it does not return any Service Connections. However, if you do a ‘az devops service-endpoint show’ and specify the ID of the service connector that is outputted during the ‘az devops service-endpoint create’ you will see the details of the service connection.</p>
</blockquote>
<p><img src="https://mscloud.be/assets/images/2020-07-11-FailedPipelineRun.PNG" alt="Failed SC Creation in Pipeline" /></p>
<p>As you can see, the service connection is never completely provisioned as it is unable to create the SPN in AAD as the YAML pipeline does not have rights to do so. This step is one of many that we have not being able to have run as a DevOps Pipeline due to similar reasons.</p>
<p>Therefore we have opted, for now, to provision new DevOps Projects from a Powershell script which will run under the context of a real user who has the necessary rights to both Azure AD and DevOps.</p>
<p><img src="https://mscloud.be/assets/images/2020-07-11-AzdoPS.PNG" alt="DevOp Project Creation via PS" /></p>
<p>Rodney.</p>Rodney AlmeidaWith more and more of our development and infrastructure projects being built and released via Azure DevOps, I find myself creating a few DevOps projects which, at creation time, share identical configs like service connections, permissions, repository names etc. Therefore, this week I have been trying to automate the creation of Azure DevOps projects. Many of the configs are easily configurable with AzureCLI and the Devops extension of it, but one thing I was struggling with was the creation of the service connections to our Azure subscriptions the way we do it from the GUI. We are using the Automatic Option when setting up the service connections for each one of our Azure subscriptions.The curious case of an Azure Application Gateway showing no metrics and logs2020-06-24T00:00:00+10:002020-06-24T00:00:00+10:00https://mscloud.be/azure/Azure-App-Gateway-Showing-no-Metrics<p>This is the curious case of an Azure Application Gateway showing no metrics and logs at all. Even thought this was one of the main customer’s production Application Gateways we could see 0 requests in the metrics. Which was strange as behind the Application Gateway was an online webshop which served thousands of customers every day.</p>
<p><img src="https://mscloud.be/assets/images/2020-06-24-Azureappgw-nometrics.png" alt="App gateway no metrics" /></p>
<p>These metrics should show up regardless if you have log analytics configured or not. Our diagnostic logs are automatically configured based on this <a href="https://blog.tyang.org/2019/05/19/deploying-azure-policy-definitions-via-azure-devops-part-1/">Azure Policy written by Tao</a>. We double checked the diagnostics settings were enabled, which was the case, but still no logs were stored in Log Analytics:</p>
<p><img src="https://mscloud.be/assets/images/2020-06-24-Azureappgw-nologs.png" alt="App gateway no metrics" /></p>
<h2 id="issue">Issue</h2>
<p>I logged a ticket and the Azure support team came back after a few days saying that the issue is due to some <a href="https://docs.microsoft.com/en-us/azure/application-gateway/application-gateway-ssl-policy-overview">custom SSL Policy</a> on the Azure Application Gateway. The customer did change the default SSL settings and was using the following custom SSL settings:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="w"> </span><span class="nl">"SSLPolicy"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"CipherSuites"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"</span><span class="p">,</span><span class="w">
</span><span class="s2">"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"</span><span class="p">,</span><span class="w">
</span><span class="s2">"TLS_RSA_WITH_AES_128_GCM_SHA256"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span></code></pre></div></div>
<p><img src="https://mscloud.be/assets/images/2020-06-24-Azureappgw-CustomSSL.png" alt="App gateway no metrics" /></p>
<h2 id="solution">Solution</h2>
<p>In order to make the logs and metrics work, we had to add 2 more cypher suites :</p>
<ul>
<li>TLS_RSA_WITH_AES_256_CBC_SHA256</li>
<li>TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256</li>
</ul>
<p>The reason for this is that the Azure Application Gateway V1 writes logs to a storage account in the backend. This storage account requires certain Cypher Suites to be enabled in order to be able to store the logs and metrics to that storage account. The following 3 cypher suites mentioned below must be enabled:</p>
<ul>
<li>TLS_RSA_WITH_AES_128_GCM_SHA256</li>
<li>TLS_RSA_WITH_AES_256_CBC_SHA256</li>
<li>TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256</li>
</ul>
<p>So make sure you have at least the 3 cypher enabled as seen in this picture.</p>
<p><img src="https://mscloud.be/assets/images/2020-06-24-Azureappgw-fixSSL.png" alt="App gateway no metrics" /></p>
<h2 id="conclusion">Conclusion</h2>
<p>Don’t mess with the SSL settings :smiley:.</p>
<p>A few seconds after adding the 2 missing cypher suites the metrics and logs started to show up.</p>
<p><img src="https://mscloud.be/assets/images/2020-06-24-Azureappgw-metrics.png" alt="App gateway no metrics" /></p>
<p>Hope this helps,</p>
<p>Alex</p>Alexandre VerkinderenThis is the curious case of an Azure Application Gateway showing no metrics and logs at all. Even thought this was one of the main customer’s production Application Gateways we could see 0 requests in the metrics. Which was strange as behind the Application Gateway was an online webshop which served thousands of customers every day.Publish the new Azure API Management Service Developer Portal behind an Application Gateway2020-06-10T00:00:00+10:002020-06-10T00:00:00+10:00https://mscloud.be/azure/Publish-New-Azure-APIM-Developer_portal<p>There are currently 2 developer portals for the Azure API Management service: a legacy portal and the <a href="https://docs.microsoft.com/en-us/azure/api-management/api-management-howto-developer-portal">new portal experience</a>. We deployed our Azure APIM instance before the new portal was released so we were still running on the legacy portal. We are running our Azure APIM instance with custom domains on an internal vnet behind an application gateway with WAF and the default OWASP 3.0 rules enabled. This appeared to be a real challenge with the new developer portal.</p>
<p>In this blogpost I will cover how to move to the new developer portal experience and changes that are required on your Application Gateway.</p>
<h2 id="1-create-new-custom-domain-for-the-new-apim-management-endpoint">1) Create new custom domain for the new APIM Management Endpoint</h2>
<p>If we want to use the new developer portal we need to publish a new APIM management endpoint and create a new CNAME in our publis DNS register. The old developer portal only requires the portal endpoint to be published as shown below:</p>
<p><img src="https://mscloud.be/assets/images/2020-06-10-AzureAPI-Customportal.png" alt="Custom domains" /></p>
<p>To add the new management endpoint click <strong>custom domains</strong> and <strong>add new</strong>. Select <strong>Management</strong> and provide your custom hostname and certificate.</p>
<p><img src="https://mscloud.be/assets/images/2020-06-10-AzureAPI-mpendpoint.png" alt="add management point" /></p>
<h2 id="2-remove-the-legacy-developer-portal-custom-domains-and-add-the-new-developer-portal-domain">2) Remove the Legacy developer portal custom domains and add the new developer portal domain</h2>
<p>Next, we will add the new developer portal with a custom domain and remove the legacy developer portal custom domain. Your custom domains should look like this:</p>
<p><img src="https://mscloud.be/assets/images/2020-06-10-AzureAPI-removelegacyportal.png" alt="remove legacy portal" /></p>
<h2 id="3-update-waf-rules-on-azure-application-gateway">3) Update WAF rules on Azure Application Gateway</h2>
<p>I have my Azure APIM sitting on a <a href="https://docs.microsoft.com/en-us/azure/api-management/api-management-howto-integrate-internal-vnet-appgateway#--scenario">virtual network behind an Azure Application gateway</a>. The legacy portal is fully comptabile with WAF OWASP 3.0 rules. However, as of writing this article, the new Azure APIM developer portal is not fully compatible with the industry standard OWASP rules. The WAF will block certain requests and treat them as SQL injections for example. Luckily my WAF and App gateway are only used for this APIM so I could lower my security, as a workaround, and disable certain rules. But I can imagine you would not be so happy to disable those rules if all your traffic is going through the same app gateway. The following rules are to be disabled:</p>
<ul>
<li>Rule 920311: Missing User Agent Header</li>
<li>Rule 931130: Possible Remote File Inclusion (RFI) Attack: Off-Domain Reference/Link</li>
<li>Rule 942100: SQL Injection Attack Detected via lib injection</li>
<li>Rule 942110: SQL Injection Attack: Common Injection Testing Detected</li>
<li>Rule 942200: Detects MySQL comment-/space-obfuscated injections and backtick termination</li>
<li>Rule 942430: Restricted SQL Character Anomaly Detection (args): # of special characters exceeded (12)</li>
<li>Rule 942440: SQL Comment Sequence Detected</li>
</ul>
<p><img src="https://mscloud.be/assets/images/2020-06-10-AzureAPI-WAFRules.png" alt="WAF Rules" /></p>
<h2 id="4-update-azure-application-gateway-routing-rules-and-health-probe">4) Update Azure Application Gateway Routing Rules and Health Probe</h2>
<p>Next, as we have a new endpoint, we need to update our health probes and app gateway settings. Go to your Azure Application Gateway and configure the listener and rules like this:</p>
<p><strong>Create a new Health Probe</strong></p>
<p>Add the new management endpoint public address the host.</p>
<p><img src="https://mscloud.be/assets/images/2020-06-10-AzureAPI-addHealthProbe.png" alt="Health probe" /></p>
<p>Notice that we used <code class="language-plaintext highlighter-rouge">/servicestatus</code> for the path.</p>
<p><strong>Create new HTTP Settings</strong>:</p>
<p>Add the following new HTTP setting to include the new management endpoint:</p>
<p><img src="https://mscloud.be/assets/images/2020-06-10-AzureAPI-addHTTPSetting.png" alt="http setting" /></p>
<p><strong>You will need to add 2 new listeners</strong>:</p>
<p>You wil need to create 2 listeners. One for the management endpoint on https on port 3443 and one for https on port 443:</p>
<p><img src="https://mscloud.be/assets/images/2020-06-10-AzureAPI-ListenerhttpsManagement.png" alt="listener https" /></p>
<p><strong>You will also need to add 2 new routing rules</strong>:</p>
<p>Create 2 new routing rules for:</p>
<p><img src="https://mscloud.be/assets/images/2020-06-10-AzureAPI-addroutingrulehttps.png" alt="add https routing rule" /></p>
<p><img src="https://mscloud.be/assets/images/2020-06-10-AzureAPI-addroutingrule.png" alt="add http routing rule" /></p>
<p>That’s it for the application gateway.</p>
<h2 id="reprovision-the-new-developer-portal">Reprovision the new Developer portal</h2>
<p>The new developer portal was not provisioned correctly in my environment. I had a blank screen when I checked if it was working properly. After logging a call with MS support they told me I had to reprovision the new developer portal. I guess that’s due to the WAF rules blocking certain requests and the new portal was not properly provisioned. To provision the new portal you will need a script that you can <a href="https://github.com/averkinderen/APIM/tree/master/ProvisioningNewPortal">find here</a>. It will require <a href="https://nodejs.org/dist/v12.16.1/node-v12.16.1-x64.msi">node js</a> to be installed.</p>
<p>For the script to execute you need a management API key. Go to your API management instance and in ‘Management API ‘ click <strong>enable</strong> and <strong>generate</strong> to get the Shared access signature token.</p>
<p><img src="https://mscloud.be/assets/images/2020-06-10-AzureAPI-APImanagement.png" alt="SAS key" /></p>
<p>Open the <code class="language-plaintext highlighter-rouge">generate-data.bat</code> file and fill in your shared access key and update the management endpoint:</p>
<p><img src="https://mscloud.be/assets/images/2020-06-10-AzureAPI-generate-data.png" alt="generate data" /></p>
<p>Run the bat file from the command prompt. This will provision the new API developer portal experience.</p>
<p>Hope this helps,</p>
<p>Alex</p>Alexandre VerkinderenThere are currently 2 developer portals for the Azure API Management service: a legacy portal and the new portal experience. We deployed our Azure APIM instance before the new portal was released so we were still running on the legacy portal. We are running our Azure APIM instance with custom domains on an internal vnet behind an application gateway with WAF and the default OWASP 3.0 rules enabled. This appeared to be a real challenge with the new developer portal.Setting an Azure AD group to Azure SQL Database with ARM templates2020-06-07T00:00:00+10:002020-06-07T00:00:00+10:00https://mscloud.be/azure/Add-AzureAD-SQL-Admin-to-AzureSQL<p>I was recently looking at a way to automatically set an Azure AD group as the SQL admin for our Azure SQL databases with ARM tempplates. We use SQL authentication and Azure AD authentication for our SQL databases. The password for the SQL admin gets generated randomly as part of our pipeline and stored in Keyvault. We also have a dedicated team of SQL DBAs who would need to connect to the deployed SQL resources using their Azure AD credentials.
Technically it is possible as per this <a href="https://docs.microsoft.com/en-us/azure/templates/microsoft.sql/2019-06-01-preview/servers/administrators">link</a> to set an Azure AD group as the SQL admins but I could not find a good example on how to do this with ARM.</p>
<p>You will need 3 things to be able to set an Azure AD SQL admin:</p>
<ol>
<li>The tenantID. This is your Azure AD tenant ID and will need to be passed on to the tenantId property.</li>
<li>ObjectID of the group. You can find this in your AzureAD group details and needs to be passed on to the sid property.</li>
<li>Name of the group.</li>
</ol>
<p><img src="https://mscloud.be/assets/images/2020-06-06-AzureSQL-ADAdmindetails.png" alt="Azure SQL Set Azure AD ADmin" /></p>
<h2 id="end-to-end-arm-template-example">End to end ARM template example</h2>
<p>If you take the example provided in the link above it’s not going to work. You need to specify the SQL server name before <code class="language-plaintext highlighter-rouge">ActiveDirectory</code>. To do this we use a concat function to concatenate the servername with the ActiveDirectory name like this:</p>
<p><code class="language-plaintext highlighter-rouge">"name": "[concat(variables('sqlServerName'),'/ActiveDirectory')]"</code></p>
<p>Below you can find an end to end solution to deploy a new Azure SQL database and immediately setting an Azure AD group as the SQL admins.</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
</span><span class="nl">"$schema"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"</span><span class="p">,</span><span class="w">
</span><span class="nl">"contentVersion"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0.0.0"</span><span class="p">,</span><span class="w">
</span><span class="nl">"parameters"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"guidValue"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w">
</span><span class="nl">"defaultValue"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[newGuid()]"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"SQLNamePrefix"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w">
</span><span class="nl">"defaultValue"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[resourceGroup().name]"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"sqlAdministratorLogin"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w">
</span><span class="nl">"defaultValue"</span><span class="p">:</span><span class="w"> </span><span class="s2">"SQLAdmin"</span><span class="p">,</span><span class="w">
</span><span class="nl">"metadata"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"The administrator username of the SQL Server."</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"sqlAdministratorLoginPassword"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"securestring"</span><span class="p">,</span><span class="w">
</span><span class="nl">"metadata"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"The administrator password of the SQL Server."</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"location"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w">
</span><span class="nl">"defaultValue"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[resourceGroup().location]"</span><span class="p">,</span><span class="w">
</span><span class="nl">"metadata"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Location for all resources."</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"LinkedTemplatePrefix"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"variables"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"sqlServerName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[take(concat('sql-',parameters('SQLNamePrefix'),'-',uniqueString(parameters('guidValue'))),32)]"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ADtenantID"</span><span class="p">:</span><span class="w"> </span><span class="s2">"91xxxbe-xxx-43bf-axxx0-c2fxxx47349"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ADobjectID"</span><span class="p">:</span><span class="w"> </span><span class="s2">"3exxc5-17xx3-xxb-9axxb2-e1xxx74b76"</span><span class="p">,</span><span class="w">
</span><span class="nl">"ADLogin"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Applications Team - Database Administrator"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"resources"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[variables('sqlServerName')]"</span><span class="p">,</span><span class="w">
</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Microsoft.Sql/servers"</span><span class="p">,</span><span class="w">
</span><span class="nl">"apiVersion"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2019-06-01-preview"</span><span class="p">,</span><span class="w">
</span><span class="nl">"location"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[parameters('location')]"</span><span class="p">,</span><span class="w">
</span><span class="nl">"tags"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"displayName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"SqlServer"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"properties"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"administratorLogin"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[parameters('sqlAdministratorLogin')]"</span><span class="p">,</span><span class="w">
</span><span class="nl">"administratorLoginPassword"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[parameters('sqlAdministratorLoginPassword')]"</span><span class="p">,</span><span class="w">
</span><span class="nl">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"12.0"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[concat(variables('sqlServerName'),'/ActiveDirectory')]"</span><span class="p">,</span><span class="w">
</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Microsoft.Sql/servers/administrators"</span><span class="p">,</span><span class="w">
</span><span class="nl">"dependsOn"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"[resourceId('Microsoft.Sql/servers', variables('sqlServerName'))]"</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"apiVersion"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2019-06-01-preview"</span><span class="p">,</span><span class="w">
</span><span class="nl">"properties"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"administratorType"</span><span class="p">:</span><span class="w"> </span><span class="s2">"ActiveDirectory"</span><span class="p">,</span><span class="w">
</span><span class="nl">"login"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[variables('ADLogin')]"</span><span class="p">,</span><span class="w">
</span><span class="nl">"sid"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[variables('ADobjectID')]"</span><span class="p">,</span><span class="w">
</span><span class="nl">"tenantId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[variables('ADtenantID')]"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"outputs"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"sqlServerFqdn"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w">
</span><span class="nl">"value"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[reference(concat('Microsoft.Sql/servers/', variables('sqlServerName'))).fullyQualifiedDomainName]"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"sqlServerName"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w">
</span><span class="nl">"value"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[variables('sqlServerName')]"</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>And as you can see, when we deploy a new Azure SQL database the AD admin will be set automatically as part of our template deployment:</p>
<p><img src="https://mscloud.be/assets/images/2020-06-06-AzureSQL-SetADadmin.png" alt="Azure SQL Set Azure AD ADmin" /></p>
<p>Alex</p>Alexandre VerkinderenI was recently looking at a way to automatically set an Azure AD group as the SQL admin for our Azure SQL databases with ARM tempplates. We use SQL authentication and Azure AD authentication for our SQL databases. The password for the SQL admin gets generated randomly as part of our pipeline and stored in Keyvault. We also have a dedicated team of SQL DBAs who would need to connect to the deployed SQL resources using their Azure AD credentials. Technically it is possible as per this link to set an Azure AD group as the SQL admins but I could not find a good example on how to do this with ARM.New Book: Inside Azure Management V42020-05-28T00:00:00+10:002020-05-28T00:00:00+10:00https://mscloud.be/azure/New-book-Inside-Azure-Management<p>Super proud to announce that our new book <strong><em>Inside Azure Management V4</em></strong> has been released! This book is the 4th edition and covers a broad range of Azure Management related topics like cloud governance, process automation, infrastructure updates, application, and container monitoring. Writing an ebook was something new to me and definitely a refreshing experience compared to paperback <a href="https://www.amazon.com/System-Center-Service-Manager-Unleashed-ebook/dp/B005G2FSRM/ref=sr_1_2?dchild=1&keywords=alexandre+verkinderen&qid=1590273037&s=digital-text&sr=1-2">System Center Unleashed</a> series.</p>
<p>Many thanks to <a href="https://twitter.com/pzerger">Pete Zerger</a> for all the work and coordination he put into this book. And of course a big thank you to the other authors as well:</p>
<ul>
<li><a href="https://twitter.com/StanZhelyazkov">Stanislav Zhelyazkov</a></li>
<li><a href="https://twitter.com/mrtaoyang">Tao Yang</a></li>
<li><a href="https://twitter.com/kgreeneit">Kevin Greene</a></li>
<li><a href="https://twitter.com/reirujo">Ryan Irujo</a></li>
<li><a href="https://twitter.com/bertwolters">Bert Wolters</a></li>
</ul>
<p>We have a free download available and all of our scripts and code is available on Github:</p>
<ul>
<li>
<p><a href="https://bit.ly/InsideAzure">Free download</a></p>
</li>
<li>
<p><a href="https://www.amazon.com/Inside-Azure-Management-authoritative-Microsofts-ebook/dp/B088TBGWYS">Amazon</a></p>
</li>
<li>
<p><a href="https://github.com/insidemscloud/InsideAzureMgmt">Code repository</a></p>
</li>
</ul>
<p><img src="https://mscloud.be/assets/images/2020-05-28-inside-azure-managemnet-v4.png" alt="New book" /></p>
<p>Hope you enjoy the book and please let us know if you have any feedback,
Alex</p>Alexandre VerkinderenSuper proud to announce that our new book Inside Azure Management V4 has been released! This book is the 4th edition and covers a broad range of Azure Management related topics like cloud governance, process automation, infrastructure updates, application, and container monitoring. Writing an ebook was something new to me and definitely a refreshing experience compared to paperback System Center Unleashed series.Define a hierarchical structure for your Azure DevOps branches2020-05-23T00:00:00+10:002020-05-23T00:00:00+10:00https://mscloud.be/azure/Require-folders-For-Branches<p>I like my Azure DevOps branches to be well structured and well named.</p>
<p>When we create a new ARM template a new feature branch is created based on the name of the template. This branch is short lived, typically only a few days and max 2 weeks. Once the development of the template is done, a pull request is created, the branch gets merged if all tests are passed and the branch gets deleted. We also have branches that will be created in the rare case a bug has been discovered in one of our ARM templates. These branches are even more short lived, typically only a few hours to maximum a few days.</p>
<p><img src="https://mscloud.be/assets/images/2020-05-23-AZDO-Branches.png" alt="Devops Branches" /></p>
<p>We use the following naming convention for our branches:</p>
<ul>
<li>Feature/ARM-template-name</li>
<li>Bugs/ARM-template-name</li>
</ul>
<p>When working with lots of different people in the same AzDo project the number of branches can increase exponentially and it can quickly become a mess. Hierarchical branch folders is an easy solution to keep your branches clean and well structured. In Azure DevOps when you create a new folder with <code class="language-plaintext highlighter-rouge">/</code> it will automatically create that new branch under a folder.</p>
<p>The following standards have been defined in our AzDo environment:</p>
<ul>
<li>Only Dev and Master branch will be allowed to exist under the root</li>
<li>Contributors will be forced to create new branches under the Features or Bugs folders</li>
</ul>
<h2 id="requirements">Requirements</h2>
<p>To be able to enforce folders in Azure DevOps we will need to use Team Foundation version control command (<code class="language-plaintext highlighter-rouge">tf.exe</code>). You can find tf.exe if you have installed Visual Studio in the following location: <code class="language-plaintext highlighter-rouge">C:\Program Files (x86)\Microsoft Visual Studio\2019\TeamExplorer\Common7\IDE\CommonExtensions\Microsoft\TeamFoundation\Team Explorer</code>. You will also need the AzDo organization name, the project name, the repository name and full permissions to change branch permissions. So in my case it is the following:</p>
<ol>
<li>Organization Name = azurelns</li>
<li>Team Project Name = Todo</li>
<li>Repository name = Todo</li>
</ol>
<p><img src="https://mscloud.be/assets/images/2020-05-23-AZDO-organization-repo.png" alt="AZDO organization" /></p>
<h2 id="enforce-branch-folder-structure">Enforce Branch Folder structure</h2>
<p>Open the Developer Command Prompt, under Start > Visual Studio > Developer Command Prompt and go to the location of tf.exe.</p>
<p>First, let’s block the creation of new branches under the root</p>
<p><code class="language-plaintext highlighter-rouge">tf git permission /deny:CreateBranch /group:[Todo]\Contributors /collection:https://dev.azure.com/azurelns/ /teamproject:Todo /repository:Todo</code></p>
<p>Then allow contributors to create new branches under the Features folder</p>
<p><code class="language-plaintext highlighter-rouge">tf git permission /allow:CreateBranch /group:[Todo]\Contributors /collection:https://dev.azure.com/azurelns/ /teamproject:Todo /repository:Todo /branch:Feature</code></p>
<p>and the bugs folder</p>
<p><code class="language-plaintext highlighter-rouge">tf git permission /allow:CreateBranch /group:[Todo]\Contributors /collection:https://dev.azure.com/azurelns/ /teamproject:Todo /repository:Todo /branch:Bugs</code></p>
<p>Finally, allow administrators to create a branch called master and dev (in case it ever gets deleted accidentally) :man_shrugging: .</p>
<p><code class="language-plaintext highlighter-rouge">tf git permission /allow:CreateBranch /group:"[Todo]\Project Administrators" /collection:https://dev.azure.com/azurelns/ /teamproject:Todo /repository:Todo /branch:master</code></p>
<p><code class="language-plaintext highlighter-rouge">tf git permission /allow:CreateBranch /group:"[Todo]\Project Administrators" /collection:https://dev.azure.com/azurelns/ /teamproject:Todo /repository:Todo /branch:dev</code></p>
<p>Now when I want to create a new branch directly under the root it will blocked as I’m not following the correct naming convention.</p>
<p><img src="https://mscloud.be/assets/images/2020-05-23-AZDO-creation-blocked.png" alt="AZDO organization" /></p>
<p>To create this branch I would need to place it in a folder like this: “Feature/testbranch”</p>
<h2 id="conclusion">Conclusion</h2>
<p>When working with multiple people on a project it is important to define some kind of hierarchical branch structure. The Team Foundation version control command lets you enforce that structure.</p>
<p><img src="https://mscloud.be/assets/images/2020-05-23-AZDO-Folders.png" alt="Devops Folders" /></p>
<p>Hope this helps,
Alex</p>Alexandre VerkinderenI like my Azure DevOps branches to be well structured and well named.Use Azure DevOps Self Hosted agents with Azure App Service access restrictions2020-05-15T00:00:00+10:002020-05-15T00:00:00+10:00https://mscloud.be/azure/Devops-Release-Pipelines-with-AppService-IP-Restrictions<p>By default, when you deploy a new Azure WebApp, Function app or API app it will be publicly available to the internet. For the current customer I’m working on we made it a standard that all webapps should not be directly publicly available. To enhance our security we deploy Azure Frontdoor and Azure API Management Service for our APIs and also <a href="https://docs.microsoft.com/en-us/azure/app-service/app-service-ip-restrictions">enable IP restrictions</a>. As mentioned in my <a href="https://mscloud.be/azure/Update-API-in-APIM-from-Azure-Devops/">previous blog post</a> we currently use Azure DevOps with Microsoft hosted agents to build and release all of our web apps and API apps.</p>
<h2 id="issue">Issue</h2>
<p>Our release pipeline broke after enabling IP restrictions on the API app.</p>
<p><img src="https://mscloud.be/assets/images/2020-05-15-AZDO-400.png" alt="Devops error message" /></p>
<p>We have a task, as mentioned in <a href="https://mscloud.be/azure/Update-API-in-APIM-from-Azure-Devops">this blogpost</a>, that reads the Swagger file from our API app to update the APIs in APIM. After enabling IP restrictions we would receive a 404 error message when trying to read the swagger file as the swagger file was not publicly availabile anymore</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="mi">2020-05-12</span><span class="err">T</span><span class="mi">04</span><span class="err">:</span><span class="mi">03</span><span class="err">:</span><span class="mf">55.0921140</span><span class="err">Z</span><span class="w"> </span><span class="p">}</span><span class="w">
</span><span class="mi">2020-05-12</span><span class="err">T</span><span class="mi">04</span><span class="err">:</span><span class="mi">03</span><span class="err">:</span><span class="mf">55.0965097</span><span class="err">Z</span><span class="w"> </span><span class="err">Creating</span><span class="w"> </span><span class="err">or</span><span class="w"> </span><span class="err">updating</span><span class="w"> </span><span class="err">API</span><span class="w"> </span><span class="err">https://management.azure.com/subscriptions/xxxx/resourceGroups/RG-providers/Microsoft.ApiManagement/service/de/apis/xxxxx-client-api?api-version=</span><span class="mi">2018-01-01</span><span class="w">
</span><span class="mi">2020-05-12</span><span class="err">T</span><span class="mi">04</span><span class="err">:</span><span class="mi">03</span><span class="err">:</span><span class="mf">56.5778313</span><span class="err">Z</span><span class="w"> </span><span class="err">@</span><span class="p">{</span><span class="err">code=ValidationError;</span><span class="w"> </span><span class="err">target=representation;</span><span class="w"> </span><span class="err">message=Parsing</span><span class="w"> </span><span class="err">error(s):</span><span class="w"> </span><span class="err">Failed</span><span class="w"> </span><span class="err">to</span><span class="w"> </span><span class="err">import</span><span class="w"> </span><span class="err">from</span><span class="w"> </span><span class="err">specified</span><span class="w"> </span><span class="err">resource</span><span class="w"> </span><span class="err">https://api-d-xxxx.azurewebsites.net/swagger/v</span><span class="mi">1</span><span class="err">/swagger.json:</span><span class="w"> </span><span class="err">Response</span><span class="w"> </span><span class="err">status</span><span class="w"> </span><span class="err">code</span><span class="w"> </span><span class="err">does</span><span class="w"> </span><span class="err">not</span><span class="w"> </span><span class="err">indicate</span><span class="w"> </span><span class="err">success:</span><span class="w"> </span><span class="mi">404</span><span class="w"> </span><span class="err">(Not</span><span class="w"> </span><span class="err">Found)..</span><span class="p">}</span><span class="w">
</span><span class="mi">2020-05-12</span><span class="err">T</span><span class="mi">04</span><span class="err">:</span><span class="mi">03</span><span class="err">:</span><span class="mf">56.8939058</span><span class="err">Z</span><span class="w"> </span><span class="err">##</span><span class="p">[</span><span class="err">error</span><span class="p">]</span><span class="err">The</span><span class="w"> </span><span class="err">remote</span><span class="w"> </span><span class="err">server</span><span class="w"> </span><span class="err">returned</span><span class="w"> </span><span class="err">an</span><span class="w"> </span><span class="err">error:</span><span class="w"> </span><span class="err">(</span><span class="mi">400</span><span class="err">)</span><span class="w"> </span><span class="err">Bad</span><span class="w"> </span><span class="err">Request.</span><span class="w">
</span><span class="mi">2020-05-12</span><span class="err">T</span><span class="mi">04</span><span class="err">:</span><span class="mi">03</span><span class="err">:</span><span class="mf">56.9309562</span><span class="err">Z</span><span class="w"> </span><span class="err">##</span><span class="p">[</span><span class="err">section</span><span class="p">]</span><span class="err">Finishing:</span><span class="w"> </span><span class="err">Update</span><span class="w"> </span><span class="err">API</span><span class="w"> </span><span class="err">in</span><span class="w"> </span><span class="err">APIM</span><span class="w">
</span></code></pre></div></div>
<p>So we had to try and find the public IP of our hosted agents and add those IPs in the allow list on our WebApp. At first I tried adding these <a href="https://docs.microsoft.com/en-us/azure/devops/organizations/security/allow-list-ip-url?view=azure-devops#ip-addresses-and-range-restrictions">Azure Devops IP addresses</a> in the allow IP list of the webApp. However, the release pipeline still failed as those are the IP adddresses of the Azure DevOps Service itself, like the control plane. Not the build agents themselves.</p>
<p>I then realised the hosted agents are being deployed as resources in Azure in the same <a href="https://azure.microsoft.com/en-us/global-infrastructure/geographies/">Azure geography</a> as your Azure DevOps Organization. So in my case, the hosted agents would be spin up in Azure EastUS 2.</p>
<p><img src="https://mscloud.be/assets/images/2020-05-16-AZDO-region.png" alt="Devops region" /></p>
<p>The list of <a href="https://www.microsoft.com/en-us/download/details.aspx?id=56519">public IPs used per</a> region is updated every week. I would need to download this list every week and update my IP restrictions on a weekly basis with some kind of automation. This is definitely not something that I wanted to do.</p>
<blockquote>
<p><strong>NOTE</strong>
Due to some capacity restrictions in some regions your hosted agents may be deployed in other Azure Geographies as your Devops location. For example if there are capacity issues in West Europe the Hosted agents fall back to France. This would make it even more difficult to automate.</p>
</blockquote>
<h2 id="solution-self-hosted-agents">Solution: Self Hosted Agents</h2>
<p>I’m a big fan of Microsoft hosted agents, it makes my life easier, I don’t need to worry about maintenance and high availability and it’s really cheap to just buy another agent if needed. However, for this scenario the only possible solution is to use Self-Hosted build agents and add the public IP to the webApp IP restriction Allow List. There are a few different options to install a self-hosted agent (<a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/docker?view=azure-devops">docker</a> and <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/agents/v2-windows?view=azure-devops">Windows</a> for example) that I’m not going to cover here. We installed a self-hosted agent in our virtual network that is sitting behind an Azure Firewall in our Hub network and all subnets have that firewall defined as the next hope for route 0.0.0.0/0. This means that all unknown traffic (like internet traffic) will go through the Azure Firewall and will exit our network using the public IP of that Azure Firewall.</p>
<p><img src="https://mscloud.be/assets/images/2020-05-16-AZDO-Selfhosted.png" alt="Azdo Self Hosted" /></p>
<p>After deploying the self-hosted agent in our network all we had to do was to add the public ip of the Azure Firewall to our IP restrictions list and change our Yaml pipeline to use the self-hosted agent instead</p>
<pre><code class="language-Yaml">- stage: DEV
variables:
- group: DEV
displayName: "Release to DEV"
jobs:
- job: DEV
displayName: "Deploy to DEV"
pool: Self-Hosted
steps:
- task: DownloadBuildArtifacts@0
displayName: "Download Build Artifact"
inputs:
buildType: 'current'
downloadType: 'single'
artifactName: 'drop'
downloadPath: '$(System.ArtifactsDirectory)'
</code></pre>
<p>And adding the public IP to the allow list on the Azure APP:</p>
<p><img src="https://mscloud.be/assets/images/2020-05-16-AZDO-IPrestrictions.png" alt="WebApp IP restrictions" /></p>
<p>After this we could successfully read the swagger file from our APIapp and update the API management. :smile:</p>
<p><img src="https://mscloud.be/assets/images/2020-05-16-AZDO-200.png" alt="Successfly read from swagger" /></p>
<p>We also updated our ARM template that deploys our webApps to automatically add the public IP of the Azure Firewall to the allow list.</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nl">"variables"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"newAppServicePlanName"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[take(concat('asp-', parameters('newAppServicePlanPrefix'),'-', uniqueString(parameters('guidValue'))),24)]"</span><span class="p">,</span><span class="w">
</span><span class="nl">"webservicefarmname"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[if(parameters('deployAppServicePlan'),variables('newAppServicePlanName'),parameters('existingAppServicePlanName'))]"</span><span class="p">,</span><span class="w">
</span><span class="nl">"copy"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"WebAppsTidy"</span><span class="p">,</span><span class="w">
</span><span class="nl">"count"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[length(parameters('WebApps'))]"</span><span class="p">,</span><span class="w">
</span><span class="nl">"input"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[take(concat(if(equals(tolower(trim(parameters('WebApps')[copyIndex('WebAppsTidy')].kind)), 'app'),'aps-','api-'),parameters('WebApps')[copyIndex('WebAppsTidy')].name,'-',uniqueString(parameters('guidValue'))),24)]"</span><span class="p">,</span><span class="w">
</span><span class="nl">"Kind"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[parameters('WebApps')[copyIndex('WebAppsTidy')].kind]"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"DevopsIP"</span><span class="p">:</span><span class="w"> </span><span class="s2">"20.x.0.0/16"</span><span class="p">,</span><span class="w">
</span><span class="nl">"PROD-vnetSubnetResourceId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/subscriptions/xxxxxxf/resourceGroups/RG-xxxx/providers/Microsoft.Network/virtualNetworks/xxxx/subnets/xxxx"</span><span class="p">,</span><span class="w">
</span><span class="nl">"AppIpSecurityRestrictions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"ipAddress"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[variables('DevopsIP')]"</span><span class="p">,</span><span class="w">
</span><span class="nl">"action"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Allow"</span><span class="p">,</span><span class="w">
</span><span class="nl">"priority"</span><span class="p">:</span><span class="w"> </span><span class="mi">100</span><span class="p">,</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Allow Devops"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"ApiIpSecurityRestrictions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"vnetSubnetResourceId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[variables('vnetSubnetResourceId')]"</span><span class="p">,</span><span class="w">
</span><span class="nl">"action"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Allow"</span><span class="p">,</span><span class="w">
</span><span class="nl">"priority"</span><span class="p">:</span><span class="w"> </span><span class="mi">200</span><span class="p">,</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Allow APIM"</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="err">,</span><span class="w">
</span><span class="nl">"resources"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"apiVersion"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2019-08-01"</span><span class="p">,</span><span class="w">
</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Microsoft.Web/serverfarms"</span><span class="p">,</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[variables('newAppServicePlanName')]"</span><span class="p">,</span><span class="w">
</span><span class="nl">"condition"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[parameters('deployAppServicePlan')]"</span><span class="p">,</span><span class="w">
</span><span class="nl">"kind"</span><span class="p">:</span><span class="w"> </span><span class="s2">"app"</span><span class="p">,</span><span class="w">
</span><span class="nl">"location"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[parameters('location')]"</span><span class="p">,</span><span class="w">
</span><span class="nl">"properties"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"dependsOn"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="p">],</span><span class="w">
</span><span class="nl">"sku"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[parameters('sku')]"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="p">{</span><span class="w">
</span><span class="nl">"apiVersion"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2019-08-01"</span><span class="p">,</span><span class="w">
</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Microsoft.Web/sites"</span><span class="p">,</span><span class="w">
</span><span class="nl">"copy"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"sitesLoop"</span><span class="p">,</span><span class="w">
</span><span class="nl">"count"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[length(parameters('WebApps'))]"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"kind"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[variables('WebAppsTidy')[copyIndex()].kind]"</span><span class="p">,</span><span class="w">
</span><span class="nl">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[variables('WebAppsTidy')[copyIndex()].name]"</span><span class="p">,</span><span class="w">
</span><span class="nl">"location"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[parameters('location')]"</span><span class="p">,</span><span class="w">
</span><span class="nl">"properties"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"serverFarmId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[resourceId('Microsoft.Web/serverfarms', variables('webservicefarmname'))]"</span><span class="p">,</span><span class="w">
</span><span class="nl">"httpsOnly"</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="p">,</span><span class="w">
</span><span class="nl">"siteConfig"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"ipSecurityRestrictions"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[if(equals(variables('WebAppsTidy')[copyIndex()].kind,'app'),variables('AppIpSecurityRestrictions'),variables('ApiIpSecurityRestrictions'))]"</span><span class="p">,</span><span class="w">
</span><span class="nl">"scmIpSecurityRestrictions"</span><span class="p">:</span><span class="w"> </span><span class="s2">"[if(equals(variables('WebAppsTidy')[copyIndex()].kind,'app'),variables('AppIpSecurityRestrictions'),variables('ApiIpSecurityRestrictions'))]"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"identity"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"SystemAssigned"</span><span class="w">
</span><span class="p">},</span><span class="w">
</span><span class="nl">"dependsOn"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
</span><span class="s2">"[resourceId('Microsoft.Web/serverfarms', variables('webservicefarmname'))]"</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>Hope this helps,
Alex</p>Alexandre VerkinderenBy default, when you deploy a new Azure WebApp, Function app or API app it will be publicly available to the internet. For the current customer I’m working on we made it a standard that all webapps should not be directly publicly available. To enhance our security we deploy Azure Frontdoor and Azure API Management Service for our APIs and also enable IP restrictions. As mentioned in my previous blog post we currently use Azure DevOps with Microsoft hosted agents to build and release all of our web apps and API apps.Use Azure DevOps pipelines for continuous delivery of APIs to Azure API Management Service2020-05-08T00:00:00+10:002020-05-08T00:00:00+10:00https://mscloud.be/azure/Update-API-in-APIM-from-Azure-Devops<p>One of my customers is on a journey to re-architect old on-premises web applications to more modern webApps using APIs. All APIs should use Azure DevOps CI/CD pipelines and will only be exposed through Azure API Management Service. We wanted to ensure that every time a developer has released a new build the API definition in APIM would get updated.</p>
<p>The challenge was that if a new build was created and released, the Swagger file of the APIapp would be updated but not the API definition of the API in APIM. Once you <a href="https://docs.microsoft.com/en-us/azure/api-management/import-and-publish#-import-and-publish-a-backend-api">import and publish</a> an API in APIM it will not be updated automatically. So, even though the developers released a new build, the consumers would still consume the older version of the API published in APIM. If we have 10 APIs with each 3 environments and we do, let’s say, 5 releases a week that’s 150 manual updates to APIM.</p>
<p><em>If we have continuous deployments for our webApps, we want continuous deployments for our APIM as well.</em></p>
<h2 id="introduction">Introduction</h2>
<p>The release contains 3 stages to deploy to DEV, UAT and Production respectively as seen in the diagram below:</p>
<p><img src="https://mscloud.be/assets/images/2020-05-08-APIM-AZDO-APIM.png" alt="AZDO" /></p>
<p>We first looked at the <a href="https://github.com/Azure/azure-api-management-devops-resource-kit">APIM DevOps Resource kit</a> to keep the APIs in sync. But that was overly complicated for what we wanted to achieve. A quick search in the Azure DevOps marketplace revealed the following extensions from <a href="https://marketplace.visualstudio.com/items?itemName=stephane-eyskens.apim">Stephan Eyskens</a>.</p>
<h2 id="setup-the-pipeline">Setup the pipeline</h2>
<p>First, <a href="https://marketplace.visualstudio.com/items?itemName=stephane-eyskens.apim">install the extension</a> in your Azure DevOps environment.</p>
<p>Next, add the following tasks to your pipeline:</p>
<ul>
<li>
<p><strong>Download Build Artifact</strong>. As we are working with a multi-staged pipeline with multiple jobs we can’t refer to the drop file being created as part of our Build stage. To solve this, we need to download the build artifact in every release stage.</p>
</li>
<li>
<p><strong>AzureRmWebAppDeployment</strong>. This task will take the downloaded build artifact and deploy it to our API app in Azure.</p>
</li>
<li>
<p><strong>PowerShell task</strong> to parse the Swagger file. This task will parse our Swagger JSON file and extract things like the name, display name etc of our API. It happened to me before that a Dev guy would come up with a name, we would setup the pipeline but once the business saw the name they wanted it changed so I had to change my pipeline or my variables. Now, if the Dev guy changes something in the Swagger file it will get processed and the API in APIM will change automatically.</p>
</li>
<li>
<p><strong>Create/Update API</strong>. This task will create or update the API in APIM based on the Swagger file and also set different API policies for dev, uat and prod. This task will also update the API in APIM in case the Dev guy will create new API methods or remove API methods.</p>
</li>
<li>
<p>Create 3 variable groups with the following settings.</p>
</li>
</ul>
<p><img src="https://mscloud.be/assets/images/2020-05-08-APIM-AZDO-variablegroup.png" alt="Variable Group" /></p>
<blockquote>
<p><strong>NOTE</strong>
I’m not going to explain how to setup a build for your dot Net solution. I assume you already have this in place.</p>
</blockquote>
<p>Copy the following yaml content into your pipeline.</p>
<div class="language-yml 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">PROD</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Release</span><span class="nv"> </span><span class="s">to</span><span class="nv"> </span><span class="s">PROD"</span>
<span class="na">condition</span><span class="pi">:</span> <span class="s">succeeded('UAT')</span>
<span class="na">variables</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">group</span><span class="pi">:</span> <span class="s">Prod</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">PROD</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">to</span><span class="nv"> </span><span class="s">Prod"</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">DownloadBuildArtifacts@0</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Download</span><span class="nv"> </span><span class="s">Build</span><span class="nv"> </span><span class="s">Artifact"</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">buildType</span><span class="pi">:</span> <span class="s1">'</span><span class="s">current'</span>
<span class="na">downloadType</span><span class="pi">:</span> <span class="s1">'</span><span class="s">single'</span>
<span class="na">artifactName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">drop'</span>
<span class="na">downloadPath</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(System.ArtifactsDirectory)'</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">AzureRmWebAppDeployment@4</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">to</span><span class="nv"> </span><span class="s">Azure</span><span class="nv"> </span><span class="s">APIapp"</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">ConnectionType</span><span class="pi">:</span> <span class="s1">'</span><span class="s">AzureRM'</span>
<span class="na">azureSubscription</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Microsoft</span><span class="nv"> </span><span class="s">Azure</span><span class="nv"> </span><span class="s">Sponsorship</span><span class="nv"> </span><span class="s">New'</span>
<span class="na">appType</span><span class="pi">:</span> <span class="s1">'</span><span class="s">apiApp'</span>
<span class="na">WebAppName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">todotapim'</span>
<span class="na">packageForLinux</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(System.ArtifactsDirectory)/drop/*.zip'</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">PowerShell@2</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Parse</span><span class="nv"> </span><span class="s">Swagger</span><span class="nv"> </span><span class="s">file"</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">targetType</span><span class="pi">:</span> <span class="s1">'</span><span class="s">inline'</span>
<span class="na">script</span><span class="pi">:</span> <span class="pi">|</span>
<span class="s">Write-Output '$(Swagger)'</span>
<span class="s">$swagger = Invoke-WebRequest '$(Swagger)' -UseBasicParsing | convertfrom-json</span>
<span class="s">$value = $swagger.info.title</span>
<span class="s">Write-Output "##vso[task.setvariable variable=Output_Title]$value"</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">stephane-eyskens.apim.apim.apim@3</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">API</span><span class="nv"> </span><span class="s">Management</span><span class="nv"> </span><span class="s">-</span><span class="nv"> </span><span class="s">Create/Update</span><span class="nv"> </span><span class="s">API"</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">ConnectedServiceNameARM</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Microsoft</span><span class="nv"> </span><span class="s">Azure</span><span class="nv"> </span><span class="s">Sponsorship</span><span class="nv"> </span><span class="s">New'</span>
<span class="na">ResourceGroupName</span><span class="pi">:</span> <span class="s">$(ResourceGroupName)</span>
<span class="na">ApiPortalName</span><span class="pi">:</span> <span class="s">$(ApiPortalName)</span>
<span class="na">UseProductCreatedByPreviousTask</span><span class="pi">:</span> <span class="no">false</span>
<span class="na">product1</span><span class="pi">:</span> <span class="s">$(product)</span>
<span class="na">OpenAPISpec</span><span class="pi">:</span> <span class="s">v3</span>
<span class="na">swaggerlocation</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(Swagger)'</span>
<span class="na">targetapi</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(targetapi)'</span>
<span class="na">pathapi</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(pathapi)'</span>
<span class="na">DisplayName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(Output_Title)'</span>
<span class="na">TemplateSelector</span><span class="pi">:</span> <span class="s">Custom</span>
<span class="na">Custom</span><span class="pi">:</span> <span class="pi">|</span>
<span class="s"><policies></span>
<span class="s"><inbound></span>
<span class="s"><base /></span>
<span class="s"><rate-limit calls=$(rate-limit-calls) renewal-period=$(renewal-period) /></span>
<span class="s"></inbound></span>
<span class="s"><backend></span>
<span class="s"><base /></span>
<span class="s"></backend></span>
<span class="s"><outbound></span>
<span class="s"><base /></span>
<span class="s"></outbound></span>
<span class="s"><on-error></span>
<span class="s"><base /></span>
<span class="s"></on-error></span>
<span class="s"></policies></span>
<span class="na">MicrosoftApiManagementAPIVersion</span><span class="pi">:</span> <span class="s1">'</span><span class="s">2018-01-01'</span>
</code></pre></div></div>
<blockquote>
<ul>
<li>Please note that the API task from the market place is not fully compatible with YAML pipelines yet. It works perfectly fine but the pipeline is going to highlight it as not supported.</li>
<li>You will also not be able to edit the settings of the task in your YAML pipeline. A work around for this is to create a classic pipeline, add the task, configure the settings and copy the YAML configuration.</li>
<li>I also struggled a bit with the APIM policy. YAML pipelines don’t like XML code inside the YAML file. The work around for this was to set the integer values in the variable groups as per screenshot above.</li>
</ul>
</blockquote>
<p>Let’s trigger our pipeline.</p>
<p><img src="https://mscloud.be/assets/images/2020-05-08-APIM-AZDO-PipelineOverview.png" alt="Pipeline Overview" /></p>
<h2 id="conclusion">Conclusion</h2>
<p>As you can see our APIs have been created/updated in APIM and our policies have been set as well. :smiley:</p>
<p><img src="https://mscloud.be/assets/images/2020-05-08-APIM-APIs.png" alt="Pipeline Overview" /></p>
<p>Now, every time a developer is releasing a new version of the API the API definition in APIM will be updated.</p>
<p>Thanks,</p>
<p>Alex</p>Alexandre VerkinderenOne of my customers is on a journey to re-architect old on-premises web applications to more modern webApps using APIs. All APIs should use Azure DevOps CI/CD pipelines and will only be exposed through Azure API Management Service. We wanted to ensure that every time a developer has released a new build the API definition in APIM would get updated.Enable Azure AD authentication for API Management Service Developer Portal2020-04-30T00:00:00+10:002020-04-30T00:00:00+10:00https://mscloud.be/azure/Enable-AzureAD-for-APIM-portal<p>We use Azure Api Management Service (APIM) quite a lot and recently I have been looking at the new <a href="https://docs.microsoft.com/en-us/azure/api-management/api-management-howto-developer-portal">APIM Developer portal</a> and how to enable Azure Active Directory authentication for the new portal.</p>
<h2 id="requirements">Requirements</h2>
<p>To be able to achieve this we will need to manually register a new application in Azure AD. This app will then be used to authenticate against Azure AD from our APIM developer portal. The following list is a list of requirements needed:</p>
<ul>
<li>Client ID of new Azure AD application</li>
<li>Client Secret of new Azure AD application</li>
<li>Redirect URL of Azure APIM</li>
<li>Permissions to register new applications in Azure AD</li>
</ul>
<h2 id="add-azure-ad-identity-provider-in-apim">Add Azure AD identity provider in APIM</h2>
<p>First, navigate to your APIM instance, and select <strong>Identities</strong> under the <strong>Developer portal</strong> settings</p>
<p><img src="https://mscloud.be/assets/images/2020-05-01-APIM-Addidentity.png" alt="Add Identity" /></p>
<p>Click add new identity and select <strong>Azure Active Directory</strong> and copy the <strong>Redirect URL</strong>. We will need this later when we create our Azure AD application.</p>
<p><img src="https://mscloud.be/assets/images/2020-05-01-APIM-redirecturl.png" alt="Redirect URL" /></p>
<blockquote>
<p>Make sure you don’t copy the legacy redirect URL if you are using the new APIM portal like me.</p>
</blockquote>
<p>Don’t close this window just yet. We will need to fill in the ClientID and Client Secret of the new app we are about to register in Azure AD.</p>
<h2 id="register-a-new-application-in-azure-ad">Register a new application in Azure AD</h2>
<p>Open a new tab and <a href="https://go.microsoft.com/fwlink/?linkid=2083908">register</a> a new app in Azure AD. Select <strong>New Registration</strong>. Give your new app a meaningful <strong>name</strong> , select “Accounts in this organizational directory only” and paste the <strong>redirect url</strong> from APIM and press <strong>register</strong>.</p>
<p><img src="https://mscloud.be/assets/images/2020-05-01-APIM-RegisterAPP.png" alt="Register APP in Azure AD" /></p>
<p>Once the app is registered, copy the <strong>ClientID</strong></p>
<p><img src="https://mscloud.be/assets/images/2020-05-01-APIM-APPClientID.png" alt="Client ID" /></p>
<p>Go to Certificate and Secrets and <strong>create</strong> a new Secret. Once created, copy the Client Secret.</p>
<p><img src="https://mscloud.be/assets/images/2020-05-01-APIM-APPSecret.png" alt="Client Secret" /></p>
<p>There is one last thing we should do in our newly created Azure AD app before switching back to our APIM. Click on <strong>Authentication</strong> and select <strong>ID Tokens</strong>.</p>
<p><img src="https://mscloud.be/assets/images/2020-05-01-APIM-APPIDTokens.png" alt="ID Tokens" /></p>
<h2 id="finalize-azure-ad-identity-provider-in-apim">Finalize Azure AD identity provider in APIM</h2>
<p>Now, let’s switch back to our APIM and fill in all the necessary information. Paste the ClientID and Client Secret from the previous step and press <strong>Add</strong>.</p>
<p><img src="https://mscloud.be/assets/images/2020-05-01-APIM-Appinfo.png" alt="Add info" /></p>
<p>At this stage you can already try and login to your APIM with Azure AD. Go to https:yoururl/signin</p>
<p><img src="https://mscloud.be/assets/images/2020-05-01-APIM-portal-signin.png" alt="AD Sign in" /></p>
<p>You will see that the sign in page is using the <strong>Sign-in button: OAuth widget</strong> for Azure AD authentication. After sign in the user will be prompted to complete the sign up process.</p>
<h2 id="modify-the-apim-portal-for-azure-ad-authentication">Modify the APIM portal for Azure AD authentication</h2>
<p>I want all my users to use Azure AD authentication instead of Basic authentication. So I customized the APIM portal and removed all <strong>Basic oAuth widgets</strong> from the portal.</p>
<p><img src="https://mscloud.be/assets/images/2020-05-01-APIM-portal-RemoveBasic.png" alt="Remove Basic oAuth" /></p>
<h2 id="conclusion">Conclusion</h2>
<p>As you can see it’s not complicated to enable Azure AD for the APIM portal. Using Azure AD also means I can now use all the security features in Azure AD like conditional access and MFA for my developers.</p>
<p>Alex</p>Alexandre VerkinderenWe use Azure Api Management Service (APIM) quite a lot and recently I have been looking at the new APIM Developer portal and how to enable Azure Active Directory authentication for the new portal.Dynamically create README Files from Azure DevOps Pipeline and Commit to Repository2020-04-26T00:00:00+10:002020-04-26T00:00:00+10:00https://mscloud.be/azure/Dynamically-create-README-from-azdo-pipeline-and-commit-repository<p>Bernie White has a Powershell Module (<a href="https://github.com/BernieWhite/PSDocs">PSDocs</a>) that can generate mark down files (*.md) and Stefan Stranger’s <a href="https://stefanstranger.github.io/2020/04/12/CreatingAzureDevOpsWIKIPagesFromWithApipeline/">blog post</a> shows us how to upload these to Azure DevOps Wiki. We started investigating this as we saw this being a great feature to automate the creation and maintenance of our README.md files within our IaC Templates. The only issue is that our README.md files live side by side with our ARM Templates in the Azure DevOps Repositories and not in the Wiki section that Stefan’s post updates.
So the challenge is, how do we make our Azure Pipelines write back the README.md files it dynamically creates on the build agent to the repository?</p>
<p>The solution was to use Git within the pipeline to commit the new or updated README file to the repository.</p>
<h2 id="requirements">Requirements</h2>
<p>First thing we found was this Microsoft <a href="https://docs.microsoft.com/en-us/azure/devops/pipelines/scripts/git-commands?view=azure-devops&tabs=yaml">documentation</a> which specifies additional access requirements for the <strong>Project Collection Build Service</strong>. However, we found that our pipelines were using the <code class="language-plaintext highlighter-rouge"><Project>'s Build Service (<Organisation>)</code> account instead and therefore we had to assign the additional access to that account.</p>
<blockquote>
<p>Note: Permissions “Read” and “Create Tag” where already Inherited so not need to reassign again.</p>
</blockquote>
<p><img src="https://mscloud.be/assets/images/2020-04-24-Permissions.png" alt="create access" /></p>
<h2 id="create-readme-template-and-calling-script">Create ReadMe Template and Calling Script</h2>
<p>Next, we created our own PowerShell template file called <code class="language-plaintext highlighter-rouge">ReadMe.doc.ps1</code> using Bernie’s <a href="https://github.com/BernieWhite/PSDocs/blob/master/docs/scenarios/arm-template/arm-template.doc.ps1">ARM Template example</a> as a base. For more information regarding how to create the template PowerShell script have a look at Bernie’s example.</p>
<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#</span><span class="w">
</span><span class="c"># Azure Resource Manager documentation definitions</span><span class="w">
</span><span class="c">#</span><span class="w">
</span><span class="c"># A function to break out parameters from an ARM template</span><span class="w">
</span><span class="kr">function</span><span class="w"> </span><span class="nf">GetTemplateParameter</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="p">(</span><span class="w">
</span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$True</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">$Path</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="kr">process</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$template</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="nv">$Path</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertFrom-Json</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">$property</span><span class="w"> </span><span class="kr">in</span><span class="w"> </span><span class="nv">$template</span><span class="o">.</span><span class="nf">parameters</span><span class="o">.</span><span class="nf">PSObject</span><span class="o">.</span><span class="nf">Properties</span><span class="p">)</span><span class="w"> </span><span class="p">{</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">$property</span><span class="err">.</span><span class="nx">Name</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">$property</span><span class="err">.</span><span class="nx">Value</span><span class="err">.</span><span class="nx">metadata</span><span class="err">.</span><span class="nx">description</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="c"># A function to import metadata</span><span class="w">
</span><span class="kr">function</span><span class="w"> </span><span class="nf">GetTemplateMetadata</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="kr">param</span><span class="w"> </span><span class="p">(</span><span class="w">
</span><span class="p">[</span><span class="n">Parameter</span><span class="p">(</span><span class="n">Mandatory</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$True</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">$Path</span><span class="w">
</span><span class="p">)</span><span class="w">
</span><span class="kr">process</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$metadata</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="nv">$Path</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">ConvertFrom-Json</span><span class="p">;</span><span class="w">
</span><span class="kr">return</span><span class="w"> </span><span class="nv">$metadata</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="c"># Description: A definition to generate markdown for an ARM template</span><span class="w">
</span><span class="n">document</span><span class="w"> </span><span class="s1">'README'</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="c"># Read JSON files</span><span class="w">
</span><span class="nv">$metadata</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">GetTemplateMetadata</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$InputObject</span><span class="o">.</span><span class="nf">MetadataFile</span><span class="p">;</span><span class="w">
</span><span class="nv">$parameters</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">GetTemplateParameter</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="nv">$InputObject</span><span class="o">.</span><span class="nf">ARMTemplate</span><span class="p">;</span><span class="w">
</span><span class="s2">"[![Build Status](https://dev.azure.com/#######/IaC/_apis/build/status/</span><span class="si">$(</span><span class="nv">$InputObject</span><span class="o">.</span><span class="nf">PipelineName</span><span class="si">)</span><span class="s2">?branchName=Dev)](https://dev.azure.com/#######/IaC/_build/latest?definitionId=</span><span class="si">$(</span><span class="nv">$InputObject</span><span class="o">.</span><span class="nf">PipelineID</span><span class="si">)</span><span class="s2">&branchName=Dev)"</span><span class="w">
</span><span class="c"># Set document title</span><span class="w">
</span><span class="n">Title</span><span class="w"> </span><span class="nv">$metadata</span><span class="o">.</span><span class="nf">itemDisplayName</span><span class="w">
</span><span class="c"># Write opening line</span><span class="w">
</span><span class="nv">$metadata</span><span class="o">.</span><span class="nf">Description</span><span class="w">
</span><span class="c"># Add each parameter to a table</span><span class="w">
</span><span class="n">Section</span><span class="w"> </span><span class="s1">'Parameters'</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="nv">$parameters</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Table</span><span class="w"> </span><span class="nt">-Property</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="s1">'Parameter name'</span><span class="p">;</span><span class="w"> </span><span class="nx">Expression</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="bp">$_</span><span class="o">.</span><span class="nf">Name</span><span class="w"> </span><span class="p">}},</span><span class="nx">Description</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="c"># Generate example command line</span><span class="w">
</span><span class="n">Section</span><span class="w"> </span><span class="s1">'Use the template'</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="n">Section</span><span class="w"> </span><span class="s1">'PowerShell'</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s1">'New-AzResourceGroupDeployment -Name <deployment-name> -ResourceGroupName <resource-group-name> -TemplateFile <path-to-template>'</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Code</span><span class="w"> </span><span class="nx">powershell</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="n">Section</span><span class="w"> </span><span class="s1">'Azure CLI'</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s1">'az group deployment create --name <deployment-name> --resource-group <resource-group-name> --template-file <path-to-template>'</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Code</span><span class="w"> </span><span class="nx">text</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="n">Section</span><span class="w"> </span><span class="s1">'Azure Custom Template'</span><span class="w"> </span><span class="p">{</span><span class="w">
</span><span class="s2">"[![Deploy to Azure](https://azuredeploy.net/deploybutton.png)](https://portal.azure.com/#create/Microsoft.Template/uri/</span><span class="si">$(</span><span class="nv">$InputObject</span><span class="o">.</span><span class="nf">LinkedTemplateURI</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="p">}</span><span class="w">
</span></code></pre></div></div>
<p>We will also create a PowerShell script called <code class="language-plaintext highlighter-rouge">CreateReadMe.ps1</code> that will call the <code class="language-plaintext highlighter-rouge">ReadMe.doc.ps1</code> and pass on the required parameters like our ARM template file and metadafile.</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="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="o">=</span><span class="bp">$true</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">$WorkingDir</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="o">=</span><span class="bp">$true</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">$PipelineName</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="o">=</span><span class="bp">$true</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">$PipelineID</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">$DocTemplatePath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'000-Scripts\ReadMe.doc.ps1'</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">$LinkedTemplatePath</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">'https://<storageaccount>.blob.core.windows.net/arm/'</span><span class="w">
</span><span class="p">)</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">PSDocs</span><span class="w"> </span><span class="nt">-Force</span><span class="w">
</span><span class="nv">$templatefile</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"</span><span class="nv">$WorkingDir</span><span class="s2">\</span><span class="nv">$PipelineName</span><span class="s2">\azuredeploy.json"</span><span class="w">
</span><span class="nv">$metadatafile</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"</span><span class="nv">$WorkingDir</span><span class="s2">\</span><span class="nv">$PipelineName</span><span class="s2">\metadata.json"</span><span class="w">
</span><span class="nv">$PSDocsInputObject</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">New-Object</span><span class="w"> </span><span class="nx">PsObject</span><span class="w"> </span><span class="nt">-property</span><span class="w"> </span><span class="p">@{</span><span class="w">
</span><span class="s1">'MetadataFile'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$metadatafile</span><span class="w">
</span><span class="s1">'ARMTemplate'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$templatefile</span><span class="w">
</span><span class="s1">'LinkedTemplateURI'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="n">uri</span><span class="p">]</span><span class="err">::</span><span class="nx">EscapeDataString</span><span class="err">(</span><span class="nv">$LinkedTemplatePath</span><span class="w"> </span><span class="err">+</span><span class="w"> </span><span class="nv">$PipelineName</span><span class="w"> </span><span class="err">+</span><span class="s2">"/azuredeploy.json"</span><span class="err">)</span><span class="w">
</span><span class="s1">'PipelineID'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$PipelineID</span><span class="w">
</span><span class="s1">'PipelineName'</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">$PipelineName</span><span class="w">
</span><span class="p">}</span><span class="w">
</span><span class="n">Invoke-PSDocument</span><span class="w"> </span><span class="nt">-Path</span><span class="w"> </span><span class="s2">"</span><span class="nv">$WorkingDir</span><span class="s2">\</span><span class="nv">$DocTemplatePath</span><span class="s2">"</span><span class="w"> </span><span class="nt">-InputObject</span><span class="w"> </span><span class="nv">$PSDocsInputObject</span><span class="w"> </span><span class="nt">-OutputPath</span><span class="w"> </span><span class="s2">"</span><span class="nv">$WorkingDir</span><span class="s2">\</span><span class="nv">$PipelineName</span><span class="s2">"</span><span class="w"> </span><span class="nt">-Instance</span><span class="w"> </span><span class="nx">README</span><span class="w">
</span></code></pre></div></div>
<h2 id="create-a-bash-script-to-run-the-git-commands-and-commit-the-new-readme-file-to-the-repository">Create a BASH script to run the git commands and commit the new README file to the repository</h2>
<p>Last we also need to create a BASH script called <code class="language-plaintext highlighter-rouge">GITCommitReadMeFile.sh</code>. This will run the necessary git commands needed to upload our new README file to our repository.</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">BRANCH</span><span class="o">=</span><span class="nv">$1</span>
<span class="nv">FILETOCOMMIT</span><span class="o">=</span><span class="nv">$2</span>
<span class="nv">BUILDNAME</span><span class="o">=</span><span class="nv">$3</span>
ECHO <span class="s2">"Setting git config..."</span>
git config <span class="nt">--global</span> user.email <span class="s2">"IaCBuildServiceAccount@<domain>"</span>
git config <span class="nt">--global</span> user.name <span class="s2">"IaC Build Service Account"</span>
ECHO <span class="s2">"CHECK GIT STATUS..."</span>
git status
ECHO <span class="s2">"CHECK OUT BRANCH..."</span>
git checkout <span class="nt">-b</span> <span class="nv">$BRANCH</span>
ECHO <span class="s2">"GIT ADD..."</span>
git add <span class="nv">$FILETOCOMMIT</span>
ECHO <span class="s2">"Commiting the changes..."</span>
git commit <span class="nt">-m</span> <span class="s2">"ReadMe Update from Build </span><span class="nv">$BUILDNAME</span><span class="s2">"</span>
ECHO <span class="s2">"Pushing the changes..."</span>
git push <span class="nt">-u</span> origin <span class="nv">$BRANCH</span>
ECHO <span class="s2">"CHECK GIT STATUS..."</span>
git status
</code></pre></div></div>
<blockquote>
<p>Note: We are using <code class="language-plaintext highlighter-rouge">git add $FILETOCOMMIT</code> to only commit the README.md file to minimise the risk of this process completely destroying our repository.</p>
</blockquote>
<h2 id="creating-a-pipeline-job">Creating a Pipeline Job</h2>
<p>Let’s put everything together into a pipeline job. The first task will call the Readme.doc.ps1 file with the required parameters as mentioned above, the second task will add and commit the new file to our repository.</p>
<div class="language-yml highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="pi">-</span> <span class="na">job</span><span class="pi">:</span> <span class="s">UpdateReadMe</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s2">"</span><span class="s">Update</span><span class="nv"> </span><span class="s">Template</span><span class="nv"> </span><span class="s">ReadMe</span><span class="nv"> </span><span class="s">File"</span>
<span class="na">steps</span><span class="pi">:</span>
<span class="pi">-</span> <span class="na">checkout</span><span class="pi">:</span> <span class="s">self</span>
<span class="na">persistCredentials</span><span class="pi">:</span> <span class="no">true</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">PowerShell@2</span>
<span class="na">name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">CreateReadMe'</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Create</span><span class="nv"> </span><span class="s">ReadMe</span><span class="nv"> </span><span class="s">File'</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">filePath</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(System.DefaultWorkingDirectory)\000-Scripts\CreateReadMe.ps1'</span>
<span class="na">arguments</span><span class="pi">:</span> <span class="s1">'</span><span class="s">-WorkingDir</span><span class="nv"> </span><span class="s">$(System.DefaultWorkingDirectory)</span><span class="nv"> </span><span class="s">-PipelineName</span><span class="nv"> </span><span class="s">$(Build.DefinitionName)</span><span class="nv"> </span><span class="s">-PipelineID</span><span class="nv"> </span><span class="s">$(System.DefinitionId)</span><span class="nv"> </span><span class="s">-DocTemplatePath</span><span class="nv"> </span><span class="s">000-Scripts\ReadMe.doc.ps1</span><span class="nv"> </span><span class="s">-LinkedTemplatePath</span><span class="nv"> </span><span class="s">$(LinkedTemplatePrefix)'</span>
<span class="na">errorActionPreference</span><span class="pi">:</span> <span class="s1">'</span><span class="s">silentlyContinue'</span>
<span class="pi">-</span> <span class="na">task</span><span class="pi">:</span> <span class="s">Bash@3</span>
<span class="na">name</span><span class="pi">:</span> <span class="s1">'</span><span class="s">CommitReadMe'</span>
<span class="na">displayName</span><span class="pi">:</span> <span class="s1">'</span><span class="s">Upload</span><span class="nv"> </span><span class="s">ReadMe</span><span class="nv"> </span><span class="s">File'</span>
<span class="na">inputs</span><span class="pi">:</span>
<span class="na">filePath</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(System.DefaultWorkingDirectory)\000-Scripts\GITCommitReadMeFile.sh'</span>
<span class="na">arguments</span><span class="pi">:</span> <span class="s1">'</span><span class="s">$(Build.SourceBranchName)</span><span class="nv"> </span><span class="s">"$(Build.DefinitionName)/README.md"</span><span class="nv"> </span><span class="s">$(Build.BuildNumber)'</span>
</code></pre></div></div>
<h2 id="running-the-pipeline">Running the pipeline</h2>
<p>Now we kick off our pipeline and have a look at the results.
<img src="https://mscloud.be/assets/images/2020-04-24-PipelineExecution.png" alt="Pipeline Execution" /></p>
<p>We can see that after the pipeline completes the README.md file has been updated.
<img src="https://mscloud.be/assets/images/2020-04-24-ReadMeFile.png" alt="ReadMe File" /></p>Rodney AlmeidaBernie White has a Powershell Module (PSDocs) that can generate mark down files (*.md) and Stefan Stranger’s blog post shows us how to upload these to Azure DevOps Wiki. We started investigating this as we saw this being a great feature to automate the creation and maintenance of our README.md files within our IaC Templates. The only issue is that our README.md files live side by side with our ARM Templates in the Azure DevOps Repositories and not in the Wiki section that Stefan’s post updates. So the challenge is, how do we make our Azure Pipelines write back the README.md files it dynamically creates on the build agent to the repository?