651 lines
12 KiB
Markdown
651 lines
12 KiB
Markdown
|
|
# 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
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# 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:
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
{
|
||
|
|
name = "my-server"
|
||
|
|
plan = "medium"
|
||
|
|
zone = "de-fra1"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Type Annotations
|
||
|
|
|
||
|
|
Add type safety with contracts:
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
{
|
||
|
|
name : String = "my-server"
|
||
|
|
plan : String = "medium"
|
||
|
|
cpu_count : Number = 4
|
||
|
|
enabled : Bool = true
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Record Merging
|
||
|
|
|
||
|
|
Compose configurations by merging records:
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
let base_config = {
|
||
|
|
provider = "upcloud"
|
||
|
|
region = "de-fra1"
|
||
|
|
} in
|
||
|
|
|
||
|
|
let server_config = base_config & {
|
||
|
|
name = "web-01"
|
||
|
|
plan = "medium"
|
||
|
|
} in
|
||
|
|
|
||
|
|
server_config
|
||
|
|
```
|
||
|
|
|
||
|
|
Result:
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
{
|
||
|
|
provider = "upcloud"
|
||
|
|
region = "de-fra1"
|
||
|
|
name = "web-01"
|
||
|
|
plan = "medium"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Contracts (Schema Validation)
|
||
|
|
|
||
|
|
Define contracts to validate structure:
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
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:
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
# 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:
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
# 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:
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
# 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
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
# 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:
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
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)
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
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
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
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:
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
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:
|
||
|
|
|
||
|
|
```text
|
||
|
|
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
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
{
|
||
|
|
string_field : String = "text"
|
||
|
|
number_field : Number = 42
|
||
|
|
bool_field : Bool = true
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Array Types
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
{
|
||
|
|
names : Array String = ["alice", "bob", "charlie"]
|
||
|
|
ports : Array Number = [80, 443, 8080]
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Enum Types
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
{
|
||
|
|
environment : | [ 'development, 'staging, 'production | ] = 'production
|
||
|
|
role : | [ 'control, 'worker, 'standalone | ] = 'worker
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Optional Fields
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
{
|
||
|
|
required_field : String = "value"
|
||
|
|
optional_field : String | optional
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Default Values
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
{
|
||
|
|
with_default : String | default = "default-value"
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Validation Patterns
|
||
|
|
|
||
|
|
### Runtime Validation
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
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
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
let PlanContract = | [ 'small, 'medium, 'large | ] in
|
||
|
|
|
||
|
|
{
|
||
|
|
plan | PlanContract = 'medium
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Real-World Examples
|
||
|
|
|
||
|
|
### Simple Server Configuration
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
{
|
||
|
|
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
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
{
|
||
|
|
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
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
{
|
||
|
|
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
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# 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
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# 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:
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# 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
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"nickel.lsp.command": "nickel-lang-lsp",
|
||
|
|
"nickel.lsp.args": ["--stdio"],
|
||
|
|
"nickel.format.onSave": true
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Common Patterns
|
||
|
|
|
||
|
|
### Environment-Specific Configuration
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
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
|
||
|
|
|
||
|
|
```nickel
|
||
|
|
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.
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# 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
|
||
|
|
|
||
|
|
1. **Use Three-File Pattern**: Separate contracts, defaults, and main entry
|
||
|
|
2. **Type Everything**: Add type annotations for all fields
|
||
|
|
3. **Validate Early**: Run `nickel typecheck` before deployment
|
||
|
|
4. **Use Makers**: Leverage maker functions for composition
|
||
|
|
5. **Document Contracts**: Add comments explaining schema requirements
|
||
|
|
6. **Avoid Duplication**: Use record merging and defaults
|
||
|
|
7. **Test Locally**: Export and verify before deploying
|
||
|
|
8. **Version Schemas**: Track schema changes in version control
|
||
|
|
|
||
|
|
## Debugging
|
||
|
|
|
||
|
|
### Type Errors
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Detailed type error messages
|
||
|
|
nickel typecheck --color always infra/cluster.ncl
|
||
|
|
```
|
||
|
|
|
||
|
|
### Schema Inspection
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Export to JSON for inspection
|
||
|
|
nickel export infra/cluster.ncl | jq '.'
|
||
|
|
|
||
|
|
# Check specific field
|
||
|
|
nickel export infra/cluster.ncl | jq '.metadata'
|
||
|
|
```
|
||
|
|
|
||
|
|
### Format Code
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# Auto-format Nickel files
|
||
|
|
nickel fmt infra/cluster.ncl
|
||
|
|
|
||
|
|
# Check formatting without modifying
|
||
|
|
nickel fmt --check infra/cluster.ncl
|
||
|
|
```
|
||
|
|
|
||
|
|
## Next Steps
|
||
|
|
|
||
|
|
- [Schemas Reference](schemas-reference.md) - Platform schema organization
|
||
|
|
- [Configuration System](configuration-system.md) - Hierarchical configuration
|
||
|
|
- [Providers](providers.md) - Cloud provider schemas
|
||
|
|
- [Batch Workflows](batch-workflows.md) - Multi-cloud orchestration with Nickel
|