<?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: Kai Walter</title>
    <description>The latest articles on DEV Community by Kai Walter (@kaiwalter).</description>
    <link>https://dev.to/kaiwalter</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%2F155914%2Fc03fdb60-3b4e-4dcd-9b3d-ddd433e2caf6.jpg</url>
      <title>DEV Community: Kai Walter</title>
      <link>https://dev.to/kaiwalter</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kaiwalter"/>
    <language>en</language>
    <item>
      <title>Challenging n8n AI Agent with a personal productivity flow</title>
      <dc:creator>Kai Walter</dc:creator>
      <pubDate>Sun, 12 Oct 2025 16:30:04 +0000</pubDate>
      <link>https://dev.to/kaiwalter/challenging-n8n-ai-agent-with-a-personal-productivity-flow-2a17</link>
      <guid>https://dev.to/kaiwalter/challenging-n8n-ai-agent-with-a-personal-productivity-flow-2a17</guid>
      <description>&lt;p&gt;I am out, mostly in the mornings for a walk or run, and I just want to drop a thought or a task immediately. Sometimes even complete sections of an upcoming presentation. Or rushing between meetings, the same: Just drop a voice recording and have it turned into a task or just as a note into my email inbox.&lt;/p&gt;

&lt;p&gt;That is my use case. Plain and simple.&lt;/p&gt;

&lt;p&gt;For that I started using &lt;a href="https://n8n.io" rel="noopener noreferrer"&gt;n8n&lt;/a&gt; a while back. A bit clumsy, but it worked. I had a flow running in the cloud, triggered by a new file in OneDrive, which downloaded the file, transcribed it using OpenAI Whisper API, then classified the intent using GPT-4.1-mini and based on that either created a task or sent an email to myself with the transcription.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In case that sounds familiar: Yes, I converted that flow with Dapr Agents already, decribed in &lt;a href="https://dev.to/kaiwalter/dipping-into-dapr-agentic-workflows-fbi"&gt;this post&lt;/a&gt; - so skip that part.&lt;/p&gt;
&lt;/blockquote&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%2Fgrj8h197h77vgn2liw2z.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%2Fgrj8h197h77vgn2liw2z.png" alt="Original n8n flow downloading, transcribing and spawning actions on a voice recording" width="800" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How it works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;on my Android phone I use the paid version of &lt;em&gt;Easy Voice Recorder Pro&lt;/em&gt; which allows to automatically upload into a predefined &lt;em&gt;OneDrive&lt;/em&gt; folder (which is &lt;code&gt;/Apps/Easy Voice Recorder Pro&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;the recording is downloaded by the n8n flow on a trigger, when a file is created in that folder&lt;/li&gt;
&lt;li&gt;before downloading, to be safe and not crash the transcription unnecessarily, the flow filters on &lt;code&gt;audio/x-wav&lt;/code&gt; or &lt;code&gt;audio/mpeg&lt;/code&gt; MIME types&lt;/li&gt;
&lt;li&gt;additionally, the flow downloads a prompt text file from OneDrive which contains the instructions for classifying the intent in the transcription; I wanted to be on OneDrive, so I can modify it easily without having to touch the flow&lt;/li&gt;
&lt;li&gt;then transcribe using OpenAI Whisper API&lt;/li&gt;
&lt;li&gt;with the transcription and the prompt run through a model like &lt;code&gt;GPT-4.1-MINI&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;that classification step also has access to a simple tool - referenced in the prompt: a list of relevant person and other entity names to make the transcription more precise&lt;/li&gt;
&lt;li&gt;based on the intent resolved then either create a task (using a webhook, as I did not want to mess around in our corporate environment) or just send an email to my corporate-self with the plain transcription&lt;/li&gt;
&lt;li&gt;as part of housekeeping, copy the file to an archive folder and delete the original&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That worked pretty well. I especially liked the capability of n8n to copy runtime data of a particular execution into the editor, which makes mapping and debugging so much easier. I moved the cloud-based flow to one to my own machines, so I could run it basically for free (download it, import it from file, rewire cloud credentials).&lt;/p&gt;

&lt;p&gt;Since the previous post I explored Dapr Agents some more and also did an exemplary implementation with &lt;a href="https://github.com/microsoft/agent-framework" rel="noopener noreferrer"&gt;Microsoft Agent Framework&lt;/a&gt; and &lt;a href="https://learn.microsoft.com/en-us/dotnet/aspire/" rel="noopener noreferrer"&gt;.NET Aspire&lt;/a&gt; in &lt;a href="https://github.com/KaiWalter/agent-framework-voice2action" rel="noopener noreferrer"&gt;this repository&lt;/a&gt;. Just to get a better understanding on the relevance of prompts, tools and the clean segregation between orchestration and agents or MCP servers.&lt;/p&gt;

&lt;p&gt;During all that time I kept the n8n flow running as I (myself) was not able to muster comparable fulfillment reliability with the other implementations. Then a few days ago I watched a guy using an "AI Agent" node in n8n which exaclty looked like something to even more simplify my flow while adding more capabilities and flexibility.&lt;/p&gt;

&lt;h2&gt;
  
  
  Improving the flow with n8n AI Agent&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;In the previous flow I ran the transcription through a model to classify the intent. From that structured output I wired up a set of tasks to fulfill the intent. That worked, but was a bit clumsy and not very flexible. If I wanted to add more capabilities, I had to modify the flow and add more branches.&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%2Faa4f1u5btx3qk1f6whcm.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%2Faa4f1u5btx3qk1f6whcm.png" alt="n8n flow with LLM classification and subsequent tasks" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With the AI Agent node I can do all that in one single node. The node has access to the transcription, the prompt and a set of tools. Based on that it can decide what to do.&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%2Fryyqx1dgefk3ptln57pc.jpg" 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%2Fryyqx1dgefk3ptln57pc.jpg" alt="n8n AI Agent node with all its capabilities" width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As most of the tools connected describe their capabilities to a certain proper degree, the prompt can be simplified. I just have to make sure that the agent understands what it can do and how to use the tools. The prompt I am using is this one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;From the voice recorded transcript between these dashed lines, determine the user's intent.
&lt;span class="p"&gt;
---
&lt;/span&gt;
&lt;span class="gu"&gt;## {{ $json.text }}&lt;/span&gt;
&lt;span class="p"&gt;
---
&lt;/span&gt;
Examples
&lt;span class="p"&gt;
-&lt;/span&gt; create a todo item
&lt;span class="p"&gt;-&lt;/span&gt; synchronize tasks
&lt;span class="p"&gt;-&lt;/span&gt; unknown

&lt;span class="gu"&gt;## PREPARATION&lt;/span&gt;

MUST: Determine current date and time to have a reference point for relative date and time expressions in the user's intent.

MUST: Determine relevant person names and only use those to spell names correctly if those are stated in the transcript. Do not add to intent or content.

&lt;span class="gu"&gt;## PROCESSING&lt;/span&gt;

RULE: For the intent to create a to-do item determine a title, try to determine a due date and time for the task and also a reminder date and time. When due dates or reminders are expressed relatively (e.g. next Monday) use available tools to convert into ISO datetime and assume a CET or CEST time zone. Omit reminder or due date in the tool call, if not determined. Do not pass empty strings.

MUST: Creating a todo item makes it mandatory to synchronize todo list. For that synchronization the id of the created todo item is required.

RULE: For the intent to synchronize to-do items or tasks retrieve all open to-do items and initiate a synchronization with the id of each to-do item.

RULE: Only if the intent bears more content than a simple command like "synchronize tasks", archive the transcript and archive the recording file.

RULE: If user's intent can be fulfilled with one of the tools and the input information for the tool is sufficient, do not ask for approval and just proceed and conclude the request.

&lt;span class="gu"&gt;## CONCLUSION&lt;/span&gt;

MUST: In any circumstance conclude by sending a summary email.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Some tweaks
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;for archiving the transcript I used the OneDrive upload file node and hence I had to adjust the tool description&lt;/li&gt;
&lt;li&gt;same for archiving the recording file, where I used the OneDrive copy file node&lt;/li&gt;
&lt;li&gt;I cannot (and shall not) connect n8n to my company resources (like to-do lists) directly, hence I use a webhook to trigger a protected synchronization flow to synchronize the tasks with my Outlook tasks&lt;/li&gt;
&lt;li&gt;I used a simple code node to return the current date and time in ISO format, so the agent can use that to resolve relative date and time expressions&lt;/li&gt;
&lt;li&gt;also I added a code node to provide a list of relevant person names, which the agent can use to spell names correctly in the transcription (yeah you get that with German names in an english recording)&lt;/li&gt;
&lt;li&gt;for a start I did not make use of agent memory as each individual flow is rather ephemeral at the moment&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Switching to Mistral API 🇪🇺
&lt;/h3&gt;

&lt;p&gt;For the agent I switched from OpenAI to Mistral API, as I wanted to try that out. With &lt;code&gt;mistral-medium-latest&lt;/code&gt; I was able to produce reliable results.&lt;/p&gt;

&lt;p&gt;There is no Mistral node for speech to text in n8n yet, but I was able to use the HTTP request node to call the API. Nice thing in n8n is, that HTTP request node has a notion of the many credentials n8n supports, so I could use my existing Mistral credentials without the need to figure out authentication headers and so on.&lt;/p&gt;

&lt;p&gt;Also here I observed, that the model is not so overly crucial to the result anymore. As with the other frameworks I tried, the prompt and the tools are much more relevant to get a good result.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cherry on top
&lt;/h3&gt;

&lt;p&gt;I particularly like the execution log of the AI Agent node. It shows the reasoning steps, the tool calls and the final response. That is super helpful to understand what is going on and why.&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%2Fl0r598h42pbe9l5w31hb.jpg" 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%2Fl0r598h42pbe9l5w31hb.jpg" alt="n8n Agent execution log" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;




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

&lt;p&gt;I like the simplicity of the new flow. It is much easier to understand and to modify. If I want to add more capabilities, I just have to add more tools and adjust the prompt. No need to modify the flow itself.&lt;/p&gt;

&lt;p&gt;In terms of implementation effort and speed I easily outpace my other agent framework implementations, although there I heavily build on specification driven agentic coding. To be fair I have to say, that with those I am mainly targeting enterprise scenarios, where many more aspects have to be considered. Anyway for this personal use case n8n is a great fit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Kudos to n8n !!!&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>productivity</category>
      <category>n8n</category>
    </item>
    <item>
      <title>Dipping into Dapr Agentic Workflows</title>
      <dc:creator>Kai Walter</dc:creator>
      <pubDate>Sun, 17 Aug 2025 16:26:10 +0000</pubDate>
      <link>https://dev.to/kaiwalter/dipping-into-dapr-agentic-workflows-fbi</link>
      <guid>https://dev.to/kaiwalter/dipping-into-dapr-agentic-workflows-fbi</guid>
      <description>&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;Since I first laid eyes and hands on &lt;a href="https://dapr.io" rel="noopener noreferrer"&gt;Dapr&lt;/a&gt; when it was created in 2019, I have been fascinated by the potential of building distributed applications with it. The Dapr team has been working hard to make it easier to build microservices and distributed systems ever since. Workflows had been added with release &lt;a href="https://blog.dapr.io/posts/2025/02/27/dapr-v1.15-is-now-available/#dapr-workflow-stable" rel="noopener noreferrer"&gt;1.15&lt;/a&gt; and AI agent support started showing up &lt;a href="https://www.cncf.io/blog/2025/03/12/announcing-dapr-ai-agents/" rel="noopener noreferrer"&gt;in spring 2025&lt;/a&gt;. When I learned about Dapr workflows and agents on this &lt;a href="https://youtu.be/VLRg4TKtLBc" rel="noopener noreferrer"&gt;episode&lt;/a&gt; just recently, I wanted to explore how Dapr can be used to create agentic workflows.&lt;/p&gt;

&lt;p&gt;I had been dabbling with some frameworks over the past few months - to one or the other degree:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/pydantic/pydantic-ai" rel="noopener noreferrer"&gt;Pydantic AI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/microsoft/semantic-kernel" rel="noopener noreferrer"&gt;Semantic Kernel&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/microsoft/autogen" rel="noopener noreferrer"&gt;AutoGen&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing really stuck with me. The challenge I wanted to solve:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I am out, mostly in the mornings for a walk or run, and I just want to drop a thought or a task immediately. Sometimes even complete sections of an upcoming presentation. Or rushing between meeting, the same: Just drop a voice recording and have it turned into a task or just as a note into my email inbox.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Finally, with &lt;a href="https://n8n.io" rel="noopener noreferrer"&gt;n8n&lt;/a&gt; I pushed myself into a working flow with a highly curated environment. I dropped that idea of developing the flow on my own for that moment, as the value of having such a flow outweighed the learning experience.&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%2Fgrj8h197h77vgn2liw2z.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%2Fgrj8h197h77vgn2liw2z.png" alt="Original n8n flow downloading, transcribing and spawning actions on a voice recording" width="800" height="283"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;How it works:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;on my Android phone I use the paid version of &lt;em&gt;Easy Voice Recorder Pro&lt;/em&gt; which allows to automatically upload into a predefined &lt;em&gt;OneDrive&lt;/em&gt; folder (which is &lt;code&gt;/Apps/Easy Voice Recorder Pro&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;the recording is downloaded by the n8n flow on a trigger, when a file is created in that folder&lt;/li&gt;
&lt;li&gt;before downloading, to be safe and not crash the transcription unnecessarily, the flow filters on &lt;code&gt;audio/x-wav&lt;/code&gt; or &lt;code&gt;audio/mpeg&lt;/code&gt; MIME types&lt;/li&gt;
&lt;li&gt;additionally, the flow downloads a prompt text file from OneDrive which contains the instructions for classifying the intent in the transcription; I wanted to be on OneDrive, so I can modify it easily without having to touch the flow&lt;/li&gt;
&lt;li&gt;then transcribe using OpenAI Whisper API&lt;/li&gt;
&lt;li&gt;with the transcription and the prompt run through a model like &lt;code&gt;GPT-4.1-MINI&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;that classification step also has access to a simple tool - referenced in the prompt: a list of relevant person and other entity names to make the transcription more precise&lt;/li&gt;
&lt;li&gt;based on the intent resolved then either create a task (using a webhook, as I did not want to mess around in our corporate environment) or just send an email to my corporate-self with the plain transcription&lt;/li&gt;
&lt;li&gt;as part of housekeeping, copy the file to an archive folder and delete the original&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That worked pretty well. I especially liked the capability of n8n to copy runtime data of a particular execution into the editor, which makes mapping and debugging so much easier. I moved the cloud-based flow so I could run it basically for free (download it, import it from file, rewire cloud credentials).&lt;/p&gt;

&lt;p&gt;Enough of n8n. A nice environment to get started quickly - without a doubt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Value proposition of Dapr Agents and Workflows for me
&lt;/h2&gt;

&lt;p&gt;This is what me got spending factor 3-4 more time into a Dapr based flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I like the &lt;strong&gt;code-first&lt;/strong&gt; approach with workflows and agents; for use cases we face in our company I additionally needed to understand what building and operating such a flow in a sustainable and scalable fashion entails&lt;/li&gt;
&lt;li&gt;with Dapr I get &lt;strong&gt;resource abstraction&lt;/strong&gt; - switch easily between &lt;em&gt;state&lt;/em&gt; and &lt;em&gt;pub/sub&lt;/em&gt; resource providers, e.g. from Redis to Azure Cosmos DB or Azure Service Bus, even locally from Redis to SQlite if required&lt;/li&gt;
&lt;li&gt;with Dapr I get &lt;strong&gt;observability&lt;/strong&gt; which I can hook easily into our environment&lt;/li&gt;
&lt;li&gt;with Dapr I achieve the desired &lt;strong&gt;separation of concerns&lt;/strong&gt; between the workflow and the agents; I can develop and deploy them independently&lt;/li&gt;
&lt;li&gt;with Dapr I can &lt;strong&gt;mix&lt;/strong&gt; in "classic" enterprise processing easily, I can mix languages among Dapr applications, e.g. Python for the agents and C# for the workflow&lt;/li&gt;
&lt;li&gt;and in the end I get &lt;strong&gt;scalability&lt;/strong&gt; with Dapr: it is &lt;strong&gt;intrinsic&lt;/strong&gt; to Dapr that for deterministic and non-deterministic workflows activities can be operated on multiple computing nodes easily, something that other frameworks do not necessarily provide out of the box&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What I wanted to do differently
&lt;/h2&gt;

&lt;p&gt;As seen above I implemented a rather deterministic flow with n8n. I wanted to explore how I can use Dapr agents and workflows to create a more agentic workflow, which is more flexible and can adapt to the situation at hand - making scaling up and bringing in new components easier. In essence this means:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;polling on OneDrive, downloading and transcribing the voice recording runs in a deterministic workflow&lt;/li&gt;
&lt;li&gt;transcript is then handed to LLM-orchestrated agents which have instructions to figure out what to do with the transcription&lt;/li&gt;
&lt;li&gt;instead of funneling all information into the flow, I want agents to make use of &lt;strong&gt;tools&lt;/strong&gt; (probably MCP servers in the future) to interact with the outside world; again here I think that Dapr can shine as I easily can wire up tools with other Dapr applications, either using pub/sub or service invocation&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  A look into the codebase
&lt;/h2&gt;

&lt;p&gt;The code can be found in my &lt;a href="https://github.com/KaiWalter/dapr-agent-flow/tree/2025-08-post" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;BIG FAT DISCLAIMER: This is a work in progress, I am still learning and exploring the capabilities of Dapr agents and workflows. The code is not production-ready and should be used for educational purposes only.&lt;br&gt;&lt;br&gt;
DISCLAIMER: Almost 95% of the code has been created with GitHub Copilot. I made this project into a "two birds, one stone" exercise as I was keen to created a larger codebase with AI support for a long time. I will share and link here learnings on my "me and my coding apprentice" journey sometime in the future. Let's just say: For me as an occasional coder, not versed in Python really, it would not have been possible to achieve that amount of &lt;a href="https://en.wikipedia.org/wiki/Function_point" rel="noopener noreferrer"&gt;function points&lt;/a&gt;, a measure we kids used some decades ago to measure the size of a software project, without AI support.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Top Level / Tier 1 Structure
&lt;/h3&gt;

&lt;p&gt;The structure leans into structures provided by &lt;a href="https://github.com/dapr/dapr-agents/tree/main/quickstarts" rel="noopener noreferrer"&gt;quickstart samples&lt;/a&gt;. Some polishing is still required, but I wanted to get the code out there to get feedback and learnings from the community.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.dapr.io/developing-applications/local-development/multi-app-dapr-run/multi-app-overview/" rel="noopener noreferrer"&gt;Dapr Multi-App Run&lt;/a&gt; file &lt;code&gt;master.yaml&lt;/code&gt; points to the top-level applications and entry points:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;services/ui/authenticator&lt;/strong&gt; : a small web UI that redirects into a MS Entra ID login which on callback serializes the access and refresh tokens into a Dapr state store;
from there token information is picked up to authenticate for OneDrive and OpenAI API calls by the other services;
basic idea is to make the login once and let the workflow processes run in the background without further interaction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;services/workflow/worker&lt;/strong&gt; : runs the main polling loop at a timed interval to kick off the workflow, and the workflows to come, with a pub/sub signal;
with that I achieve some loose coupling between the workflow and the main loop (instead of using child workflows or alike)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;services/workflow/worker_voice2action&lt;/strong&gt; : defines the deterministic steps of the main Voice-2-Action workflow;
schedules a new instance when receiving pub/sub event from the main worker &lt;strong&gt;services/workflow/worker&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;services/intent_orchestrator/app&lt;/strong&gt; : bringing a LLM orchestrator for intent processing into standby, waiting for pub/sub events from &lt;strong&gt;services/workflow/worker_voice2action&lt;/strong&gt; publish intent orchestrator activity&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;services/intent_orchestrator/agent_tasker&lt;/strong&gt; : participating in above orchestration as a utility agent which delivers information required for the flow like the transcript or time zone information&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;services/intent_orchestrator/agent_office_automation&lt;/strong&gt; : participating in above orchestration to fulfill all tasks which connect the flow to office automation, like creating tasks or sending emails&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;services/ui/monitor&lt;/strong&gt; : a small console app listening to and printing the LLM orchestration broadcast messages to allow for a better understanding of the flow; this is absolutely required to fine-tune the instructions to the orchestrator and the agents&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tier 2 Elements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;workflows/voicetoaction / voice2action_poll_orchestrator&lt;/strong&gt; : orchestrating the activities to list the files on OneDrive, marking new files and handing of each single file to child workflow ...&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;workflows/voicetoaction / voice2action_per_file_orchestrator&lt;/strong&gt; : ... orchestrating in sequential order: download recording, transcription, publish to intent workflow and then archive the file&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Tier 3 Elements
&lt;/h3&gt;

&lt;p&gt;On this level in folder &lt;strong&gt;activities&lt;/strong&gt; are workflow activities defined in modules which are referenced by deterministic workflows.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tier 4 Elements
&lt;/h3&gt;

&lt;p&gt;Folder &lt;strong&gt;services&lt;/strong&gt; directly contains helper services which are used by workflow activities or agents.&lt;/p&gt;

&lt;h3&gt;
  
  
  Other Elements
&lt;/h3&gt;

&lt;p&gt;Folder &lt;strong&gt;components&lt;/strong&gt; holds all Dapr resource components used by all applications. Important to note is, that &lt;strong&gt;state stores are segregated for their purpose&lt;/strong&gt;: for workflow state, for agent state and for token state. This is required as these state types require different configuration for prefixing state keys and the ability to hold actors.&lt;/p&gt;

&lt;p&gt;Folder &lt;strong&gt;models&lt;/strong&gt; contains common model definitions used by the workflow elements and agents.&lt;/p&gt;

&lt;h3&gt;
  
  
  PRD / requirements
&lt;/h3&gt;

&lt;p&gt;As stated above, I drove &lt;strong&gt;GitHub Copilot&lt;/strong&gt; for the majority of work. For that, most of the time, when not falling back into old habits, I used &lt;strong&gt;voice2action-requirements.md&lt;/strong&gt; PRD file to invoke feature implementation. So, most of my intentions I had with the flow are also documented there.&lt;/p&gt;

&lt;h3&gt;
  
  
  start.sh
&lt;/h3&gt;

&lt;p&gt;This script helps me to start the process with a clean state which makes debugging various issues, especially in the agent instructions sphere much easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sample Conversation
&lt;/h2&gt;

&lt;p&gt;This is what the conversation between the orchestrator and the agents looks like. In this run I put &lt;code&gt;audio_samples/sample-recording-3-send-email.mp3&lt;/code&gt; through the system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;== APP - monitor == 2025-08-18 08:50:44 INFO [monitor] IntentOrchestrator : ## Mission Briefing
== APP - monitor ==
== APP - monitor == We have received the following task:
== APP - monitor ==
== APP - monitor == Process voice transcription from [./.work/voice/sample-recording-3-send-email.json]. From first 2 sentences extract the user's intent and plan the next steps. Treat the remaining transcription text just as a note with no further intent to consider. Explicit user intent can be: create a task. If there is no intent, just send an email with the whole transcript.
== APP - monitor ==
== APP - monitor == ### Team of Agents
== APP - monitor == - TaskPlanner: Planner (Goal: Handle and provide all kind of input information e.g. voice recording transcript and provide additional reference information which are helpful to the process.)
== APP - monitor == - OfficeAutomation: Office Assistant (Goal: Handle all jobs that require interaction with personal productivity like sending emails or creating to-do tasks.)
== APP - monitor ==
== APP - monitor == ### Execution Plan
== APP - monitor == Here is the structured approach the team will follow to accomplish the task:
== APP - monitor ==
== APP - monitor == [{'step': 1, 'description': 'Read the content from the given voice transcription file: [./.work/voice/sample-recording-3-send-email.json] and load the data for further processing.', 'status': 'not_started', 'substeps': None}, {'step': 2, 'description': "Extract the first two sentences from the transcription to analyze the user's intent.", 'status': 'not_started', 'substeps': None}, {'step': 3, 'description': 'Determine if the extracted sentences indicate an explicit user intent, such as creating a task.', 'status': 'not_started', 'substeps': None}, {'step': 4, 'description': 'Plan the execution based on the identified intent.', 'status': 'not_started', 'substeps': [{'substep': 4.1, 'description': 'If there is a task creation intent, prepare to create a task using the specified task details.', 'status': 'not_started'}, {'substep': 4.2, 'description': 'If no explicit intent is identified, prepare the entire transcription to be sent as an email.', 'status': 'not_started'}]}, {'step': 5, 'description': 'Initiate the task creation process (if applicable), ensuring details are accurately recorded in the designated task management system.', 'status': 'not_started', 'substeps': None}, {'step': 6, 'description': "Prepare the transcription text for email composition if the user's intent is to send an email.", 'status': 'not_started', 'substeps': None}, {'step': 7, 'description': 'Send the email containing the full transcription after preparing the email draft.', 'status': 'not_started', 'substeps': None}, {'step': 8, 'description': 'Verify if all intended actions (task creation or email sending) have been completed successfully.', 'status': 'not_started', 'substeps': None}]
== APP - monitor ==
== APP - monitor == 2025-08-18 08:50:45 INFO [monitor] IntentOrchestrator : Read the content from the given voice transcription file and load the data for further processing.
== APP - monitor == 2025-08-18 08:50:52 INFO [monitor] TaskPlanner : The transcription text from the file reads: "Send an email to myself to remind me to find out what 42 is about."
== APP - monitor ==
== APP - monitor == I will now proceed with extracting the user's intent from the first two sentences.
== APP - monitor == ### Step Analysis
== APP - monitor ==
== APP - monitor == - **Step 2**: Extract the first two sentences from the transcription to analyze the user's intent.
== APP - monitor ==
== APP - monitor == Since the transcription consists of a single sentence, the user's intent is already clear:
== APP - monitor ==
== APP - monitor == ### Intent Identification
== APP - monitor ==
== APP - monitor == - **Identified Intent**: The user intends to send an email to themselves as a reminder to find out "what 42 is about."
== APP - monitor ==
== APP - monitor == ### Next Steps
== APP - monitor ==
== APP - monitor == Since there is no explicit task creation request, the next steps will involve preparing the content for email composition:
== APP - monitor ==
== APP - monitor == - Prepare the transcription text for composing an email to remind the user.
== APP - monitor == - Proceed with the creation of the email draft, ensuring the accuracy of the content.
== APP - monitor ==
== APP - monitor == In order to provide accurate reference, I will gather information about the office timezone and timezone offset before the email composition. Let me proceed with this.
== APP - monitor == 2025-08-18 08:50:57 INFO [monitor] IntentOrchestrator : Prepare the transcription text for composing an email to remind the user about 'what 42 is about.' Ensure the email draft is accurately written and ready to be sent.
== APP - monitor == 2025-08-18 08:51:04 INFO [monitor] OfficeAutomation : The email reminder has been successfully composed and sent with the subject "Reminder: Find Out What 42 Is About." The body of the email includes the reminder as intended. If there's anything else you need, feel free to let me know!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Learnings
&lt;/h2&gt;

&lt;p&gt;Some other points I'd like to convey:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;compared to the n8n flow, where one prompt yielded a structured intent and classification, it took some calibration on my end to balance out instructions handed to the orchestrator and the agents&lt;/li&gt;
&lt;li&gt;when refactoring agents, especially renaming or deleting them, be sure to flush or clean up the agent state store; otherwise, orchestrator will still try to involve orphaned agents&lt;/li&gt;
&lt;li&gt;closely and repeatedly observe conversation flow to see where instructions need to be more precise or where the agent needs to be more capable&lt;/li&gt;
&lt;li&gt;when passing file paths in task message, wrap it in something like square brackets - just separating with a blank from regular instructions caused that file path sometimes could not be resolved correctly&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Open Questions
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;How do I handle validation errors when I send information from the conversation over to a tool and the tool expects a structure with a validation on top of it?&lt;/li&gt;
&lt;li&gt;Is it really that the LLM orchestrator can only process one instance at a time? Can I make it multi-instance with some unique ID or do I need to put some singleton pattern in front of it like an actor?&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Other Dapr related posts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/kaiwalter/how-to-tune-dapr-bulk-publishsubscribe-for-maximum-throughput-40dd"&gt;How to tune Dapr bulk publish/subscribe for maximum throughput&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/kaiwalter/comparing-azure-functions-vs-dapr-on-azure-container-apps-2noh"&gt;Comparing throughput of Azure Functions vs Dapr on Azure Container Apps&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/kaiwalter/taking-spin-for-a-spin-on-aks-2lf1"&gt;Combining Dapr with a backend WebAssembly framework - Taking Spin for a spin on AKS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;For me the &lt;strong&gt;versatility of Dapr&lt;/strong&gt; for such scenarios seems tangible. One can combine deterministic with non-deterministic workflows. I think particularly this gives Dapr an edge.&lt;/p&gt;

&lt;p&gt;I need to operate it for a while. Add observability and surely more resilience. Also adding some more intents like "analyze this topic for me and send me a report" will show, whether my assumptions regarding scalability and flexibility hold up.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>workflow</category>
      <category>dapr</category>
    </item>
    <item>
      <title>Inject NixOS into an Azure VM with nixos-anywhere and Azure Container Intances</title>
      <dc:creator>Kai Walter</dc:creator>
      <pubDate>Sat, 14 Dec 2024 13:20:13 +0000</pubDate>
      <link>https://dev.to/kaiwalter/inject-nixos-into-an-azure-vm-with-nixos-anywhere-and-azure-container-intances-4322</link>
      <guid>https://dev.to/kaiwalter/inject-nixos-into-an-azure-vm-with-nixos-anywhere-and-azure-container-intances-4322</guid>
      <description>&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;In a &lt;a href="https://dev.to/kaiwalter/azure-vm-based-on-cbl-mariner-with-nix-package-manager-243f"&gt;2023 post&lt;/a&gt; I showed how to use Nix package manager in an Azure VM - in that case CBL Mariner / Azure Linux. As I've been intensifying using NixOS on my home systems and with that creating an extensive multi-host NixOS Flakes based configuration repository I wanted to get that native NixOS experience also over on my occasional cloud tinkering VMs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Options
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Custom Image
&lt;/h3&gt;

&lt;p&gt;For me the most obvious approach would be to generate a custom image, upload it to the cloud provider and stamp up the VM with that image. I succeeded using &lt;a href="https://github.com/society-for-the-blind/nixos-azure-deploy" rel="noopener noreferrer"&gt;Society for the Blind's nixos-azure-deploy repository&lt;/a&gt; with minor modifications. However that approach seemed too resource intensive for me and required Nix running on the initiating (source) system.&lt;/p&gt;

&lt;h3&gt;
  
  
  nixos-infect and nixos-anywhere
&lt;/h3&gt;

&lt;p&gt;Early I was pulled towards &lt;a href="https://github.com/nix-community/nixos-anywhere" rel="noopener noreferrer"&gt;nixos-anywhere&lt;/a&gt;, a set of scripts to install NixOS over SSH on an arbitrary target system having &lt;code&gt;kexec&lt;/code&gt; support. When struggling I tried my luck with &lt;a href="https://github.com/elitak/nixos-infect" rel="noopener noreferrer"&gt;nixos-inject&lt;/a&gt;, another way to install NixOS from within an existing target system.&lt;/p&gt;

&lt;p&gt;Basically flipping back and forth between the 2 I tried with an approach to deploy an Azure VM (and to cut out cloud platform side effects also AWS EC2 instance) - with Ubuntu or Azure/AWS Linux - and then initiate the &lt;strong&gt;infection&lt;/strong&gt; already during &lt;code&gt;cloud-init&lt;/code&gt;. Going down that rabbit hole for some time, trying to resolve issues around the right boot and disk configuration which made the target system not boot up again properly, I reverted back a bit and succeeded by injecting from an outside, a NixOS based source system with a command line like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nix run github:nix-community/nixos-anywhere -- --flake .#az-nixos --generate-hardware-config nixos-facter ./facter.json root@$FQDN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  nixos-anywhere with Azure Container Instances
&lt;/h3&gt;

&lt;p&gt;Nice but not yet where I wanted to be. For boot-strapping I prefer to work with a very limit set of tools/dependencies, aiming for only having shell scripting and Azure CLI, cutting out NixOS or a Nix-installation on none Linux source systems. Already using ACI/Azure Container Instances for other temporary jobs - &lt;a href="https://dev.to/kaiwalter/creating-a-certificate-authority-for-testing-with-azure-container-instances-5bnp"&gt;as temporary certificate authority&lt;/a&gt; or &lt;a href="https://dev.to/kaiwalter/handling-an-acme-challenge-response-with-a-temporary-azure-container-instance-3ae0"&gt;to handle an ACME challenge response&lt;/a&gt; I thought it to be a proper candidate to bring up a temporary NixOS source system. This post describes all components to achieve that kind of setup based on scripts in this &lt;a href="https://github.com/KaiWalter/nixos-cloud-deploy/tree/az-nixos-anywhere" rel="noopener noreferrer"&gt;repository&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;As the name &lt;code&gt;nixos-cloud-deploy&lt;/code&gt; may suggest, I want to keep this repository open for other cloud providers to be included. Out of necessity I might add AWS soon.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  VM creation
&lt;/h2&gt;

&lt;p&gt;Script &lt;code&gt;create-azvm-nixos-anywhere.sh&lt;/code&gt; drives the whole VM creation process. All general parameters to control the process, can be overwritten by command line arguments&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;argument&lt;/th&gt;
&lt;th&gt;command line argument(s)&lt;/th&gt;
&lt;th&gt;purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VMNAME=az-nixos&lt;/td&gt;
&lt;td&gt;-n --vm-name&lt;/td&gt;
&lt;td&gt;sets the name of the VM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RESOURCEGROUPNAME=$VMNAME&lt;/td&gt;
&lt;td&gt;-g --resource-group&lt;/td&gt;
&lt;td&gt;controls the Azure resource group to create and use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VMUSERNAME=johndoe&lt;/td&gt;
&lt;td&gt;-u --user-name&lt;/td&gt;
&lt;td&gt;sets the user name (additional to root) to setup on the VM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LOCATION=uksouth&lt;/td&gt;
&lt;td&gt;-l --location&lt;/td&gt;
&lt;td&gt;controls the Azure region to be used&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VMKEYNAME=azvm&lt;/td&gt;
&lt;td&gt;--vm-key-name&lt;/td&gt;
&lt;td&gt;controls the name of the SSH public key to be used on the VM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GITHUBSSHKEYNAME=github&lt;/td&gt;
&lt;td&gt;--github-key-name&lt;/td&gt;
&lt;td&gt;controls the name of the GitHub SSH keys to be used to pull the desired Nix configuration repository&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SIZE=Standard_B4ms&lt;/td&gt;
&lt;td&gt;-s --size&lt;/td&gt;
&lt;td&gt;controls the Azure VM SKU&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;MODE=aci&lt;/td&gt;
&lt;td&gt;-m --mode&lt;/td&gt;
&lt;td&gt;controls the source system mode: &lt;code&gt;aci&lt;/code&gt; using ACI, &lt;code&gt;nixos&lt;/code&gt; assuming to use the local Nix(OS) configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IMAGE=Canonical:ubuntu-24_04-lts:server:latest&lt;/td&gt;
&lt;td&gt;-i --image&lt;/td&gt;
&lt;td&gt;controls the initial Azure VM image to be used on the target system to inject NixOS into;&lt;br&gt;needs to support &lt;code&gt;kexec&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NIXCHANNEL=nixos-24.05&lt;/td&gt;
&lt;td&gt;--nix-channel&lt;/td&gt;
&lt;td&gt;controls the NixOS channel to be used for injection and installation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  sensitive information / SSH keys
&lt;/h3&gt;

&lt;p&gt;Keys are not passed but pulled into the script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# obtain sensitive information
. ./common.sh
prepare_keystore
VMPUBKEY=$(get_public_key $VMKEYNAME)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To make adaptation easier, I centralized keystore access - in my case to 1Password CLI - in a shared script &lt;code&gt;common.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prepare_keystore () {
  op account get --account my &amp;amp;&amp;gt;/dev/null
  if [ $? -ne 0 ]; then
      eval $(op signin --account my)
  fi
}

get_private_key () {
  echo "$(op read "op://Private/$1/private key?ssh-format=openssh")"
}

get_public_key () {
  echo "$(op read "op://Private/$1/public key")"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So to adapt it for just using keys on the local file system those functions could (no warranties) like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;prepare_keystore () {
  # nothing to do
}

get_private_key () {
  cat ~/.ssh/$1
}

get_public_key () {
  cat ~/.ssh/$1.pub
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From that these keys are injected either in the Nix configuration files to set directly over SSH on the target system. To keep it simple I did not trouble myself with adapting a secret handler like &lt;a href="https://github.com/Mic92/sops-nix" rel="noopener noreferrer"&gt;sops-nix&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Azure resource creation
&lt;/h3&gt;

&lt;p&gt;Creation of VM is handled pretty straight forward with Azure CLI. I added an explicit Storage Account to be able to investigate boot diagnostics, in case the provisioning process failed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Injecting NixOS
&lt;/h3&gt;

&lt;p&gt;Before starting injection, the script waits for SSH endpoint to be available on the target VM and cleans up &lt;code&gt;known_hosts&lt;/code&gt; from entries which might be left from prior attempts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FQDN=`az vm show --show-details -n $VMNAME -g $RESOURCEGROUPNAME --query fqdns -o tsv | cut -d "," -f 1`

wait_for_ssh $FQDN
cleanup_knownhosts $FQDN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again for re-used those 2 functions are defined in &lt;code&gt;common.sh&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cleanup_knownhosts () {
  case "$OSTYPE" in
    darwin*|bsd*)
      sed_no_backup=( -i "''" )
      ;;
    *)
      sed_no_backup=( -i )
      ;;
  esac

  sed ${sed_no_backup[@]} "s/$1.*//" ~/.ssh/known_hosts
  sed ${sed_no_backup[@]} "/^$/d" ~/.ssh/known_hosts
  sed ${sed_no_backup[@]} "/# ^$/d" ~/.ssh/known_hosts
}

wait_for_ssh () {
  echo "Waiting for SSH to become available..."
  while ! nc -z $1 22; do
      sleep 5
  done
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;$OSTYPE&lt;/code&gt; case handles the varying &lt;code&gt;sed&lt;/code&gt; flavors on MacOS, BSD and Linux regaring in-place replacement.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Making root available for SSH
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;nixos-anywhere&lt;/code&gt; relies on having root SSH access to the target system. Default Azure VM provisioning generates &lt;code&gt;authorized_keys&lt;/code&gt; which prevents &lt;code&gt;root&lt;/code&gt; to be used for connecting. As a remedy the script copies over VM user's SSH key to root.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;echo "configuring root for seamless SSH access"
ssh -o 'UserKnownHostsFile=/dev/null' -o 'StrictHostKeyChecking=no' $VMUSERNAME@$FQDN sudo cp /home/$VMUSERNAME/.ssh/authorized_keys /root/.ssh/

echo "test SSH with root"
ssh -o 'UserKnownHostsFile=/dev/null' -o 'StrictHostKeyChecking=no' root@$FQDN uname -a
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Skipping this step would show an error like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test SSH with root
Warning: Permanently added 'az-nixos.uksouth.cloudapp.azure.com' (ED25519) to the list of known hosts.
Please login as the user "johndoe" rather than the user "root".
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  initiating inject
&lt;/h4&gt;

&lt;p&gt;For ACI based injection, script &lt;code&gt;config-azvm-nixos-aci.sh&lt;/code&gt; is invoked, which is described below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;./config-azvm-nixos-aci.sh --vm-name $VMNAME \
    --resource-group $RESOURCEGROUPNAME \
    --user-name $VMUSERNAME \
    --location $LOCATION \
    --nix-channel $NIXCHANNEL \
    --vm-key-name $VMKEYNAME
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For direct injection with Nix, &lt;code&gt;nixos-anywhere&lt;/code&gt; is invoked directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TEMPNIX=$(mktemp -d)
trap 'rm -rf -- "$TEMPNIX"' EXIT
cp -r ./nix-config/* $TEMPNIX
sed -e "s|#PLACEHOLDER_PUBKEY|$VMPUBKEY|" \
    -e "s|#PLACEHOLDER_USERNAME|$VMUSERNAME|" \
    -e "s|#PLACEHOLDER_HOSTNAME|$VMNAME|" \
    ./nix-config/configuration.nix &amp;gt; $TEMPNIX/configuration.nix

nix run github:nix-community/nixos-anywhere -- --flake $TEMPNIX#az-nixos --generate-hardware-config nixos-facter $TEMPNIX/facter.json root@$FQDN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;VM's SSH key and host/username are replaced in a copy of the configuration files which then will be used by &lt;code&gt;nixos-anywhere&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  concluding VM installation
&lt;/h3&gt;

&lt;p&gt;When one of the 2 injection methods succeed, the Azure VM should be ready with NixOS installed and SSH-access available on the desired VM user. From that final steps to finalize the installation are executed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;set the NixOS channel to be used for the installation&lt;/li&gt;
&lt;li&gt;transfer GitHub SSH keys to pull the repository with the desired NixOS configuration&lt;/li&gt;
&lt;li&gt;transfer the VM's public key to a spot, where it can be picked up by my NixOS configuration definition later&lt;/li&gt;
&lt;li&gt;configure GitHub SSH environment; &lt;code&gt;dos2unix&lt;/code&gt; was required to bring the SSH key exported from 1Password CLI from CRLF into LF line endings&lt;/li&gt;
&lt;li&gt;pull the configuration repository and switch into the final configuration
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# finalize NixOS configuration
ssh-keyscan $FQDN &amp;gt;&amp;gt; ~/.ssh/known_hosts

echo "set Nix channel"
ssh $VMUSERNAME@$FQDN "sudo nix-channel --add https://nixos.org/channels/${NIXCHANNEL} nixos &amp;amp;&amp;amp; sudo nix-channel --update"

echo "transfer VM and Git keys..."
ssh $VMUSERNAME@$FQDN "mkdir -p ~/.ssh"
get_private_key "$GITHUBSSHKEYNAME" | ssh $VMUSERNAME@$FQDN -T 'cat &amp;gt; ~/.ssh/github'
get_public_key "$GITHUBSSHKEYNAME" | ssh $VMUSERNAME@$FQDN -T 'cat &amp;gt; ~/.ssh/github.pub'
get_public_key "$VMKEYNAME" | ssh $VMUSERNAME@$FQDN -T 'cat &amp;gt; ~/.ssh/azvm.pub'

ssh $VMUSERNAME@$FQDN bash -c "'
chmod 700 ~/.ssh
chmod 644 ~/.ssh/*pub
chmod 600 ~/.ssh/github

dos2unix ~/.ssh/github

cat &amp;lt;&amp;lt; EOF &amp;gt; ~/.ssh/config
Host github.com
  HostName github.com
  User git
  IdentityFile ~/.ssh/github

EOF

chmod 644 ~/.ssh/config
ssh-keyscan -H github.com &amp;gt;&amp;gt; ~/.ssh/known_hosts
'"

echo "clone repos..."
ssh $VMUSERNAME@$FQDN -T "git clone -v git@github.com:johndoe/nix-config.git ~/nix-config"
ssh $VMUSERNAME@$FQDN -T "sudo nixos-rebuild switch --flake ~/nix-config#az-vm --impure"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Injection with ACI
&lt;/h2&gt;

&lt;p&gt;Script &lt;code&gt;config-azvm-nixos-anywhere.sh&lt;/code&gt; is called by the creation script above to bring up an Azure Container Instance with NixOS to drive the injection process. This script could be used standalone on an existing Azure VM.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;argument&lt;/th&gt;
&lt;th&gt;command line argument(s)&lt;/th&gt;
&lt;th&gt;purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;VMNAME=az-nixos&lt;/td&gt;
&lt;td&gt;-n --vm-name&lt;/td&gt;
&lt;td&gt;specifies the name of the VM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RESOURCEGROUPNAME=$VMNAME&lt;/td&gt;
&lt;td&gt;-g --resource-group&lt;/td&gt;
&lt;td&gt;specifies the Azure resource group to use&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VMUSERNAME=johndoe&lt;/td&gt;
&lt;td&gt;-u --user-name&lt;/td&gt;
&lt;td&gt;specifies the user name&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LOCATION=uksouth&lt;/td&gt;
&lt;td&gt;-l --location&lt;/td&gt;
&lt;td&gt;specifies he Azure region to be used&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;VMKEYNAME=azvm&lt;/td&gt;
&lt;td&gt;--vm-key-name&lt;/td&gt;
&lt;td&gt;specifies the name of the SSH public key to be used on the VM&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SHARENAME=nixos-config&lt;/td&gt;
&lt;td&gt;-s --share-name&lt;/td&gt;
&lt;td&gt;specifies the Azure file share name to be used to hold configuration files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONTAINERNAME=$VMNAME&lt;/td&gt;
&lt;td&gt;-c --container-name&lt;/td&gt;
&lt;td&gt;specifies the ACI container name to be used&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NIXCHANNEL=nixos-24.05&lt;/td&gt;
&lt;td&gt;--nix-channel&lt;/td&gt;
&lt;td&gt;controls the NixOS channel to be used for injection and installation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  handling sensitive information
&lt;/h3&gt;

&lt;p&gt;Obtaining secrets and setting the configuration is done similar to the creation script. It might look redundant, but for certain cases I wanted this script to have its own lifecycle.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# obtain sensitive information
. ./common.sh
prepare_keystore
VMPUBKEY=$(get_public_key $VMKEYNAME)
VMPRIVKEY=$(get_private_key $VMKEYNAME | tr "[:cntrl:]" "|")

# parameters obtain sensitive information
TEMPNIX=$(mktemp -d)
trap 'rm -rf -- "$TEMPNIX"' EXIT
cp -r ./nix-config/* $TEMPNIX
sed -e "s|#PLACEHOLDER_PUBKEY|$VMPUBKEY|" \
  -e "s|#PLACEHOLDER_USERNAME|$VMUSERNAME|" \
  -e "s|#PLACEHOLDER_HOSTNAME|$VMNAME|" \
  ./nix-config/configuration.nix &amp;gt; $TEMPNIX/configuration.nix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Control characters in private key coming from 1Password needed to be replaced by a basic character &lt;code&gt;|&lt;/code&gt;, so that this key is passed properly into ACI. A lot of time, sometimes hours goes into resolving such tiny issues. That might seem wasted energy for some, but for me, not being on any project or other pressure, this actually is fun and helps me recharge my batteries.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Uploading configuration files to Azure storage file share
&lt;/h3&gt;

&lt;p&gt;All files, copied to the temporary configuration and then patched for the occasion, are uploaded to the file share:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STORAGENAME=$(az storage account list -g $RESOURCEGROUPNAME --query "[?kind=='StorageV2']|[0].name" -o tsv)

AZURE_STORAGE_KEY=`az storage account keys list -n $STORAGENAME -g $RESOURCEGROUPNAME --query "[0].value" -o tsv`
if [[ $(az storage share exists -n $SHARENAME --account-name $STORAGENAME --account-key $AZURE_STORAGE_KEY -o tsv) == "False" ]]; then
  az storage share create -n $SHARENAME --account-name $STORAGENAME --account-key $AZURE_STORAGE_KEY
fi

# upload Nix configuration files
for filename in $TEMPNIX/*; do
  echo "uploading ${filename}";
  az storage file upload -s $SHARENAME --account-name $STORAGENAME --account-key $AZURE_STORAGE_KEY \
    --source $filename
done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Running the container
&lt;/h3&gt;

&lt;p&gt;Finally the ACI container is created with the file share mounted to &lt;code&gt;/root/work&lt;/code&gt; and relevant parameters passed as &lt;code&gt;secure-environment-variables&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Special considerations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it turned out, that the process really needs 2GB memory - hence &lt;code&gt;--memory 2&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;in order to keep the container active, the entrypoint process is sent into a loop with &lt;code&gt;--command-line "tail -f /dev/null"&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nixos-anywhere&lt;/code&gt; still needs some preparation in the container which is accommodated by the script &lt;code&gt;aci-run.sh&lt;/code&gt; (descibed below)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;az container create --name $CONTAINERNAME -g $RESOURCEGROUPNAME \
    --image nixpkgs/nix:$NIXCHANNEL \
    --os-type Linux --cpu 1 --memory 2 \
    --azure-file-volume-account-name $STORAGENAME \
    --azure-file-volume-account-key $AZURE_STORAGE_KEY \
    --azure-file-volume-share-name $SHARENAME \
    --azure-file-volume-mount-path "/root/work" \
    --secure-environment-variables NIX_PATH="nixpkgs=channel:$NIXCHANNEL" FQDN="$FQDN" VMKEY="$VMPRIVKEY" \
    --command-line "tail -f /dev/null"

az container exec --name $CONTAINERNAME -g $RESOURCEGROUPNAME --exec-command "sh /root/work/aci-run.sh"

az container stop --name $CONTAINERNAME -g $RESOURCEGROUPNAME
az container delete --name $CONTAINERNAME -g $RESOURCEGROUPNAME -y
az storage share delete -n $SHARENAME --account-name $STORAGENAME --account-key $AZURE_STORAGE_KEY
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Process inside container
&lt;/h3&gt;

&lt;p&gt;Script &lt;code&gt;aci-run.sh&lt;/code&gt; prepares the container for &lt;code&gt;nixos-anywhere&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;configuring Nix to allow "new" Nix commands and flakes&lt;/li&gt;
&lt;li&gt;copying configuration files from file share to a local folder as this folder needs to be initialized with Git to work properly&lt;/li&gt;
&lt;li&gt;configuring Git&lt;/li&gt;
&lt;li&gt;convert the basic character &lt;code&gt;|&lt;/code&gt; passed in VM's private key to proper LF line endings&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;The last 3 steps for some may seem straightforward (the proper way to get Nix flakes working somewhere from scratch) or overdone. Again a lot of time went into getting this run smoothly.&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/sh

set -e

echo "configure Nix..."
mkdir -p /etc/nix
cat &amp;lt;&amp;lt; EOF &amp;gt;/etc/nix/nix.conf
experimental-features = nix-command flakes
warn-dirty = false
EOF

echo "initialize Nix configuration files..."
mkdir -p /root/nix-config
cp -v /root/work/*nix /root/nix-config/

git config --global init.defaultBranch main
git config --global user.name "Your Name"
git config --global user.email "your_email@example.com"

cd /root/nix-config
git init
git add .
git commit -m "WIP"
nix flake show

echo "set SSH private key to VM..."
mkdir -p /root/.ssh
KEYFILE=/root/.ssh/vmkey
echo $VMKEY | tr "|" "\n" &amp;gt;$KEYFILE
chmod 0600 $KEYFILE

nix run github:nix-community/nixos-anywhere -- --flake /root/nix-config#az-nixos --generate-hardware-config nixos-facter /root/nix-config/facter.json -i $KEYFILE root@$FQDN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Nix configuration files
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;nix-config/configuration.nix&lt;/code&gt; is roughly only a copy of existing samples with only minor adjustments (e.g. adding &lt;code&gt;dos2unix&lt;/code&gt;, &lt;code&gt;git&lt;/code&gt; and &lt;code&gt;vim&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;code&gt;nix-config/disk-config.nix&lt;/code&gt; also is a copy of a samples adjusted to fit the requirements for an Azure VM's disk layout as good as possible.&lt;/p&gt;

&lt;p&gt;The brunt of "hardware" detection is handled by including &lt;a href="https://github.com/numtide/nixos-facter" rel="noopener noreferrer"&gt;Facter&lt;/a&gt; in the &lt;code&gt;nixos-anywhere&lt;/code&gt; configuration process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;

&lt;p&gt;So by just simple running the creation script &lt;code&gt;./create-azvm-nixos-anywhere.sh&lt;/code&gt; I get a VM configured with my own Nix Flake configuration, no VM image dangling somewhere.&lt;/p&gt;

&lt;p&gt;SSHing on the VM and checking, gives me:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ nix-info
system: "x86_64-linux", multi-user?: yes, version: nix-env (Nix) 2.24.10, channels(root): "nixos-24.05", nixpkgs: /etc/nix/path/nixpkgs`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>azure</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Automating Azure VM Ubuntu install without fancy tools</title>
      <dc:creator>Kai Walter</dc:creator>
      <pubDate>Tue, 23 Jul 2024 04:43:13 +0000</pubDate>
      <link>https://dev.to/kaiwalter/automating-azure-vm-ubuntu-install-without-fancy-tools-2idg</link>
      <guid>https://dev.to/kaiwalter/automating-azure-vm-ubuntu-install-without-fancy-tools-2idg</guid>
      <description>&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;In a &lt;a href="https://dev.to/kaiwalter/create-a-disposable-azure-vm-based-on-cbl-mariner-2013"&gt;2022 post&lt;/a&gt; I showed how to bring up a &lt;strong&gt;disposable CBL-Mariner VM&lt;/strong&gt; using &lt;code&gt;cloud-init&lt;/code&gt; and (mostly) the &lt;strong&gt;DNF&lt;/strong&gt; package manager. As I explained in that post, it takes some fiddling around to find sources for various packages and also to mix installation methods. To achieve a more concise installation approach I tried mixing &lt;strong&gt;CBL Mariner with Nix package manager&lt;/strong&gt; in a &lt;a href="https://dev.to/kaiwalter/azure-vm-based-on-cbl-mariner-with-nix-package-manager-243f"&gt;later 2023 post&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Since then I have been using &lt;a href="https://nixos.org/" rel="noopener noreferrer"&gt;NixOS&lt;/a&gt; on my tinkering computers (x86 &amp;amp; ARM64) at home because I liked this one-file-format-declarative-definition of machines. With some new cloud technology evaluations ahead, for which I usually bring up dedicated disposable VMs, I wanted to transfer some of my NixOS learnings and create a disposable &lt;strong&gt;NixOS Azure VM&lt;/strong&gt;. As I did want to create a (&lt;em&gt;lame, everbody does that&lt;/em&gt;) custom image I was focusing a while on some infection methods (use any installed system and then "infect" with NixOS) &lt;a href="https://github.com/nix-community/nixos-anywhere" rel="noopener noreferrer"&gt;nixos-anywhere&lt;/a&gt; and &lt;a href="https://github.com/elitak/nixos-infect" rel="noopener noreferrer"&gt;nixos-infect&lt;/a&gt;. I did only succeed to a certain point but had to stop because time was running out. One thing I learned in the past 3 decades: pull back in time before you get stuck in a rabbit hole, contain your frustration, swallow your professional pride and move on. Maybe someone reading this already has figured out how to bring up NixOS on an Azure VM in this or another way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ambition
&lt;/h2&gt;

&lt;p&gt;Moving on, I decided to go for the simplest solution in my eyes: &lt;strong&gt;Ubuntu&lt;/strong&gt;. Why? When in the weeds of experimenting, in my experience, most on-the-spot tool installations are documented and work usually well with Ubuntu or rather Debian - basically avoiding &lt;strong&gt;yak shaving&lt;/strong&gt; when trying to transfer provided installation methods to exactly your environment. After this mental simplification, to still make it interesting, I set this "bar" for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use basic tools like &lt;strong&gt;cloud-init&lt;/strong&gt; and scripts - no Ansible, Chef, Puppet, ... - to start VM installation quickly without too many dependencies from my local machine (currently MacOS)&lt;/li&gt;
&lt;li&gt;not to use persisted SSH keys - rather read directly with CLI from &lt;strong&gt;1Password&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;make my regular working environment like &lt;strong&gt;NeoVim, TMUX, ZSH&lt;/strong&gt; available on the VM&lt;/li&gt;
&lt;li&gt;pre-install &lt;strong&gt;Node.js, Python, Rust, Docker&lt;/strong&gt; with the scripts and methods which bring exactly the desired versions for my dev workloads&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This all might not seem very exiting, however, I still had to explore and learn many things (coming from a more or less homogenous NixOS &amp;amp; Home Manager ecosystem) which I want to share here.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structure
&lt;/h2&gt;

&lt;p&gt;I will share 4 files I use to drive the installation and then pick out and comment on interesting sections within those files:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;create.sh&lt;/code&gt; - driving the installation process&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cloud-init.txt&lt;/code&gt; - basic installation of VM&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;check-creation.sh&lt;/code&gt; - connect to VM and check whether &lt;strong&gt;cloud-init&lt;/strong&gt; installation step concluded&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;install-stages.sh&lt;/code&gt; - install all requirements in several stages which build up on each other&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Have fun extracting whatever is interesting or useful for you!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;scripts will contain names of primitives - username, SSH keys, repositories. Those are already renamed or obfuscated - so it makes no sense for anybody out there to spend energy in finding those objects out there in the wild.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  create.sh
&lt;/h3&gt;

&lt;p&gt;In general this script&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reads SSH public key to be authorized on VM (file &lt;code&gt;authorized_keys&lt;/code&gt;) from 1Password&lt;/li&gt;
&lt;li&gt;creates a Resource Group and a Storage Account for Boot Diagnostics (which I used to debug NixOS infection progress and which I wanted to keep)&lt;/li&gt;
&lt;li&gt;create a VM, 1TB OS disk, &lt;code&gt;cloud-init.txt&lt;/code&gt; for initialization&lt;/li&gt;
&lt;li&gt;sets Auto Shutdown to &lt;code&gt;22:00 UTC&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;removes probably existing SSH entries from &lt;code&gt;known_hosts&lt;/code&gt; on my local machine and removes empty lines
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#! /bin/sh

set -e

VMNAME=${1:-thevm}
USERNAME=${2:-theuser}
PUBKEYNAME=${3:-theVmSshKey}
LOCATION=${4:-uksouth}
STORAGENAME=`echo $VMNAME$RANDOM | tr -cd '[a-z0-9]'`

op account get --account my
if [ $? -ne 0 ]; then
    eval $(op signin --account my)
fi

PUBKEY=`op read "op://Private/$PUBKEYNAME/public key"`

az group create -n $VMNAME -l $LOCATION

az storage account create -n $STORAGENAME -g $VMNAME \
  --sku Standard_LRS \
  --kind StorageV2 \
  --allow-blob-public-access false

az vm create -n $VMNAME -g $VMNAME \
 --image "Canonical:ubuntu-24_04-lts:server:latest" \
 --public-ip-sku Standard \
 --public-ip-address-dns-name $VMNAME \
 --ssh-key-values "$PUBKEY" \
 --admin-username $USERNAME \
 --os-disk-size-gb 1024 \
 --boot-diagnostics-storage $STORAGENAME \
 --size Standard_DS2_v2 \
 --custom-data "$(cat ./cloud-init.txt)"

az vm auto-shutdown -n $VMNAME -g $VMNAME \
  --time "22:00"

case "$OSTYPE" in
  darwin*|bsd*)
    sed_no_backup=( -i "''" )
    ;;
  *)
    sed_no_backup=( -i )
    ;;
esac

sed ${sed_no_backup[@]} "s/$VMNAME.*//" ~/.ssh/known_hosts
sed ${sed_no_backup[@]} "/^$/d" ~/.ssh/known_hosts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Azure Storage Account Name
&lt;/h4&gt;

&lt;p&gt;Reduce Storage Account name, derived from VM name to alphanumeric characters as other characters like &lt;code&gt;-&lt;/code&gt; are not allowed. Add a random number to somewhat ensure that the Storage Account name is unique.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;STORAGENAME=`echo $VMNAME$RANDOM | tr -cd '[a-z0-9]'`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Get SSH public key from 1Password
&lt;/h4&gt;

&lt;p&gt;This section tests whether 1Password CLI is already signed in, if not does the signin and then reads the public key portion from the secret. &lt;code&gt;my&lt;/code&gt;is the account (could be more than one) and &lt;code&gt;Private&lt;/code&gt; is the vault's name.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;op account get --account my
if [ $? -ne 0 ]; then
    eval $(op signin --account my)
fi

PUBKEY=`op read "op://Private/$PUBKEYNAME/public key"`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Clean up known_hosts
&lt;/h4&gt;

&lt;p&gt;In case that VM name had been used before and was signed in to with SSH, these statements remove the previous entries and potential resulting empty lines.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;sed&lt;/code&gt; swicthing is based on &lt;a href="https://stackoverflow.com/a/4247319/4947644" rel="noopener noreferrer"&gt;this StackOverflow answer&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;
&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;case "$OSTYPE" in
  darwin*|bsd*)
    sed_no_backup=( -i "''" )
    ;;
  *)
    sed_no_backup=( -i )
    ;;
esac

sed ${sed_no_backup[@]} "s/$VMNAME.*//" ~/.ssh/known_hosts
sed ${sed_no_backup[@]} "/^$/d" ~/.ssh/known_hosts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  cloud-init.txt
&lt;/h3&gt;

&lt;p&gt;This files defines&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;installation of a basic set of standard &lt;code&gt;apt&lt;/code&gt; packages&lt;/li&gt;
&lt;li&gt;installation scripts in various stages which are copied to user's home folder for later installation
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#cloud-config
package_upgrade: true
apt_sources:
- source: "ppa:zhangsongcui3371/fastfetch"
packages:
- apt-transport-https
- ca-certificates
- curl
- wget
- less
- lsb-release
- gnupg
- build-essential
- python3
- zsh
- tmux
- jq
- xclip
- dos2unix
- fzf
- ripgrep
- fastfetch
write_files:
  - path: /tmp/install-stage1.sh
    content: |
      #!/usr/bin/env bash

      # Azure CLI
      curl -sL https://aka.ms/InstallAzureCLIDeb | sudo bash

      # Rust
      curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y

      # NVM / Node part 1
      curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

      # TMUX TPM part 1
      git clone https://github.com/tmux-plugins/tpm ~/.tmux/plugins/tpm

      # Python
      sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 10

      # ZSH oh-my-sh part 1
      sudo chsh -s $(which zsh) $USER
      sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"

    permissions: '0755'
  - path: /tmp/install-stage2.sh
    content: |
      #!/usr/bin/env bash
      ssh -T git@github.com

      # clone script folders
      if ! [ -d ~/scripts ]; then git clone git@github.com:theuser/bash-scripts.git ~/scripts; fi
      if ! [ -d ~/.dotfiles.git ]; then git clone git@github.com:theuser/dotfiles.git ~/.dotfiles.git; fi

      # configurations
      [ -e ~/.zshrc ] &amp;amp;&amp;amp; rm ~/.zshrc
      [ -d ~/.dotfiles.git ] &amp;amp;&amp;amp; ln -s ~/.dotfiles.git/.zshrc ~/.zshrc

      [ -e ~/.tmux.conf ] &amp;amp;&amp;amp; rm ~/.tmux.conf
      [ -d ~/.dotfiles.git ] &amp;amp;&amp;amp; ln -s ~/.dotfiles.git/.tmux.conf ~/.tmux.conf

      [ -e ~/.configgit ] &amp;amp;&amp;amp; rm ~/.configgit
      [ -d ~/.dotfiles.git ] &amp;amp;&amp;amp; ln -s ~/.dotfiles.git/.configgit ~/.configgit

      ([ ! -L ~/.config ] &amp;amp;&amp;amp; [ -d ~/.dotfiles.git ]) &amp;amp;&amp;amp; ln -s ~/.dotfiles.git/.config ~/.config

      # TMUX TPM part 2
      .tmux/plugins/tpm/scripts/install_plugins.sh

      # NeoVim
      [ -e ~/scripts/install-neovim.sh ] &amp;amp;&amp;amp; ./scripts/install-neovim.sh

      # NVM / Node part 2
      source .nvm/nvm.sh
      nvm install --lts

    permissions: '0755'
  - path: /tmp/install-stage3.sh
    content: |
      #!/usr/bin/env bash
      ZSH=$HOME/.oh-my-zsh
      [ ! -d $ZSH/custom/plugins/zsh-autocomplete ] &amp;amp;&amp;amp; git clone --depth 1 -- https://github.com/marlonrichert/zsh-autocomplete.git $ZSH/custom/plugins/zsh-autocomplete

    permissions: '0755'
runcmd:
- export USER=$(awk -v uid=1000 -F":" '{ if($3==uid){print $1} }' /etc/passwd)

- curl -fsSL https://test.docker.com -o test-docker.sh
- sh test-docker.sh
- rm test-docker.sh
- usermod -aG docker $USER

- mv /tmp/install-stage* /home/$USER/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Determine user name
&lt;/h4&gt;

&lt;p&gt;This line extracts non-root user name with user id &lt;code&gt;1000&lt;/code&gt; from &lt;code&gt;passwd&lt;/code&gt; to reference later in variable &lt;code&gt;$USER&lt;/code&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 USER=$(awk -v uid=1000 -F":" '{ if($3==uid){print $1} }' /etc/passwd)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Install Docker Beta version
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- curl -fsSL https://test.docker.com -o test-docker.sh
- sh test-docker.sh
- rm test-docker.sh
- usermod -aG docker $USER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Map configuration files to dotfiles folder
&lt;/h4&gt;

&lt;p&gt;On my disposable VMs I only map selective files and folder from &lt;code&gt;.dotfiles.git&lt;/code&gt; folder to user's home:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# configurations
[ -e ~/.zshrc ] &amp;amp;&amp;amp; rm ~/.zshrc
[ -d ~/.dotfiles.git ] &amp;amp;&amp;amp; ln -s ~/.dotfiles.git/.zshrc ~/.zshrc

[ -e ~/.tmux.conf ] &amp;amp;&amp;amp; rm ~/.tmux.conf
[ -d ~/.dotfiles.git ] &amp;amp;&amp;amp; ln -s ~/.dotfiles.git/.tmux.conf ~/.tmux.conf

[ -e ~/.configgit ] &amp;amp;&amp;amp; rm ~/.configgit
[ -d ~/.dotfiles.git ] &amp;amp;&amp;amp; ln -s ~/.dotfiles.git/.configgit ~/.configgit

([ ! -L ~/.config ] &amp;amp;&amp;amp; [ -d ~/.dotfiles.git ]) &amp;amp;&amp;amp; ln -s ~/.dotfiles.git/.config ~/.config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  check-creation.sh
&lt;/h3&gt;

&lt;p&gt;This is used to SSH into the newly created VM and wait for the cloud init process to finish (or fail):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/sh

VMNAME=${1:-thevm}
USERNAME=${2:-theuser}
GITHUBSSHKEYNAME=${3:-theGitHubSshKey}
FQDN=`az vm show --show-details -n $VMNAME -g $VMNAME --query fqdns -o tsv | cut -d "," -f 1`
ssh $USERNAME@$FQDN sudo tail -f /var/log/cloud-init-output.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Determine VM's FQDN
&lt;/h4&gt;

&lt;p&gt;Azure CLI has an option &lt;code&gt;--show-details&lt;/code&gt; which returns (among also the provisioning/running state) the VM's FQDNs as a comma separated list.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FQDN=`az vm show --show-details -n $VMNAME -g $VMNAME --query fqdns -o tsv | cut -d "," -f 1`
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  install-stages.sh
&lt;/h3&gt;

&lt;p&gt;This script is called after &lt;code&gt;create.sh&lt;/code&gt; and &lt;code&gt;check-creation.sh&lt;/code&gt; which prepares SSH keys for GitHub and then runs the installation stages scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/sh

VMNAME=${1:-thevm}
USERNAME=${2:-theuser}
GITHUBSSHKEYNAME=${3:-theGitHubSshKey}
FQDN=`az vm show --show-details -n $VMNAME -g $VMNAME --query fqdns -o tsv | cut -d "," -f 1`

op account get --account my
if [ $? -ne 0 ]; then
    eval $(op signin --account my)
fi

op read "op://Private/$GITHUBSSHKEYNAME/private key?ssh-format=openssh" | ssh $USERNAME@$FQDN -T "cat &amp;gt; /home/$USERNAME/.ssh/github"
op read "op://Private/$GITHUBSSHKEYNAME/public key" | ssh $USERNAME@$FQDN -T "cat &amp;gt; /home/$USERNAME/.ssh/github.pub"

ssh $USERNAME@$FQDN bash -c "'
chmod 700 ~/.ssh
chmod 644 ~/.ssh/authorized_keys
chmod 644 ~/.ssh/*pub
chmod 600 ~/.ssh/github

dos2unix ~/.ssh/github

cat &amp;lt;&amp;lt; EOF &amp;gt; ~/.ssh/config
Host github.com
  HostName github.com
  User git
  IdentityFile ~/.ssh/github

EOF

chmod 644 ~/.ssh/config
'"

echo "SSH config finished ... press key"
read -s -n 1

ssh -t $USERNAME@$FQDN ./install-stage1.sh

echo "Stage 1 finished ... press key"
read -s -n 1

ssh -t $USERNAME@$FQDN ./install-stage2.sh

echo "Stage 2 finished ... press key"
read -s -n 1

ssh -t $USERNAME@$FQDN ./install-stage3.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Retrieve and set GitHub SSH keys
&lt;/h4&gt;

&lt;p&gt;These 2 statements extract private and public SSH keys and transfers those with SSH to the VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;op read "op://Private/$GITHUBSSHKEYNAME/private key?ssh-format=openssh" | ssh $USERNAME@$FQDN -T "cat &amp;gt; /home/$USERNAME/.ssh/github"
op read "op://Private/$GITHUBSSHKEYNAME/public key" | ssh $USERNAME@$FQDN -T "cat &amp;gt; /home/$USERNAME/.ssh/github.pub"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the VM line endings need to be converted from CR/LF to LF:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dos2unix ~/.ssh/github
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Multiple line SSH commands
&lt;/h4&gt;

&lt;p&gt;This one I had to figure out first and comes in handy when multiple lines have to be send over SSH to a remote machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh $USERNAME@$FQDN bash -c "'
...
cat &amp;lt;&amp;lt; EOF &amp;gt; ~/.ssh/config
Host github.com
  HostName github.com
  User git
  IdentityFile ~/.ssh/github

EOF
...
'"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Use SSH with terminal allocated
&lt;/h4&gt;

&lt;p&gt;I ran into a problem ...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host key verification failed.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... with this section when just SSHing with &lt;code&gt;ssh -t $USERNAME@$FQDN ./install-stage2.sh&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh -T git@github.com

# clone script folders
if ! [ -d ~/scripts ]; then git clone git@github.com:theuser/bash-scripts.git ~/scripts; fi
if ! [ -d ~/.dotfiles.git ]; then git clone git@github.com:theuser/dotfiles.git ~/.dotfiles.git; fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had to change to &lt;code&gt;-t&lt;/code&gt; option:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh -t $USERNAME@$FQDN ./install-stage2.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  install-neovim.sh
&lt;/h3&gt;

&lt;p&gt;I noticed that when installing NeoVim with the various package managers (DNF, apt, AUR) different configuration postures are put on the systems. Hence I always install with this script to end up with a reproducable configuration.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash

set -e

case $1 in
    nightly)  # Ok
        tag=tags/nightly
        ;;
    *)
        tag=latest
        ;;
esac

latest_nv_linux=$(curl -sL https://api.github.com/repos/neovim/neovim/releases/$tag | jq -r ".assets[].browser_download_url" | grep -E 'nvim-linux64.tar.gz$')
wget $latest_nv_linux -O ~/nvim-linux64.tar.gz
sudo tar xvf ~/nvim-linux64.tar.gz -C /usr/local/bin/
rm ~/nvim-linux64.tar.gz
mkdir -p ~/.local/bin

if [ ! -e ~/.local/bin/nvim ]; then
    sudo ln -s /usr/local/bin/nvim-linux64/bin/nvim ~/.local/bin/nvim
fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>azure</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Taking Spin for a spin on AKS</title>
      <dc:creator>Kai Walter</dc:creator>
      <pubDate>Fri, 15 Mar 2024 19:41:39 +0000</pubDate>
      <link>https://dev.to/kaiwalter/taking-spin-for-a-spin-on-aks-2lf1</link>
      <guid>https://dev.to/kaiwalter/taking-spin-for-a-spin-on-aks-2lf1</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;In this post I want to illustrate, whether a WebAssembly (aka Wasm) framework like &lt;a href="https://github.com/fermyon/spin" rel="noopener noreferrer"&gt;Spin&lt;/a&gt; can already be utilized on Kubernetes in coexistency with existing workloads using &lt;a href="https://www.spinkube.dev/" rel="noopener noreferrer"&gt;SpinKube&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;In recent posts - &lt;a href="https://dev.to/kaiwalter/comparing-azure-functions-vs-dapr-on-azure-container-apps-2noh"&gt;Comparing Azure Functions vs Dapr throughput on Azure Container Apps&lt;/a&gt; and &lt;a href="https://dev.to/kaiwalter/how-to-tune-dapr-bulk-publishsubscribe-for-maximum-throughput-40dd"&gt;How to tune Dapr bulk publish/subscribe for maximum throughput&lt;/a&gt; - I compared throughput of the same asynchronous message distribution scenario with various flavors of .NET deployments (Dapr, Functions) on Azure Container Apps.&lt;/p&gt;

&lt;p&gt;Observing the space of Wasm on the back end now for a while, I was wondering whether the same scenario already could be achieved and which of the suggested benefits are in reach to be leveraged. Those benefits being faster scalability and higher density as compiling &lt;a href="https://github.com/Wasm/WASI" rel="noopener noreferrer"&gt;WASI&lt;/a&gt;-compatible code into a Wasm module potentially produces a smaller artifact then regular OCI containers (still containing fragments of an OS even in the smallest &lt;code&gt;distroless&lt;/code&gt; variants).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;We explored the capabitlies in a team - hence in this post from now on I use &lt;strong&gt;we&lt;/strong&gt; and &lt;strong&gt;our&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Test Environment
&lt;/h2&gt;

&lt;p&gt;As the Wasm ecosystem for our kind of usage is still ramping up and not all capabilities are present yet to "go all in" and migrate complete workloads, the question is, how to utilize Wasm on the back end in coexistence with existing workloads selectively.&lt;/p&gt;

&lt;p&gt;In our environment many of the enterprise-ish workloads are hosted on Kubernetes. To get Kubernetes or rather &lt;code&gt;containerd&lt;/code&gt; executing Wasm, a so called &lt;a href="https://github.com/deislabs/containerd-wasm-shims?tab=readme-ov-file#shims" rel="noopener noreferrer"&gt;containerd Wasm Shim&lt;/a&gt; is required which itself utilizes &lt;a href="https://github.com/containerd/runwasi" rel="noopener noreferrer"&gt;RunWasi&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Programming Languages
&lt;/h3&gt;

&lt;p&gt;To be more flexible regarding the programming languages available for such a test scenario and to have a good-enough &lt;strong&gt;developer-inner-loop&lt;/strong&gt; experience, we turned to &lt;a href="https://github.com/fermyon/spin" rel="noopener noreferrer"&gt;Spin&lt;/a&gt;. Of the &lt;a href="https://github.com/fermyon/spin?tab=readme-ov-file#language-support-for-spin-features" rel="noopener noreferrer"&gt;languages available&lt;/a&gt; we used &lt;strong&gt;Rust&lt;/strong&gt; to get a feeling for maximum scaling &amp;amp; density capabitlies and &lt;strong&gt;TypeScipt/Node.js&lt;/strong&gt; to see behavior with a more runtime-heavy environment - as our prefered choice &lt;strong&gt;.NET&lt;/strong&gt; in spring 2024 was not yet supporting a setup like that good enough.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud Resources
&lt;/h3&gt;

&lt;p&gt;Currently Spin does not feature all &lt;a href="https://github.com/fermyon/spin?tab=readme-ov-file#language-support-for-spin-features" rel="noopener noreferrer"&gt;triggers and APIs&lt;/a&gt; required to get to a comparable environment as in my previous posts, so we leveraged &lt;a href="https://dapr.io/" rel="noopener noreferrer"&gt;Dapr&lt;/a&gt; to connect the &lt;strong&gt;Spin&lt;/strong&gt; application with cloud resources using &lt;strong&gt;HTTP trigger&lt;/strong&gt; and &lt;strong&gt;Outbound HTTP API&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Dapr Shared
&lt;/h3&gt;

&lt;p&gt;When deploying Spin runtime with regular Kubernetes primitives like deployment/service/..., Dapr obviously can be leveraged in its default sidecar mode. Ultimate density with Spin is reached by getting some of this primitives out of the way - hence SpinKube with its own &lt;a href="https://github.com/spinkube/spin-operator" rel="noopener noreferrer"&gt;SpinOperator&lt;/a&gt;. As SpinOperator does not support injecting side-cars yet, &lt;a href="https://github.com/dapr-sandbox/dapr-shared" rel="noopener noreferrer"&gt;Dapr Shared&lt;/a&gt; is used, which provides Dapr being hosted in a Kubernetes daemonset or deployment. That additionally enables a higher cardinality of Dapr vs (Spin) App: sidecar is a 1:1 relation while with Dapr Shared a 1:n relation can be achieved - contributing further to the higher density capabitlies that Wasm itself offers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test Flow
&lt;/h3&gt;

&lt;p&gt;The flow corresponds to the other 2 posts with minor adjustments:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local TypeScript application generates test data set e.g. with 10k orders/messages and places it in a blob storage (using a local Dapr output binding) - this is done to always feed the same shape of data into the test flow and hence provide a similar processing stream regardless of tech stack being tested&lt;/li&gt;
&lt;li&gt;for one test run local TypeScript application loads this test data set from blob storage, splits into single messages and schedules these messages for a specified time in to the future to get activated all at once basically simulating, that a bulk of orders where placed on your doorstep to be processed at once&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;distributor&lt;/strong&gt; endpoint of sample service receives incoming messages over a Dapr input binding - not pub/sub, not bulk to cause as much of erratic traffic as possible - and then decides whether to put the messages on a queue for virtual &lt;code&gt;express&lt;/code&gt; or &lt;code&gt;standard&lt;/code&gt; orders again using a Dapr outbound binding&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;receiver&lt;/strong&gt; endpoint for either a &lt;code&gt;express&lt;/code&gt; or &lt;code&gt;standard&lt;/code&gt; receives incoming message and places it in a blob object for each message&lt;/li&gt;
&lt;li&gt;the throughput is measured from time of schedule until all generated messages are written in blob storage&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%2Fb61e86d8yhxtdos1z4la.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%2Fb61e86d8yhxtdos1z4la.png" alt="Architecture of distributing messages with Spin" width="800" height="226"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Compared to arbitrarily hammering on a HTTP endpoint to evaluate performance, latency, ... this approach is intended to observe the behavior of the tech stack used within a certain environment, checking whether it is sensitive to any outside influences or side-effects or itself inflicts such for other components.&lt;/p&gt;




&lt;h2&gt;
  
  
  Observations
&lt;/h2&gt;

&lt;p&gt;Detailed setup and code can be found in &lt;a href="https://github.com/ZEISS/enterprise-wasm" rel="noopener noreferrer"&gt;this repository&lt;/a&gt; and shall be explained in further depth in coming posts. For now I want to share some high level observations.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;comparing &lt;strong&gt;Spin-Rust&lt;/strong&gt; with &lt;strong&gt;Warp-Rust&lt;/strong&gt; throughput is noticable better; even when operating Warp-Rust with classic containers on Azure &lt;strong&gt;DS2_v2&lt;/strong&gt; VM SKU nodes while running Spin-Rust on &lt;strong&gt;~60% cheaper D2pds_v5&lt;/strong&gt; (also leveraging on the strength of Wasm that it can be moved to nodes with a different architecture without the need to explicitly building and pushing a dedicated set of platform specific OCI containers)&lt;/li&gt;
&lt;li&gt;when building &lt;strong&gt;Warp-Rust&lt;/strong&gt; into a &lt;a href="https://github.com/ZEISS/enterprise-wasm/blob/main/samples/warp-dapr-rs/Dockerfile.static" rel="noopener noreferrer"&gt;stripped down static container&lt;/a&gt; comparable results can be achieved which suggests, that packing size impacts scaling capabilities&lt;/li&gt;
&lt;li&gt;comparing &lt;strong&gt;Spin-TypeScript&lt;/strong&gt; with &lt;strong&gt;Express-TypeScript&lt;/strong&gt; throughput is matched or only slightly better suggesting that runtime heavy languages languages/stacks like JavaScript, .NET, Java, ... cannot yet fully exploit Wasm potentials (as of spring 2024); but again, operating Express-TypeScript on Azure &lt;strong&gt;DS2_v2&lt;/strong&gt; VM SKU nodes while running Spin-TypeScript on &lt;strong&gt;~60% cheaper D2pds_v5&lt;/strong&gt; which is a cost benefit in itself&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/ZEISS/enterprise-wasm/tree/main/samples/spin-dapr-dotnet" rel="noopener noreferrer"&gt;.NET 7 with Spin SDK&lt;/a&gt; is not yet a serious contender - let's check again with .NET 8 or 9&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Disclaimers
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;as the goal has been to measure scaling behavior of a Spin App or its corresponding contender in isolation, we did not dynamically scale Dapr Shared replicas, setting them on a fixed replica count to handle as much incoming messages as possible&lt;/li&gt;
&lt;li&gt;results could possibly change if we would enhance our relatively plain AKS setup with improved networking and load balancing e.g. by using Cillium&lt;/li&gt;
&lt;li&gt;using KEDA HTTP scaling potentially also could yield different results due to a closer provisioned vs required replicas ratio&lt;/li&gt;
&lt;li&gt;comparing workloads Express vs Spin or Warp vs Spin is conducted on the same cluster, with a dedicated &lt;code&gt;classic&lt;/code&gt; or &lt;code&gt;wasm&lt;/code&gt; node pool to show coexistency capabilities and to ensure a clean baseline&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;The observations stated here shall be considered &lt;strong&gt;preliminary&lt;/strong&gt;. There is still a lot of movement in that space. Adding further capabilities into Wasm modules like observability or improving runtime compatibility may result in absorbing some of the density or performance advantages Wasm modules currently here show to have. Gains like cutting down on multi-platform builds and image retention already speak for Wasm on the back end. Tooling (although Spin and its Fermyon Cloud ecosystem is a fantastic closed loop offering for Wasm modules), developer inner and outer loop definitely need to mature before Wasm can be fully exploited for enterprise workloads and compete to the current main stream stacks.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mentions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;the Fermyon guys Radu, Justin, Danielle, Rajat, Mikkel, Matt for entertaining and supporting us with our thoughts and setup&lt;/li&gt;
&lt;li&gt;Ralph from Microsoft for providing context and calibration&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/salaboy" rel="noopener noreferrer"&gt;salaboy&lt;/a&gt; for helping to get &lt;a href="https://github.com/dapr-sandbox/dapr-shared" rel="noopener noreferrer"&gt;Dapr Shared&lt;/a&gt; into a state that it can be utilized for our use case&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>azure</category>
      <category>scalability</category>
      <category>webassembly</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>How to tune Dapr bulk publish/subscribe for maximum throughput</title>
      <dc:creator>Kai Walter</dc:creator>
      <pubDate>Wed, 14 Feb 2024 13:25:32 +0000</pubDate>
      <link>https://dev.to/kaiwalter/how-to-tune-dapr-bulk-publishsubscribe-for-maximum-throughput-40dd</link>
      <guid>https://dev.to/kaiwalter/how-to-tune-dapr-bulk-publishsubscribe-for-maximum-throughput-40dd</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;In this post I show&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;how to convert a .NET C# program processing messages from Azure Service Bus with &lt;strong&gt;Dapr input bindings&lt;/strong&gt; to &lt;strong&gt;Dapr bulk pubsub&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;how to activate and remove Dapr components even with an incremental &lt;strong&gt;ARM/Bicep&lt;/strong&gt; deployment&lt;/li&gt;
&lt;li&gt;how to use Dapr multi run for testing locally&lt;/li&gt;
&lt;li&gt;what to consider and where to measure to achieve desired throughput goals&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;real motivation: In my day job as an architect I am often too far away from coding artifacts that really go into production (and would deliver value and sustain there). Folks just keep me away from production code for a good reason : my prime days for such kind of work are over, as practices evolved too far for me to keep up with just coding from time to time.&lt;br&gt;
However, to unwind, I grant myself the occassional excursion, try out and combine stuff that is hard to do in product or project context's with ambitioned deadlines and value-delivery expectations.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This post builds up or is a sequel to my previous &lt;a href="https://dev.to/kaiwalter/comparing-azure-functions-vs-dapr-on-azure-container-apps-2noh"&gt;post on comparing Azure Functions vs Dapr throughput on Azure Container Apps&lt;/a&gt;. I detected back then, that processing a given workload of 10k messages with an ASP.NET application backed with Dapr seems to perform faster than doing that with a Functions container or the Functions on ACA footprint.&lt;/p&gt;

&lt;p&gt;Some of the underpinnings of that offering are going to get improved, on which I will make a post when released. These changes however already show a significant improvement in processing of the Functions stacks, with now the Dapr variant falling behind:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;timestamp&lt;/th&gt;
&lt;th&gt;stack&lt;/th&gt;
&lt;th&gt;cycle time in seconds&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-05T17:09:54&lt;/td&gt;
&lt;td&gt;FUNC&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-05T17:19:17&lt;/td&gt;
&lt;td&gt;ACAF&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-05T17:28:49&lt;/td&gt;
&lt;td&gt;DAPR&lt;/td&gt;
&lt;td&gt;46&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-05T17:38:35&lt;/td&gt;
&lt;td&gt;FUNC&lt;/td&gt;
&lt;td&gt;32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-06T05:50:08&lt;/td&gt;
&lt;td&gt;ACAF&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-06T06:11:08&lt;/td&gt;
&lt;td&gt;DAPR&lt;/td&gt;
&lt;td&gt;53&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-06T06:32:11&lt;/td&gt;
&lt;td&gt;FUNC&lt;/td&gt;
&lt;td&gt;31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-06T06:52:40&lt;/td&gt;
&lt;td&gt;ACAF&lt;/td&gt;
&lt;td&gt;34&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-06T07:15:33&lt;/td&gt;
&lt;td&gt;DAPR&lt;/td&gt;
&lt;td&gt;46&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-06T07:37:28&lt;/td&gt;
&lt;td&gt;FUNC&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-08T13:38:10&lt;/td&gt;
&lt;td&gt;ACAF&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-08T13:45:23&lt;/td&gt;
&lt;td&gt;DAPR&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-08T13:52:40&lt;/td&gt;
&lt;td&gt;FUNC&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-08T13:59:45&lt;/td&gt;
&lt;td&gt;ACAF&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-08T14:06:37&lt;/td&gt;
&lt;td&gt;DAPR&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-08T14:13:58&lt;/td&gt;
&lt;td&gt;FUNC&lt;/td&gt;
&lt;td&gt;35&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-08T14:21:01&lt;/td&gt;
&lt;td&gt;ACAF&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-08T14:27:43&lt;/td&gt;
&lt;td&gt;DAPR&lt;/td&gt;
&lt;td&gt;47&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-08T14:35:01&lt;/td&gt;
&lt;td&gt;FUNC&lt;/td&gt;
&lt;td&gt;37&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-08T14:42:00&lt;/td&gt;
&lt;td&gt;ACAF&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-08T14:48:43&lt;/td&gt;
&lt;td&gt;DAPR&lt;/td&gt;
&lt;td&gt;46&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;average / standard deviation:&lt;/td&gt;
&lt;td&gt;ACAF = Functions on ACA&lt;/td&gt;
&lt;td&gt;39.7 / 6.2&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;DAPR = Dapr in Container on ACA&lt;/td&gt;
&lt;td&gt;49.1 / 6.8&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;FUNC = Function in Container on ACA&lt;/td&gt;
&lt;td&gt;34.1 / 3.1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;measured with Azure Container Apps Consumption workload profile, not a dedicated workload profile!&lt;/li&gt;
&lt;li&gt;to achieve a more realistic measurement, different to the original post, not the timestamps (in telemetry, Application Insights) between the first and last request processed are measured but the time from when the 10k messages are scheduled until all messages have been stored into blobs&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;Back then I did not max out the capabilities of Dapr's message processing capabilities, as the point we had to prove internally - "What is faster: Dapr or Functions?" - already showed the desired results. Also the Dapr implementation with Azure Service Bus &lt;strong&gt;Dapr input bindings&lt;/strong&gt; reflected the majority of our implementations.&lt;/p&gt;

&lt;p&gt;This post is about what I did, to squeeze more performance out of the Dapr implementation. The goal would be also have a cycle time of 30-35 seconds for the Dapr implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basic Assumption
&lt;/h2&gt;

&lt;p&gt;In the Dapr implementation so far, for each of the 10k messages an input binding endpoint is frantically hit by the Dapr sidecar which has the application to invoke respective output binding endpoints in the same frequency:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"/q-order-ingress-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;testCase&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-input"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FromBody&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FromServices&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;DaprClient&lt;/span&gt; &lt;span class="n"&gt;daprClient&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delivery&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Express&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;daprClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InvokeBindingAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"q-order-express-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;testCase&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-output"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"create"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Standard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;daprClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InvokeBindingAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"q-order-standard-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;testCase&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-output"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"create"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While exploring other hosting and compute options like &lt;a href="https://github.com/ZEISS/enterprise-wasm/tree/main/samples/spin-dapr-rs" rel="noopener noreferrer"&gt;Dapr, WebAssembly and Spin&lt;/a&gt; I realized, that this kind of implementation causes a lot of traffic in the Dapr to application communication which can easily stand in the way of faster compute processing.&lt;/p&gt;

&lt;p&gt;Dapr still has an option available (currently in &lt;strong&gt;alpha&lt;/strong&gt; status) to reduce this kind of noice by reducing amount of invocations exchanged between Dapr and the App : &lt;a href="https://docs.dapr.io/developing-applications/building-blocks/pubsub/pubsub-bulk/" rel="noopener noreferrer"&gt;Publish and subscribe to bulk messages&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conversion
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;all code snippets shown hereinafter, can be followed along and found in &lt;a href="https://github.com/KaiWalter/message-distribution/tree/dapr-pubsub" rel="noopener noreferrer"&gt;dapr-pubsub branch of the repository I already used in the previous post&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Convert To Single PubSub
&lt;/h3&gt;

&lt;p&gt;I try to take small steps forward when converting or transforming implementations - just to to be able keep track on cause and effect.&lt;/p&gt;

&lt;p&gt;Conversion required in .NET code to get from input bindings to pubsub are minor. I still keep Azure Service Bus queues as messaging resource and will not switch to topics. This is a preference on my end as I prefer the dedicated dead letter handling of queues on single edges on a given messaging path than over dead letter handling with topics.&lt;/p&gt;

&lt;p&gt;When using Azure Service Bus queues with Dapr pubsub, &lt;code&gt;order-pubsub&lt;/code&gt; in &lt;code&gt;PublishEventAsync&lt;/code&gt; below refers to a single Dapr component in ACA (Azure Container Apps environment) - compared to indvidual components required for input and output bindings - while &lt;code&gt;q-order-...&lt;/code&gt; determines that queue itself.&lt;/p&gt;

&lt;p&gt;Also at his point I do not burden my self with a conversion to &lt;strong&gt;CloudEvents&lt;/strong&gt; and keep message processing "raw" : &lt;code&gt;{ "rawPayload", "true"}&lt;/code&gt; when publishing and avoiding &lt;code&gt;app.UseCloudEvents();&lt;/code&gt; on application startup. I noticed that, when combining bulk pubsub with CloudEvents, the message payload is suddenly rendered in JSON &lt;strong&gt;camelCase&lt;/strong&gt; (!) instead of the expected &lt;strong&gt;PascalCase&lt;/strong&gt; and that is definetely a worry for another day.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"/q-order-ingress-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;testCase&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-pubsub-single"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FromBody&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FromServices&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;DaprClient&lt;/span&gt; &lt;span class="n"&gt;daprClient&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"rawPayload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delivery&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Express&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;daprClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PublishEventAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"order-pubsub"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;$"q-order-express-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;testCase&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Standard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;daprClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;PublishEventAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"order-pubsub"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;$"q-order-standard-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;testCase&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As I want to manage pubsub subscriptions programmatically, an endpoint to configure these subscriptions is required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/dapr/subscribe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]{&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;pubsubname&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"order-pubsub"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"q-order-ingress-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;testCase&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"/q-order-ingress-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;testCase&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-pubsub-single"&lt;/span&gt;
&lt;span class="p"&gt;}}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;this is the &lt;code&gt;daprdistributor&lt;/code&gt; implementation; &lt;code&gt;daprreceiver&lt;/code&gt; implementation is adapted accordingly receiving messages from pubsub instead from input binding&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Dapr Component Switching
&lt;/h3&gt;

&lt;p&gt;To be able to switch the environment between both bindings and pubsub, as the general flow is controlled by Dapr components not by the application, I need to have a &lt;strong&gt;Bicep&lt;/strong&gt; parameter like ...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@description('determines whether bindings or pubsub is deployed for the experiment')
@allowed([
  'bindings'
  'pubsub'
])
param daprComponentsModel string
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... which then controls relevance of Dapr components through scoping for the individual applications:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// switch valid application ids for the respective deployment model
var scopesBindings = daprComponentsModel == 'bindings' ? {
  distributor: 'daprdistributor'
  recvexp: 'daprrecvexp'
  recvstd: 'daprrecvstd'
} : {
  distributor: 'skip'
  recvexp: 'skip'
  recvstd: 'skip'
}

var scopesPubSub = daprComponentsModel == 'pubsub' ? [
  'daprdistributor'
  'daprrecvexp'
  'daprrecvstd'
] : [
  'skip'
]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These scopes are then linked to Dapr components relevant for the desired model.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  resource pubSubComponent 'daprComponents' = {
    name: 'order-pubsub'
    properties: {
      componentType: 'pubsub.azure.servicebus.queues'
      version: 'v1'
      secrets: [
        {
          name: 'sb-root-connectionstring'
          value: '${listKeys('${sb.id}/AuthorizationRules/RootManageSharedAccessKey', sb.apiVersion).primaryConnectionString};EntityPath=orders'
        }
      ]
      metadata: [
        {
          name: 'connectionString'
          secretRef: 'sb-root-connectionstring'
        }
        {
          name: 'maxActiveMessages'
          value: '1000'
        }
        {
          name: 'maxConcurrentHandlers'
          value: '8'
        }
      ]
      scopes: scopesPubSub
    }
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Although this way of handling switching Dapr components in a &lt;strong&gt;ARM/Bicep incremental deployment context&lt;/strong&gt; is not production grade and requires a post-deployment step like ...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;az containerapp env dapr-component list -g $RESOURCE_GROUP_NAME -n $ACAENV_NAME \
  --query "[?properties.scopes[0]=='skip'].name" -o tsv | \
  xargs -n1 az containerapp env dapr-component remove -g $RESOURCE_GROUP_NAME -n $ACAENV_NAME --dapr-component-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... to clean up non-used components, it is OK-ish for such an evaluation environment at the moment.&lt;/p&gt;

&lt;p&gt;In the end I then just need to modify the &lt;code&gt;.env&lt;/code&gt; file in the repositories root, to switch the desired model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AZURE_LOCATION=eastus
AZURE_ENV_NAME=my-env
DAPR_COMPONENTS_MODEL=bindings
DAPR_PUBSUB_MODEL=single
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Local Testing
&lt;/h3&gt;

&lt;p&gt;With all those small changes and re-tests ahead I invested some more time in local testability of the whole environment. For that I created a &lt;a href="https://docs.dapr.io/developing-applications/local-development/multi-app-dapr-run/multi-app-overview/" rel="noopener noreferrer"&gt;Dapr Multi-App Run&lt;/a&gt; file which allows to run all applications in concert:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="na"&gt;apps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;appID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;distributor&lt;/span&gt;
    &lt;span class="na"&gt;appDirPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./src/daprdistributor/&lt;/span&gt;
    &lt;span class="na"&gt;resourcesPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./components-pubsub&lt;/span&gt;
    &lt;span class="na"&gt;configFilePath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./components-pubsub/config.yaml&lt;/span&gt;
    &lt;span class="na"&gt;appProtocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt;
    &lt;span class="na"&gt;appPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3001&lt;/span&gt;
    &lt;span class="na"&gt;daprHTTPPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3501&lt;/span&gt;
    &lt;span class="na"&gt;appHealthCheckPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/health"&lt;/span&gt;
    &lt;span class="na"&gt;placementHostAddress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
    &lt;span class="na"&gt;logLevel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;debug"&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dotnet"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ASPNETCORE_URLS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:3001&lt;/span&gt;
      &lt;span class="na"&gt;ASPNETCORE_ENVIRONMENT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Development&lt;/span&gt;
      &lt;span class="na"&gt;TESTCASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dapr&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;appID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;receiver-express&lt;/span&gt;
    &lt;span class="na"&gt;appDirPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./src/daprreceiver/&lt;/span&gt;
    &lt;span class="na"&gt;resourcesPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./components-pubsub-express&lt;/span&gt;
    &lt;span class="na"&gt;configFilePath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./components-pubsub-express/config.yaml&lt;/span&gt;
    &lt;span class="na"&gt;appProtocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt;
    &lt;span class="na"&gt;appPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3002&lt;/span&gt;
    &lt;span class="na"&gt;daprHTTPPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3502&lt;/span&gt;
    &lt;span class="na"&gt;appHealthCheckPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/health"&lt;/span&gt;
    &lt;span class="na"&gt;placementHostAddress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
    &lt;span class="na"&gt;logLevel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;debug"&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dotnet"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ASPNETCORE_URLS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:3002&lt;/span&gt;
      &lt;span class="na"&gt;ASPNETCORE_ENVIRONMENT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Development&lt;/span&gt;
      &lt;span class="na"&gt;TESTCASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dapr&lt;/span&gt;
      &lt;span class="na"&gt;INSTANCE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;express&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;appID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;receiver-standard&lt;/span&gt;
    &lt;span class="na"&gt;appDirPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./src/daprreceiver/&lt;/span&gt;
    &lt;span class="na"&gt;resourcesPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./components-pubsub-standard&lt;/span&gt;
    &lt;span class="na"&gt;configFilePath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./components-pubsub-standard/config.yaml&lt;/span&gt;
    &lt;span class="na"&gt;appProtocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt;
    &lt;span class="na"&gt;appPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3003&lt;/span&gt;
    &lt;span class="na"&gt;daprHTTPPort&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3503&lt;/span&gt;
    &lt;span class="na"&gt;appHealthCheckPath&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/health"&lt;/span&gt;
    &lt;span class="na"&gt;placementHostAddress&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;
    &lt;span class="na"&gt;logLevel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;debug"&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dotnet"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;run"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;ASPNETCORE_URLS&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http://localhost:3003&lt;/span&gt;
      &lt;span class="na"&gt;ASPNETCORE_ENVIRONMENT&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Development&lt;/span&gt;
      &lt;span class="na"&gt;TESTCASE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dapr&lt;/span&gt;
      &lt;span class="na"&gt;INSTANCE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;standard&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Accompanied by a small script to publish both types of messages into the local environment and observe the flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="nv"&gt;ORDERID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0

&lt;span class="k"&gt;function &lt;/span&gt;generate_message&lt;span class="o"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nv"&gt;UUID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;uuidgen&lt;span class="sb"&gt;`&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="k"&gt;$((&lt;/span&gt;ORDERID &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt; &lt;span class="nt"&gt;-eq&lt;/span&gt; 0 &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nv"&gt;DELIVERY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Express
  &lt;span class="k"&gt;else
    &lt;/span&gt;&lt;span class="nv"&gt;DELIVERY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Standard
  &lt;span class="k"&gt;fi
  &lt;/span&gt;jq &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--arg&lt;/span&gt; uuid &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$UUID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--arg&lt;/span&gt; orderid &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ORDERID&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="nt"&gt;--arg&lt;/span&gt; delivery &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$DELIVERY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="s1"&gt;'{OrderId: $orderid|tonumber, OrderGuid: $uuid, Delivery: $delivery}'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="nv"&gt;ORDERID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;ORDERID &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
dapr publish &lt;span class="nt"&gt;--publish-app-id&lt;/span&gt; distributor &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--topic&lt;/span&gt; q-order-ingress-dapr &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pubsub&lt;/span&gt; order-pubsub &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;generate_message&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--metadata&lt;/span&gt; &lt;span class="s1"&gt;'{"rawPayload":"true"}'&lt;/span&gt;

&lt;span class="nv"&gt;ORDERID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt;ORDERID &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="k"&gt;))&lt;/span&gt;
dapr publish &lt;span class="nt"&gt;--publish-app-id&lt;/span&gt; distributor &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--topic&lt;/span&gt; q-order-ingress-dapr &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--pubsub&lt;/span&gt; order-pubsub &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;generate_message&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--metadata&lt;/span&gt; &lt;span class="s1"&gt;'{"rawPayload":"true"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  First Pit Stop
&lt;/h3&gt;

&lt;p&gt;It seems using &lt;strong&gt;pubsub&lt;/strong&gt; puts a toll on the performance.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;implementation&lt;/th&gt;
&lt;th&gt;average, 5 cycles&lt;/th&gt;
&lt;th&gt;standard deviation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dapr input bindings, before conversion&lt;/td&gt;
&lt;td&gt;48.6s&lt;/td&gt;
&lt;td&gt;6.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dapr single pubsub&lt;/td&gt;
&lt;td&gt;103.4s&lt;/td&gt;
&lt;td&gt;7.5s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The Valley Of Tears
&lt;/h2&gt;

&lt;p&gt;I thought, with just implementing bulk pubsub performance will increase dramatically. I reduce single message transfers to block transfers and it seems logical, that just by that messages are bursted through the environment at light speed. Well, ...&lt;/p&gt;

&lt;h3&gt;
  
  
  Converting To Bulk PubSub
&lt;/h3&gt;

&lt;p&gt;To apply bulk pubsub on subscription programmatically, just meta information needs to be enhanced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapGet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/dapr/subscribe"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[]{&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;pubsubname&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"order-pubsub"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;topic&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"q-order-ingress-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;testCase&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;route&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;$"/q-order-ingress-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;testCase&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-pubsub"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;bulkSubscribe&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;enabled&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;maxMessagesCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;maxAwaitDurationMs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On the processing endpoint changes are more substantial:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;messages are passed in a bulk, hence looping is required to process single messages&lt;/li&gt;
&lt;li&gt;response collections need to be maintained to tell Dapr, which of the incoming entries/messages processed successful, which need to be retried or dropped&lt;/li&gt;
&lt;li&gt;messages to be published in a bulk also need to be collected for sending outbound&lt;/li&gt;
&lt;li&gt;again, let no &lt;strong&gt;CloudEvents&lt;/strong&gt; slip in at that moment
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"/q-order-ingress-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;testCase&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;-pubsub"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FromBody&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;BulkSubscribeMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;bulkOrders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FromServices&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;DaprClient&lt;/span&gt; &lt;span class="n"&gt;daprClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Program&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{Count} Orders to distribute"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bulkOrders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BulkSubscribeAppResponseEntry&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;responseEntries&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BulkSubscribeAppResponseEntry&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;bulkOrders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;expressEntries&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BulkSubscribeMessageEntry&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;bulkOrders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;standardEntries&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;BulkSubscribeMessageEntry&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;bulkOrders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;bulkOrders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Entries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delivery&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Express&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;expressEntries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Standard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;standardEntries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;Dictionary&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="s"&gt;"rawPayload"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"true"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;};&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expressEntries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;expressEntries&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;daprClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BulkPublishEventAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"order-pubsub"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;$"q-order-express-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;testCase&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;expressEntries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;responseEntries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BulkSubscribeAppResponseEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EntryId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BulkSubscribeAppResponseStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SUCCESS&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;expressEntries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;responseEntries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BulkSubscribeAppResponseEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EntryId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BulkSubscribeAppResponseStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RETRY&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;standardEntries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Count&lt;/span&gt; &lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;standardEntries&lt;/span&gt; &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;daprClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;BulkPublishEventAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"order-pubsub"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;$"q-order-standard-&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;testCase&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToArray&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;metadata&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;standardEntries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;responseEntries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BulkSubscribeAppResponseEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EntryId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BulkSubscribeAppResponseStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SUCCESS&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Exception&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;standardEntries&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;responseEntries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BulkSubscribeAppResponseEntry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EntryId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BulkSubscribeAppResponseStatus&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RETRY&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;BulkSubscribeAppResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;responseEntries&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;this is the &lt;code&gt;daprdistributor&lt;/code&gt; implementation; &lt;code&gt;daprreceiver&lt;/code&gt; implementation is adapted accordingly receiving bulk messages but storing to blob with single requests&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Second Pit Stop
&lt;/h3&gt;

&lt;p&gt;Even with having 2 cycles faster then 40s, still there are outliers which take 60-70s. Which in turn results in this average:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;implementation&lt;/th&gt;
&lt;th&gt;maxMessagesCount&lt;/th&gt;
&lt;th&gt;average, 5 cycles&lt;/th&gt;
&lt;th&gt;standard deviation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dapr input bindings&lt;/td&gt;
&lt;td&gt;n.a.&lt;/td&gt;
&lt;td&gt;48.6s&lt;/td&gt;
&lt;td&gt;6.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dapr single pubsub&lt;/td&gt;
&lt;td&gt;n.a.&lt;/td&gt;
&lt;td&gt;103.4s&lt;/td&gt;
&lt;td&gt;7.5s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dapr bulk pubsub&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;57.6s&lt;/td&gt;
&lt;td&gt;19.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Crank It Up
&lt;/h3&gt;

&lt;p&gt;A &lt;code&gt;maxMessagesCount&lt;/code&gt; of &lt;strong&gt;100&lt;/strong&gt; does not seem to make a dent. Let's increase:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      maxMessagesCount = 500,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Although having half of the requests with a significant better cycle time, the other half still took twice the time (standard deviation ~21s on a 64s average):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;timestamp&lt;/th&gt;
&lt;th&gt;cycle time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-12T06:15:09&lt;/td&gt;
&lt;td&gt;36&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-12T06:22:50&lt;/td&gt;
&lt;td&gt;43&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-12T06:29:41&lt;/td&gt;
&lt;td&gt;80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-12T06:37:04&lt;/td&gt;
&lt;td&gt;81&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-12T06:45:04&lt;/td&gt;
&lt;td&gt;78&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2024-02-12T06:52:34&lt;/td&gt;
&lt;td&gt;38&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Using a query like ...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requests
| where timestamp between( todatetime('2024-02-12T06:45:04.7285069Z') .. todatetime('2024-02-12T06:52:34.2132486Z') )
| where name startswith "POST" and cloud_RoleName matches regex "^[\\d\\w\\-]+dapr"
| where success == true
| summarize count() by cloud_RoleName, bin(timestamp, 5s)
| render columnchart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... shows a processing gap towards the end of processing ...&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%2Fra0pihc435uux71twgiy.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%2Fra0pihc435uux71twgiy.png" alt="78 seconds cycle time with spread of request processing" width="800" height="288"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;... while in this sample processing is in one block without a gap:&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%2F5myjn6op7ewvskqmf0tw.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%2F5myjn6op7ewvskqmf0tw.png" alt="38 seconds cycle time with cohesive request processing" width="800" height="291"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Looking at the replica count for all 3 services during the testing period shows, that those go consitently up to 10 and down again. Also no replica restarts are reported. Hence non-availibilty of replicas can be ruled out for causing these gaps.&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%2Fpo19x66aolqhnpni69ox.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%2Fpo19x66aolqhnpni69ox.png" alt="replica count of all 3 services during test period" width="800" height="307"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Additionally I found many &lt;strong&gt;RETRY&lt;/strong&gt; errors captured for during bulk subscribe processing, ...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;time="2024-02-12T06:30:00.08158319A3Z" level=error msg="App handler returned an error for message 5888d18e-da39-44b4-81d9-a2e11bef21bb on queue q-order-ingress-dapr: RETRY required while processing bulk subscribe event for entry id: 27180f25-502b-4d13-a738-77306a6d3f01"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... spread out over all test cycles but only logged for &lt;strong&gt;Distributor&lt;/strong&gt; service.&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%2Fv1yzlm9sxjm4oykveqs4.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%2Fv1yzlm9sxjm4oykveqs4.png" alt="occurences of bulk subscribe RETRY errors" width="800" height="316"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Looking for the root cause of these retries I found ...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dependencies
| where timestamp between( todatetime('2024-02-12T06:15:00') .. todatetime('2024-02-12T07:00:00') )
| where cloud_RoleName endswith "daprdistributor"
| where name == "/dapr.proto.runtime.v1.dapr/bulkpublisheventalpha1"
| order by timestamp asc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error when publish to topic q-order-express-dapr in pubsub order-pubsub: the message is too large
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... which means, that a maxMessagesCount of 500 can cause &lt;strong&gt;Distributor&lt;/strong&gt; to generate too large bulk publish packets.&lt;/p&gt;

&lt;p&gt;OK, so let's increase carefully ...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      maxMessagesCount = 250,
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... and then check for errors again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dependencies
| where customDimensions.error != ""
| order by timestamp desc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Third Pit Stop
&lt;/h3&gt;

&lt;p&gt;Not quite, but close.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;implementation&lt;/th&gt;
&lt;th&gt;maxMessagesCount&lt;/th&gt;
&lt;th&gt;average, 5 cycles&lt;/th&gt;
&lt;th&gt;standard deviation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dapr input bindings&lt;/td&gt;
&lt;td&gt;n.a.&lt;/td&gt;
&lt;td&gt;48.6s&lt;/td&gt;
&lt;td&gt;6.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dapr single pubsub&lt;/td&gt;
&lt;td&gt;n.a.&lt;/td&gt;
&lt;td&gt;103.4s&lt;/td&gt;
&lt;td&gt;7.5s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dapr bulk pubsub&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;57.6s&lt;/td&gt;
&lt;td&gt;19.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dapr bulk pubsub&lt;/td&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;td&gt;42.8s&lt;/td&gt;
&lt;td&gt;13.1s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Still for the slow test cycles there is a gap until all records are processed:&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%2Fdb6e8poi8mw31tdfadls.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%2Fdb6e8poi8mw31tdfadls.png" alt="76 seconds cycle time with spread of request processing" width="800" height="334"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Exactly for these kind of measurements I log the bulk messsage count with &lt;code&gt;log.LogInformation("{Count} Orders to distribute", bulkOrders.Entries.Count);&lt;/code&gt; to have an indication on how many messages are handed over in one bulk.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;At this point I even tried to switch to a dedicated workload profile type - instead of consumption - to rule out that "noisy neighbors" are causing the processing to lag. But no, same effects also with dedicated this processing gap occured.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Side node: I had to go up to &lt;strong&gt;E8&lt;/strong&gt; workload profile type to get to a processing speed similar to consumption model.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Beware Of Message Locks
&lt;/h3&gt;

&lt;p&gt;Fine. Back to telemetry. Checking what else is logged on the containers ...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ContainerAppConsoleLogs_CL
| where TimeGenerated between( todatetime('2024-02-13T16:07:05.8171428Z') .. todatetime('2024-02-13T16:45:47.5552143Z') )
| where ContainerAppName_s contains "daprdist"
| where Log_s !contains "level=info"
| order by TimeGenerated asc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... reveals that almost exactly at the point before final processing continues, this type of error is logged:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
time="2024-02-13T16:14:05.778260655Z" level=warning msg="Error renewing message locks for queue q-order-ingress-dapr (failed: 43/758):
couldn't renew active message lock for message ff3d2095-effb-428e-87bf-e131c961ac4e: lock has been lost
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looking at the &lt;code&gt;order-pubsub&lt;/code&gt; component in the &lt;code&gt;containerapps.bicep&lt;/code&gt; I realized I missed a spot. Measuring earlier how many bulks could be processed at full size (e.g. 100) I observed, that very soon during the processing cycle, the Dapr Service Bus implementation was creating smaller bulks (even with size 1) as 8 &lt;code&gt;maxConcurrentHandlers&lt;/code&gt;, after waiting &lt;code&gt;maxAwaitDurationMs = 40&lt;/code&gt; (see pubsub subscribe above), were just pulling "what they get". Hence I tuned down &lt;code&gt;maxConcurrentHandlers&lt;/code&gt; to 1 to avoid that kind of congestion.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;resource pubSubComponent 'daprComponents' = {
    name: 'order-pubsub'
    properties: {
      componentType: 'pubsub.azure.servicebus.queues'
      version: 'v1'
      secrets: [
        {
          name: 'sb-root-connectionstring'
          value: '${listKeys('${sb.id}/AuthorizationRules/RootManageSharedAccessKey', sb.apiVersion).primaryConnectionString};EntityPath=orders'
        }
      ]
      metadata: [
        {
          name: 'connectionString'
          secretRef: 'sb-root-connectionstring'
        }
        {
          name: 'maxActiveMessages'
          value: '1000'
        }
        {
          name: 'maxConcurrentHandlers'
          value: '1'
        }
      ]
      scopes: scopesPubSub
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;However I did not pay attention to &lt;code&gt;maxActiveMessages&lt;/code&gt; - which before were divided by 8 handlers, what in turn allowed Dapr sidecar and the app to process a block of 125 locked message in time (before the lock expires). Now 1 handler was locking all 1000 messages and even with bulk processing some message locks could expire.&lt;/p&gt;

&lt;p&gt;Consequently I think it makes more sense to tune the amount of active messages pulled at a time to the potential bulk size:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        {
          name: 'maxActiveMessages'
          value: '250'
        }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The Last Mile
&lt;/h3&gt;

&lt;p&gt;Changing this setting did not make the warning completely go away. As Alessandro Segala states in this &lt;a href="https://github.com/dapr/components-contrib/issues/2532" rel="noopener noreferrer"&gt;issue&lt;/a&gt; : &lt;em&gt;"This happens because when it's time to renew the locks, at the interval, we "snapshot" the active messages and then renew each one's lock in sequence. If you have a high number for maxActiveMessages, it takes time for the component to renew the locks for each one, and if the app has ACK'd the message in the meanwhile, then the lock renewal fails."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;However as results show: the better noise and congestion is reduced in the environment, the better and more reliable the throughput.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;implementation&lt;/th&gt;
&lt;th&gt;maxMessagesCount&lt;/th&gt;
&lt;th&gt;maxActiveMessages&lt;/th&gt;
&lt;th&gt;maxConcurrentHandlers&lt;/th&gt;
&lt;th&gt;average, 5 cycles&lt;/th&gt;
&lt;th&gt;standard deviation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Dapr input bindings&lt;/td&gt;
&lt;td&gt;n.a.&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;48.6s&lt;/td&gt;
&lt;td&gt;6.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dapr single pubsub&lt;/td&gt;
&lt;td&gt;n.a.&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;103.4s&lt;/td&gt;
&lt;td&gt;7.5s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dapr bulk pubsub&lt;/td&gt;
&lt;td&gt;100&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;57.6s&lt;/td&gt;
&lt;td&gt;19.2s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dapr bulk pubsub&lt;/td&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;td&gt;1000&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;42.8s&lt;/td&gt;
&lt;td&gt;13.1s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dapr bulk pubsub&lt;/td&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;td&gt;250&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;28.0s&lt;/td&gt;
&lt;td&gt;3.6s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;Disclaimer: This combination of configuration values is just tuned for this particular scenario. With this post I just want to show, what to consider, what to look out for and where to calibrate.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;Running all 3 stacks on the same environment now shows, that an invest in bulk pubsub definitely pays out.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;stack&lt;/th&gt;
&lt;th&gt;average, 6 cycles&lt;/th&gt;
&lt;th&gt;standard deviation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ACAF = Functions on ACA&lt;/td&gt;
&lt;td&gt;36.0s&lt;/td&gt;
&lt;td&gt;3.5s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DAPR = Dapr in Container on ACA&lt;/td&gt;
&lt;td&gt;27.8s&lt;/td&gt;
&lt;td&gt;1.6s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FUNC = Function in Container on ACA&lt;/td&gt;
&lt;td&gt;34.0s&lt;/td&gt;
&lt;td&gt;2.4s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;As always there is no free lunch and some points have to be considered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.dapr.io/operations/configuration/increase-request-size/" rel="noopener noreferrer"&gt;Dapr HTTP and gRPC payload size&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;just switching to bulk pubsub is not enough; the additional aggregation and collection times (see &lt;code&gt;maxAwaitDurationMs&lt;/code&gt;) need to be outweighed by the gains achieved with packaging data&lt;/li&gt;
&lt;li&gt;with larger payloads moving through an environment, impact on all involved components needs to be considered and then balanced for optimum outcome&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When I set out writing this post I already observed some of these lags shown above, not sure whether I would be able to identify and overcome those. But hey, that are exactly the challenges I seek.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>dapr</category>
      <category>scalability</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Comparing throughput of Azure Functions vs Dapr on Azure Container Apps</title>
      <dc:creator>Kai Walter</dc:creator>
      <pubDate>Mon, 09 Oct 2023 09:54:44 +0000</pubDate>
      <link>https://dev.to/kaiwalter/comparing-azure-functions-vs-dapr-on-azure-container-apps-2noh</link>
      <guid>https://dev.to/kaiwalter/comparing-azure-functions-vs-dapr-on-azure-container-apps-2noh</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;In this post I show&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;(mainly) how .NET &lt;strong&gt;Azure Functions&lt;/strong&gt; (in the 2 currently available hosting options on &lt;strong&gt;Azure Container Apps&lt;/strong&gt;) can be compared to a &lt;strong&gt;ASP.NET Dapr&lt;/strong&gt; application in terms of asynchronous messaging throughput&lt;/li&gt;
&lt;li&gt;some learnings when deploying the 3 variants on ACA (=Azure Container Apps):

&lt;ul&gt;
&lt;li&gt;Azure Functions in a container on ACA, applying KEDA scaling&lt;/li&gt;
&lt;li&gt;Azure Functions on ACA, leaving scaling up to the platform&lt;/li&gt;
&lt;li&gt;ASP.NET in a container on ACA using Dapr sidecar, apply KEDA scaling&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;extending ApplicationInsights cloud_RoleName and cloud_RoleInstance for Dapr to see instance names in telemetry&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;jump to results&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Although the &lt;a href="https://github.com/KaiWalter/message-distribution/tree/v1.0.1" rel="noopener noreferrer"&gt;sample repo&lt;/a&gt; additional to &lt;strong&gt;Bash/Azure CLI&lt;/strong&gt; contains a deployment option with &lt;strong&gt;Azure Developer CLI&lt;/strong&gt;, I never was able to sustain stable deployment with this option while Azure Functions on Container Apps was in preview.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-container-apps-hosting" rel="noopener noreferrer"&gt;Azure Container Apps hosting of Azure Functions&lt;/a&gt; is a way to host Azure Functions directly in Container Apps - additionally to App Service with and without containers. This offering also adds some Container Apps built-in capabilities like the &lt;a href="https://dapr.io/" rel="noopener noreferrer"&gt;Dapr&lt;/a&gt; microservices framework which would allow for mixing microservices workloads on the same environment with Functions.&lt;/p&gt;

&lt;p&gt;Running a sufficiently big workload already with Azure Functions inside containers on Azure Container Apps for a while, I wanted to see how both variants compare in terms of features and above all : scaling.&lt;/p&gt;

&lt;p&gt;With &lt;a href="https://customers.microsoft.com/en-us/story/1336089737047375040-zeiss-accelerates-cloud-first-development-on-azure-and-streamlines-order-processing" rel="noopener noreferrer"&gt;another environment&lt;/a&gt; we heavily rely on &lt;strong&gt;Dapr&lt;/strong&gt; for synchronous invocations as well as asynchronous message processing. Hence additionally I wanted to see whether one of the frameworks promoted by Microsoft - Azure Functions host with its bindings or Dapr with its generic components and the sidecar architecture - substantially stands out in terms of throughput.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution Overview
&lt;/h2&gt;

&lt;p&gt;The test environment can be deployed from this &lt;a href="https://github.com/KaiWalter/message-distribution/tree/v1.0.1" rel="noopener noreferrer"&gt;repo&lt;/a&gt; - &lt;code&gt;README.md&lt;/code&gt; describes the steps required.&lt;/p&gt;

&lt;h3&gt;
  
  
  Approach
&lt;/h3&gt;

&lt;p&gt;To come to a viable comparison, I applied these aspects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;logic for all contenders is written in &lt;strong&gt;C# / .NET 7&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;all contenders need to process the &lt;strong&gt;exact same volume&lt;/strong&gt; and structure of payloads - which is generated once and then sent to them for processing&lt;/li&gt;
&lt;li&gt;test payload (10k messages by default) is send on a queue and &lt;strong&gt;scheduled to exactly the same time&lt;/strong&gt; to force the stack, to deal with the amount at once&lt;/li&gt;
&lt;li&gt;both Functions variants are based on &lt;strong&gt;.NET isolated worker&lt;/strong&gt;, as Functions on Container Apps only support this model&lt;/li&gt;
&lt;li&gt;all 3 variants run staggered, not at the same time, on the same Container Apps environment, hence same region, same nodes, same resources ...&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Limitations
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Only &lt;strong&gt;Service Bus queues&lt;/strong&gt; are tested. Of course, a scenario like this can also be achieved with pub/sub Service Bus &lt;strong&gt;topics&lt;/strong&gt; and subscriptions. However in our enterprise workloads, where we apply this pattern, we work with queues as these allow a dedicated dead-lettering at each stage of the process - compared to topics, where moving messages from &lt;em&gt;dead-letter&lt;/em&gt; to &lt;em&gt;active&lt;/em&gt; results in all subscribers (if not explicitly filtered) receivng these messages again.&lt;/li&gt;
&lt;li&gt;Currently not all capabilities of the contesting stacks - like Dapr bulk message processing - are maxed out. Hence there is obviously still some potential for improving individual throughput.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Measuring Throughput
&lt;/h3&gt;

&lt;p&gt;Throughput is measured by substracting the timestamp of the last message processed from the timestamp of the first message processed - for a given scheduling timestamp. A generic query to Application Insights with &lt;code&gt;$TESTPREFIX&lt;/code&gt; representing one of the &lt;em&gt;codes&lt;/em&gt; above and &lt;code&gt;$SCHEDULE&lt;/code&gt; refering to the scheduling timestamp for a particular test run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query="requests | where cloud_RoleName matches regex '$TESTPREFIX(dist|recv)' | where name != 'Health' and name !startswith 'GET' | where timestamp &amp;gt; todatetime('$SCHEDULE') | where success == true | summarize count(),sum(duration),min(timestamp),max(timestamp) | project count_, runtimeMs=datetime_diff('millisecond', max_timestamp, min_timestamp)"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Evaluating Scaling
&lt;/h3&gt;

&lt;p&gt;While above query is used in the automated testing and recording process, I used this type of query ...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requests
| where cloud_RoleName startswith "func"
| where name != "Health"
| where timestamp &amp;gt; todatetime('2022-11-03T07:09:26.9394443Z')
| where success == true
| summarize count() by cloud_RoleInstance, bin(timestamp, 15s)
| render columnchart
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... to see whether the platform / stack scales in an expected pattern, ...&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%2Fmstmzzblgx6g1ucd3s6y.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%2Fmstmzzblgx6g1ucd3s6y.png" alt="Regular scaling of Functions container" width="800" height="327"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;... which pointed me to a strange scaling lag for Azure Functions on ACA:&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%2Fatui0u5bny69ph9d2c4x.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%2Fatui0u5bny69ph9d2c4x.png" alt="Lagged scaling for Functions on ACA" width="800" height="315"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Microsoft Product Group looked into this observation and provided an explanation in this &lt;a href="https://github.com/Azure/azure-functions-on-container-apps/issues/33" rel="noopener noreferrer"&gt;GitHub issue&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Initially, some default numbers of nodes are allocated for any ACA environment. During scaling, ACA uses these nodes to create app instances. For container apps scaling, the default number of nodes are sufficient as it uses less cpu, memory per instance. For function apps scaling, the default number of nodes is not sufficient and thus, ACA environment requests more nodes in back end. After new nodes are available to ACA environment, it uses them to create remaining instances for Function app. It takes some time to fetch new nodes and create remaining instances, therefore, we see a gap in processing between both deployments."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When conducting the final battery of tests in October'23 this behavior was partially gone (see results below) when sufficient Functions relevant nodes already had been scaled.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Elements
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;code&gt;Generate&lt;/code&gt; in Function App &lt;code&gt;testdata&lt;/code&gt; generates a test data payload (e.g. with 10k orders) and puts it in a blob storage&lt;/li&gt;
&lt;li&gt;one of the &lt;code&gt;PushIngress...&lt;/code&gt; functions in the very same Function App then can be triggered to schedule all orders at once on an ingress Service Bus queue - either for Functions or for Dapr&lt;/li&gt;
&lt;li&gt;each of the contestants has a &lt;code&gt;Dispatch&lt;/code&gt; method which picks the payload for each order from the ingress queue, inspects it and puts it either on a queue for "Standard" or "Express" orders&lt;/li&gt;
&lt;li&gt;then for these order types there is a separate &lt;code&gt;Receiver&lt;/code&gt; function which finally processes the dispatched message&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%2Fdxwf15gf51zmk0e59fqi.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%2Fdxwf15gf51zmk0e59fqi.png" alt="Solution overview showing main components" width="800" height="541"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;C# project names and queues use a consistent coding for each contestant:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;code used for solution elements&lt;/th&gt;
&lt;th&gt;implementation and deployment approach&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ACAF&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;.NET &lt;a href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-container-apps-hosting" rel="noopener noreferrer"&gt;Azure Functions on ACA deployment&lt;/a&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;DAPR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;ASP.NET with Dapr in a container on ACA&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FUNC&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;.NET Azure Functions in a container on ACA&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h4&gt;
  
  
  Dispatcher
&lt;/h4&gt;

&lt;p&gt;As .NET isolated does not support multiple outputs for Functions, an optional message output is required to either put message into &lt;code&gt;StandardMessage&lt;/code&gt; or &lt;code&gt;ExpressMessage&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Azure.Messaging.ServiceBus&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Logging&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Models&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Text.Json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;funcdistributor&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Dispatch&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Dispatch"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;DispatchedOutput&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ServiceBusTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"q-order-ingress-func"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SERVICEBUS_CONNECTION"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ingressMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ingressMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ingressMessage&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deserialize&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;ingressMessage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ingressMessage&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;outputMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;DispatchedOutput&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

            &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delivery&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Express&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;outputMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExpressMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ServiceBusMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
                    &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;ContentType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;MessageId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                    &lt;span class="p"&gt;};&lt;/span&gt;
                    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Standard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;outputMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StandardMessage&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;ServiceBusMessage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetBytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
                    &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="n"&gt;ContentType&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"application/json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="n"&gt;MessageId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
                    &lt;span class="p"&gt;};&lt;/span&gt;
                    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
                &lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"invalid Delivery type: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delivery&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
                    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;

            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;outputMessage&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DispatchedOutput&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ServiceBusOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"q-order-express-func"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SERVICEBUS_CONNECTION"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ServiceBusMessage&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;ExpressMessage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ServiceBusOutput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"q-order-standard-func"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SERVICEBUS_CONNECTION"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;ServiceBusMessage&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;StandardMessage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;get&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;set&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Dapr this dispatcher is implemented with minimal API just in the top-level file &lt;code&gt;Program.cs&lt;/code&gt; - a very concise way almost in Node.js style:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/q-order-ingress-dapr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FromBody&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FromServices&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;DaprClient&lt;/span&gt; &lt;span class="n"&gt;daprClient&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delivery&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Express&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;daprClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InvokeBindingAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"q-order-express-dapr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"create"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;Delivery&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Standard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;daprClient&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;InvokeBindingAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"q-order-standard-dapr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"create"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Receiver
&lt;/h4&gt;

&lt;p&gt;in Functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker.Http&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Azure.Functions.Worker&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.Extensions.Logging&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Models&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Text.Json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;acafrecvexp&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Receiver&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Receiver"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ServiceBusTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"q-order-express-acaf"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SERVICEBUS_CONNECTION"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ingressMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;FunctionContext&lt;/span&gt; &lt;span class="n"&gt;executionContext&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;executionContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetLogger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Receiver"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ingressMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ingressMessage&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deserialize&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;ingressMessage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

            &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{Delivery} Order received {OrderId}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delivery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;in Dapr with minimal API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/q-order-express-dapr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Program&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;FromBody&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;Order&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{Delivery} Order received {OrderId}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Delivery&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Ok&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Scaling
&lt;/h4&gt;

&lt;p&gt;For the Functions and Dapr Container App, a scaling rule can be set. For Functions on ACA this is handled by the platform.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;      scale: {
        minReplicas: 1
        maxReplicas: 10
        rules: [
          {
            name: 'queue-rule'
            custom: {
              type: 'azure-servicebus'
              metadata: {
                queueName: entityNameForScaling
                namespace: serviceBusNamespace.name
                messageCount: '100'
              }
              auth: [
                {
                  secretRef: 'servicebus-connection'
                  triggerParameter: 'connection'
                }
              ]
            }

          }
        ]
      }

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

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;This setting makes ACA scale replicas up when there are more than 100 messages in active queue.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Results&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;A first batch of tests in August'23 revealed no substantial disparity between the stacks:&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%2Femzicgsog6rkrxn52qe4.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%2Femzicgsog6rkrxn52qe4.png" alt="comparing runtimes in August" width="390" height="107"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To capture the final results in October'23, I ...&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;upgraded dependencies of the .NET projects (e.g. to Dapr 1.11)&lt;/li&gt;
&lt;li&gt;switched from Azure Service Bus Standard Tier to Premium because of that throttling issue explained below, which imho gave the whole scenario a major boost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After these upgrades and probably back-end rework done by Microsoft now a much clearer spread of average duration can be seen: Dapr is obviously handling the processing faster than Functions in Container on ACA and then (currently) Functions on ACA shows the worst performance in average:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;West Europe&lt;/th&gt;
&lt;th&gt;West US&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&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%2Ftppnurfsapuf21blt82q.png" alt="comparing total runtimes in October in West Europe" width="385" height="101"&gt;&lt;/td&gt;
&lt;td&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%2F8sf0wgnn4vmcm0j5h4tu.png" alt="comparing total runtimes in October West US" width="385" height="101"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;To be sure to have no regional deployment effects, I deployed and tested in 2 regions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Looking on the time dimension one can see that Functions on ACA has a wider spread of durations - even processing faster than Dapr at some points:&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%2Fbfjomm458tcpbmz9wbao.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%2Fbfjomm458tcpbmz9wbao.png" alt="comparing runtimes over time in October" width="800" height="466"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I am sure, that throughput of all variants can be improved by investing more time in measuring and fine tuning. My approach was to see what I can get out of the environment with a feasible amount of effort.&lt;/p&gt;




&lt;h2&gt;
  
  
  Nuggets and Gotchas
&lt;/h2&gt;

&lt;p&gt;Apart from the plain throughput evaluation above, I want to add the issues I stumbled over along the way - I guess this is the real "meat" of this post:&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploying Container Apps with no App yet built
&lt;/h3&gt;

&lt;p&gt;When deploying infrastructure without the apps yet being build, a Functions on ACA already needs a suitable container image to spin up. I solved this in &lt;strong&gt;Bicep&lt;/strong&gt; evaluating whether a ACR container image name was provided or not. Additional challenge then is that &lt;em&gt;DOCKER_REGISTRY...&lt;/em&gt; credentials are required for the final app image but not for the tempory dummy image.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
var effectiveImageName = imageName != '' ? imageName : 'mcr.microsoft.com/azure-functions/dotnet7-quickstart-demo:1.0'

var appSetingsBasic = [
  {
    name: 'AzureWebJobsStorage'
    value: 'DefaultEndpointsProtocol=https;AccountName=${stg.name};AccountKey=${stg.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}'
  }
  {
    name: 'STORAGE_CONNECTION'
    value: 'DefaultEndpointsProtocol=https;AccountName=${stg.name};AccountKey=${stg.listKeys().keys[0].value};EndpointSuffix=${environment().suffixes.storage}'
  }
  {
    name: 'SERVICEBUS_CONNECTION'
    value: '${listKeys('${serviceBusNamespace.id}/AuthorizationRules/RootManageSharedAccessKey', serviceBusNamespace.apiVersion).primaryConnectionString}'
  }
  {
    name: 'APPLICATIONINSIGHTS_CONNECTION_STRING'
    value: appInsights.properties.ConnectionString
  }
]

var appSetingsRegistry = [
  {
    name: 'DOCKER_REGISTRY_SERVER_URL'
    value: containerRegistry.properties.loginServer
  }
  {
    name: 'DOCKER_REGISTRY_SERVER_USERNAME'
    value: containerRegistry.listCredentials().username
  }
  {
    name: 'DOCKER_REGISTRY_SERVER_PASSWORD'
    value: containerRegistry.listCredentials().passwords[0].value
  }
  // https://github.com/Azure/Azure-Functions/wiki/When-and-Why-should-I-set-WEBSITE_ENABLE_APP_SERVICE_STORAGE
  // case 3a
  {
    name: 'WEBSITES_ENABLE_APP_SERVICE_STORAGE'
    value: 'false'
  }
]

var appSettings = concat(appSetingsBasic, imageName != '' ? appSetingsRegistry : [])

resource acafunction 'Microsoft.Web/sites@2022-09-01' = {
  name: '${envName}${appName}'
  location: location
  tags: union(tags, {
      'azd-service-name': appName
    })
  kind: 'functionapp'
  properties: {
    managedEnvironmentId: containerAppsEnvironment.id

    siteConfig: {
      linuxFxVersion: 'DOCKER|${effectiveImageName}'
      appSettings: appSettings
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Exactly at this point I struggle with &lt;strong&gt;Azure Developer CLI&lt;/strong&gt; currently: I am able to deploy &lt;strong&gt;infra&lt;/strong&gt; with the dummy image but as soon as I want to deploy the &lt;strong&gt;service&lt;/strong&gt;, the service deployment does not apply the above logic and set the &lt;em&gt;DOCKER_REGISTRY...&lt;/em&gt; credentials. Triggering the very same &lt;strong&gt;Bicep&lt;/strong&gt; templates with &lt;strong&gt;Azure CLI&lt;/strong&gt; seems to handle this switch properly.&lt;br&gt;
I had to use these credentials as managed identity was not working yet as supposed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Extending ApplicationInsights cloud_RoleName and cloud_RoleInstance for Dapr
&lt;/h3&gt;

&lt;p&gt;When hosting ASP.NET with Dapr on Container Apps, &lt;code&gt;cloud_RoleName&lt;/code&gt; and &lt;code&gt;cloud_RoleInstance&lt;/code&gt; are not populated - which I needed to evaluate how many instances / replicas are scaled.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/KaiWalter/message-distribution/blob/v1.0.1/src/daprdistributor/AppInsightsTelemetryInitializer.cs" rel="noopener noreferrer"&gt;AppInsightsTelemetryInitializer.cs&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.ApplicationInsights.Channel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;Microsoft.ApplicationInsights.Extensibility&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;namespace&lt;/span&gt; &lt;span class="nn"&gt;Utils&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AppInsightsTelemetryInitializer&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ITelemetryInitializer&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ITelemetry&lt;/span&gt; &lt;span class="n"&gt;telemetry&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;telemetry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cloud&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RoleName&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;telemetry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cloud&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RoleName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"CONTAINER_APP_NAME"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"CONTAINER_APP_NAME-not-set"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;IsNullOrEmpty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;telemetry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cloud&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RoleInstance&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;telemetry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Cloud&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RoleInstance&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Environment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetEnvironmentVariable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"HOSTNAME"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"HOSTNAME-not-set"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Program.cs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddApplicationInsightsTelemetry&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configure&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TelemetryConfiguration&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;((&lt;/span&gt;&lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;o&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TelemetryInitializers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;AppInsightsTelemetryInitializer&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Channeling .env values into Bash scripts for Azure CLI
&lt;/h3&gt;

&lt;p&gt;Coming from &lt;strong&gt;Azure Developer CLI&lt;/strong&gt; where I channel environment values with &lt;code&gt;source &amp;lt;(azd env get-values)&lt;/code&gt; into &lt;strong&gt;Bash&lt;/strong&gt;, I wanted to re-use as much of the scripts for &lt;strong&gt;Azure CLI&lt;/strong&gt; as possible.&lt;/p&gt;

&lt;p&gt;For that I created a &lt;code&gt;.env&lt;/code&gt; file in repository root like ...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AZURE_ENV_NAME="kw-md"
AZURE_LOCATION="westeurope"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... and then source its values into &lt;strong&gt;Bash&lt;/strong&gt; from which I then derive resource names to operate on with &lt;strong&gt;Azure CLI&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;
&lt;span class="nb"&gt;source&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;git rev-parse &lt;span class="nt"&gt;--show-toplevel&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;/.env&lt;span class="o"&gt;)&lt;/span&gt;

&lt;span class="nv"&gt;RESOURCE_GROUP_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;az group list  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"[?starts_with(name,'&lt;/span&gt;&lt;span class="nv"&gt;$AZURE_ENV_NAME&lt;/span&gt;&lt;span class="s2"&gt;')].name"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; tsv&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="nv"&gt;AZURE_CONTAINER_REGISTRY_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;az resource list &lt;span class="nt"&gt;--tag&lt;/span&gt; azd-env-name&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$AZURE_ENV_NAME&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"[?type=='Microsoft.ContainerRegistry/registries'].name"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; tsv&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="nv"&gt;AZURE_CONTAINER_REGISTRY_ENDPOINT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;az acr show &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$AZURE_CONTAINER_REGISTRY_NAME&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; loginServer &lt;span class="nt"&gt;-o&lt;/span&gt; tsv&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="nv"&gt;AZURE_CONTAINER_REGISTRY_ACRPULL_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;az identity list &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"[?ends_with(name,'acrpull')].id"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; tsv&lt;span class="sb"&gt;`&lt;/span&gt;
&lt;span class="nv"&gt;AZURE_KEY_VAULT_SERVICE_GET_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;az identity list &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"[?ends_with(name,'kv-get')].id"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; tsv&lt;span class="sb"&gt;`&lt;/span&gt;
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Dapr batching and bulk-message handling
&lt;/h3&gt;

&lt;p&gt;Dapr input binding and pub/sub Service Bus components need to be set to values much higher than &lt;a href="https://docs.dapr.io/reference/components-reference/supported-bindings/servicebusqueues/" rel="noopener noreferrer"&gt;the defaults&lt;/a&gt; to get a processing time better than Functions - keeping defaults shows Dapr E2E processing time almost factor 2 compared to Functions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        {
          name: 'maxActiveMessages'
          value: '1000'
        }
        {
          name: 'maxConcurrentHandlers'
          value: '8'
        }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;While activating bulk-message handling on the ServiceBus Dapr component did not show any significant effect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;        {
          name: 'maxBulkSubCount'
          value: '100'
        }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Functions batching
&lt;/h3&gt;

&lt;p&gt;Changing from single message dispatching to batched message dispatching and thus using batching &lt;code&gt;"MaxMessageBatchSize": 1000&lt;/code&gt; did not have a positive effect - on the contrary: processing time was 10-20% longer.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;single message dispatching&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;FunctionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Dispatch"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ServiceBusTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"q-order-ingress-func"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SERVICEBUS_CONNECTION"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;ingressMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ServiceBus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"q-order-express  {
&lt;/span&gt;    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="err"&gt;'&lt;/span&gt;&lt;span class="n"&gt;WEBSITE_SITE_NAME&lt;/span&gt;&lt;span class="err"&gt;'&lt;/span&gt;
    &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;appName&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;ollector&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ServiceBusMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;outputExpressMessages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ServiceBus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"q-order-standard-func"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SERVICEBUS_CONNECTION"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;ICollector&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ServiceBusMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;outputStandardMessages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ingressMessage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ingressMessage&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

            &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deserialize&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;ingressMessage&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

            &lt;span class="n"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ingressMessage&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;batched&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;FunctionName&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Dispatch"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ServiceBusTrigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"q-order-ingress-func"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SERVICEBUS_CONNECTION"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;ServiceBusReceivedMessage&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="n"&gt;ingressMessages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ServiceBus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"q-order-express-func"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SERVICEBUS_CONNECTION"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;ICollector&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ServiceBusMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;outputExpressMessages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;ServiceBus&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"q-order-standard-func"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Connection&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"SERVICEBUS_CONNECTION"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="n"&gt;ICollector&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ServiceBusMessage&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;outputStandardMessages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;ILogger&lt;/span&gt; &lt;span class="n"&gt;log&lt;/span&gt;&lt;span class="p"&gt;)-&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="s"&gt;", Connection = "&lt;/span&gt;&lt;span class="n"&gt;SERVICEBUS_CONNECTION&lt;/span&gt;&lt;span class="s"&gt;")] IC
&lt;/span&gt;        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;ingressMessage&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ingressMessages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deserialize&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Order&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;Encoding&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UTF8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ingressMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
                &lt;span class="n"&gt;ArgumentNullException&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ThrowIfNull&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;nameof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ingressMessage&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Functions not processing all messages
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;scheduleTimeStamp&lt;/th&gt;
&lt;th&gt;variant&lt;/th&gt;
&lt;th&gt;total message count&lt;/th&gt;
&lt;th&gt;duration ms&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2023-10-08T10:30:02.6868053Z&lt;/td&gt;
&lt;td&gt;ACAFQ&lt;/td&gt;
&lt;td&gt;20000&lt;/td&gt;
&lt;td&gt;161439&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023-10-08T10:39:04.8862227Z&lt;/td&gt;
&lt;td&gt;DAPRQ&lt;/td&gt;
&lt;td&gt;20000&lt;/td&gt;
&lt;td&gt;74056&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023-10-08T10:48:03.0727583Z&lt;/td&gt;
&lt;td&gt;FUNCQ&lt;/td&gt;
&lt;td&gt;19890 &lt;strong&gt;&amp;lt;---&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;81700&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023-10-08T10:57:43.6880713Z&lt;/td&gt;
&lt;td&gt;ACAFQ&lt;/td&gt;
&lt;td&gt;20000&lt;/td&gt;
&lt;td&gt;146270&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023-10-08T11:06:50.3649399Z&lt;/td&gt;
&lt;td&gt;DAPRQ&lt;/td&gt;
&lt;td&gt;20000&lt;/td&gt;
&lt;td&gt;95292&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023-10-08T11:15:49.0727755Z&lt;/td&gt;
&lt;td&gt;FUNCQ&lt;/td&gt;
&lt;td&gt;20000&lt;/td&gt;
&lt;td&gt;85025&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023-10-08T11:25:05.3765606Z&lt;/td&gt;
&lt;td&gt;ACAFQ&lt;/td&gt;
&lt;td&gt;20000&lt;/td&gt;
&lt;td&gt;137923&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023-10-08T11:34:03.8680341Z&lt;/td&gt;
&lt;td&gt;DAPRQ&lt;/td&gt;
&lt;td&gt;20000&lt;/td&gt;
&lt;td&gt;67746&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023-10-08T11:43:11.6807872Z&lt;/td&gt;
&lt;td&gt;FUNCQ&lt;/td&gt;
&lt;td&gt;20000&lt;/td&gt;
&lt;td&gt;84273&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023-10-08T11:52:36.0779390Z&lt;/td&gt;
&lt;td&gt;ACAFQ&lt;/td&gt;
&lt;td&gt;19753 &lt;strong&gt;&amp;lt;---&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;142073&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023-10-08T12:01:34.9800080Z&lt;/td&gt;
&lt;td&gt;DAPRQ&lt;/td&gt;
&lt;td&gt;20000&lt;/td&gt;
&lt;td&gt;55857&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023-10-08T12:10:34.5789563Z&lt;/td&gt;
&lt;td&gt;FUNCQ&lt;/td&gt;
&lt;td&gt;20000&lt;/td&gt;
&lt;td&gt;91777&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023-10-08T12:20:03.5812046Z&lt;/td&gt;
&lt;td&gt;ACAFQ&lt;/td&gt;
&lt;td&gt;20000&lt;/td&gt;
&lt;td&gt;154537&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023-10-08T12:29:01.8791564Z&lt;/td&gt;
&lt;td&gt;DAPRQ&lt;/td&gt;
&lt;td&gt;20000&lt;/td&gt;
&lt;td&gt;87938&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2023-10-08T12:38:03.6663978Z&lt;/td&gt;
&lt;td&gt;FUNCQ&lt;/td&gt;
&lt;td&gt;19975 &lt;strong&gt;&amp;lt;---&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;78416&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Looking at the queue items triggering &lt;code&gt;distributor&lt;/code&gt; logic ...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;requests
| where source startswith "sb-"
| where cloud_RoleName endswith "distributor"
| summarize count() by cloud_RoleName, bin(timestamp,15m)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;cloud_RoleName&lt;/th&gt;
&lt;th&gt;timestamp [UTC]&lt;/th&gt;
&lt;th&gt;count_&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;acafdistributor&lt;/td&gt;
&lt;td&gt;10/8/2023, 10:30:00.000 AM&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;funcdistributor&lt;/td&gt;
&lt;td&gt;10/8/2023, 10:45:00.000 AM&lt;/td&gt;
&lt;td&gt;9890 &lt;strong&gt;&amp;lt;---&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;acafdistributor&lt;/td&gt;
&lt;td&gt;10/8/2023, 10:45:00.000 AM&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;funcdistributor&lt;/td&gt;
&lt;td&gt;10/8/2023, 11:15:00.000 AM&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;acafdistributor&lt;/td&gt;
&lt;td&gt;10/8/2023, 11:15:00.000 AM&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;funcdistributor&lt;/td&gt;
&lt;td&gt;10/8/2023, 11:30:00.000 AM&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;acafdistributor&lt;/td&gt;
&lt;td&gt;10/8/2023, 11:45:00.000 AM&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;funcdistributor&lt;/td&gt;
&lt;td&gt;10/8/2023, 12:00:00.000 PM&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;acafdistributor&lt;/td&gt;
&lt;td&gt;10/8/2023, 12:15:00.000 PM&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;funcdistributor&lt;/td&gt;
&lt;td&gt;10/8/2023, 12:30:00.000 PM&lt;/td&gt;
&lt;td&gt;9975 &lt;strong&gt;&amp;lt;---&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;acafdistributor&lt;/td&gt;
&lt;td&gt;10/8/2023, 12:45:00.000 PM&lt;/td&gt;
&lt;td&gt;10000&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;... which is strange, considering that the respective &lt;code&gt;PushIngressFuncQ&lt;/code&gt; (at ~12:30) sent exactly 10.000 messages into the queue.&lt;/p&gt;

&lt;p&gt;Checking how much Service Bus dependencies have been generated for a particular request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dependencies
| where operation_Id == "cbc279bb851793e18b1c7ba69e24b9f7"
| where operation_Name == "PushIngressFuncQ"
| where type == "Queue Message | Azure Service Bus"
| summarize count()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So it seems, that between sending messages into and receiving messages from a queue, messages get lost - which is not acceptable for a scenario that assumed to be enterprise grade reliable. Checking Azure Service Bus metrics reveals, that the namespace is throttling requests:&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%2Fuyc3r34jscc078zttwqc.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%2Fuyc3r34jscc078zttwqc.png" alt="Graph showing that Azure Service Bus Standard is throttling" width="800" height="585"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;OK, but why? Reviewing &lt;a href="https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-throttling#what-are-the-credit-limits" rel="noopener noreferrer"&gt;how Azure Service Bus Standard Tier is handling throttling&lt;/a&gt; and considering the approach of moving 10.000 messages at once from &lt;em&gt;scheduled&lt;/em&gt; to &lt;em&gt;active&lt;/em&gt; hints towards this easily crashing the credit limit applied in Standard Tier. After changing to Premium Tier these throttlings definetely were gone. However when packing so much load simultaneously on the Functions stacks it seems to be system immanent, that not 100% of Functions requests are logged to Application Insights. According to my information this limitation should be put to Azure Functions some time soon.&lt;/p&gt;




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

&lt;p&gt;From results above one might immediately jump to conclude that Dapr (in an ASP.NET frame) suits best for such a message forwarding scenario, because it seems to offer best throughput and when combined with C# minimal APIs a simple enough programming model. Knowing from experience, this simple programming model will not necessarily scale to complex solutions or services with many endpoints and where a certain structure of code (see Clean Architecture etc.) and re-usability is required. Here the simplicity of Functions programming model &lt;em&gt;input-processing-output&lt;/em&gt; really can help scale even with not so mature teams - for certain scenarios. So as always in architecture it is about weighing aspects which are important to a planned environment: here technical performance vs team performance.&lt;/p&gt;

&lt;p&gt;Azure Functions on Container Apps combined with &lt;a href="https://github.com/Azure/azure-functions-dapr-extension" rel="noopener noreferrer"&gt;Dapr extension&lt;/a&gt; may help bringing some other aspects together: the capability to connect a huge variety of cloud resources with &lt;strong&gt;Dapr&lt;/strong&gt; paired with the simple programming model of &lt;strong&gt;Azure Functions&lt;/strong&gt;. I shall write about this topic soon in a future post.&lt;/p&gt;

&lt;p&gt;Cheers,&lt;br&gt;
Kai&lt;/p&gt;

</description>
      <category>azure</category>
      <category>dapr</category>
      <category>queues</category>
      <category>scalability</category>
    </item>
    <item>
      <title>Get NeoVim plugins with build processes working on Windows</title>
      <dc:creator>Kai Walter</dc:creator>
      <pubDate>Wed, 27 Sep 2023 18:35:03 +0000</pubDate>
      <link>https://dev.to/kaiwalter/get-neovim-plugins-with-build-processes-working-on-windows-i39</link>
      <guid>https://dev.to/kaiwalter/get-neovim-plugins-with-build-processes-working-on-windows-i39</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;In this post I show or share &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a PowerShell script to install LLVM / mingw  / make toolchains on Windows to be used by NeoVim plugin build processes&lt;/li&gt;
&lt;li&gt;how to install Microsoft Build Tools with &lt;strong&gt;winget&lt;/strong&gt; (that in the end did not qualify for all build cases and was discarded)&lt;/li&gt;
&lt;li&gt;a sample plugin configurations with &lt;strong&gt;Lazy&lt;/strong&gt; plugin manager&lt;/li&gt;
&lt;li&gt;an observations I made with plugin managers not working smoothly through corporate proxy configurations&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;I just want to have equal editing experience on Windows and Linux - not having myself to adapt when flipping back and forth. With an ecosystem like Visual Studio Code that is practically given without the need to care. But that's not how I am wired - I want to understand what's going on. Here imho NeoVim with Lua plugins is easier to digest and understand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;In a &lt;a href="https://dev.to/kaiwalter/share-neovim-configuration-between-linux-and-windows-4gh8"&gt;previous post&lt;/a&gt; I was showing how I was sharing one configuration for NeoVim based on one version of dotfiles in Linux and Windows.&lt;/p&gt;

&lt;p&gt;That works pretty well for &lt;a href="https://www.lua.org/" rel="noopener noreferrer"&gt;Lua&lt;/a&gt;-only plugins, as long as underlying command line tools like e.g. &lt;code&gt;ripgrep&lt;/code&gt;  or &lt;code&gt;lazygit&lt;/code&gt; are also avaible on Windows. As soon as plugins require a tool chain to build those command line tools from source, some more work is required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Selecting the right build tool chain
&lt;/h2&gt;

&lt;h3&gt;
  
  
  MS Build Tools and Gnu Make
&lt;/h3&gt;

&lt;p&gt;With this combination I was able to get &lt;strong&gt;Treesitter&lt;/strong&gt; installed and built on Windows. It requires an installation like ...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# install make
$makePath = Join-Path ${env:ProgramFiles(x86)} "GnuWin32" "bin"
if(!(Test-Path $makePath -PathType Container)) {
  winget install GnuWin32.Make
}

# install MS Build Tools
$msbtProgramFolder = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio" "2022" "BuildTools"
if(!(Test-Path $msbtProgramFolder -PathType Container)) {
    winget install Microsoft.VisualStudio.2022.BuildTools
    winget install --id Microsoft.VisualStudio.2022.BuildTools --override $("--passive --config " + (Join-Path $PSScriptRoot "BuildTools.vsconfig"))
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... with an accompaning &lt;strong&gt;BuildTools.vsconfig&lt;/strong&gt; to define components to be installed ...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "version": "1.0",
  "components": [
    "Microsoft.VisualStudio.Component.Roslyn.Compiler",
    "Microsoft.Component.MSBuild",
    "Microsoft.VisualStudio.Component.CoreBuildTools",
    "Microsoft.VisualStudio.Workload.MSBuildTools",
    "Microsoft.VisualStudio.Component.Windows10SDK",
    "Microsoft.VisualStudio.Component.VC.CoreBuildTools",
    "Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
    "Microsoft.VisualStudio.Component.VC.Redist.14.Latest",
    "Microsoft.VisualStudio.Component.Windows11SDK.22000",
    "Microsoft.VisualStudio.Component.TextTemplating",
    "Microsoft.VisualStudio.Component.VC.CoreIde",
    "Microsoft.VisualStudio.ComponentGroup.NativeDesktop.Core",
    "Microsoft.VisualStudio.Workload.VCTools",
    "Microsoft.VisualStudio.Component.VC.14.35.17.5.ATL.Spectre",
    "Microsoft.VisualStudio.Component.VC.14.35.17.5.MFC.Spectre",
    "Microsoft.VisualStudio.Component.VC.14.36.17.6.ATL.Spectre",
    "Microsoft.VisualStudio.Component.VC.14.36.17.6.MFC.Spectre"
  ]
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... and that, on plugin-installation, NeoVim is started from "Visual Studio Developer PowerShell" command prompt while adding Gnu Make to the path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$makePath = Join-Path ${env:ProgramFiles(x86)} "GnuWin32" "bin"
$env:Path += ";" + $makePath
nvim
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yet this setup was not sufficient for &lt;strong&gt;Telescope/fzf&lt;/strong&gt;, as only a &lt;strong&gt;clang&lt;/strong&gt; but obviously no &lt;strong&gt;gcc&lt;/strong&gt; compiler seemed to be available with MS Build Tools. I succeeded when adding a &lt;strong&gt;gcc&lt;/strong&gt; into the mix, but was not really happy with the extra "Visual Studio Developer PowerShell" command prompt required.&lt;/p&gt;

&lt;h3&gt;
  
  
  LLVM/Clang/LLD based mingw-w64
&lt;/h3&gt;

&lt;p&gt;In this toolchain I found &lt;strong&gt;clang&lt;/strong&gt; and &lt;strong&gt;gcc&lt;/strong&gt; compilers. Also it allowed me to just add it to the path (see script &lt;code&gt;NeoVimPluginInstall.ps1&lt;/code&gt; below) of my current shell when directing NeoVim into a plugin installation - without an extra command prompt like above.&lt;/p&gt;

&lt;p&gt;That helped me to use the same build process configuration for &lt;strong&gt;Telescope/fzf&lt;/strong&gt; on Linux and Windows without any changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;return {
    "nvim-telescope/telescope.nvim",
    branch = "0.1.x",
    dependencies = {
        "nvim-lua/plenary.nvim",
        {
            "nvim-telescope/telescope-fzf-native.nvim",
            build = "make", -- &amp;lt;&amp;lt;===== make initiates build process on Windows and Linux
        },
        "nvim-tree/nvim-web-devicons",
    },
    config = function() 
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;In the case of this plugin I tried to modify the &lt;code&gt;build&lt;/code&gt; string from &lt;code&gt;make&lt;/code&gt; to &lt;code&gt;cmake -S. -Bbuild -DCMAKE_BUILD_TYPE=Release &amp;amp;&amp;amp; cmake --build build --config Release &amp;amp;&amp;amp; cmake --install build --prefix build&lt;/code&gt; when running on Windows, but even with this small patch I was not able to cleanly succeed with MS Build Tools.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  NeoVim installation script
&lt;/h2&gt;

&lt;p&gt;Since my &lt;a href="https://dev.to/kaiwalter/share-neovim-configuration-between-linux-and-windows-4gh8"&gt;previous post&lt;/a&gt; I extended and hardened the installation script a bit. It now additionally installs&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;ripgrep&lt;/strong&gt; for Telescope &lt;code&gt;live_grep&lt;/code&gt; string finder&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;lazygit&lt;/strong&gt; for the equally named plugin&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gnu make&lt;/strong&gt; to drive some of the build processes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;llvm-mingw64&lt;/strong&gt; the &lt;a href="https://github.com/mstorsjo/llvm-mingw" rel="noopener noreferrer"&gt;LLVM/Clang/LLD based mingw-w64 toolchain&lt;/a&gt; from above
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[CmdletBinding()]
param (
    [Parameter()]
    [switch]
    $ResetState
)

# install NeoVim with WinGet, if not already present on system
if (!$(Get-Command nvim -ErrorAction SilentlyContinue)) {
    winget install Neovim.Neovim
}

# install ripgrep
if (!$(Get-Command rg -ErrorAction SilentlyContinue)) {
  winget install BurntSushi.ripgrep.MSVC
}

# install lazygit
if (!$(Get-Command lazygit -ErrorAction SilentlyContinue)) {
  winget install JesseDuffield.lazygit
}

# install make
$makePath = Join-Path ${env:ProgramFiles(x86)} "GnuWin32" "bin"
if(!(Test-Path $makePath -PathType Container)) {
  winget install GnuWin32.Make
}

$llvmFolder = Get-ChildItem -Path $env:LOCALAPPDATA -Filter "llvm*x86_64" | Select-Object -ExpandProperty FullName
if(!$llvmFolder -or !(Test-Path $llvmFolder -PathType Container)) {
  $downloadFile = "llvm-mingw.zip"
  . .\getLatestGithubRepo.ps1 -Repository "mstorsjo/llvm-mingw" -DownloadFilePattern "llvm-mingw-.*-msvcrt-x86_64.zip" -DownloadFile $downloadFile
  Expand-Archive -Path $(Join-Path $env:TEMP $downloadFile) -DestinationPath $env:LOCALAPPDATA
}

# clone my Dotfiles repo
$dotFilesRoot = Join-Path $HOME "dotfiles"

if (!(Test-Path $dotFilesRoot -PathType Container)) {
    git clone git@github.com:KaiWalter/dotfiles.git $dotFilesRoot
}

# link NeoVim configuration
$localConfiguration = Join-Path $env:LOCALAPPDATA "nvim"
$dotfilesConfiguration = Join-Path $dotFilesRoot ".config" "nvim"

if (!(Test-Path $localConfiguration -PathType Container)) { 
    Start-Process -FilePath "pwsh" -ArgumentList "-c New-Item -Path $localConfiguration -ItemType SymbolicLink -Value $dotfilesConfiguration".Split(" ") -Verb runas
}

# reset local state if required
$localState = Join-Path $env:LOCALAPPDATA "nvim-data"

if($ResetState) {
    if(Test-Path $localState -PathType Container) {
        Remove-Item $localState -Recurse -Force
        New-Item $localState -ItemType Directory
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;ATTENTION: &lt;code&gt;git@github.com:KaiWalter/dotfiles.git&lt;/code&gt; is my private Dotfiles repo - if you want to replicate my approach you would need to work from your own version;&lt;br&gt;
script &lt;code&gt;.\getLatestGithubRepo.ps1&lt;/code&gt; downloads the latest binary / installation file from a GitHub's repo release page and is shown below&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Lua configuration
&lt;/h2&gt;

&lt;p&gt;When I started with my NeoVim journey a few months back, I followed the suggestions from the NeoVim main protagonists and looked into some of the NeoVim &lt;a href="https://medium.com/@adaml.poniatowski/exploring-the-top-neovim-distributions-lazyvim-lunarvim-astrovim-and-nvchad-which-one-reigns-3adcdbfa478d" rel="noopener noreferrer"&gt;distros&lt;/a&gt; like &lt;a href="https://www.lazyvim.org/" rel="noopener noreferrer"&gt;LazyVim&lt;/a&gt;,  &lt;a href="https://www.lunarvim.org/" rel="noopener noreferrer"&gt;LunarVim&lt;/a&gt;, &lt;a href="https://astronvim.com/" rel="noopener noreferrer"&gt;AstroVim&lt;/a&gt;, and &lt;a href="https://nvchad.com/" rel="noopener noreferrer"&gt;NVChad&lt;/a&gt; to make life easier (coming fresh from Visual Studio Code even to make life even bearable). Having no knowledge in the NeoVim plugin ecosystem I struggled and stopped to try to get these distros working in parallel on Linux and Windows. Hence I decided to build a configuration with &lt;strong&gt;Packer&lt;/strong&gt; from scratch to find and understand the spots, where it breaks.&lt;/p&gt;

&lt;p&gt;While succeeding in getting NeoVim working smoothly with plugins on my own Windows machines, I struggled on my company laptop. Plugin installation with &lt;strong&gt;Packer&lt;/strong&gt; was lagging at best sometimes even hanging. Digging deeper I was able to pin the problem to our companies proxy which was interfering in the package downloads.&lt;/p&gt;

&lt;p&gt;Anyway as of August'23 it is announced on the &lt;a href="https://github.com/wbthomason/packer.nvim" rel="noopener noreferrer"&gt;Packer&lt;/a&gt; repo README, that it is not maintained anymore and suggested to move to another package manager.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/folke/lazy.nvim" rel="noopener noreferrer"&gt;lazy.nvim&lt;/a&gt; seemed to be the next best one package manager for me - also &lt;strong&gt;LazyVim&lt;/strong&gt; distro which is based on that package manager and which I checked out earlier best related to what I was looking for. Additionally the download problems with our company proxy did not manifest here.&lt;/p&gt;

&lt;p&gt;When converting from &lt;strong&gt;Packer&lt;/strong&gt; to &lt;strong&gt;Lazy&lt;/strong&gt; I wanted to clean up my configuraton file structure and follow some good practise (which is always subjective, I know) and hence I leaned on the &lt;a href="https://github.com/josean-dev/dev-environment-files" rel="noopener noreferrer"&gt;NeoVim configuration of Josean Martinez&lt;/a&gt; which he explains in this &lt;a href="https://youtu.be/NL8D8EkphUw" rel="noopener noreferrer"&gt;video&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;~/.config/nvim $ tree -n --charset UTF-16
|-- init.lua
|-- lazy-lock.json
`-- lua
    `-- kws
        |-- init.lua
        |-- lazy.lua
        |-- plugins
        |   |-- colorschema.lua
        |   |-- comment.lua
        |   |-- dap.lua
        |   |-- dressing.lua
        |   |-- harpoon.lua
        |   |-- init.lua
        |   |-- lsp
        |   |   |-- lspconfig.lua
        |   |   |-- mason.lua
        |   |   `-- null-ls.lua
        |   |-- lualine.lua
        |   |-- nvim-cmp.lua
        |   |-- nvim-tree.lua
        |   |-- nvim-treesitter.lua
        |   |-- nvim-treesitter-text-objects.lua
        |   |-- telescope.lua
        |   `-- which-key.lua
        |-- remap.lua
        `-- utils.lua
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So basically &lt;code&gt;lazy.lua&lt;/code&gt; just bootstraps the package manager itself and then pulls in the plugin specifications from &lt;code&gt;plugins&lt;/code&gt; and &lt;code&gt;plugins/lsp&lt;/code&gt; folders.&lt;/p&gt;

&lt;h2&gt;
  
  
  utility scripts
&lt;/h2&gt;

&lt;h3&gt;
  
  
  NeoVimPluginInstall.ps1
&lt;/h3&gt;

&lt;p&gt;While the setup PowerShell script above installs all the tools, I created another script which I use when starting NeoVim with the intention to install or update plugins. I did not want to put these folders on my search path permanently to not pollute the search path too much.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$llvmPath = Join-Path $(Get-ChildItem -Path $env:LOCALAPPDATA -Filter "llvm*x86_64" | Select-Object -ExpandProperty FullName) "bin"
$makePath = Join-Path ${env:ProgramFiles(x86)} "GnuWin32" "bin"
$env:Path += ";" + $llvmPath + ";" + $makePath
nvim $args
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  getLatestGithubRepo.ps1
&lt;/h3&gt;

&lt;p&gt;This script is a generalization of another &lt;a href="https://community.ops.io/kaiwalter/install-winget-latest-release-with-powershell-in-one-go-3kka" rel="noopener noreferrer"&gt;post&lt;/a&gt; to find and download a file with a given complete file name or a file pattern from a GitHub repo's latest releases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[CmdletBinding()]
param (
    [Parameter(Mandatory = $true, Position = 1)]
    [string] $Repository,
    [Parameter(Position = 2)]
    [string] $DownloadFile,
    [string] $DownloadFilePattern = "NOT_TO_BE_FOUND"
)

$latestRelease = Invoke-RestMethod -Method Get -Uri https://api.github.com/repos/$Repository/releases/latest -StatusCodeVariable sc

if ($sc -eq 200) {
    Write-Host $latestRelease.tag_name $latestRelease.published_at
    foreach ($asset in $latestRelease.assets) {
        Write-Host $asset.name $asset.size
        if($asset.name -eq $DownloadFile -or $asset.name -match $DownloadFilePattern) {
            $target = Join-Path $env:TEMP $DownloadFile
            Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $target
            Write-Host "downloaded" $asset.browser_download_url "to" $target
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The End
&lt;/h2&gt;

&lt;p&gt;With this setup I am content for the moment. From here I will add LSP servers, stylers, linters and other plugins I expect to improve my productivity. Working exclusively with NeoVim for my very few coding workloads now for ~4 months gives me enough proficiency to really enjoy editing code in my spare time. If I just could have VIM motions in MS Outlook and MS Word 😏 I would not need to reconfigure my brain when switching from day job to spare time activity.&lt;/p&gt;

</description>
      <category>powershell</category>
      <category>neovim</category>
      <category>windows</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Azure VM based on CBL-Mariner with Nix package manager</title>
      <dc:creator>Kai Walter</dc:creator>
      <pubDate>Sat, 29 Jul 2023 16:47:09 +0000</pubDate>
      <link>https://dev.to/kaiwalter/azure-vm-based-on-cbl-mariner-with-nix-package-manager-243f</link>
      <guid>https://dev.to/kaiwalter/azure-vm-based-on-cbl-mariner-with-nix-package-manager-243f</guid>
      <description>&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;In a previous &lt;a href="https://dev.to/kaiwalter/create-a-disposable-azure-vm-based-on-cbl-mariner-2013"&gt;post&lt;/a&gt; I was showing how to bring up a disposable CBL-Mariner* VM using cloud-init and (mostly) the dnf package manager. As I explained in that post, it takes some fiddling around to find sources for various packages and also to mix installation methods.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;* when reviewing I found that I had a small typo in the post - "CBM-Mariner" - I guess my subconscious mind partially still lives in the 8-bit era&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When I was coming across &lt;a href="https://nixos.org/manual/nix/stable/package-management/basic-package-mgmt.html" rel="noopener noreferrer"&gt;Nix package manager&lt;/a&gt; a few days back - while again distro hopping for my home experimental machine - I thought to combine both and maybe make package installation simpler and more versatile for CBL-Mariner - with its intended small repository to keep attack surface low.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This approach, to bring in &lt;strong&gt;Nix&lt;/strong&gt; over &lt;code&gt;cloud-init&lt;/code&gt;, should work with a vast amount of distros and should not be limited to CBL-Mariner only!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  create.sh - Creation script
&lt;/h2&gt;

&lt;p&gt;In this post I want to use a &lt;strong&gt;Bash&lt;/strong&gt; script and &lt;strong&gt;Azure CLI&lt;/strong&gt; to drive VM installation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;admin
&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;my-cblnix
&lt;span class="nv"&gt;location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;westeurope
&lt;span class="nv"&gt;keyfile&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;~/.ssh/cblnix

az deployment sub create &lt;span class="nt"&gt;-f&lt;/span&gt; ./rg.bicep &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="nv"&gt;$location&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="nv"&gt;computerName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;resourceGroupName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;location&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$location&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;adminUsername&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;adminPasswordOrKey&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="nv"&gt;$keyfile&lt;/span&gt;.pub&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;customData&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ./cloud-init-cbl-nix.txt&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;vmSize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Standard_D2s_v3 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;vmImagePublisher&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;MicrosoftCBLMariner &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;vmImageOffer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cbl-mariner &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nv"&gt;vmImageSku&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cbl-mariner-2

&lt;span class="nv"&gt;fqdn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;az network public-ip show &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="nt"&gt;-ip&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'dnsSettings.fqdn'&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ssh -i &lt;/span&gt;&lt;span class="nv"&gt;$keyfile&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$user&lt;/span&gt;&lt;span class="s2"&gt;@&lt;/span&gt;&lt;span class="nv"&gt;$fqdn&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

ssh-keygen &lt;span class="nt"&gt;-R&lt;/span&gt; &lt;span class="nv"&gt;$fqdn&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key elements and assumptions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Azure CLI using a set of Bicep templates (shown below) to deploy a Resource Group and a Virtual Machine with the same name&lt;/li&gt;
&lt;li&gt;it is assumed that public SSH key file has the same and is in the same location than the private SSH key file, just with a &lt;code&gt;.pub&lt;/code&gt; extension&lt;/li&gt;
&lt;li&gt;after VM creation FQDN is determined and printed&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ssh-keygen&lt;/code&gt; is used to clean up potentially existing entries in SSH's &lt;code&gt;known_hosts&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  cloud-init.txt
&lt;/h2&gt;

&lt;p&gt;As in the previous post a &lt;code&gt;cloud-init.txt&lt;/code&gt; is required to bootstrap the basic installation of the VM - but now in a much cleaner shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#cloud-config
write_files:
  - path: /tmp/install-nix.sh
    content: |
      #!/bin/bash
      sh &amp;lt;(curl -L https://nixos.org/nix/install) --daemon --yes
    permissions: '0755'
  - path: /tmp/base-setup.nix
    content: |
        with import &amp;lt;nixpkgs&amp;gt; {}; [
          less
          curl
          git
          gh
          azure-cli
          kubectl
          nodejs_18
          rustup
          go
          dotnet-sdk_7
          zsh
          oh-my-zsh
        ]
    permissions: '0644'
runcmd:
- export USER=$(awk -v uid=1000 -F":" '{ if($3==uid){print $1} }' /etc/passwd)

- sudo -H -u $USER bash -c '/tmp/install-nix.sh'

- /nix/var/nix/profiles/default/bin/nix-env -if /tmp/base-setup.nix

- - sudo -H -u $USER bash -c '/nix/var/nix/profiles/default/bin/rustup default stable'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Gotchas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;daemon installation of Nix package manager needs to be executed in the context of the VM main user&lt;/li&gt;
&lt;li&gt;after daemon installation &lt;code&gt;nix-env&lt;/code&gt; and all installed binaries reside in &lt;code&gt;/nix/var/nix/profiles/default/bin&lt;/code&gt; folder but as shell has not been restarted links to those binaries are not available to the session and have to be started from that location&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;do not forget to &lt;code&gt;sudo tail -f /var/log/cloud-init-output.log&lt;/code&gt; to check or observe the finalization of the installation which will take some time after the VM is deployed&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  rg.bicep
&lt;/h2&gt;

&lt;p&gt;To achieve VM installation including its Resource Group, installation is framed with this &lt;strong&gt;Bicep&lt;/strong&gt; template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;targetScope = 'subscription' // Resource group must be deployed under 'subscription' scope

param location string
param resourceGroupName string
param computerName string
param vmSize string = 'Standard_DS1_v2'
param adminUsername string = 'admin'
@secure()
param adminPasswordOrKey string
param customData string

param vmImagePublisher string
param vmImageOffer string
param vmImageSku string

resource rg 'Microsoft.Resources/resourceGroups@2021-01-01' = {
  name: resourceGroupName
  location: location
}

module vm 'vm.bicep' = {
  name: 'vm'
  scope: rg
  params: {
    location: location
    computerName: computerName
    vmSize: vmSize
    vmImagePublisher: vmImagePublisher
    vmImageOffer: vmImageOffer
    vmImageSku: vmImageSku
    adminUsername: adminUsername
    adminPasswordOrKey: adminPasswordOrKey
    customData: customData
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  vm.bicep
&lt;/h2&gt;

&lt;p&gt;Then VM is deployed with another &lt;strong&gt;Bicep&lt;/strong&gt; in the scope of the Resource Group:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;param location string = resourceGroup().location
param computerName string
param vmSize string = 'Standard_D2s_v3'

param adminUsername string = 'admin'
@secure()
param adminPasswordOrKey string
param customData string = 'echo customData'

var authenticationType = 'sshPublicKey'
param vmImagePublisher string
param vmImageOffer string
param vmImageSku string

var vnetAddressPrefix = '192.168.43.0/27'

var vmPublicIPAddressName = '${computerName}-ip'
var vmVnetName = '${computerName}-vnet'
var vmNsgName = '${computerName}-nsg'
var vmNicName = '${computerName}-nic'
var vmDiagnosticStorageAccountName = '${replace(computerName, '-', '')}${uniqueString(resourceGroup().id)}'

var shutdownTime = '2200'
var shutdownTimeZone = 'W. Europe Standard Time'

var linuxConfiguration = {
  disablePasswordAuthentication: true
  ssh: {
    publicKeys: [
      {
        path: '/home/${adminUsername}/.ssh/authorized_keys'
        keyData: adminPasswordOrKey
      }
    ]
  }
}

var resourceTags = {
  vmName: computerName
}

resource vmNsg 'Microsoft.Network/networkSecurityGroups@2022-01-01' = {
  name: vmNsgName
  location: location
  tags: resourceTags
  properties: {
    securityRules: [
      {
        name: 'in-SSH'
        properties: {
          protocol: 'Tcp'
          sourcePortRange: '*'
          destinationPortRange: '22'
          sourceAddressPrefix: '*'
          destinationAddressPrefix: '*'
          access: 'Allow'
          priority: 1000
          direction: 'Inbound'
        }
      }
    ]
  }
}

resource vmVnet 'Microsoft.Network/virtualNetworks@2022-01-01' = {
  name: vmVnetName
  location: location
  tags: resourceTags
  properties: {
    addressSpace: {
      addressPrefixes: [
        vnetAddressPrefix
      ]
    }
  }
}

resource vmSubnet 'Microsoft.Network/virtualNetworks/subnets@2022-01-01' = {
  name: 'vm'
  parent: vmVnet
  properties: {
    addressPrefix: vnetAddressPrefix
    networkSecurityGroup: {
      id: vmNsg.id
    }
  }
}

resource vmDiagnosticStorage 'Microsoft.Storage/storageAccounts@2019-06-01' = {
  name: vmDiagnosticStorageAccountName
  location: location
  sku: {
    name: 'Standard_LRS'
  }
  kind: 'Storage'
  tags: resourceTags
  properties: {}
}

resource vmPublicIP 'Microsoft.Network/publicIPAddresses@2019-11-01' = {
  name: vmPublicIPAddressName
  location: location
  tags: resourceTags
  properties: {
    publicIPAllocationMethod: 'Dynamic'
    dnsSettings: {
      domainNameLabel: computerName
    }
  }
}

resource vmNic 'Microsoft.Network/networkInterfaces@2022-01-01' = {
  name: vmNicName
  location: location
  tags: resourceTags
  properties: {
    ipConfigurations: [
      {
        name: 'ipconfig1'
        properties: {
          privateIPAllocationMethod: 'Dynamic'
          publicIPAddress: {
            id: vmPublicIP.id
          }
          subnet: {
            id: vmSubnet.id
          }
        }
      }
    ]
  }
}

resource vm 'Microsoft.Compute/virtualMachines@2022-03-01' = {
  name: computerName
  location: location
  tags: resourceTags
  identity: {
    type: 'SystemAssigned'
  }
  properties: {
    priority: 'Spot'
    evictionPolicy: 'Deallocate'
    billingProfile: {
      maxPrice: -1
    }
    hardwareProfile: {
      vmSize: vmSize
    }
    storageProfile: {
      imageReference: {
        publisher: vmImagePublisher
        offer: vmImageOffer
        sku: vmImageSku
        version: 'latest'
      }
      osDisk: {
        createOption: 'FromImage'
        diskSizeGB: 1024
      }
    }
    osProfile: {
      computerName: computerName
      adminUsername: adminUsername
      adminPassword: adminPasswordOrKey
      customData: base64(customData)
      linuxConfiguration: ((authenticationType == 'password') ? null : linuxConfiguration)
    }
    networkProfile: {
      networkInterfaces: [
        {
          id: vmNic.id
        }
      ]
    }
    diagnosticsProfile: {
      bootDiagnostics: {
        enabled: true
        storageUri: vmDiagnosticStorage.properties.primaryEndpoints.blob
      }
    }
  }
}

resource vmShutdown 'Microsoft.DevTestLab/schedules@2018-09-15' = {
  name: 'shutdown-computevm-${computerName}'
  location: location
  tags: resourceTags
  properties: {
    status: 'Enabled'
    taskType: 'ComputeVmShutdownTask'
    dailyRecurrence: {
      time: shutdownTime
    }
    timeZoneId: shutdownTimeZone
    notificationSettings: {
      status: 'Disabled'
    }
    targetResourceId: vm.id
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Key elements and assumptions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VM is installed with its own virtual network - in case VM would need to be integrated in an existing VNET, that part would need adaption&lt;/li&gt;
&lt;li&gt;a Network Security Group is created and added to the Subnet which opens SSH-port 22 - for non-experimental use it is advised to place the VM behind a Bastion service, use Just-In-Time access or protect otherwise&lt;/li&gt;
&lt;li&gt;automatic VM shutdown is achieved with a &lt;code&gt;DevTestLab/schedules&lt;/code&gt; resource, be aware that such a resource is not available everywhere e.g. missing in Azure China; additionally time zone and point of time are hard-wired currently, please adapt to your own needs&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What else can you do with Nix?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  nix-shell - temporarily running packages
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;nix-shell&lt;/code&gt; can be used to bring a package temporarily - without modifying your system persistently - to your system and shell into an environment, where you can use the package until you &lt;code&gt;exit&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ nix-shell -p python311 --run python
Python 3.11.4 (main, Jun  6 2023, 22:16:46) [GCC 12.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
&amp;gt;&amp;gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the case of the Nix Python package, it can even be extended that particular Python libraries are made available temporarily:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ nix-shell -p '((import &amp;lt;nixpkgs&amp;gt; {}).python311.withPackages (p: [p.numpy, p.pandas]))' --run python
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  nix-store - managing Nix store
&lt;/h3&gt;

&lt;p&gt;A useful command to clean up local packages from the store, which are no longer linked, is &lt;code&gt;nix-store --gc&lt;/code&gt;. &lt;/p&gt;

&lt;h2&gt;
  
  
  A slightly advanced configuration
&lt;/h2&gt;

&lt;p&gt;This configuration adds&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;activating experimental feature &lt;strong&gt;Nix Flakes&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;installing &lt;strong&gt;Docker&lt;/strong&gt; as a service&lt;/li&gt;
&lt;li&gt;separates packages I want to have system wide in &lt;code&gt;/nix/var/nix/profiles/default/bin&lt;/code&gt; (Docker, less and curl) and only for the user in &lt;code&gt;~/.nix-profile/bin&lt;/code&gt; (Git and Rust toolchain)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#cloud-config
write_files:
  - path: /tmp/install-nix.sh
    content: | 
      #!/bin/bash
      sh &amp;lt;(curl -L https://nixos.org/nix/install) --daemon --yes
      mkdir -p ~/.config/nix
      echo "experimental-features = nix-command" &amp;gt; ~/.config/nix/nix.conf
    permissions: '0755'
  - path: /tmp/root-setup.nix
    content: | 
        with import &amp;lt;nixpkgs&amp;gt; {}; [
          less
          curl
          docker
        ]
    permissions: '0644'
  - path: /tmp/user-setup.nix
    content: | 
        with import &amp;lt;nixpkgs&amp;gt; {}; [
          git
          rustup
        ]
    permissions: '0644'
  - path: /usr/lib/systemd/system/docker.service
    content: | 
        [Unit]
        Description=Docker Application Container Engine
        Documentation=https://docs.docker.com
        After=network.target

        [Service]
        Type=notify
        # the default is not to use systemd for cgroups because the delegate issues still
        # exists and systemd currently does not support the cgroup feature set required
        # for containers run by docker
        ExecStart=/nix/var/nix/profiles/default/bin/dockerd
        ExecReload=/bin/kill -s HUP $MAINPID
        # Having non-zero Limit*s causes performance problems due to accounting overhead
        # in the kernel. We recommend using cgroups to do container-local accounting.
        LimitNOFILE=infinity
        LimitNPROC=infinity
        LimitCORE=infinity
        # Uncomment TasksMax if your systemd version supports it.
        # Only systemd 226 and above support this version.
        #TasksMax=infinity
        TimeoutStartSec=0
        # set delegate yes so that systemd does not reset the cgroups of docker containers
        Delegate=yes
        # kill only the docker process, not all processes in the cgroup
        KillMode=process

        [Install]
        WantedBy=multi-user.target
runcmd:
- export USER=$(awk -v uid=1000 -F":" '{ if($3==uid){print $1} }' /etc/passwd)

- sudo -H -u $USER bash -c '/tmp/install-nix.sh'

# root configuration
- /nix/var/nix/profiles/default/bin/nix-env -if /tmp/root-setup.nix
- systemctl enable docker
- systemctl start docker

# VM user configuration
- sudo -H -u $USER bash -c '/nix/var/nix/profiles/default/bin/nix-env -if /tmp/user-setup.nix'
- sudo -H -u $USER bash -c '~/.nix-profile/bin/rustup default stable'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;With this configuration I have a slim distribution combined with a powerful package management environment available to add and remove packages in a clean way - exactly what I need for experimental and development workloads.&lt;/p&gt;

&lt;p&gt;I assume that &lt;strong&gt;Nix&lt;/strong&gt; offers far more capabilities than just installing packages - which I will continue to explore to have an alternative for the relatively clunky and sensitive &lt;code&gt;cloud-init&lt;/code&gt; installation approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  P.S.
&lt;/h2&gt;

&lt;p&gt;If you want start posting articles "at scale" and want to see how I post on &lt;a href="https://dev.to"&gt;https://dev.to&lt;/a&gt;, &lt;a href="https://ops.io" rel="noopener noreferrer"&gt;https://ops.io&lt;/a&gt; and &lt;a href="https://hashnode.com" rel="noopener noreferrer"&gt;https://hashnode.com&lt;/a&gt; in one go, check out my &lt;a href="https://github.com/KaiWalter/automate-posts/blob/main/createPosts.ps1" rel="noopener noreferrer"&gt;repo + script&lt;/a&gt;. It is not really elegant, has a bulky usability, it is PowerShell, but it does the trick - hence it is "me".&lt;/p&gt;

</description>
      <category>azure</category>
      <category>productivity</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Share NeoVim configuration between Linux and Windows</title>
      <dc:creator>Kai Walter</dc:creator>
      <pubDate>Mon, 29 May 2023 07:37:44 +0000</pubDate>
      <link>https://dev.to/kaiwalter/share-neovim-configuration-between-linux-and-windows-4gh8</link>
      <guid>https://dev.to/kaiwalter/share-neovim-configuration-between-linux-and-windows-4gh8</guid>
      <description>&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;As posted on &lt;a href="https://techhub.social/@ancientITguy/110440880551590800" rel="noopener noreferrer"&gt;techhub.social&lt;/a&gt; I am currently to stretch myself again and move out of the VS Code comfortzone into NeoVim ecosystem. That also entails that I want to use NeoVim on both : Linux and Windows.&lt;/p&gt;

&lt;p&gt;On Linux I already create a basic NeoVim configuration which I now want to share with Windows - also to see where I hit limits of that idea. On Linux I use &lt;a href="https://wiki.archlinux.org/title/Dotfiles" rel="noopener noreferrer"&gt;&lt;strong&gt;Dotfiles&lt;/strong&gt;&lt;/a&gt; to contain and share configurations for Bash, Zsh, Sway, Tmux, NeoVim, ... So how can I leverage Neovim's &lt;strong&gt;Dotfiles&lt;/strong&gt; and link it to the appropriate folder on Windows?&lt;/p&gt;

&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;On Windows NeoVim gets its configuration from &lt;code&gt;%userprofile%\AppData\Local\nvim&lt;/code&gt; and keeps its data in &lt;code&gt;%userprofile%\AppData\Local\nvim-data&lt;/code&gt;. Hence the &lt;code&gt;.config/nvim&lt;/code&gt; folder from my Dotfiles needs to be linked to the said configuration folder and a plugin like &lt;a href="https://github.com/wbthomason/packer.nvim#quickstart" rel="noopener noreferrer"&gt;&lt;strong&gt;Packer.nvim&lt;/strong&gt;&lt;/a&gt; needs to be cloned in a sub-folder in the data folder.&lt;/p&gt;

&lt;h2&gt;
  
  
  Script
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="c"&gt;# install NeoVim with WinGet, if not already present on system&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-Command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;nvim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ErrorAction&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;SilentlyContinue&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;winget&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Neovim.Neovim&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# clone my Dotfiles repo&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$dotFilesRoot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$HOME&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"dotfiles"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Test-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dotFilesRoot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-PathType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;clone&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;git&lt;/span&gt;&lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="nx"&gt;github.com:KaiWalter/dotfiles.git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dotFilesRoot&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# link NeoVim configuration&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$localConfiguration&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;LOCALAPPDATA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nvim"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$dotfilesConfiguration&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dotFilesRoot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;".config"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nvim"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Test-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$localConfiguration&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-PathType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; 
    &lt;/span&gt;&lt;span class="n"&gt;Start-Process&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-FilePath&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pwsh"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ArgumentList&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"-c New-Item -Path &lt;/span&gt;&lt;span class="nv"&gt;$localConfiguration&lt;/span&gt;&lt;span class="s2"&gt; -ItemType SymbolicLink -Value &lt;/span&gt;&lt;span class="nv"&gt;$dotfilesConfiguration&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;" "&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Verb&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;runas&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="c"&gt;# clone Packer.nvim, if not already present on system&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="nv"&gt;$localPacker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Join-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$&lt;/span&gt;&lt;span class="nn"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="nv"&gt;LOCALAPPDATA&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nvim-data"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"site"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pack"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"packer"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"start"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"packer.nvim"&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="kr"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Test-Path&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$localPacker&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-PathType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; 
    &lt;/span&gt;&lt;span class="n"&gt;git&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;clone&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;https://github.com/wbthomason/packer.nvim&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$localPacker&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;ATTENTION: &lt;code&gt;git@github.com:KaiWalter/dotfiles.git&lt;/code&gt; is my private Dotfiles repo - if you want to replicate my approach you would need to run from your own version&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After running the script and starting NeoVim a &lt;code&gt;:PackerSync&lt;/code&gt; is required to install all the plugins.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Dotfiles on Linux
&lt;/h2&gt;

&lt;p&gt;There are plenty of posts with various flavors on how to go about setting up Dotfiles. I could not get myself to suggest a particular one, so... when I setup a new Linux system, I use these commands to clone it locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; ~/.dotfiles.git &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then &lt;/span&gt;git clone &lt;span class="nt"&gt;--bare&lt;/span&gt; git@github.com:KaiWalter/dotfiles.git ~/.dotfiles.git&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;".dotfiles.git"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.gitignore
git &lt;span class="nt"&gt;--git-dir&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;/.dotfiles.git/ &lt;span class="nt"&gt;--work-tree&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;/ checkout
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;which brings in an &lt;code&gt;alias&lt;/code&gt; &lt;code&gt;dotfiles&lt;/code&gt; in &lt;code&gt;.zshrc&lt;/code&gt; or &lt;code&gt;.bashrc&lt;/code&gt; to be used when interacting with the particular repository later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; ~/.dotfiles.git &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;dotfiles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/usr/bin/git --git-dir=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.dotfiles.git/ --work-tree=&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
      <category>powershell</category>
      <category>neovim</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Private linking an Azure Container App Environment (May'23 update)</title>
      <dc:creator>Kai Walter</dc:creator>
      <pubDate>Sun, 21 May 2023 10:03:02 +0000</pubDate>
      <link>https://dev.to/kaiwalter/private-linking-an-azure-container-app-environment-may23-update-47f8</link>
      <guid>https://dev.to/kaiwalter/private-linking-an-azure-container-app-environment-may23-update-47f8</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This post is an updated version of &lt;a href="https://dev.to/kaiwalter/preliminary-private-linking-an-azure-container-app-environment-3cnf"&gt;my Feb'22 post "Private linking an Azure Container App Environment"&lt;/a&gt;, uses Bicep instead of Azure CLI to deploy Private Linking configuration but is reduced to the pure configuration without jump VM and sample applications.&lt;br&gt;
&lt;br&gt;&lt;strong&gt;+plus&lt;/strong&gt; it applies &lt;strong&gt;Bicep CIDR functions&lt;/strong&gt; to calculate sample network address prefixes&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;When originally posting in spring 2022 our challenge was, that we would not be granted multiple large enough (/21 CIDR range, 2048 IP addresses) address spaces within our corporate cloud address spaces - as being one of many, many workloads in the cloud - which could hold the various Container Apps environments - while still being connected to corporate resources. Now that this limitation is more relaxed &lt;a href="https://learn.microsoft.com/en-us/azure/container-apps/networking#subnet" rel="noopener noreferrer"&gt;- /23, 512 IP addresses for consumption only and /27, 32 IP addresses for workload profile environments - &lt;/a&gt; we could rework our configuration. However over time we learned to appreciate some of the other advantages this separation delivered:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;clear isolation of container workloads with a high degree of control what types of traffic go in (this post) and out &lt;a href="https://dev.to/kaiwalter/use-azure-application-gateway-private-link-configuration-for-an-internal-api-management-1d6o"&gt;(see also posts on Private Linking back from Container Apps to API Management&lt;/a&gt; or &lt;a href="https://dev.to/kaiwalter/private-linking-and-port-forwarding-to-non-azure-resources-5h0f"&gt;port forwarding)&lt;/a&gt; the compute environment&lt;/li&gt;
&lt;li&gt;having enough breathing space in terms of IP addresses so to almost never hit any limitations (e.g. in burst scaling scenarios)&lt;/li&gt;
&lt;li&gt;being able to stand up additional environments at any time as the number of IP addresses required in corporate address space is minimal&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Solution Elements&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;To simplify terms for this post I assume the corporate network connected virtual network with limited address space would be the &lt;strong&gt;hub network&lt;/strong&gt; and the virtual network containing the &lt;strong&gt;Container Apps Environment&lt;/strong&gt; (with the /21 address space) would be the &lt;strong&gt;spoke network&lt;/strong&gt;.****&lt;/p&gt;

&lt;p&gt;This is the suggested configuration:&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%2F6sdb1clehs2n1lui0byc.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%2F6sdb1clehs2n1lui0byc.png" alt="hub/spoke Container Apps configuration with private link" width="800" height="272"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;private link service&lt;/strong&gt; within &lt;strong&gt;spoke network&lt;/strong&gt; linked to the &lt;code&gt;kubernetes-internal&lt;/code&gt; Load balancer&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;private endpoint&lt;/strong&gt; in the &lt;strong&gt;hub network&lt;/strong&gt; linked to  &lt;strong&gt;private link service&lt;/strong&gt; above&lt;/li&gt;
&lt;li&gt;a &lt;strong&gt;private DNS zone&lt;/strong&gt; with the Container Apps domain name and a &lt;code&gt;*&lt;/code&gt; &lt;code&gt;A&lt;/code&gt; record pointing to the &lt;strong&gt;private endpoint&lt;/strong&gt;'s IP address&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;DISCLAIMER: the approach in this article is based on the &lt;strong&gt;assumption&lt;/strong&gt;, that the underlying AKS node resource group is visible, exposed and the name matches the environments domain name (in my sample configuration domain was &lt;code&gt;redrock-70deffe0.westeurope.azurecontainerapps.io&lt;/code&gt; which resulted in node pool resource group &lt;code&gt;MC_redrock-70deffe0-rg_redrock-70deffe0_westeurope&lt;/code&gt;) which in turn allows one to find the &lt;code&gt;kubernetes-internal&lt;/code&gt; &lt;strong&gt;ILB&lt;/strong&gt; to create the private endpoint; checking with the Container Apps team at Microsoft, this assumption still shall be &lt;strong&gt;valid after GA&lt;/strong&gt;/General Availability&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Below I will refer to shell scripts and &lt;strong&gt;Bicep&lt;/strong&gt; templates I keep in this repository path: &lt;a href="https://github.com/KaiWalter/container-apps-experimental/tree/main/ca-private-bicep" rel="noopener noreferrer"&gt;https://github.com/KaiWalter/container-apps-experimental/tree/main/ca-private-bicep&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In previous year's post I had to use a mix of CLI and Bicep as not yet all Container App properties like &lt;code&gt;staticIp&lt;/code&gt; and &lt;code&gt;defaultDomain&lt;/code&gt; could be processed as Bicep outputs. Now the whole deployment can be achieved purely in multi-staged Bicep modules.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Bicep CLI version &amp;gt;=0.17.1 (for CIDR calculation functions)&lt;/li&gt;
&lt;li&gt;Azure CLI version &amp;gt;=2.48.1, containerapp extension &amp;gt;= 0.3.29 (not required for deployment but useful for configuration checks)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Main Deployment
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;main.bicep&lt;/code&gt; deploys the target resource group in the given subscription and then invokes &lt;code&gt;resources.bicep&lt;/code&gt; to deploy the actual resources within the resource group.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;resources.bicep&lt;/code&gt; uses &lt;code&gt;network.bicep&lt;/code&gt; and &lt;code&gt;logging.bicep&lt;/code&gt; to deploy the &lt;strong&gt;hub and spoke network&lt;/strong&gt; as well as basic &lt;strong&gt;Log Analytics workspace with Application Insights&lt;/strong&gt;, then continues with the 3-staged deployment of the Container Apps Environment including the Private Linking and DNS resources&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Separating deployment stages into Bicep modules allows Azure Resource Manager/Bicep to feed information into deployment steps of resources &lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 1 - Container Apps Environment
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;stage1.bicep&lt;/code&gt; with &lt;code&gt;environment.bicep&lt;/code&gt; deploys the Container Apps Environment and outputs the generated &lt;strong&gt;DefaultDomain&lt;/strong&gt; for further reference in Private Linking resources.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 2 - Private Link Service and Private Endpoint
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;stage2.bicep&lt;/code&gt; references the generated Load Balancer by using previous &lt;strong&gt;DefaultDomain&lt;/strong&gt; information and uses &lt;code&gt;privatelink.bicep&lt;/code&gt; to create the Private Link Service resource on the Load Balancer + the Private Endpoint linking to the Private Link Resource. It outputs the Private Endpoint's &lt;strong&gt;Network Interface Card/NIC name&lt;/strong&gt; to be referenced in the Private DNS configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  Stage 3 - Private DNS Zone
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;stage3.bicep&lt;/code&gt; references Private Endpoint's NIC by using its &lt;strong&gt;NIC name&lt;/strong&gt; to extract Private IP address information required for private DNS configuration in &lt;code&gt;privatedns.bicep&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Finally in &lt;code&gt;privatedns.bicep&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a &lt;strong&gt;Private DNS Zone&lt;/strong&gt; with the domain name of the &lt;strong&gt;Container App Environment&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;an &lt;code&gt;A&lt;/code&gt; record pointing to the &lt;strong&gt;Private Endpoint&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Virtual Network Link&lt;/strong&gt; from the &lt;strong&gt;Private DNS Zone&lt;/strong&gt; to the &lt;strong&gt;hub network&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;is created. &lt;/p&gt;




&lt;h2&gt;
  
  
  Credits
&lt;/h2&gt;

&lt;p&gt;Thank you &lt;a href="https://twitter.com/Lebrosk" rel="noopener noreferrer"&gt;@Lebrosk&lt;/a&gt; for reaching out and asking for the all-Bicep solution which we created in the past months but I did not care to share here.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>networking</category>
      <category>privatelink</category>
    </item>
    <item>
      <title>Private linking and port forwarding to non-Azure resources</title>
      <dc:creator>Kai Walter</dc:creator>
      <pubDate>Wed, 05 Apr 2023 16:47:49 +0000</pubDate>
      <link>https://dev.to/kaiwalter/private-linking-and-port-forwarding-to-non-azure-resources-5h0f</link>
      <guid>https://dev.to/kaiwalter/private-linking-and-port-forwarding-to-non-azure-resources-5h0f</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;What can be seen in this post:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use a Load Balancer combined with a small sized VM scaleset (VMSS) configured with &lt;strong&gt;iptables&lt;/strong&gt; to forward and masquerade incoming connections to 2 IP addresses which represent 2 on-premise servers; this installation is placed in a hub network that can be shared amount several spoke networks&lt;/li&gt;
&lt;li&gt;link this Load Balancer to another virtual network - without virtual network peering - by utilizing Private Link Service and a Private Endpoint which is placed in a spoke network&lt;/li&gt;
&lt;li&gt;use Azure Container Instances to connect into hub or spoke networks and test connections&lt;/li&gt;
&lt;li&gt;how to feed &lt;code&gt;cloud-init.txt&lt;/code&gt; for VM &lt;strong&gt;customData&lt;/strong&gt; into &lt;strong&gt;azd&lt;/strong&gt; parameters&lt;/li&gt;
&lt;li&gt;how to stop Azure Container Instances immediately after &lt;strong&gt;azd&lt;/strong&gt; / &lt;strong&gt;Bicep&lt;/strong&gt; deployment with the post-provisioning hook&lt;/li&gt;
&lt;li&gt;how to persistent &lt;strong&gt;iptables&lt;/strong&gt; between reboots on the VMSS instance without an additional package&lt;/li&gt;
&lt;li&gt;how to use a &lt;strong&gt;NAT gateway&lt;/strong&gt; to allow outbound traffic for an &lt;strong&gt;ILB/Internal Load Balancer&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;For a scenario within a corporate managed virtual network, I &lt;a href="https://dev.to/kaiwalter/preliminary-private-linking-an-azure-container-app-environment-3cnf"&gt;private linked the Azure Container Apps environment with its own virtual network and non-restricted IP address space&lt;/a&gt; (here Spoke virtual network) to the corporate Hub virtual network.&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%2Fruketol7klgmhth52u2e.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%2Fruketol7klgmhth52u2e.png" alt="Azure Container Apps environment private linked into a corporate managed virtual network with limited IP address space" width="800" height="274"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;back then, there was no official icon for Container Apps Environments available, hence I used an AKS icon&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One challenge that had to be solved back then is how to let the workloads running in Azure Container Apps environment call back into an API Management instance in the Hub virtual network. To achieve that I &lt;a href="https://dev.to/kaiwalter/use-azure-application-gateway-private-link-configuration-for-an-internal-api-management-1d6o"&gt;private linked the Application Gateway, that forwards to the API Management instance, into the Spoke virtual network&lt;/a&gt;:&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%2F4b5i7ln4k7on0nou1cwu.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%2F4b5i7ln4k7on0nou1cwu.png" alt="API Management private linked back to Spoke virtual network over Application Gateway and a Private Endpoint" width="757" height="451"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A New Challenge
&lt;/h2&gt;

&lt;p&gt;Just recently a new challenge came up: We needed to forward TCP traffic on a specific port to 2 specific - usually load balanced - servers in a downstream / connected on-premise network.&lt;/p&gt;

&lt;p&gt;The first reflex was to try to put both IP addresses into a backend pool of a Load Balancer in the Hub virtual network. Then trying to establish a Private Endpoint in the Spoke virtual network to allow traffic from Azure Container Apps environment over private linking into the Load Balancer and then to the downstream servers. However some &lt;a href="https://learn.microsoft.com/en-us/azure/load-balancer/backend-pool-management#limitations" rel="noopener noreferrer"&gt;limitations&lt;/a&gt; got in the way of this endeavor:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Limitations&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;IP based backends can only be used for Standard Load Balancers&lt;/li&gt;
&lt;li&gt;The backend resources must be in the same virtual network as the load balancer for IP based LBs&lt;/li&gt;
&lt;li&gt;A load balancer with IP based Backend Pool can't function as a Private Link service&lt;/li&gt;
&lt;li&gt;...&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Going Down the Rabbit Hole
&lt;/h2&gt;

&lt;p&gt;As I usually &lt;em&gt;"Don't Accept the Defaults" (Abel Wang)&lt;/em&gt; or just am plain and simple stubborn, I tried it anyway - which in its own neat way also provided some more learnings, I otherwise would have missed.&lt;/p&gt;

&lt;p&gt;To let you follow along I created a &lt;a href="https://github.com/KaiWalter/azure-private-link-port-forward" rel="noopener noreferrer"&gt;sample repo&lt;/a&gt; which allows me to spin up an exemplary environment using &lt;a href="https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/overview" rel="noopener noreferrer"&gt;Azure Developer CLI&lt;/a&gt; and &lt;a href="https://learn.microsoft.com/en-us/azure/azure-resource-manager/bicep/overview?tabs=bicep" rel="noopener noreferrer"&gt;Bicep&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I like using &lt;strong&gt;&lt;code&gt;azd&lt;/code&gt;&lt;/strong&gt; together with &lt;strong&gt;Bicep&lt;/strong&gt; for simple Proof-of-Concept like scenarios as I can easily &lt;code&gt;azd up&lt;/code&gt; and &lt;code&gt;azd down&lt;/code&gt; the environment without having to deal with state  - as with other IaC stacks.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Learning 1:&lt;/strong&gt; I was not able to bring up the Load Balancer directly linked with the server IP addresses with &lt;a href="https://github.com/KaiWalter/azure-private-link-port-forward/blob/main/infra/modules/loadbalancer/loadbalancer.bicep" rel="noopener noreferrer"&gt;Bicep&lt;/a&gt; in one go. &lt;a href="https://stackoverflow.com/questions/75910542/backendaddresspool-in-azure-load-balancer-with-only-ip-addresses-does-not-deploy" rel="noopener noreferrer"&gt;Deployment succeeded without error but backend pool just was not configured&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learning 2:&lt;/strong&gt; Deploying with CLI, configured the Load Balancer backend pool correctly ... but forwarding did not work, because ...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;azd &lt;span class="nb"&gt;env &lt;/span&gt;get-values&lt;span class="o"&gt;)&lt;/span&gt;

az network lb delete &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; ilb-&lt;span class="nv"&gt;$RESOURCE_TOKEN&lt;/span&gt;

az network lb create &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; ilb-&lt;span class="nv"&gt;$RESOURCE_TOKEN&lt;/span&gt; &lt;span class="nt"&gt;--sku&lt;/span&gt; Standard &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--backend-pool-name&lt;/span&gt; direct &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--subnet&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;az network vnet subnet show &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; shared &lt;span class="nt"&gt;--vnet-name&lt;/span&gt; vnet-hub-&lt;span class="nv"&gt;$RESOURCE_TOKEN&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;

az network lb probe create &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;--lb-name&lt;/span&gt; ilb-&lt;span class="nv"&gt;$RESOURCE_TOKEN&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; direct &lt;span class="nt"&gt;--protocol&lt;/span&gt; tcp &lt;span class="nt"&gt;--port&lt;/span&gt; 8000

az network lb address-pool create &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;--lb-name&lt;/span&gt; ilb-&lt;span class="nv"&gt;$RESOURCE_TOKEN&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; direct &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--backend-address&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;server65 ip-address&lt;span class="o"&gt;=&lt;/span&gt;192.168.42.65 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--backend-address&lt;/span&gt; &lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;server66 ip-address&lt;span class="o"&gt;=&lt;/span&gt;192.168.42.66 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--vnet&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;az network vnet show &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt;  &lt;span class="nt"&gt;-n&lt;/span&gt; vnet-hub-&lt;span class="nv"&gt;$RESOURCE_TOKEN&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; tsv&lt;span class="si"&gt;)&lt;/span&gt;

az network lb rule create &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;--lb-name&lt;/span&gt; ilb-&lt;span class="nv"&gt;$RESOURCE_TOKEN&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; direct &lt;span class="nt"&gt;--protocol&lt;/span&gt; tcp &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--frontend-ip&lt;/span&gt; LoadBalancerFrontEnd &lt;span class="nt"&gt;--backend-pool-name&lt;/span&gt; direct &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--frontend-port&lt;/span&gt; 8000 &lt;span class="nt"&gt;--backend-port&lt;/span&gt; 8000 &lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="nt"&gt;--probe&lt;/span&gt; direct

az network lb show &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; ilb-&lt;span class="nv"&gt;$RESOURCE_TOKEN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;source &amp;lt;(azd env get-values)&lt;/code&gt; sources all &lt;code&gt;main.bicep&lt;/code&gt; output values generated by &lt;code&gt;azd up&lt;/code&gt; or &lt;code&gt;azd infra create&lt;/code&gt; as variables into the running script&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Learning 3:&lt;/strong&gt; ... specifying IP addresses together with a virtual network in the backend pool is intended for the Load Balancer to hook up the NICs/Network Interface Cards of Azure resources later automatically when these NICs get available. It is not intended for some generic IP addresses.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Learning 4:&lt;/strong&gt; Anyway Azure Portal did not allow to create a Private Link Service on a Load Balancer with IP address configured backend pool. So it would not have worked for my desired scenario anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other Options
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Virtual Network Peering&lt;/strong&gt; Hub and Spoke is not an option as we

&lt;ul&gt;
&lt;li&gt;do not want to mix up corporate IP address ranges with the arbitrary IP addresses ranges of the various Container Apps virtual networks&lt;/li&gt;
&lt;li&gt;want to avoid BGP/Border Gateway Protocol mishaps at any cost&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;with a recently &lt;a href="https://learn.microsoft.com/en-us/azure/container-apps/networking#subnet" rel="noopener noreferrer"&gt;&lt;strong&gt;reduced required subnet size&lt;/strong&gt; for Workload profiles&lt;/a&gt; moving the whole &lt;strong&gt;Azure Container Apps environment&lt;/strong&gt; or just the particular single Container App in question back to corporate IP address space would have been possible, but I did not want to give up this extra level of isolation this separation based on Private Link in and out gave us; additionally it would have required a new / separate subnet to keep it within network boundaries&lt;/li&gt;

&lt;li&gt;deploy this one containerized workload into the corporate VNET with &lt;strong&gt;Azure App Service or Azure Functions&lt;/strong&gt;, but that would have messed up the homogeneity of our environment; additionally it would have required a new / separate subnet allowing delegation for these resources&lt;/li&gt;

&lt;/ul&gt;




&lt;h2&gt;
  
  
  Bring In some IaaS and &lt;strong&gt;iptables&lt;/strong&gt; Magic
&lt;/h2&gt;

&lt;p&gt;Being a passionate &lt;strong&gt;PaaS-first&lt;/strong&gt; guy, I usually do not want (or let other people need) to deal with infrastructure / &lt;strong&gt;IaaS&lt;/strong&gt;. So for this rare occasion and isolated use case I decided to go for it and keep and eye out for a future Azure resource or feature that might cover this scenario - as with our DNS forwarded scaleset which we now can replace with &lt;a href="https://learn.microsoft.com/en-us/azure/dns/dns-private-resolver-overview" rel="noopener noreferrer"&gt;Azure DNS Private Resolver&lt;/a&gt;. For our team such a decision in the end is a matter of managing technical debt.&lt;/p&gt;

&lt;p&gt;Making such a decision easier for me is, that this is a stateless workload. VMSS nodes as implemented here can recreated at anytime without the risk of data loss.&lt;/p&gt;

&lt;h3&gt;
  
  
  Solution Overview
&lt;/h3&gt;

&lt;p&gt;All solution elements can be found in this &lt;a href="https://github.com/KaiWalter/azure-private-link-port-forward" rel="noopener noreferrer"&gt;repo&lt;/a&gt;:&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%2Fr1zzc7eytpm52a6pbl8d.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%2Fr1zzc7eytpm52a6pbl8d.png" alt="Network diagram showing connection from Private Endpoint over Private Link Service, Load Balancer to on premise Servers" width="631" height="502"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;infra/modules/forwarder/*&lt;/code&gt; : a VMSS / &lt;strong&gt;VM Scaleset&lt;/strong&gt; based on a Ubuntu 22.04 image, configure and persist &lt;strong&gt;netfilter&lt;/strong&gt; / &lt;strong&gt;iptables&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;infra/modules/forwarder/forwarder.bicep&lt;/code&gt; : a &lt;strong&gt;Load Balancer&lt;/strong&gt; on top of the VMSS&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;infra/modules/forwarder/forwarder-spoke-privatelink.bicep&lt;/code&gt; : a &lt;strong&gt;Private Link Service&lt;/strong&gt; linked to the Load Balancer&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;infra/modules/forwarder/forwarder-spoke-privatelink.bicep&lt;/code&gt; : a &lt;strong&gt;Private Endpoint&lt;/strong&gt; in the Spoke network connecting to the Private Link Server paired with a Private DNS zone to allow for name resolution&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;infra/modules/network.bicep&lt;/code&gt; : a &lt;strong&gt;NAT gateway&lt;/strong&gt; on the Hub virtual network for the &lt;strong&gt;Internal Load Balancer&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;infra/modules/containergroup.bicep&lt;/code&gt; : Azure Container Instances (&lt;strong&gt;ACI&lt;/strong&gt;) in Hub and Spoke virtual networks to hop onto these network for basic testing&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;general note: the sample repo is forwarding to web servers on port 8000 - for that I could have used (a layer 7) Application Gateway; however in our real world scenario we forward to another TCP/non-HTTP port, so the solution you see here should work for any TCP port (on layer 4)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  VM Scaleset
&lt;/h3&gt;

&lt;p&gt;I chose an image with a small footprint and which is supported for &lt;code&gt;enableAutomaticOSUpgrade: true&lt;/code&gt; to reduce some of the maintenance effort:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  imageReference: {
    publisher: 'MicrosoftCBLMariner'
    offer: 'cbl-mariner'
    sku: 'cbl-mariner-2-gen2'
    version: 'latest'
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I chose a small but feasible VM SKU:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  sku: {
    name: 'Standard_B1s'
    tier: 'Standard'
    capacity: capacity
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  HealthExtension
&lt;/h4&gt;

&lt;p&gt;I wanted the scaleset to know about "application's" health, hence whether the forwarded port is available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  extensionProfile: {
    extensions: [
      {
        name: 'HealthExtension'
        properties: {
          autoUpgradeMinorVersion: false
          publisher: 'Microsoft.ManagedServices'
          type: 'ApplicationHealthLinux'
          typeHandlerVersion: '1.0'
          settings: {
            protocol: 'tcp'
            port: port
          }
        }
      }
    ]
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I had to learn, that this check is done by the extension from within the VM, hence I had to open up an additional &lt;strong&gt;OUTPUT&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;iptables -t nat -A OUTPUT -p tcp -d 127.0.0.1 --dport $PORT -j DNAT --to-destination $ONPREMSERVER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  cloud-init.txt
&lt;/h4&gt;

&lt;p&gt;IP forwarding needs to be enabled, also on the outbound:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
write_files:
- path: /etc/sysctl.conf
  content: | 
    # added by cloud init
    net.ipv4.ip_forward=1
    net.ipv4.conf.all.route_localnet=1
  append: true
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I added a basic distribution of load to the 2 on-premise servers based on the last digit of the VMSS node's hostname:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if [[ $HOSTNAME =~ [02468]$ ]]; then export ONPREMSERVER=192.168.42.65; else export ONPREMSERVER=192.168.42.66; fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  cloud-init.txt / &lt;strong&gt;iptables&lt;/strong&gt; configuration
&lt;/h4&gt;

&lt;p&gt;To dig into some of the basics of &lt;strong&gt;iptables&lt;/strong&gt; I worked through these 2 posts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.digitalocean.com/community/tutorials/iptables-essentials-common-firewall-rules-and-commands" rel="noopener noreferrer"&gt;https://www.digitalocean.com/community/tutorials/iptables-essentials-common-firewall-rules-and-commands&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://jensd.be/343/linux/forward-a-tcp-port-to-another-ip-or-port-using-nat-with-iptables" rel="noopener noreferrer"&gt;https://jensd.be/343/linux/forward-a-tcp-port-to-another-ip-or-port-using-nat-with-iptables&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and got the idea of a simplistic persistence of &lt;strong&gt;iptables&lt;/strong&gt; accross reboots&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/oryaacov/3-ways-to-make-iptables-persistent-4pp"&gt;https://dev.to/oryaacov/3-ways-to-make-iptables-persistent-4pp&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# clear filter table
iptables --flush
# general policy : deny all incoming traffic
iptables ${IPTABLES_WAIT} -P INPUT DROP
# general policy : allow all outgoing traffic
iptables ${IPTABLES_WAIT} -P OUTPUT ACCEPT
# general policy : allow FORWARD traffic
iptables ${IPTABLES_WAIT} -A FORWARD -j ACCEPT
# allow input on loopback - is required e.g. for upstream Azure DNS resolution
iptables ${IPTABLES_WAIT} -I INPUT -i lo -j ACCEPT
# further allow established connection
iptables ${IPTABLES_WAIT} -A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
# drop invalid connections
iptables ${IPTABLES_WAIT} -A INPUT -m conntrack --ctstate INVALID -j DROP
# allow incoming SSH for testing - could be removed
iptables ${IPTABLES_WAIT} -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
# clear nat table
iptables --flush -t nat
# allow outgoing connection to target servers from inside VMSS node - is required for ApplicationHealthLinux
iptables ${IPTABLES_WAIT} -t nat -A OUTPUT -p tcp -d 127.0.0.1 --dport $PORT -j DNAT --to-destination $ONPREMSERVER
# allow incoming traffic from Load Balancer! - important!
iptables ${IPTABLES_WAIT} -t nat -A PREROUTING -s 168.63.129.16/32 -p tcp -m tcp --dport $PORT -j DNAT --to-destination $ONPREMSERVER:$PORT
# allow incoming traffic from Hub virtual network - mostly for testing/debugging, could be removed
iptables ${IPTABLES_WAIT} -t nat -A PREROUTING -s $INBOUNDNET -p tcp -m tcp --dport $PORT -j DNAT --to-destination $ONPREMSERVER:$PORT
# masquerade outgoing traffic so that target servers assume traffic originates from "allowed" server in Hub network
iptables ${IPTABLES_WAIT} -t nat -A POSTROUTING -d $ONPREMSERVER -j MASQUERADE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;I learned that I also could have used &lt;strong&gt;nftables&lt;/strong&gt; or &lt;strong&gt;ufw&lt;/strong&gt;, I just found the most suitable samples with &lt;strong&gt;iptables&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Load Balancer
&lt;/h3&gt;

&lt;p&gt;Nothing special here.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In our pretty locked-down environment I had to pair the internal Load Balancer with a Public IP address fronted Load Balancer to arrange outbound traffic for the VMSS instances. That configuration still needs to be replaced with a NAT gateway which in turn needs reconfiguration of our corporate virtual network setup. If there is something relevant to share, I will update here or create a separate post.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Private Link Service
&lt;/h3&gt;

&lt;p&gt;I got confused when I started using private linking a few months back: This service needs to be created in the virtual network of the service to be linked as a kind of pick-up point. So in my sample in the Hub virtual network.&lt;/p&gt;

&lt;h3&gt;
  
  
  Private Endpoint &amp;amp; Private DNS Zone
&lt;/h3&gt;

&lt;p&gt;Also pretty straight forward: I chose a zone &lt;code&gt;internal.net&lt;/code&gt; which is unique in the environment and potentially not appearing somewhere else.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;forwarder-private-nic-to-ip.bicep&lt;/code&gt; : Within the same Bicep file, after the deployment of the private endpoint, its private IP address is not available. This script allows to resolve the private endpoints's private address with an extra deployment step from the NIC:&lt;br&gt;
&lt;/p&gt;

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

resource nic 'Microsoft.Network/networkInterfaces@2021-05-01' existing = {
  name: substring(pepNicId, lastIndexOf(pepNicId, '/') + 1)
}

output nicPrivateIp string = nic.properties.ipConfigurations[0].properties.privateIPAddress
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is injected after deploying the private endpoint to receive the private IP address for the DNS zone, as there is no linked address update possible as with regular Azure resource private endpoints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module nic2pip 'forwarder-private-nic-to-ip.bicep' = {
  name: 'nic2pip'
  params: {
    pepNicId: pep.properties.networkInterfaces[0].id
  }
}

resource privateDnsZoneEntry 'Microsoft.Network/privateDnsZones/A@2020-06-01' = {
  name: 'onprem-server'
  parent: dns
  properties: {
    aRecords: [
      {
        ipv4Address: nic2pip.outputs.nicPrivateIp
      }
    ]
    ttl: 3600
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  NAT gateway
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://learn.microsoft.com/en-us/azure/virtual-network/nat-gateway/nat-overview" rel="noopener noreferrer"&gt;NAT gateway&lt;/a&gt; can be used to scale out outbound IP traffic with a range of public IP addresses to have a defined to avoid SNAT port exhaustion. In this scenario it is required to allow outbound IP traffic for the ILB/Internal Load Balancer - so for package manager updates during VMSS instance provisioning and for applying updates while running. It is defined in &lt;code&gt;infra/modules/network.bicep&lt;/code&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 publicip 'Microsoft.Network/publicIPAddresses@2021-05-01' = {
  name: 'nat-pip-${resourceToken}'
  location: location
  tags: tags
  sku: {
    name: 'Standard'
  }
  properties: {
    publicIPAddressVersion: 'IPv4'
    publicIPAllocationMethod: 'Static'
    idleTimeoutInMinutes: 4
  }
}

resource natgw 'Microsoft.Network/natGateways@2022-09-01' = {
  name: 'nat-gw-${resourceToken}'
  location: location
  tags: tags
  sku: {
    name: 'Standard'
  }
  properties: {
    idleTimeoutInMinutes: 4
    publicIpAddresses: [
      {
        id: publicip.id
      }
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... and then linked to Hub virtual network, shared subnet where the Internal Load Balancer is placed into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// hub virtual network where the shared resources are deployed
resource vnetHub 'Microsoft.Network/virtualNetworks@2022-09-01' = {
  name: 'vnet-hub-${resourceToken}'
  location: location
  tags: tags
  properties: {
    addressSpace: {
      addressPrefixes: [
        '10.0.1.0/24'
      ]
    }
    subnets: [
      {
        name: 'shared'
        properties: {
          addressPrefix: '10.0.1.0/26'
          privateEndpointNetworkPolicies: 'Disabled'
          privateLinkServiceNetworkPolicies: 'Disabled'
          natGateway: {
            id: natgw.id
          }
        }
      }
...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Azure Container Instances
&lt;/h3&gt;

&lt;p&gt;I use a publicly available image &lt;code&gt;hacklab/docker-nettools&lt;/code&gt; which contains some essential tools like &lt;strong&gt;curl&lt;/strong&gt; for testing and debugging.&lt;/p&gt;

&lt;p&gt;In the containerGroup resource I overwrite the startup command to send this shell-like container into a loop so that it can be re-used for a shell like &lt;code&gt;az container exec -n $HUB_JUMP_NAME -g $RESOURCE_GROUP_NAME --exec-command "/bin/bash"&lt;/code&gt; later.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;****  command: [
    'tail'
    '-f'
    '/dev/null'
  ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Other Gadgets
&lt;/h3&gt;

&lt;p&gt;I use &lt;strong&gt;azd env set&lt;/strong&gt; to funnel e.g. &lt;code&gt;cloud-init.txt&lt;/code&gt; or SSH public key &lt;code&gt;id_rsa&lt;/code&gt; content into deployment variables - see &lt;code&gt;scripts/set-environment.sh&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

azd &lt;span class="nb"&gt;env set &lt;/span&gt;SSH_PUBLIC_KEY &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ~/.ssh/id_rsa.pub&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
azd &lt;span class="nb"&gt;env set &lt;/span&gt;CLOUD_INIT_ONPREM &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ./infra/modules/onprem-server/cloud-init.txt | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; 0&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
azd &lt;span class="nb"&gt;env set &lt;/span&gt;CLOUD_INIT_FWD &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; ./infra/modules/forwarder/cloud-init.txt | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-w&lt;/span&gt; 0&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;a. converting to base 64 avoids having to deal with line breaks or other control characters in the variables&lt;/p&gt;

&lt;p&gt;b. I started having &lt;code&gt;scripts/set-environment.sh&lt;/code&gt; as a &lt;code&gt;preifnracreate&lt;/code&gt; or &lt;code&gt;preup&lt;/code&gt; hook but experienced that changes made in such a hook script to e.g. &lt;code&gt;cloud-init.txt&lt;/code&gt; were not picked up consistently. Values seemed to be cached somewhere. This lagging update of &lt;code&gt;cloud-init.txt&lt;/code&gt; on the scaleset let me to figure out &lt;a href="https://stackoverflow.com/questions/75956905/who-can-i-check-if-the-correct-version-of-cloud-init-txt-was-used-on-my-linux-az/75956906#75956906" rel="noopener noreferrer"&gt;how to check, which content of cloud-init.txt was used during deployment or reimaging&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;After the deployment I immediately stop Azure Container Instances to not induce cost permanently - using the post provisioning hook &lt;code&gt;hooks/post-provision.sh&lt;/code&gt; ...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt;

&lt;span class="nb"&gt;source&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;azd &lt;span class="nb"&gt;env &lt;/span&gt;get-values | &lt;span class="nb"&gt;grep &lt;/span&gt;NAME&lt;span class="o"&gt;)&lt;/span&gt;

az container stop &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$HUB_JUMP_NAME&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;--verbose&lt;/span&gt;
az container stop &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$SPOKE_JUMP_NAME&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;--verbose&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;... which and then spin up for testing on demand&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;azd &lt;span class="nb"&gt;env &lt;/span&gt;get-values | &lt;span class="nb"&gt;grep &lt;/span&gt;NAME&lt;span class="o"&gt;)&lt;/span&gt;
az container start &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$SPOKE_JUMP_NAME&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt;
az container &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$SPOKE_JUMP_NAME&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;--exec-command&lt;/span&gt; &lt;span class="s2"&gt;"curl http://onprem-server.internal.net:8000"&lt;/span&gt;
az container stop &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$SPOKE_JUMP_NAME&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;source &amp;lt;(azd env get-values | grep NAME)&lt;/code&gt; is a simple way to source some &lt;strong&gt;azd&lt;/strong&gt; / &lt;strong&gt;Bicep&lt;/strong&gt; deployment outputs into shell variables.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deployment
&lt;/h2&gt;

&lt;h2&gt;
  
  
  Preparation
&lt;/h2&gt;

&lt;p&gt;This setup assumes it works with &lt;code&gt;~/.ssh/id_rsa&lt;/code&gt; key pair - to use other key pairs adjust &lt;code&gt;./hooks/preprovision.sh&lt;/code&gt;. If you don't already have a suitable key pair, generate one or modify the preprovision script to point to another public key file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-keygen &lt;span class="nt"&gt;-m&lt;/span&gt; PEM &lt;span class="nt"&gt;-t&lt;/span&gt; rsa &lt;span class="nt"&gt;-b&lt;/span&gt; 4096
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Login to your subscription first with Azure CLI and Azure Developer CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;az login
azd login
azd init
scripts/set-environment.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;be sure to set Azure CLI subscription to same subscription with &lt;code&gt;az account set -s ...&lt;/code&gt; as specified for &lt;code&gt;azd init&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Deploy&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Check, that connection to sample server is working from within Hub network directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;azd &lt;span class="nb"&gt;env &lt;/span&gt;get-values | &lt;span class="nb"&gt;grep &lt;/span&gt;NAME&lt;span class="o"&gt;)&lt;/span&gt;
az container start &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$HUB_JUMP_NAME&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt;
az container &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$HUB_JUMP_NAME&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;--exec-command&lt;/span&gt; &lt;span class="s2"&gt;"wget http://192.168.42.65:8000 -O -"&lt;/span&gt;
az container &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$HUB_JUMP_NAME&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;--exec-command&lt;/span&gt; &lt;span class="s2"&gt;"wget http://192.168.42.66:8000 -O -"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check, that connection to sample servers is working from over Load Balancer within Hub network directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;azd &lt;span class="nb"&gt;env &lt;/span&gt;get-values | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'NAME|TOKEN'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
az container start &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$HUB_JUMP_NAME&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt;
&lt;span class="nv"&gt;ILBIP&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sb"&gt;`&lt;/span&gt;az network lb list &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s2"&gt;"[?contains(name, '&lt;/span&gt;&lt;span class="nv"&gt;$RESOURCE_TOKEN&lt;/span&gt;&lt;span class="s2"&gt;')].frontendIPConfigurations[].privateIPAddress"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; tsv&lt;span class="sb"&gt;`&lt;/span&gt;
az container &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$HUB_JUMP_NAME&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;--exec-command&lt;/span&gt; &lt;span class="s2"&gt;"wget http://&lt;/span&gt;&lt;span class="nv"&gt;$ILBIP&lt;/span&gt;&lt;span class="s2"&gt;:8000 -O -"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check, that connection to sample servers is working from Spoke network&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;azd &lt;span class="nb"&gt;env &lt;/span&gt;get-values | &lt;span class="nb"&gt;grep &lt;/span&gt;NAME&lt;span class="o"&gt;)&lt;/span&gt;
az container start &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$SPOKE_JUMP_NAME&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt;
az container &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nv"&gt;$SPOKE_JUMP_NAME&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; &lt;span class="nv"&gt;$RESOURCE_GROUP_NAME&lt;/span&gt; &lt;span class="nt"&gt;--exec-command&lt;/span&gt; &lt;span class="s2"&gt;"curl http://onprem-server.internal.net:8000"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Final Words
&lt;/h2&gt;

&lt;p&gt;As stated I am not a huge fan of bringing in IaaS components into our cloud infrastructures. Just one more spinning piece one has to keep an eye on. Particularly for smaller team sizes have too many IaaS elements does not scale and shifts a substantial portion of the focus on operations instead of delivering business value - features.&lt;/p&gt;

&lt;p&gt;However, after careful consideration of all options, it sometimes makes sense to bring in such a small piece to avoid to reworking your whole infrastructure or deviate from fundamental design goals.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>networking</category>
      <category>privatelink</category>
    </item>
  </channel>
</rss>
