通过域名前缀就能实现随意切环境

890 阅读3分钟

背景

在现代应用开发中,随着项目的复杂性和规模的增加,往往需要在多个环境中进行开发、测试和部署,如开发环境、测试环境、预生产环境和生产环境等。如何在这些环境之间动态切换,通常我们使用 SwitchHosts 来实现环境切换,虽然它是立即生效的,但是对于浏览器来说还是有 DNS 缓存的

目标

公司服务端已经支持通过添加请求头 x1-mirror 打到基座环境,基座环境再转发到对应的机器上。

染色示意图.png

基于上面的能力,前端如果需要切环境,还是要用到两个工具 SwitchHosts + ModHeader。

为了在同一台服务器上灵活处理不同环境的域名解析,我们需要一个能够动态映射域名到指定 IP 的方案。如下:

将内网的 test-*.abc.om DNS解析到一台固定服务作为代理基座服务

  • 代理基座服务匹配到 test-01-*.abc.om 代理到 172.1.1.1
  • 代理基座服务匹配到 test-02-*.abc.om 代理到 172.1.1.2
  • 代理基座服务匹配到 test-03-*.abc.om 代理到 172.1.1.3

实现(准备环境)

  • 安装 Nginx 和 Lua 模块: 首先需要安装支持 Lua 模块的 Nginx,例如 OpenResty,这是一个集成了 LuaJIT 的高性能 Web 平台。
IMAGE_NAME="openresty"
CONTAINER_NAME="my-nginx"
docker build -t $IMAGE_NAME .

if docker ps -a | grep $CONTAINER_NAME > /dev/null 2>&1; then
    docker container stop $CONTAINER_NAME
    docker container rm $CONTAINER_NAME
else
    echo "No such container: $CONTAINER_NAME"
fi

docker container run --name $CONTAINER_NAME \
-p 80:80 \
-p 443:443 \
-v $(pwd)/log:/data/log/nginx \
-v $(pwd)/openresty/nginx/certs:/etc/nginx/certs \
-v $(pwd)/openresty/nginx/lua:/etc/nginx/lua \
-v $(pwd)/openresty/nginx/config:/etc/nginx/config \
-d $IMAGE_NAME
  • 定义 IP 映射表: 这里演示使用 json 文件配置,实际使用推荐放到 Redis, Lua 搭配 Redis 也很丝滑
mirrorConfig.json
{
    "01": "172.1.1.0",
    "02": "172.1.1.2",
    "03": "172.1.1.3",
    "prod": "10.x.x.x"
}

配置 Nginx 反向代理

  1. 配置 Nginx 入口文件
lua_shared_dict mirrorConfig 32m;
init_by_lua_file /etc/nginx/lua/init.lua;

  1. init.lua 用于启动时加载配置到 nginx 的共享内存中
local dkjson = require "dkjson"

-- 读取 JSON 文件内容
local file = io.open("/etc/nginx/config/mirrorConfig.json", "r")
local content = file:read("*all")
file:close()

-- 解析JSON字符串并将其转换为Lua表(table)
local data = dkjson.decode(content)

-- 将数据存储到共享内存中
for key, value in pairs(data) do
    ngx.shared.mirrorConfig:set(key, value, 0)
end

  1. 实现一个通用获取IP地址的方法

-- 根据域名前缀获取相应的IP地址
local function map_prefix_to_ip(domain)
    local prefix = string.match(domain, "^test-(%w+)%-")
    return ngx.shared.mirrorConfig:get(prefix)
end
  1. Nginx 配置文件: 编辑 Nginx 的配置文件(例如 nginx.conf),添加以下配置以实现动态代理功能:
location / {
    # 用于匹配域名前缀,例如 test-01-abc.om、test-02-abc.om 等,正则方法用$取值
    server_name test-(pre|prod|[a-z0-9]+)-([a-z0-9-.]+.abc.com)$;

    listen 80;
    listen 443 ssl;
    ssl_certificate /etc/nginx/certs/server.pem;
    ssl_certificate_key /etc/nginx/certs/server.key;
    ssl_session_timeout 30m;
    ssl_protocols SSLv2 SSLv3 TLSv1 TLSv1.2;
    # 原始host,$2表示第二个括号内的值
    set $origin_host $2;
    # 使用 Lua 代码块动态解析域名前缀并获取对应的 IP 地址
    set_by_lua $target_ip '
        local domain = ngx.var.host
        return map_prefix_to_ip(domain)
    ';
    
    # 配置代理到目标 IP 地址
    proxy_pass http://$target_ip$request_uri;
    
    # 其他代理配置
    proxy_set_header Host $origin_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;
}
  1. 重载 Nginx 配置: 在配置完成并保存后,需要重载 Nginx 以应用新配置:
docker exec -it my-nginx nginx -s reload

测试和验证

测试 DNS 配置: 确保 test01-.abc.om、test02-.abc.om 等域名正确解析到 Nginx 服务器的 IP 地址。我们需要将所有域名的 host 添加到 SwitchHosts 并指向 127.0.0.1 (本地/或者实际部署的服务IP)。

test01-www.abc.om 127.0.0.1
test02-www.abc.om 127.0.0.1
test03-www.abc.om 127.0.0.1

测试代理功能: 使用 curl 验证请求是否正确地被代理到目标服务器。例如:

curl -H "Host: test01-abc.om" http://<nginx-server-ip> # 测试代理到 test01-abc.om  172.1.1.1
curl -H "Host: test02-abc.om" http://<nginx-server-ip> # 测试代理到 test02-abc.om  172.1.1.2

相比之前,只需一个 host 文件就可以切多个环境

引出新问题

  • 如果前端使用的是非相对路径。页面内部的请求还是正式的, www.abc.om/api/xxx 会被代理到 线上环境。 解决方案: 使用替换,将页面中的域名替换为代理的域名,保证页面与请求的环境一致。
    location / {
      ... 省略 ...
      sub_filter *.abc.om test-$1-abc.om;
    }
    

优化和扩展

  1. 动态配置更新: 可以使用 Redis 代替 JSON 文件来存储 IP 映射,并在 Lua 代码中实现动态加载和更新配置,从而无需重载 Nginx 配置即可更改代理规则。

  2. 泛解析: test-.abc.om 可以使用泛解析规则,在内网的 DNS 服务器中配置泛解析规则,将 test-.abc.om 映射到 Nginx 服务器的 IP 地址。这样,只需要一个域名即可实现多个环境的代理。本地也无需配置 host 文件。(当然这个需要找运维部门 PY)

  3. 前后端环境单独切换: 可以通过 sub_filter 向页面(head)注入 JS 代码,在页面加载时根据环境变量判断是否需要替换域名。

     sub_filter <head> '<head><script ignore src="oss.abc.com/xxx/xxx/sdk.js"></script>'
    

    sdk.js

    // 实现一个切环境的UI(浮窗):省略
    
    // 重写fetch请求
    window.fetch = function fetchHook(input: RequestInfo, init?: RequestInit) {
     // 拦截逻辑
    }
    // 拦截xhr or 重写
     Object.defineProperty(XMLHttpRequest.prototype, 'open', {
         configurable: true,
         enumerable: true,
         writable: true,
         value(
             method: string,
             url: string,
             async: boolean,
             username?: string | null,
             password?: string | null
         ) {
             // 拦截逻辑
         }
     }
    

    上面 xhr 为何推荐使用原型链的方式拦截,因为这种方式可以兼容项目中使用了其他类 Proxy 拦截库,比如 * ajax-hook *

总结

域名对应环境,研发给产品验收时,直接甩链接就可以了。不用教他们怎么切环境。而且支持在同环境二次隔离。比如上的例子就是测试环境中的 01,02,03。但么 pre,prod 也需要这样的灰度能力,便于更好的还原正式环境。便于理解,也可以将01,02,03 换成任意英文字符对使用者有更好的体验。