Caddyfile是Caddy的核心配置文件,它的设计,关乎着我们使用,开发者的解析以及扩展,所以本篇着重的介绍Caddy是如何设计一个Caddyfile的,我们也可以从中学到如何设计一个配置文件,并且让它更好的通用,更好的解析。

其实设计如此复杂的一个配置文件,已经和设计一门编程语言,很接近了。

结构

我前面的系统文章中,你也看到了如何使用Caddyfile的指令等功能,来满足我们的需求的。在我们写Caddyfile的时候,是遵循一定的规范的,哪些地方要怎么写,谁可以包含谁,这些规范就构成了Caddyfile的结构。

Caddyfile结构

这张图是了解Caddyfile的神器,它定了Caddy的规范以及结构,让我们可以很方面的使用Caddyfile。现在,我来介绍下里面的一些关键点:

看到最顶部的红色框圈出来的这部分了吗?这是一个全局配置,它在Caddyfile的最顶部,用于配置一些通用的全局信息。当然它并不是必须的,你也可以不用配置它。

第二部分的 snippet 是一个可以复用的片段,你可以在其他地方通过 import 来引入它,这非常适用于你的Caddyfile中有很多重复配置的情况。它和全局配置的差别在于 { 前面有一对小括号,用于定义可复用片段的名字,这样你才可以在其他地方通过这个名字引用。下面我通过一个例子来说明它的使用,如下所示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
(static_file){
  root * /var/www/mysite
  file_server 
}

www.example.com{
  import static_file
}
www.example.com{
  import static_file
}

接下来就是 Site Block 了,也就是定义你的站点的块,在Apache中叫虚拟主机。写到这里你可以看到,Caddyfile只有这三个顶级的定义块,一个全局配置、一个可复用的片段、一个就是站点配置,其他所有的配置,都要放在这三个顶级的配置中。

你可以通过站点块定义多个站点,但是他们之间没什么关系。如果你只有一个站点,你可以省略站点后面的大括号,比如下面两种定义是等价的:

1
2
3
4
localhost

reverse_proxy /api/* localhost:9001
file_server

等价于:

localhost {
	reverse_proxy /api/* localhost:9001
	file_server
}

因为整个Caddyfile中只有这么一个站点,所以大括号是可选的。

在一个Caddyfile中,你可以至少得定义一个站点,也可以定义多个,并且定义站点的时候,大括号前面的部分必须是 Site Address ,比如示例中的 localhost ,一个站点可以有一个站点地址,也可以有多个。

Block ,也就是大括号 {} 内的这部分。左大括号 { 必须在行的末尾,而右大括号 } 则必须自己单独占一行,这和我们Go语言编程很像,这样可以保持美观。

1
2
3
... {
	...
}

我们前面讲过,只有一个站点的时候,大括号是可选的,但是当有多个站点的时候,必须得用大括号把他们分开。

1
2
3
4
5
6
7
example1.com {
	root * /www/example.com
	file_server
}
example2.com {
	reverse_proxy localhost:9000
}

如果一个网络请求,匹配多个站点,那么Caddy只会选择地址最匹配的那个,不会同时匹配多个站点,这保障了站点匹配的唯一性,不会级联。

指令

指令只能属于某个站点,它是定义站点服务的关键字,位于一行中的第一个单词。比如我们示例中的 file_server 就是一个定义静态文件服务的指令。

指令也可以有子指令,子指令位于指令块中,用于进一步的配置,比如我们在反向代理文章中用到的负载均衡策略子指令。

1
2
3
4
localhost
reverse_proxy localhost:9000 localhost:9001 {
	lb_policy first
}

lb_policy 就是 reverse_proxy 的子指令, first 是子指令lb_policy 的参数。

Caddyfile解析

Caddyfile是一个普通的文本文件,只不过它具备一定的格式规范,所以它也要被解析成特定标记(Token)才能使用,这就和编译器的词法分析器一样。

在Caddyfile中,空格是非常重要的,因为Caddy使用它来分隔不同的标记。同样情况下,指令都需要一定的数量的参数,如果参数是有空格的,这可能会有问题,因为Caddy会把它们当成两个单独的标记进行词法分析,比如:

1
directive abc def

以上可能会返回异常,或者其他不可预知的行为。如果 abc def 是一个单独参数的话,最安全的做法就是使用引号,这样Caddy的词法分析器,就不会把他们当成两个标记(Token)。

1
directive "abc def"

这时候,你可能会有疑问,如果我参数中就需要引号怎么办呢?答案其实很简单,使用转义符号即可。

1
directive "\"abc def\""

不止是引号,其他空格、制表符、换行符等也可以使用转义,

这里还有一个办法,就是使用反引号:

1
directive `"foo bar"`

效果是等价的,反引号尤其是包含引号的文本中使用非常方便,比如JSON字符串等。

地址

地址就是站点块的顶部那部分,通常也是Caddyfile的第一个内容。Caddy基本上支持所有的地址样式,如下常用示例:

  1. localhost
  2. example.com
  3. :443
  4. http://example.com
  5. localhost:8080
  6. 127.0.0.1
  7. example.com/foo/*
  8. *.example.com
  9. 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

你也可以让多个站点地址共享同一个定义,只需要使用逗号分隔这些地址即可。

1
localhost:8080, example.com, www.example.com

最后,地址必须唯一,不能多次指定同一个地址。

请求匹配

一个客户端请求过来,Caddy是怎么处理的呢?比如用哪个指令来处理,这就需要设置匹配器了,通过匹配器,你可以精确的设置某个指令用于哪些请求。

如果不设置,默认情况下,该指令适用于所有请求。

指令后的第一个参数是匹配器,比如:

1
2
3
root *           /var/www  # matcher token: *
root /index.html /var/www  # matcher token: /index.html
root @post       /var/www  # matcher token: @post

* 表示匹配所有,这里的 @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 导入实现复用。

1
2
3
4
5
6
(redirect) {
	@http {
		protocol http
	}
	redir @http https://{host}{uri}
}

片段是顶级配置,片段定义的开头是片段的名称,使用小括号 () 括起来。定义好一个片段后,就可以通过 import 使用给它了。

1
import redirect

除了复用之外,片段的另一个强大之处在于可以传参给片段,实现动态化配置。

1
2
3
4
5
6
7
8
9
(snippet) {
  respond "Yahaha! You found {args.0}!"
}
a.example.com {
	import snippet "Example A"
}
b.example.com {
	import snippet "Example B"
}

使用非常简单,通过 {args.0} 可以获得传递过来的第一个参数。

环境变量

在Caddyfile,你也可以使用环境变量,这样可以让你的配置更灵活。

使用环境变量也非常简单,和占位符差不多,也是一个大括号包裹,但是多一个 $ 符号。

1
{$SITE_ADDRESS}

这种形式的环境变量在解析开始之前被替换,因此它们可以扩展为空值、部分标记、完整标记,甚至多个标记和行。和C语言的 define 一样,是不是很强大。 在具体的代码实现上,Caddy是使用Go语言的 os.LookupEnv 方法获取环境变量的。

那么,如果忘记配置环境变量怎么办呢?别急,这点Caddy肯定考虑到了,我们在使用环境变量的时候,可以设置一个默认值,如果找不到环境变量的时候,会使用这个默认值。

1
{$DOMAIN:localhost}

Caddyfile是使用 : 来分隔环境变量名称和默认值的,以上示例中,默认值是 localhost

注释

Caddyfile是支持注释的,这样我们就可以增加点注释,便于多人协作和理解。在Caddyfile中,注释以 # 开头,直到行的末尾。

1
2
# Comments can start a line
directive  # or go at the end

小结

这篇主要介绍了Caddyfile的规范以及设计,这也是一个比较完整的配置文件的设计,看了这篇相信你不光可以很好的理解Caddyfile并使用它,也可以很好的理解Nginx的conf配置,因为都差不多。

一个配置文件的设计,牵涉到规范、可以扩展性、模块化、可复用、变量,还得需要加载、替换、词法分析等,这俨然已经是在定义一门脚本言语了,所以如果你有编译器的功底,也能更好的理解Caddyfile的设计和解析。

本文为原创文章,转载注明出处,欢迎扫码关注公众号flysnow_org或者网站 https://www.flysnow.org/ ,第一时间看后续精彩文章。觉得好的话,请猛击文章右下角「在看」,感谢支持。

扫码关注