8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

KubernetesAdvent Calendar 2020

Day 17

VolumePlugin がボリュームを作成・マウントするしくみ

Posted at

はじめに

Pod の作成時、pod.spec.volumes に記述したボリュームがコンテナにマウントされます。
マウントされる Node 側のボリュームを、VolumePlugin がどのように作成・マウントしているのか調べました。

機能

VolumePlugin は kubelet 内で動作するプラグインで、ボリュームの種類(hostPath / EmptyDir / ConfigMap など)ごとに実装されています。

VolumePlugin は以下のように定義されています。(ソースはこちら

// VolumePlugin is an interface to volume plugins that can be used on a
// kubernetes node (e.g. by kubelet) to instantiate and manage volumes.
type VolumePlugin interface {
	// Init initializes the plugin.  This will be called exactly once
	// before any New* calls are made - implementations of plugins may
	// depend on this.
	Init(host VolumeHost) error

	// Name returns the plugin's name.  Plugins must use namespaced names
	// such as "example.com/volume" and contain exactly one '/' character.
	// The "kubernetes.io" namespace is reserved for plugins which are
	// bundled with kubernetes.
	GetPluginName() string

	// GetVolumeName returns the name/ID to uniquely identifying the actual
	// backing device, directory, path, etc. referenced by the specified volume
	// spec.
	// For Attachable volumes, this value must be able to be passed back to
	// volume Detach methods to identify the device to act on.
	// If the plugin does not support the given spec, this returns an error.
	GetVolumeName(spec *Spec) (string, error)

	// CanSupport tests whether the plugin supports a given volume
	// specification from the API.  The spec pointer should be considered
	// const.
	CanSupport(spec *Spec) bool

	// RequiresRemount returns true if this plugin requires mount calls to be
	// reexecuted. Atomically updating volumes, like Downward API, depend on
	// this to update the contents of the volume.
	RequiresRemount(spec *Spec) bool

	// NewMounter creates a new volume.Mounter from an API specification.
	// Ownership of the spec pointer in *not* transferred.
	// - spec: The v1.Volume spec
	// - pod: The enclosing pod
	NewMounter(spec *Spec, podRef *v1.Pod, opts VolumeOptions) (Mounter, error)

	// NewUnmounter creates a new volume.Unmounter from recoverable state.
	// - name: The volume name, as per the v1.Volume spec.
	// - podUID: The UID of the enclosing pod
	NewUnmounter(name string, podUID types.UID) (Unmounter, error)

	// ConstructVolumeSpec constructs a volume spec based on the given volume name
	// and volumePath. The spec may have incomplete information due to limited
	// information from input. This function is used by volume manager to reconstruct
	// volume spec by reading the volume directories from disk
	ConstructVolumeSpec(volumeName, volumePath string) (*Spec, error)

	// SupportsMountOption returns true if volume plugins supports Mount options
	// Specifying mount options in a volume plugin that doesn't support
	// user specified mount options will result in error creating persistent volumes
	SupportsMountOption() bool

	// SupportsBulkVolumeVerification checks if volume plugin type is capable
	// of enabling bulk polling of all nodes. This can speed up verification of
	// attached volumes by quite a bit, but underlying pluging must support it.
	SupportsBulkVolumeVerification() bool
}

ボリュームの作成・マウントに関する機能は NewMounter です。
NewMounter はボリュームの種類に応じた Mounter を生成し、Mounter が実際のボリュームの作成・マウントを行います。

同様に NewUnmounter はボリュームの削除・アンマウントを行う Unmounter を生成します。

Mounter & Unmounter

Mounter、Unmounter の機能をインタフェースの定義から確認します。(ソースはこちら

// Mounter interface provides methods to set up/mount the volume.
type Mounter interface {
	// Uses Interface to provide the path for Docker binds.
	Volume

	// CanMount is called immediately prior to Setup to check if
	// the required components (binaries, etc.) are available on
	// the underlying node to complete the subsequent SetUp (mount)
	// operation. If CanMount returns error, the mount operation is
	// aborted and an event is generated indicating that the node
	// does not have the required binaries to complete the mount.
	// If CanMount succeeds, the mount operation continues
	// normally. The CanMount check can be enabled or disabled
	// using the experimental-check-mount-binaries binary flag
	CanMount() error

	// SetUp prepares and mounts/unpacks the volume to a
	// self-determined directory path. The mount point and its
	// content should be owned by `fsUser` or 'fsGroup' so that it can be
	// accessed by the pod. This may be called more than once, so
	// implementations must be idempotent.
	// It could return following types of errors:
	//   - TransientOperationFailure
	//   - UncertainProgressError
	//   - Error of any other type should be considered a final error
	SetUp(mounterArgs MounterArgs) error

	// SetUpAt prepares and mounts/unpacks the volume to the
	// specified directory path, which may or may not exist yet.
	// The mount point and its content should be owned by `fsUser`
	// 'fsGroup' so that it can be accessed by the pod. This may
	// be called more than once, so implementations must be
	// idempotent.
	SetUpAt(dir string, mounterArgs MounterArgs) error
	// GetAttributes returns the attributes of the mounter.
	// This function is called after SetUp()/SetUpAt().
	GetAttributes() Attributes
}

Mounter にはパスを指定してボリュームを作成する SetUpAt、パスを指定しないで作成する SetUp が用意されています。
SetUp からパスを指定して SetUpAt を呼び出す実装が多いようです。

CanMount では該当 Node でボリュームを作成・マウント可能かどうかを確認します。
ほとんどは nil を返すだけの実装ですが、NFS では OS の種類(Linux / Windows)に応じたテストコマンドを実行しています。

またGetAttributes では以下の値を返します。

// Attributes represents the attributes of this mounter.
type Attributes struct {
	ReadOnly        bool
	Managed         bool
	SupportsSELinux bool
}

続いて Unmounter を軽く見ていきます。

// Unmounter interface provides methods to cleanup/unmount the volumes.
type Unmounter interface {
	Volume
	// TearDown unmounts the volume from a self-determined directory and
	// removes traces of the SetUp procedure.
	TearDown() error
	// TearDown unmounts the volume from the specified directory and
	// removes traces of the SetUp procedure.
	TearDownAt(dir string) error
}

Unmounter はボリュームの削除・アンマウントの機能を提供します。
Mounter と同様にパス指定、パス未指定のメソッドがそれぞれ用意されています。

実装

VolumePlugin の具体的な実装について、いくつか調べたものを紹介します。
Mounter の実装を中心に見ていきます。

EmptyDir

ソースはこちら

SetUp はパスを指定して SetUpAt を呼び出します。

func (ed *emptyDir) SetUp(mounterArgs volume.MounterArgs) error {
	return ed.SetUpAt(ed.GetPath(), mounterArgs)
}

ボリュームのパスは以下で生成し、/var/lib/kubelet/pods/{Pod の UID}/volumes/kubernetes.io~empty-dir/{ボリューム名} の形になります。

func getPath(uid types.UID, volName string, host volume.VolumeHost) string {
	return host.GetPodVolumeDir(uid, utilstrings.EscapeQualifiedName(emptyDirPluginName), volName)
}

SetUpAt は次の流れでボリューム(ディレクトリ)を作成・マウントします。

  1. ディレクトリが ready であるか確認し、ready の場合は何もしない
  2. pod.spec.volumes.emptyDir.medium の設定に従ってディレクトリを作成・マウント
  3. fsGroup が設定されている場合は所有者を設定
  4. ディレクトリを ready に設定
  5. ディレクトリに Quota を設定
// コメントは省略
func (ed *emptyDir) SetUpAt(dir string, mounterArgs volume.MounterArgs) error {
	notMnt, err := ed.mounter.IsLikelyNotMountPoint(dir)
	if err != nil && !os.IsNotExist(err) {
		return err
	}

	readyDir := ed.getMetaDir()
	if volumeutil.IsReady(readyDir) {  // 1
		if ed.medium == v1.StorageMediumMemory && !notMnt {
			return nil
		} else if ed.medium == v1.StorageMediumDefault {
			if _, err := os.Stat(dir); err == nil {
				return nil
			}
			klog.Warningf("volume ready file dir %s exist, but volume dir %s does not. Remove ready dir", readyDir, dir)
			if err := os.RemoveAll(readyDir); err != nil && !os.IsNotExist(err) {
				klog.Warningf("failed to remove ready dir [%s]: %v", readyDir, err)
			}
		}
	}

	switch {  // 2
	case ed.medium == v1.StorageMediumDefault:
		err = ed.setupDir(dir)
	case ed.medium == v1.StorageMediumMemory:
		err = ed.setupTmpfs(dir)
	case v1helper.IsHugePageMedium(ed.medium):
		err = ed.setupHugepages(dir)
	default:
		err = fmt.Errorf("unknown storage medium %q", ed.medium)
	}

	volume.SetVolumeOwnership(ed, mounterArgs.FsGroup, nil /*fsGroupChangePolicy*/, volumeutil.FSGroupCompleteHook(ed.plugin, nil))  // 3

	if err == nil {
		volumeutil.SetReady(ed.getMetaDir())  // 4
		if mounterArgs.DesiredSize != nil {
			hasQuotas, err := fsquota.SupportsQuotas(ed.mounter, dir)
			if err != nil {
				klog.V(3).Infof("Unable to check for quota support on %s: %s", dir, err.Error())
			} else if hasQuotas {
				klog.V(4).Infof("emptydir trying to assign quota %v on %s", mounterArgs.DesiredSize, dir)
				err := fsquota.AssignQuota(ed.mounter, dir, ed.pod.UID, mounterArgs.DesiredSize)  // 5
				if err != nil {
					klog.V(3).Infof("Set quota on %s failed %s", dir, err.Error())
				}
			}
		}
	}
	return err
}

実際のディレクトリの作成には os.MkdirAll を使用しています。
pod.spec.volumes.emptyDir.mediumMemory の場合は、mount コマンドで tmpfs にマウントします。
(Hugepages については省略)

ちなみにディレクトリの ready の確認は、/var/lib/kubelet/pods/{Pod の UID}/plugins/kubernetes.io~empty-dir/{ボリューム名} 以下の ready ファイルの有無を調べています。

ConfigMap

ソースはこちら

SetUp は EmptyDir と同様で、渡されるボリュームのパスは /var/lib/kubelet/pods/{Pod の UID}/volumes/kubernetes.io~configmap/{ボリューム名} になります。

func (b *configMapVolumeMounter) SetUp(mounterArgs volume.MounterArgs) error {
	return b.SetUpAt(b.GetPath(), mounterArgs)
}

ConfigMap のボリュームの実態は EmptyDir です。
SetUpAt は次の流れでボリュームを作成・マウントします。

  1. EmptyDir の WrapperMounter を生成
  2. ConfigMap を取得
  3. ConfigMap から作成するファイル用のデータ(payload)を生成
  4. EmptyDir の WrapperMounter でディレクトリを作成
  5. ディレクトリにファイルを作成
  6. fsGroup が設定されている場合は所有者を設定
// コメントは省略
func (b *configMapVolumeMounter) SetUpAt(dir string, mounterArgs volume.MounterArgs) error {
	klog.V(3).Infof("Setting up volume %v for pod %v at %v", b.volName, b.pod.UID, dir)

	wrapped, err := b.plugin.host.NewWrapperMounter(b.volName, wrappedVolumeSpec(), &b.pod, *b.opts)  // 1
	if err != nil {
		return err
	}

	optional := b.source.Optional != nil && *b.source.Optional
	configMap, err := b.getConfigMap(b.pod.Namespace, b.source.Name)  // 2
	if err != nil {
		if !(errors.IsNotFound(err) && optional) {
			klog.Errorf("Couldn't get configMap %v/%v: %v", b.pod.Namespace, b.source.Name, err)
			return err
		}
		configMap = &v1.ConfigMap{
			ObjectMeta: metav1.ObjectMeta{
				Namespace: b.pod.Namespace,
				Name:      b.source.Name,
			},
		}
	}

	totalBytes := totalBytes(configMap)
	klog.V(3).Infof("Received configMap %v/%v containing (%v) pieces of data, %v total bytes",
		b.pod.Namespace,
		b.source.Name,
		len(configMap.Data)+len(configMap.BinaryData),
		totalBytes)

	payload, err := MakePayload(b.source.Items, configMap, b.source.DefaultMode, optional)  // 3
	if err != nil {
		return err
	}

	setupSuccess := false
	if err := wrapped.SetUpAt(dir, mounterArgs); err != nil {  // 4
		return err
	}
	if err := volumeutil.MakeNestedMountpoints(b.volName, dir, b.pod); err != nil {
		return err
	}

	defer func() {
		if !setupSuccess {
			unmounter, unmountCreateErr := b.plugin.NewUnmounter(b.volName, b.podUID)
			if unmountCreateErr != nil {
				klog.Errorf("error cleaning up mount %s after failure. Create unmounter failed with %v", b.volName, unmountCreateErr)
				return
			}
			tearDownErr := unmounter.TearDown()
			if tearDownErr != nil {
				klog.Errorf("Error tearing down volume %s with : %v", b.volName, tearDownErr)
			}
		}
	}()

	writerContext := fmt.Sprintf("pod %v/%v volume %v", b.pod.Namespace, b.pod.Name, b.volName)
	writer, err := volumeutil.NewAtomicWriter(dir, writerContext)
	if err != nil {
		klog.Errorf("Error creating atomic writer: %v", err)
		return err
	}

	err = writer.Write(payload)  // 5
	if err != nil {
		klog.Errorf("Error writing payload to dir: %v", err)
		return err
	}

	err = volume.SetVolumeOwnership(b, mounterArgs.FsGroup, nil /*fsGroupChangePolicy*/, volumeutil.FSGroupCompleteHook(b.plugin, nil))  // 6
	if err != nil {
		klog.Errorf("Error applying volume ownership settings for group: %v", mounterArgs.FsGroup)
		return err
	}
	setupSuccess = true
	return nil
}

NewWrapperMounter はボリューム名にプレフィックスとして wrapped_ を付与した Mounter を生成します。
Mounter の生成には直接 EmptyDir を指定するのではなく、volume.Spec からプラグインを特定する方法を取ります。
ConfigMap では以下のように VolumeSource に EmptyDir を設定しているため、EmptyDir の Mounter が生成されます。

// コメントは省略
func wrappedVolumeSpec() volume.Spec {
	return volume.Spec{
		Volume: &v1.Volume{VolumeSource: v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{}}},
	}
}

EmptyDir の Mounter で空のディレクトリを作成したら、あとはファイルを作成するだけです。
ファイルの作成には ioutil.WriteFile os.Chmod os.Chown を使用します。

Secret

ソースはこちら

Secret は ConfigMap とほとんど同じ実装です。
EmptyDir の Mounter で作成したディレクトリにファイルを書き込みます。

ただし Secret のボリュームは tmpfs にマウントする必要があるため、volume.Spec で EmptyDir を設定する時に Medium: v1.StorageMediumMemory を指定しておきます。

func wrappedVolumeSpec() volume.Spec {
	return volume.Spec{
		Volume: &v1.Volume{VolumeSource: v1.VolumeSource{EmptyDir: &v1.EmptyDirVolumeSource{Medium: v1.StorageMediumMemory}}},
	}
}

ボリュームのパスは /var/lib/kubelet/pods/{Pod の UID}/volumes/kubernetes.io~secret/{ボリューム名} になります。

Projected

ソースはこちら

Projected は Secret とほとんど同じ実装です。
ConfigMap / Secret/ DownwardAPI / ServiceAccountToken のデータを取得し、tmpfs にマウントしたディレクトリに書き込みます。

VolumePlugin の種類

通常の VolumePlugin の他に、VolumePlugin を埋め込んだインタフェースが複数用意されています。
以下はその一例です。

  • PersistentVolumePlugin:GetAccessModes を実装(hostPath / Amazon EBS など)
  • RecyclableVolumePlugin:Recycle を実装(hostPath / NFS など)
  • ProvisionableVolumePlugin:NewProvisioner を実装(hostPath / Amazon EBS など)

※ hostPath は通常のボリュームと PV の両方の実装を含む

今回取り扱ったのは通常の VolumePlugin のみですので、VolumePlugin の実装によっては紹介した内容と異なるものがあります。

おわりに

Kubernetes を利用する上であまり意識することのない部分ですが、しくみを理解していればトラブルシューティング等での助けになるのではないかと思います。
ConfigMap / Secret のマウントについては以前から気になっていたので、今回詳しく知ることができて満足です。

ご指摘等ありましたらよろしくお願いします。

8
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?