Traefik Plugins 全面解析

介绍

前置知识:Traefik 使用指北

Traefik v2.3 及以上版本允许开发人员使用 Plugins 插件向 Traefik 添加新功能或定义新行为。例如,可以修改请求或标头、重定向、添加身份验证等,提供与 Traefik 中间件类似的功能。

不过,和传统中间件不同,插件是动态加载的,并由嵌入式解释器执行,无需编译二进制文件,所有插件都是 100% 跨平台的,这使得它们易于开发和共享(通过 Traefik Pilot)。

Traefik Pilot

Traefik Pilot 是一个 Traefik 的监控和管理平台,可以集中管理在任何环境中运行的所有 Traefik 实例。它通过统一的仪表板提供对 Traefik 实例的观察性和控制,可提供详细的网络指标、服务器监控和安全通知。

Traefik Pilot 还为自定义中间件插件托管了一个公共插件中心(public plugins hub),支持流量整形、流量 QoS、流量速率限制等。

在使用 Plugins 之前,需要在 Traefik Pilot 平台(https://pilot.traefik.io/)上注册一个账号,这里我直接使用 Github 授权登陆:

登陆成功后,我们需要注册一个 Traefik Instance ,点击 Register New Traefik Instance 按钮会生成一个 token ,复制 token :

勾选 I have restarted my Traefik instance 保存该 Traefik Instance :

此时,显示还未绑定我们的 Trarfik 实例。

绑定 Traefik Instance

创建 traefik 配置 traefik-config.yaml ,并填入上面得到的 token :

  • pilot:
  • enabled: true
  • token: "898bb869-77ad-4594-b68f-1f87e0aa2e9b"
  • dashboard: true
  • ports:
  • traefik:
  • expose: true
  • web:
  • nodePort: 80
  • websecure:
  • nodePort: 443

使用 helm 安装 :

  • helm upgrade --install traefik traefik/traefik -n traefik -f traefik-config.yaml

访问面板,可以看到,Traefik Instance 已经绑定了我们的 traefik 实例:

可以通过 Metrics 观察指标:

Traefik Plugins 使用

以 plugindemo 插件为例,为请求头部添加一个 whoami-header: hello world

修改 traefik 配置 traefik-config.yaml ,开启并安装 plugindemo@v0.2.1 插件:

  • pilot:
  • enabled: true
  • token: "898bb869-77ad-4594-b68f-1f87e0aa2e9b"
  • dashboard: true
  • additionalArguments:
  • - "--experimental.plugins.plugindemo.modulename=github.com/traefik/plugindemo"
  • - "--experimental.plugins.plugindemo.version=v0.2.1"
  • experimental:
  • plugins:
  • enabled: true
  • ports:
  • traefik:
  • expose: true
  • web:
  • nodePort: 80
  • websecure:
  • nodePort: 443

使用 helm 更新重启 traefik :

  • helm upgrade --install traefik traefik/traefik -n traefik -f traefik-config.yaml

创建 whoami.yaml

  • apiVersion: traefik.containo.us/v1alpha1
  • kind: Middleware
  • metadata:
  • name: whoamiplugin
  • spec:
  • plugin:
  • plugindemo: # plugindemo 插件
  • Headers:
  • whoami-header: hello world
  • ---
  • apiVersion: traefik.containo.us/v1alpha1
  • kind: IngressRoute
  • metadata:
  • name: whoamiingressroute # 入口路由名称
  • spec:
  • entryPoints: # 网络入口点
  • - web
  • routes:
  • - match: Host(`master`) && PathPrefix(`/whoami/`) # 路由匹配器,匹配 http://master/whoami/
  • middlewares: # 使用 plugindemo 插件
  • - name: whoamiplugin
  • kind: Rule
  • services: # 代理服务
  • - name: whoami
  • port: 80
  • ---
  • apiVersion: v1
  • kind: Service
  • metadata:
  • name: whoami
  • spec:
  • ports:
  • - protocol: TCP
  • name: web
  • port: 80
  • selector:
  • app: whoami
  • ---
  • kind: Deployment
  • apiVersion: apps/v1
  • metadata:
  • name: whoami
  • labels:
  • app: whoami
  • spec:
  • replicas: 2
  • selector:
  • matchLabels:
  • app: whoami
  • template:
  • metadata:
  • labels:
  • app: whoami
  • spec:
  • containers:
  • - name: whoami
  • image: containous/whoami
  • ports:
  • - name: web
  • containerPort: 80
展开

使用 kubectl 启动 whoami :

  • kubectl apply -f whoami.yaml

查看插件使用效果:

请求 header 已增加 whoami-header: hello world

Traefik Plugins 源码分析

“源码以 https://github.com/traefik/traefik/commit/ca2ff214c49a2aa1f8b590d4f2158f0ea734322b 版本为例。

traefik 会在 cmd/traefik/traefik.go 的 172 行 setupServer 函数进行初始化服务,其中构造插件实例也是在此函数内完成:

  • // 构造插件实例
  • pluginBuilder, err := createPluginBuilder(staticConfiguration)
  • if err != nil {
  • return nil, err
  • }

深入 createPluginBuilder 函数:

  • func createPluginBuilder(staticConfiguration *static.Configuration) (*plugins.Builder, error) {
  • // 初始化插件
  • client, plgs, localPlgs, err := initPlugins(staticConfiguration)
  • if err != nil {
  • return nil, err
  • }
  • // 返回 *plugins.Builder ,实现了 PluginsBuilder 接口
  • return plugins.NewBuilder(client, plgs, localPlgs)
  • }

initPlugins 过程中,会进行插件配置检查,下载,解压等一系列操作:

  • func initPlugins(staticCfg *static.Configuration) (*plugins.Client, map[string]plugins.Descriptor, map[string]plugins.LocalDescriptor, error) {
  • err := checkUniquePluginNames(staticCfg.Experimental)
  • if err != nil {
  • return nil, nil, nil, err
  • }
  • var client *plugins.Client
  • plgs := map[string]plugins.Descriptor{}
  • // 是否启用了 Traefik Pilot,并且配置了插件
  • if isPilotEnabled(staticCfg) && hasPlugins(staticCfg) {
  • opts := plugins.ClientOptions{
  • Output: outputDir,
  • Token: staticCfg.Pilot.Token, // Pilot 的 token
  • }
  • var err error
  • // 创建插件客户端
  • client, err = plugins.NewClient(opts)
  • if err != nil {
  • return nil, nil, nil, err
  • }
  • // 初始化所有插件
  • err = plugins.SetupRemotePlugins(client, staticCfg.Experimental.Plugins)
  • if err != nil {
  • return nil, nil, nil, err
  • }
  • plgs = staticCfg.Experimental.Plugins
  • }
  • localPlgs := map[string]plugins.LocalDescriptor{}
  • if hasLocalPlugins(staticCfg) {
  • err := plugins.SetupLocalPlugins(staticCfg.Experimental.LocalPlugins)
  • if err != nil {
  • return nil, nil, nil, err
  • }
  • localPlgs = staticCfg.Experimental.LocalPlugins
  • }
  • return client, plgs, localPlgs, nil
  • }
展开

其中下载插件的逻辑如下:

  • func SetupRemotePlugins(client *Client, plugins map[string]Descriptor) error {
  • // 检查插件配置
  • err := checkRemotePluginsConfiguration(plugins)
  • if err != nil {
  • return fmt.Errorf("invalid configuration: %w", err)
  • }
  • // 清理旧插件
  • err = client.CleanArchives(plugins)
  • if err != nil {
  • return fmt.Errorf("failed to clean archives: %w", err)
  • }
  • ctx := context.Background()
  • // 依次下载所有插件
  • for pAlias, desc := range plugins {
  • log.FromContext(ctx).Debugf("loading of plugin: %s: %s@%s", pAlias, desc.ModuleName, desc.Version)
  • // 开始下载插件
  • hash, err := client.Download(ctx, desc.ModuleName, desc.Version)
  • if err != nil {
  • _ = client.ResetAll()
  • return fmt.Errorf("failed to download plugin %s: %w", desc.ModuleName, err)
  • }
  • // hash 校验
  • err = client.Check(ctx, desc.ModuleName, desc.Version, hash)
  • if err != nil {
  • _ = client.ResetAll()
  • return fmt.Errorf("failed to check archive integrity of the plugin %s: %w", desc.ModuleName, err)
  • }
  • }
  • err = client.WriteState(plugins)
  • if err != nil {
  • _ = client.ResetAll()
  • return fmt.Errorf("failed to write plugins state: %w", err)
  • }
  • // 解压所有下载成功的插件
  • for _, desc := range plugins {
  • err = client.Unzip(desc.ModuleName, desc.Version)
  • if err != nil {
  • _ = client.ResetAll()
  • return fmt.Errorf("failed to unzip archive: %w", err)
  • }
  • }
  • return nil
  • }
展开

分析 client.Download() 的关键源码,可以知道,traefik 的插件下载 url 格式为 https://plugin.pilot.traefik.io/public/download/github.com/traefik/plugindemo/v0.2.1 :

  • const pilotURL = "https://plugin.pilot.traefik.io/public/"
  • ...
  • // 组合 url , pilotURL/download/插件名/版本号
  • endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "download", pName, pVersion))
  • if err != nil {
  • return "", fmt.Errorf("failed to parse endpoint URL: %w", err)
  • }
  • req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil)
  • if err != nil {
  • return "", fmt.Errorf("failed to create request: %w", err)
  • }
  • if hash != "" {
  • req.Header.Set(hashHeader, hash)
  • }
  • if c.token != "" {
  • req.Header.Set(tokenHeader, c.token)
  • }
  • resp, err := c.HTTPClient.Do(req)
  • ...

可以使用 curl 工具验证:

  • curl -H 'X-Token:898bb869-77ad-4594-b68f-1f87e0aa2e9b' -O https://plugin.pilot.traefik.io/public/download/github.com/traefik/plugindemo/v0.2.1

traefik 会将插件下载到 /plugins-storage 目录。

插件下载解压完成后,会使用 plugins.NewBuilder(client, plgs, localPlgs) 将插件源码读取加载到 *plugins.Builder 实例,这里用到一个十分强大的 go 解释器库 yaegi ,同样出自 traefik 之手,地址在 https://github.com/traefik/yaegi ,使用起来也很简单,只要使用 new()创建解释器,后续使用 Eval()就可以运行代码了。

traefik 插件分为 provider 和 middleware 两种,故 *plugins.Builder 实例也提供了 middlewareDescriptors 和 providerDescriptors 两种 map 类型来存放插件:

  • // NewBuilder creates a new Builder.
  • func NewBuilder(client *Client, plugins map[string]Descriptor, localPlugins map[string]LocalDescriptor) (*Builder, error) {
  • pb := &Builder{
  • middlewareDescriptors: map[string]pluginContext{}, // 中间件类型插件包名:插件实例
  • providerDescriptors: map[string]pluginContext{}, // 提供者类型插件包名:插件实例
  • }
  • for pName, desc := range plugins {
  • manifest, err := client.ReadManifest(desc.ModuleName)
  • if err != nil {
  • _ = client.ResetAll()
  • return nil, fmt.Errorf("%s: failed to read manifest: %w", desc.ModuleName, err)
  • }
  • // 创建 go 解释器
  • i := interp.New(interp.Options{GoPath: client.GoPath()})
  • err = i.Use(stdlib.Symbols)
  • if err != nil {
  • return nil, fmt.Errorf("%s: failed to load symbols: %w", desc.ModuleName, err)
  • }
  • err = i.Use(ppSymbols())
  • if err != nil {
  • return nil, fmt.Errorf("%s: failed to load provider symbols: %w", desc.ModuleName, err)
  • }
  • // 导入包
  • _, err = i.Eval(fmt.Sprintf(`import "%s"`, manifest.Import))
  • if err != nil {
  • return nil, fmt.Errorf("%s: failed to import plugin code %q: %w", desc.ModuleName, manifest.Import, err)
  • }
  • switch manifest.Type {
  • case "middleware":
  • // 将 middleware 类型插件放置到这里
  • pb.middlewareDescriptors[pName] = pluginContext{
  • interpreter: i, // 解释器实例
  • GoPath: client.GoPath(),
  • Import: manifest.Import,
  • BasePkg: manifest.BasePkg,
  • }
  • case "provider":
  • // 将 provider 类型插件放置到这里
  • pb.providerDescriptors[pName] = pluginContext{
  • interpreter: i, // 解释器实例
  • GoPath: client.GoPath(),
  • Import: manifest.Import,
  • BasePkg: manifest.BasePkg,
  • }
  • default:
  • return nil, fmt.Errorf("unknow plugin type: %s", manifest.Type)
  • }
  • }
  • ......
  • // 返回 *plugins.Builder 实例
  • return pb, nil
  • }
展开

初始化构造插件完成后回到 cmd/traefik/traefik.gosetupServer 函数中,会进行插件的动态加载过程,首先是 Provider 类型的插件:

  • // Providers plugins
  • // 遍历 Provider 类型的插件
  • for name, conf := range staticConfiguration.Providers.Plugin {
  • // 实例化插件
  • p, err := pluginBuilder.BuildProvider(name, conf)
  • if err != nil {
  • return nil, fmt.Errorf("plugin: failed to build provider: %w", err)
  • }
  • err = providerAggregator.AddProvider(p)
  • if err != nil {
  • return nil, fmt.Errorf("plugin: failed to add provider: %w", err)
  • }
  • }

traefik 插件规定必须实现 CreateConfig 和 New 函数,而 pluginBuilder.BuildProvider 就是使用解释器执行插件的 CreateConfig 函数,然后使用 wrapper.NewWrapper 调用插件的 New 函数:

  • // 使用之前保存的解释器去调用插件的 CreateConfig 函数
  • vConfig, err := descriptor.interpreter.Eval(basePkg + `.CreateConfig()`)
  • if err != nil {
  • return nil, fmt.Errorf("failed to eval CreateConfig: %w", err)
  • }
  • ......
  • _, err = descriptor.interpreter.Eval(`package wrapper
  • import (
  • "context"
  • ` + basePkg + ` "` + descriptor.Import + `"
  • "github.com/traefik/traefik/v2/pkg/plugins"
  • )
  • func NewWrapper(ctx context.Context, config *` + basePkg + `.Config, name string) (plugins.PP, error) {
  • p, err := ` + basePkg + `.New(ctx, config, name)
  • var pv plugins.PP = p
  • return pv, err
  • }
  • `)
  • if err != nil {
  • return nil, fmt.Errorf("failed to eval wrapper: %w", err)
  • }
  • fnNew, err := descriptor.interpreter.Eval("wrapper.NewWrapper")
  • if err != nil {
  • return nil, fmt.Errorf("failed to eval New: %w", err)
  • }
  • ...

Provider 加载完成后,traefik 就会开始监听路由,若路由配置了中间件插件,traefik 就会同理去加载对应的 middleware 类型插件:

  • // Plugin
  • if config.Plugin != nil {
  • if middleware != nil {
  • return nil, badConf
  • }
  • pluginType, rawPluginConfig, err := findPluginConfig(config.Plugin)
  • if err != nil {
  • return nil, fmt.Errorf("plugin: %w", err)
  • }
  • // 执行中间件类型插件
  • plug, err := b.pluginBuilder.Build(pluginType, rawPluginConfig, middlewareName)
  • if err != nil {
  • return nil, fmt.Errorf("plugin: %w", err)
  • }
  • middleware = func(next http.Handler) (http.Handler, error) {
  • return plug(ctx, next)
  • }
  • }

关键地方在 pkg/plugins/middlewares.go 的 40 行,同样是使用解释器执行插件的 CreateConfig 和 New 函数:

  • func newMiddleware(descriptor pluginContext, config map[string]interface{}, middlewareName string) (*Middleware, error) {
  • basePkg := descriptor.BasePkg
  • if basePkg == "" {
  • basePkg = strings.ReplaceAll(path.Base(descriptor.Import), "-", "_")
  • }
  • // 使用之前保存的解释器去调用插件的 CreateConfig 函数
  • vConfig, err := descriptor.interpreter.Eval(basePkg + `.CreateConfig()`)
  • if err != nil {
  • return nil, fmt.Errorf("failed to eval CreateConfig: %w", err)
  • }
  • cfg := &mapstructure.DecoderConfig{
  • DecodeHook: mapstructure.StringToSliceHookFunc(","),
  • WeaklyTypedInput: true,
  • Result: vConfig.Interface(),
  • }
  • decoder, err := mapstructure.NewDecoder(cfg)
  • if err != nil {
  • return nil, fmt.Errorf("failed to create configuration decoder: %w", err)
  • }
  • err = decoder.Decode(config)
  • if err != nil {
  • return nil, fmt.Errorf("failed to decode configuration: %w", err)
  • }
  • // 使用之前保存的解释器去调用插件的 New 函数
  • fnNew, err := descriptor.interpreter.Eval(basePkg + `.New`)
  • if err != nil {
  • return nil, fmt.Errorf("failed to eval New: %w", err)
  • }
  • return &Middleware{
  • middlewareName: middlewareName,
  • fnNew: fnNew,
  • config: vConfig,
  • }, nil
  • }
展开

traefik插件架构

Traefik Plugins 开发

上文分析 traefik 的插件实现源码已经知道,traefik 的插件是靠 Yaegi 解释器动态加载实现的,所以开发 traefik 插件变得很简单,和开发 web 浏览器扩展一样。

traefik 的插件托管在 GitHub ,这里以 https://github.com/togettoyou/traefik-timer-plugin 为例。

GitHub 仓库需要按照规范,有 readme.md ,需设置一个名称为 traefik-plugin 的 topic ,根目录下需要创建一个 .traefik.yml 配置文件:

  • # 在 Traefik Pilot Web UI 中显示的插件的名称
  • displayName: Timer Plugin
  • # 插件类型,目前版本只支持 middleware
  • type: middleware
  • # 插件导入路径
  • import: github.com/togettoyou/traefik-timer-plugin
  • # 插件简介
  • summary: 用于请求响应计时
  • # 配置数据
  • testData:
  • log: true

接下来就是开发代码了,traefik 也为插件代码提供了规范,需要包含以下对象:

  • type Config struct { ... } 结构体,字段任意。
  • func CreateConfig() *Config 函数。
  • func New(ctx context.Context, next http.Handler, config *Config, name string) (http.Handler, error) 函数。

代码示例:

  • package traefik_timer_plugin
  • import (
  • "context"
  • "fmt"
  • "net/http"
  • "time"
  • )
  • // Config 自定义配置
  • type Config struct {
  • Log bool `json:"log,omitempty"`
  • }
  • // CreateConfig 提供给 traefik 设置配置
  • func CreateConfig() *Config {
  • return &Config{}
  • }
  • // New 提供给 traefik 创建 Timer 插件
  • func New(_ context.Context, next http.Handler, config *Config, name string) (http.Handler, error) {
  • return &Timer{
  • next: next,
  • name: name,
  • log: config.Log,
  • }, nil
  • }
  • type Timer struct {
  • next http.Handler
  • name string
  • log bool
  • }
  • func (t *Timer) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
  • start := time.Now()
  • t.next.ServeHTTP(rw, req)
  • cost := time.Since(start)
  • if t.log {
  • fmt.Println("请求花费时间:", cost)
  • }
  • }
展开

最后的最后,为仓库打一个版本标签如 v0.1.0 即可发布插件。

插件验证,修改 traefik 配置 traefik-config.yaml ,安装 traefik-timer-plugin@v0.1.0 插件:

  • pilot:
  • enabled: true
  • token: "898bb869-77ad-4594-b68f-1f87e0aa2e9b"
  • dashboard: true
  • additionalArguments:
  • - "--experimental.plugins.traefik_timer_plugin.modulename=github.com/togettoyou/traefik-timer-plugin"
  • - "--experimental.plugins.traefik_timer_plugin.version=v0.1.0"
  • experimental:
  • plugins:
  • enabled: true
  • ports:
  • traefik:
  • expose: true
  • web:
  • nodePort: 80
  • websecure:
  • nodePort: 443

使用 helm 更新重启 traefik :

  • helm upgrade --install traefik traefik/traefik -n traefik -f traefik-config.yaml

更改 whoami.yaml

  • apiVersion: traefik.containo.us/v1alpha1
  • kind: Middleware
  • metadata:
  • name: timerplugin
  • spec:
  • plugin:
  • traefik_timer_plugin: # 自定义的计时插件
  • log: true
  • ---
  • apiVersion: traefik.containo.us/v1alpha1
  • kind: IngressRoute
  • metadata:
  • name: whoamiingressroute # 入口路由名称
  • spec:
  • entryPoints: # 网络入口点
  • - web
  • routes:
  • - match: Host(`master`) && PathPrefix(`/whoami/`) # 路由匹配器,匹配 http://master/whoami/
  • middlewares: # 使用 timerplugin 插件
  • - name: timerplugin
  • kind: Rule
  • services: # 代理服务
  • - name: whoami
  • port: 80
  • ---
  • apiVersion: v1
  • kind: Service
  • metadata:
  • name: whoami
  • spec:
  • ports:
  • - protocol: TCP
  • name: web
  • port: 80
  • selector:
  • app: whoami
  • ---
  • kind: Deployment
  • apiVersion: apps/v1
  • metadata:
  • name: whoami
  • labels:
  • app: whoami
  • spec:
  • replicas: 2
  • selector:
  • matchLabels:
  • app: whoami
  • template:
  • metadata:
  • labels:
  • app: whoami
  • spec:
  • containers:
  • - name: whoami
  • image: containous/whoami
  • ports:
  • - name: web
  • containerPort: 80
展开

重启 whoami :

  • kubectl apply -f whoami.yaml

访问路由并查看 traefik 日志:

  • kubectl logs -f traefik-xxxxxx -n traefik

当然,这里只是作为示例,traefik 的插件机制开发必然可以为我们提供更强大的功能。

本站文章资源均来源自网络,除非特别声明,否则均不代表站方观点,并仅供查阅,不作为任何参考依据!
如有侵权请及时跟我们联系,本站将及时删除!
如遇版权问题,请查看 本站版权声明
THE END
分享
二维码
海报
<<上一篇
下一篇>>