Skip to content

Modules

Modules are reusable units of Carina configuration. A module defines a set of resources along with input parameters (arguments) and output values (attributes).

Module Structure

A module is a .crn file (or a directory containing main.crn) that declares arguments and attributes blocks.

modules/network/main.crn
arguments {
cidr_block : string
subnet_cidr: string
az : string
}
let vpc = awscc.ec2.vpc {
cidr_block = cidr_block
tags = {
Name = 'module-test'
}
}
let subnet = awscc.ec2.subnet {
vpc_id = vpc.vpc_id
cidr_block = subnet_cidr
availability_zone = az
tags = {
Name = 'module-test-subnet'
}
}
attributes {
vpc_id : awscc.ec2.vpc = vpc.vpc_id
subnet_id: awscc.ec2.subnet = subnet.subnet_id
}

Arguments Block

The arguments block defines the parameters a module accepts from its caller.

Basic Parameters

Each parameter has a name and a type:

arguments {
cidr_block: string
count : int
enabled : bool
}

Default Values

Parameters can have default values:

arguments {
cidr_block: string
az : string = 'ap-northeast-1a'
enable_dns: bool = true
}

Block Form with Description and Validation

Parameters can use an expanded block form for description, default values, and validation rules:

arguments {
instance_count: int {
description = 'Number of instances to create'
default = 1
validation {
condition = instance_count >= 1 && instance_count <= 10
error_message = 'Instance count must be between 1 and 10'
}
}
cidr_block: string {
description = 'The CIDR block for the VPC'
validation {
condition = length(cidr_block) > 0
error_message = 'CIDR block cannot be empty'
}
}
}

Supported Types

Parameter types can be any type expression:

arguments {
name : string
count : int
ratio : float
enabled : bool
subnet_ids : list(string)
tags : map(string)
cidr_block : cidr
role_arn : arn
}

Attributes Block

The attributes block defines the output values that a module exposes to its caller.

Each attribute has a name, an optional type, and a value expression:

attributes {
vpc_id : awscc.ec2.vpc = vpc.vpc_id
subnet_id : awscc.ec2.subnet = subnet.subnet_id
route_table_id: awscc.ec2.route_table = rt.route_table_id
}

The type annotation is optional; you can also use the simpler form:

attributes {
vpc_id = vpc.vpc_id
}

Importing Modules

Use import to load a module, then call it by name with arguments:

let network = import './modules/network'
network {
cidr_block = '10.0.0.0/16'
subnet_cidr = '10.0.1.0/24'
az = 'ap-northeast-1a'
}

Accessing Module Outputs

When a module call is bound with let, its attributes values can be accessed with dot notation:

let network = import './modules/network'
let net = network {
cidr_block = '10.0.0.0/16'
subnet_cidr = '10.0.1.0/24'
az = 'ap-northeast-1a'
}
awscc.ec2.security_group {
vpc_id = net.vpc_id
}

Directory Modules

A module can be either:

  • A single file: import "./modules/network.crn" (the .crn extension can be omitted)
  • A directory: import "./modules/network" which loads ./modules/network/main.crn

Directory modules are useful when a module needs helper files or becomes complex enough to warrant its own directory.

Module Resolution

Import paths are resolved relative to the file containing the import statement:

# From project/main.crn, imports project/modules/network/main.crn
let network = import './modules/network'
# Relative path from current file
let helper = import '../shared/helper'

Nested Modules

Modules can import and use other modules:

modules/network_with_rt/main.crn
let network = import '../network'
arguments {
cidr_block : string
subnet_cidr: string
az : string
}
let net = network {
cidr_block = cidr_block
subnet_cidr = subnet_cidr
az = az
}
let rt = awscc.ec2.route_table {
vpc_id = net.vpc_id
tags = {
Name = 'nested-module-test-rt'
}
}
attributes {
vpc_id : awscc.ec2.vpc = net.vpc_id
route_table_id: awscc.ec2.route_table = rt.route_table_id
}

Modules with For Expressions

Modules can be used inside for expressions to create multiple instances:

let network = import './modules/network'
let cidrs = {
dev = '10.0.0.0/16'
stg = '10.1.0.0/16'
}
let networks = for name, cidr in cidrs {
network {
cidr_block = cidr
subnet_cidr = cidr_subnet(cidr, 8, 1)
az = 'ap-northeast-1a'
}
}

This creates a separate set of network resources for each entry in the map. The name variable (dev, stg) is used to distinguish the resources in state.

Provider Configuration

Modules do not declare their own provider blocks. The provider configuration from the root .crn file is inherited by all modules. A module only needs to declare arguments, attributes, and resources.