การใช้ S3 เป็นคอนเทนเนอร์รีจิสทรี
(ochagavia.nl)- โดยปกติคอนเทนเนอร์อิมเมจจะถูกอัปโหลดไปยังรีจิสทรีเฉพาะทาง แต่หาก เปิดเผย S3 bucket ผ่าน HTTP และวางไฟล์ไว้ตามพาธที่กำหนด ก็สามารถใช้งานเป็นเป้าหมายของ
docker pullได้ - ใน custom image builder ขั้นตอนการสร้างอิมเมจที่สามารถ pull ได้ให้เสร็จ ภายในไม่กี่วินาที กลายเป็นคอขวด และเวลาในการ push เลเยอร์กินสัดส่วนมาก
- ในเบนช์มาร์กการอัปโหลดเลเยอร์ขนาด 198 MiB พบว่า ECR ทำได้ 24–28 MiB/s ส่วน S3 ทำได้ 115–190 MiB/s ทำให้ S3 เร็วกว่าได้สูงสุด 8 เท่า
- ความต่างเกิดจาก S3 สามารถ อัปโหลดชังก์ของเลเยอร์เดียวแบบขนาน ได้ ขณะที่ ECR ซึ่งอิงตาม OCI Distribution Spec ต้องประมวลผลชังก์แบบลำดับ
- วิธีนี้ยังไม่สามารถทดแทน ความสามารถของรีจิสทรี เช่น
docker push, การตรวจสอบความถูกต้องของอิมเมจ, การสแกนความปลอดภัย, หรือการควบคุมสิทธิ์เข้าถึงรีโพสิตอรีแบบ private ได้ จึงควรมองว่าเป็นการปรับแต่งเชิงทดลอง
เงื่อนไขที่ทำให้ docker pull จาก S3 bucket ใช้งานได้
- หากต้องการใช้ S3 เสมือนเป็นคอนเทนเนอร์รีจิสทรี ต้อง เปิดเผย bucket ผ่าน HTTP และอัปโหลดไฟล์อิมเมจไปยังพาธที่ Docker คาดหวัง
- หากเงื่อนไขตรงกัน ก็สามารถดึงอิมเมจด้วย
docker pullได้เหมือนรีจิสทรีทั่วไป - อิมเมจสาธิตจะรัน cowsay และใช้ URL ของ bucket เป็นชื่ออิมเมจ ทำงานได้ด้วย
docker run --rm .../cowsay - ในเดโมใช้ Cloudflare R2
- เลือกเพราะไม่มีค่า egress
- R2 และ S3 เข้ากันได้ในระดับ API จึงใช้อัปโหลดอิมเมจไปยัง R2 ผ่าน AWS SDK
ทำไมถึงพิจารณา S3 แทนรีจิสทรีเฉพาะทาง
- โดยทั่วไปคอนเทนเนอร์อิมเมจจะโฮสต์บน รีจิสทรีเฉพาะทาง อย่าง DockerHub, GitHub Container Registry, หรือ ECR
- เป้าหมายของ custom image builder คือไปให้ถึงอิมเมจที่ pull ได้จากข้อกำหนดต่าง ๆ ภายในไม่กี่วินาที
- ในสภาพแวดล้อม AWS, ECR เป็นตัวเลือกที่ง่ายที่สุด แต่ในความเร็วการอัปโหลดจริงกลับเห็น ความต่างระหว่าง S3 กับ ECR อย่างชัดเจน
- เมื่อลองใส่ execution trace ลงในโค้ดเพื่อปรับแต่งประสิทธิภาพ ก็พบว่าเวลาที่ใช้ push เลเยอร์ไปยังคอนเทนเนอร์รีจิสทรีเป็นคอขวดสำคัญ
ผลการอัปโหลดเลเยอร์ขนาด 198 MiB
- เบนช์มาร์กขนาดเล็กนี้เปรียบเทียบเวลาและอัตรารับส่งข้อมูลในการอัปโหลด เลเยอร์ขนาด 198 MiB ไปยัง ECR และ S3 ตามลำดับ
- ความเร็วที่สังเกตได้มีดังนี้
- ECR: ต่ำสุด 24 MiB/s, 8.2 วินาที
- ECR: สูงสุด 28 MiB/s, 7.0 วินาที
- S3: ต่ำสุด 115 MiB/s, 1.7 วินาที
- S3: สูงสุด 190 MiB/s, 1.0 วินาที
- จากผลลัพธ์ S3 เร็วกว่า ECR ได้สูงสุด 8 เท่า
- โค้ดทดสอบรันอยู่บน AWS และทั้ง S3 กับ ECR เชื่อมต่อกันภายในผ่าน VPC โดยไม่ผ่านอินเทอร์เน็ตสาธารณะ
- เงื่อนไขนี้ช่วยให้ได้ latency และแบนด์วิดท์ที่ดีที่สุดเท่าที่เป็นไปได้
ความต่างของความเร็วจากการอัปโหลดชังก์แบบขนาน
- การอัปโหลดไปยัง S3 สามารถ อัปโหลดชังก์ของเลเยอร์เดียวแบบขนาน ได้
- หากมีแบนด์วิดท์เพียงพอ การอัปโหลดชังก์แบบขนานจะเพิ่ม throughput ได้มาก
- เอกสารของ AWS ก็แนะนำการอัปโหลดชังก์แบบขนานเพื่อใช้แบนด์วิดท์ให้เต็มประสิทธิภาพ
- ECR นั้นอิมพลีเมนต์ OCI Distribution Spec
- สเปกนี้คือมาตรฐานที่ทำให้
docker pullและdocker pushใช้งานข้ามหลายรีจิสทรีได้ - การ push เลเยอร์ต้องดำเนินไปแบบลำดับ ดังนั้นแม้จะอัปโหลดเป็นชังก์ ก็ต้องรอให้ชังก์ก่อนหน้าจบก่อนจึงไปต่อชังก์ถัดไปได้
- สเปกนี้คือมาตรฐานที่ทำให้
- เมื่อทดสอบการอัปโหลดแบบลำดับบน S3 เช่นกัน throughput จะลดลงมาอยู่ในระดับ ใกล้เคียงกับ ECR
โครงสร้างคำขอจริงของ docker pull
- คำขอภายในของ
docker pullประกอบด้วยคำขอ HEAD และ GET หลายรายการ - ตัวอย่างลำดับการทำงานมีดังนี้
- ตรวจสอบว่ามี image manifest หรือไม่:
HEAD /v2/my-image/manifests/latest - ดาวน์โหลด image manifest:
GET /v2/my-image/manifests/latest - ดาวน์โหลดอีกครั้งโดยอ้างอิงแฮชของ manifest:
GET /v2/my-image/manifests/sha256:... - ดาวน์โหลด blob เมทาดาทาของอิมเมจ:
GET /v2/my-image/blobs/sha256:... - ดาวน์โหลด blob เลเยอร์ของอิมเมจ:
GET /v2/my-image/blobs/sha256:...
- ตรวจสอบว่ามี image manifest หรือไม่:
- โดยสรุปแล้ว
docker pullใกล้เคียงกับกระบวนการดาวน์โหลดไฟล์ที่จำเป็นผ่าน HTTP - หาก static file server วางไฟล์ที่ต้องใช้ไว้ในพาธที่คาดหวัง และตั้งค่าเฮดเดอร์ Content-Type ให้ตรงกับแต่ละคำขอ ก็จะสามารถ pull คอนเทนเนอร์อิมเมจได้
- S3 bucket สามารถตอบโจทย์ทั้งสองเงื่อนไขนี้ได้ จึงอาจตั้งค่าอย่างระมัดระวังให้ทำงานเสมือนคอนเทนเนอร์รีจิสทรีได้
ข้อจำกัดของแนวทางเชิงทดลองนี้
- วิธีนี้ยังถือเป็น การทดลอง และยังยากจะสรุปแบบหนักแน่นก่อนจะมีการตรวจสอบเพิ่มเติม
- S3 ไม่ใช่คอนเทนเนอร์รีจิสทรีในความหมายที่เคร่งครัด
- ไม่สามารถทำ
docker pushได้ - ที่
docker pullใช้งานได้ เป็นผลจากโครงสร้างคำขอ HTTP และวิธีเสิร์ฟไฟล์แบบ static ที่สอดคล้องกัน
- ไม่สามารถทำ
- คอนเทนเนอร์รีจิสทรีทั่วไปมีความสามารถมากกว่าการอัปโหลดไฟล์ขึ้น bucket
- เชื่อถือได้ว่าอิมเมจที่อัปโหลดผ่านวิธี push มาตรฐานนั้นถูกต้องจริง
- สามารถมีการสแกนความปลอดภัยของเลเยอร์และแจ้งเตือนโดยอัตโนมัติ
- กำหนดสิทธิ์เข้าถึงรีโพสิตอรีแบบ private ได้ในตัว
- หากทำงานได้ตามที่คาดไว้ ก็อาจเปิดทางให้โฮสต์คอนเทนเนอร์อิมเมจแบบ public บน Cloudflare R2 ได้เช่นกัน
1 ความคิดเห็น
ความคิดเห็นบน Hacker News
รู้สึกเสียดายที่ OCI Distribution Spec อ่านแล้วไม่เหมือนสเปกที่ออกแบบมาอย่างดี
ตามสเปก การ push เลเยอร์ต้องทำแบบลำดับต่อเนื่อง ดังนั้นถึงจะอัปโหลดแบบ chunk ก็ต้องรอให้แต่ละ chunk เสร็จก่อนถึงไปต่ออันถัดไปได้ จากที่ทดสอบกับ DockerHub และ GHCR ดูเหมือนว่าตัว chunk upload เองก็พังอยู่ดี และฝั่งไคลเอนต์ก็มักจะอัปโหลดแต่ละ blob/เลเยอร์แบบทั้งก้อนอยู่แล้ว อีกทั้งสเปกยังแนะนำรูปแบบค่า
Content-Rangeที่ไม่สอดคล้องกับรูปแบบ RFC7233 ด้วยแน่นอนว่ายังมีการทำงานขนานกันได้ในระดับ blob แต่ไม่มีการทำงานขนานภายใน blob และยังน่าเสียดายที่พลาดโอกาสในการ ทำมาตรฐาน pagination สำหรับรายการแท็กด้วย เพราะข้อความที่เกี่ยวข้องในสเปกถูกลบออกไปโดยไม่ตั้งใจ [1] ทำให้แต่ละ registry ไปทำกันคนละแบบ
[1] https://github.com/opencontainers/distribution-spec/issues/4...
ไม่ได้จะด่าเสียทั้งหมดนะ เพราะมันปฏิวัติวงการจริง ๆ ทำให้การใช้ Linux namespace ง่ายกว่าสมัยก่อนมาก และเปลี่ยนโลกไปในทางที่ดีขึ้น เพียงแต่มันให้ความสำคัญกับ ประสบการณ์ผู้ใช้ มากกว่าความสมบูรณ์ทางเทคนิคมาโดยตลอด ซึ่งตัวมันเองก็ไม่ใช่เรื่องแย่เสมอไป เหมือนกับที่มีบริษัทน่าเบื่อจำนวนมากแก้ปัญหาราคาแพงด้วย Perl หรือ CSV ที่ส่งกันผ่าน FTP เทคโนโลยีที่น่าเบื่อหรือกระทั่งแย่ ถ้าจัดแพ็กเกจมาดีก็สร้างคุณค่าได้มาก
ถึงอย่างนั้นบางทีก็อดรู้สึกขมขื่นไม่ได้ว่ามันน่าจะดีกว่านี้ได้มาก
ตัวอย่างเช่น รูปแบบอย่าง
.dkr.ecr..amazonaws.com/foo/bar/baz:tagใช้ไม่ได้ และเก็บได้แค่แบบแบน ๆ ทำให้ชื่ออิมเมจหรือแท็กยาวเกินจำเป็น ตามทฤษฎีก็พอจะสร้างอ็อบเจ็กต์ ECR repository ใน Terraform เพื่อเลียนแบบพฤติกรรมคล้าย ๆ กันได้ แต่ก็ไม่เหมาะกับ pipeline ที่เส้นทางอิมเมจเป็นแบบไดนามิก ต้องให้สิทธิ์กับ IAM role ของ CI pipeline มากเกินน่ารำคาญ และก็ไม่ชอบที่ทรัพยากร AWS ถูกจัดการอยู่นอกที่เก็บ Terraform ส่วนกลางด้วย[1] https://stackoverflow.com/questions/64232268/storing-images-...
Cloudflare เคยโอเพนซอร์ส เซิร์ฟเวอร์คอนเทนเนอร์รีจิสทรี ที่ใช้ R2
เลยสงสัยว่ามีใครเคยลองใช้บ้างไหม
[1]: https://github.com/cloudflare/serverless-registry
สำหรับบางการใช้งานอาจไม่ใช่ปัญหาใหญ่ แต่บางกรณีก็อาจตัดทิ้งได้ตั้งแต่แรก
ผู้เขียนเอง ถ้าใครรู้ว่าทำไมในสเปก OCI การ push เลเยอร์ถึงต้องเป็นแบบลำดับ ก็อยากให้ช่วยบอกที
สงสัยว่าเป็นแค่อุบัติเหตุทางประวัติศาสตร์เฉยๆ หรือมีเหตุผลแฝงอยู่กันแน่ เพื่อให้ชัดเจน หลายเลเยอร์แน่นอนว่าสามารถ push แบบขนานกันได้ และที่พูดถึงตรงนี้คือส่วนที่ต้อง push เนื้อหาของเลเยอร์เดียว แบบตามลำดับ
N+Timeoutและลบทิ้งได้เท่ากับลดความซับซ้อนของรายละเอียดการ implement ว่าจะจัดการ partial upload อย่างไร ไม่อย่างนั้นทุกครั้งที่จบแต่ละชังก์ ก็ต้องคอยตรวจว่าชังก์อื่นมาครบหมดหรือยังแล้วค่อยปิดงาน แต่ก็ยังเป็นรายละเอียดระดับ implementation และก็น่าสงสัยว่าเป็นการออกแบบที่มีความหมายหรือตั้งใจไว้จริงหรือไม่ แนวทางแบบ S3 ดูน่าจะใช้ได้ดี และเคยทำอะไรคล้ายกันมาก่อนตอนอยู่บริษัทที่แจกจ่ายอิมเมจขนาดใหญ่ สุดท้ายค่าใช้จ่าย 0.10 ดอลลาร์ต่อ GB ต่อเดือน สะสมขึ้นมาไม่น้อย
แม้จะเสียฟีเจอร์เสริมของ ECR ไป แต่ส่วนตัวมองว่าฟีเจอร์พวกนั้นค่อนข้างจำกัด
ตอนที่ไคลเอนต์ปิดการอัปโหลด blob จะต้องส่ง digest ของ blob ทั้งก้อนมาด้วย ข้อกำหนดนี้ดูเหมือนมีไว้เพื่อให้เซิร์ฟเวอร์ตรวจสอบความถูกต้องของไบต์ที่ได้รับได้ ถ้าเซิร์ฟเวอร์เพิ่งเริ่มตรวจ digest ใน HTTP request สุดท้าย ก็ต้องย้อนกลับไปอ่านเนื้อหา blob ทั้งหมดที่เขียนลง storage จาก HTTP request ก่อนหน้าอีกครั้ง ซึ่งกับเลเยอร์ขนาดใหญ่ latency แบบนี้อาจรับไม่ไหว เคยตรวจให้รองรับได้ถึง 150GiB blob เพราะมีความต้องการจากไคลเอนต์บางราย
ดังนั้นใน implementation จึงคำนวณ digest ต่อเนื่องตลอดทั้งลำดับของ request ไปเลย รับข้อมูล blob เป็นชังก์ๆ พร้อมกับคำนวณ digest และสตรีมลง blob storage ไปพร้อมกัน ระหว่างแต่ละ request จะ serialize สถานะการคำนวณ digest ใส่ไว้ใน upload URL ที่ส่งกลับไปให้ไคลเอนต์ผ่าน
Locationheader จัดการประมาณนี้ในโค้ด: https://github.com/sapcc/keppel/blob/7e43d1f6e77ca72f0020645...เท่าที่ทราบ reference implementation ก็ใช้แนวทางเดียวกัน การคำนวณ digest ทำได้แบบลำดับเท่านั้น ดังนั้นการอัปโหลดจึงต้องดำเนินไปแบบลำดับด้วย
[1] https://github.com/sapcc/keppel
[2] https://github.com/distribution/distribution
ถ้ามีลิงก์ไปยัง GitHub repository ด้วยจะดีมาก ที่ถามเพราะกำลังหาวิธีสร้าง OCI image แบบเป็นโปรแกรมใน
$PROGRAMMING_LANGUAGEหรือไม่ก็คิดจะลงมือทำเอง อยากได้อะไรคล้าย Buildah แต่เป็น API สำหรับภาษาโปรแกรมจริงๆ ไม่ใช่ command-line interface แน่นอนว่าจะเรียก Buildah เป็น subprocess ก็ได้ แต่ค่อนข้างยุ่งยาก ต้องคอยจัดการปฏิสัมพันธ์กับสถานะภายในของ Buildah และการ cleanup ด้วย อีกทั้งตอนนี้ Buildah ก็ยังไม่รองรับ Macจำได้รางๆ ว่าเมื่อก่อนเหมือนเคยเพิ่ม parallel push ให้ Docker แต่อาจกำลังสับสนระหว่าง pull กับ push พอกลับไปดูอีกที งานที่เคยทำไม่ใช่การ push ขั้นสุดท้าย แต่เป็น การทำตรวจสอบแบบขนาน มากกว่า ถ้ามีการระบุว่าเลเยอร์หนึ่งอยู่ “บน” เลเยอร์ไหน ก็อาจเป็นเพราะ ID ที่อ้างถึงต้องมีอยู่ก่อนแล้วก็ได้
เป็น use case ที่เจ๋งมาก
ส่วนตัวผมใช้ Nexus ไปเลย มันทำงานได้ดีพอสมควร และรองรับตั้งแต่ OCI image, apt package, Maven แบบกำหนดเอง, NuGet, ไปจนถึง npm repository เพียงแต่การตั้งค่าและการใช้ทรัพยากรค่อนข้างน่าปวดหัว โดยเฉพาะเรื่อง นโยบายการ cleanup: https://www.sonatype.com/products/sonatype-nexus-repository
แต่ถึงอย่างนั้น สิ่งที่ดีมากจริงๆ คือ
docker pullมัน “ก็แค่” ชุดของคำขอHEADกับGETเท่านั้น เป็นการเอาสิ่งที่ใช้ได้ดีมานานแล้วมาใช้ต่อ โดยไม่ทำให้ซับซ้อนเกินจำเป็น อยากเห็นการตัดสินใจแบบมีสามัญสำนึกเช่นนี้ในเทคโนโลยีอื่นๆ มากกว่านี้ น่าแปลกที่คอนเทนเนอร์สโตร์แบบเรียบง่ายที่มีทั้งการยืนยันตัวตนและฟีเจอร์ cleanup กลับมีไม่มากนัก ทั้ง Nexus และ Harbor ต่างก็ซับซ้อนพอสมควรถ้าจะเอามาใช้งานจริงแปลกใจเหมือนกันที่ไม่มีใครพูดถึงในเธรดนี้เลย
CNCF Distribution หรือ Docker Registry เดิม มีฟังก์ชันรองรับรีจิสทรีด้วย CloudFront signed URL ที่ดึงจาก S3 อยู่แล้ว [1]
https://distribution.github.io/distribution/storage-drivers/...
สงสัยว่า https://github.com/distribution/distribution มีปัญหาอะไร
วิธีนี้ดูแพงมาก และน่าจะดีถ้าบทความพูดถึงเรื่อง ต้นทุน ด้วย อยากรู้ทั้ง S3 และ R2
ค่าทราฟฟิกขาออกสู่อินเทอร์เน็ตสาธารณะเท่ากัน แต่ ECR แบบ public มีข้อยกเว้นคือทราฟฟิกที่ออกไปสำหรับการใช้งานภายใน AWS นั้นฟรี
GET/PUTและค่าแบนด์วิดท์ได้จากเว็บไซต์ AWS: https://aws.amazon.com/s3/pricing/นอกเหนือจากเครื่องมือพัฒนาแล้ว ผมไม่ได้ใช้ Docker มากนัก แต่ไม่เคยเข้าใจว่าทำไม private container registry ถึงต้องมีอยู่
มันดูเหมือนการแสวงหาค่าเช่าเฉยๆ เลยสงสัยว่าจริงๆ แล้วมันมีข้อดีอะไร เมื่อเทียบกับการทำไฟล์อิมเมจที่จัดการเองแล้วใช้งานตามต้องการ
docker saveและdocker importก็พอdocker save alpine:3.19 > alpine.tardocker load < alpine.tarแต่จากนั้นก็ต้องมาจัดการไฟล์ tar นั้นเอง และทุกระบบต้องรู้ว่ามันอยู่ที่ไหนและจะเข้าถึงอย่างไร หรือจะไม่ต้องประดิษฐ์ล้อใหม่แล้วใช้วิธีที่ Docker มีให้อยู่แล้วก็ได้
ดังนั้นจึงต้องสร้าง registry เองหรือจ่ายเงินให้คนอื่นดูแลให้ แน่นอนว่าถ้าใช้อิมเมจเพื่อการพัฒนาอย่างเดียว เรื่องพวกนี้ก็ไม่มีความหมาย แค่เก็บอิมเมจไว้ในเครื่องพัฒนาก็พอ
เพราะต้องมีที่เก็บส่วนกลางที่มีเวอร์ชันเก่าทั้งหมดและให้ผู้ใช้งานหลายรายเข้าถึงได้ง่าย คุณคงไม่อยากคอย push แอปที่ build แล้วไปยังทุกที่ที่มันอาจถูกรัน แค่ build ครั้งเดียวแล้ว push ไปที่เก็บส่วนกลาง จากนั้นให้ทุกที่อ้างอิงที่เก็บนั้นก็พอ
และก็ไม่จำเป็นต้องจ่ายเงินสำหรับโฮสต์ private registry เสมอไป เพราะมีเครื่องมือสำหรับโฮสต์เองอยู่มากมาย
สามารถตั้งค่าทั้งหมดแยกตาม environment ได้ด้วย Terraform, Bicep, Pulumi
ECR ดูเหมือนถูกออกแบบมาให้สามารถอัปโหลด image layer แบบแบ่งเป็นหลายส่วนได้จริง
API ของ ECR ที่เกี่ยวข้องมี
InitiateLayerUpload APIที่เรียกตอนเริ่มอัปโหลดแต่ละ image layer,UploadLayerPart APIที่เรียกสำหรับแต่ละ chunk ของ layer (สูงสุด 20MB) และPutImage APIที่ push image manifest ซึ่งมีการอ้างอิงถึง image layer หลังอัปโหลด layer เสร็จแล้ว จุดแปลกคือการอัปโหลด layer chunk ต้องใช้ base64 encoding ทำให้ข้อมูลเพิ่มขึ้นประมาณ 33%ไอเดียที่ใช้การจัดวาง file path เป็นวิธีควบคุม endpoint น่าสนใจ
แต่อยากรู้ว่าจะจัดการ
Docker-Content-Digestheader อย่างไร แม้จะไม่บังคับ แต่แนะนำให้ใส่ไว้ใน response และ client จำนวนมากก็คาดหวังมันอยู่ โดยอาจปฏิเสธ layer ที่ไม่มี header นี้ได้ อีกทั้งฟีเจอร์อย่าง referrers API ในสเปก OCI 1.1 ก็อาจหายไปด้วย ดูเหมือนจะ implement ได้ค่อนข้างยาก