Parameters as variables¶
Templates take values from outside: a VPC id, a list of subnets, a stage name. In plain
CloudFormation each is a string you type twice, once in Parameters and again in every
{"Ref": "VpcId"}. Misspell one and nothing complains until deploy.
cfn_typed.Params makes the parameter a variable. You declare it once and get back
the Ref to use:
params = cfn.Params()
vpc_id = params.ssm("VpcId", "/platform/network/vpc-id") # a plain {"Ref": "VpcId"}
...
template["Parameters"] = params.declarations()
vpc_id is an ordinary Ref dict, so it is valid anywhere an Expr[str] is accepted.
Misspelling the variable is a name error the editor shows immediately, and the string
"VpcId" appears exactly once.
Plain and SSM parameters¶
| Call | Declares |
|---|---|
params.string("Stage", default="dev", allowed_values=[...]) |
a String parameter; also allowed_pattern, min_length, max_length, constraint_description |
params.string("Ami", type="AWS::EC2::Image::Id") |
a string of an AWS-specific type, which CloudFormation validates (AWS::EC2::VPC::Id, AWS::EC2::KeyPair::KeyName, ...) |
params.number("Size", default=3) |
a Number parameter; also min_value, max_value, constraint_description |
params.list_of("Cidrs", default=["10.0.0.0/8"]) |
a CommaDelimitedList; the Ref is accepted wherever a list is |
params.list_of("Subnets", type="AWS::EC2::Subnet::Id") |
a List<AWS::EC2::Subnet::Id>; type is the element type, so Number gives List<Number> |
params.ssm("VpcId", "/path") |
an SSM String, resolved by CloudFormation at deploy time |
params.ssm("Ami", "/path", type="AWS::EC2::Image::Id") |
an SSM value of that type, as in AWS::SSM::Parameter::Value<AWS::EC2::Image::Id> |
params.ssm_list("Subnets", "/path") |
an SSM StringList; type= sets the element type, as for list_of |
SSM values are not read when you build the template. The template carries only the parameter's name in SSM, and CloudFormation looks the value up when the stack is created or updated and shows it in the change set. That keeps template generation a pure function of its inputs, which is the property the rest of this book relies on.
Names must be letters and digits only (a CloudFormation rule), and Params checks that.
Declaring the same name twice with the same settings is fine and returns the same Ref;
declaring it differently raises, so two modules cannot quietly disagree.
A shared "defaults" library¶
Organisations usually want one place that knows where the shared network lives, which tags every stack carries, and so on. That belongs in your own package, not in a public one. Each default is a function that takes the registry and returns variables:
from typing import NamedTuple
from cfn_typed import Expr, Params, Tag, tags
from cfn_typed.intrinsics import Ref
class Network(NamedTuple):
vpc_id: Ref
private_subnets: Ref
def network(params: Params) -> Network:
"""The shared network, read from SSM by CloudFormation at deploy time."""
return Network(
vpc_id=params.ssm("VpcId", "/platform/network/vpc-id", description="Shared VPC"),
private_subnets=params.ssm_list("PrivateSubnets", "/platform/network/private-subnets"),
)
def standard_tags(params: Params) -> list[Expr[Tag]]:
stage = params.string("Stage", default="dev", allowed_values=["dev", "test", "prod"])
return tags(Stage=stage, ManagedBy="cfn-typed")
A stack calls only what it needs:
def build() -> cfn.Template:
params = cfn.Params()
net = network(params) # only what is called gets declared
tags = standard_tags(params)
return {
"AWSTemplateFormatVersion": "2010-09-09",
"Parameters": params.declarations(),
"Resources": web_entrypoint(
"Web",
vpc_id=net.vpc_id,
subnets=net.private_subnets,
ports=[8080],
allowed_cidrs=["10.0.0.0/8"],
scheme="internal",
tags=tags,
),
}
- Only what is called gets declared. A stack that never calls
network(params)does not getVpcIdorPrivateSubnetsin itsParameters, so unused defaults never clutter a template or count against its parameter limit. - The result is a typed bundle.
net.vpc_idandnet.private_subnetsautocomplete; there is no string to mistype. - The paths live in one place. When the SSM path moves, one line in the defaults package changes and every stack follows on its next deploy.
- Nothing organisation-specific is in
cfn-typed. It supplies the mechanism (Params); your defaults package supplies the names and paths.
The full rendered stack has the parameters first, and every use is
!Ref VpcId, !Ref PrivateSubnets:
Parameters:
VpcId:
Type: AWS::SSM::Parameter::Value<String>
Default: /platform/network/vpc-id
PrivateSubnets:
Type: AWS::SSM::Parameter::Value<List<String>>
Default: /platform/network/private-subnets
Values from other stacks¶
A value exported by another stack needs no registry, because Fn::ImportValue is not
declared anywhere. The variable is simply a module-level constant, defined once:
from cfn_typed import import_value
shared_alb_arn = import_value("platform-alb-arn") # import this name everywhere
Dynamic references and secrets¶
cfn.dynamic_ssm("/app/key") returns the string {{resolve:ssm:/app/key}}, and
dynamic_ssm("/app/password", secure=True) the ssm-secure form. These need no template
parameter, but CloudFormation accepts them only in some properties, and the secure form in
fewer still, so check the resource's documentation before relying on one. For anything
secret, prefer a managed secret and refer to it rather than passing a value through the
template.
What stays a string¶
A logical ID in ref("OrderQueue") or get_att(...) is still a string, because it names a
resource rather than a parameter. Build those from one name inside a pattern, as the
other pages do, and keep cfn-lint in CI.