Skip to content
AWS aws deployment 6 min read

Writing a CloudFormation Template

A CloudFormation template is a text file (written in YAML or JSON) that describes the AWS resources you want — like servers, databases, and networks — and how they connect. AWS reads this file and builds everything for you, so your infrastructure becomes code you can version, review, and reuse. This page walks through the anatomy of a template, the small helper functions that make it flexible, and how to deploy it from both the AWS Management Console (the web dashboard) and the AWS Command Line Interface (AWS CLI, the terminal tool). We will use YAML throughout because it is easier to read than JSON.

Template anatomy

A template is made of several top-level sections. Only Resources is required; the rest are optional but make your template far more powerful. Here is what each one does and when to reach for it.

SectionPurposeWhen to use
ParametersInputs you supply at deploy timeWhen the same template should work for dev, staging, and prod
MappingsFixed lookup tables (key to value)When a value depends on region or environment but is known in advance
ConditionsTrue/false rules that turn resources on or offWhen prod needs extra resources that dev does not
ResourcesThe actual AWS things to create (required)Always
OutputsValues to return after the stack is builtWhen you need an IP, URL, or ID to use elsewhere

A “stack” is simply the running result of a deployed template — all the resources managed together as one unit.

Parameters

Parameters let you pass values in at deploy time instead of hard-coding them. This means one template can build many environments.

Parameters:
  EnvironmentName:
    Type: String
    AllowedValues: [dev, staging, prod]
    Default: dev
    Description: Which environment this stack is for
  InstanceType:
    Type: String
    Default: t3.micro

When to use: any value that changes between deployments (sizes, names, counts). When NOT to use: secrets like passwords — put those in AWS Secrets Manager and reference them, never as a plain parameter default.

Mappings

A mapping is a static lookup table. The most common use is picking the right Amazon Machine Image (AMI — a prebuilt server image) per region, because an AMI ID is different in every region.

Mappings:
  RegionToAMI:
    us-east-1:
      AMI: ami-0abcdef1234567890
    eu-west-1:
      AMI: ami-0123456789abcdef0

When to use: values that are known ahead of time and vary by a fixed key like region. When NOT to use: values a user should choose — use a parameter instead.

Conditions

Conditions create true/false flags from parameters, then attach them to resources so they are only built when the flag is true.

Conditions:
  IsProd: !Equals [!Ref EnvironmentName, prod]

Resources

This is the heart of the template. Each resource has a logical name (your label), a Type (the AWS service), and Properties (its settings).

Resources:
  AppServer:
    Type: AWS::EC2::Instance
    Properties:
      InstanceType: !Ref InstanceType
      ImageId: !FindInMap [RegionToAMI, !Ref "AWS::Region", AMI]
      Tags:
        - Key: Name
          Value: !Sub "${EnvironmentName}-app-server"
  AppDatabase:
    Type: AWS::RDS::DBInstance
    DeletionPolicy: Retain
    Properties:
      Engine: postgres
      DBInstanceClass: db.t3.micro
      AllocatedStorage: "20"
      Condition: IsProd

Outputs

Outputs return useful values after the build, such as a server ID or a public address you need for the next step.

Outputs:
  ServerId:
    Description: The EC2 instance ID
    Value: !Ref AppServer
  ServerPublicIp:
    Value: !GetAtt AppServer.PublicIp

Intrinsic functions

Intrinsic functions are small built-in helpers that compute values while the stack is building. The three you will use most:

FunctionShort formWhat it returnsExample
Ref!RefThe main ID/value of a resource or parameter!Ref AppServer gives the instance ID
Fn::GetAtt!GetAttA specific attribute of a resource!GetAtt AppServer.PublicIp
Fn::Sub!SubA string with variables filled in!Sub "${EnvironmentName}-bucket"

Use Ref for the obvious identifier, GetAtt when you need a detail like a private IP or Amazon Resource Name (ARN, a unique ID for an AWS resource), and Sub to build names and scripts from other values.

Deploying the template

Using the Console

  1. Open the AWS Management Console and go to the CloudFormation service.
  2. Click Create stack, then With new resources (standard).
  3. Under Specify template, choose Upload a template file and select your .yaml file.
  4. Click Next, give the stack a name (for example app-stack-dev), and fill in the parameter values.
  5. Click Next through the options, check the acknowledgment box if it asks about creating IAM resources, then click Submit.
  6. Watch the Events tab — when the status reads CREATE_COMPLETE, your resources are live.

Using the AWS CLI

aws cloudformation deploy \
  --template-file app-stack.yaml \
  --stack-name app-stack-dev \
  --parameter-overrides EnvironmentName=dev InstanceType=t3.micro \
  --capabilities CAPABILITY_NAMED_IAM

Output:

Waiting for changeset to be created..
Waiting for stack create/update to complete
Successfully created/updated stack - app-stack-dev

To see your outputs after deploy:

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

Output:

[
    {
        "OutputKey": "ServerId",
        "OutputValue": "i-0a1b2c3d4e5f"
    },
    {
        "OutputKey": "ServerPublicIp",
        "OutputValue": "52.23.45.67"
    }
]

The “update requires replacement” gotcha

When you change a property and update the stack, CloudFormation does one of three things: an in-place update (safe), an update with some interruption (a reboot), or a replacement — it creates a brand-new resource and deletes the old one. Replacement is the dangerous one. Changing the ImageId on an EC2 instance, or the DBName on a database, forces a replacement. That means the old EC2 instance is destroyed (its public IP changes) or, worse, your database is deleted along with its data.

Every property in the AWS documentation has an “Update requires” note: No interruption, Some interruption, or Replacement. Always read it before changing a property on a live stateful resource.

Warning: Set DeletionPolicy: Retain (and UpdateReplacePolicy: Retain) on databases, S3 buckets, and anything else holding data. This tells CloudFormation to keep the resource even if the stack would otherwise delete or replace it — your safety net against accidental data loss.

Tip: Run aws cloudformation deploy without --no-execute-changeset removed, or create a change set first, to preview exactly what will change. A change set lists every action — including any Replacement: True — before you commit. An accidental RDS replacement can mean hours of downtime and lost data, so this preview is free insurance.

Cost note: CloudFormation itself is free — you pay only for the resources it creates. A t3.micro EC2 instance runs about 0.0104 USD per hour (around 7.50 USD per month) in us-east-1, and a db.t3.micro PostgreSQL database about 0.017 USD per hour. Deleting the stack removes those resources and stops the charges.

Best Practices

  • Keep Resources minimal and split large systems into multiple smaller stacks that are easier to reason about and update.
  • Use Parameters for anything that varies between environments, and Mappings only for fixed, known-ahead values.
  • Always check the “Update requires” note before changing a property on a live database or instance.
  • Add DeletionPolicy: Retain and UpdateReplacePolicy: Retain to every stateful resource (databases, buckets, volumes).
  • Preview changes with a change set before applying them to production stacks.
  • Store templates in version control (Git) and review changes like any other code.
  • Tag resources with !Sub so every environment’s resources are clearly named and easy to find.
Last updated June 15, 2026
Was this helpful?