<?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>copilot &#8211; achraf ben alaya</title>
	<atom:link href="https://achrafbenalaya-ekgvbjdjgta4b4hz.francecentral-01.azurewebsites.net/category/copilot/feed/" rel="self" type="application/rss+xml" />
	<link>https://achrafbenalaya-ekgvbjdjgta4b4hz.francecentral-01.azurewebsites.net</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>copilot &#8211; achraf ben alaya</title>
	<link>https://achrafbenalaya-ekgvbjdjgta4b4hz.francecentral-01.azurewebsites.net</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-ekgvbjdjgta4b4hz.francecentral-01.azurewebsites.net/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-ekgvbjdjgta4b4hz.francecentral-01.azurewebsites.net/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-ekgvbjdjgta4b4hz.francecentral-01.azurewebsites.net/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-ekgvbjdjgta4b4hz.francecentral-01.azurewebsites.net/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-ekgvbjdjgta4b4hz.francecentral-01.azurewebsites.net/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-ekgvbjdjgta4b4hz.francecentral-01.azurewebsites.net/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>
	</channel>
</rss>
