从 Nginx 切换到 Caddy
这几天把公司测试环境 Nginx 切换到了 Caddy,在实际切换过程中还是有一点小问题,但是目前感觉良好,这里记录一些细节。
一、为什么要切换
大部分情况我们的生产环境使用一个域名,为了保证隔离性我们会在测试环境采用另一个域名(偷偷透露一下,测试环境买 *.link
域名,国内能备案还贼便宜);然而我们不太舍得掏钱去给测试域名再买个证书,所以一直 ACME 大法。
众所周知这个玩意的证书 3 个月需要续签一次,脚本式续签然后 nginx reload 有时候还不太靠谱,总之内部环境复杂下脚本式操作还是有点风险,所以最后决定 Caddy 一把梭一劳永逸了。
二、切换中涉及到的细节
2.1、规则匹配
在某个站点中我们采用了 Nginx 判断 User-Agent 来处理访问到底是移动端还是桌面端,说实话我比较讨厌这种骚这种东西:
1 |
|
一开始通过查找 Caddy 文档发现 Caddy 也是支持 map 的:
1 |
|
在实际配置时发现其实这个问题只需要用自定义规则匹配器判断一下是不是移动端即可:
1 |
|
在后续编写匹配规则时发现 Caddy 的匹配规则确实是非常强大,在官方的 Request Matchers 文档页面上可以找到基本上满足所有需求的匹配器,从请求头到请求方法、协议、请求路径,从标准匹配到通配符、正则匹配基本上样样俱全,甚至支持代码式的 CEL (Common Expression Language) 表达式匹配;多个匹配还可以自定义命名作为业务相关的匹配器使用。
2.2、规则重写
在 Nginx 中 rewrite 指令是多种行为的,比如可以进行 URL 隐式改写,也可以返回 301、307 等重定向代码;但是在 Caddy 中这两种行为被划分为两个指令:
- rewrite: 内部重写,对 URL、参数等进行内部替换,浏览器地址将保持不变
- redir: 重定向,返回 HTTP 状态码让客户端自行重定向到新页面
2.2.1、rewrite
针对于地址的隐式重写 rewrite 指令其语法规则如下:
1 |
|
匹配器就是全局标准的匹配器定义,可以使用内置的,也可以组合内置匹配器为自定义匹配器,这个匹配器比 Nginx 强大太多;to
中分为三种情况:
- 只替换 PATH:
rewrite /abc /bcd
:
这种情况下,rewrite 根据 “匹配器” 确定匹配路径,然后完全替换为最后一个路径;最后面的路径可以使用 {path}
占位符引用原始路径。
- 只替换 请求参数:
rewrite /api ?a=b
:
这种情况下,Caddy 以 ?
作为分隔符,如果 ?
后面有内容就意味着将请求参数替换为后面的请求参数;最后面的请求参数可以通过 {query}
引用原始请求参数。
- 全部替换:
rewrite /abc /bcd?{query}&custom=1
:
这种情况下,Caddy 根据 “匹配器” 匹配会即替换请求路径也替换请求参数,当然两个占位符也都是可用的。
需要注意的是: rewrite
只做重写,不会中断请求链,这意味着最终返回结果根据后续的请求匹配来决定。
2.2.2、redir
redir 用于向客户端声明显式的重定向,即返回特定重定向状态码,其语法如下:
1 |
|
匹配器就不说了,全都一样;**<to>
这个参数会作为 Location
头部值返回,其中可以使用占位符引用原始变量:**
1 |
|
code
部分分为四种情况:
- 一个
3xx
的自定义状态码 temporary
: 返回 302 临时重定向permanent
: 返回 301 永久重定向html
: 使用 HTML 文档方式重定向
例如将所有请求永久重定向到新站点:
1 |
|
这里面 HTML 方式是比较难理解的,这起源于一个规范,具体如下:
HTTP 协议中重定向机制是应该优先采用的创建重定向映射的方式,但是有时候 Web 开发者对于服务器没有控制权,或者无法对其进行配置。针对这些特定的应用情景,Web 开发者可以在精心制作的 HTML 页面的
部分添加一个元素,并将其 http-equiv 属性的值设置为 refresh 。当显示页面的时候,浏览器会检测该元素,然后跳转到指定的页面。
在源码中如果使用了 html
重定向方式,Caddy 会返回一个 HTML 页面以满足上述方式的情况下让浏览器自行刷新:
1 |
|
2.2.3、uri
uri
指令是一个特殊指令,它与 rewrite
类似,不同的是它用于对 URI 重写更加方便,其语法如下:
1 |
|
语法中第二个参数为一个动词,用来定义如何替换 URI:
strip_prefix
: 从路径中去除前缀strip_suffix
: 从路径中去除后缀replace
: 在整个 URI 路径中执行子替换(例如/a/b/c/d
替换为/a/1/2/d
)path_regexp
: 在路径中进行正则替换
以下为一些样例:
1 |
|
其中在使用 replace
时最后面可以跟一个数字,代表从 URI 中找找替换多少次,默认为 -1
即全部替换。
2.3、WebSocket 代理
在 Nginx 配置中,如果想要代理 WebSocket 链接,我们需要增加以下设置:
1 |
|
但是在 Caddy 中一切变得更加简单… 简单到就是我们啥也不用干,自动支持:
Websocket proxying “just works” in v2; there is no need to “enable” websockets like in v1.
2.4、URL 编码
在使用路径匹配器时,URL 默认是被解码的,例如:
1 |
|
至于反向代理 reverse_proxy
传出时的编码暂时还没有遇到,还需要测试一下。
2.5、强制 HTTP
有些站点可能默认就是 HTTP 的,我们也不期望以 HTTPS 方式访问;但是 Caddy 默认会为站点进行 ACME 证书申请,而申请不下来证书时又访问不了;这种情况下只需要在站点地址上强制写明 HTTP 协议即可:
1 |
|
2.6、代理 HTTPS
如果想要代理 HTTPS 服务,那么只需要在 reverse_proxy
中填写 HTTPS 地址即可;不过与 Nginx 不同,Caddy 的 TLS 校验默认是开着的,所以如果后端 HTTPS 证书过期等情况可能导致 Caddy 返回 502 错误; 这种情况可以通过 transport
进行关闭:
1 |
|
2.7、自定义证书
如果已经有自己的证书,而不期望 Caddy 自动申请,那么只需要在 tls
指令后加上证书即可:
1 |
|
2.8、日志打印
Caddy 的日志系统与 Nginx 完全不同,Caddy 日志按照 Namespace 划分,在站点配置中默认为只可以打印当前站点的请求日志,如果需要打印例如反向代理的上游地址等需要在全局日志配置中配置。 日志这一块一句两句说不清,推荐直接看官方文档以及日志实现逻辑,如果懂 go 的话可以看看 uber-go/zap 这个日志框架;下面是按文件分开打印请求日志和上游日志的样例:
1 |
|
2.9、TLS 版本不支持
很不幸的是我们有一个 TLS 1.1 兼容的服务,当切换到 Caddy 后 TLS 1.1 已经不被支持,目前 Caddy 的 TLS 兼容性最小为 TLS 1.2,最大为 TLS 1.3:
protocols <min> [<max>]
protocols
specifies the minimum and maximum protocol versions. Default min:tls1.2
. Default max:tls1.3
.
三、切换总结
总结一句话: 匹配器舒服,配置行为明确,配置引用少写一万行,其他的坑继续踩。