【實用】用Google Cloud Run快速構建Nginx反向代理

平常把專案部署在 Vercel 上只要透過 vercel.json 中定義一些重新導向跟重寫即可,比方說若我要將 example.com/api 網址重寫到 api.example.com 上,只需要在 rewrites 內加入指定路徑的設定:

{
  "version": 2,
  "rewrites": [
    {
      "source": "/api/:match*",
      "destination": "https://api.example.com/:match*"
    }
  ]
}

說真的 Vercel 是一個造福軟體生態的救星啊,什麼 CI/CD 跟伺服器設定都免了,只要有一個 Github 倉庫在手,專案馬上跟我走。但唯一缺點就是喚醒時間稍久,而且進入睡眠的時間也很快,再加上 UMEANS 的主要服務跟資料庫都還是建置在Google Cloud 的台灣節點上,而 Vercel 上專案大都部署在 AWS 日本的節點,導致服務對服務一來一往會有些微的延遲感,所以才開始著手將 所以將 UMEANS ECRM 的服務從 Vercel 轉移至 Cloud Run 的台灣節點。

[ez-toc]

策略與工具選擇

但要實現 Cloud Run 上建立反向代理對應到指定容器,在不更動程式碼的情況下,通常會使用 Google Cloud 所提供的負載平衡服務「Cloud Load Balancing」,但除了要增加一些預算外,Cloud Load Balancing 的設定我覺得沒有那麼人性化,而且網路資源匱乏,很少看到有文章真正深入去解析這個服務,我自己當初在摸索都花了一點時間,如果要做更深入的設定我想可能會卡關,所以 Cloud Load Balancing 很快地從我的方案中剔除,目前剩下的解決方案為:

  • 改程式碼,從專案中設定反向代理:下下策,如果要自行實現 Proxy 中介層時間成本及維護要額外的時間,性價比不好,那如果用 Next.js 或 Nuxt2 等框架可以直接使用或搭配套件完成,但目前新專案都是以 Nuxt3 開發,似乎沒有可以在營運環境中可以穩定運作的套件。
  • 不改程式碼,開一台 VM 上設定 Nginx 做反向代理到另外的服務:自行安裝Nginx,需要額外的固定預算以及多管理一台 VM 也要額外的心思。
  • 不改程式碼,使用 Cloudflare 設定路由規則:簡單暴力的方法,不過要在營運環境運作至少要 Business 方案,一樣需要額外的固定預算。
  • 不改程式碼,嘗試用無伺服器服務去解決:這是我目前希望的方向。

後來在官方文件中找到了一篇解釋使用Nginx實踐前端代理的說明指引,大致的方向是使用 Cloud Run 預設的 nginx 的容器映像,在建立服務時需要分別設定 nginx 及 ECRM 兩個容器,並且將 nginx.conf 檔案掛載磁碟區的密鑰。

但依照官方提供的流程會有一些問題:

  • 使用持續部署會覆蓋設定:需要將 nginx 的容器放置於第一個,如果有設定持續部署的話再下一次更新的時候會自動將第一個容器的映像檔網址覆蓋掉。
  • Nginx 偶爾會有 500 Bad Gateway 的錯誤:在 Cloud Run 的紀錄內可以查看 Connection refused) while connecting to upstream,發生問題的點在於 nginx 容器啟動時,ECRM 的容器仍在啟動中使得 nginx 在 upstream 時無法解析到正確的位置,在第二次重新整理頁面時因為 ECRM 容器已經啟動,所以又回歸正常,但如果容器又進入沈睡狀態的話又會再重複發生一次這個問題,這個狀況就算調整「執行個體數量」下限大於0也無法解決。在 Stackoverflow 也有針對此問題做討論,不過沒有解決我的問題就是了……

所以後來依照官方提供的說明文件進行改良,將 nginx 與 ECRM 專案分別拆成兩個Cloud Run 服務,而不是在一個服務中分別建立兩個容器,這個方法的好處在於:

  • 彈性:只需要在 Secret Manager 內調整 nginx 設定,並且使用原映像檔更新 Cloud Run 服務即可。如果將 nginx 與專案一同進入 Dockerfile 的部署流程的話,假如我只是要調整 nginx 的設定就需要更新一整包專案,就會因為需要等待部屬完成而沒辦法即時調整。
  • 延展性:只要用 Cloud Run 架設 nginx proxy 服務,就可以將不同網址的流量導向到其他對應的服務或專案。
  • 實踐快速:設定其實不複雜,只要理解如何設定 nginx 即可,網路上已經有很多介紹文,動動手指問問 ChatGPT 或 Gemini 應該也能獲得解答。

前置作業

需要請先啟用以下的服務,這個方式將使用到 Google Cloud 的服務:

  • Cloud Run
  • Secret Manager (密鑰管理員)

上傳 Nginx 設定到密鑰管理員

進入 Google Cloud 中的控制台,找到選單後選擇「安全性 > Secret Manager」。

點選「建立密鑰」。

進入「密鑰詳細資料」頁面後,只需要填寫名稱密鑰值即可,密鑰值可以透過上傳文字檔或是直接輸入都可以,我自己是會先用 IDE 將 nginx.conf 先寫好後再複製貼上過去。

完整的 nginx.conf 的密鑰值如下:

server {
    listen 8080;
    server_name example.com;
    gzip on;

    location /api {
        proxy_pass   https://api.example.com/;
    }

    location / {
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
        proxy_pass https://other.example.com;
    }
}

有個地方需要特別注意:若 proxy_pass 的位置是 Cloud Run 的動態網址,Host 需要設定成與動態網址相同,不能直接使用 $host 這個變數,因為 Cloud Run 是透過 Host 去辨識對應的服務網址,所以在 proxy_set_header 這邊的 Host 需要設定成「{service_name}-{hash}.a.run.app」,service_name 是您的 Cloud Run 服務名稱,hash 是網址中的亂數,完整網址可以到 Cloud Run 服務中查詢。

server {
    listen 8080;
    server_name example.com;
    gzip on;

    location /api {
        proxy_pass   https://api.example.com/;
    }

    location / {
        proxy_set_header Host {service_name}-{hash}.a.run.app;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
        proxy_pass https://{service_name}-{hash}.a.run.app;
    }
}

設定好後直接按下「創建密鑰」,其他設定可以先不用理會。

建立 Nginx proxy 服務

回到選單後再無伺服器分類這邊選擇「Cloud Run」,進入頁面後按下上面大顆的「建立服務」。

建立服務中的設定做一個詳細解釋:

  • 在建立服務頁面中,將容器映像檔網址輸入「nginx」,服務名稱可以自行發揮。
  • 地區的話選擇離服務區域最近的節點即可,像我習慣都是選擇 asia-east1 台灣。
  • 驗證的部分一般來說建立一個公用的服務,請選擇「允許未經驗證的叫用」。
  • CPU 分配與定價請選擇「只在要求處理期間分配 CPU」,如果選擇隨時分配CPU預算會炸裂,nginx 的反向代理服務基本上不太需要隨時分配CPU。
  • 自動調度資源設定可以設定 0 即可,如果覺得喚醒時間要短一點,可以設定 1 以上減少冷啟動次數,不過我自己目前用起來設定 0 就夠了。
  • 輸入控管請選擇「全部」。

接下來的設定很重要,容器通訊埠請輸入8080,與 nginx.conf 密鑰內設定的Listen Port 要相同,記憶體選擇 256 MiB 即可,CPU請選擇 1。

然後切換到「磁碟區」這個頁籤,按下新增磁碟區後,密鑰選擇剛剛在「Secret Manager」內建立好的密鑰,磁碟區名稱可自由發揮,其他設定請依照下圖所示。

回到「容器」頁籤,在編輯容器內的頁籤中選擇「磁碟區掛接」,並新增掛接磁碟區,選擇好密鑰後,掛接路徑請輸入「etc/nginx/conf.d」,設定方式請依照下圖所示:

要求逾時可依照需求設定即可,建議設定 10~30 秒之間,其他設定可以略過,若擔心預算會莫名爆炸的話,執行個體數量上限可以先寫 10 即可,只要能確保執行個體是夠用的即可。

「啟動時 CPU 效能強化」這個可以依照狀況決定是否要勾選,我自己是有打勾,可以讓冷啟動的時間更少。

完成設定後,按下「建立」後就完成建立好 Nginx proxy!

如果後續要調整 Nginx 的設定,可以回到「Secret Manager」內按下「新增版本」,將 nginx.conf 的設定重新貼進密鑰值中。

然後回到 Cloud Run 中的 Nginx Proxy服務按下「編輯及部署新的修訂版本」,不需要更改任何設定,因為我們一開始建立服務時已經將密鑰版本的指定路徑設定成 latest (代表最新版本),直接大力地按下「部署」即可!

網址 DNS 處理

設定 DNS 轉址只要將解析指向到 Nginx proxy 這個服務,可以透過「管理自訂網域」這個功能設定網域。

網址設定時因為需要建立SSL證書,會需要一些時間,如果需要「www.example.com」重新導向到「example.com」這樣的需求,會建議使用「Firebase 託管」或「Google Cloud Load Balancing」來設定網域。

返回頂端