#kubelet #kubernetes #containerd
- Один приватный регистри с аутентификация.
- containerD 1.6.
- kubernetes 1.28.
- Абсолютно все образа загружаются с этого регистри.
Вопрос:
- Где необходимо и достаточно указать данные для доступа к регистри чтобы запустить под?
Уверен большинство из вас правильно ответили на этот вопрос.
Для тех кто сомневается ответ под катом.
Необходимо и достаточно указать данные для доступа в конфиге containerD.А теперь чуть более подробное объяснение почему именно так.
Как вы знаете, чтобы под запустился на ноде надо минимум 2 контейнера:
- sandbox, в котором происходит очень мало магии, но весьма занимательной.
- Обычно его называют pause контейнер и он очень прост.
- Сам контейнер с полезной нагрузкой.
Сначала кубелет запускает sandbox, получает его ID и после этого запускает полезную нагрузку.
Давайте более подробно пройдемся по этому процессу.
Вызвали m.runtimeService.RunPodSandbox
func (m *kubeGenericRuntimeManager)
createPodSandbox(ctx context.Context, pod *v1.Pod, attempt uint32)
(string, string, error)
...
podSandBoxID, err := m.runtimeService.RunPodSandbox(ctx, podSandboxConfig,
runtimeHandler)
...
return podSandBoxID, "", nil
(я не буду углубляться в то как работают gRPC вызовы между kubelet и containerD)
Рантайм как я уже говорил выше у нас containerD
Убеждаемся, что sandbox image существует.
func (c *criService) RunPodSandbox(ctx context.Context, r *runtime.RunPodSandboxRequest)
(_ *runtime.RunPodSandboxResponse, retErr error) {
...
image, err := c.ensureImageExists(ctx, c.config.SandboxImage, config)
func (c *criService) ensureImageExists(ctx context.Context, ref string,
config *runtime.PodSandboxConfig) (*imagestore.Image, error) {
...
// Pull image to ensure the image exists
resp, err := c.PullImage(ctx,
&runtime.PullImageRequest{Image: &runtime.ImageSpec{Image: ref}, SandboxConfig: config})
Тут стоит обратить внимание на PullImageRequest
Вот его определение:
type PullImageRequest struct {
// Spec of the image.
Image *ImageSpec `protobuf:"bytes,1,opt,name=image,proto3" json:"image,omitempty"`
// Authentication configuration for pulling the image.
Auth *AuthConfig `protobuf:"bytes,2,opt,name=auth,proto3" json:"auth,omitempty"`
// Config of the PodSandbox, which is used to pull image in PodSandbox context.
SandboxConfig *PodSandboxConfig `protobuf:"bytes,3,opt,name=sandbox_config,json=sandboxConfig,proto3" json:"sandbox_config,omitempty"`
XXX_NoUnkeyedLiteral struct{} `json:"-"`
XXX_sizecache int32 `json:"-"`
}
Как вы можете заметить в вызове PullImage
не передается никакая auth информация.
func (c *criService) PullImage(ctx context.Context, r *runtime.PullImageRequest)
(*runtime.PullImageResponse, error) {
...
var (
resolver = docker.NewResolver(docker.ResolverOptions{
Headers: c.config.Registry.Headers,
Hosts: c.registryHosts(ctx, r.GetAuth()),
})
isSchema1 bool
imageHandler containerdimages.HandlerFunc = func(_ context.Context,
desc imagespec.Descriptor) ([]imagespec.Descriptor, error) {
if desc.MediaType == containerdimages.MediaTypeDockerSchema1Manifest {
isSchema1 = true
}
return nil, nil
}
)
Нас тут интересует
Headers: c.config.Registry.Headers,
Hosts: c.registryHosts(ctx, r.GetAuth()),
GetAuth()
вытаскивает информацию для доступа из структуры PullImageRequest
func (m *PullImageRequest) GetAuth() *AuthConfig {
if m != nil {
return m.Auth
}
return nil
}
Но как мы видели выше - у нас нет этой информации при создании sandbox контейнера.
Пойдем дальше.
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 {
hostauth = toRuntimeAuthConfig(*config.Auth)
}
}
return ParseAuth(hostauth, host)
}
hostOptions.HostDir = hostDirFromRoots(paths)
return config.ConfigureHosts(ctx, hostOptions)
}
...
Обратите внимание на это
if config.Auth != nil {
hostauth = toRuntimeAuthConfig(*config.Auth)
}
Именно тут мы получаем наши креды
// toRuntimeAuthConfig converts cri plugin auth config to runtime auth config.
func toRuntimeAuthConfig(a criconfig.AuthConfig) *runtime.AuthConfig {
return &runtime.AuthConfig{
Username: a.Username,
Password: a.Password,
Auth: a.Auth,
IdentityToken: a.IdentityToken,
}
}
А в конфиге (/etc/containerd/config.toml) это выглядит так:
[plugins."io.containerd.grpc.v1.cri".registry.configs]
[plugins."io.containerd.grpc.v1.cri".registry.configs."registry-1.docker.io".auth]
auth = "Очень секретный токен"
Далее образ скачивается и отдается ID сандбокса.
Заметьте, без этих данных мы бы не смогли скачать pause контейнер.
Создание контейнера с полезной нагрузкой выглядит так:
if msg, err := m.startContainer(ctx, podSandboxID, podSandboxConfig,
spec, pod, podStatus, pullSecrets, podIP, podIPs); err != nil {
...
func (m *kubeGenericRuntimeManager) startContainer(ctx context.Context, podSandboxID string, podSandboxConfig *runtimeapi.PodSandboxConfig, spec *startSpec, pod *v1.Pod, podStatus *kubecontainer.PodStatus, pullSecrets []v1.Secret, podIP string, podIPs []string) (string, error) {
container := spec.container
// Step 1: pull the image.
imageRef, msg, err := m.imagePuller.EnsureImageExists(ctx, pod, container, pullSecrets, podSandboxConfig)
...
Обратите внимание, в EnsureImageExists
сразу передаются данные для аудентификации.
https://github.com/kubernetes/kubernetes/blob/release-1.28/pkg/kubelet/images/image_manager.go#L101
func (m *imageManager) EnsureImageExists(ctx context.Context, pod *v1.Pod,
container *v1.Container, pullSecrets []v1.Secret,
podSandboxConfig *runtimeapi.PodSandboxConfig) (string, string, error) {
...
m.puller.pullImage(ctx, spec, pullSecrets, pullChan, podSandboxConfig)
...
type imagePuller interface {
pullImage(context.Context, kubecontainer.ImageSpec,
[]v1.Secret, chan<- pullResult, *runtimeapi.PodSandboxConfig)
}
А тут мы еще добавляем данные из config.json
func (m *kubeGenericRuntimeManager) PullImage(ctx context.Context,
image kubecontainer.ImageSpec, pullSecrets []v1.Secret,
podSandboxConfig *runtimeapi.PodSandboxConfig) (string, error) {
...
keyring, err := credentialprovidersecrets.MakeDockerKeyring(pullSecrets,
m.keyring)
...
Объединяем секреты от пода и секреты от config.json
func MakeDockerKeyring(passedSecrets []v1.Secret,
defaultKeyring credentialprovider.DockerKeyring)
(credentialprovider.DockerKeyring, error) {
...
Ну а далее, запрос передается в containerD и происходит скачивание образа.
Но откуда же kubelet получает данные для аудентификации? Тут он смотрит секреты пода.
func (kl *Kubelet) getPullSecretsForPod(pod *v1.Pod) []v1.Secret {
...
Итого:
- Имея данные для аудентификации в config.toml containerD мы можем вытянуть любой образ из регистри.
- Если их нет - мы можем вытянуть все, кроме pause.
- А как вы запустите под без pause? :)
Бонус для тех кто дочитал.
Пока читал про все это, нашел issue в amazon-eks-ami посвященный тому,
что pause контейнер удаляется GC.
Прочитав его, узнал про pinned images в containerD.
Я веду телеграм канал, подписывайтесь, там много подобного :)