<?xml version='1.0' encoding='utf-8'?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>https://blog.0xian.dev/</id>
  <title><![CDATA[Ian's Coding Abyss]]></title>
  <subtitle><![CDATA[碼農的補坑筆記]]></subtitle>
  <icon>https://blog.0xian.dev/favicon.png</icon>
  <link href="https://blog.0xian.dev" />
  <link href="https://blog.0xian.dev/atom.xml" rel="self" type="application/atom+xml" />
  <updated>2026-04-10T06:28:17.646Z</updated>
  <author>
    <name><![CDATA[Ian]]></name>
  </author>
  <category term="Skills Competition" scheme="https://blog.0xian.dev/?tags=Skills%20Competition" />
  <category term="Cloud" scheme="https://blog.0xian.dev/?tags=Cloud" />
  <category term="AWS" scheme="https://blog.0xian.dev/?tags=AWS" />
  <category term="55th Cloud Computing" scheme="https://blog.0xian.dev/?tags=55th%20Cloud%20Computing" />
  <category term="Cloudflare" scheme="https://blog.0xian.dev/?tags=Cloudflare" />
  <category term="Email" scheme="https://blog.0xian.dev/?tags=Email" />
  <category term="Serverless" scheme="https://blog.0xian.dev/?tags=Serverless" />
  <category term="Web" scheme="https://blog.0xian.dev/?tags=Web" />
  <category term="Reverse Proxy" scheme="https://blog.0xian.dev/?tags=Reverse%20Proxy" />
  <category term="Access Control" scheme="https://blog.0xian.dev/?tags=Access%20Control" />
  <category term="Free" scheme="https://blog.0xian.dev/?tags=Free" />
  <entry>
    <title type="html"><![CDATA[55屆全國技能競賽雲端運算 賽後隨筆 - Part 5]]></title>
    <link href="https://blog.0xian.dev/posts/55rd-national-skills-competition-cloud-computing-review-part5" />
    <id>https://blog.0xian.dev/posts/55rd-national-skills-competition-cloud-computing-review-part5</id>
    <published>2025-07-25T00:00:00.000Z</published>
    <updated>2025-07-25T00:00:00.000Z</updated>
    <summary type="html"><![CDATA[📹👀]]></summary>
    <content type="html">
      <![CDATA[<!--[--><!--[--><!--[--><!--[--><p>早安，這是「<abbr title="長到靠北，以下簡稱&quot;全國賽&quot;">第 55 屆全國技能競賽英雄榜暨第3屆亞洲技能競賽及第 48 屆國際技能競賽國手選拔賽青年組雲端運算職類</abbr>」系列文章的第五篇，這次來分享科目五的題目和解題過程。轉眼間就寫到最後一篇了，等九月的二階國手選拔結束後再來繼續寫幾篇吧。</p> <p><!--[--><picture><source srcset="/_app/immutable/assets/IMG_3433.l2G1uczH.avif 736w" type="image/avif"/> <img src="/posts/55rd-national-skills-competition-cloud-computing-review-part5/images/IMG_3433.jpg" alt="在雷門吃草莓可麗餅的カズサ" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <h2 id="科目五-一堆監控"><a href="#科目五-一堆監控">科目五: 一堆監控</a></h2> <p>這題都圍繞在 CloudWatch 的各項功能上，從 Metrics、Logs、Application Signals (APM) 到 Network Monitoring，可以說是把 CloudWatch 的功能都用上了。</p> <p>題目提供了一個 CloudFront + S3 + API Gateway + Lambda 的架構，並要求使用者去監控這個架構的各項指標。</p> <p>這題必須要先解第一大題才會開啟剩下的題目，因為整題都圍繞在 CloudWatch，且我沒有照順序做也沒做完，所以只列出大致的方向：</p> <ul><li>Lambda Code 改 metric unit</li> <li>Metric filter 擷取特定 log 並轉換為 metric</li> <li>Synthetics Canaries 監控 API 的可用性</li> <li>建立 Dashboard 來顯示各項指標</li> <li>Metric anomaly detection 異常偵測</li> <li>Internet monitors 監控各國連線狀態</li> <li>RUM 監控實際使用者行為</li> <li>從 JSON 匯入 Dashboard</li> <li>Dashboard 新增 RSS reader</li> <li>WAF 封鎖 VPN 流量</li> <li>從 WAF logs 提取欄位</li> <li>Logs Insights 查詢並產生指定報表</li></ul> <h3 id="lambda-code-改-metric-unit"><a href="#lambda-code-改-metric-unit">Lambda Code 改 metric unit</a></h3> <p>題目提到某 API 端點產生的 metrics unit 為 <code>Percentage</code>，需要將其改為 <code>Count</code>。</p> <p>到 CloudFront 可以得知 <code>/api</code> 的 origin 是 API Gateway，進入 API Gateway 後找到對應的 endpoint 即可找到目標 Lambda。進入 Lambda 後可以很明顯地看到 <code>unit</code> 設為 <code>Percentage</code>，將其改為 <code>Count</code> 並 Deploy 即可。</p> <!----><pre class="shiki material-default" python="true"><div class="language-id">python</div><div class='code-container'><code><div class='line dim'>def lambda_handler(event, context):</div><div class='line dim'>    # ...</div><div class='line dim'>    metric_data = &#123;</div><div class='line dim'>        'MetricName': 'APIRequests',</div><div class='line highlight'>        'Unit': 'Percentage',  # 改為 Count</div><div class='line dim'>        'Value': 1,</div><div class='line dim'>        # ...</div><div class='line dim'>    &#125;</div><div class='line dim'>    # ...</div></code></div></pre><!----> <h3 id="metric-filter-擷取特定-log-並轉換為-metric"><a href="#metric-filter-擷取特定-log-並轉換為-metric">Metric filter 擷取特定 log 並轉換為 metric</a></h3> <p>題目要求將 <code>/api/donate</code> 的 payload 提取 <code>amount</code> 出來，並轉換為 metric。</p> <p>透過剛剛的方式找到目標 Lambda，進入後可以看到程式碼內並沒有用到 metric，且我們也沒有修改程式碼的權限，但程式碼內有 logging 且內容為我們要的 <code>amount</code>。因此我們可以透過 CloudWatch Logs 的 Metric filter 來擷取這些訊息。</p> <!----><pre class="shiki material-default" python="true"><div class="language-id">python</div><div class='code-container'><code><div class='line'>def lambda_handler(event, context):</div><div class='line'>    # ...</div><div class='line'>    payload = event['body']</div><div class='line'>    logger.info(&#123;</div><div class='line'>      'event': 'donate',</div><div class='line'>      'amount': payload['amount'],</div><div class='line'>    &#125;)</div><div class='line'>    # ...</div></code></div></pre><!----> <p>在 CloudWatch Logs 找到對應的 Lambda log group，Actions -> Create metric filter，然後輸入以下 pattern：</p> <!----><pre class="shiki material-default" json="true"><div class="language-id">json</div><div class='code-container'><code><div class='line'>&#123; $.event = "donate" && $.amount = * &#125;</div></code></div></pre><!----> <p>接著在下一步中設定 namespace 為題目指定的 <code>UnicornFarm</code>，metric name 為 <code>Donations</code>，metric value 為 <code>$.amount</code>，unit 為 <code>Count</code> 即可。</p> <h3 id="synthetics-canaries-監控-api-的可用性"><a href="#synthetics-canaries-監控-api-的可用性">Synthetics Canaries 監控 API 的可用性</a></h3> <p>這題要透過 Synthetics Canaries 來監控 API 的可用性。功能跟 uptime robot 類似，就是定期發 request 去戳一下看正不正常。</p> <p>這題因為題目講得不清不楚，我直接跑去設 UAM 想說怎麼沒得分，花了 20 分鐘把 CloudWatch 都摸過才知道原來是要用它…</p> <p>在 CloudWatch Synthetics 中建立一個新的 Canary，Blueprints 選擇 <code>API Canary</code>，然後新增 HTTP request:</p> <!----><pre class="shiki material-default" yaml="true"><div class="language-id">yaml</div><div class='code-container'><code><div class='line'>- Method: POST</div><div class='line'>- Application or endpoint URL: https://xxx.cloudfront.net/api/donate</div><div class='line'>- Headers:</div><div class='line'>  - Content-Type: application/json</div><div class='line'>- Request data:</div><div class='line'>  &#123; "amount": 0 &#125;</div></code></div></pre><!----> <p>需要留意題目提到的 API 為走 CloudFront 的，所以不能勾選 “I’m using an Amazon API Gateway API”。</p> <p>接下來設定 Schedule 為每分鐘執行一次，然後 Deploy 即可。</p> <p>賽後跟其他選手討論發現蠻多人設定完也沒有得分被卡著不能繼續，好像一定要用 API canary 的 blueprints 才會過。</p> <h3 id="建立-dashboard-來顯示各項指標"><a href="#建立-dashboard-來顯示各項指標">建立 Dashboard 來顯示各項指標</a></h3> <p>題目要求建立一個 Dashboard 來顯示各項指標。</p> <p>在 CloudWatch 中建立一個新的 Dashboard，然後把剛剛建立的 metric 和 canary 加入到 Dashboard 中即可。</p> <h3 id="metric-anomaly-detection-異常偵測"><a href="#metric-anomaly-detection-異常偵測">Metric anomaly detection 異常偵測</a></h3> <p>題目要求對 <code>Donations</code> metric 建立異常偵測。當每分鐘的數值過小時發出 SNS 通知。</p> <p>SNS 已經幫你建好了，只需要在 CloudWatch 中對 <code>Donations</code> metric 建立 alarm，threshold type 設為 <code>Anomaly detection</code>，然後選擇 <code>Lower than the band</code>，並設定 SNS topic 為題目提供的 <code>UnicornFarmSNS</code> 即可。</p> <h3 id="internet-monitors-監控各國連線狀態"><a href="#internet-monitors-監控各國連線狀態">Internet monitors 監控各國連線狀態</a></h3> <p>題目要求建立 Internet monitors 來監控各國連線狀態。</p> <p>在 CloudWatch 中建立一個新的 Internet monitor，在 Resources to monitor 中選擇自己的 CloudFront distribution 並建立即可。</p> <h3 id="rum-監控實際使用者行為"><a href="#rum-監控實際使用者行為">RUM 監控實際使用者行為</a></h3> <p>題目要求使用 RUM 來監控實際使用者行為，功能與 Google Analytics、Cloudflare Web Analytics 類似。</p> <p>在 CloudWatch 中建立一個新的 RUM app monitor，指定 Application domain list 為自己的 CloudFront domain，題目暗示到”所有需要使用到的驗證都已幫你設定好”，所以在 Authorization 需要選擇環境中唯一的 Identity Pool，其餘參數保持預設即可。</p> <p>建立完成後會提供一段 JavaScript 程式碼，正常步驟應該自己更新 S3 上的 HTML，但只要提交建立的 RUM name 題目會自己幫你更新，真是貼心。</p> <h3 id="從-json-匯入-dashboard"><a href="#從-json-匯入-dashboard">從 JSON 匯入 Dashboard</a></h3> <p>題目提供了一個 JSON 檔案，要求從中匯入 Dashboard。</p> <p>需要留意的是 JSON 檔裡面有很多 <code>[placeholder-your-xxx]</code> 的部分，需要根據前幾題的設定來替換成自己的值，若直接上傳會跳第 n 個 widget 找不到資源的錯誤。改好之後在 CloudWatch 中建立一個新的 Dashboard，然後選擇 Import dashboard，將 JSON 檔案上傳即可。</p> <h3 id="dashboard-新增-rss-reader"><a href="#dashboard-新增-rss-reader">Dashboard 新增 RSS reader</a></h3> <p>題目要求在 Dashboard 中新增一個 RSS reader，顯示指定的 RSS feed。</p> <p>在新增 widget 的 Other content types > Custom widget 中可以選擇內建的 RSS reader，會透過 CloudFormation 替你建立 Lambda。但由於往年題目都要求不能使用 CloudFormation，雖然今年沒說(?而且昨天有用到 SAM 但我猶豫了，所以這題沒有做。我相信應該是用這方法做沒錯?</p> <h3 id="waf-封鎖-vpn-流量"><a href="#waf-封鎖-vpn-流量">WAF 封鎖 VPN 流量</a></h3> <p>題目要求透過 WAF 封鎖 VPN 流量。</p> <p>在 WAF 中建立一個新的 Web ACL，然後新增一個規則，選擇 <code>AWS Managed rule groups</code>，其中有封鎖 VPN 的規則可以使用，勾選後套用到 CloudFront distribution 即可。</p> <p>沒錢不敢在自已帳號開 WAF，有寫錯歡迎告知。</p> <h3 id="從-waf-logs-提取欄位"><a href="#從-waf-logs-提取欄位">從 WAF logs 提取欄位</a></h3> <p>題目要求從 WAF logs 中提取每個請求的 host 與 uri，並另存為新的欄位。</p> <p>在 Web ACL 內的 logging and metrics 中啟用 logging，將 log 儲存到指定的 log group。然後到 Log Insights 中，選擇剛剛的 log group，使用以下查詢語句來提取欄位：</p> <!----><pre class="shiki material-default" sql="true"><div class="language-id">sql</div><div class='code-container'><code><div class='line'>fields @timestamp, @message</div><div class='line'>| parse @message ""host": "*"" as host_url</div><div class='line'>| parse @message ""uri": "*"" as host_uri</div></code></div></pre><!----> <p>即可在結果中看到 <code>host_url</code> 與 <code>host_uri</code> 兩個欄位，將 Query 儲存並提交即可。</p> <h3 id="logs-insights-查詢並產生指定報表"><a href="#logs-insights-查詢並產生指定報表">Logs Insights 查詢並產生指定報表</a></h3> <p>題目要求使用 Logs Insights 查詢並產生各 API endpoint 的請求數量報表。</p> <p>這步驟的前提是須要到 API Gateway 的 stage 中啟用 CloudWatch Logs，將請求日誌記錄到 CloudWatch Logs 中。</p> <p>在 Log Insights 中選擇對應的 log group，使用以下查詢語句來統計各 endpoint 的請求數量：</p> <!----><pre class="shiki material-default" sql="true"><div class="language-id">sql</div><div class='code-container'><code><div class='line'>fields @timestamp, @message</div><div class='line'>| parse @message ""uri": "*"" as @path</div><div class='line'>| stats count() as @request_count by @path</div><div class='line'>| sort @request_count desc</div></code></div></pre><!----> <p>這樣就可以得到各 API endpoint 的請求數量，一樣將 Query 儲存並提交即可。</p> <h3 id="科目心得"><a href="#科目心得">科目心得</a></h3> <p>這次題目的難度沒記錯是從 300 ~ 400，看記分板發現有做到第二大題的人大概只有 5 位，可見大家都被 Canary 卡住。我到最後還有約 10% 的題目沒做完，東西真的太多了，只有不到三小時一個人怎麼做的完啊。但我又學到了很多東西，像是我更懂 metric 跟 Log Insights 語法了!</p> <h2 id="競賽結語"><a href="#競賽結語">競賽結語</a></h2> <p>為期五天(扣掉開幕閉幕只有三天)的競賽，說長不長、說短不短，卻是一段充實無比的旅程。</p> <p>兩年前我第一次參加技能競賽(53屆)，當時的我接觸雲端還不到一年，雖然還是雲端小白但靠著多年來的實作經驗以及對資訊科技的熱愛，第一次參賽就拿到中區第四與全國第三，頒獎時自己也是嚇了一跳，雖然晉級二階國手選拔，但能力還是敵不過其他選手，與法國里昂的國際賽無緣。</p> <p>兩年後，這次的我(55屆)在比完賽後才發現自己又變強了，總共五個科目都拿到第一，雖說大概有 70% 我沒接觸過的服務，但我還是靠著對資訊領域的知識來推測邏輯並快速釐清方向，也在幾個科目中拿到首殺(第一個得分，但不會因此加分)。</p> <p>這次的競賽看到有些人在質疑我的成績，<del>畢竟五個科目都第一難免會被針對</del>。但我想說的是，雲端運算不單只是比誰會用雲端平台，而是比誰能在有限的時間內快速理解題目、找到符合真實世界的解決方案並實作出來。</p> <p>雲端競賽考的是企業會遇到的真實問題，而不是課本上的標準答案。在這科技日新月異、AI 技術週週更新的時代，只有不斷學習、快速適應新技術的人才能在競爭中存活，不被世界<abbr title="丟掉">蛋雕</abbr>。</p> <p>這一路走來常常都是自己一個人在學習與摸索<span class="spoiler">(母胎單身😭)</span>。高中暑假的某一天，突然腦袋撞到、異想天開地用手機學寫 Python 聊天機器人<span class="spoiler">(對，就是這麼白癡)</span>，從此踏上了我不斷自學、撞牆、把牆撞破又遇到下一道牆的獨自升級之路。</p> <p>感謝這幾天陪我討論跟吃晚餐的夥伴們，以及前幾個月一起參加訓練營的朋友們，讓我們一起加油，繼續在資訊科技的道路上前進。</p><!--]--><!--]--><!--]--><!--]-->]]>
    </content>
    <category term="Skills Competition" scheme="https://blog.0xian.dev/?tags=Skills%20Competition" />
    <category term="Cloud" scheme="https://blog.0xian.dev/?tags=Cloud" />
    <category term="AWS" scheme="https://blog.0xian.dev/?tags=AWS" />
    <category term="55th Cloud Computing" scheme="https://blog.0xian.dev/?tags=55th%20Cloud%20Computing" />
  </entry>
  <entry>
    <title type="html"><![CDATA[55屆全國技能競賽雲端運算 賽後隨筆 - Part 4]]></title>
    <link href="https://blog.0xian.dev/posts/55rd-national-skills-competition-cloud-computing-review-part4" />
    <id>https://blog.0xian.dev/posts/55rd-national-skills-competition-cloud-computing-review-part4</id>
    <published>2025-07-24T00:00:00.000Z</published>
    <updated>2025-07-31T00:00:00.000Z</updated>
    <summary type="html"><![CDATA[🤯🤯]]></summary>
    <content type="html">
      <![CDATA[<!--[--><!--[--><!--[--><!--[--><p>早安，這是「<abbr title="長到靠北，以下簡稱&quot;全國賽&quot;">第 55 屆全國技能競賽英雄榜暨第3屆亞洲技能競賽及第 48 屆國際技能競賽國手選拔賽青年組雲端運算職類</abbr>」系列文章的第四篇，這次來分享科目四的題目和解題過程。</p> <p><!--[--><picture><source srcset="/_app/immutable/assets/IMG_4203.BN8NrQEL.avif 736w" type="image/avif"/> <img src="/posts/55rd-national-skills-competition-cloud-computing-review-part4/images/IMG_4203.jpg" alt="カズサ跟光比" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <h2 id="科目四-大雜燴"><a href="#科目四-大雜燴">科目四: 大雜燴</a></h2> <p>共有六大題，內容非常的廣，從 ECS 到 IoT，資料分析到資料串流，應有盡有。題目大致如下:</p> <ul><li>給一 ECR，把它用 ECS 跑起來</li> <li>把 AWS 的 IoT services 都用過一遍</li> <li>EMR studio 分析 S3 資料</li> <li>Kinesis Data Stream 串來串去</li> <li>CodePipeline 增加 CodeDeploy 部署</li> <li>SageMaker AI 模型訓練 (感謝 <a href="https://github.com/garyellow" rel="nofollow noopener noreferrer external" target="_blank">@garyellow</a> 補充)</li></ul> <p>這場比完細節大概就快忘光了，歡迎其他選手留言補充，我會再補上 🙇。</p> <h3 id="ecr--ecs"><a href="#ecr--ecs">ECR + ECS</a></h3> <p>這題給了一個放在 ECR 的 image，然後要把它用 ECS 跑起來。</p> <p>花了點時間在看它跑在哪個 port 上，我是直接跑起來看 log 後才知道 port 是 8080，賽後交流才得知原始程式碼有放在 CodeCommit 上。</p> <p>要做的事就只有建一個 ECS cluster，定義 task，然後跑起來即可。</p> <h3 id="iot-services"><a href="#iot-services">IoT Services</a></h3> <p>這題用到了 IoT Core、IoT Greengrass、IoT Analytics、IoT SiteWise、IoT Events、IoT Device Management，對於沒用過的人(我)來說超級複雜，幸好之前碰過 Arduino 跟 ESP32，對於 IoT 的概念還算熟悉，難不倒我。</p> <p>沒有很想開 IoT services 的服務來做實驗加上忘得差不多，這邊就不寫的太詳細了。</p> <h4 id="iot-core-mqtt-client"><a href="#iot-core-mqtt-client">IoT Core MQTT client</a></h4> <p>這題很多步驟都圍繞著 IoT Core 的 MQTT client，必須使用它來收發訊息。</p> <p>透過 AWS IoT Core 的 console 來開啟 MQTT client，然後訂閱一個 topic，接著就可以透過 publish 來發送訊息。</p> <p>基本上都是題目叫你幹嘛就幹嘛。</p> <h4 id="iot-sitewise-修正"><a href="#iot-sitewise-修正">IoT SiteWise 修正</a></h4> <p>這題提供的 SiteWise 有個設備的 data stream 的資料來源沒設定好，需要參考其他設備的設定來補上。</p> <p>後續設定完成後要把四個設備的資料(溫度)在 dashboard 上放在同一圖表上做呈現。進編輯然後把四個設備的資料都拖進去即可。</p> <h4 id="iot-greengrass-deploy-component"><a href="#iot-greengrass-deploy-component">IoT Greengrass deploy component</a></h4> <p>根據提供的 json 檔，建立一個 Greengrass component，然後 deploy 到 Greengrass core device。</p> <p>首先先建立 component，設定完成後直接到 core devices 把它 deploy 上去即可。</p> <h4 id="iot-device-management-更換憑證"><a href="#iot-device-management-更換憑證">IoT Device Management 更換憑證</a></h4> <p>這題要把 IoT Device 的憑證 deactivate，然後再 activate 一個新的憑證。</p> <p>題目提供了一個 Certificate Signing Request(CSR)，在 IoT Device Management 內建立憑證的某處可以選擇透過 CSR 來建立憑證，建立完然後 activate 即可。</p> <h3 id="emr-studio-分析-s3-資料"><a href="#emr-studio-分析-s3-資料">EMR Studio 分析 S3 資料</a></h3> <p>這題給了一個 S3 bucket，裡面有一堆我看不懂的資料，然後要用 EMR Studio 來分析。目標為找出資料內的 <code>inner</code>、<code>mana</code> 與 <code>outer</code> 的關聯性，看了半小時還是看不懂這三個東西是什麼鬼，幸好直接跑程式就可以拿到答案。</p> <p>題目提供了一個 EMR studio 可以直接跑 spark job，也提供了對應的 Python 程式碼。透過 EMR studio 建立 spark job，指定參數為 <code>--inner xxx --outer yyy</code>，然後跑起來即可。</p> <p>這題做太慢會被扣分，後續還會要求接 S3 event 來自動化，但不知道是我做太慢還是怎樣，有好多檔案跑完沒得到分數。</p> <h3 id="kinesis-data-stream-串來串去"><a href="#kinesis-data-stream-串來串去">Kinesis Data Stream 串來串去</a></h3> <p>這題要求建立一個 Kinesis Data Stream，提供名稱後會開始打資料到這個 stream。</p> <p>收到 stream 後會要求將它丟到 Apache Flink 做分析，notebook 也有提供，就丟上去直接跑即可。</p> <p>後面好像還有 Data Firehose 做 transform，但內容忘了，只記得被 Gameday 上的獨角獸說我做太慢。</p> <h3 id="codepipeline-增加-codedeploy-部署"><a href="#codepipeline-增加-codedeploy-部署">CodePipeline 增加 CodeDeploy 部署</a></h3> <p>這題要求在 CodePipeline 上增加一個 CodeDeploy 的部署步驟。</p> <p>前天就碰過 CodePipeline 了，沒什麼難度，建一個 CodeDeploy 設定部署到 auto scaling group，然後到 CodePipeline 增加一個 stage，把 CodeDeploy 加進去即可。</p> <h3 id="sagemaker-ai-模型訓練"><a href="#sagemaker-ai-模型訓練">SageMaker AI 模型訓練</a></h3> <p><del>這題我因為點開 JupyterLab 後沒看到 code 以為是 bug 就先跳去做其他題了，所以沒印象。</del></p> <p>感謝 <a href="https://github.com/garyellow" rel="nofollow noopener noreferrer external" target="_blank">@garyellow</a> 的補充，這邊引用他的流言:</p> <blockquote><ol><li>打開預先建立好的 JupyterLab (在 Amazon SageMaker AI 中)</li> <li>將專案從 Gitea clone 到 JupyterLab 中</li> <li>依說明修改專案中 jupyter notebook 檔案裡的 3 個地方</li> <li>改完執行全部程式碼，然後等模型練好</li> <li>把練好的模型部署到 Endpoint (用 Amazon SageMaker Studio)</li> <li>回答有幾顆星星 (應該是用部署的模型去得出，這邊我沒弄出來)</li></ol></blockquote> <h3 id="科目心得"><a href="#科目心得">科目心得</a></h3> <p>這題的難度比前面幾題高很多，沒記錯難度都是 300，因為涉及的服務非常多，對於沒碰過相關服務與領域的人來說有點難度。</p> <p>在碰 IoT services 的過程感覺 AWS 這方面其實做的蠻好的，只是我沒錢沒機會用到而已。</p><!--]--><!--]--><!--]--><!--]-->]]>
    </content>
    <category term="Skills Competition" scheme="https://blog.0xian.dev/?tags=Skills%20Competition" />
    <category term="Cloud" scheme="https://blog.0xian.dev/?tags=Cloud" />
    <category term="AWS" scheme="https://blog.0xian.dev/?tags=AWS" />
    <category term="55th Cloud Computing" scheme="https://blog.0xian.dev/?tags=55th%20Cloud%20Computing" />
  </entry>
  <entry>
    <title type="html"><![CDATA[55屆全國技能競賽雲端運算 賽後隨筆 - Part 3]]></title>
    <link href="https://blog.0xian.dev/posts/55rd-national-skills-competition-cloud-computing-review-part3" />
    <id>https://blog.0xian.dev/posts/55rd-national-skills-competition-cloud-computing-review-part3</id>
    <published>2025-07-23T00:00:00.000Z</published>
    <updated>2025-07-23T00:00:00.000Z</updated>
    <summary type="html"><![CDATA[🦄🚜]]></summary>
    <content type="html">
      <![CDATA[<!--[--><!--[--><!--[--><!--[--><p>早安，這是「<abbr title="長到靠北，以下簡稱&quot;全國賽&quot;">第 55 屆全國技能競賽英雄榜暨第3屆亞洲技能競賽及第 48 屆國際技能競賽國手選拔賽青年組雲端運算職類</abbr>」系列文章的第三篇，這次來分享科目三的題目和解題過程。</p> <p><!--[--><picture><source srcset="/_app/immutable/assets/Screenshot_20250722_185604.C_PjFk0i.avif 736w" type="image/avif"/> <img src="/posts/55rd-national-skills-competition-cloud-computing-review-part3/images/Screenshot_20250722_185604.png" alt="昨天開專4的カズサ" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <h2 id="科目三-unicorn-farm"><a href="#科目三-unicorn-farm">科目三: Unicorn Farm</a></h2> <p>共有兩大題，考驗的是懂 serverless 與 no-code 的概念與實作能力，題目如下:</p> <ul><li>無程式碼搬遷<ul><li>給一既有 Lambda + DynamoDB 架構，透過 Step Functions 來取代 Lambda</li></ul></li> <li>Serverless Application Model 部屬<ul><li>給一 Python 程式碼，透過 Serverless Application Model(SAM) 來部署</li></ul></li></ul> <h3 id="無程式碼搬遷"><a href="#無程式碼搬遷">無程式碼搬遷</a></h3> <p>這題給了兩個 Lambda 與兩個 DynamoDB table，要求透過 Step Functions 來取代 Lambda 的功能。</p> <p><!--[--><picture><source srcset="/_app/immutable/assets/lambda-arch.DNRwJ1lW.avif 736w" type="image/avif"/> <img src="/posts/55rd-national-skills-competition-cloud-computing-review-part3/images/lambda-arch.png" alt="Architecture" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <h4 id="看懂既有架構"><a href="#看懂既有架構">看懂既有架構</a></h4> <p>先看下方負責 feed unicorns 的 Lambda 程式碼，我們可以得知它會吃到一個 event，裡面包含了多個 unicorn 的 ID 以及 feed_amount，然後去 DynamoDB 更新每個 unicorn 對應的數值與 last_feed。</p> <!----><pre class="shiki material-default" json="true"><div class="language-id">json</div><div class='code-container'><code><div class='line'>&#123;</div><div class='line'>  "Unicorns": [</div><div class='line'>    &#123;</div><div class='line'>      "id": "unicorn-1",</div><div class='line'>      "amount": 10</div><div class='line'>    &#125;,</div><div class='line'>    &#123;</div><div class='line'>      "id": "unicorn-2",</div><div class='line'>      "amount": 20</div><div class='line'>    &#125;</div><div class='line'>  ],</div><div class='line'>  "TotalFeedAmount": 30,</div><div class='line'>  "TableName": "unicorn_status",</div><div class='line'>  "FeedTableName": "feed_amount"</div><div class='line'>&#125;</div></code></div></pre><!----> <div class="overflow-x-auto mb-4"><table class="table w-full"><!--[--><thead><tr><th>id</th><th>name</th><th>hunger</th><th>last_feed</th></tr></thead> <tbody><tr><td>unicorn-1</td><td>Twilight Sparkle</td><td>50</td><td>2025-07-22T10:00:00Z</td></tr><tr><td>unicorn-2</td><td>Fluttershy</td><td>30</td><td>2025-07-22T10:05:00Z</td></tr></tbody><!--]--></table></div><!----> <p>上方的 Lambda 只會檢查 feed amount 是否小於 50，若是的話則補到 300。</p> <div class="overflow-x-auto mb-4"><table class="table w-full"><!--[--><thead><tr><th>id</th><th>amount</th></tr></thead> <tbody><tr><td>feed_amount</td><td>45 -> 300</td></tr></tbody><!--]--></table></div><!----> <h4 id="設計-step-functions"><a href="#設計-step-functions">設計 Step Functions</a></h4> <p>對於第一次接觸 Step Functions 的我來說，第一眼看到時一臉懵逼，我要怎麼把 event 讀出來? Array 要怎麼分別處理?</p> <p>幸好雲端運算職類可以看官方文檔，需要用到 Parallel 與 Map 兩個狀態，邏輯如下圖所示:</p> <p><!--[--><picture><source srcset="/_app/immutable/assets/step-func-1.BdzlBGUF.avif 736w" type="image/avif"/> <img src="/posts/55rd-national-skills-competition-cloud-computing-review-part3/images/step-func-1.png" alt="Step Functions 1" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <p>Step Functions Code: <a href="/posts/55rd-national-skills-competition-cloud-computing-review-part3/files/step-func-1.json">step-func-1.json</a></p> <p>對於 Map state，我們需要設定 Provide items 為 <code>{% $states.input.Unicorns %}</code>，這樣才會將每個元素傳入其中。</p> <p>兩個更新 DynamoDB 的 PutItem 則需修改 Arguments，需要留意 DynamoDB 對於 Number 欄位的 API 呼叫需要傳遞字串，而不是數字。</p> <!----><pre class="shiki material-default" json="true"><div class="language-id">json</div><div class='code-container'><code><div class='line dim'>&#123;</div><div class='line dim'>  "TableName": "unicorn_status",</div><div class='line dim'>  "Key": &#123;</div><div class='line dim'>    "id": &#123;</div><div class='line dim'>      "S": "&#123;% $states.input.id %&#125;"</div><div class='line dim'>    &#125;</div><div class='line dim'>  &#125;,</div><div class='line dim'>  "UpdateExpression": "SET hunger = hunger + :feedRef",</div><div class='line dim'>  "ExpressionAttributeValues": &#123;</div><div class='line dim'>    ":feedRef": &#123;</div><div class='line highlight'>      "N": "&#123;% $string($states.input.amount) %&#125;"</div><div class='line dim'>    &#125;</div><div class='line dim'>  &#125;</div><div class='line dim'>&#125;</div></code></div></pre><!----> <p>對於上方的 Lambda，我們則須先從 DynamoDB 讀取剩餘的 feed_amount，檢查是否足夠，然後再更新。</p> <p><!--[--><picture><source srcset="/_app/immutable/assets/step-func-2.Dgq4HsR_.avif 736w" type="image/avif"/> <img src="/posts/55rd-national-skills-competition-cloud-computing-review-part3/images/step-func-2.png" alt="Step Functions 2" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <p>Step Functions Code: <a href="/posts/55rd-national-skills-competition-cloud-computing-review-part3/files/step-func-2.json">step-func-2.json</a></p> <p>要留意的是 GetItem 後需要將結果存在 Output，每個 state 的 <code>$states.input</code> 都是根據上一個 state 的輸出而來的。此處的 <code>$states.result</code>等同於 <a href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html#API_GetItem_ResponseSyntax" rel="nofollow noopener noreferrer external" target="_blank">DynamoDB API response</a> 的格式。</p> <!----><pre class="shiki material-default" json="true"><div class="language-id">json</div><div class='code-container'><code><div class='line'>&#123;</div><div class='line'>  "dbAmount": "&#123;% $number($states.result.Item.feed_amount.N) %&#125;"</div><div class='line'>&#125;</div></code></div></pre><!----> <p>接下來透過 Choice state 與判斷式 <code>{% $states.input.dbAmount &lt; 50 %}</code> 來決定是否執行後方 UpdateItem 來更新 feed_amount。</p> <p>最後，透過 EventBridge 建立一個 cron job 來定時觸發 Step Functions，這樣這題就完成了。</p> <h3 id="serverless-application-model-部屬"><a href="#serverless-application-model-部屬">Serverless Application Model 部屬</a></h3> <p>這題提供了一個 Cloud9 雲端 IDE 環境，以及一個 Python 程式碼，要求透過 Serverless Application Model(SAM) 來部署。</p> <p>題目共有多個階段，第一步先叫你設計一個能跑的 template 出來，再來要求把 Lambda 拔掉但要維持功能…</p> <h4 id="看懂要求"><a href="#看懂要求">看懂要求</a></h4> <p>文檔中隱喻提到會用到 API Gateway、Lambda 與 DynamoDB，這些都是 Serverless 架構中常見的組件，也是 SAM 這骨董唯數不多支援的服務。</p> <p>看的出來這題設計時還沒有 Lambda Function URL，所以才會需要 API Gateway。</p> <h4 id="設計-sam-template"><a href="#設計-sam-template">設計 SAM template</a></h4> <p>SAM template 需要包含以下幾個部分:</p> <ul><li>AWS::Serverless::Api => API Gateway (RESTful)</li> <li>AWS::Serverless::Function => Lambda Function</li> <li>AWS::Serverless::SimpleTable => DynamoDB Table</li></ul> <p>基本這題上要做的事只有寫 yaml 檔，然後透過 <code>sam deploy</code> 部署上去。</p> <p>考 Cloud Formation 我還願意學，考這 SAM 搞得我有點沒心態，下方範例就靠 Copilot 幫我想了 ❤️</p> <!----><pre class="shiki material-default" yaml="true"><div class="language-id">yaml</div><div class='code-container'><code><div class='line'>AWSTemplateFormatVersion: "2010-09-09"</div><div class='line'></div><div class='line'>Resources:</div><div class='line'>  UnicornApi: # API Gateway</div><div class='line'>    Type: AWS::Serverless::Api</div><div class='line'>    Properties:</div><div class='line'>      StageName: prod</div><div class='line'></div><div class='line'>  UnicornFunction: # Lambda Function</div><div class='line'>    Type: AWS::Serverless::Function</div><div class='line'>    Properties:</div><div class='line'>      Handler: lambda_function.lambda_handler</div><div class='line'>      Runtime: python3.11</div><div class='line'>      CodeUri: unicorn_function/</div><div class='line'>      Events:</div><div class='line'>        Request: # API Gateway Event</div><div class='line'>          Type: Api</div><div class='line'>          Properties:</div><div class='line'>            Path: /</div><div class='line'>            Method: POST</div><div class='line'>            RestApiId: !Ref UnicornApi</div><div class='line'></div><div class='line'>  UnicornTable: # DynamoDB Table</div><div class='line'>    Type: AWS::Serverless::SimpleTable</div><div class='line'>    Properties:</div><div class='line'>      PrimaryKey:</div><div class='line'>        Name: id</div><div class='line'>        Type: String</div></code></div></pre><!----> <p>能跑的 template 大概就是這樣，接著把 Python 程式碼放到 <code>unicorn_function/</code> 資料夾下，然後透過 <code>sam deploy</code> 就搞定了。</p> <p>第二步要求把 Lambda Function 拔掉但又要維持功能，原本以為也是用 Step Functions 來取代，但搞好之後一直不給我分數 😡😡😡。</p> <p>花錢買 hint 才知道要到 API Gateway 設定 Integration type 為 AWS service ，並透過 Integration Request 與 Integration Response 去修改請求與回應…這是省 Lambda 費用的方法沒錯，但沒必要這麼噁心吧。</p> <p><!--[--><picture><source srcset="/_app/immutable/assets/api-transform.CwGgSHfq.avif 736w" type="image/avif"/> <img src="/posts/55rd-national-skills-competition-cloud-computing-review-part3/images/api-transform.png" alt="API Gateway Integration" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <p>圖很醜懶得畫好看，我盡力了。總之就是 request body 會被轉換成 DynamoDB UpdateItem 的格式，呼叫到 service 後再把 API response 轉換成正常的格式。</p> <p>看了半小時搞懂了原理但想不到 yaml 怎麼寫，文檔翻了三遍還是找不到哪裡可以設 integration type，最後有想到應該是要直接導入 OpenAPI schema 但剩下時間不多就放棄了。</p> <h3 id="科目心得"><a href="#科目心得">科目心得</a></h3> <p>之前就有想學 Step Functions 的念頭，但一直沒有機會深入了解。謝謝這次題目(除了第二題最後一步，我不喜歡你)讓我有機會被迫學習。這應該也是近幾年雲端運算職類第一次碰到 Infrastructure as Code 的題目，做起來其實還蠻有趣的，希望未來的我可以順便去學一下 Terraform。</p><!--]--><!--]--><!--]--><!--]-->]]>
    </content>
    <category term="Skills Competition" scheme="https://blog.0xian.dev/?tags=Skills%20Competition" />
    <category term="Cloud" scheme="https://blog.0xian.dev/?tags=Cloud" />
    <category term="AWS" scheme="https://blog.0xian.dev/?tags=AWS" />
    <category term="55th Cloud Computing" scheme="https://blog.0xian.dev/?tags=55th%20Cloud%20Computing" />
  </entry>
  <entry>
    <title type="html"><![CDATA[55屆全國技能競賽雲端運算 賽後隨筆 - Part 2]]></title>
    <link href="https://blog.0xian.dev/posts/55rd-national-skills-competition-cloud-computing-review-part2" />
    <id>https://blog.0xian.dev/posts/55rd-national-skills-competition-cloud-computing-review-part2</id>
    <published>2025-07-22T00:00:00.000Z</published>
    <updated>2025-07-22T00:00:00.000Z</updated>
    <summary type="html"><![CDATA[🛡️🔐]]></summary>
    <content type="html">
      <![CDATA[<!--[--><!--[--><!--[--><!--[--><p>早安，這是「<abbr title="長到靠北，以下簡稱&quot;全國賽&quot;">第 55 屆全國技能競賽英雄榜暨第3屆亞洲技能競賽及第 48 屆國際技能競賽國手選拔賽青年組雲端運算職類</abbr>」系列文章的第二篇，這次來分享科目二的題目和解題過程。</p> <p><!--[--><picture><source srcset="/_app/immutable/assets/Screenshot_20250721_234440.mMYtapEN.avif 736w" type="image/avif"/> <img src="/posts/55rd-national-skills-competition-cloud-computing-review-part2/images/Screenshot_20250721_234440.png" alt="昨天升到羈絆84等的カズサ" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <h2 id="科目二-secure-legends"><a href="#科目二-secure-legends">科目二: Secure Legends</a></h2> <p><a href="https://aws-experience.com/emea/north/e/f7076/aws-gameday---secure-legends" rel="nofollow noopener noreferrer external" target="_blank">AWS experience: AWS GameDay - Secure legends</a></p> <p>共有四大題，考驗的是有沒有看懂既有架構與資源，並且能夠在不影響服務的情況下，修正架構中的安全性問題。</p> <p>Gameday題目的命名都很ㄎㄧㄤ，後續命名都以我自己記得的名稱為主:</p> <ul><li>CodePipeline 修正</li> <li>Auto Scaling Instance 隔離與替換版本</li> <li>Macie PII detection</li> <li>超 級 資 安 大 雜 燴</li></ul> <h3 id="codepipeline-修正"><a href="#codepipeline-修正">CodePipeline 修正</a></h3> <p>這題給了一個 CodePipeline，結構如下: <!--[--><picture><source srcset="/_app/immutable/assets/codepipeline.6n9CP6Aa.avif 736w" type="image/avif"/> <img src="/posts/55rd-national-skills-competition-cloud-computing-review-part2/images/codepipeline.png" alt="CodePipeline" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <p>中間 Build stage 中的三個 CodeBuild 都是爛的，這題目標就是把它修好。</p> <p>在 config.zip 裡面有三個 CodeBuild 的 buildspec.yml 檔案，要做的事基本上只有看 error message 然後改它而已。</p> <h4 id="code-lint"><a href="#code-lint">Code Lint</a></h4> <p>這步有點忘記，反正就是看 build log 然後看文件提供的 Github 把指令加上一個參數就好。</p> <h4 id="git-secrets-check"><a href="#git-secrets-check">Git-secrets check</a></h4> <p>這步會用 git-secrets 檢查 Git commit 中是否有敏感資訊，題目中某個地方有一組 ID 會被當作敏感資訊檢查到，所以要把它加到白名單。</p> <h4 id="grype-cve-check"><a href="#grype-cve-check">Grype CVE check</a></h4> <p>這步會用 Grype 檢查程式碼與 dependecies 是否有 CVE 漏洞，題目中的 flask 會因為版本過舊而報錯，需要把 requirements.txt 中的 flask 版本升級。</p> <p>都改好之後重新打包 config.zip 並上傳到 S3，然後待 CodePipeline 順利執行完畢即可。</p> <h3 id="auto-scaling-instance-隔離與替換版本"><a href="#auto-scaling-instance-隔離與替換版本">Auto Scaling Instance 隔離與替換版本</a></h3> <p>這題會給你一個 Auto Scaling Group，裡面有兩台被駭的 EC2 instance，目標是把這兩台 instance 隔離、修補漏洞並替換成新的。</p> <h4 id="找出為什麼被駭"><a href="#找出為什麼被駭">找出為什麼被駭</a></h4> <p>該題沒有給程式碼，連進網頁後會看到亂碼(被駭入加密)，只能直接 SSH 進機器去看 log。</p> <p>透過 user data 我們可以得知網站程式碼來自 S3，拉取後放在 <code>/website/</code>，裡面有個 flask server 跟他的 log 檔。在程式碼中我們可以看到有一個 endpoint <code>/backdoor</code>，可以透過它來執行任意指令，在 log 檔中也看到駭客透過 openssl 執行了 aes-256-cbc 加密指令。</p> <h4 id="修補漏洞"><a href="#修補漏洞">修補漏洞</a></h4> <p>User data 中是從 S3 拉取程式碼，該 S3 bucket 我們沒權限動，因此只能複製一份到自己的 bucket 中並修改。</p> <p>修補 <code>/backdoor</code> 有三種作法:</p> <ul><li>程式碼中直接刪除 <code>/backdoor</code> endpoint (我的作法)</li> <li>用 WAF 規則封鎖 <code>/backdoor</code></li> <li>ALB 的 listener 規則封鎖 <code>/backdoor</code> (其他選手告知的騷操作)</li></ul> <h4 id="隔離"><a href="#隔離">隔離</a></h4> <p>一開始看到題目說 “isolate” 以為是把它 public IP 拔掉讓它不能被存取或是建 AMI 然後 stop instance，結果買 hint 後才知道是建一個空的 Security Group 並把它套用到這兩台 instance 上…</p> <p>因為兩台機器在 Auto Scaling Group 裡面，套空的 Security Group 會導致 Health Check 失敗而被自動替換，所以需要先把它移出 Target Group。</p> <h4 id="替換"><a href="#替換">替換</a></h4> <p>隔離後就可以直接在 Auto Scaling Group 中指定新的 Launch Template 讓它自動開新 instance 即可。</p> <h3 id="macie-pii-detection"><a href="#macie-pii-detection">Macie PII detection</a></h3> <p>這題給一個 S3 bucket，你看不到內容，需要透過 Macie 來掃描是否有 PII 資訊。</p> <p>在 Macie 中建立一個新的 PII 掃描任務，指定剛剛的 S3 bucket，並選擇掃描除了 Financial information 以外的 PII 資訊(題目要求)。</p> <p>掃描完成後會有一個報告，裡面會列出所有 PII 資訊的檔案與位置。</p> <p>還有一題是必須自訂 data identifier 來偵測某個特定的 ID 格式，regex 如下:</p> <!----><pre class="shiki material-default"><div class='code-container'><code><div class='line'>[pk]_\d&#123;3&#125;[A-Z]&#123;3&#125;e\d&#123;8&#125;</div></code></div></pre><!----> <p>也是一樣建完 data identifier 後再建立一個新的 PII 掃描任務即可。</p> <h3 id="超-級-資-安-大-雜-燴"><a href="#超-級-資-安-大-雜-燴">超 級 資 安 大 雜 燴</a></h3> <p>這題超雜，會用到 CloudTrail、AWS Config 跟 Lambda。</p> <h4 id="是誰改了-s3-設定"><a href="#是誰改了-s3-設定">是誰改了 S3 設定</a></h4> <p>這題要求你找出哪個 IP 改了 S3 bucket 的 encryption 設定。在 CloudTrail 中搜尋對應的 S3 bucket 事件，並查看 source IP 即可。</p> <h4 id="是誰改了-ec2-名稱"><a href="#是誰改了-ec2-名稱">是誰改了 EC2 名稱</a></h4> <p>這題要求你找出誰用 access key 改了某個 EC2 instance 的名稱，並把它的 access key deactivate。解法同上，搜尋 CloudTrail 中對應的 EC2 instance 事件，找出是誰改的並去 IAM 把他的 access key deactivate。</p> <h4 id="continuously-key--password-rotation"><a href="#continuously-key--password-rotation">Continuously key &amp; password rotation</a></h4> <p>這題要求持續輪替 IAM 使用者的 access key 與 password，並停用 unused password，間隔時間為 90 天。</p> <p>在 AWS Config 中建立一個新的 rule，選擇兩個 managed rule 並設定 90 天即可。</p> <h4 id="改-lambda-網頁密碼"><a href="#改-lambda-網頁密碼">改 lambda 網頁密碼</a></h4> <p>這題給了一個 lambda function URL，要求你改網頁中的管理員密碼。在 Lambda function 的程式碼中找到明文的密碼變數，把它改成其他的即可。</p> <h3 id="科目心得"><a href="#科目心得">科目心得</a></h3> <p>好懶得寫，總之就是題目說啥就幹啥，沒有什麼特別的。</p> <p>沒記錯的話最後領先快一大題的分數守住了第一名🥵。</p><!--]--><!--]--><!--]--><!--]-->]]>
    </content>
    <category term="Skills Competition" scheme="https://blog.0xian.dev/?tags=Skills%20Competition" />
    <category term="Cloud" scheme="https://blog.0xian.dev/?tags=Cloud" />
    <category term="AWS" scheme="https://blog.0xian.dev/?tags=AWS" />
    <category term="55th Cloud Computing" scheme="https://blog.0xian.dev/?tags=55th%20Cloud%20Computing" />
  </entry>
  <entry>
    <title type="html"><![CDATA[55屆全國技能競賽雲端運算 賽後隨筆 - Part 1]]></title>
    <link href="https://blog.0xian.dev/posts/55rd-national-skills-competition-cloud-computing-review-part1" />
    <id>https://blog.0xian.dev/posts/55rd-national-skills-competition-cloud-computing-review-part1</id>
    <published>2025-07-21T00:00:00.000Z</published>
    <updated>2025-07-21T00:00:00.000Z</updated>
    <summary type="html"><![CDATA[🦄☁️]]></summary>
    <content type="html">
      <![CDATA[<!--[--><!--[--><!--[--><!--[--><p>早安，我是今年參與「<abbr title="長到靠北，以下簡稱&quot;全國賽&quot;">第 55 屆全國技能競賽英雄榜暨第3屆亞洲技能競賽及第 48 屆國際技能競賽國手選拔賽青年組雲端運算職類</abbr>」並入圍二階國手選拔的選手，寫這篇文章是為了分享我在比賽過程中的一些心得和經驗。</p> <p>在開始分享之前，先<del>客套的</del>感謝一下主辦單位、裁判及工作人員們的辛勤付出，讓這次比賽能夠順利進行。雖然比賽過程中浪費了點時間在處理設備問題，但整體還是很順利的。</p> <h2 id="簡單介紹一下我自己"><a href="#簡單介紹一下我自己">簡單介紹一下我自己：</a></h2> <ul><li>逢甲大學資訊工程學系畢業</li> <li>國立中央大學資訊工程學系碩士</li> <li>第 53 屆全國技能競賽雲端運算職類 第三名、入圍二階國手選拔<sup id="fnref-1"><a href="#fn-1" class="footnote-ref">1</a></sup></li> <li><a href="/posts/54rd-regional-skills-competition-web-technologies-review/"><abbr title="才不是比不了雲端運算想去水獎金🥵">第 54 屆中區技能競賽網頁技術職類 第二名</abbr></a>、<del>全國賽請假跑去學海築夢爽</del><sup id="fnref-2"><a href="#fn-2" class="footnote-ref">2</a></sup></li> <li>第 55 屆全國技能競賽雲端運算職類 一階國手選拔第一名、入圍二階國手選拔<sup id="fnref-3"><a href="#fn-3" class="footnote-ref">3</a></sup></li> <li>自籌 2025 中區雲端運算訓練營 講師兼出題仔</li></ul> <p>第一次接觸到雲端運算職類是在第 53 屆，當時的我接觸雲端還不到一年，但不知不覺就拿到了個全國賽第三名還進入二階國手選拔，可惜當時的我實戰經驗不夠，與法國里昂的國際賽無緣。今年的比賽帶著更充足的準備和實戰經驗，在全國賽的五個科目中全部拿下第一，希望 9 月的二階國手選拔也能如此順利。</p> <p><!--[--><picture><source srcset="/_app/immutable/assets/IMG_4159.Dcav3f7n.avif 736w" type="image/avif"/> <img src="/posts/55rd-national-skills-competition-cloud-computing-review-part1/images/IMG_4159.jpg" alt="カズサ來看我比賽" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <h2 id="比賽流程"><a href="#比賽流程">比賽流程</a></h2> <p>扣掉第一天報到跟最後一天的頒獎，比賽共有五個科目，各科目約為 3 小時且內容獨立。</p> <p>比賽內容不公開，但這次題目都是用 AWS Workshop，賽後 Google 一下關鍵字能找到蠻多有的沒的資源。這篇文會分享一下科目一的題目和我的解題過程。因為科目內的題目內容高度糾纏，比完賽出來就快記不起來了，有錯誤歡迎指正。</p> <p>競賽過程中會有 Scoreboard 顯示目前的分數和排名，分數則是根據解題進度來計算，但有些靠北的題目送錯答案或是做太慢會被扣分，這點要特別注意不要一口氣把題目全開。</p> <blockquote><p>參考資料： <a href="https://www.wdasec.gov.tw/News_Content.aspx?n=BE1CD914D1A8176B&amp;sms=FDDD385F34312990&amp;s=5981855C5A245053" rel="nofollow noopener noreferrer external" target="_blank">【全國賽】114年第55屆全國技能競賽-賽前公告試題資料</a></p></blockquote> <h2 id="科目一-unicornrentals"><a href="#科目一-unicornrentals">科目一: Unicorn.Rentals</a></h2> <p>這題目考驗的是怎麼動態調整 Compute 資源，能應付忽大忽小的 HTTP 流量，同時保持可用性、回應時間與成本最佳化。</p> <p>題目只有一個送出欄位讓你提交 endpoint URL，後臺會自動發請求到你的 endpoint，並根據每個 request 的回應時間來計算分數。同時每分鐘會根據使用的資源來做扣分，只要流量得分小於成本扣分，分數就會被扣爛。</p> <p>該題目提供了一個既有的 AWS 環境，內容大致如下:</p> <ul><li>1x VPC<ul><li>有點忘記 resource map 長怎樣，但記得沒有 S3-gateway</li></ul></li> <li>1x 有 Elastic IP 的 EC2 instance<ul><li>完全獨立，endpoint 預設指在這台</li></ul></li> <li>1x Application Load Balancer (ALB)<ul><li>1x target group</li> <li>2x EC2 instances，都有 public IP</li></ul></li></ul> <p>EC2 裡面跑的是 Python HTTP server，題目文件中也有提供 user data 來協助設定環境，大概長這樣:</p> <!----><pre class="shiki material-default" bash="true"><div class="language-id">bash</div><div class='code-container'><code><div class='line'>#!/bin/bash</div><div class='line'>wget https://s3.amazonaws.com/xxx/web-binary.py -O /website/web-binary.py</div><div class='line'>chmod +700 /website/web-binary.py</div><div class='line'>python3 /website/web-binary.py</div><div class='line'>shutdown -h now</div></code></div></pre><!----> <p>沒記錯的話整場的流量大概長這樣: <!--[--><picture><source srcset="/_app/immutable/assets/rpm.BCGbTesI.avif 736w" type="image/avif"/> <img src="/posts/55rd-national-skills-competition-cloud-computing-review-part1/images/rpm.png" alt="Requests per minute" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <h3 id="cloudfront-快取"><a href="#cloudfront-快取">CloudFront 快取</a></h3> <p>這題的重點在於使用 CloudFront 快取來降低計算資源的使用率，同時降低回應時間。</p> <p>在題目文件中我們可以看到請求會長的像這樣：</p> <!----><pre class="shiki material-default"><div class='code-container'><code><div class='line'>GET /calc?input=xxxxx</div></code></div></pre><!----> <p>我們可以推測一個 <code>input</code> 只會對應到一個結果，所以可以利用 CloudFront 的 Cache policy 來做快取。</p> <p>透過在 CloudFront 建立一個 Cache policy，設定 Default TTL 不要太低(預設一天即可)、指定 Cache key settings 中的 Query strings 為 <code>All</code>，並於對應的 behavior 中使用該 policy 即可。</p> <p>比賽結束前幾分鐘看到 CloudFront 的 Hit Rate 大概為 10.5%，對於回應時間和成本的優化都有很大的幫助。</p> <h3 id="auto-scaling"><a href="#auto-scaling">Auto Scaling</a></h3> <p>題目提到流量會隨時間變化，開一台大機器接請求肯定是不划算的，因此需要使用 Auto Scaling 來動態調整 EC2 instance 的數量，應對變化的流量需求。當然這題也可以用 ECS 或 EKS 來做，但礙於時間有限加上懶得寫 Dockerfile，所以就直接用 EC2 來做了。</p> <p>在做 auto scaling 之前，我們可以看到 user data 裡面只有對外抓取 S3 上的程式碼，所以我們可以不給他 public IP 跟 NAT gateway，直接透過 VPC endpoints 來存取 S3 gateway，省下 IP 跟 NAT 的費用。</p> <p>新建一個 VPC，資源如下: <!--[--><picture><source srcset="/_app/immutable/assets/vpc.H5-M2xut.avif 736w" type="image/avif"/> <img src="/posts/55rd-national-skills-competition-cloud-computing-review-part1/images/vpc.png" alt="VPC resources" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <p>接下來建立一個 Launch Template，指定 AMI、user data、disable public IP 等。從 S3 抓程式碼是用 wget，可以得知不需要 IAM role。</p> <p>接著建立一個 Auto Scaling Group，指定 VPC 為剛剛建立的 VPC，subnets 選擇建立的兩個 private subnets，並指定 Launch Template。最省錢的作法會是用 t3.nano 的 spot instance，但我忘記而是用 t2.nano on-demand，賽後跟其他選手討論後才得知每台可以少被扣 4 分。</p> <p>最後建立一個 Application Load Balancer，並將 ALB 的 subnets 改為 public subnets (預設會自動選跟 EC2 的一樣，但 ALB 需要 internet gateway，只有 public subnets 才有)、target group 指向 Auto Scaling Group，這樣就完成了。</p> <p>對於 Auto Scaling Policy 的設定，單看 EC2 的 CPU 使用率並沒有太大變化，因此我使用 Request Count per Target 來做 scaling policy，設定為每分鐘約 15 個請求。</p> <p>除此之外還有幾點可以優化的地方:</p> <ul><li>Target group 的 algorithm 可以改成 least outstanding requests，讓流量優先分配到請求較少的 instance 上。</li> <li>降低 Auto Scaling 設定的 health check grace period，讓 instance 在啟動後就能更快開始接收請求。</li> <li>降低 Load Balancer 的 connection draining 時間，更快釋放要縮減的 instance。</li></ul> <p>建完後記得回到 CloudFront 建立新的 origin，並把對應的 behavior 指向新的 origin。</p> <h3 id="更新版本"><a href="#更新版本">更新版本</a></h3> <p>題目在後期會有一個叫你更新 server-binary.py 版本的步驟，因為我們已經建好 launch template 了，直接建立新版本並在 Auto Scaling Group 中做 instance refresh 即可(慢到靠北，追求速度的話直接 terminate 再開其實也行)。</p> <h3 id="猴子"><a href="#猴子">猴子</a></h3> <p>在競賽過程中會遇到「猴子」來亂條你的設定，這時候就需要透過監控來快速反應，把它亂改的設定改回來。賽後跟其他選手討論總結出來被改到的地方只有 Auto scaling group 的 desired capacity，透過監控 scaling activity 便可快速發現並修正。但我還是花了點時間搞了 CloudWatch Dashboard 來監控整體流量、回應時間以及 CloudFront 狀態(不知道為什麼有時候會突然跑出幾個 504)。</p> <h3 id="科目心得"><a href="#科目心得">科目心得</a></h3> <p>第一步先看懂文件並清點預設環境內有哪些資源跟它的邏輯蠻重要的，像我一開始就發現它建好了個可以用的 ALB，連上去測也是正常的，套完 CloudFront 就直接上線開始賺分，整場沒被扣分過。</p> <p>雖然更新版本的過程中被其他選手分數反超，但我把 auto scaling 的 scale-in/out 設定得很極致，最後半小時流量上上下下依舊穩定輸出，直接把分數拉回來，最後領先 2000 分守住了第一名。</p> <p>這幾天會再寫其他科目的心得，一個科目一篇，慢慢寫到 Part 5。有興趣的話可以關注一下或是<a href="https://static.0xian.dev/donate.html" rel="nofollow noopener noreferrer external" target="_blank">請我喝杯咖啡或吃一頓好料</a> 🥰。</p> <div class="footnotes"><hr/> <ol><li id="fn-1"><a href="https://www.wdasec.gov.tw/News_Content.aspx?n=4189469C16B9FCD8&amp;sms=B5C3C25D4B934F97&amp;s=85E4675A23FAA6EC" rel="nofollow noopener noreferrer external" target="_blank">第53屆全國技能競賽英雄榜</a><a href="#fnref-1" class="footnote-backref">↩</a></li> <li id="fn-2"><a href="https://www.wdasec.gov.tw/News_Content.aspx?n=4189469C16B9FCD8&amp;sms=B5C3C25D4B934F97&amp;s=36CA034AED234D0A" rel="nofollow noopener noreferrer external" target="_blank">第54屆分區技能競賽青年組英雄榜</a><a href="#fnref-2" class="footnote-backref">↩</a></li> <li id="fn-3"><a href="https://www.wdasec.gov.tw/News_Content.aspx?n=BD34663BD4EC9F4C&amp;sms=535E01AC52EFB5A7&amp;s=D0935C80C3C5CF8E" rel="nofollow noopener noreferrer external" target="_blank">第55屆全國技能競賽英雄榜暨第3屆亞洲技能競賽及第48屆國際技能競賽國手選拔賽入圍名冊及參賽意願書</a><a href="#fnref-3" class="footnote-backref">↩</a></li></ol></div><!--]--><!--]--><!--]--><!--]-->]]>
    </content>
    <category term="Skills Competition" scheme="https://blog.0xian.dev/?tags=Skills%20Competition" />
    <category term="Cloud" scheme="https://blog.0xian.dev/?tags=Cloud" />
    <category term="AWS" scheme="https://blog.0xian.dev/?tags=AWS" />
    <category term="55th Cloud Computing" scheme="https://blog.0xian.dev/?tags=55th%20Cloud%20Computing" />
  </entry>
  <entry>
    <title type="html"><![CDATA[用 Cloudflare + SendGrid 在自己的 domain 免費收發 email]]></title>
    <link href="https://blog.0xian.dev/posts/cloudflare-sendgrid-free-email-hosting" />
    <id>https://blog.0xian.dev/posts/cloudflare-sendgrid-free-email-hosting</id>
    <published>2024-10-15T00:00:00.000Z</published>
    <updated>2024-10-15T00:00:00.000Z</updated>
    <summary type="html"><![CDATA[不用伺服器的 email hosting 真香]]></summary>
    <content type="html">
      <![CDATA[<!--[--><!--[--><!--[--><!--[--><h2 id="前言"><a href="#前言">前言</a></h2> <p>要架一個 mail server 並不容易，除了要隨時跑著一台伺服器外，家用或 VPS 的 IP 也不夠純淨，通常會被 Google 跟 Microsoft 當成垃圾信。</p> <p>此文會介紹怎麼使用 Cloudflare 來收信並轉寄到 Gmail，並從 Gmail 透過 SendGrid 來寄信，而且可以用無限多個 email address。</p> <p><!--[--><picture><source srcset="/_app/immutable/assets/01.BsIQOjMQ.avif 736w" type="image/avif"/> <img src="/posts/cloudflare-sendgrid-free-email-hosting/images/01.png" alt="Email flow" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <h2 id="cloudflare-收信"><a href="#cloudflare-收信">Cloudflare 收信</a></h2> <p>Cloudflare 免費方案提供了無限用量的 <a href="https://developers.cloudflare.com/email-routing/" rel="nofollow noopener noreferrer external" target="_blank">Email Routing</a> 服務，可以將收到的 email 轉寄到指定的信箱，而且支援 Catch-all。</p> <p>在 Dashboard 啟用 Email Routing 後，Cloudflare 會自動替你設定三個 MX record 跟一個 SPF record，可以在 Settings Tab 看到。為了後續 SendGrid 的設定，我們必須先將它 Unlock，不然 Cloudflare 不會讓我們動這四個 record。</p> <p><!--[--><picture><source srcset="/_app/immutable/assets/02.Thd-drDe.avif 736w" type="image/avif"/> <img src="/posts/cloudflare-sendgrid-free-email-hosting/images/02.png" alt="Email Routing records" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <p>開啟 Email Routing 後，先在 Destination addresses 新增自己的 Gmail 並驗證，接下來就可以去 Routing Rules 新增路由規則。</p> <p>要接收所有 email 可以直接使用 Catch-all，也可以針對不同的 email address 設定不同的轉寄規則。</p> <p><!--[--><picture><source srcset="/_app/immutable/assets/03.C5oyeMam.avif 736w" type="image/avif"/> <img src="/posts/cloudflare-sendgrid-free-email-hosting/images/03.png" alt="Routing Rules" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <h2 id="sendgrid-寄信"><a href="#sendgrid-寄信">SendGrid 寄信</a></h2> <p><a href="https://sendgrid.com/" rel="nofollow noopener noreferrer external" target="_blank">SendGrid</a> 是一個可以用 API、SMTP Relay 來寄信的服務，免費方案每天可以寄 100 封，驗證 domain 後可以使用任意 email address 來寄信。</p> <p>要註冊 SendGrid 帳號需要一點技巧，如果從官網註冊的話有 99% 會被擋掉(至少我不是那 1%，送了三次都被擋==)，個人用戶開 ticket 也只會等三天然後被拒絕，這時我們可以用 Google Cloud Platform 裡面的 Marketplace 來訂閱 SendGrid Free Plan，理論上只要開一張 ticket 證明一下你是誰就可以了，內容大致如下：</p> <!----><pre class="shiki material-default"><div class='code-container'><code><div class='line'>To have your account reviewed, please reply back to this email with the following:</div><div class='line'></div><div class='line'>1. User Identification</div><div class='line'>Your first and last name to help us confirm your identity.</div><div class='line'>Any of your personal social media profiles such as LinkedIn, Facebook, Twitter, or your online portfolio such as GitHub or Upwork, showing your full name.</div><div class='line'>An email address which matches your website domain or a screenshot of a receipt that verifies domain ownership.</div><div class='line'></div><div class='line'>2. Intended Use Case</div><div class='line'>A complete description of the services/products your organization provides to better understand the nature of your business.</div><div class='line'></div><div class='line'>3. Email Type</div><div class='line'>Will you be sending Transactional or Marketing emails?</div><div class='line'>How will your recipient's email addresses be collected (opt-in)? Please provide a link to an accessible website for your organization or a screenshot of the registration flow. If the website is not publicly available yet, a mock-up of the sign-up page will suffice.</div></code></div></pre><!----> <p>成功註冊後，先在 Settings > Sender Authentication 驗證自己的 domain，依照指示設定三個 CNAME record (emXXX.、s1._domainkey、s1._domainkey) 跟一個 TXT record (_dmarc)。</p> <p>設定完成後建議 Single Sender Verification 也一同設定，我不太確定會不會影響被當垃圾信的機率。</p> <p><!--[--><picture><source srcset="/_app/immutable/assets/04.CziitDhw.avif 736w" type="image/avif"/> <img src="/posts/cloudflare-sendgrid-free-email-hosting/images/04.png" alt="Sender Authentication" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <p>設定完後就可以在 Settings > API Keys 新增一個 API Key，權限可以只給 <code>Mail Send</code>。</p> <h2 id="gmail-設定"><a href="#gmail-設定">Gmail 設定</a></h2> <p>拿到 SendGrid 的 API Key 後，就可以在 Gmail 設定 SMTP Relay 了，這樣就可以用 SendGrid 來寄信了。</p> <p>點開右上角齒輪 > 查看所有設定 > 帳戶和匯入 > 選擇寄件地址 > 添加另一個電子郵件地址，名稱任取，電子郵件地址為你的 domain，照下列設定 SMTP 伺服器並儲存：</p> <ul><li>SMTP 伺服器：<code>smtp.sendgrid.net</code></li> <li>連接埠：<code>587</code></li> <li>使用者名稱：<code>apikey</code></li> <li>密碼：<code>你的 SendGrid API Key</code></li> <li>採用 TLS 的加密連線 (建議使用)</li></ul> <p><!--[--><picture><source srcset="/_app/immutable/assets/05.D5Ps7gRJ.avif 736w" type="image/avif"/> <img src="/posts/cloudflare-sendgrid-free-email-hosting/images/05.png" alt="Gmail SMTP settings" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <p><!--[--><picture><source srcset="/_app/immutable/assets/06.ClSV87kA.avif 736w" type="image/avif"/> <img src="/posts/cloudflare-sendgrid-free-email-hosting/images/06.png" alt="Gmail SMTP settings" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <p>接下來在寄信時就可以選這個 email address 來寄信了 🎉</p> <h2 id="spfdkimdmarc"><a href="#spfdkimdmarc">SPF、DKIM、DMARC</a></h2> <p>為了避免被當成垃圾信，我們還需要確認 SPF、DKIM 跟 DMARC 有正確設定。</p> <h3 id="spf"><a href="#spf">SPF</a></h3> <p>Sender Policy Framework 用來確認寄件者是否有權利使用這個 domain 來寄信。在這案例中會有兩個寄件者，分別是用來寄信的 SendGrid，還有 Cloudflare 轉寄給你。DNS record 會長這樣：</p> <div class="overflow-x-auto mb-4"><table class="table w-full"><!--[--><thead><tr><th>Type</th><th>Name</th><th>Value</th></tr></thead> <tbody><tr><td>TXT</td><td>@</td><td><code>v=spf1 include:_spf.mx.cloudflare.net include:sendgrid.net ~all</code></td></tr></tbody><!--]--></table></div><!----> <p>其中 <code>include</code> 代表允許這些 domain 下的 IP 來寄信，<code>~all</code> 代表把不符合規則的信標記為垃圾信。</p> <h3 id="dkim"><a href="#dkim">DKIM</a></h3> <p>DomainKeys Identified Mail 用來簽署寄出的 email。在 SendGrid 設定時便已設定好兩個 domain key，DNS record 長這樣：</p> <div class="overflow-x-auto mb-4"><table class="table w-full"><!--[--><thead><tr><th>Type</th><th>Name</th><th>Value</th></tr></thead> <tbody><tr><td>CNAME</td><td>s1._domainkey</td><td><code>s1._domainkey.uXXXX.wlXXX.sendgrid.net</code></td></tr><tr><td>CNAME</td><td>s2._domainkey</td><td><code>s2._domainkey.uXXXX.wlXXX.sendgrid.net</code></td></tr></tbody><!--]--></table></div><!----> <h3 id="dmarc"><a href="#dmarc">DMARC</a></h3> <p>Domain-based Message Authentication Reporting and Conformance 用來告訴收信者這個 domain 的寄信規則。</p> <p>Cloudflare 提供了 DMARC Management，啟用後會設定一個 TXT record，DNS record 長這樣：</p> <div class="overflow-x-auto mb-4"><table class="table w-full"><!--[--><thead><tr><th>Type</th><th>Name</th><th>Value</th></tr></thead> <tbody><tr><td>TXT</td><td>_dmarc</td><td><code>v=DMARC1; p=quarantine; rua=mailto:xxxx@dmarc-reports.cloudflare.net</code></td></tr></tbody><!--]--></table></div><!----> <p>其中 <code>p</code> 代表不符合規則的信要怎麼處理，<code>none</code> 代表不處理，<code>quarantine</code> 代表標記為垃圾信，<code>reject</code> 會直接拒收。 <code>rua</code> 代表收件者該把狀況通報給誰，這裡是通報給 Cloudflare 自動分配的 email，不用動它。</p> <h2 id="結語"><a href="#結語">結語</a></h2> <p>筆者用這個方法大概一年多了，收信、寄信都沒問題，但就是有時會被 Gmail 當成垃圾信，通常是因為 domain 的信譽不夠好，可以透過 <a href="https://postmaster.google.com/" rel="nofollow noopener noreferrer external" target="_blank">Gmail Postmaster Tools</a> 來看收信狀況還有被當垃圾信的原因。</p> <p>另外 SendGrid 還有提供自訂 domain 的連結追蹤(Link Branding)、開信追蹤(Pixel tracking，domain 信譽不好會被當垃圾圖片QQ)等功能，可以摸索看看。</p><!--]--><!--]--><!--]--><!--]-->]]>
    </content>
    <category term="Cloudflare" scheme="https://blog.0xian.dev/?tags=Cloudflare" />
    <category term="Email" scheme="https://blog.0xian.dev/?tags=Email" />
    <category term="Serverless" scheme="https://blog.0xian.dev/?tags=Serverless" />
  </entry>
  <entry>
    <title type="html"><![CDATA[54屆分區技能競賽網頁技術 賽後隨筆]]></title>
    <link href="https://blog.0xian.dev/posts/54rd-regional-skills-competition-web-technologies-review" />
    <id>https://blog.0xian.dev/posts/54rd-regional-skills-competition-web-technologies-review</id>
    <published>2024-04-03T00:00:00.000Z</published>
    <updated>2024-04-03T00:00:00.000Z</updated>
    <summary type="html"><![CDATA[我不喜歡 PHP 🥲]]></summary>
    <content type="html">
      <![CDATA[<!--[--><!--[--><!--[--><!--[--><p>去年比雲端運算職類拿到全國第三後被 BAN 掉，不能再比相同職類，剛好這次年紀壓線可以報名網頁技術，自己也寫網頁一段時間了，就報名來玩看看，也有幸拿到了中區第二，可以再去全國賽玩ㄌ。</p> <p>這篇文章主要是記錄一下競賽中遇到的問題及心得。</p> <blockquote><p>參考資料： <a href="https://www.wdasec.gov.tw/News_Content.aspx?sms=FDDD385F34312990&amp;s=D10C309CEB39384D" rel="nofollow noopener noreferrer external" target="_blank">【分區賽】113年第54屆分區技能競賽-賽前公告試題資料</a></p></blockquote> <h2 id="競賽環境"><a href="#競賽環境">競賽環境</a></h2> <p>如競賽公告的”場地設備準備表”所述，競賽環境提供了下列幾項軟體，粗體是我有用到的：</p> <ul><li><p><strong>Windows 10</strong></p></li> <li><p>Chrome、Firefox Developer Edition、<strong>內建的 Edge</strong></p> <ul><li>挑習慣的用就好，反正也只會開網頁 + DevTools</li> <li><del>其實我都用 Chrome，但它預設瀏覽器是 Edge 我懶得改</del></li></ul></li> <li><p><strong>XAMPP 8.2.0</strong></p> <ul><li>監評要求檔案放置於桌面 webXX 資料夾，可以改 httpd.conf 的 DocumentRoot 直接把它指過去</li></ul></li> <li><p>jQuery、<strong>jQuery UI</strong></p> <ul><li>jQuery UI 只用到了 button 跟 dialog</li></ul></li> <li><p>三大框架(Angular、React、Vue)</p> <ul><li>分區賽的題目用 Vanilla JS 就夠了</li></ul></li> <li><p>Prototype</p> <ul><li>這是不是很久以前的東西了</li></ul></li> <li><p>Adobe 全家統(Dreamweaver、Photoshop、Illustrator)、CorelDRAW</p> <ul><li>我都不會用</li></ul></li> <li><p>Visual Studio Code、Sublime Text、<strong>PHPStorm</strong></p> <ul><li>VS Code 沒裝套件，只有 HTML、CSS、JS 的語法提示</li> <li>PHPStorm 內建 PHP 文檔可以看，還可以開 live reload，大推 👍👍👍</li> <li>環境提供 PHPStorm 2021.3 及 2023.3，但後者因為沒網路起用不了，只能用 2021.3</li></ul></li> <li><p>Notepad++、PS Pad、EditPlus、UltraEdit</p> <ul><li>這幾個都用不到</li></ul></li></ul> <h2 id="競賽過程"><a href="#競賽過程">競賽過程</a></h2> <h3 id="day-1"><a href="#day-1">Day 1</a></h3> <p>中午 12:30 開放報到，人都到齊後帶隊去競賽場地，抽崗位編號然後入座，給大家 15 分鐘測試環境(測硬體、安裝軟體)，電腦不會還原，隔天競賽時直接繼續用。</p> <h3 id="day-2"><a href="#day-2">Day 2</a></h3> <p>早上 8:00 前報到，講解題目及注意事項大概 30 分鐘，然後就開始 4 小時的競賽。</p> <p>題目會事先公告在競賽公告上的”競賽試題及說明”，當天會有紙本試題，題目會稍微修改，增加一些有的沒的 feature。</p> <p>競賽結束後會叫大家把檔案複製到 USB，並把網頁打開，然後就可以離場用餐了，監評會趁這段時間先評主觀分數。</p> <p>大概 13:30 開始照崗位編號叫進去 demo 評客觀分數，每個人大概 10 分鐘，評完就可以閃人了。</p> <h2 id="題目解析"><a href="#題目解析">題目解析</a></h2> <p>這次題目是”快樂旅遊網”，主要有訪客留言、網頁管理、訪客訂餐、訪客訂房這四個功能。</p> <h3 id="訪客留言"><a href="#訪客留言">訪客留言</a></h3> <p>如題目所敘，輸入資料並送出後會顯示在留言列表，可以輸入留言序號(把它當作密碼)來編輯或刪除留言。</p> <p>輸入欄位有要求像要驗證格式，可以直接用 input type 或 pattern 來做。</p> <!----><pre class="shiki material-default" html="true"><div class="language-id">html</div><div class='code-container'><code><div class='line'>&lt;!-- Email 只少要有一個 &#96;@&#96; 跟 &#96;.&#96; --&gt;</div><div class='line'>&lt;input type="email" /&gt;</div><div class='line'></div><div class='line'>&lt;!-- 電話號碼只能輸入數字及 &#96;-&#96; --&gt;</div><div class='line'>&lt;input pattern="[0-9-]+" /&gt;</div><div class='line'></div><div class='line'>&lt;!-- 留言序號只能 3 位數小寫英文及數字 --&gt;</div><div class='line'>&lt;input pattern="[a-z0-9]&#123;3&#125;" /&gt;</div></code></div></pre><!----> <p>輸入的資料用 mysqli 丟進資料庫，我把編輯跟刪除都寫在同一個頁面，透過網址參數 <code>?action=edit</code> 來判斷要幹嘛，還有 session 來判斷是不是管理員，藉此少寫幾個頁面。</p> <h3 id="網頁管理"><a href="#網頁管理">網頁管理</a></h3> <p>要先做一個登入頁面，登入後可以編輯、刪除留言，還有更改訂房資料。</p> <p>登入頁面要做一個圖片驗證碼，還要可以重新產生。</p> <p>我原本想說用小畫家畫個 5 張圖片然後隨機輪替，但好懶得 P 圖，最後選擇直接放一個 <code>&lt;p>1234&lt;/p></code> 然後設定 <code>user-select: none</code> 還有把它搞得很難看到，這樣監評也給過 XD。</p> <p>現場題目還多了一個第二階段驗證碼，要產隨機的 9 宮格數字，並用拖曳的方式把它照順序排好，但這題才占 1 分，寫起來不划算果斷跳過。</p> <p>登入驗證反正題目要求就那樣，直接寫死就好 🥰，然後用 $_SESSION 存一個 boolean 來判斷是否登入。</p> <!----><pre class="shiki material-default" php="true"><div class="language-id">php</div><div class='code-container'><code><div class='line'>&lt;?php</div><div class='line'>session_start();</div><div class='line'></div><div class='line'>if ($_POST['username'] === 'admin' && $_POST['password'] === '1234') &#123;</div><div class='line'>  $_SESSION['login'] = true;</div><div class='line'>  header('Location: index.php');</div><div class='line'>  exit;</div><div class='line'>&#125;</div><div class='line'>?&gt;</div></code></div></pre><!----> <p>編輯留言我直接使用上一段所寫的功能，加了些權限判斷讓管理員登入後可以編輯、回應還有刪除。</p> <p>訂房管理… 沒時間做 PASS。</p> <h3 id="訪客訂餐"><a href="#訪客訂餐">訪客訂餐</a></h3> <p><del>沒有任何”訂餐”的要素</del></p> <p>給 16 張熱炒圖片，設計 RWD 頁面，題目要求寬版(>= 1024px)時一列 4 張，中版(>= 800px)時一列 2 張，窄版(&lt; 800px)時一列 1 張。</p> <p>這部分直接使用 grid layout 搭配 media query 來做即可。</p> <!----><pre class="shiki material-default" css="true"><div class="language-id">css</div><div class='code-container'><code><div class='line'>@media (min-width: 1024px) &#123;</div><div class='line'>  .img-container &#123;</div><div class='line'>    grid-template-columns: repeat(4, 1fr);</div><div class='line'>  &#125;</div><div class='line'>&#125;</div><div class='line'></div><div class='line'>/* ...etc */</div><div class='line'></div><div class='line'>.img-container &#123;</div><div class='line'>  display: grid;</div><div class='line'>  grid-template-columns: repeat(1, 1fr);</div><div class='line'>&#125;</div></code></div></pre><!----> <p>點擊圖片後開啟一個 dialog，有 <code>&lt; O ></code> 三個按鈕，分別是減少透明度、重設透明度、增加透明度。</p> <p>這部分直接使用 jQuery UI 的 dialog，透明度丟到全域變數，然後按鈕點擊時改變變數即可。</p> <!----><pre class="shiki material-default" js="true"><div class="language-id">js</div><div class='code-container'><code><div class='line'>window.OPACITY = 1;</div><div class='line'></div><div class='line'>const updateOpacity = val =&gt; &#123;</div><div class='line'>  const img = document.querySelector("#dialog img");</div><div class='line'>  window.OPACITY = val;</div><div class='line'>  img.style.opacity = window.OPACITY;</div><div class='line'>&#125;;</div><div class='line'></div><div class='line'>$("#dialog").dialog(&#123;</div><div class='line'>  buttons: [</div><div class='line'>    &#123;</div><div class='line'>      text: "&lt;",</div><div class='line'>      click: () =&gt; updateOpacity(window.OPACITY - 0.1),</div><div class='line'>    &#125;,</div><div class='line'>    &#123;</div><div class='line'>      text: "O",</div><div class='line'>      click: () =&gt; updateOpacity(1),</div><div class='line'>    &#125;,</div><div class='line'>    &#123;</div><div class='line'>      text: "&gt;",</div><div class='line'>      click: () =&gt; updateOpacity(window.OPACITY + 0.1),</div><div class='line'>    &#125;,</div><div class='line'>  ],</div><div class='line'>&#125;);</div></code></div></pre><!----> <h3 id="訪客訂房"><a href="#訪客訂房">訪客訂房</a></h3> <p>寫到這邊剩下 5 分鐘，乖乖吃零食等結束。</p> <p>原本還以為只有我寫不完，但看來這很正常 XD。</p> <h2 id="結語"><a href="#結語">結語</a></h2> <p>報名時還在想我都沒寫過半行 PHP 會不會爛掉，幸好競賽前一天的臨時抱佛腳有派上用場，加上分區賽題目根本就是在考驗手刻 CSS，除了寫不完之外都很順利。</p> <p>希望 7 月的全國賽競賽順利，趁這段時間給自己一點動力去碰 PHP。</p><!--]--><!--]--><!--]--><!--]-->]]>
    </content>
    <category term="Skills Competition" scheme="https://blog.0xian.dev/?tags=Skills%20Competition" />
    <category term="Web" scheme="https://blog.0xian.dev/?tags=Web" />
  </entry>
  <entry>
    <title type="html"><![CDATA[使用 Cloudflare Tunnel 取代 Ngrok，免費的反向代理工具及存取控管]]></title>
    <link href="https://blog.0xian.dev/posts/cloudflare-tunnel" />
    <id>https://blog.0xian.dev/posts/cloudflare-tunnel</id>
    <published>2024-03-01T00:00:00.000Z</published>
    <updated>2024-03-01T00:00:00.000Z</updated>
    <summary type="html"><![CDATA[Cloudflare 拔拔的免費服務]]></summary>
    <content type="html">
      <![CDATA[<!--[--><!--[--><!--[--><!--[--><h2 id="前言"><a href="#前言">前言</a></h2> <p>反向代理是網頁開發環境上常見到的工具，在開發過程如果需要讓其他人存取區網內的伺服器，除了進一層又一層的 router 開防火牆權限讓外面連進來之外，最簡單的方法就是直接從 local 端建立一條連線到外部，讓其他人可以透過這條連線存取 local 端的資源。</p> <p>常見的應用場合像是 Line Bot 或第三方金流 API，我們需要接收它們所發送的 Webhook，而且必須是 <strong>https</strong>。</p> <p>還有可以用來繞過 <a href="https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts" rel="nofollow noopener noreferrer external" target="_blank">Secure Context</a>，在做網頁開發時通常為了方便都會直接走 http protocol，在 http://localhost 存取不會有問題，但當要用區網內其它裝置連去測試時，就容易被 Secure Context 搞到，拿個 MediaDevices 都不行 😥。</p> <p>使用反向代理還有一個好處，就是可以避免伺服器的 IP 洩漏，對方只會看到代理伺服器的 IP，且大多數反向代理都至少會幫你做一點點 🤏 的防護，比較不容易被打爛。</p> <h2 id="cloudflare-tunnel"><a href="#cloudflare-tunnel">Cloudflare Tunnel</a></h2> <p><a href="https://www.cloudflare.com/tunnel/" rel="nofollow noopener noreferrer external" target="_blank">Cloudflare Tunnel</a> 是 Cloudflare 提供的一個免費服務，可以讓你在 local 端建立一條連線到 Cloudflare 的伺服器，讓外部的人可以透過 Cloudflare 的伺服器存取 local 端的資源。</p> <p><!--[!--><img src="https://blog.cloudflare.com/content/images/2021/10/2-17.png" alt="Cloudflare Tunnel Structure" class="rounded-lg my-2" loading="lazy" decoding="async"/><!--]--><!----> (圖片來源: <a href="https://blog.cloudflare.com/getting-cloudflare-tunnels-to-connect-to-the-cloudflare-network-with-quic/" rel="nofollow noopener noreferrer external" target="_blank">Cloudflare Blog</a>)</p> <p>與 <a href="https://ngrok.com/" rel="nofollow noopener noreferrer external" target="_blank">Ngrok</a> 相比，Cloudflare 擁有自己的全球骨幹，連線品質相對穩定，而且免費方案的功能很完整，但需要擁有自己的 domain 才能把它用到極致。</p> <h3 id="實際使用"><a href="#實際使用">實際使用</a></h3> <p>要用這酷酷的東西，首先要去 <a href="https://github.com/cloudflare/cloudflared/releases" rel="nofollow noopener noreferrer external" target="_blank">Cloudflare GitHub</a> 下載 <code>cloudflared</code>，之後直接對它下指令就可以了。</p> <p>在沒有登入的情況下，Cloudflare 會給你一個隨機的 <code>&lt;some-random-words>.trycloudflare.com</code> 網域，以下以建立一個反向代理到 <code>http://localhost:3000</code> 為例:</p> <!----><pre class="shiki material-default" bash="true"><div class="language-id">bash</div><div class='code-container'><code><div class='line'>$ cloudflared tunnel --url http://localhost:3000</div><div class='line'></div><div class='line'>2024-03-01T00:00:00Z INF Thank you for trying Cloudflare Tunnel. Doing so, without a Cloudflare account, is a quick way to experiment and try it out. However, be aware that these account-less Tunnels have no uptime guarantee. If you intend to use Tunnels in production you should use a pre-created named tunnel by following: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps</div><div class='line'>2024-03-01T00:00:00Z INF Requesting new quick Tunnel on trycloudflare.com...</div><div class='line'>2024-03-01T00:00:00Z INF +--------------------------------------------------------------------------------------------+</div><div class='line'>2024-03-01T00:00:00Z INF |  Your quick Tunnel has been created! Visit it at (it may take some time to be reachable):  |</div><div class='line'>2024-03-01T00:00:00Z INF |  https://pad-supplier-bailey-representative.trycloudflare.com                              |</div><div class='line'>2024-03-01T00:00:00Z INF +--------------------------------------------------------------------------------------------+</div><div class='line'></div><div class='line'>...</div></code></div></pre><!----> <p>大概2秒後就會看到 <code>Your quick Tunnel has been created!</code> ，然後就可以用下方的網址去存取你的網站了，非常快速。</p> <h3 id="自訂網域"><a href="#自訂網域">自訂網域</a></h3> <blockquote><p>以下步驟需要: 自己的網域、Cloudflare 帳號及<acronym title="信用卡/簽帳卡">大人的卡片</acronym>(免費方案需填寫付款方式)</p></blockquote> <p>如果你有自己的網域，<strong>且 Name Server 為 Cloudflare</strong>，那就可以搭配 Cloudflare 的 Zero Trust 服務，將你的主機加入到 Zero Trust Network 中，這樣就可在控制台直接管理 Tunnel 及設定路由。</p> <p>首先，登入 Cloudflare Dashboard 並點選 Zero Trust，照畫面指示開通免費方案。</p> <p><!--[--><picture><source srcset="/_app/immutable/assets/01.DdBgV1uv.avif 736w" type="image/avif"/> <img src="/posts/cloudflare-tunnel/images/01.png" alt="Cloudflare dashboard" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <h4 id="建立-tunnel"><a href="#建立-tunnel">建立 Tunnel</a></h4> <p>點選 Networks => Tunnels => Create a tunnel 來建立新 Tunnel。</p> <p><!--[--><picture><source srcset="/_app/immutable/assets/02.CMQ0NPNK.avif 736w" type="image/avif"/> <img src="/posts/cloudflare-tunnel/images/02.png" alt="Create tunnels" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <p>Connector 選擇 Cloudflared，輸入 Tunnel name，最後複製下方 cloudflared 的指令，並用系統管理員執行。執行後會在主機上建立一個常駐 service，負責與 cloudflare 連線。</p> <p>成功後會在 Connectors 看到你的主機，之後便可以直接透過網頁來管理這台主機上的路由。</p> <p>每個 Tunnel 都可以同時設定多個路由，也就是多個 domain 可以指到 local 端不同的 port。舉例來說：</p> <!----><pre class="shiki material-default"><div class='code-container'><code><div class='line'>alpha.example.com =&gt; http://localhost:3000</div><div class='line'>beta.example.com  =&gt; https://192.168.1.1:443</div><div class='line'>ssh.example.com   =&gt; ssh://localhost:22</div></code></div></pre><!----> <p>除了常見的 http、https 協定外，ssh、rdp、tcp 等都是可以使用，但需要在存取端安裝 cloudflared，詳細可參考 <a href="https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/use-cases/" rel="nofollow noopener noreferrer external" target="_blank">Cloudflare Docs</a>。若連線到的是 ssh 或 vnc，下方<a href="#%E5%AD%98%E5%8F%96%E6%8E%A7%E7%AE%A1">存取控管</a>章節有提到瀏覽器直接渲染的功能。</p> <p>首先選擇要設定的 Tunnel，進入 Public Hostname => Add a public hostname 來設定 domain 及路由。</p> <p><!--[--><picture><source srcset="/_app/immutable/assets/03.I73UT4UI.avif 736w" type="image/avif"/> <img src="/posts/cloudflare-tunnel/images/03.png" alt="Setup public hostname" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <p>Additional application settings 內還有許多細節可以做設定，儲存後即可開啟網站測試連線。要設定多個 domain 就重複上述步驟繼續新增 Public hostname。</p> <h3 id="存取控管"><a href="#存取控管">存取控管</a></h3> <p>Cloudflare Access 可用於管理網頁存取權限，透過指定使用者的 email address，並藉由 Google、Github 等常見的 OAuth 管道，或是內建的 One-Time Pin 來進行驗證。</p> <p><!--[--><picture><source srcset="/_app/immutable/assets/04.uppiqBpS.avif 736w" type="image/avif"/> <img src="/posts/cloudflare-tunnel/images/04.png" alt="Cloudflare Access Login Page" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <h4 id="設定-login-methods"><a href="#設定-login-methods">設定 Login methods</a></h4> <p>首先選擇 Settings => Authentication => Login methods => Add new ，點選要使用的驗證方式，並照各平台的指示設定。</p> <p><!--[--><picture><source srcset="/_app/immutable/assets/05.Z48U10IT.avif 736w" type="image/avif"/> <img src="/posts/cloudflare-tunnel/images/05.png" alt="Setup login methods" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <h4 id="建立-application"><a href="#建立-application">建立 Application</a></h4> <p>設定完成後，我們需要將要做存取控管的 domain 加入到 Access 中，選擇 Access => Applications => Add application ，我們的 Tunnel 屬於 Self-hosted application ，選擇這個選項後填入 Application name(會顯示於登入頁面上) 及 domain，其他設定保持預設即可，並進入下一步。</p> <p><!--[--><picture><source srcset="/_app/immutable/assets/06.an1IKblB.avif 736w" type="image/avif"/> <img src="/posts/cloudflare-tunnel/images/06.png" alt="Setup access application" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <p>接下來我們需要設定 Access Policy，預設是 Allow(白名單) 模式，只有符合 Policy 的人可以存取，以下舉例只允許 <code>user1@example.com</code> 及 <code>user2@example.com</code> 存取，且存取國家必須是 Taiwan。</p> <p><!--[--><picture><source srcset="/_app/immutable/assets/07.CZFA5IzG.avif 736w" type="image/avif"/> <img src="/posts/cloudflare-tunnel/images/07.png" alt="Setup access policy" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <p>除了上述所用的條件外，還可以透過 IP range、是否於 Github Organization 中，甚至呼叫外部 API(External Evaluation) 來進行驗證，詳細可參考 <a href="https://developers.cloudflare.com/cloudflare-one/policies/access/" rel="nofollow noopener noreferrer external" target="_blank">Cloudflare Docs</a>。</p> <p>BTW，若要一次設定多個 email address，可在 My Team => Lists 建立 User Emails List，之後在 Policy 中選擇這個 List 即可。</p> <p>設定完後，下一步還有 CORS、Cookie 的詳細設定，這邊就不贅述，但 Additional settings 中有一個很強大的功能叫 Browser rendering，若這個 tunnel 的目標服務是 ssh 或是 vnc，開啟此功能後就可以直接在瀏覽器渲染 SSH Terminal 或是 VNC 畫面。</p> <p><!--[--><picture><source srcset="/_app/immutable/assets/08.Bzz2DMaW.avif 736w" type="image/avif"/> <img src="/posts/cloudflare-tunnel/images/08.png" alt="Browser rendering" class="rounded-lg my-2" loading="lazy" decoding="async"/></picture><!--]--><!----></p> <h3 id="結語"><a href="#結語">結語</a></h3> <p>在 Cloudflare Tunnel 推出前，原本自已的伺服器是走種花固定 IP 出去，並掛一層 Cloudflare proxy 來架 web server，但這樣畢竟還是有個公開 IP 在網路上晃，難免還是會被機器人掃到，很怕哪天被打爛。現在直接在機器上跑一個 cloudflared，全部連線都走 tunnel 出去，連固定 IP 都不需要，超級快樂。</p> <p>Cloudflare Tunnel 跟 Access 只是其中的一部分，還有許多騷操作可以玩，像是拿它來擋廣告、在多裝置建立虛擬私有網路等，後續會在寫幾篇文章來分享。</p><!--]--><!--]--><!--]--><!--]-->]]>
    </content>
    <category term="Cloudflare" scheme="https://blog.0xian.dev/?tags=Cloudflare" />
    <category term="Reverse Proxy" scheme="https://blog.0xian.dev/?tags=Reverse%20Proxy" />
    <category term="Access Control" scheme="https://blog.0xian.dev/?tags=Access%20Control" />
    <category term="Free" scheme="https://blog.0xian.dev/?tags=Free" />
  </entry>
  <entry>
    <title type="html"><![CDATA[Hello World!]]></title>
    <link href="https://blog.0xian.dev/posts/init-blog" />
    <id>https://blog.0xian.dev/posts/init-blog</id>
    <published>2024-03-01T00:00:00.000Z</published>
    <updated>2024-07-31T00:00:00.000Z</updated>
    <content type="html">
      <![CDATA[<!--[--><!--[--><!--[--><!--[--><p>轉眼間就<del>快大四</del>畢業了，感覺是時候開始寫點 blog 紀錄一下自己這幾年到底在幹嘛，還有練習一下寫作能力。</p> <p>希望我能夠一個月至少寫一篇 🥲</p> <p>To be continued…</p><!--]--><!--]--><!--]--><!--]-->]]>
    </content>
  </entry>
</feed>