<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Ice</title>
    <description>The latest articles on DEV Community by Ice (@icelimited).</description>
    <link>https://dev.to/icelimited</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F357164%2Ff8138d55-c522-4d5a-bf5d-169a8a182a87.jpg</url>
      <title>DEV Community: Ice</title>
      <link>https://dev.to/icelimited</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/icelimited"/>
    <language>en</language>
    <item>
      <title>อ่านบิล AWS ด้วย Usage Type: 6 บรรทัดที่รั่วเงียบๆ ที่ Cost Explorer ระดับ Service ไม่ยอมบอก</title>
      <dc:creator>Ice</dc:creator>
      <pubDate>Fri, 30 Oct 2026 13:01:17 +0000</pubDate>
      <link>https://dev.to/icelimited/aanbil-aws-dwy-usage-type-6-brrthadthiirawengiiyb-thii-cost-explorer-radab-service-aimymbk-3g4e</link>
      <guid>https://dev.to/icelimited/aanbil-aws-dwy-usage-type-6-brrthadthiirawengiiyb-thii-cost-explorer-radab-service-aimymbk-3g4e</guid>
      <description>&lt;p&gt;ตอนเปิด Cost Explorer แล้วเห็นกราฟ EC2 กับ RDS โตเป็นแท่งใหญ่ ใครๆ ก็รู้ว่าต้องไปดูตรงนั้นก่อน ปัญหาคือเงินที่รั่วจริงๆ มักไม่ได้อยู่ในแท่งใหญ่ มันซ่อนอยู่ในบรรทัดเล็กๆ ที่ชื่อฟังดูไม่น่ากลัว เช่น "Data Transfer" หรือ "EC2-Other" ซึ่งเป็นถังรวมที่ยัดสารพัดอย่างไว้ข้างใน&lt;/p&gt;

&lt;p&gt;บทความนี้เล่าวิธีที่ใช้จริงเวลาไล่บิล: เลิกดูระดับ Service แล้วเจาะลงไปที่ &lt;strong&gt;Usage Type&lt;/strong&gt; ทีละบรรทัด พร้อม 6 จุดที่เจอบ่อยที่สุด ตัวเลขราคาทั้งหมดเป็น list price ของ us-east-1 ณ ตอนเขียน (ราคาเปลี่ยนได้ และแต่ละ region ไม่เท่ากัน เช็คหน้า pricing ของ AWS อีกทีก่อนเอาไปคำนวณ) ส่วนตัวเลขบิลจริงของที่ไหนไม่เอามาใส่ทั้งนั้น&lt;/p&gt;

&lt;h2&gt;
  
  
  ทำไมระดับ Service ถึงไม่พอ
&lt;/h2&gt;

&lt;p&gt;"EC2-Other" ใน Cost Explorer ไม่ใช่ instance นะ มันคือทุกอย่างที่เกี่ยวกับ EC2 แต่ไม่ใช่ค่า instance: EBS volume, snapshot, NAT Gateway, Elastic IP, data transfer ข้าม AZ ทั้งหมดรวมอยู่ในก้อนเดียว ถ้าก้อนนี้โต คุณไม่มีทางรู้เลยว่าเป็นเพราะอะไร จนกว่าจะแตกมันออกเป็น Usage Type&lt;/p&gt;

&lt;p&gt;วิธีแตกที่เร็วที่สุดคือใน Cost Explorer เลือก Group by เป็น &lt;strong&gt;Usage Type&lt;/strong&gt; แล้ว filter Service เป็น "EC2-Other" ถ้าชอบ CLI:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;aws ce get-cost-and-usage &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--time-period&lt;/span&gt; &lt;span class="nv"&gt;Start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2026-09-01,End&lt;span class="o"&gt;=&lt;/span&gt;2026-10-01 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--granularity&lt;/span&gt; MONTHLY &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--metrics&lt;/span&gt; UnblendedCost &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--filter&lt;/span&gt; &lt;span class="s1"&gt;'{"Dimensions":{"Key":"SERVICE","Values":["EC2 - Other"]}}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--group-by&lt;/span&gt; &lt;span class="nv"&gt;Type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;DIMENSION,Key&lt;span class="o"&gt;=&lt;/span&gt;USAGE_TYPE &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--query&lt;/span&gt; &lt;span class="s1"&gt;'ResultsByTime[0].Groups[].[Keys[0],Metrics.UnblendedCost.Amount]'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--output&lt;/span&gt; text | &lt;span class="nb"&gt;sort&lt;/span&gt; &lt;span class="nt"&gt;-k2&lt;/span&gt; &lt;span class="nt"&gt;-nr&lt;/span&gt; | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-15&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;หมายเหตุเล็กๆ: Cost Explorer API คิดเงินต่อ request (ราว $0.01 ต่อครั้ง) ไม่ต้องยิงรัวใน loop แต่ถ้าครั้งสองครั้งต่อเดือน ไม่ถึงกับเจ็บ&lt;/p&gt;

&lt;p&gt;พอได้ลิสต์ Usage Type แล้ว ลองไล่ดูหกตัวนี้ก่อน&lt;/p&gt;

&lt;h2&gt;
  
  
  1. NatGateway-Bytes: ค่าประมวลผลต่อ GB ที่ไม่มีใครมอง
&lt;/h2&gt;

&lt;p&gt;NAT Gateway คิดสองก้อน คือค่าชั่วโมง (&lt;code&gt;NatGateway-Hours&lt;/code&gt;) กับค่าประมวลผลข้อมูลต่อ GB (&lt;code&gt;NatGateway-Bytes&lt;/code&gt;, ราว $0.045 ต่อ GB) ก้อนหลังคือตัวที่โตเงียบๆ เพราะทุก byte ที่ผ่าน NAT โดนนับหมด ไม่ว่าจะไปไหน&lt;/p&gt;

&lt;p&gt;ตัวการที่เจอบ่อยคือ traffic ไป S3 และ DynamoDB ที่ไหลผ่าน NAT ทั้งที่ไม่จำเป็น เพราะ &lt;strong&gt;Gateway endpoint ของ S3 กับ DynamoDB ไม่มีค่าใช้จ่าย&lt;/strong&gt; และ pod ที่ pull image จาก ECR ก็เหมือนกัน layer จริงๆ อยู่ใน S3 ใส่ S3 gateway endpoint ใน route table ของ private subnet ก้อนใหญ่หายไปได้เลย&lt;/p&gt;

&lt;p&gt;ส่วน interface endpoint (เช่นของ ECR API, STS) ไม่ฟรี คิดต่อ AZ ต่อชั่วโมงบวกค่า data ต้องคำนวณก่อนว่าคุ้มกว่า NAT หรือเปล่า ไม่ใช่ใส่ทุกตัวตามกระแส&lt;/p&gt;

&lt;h2&gt;
  
  
  2. PublicIPv4:InUseAddress และ IdleAddress: IP ที่เคยฟรีกลายเป็นบิล
&lt;/h2&gt;

&lt;p&gt;ตั้งแต่ AWS เริ่มคิดค่า public IPv4 ทุกตัว (ตัวละ $0.005 ต่อชั่วโมง ราว $3.65 ต่อเดือน) ไม่ว่าจะใช้งานอยู่หรือไม่ก็ตาม บรรทัดนี้โผล่ในบิลของแทบทุกคน Usage Type จะแยกเป็น &lt;code&gt;PublicIPv4:InUseAddress&lt;/code&gt; กับ &lt;code&gt;PublicIPv4:IdleAddress&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;ที่ควรเช็คตามลำดับ:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Elastic IP ที่ไม่ได้ผูกกับอะไร (Idle) ลบได้เลย&lt;/li&gt;
&lt;li&gt;ENI ที่ค้างอยู่หลังลบ resource ที่เคยใช้มัน&lt;/li&gt;
&lt;li&gt;EC2 ใน public subnet ที่จริงๆ ไม่ต้องมี IP ตัวเอง ย้ายไปหลัง load balancer ได้&lt;/li&gt;
&lt;li&gt;ALB เองก็ใช้ public IP ต่อ AZ นะ อย่าลืมนับด้วย&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ข้อควรระวัง: ถ้าย้ายของไปหลัง NAT เพื่อประหยัด IP ให้กลับไปดูข้อ 1 ด้วย เดี๋ยวประหยัดค่า IP แล้วไปเสียค่า NAT แทน&lt;/p&gt;

&lt;h2&gt;
  
  
  3. DataTransfer-Regional-Bytes: ค่า traffic ข้าม AZ
&lt;/h2&gt;

&lt;p&gt;ข้อนี้ทำให้คนงงบ่อยที่สุด traffic ระหว่าง AZ ใน region เดียวกันคิดเงินทั้งสองฝั่ง (ราว $0.01 ต่อ GB ขาออกและ $0.01 ต่อ GB ขาเข้า รวมเป็น $0.02 ต่อ GB ต่อการส่งหนึ่งครั้ง) ฟังดูเล็ก แต่ cluster ที่ service คุยกันถี่ๆ กระจายอยู่หลาย AZ จะสะสมเร็วมาก&lt;/p&gt;

&lt;p&gt;จุดที่ต้องดู: service ที่คุยกันเยอะแต่ถูก schedule คนละ AZ, ระบบ messaging หรือ database replication ข้าม AZ, และ log shipper ที่ส่งข้ามไปรวมศูนย์ ใน Kubernetes ลองใช้ topology aware routing (ให้ Service ส่ง traffic ไปที่ endpoint ใน AZ เดียวกันก่อน) หรือ &lt;code&gt;topologySpreadConstraints&lt;/code&gt; ที่คิดมาแล้วว่า service คู่ไหนควรอยู่ใกล้กัน แลกกับความ resilient ที่ลดลงนิดหน่อย ซึ่งต้องตัดสินใจเป็นรายระบบ ไม่ใช่เปิดทุกที่&lt;/p&gt;

&lt;h2&gt;
  
  
  4. DataProcessing-Bytes ของ CloudWatch Logs: ค่า ingest
&lt;/h2&gt;

&lt;p&gt;CloudWatch Logs คิดค่า ingest ราว $0.50 ต่อ GB (ราคาที่แพงกว่าที่คนส่วนใหญ่นึก) และ retention ค่าเริ่มต้นคือ &lt;strong&gt;Never expire&lt;/strong&gt; บรรทัดนี้โตเงียบๆ จากสาเหตุเดิมซ้ำๆ: service ยิง DEBUG ใน production ค้างไว้, log ของ health check ทุก 10 วินาที, หรือ log แบบ JSON ก้อนใหญ่ที่ไม่มีใครอ่าน&lt;/p&gt;

&lt;p&gt;ไล่แก้ตามนี้: ตั้ง retention ทุก log group (หลักวันถึงหลักเดือนตามที่ต้องใช้จริง), ปรับ log level, กรอง health check ออกที่ต้นทาง และสำหรับ log ที่ต้องเก็บยาวแต่ไม่ค่อยอ่าน พิจารณา Infrequent Access log class ที่ราคา ingest ถูกลง (แลกกับฟีเจอร์ที่ลดลง เช็คข้อจำกัดก่อนใช้)&lt;/p&gt;

&lt;h2&gt;
  
  
  5. VolumeUsage.gp2 และ snapshot ที่ไม่มีเจ้าของ
&lt;/h2&gt;

&lt;p&gt;EBS gp3 ถูกกว่า gp2 ราว 20% ต่อ GB ($0.08 เทียบกับ $0.10) และได้ baseline 3,000 IOPS ทุกขนาด แถมเปลี่ยนจาก gp2 เป็น gp3 ได้แบบ online ด้วย &lt;code&gt;modify-volume&lt;/code&gt; ไม่ต้อง detach ที่เจอบ่อยคือ module Terraform เก่าที่ก๊อปต่อกันมายังตั้ง default เป็น gp2 อยู่ แก้ที่ default ใน IaC ด้วย ไม่งั้นของใหม่ก็ยังเป็น gp2 ต่อไป&lt;/p&gt;

&lt;p&gt;อีกก้อนคือ snapshot ที่ค้างอยู่หลัง AMI ถูก deregister ซึ่ง AWS ไม่ลบให้ ลองไล่ดู snapshot ที่ไม่มี AMI หรือ volume ต้นทางอ้างถึงแล้ว ตั้ง lifecycle policy (Data Lifecycle Manager) ให้ลบเองตามอายุ&lt;/p&gt;

&lt;h2&gt;
  
  
  6. ELB ที่ไม่มี traffic
&lt;/h2&gt;

&lt;p&gt;Load balancer คิดค่าชั่วโมงตลอดเวลาที่มันอยู่ ต่อให้ไม่มี request สักตัว ของที่เจอบ่อยคือ ALB/NLB ที่เหลือจาก environment ที่เคยทดลองแล้วไม่ได้ลบ เช็คจาก metric &lt;code&gt;RequestCount&lt;/code&gt; หรือ &lt;code&gt;ActiveFlowCount&lt;/code&gt; ใน CloudWatch ย้อนหลัง 30 วัน ถ้าเป็นศูนย์ตลอด ถามเจ้าของก่อน แล้วค่อยลบ&lt;/p&gt;

&lt;h2&gt;
  
  
  จัดลำดับก่อนลงมือ
&lt;/h2&gt;

&lt;p&gt;ไล่ครบหกข้อแล้วอย่าเพิ่งแก้ทุกอย่างพร้อมกัน เรียงตามเงินที่ได้คืนต่อแรงที่เสีย แล้วเริ่มจากของที่ไม่เสี่ยงก่อน:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;ตั้ง retention ให้ log group และลบ Elastic IP / snapshot / ELB ที่ไม่มีใครใช้ (เสี่ยงต่ำ ได้เงินคืนเร็ว)&lt;/li&gt;
&lt;li&gt;ใส่ S3 gateway endpoint และเปลี่ยน default เป็น gp3 ใน IaC&lt;/li&gt;
&lt;li&gt;ค่อยพิจารณา cross-AZ กับ interface endpoint ที่ต้องคำนวณและมี trade-off เรื่อง resilience&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;ก่อนลบอะไรที่ไม่ใช่ของตัวเอง ให้ดูก่อนว่ามี tag เจ้าของไหม ถ้าไม่มี ถามในช่องทีมก่อน และเปิด cost allocation tag ไว้ตั้งแต่วันนี้ เพราะเดือนหน้าคุณจะอยากรู้ว่าบรรทัดไหนเป็นของทีมไหน&lt;/p&gt;

&lt;h2&gt;
  
  
  สรุป
&lt;/h2&gt;

&lt;p&gt;Cost Explorer ระดับ Service บอกว่า "เงินไปไหน" ได้แค่ครึ่งเดียว ครึ่งที่เหลืออยู่ใน Usage Type ซึ่งใช้เวลาอ่านไม่ถึงครึ่งชั่วโมงต่อเดือน ลองตั้งเป็นกิจวัตรสิ้นเดือน เปิดกราฟ group by Usage Type เรียงจากมากไปน้อย แล้วถามตัวเองว่า "บรรทัดนี้ ใครเป็นเจ้าของ แล้วเรารู้หรือเปล่าว่ามันควรอยู่เท่านี้" แค่นี้ก็จับของรั่วได้ก่อนมันกลายเป็นบิลก้อนโต 555&lt;/p&gt;

&lt;p&gt;ตัวเลขราคาในบทความเป็น list price โดยประมาณของ us-east-1 ราคาจริงของคุณอาจต่างตาม region, ส่วนลด หรือสัญญา ตรวจกับหน้า pricing และ pricing calculator ของ AWS ก่อนตัดสินใจทุกครั้ง&lt;/p&gt;

</description>
      <category>finops</category>
      <category>aws</category>
      <category>costoptimization</category>
      <category>cloudcost</category>
    </item>
    <item>
      <title>MCP server ในมือ agent คือ "คำสั่งที่รันบนเครื่องคุณ"</title>
      <dc:creator>Ice</dc:creator>
      <pubDate>Fri, 30 Oct 2026 12:59:49 +0000</pubDate>
      <link>https://dev.to/icelimited/mcp-server-ainmuue-agent-khuue-khamsangthiiranbnekhruuengkhun-1539</link>
      <guid>https://dev.to/icelimited/mcp-server-ainmuue-agent-khuue-khamsangthiiranbnekhruuengkhun-1539</guid>
      <description>&lt;p&gt;สัปดาห์ที่แล้วมีข่าวหนึ่งที่ผมอ่านแล้วต้องวางกาแฟ: OpenCode (coding agent ตัวหนึ่ง) มีช่องโหว่ที่ทำให้ MCP server ที่ไม่น่าไว้ใจรันคำสั่งบนเครื่องนักพัฒนาได้ แค่ตอนกด sign-in ผ่าน OAuth ไม่ต้องให้ agent เรียก tool อะไรเลยด้วยซ้ำ&lt;/p&gt;

&lt;p&gt;ข่าวนี้ไม่ใช่เรื่องของ OpenCode ตัวเดียว มันชี้ให้เห็นสิ่งที่ทีม ops/security หลายทีมยังมองข้าม: &lt;strong&gt;การเพิ่ม MCP server เข้า agent ไม่ต่างจากการ &lt;code&gt;curl | bash&lt;/code&gt; ของเวอร์ชันสมัยใหม่&lt;/strong&gt; บทความนี้จะเล่าข่าวก่อน แล้วค่อยไล่ว่าถ้าจะรัน agent กับ MCP ใน infra จริง ควรวางด่านอะไรบ้าง ทั้งหมดเป็นมุม defensive ไม่มี exploit และตัวอย่าง config เป็นของสมมติทั้งหมด&lt;/p&gt;

&lt;h2&gt;
  
  
  ข่าวที่ 1: OpenCode และ OAuth sign-in ของ MCP server
&lt;/h2&gt;

&lt;p&gt;ตามรายงานของ The Hacker News (เผยแพร่ 22 ตุลาคม 2026) บริษัท Pillar Security พบว่า OpenCode เวอร์ชัน 1.0.137 ถึง 1.18.32 บน &lt;strong&gt;Windows และ WSL เท่านั้น&lt;/strong&gt; (macOS กับ Linux ไม่โดน) มีปัญหาตอนทำ OAuth กับ remote MCP server&lt;/p&gt;

&lt;p&gt;กลไกคือ remote MCP server เป็นคนส่ง "authorization endpoint" URL กลับมา แล้ว OpenCode เอา URL นั้นไปเปิดเบราว์เซอร์ผ่าน helper ที่ประกอบคำสั่ง PowerShell โดยไม่ escape ผลคือ URL ที่ถูกออกแบบมาให้มีคำสั่ง PowerShell แฝง จะถูกรันด้วยสิทธิ์ของนักพัฒนาคนนั้นเลย ที่น่าสนใจคือ URL ยังหน้าตาเป็น HTTPS ธรรมดา เช็คแค่ว่าขึ้นต้นด้วย http หรือ https จึงกันไม่ได้&lt;/p&gt;

&lt;p&gt;รายละเอียดที่ควรรู้ตามรายงาน: Pillar แจ้งผู้พัฒนาแบบ private เมื่อ 29 สิงหาคม ทีมแก้ในเวอร์ชัน 1.18.33 ซึ่งออกวันที่ 28 กันยายน และเปิดเผยต่อสาธารณะ 19 ตุลาคม Pillar ให้คะแนนความรุนแรงที่เสนอไว้ 9.8 จาก 10 ไม่มี CVE (ผู้พัฒนาไม่ได้ขอ) ซึ่งแปลว่า scanner ที่ไล่ดูแต่ CVE จะ &lt;strong&gt;ไม่เห็น&lt;/strong&gt; เคสนี้เลย และรายงานระบุว่ายังไม่พบการโจมตีจริง&lt;/p&gt;

&lt;p&gt;บทเรียนข้อแรกจึงเป็นเรื่องที่ไม่เกี่ยวกับ MCP โดยตรง: ถ้า inventory ของคุณพึ่ง CVE feed อย่างเดียว คุณจะพลาดช่องโหว่ในเครื่องมือฝั่ง developer ที่ไม่มีเลข CVE ต้องมีรายการ "agent/CLI ที่ใช้อยู่ในองค์กรและเวอร์ชัน" แยกต่างหาก&lt;/p&gt;

&lt;h2&gt;
  
  
  ข่าวที่ 2: ปัญหาเชิงระบบของ STDIO transport (OX Security)
&lt;/h2&gt;

&lt;p&gt;อีกชิ้นที่ควรอ่านคู่กันคือ advisory ของ OX Security ที่เผยแพร่ 15 เมษายน 2026 (ไม่ใช่ข่าวสดแต่ยังเกี่ยวข้อง และถูกอ้างถึงต่อเนื่อง) ประเด็นหลักคือ MCP แบบ STDIO ให้ผู้ใช้กำหนด &lt;code&gt;command&lt;/code&gt; กับ &lt;code&gt;args&lt;/code&gt; ซึ่งถูกส่งไปรันเป็น subprocess ตรงๆ โดยไม่มี allowlist หรือการ sanitize ใน SDK เอง OX จึงเรียกว่าเป็นช่องโหว่ระบบ (systemic) ที่ไหลไปอยู่ในหลายผลิตภัณฑ์&lt;/p&gt;

&lt;p&gt;ผลิตภัณฑ์ที่ OX ระบุว่าได้รับผลมีหลายตัว ตัวที่คนในวงการ AI infra น่าจะคุ้นคือ LiteLLM ซึ่งมี CVE-2026-30623 (ต้อง authenticate, แก้แล้วใน v1.83.6-nightly และ v1.83.7-stable) รวมถึง LangFlow, Flowise, Windsurf และอื่นๆ อีกหลายตัว&lt;/p&gt;

&lt;p&gt;อีกส่วนที่น่าคิดคือจุดยืนของผู้ให้บริการ OX ระบุว่าหลายเจ้าตอบว่าพฤติกรรมนี้ "by design" และชี้ว่า sandbox เช่น Docker ช่วยลดความเสี่ยงได้ ส่วน OX แย้งว่า sandbox ก็ยังโดนใช้ขุดเหรียญหรือเป็น proxy ได้ ผมไม่ตัดสินว่าฝั่งไหนถูก เพราะจากมุม ops สิ่งที่ต้องรู้คือ &lt;strong&gt;คุณคาดหวังให้ upstream ปิดช่องนี้ให้ไม่ได้&lt;/strong&gt; ต้องออกแบบด่านของตัวเอง&lt;/p&gt;

&lt;h2&gt;
  
  
  ทำไม MCP ถึงเป็นโจทย์ ops ไม่ใช่แค่โจทย์ dev
&lt;/h2&gt;

&lt;p&gt;ลองนึกภาพ MCP server เป็น 3 อย่างพร้อมกัน&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;โปรแกรมที่รันบนเครื่อง&lt;/strong&gt; (STDIO แบบ &lt;code&gt;npx some-mcp-server&lt;/code&gt; คือดาวน์โหลดแพ็กเกจแล้วรันทันที)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ผู้ให้ข้อความที่ LLM จะอ่านและเชื่อ&lt;/strong&gt; (tool description, ผลลัพธ์ของ tool คือ input เข้า prompt)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ตัวถือสิทธิ์&lt;/strong&gt; (token ของ GitHub, Jira, cloud API ที่ใส่ไว้ให้ tool ใช้)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;ทั้งสามอย่างนี้ปกติทีม ops ดูแลแยกกัน: ด่านของ software supply chain, ด่านของ prompt injection, ด่านของ secret management พอมาอยู่ในก้อนเดียวคือ "MCP server" มันเลยตกหล่นระหว่างทีม ไม่มีใครเป็นเจ้าของ&lt;/p&gt;

&lt;h2&gt;
  
  
  6 ด่านที่ควรวางก่อนปล่อย agent เข้า infra จริง
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F89gb12qeybm7wwtr020f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F89gb12qeybm7wwtr020f.png" alt="checklist 6 ด่านคุม MCP" width="800" height="655"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  ด่านที่ 1: ทะเบียน MCP server ที่อนุมัติแล้ว (allowlist ระดับองค์กร)
&lt;/h3&gt;

&lt;p&gt;ใครจะเพิ่ม MCP server ต้องผ่านรีวิวเหมือนเพิ่ม dependency ใหม่ เก็บเป็นไฟล์ใน repo ที่มี owner ชัดเจน เช่น&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# mcp-registry.yaml (ตัวอย่างสมมติ)&lt;/span&gt;
&lt;span class="na"&gt;servers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;internal-docs-search&lt;/span&gt;
    &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;streamable-http&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://mcp.example.internal/docs&lt;/span&gt;
    &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;platform-team&lt;/span&gt;
    &lt;span class="na"&gt;scopes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;read&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;reviewed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-10-01&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ticket-reader&lt;/span&gt;
    &lt;span class="na"&gt;transport&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;stdio&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/opt/mcp/bin/ticket-reader&lt;/span&gt;   &lt;span class="c1"&gt;# pin path, ไม่ใช้ npx&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--readonly"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sre-team&lt;/span&gt;
    &lt;span class="na"&gt;reviewed&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2026-09-20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;จุดที่ผมย้ำกับตัวเองคือ &lt;strong&gt;allowlist ต้องเช็คทั้ง command และ args&lt;/strong&gt; จาก advisory ของ OX จะเห็นว่ามีเคสที่ allowlist ชื่อคำสั่งอย่าง &lt;code&gt;npx&lt;/code&gt; แต่ผู้ใช้ยัดอาร์กิวเมนต์ที่ทำให้มันรันคำสั่งอื่นได้ ถ้าอนุญาตแค่ "npx" เท่ากับไม่ได้อนุญาตอะไรเลย&lt;/p&gt;

&lt;h3&gt;
  
  
  ด่านที่ 2: ตรึงเวอร์ชันและที่มา (pin + verify)
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;npx -y some-mcp-server@latest&lt;/code&gt; ในไฟล์ config ของ agent คือการให้ใครก็ตามที่ครอบครอง package นั้นรันโค้ดบนเครื่องคุณทุกครั้งที่ agent เริ่ม ให้ pin เวอร์ชันเป็นเลขตายตัว หรือดีกว่านั้นคือ build เป็น image/binary ภายในแล้วอ้างด้วย digest&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# ดูว่าเครื่อง dev ในทีมมี MCP config อะไรอยู่บ้าง (ตัวอย่างหาแบบหยาบ)&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rn&lt;/span&gt; &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"*.json"&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'"(command|url)"\s*:'&lt;/span&gt; ~/.config 2&amp;gt;/dev/null | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;คำสั่งข้างบนเป็นแค่จุดเริ่ม ในองค์กรจริงควรให้ MDM หรือ endpoint tooling เก็บ inventory แทน&lt;/p&gt;

&lt;h3&gt;
  
  
  ด่านที่ 3: แยก identity ของ agent ออกจากคน
&lt;/h3&gt;

&lt;p&gt;อย่าให้ agent ใช้ token ส่วนตัวของนักพัฒนา สร้าง service identity แยก ให้สิทธิ์เท่าที่ tool ต้องใช้ ถ้า tool แค่อ่านตั๋ว ก็ควรได้ token แบบ read-only เหมือนที่เราทำกับ IRSA บน EKS: role ต่อ workload ไม่ใช่ role กลางที่ทุกอย่างแชร์กัน&lt;/p&gt;

&lt;p&gt;ถ้าเป็นฝั่ง cloud ให้ใช้ credential อายุสั้น (STS, workload identity) แทน long-lived key และอย่าใส่ secret ลงในไฟล์ config ของ MCP ตรงๆ ให้ดึงจาก secret manager ตอน runtime&lt;/p&gt;

&lt;h3&gt;
  
  
  ด่านที่ 4: Sandbox ที่ปิดเน็ตและ filesystem โดยค่าเริ่มต้น
&lt;/h3&gt;

&lt;p&gt;MCP server แบบ STDIO ที่ต้องรันในเครื่องเดียวกับ agent ให้รันใน container หรือ sandbox ที่&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;filesystem เป็น read-only ยกเว้น directory ทำงานที่กำหนด&lt;/li&gt;
&lt;li&gt;ไม่มีสิทธิ์ root และ drop capability ที่ไม่จำเป็น&lt;/li&gt;
&lt;li&gt;egress ถูกจำกัดเหลือ domain ที่ tool ต้องคุยจริง
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# ตัวอย่าง pod spec สมมติสำหรับ MCP server ที่รันใน cluster&lt;/span&gt;
&lt;span class="na"&gt;securityContext&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;runAsNonRoot&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;readOnlyRootFilesystem&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;allowPrivilegeEscalation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
  &lt;span class="na"&gt;capabilities&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;drop&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ALL"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ตรงนี้ขอยอมรับฝั่ง "sandbox ช่วยได้" ที่เหล่าผู้ให้บริการอ้าง: มันช่วยลด blast radius จริง แต่ OX ก็พูดถูกว่ามันไม่ได้ทำให้ปลอดภัย ถ้า container ออกเน็ตได้เต็มที่ ก็ยังถูกใช้ทำอะไรแปลกๆ ได้ ต้องมีทั้งสองอย่าง: sandbox คู่กับ egress policy&lt;/p&gt;

&lt;h3&gt;
  
  
  ด่านที่ 5: Human-in-the-loop เฉพาะ action ที่ย้อนกลับไม่ได้
&lt;/h3&gt;

&lt;p&gt;ไม่ต้องให้คนกดอนุมัติทุก tool call เพราะสุดท้ายคนจะกด approve รัวจนไร้ความหมาย (approval fatigue) แบ่งเป็นระดับ&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;อ่านอย่างเดียว&lt;/strong&gt; (list, get, search): รันได้เลย แต่ log&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;เขียนที่ย้อนกลับได้&lt;/strong&gt; (สร้างตั๋ว, comment): รันได้ แต่มีโควตาต่อชั่วโมง&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ย้อนกลับไม่ได้หรือกระทบ production&lt;/strong&gt; (delete, deploy, เปลี่ยน IAM, ส่งข้อความออกนอกองค์กร): ต้องมีคนยืนยันทุกครั้ง&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ด่านที่ 6: Audit log ที่ตอบได้ว่า "ใคร สั่งอะไร ผ่าน agent ตัวไหน ได้ผลอะไร"
&lt;/h3&gt;

&lt;p&gt;log ที่มีแค่ "agent เรียก tool X" ไม่พอตอนเกิดเหตุ ควรเก็บอย่างน้อย: identity ของ agent, ผู้ใช้ที่ต้นเรื่อง, ชื่อ MCP server และเวอร์ชัน, ชื่อ tool, พารามิเตอร์ (mask ค่า secret), เวลา, ผลลัพธ์ (สำเร็จ/ล้มเหลว) และส่งเข้าที่เดียวกับ log security อื่นๆ จะได้ค้นข้ามกันได้ เช่น ตอนจะตอบว่า "token ที่รั่วถูกใช้ผ่าน tool ไหนบ้าง"&lt;/p&gt;

&lt;h2&gt;
  
  
  แล้วข่าว OpenCode สอนอะไรเพิ่ม
&lt;/h2&gt;

&lt;p&gt;กลับไปที่เคส OAuth ของ OpenCode มีบทเรียนที่ผมว่าใช้ได้กับทุก agent&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ทุกค่าที่มาจาก MCP server ต้องถือว่าไม่น่าไว้ใจ ไม่ใช่แค่ผลลัพธ์ของ tool&lt;/strong&gt; URL สำหรับ OAuth, ชื่อ tool, description, metadata ล้วนเป็น input จากภายนอก ถ้าโค้ดฝั่ง client เอาไปประกอบเป็นคำสั่ง shell ก็จบเหมือนกัน นี่คือ command injection แบบคลาสสิกที่ใส่ชุดใหม่&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ช่วงเวลาที่เสี่ยงที่สุดคือตอนเชื่อมต่อครั้งแรก&lt;/strong&gt; ไม่ใช่ตอนใช้งาน ดังนั้นกฎ "ห้ามต่อ remote MCP server ที่ไม่อยู่ในทะเบียน" ต้องบังคับตั้งแต่ขั้น sign-in ไม่ใช่รอดูตอนเรียก tool&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;อัปเดตเครื่องมือ developer ให้เป็นกิจวัตรเหมือน patch OS&lt;/strong&gt; เคสนี้แก้ออกมาตั้งแต่ 28 กันยายน แต่เปิดเผยเมื่อ 19 ตุลาคม คนที่อัปเดตตามปกติปลอดภัยก่อนข่าวออกสามสัปดาห์ คนที่ปล่อยเวอร์ชันเก่าไว้ก็เพิ่งรู้ตอนอ่านข่าว&lt;/p&gt;

&lt;h2&gt;
  
  
  เช็คลิสต์เริ่มต้นสำหรับสัปดาห์นี้
&lt;/h2&gt;

&lt;p&gt;ถ้าไม่มีเวลาทำครบทั้ง 6 ด่าน ลองเริ่มจากสามอย่างนี้ซึ่งใช้แรงน้อยแต่ได้ผลมาก&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;ทำ inventory ว่าในทีมใช้ agent/CLI ตัวไหน เวอร์ชันอะไร (รวมตัวที่ไม่มี CVE) แล้วเช็คกับข่าวล่าสุด เช่น ถ้ามีคนใช้ OpenCode บน Windows/WSL ให้ยืนยันว่าเป็น 1.18.33 ขึ้นไป&lt;/li&gt;
&lt;li&gt;ค้นหา &lt;code&gt;npx ... @latest&lt;/code&gt; หรือคำสั่ง STDIO ที่ไม่ได้ pin ในไฟล์ config ของ MCP แล้ว pin ให้หมด&lt;/li&gt;
&lt;li&gt;ถอด token ส่วนตัวออกจาก config ของ agent แล้วย้ายไปใช้ identity แยกที่สิทธิ์น้อยที่สุด&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  ปิดท้าย
&lt;/h2&gt;

&lt;p&gt;ผมไม่ได้บอกว่าอย่าใช้ MCP มันเป็นของที่มีประโยชน์จริง ทุกวันนี้ agent ที่คุยกับระบบ ticket, เอกสาร และ log ได้ช่วยลดงานจุกจิกไปเยอะ แต่เราควรคุยกันตรงๆ ว่ามันคือ "โค้ดจากภายนอกที่ได้สิทธิ์ของเรา" และปฏิบัติกับมันเท่ากับ dependency ที่ผ่านการรีวิว ไม่ใช่ปลั๊กอินที่ติดตั้งตามบล็อก&lt;/p&gt;

&lt;p&gt;ครั้งหน้าที่มีคนในทีมบอกว่า "ผมเพิ่มเซิร์ฟเวอร์ MCP ตัวใหม่เข้า agent แล้วนะ" ลองถามกลับสามคำถาม: ใครเป็นเจ้าของ, มันรันด้วยสิทธิ์ของใคร, และถ้ามันทำอะไรแปลกๆ เราจะรู้จาก log ตรงไหน ถ้าตอบไม่ได้ครบ ก็ยังไม่ถึงเวลาเข้า production ครับ&lt;/p&gt;

&lt;h2&gt;
  
  
  แหล่งอ้างอิง
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The Hacker News, "OpenCode Flaw Lets Malicious MCP Servers Execute Code During Sign-In on Windows and WSL" (22 ต.ค. 2026): &lt;a href="https://thehackernews.com/2026/10/opencode-flaw-lets-malicious-mcp.html" rel="noopener noreferrer"&gt;https://thehackernews.com/2026/10/opencode-flaw-lets-malicious-mcp.html&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;OX Security, "MCP Supply Chain Advisory: RCE Vulnerabilities Across the AI Ecosystem" (15 เม.ย. 2026): &lt;a href="https://www.ox.security/blog/mcp-supply-chain-advisory-rce-vulnerabilities-across-the-ai-ecosystem/" rel="noopener noreferrer"&gt;https://www.ox.security/blog/mcp-supply-chain-advisory-rce-vulnerabilities-across-the-ai-ecosystem/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;หมายเหตุ: รายละเอียดช่องโหว่ในบทความนี้สรุปจากรายงานสาธารณะข้างต้น ตัวอย่าง config ทั้งหมดเป็นของสมมติ ไม่ใช่ข้อมูลระบบจริงของใคร&lt;/p&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>security</category>
    </item>
    <item>
      <title>GhostAction กลับมาอีกรอบ: เมื่อ workflow ชื่อ "security-audit" คือตัวขโมย secret และสิ่งที่ทีม DevSecOps ควรเช็คเดี๋ยวนี้</title>
      <dc:creator>Ice</dc:creator>
      <pubDate>Mon, 12 Oct 2026 03:34:53 +0000</pubDate>
      <link>https://dev.to/icelimited/ghostaction-klabmaaiikrb-emuue-workflow-chuue-security-audit-khuuetawkhomy-secret-aelasingthiithiim-k65</link>
      <guid>https://dev.to/icelimited/ghostaction-klabmaaiikrb-emuue-workflow-chuue-security-audit-khuuetawkhomy-secret-aelasingthiithiim-k65</guid>
      <description>&lt;p&gt;เช้าวันจันทร์ เปิดข่าวมาเจอเรื่องนี้เลยต้องวางกาแฟแล้วไปไล่เช็ค repo ตัวเองก่อน เรื่องคือแคมเปญ GhostAction ที่เคยเป็นข่าวเมื่อกันยายนปีที่แล้ว กลับมาอีกรอบ รอบนี้ The Hacker News รายงานเมื่อ 9 ตุลาคม 2026 ว่ามี workflow ไฟล์หนึ่งถูกยัดเข้าไปใน repository เป็นหลักหมื่น และเป้าหมายของมันคือสิ่งเดียวที่ทีม CI/CD ทุกทีมมีเหมือนกัน นั่นคือ secret ที่เก็บไว้ใน GitHub Actions&lt;/p&gt;

&lt;p&gt;บทความนี้จะแยกเป็นสามส่วน: ข่าวมันเกิดอะไรขึ้นจริง (เท่าที่แหล่งข่าวยืนยัน), ทำไมวิธีนี้ถึงได้ผลกับทีมที่คิดว่าตัวเอง "ก็ตั้งค่าปลอดภัยแล้ว", และถ้าเป็นเราต้องทำอะไรบ้างทั้งวันนี้และระยะยาว ทุกอย่างในนี้เป็นแนวป้องกัน ไม่มีขั้นตอนโจมตี&lt;/p&gt;

&lt;h2&gt;
  
  
  ข่าวคืออะไร
&lt;/h2&gt;

&lt;p&gt;ข้อมูลหลักมาจากรายงานของ The Hacker News อ้าง StepSecurity, Socket และ GitGuardian สรุปเฉพาะที่ยืนยันได้:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Socket นับได้ว่ามีบัญชี GitHub มากกว่า 500 บัญชี commit workflow ตัวนี้เข้าไปใน repository หลักหมื่น นับตั้งแต่ 7 ตุลาคม 2026&lt;/li&gt;
&lt;li&gt;StepSecurity บันทึกสองเคสของบัญชี maintainer ที่ถูกใช้งาน เคสแรก workflow ถูก push เข้า 27 repository เริ่มประมาณ 13:20 UTC อีกแปดชั่วโมงถัดมาอีกบัญชีหนึ่งถูก push เข้า 318 repository ภายในเวลาแค่ราว 16 นาที&lt;/li&gt;
&lt;li&gt;GitGuardian รายงานเฟสก่อนหน้า: 772 public repository ของ 373 ผู้ใช้/องค์กร ระหว่าง 31 สิงหาคมถึง 30 กันยายน 2026&lt;/li&gt;
&lt;li&gt;แคมเปญรอบแรก (2025) เคยเก็บ secret ไปได้ 3,325 รายการจาก 817 repository&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;วิธีเข้าถึงที่แหล่งข่าวประเมินว่าเป็นไปได้มากที่สุดคือ personal access token ของ maintainer รั่ว อาจมาจาก infostealer log หรือ credential dump ไม่ใช่ช่องโหว่ของ GitHub เอง แปลว่าต่อให้ GitHub ปลอดภัยแค่ไหน token ที่หลุดอยู่นอกแพลตฟอร์มก็ยังเป็นประตูหน้าบ้านอยู่ดี&lt;/p&gt;

&lt;h2&gt;
  
  
  หน้าตาของมัน และทำไมถึงเนียน
&lt;/h2&gt;

&lt;p&gt;Workflow ที่ถูกยัดมีชื่อไฟล์ว่า &lt;code&gt;security-audit.yml&lt;/code&gt; หรือ &lt;code&gt;github_actions_security.yml&lt;/code&gt; ชื่อแบบนี้เองคือจุดที่น่าสนใจ มันไม่ได้ตั้งชื่อว่า &lt;code&gt;steal.yml&lt;/code&gt; แต่แกล้งเป็นงาน security ที่ใครเห็นในรายการ workflow ก็อาจคิดว่า "อ๋อ ทีมเพิ่มตัวสแกนมั้ง" แล้วปล่อยผ่าน&lt;/p&gt;

&lt;p&gt;ลักษณะทางเทคนิคที่แหล่งข่าวบรรยายไว้ ซึ่งเอาไปใช้เป็นตัวตรวจจับได้:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;trigger เป็น &lt;code&gt;workflow_dispatch&lt;/code&gt; และ push ทุก branch ทุก tag โดยไม่กรอง&lt;/li&gt;
&lt;li&gt;checkout ด้วย &lt;code&gt;fetch-depth: 0&lt;/code&gt; คือดึง git history ทั้งหมดมา ไม่ใช่แค่ commit ล่าสุด&lt;/li&gt;
&lt;li&gt;มี step เดียวชื่อ "Audit" ที่ส่งข้อมูลออกไปยัง IP ฮาร์ดโค้ดผ่าน HTTP ธรรมดา (IOC ที่รายงานคือ 193.32.204[.]199)&lt;/li&gt;
&lt;li&gt;เป้าหมายคือ secret ที่ตั้งไว้ใน Actions, credential ที่อยู่ใน working tree และที่ซ่อนอยู่ใน git history รวมถึง AWS key, token ของ GitHub/GitLab และ API key ของฝั่ง AI อย่าง OpenAI, Anthropic, OpenRouter&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ประเด็นที่ผมอยากให้สังเกตคือ &lt;code&gt;fetch-depth: 0&lt;/code&gt; ตรงนี้เป็นเรื่องที่คนมักมองข้าม เราชอบคิดว่า "secret ที่เคย commit พลาดแล้วลบไปแล้ว" ปลอดภัย แต่ใน history มันยังอยู่ครบ workflow ที่ดึง history ทั้งก้อนแล้วเอาไป grep คือการเก็บเกี่ยวหนี้เก่าทั้งหมดของ repo ในรอบเดียว&lt;/p&gt;

&lt;p&gt;อีกเรื่องที่น่าคิด: GitGuardian บอกว่าแคมเปญเล็งไปถึง 2,577 รูปแบบของ secret ตั้งแต่ SSH key, Azure, Docker Hub/GHCR, database, GCP/Firebase, bot token ของ Slack/Discord/Telegram ไปจนถึง credential ของ npm และ PyPI ตอนนี้แหล่งข่าวบอกว่ายังไม่พบการ publish package ด้วย credential ที่ขโมยไป แต่คนที่ดูแล package ควรถือว่าเป็นความเสี่ยงที่ใกล้ที่สุด&lt;/p&gt;

&lt;h2&gt;
  
  
  เช็คอะไรวันนี้ (ใช้เวลาไม่เกิน 30 นาที)
&lt;/h2&gt;

&lt;p&gt;ถ้าคุณดูแลหลาย repo ใน organization ให้เริ่มจากชุดคำสั่งง่ายๆ นี้ ใช้ &lt;code&gt;gh&lt;/code&gt; CLI ที่ login ด้วยสิทธิ์อ่านอย่างเดียวพอ&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# หา workflow ชื่อต้องสงสัยในทุก repo ของ org&lt;/span&gt;
&lt;span class="nv"&gt;ORG&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;your-org
gh repo list &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$ORG&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;--limit&lt;/span&gt; 1000 &lt;span class="nt"&gt;--json&lt;/span&gt; nameWithOwner &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'.[].nameWithOwner'&lt;/span&gt; | &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; repo&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  for &lt;/span&gt;f &lt;span class="k"&gt;in &lt;/span&gt;security-audit.yml github_actions_security.yml&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
    if &lt;/span&gt;gh api &lt;span class="s2"&gt;"repos/&lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&gt;/contents/.github/workflows/&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;&amp;amp;1&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"FOUND: &lt;/span&gt;&lt;span class="nv"&gt;$repo&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="nv"&gt;$f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;fi
  done
done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;แล้วเช็ค commit ล่าสุดที่แตะโฟลเดอร์ &lt;code&gt;.github/workflows&lt;/code&gt; ว่าใครเป็นคน push ตรงเข้า default branch โดยไม่ผ่าน pull request:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gh api &lt;span class="s2"&gt;"repos/&lt;/span&gt;&lt;span class="nv"&gt;$ORG&lt;/span&gt;&lt;span class="s2"&gt;/REPO/commits?path=.github/workflows&amp;amp;since=2026-08-31T00:00:00Z"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-q&lt;/span&gt; &lt;span class="s1"&gt;'.[] | [.sha[0:7], .commit.author.name, .commit.message] | @tsv'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;ถ้าเจอไฟล์จริง แหล่งข่าวแนะนำให้ถือว่าถูกบุกรุกแล้ว ลำดับที่ผมจะทำคือ:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;revoke token/PAT ของบัญชีที่ push เข้ามา ก่อนอย่างอื่น&lt;/li&gt;
&lt;li&gt;ลบ workflow ออกจากทุก branch (ไม่ใช่แค่ default branch เพราะ trigger ของมันดักทุก branch)&lt;/li&gt;
&lt;li&gt;หมุน secret ทั้งหมดที่ repo นั้นเข้าถึงได้ ทั้ง Actions secrets, cloud key, API key ที่เคยอยู่ใน history ให้ถือว่าหลุดหมด&lt;/li&gt;
&lt;li&gt;ไล่ดู CloudTrail / audit log ของ provider ว่ามี key เหล่านั้นถูกเรียกจากที่แปลกๆ ช่วงที่ผ่านมาไหม&lt;/li&gt;
&lt;li&gt;เช็ค fork โดยเฉพาะ private fork และ mirror เพราะ Socket เตือนว่า fork ที่สร้างหรือ sync ตามหลังจะรับ workflow นั้นไปด้วย&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  ป้องกันระยะยาว: เปลี่ยนจาก "secret ที่เก็บไว้" เป็น "สิทธิ์ที่ออกให้ชั่วคราว"
&lt;/h2&gt;

&lt;p&gt;ถ้าให้เลือกข้อเดียวจากบทความนี้ ผมเลือกข้อนี้: &lt;strong&gt;ถ้า secret ไม่ได้มีอยู่ใน repo ตั้งแต่แรก ต่อให้ workflow ตัวร้ายถูกยัดเข้ามา ก็ไม่มีอะไรให้ขโมย&lt;/strong&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  1. ใช้ OIDC แทน static cloud key
&lt;/h3&gt;

&lt;p&gt;สำหรับ AWS ให้ workflow ขอ role แบบชั่วคราวผ่าน OIDC แทนการเก็บ &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt; ไว้ใน secrets&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;id-token&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-actions/configure-aws-credentials@&amp;lt;pin-to-full-sha&amp;gt;&lt;/span&gt;
    &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;role-to-assume&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arn:aws:iam::123456789012:role/ci-deploy&lt;/span&gt;
      &lt;span class="na"&gt;aws-region&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ap-southeast-1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;แล้วที่ trust policy ของ role ล็อก &lt;code&gt;sub&lt;/code&gt; ให้เจาะจงถึง repo และ branch เช่น &lt;code&gt;repo:your-org/your-repo:ref:refs/heads/main&lt;/code&gt; อย่าใช้ wildcard กว้างๆ ไม่งั้น workflow ที่ถูกยัดใน branch อื่นก็ขอ role ได้เหมือนกัน (ตัวเลข account ด้านบนเป็นตัวอย่างสมมติ)&lt;/p&gt;
&lt;h3&gt;
  
  
  2. ให้ &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; มีสิทธิ์น้อยที่สุดเป็นค่าเริ่มต้น
&lt;/h3&gt;

&lt;p&gt;ตั้งที่ระดับ org/repo ให้ default เป็น read-only แล้วเปิดเพิ่มเป็นราย job เท่านั้น ใน workflow เองก็ใส่ &lt;code&gt;permissions:&lt;/code&gt; ระบุชัดทุกไฟล์&lt;/p&gt;
&lt;h3&gt;
  
  
  3. บังคับ review กับโฟลเดอร์ workflow
&lt;/h3&gt;

&lt;p&gt;ใช้ CODEOWNERS ชี้ &lt;code&gt;.github/workflows/&lt;/code&gt; ให้เป็นของทีม platform/security และเปิด branch protection ให้ต้องมี approval รวมถึง "do not allow bypassing" สำหรับ admin ด้วย กรณีนี้ attacker ใช้ identity ของ maintainer จริงๆ push ตรง ถ้า default branch ปิดการ push ตรง workflow ที่ยัดมาจะต้องผ่านคนอื่นก่อน&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Pin action ด้วย commit SHA และจำกัดว่า action ไหนรันได้
&lt;/h3&gt;

&lt;p&gt;ข้อนี้ไม่ได้กัน GhostAction รอบนี้โดยตรง (เพราะมันเป็น workflow ของตัวเอง ไม่ใช่ action ภายนอก) แต่เป็นเรื่องเดียวกันคือการลดพื้นที่ที่คนนอกแทรกโค้ดเข้า pipeline ได้ ตั้ง policy ระดับ org ให้อนุญาตเฉพาะ action ที่ approve แล้ว&lt;/p&gt;
&lt;h3&gt;
  
  
  5. ดักที่ปลายทาง: egress และ secret scanning
&lt;/h3&gt;

&lt;p&gt;Self-hosted runner ควรมี egress policy ให้ออกได้เฉพาะปลายทางที่จำเป็น ในข่าวนี้ข้อมูลถูกส่งผ่าน HTTP ไปยัง IP เปล่าๆ ซึ่งกฎ egress ธรรมดาก็จับได้ ส่วน GitHub-hosted runner ให้ดักด้วย audit log stream ไปที่ SIEM แล้วตั้ง alert เมื่อมีไฟล์ใหม่ใน &lt;code&gt;.github/workflows&lt;/code&gt; ที่ไม่ได้มาจาก pull request&lt;/p&gt;

&lt;p&gt;และเปิด secret scanning กับ push protection ไว้ แต่อย่าหยุดแค่นั้น ให้สแกน history เก่าด้วยเครื่องมืออย่าง Gitleaks เป็นรอบๆ เพราะของที่เคยหลุดแล้วลบ ถ้ายังไม่ได้หมุน ก็ยังเป็นของที่ใช้ได้&lt;/p&gt;
&lt;h2&gt;
  
  
  ความเห็นส่วนตัวหลังไล่ข่าวนี้
&lt;/h2&gt;

&lt;p&gt;สิ่งที่ผมได้จากข่าวนี้ไม่ใช่ "attacker เก่ง" เพราะเทคนิคมันเรียบมาก ไม่มี zero-day ไม่มีอะไรหรูหรา สิ่งที่ได้ผลคือสามอย่างที่เราเองเป็นคนสร้างไว้: PAT อายุยาวที่ไม่มีใครหมุน, secret ที่อยู่ใน repo ตลอดกาลรวมถึงใน history, และสิทธิ์ push ตรงเข้า default branch ที่ไม่มีใครถาม&lt;/p&gt;

&lt;p&gt;ผมเองเคยนั่งไล่ลบ PAT เก่าของทีมแล้วเจออันที่สร้างไว้ตั้งแต่สามปีก่อน สิทธิ์ครบ ไม่มีวันหมดอายุ เจ้าของเปลี่ยนงานไปแล้ว (ขอเล่าแบบไม่ลงรายละเอียดนะ) ความรู้สึกตอนนั้นคือเย็นสันหลังวาบ ถ้าวันนั้นมันหลุดไปอยู่ใน infostealer log ใครสักคน เรื่องที่อ่านวันนี้อาจเป็นเรื่องของเราเอง&lt;/p&gt;

&lt;p&gt;เลยอยากฝากเป็นการบ้านสั้นๆ ถ้าอ่านจบแล้วยังไม่รู้จะเริ่มตรงไหน: เปิดหน้า Personal access tokens ของตัวเอง ดูว่ามีอันไหนที่ไม่ได้ใช้แล้วหรือไม่มีวันหมดอายุ ลบเดี๋ยวนี้เลย แล้วค่อยกลับมาคุยเรื่อง OIDC กัน&lt;/p&gt;
&lt;h2&gt;
  
  
  สรุปเช็คลิสต์
&lt;/h2&gt;

&lt;p&gt;ภาพสรุปด้านล่างเอาไปแปะเตือนทีมได้เลย&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fetliy5k89zltmo96f395.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fetliy5k89zltmo96f395.png" alt="เช็คลิสต์รับมือ GhostAction" width="800" height="655"&gt;&lt;/a&gt;&lt;/p&gt;



&lt;p&gt;แหล่งอ้างอิงข่าว: The Hacker News (9 ต.ค. 2026) "Credential-Stealing GitHub Actions Workflows Planted in Tens of Thousands of Repositories", GitGuardian blog "GhostAction Returns: 772 Repos Hit in New GitHub Actions Wave"&lt;/p&gt;


&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://thehackernews.com/2026/10/credential-stealing-github-actions.html" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fblogger.googleusercontent.com%2Fimg%2Fb%2FR29vZ2xl%2FAVvXsEjoJXBEAVVijhwsYs32WSejvp0sG3ej-Qoi7Z6qHDp8rY5IZENkKOpexrbbU1BNyiCbbB0ZXHjEDL-UYqu7CDpPpoYLiGXkuLCZJTItgP5XeLD_Sb1galPzOGtrFZigEM5CGUbwi9gcL_-XCYY6NKu9j5FiakIWn5s66DZE-OjrLZO3sMGka3YNFvGUecJT%2Fs1600%2Fgitworm.jpg" height="418" class="m-0" width="800"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://thehackernews.com/2026/10/credential-stealing-github-actions.html" rel="noopener noreferrer" class="c-link"&gt;
            Credential-Stealing GitHub Actions Workflows Planted in Tens of Thousands of Repositories
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            More than 500 GitHub accounts committed credential-stealing workflows to tens of thousands of repositories since October 7, Socket says.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fthehackernews.com%2Fdownloads%2Ffavicon.ico" width="48" height="48"&gt;
          thehackernews.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;



</description>
      <category>githubactions</category>
      <category>devsecops</category>
      <category>cicd</category>
      <category>incidentresponse</category>
    </item>
    <item>
      <title>9 คำสั่ง kubectl ที่ควรอยู่ใน muscle memory ก่อนโดนปลุกตอนตี 2</title>
      <dc:creator>Ice</dc:creator>
      <pubDate>Mon, 05 Oct 2026 03:57:49 +0000</pubDate>
      <link>https://dev.to/icelimited/9-khamsang-kubectl-thiikhwryuuain-muscle-memory-knodnpluktntii-2-7e0</link>
      <guid>https://dev.to/icelimited/9-khamsang-kubectl-thiikhwryuuain-muscle-memory-knodnpluktntii-2-7e0</guid>
      <description>&lt;p&gt;เมื่อคืนไม่ได้โดนปลุกนะ แต่ลองนึกย้อนดูว่าครั้งล่าสุดที่โดน ใช้เวลาไปกับอะไรมากที่สุด คำตอบมันน่าอายนิดนึง ไม่ใช่การแก้ปัญหา แต่เป็นการนั่งนึกว่า "jsonpath ของ lastState มันเขียนยังไงวะ" แล้วก็เปิด Google ไปสามแท็บ ขณะที่ alert ยังดังอยู่&lt;/p&gt;

&lt;p&gt;เลยตัดสินใจรวบรวมคำสั่งที่ใช้จริงบ่อยที่สุดตอน incident มาไว้ที่เดียว ไม่ใช่ cheat sheet ยาวสามหน้าที่ไม่มีใครอ่าน แต่เป็นชุดเล็กๆ ที่เรียงตามลำดับที่ควรไล่จริงตอนของพัง ทุกคำสั่งในนี้ใช้ได้กับ cluster ทั่วไป ไม่ผูกกับ EKS ยกเว้นข้อที่บอกไว้&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3pqs06lu18a19dzkpmfe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3pqs06lu18a19dzkpmfe.png" alt="ลำดับไล่ debug pod พัง" width="800" height="698"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  1. ดู Warning event ทั้ง cluster ก่อนอย่างอื่น
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl events &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;--types&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Warning
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ข้อนี้คือข้อที่อยากให้ทุกคนจำได้ที่สุด เพราะมันตอบคำถาม "ตอนนี้มีอะไรแปลกๆ เกิดขึ้นบ้าง" ได้ในคำสั่งเดียว — FailedScheduling, BackOff, FailedMount, Unhealthy โผล่มาหมด&lt;/p&gt;

&lt;p&gt;ถ้า kubectl เก่ากว่า 1.28 ที่ &lt;code&gt;kubectl events&lt;/code&gt; ยังไม่ stable ใช้ตัวคลาสสิกแทน:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get events &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;--field-selector&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Warning &lt;span class="nt"&gt;--sort-by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.metadata.creationTimestamp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ที่เลือก sort ด้วย &lt;code&gt;creationTimestamp&lt;/code&gt; แทน &lt;code&gt;.lastTimestamp&lt;/code&gt; เพราะ event รุ่นใหม่บางตัวไม่มี lastTimestamp เลย sort แล้วลำดับเพี้ยน เคยนั่งงงอยู่ห้านาทีว่าทำไม event ล่าสุดไปอยู่บนสุด อีกเรื่องที่ต้องรู้ไว้คือ event มีอายุสั้น (ค่า default ของ API server อยู่ที่ราว 1 ชั่วโมง) ถ้าตื่นมาดูช้า หลักฐานหายไปแล้ว อันนี้เป็นเหตุผลที่ควรส่ง event ไปเก็บที่อื่นด้วยตั้งแต่แรก&lt;/p&gt;

&lt;h2&gt;
  
  
  2. หา pod ที่ไม่ปกติ แบบไม่ต้องไล่ทั้ง namespace
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;--field-selector&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;status.phase!&lt;span class="o"&gt;=&lt;/span&gt;Running,status.phase!&lt;span class="o"&gt;=&lt;/span&gt;Succeeded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;กรอง Running กับ Succeeded ออก ที่เหลือคือ Pending, Failed, Unknown ล้วนๆ จุดที่ต้องระวังคือ pod ที่ CrashLoopBackOff ส่วนใหญ่ phase ยังเป็น Running อยู่นะ มันเลยไม่โผล่ในคำสั่งนี้ ต้องเสริมด้วย:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pods &lt;span class="nt"&gt;-A&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-vE&lt;/span&gt; &lt;span class="s1"&gt;'Running|Completed'&lt;/span&gt; &lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
kubectl get pods &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;--sort-by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'.status.containerStatuses[0].restartCount'&lt;/span&gt; | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ตัวหลังเรียง restart count จากน้อยไปมาก tail 10 ตัวท้ายคือพวกที่รีสตาร์ทเยอะสุด ส่วนมากต้นเหตุอยู่แถวนี้แหละ&lt;/p&gt;

&lt;h2&gt;
  
  
  3. ถามว่า container ตายเพราะอะไร โดยไม่ต้อง describe ยาวเหยียด
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get pod &amp;lt;pod&amp;gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{range .status.containerStatuses[*]}{.name}{"\t"}{.restartCount}{"\t"}{.lastState.terminated.reason}{"\t"}{.lastState.terminated.exitCode}{"\n"}{end}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;นี่แหละตัวที่เคยต้องเปิด Google สามแท็บ ตอนนี้ทำเป็น alias ไว้แล้ว output จะได้หน้าตาประมาณ &lt;code&gt;app  7  OOMKilled  137&lt;/code&gt; อ่านปุ๊บรู้ปั๊บ&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;OOMKilled&lt;/code&gt; / 137 → memory limit ต่ำไป หรือแอป leak&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Error&lt;/code&gt; / 1 → แอปตายเอง ไปดู log ต่อ&lt;/li&gt;
&lt;li&gt;143 → โดน SIGTERM ปกติ มักเกี่ยวกับ probe fail แล้วโดน kill&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;kubectl describe pod&lt;/code&gt; ก็ให้ข้อมูลเดียวกัน แต่ต้องเลื่อนหาท่ามกลาง volume, toleration และอีกร้อยบรรทัด ตอนตี 2 สายตาไม่ไหวอ่ะ&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Log ของรอบก่อนตาย ไม่ใช่รอบปัจจุบัน
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl logs &amp;lt;pod&amp;gt; &lt;span class="nt"&gt;-c&lt;/span&gt; &amp;lt;container&amp;gt; &lt;span class="nt"&gt;--previous&lt;/span&gt; &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;100
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;พลาดกันบ่อยมาก (ตัวเองด้วย) คือรัน &lt;code&gt;kubectl logs&lt;/code&gt; เฉยๆ แล้วเห็น log สวยงามของ container ที่เพิ่ง start ใหม่ ไม่มี error สักบรรทัด แล้วก็งงว่าทำไมมันพัง เพราะ error อยู่ใน instance ก่อนหน้าที่ตายไปแล้วไง &lt;code&gt;--previous&lt;/code&gt; คือตัวช่วยชีวิต&lt;/p&gt;

&lt;h2&gt;
  
  
  5. ใครกิน memory เยอะสุดตอนนี้
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl top pod &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="nt"&gt;--containers&lt;/span&gt; &lt;span class="nt"&gt;--sort-by&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;memory | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-15&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ต้องมี metrics-server ก่อนนะ ถ้ารันแล้วได้ &lt;code&gt;error: Metrics API not available&lt;/code&gt; แปลว่ายังไม่ได้ลง บน EKS ลงเป็น add-on ได้เลย ใส่ &lt;code&gt;--containers&lt;/code&gt; เพื่อให้เห็นราย container เพราะหลายครั้งตัวกิน memory ไม่ใช่แอปหลัก แต่เป็น sidecar ที่ไม่มีใครตั้ง limit ให้&lt;/p&gt;

&lt;h2&gt;
  
  
  6. เข้าไปส่องใน pod ที่ไม่มี shell
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl debug &lt;span class="nt"&gt;-it&lt;/span&gt; &amp;lt;pod&amp;gt; &lt;span class="nt"&gt;--image&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;busybox:1.36 &lt;span class="nt"&gt;--target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;container&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Image สมัยนี้เป็น distroless กันเยอะ ซึ่งดีด้าน security แต่พอจะ &lt;code&gt;exec&lt;/code&gt; เข้าไปดูอะไร ก็เจอ &lt;code&gt;executable file not found&lt;/code&gt; ทันที ephemeral container แก้ปัญหานี้ ได้ shell ที่แชร์ process namespace กับ container เป้าหมาย ดู &lt;code&gt;/proc&lt;/code&gt;, เช็ค DNS, เช็ค network ได้โดยไม่ต้อง rebuild image&lt;/p&gt;

&lt;p&gt;มุม DevSecOps ที่ต้องพูดถึง: สิทธิ์ &lt;code&gt;pods/ephemeralcontainers&lt;/code&gt; มีพลังพอๆ กับ &lt;code&gt;pods/exec&lt;/code&gt; เลยนะ ถ้าให้ใครก็ได้ใน cluster ใช้ ก็เท่ากับเปิดประตูหลังเอง ควรจำกัดไว้ที่ role ของ on-call และให้ audit log จับไว้ด้วย ใน kubectl รุ่นใหม่ใส่ &lt;code&gt;--profile=restricted&lt;/code&gt; ได้เพื่อไม่ให้ debug container ได้ privilege เกินจำเป็น&lt;/p&gt;

&lt;h2&gt;
  
  
  7. ตรวจว่า pod ถือ IAM role ไหนอยู่ (เฉพาะ EKS)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get sa &amp;lt;sa-name&amp;gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &amp;lt;ns&amp;gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{.metadata.annotations.eks\.amazonaws\.com/role-arn}{"\n"}'&lt;/span&gt;
kubectl &lt;span class="nb"&gt;exec&lt;/span&gt; &amp;lt;pod&amp;gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nb"&gt;printenv &lt;/span&gt;AWS_ROLE_ARN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;บรรทัดแรกดูว่า ServiceAccount ผูก role ไว้ยังไง บรรทัดสองดูว่า pod ที่รันอยู่จริงได้ role นั้นมาหรือยัง เพราะเคยเจอเคสแก้ annotation แล้วลืม restart pod — SA บอกว่าถูก แต่ pod เก่ายังไม่มี env ตัวนี้เลย AccessDenied รัวๆ&lt;/p&gt;

&lt;p&gt;ถ้า cluster ใช้ EKS Pod Identity แทน IRSA จะไม่มี &lt;code&gt;AWS_ROLE_ARN&lt;/code&gt; แต่จะเห็น &lt;code&gt;AWS_CONTAINER_CREDENTIALS_FULL_URI&lt;/code&gt; แทน อย่าเพิ่งตกใจว่ามันหาย ส่วนการ &lt;code&gt;printenv&lt;/code&gt; ทั้งก้อนออกมาดู ไม่แนะนำ เพราะบางทีมยัง inject secret ผ่าน env อยู่ ระบุชื่อ env ที่ต้องการไปเลยดีกว่า&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Rollback แบบรู้ว่ากำลังย้อนไปไหน
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl rollout &lt;span class="nb"&gt;history &lt;/span&gt;deploy/&amp;lt;name&amp;gt;
kubectl rollout &lt;span class="nb"&gt;history &lt;/span&gt;deploy/&amp;lt;name&amp;gt; &lt;span class="nt"&gt;--revision&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;N&amp;gt;
kubectl rollout undo deploy/&amp;lt;name&amp;gt; &lt;span class="nt"&gt;--to-revision&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;N&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;kubectl rollout undo&lt;/code&gt; เฉยๆ จะถอยไปหนึ่ง revision ฟังดูโอเค จนวันที่มีคน deploy ซ้อนกันสองรอบในสิบนาที แล้วถอยหนึ่งขั้นก็ยังไปเจอเวอร์ชันพังอยู่ดี ดู history ก่อนเสมอ บรรทัดที่สองโชว์ image tag ของ revision นั้นให้ด้วย จะได้มั่นใจว่ากำลังย้อนไปตัวที่ดีจริง&lt;/p&gt;

&lt;p&gt;ข้อสังเกตสำหรับทีมที่ใช้ GitOps (Argo CD/Flux): rollback ด้วย kubectl ตรงๆ จะโดน sync ทับกลับภายในไม่กี่นาที ต้อง revert ที่ Git หรือ pause auto-sync ก่อน ไม่งั้นจะงงว่าทำไม rollback แล้วมันกลับมาพังเอง&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Control plane ยังดีอยู่มั้ย
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;kubectl get &lt;span class="nt"&gt;--raw&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'/readyz?verbose'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ข้อนี้ไม่ค่อยได้ใช้ แต่ตอนที่ต้องใช้คือสำคัญมาก โดยเฉพาะเวลาที่ kubectl ช้าผิดปกติ หรือ webhook timeout รัวๆ ผลลัพธ์จะไล่ check ทีละตัว &lt;code&gt;[+]etcd ok&lt;/code&gt;, &lt;code&gt;[+]informer-sync ok&lt;/code&gt; ถ้ามีตัวไหนเป็น &lt;code&gt;[-]&lt;/code&gt; ก็รู้เลยว่าไม่ใช่ปัญหาที่แอป&lt;/p&gt;

&lt;p&gt;บน EKS ปกติ control plane AWS ดูแลให้ แต่ admission webhook ที่เราลงเอง (เช่น policy engine) ถ้าล่มหรือช้า ก็ทำให้ทุก request ไป API server ช้าตามได้ เคสนี้ดู &lt;code&gt;kubectl get validatingwebhookconfigurations&lt;/code&gt; ประกอบ แล้วเช็คว่า &lt;code&gt;failurePolicy&lt;/code&gt; ตั้งไว้เป็นอะไร&lt;/p&gt;

&lt;h2&gt;
  
  
  ทำเป็น alias ไว้ ไม่ต้องจำ
&lt;/h2&gt;

&lt;p&gt;ไม่มีใครจำ jsonpath ได้หรอก 555 ใส่ลง &lt;code&gt;~/.zshrc&lt;/code&gt; หรือ &lt;code&gt;~/.bashrc&lt;/code&gt; ไปเลย:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;kwarn&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'kubectl events -A --types=Warning'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;kbad&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'kubectl get pods -A --field-selector=status.phase!=Running,status.phase!=Succeeded'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;ktopmem&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'kubectl top pod -A --containers --sort-by=memory | head -15'&lt;/span&gt;

kwhy&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  kubectl get pod &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;2&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;default&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; &lt;span class="nv"&gt;jsonpath&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{range .status.containerStatuses[*]}{.name}{"\t"}{.restartCount}{"\t"}{.lastState.terminated.reason}{"\t"}{.lastState.terminated.exitCode}{"\n"}{end}'&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ใช้ &lt;code&gt;kwhy my-pod prod&lt;/code&gt; แล้วได้คำตอบเลย ไม่ต้องเปิดแท็บไหนทั้งนั้น&lt;/p&gt;

&lt;h2&gt;
  
  
  ลำดับที่ใช้จริง
&lt;/h2&gt;

&lt;p&gt;ถ้าต้องสรุปเป็นลำดับเดียวตอนโดนปลุก: เริ่มจาก event (ข้อ 1) → หา pod ที่ผิดปกติ (ข้อ 2) → ถามว่าตายเพราะอะไร (ข้อ 3) → อ่าน log รอบก่อน (ข้อ 4) → ถ้าเป็นเรื่อง resource ดู top (ข้อ 5) → ถ้ายังไม่เจอ ค่อยเข้าไปส่อง (ข้อ 6) → ถ้าเป็นเรื่องสิทธิ์ AWS ไปข้อ 7 → ถ้าต้องหยุดเลือดก่อน rollback (ข้อ 8)&lt;/p&gt;

&lt;p&gt;ส่วนใหญ่จบที่ข้อ 3-4 แล้ว ที่เหลือเป็นของสำรองไว้วันที่ดวงไม่ดี&lt;/p&gt;

&lt;p&gt;ไม่ได้บอกว่านี่คือชุดคำสั่งที่ดีที่สุดในโลกนะ แต่เป็นชุดที่หยิบใช้จริงบ่อยสุดจากที่ผ่านมา ใครมี one-liner ตัวเด็ดที่ช่วยชีวิตตอนดึกๆ มาแชร์กันได้ อยากขโมยไปใส่ alias เพิ่มเหมือนกัน&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>devops</category>
      <category>sre</category>
      <category>debugging</category>
    </item>
    <item>
      <title>เลิกแปะ AWS key ใน Pod ได้แล้ว — IRSA, least privilege และ audit trail ที่คุมได้จริง</title>
      <dc:creator>Ice</dc:creator>
      <pubDate>Mon, 27 Jul 2026 05:18:02 +0000</pubDate>
      <link>https://dev.to/icelimited/elikaepa-aws-key-ain-pod-aidaelw-irsa-least-privilege-aela-audit-trail-thiikhumaidcchring-dj</link>
      <guid>https://dev.to/icelimited/elikaepa-aws-key-ain-pod-aidaelw-irsa-least-privilege-aela-audit-trail-thiikhumaidcchring-dj</guid>
      <description>&lt;p&gt;เจอ Pod ตัวหนึ่งใน cluster เก่าที่ mount &lt;code&gt;~/.aws/credentials&lt;/code&gt; แบบ static key เข้าไปตรงๆ เมื่ออาทิตย์ก่อน — key นั้นสร้างมาตั้งแต่ปีก่อน ไม่เคย rotate เลยสักครั้ง แล้ว policy ที่แนบก็คือ &lt;code&gt;AdministratorAccess&lt;/code&gt; ตรงๆ เพราะตอนนั้นมีคนรีบ deploy ให้ทันและไม่มีเวลานั่งไล่ permission ทีละตัว เจอแบบนี้แล้วสะอึกนิดหน่อย เพราะ Pod นั้นแค่ต้องอ่าน object จาก S3 bucket เดียว แต่ดันถือกุญแจทั้งบ้านไว้&lt;/p&gt;

&lt;p&gt;เรื่องนี้เป็นปัญหาคลาสสิกของ IAM บน EKS ที่หลายทีมยังเจอ วันนี้เลยอยากพาดูสามเรื่องที่มักโดนมองข้ามพร้อมกัน: IRSA แทน static key, least privilege ที่ทำได้จริงไม่ใช่แค่ทฤษฎี และ audit logging ที่ตอบคำถามได้เวลามีคนถามว่า "ใครทำอะไรตอนไหน"&lt;/p&gt;

&lt;h2&gt;
  
  
  ทำไม static IAM key ถึงเป็นปัญหา
&lt;/h2&gt;

&lt;p&gt;Key แบบ long-lived มีปัญหาซ้อนกันสามชั้น หนึ่งคือมันไม่หมดอายุเองถ้าไม่ตั้ง rotation policy เอาไว้ สองคือมันมักถูกแปะไว้ใน config file, environment variable หรือแย่กว่านั้นคือ commit เข้า repo โดยไม่ตั้งใจ (Gitleaks เจอเคสนี้บ่อยมาก) สามคือมันผูกกับ Pod ทุกตัวเท่ากันหมด ไม่ว่า Pod นั้นจะทำหน้าที่อะไร ถ้า Pod หนึ่งโดน compromise ก็เท่ากับ credential ทั้งชุดโดนไปด้วย&lt;/p&gt;

&lt;p&gt;IRSA (IAM Roles for Service Accounts) แก้ปัญหานี้ตรงจุด คือให้แต่ละ Kubernetes ServiceAccount ผูกกับ IAM Role ผ่าน OIDC provider ของ EKS cluster เอง ไม่มี static key ไม่มีไฟล์ credential ต้องจัดการ Pod ขอ token ชั่วคราวผ่าน service account token ที่ EKS สร้างให้อัตโนมัติ แล้ว AWS SDK ฝั่ง Pod จะ assume role ผ่าน STS ให้เอง&lt;/p&gt;

&lt;h2&gt;
  
  
  ตั้ง IRSA แบบคร่าวๆ
&lt;/h2&gt;

&lt;p&gt;ก่อนอื่นต้องเปิด OIDC provider ให้ cluster (ถ้ายังไม่เปิด):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;eksctl utils associate-iam-oidc-provider &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--cluster&lt;/span&gt; my-cluster &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--approve&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;จากนั้นสร้าง IAM Role ที่มี trust policy ผูกกับ OIDC provider และ namespace/service account เจาะจง ไม่ใช่แบบเปิดกว้าง:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2012-10-17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"Statement"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Effect"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Allow"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Principal"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"Federated"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"arn:aws:iam::&amp;lt;account-id&amp;gt;:oidc-provider/&amp;lt;oidc-provider-url&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts:AssumeRoleWithWebIdentity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Condition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"StringEquals"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"&amp;lt;oidc-provider-url&amp;gt;:sub"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"system:serviceaccount:my-namespace:my-app-sa"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"&amp;lt;oidc-provider-url&amp;gt;:aud"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"sts.amazonaws.com"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;จุดสำคัญคือ &lt;code&gt;sub&lt;/code&gt; ต้องระบุ namespace และ service account name ตรงตัว ไม่ใช่ wildcard เพราะถ้าเปิดกว้างเกินไป Pod อื่นใน cluster ที่ไม่เกี่ยวก็จะ assume role นี้ได้ด้วย เท่ากับย้อนกลับไปปัญหาเดิม&lt;/p&gt;

&lt;p&gt;ฝั่ง Kubernetes ก็แค่ annotate ServiceAccount:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ServiceAccount&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-app-sa&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-namespace&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;eks.amazonaws.com/role-arn&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;arn:aws:iam::&amp;lt;account-id&amp;gt;:role/my-app-s3-reader&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;แล้ว Deployment ก็ชี้ไปใช้ ServiceAccount นี้ตามปกติ ไม่ต้องแตะ SDK code เลยด้วยซ้ำ เพราะ AWS SDK เวอร์ชันใหม่ๆ รู้จัก &lt;code&gt;AWS_WEB_IDENTITY_TOKEN_FILE&lt;/code&gt; ที่ EKS inject ให้อัตโนมัติอยู่แล้ว&lt;/p&gt;

&lt;h2&gt;
  
  
  Least privilege ที่ทำได้จริง ไม่ใช่แค่คำสวยๆ
&lt;/h2&gt;

&lt;p&gt;ปัญหาของ least privilege คือทุกคนรู้ว่าดี แต่พอถึงเวลาจริงมักจะแนบ policy กว้างไว้ก่อนเพราะกลัวแอพพัง ตรงนี้มีวิธีที่พอช่วยได้บ้าง:&lt;/p&gt;

&lt;p&gt;หนึ่ง เริ่มจาก policy แคบสุดเท่าที่รู้ว่าแอพต้องใช้ อย่างเคส S3 reader ด้านบน ควรจำกัด action ให้เหลือแค่ &lt;code&gt;s3:GetObject&lt;/code&gt; และ &lt;code&gt;s3:ListBucket&lt;/code&gt; บน ARN ของ bucket ที่ใช้จริงเท่านั้น ไม่ใช่ &lt;code&gt;s3:*&lt;/code&gt; บน &lt;code&gt;*&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;สอง ใช้ IAM Access Analyzer ช่วยดู policy ที่ generate จาก CloudTrail log ย้อนหลัง มันจะบอกว่า role นี้เรียก action อะไรบ้างจริงๆ ในช่วงที่ผ่านมา เอาผลนั้นมาเทียบกับ policy ที่แนบอยู่ ถ้าเจอ action ที่ไม่เคยถูกเรียกเลยใน 90 วัน ก็เป็นตัวเลือกแรกๆ ที่ควรตัดออก&lt;/p&gt;

&lt;p&gt;สาม แยก Role ตามหน้าที่ อย่าใช้ Role เดียวกันสำหรับหลายๆ microservice เพราะแม้แต่ละ service จะ "ดูคล้ายกัน" แต่ blast radius ตอนมีปัญหาจะต่างกันมาก ถ้า Role เดียวใช้ร่วมกันสิบ service แล้ว service หนึ่งโดนเจาะ อีกเก้าก็เสี่ยงไปด้วย&lt;/p&gt;

&lt;h2&gt;
  
  
  Secrets ที่ไม่ใช่ IAM
&lt;/h2&gt;

&lt;p&gt;Secrets management เป็นอีกเรื่องที่มักปนกับ IAM แต่จริงๆ ควรแยก IAM คือควบคุมว่า "ใคร/อะไรทำอะไรได้" ส่วน secrets คือ "ข้อมูลลับที่แอพต้องใช้" เช่น database password, API key ของ third-party ที่ไม่ใช่ AWS&lt;/p&gt;

&lt;p&gt;แนวทางที่ใช้ได้จริงคือเก็บใน AWS Secrets Manager หรือ Parameter Store (SecureString) แล้วดึงเข้า Kubernetes ผ่าน External Secrets Operator แทนการฝัง secret ไว้ใน manifest หรือ Helm values ตรงๆ&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;external-secrets.io/v1beta1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ExternalSecret&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db-credentials&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;my-namespace&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;refreshInterval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1h&lt;/span&gt;
  &lt;span class="na"&gt;secretStoreRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;aws-secrets-manager&lt;/span&gt;
    &lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SecretStore&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;db-credentials-k8s&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;secretKey&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;password&lt;/span&gt;
      &lt;span class="na"&gt;remoteRef&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prod/my-app/db-password&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ตรงนี้ IRSA ก็เข้ามาช่วยอีกชั้น เพราะ External Secrets Operator เองก็ต้องมี permission อ่าน Secrets Manager ซึ่งควรผูกผ่าน IRSA เช่นกัน ไม่ใช่ static key เหมือนกัน วนกลับมาที่หลักการเดิม&lt;/p&gt;

&lt;h2&gt;
  
  
  Audit logging — ตอบคำถามได้เวลาต้องสืบ
&lt;/h2&gt;

&lt;p&gt;Least privilege ช่วยลดความเสียหายถ้ามีอะไรผิดพลาด แต่ audit log คือสิ่งที่ตอบได้ว่า "เกิดอะไรขึ้นจริง" เปิด CloudTrail ให้ log ทุก management event เป็นพื้นฐานที่ควรมีอยู่แล้ว แต่ที่มักถูกลืมคือ data event ของ S3 และ Lambda ซึ่งไม่เปิดมาให้ default ต้องเปิดเพิ่มเอง ถ้า bucket เก็บข้อมูลสำคัญแต่ไม่ได้เปิด data event ไว้ ตอนเกิดเหตุจะสืบย้อนไม่ได้ว่ามีใคร GetObject ไฟล์ไหนไปบ้าง&lt;/p&gt;

&lt;p&gt;อีกจุดคือ EKS control plane logging เอง แนะนำให้เปิดอย่างน้อย &lt;code&gt;audit&lt;/code&gt; และ &lt;code&gt;authenticator&lt;/code&gt; log type ส่งเข้า CloudWatch Logs เพราะ log พวกนี้จะโชว์ว่า service account ไหน call Kubernetes API อะไร ซึ่งเวลาต้อง correlate กับ CloudTrail (ฝั่ง AWS API) จะได้ภาพครบทั้งสองชั้น ทั้ง container orchestration และ cloud resource&lt;/p&gt;

&lt;p&gt;สุดท้ายคือให้ log พวกนี้ไป SIEM หรืออย่างน้อยตั้ง alert บน pattern ที่ผิดปกติ เช่น AssumeRole จาก IP แปลกๆ หรือ IAM policy ถูกแก้ตอนดึกนอกเวลางาน มี log เก็บไว้เฉยๆ โดยไม่มีคนดูก็แทบไม่ต่างจากไม่มี log&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1webwzyhyu11yf0bj3d8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1webwzyhyu11yf0bj3d8.png" alt="เช็คลิสต์เปลี่ยนจาก static key เป็น IRSA + least privilege + audit trail" width="800" height="704"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  สรุปสั้นๆ
&lt;/h2&gt;

&lt;p&gt;ทั้งสามเรื่องนี้เกี่ยวโยงกันเป็นชั้นๆ IRSA ตัดปัญหา static credential ที่จุดตั้งต้น least privilege จำกัดความเสียหายถ้า Role หลุด และ audit logging ทำให้สืบย้อนได้เมื่อมีเหตุ ทำแค่อย่างเดียวไม่พอ ต้องทำควบคู่กันถึงจะปิดช่องโหว่ได้จริง เคส Pod ที่ถือ Admin key ที่เล่าไปตอนต้น สุดท้ายก็แก้ด้วยการย้ายไป IRSA พร้อม policy เฉพาะ S3 bucket เดียว ใช้เวลาไม่ถึงบ่ายวันนั้นเอง แค่ต้องมีคนมานั่งไล่ก่อนว่า Pod นี้ต้องใช้ permission อะไรจริงๆ&lt;/p&gt;

</description>
      <category>devops</category>
      <category>cloudsecurity</category>
      <category>irsa</category>
      <category>kubernetes</category>
    </item>
    <item>
      <title>วาง LLM Gateway ด้วย LiteLLM Proxy: คุม cost, token budget และ failover เวลาต้องยิงหลาย provider พร้อมกัน</title>
      <dc:creator>Ice</dc:creator>
      <pubDate>Mon, 20 Jul 2026 07:34:08 +0000</pubDate>
      <link>https://dev.to/icelimited/waang-llm-gateway-dwy-litellm-proxy-khum-cost-token-budget-aela-failover-ewlaatngyinghlaay-provider-200g</link>
      <guid>https://dev.to/icelimited/waang-llm-gateway-dwy-litellm-proxy-khum-cost-token-budget-aela-failover-ewlaatngyinghlaay-provider-200g</guid>
      <description>&lt;p&gt;เดือนก่อนทีมงานหนึ่งที่คุยด้วยมาบ่นให้ฟังว่า bill LLM ของเดือนนั้นพุ่งขึ้นเกือบ 3 เท่าโดยไม่มีใครรู้ตัวจนบิลออก สาเหตุไม่ใช่ traffic เพิ่ม แต่เป็นเพราะทีม dev คนหนึ่งเปลี่ยน model จาก Haiku ไปเป็น Opus ใน service เดียวเพื่อ debug แล้วลืมเปลี่ยนกลับ ปัญหาคือไม่มีใคร monitor ระดับ per-service เลย เพราะแต่ละทีมยิง API ตรงเข้า provider ของตัวเอง ใครใช้ key ใคร ใครยิงเท่าไหร่ ไม่มี central view&lt;/p&gt;

&lt;p&gt;นี่คือปัญหาคลาสสิกของทีมที่โตเร็วแล้วเริ่มใช้ LLM หลาย provider พร้อมกัน (Bedrock, OpenAI, Anthropic API ตรง) โดยไม่มี layer กลางคั่นระหว่าง application กับ provider เลย&lt;/p&gt;

&lt;h2&gt;
  
  
  ทำไม direct integration ถึงพังตอนสเกล
&lt;/h2&gt;

&lt;p&gt;ตอนมี 1-2 service เรียก LLM ตรงมันโอเค แต่พอเกิน 5 service ขึ้นไป ปัญหาที่เจอซ้ำๆ คือ&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Key กระจัดกระจาย&lt;/strong&gt; — แต่ละ service มี API key ของตัวเอง เก็บใน env var บ้าง Secrets Manager บ้าง ไม่มี pattern เดียวกัน&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ไม่มี cost visibility ระดับทีม/โปรเจกต์&lt;/strong&gt; — เห็นแต่บิลรวมท้ายเดือน ไม่รู้ว่าใครใช้เท่าไหร่&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limit ชนกันเอง&lt;/strong&gt; — service A กับ B ใช้ key เดียวกันของ provider เดียวกัน พอ A ยิงหนักช่วงพีค B โดน throttle ไปด้วยทั้งที่ไม่เกี่ยว&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Provider ล่มแล้วจบเลย&lt;/strong&gt; — Bedrock throttle หรือ region มีปัญหา ไม่มี fallback ไป provider อื่นอัตโนมัติ ต้องรอ manual fix&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Format ไม่ตรงกัน&lt;/strong&gt; — สลับจาก Anthropic API ไป Bedrock ต้องแก้ code เพราะ request/response shape ไม่เหมือนกัน&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;วิธีแก้ที่หลายทีมใช้ตอนนี้คือใส่ &lt;strong&gt;LLM gateway&lt;/strong&gt; คั่นกลาง — จุดเดียวที่ทุก service ยิงผ่าน แล้ว gateway เป็นคนจัดการ routing, budget, retry ให้แทน&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcjx6yagl86wimyfzgv71.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcjx6yagl86wimyfzgv71.png" alt=" " width="800" height="509"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  LiteLLM Proxy ทำอะไรได้บ้าง
&lt;/h2&gt;

&lt;p&gt;LiteLLM Proxy คือ open-source proxy ที่รองรับ provider เกือบทุกเจ้า (OpenAI, Anthropic, Bedrock, Vertex, Azure OpenAI ฯลฯ) ผ่าน interface แบบเดียวกันคือ OpenAI-compatible API ตัว application ฝั่ง client ไม่ต้องรู้เลยว่าข้างหลังยิงไป provider ไหน แค่เรียก endpoint เดียว แล้ว proxy จัดการ routing เอง&lt;/p&gt;

&lt;p&gt;Config พื้นฐานหน้าตาประมาณนี้ (ตัวอย่าง ไม่ใช่ค่าจริงจาก production ใดๆ):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;model_list&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;model_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gpt-tier-fast&lt;/span&gt;
    &lt;span class="na"&gt;litellm_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bedrock/anthropic.claude-3-haiku-20240307-v1:0&lt;/span&gt;
      &lt;span class="na"&gt;aws_region_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ap-southeast-1&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;model_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gpt-tier-fast&lt;/span&gt;
    &lt;span class="na"&gt;litellm_params&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;openai/gpt-4o-mini&lt;/span&gt;
      &lt;span class="na"&gt;api_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;os.environ/OPENAI_API_KEY&lt;/span&gt;

&lt;span class="na"&gt;router_settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;routing_strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;latency-based-routing&lt;/span&gt;
  &lt;span class="na"&gt;fallbacks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[{&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-tier-fast"&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gpt-tier-fast-backup"&lt;/span&gt;&lt;span class="pi"&gt;]}]&lt;/span&gt;

&lt;span class="na"&gt;general_settings&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;master_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;os.environ/LITELLM_MASTER_KEY&lt;/span&gt;
  &lt;span class="na"&gt;database_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;os.environ/DATABASE_URL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;สังเกตว่า &lt;code&gt;model_name&lt;/code&gt; เดียวกัน (&lt;code&gt;gpt-tier-fast&lt;/code&gt;) ผูกกับ 2 provider — นี่คือจุดที่ทำให้ failover เกิดขึ้นได้แบบ transparent ถ้า Bedrock throttle proxy จะ route ไป OpenAI ให้เองโดย client ไม่ต้องรู้เลย&lt;/p&gt;

&lt;h2&gt;
  
  
  Token budgeting ต่อทีม ไม่ใช่ต่อบิลรวม
&lt;/h2&gt;

&lt;p&gt;จุดที่ช่วยเรื่อง cost control จริงๆ คือ virtual key แต่ละทีม/โปรเจกต์ขอ key จาก proxy ได้ พร้อมตั้ง budget แยกกัน&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://litellm-proxy:4000/key/generate &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$LITELLM_MASTER_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "models": ["gpt-tier-fast"],
    "max_budget": 50,
    "budget_duration": "30d",
    "team_id": "rag-search-team"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ทีมไหนเกิน budget key จะถูก block อัตโนมัติ ไม่ต้องรอ finance มาเคาะไหล่ตอนสิ้นเดือน แล้วก็มี dashboard ในตัวให้ดูว่า spend ต่อ team ต่อ model เป็นยังไง อันนี้คือสิ่งที่แก้เคส "เปลี่ยน model แล้วลืมเปลี่ยนกลับ" ได้ตรงจุด เพราะ budget alert จะเด้งก่อนบิลจะบาน ไม่ใช่รู้ตัวหลังบิลออกแล้ว&lt;/p&gt;

&lt;h2&gt;
  
  
  Failover ที่ต้องคิดมากกว่า retry เฉยๆ
&lt;/h2&gt;

&lt;p&gt;Retry แบบ naive (ยิงซ้ำเมื่อ error) อันตรายกว่าที่คิด โดยเฉพาะถ้า error เป็น rate limit จริง retry รัวๆ จะยิ่งซ้ำเติมปัญหา สิ่งที่ควรตั้งคือ&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Exponential backoff&lt;/strong&gt; สำหรับ retry ไป provider เดิม (2-3 ครั้งพอ)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallback ไป model/provider สำรอง&lt;/strong&gt; ถ้า retry เดิมไม่ผ่าน ไม่ใช่ retry ไม่จำกัดรอบ&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cooldown period&lt;/strong&gt; — ถ้า provider หนึ่งพัง ให้ proxy จำไว้สักพัก (เช่น 60 วินาที) ก่อนลองยิงกลับไปใหม่ ไม่ใช่ลองทุก request&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Timeout ที่สมเหตุสมผลต่อ use case&lt;/strong&gt; — chat แบบ interactive timeout สั้นได้ (10-15s) แต่ถ้าเป็น batch summarization ปล่อยยาวกว่านั้นได้&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;เคยเจอเคสจริงที่ setup fallback ไว้แล้ว แต่ timeout ตั้งไว้สั้นเกินไป (5 วินาที) ผลคือ request ที่ปกติจะสำเร็จถ้ารอสัก 8 วินาที กลับ fallback ไป provider สำรองทุกครั้งเพราะ timeout ก่อน กลายเป็นจ่ายแพงขึ้นโดยไม่จำเป็นเพราะ provider สำรองแพงกว่า บทเรียนคือ tuning timeout สำคัญพอๆ กับ setup fallback logic เอง&lt;/p&gt;

&lt;h2&gt;
  
  
  RAG กับ proxy — จุดที่มักมองข้าม
&lt;/h2&gt;

&lt;p&gt;ถ้าใช้ RAG pipeline ต้องแยกให้ชัดระหว่าง embedding call กับ generation call เพราะสอง workload นี้ pattern การใช้ต่างกันมาก embedding มัก burst หนักตอน ingest เอกสารใหม่ ส่วน generation กระจายตาม user traffic ถ้าใช้ virtual key เดียวกันสำหรับทั้งคู่ budget/rate limit จะปนกัน แนะนำแยก key ต่อ workload type ไปเลย จะได้ตั้ง rate limit ที่เหมาะกับแต่ละแบบจริงๆ ไม่ใช่เผื่อไว้กว้างๆ จนคุมไม่ได้&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist ก่อนเอาเข้า production
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;แยก virtual key ต่อทีม/โปรเจกต์ ไม่ใช้ master key ยิงตรงจาก application&lt;/li&gt;
&lt;li&gt;ตั้ง &lt;code&gt;max_budget&lt;/code&gt; + &lt;code&gt;budget_duration&lt;/code&gt; ให้ทุก key ตั้งแต่วันแรก ไม่ใช่ค่อยตั้งทีหลังตอนบิลบาน&lt;/li&gt;
&lt;li&gt;Map model tier (fast/cheap vs premium) แทนการ hardcode ชื่อ model เฉพาะเจาะจงใน application code&lt;/li&gt;
&lt;li&gt;ตั้ง fallback chain อย่างน้อย 1 ชั้น ต่อ model tier ที่สำคัญ&lt;/li&gt;
&lt;li&gt;Tune timeout แยกตาม use case (interactive vs batch) อย่าใช้ค่า default เดียวกันทั้งระบบ&lt;/li&gt;
&lt;li&gt;แยก key ระหว่าง embedding workload กับ generation workload&lt;/li&gt;
&lt;li&gt;เปิด logging ระดับ request ไปที่ observability stack ที่มีอยู่แล้ว (อย่าผูกกับ dashboard ของ proxy อย่างเดียว เผื่อวันหนึ่งย้ายออกจาก LiteLLM)&lt;/li&gt;
&lt;li&gt;ทดสอบ failover จริงด้วยการปิด provider หลักชั่วคราวใน staging ก่อนเชื่อว่ามันทำงาน อย่าเชื่อแค่ตาม config&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ส่วนที่คนมักข้ามคือข้อสุดท้าย — เขียน fallback config ไว้แล้วไม่เคยทดสอบจริงว่ามัน trigger ตอนไหน พอวันที่ provider หลักล่มจริงถึงจะรู้ว่า config ผิดจุดไหน ซึ่งเป็นเวลาที่แย่ที่สุดที่จะเพิ่งมาเจอ bug&lt;/p&gt;

&lt;p&gt;Gateway แบบนี้ไม่ได้แก้ทุกปัญหา ถ้าทีมเล็กมากมี service เดียวยิง provider เดียว อาจจะ overkill เกินไปด้วยซ้ำ แต่พอเริ่มมีมากกว่า 1 provider หรือมากกว่า 3-4 service ที่ยิง LLM เป็นเรื่องปกติ การมี layer กลางแบบนี้ช่วยประหยัดเวลา debug และเงินได้เยอะกว่าที่คิดตอนแรก&lt;/p&gt;

</description>
      <category>devops</category>
      <category>ai</category>
      <category>litellm</category>
      <category>cloudcost</category>
    </item>
    <item>
      <title>เมื่อ annotation ของ ALB Ingress เยอะจนอ่านไม่ไหว — ถึงเวลาคุยเรื่อง Gateway API บน EKS หรือยัง</title>
      <dc:creator>Ice</dc:creator>
      <pubDate>Mon, 13 Jul 2026 04:28:47 +0000</pubDate>
      <link>https://dev.to/icelimited/emuue-annotation-khng-alb-ingress-eyacchnaanaimaihw-thuengewlaakhuyeruueng-gateway-api-bn-eks-hruueyang-1el4</link>
      <guid>https://dev.to/icelimited/emuue-annotation-khng-alb-ingress-eyacchnaanaimaihw-thuengewlaakhuyeruueng-gateway-api-bn-eks-hruueyang-1el4</guid>
      <description>&lt;p&gt;เปิด &lt;code&gt;ingress.yaml&lt;/code&gt; ของ service หนึ่งใน cluster ที่เคยดูแล แล้วนับ annotation ได้ 17 บรรทัด ตั้งแต่ &lt;code&gt;alb.ingress.kubernetes.io/scheme&lt;/code&gt;, &lt;code&gt;target-type&lt;/code&gt;, &lt;code&gt;certificate-arn&lt;/code&gt;, &lt;code&gt;ssl-policy&lt;/code&gt;, &lt;code&gt;healthcheck-path&lt;/code&gt;, ไปจนถึง &lt;code&gt;actions.weighted-routing&lt;/code&gt; ที่เป็น JSON string ยัดอยู่ในค่า annotation ตัวเดียว อ่านครั้งแรกงงเลยว่า string นี้คือ routing rule จริง ๆ ไม่ใช่ comment ที่ลืมลบ&lt;/p&gt;

&lt;p&gt;นี่แหละคือจุดที่ทำให้เริ่มมองหาทางอื่น ไม่ใช่เพราะ ALB Ingress Controller ห่วยนะ มันเสถียรมากด้วยซ้ำ ใช้กันมาเป็นมาตรฐานของ EKS หลายปีแล้ว แต่พอ cluster โตขึ้น มีหลายทีมมาแชร์กัน ปัญหาที่ annotation-based config มันไม่เคยถูกออกแบบมาแก้ ก็เริ่มโผล่ทีละอย่าง&lt;/p&gt;

&lt;h2&gt;
  
  
  ปัญหาที่ ALB Ingress Controller ไม่ได้ถูกออกแบบมาแก้
&lt;/h2&gt;

&lt;p&gt;Ingress resource แบบเดิมมีข้อจำกัดที่ฝังอยู่ใน spec ตั้งแต่ต้น คือมันถูกออกแบบมาให้เป็น resource เดียว ครอบทุกอย่างตั้งแต่ layer 7 routing ไปจนถึง TLS termination ไปจนถึง load balancer attribute เฉพาะของ cloud provider — ทั้งหมดนี้ยัดผ่าน annotation ที่เป็น string ธรรมดา ไม่มี schema validation ไม่มี type checking พิมพ์ผิดตัวเดียวก็ deploy ผ่านเฉยๆ แล้วไปพังตอน runtime&lt;/p&gt;

&lt;p&gt;ปัญหาที่เจอบ่อยที่สุดมีสามเรื่อง&lt;/p&gt;

&lt;p&gt;เรื่องแรกคือ &lt;strong&gt;ownership ปนกัน&lt;/strong&gt; ทีม platform ที่ดูแล load balancer scheme, security group, WAF association กับทีม app ที่แค่อยากเพิ่ม path ใหม่ ต้องมาแก้ resource เดียวกัน แก้ผิดจุดทีนึงกระทบทั้ง service เพราะ annotation ผูกกับ Ingress object ทั้งก้อน ไม่ได้แยกตาม concern&lt;/p&gt;

&lt;p&gt;เรื่องที่สองคือ &lt;strong&gt;cross-namespace routing ทำได้ยาก&lt;/strong&gt; อยากให้ path หนึ่งของ domain เดียวกันชี้ไป service ที่อยู่คนละ namespace ต้องเล่นทริค เช่น ExternalName service หรือรวม Ingress ไว้ namespace เดียวแล้วเปิดสิทธิ์ backend ข้าม namespace ซึ่งขัดกับ multi-tenant model ที่ EKS ส่วนใหญ่ใช้กันตอนนี้&lt;/p&gt;

&lt;p&gt;เรื่องที่สามคือ &lt;strong&gt;debug ยาก&lt;/strong&gt; เวลา routing ไม่ทำงานตามที่คิด ต้องไล่อ่าน annotation string ทีละตัว เทียบกับ AWS Load Balancer Controller log แล้วเดาว่า controller ตีความ annotation ยังไง เพราะ syntax ของ annotation ไม่ได้มี IDE autocomplete หรือ kubectl validation ช่วยเลย&lt;/p&gt;

&lt;h2&gt;
  
  
  Gateway API คืออะไร ทำไมถึงถูกพูดถึงเยอะขึ้น
&lt;/h2&gt;

&lt;p&gt;Gateway API เป็น evolution ของ Ingress ที่ SIG-Network ผลักดันมาหลายปี จนตัว core API (v1) graduate เป็น GA ไปแล้ว หัวใจของมันคือการ&lt;strong&gt;แยก role ออกจากกัน&lt;/strong&gt;เป็นสาม layer&lt;/p&gt;

&lt;p&gt;&lt;code&gt;GatewayClass&lt;/code&gt; เป็นของ cluster operator กำหนดว่า controller ตัวไหนจะจัดการ (เช่น AWS Gateway API Controller หรือ Istio) &lt;code&gt;Gateway&lt;/code&gt; เป็นของทีม platform กำหนด listener, port, TLS cert, ผูกกับ load balancer จริง ส่วน &lt;code&gt;HTTPRoute&lt;/code&gt; (หรือ &lt;code&gt;GRPCRoute&lt;/code&gt;, &lt;code&gt;TCPRoute&lt;/code&gt;) เป็นของทีม app แก้ path/routing rule ของตัวเองได้โดยไม่ต้องแตะ Gateway เลย&lt;/p&gt;

&lt;p&gt;ข้อดีที่เห็นชัดสุดคือทีม app ขอ HTTPRoute resource แยกของตัวเอง อยู่คนละ namespace กับ Gateway ได้เลย ผ่าน &lt;code&gt;ReferenceGrant&lt;/code&gt; ที่ทีม platform เป็นคนอนุญาตไว้ล่วงหน้า ทีม app ไม่ต้องรอ platform ทีมมาแก้ Ingress ก้อนใหญ่ทุกครั้งที่จะเพิ่ม path ใหม่&lt;/p&gt;

&lt;p&gt;อีกจุดที่ต่างชัดคือ extensibility ผ่าน &lt;strong&gt;Policy attachment&lt;/strong&gt; แทนการยัด annotation รวมกันหมด เช่นจะทำ retry policy หรือ traffic split ก็สร้างเป็น CRD แยกต่างหาก ผูกกับ Gateway/HTTPRoute ผ่าน &lt;code&gt;targetRef&lt;/code&gt; ทำให้ตรวจสอบและ validate แยกเป็นชิ้นๆ ได้ ไม่ต้อง parse string เดา syntax เหมือนเดิม&lt;/p&gt;

&lt;h2&gt;
  
  
  เทียบกันตรงๆ
&lt;/h2&gt;

&lt;p&gt;ลองแตกเป็นภาพเปรียบเทียบไว้ (ดูรูปประกอบด้านล่าง) สรุปสั้นๆ ตรงนี้ก่อน&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzy22caojfy3a8flvooy5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzy22caojfy3a8flvooy5.png" alt="เปรียบเทียบ ALB Ingress Controller กับ Gateway API บน EKS" width="800" height="509"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;ALB Ingress Controller ยังเป็นตัวเลือกที่ดีถ้า cluster เล็ก ทีมเดียวดูแลทั้งหมด ไม่ต้องการ cross-namespace routing ซับซ้อน และอยากได้ความเสถียรที่ผ่านการใช้งานจริงมานาน ส่วน Gateway API เหมาะกับ cluster ที่มีหลายทีมแชร์กัน ต้องการแยก ownership ชัดเจน หรือกำลังจะไปทาง multi-cluster/service mesh ในอนาคต เพราะ Gateway API ถูกออกแบบให้ทำงานร่วมกับ mesh ได้ตั้งแต่ต้น ไม่ใช่ patch ทีหลังแบบ Ingress&lt;/p&gt;

&lt;p&gt;จุดที่ต้องคิดหนักคือ maturity ของ &lt;strong&gt;AWS Gateway API Controller&lt;/strong&gt; เอง มันเป็นคนละ binary กับ AWS Load Balancer Controller ที่ใช้กับ Ingress อยู่แล้ว ต้อง install แยก และ feature parity บางตัวอย่าง sticky session, custom health check attribute ยังตามหลัง ALB Ingress Controller อยู่พอสมควร ถ้าจะย้ายต้องเช็ค feature ที่ใช้จริงในทีมก่อนว่ารองรับหรือยัง อย่าเชื่อ marketing slide เฉยๆ&lt;/p&gt;

&lt;h2&gt;
  
  
  ตัวอย่างการแปลงจาก Ingress annotation เป็น Gateway API
&lt;/h2&gt;

&lt;p&gt;Ingress แบบเดิมที่ทำ weighted routing ระหว่างสอง service หน้าตาประมาณนี้&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Ingress&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout&lt;/span&gt;
  &lt;span class="na"&gt;annotations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;alb.ingress.kubernetes.io/scheme&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;internet-facing&lt;/span&gt;
    &lt;span class="na"&gt;alb.ingress.kubernetes.io/target-type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ip&lt;/span&gt;
    &lt;span class="na"&gt;alb.ingress.kubernetes.io/actions.weighted-routing&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="s"&gt;{"type":"forward","forwardConfig":{"targetGroups":[&lt;/span&gt;
        &lt;span class="s"&gt;{"serviceName":"checkout-v1","servicePort":"80","weight":90},&lt;/span&gt;
        &lt;span class="s"&gt;{"serviceName":"checkout-v2","servicePort":"80","weight":10}&lt;/span&gt;
      &lt;span class="s"&gt;]}}&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/checkout&lt;/span&gt;
            &lt;span class="na"&gt;pathType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Prefix&lt;/span&gt;
            &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;weighted-routing&lt;/span&gt;
                &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;use-annotation&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;แปลงเป็น Gateway API แยกเป็นสองไฟล์ ทีม platform ดูแล Gateway ทีม app ดูแล HTTPRoute&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gateway.networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Gateway&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shared-gw&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;platform-ingress&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;gatewayClassName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;amazon-vpc-lattice&lt;/span&gt;   &lt;span class="c1"&gt;# หรือ eks-alb ตาม controller ที่ใช้&lt;/span&gt;
  &lt;span class="na"&gt;listeners&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;http&lt;/span&gt;
      &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HTTP&lt;/span&gt;
      &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
      &lt;span class="na"&gt;allowedRoutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;namespaces&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;from&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Selector&lt;/span&gt;
          &lt;span class="na"&gt;selector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;matchLabels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;gateway-access&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;allowed&lt;/span&gt;
&lt;span class="nn"&gt;---&lt;/span&gt;
&lt;span class="na"&gt;apiVersion&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gateway.networking.k8s.io/v1&lt;/span&gt;
&lt;span class="na"&gt;kind&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;HTTPRoute&lt;/span&gt;
&lt;span class="na"&gt;metadata&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout-route&lt;/span&gt;
  &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout-team&lt;/span&gt;
&lt;span class="na"&gt;spec&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;parentRefs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;shared-gw&lt;/span&gt;
      &lt;span class="na"&gt;namespace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;platform-ingress&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;matches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PathPrefix&lt;/span&gt;
            &lt;span class="na"&gt;value&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/checkout&lt;/span&gt;
      &lt;span class="na"&gt;backendRefs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout-v1&lt;/span&gt;
          &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
          &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;90&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;checkout-v2&lt;/span&gt;
          &lt;span class="na"&gt;port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;80&lt;/span&gt;
          &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;เห็นความต่างชัดๆ คือ weight ไม่ใช่ JSON string ที่ซ่อนใน annotation อีกแล้ว มันเป็น field จริงใน spec ที่ kubectl validate ได้ และทีม checkout แก้ HTTPRoute ของตัวเองได้เลยโดยไม่ต้องขอ access ไปแก้ Gateway ที่ platform ทีมดูแล&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist ก่อนตัดสินใจย้ายจริง
&lt;/h2&gt;

&lt;p&gt;ถ้าคิดจะย้ายจริง อย่าทำทีเดียวทั้ง cluster ทำตามลำดับนี้ดีกว่า&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;เช็ค EKS version ก่อน แนะนำ 1.28 ขึ้นไปเพื่อความเข้ากันได้ของ CRD ที่ Gateway API ต้องการ&lt;/li&gt;
&lt;li&gt;Inventory annotation ทั้งหมดที่ใช้อยู่จริงใน Ingress ปัจจุบัน แล้ว map ทีละตัวว่ามี field ตรงใน Gateway API หรือต้องใช้ Policy attachment เสริม&lt;/li&gt;
&lt;li&gt;Install AWS Gateway API Controller แยกต่างหาก อย่า uninstall AWS Load Balancer Controller เดิมทันที ให้รันคู่กันไปก่อนช่วง migration&lt;/li&gt;
&lt;li&gt;เลือก namespace ที่ non-critical สุดมา pilot ก่อน วัด behavior จริงของ weighted routing, health check, TLS&lt;/li&gt;
&lt;li&gt;ตรวจ feature parity ที่ทีมใช้จริง เช่น sticky session, custom timeout ว่า controller เวอร์ชันที่ใช้รองรับหรือยัง&lt;/li&gt;
&lt;li&gt;อัปเดต GitOps repo / Helm chart ให้ generate CRD ใหม่แทน Ingress annotation แล้ว review diff ให้ทีม platform เห็นก่อน merge&lt;/li&gt;
&lt;li&gt;เตรียม rollback plan ไว้เสมอ เผื่อ controller ที่ใช้ยังอยู่ใน early adoption แล้วเจอ edge case ที่ไม่คาดคิด&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;ข้อสุดท้ายสำคัญสุด อย่าลืมว่า AWS Gateway API Controller ยังใหม่กว่า AWS Load Balancer Controller มาก edge case ที่ไม่มีคนเจอมาก่อนมีสิทธิ์โผล่ได้เสมอ ย้ายแบบค่อยเป็นค่อยไป ดีกว่าเปลี่ยนทีเดียวแล้วมานั่งแก้กลางดึก&lt;/p&gt;

&lt;h2&gt;
  
  
  จะย้ายเลยไหม
&lt;/h2&gt;

&lt;p&gt;ถ้า cluster เล็ก ทีมเดียวดูแล ALB Ingress Controller ยังทำงานได้ดีอยู่ ไม่ต้องรีบย้ายเพราะกระแส แต่ถ้าเริ่มเห็นสัญญาณแบบที่เล่าไปตอนต้น คือ annotation เยอะจนอ่านไม่ไหว หลายทีมแย่งกันแก้ Ingress ก้อนเดียว หรือกำลังวางแผนไปทาง service mesh ในปีหน้าอยู่แล้ว Gateway API คุ้มที่จะเริ่มทดลองตอนนี้ เพราะ ecosystem กำลังโตเร็ว รอช้าไปอาจต้อง migrate แบบเร่งรีบทีหลัง&lt;/p&gt;

</description>
      <category>eks</category>
      <category>kubernetes</category>
      <category>albingress</category>
      <category>gatewatapi</category>
    </item>
    <item>
      <title>วางเลเยอร์ security scan ใน pipeline ยังไงไม่ให้ dev เกลียด</title>
      <dc:creator>Ice</dc:creator>
      <pubDate>Fri, 10 Jul 2026 02:06:42 +0000</pubDate>
      <link>https://dev.to/icelimited/waangeleyr-security-scan-ain-pipeline-yangaingaimaih-dev-ekliiyd-30m4</link>
      <guid>https://dev.to/icelimited/waangeleyr-security-scan-ain-pipeline-yangaingaimaih-dev-ekliiyd-30m4</guid>
      <description>&lt;p&gt;มีอยู่ช่วงนึงที่ pipeline เราช้าลงจาก 4 นาทีเป็น 18 นาที แล้วทุกคนก็เริ่มบ่น จนเกือบมีมติให้ปิด security scan บางตัวทิ้งเพื่อความเร็ว นั่นแหละคือจุดที่รู้ตัวว่าเราวาง scanning layer ผิดตั้งแต่ต้น ไม่ใช่เพราะ tool ห่วย แต่เพราะยัดทุกอย่างไว้จุดเดียวแล้วรันตามลำดับแบบโง่ๆ&lt;/p&gt;

&lt;p&gt;เรื่องนี้เลยอยากเขียนสรุปไว้ว่าจริงๆ แล้ว pipeline ที่มี security gate ครบ ไม่จำเป็นต้องช้า ไม่จำเป็นต้องทำให้ dev ด่า ถ้าวางเลเยอร์ให้ถูกจุด&lt;/p&gt;

&lt;h2&gt;
  
  
  ทำไมต้องมีหลายเลเยอร์ ไม่ใช่ tool เดียวจบ
&lt;/h2&gt;

&lt;p&gt;คำถามที่เจอบ่อยคือ "ทำไมไม่ใช้ตัวเดียวสแกนให้หมดเลย" คำตอบสั้นๆ คือไม่มี tool ตัวไหนสแกนทุกชั้นได้ดีเท่ากันหมด แต่ละตัวถูกออกแบบมาสำหรับ attack surface คนละแบบ:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Secret ที่หลุดเข้า commit&lt;/strong&gt; — ต้องใช้ tool ที่สแกนเนื้อหา diff/history โดยเฉพาะ&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logic bug และ security anti-pattern ในโค้ด&lt;/strong&gt; — ต้องใช้ static analysis ที่เข้าใจ syntax ของภาษานั้นจริงๆ&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vulnerability ใน dependency ที่ดึงมาใช้&lt;/strong&gt; — ต้องเทียบกับ CVE database&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ช่องโหว่ใน base image ตอน build container&lt;/strong&gt; — ต้องสแกนที่ layer ของ image ไม่ใช่ source code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;รวมเป็น scan ตัวเดียวไม่ได้ เพราะแต่ละอันมองคนละมุม ทีมเราเลยจบที่ 4 ชั้น: Gitleaks, Semgrep, OWASP Dependency-Check (กับ OSV เสริม), แล้วก็ Trivy&lt;/p&gt;

&lt;h2&gt;
  
  
  เลเยอร์ที่ 1: Gitleaks — ดักตั้งแต่ก่อน commit หลุด
&lt;/h2&gt;

&lt;p&gt;Gitleaks เป็นด่านแรกที่ต้องไวที่สุด เพราะ secret ที่หลุดเข้า git history แล้วลบยากกว่าที่คิดเยอะ ต่อให้ force-push ทับ commit ปัจจุบัน key เก่าก็ยังอยู่ใน history เดิม (เว้นแต่จะ rewrite history ทั้ง repo ซึ่งเจ็บกว่าเดิมอีก)&lt;/p&gt;

&lt;p&gt;ทีมเราวาง Gitleaks ไว้ 2 จุด:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;pre-commit hook&lt;/strong&gt; — เบาสุด รันแค่ diff ที่กำลังจะ commit ไม่กิน CI time เลย&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI stage แรกสุด&lt;/strong&gt; — เผื่อกรณีมีคน bypass pre-commit hook มา (เกิดขึ้นจริง บ่อยกว่าที่คิด)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;จุดที่คนมักพลาด: ตั้ง Gitleaks แล้วไม่เคย custom allowlist สำหรับ false positive พวก test fixture ที่มี string หน้าตาเหมือน API key แต่จริงๆ เป็น dummy data — พอ noise เยอะ dev ก็เริ่ม ignore alert ทั้งหมด ซึ่งอันตรายกว่าไม่มี scan อีก&lt;/p&gt;

&lt;h2&gt;
  
  
  เลเยอร์ที่ 2: Semgrep — จับ pattern ที่ linter ธรรมดาจับไม่ได้
&lt;/h2&gt;

&lt;p&gt;Semgrep ต่างจาก linter ทั่วไปตรงที่เขียน rule แบบ semantic ได้ ไม่ใช่แค่ regex บนข้อความ เช่นจับ pattern "query string ต่อกับ user input ตรงๆ โดยไม่ผ่าน parameterized query" ซึ่งเป็นต้นตอ SQL injection&lt;/p&gt;

&lt;p&gt;ข้อดีคือ community rule set ของ Semgrep ครอบคลุมเยอะมากอยู่แล้ว ไม่ต้องเขียนเองทั้งหมด แต่ rule set default บางอันก็ noisy เกินไปสำหรับ codebase เฉพาะทาง ทีมเราเลยแยก rule เป็น 2 ระดับ:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Blocking rule&lt;/strong&gt; — เจอแล้ว fail pipeline ทันที (injection pattern, hardcoded credential pattern, insecure deserialization)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Warning rule&lt;/strong&gt; — ขึ้นเป็น comment ใน PR แต่ไม่ block ให้ reviewer ตัดสินใจเอง&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;แบ่งแบบนี้แล้ว noise ลดไปเยอะ คนไม่เบื่อ alert&lt;/p&gt;

&lt;h2&gt;
  
  
  เลเยอร์ที่ 3: OWASP Dependency-Check + OSV — เช็ค dependency ที่ดึงมาใช้
&lt;/h2&gt;

&lt;p&gt;อันนี้คือจุดที่ pipeline เราเคยช้าที่สุด เพราะ OWASP Dependency-Check ต้อง sync NVD database ซึ่งบางทีช้าหรือ rate-limit เอา ทางแก้คือ:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Cache NVD database ไว้ใน artifact storage แทนที่จะ download ใหม่ทุกรอบ&lt;/li&gt;
&lt;li&gt;รัน OSV-Scanner คู่กันไปด้วย เพราะ query ผ่าน API ตรงๆ ไม่ต้อง sync database เอง เร็วกว่ามาก แต่ coverage อาจไม่ครบเท่า NVD ในบาง ecosystem&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;พอทำสองข้อนี้ เวลารวมของเลเยอร์นี้ลดจาก ~6 นาที เหลือประมาณ 40 วินาที นี่แหละคือจุดที่กู้เวลากลับมาได้เยอะสุดในทั้ง pipeline&lt;/p&gt;

&lt;h2&gt;
  
  
  เลเยอร์ที่ 4: Trivy — สแกน container image ก่อน push ขึ้น registry
&lt;/h2&gt;

&lt;p&gt;เลเยอร์สุดท้ายอยู่หลัง build image เสร็จ สแกนทั้ง OS package และ language-specific dependency ที่ฝังอยู่ใน image เกณฑ์ที่ทีมเราใช้คือ:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CRITICAL/HIGH ที่มี fix แล้ว&lt;/strong&gt; → block ทันที ห้าม push&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CRITICAL/HIGH ที่ยังไม่มี fix&lt;/strong&gt; → ให้ผ่านได้แต่ต้องมี ticket track ไว้ ห้ามเงียบหาย&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MEDIUM ลงไป&lt;/strong&gt; → ไม่ block แต่รวมอยู่ใน weekly report&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;จุดสำคัญคือต้อง set exit-code ให้แยกตาม severity ไม่ใช่ fail ทุกอย่างเป็นก้อนเดียว ไม่งั้น dev จะเจอ pipeline แดงจาก MEDIUM vulnerability ที่ยังไม่มี fix ด้วยซ้ำ แล้วก็เริ่มเบื่อ ignore ทั้งกระดาน&lt;/p&gt;

&lt;h2&gt;
  
  
  ตัวอย่าง Jenkinsfile แบบย่อ
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight groovy"&gt;&lt;code&gt;&lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="n"&gt;any&lt;/span&gt;
  &lt;span class="n"&gt;stages&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Secret scan'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'gitleaks detect --source . --exit-code 1'&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SAST'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'semgrep --config=p/security-audit --error'&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'SCA'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;parallel&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'OWASP DC'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'dependency-check.sh --project myapp --scan . --nvdApiKey $NVD_KEY'&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'OSV'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
          &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'osv-scanner -r .'&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
        &lt;span class="o"&gt;}&lt;/span&gt;
      &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Build image'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'docker build -t myapp:$BUILD_NUMBER .'&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;stage&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Image scan'&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
      &lt;span class="n"&gt;steps&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="n"&gt;sh&lt;/span&gt; &lt;span class="s1"&gt;'trivy image --severity CRITICAL,HIGH --exit-code 1 myapp:$BUILD_NUMBER'&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;จุดที่ทำให้เร็วขึ้นเยอะคือเอา SCA (OWASP DC กับ OSV) มารันแบบ &lt;code&gt;parallel&lt;/code&gt; กัน แทนที่จะรันเรียงต่อกัน เพราะสองตัวนี้ไม่มี dependency ต่อกันเลย&lt;/p&gt;

&lt;h2&gt;
  
  
  บทเรียนที่แพงที่สุดจากเรื่องนี้
&lt;/h2&gt;

&lt;p&gt;ตอนที่ pipeline ช้า 18 นาทีนั่น ปัญหาจริงๆ ไม่ได้อยู่ที่ tool scan ช้า แต่อยู่ที่เรารันทุกอย่างแบบ sequential ทั้งที่ Gitleaks กับ Semgrep ไม่เกี่ยวกันเลย รันคู่กันได้สบายๆ พอจัดใหม่เป็น parallel ตาม dependency จริง เวลารวมลดลงมากกว่าครึ่งโดยไม่ต้องปิด scan ตัวไหนทิ้งสักตัว&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist สรุปเลเยอร์ทั้งหมด
&lt;/h2&gt;

&lt;p&gt;ลำดับที่ทีมเราใช้จริงตอนนี้ (บาง stage รันขนานกันได้ตามที่บอกไปข้างบน):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4bw1vzd35vx2po7abouq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4bw1vzd35vx2po7abouq.png" alt=" " width="800" height="509"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;ถ้าใครกำลังเริ่มวาง pipeline ใหม่ ลองเริ่มจาก Gitleaks กับ Trivy ก่อนสองตัวนี้ effort ต่ำ ผลตอบแทนไว แล้วค่อยๆ เพิ่ม Semgrep กับ SCA ทีหลังได้ ไม่ต้องรีบทำครบ 4 ชั้นตั้งแต่วันแรก&lt;/p&gt;

</description>
      <category>devops</category>
      <category>security</category>
      <category>cicd</category>
    </item>
  </channel>
</rss>
