Carina
Carina represents the keel of Argo Navis: the structural backbone that quietly supports everything above it.
[!CAUTION] This is an experimental project. The DSL syntax, APIs, and features are subject to change without notice.
A strongly typed infrastructure management tool written in Rust.
Features
- Custom DSL: Simple, expressive syntax for defining infrastructure
- Effects as Values: Side effects are represented as data structures, not immediately executed
- Strong Typing: Catch configuration errors at parse time with schema validation
- Data Sources: Reference existing infrastructure without managing its lifecycle
- Provider Architecture: Extensible provider system for multi-cloud support
- Modules: Reusable infrastructure components with typed inputs/outputs
- State Management: Remote state storage with locking (S3 backend)
- LSP Support: Editor integration with completion, diagnostics, and syntax highlighting
- Terraform-like Workflow: Familiar
validate,plan,apply,destroycommands
Installation
cargo build --release
The binary will be available at target/release/carina.
Quick Start
1. Define your infrastructure
Create a .crn file:
# main.crn
provider aws {
region = aws.Region.ap_northeast_1
}
let main_vpc = aws.vpc {
name = 'main-vpc'
cidr_block = '10.0.0.0/16'
}
let web_sg = aws.security_group {
name = 'web-sg'
vpc_id = main_vpc.id
}
aws.security_group.ingress_rule {
name = 'http'
security_group_id = web_sg.id
from_port = 80
to_port = 80
protocol = 'tcp'
cidr_blocks = ['0.0.0.0/0']
}
2. Validate
Point carina at the directory containing your .crn files (all .crn files in that directory are merged):
$ carina validate .
Validating...
✓ 3 resources validated successfully.
• vpc.main-vpc
• security_group.web-sg
• security_group.ingress_rule.http
3. Plan
$ carina plan .
Execution Plan:
+ vpc
name: "main-vpc"
cidr_block: "10.0.0.0/16"
└─ + security_group
name: "web-sg"
vpc_id: main_vpc.id
└─ + security_group.ingress_rule
name: "http"
security_group_id: web_sg.id
Plan: 3 to add, 0 to change, 0 to destroy.
4. Apply
$ carina apply .
Applying changes...
✓ Create vpc.main-vpc
✓ Create security_group.web-sg
✓ Create security_group.ingress_rule.http
Apply complete! 3 changes applied.
DSL Syntax
Provider Block
provider aws {
region = aws.Region.ap_northeast_1
}
Resources
Anonymous resources - ID is derived from the name attribute:
aws.security_group.ingress_rule {
name = 'http'
security_group_id = web_sg.id
from_port = 80
to_port = 80
protocol = 'tcp'
}
Named resources - Use let binding for referencing:
let web_sg = aws.security_group {
name = 'web-sg'
vpc_id = main_vpc.id
}
Data Sources
Use the read keyword to reference existing infrastructure without managing its lifecycle. Data sources are read-only and cannot be created, modified, or deleted by Carina.
# Read an existing VPC (data source)
let existing_vpc = read aws.vpc {
name = 'production-vpc'
}
# Create a new subnet that references the existing VPC
let app_subnet = aws.subnet {
name = 'app-subnet'
vpc_id = existing_vpc.id
cidr_block = '10.0.100.0/24'
availability_zone = aws.AvailabilityZone.ZoneName.ap_northeast_1a
}
In plan output, read effects are displayed with the <= symbol to distinguish them from mutations.
Enum Values
Enum values support multiple formats. The shorthand forms are automatically resolved based on schema context:
# Full namespace format
instance_tenancy = aws.ec2.Vpc.InstanceTenancy.dedicated
# Type.value format
instance_tenancy = InstanceTenancy.dedicated
# Value-only format (shortest, recommended)
instance_tenancy = dedicated
Nested Objects (Struct Types)
Some resources support nested objects for inline configuration. Use repeated blocks for multiple items:
awscc.ec2.SecurityGroup {
name = 'web-sg'
vpc_id = vpc.vpc_id
group_description = 'Web server security group'
security_group_ingress {
ip_protocol = 'tcp'
from_port = 80
to_port = 80
cidr_ip = '0.0.0.0/0'
}
security_group_ingress {
ip_protocol = 'tcp'
from_port = 443
to_port = 443
cidr_ip = '0.0.0.0/0'
}
}
Array syntax is also supported:
security_group_ingress = [
{
ip_protocol = 'tcp'
from_port = 80
to_port = 80
cidr_ip = '0.0.0.0/0'
}
]
Modules
Modules enable reusable infrastructure components with typed inputs and outputs.
Module definition (modules/web_tier/main.crn):
input {
vpc: aws.vpc
cidr_blocks: list(cidr)
enable_https: bool = true
}
output {
security_group: aws.security_group = web_sg.id
}
let web_sg = aws.security_group {
name = 'web-sg'
vpc_id = input.vpc
description = 'Security group for web servers'
}
Using modules:
let web_tier = use { source = './modules/web_tier' }
let main_vpc = aws.vpc {
name = 'main-vpc'
cidr_block = '10.0.0.0/16'
}
web_tier {
vpc = main_vpc.id
cidr_blocks = ['10.0.1.0/24', '10.0.2.0/25']
}
Inspect module structure:
$ carina module info modules/web_tier
Module: web_tier
=== REQUIRES ===
vpc: aws.vpc (required)
cidr_blocks: list(cidr) (required)
enable_https: bool = true
=== CREATES ===
input { vpc: aws.vpc }
└── web_sg: aws.security_group
├── http: aws.security_group.ingress_rule
└── https: aws.security_group.ingress_rule
=== ATTRIBUTES ===
security_group: aws.security_group
Architecture
Carina follows a functional architecture where side effects are treated as values:
DSL File (.crn)
│
▼
┌─────────┐
│ Parser │ Parse DSL into Resources
└────┬────┘
│
▼
┌─────────┐
│ Differ │ Compare desired vs current state
└────┬────┘
│
▼
┌─────────┐
│ Plan │ Collection of Effects (Create/Update/Delete)
└────┬────┘
│
▼
┌──────────┐
│ Provider │ Execute Effects (AWS, GCP, etc.)
└──────────┘
Core Concepts
- Resource: Desired state declared in DSL
- State: Current state fetched from infrastructure
- Effect: Represents a side effect (Create, Update, Delete, Read)
- Plan: Collection of Effects to be executed
- Provider: Abstraction for infrastructure operations
Project Structure
carina/
├── carina-cli/ # CLI application
├── carina-core/ # Core library (provider-agnostic)
│ ├── src/
│ │ ├── effect.rs # Effect type definitions
│ │ ├── plan.rs # Plan (collection of Effects)
│ │ ├── resource.rs # Resource and State types
│ │ ├── provider.rs # Provider trait
│ │ ├── differ/ # State comparison
│ │ ├── parser/ # DSL parser (pest-based)
│ │ ├── schema.rs # Type validation (generic types only)
│ │ ├── module.rs # Module signature and dependency graph
│ │ ├── module_resolver/ # Module import and expansion
│ │ └── formatter/ # Code formatter
│ └── ...
├── carina-plugin-host/ # WASM plugin host for provider plugins
├── carina-plugin-sdk/ # SDK for building WASM provider plugins
├── carina-provider-mock/ # Mock provider for testing
├── carina-provider-protocol/ # Protocol definitions for provider communication
├── carina-provider-resolver/ # Resolves and loads provider plugins
├── carina-state/ # State management
│ └── src/backends/ # State backends (S3, etc.)
├── carina-lsp/ # Language Server Protocol implementation
└── carina-tui/ # Terminal UI for plan display
AWS Provider
AWS providers are distributed as separate repositories under carina-rs and loaded as WASM plugins by the core runtime:
- carina-provider-aws — AWS provider (Smithy-based codegen)
- carina-provider-awscc — AWS Cloud Control provider
Configure valid AWS credentials via:
- Environment variables (
AWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY) - AWS credentials file (
~/.aws/credentials) - IAM roles (when running on AWS)
Using with aws-vault
aws-vault exec myprofile -- carina apply .
Commands
Format
Format .crn files:
# Format a single file
$ carina fmt example.crn
# Format all .crn files in current directory
$ carina fmt
# Format recursively
$ carina fmt -r
# Check formatting without modifying files
$ carina fmt --check
# Show diff of formatting changes
$ carina fmt --diff
Destroy
Remove all resources defined in a configuration:
$ carina destroy .
Destroy Plan:
- security_group.ingress_rule.http
- security_group.web-sg
- vpc.main-vpc
Plan: 3 to destroy.
Do you really want to destroy all resources?
This action cannot be undone. Type 'yes' to confirm.
Enter a value: yes
Destroying resources...
✓ Delete security_group.ingress_rule.http
✓ Delete security_group.web-sg
✓ Delete vpc.main-vpc
Destroy complete! 3 resources destroyed.
Use --auto-approve to skip the confirmation prompt.
Module Info
Inspect module structure and dependencies:
$ carina module info modules/web_tier
State Management
Carina supports remote state storage for tracking infrastructure state across team members and CI/CD pipelines.
S3 Backend
Store state in an S3 bucket:
backend s3 {
bucket = 'my-carina-state'
key = 'infra/prod/carina.crnstate'
region = aws.Region.ap_northeast_1
encrypt = true
auto_create = true # Automatically create the bucket if it doesn't exist
}
provider aws {
region = aws.Region.ap_northeast_1
}
aws.s3.Bucket {
name = 'my-app-data'
}
The state file tracks:
- Resource states and attributes
- Serial number for change detection
- Locking to prevent concurrent modifications
Development
Run tests
cargo test
Build
cargo build
License
MIT
Roadmap
- [x] Resource dependencies and references
- [x] Modules and reusability
- [x] Destroy command
- [x] State file management (S3 backend)
- [x] Data sources (read existing infrastructure)
- [ ] More AWS resources (EC2, IAM, Lambda, etc.)
- [ ] GCP provider
- [ ] Import existing resources