はじめに
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
は次の流れでボリューム(ディレクトリ)を作成・マウントします。
- ディレクトリが ready であるか確認し、ready の場合は何もしない
-
pod.spec.volumes.emptyDir.medium
の設定に従ってディレクトリを作成・マウント - fsGroup が設定されている場合は所有者を設定
- ディレクトリを ready に設定
- ディレクトリに 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.medium
が Memory
の場合は、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
は次の流れでボリュームを作成・マウントします。
- EmptyDir の WrapperMounter を生成
- ConfigMap を取得
- ConfigMap から作成するファイル用のデータ(payload)を生成
- EmptyDir の WrapperMounter でディレクトリを作成
- ディレクトリにファイルを作成
- 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 のマウントについては以前から気になっていたので、今回詳しく知ることができて満足です。
ご指摘等ありましたらよろしくお願いします。