はじめに
STORES EC の SRE を担当している北原と申します。 hey では SRE チームが主催の 'SRE なんでも相談会' という勉強会を定期的に開催しています。 今回は、先日私が担当した 'Docker ことはじめ #1' の内容について紹介させていただきます。 Docker 勉強会の初回ということで、勉強会では以下の話をしました。
- Docker のドキュメントをざっくりよむ
- Docker が利用している要素技術について
- STORES EC のコンテナ化について
このブログでは上記のうち 'Docker が利用している要素技術について' が範囲となります。
コンテナ技術とは
Docker ではコンテナ技術が利用されています。 公式ドキュメントの 'What is a Container?' では、コンテナ技術を仮想マシン (VM) と比較紹介されています。
要約すると、双方の違いは以下の通りです。
アプリケーションをユーザースペースで分離されたプロセスとして実行することにより抽象化する とは具体的にどういうことなのかを解説していきます。
(Linux) カーネルについて
まずはじめに Linux カーネルは大きく2つのスペースに分かれています。
- ユーザースペース (ユーザーランド): ユーザーに割り当てられるリソース
- カーネルスペース: ユーザー (アプリケーション) が直接アクセスできないリソース
Linux ではカーネルスペースへの System Call という API が提供されており、ユーザーは主に System Call 経由でカーネルリソースにアクセスします。
コンテナ技術は、以下を実現することで成り立っています。
- ユーザースペースで実行されるプロセスを、同じくユーザースペースで実行されている他のプロセスから隔離
- ユーザースペースで実行されるプロセスがアクセス可能なカーネルスペースを制限
この隔離する機能はカーネルの セキュリティー機能 として提供されています。
隔離する技術
Docker は主に以下の隔離する技術に支えられています。
詳細については公式ドキュメント 'Docker security' を参照してください。
docker run
でコンテナを起動すると、Docker は裏側でコンテナの名前空間とコントロールグループのセットを作成します。
実際に Docker を使って、この隔離する技術がどのように動作するか見てみましょう。
事前準備
最初に、以下の Docker ファイルを about_container
というイメージ名でビルドしてみましょう。
FROM centos RUN rm --force /etc/rpm/macros.image-language-conf \ && sed -i '/^override_install_langs=/d' /etc/yum.conf \ && yum install --assumeyes \ epel-release \ && yum install --assumeyes \ libcgroup-tools \ stress \ && yum clean all
docker build . -t about_container
このコンテナを使って、コンテナの動作を確認してみましょう。
名前空間
名前空間は OS のリソースを隔離します。 名前空間一覧は以下のコマンドで確認できます。
$ lsns # List Namespaces
- cgroup: コントロールグループPC
- user: ユーザー ID とグループ ID
- mnt: マウントポイント。異なるファイルシステムレイアウトを作ったり、特定のマウントポイントを読み取り専用にできたり
- uts: ホスト名と NIS ドメイン名
- ipc: System V IPC, POSIX メッセージキュー
- pid: プロセス ID
- net: ネットワークデバイス、スタック、ポートなど。iptables やルーティングテーブルなどを分離できる
隔離可能なリソースはどんどん追加されていっています。 実際に名前空間を使ってプロセスを OS のリソースから隔離してみましょう。
事前準備でビルドしたコンテナを単純に docker run
しただけでは、名前空間は動作しません。
Docker はデフォルトで Linux カーネルの seccomp
機能により重要なシステムコールをブロックしています。 1
デフォルトの
seccomp
プロファイルは、seccomp
を使用してコンテナーを実行するための適切なデフォルトを提供し、300以上のうち約44のシステムコールを無効にします。 幅広いアプリケーション互換性を提供しながら、適度に保護します。 デフォルトの Docker プロファイルは ここ にあります。
docker run
時に以下のオプションを指定し、制限されているシステムコールへのアクセスを許可します。
docker run --rm -it --security-opt seccomp=unconfined about_container unshare --map-root-user --user sh -c /bin/bash
これが上述した Docker が利用している隔離するための セキュリティー の一部です。
名前空間の隔離には unshare
というコマンドを利用しますが、何もオプションを指定せずに unshare
を実行するとエラーとなります。
それではいくつかの名前空間を隔離してみましょう。
UTS の隔離
まずは UNIX Time-sharing System を隔離してみます。 UTS に関する詳細が知りたい方は Wikipedia を参照してください。 確認のために2つのシェルを起動します。
# シェル1 $ hostname # 現在のホスト名を確認 (コンテナ ID と同一) <コンテナ ID> $ echo $$ # 自分のプロセス ID を確認 1 $ unshare --uts /bin/bash # この /bin/bash プロセスのホスト名 (UTS) を隔離する $ echo $$ # unshare で隔離されているプロセス ID が表示される (1 以外) $ hostname isolated # ホスト名を isolated に変更 $ hostname # ホスト名が isolated になっていることを確認 isolated
別プロセスから現在実行しているコンテナに接続して確認してみましょう。 以下のコマンドで現在実行中のコンテナ ID を取得し、接続します。
$ docker ps --filter ancestor=about_container --filter status=running <コンテナ ID> $ docker exec -it <コンテナID> /bin/bash
# シェル2 $ hostname <コンテナ ID>
現在 unshare
で隔離したプロセスのみホスト名が isolated
となっています。
その他のプロセスへの影響はありません。
unshare
した bash プロセスを exit
で終了します。
# シェル1 $ exit $ hostname # ホスト名が変更されていないことを確認 <コンテナ ID>
マウントの隔離
続いてマウント (ファイルシステム) を隔離してみましょう。
# シェル1 $ readlink /proc/$$/ns/mnt # 現在のプロセスの mnt 名前空間を確認 $ findmnt mnt/ # mnt/ にマウントポイントがないことを確認 $ unshare --mount /bin/bash # この /bin/bash プロセスのマウントを隔離します $ readlink /proc/$$/ns/mnt $ mount -t tmpfs tmpfs mnt $ findmnt mnt/
# シェル2 $ readlink /proc/$$/ns/mnt $ findmnt mnt/ # マウントがないことを確認
unshare
で隔離したプロセスのみマウントされたファイルシステムが表示されるのを確認できました。
プロセスの隔離
最後にプロセスを隔離してみましょう。
$ sleep infinity & # 適当なプロセスを起動 $ echo $! # 起動したプロセス ID を確認 <プロセス ID> $ unshare --fork --pid /bin/bash $ kill <プロセス ID> # 隔離されたプロセスから先程起動したプロセスを kill してみる
ps auxf
するとプロセスは見えていますが、 kill すると No such process
というエラーが表示されます。
プロセス空間が隔離されているので、他のプロセスを操作できないことが確認できます。
/proc
ファイルシステムを共有しているので ps
で親プロセスが見えていますが、ファイルシステムを隔離すると親プロセスは表示されなくなります。
cgroups (コントロールグループ)
cgroups (control groups) とは、プロセスグループのリソース(CPU、メモリ、ディスクI/Oなど)の利用を制限・隔離するLinux カーネルの機能 [^5]
大雑把に、名前空間でプロセスがどのリソースやデーターが見えるかを制限し、cgroups でリソースをどのぐらい使えるかを制限します。
事前準備
コンテナ内で cgroup を利用する際は、さらに --privilege
オプションを指定し、すべてのデバイスへのアクセスを許可する必要があります。 2
このオプションにより、Docker on Docker も可能になります 3
docker run --rm -it --privileged=true --security-opt seccomp=unconfined about_container unshare --map-root-user --user sh -c /bin/bash
これも Docker の セキュリティー の一部です。
CPU リソースの制限
一番わかりやすそうな CPU リソースを制限してみましょう。
対象が cpu
の private
という名前のコントロールグループを作成します。
# シェル1 $ cgcreate -g cpu:/private
作成した cgroup を確認してみましょう。
# シェル1 $ lscgroup cpu:/ # プロセスのように親子関係があります $ cgget -a private # private グループの現状
では cpu の上限を10%に制限します。
名前空間同様に別のプロセスで top
コマンドを実行しておくと隔離されているのがわかりやすいです。
# シェル2 top
cgget
で確認したように現状の上限は cpu.rt_period_us: 1000000
で 1sec (=== 1000000µs) です。
現在 private
コントロールグループの cpu.cfs_quota_us
はデフォルト値の -1
となっており、CPU 時間による制限を受けていません。
今回は 20% の 0.2secs (==200000µs) に制限します。
これは全コアの上限なので、 200000µs / コア数
したものをセットしましょう。
# シェル1 $ cgset -r cpu.cfs_quota_us=20000 private
# シェル1 $ stress -c 1 -q # CPU を無制限に利用 $ cgexec -g cpu:private stress -c 1 -q # CPU を 20% 程度利用
以下のコマンドで既存プロセスの移動も可能です。
$ cgclassify -g cpu:private <プロセス ID> $ cgclassify -g cpu:/ <プロセス ID>
cgclassify
で stress
プロセスを cgroup 配下に移動すると CPU 使用率が100%から指定した使用率へ隔離されるのを確認できます。
capabilities (seccomp)
上述しましたが、Docker は Linux Kernel の機能により、高度なセキュリティーを保っています。
これはデフォルトでセキュアですが docker run
時により細かく制御できます。
Docker containers are very similar to LXC containers, and they have similar security features. When you start a container with docker run, behind the scenes Docker creates a set of namespaces and control groups for the container. 4
また、名前空間や cgroups により、プロセスをセキュアな状態に保っています。
まとめ
docker run
を実行した時に Docker が裏側で利用する技術を実際に動かしてみました。
例えば Rails プロセスのコンテナ化は、適切な cgroup
を作成し、以下のように起動すれば完了します。
$ unshare ... bundle exec rails s...
これを docker run
だけで実現できる Docker は便利ですね。
次回は Docker のオーケストレーションツールや周辺ツールが題材です。 もしご興味がある方はぜひ入社してご参加ください!