บทความ

บทนำ: ทำไมการตั้งค่าแบบ «เหมือนเซิร์ฟเวอร์ทั่วไป» ถึงใช้ในคลัสเตอร์ไม่ได้

บน VM ทั่วไป การตั้งค่าพร็อกซีใช้เวลาห้านาที คุณเขียน HTTP_PROXY ลงในโปรไฟล์ เพิ่มที่อยู่สองสามรายการในข้อยกเว้น รีสตาร์ทเซอร์วิส แล้วก็ไปกินกาแฟ ใน Kubernetes เรื่องกลับจบต่างออกไป: ครึ่งหนึ่งของเซอร์วิสตอบกลับ 502 ทันที health-check เริ่มฆ่า pod และเซอร์วิสเพย์เมนต์บ่นว่าติดต่อ pod ข้างเคียงไม่ได้ และในขณะเดียวกันคำขอภายนอกที่ตั้งใจจะให้ผ่านก็กลับทำงานได้อยู่

ทำไมถึงเป็นแบบนั้น? เพราะในคลัสเตอร์ กระบวนการหนึ่ง ๆ มีโลกของการสื่อสารเครือข่ายอยู่สองโลกพร้อมกัน โลกแรกคืออินเทอร์เน็ตภายนอก: API ของพาร์ทเนอร์ มาร์เก็ตเพลส แหล่งข้อมูลสำหรับ scraping เซอร์วิสสาธารณะ โลกที่สองคือเครือข่ายภายในคลัสเตอร์: เซอร์วิสอื่น ๆ ผ่าน ClusterIP, API server ของ Kubernetes, DNS, ฐานข้อมูล, คิว, เมตริก บนเซิร์ฟเวอร์ทั่วไป โลกที่สองแทบว่างเปล่า ตัวแปรพร็อกซีแบบ global จึงไม่ทำอะไรพัง แต่ในคลัสเตอร์ โลกที่สองใหญ่โต และพร็อกซีตัวไหนก็ตามที่ได้รับคำขอไปยังที่อยู่อย่าง payments.default.svc.cluster.local จะ resolve ไม่ได้เลย พร็อกซียืนอยู่ข้างนอกและไม่รู้อะไรเกี่ยวกับ DNS ภายในของคุณ

บวกกับลักษณะเฉพาะของ Kubernetes เอง ตัวแปรสภาพแวดล้อมของ pod ไม่มีผลกับ kubelet และ container runtime อิมเมจมักพาคอนฟิกของตัวเองมา ซึ่งไม่สนใจสภาพแวดล้อม SDK ของแต่ละภาษาก็เข้าใจคำว่า «ข้อยกเว้นจากการใช้พร็อกซี» ไม่เหมือนกัน และทั้งหมดนี้เกิดขึ้นบนพื้นฐานที่ว่า IP ขาออกของ pod มองไม่เห็นจากโลกภายนอกเลย: สิ่งที่ออกไปคือที่อยู่ของโหนดหรือ NAT gateway ของคลาวด์

ในคู่มือนี้ เราจะแกะทราฟฟิกขาออกของ Kubernetes «แบบเป็นชั้น»: แพ็กเก็ตออกจาก pod จริงอย่างไร จะแทรกพร็อกซีตรงไหนได้บ้าง ทำไม NO_PROXY ในคลัสเตอร์จึงเปลี่ยนจากเรื่องพิธีการเป็นองค์ประกอบสำคัญของสถาปัตยกรรม วิธีส่งเครดENTIALS อย่างปลอดภัย เมื่อไหร่ sidecar คุ้มค่า เมื่อไหร่เกินความจำเป็น และวิธีดีบักอาการล้มเหลวโดยไม่ต้องเดา เราจะไม่เล่าไวยากรณ์ของตัวแปรสภาพแวดล้อมซ้ำ ที่นี่มีแค่เรื่องเฉพาะของคลัสเตอร์ ทุกชิ้นส่วน manifest ใช้งานได้จริง เอาไปเป็นฐานได้เลย

พื้นฐาน: ทราฟฟิกขาออกของ pod ทำงานทางกายภาพอย่างไร

ก่อนจะวางพร็อกซี ควรเข้าใจก่อนว่าแพ็กเก็ตเดินทางอย่างไรเมื่อไม่มีพร็อกซี ไม่งั้นการวินิจฉัยก็กลายเป็นการเดาสุ่ม

จากคอนเทนเนอร์ถึงโหนด

แต่ละ pod ได้ IP ของตัวเองจาก pod CIDR เช่น 10.244.3.17 ภายใน pod คอนเทนเนอร์ทุกตัวใช้ network namespace ร่วมกัน: IP เดียว ชุดอินเทอร์เฟซเดียว localhost ร่วมกัน นี่คือข้อเท็จจริงสำคัญสำหรับแนวทาง sidecar ที่เราจะกลับมาพูดถึง แพ็กเก็ตจากคอนเทนเนอร์ออกผ่านอินเทอร์เฟซเสมือนของ pod ไปยังโหนด ซึ่ง CNI plugin จัดการต่อ จากนั้นมีสองสถานการณ์

  • ปลายทางอยู่ในคลัสเตอร์ ถ้าแอปพลิเคชันติดต่อ ClusterIP ของเซอร์วิส (เช่น 10.96.14.2) kube-proxy หรือ eBPF dataplane ของ CNI จะสลับที่อยู่ปลายทางเป็น IP ของ pod แบ็กเอนด์ตัวใดตัวหนึ่ง ทราฟฟิกไปยังโหนดที่ต้องการผ่าน overlay หรือตรง ๆ ทาง L3 และไม่เคยออกจากคลัสเตอร์เลย
  • ปลายทางอยู่นอกคลัสเตอร์ ถ้าที่อยู่ไม่ได้อยู่ใน pod CIDR หรือ service CIDR แพ็กเก็ตจะออกผ่านเส้นทางเริ่มต้น (default route) ของโหนด ตอนออกจากโหนดมักมี SNAT ทำงาน: ที่อยู่ต้นทาง 10.244.3.17 ถูกเปลี่ยนเป็น IP ของโหนด ในคลาวด์อาจมี NAT gateway ซ้อนอีกชั้น และโลกภายนอกจะเห็นที่อยู่ของตัวนั้นแทน

จากตรงนี้ได้ข้อสรุปเชิงปฏิบัติข้อแรก: เซอร์วิสภายนอกไม่เคยเห็น IP ของ pod เลย มันเห็น IP ของโหนดหรือ NAT ของคลาวด์ ถ้าคุณมี autoscaling ของโหนด IP ขาออกก็เปลี่ยนไปพร้อมกัน นี่คือเหตุผลที่การยืนยันตัวตนกับพร็อกซีด้วย IP โดยไม่มี egress node เฉพาะทางในคลัสเตอร์มักทำงานได้ไม่ดี — เราจะพูดถึงเรื่องนี้ด้านล่าง

DNS ภายในคลัสเตอร์

Pod ใช้ DNS ของคลัสเตอร์ (ปกติคือ CoreDNS) ในไฟล์ resolv.conf ของ pod มีโดเมนค้นหาติดไว้: namespace.svc.cluster.local, svc.cluster.local, cluster.local ด้วยเหตุนี้ชื่อ payments จึงถูกขยายเป็น payments.default.svc.cluster.local พร็อกซีภายนอกไม่รู้จักรูปแบบนี้เลย มันจะได้คำขอไปยังโฮสต์ payments จะลอง resolve ผ่าน DNS สาธารณะ แล้วคืนข้อผิดพลาด นี่คือเหตุผลพื้นฐานที่ทำให้ NO_PROXY ในคลัสเตอร์สำคัญยิ่งกว่าเซิร์ฟเวอร์เดี่ยวตัวไหน ๆ

«การวางพร็อกซี» ในคลัสเตอร์หมายถึงอะไรกันแน่

วิศวกรมักเข้าใจวลีนี้เป็นสามเรื่องที่ต่างกัน และการสับสนมันอันตราย:

  1. บังคับให้แอปพลิเคชันหนึ่ง ๆ ออกไปข้างนอกผ่านพร็อกซีเซิร์ฟเวอร์ (เช่น ผ่านที่อยู่เฉพาะของ Proxeon) โดยไม่แตะเซอร์วิสอื่น
  2. ทำให้มี IP ขาออกที่เสถียรและคาดเดาได้สำหรับทั้งคลัสเตอร์หรือบางส่วน — งานของเครือข่ายองค์กร compliance การตรวจสอบ
  3. รวมศูนย์การจัดการทราฟฟิกภายนอก: logging ขีดจำกัด จุดออกจุดเดียว

งานแรกแก้ที่ระดับ pod งานที่สองและสามแก้ที่ระดับคลัสเตอร์ การผสมระดับกันทำให้เกิดปัญหาใหญ่ ๆ ที่เราจะพูดถึง

สามระดับที่วางพร็อกซีได้: คอนเทนเนอร์, sidecar, egress gateway

ลองนึกภาพคลัสเตอร์เป็นอาคารสำนักงาน จะแจกพร็อกซีให้พนักงานแต่ละคน (คอนเทนเนอร์) วางผู้ช่วยไว้ข้าง ๆ โต๊ะทำงานทุกโต๊ะ (sidecar) หรือตั้งประตูทางออกเดียวที่หน้าอาคาร (egress gateway) แต่ละแบบมีราคาของมัน

ระดับ 1: ตัวคอนเทนเนอร์เอง

แอปพลิเคชันได้ที่อยู่พร็อกซีผ่านตัวแปรสภาพแวดล้อม ไฟล์คอนฟิก หรือ flag ตอนเริ่ม และส่งทราฟฟิกผ่านพร็อกซีด้วยตัวเอง นี่คือวิธีที่ง่ายและพบได้บ่อยที่สุด

ข้อดี:

  • ไม่มีโครงสร้างพื้นฐานเพิ่มเลย: เปลี่ยนแปลงแค่ Deployment manifest
  • ละเอียดยิบ: เซอร์วิสหนึ่งต้องการพร็อกซี อีกตัวข้าง ๆ ไม่ต้องการ
  • พร็อกซี «เข้าใจ» โปรโตคอล: พร็อกซี HTTP เห็นโฮสต์ปลายทาง log ได้ ใส่กฎได้

ข้อเสีย:

  • ใช้ได้เฉพาะถ้าโค้ดหรือไลบรารีสนใจการตั้งค่า Java, ไคลเอนต์ Node.js บางตัว, การ implement gRPC บางตัวต้องตั้งค่าแยก
  • พึ่งพา NO_PROXY ที่ถูกต้องอย่างสมบูรณ์: ผิดพลาดครั้งเดียวการสื่อสารภายในคลัสเตอร์พังทันที
  • เครดENTIALS ของพร็อกซีไปอยู่ในสภาพแวดล้อมของโปรเซส ใครก็ตามที่ exec เข้าไปในคอนเทนเนอร์ก็เห็น
  • การหมุนพร็อกซีหรือเปลี่ยนที่อยู่ต้องสร้าง pod ใหม่

ระดับ 2: sidecar container

ข้าง ๆ แอปพลิเคชันใน pod เดียวกันรันคอนเทนเนอร์ตัวที่สอง — พร็อกซีส่งต่อภายในเครื่อง แอปพลิเคชันส่งทราฟฟิกไปที่ localhost:3128 (หรือถูกดักด้วยกฎ iptables) แล้ว sidecar ค่อยตัดสินใจว่าจะส่งต่อไปที่ไหนและอย่างไร: ผ่าน upstream พร็อกซีตัวไหน พร้อม auth อะไร timeout เท่าไหร่

ข้อดี:

  • แอปพลิเคชันไม่รู้เรื่องเครดENTIALS ภายนอกเลย: มันอยู่ใน sidecar เท่านั้น
  • จุดรวมตรรกะเดียว: การหมุน การลองซ้ำ การเลือกพูลที่อยู่ เมตริก — โดยไม่ต้องแก้โค้ดแอปพลิเคชัน
  • โหมดโปร่งใสผ่าน iptables ทำงานได้แม้กับไลบรารีที่เมิน HTTP_PROXY
  • อัปเดตตรรกะการพร็อกซี = อัปเดตอิมเมจ sidecar ตัวเดียว

ข้อเสีย:

  • คอนเทนเนอร์เพิ่มในทุก pod: หน่วยความจำ CPU อ็อบเจกต์ให้มอนิเตอร์อีกตัว
  • การดักจับแบบโปร่งใสต้องมี NET_ADMIN ใน init container ซึ่งไม่ใช่ทุกที่ที่นโยบายความปลอดภัยยอม
  • ดีบักยากขึ้น: มีสองจุดที่พังได้

ระดับ 3: egress gateway ของคลัสเตอร์

ชุด pod (หรือโหนด) เฉพาะทางกลายเป็นจุดออกจุดเดียว pod อื่น ๆ จะติดต่อผ่าน Service ภายในของ gateway อย่างชัดเจน หรือถูกบังคับด้วย network policy และการจัดเส้นทาง gateway ค่อยออกไปข้างนอกตรง ๆ หรือผ่านพร็อกซีภายนอก

ข้อดี:

  • IP ขาออกเดียวที่เสถียร (หรือชุดสั้น ๆ) สำหรับทั้งคลัสเตอร์ เหมาะมากกับการยืนยันตัวตนด้วย IP ฝั่งผู้ให้บริการพร็อกซี และกับ whitelist ของพาร์ทเนอร์
  • log การจำกัด การตรวจสอบ แบบรวมศูนย์
  • แอปพลิเคชันไม่เก็บเครดENTIALS ใด ๆ: เห็นแค่ที่อยู่ภายในของ gateway
  • การหมุนที่อยู่ภายนอกเกิดขึ้นในที่เดียว

ข้อเสีย:

  • จุดล้มเหลวจุดเดียว: ต้องทำให้ gateway ทนทานต่อความล้มเหลว (หลาย replica, PodDisruptionBudget, anti-affinity)
  • มี hop เครือข่ายเพิ่ม และอาจเป็นคอขวดด้าน throughput
  • ต้องมี NetworkPolicy ที่รอบคอบ ไม่งั้นยังเหลือ «รู» ที่ออกตรงโดยไม่ผ่าน gateway
  • ความละเอียดทำได้ยากกว่า: ถ้าแต่ละทีมต้องการที่อยู่ภายนอกต่างกัน ต้องตั้ง gateway หลายตัว

เลือกอย่างไร

หลักปฏิบัติที่เราใช้ตอนออกแบบ:

  • แอปหนึ่ง-สองตัว คุมโค้ดได้ ใช้ HTTP client มาตรฐาน — ระดับคอนเทนเนอร์
  • เซอร์วิสเยอะ สแต็กต่างกัน มีข้อกำหนดเรื่องการหมุนและการลองซ้ำ ไม่อยากแจกเครดENTIALS — sidecar
  • ข้อกำหนด compliance, ยืนยันตัวตนด้วย IP, ตรวจสอบทราฟฟิกภายนอกทั้งหมด, ทีมหลายสิบทีม — egress gateway มักใช้ร่วมกับ sidecar สำหรับตรรกะละเอียด

สังเกตว่าระดับเหล่านี้ไม่ได้ตัดกัน สถาปัตยกรรมที่โตเต็มที่มักเป็น egress gateway เป็นเส้นเขตบังคับ บวกกับตัวแปรสภาพแวดล้อมหรือ sidecar ตรงที่ต้องมีเรื่องเฉพาะ

ตัวแปรสภาพแวดล้อมใน manifest: HTTP_PROXY, HTTPS_PROXY, NO_PROXY และทำไมถ้า NO_PROXY ไม่ถูกต้องคลัสเตอร์ถึงพัง

ไวยากรณ์ของตัวแปรคุณคงรู้แล้ว สิ่งที่เราสนใจคือเกิดอะไรขึ้นกับมันใน pod และทำไมค่า NO_PROXY มาตรฐาน «localhost,127.0.0.1» จึงน้อยเกินไปอย่างร้ายกาจที่นี่

กายวิภาคของความล้มเหลว

มาดูเป็นลำดับ เซอร์วิส orders เรียก payments ผ่านที่อยู่ http://payments:8080/charge ในสภาพแวดล้อมกำหนด HTTP_PROXY ไว้ HTTP client มองโฮสต์ปลายทาง ไม่เจอใน NO_PROXY และแทนที่จะเปิด TCP ไปที่ payments กลับเปิดไปที่พร็อกซี แล้วส่งคำขอ GET http://payments:8080/charge ให้มัน พร็อกซีลอง resolve ชื่อ payments ผ่าน resolver ของตัวเอง ไม่มีชื่อนี้ พร็อกซีคืน 502 หรือ 503 เซอร์วิสของคุณได้ข้อผิดพลาดจากพร็อกซี แต่ใน log มันดูเหมือนข้อผิดพลาดจาก payments ทีมก็ไปซ่อม payments ซึ่งปกติดี

ยังมีแบบที่สองที่เจ้าเล่ห์กว่า ถ้าเซอร์วิสติดต่อ ClusterIP ตรง ๆ อย่าง 10.96.14.2 พร็อกซีจะได้คำขอไปยังที่อยู่ private พร็อกซีบางตัวทิ้งคำขอแบบนี้ทันที ตัวอื่นลองเชื่อมต่อ 10.96.14.2 ในเครือข่ายของตัวเอง ในกรณีที่แย่ที่สุด อาจมีโฮสต์อื่นที่มีที่อยู่นั้นอยู่จริง

แบบที่สามเกี่ยวกับ Kubernetes เอง ไคลเอนต์ไลบรารีที่คุยกับ API server (operator, controller, แอปที่มี leader election) ใช้ที่อยู่ kubernetes.default.svc หรือ IP จากตัวแปร KUBERNETES_SERVICE_HOST ถ้าไม่ใส่ข้อยกเว้น พวกนี้ก็จะผ่านพร็อกซีและขาดการติดต่อกับ control plane

อะไรต้องอยู่ใน NO_PROXY สำหรับ pod

ชุดขั้นต่ำที่เราแนะนำเป็นจุดเริ่มต้น ใส่ CIDR ของคุณเอง — หาได้จากคอนฟิกของคลัสเตอร์หรือ annotation ของเซอร์วิส:

  • localhost, 127.0.0.1, ::1 — ที่อยู่ local รวมถึงการคุยกับ sidecar
  • .svc, .svc.cluster.local, .cluster.local — ชื่อเซอร์วิสทั้งหมด จุดนำหน้าหมายถึง «subdomain ใดก็ได้»
  • kubernetes.default.svc, kubernetes.default.svc.cluster.local — ระบุชัดเจน สำหรับไคลเอนต์ API
  • Service CIDR เช่น 10.96.0.0/12 — การติดต่อ ClusterIP โดยตรง
  • Pod CIDR เช่น 10.244.0.0/16 — headless service และการติดต่อแบบ pod-to-pod
  • Node CIDR — ถ้าแอปติดต่อพอร์ตของโหนด (เมตริก, เซอร์วิส hostNetwork)
  • 169.254.169.254 — metadata ของคลาวด์ ถ้าใช้อยู่
  • โดเมนภายในของบริษัท เช่น .corp.local ถ้ามีเส้นทางตรงจากคลัสเตอร์
  • ที่อยู่ของฐานข้อมูลและคิวภายใน ถ้าอยู่นอกคลัสเตอร์แต่อยู่ในเครือข่าย private

กับดักที่หลายคนไม่รู้: ไม่ใช่ทุกไคลเอนต์รองรับ CIDR

ตรงนี้คือของจริง รูปแบบของ NO_PROXY ไม่ได้มาตรฐาน ทุกไลบรารีตีความต่างกันไป:

  • Go (net/http) เข้าใจ CIDR notation, suffix ที่มีจุดนำหน้า และโฮสต์เดี่ยว ๆ นี่คือกรณีที่ดีที่สุด และเครื่องมือ Kubernetes ส่วนใหญ่เขียนด้วย Go
  • curl รองรับ CIDR ตั้งแต่เวอร์ชัน 7.86 เท่านั้น ในอิมเมจเก่า ๆ บน distro เก่า ๆ ค่า 10.96.0.0/12 จะถูกมองข้ามไปเฉย ๆ
  • Python (urllib, requests) ไม่เข้าใจ CIDR เลย เทียบกันด้วย suffix ของสตริง ค่า 10.96.0.0/12 ไม่มีทางทำงาน ขณะที่ 10.96. ทำงานเป็นแบบ prefix-like match เฉพาะโฮสต์ที่ลงท้ายด้วยสตริงนั้น จึงไม่ช่วยอะไรเช่นกัน
  • Node.js HTTP client ในตัวเมินตัวแปรสภาพแวดล้อมไปเลย ต้องมี ProxyAgent ใน undici หรือ agent ของบุคคลที่สาม แล้วตัวนั้นแหละตัดสินใจว่าจะตีความ NO_PROXY อย่างไร
  • Java ใช้ตัวแปรของตัวเอง http.nonProxyHosts ด้วยไวยากรณ์ wildcard แบบ *.svc.cluster.local และคั่นด้วยเครื่องหมาย pipe

ข้อสรุปเชิงปฏิบัติ: ถ้าในคลัสเตอร์มีภาษาหลากหลาย วิธีเดียวที่เชื่อถือได้ในการกันทราฟฟิกภายในด้วย CIDR คืออย่าพึ่ง NO_PROXY ของไคลเอนต์ แต่แก้ที่ระดับ sidecar หรือ gateway ที่ parser อยู่ในมือคุณ สำหรับชื่อ DNS แบบ suffix ที่มีจุดใช้ได้เกือบทุกที่ ดังนั้นในแอปพลิเคชันควรติดต่อเซอร์วิสด้วยชื่อ ไม่ใช่ ClusterIP

ตัวพิมพ์ของตัวแปร

อีกจุดที่กินเวลาดีบักเป็นชั่วโมงบ่อย ๆ curl อ่าน http_proxy เฉพาะตัวพิมพ์เล็ก: HTTP_PROXY ตัวใหญ่จะถูกเมินโดยเจตนา เพราะช่องโหว่ทางประวัติศาสตร์กับ CGI ขณะที่ HTTPS_PROXY และ NO_PROXY curl รับได้ทั้งสองแบบ Go, Python และ .NET อ่านทั้งคู่ ดังนั้นใน manifest เราจึงกำหนดทั้งสองรูปแบบเสมอ — ราคาถูกและตัดปัญหาไปได้ทั้งชั้น

ชิ้นส่วนใช้งานได้: ConfigMap กับ NO_PROXY และ Deployment

เอา NO_PROXY ไปใส่ ConfigMap ระดับเนมสเปซ จะได้แก้ที่เดียวไม่ใช่ยี่สิบ Deployment:

apiVersion: v1
kind: ConfigMap
metadata:
name: egress-proxy-common
namespace: crawlers
data:
NO_PROXY: "localhost,127.0.0.1,::1,.svc,.svc.cluster.local,.cluster.local,kubernetes.default.svc,10.96.0.0/12,10.244.0.0/16,192.168.10.0/24,169.254.169.254,.corp.local"
no_proxy: "localhost,127.0.0.1,::1,.svc,.svc.cluster.local,.cluster.local,kubernetes.default.svc,10.96.0.0/12,10.244.0.0/16,192.168.10.0/24,169.254.169.254,.corp.local"

และ Deployment ที่ใช้มัน ที่อยู่พร็อกซีตรงนี้ยังไม่มีเครดENTIALS — วิธีเก็บเครดENTIALS เราจะแยกพูดในหัวข้อหนึ่ง:

apiVersion: apps/v1
kind: Deployment
metadata:
name: price-collector
namespace: crawlers
spec:
replicas: 3
selector:
matchLabels:
app: price-collector
template:
metadata:
labels:
app: price-collector
spec:
containers:
- name: app
image: registry.internal/price-collector:2.7.0
envFrom:
- configMapRef:
name: egress-proxy-common
env:
- name: HTTP_PROXY
value: "http://proxy-host.example:8080"
- name: http_proxy
value: "http://proxy-host.example:8080"
- name: HTTPS_PROXY
value: "http://proxy-host.example:8080"
- name: https_proxy
value: "http://proxy-host.example:8080"
readinessProbe:
httpGet:
path: /healthz
port: 8080

สังเกต schema ใน HTTPS_PROXY: ส่วนมากคือ http:// เพราะไคลเอนต์เชื่อมต่อกับพร็อกซีเองผ่าน TCP ธรรมดา แล้วค่อยสร้าง tunnel CONNECT สำหรับคำขอ HTTPS ค่า https:// ตรงนี้หมายถึง TLS ไปถึงพร็อกซีเอง ซึ่งทั้งไคลเอนต์และพร็อกซีเซิร์ฟเวอร์รองรับไม่หมด ตรวจสอบในแผงของ Proxeon ว่าแพ็กเกจของคุณให้การเชื่อมต่อแบบไหน

ตรวจสอบว่า probe ไม่ผ่านพร็อกซี

Liveness และ readiness probe ดำเนินการโดย kubelet ไม่ใช่แอปพลิเคชัน จึงไม่เห็นตัวแปรสภาพแวดล้อม และไม่กลัวการพร็อกซี แต่ถ้าแอปพลิเคชันของคุณใน handler /healthz เองเรียก dependency ผ่านที่อยู่ภายใน คำเรียกพวกนั้นจะเข้าใต้ HTTP_PROXY นี่แหละคืออาการคลาสสิก «หลังเพิ่มพร็อกซี pod เริ่มไม่ผ่าน readiness»

อะไร «ไม่» ดึงตัวแปรเหล่านี้ไปใช้: kubelet, อิมเมจที่มีคอนฟิกของตัวเอง, SDK

ตัวแปรสภาพแวดล้อมของ pod อยู่ภายในโปรเซสของแอปพลิเคชัน ทุกอย่างที่อยู่นอกมัน หรือจงใจเมินสภาพแวดล้อม ต้องตั้งค่าแยก มาดูสามกลุ่ม

Kubelet และ container runtime

คำถามที่พบบ่อยที่สุดตอนย้ายคลัสเตอร์หลังพร็อกซี: «ทำไมอิมเมจดึงไม่ได้ เราใส่ HTTP_PROXY ใน Deployment แล้วนะ» คำตอบ: อิมเมจดึงโดย container runtime (containerd หรือ CRI-O) บนโหนดตามคำสั่งของ kubelet นี่คือเซอร์วิสระบบที่รันผ่าน systemd และอ่านเฉพาะสภาพแวดล้อมของตัวเอง

การตั้งค่าทำผ่าน drop-in file สำหรับ systemd unit บนทุกโหนด สำหรับ containerd หน้าตาเป็นแบบนี้ (ไฟล์ /etc/systemd/system/containerd.service.d/http-proxy.conf):

[Service]
Environment="HTTP_PROXY=http://proxy-host.example:8080"
Environment="HTTPS_PROXY=http://proxy-host.example:8080"
Environment="NO_PROXY=localhost,127.0.0.1,10.96.0.0/12,10.244.0.0/16,192.168.10.0/24,.svc,.cluster.local,registry.internal"

หลังจากนั้น — systemctl daemon-reload และ systemctl restart containerd ใน NO_PROXY ต้องมี registry ภายใน ไม่งั้น runtime จะไปดึงอิมเมจของคุณเองผ่านพร็อกซีภายนอก สำหรับ kubelet ต้องมี drop-in แยกน้อยกว่า — เฉพาะตอนที่มันติดต่อ API ของคลาวด์หรือ webhook ภายนอกเอง

ใน managed cluster อาจไม่มีสิทธิ์เข้าถึงโหนด การตั้งค่า runtime ก็ทำผ่านกลไกของผู้ให้บริการคลาวด์ หรือผ่าน DaemonSet ที่มี privileged container ซึ่งแก้คอนฟิกบนโฮสต์ วิธีที่สองเป็นการประนีประนอมที่ควรปรึกษาทีมความปลอดภัย

อิมเมจที่มีคอนฟิกของตัวเอง

อิมเมจสำเร็จรูปหลายตัวพาไฟล์คอนฟิกมาที่เขียนทับหรือเมินสภาพแวดล้อม:

  • แอปพลิเคชัน JVM Java ไม่อ่าน HTTP_PROXY ต้องใช้ system property สะดวกคือส่งผ่าน JAVA_TOOL_OPTIONS ซึ่ง JVM ดึงไปใช้ได้โดยไม่ต้องแก้สคริปต์เริ่ม:
env:
- name: JAVA_TOOL_OPTIONS
value: "-Dhttp.proxyHost=proxy-host.example -Dhttp.proxyPort=8080 -Dhttps.proxyHost=proxy-host.example -Dhttps.proxyPort=8080 -Dhttp.nonProxyHosts=localhost|127.0.0.1|*.svc|*.svc.cluster.local|*.cluster.local|10.*|192.168.*"
  • อิมเมจที่มีคอนฟิก HTTP client ของตัวเอง (agent มอนิเตอร์บางตัว, connector, เครื่องมือ ETL) อ่านพร็อกซีจาก YAML หรือ TOML ของตัวเอง มันเห็นสภาพแวดล้อมแต่ไม่ใช้ อ่านเอกสารของอิมเมจและหาคีย์อย่าง proxy_url
  • อิมเมจที่มี DNS resolver ฮาร์ดโค้ด ในแอปพลิเคชันทำลายแนวคิด NO_PROXY ตามชื่อ: แอป resolve ชื่อเอง ได้ IP มา แล้วเอา IP เทียบกับรายการที่คุณมีแค่ suffix ของโดเมน
  • สคริปต์ entrypoint ที่เขียนทับตัวแปร เจอน้อยแต่ควรตรวจ: kubectl exec pod -- cat /proc/1/environ | tr ไบต์ null เป็นบรรทัดใหม่ จะโชว์สภาพแวดล้อมจริงของโปรเซสหลัก

SDK และโปรโตคอลเฉพาะ

รายการยาว เรายกที่พบบ่อยที่สุด:

  • gRPC การ implement หลายตัวรองรับ HTTP_PROXY เฉพาะผ่าน CONNECT และมีข้อจำกัด บางตัวเมิน NO_PROXY สำหรับทราฟฟิก gRPC ภายในคลัสเตอร์ อันตรายเป็นทวีคูณ
  • Node.js fetch และโมดูล http/https ในตัวไม่อ่านสภาพแวดล้อม ต้องมี agent อย่างชัดเจน ไลบรารี axios อ่าน HTTP_PROXY ผ่าน dependency ของมัน แต่ fetch แบบเนทีฟไม่อ่าน ในแอปเดียวไคลเอนต์สองตัวอาจทำงานต่างกัน
  • Cloud SDK ส่วนใหญ่เคารพ HTTP_PROXY แต่การติดต่อ endpoint ของ metadata (169.254.169.254) ต้องอยู่ในข้อยกเว้น ไม่งั้นการดึงข้อมูลรับรองชั่วคราวจะพัง
  • WebSocket และ raw TCP ไคลเอนต์ฐานข้อมูล, message broker, Redis ทำงานบนโปรโตคอลของตัวเองเหนือ TCP และไม่ผ่าน HTTP proxy เลย ถ้าทราฟฟิกแบบนี้ต้องผ่านพร็อกซี ต้องใช้โหมด SOCKS หรือการดักจับแบบโปร่งใสที่ระดับ sidecar
  • DNS client ของแอปเอง (resolve ผ่านไลบรารีแยก) ไม่ปฏิสัมพันธ์กับพร็อกซีและวิ่งตรงไปทาง UDP 53 เป็นเรื่องปกติ แต่ต้องจำไว้ตอนเขียน NetworkPolicy

สรุปหัวข้อนี้: ก่อนจะเปิดพร็อกซีให้เซอร์วิสไหน ทำแผนที่การเชื่อมต่อขาออกของมันตามโปรโตคอลและไลบรารี ครึ่งชั่วโมงที่ทำ inventory ประหยัดวันดีบัก

Secret: เครดENTIALS พร็อกซีผ่าน Secret ไม่ใช่ใน manifest

ที่อยู่พร็อกซีพร้อม login และรหัสผ่านในเนื้อ Deployment คือการรั่วไหลที่รอเวลาของมัน Manifest อยู่ใน git ไปอยู่ใน CI log แสดงใน kubectl get deploy -o yaml ให้ใครก็ตามที่มีสิทธิ์อ่านเนมสเปซ มาทำให้ถูกกันเถอะ

วิธีที่ 1: URL พร็อกซีเต็มใน Secret

แบบง่ายที่สุด: ใน Secret เก็บสตริงสำเร็จรูปพร้อมเครดENTIALS แอปพลิเคชันได้มันเป็นตัวแปรสภาพแวดล้อม สร้าง secret นอก git (ผ่าน CI variable, secret manager ภายนอก, หรือด้วยมือ):

kubectl -n crawlers create secret generic egress-proxy-creds --from-literal=HTTP_PROXY="http://user123:s3cretPass@proxy-host.example:8080" --from-literal=HTTPS_PROXY="http://user123:s3cretPass@proxy-host.example:8080" --from-literal=http_proxy="http://user123:s3cretPass@proxy-host.example:8080" --from-literal=https_proxy="http://user123:s3cretPass@proxy-host.example:8080"

ใน Deployment แทน env ตรง ๆ — อ้าง Secret และ ConfigMap ที่มี NO_PROXY:

containers:
- name: app
image: registry.internal/price-collector:2.7.0
envFrom:
- configMapRef:
name: egress-proxy-common
- secretRef:
name: egress-proxy-creds

รายละเอียดสำคัญ: ถ้า login หรือรหัสผ่านมีอักขระพิเศษ (@, :, /, %) ต้อง URL-encode รหัสผ่านที่มี @ โดยไม่ encode จะทำให้ parse URL พัง และไคลเอนต์จะพยายามเชื่อมต่อไปยังโฮสต์ผิด ในแผง Proxeon ตอนสร้างข้อมูลรับรอง สะดวกคือเลือกรหัสผ่านที่ไม่มีอักขระพิเศษ หรือ copy URL สำเร็จรูปไปเลย

วิธีที่ 2: แยกฟิลด์ แล้วประกอบ URL ในคอนเทนเนอร์

เมื่อ login และรหัสผ่านเก็บแยกกัน (เช่น sync มาจาก secret store ภายนอก) URL ประกอบใน entrypoint หรือในตัวแอป:

env:
- name: PROXY_USER
valueFrom:
secretKeyRef:
name: egress-proxy-creds
key: username
- name: PROXY_PASS
valueFrom:
secretKeyRef:
name: egress-proxy-creds
key: password
- name: PROXY_HOST
value: "proxy-host.example:8080"

วิธีนี้สะดวกกว่าสำหรับการหมุนรหัสผ่าน: แก้คีย์ Secret อันเดียว ไม่ใช่สตริงสำเร็จรูปสี่ตัว

วิธีที่ 3: ไฟล์แทนตัวแปร

ตัวแปรสภาพแวดล้อมของโปรเซสอ่านได้จาก /proc และมักไปอยู่ใน crash dump กับ error report ถ้านโยบายความปลอดภัยเข้ม ให้ mount Secret เป็นไฟล์ vớiสิทธิ์ 0400 แล้วอ่านในแอปหรือใน sidecar:

volumes:
- name: proxy-creds
secret:
secretName: egress-proxy-creds
defaultMode: 0400
containers:
- name: app
volumeMounts:
- name: proxy-creds
mountPath: /var/run/secrets/proxy
readOnly: true

วิธีที่ 4: เอาเครดENTIALS ออกไปเลย

ถ้ามี egress gateway ที่มี IP ขาออกเสถียร การยืนยันตัวตนกับพร็อกซีเปลี่ยนจาก login-rหัสผ่านเป็น whitelist IP ได้ ในกรณีนี้ในคลัสเตอร์ไม่เก็บอะไรที่เป็นความลับเลย: pod ติดต่อ Service ภายในของ gateway gateway ออกไปด้วยที่อยู่คงที่ ผู้ให้บริการปล่อยทราฟฟิกตามที่อยู่ Proxeon รองรับทั้งสองโหมดการยืนยันตัวตน และสำหรับคลัสเตอร์ที่มี egress เฉพาะทาง เราแนะนำโหมด IP ว่าปลอดภัยกว่าและดูแลง่ายกว่า

สุขอนามัยรอบ Secret

  • เปิด encryption Secret at rest ใน etcd — โดยค่าเริ่มต้นปิดใน self-managed ส่วนใหญ่
  • จำกัด RBAC: สิทธิ์อ่าน secret ของเนมสเปซควรมีแค่ service account ที่จำเป็นจริง แอปที่ได้ secret ผ่าน envFrom ไม่ควรอ่านอ็อบเจกต์ Secret ผ่าน API
  • อย่า log สภาพแวดล้อม ตรวจว่าเฟรมเวิร์กไม่พ่น env ตอนเริ่มในโหมด debug
  • ตกลงเรื่องการหมุน การเปลี่ยนรหัสผ่านพร็อกซีควรเป็นงานปกติ: อัปเดต Secret, rollout restart, ปิดรหัสเก่า
  • จำไว้ว่าเมื่อใช้ envFrom การอัปเดต Secret ไม่เปลี่ยนสภาพแวดล้อมของ pod ที่รันอยู่ ต้อง rollout restart เมื่อ mount เป็นไฟล์ kubelet จะอัปเดตเนื้อหาภายในหนึ่ง-สองนาที แต่แอปต้องอ่านไฟล์ซ้ำได้

แนวทาง sidecar: เมื่อไหร่คุ้มค่า และให้อะไรนอกจากพร็อกซี

Sidecar ในบริบท egress คือพร็อกซีคอนเทนเนอร์ในเครื่องที่อยู่ใน pod รับทราฟฟิกจากแอปแล้วส่งต่อไปยังพร็อกซีภายนอก มันเปลี่ยนคำถาม «ไลบรารีเคารพ HTTP_PROXY ไหม» เป็นคำถาม «sidecar ดักทราฟฟิกได้ไหม» ซึ่งตอบง่ายกว่า

เมื่อไหร่ sidecar คุ้มค่า

  • แอปเขียนบนสแต็กที่จัดการพร็อกซียุ่งยาก (JVM กับไคลเอนต์สิบตัว, Node.js ที่ผสม fetch กับ axios, legacy บน C++)
  • ต้องการหมุนที่อยู่ตามตารางหรือตามข้อผิดพลาดโดยไม่แก้โค้ด: sidecar ถือพูลพร็อกซี Proxeon หลายตัวและสลับเอง
  • อยากเอาเครดENTIALS ออกจากแอป: sidecar เป็นตัวเดียวที่รู้รหัสผ่าน แอปติดต่อ localhost โดยไม่ต้อง auth
  • ต้องการนโยบายการลองซ้ำ timeout และขีดจำกัดคำขอขาออกเป็นหนึ่งเดียวสำหรับเซอร์วิส
  • ต้องการทราฟฟิกที่ไม่ใช่แค่ HTTP: โหมด SOCKS หรือดักจับ TCP แบบโปร่งใส

เมื่อไหร่ sidecar เกินความจำเป็น

  • เซอร์วิสเดียวบน Go หรือ Python กับ HTTP client ตัวเดียว ตัวแปรสภาพแวดล้อมพอ
  • ขีดจำกัดทรัพยากรเข้ม: pod เล็ก ๆ พันตัว ที่ 30–50 MB ต่อ sidecar คูณกันเป็นตัน
  • มี egress gateway ที่มีตรรกะ нужอยู่แล้ว การทำซ้ำในทุก pod ไม่มีประโยชน์

Native sidecar container

ตั้งแต่ Kubernetes 1.29 มีกลไก native sidecar และเสถียรที่ 1.33: init container ที่มี restartPolicy: Always คอนเทนเนอร์แบบนี้เริ่มก่อนตัวหลัก อยู่ตลอดอายุของ pod และปิดอย่างถูกต้องหลังตัวหลัก นี่แก้ปัญหาเก่า: แต่ก่อนแอปอาจเริ่มก่อนพร็อกซีและเจอ connection refused ในคำขอแรก ๆ และ Job ที่มี sidecar ไม่เคยจบเพราะ sidecar ยังทำงานอยู่

ชิ้นส่วนใช้งานได้: pod กับ sidecar พร็อกซี

ตรงนี้ sidecar — พร็อกซีส่งต่อเล็ก ๆ (อิมเมจสมมุติ egress-sidecar) ที่ฟังที่ 127.0.0.1:3128 อ่านเครดENTIALS upstream จาก Secret และรู้ว่าที่อยู่ไหนให้ผ่านตรง:

apiVersion: apps/v1
kind: Deployment
metadata:
name: feed-importer
namespace: integrations
spec:
replicas: 2
selector:
matchLabels:
app: feed-importer
template:
metadata:
labels:
app: feed-importer
spec:
initContainers:
- name: egress-sidecar
image: registry.internal/egress-sidecar:1.3.0
restartPolicy: Always
env:
- name: LISTEN_ADDR
value: "127.0.0.1:3128"
- name: DIRECT_CIDRS
value: "10.96.0.0/12,10.244.0.0/16,192.168.10.0/24"
- name: DIRECT_SUFFIXES
value: ".svc,.cluster.local,.corp.local"
envFrom:
- secretRef:
name: egress-upstream-creds
resources:
requests:
cpu: 20m
memory: 32Mi
limits:
memory: 96Mi
readinessProbe:
tcpSocket:
port: 3128
containers:
- name: app
image: registry.internal/feed-importer:4.1.0
envFrom:
- configMapRef:
name: egress-proxy-common
env:
- name: HTTP_PROXY
value: "http://127.0.0.1:3128"
- name: HTTPS_PROXY
value: "http://127.0.0.1:3128"
- name: http_proxy
value: "http://127.0.0.1:3128"
- name: https_proxy
value: "http://127.0.0.1:3128"

สังเกตว่า NO_PROXY ของแอปยังต้องมีอยู่ sidecar ผ่านทราฟฟิกภายในตรงได้ แต่จะไปผ่าน hop เพิ่มที่ localhost ทำไมล่ะ? อีกอย่าง การ parse CIDR ใน sidecar อยู่ในมือคุณ แต่ในแอปไม่อยู่ การป้องกันสองชั้นตรงนี้เหมาะสม

โหมดโปร่งใสผ่าน iptables

ถ้าแอปเมินสภาพแวดล้อมไปเลย ให้เพิ่ม init container ที่ redirect ทราฟฟิก TCP ขาออกทั้งหมดของ pod ยกเว้นเครือข่ายภายในและทราฟฟิกของ sidecar เอง ไปยังพอร์ตของ sidecar sidecar ต้องทำงานในโหมดโปร่งใสและกู้ที่อยู่ปลายทางเดิมกลับมา init container ต้องมี capability NET_ADMIN และ sidecar ต้องรันด้วย UID แยก เพื่อไม่ให้ทราฟฟิกของตัวเองวนกลับ:

initContainers:
- name: egress-redirect
image: registry.internal/egress-init:1.0.0
securityContext:
capabilities:
add: ["NET_ADMIN"]
command: ["sh", "-c"]
args:
- |
iptables -t nat -N EGRESS_REDIRECT
iptables -t nat -A EGRESS_REDIRECT -d 10.96.0.0/12 -j RETURN
iptables -t nat -A EGRESS_REDIRECT -d 10.244.0.0/16 -j RETURN
iptables -t nat -A EGRESS_REDIRECT -d 127.0.0.0/8 -j RETURN
iptables -t nat -A EGRESS_REDIRECT -m owner --uid-owner 1337 -j RETURN
iptables -t nat -A EGRESS_REDIRECT -p tcp -j REDIRECT --to-ports 15001
iptables -t nat -A OUTPUT -p tcp -j EGRESS_REDIRECT

ตรงนี้ 1337 — UID ที่ sidecar รัน และ 15001 — พอร์ตโปร่งใสของมัน นี่คือหลักการเดียวกับที่ service mesh ใช้ดักทราฟฟิก ถ้าในคลัสเตอร์มี mesh อยู่แล้ว ความสามารถ egress ของมันน่าจะพอ และไม่ต้องมี sidecar แยก

Sidecar ให้อะไรนอกจากพร็อกซี

  • เมตริกทราฟฟิกขาออก แยกตามโฮสต์ปลายทาง รหัสตอบกลับ เวลา: แอปไม่รู้เรื่อง แต่ sidecar เห็นหมด
  • Circuit breaker กับ API ภายนอก: เมื่อมีข้อผิดพลาดต่อเนื่อง sidecar หยุดกระหน่ำพาร์ทเนอร์และตอบกลับแบบล้มเหลวเร็ว
  • พูลที่อยู่และการหมุน: รายการพร็อกซี Proxeon ในคอนฟิก สลับตามกลยุทธ์ round-robin หรือตามข้อผิดพลาด 407/403
  • DNS cache ในเครื่อง สำหรับชื่อภายนอก ลดโหลดบน CoreDNS
  • จำกัดความเร็ว per-destination เพื่อทำตามขีดจำกัด API ของพาร์ทเนอร์และไม่ตกใต้ข้อจำกัดของมัน

Egress gateway ของคลัสเตอร์: จุดออกจุดเดียว

Egress gateway แก้ปัญหาคนละสเกล: ไม่ใช่ «ทำไมเซอร์วิสนี้ต้องผ่านพร็อกซี» แต่ «คลัสเตอร์ออกไปสู่โลกผ่านที่อยู่ไหนกันแน่» มาดูการ implement ที่ใช้ได้จริงโดยไม่ผูกกับ CNI ตัวใดตัวหนึ่ง

สถาปัตยกรรม

  1. จัดกลุ่มโหนดที่มี IP ภายนอกคงที่ (ในคลาวด์ — ผูกกับ public address หรือผ่าน NAT gateway แยก) โหนดได้รับ label node-role: egress และ taint เพื่อไม่ให้ pod อื่น ๆ มาอาศัย
  2. บนโหนดเหล่านี้ deploy Deployment ของพร็อกซีส่งต่อหลาย replica พร้อม anti-affinity ข้างหน้าเป็น Service ชนิด ClusterIP
  3. Pod แอปพลิเคชันได้ HTTP_PROXY ชี้ไปที่ Service นี้ NO_PROXY ของมันมีเครือข่ายภายใน
  4. NetworkPolicy ห้าม pod แอป egress ออกนอกคลัสเตอร์ทั้งหมด ยกเว้น DNS และทราฟฟิกไป Service ของ gateway
  5. Gateway ออกไปข้างนอกตรง ๆ (ที่อยู่ของมันคือที่อยู่ขาออกของคลัสเตอร์) หรือผ่านพร็อกซี Proxeon ภายนอกด้วยการยืนยันตัวตนตาม IP ของ gateway

ชิ้นส่วนใช้งานได้: Deployment ของ gateway และ NetworkPolicy

apiVersion: apps/v1
kind: Deployment
metadata:
name: egress-gateway
namespace: egress
spec:
replicas: 3
selector:
matchLabels:
app: egress-gateway
template:
metadata:
labels:
app: egress-gateway
spec:
nodeSelector:
node-role: egress
tolerations:
- key: dedicated
value: egress
effect: NoSchedule
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app: egress-gateway
topologyKey: kubernetes.io/hostname
containers:
- name: proxy
image: registry.internal/egress-gateway:2.0.1
ports:
- containerPort: 3128
envFrom:
- secretRef:
name: egress-upstream-creds
---
apiVersion: v1
kind: Service
metadata:
name: egress-gateway
namespace: egress
spec:
selector:
app: egress-gateway
ports:
- port: 3128
targetPort: 3128

นโยบายสำหรับเนมสเปซแอปที่ปิดการออกอินเทอร์เน็ตโดยตรง:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: egress-only-via-gateway
namespace: crawlers
spec:
podSelector: {}
policyTypes: ["Egress"]
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: egress
podSelector:
matchLabels:
app: egress-gateway
ports:
- protocol: TCP
port: 3128
- to:
- ipBlock:
cidr: 10.244.0.0/16
- ipBlock:
cidr: 10.96.0.0/12

นโยบายแบบนี้เปลี่ยน gateway จากคำแนะนำเป็นข้อบังคับ: pod ที่ลืมใส่ HTTP_PROXY จะไม่ออกไปข้างนอกผ่านมัน แต่มันจะได้ timeout แทน เรื่องแบบนี้เจอบน staging ภายในไม่กี่นาที ไม่ใช่ในโปรดักชันภายในหลายสัปดาห์

ความทนทานต่อความล้มเหลวและความจุ

  • อย่างน้อยสาม replica บนโหนดต่างกัน และ PodDisruptionBudget ที่ minAvailable: 2
  • มอนิเตอร์การเชื่อมต่อ: จำนวนที่เปิดอยู่ เวลาตั้ง CONNECT สัดส่วน 5xx จาก upstream
  • จำกัดจำนวนการเชื่อมต่อพร้อมกันจาก pod ไคลเอนต์หนึ่งตัว เพื่อไม่ให้เซอร์วิสที่พังตัวเดียวถม gateway
  • คำนึงถึง conntrack บนโหนด gateway: ที่หมื่นกว่าการเชื่อมต่อต่อวินาที ตารางจะเต็ม และคุณจะได้ packet loss ที่ลึกลับ ต้องเพิ่ม nf_conntrack_max ล่วงหน้า

การวินิจฉัยในคลัสเตอร์: วิธีตรวจสอบ IP ขาออกจาก pod และดูอาการล้มเหลวที่ไหน

ปัญหาพร็อกซีในคลัสเตอร์เจ้าเล่ห์ตรงที่อาการโผล่ไกลจากสาเหตุ ข้างล่างคือวิธีที่ทำให้ระบุจุดพังได้ในไม่กี่ขั้น

ขั้นที่ 1: ยืนยันว่าสภาพแวดล้อมถูกนำไปใช้จริง

kubectl -n crawlers exec deploy/price-collector -- env | grep -i proxy

มองหาทั้งสองตัวพิมพ์ ความครบของ NO_PROXY การไม่มีช่องว่างเกิน ถ้าแอปเริ่มผ่านสคริปต์ entrypoint ให้ตรวจสภาพแวดล้อมของโปรเซสหลักผ่าน /proc/1/environ ด้วย — มันอาจต่างจากที่ exec ซึ่งเปิดเชลล์ใหม่โชว์

ขั้นที่ 2: ตรวจสอบ IP ขาออก

Deploy เซอร์วิส echo ง่าย ๆ ในคลัสเตอร์หรือบนโฮสต์ภายนอกของบริษัท ที่คืนที่อยู่ไคลเอนต์และ header จากนั้นจาก pod:

kubectl -n crawlers exec deploy/price-collector -- curl -s https://echo.yourcompany.example/ip
kubectl -n crawlers exec deploy/price-collector -- curl -s --noproxy '*' https://echo.yourcompany.example/ip

คำสั่งแรกไปตามกฎของสภาพแวดล้อม คำสั่งที่สองบังคับให้เลี่ยงพร็อกซี ถ้าที่อยู่เหมือนกัน — ไม่ได้ใช้พร็อกซี ถ้าอันแรกคืน IP ของผู้ให้บริการพร็อกซี อันที่สองคืน IP ของโหนดหรือ NAT ของคลาวด์ แสดงว่าโครงสร้างทำงานแล้ว นี่ก็เป็นวิธีเดียวที่เชื่อถือได้ในการยืนยันว่าคุณเห็นที่อยู่ที่แผง Proxeon ให้มาจริง ๆ ไม่ใช่ที่อยู่ของโหนด

ขั้นที่ 3: ดูบทสนทนากับพร็อกซีในมุมมองไคลเอนต์

kubectl -n crawlers exec deploy/price-collector -- curl -sv https://api.partner.example/status 2>&1 | head -30

ในผลลัพธ์มองหาบรรทัดที่มี CONNECT และรหัสตอบกลับของพร็อกซีต่อมัน สัญญาณที่พบบ่อย:

  • 407 Proxy Authentication Required — เครดENTIALS ไม่ถึงหรือไม่ถูก ตรวจ URL-encoding ของรหัสผ่าน และว่าพร็อกซีคาดหวัง Basic auth จริง
  • 403 จากพร็อกซีต่อ CONNECT — โฮสต์หรือพอร์ตปลายทางถูกนโยบายพร็อกซีห้าม หรือ IP ของคุณไม่อยู่ใน whitelist เมื่อใช้ IP auth
  • 502/503 ต่อ CONNECT — พร็อกซีติดต่อปลายทางไม่ได้ ถ้าปลายทางอยู่ภายใน — นี่คือการวินิจฉัย «รูใน NO_PROXY»
  • Connection refused ไปที่ตัวพร็อกซี — ที่อยู่หรือพอร์ตผิด หรือ NetworkPolicy ไม่ปล่อยทราฟฟิกไป gateway
  • Timeout ไม่มีตอบกลับ — มักเป็นนโยบายเครือข่ายหรือเส้นทางที่ขาด ตรวจว่า DNS resolve ได้ไหมก่อน

ขั้นที่ 4: ตรวจการเรียกภายในคลัสเตอร์

kubectl -n crawlers exec deploy/price-collector -- curl -sv http://payments.default.svc.cluster.local:8080/healthz 2>&1 | grep -iE "connected|proxy|HTTP/"

ถ้าในผลลัพธ์มีการติดต่อพร็อกซี แทนที่จะเชื่อมต่อตรงกับ ClusterIP แสดงว่า NO_PROXY ไม่ครอบคลุมที่อยู่นี้ ทำซ้ำกับชื่อสั้น payments และ IP ของเซอร์วิส — ทั้งสามต้องไปตรงทั้งหมด

ขั้นที่ 5: ephemeral container สำหรับอิมเมจที่ไม่มีเครื่องมือ

ในอิมเมจขนาดเล็กไม่มีทั้ง curl และ nslookup ใช้ kubectl debug ที่จะต่อคอนเทนเนอร์ดีบักเข้าไปใน network namespace ของ pod:

kubectl -n crawlers debug -it pod/price-collector-7d9f4-x2k8q --image=registry.internal/netdebug:latest --target=app -- sh

ข้างในเข้าถึง IP เดียวกัน resolv.conf เดียวกัน sidecar เดียวกันบน localhost แต่สภาพแวดล้อมของแอปไม่ถูกสืบทอดมาตรงนี้: ตรวจตัวแปรผ่าน /proc/PID/environ ของโปรเซสหลัก

ขั้นที่ 6: log และ event

  • Log ของ sidecar หรือ gateway: รหัสตอบกลับ upstream, เวลา CONNECT, ติดต่อโฮสต์อะไร ตรงนี้แหละที่เห็นความพยายามส่งที่อยู่ภายในออกไปข้างนอก
  • Event ของ pod: kubectl describe pod จะโชว์ ImagePullBackOff — สัญญาณว่า container runtime ไม่ได้ตั้งค่าพร็อกซี หรือ registry ไม่อยู่ใน NO_PROXY ของมัน
  • Log ของ CoreDNS ในโหมด debug: NXDOMAIN พุ่งขึ้นกับชื่อภายนอก บอกว่าแอป resolve โฮสต์ภายนอกเอง แทนที่จะปล่อยให้พร็อกซีทำ
  • เมตริก conntrack บนโหนด egress: nf_conntrack_count เพิ่มขึ้นสู่ขีดจำกัด — ลางบอกเหตุของการสูญหายของแพ็กเก็ต

ขั้นที่ 7: แยกปัญหาพร็อกซีออกจากปัญหาปลายทาง

403 เดียวกันอาจมาจากพร็อกซีหรือจากเว็บปลายทาง การแยกแยะช่วยได้ด้วย header Via หรือ header เฉพาะของผู้ให้บริการในผลลัพธ์ รวมถึงเปรียบเทียบกับคำขอผ่าน --noproxy ถ้าผ่านพร็อกซีได้ 403 แต่ตรง ๆ ได้ 200 — เป็นเรื่องการตั้งค่าพร็อกซี หรือว่าแหล่งปลายทางจำกัดทราฟฟิกจากที่อยู่ของพูลนั้น ถ้า 403 ทั้งสองกรณี — ปัญหาอยู่ที่คำขอเอง

ข้อผิดพลาดที่พบบ่อย

มารวมกับดักที่คนเหยียบบ่อยที่สุด หลายข้อเราเล่าไปแล้ว นี่คือสรุปสำหรับตรวจเร็ว

  1. NO_PROXY copy มาจากเซิร์ฟเวอร์ ค่า localhost,127.0.0.1 ในคลัสเตอร์การันตีว่าทำการสื่อสารเซอร์วิสพัง ต้องมี .svc, .cluster.local, service และ pod CIDR
  2. ฝากความหวังกับ CIDR ใน NO_PROXY ของ Python หรือ curl เก่า ไลบรารีเมินค่าเงียบ ๆ ทราฟฟิกไป ClusterIP ผ่านพร็อกซี ใช้ชื่อ DNS และ suffix แทน
  3. ใส่แค่ตัวพิมพ์ใหญ่ curl ไม่อ่าน HTTP_PROXY ตัวใหญ่ กำหนดทั้งสองแบบ
  4. เครดENTIALS ใน manifest รหัสผ่านใน git และในผลลัพธ์ kubectl get -o yaml ใช้แค่ Secret ดีกว่าคือ IP auth ผ่าน gateway
  5. รหัสผ่านมีอักขระพิเศษโดยไม่ URL-encode ไคลเอนต์ parse URL ผิด ได้ connection refused ไปโฮสต์ที่ไม่มีอยู่
  6. คาดหวังว่า kubelet กับ containerd จะดึง env ของ pod ไปใช้ อิมเมจดึงไม่ได้ ทุกคนไปหาปัญหาใน Deployment แต่ที่ต้องคือ systemd drop-in บนโหนด
  7. ลืม registry ใน NO_PROXY ของ runtime registry ภายในเข้าไม่ได้ เพราะ containerd ไปผ่านพร็อกซีภายนอก
  8. Java กับ HTTP_PROXY JVM ไม่อ่านตัวแปรนี้ ต้องใช้ system property ผ่าน JAVA_TOOL_OPTIONS
  9. Sidecar เป็นคอนเทนเนอร์ธรรมดา ไม่ใช่ native init ที่มี restartPolicy: Always แอปเริ่มก่อนพร็อกซีและพังในคำขอแรก ๆ Job ไม่เคยจบ
  10. Egress gateway ไม่มี NetworkPolicy Gateway มีอยู่ แต่ pod ครึ่งหนึ่งไปทางอื่น และคุณรู้ตอนพาร์ทเนอร์ส่งรายชื่อ IP แปลก ๆ มาให้
  11. อัปเดต Secret แล้วไม่ rollout restart Pod เก่ายังใช้รหัสผ่านเก่า ที่คุณปิดไปแล้วในแผง
  12. มองข้าม metadata ของคลาวด์ 169.254.169.254 ไม่อยู่ใน NO_PROXY — cloud SDK ขอคีย์ชั่วคราวไม่ได้ แอปพังด้วยข้อผิดพลาด auth ลึกลับ
  13. ถมพร็อกซีที่อยู่เดียวด้วยทุก replica สามสิบ replica กระหน่ำที่อยู่ขาออกเดียว API พาร์ทเนอร์ตอบกลับด้วย rate limit กระจายโหลดไปยังพูลที่อยู่ผ่าน sidecar หรือ gateway

เครื่องมือและแหล่งข้อมูล

ชุดที่ควรมีติดมือตอนทำงานกับทราฟฟิกขาออกในคลัสเตอร์

เครื่องมือในตัว Kubernetes

  • kubectl exec และ kubectl debug — ตรวจสภาพแวดล้อมและการเชื่อมต่อเครือข่ายใน pod รวมถึง ephemeral container สำหรับอิมเมจขนาดเล็ก
  • kubectl describe pod และ event — ImagePullBackOff, probe ปฏิเสธ, ข้อผิดพลาดการ mount secret
  • NetworkPolicy — บังคับการออกผ่าน gateway และทดสอบสมมติฐาน «ใครไปทางอื่น»
  • Native sidecar container (restartPolicy: Always ใน initContainers) — วงจรชีวิตของพร็อกซีผู้ช่วยที่ถูกต้อง

การวินิจฉัยเครือข่าย

  • อิมเมจดีบักที่มี curl, dig, tcpdump, ss, iptables — สร้างของตัวเองจาก distro ขนาดเล็กและเก็บใน registry ภายใน
  • เซอร์วิส echo ของตัวเองที่คืน IP ไคลเอนต์และ header — วิธีที่ซื่อสัตย์ที่สุดในการเห็นที่อยู่ขาออก
  • tcpdump บนโหนดพร้อม filter ตาม IP ของ pod — เมื่อต้องเห็นชัด ๆ ว่าเชื่อมต่อไปที่อยู่ไหน
  • Log ของ CoreDNS ในโหมด debug — เพื่อเข้าใจว่าใคร resolve อะไร

การจัดการ secret และคอนฟิก

  • Secret manager ภายนอกที่ sync ลง Secret ของคลัสเตอร์ — เพื่อให้เครดENTIALS พร็อกซีไม่อยู่ใน git และหมุนเป็นศูนย์
  • Kustomize หรือ Helm กับ ConfigMap ร่วมสำหรับ NO_PROXY — ความจริงหนึ่งเดียวสำหรับทุก Deployment ในเนมสเปซ
  • Admission controller ที่ตรวจว่าทุก pod ที่มี HTTP_PROXY ต้องมี NO_PROXY — การป้องกันการ copy «จากเซิร์ฟเวอร์» อัตโนมัติ

ผู้ให้บริการพร็อกซี

ฝั่งพร็อกซีมีสามเรื่องสำคัญ: ความสามารถยืนยันตัวตนด้วย IP (สำหรับโครงสร้างกับ gateway) ที่อยู่คงที่ที่มีกรอบเวลาการผูกชัดเจน และ API สำหรับจัดการสิทธิ์เข้าถึง เพื่อให้การหมุนรหัสผ่านทำเป็นอัตโนมัติจาก CI ได้ Proxeon มีกลไกเหล่านี้ในแผงและผ่าน API ทำให้ฝังผู้ให้บริการเข้าในกระบวนการ GitOps ได้แทนการ copy สตริงการเชื่อมต่อด้วยมือ

เคสและผลลัพธ์

เคส 1: แพลตฟอร์ม ETL กับเซอร์วิสภายใน «ที่พัง»

ทีม data เพิ่ม HTTP_PROXY ใน Deployment ยี่สิบตัว เพื่อให้ตัวเก็บแค็ตตาล็อกมาร์เก็ตเพลสผ่านที่อยู่เฉพาะ NO_PROXY copy มาจากเอกสารของเซิร์ฟเวอร์: localhost กับ 127.0.0.1 ภายในหนึ่งชั่วโมง สัดส่วน 5xx ระหว่างเซอร์วิสภายในพุ่งจาก 0,1% เป็น 34% readiness probe ของสามเซอร์วิสเริ่มกระพริบ หลังเพิ่ม .svc, .cluster.local และ CIDR ทั้งสองใน ConfigMap ร่วมและ rollout restart ข้อผิดพลาดหายไปในห้านาที ผลพลอยได้: การย้าย NO_PROXY ไป ConfigMap ลดเวลาการเปลี่ยนรายการครั้งถัดไปจากครึ่งวัน (แก้ manifest ยี่สิบไฟล์) เหลือ commit เดียว

เคส 2: การอินทิเกรต JVM กับ sidecar

ผู้รวมระบบการเงินบน Java ที่มี HTTP client ต่างกันเจ็ดตัวในแอปเดียว (บางตัวผ่านไลบรารีที่ไม่システм property พร็อกซี) ความพยายามตั้งค่าผ่าน JAVA_TOOL_OPTIONS ครอบคลุมห้าไคลเอนต์จากเจ็ด ทางออกคือ native sidecar ที่ดักจับแบบโปร่งใสผ่าน iptables ไคลเอนต์ทั้งเจ็ดผ่านพร็อกซีโดยไม่แก้โค้ด เครดENTIALS หายไปจากแอป และเมตริกทราฟฟิกขาออกปรากฏขึ้นเป็นครั้งแรกในประวัติของเซอร์วิส ต้นทุน: 40 MB หน่วยความจำต่อ pod ซึ่งกับสิบสอง replica ถือว่าไม่มีนัย

เคส 3: egress gateway กับการยืนยันตัวตนด้วย IP

คลัสเตอร์หกสิบโหนดที่มี autoscaling ติดต่อ API พาร์ทเนอร์หลายตัวที่ต้องการ whitelist IP ทุกครั้งที่ scale โหนด ชุดที่อยู่ขาออกเปลี่ยน พาร์ทเนอร์บล็อกทราฟฟิก แก้โดย deploy egress gateway บนสามโหนดเฉพาะที่มีที่อยู่ผูกไว้ ใส่ NetworkPolicy ในทุกเนมสเปซแอป เปลี่ยน auth บนพร็อกซี Proxeon เป็นโหมด IP ผลลัพธ์: จำนวนอินซิเดนต์ «พาร์ทเนอร์บล็อกเราอีกแล้ว» ลดจากสาม-สี่ครั้งต่อเดือนเหลือศูนย์ในหนึ่งไตรมาส และในคลัสเตอร์ไม่เหลือ Secret กับรหัสผ่านพร็อกซีเลย ผลพลอยได้: log แบบรวมศูนย์ของ gateway เปิดเผยเซอร์วิสสองตัวที่ติดต่อ API ภายนอกโดยไม่มีใครรู้

เคส 4: containerd กับ ImagePullBackOff ลึกลับ

หลังย้ายคลัสเตอร์เข้าเครือข่ายที่บังคับพร็อกซี pod ที่ใส่ HTTP_PROXY ทำงานได้ แต่โหนดใหม่ดึงอิมเมจไม่ได้ สาเหตุ — ไม่มี drop-in สำหรับ containerd หลังเพิ่มไฟล์ที่มีพร็อกซีและ registry ภายในใน NO_PROXY ปัญหาหาย บทเรียนถูกรวบเป็นขั้นตอนตรวจในสคริปต์ bootstrap ของโหนด: ก่อน join คลัสเตอร์ โหนดตรวจว่า containerd ดึงอิมเมจทดสอบจาก registry ภายในและภายนอกได้

เช็กลิสต์ก่อนปล่อย

ผ่านรายการนี้ก่อนปล่อยพร็อกซีให้เซอร์วิสโปรดักชัน ทุกข้อคืออินซิเดนต์กลางคืนของใครสักคน

การเตรียม

  1. มีแผนที่การเชื่อมต่อขาออกของเซอร์วิส: โฮสต์ภายนอก เซอร์วิสภายใน ฐานข้อมูล คิว endpoint ของคลาวด์ โปรโตคอล
  2. รู้จัก service CIDR, pod CIDR, node CIDR ของคลัสเตอร์ และโดเมนภายในของบริษัท
  3. ตรวจแล้วว่าแอปใช้ HTTP client อะไร และแต่ละตัวตีความ NO_PROXY อย่างไร (CIDR, suffix, ตัวพิมพ์)
  4. เลือกระดับ: คอนเทนเนอร์, sidecar หรือ gateway จดบันทึกการตัดสินใจไว้

คอนฟิก

  1. NO_PROXY อยู่ใน ConfigMap ของเนมสเปซ และมี: localhost, 127.0.0.1, .svc, .svc.cluster.local, .cluster.local, kubernetes.default.svc, service CIDR, pod CIDR, 169.254.169.254, โดเมนภายใน
  2. ตัวแปรกำหนดทั้งสองตัวพิมพ์
  3. เครดENTIALSอยู่ใน Secret เท่านั้น; รหัสผ่าน URL-encode แล้ว; RBAC บน secret จำกัด
  4. สำหรับ JVM ตั้ง JAVA_TOOL_OPTIONS พร้อม nonProxyHosts
  5. ถ้าใช้ sidecar — เป็น native (initContainers ที่มี restartPolicy: Always) พร้อม readinessProbe และขีดจำกัดทรัพยากร
  6. ถ้าใช้ gateway — อย่างน้อยสาม replica, anti-affinity, PDB, NetworkPolicy ในเนมสเปซแอป
  7. Container runtime บนโหนดตั้งค่าแยก; registry ภายในอยู่ใน NO_PROXY ของมัน

ตรวจบน staging

  1. env ใน pod แสดงค่าที่คาดไว้
  2. คำขอไปเซอร์วิส echo คืนที่อยู่พร็อกซี; กับ --noproxy — คืนที่อยู่โหนด
  3. คำขอไปเซอร์วิสภายในด้วยชื่อสั้น ชื่อเต็ม และ ClusterIP ไปตรง (เห็นใน curl -v)
  4. Readiness และ liveness ผ่านเสถียรอย่างน้อยสิบนาที
  5. ไคลเอนต์ API Kubernetes (ถ้ามี) เห็น control plane
  6. Log ของ sidecar หรือ gateway ไม่มีการติดต่อที่อยู่ภายใน

ปล่อยและหลังจากนั้น

  1. Rollout เป็นขั้นตอนด้วย maxUnavailable ไม่เกิน 25%; มอนิเตอร์ 5xx ระหว่างเซอร์วิส และข้อผิดพลาด 407/403 จากพร็อกซี
  2. มีขั้นตอนการหมุนรหัสผ่านจดไว้: อัปเดต Secret, rollout restart, ปิดการเข้าถึงเก่า
  3. ตั้ง alert กับการเพิ่มสัดส่วนข้อผิดพลาดจากพร็อกซี และการที่ conntrack ใกล้ขีดจำกัดบนโหนด egress
  4. อีกสัปดาห์ — ทบทวน log ของ gateway: มีปลายทางภายนอกที่ไม่คาดคิดโผล่มาไหม

FAQ

กำหนด HTTP_PROXY แบบ global ทั้งคลัสเตอร์ แทนที่จะตั้งในทุก Deployment ได้ไหม?

ไม่มีกลไกตรง ๆ: ตัวแปรสภาพแวดล้อมเป็นส่วนหนึ่งของ pod spec วิธีปฏิบัติคือ ConfigMap ร่วมกับ envFrom ในทุก manifest ผ่าน Helm หรือ Kustomize หรือ mutating admission controller ที่เพิ่มตัวแปรอัตโนมัติให้ pod ที่มี annotation เฉพาะ วิธีที่สองทรงพลังกว่า แต่ต้องระวัง: มันต้องเพิ่ม NO_PROXY ด้วย ไม่งั้นระบบอัตโนมัติจะทำเซอร์วิสพังยกแผง

ทำไม curl ใน pod ผ่านพร็อกซี แต่แอป Python ไม่ผ่าน ทั้งที่สภาพแวดล้อมเดียวกัน?

น่าจะเป็นเรื่องตัวพิมพ์หรือการ parse NO_PROXY curl อ่าน http_proxy เฉพาะตัวพิมพ์เล็ก Python อ่านทั้งคู่ ในทางกลับกัน Python ไม่เข้าใจ CIDR ใน NO_PROXY ขณะที่ curl ใหม่เข้าใจ ตรวจทั้งสองสมมติฐาน: กำหนดตัวแปรทั้งสองตัวพิมพ์ และใช้ suffix ของโดเมนแทน CIDR

ต้องมี NO_PROXY ไหม ถ้าทราฟฟิกภายในทั้งหมดผ่าน sidecar ที่มีกฎ direct อยู่แล้ว?

ในทางเทคนิคไม่ต้องก็ได้ แต่เราแนะนำให้มี อย่างแรก hop เพิ่มผ่าน localhost สำหรับคำขอภายในนับพันคือ latency และโหลดที่ไม่ใช่ศูนย์บน sidecar อย่างที่สอง ถ้า sidecar ล่มหรือรีสตาร์ท แอปที่มี NO_PROXY ถูกต้องจะยังคุยกับเซอร์วิสภายในต่อได้ แต่ถ้าไม่มีจะเสียทุกอย่าง

รู้ได้อย่างไรว่าคำขอไปพาร์ทเนอร์ออกจากที่อยู่พร็อกซี ไม่ใช่ที่อยู่โหนด?

วิธีเดียวที่เชื่อถือได้ — คำขอทดสอบไปเซอร์วิส echo ที่คืน IP ไคลเอนต์ จาก pod เดียวกันและสภาพแวดล้อมเดียวกัน เปรียบเทียบคำตอบกับที่อยู่ที่แผง Proxeon แสดงสำหรับการเข้าถึงของคุณ และกับคำตอบเมื่อ --noproxy Log ของพาร์ทเนอร์เองก็ช่วยได้ แต่ปกติไม่ค่อยมีติดมือ

ควรเข้ารหัสการเชื่อมต่อถึงพร็อกซีไหม (schema https:// ใน HTTPS_PROXY)?

สำหรับทราฟฟิก HTTPS ไปเว็บปลายทาง เนื้อหาถูกเข้ารหัส end-to-end อยู่แล้วในอุโมงค์ CONNECT พร็อกซีเห็นแค่โฮสต์ปลายทาง TLS ถึงพร็อกซีป้องกันเครดENTIALSและชื่อโฮสต์ในเส้นทางจาก pod ถึงพร็อกซี ถ้าเส้นทางนี้ผ่านเครือข่ายที่ไม่น่าเชื่อถือ TLS ถึงพร็อกซีก็สมเหตุสมผล แต่ต้องแน่ใจว่าทั้งไคลเอนต์และพร็อกซีรองรับ: ไลบรารีบางตัวเมิน schema https:// ใน HTTPS_PROXY ภายในคลัสเตอร์ระหว่าง pod กับ egress gateway ปกติ network policy ก็เพียงพอ

ทำอย่างไรกับทราฟฟิกฐานข้อมูลและ broker ที่ต้องออกนอกคลัสเตอร์?

มันไม่ผ่าน HTTP proxy — มันไม่ใช่ HTTP ทางเลือก: โหมด SOCKS5 ถ้าผู้ให้บริการและไลบรารีไคลเอนต์รองรับ; การดักจับแบบโปร่งใสที่ระดับ sidecar พร้อม TCP tunneling; หรือ egress gateway ที่ทำงานระดับการจัดเส้นทาง ไม่ใช่โปรโตคอล ทางเลือกขึ้นอยู่กับว่าคุณต้องการ IP ภายนอกเฉพาะเจาะจง หรือแค่จุดออกแบบรวมศูนย์ก็พอ

ใช้พร็อกซีตัวเดียวกันกับ containerd และกับแอปได้ไหม?

ในทางเทคนิคได้ แต่ปกติเป็นงานคนละอย่าง Runtime ดึงอิมเมจจาก registry สาธารณะ ต้องใช้พร็อกซีของ perimeter องค์กร แอปติดต่อ API พาร์ทเนอร์ ต้องใช้ที่อยู่เฉพาะที่มีคุณสมบัติกำหนด การผสมกันทำให้การดึงอิมเมจใช้โควตาทราฟฟิกที่ตั้งใจไว้สำหรับงานแอป แยกกันดีกว่า

เปลี่ยนรหัสผ่านพร็อกซีโดยไม่มีดาวน์ไทม์ได้อย่างไร?

สร้างการเข้าถึงชุดที่สองที่ผู้ให้บริการ อัปเดต Secret เป็นเครดENTIALS ใหม่ ทำ rollout restart แบบทยอยอัปเดต replica ยืนยันจาก log ว่าไม่มี 407 แล้วค่อยปิดการเข้าถึงเก่า ถ้าใช้ IP auth ผ่าน gateway การหมุนรหัสผ่านก็หายไปจากรายการงานเลย

จำกัดให้พร็อกซีใช้เฉพาะโดเมนภายนอกเจาะจงได้อย่างไร?

NO_PROXY ทำงานกลับกัน — มันบอกว่าที่ไหนไม่ต้องผ่านพร็อกซี และสร้าง whitelist จากมันไม่ได้ ตรรกะแบบนี้ทำใน sidecar หรือ gateway: กฎแบบ «โดเมนในรายการ A — ผ่านพูลที่อยู่ Proxeon ที่เหลือ — ตรงหรือห้าม» บวก NetworkPolicy เพื่อให้การออกอินเทอร์เน็ตตรงไม่มีอยู่จริง

บทสรุป

ทราฟฟิกขาออกใน Kubernetes ต่างจากเซิร์ฟเวอร์ไม่ใช่ที่รายละเอียด แต่ที่โครงสร้างเอง: pod ทุกตัวมีสองโลก — ภายในและภายนอก และพร็อกซีต้องเห็นแค่โลกที่สอง จากตรงนี้ทุกอย่างที่เหลือตามมา NO_PROXY เปลี่ยนจากเรื่องพิธีการเป็นองค์ประกอบสถาปัตยกรรม ที่ต้องอธิบายเครือข่ายภายในคลัสเตอร์ทั้งหมด และคำนึงว่าไลบรารีต่าง ๆ ตีความมันต่างกัน ตัวแปรสภาพแวดล้อมใช้ได้เฉพาะในโปรเซส — kubelet, container runtime และ SDK ดื้อ ๆ ต้องดูแลแยก เครดENTIALSไม่ควรอยู่ใน manifest และในอุดมคติ — หายไปจากคลัสเตอร์เลยด้วย gateway ที่ใช้ IP auth

สามระดับของการฝังพร็อกซี — คอนเทนเนอร์, sidecar, egress gateway — ไม่ใช่คู่แข่ง แต่เป็นเครื่องมือสำหรับงานต่างกัน ระดับคอนเทนเนอร์เร็วและละเอียด Sidecar ตัดการพึ่งพาพฤติกรรมไลบรารี และเพิ่มเมตริก การหมุน การลองซ้ำ Gateway ให้คลัสเตอร์มีใบหน้าที่คาดเดาได้ต่อโลกภายนอก และทำให้การออกผ่านพร็อกซีไม่ใช่คำแนะนำ แต่เป็นกฎที่หนุนด้วย network policy

ทำอะไรต่อ? เริ่มจาก inventory: ทำแผนที่การเชื่อมต่อขาออกของเซอร์วิสคุณ แล้วดูว่ากี่รายการเป็นภายใน จากนั้นย้าย NO_PROXY ไป ConfigMap ร่วม และตรวจบน staging ตามวิธีในหัวข้อวินิจฉัย ถ้าในคลัสเตอร์มีมากกว่าหนึ่งสแต็ก หรือมีข้อกำหนดเรื่องที่อยู่เสถียร — ออกแบบ egress gateway และต่อที่อยู่เฉพาะของ Proxeon ด้วยการยืนยันตัวตนด้วย IP และเก็บเช็กลิสต์ก่อนปล่อยไว้ใกล้มือ: มันคืนทุนแล้วตั้งแต่ตอนที่อินซิเดนต์แรกไม่เกิดขึ้น