การเขียน Kubernetes Controller, Part 7 — องค์ประกอบของ Terraform Runner
จากปัญหาเรื่อง Multi-Tenancy ทำให้ต้องออกแบบตัว sub-system ตัวใหม่ซึ่งก็คือ Terraform Runner ที่ต้องสามารถ 1. มี ServiceAccount เป็นของตัวเอง 2. อยู่ใน Namespace ที่กำหนด เพื่อให้ตอบโจทย์ Multi-Tenancy
เพื่อให้มี ServiceAccount เป็นของตัวเองก็เลยต้องเป็น Pod รันแยกออกมา
และเพื่อให้ TF-controller ควบคุม life-cycle ของตัว Runner Pod ได้ 100% นั้นก็ต้องทำตั้งแต่กลไกการสร้าง, ดึงค่า Pod IP มา แล้วก็ทำการเชื่อมต่อจาก Controller ไปยัง Runner Pod เพื่อเริ่มขั้นตอนแต่ละขั้นตอนของ Terraform ตั้งแต่การ Init ไปยัง Plan และ Apply
หลังจาก POC ไปหลาย ๆ วิธี พบว่าวิธีที่เหมาะสมระดับนึงก็คือการใช้ gRPC
โดยมี 2 แนวคิด แนวคิดแรก Pull Model, ให้ Terraform Controller เป็น Server ให้ Runner Pod เป็น Client เชื่อมต่อเข้ามา
แนวคิดที่ 2 คือ Push Model ให้ Terraform Controller เป็น Client ให้ Runner Pod แต่ละตัวเป็น Server
ทั้ง 2 Model จะมีปัญหาเชิง Security ทั้งคู่ แต่คนละมุม
ถ้าใช้ Pull Model (Runner Pod ดึงงานไปทำ) และให้ Terraform Controller เป็น Server ก็มีความเสี่ยงเชิง Security มากกว่า เพราะตัว Controller อาจจะต้องอยู่ใน Namespace เดียวกับ Flux Controller อื่น ๆ และมีสิทธิ์ที่จำเป็นเยอะกว่า Runner Pod
ถ้าเป็น Push Model (ให้ Terraform Controller ผลักงานไปให้ Runner Pod) ปัญหาเชิง Security จะลดลงเนื่องจาก Namespace ปลายทางจะเป็น Namespace ที่ Terraform Controller ต้องมีสิทธิ์ในการ Get / List ตัว Terraform Object อยู่แล้ว แต่ก็จำเป็นเรื่องสิทธิ์ในการสร้าง / Update / ลบ Pod ที่ต้องเพิ่มเข้ามา
แต่สิ่งที่ทำให้ Model นี้ปลอดภัยขึ้นก็คือ Terraform Controller สามารถลดสิทธิ์เรื่องการเข้าถึง Secret, ConfigMap ใน Namespace ปลายทางได้ โดยเราจะเอาสิทธิ์เหล่านี้ไปให้ Runner Pod แทน ซึ่ง Make Sense กว่าเพราะ Runner Pod อยู่ใน Namespace ปลายทางอยู่แล้ว
จากนั้นเราก็จะแก้ปัญหาเชิง Security ของการ Communication ด้วย mTLS ซะ จะได้ทำให้ Terraform Controller เท่านั้นที่ติดต่อทาง gRPC ไปหา Runner Pod ได้
แล้วก็ยังพบว่า Runner Pod ต้องการ Information บางอย่างจาก Terraform Object ตัวที่มันรับผิดชอบ แต่เพราะว่าต้องการจำกัดสิทธิ์ของ Runner Pod ให้น้อยที่สุด เลยใช้วิธีให้ Terraform Controller ส่ง Bytes Data ของตัว Object ไปให้ Runner Pod ผ่าน gRPC แทน ซึ่งวิธีนี้มีข้อดีอีกอย่างคือ ทั้ง Terraform Controller และ Runner Pod จะรับรู้ State ของ Object ที่กำลังจัดการอยู่ ว่าเป็นตัวเดียวกันจริง ๆ (ถ้ายอมให้ Runner Pod ดึง Terraform Object มาประมวลผลเองก็อาจจะเห็นตัว Object คนละ version กัน เพราะดึงมาจากคนละเวลากัน)
สิ่งที่ได้จาก Architecture แบบนี้คือ
เราได้ Logic ของการ Reconciliation แบบเดิมอยู่ที่ตัว Controller แต่ Delegate การประมวลผลของ Terraform แยกไปอยู่ใน Pod
การใช้ gRPC เพื่อ Re-Architect ทำให้ Reuse ตัว Test Case ระดับ Unit และ Integration ได้ทั้งหมดด้วยการแก้ Test เพียงเล็กน้อย อันนี้คือเจ๋งมาก ทำให้มั่นใจได้ว่า Logic ของการ Reconcile ยัง Work อยู่แม้จะวิ่งผ่าน gRPC
การ Re-Architect แบ่งออกเป็น 3 ระยะ
ระยะแยกทำเสร็จไปแล้วคือสร้าง Embedded gRPC Server ฝังไว้ในตัว Controller เพื่อแยกการรัน Terraform ไปเป็น gRPC แล้วฝังการเรียก gRPC แทนกลไกเดิมกลับไปในตัว Reconciliation Logic แล้ว Test ต้องผ่านหมด
โดยทำการ Double Check ผ่าน Test Coverage ว่า Code มันวิ่งผ่าน gRPC Server ที่สร้างขึ้นใหม่จริง ๆ
ระยะสองที่กำลังทำอยู่คือการเอา gRPC Server ไป Pack เป็น Container Image แล้วรันแยกเป็น Pod แล้วทำให้เกิดการสร้าง Pod และเรียกใช้ไปยัง gRPC Server ของ Pod นั้น
ระยะสามที่ยังออกแบบอยู่ก็คือการใส่ mTLS ให้ gRPC และทำ Certificate Rotation แบบอัตโนมัติ ซึ่งยังไม่ได้ข้อสรุป