GitHub Copilot CLI for DevOps Engineers

GitHub Copilot CLIAcademy lesson 59Cluster 5 · Lesson 8 of 12Intermediate → Advanced15 min readVersion-sensitive
Published
Updated
Last technically verified
GitHub Copilot CLI for DevOps EngineersGitHub Copilot CLI8Intermediate → Advanced/github-copilot/cli/devops/

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.

The ordering

Validation before mutation

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.

Example policyTerraform: read, format, validate and plan freely. Never apply, never destroy.
Example tool permission policy
PatternDecisionWhy
shell(terraform fmt:*)AllowedFormatting is mechanical and reversible.
shell(terraform validate:*)AllowedType-checks against the provider schema; touches nothing.
shell(terraform plan:*)Asks firstReads state and needs credentials — not as inert as it looks.
shell(terraform apply:*)DeniedChanges real infrastructure.
shell(terraform destroy:*)DeniedThere 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

Example policykubectl: inspect anything, change nothing.
Example tool permission policy
PatternDecisionWhy
shell(kubectl get:*)AllowedRead-only inspection.
shell(kubectl describe:*)AllowedRead-only detail.
shell(kubectl logs:*)AllowedReading logs changes nothing.
shell(kubectl apply:*)Asks firstEven a dry run reaches the API server.
shell(kubectl delete:*)DeniedDeleting 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.

Example policyAnsible: syntax and check mode freely; real runs stay deliberate.
Example tool permission policy
PatternDecisionWhy
shell(ansible-lint:*)AllowedStatic analysis.
shell(ansible-playbook --syntax-check:*)AllowedParses without connecting.
shell(ansible-playbook --check:*)Asks firstCheck mode still connects to hosts.
shell(ansible-playbook:*)DeniedA 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:

Copilot prompt

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:

Copilot prompt

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.

Example policyScanners are static analysis: no credentials, no network, no state. Pre-approve them all.
Example tool permission policy
PatternDecisionWhy
shell(checkov:*)AllowedStatic policy scan of local files.
shell(tfsec:*)AllowedStatic security scan.
shell(kubeconform:*)AllowedSchema validation from local schemas.
shell(ansible-lint:*)AllowedStatic playbook analysis.
shell(hadolint:*)AllowedDockerfile 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

Copilot prompt

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.

Copilot prompt

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.

Primary sources