| title | 08 - CloudFormation: infrastructure as code |
|---|---|
| permalink | /tutorials/08-iac/ |
Time: 60 minutes. The last tutorial. It rebuilds pieces from
01-s3, 02-dynamodb and
04-messaging, so those are useful background.
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"))
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.
floci start && eval $(floci env)cd tutorials/08-iacOpen templates/order-platform.yaml. It is
three resources and a parameter.
Parameters:
EnvName:
Type: String
Default: devA 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.
aws cloudformation create-stack --stack-name order-platform --template-body file://templates/order-platform.yaml --parameters ParameterKey=EnvName,ParameterValue=devYou 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 textCREATE_COMPLETE
See what it built:
aws cloudformation describe-stack-resources --stack-name order-platform --query 'StackResources[].[LogicalResourceId,ResourceType,ResourceStatus]' --output tableThe stack claiming success is not the same as the resources existing. Check directly, using the same commands from earlier tutorials:
aws s3 lsaws dynamodb describe-table --table-name orders-dev --query 'Table.TableStatus' --output textaws sqs get-queue-url --queue-name orders-dev-queue --query QueueUrl --output textAll three are there, and they are ordinary resources. Nothing about them remembers that a template made them, other than the stack's own record.
aws cloudformation describe-stacks --stack-name order-platform --query 'Stacks[0].Outputs' --output tableThis is how a deployment pipeline learns the name of the bucket it should write to, without anybody hardcoding it.
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=devaws cloudformation describe-stacks --stack-name order-platform --query 'Stacks[0].StackStatus' --output textUPDATE_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 textThis 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.
aws cloudformation describe-stack-events --stack-name order-platform --query 'StackEvents[].[Timestamp,LogicalResourceId,ResourceStatus]' --output tableEvents run newest first. On real AWS this is the first place you look when a deployment fails, because the failing resource carries the reason.
aws cloudformation delete-stack --stack-name order-platformThen check:
aws s3 lsaws dynamodb list-tablesGone. 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.
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: HASHOn 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.
cd python && pip install -r requirements.txt && python deploy.pycd node && npm install && node deploy.mjsBoth deploy the stack, wait for completion, print the outputs, apply the update, demonstrate the validation gap, and tear everything down.
./verify.shTwenty six checks covering creation, real resource existence, parameters and intrinsic functions, outputs, events, updates, the validation divergence, and deletion.
aws cloudformation delete-stack --stack-name order-platformIf a stack ever gets stuck, deleting the underlying resources by hand and then deleting the stack again usually clears it.
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.
- 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?