Pulumi
What is Pulumi?
Pulumi is a modern Infrastructure as Code (IaC) platform that allows you to define, deploy, and manage cloud infrastructure using familiar programming languages. Unlike traditional IaC tools that use domain-specific languages, Pulumi enables you to use languages like TypeScript, Python, Go, and in our case, ReScript.
Key Benefits:
- Type Safety: Compile-time checking of infrastructure configurations
- Familiar Languages: Use programming constructs like loops, conditionals, and functions
- Cloud Agnostic: Support for multiple cloud providers
- State Management: Automatic state tracking and conflict resolution
- Resource Dependencies: Automatic dependency resolution and ordering
Learn More: Official Pulumi Documentation
Why Pulumi in Reventless?
Reventless uses Pulumi for several strategic reasons that align perfectly with the framework's architecture and goals:
1. Type Safety with ReScript Integration
Pulumi's programmatic approach integrates seamlessly with ReScript's type system, providing compile-time guarantees for infrastructure configurations. This eliminates entire classes of deployment errors that would only be discovered at runtime with traditional IaC tools.
2. Component-Based Architecture Alignment
Pulumi's component resource model aligns naturally with reventless's component-based architecture. Each reventless component can encapsulate its infrastructure requirements as a Pulumi component resource, creating clear boundaries and reusable patterns.
3. AWS-First Approach with Extensibility
While reventless is AWS-first, Pulumi's multi-cloud support provides a path for future extensibility without architectural changes. The framework can leverage Pulumi's excellent AWS provider while maintaining the option to support other cloud providers.
4. Programmatic Infrastructure Management
Complex infrastructure patterns that would be difficult or impossible to express in declarative languages become straightforward with Pulumi's programmatic approach. This enables sophisticated resource orchestration and dynamic configuration.
5. Deploy-time/Runtime Separation
Pulumi enables a clear separation between deploy-time infrastructure provisioning and runtime application logic, which is fundamental to reventless's architecture.
Pulumi in Reventless Architecture
Deploy-time vs Runtime Separation
The reventless framework maintains a strict separation between two distinct phases:
- Deploy-time: Pulumi infrastructure definitions, resource creation, and dependency resolution
- Runtime: Lambda handlers, application logic, and business operations
This separation ensures that:
- Infrastructure concerns don't leak into runtime code
- Runtime code remains lightweight and focused on business logic
- Infrastructure can be reasoned about independently
- Deployment and runtime scaling can be optimized separately
Pulumi.Output.t Wrapping
All infrastructure values in reventless are wrapped in Pulumi.Output.t<'a> to handle:
- Asynchronous Resource Creation: Resources may not be immediately available during deployment
- Dependency Resolution: Pulumi automatically resolves dependencies between resources
- Value Propagation: Values flow correctly through the dependency graph
Example from EventLog_Builder.res:
self->Component.setOperations(
(storage.operations, eventTopic->Component.operations)
->Pulumi.Output.all2
->Pulumi.Output.apply(((storageOps, eventTopicOps)) => {
// Operations are only available after both dependencies resolve
{
append: Ops.append,
replay: Ops.replay,
}
}),
)
Component Resource Patterns
Reventless components extend Pulumi.ComponentResource to create reusable infrastructure patterns with proper parent-child relationships:
@module("./Component") @new
external make: (
~componentType: string,
~name: string,
~construct: 'construct,
~opts: option<Pulumi.ComponentResource.options>,
) => t<'component, 'outputs, 'operations> = "default"
This pattern ensures:
- Resource Organization: Related resources are grouped under a parent component
- Dependency Management: Child resources automatically depend on parent resources
- Naming Consistency: Resource names follow a consistent hierarchy
- Cleanup: Deleting a parent component removes all child resources
ReScript Bindings
Reventless includes custom ReScript bindings for Pulumi that provide type-safe access to Pulumi APIs:
Core Dependencies
- @reventless/rescript-pulumi-pulumi: ^2.2.0 (Core Pulumi bindings)
- @reventless/rescript-pulumi-aws: ^2.3.0 (AWS provider bindings)
Advantages of Custom Bindings
- Type-Safe Access: All Pulumi APIs are properly typed in ReScript
- Integration with ReScript's Type System: Leverages ReScript's powerful type inference
- Compile-Time Error Checking: Catches configuration errors before deployment
- Better Developer Experience: IDE support with autocomplete and type hints
- Consistent API Surface: Uniform interface across all Pulumi providers
Common Patterns and Examples
Component Creation Pattern
From EventLog_Builder.res:
let construct = (self, name) => {
let opts = {Pulumi.CustomResourceOptions.parent: self->Component.toPulumiResource}
let storage = Storage.make(~name=name->ComponentType.name(EventLog.componentType), ~opts)
module SpecificEventTopic = EventTopic_Builder.Make(Spec, EventTopicPublisher)
let eventTopic = SpecificEventTopic.make(
~name,
~storageResources=storage.resources,
~opts=opts->Util.Pulumi.ComponentResourceOptions.ofCustomResourceOptions,
)
// ...
}
Output Wrapping and Dependency Management
From Component.res:
let wrappedOutputs = component => component->Pulumi.Output.apply(component => component->outputs)
Resource Type Definitions
From QueryDb_Adapter.res:
type storage = {
resources: array<Reventless.Adapter.resource>,
dataSourceName: Pulumi.Output.t<string>,
operations: Pulumi.Output.t<operations>,
}
Cross-Component Dependencies
Components can depend on outputs from other components, with Pulumi automatically managing the dependency graph:
let eventTopic = SpecificEventTopic.make(
~name,
~storageResources=storage.resources, // Dependency on storage component
~opts,
)
Best Practices in Reventless
1. Resource Naming Conventions
- Use consistent naming patterns across components
- Include component type and instance name in resource names
- Leverage
ComponentType.namefor standardized naming
2. Dependency Management
- Always wrap infrastructure values in
Pulumi.Output.t - Use
Pulumi.Output.all2and similar functions to combine multiple outputs - Apply transformations using
Pulumi.Output.apply
3. Component Resource Organization
- Create component resources for logical groupings of infrastructure
- Set proper parent-child relationships using
Pulumi.CustomResourceOptions.parent - Register outputs using
Component.setOutputs
4. Error Handling Patterns
- Leverage ReScript's type system to catch configuration errors at compile time
- Use option types for optional configuration parameters
- Validate inputs early in the component construction process
5. Performance Considerations
- Minimize the number of Pulumi operations in hot paths
- Cache expensive computations in component outputs
- Use resource dependencies efficiently to minimize deployment time
Integration with AWS
Reventless leverages Pulumi's AWS provider through custom ReScript bindings to create:
- Lambda Functions: For serverless compute
- DynamoDB Tables: For event storage and state management
- SNS Topics: For event publishing and messaging
- API Gateway: For HTTP endpoints
- IAM Roles and Policies: For security and access control
The AWS-specific adapters (like those in EventLog_Builder.res) encapsulate the complexity of AWS resource creation while providing a clean, type-safe interface to the reventless components.
Troubleshooting
Common Issues
- Dependency Cycles: Ensure that component dependencies form a directed acyclic graph
- Output Access: Always use
Pulumi.Output.applyto access values inside outputs - Resource Naming: Avoid hardcoded resource names that might conflict across deployments
- Type Mismatches: Leverage ReScript's type system to catch binding issues early
Debugging Tips
- Use Pulumi's preview mode to understand resource changes before deployment
- Leverage ReScript's compiler errors to identify type mismatches
- Check resource dependencies in the Pulumi console
- Use structured logging in component constructors for debugging
Conclusion
Pulumi's integration with reventless provides a powerful foundation for type-safe, programmatic infrastructure management. The combination of ReScript's type system, Pulumi's resource model, and reventless's component architecture creates a robust platform for building and deploying event-sourced applications on AWS.
The patterns established in reventless demonstrate how modern Infrastructure as Code can be both powerful and maintainable when properly integrated with a strongly-typed functional programming language.