About Experience Skills Contact Blog Tools
← Back to articles

CloudFormation Basics: A Practical Introduction for New Developers

· 7 min read

The first time I deployed an application to AWS, I spent an afternoon clicking through the console — creating an S3 bucket, configuring a CloudFront distribution, setting up IAM roles, wiring permissions together. It worked. Then I needed to create the same setup for a staging environment. I spent another afternoon clicking through the same screens, making slightly different choices, and wondering which settings I’d forgotten.

That experience is why infrastructure as code exists. Instead of clicking through a web console, you describe your infrastructure in a file, and a tool builds it for you — identically, every time. AWS CloudFormation is AWS’s own tool for this, and it’s where I’d recommend starting if you’re working exclusively with AWS services.

What CloudFormation Actually Does

CloudFormation reads a template file — written in YAML or JSON — and creates the AWS resources described in it. That’s the core of it. You write a file that says “I want an S3 bucket with these settings and a CloudFront distribution that points to it,” and CloudFormation calls the necessary AWS APIs to make it happen.

The collection of resources created from a single template is called a stack. A stack is CloudFormation’s unit of management. You create a stack, update a stack, or delete a stack. When you delete a stack, CloudFormation tears down all the resources it created — in the correct order, handling dependencies automatically. This is one of the things that makes CloudFormation genuinely useful early on: clean teardown. If you’ve ever created a bunch of AWS resources manually and then tried to delete them, you know the pain of figuring out which ones depend on which and what order to remove them in.

CloudFormation also tracks state for you automatically. It knows what resources belong to each stack and what their current configuration should be. There’s no external state file to manage, no database to maintain, no S3 bucket to configure for storing state. This is different from tools like Terraform, where state management is something you have to set up and secure yourself. For someone just getting started, not having to think about state management is a meaningful advantage.

Anatomy of a Template

A CloudFormation template has a few top-level sections. Here’s a minimal but realistic example that creates an S3 bucket configured for static website hosting:

AWSTemplateFormatVersion: "2010-09-09"
Description: Static website hosting with S3

Parameters:
  BucketName:
    Type: String
    Description: Name for the S3 bucket

Resources:
  WebsiteBucket:
    Type: AWS::S3::Bucket
    Properties:
      BucketName: !Ref BucketName
      WebsiteConfiguration:
        IndexDocument: index.html
        ErrorDocument: error.html

  BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref WebsiteBucket
      PolicyDocument:
        Statement:
          - Effect: Allow
            Principal: "*"
            Action: s3:GetObject
            Resource: !Sub "arn:aws:s3:::${WebsiteBucket}/*"

Outputs:
  WebsiteURL:
    Description: URL of the static website
    Value: !GetAtt WebsiteBucket.WebsiteURL

Let me walk through each section.

AWSTemplateFormatVersion is always "2010-09-09". It hasn’t changed since CloudFormation launched, and it probably never will. Just include it and move on.

Parameters define inputs that you provide when creating or updating a stack. In this case, the bucket name. Parameters make templates reusable — the same template can create buckets with different names for development, staging, and production environments.

Resources is the only required section and the heart of every template. Each resource has a logical name (like WebsiteBucket), a type (like AWS::S3::Bucket), and properties specific to that resource type. The AWS documentation lists every resource type and its properties — this is the reference you’ll use constantly.

Outputs expose values from the stack that you might need elsewhere. Here, the website URL is output so you know where to find your site after the stack is created. Outputs also enable cross-stack references, where one stack’s output becomes another stack’s input, but that’s a topic for later.

The Glue: Intrinsic Functions

The !Ref, !Sub, and !GetAtt expressions in the template above are intrinsic functions — CloudFormation’s way of wiring resources together.

!Ref returns the value of a parameter or the physical ID of a resource. !Ref BucketName gives you the parameter value. !Ref WebsiteBucket gives you the actual bucket name that was created.

!Sub does string interpolation. !Sub "arn:aws:s3:::${WebsiteBucket}/*" substitutes the bucket’s name into the ARN string. It’s how you construct values that depend on other resources.

!GetAtt retrieves a specific attribute from a resource. !GetAtt WebsiteBucket.WebsiteURL gets the website endpoint URL from the bucket. Different resource types expose different attributes — the docs tell you which ones are available.

These three functions handle the majority of what you need. There are others (!If, !Join, !Select, !Split), but you can build a lot of infrastructure before you need them.

Deploying Your First Stack

You can deploy a CloudFormation stack through the AWS Console, but I’d recommend using the AWS CLI from the start. It’s faster, scriptable, and teaches you the workflow that you’ll use in practice.

First, save the template above to a file called website.yaml. Then run:

aws cloudformation create-stack \
  --stack-name my-website \
  --template-body file://website.yaml \
  --parameters ParameterKey=BucketName,ParameterValue=my-unique-bucket-name

CloudFormation starts provisioning resources. You can watch the progress:

aws cloudformation describe-stack-events \
  --stack-name my-website \
  --query "StackEvents[*].[ResourceType,ResourceStatus,ResourceStatusReason]" \
  --output table

When the stack reaches CREATE_COMPLETE, your resources exist. To see the outputs:

aws cloudformation describe-stacks \
  --stack-name my-website \
  --query "Stacks[0].Outputs"

When you’re done experimenting:

aws cloudformation delete-stack --stack-name my-website

That single command removes everything the stack created. No orphaned resources, no forgotten S3 buckets accumulating storage costs.

Change Sets: Preview Before You Deploy

Once a stack exists, you don’t update it by deleting and recreating it. You modify the template and apply the changes. But blindly applying changes to running infrastructure is risky — you might not realize that renaming a property causes a resource to be replaced rather than updated in place.

This is where change sets come in. A change set previews what CloudFormation will do before it does it:

aws cloudformation create-change-set \
  --stack-name my-website \
  --template-body file://website.yaml \
  --change-set-name add-versioning \
  --parameters ParameterKey=BucketName,ParameterValue=my-unique-bucket-name

CloudFormation analyzes the difference between your updated template and the current stack, then shows you exactly which resources will be added, modified, or replaced. You review the change set, and if it looks right, you execute it:

aws cloudformation execute-change-set \
  --stack-name my-website \
  --change-set-name add-versioning

I use change sets for every update, even ones I’m confident about. The cost is a few extra seconds. The benefit is not accidentally replacing a database because you renamed a property that triggers replacement.

Mistakes I Made Early On (So You Don’t Have To)

Hardcoding values instead of using parameters. My early templates had AWS account IDs, region names, and environment-specific values scattered throughout. The template worked for one environment and nowhere else. Use parameters for anything that changes between environments, and use pseudo parameters like AWS::Region and AWS::AccountId for values that CloudFormation already knows.

# Don't do this
Resource: "arn:aws:s3:::my-prod-bucket/*"

# Do this
Resource: !Sub "arn:aws:s3:::${WebsiteBucket}/*"

Writing one massive template. I once had a single template with 47 resources — VPC, subnets, security groups, EC2 instances, RDS database, Lambda functions, API Gateway. Deploying it took twelve minutes. A failure halfway through left half-created resources in a rollback state. Break your infrastructure into logical stacks: a networking stack, a database stack, an application stack. Use outputs and cross-stack references to connect them.

Ignoring the resource documentation. CloudFormation resource types have specific behaviors around updates. Some property changes cause in-place updates. Others cause resource replacement — CloudFormation creates a new resource, then deletes the old one. For a stateless resource like a Lambda function, replacement is fine. For a database, replacement means data loss. The documentation marks which properties trigger replacement. Read it before modifying resources that hold state.

Not using --capabilities CAPABILITY_IAM. If your template creates IAM roles or policies, CloudFormation requires you to explicitly acknowledge this with the --capabilities flag. This isn’t an error — it’s a safety check. The first time you see the error message, it’s confusing. Now you know.

aws cloudformation create-stack \
  --stack-name my-app \
  --template-body file://app.yaml \
  --capabilities CAPABILITY_IAM

A More Realistic Example

Most real CloudFormation usage goes beyond a single S3 bucket. Here’s a template for a common pattern — a Lambda function behind an API Gateway endpoint:

AWSTemplateFormatVersion: "2010-09-09"
Transform: AWS::Serverless-2016-10-31
Description: API endpoint backed by a Lambda function

Parameters:
  Environment:
    Type: String
    AllowedValues: [dev, staging, prod]

Resources:
  ApiFunction:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: !Sub "my-api-${Environment}"
      Runtime: nodejs22.x
      Handler: index.handler
      CodeUri: ./src
      MemorySize: 256
      Timeout: 30
      Environment:
        Variables:
          ENV: !Ref Environment
      Events:
        ApiEvent:
          Type: Api
          Properties:
            Path: /items
            Method: GET

This uses the SAM transform (AWS::Serverless-2016-10-31), which is an extension of CloudFormation specifically for serverless applications. SAM simplifies the boilerplate — without it, you’d need to define the Lambda function, an API Gateway REST API, a deployment stage, a method, a permission resource, and an IAM role separately. SAM creates all of that from the concise definition above.

You deploy SAM templates with the sam CLI rather than the aws cloudformation CLI:

sam build
sam deploy --guided

The --guided flag walks you through the parameters interactively, which is helpful when you’re getting started. It creates a samconfig.toml file that remembers your choices for subsequent deployments.

Where to Go From Here

If you’ve followed along this far, you understand the core model: templates define resources, stacks manage them, parameters make templates reusable, and intrinsic functions wire resources together. That’s enough to start building real infrastructure.

A few suggestions for next steps:

Read the resource reference for services you use. When you need to create a resource, search for its CloudFormation type (e.g., AWS::RDS::DBInstance) in the AWS documentation. The property reference is your source of truth.

Set up the AWS CloudFormation Language Server in your editor. It provides auto-completion, real-time validation, and inline documentation. It catches typos and invalid properties before you deploy, which dramatically shortens the feedback loop.

Use cfn-lint to validate templates locally. It’s stricter than CloudFormation’s built-in validation and catches issues that would otherwise only surface during deployment.

pip install cfn-lint
cfn-lint my-template.yaml

Start with SAM if you’re building serverless applications. The reduced boilerplate makes the learning curve less steep, and you can always drop down to raw CloudFormation when you need more control.

CloudFormation isn’t glamorous. Nobody gives conference talks about writing YAML. But it’s reliable, it’s deeply integrated with AWS, and it handles the hard parts — dependency ordering, rollback, state management — so you can focus on what your infrastructure actually needs to do. For a new developer working with AWS, that’s a solid foundation to build on.