Nginx Proxy Manager服务发现集成:Consul与etcd动态后端配置

Nginx Proxy Manager服务发现集成:Consul与etcd动态后端配置

【免费下载链接】nginx-proxy-manager Docker container for managing Nginx proxy hosts with a simple, powerful interface 【免费下载链接】nginx-proxy-manager 项目地址: https://gitcode.com/GitHub_Trending/ng/nginx-proxy-manager

痛点与解决方案概述

当你还在为Nginx Proxy Manager手动配置后端服务而烦恼?当后端服务动态扩缩容时还在人工修改配置文件?本文将详解如何通过Consul与etcd实现Nginx Proxy Manager的服务发现功能,实现后端服务的动态配置与自动更新,彻底摆脱手动运维的繁琐。

读完本文你将获得:

  • 理解服务发现如何解决传统反向代理的痛点
  • 掌握Nginx Proxy Manager集成Consul的完整实现方案
  • 学会etcd后端配置的动态更新机制
  • 获取可直接应用的代码实现与配置示例

传统反向代理的挑战

传统Nginx Proxy Manager配置存在以下关键痛点:

mermaid

服务发现机制通过以下方式解决这些问题:

  1. 动态服务注册:后端服务启动时自动注册到注册中心
  2. 健康检查:自动检测服务可用性,下线异常实例
  3. 配置自动更新:当服务发生变化时,自动更新Nginx配置
  4. 负载均衡:自动实现服务集群的负载均衡

集成方案架构设计

整体架构

mermaid

工作流程

mermaid

Consul集成实现

环境准备与依赖安装

首先安装必要的Node.js依赖:

npm install consul axios

Consul服务发现模块实现

创建backend/lib/service-discovery/consul.js文件:

const Consul = require('consul');
const logger = require('../../logger').consul;
const internalNginx = require('../../internal/nginx');

class ConsulServiceDiscovery {
  constructor(config) {
    this.consul = new Consul({
      host: config.host || 'localhost',
      port: config.port || 8500,
      promisify: true
    });
    this.services = new Map();
    this.watches = new Map();
  }

  /**
   * 初始化Consul监听
   */
  async init() {
    logger.info('Initializing Consul service discovery');
    
    // 监听所有服务变化
    const watch = await this.consul.watch({
      method: this.consul.catalog.service.list,
      options: {}
    });
    
    watch.on('change', (data) => {
      this.handleServiceListChange(data);
    });
    
    watch.on('error', (err) => {
      logger.error('Consul watch error:', err);
    });
    
    return this;
  }
  
  /**
   * 处理服务列表变化
   */
  async handleServiceListChange(services) {
    if (!services) return;
    
    for (const serviceName of Object.keys(services)) {
      await this.watchService(serviceName);
    }
    
    // 清理已不存在的服务监听
    for (const [serviceName, watch] of this.watches.entries()) {
      if (!services[serviceName]) {
        watch.end();
        this.watches.delete(serviceName);
        this.services.delete(serviceName);
        await this.updateNginxConfig(serviceName);
      }
    }
  }
  
  /**
   * 监听特定服务
   */
  async watchService(serviceName) {
    if (this.watches.has(serviceName)) return;
    
    logger.info(`Watching service: ${serviceName}`);
    
    try {
      const watch = await this.consul.watch({
        method: this.consul.health.service,
        options: {
          service: serviceName,
          passing: true
        }
      });
      
      watch.on('change', (data) => {
        this.handleServiceChange(serviceName, data);
      });
      
      watch.on('error', (err) => {
        logger.error(`Error watching service ${serviceName}:`, err);
        this.watches.delete(serviceName);
      });
      
      this.watches.set(serviceName, watch);
    } catch (err) {
      logger.error(`Failed to watch service ${serviceName}:`, err);
    }
  }
  
  /**
   * 处理服务实例变化
   */
  async handleServiceChange(serviceName, instances) {
    if (!instances || instances.length === 0) {
      logger.info(`No healthy instances for service: ${serviceName}`);
      this.services.delete(serviceName);
    } else {
      const endpoints = instances.map(instance => ({
        id: instance.Service.ID,
        address: instance.Service.Address || instance.Node.Address,
        port: instance.Service.Port,
        tags: instance.Service.Tags || []
      }));
      
      this.services.set(serviceName, endpoints);
      logger.info(`Updated service ${serviceName}: ${endpoints.length} healthy instances`);
    }
    
    // 更新Nginx配置
    await this.updateNginxConfig(serviceName);
  }
  
  /**
   * 更新Nginx配置
   */
  async updateNginxConfig(serviceName) {
    const endpoints = this.services.get(serviceName) || [];
    
    // 查找关联的代理主机
    const proxyHosts = await this.findProxyHostsByService(serviceName);
    
    for (const host of proxyHosts) {
      // 更新代理主机的转发目标
      host.forward_host = `consul_${serviceName}`; // 特殊标记,供Nginx模板使用
      host.meta = host.meta || {};
      host.meta.service_discovery = {
        type: 'consul',
        service: serviceName,
        endpoints: endpoints
      };
      
      // 重新生成Nginx配置
      await internalNginx.configure(
        require('../../models/proxy_host'),
        'proxy_host',
        host
      );
    }
    
    // 重新加载Nginx
    await internalNginx.reload();
  }
  
  /**
   * 查找关联的代理主机
   */
  async findProxyHostsByService(serviceName) {
    const proxyHostModel = require('../../models/proxy_host');
    return proxyHostModel.query()
      .where('meta', 'like', `%service:${serviceName}%`)
      .andWhere('enabled', 1)
      .andWhere('is_deleted', 0);
  }
  
  /**
   * 获取服务实例列表
   */
  getServiceEndpoints(serviceName) {
    return this.services.get(serviceName) || [];
  }
}

module.exports = ConsulServiceDiscovery;

修改Nginx配置生成模板

编辑backend/templates/proxy_host.conf文件,添加服务发现支持:

{% include "_header_comment.conf" %}

{% if enabled %}

{% include "_hsts_map.conf" %}

server {
  set $forward_scheme {{ forward_scheme }};
  set $server         "{{ forward_host }}";
  set $port           {{ forward_port }};

  {% include "_listen.conf" %}
  {% include "_certificates.conf" %}
  {% include "_assets.conf" %}
  {% include "_exploits.conf" %}
  {% include "_hsts.conf" %}
  {% include "_forced_ssl.conf" %}

  {% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %}
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection $http_connection;
  proxy_http_version 1.1;
  {% endif %}

  access_log /data/logs/proxy-host-{{ id }}_access.log proxy;
  error_log /data/logs/proxy-host-{{ id }}_error.log warn;

  {{ advanced_config }}

  {{ locations }}

  {% if use_default_location %}

  location / {
    {% include "_access.conf" %}
    {% include "_hsts.conf" %}

    {% if allow_websocket_upgrade == 1 or allow_websocket_upgrade == true %}
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $http_connection;
    proxy_http_version 1.1;
    {% endif %}

    # 服务发现配置
    {% if meta.service_discovery and meta.service_discovery.type == 'consul' %}
      {% set service = meta.service_discovery.service %}
      {% set endpoints = meta.service_discovery.endpoints %}
      
      # 配置上游服务器组
      upstream consul_{{ service }} {
        {% for endpoint in endpoints %}
        server {{ endpoint.address }}:{{ endpoint.port }};
        {% endfor %}
        
        # 健康检查
        keepalive 32;
      }
      
      # 使用上游服务器组
      proxy_pass http://consul_{{ service }};
    {% else %}
      # 传统代理配置
      proxy_pass {{ forward_scheme }}://{{ forward_host }}:{{ forward_port }}{{ forward_path | default('/') }};
    {% endif %}
    
    include conf.d/include/proxy.conf;
  }
  {% endif %}

  # Custom
  include /data/nginx/custom/server_proxy[.]conf;
}
{% endif %}

添加配置与初始化代码

编辑backend/config/default.json,添加Consul配置:

{
  "database": {
    "engine": "mysql2",
    "host": "db",
    "name": "npm",
    "user": "npm",
    "password": "npm",
    "port": 3306
  },
  "serviceDiscovery": {
    "consul": {
      "host": "consul",
      "port": 8500
    }
  }
}

修改backend/app.js,添加服务发现初始化:

// 在文件顶部添加
const config = require('./lib/config');
let serviceDiscovery = null;

// ... 现有代码 ...

// 在app定义之后添加服务发现初始化
if (config.has('serviceDiscovery.consul')) {
  const ConsulServiceDiscovery = require('./lib/service-discovery/consul');
  serviceDiscovery = new ConsulServiceDiscovery(config.get('serviceDiscovery.consul'));
  serviceDiscovery.init().catch(err => {
    logger.error('Failed to initialize Consul service discovery:', err);
  });
}

// 导出供其他模块使用
module.exports = {
  app,
  serviceDiscovery
};

添加UI支持

修改前端代码,允许在添加/编辑代理主机时指定Consul服务:

编辑frontend/js/app/nginx/proxy-hosts/form.js,添加服务发现选项:

// 在表单字段中添加服务发现选项
this.formFields = [
  // ... 现有字段 ...
  {
    type: 'select',
    name: 'service_discovery_type',
    label: '服务发现类型',
    options: [
      { value: '', label: '无' },
      { value: 'consul', label: 'Consul' },
      { value: 'etcd', label: 'etcd' }
    ],
    value: this.model.get('meta')?.service_discovery?.type || ''
  },
  {
    type: 'text',
    name: 'service_name',
    label: '服务名称',
    placeholder: 'Consul或etcd中的服务名称',
    value: this.model.get('meta')?.service_discovery?.service || '',
    show: () => this.getFormValue('service_discovery_type') !== ''
  }
];

// 保存时处理服务发现元数据
onSave: function() {
  // ... 现有代码 ...
  
  const serviceDiscoveryType = this.getFormValue('service_discovery_type');
  if (serviceDiscoveryType) {
    this.model.set('meta', this.model.get('meta') || {});
    this.model.get('meta').service_discovery = {
      type: serviceDiscoveryType,
      service: this.getFormValue('service_name')
    };
    
    // 特殊处理:清除手动设置的转发目标
    this.model.set('forward_host', `{{${serviceDiscoveryType}_${this.getFormValue('service_name')}}}`);
    this.model.set('forward_port', 0);
  } else {
    // 清除服务发现元数据
    if (this.model.get('meta')) {
      delete this.model.get('meta').service_discovery;
    }
  }
  
  // ... 保存逻辑 ...
}

etcd集成实现

etcd服务发现模块

创建backend/lib/service-discovery/etcd.js文件:

const { Etcd3 } = require('etcd3');
const logger = require('../../logger').etcd;
const internalNginx = require('../../internal/nginx');

class EtcdServiceDiscovery {
  constructor(config) {
    this.client = new Etcd3({
      hosts: config.hosts || ['localhost:2379']
    });
    this.services = new Map();
    this.watchers = new Map();
  }

  /**
   * 初始化etcd监听
   */
  async init() {
    logger.info('Initializing etcd service discovery');
    
    // 监听服务前缀
    const servicePrefix = '/services/';
    const watcher = this.client.watch().prefix(servicePrefix).create();
    
    watcher.on('put', async (event) => {
      await this.handleServiceUpdate(event.key.toString(), event.value.toString());
    });
    
    watcher.on('delete', async (event) => {
      await this.handleServiceDelete(event.key.toString());
    });
    
    watcher.on('error', (err) => {
      logger.error('etcd watch error:', err);
    });
    
    // 加载现有服务
    const services = await this.client.get().prefix(servicePrefix).strings();
    for (const [key, value] of Object.entries(services)) {
      await this.handleServiceUpdate(key, value);
    }
    
    return this;
  }
  
  /**
   * 处理服务更新
   */
  async handleServiceUpdate(key, value) {
    // 解析服务键: /services/{serviceName}/{instanceId}
    const parts = key.split('/').filter(part => part);
    if (parts.length < 2) return;
    
    const serviceName = parts[1];
    const instanceId = parts.slice(2).join('/');
    
    try {
      const instance = JSON.parse(value);
      
      if (!this.services.has(serviceName)) {
        this.services.set(serviceName, new Map());
      }
      
      const instances = this.services.get(serviceName);
      
      if (instance.status === 'up') {
        instances.set(instanceId, {
          id: instanceId,
          address: instance.address,
          port: instance.port,
          metadata: instance.metadata || {}
        });
        logger.info(`Added instance ${instanceId} for service ${serviceName}`);
      } else {
        instances.delete(instanceId);
        logger.info(`Removed instance ${instanceId} for service ${serviceName}`);
      }
      
      // 如果服务实例为空,删除服务
      if (instances.size === 0) {
        this.services.delete(serviceName);
      }
      
      // 更新Nginx配置
      await this.updateNginxConfig(serviceName);
    } catch (err) {
      logger.error(`Failed to process service update ${key}:`, err);
    }
  }
  
  /**
   * 处理服务删除
   */
  async handleServiceDelete(key) {
    const parts = key.split('/').filter(part => part);
    if (parts.length < 2) return;
    
    const serviceName = parts[1];
    const instanceId = parts.slice(2).join('/');
    
    if (this.services.has(serviceName)) {
      const instances = this.services.get(serviceName);
      instances.delete(instanceId);
      
      logger.info(`Deleted instance ${instanceId} for service ${serviceName}`);
      
      if (instances.size === 0) {
        this.services.delete(serviceName);
      }
      
      // 更新Nginx配置
      await this.updateNginxConfig(serviceName);
    }
  }
  
  /**
   * 更新Nginx配置
   */
  async updateNginxConfig(serviceName) {
    const instancesMap = this.services.get(serviceName);
    const endpoints = instancesMap ? Array.from(instancesMap.values()) : [];
    
    // 查找关联的代理主机
    const proxyHosts = await this.findProxyHostsByService(serviceName);
    
    for (const host of proxyHosts) {
      // 更新代理主机的转发目标
      host.forward_host = `etcd_${serviceName}`; // 特殊标记,供Nginx模板使用
      host.meta = host.meta || {};
      host.meta.service_discovery = {
        type: 'etcd',
        service: serviceName,
        endpoints: endpoints
      };
      
      // 重新生成Nginx配置
      await internalNginx.configure(
        require('../../models/proxy_host'),
        'proxy_host',
        host
      );
    }
    
    // 重新加载Nginx
    await internalNginx.reload();
  }
  
  /**
   * 查找关联的代理主机
   */
  async findProxyHostsByService(serviceName) {
    const proxyHostModel = require('../../models/proxy_host');
    return proxyHostModel.query()
      .where('meta', 'like', `%service:${serviceName}%`)
      .andWhere('enabled', 1)
      .andWhere('is_deleted', 0);
  }
  
  /**
   * 获取服务实例列表
   */
  getServiceEndpoints(serviceName) {
    const instancesMap = this.services.get(serviceName);
    return instancesMap ? Array.from(instancesMap.values()) : [];
  }
}

module.exports = EtcdServiceDiscovery;

更新Nginx模板支持etcd

继续编辑backend/templates/proxy_host.conf,添加etcd支持:

{% if meta.service_discovery %}
  {% if meta.service_discovery.type == 'consul' %}
    # Consul服务发现配置
    upstream consul_{{ meta.service_discovery.service }} {
      {% for endpoint in meta.service_discovery.endpoints %}
      server {{ endpoint.address }}:{{ endpoint.port }};
      {% endfor %}
      
      keepalive 32;
    }
  {% elsif meta.service_discovery.type == 'etcd' %}
    # etcd服务发现配置
    upstream etcd_{{ meta.service_discovery.service }} {
      {% for endpoint in meta.service_discovery.endpoints %}
      server {{ endpoint.address }}:{{ endpoint.port }};
      {% endfor %}
      
      keepalive 32;
    }
  {% endif %}
{% endif %}

集成到应用

更新backend/app.js,添加etcd支持:

// 在服务发现初始化部分添加
if (config.has('serviceDiscovery.consul')) {
  // ... 现有Consul代码 ...
}

// 添加etcd支持
if (config.has('serviceDiscovery.etcd')) {
  const EtcdServiceDiscovery = require('./lib/service-discovery/etcd');
  serviceDiscovery = new EtcdServiceDiscovery(config.get('serviceDiscovery.etcd'));
  serviceDiscovery.init().catch(err => {
    logger.error('Failed to initialize etcd service discovery:', err);
  });
}

验证与测试

启动Consul并注册服务

# 启动Consul
docker run -d --name consul -p 8500:8500 consul

# 注册测试服务
curl -X PUT http://localhost:8500/v1/agent/service/register -d '{
  "Name": "web-service",
  "ID": "web-service-1",
  "Address": "192.168.1.100",
  "Port": 8080,
  "Check": {
    "HTTP": "http://192.168.1.100:8080/health",
    "Interval": "10s"
  }
}'

在Nginx Proxy Manager中配置服务发现

  1. 登录Nginx Proxy Manager管理界面
  2. 添加新的代理主机
  3. 在服务发现类型中选择"Consul"
  4. 服务名称填写"web-service"
  5. 保存配置

验证动态更新

  1. 添加第二个服务实例:
curl -X PUT http://localhost:8500/v1/agent/service/register -d '{
  "Name": "web-service",
  "ID": "web-service-2",
  "Address": "192.168.1.101",
  "Port": 8080,
  "Check": {
    "HTTP": "http://192.168.1.101:8080/health",
    "Interval": "10s"
  }
}'
  1. 查看Nginx配置是否自动更新:
cat /data/nginx/proxy_host/[host-id].conf
  1. 确认新实例已添加到upstream配置中

  2. 下线一个实例:

curl -X PUT http://localhost:8500/v1/agent/service/deregister/web-service-1
  1. 再次检查Nginx配置,确认实例已被移除

高可用与最佳实践

服务发现高可用

mermaid

配置建议

  1. 多实例部署:Consul/etcd采用集群部署,避免单点故障
  2. 健康检查:配置适当的健康检查策略,确保只将流量转发到健康实例
  3. 超时设置:合理设置Nginx的连接超时和重试机制
  4. 缓存策略:实现本地缓存,在服务发现不可用时提供降级服务
  5. 监控告警:监控服务发现状态,当服务实例异常时及时告警

性能优化

mermaid

总结与展望

通过本文介绍的方法,我们成功实现了Nginx Proxy Manager与Consul、etcd的服务发现集成,解决了传统静态配置的痛点。这一方案具有以下优势:

  1. 自动化:后端服务动态变化时自动更新配置
  2. 高可用:自动摘除故障实例,提高系统稳定性
  3. 弹性伸缩:支持后端服务的无缝扩缩容
  4. 减少人工干预:降低运维成本,减少人为错误

未来可以进一步扩展以下功能:

  1. 更智能的负载均衡:结合服务健康状态和性能指标实现智能路由
  2. 流量控制:基于服务权重的流量分配
  3. 服务熔断:在服务异常时快速熔断,保护系统
  4. 多区域部署:跨区域服务发现与流量路由

通过服务发现集成,Nginx Proxy Manager不仅保留了原有的易用性,还获得了企业级的动态配置能力,使其在容器化、微服务架构环境中发挥更大价值。

附录:完整配置示例

Consul配置示例

{
  "serviceDiscovery": {
    "consul": {
      "host": "consul-server",
      "port": 8500,
      "timeout": 5000,
      "retry": 3
    }
  }
}

etcd配置示例

{
  "serviceDiscovery": {
    "etcd": {
      "hosts": ["etcd1:2379", "etcd2:2379", "etcd3:2379"],
      "timeout": 5000
    }
  }
}

Docker Compose部署示例

version: '3'

services:
  app:
    image: nginx-proxy-manager:custom
    ports:
      - "80:80"
      - "443:443"
      - "81:81"
    environment:
      - DB_MYSQL_HOST=db
      - DB_MYSQL_USER=npm
      - DB_MYSQL_PASSWORD=npm
      - DB_MYSQL_NAME=npm
    volumes:
      - ./data:/data
      - ./letsencrypt:/etc/letsencrypt
    depends_on:
      - db
      - consul

  db:
    image: mysql:5.7
    environment:
      - MYSQL_ROOT_PASSWORD=root
      - MYSQL_DATABASE=npm
      - MYSQL_USER=npm
      - MYSQL_PASSWORD=npm
    volumes:
      - ./mysql:/var/lib/mysql

  consul:
    image: consul
    ports:
      - "8500:8500"
    volumes:
      - ./consul:/consul/data

【免费下载链接】nginx-proxy-manager Docker container for managing Nginx proxy hosts with a simple, powerful interface 【免费下载链接】nginx-proxy-manager 项目地址: https://gitcode.com/GitHub_Trending/ng/nginx-proxy-manager

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值