延續上一篇的內容,這篇文章要先來討論比較好懂的 Router
部分。
首先,在 Deis 的設計裡面,基本上所有的服務都是包成一個 Image 作為 Continaer 在 CoreOS 運行的。就這點來看,其實是非常符合 Mircoservice 架構的設計。同時我們也可以很輕鬆地將這些服務獨立出來使用,這篇文章討論的 Router
除了原本的用途外,也很適合用來學習透過 etcd 部署自動化更新設定檔的環境。
Deis 的原始碼都放在一起,其中 Router 部分是裡面的一個子目錄,那麼就讓我們開始了解運行的架構吧!
Dockerfile
1FROM alpine:3.2
2
3# install common packages
4RUN apk add --update-cache \
5 bash \
6 curl \
7 geoip \
8 libssl1.0 \
9 openssl \
10 pcre \
11 sudo \
12 && rm -rf /var/cache/apk/*
13
14# install confd
15RUN curl -sSL -o /usr/local/bin/confd https://s3-us-west-2.amazonaws.com/opdemand/confd-git-73f7489 \
16 && chmod +x /usr/local/bin/confd
17
18# add nginx user
19RUN addgroup -S nginx && \
20 adduser -S -G nginx -H -h /opt/nginx -s /sbin/nologin -D nginx
21
22COPY rootfs /
23
24# compile nginx from source
25RUN build
26
27CMD ["boot"]
28EXPOSE 80 2222 9090
29
30ENV DEIS_RELEASE 1.13.0-dev
透過 Dockerfile 可以簡單了解到這個服務是怎麼啟動的,整體上來說非常簡單,除了安裝 confd 之外。就是把名為 rootfs
的目錄加進去,並且編譯客製化的 Nginx 接著啟動伺服器。
這邊比較特別的地方是客製化編譯 Nginx 的部份,主要是因為 Deis 除了基本的 Nginx 功能外,也增加了防火牆模組(NAXSI)跟一些模組在裡面,有興趣的可以自行閱讀
rootfs/bin/build
這個檔案的內容。
boot
我想大家會覺得奇怪,為什麼不是直接執行 Nginx 而是執行一個名為 boot
的指令呢?
這是因為 Deis 除了啟動 Nginx 之外,還要將像是 confd 之類的服務也一併啟動。
這邊比較有趣的是,一般會用 Shell Script 來處理。但是 Deis 使用 Golang 來做處理。
從 Makefile 可以看出 boot
這個檔案是透過 Golang 的 Cross-compile 功能所編譯後,再構出 Docker 的 Image 來使用。
1build: check-docker
2 GOOS=linux GOARCH=amd64 CGO_ENABLED=0 godep go build -a -installsuffix -v -ldflags '-s' -o $(BINARY_DEST_DIR)/boot cmd/boot/boot.go || exit 1
3 @$(call check-static-binary,rootfs/bin/boot)
4 docker build -t $(IMAGE) .
5 rm rootfs/bin/boot
所以如果要自己封裝,記得要用
make build
的指令,而不是直接docker build
否則是會包出無法執行的 Image。
從 cmd/boot/boot.go
可以發現引用了 logger/stdout_formatter.go
這個檔案,基本上就是統一格式化 Deis 輸出的紀錄檔訊息,因此這邊就不多做討論。
1func main() {
2 // 略
3
4 log.Debug("reading environment variables...")
5 host := getopt("HOST", "127.0.0.1")
6
7 etcdPort := getopt("ETCD_PORT", "4001")
8
9 etcdPath := getopt("ETCD_PATH", "/deis/router")
10
11 hostEtcdPath := getopt("HOST_ETCD_PATH", "/deis/router/hosts/"+host)
12
13 externalPort := getopt("EXTERNAL_PORT", "80")
14
15 client := etcd.NewClient([]string{"https://" + host + ":" + etcdPort})
16
17 // wait until etcd has discarded potentially stale values
18 time.Sleep(timeout + 1)
19
20 log.Debug("creating required defaults in etcd...")
21 mkdirEtcd(client, "/deis/config")
22 mkdirEtcd(client, "/deis/controller")
23 mkdirEtcd(client, "/deis/services")
24 mkdirEtcd(client, "/deis/domains")
25 mkdirEtcd(client, "/deis/builder")
26 mkdirEtcd(client, "/deis/certs")
27 mkdirEtcd(client, "/deis/router/hosts")
28 mkdirEtcd(client, "/deis/router/hsts")
29
30 setDefaultEtcd(client, etcdPath+"/gzip", "on")
31
32 log.Info("Starting Nginx...")
33
34 go tailFile(nginxAccessLog)
35 go tailFile(nginxErrorLog)
36
37 nginxChan := make(chan bool)
38 go launchNginx(nginxChan)
39 <-nginxChan
40
41 // FIXME: have to launch cron first so generate-certs will generate the files nginx requires
42 go launchCron()
43
44 waitForInitialConfd(host+":"+etcdPort, timeout)
45
46 go launchConfd(host + ":" + etcdPort)
47
48 go publishService(client, hostEtcdPath, host, externalPort, uint64(ttl.Seconds()))
49
50 log.Info("deis-router running...")
51
52 exitChan := make(chan os.Signal, 2)
53 signal.Notify(exitChan, syscall.SIGTERM, syscall.SIGINT)
54 <-exitChan
55 tail.Cleanup()
56}
基本上,整個 main()
分為兩個部分。
第一個部分是讀取環境變數的部份(透過 getopt()
方法),第二個部分則是啟動各項服務的部份。這邊比較特別的是利用 Golang 的 Channel 功能,做出依序啟動服務的效果。
Golang 的 Channel 在做
receive
動作時,會阻止程式繼續運作。
最後的 signal.Notify
可以設定當接收到一些 Signal 時要做出什麼對應的處理。
這邊接受的是常見的中斷訊號,一般就是 Ctrl + C 會送過去的訊號。
另外,這邊透過 tail 這個函式庫將 Nginx 的記錄檔讀取出來,並且格式化後輸出到 stdout 顯示。
使用 Docker 的好習慣就是要將當前運行的程式輸出一律導向 stdout / stderr 讓 Docker 來幫忙做記錄,否則所有的 Log 都會被存在 Container 裡面反而難以除錯。
confd
confd 會監聽 etcd 的 key-value 變動情況,然後動態的執行一些指令。
1[template]
2src = "nginx.conf"
3dest = "/opt/nginx/conf/nginx.conf"
4uid = 0
5gid = 0
6mode = "0644"
7keys = [
8 "/deis/config",
9 "/deis/services",
10 "/deis/router",
11 "/deis/domains",
12 "/deis/controller",
13 "/deis/builder",
14 "/deis/store/gateway",
15 "/deis/certs",
16]
17check_cmd = "check {{ .src }}"
18reload_cmd = "/opt/nginx/sbin/nginx -s reload"
以 Nginx 的重起來說,當上述指定的 Key (如 /deis/domains
)有更動,那麼就會先從樣板檔案產生新的設定檔,並且重新啟動 Nginx。
有興趣的話可以去看看 rootfs/etc/confd
的設定檔是如何撰寫的。
比較有趣的是 /bin/generate-certs
這個指令,他也是透過 confd 去產生的。
1#!/usr/bin/env bash
2
3# create or truncate the file
4> /etc/ssl/deis_certs
5
6{{ range $cert := ls "/deis/certs" }}
7echo {{ $cert }} >> /etc/ssl/deis_certs
8{{ end }}
9
10CERT_PATH=/etc/ssl/deis/certs
11KEY_PATH=/etc/ssl/deis/keys
12
13# clean up all certs
14rm -rf $CERT_PATH
15rm -rf $KEY_PATH
16
17# ...then re-create the paths
18mkdir -p $CERT_PATH
19mkdir -p $KEY_PATH
20
21{{ if gt (len (lsdir "/deis/certs")) 0 }}
22while read etcd_path; do
23 {{ range $cert := ls "/deis/certs" }}
24 if [[ "$etcd_path" == "{{ $cert }}" ]]; then
25 cat << EOF > "$CERT_PATH/$etcd_path.cert"
26{{ getv (printf "/deis/certs/%s/cert" $cert) }}
27EOF
28 cat << EOF > "$KEY_PATH/$etcd_path.key"
29{{ getv (printf "/deis/certs/%s/key" $cert) }}
30EOF
31 fi{{ end }}
32done < /etc/ssl/deis_certs
33{{ else }}
34# there is no certificates to generate
35{{ end }}
這邊很有趣的是,他會從 etcd 裡面讀取每一組 SSL 的 Private Key / Certificates 並且依照 Key 產生一組檔案來寫入檔案。
如此一來每一個不同 Domain 所需的 SSL 設定就可以透過 etcd 來做管理。不過其實另一方面來看,這個檔案其實會不斷地增長⋯⋯ 在
/bin/boot
中,初次執行會設定一個 Cron Job 去執行這個指令,理由還不清楚不過應該是為了修正檔案沒有順利產生之類的問題吧(就註解來看是一個修正)
到此為止,基本上一個簡單的 Router 就算是設定完成了。
如果對防火牆設定有興趣的話,可以參考 rootfs/opt/nginx
裡面的設定是如何撰寫的。
大致上剩下的都是設定檔的部份,稍微詳讀之後就可以瞭解其背後運作的原理。 若要建構自己的簡易 Router 參考這樣的方式設計,其實也沒有想像中的困難。