AWS CloudFormation JSON Templates: Complete Reference and YAML Comparison
Last updated:
A CloudFormation template is a JSON or YAML document that declares a set of AWS resources, their properties, and the relationships between them — CloudFormation reads the template and creates, updates, or deletes the corresponding stack of resources. The JSON form is strict RFC 8259 JSON with a fixed set of top-level sections (AWSTemplateFormatVersion, Description,Metadata, Parameters, Mappings, Conditions, Resources, Outputs) and a defined grammar of intrinsic functions for dynamic values. JSON templates max out at 51,200 bytes inline and 460,800 bytes when uploaded to S3 — limits worth knowing before templates grow large. YAML is generally easier to write by hand because it allows comments and short-form intrinsics, but JSON remains the preferred format for tool-generated templates, templates embedded inside other JSON documents, and any pipeline that already parses JSON. Both formats are feature-equivalent.
Hand-editing a CloudFormation JSON template and the deploy keeps failing with "Template format error"? Paste it into Jsonic's JSON Validatorto pinpoint trailing commas, missing quotes, and bracket mismatches at the exact line — CloudFormation's parser does not show line numbers.
Validate CloudFormation JSONCloudFormation JSON vs YAML: when to choose which format
CloudFormation accepts both JSON and YAML — they parse to the same internal representation, support the same resource types, and produce identical stacks. The choice between them is about authoring ergonomics, not capability. YAML wins on line count (typically 30–40% fewer lines than equivalent JSON), supports # comments natively, and allows short-form intrinsics like !Ref and !GetAttthat JSON cannot express. JSON wins on tooling compatibility — it round-trips through any JSON parser without quirks, lacks YAML's whitespace sensitivity, and embeds cleanly inside other JSON documents.
Pick JSON when:the template is generated by a tool (CDK synth emits JSON by default, Former2 outputs JSON, Troposphere's default backend is JSON); the template needs to live inside another JSON document (a Step Functions state machine definition that references a stack template, a Service Catalog product JSON, a CodePipeline artifact); your team's tooling already parses JSON and adding a YAML dependency is unjustified.
Pick YAML when: the template is hand-written and read frequently by humans; you want comments to document why a particular resource configuration exists; you want short-form intrinsics (!Sub, !Ref, !If) to reduce visual noise; the template will be edited by people who find YAML's indentation rules more natural than JSON's brace nesting.
The conversion is mechanical — cfn-flip from AWS Labs handles both directions correctly, expanding short-form intrinsics into JSON-safe long form. See also our AWS JSON patterns guide for broader context on JSON across AWS services.
Template anatomy: AWSTemplateFormatVersion, Parameters, Resources, Outputs
A CloudFormation JSON template is an object with a fixed set of top-level keys. Only Resources is required; everything else is optional but conventional. The minimum useful template — one S3 bucket — looks like this:
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "Single S3 bucket with versioning enabled",
"Resources": {
"RawUploadsBucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"VersioningConfiguration": { "Status": "Enabled" },
"PublicAccessBlockConfiguration": {
"BlockPublicAcls": true,
"BlockPublicPolicy": true,
"IgnorePublicAcls": true,
"RestrictPublicBuckets": true
}
}
}
},
"Outputs": {
"BucketName": {
"Description": "Name of the raw uploads bucket",
"Value": { "Fn::Ref": "RawUploadsBucket" },
"Export": { "Name": "raw-uploads-bucket-name" }
}
}
}The eight top-level sections in order of appearance:
AWSTemplateFormatVersion— always"2010-09-09". Optional but recommended.Description— free-text describing the stack. Shown in the console.Metadata— arbitrary structured data. CloudFormation reads specific sub-keys (e.g.,AWS::CloudFormation::Interfacefor parameter grouping in the console) and ignores everything else.Parameters— deploy-time inputs with type and validation.Mappings— static lookup tables, typically used for region-specific AMI IDs.Conditions— boolean expressions controlling whether resources or outputs are created.Resources— the AWS resources to manage. The only required section.Outputs— values to surface after deploy, optionally exported for cross-stack imports.
Each resource needs three fields: a logical name (the key), a Type (e.g., AWS::S3::Bucket, AWS::Lambda::Function), and a Properties object whose shape is documented in the AWS Resource and Property Types Reference. Optional resource-level fields include DependsOn, Condition, DeletionPolicy, UpdateReplacePolicy, and Metadata.
Intrinsic functions in JSON: Fn::Ref, Fn::GetAtt, Fn::Sub, Fn::Join
Intrinsic functions are how a template references values that do not exist until deploy time — resource IDs, parameter values, computed attributes. In JSON each intrinsic is an object with a single Fn:: key. YAML supports short-form tags (!Ref, !GetAtt) that JSON cannot express.
| Function | JSON form | YAML short form | Returns |
|---|---|---|---|
Fn::Ref | { "Ref": "MyBucket" } | !Ref MyBucket | Default identifier (name, ID, URL) |
Fn::GetAtt | { "Fn::GetAtt": ["MyBucket", "Arn"] } | !GetAtt MyBucket.Arn | Named attribute (ARN, DNS, IP) |
Fn::Sub | { "Fn::Sub": "arn:aws:s3:::${MyBucket}" } | !Sub 'arn:aws:s3:::${MyBucket}' | String with interpolation |
Fn::Join | { "Fn::Join": [",", ["a", "b"]] } | !Join [',', [a, b]] | Concatenated string |
Fn::Select | { "Fn::Select": [0, ["a","b"]] } | !Select [0, [a, b]] | Element at index |
Fn::Split | { "Fn::Split": [",", "a,b"] } | !Split [',', 'a,b'] | String split into list |
Fn::FindInMap | { "Fn::FindInMap": ["RegionMap", { "Ref": "AWS::Region" }, "AMI"] } | !FindInMap [RegionMap, !Ref AWS::Region, AMI] | Mapping lookup |
Fn::ImportValue | { "Fn::ImportValue": "shared-vpc-id" } | !ImportValue shared-vpc-id | Cross-stack export value |
Fn::Sub is the workhorse for string assembly — it interpolates ${Resource} tokens against logical names, parameters, and the AWS pseudo-parameters (AWS::Region, AWS::AccountId, AWS::StackName). Preferring Fn::Sub over Fn::Join produces dramatically more readable templates:
// Verbose Fn::Join form
{ "Fn::Join": ["", [
"arn:aws:s3:::",
{ "Ref": "MyBucket" },
"/uploads/*"
]] }
// Cleaner Fn::Sub form — same result
{ "Fn::Sub": "arn:aws:s3:::${MyBucket}/uploads/*" }Intrinsic functions nest freely — Fn::Sub inside Fn::If inside Fn::Joinis fine — but JSON's verbosity makes deeply nested forms hard to read. That readability pressure is the strongest argument for YAML on hand-edited templates.
Conditions: Fn::If, Fn::And, Fn::Or, Fn::Equals
The Conditions section declares named boolean expressions evaluated at deploy time against parameter values. A condition can gate whether a resource is created (the resource-level Condition field), what value a property gets (Fn::If inside Properties), or whether an output is emitted (the output-level Condition field).
{
"Parameters": {
"Environment": {
"Type": "String",
"AllowedValues": ["dev", "staging", "prod"],
"Default": "dev"
}
},
"Conditions": {
"IsProd": {
"Fn::Equals": [{ "Ref": "Environment" }, "prod"]
},
"IsProdOrStaging": {
"Fn::Or": [
{ "Fn::Equals": [{ "Ref": "Environment" }, "prod"] },
{ "Fn::Equals": [{ "Ref": "Environment" }, "staging"] }
]
}
},
"Resources": {
"ProdOnlyBucket": {
"Type": "AWS::S3::Bucket",
"Condition": "IsProd",
"Properties": {}
},
"AppBucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"VersioningConfiguration": {
"Status": {
"Fn::If": ["IsProdOrStaging", "Enabled", "Suspended"]
}
}
}
}
}
}The four condition functions:
Fn::Equals— takes a two-element array; returns true if the values are equal.Fn::And— takes an array of 2–10 conditions; returns true if all are true.Fn::Or— takes an array of 2–10 conditions; returns true if any are true.Fn::Not— takes a single-element array containing one condition; returns the negation.Fn::If— takes[ConditionName, ValueIfTrue, ValueIfFalse]. Use the special pseudo-valueAWS::NoValueon either branch to omit a property entirely (useful when a property should not appear at all in some environments).
The AWS::NoValue trick: setting a property to { "Ref": "AWS::NoValue" } removes it from the resource definition entirely — different from setting it to null or an empty string. This is how you conditionally include or exclude a property without rewriting the resource twice.
Parameter types and AllowedValues constraints
Parameters declare deploy-time inputs. Each parameter has a Type (required) plus optional validation constraints. CloudFormation validates the value before resources are created — invalid input rejects the stack create or update before any AWS API calls fire.
Basic types: String, Number, CommaDelimitedList (string of comma-separated values), List<Number> (list of numbers). AWS-specific types tie the parameter to a real resource and provide a dropdown picker in the console: AWS::EC2::VPC::Id, AWS::EC2::Subnet::Id, AWS::EC2::KeyPair::KeyName, AWS::EC2::Image::Id, AWS::SSM::Parameter::Value<String> and many more.
{
"Parameters": {
"InstanceType": {
"Type": "String",
"Description": "EC2 instance class",
"AllowedValues": ["t3.micro", "t3.small", "t3.medium"],
"Default": "t3.micro"
},
"Port": {
"Type": "Number",
"Description": "TCP port for the listener",
"MinValue": 1024,
"MaxValue": 65535,
"Default": 8080
},
"DomainName": {
"Type": "String",
"AllowedPattern": "^[a-z0-9-]+\\.[a-z]{2,}$",
"ConstraintDescription": "Must be a lowercase domain like example.com"
},
"VpcId": {
"Type": "AWS::EC2::VPC::Id",
"Description": "Pick a VPC from the dropdown"
},
"DbPassword": {
"Type": "String",
"NoEcho": true,
"MinLength": 12,
"MaxLength": 64
}
}
}Constraint fields and which types they apply to:
| Constraint | Applies to | Notes |
|---|---|---|
AllowedValues | All types | Type must match — strings for String, numbers for Number |
AllowedPattern | String only | Regex anchored at both ends implicitly |
MinLength / MaxLength | String only | Silently ignored on Number types |
MinValue / MaxValue | Number only | Inclusive bounds |
NoEcho | String | Masks value in console and API responses |
ConstraintDescription | All types | Custom error message when validation fails |
Common pitfall: AllowedValues on a Number parameter must contain numeric literals, not strings — [1, 2, 3], never ["1", "2", "3"]. The latter silently fails to match anything because CloudFormation compares values after type coercion.
Mappings for region/environment lookups
Mappings declares static lookup tables. The classic use case is region-specific AMI IDs: the same template deploys to us-east-1, eu-west-1, and ap-southeast-1, picking the right AMI for each region without parameter input. Mappings is two levels deep: top-level map name, then a key, then key-value pairs.
{
"Mappings": {
"RegionAmiMap": {
"us-east-1": { "AMI": "ami-0abcd1234efgh5678" },
"us-west-2": { "AMI": "ami-0fedc8765hgfe4321" },
"eu-west-1": { "AMI": "ami-0aaaa1111bbbb2222" },
"ap-southeast-1": { "AMI": "ami-0cccc3333dddd4444" }
},
"EnvironmentConfig": {
"dev": { "InstanceType": "t3.micro", "MinSize": 1, "MaxSize": 2 },
"staging": { "InstanceType": "t3.small", "MinSize": 2, "MaxSize": 4 },
"prod": { "InstanceType": "t3.medium", "MinSize": 3, "MaxSize": 12 }
}
},
"Resources": {
"AppInstance": {
"Type": "AWS::EC2::Instance",
"Properties": {
"ImageId": {
"Fn::FindInMap": ["RegionAmiMap", { "Ref": "AWS::Region" }, "AMI"]
},
"InstanceType": {
"Fn::FindInMap": ["EnvironmentConfig", { "Ref": "Environment" }, "InstanceType"]
}
}
}
}
}Fn::FindInMap takes three arguments: the map name, the top-level key, and the secondary key. It returns the value at that path. Mappings cannot reference parameters or resources — they are evaluated at template-parse time and must contain only literal values.
When to use Mappings vs Parameters vs Conditions:
- Mappings: when the answer depends on a finite, known set of keys (region, environment, account) and the value is static across deployments.
- Parameters: when the value needs to vary per deploy (instance count, domain name, tag values) and the operator should pick at deploy time.
- Conditions + Fn::If: when the choice is between two values and the trigger is a boolean check on a parameter or pseudo-parameter.
For multi-region AMI selection in modern templates, prefer AWS::SSM::Parameter::Value<AWS::EC2::Image::Id> with the public AMI parameter store paths (e.g., /aws/service/ami-amazon-linux-latest/...) — AWS publishes and updates these centrally, so your template stays current without editing the Mappings table.
Resource creation order, DependsOn, and circular references
CloudFormation creates resources in parallel where possible, sequentially where required. The dependency graph is inferred from intrinsic functions: Fn::Ref and Fn::GetAtt between two resources create an implicit dependency, and CloudFormation orders the dependent resource after its referent. You almost never need to declare order manually — the engine figures it out.
When you need DependsOn: when a resource depends on another without referencing it via an intrinsic function. The most common case is IAM: an EC2 instance depends on an IAM role being attached and the instance profile being created, but the instance only references the profile name (a string), so CloudFormation does not see the dependency on the role itself. The fix:
{
"Resources": {
"AppRole": {
"Type": "AWS::IAM::Role",
"Properties": { /* ... */ }
},
"AppInstanceProfile": {
"Type": "AWS::IAM::InstanceProfile",
"Properties": {
"Roles": [{ "Ref": "AppRole" }]
}
},
"AppInstance": {
"Type": "AWS::EC2::Instance",
"DependsOn": ["AppInstanceProfile"],
"Properties": {
"IamInstanceProfile": { "Ref": "AppInstanceProfile" }
}
}
}
}Circular references happen when resource A references resource B and resource B references resource A. CloudFormation refuses to deploy with the error "Circular dependency between resources". The fix is almost always extracting one of the values into a parameter or breaking the cycle with a third resource that both depend on. A common circular case: a security group rule that references two security groups by ID — solve it by using a separate AWS::EC2::SecurityGroupIngress resource declared outside both groups.
Error messages worth memorizing:
Template format error: unsupported structure— your JSON has a typo in a top-level section name (e.g.,Resorucesinstead ofResources) or invalid nesting.Unresolved resource dependencies— an intrinsic function references a logical name that does not exist; usually a typo.Circular dependency between resources— two or more resources reference each other in a cycle. Extract values into parameters or break the cycle with a separate resource.No updates are to be performed— your update template is byte-identical to the deployed one. Add a change before deploying.
Migration paths: CDK, Terraform, SAM, and YAML conversion
CloudFormation JSON is rarely the long-term destination. As templates grow past a thousand lines or as teams want type-checked infrastructure code, several migration paths open up. Each has tradeoffs and none is universally right.
To YAML: the smallest step. Run cfn-flip template.json template.yaml and the result is a feature-equivalent template that is 30–40% shorter, supports comments, and uses short-form intrinsics. The deploy pipeline does not change — CloudFormation accepts both formats identically. This is the right move when the template is genuinely going to stay declarative and stack-shaped.
To AWS SAM: the natural step when the template is primarily Lambda functions, API Gateway routes, and DynamoDB tables. SAM is a CloudFormation extension — your existing CloudFormation resources keep working untouched, and SAM adds shortcuts like AWS::Serverless::Function that compile to multiple underlying resources. The migration is incremental: add Transform: AWS::Serverless-2016-10-31at the top, replace the most verbose Lambda function definitions with SAM's shorter form. See Lambda JSON in CFN for examples of both forms.
To CDK: the right step when the template has grown past 1,000 lines, when you need conditional logic that the Conditions section makes ugly, or when you want to reuse infrastructure patterns across stacks. CDK is a TypeScript or Python program that synthesizesCloudFormation JSON — so you keep all of CloudFormation's deployment guarantees while getting types, loops, imports, and the ability to emit multiple stacks from one program. The migration is one-way and non-trivial; budget significant time.
To Terraform:the right step when your infrastructure already spans multiple cloud providers, or when the team has more Terraform experience than CloudFormation experience. Terraform's JSON syntax (not the more common HCL) is instructive to study even if you do not migrate — see our Terraform JSON comparisonfor the structural differences. Terraform manages state separately from the cloud provider, which is the biggest operational difference from CloudFormation's service-managed state. Sister cloud comparisons are also worth reading: Azure ARM JSON comparison for the Microsoft equivalent, and Pulumi JSON comparison for the code-first multi-cloud option that competes with CDK.
Validation tooling worth installing: cfn-lint catches dozens of resource-property mismatches the AWS validator does not (wrong types in property values, invalid resource ARNs, deprecated property names). It runs in CI against any template format and is the single highest-value addition to a CloudFormation pipeline.
Side by side: same template in JSON and YAML
Reading the same template in both formats is the fastest way to internalize the JSON-to-YAML mapping. Below is a small but realistic template with a parameter, a condition, a mapping, two resources, and an output — first in JSON, then in YAML. Both deploy to identical stacks.
{
"AWSTemplateFormatVersion": "2010-09-09",
"Description": "App bucket with versioning controlled by environment",
"Parameters": {
"Environment": {
"Type": "String",
"AllowedValues": ["dev", "prod"],
"Default": "dev"
}
},
"Mappings": {
"EnvConfig": {
"dev": { "Lifecycle": 30 },
"prod": { "Lifecycle": 365 }
}
},
"Conditions": {
"IsProd": {
"Fn::Equals": [{ "Ref": "Environment" }, "prod"]
}
},
"Resources": {
"AppBucket": {
"Type": "AWS::S3::Bucket",
"Properties": {
"VersioningConfiguration": {
"Status": {
"Fn::If": ["IsProd", "Enabled", "Suspended"]
}
},
"LifecycleConfiguration": {
"Rules": [{
"Id": "ExpireOldVersions",
"Status": "Enabled",
"ExpirationInDays": {
"Fn::FindInMap": ["EnvConfig", { "Ref": "Environment" }, "Lifecycle"]
}
}]
}
}
}
},
"Outputs": {
"BucketArn": {
"Value": { "Fn::GetAtt": ["AppBucket", "Arn"] },
"Export": { "Name": { "Fn::Sub": "${AWS::StackName}-bucket-arn" } }
}
}
}The same template in YAML — note the comments, short-form intrinsics, and absence of braces and commas:
AWSTemplateFormatVersion: '2010-09-09'
Description: App bucket with versioning controlled by environment
Parameters:
Environment:
Type: String
AllowedValues: [dev, prod]
Default: dev
Mappings:
EnvConfig:
dev: { Lifecycle: 30 }
prod: { Lifecycle: 365 }
Conditions:
IsProd: !Equals [!Ref Environment, prod]
Resources:
AppBucket:
Type: AWS::S3::Bucket
Properties:
VersioningConfiguration:
Status: !If [IsProd, Enabled, Suspended]
LifecycleConfiguration:
Rules:
- Id: ExpireOldVersions
Status: Enabled
ExpirationInDays: !FindInMap [EnvConfig, !Ref Environment, Lifecycle]
Outputs:
BucketArn:
Value: !GetAtt AppBucket.Arn
Export:
Name: !Sub '${AWS::StackName}-bucket-arn'The YAML version is roughly 25 lines vs 45 for the JSON — a typical ratio. For hand-edited templates that count compounds quickly: a 500-line YAML template is a 900-line JSON template, and the JSON form gets harder to scan as nesting deepens.
Key terms
- Stack
- The deployed instance of a CloudFormation template — a named collection of AWS resources managed as a single unit. Creating, updating, or deleting the stack applies the template's declared state to the account and region.
- Logical name
- The key under
Resourcesthat identifies a resource within the template (e.g.,AppBucket). Used byFn::RefandFn::GetAtt. Distinct from the physical name AWS assigns at deploy time. - Intrinsic function
- A built-in function that CloudFormation evaluates at deploy time. In JSON each appears as an object with a single
Fn::key (e.g.,{ "Fn::GetAtt": ["MyBucket", "Arn"] }). YAML supports short-form tags (!GetAtt) that JSON cannot express. - Pseudo-parameter
- A built-in value CloudFormation provides at runtime without you declaring it. Common ones:
AWS::Region,AWS::AccountId,AWS::StackName,AWS::Partition,AWS::NoValue. - Nested stack
- A stack created as a resource inside another stack via
AWS::CloudFormation::Stack. Used to split large templates into manageable pieces and to share modules across stacks. - Cross-stack reference
- A value exported from one stack's
Outputssection and consumed by another stack viaFn::ImportValue. Cannot be modified while the importer still depends on it. - cfn-lint
- The community linter for CloudFormation templates. Catches property-level errors (wrong types, invalid ARNs, deprecated fields) that the AWS
validate-templateAPI does not. Runs against JSON and YAML alike.
Frequently asked questions
Should I use JSON or YAML for CloudFormation templates?
For new templates written by hand, YAML is almost always the better answer — it allows comments, supports the short-form intrinsic function syntax (!Ref, !GetAtt, !Sub), and is roughly 30% fewer lines than the equivalent JSON. Use JSON when the template is generated by a tool that emits JSON (CDK synth, Former2, Troposphere with the JSON backend, AWS Config rules), when you need to embed the template inside another JSON document (Step Functions, Service Catalog product definitions, ECS task definitions stored in Parameter Store), or when your team already has tooling that parses JSON and adding a YAML parser is unjustified. JSON also tends to be more reliable for programmatic generation because there is no whitespace sensitivity and the round-trip through JSON.parse and JSON.stringify is lossless. Both formats are 100% feature-equivalent — anything one supports the other does too — and AWS treats them identically once parsed.
What is the maximum template size in CloudFormation?
CloudFormation has two template size limits depending on how you submit the template. When you pass the template body inline (the TemplateBody parameter on CreateStack or UpdateStack), the limit is 51,200 bytes — about 50 KB. When you upload the template to S3 first and reference it via TemplateURL, the limit jumps to 460,800 bytes — about 450 KB. The S3 upload path is what the AWS console, AWS CLI, and most CI/CD tools use under the hood once a template exceeds the inline threshold. Both limits count the raw byte size after any tool transformations, so a YAML template that compiles to a 60 KB JSON intermediate counts as 60 KB. If your template still bumps against the 460 KB limit, the standard workarounds are nested stacks (split the template into child stacks referenced via AWS::CloudFormation::Stack resources) or moving to CDK, which can emit multiple stacks from one program.
Can I use comments in a CloudFormation JSON template?
No. CloudFormation JSON templates are parsed as strict JSON (RFC 8259), which does not allow // line comments, /* block comments */, or trailing commas. Adding any of those produces a template validation error at submission time: "Template format error: unsupported structure" or "JSON not well-formed". The standard workarounds are the Metadata section (a top-level key whose contents CloudFormation ignores beyond known sub-keys like AWS::CloudFormation::Interface) and an unused string field inside any resource property bag (e.g., "_comment": "this bucket holds raw uploads before processing"). Resource-level comments via Metadata work cleanly: { "Resources": { "MyBucket": { "Type": "AWS::S3::Bucket", "Metadata": { "Comment": "Stores raw user uploads with 30-day lifecycle" }, "Properties": { ... } } } }. If you want true comment support, switch to YAML — both # line comments and the more verbose template structure become much easier to maintain.
How do I convert a YAML CloudFormation template to JSON?
The cleanest path is the AWS CLI, which exposes a built-in converter via the cfn-flip companion tool. Install it with pip install cfn-flip, then run cfn-flip template.yaml template.json — it converts both directions and correctly expands short-form intrinsic functions like !Ref into their long-form Fn::Ref equivalents that JSON requires. The Python cfn-flip package handles edge cases the naive yaml-to-json approach gets wrong: short tag intrinsics, multi-line strings, and the !GetAtt dot-syntax that needs to become a two-element array in JSON. If you cannot install cfn-flip, AWS CloudFormation Designer in the console offers an interactive convert button, and the rain CLI from awslabs ships a similar transform. Avoid generic yaml-to-json converters — they leave !Ref tags in place, producing templates CloudFormation refuses to parse.
What's the difference between !Ref and !GetAtt?
Fn::Ref (or !Ref in YAML) returns the default identifier of a resource — what AWS considers the "primary" value. For most resources that means the physical name or ID: !Ref MyBucket returns the bucket name, !Ref MyQueue returns the queue URL, !Ref MyParameter returns the parameter value at deploy time. Fn::GetAtt (or !GetAtt) returns a specific named attribute and you must pick which one — !GetAtt MyBucket.Arn returns the bucket ARN, !GetAtt MyQueue.QueueArn returns the queue ARN, !GetAtt MyInstance.PublicDnsName returns the public DNS name of an EC2 instance. The rule of thumb: use Ref when you want the resource's identity for cross-referencing inside CloudFormation; use GetAtt when you need a specific computed attribute (ARN, endpoint, DNS name, IP address) that another resource's property requires. The list of attributes each resource type exposes is documented in the AWS Resource and Property Types Reference.
How do I reference an existing resource in CloudFormation?
There are three patterns depending on what you mean by "existing". For a resource in the same stack, use Fn::Ref or Fn::GetAtt with the logical name — CloudFormation resolves these during deployment ordering. For a resource in a different stack you own, the recommended pattern is cross-stack references via Outputs and Fn::ImportValue: the producer stack exports a value with Outputs.MyValue.Export.Name, and the consumer uses { "Fn::ImportValue": "shared-vpc-id" }. Cross-stack imports cannot be changed while the importer still depends on them. For a resource that already exists outside CloudFormation, you have two options: pass the ID as a Parameter (simple but means CloudFormation does not manage it), or import the resource into the stack using the resource import feature, which gives CloudFormation full management of pre-existing infrastructure. As of 2026 most resource types support import, though the resource definition in the template must match the existing resource exactly or import fails.
Why does my parameter constraint not validate?
Parameter constraints in CloudFormation only fire when the parameter value is supplied at deploy time — they are not enforced if the parameter has a Default and you do not pass a value, and they are not enforced if a Mapping or Fn::FindInMap supplies the value internally. Three common failure modes: (1) AllowedValues with the wrong type — if the parameter Type is Number but AllowedValues is ["1", "2", "3"] (strings in JSON), AllowedValues silently fails to match anything; quote-strip to [1, 2, 3]. (2) AllowedPattern regex anchored incorrectly — CloudFormation evaluates the pattern as if anchored at both ends, so ^[a-z]+$ is implicit; [a-z]+ alone matches any string containing a lowercase letter, which is rarely what you want. (3) MinLength and MaxLength on a Type: Number parameter — those constraints only apply to String types and CloudFormation silently ignores them on numeric parameters. Use MinValue and MaxValue instead.
How do I avoid the 51,200 byte limit when templates get big?
Four practical paths once your template exceeds 51,200 bytes inline. First, upload to S3 — the same template referenced via TemplateURL gets a 460,800 byte limit (about 450 KB), enough for most monolithic templates. The AWS CLI does this automatically for any template larger than 51 KB. Second, split into nested stacks: extract logical groupings (networking, IAM, application tier) into child templates referenced as AWS::CloudFormation::Stack resources; each child counts against its own size budget. Third, use cross-stack references via Outputs and Fn::ImportValue to share values between independently deployed stacks — cleaner than nesting when teams own different pieces. Fourth, migrate to CDK, which compiles a TypeScript or Python program into multiple stacks and handles the size split automatically; a single CDK app routinely emits 10+ stacks that together would exceed any single template limit. CDK is the recommended path for templates that have grown past 1,000 lines.
Further reading and primary sources
- AWS — Template Anatomy — Authoritative reference for every top-level section and resource field
- AWS — Intrinsic Function Reference — Full reference for Fn::Ref, Fn::GetAtt, Fn::Sub, conditions, and the rest
- AWS — Resource and Property Types Reference — Every AWS::* resource type with its Properties schema and Fn::GetAtt attributes
- AWS — CloudFormation Limits — Authoritative size, count, and quota limits including the 51,200 byte template body limit
- AWS Labs — cfn-lint — Community linter that catches resource-property errors the AWS validator misses