08 - CloudFormation: infrastructure as code
Time: 60 minutes. The last tutorial. It rebuilds pieces from
01-s3, 02-dynamodb and
04-messaging, so those are useful background.
What you will build
Everything you have been creating by hand, declared once in a file. One command brings the whole platform up. One command tears it down. Change the file, apply it again, and only the differences are applied.
flowchart LR
T["order-platform.yaml"] -->|"create-stack"| S["CloudFormation stack"]
S --> B[("S3 bucket")]
S --> D[("DynamoDB table")]
S --> Q(("SQS queue"))
T2["order-platform-v2.yaml"] -->|"update-stack"| S
S -.->|"adds only the difference"| N(("SNS topic"))
Why infrastructure as code
Every previous tutorial had you type create-bucket, create-table,
create-queue. That works once. Then you need the same thing for staging, and
you type it again slightly differently. Six months later nobody knows what is
actually deployed, and the person who set it up has left.
The fix is to stop performing the steps and start describing the result. A template says what should exist. CloudFormation works out what to do.
That change buys you three things:
- Repeatability. The same template gives the same infrastructure, every time, in any account.
- Review. Infrastructure changes arrive as a diff in a pull request.
- Deletion. The stack knows everything it created, so it can remove all of it. Forgotten resources are how cloud bills grow mysteriously.
Read section 8 before trusting this locally. Floci does not validate resource properties, so a template that real AWS would reject reports success here. That is the main thing this tutorial has to warn you about.
Prerequisites
floci start && eval $(floci env)
cd tutorials/08-iac
1. Read the template
Open templates/order-platform.yaml. It is
three resources and a parameter.
Parameters:
EnvName:
Type: String
Default: dev
A parameter is an input. The same template deployed with EnvName=dev and
EnvName=prod produces two independent copies with different names.
ReceiptsBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Sub "orders-${EnvName}-receipts"
ReceiptsBucket is the logical id, the name used inside the template.
orders-dev-receipts is the physical name, the one AWS actually uses. Keeping
them separate is what lets one template produce many environments.
!Sub substitutes parameters into a string. Two related functions appear in the
Outputs block:
Outputs:
BucketName:
Value: !Ref ReceiptsBucket
TableArn:
Value: !GetAtt OrdersTable.Arn
!Ref on most resources gives you its name. !GetAtt reaches inside for an
attribute that is not the name, such as an ARN. Outputs are how a stack reports
what it made.
2. Deploy it
aws cloudformation create-stack --stack-name order-platform --template-body file://templates/order-platform.yaml --parameters ParameterKey=EnvName,ParameterValue=dev
You get a StackId immediately. Creation happens in the background, so ask how
it went:
aws cloudformation describe-stacks --stack-name order-platform --query 'Stacks[0].StackStatus' --output text
CREATE_COMPLETE
See what it built:
aws cloudformation describe-stack-resources --stack-name order-platform --query 'StackResources[].[LogicalResourceId,ResourceType,ResourceStatus]' --output table
3. Confirm the resources are real
The stack claiming success is not the same as the resources existing. Check directly, using the same commands from earlier tutorials:
aws s3 ls
aws dynamodb describe-table --table-name orders-dev --query 'Table.TableStatus' --output text
aws sqs get-queue-url --queue-name orders-dev-queue --query QueueUrl --output text
All three are there, and they are ordinary resources. Nothing about them remembers that a template made them, other than the stack’s own record.
4. Read the outputs
aws cloudformation describe-stacks --stack-name order-platform --query 'Stacks[0].Outputs' --output table
This is how a deployment pipeline learns the name of the bucket it should write to, without anybody hardcoding it.
5. Change something
Open templates/order-platform-v2.yaml. It
is identical except for one added resource:
NotificationTopic:
Type: AWS::SNS::Topic
Properties:
TopicName: !Sub "orders-${EnvName}-notifications"
Apply it:
aws cloudformation update-stack --stack-name order-platform --template-body file://templates/order-platform-v2.yaml --parameters ParameterKey=EnvName,ParameterValue=dev
aws cloudformation describe-stacks --stack-name order-platform --query 'Stacks[0].StackStatus' --output text
UPDATE_COMPLETE
The topic now exists, and the bucket, table and queue were left completely alone:
aws cloudformation describe-stack-resources --stack-name order-platform --query 'StackResources[].LogicalResourceId' --output text
This is the declarative model doing its job. You did not tell it to add a topic. You described a world containing a topic, and it worked out the difference.
6. Read the history
aws cloudformation describe-stack-events --stack-name order-platform --query 'StackEvents[].[Timestamp,LogicalResourceId,ResourceStatus]' --output table
Events run newest first. On real AWS this is the first place you look when a deployment fails, because the failing resource carries the reason.
7. Delete everything
aws cloudformation delete-stack --stack-name order-platform
Then check:
aws s3 ls
aws dynamodb list-tables
Gone. All of it, in one command, because the stack knew exactly what it owned.
This is the strongest practical argument for templates. Clicking things together by hand is easy. Finding all of them again months later, in the right order, without breaking something else, is not.
8. What this cannot teach you locally
Try deploying something real AWS would refuse. This DynamoDB table has a
KeySchema naming an attribute that is never declared:
Resources:
BadTable:
Type: AWS::DynamoDB::Table
Properties:
TableName: broken-table
KeySchema:
- AttributeName: undeclared
KeyType: HASH
On real AWS this fails template validation. The stack goes
CREATE_FAILED, then ROLLBACK_COMPLETE, and anything else in that template is
destroyed so you are not left with half a deployment.
Under Floci the stack reports CREATE_COMPLETE, and the table is created
with an empty AttributeDefinitions list:
aws dynamodb describe-table --table-name broken-table --query 'Table.AttributeDefinitions'
[]
That is a resource in a state real AWS would never allow to exist.
Two consequences follow, and both matter:
CREATE_COMPLETE does not mean your template is correct. It means Floci
found nothing it chose to object to. A template can pass here and fail
immediately on a real account.
You cannot practise rollback. Nothing fails, so nothing rolls back, so you never see the half-deployed state that rollback exists to prevent. Reading a failed stack event is a genuinely useful skill and this environment cannot teach it.
Use Floci to learn what templates are and how the pieces fit together. Validate
the templates themselves with a real linter such as cfn-lint, and confirm
anything important against a real account before relying on it.
9. The same thing in code
cd python && pip install -r requirements.txt && python deploy.py
cd node && npm install && node deploy.mjs
Both deploy the stack, wait for completion, print the outputs, apply the update, demonstrate the validation gap, and tear everything down.
Verify
./verify.sh
Twenty six checks covering creation, real resource existence, parameters and intrinsic functions, outputs, events, updates, the validation divergence, and deletion.
Clean up
aws cloudformation delete-stack --stack-name order-platform
If a stack ever gets stuck, deleting the underlying resources by hand and then deleting the stack again usually clears it.
How this differs from real AWS
Verified by hand against Floci 0.2.0 on 2026-08-06. The core is genuine.
Multi-resource stacks really provision, !Sub, !Ref and !GetAtt all work,
parameters work, outputs work, updates apply only the difference, events are
recorded, and deleting a stack really deletes the resources.
- Resource properties are not validated. Covered in section 8. A template
real AWS rejects reports
CREATE_COMPLETEhere, and the resource is built in an impossible state. This is the divergence to remember. - There is no rollback, because nothing fails.
ROLLBACK_IN_PROGRESSandROLLBACK_COMPLETEcannot be observed, and neither can the failure modes they protect against. - Unknown resource types are ignored. A resource of type
AWS::Nonexistent::Serviceis accepted and the stack still reports success. On real AWS that is an immediate validation error. validate-templateaccepts almost anything. It returned success for a template containing an invented resource type, so it is not a useful check here.- Change sets are only partly present.
create-change-setreturns an id, but change set previewing and execution were not verified and should not be relied on. - No drift detection, no stack policies, no nested stacks, no
DependsOnordering guarantees were tested. Treat all of them as unverified rather than working.
Exercises
- Deploy the same template a second time as a separate stack with
EnvName=staging. Confirm you now have two independent sets of resources and that deleting one leaves the other untouched. - Combine with tutorial 05. Add the Lambda function and HTTP API from the
serverless capstone to this template, so the entire API deploys from one
file. Which values that you previously copied by hand become
!Refor!GetAtt? - Section 8 showed that a stack can report success while containing a resource real AWS would reject. Describe how you would catch that class of problem before it reaches production, given that the emulator will not tell you. Hint: the template is a file. What can you check about a file without deploying it at all?