<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Azure &#8211; achraf ben alaya</title>
	<atom:link href="https://achrafbenalaya.com/category/blog/cloud/azure/feed/" rel="self" type="application/rss+xml" />
	<link>https://achrafbenalaya.com</link>
	<description>Tech Blog By Achraf Ben Alaya</description>
	<lastBuildDate>Sun, 29 Mar 2026 13:37:29 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.7.5</generator>

<image>
	<url>/wp-content/uploads/2022/02/cropped-me-scaled-1-32x32.jpeg</url>
	<title>Azure &#8211; achraf ben alaya</title>
	<link>https://achrafbenalaya.com</link>
	<width>32</width>
	<height>32</height>
</image> 
<site xmlns="com-wordpress:feed-additions:1">189072172</site>	<item>
		<title>GitHub Copilot Skills for Terraform: 5 On-Demand AI Assistants for Azure Container Apps</title>
		<link>https://achrafbenalaya.com/2026/03/29/github-copilot-skills-for-terraform-5-on-demand-ai-assistants-for-azure-container-apps/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=github-copilot-skills-for-terraform-5-on-demand-ai-assistants-for-azure-container-apps</link>
					<comments>https://achrafbenalaya.com/2026/03/29/github-copilot-skills-for-terraform-5-on-demand-ai-assistants-for-azure-container-apps/#respond</comments>
		
		<dc:creator><![CDATA[achraf]]></dc:creator>
		<pubDate>Sun, 29 Mar 2026 13:37:29 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[Azure]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Cloud]]></category>
		<category><![CDATA[copilot]]></category>
		<category><![CDATA[azure]]></category>
		<guid isPermaLink="false">https://achrafbenalaya.com/?p=2489</guid>

					<description><![CDATA[Teaching Copilot to Know Your Stack: GitHub Copilot Skills for Azure Container Apps Part 4 In Part 3, we gave GitHub Copilot a project identity. Custom instructions told it about our Zero Trust rules. Path-specific instructions scoped guidance to the right files. Prompt files turned repetitive tasks into one-click workflows. Custom agents gave it specialized [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p><strong>Teaching Copilot to Know Your Stack: GitHub Copilot Skills for Azure Container Apps Part 4</strong></p>



<p>In Part 3, we gave GitHub Copilot a project identity. Custom instructions told it about our Zero Trust rules. Path-specific instructions scoped guidance to the right files. Prompt files turned repetitive tasks into one-click workflows. Custom agents gave it specialized personas with scoped tools.</p>



<p>But all of that is <em>always on</em>. Every Copilot conversation loads the custom instructions, whether you&#8217;re asking about networking or asking it to write a commit message. That&#8217;s fine when the instructions are small. It becomes a problem when your project grows  when you accumulate rules for security reviews, cost analysis, state management, scaling, and image lifecycle. You can&#8217;t fit everything into `copilot-instructions.md` without turning it into a sprawling document that confuses the model as much as it helps it.</p>



<p>Skills solve this. They&#8217;re the on-demand counterpart to always-on instructions. A skill is a folder with a `SKILL.md` file that Copilot loads <em>*only when your prompt is relevant to it</em>. Ask about drift? The drift detector skill loads. Ask about costs? The cost estimator loads. Ask about a new Container App resource? Copilot uses the base instructions and leaves the cost estimator alone.</p>



<p>This is Part 4, and it&#8217;s entirely about skills.</p>



<h2 class="wp-block-heading"><strong>What skills actually are</strong></h2>



<p>The mental model is straightforward. Custom instructions are rules you want Copilot to follow always  your naming conventions, your provider version, your security posture. Skills are specialized knowledge packages you want available on demand the deep expertise for a specific task that would clutter the always-on context if it were always loaded.</p>



<p>Mechanically, a skill is a directory under `.github/skills/` with a single required file: `SKILL.md`. That file has two parts: a YAML frontmatter block and a Markdown body.</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
```
.github/
└── skills/
    └── my-skill-name/
        └── SKILL.md
```

The frontmatter defines the skill&#039;s identity:

```yaml
---
name: my-skill-name
description: &gt;
  One sentence summary.

  When to use this skill:
  - &quot;trigger phrase 1&quot;
  - &quot;trigger phrase 2&quot;
---
```

</pre></div>


<p>The <code>description</code> field is doing a lot of work here. Copilot reads it to decide whether this skill is relevant to your current prompt. The &#8220;when to use&#8221; section isn&#8217;t documentation for humans it&#8217;s signal for the model. Write it as a list of phrases someone would actually type when they need this skill. The more specific and realistic, the better the match.</p>



<p>The Markdown body is the skill&#8217;s actual content: instructions, workflows, code examples, lookup tables, templates. Whatever Copilot needs to execute the task well.</p>



<h2 class="wp-block-heading">Skills vs. the other tools in the toolbox</h2>



<p>After three parts covering custom instructions, path-specific instructions, prompt files, and agents, it&#8217;s worth being precise about where skills fit.</p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Tool</th><th>Location</th><th>When loaded</th><th>Best for</th></tr></thead><tbody><tr><td>Custom instructions</td><td><code>.github/copilot-instructions.md</code></td><td>Every conversation</td><td>Universal rules (naming, security, provider version)</td></tr><tr><td>Path-specific instructions</td><td><code>.github/instructions/*.instructions.md</code></td><td>When editing matching files</td><td>File-type-specific guidance</td></tr><tr><td>Prompt files</td><td><code>.github/prompts/*.prompt.md</code></td><td>When you select them manually</td><td>Repeatable multi-step tasks</td></tr><tr><td>Custom agents</td><td><code>.github/agents/*.agent.md</code></td><td>When you select the agent</td><td>Specialized personas with tool access</td></tr><tr><td><strong>Skills</strong></td><td><strong><code>.github/skills/*/SKILL.md</code></strong></td><td><strong>When prompt matches description</strong></td><td><strong>Deep expertise for specific tasks</strong></td></tr></tbody></table></figure>



<p>The key distinction from agents: agents are personas you <em>*select*</em>. Skills are knowledge packages that Copilot <em>*discovers*</em>. When you assign an issue to the coding agent with the implementation agent selected, that&#8217;s an explicit choice. When you ask &#8220;how much does this architecture cost?&#8221; and the cost estimator skill loads, that&#8217;s automatic.</p>



<h2 class="wp-block-heading"><strong>Building five skills for this project</strong></h2>



<p>Let&#8217;s build a complete skill library for the Azure Container Apps infrastructure we&#8217;ve been assembling across Parts 1, 2, and 3. Each skill addresses a real operational need.</p>



<h2 class="wp-block-heading"><strong>Skill 1  Terraform Drift Detector</strong> :</h2>



<p>Create `.github/skills/terraform-drift-detector/SKILL.md`: </p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
---
name: terraform-drift-detector
description: &gt;
  Detect, explain, and resolve Terraform state drift in this Azure Container Apps project.

  When to use this skill:
  - &quot;Why does my terraform plan show unexpected changes?&quot;
  - &quot;Something changed in Azure but I didn&#039;t touch the Terraform&quot;
  - &quot;Detect drift&quot;, &quot;check for drift&quot;, &quot;why is plan not clean?&quot;
  - &quot;Azure Portal changes not reflected in state&quot;
  - After manual changes via Azure CLI or Portal that weren&#039;t tracked in state
---

# Terraform Drift Detector

You are an expert in Terraform state management for Azure Container Apps infrastructure.
Your job is to detect, explain, and resolve drift between the Terraform state and actual Azure resources.

## What Is Drift

Drift happens when real Azure resources no longer match what Terraform&#039;s state file says they should be.
Common causes in this project:
- Manual changes via Azure Portal or CLI (e.g., scaling a Container App by hand)
- Azure auto-healing or auto-upgrading resources (e.g., Container App revision updates)
- Expiry or auto-rotation of identities
- Out-of-band certificate renewals on Application Gateway

## Detection Workflow

1. Read the current state using `#readFile` on `terraform.tfstate`
2. Run `terraform plan -detailed-exitcode`
   - Exit code 0 = no drift
   - Exit code 2 = drift found
3. Categorize each change by severity:
   - `external_enabled` flip on any Container App → 🔴 CRITICAL
   - `admin_enabled` change on ACR → 🔴 CRITICAL
   - Scaling values (min/max replicas) → 🟡 MEDIUM
   - Tag or label drifts → 🟢 LOW

## Reconciliation Rules

- NEVER auto-run `terraform apply` — show the plan first, ask for confirmation
- For CRITICAL: surface clearly, explain what probably happened, recommend remediation steps
- For LOW/MEDIUM: propose `terraform apply -target=&amp;lt;resource&gt;` with the specific address

## Output Format

Present findings as a Drift Report with Critical Changes and Safe-to-Reconcile sections.



</pre></div>


<p><strong>Why this skill matters.</strong> In a real team, someone will make a manual change in the Azure Portal  usually in an incident, under pressure. Terraform will then want to revert it. The worst case is a <code>terraform apply</code> that flips <code>external_enabled</code> from <code>true</code> to <code>false</code> on the frontend, taking it offline. The drift detector loads the right context to catch that before it happens.</p>



<h2 class="wp-block-heading">Skill 2 ACA Scaling Advisor</h2>



<p>Create `.github/skills/aca-scaling-advisor/SKILL.md`:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
---
name: aca-scaling-advisor
description: &gt;
  Design, review, and optimize scaling rules for Azure Container Apps in this project.

  When to use this skill:
  - &quot;Add scaling rules to my Container App&quot;
  - &quot;How should I scale the backend API?&quot;
  - &quot;My app is slow under load&quot;, &quot;optimize scaling&quot;, &quot;autoscale&quot;
  - &quot;Add KEDA scaler&quot;, &quot;HTTP scaling&quot;, &quot;queue-based scaling&quot;
  - &quot;min_replicas is too high&quot;, &quot;I&#039;m paying too much for idle containers&quot;
---

# ACA Scaling Advisor

You are an expert in Azure Container Apps autoscaling using KEDA.
Architecture context: frontend is external-facing via Application Gateway,
backend is internal-only. Both run on Consumption workload profile.

## Key Rules

**Frontend** — safe for `min_replicas = 0`. Application Gateway handles the queue.
Use HTTP scaler with `concurrentRequests = 100`.

**Backend** — keep `min_replicas = 1` unless the frontend uses async calls.
Scale-to-zero on a synchronously-called backend means the frontend sees cold start latency.
Use CPU scaler at 70% utilization.

## HTTP Scaling (Frontend)

\`\`\`hcl
template {
  min_replicas = 0
  max_replicas = 10

  custom_scale_rule {
    name             = &quot;http-scaler&quot;
    custom_rule_type = &quot;http&quot;
    metadata = {
      concurrentRequests = &quot;100&quot;
    }
  }
}
\`\`\`

## CPU Scaling (Backend)

\`\`\`hcl
template {
  min_replicas = 1
  max_replicas = 5

  custom_scale_rule {
    name             = &quot;cpu-scaler&quot;
    custom_rule_type = &quot;cpu&quot;
    metadata = {
      type  = &quot;Utilization&quot;
      value = &quot;70&quot;
    }
  }
}
\`\`\`

Always validate: no conflicting scale rules, `max_replicas` fits your cost ceiling,
and `min_replicas ≥ 1` on synchronously-called services.



</pre></div>


<p>The scaling advisor encodes the architectural constraints of this specific project. A generic Copilot response might suggest <code>min_replicas = 0</code> on the backend because it reduces costs. That&#8217;s correct in isolation. It&#8217;s wrong here because the frontend calls the backend synchronously a cold start becomes frontend latency. The skill carries that context.</p>



<h2 class="wp-block-heading">Skill 3 Azure Cost Estimator</h2>



<p>Create&nbsp;<code>.github/skills/azure-cost-estimator/SKILL.md</code>:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
---
name: azure-cost-estimator
description: &gt;
  Estimate and break down monthly Azure costs for this Container Apps infrastructure.

  When to use this skill:
  - &quot;How much does this architecture cost?&quot;
  - &quot;Estimate my Azure bill&quot;, &quot;cost breakdown&quot;, &quot;cost estimate&quot;
  - &quot;Is the Application Gateway expensive?&quot;
  - &quot;I want to reduce costs&quot;, &quot;cheapest way to run this&quot;
  - &quot;What&#039;s the difference in cost between Consumption and Dedicated?&quot;
  - Before adding new resources — estimate the cost impact first
---

# Azure Cost Estimator

Pricing reference for West Europe (adjust by ~10-20% for other regions):

| Resource | Config | Est. Monthly Cost |
|---|---|---|
| Application Gateway | Standard_v2 | ~$185 |
| Container Registry | Standard SKU | ~$20 |
| Container Apps (per app) | 0.25 vCPU, 0.5GiB, 8h/day | ~$15 |
| Public IP | Standard | ~$4 |
| Private DNS Zone | 1 zone | ~$1 |
| Log Analytics | &amp;lt;5GB/day ingestion | ~$0 |

**Important:** Application Gateway accounts for ~75% of the bill at this scale.
It costs ~$185/month regardless of traffic volume.

When asked for an estimate:
1. Read `.tf` files to identify all provisioned resources
2. Ask for region if not West Europe
3. Present the cost table with actual resource configs from Terraform
4. Highlight the biggest cost driver
5. Suggest one or two project-specific optimizations
6. Always point to the Azure Pricing Calculator for accurate quotes


</pre></div>


<p>Every project eventually hits the &#8220;what&#8217;s this costing us?&#8221; question. Without the skill, Copilot would give a generic answer that doesn&#8217;t account for the Application Gateway&#8217;s flat cost, the specific SKUs we chose, or the Consumption billing model. The skill bakes those numbers in.</p>



<h2 class="wp-block-heading"><strong>Skill 4  Security Posture Reviewer</strong></h2>



<p>Create&nbsp;<code>.github/skills/security-posture-reviewer/SKILL.md</code>:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: yaml; title: ; notranslate">
---
name: security-posture-reviewer
description: &gt;
  Review the Zero Trust security posture of this Azure Container Apps Terraform project.

  When to use this skill:
  - &quot;Review my security&quot;, &quot;security audit&quot;, &quot;security check&quot;
  - &quot;Is this Zero Trust?&quot;, &quot;what are my security gaps?&quot;
  - &quot;Check my NSG rules&quot;, &quot;are my secrets safe?&quot;
  - Before a production deployment or architecture review
  - After adding new resources — verify they follow the project&#039;s security rules
---

# Security Posture Reviewer

Review the project&#039;s Zero Trust compliance against this checklist.
Output ✅ or ❌ for each item after reading the Terraform files.

### Networking
- &#x5B; ] `internal_load_balancer_enabled = true` on Container App Environment
- &#x5B; ] ACA subnet CIDR is minimum `/23`
- &#x5B; ] NSG includes `GatewayManager` and `AzureLoadBalancer` inbound rules on AppGW subnet
- &#x5B; ] No `0.0.0.0/0` allow-all inbound rules (except those required by Azure platform)
- &#x5B; ] Private DNS zone linked to VNet with `registration_enabled = false`

### Container Apps
- &#x5B; ] Backend uses `external_enabled = false` (not just an NSG rule)
- &#x5B; ] No plaintext secrets in environment variables

### Container Registry
- &#x5B; ] `admin_enabled = false`
- &#x5B; ] SKU is Standard or Premium

### Identity and Credentials
- &#x5B; ] System-assigned Managed Identity enabled on both Container Apps
- &#x5B; ] `AcrPull` role assigned for each Container App&#039;s principal_id
- &#x5B; ] GitHub Actions uses workload identity federation (not service principal secrets)
- &#x5B; ] `terraform.tfstate` is NOT committed to the repository

## Common Gaps to Flag

**State file in repo**  contains resource IDs and output values. Add to `.gitignore` and use Azure Storage backend.

**Missing WAF policy**  Standard_v2 supports WAF but it&#039;s not enabled by default. Without it, no OWASP rule set protects the frontend from injection attacks.

**Log Analytics retention at default 30 days**  insufficient for incident response. Set `retention_in_days = 90`.
</pre></div>


<p>This skill is the automated version of the senior engineer&#8217;s pre-deployment checklist. It doesn&#8217;t just know generic security best practices  it knows <em>this project&#8217;s</em> security model and can check it against the actual Terraform files.</p>



<h2 class="wp-block-heading">Skill 5  ACR Image Manager</h2>



<p>Create&nbsp;<code>.github/skills/acr-image-manager/SKILL.md</code>:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: yaml; title: ; notranslate">
---
name: acr-image-manager
description: &gt;
  Manage container images in Azure Container Registry — tagging strategy, image promotion,
  cleanup, and updating Container App image references in Terraform.

  When to use this skill:
  - &quot;Tag my image for production&quot;, &quot;promote image from staging to prod&quot;
  - &quot;Clean up old images in ACR&quot;, &quot;delete untagged manifests&quot;
  - &quot;Update the Container App to use image version X&quot;
  - &quot;How should I tag my Docker images?&quot;, &quot;image versioning strategy&quot;
  - &quot;Purge images older than 30 days&quot;, &quot;reduce ACR storage costs&quot;
---

# ACR Image Manager

ACR was created with `admin_enabled = false`. All operations use RBAC, not admin credentials.
Always authenticate with `az acr login --name &lt;acr_name&gt;` using the user&#039;s Azure CLI identity.

## Tagging Strategy

Use semantic versioning with a build reference. Never rely on `latest` alone for production.

\`\`\`
&lt;acr_login_server&gt;/&lt;service&gt;:&lt;semver&gt;-&lt;short_sha&gt;
\`\`\`

Examples:
- `acrab3k2m.azurecr.io/frontend:1.2.0-a3f9c1e`  ← immutable, for rollback
- `acrab3k2m.azurecr.io/frontend:latest`           ← mutable, for convenience

## Image Promotion (Staging → Production)

Use `az acr import` to copy between registries without pulling locally:

\`\`\`bash
az acr import \
  --name &lt;prod_acr_name&gt; \
  --source &lt;staging_acr_login_server&gt;/backend:1.2.0-a3f9c1e \
  --image backend:prod-1.2.0 \
  --registry &lt;staging_acr_resource_id&gt;
\`\`\`

## Updating Container App Image in Terraform

After promoting, update the `image` field in `aca.tf`:

\`\`\`hcl
image = &quot;${azurerm_container_registry.acr.login_server}/backend:1.2.0-a3f9c1e&quot;
\`\`\`

Then run `terraform plan` — only the image tag should change. Azure Container Apps creates a new revision automatically.

## Cleanup

Always dry-run before executing:

\`\`\`bash
az acr run \
  --registry &lt;acr_name&gt; \
  --cmd &quot;acr purge --filter &#039;backend:.*&#039; --untagged --ago 30d --dry-run&quot; \
  /dev/null
\`\`\`

Remove `--dry-run` once the output looks right.

</pre></div>


<p>The image management skill is particularly useful after the GitHub Actions CI/CD pipeline from Part 3 starts pushing images. Without it, Copilot doesn&#8217;t know whether to suggest <code>az acr</code> commands, direct Docker commands, or Terraform changes. The skill normalizes that: use RBAC auth, use the ACR import command for promotion, update the specific <code>image</code> field in <code>aca.tf</code>.</p>



<h2 class="wp-block-heading"><strong>How Copilot discovers and loads skills</strong></h2>



<p>You don&#8217;t invoke skills manually. When you type a prompt in Copilot Chat (or an issue body that gets routed to the coding agent), Copilot reads the&nbsp;<code>description</code>&nbsp;field of every skill in&nbsp;<code>.github/skills/</code>&nbsp;and decides which ones are relevant. If the match is strong, the&nbsp;<code>SKILL.md</code>&nbsp;content is injected into the context for that conversation.</p>



<p>This means the&nbsp;<code>description</code>&nbsp;field is actually the most important part of the file. Write it as if you&#8217;re writing the queries that should trigger it. The more concrete and realistic, the better.</p>



<p>A few things that improve match quality:</p>



<p><strong>Be specific about trigger phrases.</strong>&nbsp;&#8220;Detect drift&#8221;, &#8220;check for drift&#8221;, and &#8220;why is plan not clean?&#8221; are all things a real engineer would type. Generic phrases like &#8220;help with infrastructure&#8221; are too broad — they&#8217;d match every skill and load them all.</p>



<p><strong>Include anti-examples if needed.</strong>&nbsp;If two skills might get confused (say, cost estimator and scaling advisor both relate to &#8220;spending less money&#8221;), mention what each one&nbsp;<em>doesn&#8217;t</em>&nbsp;cover in the description.</p>



<p><strong>Keep the body focused.</strong>&nbsp;A skill loaded into context costs tokens. If the body is bloated with tangential information, the model&#8217;s attention dilutes. Each skill should do one thing well.</p>



<h2 class="wp-block-heading">Your repo structure after Part 4</h2>



<p></p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: yaml; title: ; notranslate">
.github/
├── copilot-instructions.md            # Always-on: naming, provider, Zero Trust rules
├── agents/
│   ├── terraform-aca-implement.agent.md  # Specialist: writes and validates Terraform
│   └── terraform-aca-planning.agent.md   # Specialist: designs changes, creates plans
├── instructions/
│   ├── networking.instructions.md     # File-scoped: vnet.tf, nsg.tf, dns.tf
│   └── containers.instructions.md    # File-scoped: aca.tf, acr.tf
├── prompts/
│   ├── new-container-app.prompt.md   # One-click: scaffold a new Container App
│   └── terraform-review.prompt.md   # One-click: security review checklist
├── skills/
│   ├── terraform-drift-detector/
│   │   └── SKILL.md                 # On-demand: detect and resolve state drift
│   ├── aca-scaling-advisor/
│   │   └── SKILL.md                 # On-demand: design KEDA scaling rules
│   ├── azure-cost-estimator/
│   │   └── SKILL.md                 # On-demand: monthly cost breakdown
│   ├── security-posture-reviewer/
│   │   └── SKILL.md                 # On-demand: Zero Trust compliance check
│   └── acr-image-manager/
│       └── SKILL.md                 # On-demand: image tagging, promotion, cleanup
└── workflows/
    └── terraform.yml                # CI/CD: plan on PR, apply on merge
</pre></div>


<p>Each layer serves a different purpose. Always-on instructions keep every AI interaction aligned with your project&#8217;s conventions. Path-specific instructions add depth when editing specific file types. Prompt files make repetitive tasks one-click. Agents give you specialized personas. Skills provide deep expertise exactly when  and only when  you need it.</p>



<h2 class="wp-block-heading">Where to find more skills</h2>



<p>GitHub maintains the&nbsp;<a href="https://github.com/github/awesome-copilot/tree/main/skills">github/awesome-copilot</a>&nbsp;repository with 247+ community-contributed skills. For Azure infrastructure work specifically, look at&nbsp;<code>azure-architecture-autopilot</code>&nbsp;(design and deploy Azure resources from natural language) and&nbsp;<code>create-specification</code>&nbsp;(generate structured spec files optimized for AI consumption). These are production-grade starting points — fork them, trim what you don&#8217;t need, add your project&#8217;s specific context.</p>



<p>One thing worth knowing: skills in&nbsp;<code>awesome-copilot</code>&nbsp;are organized by domain rather than by project. They&#8217;re designed to be generally useful, not specifically aware of your infrastructure. The value you get from writing your own is exactly that specificity — the drift detector that knows the difference between a MEDIUM and CRITICAL change for&nbsp;<em>this</em>&nbsp;architecture, the scaling advisor that knows&nbsp;<em>this</em>&nbsp;backend is called synchronously, the cost estimator that already has&nbsp;<em>these</em>&nbsp;SKUs and region prices loaded.</p>



<h2 class="wp-block-heading">The full picture</h2>



<p>Three parts built an infrastructure platform. Part 4 built the AI layer on top of it.</p>



<p>Start with what&#8217;s always relevant: custom instructions for project-wide rules that apply to every conversation. Add path-specific instructions for file types that have specialized concerns. Create prompt files for the tasks your team runs repeatedly. Build agents for the workflows that need specialized personas and specific tool access. Then write skills for the deep expertise that&#8217;s only needed sometimes  state drift, scaling design, cost analysis, security review, image lifecycle.</p>



<p>None of these are about making Copilot smarter in the abstract. They&#8217;re about making it useful <em>for your specific project</em>, in the way that a colleague who&#8217;s worked on the codebase for six months is useful  not because they know more general programming knowledge than a new hire, but because they know what matters here.</p>



<p><em>This is Part 4 of a series on building production-ready microservices on Azure Container Apps with Terraform and GitHub Copilot.&nbsp;<a href="https://achrafbenalaya.com/2025/12/23/from-manual-terraform-to-ai-assisted-devops-building-an-azure-container-platform-part-1/">Part</a>1&nbsp;covers the secure networking baseline.&nbsp;<a href="https://achrafbenalaya.com/2026/03/01/building-a-microservices-architecture-on-azure-container-apps-with-terraform-part-2/">Part 2</a>&nbsp;covers ACR, internal backend, and frontend-backend wiring.&nbsp;<a href="https://achrafbenalaya.com/2026/03/08/from-terraform-to-autopilot-ai-assisted-automation-for-azure-container-apps-part-3/">Part 3</a>&nbsp;covers AI-assisted automation, Managed Identities, and CI/CD.</em></p>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://achrafbenalaya.com/2026/03/29/github-copilot-skills-for-terraform-5-on-demand-ai-assistants-for-azure-container-apps/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2489</post-id>	</item>
		<item>
		<title>From Terraform to Autopilot: AI-Assisted Automation for Azure Container Apps  Part 3</title>
		<link>https://achrafbenalaya.com/2026/03/08/from-terraform-to-autopilot-ai-assisted-automation-for-azure-container-apps-part-3/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=from-terraform-to-autopilot-ai-assisted-automation-for-azure-container-apps-part-3</link>
					<comments>https://achrafbenalaya.com/2026/03/08/from-terraform-to-autopilot-ai-assisted-automation-for-azure-container-apps-part-3/#respond</comments>
		
		<dc:creator><![CDATA[achraf]]></dc:creator>
		<pubDate>Sun, 08 Mar 2026 23:24:43 +0000</pubDate>
				<category><![CDATA[Azure]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Cloud]]></category>
		<category><![CDATA[copilot]]></category>
		<guid isPermaLink="false">https://achrafbenalaya.com/?p=2478</guid>

					<description><![CDATA[Intro Most infrastructure tutorials end where Part 2 left off &#160;you have working Terraform, a microservices architecture, and a mental model of how the pieces fit together. Then reality hits. Someone on the team pushes a Terraform change without running `terraform validate`. A new engineer copies an old module and hardcodes a subnet CIDR. The [&#8230;]]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">Intro</h2>



<p>Most infrastructure tutorials end where Part 2 left off &nbsp;you have working Terraform, a microservices architecture, and a mental model of how the pieces fit together. Then reality hits. Someone on the team pushes a Terraform change without running `terraform validate`. A new engineer copies an old module and hardcodes a subnet CIDR. The backend Container App gets deployed with `external_enabled = true` because someone missed it in review.</p>



<p>Part 3 is about preventing those mistakes before they happen, and automating everything else so you don&#8217;t have to think about it.</p>



<p>We&#8217;re going to wire up three things that don&#8217;t usually appear in the same blog post: GitHub Copilot custom instructions (so AI understands your infrastructure conventions), GitHub Actions pipelines (so deployments are repeatable), and Managed Identities (so there are zero credentials in your codebase). By the end, your repo will be a self-documenting, self-deploying, self-securing system.</p>



<h2 class="wp-block-heading"><strong> Where we left off</strong></h2>



<p>Quick recap. In Part 1, we built the networking foundation: a VNet, NSGs, an Application Gateway, and a single frontend Container App behind an internal load balancer. In Part 2, we expanded to microservices: an Azure Container Registry with admin access disabled, an internal-only backend Container App (`external_enabled = false`), and frontend-to-backend wiring through an environment variable.</p>



<p>The architecture works. But it has three gaps that would make any senior engineer uncomfortable in production.</p>



<p>First, the Container Apps are still pulling from Microsoft&#8217;s public registry (`mcr.microsoft.com`). Our ACR exists but nothing pushes to it and nothing pulls from it. Second, there are no credentials connecting ACR to the Container Apps &nbsp;because we disabled admin access (correctly), but haven&#8217;t set up the alternative yet. Third, deployments are manual. Every change requires someone to run `terraform plan` and `terraform apply` from their laptop.</p>



<p>Part 3 closes all three gaps.</p>



<h2 class="wp-block-heading"><strong>Teaching your AI pair-programmer to think in Terraform</strong></h2>



<p>Before writing a single line of automation code, we&#8217;re going to do something that pays compounding dividends: teach GitHub Copilot how your project works.</p>



<p>If you&#8217;ve used Copilot in a Terraform project, you&#8217;ve probably noticed it generates syntactically correct HCL that&#8217;s architecturally wrong. It&#8217;ll suggest `admin_enabled = true` on a container registry because that&#8217;s what most tutorials do. It&#8217;ll hardcode resource group names instead of using references. It&#8217;ll create subnets without delegations because it doesn&#8217;t know you&#8217;re deploying Container Apps.</p>



<p>Custom instructions fix this by giving Copilot persistent context about your project&#8217;s rules and conventions.</p>



<h2 class="wp-block-heading"><strong>The main instructions file</strong></h2>



<p>Create `.github/copilot-instructions.md` in your repo root. This file is automatically loaded by Copilot Chat in VS Code, Visual Studio, and JetBrains &nbsp;every time, for every conversation. No slash commands, no manual attachment.</p>



<p>Here&#8217;s what ours looks like for this project:</p>



<p>&#8220;`markdown</p>



<h2 class="wp-block-heading"><strong> Project Context</strong></h2>



<p>This is a 3-part Azure Container Apps infrastructure project using Terraform.</p>



<p>The architecture follows Zero Trust principles with internal-only networking.</p>



<h2 class="wp-block-heading"><strong>Architecture Rules (ALWAYS follow these)</strong></h2>



<p>&#8211; Container App Environment uses internal load balancer (`internal_load_balancer_enabled = true`)</p>



<p>&#8211; Backend Container Apps MUST use `external_enabled = false` in their ingress block</p>



<p>&#8211; Only the frontend Container App may use `external_enabled = true`</p>



<p>&#8211; All traffic from the internet enters through the Application Gateway &nbsp;never directly to a Container App</p>



<p>&#8211; Azure Container Registry MUST have `admin_enabled = false` &nbsp;use Managed Identity with AcrPull role instead</p>



<p>&#8211; Container images are referenced via `azurerm_container_registry.acr.login_server` &nbsp;never hardcode registry URLs</p>



<h2 class="wp-block-heading"><strong>Terraform Code Style</strong></h2>



<p>&#8211; Provider: `azurerm ~&gt; 4.33.0` (do NOT suggest older versions)</p>



<p>&#8211; Use resource references (e.g., `azurerm_resource_group.rg.name`) &nbsp;never hardcode names</p>



<p>&#8211; Use `random_string.suffix.result` for globally unique resource names</p>



<p>&#8211; Every resource must include `resource_group_name` and `location` from `azurerm_resource_group.rg`</p>



<p>&#8211; Avoid deprecated arguments &nbsp;check the latest azurerm provider docs</p>



<p>&#8211; Use `depends_on` only when Terraform cannot infer the dependency graph automatically</p>



<h2 class="wp-block-heading"><strong> Naming Conventions</strong></h2>



<p>&#8211; Resource groups: `rg-&lt;purpose&gt;`</p>



<p>&#8211; VNets: `vnet-&lt;purpose&gt;`</p>



<p>&#8211; Subnets: `snet-&lt;purpose&gt;`</p>



<p>&#8211; NSGs: `nsg-&lt;purpose&gt;`</p>



<p>&#8211; Container Apps: `ca-&lt;service-name&gt;`</p>



<p>&#8211; Container App Environment: `cae-&lt;purpose&gt;`</p>



<h2 class="wp-block-heading"><strong>Security Requirements</strong></h2>



<p>&#8211; No static credentials anywhere in the codebase</p>



<p>&#8211; ACR access via Managed Identity only (AcrPull role)</p>



<p>&#8211; Secrets go in Azure Key Vault &nbsp;never in Terraform variables or environment variables</p>



<p>&#8211; NSG rules follow least-privilege: allow only the specific ports and CIDRs needed</p>



<p>&#8220;`</p>



<p>This file is roughly 40 lines of Markdown. It takes five minutes to write. But now, every time someone on your team asks Copilot to &#8220;add a new Container App for the payment service,&#8221; the generated code will use internal ingress, reference the existing Container App Environment, follow your naming conventions, and avoid admin credentials.</p>



<p>The key insight here is that custom instructions aren&#8217;t about making Copilot smarter &nbsp;they&#8217;re about making it <em>*contextual*</em>. Copilot already knows Terraform syntax. It doesn&#8217;t know your project&#8217;s rules.</p>



<h2 class="wp-block-heading"><strong> Path-specific instructions for different concerns</strong></h2>



<p>The main instructions file covers project-wide rules. But Terraform projects have different zones of concern &nbsp;networking files need different guidance than application files.</p>



<p>Create `.github/instructions/` and add focused instruction files:</p>



<p><strong>`.github/instructions/networking.instructions.md`</strong>:</p>



<p>&#8220;`markdown</p>



<p>&#8212;</p>



<p>applyTo: &#8220;<strong>**/vnet.tf,**</strong>/nsg.tf,**/dns.tf&#8221;</p>



<p><strong>&#8212;</strong></p>



<p>When working on networking files:</p>



<p>&#8211; Subnets for Container Apps MUST include a delegation block for `Microsoft.App/environments`</p>



<p>&#8211; The ACA subnet requires a minimum /23 CIDR range</p>



<p>&#8211; NSG rules: always include GatewayManager and AzureLoadBalancer inbound rules on the AppGW subnet</p>



<p>&#8211; Private DNS zones must be linked to the VNet with `registration_enabled = false`</p>



<p>&#8220;`</p>



<p><strong>`.github/instructions/containers.instructions.md`</strong>:</p>



<p>&#8220;`markdown</p>



<p>&#8212;</p>



<p>applyTo: &#8220;<strong>**/aca.tf,**</strong>/acr.tf&#8221;</p>



<p><strong>&#8212;</strong></p>



<p>When working on container resources:</p>



<p>&#8211; Container Apps use `workload_profile_name = &#8220;Consumption&#8221;` unless dedicated compute is needed</p>



<p>&#8211; Backend services: `external_enabled = false`, `target_port = 80`, `transport = &#8220;auto&#8221;`</p>



<p>&#8211; Frontend services: inject backend URLs via `env` blocks, using the pattern `http://&lt;backend&gt;.ingress[0].fqdn`</p>



<p>&#8211; ACR: Standard SKU minimum. Never enable admin access. Future: connect via `registry` block with `identity = &#8220;System&#8221;`</p>



<p>&#8211; Use `min_replicas = 1` for always-on services, `min_replicas = 0` for event-driven scale-to-zero</p>



<p>&#8220;`</p>



<p>The `applyTo` glob pattern is the magic &nbsp;Copilot only loads these instructions when you&#8217;re editing files that match. So when you&#8217;re in `nsg.tf`, you get networking-specific guidance. When you&#8217;re in `aca.tf`, you get container-specific guidance. No noise, no irrelevant suggestions.</p>



<h2 class="wp-block-heading"><strong> Reusable prompt files for common tasks</strong></h2>



<p>Your team probably does the same Terraform tasks repeatedly: adding a new Container App, creating a new subnet, writing an output block. Prompt files turn these into one-click workflows.</p>



<p>Create `.github/prompts/` and add prompt files for your most common operations.</p>



<p><strong>`.github/prompts/new-container-app.prompt.md`</strong>:</p>



<p>&#8220;`markdown</p>



<p>&#8212;</p>



<p>description: &#8216;Scaffold a new Azure Container App following project conventions&#8217;</p>



<p><strong>&#8212;</strong></p>



<p>Create a new `azurerm_container_app` resource with these requirements:</p>



<p>1. Use the existing Container App Environment: `azurerm_container_app_environment.env`</p>



<p>2. Resource group and location from `azurerm_resource_group.rg`</p>



<p>3. Naming: `ca-&lt;service-name&gt;` (ask me for the service name)</p>



<p>4. Workload profile: Consumption</p>



<p>5. Ingress: ask whether this is a backend (internal) or frontend (external) service</p>



<p>6. If backend: `external_enabled = false`, explain that this is only reachable within the CAE</p>



<p>7. If frontend: add `env` block for BACKEND_API_URL pointing to the backend&#8217;s internal FQDN</p>



<p>8. Template: placeholder image from MCR, cpu 0.25, memory 0.5Gi</p>



<p>9. Add a corresponding output for the app&#8217;s FQDN in main.tf</p>



<p>Follow the naming conventions and security rules in copilot-instructions.md.</p>



<p>&#8220;`</p>



<p><strong>`.github/prompts/terraform-review.prompt.md`</strong>:</p>



<p>&#8220;`markdown</p>



<p>&#8212;</p>



<p>description: &#8216;Review Terraform code for security and best practice violations&#8217;</p>



<p><strong>&#8212;</strong></p>



<p>Review the selected Terraform code and check for:</p>



<p>1. <strong>Security</strong>: Any `admin_enabled = true` on registries? Any `external_enabled = true` on backend services? Any hardcoded credentials or secrets?</p>



<p>2. <strong>References</strong>: Are all resource names, locations, and resource groups using Terraform references (not hardcoded strings)?</p>



<p>3. <strong>Deprecated arguments</strong>: Flag any arguments deprecated in azurerm ~> 4.33.0</p>



<p>4.<strong>Naming conventions</strong>: Do resource names follow the `ca-`, `snet-`, `nsg-`, `rg-` patterns?</p>



<p>5. <strong>Dependencies</strong>: Are `depends_on` blocks only used where Terraform can&#8217;t infer the dependency?</p>



<p>6. <strong>Ingress rules</strong>: Is every backend Container App using `external_enabled = false`?</p>



<p>Output findings as a checklist with <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> or <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/274c.png" alt="❌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> for each item.</p>



<p>&#8220;`</p>



<p>In VS Code, you invoke these by typing `/` in Copilot Chat and selecting the prompt name. The prompt file becomes the instruction, and Copilot executes it in the context of your current workspace. It&#8217;s like having a senior engineer&#8217;s code review checklist that actually runs itself.</p>



<h2 class="wp-block-heading"><strong> Custom agents: giving Copilot a Terraform specialty</strong></h2>



<p>Custom instructions and prompt files make Copilot <em>*aware*</em> of your project. Custom agents take this further &nbsp;they create specialized personas with specific tools, focused expertise, and scoped permissions. Instead of one generalist Copilot, you can have a Terraform implementation agent, a planning agent, and a security review agent, each with its own toolset and behavioral rules.</p>



<h2 class="wp-block-heading"><strong> What custom agents are</strong></h2>



<p>A custom agent is a Markdown file with YAML frontmatter, stored in `.github/agents/`. The frontmatter defines the agent&#8217;s name, description, and which tools it can access. The Markdown body contains the agent&#8217;s system instructions &nbsp;its expertise, rules, and workflow. When you select a custom agent in Copilot Chat or assign it to an issue, the coding agent loads these instructions and operates as that specialized persona.</p>



<p>The file format is simple:</p>



<p>&#8220;`markdown</p>



<p>&#8212;</p>



<p>name: &#8220;Agent Name&#8221;</p>



<p>description: &#8220;What this agent does&#8221;</p>



<p>tools: [list, of, tools]</p>



<p><strong>&#8212;</strong></p>



<h2 class="wp-block-heading"><strong> Agent Instructions</strong></h2>



<p>Your behavioral rules, expertise, and workflow go here.</p>



<p>&#8220;`</p>



<p>The `tools` property is the interesting part. You can restrict an agent to read-only tools (for planning and review agents that shouldn&#8217;t modify code), or give it full access to edit files, run terminal commands, and fetch documentation. If you omit `tools` entirely, the agent gets access to everything.</p>



<h2 class="wp-block-heading"><strong> Building a Terraform implementation agent for this project</strong></h2>



<p>Let&#8217;s build a custom agent tailored to our Azure Container Apps infrastructure. Create `.github/agents/terraform-aca-implement.agent.md`:</p>



<p>&#8220;`markdown</p>



<p>&#8212;</p>



<p>name: &#8220;ACA Terraform Implementation&#8221;</p>



<p>description: &#8220;Creates and reviews Terraform for Azure Container Apps following project conventions, Zero Trust networking, and Managed Identity patterns.&#8221;</p>



<p>tools: [execute/getTerminalOutput, execute/runInTerminal, read/readFile, read/terminalLastCommand, edit/createFile, edit/editFiles, search, web/fetch, todo]</p>



<p><strong>&#8212;</strong></p>



<h2 class="wp-block-heading"><strong> Azure Container Apps Terraform Implementation Specialist</strong></h2>



<p>You are an expert in Azure Container Apps infrastructure using Terraform.</p>



<p>This project follows Zero Trust principles with internal-only networking.</p>



<h2 class="wp-block-heading"><strong>Architecture rules (ALWAYS follow these)</strong></h2>



<p>&#8211; Container App Environment uses internal load balancer (`internal_load_balancer_enabled = true`)</p>



<p>&#8211; Backend Container Apps MUST use `external_enabled = false` in their ingress block</p>



<p>&#8211; Only the frontend Container App may use `external_enabled = true`</p>



<p>&#8211; All internet traffic enters through the Application Gateway &nbsp;never directly to a Container App</p>



<p>&#8211; Azure Container Registry MUST have `admin_enabled = false` &nbsp;use Managed Identity with AcrPull role</p>



<p>&#8211; Container images are referenced via `azurerm_container_registry.acr.login_server` &nbsp;never hardcode registry URLs</p>



<p>&#8211; No static credentials anywhere &nbsp;ACR access via system-assigned Managed Identity only</p>



<h2 class="wp-block-heading"><strong> Workflow</strong></h2>



<p>1. Review existing `.tf` files using `#search` before making changes</p>



<p>2. Write Terraform configurations using `#editFiles`</p>



<p>3. Break the user&#8217;s request into actionable items using `#todos`</p>



<p>4. After creating or editing files, run: `terraform fmt`, `terraform validate`</p>



<p>5. Offer to run `terraform plan` &nbsp;but NEVER run it without explicit user confirmation</p>



<p>6. Prefer implicit dependencies over explicit `depends_on`</p>



<p>7. Remove dead code: unused variables, locals, and outputs</p>



<h2 class="wp-block-heading"><strong> Naming conventions</strong></h2>



<p>&#8211; Resource groups: `rg-&lt;purpose&gt;`</p>



<p>&#8211; VNets: `vnet-&lt;purpose&gt;`, Subnets: `snet-&lt;purpose&gt;`</p>



<p>&#8211; NSGs: `nsg-&lt;purpose&gt;`</p>



<p>&#8211; Container Apps: `ca-&lt;service-name&gt;`</p>



<p>&#8211; Container App Environment: `cae-&lt;purpose&gt;`</p>



<h2 class="wp-block-heading"><strong>Final checklist</strong></h2>



<p>&#8211; All resource names follow naming conventions and include appropriate tags</p>



<p>&#8211; No secrets or environment-specific values hardcoded</p>



<p>&#8211; AcrPull role assignments exist for every Container App with a system-assigned identity</p>



<p>&#8211; Backend services use `external_enabled = false`</p>



<p>&#8211; Provider version: `azurerm ~&gt; 4.33.0`</p>



<p>&#8211; Generated Terraform validates cleanly and passes format checks</p>



<p>&#8220;`</p>



<p>This agent encodes everything from our `copilot-instructions.md` plus implementation-specific behavior: it runs `terraform fmt` and `validate` after every edit, it tracks work with todos, it refuses to run `terraform plan` without asking first, and it checks for dead code. The `tools` list gives it file editing, terminal execution, and search &nbsp;but not destructive operations.</p>



<ul class="wp-block-list">
<li><strong> Pairing it with a planning agent</strong></li>
</ul>



<p>For larger changes &nbsp;adding a new microservice, refactoring the networking layer &nbsp;you want a separate agent that plans <em>*before*</em> anyone writes code. Create `.github/agents/terraform-aca-planning.agent.md`:</p>



<p>&#8220;`markdown</p>



<p>&#8212;</p>



<p>name: &#8220;ACA Terraform Planning&#8221;</p>



<p>description: &#8220;Creates implementation plans for Azure Container Apps infrastructure changes. Read-only &nbsp;does not modify Terraform files.&#8221;</p>



<p>tools: [read/readFile, search, web/fetch, edit/createFile, edit/editFiles, todo]</p>



<p><strong>&#8212;</strong></p>



<h2 class="wp-block-heading"><strong> Azure Container Apps Infrastructure Planner</strong></h2>



<p>You create implementation plans for Terraform changes. You do NOT write Terraform code.</p>



<h2 class="wp-block-heading"><strong> Workflow</strong></h2>



<p>1. Review existing `.tf` files and understand the current architecture</p>



<p>2. Research Azure resource requirements using `#fetch` against Microsoft docs</p>



<p>3. Write a structured plan to `.terraform-planning-files/INFRA.{goal}.md`</p>



<p>4. The plan must list every resource, its dependencies, required variables, and outputs</p>



<p>5. Break implementation into phased tasks with clear acceptance criteria</p>



<h2 class="wp-block-heading"><strong> Plan structure</strong></h2>



<p>Each plan includes: an introduction summarizing the change, a resources section with YAML blocks defining each Azure resource (kind, module/provider, variables, outputs, dependencies), and phased implementation tasks with specific file-level actions.</p>



<h2 class="wp-block-heading"><strong> Constraints</strong></h2>



<p>&#8211; Only create or modify files under `.terraform-planning-files/`</p>



<p>&#8211; Do NOT modify `.tf` files &nbsp;that&#8217;s the implementation agent&#8217;s job</p>



<p>&#8211; Always consult Microsoft docs for resource configurations</p>



<p>&#8211; Flag any changes that would affect networking (CIDR ranges, NSG rules) as requiring human review</p>



<p>&#8220;`</p>



<p>The workflow becomes: assign a planning issue to the planning agent, review the generated plan, then assign the implementation issue to the implementation agent with a reference to the plan. The implementation agent reads the plan from `.terraform-planning-files/` and executes it.</p>



<h2 class="wp-block-heading"><strong> Using agents with the coding agent</strong></h2>



<p>When you assign a GitHub issue to Copilot, you can select which custom agent handles it from a dropdown. The coding agent spins up an ephemeral GitHub Actions environment, loads the selected agent&#8217;s instructions and tools, reads your custom instructions and prompt files, makes changes, and opens a pull request.</p>



<p>For example, say your team needs a new internal microservice for notifications. You create an issue:</p>



<p><strong>Issue title:</strong> Add internal Container App for notification service</p>



<p><strong>Issue body:</strong></p>



<p>&#8220;`</p>



<p>Create a new Container App `ca-notification` in the existing Container App Environment.</p>



<p>This is a backend service &nbsp;it should only be reachable internally.</p>



<p>Use the placeholder nginx image from MCR for now.</p>



<p>CPU: 0.25, Memory: 0.5Gi, min replicas: 1.</p>



<p>Add an output for the notification service FQDN.</p>



<p>&#8220;`</p>



<p>You assign the issue to <strong>Copilot</strong> and select the <strong>ACA Terraform Implementation</strong> agent. The agent creates a `copilot/issue-42` branch, writes the resource in `aca.tf` with internal ingress, adds the identity block and `AcrPull` role assignment, runs `terraform fmt` and `terraform validate`, self-reviews its changes using Copilot code review, and opens a pull request. If you leave a comment like &#8220;@copilot also add an env block so the frontend can reach this service,&#8221; it picks up the feedback, pushes a new commit, and re-requests review.</p>



<p>Your CI pipeline runs `terraform plan` on the PR, so you can see exactly what the agent&#8217;s code would do to your infrastructure before approving.</p>



<h2 class="wp-block-heading"><strong> Where to find more agents</strong></h2>



<p>GitHub maintains a curated collection of community agents at [github/awesome-copilot](https://github.com/github/awesome-copilot/tree/main/agents) &nbsp;over 170 agent profiles covering everything from Azure infrastructure to security scanning, database administration, and code review. For Terraform specifically, look at `terraform.agent.md`, `terraform-azure-implement.agent.md`, `terraform-azure-planning.agent.md`, and `terraform-iac-reviewer.agent.md`. These are production-grade starting points that you can fork and customize for your project&#8217;s conventions.</p>



<p>The tasks where you still want a human: anything involving networking changes (CIDR ranges, NSG rules), provider version upgrades, or state-sensitive operations like resource renames. The agent doesn&#8217;t have access to your Terraform state, so it can&#8217;t predict plan output.</p>



<h2 class="wp-block-heading"><strong> Managed Identities: killing the last static credential</strong></h2>



<p>In Part 2, we created an ACR with `admin_enabled = false` and left a comment saying &#8220;connect via Managed Identity in Part 3.&#8221; Here&#8217;s that connection.</p>



<p>The idea is simple: instead of giving Container Apps a username and password to pull images from ACR, we give them a Microsoft Entra ID identity and assign it the `AcrPull` role. The identity is managed by Azure &nbsp;it&#8217;s created, rotated, and destroyed automatically. There&#8217;s nothing to store, nothing to leak.</p>



<p>Here&#8217;s the Terraform to add. First, enable system-assigned managed identity on both Container Apps by adding an `identity` block:</p>



<p>&#8220;`hcl</p>



<p>identity {</p>



<p>&nbsp; type = &#8220;SystemAssigned&#8221;</p>



<p>}</p>



<p>&#8220;`</p>



<p>Then create role assignments that grant each Container App&#8217;s identity the `AcrPull` permission on the ACR:</p>



<p>&#8220;`hcl</p>



<p>resource &#8220;azurerm_role_assignment&#8221; &#8220;frontend_acr_pull&#8221; {</p>



<p>&nbsp; scope &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;= azurerm_container_registry.acr.id</p>



<p>&nbsp; role_definition_name = &#8220;AcrPull&#8221;</p>



<p>&nbsp; principal_id &nbsp; &nbsp; &nbsp; &nbsp; = azurerm_container_app.app.identity[0].principal_id</p>



<p>}</p>



<p>resource &#8220;azurerm_role_assignment&#8221; &#8220;backend_acr_pull&#8221; {</p>



<p>&nbsp; scope &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;= azurerm_container_registry.acr.id</p>



<p>&nbsp; role_definition_name = &#8220;AcrPull&#8221;</p>



<p>&nbsp; principal_id &nbsp; &nbsp; &nbsp; &nbsp; = azurerm_container_app.backend.identity[0].principal_id</p>



<p>}</p>



<p>&#8220;`</p>



<p>Finally, update the Container App definitions to use the `registry` block instead of pulling from a public registry:</p>



<p>&#8220;`hcl</p>



<p>registry {</p>



<p>&nbsp; server &nbsp; = azurerm_container_registry.acr.login_server</p>



<p>&nbsp; identity = &#8220;System&#8221;</p>



<p>}</p>



<p>&#8220;`</p>



<p>The `identity = &#8220;System&#8221;` parameter tells Azure Container Apps to authenticate to the ACR using its own system-assigned identity. No passwords, no tokens, no environment variables. The authentication is handled entirely within the Azure control plane.</p>



<p>This also means your Container Apps will fail to start if the role assignment is missing or wrong &nbsp;which is exactly the behavior you want. Fail closed, not open. If someone accidentally removes the `AcrPull` assignment, the app doesn&#8217;t silently fall back to anonymous access &nbsp;it refuses to pull the image.</p>



<h2 class="wp-block-heading"><strong> GitHub Actions: from `git push` to production</strong></h2>



<p>The final piece is a CI/CD pipeline that runs `terraform plan` on pull requests and `terraform apply` on merge to main. We&#8217;re using OIDC (OpenID Connect) to authenticate GitHub Actions to Azure &nbsp;no service principal secrets stored in GitHub.</p>



<h2 class="wp-block-heading"><strong>Setting up workload identity federation</strong></h2>



<p>This is Microsoft&#8217;s recommended approach for authenticating external workloads  like a GitHub Actions runner  to Azure without storing any secrets. Microsoft calls it <strong>workload identity federation</strong>, and it&#8217;s built on top of OpenID Connect (OIDC).</p>



<p>Here&#8217;s what&#8217;s happening under the hood. GitHub Actions has a built-in OIDC provider at `https://token.actions.githubusercontent.com`. Every time a workflow run needs to authenticate, GitHub issues a short-lived JSON Web Token (JWT) that contains claims about the workflow &nbsp;which repository triggered it, which branch, which environment. That token is valid for minutes, not months.</p>



<p>On the Azure side, you create an app registration(or a user-assigned managed identity) in Microsoft Entra ID (formerly Azure AD) and add a federated identity credential to it. This credential tells Entra ID: &#8220;trust tokens from GitHub&#8217;s OIDC provider, but only when the subject claim matches this specific repository and branch.&#8221; The audience is set to `api://AzureADTokenExchange`.</p>



<p>At runtime, the `azure/login` action in your workflow exchanges the GitHub-issued JWT for a short-lived Azure access token via the Microsoft identity platform. The access token is scoped to your subscription and expires quickly. There&#8217;s no service principal secret, no client certificate, nothing to rotate or leak.</p>



<p>You&#8217;ll need to set up three things:</p>



<p>1. <strong>App registration in Entra ID</strong> &nbsp;go to the Microsoft Entra admin center → App registrations → New registration. This creates the identity your GitHub workflow will authenticate as. Assign it a role (like `Contributor`) on your subscription or resource group.</p>



<p>2. <strong>Federated identity credential</strong> &nbsp;on the app registration, go to Certificates &amp; secrets → Federated credentials → Add credential. Select &#8220;GitHub Actions&#8221; as the scenario. Set the organization, repository, and entity type (branch, environment, or tag). For production deployments, use the `environment` entity type pointed at a GitHub Environment with required reviewers &nbsp;this is more secure than branch-based subjects because it ensures a human approved the deployment.</p>



<p>3. <strong>GitHub repository secrets</strong> &nbsp;store three values: `AZURE_CLIENT_ID` (the app registration&#8217;s Application ID), `AZURE_TENANT_ID` (your Entra ID tenant), and `AZURE_SUBSCRIPTION_ID`. These are identifiers, not credentials. If someone exfiltrates them, they get three GUIDs that are useless without a valid OIDC token from your specific GitHub repository, branch, and environment.</p>



<h2 class="wp-block-heading"><strong> The pipeline</strong></h2>



<p>Here&#8217;s the structure of the workflow file (`.github/workflows/terraform.yml`):</p>



<p>&#8220;`yaml</p>



<p>name: &#8216;Terraform Plan &amp; Apply&#8217;</p>



<p>on:</p>



<p>&nbsp; pull_request:</p>



<p>&nbsp; &nbsp; branches: [main]</p>



<p>&nbsp; &nbsp; paths: [&#8216;**.tf&#8217;]</p>



<p>&nbsp; push:</p>



<p>&nbsp; &nbsp; branches: [main]</p>



<p>&nbsp; &nbsp; paths: [&#8216;**.tf&#8217;]</p>



<p>permissions:</p>



<p>&nbsp; id-token: write &nbsp; &nbsp;# Required for OIDC</p>



<p>&nbsp; contents: read</p>



<p>&nbsp; pull-requests: write &nbsp;# Post plan output to PR</p>



<p>concurrency:</p>



<p>&nbsp; group: terraform-${{ github.ref }}</p>



<p>&nbsp; cancel-in-progress: false</p>



<p>jobs:</p>



<p>&nbsp; plan:</p>



<p>&nbsp; &nbsp; runs-on: ubuntu-latest</p>



<p>&nbsp; &nbsp; steps:</p>



<p>&nbsp; &nbsp; &nbsp; &#8211; uses: actions/checkout@v4</p>



<p>&nbsp; &nbsp; &nbsp; &#8211; uses: hashicorp/setup-terraform@v3</p>



<p>&nbsp; &nbsp; &nbsp; &nbsp; with:</p>



<p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; terraform_version: &#8216;1.8.0&#8217;</p>



<p>&nbsp; &nbsp; &nbsp; &#8211; uses: azure/login@v2</p>



<p>&nbsp; &nbsp; &nbsp; &nbsp; with:</p>



<p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; client-id: ${{ secrets.AZURE_CLIENT_ID }}</p>



<p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; tenant-id: ${{ secrets.AZURE_TENANT_ID }}</p>



<p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}</p>



<p>&nbsp; &nbsp; &nbsp; &#8211; name: Terraform Init</p>



<p>&nbsp; &nbsp; &nbsp; &nbsp; run: terraform init</p>



<p>&nbsp; &nbsp; &nbsp; &#8211; name: Terraform Format Check</p>



<p>&nbsp; &nbsp; &nbsp; &nbsp; run: terraform fmt -check -recursive</p>



<p>&nbsp; &nbsp; &nbsp; &#8211; name: Terraform Validate</p>



<p>&nbsp; &nbsp; &nbsp; &nbsp; run: terraform validate</p>



<p>&nbsp; &nbsp; &nbsp; &#8211; name: Terraform Plan</p>



<p>&nbsp; &nbsp; &nbsp; &nbsp; run: terraform plan -out=tfplan -no-color</p>



<p>&nbsp; &nbsp; &nbsp; # Post plan summary to PR as a comment</p>



<p>&nbsp; &nbsp; &nbsp; &#8211; name: Comment Plan on PR</p>



<p>&nbsp; &nbsp; &nbsp; &nbsp; if: github.event_name == &#8216;pull_request&#8217;</p>



<p>&nbsp; &nbsp; &nbsp; &nbsp; uses: actions/github-script@v7</p>



<p>&nbsp; &nbsp; &nbsp; &nbsp; with:</p>



<p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; script: |</p>



<p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; // Post truncated plan output to PR comment</p>



<p>&nbsp; apply:</p>



<p>&nbsp; &nbsp; needs: plan</p>



<p>&nbsp; &nbsp; if: github.ref == &#8216;refs/heads/main&#8217; &amp;&amp; github.event_name == &#8216;push&#8217;</p>



<p>&nbsp; &nbsp; runs-on: ubuntu-latest</p>



<p>&nbsp; &nbsp; environment: production &nbsp;# Requires approval</p>



<p>&nbsp; &nbsp; steps:</p>



<p>&nbsp; &nbsp; &nbsp; &#8211; uses: actions/checkout@v4</p>



<p>&nbsp; &nbsp; &nbsp; &#8211; uses: hashicorp/setup-terraform@v3</p>



<p>&nbsp; &nbsp; &nbsp; &#8211; uses: azure/login@v2</p>



<p>&nbsp; &nbsp; &nbsp; &nbsp; with:</p>



<p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; client-id: ${{ secrets.AZURE_CLIENT_ID }}</p>



<p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; tenant-id: ${{ secrets.AZURE_TENANT_ID }}</p>



<p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}</p>



<p>&nbsp; &nbsp; &nbsp; &#8211; run: terraform init</p>



<p>&nbsp; &nbsp; &nbsp; &#8211; run: terraform apply -auto-approve</p>



<p>&#8220;`</p>



<p>A few design decisions worth calling out.</p>



<p>The `concurrency` block prevents two Terraform runs from executing simultaneously on the same branch. Terraform state is not designed for concurrent writes &nbsp;parallel runs will corrupt it. The `cancel-in-progress: false` setting means new runs queue instead of canceling in-flight ones, because you never want to interrupt a `terraform apply` mid-execution.</p>



<p>The `paths` filter ensures the pipeline only triggers when `.tf` files change. A README update shouldn&#8217;t trigger an infrastructure deployment.</p>



<p>The `environment: production` on the apply job means someone has to click &#8220;Approve&#8221; in the GitHub UI before `terraform apply` runs. This is your human gate &nbsp;the pipeline does the mechanical work (format, validate, plan), and a human makes the final call on whether the plan looks right.</p>



<p>This is the workload identity federation model in action &nbsp;the only values in your GitHub secrets are non-sensitive identifiers. The actual authentication happens at runtime through the OIDC token exchange described above, and every token is short-lived and scoped.</p>



<h2 class="wp-block-heading"><strong> What your repo looks like after Part 3</strong></h2>



<p>&#8220;`</p>



<p>.</p>



<p>├── .github/</p>



<p>│ &nbsp; ├── copilot-instructions.md &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# Project-wide AI context</p>



<p>│ &nbsp; ├── agents/</p>



<p>│ &nbsp; │ &nbsp; ├── terraform-aca-implement.agent.md &nbsp;# Implementation specialist</p>



<p>│ &nbsp; │ &nbsp; └── terraform-aca-planning.agent.md &nbsp; # Planning specialist</p>



<p>│ &nbsp; ├── instructions/</p>



<p>│ &nbsp; │ &nbsp; ├── networking.instructions.md &nbsp; # Path-specific: vnet, nsg, dns</p>



<p>│ &nbsp; │ &nbsp; └── containers.instructions.md &nbsp; # Path-specific: aca, acr</p>



<p>│ &nbsp; ├── prompts/</p>



<p>│ &nbsp; │ &nbsp; ├── readme.prompt.md &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # Generate README (from Part 1)</p>



<p>│ &nbsp; │ &nbsp; ├── new-container-app.prompt.md &nbsp;# Scaffold new Container App</p>



<p>│ &nbsp; │ &nbsp; └── terraform-review.prompt.md &nbsp; # Security review checklist</p>



<p>│ &nbsp; └── workflows/</p>



<p>│ &nbsp; &nbsp; &nbsp; └── terraform.yml &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# CI/CD pipeline</p>



<p>├── aca.tf &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# Frontend + Backend Container Apps (with identity blocks)</p>



<p>├── acr.tf &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# Container Registry</p>



<p>├── appg.tf &nbsp; &nbsp; &nbsp; &nbsp; # Application Gateway</p>



<p>├── dns.tf &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# Private DNS</p>



<p>├── main.tf &nbsp; &nbsp; &nbsp; &nbsp; # Outputs, Log Analytics, role assignments</p>



<p>├── nsg.tf &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;# Network Security Groups</p>



<p>├── provider.tf &nbsp; &nbsp; # Provider config</p>



<p>├── rg.tf &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; # Resource Group</p>



<p>└── vnet.tf &nbsp; &nbsp; &nbsp; &nbsp; # Virtual Network</p>



<p>&#8220;`</p>



<h2 class="wp-block-heading"><strong> The security posture, end to end</strong></h2>



<p>Let&#8217;s step back and look at what three parts of Terraform and a few config files got us.</p>



<p>The networking layer is locked down. Container Apps sit behind an internal load balancer in a delegated subnet. The only public entry is through the Application Gateway, which terminates HTTP and forwards to the frontend&#8217;s internal FQDN via private DNS.</p>



<p>The application layer follows Zero Trust. The backend is platform-isolated &nbsp;`external_enabled = false` means no load balancer rule exists, so there&#8217;s no path to misconfigure. The frontend reaches the backend via its internal FQDN, which resolves only within the Container App Environment.</p>



<p>The identity layer has zero static credentials. ACR admin is disabled. Container Apps authenticate to ACR using system-assigned Managed Identities with the `AcrPull` role. GitHub Actions authenticates to Azure using workload identity federation via OIDC &nbsp;no service principal secrets, just short-lived tokens exchanged at runtime through Microsoft Entra ID.</p>



<p>The human layer is AI-augmented. Copilot custom instructions encode your project&#8217;s security rules, so AI-generated code follows them by default. Custom agents specialize Copilot into focused personas &nbsp;a planning agent that researches and designs, an implementation agent that writes and validates code, each with scoped tools and guardrails. Prompt files automate code reviews that catch violations before they reach a PR. The CI/CD pipeline runs format, validate, and plan on every pull request, with mandatory approval before apply.</p>



<p>No admin passwords. No static credentials. No manual deployments. No AI-generated code that doesn&#8217;t understand your architecture.</p>



<p>That&#8217;s the end state of Part 3, and the end of this series.</p>



<h2 class="wp-block-heading"><strong> What to explore next</strong></h2>



<p>This project is a solid foundation, but production environments always have more knobs to turn. A few ideas worth exploring: remote state with Azure Storage and state locking, Azure Key Vault integration for application secrets (database connection strings, API keys), custom domains with managed TLS certificates on the frontend, Dapr integration for service-to-service communication (sidecars, pub/sub, state management), and monitoring with Azure Application Insights wired through the existing Log Analytics workspace.</p>



<p>If you&#8217;ve followed along through all three parts, you have a microservices platform that&#8217;s network-isolated, identity-secured, AI-assisted, and pipeline-deployed. That&#8217;s not a tutorial &nbsp;that&#8217;s a production foundation.</p>



<p>&#8212;</p>



<p><em>*This is Part 3 of a 3-part series (a few more series may be added on the future) on building production-ready microservices on Azure Container Apps with Terraform. [Part 1](</em>./README.md<em>) covers the secure networking baseline. [Part 2](</em>./blog-part-2-microservices.md<em>) covers ACR, internal backend, and frontend-backend wiring.*</em></p>
]]></content:encoded>
					
					<wfw:commentRss>https://achrafbenalaya.com/2026/03/08/from-terraform-to-autopilot-ai-assisted-automation-for-azure-container-apps-part-3/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2478</post-id>	</item>
		<item>
		<title>Building a Microservices Architecture on Azure Container Apps with Terraform Part 2</title>
		<link>https://achrafbenalaya.com/2026/03/01/building-a-microservices-architecture-on-azure-container-apps-with-terraform-part-2/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=building-a-microservices-architecture-on-azure-container-apps-with-terraform-part-2</link>
					<comments>https://achrafbenalaya.com/2026/03/01/building-a-microservices-architecture-on-azure-container-apps-with-terraform-part-2/#respond</comments>
		
		<dc:creator><![CDATA[achraf]]></dc:creator>
		<pubDate>Sun, 01 Mar 2026 22:45:46 +0000</pubDate>
				<category><![CDATA[Azure]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Cloud]]></category>
		<guid isPermaLink="false">https://achrafbenalaya.com/?p=2461</guid>

					<description><![CDATA[In Part 1, we established a secure baseline: a VNet with NSGs, centralized logging, an Application Gateway as our public entry point, and a single frontend Container App behind an internal load balancer. Everything was locked down no direct internet access to the Container App, all traffic flowing through the Application Gateway. Part 2 evolves [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p></p>



<p>In Part 1, we established a secure baseline: a VNet with NSGs, centralized logging, an Application Gateway as our public entry point, and a single frontend Container App behind an internal load balancer. Everything was locked down no direct internet access to the Container App, all traffic flowing through the Application Gateway.</p>



<p>Part 2 evolves that single-app deployment into a proper microservices architecture. We&#8217;re adding three things: a container registry to store our images, a backend API that only the frontend can reach, and the wiring between them.</p>



<p>By the end of this tutorial, the architecture looks like this:</p>



<p></p>



<p>Internet → App Gateway → Frontend ACA → (internal FQDN) → Backend ACA</p>



<p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; ↑</p>



<p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;Only reachable inside the</p>



<p>&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;Container App Environment</p>



<p></p>



<h2 class="wp-block-heading"><strong> Prerequisites</strong></h2>



<p>You need Part 1 deployed and running. Specifically, your Terraform state should contain these resources: `azurerm_container_app_environment.env`, `azurerm_container_app.app`, and `random_string.suffix`. If you&#8217;re starting fresh, deploy Part 1 first.</p>



<p>Tools required: Terraform &gt;= 1.8.0, Azure CLI, and an Azure subscription with permissions to create container registries and container apps.</p>



<p><strong>What we&#8217;re building</strong></p>



<p>Three resources, each with a specific role in the Zero Trust model:</p>



<p><strong>Azure Container Registry (ACR)</strong>  a private registry to store container images. We&#8217;re using Standard SKU with admin access disabled. No static credentials, no shared passwords. In Part 3, we&#8217;ll wire this up with Managed Identities so our Container Apps can pull images using RBAC instead of admin keys.</p>



<p><strong>Backend Container App</strong> an internal-only API service. The critical setting here is `external_enabled = false` on the ingress block. This isn&#8217;t just &#8220;not public&#8221;  it means the backend has no ingress from outside the Container App Environment. Not from the VNet, not from the Application Gateway, not from the internet. Only sibling apps running in the same environment can resolve its internal FQDN.</p>



<p><strong>Frontend Container App (updated)</strong> the existing frontend gets a new environment variable (`BACKEND_API_URL`) pointing to the backend&#8217;s internal FQDN. This is how the frontend discovers the backend at runtime, without hardcoding addresses.</p>



<h2 class="wp-block-heading"><strong>Step 1 Create the Azure Container Registry</strong></h2>



<p>Create a new file called `acr.tf` in your project root:</p>



<pre class="wp-block-code"><code>resource "azurerm_container_registry" "acr" {

  name                = "acr${random_string.suffix.result}"

  resource_group_name = azurerm_resource_group.rg.name

  location            = azurerm_resource_group.rg.location

  sku                 = "Standard"

  admin_enabled       = false

}</code></pre>



<p>A few things to note about this block.</p>



<p>The name uses the same `random_string.suffix` from Part 1, which gives us a globally unique name like `acrab3k2m`. ACR names must be 5–50 characters, alphanumeric only  our 9-character name fits comfortably.</p>



<p>We chose Standard SKU deliberately. Basic lacks support for Private Endpoints (which we&#8217;ll need in Part 3 to lock down the registry to VNet traffic only) and has limited throughput. Premium adds geo-replication and content trust, but that&#8217;s overkill for this stage.</p>



<p>Setting `admin_enabled = false` is the Zero Trust play. The admin account is a single shared username/password that never rotates and can&#8217;t be scoped. By disabling it, we force all image pulls to go through Azure RBAC. In Part 3, we&#8217;ll create a Managed Identity for each Container App and assign the `AcrPull` role  no credentials to store, rotate, or leak.</p>



<h2 class="wp-block-heading"><strong> Step 2 Add the internal backend Container App</strong></h2>



<p>In your existing `aca.tf`, add the backend resource <em>*before*</em> the frontend (since the frontend will reference it):</p>



<pre class="wp-block-code"><code>resource "azurerm_container_app" "backend" {

  name                         = "ca-backend-api"

  container_app_environment_id = azurerm_container_app_environment.env.id

  resource_group_name          = azurerm_resource_group.rg.name

  revision_mode                = "Single"

  workload_profile_name        = "Consumption"

  template {

    container {

      name   = "backend-api"

      image  = "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest"

      cpu    = 0.25

      memory = "0.5Gi"

    }

    min_replicas = 1

    max_replicas = 3

  }

  ingress {

    external_enabled = false

    target_port      = 80

    transport        = "auto"

    traffic_weight {

      latest_revision = true

      percentage      = 100

    }

  }

}</code></pre>



<p>The image is a placeholder  we&#8217;re using Microsoft&#8217;s hello-world image until Part 3, when we&#8217;ll push our own images to ACR and reference them via `${azurerm_container_registry.acr.login_server}/backend:latest`.</p>



<p>The `external_enabled = false` line is the most important setting in this entire tutorial. When set to `false`, Azure&#8217;s control plane never provisions a public-facing load balancer rule for this app. The backend gets an internal FQDN that follows this pattern:</p>



<p>&#8220;`</p>



<p>ca-backend-api.internal.&lt;container-app-environment-default-domain&gt;</p>



<p>&#8220;`</p>



<p>That FQDN resolves only within the Container App Environment&#8217;s internal DNS. If you try to `curl` it from your local machine, from a VM in the same VNet, or even from the Application Gateway  it won&#8217;t resolve. Only apps running inside the same Container App Environment can reach it. This is platform-level isolation, not just network-level.</p>



<h2 class="wp-block-heading"><strong>Step 3 Wire the frontend to the backend</strong></h2>



<p>Update the existing `azurerm_container_app.app` resource in `aca.tf`. The only change is adding an `env` block inside the `container` block:</p>



<pre class="wp-block-code"><code>resource "azurerm_container_app" "app" {

  name                         = "ca-hello-world"

  container_app_environment_id = azurerm_container_app_environment.env.id

  resource_group_name          = azurerm_resource_group.rg.name

  revision_mode                = "Single"

  workload_profile_name        = "Consumption"

  template {

    container {

      name   = "hello-world"

      image  = "mcr.microsoft.com/azuredocs/containerapps-helloworld:latest"

      cpu    = 0.25

      memory = "0.5Gi"

      env {

        name  = "BACKEND_API_URL"

        value = "http://${azurerm_container_app.backend.ingress&#91;0].fqdn}"

      }

    }

    min_replicas = 1

    max_replicas = 3

  }

  ingress {

    external_enabled = true

    target_port      = 80

    transport        = "auto"

    traffic_weight {

      latest_revision = true

      percentage      = 100

    }

  }

}</code></pre>



<p>We&#8217;re using `http://` (not `https://`) for the `BACKEND_API_URL`. This is intentional. Internal ACA-to-ACA traffic within the same environment doesn&#8217;t terminate TLS at the Envoy sidecar by default. If you sent `https://`, the request would fail certificate validation because there&#8217;s no managed certificate on the internal FQDN. Use `https://` only if you&#8217;ve configured a custom domain with a managed cert on the backend.</p>



<p>Terraform creates an implicit dependency here: the frontend resource depends on the backend resource (because it references `azurerm_container_app.backend.ingress[0].fqdn`). This means Terraform will always create the backend first, which is exactly the ordering we want.</p>



<h2 class="wp-block-heading"><strong> Step 4 Add outputs</strong></h2>



<p>In `main.tf`, add these two outputs after the existing ones:</p>



<pre class="wp-block-code"><code>output "acr_login_server" {

  value       = azurerm_container_registry.acr.login_server

  description = "ACR login server used as image prefix in container definitions"

}

output "backend_app_fqdn" {

  value       = azurerm_container_app.backend.ingress&#91;0].fqdn

  description = "Internal FQDN of the Backend Container App (resolvable only within the CAE)"

}</code></pre>



<p>The `acr_login_server` output gives you the hostname (e.g., `acrab3k2m.azurecr.io`) that you&#8217;ll use in Part 3 to tag and push images. The `backend_app_fqdn` output is useful for debugging  you can verify the internal FQDN pattern and confirm it matches what the frontend receives in its environment variable.</p>



<h2 class="wp-block-heading"><strong> Step 5  Plan and apply</strong></h2>



<p></p>



<pre class="wp-block-code"><code>terraform plan -out=tfplan

terraform apply tfplan</code></pre>



<p></p>



<p>Expected changes: 2 new resources (ACR + backend ACA), 1 modified resource (frontend ACA with new env var). The Container App Environment, Application Gateway, and all networking resources remain unchanged.</p>



<p>After apply, verify the outputs:</p>



<pre class="wp-block-code"><code>terraform output acr_login_server

terraform output backend_app_fqdn</code></pre>



<p>The `backend_app_fqdn` should look something like `ca-backend-api.internal.cae-internal-demo.westeurope.azurecontainerapps.io`.</p>



<p><strong>## Updated architecture</strong></p>



<p>Here&#8217;s how the architecture looks after Part 2:</p>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1024" height="597" src="/wp-content/uploads/2026/03/image-1024x597.png" alt="" class="wp-image-2468" srcset="/wp-content/uploads/2026/03/image-1024x597.png 1024w, /wp-content/uploads/2026/03/image-300x175.png 300w, /wp-content/uploads/2026/03/image-768x448.png 768w, /wp-content/uploads/2026/03/image-1536x896.png 1536w, /wp-content/uploads/2026/03/image-750x437.png 750w, /wp-content/uploads/2026/03/image-1140x665.png 1140w, /wp-content/uploads/2026/03/image.png 1569w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p></p>



<p><strong>What changed from Part 1</strong></p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th><strong>Component</strong></th><th><strong>Part 1</strong></th><th><strong>Part 2</strong></th></tr></thead><tbody><tr><td><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f9e9.png" alt="🧩" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Container Apps</strong></td><td>1 (frontend only)</td><td>2 (frontend + internal backend)</td></tr><tr><td><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f4e6.png" alt="📦" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Container Registry</strong></td><td>None</td><td>Standard SKU, admin disabled</td></tr><tr><td><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/2699.png" alt="⚙" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Frontend Config</strong></td><td>Static image, no env vars</td><td><code>BACKEND_API_URL</code> injected</td></tr><tr><td><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f512.png" alt="🔒" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Backend Ingress</strong></td><td>N/A</td><td><code>external_enabled = false</code> (internal only)</td></tr><tr><td><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f4e4.png" alt="📤" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Outputs</strong></td><td>5 (gateway IP, frontend FQDN, URL, static IP, DNS zone)</td><td>7 (+ ACR login server, backend FQDN)</td></tr><tr><td><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f4c1.png" alt="📁" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>New Files</strong></td><td>—</td><td><code>acr.tf</code></td></tr></tbody></table></figure>



<p></p>



<p><strong> Security posture</strong></p>



<p>Part 2 adds two important Zero Trust controls.</p>



<p>First, the backend is platform-isolated. Setting `external_enabled = false` isn&#8217;t the same as putting a deny rule in an NSG. NSG rules operate at the network layer and can be misconfigured, have priority conflicts, or get accidentally modified. The `external_enabled = false` setting is enforced by the Azure Container Apps control plane  it never provisions the external load balancer rule. There&#8217;s no network path to misconfigure.</p>



<p>Second, the ACR has no admin credentials. There&#8217;s no username/password to steal, share, or accidentally commit to a repo. When we connect ACR to the Container Apps in Part 3, it will be through Azure RBAC and Managed Identities  ephemeral, automatically rotated, least-privilege tokens.</p>



<p><strong> What&#8217;s next in Part 3</strong></p>



<p>Part 3 will close the loop with CI/CD and identity:</p>



<p>&#8211; <strong>Managed Identities</strong> : System-assigned identities on each Container App, with `AcrPull` role assignments to pull images from ACR without any credentials.</p>



<p>&#8211; <strong>GitHub Actions</strong> :  CI/CD pipelines that build, push to ACR, and update Container App revisions.</p>



<p>&#8211; <strong>Full automation</strong> : From `git push` to production deployment with no manual steps.</p>



<p>The foundation we&#8217;ve built in Parts 1 and 2 internal networking, platform-level isolation, RBAC-ready registry  makes Part 3 a matter of wiring, not rearchitecting.</p>



<p></p>



<p><em>This is Part 2 of a full series on building production-ready microservices on Azure Container Apps with Terraform. Part 1 covers the secure networking baseline.</em></p>



<p>Repo : <a href="https://github.com/achrafbenalaya/azure-workshop-aca">achrafbenalaya/azure-workshop-aca</a></p>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://achrafbenalaya.com/2026/03/01/building-a-microservices-architecture-on-azure-container-apps-with-terraform-part-2/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2461</post-id>	</item>
		<item>
		<title>From Manual Terraform to AI-Assisted DevOps: Building an Azure Container Platform (Part 1)</title>
		<link>https://achrafbenalaya.com/2025/12/23/from-manual-terraform-to-ai-assisted-devops-building-an-azure-container-platform-part-1/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=from-manual-terraform-to-ai-assisted-devops-building-an-azure-container-platform-part-1</link>
					<comments>https://achrafbenalaya.com/2025/12/23/from-manual-terraform-to-ai-assisted-devops-building-an-azure-container-platform-part-1/#respond</comments>
		
		<dc:creator><![CDATA[achraf]]></dc:creator>
		<pubDate>Tue, 23 Dec 2025 13:09:45 +0000</pubDate>
				<category><![CDATA[Azure]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Cloud]]></category>
		<category><![CDATA[Terrafrom]]></category>
		<category><![CDATA[azure]]></category>
		<category><![CDATA[terraform]]></category>
		<guid isPermaLink="false">https://achrafbenalaya.com/?p=2399</guid>

					<description><![CDATA[Introduction Infrastructure as Code (IaC) has become the backbone of modern cloud architectures. Terraform, combined with Azure services, enables us to build scalable, secure, and reproducible platforms. In this blog series, I’m starting from a real Terraform project that I initially built by hand, without AI assistance. This first part focuses on laying a solid [&#8230;]]]></description>
										<content:encoded><![CDATA[
<h4 class="wp-block-heading">Introduction</h4>



<p>Infrastructure as Code (IaC) has become the backbone of modern cloud architectures. Terraform, combined with Azure services, enables us to build scalable, secure, and reproducible platforms.</p>



<p>In this blog series, I’m starting from a <strong>real Terraform project that I initially built by hand</strong>, without AI assistance. This first part focuses on <strong>laying a solid foundation</strong>: a production-oriented Azure container infrastructure.</p>



<p>In the next parts, things get more interesting <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f680.png" alt="🚀" class="wp-smiley" style="height: 1em; max-height: 1em;" /><br>We’ll <strong>enhance this infrastructure using GitHub Copilot</strong>, exploring:</p>



<ul class="wp-block-list">
<li>Chat mode</li>



<li>Custom instructions</li>



<li>Prompt-driven infrastructure evolution</li>
</ul>



<p>This repository will be <strong>open-source</strong>, and anyone is welcome to contribute, learn, or suggest improvements.</p>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f3af.png" alt="🎯" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Goal of This Series</h2>



<p>This series has three main objectives:</p>



<ol class="wp-block-list">
<li><strong>Build a real-world Azure container architecture</strong></li>



<li><strong>Demonstrate Terraform best practices incrementally</strong></li>



<li><strong>Show how GitHub Copilot can assist cloud engineers in evolving infrastructure</strong></li>
</ol>



<p>Each article will introduce <strong>one logical improvement</strong>, keeping things practical and easy to follow.</p>



<h2 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f3d7.png" alt="🏗" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Architecture – What We’re Building (Part 1)</h2>



<p>In this first iteration, we deploy a <strong>public-facing containerized application</strong> with secure networking and observability.</p>



<h3 class="wp-block-heading">Core Components</h3>



<p>The current Terraform setup includes:</p>



<ul class="wp-block-list">
<li><strong>Azure Application Gateway (Public)</strong>
<ul class="wp-block-list">
<li>Acts as the entry point</li>



<li>Handles HTTP/HTTPS traffic</li>
</ul>
</li>



<li><strong>Azure Container Apps Environment</strong></li>



<li><strong>Azure Container App</strong>
<ul class="wp-block-list">
<li>Hosts the main application</li>
</ul>
</li>



<li><strong>Azure Log Analytics Workspace</strong>
<ul class="wp-block-list">
<li>Centralized logs and diagnostics</li>
</ul>
</li>



<li><strong>Virtual Network (VNet)</strong></li>



<li><strong>Network Security Groups (NSGs)</strong>
<ul class="wp-block-list">
<li>Network-level security controls</li>
</ul>
</li>



<li><strong>Private DNS Zone</strong>
<ul class="wp-block-list">
<li>Internal name resolution between services</li>
</ul>
</li>
</ul>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="465" src="/wp-content/uploads/2025/12/image-1024x465.png" alt="" class="wp-image-2408" srcset="/wp-content/uploads/2025/12/image-1024x465.png 1024w, /wp-content/uploads/2025/12/image-300x136.png 300w, /wp-content/uploads/2025/12/image-768x349.png 768w, /wp-content/uploads/2025/12/image-1536x698.png 1536w, /wp-content/uploads/2025/12/image-750x341.png 750w, /wp-content/uploads/2025/12/image-1140x518.png 1140w, /wp-content/uploads/2025/12/image.png 1715w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img decoding="async" width="716" height="1024" src="/wp-content/uploads/2025/12/rg-internal-aca-demo-716x1024.png" alt="" class="wp-image-2409" srcset="/wp-content/uploads/2025/12/rg-internal-aca-demo-716x1024.png 716w, /wp-content/uploads/2025/12/rg-internal-aca-demo-210x300.png 210w, /wp-content/uploads/2025/12/rg-internal-aca-demo-768x1098.png 768w, /wp-content/uploads/2025/12/rg-internal-aca-demo-1074x1536.png 1074w, /wp-content/uploads/2025/12/rg-internal-aca-demo-1432x2048.png 1432w, /wp-content/uploads/2025/12/rg-internal-aca-demo-750x1072.png 750w, /wp-content/uploads/2025/12/rg-internal-aca-demo-1140x1630.png 1140w" sizes="(max-width: 716px) 100vw, 716px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="375" src="/wp-content/uploads/2025/12/image-1-1024x375.png" alt="" class="wp-image-2411" srcset="/wp-content/uploads/2025/12/image-1-1024x375.png 1024w, /wp-content/uploads/2025/12/image-1-300x110.png 300w, /wp-content/uploads/2025/12/image-1-768x281.png 768w, /wp-content/uploads/2025/12/image-1-1536x563.png 1536w, /wp-content/uploads/2025/12/image-1-750x275.png 750w, /wp-content/uploads/2025/12/image-1-1140x418.png 1140w, /wp-content/uploads/2025/12/image-1.png 1769w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p><br><br>This design already follows <strong>production-grade principles</strong>:</p>



<ul class="wp-block-list">
<li>Network isolation</li>



<li>Centralized logging</li>



<li>Clear separation of responsibilities</li>
</ul>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f9f1.png" alt="🧱" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Why Start Without Copilot?</h3>



<p>For this first blog post, <strong>everything was written manually</strong>.</p>



<p>Why?</p>



<p>Because before using AI effectively, it’s important to:</p>



<ul class="wp-block-list">
<li>Understand the architecture</li>



<li>Control the Terraform structure</li>



<li>Define clear boundaries and responsibilities</li>
</ul>



<p>This baseline will allow us to <strong>objectively measure Copilot’s value</strong> in the next parts:</p>



<ul class="wp-block-list">
<li>Does it accelerate development?</li>



<li>Does it suggest better patterns?</li>



<li>Does it catch errors or improve readability?</li>
</ul>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f51c.png" alt="🔜" class="wp-smiley" style="height: 1em; max-height: 1em;" /> What’s Coming Next</h3>



<p>In <strong>Part 2</strong>, we’ll enhance this platform by:</p>



<ul class="wp-block-list">
<li>Adding <strong>Azure Container Registry (ACR)</strong></li>



<li>Introducing a <strong>second Container App</strong> acting as a backend API</li>



<li>Connecting frontend <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/2194.png" alt="↔" class="wp-smiley" style="height: 1em; max-height: 1em;" /> backend securely</li>



<li><strong>Using GitHub Copilot Chat</strong> to guide Terraform changes</li>
</ul>



<p>Later parts will include:</p>



<ul class="wp-block-list">
<li>Copilot custom instructions</li>



<li>Prompt files</li>



<li>Security improvements</li>



<li>CI/CD with GitHub Actions</li>



<li>Community-driven enhancements</li>
</ul>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f91d.png" alt="🤝" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Open Source &amp; Contributions</h3>



<p>This project is <strong>100% open-source</strong>.</p>



<p>If you want to:</p>



<ul class="wp-block-list">
<li>Learn Terraform on Azure</li>



<li>Experiment with GitHub Copilot</li>



<li>Contribute improvements or ideas<br><br>Url  : <a href="https://github.com/achrafbenalaya/azure-workshop-aca">achrafbenalaya/azure-workshop-aca</a></li>
</ul>



<p>You’re more than welcome to join.</p>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f4cc.png" alt="📌" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Conclusion</h3>



<p>This first part sets the foundation: a clean, functional Azure container platform built with Terraform.</p>



<p>From here on, we’ll <strong>iterate, enhance, and challenge our own work</strong>, with GitHub Copilot as a real teammate not a magic button.</p>



<p>Stay tuned for <strong>Part 2</strong>, where AI enters the game <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f916.png" alt="🤖" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/2601.png" alt="☁" class="wp-smiley" style="height: 1em; max-height: 1em;" /><br></p>



<p><br></p>
]]></content:encoded>
					
					<wfw:commentRss>https://achrafbenalaya.com/2025/12/23/from-manual-terraform-to-ai-assisted-devops-building-an-azure-container-platform-part-1/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2399</post-id>	</item>
		<item>
		<title>Build and Host an Expense Tracking MCP Server with Azure Functions</title>
		<link>https://achrafbenalaya.com/2025/11/02/build-and-host-an-expense-tracking-mcp-server-with-azure-functions/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=build-and-host-an-expense-tracking-mcp-server-with-azure-functions</link>
					<comments>https://achrafbenalaya.com/2025/11/02/build-and-host-an-expense-tracking-mcp-server-with-azure-functions/#respond</comments>
		
		<dc:creator><![CDATA[achraf]]></dc:creator>
		<pubDate>Sun, 02 Nov 2025 19:05:11 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[Azure]]></category>
		<category><![CDATA[Cloud]]></category>
		<guid isPermaLink="false">https://achrafbenalaya.com/?p=2292</guid>

					<description><![CDATA[It’s been a while since I started talking about the Model Context Protocol (MCP) how it works, how to integrate it, and how much it can simplify your workflow. I’ve been experimenting with MCP clients, exploring agent mode, even creating custom modes, and sharing everything I learn along the way. The more I dive into [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>It’s been a while since I started talking about the <strong>Model Context Protocol (MCP)</strong> how it works, how to integrate it, and how much it can simplify your workflow. I’ve been experimenting with MCP clients, exploring agent mode, even creating custom modes, and sharing everything I learn along the way. The more I dive into MCP, the more fascinated I become, and I love showing others just how powerful it is.</p>



<p>Up until now, I’ve mainly been using existing MCP servers. But at some point the real question arrives:<br><strong>How do you build your own MCP server? Where do you host it? And how do you actually use it in real projects?</strong></p>



<p>That’s exactly what led me here. I wanted to create my first MCP server from scratch. While exploring different approaches, I came across several sample repositories in <strong>Azure-samples</strong>. Since I’m a long-time <strong>C# developer</strong>, my first instinct was to build a server using <strong>Azure Functions + C#</strong>  totally in my comfort zone.</p>



<p>But this time, I made a promise to myself: <strong>step outside the comfort zone.</strong><br>And after watching many presentations by the amazing <strong>Pamela Fox</strong>, I said  why not Python for this first example?</p>



<p>So in this blog post, we’re going to build a <strong>simple expense-tracker MCP server using Python</strong>, deploy it as an <strong>Azure Function</strong>, and test it using <strong>MCP Inspector</strong> and <strong>VS Code’s MCP extension</strong>.</p>



<p>You&#8217;ll find a clear, step-by-step guide including:</p>



<p><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Cloning the repo : <a href="https://github.com/achrafbenalaya/expensetrackermcpserver">https://github.com/achrafbenalaya/expensetrackermcpserver</a><br><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Writing the server logic<br><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Creating &amp; deploying the Azure Function<br><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Testing it locally and in VS Code with MCP Inspector</p>



<p>If you enjoy this walkthrough, feel free to share it  and if you&#8217;d like to support my content, you can buy me a coffee here: <strong>buymeacoffee.com/ben2code <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/2615.png" alt="☕" class="wp-smiley" style="height: 1em; max-height: 1em;" /><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/2728.png" alt="✨" class="wp-smiley" style="height: 1em; max-height: 1em;" /></strong><br></p>



<h3 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Prerequisites to follow along</h3>



<p>To build and test this project, make sure you have:</p>



<ul class="wp-block-list">
<li><strong>VS Code</strong></li>



<li><strong>MCP Inspector</strong></li>



<li><strong>Python</strong></li>



<li><strong>Browser</strong></li>



<li><strong>Docker</strong> <em>(optional  I’ll show how to run without it)</em></li>



<li><strong>Azure account with permissions to create Azure Functions</strong></li>



<li>And of course… <strong>an internet connection <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f604.png" alt="😄" class="wp-smiley" style="height: 1em; max-height: 1em;" /></strong></li>
</ul>



<p>Alright  let’s jump right in.</p>



<p>When you walk into a car showroom, you don’t start by asking about the engine specs, compression ratio, or fuel injection system. First, you want to <strong>see the car</strong>. You look at the design, step inside, feel the seats, maybe even take it for a quick test drive. <em>Only after that</em> do you start digging into the technical details.</p>



<p>We’re going to take the exact same approach here.</p>



<p>Before diving into code, architecture, Azure Functions, or deployment pipelines, let me <strong>show you the final result</strong>  what the MCP server can actually do once it&#8217;s up and running.<br><br></p>



<p class="has-medium-font-size"><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f6e0.png" alt="🛠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Tool #1: <code>get_expenses</code></p>



<p>This first tool simply retrieves your saved expenses a clean, structured way to read your stored data through MCP. Think of it like opening your car&#8217;s dashboard to see your mileage and trip history before we start checking the engine.</p>



<p>Let’s take a look at how it works in action <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f447.png" alt="👇" class="wp-smiley" style="height: 1em; max-height: 1em;" /><br></p>


<div class="wp-block-image">
<figure class="aligncenter size-full is-resized"><img loading="lazy" decoding="async" width="763" height="673" src="/wp-content/uploads/2025/11/image.png" alt="" class="wp-image-2295" style="width:764px;height:auto" srcset="/wp-content/uploads/2025/11/image.png 763w, /wp-content/uploads/2025/11/image-300x265.png 300w, /wp-content/uploads/2025/11/image-750x662.png 750w" sizes="(max-width: 763px) 100vw, 763px" /></figure></div>


<p>Simply here i was asking for my expenses for the month of October for the year 2025 </p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="754" height="520" src="/wp-content/uploads/2025/11/image-2.png" alt="" class="wp-image-2297" srcset="/wp-content/uploads/2025/11/image-2.png 754w, /wp-content/uploads/2025/11/image-2-300x207.png 300w, /wp-content/uploads/2025/11/image-2-750x517.png 750w" sizes="(max-width: 754px) 100vw, 754px" /></figure></div>


<p></p>



<p class="has-medium-font-size"><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f6e0.png" alt="🛠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Tool #2: <code>get_budget</code></p>



<p>The second tool gives us visibility into our <strong>monthly budget</strong>.</p>



<p>For the demo, I’ve set the default budget to <strong>$1000 per month</strong>. Later, we’ll make this more flexible by storing the budget in an environment variable which means you’ll be able to adjust it per month, per environment, or even per user without touching the code.Think of this tool like checking the car’s fuel range before a road trip you need to know how far you can go before running out <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f604.png" alt="😄" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>



<p>This command simply returns your current budget value, so you always have a clear reference point before adding or reviewing expenses.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="886" height="660" src="/wp-content/uploads/2025/11/image-4.png" alt="" class="wp-image-2299" srcset="/wp-content/uploads/2025/11/image-4.png 886w, /wp-content/uploads/2025/11/image-4-300x223.png 300w, /wp-content/uploads/2025/11/image-4-768x572.png 768w, /wp-content/uploads/2025/11/image-4-750x559.png 750w" sizes="(max-width: 886px) 100vw, 886px" /></figure></div>


<p></p>



<h3 class="wp-block-heading has-medium-font-size"><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f6e0.png" alt="🛠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Tool #3: <code>get_spending_summary</code></h3>



<p>This tool gives you a <strong>summary of your spending over a specific time range</strong>.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="891" height="559" src="/wp-content/uploads/2025/11/image-6.png" alt="" class="wp-image-2301" srcset="/wp-content/uploads/2025/11/image-6.png 891w, /wp-content/uploads/2025/11/image-6-300x188.png 300w, /wp-content/uploads/2025/11/image-6-768x482.png 768w, /wp-content/uploads/2025/11/image-6-750x471.png 750w" sizes="(max-width: 891px) 100vw, 891px" /></figure></div>


<p></p>



<p class="has-medium-font-size"><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f6e0.png" alt="🛠" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Tool #4: <code>add_expense</code></p>



<p>Now that we can read our budget and existing data, it&#8217;s time for the fun part <strong>adding new expenses</strong>.</p>



<p>This tool allows you to record a new expense with all the important details: category, amount, description, and date.<br></p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="886" height="571" src="/wp-content/uploads/2025/11/image-8.png" alt="" class="wp-image-2303" srcset="/wp-content/uploads/2025/11/image-8.png 886w, /wp-content/uploads/2025/11/image-8-300x193.png 300w, /wp-content/uploads/2025/11/image-8-768x495.png 768w, /wp-content/uploads/2025/11/image-8-750x483.png 750w" sizes="(max-width: 886px) 100vw, 886px" /></figure></div>


<p>Let&#8217;s check if the record is added : <br><br></p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="886" height="335" src="/wp-content/uploads/2025/11/image-10.png" alt="" class="wp-image-2305" srcset="/wp-content/uploads/2025/11/image-10.png 886w, /wp-content/uploads/2025/11/image-10-300x113.png 300w, /wp-content/uploads/2025/11/image-10-768x290.png 768w, /wp-content/uploads/2025/11/image-10-750x284.png 750w" sizes="(max-width: 886px) 100vw, 886px" /></figure></div>


<p>But wait  where is all this data actually stored?<br>Behind the scenes, everything is saved in an <strong>Azure Storage account</strong>, inside a <strong>Blob</strong> as a CSV file. Simple, lightweight, and easy to query.</p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="1023" height="849" src="/wp-content/uploads/2025/11/image-12.png" alt="" class="wp-image-2307" srcset="/wp-content/uploads/2025/11/image-12.png 1023w, /wp-content/uploads/2025/11/image-12-300x249.png 300w, /wp-content/uploads/2025/11/image-12-768x637.png 768w, /wp-content/uploads/2025/11/image-12-750x622.png 750w" sizes="(max-width: 1023px) 100vw, 1023px" /></figure></div>


<p></p>



<p class="has-medium-font-size"><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f9f1.png" alt="🧱" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Project Overview  How We Built This MCP Server</p>



<p>Now that you&#8217;ve seen the final result, let’s open the hood and see how everything works under the engine. In this section, we&#8217;ll walk through:</p>



<ul class="wp-block-list">
<li>Where the project skeleton came from</li>



<li>What was modified and why</li>



<li>How the MCP server was implemented in Python</li>



<li>How we deployed it to Azure Functions</li>



<li>The configuration (environment variables, Storage, Blob, etc.)</li>



<li>How we tested locally and in the cloud</li>



<li>Tools used along the way (MCP Inspector, VS Code, Claude Desktop, etc.)</li>
</ul>



<p>Let&#8217;s break it down step-by-step <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f447.png" alt="👇" class="wp-smiley" style="height: 1em; max-height: 1em;" /><br></p>



<h3 class="wp-block-heading has-medium-font-size">1&#x20e3; Pulling the Base Template (Source Repo)</h3>



<p>I started by cloning the official Model Context Protocol example repository from <strong>Azure-Samples</strong>, which provides a clean starting point for building MCP servers.</p>



<p>It gave me a Python-based skeleton so I could focus on extending the logic instead of building everything from scratch.<br><br>repo url  =====&gt; <a href="https://github.com/Azure-Samples/remote-mcp-functions-python">Azure-Samples/remote-mcp-functions-python</a></p>



<p>Full functionel repo ===> https://github.com/achrafbenalaya/expensetrackermcpserver</p>



<pre class="wp-block-code"><code>git clone https://github.com/Azure-Samples/remote-mcp-functions-python.git
</code></pre>



<h3 class="wp-block-heading has-medium-font-size">2&#x20e3; Understanding the Starter Code &amp; Modifying It</h3>



<p>From the base template, I:</p>



<ul class="wp-block-list">
<li>Reviewed the MCP handler logic</li>



<li>Created new MCP tools (get_budget, get_expenses, add_expense, get_spending_summary)</li>



<li>Integrated Azure Blob Storage as the backend</li>



<li>Added CSV read/write operations</li>



<li>Structured responses to match MCP protocol expectations<br><br></li>
</ul>



<p>Basically , I built a lightweight expense-tracking service in Python backed by Azure Blob Storage  no pandas required. It handles both CSV and XLSX formats, includes reliable date and amount parsing, supports dynamic monthly budgets via environment variables, and provides simple rule-based expense categorization. Each capability is implemented as small, testable functions and exposed as MCP tools (<code>get_expenses</code>, <code>add_expense</code>, <code>get_budget_status</code>, <code>get_spending_summary</code>, <code>categorize_expense</code>).</p>



<p>I’ll first explain, in plain steps, how the existing &#8220;get all expenses&#8221; tool was added (what the method does and why). Then I’ll show a minimal example of adding a new simple &#8220;hello world&#8221; tool — both the class method and a small HTTP wrapper you can drop into an Azure Functions HTTP trigger or run locally.</p>



<h2 class="wp-block-heading">How the &#8220;get all expenses&#8221; tool was added  short, concrete steps</h2>



<p>What we added: a public method&nbsp;<code>get_expenses(month: str, category: Optional[str] = None) -&gt; List[Dict]</code>&nbsp;on&nbsp;<code>ExpenseTracker</code>.</p>



<p>Why: expose a small, testable API that lists expenses for a given month (and optional category) in a JSON-serializable format so it can be used by MCP tools, HTTP endpoints, or scripts.</p>



<p>Implementation summary :</p>



<ul class="wp-block-list">
<li>Input normalization:
<ul class="wp-block-list">
<li><code>_parse_yyyymm(month)</code>&nbsp;accepts &#8220;YYYY-MM&#8221; or &#8220;YYYYMM&#8221; and normalizes to &#8220;YYYY-MM&#8221;. This makes the interface robust to common formats.</li>
</ul>
</li>



<li>Read source of truth:
<ul class="wp-block-list">
<li>Calls&nbsp;<code>self._download_rows()</code>&nbsp;which fetches the blob (XLSX or CSV) from the configured container and returns rows as dicts. If the blob is missing,&nbsp;<code>_download_rows()</code>&nbsp;returns an empty list.</li>
</ul>
</li>



<li>Iterate and filter:
<ul class="wp-block-list">
<li>For each row, read the&nbsp;<code>Date</code>&nbsp;field. The code accepts:
<ul class="wp-block-list">
<li>Excel date objects (openpyxl returns&nbsp;<code>datetime</code>/<code>date</code>),</li>



<li><code>datetime</code>&nbsp;objects,</li>



<li>or ISO date strings &#8220;YYYY-MM-DD&#8221;.</li>
</ul>
</li>



<li>Convert/normalize to a&nbsp;<code>date</code>, build&nbsp;<code>row_month = "YYYY-MM"</code>&nbsp;and compare with the requested month.</li>



<li>If&nbsp;<code>category</code>&nbsp;is supplied, compare case-insensitively and skip non-matching rows.</li>
</ul>
</li>



<li>Normalize fields:
<ul class="wp-block-list">
<li>Ensure&nbsp;<code>Amount</code>&nbsp;is cast to&nbsp;<code>float()</code>&nbsp;(on failure fallback to 0.0).</li>



<li>Ensure&nbsp;<code>Category</code>&nbsp;and&nbsp;<code>Description</code>&nbsp;are strings.</li>
</ul>
</li>



<li>Return shape:
<ul class="wp-block-list">
<li>A list of dicts like:
<ul class="wp-block-list">
<li>{ &#8220;Date&#8221;: &#8220;YYYY-MM-DD&#8221;, &#8220;Amount&#8221;: float, &#8220;Category&#8221;: str, &#8220;Description&#8221;: str }</li>
</ul>
</li>



<li>This JSON-friendly shape is easy to serialize as an HTTP response or an MCP tool return.</li>
</ul>
</li>
</ul>



<p>Error/edge behavior:</p>



<ul class="wp-block-list">
<li>If month format invalid →&nbsp;<code>_parse_yyyymm</code>&nbsp;raises ValueError.</li>



<li>If no blob exists → returns [] (safe).</li>



<li>Bad amount strings → amount becomes 0.0 for listing (but&nbsp;<code>add_expense</code>&nbsp;raises ValueError for invalid amounts).</li>
</ul>



<p>How it becomes a “tool” (exposure options)</p>



<ul class="wp-block-list">
<li>Direct use in scripts: instantiate&nbsp;<code>ExpenseTracker()</code>&nbsp;and call&nbsp;<code>get_expenses("2025-12")</code>.</li>



<li>Wrap as HTTP endpoints (Azure Functions) where the function handler instantiates&nbsp;<code>ExpenseTracker()</code>&nbsp;and returns the list as JSON.</li>



<li>Register it as an MCP tool wrapper (the repository already contains tooling that triggers these methods)  the method contract is intentionally simple (primitive types + JSON-serializable results).<br><br>example below the get_expenses function </li>
</ul>



<pre class="wp-block-code"><code>def get_expenses(self, month: str, category: Optional&#91;str] = None) -&gt; List&#91;Dict&#91;str, Any]]:
    month = _parse_yyyymm(month)
    rows = self._download_rows()
    result = &#91;]
    for r in rows:
        d = r.get("Date")
        if not d:
            continue
        if isinstance(d, datetime):
            ddate = d.date()
        elif isinstance(d, date):
            ddate = d
        else:
            try:
                ddate = datetime.strptime(str(d), "%Y-%m-%d").date()
            except Exception:
                continue
        row_month = f"{ddate.year:04d}-{ddate.month:02d}"
        if row_month != month:
            continue
        cat = r.get("Category") or ""
        if category and str(cat).strip().lower() != category.strip().lower():
            continue
        amount = r.get("Amount", 0.0)
        try:
            amount = float(amount)
        except Exception:
            amount = 0.0
        result.append({
            "Date": ddate.isoformat(),
            "Amount": amount,
            "Category": str(cat),
            "Description": str(r.get("Description", "")),
        })
    return result</code></pre>



<p class="has-medium-font-size">Where the tool is declared ?</p>



<p>in this part we define the new tool and we call the function</p>



<pre class="wp-block-code"><code>@app.generic_trigger(
    arg_name="context",
    type="mcpToolTrigger",
    toolName="get_expenses",
    description="Retrieve expenses filtered by month and optional category.",
    toolProperties=tool_properties_get_expenses_json,
)
def get_expenses(context) -&gt; str:
    ...</code></pre>



<h2 class="wp-block-heading">Input the tool expects</h2>



<p>The function receives a single&nbsp;<code>context</code>&nbsp;parameter which is a JSON string (the trigger payload). The function parses it like this:</p>



<p>content = json.loads(context)<br>month = content.get(&#8220;arguments&#8221;, {}).get(&#8220;month&#8221;)<br>category = content.get(&#8220;arguments&#8221;, {}).get(&#8220;category&#8221;)</p>



<p>So the caller should send a JSON payload shaped like:</p>



<pre class="wp-block-code"><code>{
  "arguments": {
    "month": "2025-12",
    "category": "IT"            // optional
  }
}</code></pre>



<p class="has-medium-font-size">wrapper function : </p>



<ol class="wp-block-list">
<li>Parse&nbsp;<code>context</code>&nbsp;JSON and extract&nbsp;<code>month</code>&nbsp;and optional&nbsp;<code>category</code>.</li>



<li>Validate required&nbsp;<code>month</code>&nbsp;argument. If missing, return a JSON error object.</li>



<li>Call the underlying tool method:
<ul class="wp-block-list">
<li>Uses&nbsp;<code>_get_tracker()</code>&nbsp;to get a singleton&nbsp;<code>ExpenseTracker</code>&nbsp;instance.</li>



<li>Calls&nbsp;<code>_get_tracker().get_expenses(month=month, category=category)</code>.</li>
</ul>
</li>



<li>Wrap the result in a uniform response object and return it as JSON string:</li>
</ol>



<pre class="wp-block-code"><code>return json.dumps({
    "ok": True,
    "month": month,
    "category": category or "all",
    "count": len(result) if isinstance(result, list) else 0,
    "result": result
})</code></pre>



<h2 class="wp-block-heading has-medium-font-size">Sequence  : </h2>



<p>Client (MCP caller / script)<br>-&gt; sends JSON&nbsp;<code>context</code>&nbsp;to MCP trigger<br>-&gt;&nbsp;<code>function_app.get_expenses(context)</code>&nbsp;wrapper<br>-&gt; parses JSON, validates&nbsp;<code>month</code><br>-&gt; calls&nbsp;<code>_get_tracker().get_expenses(month, category)</code><br>-&gt;&nbsp;<code>ExpenseTracker.get_expenses(...)</code><br>-&gt;&nbsp;<code>_download_rows()</code>&nbsp;reads blob (XLSX/CSV) &lt;&#8211; Azure Blob Storage<br>&lt;- rows<br>&lt;- filtered/normalized result<br>&lt;- wrapper returns JSON string { ok, month, count, result }</p>



<p>In the repo you will find readme file that will explain full code , so no worries.</p>



<p class="has-medium-font-size">3&#x20e3; Adding Environment Variables</p>



<p>To keep settings flexible and secure, I added environment variables for:<br></p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Variable</th><th>Purpose</th></tr></thead><tbody><tr><td><code>AZURE_STORAGE_CONNECTION_STRING</code></td><td>Connect to Storage account</td></tr><tr><td><code>EXPENSES_CONTAINER_SAS_URL</code></td><td>Where expenses CSV lives : container Saas Access</td></tr><tr><td><code>EXPENSES_BLOB_NAME</code></td><td>CSV filename</td></tr><tr><td><code>EXPENSES_BLOB_SAS_URL</code></td><td>Blob Saas Access</td></tr></tbody></table></figure>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="332" src="/wp-content/uploads/2025/11/image-14-1024x332.png" alt="" class="wp-image-2326" srcset="/wp-content/uploads/2025/11/image-14-1024x332.png 1024w, /wp-content/uploads/2025/11/image-14-300x97.png 300w, /wp-content/uploads/2025/11/image-14-768x249.png 768w, /wp-content/uploads/2025/11/image-14-1536x497.png 1536w, /wp-content/uploads/2025/11/image-14-750x243.png 750w, /wp-content/uploads/2025/11/image-14-1140x369.png 1140w, /wp-content/uploads/2025/11/image-14.png 1726w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure></div>


<p class="has-medium-font-size">4&#x20e3; Local Development &amp; Testing</p>



<p>To test everything locally, I used:</p>



<p><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> VS Code : to code and run the azure function =&gt; <a href="https://code.visualstudio.com/?wt.mc_id=MVP_328341">Download here</a> <br><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Python : because we are using python for this demo<br><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Azure Functions Core Tools : <a href="https://learn.microsoft.com/en-us/azure/azure-functions/functions-run-local?wt.mc_id=MVP_328341"> Azure Functions Core Tools lets you develop and test your functions on your local computer</a><br><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> MCP Inspector (to simulate LLM connection) : <a href="https://modelcontextprotocol.io/docs/tools/inspector">MCP Inspector</a> will be used later for testing .<br>     it can be launched via the cmd :  npx @modelcontextprotocol/inspector<br><br>      Once you run it you can browse :  http://127.0.0.1:6274/#resources to see the mcp inspector home page and you can connect to your function app by inserting this link to <br>      http://localhost:7071/runtime/webhooks/mcp/sse to the URL and set <br>     </p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="428" src="/wp-content/uploads/2025/11/image-18-1024x428.png" alt="" class="wp-image-2334" srcset="/wp-content/uploads/2025/11/image-18-1024x428.png 1024w, /wp-content/uploads/2025/11/image-18-300x125.png 300w, /wp-content/uploads/2025/11/image-18-768x321.png 768w, /wp-content/uploads/2025/11/image-18-1536x641.png 1536w, /wp-content/uploads/2025/11/image-18-750x313.png 750w, /wp-content/uploads/2025/11/image-18-1140x476.png 1140w, /wp-content/uploads/2025/11/image-18.png 1892w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure></div>


<p>You can choose any tool and test it from the List Tools : <br></p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="376" src="/wp-content/uploads/2025/11/image-19-1024x376.png" alt="" class="wp-image-2336" srcset="/wp-content/uploads/2025/11/image-19-1024x376.png 1024w, /wp-content/uploads/2025/11/image-19-300x110.png 300w, /wp-content/uploads/2025/11/image-19-768x282.png 768w, /wp-content/uploads/2025/11/image-19-1536x564.png 1536w, /wp-content/uploads/2025/11/image-19-750x275.png 750w, /wp-content/uploads/2025/11/image-19-1140x419.png 1140w, /wp-content/uploads/2025/11/image-19.png 1893w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure></div>


<p><br><br><br><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> An storage emulator : An storage emulator is needed when developing azure function app in VScode :      <a href="https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite?wt.mc_id=MVP_328341">Azurite emulator for local Azure Storage development</a><br>     You can run the Azurite in VS Code&nbsp; : C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\Extensions\Microsoft\Azure Storage Emulator&gt; .\azurite.exe<br>     or Using Docker : docker run -p 10000:10000 -p 10001:10001 -p 10002:10002  mcr.microsoft.com/azure-storage/azurite<br></p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="167" src="/wp-content/uploads/2025/11/image-15-1024x167.png" alt="" class="wp-image-2330" srcset="/wp-content/uploads/2025/11/image-15-1024x167.png 1024w, /wp-content/uploads/2025/11/image-15-300x49.png 300w, /wp-content/uploads/2025/11/image-15-768x125.png 768w, /wp-content/uploads/2025/11/image-15-1536x250.png 1536w, /wp-content/uploads/2025/11/image-15-750x122.png 750w, /wp-content/uploads/2025/11/image-15-1140x185.png 1140w, /wp-content/uploads/2025/11/image-15.png 1684w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure></div>


<p class="has-text-align-center"><br></p>



<p class="has-medium-font-size">5&#x20e3; Deploying to Azure Functions</p>



<p>Once everything worked locally:</p>



<ul class="wp-block-list">
<li>Created an Azure Function App</li>



<li>Pushed the code via VS Code deployment</li>



<li>Added same environment variables in Azure portal</li>



<li>Uploaded/created the blob file for storage</li>
</ul>



<p>The simplest way to deploy an azure function via vs code is to click on it and select :  Deploy to function app  (As I&#8217;m writing this article, I&#8217;m also planning to prepare a Terraform version of this Azure Function to streamline and automate the deployment process.)<br></p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="610" src="/wp-content/uploads/2025/11/image-21-1024x610.png" alt="" class="wp-image-2339" srcset="/wp-content/uploads/2025/11/image-21-1024x610.png 1024w, /wp-content/uploads/2025/11/image-21-300x179.png 300w, /wp-content/uploads/2025/11/image-21-768x458.png 768w, /wp-content/uploads/2025/11/image-21-1536x915.png 1536w, /wp-content/uploads/2025/11/image-21-750x447.png 750w, /wp-content/uploads/2025/11/image-21-1140x679.png 1140w, /wp-content/uploads/2025/11/image-21.png 1752w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure></div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="553" src="/wp-content/uploads/2025/11/image-23-1024x553.png" alt="" class="wp-image-2341" srcset="/wp-content/uploads/2025/11/image-23-1024x553.png 1024w, /wp-content/uploads/2025/11/image-23-300x162.png 300w, /wp-content/uploads/2025/11/image-23-768x415.png 768w, /wp-content/uploads/2025/11/image-23-1536x830.png 1536w, /wp-content/uploads/2025/11/image-23-750x405.png 750w, /wp-content/uploads/2025/11/image-23-1140x616.png 1140w, /wp-content/uploads/2025/11/image-23.png 1538w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure></div>


<p>Once the app is deployed , you can see the tools by clicking on functions from the portal : <br></p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="592" src="/wp-content/uploads/2025/11/image-25-1024x592.png" alt="" class="wp-image-2343" srcset="/wp-content/uploads/2025/11/image-25-1024x592.png 1024w, /wp-content/uploads/2025/11/image-25-300x173.png 300w, /wp-content/uploads/2025/11/image-25-768x444.png 768w, /wp-content/uploads/2025/11/image-25-1536x888.png 1536w, /wp-content/uploads/2025/11/image-25-750x434.png 750w, /wp-content/uploads/2025/11/image-25-1140x659.png 1140w, /wp-content/uploads/2025/11/image-25.png 1557w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure></div>


<p>Now before we go and test on MCP inspector or vs code we need to copy the system keys to use (not the best practice for now ,but its for the demo) .<br>where to find it ? simply go to App Keys and copy the mcp_extension key and save it .<br></p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="584" src="/wp-content/uploads/2025/11/image-26-1024x584.png" alt="" class="wp-image-2346" srcset="/wp-content/uploads/2025/11/image-26-1024x584.png 1024w, /wp-content/uploads/2025/11/image-26-300x171.png 300w, /wp-content/uploads/2025/11/image-26-768x438.png 768w, /wp-content/uploads/2025/11/image-26-1536x876.png 1536w, /wp-content/uploads/2025/11/image-26-750x428.png 750w, /wp-content/uploads/2025/11/image-26-1140x650.png 1140w, /wp-content/uploads/2025/11/image-26.png 1580w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>If you&#8217;re familiar with the Model Context Protocol and how servers work with it, you know that we need an <code>mcp.json</code> file to define the MCP servers we plan to use.<br></p>



<pre class="wp-block-code"><code>{
    "inputs": &#91;
        {
            "type": "promptString",
            "id": "functions-mcp-extension-system-key",
            "description": "Azure Functions MCP Extension System Key",
            "password": true
        },
        {
            "type": "promptString",
            "id": "functionapp-name",
            "description": "Azure Functions App Name"
        }
    ],
    "servers": {
        "remote-mcp-function": {
            "type": "sse",
            "url": "https://${input:functionapp-name}.azurewebsites.net/runtime/webhooks/mcp/sse",
            "headers": {
                "x-functions-key": "${input:functions-mcp-extension-system-key}"
            }
        },
        "local-mcp-function": {
            "type": "sse",
            "url": "http://0.0.0.0:7071/runtime/webhooks/mcp/sse"
        }
    }
}</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="899" height="191" src="/wp-content/uploads/2025/11/image-28.png" alt="" class="wp-image-2348" srcset="/wp-content/uploads/2025/11/image-28.png 899w, /wp-content/uploads/2025/11/image-28-300x64.png 300w, /wp-content/uploads/2025/11/image-28-768x163.png 768w, /wp-content/uploads/2025/11/image-28-750x159.png 750w" sizes="(max-width: 899px) 100vw, 899px" /></figure></div>


<p>You can add a new server always by using the CTRL+shift +P <br></p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="760" height="171" src="/wp-content/uploads/2025/11/image-30.png" alt="" class="wp-image-2350" srcset="/wp-content/uploads/2025/11/image-30.png 760w, /wp-content/uploads/2025/11/image-30-300x68.png 300w, /wp-content/uploads/2025/11/image-30-750x169.png 750w" sizes="(max-width: 760px) 100vw, 760px" /></figure></div>


<p class="has-medium-font-size"><br>Storage account : </p>



<p class="has-small-font-size">I have uploaded a simple csvp file to a storage account on azure and i genrated SAS token for the blob and for the container of the blob<br></p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="581" src="/wp-content/uploads/2025/11/image-31-1024x581.png" alt="" class="wp-image-2352" srcset="/wp-content/uploads/2025/11/image-31-1024x581.png 1024w, /wp-content/uploads/2025/11/image-31-300x170.png 300w, /wp-content/uploads/2025/11/image-31-768x436.png 768w, /wp-content/uploads/2025/11/image-31-750x425.png 750w, /wp-content/uploads/2025/11/image-31.png 1033w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p class="has-small-font-size"><br></p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="364" src="/wp-content/uploads/2025/11/image-33-1024x364.png" alt="" class="wp-image-2354" srcset="/wp-content/uploads/2025/11/image-33-1024x364.png 1024w, /wp-content/uploads/2025/11/image-33-300x107.png 300w, /wp-content/uploads/2025/11/image-33-768x273.png 768w, /wp-content/uploads/2025/11/image-33-750x266.png 750w, /wp-content/uploads/2025/11/image-33-1140x405.png 1140w, /wp-content/uploads/2025/11/image-33.png 1225w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure></div>


<p class="has-small-font-size"></p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="760" src="/wp-content/uploads/2025/11/image-34-1024x760.png" alt="" class="wp-image-2355" srcset="/wp-content/uploads/2025/11/image-34-1024x760.png 1024w, /wp-content/uploads/2025/11/image-34-300x223.png 300w, /wp-content/uploads/2025/11/image-34-768x570.png 768w, /wp-content/uploads/2025/11/image-34-750x557.png 750w, /wp-content/uploads/2025/11/image-34-1140x846.png 1140w, /wp-content/uploads/2025/11/image-34.png 1249w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p class="has-small-font-size"><br></p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="751" src="/wp-content/uploads/2025/11/image-35-1024x751.png" alt="" class="wp-image-2357" srcset="/wp-content/uploads/2025/11/image-35-1024x751.png 1024w, /wp-content/uploads/2025/11/image-35-300x220.png 300w, /wp-content/uploads/2025/11/image-35-768x564.png 768w, /wp-content/uploads/2025/11/image-35-750x550.png 750w, /wp-content/uploads/2025/11/image-35-1140x837.png 1140w, /wp-content/uploads/2025/11/image-35.png 1240w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p class="has-small-font-size"><br><br>Generate two sas and save them because we will use them later</p>



<p class="has-medium-font-size"><br><br>6&#x20e3; Testing our MCP server</p>



<p>First lets run the MCP server in VS Code : </p>



<pre class="wp-block-code"><code>cd src
pip install -r requirements.txt
func start

</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="605" src="/wp-content/uploads/2025/11/image-37-1024x605.png" alt="" class="wp-image-2360" srcset="/wp-content/uploads/2025/11/image-37-1024x605.png 1024w, /wp-content/uploads/2025/11/image-37-300x177.png 300w, /wp-content/uploads/2025/11/image-37-768x454.png 768w, /wp-content/uploads/2025/11/image-37-750x443.png 750w, /wp-content/uploads/2025/11/image-37-1140x674.png 1140w, /wp-content/uploads/2025/11/image-37.png 1315w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure></div>


<p>you make sure you already have the mcp inspector runing and also the  <a href="https://learn.microsoft.com/en-us/azure/storage/common/storage-use-azurite?wt.mc_id=MVP_328341">Azurite emulator for local Azure Storage development</a> and now open the mcp inspector page and paste that url on the url tab : <br></p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="542" src="/wp-content/uploads/2025/11/image-41-1024x542.png" alt="" class="wp-image-2365" srcset="/wp-content/uploads/2025/11/image-41-1024x542.png 1024w, /wp-content/uploads/2025/11/image-41-300x159.png 300w, /wp-content/uploads/2025/11/image-41-768x406.png 768w, /wp-content/uploads/2025/11/image-41-1536x813.png 1536w, /wp-content/uploads/2025/11/image-41-750x397.png 750w, /wp-content/uploads/2025/11/image-41-1140x603.png 1140w, /wp-content/uploads/2025/11/image-41.png 1882w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure></div>


<p>Testing the online server : <br><br>Now that we&#8217;ve tested the local server and added our MCP server to the <code>mcp.json</code> file, there&#8217;s one final step before we test it. We need to add the environment variables — using the values we copied earlier — to the <code>local.settings.json</code> file</p>



<pre class="wp-block-code"><code>{
    "IsEncrypted": false,
    "Values": {
      "FUNCTIONS_WORKER_RUNTIME": "python",
      "AzureWebJobsStorage": "UseDevelopmentStorage=true",
      "EXPENSES_CONTAINER_SAS_URL": "EXPENSES_CONTAINER_SAS_URL",
      "EXPENSES_BLOB_NAME": "expenses.sample.csv",
      "EXPENSES_BLOB_SAS_URL": "EXPENSES_BLOB_SAS_URL"
    }
  }</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="445" src="/wp-content/uploads/2025/11/image-43-1024x445.png" alt="" class="wp-image-2369" srcset="/wp-content/uploads/2025/11/image-43-1024x445.png 1024w, /wp-content/uploads/2025/11/image-43-300x130.png 300w, /wp-content/uploads/2025/11/image-43-768x334.png 768w, /wp-content/uploads/2025/11/image-43-1536x667.png 1536w, /wp-content/uploads/2025/11/image-43-750x326.png 750w, /wp-content/uploads/2025/11/image-43-1140x495.png 1140w, /wp-content/uploads/2025/11/image-43.png 1561w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure></div>


<p>To test the setup, copy the Azure Function URL and append <code>/runtime/webhooks/mcp/sse</code> to it.<br>Then, go to <strong>Authentication</strong>, select the app key, and use it to authenticate the request.<br>Make sure to set the header name to <code>x-functions-key</code>.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="490" src="/wp-content/uploads/2025/11/image-44-1024x490.png" alt="" class="wp-image-2370" srcset="/wp-content/uploads/2025/11/image-44-1024x490.png 1024w, /wp-content/uploads/2025/11/image-44-300x144.png 300w, /wp-content/uploads/2025/11/image-44-768x368.png 768w, /wp-content/uploads/2025/11/image-44-1536x735.png 1536w, /wp-content/uploads/2025/11/image-44-750x359.png 750w, /wp-content/uploads/2025/11/image-44-1140x546.png 1140w, /wp-content/uploads/2025/11/image-44.png 1876w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p></p>



<p class="has-medium-font-size">Testing our MCP server with GitHub Copilot </p>



<p>Testing an MCP server with GitHub Copilot means configuring the server in your development environment so Copilot Chat can leverage it for richer context and enhanced capabilities.<br>we have already added the configuration in the mcp.json we should see the server running by now and we have Agent mode selected .<br></p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="323" src="/wp-content/uploads/2025/11/image-45-1024x323.png" alt="" class="wp-image-2373" srcset="/wp-content/uploads/2025/11/image-45-1024x323.png 1024w, /wp-content/uploads/2025/11/image-45-300x95.png 300w, /wp-content/uploads/2025/11/image-45-768x243.png 768w, /wp-content/uploads/2025/11/image-45-750x237.png 750w, /wp-content/uploads/2025/11/image-45-1140x360.png 1140w, /wp-content/uploads/2025/11/image-45.png 1491w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure></div>

<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="831" height="651" src="/wp-content/uploads/2025/11/image-47.png" alt="" class="wp-image-2375" srcset="/wp-content/uploads/2025/11/image-47.png 831w, /wp-content/uploads/2025/11/image-47-300x235.png 300w, /wp-content/uploads/2025/11/image-47-768x602.png 768w, /wp-content/uploads/2025/11/image-47-750x588.png 750w" sizes="(max-width: 831px) 100vw, 831px" /></figure></div>


<p>Lets try another tool  , lets add some expenses : <br></p>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="873" height="431" src="/wp-content/uploads/2025/11/image-49.png" alt="" class="wp-image-2378" srcset="/wp-content/uploads/2025/11/image-49.png 873w, /wp-content/uploads/2025/11/image-49-300x148.png 300w, /wp-content/uploads/2025/11/image-49-768x379.png 768w, /wp-content/uploads/2025/11/image-49-750x370.png 750w" sizes="(max-width: 873px) 100vw, 873px" /></figure></div>


<p class="has-medium-font-size"><br>Testing our MCP server with Claude Desktop : </p>



<p class="has-small-font-size">first we need to edit the claude_desktop_config.json under the &nbsp;C:\Users\&lt;username&gt;\AppData\Roaming\Claude<br></p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="586" src="/wp-content/uploads/2025/11/image-55-1024x586.png" alt="" class="wp-image-2386" srcset="/wp-content/uploads/2025/11/image-55-1024x586.png 1024w, /wp-content/uploads/2025/11/image-55-300x172.png 300w, /wp-content/uploads/2025/11/image-55-768x440.png 768w, /wp-content/uploads/2025/11/image-55-750x429.png 750w, /wp-content/uploads/2025/11/image-55-1140x652.png 1140w, /wp-content/uploads/2025/11/image-55.png 1508w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure></div>


<p>We need to add to the file the config below </p>



<pre class="wp-block-code"><code>{
  "mcpServers": {
    "my‑mcp": {
      "command": "npx",
      "args": &#91;
        "mcp-remote",
        "http://localhost:7071/runtime/webhooks/mcp/sse"
      ]
    }
  }
}
</code></pre>


<div class="wp-block-image">
<figure class="aligncenter size-full"><img loading="lazy" decoding="async" width="895" height="620" src="/wp-content/uploads/2025/11/image-53.png" alt="" class="wp-image-2384" srcset="/wp-content/uploads/2025/11/image-53.png 895w, /wp-content/uploads/2025/11/image-53-300x208.png 300w, /wp-content/uploads/2025/11/image-53-768x532.png 768w, /wp-content/uploads/2025/11/image-53-750x520.png 750w" sizes="(max-width: 895px) 100vw, 895px" /></figure></div>


<p>we can see our local server up and running ,its time to test</p>



<p></p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="623" src="/wp-content/uploads/2025/11/image-51-1024x623.png" alt="" class="wp-image-2382" srcset="/wp-content/uploads/2025/11/image-51-1024x623.png 1024w, /wp-content/uploads/2025/11/image-51-300x182.png 300w, /wp-content/uploads/2025/11/image-51-768x467.png 768w, /wp-content/uploads/2025/11/image-51-750x456.png 750w, /wp-content/uploads/2025/11/image-51-1140x693.png 1140w, /wp-content/uploads/2025/11/image-51.png 1380w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure></div>

<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="706" src="/wp-content/uploads/2025/11/image-57-1024x706.png" alt="" class="wp-image-2388" srcset="/wp-content/uploads/2025/11/image-57-1024x706.png 1024w, /wp-content/uploads/2025/11/image-57-300x207.png 300w, /wp-content/uploads/2025/11/image-57-768x530.png 768w, /wp-content/uploads/2025/11/image-57-750x517.png 750w, /wp-content/uploads/2025/11/image-57.png 1086w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure></div>


<h2 class="wp-block-heading">Conclusion</h2>



<p>In this guide, we walked through how to create an MCP server using <strong>Azure Functions</strong> and <strong>Python</strong>, and tested it with tools like <strong>MCP Inspector</strong>, <strong>VS Code</strong>, and <strong>Claude Desktop</strong>. By following these steps, you now have a fully functional server environment ready for further experimentation and development.</p>



<p>In our next blog post, we’ll explore how to achieve the same setup using <strong>C#</strong>, giving you an alternative approach and expanding your toolkit for building MCP servers in Azure. Stay tuned!</p>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://achrafbenalaya.com/2025/11/02/build-and-host-an-expense-tracking-mcp-server-with-azure-functions/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2292</post-id>	</item>
		<item>
		<title>Log Analytics Workspace Chaos: How We Tamed 100+ Orphaned Workspaces</title>
		<link>https://achrafbenalaya.com/2025/10/17/log-analytics-workspace-chaos-how-we-tamed-100-orphaned-workspaces/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=log-analytics-workspace-chaos-how-we-tamed-100-orphaned-workspaces</link>
					<comments>https://achrafbenalaya.com/2025/10/17/log-analytics-workspace-chaos-how-we-tamed-100-orphaned-workspaces/#respond</comments>
		
		<dc:creator><![CDATA[achraf]]></dc:creator>
		<pubDate>Fri, 17 Oct 2025 15:56:25 +0000</pubDate>
				<category><![CDATA[Azure]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[azure]]></category>
		<category><![CDATA[log analytics workspace]]></category>
		<guid isPermaLink="false">https://achrafbenalaya.com/?p=2278</guid>

					<description><![CDATA[Log Analytics Workspace Chaos: How We Tamed 100+ Orphaned Workspaces The Problem That Kept Me Up at Night Let me paint you a picture. You&#8217;re managing an Azure environment, and over the years, different teams have spun up Log Analytics workspaces like they&#8217;re going out of style. Development team needs one? Boom, new workspace. QA [&#8230;]]]></description>
										<content:encoded><![CDATA[
<h1 class="wp-block-heading">Log Analytics Workspace Chaos: How We Tamed 100+ Orphaned Workspaces</h1>



<h2 class="wp-block-heading">The Problem That Kept Me Up at Night</h2>



<p>Let me paint you a picture. You&#8217;re managing an Azure environment, and over the years, different teams have spun up Log Analytics workspaces like they&#8217;re going out of style. Development team needs one? Boom, new workspace. QA wants their own? Sure, why not. That POC project from 2022? Yeah, it probably has three workspaces nobody remembers.</p>



<p>Fast forward to today, and you&#8217;re staring at over 100 Log Analytics workspaces across multiple subscriptions. Some are actively used, some are ghost towns, and honestly? You have no idea which is which. The monthly Azure bill keeps climbing, and management is asking questions you can&#8217;t answer:</p>



<ul class="wp-block-list">
<li>&#8220;Can we delete this workspace?&#8221;</li>



<li>&#8220;What&#8217;s actually using it?&#8221;</li>



<li>&#8220;How much is this costing us?&#8221;</li>



<li>&#8220;Will anything break if we remove it?&#8221;</li>
</ul>



<p>Sound familiar? This was my reality six months ago.</p>



<h2 class="wp-block-heading">The &#8220;<mark>Just Delete It&#8221;</mark> Disaster</h2>



<p>Here&#8217;s what happened when we tried the cowboy approach. A colleague decided to clean up what looked like an unused workspace. No data in the portal, no obvious activity, seemed safe enough.</p>



<p>Three hours later, we had:</p>



<ul class="wp-block-list">
<li>Two production alerts that stopped firing (hello, missed incidents!)</li>



<li>An Application Insights of a web app resource that couldn&#8217;t send telemetry</li>



<li>Three App Services with broken diagnostic settings</li>



<li>One very angry DevOps Engineer</li>
</ul>



<p>Turns out, the workspace wasn&#8217;t empty it just <em>looked</em> empty from a casual glance. The data was there, the dependencies were real, and we learned a very expensive lesson: <strong>you can&#8217;t audit Log Analytics workspaces with your eyeballs</strong>.</p>



<h2 class="wp-block-heading">Enter the Bulk Audit Tool</h2>



<p>After that incident, I spent a weekend building something (with the help of Ai) we desperately needed: an automated auditing tool that could answer all those questions <em>before</em> we touched anything. Not just for one workspace, but for <strong>all of them</strong>.</p>



<p>This PowerShell script became our safety net. It does what should be obvious but somehow isn&#8217;t it actually <em>looks</em> at what&#8217;s in each workspace, what&#8217;s connected to it, and what would break if you deleted it.</p>



<h2 class="wp-block-heading">What This Tool Actually Does</h2>



<p>Think of it as a full health check and dependency scanner rolled into one. For each workspace, it:</p>



<h3 class="wp-block-heading">1. <strong>Discovers All Your Workspaces</strong></h3>



<p>Scans across all your subscriptions (or specific ones you target) and finds every Log Analytics workspace. No more &#8220;I didn&#8217;t know that existed&#8221; surprises.<br><br>.\BulkLAWorkspaceAudit.ps1 -AllWorkspaces -GenerateSummaryReport</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="736" src="/wp-content/uploads/2025/10/image-1024x736.png" alt="" class="wp-image-2281" srcset="/wp-content/uploads/2025/10/image-1024x736.png 1024w, /wp-content/uploads/2025/10/image-300x216.png 300w, /wp-content/uploads/2025/10/image-768x552.png 768w, /wp-content/uploads/2025/10/image-120x86.png 120w, /wp-content/uploads/2025/10/image-750x539.png 750w, /wp-content/uploads/2025/10/image-1140x819.png 1140w, /wp-content/uploads/2025/10/image.png 1262w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<h3 class="wp-block-heading">2. <strong>Analyzes Active Data Tables</strong></h3>



<p>It doesn&#8217;t just count tables it checks which ones are <em>actually receiving data</em>. That 90-day-old SecurityEvent table with zero records? The script knows it&#8217;s dead weight.<br></p>



<figure class="wp-block-image size-large is-resized"><img loading="lazy" decoding="async" width="1024" height="275" src="/wp-content/uploads/2025/10/image-1-1024x275.png" alt="" class="wp-image-2283" style="width:1091px;height:auto" srcset="/wp-content/uploads/2025/10/image-1-1024x275.png 1024w, /wp-content/uploads/2025/10/image-1-300x81.png 300w, /wp-content/uploads/2025/10/image-1-768x206.png 768w, /wp-content/uploads/2025/10/image-1-1536x413.png 1536w, /wp-content/uploads/2025/10/image-1-2048x550.png 2048w, /wp-content/uploads/2025/10/image-1-750x201.png 750w, /wp-content/uploads/2025/10/image-1-1140x306.png 1140w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<h3 class="wp-block-heading">3. <strong>Identifies Application Insights Data</strong></h3>



<p>Automatically detects if you&#8217;re dealing with a workspace-based Application Insights resource. These need special handling, and the script flags them clearly.</p>



<h3 class="wp-block-heading">4. <strong>Maps Diagnostic Settings</strong></h3>



<p>This is the big one. It scans your Azure resources (App Services, Logic Apps, Functions, VMs, SQL Servers) and tells you which ones are sending diagnostics to each workspace. This is your &#8220;oh crap, this IS being used&#8221; detector.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="136" src="/wp-content/uploads/2025/10/image-2-1024x136.png" alt="" class="wp-image-2285" srcset="/wp-content/uploads/2025/10/image-2-1024x136.png 1024w, /wp-content/uploads/2025/10/image-2-300x40.png 300w, /wp-content/uploads/2025/10/image-2-768x102.png 768w, /wp-content/uploads/2025/10/image-2-1536x204.png 1536w, /wp-content/uploads/2025/10/image-2-2048x272.png 2048w, /wp-content/uploads/2025/10/image-2-750x100.png 750w, /wp-content/uploads/2025/10/image-2-1140x152.png 1140w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p><br></p>



<h3 class="wp-block-heading">5. <strong>Finds Dependent Alerts</strong></h3>



<p>Checks for scheduled query alerts that rely on the workspace. Delete the workspace, and these alerts go silent. Not ideal for production monitoring.<br></p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="258" src="/wp-content/uploads/2025/10/image-3-1024x258.png" alt="" class="wp-image-2286" srcset="/wp-content/uploads/2025/10/image-3-1024x258.png 1024w, /wp-content/uploads/2025/10/image-3-300x75.png 300w, /wp-content/uploads/2025/10/image-3-768x193.png 768w, /wp-content/uploads/2025/10/image-3-1536x386.png 1536w, /wp-content/uploads/2025/10/image-3-2048x515.png 2048w, /wp-content/uploads/2025/10/image-3-750x189.png 750w, /wp-content/uploads/2025/10/image-3-1140x287.png 1140w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<h3 class="wp-block-heading">6. <strong>Calculates Actual Costs</strong></h3>



<p>Pulls Cost Management data to show you what each workspace has cost over the last 30 days. Real euros, real budget impact. (not working for now , working on a fix)</p>



<h3 class="wp-block-heading">7. <strong>Assigns Risk Levels</strong></h3>



<p>Based on everything it finds, it gives you a simple risk rating:<br></p>



<ul class="wp-block-list">
<li><strong>LOW</strong>: Empty or minimal usage, safe to delete</li>



<li><strong>MEDIUM</strong>: Has some data but no critical dependencies</li>



<li><strong>HIGH</strong>: Active diagnostic settings connected</li>



<li><strong>CRITICAL</strong>: Alerts would break, proceed with extreme caution<br></li>
</ul>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="123" src="/wp-content/uploads/2025/10/image-4-1024x123.png" alt="" class="wp-image-2287" srcset="/wp-content/uploads/2025/10/image-4-1024x123.png 1024w, /wp-content/uploads/2025/10/image-4-300x36.png 300w, /wp-content/uploads/2025/10/image-4-768x92.png 768w, /wp-content/uploads/2025/10/image-4-1536x184.png 1536w, /wp-content/uploads/2025/10/image-4-2048x246.png 2048w, /wp-content/uploads/2025/10/image-4-750x90.png 750w, /wp-content/uploads/2025/10/image-4-1140x137.png 1140w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<h3 class="wp-block-heading">8. <strong>Generates Beautiful HTML Reports</strong></h3>



<p>Creates individual reports for each workspace AND a summary report showing your entire estate at a glance. No more spreadsheets, no more guesswork.</p>



<h2 class="wp-block-heading">How to Use It</h2>



<h3 class="wp-block-heading">Quick Start &#8211; Audit Everything</h3>



<p>The simplest approach audit all workspaces across all your subscriptions:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
# Login first
Connect-AzAccount

# Audit all workspaces and generate a summary
.\BulkLAWorkspaceAudit.ps1 -AllWorkspaces -GenerateSummaryReport

</pre></div>


<p>This creates an output folder with individual reports for each workspace plus a master summary. Open the summary HTML in your browser, and you&#8217;ll immediately see which workspaces are high-risk vs. safe to delete.</p>



<h3 class="wp-block-heading">Target Specific Subscriptions</h3>



<p>If you only want to audit certain subscriptions (smart move for large enterprises):</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
.\BulkLAWorkspaceAudit.ps1 `
    -AllWorkspaces `
    -SubscriptionIds @(&quot;sub-id-1&quot;, &quot;sub-id-2&quot;, &quot;sub-id-3&quot;) `
    -GenerateSummaryReport

</pre></div>


<h3 class="wp-block-heading">Single Workspace Deep Dive</h3>



<p>Need to investigate one specific workspace before making a decision?</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
.\BulkLAWorkspaceAudit.ps1 `
    -WorkspaceName &quot;production-logs&quot; `
    -ResourceGroupName &quot;prod-monitoring-rg&quot;

</pre></div>


<h3 class="wp-block-heading">Adjust the Analysis Window</h3>



<p>By default, it looks at the last 30 days. But maybe you want to be more aggressive:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
# Only check last 7 days (more aggressive cleanup)
.\BulkLAWorkspaceAudit.ps1 -AllWorkspaces -DaysBack 7

# Or more conservative - check last 90 days
.\BulkLAWorkspaceAudit.ps1 -AllWorkspaces -DaysBack 90

</pre></div>


<h2 class="wp-block-heading">Real-World Use Cases</h2>



<h3 class="wp-block-heading">Use Case 1: The Big Spring Cleaning</h3>



<p><strong>Scenario</strong>: Your CFO wants to cut cloud costs by 15%. You need to identify optimization opportunities fast.</p>



<p><strong>Solution</strong>: Run the bulk audit with <code>-AllWorkspaces</code>. Sort the summary report by cost. You&#8217;ll immediately see:</p>



<ul class="wp-block-list">
<li>Expensive workspaces with LOW risk (easy wins!)</li>



<li>Workspaces collecting data nobody looks at</li>



<li>Duplicate workspaces serving the same purpose</li>
</ul>



<p>In our case, we found 23 workspaces marked LOW or MEDIUM risk that were costing us €8,000/month. Easy decisions.</p>



<h3 class="wp-block-heading">Use Case 2: Pre-Migration Assessment</h3>



<p><strong>Scenario</strong>: You&#8217;re consolidating multiple workspaces into a centralized logging strategy.</p>



<p><strong>Solution</strong>: Audit all current workspaces to understand:</p>



<ul class="wp-block-list">
<li>Which ones have active Application Insights data (need special handling)</li>



<li>What diagnostic settings will need to be reconfigured</li>



<li>Which alerts need to be migrated to the new workspace</li>
</ul>



<p>The script&#8217;s diagnostic settings mapping becomes your migration checklist.</p>



<h3 class="wp-block-heading">Use Case 3: Compliance Audit</h3>



<p><strong>Scenario</strong>: Security team needs to know which workspaces contain what type of data and who&#8217;s using them.</p>



<p><strong>Solution</strong>: Run the audit and export the data. The reports show:</p>



<ul class="wp-block-list">
<li>Table names (SecurityEvent, Syslog, etc. = security-relevant)</li>



<li>Resource types sending data (helps identify data owners)</li>



<li>Active vs. inactive workspaces (data retention compliance)</li>
</ul>



<h3 class="wp-block-heading">Use Case 4: Inheriting an Unknown Azure Environment</h3>



<p><strong>Scenario</strong>: You&#8217;re acquiring another company&#8217;s Azure environment. You need to understand their Log Analytics setup.</p>



<p><strong>Solution</strong>: Get contributor access to their subscriptions, run the audit. Within hours, you know:</p>



<ul class="wp-block-list">
<li>Total workspace footprint</li>



<li>Critical dependencies</li>



<li>Cost implications</li>



<li>Technical debt (orphaned workspaces)</li>
</ul>



<h2 class="wp-block-heading">The Results: Our Success Story</h2>



<p>After implementing this tool, here&#8217;s what changed for us:</p>



<p><strong>Before the audit:</strong></p>



<ul class="wp-block-list">
<li>127 Log Analytics workspaces</li>



<li>~€15,000/month in Log Analytics costs</li>



<li>Zero visibility into usage</li>



<li>Afraid to delete anything</li>
</ul>



<p><strong>After the audit + cleanup:</strong></p>



<ul class="wp-block-list">
<li>34 workspaces (73% reduction!)</li>



<li>~€4,200/month in costs (72% savings!)</li>



<li>Complete documentation of what each workspace does</li>



<li>Confident decision-making process</li>
</ul>



<p>But the real win? <strong>No production incidents during cleanup.</strong> Every workspace we deleted was marked LOW risk, and we had the data to back up our decisions.</p>



<h2 class="wp-block-heading">Pro Tips from the Trenches</h2>



<p><strong>1. Start with a Read-Only Run</strong> Your first audit is pure reconnaissance. Don&#8217;t delete anything yet. Just generate the reports and share them with the relevant teams. You&#8217;ll learn things about your environment you didn&#8217;t know.</p>



<p><strong>2. Use the Risk Levels as Guidelines, Not Gospel</strong> A HIGH risk workspace isn&#8217;t necessarily &#8220;don&#8217;t touch it ever.&#8221; It means &#8220;talk to the teams first.&#8221; We&#8217;ve deleted HIGH risk workspaces after confirming with owners that the diagnostic settings were legacy cruft.</p>



<p><strong>3. Schedule Regular Audits</strong> We run this quarterly now. New workspaces appear, old ones become obsolete. Make it part of your regular housekeeping.</p>



<p><strong>4. Export the Data</strong> The HTML reports are great for humans, but keep the raw data. We pipe the results into our CMDB to track workspace lifecycle.</p>



<p><strong>5. Cost Threshold Alerts</strong> We added a second pass: any workspace costing more than €200/month gets flagged for review, regardless of risk level. Sometimes expensive LOW-risk workspaces are still worth investigating.</p>



<h2 class="wp-block-heading">What&#8217;s Next?</h2>



<p>We&#8217;re continuously improving this tool. Current wishlist includes:</p>



<ul class="wp-block-list">
<li>Workspace-to-workspace data flow mapping</li>



<li>Retention policy analysis and recommendations</li>



<li>Automated deletion with approval workflows</li>



<li>Integration with Azure DevOps for change tracking</li>
</ul>



<p>But honestly? Even in its current form, this tool has paid for itself a hundred times over.</p>



<h2 class="wp-block-heading">The Bottom Line</h2>



<p>If you&#8217;re managing more than a handful of Log Analytics workspaces, you need systematic auditing. Manual reviews don&#8217;t scale, and &#8220;it looks unused&#8221; isn&#8217;t a strategy.</p>



<p>This tool isn&#8217;t fancy. It&#8217;s not AI-powered or blockchain-enabled (thank god). It&#8217;s just a well-crafted PowerShell script that does the boring, important work of actually checking what&#8217;s in your environment.</p>



<p>And sometimes, that&#8217;s exactly what you need.<br><br>LInk : <a href="https://github.com/achrafbenalaya/achrafbenalaya.com/blob/main/Log_Analytics_Workspace_report/BulkLAWorkspaceAudit.ps1">achrafbenalaya.com/Log_Analytics_Workspace_report/BulkLAWorkspaceAudit.ps1 at main · achrafbenalaya/achrafbenalaya.com</a><br><br>the link for the script : <a href="https://github.com/achrafbenalaya/achrafbenalaya.com/blob/main/Log_Analytics_Workspace_report/BulkLAWorkspaceAudit.ps1"> </a>Tap_Link_here</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p><strong>Got questions? Found this useful?</strong> I&#8217;d love to hear about your Log Analytics cleanup adventures. What worked? What disasters did you narrowly avoid? Drop a comment or reach out.</p>



<p><strong>Want to contribute?</strong> The script is designed to be extended. If you add cool features (better cost analysis, workspace recommendations, etc.), share them back with the community.</p>



<p>Remember: In cloud cost management, knowledge is literally money. Every workspace you can confidently delete is savings you can track. And every CRITICAL workspace you DON&#8217;T delete accidentally is a disaster you avoided.</p>



<p>Happy auditing! <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f50d.png" alt="🔍" class="wp-smiley" style="height: 1em; max-height: 1em;" /></p>
]]></content:encoded>
					
					<wfw:commentRss>https://achrafbenalaya.com/2025/10/17/log-analytics-workspace-chaos-how-we-tamed-100-orphaned-workspaces/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2278</post-id>	</item>
		<item>
		<title>Honored to be recognized as a Microsoft Azure MVP for 2025-2026</title>
		<link>https://achrafbenalaya.com/2025/07/20/honored-to-be-recognized-as-a-microsoft-azure-mvp-for-2025-2026/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=honored-to-be-recognized-as-a-microsoft-azure-mvp-for-2025-2026</link>
					<comments>https://achrafbenalaya.com/2025/07/20/honored-to-be-recognized-as-a-microsoft-azure-mvp-for-2025-2026/#respond</comments>
		
		<dc:creator><![CDATA[achraf]]></dc:creator>
		<pubDate>Sun, 20 Jul 2025 10:04:25 +0000</pubDate>
				<category><![CDATA[Azure]]></category>
		<category><![CDATA[Cloud]]></category>
		<category><![CDATA[azure]]></category>
		<guid isPermaLink="false">https://achrafbenalaya.com/?p=2246</guid>

					<description><![CDATA[I&#8217;m excited to share that I&#8217;ve been renewed as a Microsoft Most Valuable Professional (MVP) for the 2025–2026 award year for the second year in a row! Even more special, I’ve been recognized in two areas that are deeply connected to both my passion and the work I do every day: It’s an honor to [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>I&#8217;m excited to share that I&#8217;ve been renewed as a Microsoft Most Valuable Professional (MVP) for the 2025–2026 award year for the second year in a row!</p>



<p>Even more special, I’ve been recognized in two areas that are deeply connected to both my passion and the work I do every day:</p>



<ul class="wp-block-list">
<li>Azure Compute Infrastructure</li>



<li>Azure Infrastructure as Code</li>
</ul>



<p>It’s an honor to be part of this amazing community again. I’m incredibly grateful for the recognition and even more motivated to keep learning, sharing, and giving back to the tech community that’s been such a big part of my journey.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="589" src="/wp-content/uploads/2025/07/image-1024x589.png" alt="" class="wp-image-2248" srcset="/wp-content/uploads/2025/07/image-1024x589.png 1024w, /wp-content/uploads/2025/07/image-300x173.png 300w, /wp-content/uploads/2025/07/image-768x442.png 768w, /wp-content/uploads/2025/07/image-750x431.png 750w, /wp-content/uploads/2025/07/image-1140x656.png 1140w, /wp-content/uploads/2025/07/image.png 1485w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>Receiving the Microsoft MVP award again is incredibly meaningful to me  not just as a recognition, but as a reflection of the journey so far.</p>



<p>Over the past year, I’ve had the chance to share knowledge through community talks, open-source contributions, technical writing, and one-on-one support  whether it’s helping someone troubleshoot an issue or guiding teams through complex cloud architectures. I’ve worked with a wide range of people from students and early-career devs to experienced architects  all united by a shared passion for learning and building.</p>



<p>The Azure ecosystem continues to grow and shift rapidly, and I’ve been fortunate to be hands-on with real-world solutions designing and delivering infrastructure using IaaS, PaaS, and Infrastructure as Code, while helping organizations automate and scale more effectively.</p>



<p>Being part of the MVP community is something I value deeply. It’s full of incredibly talented individuals who are generous with their time, knowledge, and experience. This program has opened doors to collaborate globally, speak at inspiring events, and continue growing both personally and professionally.</p>



<p>I’m excited for what’s ahead and grateful to continue being part of this journey.</p>



<h1 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/2764.png" alt="❤" class="wp-smiley" style="height: 1em; max-height: 1em;" /> community </h1>



<p><br><br>A heartfelt thank you to the incredible global tech community and to Microsoft for this continued recognition and support.<br>I&#8217;m constantly inspired by the passion, generosity, and collaboration I see every day  whether it’s at events, in open-source projects, or in online conversations.<br>Being part of this journey with such talented and driven individuals is truly an honor.<br>I’m grateful for the opportunities, the learning, and most of all  the people.<br>Here’s to another amazing year of sharing, growing, and building together!<br></p>



<h1 class="wp-block-heading"><img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f463.png" alt="👣" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Next Steps for me </h1>



<p><br><br>Azure keeps evolving and so will my contributions to the community. In the coming year, I’m excited to dive even deeper and share more through:</p>



<ul class="wp-block-list">
<li>Hands-on content around PaaS services,IAC and Azure AI Foundry</li>



<li>New talks and workshops at both local and international events</li>



<li>Ongoing open-source contributions and technical blog posts</li>



<li>Real-world client projects focused on scalable, secure Azure solutions</li>
</ul>



<p>I’m always open to new ideas, questions, or collaborations feel free to reach out via LinkedIn : <a href="https://www.linkedin.com/in/achrafbenalaya/">(1) Achraf Ben Alaya | LinkedIn</a>  or drop me a message anytime.</p>



<p>Thanks for reading and supporting! If you’ve enjoyed the content and want to help fuel more of it ! <img src="https://s.w.org/images/core/emoji/15.0.3/72x72/1f60a.png" alt="😊" class="wp-smiley" style="height: 1em; max-height: 1em;" /><br></p>



<figure class="wp-block-image size-large is-resized"><a href="https://buymeacoffee.com/ben2code" target="_blank"><img loading="lazy" decoding="async" width="1024" height="287" src="/wp-content/uploads/2025/07/bmc-button-1-1024x287.png" alt="" class="wp-image-2252" style="width:149px;height:auto" srcset="/wp-content/uploads/2025/07/bmc-button-1-1024x287.png 1024w, /wp-content/uploads/2025/07/bmc-button-1-300x84.png 300w, /wp-content/uploads/2025/07/bmc-button-1-768x216.png 768w, /wp-content/uploads/2025/07/bmc-button-1-750x211.png 750w, /wp-content/uploads/2025/07/bmc-button-1.png 1090w" sizes="(max-width: 1024px) 100vw, 1024px" /></a></figure>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://achrafbenalaya.com/2025/07/20/honored-to-be-recognized-as-a-microsoft-azure-mvp-for-2025-2026/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2246</post-id>	</item>
		<item>
		<title>Step-by-Step Guide: Azure Front Door + Storage Account Static Website + Custom Domain with Terraform</title>
		<link>https://achrafbenalaya.com/2025/03/11/step-by-step-guide-azure-front-door-storage-account-static-website-custom-domain-with-terraform/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=step-by-step-guide-azure-front-door-storage-account-static-website-custom-domain-with-terraform</link>
					<comments>https://achrafbenalaya.com/2025/03/11/step-by-step-guide-azure-front-door-storage-account-static-website-custom-domain-with-terraform/#respond</comments>
		
		<dc:creator><![CDATA[achraf]]></dc:creator>
		<pubDate>Tue, 11 Mar 2025 11:11:30 +0000</pubDate>
				<category><![CDATA[Azure]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Cloud]]></category>
		<category><![CDATA[azure]]></category>
		<guid isPermaLink="false">https://achrafbenalaya.com/?p=2166</guid>

					<description><![CDATA[In this post, we will use Terraform to deploy a static website on Azure, leveraging Azure Storage and Azure Front Door. To keep things simple, I will be deploying everything from my local machine. However, in a real-world scenario, I would store the Terraform state in an Azure Storage Account, create a service principal (SPN), [&#8230;]]]></description>
										<content:encoded><![CDATA[
<h1 class="wp-block-heading"></h1>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="416" src="/wp-content/uploads/2025/03/wenb-1024x416.png" alt="" class="wp-image-2192" srcset="/wp-content/uploads/2025/03/wenb-1024x416.png 1024w, /wp-content/uploads/2025/03/wenb-300x122.png 300w, /wp-content/uploads/2025/03/wenb-768x312.png 768w, /wp-content/uploads/2025/03/wenb-750x305.png 750w, /wp-content/uploads/2025/03/wenb-1140x463.png 1140w, /wp-content/uploads/2025/03/wenb.png 1494w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>In this post, we will use Terraform to deploy a static website on Azure, leveraging Azure Storage and Azure Front Door. To keep things simple, I will be deploying everything from my local machine. However, in a real-world scenario, I would store the Terraform state in an Azure Storage Account, create a service principal (SPN), and set up federation with my GitHub repository where my code is hosted. But for now, the goal is to demonstrate how to set up Azure Front Door with an Azure Storage Account that has static website hosting enabled.</p>



<h2 class="wp-block-heading">Step 1: Create the Storage Account with Terraform</h2>



<p>The first step is to create a resource group and an Azure Storage Account. For redundancy and performance, I will use <strong>Read-Access Geo-Redundant Storage (RAGRS)</strong>. This setup ensures that if the primary region becomes unavailable, the data can still be accessed from the secondary region, improving availability.</p>



<p>To learn more about RAGRS, check out <a href="https://learn.microsoft.com/en-us/azure/storage?wt.mc_id=MVP_328341">Microsoft’s documentation</a>.</p>



<h3 class="wp-block-heading">Building the First Part of the Infrastructure</h3>



<pre class="wp-block-code"><code>resource "azurerm_resource_group" "rg_demo_static_website" {
  name     = "rg-demo-static-website-002"
  location = "francecentral" # Update with the actual location of your resource group
  tags = {
    environment = "Dev"
  }
}


# This resource creates an Azure Storage Account with the specified configurations.
# - `name`: The name of the storage account.
# - `resource_group_name`: The name of the resource group where the storage account will be created.
# - `location`: The location/region where the storage account will be created.
# - `account_tier`: The performance tier of the storage account (Standard or Premium).
# - `account_replication_type`: The replication strategy for the storage account (e.g., LRS, GRS, RAGRS, ZRS).
# - `account_kind`: The kind of storage account (e.g., Storage, StorageV2, BlobStorage).
# - `infrastructure_encryption_enabled`: Whether infrastructure encryption is enabled for the storage account.
# - `tags`: A map of tags to assign to the storage account.

# This resource configures a static website for the specified Azure Storage Account.
# - `storage_account_id`: The ID of the storage account to enable the static website on.
# - `index_document`: The name of the index document for the static website.
# - `error_404_document`: (Optional) The name of the custom 404 error document for the static website.

resource "azurerm_storage_account" "storage_staticwebtorageachraf002" {
  name                              = "staticwebtorageachraf002"
  resource_group_name               = azurerm_resource_group.rg_demo_static_website.name
  location                          = azurerm_resource_group.rg_demo_static_website.location
  account_tier                      = "Standard"
  account_replication_type          = "RAGRS"
  account_kind                      = "StorageV2"
  infrastructure_encryption_enabled = true
  tags = {
    environment = "Dev"
  }
}

resource "azurerm_storage_account_static_website" "static_website02" {
  storage_account_id = azurerm_storage_account.storage_staticwebtorageachraf002.id
  #error_404_document = "customnotfound.html"
  index_document = "index.html"
}

</code></pre>



<p>Once the Terraform deployment is complete, our storage account is created with <strong>static website hosting enabled</strong>. Azure automatically creates a new container called <strong>$web</strong>, where we will store our website files. For now, I will place a simple <code>index.html</code> file in this folder.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="547" src="/wp-content/uploads/2025/03/1.0-1-1024x547.png" alt="" class="wp-image-2180" srcset="/wp-content/uploads/2025/03/1.0-1-1024x547.png 1024w, /wp-content/uploads/2025/03/1.0-1-300x160.png 300w, /wp-content/uploads/2025/03/1.0-1-768x411.png 768w, /wp-content/uploads/2025/03/1.0-1-1536x821.png 1536w, /wp-content/uploads/2025/03/1.0-1-2048x1095.png 2048w, /wp-content/uploads/2025/03/1.0-1-750x401.png 750w, /wp-content/uploads/2025/03/1.0-1-1140x609.png 1140w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="351" src="/wp-content/uploads/2025/03/1-1-1024x351.png" alt="" class="wp-image-2183" srcset="/wp-content/uploads/2025/03/1-1-1024x351.png 1024w, /wp-content/uploads/2025/03/1-1-300x103.png 300w, /wp-content/uploads/2025/03/1-1-768x263.png 768w, /wp-content/uploads/2025/03/1-1-1536x526.png 1536w, /wp-content/uploads/2025/03/1-1-2048x702.png 2048w, /wp-content/uploads/2025/03/1-1-750x257.png 750w, /wp-content/uploads/2025/03/1-1-1140x390.png 1140w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="254" src="/wp-content/uploads/2025/03/3-1024x254.png" alt="" class="wp-image-2185" srcset="/wp-content/uploads/2025/03/3-1024x254.png 1024w, /wp-content/uploads/2025/03/3-300x74.png 300w, /wp-content/uploads/2025/03/3-768x190.png 768w, /wp-content/uploads/2025/03/3-1536x380.png 1536w, /wp-content/uploads/2025/03/3-2048x507.png 2048w, /wp-content/uploads/2025/03/3-750x186.png 750w, /wp-content/uploads/2025/03/3-1140x282.png 1140w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p><br><br></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Step 2: Deploy Azure Front Door with Terraform</h2>



<p>Now that we have our storage account ready, the next step is to deploy <strong>Azure Front Door Standard</strong>. But before jumping into the deployment, let’s briefly understand what Azure Front Door is and why it’s useful.</p>



<h3 class="wp-block-heading">What is Azure Front Door?</h3>



<p>Azure Front Door is a globally distributed service that enhances website performance, security, and availability. Here are some key benefits:</p>



<ol start="1" class="wp-block-list">
<li><strong>Global Load Balancing</strong>
<ul class="wp-block-list">
<li>Distributes traffic across multiple backends (App Services, VMs, AKS, etc.).</li>



<li>Ensures high availability by routing traffic to the healthiest/closest backend.</li>
</ul>
</li>



<li><strong>Performance Acceleration</strong>
<ul class="wp-block-list">
<li>Uses smart routing to reduce latency and improve response times.</li>



<li>Supports caching for static content (available in Azure Front Door Standard/Premium).</li>
</ul>
</li>



<li><strong>Security Enhancements</strong>
<ul class="wp-block-list">
<li>Includes a Web Application Firewall (WAF) to protect against DDoS, SQL injection, and XSS attacks.</li>



<li>Provides TLS termination for secure HTTPS connections.</li>



<li>Offers bot protection to filter malicious traffic.</li>
</ul>
</li>



<li><strong>URL-Based Routing &amp; Redirection</strong>
<ul class="wp-block-list">
<li>Routes requests based on path, hostname, or query parameters.</li>



<li>Supports HTTP to HTTPS redirection and domain changes.</li>
</ul>
</li>



<li><strong>High Availability &amp; Failover</strong>
<ul class="wp-block-list">
<li>Automatically fails over to the nearest available backend.</li>



<li>Uses health probes to monitor backend status in real time.</li>
</ul>
</li>
</ol>



<p>For more details, check out <a href="https://learn.microsoft.com/en-us/azure/frontdoor?wt.mc_id=MVP_328341">Microsoft’s documentation</a>.</p>



<p><br></p>



<pre class="wp-block-code"><code>resource "azurerm_cdn_frontdoor_profile" "fqdn_profile" {
  name                     = "achrafbenalayastaticwebsite008"
  resource_group_name      = azurerm_resource_group.rg_demo_static_website.name
  response_timeout_seconds = 16
  sku_name                 = "Standard_AzureFrontDoor"
}

resource "azurerm_cdn_frontdoor_endpoint" "web_endpoint" {
  name                     = "web"
  cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.fqdn_profile.id
}

resource "azurerm_cdn_frontdoor_rule_set" "rule_set" {
  name                     = "caching"
  cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.fqdn_profile.id
}


resource "azurerm_cdn_frontdoor_origin_group" "origin_group_web" {
  name                     = "web"
  cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.fqdn_profile.id

  load_balancing {
    additional_latency_in_milliseconds = 0
    sample_size                        = 16
    successful_samples_required        = 3
  }
  health_probe {
    interval_in_seconds = 100
    path                = "/index.html"
    protocol            = "Http"
    request_type        = "HEAD"
  }
}

resource "azurerm_cdn_frontdoor_origin" "web_origin" {
  depends_on                     = &#91;azurerm_storage_account.storage_staticwebtorageachraf002]
  name                           = "web"
  cdn_frontdoor_origin_group_id  = azurerm_cdn_frontdoor_origin_group.origin_group_web.id
  enabled                        = true
  certificate_name_check_enabled = false
  host_name                      = azurerm_storage_account.storage_staticwebtorageachraf002.primary_web_host
  origin_host_header             = azurerm_storage_account.storage_staticwebtorageachraf002.primary_web_host

  http_port  = 80
  https_port = 443
  priority   = 1
  weight     = 1000
}


resource "azurerm_cdn_frontdoor_rule" "cache_rule" {
  depends_on = &#91;
    azurerm_cdn_frontdoor_origin_group.origin_group_web,
    azurerm_cdn_frontdoor_origin.web_origin
  ]
  name                      = "static"
  cdn_frontdoor_rule_set_id = azurerm_cdn_frontdoor_rule_set.rule_set.id
  order                     = 1
  behavior_on_match         = "Stop"


  conditions {
    url_file_extension_condition {
      operator     = "Equal"
      match_values = &#91;"css", "js", "ico", "png", "jpeg", "jpg", ".map"]
    }
  }
  actions {
    route_configuration_override_action {
      compression_enabled = true
      cache_behavior      = "HonorOrigin"
    }
  }
}


resource "azurerm_cdn_frontdoor_route" "default_route" {
  name                            = "default"
  cdn_frontdoor_endpoint_id       = azurerm_cdn_frontdoor_endpoint.web_endpoint.id
  cdn_frontdoor_origin_group_id   = azurerm_cdn_frontdoor_origin_group.origin_group_web.id
  cdn_frontdoor_origin_ids        = &#91;azurerm_cdn_frontdoor_origin.web_origin.id]
  cdn_frontdoor_rule_set_ids      = &#91;azurerm_cdn_frontdoor_rule_set.rule_set.id]
  enabled                         = true
  cdn_frontdoor_custom_domain_ids = &#91;azurerm_cdn_frontdoor_custom_domain.ihelpyoutodo_domain.id]
  forwarding_protocol             = "MatchRequest"
  https_redirect_enabled          = true
  patterns_to_match               = &#91;"/*"]
  supported_protocols             = &#91;"Http", "Https"]
  link_to_default_domain          = false
}



resource "azurerm_cdn_frontdoor_custom_domain" "ihelpyoutodo_domain" {
  name                     = "yourdomain"
  cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.fqdn_profile.id
  host_name                = "www.yourdomain.com"

  tls {
    certificate_type    = "ManagedCertificate"
    minimum_tls_version = "TLS12"
  }
}


resource "azurerm_cdn_frontdoor_custom_domain_association" "domain_association" {
  cdn_frontdoor_custom_domain_id = azurerm_cdn_frontdoor_custom_domain.ihelpyoutodo_domain.id
  cdn_frontdoor_route_ids        = &#91;azurerm_cdn_frontdoor_route.default_route.id]
}</code></pre>



<p>After deploying Azure Front Door, the next step is to configure a <strong>custom domain</strong> to serve our website.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Step 3: Configure a Custom Domain</h2>



<p>To configure a custom domain, we need to make some changes in our <strong>DNS provider</strong> (in my case, <strong>GoDaddy</strong>). The first step is to add a <strong>TXT record</strong> in the domain’s DNS settings to validate the domain in Azure.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="614" src="/wp-content/uploads/2025/03/verifieddomain-2-1024x614.png" alt="" class="wp-image-2187" srcset="/wp-content/uploads/2025/03/verifieddomain-2-1024x614.png 1024w, /wp-content/uploads/2025/03/verifieddomain-2-300x180.png 300w, /wp-content/uploads/2025/03/verifieddomain-2-768x460.png 768w, /wp-content/uploads/2025/03/verifieddomain-2-1536x921.png 1536w, /wp-content/uploads/2025/03/verifieddomain-2-2048x1227.png 2048w, /wp-content/uploads/2025/03/verifieddomain-2-750x450.png 750w, /wp-content/uploads/2025/03/verifieddomain-2-1140x683.png 1140w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>Ps : After you add the Text record , you need also to add 3 cname record that point to your front door default root that end with : .azurefd.net else you will have this error <br></p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="76" src="/wp-content/uploads/2025/03/4-1024x76.png" alt="" class="wp-image-2191" srcset="/wp-content/uploads/2025/03/4-1024x76.png 1024w, /wp-content/uploads/2025/03/4-300x22.png 300w, /wp-content/uploads/2025/03/4-768x57.png 768w, /wp-content/uploads/2025/03/4-1536x115.png 1536w, /wp-content/uploads/2025/03/4-750x56.png 750w, /wp-content/uploads/2025/03/4-1140x85.png 1140w, /wp-content/uploads/2025/03/4.png 1850w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="996" height="761" src="/wp-content/uploads/2025/03/cname.png" alt="" class="wp-image-2198" srcset="/wp-content/uploads/2025/03/cname.png 996w, /wp-content/uploads/2025/03/cname-300x229.png 300w, /wp-content/uploads/2025/03/cname-768x587.png 768w, /wp-content/uploads/2025/03/cname-750x573.png 750w" sizes="(max-width: 996px) 100vw, 996px" /></figure>



<h3 class="wp-block-heading">Domain Validation Process</h3>



<p>All domains added to Azure Front Door must be validated. This ensures protection against accidental misconfigurations and prevents domain spoofing. In some cases, Azure prevalidates the domain if it’s already verified by another Azure service. Otherwise, you must manually validate the domain following Azure’s verification steps.</p>



<p>One feature I really appreciate is that <strong>Azure Front Door can automatically manage TLS certificates</strong> for both subdomains and apex domains. With managed certificates:</p>



<ul class="wp-block-list">
<li>You don’t need to create, store, or manually install certificates.</li>



<li>Azure automatically renews certificates, preventing downtime due to expired SSL certificates.</li>
</ul>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Note: The process of generating, issuing, and installing a managed TLS certificate can take several minutes to an hour, sometimes longer.</p>
</blockquote>



<p>Once the domain is validated, you’ll see a status of <strong>Approved</strong>, and the TLS certificate will be deployed.</p>



<h2 class="wp-block-heading">Final Step: Accessing the Website</h2>



<p>Now that everything is set up, we can visit our website using our custom domain, benefiting from Azure’s global load balancing, caching, and security features!</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="479" src="/wp-content/uploads/2025/03/web-1024x479.png" alt="" class="wp-image-2189" srcset="/wp-content/uploads/2025/03/web-1024x479.png 1024w, /wp-content/uploads/2025/03/web-300x140.png 300w, /wp-content/uploads/2025/03/web-768x360.png 768w, /wp-content/uploads/2025/03/web-1536x719.png 1536w, /wp-content/uploads/2025/03/web-750x351.png 750w, /wp-content/uploads/2025/03/web-1140x534.png 1140w, /wp-content/uploads/2025/03/web.png 1997w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading">Conclusion</h2>



<p>In this tutorial, we successfully deployed a static website on Azure using Terraform. We:</p>



<ul class="wp-block-list">
<li>Created a <strong>Storage Account</strong> with <strong>static website hosting enabled</strong>.</li>



<li>Deployed <strong>Azure Front Door</strong> to provide global load balancing, security, and performance improvements.</li>



<li>Configured a <strong>custom domain</strong> and enabled <strong>managed TLS certificates</strong>.</li>
</ul>



<p>This setup ensures high availability, security, and performance while keeping things simple with Terraform automation. Let me know if you have any questions or if you’d like a deeper dive into any of these steps!</p>
]]></content:encoded>
					
					<wfw:commentRss>https://achrafbenalaya.com/2025/03/11/step-by-step-guide-azure-front-door-storage-account-static-website-custom-domain-with-terraform/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2166</post-id>	</item>
		<item>
		<title>Network Security &#038; Route Tables – Checking NSGs, route tables, and service endpoints for a targeted VNET or Subnet</title>
		<link>https://achrafbenalaya.com/2025/02/01/network-security-route-tables-checking-nsgs-route-tables-and-service-endpoints-for-a-targeted-vnet-or-subnet/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=network-security-route-tables-checking-nsgs-route-tables-and-service-endpoints-for-a-targeted-vnet-or-subnet</link>
					<comments>https://achrafbenalaya.com/2025/02/01/network-security-route-tables-checking-nsgs-route-tables-and-service-endpoints-for-a-targeted-vnet-or-subnet/#respond</comments>
		
		<dc:creator><![CDATA[achraf]]></dc:creator>
		<pubDate>Sat, 01 Feb 2025 13:04:38 +0000</pubDate>
				<category><![CDATA[Azure]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Cloud]]></category>
		<category><![CDATA[azure]]></category>
		<category><![CDATA[powershell]]></category>
		<category><![CDATA[subnet]]></category>
		<category><![CDATA[vnet]]></category>
		<guid isPermaLink="false">https://achrafbenalaya.com/?p=2114</guid>

					<description><![CDATA[For an inventory for our company related to remediation (anything that was deployed before using the portal we import it via terraform and later apply our standards too) we have been asked to get details about each virtual network and subnet and the connected ressources to those vnet ,why ? because sometimes we will need [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>For an inventory for our company related to remediation (anything that was deployed before using the portal we import it via terraform and later apply our standards too) we have been asked to get details about each virtual network and subnet and the connected ressources to those vnet ,why ? because sometimes we will need to add some routes in our udr , sometimes we update the nsgs and some other times if we found out a vnet is a legacy we see if we are going to delete it .<br><br>In an earlier blog post we have written : PowerShell Automation for Azure Networks: Detailed VNET and Subnet Analysis we have extracted everything related to all the vent in our </p>


<a class="wp-block-read-more" href="https://achrafbenalaya.com/2025/02/01/network-security-route-tables-checking-nsgs-route-tables-and-service-endpoints-for-a-targeted-vnet-or-subnet/" target="_self">https://achrafbenalaya.com/2024/11/02/powershell-automation-for-azure-networks-detailed-vnet-and-subnet-analysis/<span class="screen-reader-text">: Network Security &amp; Route Tables – Checking NSGs, route tables, and service endpoints for a targeted VNET or Subnet</span></a>


<p class="has-text-align-left">subscriptions (over 100 sub) for that my college asked me if it is possible to write another script only to target one vnet or one subnet in a vnet ,and that&#8217;s normal since he does not need details about all the vnet&#8217;s that exist , and he need an updated version of the report since any change can happen and he can no be based on an older report .<br><br>for that I have added this two scripts below to help extract details about the vnet and subent and save the report in a stylish table format in excel .<br><br></p>



<h2 class="wp-block-heading">How to Use :</h2>



<p>1 – Connect to Azure: Run Connect-AzAccount to authenticate and connect to your Azure account.<br>2- Install-Module -Name ImportExcel -Scope CurrentUser -Force<br>3- Insert the subscription id ,the ressource group name ,the vnet ,and the subnet is optionel.<br>4 – Execute the Script: Copy and run the script in your PowerShell environment.<br>5 – View Results: The script outputs a summary to the console and saves detailed results to a specified Excel file.<br>6 – Access the Excel : Open the XlSX file located at path.xlxs` to review the details.</p>



<p>This script is useful for administrators needing to audit network configurations and IP usage across multiple Azure subscriptions.</p>



<p>Script:<br><br></p>



<pre class="wp-block-code"><code>$subscriptionId = ''
$resourceGroupName = ''
$vnetName = ''
$subnetName = ''  # Can be empty to process all subnets
$desktopPath = &#91;System.Environment]::GetFolderPath("Desktop")
$exportDirectory = "$desktopPath\export_subnets"

Import-Module ImportExcel -Force

# Connect to Azure
Select-AzSubscription -SubscriptionId $subscriptionId

# Get the virtual network
$vnet = Get-AzVirtualNetwork -Name $vnetName -ResourceGroupName $resourceGroupName
if (-not $vnet) {
    Write-Host "The VNet $vnetName was not found." -ForegroundColor Red
    exit
}

# Function to process a single subnet
function Process-Subnet {
    param (
        &#91;Parameter(Mandatory=$true)]
        $subnet
    )

    Write-Host "--------------------------"
    Write-Host " "
    Write-Host "   Subnet: $($subnet.Name)"
    $connectedDevices = $subnet.IpConfigurations.Count
    Write-Host "   Connected devices: $connectedDevices"

    # Calculate IPs
    $subnetMask = $subnet.AddressPrefix.Split('/')&#91;1]
    $totalIps = &#91;math]::Pow(2, 32 - $subnetMask)
    $reservedIps = 5
    $usedIps = $connectedDevices + $reservedIps
    $availableIps = $totalIps - $usedIps
    Write-Host "   Total IPs: $totalIps"
    Write-Host "   Used IPs: $usedIps"
    Write-Host "   Available IPs: $availableIps"

    # Service Endpoints and Delegations
    $serviceEndpoints = if ($subnet.ServiceEndpoints) { $subnet.ServiceEndpoints.Service -join ', ' } else { "None" }
    Write-Host "   Service Endpoints: $serviceEndpoints"
    $delegations = if ($subnet.Delegations) { $subnet.Delegations.ServiceName -join ', ' } else { "None" }
    Write-Host "   Delegations: $delegations"

    # Subnet address
    $addressPrefixString = $subnet.AddressPrefix -join ', '

    # Network interfaces
    $networkInterfaces = Get-AzNetworkInterface | Where-Object { $_.IpConfigurations.Subnet.Id -eq $subnet.Id }
    $results = @()

    foreach ($nic in $networkInterfaces) {
        foreach ($ipConfig in $nic.IpConfigurations) {
            $vm = Get-AzVM | Where-Object { $_.Id -eq $nic.VirtualMachine.Id }
            $vmName = if ($vm) { $vm.Name } else { "Not Available" }

            $results += &#91;PSCustomObject]@{
                Subscription     = &#91;string]$subscriptionId
                VNet            = &#91;string]$vnetName
                Subnet          = &#91;string]$subnet.Name
                AddressPrefix   = &#91;string]$addressPrefixString
                TotalIps        = &#91;int64]$totalIps
                UsedIps         = &#91;int64]$usedIps
                AvailableIps    = &#91;int64]$availableIps
                ConnectedDevices= &#91;int]$connectedDevices
                ServiceEndpoints= &#91;string]$serviceEndpoints
                Delegations     = &#91;string]$delegations
                IpAddress       = &#91;string]$ipConfig.PrivateIpAddress
                VMName          = &#91;string]$vmName
                NicName         = &#91;string]$nic.Name
                AttachedTo      = &#91;string]"NIC: $($nic.Name), VM: $vmName"
            }
        }
    }

    # If no device found, add an empty row
    if ($results.Count -eq 0) {
        $results += &#91;PSCustomObject]@{
            Subscription     = &#91;string]$subscriptionId
            VNet            = &#91;string]$vnetName
            Subnet          = &#91;string]$subnet.Name
            AddressPrefix   = &#91;string]$addressPrefixString
            TotalIPs        = &#91;int64]$totalIps
            UsedIPs         = &#91;int64]$usedIps
            AvailableIPs    = &#91;int64]$availableIps
            ConnectedDevices= &#91;int]0
            ServiceEndpoints= &#91;string]$serviceEndpoints
            Delegations     = &#91;string]$delegations
            IpAddress       = &#91;string]""
            VMName          = &#91;string]""
            NicName         = &#91;string]""
            AttachedTo      = &#91;string]"Not Applicable"
        }
    }

    return $results
}

# Determine which subnets to process
$subnetsToProcess = @()
if (&#91;string]::IsNullOrEmpty($subnetName)) {
    $subnetsToProcess = $vnet.Subnets
    $exportFileName = "all_subnets.xlsx"
} else {
    $subnet = $vnet.Subnets | Where-Object { $_.Name -eq $subnetName }
    if (-not $subnet) {
        Write-Host "The subnet $subnetName was not found." -ForegroundColor Red
        exit
    }
    $subnetsToProcess = @($subnet)
    $exportFileName = "$subnetName.xlsx"
}

# Process all selected subnets
$allResults = @()
foreach ($subnet in $subnetsToProcess) {
    $results = Process-Subnet -subnet $subnet
    $allResults += $results
}

# Create export directory if it doesn't exist
if (-not (Test-Path -Path $exportDirectory)) {
    Write-Host "Creating export directory: $exportDirectory"
    New-Item -ItemType Directory -Path $exportDirectory | Out-Null
}

$excelFilePath = "$exportDirectory\$exportFileName"

try {
    $excelApp = New-Object -ComObject Excel.Application
    $excelApp.Visible = $false

    $workbook = $excelApp.Workbooks.Add()
    $worksheet = $workbook.Sheets.Item(1)
    $worksheet.Name = "Subnet Report"

    # Define headers with formatting
    $headers = @("Subscription", "VNet", "Subnet", "AddressPrefix", "TotalIPs", "UsedIPs", "AvailableIPs", 
                "ConnectedDevices", "ServiceEndpoints", "Delegations", "IpAddress", "VMName", "NicName", "AttachedTo")

    for ($i = 0; $i -lt $headers.Count; $i++) {
        $cell = $worksheet.Cells.Item(1, $i + 1)
        $cell.Value = $headers&#91;$i]
        $cell.Font.Bold = $true
        $cell.Interior.ColorIndex = 37
        $cell.Borders.LineStyle = 1
    }

    # Insert data with borders
    $row = 2
    foreach ($result in $allResults) {
        for ($col = 1; $col -le $headers.Count; $col++) {
            $cell = $worksheet.Cells.Item($row, $col)
            $propertyName = $headers&#91;$col - 1]
            $propertyValue = $result.$propertyName
            
            # Convert numeric values to strings for Excel
            if ($propertyValue -is &#91;int64] -or $propertyValue -is &#91;int] -or $propertyValue -is &#91;double]) {
                $cell.Value2 = &#91;double]$propertyValue
            } else {
                $cell.Value2 = &#91;string]$propertyValue
            }
            
            $cell.Borders.LineStyle = 1
        }
        $row++
    }

    # Auto-fit column widths
    $worksheet.Columns.AutoFit()

    # Save and close the Excel file
    $workbook.SaveAs($excelFilePath)
    $workbook.Close()
    $excelApp.Quit()

    Write-Host "&#x2705; Export completed! File saved at: $excelFilePath" -ForegroundColor Green
    Invoke-Item -Path $excelFilePath
} catch {
    Write-Host "&#x274c; An error occurred: $($_.Exception.Message)" -ForegroundColor Red
} finally {
    if ($excelApp) { &#91;System.Runtime.Interopservices.Marshal]::ReleaseComObject($excelApp) }
}</code></pre>



<figure class="wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-1 is-layout-flex wp-block-gallery-is-layout-flex">
<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="2229" height="835" data-id="2127" src="/wp-content/uploads/2025/02/Sans-titre-3.png" alt="" class="wp-image-2127" srcset="/wp-content/uploads/2025/02/Sans-titre-3.png 2229w, /wp-content/uploads/2025/02/Sans-titre-3-300x112.png 300w, /wp-content/uploads/2025/02/Sans-titre-3-1024x384.png 1024w, /wp-content/uploads/2025/02/Sans-titre-3-768x288.png 768w, /wp-content/uploads/2025/02/Sans-titre-3-1536x575.png 1536w, /wp-content/uploads/2025/02/Sans-titre-3-2048x767.png 2048w, /wp-content/uploads/2025/02/Sans-titre-3-750x281.png 750w, /wp-content/uploads/2025/02/Sans-titre-3-1140x427.png 1140w" sizes="(max-width: 2229px) 100vw, 2229px" /></figure>
</figure>



<p class="has-vivid-cyan-blue-color has-text-color has-link-color wp-elements-6c20d23f8fc4e25fb4a61a70d6578137">Ps : This article was written in collaboration with my friend Malik .</p>
]]></content:encoded>
					
					<wfw:commentRss>https://achrafbenalaya.com/2025/02/01/network-security-route-tables-checking-nsgs-route-tables-and-service-endpoints-for-a-targeted-vnet-or-subnet/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2114</post-id>	</item>
		<item>
		<title>Azure Communication Services Email Sending Simplified: From Setup to Execution and Monitoring</title>
		<link>https://achrafbenalaya.com/2024/12/08/azure-communication-services-email-sending-simplified-from-setup-to-execution-and-monitoring/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=azure-communication-services-email-sending-simplified-from-setup-to-execution-and-monitoring</link>
					<comments>https://achrafbenalaya.com/2024/12/08/azure-communication-services-email-sending-simplified-from-setup-to-execution-and-monitoring/#respond</comments>
		
		<dc:creator><![CDATA[achraf]]></dc:creator>
		<pubDate>Sun, 08 Dec 2024 16:46:47 +0000</pubDate>
				<category><![CDATA[Azure]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Cloud]]></category>
		<guid isPermaLink="false">https://achrafbenalaya.com/?p=2022</guid>

					<description><![CDATA[1-General introduction The recent critical issue my customer experienced was that an application stopped sending emails for more than six months. And what was the reason behind this? It included using a solution that had been marked as deprecated and had been working all along-that is, until it did not work anymore. Of course, as [&#8230;]]]></description>
										<content:encoded><![CDATA[<p><!--ScriptorStartFragment--></p>
<div>
<p><!--ScriptorStartFragment--></p>
<h2><span style="font-family: 'arial black', sans-serif;">1-General introduction</span></h2>
</div>
<p><span style="font-family: arial, helvetica, sans-serif;"><br />
The recent critical issue my customer experienced was that an application stopped sending emails for more than six months. And what was the reason behind this? It included using a solution that had been marked as deprecated and had been working all along-that is, until it did not work anymore. Of course, as we know, once something is marked as deprecated, it&#8217;s only a question of time before it gets retired completely.</span></p>
<p><span style="font-family: arial, helvetica, sans-serif;">The challenge was clear when they called me for help-to fix the problem in one day. Fortunately, my background as a full-stack.NET developer gave me an edge to understand their system in no time. After diving into their codebase, I found they were using some very outdated library for SMTP to handle their email functionality.</span></p>
<p><span style="font-family: arial, helvetica, sans-serif;">With time running out, I needed a modern and reliable solution that could be implemented quickly. Azure Communication Services immediately came to mind: robust, scalable, and fresh from general availability. Its email capability was just what I needed to replace the deprecated SMTP library.</span></p>
<p><span style="font-family: arial, helvetica, sans-serif;">I managed, within the same day, the integration of their application to Azure Communication Services, along with testing, making it live. And then there I went, making them have emails up by the end of that day. This made their team super happy as their updated modern solution was a good solution.</span></p>
<p><span style="font-family: arial, helvetica, sans-serif;">This was an experience, and it reminded me that familiarization with newer tools and technologies is priceless in solving various problems quickly and well, even when the pressure might be really high.</span></p>
<p><span style="font-family: arial, helvetica, sans-serif;">so what is Azure Communication Services ?</span></p>
<div>
<p><span style="font-family: arial, helvetica, sans-serif;"><!--ScriptorStartFragment--></span></p>
<p class="scriptor-paragraph"><span style="font-family: arial, helvetica, sans-serif;">Azure Communication Services offers multichannel communication APIs for adding voice, video, chat, text messaging/SMS, email, and more to all your applications.</span></p>
<p class="scriptor-paragraph"><span style="font-family: arial, helvetica, sans-serif;">Azure Communication Services include REST APIs and client library SDKs, so you don&#8217;t need to be an expert in the underlying technologies to add communication into your apps. Azure Communication Services is available in multiple <a href="https://learn.microsoft.com/en-us/azure/communication-services/concepts/privacy?wt.mc_id=MVP_328341" rel="noreferrer noopener">Azure geographies</a> and Azure for government.</span></p>
<p class="scriptor-paragraph"><span style="font-family: arial, helvetica, sans-serif;">Azure Communication Services supports various communication formats:</span></p>
<p><a href="/wp-content/uploads/2024/12/1.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2024" src="/wp-content/uploads/2024/12/1.png" alt="" width="702" height="574" srcset="/wp-content/uploads/2024/12/1.png 702w, /wp-content/uploads/2024/12/1-300x245.png 300w" sizes="(max-width: 702px) 100vw, 702px" /></a></p>
<div>
<div class="scriptor-paragraph"><span style="font-family: arial, helvetica, sans-serif;">You can connect custom client apps, custom services, and the publicly switched telephone network (PSTN) to your communications experience. You can acquire <a href="https://learn.microsoft.com/en-us/azure/communication-services/concepts/telephony/plan-solution?wt.mc_id=MVP_328341" rel="noreferrer noopener">phone numbers</a> directly through Azure Communication Services REST APIs, SDKs, or the Azure portal and use these numbers for SMS or calling applications.https://learn.microsoft.com/en-us/azure/communication-services/concepts/sdk-options?wt.mc_id=MVP_328341</span></div>
<div class="scriptor-paragraph"><span style="font-family: arial, helvetica, sans-serif;">You can also integrate email capabilities to your applications using production-ready email SDKs. Azure Communication Services <a href="https://learn.microsoft.com/en-us/azure/communication-services/concepts/telephony/plan-solution?wt.mc_id=MVP_328341" rel="noreferrer noopener">direct routing</a> enables you to use SIP and session border controllers to connect your own PSTN carriers and bring your own phone numbers.</span></div>
<div class="scriptor-paragraph"><span style="font-family: arial, helvetica, sans-serif;">In addition to REST APIs, <a href="https://learn.microsoft.com/en-us/azure/communication-services/concepts/sdk-options?wt.mc_id=MVP_328341" rel="noreferrer noopener">Azure Communication Services client libraries</a> are available for various platforms and languages, including Web browsers (JavaScript), iOS (Swift), Android (Java), Windows (.NET). Take advantage of the <a href="https://learn.microsoft.com/en-us/azure/communication-services/concepts/ui-library/ui-library-overview?wt.mc_id=MVP_328341" rel="noreferrer noopener">UI library</a> to accelerate development for Web, iOS, and Android apps. Azure Communication Services is identity agnostic, and you control how to identify and authenticate your customers<br />
<a href="/wp-content/uploads/2024/12/2.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2025" src="/wp-content/uploads/2024/12/2.png" alt="" width="890" height="632" srcset="/wp-content/uploads/2024/12/2.png 890w, /wp-content/uploads/2024/12/2-300x213.png 300w, /wp-content/uploads/2024/12/2-768x545.png 768w, /wp-content/uploads/2024/12/2-120x86.png 120w, /wp-content/uploads/2024/12/2-350x250.png 350w, /wp-content/uploads/2024/12/2-750x533.png 750w" sizes="(max-width: 890px) 100vw, 890px" /></a></span></div>
<h2><span style="font-family: 'arial black', sans-serif;">2-Setup Azure Communication Service with a custom domain name to send Email</span></h2>
<p><span style="font-family: arial, helvetica, sans-serif;">First of all, you need to have a verified domain. In my use case, I already have a domain name in GoDaddy that I will be using for this demo.</span></p>
<div>
<div class="scriptor-paragraph">
<p><span style="font-family: arial, helvetica, sans-serif;">So, let&#8217;s get started :</span></p>
<p>&nbsp;</p>
</div>
<div class="scriptor-paragraph"><span style="font-family: arial, helvetica, sans-serif;">First, we need to go to Azure Portal and create the resource by searching for Azure Communication Services. We will then create Azure Communication Services, set up email resources, and configure domain ownership, SPF, and DKIM.</span></div>
<div class="scriptor-paragraph"><span style="font-family: arial, helvetica, sans-serif;">SPF ensures that only authorized mail servers can send mail using your domain, aiding to prevent email spoofing. DKIM adds a verified domain-based digital signature in every email, assuring recipients that messages haven&#8217;t been manipulated or tampered with in transitory ways.</span></div>
<h4 class="scriptor-paragraph"><span style="font-family: arial, helvetica, sans-serif;">For SPF and DKIM configurations, certain TXT records have to be added to the DNS settings of your domain<br />
<a href="/wp-content/uploads/2024/12/3.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2026" src="/wp-content/uploads/2024/12/3.png" alt="" width="560" height="1022" srcset="/wp-content/uploads/2024/12/3.png 560w, /wp-content/uploads/2024/12/3-164x300.png 164w" sizes="(max-width: 560px) 100vw, 560px" /></a></span></h4>
<ul>
<li><strong>create email resource</strong></li>
</ul>
<div class="scriptor-paragraph"><span style="font-family: arial, helvetica, sans-serif;"> </span></div>
<h4><a href="/wp-content/uploads/2024/12/1-1.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2027" src="/wp-content/uploads/2024/12/1-1.png" alt="" width="2294" height="1288" srcset="/wp-content/uploads/2024/12/1-1.png 2294w, /wp-content/uploads/2024/12/1-1-300x168.png 300w, /wp-content/uploads/2024/12/1-1-1024x575.png 1024w, /wp-content/uploads/2024/12/1-1-768x431.png 768w, /wp-content/uploads/2024/12/1-1-1536x862.png 1536w, /wp-content/uploads/2024/12/1-1-2048x1150.png 2048w, /wp-content/uploads/2024/12/1-1-750x421.png 750w, /wp-content/uploads/2024/12/1-1-1140x640.png 1140w" sizes="(max-width: 2294px) 100vw, 2294px" /></a> <a href="/wp-content/uploads/2024/12/2-1.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2028" src="/wp-content/uploads/2024/12/2-1.png" alt="" width="2290" height="1319" srcset="/wp-content/uploads/2024/12/2-1.png 2290w, /wp-content/uploads/2024/12/2-1-300x173.png 300w, /wp-content/uploads/2024/12/2-1-1024x590.png 1024w, /wp-content/uploads/2024/12/2-1-768x442.png 768w, /wp-content/uploads/2024/12/2-1-1536x885.png 1536w, /wp-content/uploads/2024/12/2-1-2048x1180.png 2048w, /wp-content/uploads/2024/12/2-1-750x432.png 750w, /wp-content/uploads/2024/12/2-1-1140x657.png 1140w" sizes="(max-width: 2290px) 100vw, 2290px" /></a> <a href="/wp-content/uploads/2024/12/3-1.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2029" src="/wp-content/uploads/2024/12/3-1.png" alt="" width="2296" height="1318" srcset="/wp-content/uploads/2024/12/3-1.png 2296w, /wp-content/uploads/2024/12/3-1-300x172.png 300w, /wp-content/uploads/2024/12/3-1-1024x588.png 1024w, /wp-content/uploads/2024/12/3-1-768x441.png 768w, /wp-content/uploads/2024/12/3-1-1536x882.png 1536w, /wp-content/uploads/2024/12/3-1-2048x1176.png 2048w, /wp-content/uploads/2024/12/3-1-750x431.png 750w, /wp-content/uploads/2024/12/3-1-1140x654.png 1140w" sizes="(max-width: 2296px) 100vw, 2296px" /></a> <a href="/wp-content/uploads/2024/12/4.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2030" src="/wp-content/uploads/2024/12/4.png" alt="" width="2297" height="1309" srcset="/wp-content/uploads/2024/12/4.png 2297w, /wp-content/uploads/2024/12/4-300x171.png 300w, /wp-content/uploads/2024/12/4-1024x584.png 1024w, /wp-content/uploads/2024/12/4-768x438.png 768w, /wp-content/uploads/2024/12/4-1536x875.png 1536w, /wp-content/uploads/2024/12/4-2048x1167.png 2048w, /wp-content/uploads/2024/12/4-750x427.png 750w, /wp-content/uploads/2024/12/4-1140x650.png 1140w" sizes="(max-width: 2297px) 100vw, 2297px" /></a> <a href="/wp-content/uploads/2024/12/5.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2031" src="/wp-content/uploads/2024/12/5.png" alt="" width="2293" height="1320" srcset="/wp-content/uploads/2024/12/5.png 2293w, /wp-content/uploads/2024/12/5-300x173.png 300w, /wp-content/uploads/2024/12/5-1024x589.png 1024w, /wp-content/uploads/2024/12/5-768x442.png 768w, /wp-content/uploads/2024/12/5-1536x884.png 1536w, /wp-content/uploads/2024/12/5-2048x1179.png 2048w, /wp-content/uploads/2024/12/5-750x432.png 750w, /wp-content/uploads/2024/12/5-1140x656.png 1140w" sizes="(max-width: 2293px) 100vw, 2293px" /></a> <a href="/wp-content/uploads/2024/12/6.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2032" src="/wp-content/uploads/2024/12/6.png" alt="" width="2291" height="1319" srcset="/wp-content/uploads/2024/12/6.png 2291w, /wp-content/uploads/2024/12/6-300x173.png 300w, /wp-content/uploads/2024/12/6-1024x590.png 1024w, /wp-content/uploads/2024/12/6-768x442.png 768w, /wp-content/uploads/2024/12/6-1536x884.png 1536w, /wp-content/uploads/2024/12/6-2048x1179.png 2048w, /wp-content/uploads/2024/12/6-750x432.png 750w, /wp-content/uploads/2024/12/6-1140x656.png 1140w" sizes="(max-width: 2291px) 100vw, 2291px" /></a> <a href="/wp-content/uploads/2024/12/7.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2033" src="/wp-content/uploads/2024/12/7.png" alt="" width="2241" height="1278" srcset="/wp-content/uploads/2024/12/7.png 2241w, /wp-content/uploads/2024/12/7-300x171.png 300w, /wp-content/uploads/2024/12/7-1024x584.png 1024w, /wp-content/uploads/2024/12/7-768x438.png 768w, /wp-content/uploads/2024/12/7-1536x876.png 1536w, /wp-content/uploads/2024/12/7-2048x1168.png 2048w, /wp-content/uploads/2024/12/7-750x428.png 750w, /wp-content/uploads/2024/12/7-1140x650.png 1140w" sizes="(max-width: 2241px) 100vw, 2241px" /></a></h4>
<ul>
<li><span style="font-family: 'arial black', sans-serif;">Add custom domain<br />
</span><span style="font-family: 'arial black', sans-serif;"><span style="font-family: 'arial black', sans-serif;"><br />
<a href="/wp-content/uploads/2024/12/11-1.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2041" src="/wp-content/uploads/2024/12/11-1.png" alt="" width="2286" height="1321" srcset="/wp-content/uploads/2024/12/11-1.png 2286w, /wp-content/uploads/2024/12/11-1-300x173.png 300w, /wp-content/uploads/2024/12/11-1-1024x592.png 1024w, /wp-content/uploads/2024/12/11-1-768x444.png 768w, /wp-content/uploads/2024/12/11-1-1536x888.png 1536w, /wp-content/uploads/2024/12/11-1-2048x1183.png 2048w, /wp-content/uploads/2024/12/11-1-750x433.png 750w, /wp-content/uploads/2024/12/11-1-1140x659.png 1140w" sizes="(max-width: 2286px) 100vw, 2286px" /></a><a href="/wp-content/uploads/2024/12/12.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2042" src="/wp-content/uploads/2024/12/12.png" alt="" width="1990" height="750" srcset="/wp-content/uploads/2024/12/12.png 1990w, /wp-content/uploads/2024/12/12-300x113.png 300w, /wp-content/uploads/2024/12/12-1024x386.png 1024w, /wp-content/uploads/2024/12/12-768x289.png 768w, /wp-content/uploads/2024/12/12-1536x579.png 1536w, /wp-content/uploads/2024/12/12-750x283.png 750w, /wp-content/uploads/2024/12/12-1140x430.png 1140w" sizes="(max-width: 1990px) 100vw, 1990px" /></a><a href="/wp-content/uploads/2024/12/13.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2043" src="/wp-content/uploads/2024/12/13.png" alt="" width="2292" height="887" srcset="/wp-content/uploads/2024/12/13.png 2292w, /wp-content/uploads/2024/12/13-300x116.png 300w, /wp-content/uploads/2024/12/13-1024x396.png 1024w, /wp-content/uploads/2024/12/13-768x297.png 768w, /wp-content/uploads/2024/12/13-1536x594.png 1536w, /wp-content/uploads/2024/12/13-2048x793.png 2048w, /wp-content/uploads/2024/12/13-750x290.png 750w, /wp-content/uploads/2024/12/13-1140x441.png 1140w" sizes="(max-width: 2292px) 100vw, 2292px" /></a><a href="/wp-content/uploads/2024/12/14.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2044" src="/wp-content/uploads/2024/12/14.png" alt="" width="2292" height="868" srcset="/wp-content/uploads/2024/12/14.png 2292w, /wp-content/uploads/2024/12/14-300x114.png 300w, /wp-content/uploads/2024/12/14-1024x388.png 1024w, /wp-content/uploads/2024/12/14-768x291.png 768w, /wp-content/uploads/2024/12/14-1536x582.png 1536w, /wp-content/uploads/2024/12/14-2048x776.png 2048w, /wp-content/uploads/2024/12/14-750x284.png 750w, /wp-content/uploads/2024/12/14-1140x432.png 1140w" sizes="(max-width: 2292px) 100vw, 2292px" /></a><a href="/wp-content/uploads/2024/12/15.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2045" src="/wp-content/uploads/2024/12/15.png" alt="" width="2280" height="1320" srcset="/wp-content/uploads/2024/12/15.png 2280w, /wp-content/uploads/2024/12/15-300x174.png 300w, /wp-content/uploads/2024/12/15-1024x593.png 1024w, /wp-content/uploads/2024/12/15-768x445.png 768w, /wp-content/uploads/2024/12/15-1536x889.png 1536w, /wp-content/uploads/2024/12/15-2048x1186.png 2048w, /wp-content/uploads/2024/12/15-750x434.png 750w, /wp-content/uploads/2024/12/15-1140x660.png 1140w" sizes="(max-width: 2280px) 100vw, 2280px" /></a><a href="/wp-content/uploads/2024/12/16.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2046" src="/wp-content/uploads/2024/12/16.png" alt="" width="2270" height="940" srcset="/wp-content/uploads/2024/12/16.png 2270w, /wp-content/uploads/2024/12/16-300x124.png 300w, /wp-content/uploads/2024/12/16-1024x424.png 1024w, /wp-content/uploads/2024/12/16-768x318.png 768w, /wp-content/uploads/2024/12/16-1536x636.png 1536w, /wp-content/uploads/2024/12/16-2048x848.png 2048w, /wp-content/uploads/2024/12/16-750x311.png 750w, /wp-content/uploads/2024/12/16-1140x472.png 1140w" sizes="(max-width: 2270px) 100vw, 2270px" /></a><a href="/wp-content/uploads/2024/12/16-1.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2047" src="/wp-content/uploads/2024/12/16-1.png" alt="" width="2245" height="1275" srcset="/wp-content/uploads/2024/12/16-1.png 2245w, /wp-content/uploads/2024/12/16-1-300x170.png 300w, /wp-content/uploads/2024/12/16-1-1024x582.png 1024w, /wp-content/uploads/2024/12/16-1-768x436.png 768w, /wp-content/uploads/2024/12/16-1-1536x872.png 1536w, /wp-content/uploads/2024/12/16-1-2048x1163.png 2048w, /wp-content/uploads/2024/12/16-1-750x426.png 750w, /wp-content/uploads/2024/12/16-1-1140x647.png 1140w" sizes="(max-width: 2245px) 100vw, 2245px" /></a><a href="/wp-content/uploads/2024/12/17.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2048" src="/wp-content/uploads/2024/12/17.png" alt="" width="2295" height="1323" srcset="/wp-content/uploads/2024/12/17.png 2295w, /wp-content/uploads/2024/12/17-300x173.png 300w, /wp-content/uploads/2024/12/17-1024x590.png 1024w, /wp-content/uploads/2024/12/17-768x443.png 768w, /wp-content/uploads/2024/12/17-1536x885.png 1536w, /wp-content/uploads/2024/12/17-2048x1181.png 2048w, /wp-content/uploads/2024/12/17-750x432.png 750w, /wp-content/uploads/2024/12/17-1140x657.png 1140w" sizes="(max-width: 2295px) 100vw, 2295px" /></a><a href="/wp-content/uploads/2024/12/18.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2049" src="/wp-content/uploads/2024/12/18.png" alt="" width="2238" height="1251" srcset="/wp-content/uploads/2024/12/18.png 2238w, /wp-content/uploads/2024/12/18-300x168.png 300w, /wp-content/uploads/2024/12/18-1024x572.png 1024w, /wp-content/uploads/2024/12/18-768x429.png 768w, /wp-content/uploads/2024/12/18-1536x859.png 1536w, /wp-content/uploads/2024/12/18-2048x1145.png 2048w, /wp-content/uploads/2024/12/18-750x419.png 750w, /wp-content/uploads/2024/12/18-1140x637.png 1140w" sizes="(max-width: 2238px) 100vw, 2238px" /></a></span></span></li>
<li><span style="font-family: 'arial black', sans-serif;"><span style="font-family: 'arial black', sans-serif;">connect domain to send email</span></span></li>
</ul>
<p><a href="/wp-content/uploads/2024/12/17-1.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2050" src="/wp-content/uploads/2024/12/17-1.png" alt="" width="2295" height="1323" srcset="/wp-content/uploads/2024/12/17-1.png 2295w, /wp-content/uploads/2024/12/17-1-300x173.png 300w, /wp-content/uploads/2024/12/17-1-1024x590.png 1024w, /wp-content/uploads/2024/12/17-1-768x443.png 768w, /wp-content/uploads/2024/12/17-1-1536x885.png 1536w, /wp-content/uploads/2024/12/17-1-2048x1181.png 2048w, /wp-content/uploads/2024/12/17-1-750x432.png 750w, /wp-content/uploads/2024/12/17-1-1140x657.png 1140w" sizes="(max-width: 2295px) 100vw, 2295px" /></a> <a href="/wp-content/uploads/2024/12/18-1.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2051" src="/wp-content/uploads/2024/12/18-1.png" alt="" width="2238" height="1251" srcset="/wp-content/uploads/2024/12/18-1.png 2238w, /wp-content/uploads/2024/12/18-1-300x168.png 300w, /wp-content/uploads/2024/12/18-1-1024x572.png 1024w, /wp-content/uploads/2024/12/18-1-768x429.png 768w, /wp-content/uploads/2024/12/18-1-1536x859.png 1536w, /wp-content/uploads/2024/12/18-1-2048x1145.png 2048w, /wp-content/uploads/2024/12/18-1-750x419.png 750w, /wp-content/uploads/2024/12/18-1-1140x637.png 1140w" sizes="(max-width: 2238px) 100vw, 2238px" /></a></p>
</div>
<h2><span style="font-family: 'arial black', sans-serif;">3-Test Azure Communication Service from the portal<br />
<a href="/wp-content/uploads/2024/12/19-1.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2052" src="/wp-content/uploads/2024/12/19-1.png" alt="" width="2293" height="1305" srcset="/wp-content/uploads/2024/12/19-1.png 2293w, /wp-content/uploads/2024/12/19-1-300x171.png 300w, /wp-content/uploads/2024/12/19-1-1024x583.png 1024w, /wp-content/uploads/2024/12/19-1-768x437.png 768w, /wp-content/uploads/2024/12/19-1-1536x874.png 1536w, /wp-content/uploads/2024/12/19-1-2048x1166.png 2048w, /wp-content/uploads/2024/12/19-1-750x427.png 750w, /wp-content/uploads/2024/12/19-1-1140x649.png 1140w" sizes="(max-width: 2293px) 100vw, 2293px" /></a><a href="/wp-content/uploads/2024/12/20.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2053" src="/wp-content/uploads/2024/12/20.png" alt="" width="2291" height="1318" srcset="/wp-content/uploads/2024/12/20.png 2291w, /wp-content/uploads/2024/12/20-300x173.png 300w, /wp-content/uploads/2024/12/20-1024x589.png 1024w, /wp-content/uploads/2024/12/20-768x442.png 768w, /wp-content/uploads/2024/12/20-1536x884.png 1536w, /wp-content/uploads/2024/12/20-2048x1178.png 2048w, /wp-content/uploads/2024/12/20-750x431.png 750w, /wp-content/uploads/2024/12/20-1140x656.png 1140w" sizes="(max-width: 2291px) 100vw, 2291px" /></a><a href="/wp-content/uploads/2024/12/21.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2054" src="/wp-content/uploads/2024/12/21.png" alt="" width="1010" height="1311" srcset="/wp-content/uploads/2024/12/21.png 1010w, /wp-content/uploads/2024/12/21-231x300.png 231w, /wp-content/uploads/2024/12/21-789x1024.png 789w, /wp-content/uploads/2024/12/21-768x997.png 768w, /wp-content/uploads/2024/12/21-750x974.png 750w" sizes="(max-width: 1010px) 100vw, 1010px" /></a><br />
</span></h2>
<h2><span style="font-family: 'arial black', sans-serif;">4- Use Azure Communication Service with C# (including SMTP)</span></h2>
<p>now lets try the code provided from the portal :</p>
<p><a href="/wp-content/uploads/2024/12/1-2.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2055" src="/wp-content/uploads/2024/12/1-2.png" alt="" width="2283" height="821" srcset="/wp-content/uploads/2024/12/1-2.png 2283w, /wp-content/uploads/2024/12/1-2-300x108.png 300w, /wp-content/uploads/2024/12/1-2-1024x368.png 1024w, /wp-content/uploads/2024/12/1-2-768x276.png 768w, /wp-content/uploads/2024/12/1-2-1536x552.png 1536w, /wp-content/uploads/2024/12/1-2-2048x736.png 2048w, /wp-content/uploads/2024/12/1-2-750x270.png 750w, /wp-content/uploads/2024/12/1-2-1140x410.png 1140w" sizes="(max-width: 2283px) 100vw, 2283px" /></a> <a href="/wp-content/uploads/2024/12/2-2.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2056" src="/wp-content/uploads/2024/12/2-2.png" alt="" width="2542" height="1383" srcset="/wp-content/uploads/2024/12/2-2.png 2542w, /wp-content/uploads/2024/12/2-2-300x163.png 300w, /wp-content/uploads/2024/12/2-2-1024x557.png 1024w, /wp-content/uploads/2024/12/2-2-768x418.png 768w, /wp-content/uploads/2024/12/2-2-1536x836.png 1536w, /wp-content/uploads/2024/12/2-2-2048x1114.png 2048w, /wp-content/uploads/2024/12/2-2-750x408.png 750w, /wp-content/uploads/2024/12/2-2-1140x620.png 1140w" sizes="(max-width: 2542px) 100vw, 2542px" /></a> <a href="/wp-content/uploads/2024/12/3-2.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2057" src="/wp-content/uploads/2024/12/3-2.png" alt="" width="2285" height="1308" srcset="/wp-content/uploads/2024/12/3-2.png 2285w, /wp-content/uploads/2024/12/3-2-300x172.png 300w, /wp-content/uploads/2024/12/3-2-1024x586.png 1024w, /wp-content/uploads/2024/12/3-2-768x440.png 768w, /wp-content/uploads/2024/12/3-2-1536x879.png 1536w, /wp-content/uploads/2024/12/3-2-2048x1172.png 2048w, /wp-content/uploads/2024/12/3-2-750x429.png 750w, /wp-content/uploads/2024/12/3-2-1140x653.png 1140w" sizes="(max-width: 2285px) 100vw, 2285px" /></a></p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">using System;
using System.Collections.Generic;
using Azure;
using Azure.Communication.Email;

string connectionString = "endpoint=replace this with your endpoint";
var emailClient = new EmailClient(connectionString);


var emailMessage = new EmailMessage(
    senderAddress: "DoNotReply@yourdomain.com",
    content: new EmailContent("Test Email from communication service")
    {
        PlainText = "Hello world via email from visual studio code.",
        Html = @"
        &lt;html&gt;
            &lt;body&gt;
                &lt;h1&gt;Hello world via email from visual studio code..&lt;/h1&gt;
            &lt;/body&gt;
        &lt;/html&gt;"
    },
    recipients: new EmailRecipients(new List&lt;EmailAddress&gt; { new EmailAddress("youremail@outlook.com") }));
    

EmailSendOperation emailSendOperation = emailClient.Send(
    WaitUntil.Completed,
    emailMessage);
</pre>
<h2><strong>SMTP Sample</strong></h2>
<p>&nbsp;</p>
<p>We will need to implement additional configurations such as custom role to optimize the SMTP sample and ensure seamless functionality.<br />
<a href="/wp-content/uploads/2024/12/1-3.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2058" src="/wp-content/uploads/2024/12/1-3.png" alt="" width="2263" height="1320" srcset="/wp-content/uploads/2024/12/1-3.png 2263w, /wp-content/uploads/2024/12/1-3-300x175.png 300w, /wp-content/uploads/2024/12/1-3-1024x597.png 1024w, /wp-content/uploads/2024/12/1-3-768x448.png 768w, /wp-content/uploads/2024/12/1-3-1536x896.png 1536w, /wp-content/uploads/2024/12/1-3-2048x1195.png 2048w, /wp-content/uploads/2024/12/1-3-750x437.png 750w, /wp-content/uploads/2024/12/1-3-1140x665.png 1140w" sizes="(max-width: 2263px) 100vw, 2263px" /></a> <a href="/wp-content/uploads/2024/12/2-3.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2059" src="/wp-content/uploads/2024/12/2-3.png" alt="" width="2297" height="1283" srcset="/wp-content/uploads/2024/12/2-3.png 2297w, /wp-content/uploads/2024/12/2-3-300x168.png 300w, /wp-content/uploads/2024/12/2-3-1024x572.png 1024w, /wp-content/uploads/2024/12/2-3-768x429.png 768w, /wp-content/uploads/2024/12/2-3-1536x858.png 1536w, /wp-content/uploads/2024/12/2-3-2048x1144.png 2048w, /wp-content/uploads/2024/12/2-3-750x419.png 750w, /wp-content/uploads/2024/12/2-3-1140x637.png 1140w" sizes="(max-width: 2297px) 100vw, 2297px" /></a> <a href="/wp-content/uploads/2024/12/3-3.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2060" src="/wp-content/uploads/2024/12/3-3.png" alt="" width="2285" height="1306" srcset="/wp-content/uploads/2024/12/3-3.png 2285w, /wp-content/uploads/2024/12/3-3-300x171.png 300w, /wp-content/uploads/2024/12/3-3-1024x585.png 1024w, /wp-content/uploads/2024/12/3-3-768x439.png 768w, /wp-content/uploads/2024/12/3-3-1536x878.png 1536w, /wp-content/uploads/2024/12/3-3-2048x1171.png 2048w, /wp-content/uploads/2024/12/3-3-750x429.png 750w, /wp-content/uploads/2024/12/3-3-1140x652.png 1140w" sizes="(max-width: 2285px) 100vw, 2285px" /></a> <a href="/wp-content/uploads/2024/12/4-1.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2061" src="/wp-content/uploads/2024/12/4-1.png" alt="" width="2297" height="1314" srcset="/wp-content/uploads/2024/12/4-1.png 2297w, /wp-content/uploads/2024/12/4-1-300x172.png 300w, /wp-content/uploads/2024/12/4-1-1024x586.png 1024w, /wp-content/uploads/2024/12/4-1-768x439.png 768w, /wp-content/uploads/2024/12/4-1-1536x879.png 1536w, /wp-content/uploads/2024/12/4-1-2048x1172.png 2048w, /wp-content/uploads/2024/12/4-1-750x429.png 750w, /wp-content/uploads/2024/12/4-1-1140x652.png 1140w" sizes="(max-width: 2297px) 100vw, 2297px" /></a> <a href="/wp-content/uploads/2024/12/5-1.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2062" src="/wp-content/uploads/2024/12/5-1.png" alt="" width="2266" height="1319" srcset="/wp-content/uploads/2024/12/5-1.png 2266w, /wp-content/uploads/2024/12/5-1-300x175.png 300w, /wp-content/uploads/2024/12/5-1-1024x596.png 1024w, /wp-content/uploads/2024/12/5-1-768x447.png 768w, /wp-content/uploads/2024/12/5-1-1536x894.png 1536w, /wp-content/uploads/2024/12/5-1-2048x1192.png 2048w, /wp-content/uploads/2024/12/5-1-750x437.png 750w, /wp-content/uploads/2024/12/5-1-1140x664.png 1140w" sizes="(max-width: 2266px) 100vw, 2266px" /></a> <a href="/wp-content/uploads/2024/12/6-1.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2063" src="/wp-content/uploads/2024/12/6-1.png" alt="" width="2291" height="1316" srcset="/wp-content/uploads/2024/12/6-1.png 2291w, /wp-content/uploads/2024/12/6-1-300x172.png 300w, /wp-content/uploads/2024/12/6-1-1024x588.png 1024w, /wp-content/uploads/2024/12/6-1-768x441.png 768w, /wp-content/uploads/2024/12/6-1-1536x882.png 1536w, /wp-content/uploads/2024/12/6-1-2048x1176.png 2048w, /wp-content/uploads/2024/12/6-1-750x431.png 750w, /wp-content/uploads/2024/12/6-1-1140x655.png 1140w" sizes="(max-width: 2291px) 100vw, 2291px" /></a> <a href="/wp-content/uploads/2024/12/7-1.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2064" src="/wp-content/uploads/2024/12/7-1.png" alt="" width="1751" height="2021" srcset="/wp-content/uploads/2024/12/7-1.png 1751w, /wp-content/uploads/2024/12/7-1-260x300.png 260w, /wp-content/uploads/2024/12/7-1-887x1024.png 887w, /wp-content/uploads/2024/12/7-1-768x886.png 768w, /wp-content/uploads/2024/12/7-1-1331x1536.png 1331w, /wp-content/uploads/2024/12/7-1-750x866.png 750w, /wp-content/uploads/2024/12/7-1-1140x1316.png 1140w" sizes="(max-width: 1751px) 100vw, 1751px" /></a> <a href="/wp-content/uploads/2024/12/8-1.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2065" src="/wp-content/uploads/2024/12/8-1.png" alt="" width="2291" height="1311" srcset="/wp-content/uploads/2024/12/8-1.png 2291w, /wp-content/uploads/2024/12/8-1-300x172.png 300w, /wp-content/uploads/2024/12/8-1-1024x586.png 1024w, /wp-content/uploads/2024/12/8-1-768x439.png 768w, /wp-content/uploads/2024/12/8-1-1536x879.png 1536w, /wp-content/uploads/2024/12/8-1-2048x1172.png 2048w, /wp-content/uploads/2024/12/8-1-750x429.png 750w, /wp-content/uploads/2024/12/8-1-1140x652.png 1140w" sizes="(max-width: 2291px) 100vw, 2291px" /></a></p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">using System.Net;
using System.Net.Mail;

string smtpAuthUsername = "&lt;Azure Communication Services Resource name&gt;|&lt;Entra Application Id&gt;|&lt;Entra Application Tenant Id&gt;";
string smtpAuthPassword = "Entra Application Client Secret";
string sender = "DoNotReply@yourdomain.com";
string recipient = "testemail@gmail.com";
string subject = "Welcome to Azure Communication Service Email SMTP";
string body = "This email message is sent from Azure Communication Service Email using SMTP.";

string smtpHostUrl = "smtp.azurecomm.net";
var client = new SmtpClient(smtpHostUrl)
{
    Port = 587,
    Credentials = new NetworkCredential(smtpAuthUsername, smtpAuthPassword),
    EnableSsl = true
};

var message = new MailMessage(sender, recipient, subject, body);

try
{
    client.Send(message);
    Console.WriteLine("The email was successfully sent using Smtp.");
}
catch (Exception ex)
{
    Console.WriteLine($"Smtp send failed with the exception: {ex.Message}.");
}</pre>
<h2><span style="font-family: 'arial black', sans-serif;">6- Use Azure Communication Service with PowerShell</span></h2>
<p><a href="/wp-content/uploads/2024/12/Sans-titre.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2066" src="/wp-content/uploads/2024/12/Sans-titre.png" alt="" width="2281" height="1375" srcset="/wp-content/uploads/2024/12/Sans-titre.png 2281w, /wp-content/uploads/2024/12/Sans-titre-300x181.png 300w, /wp-content/uploads/2024/12/Sans-titre-1024x617.png 1024w, /wp-content/uploads/2024/12/Sans-titre-768x463.png 768w, /wp-content/uploads/2024/12/Sans-titre-1536x926.png 1536w, /wp-content/uploads/2024/12/Sans-titre-2048x1235.png 2048w, /wp-content/uploads/2024/12/Sans-titre-750x452.png 750w, /wp-content/uploads/2024/12/Sans-titre-1140x687.png 1140w" sizes="(max-width: 2281px) 100vw, 2281px" /></a></p>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">$Password = ConvertTo-SecureString -AsPlainText -Force -String 'Entra Application Client Secret'
$Cred = New-Object -TypeName PSCredential -ArgumentList '&lt;Azure Communication Services Resource name&gt;|&lt;Entra Application ID&gt;|&lt;Entra Tenant ID&gt;', $Password
Send-MailMessage -From 'DoNotReply@yourdomain.com' -To 'email to test' -Subject 'Test mail' -Body 'test' -SmtpServer 'smtp.azurecomm.net' -Port 587 -Credential $Cred -UseSsl</pre>
<h2><span style="font-family: 'arial black', sans-serif;">7- Sample : sending A news Letter to multi Emails with Attachment</span></h2>
<p>Azure Communication Services also support sending emails to multiple recipients and attaching files. For more details, refer to this guide: <a href="https://learn.microsoft.com/en-us/azure/communication-services/quickstarts/email/send-email-advanced/send-email-to-multiple-recipients?wt.mc_id=MVP_328341&amp;tabs=connection-string&amp;pivots=programming-language-csharp">Send email to multiple recipients</a></p>
<h2><span style="font-family: 'arial black', sans-serif;"><br />
<img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2068" src="/wp-content/uploads/2024/12/1-4.png" alt="" width="1129" height="929" srcset="/wp-content/uploads/2024/12/1-4.png 1129w, /wp-content/uploads/2024/12/1-4-300x247.png 300w, /wp-content/uploads/2024/12/1-4-1024x843.png 1024w, /wp-content/uploads/2024/12/1-4-768x632.png 768w, /wp-content/uploads/2024/12/1-4-750x617.png 750w" sizes="(max-width: 1129px) 100vw, 1129px" /><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2069" src="/wp-content/uploads/2024/12/sample.png" alt="" width="1120" height="1354" srcset="/wp-content/uploads/2024/12/sample.png 1120w, /wp-content/uploads/2024/12/sample-248x300.png 248w, /wp-content/uploads/2024/12/sample-847x1024.png 847w, /wp-content/uploads/2024/12/sample-768x928.png 768w, /wp-content/uploads/2024/12/sample-750x907.png 750w" sizes="(max-width: 1120px) 100vw, 1120px" /></span></h2>
<pre class="EnlighterJSRAW" data-enlighter-language="generic">using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Mime;
using System.Threading.Tasks;
using Azure;
using Azure.Communication.Email;

namespace SendEmail
{
    internal class Program
    {
        static async Task Main(string[] args)
        {
            // This code demonstrates how to fetch your connection string from an environment variable.
            string connectionString = "endpoint=............................................................";
            EmailClient emailClient = new EmailClient(connectionString);

            // Create the email content
            var emailContent = new EmailContent("Newsletter from Achraf Ben Alaya")
            {
                PlainText = "This is the latest news from Achraf Ben Alaya's website.",
                Html = @"
                &lt;html&gt;
                &lt;body&gt;
                    &lt;h1&gt;Latest News from Achraf Ben Alaya&lt;/h1&gt;
                    &lt;p&gt;Dear Subscriber,&lt;/p&gt;
                    &lt;p&gt;We are excited to share the latest updates from &lt;a href='https://achrafbenalaya.com'&gt;achrafbenalaya.com&lt;/a&gt;.&lt;/p&gt;
                    &lt;h2&gt;New Features&lt;/h2&gt;
                    &lt;ul&gt;
                        &lt;li&gt;Feature 1: Description of feature 1.&lt;/li&gt;
                        &lt;li&gt;Feature 2: Description of feature 2.&lt;/li&gt;
                        &lt;li&gt;Feature 3: Description of feature 3.&lt;/li&gt;
                    &lt;/ul&gt;
                    &lt;h2&gt;Upcoming Events&lt;/h2&gt;
                    &lt;p&gt;Stay tuned for our upcoming events:&lt;/p&gt;
                    &lt;ul&gt;
                        &lt;li&gt;Event 1: Date and details of event 1.&lt;/li&gt;
                        &lt;li&gt;Event 2: Date and details of event 2.&lt;/li&gt;
                    &lt;/ul&gt;
                    &lt;p&gt;Thank you for being a valued subscriber.&lt;/p&gt;
                    &lt;p&gt;Best regards,&lt;br/&gt;Achraf Ben Alaya&lt;/p&gt;
                &lt;/body&gt;
                &lt;/html&gt;"
            };

            // Create the To list
            var toRecipients = new List&lt;EmailAddress&gt;
            {
                new EmailAddress("youremail@outlook.com"),
                new EmailAddress("ryle.kutler@finestudio.org"),
            };

            // Create the CC list
            var ccRecipients = new List&lt;EmailAddress&gt;
            {
                new EmailAddress("ripejo9625@bawsny.com"),
            };

            // Create the BCC list
            var bccRecipients = new List&lt;EmailAddress&gt;
            {
                new EmailAddress("wizorvlad@hulas.me"),
            };

            EmailRecipients emailRecipients = new EmailRecipients(toRecipients, ccRecipients, bccRecipients);

            // Create the EmailMessage
            var emailMessage = new EmailMessage(
                senderAddress: "DoNotReply@ihelpyoutodo.com",
                emailRecipients,
                emailContent);

            // Add optional ReplyTo address which is where any replies to the email will go to.
            // emailMessage.ReplyTo.Add(new EmailAddress("&lt;replytoemailalias@emaildomain.com&gt;"));

            // Create the EmailAttachment
            var filePath = @"";
            byte[] bytes = File.ReadAllBytes(filePath);
            var contentBinaryData = new BinaryData(bytes);
            var emailAttachment = new EmailAttachment("micso.png", MediaTypeNames.Application.Pdf, contentBinaryData);

            emailMessage.Attachments.Add(emailAttachment);

            try
            {
                EmailSendOperation emailSendOperation = await emailClient.SendAsync(WaitUntil.Completed, emailMessage);
                Console.WriteLine($"Email Sent. Status = {emailSendOperation.Value.Status}");

                // Get the OperationId so that it can be used for tracking the message for troubleshooting
                string operationId = emailSendOperation.Id;
                Console.WriteLine($"Email operation id = {operationId}");
            }
            catch (RequestFailedException ex)
            {
                // OperationID is contained in the exception message and can be used for troubleshooting purposes
                Console.WriteLine($"Email send operation failed with error code: {ex.ErrorCode}, message: {ex.Message}");
            }
        }
    }
}</pre>
<h2><span style="font-family: 'arial black', sans-serif;">8 &#8211; Monitoring<br />
</span></h2>
</div>
<p>Communications Services provides monitoring and analytics features via Azure Monitor Logs overview and Azure Monitor Metrics. Each Azure resource requires its own diagnostic setting :</p>
<div>
<p><a href="/wp-content/uploads/2024/12/m1.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2071" src="/wp-content/uploads/2024/12/m1.png" alt="" width="806" height="1095" srcset="/wp-content/uploads/2024/12/m1.png 806w, /wp-content/uploads/2024/12/m1-221x300.png 221w, /wp-content/uploads/2024/12/m1-754x1024.png 754w, /wp-content/uploads/2024/12/m1-768x1043.png 768w, /wp-content/uploads/2024/12/m1-750x1019.png 750w" sizes="(max-width: 806px) 100vw, 806px" /></a></p>
<p><a href="/wp-content/uploads/2024/12/m2.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2079" src="/wp-content/uploads/2024/12/m2.png" alt="" width="2101" height="1290" srcset="/wp-content/uploads/2024/12/m2.png 2101w, /wp-content/uploads/2024/12/m2-300x184.png 300w, /wp-content/uploads/2024/12/m2-1024x629.png 1024w, /wp-content/uploads/2024/12/m2-768x472.png 768w, /wp-content/uploads/2024/12/m2-1536x943.png 1536w, /wp-content/uploads/2024/12/m2-2048x1257.png 2048w, /wp-content/uploads/2024/12/m2-750x460.png 750w, /wp-content/uploads/2024/12/m2-1140x700.png 1140w" sizes="(max-width: 2101px) 100vw, 2101px" /></a>Once diagnostic settings are activated, you can monitor email activity directly from the Azure portal. This includes viewing metrics such as the number of emails sent, failed, and the total success rate. Additionally, you can filter the data by sender to gain more detailed insights. <a href="/wp-content/uploads/2024/12/m3.png"><img loading="lazy" decoding="async" class="aligncenter size-full wp-image-2080" src="/wp-content/uploads/2024/12/m3.png" alt="" width="2084" height="1217" srcset="/wp-content/uploads/2024/12/m3.png 2084w, /wp-content/uploads/2024/12/m3-300x175.png 300w, /wp-content/uploads/2024/12/m3-1024x598.png 1024w, /wp-content/uploads/2024/12/m3-768x448.png 768w, /wp-content/uploads/2024/12/m3-1536x897.png 1536w, /wp-content/uploads/2024/12/m3-2048x1196.png 2048w, /wp-content/uploads/2024/12/m3-750x438.png 750w, /wp-content/uploads/2024/12/m3-1140x666.png 1140w" sizes="(max-width: 2084px) 100vw, 2084px" /></a></p>
</div>
<p><!--ScriptorEndFragment--></p>
</div>
<p><!--ScriptorEndFragment--></p>
]]></content:encoded>
					
					<wfw:commentRss>https://achrafbenalaya.com/2024/12/08/azure-communication-services-email-sending-simplified-from-setup-to-execution-and-monitoring/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2022</post-id>	</item>
	</channel>
</rss>
