---
title: "Rails 部署實踐 - 使用 HTTPS 協定加密連線"
date: 2022-03-25T00:00:00+08:00
publishDate: 2022-03-25T00:00:00+08:00
lastmod: 2023-09-03T17:41:34+08:00
tags: ["Rails","教學","部署","實作","Rails 部署實踐","Traefik","HTTPS"]
series: "rails-deployment-in-practice"
toc: true
permalink: "https://blog.aotoki.me/posts/2022/03/25/rails-deployment-in-practice-enable-https/"
language: "zh-tw"
---


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

然而，在現代的網站部署中，使用 SSL 將使用者的連線加密已經是必備的條件，有 [Let's Encrypt](https://letsencrypt.org/) 提供的免費憑證也能讓我們快速的搭建安全的網站服務。

<!--more-->

## 申請憑證{#apply-certificate}

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

然而隨著資安意識的提高，Let's Encrypt 提供了 ACME（ [Automatic Certificate Management Environment](https://datatracker.ietf.org/doc/html/rfc8555)，自動憑證管理環境）的機制，讓我們可以透過 API 向 Let's Encrypt 申請免費的憑證來使用。

一般來說我們會使用 [certbot](https://certbot.eff.org/) 來協助我們簡化產生 SSL 憑證的流程，然而我們採用容器化的方式，自然會有一些有用的工具可以來讓我們更加簡單的來設置憑證。

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

## [Traefik Proxy](https://traefik.io/traefik/)

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

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

了解 Traefik 是怎樣的工具後，我們可以來更新在 [Rails 部署實踐 - 撰寫 Docker Compose](https://blog.aotoki.me/posts/2022/03/18/rails-deployment-in-practice-write-docker-compose/) 中所撰寫只有 HTTP 版本的設定，修改為可以支援 HTTPS 連線的版本。

```yaml
# ...

services:
  # ...
  traefik:
    image: "traefik:v2.6"
    restart: unless-stopped
    command:
      - "--api.insecure=true"
      - "--providers.docker=true"
      - "--providers.docker.exposedbydefault=false"
      - "--entrypoints.web.address=:80"
      - "--entrypoints.websecure.address=:443"
      - "traefik.http.middlewares.https-redirect.redirectscheme.scheme=https"
      - "traefik.http.middlewares.https-redirect.redirectscheme.permanent=true"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge=true"
      - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web"
      - "--certificatesresolvers.letsencrypt.acme.email=[YOUR_EMAIL]"
      - "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
    ports:
      - "80:80"
      - "443:443"
      - "8080:8080"
    volumes:
      - "./letsencrypt:/letsencrypt"
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
    networks:
      - frontend
# ...
```

因為 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（進程）

### 服務標籤{#service-label}

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

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

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

```yaml
# ...
services:
  # ...
  rails:
    image: "registry.example.com/myapp:${VERSION:-latest}"
    restart: unless-stopped
    environment:
      - DATABASE_URL=postgres://$POSTGRES_USER:$POSTGRES_PASSWORD@postgres/$POSTGRES_DB
      - RAILS_MASTER_KEY
    # ports:
    #  - "80:3000"
    labels:
    - "traefik.enable=true"
    # Hosts
    - "traefik.http.services.rails.loadbalancer.server.port=3000"
    - "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:
      - frontend
      - backend
    depends_on: # 在以下服務之後啟動
      - postgres
# ...
```

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

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

後續的 `traefik.http.routers` 則定義了 `rails-http` 和 `rails-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 位置錯開的狀況下也都非常足夠使用。

---

如果想在第一時間收到更新，歡迎[訂閱弦而時習之](https://mailchi.mp/aotoki/rails-deployment-in-practice)在這系列文章更新時收到通知，如果有希望了解的知識，可以利用[Rails 部署實踐回饋表單](https://us4.list-manage.com/survey?u=dd3d68032c0510041f1302539&id=f25e0dc43e&attribution=false)告訴我。

