- Original Author(s):: @omarqureshi
- Tracking Issue: aws/jsii#5129
- API Bar Raiser: @{BAR_RAISER_USER}
This RFC proposes the introduction of native Ruby language bindings for the AWS Cloud Development Kit (CDK).
The current state of the AWS CDK forces Ruby-centric engineering organizations into an adoption bottleneck. While the CDK natively supports TypeScript, Python, Java, Go, and C#, organizations whose core application stacks and internal expertise are built entirely on Ruby (e.g., large-scale Rails deployments) face significant friction.
To manage their infrastructure as code via the CDK, these developers must accept a high cognitive load—managing disjointed local toolchains, foreign dependency managers (e.g., npm, pip), and isolated linting configurations. This fragmentation creates severe context-switching overhead. While Python is often cited as an alternative due to its readable syntax, forcing a Ruby team to adopt Python solely for infrastructure introduces artificial training costs, fractures internal shared libraries, and prevents organizations from leveraging their deeply established testing and automation ecosystems.
By introducing Ruby support to jsii, we unlock native AWS CDK capabilities for a massive, enterprise-grade developer community, allowing them to treat infrastructure as a first-class citizen using their primary language.
- Native Dependency Management: Ruby developers will manage their infrastructure dependencies using Bundler (
Gemfile) and distribute reusable infrastructure constructs as standard Ruby Gems published to RubyGems.org. - Familiar Tooling: Developers will execute CDK pipelines using familiar testing frameworks (e.g., RSpec, Minitest), entirely removing the need to interact directly with Python or Java toolchains.
- Idiomatic Syntax: Complex cloud architecture topologies will be declared using standard Ruby idioms—such as keyword arguments and snake_case naming—preserving the fluid design patterns characteristic of the language.
The impact of this proposal is strictly additive. Because jsii isolates language-specific bindings to decoupled code-generation targets inside the jsii-pacmak compiler, the introduction of a Ruby target introduces zero breaking changes, performance regressions, or syntax alterations to existing language modules.
The compiler change is limited to an additive schema extension: targets.ruby configuration validation in project-info.ts / assembler.ts (aws/jsii-compiler#2663). The intermediate .jsii assembly format itself is entirely unchanged — existing assemblies gain Ruby support with no recompilation.
Unlike a speculative design document, this RFC is backed by a working implementation; every generated-code example below is real output from the current generator:
- 123/123 standard compliance suite tests pass, reported through the same
tools/jsii-compliancematrix that tracks Java (97.5%) and Go (78.9%) — see the generated compliance report in the specification docs. - 243 runtime test examples (compliance + unit) run green in CI on every push, alongside generated-code snapshot coverage in
jsii-pacmak's cross-language test harness. - The full
aws/jsiimonorepo CI (build, unit tests across the OS/language matrix, pacmak integration againstaws-cdk-lib) passes end-to-end on the implementation branch. - Validated end-to-end at
aws-cdk-libscale. The generator emits the entireaws-cdk-lib(20,351 types across 613 submodules) in ~10 seconds. The generated assembly loads lazily —require 'aws-cdk-lib'registers ~20,400 autoload entries and eager-defines zero types (see Lazy loading under Detailed Design) — and a realApp → Stackwith an S3 bucket, a VPC and a Lambda function synthesizes correct CloudFormation through the kernel: unresolved attribute tokens resolve toFn::GetAttintrinsics, struct keyword arguments and enums round-trip,vpc.public_subnetshydrates subnet proxies the program never referenced by constant, and cross-assemblyconstructs.Nodereferences resolve. This is the full path a CDK user exercises, on the real library, not a fixture. - A real production deployment consumes the generated gems today: the stack at https://github.com/omarqureshi/blog compiles, synthesizes and deploys via the
aws-cdk-libRuby bindings, spanning six service modules (S3, CloudFront, ACM, Route53, Cognito, DynamoDB) — breadth across the generated API surface, not a single cherry-picked construct. - The full test matrix passes on Linux, macOS and Windows runners, and the runtime reports itself via the standard
JSII_AGENTmechanism (Ruby/<version>), integrating with jsii's existing runtime telemetry.
To map the jsii specification to native Ruby, the code generator (jsii-pacmak) translates each jsii type-system feature into an idiomatic Ruby construct:
| jsii concept | Ruby construct |
|---|---|
| Class | class Foo < Jsii::Object (or its jsii base class) |
| Behavioral interface | module IFoo (mixed in with include) |
| Struct (datatype interface) | Value class < Jsii::Struct, kwargs constructor; plain Hashes coerced at call sites |
| Enum | module Foo containing Jsii::Enum constants |
| Static member | Singleton method on the defining class |
camelCase member |
snake_case method |
| Namespace / submodule | PascalCase nested modules (configurable acronyms) |
| Optional value | nil / omitted keyword argument |
Promise<T> |
Synchronous call (kernel begin/end bridged internally) |
Where the sibling runtimes faced the same mapping questions, Ruby's positions are deliberate choices within the family:
| Concern | Python | Java | Go | Ruby |
|---|---|---|---|---|
| Struct passing | keyword arguments | generated Builders | struct types with pointer fields | value classes + hash/kwargs coercion |
| Optionals | None defaults |
@Nullable / builder omission |
pointer types | nil keyword defaults |
| Guest interface impl | @jsii.implements decorator |
implements |
interface embedding | include Module |
| Enums | enum.Enum |
Java enum |
typed string constants | Jsii::Enum constants |
Enum members are wrapper objects, not raw strings: they must serialize as $jsii.enum wire envelopes carrying their fully-qualified name to round-trip through the kernel. Generated output:
module AllTypesEnum
MY_ENUM_VALUE = Jsii::Enum.new("jsii-calc.AllTypesEnum", "MY_ENUM_VALUE")
YOUR_ENUM_VALUE = Jsii::Enum.new("jsii-calc.AllTypesEnum", "YOUR_ENUM_VALUE")
THIS_IS_GREAT = Jsii::Enum.new("jsii-calc.AllTypesEnum", "THIS_IS_GREAT")
endMembers compare by value (fqn + name), so values received from the kernel are == to the generated constants. Integer-backed and string-backed TypeScript enums are handled identically — the wire format only ever carries the member name.
Behavioral interfaces map to Ruby modules, implemented via include. This is Ruby's native contract idiom (Comparable, Enumerable) — but here the include is also load-bearing for the wire protocol: jsii is nominally typed, and when a native Ruby object is passed to the kernel, the runtime gathers the jsii FQNs of every included interface module and registers them (with an overrides table) so the JavaScript side builds a proxy honoring exactly those contracts. Duck typing cannot cross the process boundary; include IBellRinger is the one-line nominal declaration the protocol requires.
class MyBellRinger
include JsiiCalc::IBellRinger
def your_turn(bell)
bell.ring # `bell` is a live proxy; this call re-enters the kernel
end
end
JsiiCalc::ConsumerCanRingBell.static_implemented_by_public_class(MyBellRinger.new) # => trueRequired members are validated at construction time (fail-fast missing required method/property: ...) instead of erroring later inside a Node-side callback.
Structs generate as value classes inheriting Jsii::Struct, with keyword-argument constructors, per-member runtime type validation, and content-based ==/hash. Real generated output:
class CalculatorProps < Jsii::Struct
Jsii::Object.register_jsii_fqn("jsii-calc.CalculatorProps", self)
# @param initial_value [Numeric, nil] The initial value of the calculator.
# @param maximum_value [Numeric, nil] The maximum value the calculator can store.
def initialize(initial_value: nil, maximum_value: nil)
@initial_value = initial_value
Jsii::Type.check_type(@initial_value, ..., "initialValue") unless @initial_value.nil?
@maximum_value = maximum_value
Jsii::Type.check_type(@maximum_value, ..., "maximumValue") unless @maximum_value.nil?
end
# The initial value of the calculator.
#
# @return [Numeric, nil]
# @note Default: 0
attr_reader :initial_value
endAt call sites, plain Ruby Hashes are accepted and coerced into struct instances — recursively, including elements of arrays and maps, and the single struct arm of unambiguous unions — so the idiomatic hash-literal style works everywhere. Ruby 3's keyword-to-positional-hash conversion makes both spellings equivalent:
Bucket.new(self, 'MyBucket', { versioned: true }) # explicit hash
Bucket.new(self, 'MyBucket', versioned: true) # trailing keywordsCoercion matters beyond ergonomics: an uncoerced Hash would serialize with its literal snake_case keys while the kernel expects the struct's camelCase wire form — so coercion happens before validation and serialization.
jsii structs support multiple inheritance ("diamond" hierarchies); Ruby classes do not. The generator subclasses the first declared parent and records the rest in a conformance registry on Jsii::Struct, which is_a?, kind_of? and case/when dispatch consult — so a DiamondInheritanceTopLevelStruct instance is an is_a? match for both mid-level parents. Equality remains exact-class to keep == symmetric.
Generated classes inherit Jsii::Object (the RPC proxy base) and self-register their FQN for ref hydration. Members carry YARD documentation generated from the assembly docs:
# A calculator which maintains a current value and allows adding operations.
class Calculator < ::JsiiCalc::Composition::CompositeOperation
self.jsii_fqn = "jsii-calc.Calculator"
Jsii::Object.register_jsii_fqn("jsii-calc.Calculator", self)
# Creates a Calculator object.
#
# @param props [JsiiCalc::CalculatorProps, nil] Initialization properties.
def initialize(props = nil)
props = props.is_a?(Hash) ? ::JsiiCalc::CalculatorProps.new(**props.transform_keys(&:to_sym)) : props
Jsii::Type.check_type(props, ..., "props") unless props.nil?
Jsii::Object.instance_method(:initialize).bind(self).call(props)
end
# Adds a number to the current value.
#
# @param value [Numeric]
# @return [void]
def add(value)
Jsii::Type.check_type(value, ..., "value")
jsii_call_method("add", [value])
end
end(Type metadata is embedded as Base64-encoded JSON literals — elided as ... above — so no jsii-supplied string can ever inject into generated source via #{} interpolation.)
Inheritance maps directly; host interfaces appear as includes. Real output:
class Multiply < ::JsiiCalc::BinaryOperation
include ::JsiiCalc::IFriendlier
include ::JsiiCalc::IRandomNumberGenerator
self.jsii_fqn = "jsii-calc.Multiply"
Jsii::Object.register_jsii_fqn("jsii-calc.Multiply", self)
# Creates a BinaryOperation.
#
# @param lhs [Scope::JsiiCalcLib::NumericValue] Left-hand side operand.
# @param rhs [Scope::JsiiCalcLib::NumericValue] Right-hand side operand.
def initialize(lhs, rhs)
Jsii::Type.check_type(lhs, ..., "lhs")
Jsii::Type.check_type(rhs, ..., "rhs")
Jsii::Object.instance_method(:initialize).bind(self).call(lhs, rhs)
end
...
endEvery class also emits a jsii_overridable_methods table mapping Ruby member names to their jsii wire names — the runtime diffs a user's subclass against this table at construction time to compute the overrides list registered with the kernel.
Users subclass generated classes with ordinary Ruby. Overridden members are detected automatically and registered with the kernel; when host code invokes them, the kernel calls back into the Ruby implementation — including super, which crosses the boundary back to the original JS implementation without ping-ponging:
class OverrideCallsSuper < JsiiCalc::AsyncVirtualMethods
def override_me(mult)
super(mult) * 10 + 1 # `super` executes the original JS implementation
end
endAbstractness only exists at the TypeScript level — on the wire there are just object refs and member names. The generated Ruby class for an abstract type has a concrete forwarding stub for every member, and instances returned by the kernel hydrate via allocate (never new), so "you cannot instantiate an abstract class" never arises for kernel-returned values. When the kernel returns an instance of an unexported concrete subclass, the reference is labelled with the nearest exported ancestor, and virtual dispatch on the real JS object does the rest. Guest subclasses of abstract classes supply the abstract members as plain Ruby methods and are driven by host callbacks (verified by the abstractMembersAreCorrectlyHandled compliance test).
Static members are emitted only on their defining class: Ruby inherits singleton methods, which matches the ES6 static-inheritance semantics the jsii kernel implements (its member lookups walk the base chain). A child that overrides a static gets its own stub carrying the child's FQN — pinned by the StaticHelloParent/StaticHelloChild fixture. Classes whose jsii constructor is private generate an initialize that raises eagerly with a pointer to the factory methods.
This is one of the places Ruby maps cleanly where other languages struggle (cf. Go's pointer-types design in RFC 204):
- Optional parameters and struct members become
param = nil/key: nildefaults;nilis serialized asundefined. - Unset optionals read back as
nil; a host API legitimately returningundefinedcollections surfacesnil. - Unset struct members are absent from the wire payload (not present-as-null) — JS
'key' in objsemantics are preserved (eraseUnsetDataValues). - Runtime validation is nil-tolerant for optionals and strict for required members (missing required kwargs raise
ArgumentErrornatively).
No wrapper types, no pointers, no sentinel values.
Async host methods are bridged synchronously: the runtime issues the kernel's begin, services any callback requests while the promise is pending (this is when Ruby overrides of async methods execute), then collects the result with end. From the Ruby caller's perspective the method is an ordinary blocking call:
# async callMe(): Promise<number> →
def call_me()
jsii_async_call_method("callMe", [])
endGuest async overrides, overrides-calling-super, multiple simultaneous overrides, exception propagation (any Ruby exception class → host promise rejection → Jsii::RuntimeError), and Promise<void> → nil are all covered by the asyncOverrides_* compliance tests. Async static methods are invoked via the synchronous kernel path, as the kernel's begin API only accepts object references.
- Members:
camelCase→snake_case; constants:UPPER_SNAKE(Statics.FOO); modules/classes:PascalCasewith per-assembly configurable acronyms (APIGateway,DynamoDB) — acronym lists are scoped to the assembly that declared them and treated as literal text, not patterns. - Names colliding with Ruby keywords (
class,while,end...) or with the object model and the runtime's own API (initialize,new,allocate,to_jsii, and the entire reservedjsii_prefix) are deterministically prefixed with_. The kernel callback dispatcher applies the identical mapping — enforced by a cross-package consistency test — so an override of a renamed member always dispatches. Visible in real output:IRandomNumberGenerator#nextbecomes_next(Ruby keyword). - Package names that cannot start a Ruby constant (npm allows
3d-tools) are prefixed (V_3dTools).
The dominant assembly is aws-cdk-lib: ~20,000 types in one gem. Eager-defining every class at require time — parsing and evaluating ~180 MB of generated source — is untenable; a program that touches one S3 bucket should not pay to define every CloudFormation resource in AWS.
The generator therefore emits each assembly as a thin loader plus one file per type, rather than a single monolith:
lib/<assembly>.rb(the loader) loads the assembly into the kernel and, for every type, declares a Rubyautoloadand callsJsii::Object.register_autoload(fqn, path). It defines no class bodies.lib/<assembly>/<namespace>/<type>.rbdefines a single type, loaded on first use.- Types nested under a class (e.g.
CfnBucket.ReplicationRuleProperty) are bundled into their owner's file, since a constant nested under a class cannot be autoloaded without forcing the class to load.
Two load triggers cover the two ways a type is reached:
- Ruby
autoloadfires when user code references a constant (Aws::S3::Bucket). register_autoloadcovers the case the kernel hands back an FQN the user never named — pervasive in CDK, wherevpc.public_subnetsreturnsSubnetproxies for types the program never wrote down. The registry'sfind_class_by_fqndoes a load-on-missrequirebefore hydrating, so a kernel-returned object always resolves to its real proxy class rather than a bareJsii::Object.
The result is measured above: require 'aws-cdk-lib' registers ~20,400 autoloads and defines zero type bodies; constructing a small stack loads only the handful of files actually touched. In practice this is not a micro-optimization: on the production reference deployment, switching to lazy autoload emission cut roughly 30 seconds off cdk deploy versus the prior single-file eager layout.
Alongside the runtime sources, the generator emits RBS signatures (sig/<assembly>.rbs) describing every generated class, module, method and struct. This gives Ruby's static type tooling (Steep, TypeProf) and editor tooling a precise view of the CDK API — the same intent as the .d.ts/typeshed surface other ecosystems ship — without affecting runtime behavior. Real generated output, mirroring the Calculator/CalculatorProps sources above:
class JsiiCalc::Calculator < ::JsiiCalc::Composition::CompositeOperation
def initialize: (?::JsiiCalc::CalculatorProps? props) -> void
attr_reader value: Numeric
end
class JsiiCalc::CalculatorProps < ::Jsii::Struct
def initialize: (?initial_value: Numeric?, ?maximum_value: Numeric?) -> void
attr_reader initial_value: Numeric?
attr_reader maximum_value: Numeric?
end
# Optional members map to nilable types (`?`); union types are parenthesized so
# they don't collide with RBS's method-overload separator:
attr_reader union_array_property: Array[(Numeric | ::Scope::JsiiCalcLib::NumericValue)]The generated RBS validates clean under rbs validate across jsii-calc and its dependency closure (with the runtime gem's own signatures and the stdlib date library on the load path) — a check wired into the runtime test suite, including a negative control that fails the build if the generator ever emits a malformed signature.
- Kernel client: spawns the
jsii-runtimeNode child process over stdio pipes; a re-entrantMonitormakes the bidirectional JSON-RPC pipe thread-safe (dedicated concurrency tests), and an isolated stderr-draining thread prevents pipe deadlocks. - Object registry: byref handles map to live proxies for reference identity across the boundary; hydration uses
allocateso constructor side-effects never run twice; a pending-object mechanism preservesselfidentity when JS constructors call back before registration completes. - Object lifetime: the registry holds strong references and the runtime never issues the kernel's per-object
delon garbage collection — matching both reference runtimes. A jsii object is referenced from both sides of the boundary, and the guest cannot know the host's reference count, so a guest-initiateddelwould risk a dangling reference (use-after-free) on the host. Python documents this explicitly ("we can never free the memory of JSII objects ever, because we have no idea how many references exist on the other side"); Go defines aDelrequest but never calls it per-object, finalizing only the kernel client to close the process. Ruby does the same: wholesale cleanup happens when the kernel shuts down and the Node sidecar exits, which suits jsii's short-livedsynthworkloads. - Dispatch: generated explicit stubs are the primary path; a guarded
method_missingfallback (with matchingrespond_to?discipline) covers members with no generated stub, e.g. on dynamically-returned anonymous objects. - Serialization: a dedicated
Jsii::Serializerhandles the wire envelopes ($jsii.byref/enum/date/map/struct) — no core-class monkey-patching, so the runtime coexists with ActiveSupport and never walks host-application objects. - Errors: JS exceptions re-raise as
Jsii::RuntimeErrorpreserving the original name, message and remote stack trace.
Namespaces nest as modules (AWSCDK::AWSS3::Bucket); submodules may pin explicit Ruby module names via targets.ruby.module. Each assembly generates a complete gemspec from its own metadata. Real output:
Gem::Specification.new do |s|
s.name = 'jsii-calc'
s.version = '3.20.120'
s.summary = 'Ruby bindings for jsii-calc'
s.description = 'A simple calcuator built on JSII.'
s.authors = ['Amazon Web Services']
s.license = 'Apache-2.0'
s.homepage = 'https://github.com/aws/jsii'
s.files = Dir["lib/**/*"]
s.required_ruby_version = '>= 3.3.0'
s.add_dependency 'jsii-ruby-runtime', '< 0.0.1'
s.add_dependency 'base64', '~> 0.2'
endNPM version semantics translate to RubyGems equivalents via toRubyVersionRange (pre-release identifiers map to .alpha.1 / .dev.1-style suffixes; caret/tilde ranges to ~> constraints). Gems for CDK libraries version in lockstep with their npm counterparts.
Gem name governance: as of 2026-06-07, aws-cdk-lib, constructs, aws-cdk, jsii and the aws-cdk-asset-* names are unregistered on RubyGems.org (verified via the RubyGems API). AWS should reserve these — with genuine placeholder releases under an MFA-enforced AWS-owned RubyGems account — before any public preview is announced; RubyGems has no reservation mechanism and adjudicates name disputes case-by-case, so early registration is the only reliable protection. The jsii-ruby-runtime gem name will be claimed by the RFC author with a genuine release of the working runtime, with ownership transferred to AWS (gem owner --add) at the official-publishing milestone.
API documentation: jsii-docgen gains a Ruby renderer (delivered as a separate PR to cdklabs/jsii-docgen), so Construct Hub can present a Ruby tab — API reference in Ruby syntax — for every published construct library, exactly as it does for the existing languages.
- Generated code contains no runtime
evalor dynamic code construction: type metadata is embedded as Base64-encoded JSON literals, so no jsii-supplied identifier or docstring can inject into generated Ruby source through string interpolation. - The serializer is explicit and type-driven — it never monkey-patches core classes and never walks arbitrary host-application objects, so application state cannot leak unintentionally into the kernel process.
- The Node.js child process communicates exclusively over stdio pipes; it opens no network listeners.
- Reserved-name mangling prevents generated members from shadowing the runtime's own dispatch surface (
send,initialize, thejsii_prefix), closing a class of confused-deputy bugs between generated and runtime code.
Product Press Release
Today, Amazon Web Services (AWS) announced the preview of native Ruby language support for the AWS Cloud Development Kit (CDK), expanding the open-source software development framework to the global Ruby community. The AWS CDK allows developers to define cloud infrastructure using familiar programming languages. With this release, Ruby developers can now provision AWS resources natively using idiomatic Ruby code, eliminating the need to learn alternative language syntaxes or maintain fragmented DevOps toolchains.
Historically, Ruby-centric engineering teams, such as those running large-scale Ruby on Rails deployments, had to context-switch to TypeScript or Python to leverage the power of the AWS CDK. This introduced friction, requiring developers to manage distinct runtime environments, foreign package managers, and isolated linting systems solely for infrastructure provisioning.
By integrating Ruby directly into jsii—the underlying technology that powers the CDK's polyglot capabilities—Ruby developers can now use Bundler to manage infrastructure dependencies and publish reusable cloud architecture components as standard Ruby Gems. Infrastructure definitions now look, feel, and execute like native Ruby applications, allowing teams to integrate infrastructure definitions directly into their existing application codebases and test suites.
With the AWS CDK for Ruby, developers can seamlessly weave cloud infrastructure into their existing Ruby environments. Below is an example of defining an Amazon S3 bucket within a custom stack:
require 'aws-cdk-lib'
class MyCustomStack < AWSCDK::Stack
def initialize(scope, id, props = nil)
super(scope, id, props)
AWSCDK::AWSS3::Bucket.new(self, 'MyBucket', {
versioned: true,
encryption: AWSCDK::AWSS3::BucketEncryption::KMS_MANAGED
})
end
end
app = AWSCDK::App.new
MyCustomStack.new(app, 'MyStack')
app.synthThis is not hypothetical syntax: an equivalent stack (S3 + CloudFront + ACM + Route53 + Cognito + DynamoDB) runs in production today, deployed entirely through the Ruby bindings — see https://github.com/omarqureshi/blog.
To get started with the AWS CDK for Ruby, visit the AWS CDK Getting Started Guide or explore the open-source repository on GitHub.
Ticking the box below indicates that the public API of this RFC has been
signed-off by the API bar raiser (the status/api-approved label was applied to the
RFC pull request):
[ ] Signed-off by API Bar Raiser @xxxxx
We want to eliminate toolchain friction for engineering organizations whose primary expertise is in Ruby. Prior to this release, Ruby teams had to adopt a second language ecosystem (like Python or TypeScript) strictly to manage their infrastructure via the CDK. Native Ruby support allows these teams to consolidate their codebases, reduce cognitive switching overhead, and leverage their existing Ruby testing and automation workflows.
MRI (CRuby) 3.3 and newer. Ruby 3.1 and 3.2 have reached end-of-life and are not supported; the gemspecs enforce required_ruby_version >= 3.3.0, and CI exercises the currently-supported MRI series (3.3, 3.4, 4.0). The production reference deployment runs on Ruby 4.0.
The performance will be functionally identical to existing supported languages like Python. Because the AWS CDK uses jsii to serialize and pass tracking commands to an underlying Node.js worker process via a high-speed JSON-RPC protocol, the execution overhead is minimal, and the speed of cloud assembly generation matches current benchmarks.
Yes. Just like the Python, Java, and Go variants of the CDK, the Ruby bindings rely on jsii, which requires a local Node.js runtime to execute the core cloud assembly compiler behind the scenes. However, as a Ruby developer, you will interact entirely with .rb files and standard Ruby tools; the Node.js process operates transparently in the background.
You will use standard Ruby tools. Dependencies are defined in a standard Gemfile and locked using Bundler (bundle install). Custom or shared infrastructure constructs can be packaged and distributed internally or publicly as standard Ruby Gems.
Not at all. The Ruby implementation is built as an entirely isolated code-generation target within the jsii-pacmak compiler layer as an additional target. The only compiler-side change is additive targets.ruby configuration schema validation; no changes were made to existing language generators or the assembly format, ensuring stability and zero regression for TypeScript, Python, Java, Go, and .NET users.
The Ruby constructs will be published as standard .gem packages to RubyGems.org. They will follow the exact same versioning scheme as the core AWS CDK releases (e.g., if the CDK is on version 2.150.0, the corresponding aws-cdk-lib gem will also be 2.150.0).
The initial release and testing matrix specifically targets standard MRI Ruby (CRuby). While the bindings may functionally operate on alternative Ruby interpreters, they are not officially supported or tested as part of the initial launch.
1. Customer Obsession and Market Opportunity There is a massive, underserved segment of the developer community relying on Ruby—specifically Ruby on Rails—to power enterprise-grade applications. Thousands of high-growth startups and industry giants use Ruby as their primary backend language. Currently, the AWS CDK ecosystem creates friction for these teams, forcing them to accept the high cognitive load of context-switching to TypeScript or Python for their infrastructure provisioning. By building native Ruby bindings, we remove this adoption barrier and create a frictionless path for a highly passionate community to embrace the AWS CDK, ultimately driving deeper integration and consumption of AWS services.
2. Tapping into a DevOps Legacy Ruby has a storied legacy in the infrastructure-as-code space. The original DevOps revolution (Puppet, Chef, Vagrant, Capistrano) was heavily driven by Ruby because of its unique capacity for creating elegant Internal Domain Specific Languages (DSLs). Its flexible syntax allows developers to write configuration blueprints that read naturally while retaining the power of a Turing-complete language. Bringing Ruby to the CDK taps into this legacy, offering an idiomatic, powerful experience that resonates deeply with infrastructure engineers.
3. Enhancing the Polyglot CDK Vision
The AWS CDK was built on jsii precisely to be polyglot. By adding Ruby alongside TypeScript, Python, Java, Go, and C#, we validate and strengthen the core jsii architecture. Expanding the supported languages reinforces the AWS CDK's position as the universal, developer-first infrastructure framework, regardless of the user's technology stack.
- Market Share Decline: While the Ruby community is highly passionate and powers iconic enterprise web frameworks like Ruby on Rails, its overall share in the modern cloud-native, serverless, and DevOps ecosystems has decreased relative to TypeScript, Python, and Go.
- Maintenance Tail: Implementing a new JSII language target creates a permanent, non-zero tail of maintenance (handling new JSII wire protocol versions, debugging runtime edge cases, maintaining the
jsii-rosettaRuby translator, and updating CI/CD pipelines). - Opportunity Cost: The ongoing engineering investment might yield a lower return on investment (ROI) in terms of net-new CDK adoption compared to spending those engineering hours optimizing existing, highly requested features for TypeScript or Python.
- Release Pipeline Complexity: Adding Ruby means adding RubyGems to the global AWS CDK release pipeline. Any localized outage or rate-limiting on RubyGems.org could theoretically block or delay a global multi-language CDK release.
- Tooling Fragmentation: Introducing another language dilutes the core team's focus. The core AWS CDK maintainers will now need to field bug reports, review pull requests, and debug memory/IPC issues specific to the Ruby VM and its interaction with the JSII Node.js subprocess.
Because the CDK relies heavily on the jsii code generation engine, any new AWS CDK construct written in TypeScript automatically gets Ruby bindings generated for free. The ongoing maintenance surface is therefore isolated to the Ruby generator (jsii-pacmak), the @jsii/ruby-runtime gem, the Rosetta Ruby translator, and the Gem publishing pipeline.
To directly address the maintenance-tail concern above, this RFC proposes a community-maintainer model rather than asking the core team to absorb the full cost:
- @omarqureshi commits to acting as the primary maintainer for the Ruby target: triaging Ruby-specific issues, keeping the runtime current with jsii wire-protocol changes, and maintaining the compliance-suite pass rate as the suite evolves.
- The Ruby target launches under an experimental / Developer Preview tier with explicit stability annotations: no semantic-versioning guarantees on the Ruby API surface until promotion criteria (defined with the core team) are met. This contains the blast radius: a Ruby-specific issue can never block a core CDK release.
- The standard compliance suite provides an objective, language-neutral health metric (currently 123/123); a sustained regression in that matrix is the agreed signal for marking the target unmaintained, mirroring how other targets are tracked.
Since the CDK synth process runs locally on developer machines or inside persistent CI/CD runners (not as an AWS Lambda function), cold-start times are largely irrelevant. The time required to boot the Ruby VM and the child Node.js worker process is measured in fractions of a second and does not negatively impact the developer experience compared to Python or Java.
See Detailed Design above; in summary, three pillars:
1. The Ruby JSII Runtime (@jsii/ruby-runtime) — thread-safe JSON-RPC kernel client, object registry with allocate-based hydration and reference identity, structural runtime type validation, explicit serializer, guarded dynamic-dispatch fallback.
2. The pacmak Generator (jsii-pacmak/lib/targets/ruby.ts) — translates .jsii assemblies into idiomatic Ruby gems with YARD documentation, runtime type checks, hash-to-struct coercion and complete gemspec metadata, packaged as standard .gem archives.
3. Documentation Translation (jsii-rosetta) — translates the TypeScript example snippets embedded in CDK documentation into idiomatic Ruby (implemented; delivered as a separate PR to aws/jsii-rosetta).
No
- AWS SDK for Ruby (
aws-sdk-ruby): Teams can write custom Ruby scripts using the official AWS SDK to imperatively provision infrastructure. However, this forces developers to manually handle state management, rollback logic, idempotency, and resource dependency graphs. The AWS CDK abstracts this away by synthesizing declarative CloudFormation templates, providing a far safer infrastructure-as-code solution. - CloudFormation Generators (
cfndsl): Developers currently use gems likecfndslto generate CloudFormation JSON/YAML. However,cfndslonly provides a syntactic wrapper around raw CloudFormation resources; it lacks the higher-level abstraction (L2/L3 constructs), state management, and asset deployment capabilities of the AWS CDK. - Third-Party IaC (Pulumi / Terraform CDK): Unlike Python or TypeScript, major third-party declarative IaC frameworks like Pulumi and CDK for Terraform (CDKTF) do not currently have native support for Ruby. Adding Ruby bindings to the AWS CDK makes it the premier, and largely the only, high-level declarative infrastructure framework available for the Ruby ecosystem.
The primary drawback is the impedance mismatch between Ruby's dynamic, metaprogramming-heavy nature and the strict structural typing enforced by jsii. To remain compatible with the polyglot JSII kernel, the Ruby bindings favor explicit generated stubs and validation over Ruby's fully dynamic idioms; method_missing exists only as a guarded fallback for members the generator could not know about. We lean on the architectural patterns established by the Python JSII runtime to ensure stability, which may occasionally feel restrictive compared to pure Ruby libraries. Furthermore, like all jsii targets, the Ruby runtime requires a hidden Node.js subprocess to execute, which can complicate debugging and slightly increase memory overhead.
In the spirit of RFC 204's honesty section — current open items, none of which block the compliance suite:
- Ambiguous unions: Hashes passed where a union allows multiple struct types are forwarded as-is; coercion only happens when exactly one struct arm makes the conversion unambiguous. (Matching by key-shape was considered and rejected as too magical.)
- Hash key casing: user-supplied hashes must use snake_case keys; camelCase (wire-shape) hashes are handled by the deserializer, not at call sites.
- Async static methods are invoked via the synchronous kernel path (the kernel's
beginAPI only accepts object references) — a kernel-level limitation shared by all bindings. - Naming of generated artifacts (gem names for scoped npm packages, acronym defaults) follows conventions established here but deserves bar-raiser review before the API freezes.
The delivery of the Ruby bindings will be executed in three major phases:
Phase 1: Prototyping & Core Runtime (Completed)
- Establish the
jsiiRuby runtime environment (@jsii/ruby-runtime), managing the child Node.js process and standard IO pipes. - Develop the proxy
Objectfoundation and robust runtime type validation across the language boundary. - Implement the
jsii-pacmakcode generator target for Ruby, including YARD documentation emission and generated-code snapshot coverage in the cross-language test harness. - Milestone: Achieve 100% pass rate on the JSII standard compliance suite. ✅ Achieved: 123/123, reported through
tools/jsii-complianceinto the same compliance matrix as Java and Go.
Phase 2: Documentation, Tooling & Standard Library (In Progress)
- Extend the
jsii-rosettatranslation engine to automatically generate idiomatic Ruby code snippets and usage examples from the original TypeScript sources (implemented; under review). - Ruby renderer for
jsii-docgen, enabling a Construct Hub Ruby documentation tab for every published construct library (implemented; under review). - Reserve the AWS-owned gem names on RubyGems.org (see Gem name governance); generate and publish the core
aws-cdk-libmodules and dependencies as standard Ruby Gems. - CDK CLI integration: a
cdk init --language rubyproject template (Gemfile,app.rb,cdk.jsonwithbundle exec ruby app.rb) so the first-run experience matches the other languages. - Implement comprehensive CI/CD pipelines to ensure ongoing stability against new JSII wire protocol updates (monorepo CI passes end-to-end today, including pacmak integration against
aws-cdk-lib).
Phase 3: Community Preview & General Availability (Upcoming)
- Release an Alpha/Developer Preview to gather community feedback regarding the Developer Experience (DX).
- Publish migration guides, "Getting Started" documentation, and tutorials tailored for Ruby on Rails engineers.
- Validate
require-time and lazy-load behavior across the preview cohort ataws-cdk-libscale (lazyautoloademission is already implemented — see Lazy loading in Detailed Design). - Finalize API stabilization and fix edge-case bugs based on user feedback.
- Promote to General Availability (GA).
Beyond GA, the type information already present in .jsii assemblies enables several natural extensions:
Block-based DSL sugar: optional block-initializer style ((Struck: a post-construction mutation block adds nothing over the kwargs form, and L2 construct properties are construct-time-only — there is noBucket.new(self, 'B') { |b| b.versioned = true }) layered over the kwargs API, leaning into Ruby's configuration-DSL heritage without altering the wire contract.bucket.versioned=setter — so the block has nothing to set. The kwargs/struct API already covers this idiomatically.)- JRuby / TruffleRuby evaluation: the runtime is pure Ruby over stdio pipes with no C extensions, so alternative interpreters are plausible targets once officially tested.
- Rails integration: generators/railties that scaffold CDK stacks inside existing Rails applications, unifying app and infrastructure in a single repository and test suite.