Carina

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:

  1. Does the block’s body schema depend on a kind label? The attributes valid inside provider awscc { ... } differ from those inside provider aws { ... }, and similarly for backend kinds. When the schema varies by kind, the construct carries a kind label (provider <kind>, backend <kind>).
  2. 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 let binding, and the binding name on the left is how the value is referenced later.

The two questions are orthogonal:

kind label neededkind label not needed
singleton / not named-referencedprovider awscc { ... }, backend s3 { ... }
named referencelet us = provider awscc { ... }let orgs = upstream_state { ... }
  • provider / backend carry kind labels because the body schema is kind-specific. A bare provider <kind> { ... } block declares the kind’s default instance; wrapping the same expression in let <name> = provider <kind> { ... } declares a named instance. See Named provider instances below.
  • upstream_state has 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:

StatementPurpose
providerConfigure a cloud provider
backendConfigure remote state storage
letBind a name to a resource or value
Anonymous resourceDeclare a resource without a binding
import (state)Import an existing cloud resource into state
removedRemove a resource from state without deleting it
movedRename a resource in state
forIterate to create multiple resources
ifConditionally create resources
fnDefine a reusable function
requireAssert a condition with an error message
argumentsDeclare module input parameters
attributesDefine module output values (for callers who bind the module with let)
exportsDefine 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:

ConstructWhenWhatFrom
let m = use { source = '...' }parse timeDSL source (a module)local directory
let x = read <type> { }plan timecurrent cloud stateCloud API (data source)
import { to = ..., id = ... }apply timean existing cloud resource, adopted into stateCloud API + state backend
let x = upstream_state { source = '...' }plan timeanother Carina project’s exportsstate backend

The shape differences track these semantic differences:

  • use and upstream_state both name an external location via a source attribute and share their shape accordingly.
  • read leads with the resource type because the type is the primary piece of information for a live data-source read.
  • import is a top-level state-manipulation block parallel to moved / removed, not a let-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 (&&, ||, !).