Terraform is the worst place to accept generated code uncritically, because it is the one where "wrong" can mean a deleted database rather than a failing test.
It is also a place where Copilot is genuinely useful — provider schemas are exactly the kind of thing nobody should be memorising. The trick is knowing which parts of the output are checkable and which parts are judgement.
Here is what I check, and the two gotchas that have actually bitten me.
terraform validate does not mean safe
terraform validate checks your configuration against the provider schema. Types line up, required arguments are present, references resolve.
It says precisely nothing about whether the infrastructure you described is a good idea. A bucket open to the world, a security group admitting 0.0.0.0/0, an IAM policy with Action: "*" — all valid configuration. All pass validate without a murmur.
Run a policy scanner too. tflint, checkov, tfsec — pick one and put it in CI. Same distinction as schema-versus-policy everywhere else: one tool checks the shape, the other checks the idea.
The number to read in a plan is the destroy count
Not the create count. Not the change count.
Plan: 3 to add, 1 to change, 2 to destroy.
And the phrase to search the plan output for is forces replacement.
A replacement is a delete followed by a create. For a stateless resource that is a rolling update. For an RDS instance, an EBS volume or anything else holding data, it is exactly what it sounds like. forces replacement on a stateful resource is the line that should stop the review.
This is the single highest-value habit in working with generated Terraform, and it takes about four seconds.
count versus for_each: the one that destroys things quietly
count = length(var.names) # positional
for_each = toset(var.names) # keyed
With count, resources are tracked by index. Remove the second item from a three-item list and Terraform does not "delete the second one" — it re-indexes. Item three shifts into slot two. The plan shows a modify of slot two and a destroy of slot three.
With for_each, resources are tracked by key. Remove an item and exactly that item is destroyed.
Use for_each for collections. Reserve count for genuine on/off conditionals. Copilot reaches for count roughly every time, because count is older and more of the corpus uses it.
Validation blocks are free and nobody writes them
variable "instance_count" {
type = number
description = "Number of application instances."
validation {
condition = var.instance_count > 0 && var.instance_count <= 20
error_message = "instance_count must be between 1 and 20."
}
}
An unvalidated ranged variable is a typo that reaches apply. This is the kind of boilerplate Copilot writes well and instantly, and it will not write it unprompted — ask for validation blocks on every bounded variable and you get them.
Suppressing a scanner finding: put the comment inside the block
This one is pure trivia until it wastes an afternoon.
resource "aws_s3_bucket" "logs" {
# checkov:skip=CKV_AWS_18:Access logging would recurse on the log bucket itself
bucket = "example-logs"
}
Checkov attributes findings to a line range, so a skip comment placed above the resource block is outside the range and is silently ignored. The scan keeps failing, the comment looks right, and you start doubting the tool.
Inside the block. Always with a reason — a bare suppression is a finding somebody decided not to think about.
Credit where due: Checkov's CKV_AWS_41 catches hardcoded credentials in .tf files, which is more than most linters manage for secrets.
Never let a credential into a .tf file
Not as a default, not as a .tfvars committed "temporarily". And remember that a secret referenced anywhere in a configuration ends up in state, in plaintext — so state gets a remote backend with encryption and locking, and *.tfstate goes in .gitignore next to *.tfvars.
The rules worth committing
- Every variable has a type, a description, and a validation block where
the value is bounded.
- for_each for collections; count only for on/off conditionals.
- No hardcoded region, account id, or ARN — variables or data sources.
- Secrets come from the secret manager. Never a variable default, never
a committed .tfvars.
- Security groups name a source. 0.0.0.0/0 needs a comment justifying it.
Four lines in .github/copilot-instructions.md change every future suggestion in the repository. That is a better return than any amount of prompt refinement, because it supplies context the model did not have rather than asking it to try harder.
Fuller version, including the plan-review checklist and the policy-scanner setup, at GitHub Copilot for Terraform. There is a hands-on lab that walks through writing a validated module without ever running apply, and a starter repository to go with it.
Top comments (0)