目录

Containerd系列-01-镜像仓库

概述

先梳理一下 kubelet 创建容器,到拉取镜像的逻辑。

1
kubelet create sandbox -> image service(grpc) -> containerd pull
1
ctr images pull --hosts-dir "/etc/containerd/certs.d" myregistry.io:5000/image_name:tag

现况是,公司内部的 Harbor 镜像仓库具有下面的特点,那么要怎么配置 containerd 来使用具有这些特点的内部仓库呢。

  1. 以http方式通过用户名和密码来登录的
  2. 局域网节点不具有外网条件大部分yaml不想改镜像地址

对于第2点,具体的场景就是大部分的研发都是在网上或者 github 获取 yaml 或者 helm 来部署的,上面的镜像地址改起来工程太大了,也非常繁琐,像比较常见的镜像地址有下面几个。

  1. k8s.io
  2. quay.io
  3. ghcr.io
  4. docker.io

那么配置的目标应该最终符合下面的结果,才算是好用的配置。

  1. 拉取大部分已知的镜像地址都可以自动转成内部仓库地址
  2. 可以正常通过内部仓库地址来拉取镜像

达到上面的目标,那么对于用户在使用镜像仓库的时候,就会方面很多,隐藏了很多麻烦的配置细节,清楚了目标之后就开搞,首先分析一下 containerd 的配置文件,下面是针对 1.6.6 版本的 containerd 进行分析和实践。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[plugins."io.containerd.grpc.v1.cri".registry]
  config_path = "/etc/containerd/certs.d"

  [plugins."io.containerd.grpc.v1.cri".registry.auths]

  [plugins."io.containerd.grpc.v1.cri".registry.configs]

  [plugins."io.containerd.grpc.v1.cri".registry.headers]

  [plugins."io.containerd.grpc.v1.cri".registry.mirrors]

下面是 containerd 中,Registry 的结构体,可以见到 config_path 是一个比较重要的配置参数,只要设置了,其他参数都是无效的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
// Registry is registry settings configured
type Registry struct {
	// ConfigPath is a path to the root directory containing registry-specific
	// configurations.
	// If ConfigPath is set, the rest of the registry specific options are ignored.
	ConfigPath string `toml:"config_path" json:"configPath"`
	// Mirrors are namespace to mirror mapping for all namespaces.
	// This option will not be used when ConfigPath is provided.
	// DEPRECATED: Use ConfigPath instead. Remove in containerd 1.7.
	Mirrors map[string]Mirror `toml:"mirrors" json:"mirrors"`
	// Configs are configs for each registry.
	// The key is the domain name or IP of the registry.
	// This option will be fully deprecated for ConfigPath in the future.
	Configs map[string]RegistryConfig `toml:"configs" json:"configs"`
	// Auths are registry endpoint to auth config mapping. The registry endpoint must
	// be a valid url with host specified.
	// DEPRECATED: Use ConfigPath instead. Remove in containerd 1.6.
	Auths map[string]AuthConfig `toml:"auths" json:"auths"`
	// Headers adds additional HTTP headers that get sent to all registries
	Headers map[string][]string `toml:"headers" json:"headers"`
}

另外 HostOptions 也是一个很重要的结构体,下面会再说到。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// HostOptions is used to configure registry hosts
type HostOptions struct {
	HostDir       func(string) (string, error)
	Credentials   func(host string) (string, string, error)
	DefaultTLS    *tls.Config
	DefaultScheme string
	// UpdateClient will be called after creating http.Client object, so clients can provide extra configuration
	UpdateClient   UpdateClientFunc
	AuthorizerOpts []docker.AuthorizerOpt
}

还有这个 RegistryHost,理解上很重要,他是上述这些的配置最终转化出来的数据结构,如果上面配置有问题,这个结构体就不会是正常的了,也就无法正常拉取镜像。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// RegistryHost represents a complete configuration for a registry
// host, representing the capabilities, authorizations, connection
// configuration, and location.
type RegistryHost struct {
	Client       *http.Client
	Authorizer   Authorizer
	Host         string
	Scheme       string
	Path         string
	Capabilities HostCapabilities
	Header       http.Header
}

之后分析一下,配置了 config_path 之后,在镜像拉取的时候会怎么解析和用到,下面这个方法比较长,耐心分析一下。

 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
// registryHosts is the registry hosts to be used by the resolver.
func (c *criService) registryHosts(ctx context.Context, auth *runtime.AuthConfig) docker.RegistryHosts {
	paths := filepath.SplitList(c.config.Registry.ConfigPath)
	if len(paths) > 0 {
		hostOptions := config.HostOptions{}
		hostOptions.Credentials = func(host string) (string, string, error) {
			hostauth := auth
			if hostauth == nil {
				config := c.config.Registry.Configs[host]
				if config.Auth != nil {
					// 这里很重要,会把配置文件的配置拿来做runtime的认证信息
					hostauth = toRuntimeAuthConfig(*config.Auth)
				}
			}
			return ParseAuth(hostauth, host)
		}
		hostOptions.HostDir = hostDirFromRoots(paths)

		return config.ConfigureHosts(ctx, hostOptions)
	}

	return func(host string) ([]docker.RegistryHost, error) {
		var registries []docker.RegistryHost

		endpoints, err := c.registryEndpoints(host)
		if err != nil {
			return nil, fmt.Errorf("get registry endpoints: %w", err)
		}
		for _, e := range endpoints {
			u, err := url.Parse(e)
			if err != nil {
				return nil, fmt.Errorf("parse registry endpoint %q from mirrors: %w", e, err)
			}

			var (
				transport = newTransport()
				client    = &http.Client{Transport: transport}
				config    = c.config.Registry.Configs[u.Host]
			)

			if config.TLS != nil {
				transport.TLSClientConfig, err = c.getTLSConfig(*config.TLS)
				if err != nil {
					return nil, fmt.Errorf("get TLSConfig for registry %q: %w", e, err)
				}
			} else if isLocalHost(host) && u.Scheme == "http" {
				// Skipping TLS verification for localhost
				transport.TLSClientConfig = &tls.Config{
					InsecureSkipVerify: true,
				}
			}

			// Make a copy of `auth`, so that different authorizers would not reference
			// the same auth variable.
			auth := auth
			if auth == nil && config.Auth != nil {
				auth = toRuntimeAuthConfig(*config.Auth)
			}
			authorizer := docker.NewDockerAuthorizer(
				docker.WithAuthClient(client),
				docker.WithAuthCreds(func(host string) (string, string, error) {
					return ParseAuth(auth, host)
				}))

			if u.Path == "" {
				u.Path = "/v2"
			}

			registries = append(registries, docker.RegistryHost{
				Client:       client,
				Authorizer:   authorizer,
				Host:         u.Host,
				Scheme:       u.Scheme,
				Path:         u.Path,
				Capabilities: docker.HostCapabilityResolve | docker.HostCapabilityPull,
			})
		}
		return registries, nil
	}
}

从代码上看,hostmirror 是需要匹配上的,有下面两种情况。

  1. host配了,但是应对的mirror没配,就只会返回hostendpoint
  2. hostmirror都配了,返回hostmirrorendpoint

为了省事,可以在 mirror 的地址上配置通配符 *,这样不管是什么 host 都会匹配到所有 mirror 的地址,另外关于 http,针对内部 harbor 仓库的时候,建议全部用 http,不管是域名还是 IP。

下面是一个可行的配置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
[plugins."io.containerd.grpc.v1.cri".registry]
  config_path = ""

  [plugins."io.containerd.grpc.v1.cri".registry.auths]

  [plugins."io.containerd.grpc.v1.cri".registry.configs]
    [plugins."io.containerd.grpc.v1.cri".registry.configs."harbor.dev-prev.com".tls]
      insecure_skip_verify = false
    [plugins."io.containerd.grpc.v1.cri".registry.configs."harbor.dev-prev.com".auth]
      username = "admin"
      password = "Harbor12345"

  [plugins."io.containerd.grpc.v1.cri".registry.headers]

  [plugins."io.containerd.grpc.v1.cri".registry.mirrors]
    [plugins."io.containerd.grpc.v1.cri".registry.mirrors."harbor.dev-prev.com"]
       endpoint = ["http://harbor.dev-prev.com"]

但是我头铁,就是想用 config_path,而且下面的那些插件参数在 1.7 版本也会被移除,所以就必须头铁一回。

1
2
3
4
5
server = "harbor.dev-prev.com"

[host."http://10.9.28.38:80"]
  capabilities = ["pull", "resolve", "push"]
  skip_verify = true
1
ctr -n k8s.io image pull harbor.dev-prev.com/middleware/longhorn-manager:v1.2.2 --plain-http

参考资料

警告
本文最后更新于 2017年2月1日,文中内容可能已过时,请谨慎参考。