<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Exploring Azure, DevOps and Software Development</title>
  
  <subtitle>Welcome to Ricky&#39;s Blog</subtitle>
  <link href="https://clouddev.blog/atom.xml" rel="self"/>
  
  <link href="https://clouddev.blog/"/>
  <updated>2026-03-14T10:20:29.551Z</updated>
  <id>https://clouddev.blog/</id>
  
  <author>
    <name>Ricky Gummadi</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>The Four Types of GitHub Copilot Agents: Local, Background, Cloud, and Sub-Agents Explained</title>
    <link href="https://clouddev.blog/GitHub/Copilot/the-four-types-of-github-copilot-agents-local-background-cloud-and-sub-agents-explained/"/>
    <id>https://clouddev.blog/GitHub/Copilot/the-four-types-of-github-copilot-agents-local-background-cloud-and-sub-agents-explained/</id>
    <published>2026-01-09T11:00:00.000Z</published>
    <updated>2026-03-14T10:20:29.551Z</updated>
    
    <content type="html"><![CDATA[<hr><blockquote><p><strong>🎯 TL;DR: Four Agent Types, Four Different Workflows</strong></p><p>GitHub Copilot in VS Code now supports four distinct agent types, each designed for different workflows and levels of autonomy. <strong>Local Agent</strong> is your interactive coding partner, running in VS Code with full access to all your tools, MCP servers, and three personas (Agent, Plan, Ask). <strong>Coding Agent (Cloud)</strong> runs on GitHub’s cloud infrastructure via Actions runners, works fully autonomously on issues, and creates PRs while you’re away. <strong>Background Agent (Copilot CLI)</strong> runs locally but outside the VS Code process; it survives restarts, supports parallel sessions, and can hand off work to cloud agents with <code>/delegate</code>. <strong>Sub-Agents</strong> are the secret weapon for context management, running as isolated subtasks within a parent agent session, keeping the main agent’s context window clean while handling research, analysis, or parallel tasks.</p><p><strong>Key insight:</strong> If you’re using a 1x premium model like Claude Sonnet 4, sub-agent calls are effectively free, making them the most cost-efficient way to scale complex multi-step workflows without burning through your premium request budget.</p></blockquote><hr><p>GitHub Copilot has evolved far beyond simple code completions. With agent mode in VS Code, developers gained an autonomous coding assistant that could plan, execute, and iterate on complex tasks. But as workflows grew more sophisticated, a single agent type wasn’t enough to cover every scenario, from quick interactive debugging to full autonomous issue resolution that runs while you sleep.</p><p>Today, GitHub Copilot in VS Code supports <strong>four distinct agent types</strong>, each optimized for different workflows, contexts, and levels of autonomy. Understanding when to use each one, and how they interact, is the difference between fighting your tools and having them work seamlessly for you.</p><span id="more"></span><h2 id="The-Four-Agent-Types-at-a-Glance"><a href="#The-Four-Agent-Types-at-a-Glance" class="headerlink" title="The Four Agent Types at a Glance"></a>The Four Agent Types at a Glance</h2><p>Before diving deep into each agent type, here’s a high-level comparison:</p><table><thead><tr><th>Feature</th><th>Local Agent</th><th>Background Agent (Copilot CLI)</th><th>Coding Agent (Cloud)</th><th>Sub-Agent</th></tr></thead><tbody><tr><td><strong>Where it runs</strong></td><td>VS Code (local)</td><td>Local machine (outside VS Code)</td><td>GitHub Actions runner (cloud)</td><td>Within parent agent session</td></tr><tr><td><strong>How to invoke</strong></td><td>Chat view (<code>Ctrl+Alt+I</code>)</td><td>Chat dropdown → “Copilot CLI” or terminal <code>copilot</code></td><td>Chat dropdown → “Cloud”, GitHub Issues, PRs, CLI</td><td>Auto-invoked by parent agent or <code>#runSubAgent</code></td></tr><tr><td><strong>Autonomy</strong></td><td>Interactive</td><td>Autonomous (local)</td><td>Fully autonomous (remote)</td><td>Task-scoped</td></tr><tr><td><strong>Survives VS Code close</strong></td><td>❌ No</td><td>✅ Yes</td><td>✅ Yes</td><td>N&#x2F;A (tied to parent)</td></tr><tr><td><strong>Access to VS Code tools</strong></td><td>✅ All tools, MCP servers, extensions</td><td>⚠️ Built-in tools only (no extension tools)</td><td>⚠️ Limited (cloud environment)</td><td>✅ Inherits parent’s tools</td></tr><tr><td><strong>Model support</strong></td><td>All models + BYOK</td><td>All models + BYOK</td><td>Limited models</td><td>Inherits parent model</td></tr><tr><td><strong>Parallel sessions</strong></td><td>✅ Multiple sessions possible</td><td>✅ Multiple parallel sessions</td><td>✅ Multiple parallel sessions</td><td>✅ Multiple parallel sub-agents</td></tr><tr><td><strong>Creates PRs</strong></td><td>❌ No</td><td>❌ No</td><td>✅ Yes (draft PRs)</td><td>❌ No</td></tr><tr><td><strong>Cost</strong></td><td>Premium requests</td><td>Premium requests</td><td>Premium requests + Actions minutes</td><td>Shares parent’s premium requests</td></tr><tr><td><strong>Context</strong></td><td>Main chat window</td><td>Independent per session</td><td>Issue&#x2F;PR&#x2F;repo context</td><td>Isolated (key benefit!)</td></tr></tbody></table><blockquote><p>💡 <strong>Key concept:</strong> The session type dropdown in VS Code’s Chat view is your primary control for switching between Local, Background (Copilot CLI), and Cloud agents. Sub-agents are invoked programmatically within any session.</p></blockquote><hr><h2 id="1-Local-Agent-Standard-Agent-Mode"><a href="#1-Local-Agent-Standard-Agent-Mode" class="headerlink" title="1. Local Agent (Standard Agent Mode)"></a>1. Local Agent (Standard Agent Mode)</h2><p>The <strong>Local Agent</strong> is the standard interactive agent in VS Code. It’s the workhorse of day-to-day Copilot interactions, running directly in your VS Code instance with full access to your workspace, tools, and extensions.</p><h3 id="How-It-Works"><a href="#How-It-Works" class="headerlink" title="How It Works"></a>How It Works</h3><p>You open it with <code>Ctrl+Alt+I</code> (or <code>Cmd+Shift+I</code> on macOS) and interact with it directly in the Chat view. It can read and edit files, run terminal commands, and leverage any tools you’ve configured.</p><h3 id="Three-Built-In-Personas"><a href="#Three-Built-In-Personas" class="headerlink" title="Three Built-In Personas"></a>Three Built-In Personas</h3><table><thead><tr><th>Persona</th><th>Behavior</th><th>Best For</th></tr></thead><tbody><tr><td><strong>Agent</strong></td><td>Autonomous multi-step coding: plans, executes, iterates, and validates</td><td>Implementing features, fixing bugs, refactoring code</td></tr><tr><td><strong>Plan</strong></td><td>Creates structured implementation plans without making changes</td><td>Understanding scope, architecture planning</td></tr><tr><td><strong>Ask</strong></td><td>Q&amp;A about your codebase: reads code, explains patterns</td><td>Learning a new codebase, understanding existing code</td></tr></tbody></table><h3 id="Full-Tool-Access"><a href="#Full-Tool-Access" class="headerlink" title="Full Tool Access"></a>Full Tool Access</h3><p>The Local Agent’s biggest advantage is <strong>unrestricted access to tools</strong>: built-in tools (file editing, terminal, search, debugging), any configured MCP servers, extension-provided tools, and BYOK models. This makes it the most versatile of all four types.</p><h3 id="Autopilot-Mode"><a href="#Autopilot-Mode" class="headerlink" title="Autopilot Mode"></a>Autopilot Mode</h3><p>For tasks where you want minimal interruption, enable <strong>Autopilot mode</strong>. The agent automatically approves tool calls and continues executing without per-step confirmation.</p><blockquote><p>⚠️ <strong>Use Autopilot mode with caution.</strong> The agent will execute terminal commands and edit files without asking for permission. Always review the results when it’s done.</p></blockquote><h3 id="When-to-Use-the-Local-Agent"><a href="#When-to-Use-the-Local-Agent" class="headerlink" title="When to Use the Local Agent"></a>When to Use the Local Agent</h3><ul><li><strong>Interactive coding sessions</strong> needing back-and-forth conversation</li><li><strong>Tasks requiring extension tools</strong> (test runners, debuggers, specialized MCP servers)</li><li><strong>BYOK model usage</strong> when you need a specific model not available in cloud mode</li><li><strong>Sensitive codebases</strong> where you don’t want code leaving your machine</li><li><strong>Quick fixes</strong> that take a few minutes</li></ul><h3 id="Limitations"><a href="#Limitations" class="headerlink" title="Limitations"></a>Limitations</h3><ul><li><strong>Tied to VS Code:</strong> closing VS Code stops the agent</li><li><strong>Not collaborative:</strong> other team members can’t see or interact with your session</li></ul><hr><h2 id="2-Coding-Agent-Cloud"><a href="#2-Coding-Agent-Cloud" class="headerlink" title="2. Coding Agent (Cloud)"></a>2. Coding Agent (Cloud)</h2><p>The <strong>Coding Agent</strong> is GitHub Copilot’s fully autonomous cloud-based agent. It runs on GitHub’s infrastructure using Actions runners and is designed for tasks that don’t require your active involvement. You can assign it an issue and walk away.</p><h3 id="How-It-Works-1"><a href="#How-It-Works-1" class="headerlink" title="How It Works"></a>How It Works</h3><p>You can invoke it from multiple entry points: VS Code Chat view (“Cloud” session type), GitHub Issues (assign to <code>@copilot</code>), Pull Request comments, GitHub CLI, or external integrations (Jira, Slack, Teams).</p><p>Once triggered, the Coding Agent analyzes the task, reads the repository code, creates a branch with a <code>copilot/</code> prefix, implements changes autonomously, runs security checks (CodeQL, dependency scanning, secret scanning), and creates a <strong>draft Pull Request</strong>.</p><h3 id="Fully-Asynchronous"><a href="#Fully-Asynchronous" class="headerlink" title="Fully Asynchronous"></a>Fully Asynchronous</h3><p>This is the defining characteristic: <strong>you can close your laptop</strong>. The agent runs entirely on GitHub’s cloud infrastructure, so it keeps working whether or not you’re at your desk.</p><blockquote><p>🔒 <strong>Security by default.</strong> The Coding Agent enforces CodeQL analysis, dependency scanning, and secret scanning on every PR it creates. These checks are non-negotiable.</p></blockquote><h3 id="When-to-Use-the-Coding-Agent"><a href="#When-to-Use-the-Coding-Agent" class="headerlink" title="When to Use the Coding Agent"></a>When to Use the Coding Agent</h3><ul><li><strong>Well-defined issues</strong> with clear acceptance criteria</li><li><strong>Tasks you want done while you’re away</strong> (overnight, during meetings)</li><li><strong>Bug fixes with clear reproduction steps</strong></li><li><strong>Boilerplate or repetitive tasks</strong> (CRUD endpoints, adding tests, updating configs)</li><li><strong>Team workflows</strong> where any team member can assign issues to <code>@copilot</code></li></ul><h3 id="Limitations-1"><a href="#Limitations-1" class="headerlink" title="Limitations"></a>Limitations</h3><p>The Coding Agent is scoped to <strong>one repository per task</strong>, produces exactly <strong>one draft PR per task</strong>, cannot access your VS Code extensions, and uses both premium requests and Actions minutes (the most expensive agent type per task). You also can’t guide it mid-task, though you can comment on the PR afterward.</p><p>The Coding Agent is available on Pro, Pro+, Business, and Enterprise plans.</p><hr><h2 id="3-Background-Agent-Copilot-CLI"><a href="#3-Background-Agent-Copilot-CLI" class="headerlink" title="3. Background Agent (Copilot CLI)"></a>3. Background Agent (Copilot CLI)</h2><p>The <strong>Background Agent</strong> bridges the gap between local and cloud agents. It runs on your local machine but <strong>outside the VS Code process</strong>, meaning it survives VS Code restarts and can run multiple sessions in parallel.</p><h3 id="How-It-Works-2"><a href="#How-It-Works-2" class="headerlink" title="How It Works"></a>How It Works</h3><p>The Background Agent is powered by the <strong>Copilot CLI agent harness</strong>. You can invoke it from the Chat view dropdown (“Copilot CLI”), Command Palette, or by typing <code>copilot</code> directly in your terminal.</p><h3 id="Isolation-Modes"><a href="#Isolation-Modes" class="headerlink" title="Isolation Modes"></a>Isolation Modes</h3><table><thead><tr><th>Mode</th><th>Behavior</th><th>Use Case</th></tr></thead><tbody><tr><td><strong>Worktree</strong></td><td>Creates a Git worktree in a separate folder; changes are isolated from your working directory</td><td>Safe experimentation, parallel feature development</td></tr><tr><td><strong>Workspace</strong></td><td>Makes changes directly in your current workspace</td><td>Quick tasks where you want changes applied immediately</td></tr></tbody></table><blockquote><p>💡 <strong>Worktree mode is the safer option.</strong> It creates a separate copy of your repository, so the Background Agent’s changes never interfere with your active work.</p></blockquote><h3 id="Parallel-Sessions"><a href="#Parallel-Sessions" class="headerlink" title="Parallel Sessions"></a>Parallel Sessions</h3><p>Unlike the Local Agent, the Background Agent can run <strong>multiple sessions simultaneously</strong>, each with its own independent context window and worktree. This is powerful for parallelizing work across different tasks.</p><h3 id="The-x2F-delegate-Command-and-Hand-Off"><a href="#The-x2F-delegate-Command-and-Hand-Off" class="headerlink" title="The &#x2F;delegate Command and Hand-Off"></a>The &#x2F;delegate Command and Hand-Off</h3><p>The <code>/delegate</code> command hands off a task to the <strong>Coding Agent (Cloud)</strong>, creating a smooth escalation path: start working locally, realize the task needs full autonomy and a PR, and delegate to the cloud without losing context.</p><table><thead><tr><th>From</th><th>To</th><th>How</th></tr></thead><tbody><tr><td>Local Agent</td><td>Background Agent</td><td>Change session type dropdown to “Copilot CLI”</td></tr><tr><td>Local Agent</td><td>Coding Agent (Cloud)</td><td>Change session type dropdown to “Cloud”</td></tr><tr><td>Background Agent</td><td>Coding Agent (Cloud)</td><td>Enter <code>/delegate</code> in the chat input</td></tr></tbody></table><blockquote><p>⚡ <strong>Conversation history carries over</strong> during hand-offs. The new agent receives the full conversation context so it can continue where the previous agent left off.</p></blockquote><h3 id="Cost-Premium-Requests-Only"><a href="#Cost-Premium-Requests-Only" class="headerlink" title="Cost: Premium Requests Only"></a>Cost: Premium Requests Only</h3><p>The Background Agent uses <strong>only Copilot premium requests</strong>, with no GitHub Actions minutes. This makes it significantly cheaper than the Coding Agent for comparable tasks.</p><h3 id="When-to-Use-the-Background-Agent"><a href="#When-to-Use-the-Background-Agent" class="headerlink" title="When to Use the Background Agent"></a>When to Use the Background Agent</h3><ul><li><strong>Long-running tasks</strong> that you don’t want tied to your VS Code session</li><li><strong>Parallel work</strong> where you need multiple agents on different tasks simultaneously</li><li><strong>Tasks you want to start and check on later</strong></li><li><strong>Exploratory work</strong> using Worktree mode to safely experiment</li><li><strong>Pipeline to cloud</strong>: start locally, get context, then <code>/delegate</code> for PR creation</li></ul><h3 id="Limitations-2"><a href="#Limitations-2" class="headerlink" title="Limitations"></a>Limitations</h3><ul><li><strong>No extension-provided tools:</strong> runs outside the VS Code process, so it can’t access extension tools</li><li><strong>Requires machine to stay running:</strong> survives VS Code restarts but not machine shutdown or sleep</li><li><strong>Local resources only:</strong> uses your machine’s CPU, memory, and network</li></ul><hr><h2 id="4-Sub-Agents-The-Context-Management-Secret-Weapon"><a href="#4-Sub-Agents-The-Context-Management-Secret-Weapon" class="headerlink" title="4. Sub-Agents: The Context Management Secret Weapon"></a>4. Sub-Agents: The Context Management Secret Weapon</h2><p><strong>Sub-Agents</strong> are the most underappreciated feature in the Copilot agent ecosystem. They’re spawned as <strong>isolated subtasks within a parent agent session</strong>, and their primary superpower is <strong>context isolation</strong>.</p><h3 id="The-Context-Problem"><a href="#The-Context-Problem" class="headerlink" title="The Context Problem"></a>The Context Problem</h3><p>Every AI agent has a finite context window. As your conversation grows with file reads, search results, and intermediate reasoning, that window fills up and the agent starts losing important details. For complex multi-step tasks, the research phases can consume so much context that the agent forgets critical details by the time it begins implementation.</p><h3 id="How-Sub-Agents-Solve-This"><a href="#How-Sub-Agents-Solve-This" class="headerlink" title="How Sub-Agents Solve This"></a>How Sub-Agents Solve This</h3><p>Sub-Agents run in a <strong>completely isolated context window</strong>:</p><ol><li>The parent agent spawns a sub-agent with a specific task prompt (the sub-agent does NOT inherit the parent’s conversation history)</li><li>The sub-agent performs all its work (file reads, searches, analysis) in its own isolated context</li><li>It returns <strong>only a final summary&#x2F;result</strong> to the parent; all intermediate data is discarded</li></ol><p>The result: the parent agent gets concise answers without its context window being polluted by hundreds of lines of intermediate data.</p><h3 id="Visual-How-the-runSubAgent-Tool-Works"><a href="#Visual-How-the-runSubAgent-Tool-Works" class="headerlink" title="Visual: How the #runSubAgent Tool Works"></a>Visual: How the #runSubAgent Tool Works</h3><p><img src="/img/github-subagent.png" alt="Diagram showing how sub-agents run in isolated context windows, performing tool calls independently and returning only summaries to the parent agent"></p><p>The image above illustrates the sub-agent flow. When you invoke the <code>#runSubAgent</code> tool, it creates a <strong>separate context window</strong> for the sub-agent. The sub-agent performs all tool calls independently, and only the <strong>final result summary</strong> is passed back to the main agent. The sub-agent appears as a collapsible tool call in the Chat UI.</p><h3 id="Invoking-Sub-Agents"><a href="#Invoking-Sub-Agents" class="headerlink" title="Invoking Sub-Agents"></a>Invoking Sub-Agents</h3><p>Sub-Agents can be triggered in two ways:</p><p><strong>Agent-initiated:</strong> The main agent autonomously decides to spawn a sub-agent when it recognizes a task that would benefit from context isolation.</p><p><strong>User-hinted:</strong> You can explicitly request one:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">You: Run a subagent to research all the authentication patterns used in this </span><br><span class="line">     codebase and give me a summary of the approaches used.</span><br></pre></td></tr></table></figure><p>Or reference the tool directly: <code>#runSubAgent</code></p><h3 id="The-Cost-Advantage"><a href="#The-Cost-Advantage" class="headerlink" title="The Cost Advantage"></a>The Cost Advantage</h3><p>If you’re using a 1x premium model, sub-agent calls are effectively free. You can run dozens of sub-agents in parallel without worrying about your premium request budget.</p><p>Instead of using one expensive model call that reads 50 files and floods context, spawn multiple cheap sub-agents to research different aspects in parallel, collect their concise summaries, then use a single focused model call for the final implementation.</p><h3 id="Orchestration-Pattern-Coordinator-amp-Worker"><a href="#Orchestration-Pattern-Coordinator-amp-Worker" class="headerlink" title="Orchestration Pattern: Coordinator &amp; Worker"></a>Orchestration Pattern: Coordinator &amp; Worker</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Main Agent (Coordinator):</span><br><span class="line">├── SubAgent 1: &quot;Analyze the authentication module and list all auth strategies used&quot;</span><br><span class="line">├── SubAgent 2: &quot;Analyze the database layer and list all query patterns&quot;  </span><br><span class="line">├── SubAgent 3: &quot;Analyze the API layer and list all middleware in the pipeline&quot;</span><br><span class="line">└── Main Agent: Uses summaries from all three to implement a new feature</span><br></pre></td></tr></table></figure><h3 id="What-Sub-Agents-CAN-and-CANNOT-Do"><a href="#What-Sub-Agents-CAN-and-CANNOT-Do" class="headerlink" title="What Sub-Agents CAN and CANNOT Do"></a>What Sub-Agents CAN and CANNOT Do</h3><table><thead><tr><th>✅ Can Do</th><th>❌ Cannot Do</th></tr></thead><tbody><tr><td>Read files in the workspace</td><td>Access the parent agent’s conversation history</td></tr><tr><td>Run terminal commands</td><td>Modify the parent agent’s context</td></tr><tr><td>Search the codebase</td><td>Persist state between invocations</td></tr><tr><td>Use MCP server tools (inherits parent’s tools)</td><td>Run as a standalone session</td></tr><tr><td>Run in parallel with other sub-agents</td><td>Create PRs or branches</td></tr><tr><td>Return structured results to parent</td><td>Directly communicate with the user</td></tr></tbody></table><hr><h2 id="When-to-Use-Which-Agent-Quick-Decision-Matrix"><a href="#When-to-Use-Which-Agent-Quick-Decision-Matrix" class="headerlink" title="When to Use Which Agent: Quick Decision Matrix"></a>When to Use Which Agent: Quick Decision Matrix</h2><table><thead><tr><th>Scenario</th><th>Recommended Agent</th><th>Why</th></tr></thead><tbody><tr><td>Quick bug fix while coding</td><td><strong>Local Agent</strong></td><td>Interactive, immediate feedback, full tool access</td></tr><tr><td>“Explain this code to me”</td><td><strong>Local Agent</strong> (Ask persona)</td><td>Read-only, conversational</td></tr><tr><td>Plan a multi-file refactor</td><td><strong>Local Agent</strong> (Plan persona)</td><td>Creates structured plan before changes</td></tr><tr><td>Long-running test generation</td><td><strong>Background Agent</strong></td><td>Survives VS Code restart, doesn’t block your editor</td></tr><tr><td>Working on 3 features in parallel</td><td><strong>Background Agent</strong></td><td>Multiple parallel sessions with Worktree isolation</td></tr><tr><td>Well-defined GitHub issue</td><td><strong>Coding Agent</strong></td><td>Fully autonomous, creates PR, runs while you’re away</td></tr><tr><td>“Fix this overnight”</td><td><strong>Coding Agent</strong></td><td>Asynchronous cloud execution</td></tr><tr><td>Research codebase patterns</td><td><strong>Sub-Agent</strong></td><td>Context-isolated research</td></tr><tr><td>Complex task with pre-research</td><td><strong>Sub-Agent</strong> → Parent</td><td>Research in sub-agents, implement in main agent</td></tr><tr><td>Start local, realize it needs a PR</td><td><strong>Background</strong> → <code>/delegate</code> → <strong>Cloud</strong></td><td>Seamless hand-off</td></tr></tbody></table><hr><h2 id="Best-Practices"><a href="#Best-Practices" class="headerlink" title="Best Practices"></a>Best Practices</h2><table><thead><tr><th>✅ Do</th><th>❌ Don’t</th></tr></thead><tbody><tr><td>Match agent type to task size: Local for &lt; 5 min, Background for 5-30 min, Coding Agent for 30+ min</td><td>Use the Coding Agent for exploratory work (it creates a PR every time)</td></tr><tr><td>Use sub-agents for research-heavy tasks to keep main context clean</td><td>Ignore context window limits; if the agent starts “forgetting,” offload research to sub-agents</td></tr><tr><td>Default to 1x premium models for sub-agents (research rarely needs the most powerful models)</td><td>Run Coding Agent tasks for multi-repo changes (it’s scoped to a single repo per task)</td></tr><tr><td>Write clear, detailed issue descriptions for the Coding Agent with acceptance criteria and edge cases</td><td>Use a single long agent session for everything; break complex work across agent types instead</td></tr><tr><td>Use the Research-Delegate Pipeline: Ask → Plan → Background + Sub-Agents → <code>/delegate</code> → Cloud PR</td><td></td></tr></tbody></table><hr><h2 id="Key-Takeaways"><a href="#Key-Takeaways" class="headerlink" title="Key Takeaways"></a>Key Takeaways</h2><ul><li><strong>Local Agent</strong> is your interactive coding partner: versatile, full-featured, but tied to your VS Code session</li><li><strong>Coding Agent (Cloud)</strong> is your autonomous worker: fire and forget, creates PRs, runs on GitHub’s infrastructure</li><li><strong>Background Agent (Copilot CLI)</strong> is the bridge: local execution with persistence, parallelism, and cloud delegation</li><li><strong>Sub-Agents</strong> are the context management secret weapon: keep your main agent’s context clean while parallelizing research and analysis</li><li><strong>Cost optimization</strong> is key: use 1x premium models for sub-agents and routine tasks, reserve premium models for complex work</li><li><strong>Hand-off between agents is seamless</strong>: start local, escalate to background, delegate to cloud as needed</li></ul><p>The most effective Copilot users don’t just use one agent type. They orchestrate all four to match the right tool to the right task.</p><hr><h2 id="References"><a href="#References" class="headerlink" title="References"></a>References</h2><ul><li><a href="https://docs.github.com/en/copilot/using-github-copilot/coding-agent">GitHub Copilot Coding Agent</a></li><li><a href="https://docs.github.com/en/copilot/about-github-copilot/subscription-plans-for-github-copilot">GitHub Copilot Plans and Pricing</a></li></ul><hr><p><strong>Image Credits:</strong></p><ul><li>Main image generated by <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/dall-e?view=foundry-classic&tabs=gpt-image-1">GPT-Image-1.5</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🎯 TL;DR: Four Agent Types, Four Different Workflows&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;GitHub Copilot in VS Code now supports four distinct agent types, each designed for different workflows and levels of autonomy. &lt;strong&gt;Local Agent&lt;/strong&gt; is your interactive coding partner, running in VS Code with full access to all your tools, MCP servers, and three personas (Agent, Plan, Ask). &lt;strong&gt;Coding Agent (Cloud)&lt;/strong&gt; runs on GitHub’s cloud infrastructure via Actions runners, works fully autonomously on issues, and creates PRs while you’re away. &lt;strong&gt;Background Agent (Copilot CLI)&lt;/strong&gt; runs locally but outside the VS Code process; it survives restarts, supports parallel sessions, and can hand off work to cloud agents with &lt;code&gt;/delegate&lt;/code&gt;. &lt;strong&gt;Sub-Agents&lt;/strong&gt; are the secret weapon for context management, running as isolated subtasks within a parent agent session, keeping the main agent’s context window clean while handling research, analysis, or parallel tasks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key insight:&lt;/strong&gt; If you’re using a 1x premium model like Claude Sonnet 4, sub-agent calls are effectively free, making them the most cost-efficient way to scale complex multi-step workflows without burning through your premium request budget.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;GitHub Copilot has evolved far beyond simple code completions. With agent mode in VS Code, developers gained an autonomous coding assistant that could plan, execute, and iterate on complex tasks. But as workflows grew more sophisticated, a single agent type wasn’t enough to cover every scenario, from quick interactive debugging to full autonomous issue resolution that runs while you sleep.&lt;/p&gt;
&lt;p&gt;Today, GitHub Copilot in VS Code supports &lt;strong&gt;four distinct agent types&lt;/strong&gt;, each optimized for different workflows, contexts, and levels of autonomy. Understanding when to use each one, and how they interact, is the difference between fighting your tools and having them work seamlessly for you.&lt;/p&gt;</summary>
    
    
    
    <category term="GitHub" scheme="https://clouddev.blog/categories/GitHub/"/>
    
    <category term="Copilot" scheme="https://clouddev.blog/categories/GitHub/Copilot/"/>
    
    
    <category term="AI" scheme="https://clouddev.blog/tags/AI/"/>
    
    <category term="VS Code" scheme="https://clouddev.blog/tags/VS-Code/"/>
    
    <category term="AI Development" scheme="https://clouddev.blog/tags/AI-Development/"/>
    
    <category term="GitHub" scheme="https://clouddev.blog/tags/GitHub/"/>
    
    <category term="GitHub Copilot" scheme="https://clouddev.blog/tags/GitHub-Copilot/"/>
    
  </entry>
  
  <entry>
    <title>Running FLUX.1 OmniControl on a Consumer GPU: A Docker Implementation tested on RTX 3060</title>
    <link href="https://clouddev.blog/AI/LLMs/running-flux-1-omnicontrol-on-a-consumer-gpu-a-docker-implementation-tested-on-rtx-3060/"/>
    <id>https://clouddev.blog/AI/LLMs/running-flux-1-omnicontrol-on-a-consumer-gpu-a-docker-implementation-tested-on-rtx-3060/</id>
    <published>2025-11-10T11:00:00.000Z</published>
    <updated>2026-03-14T04:23:22.833Z</updated>
    
    <content type="html"><![CDATA[<hr><blockquote><p><strong>🎯 TL;DR: Subject-Driven Image Generation on 12GB VRAM</strong></p><p>Large AI models like FLUX.1-schnell typically require datacenter GPUs with 48GB+ VRAM. Problem: Most developers and hobbyists only have access to consumer RTX cards which vary from 6 - 12GB VRAM in most cases (with the exception of the expensive 4090&#x2F;5090 cards which can go up to 32gb). </p><p><strong>Solution:</strong> Using mmgp (Memory Management for GPU Poor) with Docker containerization enables FLUX.1 OmniControl to run on RTX 3060 12GB through 8-bit quantization, dynamic VRAM&#x2F;RAM offloading, and selective layer loading. The implementation provides a Gradio web interface generating 512x512 images in ~10 seconds after initial model loading, with models persisting in system RAM to avoid reload overhead.</p><p><strong>Technical Approach:</strong> Profile 3 configuration quantizes the T5 text encoder (8.8GB → ~4.4GB), pins the FLUX transformer (22.7GB) to reserved system RAM, and dynamically loads only active layers to VRAM during inference. Tested and validated on RTX 3060 12GB with 64GB system RAM running Windows 11 + WSL2 + Docker Desktop.</p><p><strong>Complete Implementation:</strong> All code, Dockerfile, and setup instructions are available at <a href="https://github.com/Ricky-G/docker-ai-models/tree/main/omnicontrol">github.com&#x2F;Ricky-G&#x2F;docker-ai-models&#x2F;omnicontrol</a></p></blockquote><hr><p>Recently, I wanted to experiment with OmniControl, a subject-driven image generation model that extends FLUX.1-schnell with LoRA adapters for precise control over object placement. The challenge? The model requirements listed 48GB+ VRAM, and I only had an RTX 3060 with 12GB sitting in my workstation.</p><p>This is a common frustration in the AI development community. Research papers showcase impressive results on expensive datacenter hardware, but practical implementation on consumer GPUs requires significant engineering effort. Could I actually run this model locally without upgrading to an RTX 4090&#x2F;5090 or pay for a VM in Azure with A100?</p><p>The answer turned out to be yes - with some clever memory management and containerization. This blog post walks through the complete process of dockerizing OmniControl to run efficiently on a 12GB consumer GPU.</p><span id="more"></span><h2 id="What-is-FLUX-1-OmniControl"><a href="#What-is-FLUX-1-OmniControl" class="headerlink" title="What is FLUX.1 OmniControl?"></a>What is FLUX.1 OmniControl?</h2><p>Before diving into the technical implementation, let’s understand what we’re working with. <strong>FLUX.1 OmniControl</strong> is a subject-driven image generation model that extends the base FLUX.1-schnell diffusion model with LoRA (Low-Rank Adaptation) adapters for precise control over object placement and composition.</p><p>Unlike traditional text-to-image models where you only provide a text prompt, OmniControl allows you to:</p><ul><li><strong>Subject Consistency:</strong> Provide a reference image of a specific object (like a toy, person, or product) and have it accurately reproduced in generated images</li><li><strong>Spatial Control:</strong> Specify exactly where in the scene you want objects placed</li><li><strong>Style Preservation:</strong> Maintain the visual characteristics of the reference object across different contexts and environments</li></ul><p>Think of it as “Photoshop + AI” - you can place your specific objects into any scene you can describe with text. This makes it incredibly powerful for product visualization, creative content generation, and prototyping visual concepts.</p><p>The trade-off? The model is <strong>massive</strong> - requiring over 30GB of model weights to achieve this level of control and quality. This is where the engineering challenge begins.</p><h2 id="The-Challenge-Model-Size-vs-Available-VRAM"><a href="#The-Challenge-Model-Size-vs-Available-VRAM" class="headerlink" title="The Challenge: Model Size vs Available VRAM"></a>The Challenge: Model Size vs Available VRAM</h2><p>Let’s start with the hard numbers:</p><p><strong>FLUX.1-schnell model components:</strong></p><ul><li>Transformer: 22.7GB (torch.bfloat16)</li><li>T5 Text Encoder: 8.8GB</li><li>CLIP Text Encoder: 162MB</li><li>VAE: ~1GB</li><li>OminiControl LoRA: 200MB</li><li><strong>Total</strong>: ~32.8GB of model weights</li></ul><p><strong>Available hardware:</strong><br>I am constrained by my existing workstation specs:</p><ul><li>RTX 3060: 12GB VRAM</li><li>System RAM: 64GB DDR4</li><li>Storage: 1TB NVMe SSD + 2TB HDD</li><li>CPU: Intel i7-11700K</li><li>OS: Windows 11 + WSL2 (Ubuntu 22.04)</li><li>Docker Desktop with NVIDIA Container Toolkit</li></ul><p>The gap is obvious - we need nearly 3x more VRAM than the GPU provides. Traditional approaches like FP16 precision or model pruning weren’t going to cut it. We needed something more aggressive.</p><h2 id="Understanding-mmgp-Memory-Management-for-GPU-Poor"><a href="#Understanding-mmgp-Memory-Management-for-GPU-Poor" class="headerlink" title="Understanding mmgp: Memory Management for GPU Poor"></a>Understanding mmgp: Memory Management for GPU Poor</h2><p>The key enabler for this project is <a href="https://pypi.org/project/mmgp/">mmgp</a> (Memory Management for GPU Poor), a Python library specifically designed to run large models on consumer hardware. Here’s how it works:</p><h3 id="8-Bit-Quantization"><a href="#8-Bit-Quantization" class="headerlink" title="8-Bit Quantization"></a>8-Bit Quantization</h3><p>mmgp uses <code>quanto</code> to quantize large model components from 16-bit to 8-bit precision:</p><ul><li>T5 encoder: 8.8GB → ~4.4GB (50% reduction)</li><li>Quality impact: Minimal for text encoding tasks</li><li>Speed impact: Slight increase in encoding time (~10-15%)</li></ul><h3 id="Dynamic-VRAM-x2F-RAM-Offloading"><a href="#Dynamic-VRAM-x2F-RAM-Offloading" class="headerlink" title="Dynamic VRAM&#x2F;RAM Offloading"></a>Dynamic VRAM&#x2F;RAM Offloading</h3><p>Instead of keeping all model weights in VRAM, mmgp maintains a “working set”:</p><ul><li>Critical layers: Loaded to VRAM during active use</li><li>Inactive layers: Offloaded to pinned system RAM</li><li>Transfers: Handled automatically during forward passes</li></ul><h3 id="RAM-Pinning-Strategy"><a href="#RAM-Pinning-Strategy" class="headerlink" title="RAM Pinning Strategy"></a>RAM Pinning Strategy</h3><p>Models are loaded once from disk to system RAM (one-time cost), then:</p><ul><li>Pinned memory allocation: 75% of system RAM reserved (48GB in my case)</li><li>Fast transfers: Pinned RAM → VRAM takes ~200ms for 1GB</li><li>Persistent storage: Models stay in RAM across generations</li></ul><h3 id="Profile-System"><a href="#Profile-System" class="headerlink" title="Profile System"></a>Profile System</h3><p>mmgp provides 5 preconfigured profiles:</p><table><thead><tr><th>Profile</th><th>Target VRAM</th><th>Strategy</th><th>Use Case</th></tr></thead><tbody><tr><td>1</td><td>16-24GB</td><td>Full model in VRAM</td><td>Maximum speed</td></tr><tr><td>2</td><td>12-16GB</td><td>Partial VRAM + RAM</td><td>Balanced</td></tr><tr><td><strong>3</strong></td><td><strong>12GB</strong></td><td><strong>Quantization + pinning</strong></td><td><strong>RTX 3060 sweet spot</strong></td></tr><tr><td>4</td><td>8-12GB</td><td>Aggressive quantization</td><td>Lower-end cards</td></tr><tr><td>5</td><td>6-8GB</td><td>Minimal VRAM usage</td><td>GPU Poor mode</td></tr></tbody></table><p>For RTX 3060, Profile 3 provides the best balance between speed and stability.</p><h2 id="Prerequisites-What-You’ll-Need"><a href="#Prerequisites-What-You’ll-Need" class="headerlink" title="Prerequisites: What You’ll Need"></a>Prerequisites: What You’ll Need</h2><p>Before starting the implementation, ensure you have the following components set up:</p><h3 id="Hardware-Requirements"><a href="#Hardware-Requirements" class="headerlink" title="Hardware Requirements"></a>Hardware Requirements</h3><p><strong>Minimum Configuration:</strong></p><ul><li>NVIDIA GPU: 12GB VRAM (RTX 3060, 3060 Ti, or better)</li><li>System RAM: 64GB DDR4&#x2F;DDR5 (48GB will be pinned for model storage)</li><li>Storage: 50GB free space (35GB for models + overhead)</li><li>CPU: Any modern multi-core processor</li></ul><p><strong>Recommended Configuration:</strong></p><ul><li>GPU: RTX 3060 12GB or RTX 4060 Ti 16GB</li><li>RAM: 64GB or more</li><li>Storage: NVMe SSD for faster startup times (HDD works but adds 2-3 min to load times)</li></ul><h3 id="Software-Requirements"><a href="#Software-Requirements" class="headerlink" title="Software Requirements"></a>Software Requirements</h3><p><strong>Windows Users:</strong></p><ul><li>Windows 11 (Windows 10 with WSL2 also works)</li><li>WSL2 installed and configured</li><li>Docker Desktop for Windows (latest version)</li><li>NVIDIA Container Toolkit (installed via Docker Desktop)</li></ul><p><strong>Linux Users:</strong></p><ul><li>Ubuntu 22.04 or similar distribution</li><li>Docker Engine (latest version)</li><li>NVIDIA Container Toolkit</li><li>NVIDIA drivers (version 525+)</li></ul><h3 id="Account-Requirements"><a href="#Account-Requirements" class="headerlink" title="Account Requirements"></a>Account Requirements</h3><ul><li><strong>HuggingFace Account:</strong> Required to download models</li><li><strong>HuggingFace Token:</strong> Generate a read-access token at <a href="https://huggingface.co/settings/tokens">huggingface.co&#x2F;settings&#x2F;tokens</a></li></ul><h3 id="Verification-Steps"><a href="#Verification-Steps" class="headerlink" title="Verification Steps"></a>Verification Steps</h3><p>Before proceeding, verify your setup:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Check GPU availability</span></span><br><span class="line">nvidia-smi</span><br><span class="line"></span><br><span class="line"><span class="comment"># Verify Docker installation</span></span><br><span class="line">docker --version</span><br><span class="line"></span><br><span class="line"><span class="comment"># Test GPU access in Docker</span></span><br><span class="line">docker run --<span class="built_in">rm</span> --gpus all nvidia/cuda:12.1.1-base-ubuntu22.04 nvidia-smi</span><br></pre></td></tr></table></figure><p>If all commands execute successfully, you’re ready to begin!</p><h2 id="Docker-Architecture-Why-Containerization"><a href="#Docker-Architecture-Why-Containerization" class="headerlink" title="Docker Architecture: Why Containerization?"></a>Docker Architecture: Why Containerization?</h2><p>With prerequisites confirmed, let’s talk about <strong>why Docker</strong> is the right choice for this project. Running large AI models involves complex dependency chains - specific versions of PyTorch, CUDA libraries, Python packages, and system libraries that can conflict with your existing environment.</p><blockquote><p><strong>💡 Want to Skip Ahead?</strong></p><p>The complete Docker implementation, including the Dockerfile, all Python code, and deployment scripts, is available in my GitHub repository: <strong><a href="https://github.com/Ricky-G/docker-ai-models/tree/main/omnicontrol">docker-ai-models&#x2F;omnicontrol</a></strong></p><p>You can clone and run it immediately, or continue reading to understand how it works under the hood.</p></blockquote><p>Containerization solves this by:</p><ul><li><strong>Isolating dependencies</strong> from your host system</li><li><strong>Ensuring reproducibility</strong> across different machines</li><li><strong>Simplifying deployment</strong> - one command to run the entire stack</li><li><strong>Enabling version control</strong> of the entire environment</li></ul><p>The containerization approach provides several additional benefits:</p><ul><li>Eliminates dependency conflicts</li><li>Ensures reproducible builds</li><li>Simplifies deployment across machines</li><li>Isolates model storage from application code</li></ul><h3 id="Container-Structure"><a href="#Container-Structure" class="headerlink" title="Container Structure"></a>Container Structure</h3><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">nvidia/cuda:12.1.1-cudnn8-runtime-ubuntu22.04</span><br><span class="line">├── Python 3.10 + CUDA libraries</span><br><span class="line">├── PyTorch 2.0 with CUDA support</span><br><span class="line">├── Diffusers + Transformers</span><br><span class="line">├── mmgp for memory management</span><br><span class="line">├── Gradio for web interface</span><br><span class="line">└── Custom FLUX integration code</span><br></pre></td></tr></table></figure><h3 id="Volume-Mounts"><a href="#Volume-Mounts" class="headerlink" title="Volume Mounts"></a>Volume Mounts</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">-v D:\_Models\omnicontrol:/app/models    <span class="comment"># Persistent model storage (34GB)</span></span><br></pre></td></tr></table></figure><p>Models download once to the host system and persist across container rebuilds. This is critical for development iteration - rebuilding the container doesn’t trigger 30-minute model downloads.</p><h3 id="GPU-Access"><a href="#GPU-Access" class="headerlink" title="GPU Access"></a>GPU Access</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">--gpus all    <span class="comment"># Exposes all NVIDIA GPUs to container</span></span><br></pre></td></tr></table></figure><p>Docker Desktop + NVIDIA Container Toolkit handles GPU passthrough automatically on Windows via WSL2.</p><h2 id="Implementation-Details-From-Code-to-Running-System"><a href="#Implementation-Details-From-Code-to-Running-System" class="headerlink" title="Implementation Details: From Code to Running System"></a>Implementation Details: From Code to Running System</h2><p>Now that we understand the architecture and tools, let’s dive into how everything works in practice. This section covers the actual startup sequence, performance characteristics, and a critical optimization that makes this entire approach viable.</p><blockquote><p><strong>⚡ Key Performance Insight Ahead</strong></p><p>One of the biggest challenges in this implementation was preventing VRAM from being cleared after each generation, which would cause 80+ second reload times. The solution? A single line of code change that reduced subsequent generation times from 120s to 10s. We’ll cover this critical fix in detail below.</p></blockquote><h3 id="Startup-Sequence"><a href="#Startup-Sequence" class="headerlink" title="Startup Sequence"></a>Startup Sequence</h3><p>The container initialization follows this sequence:</p><p><strong>1. GPU Detection</strong> (~1 second)</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">nvidia-smi --query-gpu=name,memory.total --format=csv</span><br><span class="line"><span class="comment"># Output: NVIDIA GeForce RTX 3060, 12288 MiB</span></span><br></pre></td></tr></table></figure><p><strong>2. Profile Selection</strong> (automatic)</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">vram = get_gpu_memory()</span><br><span class="line"><span class="keyword">if</span> vram &gt;= <span class="number">11000</span>:</span><br><span class="line">    profile = <span class="number">3</span>  <span class="comment"># 12GB optimized</span></span><br></pre></td></tr></table></figure><p><strong>3. Model Loading</strong> (2-3 minutes from HDD)</p><ul><li>FLUX.1-schnell: Downloads from HuggingFace (~22.7GB)</li><li>OminiControl LoRA: Downloads adapter weights (~200MB)</li><li>Loads to CPU first, then applies mmgp profiling</li></ul><p><strong>4. mmgp Profiling</strong> (1-2 minutes)</p><ul><li>Quantizes T5 encoder to 8-bit</li><li>Allocates 48GB pinned RAM (75% of 64GB)</li><li>Hooks model layers for dynamic offloading</li></ul><p><strong>5. Gradio Launch</strong> (~5 seconds)</p><ul><li>Web interface starts on port 7860</li><li>Ready to accept generation requests</li></ul><p><strong>Total first-run time:</strong> 5-10 minutes (mostly downloading models)<br><strong>Subsequent runs:</strong> ~3 minutes (loading from disk to RAM)</p><h3 id="Generation-Performance"><a href="#Generation-Performance" class="headerlink" title="Generation Performance"></a>Generation Performance</h3><p><strong>First generation after startup:</strong></p><ul><li>Time: ~110 seconds</li><li>Breakdown:<ul><li>VRAM loading: 80 seconds (22.7GB from RAM → VRAM)</li><li>Actual inference: 30 seconds (8 steps @ 512x512)</li></ul></li><li>GPU memory: Climbs from 3GB → 10-12GB</li></ul><p><strong>Subsequent generations:</strong></p><ul><li>Time: ~10 seconds (target achieved!)</li><li>VRAM stays at 10-12GB between generations</li><li>No reload overhead</li></ul><h3 id="Critical-Fix-Preventing-VRAM-Clearing"><a href="#Critical-Fix-Preventing-VRAM-Clearing" class="headerlink" title="Critical Fix: Preventing VRAM Clearing"></a>Critical Fix: Preventing VRAM Clearing</h3><blockquote><p><strong>🚨 This Section Contains The Key Optimization</strong></p><p>Initial testing revealed a major performance bottleneck that would have made this entire approach impractical. Understanding and fixing this issue is critical for achieving acceptable performance.</p></blockquote><p><strong>The Problem:</strong></p><p>During initial testing, the first image generation took about 110 seconds (expected), but <strong>every subsequent generation also took 110+ seconds</strong>. Monitoring GPU memory usage revealed the issue:</p><ul><li>After generation completes: VRAM drops from 10-12GB back to 3GB</li><li>Next generation starts: 80 seconds spent reloading models from RAM to VRAM</li><li>Inference runs: 30 seconds of actual generation</li><li><strong>Total: 110 seconds per image, no matter how many you generate</strong></li></ul><p>This made the system unusable for practical work - imagine waiting nearly 2 minutes for every single image!</p><p><strong>The Root Cause:</strong></p><p>Diagnosis revealed that FLUX’s generation code was calling <code>maybe_free_model_hooks()</code> after every inference pass. This function is designed to free memory for systems running multiple models or tight memory scenarios, but in our case where we want to generate multiple images in sequence, it was counterproductive.</p><p>The culprit was in <code>src/flux/generate.py</code>:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># BEFORE (problematic)</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">generate</span>():</span><br><span class="line">    <span class="comment"># ... generation code ...</span></span><br><span class="line">    self.maybe_free_model_hooks()  <span class="comment"># ❌ Unloads everything from VRAM!</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># AFTER (fixed)</span></span><br><span class="line"><span class="keyword">def</span> <span class="title function_">generate</span>():</span><br><span class="line">    <span class="comment"># ... generation code ...</span></span><br><span class="line">    <span class="comment"># DISABLED: Keep models in VRAM between generations</span></span><br><span class="line">    <span class="comment"># self.maybe_free_model_hooks()  # ✅ Models stay loaded</span></span><br></pre></td></tr></table></figure><p><strong>The Impact:</strong></p><p>This single line change transformed the performance profile:</p><table><thead><tr><th>Metric</th><th>Before Fix</th><th>After Fix</th><th>Improvement</th></tr></thead><tbody><tr><td>First generation</td><td>110s</td><td>110s</td><td>(same)</td></tr><tr><td>Second generation</td><td>110s</td><td><strong>10s</strong></td><td><strong>11x faster</strong></td></tr><tr><td>Third generation</td><td>110s</td><td><strong>10s</strong></td><td><strong>11x faster</strong></td></tr><tr><td>VRAM after gen</td><td>3GB</td><td>10-12GB</td><td>(persistent)</td></tr></tbody></table><p>Suddenly, generating 10 images went from 18 minutes to just 2 minutes (110s + 9 × 10s). This made the difference between “technically possible but impractical” and “actually usable for real work.”</p><p><strong>📂 See the Implementation:</strong></p><p>The complete modified FLUX generation code with this optimization is available in the GitHub repository at <a href="https://github.com/Ricky-G/docker-ai-models/blob/main/omnicontrol/src/flux/generate.py"><code>src/flux/generate.py</code></a>. You can see exactly how the model loading and generation pipeline is structured, along with all the mmgp integration code.</p><h2 id="Real-World-Testing-Results"><a href="#Real-World-Testing-Results" class="headerlink" title="Real-World Testing Results"></a>Real-World Testing Results</h2><h3 id="Test-Configuration"><a href="#Test-Configuration" class="headerlink" title="Test Configuration"></a>Test Configuration</h3><p><strong>Hardware:</strong> RTX 3060 12GB, Intel i7-11700K, 64GB DDR4, Micron NVMe main drive, 7200RPM HDD secondary</p><p><strong>OS:</strong> Windows 11 + WSL2 (Ubuntu 22.04)</p><p><strong>Docker:</strong> Desktop 4.28 with NVIDIA Container Toolkit</p><p><strong>Model:</strong> FLUX.1-schnell + OminiControl subject_512.safetensors</p><p><strong>Settings:</strong> 8 inference steps, 512x512 resolution</p><h3 id="Generation-Tests"><a href="#Generation-Tests" class="headerlink" title="Generation Tests"></a>Generation Tests</h3><p><strong>Test 1: Cold start</strong></p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Prompt: &quot;A film photography shot. This item is placed on a wooden desk </span><br><span class="line">         in a cozy study room. Warm afternoon sunlight streams through </span><br><span class="line">         the window.&quot;</span><br><span class="line">Subject: Toy robot figure</span><br><span class="line">Time: 108 seconds</span><br><span class="line">Quality: Excellent, subject preserved with accurate placement</span><br></pre></td></tr></table></figure><p><strong>Test 2: Immediate follow-up</strong></p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Prompt: &quot;On Christmas evening, on a crowded sidewalk, this item sits </span><br><span class="line">         covered in snow wearing a Santa hat.&quot;</span><br><span class="line">Subject: Same toy robot</span><br><span class="line">Time: 11 seconds</span><br><span class="line">Quality: Excellent, consistent subject representation</span><br></pre></td></tr></table></figure><p><strong>Test 3: Third generation</strong></p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">Prompt: &quot;Underwater photography. This item sits on a coral reef with </span><br><span class="line">         tropical fish swimming around it.&quot;</span><br><span class="line">Subject: Same toy robot</span><br><span class="line">Time: 10 seconds</span><br><span class="line">Quality: Good, some water distortion artifacts (expected)</span><br></pre></td></tr></table></figure><h3 id="Resource-Monitoring"><a href="#Resource-Monitoring" class="headerlink" title="Resource Monitoring"></a>Resource Monitoring</h3><p>During active generation:</p><ul><li>GPU Utilization: 95-100%</li><li>VRAM Usage: 10.2GB &#x2F; 12GB (85%)</li><li>System RAM: 52GB &#x2F; 64GB (model pinning)</li><li>CPU Usage: 15-20% (mainly data preprocessing)</li><li>Power Draw: 170W (RTX 3060 TDP)</li></ul><h3 id="Storage-Impact"><a href="#Storage-Impact" class="headerlink" title="Storage Impact"></a>Storage Impact</h3><p>HDD vs SSD comparison (estimated):</p><ul><li><strong>HDD</strong>: 2-3 minutes initial load from disk</li><li><strong>SSD</strong>: 30-45 seconds initial load (2.5x faster)</li><li><strong>During generation</strong>: No difference (models in RAM)</li></ul><p>Recommendation: SSD for faster startup, but not required for generation performance.</p><h2 id="Lessons-Learned-What-Works-and-What-Doesn’t"><a href="#Lessons-Learned-What-Works-and-What-Doesn’t" class="headerlink" title="Lessons Learned: What Works and What Doesn’t"></a>Lessons Learned: What Works and What Doesn’t</h2><p>After extensive testing and iteration, here are the key insights organized by category. These lessons can save you hours of troubleshooting if you’re implementing something similar.</p><h3 id="Memory-Management-Insights"><a href="#Memory-Management-Insights" class="headerlink" title="Memory Management Insights"></a>Memory Management Insights</h3><p><strong>✅ Profile 3 is the Sweet Spot for 12GB Cards</strong></p><p>Tested all five mmgp profiles extensively. Profile 3 provides the perfect balance:</p><ul><li>Stable VRAM usage at 85% capacity (10.2GB &#x2F; 12GB)</li><li>Fast inference times (10s per image)</li><li>No OOM errors or crashes across 100+ test generations</li></ul><p>Profiles 1-2 required more VRAM than available, Profiles 4-5 were unnecessarily slow.</p><p><strong>✅ RAM Pinning Eliminates the Disk Bottleneck</strong></p><p>The 75% RAM allocation strategy (48GB pinned) was crucial:</p><ul><li>First load: 2-3 minutes from HDD to RAM (one-time cost)</li><li>Subsequent loads: &lt;5 seconds from pinned RAM to VRAM</li><li>Models persist across generations with zero disk I&#x2F;O</li></ul><p>Without pinning, every generation would require disk access - absolutely impractical.</p><p><strong>⚠️ WSL2 Memory Limits Are Deceiving</strong></p><p>Initial attempts with default WSL2 settings failed. The issue:</p><ul><li>Host system: 64GB RAM available</li><li>WSL2 container: Only sees ~31GB (50% default limit)</li><li>mmgp profile calculation: Incorrectly assumes full RAM available</li></ul><p><strong>Solution:</strong> Explicitly configure <code>.wslconfig</code> to allocate more memory to WSL2, or force mmgp to use <code>perc_reserved_mem_max=0.75</code> parameter.</p><p><strong>❌ Auto-Offloading Strategies Don’t Work Well</strong></p><p>Tried mmgp’s <code>offloadAfterEveryCall</code> feature - it caused frequent crashes:</p><ul><li>Unpredictable VRAM usage patterns</li><li>Race conditions between loading&#x2F;offloading</li><li>No performance benefit over persistent loading</li></ul><p>Lesson: For sequential generation workloads, keep models loaded.</p><h3 id="Storage-and-I-x2F-O-Optimization"><a href="#Storage-and-I-x2F-O-Optimization" class="headerlink" title="Storage and I&#x2F;O Optimization"></a>Storage and I&#x2F;O Optimization</h3><p><strong>📊 HDD vs SSD Impact Analysis</strong></p><table><thead><tr><th>Phase</th><th>HDD</th><th>SSD</th><th>Impact</th></tr></thead><tbody><tr><td>Initial model download</td><td>5-10 min</td><td>5-10 min</td><td>Network-bound</td></tr><tr><td>First load (disk → RAM)</td><td>2-3 min</td><td>30-45 sec</td><td><strong>4x faster</strong></td></tr><tr><td>RAM → VRAM transfer</td><td>200ms&#x2F;GB</td><td>200ms&#x2F;GB</td><td>RAM speed</td></tr><tr><td>During generation</td><td>0 disk I&#x2F;O</td><td>0 disk I&#x2F;O</td><td>No difference</td></tr></tbody></table><p><strong>Key Insight:</strong> SSD only matters for startup time. Once models are in RAM, storage speed is irrelevant. If you’re doing many generations in one session, HDD is perfectly acceptable.</p><p><strong>💾 Volume Mount Strategy Was Critical</strong></p><p>Storing models on the host filesystem (<code>-v D:\_Models:/app/models</code>) provided:</p><ul><li>Persistence across container rebuilds</li><li>Ability to share models between different containers</li><li>Easy backup and version management</li><li>No re-downloading during development iterations</li></ul><p>Without this, every code change would require re-downloading 35GB of models.</p><h3 id="Configuration-and-Deployment"><a href="#Configuration-and-Deployment" class="headerlink" title="Configuration and Deployment"></a>Configuration and Deployment</h3><p><strong>✅ Gradio Provided Zero-Effort UI</strong></p><p>Using Gradio for the web interface was brilliant:</p><ul><li>20 lines of Python for complete web UI</li><li>Automatic file upload handling</li><li>Built-in image preview and download</li><li>No frontend development required</li></ul><p>Alternative approaches (Flask, React frontend) would have taken days vs hours.</p><p><strong>✅ Docker Isolated the Complexity</strong></p><p>Containerization proved invaluable:</p><ul><li>No conflicts with host Python environment</li><li>Reproducible across machines (tested on 3 different PCs)</li><li>Easy version control of entire stack</li><li>Simple deployment (<code>docker run</code> and done)</li></ul><p><strong>❌ Profile 1 Caused Out-of-Memory Errors</strong></p><p>Attempted to use Profile 1 (full model in VRAM) for maximum speed:</p><ul><li>Required 16GB+ VRAM</li><li>RTX 3060’s 12GB couldn’t handle it</li><li>Resulted in CUDA OOM errors mid-generation</li></ul><p>Lesson: Always profile your actual available memory, not theoretical specs.</p><h3 id="Quality-and-Performance-Trade-offs"><a href="#Quality-and-Performance-Trade-offs" class="headerlink" title="Quality and Performance Trade-offs"></a>Quality and Performance Trade-offs</h3><p><strong>✅ 8-Bit Quantization Had Minimal Quality Impact</strong></p><p>Side-by-side comparison of T5 encoder outputs:</p><ul><li>FP16 (original): Baseline quality</li><li>INT8 (quantized): &lt;5% subjective quality difference</li><li>Memory savings: 8.8GB → 4.4GB (50% reduction)</li></ul><p><strong>Conclusion:</strong> For text encoding tasks, 8-bit quantization is essentially free VRAM.</p><p><strong>📈 Generation Speed Met Targets</strong></p><table><thead><tr><th>Goal</th><th>Result</th><th>Status</th></tr></thead><tbody><tr><td>First gen &lt; 2 min</td><td>110s</td><td>✅ Achieved</td></tr><tr><td>Subsequent gen &lt; 15s</td><td>10s</td><td>✅ Exceeded</td></tr><tr><td>VRAM stable</td><td>10-12GB consistent</td><td>✅ Achieved</td></tr><tr><td>Quality acceptable</td><td>Excellent outputs</td><td>✅ Achieved</td></tr></tbody></table><p>The 10-second generation time makes this practical for real creative work.</p><h2 id="Production-Considerations"><a href="#Production-Considerations" class="headerlink" title="Production Considerations"></a>Production Considerations</h2><h3 id="For-Personal-Use"><a href="#For-Personal-Use" class="headerlink" title="For Personal Use"></a>For Personal Use</h3><p>This setup works great for:</p><ul><li>Hobbyist AI experimentation</li><li>Content creation (social media, art projects)</li><li>Proof-of-concept development</li><li>Learning FLUX.1 architecture</li></ul><h3 id="For-Commercial-Use"><a href="#For-Commercial-Use" class="headerlink" title="For Commercial Use"></a>For Commercial Use</h3><p>Consider these factors:</p><ul><li>Generation time: 10s&#x2F;image × 1000 images &#x3D; 2.8 hours</li><li>Scalability: Single GPU, no batch processing</li><li>Reliability: Consumer GPU thermal throttling under sustained load</li><li>Support: mmgp is community-maintained, not enterprise-supported</li></ul><p>For production workloads, consider:</p><ul><li>Cloud GPUs (Azure N-Series VMs or Azure Container Apps with GPU nodes) - minimum A40&#x2F;A100</li><li>Local GPU upgrade to RTX 4090 or A6000</li><li>Batch processing optimizations</li><li>Multiple parallel containers</li></ul><h2 id="Docker-Hub-amp-Repository"><a href="#Docker-Hub-amp-Repository" class="headerlink" title="Docker Hub &amp; Repository"></a>Docker Hub &amp; Repository</h2><p>The complete implementation is available:</p><ul><li>GitHub: <a href="https://github.com/Ricky-G/docker-ai-models/tree/main/omnicontrol">docker-ai-models&#x2F;omnicontrol</a></li><li>README: Full setup instructions and troubleshooting</li><li>Dockerfile: Production-ready container definition</li><li>Source code: Custom FLUX integration with mmgp</li></ul><h3 id="Quick-Start"><a href="#Quick-Start" class="headerlink" title="Quick Start"></a>Quick Start</h3><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Clone repository</span></span><br><span class="line">git <span class="built_in">clone</span> https://github.com/Ricky-G/docker-ai-models.git</span><br><span class="line"><span class="built_in">cd</span> docker-ai-models/omnicontrol</span><br><span class="line"></span><br><span class="line"><span class="comment"># Build container</span></span><br><span class="line">docker build -t omnicontrol .</span><br><span class="line"></span><br><span class="line"><span class="comment"># Run with HuggingFace token</span></span><br><span class="line">docker run -d --gpus all --name omnicontrol \</span><br><span class="line">  -p 7860:7860 \</span><br><span class="line">  -v D:\_Models\omnicontrol:/app/models \</span><br><span class="line">  -e HF_TOKEN=your_token_here \</span><br><span class="line">  omnicontrol</span><br><span class="line"></span><br><span class="line"><span class="comment"># Access web interface</span></span><br><span class="line"><span class="comment"># http://localhost:7860</span></span><br></pre></td></tr></table></figure><h2 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h2><p>Running FLUX.1 OmniControl on a 12GB RTX 3060 is not only possible but practical. Through careful memory management with mmgp, strategic quantization, and container optimization, we achieved:</p><ul><li>✅ 10-second generation times (after initial load)</li><li>✅ Stable VRAM usage across multiple generations</li><li>✅ No quality degradation from quantization</li><li>✅ Reproducible Docker deployment</li></ul><p>The key insight: <strong>Memory management is more important than raw VRAM capacity</strong>. With the right tools and configuration, consumer GPUs can run models designed for datacenter hardware.</p><p>If you have an RTX 3060 (or similar 12GB card) collecting dust because you thought it couldn’t handle modern AI models, give this approach a try. The democratization of AI isn’t just about open-source models - it’s about making them runnable on hardware people actually own.</p><hr><p><strong>Hardware tested:</strong> RTX 3060 12GB, 64GB RAM, Windows 11 + WSL2</p><p><strong>Software stack:</strong> Docker Desktop, NVIDIA Container Toolkit, mmgp 3.6.9</p><p><strong>Model:</strong> FLUX.1-schnell + OminiControl LoRA</p><p><strong>Performance:</strong> 10s per 512x512 image (8 steps)</p><hr><p><strong>References:</strong></p><ul><li>Memory optimization via <a href="https://pypi.org/project/mmgp/">mmgp</a> (Memory Management for GPU Poor)</li><li><a href="https://huggingface.co/black-forest-labs/FLUX.1-schnell">FLUX.1-schnell Model</a></li><li><a href="https://huggingface.co/Yuanshi/OmniControl">OminiControl LoRA</a></li><li><a href="https://github.com/Ricky-G/docker-ai-models/tree/main/omnicontrol">Docker Implementation Repository</a></li></ul><hr><p><strong>Image Credits:</strong></p><ul><li>Main image generated by <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/dall-e?view=foundry-classic&tabs=gpt-image-1">GPT-Image-1.5</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🎯 TL;DR: Subject-Driven Image Generation on 12GB VRAM&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Large AI models like FLUX.1-schnell typically require datacenter GPUs with 48GB+ VRAM. Problem: Most developers and hobbyists only have access to consumer RTX cards which vary from 6 - 12GB VRAM in most cases (with the exception of the expensive 4090&amp;#x2F;5090 cards which can go up to 32gb). &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; Using mmgp (Memory Management for GPU Poor) with Docker containerization enables FLUX.1 OmniControl to run on RTX 3060 12GB through 8-bit quantization, dynamic VRAM&amp;#x2F;RAM offloading, and selective layer loading. The implementation provides a Gradio web interface generating 512x512 images in ~10 seconds after initial model loading, with models persisting in system RAM to avoid reload overhead.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Technical Approach:&lt;/strong&gt; Profile 3 configuration quantizes the T5 text encoder (8.8GB → ~4.4GB), pins the FLUX transformer (22.7GB) to reserved system RAM, and dynamically loads only active layers to VRAM during inference. Tested and validated on RTX 3060 12GB with 64GB system RAM running Windows 11 + WSL2 + Docker Desktop.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Complete Implementation:&lt;/strong&gt; All code, Dockerfile, and setup instructions are available at &lt;a href=&quot;https://github.com/Ricky-G/docker-ai-models/tree/main/omnicontrol&quot;&gt;github.com&amp;#x2F;Ricky-G&amp;#x2F;docker-ai-models&amp;#x2F;omnicontrol&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;Recently, I wanted to experiment with OmniControl, a subject-driven image generation model that extends FLUX.1-schnell with LoRA adapters for precise control over object placement. The challenge? The model requirements listed 48GB+ VRAM, and I only had an RTX 3060 with 12GB sitting in my workstation.&lt;/p&gt;
&lt;p&gt;This is a common frustration in the AI development community. Research papers showcase impressive results on expensive datacenter hardware, but practical implementation on consumer GPUs requires significant engineering effort. Could I actually run this model locally without upgrading to an RTX 4090&amp;#x2F;5090 or pay for a VM in Azure with A100?&lt;/p&gt;
&lt;p&gt;The answer turned out to be yes - with some clever memory management and containerization. This blog post walks through the complete process of dockerizing OmniControl to run efficiently on a 12GB consumer GPU.&lt;/p&gt;</summary>
    
    
    
    <category term="AI" scheme="https://clouddev.blog/categories/AI/"/>
    
    <category term="LLMs" scheme="https://clouddev.blog/categories/AI/LLMs/"/>
    
    
    <category term="AI" scheme="https://clouddev.blog/tags/AI/"/>
    
    <category term="LLMs" scheme="https://clouddev.blog/tags/LLMs/"/>
    
    <category term="Local GPUs" scheme="https://clouddev.blog/tags/Local-GPUs/"/>
    
  </entry>
  
  <entry>
    <title>Microsoft Foundry Cross-Region with Private Endpoints (Part 1)</title>
    <link href="https://clouddev.blog/Azure/AI/Networking/microsoft-foundry-cross-region-with-private-endpoints-part-1/"/>
    <id>https://clouddev.blog/Azure/AI/Networking/microsoft-foundry-cross-region-with-private-endpoints-part-1/</id>
    <published>2025-10-10T11:00:00.000Z</published>
    <updated>2026-03-14T04:23:22.831Z</updated>
    
    <content type="html"><![CDATA[<hr><blockquote><p><strong>🎯 TL;DR: Deploy Microsoft Foundry Cross-Region with Private Endpoints</strong></p><p>Microsoft Foundry isn’t available in every Azure region, but data residency requirements often mandate that all data at rest stays within specific regions. This post demonstrates how to keep your data in your compliant region (e.g., New Zealand North) while leveraging Microsoft Foundry in another region (e.g., Australia East) purely for AI inferencing. Using cross-region Private Endpoints over Azure’s backbone network, applications securely access Foundry’s AI capabilities without data traversing the public internet—maintaining both regional compliance and zero-trust security posture.</p><p><strong>The Solution:</strong> All data at rest, applications, and Private Endpoints remain in NZN. Microsoft Foundry deployed in AUE provides AI inferencing only. Private connectivity ensures secure, compliant architecture across regions.</p></blockquote><hr><p>When deploying Microsoft Foundry (formerly Azure AI Foundry) in enterprise environments, you’ll face a critical constraint: <strong>Microsoft Foundry isn’t available in every Azure region, yet data residency requirements mandate that all data at rest remains within specific regions.</strong> </p><p>Imagine this scenario: Your organization must keep all data in New Zealand North due to regulatory compliance, but Microsoft Foundry is only available in Australia East. You can’t move data to AUE, but you need Foundry’s AI capabilities. How do you maintain compliance while accessing AI inferencing services?</p><p>The solution is architectural: <strong>Keep all data at rest in your compliant region (NZN) and use Microsoft Foundry in the available region (AUE) purely for AI inferencing.</strong> By deploying cross-region Private Endpoints, applications in NZN securely access Foundry’s AI services over Azure’s backbone network—no public internet, no data residency violations, no compromises.</p><p>This guide walks through the complete architecture, DNS configuration, security considerations, and implementation steps for deploying this cross-region private endpoint pattern.</p><blockquote><p><strong>⚠️ Important: Foundry Agents Service Limitation</strong></p><p><strong>If you plan to use the Foundry Agents service specifically</strong>, there is a known limitation at the time of writing: all Foundry workspace resources (Cosmos DB, Storage Account, AI Search, Foundry Account, Project, Managed Identity, Azure OpenAI, or other Foundry resources used for model deployments) <strong>must be deployed in the same region as the VNet</strong>.</p><p>This means the cross-region pattern described in this post <strong>will not work for Foundry Agents deployments</strong>—you would need to deploy everything in the same region (e.g., all resources in Australia East where Foundry is available).</p><p><strong>However, if you are NOT using the Foundry Agents service</strong> (i.e., you’re only using Foundry for AI inferencing via API calls—OpenAI models, Speech Services, Vision, etc.), then the cross-region private endpoint pattern works perfectly, and all your data can reside in your chosen compliant region as described in this post.</p><p>For more details, see <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/virtual-networks?view=foundry#known-limitations">Microsoft Learn - Virtual Networks with Foundry Agents - Known Limitations</a></p></blockquote><pre class="mermaid">flowchart TB    subgraph azure["☁️ Azure Backbone"]        direction TB        subgraph NZN["🌏 NZN - Data Residency Region"]            direction TB            subgraph vnet["VNet:  10.1.0.0/16"]                subgraph appsnet["Subnet: snet-apps • 10.1.1.0/24"]                    client[👤 Client App / VM<br/>10.1.1.10]                    data[(💾 Data at Rest<br/>Storage, SQL, etc.)]                end                subgraph pesnet["Subnet: snet • 10.1.2.0/24"]                    pe[🔒 Private Endpoint<br/>10.1.2.4]                end            end            dns[🔐 Private DNS Zones<br/>Resolves to Private IP]        end                subgraph AUE["🌏 AUE - AI Inferencing"]            foundry[[🤖 Microsoft Foundry<br/>myFoundry. cognitiveservices.azure.com]]        end                pe ==>|"🔐 Private Link<br/>"| foundry    end        internet[/"🌐 Public Internet<br/>❌ Blocked"/]        client --> dns    dns -.->|10.1.2.4| pe    client -->|HTTPS| pe        foundry -.-x internet        style azure fill:#f5f5f5,stroke:#666,stroke-width:2px,stroke-dasharray: 5 5    style NZN fill:#e3f2fd,stroke:#1976d2,stroke-width:3px    style AUE fill:#e8f5e9,stroke:#388e3c,stroke-width:3px    style internet fill:#ffebee,stroke:#c62828,stroke-width:2px    style vnet fill:#e1f5fe,stroke:#0288d1,stroke-width: 2px    style dns fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px    style pe fill:#fff3e0,stroke:#ef6c00,stroke-width:3px    style data fill:#e8f5e9,stroke:#388e3c,stroke-width:2px</pre><span id="more"></span><h2 id="Understanding-Microsoft-Foundry"><a href="#Understanding-Microsoft-Foundry" class="headerlink" title="Understanding Microsoft Foundry"></a>Understanding Microsoft Foundry</h2><p>Microsoft Foundry is a unified platform-as-a-service for enterprise AI that brings together agents, models (including Azure OpenAI, Speech, Vision), and AI development tools under one managed resource. Think of it as a comprehensive AI studio environment where developers can build, evaluate, and deploy generative AI applications without the complexity of stitching together multiple Azure services manually.</p><h3 id="Why-Foundry-Matters-for-Enterprise-AI"><a href="#Why-Foundry-Matters-for-Enterprise-AI" class="headerlink" title="Why Foundry Matters for Enterprise AI"></a>Why Foundry Matters for Enterprise AI</h3><p>Foundry provides several enterprise-grade capabilities out of the box:</p><p><strong>Unified AI Platform:</strong> Instead of managing separate Azure OpenAI, Cognitive Services, and ML resources, Foundry consolidates these into a single managed environment with consistent APIs and management experiences.</p><p><strong>Enterprise Security &amp; Compliance:</strong> Built-in support for managed identities, RBAC, audit logging, and network isolation ensures your AI applications meet corporate security requirements.</p><p><strong>Observability &amp; Monitoring:</strong> Integrated tracing, logging, and monitoring capabilities help you understand how your AI applications are performing and troubleshoot issues quickly.</p><p><strong>Development Productivity:</strong> A unified development experience through Azure AI Studio portal and SDKs accelerates AI application development by reducing integration overhead.</p><p>Because Foundry encapsulates many Azure AI services and provides such comprehensive capabilities, it has specific networking requirements when you want to integrate it into a secure, enterprise cloud environment. This is where Private Endpoints become critical.</p><h2 id="Azure-Private-Endpoints-The-Foundation-of-Secure-Connectivity"><a href="#Azure-Private-Endpoints-The-Foundation-of-Secure-Connectivity" class="headerlink" title="Azure Private Endpoints: The Foundation of Secure Connectivity"></a>Azure Private Endpoints: The Foundation of Secure Connectivity</h2><p>An Azure Private Endpoint is essentially a network interface (NIC) with a private IP address from your Azure Virtual Network that connects privately to a supported Azure service. When you create a private endpoint for a service like Foundry, you’re bringing that service into your VNet’s address space, allowing VNet resources to reach the service via an internal IP address instead of traversing the public internet.</p><h3 id="How-Private-Link-Works-Under-the-Hood"><a href="#How-Private-Link-Works-Under-the-Hood" class="headerlink" title="How Private Link Works Under the Hood"></a>How Private Link Works Under the Hood</h3><p>Azure Private Link is the technology that makes Private Endpoints possible. Here’s what happens when you enable private connectivity:</p><ol><li><strong>Private IP Assignment:</strong> A network interface is created in your VNet subnet with a private IP (e.g., <code>10.1.2.4</code>)</li><li><strong>DNS Resolution:</strong> The service’s FQDN (e.g., <code>myFoundry.cognitiveservices.azure.com</code>) is configured to resolve to the private IP instead of the public endpoint</li><li><strong>Backbone Network Transit:</strong> Traffic between your VNet and the service travels entirely on Microsoft’s backbone network, never touching public internet routes</li><li><strong>Regional Flexibility:</strong> The private endpoint can be in a different region than the target service, enabling cross-region private connectivity</li></ol><h3 id="Key-Benefits-of-Private-Endpoints"><a href="#Key-Benefits-of-Private-Endpoints" class="headerlink" title="Key Benefits of Private Endpoints"></a>Key Benefits of Private Endpoints</h3><p><strong>Enhanced Security:</strong> Traffic never leaves Azure’s internal network, eliminating internet-based attack vectors and data exfiltration risks.</p><p><strong>Supported Across Azure PaaS:</strong> Many Azure services support private endpoints including Storage accounts, SQL databases, Cosmos DB, Cognitive Services (which Foundry uses), Key Vault, and more.</p><p><strong>Cross-Region Capability:</strong> Critically, Azure allows the private endpoint’s NIC to reside in a different region than the service’s region. The private endpoint must be in the same region as the VNet you place it in, but the target resource can be in any Azure region. This is the key feature that enables our solution.</p><p><strong>DNS Integration:</strong> Azure Private DNS Zones provide seamless name resolution, making private endpoint connectivity transparent to applications.</p><h3 id="DNS-Configuration-Requirements"><a href="#DNS-Configuration-Requirements" class="headerlink" title="DNS Configuration Requirements"></a>DNS Configuration Requirements</h3><p>Using a private endpoint requires configuring DNS so that the service’s FQDN resolves to the private IP instead of the public IP. For Azure Cognitive Services (which Foundry is built on), this typically involves:</p><ol><li>Creating a Private DNS Zone: <code>privatelink.cognitiveservices.azure.com</code></li><li>Linking the zone to your VNets</li><li>Adding an A record mapping the Foundry hostname to the private endpoint IP</li></ol><p>Without proper DNS configuration, clients will continue resolving the public IP address and bypass the private link entirely, defeating the purpose of the security setup.</p><h2 id="The-Cross-Region-Challenge-When-Foundry-Isn’t-Available-Locally"><a href="#The-Cross-Region-Challenge-When-Foundry-Isn’t-Available-Locally" class="headerlink" title="The Cross-Region Challenge: When Foundry Isn’t Available Locally"></a>The Cross-Region Challenge: When Foundry Isn’t Available Locally</h2><h3 id="Understanding-the-Regional-Availability-Problem"><a href="#Understanding-the-Regional-Availability-Problem" class="headerlink" title="Understanding the Regional Availability Problem"></a>Understanding the Regional Availability Problem</h3><p>Microsoft Foundry is not yet available in every Azure region. At the time of writing, Foundry has limited regional availability as Microsoft continues rolling out the service globally. This creates a common scenario for many organizations:</p><p><strong>The Scenario:</strong> Let’s imagine an organization with Azure infrastructure primarily deployed in New Zealand North (NZN) – perhaps due to data residency requirements, latency optimization for local users, or established governance policies. However, Microsoft Foundry is only available in Australia East (AUE), with New Zealand North not yet on the supported regions list.</p><p><strong>The Challenge:</strong> The organization needs to leverage Foundry’s powerful AI capabilities while maintaining private network connectivity and keeping all traffic within the Azure environment. Simply deploying everything in Australia East isn’t feasible due to data residency requirements etc. So all data at rest needs to be in New Zealand North region.  Microsoft Foundry will be used for AI inferencing only and all data at rest must remain in New Zealand North.</p><h3 id="Why-Standard-Approaches-Don’t-Work"><a href="#Why-Standard-Approaches-Don’t-Work" class="headerlink" title="Why Standard Approaches Don’t Work"></a>Why Standard Approaches Don’t Work</h3><p>Let’s consider the typical alternatives and why they fall short:</p><p><strong>Deploy Everything in AUE:</strong> This requires relocating or duplicating existing infrastructure, increasing costs and complexity. Data residency requirements may prohibit this approach entirely.</p><p><strong>Use Public Endpoints:</strong> Exposing Foundry via public endpoints contradicts enterprise zero-trust security policies and increases attack surface unnecessarily.</p><p><strong>Accept Regional Limitations:</strong> Waiting for Foundry to become available in your region delays AI initiatives and competitive advantages.</p><h3 id="The-Solution-Cross-Region-Private-Endpoints"><a href="#The-Solution-Cross-Region-Private-Endpoints" class="headerlink" title="The Solution: Cross-Region Private Endpoints"></a>The Solution: Cross-Region Private Endpoints</h3><p>The solution leverages Azure’s support for cross-region Private Endpoints. By deploying a Private Endpoint in your New Zealand North VNet that connects to the Foundry service in Australia East, you achieve several critical outcomes:</p><ul><li><strong>Private Connectivity:</strong> All traffic between NZN and AUE travels on Azure’s backbone network</li><li><strong>Regional Compliance:</strong> Resources remain in their designated regions with private interconnection</li><li><strong>Transparent Access:</strong> Applications in NZN access Foundry as if it’s a local service via private IP</li><li><strong>Security Posture:</strong> Foundry’s public endpoints can be disabled, enforcing private-only access</li></ul><h2 id="Architecture-Deep-Dive-Cross-Region-Foundry-Deployment"><a href="#Architecture-Deep-Dive-Cross-Region-Foundry-Deployment" class="headerlink" title="Architecture Deep Dive: Cross-Region Foundry Deployment"></a>Architecture Deep Dive: Cross-Region Foundry Deployment</h2><h3 id="High-Level-Architecture-Overview"><a href="#High-Level-Architecture-Overview" class="headerlink" title="High-Level Architecture Overview"></a>High-Level Architecture Overview</h3><p>The architecture deploys Microsoft Foundry in Australia East while enabling secure access from resources in New Zealand North. Here’s how the components fit together:</p><p><strong>Foundry Service (Australia East):</strong> The actual Microsoft Foundry resource deployed in AUE, configured as a Cognitive Services account with AI capabilities enabled.</p><p><strong>Private Endpoint (New Zealand North):</strong> A network interface in your NZN VNet with a private IP address (e.g., <code>10.1.2.4</code>) that acts as the gateway to the AUE Foundry service.</p><p><strong>Private DNS Zone:</strong> A DNS zone (<code>privatelink.cognitiveservices.azure.com</code>) linked to your VNets that resolves the Foundry FQDN to the private endpoint IP.</p><p><strong>Client Applications (New Zealand North):</strong> VMs, App Services, containers, or other resources in NZN that consume the Foundry APIs.</p><pre class="mermaid">sequenceDiagram    participant C as 👤 Client<br/>10.1.1.10    participant D as 🔐 Private DNS    participant P as 🔒 Private EP<br/>10.1.2.4    participant B as 🌐 Public Internet    participant F as 🤖 Foundry<br/>(AUE)        rect rgb(227, 242, 253)        Note over C,D:  New Zealand North Region    end    rect rgb(232, 245, 232)        Note over F: Australia East Region    end        C->>D: 1. DNS Query:  myFoundry.cognitiveservices. azure.com    D->>C:  2. Returns Private IP: 10.1.2.4        Note over C,P: Traffic stays on Azure backbone    C->>P: 3. HTTPS Request to 10.1.2.4:443    P->>F: 4. Azure Private Link (backbone)    F->>P: 5. AI Response    P->>C: 6. Response delivered        rect rgb(255, 235, 238)        Note over B:  Public path blocked        C--xB: ❌ No public internet route        B--xF: ❌ Public access disabled    end</pre><h3 id="Traffic-Flow-Explanation"><a href="#Traffic-Flow-Explanation" class="headerlink" title="Traffic Flow Explanation"></a>Traffic Flow Explanation</h3><p>Let’s trace what happens when a client application in NZN makes a request to Foundry:</p><ol><li><strong>Application Request:</strong> The application initiates a connection to <code>myFoundry.cognitiveservices.azure.com</code></li><li><strong>DNS Resolution:</strong> The VNet’s DNS configuration queries the Private DNS Zone</li><li><strong>Private IP Return:</strong> DNS returns <code>10.1.2.4</code> (the private endpoint IP) instead of the public IP</li><li><strong>Private Endpoint Connection:</strong> The application connects to the private endpoint in NZN</li><li><strong>Cross-Region Transit:</strong> Azure Private Link routes the traffic across the backbone network to AUE</li><li><strong>Foundry Processing:</strong> The Foundry service in AUE receives and processes the request</li><li><strong>Response Path:</strong> The response follows the same path back through the private endpoint</li></ol><p>From the application’s perspective, it’s communicating with a local service in NZN. All the cross-region complexity is abstracted by Azure’s networking layer.</p><h3 id="Critical-Architectural-Considerations"><a href="#Critical-Architectural-Considerations" class="headerlink" title="Critical Architectural Considerations"></a>Critical Architectural Considerations</h3><h4 id="Azure-Backbone-Network-Transit"><a href="#Azure-Backbone-Network-Transit" class="headerlink" title="Azure Backbone Network Transit"></a>Azure Backbone Network Transit</h4><p>The connection between NZN and AUE is entirely internal to Azure. No traffic exits to the public internet, even though the regions are geographically separated across the Tasman Sea. Azure’s global backbone network handles the routing, ensuring:</p><ul><li><strong>Security:</strong> Traffic never traverses public networks</li><li><strong>Reliability:</strong> Azure’s backbone offers higher SLAs than internet routes  </li><li><strong>Performance:</strong> Optimized routing between Azure datacenters</li></ul><h4 id="Private-DNS-Resolution-Requirements"><a href="#Private-DNS-Resolution-Requirements" class="headerlink" title="Private DNS Resolution Requirements"></a>Private DNS Resolution Requirements</h4><p>DNS configuration is the linchpin that makes private endpoints work correctly. Microsoft Foundry (AIServices kind) requires multiple Private DNS zones for full functionality:</p><p><strong>Required Private DNS Zones:</strong></p><ol><li><code>privatelink.cognitiveservices.azure.com</code> - Primary Cognitive Services endpoint</li><li><code>privatelink.openai.azure.com</code> - Azure OpenAI service endpoints</li><li><code>privatelink.services.ai.azure.com</code> - AI Foundry management endpoints</li></ol><p><strong>Configuration Steps:</strong></p><p><strong>DNS Zone Creation:</strong> Create all required Private DNS zones in your subscription</p><p><strong>VNet Linking:</strong> Link the DNS zones to your NZN VNet (and any other VNets that need access)</p><p><strong>A Record Mapping:</strong> Azure automatically creates the necessary A records when using DNS zone groups with private endpoints, or you can manually add A records for your Foundry resource pointing to the private endpoint IP</p><p><strong>Multi-VNet Scenarios:</strong> If you have separate VNets (e.g., hub-and-spoke topology), ensure all VNets are linked to the DNS zones or use DNS forwarding</p><p>Without proper DNS configuration across all zones, clients will resolve public IPs and bypass your private endpoint entirely, rendering the security setup ineffective.</p><h4 id="Performance-and-Latency-Implications"><a href="#Performance-and-Latency-Implications" class="headerlink" title="Performance and Latency Implications"></a>Performance and Latency Implications</h4><p>While traffic stays private, the cross-region architecture introduces additional latency:</p><p><strong>Typical Latency:</strong> NZN to AUE is a trans-Tasman hop, usually adding 30-60ms compared to in-region calls</p><p><strong>Acceptable Use Cases:</strong> Most AI API calls (text generation, embeddings, etc.) can tolerate this latency</p><p><strong>Latency-Sensitive Workloads:</strong> Real-time voice applications or ultra-low-latency requirements may need careful evaluation</p><p><strong>Bandwidth Charges:</strong> Azure charges for cross-region data transfer. Egress from AUE to NZN incurs bandwidth costs. Budget accordingly for high-volume scenarios.</p><h4 id="Security-Posture-Enhancement"><a href="#Security-Posture-Enhancement" class="headerlink" title="Security Posture Enhancement"></a>Security Posture Enhancement</h4><p>Private Endpoints enable several security improvements:</p><p><strong>Disable Public Access:</strong> Configure Foundry to reject all public network connections, forcing traffic through approved private endpoints</p><p><strong>Network Security Groups (NSGs):</strong> Apply NSGs to the private endpoint subnet to control which source IPs can reach the Foundry service</p><p><strong>Azure Firewall Integration:</strong> Route private endpoint traffic through Azure Firewall for additional inspection and logging</p><p><strong>On-Premises Connectivity:</strong> If you have ExpressRoute or VPN connecting on-premises to Azure, those networks can also access Foundry privately through the NZN VNet</p><h2 id="Step-by-Step-Implementation-Overview"><a href="#Step-by-Step-Implementation-Overview" class="headerlink" title="Step-by-Step Implementation Overview"></a>Step-by-Step Implementation Overview</h2><blockquote><p><strong>⚠️ Important: Choose Your Own Unique Name</strong></p><p>The name <code>myFoundryDemo</code> used throughout these examples is <strong>already taken</strong> in Azure’s global namespace. Cognitive Services accounts require globally unique names across all Azure subscriptions worldwide.</p><p><strong>Before running any commands below:</strong> Replace <code>myFoundryDemo</code> with your own unique name (e.g., <code>myFoundryProd2025</code>, <code>contoso-ai-foundry</code>, etc.) in all the Azure CLI commands. This name will become part of your endpoint URL: <code>&lt;your-unique-name&gt;.cognitiveservices.azure.com</code></p></blockquote><p>The following sections walk through the high-level steps to implement this architecture. Part 2 will provide detailed scripts and Infrastructure-as-Code templates.</p><h3 id="Step-1-Deploy-Foundry-in-Australia-East"><a href="#Step-1-Deploy-Foundry-in-Australia-East" class="headerlink" title="Step 1: Deploy Foundry in Australia East"></a>Step 1: Deploy Foundry in Australia East</h3><p>Start by creating your Microsoft Foundry resource in the Australia East region:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Create resource group in Australia East</span></span><br><span class="line">az group create --name rg-foundry-aue --location australiaeast</span><br><span class="line"></span><br><span class="line"><span class="comment"># Create Foundry resource (Cognitive Services account)</span></span><br><span class="line">az cognitiveservices account create \</span><br><span class="line">  --name myFoundryDemo \</span><br><span class="line">  --resource-group rg-foundry-aue \</span><br><span class="line">  --kind AIServices \</span><br><span class="line">  --sku S0 \</span><br><span class="line">  --location australiaeast \</span><br><span class="line">  --custom-domain myFoundryDemo</span><br></pre></td></tr></table></figure><p><strong>Important Notes:</strong></p><ul><li>Initially allow public network access during setup</li><li>Note the resource name and ID for the next steps</li><li>Foundry appears as a Cognitive Services account of kind <code>AIServices</code></li></ul><h3 id="Step-2-Create-Virtual-Network-in-New-Zealand-North"><a href="#Step-2-Create-Virtual-Network-in-New-Zealand-North" class="headerlink" title="Step 2: Create Virtual Network in New Zealand North"></a>Step 2: Create Virtual Network in New Zealand North</h3><p>Set up a VNet in NZN where the private endpoint will reside:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Create resource group in New Zealand North</span></span><br><span class="line">az group create --name rg-networking-nzn --location newzealandnorth</span><br><span class="line"></span><br><span class="line"><span class="comment"># Create virtual network</span></span><br><span class="line">az network vnet create \</span><br><span class="line">  --name vnet-nzn-main \</span><br><span class="line">  --resource-group rg-networking-nzn \</span><br><span class="line">  --location newzealandnorth \</span><br><span class="line">  --address-prefix 10.1.0.0/16 \</span><br><span class="line">  --subnet-name snet-apps \</span><br><span class="line">  --subnet-prefix 10.1.1.0/24</span><br><span class="line"></span><br><span class="line"><span class="comment"># Create subnet for private endpoints</span></span><br><span class="line">az network vnet subnet create \</span><br><span class="line">  --name snet-private-endpoints \</span><br><span class="line">  --resource-group rg-networking-nzn \</span><br><span class="line">  --vnet-name vnet-nzn-main \</span><br><span class="line">  --address-prefix 10.1.2.0/24 \</span><br><span class="line">  --disable-private-endpoint-network-policies <span class="literal">true</span></span><br></pre></td></tr></table></figure><p><strong>Network Planning Considerations:</strong></p><ul><li>Ensure address space doesn’t conflict with other VNets you’ll peer</li><li>Create a dedicated subnet for private endpoints</li><li>Disable network policies on the private endpoint subnet</li></ul><h3 id="Step-3-Create-the-Cross-Region-Private-Endpoint"><a href="#Step-3-Create-the-Cross-Region-Private-Endpoint" class="headerlink" title="Step 3: Create the Cross-Region Private Endpoint"></a>Step 3: Create the Cross-Region Private Endpoint</h3><p>Now create the private endpoint in NZN that connects to the AUE Foundry resource:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Get Foundry resource ID</span></span><br><span class="line">FOUNDRY_ID=$(az cognitiveservices account show \</span><br><span class="line">  --name myFoundryDemo \</span><br><span class="line">  --resource-group rg-foundry-aue \</span><br><span class="line">  --query <span class="built_in">id</span> -o tsv)</span><br><span class="line"></span><br><span class="line"><span class="comment"># Create private endpoint</span></span><br><span class="line">az network private-endpoint create \</span><br><span class="line">  --name pe-foundry-nzn \</span><br><span class="line">  --resource-group rg-networking-nzn \</span><br><span class="line">  --location newzealandnorth \</span><br><span class="line">  --vnet-name vnet-nzn-main \</span><br><span class="line">  --subnet snet-private-endpoints \</span><br><span class="line">  --private-connection-resource-id <span class="variable">$FOUNDRY_ID</span> \</span><br><span class="line">  --group-id account \</span><br><span class="line">  --connection-name foundry-connection</span><br></pre></td></tr></table></figure><p><strong>Key Parameters:</strong></p><ul><li><code>--location</code> must match the VNet’s region (newzealandnorth)</li><li><code>--private-connection-resource-id</code> points to the AUE Foundry resource</li><li><code>--group-id account</code> specifies the Cognitive Services sub-resource type</li><li>Connection auto-approves since you own both resources</li></ul><h3 id="Step-4-Configure-Private-DNS-Integration"><a href="#Step-4-Configure-Private-DNS-Integration" class="headerlink" title="Step 4: Configure Private DNS Integration"></a>Step 4: Configure Private DNS Integration</h3><p>Set up DNS to resolve the Foundry FQDN to the private endpoint IP. Microsoft Foundry requires multiple Private DNS zones:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Create all required Private DNS Zones for Microsoft Foundry</span></span><br><span class="line">az network private-dns zone create \</span><br><span class="line">  --resource-group rg-networking-nzn \</span><br><span class="line">  --name privatelink.cognitiveservices.azure.com</span><br><span class="line"></span><br><span class="line">az network private-dns zone create \</span><br><span class="line">  --resource-group rg-networking-nzn \</span><br><span class="line">  --name privatelink.openai.azure.com</span><br><span class="line"></span><br><span class="line">az network private-dns zone create \</span><br><span class="line">  --resource-group rg-networking-nzn \</span><br><span class="line">  --name privatelink.services.ai.azure.com</span><br><span class="line"></span><br><span class="line"><span class="comment"># Link DNS zones to VNet</span></span><br><span class="line"><span class="keyword">for</span> zone <span class="keyword">in</span> <span class="string">&quot;privatelink.cognitiveservices.azure.com&quot;</span> <span class="string">&quot;privatelink.openai.azure.com&quot;</span> <span class="string">&quot;privatelink.services.ai.azure.com&quot;</span></span><br><span class="line"><span class="keyword">do</span></span><br><span class="line">  az network private-dns <span class="built_in">link</span> vnet create \</span><br><span class="line">    --resource-group rg-networking-nzn \</span><br><span class="line">    --zone-name <span class="variable">$zone</span> \</span><br><span class="line">    --name <span class="string">&quot;link-vnet-nzn-main-<span class="variable">$&#123;zone%%.azure.com&#125;</span>&quot;</span> \</span><br><span class="line">    --virtual-network vnet-nzn-main \</span><br><span class="line">    --registration-enabled <span class="literal">false</span></span><br><span class="line"><span class="keyword">done</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Alternative: Use DNS Zone Group for automatic configuration</span></span><br><span class="line"><span class="comment"># This automatically creates A records in all required zones</span></span><br><span class="line">az network private-endpoint dns-zone-group create \</span><br><span class="line">  --resource-group rg-networking-nzn \</span><br><span class="line">  --endpoint-name pe-foundry-nzn \</span><br><span class="line">  --name default \</span><br><span class="line">  --private-dns-zone privatelink.cognitiveservices.azure.com \</span><br><span class="line">  --zone-name cognitiveservices</span><br><span class="line"></span><br><span class="line">az network private-endpoint dns-zone-group add \</span><br><span class="line">  --resource-group rg-networking-nzn \</span><br><span class="line">  --endpoint-name pe-foundry-nzn \</span><br><span class="line">  --name default \</span><br><span class="line">  --private-dns-zone privatelink.openai.azure.com \</span><br><span class="line">  --zone-name openai</span><br><span class="line"></span><br><span class="line">az network private-endpoint dns-zone-group add \</span><br><span class="line">  --resource-group rg-networking-nzn \</span><br><span class="line">  --endpoint-name pe-foundry-nzn \</span><br><span class="line">  --name default \</span><br><span class="line">  --private-dns-zone privatelink.services.ai.azure.com \</span><br><span class="line">  --zone-name aiservices</span><br></pre></td></tr></table></figure><p><strong>DNS Configuration Options:</strong></p><p><strong>Option 1 - DNS Zone Groups (Recommended):</strong> Using DNS zone groups (shown above) automatically creates and maintains A records in all zones. This is the preferred approach as Azure manages the DNS records lifecycle.</p><p><strong>Option 2 - Manual A Records:</strong> If you need manual control, get the private endpoint IP and create A records manually:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Get private endpoint IP</span></span><br><span class="line">PE_IP=$(az network private-endpoint show \</span><br><span class="line">  --name pe-foundry-nzn \</span><br><span class="line">  --resource-group rg-networking-nzn \</span><br><span class="line">  --query <span class="string">&#x27;customDnsConfigs[0].ipAddresses[0]&#x27;</span> -o tsv)</span><br><span class="line"></span><br><span class="line"><span class="comment"># Create DNS A record in the primary zone</span></span><br><span class="line">az network private-dns record-set a add-record \</span><br><span class="line">  --resource-group rg-networking-nzn \</span><br><span class="line">  --zone-name privatelink.cognitiveservices.azure.com \</span><br><span class="line">  --record-set-name myFoundryDemo \</span><br><span class="line">  --ipv4-address <span class="variable">$PE_IP</span></span><br></pre></td></tr></table></figure><p><strong>DNS Verification:</strong></p><ul><li>All DNS zones must be created and linked to VNets needing access</li><li>DNS zone groups automatically handle A record creation and updates</li><li>For manual records, ensure the record name matches your Foundry resource name</li><li>Multi-VNet deployments require linking all zones to each VNet</li></ul><h3 id="Step-5-Test-Private-Connectivity"><a href="#Step-5-Test-Private-Connectivity" class="headerlink" title="Step 5: Test Private Connectivity"></a>Step 5: Test Private Connectivity</h3><p>Deploy a test VM in the NZN VNet and verify connectivity:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Create test VM</span></span><br><span class="line">az vm create \</span><br><span class="line">  --resource-group rg-networking-nzn \</span><br><span class="line">  --name vm-test-nzn \</span><br><span class="line">  --location newzealandnorth \</span><br><span class="line">  --vnet-name vnet-nzn-main \</span><br><span class="line">  --subnet snet-apps \</span><br><span class="line">  --image Ubuntu2204 \</span><br><span class="line">  --admin-username azureuser \</span><br><span class="line">  --generate-ssh-keys</span><br><span class="line"></span><br><span class="line"><span class="comment"># SSH to VM and test DNS resolution</span></span><br><span class="line">ssh azureuser@&lt;vm-public-ip&gt;</span><br><span class="line"></span><br><span class="line"><span class="comment"># Verify DNS resolves to private IP</span></span><br><span class="line">nslookup myFoundryDemo.cognitiveservices.azure.com</span><br></pre></td></tr></table></figure><p><strong>Expected DNS Output:</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">Server:  10.1.0.4</span><br><span class="line">Address: 10.1.0.4#53</span><br><span class="line"></span><br><span class="line">Name:    myFoundryDemo.privatelink.cognitiveservices.azure.com</span><br><span class="line">Address: 10.1.2.4</span><br><span class="line">Aliases: myFoundryDemo.cognitiveservices.azure.com</span><br></pre></td></tr></table></figure><p>The resolution to <code>10.1.2.4</code> (private IP) confirms DNS is working correctly.</p><p><strong>Test API Connectivity:</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Get Foundry API key (from Azure portal or CLI)</span></span><br><span class="line">API_KEY=<span class="string">&quot;your-foundry-api-key&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Test API call</span></span><br><span class="line">curl -X GET <span class="string">&quot;https://myFoundryDemo.cognitiveservices.azure.com/openai/deployments?api-version=2024-02-01&quot;</span> \</span><br><span class="line">  -H <span class="string">&quot;api-key: <span class="variable">$API_KEY</span>&quot;</span></span><br></pre></td></tr></table></figure><p>If you receive a valid response (list of deployments or empty array), private connectivity is working. If you disabled public access on Foundry, this same curl command from your local machine should fail with a network error.</p><h3 id="Step-6-Lock-Down-Public-Access"><a href="#Step-6-Lock-Down-Public-Access" class="headerlink" title="Step 6: Lock Down Public Access"></a>Step 6: Lock Down Public Access</h3><p>With private connectivity confirmed, enhance security by disabling public access:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Disable public network access</span></span><br><span class="line">az cognitiveservices account update \</span><br><span class="line">  --name myFoundryDemo \</span><br><span class="line">  --resource-group rg-foundry-aue \</span><br><span class="line">  --public-network-access Disabled</span><br></pre></td></tr></table></figure><p><strong>Additional Security Hardening:</strong></p><ul><li>Apply NSGs to the private endpoint subnet restricting source IPs</li><li>Enable Azure Firewall for additional traffic inspection</li><li>Configure diagnostic logs to monitor access patterns</li><li>Use managed identities instead of API keys for authentication</li></ul><h2 id="Real-World-Considerations-and-Trade-Offs"><a href="#Real-World-Considerations-and-Trade-Offs" class="headerlink" title="Real-World Considerations and Trade-Offs"></a>Real-World Considerations and Trade-Offs</h2><h3 id="Cost-Implications"><a href="#Cost-Implications" class="headerlink" title="Cost Implications"></a>Cost Implications</h3><p><strong>Cross-Region Data Transfer:</strong> Azure charges for data egress between regions. For NZN ↔ AUE, expect approximately $0.02-0.05 per GB transferred (approx pricing at the time of this writing). Monitor your Foundry usage patterns and budget accordingly.</p><p><strong>Private Endpoint Costs:</strong> Private endpoints incur a small hourly charge plus data processing charges per GB.</p><p><strong>Foundry Service Costs:</strong> The Foundry service itself has per-transaction or per-token costs depending on the AI services used (OpenAI, Speech, etc.).</p><h3 id="Latency-Considerations"><a href="#Latency-Considerations" class="headerlink" title="Latency Considerations"></a>Latency Considerations</h3><p><strong>Baseline Latency:</strong> Expect 30-60ms additional latency for cross-region calls compared to in-region</p><p><strong>Impact Assessment:</strong> For most AI workloads (text generation, embeddings, document analysis), this latency is acceptable</p><p><strong>Optimization Strategies:</strong></p><ul><li>Use async&#x2F;await patterns to parallelize independent API calls</li><li>Implement request batching where applicable</li><li>Cache frequently-requested results</li><li>Consider regional caching layers for static content</li></ul><h2 id="On-Premises-Integration"><a href="#On-Premises-Integration" class="headerlink" title="On-Premises Integration"></a>On-Premises Integration</h2><p>If your organization has on-premises datacenters connected to Azure via ExpressRoute or Site-to-Site VPN, you can extend private connectivity to those environments:</p><h3 id="DNS-Forwarding"><a href="#DNS-Forwarding" class="headerlink" title="DNS Forwarding"></a>DNS Forwarding</h3><p>Configure on-premises DNS servers to forward queries for <code>*.cognitiveservices.azure.com</code> to Azure’s DNS resolvers:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Example BIND configuration</span></span><br><span class="line">zone <span class="string">&quot;cognitiveservices.azure.com&quot;</span> &#123;</span><br><span class="line">    <span class="built_in">type</span> forward;</span><br><span class="line">    forwarders &#123; 10.1.0.4; &#125;;  <span class="comment"># Azure VNet DNS server</span></span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h3 id="Network-Connectivity"><a href="#Network-Connectivity" class="headerlink" title="Network Connectivity"></a>Network Connectivity</h3><p>Ensure your on-premises network has routes to the NZN VNet where the private endpoint resides:</p><ul><li><strong>ExpressRoute:</strong> Private peering enables private IP connectivity</li><li><strong>Site-to-Site VPN:</strong> VPN gateway provides encrypted connectivity</li><li><strong>Route Tables:</strong> Verify routes exist for the private endpoint subnet (<code>10.1.2.0/24</code>)</li></ul><p>With proper DNS and routing configuration, on-premises applications can access Foundry privately through the Azure backbone network.</p><h2 id="Cleanup-Resources"><a href="#Cleanup-Resources" class="headerlink" title="Cleanup Resources"></a>Cleanup Resources</h2><p>If you’re finished testing and want to remove all resources created in this guide, you can delete the resource groups:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Delete the networking resource group in New Zealand North</span></span><br><span class="line"><span class="comment"># This removes the VNet, subnets, private endpoint, DNS zones, and test VM</span></span><br><span class="line">az group delete \</span><br><span class="line">  --name rg-networking-nzn \</span><br><span class="line">  --<span class="built_in">yes</span> \</span><br><span class="line">  --no-wait</span><br><span class="line"></span><br><span class="line"><span class="comment"># Delete the Foundry resource group in Australia East</span></span><br><span class="line"><span class="comment"># This removes the Microsoft Foundry (Cognitive Services) account</span></span><br><span class="line">az group delete \</span><br><span class="line">  --name rg-foundry-aue \</span><br><span class="line">  --<span class="built_in">yes</span> \</span><br><span class="line">  --no-wait</span><br></pre></td></tr></table></figure><p><strong>Important Notes:</strong></p><ul><li>The <code>--yes</code> flag skips confirmation prompts</li><li>The <code>--no-wait</code> flag allows the command to return immediately without waiting for deletion to complete</li><li>Resource group deletion is permanent and cannot be undone</li><li>Deletion can take several minutes; you can check status in the Azure portal or with <code>az group show</code></li></ul><h2 id="Wrapping-Up-Part-1"><a href="#Wrapping-Up-Part-1" class="headerlink" title="Wrapping Up Part 1"></a>Wrapping Up Part 1</h2><p>This architecture solves a critical challenge: <strong>Microsoft Foundry isn’t available in all Azure regions, but data residency requirements often mandate that data at rest remains within specific regions.</strong></p><p>The solution demonstrated here keeps all data at rest in New Zealand North (your region of choice), while leveraging Microsoft Foundry in Australia East purely for AI inferencing capabilities. By deploying Private Endpoints in the NZN region, applications access Foundry’s AI services securely over Azure’s backbone network without any data ever traversing the public internet.</p><p><strong>Key Takeaway:</strong> Data stays in your compliant region (NZN), AI inferencing happens in the Foundry region (AUE), and all communication flows privately through Azure’s backbone. This pattern works for any region pair where Foundry availability doesn’t align with your data residency requirements.</p><p>In Part 2, we’ll look at Infrastructure-as-Code templates using Bicep and Terraform to automate this deployment.</p><h2 id="References"><a href="#References" class="headerlink" title="References"></a>References</h2><ul><li><a href="https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-azure-ai-foundry?view=foundry-classic">Microsoft Docs – What is Azure AI Foundry?</a></li><li><a href="https://learn.microsoft.com/en-us/azure/private-link/private-endpoint-overview">Microsoft Docs – What is a private endpoint?</a></li><li><a href="https://learn.microsoft.com/en-us/azure/architecture/ai-ml/architecture/baseline-microsoft-foundry-chat">Azure Architecture Center – Baseline Azure AI Foundry Architecture</a></li><li><a href="https://learn.microsoft.com/en-us/answers/questions/5627096/vnet-integrated-open-ai-deployment-issues">Microsoft Q&amp;A – VNet-Integrated AI Deployment</a></li><li>Main image generated by <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/dall-e?view=foundry-classic&tabs=gpt-image-1">GPT-Image-1.5</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🎯 TL;DR: Deploy Microsoft Foundry Cross-Region with Private Endpoints&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Microsoft Foundry isn’t available in every Azure region, but data residency requirements often mandate that all data at rest stays within specific regions. This post demonstrates how to keep your data in your compliant region (e.g., New Zealand North) while leveraging Microsoft Foundry in another region (e.g., Australia East) purely for AI inferencing. Using cross-region Private Endpoints over Azure’s backbone network, applications securely access Foundry’s AI capabilities without data traversing the public internet—maintaining both regional compliance and zero-trust security posture.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Solution:&lt;/strong&gt; All data at rest, applications, and Private Endpoints remain in NZN. Microsoft Foundry deployed in AUE provides AI inferencing only. Private connectivity ensures secure, compliant architecture across regions.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;When deploying Microsoft Foundry (formerly Azure AI Foundry) in enterprise environments, you’ll face a critical constraint: &lt;strong&gt;Microsoft Foundry isn’t available in every Azure region, yet data residency requirements mandate that all data at rest remains within specific regions.&lt;/strong&gt; &lt;/p&gt;
&lt;p&gt;Imagine this scenario: Your organization must keep all data in New Zealand North due to regulatory compliance, but Microsoft Foundry is only available in Australia East. You can’t move data to AUE, but you need Foundry’s AI capabilities. How do you maintain compliance while accessing AI inferencing services?&lt;/p&gt;
&lt;p&gt;The solution is architectural: &lt;strong&gt;Keep all data at rest in your compliant region (NZN) and use Microsoft Foundry in the available region (AUE) purely for AI inferencing.&lt;/strong&gt; By deploying cross-region Private Endpoints, applications in NZN securely access Foundry’s AI services over Azure’s backbone network—no public internet, no data residency violations, no compromises.&lt;/p&gt;
&lt;p&gt;This guide walks through the complete architecture, DNS configuration, security considerations, and implementation steps for deploying this cross-region private endpoint pattern.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Important: Foundry Agents Service Limitation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If you plan to use the Foundry Agents service specifically&lt;/strong&gt;, there is a known limitation at the time of writing: all Foundry workspace resources (Cosmos DB, Storage Account, AI Search, Foundry Account, Project, Managed Identity, Azure OpenAI, or other Foundry resources used for model deployments) &lt;strong&gt;must be deployed in the same region as the VNet&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This means the cross-region pattern described in this post &lt;strong&gt;will not work for Foundry Agents deployments&lt;/strong&gt;—you would need to deploy everything in the same region (e.g., all resources in Australia East where Foundry is available).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;However, if you are NOT using the Foundry Agents service&lt;/strong&gt; (i.e., you’re only using Foundry for AI inferencing via API calls—OpenAI models, Speech Services, Vision, etc.), then the cross-region private endpoint pattern works perfectly, and all your data can reside in your chosen compliant region as described in this post.&lt;/p&gt;
&lt;p&gt;For more details, see &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/virtual-networks?view=foundry#known-limitations&quot;&gt;Microsoft Learn - Virtual Networks with Foundry Agents - Known Limitations&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class=&quot;mermaid&quot;&gt;flowchart TB
    subgraph azure[&quot;☁️ Azure Backbone&quot;]
        direction TB
        subgraph NZN[&quot;🌏 NZN - Data Residency Region&quot;]
            direction TB
            subgraph vnet[&quot;VNet:  10.1.0.0/16&quot;]
                subgraph appsnet[&quot;Subnet: snet-apps • 10.1.1.0/24&quot;]
                    client[👤 Client App / VM&lt;br/&gt;10.1.1.10]
                    data[(💾 Data at Rest&lt;br/&gt;Storage, SQL, etc.)]
                end
                subgraph pesnet[&quot;Subnet: snet • 10.1.2.0/24&quot;]
                    pe[🔒 Private Endpoint&lt;br/&gt;10.1.2.4]
                end
            end
            dns[🔐 Private DNS Zones&lt;br/&gt;Resolves to Private IP]
        end
        
        subgraph AUE[&quot;🌏 AUE - AI Inferencing&quot;]
            foundry[[🤖 Microsoft Foundry&lt;br/&gt;myFoundry. cognitiveservices.azure.com]]
        end
        
        pe ==&gt;|&quot;🔐 Private Link&lt;br/&gt;&quot;| foundry
    end
    
    internet[/&quot;🌐 Public Internet&lt;br/&gt;❌ Blocked&quot;/]
    
    client --&gt; dns
    dns -.-&gt;|10.1.2.4| pe
    client --&gt;|HTTPS| pe
    
    foundry -.-x internet
    
    style azure fill:#f5f5f5,stroke:#666,stroke-width:2px,stroke-dasharray: 5 5
    style NZN fill:#e3f2fd,stroke:#1976d2,stroke-width:3px
    style AUE fill:#e8f5e9,stroke:#388e3c,stroke-width:3px
    style internet fill:#ffebee,stroke:#c62828,stroke-width:2px
    style vnet fill:#e1f5fe,stroke:#0288d1,stroke-width: 2px
    style dns fill:#f3e5f5,stroke:#7b1fa2,stroke-width:2px
    style pe fill:#fff3e0,stroke:#ef6c00,stroke-width:3px
    style data fill:#e8f5e9,stroke:#388e3c,stroke-width:2px&lt;/pre&gt;</summary>
    
    
    
    <category term="Azure" scheme="https://clouddev.blog/categories/Azure/"/>
    
    <category term="AI" scheme="https://clouddev.blog/categories/Azure/AI/"/>
    
    <category term="Networking" scheme="https://clouddev.blog/categories/Azure/AI/Networking/"/>
    
    
    <category term="Azure" scheme="https://clouddev.blog/tags/Azure/"/>
    
    <category term="AI" scheme="https://clouddev.blog/tags/AI/"/>
    
    <category term="Microsoft Foundry" scheme="https://clouddev.blog/tags/Microsoft-Foundry/"/>
    
    <category term="Private Endpoints" scheme="https://clouddev.blog/tags/Private-Endpoints/"/>
    
    <category term="Private Link" scheme="https://clouddev.blog/tags/Private-Link/"/>
    
    <category term="Networking" scheme="https://clouddev.blog/tags/Networking/"/>
    
    <category term="Security" scheme="https://clouddev.blog/tags/Security/"/>
    
  </entry>
  
  <entry>
    <title>Pimp My Terminal - Terminal Customization with Oh My Posh - A Cloud Native Terminal Setup</title>
    <link href="https://clouddev.blog/Engineering/Tooling/pimp-my-terminal-terminal-customization-with-oh-my-posh-a-cloud-native-terminal-setup/"/>
    <id>https://clouddev.blog/Engineering/Tooling/pimp-my-terminal-terminal-customization-with-oh-my-posh-a-cloud-native-terminal-setup/</id>
    <published>2025-09-19T12:00:00.000Z</published>
    <updated>2026-03-14T04:23:22.826Z</updated>
    
    <content type="html"><![CDATA[<hr><blockquote><p><strong>🎯 TL;DR: Automated Oh My Posh Terminal Setup for Cloud Native Development</strong></p><p>Every new machine or fresh Windows install means reconfiguring your terminal environment from scratch. Problem: Manually setting up Oh My Posh, installing Nerd Fonts, and configuring custom themes is tedious and error-prone across multiple machines. </p><p><strong>Solution:</strong> (<a href="https://github.com/Ricky-G/script-library/blob/main/pimp-my-terminal.ps1">A single PowerShell script available on GitHub https://github.com/Ricky-G/script-library/blob/main/pimp-my-terminal.ps1</a>) that automates the entire process - installing Oh My Posh via winget, deploying a Nerd Font, Terminal-Icons module, creating a custom “Cloud Native Azure” theme optimized for Kubernetes and Azure workflows, and configuring your PowerShell profile with PSReadLine enhancements. </p><p><strong>Prerequisites:</strong> Enable script execution with <code>Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser</code> before running. This approach transforms the multi-hour setup process into a one-command operation, providing immediate visual context for Git branches, Kubernetes clusters, Azure subscriptions, and command execution times - critical information for modern cloud native development.</p></blockquote><hr><p>Recently, I found myself setting up yet another development machine, and as I stared at the blank PowerShell terminal, I realized I’d reached my limit with manual terminal configuration. Every new machine or clean install meant the same tedious process: download Oh My Posh, find a Nerd Font installer, copy configuration files, edit PowerShell profiles, and spend 30 minutes getting everything just right.</p><p>The frustration wasn’t just about aesthetics - a properly configured terminal is a productivity multiplier. When you’re constantly switching between multiple Git repositories, Kubernetes clusters, and Azure subscriptions throughout the day, having that contextual information immediately visible saves countless keystrokes and eliminates mental overhead.</p><p>This blog post shares my automated solution: a single PowerShell script that takes a bare Windows terminal and transforms it into a fully-configured, cloud native-ready development environment in under 5 minutes. Whether you’re setting up a new machine, rebuilding after a Windows update disaster, or just want to standardize terminal configuration across your team, this automation eliminates the manual work.</p><p><img src="/Engineering/Tooling/pimp-my-terminal-terminal-customization-with-oh-my-posh-a-cloud-native-terminal-setup/before-after-terminal.png" alt="Before and After Terminal"></p><h2 id="Quick-Start-Get-Up-and-Running-in-5-Minutes"><a href="#Quick-Start-Get-Up-and-Running-in-5-Minutes" class="headerlink" title="Quick Start - Get Up and Running in 5 Minutes"></a>Quick Start - Get Up and Running in 5 Minutes</h2><p><strong>Want to skip the details and just get started?</strong> Here’s everything you need to run the automation script:</p><h3 id="Step-1-Enable-Script-Execution"><a href="#Step-1-Enable-Script-Execution" class="headerlink" title="Step 1: Enable Script Execution"></a>Step 1: Enable Script Execution</h3><p>Open PowerShell as Administrator and run:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">Set-ExecutionPolicy</span> <span class="literal">-ExecutionPolicy</span> RemoteSigned <span class="literal">-Scope</span> CurrentUser</span><br></pre></td></tr></table></figure><p>When prompted, type <code>Y</code> and press Enter.</p><h3 id="Step-2-Download-and-Run-the-Script"><a href="#Step-2-Download-and-Run-the-Script" class="headerlink" title="Step 2: Download and Run the Script"></a>Step 2: Download and Run the Script</h3><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Download and run the automation script</span></span><br><span class="line"><span class="built_in">Invoke-WebRequest</span> <span class="literal">-Uri</span> <span class="string">&quot;https://raw.githubusercontent.com/Ricky-G/script-library/main/pimp-my-terminal.ps1&quot;</span> <span class="literal">-OutFile</span> <span class="string">&quot;<span class="variable">$env:TEMP</span>\pimp-my-terminal.ps1&quot;</span></span><br><span class="line">&amp; <span class="string">&quot;<span class="variable">$env:TEMP</span>\pimp-my-terminal.ps1&quot;</span></span><br></pre></td></tr></table></figure><p>The script will automatically install:</p><ul><li>✅ Oh My Posh via winget</li><li>✅ MesloLGM Nerd Font</li><li>✅ Terminal-Icons PowerShell module</li><li>✅ Cloud Native Azure theme</li><li>✅ PSReadLine enhancements</li><li>✅ Custom keyboard shortcuts</li></ul><h3 id="Step-3-Configure-Your-Terminal-Font"><a href="#Step-3-Configure-Your-Terminal-Font" class="headerlink" title="Step 3: Configure Your Terminal Font"></a>Step 3: Configure Your Terminal Font</h3><p>After the script completes, configure your terminal font:</p><p><strong>Windows Terminal:</strong></p><ol><li>Open Settings (<code>Ctrl + ,</code>)</li><li>Go to Profiles → Defaults → Appearance</li><li>Set Font face to: <code>MesloLGM Nerd Font</code></li><li>Save and restart terminal</li></ol><p><strong>VS Code:</strong></p><ol><li>Open Settings (<code>Ctrl + ,</code>)</li><li>Search for “terminal font”</li><li>Set Terminal › Integrated: Font Family to: <code>MesloLGM Nerd Font</code></li></ol><p><strong>Done!</strong> Open a new terminal and enjoy your beautiful, cloud native-ready prompt.</p><hr><h2 id="Understanding-Oh-My-Posh-The-Modern-Prompt-Engine"><a href="#Understanding-Oh-My-Posh-The-Modern-Prompt-Engine" class="headerlink" title="Understanding Oh My Posh: The Modern Prompt Engine"></a>Understanding Oh My Posh: The Modern Prompt Engine</h2><p>Before diving into the automation, it’s worth understanding what Oh My Posh brings to the table and why it’s become the de facto standard for PowerShell prompt customization.</p><span id="more"></span> <h3 id="What-Is-Oh-My-Posh"><a href="#What-Is-Oh-My-Posh" class="headerlink" title="What Is Oh My Posh?"></a>What Is Oh My Posh?</h3><p><a href="https://ohmyposh.dev/">Oh My Posh</a> is a custom prompt theme engine that works across multiple shells including PowerShell, Bash, Zsh, Fish, and more. Originally inspired by the popular Oh My Zsh project for Linux&#x2F;macOS, Oh My Posh brings the same level of customization and visual polish to Windows terminals while maintaining cross-platform compatibility.</p><p>The key differentiator of Oh My Posh is its <strong>segment-based architecture</strong>. Rather than being a monolithic prompt generator, it provides a framework for composing different “segments” of information into your prompt. Each segment represents a different piece of contextual information:</p><p><strong>Version Control Segments:</strong> Display Git branch names, ahead&#x2F;behind status, working directory changes, stash counts, and more. Supports multiple version control systems.</p><p><strong>Cloud Context Segments:</strong> Show your active Kubernetes cluster and namespace, AWS profile, Azure subscription, or Google Cloud project. Essential for multi-cloud development.</p><p><strong>Development Environment Segments:</strong> Display programming language versions (Python, Node.js, Go, .NET), virtual environment status, package manager context, and more.</p><p><strong>System Information Segments:</strong> Show current directory, execution time of the last command, error status, battery level, and system load.</p><h3 id="The-Nerd-Fonts-Requirement"><a href="#The-Nerd-Fonts-Requirement" class="headerlink" title="The Nerd Fonts Requirement"></a>The Nerd Fonts Requirement</h3><p>One aspect of Oh My Posh that initially confuses newcomers is the requirement for Nerd Fonts. Standard fonts don’t include the special glyphs and icons that Oh My Posh uses to display information compactly and beautifully.</p><p><strong>Nerd Fonts</strong> are patches of popular programming fonts that add thousands of additional glyphs from various icon sets including:</p><ul><li>Font Awesome icons</li><li>Devicons for programming languages</li><li>Octicons from GitHub</li><li>Material Design icons</li><li>Weather icons</li><li>Powerline extra symbols</li></ul><p>Without a Nerd Font installed, you’ll see blank squares or question marks instead of the beautiful icons that make Oh My Posh themes shine. This is why the installation script specifically handles font installation as a critical step.</p><h2 id="The-Solution-Automated-Setup-Script"><a href="#The-Solution-Automated-Setup-Script" class="headerlink" title="The Solution: Automated Setup Script"></a>The Solution: Automated Setup Script</h2><p>The core idea behind this automation is simple: replicate exactly what you’d do manually, but in a repeatable, version-controlled script that can be run on any Windows machine. The script handles five critical steps in sequence.</p><h3 id="What-The-Script-Installs-and-Configures"><a href="#What-The-Script-Installs-and-Configures" class="headerlink" title="What The Script Installs and Configures"></a>What The Script Installs and Configures</h3><p>The PowerShell script automates the installation and configuration of several components:</p><table><thead><tr><th>Step</th><th>Component</th><th>Purpose</th></tr></thead><tbody><tr><td>1</td><td><strong>Oh My Posh</strong></td><td>Prompt theme engine that makes your terminal beautiful and informative</td></tr><tr><td>2</td><td><strong>Meslo Nerd Font</strong></td><td>Special font with icons and glyphs required for theme display</td></tr><tr><td>3</td><td><strong>Terminal-Icons Module</strong></td><td>PowerShell module that shows file&#x2F;folder icons when you run <code>ls</code> or <code>Get-ChildItem</code></td></tr><tr><td>4</td><td><strong>Custom Theme</strong></td><td>Cloud Native Azure theme optimized for Kubernetes and Azure workflows</td></tr><tr><td>5</td><td><strong>PowerShell Profile</strong></td><td>Configures everything to load automatically on every terminal session</td></tr></tbody></table><h3 id="PowerShell-Profile-Enhancements"><a href="#PowerShell-Profile-Enhancements" class="headerlink" title="PowerShell Profile Enhancements"></a>PowerShell Profile Enhancements</h3><p>The script creates a comprehensive PowerShell profile (<code>$PROFILE</code>) that loads automatically every time you open a terminal:</p><table><thead><tr><th>Feature</th><th>Component</th><th>Benefit</th></tr></thead><tbody><tr><td><strong>Oh My Posh Theme</strong></td><td>Cloud Native Azure theme</td><td>Beautiful prompt with contextual information at a glance</td></tr><tr><td><strong>Terminal-Icons</strong></td><td>File type icons</td><td>Quickly identify file types: 📁 folders, 🐍 Python files, 📄 docs, etc.</td></tr><tr><td><strong>PSReadLine History</strong></td><td><code>↑</code> &#x2F; <code>↓</code> arrow keys</td><td>Search command history based on what you’ve typed</td></tr><tr><td><strong>PSReadLine Predictions</strong></td><td>Auto-suggestions</td><td>Shows grayed-out suggestions from history as you type</td></tr><tr><td><strong>F7 History Grid</strong></td><td><code>F7</code> key</td><td>Opens searchable popup of entire command history</td></tr><tr><td><strong>Dotnet Shortcuts</strong></td><td><code>Ctrl+Shift+B</code> &#x2F; <code>Ctrl+Shift+T</code></td><td>Instantly run <code>dotnet build</code> and <code>dotnet test</code></td></tr><tr><td><strong>Tab Completion</strong></td><td>Winget &amp; Dotnet</td><td>Intelligent tab completion for package managers</td></tr></tbody></table><h3 id="PSReadLine-Enhanced-Command-Line-Editing"><a href="#PSReadLine-Enhanced-Command-Line-Editing" class="headerlink" title="PSReadLine: Enhanced Command-Line Editing"></a>PSReadLine: Enhanced Command-Line Editing</h3><p><a href="https://docs.microsoft.com/en-us/powershell/module/psreadline/">PSReadLine</a> is a PowerShell module that dramatically improves command-line editing. The script configures these productivity features:</p><table><thead><tr><th>Feature</th><th>Shortcut</th><th>Description</th><th>Example Use Case</th></tr></thead><tbody><tr><td><strong>History Search</strong></td><td><code>↑</code> &#x2F; <code>↓</code></td><td>Searches history based on current input</td><td>Type <code>git</code> then press <code>↑</code> to cycle through all previous git commands</td></tr><tr><td><strong>Inline Predictions</strong></td><td><em>(automatic)</em></td><td>Shows grayed-out suggestions from history</td><td>Faster command entry - just press <code>→</code> to accept</td></tr><tr><td><strong>History Grid</strong></td><td><code>F7</code></td><td>Opens searchable popup of command history</td><td>Visual history browsing - select any command to insert</td></tr><tr><td><strong>Quick Build</strong></td><td><code>Ctrl+Shift+B</code></td><td>Instantly runs <code>dotnet build</code></td><td>One keystroke to build your .NET project</td></tr><tr><td><strong>Quick Test</strong></td><td><code>Ctrl+Shift+T</code></td><td>Instantly runs <code>dotnet test</code></td><td>One keystroke to run your test suite</td></tr></tbody></table><p>These features work together to minimize typing and maximize efficiency. For example, if you previously ran <code>kubectl get pods -n production</code>, you can start typing <code>kub</code> and press <code>↑</code> to find it immediately, or wait for the inline prediction to appear and press <code>→</code> to accept it.</p><h2 id="Exploring-Oh-My-Posh-Themes"><a href="#Exploring-Oh-My-Posh-Themes" class="headerlink" title="Exploring Oh My Posh Themes"></a>Exploring Oh My Posh Themes</h2><p>Before we dive into my custom theme, it’s worth exploring the extensive theme library that Oh My Posh provides out of the box. The project includes over 200 pre-built themes ranging from minimal single-line prompts to elaborate multi-line displays with extensive system information.</p><h3 id="Browsing-the-Theme-Gallery"><a href="#Browsing-the-Theme-Gallery" class="headerlink" title="Browsing the Theme Gallery"></a>Browsing the Theme Gallery</h3><p>Oh My Posh provides an excellent visual gallery where you can see screenshots of all available themes:</p><p>👉 <strong><a href="https://ohmyposh.dev/docs/themes">https://ohmyposh.dev/docs/themes</a></strong></p><p>The gallery is searchable and filterable, making it easy to find themes that match your preferences:</p><p><strong>Minimal Themes:</strong> Clean, single-line prompts with just essential information (agnoster, paradox, clean-detailed)</p><p><strong>Powerline Themes:</strong> Classic powerline-style prompts with angled segments and rich colors (powerlevel10k_rainbow, hotstick.minimal, jandedobbeleer)</p><p><strong>Cloud-Native Themes:</strong> Themes specifically designed for cloud development with Kubernetes and Azure context (cloud-native-azure, night-owl, atomic)</p><p><strong>Language-Specific Themes:</strong> Optimized for specific programming languages or frameworks (kushal, amro, lambda)</p><p><strong>Fun and Whimsical:</strong> Themes with unique character or personality (di4am0nd, emodipt-extend, iterm2)</p><h3 id="Testing-Themes-Before-Committing"><a href="#Testing-Themes-Before-Committing" class="headerlink" title="Testing Themes Before Committing"></a>Testing Themes Before Committing</h3><p>Oh My Posh makes it easy to preview themes before making them permanent:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Preview a specific theme temporarily</span></span><br><span class="line"><span class="built_in">oh</span><span class="literal">-my-posh</span> init pwsh <span class="literal">--config</span> <span class="string">&quot;<span class="variable">$env:POSH_THEMES_PATH</span>\jandedobbeleer.omp.json&quot;</span> | <span class="built_in">Invoke-Expression</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># View all available themes with Get-PoshThemes</span></span><br><span class="line"><span class="built_in">Get-PoshThemes</span></span><br></pre></td></tr></table></figure><p>This is particularly useful when setting up a new machine - you can quickly cycle through themes to find one that resonates with your workflow before committing to a permanent configuration.</p><h2 id="The-Cloud-Native-Azure-Theme-Design-Philosophy"><a href="#The-Cloud-Native-Azure-Theme-Design-Philosophy" class="headerlink" title="The Cloud Native Azure Theme: Design Philosophy"></a>The Cloud Native Azure Theme: Design Philosophy</h2><p>The script uses the official <strong><a href="https://github.com/JanDeDobbeleer/oh-my-posh/blob/main/themes/cloud-native-azure.omp.json">Cloud Native Azure</a></strong> theme from Oh My Posh. It’s specifically designed for developers working in the Azure and Kubernetes ecosystem.</p><h3 id="What-The-Theme-Displays"><a href="#What-The-Theme-Displays" class="headerlink" title="What The Theme Displays"></a>What The Theme Displays</h3><p>At a glance, your prompt shows comprehensive contextual information:</p><table><thead><tr><th>Segment</th><th>Icon</th><th>Information Displayed</th><th>Why It’s Useful</th></tr></thead><tbody><tr><td><strong>Session</strong></td><td>👤</td><td>Username &amp; hostname</td><td>Know which machine&#x2F;user you’re logged in as</td></tr><tr><td><strong>Path</strong></td><td>📁</td><td>Current directory</td><td>Always know where you are in the filesystem</td></tr><tr><td><strong>Git</strong></td><td>🔀</td><td>Branch, status, ahead&#x2F;behind</td><td>See uncommitted changes and unpushed commits at a glance</td></tr><tr><td><strong>Status</strong></td><td>✅&#x2F;❌</td><td>Last command success&#x2F;failure</td><td>Immediately know if your last command worked</td></tr><tr><td><strong>Kubernetes</strong></td><td>☸️</td><td>Cluster name &amp; namespace</td><td>Prevent running commands against the wrong cluster</td></tr><tr><td><strong>Azure</strong></td><td>☁️</td><td>Active subscription name</td><td>Know which Azure subscription is active</td></tr><tr><td><strong>Battery</strong></td><td>🔋</td><td>Charge level</td><td>Keep an eye on laptop battery during long sessions</td></tr><tr><td><strong>Time</strong></td><td>🕐</td><td>Current time</td><td>Timestamp your terminal sessions</td></tr></tbody></table><h3 id="Theme-Color-Scheme"><a href="#Theme-Color-Scheme" class="headerlink" title="Theme Color Scheme"></a>Theme Color Scheme</h3><p>The theme uses carefully chosen colors for instant visual recognition:</p><table><thead><tr><th>Segment</th><th>Color</th><th>Hex Code</th><th>Purpose</th></tr></thead><tbody><tr><td>Session</td><td>Purple</td><td><code>#c386f1</code></td><td>User context</td></tr><tr><td>Path</td><td>Pink</td><td><code>#ff479c</code></td><td>Navigation</td></tr><tr><td>Git (clean)</td><td>Yellow</td><td><code>#fffb38</code></td><td>Version control (clean state)</td></tr><tr><td>Git (dirty)</td><td>Orange</td><td><code>#FF9248</code></td><td>Version control (uncommitted changes)</td></tr><tr><td>Kubernetes</td><td>Yellow</td><td><code>#ebcc34</code></td><td>Cloud infrastructure</td></tr><tr><td>Azure</td><td>Light Blue</td><td><code>#9ec3f0</code></td><td>Cloud subscription</td></tr><tr><td>Status (success)</td><td>Teal</td><td><code>#2e9599</code></td><td>Success indicator</td></tr><tr><td>Status (error)</td><td>Red</td><td><code>#f1184c</code></td><td>Error indicator</td></tr></tbody></table><p>While Oh My Posh ships with many excellent themes, this one is specifically optimized for cloud native development workflows. The design philosophy centers on three core principles: </p><h3 id="Priority-Information-First"><a href="#Priority-Information-First" class="headerlink" title="Priority Information First"></a>Priority Information First</h3><p>The most critical contextual information - path, Git status, Kubernetes context, and Azure subscription - is always visible without requiring any additional commands. This eliminates the “where am I?” moment that costs seconds of mental processing dozens of times per day.</p><h3 id="Visual-Hierarchy-Through-Color"><a href="#Visual-Hierarchy-Through-Color" class="headerlink" title="Visual Hierarchy Through Color"></a>Visual Hierarchy Through Color</h3><p>The theme uses the official Azure brand colors strategically:</p><ul><li><strong>Azure Blue (#0078D4)</strong> for operating system and Azure context</li><li><strong>Cyan (#00A4EF)</strong> for file path navigation  </li><li><strong>Green (#7FBA00)</strong> for Git status with dynamic background colors indicating repository state</li><li><strong>Kubernetes Blue (#326CE5)</strong> for cluster context</li><li><strong>Gray (#505050)</strong> for secondary information like execution time</li></ul><p>This color-coding creates instant visual recognition - your eyes learn to find specific information based on color alone.</p><h3 id="Cloud-Native-Workflow-Optimization"><a href="#Cloud-Native-Workflow-Optimization" class="headerlink" title="Cloud Native Workflow Optimization"></a>Cloud Native Workflow Optimization</h3><p>The theme specifically addresses common scenarios in cloud native development:</p><p><strong>Multi-Cluster Scenarios:</strong> When working with development, staging, and production Kubernetes clusters, the prominent cluster name prevents accidentally running destructive commands in production.</p><p><strong>Multi-Subscription Development:</strong> Azure developers often switch between client subscriptions, personal subscriptions, or different environments. The Azure segment shows which subscription is active to prevent resource creation in the wrong tenant.</p><p><strong>Git-Heavy Workflows:</strong> With branch name, ahead&#x2F;behind indicators, working directory changes, staging status, and stash count all visible, you have complete Git situational awareness without running <code>git status</code>.</p><h2 id="Theme-Anatomy-Understanding-the-Configuration"><a href="#Theme-Anatomy-Understanding-the-Configuration" class="headerlink" title="Theme Anatomy: Understanding the Configuration"></a>Theme Anatomy: Understanding the Configuration</h2><p>The Cloud Native Azure theme is defined as a JSON configuration file that Oh My Posh parses to render your prompt. Understanding this structure allows you to customize the theme to your specific needs.</p><h3 id="Block-Structure"><a href="#Block-Structure" class="headerlink" title="Block Structure"></a>Block Structure</h3><p>The theme uses Oh My Posh’s block system to organize segments into logical groupings:</p><p><strong>Block 1 (Left-Aligned):</strong> Contains the primary context segments that appear on the main prompt line:</p><ul><li>Operating System indicator</li><li>Current path with folder-style display</li><li>Git repository information with status</li></ul><p><strong>Block 2 (Right-Aligned):</strong> Displays cloud and infrastructure context:</p><ul><li>Kubernetes cluster and namespace</li><li>Azure subscription name</li><li>Command execution time</li></ul><p><strong>Block 3 (Left-Aligned, New Line):</strong> Simple prompt character for command entry</p><p>This three-block structure creates a balanced layout where essential information doesn’t crowd the area where you’re typing commands, while cloud context is visible but not intrusive.</p><h3 id="Segment-Configuration-Details"><a href="#Segment-Configuration-Details" class="headerlink" title="Segment Configuration Details"></a>Segment Configuration Details</h3><p>Each segment in the theme has specific configuration properties that control its behavior and appearance:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;kubectl&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;style&quot;</span><span class="punctuation">:</span> <span class="string">&quot;powerline&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;powerline_symbol&quot;</span><span class="punctuation">:</span> <span class="string">&quot;\ue0b2&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;invert_powerline&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;foreground&quot;</span><span class="punctuation">:</span> <span class="string">&quot;#ffffff&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;background&quot;</span><span class="punctuation">:</span> <span class="string">&quot;#326CE5&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;template&quot;</span><span class="punctuation">:</span> <span class="string">&quot; \ufd31 &#123;&#123; .Context &#125;&#125;&#123;&#123; if .Namespace &#125;&#125; :: &#123;&#123; .Namespace &#125;&#125;&#123;&#123; end &#125;&#125; &quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;properties&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;parse_kubeconfig&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>Breaking down this Kubernetes segment:</p><p><strong>Type Property:</strong> Specifies which Oh My Posh segment provider to use (<code>kubectl</code> for Kubernetes context)</p><p><strong>Style and Powerline Symbol:</strong> Creates the angled transitions between segments that give the prompt its distinctive look</p><p><strong>Color Configuration:</strong> Foreground and background colors using hex codes for precise brand matching</p><p><strong>Template String:</strong> Defines what information to display using Go template syntax with conditional logic</p><p><strong>Properties:</strong> Segment-specific settings like <code>parse_kubeconfig</code> which tells the segment to read from your kubectl config file</p><h3 id="Dynamic-Git-Status-Indicators"><a href="#Dynamic-Git-Status-Indicators" class="headerlink" title="Dynamic Git Status Indicators"></a>Dynamic Git Status Indicators</h3><p>The Git segment includes sophisticated logic for changing appearance based on repository state:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;background_templates&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line">  <span class="string">&quot;&#123;&#123; if or (.Working.Changed) (.Staging.Changed) &#125;&#125;#FFB900&#123;&#123; end &#125;&#125;&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="string">&quot;&#123;&#123; if and (gt .Ahead 0) (gt .Behind 0) &#125;&#125;#F25022&#123;&#123; end &#125;&#125;&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="string">&quot;&#123;&#123; if gt .Ahead 0 &#125;&#125;#B4009E&#123;&#123; end &#125;&#125;&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="string">&quot;&#123;&#123; if gt .Behind 0 &#125;&#125;#F25022&#123;&#123; end &#125;&#125;&quot;</span></span><br><span class="line"><span class="punctuation">]</span></span><br></pre></td></tr></table></figure><p>These background templates create visual warnings:</p><ul><li><strong>Yellow (#FFB900):</strong> Uncommitted changes in working directory or staging area</li><li><strong>Orange&#x2F;Red (#F25022):</strong> Branch is behind the remote (need to pull)</li><li><strong>Purple (#B4009E):</strong> Branch is ahead of remote (need to push)</li></ul><p>This color-coding provides instant feedback about repository state without reading the status text.</p><h2 id="The-Complete-Automation-Script"><a href="#The-Complete-Automation-Script" class="headerlink" title="The Complete Automation Script"></a>The Complete Automation Script</h2><p>The full automation script is maintained in a GitHub repository for easy access and version control. You can find the complete, up-to-date script here:</p><p><strong>📜 Script Location:</strong> <a href="https://github.com/Ricky-G/script-library/blob/main/pimp-my-terminal.ps1">https://github.com/Ricky-G/script-library/blob/main/pimp-my-terminal.ps1</a></p><p>The script handles the entire setup process including:</p><ul><li>Oh My Posh installation via winget</li><li>Nerd Font (MesloLGM) installation</li><li>Terminal-Icons PowerShell module installation</li><li>Cloud Native Azure theme configuration</li><li>PowerShell profile setup with PSReadLine enhancements</li><li>Custom keyboard shortcuts for dotnet commands</li><li>Intelligent tab completion for winget and dotnet CLI</li></ul><h3 id="Quick-Start"><a href="#Quick-Start" class="headerlink" title="Quick Start"></a>Quick Start</h3><p>To run the script, open PowerShell as Administrator and execute:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Download and run the script directly</span></span><br><span class="line"><span class="built_in">Invoke-WebRequest</span> <span class="literal">-Uri</span> <span class="string">&quot;https://raw.githubusercontent.com/Ricky-G/script-library/main/pimp-my-terminal.ps1&quot;</span> <span class="literal">-OutFile</span> <span class="string">&quot;<span class="variable">$env:TEMP</span>\pimp-my-terminal.ps1&quot;</span></span><br><span class="line">&amp; <span class="string">&quot;<span class="variable">$env:TEMP</span>\pimp-my-terminal.ps1&quot;</span></span><br></pre></td></tr></table></figure><p>Or if you’ve cloned the repository locally:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Run from local clone</span></span><br><span class="line">.\pimp<span class="literal">-my-terminal</span>.ps1</span><br></pre></td></tr></table></figure><h2 id="Script-Breakdown-Key-Components"><a href="#Script-Breakdown-Key-Components" class="headerlink" title="Script Breakdown: Key Components"></a>Script Breakdown: Key Components</h2><p>The automation script includes several critical sections that ensure a smooth, repeatable setup process. Let’s examine the key components:</p><h3 id="Administrative-Privileges-Requirement"><a href="#Administrative-Privileges-Requirement" class="headerlink" title="Administrative Privileges Requirement"></a>Administrative Privileges Requirement</h3><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">#Requires -RunAsAdministrator</span></span><br></pre></td></tr></table></figure><p>The script requires administrative privileges for two reasons:</p><ol><li><strong>Font Installation:</strong> Installing system-wide fonts requires elevated permissions to write to <code>C:\Windows\Fonts</code></li><li><strong>Winget Operations:</strong> While winget can run without admin rights, some installations require elevation for proper PATH configuration</li></ol><p>If you run the script without elevation, PowerShell will automatically prompt for admin credentials before execution.</p><h3 id="Winget-Installation-with-Accept-Flags"><a href="#Winget-Installation-with-Accept-Flags" class="headerlink" title="Winget Installation with Accept Flags"></a>Winget Installation with Accept Flags</h3><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">winget install JanDeDobbeleer.OhMyPosh <span class="literal">-s</span> winget <span class="literal">--accept-package-agreements</span> <span class="literal">--accept-source-agreements</span></span><br></pre></td></tr></table></figure><p>The <code>--accept-package-agreements</code> and <code>--accept-source-agreements</code> flags automate the acceptance of license terms, enabling the script to run non-interactively. This is crucial for CI&#x2F;CD scenarios or remote machine setup where manual interaction isn’t possible.</p><p>The <code>-s winget</code> parameter explicitly specifies the winget repository, preventing potential conflicts if you have multiple package sources configured.</p><h3 id="Environment-Path-Refresh"><a href="#Environment-Path-Refresh" class="headerlink" title="Environment Path Refresh"></a>Environment Path Refresh</h3><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable">$env:Path</span> = [<span class="type">System.Environment</span>]::GetEnvironmentVariable(<span class="string">&quot;Path&quot;</span>,<span class="string">&quot;Machine&quot;</span>) + <span class="string">&quot;;&quot;</span> + [<span class="type">System.Environment</span>]::GetEnvironmentVariable(<span class="string">&quot;Path&quot;</span>,<span class="string">&quot;User&quot;</span>)</span><br></pre></td></tr></table></figure><p>This is a critical step that’s often overlooked in automation scripts. When winget installs Oh My Posh, it modifies the system PATH variable, but PowerShell sessions don’t automatically refresh their environment. This line explicitly reloads the PATH, making the <code>oh-my-posh</code> command immediately available without requiring a terminal restart.</p><p>Without this refresh, the subsequent <code>oh-my-posh font install</code> command would fail with a “command not found” error.</p><h3 id="Terminal-Icons-Module-Installation"><a href="#Terminal-Icons-Module-Installation" class="headerlink" title="Terminal-Icons Module Installation"></a>Terminal-Icons Module Installation</h3><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">Install-Module</span> <span class="literal">-Name</span> Terminal<span class="literal">-Icons</span> <span class="literal">-Repository</span> PSGallery <span class="literal">-Force</span></span><br></pre></td></tr></table></figure><p>The script also installs the Terminal-Icons module, which adds beautiful file type icons to your directory listings. When you run <code>ls</code> or <code>Get-ChildItem</code>, you’ll see:</p><ul><li>📁 Folder icons for directories</li><li>🐍 Python icon for .py files</li><li>📄 Document icons for text files</li><li>⚙️ Config icons for .json, .xml, .yaml files</li><li>And many more language-specific icons</li></ul><h3 id="Nerd-Font-Installation"><a href="#Nerd-Font-Installation" class="headerlink" title="Nerd Font Installation"></a>Nerd Font Installation</h3><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">oh</span><span class="literal">-my-posh</span> font install Meslo</span><br></pre></td></tr></table></figure><p>Oh My Posh includes a built-in font installer that downloads and installs Nerd Fonts from their GitHub releases. The <code>Meslo</code> font (MesloLGM Nerd Font) is recommended because:</p><ul><li>Excellent readability at small and large sizes</li><li>Wide character coverage including all necessary glyphs</li><li>Good distinction between similar characters (1, l, I, 0, O)</li><li>Proper line height and spacing for terminal use</li></ul><p>Alternative Nerd Fonts you might consider:</p><ul><li><code>CascadiaCode</code> - Microsoft’s open-source programming font</li><li><code>FiraCode</code> - Popular font with extensive ligature support  </li><li><code>JetBrainsMono</code> - Optimized for long coding sessions</li><li><code>Hack</code> - Minimal, clean appearance</li></ul><h3 id="Profile-Configuration-with-Idempotency"><a href="#Profile-Configuration-with-Idempotency" class="headerlink" title="Profile Configuration with Idempotency"></a>Profile Configuration with Idempotency</h3><p>The script checks if Oh My Posh is already configured before making changes, allowing safe repeated execution:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (<span class="built_in">Test-Path</span> <span class="variable">$PROFILE</span>) &#123;</span><br><span class="line">    <span class="variable">$existingProfile</span> = <span class="built_in">Get-Content</span> <span class="variable">$PROFILE</span> <span class="literal">-Raw</span></span><br><span class="line">    <span class="keyword">if</span> (<span class="variable">$existingProfile</span> <span class="operator">-notmatch</span> <span class="string">&quot;oh-my-posh&quot;</span>) &#123;</span><br><span class="line">        <span class="built_in">Add-Content</span> <span class="literal">-Path</span> <span class="variable">$PROFILE</span> <span class="literal">-Value</span> <span class="variable">$profileContent</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>This idempotency is essential for:</p><ul><li>Updating the theme configuration without duplicating profile entries</li><li>Re-running the script after failed installations</li><li>Using the script in configuration management systems</li></ul><h2 id="Step-by-Step-Implementation-Guide"><a href="#Step-by-Step-Implementation-Guide" class="headerlink" title="Step-by-Step Implementation Guide"></a>Step-by-Step Implementation Guide</h2><h3 id="Prerequisites-and-Preparation"><a href="#Prerequisites-and-Preparation" class="headerlink" title="Prerequisites and Preparation"></a>Prerequisites and Preparation</h3><p>Before running the script, ensure you have:<br>5. Click <strong>Appearance</strong> in the sub-menu<br>6. Under <strong>Font face</strong>, select <code>MesloLGM Nerd Font</code><br>7. Optionally adjust <strong>Font size</strong> (I recommend 10-11pt for readability)<br>8. Click <strong>Save</strong></p><p><strong>Pro Tip:</strong> You can also configure this per-profile (PowerShell, Command Prompt, Ubuntu, etc.) if you want different fonts for different shells. However, setting it in Defaults applies to all profiles consistently.</p><h4 id="Visual-Studio-Code-Terminal-Configuration"><a href="#Visual-Studio-Code-Terminal-Configuration" class="headerlink" title="Visual Studio Code Terminal Configuration"></a>Visual Studio Code Terminal Configuration</h4><p>If you spend significant time in VS Code’s integrated terminal, you’ll want the same beautiful prompt there:</p><ol><li>Open VS Code</li><li>Press <code>Ctrl + ,</code> to open Settings</li><li>Search for <code>terminal font</code></li><li>Find <strong>Terminal › Integrated: Font Family</strong></li><li>Enter: <code>MesloLGM Nerd Font</code></li><li>Restart any open terminal instances</li></ol><p>Alternatively, edit your <code>settings.json</code> directly:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;terminal.integrated.fontFamily&quot;</span><span class="punctuation">:</span> <span class="string">&quot;MesloLGM Nerd Font&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;terminal.integrated.fontSize&quot;</span><span class="punctuation">:</span> <span class="number">11</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h4 id="Verifying-Font-Installation"><a href="#Verifying-Font-Installation" class="headerlink" title="Verifying Font Installation"></a>Verifying Font Installation</h4><p>After configuring your terminal, restart it and open a new PowerShell session. You should see the themed prompt with proper icons. If you see squares, question marks, or missing characters, the font isn’t properly applied.</p><p>Common fixes:</p><ul><li>Ensure you spelled the font name exactly: <code>MesloLGM Nerd Font</code> (not MesloLGM NF)</li><li>Try restarting Windows Terminal completely (close all windows)</li><li>Verify the font appears in Windows Settings → Personalization → Fonts</li></ul><h2 id="Advanced-Customization-Options"><a href="#Advanced-Customization-Options" class="headerlink" title="Advanced Customization Options"></a>Advanced Customization Options</h2><h3 id="Modifying-the-Theme"><a href="#Modifying-the-Theme" class="headerlink" title="Modifying the Theme"></a>Modifying the Theme</h3><p>The beauty of this automated setup is that your theme is now stored as a JSON file at <code>$HOME\.config\ohmyposh\cloud-native-azure.omp.json</code>. You can modify this file to customize the prompt to your preferences.</p><h4 id="Adding-Additional-Segments"><a href="#Adding-Additional-Segments" class="headerlink" title="Adding Additional Segments"></a>Adding Additional Segments</h4><p>Want to display your Python virtual environment or Node.js version? Add segments to the appropriate block:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;type&quot;</span><span class="punctuation">:</span> <span class="string">&quot;python&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;style&quot;</span><span class="punctuation">:</span> <span class="string">&quot;powerline&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;powerline_symbol&quot;</span><span class="punctuation">:</span> <span class="string">&quot;\ue0b0&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;foreground&quot;</span><span class="punctuation">:</span> <span class="string">&quot;#ffffff&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;background&quot;</span><span class="punctuation">:</span> <span class="string">&quot;#306998&quot;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;template&quot;</span><span class="punctuation">:</span> <span class="string">&quot; \ue235 &#123;&#123; if .Venv &#125;&#125;&#123;&#123; .Venv &#125;&#125; &#123;&#123; end &#125;&#125;&#123;&#123; .Full &#125;&#125; &quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>Available segment types include: <code>aws</code>, <code>battery</code>, <code>cmake</code>, <code>dart</code>, <code>docker</code>, <code>dotnet</code>, <code>elixir</code>, <code>flutter</code>, <code>go</code>, <code>java</code>, <code>julia</code>, <code>kotlin</code>, <code>lua</code>, <code>node</code>, <code>perl</code>, <code>php</code>, <code>python</code>, <code>ruby</code>, <code>rust</code>, <code>scala</code>, <code>swift</code>, <code>terraform</code>, and many more.</p><h4 id="Changing-Colors"><a href="#Changing-Colors" class="headerlink" title="Changing Colors"></a>Changing Colors</h4><p>Modify the <code>foreground</code> and <code>background</code> properties to use your preferred color scheme:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;foreground&quot;</span><span class="punctuation">:</span> <span class="string">&quot;#ffffff&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">&quot;background&quot;</span><span class="punctuation">:</span> <span class="string">&quot;#your-hex-color&quot;</span></span><br></pre></td></tr></table></figure><p>You can also use color definitions for dark&#x2F;light terminal themes:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;foreground&quot;</span><span class="punctuation">:</span> <span class="string">&quot;p:white&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">&quot;background&quot;</span><span class="punctuation">:</span> <span class="string">&quot;p:blue&quot;</span></span><br></pre></td></tr></table></figure><h4 id="Adjusting-Git-Status-Indicators"><a href="#Adjusting-Git-Status-Indicators" class="headerlink" title="Adjusting Git Status Indicators"></a>Adjusting Git Status Indicators</h4><p>Customize which Git information displays by modifying the template:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;template&quot;</span><span class="punctuation">:</span> <span class="string">&quot; &#123;&#123; .HEAD &#125;&#125;&#123;&#123; if .BranchStatus &#125;&#125; &#123;&#123; .BranchStatus &#125;&#125;&#123;&#123; end &#125;&#125;&#123;&#123; if .Working.Changed &#125;&#125; \uf044 &#123;&#123; .Working.String &#125;&#125;&#123;&#123; end &#125;&#125; &quot;</span></span><br></pre></td></tr></table></figure><p>Remove sections you don’t need or add additional information like:</p><ul><li><code>&#123;&#123; .UpstreamIcon &#125;&#125;</code> - Shows if branch has an upstream</li><li><code>&#123;&#123; .StashCount &#125;&#125;</code> - Number of stashed changes</li><li><code>&#123;&#123; .WorktreeCount &#125;&#125;</code> - Number of worktrees</li></ul><h3 id="Creating-Multiple-Theme-Configurations"><a href="#Creating-Multiple-Theme-Configurations" class="headerlink" title="Creating Multiple Theme Configurations"></a>Creating Multiple Theme Configurations</h3><p>You might want different themes for different scenarios - perhaps a minimal theme for screen recordings or presentations, and a detailed theme for daily work.</p><p>Create multiple theme files:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Create a minimal theme</span></span><br><span class="line"><span class="variable">$minimalTheme</span> = <span class="string">@&#x27;</span></span><br><span class="line"><span class="string">&#123;</span></span><br><span class="line"><span class="string">  &quot;$schema&quot;: &quot;https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json&quot;,</span></span><br><span class="line"><span class="string">  &quot;version&quot;: 2,</span></span><br><span class="line"><span class="string">  &quot;final_space&quot;: true,</span></span><br><span class="line"><span class="string">  &quot;blocks&quot;: [</span></span><br><span class="line"><span class="string">    &#123;</span></span><br><span class="line"><span class="string">      &quot;type&quot;: &quot;prompt&quot;,</span></span><br><span class="line"><span class="string">      &quot;alignment&quot;: &quot;left&quot;,</span></span><br><span class="line"><span class="string">      &quot;segments&quot;: [</span></span><br><span class="line"><span class="string">        &#123;</span></span><br><span class="line"><span class="string">          &quot;type&quot;: &quot;path&quot;,</span></span><br><span class="line"><span class="string">          &quot;style&quot;: &quot;plain&quot;,</span></span><br><span class="line"><span class="string">          &quot;foreground&quot;: &quot;#00A4EF&quot;,</span></span><br><span class="line"><span class="string">          &quot;template&quot;: &quot;&#123;&#123; .Path &#125;&#125; &quot;</span></span><br><span class="line"><span class="string">        &#125;,</span></span><br><span class="line"><span class="string">        &#123;</span></span><br><span class="line"><span class="string">          &quot;type&quot;: &quot;git&quot;,</span></span><br><span class="line"><span class="string">          &quot;style&quot;: &quot;plain&quot;,</span></span><br><span class="line"><span class="string">          &quot;foreground&quot;: &quot;#7FBA00&quot;,</span></span><br><span class="line"><span class="string">          &quot;template&quot;: &quot;&#123;&#123; .HEAD &#125;&#125; &quot;</span></span><br><span class="line"><span class="string">        &#125;,</span></span><br><span class="line"><span class="string">        &#123;</span></span><br><span class="line"><span class="string">          &quot;type&quot;: &quot;text&quot;,</span></span><br><span class="line"><span class="string">          &quot;style&quot;: &quot;plain&quot;,</span></span><br><span class="line"><span class="string">          &quot;foreground&quot;: &quot;#0078D4&quot;,</span></span><br><span class="line"><span class="string">          &quot;template&quot;: &quot;\u276f &quot;</span></span><br><span class="line"><span class="string">        &#125;</span></span><br><span class="line"><span class="string">      ]</span></span><br><span class="line"><span class="string">    &#125;</span></span><br><span class="line"><span class="string">  ]</span></span><br><span class="line"><span class="string">&#125;</span></span><br><span class="line"><span class="string">&#x27;@</span></span><br><span class="line"></span><br><span class="line"><span class="variable">$minimalTheme</span> | <span class="built_in">Out-File</span> <span class="literal">-FilePath</span> <span class="string">&quot;<span class="variable">$HOME</span>\.config\ohmyposh\minimal.omp.json&quot;</span> <span class="literal">-Encoding</span> utf8</span><br></pre></td></tr></table></figure><p>Switch themes by modifying your profile or creating PowerShell functions:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Add to your PowerShell profile</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Set-CloudNativeTheme</span></span> &#123;</span><br><span class="line">    <span class="built_in">oh</span><span class="literal">-my-posh</span> init pwsh <span class="literal">--config</span> <span class="string">&quot;<span class="variable">$HOME</span>\.config\ohmyposh\cloud-native-azure.omp.json&quot;</span> | <span class="built_in">Invoke-Expression</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Set-MinimalTheme</span></span> &#123;</span><br><span class="line">    <span class="built_in">oh</span><span class="literal">-my-posh</span> init pwsh <span class="literal">--config</span> <span class="string">&quot;<span class="variable">$HOME</span>\.config\ohmyposh\minimal.omp.json&quot;</span> | <span class="built_in">Invoke-Expression</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Conditional-Theme-Loading"><a href="#Conditional-Theme-Loading" class="headerlink" title="Conditional Theme Loading"></a>Conditional Theme Loading</h3><p>Load different themes based on context - for example, use a detailed theme on your workstation but a minimal theme when SSH’d into servers:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># In your PowerShell profile</span></span><br><span class="line"><span class="keyword">if</span> (<span class="variable">$env:SSH_CONNECTION</span>) &#123;</span><br><span class="line">    <span class="built_in">oh</span><span class="literal">-my-posh</span> init pwsh <span class="literal">--config</span> <span class="string">&quot;<span class="variable">$HOME</span>\.config\ohmyposh\minimal.omp.json&quot;</span> | <span class="built_in">Invoke-Expression</span></span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="built_in">oh</span><span class="literal">-my-posh</span> init pwsh <span class="literal">--config</span> <span class="string">&quot;<span class="variable">$HOME</span>\.config\ohmyposh\cloud-native-azure.omp.json&quot;</span> | <span class="built_in">Invoke-Expression</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Keyboard-Shortcuts-Reference"><a href="#Keyboard-Shortcuts-Reference" class="headerlink" title="Keyboard Shortcuts Reference"></a>Keyboard Shortcuts Reference</h2><p>After setup, you’ll have enhanced keyboard shortcuts available in PowerShell thanks to PSReadLine configuration:</p><table><thead><tr><th>Shortcut</th><th>Action</th><th>Description</th></tr></thead><tbody><tr><td><code>↑</code></td><td>History Search Backward</td><td>Searches command history based on what you’ve already typed</td></tr><tr><td><code>↓</code></td><td>History Search Forward</td><td>Continues searching forward through matching history</td></tr><tr><td><code>F7</code></td><td>History Grid View</td><td>Opens a searchable popup grid of your entire command history</td></tr><tr><td><code>Ctrl+Shift+B</code></td><td>Dotnet Build</td><td>Instantly runs <code>dotnet build</code> in the current directory</td></tr><tr><td><code>Ctrl+Shift+T</code></td><td>Dotnet Test</td><td>Instantly runs <code>dotnet test</code> for your test suite</td></tr><tr><td><code>Tab</code></td><td>Smart Completion</td><td>Context-aware tab completion for winget, dotnet, and more</td></tr><tr><td><code>→</code></td><td>Accept Prediction</td><td>Accepts the grayed-out inline prediction from history</td></tr><tr><td><code>Ctrl+RightArrow</code></td><td>Accept Next Word</td><td>Accepts only the next word from the prediction</td></tr></tbody></table><h3 id="Using-History-Search-Effectively"><a href="#Using-History-Search-Effectively" class="headerlink" title="Using History Search Effectively"></a>Using History Search Effectively</h3><p>The history search feature is particularly powerful for repetitive commands:</p><p><strong>Example 1 - Git Commands:</strong></p><ul><li>Type <code>git</code> and press <code>↑</code></li><li>Cycles through all previous git commands</li><li>Much faster than retyping or searching entire history</li></ul><p><strong>Example 2 - Kubectl Commands:</strong></p><ul><li>Type <code>kubectl get pods -n</code> and press <code>↑</code>  </li><li>Finds all previous kubectl commands starting with that pattern</li><li>Quickly switch between namespaces you’ve used before</li></ul><p><strong>Example 3 - Complex Commands:</strong></p><ul><li>Type the first few characters of a long command</li><li>Press <code>↑</code> to find it immediately</li><li>No need to remember the entire command syntax</li></ul><h3 id="PSReadLine-Prediction-Modes"><a href="#PSReadLine-Prediction-Modes" class="headerlink" title="PSReadLine Prediction Modes"></a>PSReadLine Prediction Modes</h3><p>The script configures PSReadLine to show predictions as you type:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Predictions appear grayed out based on your command history</span></span><br><span class="line"><span class="built_in">PS</span>&gt; git checkout ma|ain     <span class="comment"># Gray text shows prediction</span></span><br><span class="line">                  ↑</span><br><span class="line">            Press → to accept</span><br></pre></td></tr></table></figure><p>This feature learns from your command patterns and becomes more useful over time as you build up command history.</p><h2 id="Customization-and-Theme-Switching"><a href="#Customization-and-Theme-Switching" class="headerlink" title="Customization and Theme Switching"></a>Customization and Theme Switching</h2><h3 id="Switching-to-a-Different-Theme"><a href="#Switching-to-a-Different-Theme" class="headerlink" title="Switching to a Different Theme"></a>Switching to a Different Theme</h3><p>To use a different Oh My Posh theme after installation, edit your PowerShell profile:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">notepad <span class="variable">$PROFILE</span></span><br></pre></td></tr></table></figure><p>Replace the theme URL with any theme from the <a href="https://ohmyposh.dev/docs/themes">themes gallery</a>:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Example: Switch to &#x27;agnoster&#x27; theme</span></span><br><span class="line"><span class="built_in">oh</span><span class="literal">-my-posh</span> init pwsh <span class="literal">--config</span> <span class="string">&#x27;https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/agnoster.omp.json&#x27;</span> | <span class="built_in">Invoke-Expression</span></span><br></pre></td></tr></table></figure><h3 id="Adding-Custom-Keyboard-Shortcuts"><a href="#Adding-Custom-Keyboard-Shortcuts" class="headerlink" title="Adding Custom Keyboard Shortcuts"></a>Adding Custom Keyboard Shortcuts</h3><p>You can add more PSReadLine shortcuts to your profile:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Example: Ctrl+Shift+R for dotnet run</span></span><br><span class="line"><span class="built_in">Set-PSReadLineKeyHandler</span> <span class="literal">-Key</span> Ctrl+Shift+<span class="built_in">r</span> <span class="literal">-ScriptBlock</span> &#123;</span><br><span class="line">    [<span class="type">Microsoft.PowerShell.PSConsoleReadLine</span>]::RevertLine()</span><br><span class="line">    [<span class="type">Microsoft.PowerShell.PSConsoleReadLine</span>]::Insert(<span class="string">&quot;dotnet run&quot;</span>)</span><br><span class="line">    [<span class="type">Microsoft.PowerShell.PSConsoleReadLine</span>]::AcceptLine()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment"># Example: Ctrl+Shift+G for git status</span></span><br><span class="line"><span class="built_in">Set-PSReadLineKeyHandler</span> <span class="literal">-Key</span> Ctrl+Shift+g <span class="literal">-ScriptBlock</span> &#123;</span><br><span class="line">    [<span class="type">Microsoft.PowerShell.PSConsoleReadLine</span>]::RevertLine()</span><br><span class="line">    [<span class="type">Microsoft.PowerShell.PSConsoleReadLine</span>]::Insert(<span class="string">&quot;git status&quot;</span>)</span><br><span class="line">    [<span class="type">Microsoft.PowerShell.PSConsoleReadLine</span>]::AcceptLine()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Troubleshooting-Common-Issues"><a href="#Troubleshooting-Common-Issues" class="headerlink" title="Troubleshooting Common Issues"></a>Troubleshooting Common Issues</h2><h3 id="Oh-My-Posh-Command-Not-Found"><a href="#Oh-My-Posh-Command-Not-Found" class="headerlink" title="Oh My Posh Command Not Found"></a>Oh My Posh Command Not Found</h3><p><strong>Symptom:</strong> After running the script, <code>oh-my-posh</code> command isn’t recognized.</p><p><strong>Causes and Fixes:</strong></p><ol><li><p><strong>PATH not refreshed:</strong> Close and reopen your terminal, or run:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable">$env:Path</span> = [<span class="type">System.Environment</span>]::GetEnvironmentVariable(<span class="string">&quot;Path&quot;</span>,<span class="string">&quot;Machine&quot;</span>) + <span class="string">&quot;;&quot;</span> + [<span class="type">System.Environment</span>]::GetEnvironmentVariable(<span class="string">&quot;Path&quot;</span>,<span class="string">&quot;User&quot;</span>)</span><br></pre></td></tr></table></figure></li><li><p><strong>Winget installation failed:</strong> Check if Oh My Posh was actually installed:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">winget list | <span class="built_in">Select-String</span> <span class="string">&quot;OhMyPosh&quot;</span></span><br></pre></td></tr></table></figure><p>If not present, manually install:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">winget install JanDeDobbeleer.OhMyPosh</span><br></pre></td></tr></table></figure></li><li><p><strong>Installation location issues:</strong> Verify the executable location:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">Get-Command</span> <span class="built_in">oh</span><span class="literal">-my-posh</span></span><br></pre></td></tr></table></figure></li></ol><h3 id="Script-Execution-Is-Disabled"><a href="#Script-Execution-Is-Disabled" class="headerlink" title="Script Execution Is Disabled"></a>Script Execution Is Disabled</h3><p><strong>Symptom:</strong> Error message “running scripts is disabled on this system”</p><p><strong>Fix:</strong></p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">Set-ExecutionPolicy</span> <span class="literal">-ExecutionPolicy</span> RemoteSigned <span class="literal">-Scope</span> CurrentUser</span><br></pre></td></tr></table></figure><p>When prompted, type <code>Y</code> and press Enter. This policy allows locally-created scripts to run while maintaining security for downloaded scripts.</p><h3 id="Font-Rendering-Issues"><a href="#Font-Rendering-Issues" class="headerlink" title="Font Rendering Issues"></a>Font Rendering Issues</h3><p><strong>Symptom:</strong> Boxes, question marks, or missing icons in your prompt.</p><p><strong>Fixes:</strong></p><ol><li><p><strong>Verify font installation:</strong></p><ul><li>Open Windows Settings → Personalization → Fonts</li><li>Search for “MesloLGM”</li><li>If not present, manually run: <code>oh-my-posh font install Meslo</code></li></ul></li><li><p><strong>Correct font name in terminal:</strong></p><ul><li>The exact font name is <code>MesloLGM Nerd Font</code></li><li>Some terminals are case-sensitive</li><li>Don’t use abbreviations like “MesloLGM NF”</li></ul></li><li><p><strong>Terminal compatibility:</strong></p><ul><li>Windows Terminal, VS Code, and modern terminals support Nerd Fonts well</li><li>Legacy terminals (cmd.exe window) may have poor support</li><li>Consider upgrading to Windows Terminal</li></ul></li><li><p><strong>Manual font installation:</strong><br>If automatic installation fails, download manually:</p><ul><li>Go to <a href="https://github.com/ryanoasis/nerd-fonts/releases">https://github.com/ryanoasis/nerd-fonts/releases</a></li><li>Download <code>Meslo.zip</code></li><li>Extract and right-click the <code>.ttf</code> files</li><li>Select <strong>Install for all users</strong></li></ul></li></ol><h3 id="PSReadLine-Version-Error"><a href="#PSReadLine-Version-Error" class="headerlink" title="PSReadLine Version Error"></a>PSReadLine Version Error</h3><p><strong>Symptom:</strong> Error about <code>-PredictionSource</code> parameter</p><p><strong>Fix:</strong><br>The script includes version checking, but if you manually edited your profile, ensure PSReadLine 2.1+ is installed:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Check version</span></span><br><span class="line">(<span class="built_in">Get-Module</span> PSReadLine).Version</span><br><span class="line"></span><br><span class="line"><span class="comment"># Update if needed</span></span><br><span class="line"><span class="built_in">Install-Module</span> <span class="literal">-Name</span> PSReadLine <span class="literal">-Force</span> <span class="literal">-SkipPublisherCheck</span></span><br></pre></td></tr></table></figure><h3 id="Kubernetes-Segment-Not-Appearing"><a href="#Kubernetes-Segment-Not-Appearing" class="headerlink" title="Kubernetes Segment Not Appearing"></a>Kubernetes Segment Not Appearing</h3><p><strong>Symptom:</strong> The kubectl segment doesn’t show even though you have kubectl configured.</p><p><strong>Causes:</strong></p><ol><li><p><strong>No current context:</strong> Run <code>kubectl config current-context</code> to verify you have an active context</p></li><li><p><strong>Kubeconfig not in default location:</strong> Oh My Posh looks for <code>~/.kube/config</code>. If you use <code>KUBECONFIG</code> environment variable with custom paths, the segment may not find it.</p></li><li><p><strong>Performance optimization:</strong> Oh My Posh might disable slow segments. Check if kubectl commands are responsive:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">Measure-Command</span> &#123; kubectl config current<span class="literal">-context</span> &#125;</span><br></pre></td></tr></table></figure><p>If this takes more than 500ms, Oh My Posh may timeout the segment.</p></li></ol><h3 id="Azure-Segment-Not-Showing"><a href="#Azure-Segment-Not-Showing" class="headerlink" title="Azure Segment Not Showing"></a>Azure Segment Not Showing</h3><p><strong>Symptom:</strong> No Azure subscription information in your prompt.</p><p><strong>Fixes:</strong></p><ol><li><p><strong>Azure CLI not installed:</strong> The segment requires Azure CLI:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">winget install Microsoft.AzureCLI</span><br></pre></td></tr></table></figure></li><li><p><strong>Not logged in:</strong> Authenticate with Azure:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">az login</span><br></pre></td></tr></table></figure></li><li><p><strong>No active subscription:</strong> Set a default subscription:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">az account <span class="built_in">set</span> <span class="literal">--subscription</span> <span class="string">&quot;Your Subscription Name&quot;</span></span><br></pre></td></tr></table></figure></li></ol><h3 id="Slow-Prompt-Performance"><a href="#Slow-Prompt-Performance" class="headerlink" title="Slow Prompt Performance"></a>Slow Prompt Performance</h3><p><strong>Symptom:</strong> Noticeable delay before prompt appears, especially after pressing Enter.</p><p><strong>Common Causes:</strong></p><ol><li><p><strong>Slow Git operations:</strong> Large repositories or network-based Git remotes can slow the Git segment. Disable fetch_status for specific repositories:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;properties&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;fetch_status&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure></li><li><p><strong>Multiple cloud segments:</strong> Each cloud segment (kubectl, az, aws) makes system calls. Remove segments you don’t actively use.</p></li><li><p><strong>Network timeouts:</strong> If segments query network resources (like Kubernetes API servers), timeouts can cause delays. Consider adjusting timeout settings or removing problematic segments.</p></li></ol><h2 id="Performance-Considerations-and-Optimizations"><a href="#Performance-Considerations-and-Optimizations" class="headerlink" title="Performance Considerations and Optimizations"></a>Performance Considerations and Optimizations</h2><h3 id="Segment-Caching"><a href="#Segment-Caching" class="headerlink" title="Segment Caching"></a>Segment Caching</h3><p>Oh My Posh caches segment results to improve performance. You can adjust cache durations in the segment properties:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;properties&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;cache_timeout&quot;</span><span class="punctuation">:</span> <span class="number">5</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>This tells the segment to cache results for 5 minutes, reducing repeated system calls.</p><h3 id="Selective-Segment-Enablement"><a href="#Selective-Segment-Enablement" class="headerlink" title="Selective Segment Enablement"></a>Selective Segment Enablement</h3><p>Not every developer needs every segment. Consider creating role-specific variants:</p><p><strong>Backend Developers:</strong> Focus on Git, Azure, and database context<br><strong>Frontend Developers:</strong> Emphasize Node.js version, Git, and build tool status<br><strong>DevOps Engineers:</strong> Full cloud context with Kubernetes, Azure, and AWS<br><strong>Full Stack:</strong> Balanced approach with programming language versions and cloud context</p><h3 id="Async-Segment-Updates"><a href="#Async-Segment-Updates" class="headerlink" title="Async Segment Updates"></a>Async Segment Updates</h3><p>For segments that make network calls, consider using Oh My Posh’s background update feature to prevent blocking the prompt:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;properties&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;async&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h2 id="Wrapping-Up"><a href="#Wrapping-Up" class="headerlink" title="Wrapping Up"></a>Wrapping Up</h2><p>What started as frustration with repeatedly configuring terminals across new machines evolved into a robust automation solution that eliminates setup friction entirely. One PowerShell script, a few minutes of execution time, and your terminal transforms from a blank slate into a fully-configured, cloud native development environment.</p><p>The script and theme are opinionated - they reflect my specific workflow and preferences. But that’s the beauty of Oh My Posh’s flexibility. Take this automation as a starting point, customize the theme to match your daily tasks, add or remove segments based on your tech stack, and create your perfect terminal environment.</p><p>No more hunting for font installers. No more manually editing profile files. No more copying configuration snippets from old machines. Just run the script, configure your font settings, and get back to building amazing things.</p><p>Happy terminal pimping! 🎉</p><h2 id="References"><a href="#References" class="headerlink" title="References"></a>References</h2><ul><li><a href="https://ohmyposh.dev/">Oh My Posh Official Website</a></li><li><a href="https://github.com/JanDeDobbeleer/oh-my-posh">Oh My Posh GitHub Repository</a></li><li><a href="https://docs.microsoft.com/en-us/windows/package-manager/winget/">Windows Package Manager (winget)</a></li><li><a href="https://www.nerdfonts.com/">Nerd Fonts Project</a></li><li><a href="https://docs.microsoft.com/en-us/windows/terminal/">Windows Terminal Documentation</a></li><li><a href="https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_profiles">PowerShell Profile Documentation</a></li><li>Main image generated by <a href="https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/dall-e?view=foundry-classic&tabs=gpt-image-1">GPT-Image-1.5</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🎯 TL;DR: Automated Oh My Posh Terminal Setup for Cloud Native Development&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Every new machine or fresh Windows install means reconfiguring your terminal environment from scratch. Problem: Manually setting up Oh My Posh, installing Nerd Fonts, and configuring custom themes is tedious and error-prone across multiple machines. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Solution:&lt;/strong&gt; (&lt;a href=&quot;https://github.com/Ricky-G/script-library/blob/main/pimp-my-terminal.ps1&quot;&gt;A single PowerShell script available on GitHub https://github.com/Ricky-G/script-library/blob/main/pimp-my-terminal.ps1&lt;/a&gt;) that automates the entire process - installing Oh My Posh via winget, deploying a Nerd Font, Terminal-Icons module, creating a custom “Cloud Native Azure” theme optimized for Kubernetes and Azure workflows, and configuring your PowerShell profile with PSReadLine enhancements. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Prerequisites:&lt;/strong&gt; Enable script execution with &lt;code&gt;Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser&lt;/code&gt; before running. This approach transforms the multi-hour setup process into a one-command operation, providing immediate visual context for Git branches, Kubernetes clusters, Azure subscriptions, and command execution times - critical information for modern cloud native development.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;Recently, I found myself setting up yet another development machine, and as I stared at the blank PowerShell terminal, I realized I’d reached my limit with manual terminal configuration. Every new machine or clean install meant the same tedious process: download Oh My Posh, find a Nerd Font installer, copy configuration files, edit PowerShell profiles, and spend 30 minutes getting everything just right.&lt;/p&gt;
&lt;p&gt;The frustration wasn’t just about aesthetics - a properly configured terminal is a productivity multiplier. When you’re constantly switching between multiple Git repositories, Kubernetes clusters, and Azure subscriptions throughout the day, having that contextual information immediately visible saves countless keystrokes and eliminates mental overhead.&lt;/p&gt;
&lt;p&gt;This blog post shares my automated solution: a single PowerShell script that takes a bare Windows terminal and transforms it into a fully-configured, cloud native-ready development environment in under 5 minutes. Whether you’re setting up a new machine, rebuilding after a Windows update disaster, or just want to standardize terminal configuration across your team, this automation eliminates the manual work.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/Engineering/Tooling/pimp-my-terminal-terminal-customization-with-oh-my-posh-a-cloud-native-terminal-setup/before-after-terminal.png&quot; alt=&quot;Before and After Terminal&quot;&gt;&lt;/p&gt;
&lt;h2 id=&quot;Quick-Start-Get-Up-and-Running-in-5-Minutes&quot;&gt;&lt;a href=&quot;#Quick-Start-Get-Up-and-Running-in-5-Minutes&quot; class=&quot;headerlink&quot; title=&quot;Quick Start - Get Up and Running in 5 Minutes&quot;&gt;&lt;/a&gt;Quick Start - Get Up and Running in 5 Minutes&lt;/h2&gt;&lt;p&gt;&lt;strong&gt;Want to skip the details and just get started?&lt;/strong&gt; Here’s everything you need to run the automation script:&lt;/p&gt;
&lt;h3 id=&quot;Step-1-Enable-Script-Execution&quot;&gt;&lt;a href=&quot;#Step-1-Enable-Script-Execution&quot; class=&quot;headerlink&quot; title=&quot;Step 1: Enable Script Execution&quot;&gt;&lt;/a&gt;Step 1: Enable Script Execution&lt;/h3&gt;&lt;p&gt;Open PowerShell as Administrator and run:&lt;/p&gt;
&lt;figure class=&quot;highlight powershell&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;built_in&quot;&gt;Set-ExecutionPolicy&lt;/span&gt; &lt;span class=&quot;literal&quot;&gt;-ExecutionPolicy&lt;/span&gt; RemoteSigned &lt;span class=&quot;literal&quot;&gt;-Scope&lt;/span&gt; CurrentUser&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;When prompted, type &lt;code&gt;Y&lt;/code&gt; and press Enter.&lt;/p&gt;
&lt;h3 id=&quot;Step-2-Download-and-Run-the-Script&quot;&gt;&lt;a href=&quot;#Step-2-Download-and-Run-the-Script&quot; class=&quot;headerlink&quot; title=&quot;Step 2: Download and Run the Script&quot;&gt;&lt;/a&gt;Step 2: Download and Run the Script&lt;/h3&gt;&lt;figure class=&quot;highlight powershell&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt;# Download and run the automation script&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;built_in&quot;&gt;Invoke-WebRequest&lt;/span&gt; &lt;span class=&quot;literal&quot;&gt;-Uri&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;quot;https://raw.githubusercontent.com/Ricky-G/script-library/main/pimp-my-terminal.ps1&amp;quot;&lt;/span&gt; &lt;span class=&quot;literal&quot;&gt;-OutFile&lt;/span&gt; &lt;span class=&quot;string&quot;&gt;&amp;quot;&lt;span class=&quot;variable&quot;&gt;$env:TEMP&lt;/span&gt;&#92;pimp-my-terminal.ps1&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;amp; &lt;span class=&quot;string&quot;&gt;&amp;quot;&lt;span class=&quot;variable&quot;&gt;$env:TEMP&lt;/span&gt;&#92;pimp-my-terminal.ps1&amp;quot;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;

&lt;p&gt;The script will automatically install:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;✅ Oh My Posh via winget&lt;/li&gt;
&lt;li&gt;✅ MesloLGM Nerd Font&lt;/li&gt;
&lt;li&gt;✅ Terminal-Icons PowerShell module&lt;/li&gt;
&lt;li&gt;✅ Cloud Native Azure theme&lt;/li&gt;
&lt;li&gt;✅ PSReadLine enhancements&lt;/li&gt;
&lt;li&gt;✅ Custom keyboard shortcuts&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;Step-3-Configure-Your-Terminal-Font&quot;&gt;&lt;a href=&quot;#Step-3-Configure-Your-Terminal-Font&quot; class=&quot;headerlink&quot; title=&quot;Step 3: Configure Your Terminal Font&quot;&gt;&lt;/a&gt;Step 3: Configure Your Terminal Font&lt;/h3&gt;&lt;p&gt;After the script completes, configure your terminal font:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Windows Terminal:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open Settings (&lt;code&gt;Ctrl + ,&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Go to Profiles → Defaults → Appearance&lt;/li&gt;
&lt;li&gt;Set Font face to: &lt;code&gt;MesloLGM Nerd Font&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Save and restart terminal&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;VS Code:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Open Settings (&lt;code&gt;Ctrl + ,&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Search for “terminal font”&lt;/li&gt;
&lt;li&gt;Set Terminal › Integrated: Font Family to: &lt;code&gt;MesloLGM Nerd Font&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;Done!&lt;/strong&gt; Open a new terminal and enjoy your beautiful, cloud native-ready prompt.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&quot;Understanding-Oh-My-Posh-The-Modern-Prompt-Engine&quot;&gt;&lt;a href=&quot;#Understanding-Oh-My-Posh-The-Modern-Prompt-Engine&quot; class=&quot;headerlink&quot; title=&quot;Understanding Oh My Posh: The Modern Prompt Engine&quot;&gt;&lt;/a&gt;Understanding Oh My Posh: The Modern Prompt Engine&lt;/h2&gt;&lt;p&gt;Before diving into the automation, it’s worth understanding what Oh My Posh brings to the table and why it’s become the de facto standard for PowerShell prompt customization.&lt;/p&gt;</summary>
    
    
    
    <category term="Engineering" scheme="https://clouddev.blog/categories/Engineering/"/>
    
    <category term="Tooling" scheme="https://clouddev.blog/categories/Engineering/Tooling/"/>
    
    
    <category term="PowerShell" scheme="https://clouddev.blog/tags/PowerShell/"/>
    
    <category term="Azure" scheme="https://clouddev.blog/tags/Azure/"/>
    
    <category term="Development Environment" scheme="https://clouddev.blog/tags/Development-Environment/"/>
    
    <category term="Remote Development" scheme="https://clouddev.blog/tags/Remote-Development/"/>
    
    <category term="VS Code" scheme="https://clouddev.blog/tags/VS-Code/"/>
    
    <category term="Kubernetes" scheme="https://clouddev.blog/tags/Kubernetes/"/>
    
  </entry>
  
  <entry>
    <title>Automating Searchable Branch Configuration in Azure DevOps Repos via REST API</title>
    <link href="https://clouddev.blog/Azure-DevOps/Azure-DevOps-API/automating-searchable-branch-configuration-in-azure-devops-repos-via-rest-api/"/>
    <id>https://clouddev.blog/Azure-DevOps/Azure-DevOps-API/automating-searchable-branch-configuration-in-azure-devops-repos-via-rest-api/</id>
    <published>2025-08-14T12:00:00.000Z</published>
    <updated>2025-09-14T11:42:32.450Z</updated>
    
    <content type="html"><![CDATA[<hr><blockquote><p><strong>🎯 TL;DR: Bulk Configure Searchable Branches in Azure DevOps via Hidden Policy API</strong></p><p>Azure DevOps code search only indexes the default branch (master&#x2F;main) by default, causing issues when teams use <code>develop</code> branches for JFrog Artifactory detection scripts. Problem: No documented API exists for bulk updating searchable branches across thousands of repositories. Solution: Use the undocumented Policy Configuration API with policy type <code>0517f88d-4ec5-4343-9d26-9930ebd53069</code> to programmatically add branches to the searchable list. This approach leverages the same API calls the Azure DevOps UI uses internally, enabling automation of what would otherwise require manual configuration across massive repository collections.</p></blockquote><hr><p>Recently, I encountered an interesting challenge while working on a JFrog Artifactory adoption tracking project across a large Azure DevOps organization. The requirement was to scan repositories for JFrog URL references to determine which teams had successfully onboarded to their new artifact management system. The problem? Some development teams exclusively work in <code>develop</code> branches instead of <code>master</code> or <code>main</code>, and Azure DevOps code search only indexes the default branch by default.</p><p>This seemingly simple requirement - adding <code>develop</code> to the searchable branches for thousands of repositories - turned into a fascinating exploration of Azure DevOps’ undocumented APIs. While there’s no official documentation for bulk updating searchable branches, I discovered that the Azure DevOps UI uses a specific Policy Configuration API under the hood that we can leverage for automation.</p><p>This blog post shares a practical approach to programmatically configure searchable branches across large Azure DevOps organizations using REST APIs that Microsoft doesn’t officially document but absolutely supports.</p><h2 id="The-Challenge-Azure-DevOps-Code-Search-Limitations"><a href="#The-Challenge-Azure-DevOps-Code-Search-Limitations" class="headerlink" title="The Challenge: Azure DevOps Code Search Limitations"></a>The Challenge: Azure DevOps Code Search Limitations</h2><p>Azure DevOps code search is a powerful feature, but it comes with a significant limitation that affects many organizations: by default, only the repository’s default branch (typically <code>master</code> or <code>main</code>) is indexed for search operations.</p><p>This creates problems in several scenarios:</p><p><strong>JFrog Adoption Tracking:</strong> Organizations implementing JFrog Artifactory need to scan all repositories for configuration files and dependency references, but teams using feature branches or <code>develop</code> as their primary branch won’t be detected.</p><p><strong>Multi-Branch Development:</strong> Teams practicing GitFlow or similar branching strategies may have critical code in <code>develop</code>, <code>release/*</code>, or feature branches that needs to be searchable.</p><p><strong>Compliance and Security Scanning:</strong> Security tools and compliance scripts that rely on code search may miss important files if they’re not in the default branch.</p><span id="more"></span><h2 id="Understanding-Azure-DevOps-Searchable-Branches"><a href="#Understanding-Azure-DevOps-Searchable-Branches" class="headerlink" title="Understanding Azure DevOps Searchable Branches"></a>Understanding Azure DevOps Searchable Branches</h2><p>In Azure DevOps, searchable branches are configured at the repository level through the UI:</p><p><strong>Manual Configuration Path:</strong></p><ol><li>Navigate to Project Settings → Repositories</li><li>Select your repository</li><li>Go to Settings → Searchable Branches</li><li>Add additional branches (maximum of 5 extra branches)</li></ol><p><strong>What Actually Happens:</strong><br>When you configure searchable branches through the UI, Azure DevOps creates or updates a policy configuration with a specific policy type. This policy type - <code>0517f88d-4ec5-4343-9d26-9930ebd53069</code> - controls which branches are indexed for code search.</p><h2 id="The-Discovery-Hidden-Policy-Configuration-API"><a href="#The-Discovery-Hidden-Policy-Configuration-API" class="headerlink" title="The Discovery: Hidden Policy Configuration API"></a>The Discovery: Hidden Policy Configuration API</h2><p>After extensive research and reverse engineering the Azure DevOps UI network traffic, I discovered that searchable branch configuration is managed through the Policy Configuration API with a specific, undocumented policy type.</p><p><strong>Key Insights:</strong></p><ul><li>Azure DevOps doesn’t expose a direct “searchable branches” API endpoint</li><li>The UI uses the Policy Configuration API with policy type <code>0517f88d-4ec5-4343-9d26-9930ebd53069</code></li><li>This API is officially supported but not documented for this specific use case</li><li>The same approach works for both Azure DevOps Server and Azure DevOps Cloud</li></ul><h2 id="The-Solution-Automated-Policy-Configuration"><a href="#The-Solution-Automated-Policy-Configuration" class="headerlink" title="The Solution: Automated Policy Configuration"></a>The Solution: Automated Policy Configuration</h2><p>The approach involves three main steps:</p><h3 id="Step-1-Retrieve-Existing-Policy-Configuration"><a href="#Step-1-Retrieve-Existing-Policy-Configuration" class="headerlink" title="Step 1: Retrieve Existing Policy Configuration"></a>Step 1: Retrieve Existing Policy Configuration</h3><p>First, we need to check if a searchable branches policy already exists for the repository:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Get existing searchable branch policy for a repository</span></span><br><span class="line"><span class="variable">$policyUrl</span> = <span class="string">&quot;https://dev.azure.com/&#123;organization&#125;/&#123;project&#125;/_apis/policy/configurations&quot;</span></span><br><span class="line"><span class="variable">$params</span> = <span class="selector-tag">@</span>&#123;</span><br><span class="line">    <span class="string">&#x27;repositoryId&#x27;</span> = <span class="variable">$repositoryId</span></span><br><span class="line">    <span class="string">&#x27;policyType&#x27;</span> = <span class="string">&#x27;0517f88d-4ec5-4343-9d26-9930ebd53069&#x27;</span></span><br><span class="line">    <span class="string">&#x27;api-version&#x27;</span> = <span class="string">&#x27;7.1-preview.1&#x27;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="variable">$existingPolicy</span> = <span class="built_in">Invoke-RestMethod</span> <span class="literal">-Uri</span> <span class="variable">$policyUrl</span> <span class="literal">-Headers</span> <span class="variable">$headers</span> <span class="literal">-Method</span> Get <span class="literal">-Body</span> <span class="variable">$params</span></span><br></pre></td></tr></table></figure><h3 id="Step-2-Create-or-Update-Policy-Configuration"><a href="#Step-2-Create-or-Update-Policy-Configuration" class="headerlink" title="Step 2: Create or Update Policy Configuration"></a>Step 2: Create or Update Policy Configuration</h3><p>If a policy exists, we update it. If not, we create a new one:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Policy configuration for searchable branches</span></span><br><span class="line"><span class="variable">$policySettings</span> = <span class="selector-tag">@</span>&#123;</span><br><span class="line">    <span class="string">&#x27;type&#x27;</span> = <span class="selector-tag">@</span>&#123;</span><br><span class="line">        <span class="string">&#x27;id&#x27;</span> = <span class="string">&#x27;0517f88d-4ec5-4343-9d26-9930ebd53069&#x27;</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="string">&#x27;isEnabled&#x27;</span> = <span class="variable">$true</span></span><br><span class="line">    <span class="string">&#x27;isBlocking&#x27;</span> = <span class="variable">$false</span></span><br><span class="line">    <span class="string">&#x27;settings&#x27;</span> = <span class="selector-tag">@</span>&#123;</span><br><span class="line">        <span class="string">&#x27;searchBranches&#x27;</span> = <span class="selector-tag">@</span>(</span><br><span class="line">            <span class="string">&#x27;refs/heads/master&#x27;</span>,</span><br><span class="line">            <span class="string">&#x27;refs/heads/main&#x27;</span>, </span><br><span class="line">            <span class="string">&#x27;refs/heads/develop&#x27;</span></span><br><span class="line">        )</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="string">&#x27;scope&#x27;</span> = <span class="selector-tag">@</span>(</span><br><span class="line">        <span class="selector-tag">@</span>&#123;</span><br><span class="line">            <span class="string">&#x27;repositoryId&#x27;</span> = <span class="variable">$repositoryId</span></span><br><span class="line">            <span class="string">&#x27;refName&#x27;</span> = <span class="string">&#x27;refs/heads/master&#x27;</span></span><br><span class="line">            <span class="string">&#x27;matchKind&#x27;</span> = <span class="string">&#x27;exact&#x27;</span></span><br><span class="line">        &#125;</span><br><span class="line">    )</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Step-3-Apply-the-Configuration"><a href="#Step-3-Apply-the-Configuration" class="headerlink" title="Step 3: Apply the Configuration"></a>Step 3: Apply the Configuration</h3><p>Send the policy configuration to Azure DevOps:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">if</span> (<span class="variable">$existingPolicy</span>.count <span class="operator">-gt</span> <span class="number">0</span>) &#123;</span><br><span class="line">    <span class="comment"># Update existing policy</span></span><br><span class="line">    <span class="variable">$configId</span> = <span class="variable">$existingPolicy</span>.value[<span class="number">0</span>].id</span><br><span class="line">    <span class="variable">$updateUrl</span> = <span class="string">&quot;https://dev.azure.com/&#123;organization&#125;/&#123;project&#125;/_apis/policy/configurations/<span class="variable">$configId</span>&quot;</span></span><br><span class="line">    <span class="built_in">Invoke-RestMethod</span> <span class="literal">-Uri</span> <span class="variable">$updateUrl</span> <span class="literal">-Headers</span> <span class="variable">$headers</span> <span class="literal">-Method</span> Put <span class="literal">-Body</span> (<span class="variable">$policySettings</span> | <span class="built_in">ConvertTo-Json</span> <span class="literal">-Depth</span> <span class="number">10</span>) <span class="literal">-ContentType</span> <span class="string">&quot;application/json&quot;</span></span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="comment"># Create new policy</span></span><br><span class="line">    <span class="variable">$createUrl</span> = <span class="string">&quot;https://dev.azure.com/&#123;organization&#125;/&#123;project&#125;/_apis/policy/configurations&quot;</span></span><br><span class="line">    <span class="built_in">Invoke-RestMethod</span> <span class="literal">-Uri</span> <span class="variable">$createUrl</span> <span class="literal">-Headers</span> <span class="variable">$headers</span> <span class="literal">-Method</span> Post <span class="literal">-Body</span> (<span class="variable">$policySettings</span> | <span class="built_in">ConvertTo-Json</span> <span class="literal">-Depth</span> <span class="number">10</span>) <span class="literal">-ContentType</span> <span class="string">&quot;application/json&quot;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Complete-PowerShell-Implementation"><a href="#Complete-PowerShell-Implementation" class="headerlink" title="Complete PowerShell Implementation"></a>Complete PowerShell Implementation</h2><p>Here’s a complete script that implements this solution for bulk updating searchable branches across multiple repositories:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Azure DevOps configuration</span></span><br><span class="line"><span class="variable">$organization</span> = <span class="string">&quot;your-org&quot;</span></span><br><span class="line"><span class="variable">$project</span> = <span class="string">&quot;your-project&quot;</span> </span><br><span class="line"><span class="variable">$pat</span> = <span class="string">&quot;your-personal-access-token&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Create authentication headers</span></span><br><span class="line"><span class="variable">$headers</span> = <span class="selector-tag">@</span>&#123;</span><br><span class="line">    <span class="string">&#x27;Authorization&#x27;</span> = <span class="string">&quot;Basic &quot;</span> + [<span class="type">Convert</span>]::ToBase64String([<span class="type">Text.Encoding</span>]::ASCII.GetBytes(<span class="string">&quot;:<span class="variable">$pat</span>&quot;</span>))</span><br><span class="line">    <span class="string">&#x27;Content-Type&#x27;</span> = <span class="string">&#x27;application/json&#x27;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment"># Function to update searchable branches for a repository</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">Update-SearchableBranches</span></span> &#123;</span><br><span class="line">    <span class="keyword">param</span>(</span><br><span class="line">        [<span class="built_in">string</span>]<span class="variable">$organizationName</span>,</span><br><span class="line">        [<span class="built_in">string</span>]<span class="variable">$projectName</span>,</span><br><span class="line">        [<span class="built_in">string</span>]<span class="variable">$repositoryId</span>,</span><br><span class="line">        [<span class="built_in">array</span>]<span class="variable">$branches</span>,</span><br><span class="line">        [<span class="built_in">hashtable</span>]<span class="variable">$authHeaders</span></span><br><span class="line">    )</span><br><span class="line">    </span><br><span class="line">    <span class="variable">$policyTypeId</span> = <span class="string">&#x27;0517f88d-4ec5-4343-9d26-9930ebd53069&#x27;</span></span><br><span class="line">    <span class="variable">$baseUrl</span> = <span class="string">&quot;https://dev.azure.com/<span class="variable">$organizationName</span>/<span class="variable">$projectName</span>/_apis/policy/configurations&quot;</span></span><br><span class="line">    </span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="comment"># Check for existing policy</span></span><br><span class="line">        <span class="variable">$getUrl</span> = <span class="string">&quot;<span class="variable">$baseUrl</span>&quot;</span> + <span class="string">&quot;?repositoryId=<span class="variable">$repositoryId</span>&amp;policyType=<span class="variable">$policyTypeId</span>&amp;api-version=7.1-preview.1&quot;</span></span><br><span class="line">        <span class="variable">$existingPolicy</span> = <span class="built_in">Invoke-RestMethod</span> <span class="literal">-Uri</span> <span class="variable">$getUrl</span> <span class="literal">-Headers</span> <span class="variable">$authHeaders</span> <span class="literal">-Method</span> Get</span><br><span class="line">        </span><br><span class="line">        <span class="comment"># Format branches with refs/heads/ prefix</span></span><br><span class="line">        <span class="variable">$searchBranches</span> = <span class="variable">$branches</span> | <span class="built_in">ForEach-Object</span> &#123; </span><br><span class="line">            <span class="keyword">if</span> (<span class="variable">$_</span> <span class="operator">-like</span> <span class="string">&quot;refs/heads/*&quot;</span>) &#123; <span class="variable">$_</span> &#125; <span class="keyword">else</span> &#123; <span class="string">&quot;refs/heads/<span class="variable">$_</span>&quot;</span> &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="comment"># Create policy configuration</span></span><br><span class="line">        <span class="variable">$policyConfig</span> = <span class="selector-tag">@</span>&#123;</span><br><span class="line">            <span class="string">&#x27;type&#x27;</span> = <span class="selector-tag">@</span>&#123; <span class="string">&#x27;id&#x27;</span> = <span class="variable">$policyTypeId</span> &#125;</span><br><span class="line">            <span class="string">&#x27;isEnabled&#x27;</span> = <span class="variable">$true</span></span><br><span class="line">            <span class="string">&#x27;isBlocking&#x27;</span> = <span class="variable">$false</span></span><br><span class="line">            <span class="string">&#x27;settings&#x27;</span> = <span class="selector-tag">@</span>&#123;</span><br><span class="line">                <span class="string">&#x27;searchBranches&#x27;</span> = <span class="variable">$searchBranches</span></span><br><span class="line">            &#125;</span><br><span class="line">            <span class="string">&#x27;scope&#x27;</span> = <span class="selector-tag">@</span>(</span><br><span class="line">                <span class="selector-tag">@</span>&#123;</span><br><span class="line">                    <span class="string">&#x27;repositoryId&#x27;</span> = <span class="variable">$repositoryId</span></span><br><span class="line">                    <span class="string">&#x27;refName&#x27;</span> = <span class="variable">$searchBranches</span>[<span class="number">0</span>]</span><br><span class="line">                    <span class="string">&#x27;matchKind&#x27;</span> = <span class="string">&#x27;exact&#x27;</span></span><br><span class="line">                &#125;</span><br><span class="line">            )</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="variable">$jsonBody</span> = <span class="variable">$policyConfig</span> | <span class="built_in">ConvertTo-Json</span> <span class="literal">-Depth</span> <span class="number">10</span></span><br><span class="line">        </span><br><span class="line">        <span class="keyword">if</span> (<span class="variable">$existingPolicy</span>.count <span class="operator">-gt</span> <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="comment"># Update existing policy</span></span><br><span class="line">            <span class="variable">$configId</span> = <span class="variable">$existingPolicy</span>.value[<span class="number">0</span>].id</span><br><span class="line">            <span class="variable">$updateUrl</span> = <span class="string">&quot;<span class="variable">$baseUrl</span>/<span class="variable">$configId</span>&quot;</span> + <span class="string">&quot;?api-version=7.1-preview.1&quot;</span></span><br><span class="line">            <span class="variable">$result</span> = <span class="built_in">Invoke-RestMethod</span> <span class="literal">-Uri</span> <span class="variable">$updateUrl</span> <span class="literal">-Headers</span> <span class="variable">$authHeaders</span> <span class="literal">-Method</span> Put <span class="literal">-Body</span> <span class="variable">$jsonBody</span></span><br><span class="line">            <span class="built_in">Write-Host</span> <span class="string">&quot;Updated searchable branches for repository <span class="variable">$repositoryId</span>&quot;</span> <span class="literal">-ForegroundColor</span> Green</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="comment"># Create new policy</span></span><br><span class="line">            <span class="variable">$createUrl</span> = <span class="string">&quot;<span class="variable">$baseUrl</span>&quot;</span> + <span class="string">&quot;?api-version=7.1-preview.1&quot;</span></span><br><span class="line">            <span class="variable">$result</span> = <span class="built_in">Invoke-RestMethod</span> <span class="literal">-Uri</span> <span class="variable">$createUrl</span> <span class="literal">-Headers</span> <span class="variable">$authHeaders</span> <span class="literal">-Method</span> Post <span class="literal">-Body</span> <span class="variable">$jsonBody</span></span><br><span class="line">            <span class="built_in">Write-Host</span> <span class="string">&quot;Created searchable branches policy for repository <span class="variable">$repositoryId</span>&quot;</span> <span class="literal">-ForegroundColor</span> Green</span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">return</span> <span class="variable">$result</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">catch</span> &#123;</span><br><span class="line">        <span class="built_in">Write-Error</span> <span class="string">&quot;Failed to update searchable branches for repository <span class="variable">$repositoryId</span>`: <span class="variable">$</span>(<span class="variable">$_</span>.Exception.Message)&quot;</span></span><br><span class="line">        <span class="keyword">return</span> <span class="variable">$null</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment"># Get all repositories in the project</span></span><br><span class="line"><span class="variable">$reposUrl</span> = <span class="string">&quot;https://dev.azure.com/<span class="variable">$organization</span>/<span class="variable">$project</span>/_apis/git/repositories?api-version=7.1&quot;</span></span><br><span class="line"><span class="variable">$repositories</span> = <span class="built_in">Invoke-RestMethod</span> <span class="literal">-Uri</span> <span class="variable">$reposUrl</span> <span class="literal">-Headers</span> <span class="variable">$headers</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Define branches to make searchable</span></span><br><span class="line"><span class="variable">$searchableBranches</span> = <span class="selector-tag">@</span>(<span class="string">&#x27;master&#x27;</span>, <span class="string">&#x27;main&#x27;</span>, <span class="string">&#x27;develop&#x27;</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment"># Update searchable branches for each repository</span></span><br><span class="line"><span class="keyword">foreach</span> (<span class="variable">$repo</span> <span class="keyword">in</span> <span class="variable">$repositories</span>.value) &#123;</span><br><span class="line">    <span class="built_in">Write-Host</span> <span class="string">&quot;Processing repository: <span class="variable">$</span>(<span class="variable">$repo</span>.name)&quot;</span> <span class="literal">-ForegroundColor</span> Yellow</span><br><span class="line">    </span><br><span class="line">    <span class="variable">$result</span> = <span class="built_in">Update-SearchableBranches</span> <span class="literal">-organizationName</span> <span class="variable">$organization</span> <span class="literal">-projectName</span> <span class="variable">$project</span> <span class="literal">-repositoryId</span> <span class="variable">$repo</span>.id <span class="literal">-branches</span> <span class="variable">$searchableBranches</span> <span class="literal">-authHeaders</span> <span class="variable">$headers</span></span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> (<span class="variable">$result</span>) &#123;</span><br><span class="line">        <span class="built_in">Write-Host</span> <span class="string">&quot;Successfully configured searchable branches for <span class="variable">$</span>(<span class="variable">$repo</span>.name)&quot;</span> <span class="literal">-ForegroundColor</span> Green</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="built_in">Write-Host</span> <span class="string">&quot;Failed to configure searchable branches for <span class="variable">$</span>(<span class="variable">$repo</span>.name)&quot;</span> <span class="literal">-ForegroundColor</span> Red</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># Add delay to avoid rate limiting</span></span><br><span class="line">    <span class="built_in">Start-Sleep</span> <span class="literal">-Milliseconds</span> <span class="number">500</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="built_in">Write-Host</span> <span class="string">&quot;Searchable branches configuration complete!&quot;</span> <span class="literal">-ForegroundColor</span> Cyan</span><br></pre></td></tr></table></figure><h2 id="Important-Considerations-and-Limitations"><a href="#Important-Considerations-and-Limitations" class="headerlink" title="Important Considerations and Limitations"></a>Important Considerations and Limitations</h2><h3 id="API-Version-and-Stability"><a href="#API-Version-and-Stability" class="headerlink" title="API Version and Stability"></a>API Version and Stability</h3><p><strong>Preview API:</strong> The Policy Configuration API is in preview (<code>7.1-preview.1</code>), which means:</p><ul><li>The API may change without notice</li><li>Microsoft doesn’t guarantee backward compatibility</li><li>Monitor for API updates and test thoroughly before production use</li></ul><h3 id="Repository-Limitations"><a href="#Repository-Limitations" class="headerlink" title="Repository Limitations"></a>Repository Limitations</h3><p><strong>Branch Limits:</strong> Azure DevOps allows a maximum of 6 searchable branches per repository (including the default branch).</p><p><strong>Indexing Delays:</strong> After updating searchable branches, Azure DevOps may take several hours to index the new branches. Search results won’t be immediately available.</p><h3 id="Performance-Considerations"><a href="#Performance-Considerations" class="headerlink" title="Performance Considerations"></a>Performance Considerations</h3><p><strong>Rate Limiting:</strong> Implement appropriate delays between API calls to avoid hitting rate limits, especially when processing thousands of repositories.</p><p><strong>Batch Processing:</strong> For large organizations, consider processing repositories in batches and implementing retry logic for failed requests.</p><h3 id="Error-Handling"><a href="#Error-Handling" class="headerlink" title="Error Handling"></a>Error Handling</h3><p><strong>Repository States:</strong> Some repositories may not have branches configured or may be in archived states. Implement proper error handling for these scenarios.</p><p><strong>Permission Issues:</strong> Ensure your Personal Access Token has sufficient permissions for policy configuration (Project Settings, Read &amp; Manage).</p><h2 id="Authentication-and-Security-Setup"><a href="#Authentication-and-Security-Setup" class="headerlink" title="Authentication and Security Setup"></a>Authentication and Security Setup</h2><h3 id="Personal-Access-Token-Configuration"><a href="#Personal-Access-Token-Configuration" class="headerlink" title="Personal Access Token Configuration"></a>Personal Access Token Configuration</h3><p>Create a Personal Access Token with the following permissions:</p><ul><li><strong>Code (Read)</strong> - To access repository information</li><li><strong>Project and Team (Read)</strong> - To list projects and repositories  </li><li><strong>Policy Configuration (Read &amp; Manage)</strong> - To update searchable branch policies</li></ul><h3 id="Security-Best-Practices"><a href="#Security-Best-Practices" class="headerlink" title="Security Best Practices"></a>Security Best Practices</h3><p><strong>Token Storage:</strong> Store Personal Access Tokens securely and rotate them regularly according to your organization’s security policies.</p><p><strong>Least Privilege:</strong> Use dedicated service accounts with minimal required permissions for automation scripts.</p><p><strong>Audit Logging:</strong> Log all policy changes for compliance and troubleshooting purposes.</p><h2 id="Advanced-Usage-Scenarios"><a href="#Advanced-Usage-Scenarios" class="headerlink" title="Advanced Usage Scenarios"></a>Advanced Usage Scenarios</h2><h3 id="Conditional-Branch-Configuration"><a href="#Conditional-Branch-Configuration" class="headerlink" title="Conditional Branch Configuration"></a>Conditional Branch Configuration</h3><p>Configure different searchable branches based on repository naming conventions or team requirements:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Example: Configure different branches based on repository name patterns</span></span><br><span class="line"><span class="variable">$branchConfig</span> = <span class="selector-tag">@</span>&#123;</span><br><span class="line">    <span class="string">&#x27;web-*&#x27;</span> = <span class="selector-tag">@</span>(<span class="string">&#x27;master&#x27;</span>, <span class="string">&#x27;develop&#x27;</span>, <span class="string">&#x27;staging&#x27;</span>)</span><br><span class="line">    <span class="string">&#x27;api-*&#x27;</span> = <span class="selector-tag">@</span>(<span class="string">&#x27;master&#x27;</span>, <span class="string">&#x27;develop&#x27;</span>, <span class="string">&#x27;release&#x27;</span>)</span><br><span class="line">    <span class="string">&#x27;mobile-*&#x27;</span> = <span class="selector-tag">@</span>(<span class="string">&#x27;master&#x27;</span>, <span class="string">&#x27;develop&#x27;</span>, <span class="string">&#x27;feature/*&#x27;</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">foreach</span> (<span class="variable">$repo</span> <span class="keyword">in</span> <span class="variable">$repositories</span>.value) &#123;</span><br><span class="line">    <span class="variable">$branches</span> = <span class="selector-tag">@</span>(<span class="string">&#x27;master&#x27;</span>, <span class="string">&#x27;main&#x27;</span>) <span class="comment"># Default branches</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment"># Add specific branches based on repository name</span></span><br><span class="line">    <span class="keyword">foreach</span> (<span class="variable">$pattern</span> <span class="keyword">in</span> <span class="variable">$branchConfig</span>.Keys) &#123;</span><br><span class="line">        <span class="keyword">if</span> (<span class="variable">$repo</span>.name <span class="operator">-like</span> <span class="variable">$pattern</span>) &#123;</span><br><span class="line">            <span class="variable">$branches</span> += <span class="variable">$branchConfig</span>[<span class="variable">$pattern</span>]</span><br><span class="line">            <span class="keyword">break</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># Remove duplicates and configure</span></span><br><span class="line">    <span class="variable">$uniqueBranches</span> = <span class="variable">$branches</span> | <span class="built_in">Select-Object</span> <span class="literal">-Unique</span></span><br><span class="line">    <span class="built_in">Update-SearchableBranches</span> <span class="literal">-repositoryId</span> <span class="variable">$repo</span>.id <span class="literal">-branches</span> <span class="variable">$uniqueBranches</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Integration-with-CI-x2F-CD-Pipelines"><a href="#Integration-with-CI-x2F-CD-Pipelines" class="headerlink" title="Integration with CI&#x2F;CD Pipelines"></a>Integration with CI&#x2F;CD Pipelines</h3><p>Integrate searchable branch configuration into your DevOps workflows:</p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Azure DevOps Pipeline example</span></span><br><span class="line"><span class="bullet">-</span> <span class="attr">task:</span> <span class="string">PowerShell@2</span></span><br><span class="line">  <span class="attr">displayName:</span> <span class="string">&#x27;Configure Searchable Branches&#x27;</span></span><br><span class="line">  <span class="attr">inputs:</span></span><br><span class="line">    <span class="attr">targetType:</span> <span class="string">&#x27;inline&#x27;</span></span><br><span class="line">    <span class="attr">script:</span> <span class="string">|</span></span><br><span class="line"><span class="string">      # Your PowerShell script here</span></span><br><span class="line"><span class="string">      # Use pipeline variables for organization, project, and PAT</span></span><br></pre></td></tr></table></figure><h2 id="Troubleshooting-Common-Issues"><a href="#Troubleshooting-Common-Issues" class="headerlink" title="Troubleshooting Common Issues"></a>Troubleshooting Common Issues</h2><h3 id="Policy-Configuration-Not-Found"><a href="#Policy-Configuration-Not-Found" class="headerlink" title="Policy Configuration Not Found"></a>Policy Configuration Not Found</h3><p>If the GET request returns empty results, the repository may not have searchable branches configured yet. Create a new policy instead of updating an existing one.</p><h3 id="Invalid-Branch-References"><a href="#Invalid-Branch-References" class="headerlink" title="Invalid Branch References"></a>Invalid Branch References</h3><p>Ensure branch names use the correct format:</p><ul><li>Correct: <code>refs/heads/develop</code></li><li>Incorrect: <code>develop</code> or <code>origin/develop</code></li></ul><h3 id="Permission-Denied-Errors"><a href="#Permission-Denied-Errors" class="headerlink" title="Permission Denied Errors"></a>Permission Denied Errors</h3><p>Verify that your Personal Access Token has the required permissions and that you have administrative access to the project.</p><h3 id="Indexing-Delays"><a href="#Indexing-Delays" class="headerlink" title="Indexing Delays"></a>Indexing Delays</h3><p>After configuration, search indexing may take several hours. Test with small repositories first to verify the configuration is working before processing large numbers of repositories.</p><h2 id="Real-World-Use-Cases"><a href="#Real-World-Use-Cases" class="headerlink" title="Real-World Use Cases"></a>Real-World Use Cases</h2><h3 id="JFrog-Artifactory-Adoption-Tracking"><a href="#JFrog-Artifactory-Adoption-Tracking" class="headerlink" title="JFrog Artifactory Adoption Tracking"></a>JFrog Artifactory Adoption Tracking</h3><p>The original use case that sparked this solution:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Search for JFrog references across all configured branches</span></span><br><span class="line"><span class="variable">$searchQuery</span> = <span class="string">&quot;jfrog.yourcompany.com&quot;</span></span><br><span class="line"><span class="variable">$searchUrl</span> = <span class="string">&quot;https://dev.azure.com/<span class="variable">$organization</span>/<span class="variable">$project</span>/_apis/search/codesearchresults?api-version=7.1-preview.1&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="variable">$searchBody</span> = <span class="selector-tag">@</span>&#123;</span><br><span class="line">    searchText = <span class="variable">$searchQuery</span></span><br><span class="line">    includeFacets = <span class="variable">$true</span></span><br><span class="line">    top = <span class="number">1000</span></span><br><span class="line">&#125; | <span class="built_in">ConvertTo-Json</span></span><br><span class="line"></span><br><span class="line"><span class="variable">$searchResults</span> = <span class="built_in">Invoke-RestMethod</span> <span class="literal">-Uri</span> <span class="variable">$searchUrl</span> <span class="literal">-Headers</span> <span class="variable">$headers</span> <span class="literal">-Method</span> Post <span class="literal">-Body</span> <span class="variable">$searchBody</span> <span class="literal">-ContentType</span> <span class="string">&quot;application/json&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Process results to identify repositories using JFrog</span></span><br></pre></td></tr></table></figure><h3 id="Security-Compliance-Scanning"><a href="#Security-Compliance-Scanning" class="headerlink" title="Security Compliance Scanning"></a>Security Compliance Scanning</h3><p>Scan for security-sensitive patterns across multiple branches:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Search for potential security issues across all searchable branches</span></span><br><span class="line"><span class="variable">$securityPatterns</span> = <span class="selector-tag">@</span>(</span><br><span class="line">    <span class="string">&quot;password\s*=&quot;</span>,</span><br><span class="line">    <span class="string">&quot;api[_-]?key\s*[:=]&quot;</span>,</span><br><span class="line">    <span class="string">&quot;secret[_-]?key\s*[:=]&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="keyword">foreach</span> (<span class="variable">$pattern</span> <span class="keyword">in</span> <span class="variable">$securityPatterns</span>) &#123;</span><br><span class="line">    <span class="comment"># Search across all configured branches</span></span><br><span class="line">    <span class="comment"># Generate compliance reports</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Code-Quality-Assessment"><a href="#Code-Quality-Assessment" class="headerlink" title="Code Quality Assessment"></a>Code Quality Assessment</h3><p>Analyze code patterns and best practices across development branches:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Search for deprecated patterns across main and develop branches</span></span><br><span class="line"><span class="variable">$deprecatedPatterns</span> = <span class="selector-tag">@</span>(</span><br><span class="line">    <span class="string">&quot;System.Web.HttpContext&quot;</span>,</span><br><span class="line">    <span class="string">&quot;ConfigurationManager.AppSettings&quot;</span>,</span><br><span class="line">    <span class="string">&quot;HttpResponse.Write&quot;</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment"># Generate modernization reports</span></span><br></pre></td></tr></table></figure><h2 id="Future-Considerations-and-Alternatives"><a href="#Future-Considerations-and-Alternatives" class="headerlink" title="Future Considerations and Alternatives"></a>Future Considerations and Alternatives</h2><h3 id="Azure-DevOps-CLI-Integration"><a href="#Azure-DevOps-CLI-Integration" class="headerlink" title="Azure DevOps CLI Integration"></a>Azure DevOps CLI Integration</h3><p>While the Azure DevOps CLI doesn’t have native support for searchable branches, you can use <code>az devops invoke</code> to call the REST APIs:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">az devops invoke --area policy --resource configurations --route-parameters project=YourProject --http-method GET --api-version 7.1-preview.1</span><br></pre></td></tr></table></figure><h3 id="PowerShell-Module-Development"><a href="#PowerShell-Module-Development" class="headerlink" title="PowerShell Module Development"></a>PowerShell Module Development</h3><p>Consider creating a dedicated PowerShell module for searchable branch management:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Future PowerShell module structure</span></span><br><span class="line"><span class="built_in">Import-Module</span> AzureDevOpsSearchableBranches</span><br><span class="line"></span><br><span class="line"><span class="built_in">Get-AdoSearchableBranches</span> <span class="literal">-Organization</span> <span class="string">&quot;YourOrg&quot;</span> <span class="literal">-Project</span> <span class="string">&quot;YourProject&quot;</span> <span class="literal">-Repository</span> <span class="string">&quot;YourRepo&quot;</span></span><br><span class="line"><span class="built_in">Set-AdoSearchableBranches</span> <span class="literal">-Organization</span> <span class="string">&quot;YourOrg&quot;</span> <span class="literal">-Project</span> <span class="string">&quot;YourProject&quot;</span> <span class="literal">-Repository</span> <span class="string">&quot;YourRepo&quot;</span> <span class="literal">-Branches</span> <span class="selector-tag">@</span>(<span class="string">&quot;master&quot;</span>, <span class="string">&quot;develop&quot;</span>)</span><br></pre></td></tr></table></figure><h3 id="Microsoft-Graph-Integration"><a href="#Microsoft-Graph-Integration" class="headerlink" title="Microsoft Graph Integration"></a>Microsoft Graph Integration</h3><p>For organizations using Microsoft Graph, consider integrating searchable branch configuration with broader DevOps governance workflows.</p><h2 id="Key-Takeaways"><a href="#Key-Takeaways" class="headerlink" title="Key Takeaways"></a>Key Takeaways</h2><p>Working with Azure DevOps searchable branches requires understanding the underlying Policy Configuration API that Microsoft uses internally but doesn’t officially document for this purpose. The key insights from this solution are:</p><ul><li><strong>Hidden APIs Exist:</strong> Azure DevOps has powerful APIs that aren’t always documented for every use case</li><li><strong>UI Reverse Engineering:</strong> Network traffic analysis can reveal API patterns for automation</li><li><strong>Policy-Based Configuration:</strong> Many Azure DevOps features use the Policy Configuration API under the hood</li><li><strong>Batch Processing Considerations:</strong> Large-scale automation requires careful attention to rate limiting and error handling</li></ul><p>This solution provides a robust foundation for any Azure DevOps organization needing to manage searchable branches at scale. Whether you’re tracking technology adoption, performing security scanning, or implementing code quality initiatives, this approach enables the automation that makes these tasks feasible across large repository collections.</p><p>The undocumented nature of this API means it should be used with appropriate caution and monitoring, but for organizations with thousands of repositories, it’s currently the only viable approach for bulk searchable branch configuration.</p><h2 id="References"><a href="#References" class="headerlink" title="References"></a>References</h2><ul><li><a href="https://docs.microsoft.com/en-us/rest/api/azure/devops/policy/configurations?view=azure-devops-rest-7.1">Azure DevOps REST API - Policy Configurations</a></li><li><a href="https://docs.microsoft.com/en-us/rest/api/azure/devops/git/repositories?view=azure-devops-rest-7.1">Azure DevOps REST API - Git Repositories</a></li><li><a href="https://stackoverflow.com/questions/68167072/cross-search-in-all-repositories-and-branches-in-azure-devops-repos">Stack Overflow: Cross Search in all repositories and branches in Azure DevOps Repos</a></li><li><a href="https://docs.microsoft.com/en-us/azure/devops/project/search/functional-code-search?view=azure-devops">Azure DevOps Code Search Documentation</a></li><li>Main image generated by <a href="https://openai.com/blog/dall-e/">DALL-E</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🎯 TL;DR: Bulk Configure Searchable Branches in Azure DevOps via Hidden Policy API&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Azure DevOps code search only indexes the default branch (master&amp;#x2F;main) by default, causing issues when teams use &lt;code&gt;develop&lt;/code&gt; branches for JFrog Artifactory detection scripts. Problem: No documented API exists for bulk updating searchable branches across thousands of repositories. Solution: Use the undocumented Policy Configuration API with policy type &lt;code&gt;0517f88d-4ec5-4343-9d26-9930ebd53069&lt;/code&gt; to programmatically add branches to the searchable list. This approach leverages the same API calls the Azure DevOps UI uses internally, enabling automation of what would otherwise require manual configuration across massive repository collections.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;Recently, I encountered an interesting challenge while working on a JFrog Artifactory adoption tracking project across a large Azure DevOps organization. The requirement was to scan repositories for JFrog URL references to determine which teams had successfully onboarded to their new artifact management system. The problem? Some development teams exclusively work in &lt;code&gt;develop&lt;/code&gt; branches instead of &lt;code&gt;master&lt;/code&gt; or &lt;code&gt;main&lt;/code&gt;, and Azure DevOps code search only indexes the default branch by default.&lt;/p&gt;
&lt;p&gt;This seemingly simple requirement - adding &lt;code&gt;develop&lt;/code&gt; to the searchable branches for thousands of repositories - turned into a fascinating exploration of Azure DevOps’ undocumented APIs. While there’s no official documentation for bulk updating searchable branches, I discovered that the Azure DevOps UI uses a specific Policy Configuration API under the hood that we can leverage for automation.&lt;/p&gt;
&lt;p&gt;This blog post shares a practical approach to programmatically configure searchable branches across large Azure DevOps organizations using REST APIs that Microsoft doesn’t officially document but absolutely supports.&lt;/p&gt;
&lt;h2 id=&quot;The-Challenge-Azure-DevOps-Code-Search-Limitations&quot;&gt;&lt;a href=&quot;#The-Challenge-Azure-DevOps-Code-Search-Limitations&quot; class=&quot;headerlink&quot; title=&quot;The Challenge: Azure DevOps Code Search Limitations&quot;&gt;&lt;/a&gt;The Challenge: Azure DevOps Code Search Limitations&lt;/h2&gt;&lt;p&gt;Azure DevOps code search is a powerful feature, but it comes with a significant limitation that affects many organizations: by default, only the repository’s default branch (typically &lt;code&gt;master&lt;/code&gt; or &lt;code&gt;main&lt;/code&gt;) is indexed for search operations.&lt;/p&gt;
&lt;p&gt;This creates problems in several scenarios:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JFrog Adoption Tracking:&lt;/strong&gt; Organizations implementing JFrog Artifactory need to scan all repositories for configuration files and dependency references, but teams using feature branches or &lt;code&gt;develop&lt;/code&gt; as their primary branch won’t be detected.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Multi-Branch Development:&lt;/strong&gt; Teams practicing GitFlow or similar branching strategies may have critical code in &lt;code&gt;develop&lt;/code&gt;, &lt;code&gt;release/*&lt;/code&gt;, or feature branches that needs to be searchable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Compliance and Security Scanning:&lt;/strong&gt; Security tools and compliance scripts that rely on code search may miss important files if they’re not in the default branch.&lt;/p&gt;</summary>
    
    
    
    <category term="Azure DevOps" scheme="https://clouddev.blog/categories/Azure-DevOps/"/>
    
    <category term="Azure DevOps API" scheme="https://clouddev.blog/categories/Azure-DevOps/Azure-DevOps-API/"/>
    
    
    <category term="Azure DevOps" scheme="https://clouddev.blog/tags/Azure-DevOps/"/>
    
    <category term="REST API" scheme="https://clouddev.blog/tags/REST-API/"/>
    
    <category term="PowerShell" scheme="https://clouddev.blog/tags/PowerShell/"/>
    
    <category term="Policy Configuration" scheme="https://clouddev.blog/tags/Policy-Configuration/"/>
    
    <category term="Code Search" scheme="https://clouddev.blog/tags/Code-Search/"/>
    
  </entry>
  
  <entry>
    <title>Building Voice Agents with Azure Communication Services Voice Live API and Azure AI Agent Service</title>
    <link href="https://clouddev.blog/Azure/AI/Voice-Live-API/building-voice-agents-with-azure-communication-services-voice-live-api-and-azure-ai-agent-service/"/>
    <id>https://clouddev.blog/Azure/AI/Voice-Live-API/building-voice-agents-with-azure-communication-services-voice-live-api-and-azure-ai-agent-service/</id>
    <published>2025-07-07T12:00:00.000Z</published>
    <updated>2026-03-14T04:23:22.823Z</updated>
    
    <content type="html"><![CDATA[<hr><blockquote><p><strong>🎯 TL;DR: Real-time Voice Agent Implementation</strong></p><p>This post walks through building a voice agent that connects traditional phone calls to Azure’s AI services. The system intercepts incoming calls via Azure Communication Services, streams audio in real-time to the Voice Live API, and processes conversations through pre-configured AI agents in Azure AI Studio. The implementation uses FastAPI for webhook handling, WebSocket connections for bidirectional audio streaming, and Azure Managed Identity for authentication (no API keys to manage). The architecture handles multiple concurrent calls on a single Python thread using asyncio.</p><p><strong>Implementation details:</strong> Audio resampling between 16kHz (ACS requirement) and 24kHz (Voice Live requirement), connection resilience for preview services, and production deployment considerations. <strong><a href="https://github.com/Ricky-G/azure-scenario-hub/tree/main/src/azure-communication-services-integrate-voice-live-api/python">Full source code and documentation available here</a></strong></p></blockquote><hr><p>Recently, I found myself co-leading an innovation project that pushed me into uncharted territory. The challenge? Developing a voice-based agentic solution with an ambitious goal - routing at least 25% of current contact center calls to AI voice agents. This was bleeding-edge stuff, with both the Azure Voice Live API and Azure AI Agent Service voice agents still in preview at the time of writing.</p><p>When you’re working with preview services, documentation is often sparse, and you quickly learn that reverse engineering network calls and maintaining close relationships with product teams becomes part of your daily routine. This blog post shares the practical lessons learned and the working solution we built to integrate these cutting-edge services.</p><h2 id="The-Innovation-Challenge"><a href="#The-Innovation-Challenge" class="headerlink" title="The Innovation Challenge"></a>The Innovation Challenge</h2><p>Building a voice agent system that could handle real customer interactions meant tackling several complex requirements:</p><ul><li>Real-time voice processing with minimal latency</li><li>Natural conversation flow without awkward pauses</li><li>Integration with existing contact center infrastructure</li><li>Scalability to handle multiple concurrent calls</li><li>Reliability for production use cases</li></ul><p>With both <a href="https://learn.microsoft.com/azure/ai-services/speech-service/voice-live">Azure Voice Live API</a> and <a href="https://learn.microsoft.com/azure/ai-foundry/agents/overview">Azure AI Voice Agent Service</a> in preview, we were essentially building on shifting sands. But that’s what innovation is about - pushing boundaries and finding solutions where documentation doesn’t yet exist.</p><h2 id="Understanding-the-Architecture"><a href="#Understanding-the-Architecture" class="headerlink" title="Understanding the Architecture"></a>Understanding the Architecture</h2><p>Our solution bridges Azure Communication Services (ACS) with Azure AI services to create an intelligent voice agent. Here’s how the pieces fit together:</p><pre class="mermaid">graph TB    subgraph "Phone Network"        PSTN[📞 PSTN Number<br/>+1-555-123-4567]    end        subgraph "Azure Communication Services"        ACS[🔗 ACS Call Automation<br/>Event Grid Webhooks]        MEDIA[🎵 Media Streaming<br/>WebSocket Audio]    end        subgraph "Python FastAPI App"        API[🐍 FastAPI Server<br/>localhost:49412]        WS[🔌 WebSocket Handler<br/>Audio Processing]        HANDLER[⚡ Media Handler<br/>Audio Resampling]    end        subgraph "Azure OpenAI"        VOICE[🤖 Voice Live API<br/>Agent Mode<br/>gpt-4o Realtime]        AGENT[👤 Pre-configured Agent<br/>Azure AI Studio]    end        subgraph "Dev Infrastructure"        TUNNEL[🚇 Dev Tunnel<br/>Public HTTPS Endpoint]    end        PSTN -->|Incoming Call| ACS    ACS -->|Webhook Events| TUNNEL    TUNNEL -->|HTTPS| API    ACS -->|WebSocket Audio| WS    WS -->|PCM 16kHz| HANDLER    HANDLER -->|PCM 24kHz| VOICE    VOICE -->|Agent Processing| AGENT    AGENT -->|AI Response| VOICE    VOICE -->|AI Response| HANDLER    HANDLER -->|PCM 16kHz| WS    WS -->|Audio Stream| ACS    ACS -->|Audio| PSTN        style PSTN fill:#ff9999    style ACS fill:#87CEEB    style API fill:#90EE90    style VOICE fill:#DDA0DD    style TUNNEL fill:#F0E68C</pre><h3 id="Core-Components"><a href="#Core-Components" class="headerlink" title="Core Components"></a>Core Components</h3><ol><li><strong>Azure Communication Services</strong>: Handles the telephony infrastructure, providing phone numbers and call routing</li><li><strong>Voice Live API</strong>: Enables real-time speech recognition and synthesis with WebRTC streaming</li><li><strong>Azure AI Agent Service</strong>: Provides the intelligence layer for understanding and responding to customer queries</li><li><strong>WebSocket Bridge</strong>: Our custom Python application that connects these services</li></ol><span id="more"></span><h3 id="The-Flow"><a href="#The-Flow" class="headerlink" title="The Flow"></a>The Flow</h3><p>When a customer calls, here’s what happens behind the scenes:</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Customer Call → ACS Phone Number → Webhook to Our Service → </span><br><span class="line">WebSocket Connection → Voice Live API ↔ AI Agent Service → </span><br><span class="line">Real-time Voice Response → Customer</span><br></pre></td></tr></table></figure><h2 id="Setting-Up-the-Foundation"><a href="#Setting-Up-the-Foundation" class="headerlink" title="Setting Up the Foundation"></a>Setting Up the Foundation</h2><p>Let’s walk through the practical implementation. You can find the complete code in my <a href="https://github.com/Ricky-G/azure-scenario-hub/tree/main/src/azure-communication-services-integrate-voice-live-api/python">GitHub repository</a>.</p><h3 id="Prerequisites"><a href="#Prerequisites" class="headerlink" title="Prerequisites"></a>Prerequisites</h3><p>First, you’ll need to set up several Azure services. Here’s what we discovered through trial and error:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Required Azure services</span></span><br><span class="line">- Azure Communication Services (with phone number provisioning)</span><br><span class="line">- Azure AI Services (Speech Service enabled)</span><br><span class="line">- Azure AI Agent Service (with voice capabilities)</span><br><span class="line">- Azure App Service or Container Instance (<span class="keyword">for</span> hosting)</span><br></pre></td></tr></table></figure><h3 id="Environment-Configuration"><a href="#Environment-Configuration" class="headerlink" title="Environment Configuration"></a>Environment Configuration</h3><p>One of the first challenges was figuring out all the required configuration parameters. Here’s what you’ll need:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Essential environment variables (Using Azure Managed Identity - No API Keys!)</span></span><br><span class="line">ACS_CONNECTION_STRING = <span class="string">&quot;endpoint=https://your-acs.communication.azure.com/;accesskey=your-key&quot;</span></span><br><span class="line">AZURE_VOICE_LIVE_ENDPOINT = <span class="string">&quot;https://your-aoai.cognitiveservices.azure.com/&quot;</span></span><br><span class="line">AGENT_ID = <span class="string">&quot;your_agent_id_from_azure_ai_studio&quot;</span></span><br><span class="line">AGENT_PROJECT_NAME = <span class="string">&quot;your_project_name&quot;</span></span><br><span class="line">BASE_URL = <span class="string">&quot;https://your-tunnel-url.asse.devtunnels.ms&quot;</span>  <span class="comment"># Dev Tunnel URL</span></span><br></pre></td></tr></table></figure><h2 id="Building-the-WebSocket-Bridge"><a href="#Building-the-WebSocket-Bridge" class="headerlink" title="Building the WebSocket Bridge"></a>Building the WebSocket Bridge</h2><p>The heart of our solution is a Python application that acts as a bridge between ACS and the Voice Live API. This wasn’t documented anywhere - we had to figure it out by analyzing network traffic and experimenting.</p><h3 id="Handling-Incoming-Calls"><a href="#Handling-Incoming-Calls" class="headerlink" title="Handling Incoming Calls"></a>Handling Incoming Calls</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">from</span> fastapi <span class="keyword">import</span> FastAPI, WebSocket</span><br><span class="line"><span class="keyword">from</span> azure.communication.callautomation <span class="keyword">import</span> CallAutomationClient</span><br><span class="line"><span class="keyword">from</span> azure.identity <span class="keyword">import</span> DefaultAzureCredential</span><br><span class="line"><span class="keyword">import</span> asyncio</span><br><span class="line"><span class="keyword">import</span> websockets</span><br><span class="line"></span><br><span class="line">app = FastAPI()</span><br><span class="line">call_automation_client = CallAutomationClient.from_connection_string(</span><br><span class="line">    ACS_CONNECTION_STRING</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="meta">@app.post(<span class="params"><span class="string">&quot;/api/incomingCall&quot;</span></span>)</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">incoming_call</span>(<span class="params">request: <span class="built_in">dict</span></span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;Handle incoming call webhook from ACS&quot;&quot;&quot;</span></span><br><span class="line">    <span class="keyword">try</span>:</span><br><span class="line">        <span class="comment"># Parse the incoming call context</span></span><br><span class="line">        incoming_call_context = request.get(<span class="string">&quot;incomingCallContext&quot;</span>)</span><br><span class="line">        </span><br><span class="line">        <span class="comment"># Answer the call</span></span><br><span class="line">        call_connection = call_automation_client.answer_call(</span><br><span class="line">            incoming_call_context=incoming_call_context,</span><br><span class="line">            callback_url=<span class="string">f&quot;<span class="subst">&#123;CALLBACK_URI&#125;</span>/api/callbacks/<span class="subst">&#123;call_id&#125;</span>&quot;</span>,</span><br><span class="line">        )</span><br><span class="line">        </span><br><span class="line">        <span class="comment"># Start WebSocket connection to Voice Live API</span></span><br><span class="line">        asyncio.create_task(</span><br><span class="line">            establish_voice_connection(call_connection.call_connection_id)</span><br><span class="line">        )</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">return</span> &#123;<span class="string">&quot;status&quot;</span>: <span class="string">&quot;success&quot;</span>&#125;</span><br><span class="line">        </span><br><span class="line">    <span class="keyword">except</span> Exception <span class="keyword">as</span> e:</span><br><span class="line">        logger.error(<span class="string">f&quot;Error handling incoming call: <span class="subst">&#123;e&#125;</span>&quot;</span>)</span><br><span class="line">        <span class="keyword">return</span> &#123;<span class="string">&quot;error&quot;</span>: <span class="built_in">str</span>(e)&#125;</span><br></pre></td></tr></table></figure><h3 id="Establishing-the-Voice-Connection"><a href="#Establishing-the-Voice-Connection" class="headerlink" title="Establishing the Voice Connection"></a>Establishing the Voice Connection</h3><p>This is where things got interesting. The Voice Live API uses WebRTC for real-time audio streaming, but the documentation was minimal. Here’s what we discovered:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">establish_voice_connection</span>(<span class="params">call_connection_id</span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;Establish WebSocket connection to Voice Live API using Azure Managed Identity&quot;&quot;&quot;</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment"># Get access token using managed identity</span></span><br><span class="line">    <span class="keyword">from</span> azure.identity <span class="keyword">import</span> DefaultAzureCredential</span><br><span class="line">    credential = DefaultAzureCredential()</span><br><span class="line">    token = credential.get_token(<span class="string">&quot;https://cognitiveservices.azure.com/.default&quot;</span>)</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># Construct the WebSocket URL for Voice Live API</span></span><br><span class="line">    ws_url = <span class="string">f&quot;wss://your-region.cognitiveservices.azure.com/openai/realtime?api-version=2024-10-01-preview&quot;</span></span><br><span class="line">    </span><br><span class="line">    headers = &#123;</span><br><span class="line">        <span class="string">&quot;Authorization&quot;</span>: <span class="string">f&quot;Bearer <span class="subst">&#123;token.token&#125;</span>&quot;</span>,</span><br><span class="line">        <span class="string">&quot;OpenAI-Beta&quot;</span>: <span class="string">&quot;realtime=v1&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">async</span> <span class="keyword">with</span> websockets.connect(ws_url, extra_headers=headers) <span class="keyword">as</span> websocket:</span><br><span class="line">        <span class="comment"># Initialize session with Agent ID</span></span><br><span class="line">        <span class="keyword">await</span> websocket.send(json.dumps(&#123;</span><br><span class="line">            <span class="string">&quot;type&quot;</span>: <span class="string">&quot;session.update&quot;</span>,</span><br><span class="line">            <span class="string">&quot;session&quot;</span>: &#123;</span><br><span class="line">                <span class="string">&quot;agent&quot;</span>: &#123;</span><br><span class="line">                    <span class="string">&quot;agent_id&quot;</span>: AGENT_ID,</span><br><span class="line">                    <span class="string">&quot;project_name&quot;</span>: AGENT_PROJECT_NAME</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;))</span><br><span class="line">        </span><br><span class="line">        <span class="comment"># Handle bidirectional audio streaming</span></span><br><span class="line">        <span class="keyword">await</span> asyncio.gather(</span><br><span class="line">            receive_audio_from_caller(websocket, call_connection_id),</span><br><span class="line">            send_audio_to_caller(websocket, call_connection_id)</span><br><span class="line">        )</span><br></pre></td></tr></table></figure><h2 id="Integrating-with-Azure-AI-Agent-Service"><a href="#Integrating-with-Azure-AI-Agent-Service" class="headerlink" title="Integrating with Azure AI Agent Service"></a>Integrating with Azure AI Agent Service</h2><p>The AI Agent Service provides the intelligence for our voice agent. Here’s how we connected it:</p><h3 id="Processing-Voice-Input"><a href="#Processing-Voice-Input" class="headerlink" title="Processing Voice Input"></a>Processing Voice Input</h3><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">process_voice_with_agent</span>(<span class="params">audio_data, session_id</span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;Send audio directly to Voice Live API in Agent Mode&quot;&quot;&quot;</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment"># Using Azure Managed Identity - no API keys needed</span></span><br><span class="line">    <span class="keyword">from</span> azure.identity <span class="keyword">import</span> DefaultAzureCredential</span><br><span class="line">    credential = DefaultAzureCredential()</span><br><span class="line">    token = credential.get_token(<span class="string">&quot;https://cognitiveservices.azure.com/.default&quot;</span>)</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># Send audio input event to Voice Live API</span></span><br><span class="line">    audio_event = &#123;</span><br><span class="line">        <span class="string">&quot;type&quot;</span>: <span class="string">&quot;input_audio_buffer.append&quot;</span>,</span><br><span class="line">        <span class="string">&quot;audio&quot;</span>: base64.b64encode(audio_data).decode()</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># Voice Live API will handle agent processing automatically</span></span><br><span class="line">    <span class="comment"># when configured with agent_id in session.update</span></span><br><span class="line">    <span class="keyword">return</span> audio_event</span><br></pre></td></tr></table></figure><h2 id="Handling-Real-World-Challenges"><a href="#Handling-Real-World-Challenges" class="headerlink" title="Handling Real-World Challenges"></a>Handling Real-World Challenges</h2><p>Working with preview services meant encountering numerous undocumented behaviors. Here are some key challenges we solved:</p><h3 id="1-Audio-Format-Compatibility"><a href="#1-Audio-Format-Compatibility" class="headerlink" title="1. Audio Format Compatibility"></a>1. Audio Format Compatibility</h3><p>The Voice Live API expects specific audio formats. We discovered through trial and error:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Audio configuration that actually works (Voice Live API format)</span></span><br><span class="line">AUDIO_CONFIG = &#123;</span><br><span class="line">    <span class="string">&quot;format&quot;</span>: <span class="string">&quot;pcm16&quot;</span>,  <span class="comment"># 16-bit PCM for Voice Live API</span></span><br><span class="line">    <span class="string">&quot;sample_rate&quot;</span>: <span class="number">24000</span>,  <span class="comment"># 24kHz required by Voice Live</span></span><br><span class="line">    <span class="string">&quot;channels&quot;</span>: <span class="number">1</span>  <span class="comment"># Mono</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment"># ACS requires 16kHz, so we need resampling</span></span><br><span class="line">ACS_AUDIO_CONFIG = &#123;</span><br><span class="line">    <span class="string">&quot;format&quot;</span>: <span class="string">&quot;pcm16&quot;</span>,</span><br><span class="line">    <span class="string">&quot;sample_rate&quot;</span>: <span class="number">16000</span>,  <span class="comment"># ACS requirement</span></span><br><span class="line">    <span class="string">&quot;channels&quot;</span>: <span class="number">1</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-Latency-Optimization"><a href="#2-Latency-Optimization" class="headerlink" title="2. Latency Optimization"></a>2. Latency Optimization</h3><p>To achieve natural conversation flow, we implemented several optimizations:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Start voice synthesis before full response is ready</span></span><br><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">stream_synthesize_speech</span>(<span class="params">text_stream</span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;Synthesize speech in chunks for lower latency&quot;&quot;&quot;</span></span><br><span class="line">    </span><br><span class="line">    buffer = <span class="string">&quot;&quot;</span></span><br><span class="line">    <span class="keyword">async</span> <span class="keyword">for</span> chunk <span class="keyword">in</span> text_stream:</span><br><span class="line">        buffer += chunk</span><br><span class="line">        </span><br><span class="line">        <span class="comment"># Send to synthesis when we have a complete sentence</span></span><br><span class="line">        <span class="keyword">if</span> <span class="built_in">any</span>(punct <span class="keyword">in</span> buffer <span class="keyword">for</span> punct <span class="keyword">in</span> [<span class="string">&#x27;.&#x27;</span>, <span class="string">&#x27;!&#x27;</span>, <span class="string">&#x27;?&#x27;</span>]):</span><br><span class="line">            <span class="keyword">await</span> synthesize_and_send(buffer)</span><br><span class="line">            buffer = <span class="string">&quot;&quot;</span></span><br></pre></td></tr></table></figure><h3 id="3-Connection-Resilience"><a href="#3-Connection-Resilience" class="headerlink" title="3. Connection Resilience"></a>3. Connection Resilience</h3><p>Preview services can be unstable. We added robust error handling:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">async</span> <span class="keyword">def</span> <span class="title function_">maintain_connection</span>(<span class="params">websocket, call_id</span>):</span><br><span class="line">    <span class="string">&quot;&quot;&quot;Maintain WebSocket connection with automatic reconnection&quot;&quot;&quot;</span></span><br><span class="line">    </span><br><span class="line">    retry_count = <span class="number">0</span></span><br><span class="line">    max_retries = <span class="number">3</span></span><br><span class="line">    </span><br><span class="line">    <span class="keyword">while</span> retry_count &lt; max_retries:</span><br><span class="line">        <span class="keyword">try</span>:</span><br><span class="line">            <span class="keyword">await</span> websocket.ping()</span><br><span class="line">            <span class="keyword">await</span> asyncio.sleep(<span class="number">30</span>)  <span class="comment"># Ping every 30 seconds</span></span><br><span class="line">            </span><br><span class="line">        <span class="keyword">except</span> websockets.ConnectionClosed:</span><br><span class="line">            logger.warning(<span class="string">f&quot;Connection lost for call <span class="subst">&#123;call_id&#125;</span>&quot;</span>)</span><br><span class="line">            retry_count += <span class="number">1</span></span><br><span class="line">            <span class="keyword">await</span> asyncio.sleep(<span class="number">2</span> ** retry_count)  <span class="comment"># Exponential backoff</span></span><br><span class="line">            </span><br><span class="line">            <span class="comment"># Attempt reconnection</span></span><br><span class="line">            websocket = <span class="keyword">await</span> reconnect_websocket(call_id)</span><br></pre></td></tr></table></figure><h2 id="Deployment-Considerations"><a href="#Deployment-Considerations" class="headerlink" title="Deployment Considerations"></a>Deployment Considerations</h2><p>When deploying this solution, we learned several important lessons:</p><h3 id="Container-Deployment"><a href="#Container-Deployment" class="headerlink" title="Container Deployment"></a>Container Deployment</h3><p>We packaged our Python application as a container for easier deployment:</p><figure class="highlight dockerfile"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">FROM</span> python:<span class="number">3.11</span>-slim</span><br><span class="line"></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="language-bash"> /app</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Install system dependencies for audio processing</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> apt-get update &amp;&amp; apt-get install -y \</span></span><br><span class="line"><span class="language-bash">    libopus0 \</span></span><br><span class="line"><span class="language-bash">    libopus-dev \</span></span><br><span class="line"><span class="language-bash">    &amp;&amp; <span class="built_in">rm</span> -rf /var/lib/apt/lists/*</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> requirements.txt .</span></span><br><span class="line"><span class="keyword">RUN</span><span class="language-bash"> pip install --no-cache-dir -r requirements.txt</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">COPY</span><span class="language-bash"> . .</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">CMD</span><span class="language-bash"> [<span class="string">&quot;python&quot;</span>, <span class="string">&quot;start.py&quot;</span>]</span></span><br></pre></td></tr></table></figure><h3 id="Scaling-Considerations"><a href="#Scaling-Considerations" class="headerlink" title="Scaling Considerations"></a>Scaling Considerations</h3><p>For handling multiple concurrent calls:</p><ol><li><strong>Use Azure Container Instances</strong> or <strong>App Service</strong> with autoscaling</li><li><strong>Implement connection pooling</strong> for WebSocket connections</li><li><strong>Monitor memory usage</strong> - audio processing can be memory-intensive</li></ol><h2 id="Monitoring-and-Debugging"><a href="#Monitoring-and-Debugging" class="headerlink" title="Monitoring and Debugging"></a>Monitoring and Debugging</h2><p>Working with preview services means extensive logging is crucial:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> logging</span><br><span class="line"><span class="keyword">from</span> azure.monitor.opentelemetry <span class="keyword">import</span> configure_azure_monitor</span><br><span class="line"></span><br><span class="line"><span class="comment"># Configure Azure Monitor for production debugging</span></span><br><span class="line">configure_azure_monitor(</span><br><span class="line">    connection_string=APPLICATIONINSIGHTS_CONNECTION_STRING</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment"># Log all WebSocket events</span></span><br><span class="line">logging.getLogger(<span class="string">&#x27;websockets&#x27;</span>).setLevel(logging.DEBUG)</span><br></pre></td></tr></table></figure><h2 id="Lessons-Learned"><a href="#Lessons-Learned" class="headerlink" title="Lessons Learned"></a>Lessons Learned</h2><p>After weeks of development and close collaboration with Azure product teams, here are our key takeaways:</p><ol><li><strong>Preview Services Require Patience</strong>: Be prepared for undocumented features and changing APIs</li><li><strong>Network Analysis is Your Friend</strong>: Tools like Wireshark helped us understand the protocol</li><li><strong>Build in Resilience</strong>: Assume connections will drop and services will be intermittently unavailable</li><li><strong>Start Simple</strong>: Get basic voice working before adding complex AI interactions</li><li><strong>Monitor Everything</strong>: You’ll need extensive logging to debug issues in production</li></ol><h2 id="Get-Started"><a href="#Get-Started" class="headerlink" title="Get Started"></a>Get Started</h2><p>Ready to build your own voice agent? Check out the complete implementation in my <a href="https://github.com/Ricky-G/azure-scenario-hub/tree/main/src/azure-communication-services-integrate-voice-live-api/python">GitHub repository</a>. The repository includes:</p><ul><li>Complete Python application code</li><li>Deployment scripts and Docker configuration</li><li>Environment setup instructions</li><li>Troubleshooting guide</li></ul><p>Remember, innovation often means venturing into undocumented territory. Don’t be afraid to experiment, reverse-engineer, and collaborate with product teams. The future of voice-based AI agents is being written right now, and you can be part of it.</p><h2 id="References"><a href="#References" class="headerlink" title="References"></a>References</h2><ul><li><a href="https://learn.microsoft.com/azure/ai-services/speech-service/voice-live">Azure Voice Live API Documentation</a></li><li><a href="https://learn.microsoft.com/azure/ai-foundry/agents/overview">Azure AI Agent Service Overview</a></li><li><a href="https://github.com/Ricky-G/azure-scenario-hub/tree/main/src/azure-communication-services-integrate-voice-live-api/python">Complete Code Repository</a></li><li><a href="https://learn.microsoft.com/azure/communication-services/">Azure Communication Services Documentation</a></li><li>Main image generated by <a href="https://openai.com/blog/dall-e/">DALL-E</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🎯 TL;DR: Real-time Voice Agent Implementation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This post walks through building a voice agent that connects traditional phone calls to Azure’s AI services. The system intercepts incoming calls via Azure Communication Services, streams audio in real-time to the Voice Live API, and processes conversations through pre-configured AI agents in Azure AI Studio. The implementation uses FastAPI for webhook handling, WebSocket connections for bidirectional audio streaming, and Azure Managed Identity for authentication (no API keys to manage). The architecture handles multiple concurrent calls on a single Python thread using asyncio.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Implementation details:&lt;/strong&gt; Audio resampling between 16kHz (ACS requirement) and 24kHz (Voice Live requirement), connection resilience for preview services, and production deployment considerations. &lt;strong&gt;&lt;a href=&quot;https://github.com/Ricky-G/azure-scenario-hub/tree/main/src/azure-communication-services-integrate-voice-live-api/python&quot;&gt;Full source code and documentation available here&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;Recently, I found myself co-leading an innovation project that pushed me into uncharted territory. The challenge? Developing a voice-based agentic solution with an ambitious goal - routing at least 25% of current contact center calls to AI voice agents. This was bleeding-edge stuff, with both the Azure Voice Live API and Azure AI Agent Service voice agents still in preview at the time of writing.&lt;/p&gt;
&lt;p&gt;When you’re working with preview services, documentation is often sparse, and you quickly learn that reverse engineering network calls and maintaining close relationships with product teams becomes part of your daily routine. This blog post shares the practical lessons learned and the working solution we built to integrate these cutting-edge services.&lt;/p&gt;
&lt;h2 id=&quot;The-Innovation-Challenge&quot;&gt;&lt;a href=&quot;#The-Innovation-Challenge&quot; class=&quot;headerlink&quot; title=&quot;The Innovation Challenge&quot;&gt;&lt;/a&gt;The Innovation Challenge&lt;/h2&gt;&lt;p&gt;Building a voice agent system that could handle real customer interactions meant tackling several complex requirements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Real-time voice processing with minimal latency&lt;/li&gt;
&lt;li&gt;Natural conversation flow without awkward pauses&lt;/li&gt;
&lt;li&gt;Integration with existing contact center infrastructure&lt;/li&gt;
&lt;li&gt;Scalability to handle multiple concurrent calls&lt;/li&gt;
&lt;li&gt;Reliability for production use cases&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With both &lt;a href=&quot;https://learn.microsoft.com/azure/ai-services/speech-service/voice-live&quot;&gt;Azure Voice Live API&lt;/a&gt; and &lt;a href=&quot;https://learn.microsoft.com/azure/ai-foundry/agents/overview&quot;&gt;Azure AI Voice Agent Service&lt;/a&gt; in preview, we were essentially building on shifting sands. But that’s what innovation is about - pushing boundaries and finding solutions where documentation doesn’t yet exist.&lt;/p&gt;
&lt;h2 id=&quot;Understanding-the-Architecture&quot;&gt;&lt;a href=&quot;#Understanding-the-Architecture&quot; class=&quot;headerlink&quot; title=&quot;Understanding the Architecture&quot;&gt;&lt;/a&gt;Understanding the Architecture&lt;/h2&gt;&lt;p&gt;Our solution bridges Azure Communication Services (ACS) with Azure AI services to create an intelligent voice agent. Here’s how the pieces fit together:&lt;/p&gt;
&lt;pre class=&quot;mermaid&quot;&gt;graph TB
    subgraph &quot;Phone Network&quot;
        PSTN[📞 PSTN Number&lt;br/&gt;+1-555-123-4567]
    end
    
    subgraph &quot;Azure Communication Services&quot;
        ACS[🔗 ACS Call Automation&lt;br/&gt;Event Grid Webhooks]
        MEDIA[🎵 Media Streaming&lt;br/&gt;WebSocket Audio]
    end
    
    subgraph &quot;Python FastAPI App&quot;
        API[🐍 FastAPI Server&lt;br/&gt;localhost:49412]
        WS[🔌 WebSocket Handler&lt;br/&gt;Audio Processing]
        HANDLER[⚡ Media Handler&lt;br/&gt;Audio Resampling]
    end
    
    subgraph &quot;Azure OpenAI&quot;
        VOICE[🤖 Voice Live API&lt;br/&gt;Agent Mode&lt;br/&gt;gpt-4o Realtime]
        AGENT[👤 Pre-configured Agent&lt;br/&gt;Azure AI Studio]
    end
    
    subgraph &quot;Dev Infrastructure&quot;
        TUNNEL[🚇 Dev Tunnel&lt;br/&gt;Public HTTPS Endpoint]
    end
    
    PSTN --&gt;|Incoming Call| ACS
    ACS --&gt;|Webhook Events| TUNNEL
    TUNNEL --&gt;|HTTPS| API
    ACS --&gt;|WebSocket Audio| WS
    WS --&gt;|PCM 16kHz| HANDLER
    HANDLER --&gt;|PCM 24kHz| VOICE
    VOICE --&gt;|Agent Processing| AGENT
    AGENT --&gt;|AI Response| VOICE
    VOICE --&gt;|AI Response| HANDLER
    HANDLER --&gt;|PCM 16kHz| WS
    WS --&gt;|Audio Stream| ACS
    ACS --&gt;|Audio| PSTN
    
    style PSTN fill:#ff9999
    style ACS fill:#87CEEB
    style API fill:#90EE90
    style VOICE fill:#DDA0DD
    style TUNNEL fill:#F0E68C&lt;/pre&gt;

&lt;h3 id=&quot;Core-Components&quot;&gt;&lt;a href=&quot;#Core-Components&quot; class=&quot;headerlink&quot; title=&quot;Core Components&quot;&gt;&lt;/a&gt;Core Components&lt;/h3&gt;&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Azure Communication Services&lt;/strong&gt;: Handles the telephony infrastructure, providing phone numbers and call routing&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Voice Live API&lt;/strong&gt;: Enables real-time speech recognition and synthesis with WebRTC streaming&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Azure AI Agent Service&lt;/strong&gt;: Provides the intelligence layer for understanding and responding to customer queries&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WebSocket Bridge&lt;/strong&gt;: Our custom Python application that connects these services&lt;/li&gt;
&lt;/ol&gt;</summary>
    
    
    
    <category term="Azure" scheme="https://clouddev.blog/categories/Azure/"/>
    
    <category term="AI" scheme="https://clouddev.blog/categories/Azure/AI/"/>
    
    <category term="Voice Live API" scheme="https://clouddev.blog/categories/Azure/AI/Voice-Live-API/"/>
    
    
    <category term="Azure Communication Services" scheme="https://clouddev.blog/tags/Azure-Communication-Services/"/>
    
    <category term="Azure AI Agent Service" scheme="https://clouddev.blog/tags/Azure-AI-Agent-Service/"/>
    
    <category term="Voice Live API" scheme="https://clouddev.blog/tags/Voice-Live-API/"/>
    
    <category term="AI Voice Agents" scheme="https://clouddev.blog/tags/AI-Voice-Agents/"/>
    
    <category term="Contact Center" scheme="https://clouddev.blog/tags/Contact-Center/"/>
    
    <category term="Python" scheme="https://clouddev.blog/tags/Python/"/>
    
    <category term="WebRTC" scheme="https://clouddev.blog/tags/WebRTC/"/>
    
  </entry>
  
  <entry>
    <title>Getting TFVC Repository Structure via Azure DevOps Server API</title>
    <link href="https://clouddev.blog/Azure-DevOps/Azure-DevOps-API/getting-tfvc-repository-structure-via-azure-devops-server-api/"/>
    <id>https://clouddev.blog/Azure-DevOps/Azure-DevOps-API/getting-tfvc-repository-structure-via-azure-devops-server-api/</id>
    <published>2025-06-17T12:00:00.000Z</published>
    <updated>2025-08-06T11:03:44.566Z</updated>
    
    <content type="html"><![CDATA[<hr><blockquote><p><strong>🎯 TL;DR: Retrieving TFVC Repository Structure via REST API</strong></p><p>This post demonstrates how to programmatically enumerate TFVC repository folders using Azure DevOps Server REST APIs. Unlike Git repositories, TFVC follows a one-repository-per-project model with hierarchical folder structures starting at <code>$/ProjectName</code>. The solution uses the TFVC Items API with specific parameters: <code>scopePath=$/ProjectName</code> to target the project root, and <code>recursionLevel=OneLevel</code> to retrieve immediate children. The implementation handles authentication via Personal Access Tokens, filters results to show only folders (excluding the root), and includes error handling for projects without TFVC repositories or insufficient permissions.</p><p><strong>Key technical details:</strong> PowerShell script implementation, proper API parameter usage, authentication setup, and handling edge cases like empty repositories and access permissions. <strong><a href="https://github.com/Ricky-G/script-library/blob/main/TFS-TFVC-Scripts-README.md">Complete PowerShell script and utilities available here</a></strong></p></blockquote><hr><p>Recently, I was asked an interesting question by a developer who was struggling with Azure DevOps Server APIs around fetching repository metadata for legacy TFVC structures as part of a GitHub migration from ADO Server. This was a nice little problem to solve because, let’s be honest, we don’t really deal with these legacy TFVC repositories much anymore. Most teams have migrated to Git, and the documentation around TFVC API interactions has become somewhat sparse over the years.</p><p>The challenge was straightforward but frustrating: they could retrieve project information just fine, but getting the actual TFVC folder structure within each project? That’s where things got tricky. After doing a bit of digging through the API documentation and testing different approaches, I’m happy to say that yes, it is absolutely possible to enumerate all TFVC repositories and their folder structures programmatically.</p><p>This blog post shares the solution I put together - a practical approach to retrieve TFVC repository structure using the Azure DevOps Server REST APIs. If you’re working with legacy TFVC repositories and need to interact with them programmatically, this one’s for you.</p><h2 id="The-Challenge-Understanding-TFVC-API-Limitations"><a href="#The-Challenge-Understanding-TFVC-API-Limitations" class="headerlink" title="The Challenge: Understanding TFVC API Limitations"></a>The Challenge: Understanding TFVC API Limitations</h2><p>Unlike Git repositories where each project can contain multiple repos, TFVC follows a different model where each project contains exactly one TFVC repository. This fundamental difference affects how you interact with the API and retrieve repository information.</p><p>The main challenge developers face is distinguishing between project metadata and actual TFVC repository structure. When calling the standard Projects API, you receive project information but not the folder structure within the TFVC repository itself.</p><span id="more"></span><h2 id="Common-Misconceptions-About-TFVC-APIs"><a href="#Common-Misconceptions-About-TFVC-APIs" class="headerlink" title="Common Misconceptions About TFVC APIs"></a>Common Misconceptions About TFVC APIs</h2><p>Many developers make the mistake of thinking that the Projects API or the Items API with default parameters will return the TFVC folder structure. Here’s what typically happens:</p><p><strong>What doesn’t work:</strong></p><ul><li>Using only the Projects API - returns project metadata, not TFVC structure</li><li>Calling Items API without proper <code>scopePath</code> parameter - returns all items across the organization</li><li>Using the Branches API - doesn’t apply to TFVC repositories the same way as Git</li></ul><p><strong>The root cause:</strong> The API requires specific parameters to traverse the TFVC hierarchy correctly.</p><h2 id="Understanding-TFVC-Repository-Structure"><a href="#Understanding-TFVC-Repository-Structure" class="headerlink" title="Understanding TFVC Repository Structure"></a>Understanding TFVC Repository Structure</h2><p>In TFVC, the repository structure follows this pattern:</p><ul><li>Each project has one TFVC repository</li><li>The repository root is always <code>$/ProjectName</code></li><li>Folders are organized hierarchically under this root</li><li>You need to specify recursion levels to control how deep the API traverses</li></ul><h2 id="The-Solution-Targeted-API-Calls-with-Proper-Parameters"><a href="#The-Solution-Targeted-API-Calls-with-Proper-Parameters" class="headerlink" title="The Solution: Targeted API Calls with Proper Parameters"></a>The Solution: Targeted API Calls with Proper Parameters</h2><p>The key to retrieving TFVC folder structure lies in using the correct combination of API endpoints and parameters. Here’s the step-by-step approach:</p><h3 id="Step-1-Get-All-Projects"><a href="#Step-1-Get-All-Projects" class="headerlink" title="Step 1: Get All Projects"></a>Step 1: Get All Projects</h3><p>First, retrieve all projects in your Azure DevOps Server instance using the Projects API.</p><h3 id="Step-2-Query-TFVC-Items-for-Each-Project"><a href="#Step-2-Query-TFVC-Items-for-Each-Project" class="headerlink" title="Step 2: Query TFVC Items for Each Project"></a>Step 2: Query TFVC Items for Each Project</h3><p>For each project, call the TFVC Items API with these specific parameters:</p><ul><li><code>scopePath=$/ProjectName</code> - Sets the starting point to the project’s TFVC root</li><li><code>recursionLevel=OneLevel</code> - Returns immediate children only (not recursive)</li><li>Filter results to show only folders, excluding the root itself</li></ul><h2 id="PowerShell-Script-Implementation"><a href="#PowerShell-Script-Implementation" class="headerlink" title="PowerShell Script Implementation"></a>PowerShell Script Implementation</h2><p>Here’s a complete PowerShell script that implements this solution. You can find the full script and additional TFVC utilities in my <a href="https://github.com/Ricky-G/script-library/blob/main/TFS-TFVC-Scripts-README.md">TFS&#x2F;TFVC Scripts collection</a>:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Azure DevOps Server configuration</span></span><br><span class="line"><span class="variable">$tfsUrl</span> = <span class="string">&quot;https://mytfs.com&quot;</span></span><br><span class="line"><span class="variable">$PAT</span> = <span class="string">&quot;your-personal-access-token&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Create authentication headers</span></span><br><span class="line"><span class="variable">$headers</span> = <span class="selector-tag">@</span>&#123;</span><br><span class="line">    Authorization = <span class="string">&quot;Basic &quot;</span> + [<span class="type">Convert</span>]::ToBase64String([<span class="type">Text.Encoding</span>]::ASCII.GetBytes(<span class="string">&quot;:<span class="variable">$PAT</span>&quot;</span>))</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment"># Get all projects</span></span><br><span class="line"><span class="variable">$projects</span> = <span class="built_in">Invoke-RestMethod</span> <span class="literal">-Uri</span> <span class="string">&quot;<span class="variable">$tfsUrl</span>/DefaultCollection/_apis/projects?api-version=5.0&quot;</span> <span class="literal">-Headers</span> <span class="variable">$headers</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">foreach</span> (<span class="variable">$project</span> <span class="keyword">in</span> <span class="variable">$projects</span>.value) &#123;</span><br><span class="line">    <span class="built_in">Write-Host</span> <span class="string">&quot;Project: <span class="variable">$</span>(<span class="variable">$project</span>.name)&quot;</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment"># Construct TFVC path for this project</span></span><br><span class="line">    <span class="variable">$tfvcPath</span> = <span class="string">&quot;`$/<span class="variable">$</span>(<span class="variable">$project</span>.name)&quot;</span></span><br><span class="line">    <span class="variable">$itemsUrl</span> = <span class="string">&quot;<span class="variable">$tfsUrl</span>/DefaultCollection/<span class="variable">$</span>(<span class="variable">$project</span>.id)/_apis/tfvc/items?scopePath=<span class="variable">$tfvcPath</span>&amp;recursionLevel=OneLevel&amp;api-version=5.0&quot;</span></span><br><span class="line">    </span><br><span class="line">    <span class="keyword">try</span> &#123;</span><br><span class="line">        <span class="comment"># Get TFVC items for this project</span></span><br><span class="line">        <span class="variable">$items</span> = <span class="built_in">Invoke-RestMethod</span> <span class="literal">-Uri</span> <span class="variable">$itemsUrl</span> <span class="literal">-Headers</span> <span class="variable">$headers</span></span><br><span class="line">        </span><br><span class="line">        <span class="comment"># Filter to get only folders (excluding the root itself)</span></span><br><span class="line">        <span class="variable">$folders</span> = <span class="variable">$items</span>.value | <span class="built_in">Where-Object</span> &#123;</span><br><span class="line">            <span class="variable">$_</span>.isFolder <span class="operator">-eq</span> <span class="variable">$true</span> <span class="operator">-and</span></span><br><span class="line">            <span class="variable">$_</span>.path <span class="operator">-ne</span> <span class="variable">$tfvcPath</span></span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">foreach</span> (<span class="variable">$folder</span> <span class="keyword">in</span> <span class="variable">$folders</span>) &#123;</span><br><span class="line">            <span class="built_in">Write-Host</span> <span class="string">&quot;  - <span class="variable">$</span>(<span class="variable">$folder</span>.path)&quot;</span></span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">        <span class="keyword">if</span> (<span class="variable">$folders</span>.Count <span class="operator">-eq</span> <span class="number">0</span>) &#123;</span><br><span class="line">            <span class="built_in">Write-Host</span> <span class="string">&quot;  No top-level folders found&quot;</span></span><br><span class="line">        &#125;</span><br><span class="line">        </span><br><span class="line">    &#125; <span class="keyword">catch</span> &#123;</span><br><span class="line">        <span class="built_in">Write-Host</span> <span class="string">&quot;  No TFVC content or access denied&quot;</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Script-Configuration-and-Security"><a href="#Script-Configuration-and-Security" class="headerlink" title="Script Configuration and Security"></a>Script Configuration and Security</h2><h3 id="Personal-Access-Token-Setup"><a href="#Personal-Access-Token-Setup" class="headerlink" title="Personal Access Token Setup"></a>Personal Access Token Setup</h3><p>To use this script, you’ll need to create a Personal Access Token (PAT) with appropriate permissions:</p><ol><li>Navigate to your Azure DevOps Server user settings</li><li>Create a new Personal Access Token</li><li>Grant <strong>Code (read)</strong> permissions at minimum</li><li>Copy the token and replace <code>your-personal-access-token</code> in the script</li></ol><h3 id="URL-Configuration"><a href="#URL-Configuration" class="headerlink" title="URL Configuration"></a>URL Configuration</h3><p>Replace <code>https://mytfs.com</code> with your actual Azure DevOps Server URL. The format should be:</p><ul><li>On-premises TFS: <code>https://your-tfs-server</code></li><li>Azure DevOps Server: <code>https://your-server-name</code></li></ul><h2 id="Understanding-the-API-Response"><a href="#Understanding-the-API-Response" class="headerlink" title="Understanding the API Response"></a>Understanding the API Response</h2><p>The TFVC Items API returns objects with these key properties:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;path&quot;</span><span class="punctuation">:</span> <span class="string">&quot;$/ProjectName/FolderName&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;isFolder&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="number">12345</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;size&quot;</span><span class="punctuation">:</span> <span class="number">0</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p><strong>Important properties:</strong></p><ul><li><code>path</code>: The full TFVC path to the item</li><li><code>isFolder</code>: Boolean indicating if the item is a folder</li><li><code>version</code>: The changeset number when this item was last modified</li></ul><h2 id="Error-Handling-and-Edge-Cases"><a href="#Error-Handling-and-Edge-Cases" class="headerlink" title="Error Handling and Edge Cases"></a>Error Handling and Edge Cases</h2><p>The script includes error handling for common scenarios:</p><p><strong>No TFVC Repository:</strong> Some projects might not have TFVC repositories initialized. The script catches these cases and displays an appropriate message.</p><p><strong>Access Permissions:</strong> If your PAT doesn’t have sufficient permissions for a project, the API call will fail gracefully.</p><p><strong>Empty Repositories:</strong> Projects with TFVC repositories but no folders will display “No top-level folders found.”</p><h2 id="Advanced-Customizations"><a href="#Advanced-Customizations" class="headerlink" title="Advanced Customizations"></a>Advanced Customizations</h2><h3 id="Filtering-Specific-Projects"><a href="#Filtering-Specific-Projects" class="headerlink" title="Filtering Specific Projects"></a>Filtering Specific Projects</h3><p>To target specific projects, you can filter the projects array:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="variable">$projects</span> = <span class="variable">$projects</span>.value | <span class="built_in">Where-Object</span> &#123; <span class="variable">$_</span>.name <span class="operator">-like</span> <span class="string">&quot;*YourFilter*&quot;</span> &#125;</span><br></pre></td></tr></table></figure><h3 id="Deeper-Recursion"><a href="#Deeper-Recursion" class="headerlink" title="Deeper Recursion"></a>Deeper Recursion</h3><p>To get more than just top-level folders, change the <code>recursionLevel</code> parameter:</p><ul><li><code>OneLevel</code>: Immediate children only</li><li><code>Full</code>: Complete hierarchy (use with caution on large repositories)</li></ul><h3 id="Output-Formatting"><a href="#Output-Formatting" class="headerlink" title="Output Formatting"></a>Output Formatting</h3><p>You can modify the output format to suit your needs:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Export to CSV</span></span><br><span class="line"><span class="variable">$results</span> = <span class="selector-tag">@</span>()</span><br><span class="line"><span class="keyword">foreach</span> (<span class="variable">$folder</span> <span class="keyword">in</span> <span class="variable">$folders</span>) &#123;</span><br><span class="line">    <span class="variable">$results</span> += [<span class="type">PSCustomObject</span>]<span class="selector-tag">@</span>&#123;</span><br><span class="line">        Project = <span class="variable">$project</span>.name</span><br><span class="line">        FolderPath = <span class="variable">$folder</span>.path</span><br><span class="line">        LastModified = <span class="variable">$folder</span>.version</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"><span class="variable">$results</span> | <span class="built_in">Export-Csv</span> <span class="literal">-Path</span> <span class="string">&quot;tfvc-folders.csv&quot;</span> <span class="literal">-NoTypeInformation</span></span><br></pre></td></tr></table></figure><h2 id="Performance-Considerations"><a href="#Performance-Considerations" class="headerlink" title="Performance Considerations"></a>Performance Considerations</h2><p>For organizations with many projects, consider implementing:</p><p><strong>Parallel Processing:</strong> Use PowerShell jobs or runspaces to query multiple projects simultaneously.</p><p><strong>Pagination:</strong> For large result sets, implement pagination using the <code>$skip</code> and <code>$top</code> parameters.</p><p><strong>Caching:</strong> Store results locally if you need to run the script frequently.</p><h2 id="Troubleshooting-Common-Issues"><a href="#Troubleshooting-Common-Issues" class="headerlink" title="Troubleshooting Common Issues"></a>Troubleshooting Common Issues</h2><h3 id="Authentication-Failures"><a href="#Authentication-Failures" class="headerlink" title="Authentication Failures"></a>Authentication Failures</h3><ul><li>Verify your PAT is not expired</li><li>Ensure the PAT has sufficient permissions</li><li>Check that your TFS URL is correct and accessible</li></ul><h3 id="Empty-Results"><a href="#Empty-Results" class="headerlink" title="Empty Results"></a>Empty Results</h3><ul><li>Confirm the project actually uses TFVC (not Git)</li><li>Verify you have read permissions on the project</li><li>Check if the TFVC repository has been initialized</li></ul><h3 id="API-Version-Compatibility"><a href="#API-Version-Compatibility" class="headerlink" title="API Version Compatibility"></a>API Version Compatibility</h3><p>The script uses API version 5.0, which is compatible with:</p><ul><li>Team Foundation Server 2019 and later</li><li>Azure DevOps Server 2019 and later</li></ul><p>For older TFS versions, you might need to use API version 1.0 or 2.0.</p><h2 id="Best-Practices-for-TFVC-API-Integration"><a href="#Best-Practices-for-TFVC-API-Integration" class="headerlink" title="Best Practices for TFVC API Integration"></a>Best Practices for TFVC API Integration</h2><p><strong>Use Specific API Versions:</strong> Always specify the API version to ensure consistent behavior across different server versions.</p><p><strong>Implement Proper Error Handling:</strong> TFVC repositories can have various states, and not all projects may have TFVC initialized.</p><p><strong>Respect Rate Limits:</strong> While on-premises servers typically don’t have strict rate limits, implement appropriate delays if querying large numbers of projects.</p><p><strong>Secure Credential Management:</strong> Store PATs securely and rotate them regularly according to your organization’s security policies.</p><h2 id="Integration-with-CI-x2F-CD-Pipelines"><a href="#Integration-with-CI-x2F-CD-Pipelines" class="headerlink" title="Integration with CI&#x2F;CD Pipelines"></a>Integration with CI&#x2F;CD Pipelines</h2><p>This script can be integrated into DevOps workflows for:</p><p><strong>Repository Auditing:</strong> Generate reports of TFVC repository structures across your organization.</p><p><strong>Migration Planning:</strong> Identify repository structures before migrating from TFVC to Git.</p><p><strong>Compliance Reporting:</strong> Document your source control structure for regulatory requirements.</p><h2 id="Key-Takeaways"><a href="#Key-Takeaways" class="headerlink" title="Key Takeaways"></a>Key Takeaways</h2><p>Working with TFVC repositories via REST API requires understanding the fundamental differences between TFVC and Git repository models. The key insights are:</p><ul><li>TFVC has one repository per project, not multiple like Git</li><li>Use <code>scopePath</code> and <code>recursionLevel</code> parameters to control API traversal</li><li>Always filter results to distinguish between folders and the root item</li><li>Implement proper error handling for projects without TFVC repositories</li></ul><p>This solution provides a robust foundation for any TFVC repository management tasks you might need to automate. Whether you’re auditing your source control landscape, planning migrations, or building custom tooling, this approach will help you successfully retrieve TFVC repository structure data.</p><h2 id="References"><a href="#References" class="headerlink" title="References"></a>References</h2><ul><li><a href="https://docs.microsoft.com/en-us/rest/api/azure/devops/tfvc/items?view=azure-devops-rest-5.0">Azure DevOps REST API - TFVC Items</a></li><li><a href="https://docs.microsoft.com/en-us/rest/api/azure/devops/core/projects?view=azure-devops-rest-5.0">Azure DevOps REST API - Projects</a></li><li><a href="https://gist.github.com/Ricky-G/c342eb7be8918209f1e6df98e04779bc">Complete PowerShell script on GitHub Gist</a></li><li><a href="https://github.com/Ricky-G/script-library/blob/main/TFS-TFVC-Scripts-README.md">TFS&#x2F;TFVC Scripts Collection - Additional utilities and examples</a></li><li>Main image generated by <a href="https://openai.com/blog/dall-e/">DALL-E</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🎯 TL;DR: Retrieving TFVC Repository Structure via REST API&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This post demonstrates how to programmatically enumerate TFVC repository folders using Azure DevOps Server REST APIs. Unlike Git repositories, TFVC follows a one-repository-per-project model with hierarchical folder structures starting at &lt;code&gt;$/ProjectName&lt;/code&gt;. The solution uses the TFVC Items API with specific parameters: &lt;code&gt;scopePath=$/ProjectName&lt;/code&gt; to target the project root, and &lt;code&gt;recursionLevel=OneLevel&lt;/code&gt; to retrieve immediate children. The implementation handles authentication via Personal Access Tokens, filters results to show only folders (excluding the root), and includes error handling for projects without TFVC repositories or insufficient permissions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Key technical details:&lt;/strong&gt; PowerShell script implementation, proper API parameter usage, authentication setup, and handling edge cases like empty repositories and access permissions. &lt;strong&gt;&lt;a href=&quot;https://github.com/Ricky-G/script-library/blob/main/TFS-TFVC-Scripts-README.md&quot;&gt;Complete PowerShell script and utilities available here&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;Recently, I was asked an interesting question by a developer who was struggling with Azure DevOps Server APIs around fetching repository metadata for legacy TFVC structures as part of a GitHub migration from ADO Server. This was a nice little problem to solve because, let’s be honest, we don’t really deal with these legacy TFVC repositories much anymore. Most teams have migrated to Git, and the documentation around TFVC API interactions has become somewhat sparse over the years.&lt;/p&gt;
&lt;p&gt;The challenge was straightforward but frustrating: they could retrieve project information just fine, but getting the actual TFVC folder structure within each project? That’s where things got tricky. After doing a bit of digging through the API documentation and testing different approaches, I’m happy to say that yes, it is absolutely possible to enumerate all TFVC repositories and their folder structures programmatically.&lt;/p&gt;
&lt;p&gt;This blog post shares the solution I put together - a practical approach to retrieve TFVC repository structure using the Azure DevOps Server REST APIs. If you’re working with legacy TFVC repositories and need to interact with them programmatically, this one’s for you.&lt;/p&gt;
&lt;h2 id=&quot;The-Challenge-Understanding-TFVC-API-Limitations&quot;&gt;&lt;a href=&quot;#The-Challenge-Understanding-TFVC-API-Limitations&quot; class=&quot;headerlink&quot; title=&quot;The Challenge: Understanding TFVC API Limitations&quot;&gt;&lt;/a&gt;The Challenge: Understanding TFVC API Limitations&lt;/h2&gt;&lt;p&gt;Unlike Git repositories where each project can contain multiple repos, TFVC follows a different model where each project contains exactly one TFVC repository. This fundamental difference affects how you interact with the API and retrieve repository information.&lt;/p&gt;
&lt;p&gt;The main challenge developers face is distinguishing between project metadata and actual TFVC repository structure. When calling the standard Projects API, you receive project information but not the folder structure within the TFVC repository itself.&lt;/p&gt;</summary>
    
    
    
    <category term="Azure DevOps" scheme="https://clouddev.blog/categories/Azure-DevOps/"/>
    
    <category term="Azure DevOps API" scheme="https://clouddev.blog/categories/Azure-DevOps/Azure-DevOps-API/"/>
    
    
    <category term="Azure DevOps" scheme="https://clouddev.blog/tags/Azure-DevOps/"/>
    
    <category term="REST API" scheme="https://clouddev.blog/tags/REST-API/"/>
    
    <category term="PowerShell" scheme="https://clouddev.blog/tags/PowerShell/"/>
    
    <category term="TFVC" scheme="https://clouddev.blog/tags/TFVC/"/>
    
    <category term="TFS" scheme="https://clouddev.blog/tags/TFS/"/>
    
  </entry>
  
  <entry>
    <title>How We United 8 Developers Across Restricted Environments Using Azure VMs and Dev Containers</title>
    <link href="https://clouddev.blog/Azure/DevOps/Development/how-we-united-8-developers-across-restricted-environments-using-azure-vms-and-dev-containers/"/>
    <id>https://clouddev.blog/Azure/DevOps/Development/how-we-united-8-developers-across-restricted-environments-using-azure-vms-and-dev-containers/</id>
    <published>2025-04-30T12:00:00.000Z</published>
    <updated>2025-08-06T11:10:49.626Z</updated>
    
    <content type="html"><![CDATA[<hr><blockquote><p><strong>🎯 TL;DR: Distributed Development with Azure VMs and Dev Containers</strong></p><p>This post details solving a distributed development challenge where 8 developers from different organizations needed to collaborate on an AutoGen AI project - 4 from restricted corporate environments unable to install development tools, and 4 external developers without access to client systems. The solution uses a shared Azure VM (Standard D8s v3) with individual user accounts, certificate-based SSH authentication, and VS Code Remote Development connected to a shared Dev Container environment. The architecture eliminates “works on my machine” issues by providing consistent development environments, shared resources (datasets, models, configs), and enables real-time collaboration.</p><p><strong>Implementation highlights:</strong> Automated user provisioning scripts, VS Code Remote-SSH configuration, comprehensive devcontainer.json with pre-installed Python 3.12&#x2F;AutoGen&#x2F;Azure CLI, shared directory structures, and security hardening with fail2ban and UFW. <strong><a href="https://github.com/Ricky-G/script-library">Development environment setup scripts and configurations documented here</a></strong></p></blockquote><hr><h2 id="Introduction-When-Traditional-Solutions-Hit-a-Wall"><a href="#Introduction-When-Traditional-Solutions-Hit-a-Wall" class="headerlink" title="Introduction: When Traditional Solutions Hit a Wall"></a>Introduction: When Traditional Solutions Hit a Wall</h2><p>Last month, I found myself facing a challenge that I’m sure many of you have encountered: How do you enable seamless collaboration for a development team when half of them work in a locked-down environment where they can’t install any development tools, and the other half can’t access the client’s systems?</p><p>Our team of eight developers was tasked with building a proof-of-concept (PoC) for an AI-powered agentic system using Microsoft’s AutoGen framework. Here’s the kicker: this was a 3-week PoC sprint bringing together two teams from different organizations who had never worked together before. We needed a collaborative environment that could be spun up quickly, require minimal setup effort, and allow everyone to hit the ground running from day one.</p><p>The project requirements were complex enough, but the real challenge? Four developers worked from a highly restricted corporate environment where installing Python, VS Code, or any development tools was strictly prohibited. The remaining four worked from our offices but couldn’t access the client’s internal systems directly.</p><p>We tried the usual approaches:</p><ul><li><strong>RDP connections</strong>: Blocked by security policies</li><li><strong>VPN access</strong>: Denied due to compliance requirements</li><li><strong>Local development with file sharing</strong>: Immediate sync issues and “works on my machine” problems</li><li><strong>Cloud IDEs</strong>: Didn’t meet the client’s security requirements</li></ul><p>Just when we thought we’d have to resort to the dreaded “develop locally and pray it works in production” approach, we discovered a solution that not only solved our immediate problem but revolutionized how we approach distributed development.</p><h2 id="The-Architecture-That-Worked-For-Us"><a href="#The-Architecture-That-Worked-For-Us" class="headerlink" title="The Architecture That Worked For Us"></a>The Architecture That Worked For Us</h2><p>Here’s a visual representation of what we built, everyone had to work on their personal (non-corporate) laptops for this to work.</p><pre class="mermaid">flowchart TD    A["� 8 Developers on Personal Laptops<br/>4 Restricted + 4 External Teams"]        B["� SSH + VS Code Remote Connection<br/>Certificate-based Authentication"]        C["☁️ Azure VM (Standard D8s v3)<br/>8 vCPUs • 32GB RAM • Ubuntu 22.04"]        D["👤 Individual User Accounts<br/>user1, user2, user3... user8"]        E["🐳 Shared Dev Container<br/>Python 3.12 + AutoGen + Azure CLI<br/>All Dependencies Pre-installed"]        F["📂 Shared Development Resources<br/>• Project Repository<br/>• Datasets & Models<br/>• Configuration Files"]        G["✅ Results Achieved<br/>94% Faster Onboarding<br/>$400/month vs $16k laptops<br/>Enhanced Security"]        A --> B    B --> C    C --> D    D --> E    E --> F    F --> G        style A fill:#e3f2fd,stroke:#1976d2,stroke-width:3px,color:#000    style B fill:#f3e5f5,stroke:#7b1fa2,stroke-width:3px,color:#000    style C fill:#e1f5fe,stroke:#0277bd,stroke-width:3px,color:#000    style D fill:#fff3e0,stroke:#f57c00,stroke-width:3px,color:#000    style E fill:#f3e5f5,stroke:#7b1fa2,stroke-width:3px,color:#000    style F fill:#fff3e0,stroke:#f57c00,stroke-width:3px,color:#000    style G fill:#e8f5e8,stroke:#388e3c,stroke-width:3px,color:#000</pre><p>Lets check out how this was built and setup…</p><span id="more"></span><h2 id="The-Deep-Dive-How-We-Built-It"><a href="#The-Deep-Dive-How-We-Built-It" class="headerlink" title="The Deep Dive: How We Built It"></a>The Deep Dive: How We Built It</h2><h3 id="Step-1-Provisioning-the-Azure-VM"><a href="#Step-1-Provisioning-the-Azure-VM" class="headerlink" title="Step 1: Provisioning the Azure VM"></a>Step 1: Provisioning the Azure VM</h3><p>We started with a Linux VM in Azure. After some testing, we settled on a Standard D8s v3 instance (8 vCPUs, 32 GB RAM) which provided enough resources for all eight developers to work simultaneously without performance issues.</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># VM Creation (simplified for clarity)</span></span><br><span class="line">az vm create \</span><br><span class="line">  --resource-group DevEnvironmentRG \</span><br><span class="line">  --name SharedDevVM \</span><br><span class="line">  --image Ubuntu2204 \</span><br><span class="line">  --size Standard_D8s_v3 \</span><br><span class="line">  --admin-username azureuser \</span><br><span class="line">  --generate-ssh-keys \</span><br><span class="line">  --public-ip-address-allocation static</span><br></pre></td></tr></table></figure><h3 id="Step-2-User-Account-Architecture"><a href="#Step-2-User-Account-Architecture" class="headerlink" title="Step 2: User Account Architecture"></a>Step 2: User Account Architecture</h3><p>Instead of having everyone share a single account (security nightmare!), we created individual Linux user accounts for each developer. This approach gave us:</p><ul><li><strong>Audit trails</strong>: We could track who did what and when</li><li><strong>Personalized environments</strong>: Each developer could customize their shell, aliases, and local configs</li><li><strong>Security isolation</strong>: Problems with one account wouldn’t affect others</li><li><strong>Resource monitoring</strong>: We could track resource usage per developer if needed</li></ul><p>Here’s how we automated user creation:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#!/bin/bash</span></span><br><span class="line"><span class="comment"># create_dev_users.sh</span></span><br><span class="line"></span><br><span class="line">DEVELOPERS=(<span class="string">&quot;alice&quot;</span> <span class="string">&quot;bob&quot;</span> <span class="string">&quot;charlie&quot;</span> <span class="string">&quot;david&quot;</span> <span class="string">&quot;eve&quot;</span> <span class="string">&quot;frank&quot;</span> <span class="string">&quot;grace&quot;</span> <span class="string">&quot;henry&quot;</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> dev <span class="keyword">in</span> <span class="string">&quot;<span class="variable">$&#123;DEVELOPERS[@]&#125;</span>&quot;</span>; <span class="keyword">do</span></span><br><span class="line">    <span class="comment"># Create user with home directory</span></span><br><span class="line">    sudo useradd -m -s /bin/bash <span class="variable">$dev</span></span><br><span class="line">    </span><br><span class="line">    <span class="comment"># Create .ssh directory</span></span><br><span class="line">    sudo <span class="built_in">mkdir</span> -p /home/<span class="variable">$dev</span>/.ssh</span><br><span class="line">    sudo <span class="built_in">chmod</span> 700 /home/<span class="variable">$dev</span>/.ssh</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># Set up for SSH key authentication</span></span><br><span class="line">    sudo <span class="built_in">touch</span> /home/<span class="variable">$dev</span>/.ssh/authorized_keys</span><br><span class="line">    sudo <span class="built_in">chmod</span> 600 /home/<span class="variable">$dev</span>/.ssh/authorized_keys</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># Set ownership</span></span><br><span class="line">    sudo <span class="built_in">chown</span> -R <span class="variable">$dev</span>:<span class="variable">$dev</span> /home/<span class="variable">$dev</span>/.ssh</span><br><span class="line">    </span><br><span class="line">    <span class="comment"># Add to docker group (for container access)</span></span><br><span class="line">    sudo usermod -aG docker <span class="variable">$dev</span></span><br><span class="line">    </span><br><span class="line">    <span class="built_in">echo</span> <span class="string">&quot;Created user: <span class="variable">$dev</span>&quot;</span></span><br><span class="line"><span class="keyword">done</span></span><br></pre></td></tr></table></figure><h3 id="Step-3-Certificate-Based-Authentication"><a href="#Step-3-Certificate-Based-Authentication" class="headerlink" title="Step 3: Certificate-Based Authentication"></a>Step 3: Certificate-Based Authentication</h3><p>Password authentication over the internet? Not on our watch. We implemented certificate-based SSH authentication for each developer:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># On each developer&#x27;s local machine</span></span><br><span class="line">ssh-keygen -t ed25519 -C <span class="string">&quot;developer@project&quot;</span> -f ~/.ssh/project_dev_key</span><br><span class="line"></span><br><span class="line"><span class="comment"># The public key was then added to their respective authorized_keys file on the VM</span></span><br></pre></td></tr></table></figure><p>The beauty of this approach:</p><ul><li>No passwords to remember or rotate</li><li>Certificates could be revoked instantly if needed</li><li>Multi-factor authentication could be added via Azure AD if required</li><li>Worked seamlessly even from the restricted environment (SSH client was available)</li></ul><h3 id="Step-4-VS-Code-Remote-Development-Magic"><a href="#Step-4-VS-Code-Remote-Development-Magic" class="headerlink" title="Step 4: VS Code Remote Development Magic"></a>Step 4: VS Code Remote Development Magic</h3><p>This is where the magic happened. VS Code’s Remote-SSH extension turned our Linux VM into a powerful development environment. Each developer configured their VS Code with:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// .ssh/config on developer machine</span></span><br><span class="line">Host azure-dev-vm</span><br><span class="line">    HostName &lt;VM-PUBLIC-IP&gt;</span><br><span class="line">    User alice</span><br><span class="line">    IdentityFile ~/.ssh/project_dev_key</span><br><span class="line">    ForwardAgent yes</span><br></pre></td></tr></table></figure><p>Once connected, developers had the full VS Code experience:</p><ul><li>IntelliSense working perfectly</li><li>Debugging capabilities</li><li>Extension support</li><li>Integrated terminal</li><li>Git integration</li></ul><p>But we didn’t stop there…</p><h3 id="Step-5-The-Dev-Container-Revolution"><a href="#Step-5-The-Dev-Container-Revolution" class="headerlink" title="Step 5: The Dev Container Revolution"></a>Step 5: The Dev Container Revolution</h3><p>Here’s where we went from “good” to “game-changing.” We created a Dev Container that encapsulated our entire development environment. This meant:</p><p><strong>No more “pip install” parties</strong>: Everything was pre-installed<br><strong>No more version conflicts</strong>: Everyone used the exact same versions<br><strong>No more missing dependencies</strong>: If it worked for one, it worked for all</p><p>Our <code>.devcontainer/devcontainer.json</code>:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line"><span class="attr">&quot;name&quot;</span><span class="punctuation">:</span> <span class="string">&quot;Autogen Development Environment&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">&quot;image&quot;</span><span class="punctuation">:</span> <span class="string">&quot;mcr.microsoft.com/devcontainers/python:1-3.12-bullseye&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">&quot;features&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line"><span class="attr">&quot;ghcr.io/devcontainers/features/anaconda:1&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">&quot;ghcr.io/devcontainers/features/azure-cli:1&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line"><span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;latest&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">&quot;ghcr.io/devcontainers/features/docker-outside-of-docker:1&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line"><span class="attr">&quot;moby&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">&quot;installDockerBuildx&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">&quot;installDockerComposeSwitch&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">&quot;version&quot;</span><span class="punctuation">:</span> <span class="string">&quot;latest&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">&quot;dockerDashComposeVersion&quot;</span><span class="punctuation">:</span> <span class="string">&quot;v2&quot;</span></span><br><span class="line"><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">&quot;ghcr.io/devcontainers-extra/features/apt-get-packages:1&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line"><span class="attr">&quot;packages&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line"><span class="string">&quot;tig&quot;</span></span><br><span class="line"><span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line"><span class="attr">&quot;customizations&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line"><span class="attr">&quot;vscode&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line"><span class="attr">&quot;extensions&quot;</span><span class="punctuation">:</span> <span class="punctuation">[</span></span><br><span class="line"><span class="string">&quot;github.copilot&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-azuretools.vscode-docker&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-python.python&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-python.vscode-pylance&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;redhat.vscode-yaml&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;editorconfig.editorconfig&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-azure-devops.azure-pipelines&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-azure-load-testing.microsoft-testing&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-azuretools.azure-dev&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-azuretools.vscode-azure-github-copilot&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-azuretools.vscode-azureappservice&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-azuretools.vscode-azurecontainerapps&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-azuretools.vscode-azurefunctions&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;eamodio.gitlens&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-azuretools.vscode-azurelogicapps&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-azuretools.vscode-azureresourcegroups&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-azuretools.vscode-azurestaticwebapps&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-azuretools.vscode-azurestorage&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-azuretools.vscode-azurevirtualmachines&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-azuretools.vscode-bicep&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-azuretools.vscode-containers&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-azuretools.vscode-cosmosdb&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-azuretools.vscode-docker&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-python.black-formatter&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-python.debugpy&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-python.isort&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-python.pylint&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-python.python&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-python.vscode-pylance&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-vscode.azure-repos&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-vscode.azurecli&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-azuretools.azure-dev&quot;</span><span class="punctuation">,</span></span><br><span class="line"><span class="string">&quot;ms-azuretools.vscode-azure-github-copilot&quot;</span></span><br><span class="line"><span class="punctuation">]</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br><span class="line"><span class="comment">// &quot;postCreateCommand&quot;: &quot;pip3 install --user -r requirements.txt&quot;,</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h3 id="Step-6-Shared-Resources-and-Collaboration"><a href="#Step-6-Shared-Resources-and-Collaboration" class="headerlink" title="Step 6: Shared Resources and Collaboration"></a>Step 6: Shared Resources and Collaboration</h3><p>With everyone working on the same VM, we could leverage shared resources effectively:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Shared directories for common resources</span></span><br><span class="line">/home/shared/</span><br><span class="line">├── datasets/          <span class="comment"># Common datasets for AI training</span></span><br><span class="line">├── models/           <span class="comment"># Shared model artifacts</span></span><br><span class="line">├── configs/          <span class="comment"># Shared configuration files</span></span><br><span class="line">└── scripts/          <span class="comment"># Utility scripts</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Permissions set for group collaboration</span></span><br><span class="line">sudo groupadd developers</span><br><span class="line">sudo usermod -a -G developers alice bob charlie... <span class="comment"># all developers</span></span><br><span class="line">sudo <span class="built_in">chown</span> -R :developers /home/shared</span><br><span class="line">sudo <span class="built_in">chmod</span> -R 775 /home/shared</span><br></pre></td></tr></table></figure><h2 id="The-Unexpected-Benefits"><a href="#The-Unexpected-Benefits" class="headerlink" title="The Unexpected Benefits"></a>The Unexpected Benefits</h2><h3 id="1-Lightning-Fast-Onboarding"><a href="#1-Lightning-Fast-Onboarding" class="headerlink" title="1. Lightning-Fast Onboarding"></a>1. Lightning-Fast Onboarding</h3><p>Our typical onboarding process used to take 2-3 days:</p><ul><li>Day 1: Install Python, configure environment</li><li>Day 2: Debug dependency issues, version conflicts</li><li>Day 3: Finally start actual development</li></ul><p>With our new setup:</p><ul><li>Hour 1: Receive SSH certificate and connection instructions</li><li>Hour 2: Connect VS Code, open project in container</li><li>Hour 3: Writing production code</li></ul><p><strong>That’s a 94% reduction in onboarding time!</strong></p><h3 id="2-Compute-Power-Democracy"><a href="#2-Compute-Power-Democracy" class="headerlink" title="2. Compute Power Democracy"></a>2. Compute Power Democracy</h3><p>Previously, developers with older laptops struggled with AI model training and testing. Now everyone had access to:</p><ul><li>8 vCPUs for parallel processing</li><li>32 GB RAM for large datasets</li><li>Fast SSD storage for quick I&#x2F;O</li><li>Azure’s network backbone for downloading models and datasets</li></ul><h3 id="3-Cost-Optimization-That-Surprised-Finance"><a href="#3-Cost-Optimization-That-Surprised-Finance" class="headerlink" title="3. Cost Optimization That Surprised Finance"></a>3. Cost Optimization That Surprised Finance</h3><p>Our finance team loved this approach:</p><ul><li><strong>Traditional approach</strong>: 8 high-spec laptops &#x3D; ~$16,000</li><li><strong>Our approach</strong>: 1 Azure VM &#x3D; ~$400&#x2F;month</li></ul><p>Even accounting for the VM running 24&#x2F;7, we saved money within the first year.</p><h3 id="4-Security-Without-Suffering"><a href="#4-Security-Without-Suffering" class="headerlink" title="4. Security Without Suffering"></a>4. Security Without Suffering</h3><p>The restricted environment developers could finally contribute without compromising security:</p><ul><li>No software installed on their local machines</li><li>All code remained in the cloud</li><li>Audit logs for every action</li><li>Easy to revoke access when project ended</li></ul><h2 id="Real-World-Results-The-AutoGen-Project"><a href="#Real-World-Results-The-AutoGen-Project" class="headerlink" title="Real-World Results: The AutoGen Project"></a>Real-World Results: The AutoGen Project</h2><p>Let me share some specific wins from our AutoGen AI agent project:</p><h3 id="Development-Velocity"><a href="#Development-Velocity" class="headerlink" title="Development Velocity"></a>Development Velocity</h3><ul><li><strong>Before</strong>: 2-3 features per sprint (too much time on environment issues)</li><li><strong>After</strong>: 8-10 features per sprint (focus on actual development)</li></ul><h3 id="Code-Quality"><a href="#Code-Quality" class="headerlink" title="Code Quality"></a>Code Quality</h3><ul><li><strong>Before</strong>: “Works on my machine” was a daily phrase</li><li><strong>After</strong>: If it worked in the dev container, it worked everywhere</li></ul><h3 id="Team-Morale"><a href="#Team-Morale" class="headerlink" title="Team Morale"></a>Team Morale</h3><ul><li><strong>Before</strong>: Frustration with environment setup and restrictions</li><li><strong>After</strong>: Developers focused on solving interesting AI problems</li></ul><h3 id="Specific-AutoGen-Benefits"><a href="#Specific-AutoGen-Benefits" class="headerlink" title="Specific AutoGen Benefits"></a>Specific AutoGen Benefits</h3><p>Working with AutoGen requires multiple AI models, API keys, and complex configurations. Our setup handled this beautifully:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Shared configuration file accessible to all</span></span><br><span class="line"><span class="comment"># /home/shared/configs/autogen_config.py</span></span><br><span class="line"></span><br><span class="line">config_list = [</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="string">&quot;model&quot;</span>: <span class="string">&quot;gpt-4&quot;</span>,</span><br><span class="line">        <span class="string">&quot;api_key&quot;</span>: os.environ.get(<span class="string">&quot;OPENAI_API_KEY&quot;</span>),</span><br><span class="line">    &#125;,</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="string">&quot;model&quot;</span>: <span class="string">&quot;gpt-3.5-turbo&quot;</span>,</span><br><span class="line">        <span class="string">&quot;api_key&quot;</span>: os.environ.get(<span class="string">&quot;OPENAI_API_KEY&quot;</span>),</span><br><span class="line">    &#125;</span><br><span class="line">]</span><br><span class="line"></span><br><span class="line"><span class="comment"># Each developer could test with the same models and configurations</span></span><br><span class="line"><span class="comment"># No &quot;I don&#x27;t have API access&quot; blockers</span></span><br></pre></td></tr></table></figure><h2 id="Lessons-Learned-and-Best-Practices"><a href="#Lessons-Learned-and-Best-Practices" class="headerlink" title="Lessons Learned and Best Practices"></a>Lessons Learned and Best Practices</h2><h3 id="What-Worked-Well"><a href="#What-Worked-Well" class="headerlink" title="What Worked Well"></a>What Worked Well</h3><ol><li><p><strong>Start with more resources than you think you need</strong>: We initially tried a smaller VM and hit performance issues. Better to scale down than suffer with poor performance.</p></li><li><p><strong>Invest time in the Dev Container setup</strong>: Every hour spent perfecting the container saved days of debugging later.</p></li><li><p><strong>Document everything</strong>: We created a comprehensive wiki with:</p><ul><li>Connection instructions</li><li>Troubleshooting guides</li><li>Best practices for shared development</li><li>Git workflow for the shared environment</li></ul></li><li><p><strong>Regular backups</strong>: We automated daily backups of the entire VM and home directories.</p></li></ol><h3 id="Challenges-We-Faced"><a href="#Challenges-We-Faced" class="headerlink" title="Challenges We Faced"></a>Challenges We Faced</h3><ol><li><p><strong>Concurrent file editing</strong>: We needed clear Git workflows to prevent conflicts</p><ul><li>Solution: Feature branches and frequent commits</li></ul></li><li><p><strong>Resource contention</strong>: Occasionally, one developer’s process would hog resources</p><ul><li>Solution: Implemented resource limits using cgroups</li></ul></li><li><p><strong>SSH connection drops</strong>: Some developers faced connection issues</p><ul><li>Solution: Configured SSH keep-alive and implemented tmux for session persistence</li></ul></li></ol><h3 id="Security-Considerations"><a href="#Security-Considerations" class="headerlink" title="Security Considerations"></a>Security Considerations</h3><p>Don’t forget these crucial security aspects:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Implement fail2ban for SSH protection</span></span><br><span class="line">sudo apt-get install fail2ban</span><br><span class="line"></span><br><span class="line"><span class="comment"># Configure firewall rules</span></span><br><span class="line">sudo ufw allow from &lt;OFFICE_IP_RANGE&gt; to any port 22</span><br><span class="line">sudo ufw <span class="built_in">enable</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Regular security updates</span></span><br><span class="line">sudo unattended-upgrades</span><br><span class="line"></span><br><span class="line"><span class="comment"># Audit logging</span></span><br><span class="line">sudo apt-get install auditd</span><br></pre></td></tr></table></figure><h2 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h2><p>What started as a desperate attempt to enable collaboration across restricted environments turned into a revolutionary approach to distributed development. By leveraging Azure VMs, Dev Containers, and VS Code Remote Development, we not only solved our immediate problem but discovered a solution that offers:</p><ul><li><strong>94% faster onboarding</strong> for new team members</li><li><strong>Significant cost savings</strong> compared to traditional hardware approaches</li><li><strong>Enhanced security</strong> without sacrificing developer productivity</li><li><strong>True collaboration</strong> through shared resources and environments</li><li><strong>Consistent development experience</strong> across all team members</li></ul><p>The key insight was recognizing that personal laptops could serve as the bridge between restricted corporate environments and cloud-based development infrastructure. Sometimes the best solutions come from thinking outside the traditional corporate IT box.</p><p>Whether you’re dealing with similar restrictions or simply want to improve your team’s development experience, this architecture pattern could be the game-changer you’re looking for. The combination of Azure infrastructure, containerized development environments, and modern remote development tools creates a powerful platform that scales with your team’s needs.</p><h2 id="References"><a href="#References" class="headerlink" title="References"></a>References</h2><ul><li><a href="https://code.visualstudio.com/docs/remote/remote-overview">VS Code Remote Development</a></li><li><a href="https://containers.dev/">Dev Containers Documentation</a></li><li><a href="https://azure.microsoft.com/services/virtual-machines/">Azure Virtual Machines</a></li><li><a href="https://github.com/microsoft/autogen">Microsoft AutoGen Framework</a></li><li><a href="https://www.ssh.com/academy/ssh/public-key-authentication">SSH Key-Based Authentication</a></li><li>Main image generated by <a href="https://openai.com/blog/dall-e/">DALL-E</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🎯 TL;DR: Distributed Development with Azure VMs and Dev Containers&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This post details solving a distributed development challenge where 8 developers from different organizations needed to collaborate on an AutoGen AI project - 4 from restricted corporate environments unable to install development tools, and 4 external developers without access to client systems. The solution uses a shared Azure VM (Standard D8s v3) with individual user accounts, certificate-based SSH authentication, and VS Code Remote Development connected to a shared Dev Container environment. The architecture eliminates “works on my machine” issues by providing consistent development environments, shared resources (datasets, models, configs), and enables real-time collaboration.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Implementation highlights:&lt;/strong&gt; Automated user provisioning scripts, VS Code Remote-SSH configuration, comprehensive devcontainer.json with pre-installed Python 3.12&amp;#x2F;AutoGen&amp;#x2F;Azure CLI, shared directory structures, and security hardening with fail2ban and UFW. &lt;strong&gt;&lt;a href=&quot;https://github.com/Ricky-G/script-library&quot;&gt;Development environment setup scripts and configurations documented here&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;Introduction-When-Traditional-Solutions-Hit-a-Wall&quot;&gt;&lt;a href=&quot;#Introduction-When-Traditional-Solutions-Hit-a-Wall&quot; class=&quot;headerlink&quot; title=&quot;Introduction: When Traditional Solutions Hit a Wall&quot;&gt;&lt;/a&gt;Introduction: When Traditional Solutions Hit a Wall&lt;/h2&gt;&lt;p&gt;Last month, I found myself facing a challenge that I’m sure many of you have encountered: How do you enable seamless collaboration for a development team when half of them work in a locked-down environment where they can’t install any development tools, and the other half can’t access the client’s systems?&lt;/p&gt;
&lt;p&gt;Our team of eight developers was tasked with building a proof-of-concept (PoC) for an AI-powered agentic system using Microsoft’s AutoGen framework. Here’s the kicker: this was a 3-week PoC sprint bringing together two teams from different organizations who had never worked together before. We needed a collaborative environment that could be spun up quickly, require minimal setup effort, and allow everyone to hit the ground running from day one.&lt;/p&gt;
&lt;p&gt;The project requirements were complex enough, but the real challenge? Four developers worked from a highly restricted corporate environment where installing Python, VS Code, or any development tools was strictly prohibited. The remaining four worked from our offices but couldn’t access the client’s internal systems directly.&lt;/p&gt;
&lt;p&gt;We tried the usual approaches:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;RDP connections&lt;/strong&gt;: Blocked by security policies&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;VPN access&lt;/strong&gt;: Denied due to compliance requirements&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Local development with file sharing&lt;/strong&gt;: Immediate sync issues and “works on my machine” problems&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Cloud IDEs&lt;/strong&gt;: Didn’t meet the client’s security requirements&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Just when we thought we’d have to resort to the dreaded “develop locally and pray it works in production” approach, we discovered a solution that not only solved our immediate problem but revolutionized how we approach distributed development.&lt;/p&gt;
&lt;h2 id=&quot;The-Architecture-That-Worked-For-Us&quot;&gt;&lt;a href=&quot;#The-Architecture-That-Worked-For-Us&quot; class=&quot;headerlink&quot; title=&quot;The Architecture That Worked For Us&quot;&gt;&lt;/a&gt;The Architecture That Worked For Us&lt;/h2&gt;&lt;p&gt;Here’s a visual representation of what we built, everyone had to work on their personal (non-corporate) laptops for this to work.&lt;/p&gt;
&lt;pre class=&quot;mermaid&quot;&gt;flowchart TD
    A[&quot;� 8 Developers on Personal Laptops&lt;br/&gt;4 Restricted + 4 External Teams&quot;]
    
    B[&quot;� SSH + VS Code Remote Connection&lt;br/&gt;Certificate-based Authentication&quot;]
    
    C[&quot;☁️ Azure VM (Standard D8s v3)&lt;br/&gt;8 vCPUs • 32GB RAM • Ubuntu 22.04&quot;]
    
    D[&quot;👤 Individual User Accounts&lt;br/&gt;user1, user2, user3... user8&quot;]
    
    E[&quot;🐳 Shared Dev Container&lt;br/&gt;Python 3.12 + AutoGen + Azure CLI&lt;br/&gt;All Dependencies Pre-installed&quot;]
    
    F[&quot;📂 Shared Development Resources&lt;br/&gt;• Project Repository&lt;br/&gt;• Datasets &amp; Models&lt;br/&gt;• Configuration Files&quot;]
    
    G[&quot;✅ Results Achieved&lt;br/&gt;94% Faster Onboarding&lt;br/&gt;$400/month vs $16k laptops&lt;br/&gt;Enhanced Security&quot;]
    
    A --&gt; B
    B --&gt; C
    C --&gt; D
    D --&gt; E
    E --&gt; F
    F --&gt; G
    
    style A fill:#e3f2fd,stroke:#1976d2,stroke-width:3px,color:#000
    style B fill:#f3e5f5,stroke:#7b1fa2,stroke-width:3px,color:#000
    style C fill:#e1f5fe,stroke:#0277bd,stroke-width:3px,color:#000
    style D fill:#fff3e0,stroke:#f57c00,stroke-width:3px,color:#000
    style E fill:#f3e5f5,stroke:#7b1fa2,stroke-width:3px,color:#000
    style F fill:#fff3e0,stroke:#f57c00,stroke-width:3px,color:#000
    style G fill:#e8f5e8,stroke:#388e3c,stroke-width:3px,color:#000&lt;/pre&gt;
&lt;p&gt;Lets check out how this was built and setup…&lt;/p&gt;</summary>
    
    
    
    <category term="Azure" scheme="https://clouddev.blog/categories/Azure/"/>
    
    <category term="DevOps" scheme="https://clouddev.blog/categories/Azure/DevOps/"/>
    
    <category term="Development" scheme="https://clouddev.blog/categories/Azure/DevOps/Development/"/>
    
    
    <category term="Azure" scheme="https://clouddev.blog/tags/Azure/"/>
    
    <category term="DevOps" scheme="https://clouddev.blog/tags/DevOps/"/>
    
    <category term="Development Environment" scheme="https://clouddev.blog/tags/Development-Environment/"/>
    
    <category term="Dev Containers" scheme="https://clouddev.blog/tags/Dev-Containers/"/>
    
    <category term="Remote Development" scheme="https://clouddev.blog/tags/Remote-Development/"/>
    
    <category term="VS Code" scheme="https://clouddev.blog/tags/VS-Code/"/>
    
    <category term="Collaboration" scheme="https://clouddev.blog/tags/Collaboration/"/>
    
    <category term="Virtual Machines" scheme="https://clouddev.blog/tags/Virtual-Machines/"/>
    
    <category term="AutoGen" scheme="https://clouddev.blog/tags/AutoGen/"/>
    
    <category term="AI Development" scheme="https://clouddev.blog/tags/AI-Development/"/>
    
  </entry>
  
  <entry>
    <title>Custom Voices in Azure OpenAI Realtime with Azure Speech Services</title>
    <link href="https://clouddev.blog/Azure/AI/Speech/custom-voices-in-azure-openai-realtime-with-azure-speech-services/"/>
    <id>https://clouddev.blog/Azure/AI/Speech/custom-voices-in-azure-openai-realtime-with-azure-speech-services/</id>
    <published>2025-04-24T12:00:00.000Z</published>
    <updated>2026-03-14T04:23:22.822Z</updated>
    
    <content type="html"><![CDATA[<hr><blockquote><p><strong>🎯 TL;DR: Hybrid GPT-4o Realtime with Azure Speech Services Custom Voices</strong></p><p>This post demonstrates bypassing GPT-4o Realtime’s built-in voice limitations by creating a hybrid architecture that combines GPT-4o’s conversational intelligence with Azure Speech Services’ extensive voice catalog. The solution configures GPT-4o Realtime for text-only output (<code>ContentModalities.Text</code>) and routes responses through Azure Speech Services, enabling access to 400+ neural voices, custom neural voices (CNV), and SSML control. The implementation includes intelligent barge-in functionality using real-time audio amplitude monitoring, allowing users to interrupt the assistant naturally mid-response.</p><p><strong>Technical implementation:</strong> C# application using Azure.AI.OpenAI and Microsoft.CognitiveServices.Speech SDKs, NAudio for audio I&#x2F;O, streaming text collection from GPT-4o responses, RMS-based speech detection with configurable thresholds, and concurrent audio management for seamless interruption handling. <strong><a href="https://github.com/Ricky-G/azure-scenario-hub/tree/main/custom-voice-sample-code">Complete C# source code with audio helpers available here</a></strong></p></blockquote><hr><p>Building realtime voice-enabled applications with Azure OpenAI’s GPT-4o Realtime model is incredibly powerful, but there’s one significant limitation that can be a deal-breaker for many use cases: you’re stuck with OpenAI’s predefined voices like “sage”, “alloy”, “echo”, “fable”, “onyx”, and “nova”. </p><p>What if you’re building a branded customer service bot that needs to match your company’s voice identity? Or developing a therapeutic application for children with autism where the voice quality and tone are crucial for engagement? What if your users need to interrupt the assistant naturally, just like in real human conversations?</p><p>In this comprehensive guide, I’ll show you exactly how I solved these challenges by building a hybrid solution that combines the conversational intelligence of GPT-4o Realtime with the voice flexibility of Azure Speech Services. We’ll dive deep into the implementation, covering everything from the initial problem to the complete working solution.</p><pre class="mermaid">flowchart TD    A[👤 User speaks] --> B[🎤 Microphone Input]    B --> C{Barge-in Detection<br/>Audio Level > Threshold?}    C -->|Yes| D[🛑 Stop Azure Speech]    C -->|No| E[📡 Stream to GPT-4o Realtime]        E --> F[🧠 GPT-4o Processing]    F --> G[📝 Text Response<br/>ContentModalities.Text]        G --> H[🗣️ Azure Speech Services<br/>Custom/Neural Voice]    H --> I[🔊 Audio Output]        D --> E    I --> J[👂 User hears response]    J --> A        style A fill:#e1f5fe    style D fill:#ffebee    style G fill:#f3e5f5    style H fill:#e8f5e8    style I fill:#fff3e0</pre><span id="more"></span><h2 id="The-real-problem-Why-GPT-4o-Realtime’s-voice-limitations-matter"><a href="#The-real-problem-Why-GPT-4o-Realtime’s-voice-limitations-matter" class="headerlink" title="The real problem: Why GPT-4o Realtime’s voice limitations matter"></a>The real problem: Why GPT-4o Realtime’s voice limitations matter</h2><p>When you’re working with Azure OpenAI’s GPT-4o Realtime API, the standard approach involves configuring a <code>RealtimeConversationSession</code> with one of the predefined voices. While these voices are high-quality, they create several significant limitations:</p><h3 id="1-Limited-voice-selection"><a href="#1-Limited-voice-selection" class="headerlink" title="1. Limited voice selection"></a>1. Limited voice selection</h3><p>You’re restricted to just six built-in voices. There’s no access to Azure Speech Services’ extensive catalog of 400+ neural voices across 140+ languages and locales. You can’t use premium voices like Jenny Neural (en-US) or specialized voices optimized for different use cases.</p><h3 id="2-No-custom-neural-voices"><a href="#2-No-custom-neural-voices" class="headerlink" title="2. No custom neural voices"></a>2. No custom neural voices</h3><p>Perhaps most importantly, you can’t integrate custom neural voices (CNV) that you’ve trained in Azure Speech Studio. This is crucial for:</p><ul><li><strong>Brand consistency</strong>: Companies that have invested in custom voice branding</li><li><strong>Specialized applications</strong>: Healthcare, education, or accessibility apps requiring specific voice characteristics</li><li><strong>Multilingual scenarios</strong>: Custom voices trained on specific accents or dialects</li></ul><h3 id="3-No-natural-interruption-barge-in"><a href="#3-No-natural-interruption-barge-in" class="headerlink" title="3. No natural interruption (barge-in)"></a>3. No natural interruption (barge-in)</h3><p>The built-in system doesn’t provide a way for users to naturally interrupt the assistant mid-response. In real conversations, we constantly interrupt each other—it’s natural and expected. Without this capability, your bot feels robotic and frustrating to use.</p><h3 id="4-Limited-voice-control"><a href="#4-Limited-voice-control" class="headerlink" title="4. Limited voice control"></a>4. Limited voice control</h3><p>You can’t dynamically adjust speech rate, pitch, or emphasis using SSML (Speech Synthesis Markup Language) that Azure Speech Services supports.</p><h2 id="The-solution-Hybrid-architecture-with-Azure-Speech-Services"><a href="#The-solution-Hybrid-architecture-with-Azure-Speech-Services" class="headerlink" title="The solution: Hybrid architecture with Azure Speech Services"></a>The solution: Hybrid architecture with Azure Speech Services</h2><p>The solution I’ve developed bypasses GPT-4o’s built-in text-to-speech entirely and routes the conversation text through Azure Speech Services. Here’s the high-level architecture:</p><ol><li><strong>Configure GPT-4o for text-only output</strong>: Disable built-in audio synthesis</li><li><strong>Stream and capture text responses</strong>: Collect the assistant’s text as it streams</li><li><strong>Route text to Azure Speech Services</strong>: Use any voice from Azure’s catalog or your custom neural voices</li><li><strong>Implement intelligent barge-in</strong>: Monitor microphone input and stop speech when user starts talking</li><li><strong>Seamless audio management</strong>: Handle audio playback and interruption smoothly</li></ol><p>This approach gives you the best of both worlds: GPT-4o’s intelligent conversation handling with Azure Speech Services’ superior voice options and control.</p><h2 id="Deep-dive-Implementation-walkthrough"><a href="#Deep-dive-Implementation-walkthrough" class="headerlink" title="Deep dive: Implementation walkthrough"></a>Deep dive: Implementation walkthrough</h2><p>Let me walk you through the complete implementation, explaining each component and how they work together.</p><h3 id="Project-structure-and-dependencies"><a href="#Project-structure-and-dependencies" class="headerlink" title="Project structure and dependencies"></a>Project structure and dependencies</h3><p>First, let’s look at the project structure. The solution consists of several key components:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">RealtimeChat/</span><br><span class="line">├── Program.cs                 # Main application logic</span><br><span class="line">├── AppSettings.cs             # Configuration classes</span><br><span class="line">├── Constants.cs               # Application constants</span><br><span class="line">└── Helpers/</span><br><span class="line">    ├── AudioInputHelper.cs    # Microphone input and barge-in detection</span><br><span class="line">    ├── AudioOutputHelper.cs   # Audio playback management</span><br><span class="line">    └── ConsoleHelper.cs       # Console UI utilities</span><br></pre></td></tr></table></figure><p>The key NuGet packages you’ll need:</p><ul><li><code>Azure.AI.OpenAI</code> - For GPT-4o Realtime API</li><li><code>Microsoft.CognitiveServices.Speech</code> - For Azure Speech Services</li><li><code>NAudio</code> - For audio input&#x2F;output handling</li><li><code>Microsoft.Extensions.Configuration.Json</code> - For configuration management</li></ul><h3 id="Configuration-setup"><a href="#Configuration-setup" class="headerlink" title="Configuration setup"></a>Configuration setup</h3><p>The configuration is designed to be flexible and environment-specific. Here’s the complete <code>AppSettings.cs</code> structure:</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">AppSettings</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> AzureOpenAISettings AzureOpenAI &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125; = <span class="keyword">new</span>();</span><br><span class="line">    <span class="keyword">public</span> AzureSpeechSettings AzureSpeech &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125; = <span class="keyword">new</span>();</span><br><span class="line">    <span class="keyword">public</span> ConversationSettings Conversation &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125; = <span class="keyword">new</span>();</span><br><span class="line">    <span class="keyword">public</span> <span class="built_in">double</span> BargeInThreshold &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">AzureOpenAISettings</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="built_in">string</span> Endpoint &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125; = <span class="string">&quot;&quot;</span>;</span><br><span class="line">    <span class="keyword">public</span> <span class="built_in">string</span> ApiKey &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125; = <span class="string">&quot;&quot;</span>;</span><br><span class="line">    <span class="keyword">public</span> <span class="built_in">string</span> ChatModelName &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125; = <span class="string">&quot;&quot;</span>;</span><br><span class="line">    <span class="keyword">public</span> <span class="built_in">string</span> RealtimeModelName &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125; = <span class="string">&quot;&quot;</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">AzureSpeechSettings</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="built_in">string</span> SubscriptionKey &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125; = <span class="string">&quot;&quot;</span>;</span><br><span class="line">    <span class="keyword">public</span> <span class="built_in">string</span> Region &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125; = <span class="string">&quot;&quot;</span>;</span><br><span class="line">    <span class="keyword">public</span> <span class="built_in">string</span> VoiceName &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125; = <span class="string">&quot;&quot;</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">ConversationSettings</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="built_in">string</span> OpenAIBuiltInVoice &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125; = <span class="string">&quot;sage&quot;</span>;</span><br><span class="line">    <span class="keyword">public</span> <span class="built_in">float</span> ServerDetectionThreshold &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125; = <span class="number">0.1f</span>;</span><br><span class="line">    <span class="keyword">public</span> <span class="built_in">int</span> ServerSilenceMs &#123; <span class="keyword">get</span>; <span class="keyword">set</span>; &#125; = <span class="number">150</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>And your <code>appsettings.json</code>:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;AzureOpenAI&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;Endpoint&quot;</span><span class="punctuation">:</span> <span class="string">&quot;https://your-openai-resource.openai.azure.com/&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;ApiKey&quot;</span><span class="punctuation">:</span> <span class="string">&quot;your-openai-api-key&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;ChatModelName&quot;</span><span class="punctuation">:</span> <span class="string">&quot;gpt-4o&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;RealtimeModelName&quot;</span><span class="punctuation">:</span> <span class="string">&quot;gpt-4o-realtime-preview&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;AzureSpeech&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;SubscriptionKey&quot;</span><span class="punctuation">:</span> <span class="string">&quot;your-speech-service-key&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;Region&quot;</span><span class="punctuation">:</span> <span class="string">&quot;australiaeast&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;VoiceName&quot;</span><span class="punctuation">:</span> <span class="string">&quot;en-US-AnaNeural&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;Conversation&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;OpenAIBuiltInVoice&quot;</span><span class="punctuation">:</span> <span class="string">&quot;sage&quot;</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;ServerDetectionThreshold&quot;</span><span class="punctuation">:</span> <span class="number">0.1</span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;ServerSilenceMs&quot;</span><span class="punctuation">:</span> <span class="number">150</span></span><br><span class="line">  <span class="punctuation">&#125;</span><span class="punctuation">,</span></span><br><span class="line">  <span class="attr">&quot;BargeInThreshold&quot;</span><span class="punctuation">:</span> <span class="number">0.02</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h3 id="The-heart-of-the-solution-Program-cs"><a href="#The-heart-of-the-solution-Program-cs" class="headerlink" title="The heart of the solution: Program.cs"></a>The heart of the solution: Program.cs</h3><p>The main program orchestrates all the components. Let’s break down the key sections:</p><h4 id="Service-initialization"><a href="#Service-initialization" class="headerlink" title="Service initialization"></a>Service initialization</h4><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> (SpeechConfig, AzureOpenAIClient) InitializeServices(AppSettings appSettings)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">// Configure Azure Speech Services</span></span><br><span class="line">    SpeechConfig speechConfig = SpeechConfig.FromSubscription(</span><br><span class="line">        appSettings.AzureSpeech.SubscriptionKey,</span><br><span class="line">        appSettings.AzureSpeech.Region</span><br><span class="line">    );</span><br><span class="line">    speechConfig.SpeechSynthesisVoiceName = appSettings.AzureSpeech.VoiceName;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Configure Azure OpenAI client</span></span><br><span class="line">    <span class="keyword">var</span> aoaiClient = <span class="keyword">new</span> AzureOpenAIClient(</span><br><span class="line">        <span class="keyword">new</span> Uri(appSettings.AzureOpenAI.Endpoint),</span><br><span class="line">        <span class="keyword">new</span> ApiKeyCredential(appSettings.AzureOpenAI.ApiKey)</span><br><span class="line">    );</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> (speechConfig, aoaiClient);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="Critical-Text-only-configuration"><a href="#Critical-Text-only-configuration" class="headerlink" title="Critical: Text-only configuration"></a>Critical: Text-only configuration</h4><p>This is the key breakthrough—configuring GPT-4o Realtime to output only text, not audio:</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">await</span> session.ConfigureSessionAsync(<span class="keyword">new</span> ConversationSessionOptions()</span><br><span class="line">&#123;</span><br><span class="line">    Voice = <span class="keyword">new</span> ConversationVoice(appSettings.Conversation.OpenAIBuiltInVoice),</span><br><span class="line">    ContentModalities = ConversationContentModalities.Text, <span class="comment">// 🔥 This is crucial!</span></span><br><span class="line">    Instructions = Constants.MainPrompt,</span><br><span class="line">    InputTranscriptionOptions = <span class="keyword">new</span>() &#123; Model = <span class="string">&quot;whisper-1&quot;</span> &#125;,</span><br><span class="line">    TurnDetectionOptions = ConversationTurnDetectionOptions</span><br><span class="line">        .CreateServerVoiceActivityTurnDetectionOptions(</span><br><span class="line">            detectionThreshold: appSettings.Conversation.ServerDetectionThreshold,</span><br><span class="line">            silenceDuration: TimeSpan.FromMilliseconds(appSettings.Conversation.ServerSilenceMs)</span><br><span class="line">        ),</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><p>By setting <code>ContentModalities = ConversationContentModalities.Text</code>, we tell GPT-4o to only send us text responses, not audio bytes. This is what allows us to route the text through Azure Speech Services instead.</p><h3 id="Advanced-barge-in-implementation"><a href="#Advanced-barge-in-implementation" class="headerlink" title="Advanced barge-in implementation"></a>Advanced barge-in implementation</h3><p>The barge-in feature is implemented in <code>AudioInputHelper.cs</code> and is one of the most sophisticated parts of the solution. Here’s how it works:</p><h4 id="Real-time-amplitude-monitoring"><a href="#Real-time-amplitude-monitoring" class="headerlink" title="Real-time amplitude monitoring"></a>Real-time amplitude monitoring</h4><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">private</span> <span class="built_in">bool</span> <span class="title">IsSpeechAboveThreshold</span>(<span class="params"><span class="built_in">byte</span>[] buffer, <span class="built_in">int</span> length, <span class="built_in">double</span> threshold</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">double</span> sum = <span class="number">0.0</span>;</span><br><span class="line">    <span class="built_in">int</span> sampleCount = length / <span class="number">2</span>; <span class="comment">// 16-bit samples</span></span><br><span class="line">    </span><br><span class="line">    <span class="keyword">for</span> (<span class="built_in">int</span> i = <span class="number">0</span>; i &lt; length; i += <span class="number">2</span>)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="built_in">short</span> sample = BitConverter.ToInt16(buffer, i);</span><br><span class="line">        sum += sample * (<span class="built_in">double</span>)sample;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Calculate RMS (Root Mean Square) of the audio</span></span><br><span class="line">    <span class="built_in">double</span> rms = Math.Sqrt(sum / sampleCount);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// Normalize to [0..1] range</span></span><br><span class="line">    <span class="built_in">double</span> normalized = rms / <span class="number">32768.0</span>;</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// Compare to threshold</span></span><br><span class="line">    <span class="keyword">return</span> normalized &gt; threshold;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="Smart-barge-in-event-handling"><a href="#Smart-barge-in-event-handling" class="headerlink" title="Smart barge-in event handling"></a>Smart barge-in event handling</h4><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">_waveInEvent.DataAvailable += (_, e) =&gt;</span><br><span class="line">&#123;</span><br><span class="line">    <span class="comment">// 1. Always copy to ring buffer for GPT-4o input</span></span><br><span class="line">    <span class="keyword">lock</span> (_bufferLock)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="comment">// ... buffer management code ...</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 2. Check for user speech (barge-in detection)</span></span><br><span class="line">    <span class="keyword">if</span> (IsSpeechAboveThreshold(e.Buffer, e.BytesRecorded, _bargeInThreshold))</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">var</span> now = DateTime.UtcNow;</span><br><span class="line">        <span class="comment">// Prevent event spam with cooldown period</span></span><br><span class="line">        <span class="keyword">if</span> ((now - _lastSpeechDetected).TotalMilliseconds &gt; <span class="number">500</span>)</span><br><span class="line">        &#123;</span><br><span class="line">            _lastSpeechDetected = now;</span><br><span class="line">            UserSpeechDetected?.Invoke(); <span class="comment">// Trigger barge-in!</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h4 id="Barge-in-event-wiring"><a href="#Barge-in-event-wiring" class="headerlink" title="Barge-in event wiring"></a>Barge-in event wiring</h4><p>In the main session handler, we wire up the barge-in detection:</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">void</span> <span class="title">HandleSessionStartedUpdate</span>(<span class="params">RealtimeConversationSession session, AppSettings appSettings, SpeechSynthesizer? currentSynthesizer</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    _ = Task.Run(<span class="keyword">async</span> () =&gt;</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">using</span> AudioInputHelper audioInputHelper = AudioInputHelper.Start(appSettings.BargeInThreshold);</span><br><span class="line"></span><br><span class="line">        audioInputHelper.UserSpeechDetected += () =&gt;</span><br><span class="line">        &#123;</span><br><span class="line">            ConsoleHelper.DisplayMessage(<span class="string">&quot;&lt;&lt;&lt; USER INTERRUPTION DETECTED! Stopping speech...&quot;</span>, <span class="literal">true</span>);</span><br><span class="line">            </span><br><span class="line">            <span class="keyword">if</span> (currentSynthesizer != <span class="literal">null</span>)</span><br><span class="line">            &#123;</span><br><span class="line">                currentSynthesizer.StopSpeakingAsync().Wait(); <span class="comment">// Stop immediately!</span></span><br><span class="line">            &#125;</span><br><span class="line">        &#125;;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">await</span> session.SendInputAudioAsync(audioInputHelper);</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Streaming-text-processing-and-Azure-Speech-integration"><a href="#Streaming-text-processing-and-Azure-Speech-integration" class="headerlink" title="Streaming text processing and Azure Speech integration"></a>Streaming text processing and Azure Speech integration</h3><p>The magic happens in how we handle the streaming response from GPT-4o and route it to Azure Speech Services:</p><h4 id="Collecting-streaming-text"><a href="#Collecting-streaming-text" class="headerlink" title="Collecting streaming text"></a>Collecting streaming text</h4><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">void</span> <span class="title">HandleStreamingPartDeltaUpdate</span>(<span class="params"></span></span></span><br><span class="line"><span class="params"><span class="function">    ConversationItemStreamingPartDeltaUpdate deltaUpdate, </span></span></span><br><span class="line"><span class="params"><span class="function">    Dictionary&lt;<span class="built_in">string</span>, StringBuilder&gt; partialTextByItemId, </span></span></span><br><span class="line"><span class="params"><span class="function">    AudioOutputHelper audioOutputHelper</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">string</span> chunk = deltaUpdate.Text ?? deltaUpdate.AudioTranscript;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (!<span class="built_in">string</span>.IsNullOrWhiteSpace(chunk))</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">if</span> (!partialTextByItemId.ContainsKey(deltaUpdate.ItemId))</span><br><span class="line">        &#123;</span><br><span class="line">            partialTextByItemId[deltaUpdate.ItemId] = <span class="keyword">new</span> StringBuilder();</span><br><span class="line">        &#125;</span><br><span class="line">        partialTextByItemId[deltaUpdate.ItemId].Append(chunk);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// <span class="doctag">NOTE:</span> We completely ignore deltaUpdate.AudioBytes since we&#x27;re using Azure Speech</span></span><br><span class="line">    <span class="comment">// Uncomment the next line if you want to fall back to built-in voice:</span></span><br><span class="line">    <span class="comment">// audioOutputHelper.EnqueueForPlayback(deltaUpdate.AudioBytes);</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="Converting-text-to-speech-with-Azure-Speech-Services"><a href="#Converting-text-to-speech-with-Azure-Speech-Services" class="headerlink" title="Converting text to speech with Azure Speech Services"></a>Converting text to speech with Azure Speech Services</h4><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">async</span> Task <span class="title">HandleStreamingFinishedUpdate</span>(<span class="params"></span></span></span><br><span class="line"><span class="params"><span class="function">    ConversationItemStreamingFinishedUpdate itemFinishedUpdate, </span></span></span><br><span class="line"><span class="params"><span class="function">    Dictionary&lt;<span class="built_in">string</span>, StringBuilder&gt; partialTextByItemId, </span></span></span><br><span class="line"><span class="params"><span class="function">    SpeechConfig speechConfig, </span></span></span><br><span class="line"><span class="params"><span class="function">    SpeechSynthesizer? currentSynthesizer</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">if</span> (partialTextByItemId.TryGetValue(itemFinishedUpdate.ItemId, <span class="keyword">out</span> <span class="keyword">var</span> sb))</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="built_in">string</span> finalAssistantText = sb.ToString();</span><br><span class="line">        ConsoleHelper.DisplayMessage(<span class="string">$&quot;Assistant: <span class="subst">&#123;finalAssistantText&#125;</span>&quot;</span>, <span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Route to Azure Speech Services</span></span><br><span class="line">        <span class="keyword">await</span> SpeakWithAzureSpeechAsync(finalAssistantText, speechConfig, currentSynthesizer);</span><br><span class="line"></span><br><span class="line">        partialTextByItemId.Remove(itemFinishedUpdate.ItemId);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">static</span> <span class="keyword">async</span> Task&lt;SpeechSynthesizer?&gt; SpeakWithAzureSpeechAsync(</span><br><span class="line">    <span class="built_in">string</span> text, </span><br><span class="line">    SpeechConfig speechConfig, </span><br><span class="line">    SpeechSynthesizer? synthesizer)</span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="built_in">string</span>.IsNullOrWhiteSpace(text)) <span class="keyword">return</span> synthesizer;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Stop any current speech before starting new</span></span><br><span class="line">    <span class="keyword">if</span> (synthesizer != <span class="literal">null</span>)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">await</span> synthesizer.StopSpeakingAsync();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Synthesize with Azure Speech Services</span></span><br><span class="line">    <span class="keyword">var</span> result = <span class="keyword">await</span> synthesizer.SpeakTextAsync(text);</span><br><span class="line">    </span><br><span class="line">    <span class="keyword">if</span> (result.Reason == ResultReason.SynthesizingAudioCompleted)</span><br><span class="line">    &#123;</span><br><span class="line">        Console.WriteLine(<span class="string">&quot;✅ Speech synthesis completed successfully&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">else</span> <span class="keyword">if</span> (result.Reason == ResultReason.Canceled)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">var</span> cancellation = SpeechSynthesisCancellationDetails.FromResult(result);</span><br><span class="line">        Console.WriteLine(<span class="string">$&quot;❌ Speech canceled: <span class="subst">&#123;cancellation.Reason&#125;</span>, <span class="subst">&#123;cancellation.ErrorDetails&#125;</span>&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> synthesizer;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Advanced-scenarios-and-customization"><a href="#Advanced-scenarios-and-customization" class="headerlink" title="Advanced scenarios and customization"></a>Advanced scenarios and customization</h2><h3 id="Using-custom-neural-voices"><a href="#Using-custom-neural-voices" class="headerlink" title="Using custom neural voices"></a>Using custom neural voices</h3><p>To use a custom neural voice you’ve trained in Azure Speech Studio, simply update your configuration:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;AzureSpeech&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;VoiceName&quot;</span><span class="punctuation">:</span> <span class="string">&quot;YourCustomVoiceName&quot;</span><span class="punctuation">,</span> <span class="comment">// Your CNV endpoint name</span></span><br><span class="line">    <span class="attr">&quot;Region&quot;</span><span class="punctuation">:</span> <span class="string">&quot;eastus&quot;</span><span class="punctuation">,</span> <span class="comment">// Region where your CNV is deployed</span></span><br><span class="line">    <span class="attr">&quot;SubscriptionKey&quot;</span><span class="punctuation">:</span> <span class="string">&quot;your-key&quot;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h3 id="SSML-support-for-advanced-voice-control"><a href="#SSML-support-for-advanced-voice-control" class="headerlink" title="SSML support for advanced voice control"></a>SSML support for advanced voice control</h3><p>You can enhance the speech synthesis with SSML for better control:</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">async</span> Task <span class="title">SpeakWithSSMLAsync</span>(<span class="params"><span class="built_in">string</span> text, SpeechConfig speechConfig, SpeechSynthesizer synthesizer</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="built_in">string</span> ssml = <span class="string">$@&quot;</span></span><br><span class="line"><span class="string">    &lt;speak version=&#x27;1.0&#x27; xmlns=&#x27;http://www.w3.org/2001/10/synthesis&#x27; xml:lang=&#x27;en-US&#x27;&gt;</span></span><br><span class="line"><span class="string">        &lt;voice name=&#x27;<span class="subst">&#123;speechConfig.SpeechSynthesisVoiceName&#125;</span>&#x27;&gt;</span></span><br><span class="line"><span class="string">            &lt;prosody rate=&#x27;medium&#x27; pitch=&#x27;medium&#x27;&gt;</span></span><br><span class="line"><span class="string">                <span class="subst">&#123;System.Security.SecurityElement.Escape(text)&#125;</span></span></span><br><span class="line"><span class="string">            &lt;/prosody&gt;</span></span><br><span class="line"><span class="string">        &lt;/voice&gt;</span></span><br><span class="line"><span class="string">    &lt;/speak&gt;&quot;</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">await</span> synthesizer.SpeakSsmlAsync(ssml);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="Fine-tuning-barge-in-sensitivity"><a href="#Fine-tuning-barge-in-sensitivity" class="headerlink" title="Fine-tuning barge-in sensitivity"></a>Fine-tuning barge-in sensitivity</h3><p>The barge-in threshold is crucial for a good user experience. Too sensitive, and background noise triggers interruptions. Too high, and users can’t interrupt naturally:</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="punctuation">&#123;</span></span><br><span class="line">  <span class="attr">&quot;BargeInThreshold&quot;</span><span class="punctuation">:</span> <span class="number">0.02</span>  <span class="comment">// Start here and adjust based on your environment</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>Values to try:</p><ul><li><strong>0.01</strong>: Very sensitive (good for quiet environments)</li><li><strong>0.02</strong>: Balanced (recommended starting point)</li><li><strong>0.05</strong>: Less sensitive (noisy environments)</li></ul><h3 id="Error-handling-and-resilience"><a href="#Error-handling-and-resilience" class="headerlink" title="Error handling and resilience"></a>Error handling and resilience</h3><p>The solution includes comprehensive error handling:</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">void</span> <span class="title">HandleErrorUpdate</span>(<span class="params">ConversationErrorUpdate errorUpdate</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    ConsoleHelper.DisplayError(<span class="string">$&quot;❌ GPT-4o Error: <span class="subst">&#123;errorUpdate.Message&#125;</span>&quot;</span>, <span class="literal">true</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// Log full error details for debugging</span></span><br><span class="line">    ConsoleHelper.DisplayError(<span class="string">$&quot;Full error details: <span class="subst">&#123;errorUpdate.GetRawContent()&#125;</span>&quot;</span>, <span class="literal">false</span>);</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// Could implement retry logic here</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">static</span> <span class="keyword">async</span> Task <span class="title">HandleSpeechStartedUpdate</span>(<span class="params"></span></span></span><br><span class="line"><span class="params"><span class="function">    ConversationInputSpeechStartedUpdate speechStartedUpdate, </span></span></span><br><span class="line"><span class="params"><span class="function">    SpeechSynthesizer? currentSynthesizer</span>)</span></span><br><span class="line">&#123;</span><br><span class="line">    ConsoleHelper.DisplayMessage(<span class="string">$&quot;🎤 Speech detected @ <span class="subst">&#123;speechStartedUpdate.AudioStartTime&#125;</span>&quot;</span>, <span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Always stop current speech when user starts talking</span></span><br><span class="line">    <span class="keyword">if</span> (currentSynthesizer != <span class="literal">null</span>)</span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">try</span></span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">await</span> currentSynthesizer.StopSpeakingAsync();</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">catch</span> (Exception ex)</span><br><span class="line">        &#123;</span><br><span class="line">            ConsoleHelper.DisplayError(<span class="string">$&quot;Error stopping speech: <span class="subst">&#123;ex.Message&#125;</span>&quot;</span>, <span class="literal">false</span>);</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Performance-considerations-and-optimization"><a href="#Performance-considerations-and-optimization" class="headerlink" title="Performance considerations and optimization"></a>Performance considerations and optimization</h2><h3 id="Latency-optimization"><a href="#Latency-optimization" class="headerlink" title="Latency optimization"></a>Latency optimization</h3><p>The hybrid approach adds minimal latency:</p><ul><li><strong>GPT-4o streaming</strong>: Near real-time text streaming</li><li><strong>Azure Speech synthesis</strong>: 100-300ms for typical responses</li><li><strong>Barge-in detection</strong>: &lt;50ms response time</li></ul><h3 id="Memory-management"><a href="#Memory-management" class="headerlink" title="Memory management"></a>Memory management</h3><p>The ring buffer implementation efficiently manages audio data:</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// ~10 seconds buffer to handle network variations</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">readonly</span> <span class="built_in">byte</span>[] _buffer = <span class="keyword">new</span> <span class="built_in">byte</span>[BYTES_PER_SAMPLE * SAMPLES_PER_SECOND * CHANNELS * <span class="number">10</span>];</span><br></pre></td></tr></table></figure><h3 id="Concurrent-operations"><a href="#Concurrent-operations" class="headerlink" title="Concurrent operations"></a>Concurrent operations</h3><p>The solution handles multiple concurrent operations smoothly:</p><ul><li>Microphone input streaming to GPT-4o</li><li>Real-time text streaming from GPT-4o</li><li>Audio synthesis and playback via Azure Speech</li><li>Barge-in detection and response</li></ul><h2 id="Deployment-and-production-considerations"><a href="#Deployment-and-production-considerations" class="headerlink" title="Deployment and production considerations"></a>Deployment and production considerations</h2><h3 id="Security-best-practices"><a href="#Security-best-practices" class="headerlink" title="Security best practices"></a>Security best practices</h3><ol><li><strong>API key management</strong>: Use Azure Key Vault for production</li><li><strong>Network security</strong>: Implement proper firewall rules</li><li><strong>Authentication</strong>: Add user authentication for production apps</li></ol><h3 id="Scaling-considerations"><a href="#Scaling-considerations" class="headerlink" title="Scaling considerations"></a>Scaling considerations</h3><ol><li><strong>Connection limits</strong>: Both services have concurrent connection limits</li><li><strong>Regional deployment</strong>: Deploy Speech Services in the same region as OpenAI</li><li><strong>Cost optimization</strong>: Monitor token usage and synthesis characters</li></ol><h3 id="Monitoring-and-logging"><a href="#Monitoring-and-logging" class="headerlink" title="Monitoring and logging"></a>Monitoring and logging</h3><p>Implement comprehensive logging for production:</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Add structured logging</span></span><br><span class="line">services.AddLogging(builder =&gt;</span><br><span class="line">&#123;</span><br><span class="line">    builder.AddConsole();</span><br><span class="line">    builder.AddApplicationInsights(); <span class="comment">// For production monitoring</span></span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure><h2 id="Conclusion-and-next-steps"><a href="#Conclusion-and-next-steps" class="headerlink" title="Conclusion and next steps"></a>Conclusion and next steps</h2><p>This hybrid approach solves the key limitations of GPT-4o Realtime’s built-in voices by providing:</p><p>✅ <strong>Unlimited voice selection</strong>: Access to 400+ Azure Speech neural voices<br>✅ <strong>Custom neural voice support</strong>: Use your own trained voices<br>✅ <strong>Natural barge-in capability</strong>: Users can interrupt naturally<br>✅ <strong>SSML support</strong>: Advanced voice control and customization<br>✅ <strong>Production-ready architecture</strong>: Robust error handling and performance  </p><p>The complete sample code is available in my <code>custom-voice-sample-code</code> folder, which you can use as a starting point for your own applications.</p><h3 id="What’s-next"><a href="#What’s-next" class="headerlink" title="What’s next?"></a>What’s next?</h3><p>Consider these enhancements for your implementation:</p><ol><li><strong>Multiple voice support</strong>: Let users choose their preferred voice</li><li><strong>Emotion detection</strong>: Adjust voice characteristics based on conversation sentiment</li><li><strong>Multi-language support</strong>: Dynamically switch languages and voices</li><li><strong>Integration with Teams&#x2F;Bot Framework</strong>: Extend to enterprise chat platforms</li></ol><p>The combination of GPT-4o’s conversational intelligence with Azure Speech Services’ voice flexibility opens up entirely new possibilities for voice-enabled applications. Whether you’re building customer service bots, educational tools, or therapeutic applications, this approach gives you the control and quality you need for professional deployments.</p><h2 id="References"><a href="#References" class="headerlink" title="References"></a>References</h2><ul><li><a href="https://learn.microsoft.com/en-us/azure/ai-services/speech-service/custom-neural-voice">Azure Speech Services Custom Neural Voice</a></li><li><a href="https://speech.microsoft.com/portal/voicegallery">Azure Speech Services Voice Gallery</a></li><li><a href="https://github.com/naudio/NAudio">NAudio Documentation</a></li><li>Main image generated by <a href="https://openai.com/blog/dall-e/">DALL-E</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🎯 TL;DR: Hybrid GPT-4o Realtime with Azure Speech Services Custom Voices&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This post demonstrates bypassing GPT-4o Realtime’s built-in voice limitations by creating a hybrid architecture that combines GPT-4o’s conversational intelligence with Azure Speech Services’ extensive voice catalog. The solution configures GPT-4o Realtime for text-only output (&lt;code&gt;ContentModalities.Text&lt;/code&gt;) and routes responses through Azure Speech Services, enabling access to 400+ neural voices, custom neural voices (CNV), and SSML control. The implementation includes intelligent barge-in functionality using real-time audio amplitude monitoring, allowing users to interrupt the assistant naturally mid-response.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Technical implementation:&lt;/strong&gt; C# application using Azure.AI.OpenAI and Microsoft.CognitiveServices.Speech SDKs, NAudio for audio I&amp;#x2F;O, streaming text collection from GPT-4o responses, RMS-based speech detection with configurable thresholds, and concurrent audio management for seamless interruption handling. &lt;strong&gt;&lt;a href=&quot;https://github.com/Ricky-G/azure-scenario-hub/tree/main/custom-voice-sample-code&quot;&gt;Complete C# source code with audio helpers available here&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;Building realtime voice-enabled applications with Azure OpenAI’s GPT-4o Realtime model is incredibly powerful, but there’s one significant limitation that can be a deal-breaker for many use cases: you’re stuck with OpenAI’s predefined voices like “sage”, “alloy”, “echo”, “fable”, “onyx”, and “nova”. &lt;/p&gt;
&lt;p&gt;What if you’re building a branded customer service bot that needs to match your company’s voice identity? Or developing a therapeutic application for children with autism where the voice quality and tone are crucial for engagement? What if your users need to interrupt the assistant naturally, just like in real human conversations?&lt;/p&gt;
&lt;p&gt;In this comprehensive guide, I’ll show you exactly how I solved these challenges by building a hybrid solution that combines the conversational intelligence of GPT-4o Realtime with the voice flexibility of Azure Speech Services. We’ll dive deep into the implementation, covering everything from the initial problem to the complete working solution.&lt;/p&gt;
&lt;pre class=&quot;mermaid&quot;&gt;flowchart TD
    A[👤 User speaks] --&gt; B[🎤 Microphone Input]
    B --&gt; C{Barge-in Detection&lt;br/&gt;Audio Level &gt; Threshold?}
    C --&gt;|Yes| D[🛑 Stop Azure Speech]
    C --&gt;|No| E[📡 Stream to GPT-4o Realtime]
    
    E --&gt; F[🧠 GPT-4o Processing]
    F --&gt; G[📝 Text Response&lt;br/&gt;ContentModalities.Text]
    
    G --&gt; H[🗣️ Azure Speech Services&lt;br/&gt;Custom/Neural Voice]
    H --&gt; I[🔊 Audio Output]
    
    D --&gt; E
    I --&gt; J[👂 User hears response]
    J --&gt; A
    
    style A fill:#e1f5fe
    style D fill:#ffebee
    style G fill:#f3e5f5
    style H fill:#e8f5e8
    style I fill:#fff3e0&lt;/pre&gt;</summary>
    
    
    
    <category term="Azure" scheme="https://clouddev.blog/categories/Azure/"/>
    
    <category term="AI" scheme="https://clouddev.blog/categories/Azure/AI/"/>
    
    <category term="Speech" scheme="https://clouddev.blog/categories/Azure/AI/Speech/"/>
    
    
    <category term="Azure" scheme="https://clouddev.blog/tags/Azure/"/>
    
    <category term="OpenAI" scheme="https://clouddev.blog/tags/OpenAI/"/>
    
    <category term="Speech Services" scheme="https://clouddev.blog/tags/Speech-Services/"/>
    
    <category term="Realtime" scheme="https://clouddev.blog/tags/Realtime/"/>
    
    <category term="C#" scheme="https://clouddev.blog/tags/C/"/>
    
  </entry>
  
  <entry>
    <title>Ignoring Azurite Files</title>
    <link href="https://clouddev.blog/Azure/Storage/Azurite/ignoring-azurite-files/"/>
    <id>https://clouddev.blog/Azure/Storage/Azurite/ignoring-azurite-files/</id>
    <published>2024-02-22T11:00:00.000Z</published>
    <updated>2025-08-07T05:42:25.052Z</updated>
    
    <content type="html"><![CDATA[<hr><blockquote><p><strong>🎯 TL;DR: Managing Azurite Storage Emulation Files in VS Code</strong></p><p>Local development with Azure Functions often requires Azurite (Azure Storage Emulator replacement) which generates storage files that clutter VS Code workspace. Problem: <code>__azurite__</code>, <code>__blobstorage__</code>, and <code>__queuestorage__</code> directories appear in project explorer making navigation difficult. Solution: Configure VS Code <code>files.exclude</code> settings to hide these emulation artifacts while preserving their functionality for local development and testing.</p></blockquote><hr><p>In the old days, developers relied on the Azure Storage Emulator to emulate Azure Storage services locally. However, Azure Storage Emulator has been deprecated and replaced with <strong>Azurite</strong>, which is now the recommended way to emulate Azure Blob, Queue, and Table storage locally. In this post, let’s see how to set up exclusions in Visual Studio Code to prevent unwanted Azurite files from cluttering your workspace while working with Function Apps.</p><p><img src="/Azure/Storage/Azurite/ignoring-azurite-files/azurite-files.png" alt="Azurite files"></p><span id="more"></span><h2 id="Starting-Azurite-Services"><a href="#Starting-Azurite-Services" class="headerlink" title="Starting Azurite Services"></a>Starting Azurite Services</h2><p>In Visual Studio Code, you can start Azurite services</p><p><img src="/Azure/Storage/Azurite/ignoring-azurite-files/azurite-start.png" alt="Start Azurite"></p><h2 id="Visual-Studio-Code-Setting-Up-File-Exclusions"><a href="#Visual-Studio-Code-Setting-Up-File-Exclusions" class="headerlink" title="Visual Studio Code: Setting Up File Exclusions"></a>Visual Studio Code: Setting Up File Exclusions</h2><p>Azurite’s local emulation files, while essential, can quickly overpopulate your project. To keep them hidden, Visual Studio Code’s <code>files.exclude</code> feature allows you to filter them out. Here’s how to add the necessary configuration to hide these files.</p><ol><li>Open the <strong>settings.json</strong> file in your project.</li></ol><p><img src="/Azure/Storage/Azurite/ignoring-azurite-files/open-visual-studio-code-settings.png" alt="Open Visual Studio Code Settings"></p><ol start="2"><li>Add the following block to exclude Azurite files:</li></ol><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="attr">&quot;files.exclude&quot;</span><span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    <span class="attr">&quot;__azurite__&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;__blobstorage__&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span><span class="punctuation">,</span></span><br><span class="line">    <span class="attr">&quot;__queuestorage__&quot;</span><span class="punctuation">:</span> <span class="literal"><span class="keyword">true</span></span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><p>This will automatically hide Azurite-related files from the VS Code explorer.</p><p><img src="/Azure/Storage/Azurite/ignoring-azurite-files/settings-file-with-azurite-exclude.png" alt="Open Visual Studio Code Settings"></p><h2 id="Conclusion"><a href="#Conclusion" class="headerlink" title="Conclusion"></a>Conclusion</h2><p>By setting up file exclusions in Visual Studio Code and <code>.gitignore</code>, you can prevent clutter from unnecessary Azurite files. This streamlines your development process and keeps your project cleaner.</p><h2 id="References"><a href="#References" class="headerlink" title="References"></a>References</h2><ul><li>Thumbnail image <a href="https://azure.microsoft.com/svghandler/storage/?width=1280&height=720">was taken from Azure SVG icons</a></li><li>Main image generated by <a href="https://openai.com/blog/dall-e/">DALL-E</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🎯 TL;DR: Managing Azurite Storage Emulation Files in VS Code&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Local development with Azure Functions often requires Azurite (Azure Storage Emulator replacement) which generates storage files that clutter VS Code workspace. Problem: &lt;code&gt;__azurite__&lt;/code&gt;, &lt;code&gt;__blobstorage__&lt;/code&gt;, and &lt;code&gt;__queuestorage__&lt;/code&gt; directories appear in project explorer making navigation difficult. Solution: Configure VS Code &lt;code&gt;files.exclude&lt;/code&gt; settings to hide these emulation artifacts while preserving their functionality for local development and testing.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;In the old days, developers relied on the Azure Storage Emulator to emulate Azure Storage services locally. However, Azure Storage Emulator has been deprecated and replaced with &lt;strong&gt;Azurite&lt;/strong&gt;, which is now the recommended way to emulate Azure Blob, Queue, and Table storage locally. In this post, let’s see how to set up exclusions in Visual Studio Code to prevent unwanted Azurite files from cluttering your workspace while working with Function Apps.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/Azure/Storage/Azurite/ignoring-azurite-files/azurite-files.png&quot; alt=&quot;Azurite files&quot;&gt;&lt;/p&gt;</summary>
    
    
    
    <category term="Azure" scheme="https://clouddev.blog/categories/Azure/"/>
    
    <category term="Storage" scheme="https://clouddev.blog/categories/Azure/Storage/"/>
    
    <category term="Azurite" scheme="https://clouddev.blog/categories/Azure/Storage/Azurite/"/>
    
    
    <category term="Azure" scheme="https://clouddev.blog/tags/Azure/"/>
    
    <category term="Storage" scheme="https://clouddev.blog/tags/Storage/"/>
    
    <category term="Azurite" scheme="https://clouddev.blog/tags/Azurite/"/>
    
    <category term="Function Apps" scheme="https://clouddev.blog/tags/Function-Apps/"/>
    
    <category term="Logic Apps" scheme="https://clouddev.blog/tags/Logic-Apps/"/>
    
  </entry>
  
  <entry>
    <title>Extracting GZip &amp; Tar Files Natively in .NET Without External Libraries</title>
    <link href="https://clouddev.blog/Azure/Function-Apps/NET/extracting-gzip-tar-files-natively-in-net-without-external-libraries/"/>
    <id>https://clouddev.blog/Azure/Function-Apps/NET/extracting-gzip-tar-files-natively-in-net-without-external-libraries/</id>
    <published>2023-06-24T12:00:00.000Z</published>
    <updated>2025-08-07T05:42:25.051Z</updated>
    
    <content type="html"><![CDATA[<hr><blockquote><p><strong>🎯 TL;DR: Native .tar.gz Extraction in .NET 7 Without External Dependencies</strong></p><p>Processing compressed .tar.gz files in Azure Functions traditionally required external libraries like SharpZipLib. Problem: External dependencies increase complexity and security surface area. Solution: .NET 7 introduces native <code>System.Formats.Tar</code> namespace alongside existing <code>System.IO.Compression</code> for GZip, enabling complete .tar.gz extraction without external dependencies. Implementation uses <code>GZipStream</code> for decompression and <code>TarReader</code> for archive extraction with proper entry type filtering and async operations.</p></blockquote><hr><h2 id="Introduction"><a href="#Introduction" class="headerlink" title="Introduction"></a>Introduction</h2><p>Imagine being in a scenario where a file of type .tar.gz lands in your Azure Blob Storage container. This file, when uncompressed, yields a collection of individual files. The trigger event for the arrival of this file is an Azure function, which springs into action, decompressing the contents and transferring them into a different container.</p><p>In this context, a team may instinctively reach out for a robust library like SharpZipLib. However, what if there is a mandate to accomplish this without external dependencies? This becomes a reality with .NET 7.</p><p>In .NET 7, native support for Tar files has been introduced, and GZip is catered to via <code>System.IO.Compression</code>. This means we can decompress a .tar.gz file natively in .NET 7, bypassing any need for external libraries.</p><p>This post will walk you through this process, providing a practical example using .NET 7 to show how this can be achieved.</p><h2 id="NET-7-Native-TAR-Support"><a href="#NET-7-Native-TAR-Support" class="headerlink" title=".NET 7: Native TAR Support"></a>.NET 7: Native TAR Support</h2><p>As of .NET 7, the <code>System.Formats.Tar</code> namespace was introduced to deal with TAR files, adding to the toolkit of .NET developers:</p><ul><li><code>System.Formats.Tar.TarFile</code> to pack a directory into a TAR file or extract a TAR file to a directory</li><li><code>System.Formats.Tar.TarReader</code> to read a TAR file</li><li><code>System.Formats.Tar.TarWriter</code> to write a TAR file</li></ul><p>These new capabilities significantly simplify the process of working with TAR files in .NET. Lets dive in an have a look at a code sample that demonstrates how to extract a .tar.gz file natively in .NET 7.</p><span id="more"></span><h2 id="A-Simple-Example-In-NET-7"><a href="#A-Simple-Example-In-NET-7" class="headerlink" title="A Simple Example In .NET 7"></a>A Simple Example In .NET 7</h2><p>Below is an example demonstrating the extraction of a .tar.gz file natively in .NET 7 in a simple console app to extract the contents of a .tar.gz file to a directory</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">using</span> System;</span><br><span class="line"><span class="keyword">using</span> System.IO;</span><br><span class="line"><span class="keyword">using</span> System.IO.Compression;</span><br><span class="line"><span class="keyword">using</span> System.Formats.Tar;</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="title">Program</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="function"><span class="keyword">static</span> <span class="keyword">void</span> <span class="title">Main</span>(<span class="params"><span class="built_in">string</span>[] args</span>)</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="built_in">string</span> sourceTarGzFilePath = <span class="string">@&quot;C:\_Temp\test.tar.gz&quot;</span>;</span><br><span class="line">        <span class="built_in">string</span> targetDirectory = <span class="string">@&quot;C:\_Temp\ExtractedFiles\&quot;</span>;</span><br><span class="line"></span><br><span class="line">        <span class="built_in">string</span> tarFilePath = Path.ChangeExtension(sourceTarGzFilePath, <span class="string">&quot;.tar&quot;</span>);</span><br><span class="line"></span><br><span class="line">        Directory.CreateDirectory(targetDirectory);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Decompress the .gz file</span></span><br><span class="line">        <span class="keyword">using</span> (FileStream originalFileStream = File.OpenRead(sourceTarGzFilePath))</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">using</span> (FileStream decompressedFileStream = File.Create(tarFilePath))</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="keyword">using</span> (GZipStream decompressionStream = <span class="keyword">new</span> GZipStream(originalFileStream, CompressionMode.Decompress))</span><br><span class="line">                &#123;</span><br><span class="line">                    decompressionStream.CopyTo(decompressedFileStream);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Extract the .tar file</span></span><br><span class="line">        <span class="keyword">using</span> (FileStream tarStream = File.OpenRead(tarFilePath))</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">using</span> (TarReader tarReader = <span class="keyword">new</span> TarReader(tarStream))</span><br><span class="line">            &#123;</span><br><span class="line">                TarEntry entry;</span><br><span class="line">                <span class="keyword">while</span> ((entry = tarReader.GetNextEntryAsync().Result) != <span class="literal">null</span>)</span><br><span class="line">                &#123;</span><br><span class="line">                    <span class="keyword">if</span> (entry.EntryType <span class="keyword">is</span> TarEntryType.SymbolicLink <span class="keyword">or</span> TarEntryType.HardLink <span class="keyword">or</span> TarEntryType.GlobalExtendedAttributes)</span><br><span class="line">                    &#123;</span><br><span class="line">                        <span class="keyword">continue</span>;</span><br><span class="line">                    &#125;</span><br><span class="line"></span><br><span class="line">                    Console.WriteLine(<span class="string">$&quot;Extracting <span class="subst">&#123;entry.Name&#125;</span>&quot;</span>);</span><br><span class="line">                    entry.ExtractToFileAsync(Path.Combine(targetDirectory, entry.Name), <span class="literal">true</span>).Wait();</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Delete the temporary .tar file</span></span><br><span class="line">        File.Delete(tarFilePath);</span><br><span class="line"></span><br><span class="line">        Console.WriteLine(<span class="string">&quot;Extraction Completed&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>You can also find this on <a href="https://gist.github.com/Ricky-G/5562922ca29ab8f8a349dc07917d65af">GitHub Gist</a>.</p><h2 id="Wrapping-Up"><a href="#Wrapping-Up" class="headerlink" title="Wrapping Up"></a>Wrapping Up</h2><p>The introduction of System.Formats.Tar in .NET 7 marks a significant milestone for developers dealing with .tar.gz files. It provides us with the ability to decompress these file types natively, without relying on external libraries. This functionality is a game-changer as it reduces complexity, minimizes external dependencies, and enhances the versatility of .NET applications.</p><p>The new namespace <code>System.Formats.Tar</code>, along with the established <code>System.IO.Compression</code>, effectively handle TAR and GZip files. This considerably simplifies the process, making the .NET environment more self-contained and versatile.</p><h2 id="References"><a href="#References" class="headerlink" title="References"></a>References</h2><ul><li>Thumbnail image [was taken from the DotNet brand repo]<a href="https://github.com/dotnet/brand">https://github.com/dotnet/brand</a>)</li><li>Main image generated by [was taken from the DotNet brand repo]<a href="https://github.com/dotnet/brand">https://github.com/dotnet/brand</a>)</li></ul>]]></content>
    
    
    <summary type="html">&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🎯 TL;DR: Native .tar.gz Extraction in .NET 7 Without External Dependencies&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Processing compressed .tar.gz files in Azure Functions traditionally required external libraries like SharpZipLib. Problem: External dependencies increase complexity and security surface area. Solution: .NET 7 introduces native &lt;code&gt;System.Formats.Tar&lt;/code&gt; namespace alongside existing &lt;code&gt;System.IO.Compression&lt;/code&gt; for GZip, enabling complete .tar.gz extraction without external dependencies. Implementation uses &lt;code&gt;GZipStream&lt;/code&gt; for decompression and &lt;code&gt;TarReader&lt;/code&gt; for archive extraction with proper entry type filtering and async operations.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&quot;Introduction&quot;&gt;&lt;a href=&quot;#Introduction&quot; class=&quot;headerlink&quot; title=&quot;Introduction&quot;&gt;&lt;/a&gt;Introduction&lt;/h2&gt;&lt;p&gt;Imagine being in a scenario where a file of type .tar.gz lands in your Azure Blob Storage container. This file, when uncompressed, yields a collection of individual files. The trigger event for the arrival of this file is an Azure function, which springs into action, decompressing the contents and transferring them into a different container.&lt;/p&gt;
&lt;p&gt;In this context, a team may instinctively reach out for a robust library like SharpZipLib. However, what if there is a mandate to accomplish this without external dependencies? This becomes a reality with .NET 7.&lt;/p&gt;
&lt;p&gt;In .NET 7, native support for Tar files has been introduced, and GZip is catered to via &lt;code&gt;System.IO.Compression&lt;/code&gt;. This means we can decompress a .tar.gz file natively in .NET 7, bypassing any need for external libraries.&lt;/p&gt;
&lt;p&gt;This post will walk you through this process, providing a practical example using .NET 7 to show how this can be achieved.&lt;/p&gt;
&lt;h2 id=&quot;NET-7-Native-TAR-Support&quot;&gt;&lt;a href=&quot;#NET-7-Native-TAR-Support&quot; class=&quot;headerlink&quot; title=&quot;.NET 7: Native TAR Support&quot;&gt;&lt;/a&gt;.NET 7: Native TAR Support&lt;/h2&gt;&lt;p&gt;As of .NET 7, the &lt;code&gt;System.Formats.Tar&lt;/code&gt; namespace was introduced to deal with TAR files, adding to the toolkit of .NET developers:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;System.Formats.Tar.TarFile&lt;/code&gt; to pack a directory into a TAR file or extract a TAR file to a directory&lt;/li&gt;
&lt;li&gt;&lt;code&gt;System.Formats.Tar.TarReader&lt;/code&gt; to read a TAR file&lt;/li&gt;
&lt;li&gt;&lt;code&gt;System.Formats.Tar.TarWriter&lt;/code&gt; to write a TAR file&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These new capabilities significantly simplify the process of working with TAR files in .NET. Lets dive in an have a look at a code sample that demonstrates how to extract a .tar.gz file natively in .NET 7.&lt;/p&gt;</summary>
    
    
    
    <category term="Azure" scheme="https://clouddev.blog/categories/Azure/"/>
    
    <category term="Function Apps" scheme="https://clouddev.blog/categories/Azure/Function-Apps/"/>
    
    <category term=".NET" scheme="https://clouddev.blog/categories/Azure/Function-Apps/NET/"/>
    
    
    <category term="Azure" scheme="https://clouddev.blog/tags/Azure/"/>
    
    <category term="Azure Blob Storage" scheme="https://clouddev.blog/tags/Azure-Blob-Storage/"/>
    
    <category term=".NET" scheme="https://clouddev.blog/tags/NET/"/>
    
    <category term="GZip" scheme="https://clouddev.blog/tags/GZip/"/>
    
    <category term="Tar" scheme="https://clouddev.blog/tags/Tar/"/>
    
  </entry>
  
  <entry>
    <title>Unzipping and Shuffling GBs of Data Using Azure Functions</title>
    <link href="https://clouddev.blog/Azure/Function-Apps/unzipping-and-shuffling-gbs-of-data-using-azure-functions/"/>
    <id>https://clouddev.blog/Azure/Function-Apps/unzipping-and-shuffling-gbs-of-data-using-azure-functions/</id>
    <published>2023-05-18T12:00:00.000Z</published>
    <updated>2025-08-07T05:42:25.054Z</updated>
    
    <content type="html"><![CDATA[<hr><blockquote><p><strong>🎯 TL;DR: Stream-Based Large File Processing in Azure Functions</strong></p><p>Processing multi-gigabyte zip files in Azure Functions requires streaming approach due to 1.5GB memory limit on Consumption plan. Problem: Large compressed files cannot be loaded entirely into memory for extraction. Solution: Stream-based unzipping using blob triggers with two implementation options: native .NET ZipArchive (slower but dependency-free) vs SharpZipLib (faster with custom buffer sizes). Architecture includes separate blob containers for zipped&#x2F;unzipped files with Function App triggered by blob storage events for scalable data processing.</p></blockquote><hr><p>Consider this situation: you have a zip file stored in an Azure Blob Storage container (or any other location for that matter). This isn’t just any zip file; it’s large, containing gigabytes of data. It could be big data sets for your machine learning projects, log files, media files, or backups. The specific content isn’t the focus - the size is.</p><p>The task? We need to unzip this massive file(s) and relocate its contents to a different Azure Blob storage container. This task might seem daunting, especially considering the size of the file and the potential number of files that might be housed within it.</p><p>Why do we need to do this? The use cases are numerous. Handling large data sets, moving data for analysis, making backups more accessible - these are just a few examples. The key here is that we’re looking for a scalable and reliable solution to handle this task efficiently.</p><p><strong>Azure Data Factory is arguably a better fit for this sort of task, but In this blog post, we will specifically demonstrate how to establish this process using Azure Functions</strong>. Specifically we will try to achieve this within the constraints of the Consumption plan tier, where the maximum memory is capped at 1.5GB, with the supporting roles of Azure CLI and PowerShell in our setup.</p><h2 id="Setting-Up-Our-Azure-Environment"><a href="#Setting-Up-Our-Azure-Environment" class="headerlink" title="Setting Up Our Azure Environment"></a>Setting Up Our Azure Environment</h2><p>Before we dive into scripting and code, we need to set the stage - that means setting up our Azure environment. We’re going to create a storage account with two containers, one for our Zipped files and the other for Unzipped files.</p><p>To create this setup, we’ll be using the Azure CLI. Why? Because it’s efficient and lets us script out the whole process if we need to do it again in the future.</p><ol><li><p>Install Azure CLI: If you haven’t already installed Azure CLI on your local machine, <a href="https://learn.microsoft.com/en-us/cli/azure/install-azure-cli">you can get it from here</a>.</p></li><li><p>Login to Azure: Open your terminal and type the following command to login to your Azure account. You’ll be prompted to enter your credentials.</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">az login    </span><br></pre></td></tr></table></figure></li><li><p>Create a Resource Group: We’ll need a Resource Group to keep our resources organized. We’ll call this rg-function-app-unzip-test and create it in the eastus location (you can ofcourse choose which ever region you like).</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">az group create --name rg-function-app-unzip-test --location eastus    </span><br></pre></td></tr></table></figure><span id="more"></span></li><li><p>Create a Storage Account: Next, we’ll create a storage account within our Resource Group. We’ll name it unziptststorageacct.</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">az storage account create --name unziptststorageacct --resource-group rg-function-app-unzip-test --location eastus --sku Standard_LRS    </span><br></pre></td></tr></table></figure></li><li><p>Create the Blob Containers: Finally, we’ll create our two containers, ‘Zipped’ and ‘Unzipped’ in the unziptststorageacct storage account.</p> <figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">az storage container create --name zipped --account-name unziptststorageacct</span><br><span class="line">az storage container create --name unzipped --account-name unziptststorageacct    </span><br></pre></td></tr></table></figure><p>Now your Azure environment is ready with the specific resource group and storage account names you provided! We’ve got our storage account unziptststorageacct and two containers ‘Zipped’ and ‘Unzipped’ set up for our operations. The next step is to create our zip file.</p></li></ol><h2 id="Concocting-Our-Data-With-PowerShell"><a href="#Concocting-Our-Data-With-PowerShell" class="headerlink" title="Concocting Our Data With PowerShell"></a>Concocting Our Data With PowerShell</h2><p>Our next task is to create a large zip file filled with multiple 100MB files, all brimming with random text. In a real world scenario you would already have these large files, but since we are simulating lets use PowerShell to create them.</p><article class="message is-success">                <div class="message-body">            <p>If you already have an existing zip file with large’ish files for testing, you can skip this step and use that file instead.</p>        </div>    </article><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># Set the number of files we want to create</span></span><br><span class="line"><span class="variable">$fileCount</span> = <span class="number">10</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># The path where you want to create the TestFiles directory</span></span><br><span class="line"><span class="variable">$directory</span> = <span class="string">&quot;C:\_Temp\TestFiles&quot;</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># Create a new directory for our files if it doesn&#x27;t already exist</span></span><br><span class="line"><span class="keyword">if</span>(<span class="operator">-not</span> (<span class="built_in">Test-Path</span> <span class="literal">-Path</span> <span class="variable">$directory</span>))&#123;</span><br><span class="line">    <span class="built_in">New-Item</span> <span class="literal">-ItemType</span> Directory <span class="literal">-Path</span> <span class="variable">$directory</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment"># Loop through and create our files</span></span><br><span class="line"><span class="keyword">for</span> (<span class="variable">$i</span>=<span class="number">1</span>; <span class="variable">$i</span> <span class="operator">-le</span> <span class="variable">$fileCount</span>; <span class="variable">$i</span>++)&#123;</span><br><span class="line">    <span class="comment"># Generate a 100MB file filled with random text and save it in our new directory</span></span><br><span class="line">    <span class="variable">$fileContent</span> = <span class="built_in">New-Object</span> byte[] <span class="number">104857600</span></span><br><span class="line">    (<span class="built_in">New-Object</span> Random).NextBytes(<span class="variable">$fileContent</span>)</span><br><span class="line">    [<span class="type">System.IO.File</span>]::WriteAllBytes(<span class="string">&quot;<span class="variable">$directory</span>\File<span class="variable">$i</span>.txt&quot;</span>, <span class="variable">$fileContent</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment"># Now that we have all our files, let&#x27;s zip them up</span></span><br><span class="line"><span class="built_in">Compress-Archive</span> <span class="literal">-Path</span> <span class="string">&quot;<span class="variable">$directory</span>\*&quot;</span> <span class="literal">-DestinationPath</span> <span class="string">&quot;<span class="variable">$directory</span>.zip&quot;</span></span><br></pre></td></tr></table></figure><p>This is a simple script that is creating 10 files, each 100MB in size, and then zipping them up into a single file. The resulting zip file should be around the 1GB in size.</p><blockquote><p>Incase you are wondering how we end up with a 1GB+ file by compressing 1GB worth of data? we are generating files filled with random bytes. Compression algorithms work by finding and eliminating redundancy in the data. Since random data has no redundancy, it cannot be compressed. In fact, trying to compress random data can even result in output that is slightly larger than the input, due to the overhead of the compression format.</p></blockquote><p>We’ll use this file to test our Azure Function.</p><h2 id="Azure-Function-To-Unzip"><a href="#Azure-Function-To-Unzip" class="headerlink" title="Azure Function To Unzip"></a>Azure Function To Unzip</h2><p>We’re going to create a Function that magically springs into action the moment a blob (our zipped file) lands in the ‘Zipped’ container. This function will stream the data, unzip the files, and stores them neatly as individual files in the ‘Unzipped’ container.</p><p>Before we begin, ensure that you’ve installed the <a href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local?tabs=v4,windows,csharp,portal,bash#v2">Azure Functions Core Tools</a> locally. You’d also need the <a href="https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-azurefunctions">Azure Functions Extension for Visual Studio Code</a>.</p><p>First lets use the CLI to create our consumption plan function app. We’ll call it unzipfunctionapp and use the unziptststorageacct storage account we created earlier. We’ll also specify the runtime as dotnet and the functions version as 4. We are using the consumption plan to demonstrate that this solution can work within the constraints of the consumption plan, where the maximum memory is capped at 1.5GB.</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">az functionapp create --resource-group rg-function-app-unzip-test --consumption-plan-location eastus --runtime dotnet --functions-version 4 --name unzipfunctionapp123 --storage-account unziptststorageacct</span><br></pre></td></tr></table></figure><article class="message is-warning">                <div class="message-body">            <p>You might need to change the function name in the example about from ‘unzipfunctionapp123’. This could already be taken; this is because, Azure function app name must have Globally unique name.<br>When you create Azure function app, you specify the name which becomes part of URL <azurefunctionname>.azurewebsites.net</p><p>If the function app name is already taken you will get an error ‘Website with given name unzipfunctionapp already exists.’ when you run the cli command above.</p>        </div>    </article><p>Now that we have a consumption plan function infra, lets see the full code that will do the actual task of unzipping and uploading<br>There are two code samples and both are quite similar in their basic approach. They both handle the data in a streaming manner, which allows them to deal with large files without consuming a lot of memory.</p><p>However, there are some differences in the details of how they handle the streaming, which may have implications for their performance and resource usage:</p><blockquote><ul><li>The first code sample uses the ZipArchive class from the .NET Framework, which provides a high-level, user-friendly interface for dealing with zip files. The second code sample uses the ZipInputStream class from the SharpZipLib library, which provides a lower-level, more flexible interface.</li><li>In the first code sample, the ZipArchive automatically takes care of reading from the blob stream and unzipping the data. It provides an Open method for each entry in the zip file, which returns a stream that you can read the unzipped data from. In the second code sample, you manually read from the ZipInputStream and write to the blob stream using the StreamUtils.Copy method.</li><li>The second code sample manually handles the buffer size with new byte[4096] for copying data from the zip input stream to the blob output stream. In contrast, the first code sample relies on the default buffer size provided by the UploadFromStreamAsync method.</li></ul></blockquote><article class="message is-warning">                <div class="message-body">            <p>Memory wise both are similar (i.e.: they don’t download the entire zip file into memory), but the first script takes around 20 minutes to process a 1GB zip file (with 10 * 100 MB files), whereas the second script takes about 10 minutes for the same 1GB zip file.  This mainly comes down to setting the custom buffer size and the optimizations in the SharpZipLib library</p><p> First script has the benefit of not importing any custom library, but cant not run on an Azure consumption plan, at the time of this writing, consumption plan has a max 10 minute runtime.<br> Second script can potentially run on a consumption plan, but comes at a cost of having to import a 3rd party library.</p>        </div>    </article><script src="//gist.github.com/Ricky-G/a9d670728a4f554b1234e4cb3ee74189.js"></script><h2 id="References"><a href="#References" class="headerlink" title="References"></a>References</h2><ul><li>Thumbnail image <a href="https://azure.microsoft.com/svghandler">was taken from the Azure site</a></li><li>Main image generated by <a href="https://openai.com/blog/dall-e/">DALL-E</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🎯 TL;DR: Stream-Based Large File Processing in Azure Functions&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Processing multi-gigabyte zip files in Azure Functions requires streaming approach due to 1.5GB memory limit on Consumption plan. Problem: Large compressed files cannot be loaded entirely into memory for extraction. Solution: Stream-based unzipping using blob triggers with two implementation options: native .NET ZipArchive (slower but dependency-free) vs SharpZipLib (faster with custom buffer sizes). Architecture includes separate blob containers for zipped&amp;#x2F;unzipped files with Function App triggered by blob storage events for scalable data processing.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;Consider this situation: you have a zip file stored in an Azure Blob Storage container (or any other location for that matter). This isn’t just any zip file; it’s large, containing gigabytes of data. It could be big data sets for your machine learning projects, log files, media files, or backups. The specific content isn’t the focus - the size is.&lt;/p&gt;
&lt;p&gt;The task? We need to unzip this massive file(s) and relocate its contents to a different Azure Blob storage container. This task might seem daunting, especially considering the size of the file and the potential number of files that might be housed within it.&lt;/p&gt;
&lt;p&gt;Why do we need to do this? The use cases are numerous. Handling large data sets, moving data for analysis, making backups more accessible - these are just a few examples. The key here is that we’re looking for a scalable and reliable solution to handle this task efficiently.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Azure Data Factory is arguably a better fit for this sort of task, but In this blog post, we will specifically demonstrate how to establish this process using Azure Functions&lt;/strong&gt;. Specifically we will try to achieve this within the constraints of the Consumption plan tier, where the maximum memory is capped at 1.5GB, with the supporting roles of Azure CLI and PowerShell in our setup.&lt;/p&gt;
&lt;h2 id=&quot;Setting-Up-Our-Azure-Environment&quot;&gt;&lt;a href=&quot;#Setting-Up-Our-Azure-Environment&quot; class=&quot;headerlink&quot; title=&quot;Setting Up Our Azure Environment&quot;&gt;&lt;/a&gt;Setting Up Our Azure Environment&lt;/h2&gt;&lt;p&gt;Before we dive into scripting and code, we need to set the stage - that means setting up our Azure environment. We’re going to create a storage account with two containers, one for our Zipped files and the other for Unzipped files.&lt;/p&gt;
&lt;p&gt;To create this setup, we’ll be using the Azure CLI. Why? Because it’s efficient and lets us script out the whole process if we need to do it again in the future.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Install Azure CLI: If you haven’t already installed Azure CLI on your local machine, &lt;a href=&quot;https://learn.microsoft.com/en-us/cli/azure/install-azure-cli&quot;&gt;you can get it from here&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Login to Azure: Open your terminal and type the following command to login to your Azure account. You’ll be prompted to enter your credentials.&lt;/p&gt;
 &lt;figure class=&quot;highlight bash&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;az login    &lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Create a Resource Group: We’ll need a Resource Group to keep our resources organized. We’ll call this rg-function-app-unzip-test and create it in the eastus location (you can ofcourse choose which ever region you like).&lt;/p&gt;
 &lt;figure class=&quot;highlight bash&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;az group create --name rg-function-app-unzip-test --location eastus    &lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;</summary>
    
    
    
    <category term="Azure" scheme="https://clouddev.blog/categories/Azure/"/>
    
    <category term="Function Apps" scheme="https://clouddev.blog/categories/Azure/Function-Apps/"/>
    
    
    <category term="PowerShell" scheme="https://clouddev.blog/tags/PowerShell/"/>
    
    <category term="Azure" scheme="https://clouddev.blog/tags/Azure/"/>
    
    <category term="Function Apps" scheme="https://clouddev.blog/tags/Function-Apps/"/>
    
    <category term="Azure Blob Storage" scheme="https://clouddev.blog/tags/Azure-Blob-Storage/"/>
    
    <category term="Azure CLI" scheme="https://clouddev.blog/tags/Azure-CLI/"/>
    
  </entry>
  
  <entry>
    <title>Azure DevTest Labs Policies</title>
    <link href="https://clouddev.blog/Azure/DevTest-Labs/azure-devtest-labs-policies/"/>
    <id>https://clouddev.blog/Azure/DevTest-Labs/azure-devtest-labs-policies/</id>
    <published>2023-01-31T11:00:00.000Z</published>
    <updated>2025-08-07T05:42:25.051Z</updated>
    
    <content type="html"><![CDATA[<hr><blockquote><p><strong>🎯 TL;DR: DevTest Labs Policy Configuration with Bicep IaC</strong></p><p>Azure DevTest Labs documentation covers basic lab deployment but lacks policy configuration examples in Bicep. Problem: Missing guidance on linking policies to DevTest Labs using Infrastructure as Code. Solution: Use <code>Microsoft.DevTestLab/labs/policysets</code> resource with ‘default’ name as parent for policy definitions. Implementation includes VM size restrictions, user VM quotas, and premium SSD limits using evaluator types like <code>AllowedValuesPolicy</code> and <code>MaxValuePolicy</code> with proper threshold configurations.</p></blockquote><hr><p>Azure DevTest Labs offers a powerful cloud-based development workstation environment and great alternative to a local development workstation&#x2F;laptop when it comes to software development. This blog post is not so much talking about the benefits of DevTest Lab, but more about how to create policies for DevTest Labs using Bicep.  Although there is a good support for <a href="https://learn.microsoft.com/en-us/azure/templates/microsoft.devtestlab/labs?pivots=deployment-language-bicep">deploying DevTest labs with Bicep</a>, there is little to no documentation when it comes to creating policies for DevTest Labs in Bicep. In this blog post, we will focus on creating policies for DevTest Labs using Bicep and how to go about doing this.</p><h2 id="A-Brief-Overview-of-Azure-DevTest-Labs"><a href="#A-Brief-Overview-of-Azure-DevTest-Labs" class="headerlink" title="A Brief Overview of Azure DevTest Labs"></a>A Brief Overview of Azure DevTest Labs</h2><p>Azure DevTest Labs is a managed service that enables developers to quickly create, manage, and share development and test environments. It provides a range of features and tools designed to streamline the development process, minimize costs, and improve overall productivity. By leveraging the power of the cloud, developers can easily spin up virtual machines (VMs) pre-configured with the necessary tools, frameworks, and software needed for their projects.</p><h2 id="Existing-Documentation-Limitations"><a href="#Existing-Documentation-Limitations" class="headerlink" title="Existing Documentation Limitations"></a>Existing Documentation Limitations</h2><p>While the existing documentation covers various aspects of Azure DevTest Labs, it lacks clear guidance on setting up policies with DevTest Labs in Bicep. This blog post aims to address that gap by providing a Bicep script for creating a DevTest Lab and applying policies to it. Shout out to my colleague <a href="https://www.linkedin.com/in/illian-yuan">Illian Y</a> for persisting and not giving up and finding a away around undocumented features and showing me.</p><span id="more"></span><h2 id="Existing-Documentation-For-Creating-a-DevTest-Lab"><a href="#Existing-Documentation-For-Creating-a-DevTest-Lab" class="headerlink" title="Existing Documentation For Creating a DevTest Lab"></a>Existing Documentation For Creating a DevTest Lab</h2><p><a href="https://learn.microsoft.com/en-us/azure/templates/microsoft.devtestlab/labs?pivots=deployment-language-bicep">The existing documentation</a> for creating a DevTest Lab is pretty good, but when it comes to creating <a href="https://learn.microsoft.com/en-us/azure/templates/microsoft.devtestlab/labs/policysets/policies?pivots=deployment-language-bicep">policies for DevTest Lab</a> this is where the documentation falls short.  The documentation does not provide a Bicep script for creating policies for DevTest Labs.</p><h2 id="Vanilla-DevTest-Lab"><a href="#Vanilla-DevTest-Lab" class="headerlink" title="Vanilla DevTest Lab"></a>Vanilla DevTest Lab</h2><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">resource lab &#x27;Microsoft.DevTestLab/labs@<span class="number">2018</span><span class="number">-09</span><span class="number">-15</span>&#x27; = <span class="punctuation">&#123;</span></span><br><span class="line">  name<span class="punctuation">:</span> &#x27;testLab&#x27;</span><br><span class="line">  location<span class="punctuation">:</span> &#x27;australiacentral&#x27;</span><br><span class="line">  tags<span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    tagName1<span class="punctuation">:</span> &#x27;test-tag&#x27;</span><br><span class="line">    tagName2<span class="punctuation">:</span> &#x27;test-tag1&#x27;</span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line">  properties<span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    environmentPermission<span class="punctuation">:</span> &#x27;Contributor&#x27;</span><br><span class="line">    labStorageType<span class="punctuation">:</span> &#x27;Premium&#x27;</span><br><span class="line">    mandatoryArtifactsResourceIdsLinux<span class="punctuation">:</span> <span class="punctuation">[</span><span class="punctuation">]</span></span><br><span class="line">    mandatoryArtifactsResourceIdsWindows<span class="punctuation">:</span> <span class="punctuation">[</span><span class="punctuation">]</span></span><br><span class="line">    premiumDataDisks<span class="punctuation">:</span> &#x27;Disabled&#x27;</span><br><span class="line">    announcement<span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      enabled<span class="punctuation">:</span> &#x27;Disabled&#x27;</span><br><span class="line">      expired<span class="punctuation">:</span> <span class="literal"><span class="keyword">false</span></span></span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">    support<span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">      enabled<span class="punctuation">:</span> &#x27;Enabled&#x27;</span><br><span class="line">      markdown<span class="punctuation">:</span> &#x27;Test&#x27;</span><br><span class="line">    <span class="punctuation">&#125;</span></span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h2 id="Creating-Policies-for-DevTest-Labs-in-Bicep"><a href="#Creating-Policies-for-DevTest-Labs-in-Bicep" class="headerlink" title="Creating Policies for DevTest Labs in Bicep"></a>Creating Policies for DevTest Labs in Bicep</h2><p>The documentation states all the possible policies that can be created under the fact name in <a href="https://learn.microsoft.com/en-us/azure/templates/microsoft.devtestlab/labs/policysets/policies?pivots=deployment-language-bicep#policyproperties">PolicyProperties</a></p><p>Below is a list of three of those policies that can be created in Bicep.</p><ul><li>Allowed VM Sizes</li><li>Allowed VMs Per User</li><li>Allowed Premium SSD Per User</li></ul><h3 id="Linking-the-policies-to-the-DevTest-Labs"><a href="#Linking-the-policies-to-the-DevTest-Labs" class="headerlink" title="Linking the policies to the DevTest Labs"></a>Linking the policies to the DevTest Labs</h3><p>This is the important glue that is missing from the documentation, how to link the policies to the DevTest Labs.  The way to do this is to create a resource policySetParent and link it to the DevTest Labs. The policySetParent resource is then used as the parent for the policies.</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">resource policySetParent &#x27;Microsoft.DevTestLab/labs/policysets@<span class="number">2018</span><span class="number">-09</span><span class="number">-15</span>&#x27; existing = <span class="punctuation">&#123;</span></span><br><span class="line">  parent<span class="punctuation">:</span> lab</span><br><span class="line">  name<span class="punctuation">:</span> &#x27;default&#x27;</span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h3 id="Allowed-VM-Sizes"><a href="#Allowed-VM-Sizes" class="headerlink" title="Allowed VM Sizes"></a>Allowed VM Sizes</h3><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">resource allowedVmSizesPolicies &#x27;Microsoft.DevTestLab/labs/policysets/policies@<span class="number">2018</span><span class="number">-09</span><span class="number">-15</span>&#x27; = <span class="punctuation">&#123;</span></span><br><span class="line">  name<span class="punctuation">:</span> &#x27;allowedVmSizesPolicy&#x27;</span><br><span class="line">  location<span class="punctuation">:</span> location</span><br><span class="line">  parent<span class="punctuation">:</span> policySetParent</span><br><span class="line">  properties<span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    evaluatorType<span class="punctuation">:</span> &#x27;AllowedValuesPolicy&#x27;</span><br><span class="line">    factName<span class="punctuation">:</span> &#x27;LabVmSize&#x27;</span><br><span class="line">    status<span class="punctuation">:</span> &#x27;Enabled&#x27;</span><br><span class="line">    threshold<span class="punctuation">:</span> &#x27;<span class="punctuation">[</span><span class="string">&quot;Standard_D4_v2&quot;</span><span class="punctuation">,</span><span class="string">&quot;Standard_E4_v2&quot;</span><span class="punctuation">]</span>&#x27;</span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h3 id="Allowed-VM’s-per-user"><a href="#Allowed-VM’s-per-user" class="headerlink" title="Allowed VM’s per user"></a>Allowed VM’s per user</h3><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">resource allowedVmsPerUserPolicies &#x27;Microsoft.DevTestLab/labs/policysets/policies@<span class="number">2018</span><span class="number">-09</span><span class="number">-15</span>&#x27; = <span class="punctuation">&#123;</span></span><br><span class="line">  name<span class="punctuation">:</span> &#x27;allowedVmsPerUserPolicy&#x27;</span><br><span class="line">  location<span class="punctuation">:</span> location</span><br><span class="line">  parent<span class="punctuation">:</span> policySetParent</span><br><span class="line">  properties<span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    evaluatorType<span class="punctuation">:</span> &#x27;MaxValuePolicy&#x27;</span><br><span class="line">    factName<span class="punctuation">:</span> &#x27;UserOwnedLabVmCount&#x27;</span><br><span class="line">    status<span class="punctuation">:</span> &#x27;Enabled&#x27;</span><br><span class="line">    threshold<span class="punctuation">:</span> &#x27;<span class="number">4</span>&#x27;</span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h3 id="Allowed-Premium-SSD-Per-User"><a href="#Allowed-Premium-SSD-Per-User" class="headerlink" title="Allowed Premium SSD Per User"></a>Allowed Premium SSD Per User</h3><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">resource allowedPremiumSSDPerUserPolicies &#x27;Microsoft.DevTestLab/labs/policysets/policies@<span class="number">2018</span><span class="number">-09</span><span class="number">-15</span>&#x27; = <span class="punctuation">&#123;</span></span><br><span class="line">  name<span class="punctuation">:</span> &#x27;allowedPremiumSSDPerUserPolicy&#x27;</span><br><span class="line">  location<span class="punctuation">:</span> location</span><br><span class="line">  parent<span class="punctuation">:</span> policySetParent</span><br><span class="line">  properties<span class="punctuation">:</span> <span class="punctuation">&#123;</span></span><br><span class="line">    evaluatorType<span class="punctuation">:</span> &#x27;MaxValuePolicy&#x27;</span><br><span class="line">    factName<span class="punctuation">:</span> &#x27;UserOwnedLabPremiumVmCount&#x27;</span><br><span class="line">    status<span class="punctuation">:</span> &#x27;Enabled&#x27;</span><br><span class="line">    threshold<span class="punctuation">:</span> &#x27;<span class="number">4</span>&#x27;</span><br><span class="line">  <span class="punctuation">&#125;</span></span><br><span class="line"><span class="punctuation">&#125;</span></span><br></pre></td></tr></table></figure><h2 id="References"><a href="#References" class="headerlink" title="References"></a>References</h2><ul><li>Main &amp;  thumbnail image <a href="https://azure.microsoft.com/">was taken from the Azure site</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🎯 TL;DR: DevTest Labs Policy Configuration with Bicep IaC&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Azure DevTest Labs documentation covers basic lab deployment but lacks policy configuration examples in Bicep. Problem: Missing guidance on linking policies to DevTest Labs using Infrastructure as Code. Solution: Use &lt;code&gt;Microsoft.DevTestLab/labs/policysets&lt;/code&gt; resource with ‘default’ name as parent for policy definitions. Implementation includes VM size restrictions, user VM quotas, and premium SSD limits using evaluator types like &lt;code&gt;AllowedValuesPolicy&lt;/code&gt; and &lt;code&gt;MaxValuePolicy&lt;/code&gt; with proper threshold configurations.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;Azure DevTest Labs offers a powerful cloud-based development workstation environment and great alternative to a local development workstation&amp;#x2F;laptop when it comes to software development. This blog post is not so much talking about the benefits of DevTest Lab, but more about how to create policies for DevTest Labs using Bicep.  Although there is a good support for &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/templates/microsoft.devtestlab/labs?pivots=deployment-language-bicep&quot;&gt;deploying DevTest labs with Bicep&lt;/a&gt;, there is little to no documentation when it comes to creating policies for DevTest Labs in Bicep. In this blog post, we will focus on creating policies for DevTest Labs using Bicep and how to go about doing this.&lt;/p&gt;
&lt;h2 id=&quot;A-Brief-Overview-of-Azure-DevTest-Labs&quot;&gt;&lt;a href=&quot;#A-Brief-Overview-of-Azure-DevTest-Labs&quot; class=&quot;headerlink&quot; title=&quot;A Brief Overview of Azure DevTest Labs&quot;&gt;&lt;/a&gt;A Brief Overview of Azure DevTest Labs&lt;/h2&gt;&lt;p&gt;Azure DevTest Labs is a managed service that enables developers to quickly create, manage, and share development and test environments. It provides a range of features and tools designed to streamline the development process, minimize costs, and improve overall productivity. By leveraging the power of the cloud, developers can easily spin up virtual machines (VMs) pre-configured with the necessary tools, frameworks, and software needed for their projects.&lt;/p&gt;
&lt;h2 id=&quot;Existing-Documentation-Limitations&quot;&gt;&lt;a href=&quot;#Existing-Documentation-Limitations&quot; class=&quot;headerlink&quot; title=&quot;Existing Documentation Limitations&quot;&gt;&lt;/a&gt;Existing Documentation Limitations&lt;/h2&gt;&lt;p&gt;While the existing documentation covers various aspects of Azure DevTest Labs, it lacks clear guidance on setting up policies with DevTest Labs in Bicep. This blog post aims to address that gap by providing a Bicep script for creating a DevTest Lab and applying policies to it. Shout out to my colleague &lt;a href=&quot;https://www.linkedin.com/in/illian-yuan&quot;&gt;Illian Y&lt;/a&gt; for persisting and not giving up and finding a away around undocumented features and showing me.&lt;/p&gt;</summary>
    
    
    
    <category term="Azure" scheme="https://clouddev.blog/categories/Azure/"/>
    
    <category term="DevTest Labs" scheme="https://clouddev.blog/categories/Azure/DevTest-Labs/"/>
    
    
    <category term="Azure" scheme="https://clouddev.blog/tags/Azure/"/>
    
    <category term="Azure Dev Test Labs" scheme="https://clouddev.blog/tags/Azure-Dev-Test-Labs/"/>
    
    <category term="Developer Environments" scheme="https://clouddev.blog/tags/Developer-Environments/"/>
    
    <category term="Azure Policy" scheme="https://clouddev.blog/tags/Azure-Policy/"/>
    
  </entry>
  
  <entry>
    <title>Azure Logic Apps Timeout</title>
    <link href="https://clouddev.blog/Azure/Logic-Apps/azure-logic-apps-timeout/"/>
    <id>https://clouddev.blog/Azure/Logic-Apps/azure-logic-apps-timeout/</id>
    <published>2022-10-19T11:00:00.000Z</published>
    <updated>2025-08-07T05:42:25.051Z</updated>
    
    <content type="html"><![CDATA[<hr><blockquote><p><strong>🎯 TL;DR: Timeout Control Strategies for Azure Logic Apps</strong></p><p>Logic Apps default timeout behavior doesn’t match production requirements with HTTP triggers timing out at 3.9 minutes and workflow duration defaulting to 90 days. Problem: No granular timeout control per workflow causing long-running processes in production. Solutions: Global <code>Runtime.Backend.FlowRunTimeout</code> setting (minimum 7 days, affects all workflows) or per-workflow timeout branches using parallel “Delay” action with terminate condition for precise timeout control without impacting other workflows.</p></blockquote><hr><p>Recently I got pulled into a production incident where a logic app was running for a long time (long time in this scenario was &gt; 10 minutes), but the intention from the dev crew was they wanted this to time out in 60 seconds.  These logic apps were a combination of HTTP triggers and Timer based.</p><h2 id="Logic-App-Default-Time-Limits"><a href="#Logic-App-Default-Time-Limits" class="headerlink" title="Logic App Default Time Limits"></a>Logic App Default Time Limits</h2><p>First things to keep in mind are some default limits.</p><ol><li><p>If its a HTTP based trigger the <a href="https://learn.microsoft.com/en-us/azure/logic-apps/logic-apps-limits-and-config?tabs=consumption,azure-portal#timeout-duration">default timeout is around 3.9 minutes</a></p></li><li><p>For most others the <a href="https://learn.microsoft.com/en-us/azure/logic-apps/edit-app-settings-host-settings?tabs=azure-portal#run-duration-and-history-retention">default max run duration of a logic app is 90 days and min is 7 days</a></p></li></ol><h2 id="Ways-To-Change-Defaults"><a href="#Ways-To-Change-Defaults" class="headerlink" title="Ways To Change Defaults"></a>Ways To Change Defaults</h2><p>With that, here are a couple of quick ways to make sure your Logic App times out and terminates within the time frame you set. Lets say if we want our Logic App to run no more than 60 seconds at max then:</p><span id="more"></span><ol><li>You can change the setting <a href="https://learn.microsoft.com/en-us/azure/logic-apps/edit-app-settings-host-settings?tabs=azure-portal#run-duration-and-history-retention#:~:text=Runtime.Backend.FlowRunTimeout">Runtime.Backend.FlowRunTimeout</a> from the default 90 days to 7 days (keep in mind the minimum for this setting is 7 days which is quite large, refer to this issue : <a href="https://github.com/Azure/logicapps/issues/782#issuecomment-1609008805">https://github.com/Azure/logicapps/issues/782#issuecomment-1609008805</a>)</li></ol><blockquote><ul><li>PRO: This will make sure that the Logic App runs for a maximum of 7 days only (which is quite large)</li><li>CON: However this will apply to all the Logic Apps in the host&#x2F;tenant, meaning if you had 15 logic apps then all 15 will have the 7 day limit</li></ul></blockquote><ol start="2"><li>Have a branch with in the Logic App itself to control the timeout (shown in the below diagram)</li></ol><blockquote><ul><li>PRO: You have full control of timeout per Logic App, so some can have 30 second time outs while others 60 seconds etc</li><li>CON: There will be an extra branch&#x2F;logic in your logic app</li></ul></blockquote><h2 id="Time-Out-Branch-In-Logic-App"><a href="#Time-Out-Branch-In-Logic-App" class="headerlink" title="Time-Out Branch In Logic App"></a>Time-Out Branch In Logic App</h2><p>Below is how a potential timeout out setting in a Logic App could look like.  You create a “Delay” branch and set the desired time limit, in the example below its 2 minutes so if the other flow takes longer than two minutes then the delay will finish, logic app will be terminated and a cancelled status will be returned to the user in the below example.  Shout out to my colleague <a href="https://www.linkedin.com/in/johnbilliris">John B</a> for this awesome idea.</p><p><img src="/Azure/Logic-Apps/azure-logic-apps-timeout/logic-apps-timeout.png" alt=" " title="Single Threaded Container Apps"></p><h2 id="References"><a href="#References" class="headerlink" title="References"></a>References</h2><ul><li>Main image <a href="https://azure.microsoft.com/en-us/products/logic-apps/">was taken from the Azure site</a></li><li>Thumbnail image <a href="https://azure.microsoft.com/svghandler/logic-apps/?width=1280&height=720">was taken from Azure SVG icons</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🎯 TL;DR: Timeout Control Strategies for Azure Logic Apps&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Logic Apps default timeout behavior doesn’t match production requirements with HTTP triggers timing out at 3.9 minutes and workflow duration defaulting to 90 days. Problem: No granular timeout control per workflow causing long-running processes in production. Solutions: Global &lt;code&gt;Runtime.Backend.FlowRunTimeout&lt;/code&gt; setting (minimum 7 days, affects all workflows) or per-workflow timeout branches using parallel “Delay” action with terminate condition for precise timeout control without impacting other workflows.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;Recently I got pulled into a production incident where a logic app was running for a long time (long time in this scenario was &amp;gt; 10 minutes), but the intention from the dev crew was they wanted this to time out in 60 seconds.  These logic apps were a combination of HTTP triggers and Timer based.&lt;/p&gt;
&lt;h2 id=&quot;Logic-App-Default-Time-Limits&quot;&gt;&lt;a href=&quot;#Logic-App-Default-Time-Limits&quot; class=&quot;headerlink&quot; title=&quot;Logic App Default Time Limits&quot;&gt;&lt;/a&gt;Logic App Default Time Limits&lt;/h2&gt;&lt;p&gt;First things to keep in mind are some default limits.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;If its a HTTP based trigger the &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/logic-apps/logic-apps-limits-and-config?tabs=consumption,azure-portal#timeout-duration&quot;&gt;default timeout is around 3.9 minutes&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;For most others the &lt;a href=&quot;https://learn.microsoft.com/en-us/azure/logic-apps/edit-app-settings-host-settings?tabs=azure-portal#run-duration-and-history-retention&quot;&gt;default max run duration of a logic app is 90 days and min is 7 days&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;Ways-To-Change-Defaults&quot;&gt;&lt;a href=&quot;#Ways-To-Change-Defaults&quot; class=&quot;headerlink&quot; title=&quot;Ways To Change Defaults&quot;&gt;&lt;/a&gt;Ways To Change Defaults&lt;/h2&gt;&lt;p&gt;With that, here are a couple of quick ways to make sure your Logic App times out and terminates within the time frame you set. Lets say if we want our Logic App to run no more than 60 seconds at max then:&lt;/p&gt;</summary>
    
    
    
    <category term="Azure" scheme="https://clouddev.blog/categories/Azure/"/>
    
    <category term="Logic Apps" scheme="https://clouddev.blog/categories/Azure/Logic-Apps/"/>
    
    
    <category term="Azure" scheme="https://clouddev.blog/tags/Azure/"/>
    
    <category term="Azure Logic Apps" scheme="https://clouddev.blog/tags/Azure-Logic-Apps/"/>
    
  </entry>
  
  <entry>
    <title>Create A Multi User Experience For Single Threaded Applications Using Azure Container Apps</title>
    <link href="https://clouddev.blog/Azure/Container-Apps/create-a-multi-user-experience-for-single-threaded-applications-using-azure-container-apps/"/>
    <id>https://clouddev.blog/Azure/Container-Apps/create-a-multi-user-experience-for-single-threaded-applications-using-azure-container-apps/</id>
    <published>2022-09-11T12:00:00.000Z</published>
    <updated>2025-08-07T05:42:25.055Z</updated>
    
    <content type="html"><![CDATA[<hr><blockquote><p><strong>🎯 TL;DR: Simulating Multi-User Experience for Legacy Single-Threaded Apps</strong></p><p>Legacy single-threaded applications (one request per process) require multi-user support without costly re-architecture. Problem: Applications with static locks block entire process during request handling. Solution: Azure Container Apps with HTTP-based scaling rules that spawn new container instances per concurrent request. Configuration uses min-replicas&#x3D;0, max-replicas&#x3D;30 with HTTP scale triggers, achieving 70-90% request isolation across separate container instances for pseudo-multithreaded behavior without code changes.</p></blockquote><hr><p>How to make a single-threaded app multi-threaded? This is the scenario I faced very recently. These were legacy web app(s) written to be single-threaded; in this context single-threaded means can only serve one request at a time. <strong>I know this goes against everything that a web app should be</strong>, but it what it is.</p><p>So if we have a single threaded web app (legacy) now all of a sudden we have a requirement to support multiple users at the same time. What are our options:</p><ol><li>Re-architect the app to be multi threaded</li><li>Find a way to simulate multi threaded behavior</li></ol><p>Both are great options, but in this scenario option 1 was out, due to the cost involved in re-writing this app to support multi threading.  So that leaves us with option 2; how can we at a cloud infra level <strong>easily</strong> simulate multi threaded behavior. Turns out if we containerize the app (in this case it was easy enough to do) we orchestrate the app such that for each http request is routed to a new container (ie: every new http request should spin up a new container and request send to it)</p><h2 id="Options-For-Running-Containers"><a href="#Options-For-Running-Containers" class="headerlink" title="Options For Running Containers"></a>Options For Running Containers</h2><p>So when it comes to running a container in Azure our main options are below<br><img src="/Azure/Container-Apps/create-a-multi-user-experience-for-single-threaded-applications-using-azure-container-apps/container-options.png" alt=" " title="Container Options"></p><span id="more"></span><p>Here we need to orchestrate containers, ie: at a minimum for every new http request spin a new one), which means we only have two viable options, Azure Kubernetes Service (AKS) or Azure Container Apps (ACA).  Both are valid options, each with their own pros&#x2F;cons, with AKS its a lot more complex we will need to :</p><blockquote><ul><li>Think of networking</li><li>Think of vm’s&#x2F;vm scale sets for nodes</li><li>Choose ingress controller and set up ingress rules</li><li>Identity</li><li>Plus many more, <a href="https://docs.microsoft.com/en-us/azure/architecture/reference-architectures/containers/aks/baseline-aks#network-topology">here is the baseline reference for AKS</a></li></ul></blockquote><p>So in short, as flexible as AKS is its not as easy as something like ACA which is a fully managed version of AKS that abstracts all the complexities of Kubernetes. So for this scenario to prove we can simulate multi threaded experience lets go ahead with ACA.</p><h2 id="Sample-Single-Threaded-Program"><a href="#Sample-Single-Threaded-Program" class="headerlink" title="Sample Single Threaded Program"></a>Sample Single Threaded Program</h2><p>For this demo below is a simple C# DotNet app that simulates a single threaded behavior, essentially its doing a lock on a static variable which blocks the whole process for 6 seconds. So when we visit the &#x2F;test endpoint we lock the whole app.</p><figure class="highlight csharp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title">Program</span></span><br><span class="line">&#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">readonly</span> <span class="built_in">object</span> LockObject = <span class="keyword">new</span>();</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">Main</span>(<span class="params"><span class="built_in">string</span>[] args</span>)</span></span><br><span class="line">    &#123;</span><br><span class="line">        <span class="keyword">var</span> builder = WebApplication.CreateBuilder(args);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Add services to the container.</span></span><br><span class="line">        builder.Services.AddAuthorization();</span><br><span class="line"></span><br><span class="line">        builder.Services.AddEndpointsApiExplorer();</span><br><span class="line">        builder.Services.AddSwaggerGen();</span><br><span class="line"></span><br><span class="line">        builder.Services.AddApplicationInsightsTelemetry();</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">        <span class="keyword">var</span> app = builder.Build();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Configure the HTTP request pipeline.</span></span><br><span class="line">        <span class="keyword">if</span> (app.Environment.IsDevelopment())</span><br><span class="line">        &#123;</span><br><span class="line">            app.UseSwagger();</span><br><span class="line">            app.UseSwaggerUI();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        app.UseAuthorization();</span><br><span class="line"></span><br><span class="line">        app.MapGet(<span class="string">&quot;/test&quot;</span>, (HttpContext httpContext) =&gt;</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="keyword">if</span> (Monitor.TryEnter(LockObject, <span class="keyword">new</span> TimeSpan(<span class="number">0</span>, <span class="number">0</span>, <span class="number">6</span>)))</span><br><span class="line">            &#123;</span><br><span class="line">                <span class="keyword">try</span></span><br><span class="line">                &#123;</span><br><span class="line">                    Thread.Sleep(<span class="number">5000</span>);</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="keyword">finally</span></span><br><span class="line">                &#123;</span><br><span class="line">                    Monitor.Exit(LockObject);</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;</span><br><span class="line"></span><br><span class="line">            <span class="keyword">return</span> (<span class="string">&quot;Hello From Container: &quot;</span> + System.Environment.MachineName);</span><br><span class="line">        &#125;);</span><br><span class="line"></span><br><span class="line">        app.Run();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="Azure-Container-Apps"><a href="#Azure-Container-Apps" class="headerlink" title="Azure Container Apps"></a>Azure Container Apps</h2><p>For this demo the easiest way to create the Azure Container Apps environment is through Visual Studio, you right click, publish and go through the menus and in the end VS will create a Container Apps Environment and deploy the code as a container to ACA.<br><img src="/Azure/Container-Apps/create-a-multi-user-experience-for-single-threaded-applications-using-azure-container-apps/azure-container-app-create.png" alt=" " title="Single Threaded Container Apps"></p><p>Once this is all done, we should have a resource group like below<br><img src="/Azure/Container-Apps/create-a-multi-user-experience-for-single-threaded-applications-using-azure-container-apps/container-apps-resource-group.png" alt=" " title="Container Apps Resource Group"></p><h2 id="Azure-Container-Apps-Scaling"><a href="#Azure-Container-Apps-Scaling" class="headerlink" title="Azure Container Apps Scaling"></a>Azure Container Apps Scaling</h2><p>Next we go to the container app (the single threaded api we just deployed) and set up a simple http scale rule that will spin up a new container for every 1 http incoming request.  In the example below we set min-replicas to 0 and max to 30 this means that when there is no traffic it will scale down to 0 and at peak it will hit 30.<br><img src="/Azure/Container-Apps/create-a-multi-user-experience-for-single-threaded-applications-using-azure-container-apps/container-options.png" alt=" " title="Container Apps Resource Group"></p><h2 id="Testing"><a href="#Testing" class="headerlink" title="Testing"></a>Testing</h2><p>Now go to the url of the container app and hit it simultaneously in browser tabs, when I opened it in multiple browser tabs out of 10 tabs about 7 were served by unique containers and based on the test code above I see it being served by different container ids</p><figure class="highlight json"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Tab1<span class="punctuation">:</span> Hello From Container<span class="punctuation">:</span> single-threaded-api-app<span class="number">-20220731</span>--ps4yjjp<span class="number">-66</span>f4885b65-w5s6h</span><br><span class="line">Tab2<span class="punctuation">:</span> Hello From Container<span class="punctuation">:</span> single-threaded-api-app<span class="number">-20220731</span>--ps4yjjp<span class="number">-66</span>f4885b65-gs8qf</span><br><span class="line">Tab3<span class="punctuation">:</span> Hello From Container<span class="punctuation">:</span> single-threaded-api-app<span class="number">-20220731</span>--ps4yjjp<span class="number">-66</span>f4885b65-x7grl</span><br><span class="line">etc</span><br></pre></td></tr></table></figure><p>So its not 100% every single request goes to a brand new container, but very easily and very quickly with out too much complexity we were able to achieve a 70 - 90% of requests being served with new containers, so in essence we found a quick way to simulate a pseudo - multi threaded experience for our legacy single threaded app with out too much effort.</p>]]></content>
    
    
    <summary type="html">&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🎯 TL;DR: Simulating Multi-User Experience for Legacy Single-Threaded Apps&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Legacy single-threaded applications (one request per process) require multi-user support without costly re-architecture. Problem: Applications with static locks block entire process during request handling. Solution: Azure Container Apps with HTTP-based scaling rules that spawn new container instances per concurrent request. Configuration uses min-replicas&amp;#x3D;0, max-replicas&amp;#x3D;30 with HTTP scale triggers, achieving 70-90% request isolation across separate container instances for pseudo-multithreaded behavior without code changes.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;How to make a single-threaded app multi-threaded? This is the scenario I faced very recently. These were legacy web app(s) written to be single-threaded; in this context single-threaded means can only serve one request at a time. &lt;strong&gt;I know this goes against everything that a web app should be&lt;/strong&gt;, but it what it is.&lt;/p&gt;
&lt;p&gt;So if we have a single threaded web app (legacy) now all of a sudden we have a requirement to support multiple users at the same time. What are our options:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Re-architect the app to be multi threaded&lt;/li&gt;
&lt;li&gt;Find a way to simulate multi threaded behavior&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Both are great options, but in this scenario option 1 was out, due to the cost involved in re-writing this app to support multi threading.  So that leaves us with option 2; how can we at a cloud infra level &lt;strong&gt;easily&lt;/strong&gt; simulate multi threaded behavior. Turns out if we containerize the app (in this case it was easy enough to do) we orchestrate the app such that for each http request is routed to a new container (ie: every new http request should spin up a new container and request send to it)&lt;/p&gt;
&lt;h2 id=&quot;Options-For-Running-Containers&quot;&gt;&lt;a href=&quot;#Options-For-Running-Containers&quot; class=&quot;headerlink&quot; title=&quot;Options For Running Containers&quot;&gt;&lt;/a&gt;Options For Running Containers&lt;/h2&gt;&lt;p&gt;So when it comes to running a container in Azure our main options are below&lt;br&gt;&lt;img src=&quot;/Azure/Container-Apps/create-a-multi-user-experience-for-single-threaded-applications-using-azure-container-apps/container-options.png&quot; alt=&quot; &quot; title=&quot;Container Options&quot;&gt;&lt;/p&gt;</summary>
    
    
    
    <category term="Azure" scheme="https://clouddev.blog/categories/Azure/"/>
    
    <category term="Container Apps" scheme="https://clouddev.blog/categories/Azure/Container-Apps/"/>
    
    
    <category term="Azure" scheme="https://clouddev.blog/tags/Azure/"/>
    
    <category term="Azure Container Apps" scheme="https://clouddev.blog/tags/Azure-Container-Apps/"/>
    
    <category term="Containers" scheme="https://clouddev.blog/tags/Containers/"/>
    
    <category term="Docker" scheme="https://clouddev.blog/tags/Docker/"/>
    
    <category term="DotNet" scheme="https://clouddev.blog/tags/DotNet/"/>
    
    <category term="Single Threaded Apps" scheme="https://clouddev.blog/tags/Single-Threaded-Apps/"/>
    
  </entry>
  
  <entry>
    <title>Application Gateway Ingress Controller For AKS</title>
    <link href="https://clouddev.blog/AKS/AGIC/application-gateway-ingress-controller-for-aks/"/>
    <id>https://clouddev.blog/AKS/AGIC/application-gateway-ingress-controller-for-aks/</id>
    <published>2022-08-19T12:00:00.000Z</published>
    <updated>2025-08-07T05:42:25.054Z</updated>
    
    <content type="html"><![CDATA[<hr><blockquote><p><strong>🎯 TL;DR: AGIC Direct Pod Ingress for High-Performance AKS Workloads</strong></p><p>AGIC provides direct pod ingress bypassing Kubernetes ClusterIP for up to 50% lower network latency compared to in-cluster solutions. Problem: Traditional ingress controllers add network hops and consume AKS compute resources. Solution: Application Gateway routes directly to pod IPs via Azure Resource Manager integration, offering WAF, SSL termination, and managed updates as AKS add-on. Critical limitation: 100 backend pool limit means 2000+ services require 20 Application Gateways, making cost-effective deployment challenging for large-scale clusters.</p></blockquote><hr><p>Recently I ran into an interesting issue with an AKS cluster running 2000+ services.  There is nothing wrong in running 2000+ services that’s what Kubernetes is there for, scale!  but the interesting aspect that caught my attention was trying to get the Applicaiton Gateway Ingress Controller (AGIC) to ingress to all these services. I had worked with Istio and NGINX for ingress into AKS with no issues and never AGIC, so I had to try this to see where it worked well, what the advantages are and where the limitations are.</p><h2 id="Application-Gateway"><a href="#Application-Gateway" class="headerlink" title="Application Gateway"></a>Application Gateway</h2><p>Application Gateway (App Gateway) is a well-established layer 7 service that has been around for a while, some of the major features are:</p><ul><li>URL routing</li><li>Cookie-based affinity</li><li>SSL termination</li><li>End-to-end SSL</li><li>Support for public, private, and hybrid web sites</li><li>Integrated web application firewall</li><li>Zone redundancy</li><li>Connection draining</li></ul><p>This post isn’t focused on the App Gateway itself, it’s more about how and what it can do as an ingress controller for AKS. <a href="https://docs.microsoft.com/en-us/azure/application-gateway/features">You can find out more about App Gateway and all abouts its features here</a></p><span id="more"></span><h2 id="TLDR"><a href="#TLDR" class="headerlink" title="TLDR;"></a>TLDR;</h2><h3 id="Benefits-of-AGIC"><a href="#Benefits-of-AGIC" class="headerlink" title="Benefits of AGIC"></a>Benefits of AGIC</h3><blockquote><ul><li>Direct connection to the pods without an extra hop, <a href="https://azure.microsoft.com/en-au/blog/application-gateway-ingress-controller-for-azure-kubernetes-service/#:~:text=Solution%20performance">this results in a performance benefit up to 50% lower network latency compared to in-cluster ingress</a></li><li>Could make a huge difference in performance and latency sensitive applications and workloads</li><li>If going the AKS add-on route then it becomes fully managed and updated</li><li>In cluster ingress consumes and competes for AKS compute&#x2F;memory resources where was with App Gateway separated from the cluster it won’t be leeching any of the AKS compute</li><li>Full benefits of the Application Gateway such as WAF, cookie-based affinity, ssl termination amongst many others</li></ul></blockquote><h4 id="Limitations"><a href="#Limitations" class="headerlink" title="Limitations"></a>Limitations</h4><blockquote><ul><li><a href="https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#application-gateway-limits">Application Gateway has some backend limits.  Backend pools are limited to 100.</a></li><li><a href="https://azure.microsoft.com/en-us/pricing/details/application-gateway/#pricing">Application Gateway does have a pricing implication</a></li><li>Routing is directly to pod IP’s rather than the ClusterIP of the service.  <a href="https://github.com/Azure/application-gateway-kubernetes-ingress/issues/1427">There is a feature request open for this</a></li></ul></blockquote><h3 id="Application-Gateway-Ingress-Controller-AGIC"><a href="#Application-Gateway-Ingress-Controller-AGIC" class="headerlink" title="Application Gateway Ingress Controller (AGIC)"></a>Application Gateway Ingress Controller (AGIC)</h3><p>AGIC went to GA around the end of 2019 and offered the possibilities of hooking up an App Gateway as an attractive alternative for ingress into an AKS cluster.  Before moving any further with AGIC, we need to understand at a high-level how networking works in AKS.</p><p>There are two main network models:</p><ol><li><p>Kubenet networking</p><blockquote><ul><li>Default option for Kubernetes out of the box</li><li>Each Node receives an IP from the Azure virtual network subnet</li><li>Pods in the node are not associated to the Azure vnet, they are assigned an IP address from the <em>PodIPCidr</em> and a route table is created by AKS</li></ul></blockquote></li><li><p>Azure Container Networking Interface networking (CNI)</p><blockquote><ul><li>Each pod itself receives an IPaddress from the Azure virtual network subnet</li><li>Pods can be directly reached via their private IP from connected networks</li><li>Pods can access resources in the vnet directly with out issues (e.g.: function app in the same vnet)</li></ul></blockquote></li></ol><p>It’s important to note, once you create an AKS cluster with a given network model you can’t change it; you will have to create a new one. <a href="https://docs.microsoft.com/en-us/azure/aks/concepts-network#compare-network-models">There are advantages and disadvantages in both models which are listed in detail in this link</a>.</p><p>One key consideration to highlight is:</p><ul><li>Kubenet - &#x2F;24 IP range can support up to 251 nodes (each subnet reserves the first 3 IP addresses for management operations).  Given the maximum nodes per pod in Kubenet is 110, this configuration can support a maximum of 251 * 110 &#x3D; 27,610 pods</li><li>CNI - the same &#x2F;24 IP range can support a maximum of 8 nodes (CNI has a max of thirty pods per node). So, this configuration can support a maximum of 240</li></ul><p>When it comes to CNI you will have to plan for the IP addresses, you might need to a &#x2F;16 range to get a bigger node count.  <a href="https://docs.microsoft.com/en-us/azure/aks/configure-kubenet#limitations--considerations-for-kubenet">There are also limitations with the kubenet that will need to be taken into consideration</a>.</p><p>With the AKS networking models out of the way, let’s look at AGIC; regardless of which model is chosen, the goal for AGIC is to ingress directly to the pod, a simple representation of this can be seen below.  AGIC when deployed, runs in a pod in the AKS cluster and watches for changes, when changes are detected (i.e.: a new pod has been added or existing pod removed) these IP changes are propagated to the App Gateway via the Azure Resource Manager.</p><div class="mxgraph-container">    <div class="mxgraph" style="max-width:100%;border:1px solid transparent;" data-mxgraph="{&quot;highlight&quot;:&quot;#0000ff&quot;,&quot;lightbox&quot;:false,&quot;nav&quot;:true,&quot;resize&quot;:false,&quot;page&quot;:0,&quot;toolbar&quot;:&quot;lightbox zoom layers pages&quot;,&quot;url&quot;:&quot;https://raw.githubusercontent.com/Ricky-G/draw-io/main/AGIC-Ingress-AKS.drawio&quot;}"></div></div><p>If we went with the CNI networking model, then the pod would get IP address from the vnet and there would be a mapping in the App Gateway.  Alternatively, with the Kubenet model <a href="https://azure.github.io/application-gateway-kubernetes-ingress/how-tos/networking/#with-kubenet">this is how App Gateway will be setup</a>, it will try to assign the same routable created by AKS to App Gateway’s subnet.</p><p>It’s important to note, whichever model you choose the App Gateway will always connect directly to the pod and this is by design.</p><h2 id="Deploying-AGIC"><a href="#Deploying-AGIC" class="headerlink" title="Deploying AGIC"></a>Deploying AGIC</h2><p>AGIC can be deployed in two ways <a href="https://docs.microsoft.com/en-us/azure/application-gateway/ingress-controller-overview#difference-between-helm-deployment-and-AKS-add-on">either using Helm or as an AKS add-on</a>. Each has their pros and cons, the key benefit of going via an AKS add-on will be that it will be fully managed and auto updated by Azure (i.e.: all updates, patching etc. for the AGIC will be taken care of automatically) whereas with Helm you will have to do that yourself.</p><p>Let’s go ahead and deploy a demo AKS cluster with AGIC and see it in action to understand exactly what is going on. For the sake of simplicity, this demo will be creating an AKS cluster with CNI networking model and deploying the AGIC as and AKS add-on.</p><h3 id="Create-an-AKS-cluster"><a href="#Create-an-AKS-cluster" class="headerlink" title="Create an AKS cluster"></a>Create an AKS cluster</h3><p><strong>Login and set the right subscription</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">az login</span><br><span class="line">az account <span class="built_in">set</span> -s <span class="string">&quot;your-subcription-id&quot;</span></span><br></pre></td></tr></table></figure><p><strong>Create a new resource group</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">az group create --name agicTestResourceGroup --location eastus</span><br></pre></td></tr></table></figure><p>Here we are creating a new AKS cluster with CNI networking model (–network-plugin azure) and we are setting up App Gateway as ingress and in this instance we are saying our App Gateway’s name is “testAppGateway” which doesn’t exist and will be created for us</p><p><strong>Create AKS cluster</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">az aks create -n agicTestCluster -g agicTestResourceGroup --network-plugin azure --enable-managed-identity -a ingress-appgw --appgw-name testAppGateway --appgw-subnet-cidr <span class="string">&quot;10.225.0.0/16&quot;</span> --generate-ssh-keys</span><br></pre></td></tr></table></figure><p>If we go into the Azure Portal, we can see two resource groups (one of them is what we created and this where the Azure managed AKS control plane is), the other resource group (MC_agicTestResourceGroup_agicTestCluster_eastus) is where the node pool, vnet, App Gateway etc all live, this resource group gets created automatically for us as part of the <em>az aks create</em> command.</p><p><img src="/AKS/AGIC/application-gateway-ingress-controller-for-aks/aks-resource-group.png" alt=" " title="AKS Resource Group"></p><p><img src="/AKS/AGIC/application-gateway-ingress-controller-for-aks/app-gateway-resource-group.png" alt=" " title="App Gateway Resource Group"></p><h2 id="Deploy-a-sample-API"><a href="#Deploy-a-sample-API" class="headerlink" title="Deploy a sample API"></a>Deploy a sample API</h2><p>Now we have the AKS cluster up and running with AGIC deployed as an add-on, let’s deploy a sample API app and set ingress through the App Gateway.</p><p><strong>Get credentials to the AKS cluster</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">az aks get-credentials -n agicTestCluster -g agicTestResourceGroup</span><br></pre></td></tr></table></figure><p><strong>Deploy a sample API</strong></p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">kubectl apply -f https://gist.githubusercontent.com/Ricky-G/59eb109913bd45d3e9229f9cf0a97edc/raw/b336047feecd9fd89fbe1a9627ac385b525124fe/sample-api-aks-deployment.yaml</span><br></pre></td></tr></table></figure><p>The above sample API deployment yaml was taken from the <a href="https://github.com/Azure/application-gateway-kubernetes-ingress/blob/master/docs/examples/aspnetapp.yaml">AGIC GitHub repo</a>, the only change made to it was added a minimum of 10 replicas.  We are saying we need 10 pods running this API. As soon as you run this you should see the app deployed as a service and 10 pods running successfully and there is a cluster-IPIP set for this (cluster-IP is an IP load balancer that Kubernetes creates, we just need to call this IP and our traffic will be forwarded to one of the 10 pods)</p><p><img src="/AKS/AGIC/application-gateway-ingress-controller-for-aks/sample-api-sevice.png" alt=" " title="Service Deployed to AKS"></p><p>Now if we go to the resource group where we have the actual Application Gateway and go to backend pool, we can see there is one here created by AGIC and if we dig into the pool all the IP addresses of the 10 pods are listed here.  So, we have direct ingress to the pods from the Application Gateway.</p><p><img src="/AKS/AGIC/application-gateway-ingress-controller-for-aks/app-gateway-backend-pool.png" alt=" " title="Application Gateway Backend Pool"></p><p>Finally, if we run the below command, we should see an ingress IP address for “aspnetapp” which is our sample API.  This is the public IP of the Application Gateway, which has been wired up to ingress all the way to the pod.  If we paste this IP into the browser, we can see sample aspnet site served from the pod.</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">kubectl get ingress</span><br></pre></td></tr></table></figure><p>Right, so we have successfully ingressed all the way from public ip going via Application Gateway all the way to our pod.</p><h2 id="Benefits-of-AGIC-1"><a href="#Benefits-of-AGIC-1" class="headerlink" title="Benefits of AGIC"></a>Benefits of AGIC</h2><ul><li>Direct connection to the pods without an extra hop, <a href="https://azure.microsoft.com/en-au/blog/application-gateway-ingress-controller-for-azure-kubernetes-service/#:~:text=Solution%20performance">this results in a performance benefit up to 50% lower network latency compared to in-cluster ingress</a></li><li>Could make a huge difference in performance and latency sensitive applications and workloads</li><li>If going the AKS add-on route then it becomes fully managed and updated</li><li>In cluster ingress consumes and competes for AKS compute&#x2F;memory resources where was with App Gateway separated from the cluster it won’t be leeching any of the AKS compute</li><li>Full benefits of the Application Gateway such as WAF, cookie-based affinity, ssl termination amongst many others</li></ul><h2 id="Limitations-1"><a href="#Limitations-1" class="headerlink" title="Limitations"></a>Limitations</h2><ul><li><a href="https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/azure-subscription-service-limits#application-gateway-limits">Application Gateway has some backend limits. Backend pools are limited to 100.</a></li><li><a href="https://azure.microsoft.com/en-us/pricing/details/application-gateway/#pricing">Application Gateway does have a pricing implication</a></li><li>Routing is directly to pod IP’s rather than the ClusterIP of the service.  <a href="https://github.com/Azure/application-gateway-kubernetes-ingress/issues/1427">There is a feature request open for this</a></li></ul><h2 id="Closing-Thoughts"><a href="#Closing-Thoughts" class="headerlink" title="Closing Thoughts"></a>Closing Thoughts</h2><p>Key thing to keep in mind is the backend pool limitation of 100 .  If you have more than 100 “ingres-able” services, then you would need multiple Application Gateway’s to cater for this.  Although it is a supported scenario and straightforward to set up multiple App Gateways for one AKS cluster, your costs will pile up.</p><p>At the start of this post, I mentioned a scenario of 2000+ services, in this case we would need 20 App Gateways; 2000 services &#x2F; 100 &#x3D; 20. Due to cost implications this won’t be palatable in most cases.</p><p>On the plus side you get direct connection to the pod and can shave 50% of network latency. So, in this 2000+ services in one cluster scenario we could put the App Gateway as ingress for just latency sensitive apps&#x2F;API’s and use another traditional in cluster-based ingress for all the other services.  This way you get the best of both words while still keeping below the App Gateway max backend pool limits.</p><p>One neat option for an in cluster-based ingress could be <a href="https://docs.microsoft.com/en-us/azure/aks/web-app-routing">Web Application Routing</a>, which is still in preview at the time of writing this.  It’s a managed NGINX based solution that should work well as an in cluster-based ingress controller</p><h2 id="References"><a href="#References" class="headerlink" title="References"></a>References</h2><ul><li><a href="https://azure.microsoft.com/en-au/blog/application-gateway-ingress-controller-for-azure-kubernetes-service/">AGIC main documentation</a></li><li><a href="https://azure.github.io/application-gateway-kubernetes-ingress/">AGIC GitHub</a></li><li>Main image <a href="https://azure.microsoft.com/svghandler/application-gateway">was taken from the Azure site</a> and slightly modified</li></ul>]]></content>
    
    
    <summary type="html">&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🎯 TL;DR: AGIC Direct Pod Ingress for High-Performance AKS Workloads&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;AGIC provides direct pod ingress bypassing Kubernetes ClusterIP for up to 50% lower network latency compared to in-cluster solutions. Problem: Traditional ingress controllers add network hops and consume AKS compute resources. Solution: Application Gateway routes directly to pod IPs via Azure Resource Manager integration, offering WAF, SSL termination, and managed updates as AKS add-on. Critical limitation: 100 backend pool limit means 2000+ services require 20 Application Gateways, making cost-effective deployment challenging for large-scale clusters.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;Recently I ran into an interesting issue with an AKS cluster running 2000+ services.  There is nothing wrong in running 2000+ services that’s what Kubernetes is there for, scale!  but the interesting aspect that caught my attention was trying to get the Applicaiton Gateway Ingress Controller (AGIC) to ingress to all these services. I had worked with Istio and NGINX for ingress into AKS with no issues and never AGIC, so I had to try this to see where it worked well, what the advantages are and where the limitations are.&lt;/p&gt;
&lt;h2 id=&quot;Application-Gateway&quot;&gt;&lt;a href=&quot;#Application-Gateway&quot; class=&quot;headerlink&quot; title=&quot;Application Gateway&quot;&gt;&lt;/a&gt;Application Gateway&lt;/h2&gt;&lt;p&gt;Application Gateway (App Gateway) is a well-established layer 7 service that has been around for a while, some of the major features are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;URL routing&lt;/li&gt;
&lt;li&gt;Cookie-based affinity&lt;/li&gt;
&lt;li&gt;SSL termination&lt;/li&gt;
&lt;li&gt;End-to-end SSL&lt;/li&gt;
&lt;li&gt;Support for public, private, and hybrid web sites&lt;/li&gt;
&lt;li&gt;Integrated web application firewall&lt;/li&gt;
&lt;li&gt;Zone redundancy&lt;/li&gt;
&lt;li&gt;Connection draining&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This post isn’t focused on the App Gateway itself, it’s more about how and what it can do as an ingress controller for AKS. &lt;a href=&quot;https://docs.microsoft.com/en-us/azure/application-gateway/features&quot;&gt;You can find out more about App Gateway and all abouts its features here&lt;/a&gt;&lt;/p&gt;</summary>
    
    
    
    <category term="AKS" scheme="https://clouddev.blog/categories/AKS/"/>
    
    <category term="AGIC" scheme="https://clouddev.blog/categories/AKS/AGIC/"/>
    
    
    <category term="Azure" scheme="https://clouddev.blog/tags/Azure/"/>
    
    <category term="Kubernetes" scheme="https://clouddev.blog/tags/Kubernetes/"/>
    
    <category term="AKS" scheme="https://clouddev.blog/tags/AKS/"/>
    
    <category term="Ingress" scheme="https://clouddev.blog/tags/Ingress/"/>
    
    <category term="AGIC" scheme="https://clouddev.blog/tags/AGIC/"/>
    
    <category term="Application Gateway" scheme="https://clouddev.blog/tags/Application-Gateway/"/>
    
  </entry>
  
  <entry>
    <title>Deploying To IP Restricted Azure Function Apps Using GitHub Actions</title>
    <link href="https://clouddev.blog/GitHub/Actions/deploying-to-ip-restricted-azure-function-apps-using-github-actions/"/>
    <id>https://clouddev.blog/GitHub/Actions/deploying-to-ip-restricted-azure-function-apps-using-github-actions/</id>
    <published>2022-08-06T12:00:00.000Z</published>
    <updated>2025-08-07T05:42:25.054Z</updated>
    
    <content type="html"><![CDATA[<hr><blockquote><p><strong>🎯 TL;DR: Dynamic IP Management for CI&#x2F;CD to Secured Azure Functions</strong></p><p>IP-restricted Function Apps block GitHub Actions runners causing HTTP 403 deployment failures since runners use dynamic IP addresses. Problem: Cannot whitelist entire GitHub IP range due to frequent changes. Solution: Dynamic IP management in GitHub Actions workflow using Azure CLI to temporarily add runner IP to SCM site access restrictions, deploy code, then remove IP. Implementation uses <code>ipify</code> API for IP detection, <code>--use-same-restrictions-for-scm-site false</code> for SCM isolation, and automated cleanup to maintain security posture.</p></blockquote><hr><a href="/Azure/Function-Apps/Security/securing-azure-functions-and-logic-apps/" title="Securing Azure Functions and Logic Apps">In the previous post we blocked our function app to be available only to the APIM via ip restrictions</a>. <p>This secures our function app and it isn’t available publicly, any one that tries to access our function app url will get “HTTP 403 Forbidden”.</p><p>This secures our function app; now what about deploying code changes to the function app via GitHub Actions? we should be able to CI&#x2F;CD to our function app, but there is a problem here. The GitHub action will fail with the same “HTTP 403 Forbidden”, this is because GitHub actions run on runners (its a hosted virtual environment), each time we run the Action we get a new runner and it can have a different ip address.  So how can we get around this? <a href="https://api.github.com/meta">do we white list the entire GitHub ip range?</a></p><p>GitHub’s ip ranges can change any time, so will have to keep scanning for changes to these ranges and proactively update our ip restrictions, this is not very scalable or practical. So what are other ways of getting around this? we have a couple of ways to get around this.</p><h2 id="Possible-Solutions"><a href="#Possible-Solutions" class="headerlink" title="Possible Solutions"></a>Possible Solutions</h2><p>There are two viable solutions here</p><span id="more"></span><h3 id="1-Use-a-self-hosted-runner"><a href="#1-Use-a-self-hosted-runner" class="headerlink" title="1. Use a self-hosted runner"></a>1. Use a self-hosted runner</h3><blockquote><p>Where you bring your own VM’s with static ip’s and whitelist these static ip’s</p></blockquote><p><strong>Pros:</strong></p><ul><li>Full control over your devops agents</li><li>Can optimize&#x2F;reuse these agents for various CI&#x2F;CD workloads for your cloud and on-prem deployments</li></ul><p><strong>Cons:</strong></p><ul><li>You have to provision and maintain your own VM’s, there will be time and effort required for this</li><li>Extra costs to maintain your own VM(s), although this could be optimized by turning them off after hours etc</li><li>You miss out on the free GitHub Action minutes you get</li><li>Extra work of provisioning VM’s, installing all the tooling for builds, maintaining and paying for them</li></ul><h3 id="2-Do-some-extra-steps-in-the-existing-GitHub-Actions"><a href="#2-Do-some-extra-steps-in-the-existing-GitHub-Actions" class="headerlink" title="2. Do some extra steps in the existing GitHub Actions"></a>2. Do some extra steps in the existing GitHub Actions</h3><blockquote><ol><li>Use the Azure CLI</li><li>Do an az login</li><li>Grab the public ip of the GitHub runner, you could use a simple public api like the <a href="https://api.ipify.org/">ipify api</a> to grab the public ip of the Github Runner</li><li>Use az cli to update ip restriction to add this additional ip</li><li>Do-your-normal-Deployment</li><li>Use az cli to remove the ip added in step 4</li></ol></blockquote><p><strong>Pros:</strong></p><ul><li>You use the same GitHub runner and workflow</li><li>No effort in provisioning or maintaining extra virtual machines yourself</li><li>Little bit of extra code is all that is needed</li></ul><p><strong>Cons:</strong></p><ul><li>There is a possibility that the GitHub action runner fails&#x2F;crashes after doing step 4 but before it had a chance to get to step 5, you could be left with an extra ip address white listed in your app until you run the workflow again.</li></ul><p>This post is all about how to go about doing option 2 (do some extra steps in the existing GitHub Actions), although there is one con (ie: the GitHub runner crashing during step 5 and leaving an ip address of the runner there), in my view this is a very small risk.  The chances of a crash precisely at that point are low and even it does happen the risk of having the runner ip (only 1 extra ip) for a short duration until your next run happens is very low.</p><h2 id="Show-me-the-code"><a href="#Show-me-the-code" class="headerlink" title="Show me the code"></a>Show me the code</h2><p>If you want to skip and just get to the code:</p><ul><li><a href="https://github.com/Ricky-G/github-cicd-samples/tree/main/functionapp">Here is the sample hello world function app (written in .net 6)</a></li><li><a href="https://github.com/Ricky-G/github-cicd-samples/blob/main/.github/workflows/azure-function-app-deploy.yml">Here is the GitHub Action that is deploying to ip restricted app</a></li></ul><p>In the above GitHub Action it is deploying a hello world function app; it is doing a dotnet build, package and deploy.  Those are all the standard bits of deploying a function app; lets go over the interesting bits</p><ol><li>Getting the GitHub Runners public ip</li><li>Whitelisting this ip</li><li>After a successful deploy of our app, we remove the ip added in step 2</li></ol><blockquote><p>For the first step we are using a public package <a href="https://github.com/marketplace/actions/public-ip">haythem&#x2F;public-ip@v1.2</a> to get the ip.  We can also manually do a curl our &gt;selves to the <a href="https://api.ipify.org/">ipify api</a> and grab the public ip ourselves. For the purposes of this demo we will use this package.</p></blockquote><p><strong>Step 1 - getting the GitHub runners public ip</strong></p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">Public</span> <span class="string">IP</span></span><br><span class="line">  <span class="attr">id:</span> <span class="string">ip</span></span><br><span class="line">  <span class="attr">uses:</span> <span class="string">haythem/public-ip@v1.2</span></span><br></pre></td></tr></table></figure><blockquote><ul><li>Next for the second step we use the az cli to add the ip address.</li><li>First we use az webapp config to set the –use-same-restrictions-for-scm-site false, here we are saying don’t apply the same restriction as the main site to the scm site</li><li>Our main site is still safe with the right ip restrictions, our scm site is now ready for changes</li><li>Next we use az functionapp config access-restriction to add the GitHub runner ip to just the scm site</li></ul></blockquote><p><strong>Step 2 - white listing the GitHub runner’s public ip</strong></p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">&#x27;Allow Github Runner IpAddress&#x27;</span></span><br><span class="line">  <span class="attr">uses:</span> <span class="string">azure/CLI@v1</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="attr">azcliversion:</span> <span class="number">2.37</span><span class="number">.0</span></span><br><span class="line">    <span class="attr">inlineScript:</span> <span class="string">|</span></span><br><span class="line"><span class="string">        az webapp config access-restriction set -g $ -n func-app-iprest-demo --use-same-restrictions-for-scm-site false</span></span><br><span class="line"><span class="string">        az functionapp config access-restriction add -g $ -n func-app-iprest-demo --rule-name github_runner --action Allow --ip-address $ --priority 100 --scm-site true</span></span><br></pre></td></tr></table></figure><blockquote><p>Finally we remove the ip address we added from the previous step and set the scm site access the same as our main site</p></blockquote><p><strong>Step 3 - after successful deploy, remove the GitHub runner’s public ip</strong></p><figure class="highlight yaml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="bullet">-</span> <span class="attr">name:</span> <span class="string">&#x27;Remove Github Runner IpAddress&#x27;</span></span><br><span class="line">  <span class="attr">uses:</span> <span class="string">azure/CLI@v1</span></span><br><span class="line">  <span class="attr">with:</span></span><br><span class="line">    <span class="attr">azcliversion:</span> <span class="number">2.37</span><span class="number">.0</span></span><br><span class="line">    <span class="attr">inlineScript:</span> <span class="string">|</span></span><br><span class="line"><span class="string">        az functionapp config access-restriction remove -g $ -n func-app-iprest-demo --rule-name github_runner --scm-site true</span></span><br><span class="line"><span class="string">        az webapp config access-restriction set -g $ -n func-app-iprest-demo --use-same-restrictions-for-scm-site true</span></span><br></pre></td></tr></table></figure><p>Finally 👏! we can now deploy using GitHub Actions to ip restricted function apps 🙌.</p><h2 id="References"><a href="#References" class="headerlink" title="References"></a>References</h2><p>As always a big thank you to <a href="https://unsplash.com/">Unsplash</a> for providing a huge range of images for free</p><ul><li>Cover image has been taken from <a href="https://unsplash.com/photos/842ofHC6MaI">https://unsplash.com/photos/842ofHC6MaI</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🎯 TL;DR: Dynamic IP Management for CI&amp;#x2F;CD to Secured Azure Functions&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;IP-restricted Function Apps block GitHub Actions runners causing HTTP 403 deployment failures since runners use dynamic IP addresses. Problem: Cannot whitelist entire GitHub IP range due to frequent changes. Solution: Dynamic IP management in GitHub Actions workflow using Azure CLI to temporarily add runner IP to SCM site access restrictions, deploy code, then remove IP. Implementation uses &lt;code&gt;ipify&lt;/code&gt; API for IP detection, &lt;code&gt;--use-same-restrictions-for-scm-site false&lt;/code&gt; for SCM isolation, and automated cleanup to maintain security posture.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;a href=&quot;/Azure/Function-Apps/Security/securing-azure-functions-and-logic-apps/&quot; title=&quot;Securing Azure Functions and Logic Apps&quot;&gt;In the previous post we blocked our function app to be available only to the APIM via ip restrictions&lt;/a&gt;. 

&lt;p&gt;This secures our function app and it isn’t available publicly, any one that tries to access our function app url will get “HTTP 403 Forbidden”.&lt;/p&gt;
&lt;p&gt;This secures our function app; now what about deploying code changes to the function app via GitHub Actions? we should be able to CI&amp;#x2F;CD to our function app, but there is a problem here. The GitHub action will fail with the same “HTTP 403 Forbidden”, this is because GitHub actions run on runners (its a hosted virtual environment), each time we run the Action we get a new runner and it can have a different ip address.  So how can we get around this? &lt;a href=&quot;https://api.github.com/meta&quot;&gt;do we white list the entire GitHub ip range?&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;GitHub’s ip ranges can change any time, so will have to keep scanning for changes to these ranges and proactively update our ip restrictions, this is not very scalable or practical. So what are other ways of getting around this? we have a couple of ways to get around this.&lt;/p&gt;
&lt;h2 id=&quot;Possible-Solutions&quot;&gt;&lt;a href=&quot;#Possible-Solutions&quot; class=&quot;headerlink&quot; title=&quot;Possible Solutions&quot;&gt;&lt;/a&gt;Possible Solutions&lt;/h2&gt;&lt;p&gt;There are two viable solutions here&lt;/p&gt;</summary>
    
    
    
    <category term="GitHub" scheme="https://clouddev.blog/categories/GitHub/"/>
    
    <category term="Actions" scheme="https://clouddev.blog/categories/GitHub/Actions/"/>
    
    
    <category term="Azure" scheme="https://clouddev.blog/tags/Azure/"/>
    
    <category term="Security" scheme="https://clouddev.blog/tags/Security/"/>
    
    <category term="Function Apps" scheme="https://clouddev.blog/tags/Function-Apps/"/>
    
    <category term="Azure App Service" scheme="https://clouddev.blog/tags/Azure-App-Service/"/>
    
    <category term="GitHub" scheme="https://clouddev.blog/tags/GitHub/"/>
    
    <category term="CI/CD" scheme="https://clouddev.blog/tags/CI-CD/"/>
    
    <category term="IP Restrictions" scheme="https://clouddev.blog/tags/IP-Restrictions/"/>
    
    <category term="Serverless" scheme="https://clouddev.blog/tags/Serverless/"/>
    
  </entry>
  
  <entry>
    <title>Securing Azure Functions and Logic Apps</title>
    <link href="https://clouddev.blog/Azure/Function-Apps/Security/securing-azure-functions-and-logic-apps/"/>
    <id>https://clouddev.blog/Azure/Function-Apps/Security/securing-azure-functions-and-logic-apps/</id>
    <published>2022-07-31T12:00:00.000Z</published>
    <updated>2025-08-07T05:42:25.055Z</updated>
    
    <content type="html"><![CDATA[<hr><blockquote><p><strong>🎯 TL;DR: Cost-Optimized Security for Serverless Microservices</strong></p><p>Consumption plan Function Apps and APIM Standard lack VNet integration for cost optimization but expose services publicly. Problem: Serverless microservices accessible directly bypassing API Management security policies. Solution: IP restriction-based security using APIM’s public IP address to whitelist only API Management access, configuring both main site and SCM site restrictions. Architecture includes Azure Front Door for WAF capabilities since APIM Standard lacks native WAF protection.</p></blockquote><hr><p>Here is a scenario that I recently encountered. Imagine we are building micro-services using serverless (a mix on Azure Function Apps and Logic Apps) with APIM in the front.  Lets say we went with the APIM standard instance and all the logic and function apps are going to be running on consumption plan (for cost reasons as its cheaper).  This means we wont be getting any vnet capability and our function and logic apps will be exposed out to the world (remember to get vnet with APIM we have to go with the premium version, we are going APIM standard here for cost saving reasons).</p><p>So how do we restrict our function and logic apps to only go through the APIM, in another words all our function and logic apps <strong>must only</strong> go through the APIM and if anyone tries to access them directly they should be getting a “HTTP 403 Forbidden”.</p><p>Lets visualize this scenario; We have some WAF capable ingress endpoint, in this case its Azure Front Door, that is forwarding traffic to APIM which then sends the requests to the serverless apps.<br>Reason for having Front Door before APIM is because APIM doesn’t have WAF natively so we <a href="https://docs.microsoft.com/en-us/security/benchmark/azure/baselines/api-management-security-baseline#ns-6-deploy-web-application-firewall">will need to put something in front of it that has that capability to be secure</a>. </p><p><a href="https://docs.microsoft.com/en-us/security/benchmark/azure/baselines/api-management-security-baseline#ns-6-deploy-web-application-firewall">There are few options like Azure Firewall, Application Gateway etc</a>, but for the purposes of this scenario we have Azure Front Door in front of APIM (and we can have an APIM policy that will only accept traffic from Azure Font Door, we wont be going in to that, we will keep it to securing our function apps to just being available via APIM for today)</p><p><img src="/Azure/Function-Apps/Security/securing-azure-functions-and-logic-apps/apim-azure-functions-backend.png" alt=" " title="Sample Scenario"></p><span id="more"></span><h2 id="Securing-the-function-app"><a href="#Securing-the-function-app" class="headerlink" title="Securing the function app"></a>Securing the function app</h2><ol><li>First we will need to get the public ip address of the APIM</li><li>White-list this address in our function app network restrictions</li></ol><h2 id="Getting-the-public-ip-of-APIM"><a href="#Getting-the-public-ip-of-APIM" class="headerlink" title="Getting the public ip of APIM"></a>Getting the public ip of APIM</h2><p>You can go to the APIM resource in the Azure portal and get it from there<br><img src="/Azure/Function-Apps/Security/securing-azure-functions-and-logic-apps/apim-public-ip.png" alt=" " title="APIM ip address"></p><p>Or you can use the CLI and run </p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">az apim show --name &quot;apim-name&quot; --resource-group &quot;resource-group-name&quot;</span><br></pre></td></tr></table></figure><h2 id="White-listing-the-function-app"><a href="#White-listing-the-function-app" class="headerlink" title="White-listing the function app"></a>White-listing the function app</h2><ol><li>You need to go into networking -&gt; access restriction</li><li>Only allow the APIM ip (once you enter this, the deny all will automatically come ie: all other ip’s are denied)</li><li>Its important that the SCM site is also blocked. <a href="https://docs.microsoft.com/en-us/azure/app-service/resources-kudu">More about Kudu service that powers the SCM site here</a></li></ol><p><img src="/Azure/Function-Apps/Security/securing-azure-functions-and-logic-apps/func-ip-restriction-1.png" alt=" " title="Function app ip restrictions"></p><p><img src="/Azure/Function-Apps/Security/securing-azure-functions-and-logic-apps/func-ip-restriction-2.png" alt=" " title="Function app block all ips except APIM"></p><p><img src="/Azure/Function-Apps/Security/securing-azure-functions-and-logic-apps/func-ip-restriction-3.png" alt=" " title="Make sure to block the SCM site also"></p><h2 id="What-happens-if-you-try-to-access-this-function"><a href="#What-happens-if-you-try-to-access-this-function" class="headerlink" title="What happens if you try to access this function"></a>What happens if you try to access this function</h2><p>Now its all blocked we get a nice HTTP 403 Forbidden</p><p><img src="/Azure/Function-Apps/Security/securing-azure-functions-and-logic-apps/func-ip-restriction-4.png" alt=" " title="Make sure to block the SCM site also"></p><h2 id="What-about-deploying-code-to-this-function-via-GitHub-Actions"><a href="#What-about-deploying-code-to-this-function-via-GitHub-Actions" class="headerlink" title="What about deploying code to this function via GitHub Actions"></a>What about deploying code to this function via GitHub Actions</h2><p>When you try to deploy to these functions using GitHub Actions or even via Azure Devops you will get the same HTTP 403 and wont be able to deploy.  This is because the GitHub runner’s ip address will be blocked; remember we are only allowing APIM in, all others are blocked.</p><p>There are a couple of ways to get around this. <a href="/GitHub/Actions/deploying-to-ip-restricted-azure-function-apps-using-github-actions/" title="Deploying To IP Restricted Azure Function Apps Using GitHub Actions">I talk about this in the next post, you can check it out here</a></p><h2 id="References"><a href="#References" class="headerlink" title="References"></a>References</h2><ul><li>Cover image has been taken from <a href="https://azure.microsoft.com/en-us/services/functions/#overview">https://azure.microsoft.com/en-us/services/functions/#overview</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;hr&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🎯 TL;DR: Cost-Optimized Security for Serverless Microservices&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Consumption plan Function Apps and APIM Standard lack VNet integration for cost optimization but expose services publicly. Problem: Serverless microservices accessible directly bypassing API Management security policies. Solution: IP restriction-based security using APIM’s public IP address to whitelist only API Management access, configuring both main site and SCM site restrictions. Architecture includes Azure Front Door for WAF capabilities since APIM Standard lacks native WAF protection.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;p&gt;Here is a scenario that I recently encountered. Imagine we are building micro-services using serverless (a mix on Azure Function Apps and Logic Apps) with APIM in the front.  Lets say we went with the APIM standard instance and all the logic and function apps are going to be running on consumption plan (for cost reasons as its cheaper).  This means we wont be getting any vnet capability and our function and logic apps will be exposed out to the world (remember to get vnet with APIM we have to go with the premium version, we are going APIM standard here for cost saving reasons).&lt;/p&gt;
&lt;p&gt;So how do we restrict our function and logic apps to only go through the APIM, in another words all our function and logic apps &lt;strong&gt;must only&lt;/strong&gt; go through the APIM and if anyone tries to access them directly they should be getting a “HTTP 403 Forbidden”.&lt;/p&gt;
&lt;p&gt;Lets visualize this scenario; We have some WAF capable ingress endpoint, in this case its Azure Front Door, that is forwarding traffic to APIM which then sends the requests to the serverless apps.&lt;br&gt;Reason for having Front Door before APIM is because APIM doesn’t have WAF natively so we &lt;a href=&quot;https://docs.microsoft.com/en-us/security/benchmark/azure/baselines/api-management-security-baseline#ns-6-deploy-web-application-firewall&quot;&gt;will need to put something in front of it that has that capability to be secure&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://docs.microsoft.com/en-us/security/benchmark/azure/baselines/api-management-security-baseline#ns-6-deploy-web-application-firewall&quot;&gt;There are few options like Azure Firewall, Application Gateway etc&lt;/a&gt;, but for the purposes of this scenario we have Azure Front Door in front of APIM (and we can have an APIM policy that will only accept traffic from Azure Font Door, we wont be going in to that, we will keep it to securing our function apps to just being available via APIM for today)&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/Azure/Function-Apps/Security/securing-azure-functions-and-logic-apps/apim-azure-functions-backend.png&quot; alt=&quot; &quot; title=&quot;Sample Scenario&quot;&gt;&lt;/p&gt;</summary>
    
    
    
    <category term="Azure" scheme="https://clouddev.blog/categories/Azure/"/>
    
    <category term="Function Apps" scheme="https://clouddev.blog/categories/Azure/Function-Apps/"/>
    
    <category term="Security" scheme="https://clouddev.blog/categories/Azure/Function-Apps/Security/"/>
    
    
    <category term="Azure" scheme="https://clouddev.blog/tags/Azure/"/>
    
    <category term="Security" scheme="https://clouddev.blog/tags/Security/"/>
    
    <category term="Function Apps" scheme="https://clouddev.blog/tags/Function-Apps/"/>
    
    <category term="Azure App Service" scheme="https://clouddev.blog/tags/Azure-App-Service/"/>
    
    <category term="GitHub" scheme="https://clouddev.blog/tags/GitHub/"/>
    
    <category term="CI/CD" scheme="https://clouddev.blog/tags/CI-CD/"/>
    
    <category term="IP Restrictions" scheme="https://clouddev.blog/tags/IP-Restrictions/"/>
    
    <category term="Serverless" scheme="https://clouddev.blog/tags/Serverless/"/>
    
  </entry>
  
  <entry>
    <title>Hello World 👋</title>
    <link href="https://clouddev.blog/Blog/hello-world-%F0%9F%91%8B/"/>
    <id>https://clouddev.blog/Blog/hello-world-%F0%9F%91%8B/</id>
    <published>2022-07-26T12:00:00.000Z</published>
    <updated>2025-01-12T23:24:52.009Z</updated>
    
    <content type="html"><![CDATA[<p>After sitting on this for a long time and wanting to blog &#x2F; write down my thoughts, I’ve finally got my act together and started this. There were so many times I was asked some very good questions which I am sure not just the person asking me but a lot more would have been interested in knowing the answer&#x2F;solution&#x2F;thoughts around the matter.  This is a way to write about that and help the wider community who are searching for similar solutions.</p><p>I regularly answer in Stack Overflow and in some cases I wrote a question and answered it myself just incase some one was looking for something similar, that wasn’t really the ideal platform to do that. There have been so many times that going through and reading other people’s blogs have helped me and unlocked me in problems that I was stuck with; this is a in a way trying to give back to the community and helping people that are on the look out for a solution for a similar problem.</p><h1 id="How-to-power-the-blog"><a href="#How-to-power-the-blog" class="headerlink" title="How to power the blog"></a>How to power the blog</h1><p>There were so many choices out there when it came to what frameworks and libraries to use to build the blog and what to use to host the blog.</p><h2 id="My-requirements-when-it-came-to-building-were-simple"><a href="#My-requirements-when-it-came-to-building-were-simple" class="headerlink" title="My requirements when it came to building were simple"></a>My requirements when it came to building were simple</h2><ul><li>Easy to author posts</li><li>Easy to build</li><li>Easy to maintain</li><li>Most customizations (eg: search, ads, tags, categories etc) should come out of the box</li></ul><h2 id="My-requirements-when-it-came-to-hosting-were-even-simpler"><a href="#My-requirements-when-it-came-to-hosting-were-even-simpler" class="headerlink" title="My requirements when it came to hosting were even simpler"></a>My requirements when it came to hosting were even simpler</h2><ul><li>Has to be free</li><li>Has to be able to handle ‘some’ level of load</li><li>Easy to CI&#x2F;CD</li></ul><span id="more"></span><h2 id="Main-choices-here-boiled-down-to"><a href="#Main-choices-here-boiled-down-to" class="headerlink" title="Main choices here boiled down to:"></a>Main choices here boiled down to:</h2><ul><li><a href="https://github.com/OrchardCMS/OrchardCore">Orchard CMS</a></li><li><a href="https://github.com/gohugoio/hugo">Hugo</a></li><li><a href="https://github.com/TryGhost/Ghost">Ghost</a></li><li><a href="https://jekyllrb.com/docs/github-pages/">Jekyll With Github Pages</a></li><li><a href="https://github.com/hexojs/hexo">Hexo</a></li></ul><p>All the options were good, I really liked Hugo, it was so easy to create a site. But all of them were geared towards creating a CMS &#x2F; generic site.  I was looking for something that had all the things needed for a blog out of the box with out having to grab lots of plugins or write something custom.</p><p>Jekyll and GitHub pages were really good, it nailed most of the things, but I didn’t really want to go down the road of learning Jekyll just to host a blog. This left one and Hexo fit my requirements beautifully. It was a dedicated Javascript framework that has all the things I was looking for out of the box and it had <a href="https://hexo.io/themes/">360+ themes available all community built and free</a>.</p><p>One thing I loved about Hexo is the fact its builds the source to a static site and you can use GitHub to host the static site and use <a href="https://hexo.io/docs/github-pages">GitHub Actions</a> to build the static site from source.</p><p>This is what I went with in the end, Hexo to build the blog.  I write everything in markdown files and Hexo builds it out into a nice static site and I host it using <a href="https://github.com/Ricky-G/ricky-g.github.io">GitHub pages as a public repo</a></p><p>There are some <a href="https://docs.github.com/en/pages/getting-started-with-github-pages/about-github-pages#usage-limits">limits of hosting with GitHub Pages</a>, the main one is the 100GB of bandwidth as a soft limit.  Since this is just a static site 100GB should be plenty but if and when it comes to that I will look at putting a CDN in front.</p><h1 id="Final-Result"><a href="#Final-Result" class="headerlink" title="Final Result"></a>Final Result</h1><ul><li><a href="https://github.com/hexojs/hexo">Hexo</a> to build the blog into a static site</li><li><a href="https://github.com/ppoffice/hexo-theme-icarus">Icarus</a> theme</li><li><a href="https://pages.github.com/">GitHub Pages</a> to host the site</li><li><a href="https://bulma.io/">Bulma</a> to help enrich the markdown files with styling</li></ul><h2 id="References"><a href="#References" class="headerlink" title="References"></a>References</h2><p>As always a big thank you to <a href="https://unsplash.com/">Unsplash</a> for providing a huge range of images for free</p><ul><li>Cover image has been taken from <a href="https://unsplash.com/photos/3SIXZisims4">https://unsplash.com/photos/3SIXZisims4</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;After sitting on this for a long time and wanting to blog &amp;#x2F; write down my thoughts, I’ve finally got my act together and started this. There were so many times I was asked some very good questions which I am sure not just the person asking me but a lot more would have been interested in knowing the answer&amp;#x2F;solution&amp;#x2F;thoughts around the matter.  This is a way to write about that and help the wider community who are searching for similar solutions.&lt;/p&gt;
&lt;p&gt;I regularly answer in Stack Overflow and in some cases I wrote a question and answered it myself just incase some one was looking for something similar, that wasn’t really the ideal platform to do that. There have been so many times that going through and reading other people’s blogs have helped me and unlocked me in problems that I was stuck with; this is a in a way trying to give back to the community and helping people that are on the look out for a solution for a similar problem.&lt;/p&gt;
&lt;h1 id=&quot;How-to-power-the-blog&quot;&gt;&lt;a href=&quot;#How-to-power-the-blog&quot; class=&quot;headerlink&quot; title=&quot;How to power the blog&quot;&gt;&lt;/a&gt;How to power the blog&lt;/h1&gt;&lt;p&gt;There were so many choices out there when it came to what frameworks and libraries to use to build the blog and what to use to host the blog.&lt;/p&gt;
&lt;h2 id=&quot;My-requirements-when-it-came-to-building-were-simple&quot;&gt;&lt;a href=&quot;#My-requirements-when-it-came-to-building-were-simple&quot; class=&quot;headerlink&quot; title=&quot;My requirements when it came to building were simple&quot;&gt;&lt;/a&gt;My requirements when it came to building were simple&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;Easy to author posts&lt;/li&gt;
&lt;li&gt;Easy to build&lt;/li&gt;
&lt;li&gt;Easy to maintain&lt;/li&gt;
&lt;li&gt;Most customizations (eg: search, ads, tags, categories etc) should come out of the box&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;My-requirements-when-it-came-to-hosting-were-even-simpler&quot;&gt;&lt;a href=&quot;#My-requirements-when-it-came-to-hosting-were-even-simpler&quot; class=&quot;headerlink&quot; title=&quot;My requirements when it came to hosting were even simpler&quot;&gt;&lt;/a&gt;My requirements when it came to hosting were even simpler&lt;/h2&gt;&lt;ul&gt;
&lt;li&gt;Has to be free&lt;/li&gt;
&lt;li&gt;Has to be able to handle ‘some’ level of load&lt;/li&gt;
&lt;li&gt;Easy to CI&amp;#x2F;CD&lt;/li&gt;
&lt;/ul&gt;</summary>
    
    
    
    <category term="Blog" scheme="https://clouddev.blog/categories/Blog/"/>
    
    
    <category term="Hexo" scheme="https://clouddev.blog/tags/Hexo/"/>
    
    <category term="Personal" scheme="https://clouddev.blog/tags/Personal/"/>
    
    <category term="Blog" scheme="https://clouddev.blog/tags/Blog/"/>
    
  </entry>
  
</feed>
