GitHub Copilot for DevOps Engineers
Cluster 3 ended on an idea worth carrying into this one: the useful question is never how good is Copilot at this, but what does this let me check.
Infrastructure is where that question gets sharpest, for two reasons that pull in opposite directions.
The configuration languages check almost nothing. YAML has no type system. HCL has a validator that confirms the syntax parses and the references resolve, and says nothing about whether the resulting resource is safe. A Kubernetes manifest with a privileged container, no resource limits and a public load balancer is a completely valid Kubernetes manifest.
And the cost of being wrong is unbounded in a way application code rarely is. A
wrong function returns a wrong number. A wrong terraform apply replaces a
database. A wrong kubectl delete runs against whichever cluster your context
happens to point at.
So this cluster is organised around one claim, and every lesson in it supplies evidence for the same claim from a different angle:
Copilot can generate infrastructure. Validation determines whether that infrastructure is trustworthy — and the validation you have is narrower than you think.
Key takeaways
- Generated infrastructure needs a stricter review than generated application code, because the languages check less and the failures are less reversible.
- Every technology here has a local, non-destructive validation sequence: format, lint, validate, scan, plan. Run all five before you read the diff.
- Linters catch their own class of problem and are blind to the rest. While writing this cluster, hadolint reported six findings on a Dockerfile and never mentioned that it ran as root; ansible-lint reported nineteen findings on a playbook and never mentioned the plaintext password in it.
- A clean scan and an empty scan look identical in a CI log. Checkov returned no findings on a Cloud Run configuration here — because it has no policies for that resource type at all.
- Repository and path-specific custom instructions are how you stop repeating
the same corrections.
.github/copilot-instructions.mdand.github/instructions/*.instructions.mdare the two files that matter.
Why infrastructure needs a stricter review
Three properties separate a Terraform file from a Python function, and each one removes a safety net you are used to having.
There is no runtime to fail in. Application code has a place where being wrong shows up cheaply: a test run, a local execution, a staging request. A Terraform configuration’s first execution is against real infrastructure. There is no equivalent of running it locally and seeing an exception.
The blast radius is not bounded by the change. A bug in a function affects
the callers of that function. A bug in a security group affects everything
behind it. A bug in an IAM policy affects everything that role can reach, which
if the policy says "Action": "*" is the entire account.
Failures are frequently not reversible. This is the one that matters most. Deleting a bucket, terminating an instance, dropping a managed database, replacing a resource whose name is globally unique — none of these has an undo. The closest thing infrastructure has to a rollback is a second deployment, which is a new change with its own risk.
The validation model
Every lesson in this cluster instantiates the same sequence. The commands differ; the shape does not.
- GenerateCopilot produces the configuration, with your repository instructions in context.
- Formatterraform fmt, gofmt, shfmt, yamlfmt. Removes style from the diff so the real change is visible.
- Linthadolint, ansible-lint, ShellCheck, actionlint. Catches the mechanical class of error in seconds.
- Validateterraform validate, kubeconform, docker compose config, ansible-playbook --syntax-check. Confirms it parses and resolves.
- Security scanCheckov, Trivy, kube-linter. Catches the policy class — and only the policies it happens to have.
- Plan or dry runterraform plan, what-if, --check. The last mechanical step before anything changes.
- Read the planHuman judgementSpecifically: count the destroys and replacements. This is the step nobody automates and everybody skips.
- Pull request reviewHuman judgementA second person, with the scan output and the plan attached.
- Controlled deploymentHuman judgementA protected environment, an approval, and one target before all of them.
Steps marked Human judgement are the ones that do not get delegated — they are where you decide whether what Copilot produced is actually right.
The first six steps are cheap, mechanical, and belong in CI. The last three are judgement, and no amount of tooling replaces them.
The twelve technologies
| Technology | Copilot can help with | Primary validation | Major risk |
|---|---|---|---|
| DockerLesson 40 | Dockerfiles, multi-stage builds, .dockerignore | hadolint, then docker build | Root user, unpinned tags, secrets baked into a layer |
| Docker ComposeLesson 41 | Service definitions, networks, volumes, health checks | docker compose config | Ports bound to 0.0.0.0, secrets in environment blocks |
| KubernetesLesson 42 | Deployments, Services, ConfigMaps, probes, securityContext | kubeconform for schema, Checkov for policy | Privileged containers, missing limits, wrong cluster context |
| TerraformLesson 43 | Resources, variables, modules, outputs, expressions | terraform fmt, validate, then plan | A plan that replaces or destroys a live resource |
| OpenTofuLesson 44 | The same HCL, plus OpenTofu-only features | tofu fmt, validate, then plan | Assuming Terraform and OpenTofu are interchangeable |
| AnsibleLesson 45 | Playbooks, roles, handlers, templates, variables | ansible-playbook --syntax-check, then ansible-lint | Running against the wrong inventory, or a non-idempotent shell task |
| LinuxLesson 46 | Command explanation, diagnostics, scripting, log analysis | shellcheck and bash -n; read every command first | A destructive command run against the wrong host or path |
| AWSLesson 47 | IAM policies, CLI invocations, Terraform for AWS, architecture explanation | JSON and IaC validation locally; never a live apply | Wildcard IAM, public S3, a CLI command run against production |
| Microsoft AzureLesson 48 | Bicep modules, Azure CLI invocations, RBAC scoping | bicep build, then az deployment what-if | Wrong subscription context, over-scoped RBAC, public endpoints |
| Google CloudLesson 49 | gcloud commands, Terraform for GCP, IAM bindings, Cloud Run config | Terraform validate; never a live gcloud mutation | Wrong active project, primitive IAM roles, public buckets |
| GitHub ActionsLesson 50 | Workflow structure, jobs, matrices, caching, reusable workflows | actionlint, then read the permissions block | Secret exposure, script injection, over-permissive GITHUB_TOKEN |
| AI-Powered CI/CDLesson 51 | Every artefact in the pipeline — and none of the trust decisions | Every gate below, run in CI on every pull request | Treating a green pipeline as a substitute for review |
- CodeThe application, and the tests that say whether it works.
- ContainerOne image, built reproducibly, running as a non-root user.Docker
- Local environmentThe service plus its dependencies, on a laptop.Docker Compose
- OrchestrationScheduling, health, rollout and network policy across a cluster.Kubernetes
- InfrastructureThe cluster, the network and the managed services underneath it.TerraformOpenTofu
- ConfigurationThe state of the machines that are not disposable.AnsibleLinux
- CloudThe account, the identities and the bill.AWSAzureGoogle Cloud
- AutomationThe gates that decide whether any of the above is allowed to change.GitHub ActionsCI/CD
What each linter actually catches — measured
The most useful thing this cluster can tell you is not that linters are good.
It is exactly where each one stops. Every figure below was produced while
writing these lessons, against the examples in
examples/cluster-4/,
using the tool versions recorded in each article.
| Draft artifact | Linter | Found | Missed |
|---|---|---|---|
| Dockerfile | hadolint 2.15.1 | 6 | that the container runs as root |
| Kubernetes manifests | kubeconform 0.8.0 | 0 | :latest, no limits, no probes, public LoadBalancer |
| Kubernetes manifests | Checkov 3.3.13 | 21 | — |
| Ansible playbook | ansible-lint 26.8.0 | 19 | a plaintext database password |
| Shell script | ShellCheck 0.11.0 | 6 | rm -rf /tmp/* inside a “health check” |
| Actions workflow | actionlint 1.7.12 | 2 | missing permissions:, a typo’d image tag |
| Terraform | Checkov 3.3.13 | 18 | — |
Two rows deserve attention.
kubeconform found nothing. The draft manifest — :latest image, no resource
limits, no probes, no securityContext, a LoadBalancer service exposing it
publicly — passed kubeconform -strict with Valid: 2, Invalid: 0. That is not
a bug in kubeconform. Schema validation confirms the document matches the
Kubernetes API’s shape. Safety is a different question, and it needs a different
tool. Running Checkov over the same file produced twenty-one failures.
ansible-lint found nineteen problems and not the password. The draft
playbook contained line: "db_password=hunter2". The only place the word
“password” appeared in ansible-lint’s output was a complaint that the task name
set password should start with a capital letter.
A clean scan and an empty scan look identical
This one surprised me, and it is worth its own section because it undermines a habit almost everyone has.
The Google Cloud lesson’s configuration defines a Cloud Run service, two service
accounts and an IAM binding. Running checkov -d . --framework terraform
against it produced no output at all — no findings, no summary, nothing.
The obvious reading is “no problems”. The correct reading is that Checkov has no
policies for google_cloud_run_v2_service, google_service_account or
google_cloud_run_v2_service_iam_member, so zero checks ran.
The two were distinguished by running the same command against a bare
google_storage_bucket, which produced four findings — proving the Google
framework works and the coverage for those specific resource types does not
exist.
YAML, HCL and Bash: what the languages give you
It is worth being specific about the substrate, because “infrastructure as code” covers three quite different languages and each fails differently.
YAML is a serialisation format, not a language, and it has no schema of its
own. A Kubernetes manifest, a Compose file, an Ansible playbook and a GitHub
Actions workflow are all YAML, and the only thing YAML guarantees about any of
them is that the indentation parses. Everything else — is replicas a number,
does on: mean what you think, is no a string or a boolean — is enforced by
whatever consumes the file, if anything does.
Two YAML behaviours cause real incidents and both appear in generated
configuration. Unquoted values that look like other types get coerced: a version
1.10 becomes the number 1.1, and a Norwegian country code NO became false
often enough that the YAML specification changed. And indentation is
semantically load-bearing with no closing delimiter, so a block that is
accidentally nested one level deeper is usually still valid YAML meaning
something else entirely.
The mitigation is schema validation, which is why every YAML-based lesson in
this cluster has one: kubeconform for manifests, docker compose config for
Compose, ansible-playbook --syntax-check for playbooks, actionlint for
workflows. yamllint on its own tells you the file is well-formed YAML, which is
the least interesting thing you can know about it.
HCL is the strongest of the three. terraform validate type-checks
variables, resolves every reference, and rejects an attribute that does not exist
on a resource — so a hallucinated argument fails locally rather than at apply
time. That is a genuinely useful gate, and it is why the Terraform and OpenTofu
lessons can promise more than the YAML ones.
What HCL does not check is anything about the resulting infrastructure. A configuration that opens a security group to the world validates perfectly. The type system describes the provider’s API, not your intent.
Bash checks nothing at all, which is covered in the Linux lesson and in GitHub Copilot for Bash. It is the one substrate here where a wrong line executes immediately with your privileges.
Agent mode and the validation loop
The mechanical steps in this cluster’s validation sequence share a property that makes them unusually well suited to agent mode: they produce precise, structured error output, and fixing the error is usually mechanical.
That is the shape agent mode is good at. Generate, run terraform validate, read
the error, fix, repeat until clean. Each iteration is verified by a command
rather than by the model’s own judgement, which is what makes the loop converge
instead of drift.
Set it up so the loop is available:
- Put the validation commands in your repository instructions, so the agent knows what “done” means without being told each time.
- Make them runnable with no credentials.
terraform init -backend=falseexists precisely so validation does not need a backend. Every command in the first six steps of the sequence is local by design. - Keep the destructive commands out of reach. An agent that can run
terraform validateand cannot runterraform applyis exactly the right set of permissions.
The corollary is that agent mode is at its weakest here for the same reason it is strong elsewhere. It cannot tell whether the architecture is right, whether the resource should exist, or whether the plan’s two replacements are acceptable. It closes the loop on the mechanical layer, which frees your attention for the layer it cannot reach.
Reading a plan
terraform plan is the highest-signal artefact in this entire cluster, and it is
routinely skimmed. It deserves a method.
Read the summary line first, and read it as three numbers. Plan: 4 to add, 2 to change, 1 to destroy. The destroy count is the one that matters. If it is
not zero and you did not expect it to be non-zero, stop.
Find every # … must be replaced and read why. Terraform annotates the
attribute that forces replacement with # forces replacement. A replacement is
a delete followed by a create, which for a database or a bucket means the data
is gone. This is the single most expensive thing a plan can contain and it is
one line of output.
Check for (known after apply) on anything you care about. It means
Terraform cannot tell you the value in advance, which is fine for an ARN and
alarming for a count.
Look at what is not in the plan. A change you expected and do not see means the configuration does not do what you think — often because a resource is in a module you did not edit, or because state has drifted.
The same discipline applies to the other technologies’ equivalents:
az deployment group what-if produces a change list with a delete section,
kubectl diff shows what an apply would alter, and ansible-playbook --check
reports which tasks would report changed. Each is worth the minute it takes.
Troubleshooting and incident work
The most under-discussed place Copilot earns its keep in infrastructure is not writing configuration at all. It is the middle of an incident, when the useful skill is enumerating hypotheses quickly.
Explaining an error. CrashLoopBackOff with an events list,
ImagePullBackOff, a Terraform cycle, an admission webhook rejection, an
AccessDenied naming an action you have not seen. These are dense, precise
inputs and a model reads them faster than you do.
Generating the diagnostic sequence. This is the prompt shape to internalise, because it is safe by construction:
A pod is crash-looping after a deployment that passed CI.
List the read-only commands that would distinguish an image problem, a scheduling problem, a failing probe and an application error, in the order you would run them.
Do not propose changing, restarting or deleting anything until I have given you the output.
Three clauses are doing the work. Read-only stops the model proposing a restart as step one, which destroys the evidence. Distinguish these categories turns a list of commands into a narrowing sequence rather than a checklist. And not until I have given you the output prevents the most common failure mode, which is a confident remedy for a cause nobody has established.
The Linux lesson works through this shape in detail, including the follow-up prompt that makes the model’s reasoning checkable against the output you paste back.
Writing the incident timeline afterwards. Genuinely useful, genuinely tedious, and low-risk: a model summarising a log excerpt and a commit history into a chronology is doing a reading task, and you will check it anyway.
What it cannot do is know your environment. It does not know that the load balancer health check has a two-second timeout, that the previous deploy changed the readiness path, or that this cluster has a webhook that rejects Pods without a particular label. Those are the answers, and they come from your context, not the model’s.
Repository instructions: the mechanism that scales
Correcting the same three things in every review is a signal that the correction belongs in a file rather than in your head. GitHub Copilot supports two kinds, and both are worth setting up before you generate much infrastructure.
For infrastructure, the path-specific files are the more valuable of the two, because the rules genuinely differ per file type. What you want Copilot to do in a Dockerfile has nothing to do with what you want it to do in a workflow.
.github/ ├── copilot-instructions.md project layout, gates, the six rules └── instructions/ ├── docker.instructions.md applyTo: “/Dockerfile,/Dockerfile.” ├── terraform.instructions.md applyTo: ”**/.tf,/*.tfvars” ├── kubernetes.instructions.md applyTo: “k8s//.yaml” ├── ansible.instructions.md applyTo: “roles/,site.yml” ├── shell.instructions.md applyTo: “/.sh” └── actions.instructions.md applyTo: “.github/workflows/**/*.yml”
The instruction files in this cluster’s examples are real and worth reading in full, but the pattern they share is short. Each one says what to always do, what to never do, and which commands to run before considering a change complete. The last of those three does the most work.
Before considering an infrastructure task complete:
- Format the configuration.
- Run syntax validation for the file type you changed.
- Run the relevant linter and report every finding, including ones you consider unimportant.
- Never apply infrastructure. Propose the change; a human runs it.
- Never write a credential into any file.
- If a change would replace or delete a resource, say so in the first line of your reply, not in a footnote.
Rule six is the one people leave out and then wish they had not. A model that has been told to lead with destructive consequences will lead with them.
Prompting for infrastructure
The general shape from Cluster 3 applies: remove the ambiguity a competent engineer would otherwise resolve differently. What changes for infrastructure is that the ambiguities are about policy rather than about behaviour.
A weak prompt:
Write Terraform for a VPC.
A strong one:
Create Terraform configuration for an AWS VPC with three public and three private subnets across three availability zones.
Requirements:
- No hard-coded credentials. The provider reads from the environment.
- Use variables for the CIDR ranges, and derive the subnets with cidrsubnet() rather than hard-coding them.
- Enable DNS support and DNS hostnames.
- Do not set map_public_ip_on_launch.
- Empty the default security group.
- Add outputs for the VPC ID and the subnet IDs, keyed by availability zone.
- Pin the provider with a ~> constraint.
Do not run terraform apply. After generating, run terraform fmt and terraform validate and show me the output.
Six of those eight clauses are security or operability decisions that a model
will otherwise make by picking whatever is most common in public code — and
public Terraform is full of map_public_ip_on_launch = true and hard-coded
regions.
The clause that generalises best across every technology here is the last one: ask for the validation output, not just the code. It changes the interaction from “write this” to “write this and show me it holds up”, and the second is a much better use of an agent.
Cloud prompts need one more thing
For anything touching a cloud CLI, ask for the analysis before the command:
I need to change the retention policy on a production log group.
Do not execute anything.
Tell me: what the command will change, which permissions it requires, whether the action is destructive, what it will cost, a command to verify the current state before I change it, and how to roll it back.
Then give me the command.
This is a genuinely good use of a model — reading documentation and enumerating consequences is a task it does well, and it is a task most engineers skip when they are in a hurry. Asking for the rollback in advance is the part that has saved the most incidents.
Destructive commands
Every technology in this cluster has a small set of commands that cannot be undone. They are listed in each lesson. Three general rules apply to all of them.
Check the target before the command. kubectl config current-context,
aws sts get-caller-identity, az account show, gcloud config get-value project, terraform workspace show. Ambient context is how a command intended
for staging reaches production.
Never let a model produce a destructive command and a context switch in the
same block. az account set --subscription prod && az group delete … is two
decisions presented as one.
Prefer the dry run that exists. Most tools have one, and each is covered in its own lesson.
Where Copilot genuinely helps
It would be a strange cluster that spent thirteen lessons on review discipline without saying where the value is. Across every technology here, these are consistently worth it:
Explaining configuration you did not write. A Helm chart, a forty-resource
Terraform module, a systemd unit with six ExecStartPre lines, an IAM policy
with nested conditions. Reading is where models are strongest and where the
failure mode is most visible, and infrastructure repositories are full of
material nobody currently on the team wrote.
Interpreting errors. A Terraform cycle error, a CrashLoopBackOff with an
events list, an ImagePullBackOff, a Kubernetes admission rejection, an
AccessDenied naming an action you have never heard of. The error text is
precise context, which is exactly what a model normally lacks.
The repetitive layer. The fifth service that looks like the first four. The
next environment’s tfvars. The matrix expansion of a workflow. The
values-staging.yaml derived from values-prod.yaml.
Translating between formats. docker run flags into a Compose service, a
Compose service into a Kubernetes Deployment, a console click-path into
Terraform. These have a mechanical correspondence, and the review is checking
the correspondence rather than checking a design.
Writing the tests and the checks. The policy assertions, the bats cases,
the smoke test the pipeline runs after a deploy. This is the highest-leverage
thing on the list, because it improves the gate rather than the thing being
gated.
Where it consistently needs correcting
Symmetrically, and drawn from what actually happened while building this cluster’s examples:
Security defaults trend permissive. Public IPs on by default, 0.0.0.0/0
ingress, wildcard IAM, allowPrivilegeEscalation unset, buckets without a
public access block. Not maliciously — permissive configuration is what makes
examples work, and examples dominate the corpus.
Version and API drift. A version: key in a Compose file that has been
obsolete for years. javax imports for a framework that moved to jakarta.
An Actions @v4 tag against a repository now on v7. Public infrastructure code
is old, and infrastructure APIs move.
Resource limits and probes are omitted. Every generated Kubernetes Deployment in this cluster’s drafts lacked both.
Secrets end up in files. A password in a playbook, a key in a provider
block, a connection string in a Compose environment:. Checkov caught the
Terraform one. ansible-lint did not catch the playbook one.
Idempotency is not assumed. Generated Ansible reaches for shell: where a
module exists; generated scripts append rather than converge.
The “obvious” flag combination may not exist. df -P --output= and
df -i --output= are both mutually exclusive in GNU coreutils. A generated
health check used the first, df refused to run, stderr was redirected to
/dev/null, and the entire disk check silently produced nothing while the
script exited 0. That is covered in full in
the Linux lesson, and it is the best single
illustration in this cluster of why “it ran and reported no problems” is not
evidence.
What Copilot cannot see
Worth stating plainly, because most infrastructure mistakes come from a fact that was not in the context rather than from a reasoning error.
State. Copilot reads your configuration. It does not read your Terraform
state, so it cannot know that the resource you are describing already exists
with different attributes, that someone changed it in the console last week, or
that a moved block is needed to avoid a destroy. Drift is invisible to it, and
drift is what turns a benign-looking change into a replacement.
The account. It does not know your quotas, your existing security groups,
your organisation policies, your naming conventions, or which of your three
subscriptions is production. A generated az command is correct in the abstract
and dangerous in the specific.
Cost. No model can tell you what a resource will cost in your account, with
your commitments, in your region. A LoadBalancer service, a NAT gateway, a
provisioned-IOPS volume and a always-on minimum instance count are all
single-line changes with recurring bills. Ask for the cost implication and treat
the answer as a prompt to check, not as a figure.
Blast radius. It cannot know that the security group you are editing is attached to forty instances, or that the namespace you are deleting holds something another team depends on. The configuration in front of it is a fragment of a system it has never seen.
Whether the change should happen at all. GitHub’s own documentation is explicit that inline suggestions work from limited scope and cannot identify broader architectural issues. In infrastructure that limitation is the whole game: the model can produce a correct resource for a design nobody should have chosen.
Is this working?
The wrong measure is volume of accepted configuration. Better questions:
- How often does generated configuration survive the mechanical gates unmodified? Rarely means your instructions files are thin, not that the tool is bad at Terraform.
- How often do you fix the same category twice? Missing limits, a wrong provider version, a convention your team retired. That is an instructions-file problem with a one-line fix.
- Has your plan-reading discipline slipped? Generating more infrastructure than you review is not throughput; it is deferred risk.
- Did the gates get stronger? The best outcome of adopting Copilot for infrastructure is not faster configuration. It is that the policy tests, smoke tests and validation steps you never had time to write now exist.
How the lessons are organised
Each of the twelve technology lessons follows the same skeleton and shares none of its content:
- Where Copilot genuinely helps in that technology, and where it does not.
- A repository-ready example under
examples/cluster-4/, with a README. - The local validation sequence, with an honest record of which commands were run while writing the lesson and what they printed.
- A
draft/artifact — the plausible first suggestion — so linter output on a draft can be compared against linter output on the finished thing. - The security review specific to that technology.
- A
.github/instructions/file you can copy.
Where a validation could not be run, the lesson says so and says why. The Azure lesson is the clearest case: neither the Bicep CLI nor the Azure CLI was available, so its template is labelled an example implementation rather than a tested one. That is a worse outcome than the others and it is stated rather than hidden.
Where to start
If you are picking one lesson, pick the one you are about to write:
- Packaging an application: Docker, then Docker Compose.
- Running it: Kubernetes.
- Provisioning what it runs on: Terraform or OpenTofu, then your cloud — AWS, Azure or Google Cloud.
- Managing machines: Ansible and Linux.
- Automating it: GitHub Actions, then the capstone, Build an AI-Powered CI/CD Pipeline.
From earlier clusters, three lessons underpin this one: GitHub Copilot Best Practices for the review discipline, Agent Mode for the loop this cluster’s validation commands feed, and GitHub Copilot for Bash for the shell fundamentals the Linux lesson assumes.
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.
Was this lesson helpful?
Your answer is stored in this browser and is not sent anywhere.