DevTools Hub

Search tools

Search for a developer tool

How to Validate a CloudFormation Template

Part of the AWS Toolkit

"Validate the template" can mean three different checks, and mixing them up is why a template that passes one validator still fails on deploy. There's syntax (is this parseable YAML or JSON at all), structure (does it match the documented CloudFormation template anatomy), and semantics (are the resource properties inside it actually valid for those specific resource types). Each layer catches mistakes the others miss entirely.

Layer one: syntax

Before CloudFormation — or anything else — can reason about a template, it has to parse as valid YAML or JSON. This is the shallowest check and the one most editors already catch: a missing colon, a bad indent level, an unclosed bracket. It says nothing about whether the content makes sense as a CloudFormation template, only that a parser can read it at all.

Layer two: structure

Structural validation checks the parsed document against the documented template anatomy: only the ten recognized top-level sections, a Type on every resource matching AWS::Service::ResourceType, and every Ref and Fn::GetAtt pointing at something that actually exists in the template. This is exactly what our own CloudFormation Validator checks — including resolving YAML shorthand (!Ref, !GetAtt, !Sub) to its full Fn:: form first. A template can be perfectly valid YAML and still fail here — a typo'd section name, a dangling Ref to a resource that got renamed, a DependsOn pointing at a logical ID that no longer exists.

Layer three: semantics — the level a schema can't express

Neither of the first two layers checks whether a resource's Properties are actually correct for its specific type. Each of the hundreds of AWS resource types has its own property schema — required fields, valid enum values, correct nesting — and checking that requires the type-by-type resource and property types reference. This is where the two real tools for this layer differ in an important way:

  • aws cloudformation validate-template calls the real CloudFormation API. It confirms the template parses as valid JSON or YAML and — usefully — returns the exact Capabilities (like CAPABILITY_IAM or CAPABILITY_NAMED_IAM) the template will require at deploy time, which is often the actual reason people run it in CI: catching a missing-capability deploy failure before it happens. What it does not do is check resource property correctness in any depth — it's a live syntax-and-structure check, not a property-level linter. It also needs real AWS credentials and network access, since it's an actual API call.
  • cfn-lint is the property-level linter. It runs entirely offline against a bundled copy of AWS's resource specification, checking hundreds of rules — required properties present, correct types, valid enum values, even some cross-resource logic — without needing AWS credentials at all. It's the modern standard: a VS Code extension, a pre-commit hook, and a CI step all exist for it, and it's the tool AWS itself points to for this layer.

The layer beyond static tools: change sets

Even syntax, structure, and property validation combined can't catch everything — account-specific limits, a resource name that's already taken, a circular dependency that only manifests at deploy time. The only way to see those is a real change set aws cloudformation deploy --no-execute-changeset or the equivalent console flow previews exactly what CloudFormation would create, update, or replace, without actually touching infrastructure. It costs an API round trip and real credentials, which is why it's usually the last stage of a validation pipeline rather than something run on every save.

Wiring it into CI

A practical pipeline runs the cheap, offline layers on every commit and saves the credentialed layers for a later stage: structural validation and cfn-lint first (fast, no AWS access needed, safe to run on every pull request), then validate-template and a change-set preview once credentials are available, typically gated to a deploy pipeline rather than every branch. Failing the build on cfn-lint's error-level findings while treating its style warnings as non-blocking keeps the fast layer useful without becoming noisy.

Common mistakes specific to this case

  • Validating before packaging. A template with local file references — CodeUri for a Lambda function, a nested stack's TemplateURL pointing at a local path — needs aws cloudformation package (or sam build/sam package for SAM templates) run first. Validating the unpackaged template either fails outright or silently skips checking the referenced content.
  • Assuming validate-template checks your permissions. The Capabilities it returns describe what the template requires, not whether the identity actually deploying it has those permissions. A missing capability and a missing permission produce different failures at completely different stages.
  • Only linting the top-level template. Nested stacks are separate templates with their own structure and properties — a cfn-lint run against the parent template alone won't descend into a nested stack's TemplateURL automatically in every configuration; point it at each template explicitly if the setup doesn't already handle that.
  • Treating static validation as a substitute for a change set. None of the three static layers know about your account's current state — an S3 bucket name that's already taken globally, a service quota already at its limit — only a change set actually asks CloudFormation what would happen.

Try it yourself

CloudFormation Validator covers layer two — paste a template and catch structural mistakes (missing sections, malformed resource types, dangling Refs) before they reach cfn-lint or a real deploy, entirely in your browser with no AWS credentials involved. If the template embeds an IAM policy you want checked separately, pull it out and paste it into IAM Policy Viewer for a statement-by-statement breakdown.

Related tools