Alright, let’s talk about the blueprint itself: the CloudFormation template. This is the file you’ll be slinging around, and AWS, in its infinite wisdom, gives you two equally frustrating ways to write it: JSON and YAML.

I know, I know. Your first instinct is to recoil from JSON. It’s verbose, it hates comments (a truly criminal omission), and a single misplaced comma will ruin your entire afternoon. YAML, with its whitespace sensitivity, feels like the “better” option, and for the most part, it is. But be warned: YAML has its own dark arts, like anchors and aliases, that can make a simple template look like a mind-bending puzzle. My advice? Stick with straightforward YAML. It’s more human-readable, and you can actually leave notes for your future self (or the poor soul who has to maintain your code) using # comments.

The core thing to understand is that both formats, once parsed, result in exactly the same underlying data structure—a big bag of key-value pairs that AWS uses to build your stuff. The choice is purely about authoring experience.

The Absolute Non-Negotiables: AWSTemplateFormatVersion and Description

Every template, whether JSON or YAML, must start with AWSTemplateFormatVersion. It’s not a version of your template; it’s the version of the CloudFormation template language itself. As of my writing this, you’ll almost certainly just set it to '2010-09-09' and forget about it forever. It’s like a magic incantation. Just include it.

The Description is optional but, frankly, should be mandatory. Use it. Describe what this stack does. “Creates a VPC” is okay. “Creates the core production VPC with three tiers and NAT gateways” is better. Your memory is worse than you think.

AWSTemplateFormatVersion: '2010-09-09'
Description: 'A simple stack that proves I can run a server. Not for production. Seriously.'

Your Sandbox: Parameters and Pseudo Parameters

Parameters are how you make a template reusable. They’re the variables that someone (you, a CI/CD pipeline) can pass in at stack creation or update time. Things like instance types, environment names (dev, prod), or key pairs.

Then there are Pseudo Parameters. These are AWS’s gift to you—pre-defined parameters that are always available. You don’t declare them; you just reference them. They give you things like the AWS Region (AWS::Region) or Account ID (AWS::AccountId) you’re working in. Incredibly useful.

Parameters:
  InstanceType:
    Type: String
    Default: t3.micro
    AllowedValues:
      - t3.micro
      - t3.small
      - m5.large
    Description: 'EC2 instance type. Do not set to m5.metal unless you hate money.'
  EnvironmentName:
    Type: String
    Default: dev
    Description: 'Environment name used for resource tagging'

# Later, in the Resources section...
Resources:
  MyInstance:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref InstanceType # This gets the value from the parameter
      ImageId: ami-12345678
      Tags:
        - Key: Environment
          Value: !Ref EnvironmentName
        - Key: Name
          Value: !Sub "Web Server in ${AWS::Region}" # Using a pseudo parameter

The Main Event: The Resources Section

This is the heart of the template. It’s where you declare the AWS resources you want to create. Every resource has a logical ID (the name you give it inside the template, like MyInstance) and a type (like AWS::EC2::Instance).

The Properties section is the most important part. This is where you configure the resource. Getting this right is 90% of the battle. The properties you can set are dictated by the resource type. Pro tip: Bookmark the AWS Resource and Property Types Reference. It’s your bible. You will live there.

Here’s a brutally common pitfall: deletion policies. By default, when you delete a stack, CloudFormation will delete almost everything it created. This is fantastic for cleaning up test stacks and terrifying for production. If you have a database (an AWS::RDS::DBInstance) or an S3 bucket (AWS::S3::Bucket) with data you cannot lose, you must set a DeletionPolicy.

Resources:
  MyProductionDatabase:
    Type: AWS::RDS::DBInstance
    DeletionPolicy: Retain # The most important line in this template
    Properties:
      DBInstanceClass: db.t3.small
      Engine: mysql
      MasterUsername: admin
      MasterUserPassword: '{{resolve:secretsmanager:MySecret:SecretString:password}}' # Never hardcode passwords! Use Secrets Manager.
      AllocatedStorage: '20'

  MyTempBucket:
    Type: AWS::S3::Bucket
    # No DeletionPolicy, so it gets deleted on stack deletion. Perfect for temp data.

Wiring Things Together: Intrinsic Functions

This is where CloudFormation gets its power. You can’t just hardcode everything. You need to reference other resources, join strings, or conditionally create things. That’s what intrinsic functions are for. They’re the language you use inside your template.

  • !Ref: The workhorse. Gets the value of a Parameter or the ID/ARN of a Resource (which one depends on the resource).
  • !GetAtt: Gets an attribute of a resource. Need the DNS name of a load balancer? That’s !GetAtt MyLoadBalancer.DNSName.
  • !Sub: String substitution. Injects values into a string. Essential for building complex strings.
  • !ImportValue: Grabs the output value from another stack. This is how you share things between stacks.
Resources:
  MySecurityGroup:
    Type: AWS::EC2::SecurityGroup
    Properties:
      GroupDescription: Allow HTTP and SSH
      SecurityGroupIngress:
        - IpProtocol: tcp
          FromPort: 80
          ToPort: 80
          CidrIp: 0.0.0.0/0
        - IpProtocol: tcp
          FromPort: 22
          ToPort: 22
          CidrIp: !Ref MyHomeIPParameter

  MyInstance:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref InstanceType
      ImageId: ami-12345678
      SecurityGroupIds:
        - !Ref MySecurityGroup # This passes the ID of the security group
      UserData:
        Fn::Base64: !Sub |
          #!/bin/bash
          echo "Hello from ${AWS::StackName} in ${AWS::Region}" > /tmp/hello.txt

The Grand Finale: Outputs

After your stack is created, how does another stack or a human know what was created? The Outputs section. It’s a way to expose important information from your stack, like the public DNS name of your new website. These can be viewed in the AWS Console or fetched via the CLI, and most importantly, they can be !ImportValue-d by other stacks.

Outputs:
  WebsiteURL:
    Description: 'The URL of our fabulous website'
    Value: !Sub 'http://${MyInstance.PublicDnsName}' # Using the resource's public DNS attribute
  DatabaseEndpoint:
    Description: 'The connection string for the database'
    Value: !GetAtt MyProductionDatabase.Endpoint.Address

So there you have it. The skeleton of every CloudFormation template you’ll ever write. Master this structure, and you’ve got the foundation for building anything AWS has to offer, without ever clicking that cursed “Create resource” button in the console again.