弦而時習之

Rails 部署實踐 - 部署到 Docker Swarm

部署的環境已經準備好後,我們就可以來將之前所撰寫的 Docker Compose 設定檔轉換為 Docker Swarm 支援的格式,以及調整我們的架構來讓專案可以被部署到 Docker Swarm 上面。

搭建入口

想要讓使用者可以連上我們部署的網站就需要入口,最簡單的方式就是直接連上我們在 Rails 部署實踐 - Docker Swarm 安裝與設定中所建立好的 Manager Node。

然而,我們在大多數的應用情境中,如果一次需要被一個專案佔用 HTTP(80)和 HTTPS(443)這兩個埠號(Port),就很難部署其他應用,或者將服務拆分出來應用,為了對應這樣的情境,在 Kubernetes 就設計了 Ingress(入口)的機制,我們可以選則直接佔用某個埠號,或者利用雲端服務的 Load Balancer(負載平衡)來以 Host(主幾名稱)的方式區分,將流量引導到對應的服務上。

使用 Docker Swarm 的時候我們可以善用在 Rails 部署實踐 - 使用 HTTPS 協定加密連線我們所採用的 Traefik Proxy 來擔任這個角色,因此我們可以先部署一個 Stack(Docker Swarm 一組服務的單位)來設定 Traefik 負責處理流量的轉發。

首先,我們需要在任意一台 Manager Node 上建立一個「共用」的網路,讓其他服務可以被 Traefik 透過這個網路存取到。

1
$ docker network create --driver=overlay traefik-public

接下來撰寫 Traefik 的 Docker Compose 設定,將每一個 Manager Node 都部署一個 Traefik Proxy 來處理轉發。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# traefik.yml
version: '3.8'

services:
  traefik:
    image: traefik:v2.6
    ports:
      - target: 80
        published: 80
        mode: host
      - target: 443
        published: 443
        mode: host
    deploy:
      mode: global # 在所有節點部署
      placement:
        constraints:
          - node.role == manager # 限定 Manager 節點
      labels:
        - traefik.enable=true
        - traefik.docker.network=traefik-public
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - traefik-public-certificates:/certificates
    environment:
      - CLOUDFLARE_DNS_API_TOKEN=[YOUR_TOKEN]
    command:
      - --providers.docker
      - --providers.docker.exposedbydefault=false
      - --providers.docker.swarmmode # 啟用 Swarm 模式
      - --entrypoints.web.address=:80
      - --entrypoints.websecure.address=:443
      - --[email protected]
      - --certificatesresolvers.letsencrypt.acme.storage=/certificates/acme.json
      - --certificatesresolvers.letsencrypt.acme.dnsChallenge.provider=cloudflare
      - --accesslog
      - --log
    networks:
      - traefik-public

volumes:
  traefik-public-certificates:

networks:
  traefik-public:
    external: true

新的 Traefik Stack 設定跟之前基本上是差不多的,然而有幾點不太一樣的地方需要注意。

  • 增加 deploy 設定,限定在所有 Manager Node 部署,確保所有節點都能連上
  • 改為使用 DNS Challenge 模式來取得 Let’s Encrypt 的 SSL 憑證,這是因為 Traefik Enterprise 版本才有跨節點共用憑證的機制,如果使用 HTTP 驗證模式我們可能會遇到部分節點沒有部署到的狀況
  • 網路設定增加了 external: true 表示這不需要由這個 Stack 建立,是已經存在的網路

完成設定後,我們可以複製檔案到任何一台 Manager Node 部署,或者在自己的電腦上利用設定 DOCKER_HOST 環境變數暫時切換到遠端的 Docker 節點。

1
2
$ export DOCKER_HOST=ssh://[email protected]
$ docker stack deploy -c traefik.yml ingress

成功部署後,我們在每個 Manager Node 直接使用 IP 開啟,就會變成 404 Not Found 的訊息。

部署服務

解決了入口問題後,我們就可以開始著手將服務部署到 Docker Swarm 上。我們可以繼續修改之前用來部署的 Docker Compose 留下服務的部分,來撰寫用於部署的設定。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# my-app.yml

services:
  # ...
  rails:
    image: "registry.example.com/myapp:${VERSION:-latest}"
    restart: unless-stopped
    environment:
      - DATABASE_URL=postgres://$POSTGRES_USER:[email protected]/$POSTGRES_DB
      - RAILS_MASTER_KEY
	deploy:
      placement:
        constraints:
          - node.role != manager
      update_config:
        parallelism: 2
        delay: 10s
        order: start-first
    labels:
    - "traefik.enable=true"
    # Hosts
    - "traefik.http.services.rails.loadbalancer.server.port=3000"
    - "traefik.http.services.rails.loadbalancer.sticky.cookie=true"
    - "traefik.http.routers.rails-http.rule=Host(`demo.aotoki.dev`)"
    - "traefik.http.routers.rails-http.entrypoints=web"
    - "traefik.http.routers.rails-http.middlewares=https-redirect"
    - "traefik.http.routers.rails-https.rule=Host(`demo.aotoki.dev`)"
    - "traefik.http.routers.rails-https.entrypoints=websecure"
    - "traefik.http.routers.rails-https.tls=true"
    - "traefik.http.routers.rails-https.tls.certresolver=letsencrypt"
    networks:
      - traefik-public

networks:
  traefik-public:
    external: true

在這邊我們同樣的加入了 deploy 相關的設定,雖然 Manager Node 也能夠運行服務,然而如果服務的負擔太大的會可能會造成卡住,資源足夠的狀況下會設定為不要部署在 Manager Node 上來確保入口是順暢的。

在 Docker Swarm 也很容易去設定 Rolling Upgrade(滾動更新)也就是將舊版的節點跟新版的節點同時並存,因此我們可以設定同時能存在 2 個服務實體,並且先將新版本啟動等待 10 秒後再關閉舊版,這樣就不會出現更新中服務有一瞬間中斷的狀況。

因為 Rails Assets Precompile 的機制,這個做法會有一個缺點是 CSS 和 JS 會因為使用者有幾秒是能同時連到不同服務實體的,造成無法讀取到正確的 CSS 和 JS 而出現跑版的狀況,因此我們額外加上了 sticky 的設定,讓 Traefik 幫我們保持同一個使用者會連接到同一個實體,來避免這個問題。

不要忘記使用 docker login 讓 Docker Swarm 可以順利下載到容器鏡像

持久化資料難題

當我們從單一節點轉變為叢集(Cluster)的時候,必定會面臨儲存資料的問題。舉例來說,當我們的資料庫從 A 節點轉移到 B 節點的時候,我們該如何在 B 節點恢復資料庫呢?

在 Docker Swarm 最簡單的做法是使用 constriants(限制)的機制,將有需要持續保存資料的服務固定在單一一台伺服器上,這樣就能暫時性的解決這類問題,然而我們還是會遭遇單一伺服器故障造成所有服務停擺的狀況。

這也不難看出為什麼在 12 Factor 的設計中,會將資料庫等等這類服務單獨拆分出來,這樣我們就可以考慮在部署服務時,以獨立的 Stack 方式搭建 PostgreSQL 或 MySQL 的叢集來確保可用性,並利用 Docker Swarm 的 Overlay 網路串聯起來。

或者我們可以利用像是 AWS RDS 這類雲端服務,減少我們自行搭建服務的困難,至於如何取捨就完全基於我們怎樣規劃,大多數時候能使用 RDS 這類服務維護成本會相對的降低更多。


如果想在第一時間收到更新,歡迎訂閱弦而時習之在這系列文章更新時收到通知,如果有希望了解的知識,可以利用Rails 部署實踐回饋表單告訴我。

電子報

留言