What is AWS CloudFormation? Templates, Stacks and Change Sets Explained, Template Anatomy Section by Section, Create Your First Stack Step by Step, Update With a Change Set, Drift Detection, Nested Stacks and StackSets, Quotas, Pricing, CLI, and CloudFormation vs Terraform


Most of this blog uses Terraform to build AWS infrastructure, so people ask me regularly - do I still need to know CloudFormation? The answer is yes, for three reasons - it is the native infrastructure-as-code service of AWS and shows up everywhere (Control Tower, Service Catalog, SAM, CDK, the "Launch Stack" buttons in every AWS blog), the AWS certification exams assume you know it, and understanding how it works makes you better at Terraform too, because both solve the same problem in slightly different ways.

So this post is CloudFormation from zero - what it is, how a template becomes a stack, every section of a template with a complete working example, your first stack in the console and the CLI, safe updates with change sets, drift, StackSets, costs, quotas, troubleshooting, and a fair comparison with Terraform at the end. Everything follows the current CloudFormation User Guide.

Table of Content

  1. What CloudFormation is
  2. The three concepts - template, stack, change set
  3. How a template becomes a stack
  4. Template anatomy - every section explained
  5. A complete working template - security group and EC2 instance
  6. Intrinsic functions and pseudo parameters
  7. Step 1 - Create your first stack in the console
  8. Step 2 - Update the stack with a change set
  9. Step 3 - Detect drift
  10. Step 4 - Delete the stack
  11. Nested stacks, StackSets, IaC generator, Template studio
  12. Quotas and pricing
  13. The AWS CLI equivalents
  14. Troubleshooting - ROLLBACK_COMPLETE and friends
  15. CloudFormation vs Terraform
  16. Conclusion



1. What CloudFormation is

AWS CloudFormation is the AWS service for infrastructure as code - you write a text file (template) that describes the AWS resources you want and their properties, hand it to CloudFormation, and the service creates, updates and deletes those resources for you, in the right order, as one unit (stack). Instead of clicking through the EC2, VPC and RDS consoles and remembering what depends on what, you describe the end state and let the service work out the API calls.

Why that matters, in the words of the docs and in mine -

  1. Simplify infrastructure management - an Auto Scaling group, a load balancer and a database become one stack you create and delete together.
  2. Replicate quickly - the same template deploys the same environment in another Region or account, identically.
  3. Track and control changes - the template is text, so it lives in Git; every change is a diff, a review and a rollback point.

CloudFormation itself is free for AWS resource types - you pay only for the resources it creates (section 12).

AWS CloudFormation - a template is validated, turned into a dependency graph, provisioned in parallel and becomes a stack; updates go through change sets


2. The three concepts - template, stack, change set

ConceptWhat it is
Templatea YAML or JSON text file describing resources and their properties. Any extension - .yaml, .json, .template. YAML is what everyone writes today because it allows comments and the short !Ref syntax.
Stackthe running collection of resources created from one template. Create, update and delete happen at the stack level. A stack has a name, a status (CREATE_COMPLETE...), parameters, outputs and events.
Change seta preview of an update - CloudFormation compares the new template or parameters with the current stack and lists what it would add, modify, remove, and - crucially - replace. You review it, then execute it or throw it away.

The change set is the feature that makes CloudFormation safe in production. The docs give the classic example - rename an RDS instance in the template and CloudFormation creates a new database and deletes the old one; the change set shows Replacement: True before anything happens, so you can plan the backup first.


3. How a template becomes a stack

From how CloudFormation works -

  1. You write the template and save it locally or in S3. (If you upload a local file, CloudFormation puts it in an S3 bucket it creates in your account, one per Region - which is why a cf-templates-... bucket appears after your first stack.)
  2. You create a stack, pointing at the template and supplying parameter values.
  3. CloudFormation runs pre-deployment validation - syntax, property names, resource name conflicts. Fail here and nothing is created.
  4. It builds the dependency graph from Ref, Fn::GetAtt and DependsOn - the security group is created before the instance that references it.
  5. It makes the service API calls in your account with your permissions (or a stack service role you pass) - independent resources in parallel, dependent ones in order.
  6. It waits for each resource to stabilise - a database or a NAT gateway takes minutes, which is where most deployment time goes. Express mode returns as soon as the configuration is applied, for faster development loops.
  7. All good → CREATE_COMPLETE. Any resource fails → by default CloudFormation rolls back and deletes everything it created, leaving the stack in ROLLBACK_COMPLETE. You can instead choose preserve successfully provisioned resources when creating the stack, so a failure leaves the good resources in place for you to fix and retry.

Updates follow the same path through a change set; a failed update rolls back to the last known good state.


4. Template anatomy - every section explained

From template sections. Only Resources is required -

SectionRequiredPurpose
AWSTemplateFormatVersionnoalways 2010-09-09 - the only valid value
Descriptionnoup to 1,024 bytes of text shown in the console
Metadatanoarbitrary data; AWS::CloudFormation::Interface groups and orders parameters in the console form
Parametersnoinputs supplied at create or update time - instance type, environment name, CIDR. Up to 200. Types include String, Number, List<...>, AWS::EC2::KeyPair::KeyName, AWS::SSM::Parameter::Value<...> which pulls the value from Parameter Store
Rulesnovalidate parameter combinations before anything is created
Mappingsnolookup tables - RegionMap → eu-central-1 → AMI - read with Fn::FindInMap. Up to 200
Conditionsnobooleans built from parameters - IsProd: !Equals [!Ref Env, prod] - used to create a resource or set a property only sometimes
Transformnomacros - AWS::Serverless-2016-10-31 turns SAM shorthand into full resources; AWS::Include pulls in snippets from S3
Resourcesyesthe resources - each with a logical ID (your name), a Type (AWS::EC2::Instance) and Properties. Up to 500
Outputsnovalues to show after creation and to export to other stacks (Fn::ImportValue). Up to 200

The resource Type namespace tells you who provides it - AWS::* are AWS services (free), Custom::* is your own Lambda-backed resource, and third-party types from the CloudFormation registry (MongoDB::Atlas::Cluster, Datadog::Monitors::Monitor...) are billed per operation.


5. A complete working template - security group and EC2 instance

This template builds what we did by hand in Part-4 and Part-16 - a security group and an instance running nginx - and uses every common section. Save it as web-tier.yaml -

  1AWSTemplateFormatVersion: "2010-09-09"
  2Description: >
  3  jhooq.com - web tier: one EC2 instance with nginx behind a security group.
  4  The AMI comes from the public Systems Manager parameter so the template never goes stale.
  5
  6Metadata:
  7  AWS::CloudFormation::Interface:
  8    ParameterGroups:
  9      - Label: { default: "Network" }
 10        Parameters: [VpcId, SubnetId, AllowedSshCidr]
 11      - Label: { default: "Instance" }
 12        Parameters: [InstanceType, KeyName, Environment]
 13
 14Parameters:
 15  VpcId:
 16    Type: AWS::EC2::VPC::Id
 17    Description: VPC for the security group
 18  SubnetId:
 19    Type: AWS::EC2::Subnet::Id
 20    Description: Public subnet for the instance
 21  AllowedSshCidr:
 22    Type: String
 23    Default: 0.0.0.0/0
 24    AllowedPattern: ^(\d{1,3}\.){3}\d{1,3}/\d{1,2}$
 25    ConstraintDescription: must be a CIDR like 203.0.113.5/32
 26  InstanceType:
 27    Type: String
 28    Default: t3.micro
 29    AllowedValues: [t3.micro, t3.small, t3.medium]
 30  KeyName:
 31    Type: AWS::EC2::KeyPair::KeyName
 32    Description: Existing key pair for SSH
 33  Environment:
 34    Type: String
 35    Default: dev
 36    AllowedValues: [dev, prod]
 37  LatestAmiId:
 38    Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
 39    Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64
 40
 41Mappings:
 42  EnvConfig:
 43    dev:
 44      VolumeSize: 8
 45      Monitoring: false
 46    prod:
 47      VolumeSize: 20
 48      Monitoring: true
 49
 50Conditions:
 51  IsProd: !Equals [!Ref Environment, prod]
 52
 53Resources:
 54  WebSecurityGroup:
 55    Type: AWS::EC2::SecurityGroup
 56    Properties:
 57      GroupDescription: !Sub "web-sg for ${AWS::StackName}"
 58      VpcId: !Ref VpcId
 59      SecurityGroupIngress:
 60        - IpProtocol: tcp
 61          FromPort: 80
 62          ToPort: 80
 63          CidrIp: 0.0.0.0/0
 64          Description: HTTP from anywhere
 65        - IpProtocol: tcp
 66          FromPort: 22
 67          ToPort: 22
 68          CidrIp: !Ref AllowedSshCidr
 69          Description: SSH from the allowed range
 70      Tags:
 71        - Key: Name
 72          Value: !Sub "${AWS::StackName}-web-sg"
 73
 74  WebInstance:
 75    Type: AWS::EC2::Instance
 76    Properties:
 77      ImageId: !Ref LatestAmiId
 78      InstanceType: !Ref InstanceType
 79      KeyName: !Ref KeyName
 80      SubnetId: !Ref SubnetId
 81      SecurityGroupIds:
 82        - !Ref WebSecurityGroup
 83      Monitoring: !FindInMap [EnvConfig, !Ref Environment, Monitoring]
 84      BlockDeviceMappings:
 85        - DeviceName: /dev/xvda
 86          Ebs:
 87            VolumeType: gp3
 88            VolumeSize: !FindInMap [EnvConfig, !Ref Environment, VolumeSize]
 89            Encrypted: true
 90            DeleteOnTermination: true
 91      UserData:
 92        Fn::Base64: !Sub |
 93          #!/bin/bash
 94          dnf install -y nginx
 95          systemctl enable --now nginx
 96          echo "<h1>${AWS::StackName} - ${Environment} - $(hostname -f)</h1>" > /usr/share/nginx/html/index.html
 97      Tags:
 98        - Key: Name
 99          Value: !Sub "${AWS::StackName}-web"
100        - Key: Environment
101          Value: !Ref Environment
102
103  WebElasticIp:
104    Type: AWS::EC2::EIP
105    Condition: IsProd
106    Properties:
107      InstanceId: !Ref WebInstance
108      Tags:
109        - Key: Name
110          Value: !Sub "${AWS::StackName}-eip"
111
112Outputs:
113  InstanceId:
114    Description: The instance id
115    Value: !Ref WebInstance
116  PublicIp:
117    Description: Public IP of the web server
118    Value: !GetAtt WebInstance.PublicIp
119  WebUrl:
120    Description: Open this in a browser
121    Value: !Sub "http://${WebInstance.PublicDnsName}"
122  SecurityGroupId:
123    Description: Exported for other stacks
124    Value: !Ref WebSecurityGroup
125    Export:
126      Name: !Sub "${AWS::StackName}-WebSecurityGroupId"

Notice what the template does that clicking cannot - the AMI resolves from Parameter Store at deploy time, the prod environment gets a bigger disk, detailed monitoring and an Elastic IP purely from the Environment parameter, and the security group id is exported so another stack can !ImportValue web-tier-dev-WebSecurityGroupId. Every property name comes from the AWS::EC2::Instance and AWS::EC2::SecurityGroup reference pages - the resource type reference is the page you will have open permanently.



6. Intrinsic functions and pseudo parameters

The !Something tags are intrinsic functions - the small built-in language of templates. From the function reference, the ones you use daily -

FunctionDoesExample
!Refthe value of a parameter, or the primary identifier of a resource (instance id, security group id, bucket name)!Ref WebSecurityGroup → sg-0abc...
!GetAttanother attribute of a resource!GetAtt WebInstance.PublicIp
!Substring with ${...} substitutions of parameters, resources, attributes and pseudo parameters!Sub "${AWS::StackName}-web"
!Joinjoin a list with a delimiter!Join [",", [a, b]]
!Select / !Splitpick an item from a list / split a string!Select [0, !GetAZs ""]
!FindInMapread the Mappings table!FindInMap [EnvConfig, !Ref Environment, VolumeSize]
!If, !Equals, !And, !Or, !Notconditions!If [IsProd, 20, 8]
!GetAZsthe Availability Zones of a Region!GetAZs eu-central-1
!ImportValuean Export from another stack in the same Region!ImportValue web-tier-dev-WebSecurityGroupId
!Base64base64-encode - required for UserDataFn::Base64: !Sub ...
Fn::ForEachloop - repeat a resource for each item of a list (needs Transform: AWS::LanguageExtensions)one subnet per AZ

Pseudo parameters are values CloudFormation knows without you declaring them - AWS::StackName, AWS::Region, AWS::AccountId, AWS::Partition, AWS::URLSuffix, AWS::NoValue (removes a property when used in !If), AWS::NotificationARNs. The pseudo parameter reference has the full list.

One YAML gotcha - you cannot nest two short-form tags on one line (!Base64 !Sub fails). Use the long form for the outer one, as the template above does with Fn::Base64: !Sub |.


7. Step 1 - Create your first stack in the console

Following creating your first stack with our template -

  1. CloudFormation console → Stacks → Create stack → With new resources (standard).
  2. Prepare template - Choose an existing template. Template source - Upload a template file → web-tier.yaml. (The other options are an S3 URL, a Git repository via Git sync, or Build with Template studio - the visual editor, section 11.) Next.
  3. Specify stack details - Stack name web-tier-dev. The parameters appear in the groups and order the Metadata section defined - pick the VPC and public subnet from Part-5, your key pair, AllowedSshCidr as your IP /32, Environment dev. Next.
  4. Configure stack options - Tags Project = jhooq. Permissions - leave empty to run with your own permissions, or pick a service role that CloudFormation assumes instead (the production pattern - developers deploy stacks without holding the permissions themselves). Stack failure options - Roll back all stack resources (default) or Preserve successfully provisioned resources. Advanced - Termination protection on for anything you care about, an SNS topic for notifications, a timeout in minutes. Next.
  5. Review and create - the capabilities acknowledgement appears only if the template creates IAM resources (CAPABILITY_IAM) or uses macros (CAPABILITY_AUTO_EXPAND). Submit.
  6. The Events tab fills top-down - CREATE_IN_PROGRESS for the stack, then WebSecurityGroup, then WebInstance (it waited for the group), then CREATE_COMPLETE. Two to three minutes.
  7. Outputs tab → click WebUrl - the nginx page with the stack name.

Also look at the Resources tab (logical ID → physical ID mapping) and the Template tab (which now offers Open in Template studio, section 11).


8. Step 2 - Update the stack with a change set

Let's make two changes - InstanceType to t3.small and Environment to prod - and see what the change set says before we commit. From update stacks using change sets -

  1. Stacks → web-tier-dev → Stack actions → Create change set for current stack.
  2. Use existing template (we are changing only parameters; Replace existing template is for template edits). Next.
  3. Parameters - InstanceType t3.small, Environment prod. Next → Next → Change set name bigger-and-prod → Submit.
  4. CloudFormation computes the diff and shows Changes -
Logical IDActionReplacementWhy
WebInstanceModifyConditional / TrueInstanceType is an update with some interruption property (stop-start); the block device mapping size change forces replacement of an EC2 instance
WebElasticIpAdd-the IsProd condition is now true
WebInstance tagsModifyFalseEnvironment tag value
  1. Replacement True means a new instance is created and the old one deleted - the public IP changes and anything on the root disk is gone. That is the information you came for. Decide - execute (Execute change set, choosing the failure behaviour), or delete the change set and change the template so that the disk size is not touched.
  2. Execute → the Events tab shows UPDATE_IN_PROGRESS → UPDATE_COMPLETE_CLEANUP_IN_PROGRESS (deleting the replaced instance) → UPDATE_COMPLETE.

The update behaviours - No interruption, Some interruption, Replacement - are documented per property on each resource reference page, and the change set tells you which applies. A change set does not guarantee success - quotas, permissions and unsupported updates still fail at execution, with rollback to the last good state.

To protect a critical resource from accidental replacement or deletion, add a stack policy (Deny Update:Replace on the logical ID) and a DeletionPolicy: Retain or Snapshot on the resource - see protect stack resources.


9. Step 3 - Detect drift

Someone adds a rule to WebSecurityGroup by hand in the EC2 console. The stack still says UPDATE_COMPLETE, but reality no longer matches the template - that is drift. From detect drift -

  1. Stacks → web-tier-dev → Stack actions → Detect drift. Wait for the drift status.
  2. Stack actions → View drift results - the stack is DRIFTED, WebSecurityGroup is MODIFIED, and the diff view shows the expected property values from the template next to the actual values, with the extra ingress rule highlighted.
  3. Fix it one of two ways - remove the manual change in the EC2 console, or put the rule in the template and update the stack so the template becomes the truth again.

Drift detection is on-demand (or scheduled with an EventBridge rule plus a small Lambda), works for most but not all resource types, and is the thing that makes "nobody changes production by hand" enforceable. CloudFormation never fixes drift by itself - an update that does not touch the drifted property leaves it alone.


10. Step 4 - Delete the stack

Stacks → web-tier-dev → Delete. CloudFormation deletes resources in reverse dependency order - instance, then Elastic IP, then security group - and the stack disappears when DELETE_COMPLETE. Two things that stop a delete -

  1. Termination protection - turn it off under Stack actions → Edit termination protection first; it exists exactly to make you do that consciously.
  2. Resources with DeletionPolicy: Retain are left behind on purpose (the stack still deletes). An S3 bucket that is not empty, or a security group still used by something outside the stack, fails with DELETE_FAILED - fix the dependency and retry, or retain that resource.

11. Nested stacks, StackSets, IaC generator, Template studio

Once the basics work, four features carry you to real scale -

  1. Nested stacks - a stack as a resource (AWS::CloudFormation::Stack pointing at a template in S3). The way to get past the 500-resource limit and to reuse a "network module" or "database module" across many parent stacks. The parent's change set shows nested changes too.
  2. StackSets - one template deployed to many accounts and Regions from an administrator account, with automatic deployment to new accounts in an Organizations OU. This is how Control Tower and most security baselines (GuardDuty, Config rules, IAM roles) are rolled out.
  3. IaC generator - scans the resources that already exist in your account, lets you pick a set, and generates a template for them, which you can then import into a stack. The answer to "we built it by hand, now we want it in code".
  4. Template studio - the editor in the CloudFormation console that shows the template text next to a live diagram of the resources and their references (Ref, GetAtt, Sub, DependsOn), validates it, converts YAML ↔ JSON and saves to S3. The text is authoritative - you edit the template, the diagram redraws. Open it from Create stack → Build with Template studio or from any stack's template.

And three honourable mentions - Git sync (a stack that deploys itself from a branch in GitHub, GitLab or CodeCommit), the CloudFormation registry (third-party and private resource types, modules, and Hooks that block non-compliant resources before creation), and cfn-lint, the open-source linter that catches invalid property names and bad !Refs before you ever create a stack - github.com/aws-cloudformation/cfn-lint.


12. Quotas and pricing

From the quotas page -

QuotaValueWay around it
Resources per template500nested stacks
Parameters / Mappings / Outputs per template200 eachlists in parameters, nested stacks
Parameter value size4,096 bytessplit and Fn::Join
Template body in a request51,200 bytesupload to S3 (then 1 MB)
Stacks per account per Region2,000 (adjustable)delete what you do not need
StackSets per administrator account1,000 (adjustable)
Stack instances per StackSet100,000 (adjustable)
Resources a nested-stack hierarchy may touch in one operation2,500split the hierarchy
Dynamic references per template60
Custom resource response, cfn-signal data4,096 bytesput the payload in S3

Pricing, from the pricing page - no charge for stacks that use AWS::* (and Alexa::*) resource types; you pay only for the resources. Third-party registry resource types and Hooks are billed per handler operation (create, update, delete, read, list) - the published examples work with $0.0009 per operation after 1,000 free operations a month, and the first 30 seconds of each operation's duration are free, beyond that $0.00008 per second. For the templates in this post the CloudFormation line on your bill is zero.



13. The AWS CLI equivalents

 1STACK=web-tier-dev
 2
 3# lint before you deploy (pip install cfn-lint)
 4cfn-lint web-tier.yaml
 5
 6# validate with the service
 7aws cloudformation validate-template --template-body file://web-tier.yaml
 8
 9# create the stack and wait
10aws cloudformation create-stack --stack-name $STACK \
11  --template-body file://web-tier.yaml \
12  --parameters ParameterKey=VpcId,ParameterValue=vpc-0123456789abcdef0 \
13               ParameterKey=SubnetId,ParameterValue=subnet-0123456789abcdef0 \
14               ParameterKey=KeyName,ParameterValue=web-key \
15               ParameterKey=AllowedSshCidr,ParameterValue=203.0.113.5/32 \
16  --tags Key=Project,Value=jhooq \
17  --enable-termination-protection
18aws cloudformation wait stack-create-complete --stack-name $STACK
19
20# outputs and events
21aws cloudformation describe-stacks --stack-name $STACK --query "Stacks[0].Outputs" --output table
22aws cloudformation describe-stack-events --stack-name $STACK \
23  --query "StackEvents[?ResourceStatus=='CREATE_FAILED'].[LogicalResourceId,ResourceStatusReason]" --output table
24
25# update through a change set - create, inspect, execute
26aws cloudformation create-change-set --stack-name $STACK --change-set-name bigger-and-prod \
27  --use-previous-template \
28  --parameters ParameterKey=VpcId,UsePreviousValue=true ParameterKey=SubnetId,UsePreviousValue=true \
29               ParameterKey=KeyName,UsePreviousValue=true ParameterKey=AllowedSshCidr,UsePreviousValue=true \
30               ParameterKey=InstanceType,ParameterValue=t3.small ParameterKey=Environment,ParameterValue=prod
31aws cloudformation wait change-set-create-complete --stack-name $STACK --change-set-name bigger-and-prod
32aws cloudformation describe-change-set --stack-name $STACK --change-set-name bigger-and-prod \
33  --query "Changes[].ResourceChange.{id:LogicalResourceId,action:Action,replacement:Replacement}" --output table
34aws cloudformation execute-change-set --stack-name $STACK --change-set-name bigger-and-prod
35aws cloudformation wait stack-update-complete --stack-name $STACK
36
37# the one-command alternative used in CI - creates or updates, via a change set under the hood
38aws cloudformation deploy --stack-name $STACK --template-file web-tier.yaml \
39  --parameter-overrides InstanceType=t3.small Environment=prod \
40  --no-fail-on-empty-changeset
41
42# drift
43DRIFT=$(aws cloudformation detect-stack-drift --stack-name $STACK --query StackDriftDetectionId --output text)
44aws cloudformation describe-stack-drift-detection-status --stack-drift-detection-id $DRIFT
45aws cloudformation describe-stack-resource-drifts --stack-name $STACK \
46  --query "StackResourceDrifts[?StackResourceDriftStatus!='IN_SYNC'].[LogicalResourceId,StackResourceDriftStatus]" --output table
47
48# delete
49aws cloudformation update-termination-protection --stack-name $STACK --no-enable-termination-protection
50aws cloudformation delete-stack --stack-name $STACK
51aws cloudformation wait stack-delete-complete --stack-name $STACK

aws cloudformation deploy is the command most pipelines use - idempotent, change-set based, and it uploads large templates to S3 for you with --s3-bucket.


14. Troubleshooting - ROLLBACK_COMPLETE and friends

  1. ROLLBACK_COMPLETE after the first create - creation failed and everything was rolled back. The stack is now useless and cannot be updated - find the first CREATE_FAILED event, fix the template, delete the stack, create again. (Or create with Preserve successfully provisioned resources to keep the good parts next time.)
  2. CREATE_FAILED - "The following resource(s) failed to create" - the reason is in the Events tab on the first failed resource; later failures are usually just cancellations. Typical causes - a wrong property, a quota (VcpuLimitExceeded), a missing permission, an AMI that does not exist in the Region.
  3. UPDATE_ROLLBACK_FAILED - the rollback itself failed, usually because someone changed a resource by hand. Use Stack actions → Continue update rollback, skipping the resources that cannot be rolled back, then fix them.
  4. "Template format error: Unresolved resource dependencies" - a !Ref to a logical ID or parameter that does not exist. A typo, or a resource behind a Condition that is false.
  5. "Requires capabilities: [CAPABILITY_IAM]" - the template creates IAM resources; acknowledge in the console or pass --capabilities CAPABILITY_IAM (CAPABILITY_NAMED_IAM if they have fixed names).
  6. Stack stuck in *_IN_PROGRESS for an hour - a resource is waiting for a signal (cfn-signal, WaitCondition) that never comes, or a resource is slow to stabilise. Check the resource's own service console.
  7. Change set shows Replacement True unexpectedly - the property you changed is Replacement-type for that resource (names, AZs, most immutable identifiers). Check the property's update behaviour in the reference page before executing.
  8. Export cannot be deleted - "in use by stack X" - another stack imports it. Remove the import first.
  9. Delete fails on an S3 bucket - buckets must be empty; empty it or set DeletionPolicy: Retain.
  10. Cannot update a parameter with NoEcho - you must pass it again; UsePreviousValue=true works for all parameters including secrets. Better - read secrets with a dynamic reference {{resolve:secretsmanager:...}} so they never live in parameters.

15. CloudFormation vs Terraform

An honest comparison, since this blog is mostly Terraform -

CloudFormationTerraform
CloudsAWS only (plus registry extensions)AWS, Azure, Google Cloud, Kubernetes, GitHub, 4,000+ providers
LanguageYAML or JSON templates; CDK compiles TypeScript, Python, Java and others into templatesHCL, with real expressions, loops, modules
Statemanaged by AWS inside the stack - nothing to store, lock or losea state file you manage - S3 plus locking, or HCP Terraform
Previewchange setterraform plan - more detailed, shows every attribute
Rollbackautomatic on failurenone - a failed apply leaves a partial state you fix forward
Driftdrift detection, on demandterraform plan shows drift every run
Multi-account rolloutStackSets, nativeworkspaces, modules, pipelines - you build it
New AWS featuresusually day one (the service team ships the resource type)usually days to weeks after (provider release)
Modularitynested stacks, modules in the registrymodules, registries, for_each
Costfreefree (open-source / BSL binary); HCP Terraform has paid tiers
Who uses itAWS-native teams, Control Tower, Service Catalog, SAM, CDKmulti-cloud teams, platform teams, almost every DevOps job post

My practical advice - learn both. Use Terraform as your primary tool if you touch more than one cloud or already have a Terraform platform (Terraform index, Terraform Associate course); use CloudFormation when you live inside AWS-native tooling, when you need StackSets, or when the thing you are deploying ships as a template. Knowing how CloudFormation handles rollback and change sets also makes you appreciate exactly what Terraform's state file is doing for you.


16. Conclusion

CloudFormation is AWS's native infrastructure as code - a template describes resources, a stack is the running result, and a change set shows you what an update will do before it does it. You now have a complete template that uses parameters, mappings, conditions, outputs and exports, you have created, updated, drift-checked and deleted a stack in the console and the CLI, and you know where nested stacks, StackSets, the IaC generator and Template studio fit. Remember the three habits - lint and validate first, never execute a change set without reading the Replacement column, and turn on termination protection and DeletionPolicy for anything with data.

For the Terraform versions of the same builds see Terraform EC2 and the rest of the Terraform index; for the AWS services this template touches, Part-4 EC2, Part-16 security groups and the launch template post, which is what you would use instead of AWS::EC2::Instance once Auto Scaling enters the picture.



AWS step by step series -

  1. Part-1 : AWS IAM user - create a user, group, policy, access keys and MFA
  2. Part-2 : AWS Organizations - multi-account setup, OUs and SCPs
  3. Part-3 : AWS assume IAM role - trust policy, switch role in console and CLI
  4. Part-4 : How to launch an EC2 instance - key pair, security group, SSH
  5. Part-5 : AWS VPC - public and private subnets, Internet Gateway, NAT Gateway, route tables
  6. Part-8 : EC2 launch template - versions, default version, source template, SSM parameter AMI
  7. Part-10 : EC2 Auto Scaling - launch template, Auto Scaling group, target tracking, ALB
  8. Part-11 : AWS WAF - web ACL, managed rules, rate limiting, geo blocking
  9. Part-12 : AWS VPC Peering - connect two VPCs, routes, security groups, DNS
  10. Part-13 : AWS Transit Gateway - hub-and-spoke for many VPCs and on-premises
  11. Part-14 : AWS NAT Gateway deep dive - public vs private, limits, cost, troubleshooting
  12. Part-15 : Amazon Route 53 - hosted zones, records, alias, routing policies, health checks
  13. Part-16 : AWS security groups - inbound and outbound rules, stateful, referencing, quotas
  14. Part-16 : AWS Certificate Manager - free TLS certificates for ALB, CloudFront and API Gateway
  15. Part-17 : AWS Lambda - function URLs, environment variables and layers
  16. Part-18 : Network Load Balancer - setup, and ALB vs NLB
  17. Part-19 : VPC endpoints - gateway and interface endpoints (PrivateLink) instead of NAT
  18. Part-20 : AWS PrivateLink - publish your own service with an endpoint service and NLB
  19. Part-20 : Amazon EBS volumes - types, attach, mount, resize, snapshots, encryption
  20. Part-21 : VPC Flow Logs - CloudWatch Logs, S3, record format, Logs Insights, Athena
  21. Part-21 : EC2 Spot Instances - pricing, interruptions, mixed instances groups
  22. Part-24 : AWS Control Tower - landing zone, controls, Account Factory, Identity Center

Networking fundamentals -

  1. What is a VPC and a subnet? AWS networking in five minutes
  2. What is CIDR? Calculate IP ranges for VPCs and subnets
  3. What is NAT? Static NAT, dynamic NAT and PAT explained

More AWS guides -

  1. What is AWS CloudFormation? Templates, stacks, change sets, drift, StackSets
  2. Learn AWS S3 - the complete course
  3. AWS API Gateway - REST API with Lambda, authorizers, Terraform
  4. AWS Advanced Networking Specialty (ANS-C01) - course companion
  5. AWS ECS and Fargate - how to deploy a Docker container
  6. AWS S3 - how to host a static website
  7. Terraform create EC2 instance on AWS
  8. Terraform AWS IAM - users, roles and policies
  9. Terraform and AWS multi-account setup
  10. Terraform - setting up an ALB and SSL

Posts in this series