765DevOps

Thinking is the problem, Doing is the answer !

0%

禅道持续交付(三)-多环境同域名设计

在实际需求中,我们遇到了这样一个需求:在研发环境测试环境使用相同的域名,期望根据访问者IP将访问请求转发到对应环境,并能对IP环境的绑定关系进行管理。基于此我们调研最后实现这个需求。方案核心点:OpenResty方案: Nginx+lua + redis 实现根据访问者IP将访问请求转发到对应环境,并能对IP环境的绑定关系进行管理,redis用于管理用户Client IP和环境的绑定关系。

1 需求调研

研发环境测试环境使用相同的域名,期望根据访问者IP将访问请求转发到对应环境,并能对IP环境的绑定关系进行管理。

2 方案概述

  1. 使用OpenResty(集成了 Nginx 和 Lua )对用户请求进行转发
  2. 使用Redis来存储用户 IP 与环境的绑定关系数据
  3. 使用禅道操作Redis来管理用户 IP 与环境的绑定关系

3 部署实践

3.1 OpenResty 部署

OpenResty官方介绍

3.1.1 OpenResty 安装

1
2
3
4
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
sudo yum install -y openresty
sudo yum install -y openresty-resty

3.1.2 OpenResty 配置

3.1.2.1 nginx 配置

路径:/usr/local/openresty/nginx/conf/nginx.conf

  • 配置文件路径根据 openresty 安装目录变化
  • 配置文件中lua_package_path字段值也要根据 openresty 安装目录变化

配置样例:

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
user                 root root;
worker_processes auto;
worker_cpu_affinity auto;
error_log logs/error.log info;
pid logs/nginx.pid;
worker_rlimit_nofile 65000;

events {
use epoll;
accept_mutex off;
multi_accept on;
worker_connections 20480;
}

http {
include mime.types;
default_type application/octet-stream;

# lua相关配置
lua_package_path '/usr/local/openresty/lualib/?.lua;/usr/local/openresty/nginx/conf/?.lua';
lua_shared_dict xq_req_limit_cache 1m;
lua_need_request_body on;

include conf.d/*.conf;
}
3.1.2.2 代理域名配置

路径:/usr/local/openresty/nginx/conf/conf.d/domain.conf

  • 配置文件路径根据 openresty 安装目录变化
  • 配置文件中access_log、error_log、rewrite_by_lua_file字段值也要根据 openresty 安装目录变化

配置样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
server {
listen 80;
server_name domain.demo.com;

access_log /usr/local/openresty/nginx/logs/domain.demo.com.access.log;
error_log /usr/local/openresty/nginx/logs/domain.demo.com.error.log warn;

location / {
set $target_domain '';
set $target_host '';

rewrite_by_lua_file /usr/local/openresty/nginx/conf/proxy.lua;

proxy_set_header Host $target_domain;
proxy_pass http://$target_host;
}
}
3.1.2.3 Lua脚本

路径:/usr/local/openresty/nginx/conf/proxy.lua

  • 配置文件路径根据 openresty 安装目录变化
  • 配置文件中file_err字段值也要根据 openresty 安装目录变化

配置样例:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
-- 日志文件
local file_err = "/usr/local/openresty/nginx/logs/"..os.date("%Y-%m-%d", os.time()).."-proxy-lua.log"

-- json模块
local cjson = require "cjson"

-- 写日志函数
local function log(type, msg)
type = type or "INFO"
files = assert(io.open(file_err, "a+"))
files:write(os.date("%Y-%m-%d %X", os.time()).." "..type.." "..cjson.encode(msg).."\n")
files:close()
end

-- 获取客户端IP
local client_ip = ngx.req.get_headers()["X-Real-IP"]
if client_ip == nil then
client_ip = ngx.req.get_headers()["x_forwarded_for"]
end

if client_ip == nil then
client_ip = ngx.var.remote_addr
end

-- redis模块
local redis = require "resty.redis"
local red = redis.new()

-- 设置超时
red:set_timeouts(1000, 1000, 1000)

-- 连接
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
log("ERROR", "failed to connect: "..err)
ngx.say("ERROR", "failed to connect: "..err)
return
end

-- 令牌
local ok, err = red:auth("rc_redis")
if not ok then
log("ERROR", "failed to authenticate: "..err)
ngx.say("ERROR", "failed to authenticate: "..err)
red:close()
return
end

-- 选择redis db
red:select(1)

-- 获取要转发的域名
local redis_key_domain = "domain:"..ngx.var.server_name..":clientip:"..client_ip..":domain"
local domain, err = red:get(redis_key_domain)
if not domain then
log("ERROR", "failed to get "..redis_key_domain.." :", err)
ngx.say("ERROR", "failed to get "..redis_key_domain.." :", err)
red:close()
return
end

if domain == ngx.null then
log("ERROR", redis_key_domain.." not found.")
ngx.say("ERROR", redis_key_domain.." not found.")
red:close()
return
end

log("DEBUG", redis_key_domain.." => "..domain)

-- 获取要转发的host
local redis_key_host = "domain:"..ngx.var.server_name..":clientip:"..client_ip..":host"
local host, err = red:get(redis_key_host)
if not host then
log("ERROR", "failed to get "..redis_key_host.." :", err)
ngx.say("ERROR", "failed to get "..redis_key_host.." :", err)
red:close()
return
end

if host == ngx.null then
log("ERROR", redis_key_host.." not found.")
ngx.say("ERROR", redis_key_host.." not found.")
red:close()
return
end

log("DEBUG", redis_key_host.." => "..host)

ngx.var.target_domain = domain
ngx.var.target_host = host
red:close()

注意事项:

  • redis connect
  • redis auth
  • redis db

3.2 Redis 数据结构

键备注 值备注
domain:<request_domain>:clientip:<client_ip>:domain 字符串类型
<request_domain>为客户端请求的域名
<client_ip>为客户端IP
示例:domain:demo.com:clientip:172.17.18.237:domain
<target_domain> 字符串类型
<target_domain>为绑定的目标域名
示例:st2-demo.com
domain:<request_domain>:clientip:<client_ip>:host 字符串类型
<request_domain>为客户端请求的域名
<client_ip>为客户端IP
示例:domain:demo:clientip:172.17.18.237:host
<target_host>:<target_port> 字符串类型
<target_host>为绑定的目标服务器IP
<target_port>为绑定的目标服务器端口
示例:172.16.0.221:80

3.3 禅道操作

3.3.1 新增代理域名

  1. 新增一份该域名的代理域名配置到对应目录
    • 禅道生成文件到指定目录下
    • 通过rsync同步改目录到OpenResty的对应目录
  2. 重现加载 OpenResty Nginx模块
    • openresty/nginx/sbin/nginx -s reload
    • 在Redis中设置一个键值,作为重新加载的标志
    • OpenResty部署服务器设置一个定时任务,检测Redis中的标志,决定是否重新加载启动Nginx

3.3.2 修改用户绑定

  1. 按照 Redis 数据结构,修改上述2个键对应的值(新增、更新、删除)