动手撸个Caddy(十一)| Caddyfile 设计之美
Caddyfile是Caddy的核心配置文件,它的设计,关乎着我们使用,开发者的解析以及扩展,所以本篇着重的介绍Caddy是如何设计一个Caddyfile的,我们也可以从中学到如何设计一个配置文件,并且让它更好的通用,更好的解析。
其实设计如此复杂的一个配置文件,已经和设计一门编程语言,很接近了。
结构
我前面的系统文章中,你也看到了如何使用Caddyfile的指令等功能,来满足我们的需求的。在我们写Caddyfile的时候,是遵循一定的规范的,哪些地方要怎么写,谁可以包含谁,这些规范就构成了Caddyfile的结构。
这张图是了解Caddyfile的神器,它定了Caddy的规范以及结构,让我们可以很方面的使用Caddyfile。现在,我来介绍下里面的一些关键点:
看到最顶部的红色框圈出来的这部分了吗?这是一个全局配置,它在Caddyfile的最顶部,用于配置一些通用的全局信息。当然它并不是必须的,你也可以不用配置它。
第二部分的 snippet
是一个可以复用的片段,你可以在其他地方通过 import
来引入它,这非常适用于你的Caddyfile中有很多重复配置的情况。它和全局配置的差别在于 {
前面有一对小括号,用于定义可复用片段的名字,这样你才可以在其他地方通过这个名字引用。下面我通过一个例子来说明它的使用,如下所示:
|
|
接下来就是 Site Block
了,也就是定义你的站点的块,在Apache中叫虚拟主机。写到这里你可以看到,Caddyfile只有这三个顶级的定义块,一个全局配置、一个可复用的片段、一个就是站点配置,其他所有的配置,都要放在这三个顶级的配置中。
你可以通过站点块定义多个站点,但是他们之间没什么关系。如果你只有一个站点,你可以省略站点后面的大括号,比如下面两种定义是等价的:
|
|
等价于:
localhost {
reverse_proxy /api/* localhost:9001
file_server
}
因为整个Caddyfile中只有这么一个站点,所以大括号是可选的。
在一个Caddyfile中,你可以至少得定义一个站点,也可以定义多个,并且定义站点的时候,大括号前面的部分必须是 Site Address
,比如示例中的 localhost
,一个站点可以有一个站点地址,也可以有多个。
块
Block
,也就是大括号 {}
内的这部分。左大括号 {
必须在行的末尾,而右大括号 }
则必须自己单独占一行,这和我们Go语言编程很像,这样可以保持美观。
|
|
我们前面讲过,只有一个站点的时候,大括号是可选的,但是当有多个站点的时候,必须得用大括号把他们分开。
|
|
如果一个网络请求,匹配多个站点,那么Caddy只会选择地址最匹配的那个,不会同时匹配多个站点,这保障了站点匹配的唯一性,不会级联。
指令
指令只能属于某个站点,它是定义站点服务的关键字,位于一行中的第一个单词。比如我们示例中的 file_server
就是一个定义静态文件服务的指令。
指令也可以有子指令,子指令位于指令块中,用于进一步的配置,比如我们在反向代理文章中用到的负载均衡策略子指令。
|
|
lb_policy
就是 reverse_proxy
的子指令, first
是子指令lb_policy
的参数。
Caddyfile解析
Caddyfile是一个普通的文本文件,只不过它具备一定的格式规范,所以它也要被解析成特定标记(Token)才能使用,这就和编译器的词法分析器一样。
在Caddyfile中,空格是非常重要的,因为Caddy使用它来分隔不同的标记。同样情况下,指令都需要一定的数量的参数,如果参数是有空格的,这可能会有问题,因为Caddy会把它们当成两个单独的标记进行词法分析,比如:
|
|
以上可能会返回异常,或者其他不可预知的行为。如果 abc def
是一个单独参数的话,最安全的做法就是使用引号,这样Caddy的词法分析器,就不会把他们当成两个标记(Token)。
|
|
这时候,你可能会有疑问,如果我参数中就需要引号怎么办呢?答案其实很简单,使用转义符号即可。
|
|
不止是引号,其他空格、制表符、换行符等也可以使用转义,
这里还有一个办法,就是使用反引号:
|
|
效果是等价的,反引号尤其是包含引号的文本中使用非常方便,比如JSON字符串等。
地址
地址就是站点块的顶部那部分,通常也是Caddyfile的第一个内容。Caddy基本上支持所有的地址样式,如下常用示例:
- localhost
- example.com
- :443
- http://example.com
- localhost:8080
- 127.0.0.1
- example.com/foo/*
- *.example.com
- http://
根据地址,Caddy可以推断出站点的Scheme、Host、Port和Path。
如果指定主机名,则只接受具有匹配主机头的请求。换句话说,如果站点地址是 localhost
,那么Caddy将不会将请求与 127.0.0.1
匹配,因为 127.0.0.1
的请求主机头不是localhost
,没法匹配。
Caddy允许在地址中使用通配符(*),但是它也有严格的限制:它只用来匹配主机名。比如 *.example.com
可以匹配 foo.example.com
,但不匹配 foo.bar.example.com
。
你也可以让多个站点地址共享同一个定义,只需要使用逗号分隔这些地址即可。
|
|
最后,地址必须唯一,不能多次指定同一个地址。
请求匹配
一个客户端请求过来,Caddy是怎么处理的呢?比如用哪个指令来处理,这就需要设置匹配器了,通过匹配器,你可以精确的设置某个指令用于哪些请求。
如果不设置,默认情况下,该指令适用于所有请求。
指令后的第一个参数是匹配器,比如:
|
|
*
表示匹配所有,这里的 @post
是一个定义的匹配器,可以被引用、复用。匹配器的定义,详见我们结构那部分的截图。
以上示例其实代表了三种匹配器:通配符匹配器、路径匹配器和命名匹配器,更多的关于请求匹配器的说明可以详见 https://caddyserver.com/docs/caddyfile/matchers ,这里不再赘述。
占位符
使用Nginx的时候,我们会看到有 $
开头的变量,它就是占位符,是一种将动态值注入静态配置的方法,通过它可以让我们更灵活的配置Nginx。同样的,Caddy也有占位符的功能,便于我们配置Caddyfile。
在Caddyfile中,占位符的两边用大括号{}限定,并在其中包含变量名,例如: {foo.bar}
。占位符大括号可以转义, \{like so\}
。变量名通常用点命名,以避免模块之间的冲突。
你可以在Caddyfile中使用任何Caddy占位符,但为了方便起见,您也可以使用一些等效的速记占位符:
速记占位符 | 用于取代的占位符(等价) |
---|---|
{dir} | {http.request.uri.path.dir} |
{file} | {http.request.uri.path.file} |
{header.*} | {http.request.header.*} |
{host} | {http.request.host} |
{labels.*} | {http.request.host.labels.*} |
{hostport} | {http.request.hostport} |
{port} | {http.request.port} |
{method} | {http.request.method} |
{path} | {http.request.uri.path} |
{path.*} | {http.request.uri.path.*} |
{query} | {http.request.uri.query} |
{query.*} | {http.request.uri.query.*} |
{re..} | {http.regexp..} |
{remote} | {http.request.remote} |
{remote_host} | {http.request.remote.host} |
{remote_port} | {http.request.remote.port} |
{scheme} | {http.request.scheme} |
{uri} | {http.request.uri} |
{tls_cipher} | {http.request.tls.cipher_suite} |
{tls_version} | {http.request.tls.version} |
{tls_client_fingerprint} | {http.request.tls.client.fingerprint} |
{tls_client_issuer} | {http.request.tls.client.issuer} |
{tls_client_serial} | {http.request.tls.client.serial} |
{tls_client_subject} | {http.request.tls.client.subject} |
{tls_client_certificate_pem} | {http.request.tls.client.certificate_pem} |
{upstream_hostport} | {http.reverse_proxy.upstream.hostport} |
并非所有占位符在配置的所有部分都可用,哪些占位符可用取决于上下文。例如,HTTP应用程序设置的占位符仅在与处理HTTP请求相关的配置区域中可用。
片段
在结构部分我简单的介绍过片段,它是一个可以复用的配置,使用 import
导入实现复用。
|
|
片段是顶级配置,片段定义的开头是片段的名称,使用小括号 ()
括起来。定义好一个片段后,就可以通过 import
使用给它了。
|
|
除了复用之外,片段的另一个强大之处在于可以传参给片段,实现动态化配置。
|
|
使用非常简单,通过 {args.0}
可以获得传递过来的第一个参数。
环境变量
在Caddyfile,你也可以使用环境变量,这样可以让你的配置更灵活。
使用环境变量也非常简单,和占位符差不多,也是一个大括号包裹,但是多一个 $
符号。
|
|
这种形式的环境变量在解析开始之前被替换,因此它们可以扩展为空值、部分标记、完整标记,甚至多个标记和行。和C语言的 define
一样,是不是很强大。
在具体的代码实现上,Caddy是使用Go语言的 os.LookupEnv
方法获取环境变量的。
那么,如果忘记配置环境变量怎么办呢?别急,这点Caddy肯定考虑到了,我们在使用环境变量的时候,可以设置一个默认值,如果找不到环境变量的时候,会使用这个默认值。
|
|
Caddyfile是使用 :
来分隔环境变量名称和默认值的,以上示例中,默认值是 localhost
。
注释
Caddyfile是支持注释的,这样我们就可以增加点注释,便于多人协作和理解。在Caddyfile中,注释以 #
开头,直到行的末尾。
|
|
小结
这篇主要介绍了Caddyfile的规范以及设计,这也是一个比较完整的配置文件的设计,看了这篇相信你不光可以很好的理解Caddyfile并使用它,也可以很好的理解Nginx的conf配置,因为都差不多。
一个配置文件的设计,牵涉到规范、可以扩展性、模块化、可复用、变量,还得需要加载、替换、词法分析等,这俨然已经是在定义一门脚本言语了,所以如果你有编译器的功底,也能更好的理解Caddyfile的设计和解析。
本文为原创文章,转载注明出处,欢迎扫码关注公众号
flysnow_org
或者网站 https://www.flysnow.org/ ,第一时间看后续精彩文章。觉得好的话,请猛击文章右下角「在看」,感谢支持。