The main purpose of this repo is to demonstrate how to use GitHub Copilot to support DevOps activities using custom agents, repository skills, and the Azure MCP Server.
You get:
- Two pre-built custom agents (DevOps Engineer + Security Reviewer)
- Four repository skills covering Terraform, Docker, GitHub Actions, and runbooks
- A small two-service Node.js app (a task API and a web UI) ready to run locally and deploy to Azure Container Apps
- Intentionally messy / broken artefacts to use as input for Copilot review and hardening exercises
- A walkthrough of four Copilot-driven DevOps demos you can run end to end in your own subscription
Everything is designed to be opened in VS Code with GitHub Copilot Chat enabled.
- Key learning points
- Repo tour
- Prerequisites
- Quick start: run the app locally
- Custom agents
- Repository skills
- Azure MCP Server
- Demo walkthroughs
- Configuration
- Cleanup
- References
- License
| Concept | What it does | Where to look |
|---|---|---|
| Custom agents | Encode a role and a tool/skill policy. Switch agents in the chat picker. | .github/agents/ |
| Repository skills | Versioned team and project conventions, automatically loaded by the agent when the task matches. | .github/skills/ |
| Agent plan | The DevOps agent outlines its intended changes up front, then proceeds to implement them — giving you an early chance to course-correct. | Used in Demo 1 |
| Azure MCP Server | Live Azure context: best practices, resource health, monitor logs, Advisor. | .vscode/mcp.json |
terraform plan |
Reviews the infrastructure delta before you apply. Pairs with the agent's upfront plan. | Demo 1 |
| Generate → use loop | Copilot generates docs (a runbook) from real config, then reads them back as diagnostic context. | Demo 3 |
The high-level mental model:
Agents describe who is doing the work and which skills/tools they use. Skills describe what good looks like for a particular kind of artefact. Azure MCP brings live Azure facts into the conversation.
.
├── app/
│ ├── api/ # Express + TypeScript task API (in-memory store)
│ └── web/ # Express + TypeScript web UI (single-page, vanilla JS)
├── infra/
│ ├── main.tf # Monolithic Terraform — refactored in Demo 1
│ ├── variables.tf
│ ├── terraform.tfvars.example
│ └── backend.hcl.example
├── broken/ # Intentionally broken artefacts for review demos
│ ├── main.tf # Wrong resource type
│ ├── deploy.yml # Bad indentation, missing OIDC perms
│ ├── Dockerfile # Insecure Dockerfile (no USER, no multi-stage…)
│ └── README.md
├── scripts/
│ ├── setup.ps1 # Provision RG + Terraform state storage, write tfvars + backend.hcl
│ ├── setup.sh # Bash equivalent
│ └── run-local.ps1 # Run API + web locally in two terminals
├── .github/
│ ├── agents/ # DevOps Engineer + Security Reviewer
│ └── skills/ # terraform / docker / github-actions / runbook conventions
├── .devcontainer/ # Optional dev container with Node 20 + Terraform + Azure CLI
├── .vscode/
│ └── mcp.json # Azure MCP Server configuration
├── .env.example
└── .gitignore
What is not here, and is generated during the demos:
infra/modules/,infra/outputs.tf,infra/providers.tf(Demo 1)app/api/Dockerfile,app/web/Dockerfile,*/.dockerignore(Demo 2).github/workflows/deploy.yml(Demo 2)docs/runbook.md(Demo 2)
This is intentional — the point is to use Copilot to produce them.
- Node.js 20+
- Azure CLI (
az) on PATH, signed in viaaz login - Terraform 1.5+
- Docker (Docker Desktop or equivalent) — only needed if you want to build images locally; the demos use
az acr buildso this is optional - VS Code with the GitHub Copilot and GitHub Copilot Chat extensions
- An Azure subscription you can deploy resources into
Optional: open the repo in the included .devcontainer/ for a pre-configured environment with all of the above.
You can explore the app without any Azure setup.
- Install dependencies:
cd app/api; npm install; cd ../.. cd app/web; npm install; cd ../..
- Start both services in separate windows:
pwsh ./scripts/run-local.ps1
- Open the UI:
- Web: http://localhost:8080
- API: http://localhost:3000/tasks
- Health checks: http://localhost:3000/health and http://localhost:8080/health
Add a task in the browser, toggle it complete, and delete it — that exercises all four CRUD endpoints on the API.
Agents live in .github/agents/ and appear in the chat agent picker once VS Code recognises the workspace.
| Agent | Role | When to use |
|---|---|---|
| DevOps-Engineer-Agent | Senior DevOps engineer. Orchestrates Terraform, Docker, pipelines, and Azure best practices via repo skills + Azure MCP. | Building, refactoring, generating, troubleshooting. |
| Security-Reviewer-Agent | Senior security engineer. Reviews Dockerfiles, Terraform, and pipeline YAML against CIS / OWASP / Azure baselines and Advisor recommendations. | Hardening, audit, security review. |
To switch between them in VS Code, use the agent picker at the top of the Copilot Chat panel.
Agents and skills solve different problems, and it's easy to reach for the wrong one:
- Reach for a custom agent when the role changes. Agents encode a persona, a default toolset, and a policy for which skills to load. Use one when the same person would wear a different hat for the task — e.g. a DevOps engineer building infra vs. a security engineer auditing it. An agent is a long-lived configuration you switch into.
- Reach for a skill when the artefact has team conventions. Skills
describe "what good looks like" for a specific kind of file or workflow
(Terraform modules, Dockerfiles, GitHub Actions, runbooks). They load
automatically based on the task and are shared across agents — the DevOps
and Security Reviewer agents both consult
docker-conventionswhen Dockerfiles are in scope.
Rule of thumb: if you find yourself writing "act as a …" you probably want an agent; if you're writing "when editing X, always …" you probably want a skill.
Skills live in .github/skills/. Copilot loads them automatically when the task matches the skill's description.
| Skill | What it covers |
|---|---|
| terraform-conventions | Naming, tagging, module structure, Container Apps patterns, validation flow |
| docker-conventions | Pinned base images, multi-stage builds, non-root user, HEALTHCHECK, .dockerignore, deploy script |
| github-actions-conventions | Pipeline structure, OIDC auth, image scanning, environment gates |
| runbook-conventions | Architecture overview, deploy steps, rollback, troubleshooting, monitoring queries |
The docker-conventions skill bundles a working
scripts/deploy-apps.ps1
helper that the agent uses in Demo 2
to build images in ACR and update both Container Apps.
.vscode/mcp.json wires up the Azure MCP Server with the scopes used by the demos:
azureterraformbestpractices— Terraform best practices for Azure (Demo 1)resourcehealth,monitor,applicationinsights— live diagnostics (Demo 3)advisor— security/cost/reliability recommendations (Demo 4)group,subscription— context
Make sure you are signed into Azure in VS Code (az login) so the MCP server
can pick up your credentials.
The four demos take you from a messy monolith to a deployed, hardened, fully observable workload. They use the prompts shown verbatim — copy them straight into Copilot Chat.
Before you start:
- Copy
.env.exampleto.envand fill in your subscription, resource group, location, and Terraform state storage account names. - Run
pwsh ./scripts/setup.ps1(orbash ./scripts/setup.sh) to provision the resource group, the state storage account/container, and to generateinfra/terraform.tfvarsandinfra/backend.hcl. - From
infra/, runterraform init -backend-config=backend.hcl. - Open Copilot Chat and switch to DevOps-Engineer-Agent.
State expectations before Demo 1: the repo is exactly as cloned, with no
infra/modules/, no app/*/Dockerfile, no .github/workflows/, and no
docs/runbook.md. The infra/main.tf monolith uses placeholder MCR images
so it can deploy successfully before any real images exist.
Heads up:
setup.ps1writes a few extra keys intoinfra/terraform.tfvars(project,environment,owner,api_external_enabled) that the monolithicinfra/variables.tfdoes not declare yet. Terraform will warn about undeclared variables on the firstplan/apply— that is expected. Demo 1 wires those variables up properly during the refactor.
Agent: DevOps Engineer.
Skills used: terraform-conventions, plus Azure MCP azureterraformbestpractices.
-
Open infra/main.tf. Note the smell: hardcoded values, inconsistent naming (
mycontainerappvsacrprodregistry), no tags, no outputs, no modules. -
In Copilot Chat (DevOps Engineer), run:
Prepare this repo for Azure Container Apps. Refactor the Terraform in
infra/into modules following our team conventions, and validate the structure before making changes. -
The agent outlines its plan first — proposed file structure (modules, variables, outputs, naming changes, tags) — then proceeds to implement it, restructuring
infra/and runningterraform fmt,terraform validate, andterraform plan. Read the plan as it streams so you can interrupt early if the direction is wrong. -
Review the
terraform planoutput, then run:cd infra terraform apply -auto-approve
Leave it running — you can move on to Demo 2 while Azure provisions.
Why two layers of review?
The agent's upfront plan tells you what it intends to change in the repo.
terraform plan tells you what those changes will do to your environment.
One protects the codebase, the other protects the infrastructure.
Agent: DevOps Engineer.
Skills used: docker-conventions, github-actions-conventions, runbook-conventions.
Each step is one prompt:
-
Dockerfiles for both services:
Generate production-ready Dockerfiles for both the API in
app/api/and the web app inapp/web/.The
docker-conventionsskill already enforces multi-stage builds, non-root user, aHEALTHCHECK, a pinned MCR base image, and a.dockerignore— so a single prompt should produce compliant Dockerfiles on the first pass. Check the output against the skill's review checklist before moving on. -
CI/CD pipeline:
Generate a GitHub Actions CI/CD workflow for this repo. Build and test both services, push both images to ACR, then deploy to staging followed by production approval.
This produces
.github/workflows/deploy.yml. -
Runbook:
Generate an operational runbook for our Container Apps deployment. Include: architecture overview, deployment steps, rollback procedure, health check troubleshooting, common issues and fixes, monitoring queries to run. Use the Terraform and pipeline we just created.
This produces
docs/runbook.md. -
Build and deploy real images (waits for the Demo 1
terraform applyto complete first):Build and deploy both apps to our running Container Apps for testing.
The agent picks up the
docker-conventionsskill, findsdeploy-apps.ps1, and runs it. The script does anaz acr buildfor both images, updates each Container App, and syncsinfra/terraform.tfvarsso subsequentterraform applyruns preserve your real images.
Agent: DevOps Engineer.
Skills + tools: Azure MCP resourcehealth, monitor, applicationinsights,
plus the runbook generated in Demo 2.
-
Fix
broken/main.tf— pretend a colleague pushed it and the pipeline is failing. In Copilot Chat (DevOps Engineer), run:A colleague pushed
broken/main.tfand the pipeline is failing. Take a look, work out what's wrong, and fix it.The agent picks up the
terraform-conventionsskill, runsterraform init/validate/planitself, and identifies the wrong resource type (azurerm_container_group, which is Azure Container Instances, not Azure Container Apps) along with the missingcontainer_app_environment_id. -
Fix
broken/deploy.yml:Review this GitHub Actions workflow for issues. Check for: correct syntax, up-to-date action versions, security best practices, and missing configurations.
Watch it catch the indentation, the outdated
actions/checkout@v2, and the missing OIDCpermissions:block. -
Live diagnostics — the web app cannot reach the API. With the real apps deployed from Demo 2, the web app loads but tasks do not appear. The API's ingress is internal-only while the web app is calling its FQDN — a classic Container Apps networking trap.
The web app is running but can't load any data from the API. Check the health of both Container Apps and query Application Insights for any connection errors in the last 10 minutes.
Then bring in your runbook as diagnostic context:
Use our runbook at
docs/runbook.mdto diagnose why the web app can't connect to the API.Apply the suggested fix (enable external ingress on the API, or switch to internal-only on both with proper service-to-service DNS) and redeploy.
Agent: Security Reviewer.
Tools: Azure MCP advisor.
-
Switch agent to Security-Reviewer-Agent.
-
Harden
broken/Dockerfile:Review this Dockerfile for security issues and container best practices.
Then:
Fix all findings. Generate a hardened Dockerfile and a
.dockerignorefile.Compare the result with the Dockerfiles produced in Demo 2 — the
docker-conventionsskill already enforces the same standards by default. -
Full-stack audit:
Now review our Terraform in
infra/and the CI/CD pipeline for security issues. Also get Azure Advisor recommendations for our deployed resources.Expect findings on the Terraform side (HTTPS-only enforcement, VNET integration, diagnostic settings, image digests vs tags, resource locks) and an Advisor cross-reference for the live resources.
| Sample file | Copy to | Purpose |
|---|---|---|
| .env.example | .env |
Subscription, resource group, location, state storage account |
| infra/terraform.tfvars.example | infra/terraform.tfvars |
Terraform input variables (auto-generated by setup.ps1) |
| infra/backend.hcl.example | infra/backend.hcl |
Azure Storage backend config (auto-generated by setup.ps1) |
.env, terraform.tfvars, and backend.hcl are all listed in
.gitignore and will not be committed.
When you are finished, tear down everything you provisioned:
cd infra
terraform destroy -auto-approveThen delete the resource group if you want a completely clean slate:
az group delete --name <your-resource-group> --yesYou can also delete any generated demo artefacts (infra/modules/,
app/*/Dockerfile, .github/workflows/, docs/runbook.md) to return the
repo to its starting state.
- GitHub Copilot in VS Code
- Custom agents
- Custom skills
- Azure MCP Server
- Azure Container Apps
- Terraform on Azure
MIT — see LICENSE (add your preferred license file before publishing).