# First-Boot Automation for Bare-Metal Provisioning Automated cluster joining and service initialization for bare-metal provisioned NixOS nodes. ## Table of Contents - [Overview](#overview) - [Quick Start](#quick-start) - [Configuration](#configuration) - [Bootstrap vs Join](#bootstrap-vs-join) - [Systemd Services](#systemd-services) - [Troubleshooting](#troubleshooting) - [Manual Operations](#manual-operations) - [Security](#security) - [Examples](#examples) ## Overview The first-boot automation system handles automated cluster joining for distributed services (Chainfire, FlareDB, IAM) on first boot of bare-metal provisioned nodes. It supports two modes: - **Bootstrap Mode**: Initialize a new Raft cluster (first 3 nodes) - **Join Mode**: Join an existing cluster (additional nodes) ### Features - Automated health checking with retries - Idempotent operations (safe to run multiple times) - Structured JSON logging to journald - Graceful failure handling with configurable retries - Integration with TLS certificates (T031) - Support for both bootstrap and runtime join scenarios ### Architecture See [ARCHITECTURE.md](ARCHITECTURE.md) for detailed design documentation. ## Quick Start ### Prerequisites 1. Node provisioned via T032.S1-S3 (PXE boot and installation) 2. Cluster configuration file at `/etc/nixos/secrets/cluster-config.json` 3. TLS certificates at `/etc/nixos/secrets/` (T031) 4. Network connectivity to cluster leader (for join mode) ### Enable First-Boot Automation In your NixOS configuration: ```nix # /etc/nixos/configuration.nix { imports = [ ./nix/modules/first-boot-automation.nix ]; services.first-boot-automation = { enable = true; configFile = "/etc/nixos/secrets/cluster-config.json"; # Optional: disable specific services enableChainfire = true; enableFlareDB = true; enableIAM = true; enableHealthCheck = true; }; } ``` ### First Boot After provisioning and reboot: 1. Node boots from disk 2. systemd starts services 3. First-boot automation runs automatically 4. Cluster join completes within 30-60 seconds Check status: ```bash systemctl status chainfire-cluster-join.service systemctl status flaredb-cluster-join.service systemctl status iam-initial-setup.service systemctl status cluster-health-check.service ``` ## Configuration ### cluster-config.json Format ```json { "node_id": "node01", "node_role": "control-plane", "bootstrap": true, "cluster_name": "prod-cluster", "leader_url": "https://node01.prod.example.com:2379", "raft_addr": "10.0.1.10:2380", "initial_peers": [ "node01:2380", "node02:2380", "node03:2380" ], "flaredb_peers": [ "node01:2480", "node02:2480", "node03:2480" ] } ``` ### Required Fields | Field | Type | Description | |-------|------|-------------| | `node_id` | string | Unique identifier for this node | | `node_role` | string | Node role: `control-plane`, `worker`, or `all-in-one` | | `bootstrap` | boolean | `true` for first 3 nodes, `false` for additional nodes | | `cluster_name` | string | Cluster identifier | | `leader_url` | string | HTTPS URL of cluster leader (used for join) | | `raft_addr` | string | This node's Raft address (IP:port) | | `initial_peers` | array | List of bootstrap peer addresses | | `flaredb_peers` | array | List of FlareDB peer addresses | ### Optional Fields | Field | Type | Description | |-------|------|-------------| | `node_ip` | string | Node's primary IP address | | `node_fqdn` | string | Fully qualified domain name | | `datacenter` | string | Datacenter identifier | | `rack` | string | Rack identifier | | `services` | object | Per-service configuration | | `tls` | object | TLS certificate paths | | `network` | object | Network CIDR ranges | ### Example Configurations See [examples/](examples/) directory: - `cluster-config-bootstrap.json` - Bootstrap node (first 3) - `cluster-config-join.json` - Join node (additional) - `cluster-config-all-in-one.json` - Single-node deployment ## Bootstrap vs Join ### Bootstrap Mode (bootstrap: true) **When to use:** - First 3 nodes in a new cluster - Nodes configured with matching `initial_peers` - No existing cluster to join **Behavior:** 1. Services start with `--initial-cluster` configuration 2. Raft consensus automatically elects leader 3. Cluster join service detects bootstrap mode and exits immediately 4. Marker file created: `/var/lib/first-boot-automation/.chainfire-initialized` **Example:** ```json { "node_id": "node01", "bootstrap": true, "initial_peers": ["node01:2380", "node02:2380", "node03:2380"] } ``` ### Join Mode (bootstrap: false) **When to use:** - Nodes joining an existing cluster - Expansion or replacement nodes - Leader is known and reachable **Behavior:** 1. Service starts with no initial cluster config 2. Waits for local service to be healthy (max 120s) 3. POST to leader's `/admin/member/add` endpoint 4. Retries up to 5 times with 10s delay 5. Marker file created: `/var/lib/first-boot-automation/.chainfire-joined` **Example:** ```json { "node_id": "node04", "bootstrap": false, "leader_url": "https://node01.prod.example.com:2379", "raft_addr": "10.0.1.13:2380" } ``` ### Decision Matrix | Scenario | bootstrap | initial_peers | leader_url | |----------|-----------|---------------|------------| | Node 1 (first) | `true` | all 3 nodes | self | | Node 2 (first) | `true` | all 3 nodes | self | | Node 3 (first) | `true` | all 3 nodes | self | | Node 4+ (join) | `false` | all 3 nodes | node 1 | ## Systemd Services ### chainfire-cluster-join.service **Description:** Joins Chainfire cluster on first boot **Dependencies:** - After: `network-online.target`, `chainfire.service` - Before: `flaredb-cluster-join.service` **Configuration:** - Type: `oneshot` - RemainAfterExit: `true` - Restart: `on-failure` **Logs:** ```bash journalctl -u chainfire-cluster-join.service ``` ### flaredb-cluster-join.service **Description:** Joins FlareDB cluster after Chainfire **Dependencies:** - After: `chainfire-cluster-join.service`, `flaredb.service` - Requires: `chainfire-cluster-join.service` **Configuration:** - Type: `oneshot` - RemainAfterExit: `true` - Restart: `on-failure` **Logs:** ```bash journalctl -u flaredb-cluster-join.service ``` ### iam-initial-setup.service **Description:** IAM initial setup and admin user creation **Dependencies:** - After: `flaredb-cluster-join.service`, `iam.service` **Configuration:** - Type: `oneshot` - RemainAfterExit: `true` **Logs:** ```bash journalctl -u iam-initial-setup.service ``` ### cluster-health-check.service **Description:** Validates cluster health on first boot **Dependencies:** - After: all cluster-join services **Configuration:** - Type: `oneshot` - RemainAfterExit: `false` **Logs:** ```bash journalctl -u cluster-health-check.service ``` ## Troubleshooting ### Check Service Status ```bash # Overall status systemctl status chainfire-cluster-join.service systemctl status flaredb-cluster-join.service # Detailed logs with JSON output journalctl -u chainfire-cluster-join.service -o json-pretty # Follow logs in real-time journalctl -u chainfire-cluster-join.service -f ``` ### Common Issues #### 1. Health Check Timeout **Symptom:** ```json {"level":"ERROR","message":"Health check timeout after 120s"} ``` **Causes:** - Service not starting (check main service logs) - Port conflict - TLS certificate issues **Solutions:** ```bash # Check main service systemctl status chainfire.service journalctl -u chainfire.service # Test health endpoint manually curl -k https://localhost:2379/health # Restart services systemctl restart chainfire.service systemctl restart chainfire-cluster-join.service ``` #### 2. Leader Unreachable **Symptom:** ```json {"level":"ERROR","message":"Join request failed: connection error"} ``` **Causes:** - Network connectivity issues - Firewall blocking ports - Leader not running - Wrong leader URL in config **Solutions:** ```bash # Test network connectivity ping node01.prod.example.com curl -k https://node01.prod.example.com:2379/health # Check firewall iptables -L -n | grep 2379 # Verify configuration jq '.leader_url' /etc/nixos/secrets/cluster-config.json # Try manual join (see below) ``` #### 3. Invalid Configuration **Symptom:** ```json {"level":"ERROR","message":"Configuration file not found"} ``` **Causes:** - Missing configuration file - Wrong file path - Invalid JSON syntax - Missing required fields **Solutions:** ```bash # Check file exists ls -la /etc/nixos/secrets/cluster-config.json # Validate JSON syntax jq . /etc/nixos/secrets/cluster-config.json # Check required fields jq '.node_id, .bootstrap, .leader_url' /etc/nixos/secrets/cluster-config.json # Fix and restart systemctl restart chainfire-cluster-join.service ``` #### 4. Already Member (Reboot) **Symptom:** ```json {"level":"WARN","message":"Already member of cluster (HTTP 409)"} ``` **Explanation:** - This is **normal** on reboots - Marker file prevents duplicate joins - No action needed **Verify:** ```bash # Check marker file cat /var/lib/first-boot-automation/.chainfire-joined # Should show timestamp: 2025-12-10T10:30:45+00:00 ``` #### 5. Join Retry Exhausted **Symptom:** ```json {"level":"ERROR","message":"Failed to join cluster after 5 attempts"} ``` **Causes:** - Persistent network issues - Leader down or overloaded - Invalid node configuration - Cluster at capacity **Solutions:** ```bash # Check cluster status on leader curl -k https://node01.prod.example.com:2379/admin/cluster/members | jq # Verify this node's configuration jq '.node_id, .raft_addr' /etc/nixos/secrets/cluster-config.json # Increase retry attempts (edit NixOS config) # Or perform manual join (see below) ``` ### Verify Cluster Membership **On leader node:** ```bash # Chainfire members curl -k https://localhost:2379/admin/cluster/members | jq # FlareDB members curl -k https://localhost:2479/admin/cluster/members | jq ``` **Expected output:** ```json { "members": [ {"id": "node01", "raft_addr": "10.0.1.10:2380", "status": "healthy"}, {"id": "node02", "raft_addr": "10.0.1.11:2380", "status": "healthy"}, {"id": "node03", "raft_addr": "10.0.1.12:2380", "status": "healthy"} ] } ``` ### Check Marker Files ```bash # List all marker files ls -la /var/lib/first-boot-automation/ # View timestamps cat /var/lib/first-boot-automation/.chainfire-joined cat /var/lib/first-boot-automation/.flaredb-joined ``` ### Reset and Re-join **Warning:** This will remove the node from the cluster and rejoin. ```bash # Stop services systemctl stop chainfire.service flaredb.service # Remove data and markers rm -rf /var/lib/chainfire/* rm -rf /var/lib/flaredb/* rm /var/lib/first-boot-automation/.chainfire-* rm /var/lib/first-boot-automation/.flaredb-* # Restart (will auto-join) systemctl start chainfire.service systemctl restart chainfire-cluster-join.service ``` ## Manual Operations ### Manual Cluster Join If automation fails, perform manual join: **Chainfire:** ```bash # On joining node, ensure service is running and healthy curl -k https://localhost:2379/health # From any node, add member to cluster curl -k -X POST https://node01.prod.example.com:2379/admin/member/add \ -H "Content-Type: application/json" \ -d '{ "id": "node04", "raft_addr": "10.0.1.13:2380" }' # Create marker to prevent auto-retry mkdir -p /var/lib/first-boot-automation date -Iseconds > /var/lib/first-boot-automation/.chainfire-joined ``` **FlareDB:** ```bash curl -k -X POST https://node01.prod.example.com:2479/admin/member/add \ -H "Content-Type: application/json" \ -d '{ "id": "node04", "raft_addr": "10.0.1.13:2480" }' date -Iseconds > /var/lib/first-boot-automation/.flaredb-joined ``` ### Remove Node from Cluster **On leader:** ```bash # Chainfire curl -k -X DELETE https://node01.prod.example.com:2379/admin/member/node04 # FlareDB curl -k -X DELETE https://node01.prod.example.com:2479/admin/member/node04 ``` **On removed node:** ```bash # Stop services systemctl stop chainfire.service flaredb.service # Clean up data rm -rf /var/lib/chainfire/* rm -rf /var/lib/flaredb/* rm /var/lib/first-boot-automation/.chainfire-* rm /var/lib/first-boot-automation/.flaredb-* ``` ### Disable First-Boot Automation If you need to disable automation: ```nix # In NixOS configuration services.first-boot-automation.enable = false; ``` Or stop services temporarily: ```bash systemctl stop chainfire-cluster-join.service systemctl disable chainfire-cluster-join.service ``` ### Re-enable After Manual Operations After manual cluster operations: ```bash # Create marker files to indicate join complete mkdir -p /var/lib/first-boot-automation date -Iseconds > /var/lib/first-boot-automation/.chainfire-joined date -Iseconds > /var/lib/first-boot-automation/.flaredb-joined # Or re-enable automation (will skip if markers exist) systemctl enable --now chainfire-cluster-join.service ``` ## Security ### TLS Certificates **Requirements:** - All cluster communication uses TLS - Certificates must exist before first boot - Generated by T031 TLS automation **Certificate Paths:** ``` /etc/nixos/secrets/ ├── ca.crt # CA certificate ├── node01.crt # Node certificate └── node01.key # Node private key (mode 0600) ``` **Permissions:** ```bash chmod 600 /etc/nixos/secrets/node01.key chmod 644 /etc/nixos/secrets/node01.crt chmod 644 /etc/nixos/secrets/ca.crt ``` ### Configuration File Security **Cluster configuration contains sensitive data:** - IP addresses and network topology - Service URLs - Node identifiers **Recommended permissions:** ```bash chmod 600 /etc/nixos/secrets/cluster-config.json chown root:root /etc/nixos/secrets/cluster-config.json ``` ### Network Security **Required firewall rules:** ```bash # Chainfire iptables -A INPUT -p tcp --dport 2379 -s 10.0.1.0/24 -j ACCEPT # API iptables -A INPUT -p tcp --dport 2380 -s 10.0.1.0/24 -j ACCEPT # Raft iptables -A INPUT -p tcp --dport 2381 -s 10.0.1.0/24 -j ACCEPT # Gossip # FlareDB iptables -A INPUT -p tcp --dport 2479 -s 10.0.1.0/24 -j ACCEPT # API iptables -A INPUT -p tcp --dport 2480 -s 10.0.1.0/24 -j ACCEPT # Raft # IAM iptables -A INPUT -p tcp --dport 8080 -s 10.0.1.0/24 -j ACCEPT # API ``` ### Production Considerations **For production deployments:** 1. **Remove `-k` flag from curl** (validate TLS certificates) 2. **Implement mTLS** for client authentication 3. **Rotate credentials** regularly 4. **Audit logs** with structured logging 5. **Monitor health endpoints** continuously 6. **Backup cluster state** before changes ## Examples ### Example 1: 3-Node Bootstrap Cluster **Node 1:** ```json { "node_id": "node01", "bootstrap": true, "raft_addr": "10.0.1.10:2380", "initial_peers": ["node01:2380", "node02:2380", "node03:2380"] } ``` **Node 2:** ```json { "node_id": "node02", "bootstrap": true, "raft_addr": "10.0.1.11:2380", "initial_peers": ["node01:2380", "node02:2380", "node03:2380"] } ``` **Node 3:** ```json { "node_id": "node03", "bootstrap": true, "raft_addr": "10.0.1.12:2380", "initial_peers": ["node01:2380", "node02:2380", "node03:2380"] } ``` **Provisioning:** ```bash # Provision all 3 nodes simultaneously for i in {1..3}; do nixos-anywhere --flake .#node0$i root@node0$i.example.com & done wait # Nodes will bootstrap automatically on first boot ``` ### Example 2: Join Existing Cluster **Node 4 (joining):** ```json { "node_id": "node04", "bootstrap": false, "leader_url": "https://node01.prod.example.com:2379", "raft_addr": "10.0.1.13:2380" } ``` **Provisioning:** ```bash nixos-anywhere --flake .#node04 root@node04.example.com # Node will automatically join on first boot ``` ### Example 3: Single-Node All-in-One **For development/testing:** ```json { "node_id": "aio01", "bootstrap": true, "raft_addr": "10.0.2.10:2380", "initial_peers": ["aio01:2380"], "flaredb_peers": ["aio01:2480"] } ``` **Provisioning:** ```bash nixos-anywhere --flake .#aio01 root@aio01.example.com ``` ## Integration with Other Systems ### T024 NixOS Modules First-boot automation integrates with service modules: ```nix { imports = [ ./nix/modules/chainfire.nix ./nix/modules/flaredb.nix ./nix/modules/first-boot-automation.nix ]; services.chainfire.enable = true; services.flaredb.enable = true; services.first-boot-automation.enable = true; } ``` ### T025 Observability Health checks integrate with Prometheus: ```yaml # prometheus.yml scrape_configs: - job_name: 'cluster-health' static_configs: - targets: ['node01:2379', 'node02:2379', 'node03:2379'] metrics_path: '/health' ``` ### T031 TLS Certificates Certificates generated by T031 are used automatically: ```bash # On provisioning server ./tls/generate-node-cert.sh node01.example.com 10.0.1.10 # Copied during nixos-anywhere # First-boot automation reads from /etc/nixos/secrets/ ``` ## Logs and Debugging ### Structured Logging All logs are JSON-formatted: ```json { "timestamp": "2025-12-10T10:30:45+00:00", "level": "INFO", "service": "chainfire", "operation": "cluster-join", "message": "Successfully joined cluster" } ``` ### Query Examples **All first-boot logs:** ```bash journalctl -u "*cluster-join*" -u "*initial-setup*" -u "*health-check*" ``` **Errors only:** ```bash journalctl -u chainfire-cluster-join.service | grep '"level":"ERROR"' ``` **Last boot only:** ```bash journalctl -b -u chainfire-cluster-join.service ``` **JSON output for parsing:** ```bash journalctl -u chainfire-cluster-join.service -o json | jq '.MESSAGE' ``` ## Performance Tuning ### Timeout Configuration Adjust timeouts in NixOS module: ```nix services.first-boot-automation = { enable = true; # Override default ports if needed chainfirePort = 2379; flaredbPort = 2479; }; ``` ### Retry Configuration Modify retry logic in scripts: ```bash # baremetal/first-boot/cluster-join.sh MAX_ATTEMPTS=10 # Increase from 5 RETRY_DELAY=15 # Increase from 10s ``` ### Health Check Interval Adjust polling interval: ```bash # In service scripts sleep 10 # Increase from 5s for less aggressive polling ``` ## Support and Contributing ### Getting Help 1. Check logs: `journalctl -u chainfire-cluster-join.service` 2. Review troubleshooting section above 3. Consult [ARCHITECTURE.md](ARCHITECTURE.md) for design details 4. Check cluster status on leader node ### Reporting Issues Include in bug reports: ```bash # Gather diagnostic information journalctl -u chainfire-cluster-join.service > cluster-join.log systemctl status chainfire-cluster-join.service > service-status.txt cat /etc/nixos/secrets/cluster-config.json > config.json # Redact sensitive data! ls -la /var/lib/first-boot-automation/ > markers.txt ``` ### Development See [ARCHITECTURE.md](ARCHITECTURE.md) for contributing guidelines. ## References - **ARCHITECTURE.md**: Detailed design documentation - **T024**: NixOS service modules - **T025**: Observability and monitoring - **T031**: TLS certificate automation - **T032.S1-S3**: PXE boot and provisioning - **Design Document**: `/home/centra/cloud/docs/por/T032-baremetal-provisioning/design.md` ## License Internal use only - Centra Cloud Platform