12 KiB
Nickel Guide
Comprehensive guide to using Nickel as the infrastructure-as-code language for the Provisioning platform.
Critical Principle: Nickel is Source of Truth
TYPE-SAFETY ALWAYS REQUIRED: ALL configurations MUST be type-safe and validated via Nickel. TOML is NOT acceptable as source of truth. Validation is NOT optional, NOT "progressive", NOT "production-only". This applies to ALL profiles (developer, production, cicd).
Nickel is the PRIMARY IaC language. TOML files are GENERATED OUTPUT ONLY, never the source.
Why Nickel
Nickel provides:
- Type Safety: Static type checking catches errors before deployment
- Lazy Evaluation: Efficient configuration composition and merging
- Contract System: Schema validation with gradual typing
- Record Merging: Powerful composition without duplication
- LSP Support: IDE integration for autocomplete and validation
- Human-Readable: Clear syntax for infrastructure definition
Installation
# macOS (Homebrew)
brew install nickel
# Linux (Cargo)
cargo install nickel-lang-cli
# Verify installation
nickel --version # 1.15.1+
Core Concepts
Records and Fields
Records are the fundamental data structure in Nickel:
{
name = "my-server"
plan = "medium"
zone = "de-fra1"
}
Type Annotations
Add type safety with contracts:
{
name : String = "my-server"
plan : String = "medium"
cpu_count : Number = 4
enabled : Bool = true
}
Record Merging
Compose configurations by merging records:
let base_config = {
provider = "upcloud"
region = "de-fra1"
} in
let server_config = base_config & {
name = "web-01"
plan = "medium"
} in
server_config
Result:
{
provider = "upcloud"
region = "de-fra1"
name = "web-01"
plan = "medium"
}
Contracts (Schema Validation)
Define contracts to validate structure:
let ServerContract = {
name | String
plan | String | default = "small"
zone | String | default = "de-fra1"
cpu | Number | optional
} in
{
name = "my-server"
plan = "large"
} | ServerContract
Three-File Pattern (Provisioning Standard)
The platform uses a standardized three-file pattern for all schemas:
1. contracts.ncl - Type Definitions
Defines the schema contracts:
# contracts.ncl
{
Server = {
name | String
plan | String | default = "small"
zone | String | default = "de-fra1"
disk_size_gb | Number | default = 25
backup_enabled | Bool | default = false
role | | [ 'control, 'worker, 'standalone | ] | optional
}
Infrastructure = {
servers | Array Server
provider | String
environment | | [ 'development, 'staging, 'production | ]
}
}
2. defaults.ncl - Default Values
Provides sensible defaults:
# defaults.ncl
{
server = {
name = "unnamed-server"
plan = "small"
zone = "de-fra1"
disk_size_gb = 25
backup_enabled = false
}
infrastructure = {
servers = []
provider = "local"
environment = 'development
}
}
3. main.ncl - Entry Point
Combines contracts and defaults, provides makers:
# main.ncl
let contracts_lib = import "./contracts.ncl" in
let defaults_lib = import "./defaults.ncl" in
{
# Direct access to defaults (for inspection)
defaults = defaults_lib
# Convenience makers (90% of use cases)
make_server | not_exported = fun overrides =>
defaults_lib.server & overrides
make_infrastructure | not_exported = fun overrides =>
defaults_lib.infrastructure & overrides
# Default instances (bare defaults)
DefaultServer = defaults_lib.server
DefaultInfrastructure = defaults_lib.infrastructure
}
Usage Example
# user-infra.ncl
let infra_lib = import "provisioning/schemas/infrastructure/main.ncl" in
infra_lib.make_infrastructure {
provider = "upcloud"
environment = 'production
servers = [
infra_lib.make_server {
name = "web-01"
plan = "medium"
backup_enabled = true
}
infra_lib.make_server {
name = "web-02"
plan = "medium"
backup_enabled = true
}
]
}
Hybrid Interface Pattern
Records can be used both as functions (makers) and as plain data:
let config_lib = import "./config.ncl" in
# Use as function (with overrides)
let custom_config = config_lib.make_server { name = "custom" } in
# Use as plain data (defaults)
let default_config = config_lib.DefaultServer in
{
custom = custom_config
default = default_config
}
Record Merging Strategies
Priority Merging (Default)
let base = { a = 1, b = 2 } in
let override = { b = 3, c = 4 } in
base & override
# Result: { a = 1, b = 3, c = 4 }
Recursive Merging
let base = {
server = { cpu = 2, ram = 4 }
} in
let override = {
server = { ram = 8, disk = 100 }
} in
std.record.merge_all [base, override]
# Result: { server = { cpu = 2, ram = 8, disk = 100 } }
Lazy Evaluation
Nickel evaluates expressions lazily, only when needed:
let expensive_computation = std.string.join " " ["a", "b", "c"] in
{
# Only evaluated when accessed
computed_field = expensive_computation
# Conditional evaluation
conditional = if environment == 'production then
expensive_computation
else
"dev-value"
}
Schema Organization
The platform organizes Nickel schemas by domain:
provisioning/schemas/
├── main.ncl # Top-level entry point
├── config/ # Configuration schemas
│ ├── settings/
│ │ ├── main.ncl
│ │ ├── contracts.ncl
│ │ └── defaults.ncl
│ └── defaults/
│ ├── main.ncl
│ ├── contracts.ncl
│ └── defaults.ncl
├── infrastructure/ # Infrastructure definitions
│ ├── servers/
│ ├── networks/
│ └── storage/
├── deployment/ # Deployment schemas
├── services/ # Service configurations
├── operations/ # Operational schemas
└── generator/ # Runtime schema generation
Type System
Primitive Types
{
string_field : String = "text"
number_field : Number = 42
bool_field : Bool = true
}
Array Types
{
names : Array String = ["alice", "bob", "charlie"]
ports : Array Number = [80, 443, 8080]
}
Enum Types
{
environment : | [ 'development, 'staging, 'production | ] = 'production
role : | [ 'control, 'worker, 'standalone | ] = 'worker
}
Optional Fields
{
required_field : String = "value"
optional_field : String | optional
}
Default Values
{
with_default : String | default = "default-value"
}
Validation Patterns
Runtime Validation
let validate_plan = fun plan =>
if plan == "small" | | plan == "medium" | | plan == "large" then
plan
else
std.fail "Invalid plan: must be small, medium, or large"
in
{
plan = validate_plan "medium"
}
Contract-Based Validation
let PlanContract = | [ 'small, 'medium, 'large | ] in
{
plan | PlanContract = 'medium
}
Real-World Examples
Simple Server Configuration
{
metadata = {
name = "demo-server"
provider = "upcloud"
environment = 'development
}
infrastructure = {
servers = [
{
name = "web-01"
plan = "medium"
zone = "de-fra1"
disk_size_gb = 50
backup_enabled = true
role = 'standalone
}
]
}
services = {
taskservs = ["containerd", "docker"]
}
}
Kubernetes Cluster Configuration
{
metadata = {
name = "k8s-prod"
provider = "upcloud"
environment = 'production
}
infrastructure = {
servers = [
{
name = "k8s-control-01"
plan = "medium"
role = 'control
zone = "de-fra1"
disk_size_gb = 50
backup_enabled = true
}
{
name = "k8s-worker-01"
plan = "large"
role = 'worker
zone = "de-fra1"
disk_size_gb = 100
backup_enabled = true
}
{
name = "k8s-worker-02"
plan = "large"
role = 'worker
zone = "de-fra1"
disk_size_gb = 100
backup_enabled = true
}
]
}
services = {
taskservs = ["containerd", "etcd", "kubernetes", "cilium", "rook-ceph"]
}
kubernetes = {
version = "1.28.0"
pod_cidr = "10.244.0.0/16"
service_cidr = "10.96.0.0/12"
container_runtime = "containerd"
cri_socket = "/run/containerd/containerd.sock"
}
}
Multi-Provider Batch Workflow
{
batch_workflow = {
operations = [
{
id = "aws-cluster"
provider = "aws"
region = "us-east-1"
servers = [
{ name = "aws-web-01", plan = "t3.medium" }
]
}
{
id = "upcloud-cluster"
provider = "upcloud"
region = "de-fra1"
servers = [
{ name = "upcloud-web-01", plan = "medium" }
]
dependencies = ["aws-cluster"]
}
]
parallel_limit = 2
}
}
Validation Workflow
Type-Check Schema
# Check syntax and types
nickel typecheck infra/my-cluster.ncl
# Export to JSON (validates during export)
nickel export infra/my-cluster.ncl
# Export to TOML (generated output only)
nickel export --format toml infra/my-cluster.ncl > config.toml
Platform Validation
# Validate against platform contracts
provisioning validate config --infra my-cluster
# Verbose validation
provisioning validate config --verbose
IDE Integration
Language Server (nickel-lang-lsp)
Install LSP for IDE support:
# Install LSP server
cargo install nickel-lang-lsp
# Configure your editor (VS Code example)
# Install "Nickel" extension from marketplace
Features:
- Syntax highlighting
- Type checking on save
- Autocomplete
- Hover documentation
- Go to definition
VS Code Configuration
{
"nickel.lsp.command": "nickel-lang-lsp",
"nickel.lsp.args": ["--stdio"],
"nickel.format.onSave": true
}
Common Patterns
Environment-Specific Configuration
let env_configs = {
development = {
plan = "small"
backup_enabled = false
}
production = {
plan = "large"
backup_enabled = true
}
} in
let environment = 'production in
{
servers = [
env_configs.%{std.string.from_enum environment} & {
name = "server-01"
}
]
}
Configuration Composition
let base_server = {
zone = "de-fra1"
backup_enabled = false
} in
let prod_overrides = {
backup_enabled = true
disk_size_gb = 100
} in
{
servers = [
base_server & { name = "dev-01" }
base_server & prod_overrides & { name = "prod-01" }
]
}
Migration from TOML
TOML is ONLY for generated output. Source is always Nickel.
# Generate TOML from Nickel (if needed for external tools)
nickel export --format toml infra/cluster.ncl > cluster.toml
# NEVER edit cluster.toml directly - edit cluster.ncl instead
Best Practices
- Use Three-File Pattern: Separate contracts, defaults, and main entry
- Type Everything: Add type annotations for all fields
- Validate Early: Run
nickel typecheckbefore deployment - Use Makers: Leverage maker functions for composition
- Document Contracts: Add comments explaining schema requirements
- Avoid Duplication: Use record merging and defaults
- Test Locally: Export and verify before deploying
- Version Schemas: Track schema changes in version control
Debugging
Type Errors
# Detailed type error messages
nickel typecheck --color always infra/cluster.ncl
Schema Inspection
# Export to JSON for inspection
nickel export infra/cluster.ncl | jq '.'
# Check specific field
nickel export infra/cluster.ncl | jq '.metadata'
Format Code
# Auto-format Nickel files
nickel fmt infra/cluster.ncl
# Check formatting without modifying
nickel fmt --check infra/cluster.ncl
Next Steps
- Schemas Reference - Platform schema organization
- Configuration System - Hierarchical configuration
- Providers - Cloud provider schemas
- Batch Workflows - Multi-cloud orchestration with Nickel