<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Oskar Mamrzynski</title>
    <description>The latest articles on DEV Community by Oskar Mamrzynski (@oskarm93).</description>
    <link>https://dev.to/oskarm93</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F759794%2F0efc36e1-1d2d-4f24-b255-874257a1c8cc.png</url>
      <title>DEV Community: Oskar Mamrzynski</title>
      <link>https://dev.to/oskarm93</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/oskarm93"/>
    <language>en</language>
    <item>
      <title>Lessons learned: Azure Reservations</title>
      <dc:creator>Oskar Mamrzynski</dc:creator>
      <pubDate>Tue, 05 Nov 2024 04:47:30 +0000</pubDate>
      <link>https://dev.to/oskarm93/lessons-learned-azure-reservations-4n2j</link>
      <guid>https://dev.to/oskarm93/lessons-learned-azure-reservations-4n2j</guid>
      <description>&lt;p&gt;Here's what I learned while using Azure Reservations:&lt;/p&gt;

&lt;h2&gt;
  
  
  Reserve NOW
&lt;/h2&gt;

&lt;p&gt;If you have doubts whether you should reserve a Virtual Machine, just ask a question: "Will this VM still be there in 6 months?" If yes, reserve it for at least 1 year.&lt;/p&gt;

&lt;p&gt;I can't tell you how much money we've wasted using Pay-As-You-Go rates because of indecisive managers and broken promises. I've been in so many situations where after a year of deliberations we still did not reserve anything because the powers that be couldn't decide if the VMs would still be there. This next "migration project" is not a golden bullet to your infra spend.&lt;/p&gt;

&lt;p&gt;Many costs are static: Domain Controllers, Build Agents, Virtual Desktops, Gateway servers, IIS servers etc. If they are likely to be still there in 6 months, chances are - it will be longer and you can save at least 30% by reserving for a year.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reserve for longer
&lt;/h2&gt;

&lt;p&gt;Reservations for Virtual Machines in Azure can be set to 1 year or 3 years. You are able to exchange any set of reservations for another set as long as the new total cost is greater than the remaining value of existing ones.&lt;/p&gt;

&lt;p&gt;To give you an example: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I reserve 50 nodes of D8ads_v5 for 3 years.&lt;/li&gt;
&lt;li&gt;In 6 months time some teams want to use another VM size.&lt;/li&gt;
&lt;li&gt;Use Exchange button in Reservations tab to cancel existing reservation. Add new reservation for 40 nodes of D8ads_v5 and 10 nodes of E8ads_v5 (memory optimised).&lt;/li&gt;
&lt;li&gt;The value of 50-node reservation is now (3 years minus 6 months). The new reservation can even be slightly lower in value due to different VM sizes and you can still exchange without any penalty.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Point is: You do not have to fully commit to either 1 or 3 year reservations. Microsoft cares more about keeping you on the rollercoaster than about data centre capacity planning.&lt;/p&gt;

&lt;p&gt;Better still: You can cancel (without exchanging) up to $50K worth of reservations in any rolling 12 month period without penalties.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some reservations are greater than others
&lt;/h2&gt;

&lt;p&gt;Virtual Machines have the greatest return on reservations with savings of anything between 30% to 65%.&lt;/p&gt;

&lt;p&gt;Azure Redis Cache can slash costs about the same because it's primarily VM-based.&lt;/p&gt;

&lt;p&gt;Azure Managed Disks can save about 10% of PAYG price, but only from larger disks &amp;gt;= 1024 GB.&lt;/p&gt;

&lt;p&gt;Azure Databricks requires a lot of usage before you can reserve. I did not manage to hit the break-even point yet.&lt;/p&gt;

&lt;p&gt;ALWAYS try to consolidate your Log Analytics workspaces and check consolidated usage. Ingestion costs &amp;gt;$3 per GB at PAYG prices. You can slash that by at least 10% by committing to daily ingestion. Log Analytics is not part of normal Reservation, but rather SKU tier on the resource itself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F57k30bzbdrspfqfgtbi7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F57k30bzbdrspfqfgtbi7.png" alt="log-analytics-commitment-tiers" width="800" height="446"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Observe your utilisation
&lt;/h2&gt;

&lt;p&gt;You want to hit that 100% utilisation on your reservations. For a lot of static infra this is a given.&lt;/p&gt;

&lt;p&gt;Don't beat yourself up. Even if you miscalculate, your utilisation can drop down to 60% before you break even compared to PAYG price. Just exchange for a better number of nodes, or prepare for growth.&lt;/p&gt;

&lt;p&gt;You will sometimes hit bizarre scenarios where your monthly utilisation is &amp;lt;100% but you still get charged overage for that VM size in your subscription. This is because &lt;strong&gt;reservations are hourly&lt;/strong&gt; but the utilisation reporting is daily. &lt;a href="https://learn.microsoft.com/en-us/azure/cost-management-billing/manage/understand-vm-reservation-charges#how-reservation-discount-is-applied" rel="noopener noreferrer"&gt;Link here&lt;/a&gt; to the relevant doc.&lt;/p&gt;

&lt;p&gt;We found this out because our Azure Databricks clusters would spin up a lot of VMs for 2 hours of the day to process batch jobs. This would spike the hourly utilisation to &amp;gt;100% where overages are charged at PAYG price.&lt;br&gt;
Then, the rest of the day, the cluster would coast at &amp;lt;100%. Daily utilisation was 95% while we were still charged $25 per day for overages.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9pfo82xkg9vhz8ary69u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9pfo82xkg9vhz8ary69u.png" alt="reservation-utilisation-list" width="432" height="890"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Same with our Dev/Test environments which we turn off for the weekend. You cannot "accumulate" reservation hours over the weekend to then burst out to higher cores during the working week. The 2 weekend days were essentially wasted reservation compute. Still better than PAYG though.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Consider shared scope and billing subscription
&lt;/h2&gt;

&lt;p&gt;You can choose how to scope your Reservations: to a resource group, subscription or every subscription (top-level management group).&lt;/p&gt;

&lt;p&gt;Because our cost is all billed to 1 company we always choose shared scope. It allows me to reserve "All AKS nodes" across all squads and all subscriptions. I don't have to go to each team individually and ask them how many nodes they plan to use. I just reserve the total as-is. If one team scales down by 2 but another team needs more because they're growing - shared scope will make sure the utilisation stays at 100%. Otherwise, I would have to constantly review and exchange lots of little reservations.&lt;/p&gt;

&lt;p&gt;This advice won't apply to everyone because you may have strict cost reporting needs per subscription and different teams cannot dip into each other's purse.&lt;/p&gt;

&lt;p&gt;All reservations need a billing subscription. This might not be the subscription that the Reservation applies to but it is where the cost will be shown. We have a shared subscription for centralized bootstrap infra as per Azure CAF. This is where our "shared" cost goes, e.g. all Kubernetes nodes. Just be prepared to explain big cost spikes each month to bean counters.&lt;/p&gt;

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

&lt;p&gt;The other side-effect of shared subscription scope is that some subscriptions will have zero Virtual Machine cost despite having VMs. This is because the Reservation in shared sub will be applied to them, bringing their PAYG cost to zero.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>devops</category>
      <category>microsoft</category>
    </item>
    <item>
      <title>Cloudflare Pages and Origin Rulesets</title>
      <dc:creator>Oskar Mamrzynski</dc:creator>
      <pubDate>Tue, 03 Oct 2023 19:54:46 +0000</pubDate>
      <link>https://dev.to/oskarm93/cloudflare-pages-and-origin-rulesets-kjp</link>
      <guid>https://dev.to/oskarm93/cloudflare-pages-and-origin-rulesets-kjp</guid>
      <description>&lt;p&gt;In my current job we use Cloudflare (CF) extensively, most recently to migrate our main website written as an Angular SPA to a series of smaller apps in Remix (SSR) and next.js (SSG). To do this, we use &lt;a href="https://pages.cloudflare.com/"&gt;Cloudflare Pages&lt;/a&gt;. It's a great way of hosting your site directly on the edge.&lt;/p&gt;

&lt;p&gt;It would be cool if we could roll out new apps on new routes without impacting existing Angular app hosted on our mobile domain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scenario
&lt;/h2&gt;

&lt;p&gt;All traffic for our mobile website, &lt;code&gt;m.mydomain.com&lt;/code&gt; goes to Angular SPA hosted in Azure App Service. Product wants to deploy a brand new blog site on sub-path: &lt;code&gt;m.mydomain.com/blog/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Blog is a static site generation (SSG) app using next.js. Perfect candidate for hosting on CF Pages. I won't go into detail on how we build and deploy it - here's a &lt;a href="https://developers.cloudflare.com/pages/platform/direct-upload#wrangler-cli"&gt;link&lt;/a&gt; on how to use Wrangler to publish your build output to CF Pages.&lt;/p&gt;

&lt;p&gt;Once we deploy our assets to a CF Pages project, we get a &lt;code&gt;&amp;lt;project-name&amp;gt;.pages.dev&lt;/code&gt; hostname. Now, the main problem - how to host it on a sub-path of an existing domain?&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GmnqYk4b--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ah0xip5evf3dsoud6cks.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GmnqYk4b--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ah0xip5evf3dsoud6cks.png" alt="cf-pages-project" width="546" height="574"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Attempt 1 - Pages domain
&lt;/h2&gt;

&lt;p&gt;You can bind a domain to your CF Pages project as described &lt;a href="https://developers.cloudflare.com/pages/platform/custom-domains/"&gt;here&lt;/a&gt;. You need to create a proxied CNAME record:&lt;br&gt;
&lt;code&gt;m.mydomain.com -&amp;gt; my-project.pages.dev&lt;/code&gt; and bind it to that project. However, a proxied CNAME already exists! It points to our Azure App Service. Changing it would break the main site. This method is mostly for projects that are entirely served by CF Pages project, and not just part of it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Attempt 2 - Origin Rules
&lt;/h2&gt;

&lt;p&gt;Origin rules are part of the new &lt;a href="https://developers.cloudflare.com/rules/origin-rules/"&gt;ruleset engine&lt;/a&gt; and an evolution of Page Rules.&lt;/p&gt;

&lt;p&gt;The main difference between origin rules and page rules is that you can use expressions in your rule condition instead of just path-matching patterns. So you can use hostname, IP, country, headers, cookies, paths, query strings etc. in your matching expression.&lt;/p&gt;

&lt;p&gt;They are still in beta, but we use them extensively in our Production without issues - except this one! Let's hope they fix it before GA. :)&lt;/p&gt;

&lt;p&gt;We can create an origin rule like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--K-SOgtjT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eo919n4ldkm41tpm8d8x.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--K-SOgtjT--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eo919n4ldkm41tpm8d8x.png" alt="origin-rule-example" width="800" height="1021"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, let's go to &lt;code&gt;https://m.mydomain.com/blog/&lt;/code&gt;. Oh no! 522 error. The connection did not time out by the way. I got this error instantly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--SUns_CRw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p6i8qhwckth1oj6nlea1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SUns_CRw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p6i8qhwckth1oj6nlea1.png" alt="522-error" width="800" height="757"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After contacting Cloudflare, this turns out to be some security feature preventing you from calling Pages from Cloudflare - even though the hostname we put in the rule belongs to our project. Technically, we do not own the &lt;code&gt;.pages.dev&lt;/code&gt; domain - that is shared between all Cloudflare customers, but it would have been good for them to realize we use it!&lt;/p&gt;

&lt;p&gt;I also found this &lt;a href="https://developers.cloudflare.com/rules/origin-rules/features/#dns-record"&gt;doc&lt;/a&gt; which explains that your DNS override should be a hostname on your zone. That's OK - I can make a brand new CNAME on my zone: &lt;code&gt;&amp;lt;project-name&amp;gt;.mydomain.com -&amp;gt; &amp;lt;project-name&amp;gt;.pages.dev&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--iKkSCx-T--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ilcimv6vb7c26lzk1n2y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--iKkSCx-T--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ilcimv6vb7c26lzk1n2y.png" alt="create-cname" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now update the origin rule:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NdUDo8lI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q08zqnpls3g5xit9fp1j.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NdUDo8lI--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q08zqnpls3g5xit9fp1j.png" alt="update-origin-rule" width="569" height="605"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Still nothing! Always 522 error. Believe me when I tell you - I tried every permutation of DNS records with proxied, not proxied, even tried where brand new DNS record was also a Pages domain like in attempt 1. Nothing worked.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--SUns_CRw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p6i8qhwckth1oj6nlea1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SUns_CRw--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p6i8qhwckth1oj6nlea1.png" alt="522-error" width="800" height="757"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Attempt 3 - Workers
&lt;/h2&gt;

&lt;p&gt;We also have a &lt;a href="https://developers.cloudflare.com/workers/"&gt;Cloudflare Workers&lt;/a&gt; bundle purchased. Workers are a very powerful tool that allow you to write custom JavaScript to modify your requests, responses, fetch responses from different origins etc. We use it in some places to augment Cloudflare functionality where some rulesets don't work as we expect them. To date, we have been using this trick to implement our scenario:&lt;/p&gt;

&lt;p&gt;You can create a CF worker script, read the URL of the incoming request, change the hostname and use &lt;code&gt;fetch()&lt;/code&gt; to get it, as if it was a 3rd party &lt;a href="https://developers.cloudflare.com/workers/examples/respond-with-another-site/"&gt;URL&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export default {
  async fetch(request, env) {
      const url = new URL(request.url);
      url.host = "&amp;lt;my-project&amp;gt;.pages.dev";
      return fetch(url.toString(), request);
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can bind a route to be handled by your worker code. This binding is similar to Page Rules - only path based matching allowed, not full expressions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--KA2_BBIb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3keyqzz0da6m1qpvu6cr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KA2_BBIb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3keyqzz0da6m1qpvu6cr.png" alt="worker-route-binding" width="800" height="345"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GP1AUe2l--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wzb4kqgivvgyir6jc15e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GP1AUe2l--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wzb4kqgivvgyir6jc15e.png" alt="worker-route-binding-success" width="800" height="169"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And voila! We have our blog site on &lt;code&gt;m.mydomain.com/blog/&lt;/code&gt; while the rest of &lt;code&gt;m.mydomain.com&lt;/code&gt; remains the same. I had to change some domain names for the demo, but you can probably guess.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--260Dx9yj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fl5fsjmdb5w86ojwylpz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--260Dx9yj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fl5fsjmdb5w86ojwylpz.png" alt="workers-site-result" width="800" height="583"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are some disadvantages with this approach:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are paying extra for Worker requests on top of your throughput.&lt;/li&gt;
&lt;li&gt;Complex and bespoke &lt;code&gt;fetch()&lt;/code&gt; behaviours. We put our entire mobile site through Workers for few reasons, but logic in the script becomes complicated and hard to understand.&lt;/li&gt;
&lt;li&gt;Performance - it seems a little silly that we need an extra hop through Workers to hop over to our own CF Pages project. This hop always adds just a little bit more milliseconds to our overall latency.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Attempt 4 - Origin Rules (again!), with a workaround
&lt;/h2&gt;

&lt;p&gt;Attempt 2 got me thinking - We use origin rules successfully in other places where DNS host is overwritten. In those cases, we create a CNAME record on our zone to Azure resource hostnames. e.g.&lt;br&gt;
&lt;code&gt;&amp;lt;my-app&amp;gt;.mydomain.com -&amp;gt; &amp;lt;my-app&amp;gt;.azurewebsites.net&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Maybe Cloudflare has issues with it's own domain, but not if an external DNS resolves it. What if I create a CNAME that points to a hostname on Azure DNS that ultimately resolves to my CF Pages project hostname?&lt;/p&gt;

&lt;p&gt;Let's create a new public Azure DNS zone. To add insult to injury, I made it a sub-domain of my Cloudflare zone: &lt;code&gt;omtest-sub.mydomain.com&lt;/code&gt;.&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--jgUvzSDY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m5gpqxom7714i5ydzb48.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--jgUvzSDY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m5gpqxom7714i5ydzb48.png" alt="azure-dns" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now link this sub-domain to root domain using NS records in Cloudflare:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Q7eR_bw2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kn7ea9ynw50l4xkd0c7w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Q7eR_bw2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kn7ea9ynw50l4xkd0c7w.png" alt="subdomain-cf" width="800" height="254"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now I can resolve my custom domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PS&amp;gt; dig blog.omtest-sub.mydomain.com

; &amp;lt;&amp;lt;&amp;gt;&amp;gt; DiG 9.14.2 &amp;lt;&amp;lt;&amp;gt;&amp;gt; blog.omtest-sub.mydomain.com
;; global options: +cmd
;; Got answer:
;; -&amp;gt;&amp;gt;HEADER&amp;lt;&amp;lt;- opcode: QUERY, status: NOERROR, id: 10431
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 4096
;; QUESTION SECTION:
;blog.omtest-sub.mydomain.com. IN    A

;; ANSWER SECTION:
blog.omtest-sub.mydomain.com. 600 IN CNAME   &amp;lt;my-project&amp;gt;.pages.dev.
&amp;lt;my-project&amp;gt;.pages.dev.     300     IN      A       188.114.96.7
&amp;lt;my-project&amp;gt;.pages.dev.     300     IN      A       188.114.97.7

;; Query time: 123 msec
;; SERVER: 10.64.140.254#53(10.64.140.254)
;; WHEN: Tue Oct 03 20:33:20 GMT Summer Time 2023
;; MSG SIZE  rcvd: 124
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Last step is to put my new CNAME directly into DNS override box in the origin rule from Attempt 2. After all, it's already on &lt;code&gt;mydomain.com&lt;/code&gt; zone!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--MxKLpQ42--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/61bdqxjjwu5tt4saboql.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--MxKLpQ42--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/61bdqxjjwu5tt4saboql.png" alt="origin-rule-revisited" width="800" height="1008"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And we're back in business..&lt;br&gt;
&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--260Dx9yj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fl5fsjmdb5w86ojwylpz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--260Dx9yj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fl5fsjmdb5w86ojwylpz.png" alt="workers-site-result" width="800" height="583"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I assume this would also work if you used another DNS provider like NS1 or Route53. You obviously have to pay for external DNS resolution. Azure charges by millions of queries, but it's cheaper than having to go with Workers. Ultimately, I hope they fix this problem, so we can directly use &lt;code&gt;.pages.dev&lt;/code&gt; domains in origin rules.&lt;/p&gt;

</description>
      <category>cloudflare</category>
      <category>devops</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Automating high-privilege operations in Azure</title>
      <dc:creator>Oskar Mamrzynski</dc:creator>
      <pubDate>Fri, 26 May 2023 07:29:14 +0000</pubDate>
      <link>https://dev.to/oskarm93/automating-high-privilege-operations-in-azure-4d0i</link>
      <guid>https://dev.to/oskarm93/automating-high-privilege-operations-in-azure-4d0i</guid>
      <description>&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;In a lot of traditional companies the IT department holds the keys to Azure Active Directory. Many times these teams have no interest in making things easier for developers or follow DevOps mentality to automate. For example, log a ticket in your ITSM of choice and 3 days later someone will manually create an app registration for you. With luck, it may even have been configured correctly.&lt;/p&gt;

&lt;p&gt;Understandably, many of these operations require high privileges in AAD and you don't want to be giving these out left and right to developers to solve their own problems. &lt;/p&gt;

&lt;h2&gt;
  
  
  Examples of high-privilege operations we automated
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Creating AAD groups and managing memberships&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We have a group-centric RBAC model in Azure. Using team-based and role-based groups we give out granular permissions to limited scopes.&lt;/li&gt;
&lt;li&gt;Onboarding, offboarding users - managing group memberships helps when people leave or join teams, or need a subset of team's permissions to collaborate.&lt;/li&gt;
&lt;li&gt;Onboarding new product teams - We want to quickly spin up new Azure subscriptions and associated groups, roles etc.&lt;/li&gt;
&lt;li&gt;Consistent structure - each team receives the same set of basic permissions, and any deviations are reviewed and approved.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Creating service principals and app registrations&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Often you will need a service principal to give a team access to Azure resources for automation. You may need to store its Client ID and Secret in a Key Vault or ADO service connection for them to use.&lt;/li&gt;
&lt;li&gt;3rd party software like Grafana Cloud, Elastic Cloud, GitHub etc. may need service principals configured for SAML / SSO. You can manage access to these by assigning roles in the Enterprise App.&lt;/li&gt;
&lt;li&gt;Developers will want to secure their own APIs with App registrations, define roles, reply URLs and extra permissions.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Creating role definitions and assignments&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Custom role definitions require high privileges to create, but are essential for least-privilege systems.&lt;/li&gt;
&lt;li&gt;Creating consistent permission structure across &lt;a href="https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/"&gt;landing zone&lt;/a&gt; subscriptions requires creating role assignments for AAD groups.&lt;/li&gt;
&lt;li&gt;You may need to create management-group level role assignments too.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Solution overview
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Instead of creating things manually, use Terraform to create things in AAD.&lt;/li&gt;
&lt;li&gt;Put Terraform into a source control repository with branch policies. Disallow direct commits to main branch.&lt;/li&gt;
&lt;li&gt;Use pull requests as means to propose, review and approve changes.&lt;/li&gt;
&lt;li&gt;Only reviewed and approved changes can be automatically deployed.&lt;/li&gt;
&lt;li&gt;Use secure high-privilege service principals to execute Terraform pipeline.&lt;/li&gt;
&lt;li&gt;Anyone can submit changes via Pull Requests, including developers. IT / DevOps become reviewers and advisories instead of doers of the work.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Setting up Terraform repo
&lt;/h2&gt;

&lt;p&gt;We use Azure DevOps (ADO) to host our git repositories. I recommend having a dedicated project for centralised IT functions like AAD, firewall, DNS etc. This project can have a small subset of people with rights. We decided to create all 3 categories from above in the same Terraform repository, because it's easier to do onboarding of new teams and Azure subscriptions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--4quojXBo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7h4gn96ia8peaeu5u6hv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--4quojXBo--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7h4gn96ia8peaeu5u6hv.png" alt="rbac repo" width="800" height="477"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Importantly, you want to set up &lt;a href="https://learn.microsoft.com/en-us/azure/devops/repos/git/branch-policies?view=azure-devops&amp;amp;tabs=browser"&gt;branch policies&lt;/a&gt; on the repo to prevent people from submitting changes directly to main branch. Branch policies can also enforce use of Pull Requests, comment resolution, running of a validation pipeline and number of reviewer votes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--30jEJYhm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gwjjtk7ba0uy7znzmuy2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--30jEJYhm--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gwjjtk7ba0uy7znzmuy2.png" alt="branch-policies" width="800" height="964"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Comment resolution ensures that all discussion is completed with both submitter and reviewers happy. We also have an auto-generated Terraform plan summary comment from this &lt;a href="https://www.natmarchand.fr/terraform-plan-as-pr-comment-in-azure-devops/"&gt;blog post&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Build validation will run a Terraform plan pipeline to check that IAC is valid and will post the plan to PR as a comment. We always sanity check the plan against proposed changes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;DevOps team is automatically added as reviewers. You can add multiple teams or different teams automatically depending on which paths in the repo are modified.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The actual Terraform files set up is quite easy. We use &lt;a href="https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/guides/managed_service_identity"&gt;azuread&lt;/a&gt; and &lt;a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/guides/managed_service_identity"&gt;azurerm&lt;/a&gt; providers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;providers.tf&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;terraform {
  backend "azurerm" {}
  required_providers {
    azuread = {
      source  = "hashicorp/azuread"
      version = "&amp;lt;3.0.0"
    }
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "&amp;lt;4.0.0"
    }
  }
}

provider "azuread" {
  use_msi   = true
  client_id = var.group_admin_client_id
  tenant_id = local.tenant_id
}

provider "azuread" {
  alias     = "service-principal-creator"
  use_msi   = true
  client_id = var.service_principal_creator_client_id
  tenant_id = local.tenant_id
}

provider "azurerm" {
  alias                      = "access-admin"
  subscription_id            = "&amp;lt;subscription_id&amp;gt;"
  use_msi                    = true
  client_id                  = var.access_admin_client_id
  tenant_id                  = local.tenant_id
  skip_provider_registration = true
  features {
    key_vault {
      recover_soft_deleted_key_vaults = false
    }
  }
}

data "azuread_client_config" "sp_creator" {
  provider = azuread.service-principal-creator
}

data "azuread_client_config" "group_admin" {}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;dynamic-groups.tf (example)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "azuread_group" "az-all-technology-staff" {
  display_name     = "az-all-technology-staff"
  security_enabled = true
  owners           = [data.azuread_client_config.group_admin.object_id]
  types            = ["DynamicMembership"]

  dynamic_membership {
    enabled = true
    rule    = "user.department -eq \"Technology\" and (user.accountEnabled -eq true)"
  }
  lifecycle {
    ignore_changes = [members]
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;service-principals.tf (module example)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module "policy_contributor" {
  source         = "../modules/service-principal"
  principal_name = "&amp;lt;conventions_prefix&amp;gt;-azure-policy-contributor"
  providers = {
    azuread = azuread.service-principal-creator
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;service-principals.tf (inside the module)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;data "azuread_client_config" "identity" {}

resource "azuread_application" "app" {
  display_name = var.principal_name
  owners       = [data.azuread_client_config.identity.object_id]
  lifecycle {
    ignore_changes = [required_resource_access]
  }
}

resource "azuread_service_principal" "sp" {
  application_id = azuread_application.app.application_id
  owners         = [data.azuread_client_config.identity.object_id]
}

resource "azuread_application_password" "app_client_secret" {
  application_object_id = azuread_application.app.object_id
}

resource "azurerm_key_vault_secret" "client_id" {
  name         = "${var.principal_name}-client-id"
  value        = azuread_application.app.application_id
  key_vault_id = var.key_vault_id
}

resource "azurerm_key_vault_secret" "client_secret" {
  name         = "${var.principal_name}-client-secret"
  value        = azuread_application_password.app_client_secret.value
  key_vault_id = var.key_vault_id
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;role-assignments.tf (example)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "azurerm_role_assignment" "az-sub-readers-prod" {
  provider             = azurerm.access-admin
  scope                = "/subscriptions/${var.prod_subscription_id}"
  role_definition_name = "Reader"
  principal_id         = module.az-sub-readers-prod.object_id
}

resource "azurerm_role_assignment" "az-sub-contributors-prod" {
  provider             = azurerm.access-admin
  scope                = "/subscriptions/${var.prod_subscription_id}"
  role_definition_name = "Contributor"
  principal_id         = module.az-sub-contributors-prod.object_id
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We can put all of these together into a Terraform module to onboard whole sets of product teams with default permissions. Module example below creates AAD groups for readers, contributors, AKS readers, AKS admins, SQL admins, SQL readers, KV readers, KV admins, monitoring contributors, a service principal for team-abc to use in ADO pipelines and assigns roles in Azure on relevant subscriptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;portfolio-abc.tf (example)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module "team-abc" {
  source = "../modules/group"
  name   = "team-abc"
  active_members = [
    local.user_ids["user1@domain.com"],    
    local.user_ids["user2@domain.com"],
  ]
}

module "abc-default-groups" {
  source                  = "../modules/default-portfolio-groups"
  portfolio_code          = "abc"
  devtest_subscription_id = local.sub_ids["abc-devtest"]
  prod_subscription_id    = local.sub_ids["abc-prod"]
  default_team_object_id  = module.team-abc.object_id
  devops_team_object_id   = module.team-devops.object_id
  dba_team_object_id      = module.team-db-admins.object_id
  kingmakers              = true
  providers = {
    azurerm.kv-admin                  = azurerm
    azurerm.access-admin              = azurerm.access-admin
    azuread.service-principal-creator = azuread.service-principal-creator
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Setting up automation identities
&lt;/h2&gt;

&lt;p&gt;You may have noticed we use 3 provider blocks in providers.tf file. We manually created a separate user-assigned managed identity (service principal) for each scenario and assigned them the right set of privileges. You need to be a Global Admin on AAD to set these up.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--J_rdOI5C--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/30vkt3lgi9jk035q6946.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--J_rdOI5C--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/30vkt3lgi9jk035q6946.png" alt="managed-identities" width="442" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Access admin&lt;/strong&gt; - We assigned it User Access Administrator role over the tenant root management group. This way it can create role definitions scoped across all subscriptions and manage default role assignments for each sub during landing zone onboarding.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--cF2Pz6BS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2sbbq62gun10gnghmmaz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--cF2Pz6BS--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2sbbq62gun10gnghmmaz.png" alt="access-admin" width="800" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Group admin&lt;/strong&gt; - This identity needs permissions over Microsoft Graph API to create/delete AAD groups and manage them.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We assigned it &lt;code&gt;Directory.Read.All&lt;/code&gt;, &lt;code&gt;Group.ReadWrite.All&lt;/code&gt; and &lt;code&gt;RoleManagement.ReadWrite.Directory&lt;/code&gt; app roles on Microsoft Graph API. Required roles are described &lt;a href="https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/group"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I found this &lt;a href="https://aztoso.com/security/microsoft-graph-permissions-managed-identity/"&gt;blog post&lt;/a&gt; about how to assign extra app roles to managed identities. Execute this Azure PowerShell script as Global Admin:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$roles = @('Directory.Read.All', 'Group.ReadWrite.All', 'RoleManagement.ReadWrite.Directory')
$managed_identity = Get-AzADServicePrincipal -ObjectId '&amp;lt;mi_object_id&amp;gt;'

$access_token = (Get-AzAccessToken -ResourceTypeName 'MSGraph').Token
$graph_sp = Get-AzADServicePrincipal -ApplicationId '00000003-0000-0000-c000-000000000000'

$roles | % {
    $role_name = $_
    $role = $graph_sp.AppRole | ? { $_.Value -eq $role_name }
    $body = @{
        'principalId' = $managed_identity.Id;
        'resourceId'  = $graph_sp.Id;
        'appRoleId'   = $role.Id
    } | ConvertTo-Json -Compress

    Invoke-RestMethod `
        -Method POST `
        -Headers @{Authorization = "Bearer $access_token" } `
        -ContentType 'application/json' `
        -Uri "https://graph.microsoft.com/v1.0/servicePrincipals/$($managed_identity.Id)/appRoleAssignments" `
        -Body $body
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--8OT6vTyE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0w7mkdekkg83b3oujqdo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--8OT6vTyE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0w7mkdekkg83b3oujqdo.png" alt="group-admin" width="800" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Service principal creator&lt;/strong&gt; - This identity also needs MS Graph permissions, as described &lt;a href="https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application"&gt;here&lt;/a&gt; and &lt;a href="https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/app_role_assignment"&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We executed the same script as above, just replacing roles list with &lt;code&gt;Directory.Read.All&lt;/code&gt;, &lt;code&gt;Application.ReadWrite.OwnedBy&lt;/code&gt;, &lt;code&gt;AppRoleAssignment.ReadWrite.All&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--MLyFRpzY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oxtoxcx6ksuo53jn4zd3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--MLyFRpzY--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/oxtoxcx6ksuo53jn4zd3.png" alt="sp-creator" width="800" height="325"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up pipeline
&lt;/h2&gt;

&lt;p&gt;We use managed identities rather than service principals so we do not need to use and rotate client secrets. Our Terraform pipeline should execute on an ADO agent in a trusted location. See my other &lt;a href="https://dev.to/oskarm93/azure-devops-agents-on-aks-with-workload-identity-113o"&gt;blog post&lt;/a&gt; about how we set up an ADO agent linked to managed identities. The agent can run on a normal VM too and be assigned these managed identities directly instead of using federated credentials. Bottom line is that our ADO project has an agent pool where the agent is able to obtain tokens from these 3 managed identities.&lt;/p&gt;

&lt;p&gt;For Terraform to log in with 3 different managed identities with 3 different providers we need to pass in Client ID for each of them as a parameter.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;pipeline.yaml (example)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: $(Rev:rr)
trigger:
- main
pool: aks-azure-rbac

variables:
  tf_dir: $(Build.SourcesDirectory)/terraform
  tf_vars: |
    service_principal_creator_client_id = "&amp;lt;client_id_1&amp;gt;"
    group_admin_client_id = "&amp;lt;client_id_2&amp;gt;"
    access_admin_client_id = "&amp;lt;client_id_3&amp;gt;"

steps:
- checkout: self
  clean: true

- pwsh: |
    $tf_vars = '$(tf_vars)'
    [System.IO.File]::WriteAllText('terraform.tfvars', $tf_vars)
  displayName: set tf vars
  workingDirectory: $(tf_dir)

- task: TerraformTaskV2@2
  displayName: tf init
  inputs:
    provider: azurerm
    command: init
    workingDirectory: $(tf_dir)
    backendServiceArm: &amp;lt;terraform_storage_service_connection&amp;gt;
    backendAzureRmResourceGroupName: &amp;lt;storage_account_rg&amp;gt;
    backendAzureRmStorageAccountName: &amp;lt;storage_account_name&amp;gt;
    backendAzureRmContainerName: &amp;lt;storage_container&amp;gt;
    backendAzureRmKey: &amp;lt;blob_name&amp;gt;

- pwsh: |
    terraform plan -out tfplan
  displayName: tf plan
  workingDirectory: $(tf_dir)

- pwsh: |
    &amp;amp; '$(Build.SourcesDirectory)/scripts/set-tf-plan-pr-comments.ps1'
  workingDirectory: $(tf_dir)
  displayName: set pr comments
  condition: and(succeeded(), eq(variables['Build.Reason'], 'PullRequest'), ne(variables ['Build.Repository.Provider'] , 'GitHub') )
  env:
    SYSTEM_ACCESSTOKEN: $(System.AccessToken)

- pwsh: |
    terraform apply -auto-approve tfplan
  displayName: tf apply
  condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
  workingDirectory: $(tf_dir)

- task: DeleteFiles@1
  displayName: clean up tf files
  condition: always()
  inputs: 
    Contents: |
      **/tfplan
      **/*.tfvars
      **/*.tfstate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We set up the pipeline to only run the apply step when running on main branch. Terraform will still plan changes and post PR comments with a summary when running a PR validation build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Process for making changes
&lt;/h2&gt;

&lt;p&gt;In our ADO project we allow everyone in the tech department to read and contribute to our repository. They must do so via &lt;a href="https://learn.microsoft.com/en-us/azure/devops/repos/git/pull-requests?view=azure-devops&amp;amp;tabs=browser"&gt;pull requests&lt;/a&gt;. A contributor can either clone the repo to their local, make changes on a branch and submit a pull request or do it directly via the ADO UI.&lt;/p&gt;

&lt;p&gt;All pull requests go to our Slack channel so our team can review them and approve if everything is OK.&lt;/p&gt;

&lt;p&gt;Once approved, the PR is merged into main where automated pipeline picks it up, authenticates using managed identities and creates whatever is necessary via Terraform.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--eCL7QQ9Z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bt2t1pbdq1bv1x38yj4k.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--eCL7QQ9Z--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bt2t1pbdq1bv1x38yj4k.png" alt="pr" width="800" height="630"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--XtwiPIvn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9bt7hurlwm4gayn87334.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--XtwiPIvn--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9bt7hurlwm4gayn87334.png" alt="app-reg" width="800" height="684"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our own team members raising changes are treated no different from any other contributor. Someone else still has to review and approve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security considerations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You need high privilege role to set this up to begin with. At least Application Administrator in AAD and User Access Administrator over management groups. I had to elevate my account to Global Administrator using Privileged Identity Management.&lt;/li&gt;
&lt;li&gt;Creating things manually with user accounts can be safer because you would normally have to pass through Privileged Identity Management, approvals, conditional access policy, MFA etc. before you can execute a high privilege action.&lt;/li&gt;
&lt;li&gt;Automated identities have fewer safety restrictions than user accounts (lack of MFA for instance). You may be able to set up conditional access policy to only allow obtaining access tokens from 1 trusted location - ADO agent.&lt;/li&gt;
&lt;li&gt;"Who guards the guardians" - whoever controls the system responsible for automation can elevate themselves to use these managed identities. This includes Azure DevOps project admins (on that particular project), Project Collection Admins, on Azure - Contributors and Managed Identity Operator roles.&lt;/li&gt;
&lt;li&gt;If you have a Managed Identity Role over those managed identities (or equivalent role on a service principal) then you can obtain access tokens with privileges potentially higher than your own. Azure &lt;a href="https://learn.microsoft.com/en-us/azure/cloud-adoption-framework/ready/landing-zone/#azure-landing-zone-conceptual-architecture"&gt;landing zone&lt;/a&gt; architecture seems to recommend a dedicated Azure subscription where you create these managed identities and where limited number of people have access. We follow this practice.&lt;/li&gt;
&lt;li&gt;Our ADO agents run on an AKS cluster. There are a myriad of ways in which a cluster can become vulnerable and we try our best to secure it, but you also have to be careful who can execute &lt;code&gt;kubectl exec&lt;/code&gt; on it.&lt;/li&gt;
&lt;li&gt;Terraform state file is stored in Azure blob storage in our case. We create Azure Key Vault secrets via Terraform to put in Client ID and Client Secret, but plain text values are also present in the Terraform state file. Described &lt;a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_secret"&gt;here&lt;/a&gt;. We will soon mitigate this by moving most of our service principals to managed identities. Whoever has access to the state file can read out these secrets.&lt;/li&gt;
&lt;li&gt;You may want to set up any Activity Log alerts in AAD and in Azure for when these identities do anything. If the timestamp doesn't coincide with a main-branch pipeline, then something fishy may be going on.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>azure</category>
      <category>architecture</category>
      <category>terraform</category>
      <category>devops</category>
    </item>
    <item>
      <title>Azure DevOps agents on AKS with workload identity</title>
      <dc:creator>Oskar Mamrzynski</dc:creator>
      <pubDate>Tue, 23 May 2023 15:00:06 +0000</pubDate>
      <link>https://dev.to/oskarm93/azure-devops-agents-on-aks-with-workload-identity-113o</link>
      <guid>https://dev.to/oskarm93/azure-devops-agents-on-aks-with-workload-identity-113o</guid>
      <description>&lt;p&gt;Recently I deployed some Azure DevOps (ADO) agents on Azure Kubernetes Service (AKS) and use the new workload identity add-on to authenticate to Azure. We mainly use these agents to deploy other Azure with Terraform / Azure CLI / Azure PowerShell within our private network. Builds are done on Microsoft-hosted agents.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Resource utilisation is better than using VMs. Agents used for deployment are not very resource intensive on CPU/Memory as they mostly do network I/O to talk to various APIs and deploy things.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--yOFWGn7P--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/086i4oamehmym2945qew.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--yOFWGn7P--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/086i4oamehmym2945qew.png" alt="grafana cpu usage" width="800" height="166"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--d70YwQpb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7h0j3ifsp2lelaachkzz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--d70YwQpb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7h0j3ifsp2lelaachkzz.png" alt="grafana memory usage" width="800" height="150"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;You can use &lt;a href="https://keda.sh/blog/2021-05-27-azure-pipelines-scaler/"&gt;KEDA&lt;/a&gt; to auto-scale agents based on number of pending ADO jobs. This lets you scale down during quiet periods to reclaim resources.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;You can deploy agents within your private network. Most of our deployment targets are behind either private link or IP-whitelisted resources. You cannot use Microsoft-hosted agents to deploy to those. You must use self-hosted agents on e.g. network-joined AKS cluster.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;When running deployment pipelines on ADO agents, you will have to authenticate to Azure to deploy or manage resources. You can use service principals, but they are not the most secure option because they require a Client ID and Secret. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Managed identities are great for keyless authentication to Azure resources. Terraform and all Azure tools support managed identity authentication. You can get rid of the need for principal credential rotation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/aks/workload-identity-overview"&gt;Workload identity&lt;/a&gt; add-on for Kubernetes is the new way of assigning managed identities to pods in Kubernetes. This can be done between multiple Azure subscriptions (and even cross-tenant), and agent pod doesn't even have to live on Azure Kubernetes Service (could be EKS or GKE). It is less clunky than &lt;a href="https://github.com/Azure/aad-pod-identity"&gt;aad-pod-identity&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Anyone who can execute jobs on that agent pool will be able to obtain managed identity &lt;a href="https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token"&gt;access token&lt;/a&gt;. If you share agent pools between multiple teams then you shouldn't use this method. Each team will be able to access each other's resources.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For multiple teams you should ideally have 1 managed identity linked to 1 agent pool, and 1 agent pool usable only by 1 team (ADO project).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is more of a "fill in the blanks" guide than a step-by-step tutorial. I am going to only briefly mention some setup steps because they are better documented elsewhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Set up a basic ADO agent on AKS first:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Create a basic ADO agent image. This just involves a base Debian image, some dependencies and a pre-made script from Microsoft. My agents are all based on a Linux distro, so you can find the guide &lt;a href="https://learn.microsoft.com/en-us/azure/devops/pipelines/agents/docker?view=azure-devops#create-and-build-the-dockerfile-1"&gt;here&lt;/a&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add your dependencies on top of the base ADO agent image. In my case, I just need Terraform and PowerShell, but you can also install Azure CLI, kubectl etc.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FROM &amp;lt;Base ADO agent image&amp;gt;

WORKDIR /tools

# To make it easier for build and release pipelines to run apt-get,
# configure apt to not require confirmation (assume the argument by default)
ENV DEBIAN_FRONTEND=noninteractive
RUN echo "APT::Get::Assume-Yes \"true\";" &amp;gt; /etc/apt/apt.conf.d/90assumeyes

# Updating paths to tools
ENV PATH="/tools/azure-powershell-core:/tools/terraform:/tools:${PATH}"

# Install basics
RUN apt-get update 
RUN apt-get install curl zip unzip

# Install Terraform
ADD https://releases.hashicorp.com/terraform/1.4.6/terraform_1.4.6_linux_amd64.zip /tmp/terraform.zip
RUN unzip /tmp/terraform.zip -d terraform
RUN chmod +x terraform/terraform

# Install PowerShell Core
ADD https://github.com/PowerShell/PowerShell/releases/download/v7.1.5/powershell_7.1.5-1.ubuntu.20.04_amd64.deb powershell.deb
RUN dpkg -i powershell.deb ; exit 0
RUN apt-get install -f

WORKDIR /azp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Push your ADO agent image to a registry of your choice, in my case ACR:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;az acr login -n &amp;lt;acr_name&amp;gt;
docker push &amp;lt;acr_name&amp;gt;.azurecr.io/&amp;lt;ado_agent_image&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;You need an AKS cluster with workload identity add-on deployed. You can create one using Azure CLI as described &lt;a href="https://learn.microsoft.com/en-us/azure/aks/workload-identity-deploy-cluster"&gt;here&lt;/a&gt;. In my case, clusters are deployed using Terraform, so I just set &lt;a href="https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/kubernetes_cluster#workload_identity_enabled"&gt;these properties&lt;/a&gt; to true. If using Terraform, you will need to output OIDC issuer URL to be used in subsequent steps.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "azurerm_kubernetes_cluster" "cluster" {
(...)
  oidc_issuer_enabled                 = true
  workload_identity_enabled           = true
(...)
}

output "aks_oidc_issuer_url" {
  value = azurerm_kubernetes_cluster.cluster.oidc_issuer_url
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Create ADO agent pool and &lt;a href="https://learn.microsoft.com/en-us/azure/devops/organizations/accounts/use-personal-access-tokens-to-authenticate?view=azure-devops&amp;amp;tabs=Windows"&gt;personal access token&lt;/a&gt; to join agents to a pool.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--R_IPp1e2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dbfww09lgxds6dk4ff3u.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--R_IPp1e2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dbfww09lgxds6dk4ff3u.png" alt="ado-agent-pool" width="800" height="266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--hKVfcM1p--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gf94g83ti4nxpc2ia7rz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hKVfcM1p--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gf94g83ti4nxpc2ia7rz.png" alt="pat-token" width="779" height="644"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploy your ADO agent image as a Kubernetes deployment. You can use YAML, though I prefer to create everything in Terraform.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;### variables.tf
variable "pool_name" {}
variable "ado_token" {}

variable "replicas" {
  default = 1
}

variable "requests" {
  default = {
    cpu    = "100m"
    memory = "512Mi"
  }
}

variable "limits" {
  default = {
    memory = "2Gi"
  }
}

variable "disk_space_limit" {
  default = "4Gi"
}

locals {
  work_dir  = "/mnt/work"
  ado_agent = "ado-agent-${var.pool_name}"
}

### providers.tf
terraform {
  &amp;lt;backend_configuration&amp;gt;
  required_providers {
    helm = {
      source  = "hashicorp/helm"
      version = "&amp;lt;3.0.0"
    }
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "&amp;lt;3.0.0"
    }
  }
}

### kubernetes.tf
resource "kubernetes_namespace_v1" "namespace" {
  metadata {
    name = "ado-agents-${var.pool_name}"
  }
}

resource "kubernetes_secret_v1" "env_vars" {
  metadata {
    name      = local.ado_agent
    namespace = kubernetes_namespace_v1.namespace.metadata[0].name
  }

  data = {
    # These variables are used by the script from Step 1. 
    # Have a look at the script Microsoft provides in their guide and what other config options they have available. 
    # Each of these secret Key-Value entries is another environment variable mounted to the pod.
    AZP_URL   = "https://dev.azure.com/&amp;lt;your_org_name&amp;gt;"
    # You can source this from input variable, Azure Key Vault, etc.
    AZP_TOKEN = var.ado_token 
    AZP_POOL  = var.pool_name
    AZP_WORK  = local.work_dir
  }
}

resource "kubernetes_deployment_v1" "ado_agent" {
  metadata {
    name      = local.ado_agent
    namespace = kubernetes_namespace_v1.namespace.metadata[0].name
  }

  spec {
    replicas = var.replicas

    selector {
      match_labels = {
        app = local.ado_agent
      }
    }

    template {
      metadata {
        labels = {
          app = local.ado_agent
        }
      }

      spec {
        container {
          name  = local.ado_agent
          image = "&amp;lt;acr_name&amp;gt;.azurecr.io/&amp;lt;ado_agent_image&amp;gt;"

          # We link all variables from a secret ref instead of listing them here directly.
          # so that your PAT token doesn't show up on kubectl describe pod
          env_from {
            secret_ref {
              name = kubernetes_secret_v1.env_vars.metadata[0].name
            }
          }

          volume_mount {
            name       = "temp-data"
            mount_path = local.work_dir
          }

          resources {
            limits   = var.limits
            requests = var.requests
          }
        }

        volume {
          name = "temp-data"

          # Using node's ephemeral disk space for agent storage.
          # Deployments agents do not use much disk space and can be wiped after pod dies.
          empty_dir {
            size_limit = var.disk_space_limit
          }
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Use terraform plan and apply while your kubectl context is linked to your AKS cluster. This should create a new agent and link it to the pool. Microsoft's start up script downloads a latest version of the agent each time a pod starts up, registers it against the pool and removes it on pod termination.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PS&amp;gt; kubectl get deployment
NAME                     READY   UP-TO-DATE   AVAILABLE   AGE
omtest1                  1/1     1            1           11d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--LOan7mpO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/97jzfo39fw1qichho8m3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--LOan7mpO--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/97jzfo39fw1qichho8m3.png" alt="ado-agent-in-pool" width="800" height="166"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;This set up is just a normal ADO agent without workload identity linked together. You would still need a Client ID / Secret or Azure DevOps Service Connection to use in your pipelines.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Adding workload identity to ADO agent:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/aks/workload-identity-overview"&gt;Workload identity&lt;/a&gt; works by linking a Kubernetes service account to a managed identity via federated credentials. This establishes trust between a specific Kubernetes cluster + namespace + service account and a managed identity. An identity no longer has to be assigned to AKS nodes like in aad-pod-identity.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a user-assigned managed identity and give it some role assignment over Azure resources or resource group, so it can manage it. Here is modified Terraform from above.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;### variables.tf
variable "pool_name" {}

variable "ado_token" {}

variable "replicas" {
  default = 1
}

variable "requests" {
  default = {
    cpu    = "100m"
    memory = "512Mi"
  }
}

variable "limits" {
  default = {
    memory = "2Gi"
  }
}

variable "disk_space_limit" {
  default = "4Gi"
}

variable "location" {
  default = "westeurope"
}

variable "managed_identity_scope {}

# Take this from your AKS deployment
variable "aks_oidc_issuer_url" {}

locals {
  work_dir  = "/mnt/work"
  ado_agent = "ado-agent-${var.pool_name}"
}

### providers.tf
terraform {
  &amp;lt;backend_configuration&amp;gt;
  required_providers {
    helm = {
      source  = "hashicorp/helm"
      version = "&amp;lt;3.0.0"
    }
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "&amp;lt;3.0.0"
    }
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "&amp;lt;4.0.0"
    }
  }
}

provider "azurerm" {
  features {}
}

### azure.tf
resource "azurerm_resource_group" "group" {
  name     = "rg-identity-${var.pool_name}"
  location = var.location
}

resource "azurerm_user_assigned_identity" "identity" {
  name                = "mi-${var.pool_name}"
  resource_group_name = azurerm_resource_group.group.name
  location            = var.location
}

# This resource will establish trust between Kubernetes cluster, namespace and service account. 
# Only pods assigned that service account will be able to obtain an access token from Azure for this corresponding managed identity.
resource "azurerm_federated_identity_credential" "identity" {
  resource_group_name = azurerm_user_assigned_identity.identity.resource_group_name
  parent_id           = azurerm_user_assigned_identity.identity.id
  name                = "${kubernetes_namespace_v1.namespace.metadata[0].name}-${kubernetes_service_account_v1.service_account.metadata[0].name}"
  subject             = "system:serviceaccount:${kubernetes_namespace_v1.namespace.metadata[0].name}-${kubernetes_service_account_v1.service_account.metadata[0].name}"
  audience            = ["api://AzureADTokenExchange"]
  issuer              = var.aks_oidc_issuer_url
}

# Give the managed identity some role over a resource group it will manage. 
# This does not have to be done in Terraform if you don't have permissions to do it.
resource "azurerm_role_assignment" "deployment_group" {
  scope                = var.managed_identity_scope
  role_definition_name = "Contributor"
  principal_id         = azurerm_user_assigned_identity.identity.principal_id
}

### kubernetes.tf
resource "kubernetes_namespace_v1" "namespace" {
  metadata {
    name = "ado-agents-${var.pool_name}"
  }
}

resource "kubernetes_secret_v1" "env_vars" {
  metadata {
    name      = local.ado_agent
    namespace = kubernetes_namespace_v1.namespace.metadata[0].name
  }

  data = {
    AZP_URL   = "https://dev.azure.com/&amp;lt;org_name&amp;gt;"
    AZP_TOKEN = var.ado_token
    AZP_POOL  = var.pool_name
    AZP_WORK  = local.work_dir
  }
}

# Pods would normally use namespace default service account.
# Instead, we create a specific service account for these pods and link it to Azure. 
# You do not have to automount service account token. 
# Workload Identity will still mount a signed token file different to normal Kubernetes Service account token file. 
# This token can be exchanged with Azure for an Azure access token.
resource "kubernetes_service_account_v1" "service_account" {
  metadata {
    namespace = kubernetes_namespace_v1.namespace.metadata[0].name
    name      = local.ado_agent
    # These annotations are optional but they will set the "default" managed identity for this service account. 
    # This is helpful if you plan to have 1:N SA-&amp;gt;MI federated credentials.
    annotations = {
      "azure.workload.identity/client-id" = azurerm_user_assigned_identity.identity.client_id
      "azure.workload.identity/tenant-id" = azurerm_user_assigned_identity.identity.tenant_id
    }
  }
  automount_service_account_token = false
}

resource "kubernetes_deployment_v1" "ado_agent" {
  metadata {
    name      = local.ado_agent
    namespace = kubernetes_namespace_v1.namespace.metadata[0].name
  }

  spec {
    replicas = var.replicas

    selector {
      match_labels = {
        app = local.ado_agent
      }
    }

    template {
      metadata {
        # This label must be specified so workload identity add-on is able to process the pod.
        labels = {
          app                           = local.ado_agent
          "azure.workload.identity/use" = "true"
        }
        # While workload identity works by token-exchange method by default, this is not supported by Azure terraform provider. 
        # If we want to use managed identity authentication in our agent, we must inject a sidecar. 
        # This extra container will spoof a managed identity endpoint and provide us with an access token.
        annotations = {
          "azure.workload.identity/inject-proxy-sidecar" : "true"
        }
      }

      spec {
        # Link the service account to deployment, so all pods created by the deployment have the SA token.
        service_account_name            = kubernetes_service_account_v1.service_account.metadata[0].name
        automount_service_account_token = false
        container {
          name  = local.ado_agent
          image = "&amp;lt;acr_name&amp;gt;.azurecr.io/&amp;lt;ado_agent_image&amp;gt;"

          env_from {
            secret_ref {
              name = kubernetes_secret_v1.env_vars.metadata[0].name
            }
          }

          volume_mount {
            name       = "temp-data"
            mount_path = local.work_dir
          }

          resources {
            limits   = var.limits
            requests = var.requests
          }
        }

        volume {
          name = "temp-data"

          empty_dir {
            size_limit = var.disk_space_limit
          }
        }
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Once deployed, your agent pod will have an extra sidecar container and init container:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PS&amp;gt; kubectl get pod
NAME                                     READY   STATUS    RESTARTS   AGE
ado-agent-&amp;lt;pool_name&amp;gt;-7b8d79dd65-hwb22   2/2     Running   0          3d22h

PS&amp;gt; kubectl describe pod
Name:         ado-agent-&amp;lt;pool_name&amp;gt;-7b8d79dd65-hwb22
Namespace:    ado-agents-&amp;lt;pool_name&amp;gt;
(...)
Labels:       app=ado-agent-&amp;lt;pool_name&amp;gt;
              azure.workload.identity/use=true
Annotations:  azure.workload.identity/inject-proxy-sidecar: true
(...)
Init Containers:
  azwi-proxy-init:
    Image:          mcr.microsoft.com/oss/azure/workload-identity/proxy-init:v1.0.0
    (...)
    Environment:
      PROXY_PORT:                  8000
      AZURE_CLIENT_ID:             &amp;lt;managed_identity_client_id&amp;gt;
      AZURE_TENANT_ID:             &amp;lt;managed_identity_tenant_id&amp;gt;
      AZURE_FEDERATED_TOKEN_FILE:  /var/run/secrets/azure/tokens/azure-identity-token
      AZURE_AUTHORITY_HOST:        https://login.microsoftonline.com/
    Mounts:
      /var/run/secrets/azure/tokens from azure-identity-token (ro)
Containers:
  ado-agent-&amp;lt;pool_name&amp;gt;:
    Image:          &amp;lt;acr_name&amp;gt;.azurecr.io/&amp;lt;ado_agent_image&amp;gt;
    (...)
    Environment Variables from:
      ado-agent-&amp;lt;pool_name&amp;gt;  Secret  Optional: false
    Environment:
      AZURE_CLIENT_ID:             &amp;lt;managed_identity_client_id&amp;gt;
      AZURE_TENANT_ID:             &amp;lt;managed_identity_tenant_id&amp;gt;
      AZURE_FEDERATED_TOKEN_FILE:  /var/run/secrets/azure/tokens/azure-identity-token
      AZURE_AUTHORITY_HOST:        https://login.microsoftonline.com/
    Mounts:
      /mnt/work from temp-data (rw)
      /var/run/secrets/azure/tokens from azure-identity-token (ro)
  azwi-proxy:
    Image:         mcr.microsoft.com/oss/azure/workload-identity/proxy:v1.0.0
    (...)
    Port:          8000/TCP
    Host Port:     0/TCP
    Args:
      --proxy-port=8000
      --log-level=info
    Environment:
      AZURE_CLIENT_ID:             &amp;lt;managed_identity_client_id&amp;gt;
      AZURE_TENANT_ID:             &amp;lt;managed_identity_tenant_id&amp;gt;
      AZURE_FEDERATED_TOKEN_FILE:  /var/run/secrets/azure/tokens/azure-identity-token
      AZURE_AUTHORITY_HOST:        https://login.microsoftonline.com/
    Mounts:
      /var/run/secrets/azure/tokens from azure-identity-token (ro)
(...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Obtaining an access token using workload identity:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Guides &lt;a href="https://learn.microsoft.com/en-us/azure/aks/workload-identity-overview#how-it-works"&gt;here&lt;/a&gt; and &lt;a href="https://azure.github.io/azure-workload-identity/docs/introduction.html#how-it-works"&gt;here&lt;/a&gt; describe how access tokens are obtained via workload identity. Since we use a sidecar to mimic an IMDS endpoint, getting an access token is described &lt;a href="https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-powershell"&gt;here&lt;/a&gt;. Let's try it using kubectl exec.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PS&amp;gt; kubectl exec -it ado-agent-&amp;lt;pool_name&amp;gt;-7b8d79dd65-hwb22 -- pwsh
Defaulted container "ado-agent-&amp;lt;pool_name&amp;gt;" out of: ado-agent-&amp;lt;pool_name&amp;gt;, azwi-proxy, azwi-proxy-init (init)
PowerShell 7.2.6
Copyright (c) Microsoft Corporation.

https://aka.ms/powershell
Type 'help' to get help.

PS /azp&amp;gt; Invoke-WebRequest -Uri 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&amp;amp;resource=https%3A%2F%2Fmanagement.azure.com%2F' -Headers @{Metadata="true"}

StatusCode        : 200
StatusDescription : OK
Content           : {"access_token":"eyJ0eXA...
RawContent        : HTTP/1.1 200 OK
                    Server: azure-workload-identity/proxy/v1.0.0 (linux/amd64) 9893baf/2023-03-27-20:58
                    Date: Tue, 23 May 2023 09:17:48 GMT
                    Content-Type: application/json
                    Content-Length: 1707

                    {"access_to…
Headers           : {[Server, System.String[]], [Date, System.String[]], [Content-Type, System.String[]], [Content-Length, System.String[]]}
Images            : {}
InputFields       : {}
Links             : {}
RawContentLength  : 1707
RelationLink      : {}


PS /azp&amp;gt; (Invoke-RestMethod -Uri 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&amp;amp;resource=https%3A%2F%2Fmanagement.azure.com%2F' -Headers @{Metadata="true"}).access_token
eyJ0eXA...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--uD_lMSS4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vhypfmg0zm1d5lpr8qfu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--uD_lMSS4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/vhypfmg0zm1d5lpr8qfu.png" alt="workload-identity-get-token" width="662" height="232"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We are now ready to run a Terraform pipeline against this new agent pool.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Running a pipeline:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Create a Terraform repo in Azure DevOps to create some example resources.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;### main.tf
terraform {
  &amp;lt;backend_configuration&amp;gt;
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "&amp;lt;4.0.0"
    }
  }
}

provider "azurerm" {
  # We can just use this flag to authenticate
  use_msi         = true
  subscription_id = "&amp;lt;subscription_id&amp;gt;"
  features {}
}

resource "azurerm_application_insights" "example" {
  name                = "appins-omtest1"
  location            = "westeurope"
  resource_group_name = "rg-identity-omtest1"
  application_type    = "web"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Create YAML pipeline to run Terraform:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;name: $(Rev:rr)
trigger:
- main
pool: omtest1

steps:
- pwsh: |
    terraform init
    terraform plan -out tfplan
    terraform apply -auto-approve tfplan
  workingDirectory: $(Build.SourcesDirectory)/terraform-deploy-with-managed-identity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Result:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ON3F8gC2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5fxokarigyi28vtmndi5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ON3F8gC2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5fxokarigyi28vtmndi5.png" alt="pipeline-result" width="800" height="903"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--hAHX6BSZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3r8d1nay3597qo6tp8yi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--hAHX6BSZ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3r8d1nay3597qo6tp8yi.png" alt="pipeline-result-azure" width="800" height="194"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;In summary:&lt;/strong&gt;&lt;br&gt;
We have our ADO agent running in AKS and use workload + managed identity combination to achieve keyless authentication to Azure for running Terraform pipelines.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>devops</category>
      <category>kubernetes</category>
      <category>docker</category>
    </item>
    <item>
      <title>Deploy linkerd to Kubernetes using Helm and Terraform</title>
      <dc:creator>Oskar Mamrzynski</dc:creator>
      <pubDate>Sun, 31 Jul 2022 10:13:00 +0000</pubDate>
      <link>https://dev.to/oskarm93/deploy-linkerd-to-kubernetes-using-helm-and-terraform-461h</link>
      <guid>https://dev.to/oskarm93/deploy-linkerd-to-kubernetes-using-helm-and-terraform-461h</guid>
      <description>&lt;p&gt;So I've been trying to install Linkerd service mesh on our AKS clusters this week. There are a few options to install: using their command line utility or using Helm.&lt;/p&gt;

&lt;p&gt;Command line &lt;a href="https://linkerd.io/2.11/getting-started/"&gt;tool&lt;/a&gt; works well, it will generate CA and issuer certificates for you. However, you have to download it when deploying via a pipeline, and there's a small problem with running &lt;code&gt;linkerd check --pre&lt;/code&gt; - it will only succeed on first installation and fail after linkerd is installed.&lt;/p&gt;

&lt;p&gt;So I chose to install it with Helm, along with linkerd-cni and linkerd viz dashboard. You can find all their charts &lt;a href="https://artifacthub.io/packages/helm/linkerd2/linkerd2"&gt;here&lt;/a&gt;. I don't use Helm command line in pipelines, preferring to use Terraform &lt;a href="https://registry.terraform.io/providers/hashicorp/helm/latest/docs"&gt;helm provider&lt;/a&gt;. This is because I can install and configure many Helm releases using a single set of Terraform commands.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generating CA and issuer certs&lt;/strong&gt;&lt;br&gt;
You need to generate your own CA and issuer certificates if you choose to install linkerd with Helm. They give you &lt;a href="https://linkerd.io/2.11/tasks/generate-certificates/"&gt;instructions&lt;/a&gt; on how to generate them using &lt;code&gt;step&lt;/code&gt; command line utility. However, we can also generate them using Terraform &lt;a href="https://registry.terraform.io/providers/hashicorp/tls/latest/docs"&gt;tls provider&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "tls_private_key" "ca" {
  algorithm   = "ECDSA"
  ecdsa_curve = "P256"
}

resource "tls_self_signed_cert" "ca" {
  private_key_pem       = tls_private_key.ca.private_key_pem
  is_ca_certificate     = true
  set_subject_key_id    = true
  validity_period_hours = 87600
  allowed_uses = [
    "cert_signing",
    "crl_signing"
  ]
  subject {
    common_name = "root.linkerd.cluster.local"
  }
}

resource "tls_private_key" "issuer" {
  algorithm   = "ECDSA"
  ecdsa_curve = "P256"
}

resource "tls_cert_request" "issuer" {
  private_key_pem = tls_private_key.issuer.private_key_pem
  subject {
    common_name = "identity.linkerd.cluster.local"
  }
}

resource "tls_locally_signed_cert" "issuer" {
  cert_request_pem      = tls_cert_request.issuer.cert_request_pem
  ca_private_key_pem    = tls_private_key.ca.private_key_pem
  ca_cert_pem           = tls_self_signed_cert.ca.cert_pem
  is_ca_certificate     = true
  set_subject_key_id    = true
  validity_period_hours = 8760
  allowed_uses = [
    "cert_signing",
    "crl_signing"
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Configuring linkerd Helm releases&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Now all I need is to add 3 &lt;code&gt;helm_release&lt;/code&gt; resources for linkerd CNI, linkerd and linkerd viz dashboard, providing outputs of certificate resources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource "helm_release" "linkerd_cni" {
  name             = "linkerd-cni"
  namespace        = "linkerd-helm"
  repository       = "https://helm.linkerd.io/stable"
  chart            = "linkerd2-cni"
  version          = "2.11.4"
  create_namespace = true
}

resource "helm_release" "linkerd" {
  name             = "linkerd"
  namespace        = helm_release.linkerd_cni.namespace
  repository       = "https://helm.linkerd.io/stable"
  chart            = "linkerd2"
  version          = "2.11.4"
  create_namespace = false
  set {
    name  = "cniEnabled"
    value = "true"
  }
  set {
    name  = "identityTrustAnchorsPEM"
    value = tls_locally_signed_cert.issuer.ca_cert_pem
  }
  set {
    name  = "identity.issuer.tls.crtPEM"
    value = tls_locally_signed_cert.issuer.cert_pem
  }
  set {
    name  = "identity.issuer.tls.keyPEM"
    value = tls_private_key.issuer.private_key_pem
  }
}

resource "helm_release" "linkerd_viz" {
  name             = "linkerd-viz"
  namespace        = helm_release.linkerd_cni.namespace
  repository       = "https://helm.linkerd.io/stable"
  chart            = "linkerd-viz"
  version          = "2.11.4"
  create_namespace = false
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Namespaces&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Helm needs a namespace to create the release in, but you cannot pre-create default linkerd namespaces. Their Helm chart deploys and configures them with appropriate annotations. Instead I create tell Helm resource to create a new namespace called &lt;code&gt;linkerd-helm&lt;/code&gt; which houses only Helm releases but no linkerd resources. I also use implicit dependency on &lt;code&gt;helm_release_linkerd_cni.namespace&lt;/code&gt; so CNI is deployed before linkerd and viz dashboard.&lt;/p&gt;

&lt;p&gt;Once I run through the standard terraform init, plan, apply I end up with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;kubectl get namespaces
NAME                       STATUS   AGE
&amp;lt;truncated&amp;gt;
linkerd                    Active   2d19h
linkerd-cni                Active   2d19h
linkerd-helm               Active   2d19h
linkerd-viz                Active   37h

helm ls --namespace linkerd-helm
NAME            NAMESPACE       REVISION        UPDATED                                 STATUS          CHART                   APP VERSION
linkerd         linkerd-helm    4               2022-07-29 09:59:38.7045481 +0000 UTC   deployed        linkerd2-2.11.4         stable-2.11.4
linkerd-cni     linkerd-helm    1               2022-07-28 14:40:00.021646641 +0000 UTC deployed        linkerd2-cni-2.11.4     stable-2.11.4
linkerd-viz     linkerd-helm    1               2022-07-29 20:26:34.495285272 +0000 UTC deployed        linkerd-viz-2.11.4      stable-2.11.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;P.S. Caveat for Azure Kubernetes Service: Do not use &lt;a href="https://docs.microsoft.com/en-us/azure/aks/open-service-mesh-about"&gt;Open Service Mesh&lt;/a&gt; add-on if you want to install linkerd. It kept crashing for me.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>terraform</category>
      <category>helm</category>
    </item>
    <item>
      <title>Obtain Azure access token from a local Docker container</title>
      <dc:creator>Oskar Mamrzynski</dc:creator>
      <pubDate>Sat, 30 Jul 2022 02:43:59 +0000</pubDate>
      <link>https://dev.to/oskarm93/obtain-azure-access-token-from-a-local-docker-container-35df</link>
      <guid>https://dev.to/oskarm93/obtain-azure-access-token-from-a-local-docker-container-35df</guid>
      <description>&lt;p&gt;Apps talking to Azure need to obtain an access token at runtime. For example, reading secrets from &lt;a href="https://docs.microsoft.com/en-us/aspnet/core/security/key-vault-configuration?view=aspnetcore-6.0#use-managed-identities-for-azure-resources" rel="noopener noreferrer"&gt;Azure Key Vault&lt;/a&gt; as part of your configuration. There's a whole host of Azure services you can talk to using tokens: Azure Key Vault, Azure App Config, Azure SQL, Azure Event Hubs, Azure Service Bus, Azure Storage Account etc.&lt;/p&gt;

&lt;p&gt;All follow the same basic flow: obtain an access token as an Azure Identity and attach that token to API requests for that Azure service. In .NET apps you can use the new &lt;a href="https://www.nuget.org/packages/Azure.Identity" rel="noopener noreferrer"&gt;Azure.Identity&lt;/a&gt; library and it's &lt;code&gt;DefaultAzureCredential&lt;/code&gt; type.&lt;/p&gt;

&lt;p&gt;This type will automatically try to obtain an Azure access token using various methods, but 3 are of particular interest:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Environment variables - for logging in as a Service Principal using Client ID, Client Secret and Tenant ID environment variables&lt;/li&gt;
&lt;li&gt;Managed Identity - for logging in using a system or user-assigned managed identity in Azure systems like Azure App Service, Azure Kubernetes Service, Azure VM etc.&lt;/li&gt;
&lt;li&gt;Azure CLI - for logging in on your local machine for development work&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Q: So how am I supposed to log in to Azure so that my app can obtain tokens?&lt;/strong&gt;&lt;br&gt;
A: I tell devs: For local development log in to Azure CLI with your normal user account. It has Contributor over your Dev/Test subscription and you can access secrets and configuration from their Dev/Test Key Vaults.&lt;br&gt;
For staging and production running in Azure (in our case Docker containers running on AKS) we use User-Assigned Managed Identity and &lt;a href="https://github.com/Azure/aad-pod-identity" rel="noopener noreferrer"&gt;aad-pod-identity&lt;/a&gt; project. This managed identity has least-privilege permissions over staging and production environments to do it's job at runtime.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: I can obtain tokens locally using Azure CLI and Azure.Identity library when I run on the host machine, but not when inside Docker container because it doesn't have Azure CLI installed! What do I do?&lt;/strong&gt;&lt;br&gt;
A: This has already been asked about by many people &lt;a href="https://github.com/Azure/azure-sdk-for-net/issues/19167" rel="noopener noreferrer"&gt;here&lt;/a&gt; with various interesting solutions &lt;a href="https://github.com/ClrCoder/ClrPro.AzureFX" rel="noopener noreferrer"&gt;here&lt;/a&gt; and &lt;a href="https://github.com/jongio/azureclicredentialcontainer" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I decided to write another solution of my own because I would like locally run Docker containers to be as closer to staging as possible, i.e. use Managed Identity flow to obtain Azure access tokens. Obviously I cannot use Managed Identities on a laptop but I can spoof the process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Let's reproduce the problem with least code.&lt;/strong&gt;&lt;br&gt;
Made a little &lt;a href="https://github.com/xenalite/local-docker-azure-token-endpoint/tree/main/src/SampleConsumerApp" rel="noopener noreferrer"&gt;sample console app&lt;/a&gt; that connects to Azure Key Vault, adds secrets to &lt;code&gt;IConfiguration&lt;/code&gt; and then prints all config to console (not a great practice in prod btw!).&lt;/p&gt;

&lt;p&gt;Running it directly on my laptop with &lt;code&gt;dotnet&lt;/code&gt; is fine because my laptop has Azure CLI installed, and I am logged into it.&lt;/p&gt;

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

$env:AZURE_KEY_VAULT_URI="https://&amp;lt;redacted&amp;gt;.vault.azure.net/"
dotnet run .\SampleConsumerApp.csproj
mysecret=secretsauce (AzureKeyVaultConfigurationProvider)


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

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Q: What happens when I build a Docker image and try to run it?&lt;/strong&gt;&lt;/p&gt;

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

docker build -t dev:consumer -f .\Dockerfile .
&amp;lt;truncated&amp;gt;
docker run -e AZURE_KEY_VAULT_URI=https://&amp;lt;redacted&amp;gt;.vault.azure.net/ dev:consumer
Unhandled exception. Azure.Identity.CredentialUnavailableException: DefaultAzureCredential failed to retrieve a token from the included credentials. See the troubleshooting guide for more information. https://aka.ms/azsdk/net/identity/defaultazurecredential/troubleshoot
- EnvironmentCredential authentication unavailable. Environment variables are not fully configured. See the troubleshooting guide for more information. https://aka.ms/azsdk/net/identity/environmentcredential/troubleshoot
- ManagedIdentityCredential authentication unavailable. Multiple attempts failed to obtain a token from the managed identity endpoint.
- Operating system Linux 5.10.102.1-microsoft-standard-WSL2 #1 SMP Wed Mar 2 00:30:59 UTC 2022 isn't supported.
- Stored credentials not found. Need to authenticate user in VSCode Azure Account. See the troubleshooting guide for more information. https://aka.ms/azsdk/net/identity/vscodecredential/troubleshoot
- Azure CLI not installed
- PowerShell is not installed.
&amp;lt;truncated&amp;gt;


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

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Q: Why use Azure CLI at all? Why not just configure my Docker container with environment variables for ClientId, ClientSecret and TenantID?&lt;/strong&gt;&lt;br&gt;
A: While I give devs a service principal for Dev/Test to use in pipelines, this principal's credentials are not supposed to be used for local development. Configuring SP credentials with environment variables would prevent us from rotating them frequently too. Not everyone has a service principal to hand either!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: I don't want to install Azure CLI into my dev Docker images just to obtain a token. So what's next?&lt;/strong&gt;&lt;br&gt;
A: It seems getting an access token using a Managed Identity token endpoint is the easiest because you can just &lt;a href="https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-curl" rel="noopener noreferrer"&gt;curl a URL&lt;/a&gt; and get a token. One problem is that nearly all authentication libraries hardcode this URL as &lt;code&gt;http://169.254.169.254/metadata/identity/oauth2/token?&amp;lt;uri_params&amp;gt;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;So, I am going to create a "proxy" managed identity token endpoint and return a token just like the app expects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: How will this proxy provider get a token in the first place?&lt;/strong&gt;&lt;br&gt;
A: This proxy provider is a tiny ASP.NET Core web API and can use Azure.Identity library just like the sample console app. So let's make it get this token from Azure CLI. I can install Azure CLI into that token provider since it's only a development tool and not my app's production image. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Q: Ok but how would the Azure CLI inside proxy's Docker container get an access token? You'd have to log into Azure CLI every time you start the container!&lt;/strong&gt;&lt;br&gt;
A: I can mount my laptop's &lt;code&gt;.azure&lt;/code&gt; folder as a volume into the token provider's container. As long as I am logged into Azure CLI on my laptop I can obtain tokens using Azure CLI inside proxy's container! Combine that with a little web API and we're in business.&lt;/p&gt;

&lt;p&gt;If this is confusing, here's a handy diagram:&lt;br&gt;
&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6t5kqp5271ji3bpamktc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6t5kqp5271ji3bpamktc.png" alt="diagram1"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is a small problem with this that someone already spotted. It only works with Azure CLI version up to 2.29 as described &lt;a href="https://github.com/jongio/azureclicredentialcontainer/issues/2" rel="noopener noreferrer"&gt;here&lt;/a&gt;. So you'd need to install that version of Azure CLI on host machine and in the token provider's Docker image.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/xenalite/local-docker-azure-token-endpoint/tree/main/src/LocalDockerAzureTokenEndpoint" rel="noopener noreferrer"&gt;Here&lt;/a&gt; is the least effort code for this token provider. Let's build it and run it.&lt;/p&gt;

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

docker build -t dev:token -f .\Dockerfile .
&amp;lt;truncated&amp;gt;
docker run -e ASPNETCORE_URLS=http://+:80 -p 8080:80 -v C:\Users\OskarMamrzynski\.azure:/root/.azure:rw dev:token
&amp;lt;truncated&amp;gt;
      Now listening on: http://[::]:80
&amp;lt;truncated&amp;gt;
      Request finished HTTP/1.1 GET http://localhost:8080/metadata/identity/oauth2/token?api-version=2018-02-01&amp;amp;resource=https%3A%2F%2Fmanagement.azure.com%2F - - - 200 - application/json;+charset=utf-8 1196.6707ms


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

&lt;/div&gt;

&lt;p&gt;My container listens on localhost:8080 on the host machine, so we can curl it and obtain a token impersonating the user logged in with Azure CLI:&lt;/p&gt;

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

curl 'http://localhost:8080/metadata/identity/oauth2/token?api-version=2018-02-01&amp;amp;resource=https%3A%2F%2Fmanagement.azure.com%2F' -H Metadata:true -s
{"access_token":"eyJ0&amp;lt;redacted&amp;gt;","refresh_token":"","expires_in":5335.8911044,"expires_on":1659150718,"not_before":1659145382,"resource":"https://management.azure.com/","token_type":"Bearer"}


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

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Q: Fine, but your proxy container is still not using that &lt;code&gt;169.254.169.254&lt;/code&gt; IP address. How are you going to get that working?&lt;/strong&gt;&lt;br&gt;
A: We can create a local Docker network and assign a static IP to our token provider container. Consumer app would also run in this network.&lt;/p&gt;

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

docker network create omtest --subnet=169.254.0.0/16
&amp;lt;truncated&amp;gt;
docker run -d --net omtest --ip 169.254.169.254 -e ASPNETCORE_URLS=http://+:80 -p 8080:80 -v C:\Users\OskarMamrzynski\.azure:/root/.azure:rw dev:token
&amp;lt;truncated&amp;gt;
docker run -it -e AZURE_KEY_VAULT_URI=https://&amp;lt;redacted&amp;gt;.vault.azure.net/ --net omtest dev:consumer
mysecret=secretsauce (AzureKeyVaultConfigurationProvider)


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

&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;In summary&lt;/strong&gt;&lt;br&gt;
You can use Azure CLI in conjunction with this token provider container just always running there in the background on your laptop. Any Docker containers you spin up locally will be able to use this spoofed token provider endpoint as long as they are on the same Docker network. This makes it appear as if the app is assigned a Managed Identity.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>azure</category>
      <category>dotnet</category>
    </item>
  </channel>
</rss>
