Raspberry Pi 進階維運
遠端管理多台 Pi(Ansible)
Ansible 安裝與設定
# 在管理機上安裝 Ansible // 從管理機統一控制所有 Pi
sudo apt install -y ansible // 安裝 Ansible
# 建立主機清單檔案 // 列出所有要管理的 Pi
cat > /etc/ansible/hosts << 'EOF' // 寫入主機清單
[pos_machines] // POS 機器群組
pos-store-a ansible_host=192.168.1.101 ansible_user=pi // 門市 A 的 POS
pos-store-b ansible_host=192.168.1.102 ansible_user=pi // 門市 B 的 POS
pos-store-c ansible_host=192.168.1.103 ansible_user=pi // 門市 C 的 POS
[kiosk_machines] // Kiosk 機器群組
kiosk-lobby ansible_host=192.168.1.201 ansible_user=pi // 大廳 Kiosk
kiosk-entrance ansible_host=192.168.1.202 ansible_user=pi // 入口 Kiosk
[all:vars] // 所有機器的共用變數
ansible_python_interpreter=/usr/bin/python3 // 指定 Python 路徑
EOF
Ansible Playbook 範例
# deploy_pos.yml // POS 部署 Playbook
---
- name: 部署 POS 應用程式 # Playbook 名稱 // 描述用途
hosts: pos_machines # 目標主機群組 // 所有 POS 機器
become: yes # 使用 sudo // 需要管理員權限
tasks: # 任務列表 // 依序執行
- name: 更新系統套件 # 任務名稱 // 確保系統最新
apt: # 使用 apt 模組 // 套件管理
update_cache: yes # 更新套件庫 // apt update
upgrade: yes # 升級套件 // apt upgrade
- name: 安裝 .NET Runtime # 任務名稱 // 安裝執行環境
shell: | # 使用 shell 模組 // 執行指令
curl -sSL https://dot.net/v1/dotnet-install.sh | bash # 安裝腳本 // 官方安裝
- name: 部署 POS 應用 # 任務名稱 // 複製應用程式
copy: # 使用 copy 模組 // 檔案複製
src: ./publish/ # 來源路徑 // 本地編譯結果
dest: /opt/pos-app/ # 目標路徑 // Pi 上的安裝位置
- name: 重啟 POS 服務 # 任務名稱 // 套用新版本
systemd: # 使用 systemd 模組 // 服務管理
name: pos-app # 服務名稱 // POS 應用服務
state: restarted # 狀態 // 重啟服務
批量執行指令
# 對所有 POS 機器執行指令 // 一次管理多台
ansible pos_machines -m shell -a "uptime" // 查看所有機器運行時間
# 批量重啟 Kiosk 服務 // 一次重啟所有 Kiosk
ansible kiosk_machines -m systemd -a "name=kiosk state=restarted" // 重啟服務
# 執行 Playbook // 部署更新到所有 POS
ansible-playbook deploy_pos.yml // 執行部署腳本
SSH Tunnel 穿越 NAT
💡 比喻:挖隧道回家 你的 Pi 在門市的區域網路裡,就像住在一個封閉社區裡的人。 SSH Tunnel 就是從社區裡「挖一條隧道」通到外面的伺服器, 這樣你在任何地方都能透過這條隧道連回門市的 Pi。
反向 SSH Tunnel
# 在 Pi 上建立反向隧道 // 從門市 Pi 連到雲端伺服器
ssh -fN \ // 背景執行(不開 shell)
-R 2222:localhost:22 \ // 反向轉發:雲端的 2222 → Pi 的 22
-o ServerAliveInterval=60 \ // 每 60 秒發送心跳
-o ServerAliveCountMax=3 \ // 心跳失敗 3 次就斷線
-o ExitOnForwardFailure=yes \ // 轉發失敗就退出
tunnel-user@cloud-server.com // 雲端伺服器帳號
# 從雲端伺服器連回 Pi // 透過隧道遠端管理
ssh -p 2222 pi@localhost // 連到本地 2222 就是連到 Pi
自動重連腳本
#!/bin/bash // 指定直譯器
# autossh_tunnel.sh // 自動重連 SSH 隧道
REMOTE_USER="tunnel" // 雲端伺服器帳號
REMOTE_HOST="cloud-server.com" // 雲端伺服器位址
REMOTE_PORT=2222 // 雲端轉發埠號
LOCAL_PORT=22 // 本地 SSH 埠號
LOG_FILE="/var/log/ssh-tunnel.log" // 隧道日誌檔
while true; do // 無限迴圈
echo "$(date): 建立 SSH 隧道..." >> $LOG_FILE // 記錄嘗試連線
# 建立隧道 // 斷線會自動跳到下一輪迴圈
ssh -N \ // 不開 shell
-R ${REMOTE_PORT}:localhost:${LOCAL_PORT} \ // 反向轉發
-o ServerAliveInterval=60 \ // 心跳間隔
-o ServerAliveCountMax=3 \ // 心跳重試
-o ExitOnForwardFailure=yes \ // 失敗就退出
-o StrictHostKeyChecking=no \ // 不檢查主機金鑰
${REMOTE_USER}@${REMOTE_HOST} // 遠端伺服器
echo "$(date): SSH 隧道斷開,10秒後重連..." >> $LOG_FILE // 記錄斷線
sleep 10 // 等 10 秒再重連
done
systemd service 設定
# SSH Tunnel 自動啟動服務 // 開機自動建立隧道
sudo cat > /etc/systemd/system/ssh-tunnel.service << 'EOF' // 寫入 service 檔
[Unit] // 單元設定
Description=SSH Reverse Tunnel // 服務描述
After=network-online.target // 網路就緒後啟動
Wants=network-online.target // 需要網路
[Service] // 服務設定
ExecStart=/home/pi/autossh_tunnel.sh // 啟動腳本
Restart=always // 總是重啟
RestartSec=10 // 重啟間隔
User=pi // 執行用戶
[Install] // 安裝設定
WantedBy=multi-user.target // 多用戶模式啟動
EOF
sudo systemctl enable ssh-tunnel.service // 啟用自動啟動
sudo systemctl start ssh-tunnel.service // 立即啟動
自動更新機制
Webhook 觸發更新
// 自動更新控制器 // 接收 Webhook 觸發部署
[ApiController] // 標記為 API 控制器
[Route("api/[controller]")] // 路由設定
public class DeployController : ControllerBase // 部署控制器
{
private readonly ILogger<DeployController> _logger; // 日誌記錄器
// 建構函式 // 注入日誌
public DeployController( // 建構部署控制器
ILogger<DeployController> logger) // 注入 Logger
{
_logger = logger; // 儲存日誌記錄器
}
// 接收部署 Webhook // GitHub Actions 完成後呼叫
[HttpPost("webhook")] // POST api/deploy/webhook
public IActionResult TriggerDeploy( // 觸發部署方法
[FromHeader(Name = "X-Deploy-Token")] string? token) // 部署令牌
{
// 驗證令牌 // 防止未授權的部署
if (token != Environment.GetEnvironmentVariable("DEPLOY_TOKEN")) // 比對令牌
{
_logger.LogWarning("收到未授權的部署請求"); // 記錄可疑行為
return Unauthorized(); // 拒絕未授權請求
}
// 執行部署腳本 // 背景更新應用程式
_logger.LogInformation("開始執行部署..."); // 記錄開始部署
Task.Run(() => ExecuteDeployScript()); // 背景執行部署
return Ok(new { message = "部署已觸發" }); // 回傳成功
}
// 執行部署腳本 // 實際的更新步驟
private void ExecuteDeployScript() // 部署腳本方法
{
try // 嘗試部署
{
var process = new System.Diagnostics.Process(); // 建立程序
process.StartInfo.FileName = "/home/pi/deploy.sh"; // 部署腳本路徑
process.StartInfo.UseShellExecute = false; // 不使用 shell
process.Start(); // 啟動部署
process.WaitForExit(); // 等待完成
_logger.LogInformation("部署完成,退出碼:{Code}", // 記錄結果
process.ExitCode); // 傳入退出碼
}
catch (Exception ex) // 捕捉例外
{
_logger.LogError(ex, "部署失敗"); // 記錄錯誤
}
}
}
系統監控
C# 系統監控服務
// 系統監控服務 // 監控 Pi 的 CPU、記憶體、溫度
public class SystemMonitorService // 系統監控服務類別
{
private readonly ILogger<SystemMonitorService> _logger; // 日誌記錄器
// 系統狀態模型 // 記錄各項系統指標
public class SystemStatus // 系統狀態類別
{
public double CpuUsagePercent { get; set; } // CPU 使用率
public double MemoryUsagePercent { get; set; } // 記憶體使用率
public double TemperatureCelsius { get; set; } // CPU 溫度
public double DiskUsagePercent { get; set; } // 磁碟使用率
public TimeSpan Uptime { get; set; } // 系統運行時間
public DateTime Timestamp { get; set; } = DateTime.UtcNow; // 記錄時間
}
// 建構函式 // 注入日誌
public SystemMonitorService( // 建構監控服務
ILogger<SystemMonitorService> logger) // 注入 Logger
{
_logger = logger; // 儲存日誌記錄器
}
// 取得系統狀態 // 讀取各項指標
public SystemStatus GetStatus() // 取得狀態方法
{
var status = new SystemStatus // 建立狀態物件
{
CpuUsagePercent = GetCpuUsage(), // 讀取 CPU 使用率
MemoryUsagePercent = GetMemoryUsage(), // 讀取記憶體使用率
TemperatureCelsius = GetCpuTemperature(), // 讀取 CPU 溫度
DiskUsagePercent = GetDiskUsage(), // 讀取磁碟使用率
Uptime = GetUptime() // 讀取運行時間
};
// 溫度過高警告 // 超過 70°C 就記錄警告
if (status.TemperatureCelsius > 70) // 溫度門檻
{
_logger.LogWarning("CPU 溫度過高:{Temp}°C", // 記錄警告
status.TemperatureCelsius); // 傳入溫度
}
return status; // 回傳狀態
}
// 讀取 CPU 溫度 // 從系統檔案取得
private double GetCpuTemperature() // CPU 溫度方法
{
try // 嘗試讀取
{
var temp = System.IO.File.ReadAllText( // 讀取溫度檔案
"/sys/class/thermal/thermal_zone0/temp"); // Linux 溫度檔路徑
return double.Parse(temp.Trim()) / 1000.0; // 轉換為攝氏度
}
catch // 讀取失敗
{
return -1; // 回傳 -1 表示無法讀取
}
}
// 讀取記憶體使用率 // 從 /proc/meminfo 取得
private double GetMemoryUsage() // 記憶體使用率方法
{
try // 嘗試讀取
{
var info = System.IO.File.ReadAllLines("/proc/meminfo"); // 讀取記憶體資訊
var total = ParseMemInfo(info, "MemTotal"); // 取得總記憶體
var available = ParseMemInfo(info, "MemAvailable"); // 取得可用記憶體
return total > 0 ? (1.0 - (double)available / total) * 100 : 0; // 計算使用率
}
catch // 讀取失敗
{
return -1; // 回傳 -1
}
}
private long ParseMemInfo(string[] lines, string key) => // 解析記憶體資訊
long.TryParse(lines // 從行列中尋找
.FirstOrDefault(l => l.StartsWith(key))? // 找到指定鍵
.Split(':').LastOrDefault()? // 取值部分
.Trim().Split(' ').FirstOrDefault(), out var val) // 取數字
? val : 0; // 回傳解析結果
private double GetCpuUsage() => 0; // CPU 使用率(待實作)
private double GetDiskUsage() => 0; // 磁碟使用率(待實作)
private TimeSpan GetUptime() => TimeSpan.Zero; // 運行時間(待實作)
}
Log 集中管理
Serilog 設定
// Serilog 集中日誌設定 // 將 Pi 的日誌傳到中央 Server
// Program.cs 中的設定 // 應用程式啟動時設定
// 安裝 NuGet 套件 // Serilog 相關套件
// dotnet add package Serilog.AspNetCore // ASP.NET Core 整合
// dotnet add package Serilog.Sinks.Http // HTTP 傳送
// dotnet add package Serilog.Sinks.File // 本地檔案備份
// 設定 Serilog // 同時寫入本地和遠端
public static class SerilogConfig // Serilog 設定類別
{
public static void Configure( // 設定方法
string machineName, // 機器名稱
string centralLogUrl) // 中央日誌伺服器網址
{
Log.Logger = new LoggerConfiguration() // 建立日誌設定
.MinimumLevel.Information() // 最低記錄等級
.Enrich.WithProperty("MachineName", machineName) // 加入機器名稱
.Enrich.WithProperty("AppName", "POS") // 加入應用名稱
.WriteTo.Console() // 輸出到主控台
.WriteTo.File( // 寫入本地檔案
"/var/log/pos-app/log-.txt", // 本地日誌路徑
rollingInterval: RollingInterval.Day, // 每日輪替
retainedFileCountLimit: 7) // 保留 7 天
.WriteTo.Http(centralLogUrl) // 傳送到中央伺服器
.CreateLogger(); // 建立 Logger
}
}
SD 卡防損壞(Read-Only 檔案系統)
# 設定 Read-Only 根檔案系統 // 防止突然斷電導致 SD 卡損壞
# 安裝 overlayroot // 覆蓋式唯讀檔案系統
sudo apt install -y overlayroot // 安裝 overlayroot
# 編輯 overlayroot 設定 // 啟用唯讀模式
sudo cat > /etc/overlayroot.local.conf << 'EOF' // 寫入設定
overlayroot="tmpfs" // 使用 tmpfs 作為覆蓋層
EOF
# 掛載 tmpfs 給需要寫入的目錄 // 日誌和暫存檔案
echo "tmpfs /var/log tmpfs defaults,noatime,size=50M 0 0" | \ // 日誌目錄
sudo tee -a /etc/fstab // 加入開機掛載
echo "tmpfs /tmp tmpfs defaults,noatime,size=100M 0 0" | \ // 暫存目錄
sudo tee -a /etc/fstab // 加入開機掛載
# 需要永久寫入時暫時解除唯讀 // 更新程式時使用
sudo overlayroot-chroot // 進入可寫入模式
# 在這裡做需要永久保存的修改 // 例如更新應用程式
exit // 退出可寫入模式
備份與還原
SD 卡映像檔備份
# 備份整張 SD 卡 // 建立完整映像檔
sudo dd if=/dev/mmcblk0 \ // 來源:SD 卡裝置
of=/backup/pos-image-$(date +%Y%m%d).img \ // 目標:映像檔(含日期)
bs=4M \ // 區塊大小 4MB
status=progress // 顯示進度
# 壓縮映像檔 // 節省儲存空間
sudo gzip /backup/pos-image-$(date +%Y%m%d).img // 壓縮映像檔
# 還原映像檔 // 將備份寫回 SD 卡
sudo dd if=/backup/pos-image-20240101.img.gz \ // 來源:壓縮映像檔
of=/dev/sdX \ // 目標:新的 SD 卡
bs=4M \ // 區塊大小 4MB
status=progress // 顯示進度
自動備份腳本
#!/bin/bash // 指定直譯器
# auto_backup.sh // 每週自動備份 POS 資料
BACKUP_DIR="/backup" // 備份目錄
DB_FILE="/opt/pos-app/pos.db" // SQLite 資料庫
REMOTE_SERVER="backup@cloud-server.com" // 遠端備份伺服器
LOG_FILE="/var/log/backup.log" // 備份日誌
echo "$(date): 開始備份..." >> $LOG_FILE // 記錄開始
# 備份 SQLite 資料庫 // 使用 .backup 指令避免鎖定
sqlite3 $DB_FILE ".backup '$BACKUP_DIR/pos-db-$(date +%Y%m%d).db'" // 備份資料庫
# 備份設定檔 // 應用程式設定
cp /opt/pos-app/appsettings.json $BACKUP_DIR/ // 複製設定檔
# 壓縮備份 // 節省空間和傳輸時間
tar czf $BACKUP_DIR/backup-$(date +%Y%m%d).tar.gz \ // 建立壓縮檔
$BACKUP_DIR/pos-db-*.db \ // 包含資料庫
$BACKUP_DIR/appsettings.json // 包含設定檔
# 上傳到遠端 // 異地備份
scp $BACKUP_DIR/backup-$(date +%Y%m%d).tar.gz \ // 安全複製
$REMOTE_SERVER:/backup/ // 到遠端伺服器
# 清理 7 天前的本地備份 // 避免塞滿磁碟
find $BACKUP_DIR -name "backup-*.tar.gz" -mtime +7 -delete // 刪除舊備份
echo "$(date): 備份完成" >> $LOG_FILE // 記錄完成
UPS 不斷電設計
UPS 監控服務
// UPS 不斷電監控服務 // 偵測停電並安全關機
public class UpsMonitorService // UPS 監控服務類別
{
private readonly ILogger<UpsMonitorService> _logger; // 日誌記錄器
// UPS 狀態模型 // 記錄 UPS 當前狀態
public class UpsStatus // UPS 狀態類別
{
public bool IsOnBattery { get; set; } // 是否在使用電池
public int BatteryPercent { get; set; } // 電池電量百分比
public int EstimatedMinutes { get; set; } // 預估剩餘時間(分鐘)
public string PowerSource { get; set; } = "AC"; // 電源來源
}
// 建構函式 // 注入日誌
public UpsMonitorService( // 建構 UPS 監控
ILogger<UpsMonitorService> logger) // 注入 Logger
{
_logger = logger; // 儲存日誌記錄器
}
// 監控 UPS 狀態 // 持續檢查電源狀態
public async Task MonitorAsync( // 監控方法
CancellationToken ct) // 取消令牌
{
while (!ct.IsCancellationRequested) // 持續監控
{
var status = await GetUpsStatusAsync(); // 取得 UPS 狀態
if (status.IsOnBattery) // 如果使用電池(停電了)
{
_logger.LogWarning( // 記錄停電警告
"停電中!電池 {Percent}%,預估剩餘 {Min} 分鐘", // 格式化
status.BatteryPercent, status.EstimatedMinutes); // 傳入參數
if (status.BatteryPercent < 20) // 電量低於 20%
{
_logger.LogCritical("電量不足,執行安全關機!"); // 緊急記錄
await SafeShutdownAsync(); // 安全關機
}
}
await Task.Delay(10000, ct); // 每 10 秒檢查一次
}
}
// 安全關機 // 先儲存資料再關機
private async Task SafeShutdownAsync() // 安全關機方法
{
_logger.LogInformation("開始安全關機流程..."); // 記錄開始
// 1. 儲存未完成的交易 // 防止資料遺失
// 2. 同步離線佇列 // 盡量同步到雲端
// 3. 關閉應用程式 // 優雅停止
// 4. 執行系統關機 // 最後一步
await Task.Delay(1000); // 等待儲存完成
var process = new System.Diagnostics.Process(); // 建立程序
process.StartInfo.FileName = "sudo"; // 使用 sudo
process.StartInfo.Arguments = "shutdown -h now"; // 立即關機
process.Start(); // 執行關機
}
private Task<UpsStatus> GetUpsStatusAsync() => // 取得 UPS 狀態
Task.FromResult(new UpsStatus()); // 回傳狀態(待實作)
}
故障排除 SOP
// 故障排除服務 // 自動診斷常見問題
public class TroubleshootService // 故障排除服務類別
{
private readonly ILogger<TroubleshootService> _logger; // 日誌記錄器
// 診斷結果 // 記錄檢查項目和狀態
public class DiagnosticResult // 診斷結果類別
{
public string CheckName { get; set; } = ""; // 檢查項目名稱
public bool Passed { get; set; } // 是否通過
public string Message { get; set; } = ""; // 結果訊息
public string? Suggestion { get; set; } // 建議修復方式
}
// 建構函式 // 注入日誌
public TroubleshootService( // 建構故障排除服務
ILogger<TroubleshootService> logger) // 注入 Logger
{
_logger = logger; // 儲存日誌記錄器
}
// 執行完整診斷 // 檢查所有常見問題
public async Task<List<DiagnosticResult>> RunDiagnosticsAsync() // 執行診斷方法
{
var results = new List<DiagnosticResult>(); // 建立結果列表
// 1. 檢查網路連線 // 確認能上網
results.Add(await CheckNetworkAsync()); // 加入網路檢查結果
// 2. 檢查磁碟空間 // 確認還有空間
results.Add(CheckDiskSpace()); // 加入磁碟檢查結果
// 3. 檢查 CPU 溫度 // 確認沒過熱
results.Add(CheckCpuTemperature()); // 加入溫度檢查結果
// 4. 檢查服務狀態 // 確認應用程式在跑
results.Add(CheckServiceStatus()); // 加入服務檢查結果
// 5. 檢查資料庫連線 // 確認 DB 正常
results.Add(await CheckDatabaseAsync()); // 加入資料庫檢查結果
// 記錄診斷結果 // 方便事後追蹤
foreach (var r in results) // 逐一記錄
{
if (r.Passed) // 如果通過
_logger.LogInformation("✓ {Check}: {Msg}", r.CheckName, r.Message); // 記錄成功
else // 如果失敗
_logger.LogWarning("✗ {Check}: {Msg}", r.CheckName, r.Message); // 記錄警告
}
return results; // 回傳所有結果
}
// 檢查網路 // ping 外部主機
private async Task<DiagnosticResult> CheckNetworkAsync() // 網路檢查方法
{
try // 嘗試連線
{
using var client = new HttpClient(); // 建立 HTTP 客戶端
client.Timeout = TimeSpan.FromSeconds(5); // 超時 5 秒
await client.GetAsync("https://www.google.com"); // 嘗試連線
return new DiagnosticResult // 回傳成功
{
CheckName = "網路連線", // 檢查名稱
Passed = true, // 通過
Message = "網路連線正常" // 成功訊息
};
}
catch // 連線失敗
{
return new DiagnosticResult // 回傳失敗
{
CheckName = "網路連線", // 檢查名稱
Passed = false, // 未通過
Message = "無法連線到網路", // 失敗訊息
Suggestion = "檢查網路線或 Wi-Fi 設定" // 建議
};
}
}
private DiagnosticResult CheckDiskSpace() => new() // 磁碟檢查
{
CheckName = "磁碟空間", Passed = true, Message = "磁碟空間充足" // 預設結果
};
private DiagnosticResult CheckCpuTemperature() => new() // 溫度檢查
{
CheckName = "CPU 溫度", Passed = true, Message = "溫度正常" // 預設結果
};
private DiagnosticResult CheckServiceStatus() => new() // 服務檢查
{
CheckName = "應用服務", Passed = true, Message = "服務運行中" // 預設結果
};
private Task<DiagnosticResult> CheckDatabaseAsync() => // 資料庫檢查
Task.FromResult(new DiagnosticResult // 回傳結果
{
CheckName = "資料庫", Passed = true, Message = "資料庫連線正常" // 預設結果
});
}
批量佈署(Pi Imager + cloud-init)
cloud-init 設定
# user-data // cloud-init 自動設定檔
#cloud-config # cloud-init 標記 // 必須放在第一行
hostname: pos-store-001 # 設定主機名稱 // 每台 Pi 不同
users: # 建立用戶 // 設定管理帳號
- name: pi # 用戶名稱 // 預設帳號
groups: sudo # 群組 // 加入 sudo 群組
shell: /bin/bash # Shell // 使用 Bash
sudo: ALL=(ALL) NOPASSWD:ALL # sudo 權限 // 免密碼
packages: # 安裝套件 // 自動安裝需要的軟體
- chromium-browser # Chromium 瀏覽器 // Kiosk 用
- unclutter # 隱藏游標 // Kiosk 用
- sqlite3 # SQLite // 本地資料庫
runcmd: # 執行指令 // 首次開機執行
- curl -sSL https://dot.net/v1/dotnet-install.sh | bash # 安裝 .NET // 執行環境
- mkdir -p /opt/pos-app # 建立應用目錄 // POS 安裝路徑
- systemctl enable kiosk.service # 啟用 Kiosk // 開機自動啟動
批量燒錄腳本
#!/bin/bash // 指定直譯器
# batch_flash.sh // 批量燒錄 SD 卡
IMAGE_FILE="pos-base-image.img" // 基礎映像檔
STORE_LIST=("store-001" "store-002" "store-003") // 門市列表
echo "批量燒錄開始" // 顯示開始訊息
for STORE in "${STORE_LIST[@]}"; do // 逐一處理每間門市
echo "準備 ${STORE} 的 SD 卡..." // 顯示進度
echo "請插入 SD 卡後按 Enter" // 提示插入 SD 卡
read // 等待使用者按 Enter
# 偵測新插入的 SD 卡 // 通常是 /dev/sdX
DEVICE=$(lsblk -dpno NAME | tail -1) // 取得最後插入的裝置
# 燒錄映像檔 // 寫入 SD 卡
echo "燒錄到 ${DEVICE}..." // 顯示燒錄目標
sudo dd if=$IMAGE_FILE of=$DEVICE bs=4M status=progress // 執行燒錄
# 修改 hostname // 讓每台 Pi 有不同的名稱
sudo mount ${DEVICE}2 /mnt // 掛載 SD 卡分區
echo "$STORE" | sudo tee /mnt/etc/hostname // 寫入主機名稱
sudo umount /mnt // 卸載分區
echo "${STORE} 燒錄完成!" // 顯示完成
done
echo "全部燒錄完成!" // 顯示全部完成
🤔 我這樣寫為什麼會錯?
❌ 錯誤:SD 卡直接斷電不關機
# 錯誤做法 // 直接拔電源
# 直接拔掉 Pi 的電源線 ← SD 卡可能損壞! // 正在寫入的資料會毀損
# 尤其是正在寫入資料庫的時候 // 資料庫可能整個壞掉
✅ 正確:使用 UPS + 安全關機
# 正確做法 // 透過 UPS 偵測停電後安全關機
# 1. 安裝 UPS // 不斷電系統
# 2. 偵測到停電 → 儲存資料 // 先存檔
# 3. 執行 sudo shutdown -h now // 安全關機
sudo shutdown -h now // 正確的關機指令
❌ 錯誤:所有 Pi 用同一個 SSH 金鑰
# 錯誤做法 // 一把鑰匙開所有門
# 把同一個 SSH 私鑰複製到所有 Pi ← 一台被駭全部中招! // 嚴重安全漏洞
ssh-copy-id -i ~/.ssh/shared_key pi@all-machines // 共用金鑰很危險
✅ 正確:每台 Pi 獨立金鑰 + 集中管理
# 正確做法 // 每台 Pi 有自己的金鑰
for host in store-a store-b store-c; do // 逐台處理
ssh-keygen -t ed25519 \ // 產生 ED25519 金鑰
-f ~/.ssh/key_${host} \ // 每台一個獨立金鑰
-N "" // 無密碼(搭配 ssh-agent)
ssh-copy-id -i ~/.ssh/key_${host}.pub pi@${host} // 部署公鑰到 Pi
done
❌ 錯誤:日誌只存在本地 SD 卡
// 錯誤寫法 // 日誌只寫到本地 SD 卡
public class BadLogging // 錯誤的日誌設定
{
public void Configure() // 設定方法
{
// 只寫到本地檔案 ← SD 卡壞了就看不到了! // 也會加速 SD 卡損壞
Log.Logger = new LoggerConfiguration() // 建立設定
.WriteTo.File("/var/log/app.log") // 只寫本地
.CreateLogger(); // 建立 Logger
}
}
✅ 正確:日誌同時寫到本地和遠端
// 正確寫法 // 本地 + 遠端雙重保險
public class GoodLogging // 正確的日誌設定
{
public void Configure() // 設定方法
{
Log.Logger = new LoggerConfiguration() // 建立設定
.WriteTo.File("/var/log/app.log", // 本地備份
rollingInterval: RollingInterval.Day, // 每日輪替
retainedFileCountLimit: 3) // 只保留 3 天(省 SD 卡空間)
.WriteTo.Http("https://log-server.com/api/logs") // 傳到中央伺服器
.CreateLogger(); // 建立 Logger
}
}