Automate deploy using Watchtower in Docker


มาลองทำความรู้จักกับ Watchtower กันในโพสต์นี้ครับ
What is Watchtower?
Watchtower เป็น open source ตัวนึง ที่จะคอยดู docker image ให้เราว่ามี image ตัวใหม่ pushed มาแล้วหรือยัง
ดูเพิ่มเติมที่ github
การทำงานของ Watchtower จะเป็นแบบ pull-based นะ
เราตั้งเวลาไว้ว่าจะให้ตรวจสอบเป็นช่วงๆห่างกันกี่วินาที ถ้าหาก Watchtower เจอว่ามี docker image ตัวใหม่ มันก็จะ pull มาให้ แล้วก็จะ stop การทำงานตัวเก่า แล้ว start container ด้วย image ตัวใหม่ให้เราเอง
หรือจะตั้งให้ Watchtower ไปตรวจสอบเป็นช่วงเวลาได้ด้วย ด้วยการใช้ Cron expression
นอกจากนี้เรายังสามารถ set ให้ Watchtower ทำ Zero downtime ให้เราได้ด้วย (ไม่ได้ zero downtime ซะทีเดียวนะ แต่ก็ถือว่า nearly zero downtime ได้แหละ) คือหลังจาก pull image มาแล้ว Watchtower จะไป start container ตัวใหม่ก่อน ถ้าสำเร็จทำงานได้มันจะไป stop container ตัวเก่าทีหลัง
เอาละเริ่มน่าสนใจแล้วใช่มะ
ยังก่อน
ก่อนจะไปเริ่ม
คำเตือน
อยากให้คำเตือนไว้ก่อน คือใน docs เขียนไว้ชัดเลยว่า ไม่ควรใช้ Watchtower กับ production server นะ ให้ใช้บน Dev server, UAT server เท่านั้น ถ้าอยากใช้บนเครื่อง prod แนะนำให้ไปใช้ Kubernetes จะดีกว่า ซึ่ง K8S distro ก็มีหลายตัว ซึ่งเค้าก็แนะนำ MicroK8s กับ K3S
แต่ๆ ผมก็เห็นว่า Dream of code ใช้บน production server เลยด้วย
ส่วนตัวผมนั้นก็ไม่ได้ใช้บนเครื่อง prod แต่จะใช้ที่ environment อื่นๆ แต่งานที่ผมทำเดี๋ยวนี้ก็ไม่ค่อยได้ deploy ด้วย docker แล้ว ส่วนใหญ่จะไป deploy บน K8S หมดแล้ว
แต่ก็มาลองเล่นกันดู
สร้าง API ง่ายๆกันก่อน
สร้าง api ด้วยอะไรก็ได้เอาง่ายๆเลย เพราะมันไม่ใช่ topic ของเรา ในที่นี้ผมจะใช้ Go Echo นะ เพราะมัน build ไว แล้ว image ก็เล็กตอน push จะได้ไม่รอนาน
go mod init api
go get github.com/labstack/echo/v4
สร้าง file
- main.go
- Dockerfile
- compose.yaml
main.go
package main
import ( "net/http"
"github.com/labstack/echo/v4")
func main() { e := echo.New() e.GET("/", func(c echo.Context) error { return c.String(http.StatusOK, "Hello, World") }) e.Logger.Fatal(e.Start(":1323"))}
สร้าง Dockerfile
Dockerfile
# Build stageFROM golang:alpine AS builder
# Set working directoryWORKDIR /app
# Copy go mod and sum filesCOPY go.mod go.sum ./
# Download all dependenciesRUN go mod download
# Copy the source codeCOPY . .
# Build the application with optimizations# Statically linked binary with no external dependenciesRUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ -a -installsuffix cgo \ -ldflags='-w -s -extldflags "-static"' \ -o main .
# Final stageFROM scratch
# Copy the binary from builderCOPY --from=builder /app/main /main
EXPOSE 1323
# Command to run the applicationENTRYPOINT ["/main"]
ตัว Dockerfile ไม่ได้มีอะไรซับซ้อนเลย
ให้ build แล้ว push ขึ้น dockerhub ได้เลย
ในที่นี้ผม push ขึ้นไปที่ repo ของผมเองชื่อว่า hadesgod/api:prod
docker build -t hadesgod/api:prod .docker push hadesgod/api:prod
สร้าง compose file
compose.yaml
services: watchtower: image: containrrr/watchtower environment: - WATCHTOWER_LABEL_ENABLE=true # which service need to allow watchtower to watch new image that service should put the label - WATCHTOWER_POLL_INTERVAL=30 # check new image every 30 seconds - WATCHTOWER_ROLLING_RESTART=true # restart the one was changed image detected and got zero downtime deployment as well volumes: - /var/run/docker.sock:/var/run/docker.sock api: labels: - "com.centurylinklabs.watchtower.enable=true" image: hadesgod/api:prod ports: - 3333:1323
ส่วนของ docker compose file จะอธิบายให้ดีหน่อย แบบนี้
ที่ watchtower service เราจะใส่ Environment หลายตัว ค่อยๆดูไปทีละตัวตามนี้นะ
-
WATCHTOWER_LABEL_ENABLE=true
อันนี้ตั้งให้ watchtower ไม่ต้องไปเช็คว่ามี image ใหม่หรือเปล่า ไม่ว่าจะ service ไหนก็แล้วแต่
ถ้าเราไม่ได้ระบุแบบเจาะจงที่ image ตัวไหนก็ไม่ต้องไปดู image ตัวใหม่ ที่ต้องทำแบบนี้เพราะว่าโดยปกติ Watchtower จะไปเช็คทุก services เลย แต่เราอยากกำหนดเองว่าอยากให้ Watchtower คอยเช็ค image ของ service ที่เราต้องการเท่านั้น และถ้าเรามี sevice อื่นๆที่เราไม่ได้เขียนเองเช่น Redis เราก็ไม่จำเป็นต้องไปคอยเช็ค image ตัวใหม่ใช่มะ ไม่งั้นเจอ tag ที่เป็น version ใหม่เจอ breaking chaged ไป app เราก็แตกพอดี เอาแค่ app ที่เราเขียนก็พอนะซึ่งวิธีการบอกให้ watchtower รู้เนี่ย ก็คือต้องใส่ label เข้าไป ดูตัวอย่างที่ api service ก็ได้ จะเห็นว่าใส่ label แบบนี้
labels: - "com.centurylinklabs.watchtower.enable=true"
ด้วย env ตัวนี้ทำให้ watchtower จะไปเช็ค image ของ service นี้ละว่ามี update มาบ้างหรือยัง ก็ทำตามกระบวนการต่อไป
-
WATCHTOWER_POLL_INTERVAL=30
อันนี้ก็สั่งให้ watchtower ไปดูว่ามี image ตัวใหม่มาแล้วหรือยังในทุกๆ 30 วินาที -
WATCHTOWER_ROLLING_RESTART=true
อันนี้ watchtower จะไปหยุดการทำงานอันเก่า start อันใหม่ เราจะได้ near zero downtime จากตรงนี้แหละ
จากนั้นก็สั่ง
docker compose up -d
ลองแก้ code ของเรา
เราจะแก้ code
แล้ว build image ใหม่
แล้ว push ขึ้นไปที่ docker hub ด้วย tag prod
เหมือนเดิม
main.go
func main() { e := echo.New() e.GET("/", func(c echo.Context) error { return c.String(http.StatusOK, "--> Hello, Watchtower <--") // แก้ตรงนี้ }) e.Logger.Fatal(e.Start(":1323"))}
แล้วก็ build & push ได้เลย
docker build -t hadesgod/api:prod .docker push hadesgod/api:prod
รอประมาณ 30 วินาที Watchtower น่าจะทำการสร้าง container ใหม่มาให้เราละ
ย้ำอีกทีว่ามีความเสี่ยง take your own risk นะครับ