Skip to the content.

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:

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.

Exercises

  1. 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.
  2. 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 !Ref or !GetAtt?
  3. 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?