<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>The Ops Community ⚙️: Kai Walter</title>
    <description>The latest articles on The Ops Community ⚙️ by Kai Walter (@kaiwalter).</description>
    <link>https://community.ops.io/kaiwalter</link>
    <image>
      <url>https://community.ops.io/images/bdRpaLKLtQ1xBwr0_bNHIoQRxuNrMV9rLj8i_XWMryE/rs:fill:90:90/g:sm/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL3Vz/ZXIvcHJvZmlsZV9p/bWFnZS8xOTcvOTI3/ZDNjMDctNjRmOS00/Mzk3LWFhMzItNjU4/MjFiMTg5MjI4Lmpw/ZWc</url>
      <title>The Ops Community ⚙️: Kai Walter</title>
      <link>https://community.ops.io/kaiwalter</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://community.ops.io/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:29:20 +0000</pubDate>
      <link>https://community.ops.io/kaiwalter/challenging-n8n-ai-agent-with-a-personal-productivity-flow-149j</link>
      <guid>https://community.ops.io/kaiwalter/challenging-n8n-ai-agent-with-a-personal-productivity-flow-149j</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" rel="noopener noreferrer"&gt;this post&lt;/a&gt; - so skip that part.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/RVGqJRtDnDt_vxvghQneslooU_uS_GTSV5KlLk92wh0/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2g3dmwx/NnRiY3RmOXZ4aDE1/OTJrLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/RVGqJRtDnDt_vxvghQneslooU_uS_GTSV5KlLk92wh0/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2g3dmwx/NnRiY3RmOXZ4aDE1/OTJrLnBuZw" 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://community.ops.io/images/g47fTDxwZfio_bk2Y_ZHlDdWsTKR8-mbkE0kmlN9bXQ/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL29qYnZ4/b21ieHo1d2tibGF0/cW96LnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/g47fTDxwZfio_bk2Y_ZHlDdWsTKR8-mbkE0kmlN9bXQ/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL29qYnZ4/b21ieHo1d2tibGF0/cW96LnBuZw" 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://community.ops.io/images/Rj8e8MD4uatxZRZwGgNHwX9BB7vu6iD3NeH8NoGAWk4/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2p3Nmg5/MTJubzdvcTZoam5m/N3diLmpwZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/Rj8e8MD4uatxZRZwGgNHwX9BB7vu6iD3NeH8NoGAWk4/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL2p3Nmg5/MTJubzdvcTZoam5m/N3diLmpwZw" alt="n8n AI Agent node with all its capabilities" width="800" height="400"&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://community.ops.io/images/FGRcb2A4ltvSr_7zsPbYp1_7Qnb1gk0Tsz3DeJJwjk8/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL25mN3lq/bDk0aXdzeGlmNmFu/ZW9nLmpwZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/FGRcb2A4ltvSr_7zsPbYp1_7Qnb1gk0Tsz3DeJJwjk8/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9jb21t/dW5pdHkub3BzLmlv/L3JlbW90ZWltYWdl/cy91cGxvYWRzL2Fy/dGljbGVzL25mN3lq/bDk0aXdzeGlmNmFu/ZW9nLmpwZw" 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>
    </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:15 +0000</pubDate>
      <link>https://community.ops.io/kaiwalter/inject-nixos-into-an-azure-vm-with-nixos-anywhere-and-azure-container-intances-4ke6</link>
      <guid>https://community.ops.io/kaiwalter/inject-nixos-into-an-azure-vm-with-nixos-anywhere-and-azure-container-intances-4ke6</guid>
      <description>&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;In a &lt;a href="https://community.ops.io/kaiwalter/azure-vm-based-on-cbl-mariner-with-nix-package-manager-56ao"&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://community.ops.io/kaiwalter/creating-a-certificate-authority-for-testing-with-azure-container-instances-27ch"&gt;as temporary certificate authority&lt;/a&gt; or &lt;a href="https://community.ops.io/kaiwalter/handling-an-acme-challenge-response-with-a-temporary-azure-container-instance-2f2d"&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:16 +0000</pubDate>
      <link>https://community.ops.io/kaiwalter/automating-azure-vm-ubuntu-install-without-fancy-tools-1in6</link>
      <guid>https://community.ops.io/kaiwalter/automating-azure-vm-ubuntu-install-without-fancy-tools-1in6</guid>
      <description>&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;In a &lt;a href="https://community.ops.io/kaiwalter/create-a-disposable-azure-vm-based-on-cbl-mariner-2ic6"&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://community.ops.io/kaiwalter/azure-vm-based-on-cbl-mariner-with-nix-package-manager-56ao"&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:42 +0000</pubDate>
      <link>https://community.ops.io/kaiwalter/taking-spin-for-a-spin-on-aks-5e8a</link>
      <guid>https://community.ops.io/kaiwalter/taking-spin-for-a-spin-on-aks-5e8a</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"&gt;Spin&lt;/a&gt; can already be utilized on Kubernetes in coexistency with existing workloads using &lt;a href="https://www.spinkube.dev/"&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://community.ops.io/kaiwalter/comparing-azure-functions-vs-dapr-on-azure-container-apps-37ll"&gt;Comparing Azure Functions vs Dapr throughput on Azure Container Apps&lt;/a&gt; and &lt;a href="https://community.ops.io/kaiwalter/how-to-tune-dapr-bulk-publishsubscribe-for-maximum-throughput-3pkl"&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"&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"&gt;containerd Wasm Shim&lt;/a&gt; is required which itself utilizes &lt;a href="https://github.com/containerd/runwasi"&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"&gt;Spin&lt;/a&gt;. Of the &lt;a href="https://github.com/fermyon/spin?tab=readme-ov-file#language-support-for-spin-features"&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"&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/"&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"&gt;SpinOperator&lt;/a&gt;. As SpinOperator does not support injecting side-cars yet, &lt;a href="https://github.com/dapr-sandbox/dapr-shared"&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://community.ops.io/images/_Ve3YKwll8rl4I1euyZc9_qy6KwcpqnLimdleojtB9o/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9yYXcu/Z2l0aHVidXNlcmNv/bnRlbnQuY29tL0th/aVdhbHRlci9hdXRv/bWF0ZS1wb3N0cy9t/YWluL2ltYWdlcy9F/bnRlcnByaXNlV2Fz/bS5wbmc" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/_Ve3YKwll8rl4I1euyZc9_qy6KwcpqnLimdleojtB9o/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9yYXcu/Z2l0aHVidXNlcmNv/bnRlbnQuY29tL0th/aVdhbHRlci9hdXRv/bWF0ZS1wb3N0cy9t/YWluL2ltYWdlcy9F/bnRlcnByaXNlV2Fz/bS5wbmc" 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"&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"&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"&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"&gt;salaboy&lt;/a&gt; for helping to get &lt;a href="https://github.com/dapr-sandbox/dapr-shared"&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>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:35 +0000</pubDate>
      <link>https://community.ops.io/kaiwalter/how-to-tune-dapr-bulk-publishsubscribe-for-maximum-throughput-3pkl</link>
      <guid>https://community.ops.io/kaiwalter/how-to-tune-dapr-bulk-publishsubscribe-for-maximum-throughput-3pkl</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://community.ops.io/kaiwalter/comparing-azure-functions-vs-dapr-on-azure-container-apps-37ll"&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"&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/"&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"&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/"&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://community.ops.io/images/vXzqor2gSdOeEEhO4NgaX7QY0z5G_O5mZa1Tcxf4UXc/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9yYXcu/Z2l0aHVidXNlcmNv/bnRlbnQuY29tL0th/aVdhbHRlci9hdXRv/bWF0ZS1wb3N0cy9t/YWluL2ltYWdlcy8y/MDI0LTAyLWRhcHIt/cHVic3ViLTItc2xv/dy5wbmc" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/vXzqor2gSdOeEEhO4NgaX7QY0z5G_O5mZa1Tcxf4UXc/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9yYXcu/Z2l0aHVidXNlcmNv/bnRlbnQuY29tL0th/aVdhbHRlci9hdXRv/bWF0ZS1wb3N0cy9t/YWluL2ltYWdlcy8y/MDI0LTAyLWRhcHIt/cHVic3ViLTItc2xv/dy5wbmc" alt="78 seconds cycle time with spread of request processing" width="800" height="289"&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://community.ops.io/images/ncKS4P0Z_SZDRUs7zNB4KTptWr75r3e3UFy3Q30bnyk/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9yYXcu/Z2l0aHVidXNlcmNv/bnRlbnQuY29tL0th/aVdhbHRlci9hdXRv/bWF0ZS1wb3N0cy9t/YWluL2ltYWdlcy8y/MDI0LTAyLWRhcHIt/cHVicHViLTItZmFz/dC5wbmc" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/ncKS4P0Z_SZDRUs7zNB4KTptWr75r3e3UFy3Q30bnyk/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9yYXcu/Z2l0aHVidXNlcmNv/bnRlbnQuY29tL0th/aVdhbHRlci9hdXRv/bWF0ZS1wb3N0cy9t/YWluL2ltYWdlcy8y/MDI0LTAyLWRhcHIt/cHVicHViLTItZmFz/dC5wbmc" 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://community.ops.io/images/8ljC5HvTdnxJfzj_9lmsQ1cNAxg1VAkOlhNy1NQG3sI/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9yYXcu/Z2l0aHVidXNlcmNv/bnRlbnQuY29tL0th/aVdhbHRlci9hdXRv/bWF0ZS1wb3N0cy9t/YWluL2ltYWdlcy8y/MDI0LTAyLWRhcHIt/cHVic3ViLTItcmVw/bGljYWNvdW50LnBu/Zw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/8ljC5HvTdnxJfzj_9lmsQ1cNAxg1VAkOlhNy1NQG3sI/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9yYXcu/Z2l0aHVidXNlcmNv/bnRlbnQuY29tL0th/aVdhbHRlci9hdXRv/bWF0ZS1wb3N0cy9t/YWluL2ltYWdlcy8y/MDI0LTAyLWRhcHIt/cHVic3ViLTItcmVw/bGljYWNvdW50LnBu/Zw" 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://community.ops.io/images/fzZ_brLpNTZz5pnVUzHIJjgcg720fr_SWKS6W61XvoQ/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9yYXcu/Z2l0aHVidXNlcmNv/bnRlbnQuY29tL0th/aVdhbHRlci9hdXRv/bWF0ZS1wb3N0cy9t/YWluL2ltYWdlcy8y/MDI0LTAyLWRhcHIt/cHVic3ViLTItUkVU/UlkucG5n" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/fzZ_brLpNTZz5pnVUzHIJjgcg720fr_SWKS6W61XvoQ/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9yYXcu/Z2l0aHVidXNlcmNv/bnRlbnQuY29tL0th/aVdhbHRlci9hdXRv/bWF0ZS1wb3N0cy9t/YWluL2ltYWdlcy8y/MDI0LTAyLWRhcHIt/cHVic3ViLTItUkVU/UlkucG5n" 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://community.ops.io/images/euwJ12vaqV047UFX7-DNL8mlfg0fYzIYpxs1vTwK2dE/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9yYXcu/Z2l0aHVidXNlcmNv/bnRlbnQuY29tL0th/aVdhbHRlci9hdXRv/bWF0ZS1wb3N0cy9t/YWluL2ltYWdlcy8y/MDI0LTAyLWRhcHIt/cHVic3ViLTMtc3By/ZWFkLnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/euwJ12vaqV047UFX7-DNL8mlfg0fYzIYpxs1vTwK2dE/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9yYXcu/Z2l0aHVidXNlcmNv/bnRlbnQuY29tL0th/aVdhbHRlci9hdXRv/bWF0ZS1wb3N0cy9t/YWluL2ltYWdlcy8y/MDI0LTAyLWRhcHIt/cHVic3ViLTMtc3By/ZWFkLnBuZw" alt="76 seconds cycle time with spread of request processing" width="800" height="335"&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"&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/"&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>csharp</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:48 +0000</pubDate>
      <link>https://community.ops.io/kaiwalter/comparing-azure-functions-vs-dapr-on-azure-container-apps-37ll</link>
      <guid>https://community.ops.io/kaiwalter/comparing-azure-functions-vs-dapr-on-azure-container-apps-37ll</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"&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"&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/"&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"&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"&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://community.ops.io/images/gkDxIp2vkrq_qIoppdVKUgH0lp9dZlaEFo5JJsb5w0Y/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9naXRo/dWIuY29tL0thaVdh/bHRlci9tZXNzYWdl/LWRpc3RyaWJ1dGlv/bi9ibG9iL3YxLjAu/MS9tZWRpYS8yMDIz/LTA4LTA4LXNjYWxp/bmctZnVuYy5wbmc_/cmF3PXRydWU" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/gkDxIp2vkrq_qIoppdVKUgH0lp9dZlaEFo5JJsb5w0Y/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9naXRo/dWIuY29tL0thaVdh/bHRlci9tZXNzYWdl/LWRpc3RyaWJ1dGlv/bi9ibG9iL3YxLjAu/MS9tZWRpYS8yMDIz/LTA4LTA4LXNjYWxp/bmctZnVuYy5wbmc_/cmF3PXRydWU" alt="Regular scaling of Functions container" width="800" height="328"&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://community.ops.io/images/8u9DEpf9I3rg2lJ6TV3NrbPP06fhGYiJ0Rij5Ydz--I/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9naXRo/dWIuY29tL0thaVdh/bHRlci9tZXNzYWdl/LWRpc3RyaWJ1dGlv/bi9ibG9iL3YxLjAu/MS9tZWRpYS8yMDIz/LTA4LTA4LXNjYWxp/bmctYWNhZi5wbmc_/cmF3PXRydWU" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/8u9DEpf9I3rg2lJ6TV3NrbPP06fhGYiJ0Rij5Ydz--I/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9naXRo/dWIuY29tL0thaVdh/bHRlci9tZXNzYWdl/LWRpc3RyaWJ1dGlv/bi9ibG9iL3YxLjAu/MS9tZWRpYS8yMDIz/LTA4LTA4LXNjYWxp/bmctYWNhZi5wbmc_/cmF3PXRydWU" 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"&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://community.ops.io/images/39nxVnO8nhrIeFb64z0onOcdF_VeIPR9_xeZQ8A3_aU/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9naXRo/dWIuY29tL0thaVdh/bHRlci9tZXNzYWdl/LWRpc3RyaWJ1dGlv/bi9ibG9iL3YxLjAu/MS9tZWRpYS90ZXN0/LXNldHVwLnBuZz9y/YXc9dHJ1ZQ" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/39nxVnO8nhrIeFb64z0onOcdF_VeIPR9_xeZQ8A3_aU/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9naXRo/dWIuY29tL0thaVdh/bHRlci9tZXNzYWdl/LWRpc3RyaWJ1dGlv/bi9ibG9iL3YxLjAu/MS9tZWRpYS90ZXN0/LXNldHVwLnBuZz9y/YXc9dHJ1ZQ" alt="Solution overview showing main components" width="800" height="542"&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"&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://community.ops.io/images/n0ALdFYjunNd9GfoSN8N2Yq4p0AukERZYWrIu8DdhrM/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9naXRo/dWIuY29tL0thaVdh/bHRlci9tZXNzYWdl/LWRpc3RyaWJ1dGlv/bi9ibG9iL3YxLjAu/MS9tZWRpYS8yMDIz/LTA4LTA4LXJlc3Vs/dHMucG5nP3Jhdz10/cnVl" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/n0ALdFYjunNd9GfoSN8N2Yq4p0AukERZYWrIu8DdhrM/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9naXRo/dWIuY29tL0thaVdh/bHRlci9tZXNzYWdl/LWRpc3RyaWJ1dGlv/bi9ibG9iL3YxLjAu/MS9tZWRpYS8yMDIz/LTA4LTA4LXJlc3Vs/dHMucG5nP3Jhdz10/cnVl" 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://community.ops.io/images/kYavtBE1A4oea22YXGMpae7U8UzQBRxKu1giQ5mPNfw/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9yYXcu/Z2l0aHVidXNlcmNv/bnRlbnQuY29tL0th/aVdhbHRlci9hdXRv/bWF0ZS1wb3N0cy9t/YWluL2ltYWdlcy8y/MDIzLTEwLWRhcHIt/ZnVuYy1hY2EtdG90/YWxzLXdldS5wbmc" alt="comparing total runtimes in October in West Europe" width="385" height="101"&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://community.ops.io/images/7zCaXn5tSToxZHgg_Ol8f57qikuyctWBIfPKROFad7w/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9yYXcu/Z2l0aHVidXNlcmNv/bnRlbnQuY29tL0th/aVdhbHRlci9hdXRv/bWF0ZS1wb3N0cy9t/YWluL2ltYWdlcy8y/MDIzLTEwLWRhcHIt/ZnVuYy1hY2EtdG90/YWxzLXd1cy5wbmc" 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://community.ops.io/images/v4YeDWHY1ew1o8MjTenDl1SCzdnaH66OqTzS50gRz7M/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9yYXcu/Z2l0aHVidXNlcmNv/bnRlbnQuY29tL0th/aVdhbHRlci9hdXRv/bWF0ZS1wb3N0cy9t/YWluL2ltYWdlcy8y/MDIzLTEwLWRhcHIt/ZnVuYy1hY2EtdGlt/ZS13ZXUucG5n" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/v4YeDWHY1ew1o8MjTenDl1SCzdnaH66OqTzS50gRz7M/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9yYXcu/Z2l0aHVidXNlcmNv/bnRlbnQuY29tL0th/aVdhbHRlci9hdXRv/bWF0ZS1wb3N0cy9t/YWluL2ltYWdlcy8y/MDIzLTEwLWRhcHIt/ZnVuYy1hY2EtdGlt/ZS13ZXUucG5n" 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"&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/"&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://community.ops.io/images/S_GV9nV5FP27Wf9C73TqeRXkBqqKazCpWXBzdp1WQVs/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9yYXcu/Z2l0aHVidXNlcmNv/bnRlbnQuY29tL0th/aVdhbHRlci9hdXRv/bWF0ZS1wb3N0cy9t/YWluL2ltYWdlcy9j/b21wYXJpbmctZnVu/Y3Rpb25zLWRhcHIt/YWNhLXRocm90dGxp/bmcucG5n" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/S_GV9nV5FP27Wf9C73TqeRXkBqqKazCpWXBzdp1WQVs/rt:fit/w:800/g:sm/q:0/mb:500000/ar:1/aHR0cHM6Ly9yYXcu/Z2l0aHVidXNlcmNv/bnRlbnQuY29tL0th/aVdhbHRlci9hdXRv/bWF0ZS1wb3N0cy9t/YWluL2ltYWdlcy9j/b21wYXJpbmctZnVu/Y3Rpb25zLWRhcHIt/YWNhLXRocm90dGxp/bmcucG5n" 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"&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"&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>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:05 +0000</pubDate>
      <link>https://community.ops.io/kaiwalter/get-neovim-plugins-with-build-processes-working-on-windows-1m3</link>
      <guid>https://community.ops.io/kaiwalter/get-neovim-plugins-with-build-processes-working-on-windows-1m3</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://community.ops.io/kaiwalter/share-neovim-configuration-between-linux-and-windows-4p41"&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/"&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://community.ops.io/kaiwalter/share-neovim-configuration-between-linux-and-windows-4p41"&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"&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"&gt;distros&lt;/a&gt; like &lt;a href="https://www.lazyvim.org/"&gt;LazyVim&lt;/a&gt;,  &lt;a href="https://www.lunarvim.org/"&gt;LunarVim&lt;/a&gt;, &lt;a href="https://astronvim.com/"&gt;AstroVim&lt;/a&gt;, and &lt;a href="https://nvchad.com/"&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"&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"&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"&gt;NeoVim configuration of Josean Martinez&lt;/a&gt; which he explains in this &lt;a href="https://youtu.be/NL8D8EkphUw"&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"&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:11 +0000</pubDate>
      <link>https://community.ops.io/kaiwalter/azure-vm-based-on-cbl-mariner-with-nix-package-manager-56ao</link>
      <guid>https://community.ops.io/kaiwalter/azure-vm-based-on-cbl-mariner-with-nix-package-manager-56ao</guid>
      <description>&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;In a previous &lt;a href="https://community.ops.io/kaiwalter/create-a-disposable-azure-vm-based-on-cbl-mariner-2ic6"&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"&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"&gt;https://ops.io&lt;/a&gt; and &lt;a href="https://hashnode.com"&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"&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>
    </item>
    <item>
      <title>Share NeoVim configuration between Linux and Windows</title>
      <dc:creator>Kai Walter</dc:creator>
      <pubDate>Mon, 29 May 2023 07:37:46 +0000</pubDate>
      <link>https://community.ops.io/kaiwalter/share-neovim-configuration-between-linux-and-windows-4p41</link>
      <guid>https://community.ops.io/kaiwalter/share-neovim-configuration-between-linux-and-windows-4p41</guid>
      <description>&lt;h2&gt;
  
  
  Motivation
&lt;/h2&gt;

&lt;p&gt;As posted on &lt;a href="https://techhub.social/@ancientITguy/110440880551590800"&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"&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"&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>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:04 +0000</pubDate>
      <link>https://community.ops.io/kaiwalter/private-linking-an-azure-container-app-environment-may23-update-49ic</link>
      <guid>https://community.ops.io/kaiwalter/private-linking-an-azure-container-app-environment-may23-update-49ic</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"&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://community.ops.io/kaiwalter/private-linking-and-port-forwarding-to-non-azure-resources-3ea5"&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://community.ops.io/images/Bm0K9o22J-RUfrcLtXzAYtZQyAPkbti3NzvL7SQ_718/w:800/mb:500000/ar:1/aHR0cHM6Ly9yYXcu/Z2l0aHVidXNlcmNv/bnRlbnQuY29tL0th/aVdhbHRlci9hdXRv/bWF0ZS1wb3N0cy9t/YWluL2ltYWdlcy9w/cml2YXRlLWxpbmst/Y29udGFpbmVyLWFw/cC1lbnZpcm9ubWVu/dC5wbmc" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/Bm0K9o22J-RUfrcLtXzAYtZQyAPkbti3NzvL7SQ_718/w:800/mb:500000/ar:1/aHR0cHM6Ly9yYXcu/Z2l0aHVidXNlcmNv/bnRlbnQuY29tL0th/aVdhbHRlci9hdXRv/bWF0ZS1wb3N0cy9t/YWluL2ltYWdlcy9w/cml2YXRlLWxpbmst/Y29udGFpbmVyLWFw/cC1lbnZpcm9ubWVu/dC5wbmc" alt="hub/spoke Container Apps configuration with private link" width="800" height="273"&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"&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"&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>
    </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:52 +0000</pubDate>
      <link>https://community.ops.io/kaiwalter/private-linking-and-port-forwarding-to-non-azure-resources-3ea5</link>
      <guid>https://community.ops.io/kaiwalter/private-linking-and-port-forwarding-to-non-azure-resources-3ea5</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://community.ops.io/images/TCFv_e_BePpCu3xOdXcJmhjkbDT8I00lwy9SBvRzwMY/w:880/mb:500000/ar:1/aHR0cHM6Ly9kZXYt/dG8tdXBsb2Fkcy5z/My5hbWF6b25hd3Mu/Y29tL3VwbG9hZHMv/YXJ0aWNsZXMvcnVr/ZXRvbDdrbGdtaHRo/NTJ1MmUucG5n" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/TCFv_e_BePpCu3xOdXcJmhjkbDT8I00lwy9SBvRzwMY/w:880/mb:500000/ar:1/aHR0cHM6Ly9kZXYt/dG8tdXBsb2Fkcy5z/My5hbWF6b25hd3Mu/Y29tL3VwbG9hZHMv/YXJ0aWNsZXMvcnVr/ZXRvbDdrbGdtaHRo/NTJ1MmUucG5n" alt="Azure Container Apps environment private linked into a corporate managed virtual network with limited IP address space" width="880" height="302"&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://community.ops.io/images/2mBK5EysMSPCpO8rPz1034khaeWJs8P7UbxAZPU76fI/w:880/mb:500000/ar:1/aHR0cHM6Ly9kZXYt/dG8tdXBsb2Fkcy5z/My5hbWF6b25hd3Mu/Y29tL3VwbG9hZHMv/YXJ0aWNsZXMvNGI1/aTdsbjRrN29uMG5v/dTFjd3UucG5n" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/2mBK5EysMSPCpO8rPz1034khaeWJs8P7UbxAZPU76fI/w:880/mb:500000/ar:1/aHR0cHM6Ly9kZXYt/dG8tdXBsb2Fkcy5z/My5hbWF6b25hd3Mu/Y29tL3VwbG9hZHMv/YXJ0aWNsZXMvNGI1/aTdsbjRrN29uMG5v/dTFjd3UucG5n" 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"&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"&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"&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"&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"&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"&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"&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"&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"&gt;repo&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/twOc9ig3O4Ntg85oBYjuEWaNfQSuYqhGOXMAVh42z1I/w:880/mb:500000/ar:1/aHR0cHM6Ly9yYXcu/Z2l0aHVidXNlcmNv/bnRlbnQuY29tL0th/aVdhbHRlci9hdXRv/bWF0ZS1wb3N0cy9t/YWluL2ltYWdlcy9w/cml2YXRlLWxpbmst/cG9ydC1mb3J3YXJk/LnBuZw" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/twOc9ig3O4Ntg85oBYjuEWaNfQSuYqhGOXMAVh42z1I/w:880/mb:500000/ar:1/aHR0cHM6Ly9yYXcu/Z2l0aHVidXNlcmNv/bnRlbnQuY29tL0th/aVdhbHRlci9hdXRv/bWF0ZS1wb3N0cy9t/YWluL2ltYWdlcy9w/cml2YXRlLWxpbmst/cG9ydC1mb3J3YXJk/LnBuZw" 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"&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"&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"&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"&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>
    </item>
    <item>
      <title>Using Azure private links and private DNS zones with globally distributed resources</title>
      <dc:creator>Kai Walter</dc:creator>
      <pubDate>Thu, 23 Mar 2023 17:40:05 +0000</pubDate>
      <link>https://community.ops.io/kaiwalter/using-azure-private-links-and-private-dns-zones-with-globally-distributed-resources-2onm</link>
      <guid>https://community.ops.io/kaiwalter/using-azure-private-links-and-private-dns-zones-with-globally-distributed-resources-2onm</guid>
      <description>&lt;p&gt;Azure &lt;a href="https://docs.microsoft.com/en-us/azure/private-link/private-endpoint-overview"&gt;private link / endpoints&lt;/a&gt; allow you to connect resources to your private virtual network and with that - when removing public access - shield resources from being accessed or even attacked from the internet. For most of enterprise mission critical systems I help designing and implementing in the cloud, this kind of locked down environment is a hard requirement.&lt;/p&gt;

&lt;p&gt;Private link as a way of restricting access to resources only for a defined range of virtual networks is an additional offering for &lt;a href="https://docs.microsoft.com/en-us/azure/virtual-network/virtual-network-service-endpoints-overview"&gt;service endpoints&lt;/a&gt; which I used so far in projects till spring 2020. This &lt;a href="https://sameeraman.wordpress.com/2019/10/30/azure-private-link-vs-azure-service-endpoints/"&gt;post&lt;/a&gt; by &lt;a href="https://twitter.com/sameera_man"&gt;Sameera Perera&lt;/a&gt; shows the basic differences between these 2 offerings.&lt;/p&gt;

&lt;p&gt;When bringing up a new environment I learned that even some resources like Azure Container Registry have a better support for private linking then for service endpoints. Hence I started looking into this other offering to check whether I can achieve a similar or even better behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;linking multiple private DNS zones to a virtual network is possible if none or not more than 1 DNS zone has auto registration enabled&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;(as observed) most resource types do not autoregister into the private DNS zone linked to a virtual network anyway -&amp;gt; manual creation of private DNS recordsets is required&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;the private DNS zone name is also the resource name and so (if required) the same private DNS zone name can only be created once in a resource group&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  target setup&lt;a&gt;&lt;/a&gt;
&lt;/h2&gt;

&lt;p&gt;The application is deployed in multiple regions (more than 2) across the globe to allow for a certain degree of autonomous operation or even take over operation if a region is down.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://community.ops.io/images/FB4E8RtuMAVtAxoDiY7w8v0cXYTwkLah8Gg9tKhVcWA/w:880/mb:500000/ar:1/aHR0cHM6Ly9kZXYt/dG8tdXBsb2Fkcy5z/My5hbWF6b25hd3Mu/Y29tL2kvZ3A1MzUx/b2V5b2Vnd20xaDZ4/aXQuanBn" class="article-body-image-wrapper"&gt;&lt;img src="https://community.ops.io/images/FB4E8RtuMAVtAxoDiY7w8v0cXYTwkLah8Gg9tKhVcWA/w:880/mb:500000/ar:1/aHR0cHM6Ly9kZXYt/dG8tdXBsb2Fkcy5z/My5hbWF6b25hd3Mu/Y29tL2kvZ3A1MzUx/b2V5b2Vnd20xaDZ4/aXQuanBn" alt="Cloud network infrastructure this post is based on" width="880" height="660"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  global resources
&lt;/h3&gt;

&lt;p&gt;Resources that are globally deployed or replicated hold state or configuration data that is relevant throughout all regions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Front Door&lt;/li&gt;
&lt;li&gt;API Management&lt;/li&gt;
&lt;li&gt;Cosmos DB (with multi master write)&lt;/li&gt;
&lt;li&gt;Container Registry&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  regional resources
&lt;/h3&gt;

&lt;p&gt;Resources that are deployed in individual regions, hold region specific data, process regional data and should be only accessible from within the region:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Application Gateway to handle ingress from Front Door&lt;/li&gt;
&lt;li&gt;API Management (Gateway)&lt;/li&gt;
&lt;li&gt;AKS cluster&lt;/li&gt;
&lt;li&gt;Storage&lt;/li&gt;
&lt;li&gt;SQL Server&lt;/li&gt;
&lt;li&gt;ServiceBus&lt;/li&gt;
&lt;li&gt;KeyVault&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h3&gt;
  
  
  Front Door / Application Gateway
&lt;/h3&gt;

&lt;p&gt;... are just used to control global ingress into regions and have no attachment to this private link / DNS scenario.&lt;/p&gt;

&lt;h3&gt;
  
  
  API Management
&lt;/h3&gt;

&lt;p&gt;... can be deployed globally and is linked into &lt;code&gt;frontend&lt;/code&gt; virtual networks in each region with a dedicated IP address. API Management currently has no affiliation with private link and hence also no way to sensibly bring regional gateway name resolution into private DNS zones. As API gateways are only addressed internally from containers I &lt;a href="https://kubernetes.io/docs/concepts/services-networking/add-entries-to-pod-etc-hosts-with-host-aliases/"&gt;feed IP adress / FQDN pairs into K8S &lt;code&gt;hostaliases&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  AKS
&lt;/h3&gt;

&lt;p&gt;... setup is based on this &lt;a href="https://medium.com/@denniszielke/fully-private-aks-clusters-without-any-public-ips-finally-7f5688411184"&gt;post&lt;/a&gt; courtesy of &lt;a href="https://twitter.com/denzielke"&gt;Dennis Zielke&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  challenges
&lt;/h2&gt;

&lt;h3&gt;
  
  
  multiple private DNS zones linked to a virtual network
&lt;/h3&gt;

&lt;p&gt;Creating a single resource and private linking it to a virtual network is pretty straight forward and has docs dedicated to each of these resources e.g. &lt;a href="https://docs.microsoft.com/en-us/azure/cosmos-db/how-to-configure-private-endpoints"&gt;for Cosmos DB&lt;/a&gt;. To achieve DNS name resolution - without standing up an own custom DNS server or fiddling around with &lt;code&gt;hosts.&lt;/code&gt; files (which btw would not work e.g. for API Management) - private DNS zones can be used. But: each resource type needs a &lt;a href="https://docs.microsoft.com/en-us/azure/private-link/private-endpoint-dns"&gt;dedicated private DNS zone&lt;/a&gt; to be created and maintained.&lt;/p&gt;

&lt;p&gt;In my scenario I created required private DNS zones with in the ARM template used to create the network configuration, immediately linking these zones to the &lt;code&gt;frontend&lt;/code&gt; and &lt;code&gt;backend&lt;/code&gt; virtual networks.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"privateZoneNames"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"array"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"defaultValue"&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="s2"&gt;"privatelink.database.windows.net"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"privatelink.vaultcore.azure.net"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"privatelink.blob.core.windows.net"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"privatelink.servicebus.windows.net"&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="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;DNS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;zones&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Microsoft.Network/privateDnsZones"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"apiVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2018-09-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[parameters('privateZoneNames')[copyIndex()]]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"global"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"copy"&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="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"zonecopy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[length(parameters('privateZoneNames'))]"&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="p"&gt;}&lt;/span&gt;&lt;span class="err"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Microsoft.Network/privateDnsZones/virtualNetworkLinks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"apiVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2018-09-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[concat(parameters('privateZoneNames')[copyIndex()], '/', replace(parameters('privateZoneNames')[copyIndex()],'privatelink.',''),'-',parameters('vnetNameBackend'),'-link')]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"global"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&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="s2"&gt;"[resourceId('Microsoft.Network/privateDnsZones', parameters('privateZoneNames')[copyIndex()])]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetNameBackend'))]"&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="nl"&gt;"properties"&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="nl"&gt;"registrationEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"virtualNetwork"&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="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetNameBackend'))]"&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"copy"&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="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"zonebackendlinkcopy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[length(parameters('privateZoneNames'))]"&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="p"&gt;}&lt;/span&gt;&lt;span class="err"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Microsoft.Network/privateDnsZones/virtualNetworkLinks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"apiVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2018-09-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[concat(parameters('privateZoneNames')[copyIndex()], '/', replace(parameters('privateZoneNames')[copyIndex()],'privatelink.',''),'-',parameters('vnetNameFrontend'),'-link')]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"global"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&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="s2"&gt;"[resourceId('Microsoft.Network/privateDnsZones', parameters('privateZoneNames')[copyIndex()])]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetNameFrontend'))]"&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="nl"&gt;"properties"&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="nl"&gt;"registrationEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"virtualNetwork"&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="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[resourceId('Microsoft.Network/virtualNetworks', parameters('vnetNameFrontend'))]"&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"copy"&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="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"zonefrontendlinkcopy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[length(parameters('privateZoneNames'))]"&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="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&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;Only the DNS zones for the 4 regional resource types are created here for the overall network. Creation of DNS zones for the global resources is covered later.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When creating private endpoints for multiple resources and with that linking multiple private DNS zones to the same virtual network, &lt;strong&gt;auto registration cannot be enabled&lt;/strong&gt; on more than one private DNS zone.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Auto registration for me seems to make sense for automatically registering VMs in other type of scenarios - not so much when handling Azure PaaS resources.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;However when auto registration is disabled, you have to create DNS recordsets manually or to script the creation - not saying when it is enabled that resources - at least based on my observations - would necessarily register automatically.&lt;/p&gt;

&lt;p&gt;Creating these private DNS recordsets can be achieved by looping through the FQDN entries registered for the created endpoint. These entries look like this e.g. for Azure SQL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;CustomDnsConfigs&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="err"&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
                                 &lt;/span&gt;&lt;span class="nl"&gt;"Fqdn"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"my-sqlsvr.database.windows.net"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                                 &lt;/span&gt;&lt;span class="nl"&gt;"IpAddresses"&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="s2"&gt;"10.2.6.42"&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="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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This entry needs to be created in a private DNS zone &lt;code&gt;privatelink.database.windows.net&lt;/code&gt; but without the public domain name &lt;code&gt;database.windows.net&lt;/code&gt; - hence this domain name suffix needs to be removed before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nv"&gt;$resourceGroup&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="s2"&gt;"resourcegroupfordnszone"&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nv"&gt;$globalDnsSuffx&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="s2"&gt;".database.windows.net"&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nv"&gt;$dnsZoneName&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="s2"&gt;"privatelink.database.windows.net"&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="c"&gt;# get custom DNS entries for endpoint just created&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-AzPrivateEndpoint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$privateEndpoint&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CustomDnsConfigs&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="o"&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="nv"&gt;$recordSetName&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="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Fqdn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-replace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$globalDnsSuffx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="c"&gt;# remove existing record&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="n"&gt;Get-AzPrivateDnsRecordSet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ResourceGroupName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$resourceGroup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ZoneName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dnsZoneName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$recordSetName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-RecordType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;A&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;Remove-AzPrivateDnsRecordSet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ResourceGroupName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$resourceGroup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ZoneName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dnsZoneName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$recordSetName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-RecordType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;A&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;# create new record&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;New-AzPrivateDnsRecordSet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ResourceGroupName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$resourceGroup&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ZoneName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dnsZoneName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$recordSetName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nt"&gt;-PrivateDnsRecord&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;New-AzPrivateDnsRecordConfig&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-IPv4Address&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IpAddresses&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nt"&gt;-RecordType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Ttl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;3600&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;h3&gt;
  
  
  handling DNS entries for global resources with mutliple IP addresses
&lt;/h3&gt;

&lt;p&gt;Creating the DNS entries for the regional resources was possible because the resource names (SQL, KeyVault, Storage, ...) had the region somewhere in the resource name anyway (e.g. fancy-sql-westus, fancy-sql-eastus) and with that providing unique name to IP address mappings.&lt;/p&gt;

&lt;p&gt;For global resources there is one name (e.g. fancy-cosmos-global, fancy-cr-global) with an IP address / a set of IP addresses for each private link endpoint created in the regional virtual networks.&lt;/p&gt;

&lt;p&gt;For CosmosDB it would result in a list like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fancy-cosmos-global                   {10.1.6.17,10.2.6.15}
fancy-cosmos-global.eastus.data       {10.1.6.16,10.2.6.14}
fancy-cosmos-global.westus.data       {10.1.6.15,10.2.6.13}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No way for a consuming resources in &lt;code&gt;backend&lt;/code&gt; or &lt;code&gt;frontend&lt;/code&gt; network trying to resolve &lt;code&gt;fancy-cosmos-global.documents.azure.com&lt;/code&gt; or in fact &lt;code&gt;fancy-cosmos-global.privatelink.documents.azure.com&lt;/code&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In my first attempts I failed with the assumption or expectation that somehow the source network would be considered here in the private DNS resolution.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To work around this, one would only need to create a dedicated private DNS zone for each region and feed the entries/IP addresses relevant for this region, but ... as private DNS zone name is also the resource name within a resource group, you can only have one private DNS zone (for a given resource type) in one resource group (until that stage I only had one resource group holding all the network resources including private DNS zones).&lt;/p&gt;

&lt;p&gt;Hence I created additional resource groups for private links, endpoints and DNS zones specifically for a region but keep the common resource group for all the private DNS zones where this is not required.&lt;/p&gt;

&lt;p&gt;For that I use a similar ARM template to create private DNS zones and link those to the virtual networks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"contentVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.0.0.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"parameters"&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="nl"&gt;"vnetNameFrontend"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"defaultValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"frontend"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&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="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VNet frontend name"&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"vnetNameBackend"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"defaultValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"backend"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&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="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"VNet backend name"&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"networkResourceGroup"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"defaultValue"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"network"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&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="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"network resource group name"&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"privateZoneNames"&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"array"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"defaultValue"&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="s2"&gt;"privatelink.documents.azure.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="s2"&gt;"privatelink.azurecr.io"&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="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="nl"&gt;"variables"&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"resources"&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;private&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;DNS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;zones&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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Microsoft.Network/privateDnsZones"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"apiVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2018-09-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[parameters('privateZoneNames')[copyIndex()]]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"global"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"copy"&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="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"zonecopy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[length(parameters('privateZoneNames'))]"&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="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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Microsoft.Network/privateDnsZones/virtualNetworkLinks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"apiVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2018-09-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[concat(parameters('privateZoneNames')[copyIndex()], '/', replace(parameters('privateZoneNames')[copyIndex()],'privatelink.',''),'-',parameters('vnetNameBackend'),'-link')]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"global"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&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="s2"&gt;"[resourceId('Microsoft.Network/privateDnsZones', parameters('privateZoneNames')[copyIndex()])]"&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="nl"&gt;"properties"&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="nl"&gt;"registrationEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"virtualNetwork"&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="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks', parameters('vnetNameBackend'))]"&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"copy"&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="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"zoneclusterlinkcopy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[length(parameters('privateZoneNames'))]"&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="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="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Microsoft.Network/privateDnsZones/virtualNetworkLinks"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"apiVersion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2018-09-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[concat(parameters('privateZoneNames')[copyIndex()], '/', replace(parameters('privateZoneNames')[copyIndex()],'privatelink.',''),'-',parameters('vnetNameFrontend'),'-link')]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"location"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"global"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"dependsOn"&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="s2"&gt;"[resourceId('Microsoft.Network/privateDnsZones', parameters('privateZoneNames')[copyIndex()])]"&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="nl"&gt;"properties"&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="nl"&gt;"registrationEnabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"virtualNetwork"&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="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[resourceId(parameters('networkResourceGroup'), 'Microsoft.Network/virtualNetworks', parameters('vnetNameFrontend'))]"&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="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
            &lt;/span&gt;&lt;span class="nl"&gt;"copy"&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="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"zonebackendlinkcopy"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"[length(parameters('privateZoneNames'))]"&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="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="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  putting it all together
&lt;/h3&gt;

&lt;p&gt;I placed the create endpoint &amp;amp; maintain DNS entries in a common function (PowerShell module) as it was called for several resources.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="kr"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;Update-PrivateLink&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="kr"&gt;param&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="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$locationCode&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="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$resourceName&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="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$resourceId&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="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="nv"&gt;$groupId&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;# mapping from https://docs.microsoft.com/en-us/azure/private-link/private-endpoint-dns&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="c"&gt;# list of groupIds / sub resources https://docs.microsoft.com/en-us/azure/private-link/private-endpoint-overview#private-link-resource&lt;/span&gt;&lt;span class="w"&gt;

   &lt;/span&gt;&lt;span class="c"&gt;# place DNS entries for global resources in location specific resource group private DNS zone to allow dedicated assignment to local VNETs&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nv"&gt;$dnsZoneResourceGroupName&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="s2"&gt;"network-common"&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="nv"&gt;$linkPrefix&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="nv"&gt;$resourceName&lt;/span&gt;&lt;span class="w"&gt;
   &lt;/span&gt;&lt;span class="kr"&gt;switch&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$groupId&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="c"&gt;# global resources have regional resource groups for private DNS zones -----------&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"registry"&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="nv"&gt;$globalDnsSuffx&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="s2"&gt;".azurecr.io"&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nv"&gt;$dnsZoneName&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="s2"&gt;"privatelink.azurecr.io"&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nv"&gt;$dnsZoneResourceGroupName&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="s2"&gt;"network-"&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="nv"&gt;$locationCode&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nv"&gt;$linkPrefix&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="nv"&gt;$resourceName&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="s2"&gt;"-"&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="nv"&gt;$locationCode&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Sql"&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="nv"&gt;$globalDnsSuffx&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="s2"&gt;".documents.azure.com"&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nv"&gt;$dnsZoneName&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="s2"&gt;"privatelink.documents.azure.com"&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nv"&gt;$dnsZoneResourceGroupName&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="s2"&gt;"network-"&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="nv"&gt;$locationCode&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nv"&gt;$linkPrefix&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="nv"&gt;$resourceName&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="s2"&gt;"-"&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="nv"&gt;$locationCode&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;# regional resources are handled with the common network resource group -----------&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"blob"&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="nv"&gt;$globalDnsSuffx&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="s2"&gt;".blob.core.windows.net"&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nv"&gt;$dnsZoneName&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="s2"&gt;"privatelink.blob.core.windows.net"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"namespace"&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="nv"&gt;$globalDnsSuffx&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="s2"&gt;".servicebus.windows.net"&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nv"&gt;$dnsZoneName&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="s2"&gt;"privatelink.servicebus.windows.net"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"sqlServer"&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="nv"&gt;$globalDnsSuffx&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="s2"&gt;".database.windows.net"&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nv"&gt;$dnsZoneName&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="s2"&gt;"privatelink.database.windows.net"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"vault"&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="nv"&gt;$globalDnsSuffx&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="s2"&gt;"(.vaultcore.azure.net|.vault.azure.net)"&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nv"&gt;$dnsZoneName&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="s2"&gt;"privatelink.vaultcore.azure.net"&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;Default&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="kr"&gt;throw&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"no DNS name mapping defined for groupId:"&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="nv"&gt;$groupId&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="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

   &lt;/span&gt;&lt;span class="n"&gt;subnetId&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="s2"&gt;"{subnet-id-from-another-magic-function}"&lt;/span&gt;&lt;span class="w"&gt;

   &lt;/span&gt;&lt;span class="nv"&gt;$subnet&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;Get-AzVirtualNetworkSubnetConfig&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;-ResourceId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$subnetId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&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;Stop&lt;/span&gt;&lt;span class="w"&gt;

   &lt;/span&gt;&lt;span class="n"&gt;Write-Host&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"update private endpoint+DNS for"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$resourceName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$groupId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"to"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$subnet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Id&lt;/span&gt;&lt;span class="w"&gt;

   &lt;/span&gt;&lt;span class="nv"&gt;$privateLink&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;New-AzPrivateLinkServiceConnection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$linkPrefix&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="s2"&gt;"-link"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nt"&gt;-PrivateLinkServiceId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$resourceId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nt"&gt;-GroupId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$groupId&lt;/span&gt;&lt;span class="w"&gt;

   &lt;/span&gt;&lt;span class="nv"&gt;$privateEndpoint&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;New-AzPrivateEndpoint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ResourceGroupName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dnsZoneResourceGroupName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$linkPrefix&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="s2"&gt;"-endpoint"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nt"&gt;-Location&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$location&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nt"&gt;-Subnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$subnet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nt"&gt;-PrivateLinkServiceConnection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$privateLink&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nt"&gt;-Force&lt;/span&gt;&lt;span class="w"&gt;

   &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Get-AzPrivateEndpoint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$privateEndpoint&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CustomDnsConfigs&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="o"&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="nv"&gt;$recordSetName&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="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Fqdn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-replace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$globalDnsSuffx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;""&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="n"&gt;Get-AzPrivateDnsRecordSet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ResourceGroupName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dnsZoneResourceGroupName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ZoneName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dnsZoneName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$recordSetName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-RecordType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;A&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;Remove-AzPrivateDnsRecordSet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ResourceGroupName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dnsZoneResourceGroupName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ZoneName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dnsZoneName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$recordSetName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-RecordType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;A&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;New-AzPrivateDnsRecordSet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ResourceGroupName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dnsZoneResourceGroupName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ZoneName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$dnsZoneName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$recordSetName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nt"&gt;-PrivateDnsRecord&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;New-AzPrivateDnsRecordConfig&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-IPv4Address&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="bp"&gt;$_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IpAddresses&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
         &lt;/span&gt;&lt;span class="nt"&gt;-RecordType&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;A&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Ttl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;3600&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="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This function is called after creation of each of the resource types.&lt;/p&gt;

&lt;p&gt;The source of &lt;code&gt;resourceId&lt;/code&gt; to be passed to the function varies by resource type. It can be &lt;code&gt;.Id&lt;/code&gt; or &lt;code&gt;.ResourceId&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;groupId&lt;/code&gt; passed in is based on this &lt;a href="https://docs.microsoft.com/en-us/azure/private-link/private-endpoint-overview#private-link-resource"&gt;list&lt;/a&gt; and refers to the private link service &lt;code&gt;-GroupId&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Storage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nv"&gt;$storage&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;Get-AzStorageAccount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ResourceGroupName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$resourceGroupName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$storageName&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="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="nv"&gt;$storage&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;Update-PrivateLink&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-locationCode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$locationCode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nt"&gt;-resourceName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$storageName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-resourceId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$storage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nt"&gt;-groupId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"blob"&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;h3&gt;
  
  
  SQL
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nv"&gt;$sqlServer&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;Get-AzSqlServer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ResourceGroupName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$resourceGroupName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ServerName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$sqlServerName&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="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="nv"&gt;$sqlServer&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;Update-PrivateLink&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-locationCode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$locationCode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nt"&gt;-resourceName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$sqlServerName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-resourceId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$sqlServer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ResourceId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nt"&gt;-groupId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sqlServer"&lt;/span&gt;&lt;span class="w"&gt;

       &lt;/span&gt;&lt;span class="c"&gt;# can only be set after private endpoint is created&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="n"&gt;Set-AzSqlServer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ResourceGroupName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$resourceGroupName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ServerName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$sqlServerName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-PublicNetworkAccess&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Disabled"&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;SQL server resource public network access needs to be enabled when creating the resource and no private endpoints yet defined. It can be switched to disabled after the endpoint creation.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  ServiceBus
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nv"&gt;$sbNamespace&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;Get-AzServiceBusNamespace&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$serviceBusNamespaceName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ResourceGroupName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$resourceGroupName&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="nv"&gt;$sbNamespace&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;Update-PrivateLink&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-locationCode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$locationCode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;-resourceName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$sbNamespace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Name&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="s2"&gt;"-sb"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-resourceId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$sbNamespace&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;`&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nt"&gt;-groupId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"namespace"&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;h3&gt;
  
  
  KeyVault
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nv"&gt;$kv&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;Get-AzKeyVault&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-VaultName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$keyVaultName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ResourceGroupName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$resourceGroupName&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="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="nv"&gt;$kv&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;Update-PrivateLink&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-locationCode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$locationCode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nt"&gt;-resourceName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$keyVaultName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-resourceId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$kv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ResourceId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nt"&gt;-groupId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"vault"&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;h3&gt;
  
  
  CosmosDB
&lt;/h3&gt;

&lt;p&gt;For CosmosDB I iterate through an array with all the relevant global locations (which I used before to setup multi master write regions) so that endpoint and DNS entries are created for and in each region.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nv"&gt;$cosmosDb&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;Get-AzCosmosDBAccount&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ResourceGroupName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$resourceGroupName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$accountName&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="nv"&gt;$cosmosDb&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="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$instanceLocationCode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$instanceLocationCodes&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;Update-PrivateLink&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-locationCode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$instanceLocationCode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;-resourceName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$accountName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-resourceId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$cosmosDb&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;-groupId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sql"&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="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Container Registry
&lt;/h3&gt;

&lt;p&gt;Same goes for the 2nd global resource.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nv"&gt;$acr&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;Get-AzContainerRegistry&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-ResourceGroupName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$resourceGroupName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-Name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$acrName&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="nv"&gt;$acr&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="kr"&gt;foreach&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$instanceLocationCode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kr"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$instanceLocationCodes&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;Update-PrivateLink&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-locationCode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$instanceLocationCode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;-resourceName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$acrName&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-resourceId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$acr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;`
&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nt"&gt;-groupId&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"registry"&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="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;limitation: ACR build does not work out-of-the-box when placed behind a firewall or as in this case in a closed down network - except when using ACR private build agents; as I did not want to have 2 types of build agents to maintain I choose regular Azure DevOps build agents (several Docker containers running on the jump VMs) to build Docker images&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  conclusion
&lt;/h2&gt;

&lt;p&gt;Service endpoints are easier to setup and handle. Private link requires more planning and a higher sophistication in infrastructure automation but with that allows really fine grain control on network access paths.&lt;/p&gt;

&lt;p&gt;Currently I have a &lt;em&gt;mix&lt;/em&gt; of imperative and declarative infrastructure code which I basically do not prefer. This is tributed to the flexible way we want to spin up regions and instances of the infrastructure. Maybe in some other iteration we refactor and reintegrate in the one way or the other.&lt;/p&gt;

&lt;p&gt;Let me know whether this makes sense to you and whether it helped you out in your work.&lt;/p&gt;

&lt;h2&gt;
  
  
  credits
&lt;/h2&gt;

&lt;p&gt;Thanks to &lt;a href="https://dev.to/matttrakker"&gt;my good buddy Matthias&lt;/a&gt; for reviewing.&lt;/p&gt;




&lt;h2&gt;
  
  
  March 2023 addendum - Blob Triggers in container hosted Azure Functions
&lt;/h2&gt;

&lt;p&gt;Having this environment in place for a while we experienced a strange issue:&lt;strong&gt;Azure Functions&lt;/strong&gt; hosted inside containers in above environment, which access storage account over private link, did &lt;strong&gt;not get triggered by blob triggers&lt;/strong&gt;. Another esteemed team mate of mine - Fabian - figured out that in such a case a private link to only the blob endpoint is not sufficient but also a private link+DNS to the blob queue endpoint is required &lt;a href="https://github.com/fabistb/azure-function-blob-trigger-private-endpoint"&gt;see here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>networking</category>
    </item>
  </channel>
</rss>
