<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: The Copilot Stack</title>
    <description>The latest articles on DEV Community by The Copilot Stack (thecopilotstack).</description>
    <link>https://dev.to/thecopilotstack</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F14719%2Fa130f0e5-79a4-4eaf-bf92-febb7d0362aa.png</url>
      <title>DEV Community: The Copilot Stack</title>
      <link>https://dev.to/thecopilotstack</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/thecopilotstack"/>
    <language>en</language>
    <item>
      <title>Reviewing Generated Terraform: The Destroy Count, count vs for_each, and a Comment in the Wrong Place</title>
      <dc:creator>James Joyner</dc:creator>
      <pubDate>Thu, 17 Sep 2026 04:44:41 +0000</pubDate>
      <link>https://dev.to/thecopilotstack/reviewing-generated-terraform-the-destroy-count-count-vs-foreach-and-a-comment-in-the-wrong-57o9</link>
      <guid>https://dev.to/thecopilotstack/reviewing-generated-terraform-the-destroy-count-count-vs-foreach-and-a-comment-in-the-wrong-57o9</guid>
      <description>&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Here is what I check, and the two gotchas that have actually bitten me.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;terraform validate&lt;/code&gt; does not mean safe
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;terraform validate&lt;/code&gt; checks your configuration against the &lt;strong&gt;provider schema&lt;/strong&gt;. Types line up, required arguments are present, references resolve.&lt;/p&gt;

&lt;p&gt;It says precisely nothing about whether the infrastructure you described is a good idea. A bucket open to the world, a security group admitting &lt;code&gt;0.0.0.0/0&lt;/code&gt;, an IAM policy with &lt;code&gt;Action: "*"&lt;/code&gt; — all valid configuration. All pass &lt;code&gt;validate&lt;/code&gt; without a murmur.&lt;/p&gt;

&lt;p&gt;Run a policy scanner too. &lt;code&gt;tflint&lt;/code&gt;, &lt;code&gt;checkov&lt;/code&gt;, &lt;code&gt;tfsec&lt;/code&gt; — 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  The number to read in a plan is the destroy count
&lt;/h2&gt;

&lt;p&gt;Not the create count. Not the change count.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Plan: 3 to add, 1 to change, 2 to destroy.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the phrase to search the plan output for is &lt;strong&gt;&lt;code&gt;forces replacement&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;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. &lt;code&gt;forces replacement&lt;/code&gt; on a stateful resource is the line that should stop the review.&lt;/p&gt;

&lt;p&gt;This is the single highest-value habit in working with generated Terraform, and it takes about four seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;count&lt;/code&gt; versus &lt;code&gt;for_each&lt;/code&gt;: the one that destroys things quietly
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;count&lt;/span&gt;    &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# positional&lt;/span&gt;
&lt;span class="nx"&gt;for_each&lt;/span&gt; &lt;span class="err"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;toset&lt;/span&gt;&lt;span class="err"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="err"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;names&lt;/span&gt;&lt;span class="err"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# keyed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;count&lt;/code&gt;, resources are tracked by &lt;strong&gt;index&lt;/strong&gt;. 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 &lt;strong&gt;destroy of slot three&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;for_each&lt;/code&gt;, resources are tracked by key. Remove an item and exactly that item is destroyed.&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;for_each&lt;/code&gt; for collections. Reserve &lt;code&gt;count&lt;/code&gt; for genuine on/off conditionals. Copilot reaches for &lt;code&gt;count&lt;/code&gt; roughly every time, because &lt;code&gt;count&lt;/code&gt; is older and more of the corpus uses it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validation blocks are free and nobody writes them
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;variable&lt;/span&gt; &lt;span class="s2"&gt;"instance_count"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;type&lt;/span&gt;        &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;number&lt;/span&gt;
  &lt;span class="nx"&gt;description&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"Number of application instances."&lt;/span&gt;

  &lt;span class="nx"&gt;validation&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;condition&lt;/span&gt;     &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_count&lt;/span&gt; &lt;span class="err"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="err"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;var&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;instance_count&lt;/span&gt; &lt;span class="err"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
    &lt;span class="nx"&gt;error_message&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"instance_count must be between 1 and 20."&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An unvalidated ranged variable is a typo that reaches &lt;code&gt;apply&lt;/code&gt;. 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Suppressing a scanner finding: put the comment &lt;em&gt;inside&lt;/em&gt; the block
&lt;/h2&gt;

&lt;p&gt;This one is pure trivia until it wastes an afternoon.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight hcl"&gt;&lt;code&gt;&lt;span class="nx"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"aws_s3_bucket"&lt;/span&gt; &lt;span class="s2"&gt;"logs"&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;# checkov:skip=CKV_AWS_18:Access logging would recurse on the log bucket itself&lt;/span&gt;
  &lt;span class="nx"&gt;bucket&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"example-logs"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checkov attributes findings to a &lt;strong&gt;line range&lt;/strong&gt;, so a skip comment placed &lt;em&gt;above&lt;/em&gt; 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.&lt;/p&gt;

&lt;p&gt;Inside the block. Always with a reason — a bare suppression is a finding somebody decided not to think about.&lt;/p&gt;

&lt;p&gt;Credit where due: Checkov's &lt;code&gt;CKV_AWS_41&lt;/code&gt; catches hardcoded credentials in &lt;code&gt;.tf&lt;/code&gt; files, which is more than most linters manage for secrets.&lt;/p&gt;

&lt;h2&gt;
  
  
  Never let a credential into a &lt;code&gt;.tf&lt;/code&gt; file
&lt;/h2&gt;

&lt;p&gt;Not as a default, not as a &lt;code&gt;.tfvars&lt;/code&gt; committed "temporarily". And remember that a secret referenced anywhere in a configuration ends up in &lt;strong&gt;state&lt;/strong&gt;, in plaintext — so state gets a remote backend with encryption and locking, and &lt;code&gt;*.tfstate&lt;/code&gt; goes in &lt;code&gt;.gitignore&lt;/code&gt; next to &lt;code&gt;*.tfvars&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rules worth committing
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="p"&gt;-&lt;/span&gt; Every variable has a type, a description, and a validation block where 
  the value is bounded.
&lt;span class="p"&gt;-&lt;/span&gt; for_each for collections; count only for on/off conditionals.
&lt;span class="p"&gt;-&lt;/span&gt; No hardcoded region, account id, or ARN — variables or data sources.
&lt;span class="p"&gt;-&lt;/span&gt; Secrets come from the secret manager. Never a variable default, never 
  a committed .tfvars.
&lt;span class="p"&gt;-&lt;/span&gt; Security groups name a source. 0.0.0.0/0 needs a comment justifying it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four lines in &lt;code&gt;.github/copilot-instructions.md&lt;/code&gt; 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.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Fuller version, including the plan-review checklist and the policy-scanner setup, at &lt;a href="https://thecopilotstack.com/github-copilot/devops/terraform/" rel="noopener noreferrer"&gt;GitHub Copilot for Terraform&lt;/a&gt;. There is a &lt;a href="https://thecopilotstack.com/labs/terraform-with-copilot/" rel="noopener noreferrer"&gt;hands-on lab&lt;/a&gt; that walks through writing a validated module without ever running &lt;code&gt;apply&lt;/code&gt;, and a starter repository to go with it.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>terraform</category>
      <category>devops</category>
      <category>aws</category>
      <category>githubcopilot</category>
    </item>
  </channel>
</rss>
