數百萬用戶上網獲取各種目的的資訊,包括學習、娛樂、新聞以及與朋友分享他們生活’的進度。因此,在部署應用程式時,實作一個高度安全且具擴充性的基礎架構最符合您的利益。雲端提供了多種方法來保護和擴充 Django 應用程式。水平擴充是一種允許您執行多個應用程式複本的方法。這能確保其具有更高的容錯能力和高可用性。它還能提高效能,以同時處理多個請求。
水平擴充 Django 應用程式
您可以透過佈署多個執行 Django 應用程式及其 WSGI HTTP 伺服器(例如 Gunicorn 或 uWSGI)的應用程式伺服器,來水平擴充 Django 應用程式。接著,您需要建立一個基礎架構,將傳入的請求分發到這些應用程式伺服器。負載平衡器和 反向代理(如 Nginx) 可以協助您的基礎架構進行流量分發。Nginx 可以部署 SSL 憑證,以確保透過 HTTPS 安全連接到您的應用程式。最後,Nginx 還可以提供靜態內容的快取,以最大程度地減少伺服器的負載。
單獨設定這些不同的元件並確保它們相互通訊可能是一項艱鉅的任務。幸運的是,使用 Docker 可以簡化設定程序,並確保無論在何處部署,各種元件的行為都相同。
您將在本指南中做些什麼
在本指南中,您將學習如何水平擴充一個容器化的 Django 應用程式,該應用程式由 Gunicorn WSGI HTTP 伺服器提供服務。您將佈署兩台安裝了 Docker 的應用程式伺服器,它們執行同一個 Django 和 Gunicorn 應用程式容器的複本。
您還將使用 Let’s Encrypt SSL 憑證來保護您的應用程式,方法是佈署並設定一個 第三台代理伺服器,該伺服器將執行一個 Nginx 反向代理 容器和一個 Certbot 用戶端容器。Certbot 是一個有助於管理來自 Let’s Encrypt 憑證授權單位之 SSL 憑證的套件。它會獲取憑證、使用憑證的位置設定 Nginx 伺服器區塊(server blocks),並管理自動更新。它透過設定一個 cron 工作來定期檢查憑證是否即將過期並需要更新。藉由保持您的 SSL 憑證處於最新狀態,您的網站在 SSL Labs.
上將始終擁有極高的安全評級。這台 第三台代理伺服器 位於您分散式架構的前端,接收所有傳入的外部流量。然後,它將流量分發到您的應用程式伺服器。應用程式伺服器位於防火牆後方,僅允許代理伺服器存取它們。
本教學是 使用 Django、Docker 和 Kubernetes 系列三篇教學中的第二篇。您應該首先按照以下教學中說明的步驟進行:在 Ubuntu 上使用 Docker 建立 Django 和 Gunicorn 應用程式。在該教學中,我們設定了基礎專案程式碼、Dockerfile,並將應用程式連接到 MinIo 簡單儲存服務 (S3) 以提供我們的靜態檔案。
先決條件
要進行本教學,您需要準備以下內容:
- 四台 Ubuntu 20.04 伺服器:
如果您已按照先決條件教學中的步驟進行,在 Ubuntu 上使用 Docker 建立 Django 和 Gunicorn 應用程式,您就已經擁有四台伺服器中的兩台:
-
第一台伺服器將執行 PostgreSQL 資料庫執行個體。按照教學的步驟 1 和 2:在 Ubuntu 上使用 Docker 建立 Django 和 Gunicorn 應用程式 來設定資料庫。應修改 Postgres 設定,以僅允許來自您的應用程式伺服器 IP 的外部連線。
-
這台 第二台 和 第三台 伺服器將託管您應用程式程式碼的容器。您應該已經有從 先決條件教學 中執行的第二台伺服器。我們將修改其防火牆,以僅允許來自代理伺服器 IP 的外部連線。您可以按照此 逐步教學來協助您設定 Ubuntu 伺服器 在 CloudSigma 上。
-
這台 第四台 伺服器將會是 代理伺服器,負責處理負載平衡以及將流量分發到這兩個應用程式伺服器容器。
-
這兩台應用程式伺服器和代理伺服器上都應該安裝 Docker。
在按照前置準備教學中的步驟操作後,您應該已經在其中一台伺服器上安裝了 Docker。您可以按照我們安裝與操作 Docker 的教學中的步驟 1、2 和 3 進行操作。請記得將上面建立的 sudo 使用者新增到 Docker 群組中。
- 取得已註冊的網域名稱,並設定其 DNS 記錄以指向 代理 伺服器的公用 IP 位址。為了示範起見,我們將使用 example_domain.com.
步驟 1:驗證第一台 Django 應用程式伺服器是否正常運作
正如在前置準備中所說明的,本指南是在在 Ubuntu 上使用 Docker 建置 Django 和 Gunicorn 應用程式教學之後。如果您是從該教學過來的,並且已經執行了這些步驟,那麼您應該已經讓第一台伺服器運行了。該應用程式的程式碼是基於 Django 文件中的投票應用程式教學。請務必仔細閱讀這些步驟,以了解初始設定。如果您已經執行了教學中的步驟,可以跳過這第一個步驟。
否則,您只需將 Docker 化的分支複製到您的伺服器中。首先登入您的第一台應用程式伺服器,然後執行以下 git 指令來複製 django-polls-docker 分支,該分支來自 django-polls 存放庫:
|
1 |
git clone --single-branch --branch django-polls-docker --depth 1 https://github.com/jaymoh/django-polls.git |
接下來,進入 django-polls 目錄:
cd django-polls
在此目錄中,您會找到一個 Dockerfile,Docker 會使用它來建置應用程式映像檔,以及 django-polls 目錄(其中包含 Python 應用程式程式碼),和一個 env 檔案(其中包含將在啟動時傳遞到容器中以修改其行為的環境變數清單)。在 Dockerfile 中,我們透過 requirements.txt 檔案來定義 Django 套件相依性。此外,我們需要宣告一個連接埠 8000 用於接收傳入流量,並將其設定為執行 gunicorn 伺服器(具有 3 個 worker)。要深入了解 Dockerfile 指令,請參閱 步驟 7 的教學 在 Ubuntu 上使用 Docker 建置 Django 和 Gunicorn 應用程式.
您可以使用以下指令建置 Docker 映像檔:
docker build -t django-polls:v1 .
Docker 建置映像檔後,您可以使用以下指令列出伺服器上可用的映像檔:
docker images
以下是我們執行該指令時的輸出:
接下來,我們需要修改用於設定執行階段環境的 env 檔案。此檔案會在啟動容器時傳遞到 Docker 運行的容器中。使用 nano 編輯器開啟 env 檔案:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
DJANGO_SECRET_KEY=your_secret_key DEBUG= DJANGO_LOGLEVEL=info DJANGO_ALLOWED_HOSTS=your_server_IP_address DB_ENGINE=postgresql_psycopg2 DB_DATABASE=polls_db DB_USERNAME=hackins DB_PASSWORD=your_database_password DB_HOST=your_database_host DB_PORT=your_database_port STATIC_DEFAULT_FILE_STORAGE=storages.backends.s3boto3.S3Boto3Storage STATIC_MINIO_BUCKET_NAME=test-bucket MINIO_ACCESS_KEY=your_minio_access_key MINIO_SECRET_KEY=your_minio_secret_key MINIO_URL=your_minio_url:your_minio_port |
該 env 檔案包含一些佔位符文字,您需要修改並填入正確的值:
-
DJANGO_SECRET_KEY:產生一個獨特且不可預測的值,如 Django 文件 中所述。您可以使用此命令產生隨機字串並將其設定給該變數: python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'.
-
DJANGO_ALLOWED_HOSTS:此值用於保護您的應用程式免受 HTTP 主機標頭(Host Header)攻擊。您可以將其設定為 *,如果在開發模式下,萬用字元會比對所有主機。當您將應用程式部署到生產環境時,請將此設定為您註冊的網域名稱。在我們的示範中,它是 example_domain.com.
-
DB_DATABASE:將此設定為您在 先決條件 章節中建立的 PostgreSQL 資料庫名稱,在我們的案例中為 polls_db.
-
DB_USERNAME:將此設定為您為資料庫選擇的使用者名稱。
-
DB_PASSWORD:將此設定為您為資料庫選擇的密碼。
-
DB_HOST:將此設定為運行您資料庫執行個體的主機,如您在 先決條件 章節中所設定。這在教學課程的步驟 1 和 2 中有說明:在 Ubuntu 上使用 Docker 建立 Django 和 Gunicorn 應用程式 以設定資料庫.
-
DB_PORT:將此設定為您資料庫的連接埠。
編輯完成後儲存並關閉檔案。設定好資料庫憑證後,我們可以透過執行容器並覆寫 Dockerfile 中設定的 CMD 命令來建立資料庫綱要。您可以在官方文件的 Dockerfile 進入點 中找到更多資訊。接下來,執行以下命令:
|
1 |
docker run --env-file env django-polls:v1 sh -c "python manage.py makemigrations && python manage.py migrate" |
在此命令中,我們正在執行 django-polls:v1 映像檔,並傳入先前修改的 env 檔案。此部分: sh -c "python manage.py makemigrations && python manage.py migrate” 會建立由應用程式程式碼定義的資料庫綱要。如果您是第一次執行該命令,您應該會看到類似的輸出,表示已建立資料庫綱要:
建立綱要後,我們可以建立 Django 超級使用者。執行以下命令以使用互動式 shell 啟動容器:
|
1 |
docker run -i -t --env-file env django-polls:v1 sh |
該命令會啟動容器並顯示 shell 提示字元,您可以使用它與 Python shell 進行互動。讓我們使用以下命令建立使用者:
|
1 |
python manage.py createsuperuser |
按照提示提供使用者名稱、電子郵件地址和密碼。重新輸入密碼,然後按 Enter 鍵建立使用者。退出 shell 並透過按下 CTRL+D.
接下來,我們需要再次執行容器,使用 collectstatic Django 命令覆寫預設命令。該命令將為應用程式產生靜態檔案並將其上傳到 MinIO 雲端儲存空間:
|
1 |
docker run --env-file env django-polls:v1 sh -c "python manage.py collectstatic --noinput" |
該命令會產生檔案並將其上傳到您設定的物件儲存服務。以下是輸出:
您現在可以執行應用程式,而無需指定任何額外的命令來覆寫 Dockerfile 中定義的預設 CMD 命令:
|
1 |
docker run --env-file env -p 80:8000 django-polls:v1 |
Docker 會執行 Dockerfile 中定義的預設命令,使用 gunicorn 伺服器啟動容器,公開容器連接埠 8000,並將其對應到 Ubuntu 的連接埠 80。您現在可以在瀏覽器的網址列中輸入第一台伺服器的 IP 地址來檢視應用程式的介面: http://FIRST_SERVER_IP.
您將會收到 404 找不到網頁,因為我們尚未為 定義任何內容/ 路徑。導航至 http://FIRST_SERVER_IP/polls 以查看投票介面:
訪問管理員介面以建立一些投票: http://FIRST_SERVER_IP/admin:
提供您使用 createsuperuser 命令設定的憑證以存取管理介面:
如果您檢視頁面原始碼,您會注意到靜態檔案正如同定義般從儲存貯體中獲取。在確認容器如預期般提供應用程式服務後,您可以透過按下 CTRL+C 來終止容器。
接下來,我們需要讓容器在 detached 模式下保持運行,以便我們可以退出第一台伺服器的 SSH 工作階段。這將使容器在背景運行。執行以下命令:
|
1 |
docker run -d --rm --name polls --env-file env -p 80:8000 django-polls:v1 |
該 -d 旗標會在 detached 模式下啟動容器,使其可以在背景保持運行。該 --rm 旗標會在容器退出後清理容器的檔案系統。我們為容器命名為 polls,這樣我們在列出容器時就能看到它。
退出第一台伺服器的 SSH 工作階段,並在瀏覽器中導航至 http://FIRST_SERVER_IP/polls 以確認其如預期運行。如果您能看到投票介面,那麼您的第一台應用程式伺服器已成功設定。讓我們在下一步中設定第二台應用程式伺服器。
步驟 2:設定第二台應用程式伺服器
我們將複製我們在 在 Ubuntu 上使用 Docker 建立 Django 和 Gunicorn 應用程式 教程中建立的 Docker 化分支。您可以從該教程中找到我們在此處使用的命令的更多詳細資訊,或在 步驟 1.
中找到簡化版本。您應該已經讓第二台伺服器運行、新增了非 root sudo 使用者,並安裝了 Docker,如 先決條件 章節中所述。
下一步是設定此伺服器以連接到 PostgreSQL 伺服器執行個體。正如教程 在 Ubuntu 上使用 Docker 建立 Django 和 Gunicorn 應用程式 的步驟 1 中所述,您需要允許第二台伺服器的 IP 位址通過 ufw 和 PostgreSQL 設定。
首先,使用您的非 root sudo 使用者登入 PostgreSQL 資料庫伺服器執行個體。要新增 ufw 規則,請執行以下命令:
|
1 |
sudo ufw allow from SECOND_SERVER_IP to any port 5432 |
接下來,執行此命令並將第二台伺服器的 IP 位址新增到 PostgreSQL 用戶端驗證檔案中:
|
1 |
sudo nano /etc/postgresql/12/main/pg_hba.conf |
閱讀註解以深入了解設定。接下來,在 hosts 區段下新增此行,並指定您的 IP 位址:
|
1 |
host all all SECOND_SERVER_IP/24 md5 |
編輯完成後,儲存並關閉檔案。
然後,重新啟動 PostgreSQL 服務以使變更生效:
|
1 |
sudo service postgresql restart |
登出 PostgreSQL 資料庫伺服器執行個體,並繼續設定第二台應用程式伺服器執行個體。
登入 第二台應用程式伺服器,使用 ssh。然後使用以下命令複製 django-polls-分支,該分支屬於 django-polls 存放庫:
|
1 2 |
git clone --single-branch --branch django-polls-docker --depth 1 https://github.com/jaymoh/django-polls.git |
進入 django-polls 目錄:
|
1 |
cd django-polls |
之後,使用以下命令建置映像檔:
|
1 |
docker build -t django-polls:v1 . |
映像檔建置程序完成後,修改 env 檔案中的設定值,如 步驟 1 中所述。使用 nano 開啟檔案:
|
1 |
nano env |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
DJANGO_SECRET_KEY=your_secret_key DEBUG= DJANGO_LOGLEVEL=info DJANGO_ALLOWED_HOSTS=your_server_IP_address DB_ENGINE=postgresql_psycopg2 DB_DATABASE=polls_db DB_USERNAME=hackins DB_PASSWORD=your_database_password DB_HOST=your_database_host DB_PORT=your_database_port STATIC_DEFAULT_FILE_STORAGE=storages.backends.s3boto3.S3Boto3Storage STATIC_MINIO_BUCKET_NAME=test-bucket MINIO_ACCESS_KEY=your_minio_access_key MINIO_SECRET_KEY=your_minio_secret_key MINIO_URL=your_minio_url:your_minio_port |
將預留位置文字替換為您在 步驟 1 中新增的實際值。請記住適當修改 DJANGO_ALLOWED_HOSTS 變數。完成後儲存並關閉檔案。在 env 檔案中更新您的 MinIO 憑證,就像您在上一步中所做的那樣。
現在您可以使用以下命令在背景模式下執行應用程式容器:
|
1 |
docker run -d --rm --name polls --env-file env -p 80:8000 django-polls:v1 |
此命令會啟動容器並使其在背景中執行。結束第二個應用程式伺服器的 ssh 工作階段,並在瀏覽器中導覽至 http://SECOND_SERVER_IP/polls 以確認其運作正常。如果一切順利,您應該能夠看到投票介面。
您現在有兩個執行相同應用程式複本的應用程式伺服器。在下一步中,您將設定 Nginx 容器以用作反向代理。
步驟 3:設定 Nginx Docker 容器
Nginx 是世界上最受歡迎的開源網頁伺服器軟體之一。它負責確保網際網路上流量最高的網站的可用性和擴充性。它保證了 安全性 且功能非常多樣。您可以用它來進行 反向代理、快取和負載平衡。我們已將應用程式設定為使用獨立的物件儲存服務來處理其靜態和媒體檔案。因此,我們不會使用 Nginx 的快取功能。相反地,我們將使用 Nginx 的反向代理和負載平衡功能。面向前端的 Nginx 伺服器將接收傳入的流量並將其分發到後端應用程式伺服器。然後,它將使用從 Let’s Encrypt.
實作 Nginx 反向代理和負載平衡有幾種方法。其中一種方法是將 Nginx 反向代理與後端應用程式伺服器分開設定,就像我們在本教學中所做的那樣。這種設定非常靈活,允許您擴充 Nginx 代理層以及應用程式層。您可以新增多個 Nginx 代理,或實作雲端負載平衡器。另一種實作反向代理的方法是將其中一個後端應用程式伺服器用作 Nginx 代理。然後,您可以在本機以及向其他應用程式伺服器代理傳入的請求。或者,您可以在所有後端應用程式伺服器上設定 Nginx 容器,並設定一個前端雲端負載平衡器來接收傳入流量並將其分發到後端應用程式伺服器。
讓我們開始設定代理伺服器。登入您設定用作 Nginx 代理的第四台 Ubuntu 伺服器,並建立一個設定目錄:
|
1 |
mkdir conf |
在目錄中使用 nano 開啟設定檔案:
|
1 |
nano conf/nginx.conf |
接下來,將以下設定新增到檔案中:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
upstream django { server FIRST_SERVER_IP; server SECOND_SERVER_IP; } server { listen 80 default_server; return 444; } server { listen 80; listen [::]:80; server_name example_domain.com; return 301 https://$server_name$request_uri; } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name example_domain.com; # SSL ssl_certificate /etc/letsencrypt/live/example_domain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example_domain.com/privkey.pem; ssl_session_cache shared:le_nginx_SSL:10m; ssl_session_timeout 1440m; ssl_session_tickets off; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; client_max_body_size 4G; keepalive_timeout 5; location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://django; } location ^~ /.well-known/acme-challenge/ { root /var/www/html; } } |
在此設定檔中,我們指定了 server, upstream、以及 location 區塊,以指示 Nginx 將 HTTP 請求重新導向至 HTTPS,並在我們於 步驟 1 和 步驟 2 中設定的兩台應用程式伺服器之間分發請求。您可以在官方文件中找到關於 Nginx 設定檔結構的一般資訊.
我們研究了 Docker Hub 提供的設定檔,包括 Nginx 映像檔文件, Certbot、以及 Gunicorn 來想出這個極簡的 Nginx 設定檔。雖然這僅用於示範目的並讓我們的設定運行起來,但您可以自由地探索和嘗試 遵循 Nginx 指南的其他設定.
upstream 區塊用於定義將處理傳入請求的伺服器群組。一個 名稱 會給予該群組,並由 proxy_pass 指令呼叫。我們已將該區塊命名為 django 並指定了兩個後端應用程式伺服器的 IP 位址:
|
1 2 3 4 |
upstream django { server FIRST_SERVER_IP; server SECOND_SERVER_IP; } |
我們還定義了 3 個 server 區塊。其中 第一個 server 區塊會擷取所有與您的網域不相符的請求,並傳回一個 444 狀態碼(關閉連線而不向用戶端傳送回應,從而拒絕惡意或格式錯誤的請求)。由於此區塊被定義為 default_server:
|
1 2 3 4 |
server { listen 80 default_server; return 444; } |
第 二個 server 區塊處理傳入的 HTTP(連接埠 80)請求,並將其重新導向至 HTTPS(連接埠 443)使用 HTTP 301 重新導向:
|
1 2 3 4 5 6 |
server { listen 80; listen [::]:80; server_name example_domain.com; return 301 https://$server_name$request_uri; } |
第三個 server 區塊現在負責處理請求。它有幾個指令,我們將在下面定義它們的重要性。
我們有兩個指令定義了由 Certbot 提供的 TLS 憑證和金鑰的路徑。當我們啟動 Nginx 容器時,這些憑證會被掛載到容器中:
|
1 2 3 |
# SSL ssl_certificate /etc/letsencrypt/live/example_domain.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example_domain.com/privkey.pem; |
接下來,我們有 Certbot 推薦的 SSL 安全預設值。您可以從 關於 ngx_http_ssl_module 的 Nginx 官方文件 了解更多資訊。Mozilla 也提供了更多關於 伺服器端安全性 的資訊。 ssl_ciphers 在 conf 檔案中的值是取自 Mozilla 的 頁面:
|
1 2 3 4 5 6 |
ssl_session_cache shared:le_nginx_SSL:10m; ssl_session_timeout 1440m; ssl_session_tickets off; ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers off; ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384"; |
在接下來的兩個指令中,我們將定義用戶端請求主體的最大允許大小,並設定與用戶端 keep-alive 連線的逾時時間。Nginx 將在您於 keepalive_timeout 指令中設定的秒數後關閉與用戶端的連線。您可以從官方文件找到更多關於 部署 Gunicorn 的 Nginx 設定 的資訊:
|
1 2 |
client_max_body_size 4G; keepalive_timeout 5; |
在設定檔中,我們還定義了兩個 location 區塊。第一個區塊處理 proxy 指令所定義的請求代理。傳入的請求會被代理到先前定義的 upstream django 伺服器:
|
1 2 3 4 5 6 7 |
location / { proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://django; } |
您可以從以下位置找到更多關於代理指令的資訊:Nginx Module ngx_http_proxy_module 以及關於 部署 Gunicorn 伺服器的文件.
在第二個 location 區塊中,我們定義了一個路徑: /well-known/acme-challenge/。它通常由 Certbot 用於在佈署或更新 SSL 憑證之前,向 Let’s Encrypt 驗證您的網域名稱:
|
1 2 3 |
location ^~ /.well-known/acme-challenge/ { root /var/www/html; } |
Nginx 設定檔的內容就是這些。編輯完成後,您現在可以儲存並關閉該檔案。
您剛剛定義的設定檔可用於執行 Nginx 容器。然而,由於我們尚未從 Let’s Encrypt 佈署 SSL 憑證,它將會失敗。在本教學中,我們將使用 nginx:1.20.2 Docker 映像檔版本 1.20.2,來自官方的 Docker Hub 上的 Nginx 映像檔存放庫.
您可以執行以下命令來下載映像檔並驗證一切是否正常運作:
|
1 2 3 4 |
docker run --rm --name nginx -p 80:80 -p 443:443 \ -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \ -v /var/www/html:/var/www/html \ nginx:1.20.2 |
此命令會建立一個名為 nginx 的容器,並在主機系統與容器之間對應連接埠 80 與 443。 --rm 旗標會在建置成功後移除任何中間容器。我們使用 -v 旗標將設定檔掛載到容器中的 /etc/nginx/conf.d/nginx.conf,這是預設的 Nginx 設定目錄。它是使用 ro 旗標以唯讀模式掛載,以防止 Nginx 容器對其進行修改。我們設定預設的 webroot 目錄並將其掛載為 /var/www/html。最後,我們指示 Docker 使用 nginx:1.20.2 映像檔進行此建置。接下來,讓我們在下一步中從 Let’s Encrypt 取得 TLS/SSL 憑證和金鑰。
步驟 4:從 Let’s Encrypt 佈署 SSL/TLS 憑證並設定 Certbot 自動更新
Certbot 有助於從 Let’s Encrypt 佈署免費的 TLS 憑證,並在憑證過期前管理其自動更新。這可以提高您網站的安全性,並確保它們透過 HTTPS 提供服務。為了保持我們的架構容器化,我們將使用 Certbot Docker 映像檔來拉取 SSL/TLS 憑證並設定自動更新。請確保您已按照 在您的代理伺服器上安裝 Docker 的說明,如 先決條件 所述。
您還應該擁有指向您代理伺服器 IP 位址的已註冊網域名稱 DNS A 記錄。您可以透過執行 certbot Docker 映像檔並傳入 --staging 旗標來進行驗證:
|
1 2 3 4 |
docker run -it --rm -p 80:80 --name certbot \ -v "/etc/letsencrypt:/etc/letsencrypt" \ -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ certbot/certbot certonly --standalone --staging -d example_domain.com |
此命令將下載 Certbot 映像檔並在互動模式下執行。這意味著它將帶有一個 shell,允許您輸入一些詳細資訊。它將主機的連接埠 80 對應到容器內部的連接埠 80。我們使用 -v 旗標將兩個主機目錄掛載到容器中: /etc/letsencrypt/ 與 /var/lib/letsencrypt/。 --standalone 旗標指定我們希望 Certbot 映像檔在不使用 Nginx 的情況下執行。最後,我們有 --staging 旗標,這會讓 Certbot 在 staging 伺服器上執行並驗證您的網域名稱。
輸入您的電子郵件地址並接受 服務條款 (當系統提示時)。以下是成功驗證的輸出:
|
1 2 3 4 5 6 7 |
帳戶 已註冊. 正在申請 一張 憑證 給 example_domain.com 成功地 收到 憑證. 憑證 已 儲存 於: /etc/letsencrypt/live/example_domain.com/fullchain.pem 金鑰 已 儲存 於: /etc/letsencrypt/live/example_domain.com/privkey.pem 此 憑證 到期 於 2022-04-29. 這些 檔案 將 會 更新 當 該 憑證 更新. |
後續步驟:
憑證需要在過期前進行更新。Certbot 可以在背景自動更新憑證,但您可能需要採取一些步驟來啟用該功能。請檢查此 連結 以取得說明。
您可以使用以下命令檢視憑證 cat 命令:
|
1 |
sudo cat /etc/letsencrypt/live/example_domain.com/fullchain.pem |
上述命令應該會在終端機中顯示您的憑證。確認 Certbot 已簽發您的憑證後,您現在可以測試您在 步驟 3 中建立的 Nginx 設定。執行下方的 Docker 命令以啟動 Nginx 容器:
|
1 2 3 4 5 6 |
docker run --rm --name nginx -p 80:80 -p 443:443 \ -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \ -v /etc/letsencrypt:/etc/letsencrypt \ -v /var/lib/letsencrypt:/var/lib/letsencrypt \ -v /var/www/html:/var/www/html \ nginx:1.20.2 |
在此命令中,我們使用了 -v 旗標來掛載 Let’s Encrypt SSL/TLS 憑證目錄的位置。
當容器啟動並執行時,在瀏覽器中開啟網頁: http://example_domain.com。您可能會看到關於網站不安全的警告:
這是原因我們之前只簽發了 staging/測試憑證,而不是來自 Let’s Encrypt 的 正式 憑證。讓我們透過執行以下不帶 --staging 旗標的 Certbot 命令來取得正式憑證:
|
1 2 3 4 |
docker run -it --rm -p 80:80 --name certbot \ -v "/etc/letsencrypt:/etc/letsencrypt" \ -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ certbot/certbot certonly --standalone -d example_domain.com |
在提示中,確認您要 更新並取代 現有的憑證,方法是輸入 2 並按下 ENTER。這應該會簽發一個可用於正式環境的憑證。您現在可以執行 Nginx 容器,一切都應該能正常運作:
|
1 2 3 4 5 6 |
docker run --rm --name nginx -p 80:80 -p 443:443 \ -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \ -v /etc/letsencrypt:/etc/letsencrypt \ -v /var/lib/letsencrypt:/var/lib/letsencrypt \ -v /var/www/html:/var/www/html \ nginx:1.20.2 |
一旦容器啟動並執行,再次在瀏覽器中開啟網頁: http://example_domain.com。請注意,即使您輸入 HTTP,您的瀏覽器也會被重導向至 HTTPS。這意味著我們在 Nginx 設定中的伺服器以及簽發的 SSL/TLS 憑證都運作正常。導覽至 polls 路由 http://example_domain.com/polls,因為我們沒有為首頁路徑定義路由 /。您應該會看到投票介面:
到目前為止,您已成功配置了生產就緒的架構。您已部署了兩個後端伺服器,它們將處理從代理伺服器代理的傳入請求。代理伺服器將使用配置的 TLS 憑證來處理負載平衡並確保流量安全。
然而,您應該記住 Let’s Encrypt 憑證會在 90 天後過期。因此,您應該在 90 天期限之前更新它們。由於 Nginx 容器將會運行,您應該使用 webroot 模式,而不是 standalone 模式,當您執行 certbot 指令來更新憑證時。請記住,您已在 /var/www/html/.well-known/acme-challenge/ 目錄,位於 Nginx 設定檔中,在 步驟 3。Certbot 將使用此路徑來儲存驗證檔案。此外,當您嘗試更新憑證時,Let’s Encrypt 用戶端將使用驗證請求呼叫此路徑。更新指令執行完畢後,您可以重新載入 Nginx 以使變更生效。
在終端機中按下 CTRL+C 來終止容器,讓我們使用 -d 旗標在背景模式下再次啟動它:
|
1 2 3 4 5 6 |
docker run --rm --name nginx -d -p 80:80 -p 443:443 \ -v ~/conf/nginx.conf:/etc/nginx/conf.d/nginx.conf:ro \ -v /etc/letsencrypt:/etc/letsencrypt \ -v /var/lib/letsencrypt:/var/lib/letsencrypt \ -v /var/www/html:/var/www/html \ nginx:1.20.2 |
這將使 Nginx 容器在背景運行。讓我們使用 --dry-run 旗標來測試憑證更新程序,執行以下指令:
|
1 2 3 4 5 |
docker run -it --rm --name certbot \ -v "/etc/letsencrypt:/etc/letsencrypt" \ -v "/var/lib/letsencrypt:/var/lib/letsencrypt" \ -v "/var/www/html:/var/www/html" \ certbot/certbot renew --webroot -w /var/www/html --dry-run |
在此指令中,我們指定了 --webroot 外掛程式,以及用於驗證請求的路徑,使用 -w 旗標。我們還指定了 --dry-run 旗標,以在不實際配置憑證的情況下驗證自動更新程序。
模擬成功後,您應該會看到類似的輸出:
|
1 2 3 4 5 6 7 8 |
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Processing /etc/letsencrypt/renewal/example_domain.com.conf - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Simulating renewal of an existing certificate for example_domain.com - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Congratulations, all simulated renewals succeeded: /etc/letsencrypt/live/hackinroms.com/fullchain.pem (success) - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - |
每當您為執行中的應用程式更新憑證時,都必須重新載入 Nginx,以便容器開始使用新憑證。以下 Docker 指令會重新載入 nginx (請記住我們將容器命名為 nginx)容器:
|
1 |
docker kill -s HUP nginx |
該指令會發送一個 HUP Unix 訊號給運行在 nginx Docker 容器內的 Nginx 程序。這會使 Nginx 重新載入其設定並開始使用更新後的憑證。
由於我們已在代理伺服器上安裝了 TLS/SSL,且我們的網站是使用 HTTPS 提供服務,我們現在需要保護我們的後端應用程式伺服器,使其僅允許來自代理伺服器的請求。
步驟 5:保護後端 Django 伺服器免受外部存取
您在本教學中實作的反向代理伺服器會處理 SSL 終端(SSL termination),它會解密 SSL 連線並將未加密的封包轉發到後端應用程式伺服器。由於我們將保護後端伺服器免受任何外部存取,因此這種安全層級在大多數情況下應該可行。然而,如果您部署的應用程式會傳輸敏感資料(例如銀行資訊或健康數據),則您應該實作 端到端加密.
在本教學中,後端的 Gunicorn 伺服器受到 Nginx 的保護,因為它們並非設計用於直接面對外部。Nginx 代理伺服器就像是通往後端伺服器的閘道,可防止外部用戶端直接存取後端應用程式伺服器。您應該確保所有請求都通過代理伺服器。既然如此,Docker 剛好有一個 會繞過 ufw 防火牆規則並在外部開啟連接埠,這可能會使您的基礎架構面臨安全風險。這實際上很明顯,因為我們在 步驟 1 和 步驟 2 中設定了應用程式伺服器,但並未在 80 規則中允許連接埠 ufw 規則。然而,當您在瀏覽器中造訪任一伺服器的公用 IP 位址時,仍然可以存取網頁。解決此問題的一種方法是直接使用 iptables 而不經過 ufw。您可以閱讀 Docker and iptables 官方文件以了解更多資訊。另一個推薦的方法是使用雲端防火牆。
讓我們修改 UFW 的設定,以封鎖對 Docker 可能已開啟之所有連接埠的外部存取。當我們將主機連接埠 80 對映到 Docker 容器的連接埠 8000,並在 Docker 指令中使用旗標 -p 80:8000 時,我們也無意中在主機上開啟了連接埠 80。您可以透過修改 UFW 的設定來停用此存取,如 ufw-docker 存放庫 README 中所述。
讓我們為第一個 Django 應用程式伺服器進行修改。登入伺服器並開啟位於 /etc/ufw/after.rules 的檔案(以 sudo 使用者身分使用 nano):
|
1 |
sudo nano /etc/ufw/after.rules |
該檔案包含以下 ufw 規則:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
# # rules.input-after # # 在 ufw 命令列新增規則後應執行的規則。自訂 # 規則應新增至以下鏈之一: # ufw-after-input # ufw-after-output # ufw-after-forward # # 請勿刪除這些必要行,否則會出錯 *filter :ufw-after-input - [0:0] :ufw-after-output - [0:0] :ufw-after-forward - [0:0] # 結束必要行 # 預設不記錄干擾服務的日誌 -A ufw-after-input -p udp --dport 137 -j ufw-skip-to-policy-input -A ufw-after-input -p udp --dport 138 -j ufw-skip-to-policy-input -A ufw-after-input -p tcp --dport 139 -j ufw-skip-to-policy-input -A ufw-after-input -p tcp --dport 445 -j ufw-skip-to-policy-input -A ufw-after-input -p udp --dport 67 -j ufw-skip-to-policy-input -A ufw-after-input -p udp --dport 68 -j ufw-skip-to-policy-input # 預設不記錄干擾廣播的日誌 -A ufw-after-input -m addrtype --dst-type BROADCAST -j ufw-skip-to-policy-input # 請勿刪除 'COMMIT' 行,否則將不會處理這些規則 COMMIT |
將以下 UFW 設定行區塊新增至檔案底部:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# BEGIN UFW AND DOCKER *filter :ufw-user-forward - [0:0] :DOCKER-USER - [0:0] -A DOCKER-USER -j RETURN -s 10.0.0.0/8 -A DOCKER-USER -j RETURN -s 172.16.0.0/12 -A DOCKER-USER -j RETURN -s 192.168.0.0/16 -A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN -A DOCKER-USER -j ufw-user-forward -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16 -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8 -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12 -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16 -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8 -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12 -A DOCKER-USER -j RETURN COMMIT # END UFW AND DOCKER |
您新增的規則可防止公眾存取 Docker 開啟的連接埠。此外,它們允許來自 10.0.0.0/8, 172.16.0.0/12,以及 192.168.0.0/16 私有 IP 範圍的存取。您可以從 ufw-docker README 閱讀有關這些規則的更多詳細資訊。編輯完成後儲存並關閉檔案。如果您已設定虛擬私有雲端網路 (VPC),且您的所有三台伺服器都在該 VPC 中,然後您在 Nginx 的 upstream 指令中指定了 Django 伺服器的私有 IP,則此設定應該可以運作 設定 檔案。
然而,我們使用的是公用 IP,且我們可能沒有 VPC。因此,您需要向 ufw 新增一條規則,以允許來自 Nginx 代理伺服器的流量通過 80 兩台 Django 應用程式伺服器的連接埠。您可以向 ufw 新增一條允許規則,並指定來源的 伺服器 IP 到連接埠 80 ,使用以下命令:
|
1 |
sudo ufw allow from NGINX_PROXY_IP to any port 80 |
修改完成後,請重新啟動您的 Django 應用程式伺服器以使變更生效,因為執行 sudo ufw reload 似乎無法使變更生效:
|
1 |
sudo reboot |
|
1 |
docker run -d --rm --name polls --env-file env -p 80:8000 django-polls:v1 |
接下來,嘗試在瀏覽器中造訪第一台 Django 伺服器的 IP,看看它是否顯示 Polls 介面: http://FIRST_SERVER_IP/polls。這將會失敗。現在,登出第一台伺服器,並為第二台伺服器重複您在此處執行的步驟。開啟 /etc/ufw/after.rules,以 sudo 使用者身分使用 nano:
|
1 |
sudo nano /etc/ufw/after.rules |
如同您之前所做,捲動到最下方並新增 UFW 設定區塊:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
# BEGIN UFW AND DOCKER *filter :ufw-user-forward - [0:0] :DOCKER-USER - [0:0] -A DOCKER-USER -j RETURN -s 10.0.0.0/8 -A DOCKER-USER -j RETURN -s 172.16.0.0/12 -A DOCKER-USER -j RETURN -s 192.168.0.0/16 -A DOCKER-USER -p udp -m udp --sport 53 --dport 1024:65535 -j RETURN -A DOCKER-USER -j ufw-user-forward -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 192.168.0.0/16 -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 10.0.0.0/8 -A DOCKER-USER -j DROP -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -d 172.16.0.0/12 -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 192.168.0.0/16 -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 10.0.0.0/8 -A DOCKER-USER -j DROP -p udp -m udp --dport 0:32767 -d 172.16.0.0/12 -A DOCKER-USER -j RETURN COMMIT # 結束 UFW 與 DOCKER |
新增上述區塊後,儲存並關閉檔案。
接下來,新增一條允許規則至 ufw 指定來源的 伺服器 IP 至連接埠 80,使用以下指令:
|
1 |
sudo ufw allow from NGINX_PROXY_IP to any port 80 |
重新啟動您的伺服器以使變更生效:
|
1 |
sudo reboot |
當伺服器重新啟動後,使用以下指令再次啟動容器:
|
1 |
docker run -d --rm --name polls --env-file env -p 80:8000 django-polls:v1 |
直接前往第二台伺服器的 IP 地址,測試您是否可以查看 polls 介面: http://SECOND_SERVER_IP/polls。這應該也會失敗。
此架構現在已準備好進行測試。您可以造訪 https://example_domain_here/polls 以從您的瀏覽器查看預設的 Polls 介面。這表示 Nginx 代理伺服器仍然可以存取 Django 後端伺服器。
結論
在本指南中,我們向您展示了如何使用 Docker 容器實作可擴充的基礎架構。該基礎架構包括一個獨立的 PostgreSQL 資料庫伺服器、兩個後端應用程式伺服器,以及一個用於在兩台伺服器之間進行負載平衡和分配流量的 Nginx 代理伺服器。雖然我們的應用程式是基於 Django Polls 應用程式,但您可以使用不同的框架(例如 Node.js, Laravel 等)為各種應用程式自訂此架構。
這是一個協助您入門的基本指南。您可以加入一些改進,例如將您的映像檔託管在像 Docker Hub 這樣的映像檔存放庫,以便輕鬆地將映像檔分發到多台伺服器。您還可以加入持續整合和部署管線,以便在發生事件時自動建置、測試並將映像檔部署到應用程式伺服器。例如,事件可以是將新程式碼推送到 Git 存放庫上的指定分支。您可能還希望在容器遇到錯誤時自動執行後續處理。Docker 官方文件提供了一個很好的指南,關於 自動啟動容器,以應對錯誤或系統重新啟動的情況。
祝您運算愉快!








留言
目前尚無留言。成為第一個留言的人吧。