<?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>Alarm on Tarragon</title><link>https://tarrragon.github.io/blog/tags/alarm/</link><description>Recent content in Alarm on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Fri, 26 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/alarm/index.xml" rel="self" type="application/rss+xml"/><item><title>可觀測性與 log 同生命週期管理</title><link>https://tarrragon.github.io/blog/infra/06-observability-logging/log-metric-alarm-lifecycle/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/06-observability-logging/log-metric-alarm-lifecycle/</guid><description>&lt;p>可觀測性要跟它監控的資源同生命週期：log group、metric 與 alarm 寫進建立資源的同一套 IaC，資源開出來的那一刻監控就在線，而非等出事才補。這條規則的責任是讓基礎設施在出事時可被追查、在日常時可被量化，而它的建立與銷毀和被監控的資源綁在一起，則保證監控的覆蓋率不會隨時間衰退。&lt;/p>
&lt;p>沒有同生命週期管理時，新服務上線後的監控覆蓋率取決於有沒有人記得手動建立 log group 和 alarm，而這個記憶在服務數量增長後會衰退。監控缺口在平時不被注意，在事故排查時才浮現 — 需要回溯「什麼時候開始劣化」時，可能發現劣化期間根本沒有對應的 metric 資料。&lt;/p>
&lt;h2 id="同生命週期的落地方式">同生命週期的落地方式&lt;/h2>
&lt;p>可觀測性是基礎設施的一部分，它的建立、變更與銷毀要跟被監控的資源綁在同一個 apply 單位裡。一個 RDS 實例被 IaC 建立時，它的 log group、它的關鍵 metric alarm 應該在同一份 &lt;code>terraform plan&lt;/code> 裡一起出現；這個資源被 destroy 時，對應的 alarm 也一起收掉。&lt;/p>
&lt;p>落地方式是把監控宣告收進服務的 module。&lt;a href="https://tarrragon.github.io/blog/infra/04-environment-separation/" data-link-title="模組四：環境分離與模組化" data-link-desc="dev / staging / prod 切分、目錄結構 vs workspace、用可重用 module 避免環境漂移">模組四（環境分離與模組化）&lt;/a>談的模組化在這裡延伸成「每個服務模組自帶它的 observability 宣告」。一個 database module 內部除了 &lt;code>aws_db_instance&lt;/code>，還包含它的 log group、CPU alarm、連線數 alarm：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># modules/database/monitoring.tf — 跟 database 資源同一個 module
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="c1">&lt;/span>&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_cloudwatch_log_group&amp;#34; &amp;#34;db_slow_query&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;/rds/${var.env}/${var.db_identifier}/slowquery&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="n"> retention_in_days&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">var&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">log_retention_days&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="n"> kms_key_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">var&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">log_kms_key_arn&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>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_cloudwatch_metric_alarm&amp;#34; &amp;#34;db_cpu&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="n"> alarm_name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;${var.env}-${var.db_identifier}-cpu-high&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="n"> comparison_operator&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;GreaterThanThreshold&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="n"> evaluation_periods&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="n"> metric_name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;CPUUtilization&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="n"> namespace&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;AWS/RDS&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="n"> period&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">300&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="n"> statistic&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Average&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="n"> threshold&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">80&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="n"> alarm_actions&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="k">var&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">oncall_sns_arn&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="n"> dimensions&lt;/span> &lt;span class="o">=&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">20&lt;/span>&lt;span class="cl">&lt;span class="n"> DBInstanceIdentifier&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_db_instance&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">primary&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">identifier&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">21&lt;/span>&lt;span class="cl"> }
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">22&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這樣 &lt;code>terraform apply&lt;/code> 建資料庫的同一刻，監控就存在；&lt;code>terraform destroy&lt;/code> 砍資料庫時，孤兒 alarm 也一起清掉。新環境套用同一個 module 時，監控覆蓋率自動跟著資源走，不需要額外的人工記憶。&lt;/p>
&lt;h2 id="監控脫鉤造成的兩類漂移">監控脫鉤造成的兩類漂移&lt;/h2>
&lt;p>把監控外掛在資源之外（用另一份 IaC、另一個 repo、或手動在 console 設定）會製造兩種方向相反的漂移，兩者的共同根因都是監控跟資源不在同一個 apply 單位裡。&lt;/p>
&lt;h3 id="漂移一新資源沒有監控">漂移一：新資源沒有監控&lt;/h3>
&lt;p>service 透過 PR 加上去了，但 alarm 的建立依賴某人事後手動進 console 設定，或等另一個 repo 的 PR 跟上。於是有些 service 有 alarm、有些沒有，覆蓋率取決於「誰記得」。沒有 alarm 的 service 出事時，事故發現路徑從「告警 → 排查」退化成「客訴 → 排查」，反應時間從分鐘級退化到小時級。&lt;/p>
&lt;p>用一條查詢就能看出這個漂移有多嚴重：列出所有 RDS instance，比對各自有沒有對應的 CloudWatch alarm。沒有 alarm 的 instance 就是漂移的活證據。&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="c1"># 列出所有 RDS instance，比對有沒有對應的 CloudWatch alarm&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">aws rds describe-db-instances &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --query &lt;span class="s1">&amp;#39;DBInstances[].DBInstanceIdentifier&amp;#39;&lt;/span> --output text &lt;span class="p">|&lt;/span> tr &lt;span class="s1">&amp;#39;\t&amp;#39;&lt;/span> &lt;span class="s1">&amp;#39;\n&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> &lt;span class="k">while&lt;/span> &lt;span class="nb">read&lt;/span> db&lt;span class="p">;&lt;/span> &lt;span class="k">do&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nv">count&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>aws cloudwatch describe-alarms &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --alarm-name-prefix &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">db&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span> --query &lt;span class="s1">&amp;#39;MetricAlarms | length(@)&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> &lt;span class="nb">echo&lt;/span> &lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">db&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2">: &lt;/span>&lt;span class="si">${&lt;/span>&lt;span class="nv">count&lt;/span>&lt;span class="si">}&lt;/span>&lt;span class="s2"> alarms&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="k">done&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h3 id="漂移二死資源留下殘響">漂移二：死資源留下殘響&lt;/h3>
&lt;p>資源砍了但 alarm 還在，orphan alarm 對不存在的 target 持續報 &lt;code>INSUFFICIENT_DATA&lt;/code>，跟有效 alarm 混在同一個通知頻道裡，降低告警的訊噪比。訊噪比低到一定程度後，有效的 &lt;code>INSUFFICIENT_DATA&lt;/code>（某個服務停止送 metric）也被一起略過 — 告警疲勞讓 alarm 從保護機制退化成背景噪音。&lt;/p></description><content:encoded><![CDATA[<p>可觀測性要跟它監控的資源同生命週期：log group、metric 與 alarm 寫進建立資源的同一套 IaC，資源開出來的那一刻監控就在線，而非等出事才補。這條規則的責任是讓基礎設施在出事時可被追查、在日常時可被量化，而它的建立與銷毀和被監控的資源綁在一起，則保證監控的覆蓋率不會隨時間衰退。</p>
<p>沒有同生命週期管理時，新服務上線後的監控覆蓋率取決於有沒有人記得手動建立 log group 和 alarm，而這個記憶在服務數量增長後會衰退。監控缺口在平時不被注意，在事故排查時才浮現 — 需要回溯「什麼時候開始劣化」時，可能發現劣化期間根本沒有對應的 metric 資料。</p>
<h2 id="同生命週期的落地方式">同生命週期的落地方式</h2>
<p>可觀測性是基礎設施的一部分，它的建立、變更與銷毀要跟被監控的資源綁在同一個 apply 單位裡。一個 RDS 實例被 IaC 建立時，它的 log group、它的關鍵 metric alarm 應該在同一份 <code>terraform plan</code> 裡一起出現；這個資源被 destroy 時，對應的 alarm 也一起收掉。</p>
<p>落地方式是把監控宣告收進服務的 module。<a href="/blog/infra/04-environment-separation/" data-link-title="模組四：環境分離與模組化" data-link-desc="dev / staging / prod 切分、目錄結構 vs workspace、用可重用 module 避免環境漂移">模組四（環境分離與模組化）</a>談的模組化在這裡延伸成「每個服務模組自帶它的 observability 宣告」。一個 database module 內部除了 <code>aws_db_instance</code>，還包含它的 log group、CPU alarm、連線數 alarm：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># modules/database/monitoring.tf — 跟 database 資源同一個 module
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">resource</span> <span class="s2">&#34;aws_cloudwatch_log_group&#34; &#34;db_slow_query&#34;</span> {
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">  name</span>              <span class="o">=</span> <span class="s2">&#34;/rds/${var.env}/${var.db_identifier}/slowquery&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">  retention_in_days</span> <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">log_retention_days</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">  kms_key_id</span>        <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">log_kms_key_arn</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></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_cloudwatch_metric_alarm&#34; &#34;db_cpu&#34;</span> {
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">  alarm_name</span>          <span class="o">=</span> <span class="s2">&#34;${var.env}-${var.db_identifier}-cpu-high&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">  comparison_operator</span> <span class="o">=</span> <span class="s2">&#34;GreaterThanThreshold&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">  evaluation_periods</span>  <span class="o">=</span> <span class="m">3</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">  metric_name</span>         <span class="o">=</span> <span class="s2">&#34;CPUUtilization&#34;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="n">  namespace</span>           <span class="o">=</span> <span class="s2">&#34;AWS/RDS&#34;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="n">  period</span>              <span class="o">=</span> <span class="m">300</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="n">  statistic</span>           <span class="o">=</span> <span class="s2">&#34;Average&#34;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="n">  threshold</span>           <span class="o">=</span> <span class="m">80</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="n">  alarm_actions</span>       <span class="o">=</span> <span class="p">[</span><span class="k">var</span><span class="p">.</span><span class="k">oncall_sns_arn</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="n">  dimensions</span> <span class="o">=</span> {
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="n">    DBInstanceIdentifier</span> <span class="o">=</span> <span class="k">aws_db_instance</span><span class="p">.</span><span class="k">primary</span><span class="p">.</span><span class="k">identifier</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">  }
</span></span><span class="line"><span class="ln">22</span><span class="cl">}</span></span></code></pre></div><p>這樣 <code>terraform apply</code> 建資料庫的同一刻，監控就存在；<code>terraform destroy</code> 砍資料庫時，孤兒 alarm 也一起清掉。新環境套用同一個 module 時，監控覆蓋率自動跟著資源走，不需要額外的人工記憶。</p>
<h2 id="監控脫鉤造成的兩類漂移">監控脫鉤造成的兩類漂移</h2>
<p>把監控外掛在資源之外（用另一份 IaC、另一個 repo、或手動在 console 設定）會製造兩種方向相反的漂移，兩者的共同根因都是監控跟資源不在同一個 apply 單位裡。</p>
<h3 id="漂移一新資源沒有監控">漂移一：新資源沒有監控</h3>
<p>service 透過 PR 加上去了，但 alarm 的建立依賴某人事後手動進 console 設定，或等另一個 repo 的 PR 跟上。於是有些 service 有 alarm、有些沒有，覆蓋率取決於「誰記得」。沒有 alarm 的 service 出事時，事故發現路徑從「告警 → 排查」退化成「客訴 → 排查」，反應時間從分鐘級退化到小時級。</p>
<p>用一條查詢就能看出這個漂移有多嚴重：列出所有 RDS instance，比對各自有沒有對應的 CloudWatch alarm。沒有 alarm 的 instance 就是漂移的活證據。</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="c1"># 列出所有 RDS instance，比對有沒有對應的 CloudWatch alarm</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">aws rds describe-db-instances <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --query <span class="s1">&#39;DBInstances[].DBInstanceIdentifier&#39;</span> --output text <span class="p">|</span> tr <span class="s1">&#39;\t&#39;</span> <span class="s1">&#39;\n&#39;</span> <span class="p">|</span> <span class="k">while</span> <span class="nb">read</span> db<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nv">count</span><span class="o">=</span><span class="k">$(</span>aws cloudwatch describe-alarms <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>    --alarm-name-prefix <span class="s2">&#34;</span><span class="si">${</span><span class="nv">db</span><span class="si">}</span><span class="s2">&#34;</span> --query <span class="s1">&#39;MetricAlarms | length(@)&#39;</span><span class="k">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;</span><span class="si">${</span><span class="nv">db</span><span class="si">}</span><span class="s2">: </span><span class="si">${</span><span class="nv">count</span><span class="si">}</span><span class="s2"> alarms&#34;</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="k">done</span></span></span></code></pre></div><h3 id="漂移二死資源留下殘響">漂移二：死資源留下殘響</h3>
<p>資源砍了但 alarm 還在，orphan alarm 對不存在的 target 持續報 <code>INSUFFICIENT_DATA</code>，跟有效 alarm 混在同一個通知頻道裡，降低告警的訊噪比。訊噪比低到一定程度後，有效的 <code>INSUFFICIENT_DATA</code>（某個服務停止送 metric）也被一起略過 — 告警疲勞讓 alarm 從保護機制退化成背景噪音。</p>
<p>漂移二的成本不只是注意力。殘留的 alarm 會佔用 CloudWatch alarm 的配額（每個帳號有配額上限），大量孤兒 alarm 累積後，新服務要加 alarm 可能需要先清理舊的 — 這在事故當下是最不該花時間的事。</p>
<p>修法是把 alarm 的生命週期綁進 module：資源 destroy 時 alarm 跟著 destroy，不需要另一個流程去「記得清理」。如果因為歷史原因已經有大量孤兒 alarm，可以用 alarm 的 <code>StateValue</code> 為 <code>INSUFFICIENT_DATA</code> 且持續超過 7 天作為清理候選的篩選條件。</p>
<h2 id="log-group-設計">log group 設計</h2>
<p>Log group 是日誌的歸屬與保存單位，它要回答兩個治理問題：留多久（retention）、誰能讀（access control）。這兩個問題寫進 IaC 才能稽核，而非依賴 vendor 的隱性預設。</p>
<h3 id="retention三方取捨">Retention：三方取捨</h3>
<p>許多雲端服務在沒有明確宣告 log group 時會自動建一個、套上「永久保留」的預設值。永久保留的問題不是技術性的 — CloudWatch Logs 可以存到無限久 — 而是治理性的：日誌無限堆積、帳單緩慢長大，而沒有人做過「這條 log 該留多久」的顯式決定。</p>
<p>Retention 是成本、合規與除錯需求的三方取捨：</p>
<table>
  <thead>
      <tr>
          <th>日誌類型</th>
          <th>除錯需求</th>
          <th>合規需求</th>
          <th>建議 retention</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>應用 log（request、error）</td>
          <td>近 2-4 週</td>
          <td>通常無特殊要求</td>
          <td>14-30 天</td>
      </tr>
      <tr>
          <td>資料庫 slow query log</td>
          <td>近 1-2 週</td>
          <td>通常無特殊要求</td>
          <td>14 天</td>
      </tr>
      <tr>
          <td>存取稽核 log（CloudTrail）</td>
          <td>偶爾回溯</td>
          <td>1-7 年</td>
          <td>90-365 天 + 歸檔 S3</td>
      </tr>
      <tr>
          <td>金流 / 交易 log</td>
          <td>對帳用、偶爾</td>
          <td>依法規 3-7 年</td>
          <td>短期保留 + 長期歸檔</td>
      </tr>
  </tbody>
</table>
<p>較合理的做法是按日誌類型分層：高頻、除錯用的 application log 設短 retention，稽核相關的 access log 按合規要求設長期保留，必要時再把冷資料用 subscription filter 歸檔到更便宜的物件儲存（S3 + Glacier）。把這些值寫進 IaC，讓「為什麼這條 log 留 90 天」是一個能在 PR 上被討論的決定，而非某人半年前在 console 點的一個數字。成本參考：CloudWatch Logs 的儲存費用約 $0.03/GB/月。一個每天產生 10GB log 的服務，30 天 retention 的月費約 $9，7 天約 $2。retention 天數的選擇是合規需求（留多久才合規）與儲存成本的直接取捨，可以按 log 類型分層設定。</p>
<p>觀測平台的帳單在規模化後容易超線性成長，而缺乏 per-team cost attribution 的環境只能靠全域砍 retention 或降 sampling 來控制成本，兩者都會傷害觀測品質。把 log retention 跟 cardinality budget 的決定從全域級拆到團隊級（用 tag 歸因），才能做到「該省的省、該留的留」。這個取捨在 <a href="/blog/backend/04-observability/cases/observability-cost-governance-at-scale/" data-link-title="4.C14 觀測平台成本治理：從帳單驚嚇到可預測成本" data-link-desc="觀測帳單持續超線性成長時，用 cost attribution、cardinality budget、log tiering 跟 adaptive sampling 建立可預測成本模型。">4.C14 觀測平台成本治理</a> 有多家企業的具體經驗。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_cloudwatch_log_group&#34; &#34;api&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  name</span>              <span class="o">=</span> <span class="s2">&#34;/app/${var.env}/api&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">  retention_in_days</span> <span class="o">=</span><span class="n"> var.env</span> <span class="o">==</span> <span class="s2">&#34;prod&#34;</span> <span class="err">?</span> <span class="m">30</span> <span class="err">:</span> <span class="m">7</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">  kms_key_id</span>        <span class="o">=</span> <span class="k">aws_kms_key</span><span class="p">.</span><span class="k">logs</span><span class="p">.</span><span class="k">arn</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">}
</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="k">resource</span> <span class="s2">&#34;aws_cloudwatch_log_group&#34; &#34;audit&#34;</span> {
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">  name</span>              <span class="o">=</span> <span class="s2">&#34;/app/${var.env}/audit&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">  retention_in_days</span> <span class="o">=</span> <span class="m">365</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">  kms_key_id</span>        <span class="o">=</span> <span class="k">aws_kms_key</span><span class="p">.</span><span class="k">logs</span><span class="p">.</span><span class="k">arn</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">}</span></span></code></pre></div><p>Dev 環境的 retention 可以大幅縮短（7 天甚至 3 天），因為它不承擔合規責任，存取量也低，帳單節省直接對應這個差值。</p>
<h3 id="存取控制與加密">存取控制與加密</h3>
<p>「誰能讀」是 retention 之外的另一半。Log 經常夾帶 PII（使用者信箱、IP）、token 或內部結構，讀取權限要跟<a href="/blog/infra/02-identity-credentials/" data-link-title="模組二：身分與憑證地基 — IAM 與 OIDC" data-link-desc="IAM role / policy 設計、最小權限，以及用 OIDC 短期憑證取代長期 access key">模組二（身分與憑證地基）</a>建立的 IAM 角色一起管。</p>
<p>常見陷阱是 log 在傳輸與儲存都加密了（<code>kms_key_id</code> 有設），卻對整個團隊開放讀取。加密保護的是靜態資料不被未授權存取，但如果整個開發團隊都有 <code>logs:GetLogEvents</code> 權限，加密形同虛設 — read 權限應該縮到值班與稽核需要的最小集合。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 只允許 oncall role 讀取 prod log
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">data</span> <span class="s2">&#34;aws_iam_policy_document&#34; &#34;log_read&#34;</span> {
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  <span class="k">statement</span> {
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">    actions</span>   <span class="o">=</span> <span class="p">[</span><span class="s2">&#34;logs:GetLogEvents&#34;, &#34;logs:FilterLogEvents&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">    resources</span> <span class="o">=</span> <span class="p">[</span><span class="k">aws_cloudwatch_log_group</span><span class="p">.</span><span class="k">api</span><span class="p">.</span><span class="k">arn</span><span class="p">]</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></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_iam_role_policy&#34; &#34;oncall_log_read&#34;</span> {
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">  role</span>   <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">oncall_role_name</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">  policy</span> <span class="o">=</span> <span class="k">data</span><span class="p">.</span><span class="k">aws_iam_policy_document</span><span class="p">.</span><span class="k">log_read</span><span class="p">.</span><span class="k">json</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">}</span></span></code></pre></div><p>應用層該怎麼決定哪些欄位根本不該進 log（例如在 logger 層做 PII masking），屬於資料保護的範圍，見 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">backend 模組七：資安與資料保護</a>。</p>
<h2 id="metric-與-alarm-設計">metric 與 alarm 設計</h2>
<p>Metric 與 alarm 寫進 IaC，目的是讓「資源被建立的同時就帶著它的健康判準」。Alarm 是一份成文約定：哪條 metric、跨多長的評估窗口、超過什麼值要通知誰。把這份約定寫進 code，它就能被 review、被版本控制、被跨環境複用。</p>
<h3 id="症狀型-vs-成因型告警">症狀型 vs 成因型告警</h3>
<p>閾值設計是訊號與雜訊的取捨。告警可以分成兩類：症狀型（symptom-based）對應的是「使用者已經受影響」的指標 — 5xx 錯誤率、p99 延遲、佇列積壓。成因型（cause-based）對應的是「某個元件在劣化但使用者可能還沒感知」的指標 — CPU 使用率、記憶體使用率、磁碟 IOPS。</p>
<p>收益最高的起點是：症狀型設 alarm 並綁通知，成因型留在 dashboard 上作為診斷線索。理由是成因和症狀之間不一定有直接關係 — CPU 在 80% 不代表使用者受影響（可能 auto-scaling 正在長新節點），而 CPU 在 30% 也不代表安全（可能是某個 goroutine 卡住了，CPU 反而閒下來）。如果每個成因指標都獨立設 alarm，告警數量會與資源數量等比增長，訊噪比下降後症狀型告警容易被成因型告警淹沒。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 症狀型 alarm：5xx 超過閾值代表使用者已受影響
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">resource</span> <span class="s2">&#34;aws_cloudwatch_metric_alarm&#34; &#34;api_5xx&#34;</span> {
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">  alarm_name</span>          <span class="o">=</span> <span class="s2">&#34;${var.env}-api-5xx-rate&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">  comparison_operator</span> <span class="o">=</span> <span class="s2">&#34;GreaterThanThreshold&#34;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">  evaluation_periods</span>  <span class="o">=</span> <span class="m">3</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">  metric_name</span>         <span class="o">=</span> <span class="s2">&#34;5XXError&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">  namespace</span>           <span class="o">=</span> <span class="s2">&#34;AWS/ApiGateway&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">  period</span>              <span class="o">=</span> <span class="m">60</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">  statistic</span>           <span class="o">=</span> <span class="s2">&#34;Sum&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">  threshold</span>           <span class="o">=</span> <span class="m">10</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">  treat_missing_data</span>  <span class="o">=</span> <span class="s2">&#34;notBreaching&#34;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">  alarm_actions</span>       <span class="o">=</span> <span class="p">[</span><span class="k">var</span><span class="p">.</span><span class="k">oncall_sns_arn</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">}<span class="c1">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># 成因型指標：CPU 放 dashboard、不設 alarm
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"></span><span class="err">#</span> <span class="k">除非確認</span><span class="err">「</span><span class="k">CPU</span> <span class="k">到</span> <span class="k">X</span><span class="err">%</span> <span class="k">一定代表服務即將不可用</span><span class="err">」</span><span class="k">這個因果關係</span></span></span></code></pre></div><p>當成因和症狀之間有明確的因果閾值（例如 RDS 磁碟用量到 90% 就會開始拒絕寫入），那條成因也值得設 alarm — 關鍵是因果關係要確認過、而非假設。</p>
<h3 id="insufficient_data-的處理">INSUFFICIENT_DATA 的處理</h3>
<p><code>treat_missing_data</code> 決定了「沒收到 metric 資料點」時 alarm 怎麼判定。這個設定常被忽略，但它在兩個情境下會造成顯著差異：</p>
<p><strong>持續有資料的 metric</strong>（如 API request count）：資料突然消失通常代表服務掛了或 metric 管線斷了，應該設 <code>treat_missing_data = &quot;breaching&quot;</code> — 沒資料本身就是異常訊號。</p>
<p><strong>間歇性的 metric</strong>（如錯誤 count、某個低頻 Lambda 的 invocation）：平常就沒有資料點，沒資料代表正常運作，應該設 <code>treat_missing_data = &quot;notBreaching&quot;</code> — 避免每次低谷時段都觸發假告警。</p>
<p>判讀方式是問自己：「這條 metric 如果 10 分鐘沒有任何資料，代表好事還是壞事？」好事用 <code>notBreaching</code>，壞事用 <code>breaching</code>，不確定用 <code>ignore</code>（不改變 alarm 狀態，等下一個有資料的評估週期再判定）。</p>
<h3 id="告警必須連到動作">告警必須連到動作</h3>
<p>一條有用的 alarm 至少要綁定通知去向。<code>alarm_actions</code> 為空的 alarm 只會在 CloudWatch console 裡變色，而事故發生時沒有人會盯著 console 看 — alarm 的價值在於它主動推送到值班的人手上。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_sns_topic&#34; &#34;oncall&#34;</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">  name</span> <span class="o">=</span> <span class="s2">&#34;${var.env}-oncall-alerts&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">}
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_sns_topic_subscription&#34; &#34;pagerduty&#34;</span> {
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">  topic_arn</span> <span class="o">=</span> <span class="k">aws_sns_topic</span><span class="p">.</span><span class="k">oncall</span><span class="p">.</span><span class="k">arn</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="n">  protocol</span>  <span class="o">=</span> <span class="s2">&#34;https&#34;</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="n">  endpoint</span>  <span class="o">=</span> <span class="k">var</span><span class="p">.</span><span class="k">pagerduty_integration_url</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl">}</span></span></code></pre></div><p>通知去向也該寫進 IaC — SNS topic、subscription、整合端點都是基礎設施的一部分。手動建的 SNS subscription 跟手動建的 alarm 有同樣的問題：沒人記得、沒人維護、出事才發現斷了。</p>
<h3 id="把基礎告警做成-module-預設">把基礎告警做成 module 預設</h3>
<p>如果每次新服務上線都要有人「記得」去加 alarm，代表 alarm 還沒進 module 模板。把基礎告警（錯誤率、延遲、健康檢查失敗）做成服務模組的預設輸出，新服務 apply 時 alarm 跟著一起生出來：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># modules/service/variables.tf
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"></span><span class="k">variable</span> <span class="s2">&#34;alarm_5xx_threshold&#34;</span> {
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">  type</span>    <span class="o">=</span> <span class="k">number</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">  default</span> <span class="o">=</span> <span class="m">10</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">}
</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="k">variable</span> <span class="s2">&#34;alarm_latency_p99_ms&#34;</span> {
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">  type</span>    <span class="o">=</span> <span class="k">number</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">  default</span> <span class="o">=</span> <span class="m">3000</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">}</span></span></code></pre></div><p>開新服務時 alarm 跟著資源一起生出來，調整閾值才是該服務 owner 的選配。預設值的選擇依據是「保守但不擾民」— 初始閾值設寬一點，上線穩定後再根據實際基線收斂。</p>
<p>觀測訊號的設計有一個容易忽略的盲區：aggregated metric 會遮蔽局部惡化。Discord 在三代儲存架構的遷移過程中反覆遇到同一個問題——整體 p95 延遲正常，但少數 hot partition 或大型群組的延遲已經飆升，直到使用者回報才發現。教訓是 alarm 的維度要跟業務的 fan-out 結構對齊，而非只看全域聚合。詳見 <a href="/blog/backend/04-observability/cases/discord-storage-growth-observability-gap/" data-link-title="4.C13 Discord：從儲存問題回推觀測缺口" data-link-desc="每次儲存遷移都暴露觀測盲區，把儲存成長問題重新框架為訊號設計問題。">4.C13 Discord：從儲存問題回推觀測缺口</a>。規模化後叢集的動態擴縮也會改變觀測模型——擴縮事件本身要成為觀測對象，見 <a href="/blog/backend/04-observability/cases/airbnb-observability-k8s-scale-signals/" data-link-title="4.C8 Airbnb：Kubernetes 規模化下的觀測訊號治理" data-link-desc="叢集擴縮與工作負載變動如何回寫觀測模型。">4.C8 Airbnb：K8s 規模化觀測訊號治理</a>。</p>
<h2 id="基礎設施訊號-vs-客戶端行為訊號">基礎設施訊號 vs 客戶端行為訊號</h2>
<p>本模組的可觀測性處理基礎設施訊號，<a href="/blog/monitoring/" data-link-title="監控實務指南" data-link-desc="整理非伺服器端運行時的監控體系 — 行為蒐集、錯誤回報、效能指標、生命週期追蹤，從自架方案到商業方案的完整知識路線">Monitoring 監控體系</a>處理客戶端與業務行為訊號。兩者觀測的對象不同、生命週期也不同，因此分屬不同的 code 與不同的部署管道。</p>
<p>基礎設施訊號是資源層的健康狀態：log group retention、CPU、佇列深度、5xx 比例、實例存活。它們跟著資源被 IaC 建立與銷毀，回答的問題是「這個系統還活著嗎、哪裡壞了」。</p>
<p>客戶端行為訊號則是 SDK、Collector、業務埋點那一層：使用者點了什麼、轉換漏斗在哪裡流失、前端 JS 錯誤率、自訂業務事件。它們跟著產品功能演進、不跟著基礎設施資源同生共滅。</p>
<p>判讀分界的問法是：這個訊號是「資源建立時就該存在」還是「功能開發時才埋」。前者進本模組的 IaC，後者進 monitoring 那層的應用程式碼。</p>
<p>兩者在事故排查時會合流 — 基礎設施 alarm 告訴值班「RDS CPU 飆到 95%」，客戶端訊號告訴產品團隊「結帳頁面的失敗率從 0.1% 跳到 12%」。把兩條訊號交叉比對才能判斷影響範圍。但它們的擁有者、變更節奏與部署管道不同 — 基礎設施 alarm 跟著 infra PR 走，前端埋點跟著產品 sprint 走。混在同一份 code 裡會讓「誰負責這條訊號的閾值」變模糊，也讓 infra PR 的 review 範圍擴大到不相干的業務邏輯。</p>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/monitoring/" data-link-title="監控實務指南" data-link-desc="整理非伺服器端運行時的監控體系 — 行為蒐集、錯誤回報、效能指標、生命週期追蹤，從自架方案到商業方案的完整知識路線">monitoring 監控體系</a>：客戶端 SDK / Collector 那層的監控</li>
<li>→ <a href="/blog/infra/04-environment-separation/" data-link-title="模組四：環境分離與模組化" data-link-desc="dev / staging / prod 切分、目錄結構 vs workspace、用可重用 module 避免環境漂移">模組四：環境分離與模組化</a>：module 化在這裡延伸成「每個模組自帶 observability 宣告」</li>
<li>→ <a href="/blog/infra/05-core-services/" data-link-title="模組五：核心服務上 IaC" data-link-desc="資料庫、運算、儲存、load balancer 怎麼寫進基礎設施程式碼，以及上線順序">模組五：核心服務上 IaC</a>：每個核心服務帶自己的 log 與 alarm</li>
<li>→ <a href="/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：infra 走 PR 流程</a>：observability 變更也走 PR 與自動化護欄</li>
<li>→ <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">backend 模組七：資安與資料保護</a>：哪些欄位不該進 log、PII 處理</li>
</ul>
]]></content:encoded></item><item><title>模組六：可觀測性與 log 一併寫進 code</title><link>https://tarrragon.github.io/blog/infra/06-observability-logging/</link><pubDate>Fri, 26 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/infra/06-observability-logging/</guid><description>&lt;p>可觀測性要跟它監控的資源同生命週期：log group、metric 與 alarm 寫進建立資源的同一套 IaC，資源開出來的那一刻監控就在線，而非等出事才補。少了這條規則的代價很具體：凌晨資料庫 CPU 飆到 100%、API 開始逾時，值班工程師打開 console 想看 log，卻發現那個服務根本沒接 log group、metric 也只有 vendor 預設的幾條粗線，追不到呼叫鏈、查不到錯誤訊息，只能靠重啟賭它恢復。&lt;/p>
&lt;h2 id="observability-跟-infra-同一套-code同生命週期">observability 跟 infra 同一套 code、同生命週期&lt;/h2>
&lt;p>可觀測性是基礎設施的一部分，承擔「讓資源在出事時可被追查」的責任，因此它的建立、變更與銷毀要跟被監控的資源綁在同一個生命週期裡。一個 RDS 實例、一個 Lambda、一個 ECS service 被 IaC 建立時，它的 log group、它的關鍵 metric alarm 應該在同一份 plan 裡一起 apply；這個資源被 destroy 時，對應的 alarm 也一起收掉，不留下對著空資源狂叫的孤兒告警。&lt;/p>
&lt;p>把監控外掛在資源之外會製造兩種漂移。第一種是新資源沒有監控：service 透過 PR 加上去了，但 alarm 要某人事後手動進 console 點，於是有些 service 有 alarm、有些沒有，覆蓋率取決於誰記得。第二種是死資源留下殘響：資源砍了但 alarm 還在，半夜對著不存在的 target 噴 &lt;code>INSUFFICIENT_DATA&lt;/code>，值班的人學會忽略它，告警疲勞讓真的事故也被一起忽略。兩種漂移的共同根因都是監控跟資源不在同一個 apply 單位裡。&lt;/p>
&lt;p>判讀訊號很直接：如果有人能回答「這個服務有沒有 alarm」要去翻 console 而不是讀 code，監控就已經跟資源脫鉤了。修法是把監控宣告收進該資源的 module——模組四（環境分離與模組化）談的模組化在這裡延伸成「每個服務模組自帶它的 observability 宣告」，模組五（核心服務上 IaC）談的每個核心服務也應該在同一個 module 裡帶上自己的 log 與 alarm。&lt;/p>
&lt;h2 id="log-group-與-retention-設計">log group 與 retention 設計&lt;/h2>
&lt;p>Log group 是日誌的歸屬與保存單位，它要回答兩個治理問題：留多久、誰能讀。這兩個問題寫進 IaC 才能稽核，而非依賴 vendor 的隱性預設。許多雲端服務在你沒宣告 log group 時會自動建一個、套上「永久保留」的預設值，於是日誌無限堆積、帳單緩慢長大，而真正敏感的內容反而沒人管控存取。&lt;/p>
&lt;p>Retention 是成本、合規與除錯需求的三方取捨。除錯通常只需要近幾天到幾週的熱資料；合規（如稽核軌跡、金流紀錄）可能要求保留數年；而每多留一天就多一天的儲存費。划算的做法是按日誌類型分層：高頻、除錯用的 application log 設短 retention（例如 14 到 30 天），稽核相關的 access log 按合規要求設長期保留，必要時再把冷資料歸檔到更便宜的物件儲存。把這些值寫進 IaC，讓「為什麼這條 log 留 90 天」是一個能在 PR 上被討論的決定。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_cloudwatch_log_group&amp;#34; &amp;#34;api&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="n"> name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;/app/${var.env}/api&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="n"> retention_in_days&lt;/span> &lt;span class="o">=&lt;/span>&lt;span class="n"> var.env&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;prod&amp;#34;&lt;/span> &lt;span class="err">?&lt;/span> &lt;span class="m">30&lt;/span> &lt;span class="err">:&lt;/span> &lt;span class="m">7&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="n"> kms_key_id&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="k">aws_kms_key&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">logs&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">arn&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>「誰能讀」是 retention 之外的另一半，因為 log 經常夾帶 PII、token 或內部結構，讀取權限要跟身分地基一起管。存取控制掛在模組二（身分與憑證地基）建立的 IAM 角色上，加密金鑰則對應模組三、模組七一路延伸的金鑰治理。常見陷阱是 log 在傳輸與儲存都加密了，卻對整個團隊開放讀取，等於把敏感資料攤在所有人面前；read 權限應該縮到值班與稽核需要的最小集合。應用層該怎麼決定哪些欄位根本不該進 log，屬於資料保護的範圍，可往 &lt;code>/backend/07-security-data-protection/&lt;/code> 對齊。&lt;/p>
&lt;h2 id="metric-與-alarm-寫進-iac">metric 與 alarm 寫進 IaC&lt;/h2>
&lt;p>Metric 與 alarm 寫進 IaC，目的是讓「資源被建立的同時就帶著它的健康判準」。Alarm 不只是一個閾值，它是一份對「這個資源什麼狀態算不正常」的成文約定：哪條 metric、跨多長的評估窗口、超過什麼值要通知誰。把這份約定寫進 code，它就能被 review、被版本控制、被跨環境複用，而不是散落在某個人腦中或 console 的某個角落。&lt;/p>
&lt;p>Alarm 的價值在於它連到動作，而非只是亮一盞燈。一條有用的 alarm 至少要綁定通知去向（on-call 的 SNS topic、PagerDuty、Slack），並寫清楚 &lt;code>INSUFFICIENT_DATA&lt;/code> 怎麼處理——資料不足到底算正常還是異常，取決於這條 metric 平常是否持續有資料。閾值設計是訊號與雜訊的取捨：設太敏感會頻繁誤報、養出告警疲勞，設太鈍則錯過真正的劣化。划算的起點是針對「使用者已經受影響」的症狀型 metric 設 alarm（錯誤率、p99 延遲、佇列積壓），而把成因型指標（CPU、記憶體）留作 dashboard 上的診斷線索，避免每個成因都獨立告警。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-hcl" data-lang="hcl">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="k">resource&lt;/span> &lt;span class="s2">&amp;#34;aws_cloudwatch_metric_alarm&amp;#34; &amp;#34;api_5xx&amp;#34;&lt;/span> {
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="n"> alarm_name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;${var.env}-api-5xx-rate&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="n"> comparison_operator&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;GreaterThanThreshold&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="n"> evaluation_periods&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="n"> metric_name&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;5XXError&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="n"> namespace&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;AWS/ApiGateway&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="n"> period&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">60&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="n"> statistic&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;Sum&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="n"> threshold&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="m">10&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="n"> treat_missing_data&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="s2">&amp;#34;notBreaching&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="n"> alarm_actions&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="p">[&lt;/span>&lt;span class="k">aws_sns_topic&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">oncall&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="k">arn&lt;/span>&lt;span class="p">]&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">}&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>判讀訊號是：每次新服務上線都要有人「記得」去加 alarm，代表 alarm 還沒進 module 模板。修法是把基礎告警（錯誤率、延遲、健康檢查失敗）做成服務模組的預設輸出，讓開新服務時 alarm 跟著資源一起生出來，調整閾值才是該服務 owner 的選配。&lt;/p></description><content:encoded><![CDATA[<p>可觀測性要跟它監控的資源同生命週期：log group、metric 與 alarm 寫進建立資源的同一套 IaC，資源開出來的那一刻監控就在線，而非等出事才補。少了這條規則的代價很具體：凌晨資料庫 CPU 飆到 100%、API 開始逾時，值班工程師打開 console 想看 log，卻發現那個服務根本沒接 log group、metric 也只有 vendor 預設的幾條粗線，追不到呼叫鏈、查不到錯誤訊息，只能靠重啟賭它恢復。</p>
<h2 id="observability-跟-infra-同一套-code同生命週期">observability 跟 infra 同一套 code、同生命週期</h2>
<p>可觀測性是基礎設施的一部分，承擔「讓資源在出事時可被追查」的責任，因此它的建立、變更與銷毀要跟被監控的資源綁在同一個生命週期裡。一個 RDS 實例、一個 Lambda、一個 ECS service 被 IaC 建立時，它的 log group、它的關鍵 metric alarm 應該在同一份 plan 裡一起 apply；這個資源被 destroy 時，對應的 alarm 也一起收掉，不留下對著空資源狂叫的孤兒告警。</p>
<p>把監控外掛在資源之外會製造兩種漂移。第一種是新資源沒有監控：service 透過 PR 加上去了，但 alarm 要某人事後手動進 console 點，於是有些 service 有 alarm、有些沒有，覆蓋率取決於誰記得。第二種是死資源留下殘響：資源砍了但 alarm 還在，半夜對著不存在的 target 噴 <code>INSUFFICIENT_DATA</code>，值班的人學會忽略它，告警疲勞讓真的事故也被一起忽略。兩種漂移的共同根因都是監控跟資源不在同一個 apply 單位裡。</p>
<p>判讀訊號很直接：如果有人能回答「這個服務有沒有 alarm」要去翻 console 而不是讀 code，監控就已經跟資源脫鉤了。修法是把監控宣告收進該資源的 module——模組四（環境分離與模組化）談的模組化在這裡延伸成「每個服務模組自帶它的 observability 宣告」，模組五（核心服務上 IaC）談的每個核心服務也應該在同一個 module 裡帶上自己的 log 與 alarm。</p>
<h2 id="log-group-與-retention-設計">log group 與 retention 設計</h2>
<p>Log group 是日誌的歸屬與保存單位，它要回答兩個治理問題：留多久、誰能讀。這兩個問題寫進 IaC 才能稽核，而非依賴 vendor 的隱性預設。許多雲端服務在你沒宣告 log group 時會自動建一個、套上「永久保留」的預設值，於是日誌無限堆積、帳單緩慢長大，而真正敏感的內容反而沒人管控存取。</p>
<p>Retention 是成本、合規與除錯需求的三方取捨。除錯通常只需要近幾天到幾週的熱資料；合規（如稽核軌跡、金流紀錄）可能要求保留數年；而每多留一天就多一天的儲存費。划算的做法是按日誌類型分層：高頻、除錯用的 application log 設短 retention（例如 14 到 30 天），稽核相關的 access log 按合規要求設長期保留，必要時再把冷資料歸檔到更便宜的物件儲存。把這些值寫進 IaC，讓「為什麼這條 log 留 90 天」是一個能在 PR 上被討論的決定。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_cloudwatch_log_group&#34; &#34;api&#34;</span> {
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">  name</span>              <span class="o">=</span> <span class="s2">&#34;/app/${var.env}/api&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">  retention_in_days</span> <span class="o">=</span><span class="n"> var.env</span> <span class="o">==</span> <span class="s2">&#34;prod&#34;</span> <span class="err">?</span> <span class="m">30</span> <span class="err">:</span> <span class="m">7</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">  kms_key_id</span>        <span class="o">=</span> <span class="k">aws_kms_key</span><span class="p">.</span><span class="k">logs</span><span class="p">.</span><span class="k">arn</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">}</span></span></code></pre></div><p>「誰能讀」是 retention 之外的另一半，因為 log 經常夾帶 PII、token 或內部結構，讀取權限要跟身分地基一起管。存取控制掛在模組二（身分與憑證地基）建立的 IAM 角色上，加密金鑰則對應模組三、模組七一路延伸的金鑰治理。常見陷阱是 log 在傳輸與儲存都加密了，卻對整個團隊開放讀取，等於把敏感資料攤在所有人面前；read 權限應該縮到值班與稽核需要的最小集合。應用層該怎麼決定哪些欄位根本不該進 log，屬於資料保護的範圍，可往 <code>/backend/07-security-data-protection/</code> 對齊。</p>
<h2 id="metric-與-alarm-寫進-iac">metric 與 alarm 寫進 IaC</h2>
<p>Metric 與 alarm 寫進 IaC，目的是讓「資源被建立的同時就帶著它的健康判準」。Alarm 不只是一個閾值，它是一份對「這個資源什麼狀態算不正常」的成文約定：哪條 metric、跨多長的評估窗口、超過什麼值要通知誰。把這份約定寫進 code，它就能被 review、被版本控制、被跨環境複用，而不是散落在某個人腦中或 console 的某個角落。</p>
<p>Alarm 的價值在於它連到動作，而非只是亮一盞燈。一條有用的 alarm 至少要綁定通知去向（on-call 的 SNS topic、PagerDuty、Slack），並寫清楚 <code>INSUFFICIENT_DATA</code> 怎麼處理——資料不足到底算正常還是異常，取決於這條 metric 平常是否持續有資料。閾值設計是訊號與雜訊的取捨：設太敏感會頻繁誤報、養出告警疲勞，設太鈍則錯過真正的劣化。划算的起點是針對「使用者已經受影響」的症狀型 metric 設 alarm（錯誤率、p99 延遲、佇列積壓），而把成因型指標（CPU、記憶體）留作 dashboard 上的診斷線索，避免每個成因都獨立告警。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-hcl" data-lang="hcl"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">resource</span> <span class="s2">&#34;aws_cloudwatch_metric_alarm&#34; &#34;api_5xx&#34;</span> {
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">  alarm_name</span>          <span class="o">=</span> <span class="s2">&#34;${var.env}-api-5xx-rate&#34;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">  comparison_operator</span> <span class="o">=</span> <span class="s2">&#34;GreaterThanThreshold&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">  evaluation_periods</span>  <span class="o">=</span> <span class="m">3</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="n">  metric_name</span>         <span class="o">=</span> <span class="s2">&#34;5XXError&#34;</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">  namespace</span>           <span class="o">=</span> <span class="s2">&#34;AWS/ApiGateway&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">  period</span>              <span class="o">=</span> <span class="m">60</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">  statistic</span>           <span class="o">=</span> <span class="s2">&#34;Sum&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">  threshold</span>           <span class="o">=</span> <span class="m">10</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">  treat_missing_data</span>  <span class="o">=</span> <span class="s2">&#34;notBreaching&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">  alarm_actions</span>       <span class="o">=</span> <span class="p">[</span><span class="k">aws_sns_topic</span><span class="p">.</span><span class="k">oncall</span><span class="p">.</span><span class="k">arn</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">}</span></span></code></pre></div><p>判讀訊號是：每次新服務上線都要有人「記得」去加 alarm，代表 alarm 還沒進 module 模板。修法是把基礎告警（錯誤率、延遲、健康檢查失敗）做成服務模組的預設輸出，讓開新服務時 alarm 跟著資源一起生出來，調整閾值才是該服務 owner 的選配。</p>
<h2 id="跟-monitoring-系列的分工基礎設施訊號-vs-客戶端行為訊號">跟 monitoring 系列的分工：基礎設施訊號 vs 客戶端行為訊號</h2>
<p>本模組的可觀測性處理基礎設施訊號，monitoring 系列處理客戶端與業務行為訊號，兩者觀測的對象不同、生命週期也不同，因此分屬不同的 code 與不同的章節。基礎設施訊號是資源層的健康狀態：log group、CPU、佇列深度、5xx 比例、實例存活，它們跟著資源被 IaC 建立與銷毀，回答「這個系統還活著嗎、哪裡壞了」。</p>
<p>客戶端行為訊號則是 SDK、Collector、業務埋點那一層：使用者點了什麼、轉換漏斗、前端錯誤、自訂事件，它們跟著產品功能演進、不跟著基礎設施資源同生共滅，所以放在 <code>/monitoring/</code>。判讀分界的問法是：這個訊號是「資源建立時就該存在」還是「功能開發時才埋」。前者進本模組的 IaC，後者進 monitoring 那層的應用程式碼。兩者在事故排查時會合流——基礎設施 alarm 告訴你哪個資源異常，客戶端訊號告訴你使用者實際受了什麼影響——但它們的擁有者、變更節奏與部署管道不同，混在一起會讓「誰負責這條訊號」變模糊。</p>
<p>收斂成一句判準：資源建立時就該存在的訊號歸本模組的 IaC，功能開發時才埋的客戶端行為訊號歸另一層；各條延伸章節見下方跨分類引用。</p>
<h2 id="章節文章">章節文章</h2>
<table>
  <thead>
      <tr>
          <th>文章</th>
          <th>主題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/infra/06-observability-logging/log-metric-alarm-lifecycle/" data-link-title="可觀測性與 log 同生命週期管理" data-link-desc="log group、metric、alarm 寫進建立資源的同一套 IaC，讓監控跟資源同生共滅，出事時追得到查得到">可觀測性與 log 同生命週期管理</a></td>
          <td>log group、metric、alarm 寫進同一套 IaC，讓監控跟資源同生共滅，出事時追得到查得到</td>
      </tr>
  </tbody>
</table>
<h2 id="跨分類引用">跨分類引用</h2>
<ul>
<li>→ <a href="/blog/monitoring/" data-link-title="監控實務指南" data-link-desc="整理非伺服器端運行時的監控體系 — 行為蒐集、錯誤回報、效能指標、生命週期追蹤，從自架方案到商業方案的完整知識路線">Monitoring 監控體系</a>：客戶端 SDK / Collector 那層的監控</li>
<li>→ <a href="/blog/infra/05-core-services/" data-link-title="模組五：核心服務上 IaC" data-link-desc="資料庫、運算、儲存、load balancer 怎麼寫進基礎設施程式碼，以及上線順序">模組五：核心服務上 IaC</a>：每個核心服務帶自己的 log 與 alarm</li>
<li>→ <a href="/blog/infra/07-infra-as-pr/" data-link-title="模組七：infra 走 PR 流程與自動化護欄" data-link-desc="infra 變更走 PR → plan → review diff → 合併 → apply，配 fmt / validate / tflint / checkov / tfsec 與 Atlantis 自動化，讓基礎設施可審查、可回溯、可交接">模組七：infra 走 PR 流程</a>：observability 變更也走 PR 與自動化護欄</li>
<li>→ <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">backend 模組七：資安與資料保護</a>：哪些欄位不該進 log、PII 處理</li>
</ul>
]]></content:encoded></item><item><title>CloudWatch Alarms 與 Composite Alarms 操作實務</title><link>https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/alarms-composite-operations/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/alarms-composite-operations/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">AWS CloudWatch&lt;/a> 的 vendor deep article，深化 overview「Alarm + Composite alarm + EventBridge rule」段。初次接觸 CloudWatch 的讀者建議先讀 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch 服務頁&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;h2 id="問題情境">問題情境&lt;/h2>
&lt;p>CloudWatch Alarm 是 AWS 原生的告警機制，跟 Prometheus Alertmanager 或 Datadog Monitor 的定位相同 — 把 metric 異常轉成可操作通知。CloudWatch Alarm 的特性是跟 AWS 服務深度整合（Auto Scaling、SNS、Lambda、Systems Manager），但告警邏輯表達力比 PromQL alerting rule 弱。Composite Alarm 是 CloudWatch 用來降低 alert noise 的方式，把多個 alarm 的布林組合當成觸發條件。&lt;/p>
&lt;h2 id="metric-alarm-基礎">Metric Alarm 基礎&lt;/h2>
&lt;h3 id="alarm-參數">Alarm 參數&lt;/h3>
&lt;p>每個 metric alarm 由五個參數決定行為：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>參數&lt;/th>
 &lt;th>說明&lt;/th>
 &lt;th>常見設定&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Metric&lt;/td>
 &lt;td>要監控的 metric（namespace + metric name + dimension）&lt;/td>
 &lt;td>&lt;code>AWS/EC2 CPUUtilization InstanceId=i-xxx&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Statistic&lt;/td>
 &lt;td>聚合方式（Average / Sum / Maximum / Minimum / p99）&lt;/td>
 &lt;td>根據 metric 性質選擇&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Period&lt;/td>
 &lt;td>每個 data point 的時間窗&lt;/td>
 &lt;td>60s（standard）/ 10s（high-resolution）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Evaluation periods&lt;/td>
 &lt;td>連續幾個 period 超過閾值才觸發&lt;/td>
 &lt;td>3-5 個 period 減少 flapping&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Threshold&lt;/td>
 &lt;td>觸發閾值&lt;/td>
 &lt;td>跟 SLO 對齊&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Evaluation periods 的意義是「連續 N 個 period 都違反閾值才進入 ALARM 狀態」。設太低（1 個 period）容易 flapping，設太高（10 個 period）會延遲告警。多數場景 3 個 period × 60 秒 = 3 分鐘是合理起點。&lt;/p>
&lt;h3 id="datapoints-to-alarm">Datapoints to Alarm&lt;/h3>
&lt;p>除了 evaluation periods，CloudWatch 還有 &lt;code>Datapoints to Alarm&lt;/code> 參數 — 在 evaluation periods 的窗口中，至少幾個 datapoint 超過閾值就觸發。例如 &lt;code>3 of 5&lt;/code> 代表最近 5 個 period 中有 3 個超過閾值就觸發。&lt;/p>
&lt;p>這個設計讓告警在有缺失 datapoint 的環境下更穩健。容器重啟、Lambda cold start 或 scrape timeout 都可能造成某些 period 沒有 datapoint，&lt;code>M of N&lt;/code> 模式避免因為缺失資料而延遲告警。&lt;/p>
&lt;h2 id="anomaly-detection-alarm">Anomaly Detection Alarm&lt;/h2>
&lt;h3 id="用途">用途&lt;/h3>
&lt;p>Anomaly Detection alarm 用機器學習模型建立 metric 的 baseline band，metric 偏離 band 就觸發。適合沒有固定閾值的 metric — 例如 request count 在白天高、晚上低，用固定閾值會在晚上誤報或白天漏報。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">AWS CloudWatch</a> 的 vendor deep article，深化 overview「Alarm + Composite alarm + EventBridge rule」段。初次接觸 CloudWatch 的讀者建議先讀 <a href="/blog/backend/04-observability/vendors/aws-cloudwatch/" data-link-title="AWS CloudWatch" data-link-desc="AWS 原生觀測性服務、Logs / Metrics / Traces (X-Ray)">CloudWatch 服務頁</a>。</p></blockquote>
<h2 id="問題情境">問題情境</h2>
<p>CloudWatch Alarm 是 AWS 原生的告警機制，跟 Prometheus Alertmanager 或 Datadog Monitor 的定位相同 — 把 metric 異常轉成可操作通知。CloudWatch Alarm 的特性是跟 AWS 服務深度整合（Auto Scaling、SNS、Lambda、Systems Manager），但告警邏輯表達力比 PromQL alerting rule 弱。Composite Alarm 是 CloudWatch 用來降低 alert noise 的方式，把多個 alarm 的布林組合當成觸發條件。</p>
<h2 id="metric-alarm-基礎">Metric Alarm 基礎</h2>
<h3 id="alarm-參數">Alarm 參數</h3>
<p>每個 metric alarm 由五個參數決定行為：</p>
<table>
  <thead>
      <tr>
          <th>參數</th>
          <th>說明</th>
          <th>常見設定</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Metric</td>
          <td>要監控的 metric（namespace + metric name + dimension）</td>
          <td><code>AWS/EC2 CPUUtilization InstanceId=i-xxx</code></td>
      </tr>
      <tr>
          <td>Statistic</td>
          <td>聚合方式（Average / Sum / Maximum / Minimum / p99）</td>
          <td>根據 metric 性質選擇</td>
      </tr>
      <tr>
          <td>Period</td>
          <td>每個 data point 的時間窗</td>
          <td>60s（standard）/ 10s（high-resolution）</td>
      </tr>
      <tr>
          <td>Evaluation periods</td>
          <td>連續幾個 period 超過閾值才觸發</td>
          <td>3-5 個 period 減少 flapping</td>
      </tr>
      <tr>
          <td>Threshold</td>
          <td>觸發閾值</td>
          <td>跟 SLO 對齊</td>
      </tr>
  </tbody>
</table>
<p>Evaluation periods 的意義是「連續 N 個 period 都違反閾值才進入 ALARM 狀態」。設太低（1 個 period）容易 flapping，設太高（10 個 period）會延遲告警。多數場景 3 個 period × 60 秒 = 3 分鐘是合理起點。</p>
<h3 id="datapoints-to-alarm">Datapoints to Alarm</h3>
<p>除了 evaluation periods，CloudWatch 還有 <code>Datapoints to Alarm</code> 參數 — 在 evaluation periods 的窗口中，至少幾個 datapoint 超過閾值就觸發。例如 <code>3 of 5</code> 代表最近 5 個 period 中有 3 個超過閾值就觸發。</p>
<p>這個設計讓告警在有缺失 datapoint 的環境下更穩健。容器重啟、Lambda cold start 或 scrape timeout 都可能造成某些 period 沒有 datapoint，<code>M of N</code> 模式避免因為缺失資料而延遲告警。</p>
<h2 id="anomaly-detection-alarm">Anomaly Detection Alarm</h2>
<h3 id="用途">用途</h3>
<p>Anomaly Detection alarm 用機器學習模型建立 metric 的 baseline band，metric 偏離 band 就觸發。適合沒有固定閾值的 metric — 例如 request count 在白天高、晚上低，用固定閾值會在晚上誤報或白天漏報。</p>
<h3 id="設定">設定</h3>





<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">aws cloudwatch put-anomaly-detector <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --namespace AWS/ApplicationELB <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --metric-name RequestCount <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --dimensions <span class="nv">Name</span><span class="o">=</span>LoadBalancer,Value<span class="o">=</span>app/my-alb/xxx <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  --stat Sum</span></span></code></pre></div><p>Anomaly Detection 需要至少兩週的歷史資料才能建立可靠 baseline。新服務上線初期先用固定閾值 alarm，等累積足夠資料後再切換。</p>
<h3 id="band-width-控制">Band width 控制</h3>
<p>Anomaly Detection band 的寬度用標準差倍數控制（預設 2）。band 太窄（1x）容易誤報，太寬（3x）漏報。生產經驗是 API latency 用 2x、batch job duration 用 3x（batch 的自然波動較大）。</p>
<h2 id="composite-alarm">Composite Alarm</h2>
<h3 id="問題alert-noise">問題：Alert noise</h3>
<p>單一 metric alarm 太多時，on-call 會收到大量相關但重複的通知。一個下游服務故障可能同時觸發 latency alarm、error rate alarm、timeout alarm、queue lag alarm — 都指向同一個根因，但各自通知。</p>
<h3 id="解法布林組合">解法：布林組合</h3>
<p>Composite Alarm 用布林表達式組合多個 alarm，只在組合條件成立時觸發。</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">ALARM(&#34;checkout-latency-high&#34;)
</span></span><span class="line"><span class="ln">2</span><span class="cl">AND ALARM(&#34;payment-error-rate-high&#34;)
</span></span><span class="line"><span class="ln">3</span><span class="cl">AND NOT ALARM(&#34;scheduled-maintenance-window&#34;)</span></span></code></pre></div><p>這個組合代表：checkout latency 高且 payment error rate 也高，但排除了計畫維護視窗 — 才通知 on-call。</p>
<h3 id="設計原則">設計原則</h3>
<p>Composite Alarm 的設計應該反映事故判讀邏輯，而非機械式組合。三個常見模式：</p>
<p><strong>Symptom + cause 組合</strong>：外部症狀（latency 高）加上內部原因（DB connection pool 飽和）同時成立才通知。避免 latency 短暫抖動就告警。</p>
<p><strong>Cross-service correlation</strong>：多個服務同時出現異常時觸發「可能是 shared dependency 問題」的 composite alarm。一個服務異常可能是部署問題，多個同時異常更可能是共用依賴（load balancer、DNS、shared database）。</p>
<p><strong>Suppression window</strong>：用 maintenance window alarm 做 NOT 條件，在計畫維護期間抑制告警。</p>
<h3 id="限制">限制</h3>
<ul>
<li>Composite Alarm 最多引用 5 個 child alarm</li>
<li>巢狀深度最多 1 層（composite 不能引用另一個 composite）</li>
<li>Composite Alarm 本身不產生 metric，只做觸發邏輯</li>
</ul>
<p>超過 5 個 child alarm 時，需要把相關 alarm 先組成一個 composite，再讓上層 composite 引用。但因為不支援巢狀，實際能組合的 alarm 數量有限。複雜告警邏輯需要用 EventBridge rule 搭配 Lambda 處理。</p>
<h2 id="alarm-actions">Alarm actions</h2>
<h3 id="常見-action-類型">常見 action 類型</h3>
<p>Alarm 進入 ALARM 狀態時可以觸發多種 action：</p>
<table>
  <thead>
      <tr>
          <th>Action 類型</th>
          <th>用途</th>
          <th>設定方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>SNS Topic</td>
          <td>通知 on-call（email、SMS、PagerDuty integration）</td>
          <td>alarm action → SNS ARN</td>
      </tr>
      <tr>
          <td>Auto Scaling policy</td>
          <td>自動擴容</td>
          <td>alarm action → scaling policy ARN</td>
      </tr>
      <tr>
          <td>Lambda function</td>
          <td>自訂邏輯（建 ticket、關閉服務、修改 config）</td>
          <td>alarm action → Lambda ARN（透過 SNS）</td>
      </tr>
      <tr>
          <td>Systems Manager runbook</td>
          <td>自動執行 remediation runbook</td>
          <td>alarm action → SSM automation ARN</td>
      </tr>
      <tr>
          <td>EC2 action</td>
          <td>停止 / 重啟 / 終止 instance</td>
          <td>alarm action → EC2 action（僅限 EC2 metric）</td>
      </tr>
  </tbody>
</table>
<p>生產環境通常同時設定 ALARM 跟 OK action — ALARM 時通知 on-call，回到 OK 時自動 resolve incident。忘記設 OK action 會造成 on-call 收到告警但不知道何時恢復。</p>
<h3 id="跟-eventbridge-整合">跟 EventBridge 整合</h3>
<p>CloudWatch Alarm 狀態變更會自動送到 EventBridge（事件類型 <code>CloudWatch Alarm State Change</code>）。EventBridge rule 可以做更靈活的路由：</p>
<ul>
<li>根據 alarm name pattern 路由到不同 SNS topic</li>
<li>根據 alarm description 中的 severity tag 決定通知管道</li>
<li>多個 alarm 同時進入 ALARM 時觸發 incident 建立</li>
</ul>
<p>EventBridge 的路由能力彌補了 CloudWatch Alarm 本身路由邏輯簡單的限制。</p>
<h2 id="missing-data-處理">Missing data 處理</h2>
<h3 id="四種策略">四種策略</h3>
<p>Alarm evaluation 遇到缺失 datapoint 時，有四種處理方式：</p>
<table>
  <thead>
      <tr>
          <th>策略</th>
          <th>行為</th>
          <th>適合場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>missing</code></td>
          <td>維持上一個狀態</td>
          <td>多數場景的預設選擇</td>
      </tr>
      <tr>
          <td><code>breaching</code></td>
          <td>視為超過閾值</td>
          <td>metric 消失本身就是問題（heartbeat metric）</td>
      </tr>
      <tr>
          <td><code>notBreaching</code></td>
          <td>視為正常</td>
          <td>metric 在低流量時段自然消失</td>
      </tr>
      <tr>
          <td><code>ignore</code></td>
          <td>跳過該 period</td>
          <td>不影響 evaluation window</td>
      </tr>
  </tbody>
</table>
<p><code>breaching</code> 適合 heartbeat 類型的 metric — 服務應該持續回報 metric，停止回報代表服務掛了。<code>notBreaching</code> 適合流量驅動的 metric — 凌晨沒有 request 時自然沒有 latency datapoint，不應該觸發告警。</p>
<p>選錯 missing data 策略是 alarm flapping 的常見原因。Lambda function 的 metric 在沒有 invocation 時沒有 datapoint，用預設的 <code>missing</code> 或 <code>breaching</code> 都會造成問題。Lambda metric alarm 應該用 <code>notBreaching</code>。</p>
<h2 id="cross-region-限制">Cross-region 限制</h2>
<p>CloudWatch Alarm 跟 metric 綁定在同一個 region。跨 region 告警的兩種方式：</p>
<p><strong>Cross-account observability</strong>：monitoring account 可以看到 source account 的 CloudWatch 資料，但 alarm 仍然必須建在 metric 所在的 region。</p>
<p><strong>Custom metric replication</strong>：用 Lambda 或 Kinesis 把 metric 從 source region publish 到 central region，在 central region 建立統一 alarm。增加複雜度跟延遲，但能集中管理告警。</p>
<p>多數團隊選擇在每個 region 建各自的 alarm，用統一的 SNS topic（跨 region publish 到 central topic）收斂通知。告警邏輯去中心化，通知管道集中化。</p>
<h2 id="cost-考量">Cost 考量</h2>
<p>CloudWatch Alarm 的主要成本來自：</p>
<table>
  <thead>
      <tr>
          <th>計費項目</th>
          <th>計費方式</th>
          <th>常見數量</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Standard resolution alarm</td>
          <td>每 alarm / month</td>
          <td>多數服務 10-50 個 alarm</td>
      </tr>
      <tr>
          <td>High-resolution alarm（10s）</td>
          <td>每 alarm / month（3 倍 standard）</td>
          <td>只用在關鍵 SLI</td>
      </tr>
      <tr>
          <td>Anomaly Detection alarm</td>
          <td>每 alarm / month（含 ML 模型）</td>
          <td>比 standard 貴約 2-3 倍</td>
      </tr>
      <tr>
          <td>Composite Alarm</td>
          <td>免費</td>
          <td>只算 child alarm</td>
      </tr>
  </tbody>
</table>
<p>數量控制的判準：每個服務 10-30 個 metric alarm 加 2-5 個 composite alarm 是合理範圍。超過 100 個 alarm 時先檢查是否有冗餘（同一 metric 不同 period 的重複 alarm）。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<ul>
<li>告警設計原則：alarm 跟 dashboard 的搭配，見 <a href="/blog/backend/04-observability/dashboard-alert/" data-link-title="4.4 dashboard 與 alert 設計" data-link-desc="讓 dashboard 與 alert 對應 runbook 與容量趨勢">4.4 Dashboard 與 Alert 設計</a></li>
<li>SLI/SLO 對齊：把 alarm 閾值跟 SLO 對齊，見 <a href="/blog/backend/04-observability/sli-slo-signal/" data-link-title="4.6 SLI 量測與 SLO 訊號設計" data-link-desc="把可靠性目標的訊號從 metric 端設計好、餵給 6.6 SLO 政策">4.6 SLI 量測與 SLO 訊號設計</a></li>
<li>Log-based alerting：從 log 產生 metric 再建 alarm，見 <a href="../logs-insights-governance/">CloudWatch Logs Insights 查詢與日誌治理</a></li>
<li>事故響應整合：alarm → EventBridge → PagerDuty / incident tool，見 <a href="/blog/backend/08-incident-response/" data-link-title="模組八：事故處理與復盤" data-link-desc="用 IR 領域詞彙建問題節點、以服務級案例庫累積事故脈絡，先建概念與案例庫再進實作交接">08 Incident Response 模組</a></li>
</ul>
]]></content:encoded></item></channel></rss>