アットランタイム

Mount Propagation 備忘録

はじめに

Mount Propagation の備忘録です。

Mount Propagation

Linux の bind mount では、mount ポイントをどのように反映させるかのオプションが存在する。 コンテナ関連では、Docker のバインドマウントや Kubernetes のボリュームにおける下位レイヤーの動作として、Mount Propagation は重要になることがある。

具体的には、shared / slave / private / unbindable の 4 種類がある。 3 つのプロパティはサブマウントを制御するのに対して、unbindable はサブマウントに加えてバインドマウントについても禁止する点で異なる。

Docker のドキュメントがわかりやすい。 r は再起的(recursive)の略である。

Propagation settingDescription
sharedSub-mounts of the original mount are exposed to replica mounts, and sub-mounts of replica mounts are also propagated to the original mount.
slaveSimilar to a shared mount, but only in one direction. If the original mount exposes a sub-mount, the replica mount can see it. However, if the replica mount exposes a sub-mount, the original mount cannot see it.
privateThe mount is private. Sub-mounts within it are not exposed to replica mounts, and sub-mounts of replica mounts are not exposed to the original mount.
rsharedThe same as shared, but the propagation also extends to and from mount points nested within any of the original or replica mount points.
rslaveThe same as slave, but the propagation also extends to and from mount points nested within any of the original or replica mount points.
rprivateThe default. The same as private, meaning that no mount points anywhere within the original or replica mount points propagate in either direction.

マウント元が rshare の場合:

  • ルートディレクトリなどのマウントポイント(マウント元)が rshared に設定されており、バインドマウント先が rshared の場合、サブマウントは相互に伝播(propagate)される。
  • マウント元が rshared に設定されており、バインドマウント先が rslave の場合、マウント元で作成されたサブマウントはバインドマウント先にも伝播されるが、その逆は伝播されない。
  • マウント元が rshared に設定されていても、バインドマウント先が rprivate の場合、いずれのサブマウントも相互に伝播しない。

マウント元が rprivate の場合:

  • ルートディレクトリなどのマウントポイント(マウント元)が rprivate に設定されていると、バインドマウント先の設定に関わらず、いずれのサブマウントも相互に伝播しない

マウント元が rslave の場合:

  • ルートディレクトリなどのマウントポイント(マウント元)が rslave に設定されており、マウント先が rslave/rshared の場合、マウント元のサブマウントはマウント先に伝播する(マウント元のマウント元にはサブマウントは反映されない)
  • マウント元が rslave に設定されており、マウント先が rprivate の場合、いずれのサブマウントも相互に伝播しない。

マウント元とマウント先でそれぞれ制御できる点が特徴と言える。

例)Kubernetes のボリューム

k8s では、上記マウントオプションに対応した None(rpviate)、HostToContainer(rslave)、Bidirectional(rshared) がある

emptydir を使用してコンテナ 1 とコンテナ 2 で共有のボリュームを作成した場合の動作は次のようになる。

  • 前提: ホストの VM のルートディレクトリのマウントポイントが shared に設定されている。
  • kubelet は emptydir に対応するディレクトリを作成する。これがバインドマウントのマウント元となり、ルートディレクトリのマウントポイントに属するため shared となっている。
    • /var/lib/kubelet/pods/<ID></volumes/kubernetes.io~empty-dir/<ボリューム名>
  • kubelet は cri 等を経由して 2 つのコンテナを作成する。それぞれのコンテナでは、最終的に runtime-spec の mounts の source に前述のディレクトリを、destination に Pod のコンテナ定義のディレクトリを指定し、options に対応する propagation が設定され、runc などがバインドマウント・propagation を実行する。

上記踏まえ、例えば、サイドカーコンテナで NFS をマウントし、メインコンテナで NFS を参照する場合、次の設定が必要となる(コンテナのそれぞれのボリュームは兄弟のマウントポイントとも言える)。

  1. サイドカーコンテナのボリュームに rshare(Bidirectional) を設定する
    • サイドカーの NFS マウントがホストのマウント元にも伝播する
  2. メインコンテナのボリュームに rslave(HostToContainer)を設定する
    • ホストに伝播された NFS マウントがメインコンテナにも伝播するので参照可能となる

CSI ドライバーでは、ノード上で NFS 等の対象をマウントをし、kubelet はコンテナのボリュームを rslave に設定することで、対象のマウントがコンテナのマウント先に伝播する。

Docker のバインドマウントとボリューム

参考程度に、バインドマウントとボリュームについて、マウントレベルで違いを見ていく。

バインドマウント

バインドマウントは mount –bind と同等の比較的プリミティブ操作である。 ホスト(source)のディレクトリ(やファイル)をコンテナのディレクトリ(destination)にバインドマウントする。 mount の –bind の制約により、元々コンテナ上にあったディレクトリは、ホストのディレクトリがマウントによって参照できなくなる。 例) コンテナに /var/log/httpd/error.log があり、ホストの /var/log/httpd/ をコンテナの /var/log にマウントすると、コンテナの httpd ディレクトリは参照できなくなる

また、ホストとコンテナのパーミッションの不一致など、問題を引き起こすことがあり Docker であればボリュームの使用が推奨とされている。 なお、バインドマウントはプリミティブな操作ので runtime-spec の mounts フィールドにほぼほぼ類似している。

Volume

Docker が用意したディレクトリを source として、コンテナのディレクトリ(destination)に対してバインドマウントする。 Docker が用意したディレクトリはバインドマウントと同様にホストのディレクトリであるが、Docker が管理するため、バインドマウントとは異なる動きをする。 具体的に、次のように、コンテナのディレクトリ(destination)を、ホストのディレクトリ(source)にコピーした後、バインドマウントされる。これをボリュームと呼んでいる。

  1. コンテナのディレクトリやファイル(destination)を Docker が用意したディレクトリにコピーする。この際、権限や編集時刻も維持される
  2. コピーされたホストのディレクトリを source として、コンテナのディレクトリ(destination)にバインドマウントする

これは Docker が提供している機能であり、containerd、OCI image-spec や OCI runtime-spec で定義されている仕様ではない。 OCI runtime-spec では、バインドマウント同様 2. の操作のみが行われる。

https://github.com/moby/moby/blob/v28.2.1/container/container_unix.go#L126

// CopyImagePathContent copies files in destination to the volume.
func (container *Container) CopyImagePathContent(volumePath, destination string) error {
	if err := label.Relabel(volumePath, container.MountLabel, true); err != nil && !errors.Is(err, syscall.ENOTSUP) {
		return err
	}
	return copyExistingContents(destination, volumePath)
}
// copyExistingContents copies from the source to the destination and
// ensures the ownership is appropriately set.
func copyExistingContents(source, destination string) error {
	dstList, err := os.ReadDir(destination)
	if err != nil {
		return err
	}
	if len(dstList) != 0 {
		log.G(context.TODO()).WithFields(log.Fields{
			"source":      source,
			"destination": destination,
		}).Debug("destination is not empty, do not copy")
		return nil
	}

	return fs.CopyDir(destination, source, ignoreUnsupportedXAttrs())
}

具体的なコピー操作は dirCopyDirCopyで行われる。

–volumes-from

Docker ではあるコンテナのボリュームを別のコンテナから参照・共有できる仕組みがある。 これは、コンテナが持つボリュームに対応したホストのディレクトリを source として、追加で参照したいコンテナのディレクトリを destination としてバインドマウントして実現する。 したがって、追加のコンテナにある既存のディレクトリの内容は参照できなくなる。

Dockerfile の VOLUME

Dockerfile で VOLUME を指定するとそのディレクトリは OCI image-spec の Image Config 仕様の Volumesにマップされる。 Volumes にはコンテナのディレクトリのみが指定されおりそれ以上の情報はない。Docker 等のランタイムはその情報と、ランタイムの仕様従い追加で、Docker Volume のような特別な動作を行った上でバインドマウントする。