GitHub Copilot CLI for DevOps Engineers
Infrastructure work has a property that makes it the hardest case in this cluster: the tools are already authenticated. A DevOps engineer’s shell usually holds a cloud session, a Kubernetes context, and a Terraform backend configuration. An agent running in that shell inherits all of it.
That is not a reason to avoid the CLI for infrastructure work — the validation tooling here is the best of any domain, and an agent that runs it is genuinely valuable. It is a reason to be deliberate about tool policy in a way that other domains can get away with skipping.
Key takeaways
- Every major infrastructure tool has a validating command and a mutating one. Allow the first, deny the second, and most of the risk disappears.
- Denial beats
--allow-all-tools. That asymmetry is what makes a safe policy expressible even for broad automation. terraform planis not read-only in the way people assume — it reads state, needs credentials, and can refresh remote state.kubectl apply --dry-run=clientis not offline. It fetches the OpenAPI schema from the API server.- Your shell’s existing credentials are the agent’s credentials.
--secret-env-varsis the mechanism for keeping named variables out of its reach.
The ordering
The anti-pattern is short enough to state in three words: ask, autopilot, production. Every step removed from the sequence above moves a decision from a human to a model, and the last step is the one where that trade is worst.
A policy that permits validation and refuses apply
This is the central technique of the lesson. Infrastructure tools are unusually cooperative here because their commands split cleanly.
| Pattern | Decision | Why |
|---|---|---|
shell(terraform fmt:*) | Allowed | Formatting is mechanical and reversible. |
shell(terraform validate:*) | Allowed | Type-checks against the provider schema; touches nothing. |
shell(terraform plan:*) | Asks first | Reads state and needs credentials — not as inert as it looks. |
shell(terraform apply:*) | Denied | Changes real infrastructure. |
shell(terraform destroy:*) | Denied | There is no undo. |
Written for this lesson to illustrate least privilege. Not a GitHub default and not a security guarantee — read every rule before using it.
copilot \
--allow-tool='shell(terraform fmt)' \
--allow-tool='shell(terraform validate)' \
--deny-tool='shell(terraform apply:*)' \
--deny-tool='shell(terraform destroy:*)' \
-i "Review the Terraform in this repository for security problems"plan is less passive than it looks
It is tempting to treat terraform plan as read-only. It is not, quite. It reads
state, which for a remote backend means network access and credentials. It can
refresh state against real resources. It may acquire a state lock, which blocks
colleagues. And it writes a plan file if asked.
None of that changes infrastructure. All of it means plan belongs on prompt
rather than pre-approved, particularly on a shared backend.
The same caution applies elsewhere. kubectl apply --dry-run=client sounds
offline and is not — it downloads the OpenAPI schema from the API server, so it
requires a reachable cluster and a valid context. --dry-run=server goes further
and sends the object to the API server for validation.
Why the split works so cleanly here
Infrastructure tooling is unusually well suited to permission policies, and it is worth understanding why, because the property does not hold everywhere.
These tools were designed around a plan-then-apply model, for humans, long before
agents existed. Terraform separates plan from apply. Kubernetes has
--dry-run. Ansible has --check. Helm has --dry-run and template. The
separation already exists in the CLI surface, so a permission pattern matching on
subcommand lands exactly on the boundary that matters.
Compare that with the Bash lesson, where
bash -n script.sh and bash script.sh differ by a flag rather than a
subcommand — and the pattern grammar, which matches on the command stem, cannot
tell them apart. Infrastructure tools give you a boundary the permission system
can express. That is a genuine advantage and worth exploiting fully.
Per-tool policies
Kubernetes
| Pattern | Decision | Why |
|---|---|---|
shell(kubectl get:*) | Allowed | Read-only inspection. |
shell(kubectl describe:*) | Allowed | Read-only detail. |
shell(kubectl logs:*) | Allowed | Reading logs changes nothing. |
shell(kubectl apply:*) | Asks first | Even a dry run reaches the API server. |
shell(kubectl delete:*) | Denied | Deleting a resource can drop a workload. |
Written for this lesson to illustrate least privilege. Not a GitHub default and not a security guarantee — read every rule before using it.
The specific hazard in Kubernetes is context. kubectl operates against whatever
context is current, and the current context is invisible in the command. An agent
proposing kubectl delete deployment api has not specified a cluster, because it
does not need to — the shell already decided.
Docker
Building and inspecting are safe; the destructive commands are the pruning ones, and they are unusually easy to approve because they sound like tidying.
docker system prune -a removes all unused images, networks and build cache.
On a developer machine that is an inconvenience. On a build host it can delete
the base images every pipeline depends on, turning the next hundred builds into
full pulls.
Ansible
Ansible has the best safety story of these tools, because check mode is a first-class feature.
| Pattern | Decision | Why |
|---|---|---|
shell(ansible-lint:*) | Allowed | Static analysis. |
shell(ansible-playbook --syntax-check:*) | Allowed | Parses without connecting. |
shell(ansible-playbook --check:*) | Asks first | Check mode still connects to hosts. |
shell(ansible-playbook:*) | Denied | A real run changes real machines. |
Written for this lesson to illustrate least privilege. Not a GitHub default and not a security guarantee — read every rule before using it.
The caveat worth knowing: check mode is only as honest as the modules used. Modules
that do not implement check mode may report no change while a real run would
change plenty, and command or shell tasks are the usual offenders. Check mode
is a strong signal, not a guarantee.
Cloud CLIs
aws, az and gcloud are where the credential inheritance problem is sharpest,
because a single command can create billable resources or delete a production
database.
The workable split is verbs. Read verbs — describe, list, get — are safe to
allow. Everything else prompts or is denied.
copilot \
--allow-tool='shell(aws)' \
--deny-tool='shell(aws rds:*)' \
--deny-tool='shell(aws ec2 terminate-instances:*)' \
-i "Audit our S3 buckets for public access"Where the agent genuinely helps
Being fair about the value, since most of this lesson is about constraint.
Reading a plan. terraform plan output for a large change is long, and the
important lines are buried. Asking an agent to find every destroy and every
forces replacement is faster and more reliable than scrolling.
Explaining inherited configuration. A Helm chart or Terraform module written by someone who left is exactly the reading-comprehension task models do well.
Cross-referencing. “Which of these security groups actually allows ingress from the internet, and what is behind them” requires correlating several sources — tedious by hand, quick for an agent.
Recalling syntax. Provider schemas, kubectl selector syntax, Ansible module
arguments. Nobody memorises these.
Writing the boring parts. Variable definitions, output blocks, labels, documentation. This is unglamorous and it is a real fraction of the work, and it is the part where a wrong answer is caught immediately by validation.
Where it is weakest is anything requiring facts it cannot see: whether a resource is actually in use, whether the redundant-looking security group is load-bearing, which of two clusters is production. It will reason confidently from assumptions, and the assumptions will not be in the output.
Debugging a live system
The other half of DevOps work is not writing configuration but working out why something is broken, at a moment when time pressure is highest and care is lowest. This is precisely when an agent is most useful and when the discipline above is most likely to be abandoned.
The read-only investigation posture from the Linux lesson applies directly, extended to cluster and cloud tooling:
The api deployment in the production namespace is crash-looping.
Investigate using read-only commands only: kubectl get, describe, logs, and events. Do not delete, restart, scale or patch anything.
Tell me what the logs and events indicate, what you think the cause is, and what you would change — but do not change it.
Two things make this work. The command list is explicit rather than a general instruction to be careful, and the deliverable is a diagnosis rather than a fix.
The temptation during an incident is to let the agent act, because acting is faster. It is worth being clear-eyed that this is exactly the wrong moment for that: you are under pressure, your attention is degraded, the environment is production, and the diagnosis has not been confirmed. Every factor that makes supervision important is at its worst.
GitOps changes the question
If your infrastructure is managed by Argo CD or Flux, the right target for an agent is almost never the live cluster. It is the repository.
A manual kubectl patch against a GitOps-managed resource does one of two
things: it is reverted at the next sync, or it creates drift that someone
debugs later. Neither is what you wanted.
This actually makes agentic work easier and safer, because the workflow collapses into ordinary repository work:
Update the api deployment’s memory limit from 512Mi to 1Gi.
This cluster is managed by Argo CD, so change the manifest in this repository — do not touch the cluster.
After editing, run kubeconform against the changed manifest and show me the diff.
Now the agent is editing YAML in a git repository, with validation available and a pull request as the approval gate. The dangerous tools are not merely denied; they are irrelevant to the task.
Policy scanning and the second question
Validation asks whether a configuration is well-formed. It does not ask whether it is a good idea. Those are different questions and need different tools.
terraform validate will happily accept a security group open to the world on
every port, an unencrypted bucket, and hard-coded credentials. All of it is valid
HCL. Policy scanners — Checkov, tfsec, kube-score, kubeconform, ansible-lint
— answer the second question, and they are static, so they are safe to pre-approve.
| Pattern | Decision | Why |
|---|---|---|
shell(checkov:*) | Allowed | Static policy scan of local files. |
shell(tfsec:*) | Allowed | Static security scan. |
shell(kubeconform:*) | Allowed | Schema validation from local schemas. |
shell(ansible-lint:*) | Allowed | Static playbook analysis. |
shell(hadolint:*) | Allowed | Dockerfile linting. |
Written for this lesson to illustrate least privilege. Not a GitHub default and not a security guarantee — read every rule before using it.
An agent that runs a scanner and iterates against real findings produces substantially better infrastructure code than one asked to write securely. Findings are specific, they cite a rule, and they are checkable — which is exactly the kind of feedback a model uses well.
One caution from Cluster 4’s Terraform work: scanners produce false positives, and an agent asked to make findings disappear will sometimes suppress rather than fix. Ask for a justification per suppression, and read them.
A worked session: reviewing Terraform
Review the Terraform configuration in this repository.
Do not apply anything. Run only terraform fmt -check and terraform validate.
Identify:
- Resources that would be destroyed or replaced by the current configuration
- Security concerns: public access, unencrypted storage, overly broad IAM
- Hard-coded values that should be variables
If producing a plan requires cloud credentials, stop and tell me what would be needed rather than attempting it.
The final paragraph is the technique to reuse. Without it, an agent blocked on credentials tends to search for another route to the same goal — a different profile, an environment variable, a credentials file. Giving it a defined action for the blocked case prevents that.
Cluster 4’s Copilot for Terraform covers what
validate does and does not check — the short version being that it verifies the
configuration against the provider schema and says nothing about whether the
infrastructure is a good idea. A policy scanner such as Checkov answers a
different question, and both are worth running.
Reading a plan properly
When you do produce a plan, the numbers that matter are not the summary line.
Destroy count. Any non-zero destroy in a change you thought was additive is a misunderstanding somewhere.
forces replacement. This phrase means delete-then-create. For a database or
a stateful volume it is the difference between a config change and an outage.
Changes you did not ask for. Drift, provider version upgrades, or a module default that moved.
Here is the terraform plan output:
[paste the plan]
List every resource that will be destroyed or replaced, and for each one explain what causes the replacement and what the operational impact would be.
Do not tell me whether to apply it.
That last line keeps the agent doing analysis rather than making the decision. The analysis is genuinely useful; the recommendation is not something it has the context to give.
Automation, carefully
Everything above assumes a human at a terminal. Infrastructure automation with an agent is possible and is the subject of the GitHub Actions lesson, with one principle worth stating here.
Automate analysis before automating action. A workflow that reviews a Terraform plan and comments on a pull request is genuinely useful and cannot break anything. A workflow that applies infrastructure changes based on model output is a different category of decision, and it should be a deliberate one made with approval gates rather than an incremental extension of the first.
Multi-environment work, and the context problem
The single most consequential fact in an infrastructure command is usually not in
the command. kubectl delete deployment api does not say which cluster.
terraform apply does not say which workspace. aws s3 rm does not say which
account.
All of that lives in ambient state: the current context, the selected workspace, the active profile, environment variables set an hour ago. An agent proposing a command is not choosing an environment — the shell already chose, and neither of you is looking at it.
Three habits address this.
Make the environment explicit in the prompt, so a mismatch is visible:
This shell is pointed at the staging cluster. Every command you propose
should be for staging. If something appears to be operating against
production, stop and tell me.Prefer commands that name the target. kubectl --context=staging get pods is
longer and self-documenting; terraform workspace show before anything else costs
one command.
Separate shells by environment. A terminal that has never been authenticated against production cannot damage it, whatever the agent proposes. This is the only control here that does not depend on anyone reading carefully, which makes it the most reliable one.
What good looks like after a few weeks
A team using this well tends to converge on a similar setup, and it is worth naming as a target.
Instructions in the repository state the environment, the tooling, and the
validation expected — so every session starts knowing that this is OpenTofu not
Terraform, that manifests are validated with kubeconform, and that nothing is
applied from a developer machine.
A small number of named agents encode the common tasks: a reviewer that cannot write, a validator that runs the scanners. Their tool restrictions are in version control and reviewed like any other config.
Aliases carry the denials, so the safe invocation is the convenient one. Nobody relies on remembering flags under pressure.
And CI does the applying, through a workflow with approval gates, so the question “should this change go live” is answered by a pull request review rather than by an approval prompt at 2am.
Reviewing infrastructure code the agent wrote
Generated infrastructure code has a characteristic failure profile, and knowing it makes review faster.
Permissive defaults. This is the dominant pattern and it is consistent across every provider: security groups wider than needed, IAM policies with wildcards, storage without encryption enabled, public access not explicitly blocked. The generated code works, which is the problem — nothing fails to make you look.
Missing lifecycle configuration. Retention, versioning, deletion protection, backup. These are absences, so they do not appear in a diff as anything.
Version constraints omitted or too loose. A provider or module without a pinned version is a future surprise, and it is the kind of omission that costs nothing today.
Plausible but wrong attribute names. Provider schemas are large and models
occasionally produce an argument that reads correctly and does not exist.
terraform validate catches this immediately, which is why running it after every
edit matters more here than style would suggest.
Copy-paste drift across resources. Three similar resources where the third has a subtly different setting, because it was generated separately. Reading them side by side catches it; reading top to bottom does not.
What to carry forward
Split by command, not by tool. terraform validate and terraform apply are
not the same risk and should not share a permission.
Deny the destructive verbs explicitly. Denial survives --allow-all-tools,
which makes it the only durable control.
Treat dry runs as network operations. They authenticate, they connect, they can lock state.
Check the context before every stateful command. Cluster, namespace, profile, workspace — none of these appear in the command.
Enforce boundaries at the provider with read-only roles where you can. It is the only control that does not depend on your flags being right.
The through-line of this lesson is that infrastructure tooling already draws the line you need; the permission system simply lets you enforce it.
A note on OpenTofu, and on tool drift generally
Cluster 4 covers OpenTofu alongside Terraform, and the fork is a good illustration of a problem this cluster keeps running into: an agent’s defaults reflect what was common in its training data, not what your repository uses.
Ask for “Terraform” help in an OpenTofu repository and you will get terraform
commands, terraform documentation references, and occasionally provider syntax
that has diverged. The commands mostly work, which makes the mismatch harder to
notice than an outright error would be.
The fix is the same one as for distributions in the Linux lesson: state it once, in the repository’s instructions, rather than correcting it every session.
This repository uses OpenTofu, not Terraform. Use `tofu`, not `terraform`.
State is in a remote backend; never run `tofu init` with -reconfigure.The general form of the rule: anywhere your stack differs from the most common version of your stack, write it down. That is where an agent’s defaults will be confidently wrong.
Next
Custom agents turns these policies into reusable, named configurations — an infrastructure-reviewer agent whose tool list makes the constraints permanent rather than something you retype. Custom instructions does the same for the rules about your environment.
Cluster 4 covers the tools themselves in depth: DevOps pillar, Terraform, Kubernetes and CI/CD pipelines.
Sources
Every version-sensitive claim on this page was checked against first-party documentation. Only sources actually used are listed.
Your progress
Saved in this browser only. No account, no server, and nothing leaves your device. Clearing site data resets it.Sync across devices if you want it everywhere.
Saved in this browser and synced to your account, so it follows you between devices. Manage or delete it.
Was this lesson helpful?
We record which lesson you rated and whether it helped. Nothing identifies you — no account, no cookie, no session.