使用 Docker、NGINX、Route 53 和 Let's Encrypt 部署服务

导航至

本文由 Jeremy White 撰写,并最初于 2021 年 4 月 13 日发表在 Network to Code 博客

Docker 是部署应用程序或服务的强大工具,目前有许多 Docker 调度工具可以帮助简化已部署容器的管理。但如果您只想部署少量服务,并且不希望设置和管理另一个应用程序堆栈仅为了运行几个容器,那该怎么办呢?我将介绍我是如何在单个 Docker 主机上部署了一组服务。我部署的服务包括 Let’s Encrypt 生成通配符证书,Route 53 注册 A 和 CNAME 记录,以及 NGINX 使用 SNI 封装进行反向代理。我之前在树莓派上以我的 Aquarium Controller 部分部署了一些这些服务在容器中,但我想提供更好的部署灵活性,而不仅仅是局限于部署 ARM 兼容的容器。结合在家部署持久服务的需求,这促使我构建了一个新的物理 Linux 主机,运行 Ubuntu 20.04 LTS & Docker。

本文旨在展示 Docker 的灵活性,而不是部署 Docker 的生产指南。我强烈建议在部署容器时使用调度,如果我在处理多个主机或管理更多服务的情况下工作,我将通过容器调度来部署。

在本篇博文中,我将使用我的 InfluxDB 服务作为示例服务。该服务通过 NGINX 反向代理从 Docker 主机外部使用,也用于东西向容器间的通信。此外,我的所有服务都通过 docker-compose 编码定义,以提供比原始 docker run 命令更轻松的体验。

通信

以下是一个示例,说明用户消费 Docker 中部署的服务,然后消费同一主机上的另一个后端服务。最终用户并不知道流量是如何流动的,而作为管理员,您可以利用容器间的通信。

An example of a communication flow for a user consuming a service deployed in Docker that then consumes another backend service on the same host

Route 53

我正在使用Route 53来注册所有DNS记录;这简化了我本地管理服务的数量,因为我没有运行像Bind 9 DNS这样的服务。我在Route 53中发布的所有记录都解析为私有IP地址,并且无法从我的网络外部路由。在我的例子中,我为物理主机本身有一个单独的A记录,所有服务都是指向服务器的A记录的CNAME记录。我的域名是通过Route 53注册的,这有助于简化流程。

Docker容器名称解析

Docker提供了Docker守护进程内部的网络,以及为同一Docker网络上的容器执行容器名称解析的能力。为了简化这些辅助服务的声明,我正在使用docker-compose;并且为了在容器内部进行东西向通信,我只需要将流量发送到相邻的服务名称。在上面的图中,Grafana服务通过http://influxdb:8086/与InfluxDB服务通信。Docker将主机名influxdb解析为InfluxDB容器的IP地址。

Let's Encrypt

尽管服务部署在我的本地家庭网络中,并且背后有一个适当的防火墙,但我更喜欢从第一天开始部署使用TLS的服务。这是我从第一天进入企业环境就养成的最佳实践。在我的例子中,我使用CertBot来请求和管理我的证书。为了简化证书管理,我正在使用通配符证书*.whitej6.com为所有服务。此外,TLS的一个扩展是服务器名称指示SNI

NGINX和SNI

所有打算从Docker守护进程外部使用的服务都仅在本地的端口定义中暴露给localhost。这确保了我想要允许进入Docker的所有流量必须来自物理主机或部署到主机的应用程序。每个需要暴露的服务都在一个NGINX配置文件中有自己的定义。配置告诉NGINX使用哪个证书,哪个请求的服务器名称映射到哪个本地主机端口。当NGINX收到HTTPS请求时,它首先通过SNI确定请求的是哪个服务,然后执行到localhost正确端口的反向代理。这允许我在物理主机上终止TLS,并从NGINX运行到底层Docker服务的明文协议。通过在容器外部安全地执行自己的TLS终止,可以简化容器部署,减少定制供应商提供的容器,以及/或了解每个供应商如何在他们的容器中执行TLS终止。

示例部署

使用的示例是我在家中托管的实际服务的实际描述。

先决条件

此示例不会介绍如何安装Ubuntu、Docker、docker-compose、CertBot或NGINX。这些项目都有很好的文档记录,应予以参考。示例将介绍这些服务的使用。

Route 53

我正在使用Route 53来托管Docker堆栈中部署的应用程序所需的DNS记录。以下是部署所需的两个记录的表格。在示例的后续步骤中,我将创建一个所需的第三个记录来生成我的证书。

记录类型 目标
A ubuntu-server.whitej6.com 10.0.0.16
CNAME influxdb.whitej6.com ubuntu-server.whitej6.com

您可以看到输出中influxdb.whitej6.comubuntu-server.whitej6.com的CNAME,它解析为10.0.0.16

?  ~ dig influxdb.whitej6.com

; <<>> DiG 9.10.6 <<>> influxdb.whitej6.com
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 61162
;; flags: qr rd ra; QUERY: 1, ANSWER: 2, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 512
;; QUESTION SECTION:
;influxdb.whitej6.com.		IN	A

;; ANSWER SECTION:
influxdb.whitej6.com.	236	IN	CNAME	ubuntu-server.whitej6.com.
ubuntu-server.whitej6.com. 238	IN	A	10.0.0.16

;; Query time: 21 msec
;; SERVER: 8.8.8.8#53(8.8.8.8)
;; WHEN: Tue Apr 06 13:44:41 CDT 2021
;; MSG SIZE  rcvd: 93

?  ~

生成证书

使用Cerbot生成我的Let’s Encrypt证书提供了一个简单的一站式解决方案,用于生成和管理已签名的证书。可以用于验证域名所有权的挑战有很多种。由于我对Certbot的经验有限,并且我可以轻松管理我的域名中的DNS记录,因此我选择使用DNS挑战。在下面的示例中,您将看到我执行命令,Certbot响应我需要为_acme-challenge.whitej6.com创建一个TXT记录,其值为XXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX。在我创建记录后,我就可以按下Enter 继续,Certbot将执行挑战并验证TXT记录的值是否与预期相符。Let’s Encrypt证书是有效期90天的短期证书。Certbot支持通过certbot renew 命令续订证书。

?  ~ sudo certbot -d "*.whitej6.com" --manual --preferred-challenges dns certonly
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator manual, Installer None
Requesting a certificate for *.whitej6.com
Performing the following challenges:
dns-01 challenge for whitej6.com

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please deploy a DNS TXT record under the name
_acme-challenge.whitej6.com with the following value:

XXXXXX-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX

Before continuing, verify the record is deployed.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Press Enter to Continue
Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/whitej6.com/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/whitej6.com/privkey.pem
   Your certificate will expire on 2021-07-05. To obtain a new or
   tweaked version of this certificate in the future, simply run
   certbot again. To non-interactively renew *all* of your
   certificates, run "certbot renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.openssl.ac.cn/donate
   Donating to EFF:                    https://eff.org/donate-le

?  ~

部署容器

容器的部署和管理是通过docker-compose 来完成的,以简化Docker的管理。

~/influxdb/docker-compose.yml

---
version: "3"
services:
  influxdb:
    image: influxdb:latest
    restart: always
    ports:
      - "127.0.0.1:8086:8086"
    volumes:
      - "/influxdb:/var/lib/influxdb"

我想确保每个部署的服务可以按需与其他服务进行通信。为了实现这一点,我在部署中使用相同的compose项目名称。如果每个服务都在另一个compose项目中部署,名称解析和东西向通信就会变得复杂。在下面的输出中,您将看到Docker对在docker-compose.yml文件外部声明的服务不太高兴,这是预料之中的。

?  ~ export COMPOSE_PROJECT_NAME="server"
?  ~ docker-compose -f influxdb/docker-compose.yml up -d
Building with native build. Learn about native build in Compose here: https://docs.docker.net.cn/go/compose-native-build/
WARNING: Found orphan containers (server_grafana_1, server_gitlab_1, server_redis_1, server_registry_1, server_netbox_1, server_postgres_1) for this project. If you removed or renamed this service in your compose file, you can run this command with the --remove-orphans flag to clean it up.
Starting server_influxdb_1  ... done
?  ~

配置NGINX

我个人喜欢保留基础NGINX配置文件默认设置,并在conf.d文件夹中使用单独的.conf文件来覆盖所需的部分。我将每个覆盖声明在其自己的文件中,以便更容易识别和修改。下面的文件路径基于NGINX的默认安装位置。您的安装可能使用不同的路径,但概念是相同的。

/etc/nginx/conf.d/redirect.conf

强制所有HTTP流量重定向到HTTPS,并保留请求的主机。

server {
    listen 80 default_server;
    return 301 https://$host$request_uri;
}

/etc/nginx/conf.d/influxdb.conf

map $http_x_forwarded_proto $thescheme {
    default $scheme;
    https https;
}

server {
    # Inbound requested hostname
    server_name influxdb.whitej6.com;

    listen 443 ssl;

    # Let's Encrypt certificate location
    ssl_certificate /etc/letsencrypt/live/whitej6.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/whitej6.com/privkey.pem;

    client_max_body_size 25m;
    proxy_set_header X-Forwarded-Host $http_host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Proto $thescheme;
    add_header P3P 'CP="ALL DSP COR PSAa PSDa OUR NOR ONL UNI COM NAV"';

    # Where to reverse proxy HTTP traffic to
    location / {
        proxy_pass https://127.0.0.1:8086;
    }

}

重启NGINX

一旦完成NGINX的配置,您需要重启NGINX服务。我使用systemctl 来管理主机上运行的系统服务。

?  ~ sudo systemctl restart nginx
?  ~

验证堆栈

?  influxdb echo "curl from outside the docker network"
curl from outside the docker network
?  influxdb curl https://influxdb.whitej6.com/health
{"checks":[],"message":"ready for queries and writes","name":"influxdb","status":"pass","version":"1.8.4"}%
?  influxdb
?  influxdb echo "curl from inside the docker network"
curl from inside the docker network
?  influxdb docker exec -it server_gitlab_1 sh -c "curl http://influxdb:8086/health"
{"checks":[],"message":"ready for queries and writes","name":"influxdb","status":"pass","version":"1.8.4"%
?  influxdb

在所有服务部署并验证后,我有一个完全功能的多租户Docker主机,通过HTTPS安全地服务于每个服务,并使用DNS记录来提高可用性。这使我能够将Grafana和InfluxDB服务从托管我的水族控制器的主板上移到Docker主机上,以改善用户体验。希望这篇文章能帮助您安全地将服务部署到Docker主机上。