preloader
軟體工程

設定 Apache reverse proxy 反向代理功能和 RewriteRule 經驗紀錄

緣起

因為要讓單一機器正常運作多個 Rails app 且互不干擾,閱讀這篇文章 https://codepany.com/blog/rails-5-and-docker-puma-nginx/ 的內容後,決定採用第六種作法:Rails app in docker  and web server at host environment (文章內容是 Dockerize Rails app into container and run Nginx on host,我改文字符合自己環境的現況)。本文只提 apache 的設定檔。

 

軟體版本

  • Apache 2.4.7 , 要啟用 module: headers, proxy, proxy_http, rewrite
  • Ubuntu 14.04 LTS
  • Docker 17.09.01 ce
  • Docker-compose 1.22.0

 

 

架構

Host environment 架設 apache service,由 apache 當作 web server 和 reverse proxy ,處理從 Browser 傳給 App farm 的 request、自App farm 回覆給 Browser 的 response。如下圖。

 

 App farm: B Rails app + Puma + Postgres at docker network

     |

     |             http                                                         https

Docker  <———>  apache at host environment <————>  requests from Browsers

 

為了讓讀者容易理解文章內容, 以下文章內容提到 App farm 和 Rails app server 將用 backend server 取代,以求一致於 apache 文件的用詞。

 

apache virtual host 設定檔

xx-csie-io.conf

<VirtualHost *:80> # apache 用 virtualhost 運作多個站就是要用 virtualHost tag. port 80 一般是不加密的 http 協定
  ServerName xx.csie.io # 本站的網址
  Redirect permanent / https://xx.csie.io/ # 如果使用者輸入 http://xx.csie.io ,將被 apache 導向到 https://xx.csie.io
  RewriteEngine on # 開啟自動比對符合條件的網址內容,轉換成想要的結果網址
  RewriteCond %{SERVER_NAME} = xx.csie.io # 當 網址的 server name 是 xx.csie.io 
  RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent] # 那麼就永久自動轉址轉向 https://xx.csie.io 並加上查詢的網址資源. [END, NE, R=permanent] 是 rewriterule flag ,意思是 END=結束此階段的比對 Rewrite rule 且不比對後續其他 RewriteRule 規則,NE=遇到特殊字元不使用跳脫字元輔助解譯, R=向browser 說明是永久轉址(屬於 SEO 方面的意義)

</VirtualHost>


<VirtualHost *:443> # 另一個 virtualhost 開 port 443

  SSLEngine On #當網站有 SSL 證書時,需要開啟這個功能
  Include /etc/letsencrypt/options-ssl-apache.conf # 使用 letsencrypt 的 ssl 選項給 apache 套用

  # The ServerName directive sets the request scheme, hostname and port that
  # the server uses to identify itself. This is used when creating
  # redirection URLs. In the context of virtual hosts, the ServerName
  # specifies what hostname must appear in the request's Host: header to
  # match this virtual host. For the default virtual host (this file) this
  # value is not decisive as it is used as a last resort host regardless.
  # However, you must set it for any further virtual host explicitly.
  #ServerName www.example.com

  ServerAdmin [email protected] # 本 virtualhost 的管理員
  ServerName xx.csie.io  # port 443 下的網址名

  # for reverse proxy
  ProxyRequests Off # 因為開啟 reverse proxy 功能,所以建議關掉 ProxyRequests 功能, off 代表已關閉
  RequestHeader set X-Forwarded-Proto "https" # 因為 apache 兩端有一端不是 https ,所以設定 request header 在 Rails app 和 apache 之間,X-Forwarded-Proto 值都改成 https
  SSLProxyEngine on # proxy 在 Backend server 之間是 SSL
  ProxyPreserveHost On # proxy 在轉傳 request 資料到 Backend server 會保留 browser 送過來的header

  ProxyPass "/" "http://127.0.0.1:3000/" retry=0 # apache 轉傳來自 Browser 的 request 到 http://127.0.0.1:3000 (docker-compose blogapp對 host environment 的port) ,調整 request 網址成 blogapp server網址。 retry=0代表當收到Request後不使用等候秒數的限制,立刻傳給 Rails app at docker
  ProxyPassReverse "/" "http://127.0.0.1:3000/" # apache 轉傳來自 Backend server at docker 的 request 到 Browser 前, 替換 request 中的 server name 成 apache 的server name,以避免 Backend server 重導向避開 apache

  # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
  # error, crit, alert, emerg.
  # It is also possible to configure the loglevel for particular
  # modules, e.g.
  #LogLevel info ssl:warn

  ErrorLog ${APACHE_LOG_DIR}/xxx_error.log  # 如果 apache 運作上遇到錯誤, 像是找不到路徑或啟動 Rails app 失敗,一般能夠從 /var/log/apache2 找到 xxx_error.log
  CustomLog ${APACHE_LOG_DIR}/xxx_access.log combined # 如果 apache 運作上收到正常request,一般能夠從 /var/log/apache2 找到 xxx_access.log

  # For most configuration files from conf-available/, which are
  # enabled or disabled at a global level, it is possible to
  # include a line for only one particular virtual host. For example the
  # following line enables the CGI configuration for this host only
  # after it has been globally disabled with "a2disconf".
  #Include conf-available/serve-cgi-bin.conf

  # Configuration for mod_expires
  ExpiresActive On
  ExpiresDefault "access plus 1 year"

  SSLCertificateFile /etc/letsencrypt/live/xx.csie.io/cert.pem # let’s encrypte ssl file 1
  SSLCertificateKeyFile &nbsp; /etc/letsencrypt/live/xx.csie.io/privkey.pem # let’s encrypte ssl file 2
  SSLCertificateChainFile /etc/letsencrypt/live/xx.csie.io/chain.pem # let’s encrypte ssl file 3

</VirtualHost>

 

示範 ProxyPassMatchRewriteRule 寫法

如果你的 Rails app 的靜態檔案所放置目錄有跟其他不同的地方,你可以寫成以下形式讓 apache 幫你重導 request 向 Backend server 的自訂路徑去拿,如下面的語句:

ProxyPassMatch "^/assets/(.*)" "http://127.0.0.1:3000/assets/$1" # request 網址內容中是 /assets/* 系列檔案,都改從 Backend server 的 assets 目錄下載相同名稱的檔案。(Backend server 一般放置編譯過的靜態檔案是在 public/assets 目錄,而不是 assets 目錄 )

或者是改成下面,並跟樓上一起用:

# serve static files
RewriteEngine On 
RewriteRule "^/assets/(.\*)" "/assets/$1" [P] # 代表用 apache `mod_proxy` 去向 backend server 連線索取資料

但如果要一起用,我覺得直接用 ProxyPassMatch 就好。

 

不使用 ProxyPassMatchRewriteRule 的原因

原本 xx-csie-io.conf 內容中,有本文上述的 ProxyPassMatchRewriteEngineRewriteRule 語句預計讓 apache server 傳送靜態檔案(assets: css, js, img, ……etc)到 Browser,但瀏覽器一直下載不到靜態檔案,導致頁面跑版。尋找問題的原因,推測以下三點:

  1. Apache 2.4.7 預設關閉 X-sendfile
  2. 誤啟用 blogapp rails 使用 X-sendfile for apache 功能(本來在 apache 2.4.7 就預設關閉 X-sendfile,在 Rails app 啟用也不會正常運作)
  3. 沒設定由 Rails app 傳送靜態檔案到 web server

過程一度認為是 apache reverse proxy 的設定問題,所以寫 ProxyPassMatch 和 RewriteRule 語句嘗試解決問題,但沒有解決。後來決定讓 Rails app server 自行處理傳送靜態檔案的事情,雖然速率較 web server 方式慢,但至少能用就好。

 

 

過程中遇到的問題

Q1. xx_error.log 出現訊息:

HTTP Origin header (https://xxxx) didn't match request.base_url (http://xxxxx) 。

A1. 在 xx-csie-io.conf 設定檔,加入以下語句:

RequestHeader set X-Forwarded-Proto “https”

 

參考文件

  1. https://humanwhocodes.com/blog/2012/08/08/setting-up-apache-as-a-ssl-front-end-for-play/ ,這篇文章很值得一讀,建議同時參照 apache官方文件檔查詢更好。
  2. 淺談Apache和 Nginx的差異 https://www.digitalocean.com/community/tutorials/apache-vs-nginx-practical-considerations