Getting started¶
Using a coding agent?
Give it the agent quickstart, a single markdown page of the patterns to adopt and the rules to follow.
Install¶
uv add cfn-typed # the runtime pieces and the cfn-typegen command
uv add --dev ty # the type checker this project is tested with
Python 3.14 or later is required. YAML output uses ruamel.yaml, which is installed with it.
Generate the types you use¶
The package ships no resource types. You generate the ones your templates need into your own project:
Not sure of a name? cfn-typegen search queue lists matching types.
Or point it at a template that already exists and it emits exactly the types that template uses:
Each service becomes one module (cfn_types/sqs.py, cfn_types/lambda_.py, ...).
Each run writes a clean set of modules and replaces only files it generated earlier.
Whether to commit them or generate them in CI is up to you.
A minimal program¶
The whole program is one file: build the template as data, print it as JSON.
import json
from dataclasses import dataclass
import cfn_typed as cfn
from cfn_types import sqs
@dataclass(frozen=True)
class JobsConfig:
visibility_timeout: int = 60
def build(config: JobsConfig = JobsConfig()) -> cfn.Template:
params = cfn.Params()
stage = params.string("Stage", default="dev", allowed_values=["dev", "test", "prod"])
return {
"AWSTemplateFormatVersion": "2010-09-09",
"Parameters": params.declarations(),
"Resources": {
"Jobs": sqs.queue(VisibilityTimeout=config.visibility_timeout, Tags=cfn.tags(Stage=stage)),
},
"Outputs": {"JobsUrl": {"Value": cfn.ref("Jobs")}},
}
if __name__ == "__main__":
print(json.dumps(build(), indent=2))
build is a pure function of its config, so tests can call it directly. The inputs that change the
template's shape go in the config object (JobsConfig); the ones that only change a value, such as the
stage, are CloudFormation parameters, so one rendered template serves every stage. The rendered
YAML is in the book. Render and deploy:
python minimal.py > template.json
aws cloudformation deploy --template-file template.json --stack-name jobs-prod \
--parameter-overrides Stage=prod
JSON or YAML¶
cfn.to_json(template) is json.dumps with a default indent. cfn.to_yaml(template)
writes the same template as readable CloudFormation YAML:
AWSTemplateFormatVersion: '2010-09-09'
Resources:
Jobs:
Type: AWS::SQS::Queue
Properties:
VisibilityTimeout: 60
Tags:
- Key: Stage
Value: !Ref Stage
Outputs:
JobsUrl:
Value: !Ref Jobs
Intrinsics use CloudFormation's short forms (!Ref, !GetAtt Jobs.Arn, !Sub, !Join,
...), keys keep the order you wrote them, lists are indented under their key, and
multi-line strings such as UserData become | blocks. A list that is shared between
resources (the tags in the examples) is repeated, never turned into a YAML anchor.
Nested short forms
CloudFormation does not allow a short form directly inside another one
(!Base64 !Sub ...). When that happens to_yaml keeps the outer function in long
form (Fn::Base64: !Sub ...). Functions with no short form, such as Fn::Length,
also stay long. Parsing the YAML back gives exactly the template you started with;
the test suite checks that round trip.
Check the rendered YAML in CI¶
Commit what your code renders, and let CI fail when the two disagree, so that every template change appears as a diff in the pull request:
from cfn_typed.testing import assert_matches_file
def test_jobs() -> None:
assert_matches_file(build(), "rendered/jobs.yaml")
Run the tests with CFN_TYPED_UPDATE=1 to create or accept the files. The
testing page has the CI workflow and the details. You can see a committed example
under rendered examples.
Let the checker help¶
Anywhere a property takes a value you can pass either a literal or an intrinsic
such as cfn.ref(...) or cfn.get_att(...): every generated property is
Expr[T], meaning T or an intrinsic. Unknown names, wrong types and missing
required properties are reported before any template exists. Keep cfn-lint in CI
as well: types do not check values, such as whether a Lambda runtime exists.