<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>草飼工程師 | Grass-fed Engineer</title>
  
  
  <link href="https://grass-fed.engineer/atom.xml" rel="self"/>
  
  <link href="https://grass-fed.engineer/"/>
  <updated>2022-12-30T15:17:08.873Z</updated>
  <id>https://grass-fed.engineer/</id>
  
  <author>
    <name>Grass-fed Engineer</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>筆記：OAuth 2.0 的 `state` 與 PKCE</title>
    <link href="https://grass-fed.engineer/2022/04/18/oauth-state-pkce/"/>
    <id>https://grass-fed.engineer/2022/04/18/oauth-state-pkce/</id>
    <published>2022-04-18T13:14:04.000Z</published>
    <updated>2022-12-30T15:17:08.873Z</updated>
    
    <content type="html"><![CDATA[<p>前陣子公司需要製作自己的 OAuth Provider，所以看了不少文件，但是花了很久才搞清楚 <code>state</code> 跟 PKCE 之間的差異以及各自想解決的問題。</p><p>在此記錄一下跟同事之間切磋、交流後的一些筆記。</p><span id="more"></span><h2 id="state"><a href="#state" class="headerlink" title="state"></a><code>state</code></h2><p><code>state</code> 在 <a href="https://datatracker.ietf.org/doc/html/rfc6749#section-10.12">RFC 6749 #10.12</a> 有提到相關用途，主要是為了防範 CSRF。看了滿多網路上的資源都沒有提到相關的攻擊細節，研究了很久，終於有點心得。</p><h3 id="情境"><a href="#情境" class="headerlink" title="情境"></a>情境</h3><p>假設有個網站（<code>grass-fed.engineer</code>），可以讓使用者在上面做筆記，使用者可以在登入後，選擇將筆記存入自己的 GitHub (<code>github.com</code>) 中。</p><ol><li><p>使用者會先登入 <code>grass-fed.engineer</code>，做完筆記，然後按下按鈕將筆記存入 GitHub 中。</p></li><li><p>因為會需要存取 GitHub 的資源，按下「存入 GitHub」按鈕後，使用者會被導向 GitHub OAuth 授權存取。這時瀏覽器網址會是</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://github.com/login/oauth/authorize?redirect_uri=https://grass-fed.engineer/oauth/callback</span><br></pre></td></tr></table></figure></li><li><p>使用者在 GitHub 完成授權後，會被導向</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://grass-fed.engineer/oauth/callback?code=XMPcVwx8vngm7NhJIAQdQPAwKH2m2YUZ</span><br></pre></td></tr></table></figure></li><li><p>Client 收到 request，會拿出 <code>code</code>，搭配 <code>client_secret</code> 到 GitHub 交換 <code>access_token</code>。</p></li></ol><h3 id="潛在問題"><a href="#潛在問題" class="headerlink" title="潛在問題"></a>潛在問題</h3><p>一切都看似美好，但是其實任何人都可以透過讓使用者呼叫 <code>https://grass-fed.engineer/oauth/callback?code=&lt;壞人的 code&gt;</code> 來欺騙 client 執行第 4 步。</p><p>這樣就有可能使用者綁定的 GitHub 帳號會變成壞人的帳號。</p><p>聽起來好像是壞人把自己帳號送了出去，沒什麼好處，但是從此以後，壞人就會拿到所有使用者想存在自己帳號的筆記。</p><p>輸出筆記事小，但是如果是 PayPal 存入授權，想來就是會出大事的。</p><p>實務上的操作可以是透過網頁的 <code>img</code> tag 嵌入壞人的 URL，讓瀏覽器在下載圖片時變成收到 redirect 請求，進而跳到 <code>grass-fed.engineer</code> 進行帳號授權綁定壞人的 GitHub 帳號。</p><h3 id="解方"><a href="#解方" class="headerlink" title="解方"></a>解方</h3><p><code>state</code> 是由 client 產生，令人難以捉摸的一串字，通常會被存放在 cookies 或是 local storage 裡，並且會跟隨第 1 步一起送給 Authorization Server。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://github.com/login/oauth/authorize?state=9YVefLNc&amp;redirect_uri=https://grass-fed.engineer/oauth/callback</span><br></pre></td></tr></table></figure><p>Authorization Server 會在第 3 步重新導向時，在 URI 加入相同的 <code>state</code>。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://grass-fed.engineer/oauth/callback?state=9YVefLNc&amp;code=XMPcVwx8vngm7NhJIAQdQPAwKH2m2YUZ</span><br></pre></td></tr></table></figure><p>Client 在收到 request 後，會驗證 URL 中的 <code>state</code> 是否跟 local 的 <code>state</code> 相同，如果是，就代表這個 request 當初是自己發出去的，可以開始兌換 <code>access_token</code>；如果不相同，就代表有可能被偽冒了，必須終止兌換流程。</p><h3 id="小結"><a href="#小結" class="headerlink" title="小結"></a>小結</h3><p><code>state</code> 可以用來確保 request 確實是在同一個 client 發送的，而不是他人任意給的假冒 request。</p><h2 id="PKCE"><a href="#PKCE" class="headerlink" title="PKCE"></a>PKCE</h2><p>PKCE，全名是 Proof Key for Code Exchange，是 OAuth 2.0 的一種擴充協議。主要是為了解決部分 client 無法安全保護 <code>client_secret</code> 以及 authorization code 時的一種安全驗證機制。</p><h3 id="情境-1"><a href="#情境-1" class="headerlink" title="情境"></a>情境</h3><p>最常見的場景是原生手機 app，使用者在瀏覽器上完成 Authorization Server 授權後，Authorization Server 會重新導向至指定 URL。</p><p>這種 URL 的 protocol 通常不是 <code>http://</code> 或 <code>https://</code>，而是如 <code>engineer://</code> 這類的 deep link，這樣一來才能夠將使用者導回 app 中。</p><p>這類的 deep link 仰賴手機作業系統處理，使用者的手機極有可能暗藏惡意軟體，會從中攔截 <code>code</code>，然後利用 <code>code</code> 換取不當授權。</p><h3 id="解方-1"><a href="#解方-1" class="headerlink" title="解方"></a>解方</h3><p>原生手機 app 產生 <code>code_verifier</code>，並利用特定方式生成 <code>code_challenge</code>，詳細內容可以參考 <a href="https://datatracker.ietf.org/doc/html/rfc7636">RFC 7636 #4.1</a>。</p><p>在 URL 中加入特定參數 <code>code_challenge</code>，並利用瀏覽器開啟連結進行授權</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">https://github.com/login/oauth/authorize?code_challenge=DU5hBtgoNO7ejhinrnvxNOFvc5JQyA6Ki7MmFsFIdJzViY&amp;redirect_uri=engineer://oauth/callback</span><br></pre></td></tr></table></figure><p>Autorization Server 完成授權後，會將 <code>code_challenge</code> 跟 <code>code</code> 存下，之後會導向至</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">engineer://oauth/callback?code=9O5avOd4vlOTY6Ye96JSyRxSIDKGQIzF</span><br></pre></td></tr></table></figure><p>原生手機 app 開啟，並利用 <code>code</code> 以及 <code>code_verifier</code> 交換 <code>access_token</code>。如果 Authorization Server 上的 <code>code_challenge</code> 跟傳送上來的 <code>code_verifier</code> 相符，則配發 <code>access_token</code>；反之，不配發 <code>access_token</code>。</p><p>這樣一來即使 <code>code</code> 被攔截，也很難真的換到 <code>access_token</code>。</p><h3 id="小結-1"><a href="#小結-1" class="headerlink" title="小結"></a>小結</h3><p>在沒有辦法安全保存 <code>client_secret</code> 的環境中，可以使用 PCKE 產生一次性的驗證金鑰，確保 <code>code</code> 不會任意被攔截並換取不屬於自己的 <code>access_token</code>。</p><h2 id="該用什麼？"><a href="#該用什麼？" class="headerlink" title="該用什麼？"></a>該用什麼？</h2><p><code>state</code> 跟 PKCE 看似相似，但效用非常不同：</p><ul><li><code>state</code>：防止壞人利用自己的 <code>code</code> 讓好人綁錯帳號</li><li>PKCE：防止 <code>code</code> 被壞人攔截並挪作他用</li></ul><p>最佳作法是兩者皆要實作才可以換取較高的安全性。</p><h2 id="參考資料"><a href="#參考資料" class="headerlink" title="參考資料"></a>參考資料</h2><ul><li><a href="https://auth0.com/docs/get-started/authentication-and-authorization-flow/authorization-code-flow-with-proof-key-for-code-exchange-pkce">Authorization Code Flow with Proof Key for Code Exchange (PKCE)</a></li><li><a href="https://security.stackexchange.com/questions/214980/does-pkce-replace-state-in-the-authorization-code-oauth-flow">Does PKCE replace state in the Authorization Code OAuth flow?</a></li></ul>]]></content>
    
    
    <summary type="html">&lt;p&gt;前陣子公司需要製作自己的 OAuth Provider，所以看了不少文件，但是花了很久才搞清楚 &lt;code&gt;state&lt;/code&gt; 跟 PKCE 之間的差異以及各自想解決的問題。&lt;/p&gt;
&lt;p&gt;在此記錄一下跟同事之間切磋、交流後的一些筆記。&lt;/p&gt;</summary>
    
    
    
    
  </entry>
  
  <entry>
    <title>Debug 實錄：Auto Scaling 讓我的 Sidekiq 任務消失了</title>
    <link href="https://grass-fed.engineer/2021/10/24/tracing-down-lost-sidekiq-jobs/"/>
    <id>https://grass-fed.engineer/2021/10/24/tracing-down-lost-sidekiq-jobs/</id>
    <published>2021-10-24T13:13:13.000Z</published>
    <updated>2022-12-30T15:17:08.873Z</updated>
    
    <content type="html"><![CDATA[<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/hint.css/2.4.1/hint.min.css"><p>最近工作上遇到一個難題，我們的背景處理系統 Sidekiq，在 AWS Auto Scaling 自動關閉機器時，有部分的任務會憑空消失。</p><p>記錄一下追蹤問題的過程，並奉上目前的解法，希望對未來遇到此問題的人能有些幫助。</p><blockquote><p>此文寫於 2021 年 10 月 24 日，解法可能因時序推移而不再適用，請自行斟酌。</p></blockquote><span id="more"></span><h2 id="系統設定"><a href="#系統設定" class="headerlink" title="系統設定"></a>系統設定</h2><p>我們的服務是透過 AWS Elastic Beanstalk 部屬的 Ruby on Rails 應用程式，除了處理 HTTP 流量的 Puma 之外，還配有一支背景處理服務 Sidekiq，而使用的作業系統是 Amazon Linux 2。</p><p>Elastic Beanstalk 使用的是 Load Balancing 的模式，所以系統會自動根據負載開啟或是關閉機器。</p><p>而 Puma 及 Sidekiq 在機器上都是使用 <code>systemd</code> 執行的服務。</p><h2 id="問題描述"><a href="#問題描述" class="headerlink" title="問題描述"></a>問題描述</h2><p>我們有一個每隔 10 秒會執行一次的任務，每次執行的時間根據系統狀況有所不同，5 秒至 80 秒都有可能。</p><p>為了避免塞爆 Sidekiq 的任務佇列（job queue，通常是 Redis），所以我們希望的行為模式是：執行完畢後 10 秒再執行一次，用程式碼來看會是一個這樣的任務：</p><figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">CleaningService</span></span></span><br><span class="line">  <span class="keyword">include</span> Sidekiq::Worker</span><br><span class="line">  </span><br><span class="line">  <span class="function"><span class="keyword">def</span> <span class="title">perform</span></span></span><br><span class="line">    do_some_cleaning</span><br><span class="line"></span><br><span class="line">    <span class="comment"># 完成後 10 秒再做一次</span></span><br><span class="line">    CleaningService.perform_in(<span class="number">10</span>.seconds)</span><br><span class="line">  <span class="keyword">end</span></span><br><span class="line"></span><br><span class="line">  private</span><br><span class="line"></span><br><span class="line">  <span class="function"><span class="keyword">def</span> <span class="title">do_some_cleaning</span></span></span><br><span class="line">    <span class="comment"># 有可能執行超過 30 秒</span></span><br><span class="line">  <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br></pre></td></tr></table></figure><p>Sidekiq 有 graceful shutdown 的設計：</p><ul><li>收到「停止服務」訊號時，會暫停接受新任務，並且嘗試把正在執行的任務執行完</li><li>過了一段時間，會強制把未完成的任務中止，並且把這些任務重新塞回任務佇列</li></ul><p>所以理論上來說，Sidekiq 的任務至少會被執行一次<sup id="fnref:1"><a href="#fn:1" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="[Sidekiq wikis - Best Practices](https://github.com/mperham/sidekiq/wiki/Best-Practices#2-make-your-job-idempotent-and-transactional)">1</span></a></sup>。</p><p>以上面的 <code>CleaningService</code> 來說，如果 <code>do_some_cleaning</code> 執行到一半被中止，應該會被放回任務佇列，待 Sidekiq 重新啟動時再從頭開始執行。 </p><p><strong>但實際上這個任務常常過了幾個小時就自己消失了！</strong></p><h2 id="追蹤問題來源"><a href="#追蹤問題來源" class="headerlink" title="追蹤問題來源"></a>追蹤問題來源</h2><p>我先自己推測幾個可能的原因會造成這個現象：</p><ol><li><code>CleaningService</code> 有 bug，造成 Sidekiq 崩潰</li><li>Sidekiq 的 bug 導致任務沒有被重新被放回任務佇列</li><li>Auto Scaling 關閉機器時，沒有讓 Sidekiq 執行 graceful shutdown</li></ol><p>其中 1. 在經過簡單的測試以及檢視紀錄檔（log）後，可以排除。</p><p>而 2. 實際上可以透過升級成 Sidekiq Enterprise 並使用 Redis 的 SuperFetch 來保證任務執行完後才把任務從佇列中刪除<sup id="fnref:2"><a href="#fn:2" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="[r/rails - Getting Sidekiq to play nicely with auto-scaling](https://www.reddit.com/r/rails/comments/m5pbr7/getting_sidekiq_to_play_nicely_with_autoscaling/)">2</span></a></sup>，但我猜測作者應該沒這麼壞心，故意留一個 bug 想讓大家升級到 Enterpise。此外，在手動關閉 Sidekiq 時，的確可以看到 Sidekiq 把任務重新塞回佇列。所以這個也先暫時排除。</p><p>雖然覺得很不可能，但是還是得研究 3. 會不會發生。</p><h2 id="真的是-Auto-Scaling-在搞鬼嗎？"><a href="#真的是-Auto-Scaling-在搞鬼嗎？" class="headerlink" title="真的是 Auto Scaling 在搞鬼嗎？"></a>真的是 Auto Scaling 在搞鬼嗎？</h2><p>為了瞭解 Auto Scaling 機器上的狀況，我先在機器上安插了一支 <code>systemd</code> 服務，會在關機前將紀錄檔打包，並且上傳到 S3。</p><p>紀錄檔裡的 Sidekiq 紀錄很不幸的，只有以下內容：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">...</span><br><span class="line">INFO: Shutting down</span><br></pre></td></tr></table></figure><p>原本預期應該有的「讓執行中的任務有機會完成」以及「強制把未完成的任務中止，並且把這些任務重新塞回任務佇列」都沒有被記錄到。</p><p>但可以知道的是，系統有嘗試執行正常關閉程序，但是後面發生了什麼事，不得而知。</p><h4 id="S3-沒有紀錄，那硬碟上有嗎？"><a href="#S3-沒有紀錄，那硬碟上有嗎？" class="headerlink" title="S3 沒有紀錄，那硬碟上有嗎？"></a>S3 沒有紀錄，那硬碟上有嗎？</h4><p>為了更進一步瞭解問題，我開始懷疑傳到 S3 上的資料有少，但因為機器在關閉之後，附掛在上面的硬碟也會一起被刪除，所以沒辦法直接看裡面的內容。</p><p>這裡可以透過 AWS 的 API，將機器的硬碟設定為「不隨機器關閉而刪除」<sup id="fnref:3"><a href="#fn:3" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="[AWS docs - Preserve Amazon EBS volumes on instance termination](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/terminating-instances.html#preserving-volumes-on-termination)">3</span></a></sup>，這樣一來，就可以在機器關閉之後，另外開一台新的機器，並掛載舊的硬碟<sup id="fnref:4"><a href="#fn:4" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="[AWS docs - Make an Amazon EBS volume available for use on Linux](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-using-volumes.html)">4</span></a></sup>，查看關機後舊硬碟上的資料。</p><p>在這之前，我還另外把寫入紀錄檔用的服務 <code>rsyslog</code> 設定為採用永久性儲存，避免在關機過程中，因為 <code>rsyslog</code> 因為過早被關閉，而遺失後續尚未關閉的服務產生的紀錄檔<sup id="fnref:5"><a href="#fn:5" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="[Server Fault - Wait for service to gracefully exit before machine shutdown/reboot](https://serverfault.com/a/904679)">5</span></a></sup>。</p><h4 id="硬碟上真的有！"><a href="#硬碟上真的有！" class="headerlink" title="硬碟上真的有！"></a>硬碟上真的有！</h4><p>在掛上舊硬碟之後，打開 Sidekiq 的紀錄檔，發現最後幾行寫著：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">INFO: Shutting down</span><br><span class="line">INFO: Terminating quiet workers</span><br><span class="line">INFO: Scheduler exiting...</span><br><span class="line">INFO: Pausing to allow workers to finish...</span><br><span class="line">ERROR: heartbeat: Error connecting to Redis on redis.cache.amazonaws.com:6379 (SocketError)</span><br><span class="line">WARN: Terminating 1 busy worker threads</span><br><span class="line">WARN: Work still in progress [#&lt;struct Sidekiq::BasicFetch::UnitOfWork queue=&quot;queue:default&quot;, job=&quot;&#123;\&quot;retry\&quot;:true,\&quot;queue\&quot;:\&quot;default\&quot;,\&quot;class\&quot;:\&quot;CleaningService\&quot;,\&quot;args\&quot;:[],\&quot;jid\&quot;:\&quot;707373d5bfbefe4aa077a2dc\&quot;,\&quot;created_at\&quot;:1634188450.4219317,\&quot;enqueued_at\&quot;:1634188450.422139&#125;&quot;&gt;]</span><br><span class="line">WARN: Failed to requeue 1 jobs: Error connecting to Redis on redis.cache.amazonaws.com:6379 (SocketError)</span><br><span class="line">INFO: fail</span><br><span class="line">INFO: Bye!</span><br><span class="line">ERROR: heartbeat: Error connecting to Redis on redis.cache.amazonaws.com:6379 (SocketError)</span><br><span class="line">WARN: Unable to flush stats: Error connecting to Redis on redis.cache.amazonaws.com:6379 (SocketError)</span><br></pre></td></tr></table></figure><p>可以看到系統有嘗試讓 Sidekiq 進行 graceful shutdown，不過 Sidekiq 在當時已經沒有辦法跟 Redis 連線了。主要是因為網路層的服務比 Sidekiq 更早被關閉，所以產生此現象。</p><h2 id="怎麼解決？"><a href="#怎麼解決？" class="headerlink" title="怎麼解決？"></a>怎麼解決？</h2><p>要解決這個問題最直接的方式就是修改 Sidekiq 的 <code>systemd</code> 服務描述檔，讓它會在網路層的服務關閉前先關閉。</p><p>但因為我們目前使用的是 AWS 的 Elastic Beanstalk，描述檔是由它自動產生的，如果要修改，可能會需要在每次有系統升級時，都確定沒有影響到其餘的功能，維護的成本偏高，所以暫時不考慮。</p><h3 id="EC2-Auto-Scaling-Lifecycle-Hooks"><a href="#EC2-Auto-Scaling-Lifecycle-Hooks" class="headerlink" title="EC2 Auto Scaling Lifecycle Hooks"></a>EC2 Auto Scaling Lifecycle Hooks</h3><p>除了上述方法外，其實 AWS 的 Auto Scaling 有針對這種使用情境，設計專門的機器狀態，讓開發者在機器關閉前，進行一些必要的步驟，以確保機器可以安全地被關閉<sup id="fnref:6"><a href="#fn:6" rel="footnote"><span class="hint--top hint--error hint--medium hint--rounded hint--bounce" aria-label="[AWS docs - Amazon EC2 Auto Scaling lifecycle hooks](https://docs.aws.amazon.com/autoscaling/ec2/userguide/lifecycle-hooks.html)">6</span></a></sup>。</p><p>在機器關閉前，Auto Scaling Group 會先讓機器進入 <code>Terminating:Wait</code> 的狀態，此時有<em>至多</em> 7,200 秒的時間執行必要的動作，動作完成後，由開發者觸發 API 讓機器進入 <code>Terminating:Proceed</code> 狀態，機器就會正式關閉。</p><h4 id="完整解法架構"><a href="#完整解法架構" class="headerlink" title="完整解法架構"></a>完整解法架構</h4><p>最後採用的完整的流程如下，Auto Scaling Group 根據給定規則，決定該關閉機器 (scale in)，Auto Scaling Group 將欲關閉的機器設定為 <code>Terminating:Wait</code> 狀態。</p><p>這邊需要先在 Auto Scaling Group 中建立 <code>Lifecycle hooks</code>，加入 <code>Instance Terminate</code> 的 hook，詳細的操作方式可見<a href="https://docs.aws.amazon.com/autoscaling/ec2/userguide/adding-lifecycle-hooks.html">官方文件</a>。</p><p><img src="/images/ec2-graceful-shutdown/flow-1.png" alt="Flow Diagram"></p><p>接著執行 graceful shutdown 流程。</p><p>這裡可以透過 AWS Systems Manager 搭配 AWS EventBridge 來實作。</p><p>Systems Manager 是用來管理機器的服務，其中包含：在機器上執行一段腳本、呼叫 AWS API；而 EventBridge 則是可以監聽在 AWS 服務中被觸發的事件，然後進行相關操作。</p><p>利用過 EventBridge 監聽 <code>Terminating:Wait</code> 事件，收到事件後，觸發在 Systems Manager 裡預先撰寫好自動化文件，即可進行 graceful shutdown 以及呼叫 API，最後完成關機步驟。</p><p><img src="/images/ec2-graceful-shutdown/flow-2.png" alt="Flow Diagram"></p><p>在 Systems Manager 自動化文件的最後一個步驟，觸發 AWS API 讓機器進入 <code>Terminating:Proceed</code> 狀態：</p><p><img src="/images/ec2-graceful-shutdown/flow-3.png" alt="Flow Diagram"></p><p>Auto Scaling Group 在收到請求後，正式將機器關閉：</p><p><img src="/images/ec2-graceful-shutdown/flow-4.png" alt="Flow Diagram"></p><p>詳細的 Systems Manager 自動化文件可以參考<a href="https://gist.github.com/cyhsutw/97a97da04cd1bb1a458331bf2c5c018d">這個 Gist</a>。</p><p>套用這個解法後，可以從紀錄檔中看到 Sidekiq 正確的被停止，並且把未完成的任務放回任務佇列，確確實實地解決這個問題。</p><h2 id="後記"><a href="#後記" class="headerlink" title="後記"></a>後記</h2><p>Debug 最美好的就是一切苦盡甘來的感覺，但如果可以，還是不要苦最好。感謝你讀到這裡，祝福你所有的問題，都可以<a href="https://drop.com/buy/stack-overflow-the-key-macropad">從 Stack Overflow 複製貼上</a>就解決 😌</p><div id="footnotes"><hr><div id="footnotelist"><ol style="list-style: none; padding-left: 0; margin-left: 40px"><li id="fn:1"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">1.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;"><a href="https://github.com/mperham/sidekiq/wiki/Best-Practices#2-make-your-job-idempotent-and-transactional">Sidekiq wikis - Best Practices</a><a href="#fnref:1" rev="footnote">↩</a></span></li><li id="fn:2"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">2.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;"><a href="https://www.reddit.com/r/rails/comments/m5pbr7/getting_sidekiq_to_play_nicely_with_autoscaling/">r/rails - Getting Sidekiq to play nicely with auto-scaling</a><a href="#fnref:2" rev="footnote">↩</a></span></li><li id="fn:3"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">3.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;"><a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/terminating-instances.html#preserving-volumes-on-termination">AWS docs - Preserve Amazon EBS volumes on instance termination</a><a href="#fnref:3" rev="footnote">↩</a></span></li><li id="fn:4"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">4.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;"><a href="https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-using-volumes.html">AWS docs - Make an Amazon EBS volume available for use on Linux</a><a href="#fnref:4" rev="footnote">↩</a></span></li><li id="fn:5"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">5.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;"><a href="https://serverfault.com/a/904679">Server Fault - Wait for service to gracefully exit before machine shutdown/reboot</a><a href="#fnref:5" rev="footnote">↩</a></span></li><li id="fn:6"><span style="display: inline-block; vertical-align: top; padding-right: 10px; margin-left: -40px">6.</span><span style="display: inline-block; vertical-align: top; margin-left: 10px;"><a href="https://docs.aws.amazon.com/autoscaling/ec2/userguide/lifecycle-hooks.html">AWS docs - Amazon EC2 Auto Scaling lifecycle hooks</a><a href="#fnref:6" rev="footnote">↩</a></span></li></ol></div></div>]]></content>
    
    
    <summary type="html">&lt;p&gt;最近工作上遇到一個難題，我們的背景處理系統 Sidekiq，在 AWS Auto Scaling 自動關閉機器時，有部分的任務會憑空消失。&lt;/p&gt;
&lt;p&gt;記錄一下追蹤問題的過程，並奉上目前的解法，希望對未來遇到此問題的人能有些幫助。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;此文寫於 2021 年 10 月 24 日，解法可能因時序推移而不再適用，請自行斟酌。&lt;/p&gt;
&lt;/blockquote&gt;</summary>
    
    
    
    
  </entry>
  
  <entry>
    <title>利用 SF Symbols 為你的投影片增色</title>
    <link href="https://grass-fed.engineer/2021/09/11/sf-symbols-for-presentation/"/>
    <id>https://grass-fed.engineer/2021/09/11/sf-symbols-for-presentation/</id>
    <published>2021-09-11T01:37:54.000Z</published>
    <updated>2022-12-30T15:17:08.873Z</updated>
    
    <content type="html"><![CDATA[<p>在製作演說用的投影片時，除了文字之外，很多時候會需要利用一些小圖示來承載資訊。雖然網路上可以找到相當多類似的資源，但是品質參差不齊，風格也大有逕庭，只能算是堪用。</p><p>Apple 在第一台 Mac 上市時，就在電腦裡提供眾多字體選項，當時在業界是創舉。甚至到後來的 iPhone 中內建 Emoji，也直接地幫助使用者可以產生更豐富的內容。</p><p>在 2019 年，Apple 也為軟體開發者提供了一系列扁平化的圖示，可以讓 app 開發者使用風格一致的圖示設計使用者介面。</p><p>這套圖示除了提供給 app 開發者使用的<a href="https://developer.apple.com/documentation/uikit/uiimage/3294233-init">程式介面</a>之外，也提供了給設計師使用的概覽程式：<a href="https://developer.apple.com/sf-symbols/">SF Symbols</a>。</p><p><img src="/images/sf-symbols.png" alt="SF Symbols App"></p><p>這些圖示可以匯入 Keynote 使用，為投影片注入更多視覺化元素。</p><span id="more"></span><p>除了設計風格一致之外，因為匯入的內容是實質上是<strong>文字</strong>，所以可以無限制的縮放而不失真，此外，所有可以套用在文字上的效果也都可以套用在匯入的內容上。</p><h3 id="下載、安裝"><a href="#下載、安裝" class="headerlink" title="下載、安裝"></a>下載、安裝</h3><p>相當簡單，只需要到 <a href="https://developer.apple.com/sf-symbols/">SF Symbols</a> 網站下載 SF Symbols（本文撰寫時為 SF Symbols 3 beta），下載完成後，利用下載回來的安裝檔進行安裝即可。</p><h3 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h3><p>開啟瀏覽程式 SF Symbols Beta</p><ul><li>中間圖示部分為預覽區</li><li>左側欄可以根據類別瀏覽</li><li>右上角可以進行搜尋（只能用英文）</li></ul><p><img src="/images/sf-symbols-usage.png" alt="SF Symbols App Usage"></p><p>在選定圖示後，對圖示按下右鍵 &gt; 選擇「Copy Symbol」</p><p>到 Keynote 中按下右鍵 &gt; 貼上，就可以看見圖示以文字框的形式出現。</p><p><img src="/images/sf-symbols-in-keynote.png" alt="SF Symbols in Keynote"></p><p>接著就可以根據需求修改大小、效果等等。</p><p>下圖是套用了漸層以及一些動畫，簡單模擬出的「燒腦」效果：</p><img src="/images/sf-symbols-effects.gif" style="border: solid 1px #eee; border-radius: 1.5%;"><h3 id="後記"><a href="#後記" class="headerlink" title="後記"></a>後記</h3><p>最近剛好有需要在公司分享一些內容給同事，偶然發現可以這樣使用 SF Symbols。這個小技巧節省了許多找素材的時間，同時也為投影片的質感加分不少。</p><p>可惜的是，網路上比較少資源在討論相關內容，所以決定自己撰寫一篇短文。</p><p>如果你也覺得實用，歡迎分享給你的朋友、同事，讓他們也能一起享受這些優質的設計資源。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;在製作演說用的投影片時，除了文字之外，很多時候會需要利用一些小圖示來承載資訊。雖然網路上可以找到相當多類似的資源，但是品質參差不齊，風格也大有逕庭，只能算是堪用。&lt;/p&gt;
&lt;p&gt;Apple 在第一台 Mac 上市時，就在電腦裡提供眾多字體選項，當時在業界是創舉。甚至到後來的 iPhone 中內建 Emoji，也直接地幫助使用者可以產生更豐富的內容。&lt;/p&gt;
&lt;p&gt;在 2019 年，Apple 也為軟體開發者提供了一系列扁平化的圖示，可以讓 app 開發者使用風格一致的圖示設計使用者介面。&lt;/p&gt;
&lt;p&gt;這套圖示除了提供給 app 開發者使用的&lt;a href=&quot;https://developer.apple.com/documentation/uikit/uiimage/3294233-init&quot;&gt;程式介面&lt;/a&gt;之外，也提供了給設計師使用的概覽程式：&lt;a href=&quot;https://developer.apple.com/sf-symbols/&quot;&gt;SF Symbols&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/images/sf-symbols.png&quot; alt=&quot;SF Symbols App&quot;&gt;&lt;/p&gt;
&lt;p&gt;這些圖示可以匯入 Keynote 使用，為投影片注入更多視覺化元素。&lt;/p&gt;</summary>
    
    
    
    
  </entry>
  
  <entry>
    <title>精明與精簡</title>
    <link href="https://grass-fed.engineer/2021/08/12/less-clever/"/>
    <id>https://grass-fed.engineer/2021/08/12/less-clever/</id>
    <published>2021-08-12T14:02:05.000Z</published>
    <updated>2022-12-30T15:17:08.873Z</updated>
    
    <content type="html"><![CDATA[<p>在目前的公司剛待滿四年，因為是新創公司，人員規模較小，前面的三年都是獨立開發，一直到了去年，才有另一位相同部門且負責相同產品的同事。</p><p>初來乍到，前面的幾個專案中，花了不少時間帶著他熟悉整體程式碼的結構以及一些實作細節，所以基本上都是由他撰寫程式碼，而我負責審核。</p><p>這位同事的背景跟我很不一樣，我是一路大學、研究所都唸的是資訊工程，而他是自學程式設計，正是這個差異，讓我們之間有了不少火花。</p><p>某次審核他的程式碼時，看到下面這段，腦袋中充滿了各式的疑惑。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">if (condition == false) &#123; ... &#125;</span><br></pre></td></tr></table></figure><span id="more"></span><p>「為什麼要寫 <code>== false</code>？」<br>「不是寫 <code>!condition</code> 就好了嗎？」<br>「他是不是不知道怎麼用 <code>!</code>？」</p><p>我嘗試在提出改成用 <code>!</code> 的建議，這個建議非但沒有被採納，反而得到了令相當訝異的回覆。</p><p>「用 <code>== false</code> 比較好讀懂，用 <code>!</code> 很容易漏看。」</p><h3 id="這是我進入職場之後，少數幾個具有啟發性的時刻"><a href="#這是我進入職場之後，少數幾個具有啟發性的時刻" class="headerlink" title="這是我進入職場之後，少數幾個具有啟發性的時刻"></a>這是我進入職場之後，少數幾個具有啟發性的時刻</h3><p>只有自己寫程式的時候，完全不會在意這樣的事情，自己寫、自己看，非常難看到自己的盲點。</p><p>簡單易懂的程式碼容易維護，但看起來就是不夠「精明」。許多程式設計師喜歡比拼「誰寫得短」、「誰用了比較炫的語法」等等，我曾經也是這樣的人，但這些習慣都極有可能造成程式未來不易讀懂，也就變得更難維護了。</p><p>在被點醒之後，我開始鑽研如何寫出「精簡」的程式碼，發現其實真的相當不容易。最洗練的程式碼的產生過程往往需要經過抽象化，再反向的把抽象化的結果用最簡單的方式呈現。</p><p>學習如何實踐的過程，有發現一些還不錯的資源，其中一個我覺得相當受用的<a href="https://whyarecomputers.com/3">訪談</a>，就在討論如何用最直觀的方式表達你的邏輯。</p><p>這次的學習經驗很寶貴，完美地呈現了「教學相長」，我一直以為我是在教他，但其實真正學到東西的是我。</p><p>程式設計是一條漫長且孤獨的路，每位程式設計師都是走在自己獨一無二的路上，每次的相遇，都是寶貴的學習機會，值得好好把握，共勉之。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;在目前的公司剛待滿四年，因為是新創公司，人員規模較小，前面的三年都是獨立開發，一直到了去年，才有另一位相同部門且負責相同產品的同事。&lt;/p&gt;
&lt;p&gt;初來乍到，前面的幾個專案中，花了不少時間帶著他熟悉整體程式碼的結構以及一些實作細節，所以基本上都是由他撰寫程式碼，而我負責審核。&lt;/p&gt;
&lt;p&gt;這位同事的背景跟我很不一樣，我是一路大學、研究所都唸的是資訊工程，而他是自學程式設計，正是這個差異，讓我們之間有了不少火花。&lt;/p&gt;
&lt;p&gt;某次審核他的程式碼時，看到下面這段，腦袋中充滿了各式的疑惑。&lt;/p&gt;
&lt;figure class=&quot;highlight plaintext&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;if (condition == false) &amp;#123; ... &amp;#125;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;</summary>
    
    
    
    
  </entry>
  
  <entry>
    <title>讀《寶島搜神》</title>
    <link href="https://grass-fed.engineer/2021/02/13/gods-of-taiwan/"/>
    <id>https://grass-fed.engineer/2021/02/13/gods-of-taiwan/</id>
    <published>2021-02-13T15:30:00.000Z</published>
    <updated>2022-12-30T15:17:08.869Z</updated>
    
    <content type="html"><![CDATA[<p>小時候因為家庭的關係，常常走訪許多宮廟求神問卜，有很多關於神靈的碎片知識，當時也沒想這麼多，就只是當作故事在聽。</p><p>也許是耳濡目染，對於宮廟系統幾乎是沒什麼抵抗力，到外縣市玩時會想要去當地著名的廟宇參拜，甚至就連去日本玩一定都是先查神社以及搜集朱印。</p><p>某次在 Costco 進行例行採買時剛好瞄到這本書，翻了一下覺得很喜歡，就帶回家了，但這本書比我想像中的更值得一讀。</p><span id="more"></span><p>書中整理了台灣民間信仰中較常見的神靈，佐以一些較為獨特的神靈（如器物、動物崇拜），搭配作者對於神靈形象插畫，整體內容讀來輕鬆有趣。</p><p>疫情流行時期，農曆新年走訪宮廟絕對不是明智之舉，但是透過這本書以及網路搜尋資料，也能夠進行一段段虛擬進香，也為未來疫情結束後的旅遊行程多埋下一些種子。</p><p>民俗信仰因民生需求而生，在以前靠天吃飯的年代蓬勃發展，現代生活，人們對於信仰的需求已經大不相同，有些傳統儀式未來可能也就只能在書中看到了。當然，也會有更多與時俱進的信仰及服務應運而生，不過神靈的故事將會永久流傳。</p><p>對於民俗信仰有足夠瞭解的讀者，這本絕對是值得收藏的優秀圖文書；而對於民俗信仰比較沒有涉獵的讀者，這本書也可以帶給你很多有趣的小故事，讓你更暸解祂們。</p><p><img src="/images/taiwan-gods.jpg" alt="《寶島搜神》封面"></p><hr><p>《寶島搜神》</p><p>作者：角斯<br>出版社：聯經出版公司<br>出版日期：2020 年 1 月 22 日<br>語言：繁體中文<br>ISBN：9789570854541</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;小時候因為家庭的關係，常常走訪許多宮廟求神問卜，有很多關於神靈的碎片知識，當時也沒想這麼多，就只是當作故事在聽。&lt;/p&gt;
&lt;p&gt;也許是耳濡目染，對於宮廟系統幾乎是沒什麼抵抗力，到外縣市玩時會想要去當地著名的廟宇參拜，甚至就連去日本玩一定都是先查神社以及搜集朱印。&lt;/p&gt;
&lt;p&gt;某次在 Costco 進行例行採買時剛好瞄到這本書，翻了一下覺得很喜歡，就帶回家了，但這本書比我想像中的更值得一讀。&lt;/p&gt;</summary>
    
    
    
    
  </entry>
  
  <entry>
    <title>新手開新車有什麼好處？</title>
    <link href="https://grass-fed.engineer/2021/01/25/adas-for-the-newbies/"/>
    <id>https://grass-fed.engineer/2021/01/25/adas-for-the-newbies/</id>
    <published>2021-01-25T13:03:16.000Z</published>
    <updated>2022-12-30T15:17:08.869Z</updated>
    
    <content type="html"><![CDATA[<p>新手上路，第一個需要做的決定：「要開中古車還是新車？」</p><p>我想多數人應該跟我一樣，選擇開中古車，理由不外乎就是「開老車，磕碰比較不心疼」。</p><p>所以 2019 年 12 月考到駕照後，就入手了一台跟駕訓班上課用的相同車款。</p><p>那是一台剛滿 18 歲的車，車上只有收音機，甚至還沒有倒車雷達。</p><p>結果技術太差，開了不到半個月，就在社區停車場擦撞了鄰居的車 — 好不容易累積的一絲信心直接破滅。</p><blockquote><p>「是不是可以讓車子來教我怎麼駕駛？」</p></blockquote><p>我自顧自地覺得這是一個可行的學習方式。</p><p>趁著春節假期做了一些功課，在半個月後，決定購入輔助駕駛功能比較齊全的車款，讓它來教我開車。</p><span id="more"></span><h3 id="這真是一個正確的決定"><a href="#這真是一個正確的決定" class="headerlink" title="這真是一個正確的決定"></a>這真是一個正確的決定</h3><p>舉幾個我感受比較強烈的例子：</p><h4 id="停車"><a href="#停車" class="headerlink" title="停車"></a>停車</h4><p>透過雷達可以比較暸解跟附近障礙物的距離。如果雷達警告靠太近了，可以比對後視鏡跟車外周遭肉眼可見的樣貌，思考如何調整，這樣下次就可以用比較適當的方式停好車。</p><h4 id="抓車距"><a href="#抓車距" class="headerlink" title="抓車距"></a>抓車距</h4><p>使用自動跟車，可以觀摩電腦如何與前車保持適當距離，以及加速減速時機的掌控。</p><p>另外自動變道也讓我在切換車道時更有餘裕去觀察如何與後車保持適當距離，減少對於其他駕駛者的影響。</p><h4 id="控制方向盤"><a href="#控制方向盤" class="headerlink" title="控制方向盤"></a>控制方向盤</h4><p>車道維持則示範了如何把車開在車道中央，在有點弧度的路面上如何控制左右空間，不壓縮到鄰車的行駛空間。</p><h3 id="學習的成果是帶得走的！"><a href="#學習的成果是帶得走的！" class="headerlink" title="學習的成果是帶得走的！"></a>學習的成果是帶得走的！</h3><p>經過這樣「被示範」大約三個月，在公路上行駛時的穩定性有很顯著的提升。</p><p>比較有趣的是，原本以為會因此而依賴輔助駕駛，要完全自己開時會不知所措，但反而偶爾回頭去開老車時，也明顯地感覺到可以靠自己把車開好。</p><h3 id="應該還可以再進步"><a href="#應該還可以再進步" class="headerlink" title="應該還可以再進步"></a>應該還可以再進步</h3><p>開了三個月之後，幸運抽到<a href="https://www.driveaholic.com.tw/academy/">統哥駕駛學院</a>初級駕駛課程，課程內容讓我對於輔助駕駛的體會又更深了一些，之後有時間再另外寫一篇聊聊。</p><blockquote><p>哦，對了，那輛教我開車的車是這台</p></blockquote><p><img src="/images/tesla-m3.jpeg" alt="Tesla Model 3"></p><p>如果你也想體驗看看，歡迎到特斯拉官方網站<a href="https://www.tesla.com/zh_TW/drive">預約試駕</a>。</p><p>訂車時也歡迎使用我的推薦連結訂購 <a href="https://ts.la/cyhsum72569">https://ts.la/cyhsum72569</a>，這樣你可以得到 1,500 公里免費的超級充電額度。</p>]]></content>
    
    
    <summary type="html">&lt;p&gt;新手上路，第一個需要做的決定：「要開中古車還是新車？」&lt;/p&gt;
&lt;p&gt;我想多數人應該跟我一樣，選擇開中古車，理由不外乎就是「開老車，磕碰比較不心疼」。&lt;/p&gt;
&lt;p&gt;所以 2019 年 12 月考到駕照後，就入手了一台跟駕訓班上課用的相同車款。&lt;/p&gt;
&lt;p&gt;那是一台剛滿 18 歲的車，車上只有收音機，甚至還沒有倒車雷達。&lt;/p&gt;
&lt;p&gt;結果技術太差，開了不到半個月，就在社區停車場擦撞了鄰居的車 — 好不容易累積的一絲信心直接破滅。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;「是不是可以讓車子來教我怎麼駕駛？」&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;我自顧自地覺得這是一個可行的學習方式。&lt;/p&gt;
&lt;p&gt;趁著春節假期做了一些功課，在半個月後，決定購入輔助駕駛功能比較齊全的車款，讓它來教我開車。&lt;/p&gt;</summary>
    
    
    
    
  </entry>
  
</feed>
