rules_cloudformation
API reference, generated from the module’s .bzl docstrings (stardoc).
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.bzldefines anhttp_archive-backed module extension pinningaws-cloudformation/cloudformation-template-schemato a specific commit + sha256. Same shape asrules_docker_compose’scompose_spec_extension, excepthttp_archive(nothttp_file) because the upstream packages the schema as part of a Maven build, not a single JSON file — seedocs/SCHEMA_SOURCE.md. -
MODULE.bazel wires
rules_jsonschema. Addsbazel_dep(name = "rules_jsonschema", version = "0.2.0")and ause_extensionblock consuming the schema repo. -
Codegen pipeline. A single
jsonschema_starlark_codegeninvocation reads the masterSchema.templateand emitscloudformation/cloudformation_rules.bzl— onerule()perAWS::*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.bzlis diff-tested against fresh codegen on every CI build, exactly likecompose_rules.bzlinrules_docker_compose. -
Smoke test. One end-to-end example: a single
cloudformation_aws_s3_buckettarget 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 fromdeps, validates theRefgraph (every logical ID referenced is defined or has a matchingcloudformation_resource_ref), and renders one canonicaltemplate.yamlvia a Rustcfn-genbinary. Same shape asdocker_compose: shard JSON → typed struct (fromrules_jsonschema’sjsonschema_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-inoutputs.jsonor a stack-output index file), then rewrites a property of a named resource in the rendered template to that value. Same role asdocker_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 runwrapper that invokesaws cloudformation deploy --template-file <rendered> --stack-name <stack>against the rendered template, with--parameter-overridesflowing through from rule attrs. Same shape asdocker_compose_up’sbazel runwrapper. -
cloudformation_down.bazel runwrapper foraws cloudformation delete-stack. Mirrorsdocker_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 ajava_binaryinvoked from acloudformation_lint_testrule that runs against everycloudformation_stack.
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-schemaat commit5d7815b14fd533c15c30f9046a76cdcb89afd32a(sha2567f40b919bbea6109244903744262074f6afa32fdd780a6dca0540ef1b57bd774). Fetched but not on the compile path — see the Lombok wrinkle section below. Vendored undercloudformation/private/assembler_src/in delomboked form.- The us-east-1
CloudFormationResourceSpecification.jsonat sha2563bf0f8b5034b51c622da82f7cec9499112a40719f28fff5c6d2050a0c3a24459. 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
| Source | Why 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 schemas | Same per-resource shape, different repository. No advantage. |
| Hand-curated subset | rules_jsonschema’s whole point is avoiding drift between hand-written rules and upstream. Hard-no. |
Refreshing
Three independent bumps:
-
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 -
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 -
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).