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

# はじめに

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

# 機能

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

VolumePlugin は以下のように定義されています。（[ソースはこちら](https://github.com/kubernetes/kubernetes/blob/master/pkg/volume/plugins.go)）

```golang
// 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 の機能をインタフェースの定義から確認します。（[ソースはこちら](https://github.com/kubernetes/kubernetes/blob/master/pkg/volume/volume.go)）

```golang
// 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` では以下の値を返します。

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

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

```golang
// 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

[ソースはこちら](https://github.com/kubernetes/kubernetes/blob/master/pkg/volume/emptydir/empty_dir.go)

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

```golang
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/{ボリューム名}` の形になります。

```golang
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 を設定

```golang
// コメントは省略
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.medium` が `Memory` の場合は、`mount` コマンドで tmpfs にマウントします。
（Hugepages については省略）

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

## ConfigMap

[ソースはこちら](https://github.com/kubernetes/kubernetes/blob/master/pkg/volume/configmap/configmap.go)

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

```golang
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 が設定されている場合は所有者を設定

```golang
// コメントは省略
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 が生成されます。

```golang
// コメントは省略
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

[ソースはこちら](https://github.com/kubernetes/kubernetes/blob/master/pkg/volume/secret/secret.go)

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

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

```golang
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

[ソースはこちら](https://github.com/kubernetes/kubernetes/blob/master/pkg/volume/projected/projected.go)

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 のマウントについては以前から気になっていたので、今回詳しく知ることができて満足です。

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