Skip to content

Instantly share code, notes, and snippets.

@azalio
Last active May 1, 2024 20:00
Show Gist options
  • Save azalio/8584d4ef1a1f353bc317118131576095 to your computer and use it in GitHub Desktop.
Save azalio/8584d4ef1a1f353bc317118131576095 to your computer and use it in GitHub Desktop.
Аутентификация в private registry в kubernetes и containerD

#kubelet #kubernetes #containerd

Дано:

  • Один приватный регистри с аутентификация.
  • containerD 1.6.
  • kubernetes 1.28.
  • Абсолютно все образа загружаются с этого регистри.

Вопрос:

  • Где необходимо и достаточно указать данные для доступа к регистри чтобы запустить под?

Уверен большинство из вас правильно ответили на этот вопрос.

Для тех кто сомневается ответ под катом. Необходимо и достаточно указать данные для доступа в конфиге containerD.

Как происходит аутентификация и скачивание образа в 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 {
...

startContainer

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)
...

imagePuller

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.

Я веду телеграм канал, подписывайтесь, там много подобного :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment