<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Massimiliano Donini</title>
    <description>The latest articles on DEV Community by Massimiliano Donini (@maxx_don).</description>
    <link>https://dev.to/maxx_don</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F329638%2F46fab9e1-ad82-4391-87b2-8f14a6cde3c6.png</url>
      <title>DEV Community: Massimiliano Donini</title>
      <link>https://dev.to/maxx_don</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maxx_don"/>
    <language>en</language>
    <item>
      <title>Publish Dacpac to Azure SQL with Entra-only authentication using GitHub Actions</title>
      <dc:creator>Massimiliano Donini</dc:creator>
      <pubDate>Fri, 23 Aug 2024 13:01:19 +0000</pubDate>
      <link>https://dev.to/maxx_don/publish-dacpac-to-azure-sql-with-entra-only-authentication-using-github-actions-56pp</link>
      <guid>https://dev.to/maxx_don/publish-dacpac-to-azure-sql-with-entra-only-authentication-using-github-actions-56pp</guid>
      <description>&lt;p&gt;In Azure, most services nowadays supports Microsoft Entra based authentication, for example via a system managed identity or a user assigned one.&lt;br&gt;
This authentication method is preferred to the old connection string based one because it gets rids of secrets and with that the need of secret rotation.&lt;/p&gt;

&lt;p&gt;Some services then push this concept even further and allow you to completely disable a secret based connection, Azure SQL is one of them. When Entra-only authentication is enabled it's pretty easy to configure a service running in Azure to connect to the service in question but what about the CI/CD pipelines?&lt;/p&gt;

&lt;p&gt;This article explains how to connect to Azure SQL when Microsoft Entra-only authentication is enabled.&lt;/p&gt;
&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Create a service principal used by GitHub actions to connect to the Azure subscription. This can be created with or without using a shared secret. Using the shared secret option is easier but also less secure and can be achieved with the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az ad sp create-for-rbac &lt;span class="nt"&gt;-n&lt;/span&gt; YourServicePrincipalNameHere &lt;span class="nt"&gt;--role&lt;/span&gt; Owner &lt;span class="nt"&gt;--scopes&lt;/span&gt; /subscriptions/YourSubcriptionIdHere
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;If you want to use the secretless approach, you can refer to my other post here that explains step-by-step how to set up and configure a Microsoft Entra application and configure a federated identity credential on the application &lt;a href="https://maxdon.tech/posts/github-azure-oidc/" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TIP&lt;/strong&gt; The article above uses an App registration with a service principal, the same can also be achieved via a user-assigned managed identity. You can find more details on the Microsoft &lt;a href="https://learn.microsoft.com/en-us/azure/developer/github/connect-from-azure-openid-connect" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Enable Entra-only authentication
&lt;/h2&gt;

&lt;p&gt;Enable Entra-only authentication has to be done at the Azure SQL Server level and can be achieved in several ways:&lt;/p&gt;
&lt;h3&gt;
  
  
  Terraform azurerm_mssql_server
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_mssql_server"&lt;/span&gt; &lt;span class="s2"&gt;"sql_server"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;server_name&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;rg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;
  &lt;span class="nx"&gt;version&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"12.0"&lt;/span&gt;
  &lt;span class="nx"&gt;minimum_tls_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1.2"&lt;/span&gt;

  &lt;span class="nx"&gt;azuread_administrator&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;login_username&lt;/span&gt;              &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;admin_username&lt;/span&gt;
    &lt;span class="nx"&gt;object_id&lt;/span&gt;                   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;admin_object_id&lt;/span&gt;
    &lt;span class="nx"&gt;azuread_authentication_only&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;                  &lt;span class="c1"&gt;# Add this to enable Entra-only authentication&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  AZ CLI
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az sql server ad-only-auth &lt;span class="nb"&gt;enable&lt;/span&gt; &lt;span class="nt"&gt;--resource-group&lt;/span&gt; mygroup &lt;span class="nt"&gt;--name&lt;/span&gt; myServer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Azure Portal
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F97e3ty1ak8npcg1hbyy4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F97e3ty1ak8npcg1hbyy4.png" alt="Configure Entra-only authentication in the Azure portal" width="800" height="402"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Create a SQL user
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Manual script
&lt;/h3&gt;

&lt;p&gt;Now we need to create the user in the SQL Database that represent the service principal and grant the necessary permissions, we achieve this by running the following script:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ServicePrincipalName&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;EXTERNAL&lt;/span&gt; &lt;span class="n"&gt;PROVIDER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;db_owner&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="n"&gt;MEMBER&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ServicePrincipalName&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="c1"&gt;-- Depending on your requirements you can also use a less privileged role, e.g. db_ddladmin&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; When creating a user mapped to an Azure service principal (e.g., when using FROM EXTERNAL PROVIDER), you must connect to the database using Microsoft Entra authentication. If you try to run this script using a regular username and password connection, you will get an error message like the following:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Failed to execute query. Error: Principal 'ServicePrincipalName' could not be created.&lt;br&gt;
Only connections established with Active Directory accounts can create other Active Directory users.&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Terraform mssql_user
&lt;/h3&gt;

&lt;p&gt;The official Azure Terraform provider, azurerm, doesn't support creating Azure SQL users. However there's a &lt;a href="https://registry.terraform.io/providers/betr-io/mssql/latest" rel="noopener noreferrer"&gt;community provider&lt;/a&gt; that does support Azure SQL user creation.&lt;/p&gt;

&lt;p&gt;If you apply your infrastructure changes manually, configuring this might be worthwhile. However, if you use CI/CD pipelines to apply changes, it creates a chicken-and-egg problem since mssql_user still requires an Entra connection to the Azure SQL database.&lt;/p&gt;
&lt;h2&gt;
  
  
  GitHub action workflow
&lt;/h2&gt;

&lt;p&gt;Now that everything is set up, we can examine the action workflow. My workflow is a simplified example that generates the DACPAC, connects to Azure, and publishes it.&lt;/p&gt;

&lt;p&gt;In a more realistic setup, the workflow might be divided into multiple jobs. The first job would build the DACPAC and create an artifact, while the subsequent jobs would deploy the same artifact across different environments, with steps like approval, baking time, and so on.&lt;/p&gt;
&lt;h3&gt;
  
  
  Building the Dacpac
&lt;/h3&gt;

&lt;p&gt;For this demo, I've used the community built &lt;strong&gt;MSBuild.Sdk.SqlProj&lt;/strong&gt;, but the same can be achieved with a regular database project created in Visual Studio. This SDK uses regular csproj to generate a dacpac hence it's very convenient to use because all it takes is just a simple command:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet build path-to-the-project.csproj &lt;span class="nt"&gt;--configuration&lt;/span&gt; Release
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h3&gt;
  
  
  Get the connection string
&lt;/h3&gt;

&lt;p&gt;Since the connection string does not contain secrets anymore, there’s no need to put it in a GitHub Action secret. So for the sake of simplicity in this example a connection string is created in a workflow step, but you can create it in any way you like.&lt;/p&gt;

&lt;p&gt;The connection string format should be the following:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&lt;br&gt;
Server=tcp:{SqlServerName}.database.windows.net,1433; Initial Catalog={DatabaseName}; Authentication=Active Directory Default; Encrypt=True; TrustServerCertificate=False; Connection Timeout=30;"&lt;br&gt;
&lt;/code&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt; It would be nice if &lt;a href="https://learn.microsoft.com/en-us/cli/azure/sql/db?view=azure-cli-latest#az-sql-db-show-connection-string" rel="noopener noreferrer"&gt;az cli&lt;/a&gt; had support for creating the correct connection string but, as of now, this is not supported.&lt;/p&gt;

&lt;p&gt;Make sure you replace &lt;strong&gt;SqlServerName&lt;/strong&gt; and &lt;strong&gt;DatabaseName&lt;/strong&gt; with the appropriate values.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Publishing the dacpac
&lt;/h3&gt;

&lt;p&gt;Publish the dacpac can be done with the azure/sql-action, this is an action that wraps sqlcmd and provided by Microsoft. The action requires a connection string, the dacpac file and the action to perform.&lt;/p&gt;

&lt;p&gt;Here below, you can see the interesting part of the workflow:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;timeout-minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup dotnet SDK&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-dotnet@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Create Dacpac&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dotnet build Database/Database.csproj --configuration Release -o ./out&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Login to Azure&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/login@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;creds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_CREDENTIALS }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Construct connection string&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;connection_string="Server=tcp:${{ vars.SQL_SERVER }}.database.windows.net,1433; Initial Catalog=${{ vars.SQL_DATABASE }}; Authentication=Active Directory Default; Encrypt=True; TrustServerCertificate=False; Connection Timeout=30;"&lt;/span&gt;
          &lt;span class="s"&gt;echo "::add-mask::$connection_string"&lt;/span&gt;
          &lt;span class="s"&gt;echo "connection_string=$connection_string" &amp;gt;&amp;gt; $GITHUB_ENV&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy Database&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/sql-action@v2.3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;connection-string&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ env.connection_string }}&lt;/span&gt;
          &lt;span class="na"&gt;action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;publish'&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./out/Database.dacpac&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Logout of Azure&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;az logout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;And now what's left is just running the action itself!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiyvkgukds1l4kvwymgcc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fiyvkgukds1l4kvwymgcc.png" alt="GitHub Action workflow run" width="742" height="794"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All the code for this blog post can be found here:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--A9-wwsHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/ilmax" rel="noopener noreferrer"&gt;
        ilmax
      &lt;/a&gt; / &lt;a href="https://github.com/ilmax/azure-sql-entra-only-id" rel="noopener noreferrer"&gt;
        azure-sql-entra-only-id
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Sample code that demonstrates how to deploy a dacpac to Azure SQL with Entra-only authentication enableddeploy from GitHub Action linux runners
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;p&gt;This repository contains the accompaignig code for the blog post published &lt;a href="https://maxdon.tech/posts/gha-publish-dacpac-entra-only-auth/" rel="nofollow noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It shows how to deploy Azure SQL database using dacpac and dotnet via GitHub actions.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Key Features&lt;/h2&gt;

&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Generate dacpac on linux runners&lt;/li&gt;
&lt;li&gt;Deploy to a database with Entra-only authentication enabled&lt;/li&gt;
&lt;/ul&gt;

&lt;/div&gt;
&lt;br&gt;
&lt;br&gt;
  &lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/ilmax/azure-sql-entra-only-id" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Enabling Entra-only authentication can help us improve our Azure SQL security baseline. Additionally, the process of updating GitHub Action workflows to incorporate Entra-only authentication is straightforward and manageable. This means we can seamlessly integrate this security measure into our CI/CD pipelines, taking full advantage of the benefits it provides.&lt;/p&gt;

&lt;p&gt;Till the next time!&lt;/p&gt;

</description>
      <category>azure</category>
      <category>devops</category>
      <category>sql</category>
      <category>terraform</category>
    </item>
    <item>
      <title>GitHub Actions Azure Vnet Integration</title>
      <dc:creator>Massimiliano Donini</dc:creator>
      <pubDate>Sun, 18 Aug 2024 08:02:20 +0000</pubDate>
      <link>https://dev.to/maxx_don/github-actions-azure-vnet-integration-mf1</link>
      <guid>https://dev.to/maxx_don/github-actions-azure-vnet-integration-mf1</guid>
      <description>&lt;p&gt;In today's post, we will look at an interesting challenge, having GitHub actions interact with Azure PaaS services for which we have disabled public access.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem Statement
&lt;/h2&gt;

&lt;p&gt;If you are working on improving your cloud security posture on Azure, one of the first things that you should look into when deploying PaaS services (like for example Azure Storage, Azure Cosmos DB, or Azure SQL Server) is to disable the public access.&lt;/p&gt;

&lt;p&gt;Most PaaS services nowadays allow you to disable public access, meaning that you can't connect to those services over the Internet anymore. For the applications deployed in Azure, you can take advantage of Virtual Networks, Private Endpoints and Private DNS Zones to enable private connectivity without changing any single line of code.&lt;/p&gt;

&lt;p&gt;This is all well and good, but what about those services that aren't deployed in Azure, like for example the CI/CD runners?&lt;/p&gt;

&lt;p&gt;Chances are that you interact with your infrastructure in CI/CD pipelines (like for example running &lt;code&gt;terraform apply&lt;/code&gt;) and, as soon as you close the firewall, some operations will start to fail due to connectivity problems.&lt;/p&gt;

&lt;p&gt;At this point, we have several ways of fixing the issue, here's a quick non-exhaustive list off the top of my head:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Open and close the PaaS Service public access when the pipeline starts and revert the operation at the end&lt;/li&gt;
&lt;li&gt;Allow access to the PaaS Services from within your office network and self-host your runners within your office network&lt;/li&gt;
&lt;li&gt;Self-host your runners in Azure Virtual Machines with network connectivity to those services&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All the options in the above list will fix the issue but they all have some disadvantages, so can we do better?&lt;br&gt;
Not long ago, we got a new and better alternative which is to take advantage of GitHub private networking for hosted runners.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe4o7vx0ive3ci59yczx1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe4o7vx0ive3ci59yczx1.png" alt="*GitHub private networking inner workings, image courtesy of https://docs.github.com/en/organizations/managing-organization-settings/about-azure-private-networking-for-github-hosted-runners-in-your-organization*"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub private networking
&lt;/h2&gt;

&lt;p&gt;This relatively new feature (Public beta started in November 2023 and went GA in April 2024), allows GitHub-hosted runners to use a network interface card (NIC) created in a subnet under your control. This way we don't have to manage our own hosted runners, GitHub will keep managing the runners for us, while we will still be able to connect to PaaS service using private connectivity.&lt;/p&gt;

&lt;p&gt;To set this up we need to configure a few things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Get your GitHub the Enterprise ID or Organization ID (more on this below)&lt;/li&gt;
&lt;li&gt;Create the Azure VNET that will host the NICs used by the GitHub runners&lt;/li&gt;
&lt;li&gt;Create a GitHub network configuration in Azure&lt;/li&gt;
&lt;li&gt;Create a Hosted Computed Network configuration in GitHub&lt;/li&gt;
&lt;li&gt;Create Runner Group(s) and Runner(s) on GitHub&lt;/li&gt;
&lt;li&gt;Change your GitHub action &lt;code&gt;runs-on&lt;/code&gt; to reference the runner&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This functionality is only supported by &lt;strong&gt;GitHub Enterprise&lt;/strong&gt; and &lt;strong&gt;GitHub Team&lt;/strong&gt; plans&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Azure private connectivity
&lt;/h2&gt;

&lt;p&gt;Let's now take a look into a few concepts that are necessary for understanding how this can be configured.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1hwwqr6akcxcd8t4lw6w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F1hwwqr6akcxcd8t4lw6w.png" alt="Image description"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Private Endpoints
&lt;/h3&gt;

&lt;p&gt;Private Endpoints can be thought of as read-only Network Interface Cards (NICs) for your PaaS services. Those NICs are created in the subnet you specify and are assigned a private IP from the VNET address space. The connection between the private endpoint and the PaaS service uses a secure private link.&lt;/p&gt;

&lt;p&gt;When using Private Endpoints, the traffic never leaves the Microsoft backbone network as opposed to going through the public internet, making it not only more secure but also a faster way to access your PaaS services.&lt;/p&gt;

&lt;h3&gt;
  
  
  Private DNS Zones
&lt;/h3&gt;

&lt;p&gt;When a PaaS service enables the Private Endpoints, at the DNS level the service FQDN turns into a CNAME to the private link zone for the related service.&lt;br&gt;
Let's see the changes in resolving an Azure Storage Account hostname with the &lt;code&gt;dig&lt;/code&gt; command (available in Linux and MacOS, when using Windows you can use &lt;code&gt;dig&lt;/code&gt; on WSL or &lt;code&gt;nslookup&lt;/code&gt; on cmd/PowerShell)&lt;/p&gt;

&lt;p&gt;No Private Endpoints&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

dig example.blob.core.windows.net

&lt;span class="p"&gt;;;&lt;/span&gt; ANSWER SECTION:
example.blob.core.windows.net. 60 IN CNAME blob.ams08prdstr13c.store.core.windows.net.
blob.ams08prdstr13c.store.core.windows.net. 86400 IN A 1.2.3.4


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;With Private Endpoints&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

dig examplepe.blob.core.windows.net

&lt;span class="p"&gt;;;&lt;/span&gt; ANSWER SECTION:
examplepe.blob.core.windows.net. 60 IN CNAME examplepe.privatelink.blob.core.windows.net.
examplepe.privatelink.blob.core.windows.net. 60 IN CNAME blob.am5prdstr12a.store.core.windows.net.
blob.am5prdstr12a.store.core.windows.net. 60 IN A 1.2.3.5


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;As you can see above, after we turn on Private Endpoints for a particular service, we get another DNS indirection. This zone coincides with the name of the Private DNS Zone in which we have to create our records to enable private connectivity.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; You can read more about how this works in the Microsoft &lt;a href="https://learn.microsoft.com/en-us/azure/storage/common/storage-private-endpoints#dns-changes-for-private-endpoints" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Different services have different DNS Zones and those are mentioned in the documentation &lt;a href="https://learn.microsoft.com/en-us/azure/private-link/private-endpoint-dns" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;An A record is then created in the respective Private DNS Zone that resolves to the IP of the NIC that represents your PaaS service.&lt;/p&gt;

&lt;p&gt;Private Endpoint can be linked to one or more Private DNS Zones to make sure that whenever the IP of the NIC connected to the Private Endpoint changes, the DNS record(s) are automatically updated by the platform for us.&lt;/p&gt;

&lt;h3&gt;
  
  
  VNET Peering
&lt;/h3&gt;

&lt;p&gt;If you need to connect to PaaS services via Private Endpoint from a different VNET than the one where the Private Endpoint NICs live, you can take advantage of network peering and you should be able to communicate without any problems. VNET Peering is very flexible and can peer networks that are in the same regions as well as different regions, subscriptions or even tenants.&lt;/p&gt;

&lt;p&gt;To be able to resolve the hostname to the private IP of the NIC created by the Private Endpoints, we need to make sure that the private DNS Zone is linked to all the VNETs that have to connect to the PaaS service.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Bear in mind that network peering is not transitive, so if you need to traverse several networks, you need to configure a network virtual appliance (NVA) that knows how to route traffic&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;All three components briefly described above are used to configure private access to PaaS services and GitHub-hosted runners can take advantage of this infrastructure. Let's see how below.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuration
&lt;/h2&gt;

&lt;p&gt;To get this working we need to configure the networking in Azure, create the hosted network configuration in GitHub, create runner groups and runners in GitHub and, as the last step, we can change the workflow's &lt;code&gt;runs-on&lt;/code&gt; to specify the new runner name, let see how to do this in detail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Azure configuration
&lt;/h3&gt;

&lt;p&gt;In Azure, you have to decide in which VNET the GitHub-hosted runner NICs will be created. You can use the same network where the private endpoint for your PaaS services lives or create another VNET to keep things separate, this decision is up to you and depends on your networking configuration/requirements. What's worth noting is that just a subset of Azure regions are supported, so you may be forced to create a VNET in a region different from the VNET that contains your Private Endpoints.&lt;/p&gt;

&lt;p&gt;As of today (July 2024) the only supported regions are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;EastUs&lt;/li&gt;
&lt;li&gt;EastUs2&lt;/li&gt;
&lt;li&gt;WestUs2&lt;/li&gt;
&lt;li&gt;WestUs3&lt;/li&gt;
&lt;li&gt;CentralUs&lt;/li&gt;
&lt;li&gt;NorthCentralUs&lt;/li&gt;
&lt;li&gt;SouthCentralUs&lt;/li&gt;
&lt;li&gt;AustraliaEast&lt;/li&gt;
&lt;li&gt;JapanEast&lt;/li&gt;
&lt;li&gt;FranceCentral&lt;/li&gt;
&lt;li&gt;GermanyWestCentral&lt;/li&gt;
&lt;li&gt;NorthEurope&lt;/li&gt;
&lt;li&gt;NorwayEast&lt;/li&gt;
&lt;li&gt;SwedenCentral&lt;/li&gt;
&lt;li&gt;SwitzerlandNorth&lt;/li&gt;
&lt;li&gt;UkSouth&lt;/li&gt;
&lt;li&gt;SoutheastAsia&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the VNET that contains the Private Endpoints is not in any of those regions, you have to create a new VNET and use Regional VNET Peerings or create another set of Private Endpoints in the designed VNET. If you use a HUB/Spoke network topology, you may want to create a dedicated spoke that will host the GitHub NICs.&lt;/p&gt;

&lt;p&gt;When you have multiple spokes that need to communicate, you can either peer them together, configure traffic routing through an NVA or connect them through a VPN gateway. Please refer to the &lt;a href="https://learn.microsoft.com/en-us/azure/architecture/networking/guide/spoke-to-spoke-networking" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; on how to achieve that.&lt;/p&gt;

&lt;p&gt;In my case, I went with the easy option to use network peering between the two spoke VNETs.&lt;/p&gt;

&lt;p&gt;After the networking part has been taken care of, we need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Register a new resource provider&lt;/li&gt;
&lt;li&gt;Create a new resource of type GitHub.Network/networkSettings&lt;/li&gt;
&lt;li&gt;Copy the tag.GithubId output&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Register the resource provider can be done in several ways, via Terraform (see below) or via az cli running the following command:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

az provider register &lt;span class="nt"&gt;--namespace&lt;/span&gt; GitHub.Network


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;In GitHub, depending on whether you have an Enterprise Cloud or Team Plan, you can configure the &lt;strong&gt;Hosted Compute Networking&lt;/strong&gt; on the Enterprise level or at the organization level. If you have GitHub Enterprise Cloud, you can still select whether to configure it at the Enterprise or Organization level.&lt;/p&gt;

&lt;p&gt;The GitHub network settings need to know about your Enterprise/Organization so, before creating the network settings resource in Azure, we need to get a hold of the Enterprise ID/Organization ID from GitHub. As far as I know, this is not displayed anywhere in the UI so we need to execute a GraphQL API call as shown below:&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Enterprise ID
&lt;/h3&gt;

&lt;p&gt;We get the organization ID via the following GraphQL call, before the call we also need to generate a personal access token with the required grants.&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer BEARER_TOKEN"&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{ "query": "query($slug: String!) { enterprise (slug: $slug) { slug databaseId } }" ,
        "variables": {
          "slug": "ENTERPRISE_SLUG"
        }
      }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
https://api.github.com/graphql


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The documentation for configuring the private networking for GitHub-hosted runners in your &lt;strong&gt;Enterprise&lt;/strong&gt; can be found &lt;a href="https://docs.github.com/en/enterprise-cloud@latest/admin/configuring-settings/configuring-private-networking-for-hosted-compute-products/configuring-private-networking-for-github-hosted-runners-in-your-enterprise" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  GitHub Organization ID
&lt;/h3&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;

curl &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer BEARER_TOKEN"&lt;/span&gt; &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{ "query": "query($login: String!) { organization (login: $login) { login databaseId } }" ,
        "variables": {
          "login": "ORGANIZATION_LOGIN"
        }
      }'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
https://api.github.com/graphql


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; The documentation for configuring private networking for GitHub-hosted runners in your &lt;strong&gt;Organization&lt;/strong&gt; can be found &lt;a href="https://docs.github.com/en/organizations/managing-organization-settings/configuring-private-networking-for-github-hosted-runners-in-your-organization" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Create the GitHub network setting
&lt;/h3&gt;

&lt;p&gt;Here's the Terraform code to create the GitHub network settings resource in Azure:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;

&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;azurerm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/azurerm"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&amp;gt;3.0.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;azapi&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Azure/azapi"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 1.14.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Register the GitHub.Network resource provider&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_resource_provider_registration"&lt;/span&gt; &lt;span class="s2"&gt;"github_resource_provider"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GitHub.Network"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_resource_group"&lt;/span&gt; &lt;span class="s2"&gt;"resource_group"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"North Europe"&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"My-Rg"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_virtual_network"&lt;/span&gt; &lt;span class="s2"&gt;"vnet"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"My-vnet"&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;address_space&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.0.0/8"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_subnet"&lt;/span&gt; &lt;span class="s2"&gt;"runner_subnet"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"My-runner-subnet"&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;virtual_network_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_virtual_network&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;

  &lt;span class="nx"&gt;address_prefixes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10.0.1.0/24"&lt;/span&gt;

  &lt;span class="nx"&gt;delegation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"delegation"&lt;/span&gt;

    &lt;span class="nx"&gt;service_delegation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;name&lt;/span&gt;    &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GitHub.Network/networkSettings"&lt;/span&gt;
      &lt;span class="nx"&gt;actions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Microsoft.Network/virtualNetworks/subnets/join/action"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Create the GitHub Network settings&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azapi_resource"&lt;/span&gt; &lt;span class="s2"&gt;"github_network_settings"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;                      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GitHub.Network/networkSettings@2024-04-02"&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"github_network_settings_resource"&lt;/span&gt;            &lt;span class="c1"&gt;# The name of the networksettings&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;                  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"West Europe"&lt;/span&gt;                                 &lt;span class="c1"&gt;# The region in which the networksetting resource will be created&lt;/span&gt;
  &lt;span class="nx"&gt;parent_id&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;      &lt;span class="c1"&gt;# Parent Id that should point to the ID of the resource group&lt;/span&gt;
  &lt;span class="nx"&gt;schema_validation_enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nx"&gt;body&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsonencode&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;properties&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;businessId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github_business_id&lt;/span&gt;                                   &lt;span class="c1"&gt;# GitHub EnterpriseID or Organization ID based on Enterprise vs Organization level configuration&lt;/span&gt;
      &lt;span class="nx"&gt;subnetId&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runner_subnet&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;                          &lt;span class="c1"&gt;# ID of the subnet where the NICs will be injected&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
  &lt;span class="nx"&gt;response_export_values&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"tags.GitHubId"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                                &lt;span class="c1"&gt;# Export the tags.GitHubId&lt;/span&gt;

  &lt;span class="nx"&gt;lifecycle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ignore_changes&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;tags&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"github_network_settings_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"ID of the GitHub.Network/networkSettings resource"&lt;/span&gt;
    &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsondecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;azapi_resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github_network_settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;gitHubId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;You can find the whole source code at the end of the article in the references section&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  GitHub configuration
&lt;/h3&gt;

&lt;p&gt;I decided to create the Hosted Compute Networking configurations at the Organization levels because it is where it makes the most sense for my use case, but creating it at the Enterprise level is pretty much the same thing, so you can easily adapt this tutorial to it.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to your Organization Settings&lt;/li&gt;
&lt;li&gt;In Hosted Compute Networking, create a new Network Configuration and pick Azure Private Networking&lt;/li&gt;
&lt;li&gt;Add a name to the configuration and then click on the &lt;em&gt;Add Azure Virtual Network&lt;/em&gt; button&lt;/li&gt;
&lt;li&gt;Paste the ID outputted by Terraform while creating the GitHub Network settings resource&lt;/li&gt;
&lt;li&gt;Save the configuration&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe7dxbd3a4dkr20wpdufm.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fe7dxbd3a4dkr20wpdufm.jpg" alt="GitHub network setting configuration dialog, image courtesy of https://github.com/garnertb/github-runner-vnet*"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After the network configuration is created, we have to create a Runner Group that uses the network configuration just created.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to Organization settings &amp;gt; Actions &amp;gt; Runner Groups&lt;/li&gt;
&lt;li&gt;Give the Runner Group a name&lt;/li&gt;
&lt;li&gt;In the network configuration dropdown, select the network configuration created in the previous step&lt;/li&gt;
&lt;li&gt;Save the Runner group&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;After the Runner Group has been created, it's now time to create a runner (or more) within the Runner Group&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click on the Runner Group just created&lt;/li&gt;
&lt;li&gt;Click on the New runner &amp;gt; New GitHub-hosted runner&lt;/li&gt;
&lt;li&gt;Specify name, OS, Image (OS Version) and Specs (Size)&lt;/li&gt;
&lt;li&gt;Make sure the Runner Group is the previously created Runner Group&lt;/li&gt;
&lt;li&gt;Save the runner&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now that we have configured everything, we can change the &lt;code&gt;runs-on&lt;/code&gt; label on a workflow with the name of one of the runners created above and it will use the new runner with VNET connectivity with our PaaS service.&lt;/p&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;

&lt;p&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Sample workflow&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;br&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;br&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;&lt;br&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build&lt;/span&gt;&lt;br&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure-vnet-runner&lt;/span&gt; &lt;span class="c1"&gt;# Use the name of the runner just created&lt;/span&gt;&lt;/p&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
&lt;br&gt;
  &lt;br&gt;
  &lt;br&gt;
  References&lt;br&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.youtube.com/watch?v=57ZwdztCx2w&amp;amp;ab_channel=JohnSavill%27sTechnicalTraining" rel="noopener noreferrer"&gt;John Savill Technical training - Private Endpoints&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.github.com/en/organizations/managing-organization-settings/about-azure-private-networking-for-github-hosted-runners-in-your-organization" rel="noopener noreferrer"&gt;GitHub docs - Organization private networking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="//ttps://docs.github.com/en/enterprise-cloud@latest/admin/configuring-settings/configuring-private-networking-for-hosted-compute-products/configuring-private-networking-for-github-hosted-runners-in-your-enterprise"&gt;GitHub docs - Enterprise private networking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/garnertb/github-runner-vnet" rel="noopener noreferrer"&gt;Example code&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Thanks to GitHub private networking for hosted runners, we can ensure our CI/CD pipeline works seamlessly even when we deny public access to the PaaS services we use, allowing us to enhance the security posture of our Azure Subscription.&lt;/p&gt;

&lt;p&gt;In this repository, I have a simple Terraform configuration where I deploy an Azure Storage Account, disable Public Access, and create the Private Endpoints for blob and tables within a VNET. In another VNET I have configured the GitHub network settings, the output of such configuration is the token that you can input in GitHub when creating the Hosted Compute Networking configuration.&lt;/p&gt;

&lt;p&gt;I hope you enjoyed this article, if you have some questions, don't hesitate to reach out.&lt;br&gt;
Till the next time!&lt;/p&gt;

</description>
      <category>github</category>
      <category>azure</category>
      <category>terraform</category>
      <category>devops</category>
    </item>
    <item>
      <title>Ace the AZ-104 Microsoft Azure Administrator Certification Exam: Tips and Strategies</title>
      <dc:creator>Massimiliano Donini</dc:creator>
      <pubDate>Wed, 01 Feb 2023 21:09:43 +0000</pubDate>
      <link>https://dev.to/maxx_don/ace-the-az-104-microsoft-azure-administrator-certification-exam-tips-and-strategies-3kao</link>
      <guid>https://dev.to/maxx_don/ace-the-az-104-microsoft-azure-administrator-certification-exam-tips-and-strategies-3kao</guid>
      <description>&lt;p&gt;I recently passed the &lt;a href="https://learn.microsoft.com/en-us/certifications/azure-administrator/" rel="noopener noreferrer"&gt;AZ-104&lt;/a&gt; certification exam, making it the third Microsoft certification under my belt so far. I started with &lt;a href="https://learn.microsoft.com/en-us/certifications/azure-fundamentals/" rel="noopener noreferrer"&gt;AZ-900&lt;/a&gt;, then &lt;a href="https://learn.microsoft.com/en-us/certifications/azure-developer/" rel="noopener noreferrer"&gt;AZ-204&lt;/a&gt; and recently &lt;a href="https://learn.microsoft.com/en-us/certifications/azure-administrator/" rel="noopener noreferrer"&gt;AZ-104&lt;/a&gt;.&lt;br&gt;
I'm planning to get the &lt;a href="https://learn.microsoft.com/en-us/certifications/azure-solutions-architect/" rel="noopener noreferrer"&gt;AZ-305&lt;/a&gt; and eventually the &lt;a href="https://learn.microsoft.com/en-us/certifications/devops-engineer/" rel="noopener noreferrer"&gt;AZ-400&lt;/a&gt; soon™.&lt;/p&gt;

&lt;p&gt;To successfully prepare for any exam you need to come up with an effective study plan. In this article you can find the tips I use to get prepared to undergo the AZ-104 certification exam.&lt;/p&gt;

&lt;p&gt;I'm using Azure daily so I am familiar with many of the certifications objectives, but not all of them. Having a developer background, I never needed to configure Azure Active Directory for MFA, conditional access policies or Site-to-Site VPN connections for example.&lt;/p&gt;

&lt;p&gt;Below you will find how I prepared as well as some tips I used, but keep in mind that you should tailor the study plan to fit your own way of learning. Different people learn in different ways and to be successful you need to figure out what works best for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  My study agenda
&lt;/h2&gt;

&lt;p&gt;I'm a full-time employee and I work 40 hours per week, so finding the time to study was not always easy, luckily for me though, here in The Netherlands the winter is quite depressing: It's dark when I leave home for the office and it's already dark when I come back. This somehow helped me stay at home during the evening so I could dedicate time to study and prepare for the certification.&lt;/p&gt;

&lt;p&gt;The time I put into studying was around 1.5/2 hours every day and around 4 or 5 hours during the weekends. It took me more or less 1.5 months to prepare for the certification.&lt;/p&gt;

&lt;p&gt;My weekly goal was to put in around 15 hours. I wasn't studying every day to not get overwhelmed, so I took some free days to relax and blow some steam off.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;p&gt;Here's a list of materials that I used to get prepared, in no particular order:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://learn.microsoft.com/en-us/certifications/resources/study-guides/az-104" rel="noopener noreferrer"&gt;AZ-104 Study guide&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;The Study guide is a great resource to check how familiar you are with a specific subject. From the study guide, I flagged the areas I was least familiar with and I started focusing on these first.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://www.youtube.com/watch?v=VOod_VNgdJk" rel="noopener noreferrer"&gt;John Savill's AZ-104 Study cram&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;This study cram was super useful to understand concepts I wasn't familiar with because John can explain concepts in a very easy-to-understand way. I used this one to kick-start my knowledge of Azure Active Directory configuration, VPN connections and to better understand how networking works in Azure.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://learn.microsoft.com/en-us/training/browse/?terms=104" rel="noopener noreferrer"&gt;Microsoft Learn&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Contrary to many, I didn't complete the AZ-104 learning path. I found the study path too high level and not very useful, what I did instead was to use the study path to dive deeper using the resources usually listed at the end of every chapter.&lt;/p&gt;

&lt;p&gt;In my opinion the learning path &lt;strong&gt;does not prepare&lt;/strong&gt; you enough for the exam. Test questions go into a greater detail than what the learning path covers so, I'd suggest to just skim over the learning path and rather focus more on the resources you find at the end of each learning path module.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;a href="https://azure.microsoft.com/en-us/offers/ms-azr-0044p/" rel="noopener noreferrer"&gt;Azure free trial subscription&lt;/a&gt;
&lt;/h3&gt;

&lt;p&gt;Activating a trial subscription allowed me to have hands-on experience with all the areas I wasn't very familiar with e.g. Azure Active Directory. An Azure trial subscription is free, you have 170.00 € of budget that will last for 30 days. Even though you have to give your credit card details you won't be billed. If you ran out of credits, all the services will be decommissioned unless you  upgrade to a &lt;strong&gt;Pay-As-You-Go&lt;/strong&gt; subscription as described in the &lt;a href="https://azure.microsoft.com/en-gb/free/free-account-faq/" rel="noopener noreferrer"&gt;Azure free account FAQ&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Find a practice test
&lt;/h3&gt;

&lt;p&gt;There're plenty of practice tests out there, some better than others so you have to put in some research to find a good one, and then go for it. Price vary from around 20.00 € up to around 100.00 € but I think it's worth the money.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Make sure you're not using dumps because it can invalidate all your certifications and prohibit you to take new ones ever again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Microsoft sponsors the &lt;a href="https://www.mindhub.com/az-104-microsoft-azure-administrator-microsoft-official-practice-test/p/MU-AZ-104?utm_source=microsoft&amp;amp;utm_medium=certpage&amp;amp;utm_campaign=msofficialpractice" rel="noopener noreferrer"&gt;MeasureUp&lt;/a&gt; practice tests, so that will set you up for success.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tips
&lt;/h2&gt;

&lt;p&gt;Here's a collection of tips that helped me get the certification.&lt;/p&gt;

&lt;h3&gt;
  
  
  Plan the exam date as the first step
&lt;/h3&gt;

&lt;p&gt;In order to not postpone taking the exam forever, I decided to plan the exam before starting to study. I did a bit of research to understand how much it takes to get ready for the exam, then gave me some more time and planned it. I ended up giving myself two months of time to prepare, this helped me to put the time in.&lt;/p&gt;

&lt;p&gt;Planning it in advance still allows you to change the exam date up to 24 hours before the exam, so it's not a strict deadline, but I found it helpful to have a due date to look at.&lt;/p&gt;

&lt;h3&gt;
  
  
  Alternate your study routine
&lt;/h3&gt;

&lt;p&gt;Mixing up your study routine can be beneficial as it helps to keep things interesting and can prevent boredom. Additionally, different study methods can appeal to different learning styles and can help to reinforce the material in different ways. For example, reading documentation may help to build a strong understanding of the theoretical concepts, while practice exams can help to build test-taking skills and identify areas where you need to improve. YouTube videos can be helpful in providing additional explanations and examples of the concepts that you are studying. So, switching up your routine can help to make the most of your study time and increase your chances of success.&lt;/p&gt;

&lt;h3&gt;
  
  
  Carefully review wrong practice test answers
&lt;/h3&gt;

&lt;p&gt;While taking the practice test, carefully review your answers, especially the wrong ones. This may uncover some gaps in your knowledge and will give you an indication of where you need to spend some time on.&lt;/p&gt;

&lt;p&gt;This point was especially important for me because I personally learn a lot more from a wrong answer than a correct one. Every time I give the wrong answer in the practice test, I dig deeper in the documentation and it really helps me understand the subject better.&lt;/p&gt;

&lt;p&gt;Whenever I got the same question wrong multiple times, I wrote down the question and the correct answer in a small document that I used to go over and over almost daily.&lt;/p&gt;

&lt;h3&gt;
  
  
  Retake the same practice test twice
&lt;/h3&gt;

&lt;p&gt;I found it very helpful to re-take the same practice test twice, possibly in the same day. Retaking the test in the same day can help to reinforce the material that you have studied and identify areas where you may still have difficulty. This can be helpful in focusing your study efforts and pinpointing areas where you need to spend more time studying.&lt;/p&gt;

&lt;h3&gt;
  
  
  Get hands-on experience
&lt;/h3&gt;

&lt;p&gt;Hands on experience is very important. Having only theoretical knowledge derived from the documentation may not be enough for this exam so I advise you to spend some time on practicing what you're learning with the free Azure account.&lt;br&gt;
This can be invaluable to get familiar with the az cli or PowerShell cmdlet. You can do so using the Azure Cloud Shell so you don't even have to install anything on your PC.&lt;/p&gt;

&lt;h3&gt;
  
  
  Enjoy the journey
&lt;/h3&gt;

&lt;p&gt;The added value of a certification in my opinion is not the certification badge, but rather all the knowledge you accrue preparing for the actual exam.&lt;/p&gt;

&lt;h2&gt;
  
  
  The exam
&lt;/h2&gt;

&lt;p&gt;The exam I went through had 49 questions and I had 1 h and 40 minutes to complete it. The exam started with a case study comprised of 6 questions.&lt;br&gt;
I found the level of difficulty of the exam reasonable, all the questions were clear and easy to understand. The time for me was enough, but if you find yourself running out of time you can skip the question you're not sure of using the &lt;strong&gt;mark for review&lt;/strong&gt; functionality, so you can go through them at the end, avoiding the risk of running out of time.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Please note that you can't review questions from the case study at the end of the exam, you can review them at only at the end of the case study.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;In this article I wanted to share my approach to prepare for a certification exam together with some tricks I used to get ready in a reasonable amount of time and how to figure out what are the knowledge gaps I had.&lt;/p&gt;

&lt;p&gt;To sum up, I wanna point out that this is what worked well for me. It's not guaranteed that this approach works equally well for you, but you may use some of the approaches I described to get yourself ready for the certification.&lt;/p&gt;

&lt;p&gt;I hope you find this helpful, if you have any questions/suggestions don't hesitate to leave a comment below. If you're preparing for AZ-104 or any other certification, I wish you best of luck 🍀.&lt;/p&gt;

&lt;p&gt;Till the next time.&lt;/p&gt;

</description>
      <category>emptystring</category>
    </item>
    <item>
      <title>Goodbye secrets 👋, Hello token exchange: Connect Your GitHub Actions to Azure securely</title>
      <dc:creator>Massimiliano Donini</dc:creator>
      <pubDate>Wed, 25 Jan 2023 14:47:54 +0000</pubDate>
      <link>https://dev.to/maxx_don/goodbye-secrets-hello-token-exchange-connect-your-github-actions-to-azure-securely-1h03</link>
      <guid>https://dev.to/maxx_don/goodbye-secrets-hello-token-exchange-connect-your-github-actions-to-azure-securely-1h03</guid>
      <description>&lt;p&gt;OpenID Connect (OIDC) integration between Azure Active Directory and GitHub allows your GitHub Actions workflows to securely access resources in Azure, without needing to store the Azure credentials in the GitHub action secrets.&lt;/p&gt;

&lt;p&gt;This functionality has been available for quite a while, it was first announced on &lt;a href="https://azure.microsoft.com/en-us/updates/public-preview-openid-connect-integration-between-azure-ad-and-github-actions/" rel="noopener noreferrer"&gt;October 2021&lt;/a&gt; and up until now, it has been on my "things to look into" list.&lt;/p&gt;

&lt;p&gt;Recently I've been working on a project to migrate Azure DevOps to GitHub so I decided that time has come to look into this functionality.&lt;/p&gt;

&lt;h2&gt;
  
  
  How a typical connection is configured
&lt;/h2&gt;

&lt;p&gt;Usually, to connect to Azure as an application (i.e. when running a GitHub Action) you need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a Service Principal in Azure Active Directory&lt;/li&gt;
&lt;li&gt;Create a Service Principal Credential&lt;/li&gt;
&lt;li&gt;Grant to the Service Principal permissions on the subscription(s)&lt;/li&gt;
&lt;li&gt;Copy the secret created in step 3 on your GitHub secrets&lt;/li&gt;
&lt;li&gt;Authenticate the workflow using the secret created above&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Please note that the &lt;code&gt;az ad sp create-for-rbac&lt;/code&gt; can simplify the process a bit since it can do steps from 1 to 3 in a single go,  more info can be found in the &lt;a href="https://learn.microsoft.com/en-us/cli/azure/ad/sp?view=azure-cli-latest#az-ad-sp-create-for-rbac" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why should you use secret-less connections
&lt;/h2&gt;

&lt;p&gt;Having secret-less connections is far better than using some form of a shared secret.&lt;br&gt;
Since you don't have any secrets, you can't leak any moreover shared secrets usually comes with a fixed validity. Ideally, you should rotate them often to limit the risk derived from a secret leak.&lt;/p&gt;

&lt;p&gt;Additionally, in the case of automated infrastructure deployments, the Service Principal will have high privileges on your subscription(s) and/or possibly &lt;strong&gt;Azure Active Directory&lt;/strong&gt; since it has to create resources, potentially assign RBAC role assignments (that require the Owner role) making the event of a secret leak even more dangerous.&lt;/p&gt;

&lt;p&gt;With secret-less connections, on the other hand, even if the GitHub Actions secrets are leaked, an attacker can't gain direct access to the Azure subscription.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Please note that even if you store the shared secret in GitHub secrets, it's still fairly easy to get access to the original value, see this StackOverflow &lt;a href="https://stackoverflow.com/questions/59481933/how-can-i-extract-secrets-using-github-actions" rel="noopener noreferrer"&gt;question&lt;/a&gt; for example.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  I'm sold, now how can I set it up?
&lt;/h2&gt;

&lt;p&gt;To configure OpenID Connect Integrations between Azure Active Directory and GitHub you need to do a couple of things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create an App Registration in Azure Active Directory&lt;/li&gt;
&lt;li&gt;Create a Service Principal for the App Registration created above&lt;/li&gt;
&lt;li&gt;Add the Federated Credential in the App Registration&lt;/li&gt;
&lt;li&gt;Copy the configuration values CLIENT_ID, SUBSCRIPTION_ID and TENANT_ID in GitHub&lt;/li&gt;
&lt;li&gt;Configure your workflow permissions&lt;/li&gt;
&lt;li&gt;Grant to the Service Principal the desired permissions on your subscriptions(s)&lt;/li&gt;
&lt;li&gt;Use the action &lt;code&gt;azure/login@v1&lt;/code&gt; to login to Azure using token exchange&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For a step-by-step guide, refer to the GitHub &lt;a href="https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/configuring-openid-connect-in-azure" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  How does it work?
&lt;/h2&gt;

&lt;p&gt;It's not the goal of this post to dig into the nitty-gritty details, so my explanation will be quite brief.&lt;br&gt;
Under the hood, this uses &lt;strong&gt;Azure Workload identities&lt;/strong&gt; to exchange a token issued by GitHub with a token issued by Azure Active Directory.&lt;/p&gt;

&lt;p&gt;For the token exchange to be successful, you need to establish a trust relationship between the GitHub token issue and the Azure Active Directory. This trust relationship boils down to configuring the Federated Credential (created in step 3) in Azure Active Directory filling in the content of two of the claims issued by GitHub to the workflow, more specifically you need to fill in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The issuer (&lt;strong&gt;iss&lt;/strong&gt; claim of the access token issued by GitHub)&lt;/li&gt;
&lt;li&gt;The subject (&lt;strong&gt;sub&lt;/strong&gt; claim of the access token issued by GitHub)&lt;/li&gt;
&lt;li&gt;The audience (fixed value of &lt;code&gt;api://AzureADTokenExchange&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here below you can find how GitHub explains it in their announcement post.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.blog%2Fwp-content%2Fuploads%2F2021%2F11%2FOIDC-diagram.png%3Fresize%3D1024%252C355%3Fw%3D1024" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.blog%2Fwp-content%2Fuploads%2F2021%2F11%2FOIDC-diagram.png%3Fresize%3D1024%252C355%3Fw%3D1024" title="OIDC Federation" alt="OIDC Federation" width="1024" height="355"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you want to dig deeper, here's the &lt;a href="https://learn.microsoft.com/en-us/azure/active-directory/develop/workload-identity-federation" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Manual configuration
&lt;/h2&gt;

&lt;p&gt;If you go to Azure Active Directory, after you created an App Registration, when you try to add Federated Credentials, the Azure Active Directory Portal helps you with filling in the required details for setting up GitHub Federated Credentials.&lt;/p&gt;

&lt;p&gt;The screen looks like the following:&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmaxdon.tech%2Fposts%2Fgithub-azure-oidc%2Ffederated-credentials.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fmaxdon.tech%2Fposts%2Fgithub-azure-oidc%2Ffederated-credentials.png" title="GitHub Federated Credentials" alt="GitHub Federated Credentials" width="800" height="509"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have to configure multiple repositories, the manual approach falls short so let's have a look at how we can configure it with Terraform.&lt;/p&gt;
&lt;h2&gt;
  
  
  Terraform configuration
&lt;/h2&gt;

&lt;p&gt;Since Terraform has a provider-based approach, we can configure a GitHub repository (or many) and, at the same time, create the required setup in Azure Active Directory.&lt;/p&gt;

&lt;p&gt;Let's see how is done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Look-up GitHub Actions token issuer discover document&lt;/span&gt;
&lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"http"&lt;/span&gt; &lt;span class="s2"&gt;"github_actions_oidc_discovery_document"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://token.actions.githubusercontent.com/.well-known/openid-configuration"&lt;/span&gt;

  &lt;span class="nx"&gt;request_headers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;Accept&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"application/json"&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;lifecycle&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;postcondition&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;condition&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;contains&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status_code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="nx"&gt;error_message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Status code invalid"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;github_issuer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jsondecode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github_actions_oidc_discovery_document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;response_body&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s2"&gt;"issuer"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Create an Azure Active Directory Application representing GitHub Actions&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azuread_application"&lt;/span&gt; &lt;span class="s2"&gt;"github_app_registration"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;display_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GitHub-App-Registration"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Create a Service Principal for the GitHub Actions App Registration&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azuread_service_principal"&lt;/span&gt; &lt;span class="s2"&gt;"github_service_principal"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;application_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azuread_application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github_app_registration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;application_id&lt;/span&gt;
  &lt;span class="nx"&gt;use_existing&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Create the Federated Credential for the App Registration&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azuread_application_federated_identity_credential"&lt;/span&gt; &lt;span class="s2"&gt;"github_federated_credentials"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;application_object_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azuread_application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github_app_registration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object_id&lt;/span&gt;
  &lt;span class="nx"&gt;audiences&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"api://AzureADTokenExchange"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;display_name&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GitHub-FederatedCredential"&lt;/span&gt;
  &lt;span class="nx"&gt;issuer&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github_issuer&lt;/span&gt;
  &lt;span class="c1"&gt;// You need to decide how to configure this one accordingly to your use case&lt;/span&gt;
  &lt;span class="nx"&gt;subject&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"repo:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organization&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repository_name&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:environment:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environemnt&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Look-up current subscription and tenant id&lt;/span&gt;
&lt;span class="k"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_client_config"&lt;/span&gt; &lt;span class="s2"&gt;"current"&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

&lt;span class="c1"&gt;// Create a GitHub repository&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"github_repository"&lt;/span&gt; &lt;span class="s2"&gt;"repository"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repository_name&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repository_description&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Optional&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"github_repository_environment"&lt;/span&gt; &lt;span class="s2"&gt;"environment"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;environment&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environemnt&lt;/span&gt;
  &lt;span class="nx"&gt;repository&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;github_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Can be just regular repository secret if you don't use environments&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"github_actions_environment_secret"&lt;/span&gt; &lt;span class="s2"&gt;"client_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;environment&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;github_repository_environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
  &lt;span class="nx"&gt;repository&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;github_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;secret_name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"CLIENT_ID"&lt;/span&gt;
  &lt;span class="nx"&gt;plaintext_value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azuread_application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github_app_registration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;application_id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Can be just regular repository secret if you don't use environments&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"github_actions_environment_secret"&lt;/span&gt; &lt;span class="s2"&gt;"subscription_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;environment&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;github_repository_environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
  &lt;span class="nx"&gt;repository&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;github_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;secret_name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"SUBSCRIPTION_ID"&lt;/span&gt;
  &lt;span class="nx"&gt;plaintext_value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;azurerm_client_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscription_id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Can be just regular repository secret if you don't use environments&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"github_actions_environment_secret"&lt;/span&gt; &lt;span class="s2"&gt;"tenant_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;environment&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;github_repository_environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;environment&lt;/span&gt;
  &lt;span class="nx"&gt;repository&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;github_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;secret_name&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"TENANT_ID"&lt;/span&gt;
  &lt;span class="nx"&gt;plaintext_value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;azurerm_client_config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenant_id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;//providet.tf&lt;/span&gt;
&lt;span class="k"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;github&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"integrations/github"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 5.16.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;azuread&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/azuread"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 2.32.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;backend&lt;/span&gt; &lt;span class="s2"&gt;"azurerm"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Configure the GitHub Provider&lt;/span&gt;
&lt;span class="k"&gt;provider&lt;/span&gt; &lt;span class="s2"&gt;"github"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;owner&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organization&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Please note that this Terraform configuration requires multiple providers and, to run the apply successfully, you need to be authenticated into both.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Quirks
&lt;/h2&gt;

&lt;p&gt;As you can see, the configuration is quite straightforward, but there's a catch.&lt;br&gt;
Since you have to configure the subject (sub) claim in the Federated Credential with the same value of the sub claim that GitHub is issuing to your workflow and, by default, the repository name will be part of the claim value, this means you will have to create one App Registration, Federated Credential and Service Principal for each repository.&lt;/p&gt;

&lt;p&gt;This may be fine if you have a limited number of repositories, but in my case, I had around 60 repositories that needs to be deployed.&lt;br&gt;
On top of that, I (like probably most of you) have several environments e.g. Development, Testing, Acceptance, and Production and since I don't want the same Service Principal to have access to different subscriptions for security reasons, the number of App Registrations, Federated Credentials and Service Principal gets multiplied by a 4 factor (one for each environment) reaching a whopping 240.&lt;/p&gt;

&lt;p&gt;As you can imagine this was not ideal so I decided to look at possible alternatives. What I wanted to achieve was creating 4 App Registrations, one for every environment, and use the same one across all the repositories.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Please note that In order to achieve this you need to use GitHub deployment environments. Environments, environment secrets, and environment protection rules are available in public repositories for all products. For access to environments, environment secrets, and deployment branches in private or internal repositories, you must use GitHub Pro, GitHub Team, or GitHub Enterprise. For access to other environment protection rules in private or internal repositories, you must use GitHub Enterprise, see &lt;a href="https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To achieve what I described above, I needed a way to change the content of the subject claim issued by GitHub, luckily for us, this functionality is supported by GitHub using &lt;a href="https://docs.github.com/en/rest/actions/oidc?apiVersion=2022-11-28#set-the-customization-template-for-an-oidc-subject-claim-for-an-organization" rel="noopener noreferrer"&gt;this&lt;/a&gt; api, even better, this functionality is also supported by the GitHub Terraform provider.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;As far as I know, there's no UI support to change the content of the access token issued by GitHub yet.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Configure the subject claim with Terraform
&lt;/h2&gt;

&lt;p&gt;Here below you can see how to configure the subject claim for our use case:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"github_actions_repository_oidc_subject_claim_customization_template"&lt;/span&gt; &lt;span class="s2"&gt;"sub_claim_template"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;repository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;github_repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;repository&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;

  &lt;span class="nx"&gt;use_default&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nx"&gt;include_claim_keys&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"repository_owner"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"context"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Create the Federated Credential for the App Registration&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azuread_application_federated_identity_credential"&lt;/span&gt; &lt;span class="s2"&gt;"github_federated_credentials"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;application_object_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azuread_application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github_app_registration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object_id&lt;/span&gt;
  &lt;span class="nx"&gt;audiences&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"api://AzureADTokenExchange"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;display_name&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"GitHub-FederatedCredential"&lt;/span&gt;
  &lt;span class="nx"&gt;issuer&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kd"&gt;local&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;github_issuer&lt;/span&gt;
  &lt;span class="c1"&gt;// You need to decide how to configure this one accordingly to your use case&lt;/span&gt;
  &lt;span class="nx"&gt;subject&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"repository_owner:&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;organization&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:environment:{var.environment}"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Please note that what you incude in the &lt;code&gt;include_claim_keys&lt;/code&gt; depends on your specific scenario, this configuration allows me to add the &lt;strong&gt;organization&lt;/strong&gt; and the &lt;strong&gt;environment&lt;/strong&gt; in the subject claim so I can reuse the same Service Principal across all repositories for a given environment.&lt;br&gt;
Also bear in mind that however you configure the GitHub sub claim, &lt;strong&gt;MUST&lt;/strong&gt; match what's configured in the App Registration Federated Credential.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There're more customizations possible and you can learn about these in the GitHub &lt;a href="https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#customizing-the-token-claims" rel="noopener noreferrer"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The last change you need to implement, besides granting RBAC privileges to the Service Principal on your subscriptions(s), is to configure the workflow to use token exchange to authenticate into Azure, there are two parts to it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Configure the required permissions in the workflow&lt;/li&gt;
&lt;li&gt;Configure the action &lt;strong&gt;azure/login@v1&lt;/strong&gt; to use the token exchange.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's an example of a workflow that will work with the configuration above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Azure Login with OIDC&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; 
  &lt;span class="na"&gt;build-and-deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Az&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;CLI&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;login'&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;azure/login@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;client-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_CLIENT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;tenant-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_TENANT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;subscription-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_SUBSCRIPTION_ID }}&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Run&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;az&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;commands'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;az account show&lt;/span&gt;
          &lt;span class="s"&gt;az group list&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;As you can see, the configuration is quite straightforward and it allows you to get rid of secrets, improve your security posture &lt;strong&gt;and&lt;/strong&gt;, as a bonus, forget about the expiring credentials issue.&lt;/p&gt;

&lt;p&gt;I hope you found this useful.&lt;/p&gt;

&lt;p&gt;Till the next time&lt;/p&gt;

</description>
      <category>watercooler</category>
    </item>
    <item>
      <title>Terraform Tips &amp; Tricks: Managing Large-Scale Azure Resource Imports</title>
      <dc:creator>Massimiliano Donini</dc:creator>
      <pubDate>Thu, 19 Jan 2023 17:28:52 +0000</pubDate>
      <link>https://dev.to/maxx_don/terraform-tips-tricks-managing-large-scale-azure-resource-imports-597d</link>
      <guid>https://dev.to/maxx_don/terraform-tips-tricks-managing-large-scale-azure-resource-imports-597d</guid>
      <description>&lt;p&gt;This post describes my journey to import several hundred Azure resources in Terraform. Before digging into the what and the how let me give you a brief description of our environment's infrastructure.&lt;/p&gt;

&lt;p&gt;In my current company, we manage many Azure resources for each environment and we have a few of them (DEV, TEST, etc.). Every environment looks pretty much the same and it mostly differs by product SKUs, database sizes, etc.&lt;/p&gt;

&lt;p&gt;My fellow team members and I are building a microservices solution, we have around 50 independent services deployed in Azure that require some specific resources (imagine a SQL database or a Storage container), and on top of that, we have all the service agnostic infrastructure.&lt;/p&gt;

&lt;p&gt;All these resources aren't created/updated in the same way, some use ARM templates, some use PowerShell scripts, some use Azure cli scripts and so on.&lt;br&gt;
This is mostly because we need to work around some tooling limitations e.g. you can't create resources in Azure Active Directory using ARM templates.&lt;/p&gt;

&lt;p&gt;All the aforementioned scripts are placed in a shared repository and every project references what it needs in its deployment pipeline.&lt;/p&gt;

&lt;p&gt;This approach works, but it has several problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We use different technologies to deploy infrastructure and that makes it harder for newcomers to get up to speed quickly&lt;/li&gt;
&lt;li&gt;Deployments take longer than necessary&lt;/li&gt;
&lt;li&gt;It's impossible to have a preview of the changes that are going to be applied at deployment time&lt;/li&gt;
&lt;li&gt;Re-deploying infrastructure may not fix all the &lt;a href="https://wiki.gccollab.ca/index.php?title=Technology_Trends/Infrastructure_as_Code&amp;amp;mobileaction=toggle_view_desktop#Configuration_Drift" rel="noopener noreferrer"&gt;configuration drift&lt;/a&gt; especially the PowerShell/az cli scripts&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I'm sure I missed several other points but these are the most painful ones.&lt;/p&gt;

&lt;p&gt;Hence we decided to move to Terraform since it can address all the points above and, according to &lt;a href="https://octoverse.github.com/2022/top-programming-languages" rel="noopener noreferrer"&gt;GitHub Octoverse 2022&lt;/a&gt;, HCL was the fastest growing language in 2021-2022.&lt;/p&gt;
&lt;h2&gt;
  
  
  The import challenge
&lt;/h2&gt;

&lt;p&gt;If you start on a greenfield project everything is quite easy, but as you may know, if a resource has been created outside Terraform, it needs to be imported to be managed with Terraform in the future.&lt;br&gt;
Importing resources is not difficult, on the provider documentation site, at the bottom of the page of every resource, you can find the command to execute to import a given resource.&lt;/p&gt;

&lt;p&gt;My problem was that I had hundreds of them, around 160 global resources, multiplied by all the various environments + between 5-10 resource service dependent multiplied by the number of services ~50 multiplied by the number of environments.&lt;/p&gt;

&lt;p&gt;As you can imagine this adds up very quickly, especially because importing resources is a tedious task. &lt;br&gt;
To import a resource in Terraform you need a couple of things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create the resource in your configuration files&lt;/li&gt;
&lt;li&gt;The Terraform resource id &lt;/li&gt;
&lt;li&gt;The Azure resource id&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;                |--------------Terraform resource id-------------------| |-----------------------------------------Azure resource id---------------------------------------------------------------|
terraform import module.servicebus.azurerm_servicebus_namespace.example  /subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/mygroup1/providers/Microsoft.ServiceBus/namespaces/sbns1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To get the Terraform resource id, you may first need to come up with the final module structure because if you use modules, the module name would be part of the Terraform resource id (as can be seen above).&lt;/p&gt;

&lt;p&gt;Figuring out the Azure resource id can also be challenging for some nested resources, e.g. a cosmos role assignment requires some az cli gymnastics.&lt;/p&gt;

&lt;p&gt;This seemed like a herculean effort so I started looking around hoping to find a tool that could help with a bulk import.&lt;/p&gt;

&lt;h3&gt;
  
  
  Azure Terrafy - Aztfy
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/Azure/aztfy" rel="noopener noreferrer"&gt;Aztfy&lt;/a&gt; is a tool developed by Microsoft that allows you to bulk import resources, it has some configuration so you can specify what to import, the names to import and so on.&lt;br&gt;
After spending some time with the tool, I quickly realized it may be a no-go. The problem I had with this tool is twofold:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;It doesn't generate reproducible Terraform configurations&lt;/li&gt;
&lt;li&gt;It doesn't generate Terraform idiomatic code&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Let me expand on those:&lt;/p&gt;

&lt;p&gt;Not generating reproducible configurations means that after an import, when you run Terraform plan, you may still have changes that you need to fix manually, or worse, Terraform may fail due to validation problems. &lt;br&gt;
This limitation is documented in the project &lt;a href="https://github.com/Azure/aztfy#limitation" rel="noopener noreferrer"&gt;README&lt;/a&gt; and I could've lived with it.&lt;/p&gt;

&lt;p&gt;Not generating idiomatic code is a bit more problematic, since it requires manually changing most parts of the imported code to make use of variables or to reference a parent resource.&lt;br&gt;
This means that all the code that &lt;strong&gt;aztfy&lt;/strong&gt; will output, needs to be adjusted/modified. Moreover, if you decide to reorganize the code and move resources in a different module (hence changing the Terraform resource id) after it has been imported, then you have to either start modifying the Terraform state manually, or you may need to import it all over again.&lt;/p&gt;

&lt;p&gt;Given the above downsides, I decided to use it only marginally and instead start writing my configuration from scratch.&lt;/p&gt;
&lt;h2&gt;
  
  
  My approach
&lt;/h2&gt;

&lt;p&gt;Before starting I came up with a set of principles to use as guidelines when writing HCL modules, which are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One module for every different resource type used&lt;/li&gt;
&lt;li&gt;For every module that needs access to resources defined in other modules, read these resources with a data source&lt;/li&gt;
&lt;li&gt;All the module's inputs are defined in a file called &lt;code&gt;variables.tf&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;All the permissions-related stuff (RBAC, AAD group membership) will go in a file called &lt;code&gt;permission.tf&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;All the networking configuration (Firewall rules, VNet, Private endpoints and so on) will go in a file called &lt;code&gt;networking.tf&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;All the module's outputs will go in a file called &lt;code&gt;output.tf&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;These lower-level modules will be invoked by a higher-level module where most of the naming logic will be&lt;/li&gt;
&lt;li&gt;Lower-level modules can only be called by higher-level modules&lt;/li&gt;
&lt;li&gt;Several higher-level modules will be created:

&lt;ul&gt;
&lt;li&gt;Environment specific with all the global resources &lt;/li&gt;
&lt;li&gt;Several service-specific ones, one for each type of service&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Client code can only reference higher-level modules&lt;/li&gt;
&lt;li&gt;Higher level modules should have the least number of secret possible&lt;/li&gt;
&lt;li&gt;Lower-level modules shouldn't reference other lower-level modules&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Please note that these are principles I came up with and that make sense in my specific scenario, your mileage may vary&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  Directory structure
&lt;/h3&gt;

&lt;p&gt;The principles stated above helped me come up with a directory structure that looks like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;├── environments
│   ├── acc
│   ├── dev
│   ├── prod
│   └── tst
├── modules
│   ├── private                &lt;span class="nt"&gt;--&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Lower-level modules
│   │   ├── global
│   │   │   ├── global_azure_service_1          e.g. Cosmos Db 
│   │   │   ├── global_azure_service_2          e.g. vNET
│   │   │   ├── global_azure_service_3          e.g. App Service Plan
│   │   └── service
│   │       ├── service_specific_resource_1     e.g. App service
│   │       ├── service_specific_resource_2     e.g. Cosmos container
│   │       ├── service_specific_resource_3     e.g. Sql Database
│   └── public                 &lt;span class="nt"&gt;--&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; Higher-level modules
│       ├── environment     
│       └── webapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Import Script
&lt;/h3&gt;

&lt;p&gt;After coming up with this list of principles, I started creating the Terraform module for each resource type, importing it in a local state, running Terraform plan to ensure there are no changes and repeating till I created all the modules.&lt;/p&gt;

&lt;p&gt;To make the plan/import phase quick, I was applying the changes on a single lower-level module basis. &lt;/p&gt;

&lt;p&gt;Since I ended up importing the resources over and over and over again, I decided to write a small PowerShell script to help me speed up the process. &lt;/p&gt;

&lt;p&gt;This script tries to address the two main paint points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do not re-import a resource that's already imported &lt;/li&gt;
&lt;li&gt;Simplify reuse across environments via PowerShell string interpolation, whenever Azure resource Ids are predictable.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Please note that the last point depends on your resources naming conventions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The script looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Terraform init // Comment this out after the first execution

# Get all the items from Terraform state and put it inside an array
$stateItems = $(Terraform state list)

function ImportIfNotExists {
    param (
        [String]$resourceName,
        [String]$resourceId
    )

    if ($resourceId -eq $null -or $resourceId -eq "") {
        Write-Warning "Resource id for $resourceName is null"
        return
    }

    if ($stateItems -notcontains $resourceName.Replace("\", "")) {
        Write-Host "Importing $resourceName with id $resourceId"
        Terraform import "$resourceName" "$resourceId"
        if ($LASTEXITCODE -ne 0) {
            Write-Warning "Error importing $resourceName with id $resourceId"
        } else {
            Write-Host "$resourceName imported"
        }
    } else {
        Write-Host "$resourceName already exists"
    }
}

$env = "DEV"
$subscriptionId = "your-subscription-id-here"
$spokeResourceGroupName = "myrg-spoke-$env".ToLower()
$hubResourceGroupName = "myrg-hub-$env".ToLower()

$ErrorActionPreference  = "Stop"

## Resource group import
ImportIfNotExists 'module.environment.azurerm_resource_group.spoke_rg' "/subscriptions/$subscriptionId/resourceGroups/$spokeResourceGroupName"
ImportIfNotExists 'module.environment.azurerm_resource_group.hub_rg' "/subscriptions/$subscriptionId/resourceGroups/$hubResourceGroupName"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script allows me to quickly import resources and iterate faster since it allows me to re-run the same over and over without worrying about re-importing a resource that's already part of the state.&lt;/p&gt;

&lt;p&gt;As stated above, you can make the script reusable for multiple environments with few modifications. Some Azure resource ids are a bit more complex to figure out hence I usually do a manual lookup and find-and-replace.&lt;br&gt;
Some of those resources include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Cosmos Sql Role Definition (The Role id uses a guid so it's different for every role definition)&lt;/li&gt;
&lt;li&gt;Cosmos Sql Role Assignment (same as above)&lt;/li&gt;
&lt;li&gt;RBAC role assignment&lt;/li&gt;
&lt;li&gt;Automation account job schedules&lt;/li&gt;
&lt;li&gt;AAD Groups&lt;/li&gt;
&lt;li&gt;AAD Groups membership&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;Please note that your mileage may vary depending on the resources you use&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you want to, you can enhance the script to also look up these resources using the azure cli and a bit of &lt;a href="https://learn.microsoft.com/en-us/cli/azure/query-azure-cli?tabs=concepts,bash" rel="noopener noreferrer"&gt;JMESPath&lt;/a&gt;, for example, I'm doing this to look up AAD groups since in my case they follow a naming convention:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ImportIfNotExists 'sample.azuread_group.your_group_name' $(az ad group show --group "{your-group-name-prefix}-$env" --query id --output tsv)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here below you can see an example where I'm employing JMESPath to further filter the result of az cli to look up the role assignment for a given role and group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ImportIfNotExists 'sample.azurerm_role_assignment.your_group_assingments' $(az role assignment list --scope {your-scope} --query "[?principalName=='{you-principal-name}' &amp;amp;&amp;amp; roleDefinitionName=='{your-role-name}'].id" -o tsv)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;where:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;{your-scope}&lt;/strong&gt; is the resource you assigned the RBAC role assignment to (e.g. the resource group or a specific resource)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;{you-principal-name}&lt;/strong&gt; is the user name, group name or managed identity name of the principal that will be granted the role&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;{your-role-name}&lt;/strong&gt; is the name of the RBAC roles you assigned (e.g. Contributor)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is quite powerful and allows you to make the script parametric enough to allow you to reuse it for all environments. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It's also worth considering though that the import operation will be executed just once, so it may be quick to just do a find replace at times.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Quirks
&lt;/h2&gt;

&lt;p&gt;If you have declared resources that use &lt;code&gt;for_each&lt;/code&gt; in HCL, the name of the resource may contain (based on what you're foreach-ing) a string, e.g.&lt;br&gt;
imagine you're creating several service bus topic using a for_each in the following way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_servicebus_namespace"&lt;/span&gt; &lt;span class="s2"&gt;"example"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"tfex-servicebus-namespace"&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;            &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_resource_group&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;
  &lt;span class="nx"&gt;sku&lt;/span&gt;                 &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Standard"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_servicebus_topic"&lt;/span&gt; &lt;span class="s2"&gt;"topics"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;each&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;
  &lt;span class="nx"&gt;namespace_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_servicebus_namespace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;example&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;

  &lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;topics&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then the Terraform identifier will be something like the following: &lt;code&gt;module.servicebus.azurerm_servicebus_topic.topics["{topic-name}"]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;To make Terraform and PowerShell play nicely together in the import script, you have to write the above this way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ImportIfNotExists 'module.servicebus.azurerm_servicebus_topic.topics[\"{topic-name}\"]' "{servicebus-resource-id}"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To avoid the Terraform error: import requires you to specify two arguments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Useful resources
&lt;/h2&gt;

&lt;p&gt;To import a resource you need to find its unique identifier in Azure and this is not always easily doable from the portal so I took advantage of the following tools to make my life simpler&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://learn.microsoft.com/en-us/cli/azure/install-azure-cli" rel="noopener noreferrer"&gt;az cli&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="//resource.azure.com"&gt;resources.azure.com&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first one will be familiar to everyone, it's the azure command line tool and it's a must-have, the second one is a bit less known in my opinion but still an excellent resource to look into the definition of the various resources.&lt;/p&gt;

&lt;p&gt;This work took quite a bit of time but in the end, I was able to import all the resources in all the environments and come up with idiomatic HCL code.&lt;/p&gt;

&lt;p&gt;I hope you find this helpful!&lt;/p&gt;

&lt;p&gt;Till the next time.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>discuss</category>
      <category>career</category>
    </item>
    <item>
      <title>EF Core 7 is here - Welcome typed entity id 🍾</title>
      <dc:creator>Massimiliano Donini</dc:creator>
      <pubDate>Thu, 17 Nov 2022 18:59:46 +0000</pubDate>
      <link>https://dev.to/maxx_don/ef-core-7-is-here-welcome-typed-entity-id-1k3p</link>
      <guid>https://dev.to/maxx_don/ef-core-7-is-here-welcome-typed-entity-id-1k3p</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/ilmax/EfCoreTypedId" rel="noopener noreferrer"&gt;Source code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;EF 7 has been released at &lt;a href="https://www.dotnetconf.net/" rel="noopener noreferrer"&gt;dotnetconf&lt;/a&gt; and it brings a heap of new and exciting features. To read about all the new goodnes in this release you can go through the &lt;a href="https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew" rel="noopener noreferrer"&gt;What's new in EF Core 7&lt;/a&gt; docs page.&lt;/p&gt;

&lt;p&gt;One of the feature I'm more excited about that hasn't been properly advertised (hence this post), in my opinion, is support for what they call &lt;a href="https://learn.microsoft.com/en-us/ef/core/what-is-new/ef-core-7.0/whatsnew#value-generation-for-ddd-guarded-types" rel="noopener noreferrer"&gt;Value generation for DDD guarded types&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This neat new feature allow us to create custom types that wrap identifiers and supports value generation on the database side.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You could already do this in the past if you were providing a value yourself, but it was not supported to generate the value on the database.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The feature is also negatively advertised with a warning on the EF Core docs page saying that it adds complexity to the code so let's find out what's this about and what it allow us to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's primitive obsessions?
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;If you're a seasoned DDD practitioner you're probably familiar with this concept and you can skip this section altogether, if that's not the case keep reading.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Primitive obsession is a code smell and it's been defined as follows on hackernoon:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Primitive Obsession is a code smell in which primitive data types are used excessively to represent your data models.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What this means is that we fail to properly model some domain concepts and we instead use more permissive primitive data types.&lt;/p&gt;

&lt;p&gt;Sometimes using a more permissive type is a tradeoff forced by the limitations of the tools we use in our applications.&lt;/p&gt;

&lt;p&gt;With EF Core we were limited in how to model the entity primary key with the value to be generated by the database.&lt;br&gt;
If we wanted to use a sequence in the db to generate a monotonically increasing number for our entity id, we had to use an &lt;code&gt;int&lt;/code&gt; as the primary key property type.&lt;/p&gt;

&lt;p&gt;As an example let's use the following model used in most of the EF Core samples:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Blog&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Posts&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTime&lt;/span&gt; &lt;span class="n"&gt;PublishedOn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;As you can see, both &lt;strong&gt;Blog&lt;/strong&gt; and &lt;strong&gt;Post&lt;/strong&gt; entities have an &lt;code&gt;int&lt;/code&gt; primary key.&lt;/p&gt;

&lt;p&gt;This allows one subtle mistake not to be caught by the compiler: We can erroneously use the &lt;code&gt;Blog.Id&lt;/code&gt; value in places where we should use the &lt;code&gt;Post.Id&lt;/code&gt; or viceversa because both types are &lt;code&gt;int&lt;/code&gt; and satisfies the type system requirements event though they're conceptually two completely different things.&lt;br&gt;
Using the same type to represent different things besides opting out of compiler help, also hinders readability.&lt;/p&gt;

&lt;h2&gt;
  
  
  DDD typed id to the rescue
&lt;/h2&gt;

&lt;p&gt;Now with EF Core 7 we can easily avoid this problem defining two different types to represent the primary key of each entity and thanks to the C# feature &lt;code&gt;record struct&lt;/code&gt; we can even get away with it with similar performance characteristics. &lt;/p&gt;

&lt;p&gt;Let's see it in action in the new model:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Blog&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;Blog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BlogId&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;BlogId&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Name&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Posts&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Blog&lt;/span&gt; &lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Blog&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;struct&lt;/span&gt; &lt;span class="nf"&gt;BlogId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Post&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="nf"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PostId&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt; &lt;span class="n"&gt;publishedOn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;Content&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;PublishedOn&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;publishedOn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;PostId&lt;/span&gt; &lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Title&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt; &lt;span class="n"&gt;PublishedOn&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;Post&lt;/span&gt; &lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;struct&lt;/span&gt; &lt;span class="nf"&gt;PostId&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;In order for EF Core to understand how to map the two new types PostId and BlogId to the dB, we need to use value converters like the following:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BlogIdIdConverter&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ValueConverter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BlogId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;BlogIdIdConverter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PostIdIdConverter&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ValueConverter&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PostId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;PostIdIdConverter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;base&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// register value converters, we can take advantage of the new model building conventions feature and register the value converters only once for our whole context &lt;/span&gt;

&lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;ConfigureConventions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ModelConfigurationBuilder&lt;/span&gt; &lt;span class="n"&gt;configurationBuilder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;configurationBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BlogId&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="n"&gt;HaveConversion&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BlogIdIdConverter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
    &lt;span class="n"&gt;configurationBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PostId&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="n"&gt;HaveConversion&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PostIdIdConverter&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Last step is to configure the value generation for these entity keys in the OnModelCreating method&lt;/span&gt;

&lt;span class="n"&gt;modelBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Blog&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blog&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;blog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ValueGeneratedOnAdd&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;modelBuilder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Entity&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;().&lt;/span&gt;&lt;span class="nf"&gt;Property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ValueGeneratedOnAdd&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;



&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Implementing the entity ids this way allow use to fix the aforementioned problem since now the two types are different so we're unable to pass a &lt;code&gt;Blog.Id&lt;/code&gt; where we expect a &lt;code&gt;Post.Id&lt;/code&gt; or vice versa.&lt;br&gt;
This may seems like a small feature, but if you search the web, there're tons of articles that describe why this is useful (i.e. more expressive code, compile support, easier refactoring, etc).&lt;/p&gt;

&lt;h2&gt;
  
  
  Put it all together
&lt;/h2&gt;

&lt;p&gt;Let's see how this work:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;blog&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Blog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"My First Blog!"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blog&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChanges&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The code above produces the following Sql:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;

&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;IMPLICIT_TRANSACTIONS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;NOCOUNT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Blogs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;OUTPUT&lt;/span&gt; &lt;span class="n"&gt;INSERTED&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Note that since this is only inserting one value and the database already guarantees atomicity for a single insert, the statement is not wrapped into a transaction, one of the nice performance benefits that we will get for free just updating to EF Core 7.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now, add few posts:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;

&lt;span class="n"&gt;blog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"First post"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"EF Core is awesome"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;blog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Second post"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Typed Ids are amazing"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SaveChanges&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Produces the following Sql:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;

&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;IMPLICIT_TRANSACTIONS&lt;/span&gt; &lt;span class="k"&gt;OFF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;NOCOUNT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;MERGE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;USING&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;p7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;BlogId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PublishedOn&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;_Position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="n"&gt;MATCHED&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;BlogId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PublishedOn&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;BlogId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;PublishedOn&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Title&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;OUTPUT&lt;/span&gt; &lt;span class="n"&gt;INSERTED&lt;/span&gt;&lt;span class="p"&gt;.[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_Position&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; 


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;And now the reading part, reading a blog from the db:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;

&lt;span class="n"&gt;BlogId&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;blog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;blogFromDb&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Blogs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SingleOrDefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blog&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;blog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Produces the expected sql:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;

&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;TOP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Blogs&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;].[&lt;/span&gt;&lt;span class="n"&gt;Id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;__id_0&lt;/span&gt;


&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;As you can see everything works smoothly as you'd expect and with little more code, you also have some additional type safety that comes in handy especially at refactoring time, and if you, by mistake, use a &lt;code&gt;Post.Id&lt;/code&gt; where a &lt;code&gt;Blog.Id&lt;/code&gt; is expected, you get a nice compiler error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caveats
&lt;/h2&gt;

&lt;p&gt;I implemented the &lt;code&gt;BlogId&lt;/code&gt; and &lt;code&gt;PostId&lt;/code&gt; using records to keep the code succinct, in real life you may want to add a bit more to it, like for example overriding ToString to only print value, and maybe add some validation to make sure you can't create a negative value and so on, using a struct also has some similar performance characteristics of using an &lt;code&gt;int&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Please also note that EF Core 7 has few issues that will be resolved in the coming months so you may want to wait for some of these issues to be resolved before pushing it to prod.&lt;br&gt;
&lt;iframe class="tweet-embed" id="tweet-1593244935939305474-661" src="https://platform.twitter.com/embed/Tweet.html?id=1593244935939305474"&gt;
&lt;/iframe&gt;

  // Detect dark theme
  var iframe = document.getElementById('tweet-1593244935939305474-661');
  if (document.body.className.includes('dark-theme')) {
    iframe.src = "https://platform.twitter.com/embed/Tweet.html?id=1593244935939305474&amp;amp;theme=dark"
  }



 &lt;/p&gt;

&lt;p&gt;I hope you enjoyed this article, till the next time!&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>entityframework</category>
      <category>ddd</category>
      <category>sql</category>
    </item>
    <item>
      <title>Azure WebJobs, Service Bus and Managed Identity: Lesson learned</title>
      <dc:creator>Massimiliano Donini</dc:creator>
      <pubDate>Tue, 09 Aug 2022 13:20:02 +0000</pubDate>
      <link>https://dev.to/maxx_don/azure-webjobs-service-bus-and-managed-identity-lesson-learned-4jhp</link>
      <guid>https://dev.to/maxx_don/azure-webjobs-service-bus-and-managed-identity-lesson-learned-4jhp</guid>
      <description>&lt;p&gt;Today I was converting some Azure webjobs to connect to Azure Service Bus using managed service identity (MSI).&lt;/p&gt;

&lt;p&gt;The application is a simple C# Azure WebJob built using the Azure WebJob SDK that subscribe to a topic and process incoming message writing to a database.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;These are the nuget packages used:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Microsoft.Azure.WebJobs v 3.0.33&lt;/li&gt;
&lt;li&gt;Microsoft.Azure.WebJobs.Extensions.ServiceBus v 5.6.0
Please note that since Azure Functions are built on top of the WebJobs SDK, you may encounter the same issue there, I haven't verified though.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;In order to grant the required permission, I created a security group and added the managed identity of the app service to the group, then I proceeded to grant this service group the &lt;a href="https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#azure-service-bus-data-owner"&gt;Azure Service Bus Data Owner&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;According to the description, this role should have full access to the whole Service Bus namespace, so imagine my surprise when I tried to run the application and got an error that looks like the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unauthorized access. 'Listen' claim(s) are required to perform this operation. 
Resource: 'sb://{namespace-name}.servicebus.windows.net/{topic-name}/subscriptions/{service-name}'.
TrackingId:4e956a067b044b1089b5e327c0d08fd0_G9, SystemTracker:gateway7, Timestamp:2022-08-05T15:32:58 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After several trial and error, this is what I found:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If you assign the permission &lt;strong&gt;Azure Service Bus Data Owner&lt;/strong&gt; to a group and the managed identity of the application is part of the group, it won't work, no matter which permission you grant ❌&lt;/li&gt;
&lt;li&gt;If you assign the managed service identity the permission &lt;strong&gt;Azure Service Bus Data Owner&lt;/strong&gt; it won't work ❌&lt;/li&gt;
&lt;li&gt;If you assign the permission &lt;strong&gt;Azure Service Bus Data Receiver&lt;/strong&gt; to a group and the managed identity of the application is part of the group, it will work ✅&lt;/li&gt;
&lt;li&gt;If you assign the managed service identity the permission &lt;strong&gt;Azure Service Bus Data Receiver&lt;/strong&gt;, it will work ✅&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Please note it may take up to 5 minutes for the permission changes to be applied, so you may still experience failures after applying them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The only way to get it working was to assign the managed identity the &lt;strong&gt;Azure Service Bus Data Receiver&lt;/strong&gt; role to either the service identity or the security group.&lt;/p&gt;

&lt;p&gt;This seems either a bug in the Azure SDK or in the Service Bus itself, I'm not the only one that ran into this &lt;a href="https://github.com/Azure/azure-sdk-for-net/issues/24289"&gt;issue&lt;/a&gt; and here you can find additional information.&lt;/p&gt;

&lt;p&gt;Till the next time.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>csharp</category>
    </item>
    <item>
      <title>Dynamically scale down AppService outside business hours to save 💰💰</title>
      <dc:creator>Massimiliano Donini</dc:creator>
      <pubDate>Mon, 25 Jul 2022 05:51:35 +0000</pubDate>
      <link>https://dev.to/maxx_don/dynamically-scale-down-appservice-outside-business-hours-to-save-431a</link>
      <guid>https://dev.to/maxx_don/dynamically-scale-down-appservice-outside-business-hours-to-save-431a</guid>
      <description>&lt;p&gt;The other day I was on a quest to lower a bit our Azure spending.&lt;/p&gt;

&lt;p&gt;Im my current company we have several environment that we use for different purposes, Development, Test, Acceptance and so on.&lt;/p&gt;

&lt;p&gt;All these environments have slightly different tiers for various services and I was wondering how to lower App Service Plan tier outside business hours.&lt;/p&gt;

&lt;p&gt;App Services have some built-in, albeit limited, capabilities to scale but this only involves scaling out.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scaling out&lt;/strong&gt; is the process of adding additional instances of our application to adapt to an increasing load.&lt;br&gt;
&lt;strong&gt;Scaling up&lt;/strong&gt; is the process of running the application on a more performant hardware. &lt;/p&gt;

&lt;p&gt;Since there's no built-in support to scale up &amp;amp; down in App Services, I had to come up with a custom solution.&lt;/p&gt;

&lt;p&gt;After a bit of research, I ended up creating an Azure automation account, two runbooks that execute on a schedule the scale down and scale up of our App Service Plan.&lt;/p&gt;

&lt;p&gt;It turned out to be extremely simple to implement yet effective.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Disclaimer: This is just one of the possible way to scale services up &amp;amp; down outside business hours, you can achieve the same with a scheduled github action or Azure DevOps pipeline than runs your IaC code with different Sku parameter values for example.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here's what I've created:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Azure automation account&lt;/li&gt;
&lt;li&gt;Azure Runbooks&lt;/li&gt;
&lt;li&gt;Azure automation schedule&lt;/li&gt;
&lt;li&gt;Azure automation account variables&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Azure automation account - &lt;a href="https://docs.microsoft.com/en-us/azure/automation/overview"&gt;Docs&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;This is the go to resource to automate processes in Azure, where you define the runbooks, the schedule and the variables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Azure Runbooks - &lt;a href="https://docs.microsoft.com/en-us/azure/automation/overview"&gt;Docs&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;This is where I defined what needs to happen when the schedule triggers the runbook and starts a job.&lt;br&gt;
There are several &lt;a href="https://docs.microsoft.com/en-us/azure/automation/automation-runbook-types"&gt;different types&lt;/a&gt; of runbooks, here I chose the powershell one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Azure automation schedule - &lt;a href="https://docs.microsoft.com/en-us/azure/automation/shared-resources/schedules"&gt;Docs&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;This is where I defined when to execute our runbooks, I went with a weekly schedule to scale down resource in the evening and scale them back up early in the morning. &lt;/p&gt;

&lt;h2&gt;
  
  
  Azure automation account variables - &lt;a href="https://docs.microsoft.com/en-us/azure/automation/shared-resources/variables?tabs=azure-powershell"&gt;Docs&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;This is where I defined few variables used by the runbook.&lt;br&gt;
This step is optional since you can potentially hardcode everything in the runbook itself, but if you want to use the same runbook across different environment, you can define variables and read them in the runbook.&lt;br&gt;
I defined few variables, one for the resource group name, one for the app service plan name and the desired scale down sku and the one the needs to be used during business hours.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In order for the runbook to successfully change the App Service Plan, we also need to grant the identity - either managed identity or user assigned one - of the automation account enough grant on the App Service Plan. I went with managed identity.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Putting it all together
&lt;/h2&gt;

&lt;p&gt;After creating the variables, the schedules and the runbooks I linked the schedule to the runbook.&lt;br&gt;
I created two schedules called scale-down and scale-up, two runbooks named the same way and linked the schedule to the runbook. You link a runbook to a schedule in the overview page of the runbook itself.&lt;/p&gt;

&lt;p&gt;Last missing part is the code of the runbook itself, so here's the shortest possible version of it (of course you can make it smarter based on your needs) used to scale down, the scale up version is exactly the same but read a different variable for the sku.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;
 

&lt;p&gt;Till the next one!&lt;/p&gt;

</description>
      <category>azure</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Zero downtime deployment with Azure Container Apps and Github Actions - Part 1</title>
      <dc:creator>Massimiliano Donini</dc:creator>
      <pubDate>Fri, 24 Jun 2022 16:21:26 +0000</pubDate>
      <link>https://dev.to/maxx_don/zero-downtime-deployment-with-azure-container-apps-and-github-actions-part-1-25ol</link>
      <guid>https://dev.to/maxx_don/zero-downtime-deployment-with-azure-container-apps-and-github-actions-part-1-25ol</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;As you may know, Azure Container Apps went out of preview during Microsoft Build in late May this year.&lt;br&gt;
Azure Container Apps is a very interesting service that runs on top of Kubernetes adding some additional powerful capabilities in a simple and covinient way.&lt;br&gt;
Some of these capabilities are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Built in support for Keda autoscalers&lt;/li&gt;
&lt;li&gt;Built in support for Dapr components&lt;/li&gt;
&lt;li&gt;Ability to scale to zero&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's a lot more to it, you can dig deeper on the official Microsoft documentation &lt;a href="https://docs.microsoft.com/en-us/azure/container-apps/"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TL;DR; All the code described in this article is available on Github &lt;a href="https://github.com/ilmax/container-apps-sample/releases/tag/v0.3"&gt;here&lt;/a&gt;&lt;br&gt;
I'm still working on improvements so main branch may be updated by the time you read this.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Requirements
&lt;/h2&gt;

&lt;p&gt;I would like to migrate several Azure App Services to Azure Container Apps, I have two different type of services, http api that write to a Service Bus queue/topic and web jobs that consume and process the messages, a pretty common setup these days.&lt;/p&gt;

&lt;p&gt;Azure Container Apps allows me to easily scale the web jobs based on the amount of messages present in the queue/topic and also makes it easy to scale to zero outside business hours.&lt;/p&gt;

&lt;p&gt;In order to migrate from Azure App Service to Azure Container Apps I want to implement a zero downtime deployment for the http api services.&lt;/p&gt;
&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;Azure Container Apps has built in support for &lt;a href="https://docs.microsoft.com/en-us/azure/container-apps/health-probes"&gt;health probes&lt;/a&gt;, there are 3 types of health probes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Liveness&lt;/li&gt;
&lt;li&gt;Readiness&lt;/li&gt;
&lt;li&gt;Startup&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Given the built-in health probes support, I went under the assumption that, while using single revision mode, we could just deploy another revision and the control plane could take care of warming up and then swapping traffic to the new revision without downtime.&lt;br&gt;
To my surprise I figured out that's not the case and if you're reading this blog post, you might have noticed that too.&lt;/p&gt;

&lt;p&gt;I also double checked it on &lt;a href="https://aka.ms/containerapps-discord"&gt;Discord&lt;/a&gt; with the Azure Container Apps team if I was missing something on my end, but they confirmed my findings.&lt;/p&gt;



&lt;p&gt;After doing a bit of research, I figured it out that I can implement my own workflow to implement zero downtime deployment. This is far from ideal but still better than a deployment process that causes downtime.&lt;br&gt;
Hopefully Azure Container Apps will implement built support for zero downtime deployment, but in the meantime the following approach is an acceptable workaround.&lt;/p&gt;
&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;In order to implement zero downtime deployment in (pseudo) single revisions mode (meaning using multiple revision mode with a single active revision at a time, serving all the traffic), we need to do the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Redirect all the traffic to the latest revision by name (more on that later)&lt;/li&gt;
&lt;li&gt;Deploy a new revision&lt;/li&gt;
&lt;li&gt;Warm up the new revision&lt;/li&gt;
&lt;li&gt;Redirect the traffic to the newly deployed revision&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;In this blog post I am using multiple revision mode because it happened to me that moving from single revision to multiple revision mode caused downtime - I'm currently investigating it and will update this post as soon as I found.&lt;br&gt;
The idea is to have the container app configured in multiple revision mode but only have one active revision at a time.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Regarding step 1, Redirect all the traffic to the latest revision by name, has to do with the &lt;strong&gt;latest&lt;/strong&gt; revision alias.&lt;br&gt;
I found that if the traffic is set to the latest revision, when deploying a new revision, the traffic get redirected to the new revision even while it's still in the provisioning state, leading to possible timeouts and failures on the caller side.&lt;/p&gt;

&lt;p&gt;The workflow above is a bit long but not particularly difficult, and we can easily implement it with the help of the Azure Cli &lt;strong&gt;containerapp&lt;/strong&gt; extension.&lt;/p&gt;

&lt;p&gt;Out of all the steps above, point 3 (Warm up the new revision) is the most tricky since different services may have different health probe configurations. I really didn't want to duplicate health probe configurations in the infrastructure and in the deployment pipeline but rather reuse what has been configured in the Container App Container instead.&lt;/p&gt;

&lt;p&gt;Dynamically discovering and calling the health probe in bash is doable but I am a bit more proficient writing that code in a high level language so I decided to write a small C# application to do that.&lt;/p&gt;

&lt;p&gt;In order to manage Azure resources, we can use the Azure Management SDK, this is a set of packages that allows you to manage Azure resources in your language of choice.&lt;br&gt;
You can find all the supported resources &lt;a href="https://azure.github.io/azure-sdk-for-net/"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Luckily, Azure Container Apps already have an SDK available, although in beta at the moment of writing.&lt;/p&gt;

&lt;p&gt;Since I should be able to invoke this application from a  Github Action, I decided to implementing a web application that exposes an api to make my life easy.&lt;/p&gt;

&lt;p&gt;Essentially all the steps described above but steps 4 are executed in a github action, while step 4 is executed by a web application invoked by cURL in the Github Actions.&lt;/p&gt;

&lt;p&gt;The next point to solve is where should be this web application deployed, we have few options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Have it always available&lt;/li&gt;
&lt;li&gt;Make it available on demand&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;If you deploy an internal Azure Container App Environment and the application is not exposed to the internet, point two may be a bit more complicated since you need to make sure the github action runner can reach the Azure Container App you want to warm up.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I initially went with the first approach, having the application always available, but since I didn't want to also add authentication to the mix (to make sure only verified clients can call the warmup endpoint), I later decided against it.&lt;/p&gt;

&lt;p&gt;In order to make this application available on demand, I decided to use Github Actions service container.&lt;br&gt;
This post is getting already a bit too long so I won't go into detail of what service container is, let's just say that it allows you to run a container and made it available on the runner. If you wanna dig deeper, you can check the documentation &lt;a href="https://docs.github.com/en/actions/using-containerized-services/about-service-containers"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After sorting the Github Actions service container, last problem that I had to tackle was how to authenticate the application against Azure.&lt;br&gt;
Thanks to &lt;strong&gt;Azure.Identity&lt;/strong&gt; package and the &lt;strong&gt;DefaultAzureCredential&lt;/strong&gt; class, we can just set some environment variables and authenticate with a previously defined service principal.&lt;/p&gt;
&lt;h3&gt;
  
  
  Azure management SDK authentication
&lt;/h3&gt;

&lt;p&gt;In order to get the required credentials to authenticate, we need to create a service principal with the &lt;strong&gt;Reader&lt;/strong&gt; role on the resource group that contains the Azure Container Apps. &lt;br&gt;
We can quickly create this with the az cli and the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az ad sp create-for-rbac &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"HealthProbeSp"&lt;/span&gt; &lt;span class="nt"&gt;--role&lt;/span&gt; Contributor &lt;span class="nt"&gt;--scopes&lt;/span&gt; /subscriptions/&lt;span class="o"&gt;{&lt;/span&gt;subscriptionId&lt;span class="o"&gt;}&lt;/span&gt;/resourceGroups/&lt;span class="o"&gt;{&lt;/span&gt;resourceGroupId&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Remember to replace the subscription and resource group names&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After setting all this up, I was able to implement zero downtime deployment.&lt;/p&gt;

&lt;p&gt;Here's an extract of the Github Action:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build ${{ matrix.services.appName }}&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;packages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
    &lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;health-invoker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io/${{ github.repository }}/health-invoker:main&lt;/span&gt;
        &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;5000:80&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;AZURE_TENANT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_TENANT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;AZURE_CLIENT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;  &lt;span class="s"&gt;${{ secrets.AZURE_CLIENT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;AZURE_CLIENT_SECRET&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_CLIENT_SECRET }}&lt;/span&gt;
          &lt;span class="na"&gt;Azure__SubscriptionId&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.AZURE_SUBSCRIPTION_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;Azure__ResourceGroupName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.RESOURCE_GROUP_NAME }}&lt;/span&gt;

&lt;span class="c1"&gt;# Clone repo, Build and push omitted for brevity &lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy azure container app without downtime&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;github.event_name != 'pull_request' &amp;amp;&amp;amp; matrix.services.zeroDowntime == &lt;/span&gt;&lt;span class="no"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;echo "Installing containerapp extension"&lt;/span&gt;
          &lt;span class="s"&gt;az extension add --name containerapp --upgrade &amp;amp;&amp;gt; /dev/null&lt;/span&gt;
          &lt;span class="s"&gt;echo "Get latest active revision name"&lt;/span&gt;
          &lt;span class="s"&gt;latest_revision=$(az containerapp show -n ${{ matrix.services.appName }} -g ${{ secrets.RESOURCE_GROUP_NAME }} --query properties.latestRevisionName -o tsv)&lt;/span&gt;
          &lt;span class="s"&gt;echo "Redirect traffic to active revision $latest_revision"&lt;/span&gt;
          &lt;span class="s"&gt;az containerapp ingress traffic set -n ${{ matrix.services.appName }} -g ${{ secrets.RESOURCE_GROUP_NAME }} --revision-weight $latest_revision=100 &amp;amp;&amp;gt; /dev/null&lt;/span&gt;
          &lt;span class="s"&gt;echo "Create new revision"&lt;/span&gt;
          &lt;span class="s"&gt;az containerapp update -n ${{ matrix.services.appName }} -g ${{ secrets.RESOURCE_GROUP_NAME }} -i ${{ steps.image-tag.outputs.tag }} &amp;amp;&amp;gt; /dev/null&lt;/span&gt;
          &lt;span class="s"&gt;new_revision=$(az containerapp show -n ${{ matrix.services.appName }} -g ${{ secrets.RESOURCE_GROUP_NAME }} --query properties.latestRevisionName -o tsv)&lt;/span&gt;
          &lt;span class="s"&gt;echo "Warmup new revision at ${{ env.WARMUP_APP }}/warmup/${{ matrix.services.appName }}"&lt;/span&gt;
          &lt;span class="s"&gt;health_response_status=$(curl -m 180 --write-out "%{http_code}\n" -s ${{ env.WARMUP_APP }}/warmup/${{ matrix.services.appName }} --output backend.txt)&lt;/span&gt;
          &lt;span class="s"&gt;if [ $health_response_status = "200" ]; then&lt;/span&gt;
            &lt;span class="s"&gt;echo "Redirect traffic to new revision $new_revision"&lt;/span&gt;
            &lt;span class="s"&gt;az containerapp ingress traffic set -n ${{ matrix.services.appName }} -g ${{ secrets.RESOURCE_GROUP_NAME }} --revision-weight $new_revision=100 $latest_revision=0 &amp;amp;&amp;gt; /dev/null&lt;/span&gt;
            &lt;span class="s"&gt;echo "Deactivate revision $latest_revision"&lt;/span&gt;
            &lt;span class="s"&gt;az containerapp revision deactivate -n ${{ matrix.services.appName }} -g ${{ secrets.RESOURCE_GROUP_NAME }} --revision $latest_revision &amp;amp;&amp;gt; /dev/null&lt;/span&gt;
          &lt;span class="s"&gt;else&lt;/span&gt;
            &lt;span class="s"&gt;echo "Warmup failed with status code $health_response_status"&lt;/span&gt;
            &lt;span class="s"&gt;cat ./backend.txt&lt;/span&gt;
            &lt;span class="s"&gt;echo "Redirect traffic to active revision $latest_revision"&lt;/span&gt;
            &lt;span class="s"&gt;az containerapp ingress traffic set -n ${{ matrix.services.appName }} -g ${{ secrets.RESOURCE_GROUP_NAME }} --revision-weight $latest_revision=100 &amp;amp;&amp;gt; /dev/null&lt;/span&gt;
            &lt;span class="s"&gt;if [ ! -z "$new_revision" ]; then&lt;/span&gt;
              &lt;span class="s"&gt;echo "Deactivate revision $new_revision"&lt;/span&gt;
              &lt;span class="s"&gt;az containerapp revision deactivate -n ${{ matrix.services.appName }} -g ${{ secrets.RESOURCE_GROUP_NAME }} --revision $new_revision &amp;amp;&amp;gt; /dev/null&lt;/span&gt;
            &lt;span class="s"&gt;fi&lt;/span&gt;
            &lt;span class="s"&gt;exit 1&lt;/span&gt;
          &lt;span class="s"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the output of &lt;a href="https://github.com/wg/wrk"&gt;wrk&lt;/a&gt; while deploying a new revision:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wrk -t12 -c400 -d30s https://xxxxxxxx.azurecontainerapps.io/api/echo/ping
Running 30s test @ https://xxxxxxxx.azurecontainerapps.io/api/echo/ping
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   268.53ms  123.73ms   1.02s    68.90%
    Req/Sec   137.15    103.17     1.15k    66.59%
  41604 requests in 30.10s, 8.89MB read
Requests/sec:   1382.32
Transfer/sec:    302.38KB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After deployment has been completed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;wrk -t12 -c400 -d30s https://xxxxxxxx.azurecontainerapps.io/api/echo/ping
Running 30s test @ https://xxxxxxxx.azurecontainerapps.io/api/echo/ping
  12 threads and 400 connections
  Thread Stats   Avg      Stdev     Max   +/- Stdev
    Latency   251.00ms  135.47ms   1.24s    77.22%
    Req/Sec   148.38    104.52   434.00     63.10%
  44970 requests in 30.09s, 9.61MB read
Requests/sec:   1494.36
Transfer/sec:    326.89KB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see there's almost no difference and, most importantly, wrk doesn't indicate any non 2XX or 3XX response meaning that we were able to serve all requests while deploying a new revision.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The numbers are quite low because I'm using a very small configuration for testing purposes (0.25 Cores and 0.5 Gi of memory)&lt;/p&gt;
&lt;/blockquote&gt;




&lt;p&gt;I hope you find this helpful and if you have suggestions , don’t hesitate to comment or reach me out via twitter at &lt;a href="//twitter.com/maxx_don"&gt;twitter.com/maxx_don&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Stay tuned for part 2 that will be out very soon!&lt;/p&gt;

</description>
      <category>azure</category>
      <category>github</category>
      <category>devops</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Create Azure Container Apps with terraform</title>
      <dc:creator>Massimiliano Donini</dc:creator>
      <pubDate>Thu, 26 May 2022 08:33:58 +0000</pubDate>
      <link>https://dev.to/maxx_don/create-azure-container-apps-with-terraform-5bk</link>
      <guid>https://dev.to/maxx_don/create-azure-container-apps-with-terraform-5bk</guid>
      <description>&lt;p&gt;Microsoft announced at Microsoft Build that &lt;a href="https://azure.microsoft.com/en-us/services/container-apps/"&gt;Azure Container Apps&lt;/a&gt; are now generally available (GA).&lt;/p&gt;

&lt;p&gt;If you're not familiar with Azure Container Apps (ACA) I suggest you to go and check out the documentation &lt;a href="https://docs.microsoft.com/en-us/azure/container-apps/?ocid=AID3042118"&gt;here&lt;/a&gt;. &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Fully managed serverless container service for building and deploying modern apps at scale&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is how Microsoft itself markets the product.&lt;/p&gt;

&lt;p&gt;I think it's a very interesting platform and it offers some of the benefits of Kubernetes, abstracting a lot of concepts and complexity.&lt;/p&gt;

&lt;p&gt;In order to start playing around with it, I usually create a repo with some terraform code so I can spin up a set of resources, play with them and then destroy all of them when I'm done.&lt;/p&gt;

&lt;p&gt;As you may know not all Azure resources are available in terraform azure provider(s) from day 1, but for a month or so we have the awesome &lt;strong&gt;AzApi&lt;/strong&gt; provider for terraform.&lt;/p&gt;

&lt;p&gt;Follow &lt;a href="https://github.com/hashicorp/terraform-provider-azurerm/issues/14122"&gt;this&lt;/a&gt; github issue that tracks adding support for it in the &lt;strong&gt;azurerm&lt;/strong&gt; official provider.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The AzAPI provider enables you to manage any Azure resource type using any API version.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Thanks to this provider it is very easy to create an Azure Container App with terraform.&lt;/p&gt;

&lt;p&gt;Here's how you configure the provider:&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;p&gt;Here you can see how you can use &lt;strong&gt;azapi_resource&lt;/strong&gt; to create an Azure Container App&lt;br&gt;
&lt;/p&gt;
&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;


&lt;h2&gt;
  
  
  Tips
&lt;/h2&gt;

&lt;p&gt;In order to discover the properties, I first make the changes it in the Azure portal, then I'm running the following &lt;strong&gt;az-cli&lt;/strong&gt; command:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;az containerapp list&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;and then you need to transform the json to the terraform equivalent.&lt;/p&gt;

&lt;p&gt;You can also install &lt;a href="https://marketplace.visualstudio.com/items?itemName=azapi-vscode.azapi"&gt;Terraform AzApi Provider Visual Studio Code Extension&lt;/a&gt; VS Code extension that should provide completion support.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Kudos to &lt;a href="https://github.com/piizei"&gt;piizei&lt;/a&gt; for coming up with the suggestion &lt;a href="https://github.com/hashicorp/terraform-provider-azurerm/issues/14122#issuecomment-1101561028"&gt;here&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you want to see a more advanced example, you can give a look at my repo &lt;a href="https://github.com/ilmax/container-apps-sample/blob/main/infra/container-app.tf"&gt;here&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;Hope you find this helpful, if you have any question/suggestion, don't hesitate to comment!&lt;/p&gt;

</description>
      <category>azure</category>
      <category>terraform</category>
      <category>cloudnative</category>
    </item>
    <item>
      <title>Using Managed Identity with Azure WebJobs and Service Bus</title>
      <dc:creator>Massimiliano Donini</dc:creator>
      <pubDate>Tue, 24 May 2022 14:09:21 +0000</pubDate>
      <link>https://dev.to/maxx_don/using-managed-identity-with-azure-webjobs-and-service-bus-5a1n</link>
      <guid>https://dev.to/maxx_don/using-managed-identity-with-azure-webjobs-and-service-bus-5a1n</guid>
      <description>&lt;p&gt;Managed Service Identity (or MSI for short) allows Azure resources to connect to Azure services that supports AD authentication (see the full list &lt;a href="https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/services-azure-active-directory-support"&gt;here&lt;/a&gt;) without using secrets. &lt;br&gt;
This is extremely useful because handling secrets the proper way it's &lt;a href="https://docs.microsoft.com/en-us/azure/cloud-adoption-framework/secure/best-practices/manage-secrets"&gt;far from easy&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;How MSI works is beyond the scope of the article and you can find more information &lt;a href="https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/managed-identities-status"&gt;here&lt;/a&gt;, but in a nutshell:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You create a service principal object and an application in Azure AD to represents your service (this is done automatically when turning on managed identity) &lt;/li&gt;
&lt;li&gt;You grant some permissions to access the downstream service it needs to communicate to&lt;/li&gt;
&lt;li&gt;You configure the authentication to the downstream service to be done via MSI&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In my case, I have a WebJob that processes messages via a ServiceBus queue, so I granted the service principal a permission to read from the queue using the built in &lt;a href="https://docs.microsoft.com/en-us/azure/role-based-access-control/built-in-roles#azure-service-bus-data-receiver"&gt;Azure Service Bus Data Receiver&lt;/a&gt; role.&lt;/p&gt;

&lt;p&gt;The WebJob SDK supports connecting to the ServiceBus using MSI as described &lt;a href="https://docs.microsoft.com/en-us/dotnet/api/overview/azure/microsoft.azure.webjobs.extensions.servicebus-readme-pre#managed-identity-authentication"&gt;here&lt;/a&gt; so I went ahead and configured the WebJob in the following way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"ServiceBusConnection__fullyQualifiedNamespace"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;service_bus_namespace&amp;gt;.servicebus.windows.net"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nl"&gt;"QueueName"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"test-queue"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The queue and the permission were defined in terraform as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Define the queue&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_servicebus_queue"&lt;/span&gt; &lt;span class="s2"&gt;"msi-test-queue"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"test-queue"&lt;/span&gt;
  &lt;span class="nx"&gt;namespace_id&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_servicebus_namespace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;msi&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;enable_partitioning&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Grant the WebJob Azure Service Bus Data Receiver permissions&lt;/span&gt;
&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_role_assignment"&lt;/span&gt; &lt;span class="s2"&gt;"consumer_service_bus_read"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;scope&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_servicebus_queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;msi&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;role_definition_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Azure Service Bus Data Receiver"&lt;/span&gt;
  &lt;span class="nx"&gt;principal_id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azapi_resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;consumer_container_app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;principal_id&lt;/span&gt;
  &lt;span class="nx"&gt;depends_on&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;azapi_resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;consumer_container_app&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The WebJob definition in C# is something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;FunctionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Processor"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ProcessEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ServiceBusTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"%QueueName%"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"ServiceBusConnection"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IsSessionsEnabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="n"&gt;ServiceBusReceivedMessage&lt;/span&gt; &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ServiceBusMessageActions&lt;/span&gt; &lt;span class="n"&gt;messageActions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;....&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Where the string &lt;strong&gt;ServiceBusConnection&lt;/strong&gt; points to the name of the configuration value that contains the connection string to the Service Bus and the string &lt;strong&gt;%QueueName%&lt;/strong&gt; point to the configuration value that contains the queue name.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you don't use the percent sign, the string will be the name of the queue and it will be hardcoded, adding the %% allows you to configure dynamically via a configuration lookup.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This unfortunately didn't work, the WebJob was throwing exception at startup complaining about permissions, the message was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Unauthorized access. 'Listen' claim(s) are required to perform this operation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This was unexpected since the permission was there and I double checked it in the Azure portal.&lt;/p&gt;

&lt;p&gt;The only way I was able to get this working was to grant &lt;strong&gt;Azure Service Bus Data Receiver&lt;/strong&gt; permission on the whole service bus namespace.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight terraform"&gt;&lt;code&gt;&lt;span class="k"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_role_assignment"&lt;/span&gt; &lt;span class="s2"&gt;"consumer_service_bus_read"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;scope&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azurerm_servicebus_namespace&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;msi&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;test&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;sb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;role_definition_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Azure Service Bus Data Receiver"&lt;/span&gt;
  &lt;span class="nx"&gt;principal_id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azapi_resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;consumer_container_app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;principal_id&lt;/span&gt;
  &lt;span class="nx"&gt;depends_on&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;azapi_resource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;consumer_container_app&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One caveat of this approach is that it grants more permissions than strictly required, but at least it got me unblocked.&lt;/p&gt;

&lt;p&gt;If you're interested into the source code, you can find it &lt;a href="https://github.com/ilmax/container-apps-sample"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I hope you find this useful and if you have any questions/suggestions feel free to comment here below!&lt;/p&gt;

</description>
      <category>azure</category>
      <category>servicebus</category>
      <category>terraform</category>
    </item>
    <item>
      <title>Implement Azure AD Workload Identity on AKS with terraform</title>
      <dc:creator>Massimiliano Donini</dc:creator>
      <pubDate>Thu, 24 Feb 2022 07:43:47 +0000</pubDate>
      <link>https://dev.to/maxx_don/implement-azure-ad-workload-identity-on-aks-with-terraform-3oho</link>
      <guid>https://dev.to/maxx_don/implement-azure-ad-workload-identity-on-aks-with-terraform-3oho</guid>
      <description>&lt;p&gt;Azure makes it very easy to create managed identities for a variety of services (e.g. Azure Functions, App Services, Logic Apps...), but when we want to implement it for Azure Kubernetes Service, things gets just a bit more complicated.&lt;/p&gt;

&lt;p&gt;First of all we have few options to choose from:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/Azure/aad-pod-identity"&gt;AAD Pod Identity&lt;/a&gt; (deprecated)&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://azure.github.io/azure-workload-identity/docs/introduction.html"&gt;Azure AD Workload Identity&lt;/a&gt; What we discuss in this post, azwi for brevity.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both solutions aims to associate a pod with an identity in Azure Active Directory so we can grant this identity permissions to access another resource (i.e. a storage account or an Azure Sql Database).&lt;/p&gt;

&lt;p&gt;As described on the documentation, azwi is the suggested approach from now on since Azure AD Pod Identity has been (somehow) deprecated as you can read on the &lt;a href="https://github.com/Azure/aad-pod-identity"&gt;github repo&lt;/a&gt; and on the blog post &lt;a href="https://cloudblogs.microsoft.com/opensource/2022/01/18/announcing-azure-active-directory-azure-ad-workload-identity-for-kubernetes/"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The documentation describes Azure AD Workload Identity as follows:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Azure AD Workload Identity for Kubernetes integrates with the capabilities native to Kubernetes to federate with external identity providers. This approach is simpler to use and deploy, and overcomes several limitations in Azure AD Pod Identity&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Assuming you already have an AKS cluster up &amp;amp; running (I won't cover the creation of it here), in order to configure &lt;strong&gt;Azure AD Workload Identity&lt;/strong&gt; we need to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Configure the AKS cluster to enable OIDC issuer&lt;/li&gt;
&lt;li&gt;Deploy the Azure AD Workload Identity helm chart to the cluster&lt;/li&gt;
&lt;li&gt;Create a Federated Azure AD Application + a Service Principal&lt;/li&gt;
&lt;li&gt;Create a kubernetes service account manifest with some azwi specific metadata&lt;/li&gt;
&lt;li&gt;Configure our pods to run with the service account&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  1. Configure the AKS cluster to enable OIDC issuer
&lt;/h2&gt;

&lt;p&gt;Unfortunately since OIDC issuer feature is still in preview at the time of writing (February 2022), there's no built-in support in terraform, but this is a one time only operation, you can read more about it &lt;a href="https://docs.microsoft.com/en-us/azure/aks/cluster-configuration#oidc-issuer-preview"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;So we need to enable it from the azure cli with the following command:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Enable &lt;strong&gt;az cli&lt;/strong&gt; preview feature&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Install the aks-preview extension
az extension add --name aks-preview

# Update the extension to make sure you have the latest version installed
az extension update --name aks-preview
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Enable OIDC issuer on an existing cluster&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;az aks update -n aks -g myResourceGroup --enable-oidc-issuer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After we enable the &lt;strong&gt;OIDC issuer&lt;/strong&gt; feature we need to get the OIDC issuer url that will be used in the next step to federate the Azure AD Application, this can be done with the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;az aks show --resource-group &amp;lt;resource_group&amp;gt; --name &amp;lt;cluster_name&amp;gt; --query "oidcIssuerProfile.issuerUrl" -otsv
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Deploy the Azure AD Workload Identity helm chart to the cluster
&lt;/h2&gt;

&lt;p&gt;We can deploy a helm chart in several ways, in this case I decided to deploy the chart using terraform. &lt;br&gt;
You can achieve that with the following terraform code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"kubernetes_namespace"&lt;/span&gt; &lt;span class="s2"&gt;"azure-workload-identity-system"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;metadata&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;annotations&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"azure-workload-identity-system"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;   &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"azure-workload-identity-system"&lt;/span&gt;
    &lt;span class="nx"&gt;labels&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tags&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"helm_release"&lt;/span&gt; &lt;span class="s2"&gt;"azure-workload-identity-system"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"workload-identity-webhook"&lt;/span&gt;
  &lt;span class="nx"&gt;namespace&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"azure-workload-identity-system"&lt;/span&gt;
  &lt;span class="nx"&gt;chart&lt;/span&gt;      &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"workload-identity-webhook"&lt;/span&gt;
  &lt;span class="nx"&gt;repository&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"https://azure.github.io/azure-workload-identity/charts"&lt;/span&gt;
  &lt;span class="nx"&gt;wait&lt;/span&gt;       &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="nx"&gt;depends_on&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;kubernetes_namespace&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;azure&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;workload&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;identity&lt;/span&gt;&lt;span class="err"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;system&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="nx"&gt;set&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;name&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"azureTenantID"&lt;/span&gt;
    &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;azureTenantID&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we create a new kubernetes namespace and we deploy the helm release. I choose to deploy with terraform the helm charts that I depend on (i.e. my application dependencies, for example Azure AD Workload Identity and kong that I use as my ingress).&lt;br&gt;
We need to set the &lt;strong&gt;azureTenantID&lt;/strong&gt; value when we deploy the helm chart with the current azure tenant id.&lt;br&gt;
I read the current tenant id in the root module with the &lt;code&gt;data "azurerm_subscription" "current" {}&lt;/code&gt; data source and pass in as a variable in the child module.&lt;/p&gt;
&lt;h2&gt;
  
  
  3. Create a Federated Azure AD Application + a Service Principal
&lt;/h2&gt;

&lt;p&gt;Here we need to create an Azure AD Application + a Service Principal and federate the application with the OIDC Issuer so that Azure AD can exchange a token issued to the pod with a token that can be used to access other Azure resources.&lt;/p&gt;

&lt;p&gt;We can achieve it with a bit of terraform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;locals&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;namespace_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app-ns"&lt;/span&gt;
  &lt;span class="c1"&gt;## This should match the name of the service account created by helm chart&lt;/span&gt;
  &lt;span class="nx"&gt;service_account_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"app-${local.namespace_name}"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;## Azure AD application that represents the app&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azuread_application"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;display_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"sp-app-${var.env}"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azuread_service_principal"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;application_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azuread_application&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;application_id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azuread_service_principal_password"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;service_principal_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azuread_service_principal&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;## Azure AD federated identity used to federate kubernetes with Azure AD&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azuread_application_federated_identity_credential"&lt;/span&gt; &lt;span class="s2"&gt;"app"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;application_object_id&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azuread_application&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object_id&lt;/span&gt;
  &lt;span class="nx"&gt;display_name&lt;/span&gt;          &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"fed-identity-app-${var.env}"&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt;           &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"The federated identity used to federate K8s with Azure AD with the app service running in k8s ${var.env}"&lt;/span&gt;
  &lt;span class="nx"&gt;audiences&lt;/span&gt;             &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"api://AzureADTokenExchange"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="nx"&gt;issuer&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;oidc_k8s_issuer_url&lt;/span&gt;
  &lt;span class="nx"&gt;subject&lt;/span&gt;               &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"system:serviceaccount:${local.namespace_name}:${local.service_account_name}"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;output&lt;/span&gt; &lt;span class="s2"&gt;"app_client_id"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azuread_application&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;application_id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here we need to specify a couple of things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The OIDC Issuer url that we got from step 1, (I'm using a variable here to hold it's value)&lt;/li&gt;
&lt;li&gt;The subject that should follow a specific format: &lt;em&gt;system:serviceaccount:{k8s_namespace}:{k8s_service_account_name}&lt;/em&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;The namespace should match the namespace you will use to install your app in kubernetes and the Service Account name should match what you define in the kubernetes manifest.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  4. Create a kubernetes service account manifest with some specific metadata
&lt;/h2&gt;

&lt;p&gt;Here we will create the service account manifest and add the required metadata to allow the azwi do it's magic.&lt;/p&gt;

&lt;p&gt;Here's the code to create a service account and the corresponding value file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# serviceaccount.yaml&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ServiceAccount&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;include "app.serviceAccountName" .&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- include "app.labels" . | nindent 4&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
    &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- with .Values.serviceAccount.labels&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
    &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- toYaml . | nindent 4&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
    &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- end&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- with .Values.serviceAccount.annotations&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- toYaml . | nindent 4&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- end&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;

&lt;span class="c1"&gt;# value.yaml&lt;/span&gt;
&lt;span class="na"&gt;serviceAccount&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# Labels to add to the service account&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;azure.workload.identity/use&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true"&lt;/span&gt; &lt;span class="c1"&gt;# Represents the service account is to be used for workload identity, see https://azure.github.io/azure-workload-identity/docs/topics/service-account-labels-and-annotations.html&lt;/span&gt;
  &lt;span class="c1"&gt;# Annotations to add to the service account&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;azure.workload.identity/client-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{Client&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Id&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;the&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;azure&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ad&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;application}"&lt;/span&gt;
    &lt;span class="na"&gt;azure.workload.identity/tenant-id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{Tenant&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Id&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;of&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;you&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Azure&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;subscription}"&lt;/span&gt;
    &lt;span class="na"&gt;azure.workload.identity/service-account-token-expiration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;86400"&lt;/span&gt; &lt;span class="c1"&gt;# Token is valid for 1 day&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Please note that you can get the &lt;strong&gt;client id&lt;/strong&gt; from the output of the step 3 and that the name of the service account should match what you set in the subject of the &lt;strong&gt;azuread_application_federated_identity_credential&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  5. Configure our pods to run with the service account
&lt;/h2&gt;

&lt;p&gt;We need to make sure our pods run with the service account created in step 4. &lt;br&gt;
In order to do that we just need to specify the &lt;strong&gt;serviceAccountName&lt;/strong&gt; with the name of the Service Account in our &lt;em&gt;deployment.yaml&lt;/em&gt; file as shown below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;apps/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deployment&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;include "app.fullname" .&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- include "app.labels" . | nindent 4&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- if not .Values.autoscaling.enabled&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="na"&gt;replicas&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;.Values.replicaCount&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- end&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- include "app.selectorLabels" . | nindent 6&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
  &lt;span class="na"&gt;template&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- with .Values.podAnnotations&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
      &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- toYaml . | nindent 8&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
      &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- end&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
      &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- include "app.selectorLabels" . | nindent 8&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
    &lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- with .Values.imagePullSecrets&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
      &lt;span class="na"&gt;imagePullSecrets&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- toYaml . | nindent 8&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
      &lt;span class="pi"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;- end&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
      &lt;span class="na"&gt;serviceAccountName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{{&lt;/span&gt; &lt;span class="nv"&gt;include "app.serviceAccountName" .&lt;/span&gt; &lt;span class="pi"&gt;}}&lt;/span&gt;
&lt;span class="s"&gt;....&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is all it takes, after we're done here, we can grant our Service Principal some rights to, for example, allow it to access a storage account in the following way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="c1"&gt;## Lookup our storage account&lt;/span&gt;
&lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_storage_account"&lt;/span&gt; &lt;span class="s2"&gt;"storage"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage_account_name&lt;/span&gt;
  &lt;span class="nx"&gt;resource_group_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage_account_rg&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;## Role assignment to the application&lt;/span&gt;
&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"azurerm_role_assignment"&lt;/span&gt; &lt;span class="s2"&gt;"app_storage_contributor"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;scope&lt;/span&gt;                &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;azurerm_storage_account&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
  &lt;span class="nx"&gt;role_definition_name&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Storage Blob Data Contributor"&lt;/span&gt;
  &lt;span class="nx"&gt;principal_id&lt;/span&gt;         &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azuread_service_principal&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is my &lt;code&gt;required_providers&lt;/code&gt; configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;terraform&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;required_providers&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;azurerm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/azurerm"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 2.84"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;azuread&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;source&lt;/span&gt;  &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"hashicorp/azuread"&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"~&amp;gt; 2.14.0"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="nx"&gt;helm&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"2.4.1"&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's all for now, I hope you find this interesting, if you have any questions/suggestions, don't hesitate to comment!&lt;/p&gt;

</description>
      <category>azure</category>
      <category>terraform</category>
      <category>helm</category>
      <category>kubernetes</category>
    </item>
  </channel>
</rss>
