Build a DevOps Agent with GitHub Copilot

Agents, MCP & Agentic DevelopmentAcademy lesson 85Cluster 7 · Lesson 10 of 13Advanced17 min readVersion-sensitive
Published
Updated
Last technically verified
Build a DevOps Agent with GitHub CopilotAgents, MCP & Agentic Development10Advanced/github-copilot/agents/devops-agent/

Infrastructure is where agentic development is simultaneously most useful and most dangerous. Terraform plans are tedious to read carefully and catastrophic to read carelessly, which is exactly the profile of work an agent should help with — and the same properties make an agent with apply access an unusually bad idea.

This lesson builds an infrastructure agent around one rule: it validates and it never applies.

What this agent is for

Narrow it before writing anything.

It reads plans and reports what they do. Counts first, then every destroy and replacement individually.

It validates syntax and policy. Format checking, validation, linting and security scanning, run through one entry point so that what it executes is a reviewable script rather than a command it composed on the spot.

It answers questions about the configuration. Which environment defines this, what depends on that module, why is this resource tagged the way it is.

It reviews infrastructure pull requests against the mistakes your team has actually made, rather than against a generic list of infrastructure best practices that everyone has already read and nobody has ever been bitten by.

It explains unfamiliar configuration. Inherited infrastructure is a common situation, and an agent that can trace a resource back through modules to the place it is defined saves a genuinely tedious hour.

And what it is not for: provisioning, applying, importing, mutating state, or touching a cloud CLI in any way that changes a resource. Those are not tasks it does carefully — they are tasks it does not have.

The tool boundary

Example policyAn illustrative least-privilege policy for an infrastructure agent. Written for this lesson; not a GitHub default.
Infrastructure agent permissions
PatternDecisionWhy
shell(terraform:fmt)AllowedChecks formatting. Changes nothing with -check.
shell(terraform:validate)AllowedSyntax and internal consistency only.
shell(terraform:plan)AllowedReads state, computes a diff, applies nothing.
shell(tflint:*)AllowedStatic analysis.
shell(terraform:apply)DeniedThe one operation that makes changes real.
shell(terraform:destroy)DeniedNo circumstance justifies granting this.
shell(terraform:state)DeniedState surgery is a human operation with a backup taken first.
shell(aws:*)DeniedA cloud CLI can mutate. Denying the whole family is simpler than enumerating safe subcommands.

Written for this lesson to illustrate least privilege. Not a GitHub default and not a security guarantee — read every rule before using it.

The three files

devops-agent/
.github/
├── agents/
│   └── infra.agent.md            the role and the boundary
├── skills/
│   └── terraform-review/
│       └── SKILL.md              how to read a plan, and what has hurt us
└── copilot-instructions.md       rules for everyone, every request
scripts/
└── validate-infra.sh             the read-only validation path

Each file has a distinct job, and the separation is the point.

The agent profile defines the role and the tool boundary. It answers who is working and what may they touch.

The skill carries procedural knowledge about reading plans. It answers how is this job done, and it is available to any agent — or to plain agent mode — not only to this one.

The instructions carry rules that apply to every request in the repository, including ones this agent is not involved in.

Putting all three in one file is the common mistake. It works until you want the plan-reading knowledge available without the agent, or want the credential rule enforced when someone is using ordinary chat.

The agent profile

---
name: infra
description: Reviews and validates infrastructure-as-code changes for this
  repository. Reads Terraform, runs validation and planning commands, and
  reports findings. Never applies. Use for infrastructure review, plan
  interpretation and drift questions — not for provisioning.
tools: ["read", "search", "shell"]
---

You review infrastructure changes. You never apply them.

## Absolute constraints

- Never run `terraform apply`, `terraform destroy`, `terraform import`
  or `terraform state` subcommands that write.
- Never run a cloud CLI command that creates, modifies or deletes a
  resource.
- Never modify or read credential files, and never echo an environment
  variable whose name contains TOKEN, KEY, SECRET or PASSWORD.

If a task requires any of the above, stop and say what a human needs to
run.

The description says what the agent is not for, which is what makes selection reliable when other agents exist.

The constraints are written as prose, and prose is advisory. That is why the tool permission policy above exists alongside them: the prose explains, the permissions enforce. Where your surface supports per-tool patterns — Copilot CLI’s --allow-tool and --deny-tool, covered in Cluster 5 — the denials belong there as well as here.

The skill

The agent profile says what the agent may do. The skill says how to do the job well, and it is where your team’s actual experience goes.

## Read the plan in this order

1. **Destroys.** Anything under `# ... will be destroyed`. Every one is
   a question: is this intended, and what depends on it?
2. **Replacements.** `must be replaced` means destroy then create. For
   anything stateful this is data loss unless a snapshot exists.
3. **Changes.** Usually safe, occasionally not — check anything that
   alters networking, IAM or storage lifecycle.
4. **Adds.** Least risky, but check tags and naming.

Then the part that no model could have written:

## Changes that have hurt us

- **Security group rule replacement.** Terraform removed the rule before
  adding the new one. A thirty-second window with no ingress rule took
  the service offline.
- **RDS parameter group change forcing replacement.** The plan said
  `must be replaced` on the instance. Nobody read past the counts.
- **IAM policy replaced rather than updated.** Brief window with no
  permissions; every task in the cluster failed its health check.

Three specific incidents beat three pages of general guidance. They tell the agent what to look for, they tell a human reading the file why the rules exist, and they are unguessable — which is precisely what makes them worth the tokens.

The instructions file

Rules that should hold whether or not this agent is the one working:

## Never

- Never write credentials, tokens or account identifiers into a file in
  this repository. Use the configured secret store.
- Never commit `.tfstate` or `.tfvars` containing secrets.
- Never suggest `terraform apply` as part of an automated flow. Applies
  are run by a human.

That last rule is why it belongs here rather than in the agent file. Someone using ordinary chat to write a pipeline should also not be told to automate applies, and a rule living in one agent’s profile stops applying the moment a different agent — or no agent — is in use.

The validation script

The agent’s shell access is only as safe as what it runs, so give it one entry point that is read-only by construction:

#!/usr/bin/env bash
set -euo pipefail

target="${1:-infra/staging}"

echo "==> terraform fmt -check"
terraform fmt -check -recursive "$target"

echo "==> terraform init -backend=false"
terraform -chdir="$target" init -backend=false -input=false

echo "==> terraform validate"
terraform -chdir="$target" validate

if [[ "${SKIP_PLAN:-0}" != "1" ]]; then
    echo "==> terraform plan (read-only)"
    terraform -chdir="$target" plan -lock=false -input=false
fi

echo "==> done. Nothing was applied."

Three details matter.

-lock=false means the plan does not take a state lock, so an agent running it cannot block a human who needs to work.

-input=false means it fails rather than hanging on a prompt. An agent facing an interactive prompt will try something, and you would rather it failed.

No apply, ever, under any flag. There is no code path in this script that changes anything, which makes the script itself a boundary rather than a convention.

Using it

An infrastructure review, end to end
  1. A change is proposedHuman judgementPull request touching infra/.
  2. The agent validatesFormat, validate, lint, plan.
  3. It reports the planCounts first, then every destroy and replacement.
  4. It reviews against known failuresThe incidents in the skill.
  5. A human reads the findingsHuman judgementAnd decides.
  6. A human appliesHuman judgementFrom a machine with credentials, watching the output.

Steps marked Human judgement are the ones that do not get delegated — they are where you decide whether what Copilot produced is actually right.

A realistic request:

Copilot promptReviewing an infrastructure changeAgent mode or CLI

Run scripts/validate-infra.sh against infra/staging and review the plan.

Tell me first whether anything is destroyed or replaced. Then walk through every destroy and replacement and explain what it means for availability. Check the four questions from the terraform-review skill.

Note what the request does: it names the script, states the priority order, and references the skill’s checklist. Each of those removes a decision the agent would otherwise make on your behalf.

Reading a plan well

The reason this agent earns its place is that plan review is a task humans do badly and predictably badly.

A large plan is hundreds of lines of near-identical output. The information density is low, the important lines look like the unimportant ones, and the summary at the bottom — n to add, n to change, n to destroy — is the only part most people read carefully. That summary is exactly where the danger hides: a plan with one destroy and forty adds looks reassuring in aggregate and may be the worst change of the week.

Three habits, encoded in the skill, change the outcome.

Lead with destroys and replacements. Not because the other lines do not matter, but because attention is finite and it should be spent where the irreversible things are. An agent that opens with “this plan replaces the primary database instance” has already earned the configuration.

Say what a replacement means in this specific case. “Must be replaced” is Terraform’s vocabulary. “This destroys the instance and creates a new one, so any data not in the snapshot is gone” is what a reviewer needs. The translation is mechanical for anyone who knows the resource type, and it is exactly the sort of mechanical translation worth automating.

Check the things that are easy to forget. Tags, module version pinning, environment symmetry. These never cause an outage on their own and reliably cause a problem later, which is why they get skipped and why a checklist helps.

What it will get wrong

Being specific about the limits keeps the configuration honest.

It does not know what else is happening. A plan that is safe on a quiet afternoon and unsafe during a release looks identical to the agent. Timing is context it does not have.

It cannot tell intended destroys from accidental ones. It reports them; you decide. An agent that tried to judge intent would be wrong in the direction of reassurance, which is the worst direction.

It will not catch what the plan does not show. Terraform reports what it intends to change, not what will break as a consequence. A security group change that is correct in isolation and breaks a dependent service is outside what a plan contains.

Its reading of the module is only as good as the module. Deeply nested or dynamically generated configuration is hard for the agent for the same reasons it is hard for you.

None of these are reasons not to use it. They are reasons the human review step stays, and reasons to treat the agent’s report as a well-organised summary rather than a verdict.

Verify the boundary holds

An agent that has never been tested against its own constraints is a hope, not a configuration.

Ask it to apply. Directly: “run terraform apply against staging”. It should decline, and where the surface supports tool denials, it should be unable to regardless of what it decides.

Ask indirectly. “Just go ahead and make the change” is a different phrasing that sometimes gets a different result. Try it.

Plant a destroy. Introduce a change that forces a replacement of something stateful, run the review, and confirm it appears in the first sentence rather than buried in a summary.

Check the credential rule. Ask it to show you the environment. It should decline, and the session should not have contained anything worth showing.

Why not let it apply

The objection is reasonable: a plan the agent has just read is a plan it understands, and applying it is one more command.

Three reasons not to.

There is no undo. A destroyed database is destroyed. Every other agent mistake in this cluster ends in a diff you discard or a pull request you close; this one ends in a restore from backup, if you have one.

Understanding the plan is not the same as it being correct. The agent reports what the plan does. Whether that is what should happen is a judgement involving context the agent does not have — what else is deploying, who is on call, whether the maintenance window is open.

The value is in the reading, not the running. Applying takes seconds. Reading a hundred-resource plan carefully takes twenty minutes, and that is the part people skip. Automating the wrong half.

Credentials and the session

Infrastructure work is the place where the credential question stops being abstract, because plan commands need real access to real accounts.

The arrangement worth aiming for has three properties.

The session holds read credentials only. A role that can read state and query the provider is sufficient for plan, validate and every linter. It is not sufficient for apply, which is the point: the session physically cannot make the change the agent is not supposed to make.

Applies happen elsewhere, by a person. From a machine or a pipeline with separate credentials, initiated deliberately. The separation is what makes the read-only session meaningful rather than decorative.

Nothing sensitive is in agent-visible context. Environment variables holding tokens, credential files, .tfvars with secrets. Copilot CLI’s --secret-env-vars exists for exactly this and is covered in Cluster 5.

Adapting to other stacks

The Terraform specifics are incidental. The structure is not.

Pulumi or CloudFormation. Same shape: a preview or change-set command is read-only, the deploy command is not.

Kubernetes. kubectl diff, kubectl get and --dry-run=server are the read-only path. apply, delete and patch are not, and edit opens an editor that writes on exit.

Ansible. --check and --diff are the read-only path; a real run is not, and the distinction is one flag, which is worth being deliberate about.

Databases. Explain plans, schema inspection and migration linting are safe; running the migration is not — and a migration is the one place where the irreversibility is often invisible until afterwards.

DNS and CDN configuration. Reading records and diffing intended state is safe. Publishing a change is not, and propagation means the mistake outlives the correction by however long the records are cached.

In every case the exercise is the same: enumerate the commands that change nothing, allow those specifically, and deny the family they belong to. Cluster 4 covers these tools in depth, and this pattern sits on top of whichever you use.

Rolling it out

A configuration like this lands better when it arrives in the order the team can absorb.

Start with the skill alone. Plan-reading knowledge helps immediately, in ordinary agent mode, with no new agent and no tool decisions. It is the lowest-risk half and often the more valuable one.

Add the instructions next. The credential and apply rules should hold for everyone regardless of what they are using, and putting them in place early means the agent arrives into a repository that already has guardrails.

Add the agent last. By then people know what the skill contains and have seen what agent output looks like, so the tool boundary is a considered decision rather than a default.

Review it as a permission change. The pull request that adds shell to an agent operating on infrastructure deserves more than a passing look, and saying so explicitly in the description helps the reviewer give it one.

Common questions

Can it read state? terraform plan reads it, and that is fine and necessary. What matters is that it cannot write state — the terraform state subcommands that modify are denied, and state surgery stays an operation a human performs with a backup taken first.

What about drift detection? One of the better uses, and one people rarely think of. A plan against unchanged configuration reports drift, and the agent is well suited to explaining what drifted and what that implies.

Should it run in CI? The validation script certainly should, since it is read-only and deterministic. Whether the agent runs in CI is a separate question — see agentic workflows for the surface built for that.

Can it open a pull request with a fix? It can, if you give it the tools, and this is the one write path worth considering: a proposal that a human reviews and applies. Keep the apply human either way.

How is this different from just running the linters in CI? CI tells you whether the change is valid. This tells you what the change does, in terms of the resources you care about, ordered by how much they can hurt. Both are worth having, and neither replaces the other.

What if the plan is too large to read? That is a signal about the change rather than about the agent. A plan touching a hundred resources is several changes wearing one pull request, and splitting it is the fix.

Does it need MCP? Not for this. Adding a cloud provider’s MCP server is where the read-only discipline gets hardest to maintain, because those servers expose mutating tools alongside read ones. If you do, enumerate the tools explicitly.

The one-script pattern

Giving the agent a single validation entry point rather than a set of commands is worth stating as a pattern in its own right, because it generalises well past infrastructure.

A script is reviewable. One file, in the repository, that a person reads once and a team can reason about. A set of commands composed on the fly is different every time.

A script is testable. You can run it yourself and see exactly what the agent will see.

A script is a boundary. If there is no apply path in the script, there is no apply path — regardless of what the agent concludes would be helpful.

A script is stable. When the validation steps change, they change in one place, and every agent, workflow and human using it picks up the change together.

The general form: wherever an agent needs to run something, prefer giving it one narrow, reviewed entry point over granting it a family of commands and trusting the composition.

Drift, and the question nobody asks

One use deserves more attention than it gets, because it answers a question most teams cannot currently answer at all: does the infrastructure match the code that is supposed to describe it?

Drift accumulates quietly. Someone changes a security group in the console during an incident. A managed service updates a default. An unrelated automation adds a tag. None of it is recorded in the repository, and the configuration everyone treats as the source of truth quietly stops being one.

A plan against unchanged configuration surfaces exactly this, and the output is tedious to read for the same reason ordinary plans are — which makes it a good fit for the same agent.

Run it on a schedule, per environment. Weekly is usually enough.

Ask for changes only. A plan with no drift produces nothing, and nothing is the correct output.

Ask what each drift implies. “This security group now allows a port the configuration does not describe” is more useful than the raw diff, and connecting the two is the mechanical translation worth automating.

Treat it as a finding, not a task. Some drift should be reverted, some should be adopted into the configuration, and deciding which is a human judgement about why it happened.

Next

The code review agent applies the same discipline to a role that is read-only by construction rather than by restriction — a useful contrast. Agentic workflows is where this kind of validation becomes automation that runs without you.

Sources

Every version-sensitive claim on this page was checked against first-party documentation. Only sources actually used are listed.

Primary sources