Skip to content

Publishing values through SSM

A stack that owns shared resources has to tell other stacks where they are: a bucket's ARN, a key's ARN. CloudFormation has exports for that, but an export cannot be changed or removed while another stack imports it, which welds the two stacks together. SSM parameters are the looser alternative: the owner publishes values under a path, and consumers read that path.

The producer

publish turns a mapping into SSM parameters:

def publish(prefix: str, values: Mapping[str, Expr[str]]) -> Resources:
    """Publish values as SSM parameters named `{prefix}/{key}`, for other stacks to read.

    `publish("/data/storage", {"key-arn": get_att("DataKey", "Arn")})` creates the logical ID `KeyArnParameter`.
    """
    return {
        "".join(part.capitalize() for part in re.split(r"[^A-Za-z0-9]+", key)) + "Parameter": ssm.parameter(
            Type="String", Name=f"{prefix}/{key}", Value=value,
        )
        for key, value in values.items()
    }

The whole producer stack, using the storage patterns:

def build() -> cfn.Template:
    params = cfn.Params()
    stage = params.string("Stage", default="dev", allowed_values=["dev", "test", "prod"])
    deploy_role = params.string("DeployRoleArn", description="The role that deploys this stack: it must keep bucket access")
    tags = cfn.tags(Stage=stage)

    return {
        "AWSTemplateFormatVersion": "2010-09-09",
        "Parameters": params.declarations(),
        "Resources": cfn.combine(
            encryption_key("Data", alias="data-platform", tags=tags),
            secure_bucket(
                "Data",
                key_arn=get_att("DataKey", "Arn"),
                allowed_principals=[deploy_role, sub("arn:${AWS::Partition}:iam::${AWS::AccountId}:role/data-platform-*")],
                archive_after_days=90,
                tags=tags,
            ),
            {
                "EventsTable": dynamodb_table("pk", "sk", tags=tags),
                "ConfigTable": dynamodb_table("id", tags=tags),
            },
            publish(PREFIX, {"key-arn": get_att("DataKey", "Arn"), "bucket-arn": get_att("DataBucket", "Arn")}),
        ),
    }

See the rendered YAML or in the book.

The consumer

The consumer declares the same paths as parameters. CloudFormation looks the values up when the consumer deploys, so nothing about the producer appears in the consumer's source except two paths:

def build() -> cfn.Template:
    params = cfn.Params()
    key_arn = params.ssm("DataKeyArn", f"{PREFIX}/key-arn")  # a Ref: the value is looked up at deploy time
    bucket_arn = params.ssm("DataBucketArn", f"{PREFIX}/bucket-arn")

    statements = grants.S3.Bucket.read.on(bucket_arn, key_arn=key_arn)

    return {
        "AWSTemplateFormatVersion": "2010-09-09",
        "Parameters": params.declarations(),
        "Resources": {
            "ReaderRole": iam.role(
                RoleName="data-platform-reader",  # matches the producer's allowed-principals pattern
                AssumeRolePolicyDocument=cfn.assume_role_policy(service("lambda.amazonaws.com")),
                Policies=[iam.RolePolicyProperty(PolicyName="read-data", PolicyDocument=cfn.policy_document(statements))],
            ),
        },
    }

key_arn and bucket_arn are ordinary Refs. The grant builds the bucket-and-objects statement from the bucket ARN and adds the key statement from the key ARN, so the role is allowed to read the bucket and decrypt what is in it. The rendered YAML shows the result.

Things to know

  • The producer deploys first. A parameter of an SSM type needs the SSM parameter to exist when the consumer is created or updated.
  • A consumer picks up a changed value on its next deploy, not immediately. That is usually what you want: nothing changes underneath a running stack.
  • The paths are the contract. Put them in one module, as PREFIX is here, and import it from both sides, or keep them in a shared defaults package, so a typo cannot make a consumer look at a path nobody publishes. The tests for this example check that everything the consumer reads, the producer publishes.
  • Publish ARNs, not names. A consumer that needs an ARN should not have to rebuild it from a name, and a bucket without a fixed name has none to rebuild from.