cdktn-aws · v0.1.1 published · aws provider 6.62.0 · 2026-09-01
Regrouping terraform-provider-aws from one jsii submodule per resource into one per service makes the library ~24× cheaper to cold-start, keeps the API surface byte-comparable — and is the only shape in which a Go distribution can exist at all after cdk-terrain#387.
go get of the prebuilt cdktn-provider-aws-go broke permanently at tag aws/v24.6.0. The Go toolchain, module proxy and checksum database all enforce a 524,288,000 B cap on a module's source tree (golang.org/x/mod/zip, MaxZipFile):
| tag | module source bytes | outcome |
|---|---|---|
| aws/v24.5.0 | 523,366,927 | PASS last passing — 99.82% of the cap |
| aws/v24.6.0 | 525,519,743 | FAIL first failing — 100.24% |
Growth is ~1–2 MB per upstream provider minor. There is no client-side workaround for v24.6.0 and after: the cap lives in x/mod/zip, which is vendored into cmd/go itself — the proxy, the sumdb and the consumer's own toolchain all apply it, so no flag, vendor directory, private proxy or GOPRIVATE direct fetch restores it (a direct-mode go mod download fails identically). Every future monolithic Go release of this provider stays broken, and the margin only goes one way. Issue #387 proposed a per-service module split as the structural fix; this PoC is that fix, taken seriously.
M0 — the grouping map. The aws provider doesn't encode its service in the resource name (aws_lb, aws_alb, aws_db_instance), so grouping has to come from outside the schema. The only machine-readable grouping upstream maintains is the hand-written subcategory: docs frontmatter. groups.json is that mapping, mined once and then curated by hand: 257 groups covering 100% of the aws 6.62.0 schema — 1,711 resources, 679 data sources, 10 ephemeral resources — no misc bucket, an explicit alias table, and two CI gates (coverage; group-moves as marked breaking changes). The miner is deliberately not wired into automation: an upstream editorial rename lands on us as a submodule rename.
M1 — the generator. tools/aws2cdk forks the awscc generator, which itself vendors cdk-terrain's @cdktn/provider-generator verbatim. One standalone jsii package per group; classes named PascalCase-of-the-full-terraform-type (AwsLambdaFunction, DataAwsLb); every nested block type mounted inside the resource class's merged namespace — which is what keeps struct-name collisions at zero and makes the 400-export structs<N>.ts sharding rule unnecessary. Everything the awscc fork had trimmed was restored verbatim from the vendored originals, and a runtime-contract diff against an unmodified vendored build is a permanent gate.
M2 — full generation and the monolith. All 258 packages (257 groups + provider): 2,401 classes, 9,856 nested property types, 2,660 .ts files / 84.7 MiB, emitted in ~3 s. The published npm/PyPI shape is one jsii assembly, @cdktn/aws: 39.5 s to compile, 0 JSII3 / 0 JSII6, a 150,147,226 B assembly against the reference's 150,318,168 B. generated/hashes.json carries a per-group content hash — the lever the release step uses.
M3 — the Go fleet. 258 sibling modules under github.com/cdktn-io/cdktn-aws-go, no root go.mod, built by one pinned jsii 5.9.53 + pacmak 1.140.0 in 105.3 s at 12-way; go mod tidy + go build 258/258. Isolation proven on the code pacmak writes: 68,402 .go files, 137,510 import specifiers, 0 cross-group imports, 0 root-level shared packages. Sizes measured with zip.CheckDir — the function go mod download itself runs. A 36-module consumer synthesises with validations on in 324 ms, asserting on the emitted cdk.tf.json.
M4 — publishing, for real. The two-pipeline release design (monolith → npm/PyPI/Maven/NuGet; fleet → per-group prefixed tags, changed-groups-only via the hash diff) has now executed: v0.1.1 is live on all four registries and all 258 <group>/v0.1.1 fleet tags resolve through proxy.golang.org with sumdb verification. The first release run flushed out one real bug the dry-run could not — the release version wasn't stamped through the Go fleet — fixed, with re-dispatch made idempotent.
Measured against @cdktn/provider-aws 25.3.0 on one machine (Apple silicon, 48 GiB, 18 cores). Full method, every command and every caveat: docs/m2-metrics.md.
| @cdktn/aws · 258 submodules | reference · 2,402 | delta | |
|---|---|---|---|
| JS cold start — top-level import + reach two services | 45.2 ms | 1,100.0 ms | −95.9% (24.3×) |
require.cache entries after that | 206 | 2,572 | −92.0% |
| node RSS after that | 81 MB | 831 MB | −90.3% |
JS deep path (lib/<resource>) | 45.4 ms | 29.1 ms | +56% — we lose |
| Python import + reach two services | 429.6 ms | 1,263.7 ms | −66.0% (2.9×) |
sys.modules if every submodule is touched | 833 | 5,121 | −83.7% |
| Doc files (5 languages) | 1,290 projected | 12,015 measured | −89.3% |
.jsii assembly | 150,147,226 B | 150,318,168 B | −0.1% |
| Types in the assembly | 30,714 | 30,714 | identical |
npm lib/ bytes | 312,316,455 B | 319,570,579 B | −2.3% |
| Python wheel | 62,419,552 B | 69,098,649 B | −9.7% |
Read honestly, this table says three things:
export * as lambda from './lambda' to an eager require; the reference does the same thing 2,405 times. Eager-vs-eager the two shapes are within 12%. The monolith build rewrites the compiled barrel into one self-replacing getter per submodule before pacmak runs, so the npm tarball embedded in the Python wheel carries the lazy barrel too (1 require against 2,402). That is the whole Python win — same mechanism, not a second one.pip install; the headline), −87.1% / 7.7× once a type index exists. A superseded −9.1% figure in the repo's history was an eager pre-lazify wheel measured against a correct reference — an artifact of a build-order mistake, caught by a one-line grep -c 'require(' on the shipped wheel.A shape-hash census finds 5,357 duplicate structs: 4,579 within a group and 778 cross-group — where deduplication is structurally forbidden, because a shared struct needs a shared module root and the zero-cross-group-imports gate exists to guarantee there is none. That guarantee is exactly what makes each group independently hashable, taggable and releasable as a Go module. 778 duplicated definitions is the price, and it is cheap.
| bytes | % of the 524,288,000 B cap | |
|---|---|---|
Sum of the 258 fleet modules (measured, zip.CheckDir) | 567,214,581 | 108.2% summed, not a monolith |
| …minus per-module overhead a monolith would not pay | −43,494,025 | 258 embedded tarballs, LICENSE/README/version sets |
| Monolith floor | 523,720,556 | 99.89% |
| M2 projection from seven packed groups (top of the range) | 644,538,536 | 122.9% |
Largest single fleet module (awslexv2models) | 60,722,732 | 11.58% |
Median fleet module (awslocation) | 956,808 | 0.18% |
The honest statement: an unsplit aws Go monolith lands somewhere from ~99.9% of the cap upward. 99.89% is a floor in the strict sense — the one unquantified adjustment (a single 2,401-class package must disambiguate flat-namespace collisions) can only push it up. At the low end this is a margin call rather than "undownloadable": the monolith would ship with a fraction of a percent of headroom on a tree that grows 1–2 MB per upstream minor, would cross on some near-future release, and would then have no recourse short of the split. That is a real argument for the split; it is not the stronger claim, and this report does not make the stronger claim.
The per-module gate is not a projection at all. All 258 modules are measured by the same function go mod download runs: 1 module ≥ 10% of the cap, 17 in 1–10%, 158 in 0.1–1%, 82 under 0.1%, 0 over. What the split buys, priced: a consumer of one group downloads the median 956,808 B — 60.7 MB in the very worst case — instead of the whole library.
Since v0.1.1, this is measured against the real registry, not a projection. The example consumer resolves all 36 of its cdktn-aws-go modules through proxy.golang.org with sumdb verification — go mod tidy in 41 s on a cold module cache, no go.work, no replace directives — builds in 9.3 s, synthesises in 2.3 s with provider validation on, and per-module first-touch cost lands at ~3 ms, matching the M3 measurements. All 35 group resource types appear in the emitted cdk.tf.json, with a provider-defined function (${provider::aws::arn_build(…)}) rendering in a real output.
Tag growth is answered, not hand-waved. 258 tags per release would be ~13k a year at weekly cadence. hashes.json + the release planner tag only the groups whose content hash moved; cadence is tied to upstream provider bumps; shard repositories remain a last resort. The rejected design (one whole-library core module) could do none of this — and its consumer paid 849 ms to load that core before constructing anything, against 6.2 ms per group actually imported here.
The home is packages/@cdktn/provider-generator/src/get/generator/**. The PoC's vendored tree is that code copied verbatim with one mechanical change, and it drives the reference side of a contract diff — so the delta below is exactly what would have to become supported behaviour rather than a fork.
AwsWafv2WebAcl vs AwsWafv2WebAclRule); the fix iterates whole-package prefixes to a fixed point. This bug only appears at scale.zip.CheckDir size gate.Stays provider-specific: groups.json and its curation record, the slug policy, the frontmatter miner, aliases and manual assignments. The mechanism generalises; the mapping does not — a provider that encodes its service in the resource name (awscc) needs no external map at all.
targets.go.moduleName, targets.go.packageName. All 258 asserted, not sampled (1,812 assertions), rules restated in the test rather than imported from the generator./vN event — at semver major ≥ 2, pacmak appends /vN to all 258 module paths at once; Go's import-path-is-identity rule offers no migration.encoding/json), so this is inert in practice — but a differently-ordered dump would move all 258 hashes at once and falsely re-tag every module. The release planner warns loudly on "all N groups changed" for exactly this reason.proxy.golang.org with sumdb verification. Java and .NET, previously never packed, built and published cleanly from the 258-submodule assembly.lambda + s3 is 205 modules against the reference's 144 for two resources.All three were made before the first tag, and are now permanent:
packageName convention — aws + slug with underscores stripped (lex_v2_models → awslexv2models). It is the directory, the tag prefix and the last element of every import path — locked in by the 258 published <group>/v0.1.1 tags.