蒼時弦也
蒼時弦也
資深軟體工程師
發表於

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# traefik.yml
 2version: '3.8'
 3
 4services:
 5  traefik:
 6    image: traefik:v2.6
 7    ports:
 8      - target: 80
 9        published: 80
10        mode: host
11      - target: 443
12        published: 443
13        mode: host
14    deploy:
15      mode: global # 在所有節點部署
16      placement:
17        constraints:
18          - node.role == manager # 限定 Manager 節點
19      labels:
20        - traefik.enable=true
21        - traefik.docker.network=traefik-public
22    volumes:
23      - /var/run/docker.sock:/var/run/docker.sock:ro
24      - traefik-public-certificates:/certificates
25    environment:
26      - CLOUDFLARE_DNS_API_TOKEN=[YOUR_TOKEN]
27    command:
28      - --providers.docker
29      - --providers.docker.exposedbydefault=false
30      - --providers.docker.swarmmode # 啟用 Swarm 模式
31      - --entrypoints.web.address=:80
32      - --entrypoints.websecure.address=:443
33      - --[email protected]
34      - --certificatesresolvers.letsencrypt.acme.storage=/certificates/acme.json
35      - --certificatesresolvers.letsencrypt.acme.dnsChallenge.provider=cloudflare
36      - --accesslog
37      - --log
38    networks:
39      - traefik-public
40
41volumes:
42  traefik-public-certificates:
43
44networks:
45  traefik-public:
46    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$ export DOCKER_HOST=ssh://user@remotehost
2$ docker stack deploy -c traefik.yml ingress

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

部署服務

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

 1# my-app.yml
 2
 3services:
 4  # ...
 5  rails:
 6    image: "registry.example.com/myapp:${VERSION:-latest}"
 7    restart: unless-stopped
 8    environment:
 9      - DATABASE_URL=postgres://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres/$POSTGRES_DB
10      - RAILS_MASTER_KEY
11	deploy:
12      placement:
13        constraints:
14          - node.role != manager
15      update_config:
16        parallelism: 2
17        delay: 10s
18        order: start-first
19    labels:
20    - "traefik.enable=true"
21    # Hosts
22    - "traefik.http.services.rails.loadbalancer.server.port=3000"
23    - "traefik.http.services.rails.loadbalancer.sticky.cookie=true"
24    - "traefik.http.routers.rails-http.rule=Host(`demo.aotoki.dev`)"
25    - "traefik.http.routers.rails-http.entrypoints=web"
26    - "traefik.http.routers.rails-http.middlewares=https-redirect"
27    - "traefik.http.routers.rails-https.rule=Host(`demo.aotoki.dev`)"
28    - "traefik.http.routers.rails-https.entrypoints=websecure"
29    - "traefik.http.routers.rails-https.tls=true"
30    - "traefik.http.routers.rails-https.tls.certresolver=letsencrypt"
31    networks:
32      - traefik-public
33
34networks:
35  traefik-public:
36    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 部署實踐回饋表單告訴我。