<?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>Cloud &#8211; achraf ben alaya</title>
	<atom:link href="https://achrafbenalaya.com/category/blog/cloud/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>Cloud &#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> 2025 – Certifications, Community, and 50K Views</title>
		<link>https://achrafbenalaya.com/2025/12/28/2025-certifications-community-and-50k-views/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=2025-certifications-community-and-50k-views</link>
					<comments>https://achrafbenalaya.com/2025/12/28/2025-certifications-community-and-50k-views/#respond</comments>
		
		<dc:creator><![CDATA[achraf]]></dc:creator>
		<pubDate>Sun, 28 Dec 2025 12:49:10 +0000</pubDate>
				<category><![CDATA[Blog]]></category>
		<category><![CDATA[Cloud]]></category>
		<guid isPermaLink="false">https://achrafbenalaya.com/?p=2414</guid>

					<description><![CDATA[2025 is coming to an end, and looking back, this has been one of my most intense and rewarding years so far. Between new certifications, speaking engagements, content experiments, and a lot of amazing people, I feel both grateful and excited for what’s next 50K views: the blog is growing up This year, my blog [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>2025 is coming to an end, and looking back, this has been one of my most intense and rewarding years so far. Between new certifications, speaking engagements, content experiments, and a lot of amazing people, I feel both grateful and excited for what’s next</p>



<h2 class="wp-block-heading">50K views: the blog is growing up</h2>



<p>This year, my blog crossed the milestone of 50,000 all‑time views. Seeing that number on the analytics dashboard was a strong reminder that consistent sharing, even in small pieces, compounds over time. It is still a personal blog, but it is slowly becoming a useful resource for people starting or growing in cloud and AI.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img decoding="async" width="472" height="1024" src="/wp-content/uploads/2025/12/IMG_1606-1-472x1024.png" alt="" class="wp-image-2416" srcset="/wp-content/uploads/2025/12/IMG_1606-1-472x1024.png 472w, /wp-content/uploads/2025/12/IMG_1606-1-138x300.png 138w, /wp-content/uploads/2025/12/IMG_1606-1.png 476w" sizes="(max-width: 472px) 100vw, 472px" /></figure></div>


<h2 class="wp-block-heading">Certifications: keeping skills sharp<br></h2>



<p>On the learning side, I obtained two new certifications: one from Google Cloud and one from Microsoft Azure. I also renewed all my existing Azure certifications to stay aligned with the latest changes in the platform. Investing time in certification tracks is never just about the badge; it forces me to structure my knowledge, fill gaps, and bring fresher content back to my sessions, posts, and workshops.</p>



<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-large"><img decoding="async" width="1024" height="731" data-id="2430" src="/wp-content/uploads/2025/12/1746799852916-1024x731.jpg" alt="" class="wp-image-2430" srcset="/wp-content/uploads/2025/12/1746799852916-1024x731.jpg 1024w, /wp-content/uploads/2025/12/1746799852916-300x214.jpg 300w, /wp-content/uploads/2025/12/1746799852916-768x548.jpg 768w, /wp-content/uploads/2025/12/1746799852916-1536x1096.jpg 1536w, /wp-content/uploads/2025/12/1746799852916-120x86.jpg 120w, /wp-content/uploads/2025/12/1746799852916-350x250.jpg 350w, /wp-content/uploads/2025/12/1746799852916-750x535.jpg 750w, /wp-content/uploads/2025/12/1746799852916-1140x814.jpg 1140w, /wp-content/uploads/2025/12/1746799852916.jpg 1621w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="727" data-id="2431" src="/wp-content/uploads/2025/12/1746799852921-1024x727.jpg" alt="" class="wp-image-2431" srcset="/wp-content/uploads/2025/12/1746799852921-1024x727.jpg 1024w, /wp-content/uploads/2025/12/1746799852921-300x213.jpg 300w, /wp-content/uploads/2025/12/1746799852921-768x545.jpg 768w, /wp-content/uploads/2025/12/1746799852921-1536x1090.jpg 1536w, /wp-content/uploads/2025/12/1746799852921-120x86.jpg 120w, /wp-content/uploads/2025/12/1746799852921-750x532.jpg 750w, /wp-content/uploads/2025/12/1746799852921-1140x809.jpg 1140w, /wp-content/uploads/2025/12/1746799852921.jpg 1625w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="969" height="589" data-id="2432" src="/wp-content/uploads/2025/12/1750839852590.jpg" alt="" class="wp-image-2432" srcset="/wp-content/uploads/2025/12/1750839852590.jpg 969w, /wp-content/uploads/2025/12/1750839852590-300x182.jpg 300w, /wp-content/uploads/2025/12/1750839852590-768x467.jpg 768w, /wp-content/uploads/2025/12/1750839852590-750x456.jpg 750w" sizes="(max-width: 969px) 100vw, 969px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="724" data-id="2434" src="/wp-content/uploads/2025/12/1760211711536-1024x724.jpg" alt="" class="wp-image-2434" srcset="/wp-content/uploads/2025/12/1760211711536-1024x724.jpg 1024w, /wp-content/uploads/2025/12/1760211711536-300x212.jpg 300w, /wp-content/uploads/2025/12/1760211711536-768x543.jpg 768w, /wp-content/uploads/2025/12/1760211711536-120x86.jpg 120w, /wp-content/uploads/2025/12/1760211711536-750x531.jpg 750w, /wp-content/uploads/2025/12/1760211711536-1140x806.jpg 1140w, /wp-content/uploads/2025/12/1760211711536.jpg 1483w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="719" data-id="2433" src="/wp-content/uploads/2025/12/1760211711597-1024x719.jpg" alt="" class="wp-image-2433" srcset="/wp-content/uploads/2025/12/1760211711597-1024x719.jpg 1024w, /wp-content/uploads/2025/12/1760211711597-300x211.jpg 300w, /wp-content/uploads/2025/12/1760211711597-768x539.jpg 768w, /wp-content/uploads/2025/12/1760211711597-750x526.jpg 750w, /wp-content/uploads/2025/12/1760211711597-1140x800.jpg 1140w, /wp-content/uploads/2025/12/1760211711597.jpg 1485w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="1024" data-id="2435" src="/wp-content/uploads/2025/12/1763981576345-1024x1024.jpg" alt="" class="wp-image-2435" srcset="/wp-content/uploads/2025/12/1763981576345-1024x1024.jpg 1024w, /wp-content/uploads/2025/12/1763981576345-300x300.jpg 300w, /wp-content/uploads/2025/12/1763981576345-150x150.jpg 150w, /wp-content/uploads/2025/12/1763981576345-768x768.jpg 768w, /wp-content/uploads/2025/12/1763981576345-75x75.jpg 75w, /wp-content/uploads/2025/12/1763981576345-750x750.jpg 750w, /wp-content/uploads/2025/12/1763981576345.jpg 1040w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>
</figure>



<h2 class="wp-block-heading">Speaking and community events</h2>



<p>Community has been at the center of this year. I had the chance to participate in several events and share my experience: Festive Tech Calendar, Azure User Group Sweden, WeDoAI 2025, and Global Azure 2025, among others. Each event was an opportunity to meet passionate people, exchange ideas, and hopefully make cloud and AI topics a little more accessible for attendees.</p>



<figure class="wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-2 is-layout-flex wp-block-gallery-is-layout-flex">
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="1024" data-id="2422" src="/wp-content/uploads/2025/12/1755849354683-1024x1024.jpg" alt="" class="wp-image-2422" srcset="/wp-content/uploads/2025/12/1755849354683-1024x1024.jpg 1024w, /wp-content/uploads/2025/12/1755849354683-300x300.jpg 300w, /wp-content/uploads/2025/12/1755849354683-150x150.jpg 150w, /wp-content/uploads/2025/12/1755849354683-768x768.jpg 768w, /wp-content/uploads/2025/12/1755849354683-75x75.jpg 75w, /wp-content/uploads/2025/12/1755849354683-750x750.jpg 750w, /wp-content/uploads/2025/12/1755849354683.jpg 1080w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="1024" data-id="2421" src="/wp-content/uploads/2025/12/1764602953692-1024x1024.jpg" alt="" class="wp-image-2421" srcset="/wp-content/uploads/2025/12/1764602953692-1024x1024.jpg 1024w, /wp-content/uploads/2025/12/1764602953692-300x300.jpg 300w, /wp-content/uploads/2025/12/1764602953692-150x150.jpg 150w, /wp-content/uploads/2025/12/1764602953692-768x768.jpg 768w, /wp-content/uploads/2025/12/1764602953692-75x75.jpg 75w, /wp-content/uploads/2025/12/1764602953692-750x750.jpg 750w, /wp-content/uploads/2025/12/1764602953692.jpg 1080w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="788" height="547" data-id="2424" src="/wp-content/uploads/2025/12/Sans-titre.png" alt="" class="wp-image-2424" srcset="/wp-content/uploads/2025/12/Sans-titre.png 788w, /wp-content/uploads/2025/12/Sans-titre-300x208.png 300w, /wp-content/uploads/2025/12/Sans-titre-768x533.png 768w, /wp-content/uploads/2025/12/Sans-titre-750x521.png 750w" sizes="(max-width: 788px) 100vw, 788px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="674" height="477" data-id="2423" src="/wp-content/uploads/2025/12/sweeden.png" alt="" class="wp-image-2423" srcset="/wp-content/uploads/2025/12/sweeden.png 674w, /wp-content/uploads/2025/12/sweeden-300x212.png 300w, /wp-content/uploads/2025/12/sweeden-120x86.png 120w" sizes="(max-width: 674px) 100vw, 674px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="417" height="565" data-id="2427" src="/wp-content/uploads/2025/12/1744706494017.jpg" alt="" class="wp-image-2427" srcset="/wp-content/uploads/2025/12/1744706494017.jpg 417w, /wp-content/uploads/2025/12/1744706494017-221x300.jpg 221w" sizes="(max-width: 417px) 100vw, 417px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="519" height="1024" data-id="2428" src="/wp-content/uploads/2025/12/1744706494262-519x1024.jpg" alt="" class="wp-image-2428" srcset="/wp-content/uploads/2025/12/1744706494262-519x1024.jpg 519w, /wp-content/uploads/2025/12/1744706494262-152x300.jpg 152w, /wp-content/uploads/2025/12/1744706494262.jpg 572w" sizes="(max-width: 519px) 100vw, 519px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="576" data-id="2429" src="/wp-content/uploads/2025/12/1745852943065-1024x576.jpg" alt="" class="wp-image-2429" srcset="/wp-content/uploads/2025/12/1745852943065-1024x576.jpg 1024w, /wp-content/uploads/2025/12/1745852943065-300x169.jpg 300w, /wp-content/uploads/2025/12/1745852943065-768x432.jpg 768w, /wp-content/uploads/2025/12/1745852943065-750x422.jpg 750w, /wp-content/uploads/2025/12/1745852943065-1140x641.jpg 1140w, /wp-content/uploads/2025/12/1745852943065.jpg 1280w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>
</figure>



<h2 class="wp-block-heading">MVP &amp; MCT renewals</h2>



<p>Another highlight was renewing both my Microsoft MVP and Microsoft Certified Trainer status for one more year. Both recognitions are special to me because they are directly connected to sharing knowledge and helping the community grow. They also come with a responsibility: to keep learning, keep contributing, and keep showing up.</p>



<figure class="wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-3 is-layout-flex wp-block-gallery-is-layout-flex">
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="675" height="629" data-id="2443" src="/wp-content/uploads/2025/12/1752163190223-1.jpg" alt="" class="wp-image-2443" srcset="/wp-content/uploads/2025/12/1752163190223-1.jpg 675w, /wp-content/uploads/2025/12/1752163190223-1-300x280.jpg 300w" sizes="(max-width: 675px) 100vw, 675px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="715" data-id="2444" src="/wp-content/uploads/2025/12/1763981576430-1-2-1024x715.jpg" alt="" class="wp-image-2444" srcset="/wp-content/uploads/2025/12/1763981576430-1-2-1024x715.jpg 1024w, /wp-content/uploads/2025/12/1763981576430-1-2-300x210.jpg 300w, /wp-content/uploads/2025/12/1763981576430-1-2-768x536.jpg 768w, /wp-content/uploads/2025/12/1763981576430-1-2-750x524.jpg 750w, /wp-content/uploads/2025/12/1763981576430-1-2.jpg 1121w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="1024" data-id="2445" src="/wp-content/uploads/2025/12/1766586952357-2-1024x1024.jpg" alt="" class="wp-image-2445" srcset="/wp-content/uploads/2025/12/1766586952357-2-1024x1024.jpg 1024w, /wp-content/uploads/2025/12/1766586952357-2-300x300.jpg 300w, /wp-content/uploads/2025/12/1766586952357-2-150x150.jpg 150w, /wp-content/uploads/2025/12/1766586952357-2-768x768.jpg 768w, /wp-content/uploads/2025/12/1766586952357-2-75x75.jpg 75w, /wp-content/uploads/2025/12/1766586952357-2-750x750.jpg 750w, /wp-content/uploads/2025/12/1766586952357-2.jpg 1080w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>
</figure>



<h2 class="wp-block-heading">A new AI &amp; GitHub Copilot corner</h2>



<p>This year, I also introduced a new AI‑focused section on the blog, with a special emphasis on GitHub Copilot. The goal is to document how AI can practically support developers in their day‑to‑day work—from prototyping and refactoring to documentation and testing. Expect more deep dives, “how I actually use it” posts, and realistic scenarios rather than just high‑level theory.</p>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="996" height="144" src="/wp-content/uploads/2025/12/image-6.png" alt="" class="wp-image-2446" srcset="/wp-content/uploads/2025/12/image-6.png 996w, /wp-content/uploads/2025/12/image-6-300x43.png 300w, /wp-content/uploads/2025/12/image-6-768x111.png 768w, /wp-content/uploads/2025/12/image-6-750x108.png 750w" sizes="(max-width: 996px) 100vw, 996px" /></figure>



<h2 class="wp-block-heading">YouTube: not at 1K yet, and that’s OK<br></h2>



<p>On YouTube, I did not reach the symbolic 1,000‑subscriber mark yet; I am currently at 811. That said, the channel has a clear direction now: more focused series, shorter and actionable videos, and better alignment with what you read on the blog. The plan for next year is to bring more consistent content, experiment with playlists around certifications, best practices, architecture, and cost optimization, and hopefully cross that 1K line with a stronger community behind it.</p>


<div class="wp-block-image">
<figure class="aligncenter size-large"><img loading="lazy" decoding="async" width="1024" height="1024" src="/wp-content/uploads/2025/12/UCsP8qtUrdi1o0s7HnGVDtYQ-views-125000-2025-12-12--1024x1024.png" alt="" class="wp-image-2458" srcset="/wp-content/uploads/2025/12/UCsP8qtUrdi1o0s7HnGVDtYQ-views-125000-2025-12-12--1024x1024.png 1024w, /wp-content/uploads/2025/12/UCsP8qtUrdi1o0s7HnGVDtYQ-views-125000-2025-12-12--300x300.png 300w, /wp-content/uploads/2025/12/UCsP8qtUrdi1o0s7HnGVDtYQ-views-125000-2025-12-12--150x150.png 150w, /wp-content/uploads/2025/12/UCsP8qtUrdi1o0s7HnGVDtYQ-views-125000-2025-12-12--768x768.png 768w, /wp-content/uploads/2025/12/UCsP8qtUrdi1o0s7HnGVDtYQ-views-125000-2025-12-12--75x75.png 75w, /wp-content/uploads/2025/12/UCsP8qtUrdi1o0s7HnGVDtYQ-views-125000-2025-12-12--750x750.png 750w, /wp-content/uploads/2025/12/UCsP8qtUrdi1o0s7HnGVDtYQ-views-125000-2025-12-12--1140x1140.png 1140w, /wp-content/uploads/2025/12/UCsP8qtUrdi1o0s7HnGVDtYQ-views-125000-2025-12-12-.png 1360w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure></div>


<h2 class="wp-block-heading">A year full of change<br></h2>



<p>Professionally and personally, 2025 brought a lot of changes. Some were planned, others completely unexpected, but each one came with lessons and new connections. I am grateful for everyone I met this year online or in person who shared questions, feedback, opportunities, or just a friendly message. These interactions are a big part of why I keep creating content.</p>



<h2 class="wp-block-heading">Looking ahead to 2026<br></h2>



<p>For the next year, the focus is on smaller, high‑value content series both on the blog and on YouTube. Think short, themed mini‑series about cloud architecture, security fundamentals, real‑world best practices, optimization and cost savings, and of course AI. I also want to do more collaborations, and I already have a long list of people in mind from the community that I’d love to co‑create with.</p>



<p>On a more personal note, one of my goals is finally building my first custom PC. With RAM prices going up, the timing is not perfect, but the plan is to target a build in the 1–2K range and see what kind of balanced workstation we can put together for content creation, labs, and maybe some light gaming.</p>



<p>To everyone who read the blog, attended a session, watched a video, or simply reached out this year: thank you. I wish you a happy new year and all the best for the next one. More content, more learning, and more collaboration are on the way see you in 2026.</p>



<figure class="wp-block-gallery has-nested-images columns-default is-cropped wp-block-gallery-4 is-layout-flex wp-block-gallery-is-layout-flex">
<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="1024" data-id="2449" src="/wp-content/uploads/2025/12/rewind-slide-1-1024x1024.png" alt="" class="wp-image-2449" srcset="/wp-content/uploads/2025/12/rewind-slide-1-1024x1024.png 1024w, /wp-content/uploads/2025/12/rewind-slide-1-300x300.png 300w, /wp-content/uploads/2025/12/rewind-slide-1-150x150.png 150w, /wp-content/uploads/2025/12/rewind-slide-1-768x768.png 768w, /wp-content/uploads/2025/12/rewind-slide-1-75x75.png 75w, /wp-content/uploads/2025/12/rewind-slide-1-750x750.png 750w, /wp-content/uploads/2025/12/rewind-slide-1.png 1080w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="1024" data-id="2451" src="/wp-content/uploads/2025/12/rewind-slide-2-1024x1024.png" alt="" class="wp-image-2451" srcset="/wp-content/uploads/2025/12/rewind-slide-2-1024x1024.png 1024w, /wp-content/uploads/2025/12/rewind-slide-2-300x300.png 300w, /wp-content/uploads/2025/12/rewind-slide-2-150x150.png 150w, /wp-content/uploads/2025/12/rewind-slide-2-768x768.png 768w, /wp-content/uploads/2025/12/rewind-slide-2-75x75.png 75w, /wp-content/uploads/2025/12/rewind-slide-2-750x750.png 750w, /wp-content/uploads/2025/12/rewind-slide-2.png 1080w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="1024" data-id="2447" src="/wp-content/uploads/2025/12/rewind-slide-5-1024x1024.png" alt="" class="wp-image-2447" srcset="/wp-content/uploads/2025/12/rewind-slide-5-1024x1024.png 1024w, /wp-content/uploads/2025/12/rewind-slide-5-300x300.png 300w, /wp-content/uploads/2025/12/rewind-slide-5-150x150.png 150w, /wp-content/uploads/2025/12/rewind-slide-5-768x768.png 768w, /wp-content/uploads/2025/12/rewind-slide-5-75x75.png 75w, /wp-content/uploads/2025/12/rewind-slide-5-750x750.png 750w, /wp-content/uploads/2025/12/rewind-slide-5.png 1080w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="1024" data-id="2450" src="/wp-content/uploads/2025/12/rewind-slide-7-1024x1024.png" alt="" class="wp-image-2450" srcset="/wp-content/uploads/2025/12/rewind-slide-7-1024x1024.png 1024w, /wp-content/uploads/2025/12/rewind-slide-7-300x300.png 300w, /wp-content/uploads/2025/12/rewind-slide-7-150x150.png 150w, /wp-content/uploads/2025/12/rewind-slide-7-768x768.png 768w, /wp-content/uploads/2025/12/rewind-slide-7-75x75.png 75w, /wp-content/uploads/2025/12/rewind-slide-7-750x750.png 750w, /wp-content/uploads/2025/12/rewind-slide-7.png 1080w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="1024" data-id="2448" src="/wp-content/uploads/2025/12/rewind-slide-8-1024x1024.png" alt="" class="wp-image-2448" srcset="/wp-content/uploads/2025/12/rewind-slide-8-1024x1024.png 1024w, /wp-content/uploads/2025/12/rewind-slide-8-300x300.png 300w, /wp-content/uploads/2025/12/rewind-slide-8-150x150.png 150w, /wp-content/uploads/2025/12/rewind-slide-8-768x768.png 768w, /wp-content/uploads/2025/12/rewind-slide-8-75x75.png 75w, /wp-content/uploads/2025/12/rewind-slide-8-750x750.png 750w, /wp-content/uploads/2025/12/rewind-slide-8.png 1080w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="1024" data-id="2452" src="/wp-content/uploads/2025/12/rewind-slide-12-1024x1024.png" alt="" class="wp-image-2452" srcset="/wp-content/uploads/2025/12/rewind-slide-12-1024x1024.png 1024w, /wp-content/uploads/2025/12/rewind-slide-12-300x300.png 300w, /wp-content/uploads/2025/12/rewind-slide-12-150x150.png 150w, /wp-content/uploads/2025/12/rewind-slide-12-768x768.png 768w, /wp-content/uploads/2025/12/rewind-slide-12-75x75.png 75w, /wp-content/uploads/2025/12/rewind-slide-12-750x750.png 750w, /wp-content/uploads/2025/12/rewind-slide-12.png 1080w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="1024" data-id="2456" src="/wp-content/uploads/2025/12/rewind-slide-14-1024x1024.png" alt="" class="wp-image-2456" srcset="/wp-content/uploads/2025/12/rewind-slide-14-1024x1024.png 1024w, /wp-content/uploads/2025/12/rewind-slide-14-300x300.png 300w, /wp-content/uploads/2025/12/rewind-slide-14-150x150.png 150w, /wp-content/uploads/2025/12/rewind-slide-14-768x768.png 768w, /wp-content/uploads/2025/12/rewind-slide-14-75x75.png 75w, /wp-content/uploads/2025/12/rewind-slide-14-750x750.png 750w, /wp-content/uploads/2025/12/rewind-slide-14.png 1080w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="1024" data-id="2454" src="/wp-content/uploads/2025/12/rewind-slide-18-1024x1024.png" alt="" class="wp-image-2454" srcset="/wp-content/uploads/2025/12/rewind-slide-18-1024x1024.png 1024w, /wp-content/uploads/2025/12/rewind-slide-18-300x300.png 300w, /wp-content/uploads/2025/12/rewind-slide-18-150x150.png 150w, /wp-content/uploads/2025/12/rewind-slide-18-768x768.png 768w, /wp-content/uploads/2025/12/rewind-slide-18-75x75.png 75w, /wp-content/uploads/2025/12/rewind-slide-18-750x750.png 750w, /wp-content/uploads/2025/12/rewind-slide-18.png 1080w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="1024" data-id="2455" src="/wp-content/uploads/2025/12/rewind-slide-19-1024x1024.png" alt="" class="wp-image-2455" srcset="/wp-content/uploads/2025/12/rewind-slide-19-1024x1024.png 1024w, /wp-content/uploads/2025/12/rewind-slide-19-300x300.png 300w, /wp-content/uploads/2025/12/rewind-slide-19-150x150.png 150w, /wp-content/uploads/2025/12/rewind-slide-19-768x768.png 768w, /wp-content/uploads/2025/12/rewind-slide-19-75x75.png 75w, /wp-content/uploads/2025/12/rewind-slide-19-750x750.png 750w, /wp-content/uploads/2025/12/rewind-slide-19.png 1080w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>
</figure>
]]></content:encoded>
					
					<wfw:commentRss>https://achrafbenalaya.com/2025/12/28/2025-certifications-community-and-50k-views/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2414</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 loading="lazy" 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 loading="lazy" 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>Model Context Protocol (MCP): The Future of AI Integration</title>
		<link>https://achrafbenalaya.com/2025/04/21/model-context-protocol-mcp-the-future-of-ai-integration/?utm_source=rss&#038;utm_medium=rss&#038;utm_campaign=model-context-protocol-mcp-the-future-of-ai-integration</link>
					<comments>https://achrafbenalaya.com/2025/04/21/model-context-protocol-mcp-the-future-of-ai-integration/#respond</comments>
		
		<dc:creator><![CDATA[achraf]]></dc:creator>
		<pubDate>Mon, 21 Apr 2025 12:03:04 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[Blog]]></category>
		<category><![CDATA[Cloud]]></category>
		<category><![CDATA[mcp]]></category>
		<guid isPermaLink="false">https://achrafbenalaya.com/?p=2207</guid>

					<description><![CDATA[For the last couple of weeks, I&#8217;ve been seeing MCP servers and clients shared everywhere by different people. With all this buzz, I decided it was time to explore what MCP is, what it can be used for, and whether it represents a revolution in AI. This is my first blog post on the topic, [&#8230;]]]></description>
										<content:encoded><![CDATA[
<p>For the last couple of weeks, I&#8217;ve been seeing MCP servers and clients shared everywhere by different people. With all this buzz, I decided it was time to explore what MCP is, what it can be used for, and whether it represents a revolution in AI. This is my first blog post on the topic, and I plan to continue writing about it because I believe we&#8217;re entering a new era of AI integration.</p>



<h3 class="wp-block-heading">What is MCP (Model Context Protocol)?</h3>



<p>The Model Context Protocol (MCP) is an open protocol developed by Anthropic that enables large language models (LLMs) to integrate with external data sources and tools in a standardized way. It creates a framework that allows AI tools to communicate with each other seamlessly.</p>



<p>In simpler terms, instead of building custom adapters every time we want an AI tool to perform a specific function—like reading a Figma design or managing a database—MCP lets us plug that tool in directly, provided our main AI interface understands the protocol.</p>



<p>MCP defines how AI models can:</p>



<ul class="wp-block-list">
<li>Call external tools</li>



<li>Fetch data</li>



<li>Interact with various services</li>



<li>Access context in a manner that&#8217;s generalizable across different integrations<br><br></li>
</ul>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="561" src="/wp-content/uploads/2025/04/Untitled-2025-02-21-0925-1024x561.png" alt="" class="wp-image-2211" srcset="/wp-content/uploads/2025/04/Untitled-2025-02-21-0925-1024x561.png 1024w, /wp-content/uploads/2025/04/Untitled-2025-02-21-0925-300x164.png 300w, /wp-content/uploads/2025/04/Untitled-2025-02-21-0925-768x421.png 768w, /wp-content/uploads/2025/04/Untitled-2025-02-21-0925-1536x842.png 1536w, /wp-content/uploads/2025/04/Untitled-2025-02-21-0925-2048x1122.png 2048w, /wp-content/uploads/2025/04/Untitled-2025-02-21-0925-750x411.png 750w, /wp-content/uploads/2025/04/Untitled-2025-02-21-0925-1140x625.png 1140w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading">Why Use MCP?</h2>



<p>Consider the traditional approach when you want your AI model to connect to your resources. You would need to:</p>



<ol class="wp-block-list">
<li>Ask your developers to create custom connectors or extensions</li>



<li>Create web APIs</li>



<li>Handle authentication</li>



<li>Develop additional functionality as needs arise</li>
</ol>



<p>MCP eliminates this complexity by providing a standardized way for AI models to interact with external tools and data sources.</p>



<h2 class="wp-block-heading">Real-World Example: Updating My Microsoft Acronyms Project</h2>



<p>To demonstrate MCP&#8217;s capabilities, I tested it on a small project I created two years ago: <a href="https://himhelloworld.com/">Microsoft Acronyms</a>. Since I&#8217;ve been busy with work, family, and other projects, I hadn&#8217;t updated it with the latest acronyms.</p>



<h3 class="wp-block-heading">Traditional Process (Without MCP)</h3>



<p>Without MCP, updating the site would require me to:</p>



<ol class="wp-block-list">
<li>Go to GitHub and examine the project structure</li>



<li>Search the internet for new Microsoft acronyms</li>



<li>Compare them against what&#8217;s already on my website</li>



<li>Consult an AI model like GPT about the newest acronyms</li>



<li>Pull the project</li>



<li>Create a branch</li>



<li>Update the file</li>



<li>Push changes</li>



<li>Create a pull request</li>
</ol>



<h3 class="wp-block-heading">With MCP</h3>



<p>Using MCP and a GitHub MCP server, I simply asked in one prompt: &#8220;Can you create a new pull request on my repo: MicrosoftAcronyms001 and also update the data.tsx file with more and newer Microsoft Acronyms?&#8221;</p>



<p>That&#8217;s it! A new PR was created with updated content, no manual intervention required.<br><br></p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="500" src="/wp-content/uploads/2025/04/vscode-1024x500.png" alt="" class="wp-image-2213" srcset="/wp-content/uploads/2025/04/vscode-1024x500.png 1024w, /wp-content/uploads/2025/04/vscode-300x146.png 300w, /wp-content/uploads/2025/04/vscode-768x375.png 768w, /wp-content/uploads/2025/04/vscode-1536x750.png 1536w, /wp-content/uploads/2025/04/vscode-2048x1000.png 2048w, /wp-content/uploads/2025/04/vscode-750x366.png 750w, /wp-content/uploads/2025/04/vscode-1140x556.png 1140w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<h2 class="wp-block-heading">Setting Up the GitHub MCP Server</h2>



<p>If you want to try this yourself, here&#8217;s how to set up the GitHub MCP server:</p>



<ol class="wp-block-list">
<li>First, download the GitHub MCP Server from: <a href="https://github.com/github/github-mcp-server">https://github.com/github/github-mcp-server</a></li>



<li>In Visual Studio Code, go to Settings and activate the MCP server option.</li>



<li>Edit the settings.json file to add your server(s). You&#8217;ll need to replace <code>${input:github_token}</code> with a Personal Access Token (PAT) generated from your GitHub account:</li>
</ol>



<pre class="wp-block-code"><code>{
  "mcp": {
    "inputs": &#91;
      {
        "type": "promptString",
        "id": "github_token",
        "description": "GitHub Personal Access Token",
        "password": true
      }
    ],
    "servers": {
      "github": {
        "command": "docker",
        "args": &#91;
          "run",
          "-i",
          "--rm",
          "-e",
          "GITHUB_PERSONAL_ACCESS_TOKEN",
          "ghcr.io/github/github-mcp-server"
        ],
        "env": {
          "GITHUB_PERSONAL_ACCESS_TOKEN": "${input:github_token}"
        }
      }
    }
  }
}</code></pre>



<ol class="wp-block-list">
<li>Click &#8220;Run&#8221; and Docker will launch the server locally. (If you prefer not to use Docker, check the official repository for alternative configuration options.) </li>



<li>Once the server is running, go to Agent mode in Visual Studio Code and check the tool buttons for available servers &#8211; you should see your MCP server listed. </li>



<li>Now you can ask Copilot (with Agent mode and the MCP server for GitHub) to perform tasks like creating pull requests or updating files.</li>
</ol>



<p><strong>Important Note:</strong> Make sure your PAT has the correct permissions. When I first tried this, I had only given read permissions, which prevented the MCP from creating the pull request.</p>



<div class="wp-block-cover"><span aria-hidden="true" class="wp-block-cover__background has-background-dim"></span><img loading="lazy" decoding="async" width="1024" height="734" class="wp-block-cover__image-background wp-image-2215" alt="" src="/wp-content/uploads/2025/04/p-1024x734.png" style="object-position:72% 31%" data-object-fit="cover" data-object-position="72% 31%" srcset="/wp-content/uploads/2025/04/p-1024x734.png 1024w, /wp-content/uploads/2025/04/p-300x215.png 300w, /wp-content/uploads/2025/04/p-768x551.png 768w, /wp-content/uploads/2025/04/p-1536x1101.png 1536w, /wp-content/uploads/2025/04/p-120x86.png 120w, /wp-content/uploads/2025/04/p-350x250.png 350w, /wp-content/uploads/2025/04/p-750x538.png 750w, /wp-content/uploads/2025/04/p-1140x817.png 1140w, /wp-content/uploads/2025/04/p.png 1907w" sizes="(max-width: 1024px) 100vw, 1024px" /><div class="wp-block-cover__inner-container is-layout-flow wp-block-cover-is-layout-flow">
<p class="has-text-align-center">MCP SERVER running</p>
</div></div>



<p>Now since the server is running , we click on the button select tools , and we scroll down we can see any MCP server that we have added .</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="1018" src="/wp-content/uploads/2025/04/git-1024x1018.png" alt="available servers " class="wp-image-2216" title="available servers " srcset="/wp-content/uploads/2025/04/git-1024x1018.png 1024w, /wp-content/uploads/2025/04/git-300x298.png 300w, /wp-content/uploads/2025/04/git-150x150.png 150w, /wp-content/uploads/2025/04/git-768x764.png 768w, /wp-content/uploads/2025/04/git-75x75.png 75w, /wp-content/uploads/2025/04/git-750x746.png 750w, /wp-content/uploads/2025/04/git-1140x1133.png 1140w, /wp-content/uploads/2025/04/git.png 1384w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>now after I asked the Model that i have used here : Claude 3.7 , as you can see it&#8221;s asked me to use the mcp server I provided  , and started to use it&#8217;s sub models to aces my repo , get the file content that i need it to be updated it and create new branch.</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="834" height="1024" src="/wp-content/uploads/2025/04/data-834x1024.png" alt="" class="wp-image-2218" srcset="/wp-content/uploads/2025/04/data-834x1024.png 834w, /wp-content/uploads/2025/04/data-244x300.png 244w, /wp-content/uploads/2025/04/data-768x943.png 768w, /wp-content/uploads/2025/04/data-750x921.png 750w, /wp-content/uploads/2025/04/data.png 977w" sizes="(max-width: 834px) 100vw, 834px" /></figure>



<p><br><br>Results :  As you can see , I have a PR created and that have updated the file with some Acronyms , the pipeline worked fine and the file contain new content</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="628" src="/wp-content/uploads/2025/04/pr-1024x628.png" alt="" class="wp-image-2220" srcset="/wp-content/uploads/2025/04/pr-1024x628.png 1024w, /wp-content/uploads/2025/04/pr-300x184.png 300w, /wp-content/uploads/2025/04/pr-768x471.png 768w, /wp-content/uploads/2025/04/pr-1536x942.png 1536w, /wp-content/uploads/2025/04/pr-750x460.png 750w, /wp-content/uploads/2025/04/pr-1140x699.png 1140w, /wp-content/uploads/2025/04/pr.png 1806w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>And now I asked to merge the Pull request too : <br></p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="527" src="/wp-content/uploads/2025/04/image-1024x527.png" alt="" class="wp-image-2224" srcset="/wp-content/uploads/2025/04/image-1024x527.png 1024w, /wp-content/uploads/2025/04/image-300x154.png 300w, /wp-content/uploads/2025/04/image-768x395.png 768w, /wp-content/uploads/2025/04/image-1536x790.png 1536w, /wp-content/uploads/2025/04/image-2048x1054.png 2048w, /wp-content/uploads/2025/04/image-750x386.png 750w, /wp-content/uploads/2025/04/image-1140x587.png 1140w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p>After the merge , i went to my website to test the new acronyms if its exist and the result as you can see , it works .</p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="412" src="/wp-content/uploads/2025/04/image-1-1024x412.png" alt="" class="wp-image-2225" srcset="/wp-content/uploads/2025/04/image-1-1024x412.png 1024w, /wp-content/uploads/2025/04/image-1-300x121.png 300w, /wp-content/uploads/2025/04/image-1-768x309.png 768w, /wp-content/uploads/2025/04/image-1-1536x618.png 1536w, /wp-content/uploads/2025/04/image-1-750x302.png 750w, /wp-content/uploads/2025/04/image-1-1140x459.png 1140w, /wp-content/uploads/2025/04/image-1.png 1872w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p><br><br></p>



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



<p>MCP represents a significant advancement in how we integrate AI models with external tools and data. By providing a standardized protocol for these interactions, it eliminates the need for custom development work and makes AI integration much more accessible.</p>



<p>Isn&#8217;t it amazing that all of this can be accomplished without developing any connectors or manual intervention? I believe MCP is just the beginning of a new era in AI integration, and I look forward to exploring its capabilities further in future posts.<br><br>List of available Servers &amp; Clients :  https://github.com/modelcontextprotocol/servers?tab=readme-ov-file</p>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://achrafbenalaya.com/2025/04/21/model-context-protocol-mcp-the-future-of-ai-integration/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2207</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>
	</channel>
</rss>
