<?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>The Ops Community ⚙️: Alexandre Nédélec</title>
    <description>The latest articles on The Ops Community ⚙️ by Alexandre Nédélec (@techwatching).</description>
    <link>https://community.ops.io/techwatching</link>
    <image>
      <url>https://community.ops.io/images/jlsAj-f8tdW0K5i4qazTHIoHyQqNcETf6AdS2w0Y92c/rs:fill:90:90/g:sm/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL3Vz/ZXIvcHJvZmlsZV9p/bWFnZS8xNjI4L2Jh/ZWNhYmNhLTM2NTgt/NDM4Zi04NzdlLWIw/ZmUzNDIxNzZkOS5q/cGc</url>
      <title>The Ops Community ⚙️: Alexandre Nédélec</title>
      <link>https://community.ops.io/techwatching</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://community.ops.io/feed/techwatching"/>
    <language>en</language>
    <item>
      <title>Create an Azure-Ready GitHub Repository using Pulumi</title>
      <dc:creator>Alexandre Nédélec</dc:creator>
      <pubDate>Sun, 30 Jul 2023 00:00:00 +0000</pubDate>
      <link>https://community.ops.io/techwatching/create-an-azure-ready-github-repository-using-pulumi-3nl3</link>
      <guid>https://community.ops.io/techwatching/create-an-azure-ready-github-repository-using-pulumi-3nl3</guid>
      <description>&lt;p&gt;Creating an application and deploying it to Azure is not complicated. You write some code on your machine, do some clicks in the Azure portal, or run some Azure CLI commands from your terminal and that's it: your application is up and running in Azure.&lt;/p&gt;

&lt;p&gt;Yet, that's not real life, at least not what you will do when working on a professional project. Your code needs to be versioned and pushed to a location where your colleagues can work on it. The provisioning of Azure resources and deployment to Azure should be carried out using a properly configured CI/CD pipeline with the necessary authorization.&lt;/p&gt;

&lt;p&gt;That's a lot of work that would need to be done each time you start a new project. So let's automate that using Pulumi to simplify the process and create an "&lt;em&gt;Azure-Ready GitHub repository&lt;/em&gt;".&lt;/p&gt;

&lt;h2&gt;
  
  
  What's an Azure-Ready GitHub repository?
&lt;/h2&gt;

&lt;p&gt;"&lt;em&gt;Azure-Ready GitHub repository&lt;/em&gt;" is not an official term or concept, it's just something I've come up with to describe a Github repository that has everything correctly configured to provision Azure resources or deploy applications to Azure from a GitHub Actions CI/CD pipeline.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/wpeaZVFXJSLWJysHf1rvkqt40p_S1btr9Z3WQ-gJeS4/w:800/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9henVyZXJlYWR5/Z2l0aHViX292ZXJ2/aWV3XzEud2VicA" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/wpeaZVFXJSLWJysHf1rvkqt40p_S1btr9Z3WQ-gJeS4/w:800/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9henVyZXJlYWR5/Z2l0aHViX292ZXJ2/aWV3XzEud2VicA" alt="Diagram of a GitHub repository interacting with Azure." width="800" height="299"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The GitHub part
&lt;/h3&gt;

&lt;p&gt;On the GitHub side, to have an &lt;em&gt;Azure-Ready GitHub repository&lt;/em&gt;, we need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;the GitHub repository itself (already initialized with a &lt;code&gt;main&lt;/code&gt; branch)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the necessary GitHub Actions variables/secrets to authenticate to the correct Azure subscription&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;a YAML file located in the &lt;code&gt;.github/workflows/&lt;/code&gt; folder that contains the CI/CD pipeline that provisions resources in Azure&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/7B08hgG-ZRlxY7_7B2N9xRNpn9j0nGg3Zp6pNQO8Hkw/w:800/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9henVyZXJlYWR5/Z2l0aHViX2dpdGh1/Yl8xLndlYnA" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/7B08hgG-ZRlxY7_7B2N9xRNpn9j0nGg3Zp6pNQO8Hkw/w:800/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9henVyZXJlYWR5/Z2l0aHViX2dpdGh1/Yl8xLndlYnA" alt="A diagram of the GitHub repository to create." width="487" height="675"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The Azure part
&lt;/h3&gt;

&lt;p&gt;On the Azure side, to have an &lt;em&gt;Azure-Ready GitHub repository,&lt;/em&gt; we need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;the existing Azure subscription to which resources are deployed&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;an &lt;em&gt;identity&lt;/em&gt; in the Azure Active Directory of the desired tenant so that the GitHub CI/CD pipeline can authenticate to Azure and interact with the subscription&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/7fxUjYkrzdYzXNOSBcNqYWrigj-9y0ygYhBatZOoyxw/w:800/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9henVyZXJlYWR5/Z2l0aHViX2F6dXJl/XzEud2VicA" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/7fxUjYkrzdYzXNOSBcNqYWrigj-9y0ygYhBatZOoyxw/w:800/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9henVyZXJlYWR5/Z2l0aHViX2F6dXJl/XzEud2VicA" alt="A diagram of the resources to configure in Azure." width="568" height="652"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ️ &lt;em&gt;Azure Active Directory&lt;/em&gt; has recently been renamed &lt;em&gt;Microsoft Entra ID&lt;/em&gt; (as of the time of writing). However, I will continue to use the term Azure Active Directory throughout the rest of the article. Please note that both terms refer to the same service.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  The problem with secret credentials
&lt;/h3&gt;

&lt;p&gt;People tend to use secret credentials to authenticate their pipeline to Azure and that's not the best thing to do.&lt;/p&gt;

&lt;p&gt;From a security standpoint, depending on secrets always poses a security risk. Even if in that case the secret would be safely stored in a GitHub secret and never exposed publicly, it's still better to avoid secrets when we can.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🔐 That's precisely why when hosting applications in Azure, we use Managed Identities and IAM roles instead of relying on secrets. Yet, here we can't use Managed Identities for GitHub Actions pipelines.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;From a practical standpoint, depending on secrets can quickly become problematic as they expire and thus require rotation. Of course, you can set up alerting or automate secret rotation but that's something you would prefer to avoid managing.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💬 I recently encountered a situation in Azure DevOps where a deployment failed due to the expiration of an Azure AD Application secret associated with the Service Connection used in the pipeline, and we were not alerted about it. That's the kind of scenario that can easily happen with secrets and that you want to avoid.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So what can we do about that?&lt;/p&gt;

&lt;p&gt;👉 We can stop using secret credentials and use &lt;a href="https://learn.microsoft.com/en-us/azure/active-directory/workload-identities/workload-identity-federation"&gt;Workload identity federation&lt;/a&gt; instead. I suggest you have a look at this &lt;a href="https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect"&gt;GitHub documentation page&lt;/a&gt; as well to better understand how it works but basically, you can remember the following:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;this mechanism relies on Open ID Connect and trust between Azure and GitHub&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the GitHub pipeline does not need an Azure AD application secret anymore to authenticate to Azure&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;it's not an Azure thing only, it's an open standard that also works with other cloud providers and other platforms than Github&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;To establish the trust relationship between the Azure AD application and the GitHub repository, a &lt;em&gt;Federated Identity Credential&lt;/em&gt; must be created in the Azure Active Directory. You can find how to do that manually from the portal in the &lt;a href="https://learn.microsoft.com/en-us/azure/active-directory/workload-identities/workload-identity-federation-create-trust?pivots=identity-wif-apps-methods-azp"&gt;documentation&lt;/a&gt; but we are going to directly automate that 😉.&lt;/p&gt;

&lt;h3&gt;
  
  
  The complete solution to implement
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/-8lzEPl7NkfNDd0uzwP31aTnbp-UWQ1MqFAmKMfuToU/w:800/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9henVyZXJlYWR5/Z2l0aHViX292ZXJ2/aWV3XzIud2VicA" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/-8lzEPl7NkfNDd0uzwP31aTnbp-UWQ1MqFAmKMfuToU/w:800/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9henVyZXJlYWR5/Z2l0aHViX292ZXJ2/aWV3XzIud2VicA" alt="A diagram showing the interactions between Azure and GitHub." width="800" height="456"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why use Pulumi in that context?
&lt;/h2&gt;

&lt;p&gt;You might wonder why I chose to automate this process using Pulumi instead of writing a Bash or PowerShell script that would execute commands from the GitHub CLI and the Azure CLI.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡By the way, you should check &lt;a href="https://cli.github.com/"&gt;GitHub CLI&lt;/a&gt; if you have not done it yet, it's very handy. And if you have read my article about &lt;a href="https://www.techwatching.dev/posts/welcome-azure-cli"&gt;Azure CLI&lt;/a&gt;, you know it's a very convenient tool as well.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I think Pulumi is a better choice here because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;a script is imperative by nature, but declarative infrastructure seems more suitable to avoid dealing with idempotency&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Pulumi can interact with both GitHub and Azure using its providers&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the code will be easier to write and maintain&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the code could be integrated into any application (including a future self-service infrastructure portal) using Pulumi Automation API&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this article, the Pulumi code will be in TypeScript but it would work in any language supported by Pulumi.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automate the creation of the Azure-Ready GitHub Repository
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Create the Pulumi project
&lt;/h3&gt;

&lt;p&gt;Let's start by scaffolding a new Pulumi project using TypeScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;pulumi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;typescript&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AzureOIDC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dev&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A program to set up an Azure-Ready GitHub repository"&lt;/span&gt;&lt;span class="w"&gt;

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

&lt;/div&gt;



&lt;p&gt;This command creates a new pulumi project and stack from the TypeScript template:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The name of the project "&lt;em&gt;AzureOIDC"&lt;/em&gt; is specified using the &lt;code&gt;-n&lt;/code&gt; option&lt;/li&gt;
&lt;li&gt;The description of the project "&lt;em&gt;A program to set up an Azure-Ready GitHub repository&lt;/em&gt;" is specified using the &lt;code&gt;-d&lt;/code&gt; option&lt;/li&gt;
&lt;li&gt;The stack of the project "&lt;em&gt;dev&lt;/em&gt;" is specified using the &lt;code&gt;-s&lt;/code&gt; option&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ By default, the &lt;code&gt;pulumi new&lt;/code&gt; command installs the dependencies when creating the project. You can prevent this by specifying the &lt;code&gt;-g&lt;/code&gt; option, which is useful when you want to use another package manager than the default one (&lt;code&gt;pnpm&lt;/code&gt; instead of &lt;code&gt;npm&lt;/code&gt; for instance).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This project will need 3 different providers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;a href="https://www.pulumi.com/registry/packages/azure-native/"&gt;Azure Native provider&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;the &lt;a href="https://www.pulumi.com/registry/packages/azuread/"&gt;Azure Active Directory provider&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;the &lt;a href="https://www.pulumi.com/registry/packages/github/"&gt;GitHub provider&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So we can add the following packages to our &lt;code&gt;package.json&lt;/code&gt; file:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;@pulumi/azure-native&lt;/li&gt;
&lt;li&gt;@pulumi/azuread&lt;/li&gt;
&lt;li&gt;@pulumi/github&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Create the repository on GitHub
&lt;/h3&gt;

&lt;p&gt;To use the GitHub provider, we have to provide GitHub credentials. For that, we can create a personal access token (I prefer to create a &lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#fine-grained-personal-access-tokens"&gt;fine-grained personal access token&lt;/a&gt; although a &lt;a href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens#creating-a-personal-access-token-classic"&gt;classic personal access token&lt;/a&gt; would also work). Next, we simply set the GitHub token in our Pulumi configuration, and the GitHub provider will automatically use it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;pulumi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;set&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;github:token&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;XXXXXXXXXXXXXX&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--secret&lt;/span&gt;&lt;span class="w"&gt;

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

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;🔐 Don't forget to include the &lt;code&gt;--secret&lt;/code&gt; option when setting sensitive configurations, as this ensures that Pulumi encrypts the information. By doing so, we can safely commit the configuration files without creating security risks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now, it's time to create our GitHub repository!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;github&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/github&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;repository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;github&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;azure-ready-repository&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;azure-ready-repository&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;visibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;autoInit&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;repositoryCloneUrl&lt;/span&gt; &lt;span class="o"&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;httpCloneUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Pulumi has an &lt;a href="https://www.pulumi.com/docs/concepts/resources/names/#autonaming"&gt;auto-naming capability&lt;/a&gt; that is very convenient to prevent name collisions or to ensure zero-downtime resource updates. Yet, in this context, I prefer to avoid a random suffix in my GitHub repository name, that's why I am specifying the &lt;code&gt;name&lt;/code&gt; property to override the auto-naming behavior.&lt;/p&gt;

&lt;p&gt;The last line creates a stack &lt;a href="https://www.pulumi.com/docs/concepts/stack/#outputs"&gt;output&lt;/a&gt; named &lt;code&gt;repositoryCloneUrl&lt;/code&gt; so that we can easily get the URL to clone our newly created repository.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ I wanted the repository to be initialized, that's why I set the &lt;code&gt;autoInit&lt;/code&gt; property to &lt;code&gt;true&lt;/code&gt; but you should set it to &lt;code&gt;false&lt;/code&gt; if you have an existing local git repository that you want to push on this GitHub repository.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Create the &lt;em&gt;identity&lt;/em&gt; in Azure Active Directory for the GitHub Actions workflow
&lt;/h3&gt;

&lt;p&gt;Creating an Azure AD application and its service principal is not very complicated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;azuread&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/azuread&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;aadApplication&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;azuread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AzureReadyApp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Azure Ready App&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;servicePrincipal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;azuread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ServicePrincipal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AzureReadServicePrincipal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;applicationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;aadApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;applicationId&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 OIDC trust thing is a bit more complex. Fortunately, Microsoft's documentation has a detailed page &lt;a href="https://learn.microsoft.com/en-us/azure/active-directory/workload-identities/workload-identity-federation-create-trust?pivots=identity-wif-apps-methods-azp"&gt;&lt;em&gt;Configuring an app to trust an external identity provider&lt;/em&gt;&lt;/a&gt; that explains everything and shows how to add a federated identity for GitHub Actions using the Azure Portal, Azure CLI, or Azure PowerShell.&lt;/p&gt;

&lt;p&gt;Let's do the same thing using TypeScript and Pulumi Azure AD provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;azuread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ApplicationFederatedIdentityCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AzureReadyAppFederatedIdentityCredential&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;applicationObjectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;aadApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AzureReadyDeploys&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Deployments for azure-ready-repository&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;audiences&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;api://AzureADTokenExchange&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://token.actions.githubusercontent.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interpolate&lt;/span&gt;&lt;span class="s2"&gt;`repo:&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;fullName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:ref:refs/heads/main`&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 &lt;code&gt;subject&lt;/code&gt; property is what identifies the repository where the GitHub Actions workflow will be authorized to exchange its GitHub token for an Azure access token. It's worth noting that it will only work if the GitHub Actions workflow is run on the git reference (branch or tag) or the environment you specify in &lt;code&gt;subject&lt;/code&gt;. You can also specify that only workflows triggered by a pull request should be authorized. Here, I have used the &lt;code&gt;main&lt;/code&gt; branch but I could create multiple Federated Identity Credentials with different subjects if needed.&lt;/p&gt;

&lt;p&gt;With this configuration, the GitHub Actions workflow we create next will be able to obtain a valid Azure access token.&lt;/p&gt;

&lt;p&gt;If you are interested in gaining a better understanding of how all this works, you can refer to &lt;a href="https://learn.microsoft.com/en-us/azure/active-directory/workload-identities/workload-identity-federation#how-it-works"&gt;this diagram&lt;/a&gt; from Microsoft's documentation (with GitHub serving as the external identity provider in our case).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/wpeaZVFXJSLWJysHf1rvkqt40p_S1btr9Z3WQ-gJeS4/w:800/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9henVyZXJlYWR5/Z2l0aHViX292ZXJ2/aWV3XzEud2VicA" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/wpeaZVFXJSLWJysHf1rvkqt40p_S1btr9Z3WQ-gJeS4/w:800/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9henVyZXJlYWR5/Z2l0aHViX292ZXJ2/aWV3XzEud2VicA" alt="Sequence diagram explaining Azure OIDC." width="800" height="299"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Authorize the Service Principal to provision resources on the subscription
&lt;/h3&gt;

&lt;p&gt;We have created everything we need to get a valid Azure access token, but we still have not authorized the application to provision resources on our subscription.&lt;/p&gt;

&lt;p&gt;We can do that by giving the Contributor role to our service principal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;authorization&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/azure-native/authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;azureBuiltInRoles&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./builtInRoles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RoleAssignment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;contributor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;principalId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;servicePrincipal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;principalType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PrincipalType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ServicePrincipal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;roleDefinitionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;azureBuiltInRoles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contributor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interpolate&lt;/span&gt;&lt;span class="s2"&gt;`/subscriptions/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;subscriptionId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;I intentionally did not declare the variable &lt;code&gt;subscriptionId&lt;/code&gt; in the code above. It's because it's up to you to choose how you will provide it. You may want to set it in the configuration and retrieve it from it :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscriptionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;subscriptionId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Or your might want to retrieve it from the current configuration of the Azure native provider :&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;azureConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pulumi&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;authorization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getClientConfig&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscriptionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azureConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptionId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;Concerning, the contributor role definition identifier, I could have dynamically retrieved it using Azure APIs (like &lt;a href="https://github.com/pulumi/examples/blob/master/azure-ts-call-azure-sdk/index.ts"&gt;here&lt;/a&gt;). But honestly, as these identifiers don't change it's much easier to hardcode it in a dedicated &lt;code&gt;builtInRoles.ts&lt;/code&gt; file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;azureBuiltInRoles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;contributor&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c&lt;/span&gt;&lt;span class="dl"&gt;"&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 you don't have to work on the subscription scope. If you prefer to assign the contributor role (or any other role) to an existing resource group rather than the entire subscription, you can certainly do that as well.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Add the configuration for the GitHub Actions workflow
&lt;/h3&gt;

&lt;p&gt;The next step is to correctly set the configuration for the GitHub Actions of our Azure-Ready GitHub repository.&lt;/p&gt;

&lt;p&gt;The workflow requires three pieces of information for the OIDC authentication to function properly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The identifier of the Azure tenant&lt;/li&gt;
&lt;li&gt;The identifier of the Azure subscription&lt;/li&gt;
&lt;li&gt;The application identifier (also known as client ID) of the previously created Azure AD application&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These identifiers are not secrets, they are just identifiers so we could directly set them as GitHub Actions variables like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ActionsVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tenantId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;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="na"&gt;variableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ARM_TENANT_ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;azureConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&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;However, I like to keep my tenant id and my subscription id private so we will store them in GitHub secrets but that's not mandatory at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;azureConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pulumi&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;authorization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getClientConfig&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ActionsSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tenantId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;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="na"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ARM_TENANT_ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;plaintextValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;azureConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&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="nx"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ActionsSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;subscriptionId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;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="na"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ARM_SUBSCRIPTION_ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;plaintextValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;azureConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptionId&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="nx"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ActionsSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clientId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;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="na"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ARM_CLIENT_ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;plaintextValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;aadApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;applicationId&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;blockquote&gt;
&lt;p&gt;ℹ Please note that could also use &lt;a href="https://docs.github.com/en/actions/deployment/targeting-different-environments/using-environments-for-deployment"&gt;environments for deployment&lt;/a&gt; and their associated secrets and variables.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Create the GitHub Actions workflow
&lt;/h3&gt;

&lt;p&gt;Everything seems to be properly configured to provision Azure resources from a GitHub Actions workflow in this new repository, except for the workflow itself. The goal here is to have a properly configured pipeline in the repository to get started provisioning Azure infrastructure.&lt;/p&gt;

&lt;p&gt;Here is such a pipeline:&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;infra&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&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;provision-infra&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;p&gt;This workflow first authenticates to Azure using OIDC with the &lt;code&gt;azure/login&lt;/code&gt; action and then performs some Azure CLI commands to interact with Azure resources. That's fine and probably enough to get you started but you surely want to provision your infrastructure using a more declarative solution than an Azure CLI script. So let's see a more interesting pipeline still authenticating via Azure OIDC but using Pulumi to provision the Azure resources.&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;infra&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&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="c1"&gt;# required for OIDC auth&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="c1"&gt;# required to perform a checkout&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;provision-infra&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;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v3&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;Install pnpm&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;pnpm/action-setup@v2&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;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;latest&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;Set node version to &lt;/span&gt;&lt;span class="m"&gt;18&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-node@v3&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;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;18&lt;/span&gt;
          &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;pnpm'&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;Install dependencies&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;pnpm install&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;Provision infrastructure&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;pulumi/actions@v4.4.0&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;pulumi&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;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;up&lt;/span&gt;
          &lt;span class="na"&gt;stack-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dev&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;ARM_USE_OIDC&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;PULUMI_ACCESS_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PULUMI_ACCESS_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;ARM_CLIENT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ARM_CLIENT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;ARM_TENANT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ARM_TENANT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;ARM_SUBSCRIPTION_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ARM_SUBSCRIPTION_ID }}&lt;/span&gt; 

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

&lt;/div&gt;



&lt;p&gt;A permission section is required with 2 settings (more details &lt;a href="https://docs.github.com/en/actions/deployment/security-hardening-your-deployments/about-security-hardening-with-openid-connect#adding-permissions-settings"&gt;here&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;id-token: write&lt;/code&gt; ➡️ needed to request the OIDC token&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;contents: read&lt;/code&gt; ➡️ needed to perform checkout action&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;ℹ When you start to specify specific permissions, you have to specify all the permissions you need for the job because the default permissions won't apply anymore.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The 3 steps following the checkout step are actions to specify the Node.js version to use, install and correctly configure &lt;a href="https://bordeauxcoders.com/series/pnpm-101"&gt;pnpm&lt;/a&gt;. We assume here the infrastructure will be provisioned using TypeScript (and Pulumi of course) but there would have been similar steps with other runtimes/languages (a &lt;code&gt;setup-dotnet&lt;/code&gt; and a &lt;code&gt;dotnet retore&lt;/code&gt; action for .NET for instance).&lt;/p&gt;

&lt;p&gt;The last action is the Pulumi action to provision the infrastructure by running the &lt;code&gt;pulumi up&lt;/code&gt; on the &lt;code&gt;dev&lt;/code&gt; stack. We can see that this action uses environment variables whose values are based on the GitHub Actions secrets we defined earlier. To tell Pulumi to use OIDC, we just have to set the &lt;code&gt;ARM_USE_OIDC&lt;/code&gt; environment variable to &lt;code&gt;true&lt;/code&gt;.&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;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;ARM_USE_OIDC&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="no"&gt;true&lt;/span&gt;
          &lt;span class="na"&gt;PULUMI_ACCESS_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.PULUMI_ACCESS_TOKEN }}&lt;/span&gt;
          &lt;span class="na"&gt;ARM_CLIENT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ARM_CLIENT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;ARM_TENANT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ARM_TENANT_ID }}&lt;/span&gt;
          &lt;span class="na"&gt;ARM_SUBSCRIPTION_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.ARM_SUBSCRIPTION_ID }}&lt;/span&gt; 

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

&lt;/div&gt;



&lt;p&gt;A GitHub Actions secret we did not talk about is &lt;code&gt;PULUMI_ACCESS_TOKEN&lt;/code&gt; that is a &lt;a href="https://www.pulumi.com/docs/pulumi-cloud/access-management/access-tokens/"&gt;Pulumi access token&lt;/a&gt; to use Pulumi Cloud as our backend to store the infrastructure state and encrypt secrets. This token should be:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Created from Pulumi Cloud (following the documentation &lt;a href="https://www.pulumi.com/docs/pulumi-cloud/access-management/access-tokens/#personal-access-tokens"&gt;here&lt;/a&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stored in the stack configuration using the following command &lt;code&gt;pulumi config set pulumiTokenForRepository ******* --secret&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Stored in a GitHub Actions secret using this code&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The last thing to do is to add this workflow file to the GitHub repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pipelineContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;main.yml&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RepositoryFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pipelineRepositoryFile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;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="na"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;main&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.github/workflows/main.yml&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pipelineContent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;commitMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Add preconfigured pipeline file&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;commitAuthor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Alexandre Nédélec&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;commitEmail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;15186176+TechWatching@users.noreply.github.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;overwriteOnCreate&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="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;This code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;reads the &lt;code&gt;main.yml&lt;/code&gt; file that contains the workflow we saw previously&lt;/li&gt;
&lt;li&gt;creates a file with this content in the repository in the &lt;code&gt;.github/workflows/&lt;/code&gt; folder for the GitHub Actions workflows&lt;/li&gt;
&lt;li&gt;makes a commit when creating the file (or modifying it)&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;💬 To read the YAML file, I use the &lt;code&gt;readFileSync&lt;/code&gt; method from the File System API &lt;code&gt;fs&lt;/code&gt;. That's one of the things I love about Pulumi: you use the things you already know and that already exist in your ecosystem. No need to look for a module or wait for someone to write one, there is probably something standard or a popular community library you can use.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Test the Azure-Ready GitHub Repository
&lt;/h2&gt;

&lt;p&gt;Now that the infrastructure code to provision the Azure-Ready GitHub repository is written, let's run it with the &lt;code&gt;pulumi up&lt;/code&gt; command and see if it works!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/vjroPTghJsZRmhsUCkUpWDrEhvM10r0eOeECE9YTrII/w:800/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9henVyZXJlYWR5/Z2l0aHViX3B1bHVt/aV8xLndlYnA" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/vjroPTghJsZRmhsUCkUpWDrEhvM10r0eOeECE9YTrII/w:800/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9henVyZXJlYWR5/Z2l0aHViX3B1bHVt/aV8xLndlYnA" alt="Ouput of the pulumi up command with all the resources created." width="800" height="358"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All the resources are correctly created and our new GitHub repository is ready to be used.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/5ISb7HhU5s8LYY6Zikii6PIaseCJ4e6rmu6RQMxKWBI/w:800/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9henVyZXJlYWR5/Z2l0aHViX2dpdGh1/Yl8yLndlYnA" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/5ISb7HhU5s8LYY6Zikii6PIaseCJ4e6rmu6RQMxKWBI/w:800/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9henVyZXJlYWR5/Z2l0aHViX2dpdGh1/Yl8yLndlYnA" alt="Picture of the Azure Ready GitHub repository" width="800" height="395"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's clone it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;clone&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://github.com/TechWatching/azure-ready-repository&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;azure-ready-repository&lt;/span&gt;&lt;span class="w"&gt;

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

&lt;/div&gt;



&lt;p&gt;We want to verify that the GitHub project is properly configured and can provision Azure resources from its GitHub Actions workflow.&lt;/p&gt;

&lt;p&gt;Let's add some infrastructure code that provisions a few Azure resources to check that:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;pulumi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;azure-typescript&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"AzureReadyGitHuRepository"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-y&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--force&lt;/span&gt;&lt;span class="w"&gt;

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

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--force&lt;/code&gt; option allows us to create the code within a non-empty directory.&lt;/p&gt;

&lt;p&gt;I used the &lt;code&gt;azure-typescript&lt;/code&gt; template that creates a storage account and outputs retrieve its primary access key.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🔒 In the SDK, the outputs of the function that lists the storage access keys are not currently marked as secrets. There is currently an &lt;a href="https://github.com/pulumi/pulumi-azure-native/issues/2408"&gt;open issue&lt;/a&gt; to change that but in the meantime, I have just modified the code to label the stack output as secret ensuring its encryption.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let's run a &lt;code&gt;pnpm install&lt;/code&gt; to install the dependencies and generate the &lt;code&gt;pnpm-lock.yaml&lt;/code&gt; file. Then, we can push the code to GitHub and run the pipeline to see how it goes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/HwFf4mRWiuHbmdirP37x9NyqcMgn__Y1fqCV4VgH6oU/w:800/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9henVyZXJlYWR5/Z2l0aHViX2dpdGh1/Yl8zLndlYnA" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/HwFf4mRWiuHbmdirP37x9NyqcMgn__Y1fqCV4VgH6oU/w:800/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9henVyZXJlYWR5/Z2l0aHViX2dpdGh1/Yl8zLndlYnA" alt="Logs of the pipeline run showing that the workflow successfully created a storage account." width="800" height="745"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's it, we succeeded to provision a storage account from our new GitHub repository whose creation and configuration were entirely automated using Pulumi.&lt;/p&gt;

&lt;h2&gt;
  
  
  To conclude
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Additional information
&lt;/h3&gt;

&lt;p&gt;There are different platforms you can use to host your Git repositories: GitHub, GitLab, and Azure DevOps to name a few. We use GitHub in this article but you can easily apply the same logic with other platforms (Pulumi has providers for GitLab and Azure DevOps as well).&lt;/p&gt;

&lt;p&gt;Even though the Azure-Ready GitHub repository is provisioned using Pulumi, there's nothing stopping you from using another Infrastructure as Code solution that supports Azure OIDC (such as Azure CLI, which was mentioned in the article, Azure Bicep, or even Terraform) in the GitHub Actions workflow of the created repository. You don't even have to provision infrastructure; you can use this workflow to simply deploy an application to an existing Azure resource.&lt;/p&gt;

&lt;h3&gt;
  
  
  Potential Enhancements
&lt;/h3&gt;

&lt;p&gt;There are many aspects that could be improved in the infrastructure code provisioning the Azure-Ready GitHub repository, but I believe the current solution serves as a good starting point. Nevertheless, here are some ideas for potential enhancements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;make additional items, such as the commit author, configurable&lt;/li&gt;
&lt;li&gt;authorize an environment and not only a branch to retrieve an Azure token&lt;/li&gt;
&lt;li&gt;use environment variables/secrets instead of variable/secrets at the repository scope&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I think it would be interesting as well to put that code behind an API or a Web application using Pulumi Automation API to have a self-service solution to create Azure-Ready GitHub repository on the fly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Related articles
&lt;/h3&gt;

&lt;p&gt;Here are some articles on the same topic I wanted to mention:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://leebriggs.co.uk/blog/2022/01/23/gha-cloud-credentials"&gt;&lt;strong&gt;Stop using static cloud credentials in GitHub Actions&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;by Lee Briggs&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;➡️&lt;/strong&gt; This post provides examples for configuring OIDC authentication with GitHub Actions for AWS, Azure, and GCP. The code for Azure is quite similar to the code I showed here. Yet, it doesn't go so far as to initialize a pipeline ready to deploy resources with Pulumi. Anyway, it's awesome to have the code for all 3 major providers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://xaviergeerinck.com/2023/05/16/configuring-github-actions-to-azure-authentication-with-oidc/"&gt;&lt;strong&gt;Configuring GitHub Actions to Azure authentication with OIDC&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;by Xavier Geerinck&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;➡️&lt;/strong&gt; This post also shows how to configure OIDC authentication with GitHub Actions and Azure but using an Azure CLI script. Although the GitHub repository creation and configuration are done manually, automating the Azure part with a few lines of script is nice.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://samcogan.com/getting-rid-of-passwords-for-deployment-with-pulumi-oidc-support/"&gt;&lt;strong&gt;Getting Rid of Passwords for Deployment with Pulumi OIDC Support&lt;/strong&gt;&lt;/a&gt; &lt;strong&gt;by Sam Cogan&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
➡️ If you don't care about automating everything and simply want to configure OIDC authentication through the Azure portal, that's the post you will want to read. There is also an example of a pipeline to provision Azure infrastructure using a .NET Pulumi program.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Complete code solution
&lt;/h3&gt;

&lt;p&gt;In this article, I aimed to provide a step-by-step explanation of how to automate the creation of a GitHub repository with a properly configured workflow to interact with Azure using OpenID Connect. Consequently, the article turned out to be quite lengthy. I apologize for that, but I didn't want to present the code without adequate explanation.&lt;/p&gt;

&lt;p&gt;Anyway, now that we've covered everything, here is the complete code, which is just 75 lines long:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/pulumi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;github&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/github&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;azuread&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/azuread&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;authorization&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@pulumi/azure-native/authorization&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;azureBuiltInRoles&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./builtInRoles&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;readFileSync&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;repository&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;github&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;azure-ready-repository&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;azure-ready-repository&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;visibility&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;public&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;autoInit&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;repositoryCloneUrl&lt;/span&gt; &lt;span class="o"&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;httpCloneUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;aadApplication&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;azuread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AzureReadyApp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Azure Ready App&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;servicePrincipal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;azuread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ServicePrincipal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AzureReadyServicePrincipal&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;applicationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;aadApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;applicationId&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="nx"&gt;azuread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ApplicationFederatedIdentityCredential&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AzureReadyAppFederatedIdentityCredential&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;applicationObjectId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;aadApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;objectId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AzureReadyDeploys&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Deployments for azure-ready-repository&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;audiences&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;api://AzureADTokenExchange&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;issuer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://token.actions.githubusercontent.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interpolate&lt;/span&gt;&lt;span class="s2"&gt;`repo:&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;fullName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:ref:refs/heads/main`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;azureConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pulumi&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;authorization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getClientConfig&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subscriptionId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;azureConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptionId&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RoleAssignment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;contributor&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;principalId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;servicePrincipal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;principalType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;authorization&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PrincipalType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ServicePrincipal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;roleDefinitionId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;azureBuiltInRoles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;contributor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pulumi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;interpolate&lt;/span&gt;&lt;span class="s2"&gt;`/subscriptions/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;subscriptionId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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="nx"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ActionsSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tenantId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;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="na"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ARM_TENANT_ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;plaintextValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;azureConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tenantId&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="nx"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ActionsSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;subscriptionId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;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="na"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ARM_SUBSCRIPTION_ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;plaintextValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;azureConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subscriptionId&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="nx"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ActionsSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;clientId&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;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="na"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ARM_CLIENT_ID&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;plaintextValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;aadApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;applicationId&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="nx"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ActionsSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pulumiAccessToken&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;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="na"&gt;secretName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;PULUMI_ACCESS_TOKEN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;plaintextValue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;requireSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pulumiTokenForRepository&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pipelineContent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;main.yml&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;utf-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;github&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RepositoryFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pipelineRepositoryFile&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;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="na"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;main&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;file&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;.github/workflows/main.yml&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pipelineContent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;commitMessage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Add preconfigured pipeline file&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;commitAuthor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Alexandre Nédélec&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;commitEmail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;15186176+TechWatching@users.noreply.github.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;overwriteOnCreate&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="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;You can find the complete source code used for this article &lt;a href="https://github.com/TechWatching/AzureOIDC"&gt;in this GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I hope you enjoyed this article. Please feel free to share your thoughts in the comments, ask questions, or make suggestions. Keep learning.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>iac</category>
      <category>pulumi</category>
      <category>github</category>
    </item>
    <item>
      <title>4 tips about GitHub Actions environment variables and contexts</title>
      <dc:creator>Alexandre Nédélec</dc:creator>
      <pubDate>Thu, 02 Feb 2023 09:22:00 +0000</pubDate>
      <link>https://community.ops.io/techwatching/4-tips-about-github-actions-environment-variables-and-contexts-1l0</link>
      <guid>https://community.ops.io/techwatching/4-tips-about-github-actions-environment-variables-and-contexts-1l0</guid>
      <description>&lt;p&gt;I recently played a bit with GitHub Actions and as I have spent some time running again and again my workflows to understand what what going wrong I thought it could be interesting to share what I have learned especially concerning environment variables and contexts.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🗨 Disclaimer: Although I have some experience with Azure Pipelines, I am still learning GitHub Actions so I do not pretend to know everything about them nor do I never make mistakes when writing about them. Feel free to correct me in the comments if you think I am wrong about something or if something that I show can be done more effectively.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Tip n°1: Environment variables syntax depends on the shell you are using in your job
&lt;/h2&gt;

&lt;p&gt;As you know a GitHub Actions workflow is composed of different jobs where each job is a set of steps that execute on the same runner. As a runner can be hosted Ubuntu, Windows, MacOS or even another operating system (if you host your own runner) the shell that will execute your commands will not be the same by default depending on the runner you choose. For instance if you are on a Ubuntu GitHub-hosted runner, by default the shell will be bash whereas on a Windows GitHub-hosted runner it will be PowerShell (I think we don't say PowerShell Core anymore but I am speaking of &lt;code&gt;pwsh&lt;/code&gt; not the old Windows PowerShell of course).&lt;/p&gt;

&lt;p&gt;This is really important to know because depending on the shell used, the syntax to use an environment variable in a script is different as you can see on the documentation screenshot below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/ylcYRkMwKc7KyJURFWyoBTTFZYyPxf7NoTy-btwNn5o/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9naXRodWJhY3Rp/b25zX2VudnZhcl8x/LnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/ylcYRkMwKc7KyJURFWyoBTTFZYyPxf7NoTy-btwNn5o/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9naXRodWJhY3Rp/b25zX2VudnZhcl8x/LnBuZw" alt="" width="880" height="652"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🗨 When I am talking about environment variables here I am referring to GitHub &lt;a href="https://docs.github.com/en/actions/reference/environment-variables"&gt;default environment variables&lt;/a&gt; (for instance &lt;code&gt;GITHUB_EVENT_NAME&lt;/code&gt; which the name of the webhook event that triggered the workflow) and to custom variables you set in a workflow. The syntax for environment variables you just create and use in a shell script does not change of course.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The documentation briefly explains the syntax to use depending on the shell &lt;a href="https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#using-a-specific-shell"&gt;here&lt;/a&gt; but you can easily miss it like I did. In fact most of the GitHub Actions examples you can find are in bash so if you use them as-is without paying attention to the shell you are using, you will probably get it wrong. I lost a lot of time trying to figure out why my scripts were not working on a Windows runner so I hope knowing that you will avoid to do the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tip n°2: Do not use your repository &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; in tasks that need to trigger another workflow.
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; is a secret you can use in your workflow to do some actions on your GitHub repository like pushing a tag, creating a new release, creating an issue... It is very convenient because it allows you to automate in your workflow many things for your GitHub repository using built-in actions or the GitHub REST API.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🗨 Be aware that internally GitHub Actions is a GitHub App that is installed on your repository when you start creating GitHub Actions workflows for this repository. That means the token represented by &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; in your workflow is a GitHub App installation token that will only work on your repository workflow and that will have a limited set of permissions (but your can grant more permissions directly in your workflow if you want).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you want to know about authentication in a workflow, there is dedicated &lt;a href="https://docs.github.com/en/actions/reference/authentication-in-a-workflow"&gt;page&lt;/a&gt; on the topic in the GitHub documentation. However if you read it quickly you may miss an important piece of information:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"events triggered by the GITHUB_TOKEN will not create a new workflow run"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What does this mean ? It means that if you have 2 workflows and that the first one uses the &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; for an action that should trigger the second one, then the second workflow will never be triggered. The aim is to prevent you from making an infinite loop of workflows runs triggering each other.&lt;/p&gt;

&lt;p&gt;So imagine you want to implement a workflow that publish a release when a new tag is pushed on a repository, and another workflow that automatically tweet about your new release once it is published, how can you do that? You just have to &lt;a href="https://docs.github.com/en/github/authenticating-to-github/keeping-your-account-and-data-secure/creating-a-personal-access-token"&gt;create a GitHub Personal Access Token&lt;/a&gt;, add it as a secret in your repository and use it in your first workflow to create your release. This way, the second workflow will run fine when the release is published.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tip n°3: Assign information from the event triggering a GitHub Actions workflow to a PowerShell variable.
&lt;/h2&gt;

&lt;p&gt;Sometimes, in a GitHub Actions workflow we want to execute some PowerShell to do specific actions depending on information from the event that triggered the workflow. Hence having a PowerShell variable with this data could be really useful. For instance, if we have a workflow triggered by the publication of a release, maybe we need to retrieve the URLs of the binaries of this release which can be done easily if the event data is in a PowerShell variable.&lt;/p&gt;

&lt;p&gt;The webhook payload corresponding to the event that triggered a workflow is part of the properties of the &lt;code&gt;github&lt;/code&gt; context.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🗨 If you are not familiar with contexts in GitHub Actions, they are objects that contain information about the current workflow, job, runner or things like that. Contexts are often used in expressions to determine whether or not a step of the workflow should be executed or to set some variables as you can see in the sample below. &lt;a href="https://community.ops.io/images/8aTTVjv3yG7A7ip5uwbidHS5jnI50dZUmNXZdkr5uCE/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9naXRodWJhY3Rp/b25zX2NvbnRleHRf/MS5wbmc" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/8aTTVjv3yG7A7ip5uwbidHS5jnI50dZUmNXZdkr5uCE/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9naXRodWJhY3Rp/b25zX2NvbnRleHRf/MS5wbmc" alt="" width="880" height="397"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As you can read in the &lt;a href="https://docs.github.com/en/actions/reference/context-and-expression-syntax-for-github-actions"&gt;documentation&lt;/a&gt;, the event property is of type object which makes it difficult to query it and to assign it to a PowerShell variable. Fortunately, there are ways to do that which are based on the fact that GitHub event data is persisted in a json file on the runner file system.&lt;/p&gt;

&lt;p&gt;First one is to use the tool &lt;a href="https://stedolan.github.io/jq/"&gt;&lt;code&gt;jq&lt;/code&gt;&lt;/a&gt; which is already installed on GitHub's runners and that allows you to process json data. Edward Thomson has a blog post which explains how you can use jq with GitHub Actions to do that: &lt;a href="https://www.edwardthomson.com/blog/github_actions_12_information_about_your_workflow.html"&gt;&lt;code&gt;GitHub Actions Day 12: Information about your Workflow&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Second one is to directly use PowerShell to grab the GitHub context like this: &lt;code&gt;$github = Get-Content '${{ github.event_path }}' | ConvertFrom-Json&lt;/code&gt;. Thanks a lot Edward Thomson for helping me with this tip 😀.&lt;/p&gt;


&lt;blockquote class="ltag__twitter-tweet"&gt;

  &lt;div class="ltag__twitter-tweet__main"&gt;
    &lt;div class="ltag__twitter-tweet__header"&gt;
      &lt;img class="ltag__twitter-tweet__profile-image" src="https://community.ops.io/images/zw3XwUE0KZjJOh_jnYO9gRHXVHYG5cw21thcPGWDhD8/w:880/mb:500000/ar:1/aHR0cHM6Ly9wYnMu/dHdpbWcuY29tL3By/b2ZpbGVfaW1hZ2Vz/LzE1Mjc0NjMxOTkz/Njg1MzE5NzAvd0Mt/Uld5aVVfbm9ybWFs/LmpwZw" alt="Edward Thomson profile image"&gt;
      &lt;div class="ltag__twitter-tweet__full-name"&gt;
        Edward Thomson
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__username"&gt;
        @ethomson
      &lt;/div&gt;
      &lt;div class="ltag__twitter-tweet__twitter-logo"&gt;
        &lt;img src="/assets/twitter-f95605061196010f91e64806688390eb1a4dbc9e913682e043eb8b1e06ca484f.svg" alt="twitter logo"&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__body"&gt;
      &lt;a href="https://twitter.com/TechWatching"&gt;@TechWatching&lt;/a&gt; When we spin up a workflow, we'll put the github event data in a file on-disk (in json).  We store the filename in the github context itself, as `${{ github.event_path }}`.  So you can use `ConvertFrom-Json` on it.
    &lt;/div&gt;
    &lt;div class="ltag__twitter-tweet__date"&gt;
      15:39 PM - 20 Jul 2021
    &lt;/div&gt;


    &lt;div class="ltag__twitter-tweet__actions"&gt;
      &lt;a href="https://twitter.com/intent/tweet?in_reply_to=1417509550786322440" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="/assets/twitter-reply-action-238fe0a37991706a6880ed13941c3efd6b371e4aefe288fe8e0db85250708bc4.svg" alt="Twitter reply action"&gt;
      &lt;/a&gt;
      &lt;a href="https://twitter.com/intent/retweet?tweet_id=1417509550786322440" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="/assets/twitter-retweet-action-632c83532a4e7de573c5c08dbb090ee18b348b13e2793175fea914827bc42046.svg" alt="Twitter retweet action"&gt;
      &lt;/a&gt;
      &lt;a href="https://twitter.com/intent/like?tweet_id=1417509550786322440" class="ltag__twitter-tweet__actions__button"&gt;
        &lt;img src="/assets/twitter-like-action-1ea89f4b87c7d37465b0eb78d51fcb7fe6c03a089805d7ea014ba71365be5171.svg" alt="Twitter like action"&gt;
      &lt;/a&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/blockquote&gt;


&lt;p&gt;To illustrate that, here are a sample of using these two ways to assign the URL of the binary published by a release to a PowerShell variable using the event property of the &lt;code&gt;github&lt;/code&gt; context:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;using jq:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; $installerUrl = $(jq --raw-output '.release.assets[].browser_download_url | select(contains(\"windows.msi\"))' "${{ github.event_path }}")

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

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;using ConvertFrom-Json method in PowerShell:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$github = Get-Content '${{ github.event_path }}' | ConvertFrom-Json
$installerUrl = $github.release.assets | Where-Object -Property name -match 'windows.msi' | Select -ExpandProperty browser_download_url -First 1

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

&lt;/div&gt;



&lt;p&gt;I did not know about jq but I find it is a really nice tool although the syntax is not that straightforward. Yet, I prefer the PowerShell way because it allows to directly manipulate an object instead of a json string. It is what I did to automate the upgrade of the &lt;code&gt;Nushell&lt;/code&gt; winget package using GitHub Actions when a new release is published (you can read more about it in this &lt;a href="https://www.techwatching.dev/posts/wingetcreate"&gt;article&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Tip n°4: Enable debugging logs to help you understand what is going wrong in your workflow
&lt;/h2&gt;

&lt;p&gt;As many CI / CD platforms, GitHub Actions has the disadvantage of not being testable locally which makes it hard to debug a workflow when something is not working properly. Moreover, by default you won't see how some expressions / contexts are evaluated when the workflow run so logs will not help you figuring out what is wrong in your workflow definition.&lt;/p&gt;

&lt;p&gt;Hopefully, you can enable debugging logs for all pipelines in your repository (unfortunately you can't specify it for a specific pipeline only) by setting the secret &lt;code&gt;ACTIONS_STEP_DEBUG&lt;/code&gt; to true in your repository. You can read more about it in the &lt;a href="https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging"&gt;documentation&lt;/a&gt; but here is what it look likes in a workflow run:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/jZH41adUcXNMWURDrIlUx9D2tEDrdA9fW7XjLKXay4o/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9naXRodWJhY3Rp/b25zX2xvZ3NfMS5w/bmc" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/jZH41adUcXNMWURDrIlUx9D2tEDrdA9fW7XjLKXay4o/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9naXRodWJhY3Rp/b25zX2xvZ3NfMS5w/bmc" alt="" width="880" height="688"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;I hope these 4 tips will help you build awesome GitHub Actions workflows.&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>github</category>
      <category>powershell</category>
    </item>
    <item>
      <title>How to provision an Azure SQL Database with Active Directory authentication</title>
      <dc:creator>Alexandre Nédélec</dc:creator>
      <pubDate>Thu, 26 Jan 2023 09:10:00 +0000</pubDate>
      <link>https://community.ops.io/techwatching/how-to-provision-an-azure-sql-database-with-active-directory-authentication-17jl</link>
      <guid>https://community.ops.io/techwatching/how-to-provision-an-azure-sql-database-with-active-directory-authentication-17jl</guid>
      <description>&lt;p&gt;In this article, we will talk about how to provision an Azure SQL Database with authentication restricted to Active Directory users/groups/applications. We will use Pulumi to do that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this article?
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://www.techwatching.dev/posts/sqlclient-active-directory-authent"&gt;a previous article&lt;/a&gt;, I already talked about connecting to an Azure SQL Database using Azure Active Directory authentication. However, my focus was on querying an Azure SQL Database from C# code (from an ASP.NET 6 Minimal API that was using &lt;code&gt;Microsoft.Data.SqlClient&lt;/code&gt; 'Active Directory Default' authentication mode to be more precise), and not on the configuration of the Azure AD authentication itself.&lt;/p&gt;

&lt;p&gt;Still, in that article, I wrote an Azure CLI script that showed how to provision and configure the database with Azure AD authentication enabled. So why write another article about that? First because I did not show how to give an Azure AD entity (user, group, or managed identity) permission to access the database. (In my samples, to simplify things I was using the SQL server Azure AD administrator account to make my queries 🤫). Yet, it is something you will probably have to do if you want your App Service or Function App to query your database. Second because even if Azure CLI is great to handle Azure resources (if you are a reader of my blog, you probably know that I &lt;a href="https://www.techwatching.dev/posts/welcome-azure-cli"&gt;enjoy very much Azure CLI&lt;/a&gt;), in a real project I would probably use a more advanced Infrastructure as Code solution like Pulumi. And that is what we will show here.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🗨️ If you are not familiar with Pulumi, it is an IaC solution similar to Terraform but using programming languages like C#. Speaking of C#, that is what I will use to write my infrastructure code but you can easily do the same in another language supported by Pulumi (TypeScript, Go, Python,... choose the one you are used to), the concepts stay relevant and the code will be similar.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now, let's get to the heart of the matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  An Azure AD user as our SQL Server administrator
&lt;/h2&gt;

&lt;p&gt;Usually, when you create an Azure SQL Server, you have to provide an administrator login and an administrator password. But I said I wanted to limit the authentication to Azure Active Directory authentication only. So we will only need an Azure AD account to set as the administrator of our SQL Server. We could use an existing Azure AD account, but let's create a new Azure AD user just for that:&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Config&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;sqlAdAdminLogin&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sqlAdAdmin"&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;sqlAdAdminPassword&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RequireSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sqlAdPassword"&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;sqlAdAdmin&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;User&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sqlAdmin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;UserArgs&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;UserPrincipalName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlAdAdminLogin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlAdAdminPassword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;DisplayName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Global SQL Admin"&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;To create a new Azure AD user we need a login (it will be the email of the new user in our tenant) and a password. In this example, we retrieve these values from the &lt;a href="https://www.pulumi.com/docs/intro/concepts/config/"&gt;configuration&lt;/a&gt; which is stored in the YAML settings file. You can notice there that we retrieve a secret (the password) from the configuration thanks to the &lt;code&gt;config.RequireSecret&lt;/code&gt; method. Indeed to avoid exposing a secret in the configuration file or the state file, Pulumi has &lt;a href="https://www.pulumi.com/docs/intro/concepts/secrets/"&gt;built-in support for secret encryption and decryption&lt;/a&gt; (not sure Terraform folks can say the same thing 😉).&lt;/p&gt;

&lt;h2&gt;
  
  
  Create the Azure SQL Server and its database.
&lt;/h2&gt;

&lt;p&gt;Now that we have our administrator account, we can create the Azure SQL Server:&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sqlServer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"sql-sqlDbWithAzureAd-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Deployment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StackName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ServerArgs&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ResourceGroupName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resourceGroup&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;Administrators&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;ServerExternalAdministratorArgs&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Login&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlAdAdmin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserPrincipalName&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Sid&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlAdAdmin&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;AzureADOnlyAuthentication&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;AdministratorType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;AdministratorType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ActiveDirectory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;PrincipalType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PrincipalType&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;User&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;Nothing special here: we are using the variable &lt;code&gt;sqlAdmin&lt;/code&gt; that is our newly created user to set the administrator of the SQL Server and we set the authentication to Azure AD only. We can then create the database:&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;database&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Database&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"sqldb-sqlDbWithAzureAd-Main"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;DatabaseArgs&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ResourceGroupName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resourceGroup&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;ServerName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlServer&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;Sku&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;SkuArgs&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="s"&gt;"Basic"&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;h2&gt;
  
  
  Grant SQL Database access permissions to Azure AD entities
&lt;/h2&gt;

&lt;p&gt;Once we have provisioned the Azure SQL Server and its database, here comes the tough part: we need to configure who can access the database. In a project, you will probably have to give access to some users and to the Azure resources that need to query the database (you will have to assign these resources a managed identity before that). But to keep things simple, we will just consider we need to grant SQL Database access to an Azure AD group. That could be a good way to do things by the way: create an Azure AD group, grant permissions to this group and add users and managed identities that need access to the database.&lt;/p&gt;

&lt;p&gt;Why did I say that this part was tough? It's because to grant SQL database permissions, we need to execute an SQL command on the Server as you can read &lt;a href="https://docs.microsoft.com/en-us/azure/app-service/tutorial-connect-msi-sql-database?tabs=windowsclient%2Cef%2Cdotnet#grant-permissions-to-managed-identity"&gt;in the documentation&lt;/a&gt;.&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;identity&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&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;ALTER&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;db_datareader&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;identity&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;];&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_datawriter&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="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;identity&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;With this command, we are creating a user and giving &lt;code&gt;db_datareader&lt;/code&gt; and &lt;code&gt;db_datawriter&lt;/code&gt; roles. However it is not a classical user, it's a user that is "external" to the database: in our case, it corresponds to an Azure AD entity (a user, group, or application).&lt;/p&gt;

&lt;p&gt;So it's not just about setting a property to properly configure an Azure resource, it's a bit more complicated.&lt;/p&gt;

&lt;p&gt;I see multiple ways of doing that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a new Pulumi provider "SQL Server provider" that is to able manage users in an SQL Server database&lt;/li&gt;
&lt;li&gt;Write custom C# code that executes the SQL command once the database is created&lt;/li&gt;
&lt;li&gt;Use the Pulumi Command provider to execute the SQL command using the &lt;code&gt;sqlcmd&lt;/code&gt; utility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's review these solutions.&lt;/p&gt;

&lt;h3&gt;
  
  
  New "SQL Server Provider"
&lt;/h3&gt;

&lt;p&gt;To manage SQL Server resources like users and roles, we can create a complete provider. We could create it from scratch of course or use this &lt;a href="https://github.com/pulumi/pulumi-provider-boilerplate"&gt;Pulumi GitHub repository&lt;/a&gt; that provides some boilerplate code to create a Pulumi provider. Usually, Pulumi providers are written in Go (like the Terraform providers by the way) and generate SDKs for all programming languages supported by Pulumi.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/LgUXqNPQe4_kIYCH9AO103E2mUJATIqT0_85ysFIHm8/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9zcWxkYXRhYmFz/ZV9hZF9wcm92aWRl/cl8xLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/LgUXqNPQe4_kIYCH9AO103E2mUJATIqT0_85ysFIHm8/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9zcWxkYXRhYmFz/ZV9hZF9wcm92aWRl/cl8xLnBuZw" alt="" width="880" height="442"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another way would be to adapt the existing &lt;a href="https://registry.terraform.io/providers/betr-io/mssql/latest/docs"&gt;Microsoft SQL Server Provider&lt;/a&gt; for Terraform. This Terraform provider made by the community enables you to create and manage logins and users on a SQL Server. I talked about "adapting" this provider because you can create a Pulumi provider out of a Terraform provider by using the &lt;a href="https://github.com/pulumi/pulumi-terraform-bridge"&gt;Pulumi Terraform Bridge&lt;/a&gt;. That's great because instead of reinventing the wheel you can benefit from Terraform ecosystem by creating a Pulumi provider that wraps an existing Terraform provider. This &lt;a href="https://github.com/pulumi/pulumi-tf-provider-boilerplate"&gt;GitHub repository&lt;/a&gt; contains boilerplate code to do exactly that.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/hm3AwJdRD7EhHcLWJ7tVJZXyhh5fzu2H2f5H51WCttA/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9zcWxkYXRhYmFz/ZV9hZF9wcm92aWRl/cl8yLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/hm3AwJdRD7EhHcLWJ7tVJZXyhh5fzu2H2f5H51WCttA/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9zcWxkYXRhYmFz/ZV9hZF9wcm92aWRl/cl8yLnBuZw" alt="" width="880" height="291"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You might have noticed that I sometimes criticize Terraform in my articles. That's not because I think Terraform is a bad infrastructure as code solution, in fact, I think it is a great solution with a rich ecosystem. However, I am critical of Terraform because I believe Infrastructure as Code should be done with programming languages instead of Domain-Specific Languages. Moreover, there are some areas (API coverage of major cloud providers, security, IDE support, ...) where I found Terraform is not good enough, especially compared to other platforms like Pulumi. So I am always a bit disappointed when I see that many people choose by default Terraform as their infrastructure as code platform without considering alternatives (and I am not only talking about Pulumi, there are also Farmer and Bicep for instance), even when these alternatives would be better suited to their use cases. That being said, Terraform has also advantages like its great community that creates and contributes to many providers like the &lt;code&gt;mssql&lt;/code&gt; one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This first solution of creating a new "SQL Server Provider" (whether it be from scratch, from boilerplate code, or from the &lt;code&gt;mssql&lt;/code&gt; Terraform provider) is interesting but could be time-consuming because there are some things to set up and some amount of code to write.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom C# code
&lt;/h3&gt;

&lt;p&gt;When you need to do something specific and there is no existing provider that can help you with it, you can just write the code to do it yourself without creating a complete provider. It's one of the reasons why I like Pulumi, even if you are doing Infrastructure as Code, at the end of the day you are just developing software so you can code what you need in the language you are familiar with. For instance, as I am developing in .NET, I can use the &lt;a href="https://docs.microsoft.com/en-us/sql/connect/ado-net/overview-sqlclient-driver"&gt;&lt;code&gt;Microsoft.Data.SqlClient&lt;/code&gt; library&lt;/a&gt; (which is a data provider for Azure SQL Database) to connect and send commands to the database. And if I want to use &lt;a href="https://github.com/DapperLib/Dapper"&gt;Dapper&lt;/a&gt; on top of it because that's the library I am used to for querying a database I can. Hence writing the code that executes on the database the SQL command we have previously seen should not be very difficult.&lt;/p&gt;

&lt;p&gt;Now, even if we are using imperative language in Pulumi to write the infrastructure code it's still declarative infrastructure as code with a state. Therefore, we have to be careful about how and when this custom code should be executed.&lt;/p&gt;

&lt;p&gt;The easiest way is to use an &lt;code&gt;Apply&lt;/code&gt; method on an output of the database 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="n"&gt;database&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="nf"&gt;Apply&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;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="cm"&gt;/*** 
     * Indempotent code using Microsoft.Data.SqlClient library
     * to execute the SQL command that assigns the correct roles
     * to the Azure AD group we want to have access to the database.
    ***/&lt;/span&gt; 
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;true&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 code in the &lt;code&gt;Apply&lt;/code&gt; will execute on every run after the resource is created, that is why it needs to be idempotent. Having to make the code idempotent is a constraint that I would prefer to avoid but at least it gives us a simple way to execute the code that grants access to the database.&lt;/p&gt;

&lt;p&gt;Another way would be to use &lt;a href="https://www.pulumi.com/docs/intro/concepts/resources/dynamic-providers/"&gt;Dynamic Providers&lt;/a&gt; whose purpose is exactly that: do an infrastructure task that no existing provider can help you deliver. You can see some use cases of dynamic providers in &lt;a href="https://www.pulumi.com/blog/dynamic-providers/#sample-use-cases"&gt;this Pulumi article&lt;/a&gt;. In our use case, we could imagine writing a dynamic resource provider for an Azure AD entity user in an Azure SQL Database. We would have to implement the different CRUD operations to handle the different use cases properly (a user is added, a user is removed, user roles are updated, ...). Unfortunately, as you can see in &lt;a href="https://github.com/pulumi/pulumi/issues/3638"&gt;this GitHub issue&lt;/a&gt;, .NET Dynamic Providers are not yet supported (only TypesScript, JavaScript and Python are for the moment). It's a shame because Dynamic Providers provide an easy and efficient way of supporting custom resource types.&lt;/p&gt;

&lt;h3&gt;
  
  
  Command provider with the &lt;code&gt;sqlcmd&lt;/code&gt; utility
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://docs.microsoft.com/en-us/azure/app-service/tutorial-connect-msi-sql-database?tabs=windowsclient%2Cef%2Cdotnet#grant-permissions-to-managed-identity"&gt;The Microsoft tutorial&lt;/a&gt;, that shows how to grant database permissions to an Azure AD entity, explains how the necessary SQL commands can be run using the &lt;a href="https://docs.microsoft.com/en-us/sql/tools/sqlcmd-utility"&gt;&lt;code&gt;sqlcmd&lt;/code&gt; utility&lt;/a&gt;. So instead of writing some C# code to do the same, an interesting idea would be to directly run the &lt;code&gt;sqlcmd&lt;/code&gt; utility. And you know what? There is a Pulumi provider for executing commands and scripts: &lt;a href="https://www.pulumi.com/registry/packages/command/api-docs/"&gt;the Command Provider&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/cAnkf6eGj2cyN_PiAntrSvKgwvlkxiaCtdwB-xTCfvo/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9zcWxkYXRhYmFz/ZV9hZF9wdWx1bWlf/MS5wbmc" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/cAnkf6eGj2cyN_PiAntrSvKgwvlkxiaCtdwB-xTCfvo/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9zcWxkYXRhYmFz/ZV9hZF9wdWx1bWlf/MS5wbmc" alt="" width="880" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Because it's a Pulumi provider, the &lt;code&gt;sqlcmd&lt;/code&gt; command would be executed "as part of the Pulumi resource model" which means the scripts would be executed at the corresponding time of the resource life-cycle (the &lt;code&gt;create&lt;/code&gt; script when the resource is created and so on). So it's very nice and not the same as executing the &lt;code&gt;sqlcmd&lt;/code&gt; outside of a Pulumi program, without access to all the variables and where you would have to make your script idempotent. Moreover, the ability to execute commands remotely can bring interesting use cases, just not for our current concern here.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Pulumi Command Provider is currently in preview and only supports running scripts on &lt;code&gt;create&lt;/code&gt; and &lt;code&gt;destroy&lt;/code&gt; operations (support for &lt;code&gt;diff&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt; and &lt;code&gt;read&lt;/code&gt; operations &lt;a href="https://github.com/pulumi/pulumi-command/issues/20"&gt;will probably be added later&lt;/a&gt;). It works fine but does not log details about the error when a script fails, which makes debugging difficult. That should not prevent you from using it but as with any components in preview, use it with caution knowing everything is not perfect yet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Implement the database permissions for an Azure AD Group
&lt;/h2&gt;

&lt;p&gt;Of the 3 possible solutions let's take the 3rd one with the Command provider and the &lt;code&gt;sqlcmd&lt;/code&gt; utility. It is probably not the "best" solution but I thought it would be simpler to use the &lt;code&gt;sqlcmd&lt;/code&gt; utility than writing a complete provider or even custom C# code to do the same. Furthermore, it's the opportunity to test the Command provider which is fairly new.&lt;/p&gt;

&lt;h3&gt;
  
  
  Allow the machine running the Pulumi program to connect to the SQL Server
&lt;/h3&gt;

&lt;p&gt;To run a SQL command in the database, the machine that executes the Pulumi program needs to have its public IP authorized. To programmatically retrieve the public IP address from where the Pulumi program is running we can use &lt;code&gt;ipify API&lt;/code&gt;. It's a simple open source HTTP API that returns the public IP address of the caller.&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;publicIp&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Output&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="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;GetStringAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"https://api.ipify.org"&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 note here that we are just using standard C# code with an &lt;code&gt;HttpClient&lt;/code&gt; that makes a &lt;code&gt;GET&lt;/code&gt; to the API and returns asynchronously a string. I like the fact that with Pulumi we can reuse our existing C# skills, and the libraries we are used to. If we were to do that in Terraform we would have to look in the documentation how to do HTTP calls, discover that there is an &lt;a href="https://registry.terraform.io/providers/hashicorp/http/latest/docs/data-sources/http"&gt;http data source&lt;/a&gt; that can be used, understand how it works (to be honest it seems quite simple but still that is not natural) and use it.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now we can enable this public IP by creating a firewall rule in the SQL Server.&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;enableLocalMachine&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;FirewallRule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AllowLocalMachine"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;FirewallRuleArgs&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;ResourceGroupName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resourceGroup&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;ServerName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlServer&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;StartIpAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;publicIp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;EndIpAddress&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;publicIp&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Create the Azure AD group that will be given access to the database
&lt;/h3&gt;

&lt;p&gt;We said we wanted to grant SQL Database access to an Azure AD group that will contain in the future users and application managed identities that need access to the database. So let's create that:&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;sqlDatabaseAuthorizedGroup&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"SqlDbUsersGroup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;GroupArgs&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;DisplayName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SqlDbUsersGroup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SecurityEnabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Owners&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;InputList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;sqlAdAdmin&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We set the Azure SQL Server admin as the owner of the group. This way, the admin of the database can add Azure AD users to the group and they directly have the permissions configured for this group. I like authorizing an Azure AD group instead of each Azure AD user because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it is easier to manage a group than individual users (adding a user to a group is less work than using SQL commands to assign the correct role for each user)&lt;/li&gt;
&lt;li&gt;you don't lose granularity of access control (you can always create several groups with different permissions if you need to)&lt;/li&gt;
&lt;li&gt;you can ensure that your application runs with the same permissions locally (the code you debug uses your user account identity) and on Azure (the code uses the managed identity of the App Service where it is hosted) by putting users and managed identities in the same group&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Assign the roles to the Azure AD group using the Command provider
&lt;/h3&gt;

&lt;p&gt;As we already talked about, we can specify a script to run on the &lt;code&gt;create&lt;/code&gt; operation and another on the &lt;code&gt;destroy&lt;/code&gt; operations. To keep things simple for this sample, we will only handle the creation scenario where we will add our Azure AD group as a user of the database and give it the expected roles. We already showed the SQL Command to execute, with our new group name it becomes:&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;sqlDatabaseAuthorizedGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DisplayName&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;ALTER&lt;/span&gt; &lt;span class="k"&gt;ROLE&lt;/span&gt; &lt;span class="n"&gt;db_datareader&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;sqlDatabaseAuthorizedGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DisplayName&lt;/span&gt;&lt;span class="p"&gt;};&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_datawriter&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;sqlDatabaseAuthorizedGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DisplayName&lt;/span&gt;&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;GO&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;sqlcmd&lt;/code&gt; utility can be used like this to send a command on the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;sqlcmd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-S&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sqlServer.Name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;database&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;windows&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;net&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;database.Name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-U&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sqlAdAdmin.UserPrincipalName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-P&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sqlAdAdmin.Password&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-G&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Q&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;' ___SQL Command___'&lt;/span&gt;&lt;span class="w"&gt;

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

&lt;/div&gt;



&lt;p&gt;You can check the &lt;a href="https://docs.microsoft.com/en-us/sql/tools/sqlcmd-utility?view=sql-server-ver15#sqlcmd-commands"&gt;documentation&lt;/a&gt; to learn more about how to use &lt;code&gt;sqlcmd&lt;/code&gt; but that is quite simple: we are just specifying to send a command line query on our database using Azure Active Directory to authenticate.&lt;/p&gt;

&lt;p&gt;If we use all that with our Command provider, we get the following C# code.&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;authorizeAdGroup&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"AuthorizeAdGroup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;CommandArgs&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Create&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"sqlcmd -S &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sqlServer&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="s"&gt;.database.windows.net -d &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;database&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="s"&gt; -U &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sqlAdAdmin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserPrincipalName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -P &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sqlAdAdmin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Password&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -G -l 30 -Q 'CREATE USER &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sqlDatabaseAuthorizedGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DisplayName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt; FROM EXTERNAL PROVIDER; ALTER ROLE db_datareader ADD MEMBER &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sqlDatabaseAuthorizedGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DisplayName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;; ALTER ROLE db_datawriter ADD MEMBER &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sqlDatabaseAuthorizedGroup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DisplayName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;;'"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;Interpreter&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;InputList&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s"&gt;"pwsh"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s"&gt;"-c"&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, we can specify a specific interpreter to use (PowerShell here).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Don't do like me and forget that our variables are &lt;a href="https://www.pulumi.com/docs/intro/concepts/inputs-outputs/#inputs-and-outputs"&gt;outputs&lt;/a&gt; (only fully known when the infrastructure resource is completely provisioned). Because of that it is necessary to use the &lt;code&gt;Output.Format&lt;/code&gt; method for string interpolation instead of using the C# operator &lt;code&gt;$&lt;/code&gt;. Thanks to the community on Slack for helping me on that one because with the Command provider not logging the errors details I had a hard time on this.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Results
&lt;/h3&gt;

&lt;p&gt;And that's it! We now have created the Azure AD group as an external user in the database and assigned it the &lt;code&gt;db_datareader&lt;/code&gt; and &lt;code&gt;db_datawriter&lt;/code&gt; roles. Here is what it looks like in Azure Data Studio:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/JlyF2hfd_al9loytGpl5k5kieQ6VLvfb0jBwORIJMaA/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9zcWxkYXRhYmFz/ZV9hZF9henVyZWRh/dGFzdHVkaW8ucG5n" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/JlyF2hfd_al9loytGpl5k5kieQ6VLvfb0jBwORIJMaA/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9zcWxkYXRhYmFz/ZV9hZF9henVyZWRh/dGFzdHVkaW8ucG5n" alt="" width="880" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;p&gt;This article is a bit long because I explain all the steps and possibilities but the complete code is not very big or complex. You can find it in this &lt;a href="https://github.com/TechWatching/SqlDatabaseWithAzureAd"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I did not see that many articles on the web that talk about using Azure Active Directory authentication for an Azure SQL Database, and even less that showed how to properly configure it using Infrastructure as Code. Yet, I think it's an important thing to do to properly secure your Azure SQL database. So I hope you enjoyed it and learn something. Whether you use Azure CLI, Bicep, ARM Templates, Terraform, or Pulumi, don't hesitate to use Azure AD authentication on your Azure SQL Database, for me that is the right and secure way to go.&lt;/p&gt;

&lt;p&gt;As you have seen in this article, even when there is no provider for your custom resource or task, there are always several solutions to do what you want with Pulumi. Some are more elegant, some are more complex than others but you will always find a way and you will not be limited by the platform.&lt;/p&gt;

&lt;p&gt;A big thank you to the Pulumi community that gave me some insights on how to configure Azure AD authentication on a database properly using Pulumi. Without the help of some people in the Pulumi Slack or the GitHub Issues/Discussions I would not have been able to write this article. Indeed some ideas and solutions are directly inspired by people's answers to my questions. This article is my way of contributing back and helping others that would have similar questions.&lt;/p&gt;

</description>
      <category>pulumi</category>
      <category>azure</category>
      <category>csharp</category>
      <category>cloudops</category>
    </item>
    <item>
      <title>IaC Hot Reload with Pulumi Watch</title>
      <dc:creator>Alexandre Nédélec</dc:creator>
      <pubDate>Thu, 19 Jan 2023 09:26:00 +0000</pubDate>
      <link>https://community.ops.io/techwatching/iac-hot-reload-with-pulumi-watch-60b</link>
      <guid>https://community.ops.io/techwatching/iac-hot-reload-with-pulumi-watch-60b</guid>
      <description>&lt;p&gt;Do you like using hot reload when developing applications? How about using hot reload when developing the cloud infrastructure of an application? Keep reading because that's what we are going to talk about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Developing and deploying Cloud Infrastructure
&lt;/h2&gt;

&lt;p&gt;When doing Infrastructure as Code for a cloud application we usually do the following steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;we write the code describing the desired state of the infrastructure&lt;/li&gt;
&lt;li&gt;we build this infrastructure code and compare the resulting desired state against the current state of the infrastructure&lt;/li&gt;
&lt;li&gt;we deploy to the provisioned infrastructure the changes needed to achieve the desired state&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When using Pulumi, you can run the &lt;a href="https://www.pulumi.com/docs/reference/cli/pulumi_preview/"&gt;&lt;code&gt;pulumi preview&lt;/code&gt;&lt;/a&gt; command for step 2 and the &lt;a href="https://www.pulumi.com/docs/reference/cli/pulumi_up/"&gt;&lt;code&gt;pulumi up&lt;/code&gt;&lt;/a&gt; command for step 3.&lt;/p&gt;

&lt;p&gt;As its name suggests, the &lt;code&gt;pulumi preview&lt;/code&gt; command only "preview" the updates that could be made to the infrastructure but does not apply them. To perform an update of the cloud infrastructure you have to use the &lt;code&gt;pulumi update&lt;/code&gt; command which also does a preview of the changes, prompts the user to approve the changes to be made, and performs these changes. That is why, to be honest I don't bother with &lt;code&gt;pulumi preview&lt;/code&gt;: most of the time I only use the &lt;code&gt;pulumi up&lt;/code&gt; command (which means I do steps 2 and 3 in one row).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🗨 In case you wonder, if you already have run &lt;code&gt;pulumi preview&lt;/code&gt; before running &lt;code&gt;pulumi up&lt;/code&gt; you can skip the preview in the &lt;code&gt;up&lt;/code&gt; command by using the &lt;code&gt;--skip-preview&lt;/code&gt; option.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Of course, you can use these commands to automate the deployment of your cloud infrastructure using your favorite &lt;a href="https://www.pulumi.com/docs/guides/continuous-delivery/"&gt;CI/CD system&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The need for a hot reload-like experience when doing IaC
&lt;/h2&gt;

&lt;p&gt;All this is great but there are times when all you want is to quickly write your infrastructure code and check that you can successfully provision and configure the cloud resources you need. This can happen when you want to prototype something, test a new cloud resource, or simply when you are developing your infrastructure and want to verify your infrastructure code works. Usually, you are using a "sandbox" cloud environment for this, the same way you would use your local environment for debugging application code. At these moments, you don't care about CI/CD. You care about quickly experimenting with changes to your infrastructure, being productive, making changes in your code, and checking what it does, so you want to be able to quickly iterate to make your code work. Yet, this is not possible if each time you make a change to the code you have to manually run the &lt;code&gt;pulumi up&lt;/code&gt; command, and approve the deployment.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🗨 You can use the &lt;code&gt;--yes&lt;/code&gt; option to automatically approve the changes and directly perform the update when running the &lt;code&gt;pulumi up&lt;/code&gt; command.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;But guess what? Fast feedback when doing a code change is exactly what you want when developing an application. Indeed whether you are building an application or building infrastructure you are doing software development so you have the same needs and practices. And what do application developers have in their toolbox to be more productive when developing? They have "hot reload": while debugging locally an application, they can modify the source code, and changes made will be almost instantaneously reflected on the application. Wouldn't it be great if similarly you could make a change in your infrastructure code and have the provisioned infrastructure automatically updated? That is what the &lt;code&gt;pulumi watch&lt;/code&gt; command is here for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Pulumi Watch
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://www.pulumi.com/docs/reference/cli/pulumi_watch/"&gt;&lt;code&gt;pulumi watch&lt;/code&gt;&lt;/a&gt; is a command currently in preview that watches for changes in the infrastructure code directory and continuously updates the cloud resources.&lt;/p&gt;

&lt;p&gt;But the best is to see by yourself. In the following example, you can see on the left of the screen a terminal opened with the &lt;code&gt;pulumi watch&lt;/code&gt; command running, and on the right of the screen vscode opened with the code describing the currently provisioned Azure infrastructure for my project. Some lines to create a "Tweets" table in the storage account are commented. When I uncomment them and save the code file, you can see that pulumi detects it, builds the code, and deploys the changes so creates the table in that case.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/SKjniOHdEC4Yh5OyBPIDyUCNF1TPcMYcREPhXpRtsd0/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2MxaGI5/bTB6dW15d3g2OHl5/dGIxLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/SKjniOHdEC4Yh5OyBPIDyUCNF1TPcMYcREPhXpRtsd0/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2MxaGI5/bTB6dW15d3g2OHl5/dGIxLnBuZw" alt="Image description" width="880" height="406"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://community.ops.io/images/LewPUzS9v5udXwzGorPNYpjWvFiNFnk5yWtFGLL3rj4/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL25rNWkx/Ynl0a3FzaWFscHV5/ZDh0LnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/LewPUzS9v5udXwzGorPNYpjWvFiNFnk5yWtFGLL3rj4/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL25rNWkx/Ynl0a3FzaWFscHV5/ZDh0LnBuZw" alt="Image description" width="880" height="406"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://community.ops.io/images/DfYb-ijbQ4yIZ41gMPPCUxr4dFZE-d0vjQ7Ab1rEOrw/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL3cxMGJr/YnRuYmFiMTQ5NXRv/MmJ5LnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/DfYb-ijbQ4yIZ41gMPPCUxr4dFZE-d0vjQ7Ab1rEOrw/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL3cxMGJr/YnRuYmFiMTQ5NXRv/MmJ5LnBuZw" alt="Image description" width="880" height="406"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://community.ops.io/images/39Yr1HioGtr5LUi_kt6jN7ACcQzlCR_8_TWc9odbWPU/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzLzc2ejlm/bHlvdnFtdTgyNDk3/Ym9tLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/39Yr1HioGtr5LUi_kt6jN7ACcQzlCR_8_TWc9odbWPU/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzLzc2ejlm/bHlvdnFtdTgyNDk3/Ym9tLnBuZw" alt="Image description" width="880" height="406"&gt;&lt;/a&gt;&lt;br&gt;
&lt;a href="https://community.ops.io/images/W6nXysFxd2DqgQz1s-zgOJiiSRVVZVqIjxGtZz_VVYQ/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2p1aHk4/eWlyNzUwZGhzcmdj/YTc4LnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/W6nXysFxd2DqgQz1s-zgOJiiSRVVZVqIjxGtZz_VVYQ/w:880/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2p1aHk4/eWlyNzUwZGhzcmdj/YTc4LnBuZw" alt="Image description" width="880" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I don't know what you think but I find this pretty cool: it's like hot reload for your infrastructure as code. Of course, you will probably not use &lt;code&gt;pulumi watch&lt;/code&gt; all the time, but for quickly writing and testing your infrastructure code it can be very helpful.&lt;/p&gt;

&lt;p&gt;As far as I know (don't hesitate to correct me in the comments if I am wrong), there is no such feature in Terraform and it's too bad because when you start using the &lt;code&gt;watch&lt;/code&gt; command you don't want to do without it.&lt;/p&gt;

</description>
      <category>iac</category>
      <category>pulumi</category>
      <category>productivity</category>
      <category>azure</category>
    </item>
    <item>
      <title>Pulumi with an Azure Blob Storage backend</title>
      <dc:creator>Alexandre Nédélec</dc:creator>
      <pubDate>Thu, 12 Jan 2023 09:00:00 +0000</pubDate>
      <link>https://community.ops.io/techwatching/pulumi-with-an-azure-blob-storage-backend-4jci</link>
      <guid>https://community.ops.io/techwatching/pulumi-with-an-azure-blob-storage-backend-4jci</guid>
      <description>&lt;p&gt;By default when you use Pulumi, the state is managed by Pulumi Service which is very convenient as you can concentrate on building your project infrastructure instead of spending time on where to store the state and how to handle concurrency. However, sometimes for governance or pricing concerns, or any other reasons, you don't want to use Pulumi Service and you prefer to manage the state yourself with your own backend. In this article, we will see how we can do that using Azure.&lt;/p&gt;

&lt;h2&gt;
  
  
  A quick reminder about states and backends
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What is this state we need to store?
&lt;/h3&gt;

&lt;p&gt;Like other Infrastructure as Code platforms, Pulumi uses a declarative approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;we write code to describe the desired state of our infrastructure&lt;/li&gt;
&lt;li&gt;Pulumi engine compares this desired state with the current state of the infrastructure and determines what changes need to be made&lt;/li&gt;
&lt;li&gt;Pulumi deploys these changes and updates the current state of the provisioned infrastructure&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;🗨 Some people think using Pulumi means adopting an imperative approach because we are using programming languages (so imperative languages) instead of using declarative languages (like YAML, JSON, and HCL). However, being declarative is not about the language used but about defining the "what" (the infrastructure we want to provision) instead of the "how" (the steps to provision this infrastructure). So Pulumi has the best of both worlds by being declarative while using programming languages.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As you understood, being able to provision and modify an infrastructure with this declarative approach requires 2 states: the desired state and the current state of the infrastructure. The desired state is the infrastructure code that we usually store in a Git repository alongside the application code. The current state however is computed by the Pulumi engine each time we modify the infrastructure and needs to be stored somewhere.&lt;/p&gt;

&lt;p&gt;That is why we need a "backend" to use Pulumi, it's just a place to store the current state of the provisioned infrastructure.&lt;/p&gt;

&lt;p&gt;If you want more information about states and backend, Pulumi has a &lt;a href="https://www.pulumi.com/docs/intro/concepts/state/"&gt;documentation page&lt;/a&gt; about that and there is also &lt;a href="https://www.pulumi.com/docs/intro/concepts/how-pulumi-works/"&gt;a page&lt;/a&gt; about how Pulumi works.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/FITFZMHUvSSjyLwmGDbA4BvzU2bh3v9TY29HUYpTK6U/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlhenVy/ZWJhY2tlbmRfc2No/ZW1hXzEucG5n" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/FITFZMHUvSSjyLwmGDbA4BvzU2bh3v9TY29HUYpTK6U/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlhenVy/ZWJhY2tlbmRfc2No/ZW1hXzEucG5n" alt="" width="880" height="582"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What "backends" can we use to manage the infrastructure?
&lt;/h3&gt;

&lt;p&gt;The default backend is Pulumi Service which is a web application that stores the infrastructure state and has additional features like concurrent state locking, team policies, or deployment history. This service is managed by Pulumi, is free for individuals but charged for teams, and enterprises. It can be self-hosted in the enterprise plan. Just as a side note, Pulumi Service (along with support and training) is how the company Pulumi makes money because everything else is free and open source.&lt;/p&gt;

&lt;p&gt;Yet, we don't have to pay anything to use Pulumi because Pulumi Service, no matter how good it may be, is not the only solution to store the infrastructure state. Indeed, Pulumi supports other backends that we can manage ourselves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Local Filesystem&lt;/li&gt;
&lt;li&gt;AWS S3 (or compatible server)&lt;/li&gt;
&lt;li&gt;Google Cloud Storage&lt;/li&gt;
&lt;li&gt;Azure Blob Storage&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the rest of this article, we will see how to use Pulumi with Azure Blob Storage as the backend for our infrastructure state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using Pulumi with the Azure Blob Storage backend
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What do we need?
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://www.pulumi.com/docs/intro/concepts/state/#logging-into-the-azure-blob-storage-backend"&gt;Pulumi documentation&lt;/a&gt; on using Azure Blob Storage backend is short. It only says that we need to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;set the AZURE_STORAGE_ACCOUNT environment variable to specify the Azure storage account to use&lt;/li&gt;
&lt;li&gt;set the AZURE_STORAGE_KEY or the AZURE_STORAGE_SAS_TOKEN environment variables to let Pulumi access the storage&lt;/li&gt;
&lt;li&gt;execute the following command &lt;code&gt;pulumi login azblob://&amp;lt;container-path&amp;gt;&lt;/code&gt; where &lt;code&gt;container-path&lt;/code&gt; is the path to a blob container in the storage account&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once this command is executed, we can start using Pulumi as we would with any other backend. The infrastructure's current state will automatically be stored in the blob container you specified. It will be compared to the desired state when a change is made in the code to know what resources need to be created/updated/deleted.&lt;/p&gt;

&lt;p&gt;In fact, that is not very complex. Nevertheless, the documentation assumes we already have created an Azure storage account with a blob container in it and retrieved the key to access it. That is not the case, so now that we know what we need let's script it!&lt;/p&gt;

&lt;h3&gt;
  
  
  How to create and configure the Azure Blob Storage backend?
&lt;/h3&gt;

&lt;p&gt;For me, the easiest way to write a script to create and configure the storage account we need is to use Azure CLI. One nice way of writing Azure CLI scripts is to do it in vscode with the &lt;a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.azurecli"&gt;Azure CLI Tools extension&lt;/a&gt;: you can create &lt;code&gt;.azcli&lt;/code&gt; files with IntelliSense on them and run the commands you are writing in the integrated terminal (see screenshot below).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/I3sF7blnqGl6tb-vnNnFOUuHvjel0rD_2zwuXnmuPZo/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlhenVy/ZWJhY2tlbmRfdnNj/b2RlXzEucG5n" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/I3sF7blnqGl6tb-vnNnFOUuHvjel0rD_2zwuXnmuPZo/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlhenVy/ZWJhY2tlbmRfdnNj/b2RlXzEucG5n" alt="" width="880" height="522"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🗨 If you are not familiar with Azure CLI, you can check my article "&lt;a href="https://www.techwatching.dev/posts/welcome-azure-cli"&gt;Goodbye Azure Portal, Welcome Azure CLI&lt;/a&gt;".&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let's first define a few environment variables: the name of the resource group that will contain our storage account, its location, and the name of the storage account (I am using PowerShell but don't forget to change the syntax if you are using another shell like bash).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$random&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Get-Random&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Maximum&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"West Europe"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$rgName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"rg-iacstate-westeu-&lt;/span&gt;&lt;span class="nv"&gt;$random&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$saName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"stiacstate&lt;/span&gt;&lt;span class="nv"&gt;$random&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then let's create our resource group and our storage account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$rgName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$location&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$rgName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$saName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--sku&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Standard_LRS&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key to access the storage account can be retrieved with the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--account-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$saName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$rgName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tsv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'[0].value'&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using this command, we can now set the environment variables that will be used by the Pulumi CLI to access our Azure Blob Storage account backend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;AZURE_STORAGE_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$saName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$rgName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tsv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'[0].value'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;AZURE_STORAGE_ACCOUNT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$saName&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally, we can create the blob container that will contain the infrastructure state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;iacstate&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How to provision your project infrastructure using the Azure Blob Storage backend?
&lt;/h3&gt;

&lt;p&gt;Now that our blob container exists, we can use the pulumi login command we already talked about to indicate pulumi to use the newly created azure blob storage as the backend.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;pulumi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;login&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;azblob://iacstate&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To verify Pulumi can correctly provision cloud resources using our Azure Blob Storage backend, we can create a new Pulumi project using the &lt;code&gt;azure-csharp&lt;/code&gt; template and deploy the infrastructure with the &lt;code&gt;pulumi up&lt;/code&gt; command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;mkdir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;infra&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;infra&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;pulumi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;azure-csharp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AzureStorageBackend&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dev&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-y&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;pulumi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;up&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-y&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When executing these commands, Pulumi will ask us to provide a passphrase. Why is that? It is to encrypt secrets contained in the infrastructure state. This way no secret is stored in plain text in the state.&lt;/p&gt;

&lt;p&gt;Once the &lt;code&gt;pulumi up&lt;/code&gt; command is finished, the infrastructure requested is provisioned, and we can see a new state file has been created in the &lt;code&gt;iacstate&lt;/code&gt; blob container.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/3dtXQruteLZZMkDP5CWov_SMy8FKTZUV_cvDddab434/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlhenVy/ZWJhY2tlbmRfYXp1/cmVfMS5wbmc" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/3dtXQruteLZZMkDP5CWov_SMy8FKTZUV_cvDddab434/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlhenVy/ZWJhY2tlbmRfYXp1/cmVfMS5wbmc" alt="" width="880" height="255"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing state sensitive data
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why is it needed to protect sensitive data in the state ?
&lt;/h3&gt;

&lt;p&gt;The state is transmitted and stored securely by Pulumi and whatever the backend you use you should restrict its access. For instance, in our example, you should have assigned the permissions on the storage account so that only the right people have access to it. Nevertheless, securing the state file is not enough because it contains sensitive data (keys, connection strings, ...) that you probably don't want anyone that access to the file to be able to get.&lt;/p&gt;

&lt;p&gt;Indeed, it's not because a developer needs to read the state file to debug an issue that you want him to be able to see some production sensitive data in plain text in the state. Having secrets in plain text in a state file would be like putting secrets in your source control and telling it is safe because only developers of the project team have access to it. Moreover, even if an unauthorized person succeeds to get access to the state file, it won't be an issue if all secrets in it are encrypted. Hence that is very nice to see Pulumi take security seriously and always encrypt sensitive information.&lt;/p&gt;

&lt;h3&gt;
  
  
  What are the available encryption providers?
&lt;/h3&gt;

&lt;p&gt;As we have seen previously, when using a self-managed backend like Azure Blob Storage, by default Pulumi uses a passphrase to encrypt sensitive data.&lt;/p&gt;

&lt;p&gt;The passphrase is just one of the supported encryption/secrets providers but there are others:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AWS Key Management Service&lt;/li&gt;
&lt;li&gt;Azure Key Vault&lt;/li&gt;
&lt;li&gt;Google Cloud Key Management Service&lt;/li&gt;
&lt;li&gt;HashiCorp Vault Transit Secrets Engine&lt;/li&gt;
&lt;li&gt;Pulumi Service (used by default when using Pulumi Service as the backend)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As for the backend, you don't have to use the default encryption provider and can come with your own resource. These providers can be used whatever the backend you chose, which lets you many possibilities. Now let's see how to use Azure Key Vault as our encryption provider.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to use Azure Key Vault as the encryption provider?
&lt;/h3&gt;

&lt;p&gt;Let's first create a Key Vault:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$kvName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"kv-iacstate-westeu-&lt;/span&gt;&lt;span class="nv"&gt;$random&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$vaultId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keyvault&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$rgName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$kvName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--enable-rbac-authorization&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We retrieve its id so that we can use it to assign the correct role to my user to be able to perform cryptographic operations. With the &lt;code&gt;--enable-rbac-authorization&lt;/code&gt; parameter we set the permissions model on the key vault to Role-Based Access Control but you can use the classic Vault access policies as well. I prefer using RBAC because I think it's more modern and more consistent with how we manage permissions on other Azure resources.&lt;/p&gt;

&lt;p&gt;To assign the appropriate permission to the current logged-in user, we will need its current identifier in Azure that we can retrieve with the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$myUserId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;signed-in-user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;show&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"objectId"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tsv&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can then assign to this user the &lt;code&gt;Key Vault Crypto Officer&lt;/code&gt; role that will allow us to create a key and encrypt/decrypt data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;assignment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--scope&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$vaultId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Key Vault Crypto Officer"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--assignee&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$myUserId&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key to encrypt/decrypt data can be created with the following command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keyvault&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;encryptionState&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--vault-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$kvName&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By default, Pulumi CLI will try to use environment variables to authenticate to the key vault, so we need to tell it to use the Azure CLI instead as we gave the permission on the key vault to the user currently logged in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;AZURE_KEYVAULT_AUTH_VIA_CLI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now that everything is configured, we can modify our previous command to create a new Pulumi project by specifying the encryption provider to use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;pulumi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;azure-csharp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AzureStorageBackend&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dev&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-y&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--secrets-provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"azurekeyvault://&lt;/span&gt;&lt;span class="nv"&gt;$kvName&lt;/span&gt;&lt;span class="s2"&gt;.vault.azure.net/keys/encryptionState"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Comparing with how Terraform handle state
&lt;/h2&gt;

&lt;p&gt;Terraform is another very popular Infrastructure as Code platform with lots of similarities so I thought it might be interesting to look at how Terraform handles state compared to Pulumi.&lt;/p&gt;

&lt;p&gt;Terraform has a SaaS platform called Terraform Cloud that can be used to manage the infrastructure state. It is similar to what Pulumi Service offers. However, when using Terraform the default backend is not Terraform Cloud but local filesystem. That is not better or worse, just a different choice HashiCorp (the company behind Terraform) did. Although I must say that when I started working on Pulumi, I found it easier not having to take care of where the state is stored and how it is managed, so maybe a SaaS backend by default is simpler.&lt;/p&gt;

&lt;p&gt;On Microsoft documentation, there is a tutorial &lt;a href="https://docs.microsoft.com/en-us/azure/developer/terraform/store-state-in-azure-storage"&gt;"Store Terraform state in Azure Storage"&lt;/a&gt; that shows how to use Terraform with an Azure Storage backend. I have done it and it is very similar to what we have done in this article with Pulumi. Instead of using a CLI command to configure the infrastructure to use Azure Blob Storage as the backend for the state, in Terraform, you configure it directly in one of the code files but the idea is the same. Both IaC tools store the infrastructure state in a JSON file in a blob container.&lt;/p&gt;

&lt;p&gt;One big difference however is that by default Terraform does not encrypt sensitive information in the state file. As far as I know, there is no concept of secret providers in Terraform so no built-in solution. &lt;a href="https://www.terraform.io/docs/language/state/sensitive-data.html"&gt;Terraform documentation&lt;/a&gt; just says to &lt;code&gt;treat the state itself as sensitive data&lt;/code&gt;. That means when I created a storage account using Terraform with the Azure Blob Storage backend, the keys of my storage were available in plain text in my state file (as you can see in the image below).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/MiezmbS3NGw9KtnLC71moN_eGNjwdkFyB1S8q9XGRa4/w:880/mb:500000/ar:1/aHR0cHM6Ly9kZXYt/dG8tdXBsb2Fkcy5z/My5hbWF6b25hd3Mu/Y29tL3VwbG9hZHMv/YXJ0aWNsZXMvdHk2/NjlieXp1anl4Mmhz/eTY3dWkucG5n" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/MiezmbS3NGw9KtnLC71moN_eGNjwdkFyB1S8q9XGRa4/w:880/mb:500000/ar:1/aHR0cHM6Ly9kZXYt/dG8tdXBsb2Fkcy5z/My5hbWF6b25hd3Mu/Y29tL3VwbG9hZHMv/YXJ0aWNsZXMvdHk2/NjlieXp1anl4Mmhz/eTY3dWkucG5n" alt="Image description" width="880" height="666"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You should not have this kind of security issue using Terraform Cloud and there are probably external tools to avoid this, but I think an IaC platform should be secure by default and that encryption of sensitive data should be built-in.&lt;/p&gt;

&lt;h2&gt;
  
  
  To conclude
&lt;/h2&gt;

&lt;p&gt;You can find below the complete Azure CLI script used in this article:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# PowerShell variables used in the script &lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$random&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Get-Random&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Maximum&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;1000&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"West Europe"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$rgName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"rg-iacstate-westeu-&lt;/span&gt;&lt;span class="nv"&gt;$random&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$saName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"stiacstate&lt;/span&gt;&lt;span class="nv"&gt;$random&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$kvName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"kv-iacstate-westeu-&lt;/span&gt;&lt;span class="nv"&gt;$random&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;group&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$rgName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$location&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Configure the Azure Blob Storage that will contain the state &lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$rgName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$saName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-l&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--sku&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Standard_LRS&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Set environment variables needed to write on the storage account&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;AZURE_STORAGE_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$saName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$rgName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tsv&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;'[0].value'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;AZURE_STORAGE_ACCOUNT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$saName&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;storage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;iacstate&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Configure the Key Vault that will be used to encrypt the sensitive data&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$vaultId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keyvault&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-g&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$rgName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$kvName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--enable-rbac-authorization&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;true&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$myUserId&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;ad&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;signed-in-user&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;show&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--query&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"objectId"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-o&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;tsv&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;assignment&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--scope&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$vaultId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--role&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Key Vault Crypto Officer"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--assignee&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$myUserId&lt;/span&gt;&lt;span class="w"&gt; 
&lt;/span&gt;&lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;keyvault&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;create&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;encryptionState&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--vault-name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$kvName&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Use az cli to authenticate to key vault instead of using environment variables &lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;AZURE_KEYVAULT_AUTH_VIA_CLI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"true"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# Indicate pulumi to use the newly created azure blob storage as a backend&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;pulumi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;login&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;azblob://iacstate&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Create and use a folder to store the infrastructure code&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;mkdir&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;infra&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="n"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;infra&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Create a new Pulumi project using the azure blob storage as the backend and the keyvault as the encryption provider &lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;pulumi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;azure-csharp&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-n&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;AzureStorageBackend&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-s&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;dev&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-y&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--secrets-provider&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"azurekeyvault://&lt;/span&gt;&lt;span class="nv"&gt;$kvName&lt;/span&gt;&lt;span class="s2"&gt;.vault.azure.net/keys/encryptionState"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="c"&gt;# Deploy the infrastructure&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="n"&gt;pulumi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;up&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-y&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using Pulumi without Pulumi Service was not complicated as I thought it would be. I like the fact that Pulumi is not limited to be used with Pulumi Service backend and secret provider. It gives us the choice to use what we want: if I want to use Google Cloud Storage as my back-end and AWS Key Management Service as my encryption provider I totally can. Many options are available and well integrated without requiring much work which is nice.&lt;/p&gt;

&lt;p&gt;Yet honestly, I think that using Pulumi Service will be my default choice because of the many built-in features it offers (deployment history, concurrent state locking, collaboration functionalities, ...). It's free for individuals so I would not bother with a self-managed backend for individuals. For teams and companies, you have to pay (even if there is a monthly free grant of credits for the team plan) but I don't have enough perspective to say if it's worth it. You can find the pricing &lt;a href="https://www.pulumi.com/pricing/"&gt;here&lt;/a&gt; if you want to see by yourself. I guess the choice between that and a self-managed backend will probably depend more on the project and the organization you are working for.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>pulumi</category>
      <category>cloudops</category>
      <category>iac</category>
    </item>
    <item>
      <title>Why will I choose Pulumi over Terraform for my next project?</title>
      <dc:creator>Alexandre Nédélec</dc:creator>
      <pubDate>Thu, 05 Jan 2023 09:00:00 +0000</pubDate>
      <link>https://community.ops.io/techwatching/why-will-i-choose-pulumi-over-terraform-for-my-next-project-4gb1</link>
      <guid>https://community.ops.io/techwatching/why-will-i-choose-pulumi-over-terraform-for-my-next-project-4gb1</guid>
      <description>&lt;p&gt;In today's world of cloud-first applications, multi-cloud/hybrid cloud companies, and complex infrastructures, using infrastructure as code is essential. In recent years, Terraform has become one of the most popular IaC solutions, but its challenger Pulumi is quickly gaining traction. In this article, I will tell you why I think Pulumi is better and why I will choose it over Terraform for my next project.&lt;/p&gt;

&lt;p&gt;But first, let's talk about what makes a good Infrastructure as Code solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  What makes a good Infrastructure as Code solution?
&lt;/h2&gt;

&lt;p&gt;There is no universal answer to this question, but I can give you the characteristics I am looking for in an IaC solution.&lt;/p&gt;

&lt;p&gt;In my opinion, an IaC solution should be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;declarative&lt;/strong&gt;. I think it is important to focus on what infrastructure we want to provision rather than how to provision it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;open source&lt;/strong&gt;. Beyond being a good thing, open source favors the adoption of technology and makes an ecosystem healthy with contributions from the community&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;multi providers&lt;/strong&gt;. Even if I work mainly with Azure, companies tend to be multi clouds; projects often involve provisioning resources in different cloud providers and services. Moreover, beyond cloud providers, I want to be able to automate the provisioning of many things like my Azure DevOps or GitHub projects.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;easy to learn, easy to use, and easy to be productive with&lt;/strong&gt;. I love learning new things but even more when it's easy and I can be quickly productive with new technology.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;up to date with main cloud providers' resources and features&lt;/strong&gt;. IT (and specifically the cloud) is evolving very quickly. The infrastructure we provision needs to be able to benefit from the latest resources, innovations, security improvements... I don't want to have to wait for novelties to be available&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;flexible and customizable&lt;/strong&gt;. Cloud infrastructures are more and more complex, and each project has its specificities so flexibility and the ability to easily write custom code to address these specificities are important&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;secure&lt;/strong&gt;. Security in IT is of paramount importance (especially when dealing with infrastructure) and therefore should be built-in
&lt;img src="https://community.ops.io/images/kLK1a4QFyse4PncEU9SDmQpM_dUsDzoA9c6Cre4Ab1w/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X2Nsb3VkXzEuanBn" alt="" width="640" height="427"&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Terraform and Pulumi have many similarities and some of the characteristics (like being declarative, open source, or multi providers) I mentioned above are present in both solutions. However, that is not the case with all these characteristics. Furthermore, Terraform and Pulumi differ in many other aspects that we will talk about in this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pulumi, a modern IaC tool with many built-in features
&lt;/h2&gt;

&lt;p&gt;Usually, tools that have been there for quite some time have more features than new tools. New tools bring modernity and innovative features but need a bit of time to catch up with all the features. This is not at all the case with Pulumi, it is even the opposite. Although Terraform is older (created in 2014 vs. 2018 for Pulumi), Pulumi has more built-in features (including key features) than Terraform. Moreover, some of Terraform features are restricted to its paid version Terraform Cloud 💸.&lt;/p&gt;

&lt;h3&gt;
  
  
  State, backends, and security
&lt;/h3&gt;

&lt;p&gt;Both Terraform and Pulumi use code (HCL and programming languages respectively) to describe the desired state of an infrastructure that is compared to the current state of the infrastructure to know which operations (create, update, delete) to do on resources. The current state of the infrastructure is stored in a "backend" than can be for instance the local filesystem, AWS S3, Google Cloud Storage, Azure Blob Storage, or the SaaS offering of Terraform (Terraform Cloud)/Pulumi (Pulumi Service). &lt;a href="https://cloud.hashicorp.com/products/terraform"&gt;Terraform Cloud&lt;/a&gt; and &lt;a href="https://www.pulumi.com/docs/intro/pulumi-service/"&gt;Pulumi Service&lt;/a&gt; are self-managed backends that offer similar functionalities (deployment history, collaboration functionalities, RBAC for an organization...).&lt;/p&gt;

&lt;p&gt;The state stored in a backend contains sensitive data (secrets like connection strings 🔑) that you need to secure. Whatever the backend you choose, &lt;strong&gt;when using Pulumi the secrets in your state are always encrypted&lt;/strong&gt; using an encryption provider. The default encryption provider depends on the backend but you can easily configure Pulumi to specify another encryption provider to use. The encryption provider can be a passphrase, AWS Key Management Service, Azure Key Vault, Google Cloud Key Management Service, HashiCorp Vault Transit Secrets Engine, or Pulumi Service. The ability to choose an encryption provider and to encrypt secrets in the state is not something Terraform supports. By default, Terraform will store the state in a local JSON file with the secrets in it 🙀. You probably won't run into security issues if you use Terraform Cloud, yet &lt;strong&gt;security should be built-in and not something you have to pay for&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/f8zYafvSkMYiLPglik-PqdFpqhkeUZi9U7tqs0eOv0Y/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X3NlY3VyaXR5XzEu/anBn" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/f8zYafvSkMYiLPglik-PqdFpqhkeUZi9U7tqs0eOv0Y/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X3NlY3VyaXR5XzEu/anBn" alt="" width="640" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You may not see this as a big concern as you are probably not storing your state locally but on a cloud storage (like an Azure Blob Storage) where access is restricted to only a few people. But let's imagine one of your storage access keys gets compromised. If you are using Terraform, someone could have access to all the secrets of your infrastructure. If you are using Pulumi he will not because all secrets in the state file are encrypted. And he will have trouble decrypting them because he would have to gain access to the encryption provider. Whether you use Azure Key Vault, AWS Key Management Service, or any other encryption provider, these are components whose purpose is to keep data safe 🔐, and that require proper permissions to have access to encryption/decryption keys. I am not saying you can't make your infrastructure safe with Terraform. (In my example above with an Azure Storage Account, you could always prevent the &lt;a href="https://docs.microsoft.com/en-us/azure/storage/common/shared-key-authorization-prevent?tabs=portal"&gt;shared key authorization&lt;/a&gt; and use only Azure AD authorization to reduce the security risk.) I am just saying that &lt;strong&gt;Pulumi is secure by default, Terraform is not&lt;/strong&gt; and extra work is required.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you want to learn more about state, backend, security, and how Terraform handles state compared to Pulumi you can check this &lt;a href="https://www.techwatching.dev/posts/pulumi-azure-backend"&gt;article&lt;/a&gt; where I talk about all. I also show how to use Azure Blob Storage as the backend and Azure Key Vault as the encryption provider for my infrastructure.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  IaC brownfield development
&lt;/h3&gt;

&lt;p&gt;Infrastructure as Code is not a new concept and before Terraform and Pulumi arrived, cloud providers' native solutions have been widely used to provision cloud infrastructure. Some infrastructures were also created manually. So today, there is a lot of existing infrastructure and most projects are not greenfield but brownfield projects. When choosing an IaC solution it is important to consider that and have the tools to integrate the existing infrastructure.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/kjMCWwEtrwbBmqwC4V00G-dqyS7bMcd5H7zLb2RMcgc/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X2Jyb3duZmllbGQu/anBn" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/kjMCWwEtrwbBmqwC4V00G-dqyS7bMcd5H7zLb2RMcgc/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X2Jyb3duZmllbGQu/anBn" alt="" width="640" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Both Terraform and Pulumi have an &lt;code&gt;import&lt;/code&gt; CLI command to import existing infrastructure. Currently, Terraform can only import one resource at a time and can only import it into the state without generating the corresponding configuration code. Pulumi supports bulk import operations (using a JSON file to specify the resources to import) and generates the corresponding infrastructure code to add. It may seem like anecdotal features but they become important when you have a lot of resources to import.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Besides, Microsoft has recently announced a new tool &lt;a href="https://github.com/Azure/aztfy"&gt;Azure Terrafy&lt;/a&gt; to "quickly turn existing Azure infrastructure into Terraform HCL and import to Terraform state". I guess they did not want to wait for Terraform to have this feature built-in. This won't help you if you are using Terraform with AWS or GCP though.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Being able to import existing resources into Pulumi/Terraform is nice. However, for complex infrastructure, you will probably do it progressively or not at all if you want to keep some parts managed by other tools/teams. No matter the case, you will need your new infrastructure to coexist with the existing infrastructure not (yet) managed by Pulumi/Terraform. Both Terraform and Pulumi can reference existing infrastructure but Pulumi goes beyond that. First, it can reference other Pulumi stacks i.e Pulumi projects, which is especially useful when you are in a big organization or when your architecture is divided into microservices. Second, it can reference external states, i.e outputs from infrastructure created with other IaC tools than Pulumi. For instance, you could reference a Terraform state (whether it is a local &lt;code&gt;tfstate&lt;/code&gt; or a remote state like a state in a Terraform Cloud workspace) or an AWS CloudFormation stack: you would get access to all the outputs of the corresponding provisioned infrastructure. That way, even if you have some existing infrastructure managed outside your Pulumi project, you can reference it and use it in your project without having to reference each resource with hard-coded names.&lt;/p&gt;

&lt;p&gt;If you have already spent time building a complex infrastructure with another IaC tool and want to migrate to Pulumi while preserving the organization of your code and without rewriting everything with Pulumi SDKs from scratch, Pulumi has some &lt;a href="https://www.pulumi.com/docs/converters/"&gt;conversion tools&lt;/a&gt; to help you with that. For instance, you can use the &lt;a href="https://www.pulumi.com/arm2pulumi/"&gt;arm2pulumi&lt;/a&gt; converter to convert your ARM templates to Pulumi code written in your preferred programming language. You can try it yourself on the website to see what it looks like.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/3GPC7YyH7AwXLMnOzP3jT0ZKNSyqFPPtgOawP9xwUII/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X2NvbnZlcnRlcl8x/LnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/3GPC7YyH7AwXLMnOzP3jT0ZKNSyqFPPtgOawP9xwUII/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X2NvbnZlcnRlcl8x/LnBuZw" alt="" width="880" height="516"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I like how Pulumi has been designed to work with brownfield projects: it offers all that is needed to coexist with existing infrastructure, adopting it, and converting it to Pulumi code if desired. It goes far beyond the built-in capabilities of Terraform on the topic.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Pulumi has a very &lt;a href="https://www.pulumi.com/docs/guides/adopting/"&gt;well-written documentation&lt;/a&gt; about "adopting Pulumi" if you want to deep dive into working with existing infrastructure.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Environments and configuration
&lt;/h3&gt;

&lt;p&gt;Thanks to its concept of &lt;a href="https://www.pulumi.com/docs/intro/concepts/stack/"&gt;stack&lt;/a&gt;, Pulumi has built-in support for deploying the same infrastructure in multiple environments. You can use configuration files to have different settings depending on the environment. For example, if you have a development environment and a production environment that need to have different sizes of VMs to avoid paying too much for the development environment, you will have 2 configuration files: &lt;code&gt;Pulumi.development.yaml&lt;/code&gt; and a &lt;code&gt;Pulumi.production.yaml&lt;/code&gt;. These files will contain the same setting to specify the size of the VM with different values. This concept of having a configuration file by environment is quite similar to what you would use when developing an application. In addition to that, you can use the Pulumi CLI to set settings as secrets that will be encrypted in your configuration files (using the same encryption provider that encrypts your state). So Pulumi offers you everything you need to easily and safely provision infrastructure in different environments.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/pnTnvgg_Y7ZKqqDd5I4C87jkEBSgZU3j-rcRFEBx_EY/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X2NvbmZpZ3VyYXRp/b25fMS5wbmc" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/pnTnvgg_Y7ZKqqDd5I4C87jkEBSgZU3j-rcRFEBx_EY/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X2NvbmZpZ3VyYXRp/b25fMS5wbmc" alt="" width="880" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Of course, Terraform can also provision infrastructure in different environments. Having different folders for the different environments used to be the way of handling different environments in Terraform which meant a lot of code duplication (unfortunately some companies still do that today😕). Because of that and other deficiencies of Terraform, different Terraform wrappers and frameworks (like &lt;a href="https://terragrunt.gruntwork.io/"&gt;Terragrunt&lt;/a&gt; or &lt;a href="https://terraspace.cloud/"&gt;Terraspace&lt;/a&gt;) were created by the community to keep the infrastructure code DRY and structured.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you want to know the differences between Terraform, Terragrunt, and Terraspace, BlogOps (the creator of Terraspace) has an interesting article on the &lt;a href="https://blog.boltops.com/2020/09/28/terraform-vs-terragrunt-vs-terraspace/"&gt;topic&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In 2017, Terraform also introduced its built-in way of managing different environments with &lt;a href="https://www.terraform.io/language/state/workspaces"&gt;Terraform workspaces&lt;/a&gt;. I think it's great to have different alternatives, but the drawback is to have the Terraform community divided between people that are using pure Terraform and people using other tools built on top of Terraform. Moreover, whatever the tool used I find managing environments and configuration a bit more complicated in Terraform than when you use Pulumi: a lot of documentation and blog posts to read before knowing how to manage environments the proper way.&lt;/p&gt;

&lt;p&gt;One important downside of Terraform concerning environments and configuration is that there is no built-in way of managing secrets. Unless you are a Terraform Cloud customer (in which case your secrets will be stored in workspaces as sensitive variables), you have to find a custom way to keep the secrets in your configuration safe. &lt;a href="https://gruntwork.io/"&gt;Gruntwork&lt;/a&gt;, the company behind Terragrunt has a &lt;a href="https://blog.gruntwork.io/a-comprehensive-guide-to-managing-secrets-in-your-terraform-code-1d586955ace1"&gt;detailed article&lt;/a&gt; about managing secrets in Terraform that I suggest you read. It describes different techniques to keep secrets safe (so not hardcoded in plain text in Terraform code): using environment variables, encrypted files, or a secret store. As you can read in the conclusion of this article (and see in the screenshot of this article below), all these options have trade-offs and require extra work in comparison to Pulumi where secrets encryption is integrated. Moreover, even if you succeed in managing secrets properly in Terraform, they will end up in plain text in Terraform state as we previously mentioned.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/LHUaNAURSjfHpyy9yE1BZb7r8SEO8n4aQhFHIsTE-aM/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X3NlY3VyaXR5XzIu/cG5n" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/LHUaNAURSjfHpyy9yE1BZb7r8SEO8n4aQhFHIsTE-aM/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X3NlY3VyaXR5XzIu/cG5n" alt="" width="880" height="658"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Embedded IaC through an API
&lt;/h3&gt;

&lt;p&gt;To provision infrastructure using Terraform or Pulumi, you can use their respective CLI.&lt;/p&gt;

&lt;p&gt;If you are using Terraform Cloud, you can also perform Terraform runs (called &lt;a href="https://www.terraform.io/cloud-docs/run#terraform-runs-and-remote-operations"&gt;remote operations&lt;/a&gt; in the documentation which are concepts specific to Terraform Cloud) from there by using an API, UI controls or webhooks of your Version Control System (GitHub, GitLab, BitBucket, Azure DevOps, ...). These are useful capabilities but once again these are only available if you are using Terraform SaaS product.&lt;/p&gt;

&lt;p&gt;Now, there are use cases where you would want to provision infrastructure programmatically instead of using a CLI. It can be for instance to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;provide self-service infrastructure to development teams in your organization (through &lt;a href="https://www.pulumi.com/blog/organizational-patterns-developer-portal/"&gt;developer portals&lt;/a&gt; for example)&lt;/li&gt;
&lt;li&gt;integrate provisioning of infrastructure into your platforms and tools (whether it is your custom framework, CLI, or CI/CD workflow)&lt;/li&gt;
&lt;li&gt;automate infrastructure provisioning for your custom needs&lt;/li&gt;
&lt;li&gt;do complex deployments that mix infrastructure and application code such as database migrations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And for that, Pulumi has an &lt;a href="https://www.pulumi.com/automation/"&gt;Automation API&lt;/a&gt; which allows you to build, deploy, and manage infrastructure dynamically from your code thanks to an SDK instead of a CLI. I think it's awesome and unfortunately Terraform does not offer something like that. There would be much to say about Automation API (and that's something that will probably continue to evolve), yet the best is that you check the &lt;a href="https://www.pulumi.com/docs/guides/automation-api/"&gt;documentation&lt;/a&gt; to learn more about it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/LA8S9GEqwp452AWKpd1TOukYUgt5ZQav2YldUgg-dy0/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X2F1dG9tYXRpb25h/cGkucG5n" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/LA8S9GEqwp452AWKpd1TOukYUgt5ZQav2YldUgg-dy0/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X2F1dG9tYXRpb25h/cGkucG5n" alt="" width="880" height="491"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What else?
&lt;/h3&gt;

&lt;p&gt;Pulumi has many more built-in features where it stands out compared to Terraform but I can't cover everything in this blog post. The ones I previously talked about were the main ones in my opinion but you can check the &lt;a href="https://www.pulumi.com/docs/intro/vs/terraform/"&gt;Pulumi vs. Terraform page&lt;/a&gt; in Pulumi's documentation to read about other differences between Pulumi and Terraform. Other interesting capabilities of Pulumi like dynamic providers or testing will be discussed later in this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Providers
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Why providers?
&lt;/h3&gt;

&lt;p&gt;Providers are the reason why I think Terraform and Pulumi have a great future ahead. Instead of focusing on a specific cloud or platform (which is what Cloud Formation, Azure Bicep, or Google Cloud Deployment Manager do), Terraform and Pulumi allow you to provision resources in many different cloud providers and SaaS providers. That does not mean you will write the same infrastructure code to provision cloud resources on AWS and Azure. Of course not, each cloud provider has its specificities and the resources will be different on each platform so the code needs to be different as well. However, instead of having to use multiple IaC platforms (Cloud Formation and Azure Bicep for instance) if you need to provision resources from different clouds in your project, you will only use one (Terraform or Pulumi) with different providers.&lt;/p&gt;

&lt;p&gt;You can see how this works in Pulumi on this schema from the documentation: &lt;a href="https://community.ops.io/images/zXKKxAVWTimsjrZJ7_dRuXUhWPpeRHKhLGYNx-Xovx4/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X2FyY2hpdGVjdHVy/ZS5wbmc" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/zXKKxAVWTimsjrZJ7_dRuXUhWPpeRHKhLGYNx-Xovx4/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X2FyY2hpdGVjdHVy/ZS5wbmc" alt="" width="880" height="619"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Today lots of companies are using multiple clouds to meet their needs and to avoid putting all their eggs in the same basket. And beyond cloud providers, a lot of other SaaS products are used in an organization and you will probably automate the provisioning of resources for these products too to make them available to your teams (whether it is a VCS or a monitoring platform). So being able to provision and manage all these resources from the same tools, using the same concepts and processes is a must-have, and that is what Pulumi and Terraform offer by supporting many providers 👍.&lt;/p&gt;

&lt;h3&gt;
  
  
  How does Pulumi benefit from Terraform ecosystem?
&lt;/h3&gt;

&lt;p&gt;When Pulumi came out in 2018, instead of reinventing the wheel they choose to take advantage of Terraform Providers' mature ecosystem to build most of their own providers. Indeed, Pulumi created tools to adapt/bridge any existing Terraform provider.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/Zuad5-RKu7xuZ3_NEPe-OGDBX15IPkp_lAK0httLgpc/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X2JyaWRnZS5wbmc" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/Zuad5-RKu7xuZ3_NEPe-OGDBX15IPkp_lAK0httLgpc/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X2JyaWRnZS5wbmc" alt="" width="880" height="348"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To understand what it means, we have to talk about what is exactly a provider. According to Terraform's documentation, "providers are a logical abstraction of an upstream API. They are responsible for understanding API interactions and exposing resources". That means a provider defines a schema describing the resources available on a cloud provider API, and all the mappings (parameters, models, responses ...) needed to interact with this API. Instead of doing the same job of mapping everything, a Pulumi provider that is created by "bridging" a Terraform provider simply reuses the same schema, that's it. But, Pulumi itself does not use Terraform, they have a completely different engine.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you are looking for a better explanation about how Pulumi "bridges" Terraform providers you can look at this &lt;a href="https://www.leebriggs.co.uk/blog/2021/11/06/pulumi-faqs.html#doesnt-pulumi-use-terraform-under-the-hood-"&gt;article&lt;/a&gt; from Lee Briggs who work at Pulumi.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Why do I talk about all this? It's because I think it's great for Pulumi to be able to benefit from Terraform Providers ecosystem. Thanks to this, like Terraform, Pulumi supports lots of cloud providers and modern cloud SaaS offerings. And in the event there were a provider available in Terraform and not in Pulumi, it would always be possible for anyone to create a Pulumi provider out of this Terraform provider thanks to the &lt;a href="https://github.com/pulumi/pulumi-terraform-bridge"&gt;Pulumi Terraform Bridge&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;By the way, the fact that some Pulumi providers are created by adapting Terraform providers is completely assumed by Pulumi as you can read in &lt;a href="https://www.pulumi.com/docs/intro/vs/terraform/#providers-terraform"&gt;Pulumi documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/Ip4N6jv0iIxi1b6qQFEEjgJtMe5eM05yI5X0yfYBYQY/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X3Byb3ZpZGVyc18x/LnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/Ip4N6jv0iIxi1b6qQFEEjgJtMe5eM05yI5X0yfYBYQY/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X3Byb3ZpZGVyc18x/LnBuZw" alt="" width="880" height="232"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  The problem with Terraform providers
&lt;/h3&gt;

&lt;p&gt;We have seen that Pulumi benefits from Terraform providers ecosystem. However, there is a major problem with Terraform providers (and so with corresponding Pulumi providers as well): they are implemented manually.&lt;/p&gt;

&lt;p&gt;It has the following inconveniences for a provider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it is more likely to contain bugs&lt;/li&gt;
&lt;li&gt;it doesn't have full coverage of its corresponding API&lt;/li&gt;
&lt;li&gt;it is always a little behind an API because it takes time for new resources or new features to be added&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the beginning of the article I explain that I wanted my IaC solution should be &lt;em&gt;up to date with main cloud providers' resources and features&lt;/em&gt; and that is not the case with Terraform providers even if the community (Pulumi included) is doing a great job at contributing to Terraform providers to make new resources available.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/xeGZ65PBMFzVaK_NlR2HtlKkI96Dghhn_s703sBaeUY/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X2Nsb2Nrcy5qcGc" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/xeGZ65PBMFzVaK_NlR2HtlKkI96Dghhn_s703sBaeUY/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X2Nsb2Nrcy5qcGc" alt="" width="640" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Moreover, a version of a provider matches a version of the resources in the API. So you can't have 2 resources from different API versions coexist. For instance, let's say you need to use an old version of the Terraform provider for Azure because they are a lot of breaking changes in the latest version of the provider. You don't want to handle these changes yet as it involves some big changes in some of your resources. But in the same time, you want to use a resource that is only available in the latest version of the provider. Well in this case it's going to be complicated for you. The issue is that with Terraform providers you can't be very flexible with your resources and customize everything you want&lt;/p&gt;

&lt;h3&gt;
  
  
  What are Pulumi native providers?
&lt;/h3&gt;

&lt;p&gt;In order to solve the problem described in the previous section, Pulumi introduced the concept of &lt;a href="https://www.pulumi.com/blog/pulumiup-native-providers/"&gt;native providers&lt;/a&gt; for the providers Microsoft Azure, AWS, Google Cloud Platform, and Kubernetes. These providers are automatically built every day from the cloud providers' APIs 🎉. It has the following advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;100% API coverage, so all resources available including the ones in preview&lt;/li&gt;
&lt;li&gt;Providers are always up-to-date with the APIs&lt;/li&gt;
&lt;li&gt;Access to all the versions of the APIs so that resources from different API versions can coexist in a project (as you would have with a cloud provider native solution like Azure Bicep)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Honestly, these are good enough reasons to choose Pulumi over Terraform. If you are developing cloud applications, you don't want to be limited to what you can do by your IaC solution, especially when it concerns the major cloud providers you are probably using&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;It's worth noting that people from Microsoft seem also concerned about these limitations of the Terraform Azure RM provider as they announced a new Azure provider &lt;a href="https://techcommunity.microsoft.com/t5/azure-tools-blog/announcing-azure-terrafy-and-azapi-terraform-provider-previews/ba-p/3270937"&gt;AzAPI&lt;/a&gt;, built on top of the Azure ARM APIs that can be used to have access to Azure features and services in preview. However, it seems that for other resources, Microsoft expects people to continue using the existing Terraform provider with its limitations.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I think generating the providers from the APIs is the right way of doing things. Unfortunately, it's only possible to create native providers for a cloud provider or a SaaS provider if its API exposes the necessary mapping. But if more APIs do that, more Pulumi native providers will probably be added in the future.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/XLkxT0WPTR9tPqtWsUosD-xSTPom6bBEw_E2gk6qzO4/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X3Byb3ZpZGVyc18y/LnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/XLkxT0WPTR9tPqtWsUosD-xSTPom6bBEw_E2gk6qzO4/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X3Byb3ZpZGVyc18y/LnBuZw" alt="" width="880" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Another thing I appreciate a lot with these native providers: you work with the same "models" as the ones of the corresponding cloud. On the contrary, because a Terraform provider is hand-coded you will have a more or less thin abstraction layer and potential differences between the models. That's not a big deal but can sometimes complicate things because you have to understand how the Terraform models map to the cloud API models.&lt;/p&gt;

&lt;h3&gt;
  
  
  More about providers
&lt;/h3&gt;

&lt;p&gt;If you have looked at &lt;a href="https://registry.terraform.io/browse/providers"&gt;Terraform registry&lt;/a&gt;, you may have seen that there is a huge amount of community providers. Does it mean Terraform is better than Pulumi? I don't think so: quantity does not mean quality. That's not me saying that there are many bad quality Terraform providers, not at all. That's me saying that there are probably lots of providers you don't care about. For instance, I assume you don't care that there is a Terraform provider to order a Domino's Pizza 🍕. Okay, that may be fun, but that's it. And concerning the more serious providers, a lot of them are just there to overcome the inherent limitations of HCL (not being a programming language) such as not being able to make an HTTP call without using a provider. You don't have these limitations with Pulumi where you use programming languages and their libraries.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/IDRJcRkju_0e4MJtNKcgSRLf8El7Yg3947E67hAMLtw/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X3Rvb2xzXzEuanBn" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/IDRJcRkju_0e4MJtNKcgSRLf8El7Yg3947E67hAMLtw/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X3Rvb2xzXzEuanBn" alt="" width="640" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We will talk more about that in the next section but being able to use a programming language and all its ecosystem is a true advantage of Pulumi over Terraform. There are some times when you need to provision some cloud resources and there is no provider to manage them (whether it be Pulumi or Terraform). If you are using Terraform you will have to wait for the community to implement this provider, if it ever really happens. If you are using Pulumi, you are using programming languages and therefore can implement anything you need (by using SDKs or making the API calls yourself). And I am not talking about implementing yourself a whole provider covering a complete API (which would require some time), but just implementing the resources you need.&lt;/p&gt;

&lt;p&gt;For that, Pulumi has a concept of &lt;a href="https://www.pulumi.com/docs/intro/concepts/resources/dynamic-providers/"&gt;Dynamic Providers&lt;/a&gt; with which you implement the different CRUD operations for a resource so that you are still doing declarative infrastructure as code but with custom logic. It's like implementing on the fly a custom provider specific to your needs directly in your project. Dynamic providers' usage is not limited to supporting a cloud provider that does not yet exist in Pulumi, it is also to do any infrastructure task that no existing provider can help deliver (see examples in this &lt;a href="https://www.pulumi.com/blog/dynamic-providers/#sample-use-cases"&gt;article&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Using programming languages: the best way to do IaC
&lt;/h2&gt;

&lt;p&gt;Pulumi's approach is to use programming languages to write your infrastructure code. On the opposite, Terraform's approach is to uses a DSL called HCL (that stands for Hashicorp Configuration Language). It's worth reminding that both tools are declarative, even when using an imperative language.&lt;/p&gt;

&lt;p&gt;When comparing IaC, the big debate ⚡ is often: show we use Domain Specific Languages (DSL) or programming languages? If you have read the previous sections of this article you know this is just one of many questions, there are other important aspects to consider. Nevertheless, it's an important question because there is more to it than simply a question of personal preference. In my opinion, using programming languages to write your infrastructure code is the right way of doing things and is the future of infrastructure as code. Let me explain to you why and how it is particularly great to use programming languages with Pulumi.&lt;/p&gt;

&lt;h3&gt;
  
  
  Better aligned with the DevOps culture
&lt;/h3&gt;

&lt;p&gt;DevOps aims at continuously delivering value to end-users by removing the barrier between software development and IT operations. As one of the practices of DevOps, Infrastructure as Code should help bring closer people from Development teams and people from Operations teams.&lt;/p&gt;

&lt;p&gt;Therefore I don't think using different languages, tools, and practices for the application development and the infrastructure development is the right approach. And yet, this is what you do when you write your infrastructure code in HCL while the application code is written in TypeScript or C# for instance. Unfortunately, in some companies, I think using Terraform reinforces the Development and Operations silos more than anything else. It's difficult to deliver value to a customer if that means for a developer to create a ticket to the Operations team just to add a new setting in a web app because only them have access and knowledge of the Terraform code 😿.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/8nyG896VD7o9rbz3Gf1HsdrKkToW8NLg9WrMUaLUeMQ/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X3RvZ2V0aGVyLmpw/Zw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/8nyG896VD7o9rbz3Gf1HsdrKkToW8NLg9WrMUaLUeMQ/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X3RvZ2V0aGVyLmpw/Zw" alt="" width="640" height="512"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I truly believe that the proper way of adopting DevOps practices is to have multidisciplinary self-organizing teams. And what a better way to make software developers, IT operations people, security engineers, ... collaborate in such teams than by making them "speak" the same language, use the same tools, and adopt the same engineering practices. That's what using programming languages for the infrastructure code is about and that's what Pulumi is about.&lt;/p&gt;

&lt;h3&gt;
  
  
  Be productive faster and more
&lt;/h3&gt;

&lt;p&gt;Whatever the IaC solution you choose, you will have to learn new concepts that may not be obvious. Yet, writing the infrastructure code will be easy to learn if you are using a programming language you already know. On the contrary, if you use a DSL, it will add another thing for you to learn (especially HCL that I don't find very intuitive).&lt;/p&gt;

&lt;p&gt;Using Pulumi you will not only be able to use the programming language you know but also your favorite IDEs, as well as the libraries and the tools you are familiar with 🛠️. The programming languages supported by Pulumi (TypeScript/JavaScript, Python, Go, .NET languages, Java languages, and probably more to come) are used by lots of people for software development. Therefore IDE support (typing, static analysis, code completion....) is already great and tooling in general better than the tooling for an IaC DSL could ever be.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/rfaFBxZgK-yRRp3gwn9ant8b5QPoQOrO--otrd4K07c/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X2V2ZXlvbmVjYW5j/b2RlLmpwZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/rfaFBxZgK-yRRp3gwn9ant8b5QPoQOrO--otrd4K07c/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X2V2ZXlvbmVjYW5j/b2RlLmpwZw" alt="" width="640" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;So in addition to learning faster IaC using programming languages you will be more productive too. That is also true for people not coming from development but from ops, they probably already have some scripting knowledge (Bash, PowerShell, Python...), that's why programming languages can be a good fit for them as well.&lt;/p&gt;

&lt;p&gt;Making people quickly productive on an IaC solution is important because as everywhere in IT, in IaC field it's hard to find skilled people to recruit so it's sometimes easier to train people already in your company. Speaking of that, I hear sometimes people saying that: as Terraform is currently more used than Pulumi (due to Pulumi being more recent), it will be easier to hire people who know Terraform than people who know Pulumi. Yes, that's true, but it will be much harder to find people who know HCL than people who know TypeScript, Python, .NET, or Go.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Independently of HCL, I find Terraform to be more complex to learn than Pulumi, with a lot of different concepts to grasp (variables, local values, data sources, configuration, workspaces, modules) that are unique to Terraform. Pulumi felt much easier. Of course, learning is unique to each individual so you may have a different opinion but this has been my experience.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  A better developer experience
&lt;/h3&gt;

&lt;p&gt;The developer experience is so much better when using programming languages instead of a DSL.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you are not familiar with the term Developer Experience or DX, it's like User Experience but for software engineers using digital products (developer tools, IT solutions, platforms...).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I already talked about the better code completion of Pulumi which is great to know which resource you can create, what properties can be set, which ones are required, and what are the possible values.&lt;/p&gt;

&lt;p&gt;There is also the fact that you can debug infrastructure code because it's just code in a program. With Automation API, Pulumi can be used from anywhere (a console application, a custom CLI, a web application, ...) and easily debugged. Good luck debugging Terraform code 😉. You even have something similar to hot reload for infrastructure with pulumi watch command (see &lt;a href="https://www.techwatching.dev/posts/pulumi-watch"&gt;my article&lt;/a&gt; on the topic).&lt;/p&gt;

&lt;p&gt;Another topic is testing. You can only do unit testing (understand the testing of components without deploying real infrastructure) with code written in a programming language. Pulumi allows you to write unit tests by mocking the external dependencies using your usual test and mock frameworks. Integration testing/end-to-end testing however can be done with both Pulumi and Terraform. If you do a bit of search, you will find out that there are several frameworks to write end-to-end tests for Terraform and they require you to write your tests in Go or Ruby (so finally using a programming language after all 😀). You can read more about testing with Pulumi in the &lt;a href="https://www.pulumi.com/docs/guides/testing/"&gt;documentation&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  More capabilities
&lt;/h3&gt;

&lt;p&gt;Using a programming language you are not limited to what providers or modules can do. You have the power of a programming language to implement what you need (for example using the dynamic providers I talked about before). Some people are afraid of that, I don't think they should. Programming languages have static analysis mechanisms to ensure best practices are respected in the code. Pulumi also provides policy as code to let you write the rules you want your infrastructure to follow (it could be rules about security or cost for instance).&lt;/p&gt;

&lt;p&gt;Terraform has a great community that creates packages and tools for Terraform. As Pulumi is younger, the ecosystem is not as developed as Terraform's. However, when you use Pulumi you use programming languages that have a huge ecosystem from which you can benefit ❤️.&lt;/p&gt;

&lt;h3&gt;
  
  
  The future
&lt;/h3&gt;

&lt;p&gt;I have given several reasons why I think programming languages is the best way to do IaC. Today, the majority of people are using DSL based on YAML or JSON to write infrastructure code but things are changing at a great speed. Following the example of Pulumi, there are more and more IaC tools that support programming languages to write infrastructure code, &lt;a href="https://aws.amazon.com/cdk/"&gt;AWS CDK&lt;/a&gt; for instance. Even Terraform itself is starting to see the limits of HCL and has launched its CDK in beta. Unfortunately at the time of writing it's still in preview since July 2020, has some limitations, and is not yet mature according to the &lt;a href="https://www.terraform.io/cdktf#project-maturity"&gt;documentation&lt;/a&gt;. Pulumi has a clear head start but it's very interesting to see the whole IaC ecosystem evolving in this direction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;To sum up, I will choose Pulumi over Terraform for my next project because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it has 100% resource coverage of my cloud provider and more flexibility&lt;/li&gt;
&lt;li&gt;it allows me to be more productive using languages, tools, and libraries I am already familiar with&lt;/li&gt;
&lt;li&gt;it has more built-in functionalities and is more modern&lt;/li&gt;
&lt;li&gt;it is easier to learn and use (something that my teammates will appreciate)&lt;/li&gt;
&lt;li&gt;it is secure by default
&lt;img src="https://community.ops.io/images/SNdj3qOWppf4vbRjGG332jlryGorUtql_anf5Sziq_4/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWl2c3Rm/X2NoZXNzLmpwZw" alt="" width="640" height="427"&gt;
### Disclaimers&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Just like any other article on my blog, "the opinions expressed herein are my own and do not represent those of my employer or any other third-party views in any way".&lt;/p&gt;

&lt;p&gt;This article is not sponsored, I don't own any shares in Pulumi (unfortunately 😉), and I don't work for Pulumi. That means the article just reflects my point of view, the point of view of a cloud developer interested in infrastructure who has worked with both Terraform and Pulumi, and much prefers Pulumi.&lt;/p&gt;

&lt;p&gt;Even if I like Pulumi a lot as it is very nice working with it, I have tried to remain as objective as possible in this article. You will probably not agree with everything I said, but I hope you will understand what are my reasons for choosing Pulumi for my Infrastructure as Code and that it will help you with your choice, whatever it is.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Terraform bad?
&lt;/h3&gt;

&lt;p&gt;Not at all. I would have a hard time saying Terraform is bad and Pulumi is awesome when they rely on similar concepts like declarative infrastructure as code, state file to store the current state of the infrastructure, and providers to support multiple cloud providers and services. In fact, I think Terraform is a great Infrastructure as Code solution, just not the best one in my opinion.&lt;/p&gt;

&lt;p&gt;I think Terraform has truly revolutionized infrastructure as code when it came out in 2014. At that time, cloud providers native solutions were too verbose and too complex, and there was no true alternative to them. Terraform came to simplify that and has enabled people to use the same IaC solution to deploy resources of different cloud providers. Today, things have changed: cloud providers' native solutions have evolved, there are new modern alternatives to Terraform (and I am not talking only about Pulumi), and Terraform has shown some limitations.&lt;/p&gt;

&lt;p&gt;Because Terraform is used in many companies, benefits from a rich ecosystem, and has a great community, you will probably not make a big mistake if you go with Terraform instead of Pulumi for your next project. However, if you do so, you have to be aware of the limitations and weaknesses of Terraform that I talked about in this article. This is something very important because each project has its needs and constraints, so maybe Terraform's limitations are not a big deal for your use case, maybe it is. Just make sure which one it is.&lt;/p&gt;

&lt;p&gt;Despite all I said, there is one thing I like very much about Terraform, it's its community. Because many people have been using it, and for some years now, there are a lot of examples on the internet and many interesting tools or frameworks have been created around it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Is Pulumi perfect?
&lt;/h3&gt;

&lt;p&gt;No Pulumi is not perfect, no IaC solution is, there are always things to improve.&lt;/p&gt;

&lt;p&gt;For instance, a few Pulumi features are not yet available in .NET like Dynamic providers or policy as code (not a real problem as you can write them in TypeScript or Python even if your stack is in .NET).&lt;/p&gt;

&lt;p&gt;I also hope more native providers will be created (even if I know it's not only up to Pulumi) because I think native providers are a game-changer. In particular, I would want to have a native provider for Microsoft Graph (you can vote for the issue &lt;a href="https://github.com/pulumi/pulumi/issues/8963"&gt;here&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;More generally, I think Infrastructure as Code is evolving quickly and there are many things yet to come. But I am confident Pulumi will continue to bring more features and innovations to this space 🚀.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which Infrastructure as Code solution should you choose?
&lt;/h3&gt;

&lt;p&gt;To be honest, I can't think of any good reason why I would choose Terraform over Pulumi for a project. But that's just me.&lt;/p&gt;

&lt;p&gt;If you have already invested a lot of time learning HCL/Terraform and are happy with it, you should probably continue using Terraform.&lt;/p&gt;

&lt;p&gt;If you don't know either Terraform or Pulumi, I would suggest you use Pulumi even if you don't know any programming language. It's in my opinion the right way to do infrastructure as code. Furthermore, it is better to learn a programming language that could be useful somewhere else than HCL which you will only use in HashiCorp products.&lt;/p&gt;

&lt;p&gt;I would like to emphasize one thing: &lt;strong&gt;when choosing an IaC solution, make an informed decision.&lt;/strong&gt; Do not choose Terraform because your favorite influencer promotes it. Do not choose Terraform because most people around you use it. The same is true for Pulumi. Do not choose Pulumi just because I say you should. Read articles from people with other points of view. Check what topics are important for you, and compare both solutions on these topics. Make up your own mind.&lt;/p&gt;

&lt;p&gt;In the end, you should choose the solution you feel is more appropriate to you and your project, and that you think will be easier for you to learn and use in the long term. If it's Terraform then go with it! If it's Pulumi, welcome to modern Infrastructure as Code!&lt;/p&gt;

</description>
      <category>pulumi</category>
      <category>devops</category>
      <category>iac</category>
      <category>terraform</category>
    </item>
    <item>
      <title>When Pulumi met Nuke: a .NET love story</title>
      <dc:creator>Alexandre Nédélec</dc:creator>
      <pubDate>Sun, 18 Dec 2022 00:00:00 +0000</pubDate>
      <link>https://community.ops.io/techwatching/when-pulumi-met-nuke-a-net-love-story-2ief</link>
      <guid>https://community.ops.io/techwatching/when-pulumi-met-nuke-a-net-love-story-2ief</guid>
      <description>&lt;p&gt;Today is a great time to be a developer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;there are plenty of languages and frameworks to choose from to build an application&lt;/li&gt;
&lt;li&gt;there are very powerful IDEs and tools to help us write, analyze, refactor, test and debug code&lt;/li&gt;
&lt;li&gt;there are many nice CI/CD platforms that allow us to package and deploy our applications anywhere&lt;/li&gt;
&lt;li&gt;thanks to cloud platforms and infrastructure as code we can provision infrastructure on-demand in an automated way&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Yet, sometimes it seems quite complex and time-consuming to deploy an application in the cloud.&lt;/p&gt;

&lt;p&gt;As a .NET developer, do I really need to master YAML, and Domain Specific Languages like HCL to deploy a simple ASP.NET Core API in Azure? Should I forget about local debugging when developing CI/CD pipelines? Do I have to learn everything from scratch each time I use another CI/CD platform?&lt;/p&gt;

&lt;p&gt;Thanks to Nuke and Pulumi, I don't think so and that is what we are going to talk about in this article.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scenario
&lt;/h2&gt;

&lt;p&gt;They are already lots of great articles about Pulumi or Nuke, so I won't spend time explaining what they are and why you should use them. Instead, I will show you how you can use them together with an example.&lt;/p&gt;

&lt;p&gt;My scenario is the following: I have a very basic ASP.NET Core API that I want to deploy to Azure App Service using a CI/CD pipeline.&lt;/p&gt;

&lt;p&gt;To do that, I want to use my existing .NET skills and code everything with the language and tools I know and love.&lt;/p&gt;

&lt;h2&gt;
  
  
  Steps of the CI/CD pipeline
&lt;/h2&gt;

&lt;p&gt;There are often two main steps (or stages or whatever you call them) in a CI / CD pipeline: the packaging and the deployment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/QcdZhZMUJbCdu-bhXqwNNIrcVG2VUNxKquQ3SOmxWhc/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfMS5wbmc" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/QcdZhZMUJbCdu-bhXqwNNIrcVG2VUNxKquQ3SOmxWhc/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfMS5wbmc" alt="" width="370" height="137"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To package a .NET application, we have to first restore the dependencies, then compile the application and publish it. So my Package step is composed of 3 steps.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💬 A &lt;a href="https://learn.microsoft.com/en-us/dotnet/core/tools/dotnet-publish#description"&gt;&lt;code&gt;dotnet publish&lt;/code&gt;&lt;/a&gt; does an implicit restore and build the application so only one step could be used but I like separating these steps for clarity. Moreover it is sometimes needed, for instance when you are restoring packages from private Nuget feeds.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/TbdtZEue0lO7JpVXdChzlSTdAN75luXbIGxN5yPOyG8/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfMi5wbmc" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/TbdtZEue0lO7JpVXdChzlSTdAN75luXbIGxN5yPOyG8/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfMi5wbmc" alt="" width="648" height="159"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I said the application needed to be deployed to Azure App Service but I don't have an existing Azure App Service resource, and I don't want to manually create one. So I also need a step to deploy the infrastructure&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/Pe8xjBZhFaBrL_z6alEvvdEr37VEteC1RJ8f8J0DM-4/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfMy5wbmc" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/Pe8xjBZhFaBrL_z6alEvvdEr37VEteC1RJ8f8J0DM-4/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfMy5wbmc" alt="" width="877" height="346"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It seems fine. I will just add another optional step at the beginning to clean the temporary files I could have created on previous builds.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If your pipeline runs on a hosted agent/runner (managed by the CI/CD platform you use), the Clean step might not be very useful but I intend to also run this pipeline locally.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/kuw5dJ-9HZafjFWpeeYihLkPa2qfjHeaJZ-gWAWwm90/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfNC5wbmc" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/kuw5dJ-9HZafjFWpeeYihLkPa2qfjHeaJZ-gWAWwm90/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfNC5wbmc" alt="" width="880" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, that we know the different steps of our pipeline, let's get to the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started with the code
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Code organization
&lt;/h3&gt;

&lt;p&gt;I put all the code in the same Git repository because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it makes sense as everything is linked&lt;/li&gt;
&lt;li&gt;it's easier to maintain (all the code in one place)&lt;/li&gt;
&lt;li&gt;it's easier to version (one tag on one commit in one repository)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I chose to organize my repository with the following folders:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;📁 src ➡️ for the application code of the API&lt;/li&gt;
&lt;li&gt;📁 infrastructure ➡️ for the infrastructure code that provisions the App Service&lt;/li&gt;
&lt;li&gt;📁 build ➡️ for pipeline code that builds and deploys the application&lt;/li&gt;
&lt;li&gt;📁 artifacts ➡️ for the package created by the pipeline&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Create the C# projects
&lt;/h3&gt;

&lt;p&gt;To create the API project, we just use the default ASP.NET Core API template in .NET 7 that creates a simple Weather API.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/4Y7BZ19nB2JUUDKnfWOse1wd8lmFwKZYNYeRDHkESTM/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfNS5wbmc" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/4Y7BZ19nB2JUUDKnfWOse1wd8lmFwKZYNYeRDHkESTM/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfNS5wbmc" alt="" width="302" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I can initialize the infrastructure project using the Pulumi CLI new command with the azure C# template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;pulumi&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;azure-csharp&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://community.ops.io/images/M8gsOadLdomSibKFu5qV1KGDEq2c6wCQNq5jH-EN7n8/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfNi5wbmc" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/M8gsOadLdomSibKFu5qV1KGDEq2c6wCQNq5jH-EN7n8/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfNi5wbmc" alt="" width="282" height="198"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I will show later how to modify the code of the template to provision an App Service.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You can check Pulumi &lt;a href="https://www.pulumi.com/docs/get-started/azure/"&gt;Getting Started with Azure&lt;/a&gt; tutorial to see how to set up your environment and start creating Azure resources in C# (or in another language).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To initialize the build project, we can use Nuke's .NET global tool as explained in the &lt;a href="%5BBuild%20Setup%20%7C%20NUKE%5D(https://nuke.build/docs/getting-started/setup/)"&gt;documentation&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;nuke&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nx"&gt;setup&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://community.ops.io/images/w6H-gjRTmcgxObFUoXUjldcdlX3pDDP5h5uoX04CEcE/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfNy5wbmc" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/w6H-gjRTmcgxObFUoXUjldcdlX3pDDP5h5uoX04CEcE/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfNy5wbmc" alt="" width="310" height="242"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Everything in .NET
&lt;/h3&gt;

&lt;p&gt;What I like about using Pulumi (in .NET) and Nuke is that all the code is just C# code. My infrastructure project and my build project are standard .NET console applications. And I can open the 3 projects (API, infrastructure, and build) in the same solution in my preferred IDE.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/4S9n-oUWjerHSDw7HAPh2vv5rKr3giZpG2FJYtCxIhc/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfOC5wbmc" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/4S9n-oUWjerHSDw7HAPh2vv5rKr3giZpG2FJYtCxIhc/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfOC5wbmc" alt="" width="318" height="636"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Why does it matter? Because any .NET developer in a team would be able to understand and maintain this code. How many times have you seen a project slow down because the person responsible for the infrastructure code written in YAML, JSON, Bicep, or HCL was on vacation or ill? How often have you been stuck because the only few people in the team that knew how to modify the YAML pipelines were not available?&lt;/p&gt;

&lt;p&gt;But it's not a question of knowledge only. It's also because the developer experience of writing build or infrastructure code in .NET is much better than writing code in YAML or other declarative "languages".&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I talk a lot about the benefits of using programming languages for infrastructure code in my article "&lt;a href="https://www.techwatching.dev/posts/pulumi-vs-terraform"&gt;Why will I choose Pulumi over Terraform for my next project?&lt;/a&gt;" if you have not read it yet.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Implementing the pipeline steps from Clean to Publish
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Nuke pipeline
&lt;/h3&gt;

&lt;p&gt;Here is what looks like the default build project after its creation:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/cXuPX0JmcXin75vQ3U33P3Dw6NsX98gT4VXcjy1KuXI/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfOS5wbmc" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/cXuPX0JmcXin75vQ3U33P3Dw6NsX98gT4VXcjy1KuXI/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfOS5wbmc" alt="" width="880" height="741"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The main method is contained in a &lt;code&gt;Build.cs&lt;/code&gt; file. This file contains the steps of the pipeline that are called &lt;a href="https://nuke.build/docs/fundamentals/targets/"&gt;Target&lt;/a&gt; in Nuke. We can set the dependencies between targets.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡The build project is a .NET console application so it works out of the box in any .NET IDE or from the command line. But to be more productive you can install a plugin for your IDE that will add snippets and a way to easily debug each target individually.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As you see we can define properties with the attribute Parameter if we need to pass parameters to our pipeline, like the Configuration parameter.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Clean target
&lt;/h3&gt;

&lt;p&gt;We can define the Clean target like that:&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="n"&gt;Target&lt;/span&gt; &lt;span class="n"&gt;Clean&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;  
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Before&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Restore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Executes&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;  
    &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="n"&gt;SourceDirectory&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GlobDirectories&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"*/bin"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"*/obj"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;ForEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DeleteDirectory&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
        &lt;span class="nf"&gt;EnsureCleanDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ArtifactsDirectory&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;This code deletes all the bin and obj directories of the source directory. It also deletes the content in the artifacts directory. Nuke overloads the division operator to allow us to easily define paths in the project.&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="n"&gt;AbsolutePath&lt;/span&gt; &lt;span class="n"&gt;SourceDirectory&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;RootDirectory&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="s"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

&lt;span class="n"&gt;AbsolutePath&lt;/span&gt; &lt;span class="n"&gt;InfrastructureDirectory&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;RootDirectory&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="s"&gt;"infra"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  

&lt;span class="n"&gt;AbsolutePath&lt;/span&gt; &lt;span class="n"&gt;ArtifactsDirectory&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;RootDirectory&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="s"&gt;"artifacts"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Restore target
&lt;/h3&gt;

&lt;p&gt;To restore .NET dependencies, we can use the &lt;code&gt;dotnet restore&lt;/code&gt; command. Nuke supports &lt;a href="https://nuke.build/docs/common/cli-tools/"&gt;executing CLI tools&lt;/a&gt; and has even auto-generated CLI wrappers for some common tools like dotnet CLI to use a Fluent API instead of string interpolation to pass parameters.&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="n"&gt;Target&lt;/span&gt; &lt;span class="n"&gt;Restore&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;  
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Executes&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;  
    &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="nf"&gt;DotNetRestore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetProjectFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Solution&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;
  
  
  The Compile target
&lt;/h3&gt;

&lt;p&gt;The compile target uses the &lt;code&gt;dotnet build&lt;/code&gt; command. We can start to see the benefits of using this Fluent API that provides us with autocompletion and documentation. For instance, as we already restored the dependencies in the previous step, we can set the &lt;code&gt;--no-restore&lt;/code&gt; option using the &lt;code&gt;EnableNoRestore&lt;/code&gt; auto-generated method.&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="n"&gt;Target&lt;/span&gt; &lt;span class="n"&gt;Compile&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;  
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DependsOn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Restore&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Executes&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;  
    &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="nf"&gt;DotNetBuild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;  
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetProjectFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Solution&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnableNoRestore&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;
  
  
  The Publish target
&lt;/h3&gt;

&lt;p&gt;The publish target uses the &lt;code&gt;dotnet publish&lt;/code&gt; command and then creates a zip &lt;code&gt;api.zip&lt;/code&gt; of the resulting package in the artifacts directory.&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="n"&gt;Target&lt;/span&gt; &lt;span class="n"&gt;Publish&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;  
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DependsOn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Compile&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Executes&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;  
    &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="nf"&gt;DotNetPublish&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;  
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetProject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Solution&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CSharpEverything_Api&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetConfiguration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnableNoBuild&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ApiPackageDirectory&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;  

        &lt;span class="n"&gt;ZipFile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateFromDirectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ApiPackageDirectory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ArtifactsDirectory&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="s"&gt;"api.zip"&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;blockquote&gt;
&lt;p&gt;💡If you need more compressing archives options, you can check &lt;a href="https://nuke.build/docs/common/compression/#compressing-archives"&gt;Nuke documentation&lt;/a&gt;, they have some utilities to do more complex scenarios.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You may have noted on the line where I set the project that I have &lt;a href="https://nuke.build/docs/common/solution-project-model/#strong-typed-project-access"&gt;strong-typed access to the projects in my solutions&lt;/a&gt;. This is possible by adding this field with the &lt;code&gt;Solution&lt;/code&gt; attribute and its &lt;code&gt;GenerateProjects&lt;/code&gt; property set to true.&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;Solution&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GenerateProjects&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;  
&lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;Solution&lt;/span&gt; &lt;span class="n"&gt;Solution&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;🪄 It looks like magic but it's not! Nuke uses a &lt;a href="https://learn.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview"&gt;source generator&lt;/a&gt; to do that behind the scenes.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Provisioning the App Service with Pulumi
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Pulumi project
&lt;/h3&gt;

&lt;p&gt;By default, the infrastructure code is contained in the &lt;code&gt;Program.cs&lt;/code&gt; file of our project. The resources to provision are declared in the lambda in parameter of the &lt;code&gt;Deployment.RunAsync&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/ouOCBMLs8brr-SXGteSrwa_FSE2iQODlOsiIzsJ1biQ/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfMTAucG5n" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/ouOCBMLs8brr-SXGteSrwa_FSE2iQODlOsiIzsJ1biQ/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfMTAucG5n" alt="" width="880" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💬The project uses the top-level statement feature of C#.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;As we don't have many resources to declare for our scenario we will keep all the code in the &lt;code&gt;Program.cs&lt;/code&gt; file but that is not what you would do in a more complex project.&lt;/p&gt;

&lt;h3&gt;
  
  
  Azure resources to provision
&lt;/h3&gt;

&lt;p&gt;There are 3 Azure resources we need to create in our stack (instance of a Pulumi program):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a resource group to contain the different Azure resources&lt;/li&gt;
&lt;li&gt;an App Service Plan which &lt;a href="https://learn.microsoft.com/en-us/azure/app-service/overview-hosting-plans"&gt;defines the set of compute resources for a web app to run&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;a Web App / App Service which is where the API will be deployed
&lt;/li&gt;
&lt;/ul&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;resourceGroup&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ResourceGroup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"rg-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Deployment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProjectName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Deployment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StackName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&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;appServicePlan&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AppServicePlan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"plan-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Deployment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProjectName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Deployment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StackName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;AppServicePlanArgs&lt;/span&gt;  
&lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="n"&gt;ResourceGroupName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resourceGroup&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;Kind&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"App"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
    &lt;span class="n"&gt;Sku&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;SkuDescriptionArgs&lt;/span&gt;  
    &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="n"&gt;Tier&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"Basic"&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="s"&gt;"B1"&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;appService&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;WebApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"app-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Deployment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProjectName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Deployment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StackName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;WebAppArgs&lt;/span&gt;   
&lt;span class="p"&gt;{&lt;/span&gt;   
    &lt;span class="n"&gt;ResourceGroupName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resourceGroup&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;ServerFarmId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;appServicePlan&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;The code is quite simple, and because we are writing C# in our IDE, we have autocompletion and everything we need to make writing the infrastructure code easier.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡If you are used to Azure Bicep or ARM templates, the names of the classes or properties will look familiar to you. It's because we are using Azure Native, which is a Pulumi native provider that is generated from Azure APIs.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Stack outputs
&lt;/h3&gt;

&lt;p&gt;Provisioning the cloud resources we need is great we have to think about the next step which is to deploy our API on these resources. So what will we need for that?&lt;/p&gt;

&lt;p&gt;First, we will need to have the name of the provisioned App Service. That's easy it's the property Name of the &lt;code&gt;appService&lt;/code&gt; variable.&lt;/p&gt;

&lt;p&gt;Second, because we are going to use the Kudu API to zip deploy our application to the App Service, we will need the &lt;a href="https://github.com/projectkudu/kudu/wiki/Deployment-credentials#site-credentials-aka-publish-profile-credentials"&gt;site credentials (aka the Publishing Profile Credentials)&lt;/a&gt;. These can be retrieved in the Pulumi program using the following code:&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="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;publishingCredentials&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ListWebAppPublishingCredentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Invoke&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="n"&gt;ResourceGroupName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resourceGroup&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="n"&gt;appService&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;💬 Using the Kudu API is just one of the &lt;a href="https://learn.microsoft.com/en-us/azure/app-service/deploy-zip?tabs=cli#deploy-a-zip-package"&gt;many ways&lt;/a&gt; to deploy a zip package to an App Service. I could have chosen another way like using the Azure CLI, in that case retrieving the site credentials would not have been needed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Pulumi, like Terraform, has this concept of stack &lt;a href="https://www.pulumi.com/docs/intro/concepts/stack/#outputs"&gt;output&lt;/a&gt; where outputs are information about your stack/infrastructure that you want to expose. That is exactly what we need to export our App Service name and our site credentials so that they can be retrieved later by the Nuke code that will take care of the application deployment. To export these values we can return them in a Dictionary like that:&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;object&lt;/span&gt;&lt;span class="p"&gt;?&amp;gt;&lt;/span&gt;  
&lt;span class="p"&gt;{&lt;/span&gt;  
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"publishingUsername"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;publishingCredentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PublishingUserName&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;  
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"publishingUserPassword"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateSecret&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;publishingCredentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PublishingPassword&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;  
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"appServiceName"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;appService&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might notice that we use the &lt;code&gt;Output.CreateSecret&lt;/code&gt; method to create outputs for our publishing credentials. The aim is to tell Pulumi to treat these values as secrets what it will do by encrypting them in the state file for extra protection (that is not something Terraform does by the way).&lt;/p&gt;

&lt;h3&gt;
  
  
  Implementing the Provision Infrastructure step
&lt;/h3&gt;

&lt;p&gt;To deploy the infrastructure, we can use the &lt;code&gt;pulumi up&lt;/code&gt; command. We will write the code in a fluent way as we did with the dotnet CLI:&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="n"&gt;AbsolutePath&lt;/span&gt; &lt;span class="n"&gt;InfrastructureDirectory&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;RootDirectory&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="s"&gt;"infra"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;Target&lt;/span&gt; &lt;span class="n"&gt;ProvisionInfra&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;  
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Description&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Provision the infrastructure on Azure"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Executes&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;  
    &lt;span class="p"&gt;{&lt;/span&gt;  
        &lt;span class="n"&gt;PulumiTasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PulumiUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;  
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetCwd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;InfrastructureDirectory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnableSkipPreview&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;h2&gt;
  
  
  Deploying the ASP.NET Core API to Azure App Service
&lt;/h2&gt;

&lt;p&gt;I previously said we were going to use the Kudu API to deploy our application. You can check the &lt;a href="https://github.com/projectkudu/kudu/wiki/Deploying-from-a-zip-file-or-url"&gt;documentation&lt;/a&gt; about that but concretely we will do a POST request to the zipdeploy endpoint using Basic authentication.&lt;/p&gt;

&lt;p&gt;To retrieve a stack output, we can use the &lt;code&gt;pulumi stack output&lt;/code&gt; command. To avoid duplicating the code I wrote a short method:&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="kt"&gt;string&lt;/span&gt; &lt;span class="nf"&gt;GetPulumiOutput&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;outputName&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="n"&gt;PulumiTasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PulumiStackOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;  
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetCwd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;InfrastructureDirectory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SetPropertyName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outputName&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnableShowSecrets&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DisableProcessLogOutput&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;  
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StdToText&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 step itself is not very complicated, just standard C# code using an HttpClient to send a POST request (with our application package as the content) to the Kudu API.&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="n"&gt;Target&lt;/span&gt; &lt;span class="n"&gt;Deploy&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;  
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;DependsOn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Publish&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;After&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProvisionInfra&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Executes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&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;publishingUsername&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetPulumiOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"publishingUsername"&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;publishingUserPassword&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;GetPulumiOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"publishingUserPassword"&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;base64Auth&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Convert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToBase64String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Default&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;publishingUsername&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;publishingUserPassword&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;  

        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;package&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;OpenRead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ArtifactsDirectory&lt;/span&gt; &lt;span class="p"&gt;/&lt;/span&gt; &lt;span class="s"&gt;"api.zip"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
        &lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;var&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;HttpClient&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  
        &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultRequestHeaders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Authorization&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AuthenticationHeaderValue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Basic"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base64Auth&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;httpClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PostAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"https://&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;GetPulumiOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"appServiceName"&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s"&gt;.scm.azurewebsites.net/api/zipdeploy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  
            &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;StreamContent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;package&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;blockquote&gt;
&lt;p&gt;💡 George Dangl already wrote a nice &lt;a href="https://blog.dangl.me/archive/lets-use-nuke-to-quickly-deploy-an-app-to-azure-via-zip-deployment/"&gt;article&lt;/a&gt; about using Nuke to deploy an application to Azure App Service using the Kudu API, so you can have a look at it. The code in the article is similar to the one we have here except that the credentials don't come from Pulumi outputs but from an Azure Key Vault.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;What I like about this approach is that you know exactly what you are doing, and the deployment logic is not hidden from you in an obscure YAML task whose code you will never read (yes I am talking to you Azure Pipelines and GitHub Actions 😃).&lt;/p&gt;

&lt;p&gt;But the awesome part in Nuke is that you can put a breakpoint in the code and debug it locally. If you need to modify your pipeline, you don't need to write YAML code modifications without knowing if it would work or not 🤞, commit and push your modifications, wait for an agent to run the changed pipeline in the cloud, wait for it to fail, browse the logs to try to understand the problem, and try again until it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final pipeline
&lt;/h2&gt;

&lt;p&gt;If I fold everything, the pipeline code we created looks like that:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/YycSdhacpL4lpASYdF7DCk9lZauWPWOZtBleJzIdLDo/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfMTEucG5n" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/YycSdhacpL4lpASYdF7DCk9lZauWPWOZtBleJzIdLDo/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfMTEucG5n" alt="" width="512" height="803"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I think it is quite clear with the different steps/targets defined with their dependencies/order. Yet if this is not clear enough for you, you can use the &lt;code&gt;nuke --plan&lt;/code&gt; command to display a visual representation of the pipeline (how cool is that !?)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/RsYL1V-F1wFDunVwXMxQcU0I5EVBLeP2cTPJ4MYYFPM/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfMTIucG5n" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/RsYL1V-F1wFDunVwXMxQcU0I5EVBLeP2cTPJ4MYYFPM/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfMTIucG5n" alt="" width="880" height="622"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Let's execute the complete pipeline:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/JLE6Hq0UvdPaOGRmceIcxKgIzLJVzsPtJFD0qRRBFg4/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfMTMucG5n" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/JLE6Hq0UvdPaOGRmceIcxKgIzLJVzsPtJFD0qRRBFg4/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfMTMucG5n" alt="" width="440" height="316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If I go to my Azure portal I can see the new Azure resources, among them an App Service where my Weather API is deployed.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/T9BwtlRwjvZXm8jqOQZt287vf5UgokP0tKbKP2YX1lU/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfMTQucG5n" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/T9BwtlRwjvZXm8jqOQZt287vf5UgokP0tKbKP2YX1lU/w:880/mb:500000/ar:1/aHR0cHM6Ly90ZWNo/d2F0Y2hpbmcuZGV2/L3Bvc3RzL2ltYWdl/cy9wdWx1bWlfbWV0/X251a2VfMTQucG5n" alt="" width="765" height="673"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Improvements to the example pipeline
&lt;/h3&gt;

&lt;p&gt;The pipeline I have shown in this article is just a simple sample. They are lots of things that could be done to improve it. Beyond obvious ones like adding a Test target or using GitVersion to version the package, I want to talk about some choices I made in the pipeline implementation that may not be the best ones.&lt;/p&gt;

&lt;p&gt;As I said there are many ways to deploy a package to an App Service. While using the Kudu API is fine and allowed me to show you how we can use Pulumi stack outputs to retrieve publishing credentials, it might be a bit limited in some cases and involves a bit of manual code to make the HTTP request. A good alternative would be to use the Azure CLI that has &lt;a href="https://learn.microsoft.com/en-us/cli/azure/webapp/deployment/source?view=azure-cli-latest#az-webapp-deployment-source-config-zip"&gt;a command&lt;/a&gt; for that. But my preferred option would be to use the &lt;a href="https://learn.microsoft.com/en-us/dotnet/api/overview/azure/resource-manager?view=azure-dotnet"&gt;Azure Resource Manager libraries for .NET&lt;/a&gt;. Unfortunately this SDK is quite new and miss &lt;a href="https://github.com/Azure/azure-sdk-for-net/issues/30577"&gt;samples&lt;/a&gt; on how to do that.&lt;/p&gt;

&lt;p&gt;Speaking of SDK, Pulumi has an API called the &lt;a href="https://www.pulumi.com/automation/"&gt;Automation API&lt;/a&gt; to use the Pulumi engine as an SDK. I think it would be a better option than using the Pulumi CLI. Generally speaking, I think using SDK instead of CLIs to write the targets of a pipeline brings more power, more flexibility, and a better developer experience.&lt;/p&gt;

&lt;h3&gt;
  
  
  About Nuke and CI/CD
&lt;/h3&gt;

&lt;p&gt;Nuke has many features I did not show in this small example. If we add some attributes to the pipeline code, Nuke can generate YAML workflow files to execute the Nuke pipeline. When executing the pipeline locally everything works fine because I am logged in to Pulumi CLI and Azure CLI in my terminal but I have to add secret parameters to my Nuke pipeline (a Pulumi token and an Azure Service Principal identifier/password) to make the authentication works when the pipeline is run from a CI/CD platform runner/agent.&lt;/p&gt;

&lt;p&gt;Moreover, there are many things I don't know yet about Nuke because I am just starting to use it. That is why I advise you to have a look at its &lt;a href="https://nuke.build/docs/introduction/"&gt;documentation&lt;/a&gt;, &lt;a href="https://nuke.build/resources/"&gt;at some resources&lt;/a&gt; and start playing with it by yourself.&lt;/p&gt;

&lt;p&gt;In the future, I see myself using Nuke for most of my CI pipelines, and not only for .NET projects (because I can run any CLI tools from Nuke, it also works for front projects where I would use the pnpm CLI for instance). I am not saying that because I am afraid of YAML or because I'm not familiar with ready-made tasks like Azure Pipelines tasks or GitHub Actions. I have been using Azure Pipelines for several years now and I have also played a bit with GitHub Actions. They are good platforms but lack local debugging and the great developer experience provided by a tool like Nuke. So I will continue using them but to run my Nuke pipelines 😉.&lt;/p&gt;

</description>
      <category>cicd</category>
      <category>pulumi</category>
      <category>azure</category>
      <category>csharp</category>
    </item>
  </channel>
</rss>
