Syntax
File structure, provider blocks, resource blocks, let bindings, and comments in the Carina DSL.
Carina configuration files use the .crn extension. A file consists of a sequence of top-level statements that declare infrastructure resources and their relationships.
File Structure
A .crn file contains zero or more top-level statements in any order:
# Provider configuration
provider awscc {
region = awscc.Region.ap_northeast_1
}
# Backend configuration (optional)
backend s3 {
bucket = 'my-state-bucket'
key = 'infra/carina.state.json'
region = 'ap-northeast-1'
}
# Resource declarations
let vpc = awscc.ec2.Vpc {
cidr_block = '10.0.0.0/16'
tags = {
Name = 'main'
}
}
awscc.ec2.InternetGateway {
tags = {
Name = 'main'
}
}
Top-Level Construct Shapes
Carina’s top-level constructs come in two shapes, chosen by two independent questions:
- Does the block’s body schema depend on a kind label? The attributes valid inside
provider awscc { ... }differ from those insideprovider aws { ... }, and similarly for backend kinds. When the schema varies by kind, the construct carries a kind label (provider <kind>,backend <kind>). - Does the construct produce a value that other code references by name? When multiple named instances are useful, the construct is written as an expression on the right-hand side of a
letbinding, and the binding name on the left is how the value is referenced later.
The two questions are orthogonal:
| kind label needed | kind label not needed | |
|---|---|---|
| singleton / not named-referenced | provider awscc { ... }, backend s3 { ... } | — |
| named reference | let us = provider awscc { ... } | let orgs = upstream_state { ... } |
provider/backendcarry kind labels because the body schema is kind-specific. A bareprovider <kind> { ... }block declares the kind’s default instance; wrapping the same expression inlet <name> = provider <kind> { ... }declares a named instance. See Named provider instances below.upstream_statehas no kind label because there is only one kind of upstream (a sibling directory); it is an expression because projects typically reference several named upstreams.
When reading an unfamiliar top-level construct, work out which row and column of the table it sits in — that tells you whether to expect a kind label and whether to expect a let binding on the left.
Statements
The following statements are valid at the top level of a .crn file:
| Statement | Purpose |
|---|---|
provider | Configure a cloud provider |
backend | Configure remote state storage |
let | Bind a name to a resource or value |
| Anonymous resource | Declare a resource without a binding |
import (state) | Import an existing cloud resource into state |
removed | Remove a resource from state without deleting it |
moved | Rename a resource in state |
for | Iterate to create multiple resources |
if | Conditionally create resources |
fn | Define a reusable function |
require | Assert a condition with an error message |
arguments | Declare module input parameters |
attributes | Define module output values (for callers who bind the module with let) |
exports | Define values published to upstream-state consumers |
use (in let RHS) | Load a module from a directory (let m = use { source = '...' }) |
Provider Block
Every .crn file that declares resources must configure at least one provider. The provider block specifies which cloud provider to use and its settings.
provider awscc {
region = awscc.Region.ap_northeast_1
}
Multiple providers can be configured in the same file:
provider awscc {
region = awscc.Region.ap_northeast_1
}
provider aws {
region = aws.Region.ap_northeast_1
}
Named Provider Instances
A provider <kind> { ... } block declares the kind’s default instance — the one resources reach for when they say nothing about routing. To declare additional instances of the same kind (for example, an awscc provider pinned to a different region), wrap the same expression in a let binding:
provider awscc {
source = 'github.com/carina-rs/carina-provider-awscc'
version = '~0.5.0'
region = awscc.Region.ap_northeast_1
}
let us = provider awscc {
region = awscc.Region.us_east_1
}
Only the kind’s default instance carries source / version / revision. Repeating those on a named instance is rejected by the parser: they describe how to load the provider plugin, which is a property of the kind, not of an individual instance.
Resources route by attaching directives { provider = <binding> }:
awscc.s3.Bucket {
bucket_name = 'tokyo-assets'
# no directive -> kind's default instance (ap-northeast-1)
}
awscc.cloudfront.Distribution {
# ...
directives {
provider = us # routes to the `us` instance (us-east-1)
}
}
Omitting directives.provider keeps the resource on the kind’s default instance. The default instance is identified by the absence of a binding name; it is not surfaced as a completion candidate at the value position of directives { provider = | } for the same reason.
State files persist the routing decision on each resource’s identifier (the provider_instance field on ResourceId / Directives, written in state v3 and later). Existing state files written before named-instance support continue to load: the field defaults to null and serialises back out only when non-null.
Resource Blocks
Resources represent cloud infrastructure objects. A resource block specifies the provider, service, and resource type using dot notation, followed by a block of attributes.
Anonymous Resources
When you do not need to reference a resource elsewhere, declare it without a binding:
awscc.ec2.Vpc {
cidr_block = '10.0.0.0/16'
tags = {
Name = 'main'
}
}
The resource is identified in state by its name attribute or a hash of its attributes.
Named Resources (Let Bindings)
Use let to bind a name to a resource so you can reference its attributes:
let vpc = awscc.ec2.Vpc {
cidr_block = '10.0.0.0/16'
tags = {
Name = 'main'
}
}
let subnet = awscc.ec2.Subnet {
vpc_id = vpc.vpc_id
cidr_block = '10.0.1.0/24'
availability_zone = 'ap-northeast-1a'
}
The let keyword is also used to bind non-resource values:
let cidr = '10.0.0.0/16'
let environments = ['dev', 'stg', 'prod']
let config = {
dev = '10.0.0.0/16'
stg = '10.1.0.0/16'
}
Discard Pattern
Use let _ = when you want to evaluate an expression but do not need to reference the result:
let _ = awscc.ec2.VpcGatewayAttachment {
vpc_id = vpc.vpc_id
internet_gateway_id = igw.internet_gateway_id
}
Data Sources (Read Resources)
Use the read keyword to query existing cloud resources without managing them:
let identity = read aws.sts.CallerIdentity {}
The returned value can be referenced like any other bound resource:
let identity = read aws.sts.CallerIdentity {}
awscc.ec2.IpamPool {
source_resource = {
resource_owner = identity.account_id
}
}
Attributes
Attributes are key-value pairs inside a resource block:
awscc.ec2.Vpc {
cidr_block = '10.0.0.0/16'
enable_dns_support = true
enable_dns_hostnames = true
}
Nested Blocks
Some resources accept nested blocks for repeated or structured configuration:
awscc.ec2.Ipam {
tier = advanced
operating_region {
region_name = 'ap-northeast-1'
}
}
Local Bindings Inside Blocks
Use let inside a resource block to create block-scoped variables. These are evaluated during parsing but are not sent to the provider:
awscc.ec2.Vpc {
let base_cidr = '10.0.0.0/16'
cidr_block = base_cidr
tags = {
Name = "vpc-${base_cidr}"
}
}
Backend Block
The backend block configures where Carina stores state:
backend s3 {
bucket = 'my-state-bucket'
key = 'infra/carina.state.json'
region = 'ap-northeast-1'
}
When no backend is configured, state is stored locally in carina.state.json.
State Manipulation
Import
Import an existing cloud resource into Carina’s state:
import {
to = awscc.ec2.Vpc 'imported_vpc'
id = 'vpc-0123456789abcdef0'
}
awscc.ec2.Vpc {
cidr_block = '10.0.0.0/16'
tags = {
Name = 'imported'
}
}
Moved
Rename a resource in state without destroying and recreating it:
moved {
from = awscc.ec2.Vpc 'old_name'
to = awscc.ec2.Vpc 'new_name'
}
Removed
Remove a resource from Carina’s state without deleting the actual cloud resource:
removed {
from = awscc.ec2.Vpc 'old_vpc'
}
Upstream State
Reference values exported by another Carina project:
let network = upstream_state {
source = "../network"
}
awscc.ec2.SecurityGroup {
vpc_id = network.vpc_id
}
source is required and points at the upstream project’s directory. The path is resolved relative to the enclosing .crn file’s directory. Carina loads the upstream’s configuration, resolves its backend, reads its state, and exposes the values published by its exports block through the declared binding.
External References at a Glance
Carina has four constructs for pulling data or declarations in from outside the current file. They look different because they do different things; the table below shows the split on when the data is fetched, what is fetched, and from where:
| Construct | When | What | From |
|---|---|---|---|
let m = use { source = '...' } | parse time | DSL source (a module) | local directory |
let x = read <type> { } | plan time | current cloud state | Cloud API (data source) |
import { to = ..., id = ... } | apply time | an existing cloud resource, adopted into state | Cloud API + state backend |
let x = upstream_state { source = '...' } | plan time | another Carina project’s exports | state backend |
The shape differences track these semantic differences:
useandupstream_stateboth name an external location via asourceattribute and share their shape accordingly.readleads with the resource type because the type is the primary piece of information for a live data-source read.importis a top-level state-manipulation block parallel tomoved/removed, not alet-RHS expression.
Comments
Carina supports three comment styles:
# Hash-style line comment
// Slash-style line comment
/* Block comment
can span multiple lines */
/* Block comments /* can be nested */ like this */
All comments are ignored by the parser.
User-Defined Functions
Define reusable functions with fn:
fn tag_name(env: String, service: String): String {
join('-', [env, service, 'vpc'])
}
awscc.ec2.Vpc {
cidr_block = '10.0.0.0/16'
tags = {
Name = tag_name('prod', 'web')
}
}
See Expressions for details on function definitions.
Require Statement
Assert conditions that must be true, with a custom error message if they fail:
require length(subnets) > 0, 'At least one subnet must be specified'
require cidr_block != '', 'CIDR block cannot be empty'
The condition is a validate expression that supports comparison operators (==, !=, >, <, >=, <=) and logical operators (&&, ||, !).