<?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 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>
