{"id":2131,"date":"2026-02-21T01:28:49","date_gmt":"2026-02-20T12:28:49","guid":{"rendered":"https:\/\/www.ronella.xyz\/?p=2131"},"modified":"2026-02-21T01:28:49","modified_gmt":"2026-02-20T12:28:49","slug":"terraform-format-and-validation","status":"publish","type":"post","link":"https:\/\/www.ronella.xyz\/?p=2131","title":{"rendered":"Terraform Format and Validation"},"content":{"rendered":"<p>In Terraform projects, format and validation are your first line of defence against messy code and avoidable runtime errors. Think of them as \u201cstyle checking\u201d and \u201csanity checking\u201d for infrastructure as code.<\/p>\n<hr \/>\n<h2>Why Format and Validate at All?<\/h2>\n<p>Terraform configurations tend to grow into large, multi\u2011module codebases, often edited by several engineers at once. Without conventions and guards:<\/p>\n<ul>\n<li>Small style differences accumulate into noisy diffs.<\/li>\n<li>Subtle typos, type mismatches, or broken references sneak into <code>main<\/code>.<\/li>\n<li>Misused modules cause surprises late in <code>plan<\/code> or even <code>apply<\/code>.<\/li>\n<\/ul>\n<p>Formatting (<code>terraform fmt<\/code>) standardises how the code looks, while validation (<code>terraform validate<\/code> and variable validation) standardises what values are acceptable.<\/p>\n<hr \/>\n<h2>Formatting Terraform Code with <code>terraform fmt<\/code><\/h2>\n<h3>What <code>terraform fmt<\/code> Actually Does<\/h3>\n<p><code>terraform fmt<\/code> rewrites your <code>.tf<\/code> files into Terraform\u2019s canonical style.<\/p>\n<p>It:<\/p>\n<ul>\n<li>Normalises indentation and alignment.<\/li>\n<li>Orders and spaces arguments consistently.<\/li>\n<li>Applies a single canonical style across the project.<\/li>\n<\/ul>\n<p>Typical usage:<\/p>\n<pre><code class=\"language-terraform\"># Fix formatting in the current directory\nterraform fmt\n\n# Recurse through modules and subfolders (what you want in a real repo)\nterraform fmt -recursive<\/code><\/pre>\n<p>For CI or pre\u2011commit hooks you almost always want:<\/p>\n<pre><code class=\"language-terraform\">terraform fmt -check -recursive<\/code><\/pre>\n<p>This <strong>checks<\/strong> formatting, returns a non\u2011zero exit code if anything is off, but does not modify files. That makes it safe for pipelines.<\/p>\n<h3>Why This Matters Architecturally<\/h3>\n<ul>\n<li>Consistent formatting reduces cognitive load; you can scan resources quickly instead of re\u2011parsing everyone\u2019s personal style.<\/li>\n<li>Diffs stay focused on behaviour instead of whitespace and alignment.<\/li>\n<li>A shared style is essential when modules are reused across teams and repos.<\/li>\n<\/ul>\n<p>Treat <code>terraform fmt<\/code> like <code>go fmt<\/code>: it\u2019s not a suggestion, it\u2019s part of the toolchain.<\/p>\n<hr \/>\n<h2>Structural Validation with <code>terraform validate<\/code><\/h2>\n<h3>What <code>terraform validate<\/code> Checks<\/h3>\n<p><code>terraform validate<\/code> performs a static analysis of your configuration for syntactic and internal consistency.<\/p>\n<p>It verifies that:<\/p>\n<ul>\n<li>HCL syntax is valid.<\/li>\n<li>References to variables, locals, modules, resources, and data sources exist.<\/li>\n<li>Types are consistent (for example you\u2019re not passing a map where a string is expected).<\/li>\n<li>Required attributes exist on resources and data blocks.<\/li>\n<\/ul>\n<p>Basic usage:<\/p>\n<pre><code class=\"language-bash\">terraform init     # required once before validate\nterraform validate<\/code><\/pre>\n<p>If everything is fine you will see:<\/p>\n<pre><code class=\"language-bash\">Success! The configuration is valid.<\/code><\/pre>\n<p>This does <strong>not<\/strong> contact cloud providers; it is a \u201ccompile\u2011time\u201d check, not an integration test.terraformpilot+1<\/p>\n<h3>Why You Want It in Your Workflow<\/h3>\n<ul>\n<li>Catches simple but common mistakes (typos in attribute names, missing variables, wrong types) before <code>plan<\/code>.<\/li>\n<li>Cheap enough to run on every commit and pull request.<\/li>\n<li>Combined with <code>fmt<\/code>, it gives you a fast gate that keeps obviously broken code out of main.<\/li>\n<\/ul>\n<p>In CI, a very standard pattern is:<\/p>\n<pre><code class=\"language-bash\">terraform fmt -check -recursive\nterraform init -backend=false   # or with backend depending on your setup\nterraform validate<\/code><\/pre>\n<p>You can choose whether <code>init<\/code> uses the real backend or a local one; the key is that <code>validate<\/code> runs automatically.<\/p>\n<hr \/>\n<h2>Input Variable Validation: Types and Rules<\/h2>\n<p>Terraform also validates <strong>values<\/strong> going into your modules via input variables. There are three important layers.<\/p>\n<h3>1. Type Constraints<\/h3>\n<p>Every variable can and should declare a type: <code>string<\/code>, <code>number<\/code>, <code>bool<\/code>, complex types such as <code>list(string)<\/code> or <code>object({ ... })<\/code>. Terraform will reject values that do not conform.<\/p>\n<p>Example:<\/p>\n<pre><code class=\"language-terraform\">variable &quot;tags&quot; {\n  type = map(string)\n}<\/code><\/pre>\n<p>Passing a list here fails fast, long before any resource is created.<\/p>\n<h3>2. Required vs Optional<\/h3>\n<ul>\n<li>Variables <strong>without<\/strong> a <code>default<\/code> are required; if the caller does not supply a value, Terraform fails at validation time.<\/li>\n<li>Variables <strong>with<\/strong> a <code>default<\/code> are optional; they still participate in type and custom validation.<\/li>\n<\/ul>\n<p>This lets you express what callers must always provide versus what can be inferred or defaulted.<\/p>\n<h3>3. Custom <code>validation<\/code> Blocks<\/h3>\n<p>Inside each <code>variable<\/code> block you can define one or more <code>validation<\/code> blocks.<\/p>\n<p>Each block has:<\/p>\n<ul>\n<li><code>condition<\/code>: a boolean expression evaluated against the value.<\/li>\n<li><code>error_message<\/code>: a human\u2011readable message if the condition is false.<\/li>\n<\/ul>\n<p>Example patterns from common practice include:<\/p>\n<ul>\n<li>Membership checks with <code>contains<\/code> or regex.<\/li>\n<li>Ranges and integer checks for numbers.<\/li>\n<li>Multiple validation blocks to capture several independent rules.<\/li>\n<\/ul>\n<p>The rationale here is strong: you make invalid states <strong>unrepresentable<\/strong> at the module boundary, rather than having to handle them deep inside resource logic.<\/p>\n<hr \/>\n<h2>Beyond Variables: Preconditions and Postconditions<\/h2>\n<p>Terraform also lets you validate assumptions <em>around<\/em> resources and data sources using <code>precondition<\/code> and <code>postcondition<\/code> blocks.<\/p>\n<ul>\n<li>A <strong>precondition<\/strong> asserts something must be true before Terraform creates or updates the object (for example, an input computed from multiple variables is within bounds).<\/li>\n<li>A <strong>postcondition<\/strong> asserts something must be true after the resource or data source is applied (for example, an attribute returned by the provider matches expectations).<\/li>\n<\/ul>\n<p>Conceptually:<\/p>\n<ul>\n<li>Variable validation guards <strong>inputs to modules<\/strong>.<\/li>\n<li>Preconditions\/postconditions guard <strong>behaviour of resources and data sources<\/strong> exposed by those modules.<\/li>\n<\/ul>\n<p>For a team consuming your module, this is powerful: they get immediate, clear errors about violated invariants instead of mysterious provider failures later.<\/p>\n<hr \/>\n<h2>A Simple Example (Format + Validate + Variable Rules)<\/h2>\n<p>Below is a small, self\u2011contained configuration you can run locally to see formatting and validation in action.<\/p>\n<h3>Files<\/h3>\n<p>Create the files.<\/p>\n<p><code>variables.tf<\/code>:<\/p>\n<pre><code class=\"language-terraform\">variable &quot;environment&quot; {\n  description = &quot;Deployment environment.&quot;\n     type        = string\n\n  validation {\n    condition     = contains([&quot;dev&quot;, &quot;test&quot;, &quot;prod&quot;], var.environment)\n    error_message = &quot;Environment must be one of: dev, test, prod.&quot;\n  }\n}\n\nvariable &quot;app_name&quot; {\n  description = &quot;Short application name used in resource naming.&quot;\n  type        = string\n\n  validation {\n    condition     = can(regex(&quot;^[a-z0-9-]{3,20}$&quot;, var.app_name))\n    error_message = &quot;app_name must be 3-20 chars, lowercase letters, digits, and hyphens only.&quot;\n  }\n}\n\nvariable &quot;instance_count&quot; {\n  description = &quot;Number of instances to run.&quot;\n  type        = number\n\n  validation {\n    condition     = var.instance_count &gt;= 1 &amp;&amp; var.instance_count &lt;= 10\n    error_message = &quot;instance_count must be between 1 and 10.&quot;\n  }\n\n  validation {\n    condition     = !(var.environment == &quot;prod&quot; &amp;&amp; var.instance_count &lt; 3)\n    error_message = &quot;In prod, instance_count must be at least 3.&quot;\n  }\n}<\/code><\/pre>\n<p>This demonstrates:<\/p>\n<ul>\n<li>Type constraints on all inputs.<\/li>\n<li>A small \u201cenumeration\u201d for <code>environment<\/code>.<\/li>\n<li>A format rule enforced via regex on <code>app_name<\/code>.<\/li>\n<li>Multiple independent validation rules on <code>instance_count<\/code>, including one that depends on <code>environment<\/code>.<\/li>\n<\/ul>\n<p><code>main.tf<\/code>:<\/p>\n<pre><code class=\"language-terraform\">terraform {\n  required_version = &quot;&gt;= 1.5.0&quot;\n}\n\nlocals {\n  app_tag = &quot;${var.app_name}-${var.environment}&quot;\n}\n\noutput &quot;example_tags&quot; {\n  value = {\n    Environment = var.environment\n    App         = var.app_name\n    Count       = var.instance_count\n    AppTag      = local.app_tag\n  }\n}<\/code><\/pre>\n<h3>Step 1 \u2013 Format the Code<\/h3>\n<p>From inside the folder:<\/p>\n<pre><code class=\"language-terraform\">terraform fmt -recursive<\/code><\/pre>\n<p>Observe that Terraform will adjust spacing\/indentation if you intentionally misalign something and run it again. This confirms <code>fmt<\/code> is active and working.<\/p>\n<h3>Step 2 \u2013 Initialize<\/h3>\n<pre><code class=\"language-terraform\">terraform init<\/code><\/pre>\n<p>No providers are actually used here, but <code>validate<\/code> requires initialization.<\/p>\n<h3>Step 3 \u2013 Structural Validation<\/h3>\n<p>Run:<\/p>\n<pre><code class=\"language-bash\">terraform validate<\/code><\/pre>\n<p>This checks syntax, references, and type soundness of the configuration itself.<\/p>\n<p>If you see:<\/p>\n<pre><code class=\"language-bash\">Success! The configuration is valid.<\/code><\/pre>\n<p>you know the configuration is structurally sound.<\/p>\n<h3>Step 4 \u2013 Test Variable Validation with <code>plan<\/code> and <code>-var<\/code><\/h3>\n<p>To exercise your <strong>variable validation logic<\/strong> with specific values, use <code>terraform plan<\/code> with <code>-var<\/code> flags.<\/p>\n<ol>\n<li>\n<p>Valid input:<\/p>\n<pre><code class=\"language-bash\">terraform plan -var=\"environment=dev\" -var=\"app_name=demo-app\" -var=\"instance_count=2\"<\/code><\/pre>\n<ul>\n<li>Here <code>-var<\/code> is supported and your custom validation blocks are evaluated.<\/li>\n<li>This should succeed, producing a plan (no resources, but the important part is that there are no validation errors).<\/li>\n<\/ul>\n<\/li>\n<li>\n<p>Invalid environment:<\/p>\n<pre><code class=\"language-bash\">terraform plan -var=\"environment=stage\" -var=\"app_name=demo-app\" -var=\"instance_count=2\"<\/code><\/pre>\n<p>Expect Terraform to fail with the custom environment error message from the <code>validation<\/code> block.<\/p>\n<\/li>\n<li>\n<p>Invalid app name:<\/p>\n<pre><code class=\"language-bash\">terraform plan -var=\"environment=dev\" -var=\"app_name=Demo_App\" -var=\"instance_count=2\"<\/code><\/pre>\n<p>You should see the regex\u2011based <code>app_name<\/code> error.<\/p>\n<\/li>\n<li>\n<p>Invalid prod count:<\/p>\n<pre><code class=\"language-bash\">terraform plan -var=\"environment=prod\" -var=\"app_name=demo-app\" -var=\"instance_count=1\"<\/code><\/pre>\n<p>Here, the environment is valid and the type is correct, but the cross\u2011rule on <code>instance_count<\/code> fails with your custom prod message.<\/p>\n<\/li>\n<\/ol>\n<h2>Optional \u2013 Use <code>*.tfvars<\/code> Instead of <code>-var<\/code><\/h2>\n<p>If you prefer files over command\u2011line flags, create <code>dev.auto.tfvars<\/code>:<\/p>\n<pre><code class=\"language-bash\">environment    = &quot;dev&quot;\napp_name       = &quot;demo-app&quot;\ninstance_count = 2<\/code><\/pre>\n<p>Then just run:<\/p>\n<pre><code>terraform plan<\/code><\/pre>\n<p>Terraform will automatically load <code>*.auto.tfvars<\/code> files and apply the same variable validations.<\/p>\n<hr \/>\n<h2>Recommended Pattern for Teams<\/h2>\n<p>Updated to reflect current behaviour:<\/p>\n<ul>\n<li>Run <code>terraform fmt -check -recursive<\/code> and <code>terraform validate<\/code> in CI on every PR.<\/li>\n<li>Use <code>terraform plan<\/code> (with <code>-var<\/code> or <code>*.tfvars<\/code>) to exercise and gate variable validations for concrete environments (dev, test, prod).<\/li>\n<li>Enforce types and <code>validation<\/code> blocks on all externally visible variables, not just a handful.<\/li>\n<li>Use preconditions and postconditions where module consumers must rely on specific guarantees from your resources.<\/li>\n<\/ul>\n<p>From an engineering\u2011lead perspective, this gives you a clear division of responsibilities:<\/p>\n<ul>\n<li><code>fmt<\/code> \u2192 canonical style.<\/li>\n<li><code>validate<\/code> \u2192 structural soundness of the configuration.<\/li>\n<li><code>plan<\/code> (with variables) \u2192 semantic correctness of inputs and module contracts.<\/li>\n<\/ul>\n","protected":false},"excerpt":{"rendered":"<p>In Terraform projects, format and validation are your first line of defence against messy code and avoidable runtime errors. Think of them as \u201cstyle checking\u201d and \u201csanity checking\u201d for infrastructure as code. Why Format and Validate at All? Terraform configurations tend to grow into large, multi\u2011module codebases, often edited by several engineers at once. Without [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[94],"tags":[],"_links":{"self":[{"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/posts\/2131"}],"collection":[{"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=2131"}],"version-history":[{"count":1,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/posts\/2131\/revisions"}],"predecessor-version":[{"id":2132,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=\/wp\/v2\/posts\/2131\/revisions\/2132"}],"wp:attachment":[{"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=2131"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=2131"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.ronella.xyz\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=2131"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}