Docker run ภายใต้ network ของ container อื่น
Tags: docker, go
Published: 2021-05-07 01:10:42

กำลังลองเล่น package chromedp https://github.com/chromedp/chromedp ซึ่งเป็น package ช่วยให้เราเขียน Go ไปต่อกับ Chrome Engine แล้วทำอะไรต่างๆได้แบบ browser ทำ โดยผ่านการเขียนโปรแกรมเอง ประโยชน์ก็ใช้สำหรับทำ web bot ทำ web crawler/scrapper ต่างๆนั่นเอง

ประเด็นของโพสต์นี้คือ กำลังพยายามเอามันไปรันภายใต้ Docker ซึ่งไม่มี Chrome อยู่ในนั้น อย่างไรก็ตาม Chrome มีวิธีให้เชื่อมต่อหามันผ่าน remote โดยใช้ Chrome DevTools Websocket endpoint

ประเด็นต่อมา เราต้องรัน Chrome headless ซึ่งเป็น Chrome engine แบบไม่ต้องติดตั้ง Chrome application นั่นเอง ดีที่มีคนทำ docker image เอาไว้แล้วที่นี่ https://hub.docker.com/r/chromedp/headless-shell/

ดังนั้นเราจะใช้งาน headless-shell ก็รันผ่าน docker แบบนี้

```
docker run -d -p 9222:9222 --rm --name headless-shell chromedp/headless-shell
```

ต่อมาผมจะลองเล่นโค้ดตัวอย่างของ chromedp จากโค้ดนี้ https://github.com/chromedp/examples/blob/master/remote/main.go

```go
// Command remote is a chromedp example demonstrating how to connect to an
// existing Chrome DevTools instance using a remote WebSocket URL.
package main

import (
	"context"
	"flag"
	"log"

	"github.com/chromedp/chromedp"
)

func main() {
	devtoolsWsURL := flag.String("devtools-ws-url", "", "DevTools WebSsocket URL")
	flag.Parse()
	if *devtoolsWsURL == "" {
		log.Fatal("must specify -devtools-ws-url")
	}

	// create allocator context for use with creating a browser context later
	allocatorContext, cancel := chromedp.NewRemoteAllocator(context.Background(), *devtoolsWsURL)
	defer cancel()

	// create context
	ctxt, cancel := chromedp.NewContext(allocatorContext)
	defer cancel()

	// run task list
	var body string
	if err := chromedp.Run(ctxt,
		chromedp.Navigate("https://duckduckgo.com"),
		chromedp.WaitVisible("#logo_homepage_link"),
		chromedp.OuterHTML("html", &body),
	); err != nil {
		log.Fatalf("Failed getting body of duckduckgo.com: %v", err)
	}

	log.Println("Body of duckduckgo.com starts with:")
	log.Println(body[0:100])
}
```

โดยเอามาใส่ go.mod เพื่อจัดการ package ให้ ซึ่ง go.mod มีโค้ดแบบนี้อยู่

```
module example-chromedp-remote

go 1.16

require github.com/chromedp/chromedp v0.7.1
```

เสร็จแล้วก็สร้าง Dockerfile ให้มันแบบนี้

```
FROM golang:1.16.4-alpine as builder

WORKDIR /app

COPY go.mod /app
COPY main.go /app

RUN go mod download
RUN go build -o app

FROM alpine
COPY --from=builder /app/app /app
ENTRYPOINT ["/app"]
```

จากนั้นทำการ build docker ด้วยคำสั่ง

```
docker build -t example-chromedp-remote .
```

สุดท้าย เราจะรัน example-chromedp-remote โดยส่ง ws url ของ headless ไปเป็น option ให้มัน ส่วนวิธีหา ws url นั้นทำแบบนี้

```
curl 127.0.0.1:9222/json/version
{
   "Browser": "Chrome/90.0.4430.93",
   "Protocol-Version": "1.3",
   "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36",
   "V8-Version": "9.0.257.23",
   "WebKit-Version": "537.36 (@4df112c29cfe9a2c69b14195c0275faed4e997a7)",
   "webSocketDebuggerUrl": "ws://127.0.0.1:9222/devtools/browser/68424ed8-a10a-4633-9618-f93b443e0aa9"
}
```

เราจะเอา URL ตรง `"webSocketDebuggerUrl"` ไปใช้งานนั่นเอง

ต่อไปก็สั่งรัน example-chromedp-remote แบบนี้

```
docker run --rm --network="container:headless-shell" example-chromedp-remote -devtools-ws-url="ws://127.0.0.1:9222/devtools/browser/68424ed8-a10a-4633-9618-f93b443e0aa9"
```

จุดที่ทำให้รันโดยเชื่อมต่อไปให้มันเป็น network เดียวกัน IP เดียวกันได้คือ option `--network="container:headless-shell"` นั่นเอง pattern มันคือมี `container:` ด้านหน้า ตามด้วยชื่อ container ที่จะเอาไปเชื่อมต่อ (ref: https://docs.docker.com/engine/reference/run/#network-container)

แถมท้าย ถ้าใช้ docker compose แทนที่จะรันเองตรงๆนั้น จะใช้ config key ที่ชื่อ network_mode แต่ว่าแทนที่จะกำหนดชื่อ container ตรงๆ เราใช้ชื่อ service แทนได้โดยใช้ prefix `service:` ตามด้วยชื่อ service ตัวอย่างเช่น 

```yaml
---
version: "3.8"
services:

  chrome-headless:
    image: "chromedp/headless-shell"
    ports:
    - "9222:9222"

  example-chromedp-remote:
    build:
      context: .
    network_mode: "service:chrome-headless"
```

จากนั้นก็สั่งรัน chrome-headless ผ่าน docker compose แบบนี้

```
docker compose up -d chrome-headless
```

แล้วก็รัน example-chromedp-remote แบบนี้

```
docker compose run --rm example-chromedp-remote -devtools-ws-url="ws://127.0.0.1:9222/devtools/browser/cdaa6d7f-3d08-4593-ad4d-0d630abcd627"
```

## สรุป

ถ้าจะเชื่อมต่อไปใช้ network ของ container อื่นๆ ผ่าน docker run ให้ใช้ option --network ค่าที่กำหนดคือ `container:` ตามด้วยชื่อ container ที่จะไปต่อ เช่น `--network="conatiner:headless"`

แต่ถ้าใช้ docker compose ให้กำหนดโดยคีย์ที่ชื่อว่า `network_mode` ค่าที่กำหนดคือ `service:` ตามด้วยชื่อ service ที่จะไปต่อ เช่น `network_mode: "service:chrome-headless"`


## ขอฝาก Buy Me a Coffee
สำหรับท่านใดที่อ่านแล้วชอบโพสต์ต่างๆของผมที่นี่ ต้องการสนับสนุนค่ากาแฟเล็กๆน้อยๆ สามารถสนับสนุนผมได้ผ่านทาง Buy Me a Coffee คลิ๊กที่รูปด้านล่างนี้ได้เลยครับ

<a href="https://www.buymeacoffee.com/weerasak" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-yellow.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>

ส่วนท่านใดไม่สะดวกใช้บัตรเครดิต หรือ Paypal สามารถสนับสนุนผมได้ผ่านทาง PromptPay โดยดู QR Code ได้จากโพสต์ที่พินเอาไว้ได้ที่ Page DevDose ครับ https://web.facebook.com/devdoseth