Architecture
Understanding how Pulumi Any Terraform bridges Terraform providers to Pulumi
This guide explains how Pulumi Any Terraform works internally, how the bridge operates, and the technical design decisions behind the project.
Overview
Pulumi Any Terraform is a dynamic bridge that automatically converts Terraform providers into native Pulumi providers. This is achieved through Pulumi's pulumi-terraform-bridge, which translates Terraform's schema and resources into Pulumi's type system.
How It Works
graph TB
A[Terraform Provider] --> B[Pulumi Bridge]
B --> C[Generated TypeScript Types]
B --> D[Resource Definitions]
B --> E[Provider Configuration]
C --> F[npm Package]
D --> F
E --> F
F --> G[Your Pulumi Program]
G --> H[Pulumi Engine]
H --> I[Terraform Provider Runtime]
I --> J[Target API/Service]The Bridge Process
-
Terraform Provider Schema: Each Terraform provider defines its resources, data sources, and configuration in Go code with schemas.
-
Bridge Configuration: The bridge reads the provider's schema and generates corresponding Pulumi resources.
-
Type Generation: TypeScript type definitions are automatically generated, providing full IntelliSense and type safety.
-
Resource Mapping: Terraform resources are mapped to Pulumi custom resources with appropriate CRUD operations.
-
Runtime Execution: When you run
pulumi up, the Pulumi engine invokes the bridged provider, which in turn calls the original Terraform provider.
Project Structure
pulumi-any-terraform/
├── packages/ # Individual provider packages
│ ├── better-uptime/ # Better Uptime provider
│ │ ├── bin/ # Compiled output
│ │ ├── index.ts # Main exports
│ │ ├── types/ # Type definitions
│ │ ├── config/ # Provider configuration
│ │ ├── package.json # Package metadata
│ │ └── README.md # Provider documentation
│ ├── namecheap/ # Namecheap provider
│ └── ... # Other providers
├── tools/ # Build system plugins
│ ├── build.ts # Build orchestration
│ ├── linter.ts # Linting plugin
│ ├── prettier.ts # Code formatting
│ └── syncpack.ts # Dependency sync
├── docs/ # Documentation site (Next.js)
├── .github/ # CI/CD workflows
│ └── workflows/
│ ├── publish.yml # Package publishing
│ ├── test.yml # Testing & linting
│ └── update.yml # Dependency updates
├── nx.json # Nx workspace config
├── pnpm-workspace.yaml # PNPM workspace
└── package.json # Root configurationProvider Package Structure
Each provider package follows a consistent structure:
// Generated index.ts
import * as pulumi from "@pulumi/pulumi";
import * as utilities from "./utilities";
// Provider configuration
export class Provider extends pulumi.ProviderResource {
// ...
}
// Resources
export class SomeResource extends pulumi.CustomResource {
// Properties with full type definitions
public readonly someProperty!: pulumi.Output<string>;
// Constructor
constructor(name: string, args: SomeResourceArgs, opts?: pulumi.CustomResourceOptions) {
// ...
}
}
// Type definitions
export interface SomeResourceArgs {
someProperty: pulumi.Input<string>;
// ...
}Key Components
1. Provider Resource
Each package exports a Provider resource that configures the Terraform provider:
const provider = new Provider("my-provider", {
apiKey: config.requireSecret("apiKey"),
endpoint: "https://api.example.com",
});2. Custom Resources
Resources are wrapped as Pulumi CustomResource classes with:
- Full TypeScript types
- Input/output properties
- Proper lifecycle management
- State tracking
3. Type Definitions
All inputs and outputs are strongly typed:
export interface MonitorArgs {
url: pulumi.Input<string>;
checkFrequency?: pulumi.Input<number>;
monitorType: pulumi.Input<"status" | "ping" | "keyword">;
}
export interface MonitorState {
id: pulumi.Output<string>;
createdAt: pulumi.Output<string>;
status: pulumi.Output<string>;
}Parameterization
Each provider uses parameterization to specify which Terraform provider to bridge. This is stored as a base64-encoded value in package.json:
{
"pulumi": {
"resource": true,
"name": "terraform-provider",
"version": "0.14.0",
"parameterization": {
"name": "namecheap",
"version": "2.2.0",
"value": "eyJyZW1vdGUiOnsidXJsIjoicmVnaXN0cnkub3BlbnRvZnUub3JnL25hbWVjaGVhcC9uYW1lY2hlYXAiLCJ2ZXJzaW9uIjoiMi4yLjAifX0="
}
}
}Decoded, this specifies:
{
"remote": {
"url": "registry.opentofu.org/namecheap/namecheap",
"version": "2.2.0"
}
}This tells Pulumi:
- Which Terraform provider to use (
namecheap/namecheap) - Which version to bridge (
2.2.0) - Where to download it from (
registry.opentofu.org)
Build System
The project uses Nx for monorepo management and build orchestration:
Nx Workspace
{
"targetDefaults": {
"build": {
"outputs": ["{projectRoot}/bin"],
"dependsOn": ["^build"],
"cache": true
}
},
"plugins": [
"@nx/js/typescript",
"./tools/audit.ts",
"./tools/build.ts",
"./tools/linter.ts",
"./tools/prettier.ts",
"./tools/syncpack.ts"
]
}Build Plugins
Custom Nx plugins in /tools/ provide:
- Build Plugin (
build.ts): TypeScript compilation orchestration - Linter Plugin (
linter.ts): Code quality checks - Prettier Plugin (
prettier.ts): Code formatting - Syncpack Plugin (
syncpack.ts): Dependency synchronization - Audit Plugin (
audit.ts): Security vulnerability scanning
Caching
Nx provides smart caching to speed up builds:
- Local cache: Stores build outputs locally
- Remote cache: Shared cache on AWS S3 (Cloudflare R2)
- Computation caching: Skips unnecessary rebuilds
CI/CD Pipeline
Workflow Architecture
graph LR
A[Push to main] --> B[Test Workflow]
B --> C[Lint & Build]
C --> D[Autofix.ci]
D --> E[Update Workflow]
E --> F[Check Updates]
F --> G[Publish Workflow]
G --> H[Release Packages]
H --> I[NPM Registry]Key Workflows
1. Test Workflow (test.yml)
- Runs on every push and PR
- Lints code with Biome
- Builds all packages
- Runs type checking
- Uses Nx affected commands for efficiency
2. Update Workflow (update.yml)
- Runs daily or on-demand
- Checks for dependency updates
- Creates automated PRs
- Uses Renovate for dependency management
3. Publish Workflow (publish.yml)
- Runs on main branch after successful tests
- Uses Changesets for version management
- Publishes packages to NPM
- Creates release notes automatically
External Integrations
1. Terraform Registry
- Purpose: Source for Terraform providers
- Usage: Downloads provider schemas and binaries
- URL:
registry.terraform.ioandregistry.opentofu.org
2. NPM Registry
- Purpose: Distribution of Pulumi packages
- Usage: Publishing and installing packages
- Packages: All under
pulumi-*namespace
3. GitHub
- Purpose: Source control and CI/CD
- Features:
- Issue tracking
- Pull requests
- GitHub Actions
- Package registry
4. Nx Cloud
- Purpose: Distributed build caching
- Usage: Speed up CI/CD builds
- Storage: AWS S3 (via Cloudflare R2)
5. Socket Security
- Purpose: Dependency security scanning
- Usage: Blocks malicious packages during install
- Integration: GitHub Actions
6. Autofix.ci
- Purpose: Automated code fixes
- Usage: Auto-fixes linting and formatting issues
- Integration: Automatic PR commits
State Management
Pulumi manages state differently from Terraform:
Pulumi State
- Stored in Pulumi Service (default) or self-managed backend
- Contains resource metadata and outputs
- Supports encryption and access control
- Enables collaboration and history tracking
Bridge State Translation
The bridge translates between Pulumi and Terraform state:
- Pulumi tracks resources in its own state
- Bridge invokes Terraform provider with translated inputs
- Provider returns outputs
- Bridge translates back to Pulumi format
- Pulumi updates its state
Type Safety
One of the main benefits of this bridge is complete type safety:
Input Types
interface MonitorArgs {
url: pulumi.Input<string>; // Required string input
checkFrequency?: pulumi.Input<number>; // Optional number input
ssl?: pulumi.Input<{ // Nested object
checkExpiry: boolean;
expiryThreshold: number;
}>;
}Output Types
interface MonitorOutputs {
id: pulumi.Output<string>; // Output string
createdAt: pulumi.Output<string>; // Output timestamp
status: pulumi.Output<"up" | "down">; // Output enum
}Type Inference
const monitor = new betteruptime.Monitor("api", {
url: "https://api.example.com",
checkFrequency: 60,
});
// TypeScript knows these are Output<string>
export const monitorId = monitor.id;
export const monitorStatus = monitor.status;Resource Lifecycle
Resources follow Pulumi's standard lifecycle:
-
Create: When resource doesn't exist
- Bridge calls Terraform's Create method
- Returns resource ID and properties
-
Read: To refresh state
- Bridge calls Terraform's Read method
- Updates Pulumi state with current values
-
Update: When properties change
- Bridge calls Terraform's Update method
- Handles partial updates if supported
-
Delete: When resource is removed
- Bridge calls Terraform's Delete method
- Removes from Pulumi state
Performance Considerations
Build Performance
- Parallel compilation: Multiple packages build simultaneously
- Incremental builds: Only rebuild changed packages
- Caching: Skip unchanged builds entirely
- Nx affected: Only process affected packages
Runtime Performance
- Lazy loading: Resources loaded on-demand
- Concurrent operations: Multiple resources created in parallel
- State optimization: Minimal state queries
- Provider reuse: Single provider instance per program
Security
Dependency Security
- Socket Security: Scans npm packages during install
- Dependabot/Renovate: Automated security updates
- Audit checks: Regular security audits
Secrets Management
- Pulumi secrets: Encrypted at rest and in transit
- Environment variables: For local development
- Config encryption: Sensitive config encrypted in state
Package Publishing
- NPM 2FA: Required for publishing
- Provenance: Supply chain security
- Automated publishing: Reduces human error
Debugging
Enable Debug Logging
# Pulumi debug logs
export PULUMI_DEBUG_COMMANDS=true
export PULUMI_DEBUG_PROMISE_LEAKS=true
# Terraform provider logs
export TF_LOG=DEBUG
export TF_LOG_PATH=./terraform.logInspect Bridge Behavior
import * as pulumi from "@pulumi/pulumi";
// Log all inputs
pulumi.log.info(`Creating resource with: ${JSON.stringify(args)}`);
// Log outputs
resource.id.apply(id => pulumi.log.info(`Created with ID: ${id}`));Future Enhancements
Potential improvements to the architecture:
- Dynamic provider generation: Generate providers on-demand
- Better error messages: More helpful error context
- Performance optimization: Faster bridge overhead
- Multi-version support: Support multiple Terraform versions
- Enhanced types: More precise TypeScript types