mount propagation
特定のマウント処理を他のディレクトリやnamespaceで認識できるようにするかどうかを、制御する仕組みとしてshared subtreeが存在する。
例としてプロセスのカレントディレクトリや、chrootで変更した配下に対して、外部ディレクトリのマウント処理を反映したい場合などが挙げられる。 また特定のマウントポイント配下に更にマウントポイントを追加した場合に、マウント元にディレクトリには反映されるかどうか などがある。
これらをshared subtreeのpropagationという仕組みによって、mountに関するインベントの伝搬を制御することが可能となる。
Peer group
propagationではpeer groupという概念が存在しpeer group間でマウントイベントが伝搬される。 peer groupは複数のmount pointの集合であり、以下を契機にメンバが追加される。
- 新しいnamespaceの作成
- これによってマウントポイントが新しいnamepsaceに複製されるので、それぞれのマウントポイントは同一のpeer groupに所属する
- bind mountのソースとして利用されたとき
メンバが削除されるトリガーは以下のものがある - unmountの実行 - namespaceの削除 - namespace内のプロセスがすべて終了した - namespace内のプロセスすべて該当namespace外に移動した
4つのpropagation type
マウントポイントはpropagation typeという属性を持っている。 これはマウントイベントをpeer group内に伝搬するかどうかを制御するために設定され4つ種類がある。
- shared
- private
- slave
- unvindable
そしてデフォルトでは、親のマウントポイントのpropagation typeを継承する。
それぞれをマウントポイントに設定した場合にどのような挙動になるのかを確認する。
shared
同じピアグループ内でマウントイベントは伝搬され共有される。 マウントポイント配下にマウントポイントが追加削除された際は、同じpeer group内のマウントポイント配下でも反映される。
オプションとしてmountコマンドに--makes-shared
を付加する。
下記の例ではまず、/A
を/B
にbind マウントした。
$ mkdir /A /B $ touch /A/test $ tree /A /B C /A └── test /B $ mount --bind --make-shared /A /B $ tree /A /B /A └── test /B └── test
各マウントポイントの情報は/proc/{PID}/mountinfo
で参照できる.
下記の例ではshared:1
となっておりsharedはpropagation typeがsharedで、peer groupが1であることを表している。この数字が同じであれば同じpeer groupに所属する。
# /proc/self/mountinfo 実行結果を抜粋 85 40 8:1 /A /B rw,relatime shared:1 - xfs /dev/sda1 rw,seclabel,attr2,inode64,noquota
/C
を作成し、/B/mnt
にマウントする。すると/A
配下でもmnt/test2
を参照することがき、マウント情報が伝搬され反映されていることが確認できる。
$ mkdir /C /B/mnt $ touch /C/test2 $ tree /A /B /C /A └── test /B ├── mnt └── test /C └── test2 $ mount --bind --make-shared /C /B/mnt/ $ tree /A /B /C /A ├── mnt │ └── test2 └── test /B ├── mnt │ └── test2 └── test /C └── test2
マウント情報にはコマンド実行した/A
をマウントポイントとした情報以外に、伝搬された情報である/A/mnt
も記載されており、いずれのpeer group番号は1となっている。
# /proc/self/mountinfo 実行結果を抜粋 85 40 8:1 /A /B rw,relatime shared:1 - xfs /dev/sda1 rw,seclabel,attr2,inode64,noquota 91 85 8:1 /C /B/mnt rw,relatime shared:1 - xfs /dev/sda1 rw,seclabel,attr2,inode64,noquota 92 40 8:1 /C /A/mnt rw,relatime shared:1 - xfs /dev/sda1 rw,seclabel,attr2,inode64,noquota
private
aharedとは対象的にピアグループ内にイベントは共有されない。
/A
を/B
にbind mountして作業前の初期状態とする。
マウンポイントをprivateとするためには、オプションとして--make-private
をmountコマンドに使用する。
$ tree /A /B /C /A └── test /B /C └── test2 $ mount --bind --make-private /A /B tree /A /B /C /A └── test /B └── test /C └── test2
マウント情報を確認するとprivateではpeer groupに関する情報は記載されていないことがわかる。
# /proc/self/mountinfo 実行結果を抜粋 85 40 8:1 /A /B rw,relatime - xfs /dev/sda1 rw,seclabel,attr2,inode64,noquota
/C
を/B/mnt/
にマウントしても、/A/mnt/test2
は参照できず/A
には反映されていない。
$ mount --bind /C /B/mnt/ $ tree /A /B /C /A ├── mnt └── test /B ├── mnt │ └── test2 └── test /C └── test2
マウント情報は実行したコマンドと同様の2種類のエントリが存在する。
# /proc/self/mountinfo 実行結果を抜粋 85 40 8:1 /A /B rw,relatime - xfs /dev/sda1 rw,seclabel,attr2,inode64,noquota 87 85 8:1 /C /B/mnt rw,relatime shared:1 - xfs /dev/sda1 rw,seclabel,attr2,inode64,noquota
逆に/A/mnt
に/C
をマウントしてみるとこれも同じ結果となり反映されない
$ tree /A /B /C /A ├── mnt └── test /B /C └── test2 $ mount --bind --make-private /A /B $ tree /A /B /C /A ├── mnt └── test /B ├── mnt └── test /C └── test2
$ mount --bind /C /A/mnt/ $ tree /A /B /C /A ├── mnt │ └── test2 └── test /B ├── mnt └── test /C └── test2
85 40 8:1 /A /B rw,relatime - xfs /dev/sda1 rw,seclabel,attr2,inode64,noquota 87 40 8:1 /C /A/mnt rw,relatime shared:1 - xfs /dev/sda1 rw,seclabel,attr2,inode64,noquota
slave
イベントの伝播は一方通行となる。同じpeer group内でマスタとなるマウントポイントが変更された場合に、スレーブ側のマウントポイントも変更される。 しかしその逆は発生しない。
マスタ側のディスクドライブマウントなどをスレーブ側で利用したい場合で、かつ逆にスレーブ側の変更で影響を与えたくない場合に利用する。
bindコマンドに--make-slave
オプションを利用して/A
を/B
にマウントする。
$ tree /A /B /C /A └── test /B /C └── test2 $ mount --bind --make-slave /A /B tree /A /B /C /A └── test /B └── test /C └── test2
マウント情報にはmaster:1
# /proc/self/mountinfo 実行結果を抜粋 85 40 8:1 /A /B rw,relatime master:1 - xfs /dev/sda1 rw,seclabel,attr2,inode64,noquota
/C
を/B/mnt
にsharedマウントすると、/A
配下には反映されていない。
$ mkdir /B/mnt $ mount --bind /C /B/mnt $ tree /A /B /C /A ├── mnt └── test /B ├── mnt │ └── test2 └── test /C └── test2
85 40 8:1 /A /B rw,relatime master:1 - xfs /dev/sda1 rw,seclabel,attr2,inode64,noquota 87 85 8:1 /C /B/mnt rw,relatime shared:1 - xfs /dev/sda1 rw,seclabel,attr2,inode64,noquota
一旦/B/mnt
をunmountして元の状態に戻す。
$ umount /B/mnt/ $ tree /A /B /C /A ├── mnt └── test /B ├── mnt └── test /C └── test2
そして今度は逆に/A配下にマウントする。
この場合は/A
がmasterとなっているので、マウント情報は/B
に伝搬され/B/mnt/test2
の存在が確認できる。
$ mount --bind /C /A/mnt/ $ tree /A /B /C /A ├── mnt │ └── test2 └── test /B ├── mnt │ └── test2 └── test /C └── test2
unvindable
privateと同様にマウントイベントの伝播はしないが、その挙動に加えてbind mountのソースとして利用することができなくなるという制約が追加される。
mountコマンドの実行時には--make-unbindable
を付与する。ここでは/A
を/B
にマウントする。
$ tree /A /B /C /A └── test /B /C └── test2 $ mount --bind --make-unbindable /A /B $ tree /A /B /C /A └── test /B └── test /C └── test2
マウント情報にはunbindableであることが記されている。
85 40 8:1 /A /B rw,relatime unbindable - xfs /dev/sda1 rw,seclabel,attr2,inode64,noquota
当然privateと同様の挙動のため、/C
を/B/mnt
にマウントしても/A/mnt
では参照することはできない。
$ mkdir /B/mnt $ tree /A /B /C /A ├── mnt └── test /B ├── mnt └── test /C └── test2
さらに/B
を/C/mnt
をマウントしようとしてもbind mountのソースとして利用できないため失敗する。
$ mkdir /C/mnt $ tree /A /B /C /A ├── mnt └── test /B ├── mnt └── test /C ├── mnt └── test2 $ mount --bind /B /C/mnt/ $ mount: wrong fs type, bad option, bad superblock on /B, missing codepage or helper program, or other error In some cases useful info is found in syslog - try dmesg | tail or so.
unvindableは再起にマウントした際にマウントポイントが指数関数的に増加するのを防ぐ目的で利用されるようだ。
/A
配下に2つのディレクトリを用意した。まずは/A
を配下のdir1
にマウントする。
$ tree /A /A ├── dir1 └── dir2 $ mount --rbind /A /A/dir1 /A ├── dir1 │ ├── dir1 │ └── dir2 └── dir2
そして次は/A
を/A/dir2
にマウントする
$ mount --rbind /A /A/dir2 $ tree /A /A ├── dir1 │ ├── dir1 │ └── dir2 │ ├── dir1 │ │ ├── dir1 │ │ └── dir2 │ └── dir2 └── dir2 ├── dir1 │ ├── dir1 │ └── dir2 └── dir2
マウントコマンドを2回実行したがこのときのmountinfoはこのようになっている。 手動実行分以外に再帰的なマウントポイントが増えていることがわかる。同時にこれを繰り返せばマウントポイントの量が指数関数的に増加するのも納得がいく。
そのために途中のマウントポイントをバインドマウント不可とすることで、爆発的な増加を防止する仕組みがunbindableというわけだ。
85 40 8:1 /A /A/dir1 rw,relatime shared:1 - xfs /dev/sda1 rw,seclabel,attr2,inode64,noquota 87 40 8:1 /A /A/dir2 rw,relatime shared:1 - xfs /dev/sda1 rw,seclabel,attr2,inode64,noquota 88 87 8:1 /A /A/dir2/dir1 rw,relatime shared:1 - xfs /dev/sda1 rw,seclabel,attr2,inode64,noquota 89 85 8:1 /A /A/dir1/dir2 rw,relatime shared:1 - xfs /dev/sda1 rw,seclabel,attr2,inode64,noquota 90 89 8:1 /A /A/dir1/dir2/dir1 rw,relatime shared:1 - xfs /dev/sda1 rw,seclabel,attr2,inode64,noquota
参照
- Linux Kernel Doc
- LWN
- Mount namespaces and shared subtrees
- Shared subtrees
- Mount namespaces, mount propagation, and unbindable mounts
【書籍】 Programming Kubernetes
を読んだので内容の中で、新たな知見となった部分をメモ程度にまとめておく。
この本の概要・対象読者
以下のような人たちは読んでおくと良いです。 - KubernetesAPIを利用したアプリケーション開発をしようとしている人 - Kubernetesのコントローラなどの開発をしようとするひと
多少内容が古く感じる部分はありますが、そういった部分は読み飛ばしてしまってよいと思います。
各章の概要
Chapter 3. Bacis of client-go
Kubernetesのクライアントの処理ををコードレベルから詳しく説明している。
中心となっているのはクライアント操作を紐解く中で重要になる以下の2つのリポジトリであった。 - https://github.com/kubernetes/client-go - https://github.com/kubernetes/api - https://github.com/kubernetes/apimachinery
Kubernetesを扱う上で、理解するべき点が多くの本の中で一番重要な章だと感じた。
とはいえ、アプリケーションを主業務としていてプラットフォームとしてKubernetesを使う人はここまで知る必要はなく、どちらかというとこの本のターゲットであるKubernetesや関連コンポーネントの開発に携わる人が知っておきたい内容といった感じ。
Chapter 4. Using Custom Resourcesa
カスタムリソースとは何かという点から、コントローラやクライアント視点からカスタムリソースがどういった機能を持つかについて書かれていた。 - バリデーション - リソース名の省略形 - 表示カラムの変更 - サブリソース
またA Developer’s View on Custom Resources
ではDynamic Client, Typed Clientの詳細や使い分けについて触れられている。
Chapter 5. Automating Code Generation
golanは単純な言語で型を意識せずに汎用的なアルゴリズムを実装することは難しいためコードジェネレータを使用する。 - https://github.com/kubernetes/code-generator
この章ではcode-generatorの使い方とそのタグが持つ意味を解説している。
Chapter 6. Solutions for Writing Operators
カスタムコントローラを記述するための、code-generatorより更に上位レイヤーの自動化ツールについてサンプルレポジトリを題材に使い方を紹介している。
紹介されているツールは以下の通り - code-generator - Kubebuilder - Operator SDK
ただ2022年現在この本の内容はすでに古く感じた。例えばOperator SDKに関してはすでにKuberbuilderと統合されているし、コマンド体型も変更されている。
実際に使用する際は各ツールのドキュメントを読んだほうが理解が進みそうであった。
Chapter 7. Shipping Controllers and Operators
カスタムリソースに限らずKubernetesリソースのパッケージングやCI/CDについて書かれた章である。
パッケージ周りでは2つのツールが紹介されていた。 - Helm - Kustomize
加えてカスタムコントローラをデプロイする際のベストプラクティスが紹介されているが、てアカウントの権限を最小限にしたり、テストを自動化したりなど パッケージングツールの話を含めて、業務で扱っている人にとっては当たり前の内容が多い印象であった。
Chapter 8. Custom API Servers
Kubernetesでオリジナルのリソースを追加したい場合はCRDとCustom API serverを実装する方法があるが、その後者について解説している。
正直この章の内容が本書の中で一番濃く感じた。Custom API Serverの実装はkube-apiserverと構造として近いものがあり、その理解の手助けとなる章であった。
主に対象リソースへのリクエストがkube-apiserverに到達してから、レスポンスを返すまでにどういった処理が実行されているか詳しく学ぶことができた。
仮想マシンの初期設定をするcloud-init
cloud-initとは
OpenStackやAWSのEC2などIaaSを利用しVM(Virtual Machine)の払い出しをした後、ユーザアカウントの作成やソフトウェアのインストールなどの初期設定を自動化したくなる。
構成管理ツールとしてAnsibleやChefが存在し、ロールベースの複雑なセットアップはこういったツールの担当となるが、構成管理ツールを利用するまでの最低限の初期セットアップはどうしても発生してしまう。 そんなときに利用するのがloud-initという仕組みで、これを利用することクラウドプロバイダーから払い出したVMの初期セットアップを自動化することができる。
clud-initは各クラウドプラットフォームに依存することなくVM起動後に対し指定したユーザの作成やssh鍵の設定、任意のコマンドを実行するための共通インターフェースを提供している。
またcloud-initはUbuntuでおなじみのCanonicalが主体となって開発するオープンソースプロジェクトで特定のディストリビューションに依存することなく利用することができる。
処理の流れ
セットアップに使用する情報の入力
cloud-initは対象VMにインストールするOSイメージにcloud-initパッケージが含まれている必要があるが、各ディストリビューションが公開してるcloud imageには最初からインストーされている。
cloud-initは入力となる設定ファイルを何かしらの方法で受け取りそれをもとにセットアッププロセスを実行する。 設定ファイルの取得元(データストアと表現される)は様々でかくクラウドプロバイダに依存したものから、CDROMイメージを利用する汎用的な仕組みまで存在する。
例えばAWSのEC2を例に上げると、インスタンス起動後にcloud-initのプロセスはhttp://169.254.169.254/${EC2-VERSION}/meta-data/
にアクセスすることによって、実行に必要なデータを取得する。このあたりの処理はcloud-initの実装ではdatasourceとして抽象化されているため、メイン処理中では意識する必要はない。
入力となるデータは主に3つに分類される cloud-intでは設定内容はユーザから指定されたデータによって具体的にセットアップ内容を指定することができる。そのデータはメタデータ、ユーザデータ、ベンダデータに区別され、それぞれの内容を統合して最終的な実行内容を判断する。 - メタデータ - クラウドプロバイダーによって提示されるデータでVM名やアーキテクチャ、ディストリビューションなどプロパイダによって差異はあるが様々なインスタンス特有のデータがAPIを経由してアクセスできるようになっている。 - ユーザデータ - 実行してほしい指定したシェルスクリプトや、初期セットアップにに必要な引数などがユーザから渡される。 - ベンダデータ - クラウドプロバイダーから提供されているデータ。クラウドプロバイダーで必要となる特定の設定を挿入することができる。ユーザデータでベンダデータで提供されている項目を上書きすることができる。
セットアップの実行
ドキュメントによるとcloud-initは5つのboot stageから構成されており以下の順で実行される。
- Generator:
- systemdの仕組みでcloud-initのGenerator以降の実行の必要があるかを決定する
- 以下の場合はcloud-initは実行されない
/etc/cloud/cloud-init.disable
が存在する場合- カーネル引数に
cloud-init=disabled
が指定されている場合
- Local
- local datastorceを検索してネットワーク設定を適用する。
- 以降の処理でネットワーク利用の利用が可能な状態にする。
- Network
- ユーザデータを読み込み
cloud_init_modules
のモジュールを実行する。 - syslogやホスト名の設定が実施される。
- ユーザデータを読み込み
- Config
cloud_config_modules
を実行する。- システムの起動前に必要なssh関連の設定, ディスク構成のセットアップ, タイムゾーンの設定などをが含まれる。
- Final
cloud_final_modules
を実行する。- システムの起動後に実行するスクリプトやパッケージの追加を実施する。
起動スクリプトの確認
上記までのセットアップの流れが実際にどのように記述されているのかをcloud-initをインストールした筐体で確認する。 まずcloud-init関連のサービスとして以下のものが登録されているようだ。
$ systemctl list-unit-files --type=service |grep cloud- cloud-config.service enabled disabled cloud-final.service enabled disabled cloud-init-hotplugd.service static - cloud-init-local.service enabled disabled cloud-init.service enabled disabled
上記のサービスは各boot stageに対応する。例えばcloud-init.service
はcloud-init-local.service
の後に実行され/usr/bin/cloud-init init
を実行していることがわかる。
# /usr/lib/systemd/system/cloud-init.service [Unit] Description=Initial cloud-init job (metadata service crawler) DefaultDependencies=no Wants=cloud-init-local.service Wants=sshd-keygen.service Wants=sshd.service After=cloud-init-local.service After=systemd-networkd-wait-online.service After=network.service After=NetworkManager.service Before=network-online.target Before=sshd-keygen.service Before=sshd.service Before=systemd-user-sessions.service [Service] Type=oneshot ExecStart=/usr/bin/cloud-init init RemainAfterExit=yes TimeoutSec=0 # Output needs to appear in instance console output StandardOutput=journal+console [Install] WantedBy=cloud-init.target
また各ステージでの具体的な実行内容については/etc/cloud/cloud.cfg
に定義されている。
# The modules that run in the 'init' stage cloud_init_modules: - migrator - seed_random - bootcmd - write-files - growpart - resizefs - disk_setup - mounts - set_hostname - update_hostname - update_etc_hosts - ca-certs - rsyslog - users-groups - ssh # The modules that run in the 'config' stage cloud_config_modules: - ssh-import-id - keyboard - locale - set-passwords - spacewalk - yum-add-repo - ntp - timezone - disable-ec2-metadata - runcmd # The modules that run in the 'final' stage cloud_final_modules: - package-update-upgrade-install - write-files-deferred - puppet - chef - mcollective - salt-minion - reset_rmc - refresh_rmc_and_interface - rightscale_userdata - scripts-vendor - scripts-per-once - scripts-per-boot - scripts-per-instance - scripts-user - ssh-authkey-fingerprints - keys-to-console - install-hotplug - phone-home - final-message - power-state-change
各ステージで実行するモジュールは下記に実装されている。 - https://github.com/canonical/cloud-init/tree/main/cloudinit/config
まとめ
cloud-initは各クラウドプロバイダーを経由しクラウドプロバイダーやユーザからの設定の入力を共通化している。 そして実行タイミングの異なるステージで設定内容を反映した形で、セットアップスクリプトを実行するという動作になっている。
次回は実際にcloud-initを動作させてみる。
cloud-initのNoCloudを使ってお試し環境を簡単に構築する
以前cloud-initについて紹介をした。
cloud-initの仕組みはVMを決まったユーザや鍵を使ってセットアップしたり、指定スクリプトを実行したりとクラウドプロバイダ経由でなくても利用したい機能である。
自宅にOpenStackを構築していれば、DatastoreとしてOpenStackが有力な候補となるかもしれないが、「そんなもの自宅にはない」もしくは「もう少しライトにVMを使ってる」という人がほとんどだろう。
そこでNoCloudというDatastoreを使ってqemu/kvmで構築したVMに対してcloud-initを活用してセットアップする。
DatastoreとしてのNocloud
NoCloudはcloud-initのDatastoreとしては少し特殊で、ネットワークを経由せずに起動VMに対してデータを与える役割を担っている。
ではどうやってデータを渡すかというとVFATのローカルディスク経由もしくは、iso9660で外部から与える事ができる。
VM起動確認
まずcloud-initを確認するためのベースイメージを用意する。各ディストリビューションでAWSなどのクラウドプロバイダ上での動作を想定したcloud imageが配布されているが、これを利用すればcloud-initが最初からインストールされているため簡単に利用することができる。
今回はFedoraのcloud imageを利用することにした。 - https://alt.fedoraproject.org/cloud/
NoCloudを利用するためにはuser-data
とmeta-data
をディスクイメージに含める必要がある。
まずmeta-dataを作成する。 ここで指定されている、intance-idについては初回起動かどうかを識別するためのものとドキュメントに記載がある。
Note: that the instance-id provided (iid-local01 above) is what is used to determine if this is “first boot”. So if you are making updates to user-data you will also have to change that, or start the disk fresh.
local-hostanmeはその名の初期ホスト名として利用される。
$ cat << 'EOF' > meta-data instance-id: test-vm local-hostname: test-vm
次にuser-dataを作成する。
ここにはログイン情報などを記載するがせっかくなので、パスワードに加えてsshの公開鍵認証でログインできるように設定する。
今回は最小限のログイン情報を設定したが、様々な設定が可能なのでこちらのドキュメントを参照してほしい。
cat << 'EOF' > user-data #cloud-config users: - name: test groups: users, wheel lock_passwd: False plain_text_passwd: test ssh_pwauth: True ssh_authorized_keys: - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCqg4+5ISX6LuYfF+197BhGLaPGhtpsuIdl0YM26g7b/rt80l6KdccVqmDbv27dD6gfGx2l5DFFIGb1rPLIUnJ8NCsV3zQWivSRac/8thidRzHWLb/fLvmyOA2LqYeKv2SUubbtQVnm972w+gHAukALitmWp251WvCwzj7K2woMWNj63qFtplDnm8LLVZkhbC2iAvr23sNAz8LX+wsaX79A18mo6f7whTXn
この2つのファイルをイメージファイルに固めてvm起動時にcdromとして認識するようにする。
イメージの作成にはgenisoimage
コマンドを利用したが、同じように作成すれば何を使っても問題ない。このときボリュームラベルにcidata
もしくはCIDATA
を設定しなれければいけないことに注意する。
genisoimage -output cloud-init-conf.iso -volid cidata -joliet -rock user-data meta-data
起動する前にダウンロードしてきたFedoraのcloud imageをそのまま使ってもよいが、毎回新しいイメージを用意するのは面倒なので、バッキングファイルとして残しておくことにする。
sudo qemu-img create -b Fedora-Cloud-Base-32-1.6.x86_64.qcow2 -f qcow2 test-vm.qcow2 20G
この状態でcdromに先程生成したisoデータを設定して、virt-installする。
virt-install --name test-vm --ram 16384 --vcpus 4 --arch x86_64 --os-type linux --hvm --virt-type kvm --file ./test-vm.qcow2 --cdrom ./cloud-init-conf.iso --boot hd --graphics bash --serial pty --console pty --autostart
これで設定に間違えがなければ、インストール後に設定したパスワードやssh鍵でログインすることができる。
cdromからは先程設定したデータが確認できた。
$ sudo mkdir /mnt/cdrom $ sudo mount /dev/cdrom /mnt/cdrom/ mount: /mnt/cdrom: WARNING: source write-protected, mounted read-only. $ ls /mnt/cdrom/ meta-data user-data
入力したしたデータやスクリプト、cloud-initの実行結果などはVM内の/var/lib/cloud/
配下で確認することができる。
おまけ
cloud-initではどのdatastoreを利用するのかds-identifyで判定している。
https://github.com/canonical/cloud-init/blob/main/tools/ds-identify#L835-L859
コードを読んでみるとNoCloudの判定のため引数やファイル、ファイルシステムのラベルが最初に設定したcidata
もしくはCIDATA
などかをチェックしていることがわかる。
dscheck_NoCloud() { local fslabel="cidata CIDATA" d="" case " ${DI_KERNEL_CMDLINE} " in *\ ds=nocloud*) return ${DS_FOUND};; esac case " ${DI_DMI_PRODUCT_SERIAL} " in *\ ds=nocloud*) return ${DS_FOUND};; esac for d in nocloud nocloud-net; do check_seed_dir "$d" meta-data user-data && return ${DS_FOUND} check_writable_seed_dir "$d" meta-data user-data && return ${DS_FOUND} done # shellcheck disable=2086 if has_fs_with_label $fslabel; then return ${DS_FOUND} fi # This is a bit hacky, but a NoCloud false positive isn't the end of the world if check_config "NoCloud" && check_config "user-data" && check_config "meta-data"; then return ${DS_FOUND} fi return ${DS_NOT_FOUND} }
cloud-init NoClouda
cloud-init Cloud config examples
CNIの役割
最近のパブリッククラウドを利用している人はあまり意識しないかもしれないが、手動でKubernetesクラスタを構築したことのある人ならCNIという単語は聞いたことがあるだろう。 CNIプラグインはKubernetesであればpodの通信が通るネットワークの管理を行っているが、実際にCNI及びCNIプラグインの厳密な守備範囲について確認する。
CNIとは
CNIはコンテナネットワークに関するインターフェースの仕様を標準化したものである。Kubernetes関連でCNIという単語を聞いたことがある人が多いと思うが、実際はそれに限った話ではない。 コンテナを作成したらコンテナ単体が存在するネットワークなのか、あるいはアプリケーションで利用するコンテナ軍が接続されたものなのか様々あるだろうが、大抵の場合はネットワークに接続したくなるはずだ。
ネットワークの構築やコンテナに対する接続を構築するアプリケーションは一つの機能としてコンテナ構築とは別で外部に切り出されている。 この際にネットワークの定義の仕様を取り決めたものをCNI、実際にネットワークの構築や、コンテナへの接続を担当するアプリケーションをCNIプラグインという。
ここで注意したいのが、コンテナといえばDockerを思い浮かべる人が多いと思うが、Dockerでコンテナを作成した際にアタッチされるネットワークの構築にはCNIは使われておらずContainer Network Model(CNM)という別の概念を実装したlibnetworkが使われている。
すなわちコンテナ技術を使っているからといってネットワーク周りをCNIに準基する必要があるとそうではない。
CNIプラグインには様々な実装があり標準実装としてかんたんなものがこちらに用意されている。 他にはIPIPのトンネリングを利用したコンテナ間通信を行うCalicoやVXLANを利用するflannelなどさまざま存在する。
CNIの仕様
全文はこちらを参照。 CNIにおける用語は以下の通り。 - コンテナ - ネットワークで分断されたドメイン。一般的にはLinuxのnetwork namespaceが用いられるが、仮想マシンを1単位とする場合もある。このようにネットワーク的に分離しているものであればコンテナと呼ぶことになっているので、分離している技術に依存するものではない。 - ネットワーク - 相互に通信でき一意のアドレスで指定可能なエンドポイントをまとめたもの。コンテナは1つ以上のネットワークに対して追加削除できる。 - プラグイン - 指定されたネットワークの構成を担当するプログラム。 - ランタイム - CNIプラグインの実行をするためのプログラム
CNIでは上記の定義を前提に以下の5つの仕様が定義されている。 - ネットワークの構成を指定するためのフォーマット(json形式で構成を指定するが、その書式が定義されている) - https://www.cni.dev/docs/spec/#section-1-network-configuration-format - コンテナランタイムがネットワークプラグインに対してリクエストを送る際のフォーマット - https://www.cni.dev/docs/spec/#section-2-execution-protocol - 定義された構成を基にプラグインを実行するための手順 - https://www.cni.dev/docs/spec/#section-3-execution-of-network-configurations - 必要であればプラグインが他のプラグインに対して処理を委譲するがそのための手順 - https://www.cni.dev/docs/spec/#section-4-plugin-delegation - プラグインが実行結果をランタイムに返す際のフォーマット - https://www.cni.dev/docs/spec/#section-5-result-types
ひとまず全体の流れとしてはこの様になる。
ランタイムがプラグインを呼び出す際には環境変数もしくは標準入力を通した設定値を介してパラメータを設定する。 プラグインはJsonエンコードされた実行結果を標準入力、標準出力に書き込む。 また仕様ではプラグインを実行するネットワークネームスペースはランタイムのそれと同じでなければならないと定められている。
ランタイムがプラグインを実行する際に指定するパラメータいくつかあるが特に注目したいのはCNI_COMMAND
である。
このパラメータはプラグインの挙動を決める大きな役割を担っているており以下の4つのうちどれかが指定される。
ADD
CNI_IFNAME
で定義されたインターフェースをCNI_NETNS
で定義されたコンテナに対して作成するCNI_NETNS
で定義されたコンテナのCNI_IFNAME
で定義されたインターフェースを調整する。
DEL
CNI_IFNAME
で定義されたインターフェースをCNI_NETNS
から削除する。- ADDで適用された内容をもとに戻す
CHECK
- 既存のコンテナが期待通りのステータスになっているかどうかを確認する。
- CHECKはADD処理の後に呼ばれ正常に適用できていることが確認される。
VERSION
- プラグインがサポートするCNIバージョンの取得
CNIの動作確認
CNIの一連の流れを手元で動かして確認する。 containerdやCRI-Oを使ってコンテナを作成すれば、それでCNIが使われていることになるのだが、内部でgo-cniを使っているため、具体的にどういった引数を渡しているかなど追うのがわかりにくい。
CNIで登場するコンテナは説明した通り、Linuxのnetwork namspaceのようにネットワーク的に分断されているものと定義されているので、これを使うことにする。
CNIには標準実装となるプラグインが用意されていいるのでクローンしてビルドし利用する。(GOのビルド環境が必要) - https://github.com/containernetworking/plugins
$ ./build_linux.sh Building plugins bandwidth firewall portmap sbr tuning vrf bridge dummy host-device ipvlan loopback macvlan ptp vlan dhcp host-local static $ ls ./bin/ bandwidth bridge dhcp dummy firewall host-device host-local ipvlan loopback macvlan portmap ptp sbr static tuning vlan vrf
まずコンテナ(netwok namespace)を用意する。
$ sudo ip netns add container-1 $ ip netns list container-1
何も追加していない状態のためループバックインタフェースしか見えていない
$ sudo ip netns exec container-1 ip a 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
ここにCNIを利用してインターフェースを足してみる。 今回はビルドしたプラグインの中から接続したcontainer(namespace)がbridgeを介して接続されるbridge pluginを使ってみる。
ここに記載されているようにCNIの仕様ではいくつかのパラメータを環境変数を介して渡す必要がある。 紹介したCNI_COMMANDもここで渡している。今回はコンテナにインタフェースを追加するためADDを指定した。 ADDを利用するためにはこれらの環境変数を設定する必要がある。
https://www.cni.dev/docs/spec/#add-add-container-to-network-or-apply-modifications - CNI_COMMAND - CNI_CONTAINERID - CNI_NETNS - CNI_IFNAME
$ export CNI_PATH=$(readlink -f ./bin/) $ export CNI_COMMAND=ADD $ export CNI_CONTAINERID=container-1 $ export CNI_NETNS=/var/run/netns/container-1 $ export CNI_IFNAME=eth0
bridge pluginを利用する際のconfig例としてこのように記載されている。 そしてCNIプラグインへconfigを渡す際は標準入力から渡すことになっているので、以下のように実行する。 このとき先程設定した環境変数を渡すためにsudoを利用する場合は に-Eオプションを忘れずに。
$ cat << 'EOF' | sudo -E ./bin/bridge { "cniVersion": "0.3.1", "name": "mynet", "type": "bridge", "bridge": "mynet0", "isDefaultGateway": true, "forceAddress": false, "ipMasq": true, "hairpinMode": true, "ipam": { "type": "host-local", "subnet": "10.10.0.0/16" } } EOF { "cniVersion": "0.3.1", "interfaces": [ { "name": "mynet0", "mac": "02:3a:b3:d6:cc:a7" }, { "name": "vethf5f1a233", "mac": "5e:51:7c:b8:a9:44" }, { "name": "eth0", "mac": "0e:c0:ab:d5:02:bb", "sandbox": "/var/run/netns/container-1" } ], "ips": [ { "version": "4", "interface": 2, "address": "10.10.0.2/16", "gateway": "10.10.0.1" } ], "routes": [ { "dst": "0.0.0.0/0", "gw": "10.10.0.1" } ], "dns": {}
そうすると上記のように標準出力に実行結果が返される。 そして再びipコマンドでコンテナ内のインタフェースを確認すると先程指定したものが追加されていることがわかる。またbrctlでは指定したbridgeが追加されている。
$ sudo ip netns exec container-1 ip a 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 4: eth0@if7: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default link/ether d2:40:0f:c1:3f:da brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 10.10.0.3/16 brd 10.10.255.255 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::d040:fff:fec1:3fda/64 scope link valid_lft forever preferred_lft forever $ brctl show mynet0 bridge name bridge id STP enabled interfaces mynet0 8000.26dc84ed6e49 no veth24b7b96d
指定したルート情報も挿入されている。
[yota@ubuntu01 plugins]$ sudo ip netns exec container-1 route Kernel IP routing table Destination Gateway Genmask Flags Metric Ref Use Iface default _gateway 0.0.0.0 UG 0 0 0 eth0
もう1つコンテナを用意して同様にbdridgeに接続する。
$ sudo ip netns add container-2 $ export CNI_COMMAND=ADD $ export CNI_CONTAINERID=container-2 $ export CNI_NETNS=/var/run/netns/container-2 $ export CNI_IFNAME=eth0 $ cat << 'EOF' | sudo -E ./bin/bridge { "cniVersion": "0.3.1", "name": "mynet", "type": "bridge", "bridge": "mynet0", "isDefaultGateway": true, "forceAddress": false, "ipMasq": true, "hairpinMode": true, "ipam": { "type": "host-local", "subnet": "10.10.0.0/16" } } EOF { "cniVersion": "0.3.1", "interfaces": [ { "name": "mynet0", "mac": "26:dc:84:ed:6e:49" }, { "name": "vetheb3b55cd", "mac": "5a:7e:be:be:4d:32" }, { "name": "eth0", "mac": "da:dd:d9:9b:83:e6", "sandbox": "/var/run/netns/container-2" } ], "ips": [ { "version": "4", "interface": 2, "address": "10.10.0.4/16", "gateway": "10.10.0.1" } ], "routes": [ { "dst": "0.0.0.0/0", "gw": "10.10.0.1" } ], "dns": {} } $sudo ip netns exec container-2 ip a 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00 2: eth0@if8: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default link/ether da:dd:d9:9b:83:e6 brd ff:ff:ff:ff:ff:ff link-netnsid 0 inet 10.10.0.4/16 brd 10.10.255.255 scope global eth0 valid_lft forever preferred_lft forever inet6 fe80::d8dd:d9ff:fe9b:83e6/64 scope link valid_lft forever preferred_lft forever
これでコンテナ間無事疎通が取れることを確認できた。
$ sudo ip netns exec container-2 ping 10.10.0.3 -c3 PING 10.10.0.3 (10.10.0.3) 56(84) bytes of data. 64 bytes from 10.10.0.3: icmp_seq=1 ttl=64 time=0.104 ms 64 bytes from 10.10.0.3: icmp_seq=2 ttl=64 time=0.061 ms 64 bytes from 10.10.0.3: icmp_seq=3 ttl=64 time=0.061 ms $ sudo ip netns exec container-1 ping 10.10.0.4 -c3 PING 10.10.0.4 (10.10.0.4) 56(84) bytes of data. 64 bytes from 10.10.0.4: icmp_seq=1 ttl=64 time=0.145 ms 64 bytes from 10.10.0.4: icmp_seq=2 ttl=64 time=0.077 ms 64 bytes from 10.10.0.4: icmp_seq=3 ttl=64 time=0.073 ms ### メモ 後で消す mynet0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default qlen 1000 link/ether 26:dc:84:ed:6e:49 brd ff:ff:ff:ff:ff:ff inet 10.10.0.1/16 brd 10.10.255.255 scope global mynet0 valid_lft forever preferred_lft forever inet6 fe80::2cc7:a5ff:fe41:76d4/64 scope link
なお今回はbridgeを介した通信をするにあたって、iptablesに特定のルールを追加していないため、このあたりのカーネルパラメータを設定している。container間で疎通が取れないようであれば確認したほうがよい。
nsudo sysctl -w net.bridge.bridge-nf-call-iptables = 0 nsudo sysctl -w et.bridge.bridge-nf-call-arptables = 0
最後は作成したプラグイン経由で作成したインタフェースを削除してみる。 DELの実行では以下のパラーメタが必要となる。
https://www.cni.dev/docs/spec/#del-remove-container-from-network-or-un-apply-modifications - CNI_COMMAND - CNI_CONTAINERID - CNI_IFNAME
$ export CNI_COMMAND=DEL $ export CNI_CONTAINERID=container-1 $ export CNI_NETNS=/var/run/netns/container-1 # netwrok namespace指定のために必要 $ export CNI_IFNAME=eth0 cat << 'EOF' |sudo -E ./bin/bridge > { "cniVersion": "0.3.1", "name": "mynet", "type": "bridge", "bridge": "mynet0", "ipam": {} } > EOF
このように実行するとステータスコードが0で返り、指定したインタフェースが削除されていることが確認できた。
$ echo $? 0 $ sudo ip netns exec container-1 ip a 1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000 link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
まとめ
CNIが存在することによってコンテナネットワークの作成や削除を始めとしたインタフェースを共通化している。 そのおかげでプラグインを実装することによって様々なユースケースに応じたコンテナネットワークを柔軟に構成することができる。
Dockerはどのようにリソース制限しているか
Dockerを利用してコンテナを動かすと、様々なリソースをLinuxのnamespaceを利用してプロセスを独立させている。 コンテナを立ち上げる際にDockerではリソース使用量の制限を行うことができるようになっている。
例えばdocker run実行時に--memory=256mb
を指定すればメモリ使用量は256MBに制限されるし、--cpuset-cpus="0-3,15-19"
と指定すれば指定したcpu上で動作するように制限することができる。
これはLinuxのcgroupの機能を利用したもので、デフォルトではOCIに準基したruncがsystemdを介して設定をしているものになる。
今回はLinux上でDockerを利用してコンテナを動かした際に、どのようにcgroupが扱われているか確認する。
コンテナ動作時にcgroupの確認
実際にDockerでコンテナを動かしてリソースの制限をした際にcgroupがどのように機能しているか確認してみる。 今回は手持のテスト用イメージを利用したので、手元で試す際は適当なCentOSなどのイメージで必要なパッケージをインストールすると良い。
なお実行時のdocker関連のパッケージのバージョンは以下の通り。
$ rpm -qa |grep docker docker-ce-cli-20.10.22-3.el7.x86_64 docker-ce-20.10.22-3.el7.x86_64 docker-scan-plugin-0.23.0-3.el7.x86_64 docker-ce-rootless-extras-20.10.22-3.el7.x86_64
$ sudo docker run --cpuset-cpus=""0-2,4-6" --name cpu-limit -d TEST-IMAGE tail -f /dev/null
この状態で起動したコンテナ内でstressを実行してホスト上で各CPUの使用状況を確認してみると、docker run
実行時に指定したCPUの負荷のみ上昇していることが確認できる。
$ sudo docker exec -it cpu-limit bash [root@0e48eee6ab06 /]# stress -c 10 stress: info: [21] dispatching hogs: 10 cpu, 0 io, 0 vm, 0 hdd # 別窓でコンテナ中にて実行 $ sudo docker exec -it cpu-limit bash $ htop
この状態でコンテナ内からcgroupを参照すると、以下のようにdocker run実行時に指定したCPUでのみ動作するように制限されていることがわかる。
$ cat /sys/fs/cgroup/cpuset/cpuset.cpus 0-2,4-6
なお上記の挙動はcgroup v1が動作するカーネルでの実行例で、cgroup v2カーネルではこの様なパスになる
$ cat /sys/fs/cgroup/cpuset.cpus 0-2,4-6
このようにdockerに渡したリソース制限に関するオプションは、最終的にはcgroupを利用して実現している。
ホスト側の視点
ホスト側ではコンテナはいくつかのnamespaceを分離したプロセスとして見えている。
ここからはホストでマウントしているcgroupのバージョンによって挙動が異なるのでバージョンを明確にする。
下記のカーネル3系の結果を見てみる。/proc/${pid}/ns
配下を参照すると該当プロセスがどのnamepsaceに所属しているかを確認することができる。
また/proc/${pid}/cgroup
を参照すると所属cgroupを確認することができる。実行結果かからcgroupの各サブシステムにおいて/system.slice/docker-${CONTAINER_IO}.scope
に所属していることが確認できる。
カーネル 3.10
# docker runで tail -f しているのでpidofでpidを特定 $ pidof tail 1652 $ pid=1652 $ sudo -E ls -l /proc/${pid}/ns/ 合計 0 dr-x--x--x. 2 root root 0 1月 4 00:22 . dr-xr-xr-x. 9 root root 0 1月 4 00:22 .. lrwxrwxrwx. 1 root root 0 1月 4 00:24 ipc -> ipc:[4026532129] lrwxrwxrwx. 1 root root 0 1月 4 00:24 mnt -> mnt:[4026532127] lrwxrwxrwx. 1 root root 0 1月 4 00:22 net -> net:[4026532132] lrwxrwxrwx. 1 root root 0 1月 4 00:24 pid -> pid:[4026532130] lrwxrwxrwx. 1 root root 0 1月 4 00:24 user -> user:[4026531837] lrwxrwxrwx. 1 root root 0 1月 4 00:24 uts -> uts:[4026532128] $ cat /proc/${pid}/cgroup 11:cpuset:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 10:memory:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 9:devices:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 8:freezer:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 7:perf_event:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 6:pids:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 5:hugetlb:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 4:cpuacct,cpu:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 3:blkio:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 2:net_prio,net_cls:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 1:name=systemd:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope
カーネル5系ではnamesaceの種類が増えている。またcgroup v2がマウントされている状態のため、subsystemごとではなくプロセスが所属しているcgroupは1つとなっている。
カーネル 5.17
$ pidof tail 1264 $ pid=1264 $ sudo -E ls -l /proc/${pid}/ns/ total 0 lrwxrwxrwx. 1 root root 0 Jan 4 01:07 cgroup -> 'cgroup:[4026532240]' lrwxrwxrwx. 1 root root 0 Jan 4 01:07 ipc -> 'ipc:[4026532172]' lrwxrwxrwx. 1 root root 0 Jan 4 01:07 mnt -> 'mnt:[4026532170]' lrwxrwxrwx. 1 root root 0 Jan 4 00:22 net -> 'net:[4026532174]' lrwxrwxrwx. 1 root root 0 Jan 4 01:07 pid -> 'pid:[4026532173]' lrwxrwxrwx. 1 root root 0 Jan 4 01:07 pid_for_children -> 'pid:[4026532173]' lrwxrwxrwx. 1 root root 0 Jan 4 01:07 time -> 'time:[4026531834]' lrwxrwxrwx. 1 root root 0 Jan 4 01:07 time_for_children -> 'time:[4026531834]' lrwxrwxrwx. 1 root root 0 Jan 4 01:07 user -> 'user:[4026531837]' lrwxrwxrwx. 1 root root 0 Jan 4 01:07 uts -> 'uts:[4026532171]' $ cat /proc/${pid}/cgroup 0::/system.slice/docker-0e48eee6ab061f7e56b1bf00f32e7aa943e52359d097c871b4fed3f65a71c28a.scope
systemd経由のtransient cgroup
上記でホスト側からのcgroupのバージョン差によるcgroupの見え方の違いを確認したが、その時のグループはいずれも/system.slice/docker-${CONTAINER_IO}.scope
の形式となっていた。
これはsystemd経由で作成されたものである。またsystemctlによってunit fileして出力をgrepするとtransient unitとして登録されていることが確認できる。
# バージョンによってはunit名が省略して表示されるがcgroupで確認したものと同じ $ systemctl list-unit-files |grep docker docker-0e48eee6ab061f7e56b1bf00f32e7aa943e52359d097c871b4fed3f65a71c28a.scope transient - docker.service enabled disabled docker.socket disabled enabled $ systemctl cat docker-0e48eee6ab061f7e56b1bf00f32e7aa943e52359d097c871b4fed3f65a71c28a.scope # Warning: docker-0e48eee6ab061f7e56b1bf00f32e7aa943e52359d097c871b4fed3f65a71c28a.scope changed on disk, the version systemd has loaded is outdated. # This output shows the current version of the unit's original fragment and drop-in files. # If fragments or drop-ins were added or removed, they are not properly reflected in this output. # Run 'systemctl daemon-reload' to reload units. # /run/systemd/transient/docker-0e48eee6ab061f7e56b1bf00f32e7aa943e52359d097c871b4fed3f65a71c28a.scope # This is a transient unit file, created programmatically via the systemd API. Do not edit. [Unit] Description=libcontainer container 0e48eee6ab061f7e56b1bf00f32e7aa943e52359d097c871b4fed3f65a71c28a [Scope] Slice=system.slice Delegate=yes MemoryAccounting=yes CPUAccounting=yes IOAccounting=yes TasksAccounting=yes [Unit] DefaultDependencies=no # /run/systemd/transient/docker-0e48eee6ab061f7e56b1bf00f32e7aa943e52359d097c871b4fed3f65a71c28a.scope.d/50-DevicePolicy.conf # This is a drop-in unit file extension, created via "systemctl set-property" # or an equivalent operation. Do not edit. [Scope] DevicePolicy=strict # /run/systemd/transient/docker-0e48eee6ab061f7e56b1bf00f32e7aa943e52359d097c871b4fed3f65a71c28a.scope.d/50-DeviceAllow.conf # This is a drop-in unit file extension, created via "systemctl set-property" # or an equivalent operation. Do not edit. [Scope] DeviceAllow= DeviceAllow=char-pts rwm DeviceAllow=/dev/char/5:2 rwm DeviceAllow=/dev/char/5:1 rwm DeviceAllow=/dev/char/5:0 rwm DeviceAllow=/dev/char/1:9 rwm DeviceAllow=/dev/char/1:8 rwm DeviceAllow=/dev/char/1:7 rwm DeviceAllow=/dev/char/1:5 rwm DeviceAllow=/dev/char/1:3 rwm DeviceAllow=char-* m DeviceAllow=block-* m # /run/systemd/transient/docker-0e48eee6ab061f7e56b1bf00f32e7aa943e52359d097c871b4fed3f65a71c28a.scope.d/50-AllowedCPUs.conf # This is a drop-in unit file extension, created via "systemctl set-property" # or an equivalent operation. Do not edit. [Scope] AllowedCPUs=0-2 4-6 $ cat /run/systemd/transient/docker-0e48eee6ab061f7e56b1bf00f32e7aa943e52359d097c871b4fed3f65a71c28a.scope.d/50-AllowedCPUs.conf # This is a drop-in unit file extension, created via "systemctl set-property" # or an equivalent operation. Do not edit. [Scope] AllowedCPUs=0-2 4-6
Dcokerにおけるcgroupによるリソース制御に仕組みはDcoker run
実行時に引数として渡した値が起点となっている。その後近年のバージョンではCRIインタフェースを実装しているcontainerdによって処理される。
docker daemonのexec-optsでcgroupを自分で作成するようにするか、systemdの管理とするかを選択できるようになっている。(そのためこれまでに確認した内容はあくまでデフォルトのsystemdが指定されている場合の動作) - https://docs.docker.com/engine/reference/commandline/dockerd/#options-for-the-runtime
cat /etc/docker/daemon.json | grep cgroup "exec-opts": ["native.cgroupdriver=systemd"],
ここで指定された後にcontainerdによって処理された後、低レイヤーランタイム(デフォルトではrunc)によって実際に反映されることになる。
runcに渡される際には--systemd-cgroup
オプションによってsystemdによる管理としている。
- https://github.com/opencontainers/runc/blob/v1.1.4/docs/systemd.md
containerd内部ではruncを操作する際はgo-runcというライブラリが使われている。
コードを確認するとDockerからのオプションに応じて内部で--systemd-cgroup
を渡していることが確認できる。
- https://github.com/containerd/go-runc/blob/42adae7462d417ea5178c9f8b24a55968ddada14/runc.go#L755-L757
func (r *Runc) args() (out []string) { if r.Root != "" { out = append(out, "--root", r.Root) } if r.Debug { out = append(out, "--debug") } if r.Log != "" { out = append(out, "--log", r.Log) } if r.LogFormat != bash { out = append(out, "--log-format", string(r.LogFormat)) } if r.SystemdCgroup { out = append(out, "--systemd-cgroup") } if r.Rootless != nil { // nil stands for "auto" (differs from explicit "false") out = append(out, "--rootless="+strconv.FormatBool(*r.Rootless)) } if len(r.ExtraArgs) > 0 { out = append(out, r.ExtraArgs...) } return out }
Cgroup Namespaceaによるコンテナからの見え方の差
Linuxにはいくつかのnamespaceが実装されているが、カーネル4.6から搭載されるcgroup namespaceの有無によってコンテナ内でのcgroupの見え方が異なる。
先程試した2種類のバージョンで比較する。
まずdocker execでコンテナ内からpid 1(tail -f /dev/null
)のcgroup情報を見てみる。
カーネル 3.10
/sys/fs/cgroup
を見る限りはコンテナ内では対処プロセスは/(root)
グループに所属しているように見える。
# ホスト上でcgroup v1が利用されていることを確認 $ findmnt -T /sys/fs/cgroup TARGET SOURCE FSTYPE OPTIONS /sys/fs/cgroup tmpfs tmpfs ro,nosuid,nodev,noexec,seclabel,mode=755 $ sudo docker exec -it df9be6fb5e25 bash # 以下コンテナ内 # /sys/fs/cgroup/cpu配下にグループ(ディレクトリ)は存在せずtasksを確認するとコンテナ内のプロセスはすべてルートに所属している。 $ cat /sys/fs/cgroup/cpu/tasks 1 7 61
しかしコンテナ内からホストのcgroup情報を参照する手段は存在する。
コンテナ内から/proc/1/cgroup
を参照するとcgroupの各subsystemに関連づいているグループが表示される。
これは先程ホスト側で見えていたcgroupと全く同じ情報である。
$ cat /proc/1/cgroup 11:cpuset:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 10:memory:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 9:devices:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 8:freezer:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 7:perf_event:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 6:pids:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 5:hugetlb:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 4:cpuacct,cpu:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 3:blkio:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 2:net_prio,net_cls:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope 1:name=systemd:/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope $ findmnt -t cgroup TARGET SOURCE FSTYPE OPTIONS /sys/fs/cgroup/systemd cgroup[/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope] cgroup ro,nosuid,nodev,noexec,relatime,seclabel,xattr,release_agent=/usr/lib/systemd/systemd-cgroups-agent,name=systemd /sys/fs/cgroup/net_cls,net_prio cgroup[/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope] cgroup ro,nosuid,nodev,noexec,relatime,seclabel,net_prio,net_cls /sys/fs/cgroup/blkio cgroup[/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope] cgroup ro,nosuid,nodev,noexec,relatime,seclabel,blkio /sys/fs/cgroup/cpu,cpuacct cgroup[/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope] cgroup ro,nosuid,nodev,noexec,relatime,seclabel,cpuacct,cpu /sys/fs/cgroup/hugetlb cgroup[/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope] cgroup ro,nosuid,nodev,noexec,relatime,seclabel,hugetlb /sys/fs/cgroup/pids cgroup[/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope] cgroup ro,nosuid,nodev,noexec,relatime,seclabel,pids /sys/fs/cgroup/perf_event cgroup[/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope] cgroup ro,nosuid,nodev,noexec,relatime,seclabel,perf_event /sys/fs/cgroup/freezer cgroup[/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope] cgroup ro,nosuid,nodev,noexec,relatime,seclabel,freezer /sys/fs/cgroup/devices cgroup[/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope] cgroup ro,nosuid,nodev,noexec,relatime,seclabel,devices /sys/fs/cgroup/memory cgroup[/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope] cgroup ro,nosuid,nodev,noexec,relatime,seclabel,memory /sys/fs/cgroup/cpuset cgroup[/system.slice/docker-df9be6fb5e25ba4d36147c474be92e39518aabf379bb49dde5773e7489fc29e9.scope] cgroup ro,nosuid,nodev,noexec,relatime,seclabel,cpuset
カーネル 5.17
こちらも3.10と同じようにすべてのプロセスはルートグループに所属している。
# ホスト上でcgroup v2が利用されていることを確認 $ findmnt -T /sys/fs/cgroup TARGET SOURCE FSTYPE OPTIONS /sys/fs/cgroup cgroup2 cgroup2 rw,nosuid,nodev,noexec,relatime,seclabel,nsdelegate,memory_recursiveprot $ sudo docker exec -it 0e48eee6ab06 bash # 以下コンテナ内 $ cat /sys/fs/cgroup/cgroup.procs 1 7 35
先程試した3系の結果に対してcgroup namespaceが道入されている5.17ではpid 1の所属するcgroupは/(root)
となっていてホスト側で見えていた情報と異なっている。
このようにDcokerで作成したコンテナはcgroup namespaceが分割されることによってコンテナからホスト上のcgroupに関する情報は見えないようになっている。
ホスト情報とコンテナのコンテナのリソースを分割しコンテナからホスト情報へのアクセス遮断はセキュリティ的に大きなメリットがある。
$ cat /proc/1/cgroup 0::/ $ findmnt -t cgroup2 TARGET SOURCE FSTYPE OPTIONS /sys/fs/cgroup cgroup cgroup2 ro,nosuid,nodev,noexec,relatime,seclabel,nsdelegate,memory_recursiveprot
関連・参照
cgroups を使用した RHEL 7 のシステムリソースの管理 第2章 コントロールグループの使用
runc systemd cgroup driver
dockerd documentation
【書籍】SRE サイトリライアビリティエンジニアリング
書籍 SRE サイトリライアビリティエンジニアリングを読んだ。
書籍の内容
Googleの「SRE」という組織がどのような役割を果たしているのかや、従来のシステム管理者との違いについて書かれた書籍だった。 今でこそ様々な場面で耳にするSREという単語は、このGoogleの組織から来ている。
このSREという組織は従来の運用管理専門の組織とは異なり、システムの運用という観点はもちろん、開発フェーズの段階から介入し組織を横断して運用の効率化とシステム安定化を目指している。
各章は内容としてはSREチームに関連して様々な、観点からのベストプラクティス集といった感じで独立している。
感想など雑多に
まずサービスの安定稼働を目指すためには、その指標が必要不可欠である。そして4章ではその指標として、SLI,SLO,SLAをどのような値に設定するべきか記述されている。
この辺りは運用に関わるメンバーでもしっかりと、常日頃から目標値と現状を共有できているとチームとして一体感が生まれそうだと感じた。
AWSやGCPなどのクラウドサービスを利用して自社のプロダクトを運用する機会が多い昨今の状況では、運用と開発における境界は自ずと狭まりつつある(と思っている。特にマイクロサービスを代表されるアーキテクチャを取っていればなおさら)
そういったなかで、可用性工場のためデプロイメントの最適化やプロセス改善を行う専門のロールが生まれるのも納得がいく。
それなりにボリュームのある本のため、一度サクッと読んだ後は気になったものや業務に活かせそうなエピソードを選んでたまに本書を開く、といった読み方がおすすめ。
各章に踏み込んだ内容については、気に入った章をその内取り上げてまとめようかと思う。
ちなみに英語版は無料で読めます。