tomato·bazeldocs v0 · latest
Docs/Reference/Modules/rules_cloudformation

rules_cloudformation

Bazel rules for AWS CloudFormation templates — schema-derived typed Bazel rules via rules_jsonschema, Java-based linter via rules_java + the official cloudformation-template-schema.

MODULE.bazelstarlark
bazel_dep(name = "rules_cloudformation", version = "0.10.0")

View source & releases on GitHub ↗

Bazel rules for AWS CloudFormation templates — schema-derived typed Bazel rules via rules_jsonschema, plus a Java-based linter via rules_java. Each CFN resource type becomes a typed Bazel rule with one attr.* per JSON Schema property.

The user-facing Starlark surface mirrors the aws-cloudformation/cloudformation-template-schema exhaustively — every property the schema accepts is a typed Bazel attr.*. There’s no hand-curated subset and no allowlist of deferred fields. Drift is impossible by construction:

  • The canonical schema is fetched on-demand from aws-cloudformation/cloudformation-template-schema at a commit + sha256 pinned in cloudformation/private/extensions.bzl.
  • rules_jsonschema’s jsonschema_starlark_codegen emits cloudformation/cloudformation_rules.bzl — one rule() per AWS::* resource type definition in the schema, typed attr.* per property.
  • cloudformation/private/stack_aggregator.py merges the per-target JSON shards into one canonical template, rewriting the cfn_ref / cfn_getatt / cfn_sub / cfn_join / cfn_import_value sentinels and validating cross-resource names (conditions, DependsOn) so a typo fails at build time rather than at deploy.
  • The Java linter (port of cfn-lint patterns, built with rules_java) runs after rendering and reports semantic issues the schema alone cannot express (e.g. cross-property constraints, recommended-name conventions).

The hand-written part of the repo is small and scoped to things the schema can’t describe: graph aggregation, cross-stack reference resolution, and bazel run wrappers around aws cloudformation deploy. Codegen goes through rules_jsonschema’s plugin contract — see that repo’s plugin_contract.md if you want to swap a plugin for one of your own.

Status: v0.10.0

What v0.10 adds on top of v0.9.0:

  • cfn_joinFn::Join, which is what makes a list-valued Fn::GetAtt usable in a string slot. The slot that matters is a template Output’s Value:

    cloudformation_output(
        name = "ZoneNameServers",
        # AWS::Route53::HostedZone.NameServers is a List of String, and an
        # Output Value must be a string. Bare, this builds, deploys, and then
        # rolls the stack back with "Template format error: Every Value member
        # must be a string." — naming neither the output nor the attribute.
        Value = cfn_join(",", cfn_getatt("Zone", "NameServers")),
    )

    Two forms, and the difference is load-bearing. values may be a list of literals and/or sentinels, or a single sentinel that is itself list-valued:

    cfn_join("-", [cfn_ref("Environment"), "assets"])      # ["-", [{"Ref": …}, "assets"]]
    cfn_join(",", cfn_getatt("Zone", "NameServers"))       # [",", {"Fn::GetAtt": […]}]

    Passing the second form’s argument as a one-element list renders [",", [{"Fn::GetAtt": …}]] — a list of one element that happens to be a list, not a list-valued reference — and fails the same way the unjoined GetAtt does. Nested cfn_ref / cfn_getatt / cfn_import_value / cfn_find_in_map inside a list compose and are name-validated as usual; a nested cfn_join is rejected at load time.

What v0.9 adds on top of v0.8.0:

  • resource_depends_on — explicit DependsOn, for the ordering CFN cannot infer. cfn_ref/cfn_getatt already give CFN a dependency edge wherever a value flows between two resources, which covers most cases; this is for the ones where the ordering is real and no value flows. The canonical example, which every VPC hits:

    cloudformation_stack(
        name = "vpc",
        resources = [":Igw", ":IgwAttachment", ":PublicRt", ":PublicDefaultRoute"],
        # Both the route and the attachment merely Ref the gateway, so neither
        # depends on the other. Without this the route can be created first and
        # fails: "The gateway ID 'igw-…' does not exist or is not attached".
        resource_depends_on = {"PublicDefaultRoute": "IgwAttachment"},
    )

    Names are validated against the stack’s resources at build time.

What v0.6 added on top of v0.5.0:

  • Deploy wrapperscloudformation_up and cloudformation_down in cloudformation/deploy.bzl. bazel run :stack_up deploys via aws cloudformation deploy; bazel run :stack_down deletes via delete-stack.
  • AWS CLI toolchain abstraction — default toolchain auto-uses system aws on PATH. Consumers wanting hermeticity register an alternate toolchain (rules_multitool, http_file, sidecar container) for @rules_cloudformation//cloudformation/aws_cli:toolchain_type.
load("@rules_cloudformation//cloudformation:stack.bzl", "cloudformation_stack")
load("@rules_cloudformation//cloudformation:deploy.bzl",
     "cloudformation_up", "cloudformation_down")

cloudformation_stack(name = "app_stack", resources = [":Assets"])
cloudformation_up(
    name = "app_up",
    stack = ":app_stack",
    stack_name = "prod-app",
    region = "us-east-1",
    capabilities = ["CAPABILITY_IAM"],
)
cloudformation_down(name = "app_down", stack_name = "prod-app", region = "us-east-1")

# bazel run //path:app_up
# bazel run //path:app_down

Status: v0.5.0 (prior)

What v0.5 adds on top of v0.4.0:

  • Cross-resource refscfn_ref(name) and cfn_getatt(name, attr) Starlark helpers in cloudformation/stack.bzl produce sentinel strings the aggregator rewrites into {"Ref": ...} / {"Fn::GetAtt": [...]}. The aggregator validates that every ref points at a real resource in the stack — typos surface at Bazel-build time, not at AWS-deploy time.
load("@rules_cloudformation//cloudformation:defs.bzl",
     "cloudformation_aws_s3_bucket",
     "cloudformation_aws_s3_bucket_policy")
load("@rules_cloudformation//cloudformation:stack.bzl",
     "cfn_ref", "cfn_getatt", "cloudformation_stack")

cloudformation_aws_s3_bucket(name = "Assets", BucketName = "app-assets")
cloudformation_aws_s3_bucket_policy(
    name = "AssetsPolicy",
    Bucket = cfn_ref("Assets"),
    PolicyDocument = json.encode({
        "Statement": [{
            "Effect": "Allow",
            "Action": "s3:GetObject",
            "Resource": cfn_getatt("Assets", "Arn"),
        }],
    }),
)
cloudformation_stack(name = "stack", resources = [":Assets", ":AssetsPolicy"])

Status: v0.4.0 (prior)

What v0.4 adds on top of v0.3.1:

  • cloudformation_stack aggregator (cloudformation/stack.bzl) — collects typed-rule shards into a complete CFN template. Intrinsics splice into per-resource (Init) or template-level (Interface) Metadata. Phase-1 limitation: CFN logical ids = contributing rule’s label.name (name targets PascalCase). Cross- resource refs + deploy wrappers are deferred to later phases.

Example:

load("@rules_cloudformation//cloudformation:defs.bzl",
     "cloudformation_aws_s3_bucket")
load("@rules_cloudformation//cloudformation:stack.bzl",
     "cloudformation_stack")

cloudformation_aws_s3_bucket(name = "AppAssets", BucketName = "my-app-assets")
cloudformation_stack(
    name = "app_stack",
    description = "App backing services.",
    resources = [":AppAssets"],
)
# Output: `app_stack.json` — a deployable CFN template.

Status: v0.3.1 (prior)

What v0.3.1 adds on top of v0.3.0:

  • Hand-authored CFN metadata intrinsics in cloudformation/intrinsics.bzl: cloudformation_aws_cloudformation_init (cfn-init config-set tree) and cloudformation_aws_cloudformation_interface (template Metadata parameter-group block). These live outside the Resource Spec so they’re not covered by the auto-kinds pipeline; loaded separately: load("@rules_cloudformation//cloudformation:intrinsics.bzl", ...).

What v0.3 adds on top of v0.2.0:

  • Exhaustive coverage — 1582 typed rules across the entire pinned CFN Resource Specification (was 26 in v0.2 / one in v0.1). Powered by rules_jsonschema v0.3’s new auto-kinds flags (--kinds-pointer-base, --kinds-key-filter, the template flags) instead of hand-enumerating each --kind= mapping.
  • Adding a new resource type is a no-op — bump the upstream spec pin (cfn_sources_extension) and the new resources show up in the regenerated defs.bzl.
  • Single defs.bzl — the per-group files (storage.bzl, compute.bzl, …) collapsed into one generated artifact. Consumers load("//cloudformation:defs.bzl", ...) exactly like they did in v0.2; the loaded surface is a strict superset.
  • Breaking: the per-kind item-name attr is namespaced (aws_s3_bucket_name rather than the v0.2 short tag bucket_name) to prevent collisions in the 1500+ rule set. The CFN property attrs (BucketName, VersioningConfiguration, …) are unchanged.

v0.2.0 status (still shipped):

What v0.2 adds on top of v0.1.0:

  • 6 resource-type groups, 26 typed rules — every common AWS resource type for storage / compute / identity / messaging / observability / database. Each group is a separate cfn_assemble + jsonschema_starlark_codegen pair; consumers pick which group(s) to load. Re-exported from //cloudformation:defs.bzl. Adding a new resource is two edits in cloudformation/BUILD.bazel.
  • Docstring overlaycfn_overlay_descriptions layers AWS-endpoint per-resource property description fields onto the assembler-derived schemas before codegen. Trades URL-only attr docs for rich prose. v0.2 ships endpoint coverage for AWS::S3::Bucket; expanding coverage is a one-line pin per resource. Endpoints not yet pinned pass through unchanged.
  • Internal cleanup — dropped the unused cfn_template_schema_src use_repo from MODULE.bazel; trimmed the stale .bazelrc Java language-version pin.

v0.1.0 status (still shipped):

What ships:

  • Schema source — the upstream Java assembler (aws.cfn.codegen.json.Main from aws-cloudformation/cloudformation-template-schema) is built and run at build time against a sha-pinned snapshot of the AWS CloudFormation Resource Specification. The assembler sources are vendored (delomboked, see docs/SCHEMA_SOURCE.md for the Lombok trade-off); the spec is fetched by http_file at a sha256 pinned in cloudformation/private/extensions.bzl.
  • cfn_assemble custom rule (in cloudformation/private/assemble.bzl) runs the assembler with a synthesized YAML config — one region (us-east-1), one custom resource group (any AWS::* regex pattern), one emitted <group>-spec.json per invocation. The output is a consumer-ready JSON Schema.
  • Codegen pipelinerules_jsonschema’s jsonschema_starlark_codegen produces cloudformation/aws_s3_bucket.bzl from the assembled storage group’s schema. Committed + gated by a diff_test so CI fails on drift between the upstream schema source and the committed .bzl.
  • cloudformation_aws_s3_bucket — typed Bazel rule, one attr.* per CFN Resource Specification property, emits a JSON shard ready for a future cloudformation_stack aggregator. Re-exported from //cloudformation:defs.bzl.
  • End-to-end smoke (examples/smoke/) — declares an S3 bucket
    • a byte-stability diff_test on the emitted shard. Green.

Note on the schema source: v0.1.0 was retagged to swap an early per-resource AWS-endpoint approach for the upstream Java assembler. This keeps the source-of-truth aligned with cfn-lint and the CFN documentation, at the cost of a build-time Java compile. See docs/SCHEMA_SOURCE.md for the trade-offs and the Lombok wrinkle.

Deferred to v0.2 / v0.3 (see docs/ROADMAP.md):

  • Bundle tag class — opt into N resource types in one MODULE.bazel call.
  • cloudformation_stack aggregator (collects shards into one template.yaml via a Rust cfn-gen binary).
  • cloudformation_resource_ref for cross-stack refs (resolves stack outputs at build time, like docker_compose_oci_image_ref).
  • cloudformation_up / _down bazel run wrappers around aws cloudformation deploy / delete-stack.
  • Java linter port of cfn-lint patterns.

Planned architecture

Mirrors rules_docker_compose:

  • Hand-written rules (will be re-exported by cloudformation/defs.bzl):

    • cloudformation_stack — aggregator. Collects per-target resource/parameter/output/mapping shards from deps and renders one canonical template.yaml. Analogous to docker_compose.
    • cloudformation_resource_ref — resolves a cross-stack Ref / Fn::ImportValue / stack-output target at build time and overrides a referenced resource property in the rendered output. Analogous to docker_compose_oci_image_ref, which resolves OCI digests into a service’s image:.
    • cloudformation_up / cloudformation_downbazel run wrappers around aws cloudformation deploy and aws cloudformation delete-stack. Analogous to docker_compose_up / _down.
  • Schema-derived rules (generated, committed, diff_test-gated): one cloudformation_<resource_type> rule per AWS::* resource type, generated from the official CFN schema via jsonschema_starlark_codegen. Examples: cloudformation_aws_s3_bucket, cloudformation_aws_lambda_function, cloudformation_aws_ec2_instance. The full set is ~1000+ rules. See docs/SCHEMA_SOURCE.md for how the schema’s AWS::* type definitions map to Starlark rules.

  • Java linter — port of cfn-lint–style validation rules, packaged as a java_binary via rules_java. Runs over the rendered template.yaml at test time. Why Java: the upstream schema repo is itself a Maven project, so the schema’s intrinsic function tables and reference data are already in Java; reusing them avoids a parallel reimplementation.

  • Refs + labels — every shard produced by a schema-derived rule emits a CloudformationResourceInfo provider carrying its logical ID, type, and the labels of any resources it references. cloudformation_stack walks the provider graph to validate that every Ref resolves inside the stack (or is satisfied by a cloudformation_resource_ref shard).

Schema source (current)

The schema is sourced via the upstream Java assembler from aws-cloudformation/cloudformation-template-schema, run at build time against a sha-pinned snapshot of the AWS CloudFormation Resource Specification (us-east-1). The assembler sources are vendored under cloudformation/private/assembler_src/ in delomboked form (see docs/SCHEMA_SOURCE.md for the Lombok-vs-JDK context).

Install

.bazelrc:

common --registry=https://raw.githubusercontent.com/fastverk/bazel-registry/main/
common --registry=https://bcr.bazel.build/

MODULE.bazel:

bazel_dep(name = "rules_cloudformation", version = "0.2.0")

rules_jsonschema, rules_java, rules_jvm_external, and (transitively) a Rust toolchain are pulled in once the v0.1 codegen pipeline lands. The Maven artifacts for the assembler are pinned by maven_install.json; consumers don’t need to repin.

License

MIT.

Usage#

Real usage, taken from the module’s examples/.

examples/smoke/BUILD.bazel

load("@bazel_skylib//rules:diff_test.bzl", "diff_test")
load("@rules_cloudformation//cloudformation:condition.bzl", "cloudformation_condition")
load(
    "@rules_cloudformation//cloudformation:defs.bzl",
    "cloudformation_aws_ec2_instance",
    "cloudformation_aws_ec2_internet_gateway",
    "cloudformation_aws_ec2_route",
    "cloudformation_aws_ec2_route_table",
    "cloudformation_aws_ec2_vpc",
    "cloudformation_aws_ec2_vpcgateway_attachment",
    "cloudformation_aws_ecr_repository",
    "cloudformation_aws_route53_hosted_zone",
    "cloudformation_aws_s3_bucket",
    "cloudformation_aws_s3_bucket_policy",
)
load(
    "@rules_cloudformation//cloudformation:deploy.bzl",
    "cloudformation_down",
    "cloudformation_up",
)
load(
    "@rules_cloudformation//cloudformation:intrinsics.bzl",
    "cloudformation_aws_cloudformation_init",
    "cloudformation_aws_cloudformation_interface",
)
load("@rules_cloudformation//cloudformation:mapping.bzl", "cloudformation_mapping")
load("@rules_cloudformation//cloudformation:output.bzl", "cloudformation_output")
load("@rules_cloudformation//cloudformation:parameter.bzl", "cloudformation_parameter")
load(
    "@rules_cloudformation//cloudformation:stack.bzl",
    "cfn_equals",
    "cfn_find_in_map",
    "cfn_getatt",
    "cfn_if",
    "cfn_join",
    "cfn_ref",
    "cloudformation_stack",
)

package(default_visibility = ["//visibility:public"])

# Declare an S3 bucket via the schema-derived typed rule. The rule
# enforces every property's JSON Schema type at Bazel-loading time
# — pass an integer to BucketName and the load fails with a clear
# error. The output is a JSON shard the consumer's stack-aggregator
# rule would pick up.
cloudformation_aws_s3_bucket(
    name = "smoke_bucket",
    BucketName = "smoke-bucket",
    VersioningConfiguration = '{"Status": "Enabled"}',
    # Per-kind item-name attr. v0.3 namespaces this with the full
    # AWS_Service_Resource id (was `bucket_name` in v0.2 when kinds
    # were hand-enumerated with a short tag).
    aws_s3_bucket_name = "smoke-bucket",
)

# Lock down the shard's bytes so a regression in the schema codegen
# (e.g. an attr renamed, dropped, or stringified differently) shows
# up as a Bazel test failure.
diff_test(
    name = "smoke_bucket_shard_stable",
    file1 = "expected_smoke_bucket.json",
    file2 = ":smoke_bucket",
)

# AWS::CloudFormation::Init — a config-set tree attached to an
# EC2-like resource's `Metadata`. Hand-authored rule (lives outside
# the Resource Spec).
cloudformation_aws_cloudformation_init(
    name = "smoke_init",
    config_sets = '{"default": ["install", "config"]}',
    configs = json.encode({
        "install": {
            "packages": {"yum": {"httpd": []}},
        },
        "config": {
            "files": {
                "/etc/httpd/conf.d/site.conf": {
                    "content": "ServerName localhost\n",
                    "mode": "000644",
                },
            },
        },
    }),
    target_resource_name = "WebServer",
)

diff_test(
    name = "smoke_init_shard_stable",
    file1 = "expected_smoke_init.json",
    file2 = ":smoke_init",
)

# AWS::CloudFormation::Interface — top-level template metadata that
# groups Parameters for the AWS console UI.
cloudformation_aws_cloudformation_interface(
    name = "smoke_interface",
    parameter_groups = json.encode([
        {
            "Label": {"default": "Network"},
            "Parameters": [
                "VpcId",
                "SubnetIds",
            ],
        },
    ]),
    parameter_labels = '{"VpcId": {"default": "Which VPC?"}}',
)

diff_test(
    name = "smoke_interface_shard_stable",
    file1 = "expected_smoke_interface.json",
    file2 = ":smoke_interface",
)

# ---- cloudformation_stack: aggregate shards into a CFN template ----
# PascalCase target names because the v0.4 aggregator uses
# label.name as the CFN logical id (which must be alphanumeric).

cloudformation_aws_s3_bucket(
    name = "SmokeBucket",
    BucketName = "smoke-bucket",
    aws_s3_bucket_name = "smoke-bucket",
)

cloudformation_aws_ec2_instance(
    name = "WebServer",
    ImageId = "ami-0abcdef1234567890",
    InstanceType = "t3.micro",
    aws_ec2_instance_name = "web-server",
)

cloudformation_aws_cloudformation_init(
    name = "WebServerInit",
    config_sets = '{"default": ["install"]}',
    configs = json.encode({
        "install": {"packages": {"yum": {"httpd": []}}},
    }),
    target_resource_name = "WebServer",
)

cloudformation_aws_cloudformation_interface(
    name = "StackInterface",
    parameter_groups = json.encode([
        {
            "Label": {"default": "Compute"},
            "Parameters": ["InstanceType"],
        },
    ]),
)

cloudformation_aws_s3_bucket_policy(
    name = "SmokeBucketPolicy",
    Bucket = cfn_ref("SmokeBucket"),
    PolicyDocument = json.encode({
        "Version": "2012-10-17",
        "Statement": [{
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
# … truncated — see the repo for the full example

Rules & providers#

Generated with Stardoc from the module's .bzl sources.

from docs/ROADMAP.md

rules_cloudformation roadmap

Three milestones to first useful release. Numbering matches the rules_docker_compose cadence: v0.1 = schema-derived primitives, v0.2 = hand-written orchestration, v0.3 = deploy wrappers + linter.

v0.1 — schema fetch + codegen

Get the schema into the repo as Bazel-fetched data, run codegen, ship the first typed rule end-to-end.

  • Schema fetch. cloudformation/private/extensions.bzl defines an http_archive-backed module extension pinning aws-cloudformation/cloudformation-template-schema to a specific commit + sha256. Same shape as rules_docker_compose’s compose_spec_extension, except http_archive (not http_file) because the upstream packages the schema as part of a Maven build, not a single JSON file — see docs/SCHEMA_SOURCE.md.

  • MODULE.bazel wires rules_jsonschema. Adds bazel_dep(name = "rules_jsonschema", version = "0.2.0") and a use_extension block consuming the schema repo.

  • Codegen pipeline. A single jsonschema_starlark_codegen invocation reads the master Schema.template and emits cloudformation/cloudformation_rules.bzl — one rule() per AWS::* definition. Estimated ~1000+ rules (e.g. cloudformation_aws_s3_bucket, cloudformation_aws_lambda_function, cloudformation_aws_ec2_instance, cloudformation_aws_iam_role, cloudformation_aws_dynamodb_table, …). The committed .bzl is diff-tested against fresh codegen on every CI build, exactly like compose_rules.bzl in rules_docker_compose.

  • Smoke test. One end-to-end example: a single cloudformation_aws_s3_bucket target rendered through a placeholder aggregator into golden YAML. Validates that the schema-fetch → codegen → typed-attr → JSON-shard → YAML pipeline works for at least one resource type before the v0.2 aggregator arrives.

v0.2 — hand-written orchestration

Replace the placeholder aggregator with the real graph-walking implementation, plus cross-stack ref resolution.

  • cloudformation_stack. Aggregator rule. Collects shards from deps, validates the Ref graph (every logical ID referenced is defined or has a matching cloudformation_resource_ref), and renders one canonical template.yaml via a Rust cfn-gen binary. Same shape as docker_compose: shard JSON → typed struct (from rules_jsonschema’s jsonschema_rust_library) → canonical YAML. Stable key ordering so re-renders are byte-identical.

  • cloudformation_resource_ref. Cross-stack reference resolver. Given a target stack label and an output name, resolves to the exported value at build time (via either a checked-in outputs.json or a stack-output index file), then rewrites a property of a named resource in the rendered template to that value. Same role as docker_compose_oci_image_ref: a build-time override that turns a symbolic reference into a concrete pinned value before deploy.

  • Providers. CloudformationResourceInfo, CloudformationStackInfo, CloudformationResourceRefInfo.

v0.3 — deploy + lint

Ship the runtime wrappers and the Java-based linter.

  • cloudformation_up. bazel run wrapper that invokes aws cloudformation deploy --template-file <rendered> --stack-name <stack> against the rendered template, with --parameter-overrides flowing through from rule attrs. Same shape as docker_compose_up’s bazel run wrapper.

  • cloudformation_down. bazel run wrapper for aws cloudformation delete-stack. Mirrors docker_compose_down.

  • Java linter. Port of cfn-lint–style rules built with rules_java. Why Java: the upstream schema repo is a Maven project whose intrinsic-function and reference tables already exist in Java — reusing them is cheaper than reimplementing. Packaged as a java_binary invoked from a cloudformation_lint_test rule that runs against every cloudformation_stack.

from docs/SCHEMA_SOURCE.md

Schema source

Where rules_cloudformation’s typed rules ultimately come from.

Choice (v0.1)

We run the upstream Java assembler (aws.cfn.codegen.json.Main from aws-cloudformation/cloudformation-template-schema) at build time against a sha-pinned snapshot of the AWS CloudFormation Resource Specification. The assembler emits one JSON Schema per resource group; we feed the storage group’s output (scoped to AWS::S3.* in v0.1) through rules_jsonschema’s jsonschema_starlark_codegen to produce the typed Bazel rules.

Two artifacts are pinned in cloudformation/private/extensions.bzl:

  • aws-cloudformation/cloudformation-template-schema at commit 5d7815b14fd533c15c30f9046a76cdcb89afd32a (sha256 7f40b919bbea6109244903744262074f6afa32fdd780a6dca0540ef1b57bd774). Fetched but not on the compile path — see the Lombok wrinkle section below. Vendored under cloudformation/private/assembler_src/ in delomboked form.
  • The us-east-1 CloudFormationResourceSpecification.json at sha256 3bf0f8b5034b51c622da82f7cec9499112a40719f28fff5c6d2050a0c3a24459. Endpoint: https://d1uauaxba7bl26.cloudfront.net/latest/CloudFormationResourceSpecification.json.

How the build composes

@cfn_resource_spec//file:CloudFormationResourceSpecification.json


                  //cloudformation:assembled_storage   (cfn_assemble)

                          │  storage-spec.json (JSON Schema, ~280 KB,
                          │   223 AWS::S3.* + Tag definitions)

              //cloudformation:aws_s3_bucket_gen        (jsonschema_starlark_codegen)


                  aws_s3_bucket.bzl                     (committed, diff_test-gated)

cfn_assemble synthesizes a YAML config that points the assembler at the local pinned spec (the upstream bundled config.yml has all 25 region URLs hard-coded to the AWS CDN, which would defeat build-time reproducibility), narrows the region set to us-east-1 (the source-of-truth region), and declares a single custom group with the requested includes/excludes.

Lombok wrinkle

The upstream sources use Lombok 1.16.22 (released 2018). That release predates JDK 21+. The current Lombok release line (1.18.x) fails to initialize under JDK 25 with com.sun.tools.javac.code.TypeTag :: UNKNOWN, and Bazel 9.1.0’s rules_java toolchain runs the JavaBuilder on remotejdk25 by default without an easy override.

After running the prompt’s listed fallbacks (bump Lombok, pin --java_runtime_version=remotejdk_21, pin Lombok 1.18.36 — none of which sidestepped the issue because the JavaBuilder process itself runs on remotejdk25), we took the documented nuclear option: ran lombok.jar delombok against the upstream sources locally (java -jar lombok.jar delombok src/main/java -d cloudformation/private/assembler_src), stripped the @lombok.Generated annotations the delomboker leaves on each generated method, and committed the result.

Trade-off: refreshing the assembler from a newer upstream commit isn’t a one-line bump anymore — it’s a delombok + commit. In exchange, the build has no annotation-processor at compile time and no Lombok runtime dep, so it stays buildable on whichever JDK Bazel ships with going forward.

The patched upstream Codegen has one rules_cloudformation-local fix: newer CFN spec entries can have Type: Json with no PrimitiveType set, which the upstream code treats as a primitive but then NPEs on. The patch in Codegen.addPrimitiveType falls back to “Json” when the primitive name is null.

Known gap: registry-only resources

The legacy Resource Specification we pin covers ~1582 of the ~1600+ types AWS publishes. A handful of newer types (post-2023 additions — e.g. AWS::EC2::Image, AWS::EC2::SnapshotBlockPublicAccess) only ship via the newer CloudFormation Registry schema source (per-resource JSON files at schema.cloudformation.us-east-1.amazonaws.com/) and never landed in the legacy spec. Surfacing them would mean pulling from the Registry endpoint as a second source — same per-resource- file shape as the v0.0.1 source, but only for the resources the legacy spec is missing. v0.7+ work item; not on the current roadmap because demand is low (savvi-ops, the design’s stress test, hits ~1 of 87 in-use AWS types as a registry-only).

Alternatives considered

SourceWhy not chosen
Per-resource AWS endpoint (schema.cloudformation.us-east-1.amazonaws.com/<resource>.json)The v0.0.1 / first-cut v0.1 used this. It works but it’s a per-resource fetch (1200+ URLs to track for full coverage) and the schema content is the AWS resource-provider schema, which is divergent from the CloudFormation Resource Specification. Pivoting now keeps the same source-of-truth as cfn-lint and the CFN Linter docs.
aws-cloudformation/cloudformation-cli registry schemasSame per-resource shape, different repository. No advantage.
Hand-curated subsetrules_jsonschema’s whole point is avoiding drift between hand-written rules and upstream. Hard-no.

Refreshing

Three independent bumps:

  1. CFN Resource Specification (typical: track AWS-published spec versions):

    curl -fsSL https://d1uauaxba7bl26.cloudfront.net/latest/CloudFormationResourceSpecification.json | shasum -a 256
    # bump _RESOURCE_SPEC_SHA256 in cloudformation/private/extensions.bzl
    bazel run //cloudformation:update
  2. Upstream assembler source (rare: only when upstream changes how groups are computed or fixes a Codegen bug):

    # Compute the new tarball hash
    curl -fsSL https://github.com/aws-cloudformation/cloudformation-template-schema/archive/<commit>.tar.gz | shasum -a 256
    # Re-delombok + commit
    curl -fsSL https://projectlombok.org/downloads/lombok-1.18.36.jar -o /tmp/lombok.jar
    java -jar /tmp/lombok.jar delombok \
         <unpacked-src>/src/main/java \
         -d cloudformation/private/assembler_src
    find cloudformation/private/assembler_src -name '*.java' -exec sed -i '' 's/@lombok\.Generated//g' {} +
    # Bump _TEMPLATE_SCHEMA_COMMIT + _TEMPLATE_SCHEMA_SHA256 in extensions.bzl
    bazel run //cloudformation:update
  3. Maven deps (rare: only when upstream pom.xml shifts):

    # Edit MODULE.bazel's maven.install(artifacts=[...]) list
    REPIN=1 bazel run @cfn_assembler_maven//:pin

Path to ~1200 resource types

v0.1 covers AWS::S3::Bucket as a codegen smoke. v0.2 lifts the hard-coded resource set into a tag class:

cfn_resources = use_extension(
    "@rules_cloudformation//cloudformation/private:extensions.bzl",
    "cfn_sources_extension",
)
cfn_resources.bundle(
    name = "storage",
    includes = ["AWS::S3.*", "AWS::DynamoDB.*"],
)
cfn_resources.bundle(
    name = "compute",
    includes = ["AWS::EC2.*", "AWS::Lambda.*"],
)

so consumers opt into the resource set they care about — declaring 1200 typed Bazel rules per consumer when they use 10 is wasted analysis time. Bundling lands in v0.2 (see ROADMAP.md).

Conformance#

2 findings across 2 invariants. 10 contested atoms. See how gating works or the full report.

D2 a non-dev register_toolchains propagates to every transitive consumer why this matters ↗
versiontoolchain
0.10.0//cloudformation/aws_cli:default_aws_cli_toolchain
D3 a repo name CHOSEN on a SHARED extension must be namespaced why this matters ↗
repoextension
cfn_assembler_maven@rules_jvm_external//:extensions.bzl

Contested atoms

Third-party modules where this module resolves a different version than others do. Not a violation of anything this module did — it is the actionable form of a registry-level convergence finding, and the sentence a maintainer can act on.

AtomResolved hereElsewhere
apple_support 1.24.2 2.2.0 ×1
bazel_skylib 1.8.2 1.9.0 ×2
gazelle 0.36.0 0.30.0 ×50.44.0 ×10.51.0 ×3
nlohmann_json 3.6.1 3.12.0.bcr.1 ×1
protobuf 33.4 34.0.bcr.1 ×2
rules_go 0.60.0 0.39.1 ×5
rules_jvm_external 6.7 6.8 ×4
rules_python 1.7.0 2.0.1 ×1
rules_swift 3.1.2 3.6.1 ×1
upb 0.0.0-20220923-a547704 0.0.0-20230516-61a97ef ×1

Dependencies#

rules_cloudformation in the registry graph — what it depends on (left) and what depends on it (right).

Depends on

platforms1.0.0bazel_skylib1.8.2rules_jsonschema0.3.0rules_java9.1.0rules_jvm_external6.7rules_python1.7.0rules_shell0.6.1stardoc0.7.2dev

Used by (3 in the registry)

Versions#

13 published versions, newest first. Each resolves to an immutable, integrity-checked archive.

VersionIntegrity (sha256)Source archive
0.10.0 latest o5sLRyGyxPjE/Hfk… tag archive ↗
0.9.0 I8Es44u5ZBfnnopM… tag archive ↗
0.8.0 SYieaShtc2LOSfyO… tag archive ↗
0.7.1 Y30w0rVGTC6xVZX5… tag archive ↗
0.7.0 YrTOD/mfKbunLcvF… tag archive ↗
0.6.0 lK9CR3bCxLgA5txc… tag archive ↗
0.5.0 nUZF6b8LPapEgTSs… tag archive ↗
0.4.0 HhDekFeaZKE0d2ND… tag archive ↗
0.3.1 lNXr7dEGcvSD/TlS… tag archive ↗
0.3.0 h+8zn+m4lO8/wORV… tag archive ↗
0.2.0 x1Hzd5JrnQYmw5xO… tag archive ↗
0.1.0 BVKr+1RNGuCMPp7A… tag archive ↗
0.0.1 BhexA0JOtRDhYeA5… tag archive ↗

Changelog#

All notable changes to rules_cloudformation. The format is loosely Keep a Changelog — version headers mirror the published bazel-registry entries.

0.10.0 — cfn_join: Fn::Join, and list-valued GetAtt in an Output

  • New cfn_join(delimiter, values) Starlark helper in cloudformation/stack.bzl, rewritten by the aggregator into {"Fn::Join": [delimiter, <values>]}.

  • The gap it closes: a CFN Outputs.*.Value must be a string, and several Fn::GetAtt attributes are listsAWS::Route53::HostedZone.NameServers most visibly. Emitted bare, such an output builds clean, deploys, and then rolls the stack back with “Template format error: Every Value member must be a string.”, which names neither the output nor the attribute. Nothing upstream catches it: the typed rules check property types rather than output values, and the aggregator validates that a cfn_getatt names a real resource but has no notion of that attribute’s type.

  • Two forms, distinguished by the Starlark type of values and encoded in two separate sentinels (@@cfn:join: / @@cfn:joinlistref:), because both reach the aggregator as a flat string with nothing left to infer from:

    • a list of literals and/or sentinels → {"Fn::Join": [d, [v1, v2, …]]};
    • a single cfn_ref / cfn_getatt / cfn_import_value / cfn_find_in_map sentinel that is itself list-valued → {"Fn::Join": [d, {"Fn::GetAtt": […]}]}.

    Wrapping the second form’s argument in a one-element list renders [d, [{"Fn::GetAtt": …}]] — a list of one element that happens to be a list, not a list-valued reference — which fails the same template format error the join was added to fix, and reads as correct.

  • Nested sentinels inside a list are rewritten normally, so cfn_ref, cfn_getatt, cfn_import_value and cfn_find_in_map all compose, and their names are validated as anywhere else (… BucketName[1] points at a name that isn't in the stack).

  • The Join separator is the Record Separator (\036), deliberately not cfn_find_in_map’s Unit Separator (\037): a cfn_find_in_map nested in a join’s value list arrives carrying \037 inside its own sentinel, and sharing the character would shred it into fragments that match no prefix and render as literal strings — a silently vanished map lookup. Join-inside-Join is the case two characters cannot rescue, so cfn_join rejects it outright rather than mis-splitting it.

  • Rejected at Bazel-load time, each with a message naming the fix: an empty values list (an empty join evaluates to "", so a list comprehension that matched nothing would render an empty property rather than an error), a single value that is a literal or a string-valued sentinel (cfn_sub / cfn_base64 / cfn_join — CFN rejects all three as Fn::Join’s second argument), a nested cfn_join, a dict element such as cfn_if(...), and a delimiter or value containing the separator.

  • Closes #6.

0.9.0 — resource_depends_on: explicit DependsOn

  • cloudformation_stack: new resource_depends_on attr, a string_dict of resource label.name -> comma-separated resource names it must be created after. Emits DependsOn:. A single dependency renders as a bare string, several as a sorted list — both valid CFN, with the string form matching what hand-authored templates use so a generated template stays diffable against the one it replaces.
  • Only needed where the ordering is real but no value flows between the two resources, so CFN cannot infer it from a cfn_ref/cfn_getatt edge. The canonical case, which every VPC hits, is AWS::EC2::Route with a GatewayId: it and the AWS::EC2::VPCGatewayAttachment both merely Ref the gateway, so both depend on IT and neither on the OTHER. CFN is then free to create the route first, which fails with “The gateway ID ‘igw-…’ does not exist or is not attached”. AWS documents the dependency as required. Being a race, it can pass once and fail on the next rebuild — which is why the new smoke test locks a byte-stable template rather than relying on a deploy that worked.
  • Every name is validated against the stack’s resources at build time, matching resource_conditions. Unknown target, unknown dependency, self-dependency and malformed input are each a hard failure — a typo’d DependsOn is otherwise silent and reinstates the exact race it was added to remove.
  • Closes #1.

0.8.0 — cloudformation_up: in-place --use-previous-template

  • cloudformation_up: add a use_previous_template attribute. When set, stack is omitted and the launcher deploys with --use-previous-template instead of --template-file — an in-place update of an already-deployed stack that reuses its live template and leaves every unspecified parameter at its previous value. Pass the values to change via parameter_overrides (or bazel run … -- --parameter-overrides Key=Value). stack and use_previous_template are mutually exclusive; exactly one is required.
  • This lets a repo flip a single parameter (e.g. a container image tag) on a stack whose template another repo owns, without vendoring the whole template.
  • cloudformation_stack: Conditions + Mappings support. New cloudformation_condition and cloudformation_mapping rules emit top-level Conditions / Mappings blocks; new conditions / mappings / resource_conditions attrs on cloudformation_stack (the last attaches a Condition: to a resource, validated against the declared conditions). New cfn_find_in_map(...) sentinel (rewrites to Fn::FindInMap, accepts nested cfn_ref) and cfn_equals / cfn_and / cfn_or / cfn_not / cfn_if condition-function helpers. cfn_ref now also accepts CFN pseudo-parameters (AWS::Region, …) without failing name validation. This closes the gap that forced hand-authored YAML for templates using Conditions/Mappings.

0.6.0 — deploy wrappers (cloudformation_up / cloudformation_down)

  • New cloudformation_up and cloudformation_down executable rules in cloudformation/deploy.bzl. bazel run :foo_up deploys the stack via the aws CLI’s cloudformation deploy; bazel run :foo_down calls delete-stack. Extra argv after -- flows through to the aws CLI.
  • New aws CLI toolchain abstraction (cloudformation/aws_cli/toolchain_type.bzl). The default toolchain (@rules_cloudformation//cloudformation/aws_cli:default_aws_cli_toolchain, auto-registered) is a thin sh_binary around system aws on PATH — friendliest for dev + CI runners that already have aws CLI installed. Consumers can register their own toolchain (e.g. multitool-fetched, http_file, sidecar Docker) and Bazel’s toolchain resolution will prefer it without any changes to the deploy rules.
  • Deploy rule attrs: stack (the cloudformation_stack target; mandatory), stack_name (defaults to label.name), region, capabilities (list — e.g. ["CAPABILITY_IAM"]), parameter_overrides (string_dict).
  • Smoke targets exercise the launcher generation end-to-end (build-only — no real AWS calls in CI).

0.5.0 — cross-resource refs (cfn_ref / cfn_getatt)

  • New cfn_ref(resource_name) and cfn_getatt(resource_name, attribute) Starlark helpers in cloudformation/stack.bzl. They return sentinel strings (@@cfn:ref:Name, @@cfn:getatt:Name.Attr) that the aggregator rewrites into {"Ref": ...} / {"Fn::GetAtt": [...]} CFN intrinsic dicts at template-render time.
  • Aggregator validates that every sentinel points at a name in the stack’s resource set — typos fail the Bazel build with a $.Resources.X.Properties.Y breadcrumb instead of a deferred AWS-side template rejection.
  • Smoke stack now exercises both helpers: a BucketPolicy whose Bucket is cfn_ref("SmokeBucket") and whose policy statement references cfn_getatt("SmokeBucket", "Arn"). Expected JSON updated.

0.4.0 — cloudformation_stack aggregator

  • New cloudformation_stack rule (cloudformation/stack.bzl) — takes typed-rule shards (from defs.bzl) and intrinsic shards (from intrinsics.bzl) and renders one CFN template: Resources.X = {Type, Properties} per resource, Init shards spliced under Resources.<target>.Metadata.AWS::CloudFormation::Init, Interface shards spliced under template-level Metadata.AWS::CloudFormation::Interface. Optional description attr fills the template’s Description field.
  • New generated cfn_types.bzl (snake-id → AWS::Service::Resource map for all 1582 spec rules) — needed because snake-case loses the segment boundaries that distinguish e.g. ApplicationAutoScaling::ScalableTarget from Application::AutoScalingScalableTarget. Regenerated alongside defs.bzl via bazel run //cloudformation:update.
  • Smoke stack in examples/smoke aggregates an S3 bucket + EC2 instance + Init metadata + Interface block into one template, byte-stable diff_test against committed expected JSON.
  • Phase-1 limitations — the aggregator uses each contributing rule’s label.name as the CFN logical id (so Bazel targets must be PascalCase and alphanumeric). The <kind_id>_name custom-name attr on the typed rules is unused for now. Cross-resource refs (Ref / Fn::GetAtt) and deploy wrappers (cloudformation_up / _down) are deferred to later phases.

0.3.1 — CFN intrinsics (Init, Interface)

  • New cloudformation/intrinsics.bzl with two hand-authored rules for the CFN metadata directives that live outside the Resource Spec:
    • cloudformation_aws_cloudformation_init — emits the AWS::CloudFormation::Init config-set tree (configSets + named config blocks) that cfn-init interprets at instance boot. Carries a target_resource_name for the future stack aggregator to attach the shard under the right resource’s Metadata.
    • cloudformation_aws_cloudformation_interface — emits the AWS::CloudFormation::Interface template-level metadata that groups Parameters into labelled sections for the AWS console UI.
  • Smoke tests in examples/smoke cover both with byte-stable diff_test gates.

Purely additive — no changes to the spec-derived rules in defs.bzl. Consumers wanting the intrinsics load("@rules_cloudformation//cloudformation:intrinsics.bzl", ...) alongside the existing defs.bzl load.

0.3.0 — exhaustive coverage via rules_jsonschema auto-kinds

  • Switched from 6 hand-curated service groups to a single assembler invocation covering the entire CFN Resource Spec, driven by rules_jsonschema v0.3’s new auto-kinds flags (--kinds-pointer-base, --kinds-key-filter, the template flags). Result: 1582 typed Bazel rules across every AWS::Service::Resource in the pinned spec (was 26).
  • Adding a new resource type is now a no-op — bump the upstream spec pin in cfn_sources_extension and the new resources show up in the regenerated defs.bzl.
  • defs.bzl is now the generated artifact (was a hand-written re-export shim over 6 per-group .bzl files). The per-group files (storage.bzl, compute.bzl, …) are removed.
  • Breaking: the per-kind item-name attr is now namespaced with the full aws_service_resource id (e.g. aws_s3_bucket_name) rather than the v0.2 short tag (bucket_name). The change prevents collisions across the 1500+ resource set; the v0.2 short-tag form wasn’t unique once coverage expanded past one service per “kind concept” (S3, EC2, and S3Outposts all have “bucket”-ish resources, etc.).
  • Endpoint-description overlay (cfn_overlay_descriptions) preserved against the new single assembled_all target; AWS::S3::Bucket retains its rich property docs. Endpoint coverage for additional resources is still pin-per-resource in cloudformation/private/extensions.bzl.

0.2.0 — 6 groups, 26 typed rules, docstring overlay

  • Scaled the codegen pipeline from one S3 Bucket rule to 26 typed Bazel rules across 6 resource-type groups:
    • storage — AWS::S3::Bucket / BucketPolicy / AccessPoint
    • compute — Lambda Function/Permission, ECS Service/Cluster/TaskDefinition, ECR Repository
    • identity — IAM Role/Policy/ManagedPolicy/User/Group
    • messaging — SQS Queue/QueuePolicy, SNS Topic/Subscription/TopicPolicy, EventBridge EventBus/Rule
    • observability — CloudWatch Logs LogGroup/LogStream, CloudWatch Alarm
    • database — DynamoDB Table/GlobalTable
  • One cfn_assemble + jsonschema_starlark_codegen pair per group, emitting cloudformation/<group>.bzl. Each gated by its own diff_test. bazel run //cloudformation:update regenerates all groups.
  • cfn_overlay_descriptions (cloudformation/private/overlay.bzl) layers AWS-endpoint per-resource property descriptions on top of the assembler-derived schema before codegen. Trades URL-only attr docs for rich prose. v0.2 pins endpoint coverage for AWS::S3::Bucket; expanding to other resources is a one-line pin per resource in cloudformation/private/extensions.bzl.
  • defs.bzl re-exports every group’s rules + providers, so consumers only need one load(...) call.
  • Internal cleanup: dropped unused cfn_template_schema_src from use_repo; trimmed stale Java language-version pins from .bazelrc (kept only the runtime pin needed for the remote JDK).

0.1.0 — (retag) Java-assembler-based schema source

  • Pivoted the schema source from the per-resource AWS endpoint (schema.cloudformation.us-east-1.amazonaws.com/*.json) to the upstream Java assembler in aws-cloudformation/cloudformation-template-schema, run at build time against a sha-pinned snapshot of the AWS CloudFormation Resource Specification (us-east-1, sha256 3bf0f8b5...). This aligns the source of truth with cfn-lint and the CFN Linter docs.
  • New module extension cfn_sources_extension (in cloudformation/private/extensions.bzl) — http_archives the upstream source tarball (commit 5d7815b1...) and http_files the Resource Specification.
  • New cfn_assemble custom rule (in cloudformation/private/assemble.bzl) — runs the assembler with a synthesized YAML config that pins region → local-file URI and declares a single custom resource group. Output: one <group>-spec.json consumable by jsonschema_starlark_codegen.
  • Assembler sources are vendored in delomboked form under cloudformation/private/assembler_src/ because Bazel 9.1.0’s rules_java toolchain runs JavaBuilder on remotejdk25, and Lombok has no JDK-25-compatible release. The trade-off + refresh procedure is in docs/SCHEMA_SOURCE.md.
  • One local Codegen patch: addPrimitiveType falls back to “Json” when the upstream-treated-as-primitive propType is null (newer CFN spec entries can have Type: Json with no PrimitiveType set — upstream NPEs on these).
  • cloudformation_aws_s3_bucket rule re-derived from the assembled storage group’s schema; the emitted JSON shard byte-matches the v0.0.1/early-v0.1 output for the smoke test inputs.

0.0.1 — scaffold

  • Initial scaffold via rels scaffold. No public API yet.

← All modules