概述
先梳理一下 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 来使用具有这些特点的内部仓库呢。
- 以http方式通过用户名和密码来登录的
- 局域网节点不具有外网条件大部分yaml不想改镜像地址
对于第2点,具体的场景就是大部分的研发都是在网上或者 github 获取 yaml 或者 helm 来部署的,上面的镜像地址改起来工程太大了,也非常繁琐,像比较常见的镜像地址有下面几个。
- k8s.io
- quay.io
- ghcr.io
- docker.io
那么配置的目标应该最终符合下面的结果,才算是好用的配置。
- 拉取大部分已知的镜像地址都可以自动转成内部仓库地址
- 可以正常通过内部仓库地址来拉取镜像
达到上面的目标,那么对于用户在使用镜像仓库的时候,就会方面很多,隐藏了很多麻烦的配置细节,清楚了目标之后就开搞,首先分析一下 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
}
}
|
从代码上看,host
跟 mirror
是需要匹配上的,有下面两种情况。
host
配了,但是应对的mirror
没配,就只会返回host
的endpoint
host
和mirror
都配了,返回host
和mirror
的endpoint
为了省事,可以在 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日,文中内容可能已过时,请谨慎参考。