前面研究過在 CentOS 安裝及設定 ASP.NET Core + Nginx,習得徒手在 CentOS 安裝部署伺服器的技能,依循 Roadmap 來到下一階段 - 學習使用 Docker 簡化部署。

容器化及 Docker 這幾年熱到發燙,有些人甚至認為它已在軟體產業掀起一波革命。(我親身體驗的感想也是:Wow! 難怪會爆紅) 此刻才起步已算遲了,但也不是沒有好處 - 晚起鳥兒有更多蟲可以吃 XD,Docker 相關的文章資源多如牛毛,這裡便不多花篇幅贅述觀念與基本操作,只簡單整理我對 Docker 的理解:

  1. Container (容器)可以想成極度輕量化的虛擬機器(Virtual Machine),用法及優點與 VM 相同,能在一台 Host OS 同時運行多個彼此隔離的應用程式環境,但差別在 Container 會共用底層 Host OS,相較 VM 需各跑一份 Guest OS 能省下可觀的記憶體、磁碟,因此 Container 多了啟動速度快,耗用資源少(與直接跑應用程式相去不遠)的優勢。一台 4GB RAM 的機器頂多跑 2 - 3 台 VM 就緊繃了,但執行數十上百個 Container 不是問題。 2 Container 跟 VM 一樣具有很好的隔離效果,每個 Container 有自己的獨立作業環境(記憶體、磁碟空間、網路),不會彼此干擾,不必擔心 Container A 改系統設定害 Container B 跑不起來,或是兩個 Container 互相搶奪 80 Port,拿到 Image 就一定能在自己的機器跑起來。Container 在這方面的特性與 VM 完全相同,但因為不用包入作業系統,體積縮小許多,耗用記憶體也少,但便利性完全不減,取得 Container Image,靠一行指令幾秒內就能在機器把程式跑起來。
  2. Docker Hub 上有超過 10 萬個 Container Image,從 PHP、Node,js、Apache、MySQL、Mongo DB、Nginx、Redis、ASP.NET Core... 幾乎想得到的都有,下指令自動下載 Image,幾秒鐘就裝好一台 DB、Web 伺服器,再下個指令又裝好第二台,不用擔心跟作業系統不相容、與其他軟體相衝、系統環境有誤導致安裝失敗,這就是 Docker 最迷人的所在。而我們也可將自己的專案網站做成 Image,交給測試人員測試,交付 OP 幾秒部署上線,也能將做好的 Image 上傳到 Docker Hub 與全世界分享。
  3. Docker Container 起初是基於 Linux Container 技術,故在 Container 只能跑 Linux 平台應用程式,雖然在 Windows 也有 Docker for Windows,但背後是用 Hyper-V 跑 Linux 虛擬機執行 Docker Engine 再跑 Docker Container。後來微軟也依循相同概念發展出 Windows Container,並融入 Docker 體系,自此 Docker Container 開始有 Linux Container、Windows Container 之分,Windows Container 裡跑的就是不折不扣的 Windows 程式。參考:安裝 Docker 容器環境 - Windows Server 2016
    從此,在 Container 裡跑 ASP.NET WebForm不再是夢。
  4. Windows Container 問市後,ASP.NET Core 程式容器化有 Linux Container 與 Windows Container 兩種選擇。基於 Linux Container 資源數量上的優勢,加上耗用資源較少,軟硬體成本低,我選擇 Linux Container。
  5. 雖然 Container 間共用底層作業系統,Docker Engine 為容器中的應用程式提供隔離不受干擾的空間(記憶體、檔案系統、網路 Port)。例如:容器 A 寫入 /etc/aaa/default.conf 不影響容器 B /etc/aaa/default.conf 的內容、容器 A 與容器 B 都繫結到 80 Port 也不會衝突。先前文章提過將 Kestrel 轉為 Linux 服務、設定 www-data 執行權限... 等步驟,改用 Docker 後簡單很多,生命週期由 Docker 控制,在容器內部權限一律為 root 不需額外規劃權限,直接跑 dotnet WebApp.dll 聽 5000 Port 就好。

參考資料:

Docker 安裝與基本操作的參考資料很多,這裡不多介紹,直接來幾個練習暖身:在 CentOS 上用 Docker 下載現成 Conatiner Image 執行 Nginx 伺服器,再用預設專案範本建立 ASP.NET Core 網站並包進 Container 執行。最後將二者串接在一起,使用 Nginx 做為 ASP.NET Core 網站的 Reverse Proxy。

  1. 執行 Nginx Container

    sudo docker run --name mynginx -d -p 80:80 --rm nginx
    

    不誇張,真的只要這行 Nginx 就好了。-d 把 Container 丟到背景執行不要佔用命令列視窗,-p 80:80 表示將 Container 的 80 Port 對應到 Host OS 的 80 Port,--rm 表示 Container 停止時自動刪除。開個 Chrome 連上 Host OS 的 80 Port,Nginx 已經準備就緒,skr~
    註:docker 指令需繫結 Unix Socket,必須以 SuperUser 權限執行,將使用者加入 Docker 群組可省去每次加 sudo 的麻煩。參考:Manage Docker as a non-root user

    sudo groupadd docker
    sudo usermod -aG docker $USER
    

    BUT,這招在 CentOS/Fedora/RHEL 不管用!,但有替代方案:在 /etc/sudoers 加入 yourUserAccount ALL=(ALL) NOPASSWD: /usr/bin/docker 開放 sudo docker 時不用敲密碼,再用 alias docker="sudo /usr/bin/docker" 建立同義詞,也可做到不必 sudo 敲密碼跑 docker 指令。

  2. 將 ASP.NET Core 專案包進 Container 使用 .NET Core CLI 建立 MVC 專案,修改 Startup.cs 取消 app.UseHttpsRedirection(),以 Kestrel 執行 ASP.NET Core 網站。

    dotnet new mvc
    sed -i -e 's/app.UseHttps/\/\/app.UseHttps/' Startup.cs
    dotnet publish
    dotnet bin/Debug/netcoreapp2.1/web.dll
    

    由於 5000 Port 預設不對外開放,懶得開防火牆,在本機用 curl httq://localhost:5000 驗證網站運行中。

    驗證程式可執行後,寫個 Dockerfile 腳本將程式封裝成 Docker Image,這部分細節可參考保哥的文章:如何將 ASP.NET Core 2.1 網站部署到 Docker 容器中
    在實務環境可以設計成全自動化測試流程,到版控抓原始碼放進內含 .NET Core SDK 的 Container 編譯,將結果包成只有 .NET Core Runtime 的 Container Image,用它建立 Container 進行 E2E 測試,一切自動化。這裡為求簡便,我選擇用只有 Runtime 的 Container Image 當成基底,將在 Host OS 編譯好的檔案複製到 Container /app 目錄,Dockerfile 內容如下:

    FROM microsoft/dotnet:2.1-aspnetcore-runtime
    WORKDIR /app
    COPY ./bin/Debug/netcoreapp2.1 ./
    ENTRYPOINT ["dotnet", "web.dll"]
    

    做好 Dockerfile 後執行 docker build,Docker 會從 Docker Hub 下載 microsoft/dotnet:2.1-aspnetcore-runtime (microsoft/dotnet 是 Image 名稱,同一 Image 常有多種版本可選擇,:2.1-aspnetcore-runtime 是標籤可用來指定版本),-t 參數指定 Image 名稱為 testapp。Container Image 做好,後接著用 docker run -d --rm --name myapp -p 5000:80 testapp 用剛做好的 Image 建立 Container,ASP.NET Core 專案在 Container 執行時,預設聽 80 Port,故我們用 -p 5000:80 將 Container 的 80 Port 導向 Host OS 的 5000 Port。用 curl 驗證網站運行中。

    使用 docker images 及 docker ps 我們可以看到剛才建立的 Image testapp 及 Container myapp:

  3. 至此,我們做了兩個 Container,myginx 聽 Host OS 80 Port,myapp 聽 Host OS 的 5000 Port,一步要將 Nginx 設成 ASP.NET Core 網站的 Reverse Proxy。
    做法跟先前文章介紹過的概念差不多,為求簡便我們直接修改 conf.d/default.conf 將進入 80 Port 的請求導向 5000 Port。(正規做法建議一個網站開一個 conf 檔) Container 的檔案系統是隔離的,將設定檔保存在 Container 裡不是好主意 - 除非每次修改設定存檔就重新產生 Image 並要求未來一律改用新版 Image 建立 Container,否則一旦 Conatiner 被刪除,設定就會消失。同樣問題也會發生在資料庫檔、Log 檔等執行期間要動態更新的內容,這類檔案保存在 Host OS 檔案系統上比較合理,程式換版換了 Container Image 資料才不受影響。Docker 靠 Volume 解決資料保存及共用需求,docker run 有個 -v host-path:container-path 可將 Host OS 特定目錄或檔案對映到 Container,讓 Container 能讀寫 Host OS 的檔案。
    對 Nginx Container 來說,Reverse Proxy 設定放在 /etc/nginx/conf.d,我選擇在 Host OS 也建立相同路徑並將 Container 的 default.conf 複製出來(指令如下),修改後在 docker run 加上 -v /etc/nginx/conf.d:/etc/nginx/conf.d 對映回去:

    sudo docker cp mynginx:/etc/nginx/conf.d /etc/nginx/conf.d
    


    修改 /etc/nginx/conf.d/default.conf,目前是將進入 Nginx 80 Port 的請求導向 Host OS 5000 Port,但從 Docker Container 存取 Host OS IP 有些眉角,Mac 或 Windows Docker 18.3+ 可用 DNS 名稱 host.docker.internal 指向 Host OS IP,但 Docker for Linux 18.4+ 這招己失效。參考
    省事做法是 docker run 時用 --network host 讓 Container 直接繫結本機 IP 而非 Docker 所屬的隔離網段,如此 default.conf 的 proxy_pass 指向 localhost:5000 即可。

     server {
         listen       80;
         server_name  localhost;
    
         #charset koi8-r;
         #access_log  /var/log/nginx/host.access.log  main;
    
         location / {
             proxy_pass         http://localhost:5000;
             proxy_http_version 1.1;
             proxy_set_header   Upgrade $http_upgrade;
             proxy_set_header   Connection keep-alive;
             proxy_set_header   Host $host;
             proxy_cache_bypass $http_upgrade;
             proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
             proxy_set_header   X-Forwarded-Proto $scheme;
         }
     }
    

    完整啟動指令如下:

    sudo docker run --name mynginx -d -v /etc/nginx/conf.d:/etc/nginx/conf.d --network host nginx
    

    從遠端開啟 Chrome 連上 Host OS 的 80 Port,我們已被順利導向 ASP.NET Core 網站,顯示設定成功。

經過以上練習,我們體驗了從 Docker Hub 下載 Image 建立 Docker Containter 跑 Nginx、用 Dockerfile 將 ASP.NET Core 網站包成 Container、用 Port 映對 Host OS TCP Port 到 Container、使用 -v(--volume) 映對資料夾讓 Container 讀寫 Host OS 檔案。

而在實務應用上,相關的 Conatiner 需要組合在一起執行,例如一個 Container 跑網站,一個 Container 跑資料庫,此時可用 docker-compose 簡化管理;另外 Docker 也提供 Bridge 為相關 Container 建立專屬的隔離網段,防止外界接觸到不想對外公開的網路服務,也避免不相干的 Container 彼此干擾... 這些議題就留待下一篇文章討論。

Explaining the idea of using Docker containers to simplify ASP.NET Core web. In the article, I setup two independent Docker containers to run ASP.NET Core with Nginx reverse proxy.


Comments

# by alexsuper

typo error ~ curl httq://localhost:5000

Post a comment


54 - 15 =