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

Rails 部署實踐 - 使用 HTTPS 協定加密連線

當我們透過 Docker Compose 在伺服器上將 Rails 運行起來後,基本上已經符合了部署的條件,也就是我們已經完成了一個可以提供服務的環境。

然而,在現代的網站部署中,使用 SSL 將使用者的連線加密已經是必備的條件,有 Let’s Encrypt 提供的免費憑證也能讓我們快速的搭建安全的網站服務。

申請憑證

在過去我們要安裝 SSL 憑證到網站上需要向有認證的憑證經銷商購買,透過建立 CSR(Certificate Signing Request)上傳到經銷商,並且產生對應的 SSL 公鑰以及私鑰。

然而隨著資安意識的提高,Let’s Encrypt 提供了 ACME( Automatic Certificate Management Environment,自動憑證管理環境)的機制,讓我們可以透過 API 向 Let’s Encrypt 申請免費的憑證來使用。

一般來說我們會使用 certbot 來協助我們簡化產生 SSL 憑證的流程,然而我們採用容器化的方式,自然會有一些有用的工具可以來讓我們更加簡單的來設置憑證。

以 Kubernetes 經常使用的 Cert Manager 就能夠透過設定檔自動地完成憑證的配置,而不需要自己呼叫 API 或者使用 certbot 這類命令來產生憑證。

Traefik Proxy

這裡要介紹的是 Traefik 這套開放原始碼的 Edge Router(邊緣路由)他能夠協助我們將連線進來的流量引導到正確的服務,在 Kubernetes 中也可以擔任 Ingress(入口)類型的角色,然而在容器化部署中我們可以利用 Traefik 的輔助完成附載平衡、自動化 SSL 憑證等設定。

目前大多數主流的容器化解決方案都是以 Kubernetes 為前提設計,因此在沒有 Kubernetes 的狀況下不容易搭建環境,然而 Traefik 提供了能夠直接以 Docker 運行並且自動偵測 Docker 中運行的容器的功能,因此很適合用來解決部署後的服務沒有 HTTPS 的問題。

了解 Traefik 是怎樣的工具後,我們可以來更新在 Rails 部署實踐 - 撰寫 Docker Compose 中所撰寫只有 HTTP 版本的設定,修改為可以支援 HTTPS 連線的版本。

 1# ...
 2
 3services:
 4  # ...
 5  traefik:
 6    image: "traefik:v2.6"
 7    restart: unless-stopped
 8    command:
 9      - "--api.insecure=true"
10      - "--providers.docker=true"
11      - "--providers.docker.exposedbydefault=false"
12      - "--entrypoints.web.address=:80"
13      - "--entrypoints.websecure.address=:443"
14      - "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https"
15      - "traefik.http.middlewares.https-redirect.redirectscheme.permanent=true"
16      - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
17      - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
18      - "--certificatesresolvers.letsencrypt.acme.email=[YOUR_EMAIL]"
19      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
20    ports:
21      - "80:80"
22      - "443:443"
23      - "8080:8080"
24    volumes:
25      - "./letsencrypt:/letsencrypt"
26      - "/var/run/docker.sock:/var/run/docker.sock:ro"
27    networks:
28      - frontend
29# ...

因為 Traefik 官方提供了 Docker 鏡像給我們使用,因此可以很輕鬆的將 Traefik Proxy 加入到我們的部署專案中,然而我們需要先針對我們的專案做一些設定,因此會利用 command 選項對 Traefik Proxy 啟動時給予額外的命令。

命令基本上分為幾個類型,第一個 api 主是將 Web UI 在 8080 埠上開啟,如果沒有需要的話可以移除。接下來的 providers 是 Traefik Proxy 用於 Service Discovery(服務發現)的來源提供者,因為我們是使用 Docker 因此將這個選項啟用,並且將 exposedbydefault 關閉避免我們預期以外的服務被 Traefik 轉發流量而可以被連線。

第三個 entrypotins 定義了可以用於連線到 Traefik Proxy 的埠號,我們將 80 和 443 這兩個用於 HTTP / HTTPS 埠號打開讓他可以提供連線。最後的 certificateresolvers 就是用於處理 SSL 憑證的申請,我們定義了 letsencrypt 這個設定,並且使用 acme 的 HTTP Challenage(使用 HTTP 的驗證模式)來檢查憑證確實屬於這個網站。

因為 Docker 的網路是跟伺服器分開的,所以用 ports 選項將 80、443、8080(Web UI,請自己用防火牆擋住)跟伺服器的網路綁定。並且將 Docker 內部的網路套用到 frontend 跟我們的 Rails 容器放在同一個區網內來提供連線,最後因為 Docker Provider 的原因要讓 Traefik 可以讀取伺服器上的 Docker Daemon(進程)

服務標籤

雖然我們將 Traefik Proxy 設定好,然而 Traefik Proxy 還是不認得我們的 Rails 容器。這時候就需要利用 Docker 的 Label(標籤)功能,將我們的 Rails 容器加上標籤,這樣 Traefik 就可以透過掃描符合條件的標籤來找到我們的容器。

Service Discovery 的方式有很多中,這裏 Traefik 的做法是定期的呼叫 Docker API 找出符合條件的容器並且產生對應的設定。

繼續更新我們的 docker-compose.yml 調整原本 rails 服務的設定。

 1# ...
 2services:
 3  # ...
 4  rails:
 5    image: "registry.example.com/myapp:${VERSION:-latest}"
 6    restart: unless-stopped
 7    environment:
 8      - DATABASE_URL=postgres://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres/$POSTGRES_DB
 9      - RAILS_MASTER_KEY
10    # ports:
11    #  - "80:3000"
12    labels:
13    - "traefik.enable=true"
14    # Hosts
15    - "traefik.http.services.rails.loadbalancer.server.port=3000"
16    - "traefik.http.routers.rails-http.rule=Host(`demo.aotoki.dev`)"
17    - "traefik.http.routers.rails-http.entrypoints=web"
18    - "traefik.http.routers.rails-http.middlewares=https-redirect"
19    - "traefik.http.routers.rails-https.rule=Host(`demo.aotoki.dev`)"
20    - "traefik.http.routers.rails-https.entrypoints=websecure"
21    - "traefik.http.routers.rails-https.tls=true"
22    - "traefik.http.routers.rails-https.tls.certresolver=letsencrypt"
23    networks:
24      - frontend
25      - backend
26    depends_on: # 在以下服務之後啟動
27      - postgres
28# ...

首先,我們要將原本的 ports 選項取消掉,因為現在要從 Traefik Proxy 所監聽的 80/443 埠連上服務,而不是直接的連接到 Rails 上。

接下來我們先加入 traefik.enable 的標籤表示啟用 Traefik 的功能,也就是會被當作一個服務所偵測。同時我們定義了 traefik.http.services 的設定,設定 rails 這個服務目標是 3000 埠號,這樣 Traefik Proxy 就能正確地找到這個服務。

後續的 traefik.http.routers 則定義了 rails-httprails-https 兩個版本,在 HTTP 的版本中我們先用 rule=Host(demo.aotoki.dev) 來表示當主機符合 demo.aotoki.dev 的時候會套用這條規則,並且使用 Middleware(中介層)https-redirect 來處理,這是我們在 Traefik 啟動命令一起定義的設定,可以自動把 HTTP 連線重新導向到 HTTPS 連線,屬於 Traefik 內建的功能之一。至於 HTTPS 版本,則是設定了 tls 啟用來表示要使用 SSL 加密連線,並且用 certresolver 選擇以我們定義的 letsencrypt 處理 Let’s Encrypt 的 SSL 憑證申請。

如此一來 Traefik Proxy 就會注意到這個 Rails 服務,並且在收到以 demo.aotoki.dev 的網址連線到這個伺服器時,自動的轉到 HTTPS 連線並且提供服務,唯一要注意的是在設定好 demo.aotoki.dev 指向到我們伺服器的 IP 位置之前,先不要套用設定。因為會造成失敗次數過多而被 Let’s Encrypt 暫時性的封鎖申請 SSL 憑證。

Traefik Proxy 的社群版(免費版)是沒辦法跨伺服器共用憑證的,然而在比較小規模的服務或產品基本上不會有這樣的問題,稍微大一點的專案即使使用多個 Traefik Proxy 並且分開申請 SSL 憑證,基本上 IP 位置錯開的狀況下也都非常足夠使用。


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