如何实现动态二级域名的解析和转发? | nginx配置转发实战2

趁热记录下,给未来的自己

0 | 前言

前段时间,在项目上,遇到了这样一个业务场景:

我们提供一个应用开发平台,开发者可以在平台上部署自己的应用,部署方式可以是提供:

  1. 基于gradio的python代码
  2. 基于模板的html文件修改里面的src地址为用户自己提供的网址

平台会将以上代码打包成镜像,以容器方式对外提供服务,架构如下

那么,用户如果想访问应用 gradio-app1 的话,有哪些访问形式呢?

  1. 基于subpath,如: abc.com/gradio-app1...
  2. 基于subdomain,如:gradio-app1.abc.com/xxx

优劣对比:

优势 劣势
subpath 1. 只需要在ingress里配置好相关的转发路径,无需额外引入其他组件; 2. 只需要配置一个域名解析即可,请求都可以通过subpath进行转发; 1. 因为有subpath的存在,可能会和前端框架的路由产生冲突:如访问 abc.com/gradio-app1... 当请求到达gradio框架时,gradio返回给浏览器的下一跳路由是/yyy,那么浏览器访问的下一跳地址就是 abc.com/gradio-app1... 而这个地址因为缺少了subpath(gradio-app1),就无法将下一跳的请求转发给gradio-app1这个应用了
subdomain 1. 没有/subpath,只有根路由/, 对前端框架的路由友好,兼容性更强, 1. subdomain是根据用户行为动态生成的,无法做到提前配置DNS解析,需要实现动态域名解析,这是一个技术重点和难点

虽然,部分带前端页面的框架(如code-server,jupyterlab等),本身可以配置public path(即返回给浏览器的下一跳自动带上/gradio-app1/yyy),但还是有相当一部分的框架没有考虑这个特性,所以,subpath的应用场景完全依赖前端框架,导致适用面不广泛。

而基于subdomain的方式,没有subpath方案的这个block点,只需要解决动态域名解析的问题就行。因此本文要解决的问题就聚焦到:如何设计一套动态域名的 DNS 解析和转发。

1 | 方案选择

要实现动态域名解析(DDNS),其实很多DNS解析厂商会有对应的API可以去调用,那么实现逻辑是:

  1. 用户创建一个应用 gradio-app1;
  2. 业务后端通过API找DNS解析厂商申请新的域名A解析:gradio-app1.abc.com -> 特定IP;
  3. A解析添加成功后,返回创建应用成功给到前端用户;

这个方案,会存在几个问题:

  1. DDNS 依赖厂商API能力(有没有?快不快?稳不稳定?限不限流?解析的域名条数有没有上限?等等)
  2. 解析的条目和平台应用一一对应,随着应用的增多,解析条目也会日渐庞大,难于查找和管理
  3. 应用创建速度会因为多了一次外部请求而变慢,创建应用的成功率也可能会降低

因此,该方案只能算是解决了技术上有无的问题,但算不上一个好方案(算是兜底方案)。为了解决以上问题,这里我们提出了一个泛域名解析+nginx代理转发的实现方案:

大概思路:

  1. 创建一个nginx实例,通过ingress对外暴露;
  2. 将泛域名*.abc.com都解析到这个ingress绑定的外网IP上;
  3. 泛域名部分是一个二级泛域名,格式如:gradio-appx 和 static-appx,分别对应不同的APP类型;
  4. 在nginx-server模块配置转发规则:获取subdomain部分(如gradio-app1),然后将请求proxy-pass给gradio-app1.svc
  5. 注意,这里要求 gradio 应用对应的service资源名称要和subdomain保持一致(或者具有一一对应关系),这样就无需根据subdomain去查找对应的service名称和对应的k8s内部域名地址。

2 | 方案实施

这里nginx以k8s方式部署。

2.1| Deployment

这是一个Deployment资源,里面包含两个Container,一个是openresty的nginx,另一个是go-dnsmasq。

why openresty? OpenResty是一个基于Nginx的Web应用服务器,它整合了Nginx服务器和一些Lua模块,提供了更高级的功能和可扩展性。它的核心思想是使用Lua脚本语言扩展Nginx的功能,从而实现更复杂的Web应用开发。因为该方案需要在nginx里做一些业务逻辑的处理,需要使用到lua脚本对请求进行改写和转发,所以用openresty会更便捷。
why go-dnsmasq? 这是一个dns解析器,主要是将整个集群+node节点+etc/hosts的dns解析融合在一起代理给nginx,这样nginx可以实现IP、集群域名以及外部域名的解析。这里通过暴露127.0.0.1:53端口,对外提供服务。

yaml 复制代码
apiVersion: apps/v1
kind: Deployment
metadata:
  labels:
    component: nginx
  name: nginx-domain-proxy-deploy
  namespace: your_namespace
spec:
  replicas: 1
  selector:
    matchLabels:
      component: nginx
  template:
    metadata:
      labels:
        component: nginx
    spec:
      restartPolicy: Always
      containers:
      - name: nginx
        image: "openresty/openresty:centos"
        ports:
        - name: http
          containerPort: 80
          protocol: TCP
        volumeMounts:
        - mountPath: /usr/local/openresty/nginx/conf/nginx.conf
          name: config
          subPath: nginx.conf
      - name: dnsmasq
        image: "janeczku/go-dnsmasq:release-1.0.7"
        args:
          - --listen
          - "127.0.0.1:53"
          - --default-resolver
          - --append-search-domains
          - --hostsfile=/etc/hosts
          - --verbose
      volumes:
      - name: config
        configMap:
          name: nginx-domain-proxy-cm

2.2 | ConfigMap

这个ConfigMap主要定义了nginx.conf的配置。这个配置的前部分比较常规,是一些常规配置(具体的配置需要根据业务特点做适配,请勿直接搬运到生产环境):

  1. worker_processes 4;:这个指令设置了Nginx服务器的工作进程数量为4个。每个工作进程负责接收和处理客户端请求。
  2. error_log /error.log;:这个指令指定了错误日志文件的路径为/error.log。Nginx会将出现的错误和异常信息写入该文件。
  3. events { ... }:这个部分是Nginx服务器的事件模块配置。其中包含一些事件相关的指令,例如accept_mutex on;表示开启了互斥锁,multi_accept on;表示开启了多个并发连接的支持,use epoll;表示使用epoll事件模型。
  4. http { ... }:这个部分是Nginx服务器的HTTP模块配置。其中包含了一些全局的HTTP相关配置和指令。
    • include mime.types;:这个指令包含了一个外部文件mime.types,用于定义文件类型和对应的媒体类型。
    • default_type application/octet-stream;:这个指令设置了默认的媒体类型为application/octet-stream,即未知的二进制文件类型。
    • log_format ...:这里定义了几种日志格式,用于不同的日志记录。每个日志格式包含了一系列变量和对应的日志信息。
    • access_log /access.log main;:这个指令指定了访问日志文件的路径为/access.log,并使用名为"main"的日志格式记录访问日志。
    • sendfile on;:这个指令启用了使用sendfile系统调用来传输文件的功能。
    • keepalive_timeout:这个指令设置了HTTP keep-alive连接的超时时间。

这里,着重解释一下 server{ ... } 模块,主要实现的逻辑是根据请求的hots值和类型,将请求转发到不同的location块进行处理:静态资源的返回、代理请求的转发:

  1. location / { ... }:这个location块匹配任何URL路径,并且在请求匹配成功后执行定义的rewrite_by_lua指令。
  2. rewrite_by_lua ' ... ': 这个指令定义了一个Lua脚本,在Nginx服务器上执行。这个脚本通过解析请求的host值,提取出"类型-服务.abc.com"的模式,并将提取的值存储在ngx.var变量中。根据类型的不同,执行不同的操作:
    • 如果类型为"s",则执行ngx.exec("@static"),将请求转发到@static location块。
    • 如果类型为"g",则执行ngx.exec("@gradio"),将请求转发到@gradio location块。
  3. location @static { ... }:这个location块将处理被rewrite_by_lua指令转发到的静态请求。根据匹配到的服务,将其作为根目录下的子目录,并将请求的资源返回给客户端。
  4. location @gradio { ... }:这个location块将处理被rewrite_by_lua指令转发到的gradio请求。将请求转发到名为$service和端口号7860的服务。
swift 复制代码
apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    component: nginx
  name: nginx-domain-proxy-cm
  namespace: your_namespace
data:
  nginx.conf: |-
    worker_processes  4;
    error_log  /error.log;
    events {
        accept_mutex on;
        multi_accept on;
        use epoll;
        worker_connections  4096;
    }
    http {
        include       mime.types;
        default_type  application/octet-stream;
        log_format  main  '$time_local $remote_user $remote_addr $host $request_uri $request_method $http_cookie '
                          '$status $body_bytes_sent "$http_referer" '
                          '"$http_user_agent" "$http_x_forwarded_for" '
                          '$request_time $upstream_response_time "$upstream_cache_status"';

        log_format  browser '$time_iso8601 $cookie_km_uid $remote_addr $host $request_uri $request_method '
                          '$status $body_bytes_sent "$http_referer" '
                          '"$http_user_agent" "$http_x_forwarded_for" '
                          '$request_time $upstream_response_time "$upstream_cache_status" $http_x_requested_with $http_x_real_ip $upstream_addr $request_body';
        log_format client '{"@timestamp":"$time_iso8601",'
                          '"time_local":"$time_local",'
                          '"remote_user":"$remote_user",'
                          '"http_x_forwarded_for":"$http_x_forwarded_for",'
                          '"host":"$server_addr",'
                          '"remote_addr":"$remote_addr",'
                          '"http_x_real_ip":"$http_x_real_ip",'
                          '"body_bytes_sent":$body_bytes_sent,'
                          '"request_time":$request_time,'
                          '"status":$status,'
                          '"upstream_response_time":"$upstream_response_time",'
                          '"upstream_response_status":"$upstream_status",'
                          '"request":"$request",'
                          '"http_referer":"$http_referer",'
                          '"http_user_agent":"$http_user_agent"}';
        access_log  /access.log  main;
        sendfile        on;
        keepalive_timeout 120s 100s;
        keepalive_requests 500;
        send_timeout 60000s;
        client_header_buffer_size 4k;
        proxy_ignore_client_abort on;
        proxy_buffers 16 32k;
        proxy_buffer_size 64k;
        proxy_busy_buffers_size 64k;
        proxy_send_timeout 60000;
        proxy_read_timeout 60000;
        proxy_connect_timeout 60000;
        proxy_cache_valid 200 304 2h;
        proxy_cache_valid 500 404 2s;
        proxy_cache_key $host$request_uri$cookie_user;
        proxy_cache_methods GET HEAD POST;
        proxy_redirect off;
        proxy_http_version 1.1;
        proxy_set_header Host                $http_host;
        proxy_set_header X-Real-IP           $remote_addr;
        proxy_set_header X-Forwarded-For     $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto   $scheme;
        proxy_set_header X-Frame-Options     SAMEORIGIN;
        proxy_set_header Upgrade             $http_upgrade;
        proxy_set_header Connection          "upgrade";
        server_tokens off;
        client_max_body_size 50G;
        add_header X-Cache $upstream_cache_status;
        autoindex off;
        resolver      127.0.0.1:53 ipv6=off;
        server {
            listen 80;
            location / {
              set $service  '';
              set $type '';
              rewrite_by_lua '
                local host = ngx.var.host
                local m = ngx.re.match(host, "(s|g|p)-(.+).abc.com")
                ngx.var.type = m[1]
                ngx.var.service = m[2]
                if ngx.var.type == "s" then
                    ngx.exec("@static")
                elseif ngx.var.type == "g" then
                    ngx.exec("@gradio")
                elseif ngx.var.type == "p" then
                    ngx.exec("@proxy")
                end
              ';
            }
            location @static {
              root /mnt/data/$service;
            }
            location @gradio {
              proxy_pass http://$service:7860;
            }
        }
    }

2.3 | Service

Service 资源比较常规,是 nginx deployment 对应的负载均衡 (注意 label 不要写错)。

yaml 复制代码
apiVersion: v1
kind: Service
metadata:
  labels:
    component: nginx
  name: nginx-domain-proxy-svc
  namespace: your_namespace
spec:
  type: ClusterIP
  ports:
  - name: http
    port: 80
    targetPort: http
  selector:
    component: nginx

2.4 | Ingress

这里是 nginx 的动态二级域名对应的 Ingress 配置,这里是监听了泛域名*.abc.com, 并且配置了 TLS 证书

yaml 复制代码
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: nginx-domain-proxy-ing
  namespace: your_namespace
spec:
  rules:
  - host: '*.abc.com'
    http:
      paths:
      - backend:
          service:
            name: nginx-domain-proxy-svc
            port:
              number: 80
        path: /
        pathType: Prefix
  tls:
  - hosts:
    - '*.abc.com'
    secretName: generic.abc.com-https

以上。

相关推荐
dessler19 分钟前
Docker-run命令详细讲解
linux·运维·后端·docker
Q_19284999061 小时前
基于Spring Boot的九州美食城商户一体化系统
java·spring boot·后端
aherhuo1 小时前
kubevirt网络
linux·云原生·容器·kubernetes
ZSYP-S1 小时前
Day 15:Spring 框架基础
java·开发语言·数据结构·后端·spring
catoop2 小时前
K8s 无头服务(Headless Service)
云原生·容器·kubernetes
Yuan_o_2 小时前
Linux 基本使用和程序部署
java·linux·运维·服务器·数据库·后端
程序员一诺2 小时前
【Python使用】嘿马python高级进阶全体系教程第10篇:静态Web服务器-返回固定页面数据,1. 开发自己的静态Web服务器【附代码文档】
后端·python
liuxuzxx3 小时前
1.24.1-Istio安装
kubernetes·istio·service mesh
DT辰白3 小时前
如何解决基于 Redis 的网关鉴权导致的 RESTful API 拦截问题?
后端·微服务·架构