Docker: init daemon

หัวข้อนี้จะลงลึกไปถึงระบบลินุกซ์นะครับ แต่ก็เป็นจุดที่คนใช้ Docker ควรจะทราบไว้เพราะเป็นอะไรที่้ใช้ OS ปกติไม่เจอ ใช้ Docker จะเจอ แล้วเราจึงจะต้องเตรียมรับมือเอาไว้อย่าง signal forwarding

Signal forwarding

เมื่อเราสั่ง sudo docker stop นั้น Docker จะส่งสัญญาณ SIGTERM ให้โปรแกรมแรกใน container (process ID 1 หรือ PID 1) ซึ่งโปรแกรมทั่วๆ ไปเมื่อได้รับสัญญาณนี้แล้วก็จะปิดตัวเองไป หรือเราอาจจะเขียนโค้ดมาให้โปรแกรมเซฟข้อมูลก่อนก็ได้ (แทบทุกภาษาทำได้ครับ แม้แต่ shell script)

ปัญหาก็คือ… แน่ใจได้อย่างไรว่าโปรแกรมของเราได้รับสัญญาณจาก Docker เพราะถ้าไม่ได้รับแล้วสั่ง stop ไปมันก็จะไม่หยุดทำงาน (จน Docker timeout แล้ว kill ทิ้งแบบไม่สนใจ)

  • จุดแรกที่บางคนอาจจะไม่เคยสังเกตเลยคือ ถ้าเราใช้คำสั่ง CMD/ENTRYPOINT ในรูปแบบ string (CMD x) โปรแกรมแรกที่เปิดใน container คือ sh -c "x" ทำให้ sh เป็น PID 1 ไม่ใช่โปรแกรมของเรา วิธีที่ถูกคือต้องใช้ในรูปของ array (CMD ["x"]) (เลิกใช้แบบ string เถอะครับ ไม่มีเหตุผลจำเป็นเลย)
  • จุดที่สองคือคำสั่ง su/sudo นั้นก็ทำตัวเป็น PID 1 ได้ครับ ถ้าจำเป็นต้องใช้จริงๆ แนะนำ gosu ซึ่งมันทำงานเสร็จแล้วมันจะสลายตัวไปเอง ทั้งนี้ถ้าใช้ในลักษณะ shell script ใช้ sudo ก็ไม่ผิดครับ
  • จุดถัดมา บางคนเอา shell script เป็น entrypoint เพื่อให้มัน setup ก่อนเรียกโปรแกรม อันนี้ก็ต้องตรวจดูว่าการเรียกโปรแกรมของเรานั้นจะต้องใช้คำสั่ง exec นำหน้าด้วยนะครับ เช่น exec python /app/main.py เพื่อให้ sh ถูกแทนที่ด้วย python ไม่ใช่ให้ sh เป็นแม่ของ python

เหตุผลคือโปรแกรมทั้ง sh/bash/su/sudo นั้นจะไม่มีการส่งต่อสัญญาณหาโปรแกรมลูกครับ

ปัญหายังไม่จบครับ ปัญหาต่อมาคือโดยปกติ OS จะมี default behavior เวลาได้รับ signal ต่างๆ แต่พอโปรแกรมถูกรันเป็น PID 1 OS จะไม่มี default ให้ครับ นั่นคือถ้าไม่ได้เขียนระบุไว้ว่า SIGTERM ให้ปิด โปรแกรมของเราจะไม่ทำอะไรเลย (ใน Docker เองถ้า stop 10 วินาทีแล้วไม่ปิดมันจะ kill ทิ้งครับ นี่คือเหตุผลที่ stop โปรแกรมบางตัวนานมาก)

ปัญหานี้จะหมดไปถ้า container ของเราทำงานตามหลักที่ OS กำหนดให้ครับ โดยวิธีง่ายที่สุดก็คือการลง init daemon

Init daemon

ในระบบ UNIX ทั่วๆ ไปแล้วโปรแกรม process ID 1 นั้นจะเป็นโปรแกรมประเภท init daemon

init ที่ใช้กันก็มีหลายเจ้า ไม่ว่าจะเป็น sysvinit, upstart ของ Ubuntu, systemd ที่กำลังมาแรงในขณะนี้, OpenRC หรือแม้แต่ OS X ก็มี init ของตัวเองชื่อ launchd แต่ใน Docker เราจะไม่เอาโปรแกรมพวกนี้มาใช้ เพราะมันไม่ได้ออกแบบมาเหมาะสำหรับ container เท่าไร (ผมเคยมี container ที่รัน systemd เวลารันต้อง mount ไฟล์ระบบเข้าไป แถม host os ต้องเป็น systemd อีกต่างหาก)

เท่าที่เห็นกันในโลก Docker ตัวที่ใช้หลักๆ มีดังนี้ครับ

tini

สำหรับการใช้งาน tini นั้นค่อนข้างง่ายครับ เพิ่มใน Dockerfile ว่า

ENV TINI_VERSION v0.10.0
ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini
RUN chmod +x /tini
ENTRYPOINT ["/tini", "--"]

หรือถ้าใช้ alpine ก็

RUN apk add --no-cache tini
ENTRYPOINT ["/sbin/tini", "--"]

(โปรแกรมของเราให้ใช้ CMD ใส่ได้เหมือนเดิมครับ)

s6

อีกตัวหนึ่งที่อยากแนะนำตัว และผมใช้อยู่ปัจจุบันคือ s6 ครับ โดย s6 จะเหมือนกับ init ทั่วไปของลินุกซ์มากกว่า คือสามารถ start process หลายๆ ตัวได้ด้วย และมีความสามารถอื่นๆ หลายอย่าง เช่นคำสั่งแทน sudo, logrotate แต่มีขนาดเล็ก

ผมใช้ s6 กับ s6-overlay ซึ่งเหมือนเป็น s6 ที่ config เพิ่มความสามารถเข้ามาดังนี้ครับ

  • การ set permission file ก่อนรัน (อาจจะใช้กับ volume ที่ mount มาตอน runtime)
  • รันคำสั่งก่อนเริ่มทำงาน

วิธีการใช้ s6-overlay ไม่ยาก ทำตาม guide ของเค้าได้เลย

ติดตั้ง

วิธีติดตั้งไม่ยากครับ ให้ใส่ใน Dockerfile ว่า

ADD https://github.com/just-containers/s6-overlay/releases/download/v1.11.0.1/s6-overlay-amd64.tar.gz /tmp/
RUN tar xzf /tmp/s6-overlay-amd64.tar.gz -C /
ENTRYPOINT ["/init"]

แล้วโปรแกรมของเราก็ใช้ CMD ["..."] ได้ตามปกติเลย หรืออาจจะใช้วิธี start service จาก config ก็ได้ครับ

Config

สำหรับไฟล์ config หลักๆ ของ s6-overlay มีดังนี้ครับ

/etc/fix-attrs.d/

สำหรับ folder นี้จะถูกรันเป็นลำดับแรก โดยมีหน้าที่ setup permission ต่างๆ มีรูปแบบดังนี้

/var/lib/mysql true mysql 0600 0700

แปลว่าให้กำหนดให้ /var/lib/mysql เป็นของ user mysql (สามารถระบุแบบ user:group ได้เลย) ให้ไฟล์ภายในมีสิทธิ์ 0600 โฟลเดอร์ 0700 และให้เซตแบบ recursive ด้วย (จากคำสั่ง true)

สำหรับการตั้งชื่อไฟล์ตั้งอะไรก็ได้ครับ แต่โดยปกติแล้วจะตั้งเป็น 01-name เพื่อจะได้กำหนดลำดับการรันก่อน-หลังได้

/etc/cont-init.d

ใน folder นี้จะเก็บ shell scripts ที่จะรันก่อนเปิดโปรแกรมของเรา อย่าลืม chmod +x ให้ไฟล์ด้วยนะครับ และตั้งชื่อไฟล์ปกติก็จะเหมือนกันคือ 01-name

shell script ใน s6 จะไม่เห็น environment ที่เรากำหนดเข้ามา (docker run -e ...) นะครับ ถ้าจะให้เห็นให้เราเปลี่ยน #!/bin/sh เป็น #!/usr/bin/with-contenv sh

/etc/services.d

มาถึงจุดสำคัญแล้วครับ นั่นคือการกำหนดโปรแกรมที่จะรันใน container ของเรา ซึ่งจะมีหลายตัวก็ได้ครับ

(แต่อย่าลืมว่า container ไม่ใช่ VM รันที่จำเป็นจริงๆ ก็พอ ถ้าเป็นไปได้แยกโปรแกรมละ container ไปเลยยิ่งดี)

วิธีใช้งานคือให้สร้าง folder ย่อยลงไป เช่น /etc/services.d/nginx/ แล้วด้านในมีไฟล์ชื่อ run (อย่าลืม chmod +x ไฟล์นี้) เป็น shell script เขียนคำสั่งที่ต้องการรัน (อย่าลืมใช้คำสั่ง exec)

ถ้าโปรแกรมของเราปิดตัว s6 จะพยายาม restart ให้เองครับ ถ้าไม่ต้องการให้เราสร้างอีกไฟล์มาชื่อ finish (เช่นกัน chmod +x ด้วย) และเขียนข้างในว่า

#!/usr/bin/execlineb -S0

s6-svscanctl -t /var/run/s6/services

โดย finish จะเป็น script ที่รันทุกครั้งที่โปรแกรมเราถูก restart และคำสั่งในนี้ก็คือคำสั่งบอกให้ s6 ปิดตัวนั่นเอง

s6 ยังมีฟีเจอร์อีกมากมาย ลองศึกษาได้จากเว็บของ s6 เอง และของ s6-overlay ดูครับ

ไม่ใช้ init ได้ไหม

หน้าที่ที่ PID 1 ที่ OS กำหนดให้มีดังนี้ครับ

  1. ส่งสัญญาณที่ OS ให้มาไปหาโปรแกรมลูก
  2. ทำตามสัญญาณที่ OS ส่งมาให้ ซึ่งปกติโปรแกรมทั่วไป OS จะเซต default ให้ แต่ไม่ใช่กับ PID 1
  3. ถ้ามี zombie process (process ที่ตายแล้วและ process ผู้สร้างมันก็ตายไปแล้วเช่นกัน) PID 1 จะต้องบอก OS ว่าไม่ใช้งานแล้ว เพื่อให้ OS คืนหน่วยความจำ

จะเห็นว่าหน้าที่พวกนี้โปรแกรมของเราไม่ได้ทำเลยครับ (แม้แต่ sh/bash ก็ไม่ได้ทำ) ฉะนั้นแล้วการใช้ init จึงจะทำให้ container ของเราทำงานได้อย่างที่เราคิดไว้ คือสั่ง stop แล้วปิดได้จริงๆ ถ้าโปรแกรมของเรามีการเรียกโปรแกรมอื่นๆ ก็จะทำให้ไม่เกิด memory leak ขึ้น

ตรงข้อ 3 นี้ถ้าไม่ได้แยก process ลูกไปมั่วๆ ก็ไม่จำเป็นเท่าไรครับ และโปรแกรมบางตัวมีการจัดการ SIGTERM เองอยู่แล้ว (เพราะจะทำอะไรบางอย่างก่อนปิดโปรแกรม) เราเลยจะเห็นว่าอิมเมจของโปรแกรมดังๆ จะไม่ได้ใช้ init เท่าไร แต่ในโปรแกรมของเราก็ลองพิจารณาดูครับ

Docker Tip & Tricks

อยากแนะนำมาสักพักกับทริคพื้นฐานที่แนะนำคนอื่นไปบ้างแล้วใน Docker เลยขอรวมรวมไว้ในบล็อคนี้แล้วกัน

คำสั่งพื้นฐานที่ควรทราบ

  • ก๊อปไฟล์เข้าออกจาก container ด้วยคำสั่ง sudo docker cp container:/path/to/file file หรือกลับกันก็ได้ถ้าจะเอาไฟล์เข้า (อันนี้ไม่แน่ใจว่าถ้าใช้ Host OS ที่ไม่ใช่ลินุกซ์จะทำได้ไหม)
  • รัน shell ใน container ที่ใช้งานอยู่ได้ด้วยคำสั่ง sudo docker exec -it container bash (ถ้าอิมเมจที่ใช้ base จาก alpine จะต้องใช้คำสั่ง sh แทน bash)
  • อย่าลืมว่าเราสามารถทดลอง build image ด้วยมือได้ด้วยการ run shell ใน image ตัวแม่ของมัน
  • ใส่ -d ให้ run เพื่อ run container เป็น background โดยจะขึ้น container id มาให้สำหรับเวลาจะใช้หยุดทีหลัง (หรือจะหาชื่อย่อจาก docker ps ก็ได้)
  • ใส่ --restart=always ให้ run เพื่อ restart container อัตโนมัติ (สามารถใช้แทน supervisord ได้เลย) รวมถึงตอนเปิดเครื่องใหม่ด้วย
    • ถ้าลืมใส่สามารถใช้ sudo docker update container --restart=always ได้
  • container ที่ run เสร็จแล้วจะถูกเก็บไว้ ต้องไปตามลบ วิธีง่ายที่สุดคือ sudo docker rm `sudo docker ps -aq` (ps -aq คือให้แสดง id ของ container ทั้งหมด) หรือก่อน run ให้ใส่ --rm เพื่อให้ Docker ลบให้อัตโนมัติ

Security

มีบทความแนะนำเทคนิคในการใช้ Docker เพื่อกลายเป็น root อยู่ครับ ซึ่งสิ่งที่ใช้มีแค่ว่า account บนเครื่องนั้นที่เข้าถึง Docker daemon ได้ นั่นแปลว่า user group docker นั่นไม่ต่างกับให้ sudo ไม่ใส่รหัสผ่านนะครับ

ในขณะเดียวกัน ข้างใน container ก็ควรจะรันแอพด้วยสิทธิ์ผู้ใช้ปกติ (คำสั่ง USER ใน Dockerfile + อย่าลืมสร้าง user ก่อนด้วย) เพื่อลดสิทธิ์ของโปรแกรมลงไปอีก

  • ถ้าใช้ Debian สร้าง user ด้วยคำสั่ง useradd -d /app -M -s /bin/false app (user app, home folder /app)
  • ถ้าใช้ Alpine ใช้คำสั่ง adduser -h /app -s /bin/false -HD app

Image best practice

คำสั่ง RUN หนึ่งคำสั่งควรจะทำงานและเก็บกวาดให้จบในคำสั่งเดียว เพราะถ้าแยกคำสั่งเป็นหลายๆ RUN จะทำให้เกิด layer แยกกัน เช่น ถ้า RUN อันแรกสร้างไฟล์ 50GB ขึ้นมา และ RUN ถัดมาลบไฟล์นั้นออก อิมเมจที่ได้จะมีขนาด 50GB อยู่ดี

  • ถ้าสั่ง apt-get update ต้องลบ cache ออกด้วยคำสั่ง rm -rf /var/lib/apt/lists/* (ไม่ต้องสั่ง apt-get clean เพราะใน official image Debian/Ubuntu จะตั้งให้รันอัตโนมัติอยู่แล้ว)
  • ถ้าใช้ alpine ควรใช้คำสั่ง apk add --no-cache package หรือถ้าใช้ alpine ต่ำกว่า 3.3 ให้สั่ง rm -rf /var/cache/apk/* ตาม

ความแตกต่างของ ADD กับ COPY

ใน Dockerfile จะมีคำสั่งนึงที่คล้ายกันมากคือ ADD กับ COPY ซึ่งบางทีเราใช้แล้วก็จะเห็นว่าไม่ต่างกันเลย แต่จริงๆ แล้ว ADD เก่งกว่า COPY ครับ โดย

  • ADD สามารถระบุต้นทางเป็น URL ได้ โดย docker จะ download มาให้เลย (ทั้งนี้ไม่สามารถใช้ build cache ได้นะครับ ถ้าจะใช้ก็คงต้อง RUN curl เอา)
  • ถ้า ADD file tar/tar.gz/tar.bz2/tar.xz ที่มาจากบนเครื่อง (ใช้ URL ไม่ได้) docker จะแตกไฟล์ให้เลย

ฉะนั้นแล้วโดยทั่วไปถ้าไม่จงใจใช้ความสามารถเหล่านี้ ใช้ COPY ดีกว่าครับ ไม่สับสน และไม่มี surprise ครับ

ความแตกต่างของ CMD กับ Entrypoint

คำสั่งอันหนึ่งที่มือใหม่หัดเขียน Dockerfile อาจจะงงถึงความแตกต่างคือ CMD กับ ENTRYPOINT

สมมุติว่ารัน sudo docker run image sh จะได้ผลดังนี้

  • ถ้าเขียนว่า CMD ["x"] จะรันคำสั่ง sh ในอิมเมจ
  • ถ้าเขียนว่า ENTRYPOINT ["x"] จะรันคำสั่ง x sh ในอิมเมจ สังเกตว่าจะเอา argument ที่ใส่ให้ docker run ไปต่อท้ายคำสั่งที่กำหนดไว้

ฉะนั้นแล้ว ENTRYPOINT จะมีประโยชน์ในกรณีที่คำสั่งชื่อคล้ายๆ กับโปรแกรม เช่น sudo docker run willwill/imagecleanup --numbered test แต่ข้อเสียคือถ้าโปรแกรมเปิดไม่ติดแล้วจะเข้าไป debug ข้างในจะยุ่งยากกว่า

การบังคับ entrypoint ให้ใช้ของเราแทนที่ของอิมเมจทำได้โดยใช้คำสั่ง --entrypoint เช่น sudo docker run --entrypoint sh willwill/imagecleanup

เพิ่มเติมศึกษาได้จากหัวข้อ Know the Differences Between CMD and ENTRYPOINT ใน docs ของ Project Atomic

Container registry

มาถึงช่วงขายของ (ที่ผมก็ไม่ได้ตัง) กันบ้างครับ ตอนผมเอา Docker มาใช้ในเว็บนี้ก็ตามหา Docker registry อยู่เหมือนกัน ว่าจะเอา container ไปโฮสต์แบบส่วนตัวได้ที่ไหนกันบ้าง เลยอยากมาแนะนำให้รู้จักกันครับ

ตัวเลือกแรกเลยฟรีๆ อย่าง GitLab ซึ่งตอนนี้มีบริการ Container registry ให้แล้วครับ รวมถึงตัวฟรีอย่าง Community Edition ด้วย

GitLab ยังมีบริการ CI ด้วยนะครับ สามารถ build image และ push จากใน GitLab ได้เลย วิธีเซตอาจจะต้องงมๆ นิดหน่อยเพราะเอกสารเกี่ยวกับ Docker ยังไม่ค่อยมี แต่ดีที่คือไม่ต้องใส่รหัสผ่านเราลงไปให้ CI เพราะ GitLab CI จะสร้างรหัสผ่านสำหรับ push มาให้เป็นพิเศษเลย

ข้อเสียที่ผมเจอคือความเร็วการ push ค่อนข้างจะช้ากว่าเจ้าอื่นๆ หน่อย บางทีก็ต้อง push layer ด้านล่างซ้ำ และต้องใช้ username/password ของ GitLab เพื่อ pull ซึ่งก็ไม่เหมาะจะไปใส่ใน server สักเท่าไร อาจจะต้อง create user เฉพาะของ GitLab ไปเลย (ในขณะที่ถ้าเป็นบริการ git จะสามารถใช้ deploy key ได้)


มาถึงฝั่งเสียเงินกันบ้างครับ เจ้าแรกคือ Docker Hub นี่แหละ ซึ่งให้บริการ private repo ฟรี 1 อัน (แต่คิดว่าถ้าเอาอิมเมจหลายๆ อันตั้งชื่อเดียวกันแล้วเปลี่ยน tag ไปก็อาจจะได้นะ…)

สำหรับฟีเจอร์ก็จะมี automated build เช่นเดียวกันครับ รองรับ GitHub และ BitBucket และเห็นว่าตัวเสียเงินจะมีบริการ security scanning ด้วย ข้อดีอีกอย่างหนึ่งคือมันคือ default repository ฉะนั้นก็จะ pull ได้สะดวกหน่อย

สำหรับการคิดเงินจะคิดเป็นต่อ repo ครับ เริ่มที่ $7/เดือน/5 repo ไม่มีการจำกัดขนาด


เจ้าถัดมาครับ Quay.io เจ้าของเดียวกับ CoreOS ลองดูตัวอย่างหน้าตาได้จาก quay.io/coreos/hyperkube ก็ได้ครับ

ฟีเจอร์และราคาก็คล้ายๆ กันครับ คือมี automated build, security scanning เพิ่มเติมมาคือมีรหัสสำหรับ pull อย่างเดียว และ audit log และหน้าตาจะดูดีกว่า Docker Hub พอสมควร

สำหรับการคิดเงินเริ่มที่ $12/เดือน/5 repo ไม่คิดขนาดเช่นกันครับ


ทางฝั่งผู้ให้บริการ Cloud กันบ้างครับ เริ่มจาก Amazon EC2 Container Registry (ECR) บ้าง ซึ่งจุดขายหลักคงต้องเป็นเรื่องของราคาเลยครับ เพราะ ECR ไม่คิดค่าบริการเพิ่มแต่อย่างใด นอกเหนือไปจากค่า storage บน S3 และ data transfer (ฉะนั้นแล้วอยู่ในโครงการทดลองใช้ฟรี 1 ปี 500MB ด้วย) ซึ่งโดยส่วนตัวแล้วผมค่อนข้างชอบโมเดลนี้มากครับ เพราะหลักๆ อิมเมจผม build มาแล้วจะไม่ค่อยได้อัพเดตเท่าไร แต่จะมีจำนวนอิมเมจพอสมควร ฉะนั้นการคิดตามพื้นที่ก็ถือว่าคุ้มกว่ามากๆ และค่า storage ของ S3 ค่อนข้างถูกมากครับ จะแพงก็ตรงค่า data transfer มากกว่า ถ้าไม่ได้ pull วันละหลายๆ รอบ ผมว่าถูกกว่า registry เจ้าอื่นแน่นอน

ข้อเสียหลักๆ ของ ECR คือ URL ในการ push ที่ยาวยืดมากครับ คือ accountid.dkr.ecr.ap-southeast-1.amazonaws.com/reponame/ และ GUI ที่พื้นฐานมากๆ คือ list ดูอิมเมจและลบได้อย่างเดียว ไม่มีฟีเจอร์อื่นๆ เพิ่มเติมอย่าง automated build หรือ security scanning

การ login เข้า ECR ก็ค่อนข้างยุ่งยากครับ คือสามารถใช้ IAM สร้าง API Key ที่มีสิทธิ์จำกัดในการใช้ Registry ได้ แต่ API Key นั้นเอาไปใช้ตรงๆ ไม่ได้ ต้องเรียก API เพื่อสร้างรหัสผ่านมาก่อน แล้วเอารหัสผ่านนั้นไปใส่ docker login อีกทีหนึ่ง และรหัสที่ได้มีอายุเพียง 12 ชั่วโมงเท่านั้นด้วย


google-cloud-platform[1]

เจ้าสุดท้ายเป็น cloud ไฟแรงอย่าง Google Cloud Platform ครับ ซึ่งก็ต้องบอกว่าเว็บผมก็ host image อยู่บน Google Cloud นี่แหละ

ฟีเจอร์ก็ใกล้เคียงกับ AWS ครับ คือ GUI มีไว้แค่เปิดดูกับลบแค่นั้น ไม่มีหน้า public ใดๆ ราคาก็เช่นเดียวกันครับ ไม่คิดค่าบริการนอกเหนือจาก storage + data transfer (เดือนที่แล้วผมจ่าย Google Cloud ไป 30 บาทเอง รวมถึงค่า VM ที่ใช้อยู่พักนึงด้วย) มีให้ทดลองฟรี $300 ใช้ให้หมดภายใน 60 วัน

สำหรับ URL ในการ push/pull นั้นจะใช้ asia.gcr.io/abcd-1234/imagename (abcd คือชื่อที่เราตั้ง ส่วนตัวเลขสุ่มให้) ครับ ถ้าใช้ gcr.io เฉยๆ ก็จะเป็นโซนอเมริกาไป

เวลา push จะต้องใช้คำสั่ง sudo gcloud docker push ... เพื่อให้ gcloud สร้างรหัสมา login หรือเราอาจจะสร้าง service account JSON key แล้ว login ถาวรไปเลยก็ได้เช่นกัน โดยไม่ต้องอาศัยคำสั่ง gcloud

registry ของ Google นี้จะไม่ใช้ permission แยกออกไปอย่าง AWS แต่จะอาศัย permission จาก Google Cloud Storage เลย ฉะนั้นการกำหนดสิทธิ์อะไรก็เข้าไปกำหนดระดับไฟล์ได้เลย

แต่ตรงนี้ก็ขอบอกว่า interface ของ Google Cloud Storage ยังแพ้ S3 อยู่นะครับ กว่าผมจะ setup account ใหม่มา pull ที่มีสิทธิ์จำกัดได้ก็เอาการอยู่เหมือนกัน แต่ถ้าจะเอาง่ายๆ ก็ grant readonly ทั้งหมดให้ไปเลยก็ได้เช่นกัน

ข้อเสียอีกจุดนึงของ Google Cloud คือยังไม่มีที่สิงค์โปรครับ ใกล้สุดที่เค้าเรียกว่า asia นั่นคือไต้หวันครับ แต่ที่ใช้ทั่วๆ ไปก็ยังไม่เห็นความต่างเท่าไรนะ ping ไปเครื่องบน Google Cloud ไต้หวันก็ 70ms เอง (เทียบกับ DigitalOcean ประมาณ 20ms)