<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Automation on Tarragon</title><link>https://tarrragon.github.io/blog/tags/automation/</link><description>Recent content in Automation on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 01 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/automation/index.xml" rel="self" type="application/rss+xml"/><item><title>Rule engine 設計</title><link>https://tarrragon.github.io/blog/monitoring/04-collector/rule-engine/</link><pubDate>Fri, 19 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/monitoring/04-collector/rule-engine/</guid><description>&lt;p>Rule engine 是 collector 的主動處理層。事件寫入儲存後，rule engine 檢查事件是否匹配預定義的規則，匹配時執行對應的動作。沒有 rule engine 的 collector 是被動的資料倉庫 — 開發者需要主動查詢才能發現問題。Rule engine 讓 collector 能在問題發生時主動通知。&lt;/p>
&lt;h2 id="三段式規則結構">三段式規則結構&lt;/h2>
&lt;p>每條規則由三部分組成：條件（什麼事件觸發）、動作（觸發後做什麼）、模板（動作的內容格式）。&lt;/p>
&lt;h3 id="條件">條件&lt;/h3>
&lt;p>條件定義「哪些事件匹配這條規則」。條件是事件欄位的過濾器 — 事件類型、事件名稱、屬性值的比較。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;condition&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;type&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;error&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;name&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;terminal.connect.*&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;#34;severity&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;fatal&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>條件支援的匹配方式：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>精確匹配&lt;/strong>：&lt;code>&amp;quot;type&amp;quot;: &amp;quot;error&amp;quot;&lt;/code> — 事件類型必須是 error&lt;/li>
&lt;li>&lt;strong>前綴匹配&lt;/strong>：&lt;code>&amp;quot;name&amp;quot;: &amp;quot;terminal.connect.*&amp;quot;&lt;/code> — 事件名稱以 &lt;code>terminal.connect.&lt;/code> 開頭&lt;/li>
&lt;li>&lt;strong>數值比較&lt;/strong>：&lt;code>&amp;quot;data.duration_ms&amp;quot;: { &amp;quot;gt&amp;quot;: 5000 }&lt;/code> — 持續時間超過 5 秒&lt;/li>
&lt;li>&lt;strong>組合條件&lt;/strong>：多個欄位條件同時滿足（AND 邏輯）&lt;/li>
&lt;/ul>
&lt;h3 id="動作">動作&lt;/h3>
&lt;p>動作定義「條件匹配後做什麼」。常見的動作類型：&lt;/p>
&lt;p>&lt;strong>通知&lt;/strong>：發送訊息到指定管道（email、Slack webhook、Telegram bot、桌面通知）。&lt;/p>
&lt;p>&lt;strong>寫 summary&lt;/strong>：把匹配的事件摘要寫入 summary 檔案，供定期 review。和逐筆事件不同，summary 是聚合後的結果（例如「過去一小時有 15 個 terminal.connect.failed」）。&lt;/p>
&lt;p>&lt;strong>觸發 webhook&lt;/strong>：向外部 URL 發送 HTTP POST，讓其他系統可以接收事件並做進一步處理。&lt;/p>
&lt;p>&lt;strong>執行腳本&lt;/strong>：在 collector server 上執行預定義的 shell script。適合自動化回應（重啟服務、清理暫存檔、輪替 log）。執行腳本的安全風險需要控制 — 只允許白名單內的腳本。&lt;/p>
&lt;h3 id="模板">模板&lt;/h3>
&lt;p>模板定義動作的內容格式。通知的訊息內容、webhook 的 request body — 用模板語法（Go template 或 mustache）把事件欄位填入。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">{{ .name }} 發生於 {{ .ts }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">嚴重度：{{ .data.severity }}
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">訊息：{{ .data.message }}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>模板讓同一個動作類型適用不同的事件 — 不需要為每種事件寫不同的通知函式。&lt;/p>
&lt;h2 id="規則評估時機">規則評估時機&lt;/h2>
&lt;h3 id="即時評估">即時評估&lt;/h3>
&lt;p>每個事件寫入後立即評估所有規則。適合需要即時回應的規則（fatal error 通知）。&lt;/p>
&lt;p>即時評估的成本和規則數量成正比 — 100 條規則代表每個事件寫入後做 100 次條件匹配。規則數量在數十條以內時，評估時間可以忽略。&lt;/p>
&lt;h3 id="批次評估">批次評估&lt;/h3>
&lt;p>定期（每分鐘、每小時）掃描一段時間內的事件，評估聚合類規則。適合基於統計的規則（「過去 5 分鐘 error 數量超過 10」「過去 1 小時某 endpoint 的 P95 回應時間超過 2 秒」）。&lt;/p>
&lt;p>批次評估需要時間窗口的概念 — 規則條件中包含時間範圍和聚合函式（count、avg、max、percentile）。&lt;/p>
&lt;h3 id="混合策略">混合策略&lt;/h3>
&lt;p>即時評估用於單一事件觸發的規則（fatal error → 立即通知），批次評估用於聚合觸發的規則（error rate 異常 → 定期檢查）。兩者可以共存。&lt;/p>
&lt;h2 id="規則管理">規則管理&lt;/h2>
&lt;p>規則以 JSON 或 YAML 檔案儲存在 collector 的設定目錄中。新增、修改、刪除規則是編輯檔案 + 重新載入 collector（signal 或 API call）。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nt">rules&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fatal-error-notify&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">condition&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">error&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">data.severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fatal&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">action&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">type&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">slack&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">webhook&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">https://hooks.slack.com/...&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;FATAL: {{ .name }} at {{ .ts }}&amp;#34;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>規則檔案版本控制在 git 中，和 collector 的其他設定一起管理。規則變更歷史可追溯。&lt;/p>
&lt;h2 id="shell-執行的安全邊界">Shell 執行的安全邊界&lt;/h2>
&lt;p>Rule engine 的「執行腳本」動作在 collector 主機上執行 shell command。這個能力和 collector 的認證狀態組合後產生不同的風險等級。&lt;/p></description><content:encoded><![CDATA[<p>Rule engine 是 collector 的主動處理層。事件寫入儲存後，rule engine 檢查事件是否匹配預定義的規則，匹配時執行對應的動作。沒有 rule engine 的 collector 是被動的資料倉庫 — 開發者需要主動查詢才能發現問題。Rule engine 讓 collector 能在問題發生時主動通知。</p>
<h2 id="三段式規則結構">三段式規則結構</h2>
<p>每條規則由三部分組成：條件（什麼事件觸發）、動作（觸發後做什麼）、模板（動作的內容格式）。</p>
<h3 id="條件">條件</h3>
<p>條件定義「哪些事件匹配這條規則」。條件是事件欄位的過濾器 — 事件類型、事件名稱、屬性值的比較。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nt">&#34;condition&#34;</span><span class="p">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="nt">&#34;type&#34;</span><span class="p">:</span> <span class="s2">&#34;error&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;terminal.connect.*&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="nt">&#34;severity&#34;</span><span class="p">:</span> <span class="s2">&#34;fatal&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>條件支援的匹配方式：</p>
<ul>
<li><strong>精確匹配</strong>：<code>&quot;type&quot;: &quot;error&quot;</code> — 事件類型必須是 error</li>
<li><strong>前綴匹配</strong>：<code>&quot;name&quot;: &quot;terminal.connect.*&quot;</code> — 事件名稱以 <code>terminal.connect.</code> 開頭</li>
<li><strong>數值比較</strong>：<code>&quot;data.duration_ms&quot;: { &quot;gt&quot;: 5000 }</code> — 持續時間超過 5 秒</li>
<li><strong>組合條件</strong>：多個欄位條件同時滿足（AND 邏輯）</li>
</ul>
<h3 id="動作">動作</h3>
<p>動作定義「條件匹配後做什麼」。常見的動作類型：</p>
<p><strong>通知</strong>：發送訊息到指定管道（email、Slack webhook、Telegram bot、桌面通知）。</p>
<p><strong>寫 summary</strong>：把匹配的事件摘要寫入 summary 檔案，供定期 review。和逐筆事件不同，summary 是聚合後的結果（例如「過去一小時有 15 個 terminal.connect.failed」）。</p>
<p><strong>觸發 webhook</strong>：向外部 URL 發送 HTTP POST，讓其他系統可以接收事件並做進一步處理。</p>
<p><strong>執行腳本</strong>：在 collector server 上執行預定義的 shell script。適合自動化回應（重啟服務、清理暫存檔、輪替 log）。執行腳本的安全風險需要控制 — 只允許白名單內的腳本。</p>
<h3 id="模板">模板</h3>
<p>模板定義動作的內容格式。通知的訊息內容、webhook 的 request body — 用模板語法（Go template 或 mustache）把事件欄位填入。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">{{ .name }} 發生於 {{ .ts }}
</span></span><span class="line"><span class="ln">2</span><span class="cl">嚴重度：{{ .data.severity }}
</span></span><span class="line"><span class="ln">3</span><span class="cl">訊息：{{ .data.message }}</span></span></code></pre></div><p>模板讓同一個動作類型適用不同的事件 — 不需要為每種事件寫不同的通知函式。</p>
<h2 id="規則評估時機">規則評估時機</h2>
<h3 id="即時評估">即時評估</h3>
<p>每個事件寫入後立即評估所有規則。適合需要即時回應的規則（fatal error 通知）。</p>
<p>即時評估的成本和規則數量成正比 — 100 條規則代表每個事件寫入後做 100 次條件匹配。規則數量在數十條以內時，評估時間可以忽略。</p>
<h3 id="批次評估">批次評估</h3>
<p>定期（每分鐘、每小時）掃描一段時間內的事件，評估聚合類規則。適合基於統計的規則（「過去 5 分鐘 error 數量超過 10」「過去 1 小時某 endpoint 的 P95 回應時間超過 2 秒」）。</p>
<p>批次評估需要時間窗口的概念 — 規則條件中包含時間範圍和聚合函式（count、avg、max、percentile）。</p>
<h3 id="混合策略">混合策略</h3>
<p>即時評估用於單一事件觸發的規則（fatal error → 立即通知），批次評估用於聚合觸發的規則（error rate 異常 → 定期檢查）。兩者可以共存。</p>
<h2 id="規則管理">規則管理</h2>
<p>規則以 JSON 或 YAML 檔案儲存在 collector 的設定目錄中。新增、修改、刪除規則是編輯檔案 + 重新載入 collector（signal 或 API call）。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln">1</span><span class="cl"><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">fatal-error-notify</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">    </span><span class="nt">condition</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">      </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">error</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">      </span><span class="nt">data.severity</span><span class="p">:</span><span class="w"> </span><span class="l">fatal</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w">    </span><span class="nt">action</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="w">      </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">slack</span><span class="w">
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="w">      </span><span class="nt">webhook</span><span class="p">:</span><span class="w"> </span><span class="l">https://hooks.slack.com/...</span><span class="w">
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="w">      </span><span class="nt">template</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;FATAL: {{ .name }} at {{ .ts }}&#34;</span></span></span></code></pre></div><p>規則檔案版本控制在 git 中，和 collector 的其他設定一起管理。規則變更歷史可追溯。</p>
<h2 id="shell-執行的安全邊界">Shell 執行的安全邊界</h2>
<p>Rule engine 的「執行腳本」動作在 collector 主機上執行 shell command。這個能力和 collector 的認證狀態組合後產生不同的風險等級。</p>
<h3 id="攻擊鏈">攻擊鏈</h3>
<p>無認證模式下，攻擊者可以向 collector 的 <code>/v1/events</code> endpoint 注入偽造事件。如果偽造事件匹配了一條規則、且規則的動作是執行 free-form shell command，攻擊者等於取得了 collector 主機的命令執行權（RCE — Remote Code Execution）。</p>
<p>攻擊路徑：注入假事件 → 匹配 rule → 執行 shell → RCE。</p>
<h3 id="防護措施">防護措施</h3>
<p><strong>Rule 定義不可透過 API 新增</strong>。Rule 只能由管理員透過配置檔或 CLI 設定，collector 的 HTTP API 不提供 rule CRUD endpoint。攻擊者即使能注入事件也無法新增 rule — 但現有 rule 的條件如果太寬（例如 <code>type: error</code> 沒有進一步限定 name），偽造的 error 事件仍可能匹配。</p>
<p><strong>Shell command 使用 allowlist</strong>。Rule 的 action 指定 command name（如 <code>restart-ttyd</code>），command 的實際路徑在配置檔的 allowlist 中定義。Rule 不接受 free-form shell string（如 <code>sh -c &quot;rm -rf /&quot;</code>）。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># 配置檔</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="nt">allowed_commands</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">restart-ttyd</span><span class="p">:</span><span class="w"> </span><span class="l">/usr/local/bin/restart-ttyd.sh</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">notify-slack</span><span class="p">:</span><span class="w"> </span><span class="l">/usr/local/bin/notify-slack.sh</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="nt">rules</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">  </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">fatal-error-response</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="nt">condition</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">      </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">error</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">      </span><span class="nt">data.severity</span><span class="p">:</span><span class="w"> </span><span class="l">fatal</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="nt">action</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">      </span><span class="nt">type</span><span class="p">:</span><span class="w"> </span><span class="l">command</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">      </span><span class="nt">command</span><span class="p">:</span><span class="w"> </span><span class="l">restart-ttyd </span><span class="w"> </span><span class="c"># 只接受 allowlist 中的 name</span></span></span></code></pre></div><p><strong>無認證模式下的額外限制</strong>。Collector 無認證時（同區網信任），建議禁用 command 類型的動作、只允許通知和 webhook。認證啟用後才解鎖 command 動作 — 認證確保只有授權的 SDK 實例能送事件，降低偽造事件觸發 rule 的風險。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>Collector 的完整架構 → <a href="/blog/monitoring/04-collector/architecture/" data-link-title="Collector 架構" data-link-desc="HTTP endpoint → JSON Schema 驗證 → 儲存 → 查詢 → rule engine 的五段式處理鏈路">Collector 架構</a></li>
<li>規模成長後的演進路徑 → <a href="/blog/monitoring/04-collector/scaling-evolution/" data-link-title="規模演進" data-link-desc="可插拔 Storage Backend 架構 — SQLite 預設、PostgreSQL 觸發切換、時間序列 DB 長期演進">規模演進</a></li>
<li>事件的分類和命名 → <a href="/blog/monitoring/01-mental-model/four-event-types/" data-link-title="四類事件的完整定義" data-link-desc="Event / Error / Metric / Lifecycle 四類事件各自的語意、觸發時機和典型用途 — 分類是監控體系的統一語言">監控心智模型 四類事件</a></li>
<li>Rule engine 在偽造流量偵測的應用 → <a href="/blog/monitoring/07-security-privacy/client-sdk-authentication/" data-link-title="Client-side SDK 認證的根本限制" data-link-desc="嵌在 client 端的 credential 必然可被提取 — 認清 architecture 天花板後的多層緩解策略，從 origin 驗證到 device attestation">Client-side SDK 認證</a></li>
</ul>
]]></content:encoded></item><item><title>讓機器跑無人值守的長任務</title><link>https://tarrragon.github.io/blog/linux/install/unattended-remote-work/</link><pubDate>Wed, 01 Jul 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/linux/install/unattended-remote-work/</guid><description>&lt;p>一台機器能被連入、能跑 bootstrap（把它從空機器設定成可用環境的安裝流程）之後，下一個層次是讓它在你不盯著的時候自己跑完一個長任務——一次耗時的編譯、一個批次作業、一個無人值守的 agent。能不能放著走人，取決於有沒有把三件會中斷無人值守執行的事先解決掉：互動提示、斷線即死、結果出不去。這三件是「讓任務能在無人時順利啟動並交付」的障礙；任務跑起來之後的資源耗盡、OOM、額度或憑證到期是另一條軸（執行期的持久性），最後一段會接到那裡。這篇逐一拆解這三個障礙與對應的解法，並說明它們共同的代價判讀——這些便利大多拿安全性換自主性，該不該開要看這台機器的爆炸半徑。&lt;/p>
&lt;p>底下用一個具體情境當例子：在一台用完即丟的測試 VM 上，讓 Claude Code 這類 agent 自己跑完一段工作、把成果推回 GitHub 給你早上 review。同一組障礙換成 overnight 編譯或 cron 批次也成立。&lt;/p>
&lt;h2 id="障礙一互動提示擋住自動執行">障礙一：互動提示擋住自動執行&lt;/h2>
&lt;p>無人值守的程序沒有人在鍵盤前，所以任何「停下來等你輸入」的提示都會讓它卡死，其中最常見的是 sudo 密碼。一個要裝套件、改系統設定的任務，跑到 &lt;code>sudo&lt;/code> 那行就停在密碼提示、永遠等不到輸入，整個任務卡在那裡直到你回來。&lt;/p>
&lt;p>解法是讓這台機器的 sudo 免密碼（NOPASSWD），但這是一個明確的安全取捨、不是預設該開的東西。設定方式是給 sudoers 加一條 NOPASSWD 規則：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="k">$(&lt;/span>whoami&lt;span class="k">)&lt;/span>&lt;span class="s2"> ALL=(ALL:ALL) NOPASSWD: ALL&amp;#34;&lt;/span> &lt;span class="p">|&lt;/span> sudo tee /etc/sudoers.d/20-nopasswd &lt;span class="c1"># $(whoami) 會填入你的登入帳號&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">sudo chmod &lt;span class="m">440&lt;/span> /etc/sudoers.d/20-nopasswd&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>開了 NOPASSWD，等於放棄「sudo 密碼」這道在你被入侵或程序失控時的最後防線。判讀軸是這台機器的爆炸半徑——它持有哪些憑證、能觸及哪些系統，也就是最壞情況下會波及多大範圍。一台範圍受限、沒有任何真實憑證、出事就重建的測試 VM，放棄這道防線換取自動執行是划算的；一台共享主機、生產伺服器、或裝著真實憑證與資料的機器，不該為了方便開 NOPASSWD。關鍵是「可不可丟」不等於「爆炸半徑小」：一台用完即丟的 VM，一旦塞進能碰到生產系統或你帳號的憑證，爆炸半徑就不小了——看的不是機器本身，是它最壞情況能波及什麼。&lt;/p>
&lt;h2 id="障礙二ssh-斷線就把任務一起殺掉">障礙二：SSH 斷線就把任務一起殺掉&lt;/h2>
&lt;p>直接在 SSH session 裡跑的程序，會隨著 SSH 連線中斷而一起死掉——你闔上筆電、網路斷一下、或單純關掉終端機，正在跑的任務就沒了。對一個要跑好幾小時的無人值守任務，這條等於「你不能離開」，跟無人值守的目的矛盾。&lt;/p>
&lt;p>把任務搬進終端機多工器（zellij、tmux 這類，配置見 &lt;a href="https://tarrragon.github.io/blog/linux/dotfile/03-terminal-ecosystem/multiplexer-tmux-zellij/" data-link-title="Multiplexer：tmux vs zellij" data-link-desc="在終端機裡切分 pane、管理多個 session、SSH 斷線後保持工作時回來讀 — tmux 和 zellij 的配置與選型">模組三&lt;/a>）就解決了。多工器的 session 活在那台機器上、獨立於你的 SSH 連線：你在多工器裡啟動任務、然後 detach（卸離），任務繼續在機器上跑，你這頭關掉 SSH 都不影響；之後再連回來 attach（接回）就能看它跑到哪。典型流程是連入機器、起多工器、在裡面啟動任務、detach、走人：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">ssh user@host
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">zellij &lt;span class="c1"># 起多工器（tmux 同理）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">./run-my-long-task.sh &lt;span class="c1"># 在裡面啟動你的長任務（換成你的實際指令）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 然後 detach：zellij 預設 Ctrl+o 再按 d（tmux 是 Ctrl+b 再按 d）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 此時關掉 SSH 不影響任務，它在 host 上繼續跑&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1"># 之後連回來看進度：再 ssh 進去，然後&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">zellij attach &lt;span class="c1"># tmux 是 tmux attach&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>判讀訊號是「這個任務跑完前，我會不會斷線」。只要會（過夜、跨小時、不穩的網路），就把它放進多工器；幾秒鐘就結束的指令不需要這層。&lt;/p>
&lt;h2 id="障礙三成果推不出去等於沒做">障礙三：成果推不出去，等於沒做&lt;/h2>
&lt;p>無人值守任務的產出留在那台機器上，你看不到——除非它能把結果送出去。最常見的形式是把改動 commit 後 push 回 git 遠端，你在別處 pull 來看。但 push 需要認證，而一台剛連入的機器通常還沒設好推送的憑證，於是任務做完了、commit 也建了，卻卡在 push 那步推不出去，你隔天連回來才發現結果根本沒送出去。&lt;/p>
&lt;p>先在這台機器上設好推送認證，這個障礙就消失。用 GitHub CLI 是直接的一條路，它認證後會一併把 git 的 credential helper（git 用來自動帶出認證、不必每次手打的機制）設好，後續 &lt;code>git push&lt;/code> 就能用——但 &lt;code>gh auth login&lt;/code> 本身是互動式的、要你在場完成一次，屬於離開前的人工前置：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">gh auth login &lt;span class="c1"># 選 HTTPS、完成認證、同意設定 git 認證&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>判讀軸是「這個任務的價值要怎麼回到你手上」。如果你打算從遠端（GitHub）看結果，那 push 認證就是必要前置——沒設好，整段工作就被困在機器裡。連帶的紀律是讓任務頻繁 commit 當檢查點、做完務必確認 push 成功：對一個你不在場的任務，「沒推出去」跟「沒做」對你是一樣的。機器若沒裝 &lt;code>gh&lt;/code>，也可以用 PAT 走 HTTPS，見 &lt;a href="../ssh-keyless-bootstrap/">外部連入篇&lt;/a> 的私有 repo 段。&lt;/p>
&lt;p>把 push 憑證設進這台機器，等於提高了它的爆炸半徑——它現在能動你的 repo 了。這會回頭讓障礙一的 NOPASSWD、以及下面 agent 段的權限放行更該謹慎：最壞情況從「弄壞這台機器」升級成「污染你的 repo」，而後者不是重建一台 VM 就能還原的。所以設了 push 憑證之後，要連帶重估前面那些「因為機器可丟所以放心」的取捨。&lt;/p>
&lt;h2 id="額外一層宿主暫停會連帶停掉任務">額外一層：宿主暫停會連帶停掉任務&lt;/h2>
&lt;p>當這台機器是跑在某個宿主上的虛擬機，還有一個容易忽略的中斷源：宿主睡著，VM 跟著暫停，裡面的無人值守任務也一起停。你以為它整夜在跑，回來發現它從你離開那刻就凍在那裡。判讀方式是想一下「這台機器的存在依賴什麼」——VM 依賴宿主醒著、雲端主機依賴帳單沒欠費。對 VM 的情況，離開前確保宿主不會自動睡眠（macOS 用 &lt;code>caffeinate&lt;/code>、Linux 宿主用 &lt;code>systemd-inhibit&lt;/code> 或停用 suspend、Windows 調電源設定，或直接關掉節能的自動睡眠）。&lt;/p>
&lt;h2 id="如果無人值守的工作者是-ai-agent">如果無人值守的工作者是 AI agent&lt;/h2>
&lt;p>當你放著跑的是一個 AI agent，除了上面三個障礙，還多一個它自己的互動提示要處理：agent 預設會在每個有風險的動作前停下來問你確認，而無人值守時沒人回答，它就卡住。對應的是 agent 的「跳過確認」模式（如 Claude Code 的權限放行旗標），讓它不停下來問。這跟 NOPASSWD 是同一類取捨、判讀軸也一樣：放給一個無人盯著的 agent 在一台範圍受限、用完即丟的機器上自主動作是可接受的；在一台有真實資料或共享的機器上不該這樣。降低風險的兩個做法是把 agent 的工作範圍用清楚的指引限定（只動哪些目錄、別碰系統其他地方），以及讓它在分支上做、產出交給你 review，而不是直接動到你會依賴的東西。&lt;/p></description><content:encoded><![CDATA[<p>一台機器能被連入、能跑 bootstrap（把它從空機器設定成可用環境的安裝流程）之後，下一個層次是讓它在你不盯著的時候自己跑完一個長任務——一次耗時的編譯、一個批次作業、一個無人值守的 agent。能不能放著走人，取決於有沒有把三件會中斷無人值守執行的事先解決掉：互動提示、斷線即死、結果出不去。這三件是「讓任務能在無人時順利啟動並交付」的障礙；任務跑起來之後的資源耗盡、OOM、額度或憑證到期是另一條軸（執行期的持久性），最後一段會接到那裡。這篇逐一拆解這三個障礙與對應的解法，並說明它們共同的代價判讀——這些便利大多拿安全性換自主性，該不該開要看這台機器的爆炸半徑。</p>
<p>底下用一個具體情境當例子：在一台用完即丟的測試 VM 上，讓 Claude Code 這類 agent 自己跑完一段工作、把成果推回 GitHub 給你早上 review。同一組障礙換成 overnight 編譯或 cron 批次也成立。</p>
<h2 id="障礙一互動提示擋住自動執行">障礙一：互動提示擋住自動執行</h2>
<p>無人值守的程序沒有人在鍵盤前，所以任何「停下來等你輸入」的提示都會讓它卡死，其中最常見的是 sudo 密碼。一個要裝套件、改系統設定的任務，跑到 <code>sudo</code> 那行就停在密碼提示、永遠等不到輸入，整個任務卡在那裡直到你回來。</p>
<p>解法是讓這台機器的 sudo 免密碼（NOPASSWD），但這是一個明確的安全取捨、不是預設該開的東西。設定方式是給 sudoers 加一條 NOPASSWD 規則：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;</span><span class="k">$(</span>whoami<span class="k">)</span><span class="s2"> ALL=(ALL:ALL) NOPASSWD: ALL&#34;</span> <span class="p">|</span> sudo tee /etc/sudoers.d/20-nopasswd  <span class="c1"># $(whoami) 會填入你的登入帳號</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">sudo chmod <span class="m">440</span> /etc/sudoers.d/20-nopasswd</span></span></code></pre></div><p>開了 NOPASSWD，等於放棄「sudo 密碼」這道在你被入侵或程序失控時的最後防線。判讀軸是這台機器的爆炸半徑——它持有哪些憑證、能觸及哪些系統，也就是最壞情況下會波及多大範圍。一台範圍受限、沒有任何真實憑證、出事就重建的測試 VM，放棄這道防線換取自動執行是划算的；一台共享主機、生產伺服器、或裝著真實憑證與資料的機器，不該為了方便開 NOPASSWD。關鍵是「可不可丟」不等於「爆炸半徑小」：一台用完即丟的 VM，一旦塞進能碰到生產系統或你帳號的憑證，爆炸半徑就不小了——看的不是機器本身，是它最壞情況能波及什麼。</p>
<h2 id="障礙二ssh-斷線就把任務一起殺掉">障礙二：SSH 斷線就把任務一起殺掉</h2>
<p>直接在 SSH session 裡跑的程序，會隨著 SSH 連線中斷而一起死掉——你闔上筆電、網路斷一下、或單純關掉終端機，正在跑的任務就沒了。對一個要跑好幾小時的無人值守任務，這條等於「你不能離開」，跟無人值守的目的矛盾。</p>
<p>把任務搬進終端機多工器（zellij、tmux 這類，配置見 <a href="/blog/linux/dotfile/03-terminal-ecosystem/multiplexer-tmux-zellij/" data-link-title="Multiplexer：tmux vs zellij" data-link-desc="在終端機裡切分 pane、管理多個 session、SSH 斷線後保持工作時回來讀 — tmux 和 zellij 的配置與選型">模組三</a>）就解決了。多工器的 session 活在那台機器上、獨立於你的 SSH 連線：你在多工器裡啟動任務、然後 detach（卸離），任務繼續在機器上跑，你這頭關掉 SSH 都不影響；之後再連回來 attach（接回）就能看它跑到哪。典型流程是連入機器、起多工器、在裡面啟動任務、detach、走人：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">ssh user@host
</span></span><span class="line"><span class="ln">2</span><span class="cl">zellij                       <span class="c1"># 起多工器（tmux 同理）</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">./run-my-long-task.sh        <span class="c1"># 在裡面啟動你的長任務（換成你的實際指令）</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 然後 detach：zellij 預設 Ctrl+o 再按 d（tmux 是 Ctrl+b 再按 d）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># 此時關掉 SSH 不影響任務，它在 host 上繼續跑</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 之後連回來看進度：再 ssh 進去，然後</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">zellij attach                <span class="c1"># tmux 是 tmux attach</span></span></span></code></pre></div><p>判讀訊號是「這個任務跑完前，我會不會斷線」。只要會（過夜、跨小時、不穩的網路），就把它放進多工器；幾秒鐘就結束的指令不需要這層。</p>
<h2 id="障礙三成果推不出去等於沒做">障礙三：成果推不出去，等於沒做</h2>
<p>無人值守任務的產出留在那台機器上，你看不到——除非它能把結果送出去。最常見的形式是把改動 commit 後 push 回 git 遠端，你在別處 pull 來看。但 push 需要認證，而一台剛連入的機器通常還沒設好推送的憑證，於是任務做完了、commit 也建了，卻卡在 push 那步推不出去，你隔天連回來才發現結果根本沒送出去。</p>
<p>先在這台機器上設好推送認證，這個障礙就消失。用 GitHub CLI 是直接的一條路，它認證後會一併把 git 的 credential helper（git 用來自動帶出認證、不必每次手打的機制）設好，後續 <code>git push</code> 就能用——但 <code>gh auth login</code> 本身是互動式的、要你在場完成一次，屬於離開前的人工前置：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">gh auth login    <span class="c1"># 選 HTTPS、完成認證、同意設定 git 認證</span></span></span></code></pre></div><p>判讀軸是「這個任務的價值要怎麼回到你手上」。如果你打算從遠端（GitHub）看結果，那 push 認證就是必要前置——沒設好，整段工作就被困在機器裡。連帶的紀律是讓任務頻繁 commit 當檢查點、做完務必確認 push 成功：對一個你不在場的任務，「沒推出去」跟「沒做」對你是一樣的。機器若沒裝 <code>gh</code>，也可以用 PAT 走 HTTPS，見 <a href="../ssh-keyless-bootstrap/">外部連入篇</a> 的私有 repo 段。</p>
<p>把 push 憑證設進這台機器，等於提高了它的爆炸半徑——它現在能動你的 repo 了。這會回頭讓障礙一的 NOPASSWD、以及下面 agent 段的權限放行更該謹慎：最壞情況從「弄壞這台機器」升級成「污染你的 repo」，而後者不是重建一台 VM 就能還原的。所以設了 push 憑證之後，要連帶重估前面那些「因為機器可丟所以放心」的取捨。</p>
<h2 id="額外一層宿主暫停會連帶停掉任務">額外一層：宿主暫停會連帶停掉任務</h2>
<p>當這台機器是跑在某個宿主上的虛擬機，還有一個容易忽略的中斷源：宿主睡著，VM 跟著暫停，裡面的無人值守任務也一起停。你以為它整夜在跑，回來發現它從你離開那刻就凍在那裡。判讀方式是想一下「這台機器的存在依賴什麼」——VM 依賴宿主醒著、雲端主機依賴帳單沒欠費。對 VM 的情況，離開前確保宿主不會自動睡眠（macOS 用 <code>caffeinate</code>、Linux 宿主用 <code>systemd-inhibit</code> 或停用 suspend、Windows 調電源設定，或直接關掉節能的自動睡眠）。</p>
<h2 id="如果無人值守的工作者是-ai-agent">如果無人值守的工作者是 AI agent</h2>
<p>當你放著跑的是一個 AI agent，除了上面三個障礙，還多一個它自己的互動提示要處理：agent 預設會在每個有風險的動作前停下來問你確認，而無人值守時沒人回答，它就卡住。對應的是 agent 的「跳過確認」模式（如 Claude Code 的權限放行旗標），讓它不停下來問。這跟 NOPASSWD 是同一類取捨、判讀軸也一樣：放給一個無人盯著的 agent 在一台範圍受限、用完即丟的機器上自主動作是可接受的；在一台有真實資料或共享的機器上不該這樣。降低風險的兩個做法是把 agent 的工作範圍用清楚的指引限定（只動哪些目錄、別碰系統其他地方），以及讓它在分支上做、產出交給你 review，而不是直接動到你會依賴的東西。</p>
<h2 id="下一步">下一步</h2>
<p>把這三到四個障礙解決掉，一台機器就能在你離開後自己跑完工作、把成果送回你手上。這篇是 <a href="../ssh-keyless-bootstrap/">外部連入</a>（怎麼連進去）的延伸——從「我連進去手動操作」進到「我設好讓它自己跑」。而要讓那個無人值守的任務在失敗時還留得下可診斷的痕跡，回到 <a href="../observable-bootstrap/">可除錯的 bootstrap</a> 的原則：無人盯著的任務尤其需要把可觀測性內建進去，因為你不在場、只能事後從 log 重建發生了什麼。</p>
]]></content:encoded></item></channel></rss>