数以百万计的用户通过互联网获取各种目的的信息,包括学习、娱乐、新闻以及与朋友分享他们的生活’进展。因此,在部署应用程序时,为您的应用程序实现一个高度安全且可扩展的基础架构最符合您的利益。云提供了各种方法来安全地扩展和保护一个 Django 应用程序。水平扩展是一种允许您运行应用程序的多个副本的方法。这确保了它具有更高的容错性和高可用性。它还提高了同时处理多个请求的性能。
水平扩展 Django 应用程序
您可以通过配置多个运行 Django 应用程序及其 WSGI HTTP 服务器(例如 Gunicorn 或 uWSGI )的应用程序服务器来水平扩展 Django 应用程序。然后,您需要构建一个基础架构,以便在这些应用服务器之间分配传入的请求。负载均衡器和 像 Nginx 这样的反向代理 可以帮助您的基础架构进行流量分配。Nginx 可以部署 SSL 证书 ,从而确保通过 HTTPS 安全连接到您的应用程序。最后,Nginx 还可以提供静态内容的缓存,以最大程度地减轻服务器的负载。
单独配置这些不同的组件并确保它们进行通信可能是一项艰巨的任务。幸运的是,使用 Docker 简化了配置过程,并确保各种组件无论部署在何处都以相同的方式运行。
您将在本指南中做什么
在本指南中,您将学习如何水平扩展一个使用 Gunicorn WSGI HTTP 服务器提供服务的容器化 Django 应用程序。您将配置两台安装了 Docker 的应用程序服务器,它们运行相同副本的 Django 和 Gunicorn 应用程序容器。
您还将使用 Let’s Encrypt SSL 证书来保护您的应用程序,方法是配置和设置一个 第三台代理服务器 ,它将运行一个 Nginx 反向代理 容器和 Certbot 客户端容器。Certbot 是一个帮助管理来自 Let’s Encrypt 证书颁发机构的 SSL 证书的软件包。它获取证书,使用证书的位置配置 Nginx 服务器块,并管理自动续订。它通过配置 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,一个包含 Python 应用程序代码的 django-polls 目录,以及一个 env 文件,其中包含将在启动时传递到容器中以修改其行为的环境变量列表。在 Dockerfile 中,我们通过 requirements.txt 文件定义 Django 包依赖项。此外,我们需要声明一个用于接收传入流量的端口 8000,并将其设置为运行一个带有 3 个 worker 的 gunicorn 服务器。要了解有关 Dockerfile 指令的更多信息,请查看教程 第 7 步 中的 Building a Django and Gunicorn Application with Docker on Ubuntu.
您可以使用以下命令构建 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 docs 中所述。您可以使用此命令生成一个随机字符串并将其设置为该变量: python -c 'from django.core.management.utils import get_random_secret_key; print(get_random_secret_key())'.
-
DJANGO_ALLOWED_HOSTS:此值用于保护您的应用免受 HTTP 主机头攻击。您可以将其设置为 *,如果在开发模式下,通配符匹配所有主机。当您将应用部署到生产环境时,请将其设置为您注册的域名。在我们的演示中,它是 example_domain.com.
-
DB_DATABASE:将其设置为您在Prerequisites 部分中创建的 PostgreSQL 数据库的名称,在我们的例子中,它是 polls_db.
-
DB_USERNAME:将其设置为您为数据库选择的用户名。
-
DB_PASSWORD:将其设置为您为数据库选择的密码。
-
DB_HOST:将其设置为运行数据库实例的主机,正如您在Prerequisites 部分中所设置的那样。这在教程Building a Django and Gunicorn Application with Docker on Ubuntu 的步骤 1 和 2 中有关于设置数据库的说明.
-
DB_PORT:将其设置为数据库的端口。
编辑完成后保存并关闭文件。在配置好数据库凭据后,我们可以通过运行容器并覆盖 Dockerfile 中设置的 CMD 命令来创建数据库模式。您可以在官方文档的Dockerfile entry point from official Docs 中找到更多信息。接下来,执行以下命令:
|
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 |
按照提示提供用户名、电子邮件地址和密码。重新输入密码,然后按回车键创建用户。退出 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 Page Not Found,因为我们还没有为 / 路径。导航至 http://FIRST_SERVER_IP/polls 以查看 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 以确认其按预期运行。如果一切顺利,您应该能够看到投票(polls)界面。
现在您有两个运行相同应用程序副本的应用服务器。在下一步中,您将配置 Nginx 容器以用作反向代理。
步骤 3:设置 Nginx Docker 容器
Nginx 是世界上最流行的开源 Web 服务器软件之一。它负责确保互联网上流量最高的网站的可用性和可扩展性。它保证了 安全性,并且非常多功能。您可以将其用于 反向代理、缓存和负载均衡。我们已经将应用程序设置为使用独立的对象存储服务来处理其静态和媒体文件。因此,我们不会使用 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 连接。在您于 keepalive_timeout 指令中设置的秒数之后,Nginx 将关闭与客户端的连接。您可以从官方文档中找到关于 用于部署 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 模块 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,如 前提条件 说明中所述。
您还应该有一个已注册域名的 DNS A 记录,指向您代理服务器的 IP 地址。您可以通过运行 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 在暂存服务器上执行并验证您的域名。
输入您的电子邮件地址并接受服务条款(当系统提示时)。以下是成功验证的输出:
|
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。您可能会看到一个关于网站不安全的警告:
这是因为我们只配置了暂存/测试证书,而没有配置来自 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 容器将一直运行,您应该在执行用于证书更新的 certbot 命令时使用 webroot 模式,而不是 standalone 模式。请记住,您已指定了 /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 连接并将未加密的数据包转发到后端应用服务器。由于我们将保护后端服务器免受任何外部访问,因此这种安全级别应该适用于大多数情况。但是,如果您部署的应用程序传输敏感数据(例如银行信息或健康数据),则应实现 端到端加密.
在本教程中,后端的 Gunicorn 服务器受到 Nginx 的保护,因为它们不应该直接面向外部。Nginx 代理服务器就像是通往后端服务器的网关,阻止外部客户端直接访问后端应用服务器。您应该确保所有请求都通过代理服务器。既然如此,Docker 恰好有一个 绕过 ufw 的问题 防火墙规则并在外部打开端口,这可能会使您的基础设施处于不安全状态。这实际上很明显,因为我们在 步骤 1 和 步骤 2 中设置了应用服务器,而没有允许端口 80 在 ufw 规则中。然而,当您在浏览器中访问任一服务器的公网 IP 地址时,您仍然可以访问网页。解决此问题的一种方法是直接使用 iptables,而不通过 ufw。您可以阅读 Docker 和 iptables 官方文档以了解更多信息。另一个推荐的方法是使用云防火墙。
让我们修改 UFW 的配置,以阻止外部访问可能已被 Docker 打开的所有端口。当我们将主机的端口 80 映射到 Docker 容器的端口 8000,并使用 Docker 命令中的标志 -p 80:8000 时,我们也无意中在主机上打开了端口 80。您可以通过修改 UFW 的配置来禁用此访问,如 ufw-docker 仓库 README 中所述。
让我们为第一个 Django 应用服务器进行更改。登录到服务器并使用 nano 以 sudo 用户身份打开位于 /etc/ufw/after.rules 的文件:
|
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。这将会失败。现在,退出第一台服务器,并为第二台服务器重复您在此处执行的步骤。以 sudo 用户身份使用 nano 打开 /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 # END UFW AND 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 官方文档提供了一个很好的指南,关于 自动启动容器,以防出现错误或系统重启。
祝您使用愉快!








评论
暂无评论。发表第一条评论吧。