Red Hat General Container Image Guidelinesは、非常に重要で良い題材を扱っているにも関わらず、説明が不足したりして、勿体無い印象を受けた。そこで、読み解いてみることにした。この記事は、読者の理解であり誤った内容を含む可能性があります。もし発見されたら、是非コメント頂ければ幸いです。
コンテナ・イメージのビルド・ベストプラクティス
コンテナ・イメージのビルドについて、ベスト・プラクティスとなる記事を、以下にリストする。基礎的なことであったり、関連の深いことなので、見比べながら、理解する参考にした。
- Docker Docs Best practices for writing Dockerfiles
- Dockerfile を書くベスト・プラクティス 日本語訳
- Red Hat General Container Image Guidelines
- Guidance for Docker Image Authors
最初の目的は、コンテナのビルドに関するセキュリティだったが、大切なことが沢山書かれているので、セキュリティに捉われず、ポイントを整理する。
イメージのタグ指定によるトラブル回避
Dockerfile の FROM に、適切なアップストリームのイメージ指定する。これにより、更新時にセキュリティ修正プログラムを直接依存を指定するよりも簡単に取得できる。FORM命令の後に続くイメージ:タグのタグを省略すると latest が指定される。つまり FROM ubuntu
と指定すると FROM ubuntu:latest
書き換えられる。そこで、タグにバージョンを FROM ubuntu:16.04
のように設定することで、イメージのバージョンが上がることを回避できる。
次の例では、ubuntu 最新イメージが 16.04の時点に FROM ubuntu
を指定してビルドしたコンテナ・イメージについて、最新イメージが 18.04 になった以降で再度ビルドすると、そのイメージは Ubuntu のバージョンは 18.04 になる。これではサイズが、120MBから64.2MBに減ったことからも解る通り、大幅なモジュールの変更が伺われ、動作が保証できない。この問題を回避するためにFROM ubuntu:16.04
を指定しておくべきである。これによって、バージョン 16.04 の5週間前の最新に更新される。
$ docker images ubuntu |sort +2
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu 16.04 5e13f8dd4c1a 5 weeks ago 120MB
ubuntu 18.04 a2a15febcdf3 2 weeks ago 64.2MB
ubuntu latest a2a15febcdf3 2 weeks ago 64.2MB
タグによる互換性の明示
Dockerリポジトリ(REPOSITORY)とタグ(TAG)の利用法についての提案であり、タグには、ユーザーがイメージの互換性を識別できるように、タグを付与するべきである。例えば、コンテナ・イメージ my-image
を開発して登録する場合、バージョンをタグに付与する。例えば タグは v1
とする。 このイメージに含まれるパッケージを更新して、互換性を維持しながら、ビルド後に発見された脆弱性対策だけならば、タグは継続して v1
とする。これによってコンテナ・イメージのユーザーは my-image:v1
を継続して利用できる。
一方、互換性の無い新バージョンのコンテナ・イメージをリリースする際には、タグを切り替える。例えば、my-image:v2
としてリリースする。コンテナ・イメージの利用者は、意図せずに互換性の無いコンテナ・イメージをベースイメージにして、アプリケーションの障害を招くことを回避できる。また、リポジトリ名のみで指定、または、タグに latest
を指定する事は、互換性の無いコンテナ・イメージを取り込むリスクを負うことになり注意しなければならない。
ここでの互換性という事を明確にするために、少し触れておきたい。コンテナのイメージには、Linuxディストリビューションのライブラリやミドルウェアなどが、含まれている。コンテナ・イメージのユーザーは、公開されているコンテナのイメージの上に、レイヤーを重ねるように、追加パッケージや自身のアプリケーションコードを追加して利用する。そこで、例えば、同じタグ v1 を付与されたイメージであれば、ユーザーのDockerfile を変更する事なく、または、追加パッケージやコードの書き換えを行う事なく、利用できなければならない。
コンテナ内に複数プロセスを起動しない
一つのコンテナ上では、一つのプロセス、あるいは、一つのサービスに限定して稼働させるべきことが、コミュニティで議論の中で提案されている。コンテナは、他のプロセスから隔離されたプロセスの一種であることが、その理由では無いことを以下に列挙する。
- コンテナが単一機能の場合、コンテナの水平スケーリングを簡単に実施できる。 例えば、httpdのコンテナには httpdプロセス以外を稼働させていはいけない。もしも、キャッシュサーバーやデータベースなどを、異なるワークロード特性のプロセスを起動すると、事態が複雑になり、コンテナの利点が失われる。
- コンテナごとに一つの機能に限定することで、コンテナ同士の組み合わせ利用を容易にして、再利用性が増す。
- コンテナが単機能で分離されていれば、デベロッパーは容易に、リポジトリから必要なコンテナをプルして、迅速にアプリケーションの障害解析ができる。
- 開発者が、アプリケーションのコード修正時に、その影響範囲を、コンテナ内などの小さな範囲に限定して考慮することができる。
- 単一機能のコンテナは、パッチ適用、アップグレード、脆弱性対策などで、コンテナのデプロイやロールアウトやロールバックを簡単にする。
- 複合的な機能のアプリケーションを、複数の単一機能に分離することで、ネットワーク上の配置の柔軟性が向上して、セキュリティ強化に寄与する。
- 単機能にすることで、コンテナの廃棄や更新を容易にして、コンテナを一時的な存在として扱い易くなる。
- コンテナ上で実行するプロセスからの標準出力/標準エラー出力などの扱いが単純化される。
コンテナには、リモートからログインするためのsshd
を禁止するものはいないが、反対に積極的に推奨する者も少ない。次にその理由について列挙する。
- リモートからsshでログインを受ける場合、IDやログインキーはコンテナ・イメージにすることになる。そして、ユーザーの追加や削除の都度に、イメージを再構築してコンテナを再起動する必要がある。それも、イメージに、認証情報を保存することになり脆弱な状態を作ることになる。一方、永続ボリュームにIDやキーを書き込む事はできるが、複数のコンテナで共有することになり、セキュリティ状態が悪化する。
- もしも sshd に脆弱性が見つかった場合、sshd を含む全てのコンテナ・イメージを再ビルドして、sshd が稼働中の全てのコンテナを再起動する必要がある。
- sshd を起動するため プロセスマネージャーとしてsystemd などを追加する必要がある。このことは、無駄のないシンプルなコンテナを複雑なものにしてしまい、さらに、脆弱性対応が必要なモジュールを増やすことになる。
- 大きな会社組織では、アクセスポリシーとセキュリティコンプライアンスの担当が sshd について責任をもち、アクセス記録や操作など、セキュリティ監査の対象となる。
- コンテナの sshd がバックドアとなるリスクがある。
前述のように、コンテナは他のプロセスから隔離されたプロセスの一種としても、コンテナホストに入れば、コンテナには秘匿性を維持できない。そのため、コンテナへ入るための認証は、コンテナ以外の場所に置くのが好ましい。 Docker では Dockerホストへのログイン認証であり、Kubernetesではkubectlコマンドの資格情報となる。
参考資料
- Why it is recommended to run only one process in a container?, https://devops.stackexchange.com/questions/447/why-it-is-recommended-to-run-only-one-process-in-a-container
- 10 things to avoid in docker containers, https://developers.redhat.com/blog/2016/02/24/10-things-to-avoid-in-docker-containers/
- Run multiple services in a container, https://docs.docker.com/config/containers/multi-service_container/
- Docker - one process per container?, https://stackoverflow.com/questions/30003338/docker-one-process-per-container
- Why you don't need to run SSHd in your Docker containers, https://blog.docker.com/2014/06/why-you-dont-need-to-run-sshd-in-docker/
ラッパースクリプトからEXECで起動
これはコンテナ上でのアプリケーション起動に使用するシェルについての考慮点である。
アプリケーションの実行形式のファイルは、シェルに包んで実行することが一般的である。その理由は、起動前に動作環境や設定ファイルの存在をチェックするなどして、条件が整わない中で、誤動作やデータ破壊の障害を避ける上で重要である。このような作法は、コンテナ上でアプリケーションを起動する場合にも、同じことである。
しかし、コンテナのシェルからプログラムを起動には、注意するべき課題があるので、具体的に見ていきたい。ここに挙げたのは、Linux すなわち、コンテナで、新たにプログラムを起動する際に、システムコールのレベルの二つのケースである。
- 自己プロセスを終了させて、新たなプロセスを起動するケース
- 自己プロセスの子プロセスとして、新たなプロセスを終了後、起動したプロセスが継続するケース
上記の2番目のケースは、シェルからコマンドを実行する際に利用される方法である。
例えば、シェルのプロンプトに対して、lsコマンド
を実行する。その際、シェルは、lsコマンド
を子プロセスとして起動させ、その終了を待ち、再び、次のコマンドを受け取る。つまり、シェルは終了することなく、親プロセスとして子プロセスの終了を待機する。
シェルから起動した lsコマンド
を中断したい場合は、キーボードの Ctrl-C を使うことで中断できる。この仕組みは、親プロセスとしてのシェルが、キーボードの Ctrl-C の押下を受け取り、子プロセスに、シグナル SIGINTを伝えるために、途中で中止できるのである。
一方、前述の1番目のケースでは、シェルのプロセスに変わって、新たなプロセスが起動する。その際、元のプロセスは終了して削除される。起動されたプロセスは、元のシェルと親子の関係ではなく、同じプロセス番号を持った、異なるプログラムのプロセスである。
exec 実行形式ファイル名
としてプロセスを起動すると、つまり、シェル(sh)の組み込みコマンド exec を頭に付けて起動したプロセスは、前述のケース1に相当する。そして、実行形式ファイル名
とだけで、シェルから起動されたプロセスは、2番目に該当する。
再びコンテナに話題を戻すことにする。コンテナ上のプロセスに対して、シグナルは、コンテナで実行されるプロセス PID 1 に対してのみ送信される。そのため、ラッパーシェルの中で exec
を付与しない場合、シェルの機能にも依存するが、本来の伝えたいアプリケーションのプロセスへシグナルが届か無い。すなわち、アプリケーションの実行プロセスに、シグナルが伝らないことになる。このような背景から、ラッパーシェルから、実行ファイルを起動する場合には、必ず exec
を入れるべきとの推奨である。
次は、wrapper.sh の実行形式ファイル a.out の起動で、exec
と入れた場合と、無い場合の比較である。両者とも Dockerfile の CMD 行は、CMD ["/bin/sh","/wrapper.sh"]
であり、wrapper.sh の exec 以外の条件は同じである。
ダイレクトにコマンドを起動したケース
$ cat wrapper.sh
/a.out
実行形式は、PID = 7 となり シェルの子プロセスとして動作している。
$ docker exec a5fd91768450 ps -ax
PID TTY STAT TIME COMMAND
1 pts/0 Ss+ 0:00 /bin/sh /wrapper.sh
7 pts/0 S+ 0:00 /a.out
execを先頭に付与してコマンドを起動したケース
$ cat wrapper.sh
exec /a.out
exec をセットした場合は、実行形式は PID = 1 で起動されており、コンテナへのシグナルが確実に届く状態になっている。
$ docker exec 056030d1d2e9 ps -ax
PID TTY STAT TIME COMMAND
1 pts/0 Ss+ 0:00 /a.out
6 ? Rs 0:00 ps -ax
a.outのソースコードは、https://www.geeksforgeeks.org/socket-programming-cc/
を利用した。
参考資料
- Always exec in Wrapper Scripts, http://www.projectatomic.io/docs/docker-image-author-guidance/
- What are the uses of the exec command in shell scripts?, https://stackoverflow.com/questions/18351198/what-are-the-uses-of-the-exec-command-in-shell-scripts
一時ファイルをマメに消す
次の二つのコンテナ cent-test:noc
と cent-test:clean
で実行できることは同じです。しかし、前者は 478MB であり後者は 390MB である。この違いは、Dockerfile の RUN コマンドの書き方で変わる。 コンテナ・イメージのサイズは小さい程に好ましいとされ、コンテナの起動時間を短くし、迅速なロールアウトに寄与するためである。
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
cent-test noc a3d4f72884dd 2 minutes ago 478MB
cent-test clean 93180b0f1cdd 3 minutes ago 390MB
centos 7 67fa590cfc1c 13 days ago 202MB
ここから、Dockerfileの書き方の違いについて確認する。まずはじめに、サイズの大きなイメージ cent-test:noc
をビルドするDockerfileを以下にリストする。
FROM centos:7
RUN yum update
RUN yum install -y gcc gmake gdb
ADD server.c /
ADD wrapper.sh /
RUN gcc server.c
このファイルでは、追加のOSパッケージのインストールについて、RUN yum install -y gcc gmake gdb
によって実行している。
一方、サイズの小さなイメージ cent-test:clean
では、上記の命令のあとに、&& yum clean all -y
を追加する。注意点として、同じRUNコマンドの行に続けて追加する。このyum clean all は、CentOSのリポジトリから、パッケージイントールのためにダウンロードしたキャッシュデータなどを削除する。
FROM centos:7
RUN yum update
RUN yum install -y gcc gmake gdb && yum clean all -y
ADD server.c /
ADD wrapper.sh /
RUN gcc server.c
CMD ["/bin/sh","/wrapper.sh"]
以下にコンテナ・イメージのレイヤーを表示して違いを確認する。この2つの相違点は、yum install
のレイヤーであることが解る。
Dockerfileの RUN や ADD コマンドごとにレイヤーが追加され、ファイルが見えなくなっても、イメージの中に保持されている。
$ docker history cent-test:noc
IMAGE CREATED CREATED BY SIZE
a3d4f72884dd About an hour ago /bin/sh -c #(nop) CMD ["/bin/sh" "/wrapper.… 0B
71fb0d597822 About an hour ago /bin/sh -c gcc server.c 9.01kB
c5ef47195e27 About an hour ago /bin/sh -c #(nop) ADD file:52da03827b037ae91… 12B
189719145f4c About an hour ago /bin/sh -c #(nop) ADD file:6f6fafdf5f0d372f0… 1.48kB
8d67c70be40c About an hour ago /bin/sh -c yum install -y gcc gmake gdb 165MB <<--注目
4b8b0e9a6c18 About an hour ago /bin/sh -c yum update 111MB
67fa590cfc1c 13 days ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 13 days ago /bin/sh -c #(nop) LABEL org.label-schema.sc… 0B
<missing> 13 days ago /bin/sh -c #(nop) ADD file:4e7247c06de9ad117… 202MB
$ docker history cent-test:clean
IMAGE CREATED CREATED BY SIZE
93180b0f1cdd About an hour ago /bin/sh -c #(nop) CMD ["/bin/sh" "/wrapper.… 0B
9388dbf6d403 About an hour ago /bin/sh -c gcc server.c 9.01kB
5838bed80e4f About an hour ago /bin/sh -c #(nop) ADD file:52da03827b037ae91… 12B
1c81852471ee About an hour ago /bin/sh -c #(nop) ADD file:6f6fafdf5f0d372f0… 1.48kB
0b154ae2cc10 About an hour ago /bin/sh -c yum install -y gcc gmake gdb && y… 76.7MB <<--注目
4b8b0e9a6c18 About an hour ago /bin/sh -c yum update 111MB
67fa590cfc1c 13 days ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B
<missing> 13 days ago /bin/sh -c #(nop) LABEL org.label-schema.sc… 0B
<missing> 13 days ago /bin/sh -c #(nop) ADD file:4e7247c06de9ad117… 202MB
このため、RUN yum install の同一行に、後続コマンド && の記号を付けて、次のレイヤーを重ねる前に、削除するべきである。さらに、1つのRUNステートメントで複数のコマンドを実行すると、イメージのレイヤー数が減り、ダウンロードと抽出の時間が短縮されます。
Red Hatのドキュメントを参考にしながら、これを検証しているが、これはLinuxディストリビューションによって差があることが解った。yum を apt-get に変更して、パッケージ名も合わせて変更して試したところ、Ubuntu Linux では次の結果となった。
apt-get install
を実行したあと、不要な一時ファイルは消去してくれているためと思われる。
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu-test noc bed6a7b8da54 17 seconds ago 321MB
ubuntu-test clean 84b05b9cc0b4 3 minutes ago 321MB
ubuntu 18.04 a2a15febcdf3 2 weeks ago 64.2MB
適切な順番でコマンド記述
これは、Dockerfile に記述するコマンドの順番により、ビルド時間を短くなったり、毎回長くなるという考慮点である。
次のDockerfileを使った場合、4行目の server.c ソースコードに修正があっても、ビルド時間への影響が少ない。
1 FROM centos:7
2 RUN yum update
3 RUN yum install -y gcc gmake gdb && yum clean all -y
4 ADD server.c /
5 ADD wrapper.sh /
6 RUN gcc server.c
7 CMD ["/bin/sh","/wrapper.sh"]
このDockerfileで、初回のビルド時に、ベースイメージのダウンロードの次に時間を要する処理は、2行と3行目のパッケージのインストールである。4行明以降は、ファイル追加とコンパイルが再実行されるだけとなり、短時間で終了する。初回のビルド時間では、コンテナのベースイメージのダウンロードとパッケージのダウンロードで46秒程度の時間を要したが、2回目以降は、以下の通り、ソースコードのコンパイル時間とほぼ同じ5秒程度台となった。
$ time docker build -t centos-test:clean -f Dockerfile.clean1 .
<中略>
Successfully built 6d29ec1a134e
Successfully tagged centos-test:clean
real 0m5.742s
user 0m1.755s
sys 0m1.106s
ところが、コンテナビルドのコマンドの並びを、次の Dockerfile.clean2のように記述した場合、アプリケーションのソースコード servce.c が修正されること、3行目位以降のレイヤーが、無効として扱われ、以降のイメージ・レイヤーが再利用されなくなる。
1 FROM centos:7
2 ADD server.c /
3 ADD wrapper.sh /
4 RUN yum update
5 RUN yum install -y gcc gmake gdb && yum clean all -y
6 RUN gcc server.c
7 CMD ["/bin/sh","/wrapper.sh"]
そのため、4行目以降が再実行されることになり、下記のように、ビルド時間は約27秒と長くなる。
$ time docker build -t centos-test:clean -f Dockerfile.clean2 .
<中略>
Successfully built ab5a661bd731
Successfully tagged centos-test:clean
real 0m26.825s
user 0m1.740s
sys 0m1.038s
もちろん、FROMに指定するベースイメージに更新があった場合には、ビルド時間を要するが、アプリケーションのソースコードやモジュールなどのような変更が頻繁に発生するコードは、Dockerfileの後半に集めることで、コンテナ・イメージの再利用性が高まり、ビルド時間を短くすることができる。
重要なポートをマークする
DockerfileのEXPOSEに、コンテナ上のアプリケーションがリクエストを受け付けるポード番号を明示的に記述して、コンテナを再利用しやすくする。このEXPOSEにポート番号を設定することで、コンテナの動作中に、docker ps や docker inspect コマンドによって、受け付けるポート番号を知ることができる。しかし、実際にアプリケーションのコードが、EXPOSEに設定されたポートで、リッスンしていなければならない。このEXPOSEは、アプリケーションの動作に影響を与えるものではなく、人が解りやすく扱えるようにするためのマークである。
コンテナホストの外部、すなわち、ネットワーク上のクライアントからリクエスを受けられるようにするには、docker run -p 外部ポート番号:コンテナ内ポート
のように指定が必要であり、EXPOSEでマークするだけでは実現できない。
環境変数を活用する
Dockerfile の 環境変数設定コマンドENV
の有効に活用する。 環境変数を利用して、コンテナ内部の動作を操作することによって、再利用に際して、再ビルドを回避して、コンテナの再利用性を高める事ができる。 例えば、設定ファイルのパス、永続ボリュームのパス、機能選択のオプション設定、認証情報など、多くの情報をコンテナ外部から与える事が可能。
コンテナ・イメージにパスワードを埋め込まない
コンテナ・イメージは、広範囲の共有と再利用を目的としたアセットであるため、その内部にデフォルトのパスワードを設定するべきではありません。
埋め込まれたデフォルトのパスワードは、セキュリティ問題になる可能性があるためです。
パスワードなどの秘匿性の必要な情報は、Kubernetes (OpenShift) の Secret などを利用して、環境変数やパスとして、コンテナ外部から与えるべきです。
コンテナ上でsshdを使用しない
コンテナ内にsshdを起動した場合の不都合な点は、前述の通りであり、sshdの起動を避ける事が賢明である。実行中のコンテナ内で問題判別などの作業をしたい場合、代替の手段として、Dockerの場合は docker exec
コマンド、Kubernetes と OpenShift では kubectl exec
コマンド、また OpenShift ではタイプ数が少なくて済む oc exec
を利用する。
データの保存に永続ボリュームを使用
コンテナは、プロセスと同じように、一時的な存在であり、たとえその内部にファイルシステムがあっても、そこに保存されたファイルは、コンテナの停止と共に失われる。そのため、コンテナ上のアプリケーションのデータ保存は、永続ボリュームに書き込まなくてはならない。そこで、Dockerfile の VOLUME
コマンドを利用して、コンテナ上のアプリケーションが必要とする永続ボリュームを明示する。
次のDockerfileは、永続ボリュームにデータを書き出す最も単純なコードである。この中の3行目 VOLUME
によって、このコンテナは、その外部に永続ボリュームを必要とする事が、イメージの利用者は判別できる。
FROM ubuntu:18.04
RUN mkdir /myvol
VOLUME /myvol
CMD /bin/echo "hello world" > /myvol/greeting
これにより、dockerコマンドの実行の際に「-v pwd
/vol:/myvol vol:0.1」のオプションをつけて、実行結果を書き込むべき、ボリュームを指定する。このオプションは -v コンテナホストのパズ:コンテナ内パス
となる。
$ docker build -t vol:0.1 .
<中略>
$ docker run -v `pwd`/vol:/myvol vol:0.1
$ cat vol/greeting
hello world
Kubernetes (OpenShift) では、永続ボリュームを動的にプロビジョニングする機能、そして、ポッド(コンテナ)との関係を保つコントローラーがあるので、合わせて利用を検討する。
OpenShift Container Platform固有のガイドライン
S2Iツールを利用する
OpenShift Container Platform (以下 OCP) は、元々は CloudFoundry のような PaaS を目指して開発されたので、アプリのソースコード等から、コンテナをビルドしてアプリを実行する機能がある。また、コンテナ・イメージを指定してアプリも実行をできる。そして、OCPのCICD機能を利用して、コンテナをビルドすることが推奨されている。
このPaaS機能の開発ツールの一つである「Source-to-Image (以下 S2I)」は、OCPがなくても、ローカルマシンにインストールして、アプリのソースコードから、コンテナ・イメージのビルドに利用できる。S2Iの用途は、OCPで実行する前にアプリケーションとイメージをローカルでテストし、検証するための利用するツールである。そして、最終的には oc new-app
によって、OCP上でアプリを実行できる。以下に、その手順を箇条書きにする。
コンテナビルドの手順
- アプリに必要なビルダーイメージを特定する。Red Hat は、Python、Ruby、Perl、PHP および Node.js など各種の言語のビルダーイメージを複数提供、他のイメージはコミュニティースペース から取得する。
- S2I は、Git リポジトリーまたはローカルのファイルシステムのソースコードからイメージをビルドできる。ビルダーイメージおよびソースコードから新しいコンテナーイメージをビルドする。
$ s2i build <source-location> <builder-image-name> <output-image-name>
には GitのURL、またはローカルのソースコードのディレクトリのいずれかを指定できる。
- Dockerでビルドしたイメージをテスト
$ docker run -d --name <new-name> -p <port-number>:<port-number> <output-image-name>
$ curl localhost:<port-number>
- テストが完了したイメージを OpenShiftリポジトリへにプッシュ
$ docker tag <local-repository-name> <OpenShift-repository-name>
$ docker push <OpenShift-repository-name>
- ocコマンドで、OpenShiftリポジトリからアプリケーションを実行
$ oc new-app <image-name>
参考資料
- S2Iツール, https://access.redhat.com/documentation/ja-jp/openshift_container_platform/3.11/html/developer_guide/migrating-applications#dev-guide-s2i-tool
- S2Iツールのダウンロード, https://github.com/openshift/source-to-image/releases/tag/v1.1.14
- Source-To-Image (S2I), https://github.com/openshift/source-to-image
- Python container images, https://github.com/sclorg/s2i-python-container
- S2I, https://docs.openshift.com/container-platform/3.11/creating_images/s2i.html#creating-images-s2i
- SOURCE-TO-IMAGE (S2I)概説, https://access.redhat.com/documentation/ja-jp/openshift_container_platform/3.9/html/using_images/source-to-image-s2i
任意のユーザーでコンテナ実行に対応する
デフォルトで OCPはrootでのコンテナを実行を禁止している。これによって、コンテナエンジンの脆弱性が原因で権限が昇格されることを防いている。
root以外の任意のユーザーでコンテナを実行する場合、コンテナ内のrootオーナーのファイルやディレクトリに対する読込みと書込み権限がなく障害になる。さらに、実行する権限が必要である。
対策として、Dockerfileに、次の1行を追加することで、必要なアクセス権限を、任意のユーザーへ与えることができる。
RUN chgrp -R 0 /some/directory && chmod -R g=u /some/directory
コンテナのユーザーは常にルートグループのメンバーであるため、ディレクトリのグループをrootにして、グループのアクセス権限をユーザーに与える(g=u)とすることで問題を解決できる。ルートグループには、rootユーザーとは異なり特権がないため、この処置にはセキュリティ上の懸念はない。また、注意点として、コンテナで実行されているプロセスは、特権ユーザーではないため、特権ポートすなわち1024未満のポートでリッスンすることができない。
コンテナをrootユーザー以外で実行する場合には、/etc/passwdにエントリが存在しないため、任意ユーザーを/etc/passwdファイルに追加する必要がある。passwdへの追加操作は、uid_entrypoint
シェルで実行するが、その前に、グループ権限をユーザーに付与(g=u)する。そして、環境変数 USER_NAME にユーザー名、USERに uid(数値)を設定する。
RUN chmod g=u /etc/passwd
ENV USER_NAME user-name
USER 1001
ENTRYPOINT [ "uid_entrypoint" ]
uid_entrypointの内容は、Dockerfileの全体と共に提示する。DockerfileのUSER宣言は、ユーザー名ではなくユーザーID(数値)を指定しなければならない。これにより、OCPは、コンテナ・イメージを実行しようとしているユーザーの権限を検証し、ルートとしての実行を防ぐ。
以下に、Dockerfileの全体とbin以下のファイルのリストをあげ、実行例を提示する。
$ tree .
.
├── Dockerfile
└── bin
├── run-app
└── uid_entrypoint
Dockerfile
FROM centos:7
# アプリ用ディレクトリ作成
ENV APP_ROOT=/app
RUN mkdir ${APP_ROOT}
# アプリの実行ファイルをコピー、実行権を付与
COPY bin/ ${APP_ROOT}/bin/
RUN chmod +x ${APP_ROOT}/bin/*
# アプリのディレクトリをルートグループへ変更
# binのファイルの実行権を付与
RUN chgrp -R 0 ${APP_ROOT} && chmod -R g=u ${APP_ROOT}
# PATH環境変数の変更
ENV PATH=${APP_ROOT}/bin:${PATH} HOME=${APP_ROOT}
# passwdファイルもルートグループでアクセス可能にする
RUN chmod g=u /etc/passwd
# 作業用ディレクトリを変更
WORKDIR ${APP_ROOT}
# USSRに対応するユーザー名を環境変数にセットする
# この環境変数は、ENTRYPOINTのコマンドで利用される
ENV USER_NAME takara
# 重要 ユーザー名ではなくIDでセットする
USER 10001
# /etc/passwdにエントリを追加する
# ここでは、ユーザー名:takara、UID:10001として追加される
# 詳しくはuid_entrypointのシェルを参照のこと
ENTRYPOINT [ "uid_entrypoint" ]
# アプリケーション実行用シェル execでアプリを実行が必須
CMD [ "run-app" ]
bin/run-app
これは模擬アプリケーションのため、tail -f /dev/null
でコンテナが終了することを止めている。実際のアプリでは、この部分を置き換える。
#!/bin/sh
# アプリケーションの起動用ファイル
# tail -f /dev/null を置き換えること
id
whoami
exec tail -f /dev/null
bin/uid_entrypoint
パスワードファイルにユーザーを追加するためのシェル
#!/bin/sh
# /etc/passwdへ、アプリを実行するためのUSER_NAMEとUSER(uid)のエントリを追加する
if ! whoami &> /dev/null ; then
if [ -w /etc/passwd ]; then
echo "${USER_NAME:-default}:x:$(id -u):0:${USER_NAME:-default} user:${HOME}:/sbin/nologin" >> /etc/passwd
fi
fi
# 他に初期化事項があれば以下に追加する、ただし、USERで指定する一般実行権限のため注意
# 引数をつければコマンドも実行可能
exec "$@"
実行例
# Dockerfileが存在するビルドコンテキストの確認
$ ls
Dockerfile bin
# コンテナのビルド
$ docker build -t test:0.1 .
Sending build context to Docker daemon 5.632kB
Step 1/13 : FROM centos:7
---> 67fa590cfc1c
Step 2/13 : ENV APP_ROOT=/app
---> Using cache
---> 7b2fdfb0ab67
Step 3/13 : RUN mkdir ${APP_ROOT}
---> Using cache
---> 779c4e5803f5
Step 4/13 : COPY bin/ ${APP_ROOT}/bin/
---> Using cache
---> dbe9f12115ed
Step 5/13 : RUN chmod +x ${APP_ROOT}/bin/*
---> Using cache
---> 3efd1e34806d
Step 6/13 : RUN chgrp -R 0 ${APP_ROOT} && chmod -R g=u ${APP_ROOT}
---> Using cache
---> 741669a28876
Step 7/13 : ENV PATH=${APP_ROOT}/bin:${PATH} HOME=${APP_ROOT}
---> Using cache
---> 9406fd40749b
Step 8/13 : RUN chmod g=u /etc/passwd
---> Using cache
---> ccc38f6c9079
Step 9/13 : WORKDIR ${APP_ROOT}
---> Using cache
---> c42a99339133
Step 10/13 : ENV USER_NAME takara
---> Using cache
---> e7eb9c1f3c04
Step 11/13 : USER 10001
---> Using cache
---> 54a066697930
Step 12/13 : ENTRYPOINT [ "uid_entrypoint" ]
---> Using cache
---> 381b56457043
Step 13/13 : CMD [ "run-app" ]
---> Using cache
---> ebc50fb0755b
Successfully built ebc50fb0755b
Successfully tagged test:0.1
# コンテナをバックグラウンドで実行
$ docker run -d --name test1 test:0.1
57af4140dc0e5cd6cd7794fcaf7e8cad09da3e98451e3e19c9aa19a521c88a75
# 実行中コンテナに対話型シェルを起動
$ docker exec -it test1 bash
# コンテナ内部を確認
bash-4.2$ pwd
/app
bash-4.2$ ls -lR
.:
total 4
drwxrwxr-x 1 root root 4096 Sep 23 11:13 bin
./bin:
total 8
-rwxrwxr-x 1 root root 139 Sep 23 10:39 run-app
-rwxrwxr-x 1 root root 483 Sep 23 10:43 uid_entrypoint
# パスワードファイルのエントリ追加の確認
bash-4.2$ cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
bin:x:1:1:bin:/bin:/sbin/nologin
<中略>
takara:x:10001:0:takara user:/app:/sbin/nologin
# アプリのプロセスが、PID=1で実行されていることを確認
bash-4.2$ ps -ax
PID TTY STAT TIME COMMAND
1 ? Ss 0:00 tail -f /dev/null
10 pts/0 Ss 0:00 bash
18 pts/0 R+ 0:00 ps -ax
参考資料
コンテナ間の通信に Kubernetesサービスを利用する
OCPの実態は、Kubernetesであるため、コンテナはポッドとして実行される。ポッドは一時的な存在として扱われるので、IPアドレス起動のたびに付与され一定しない。そのため、ポッド間の連携には Kubernetes の APIオブジェクトの一つである Service を利用する。また、ポッド内のコンテナ同士の連携は、POSIX準拠のプロセス間通信、localhostでポート番号を指定したソケット通信、ファイル共有など、サイドカーとして知られるポッドで利用できる通信手段を利用する。
共通ライブラリを提供
アプリケーション・コードを実行するために必要なライブラリ等を含んだ共通イメージを作成することを推奨する。例えば、Javaアプリを開発する場合、データベースをアクセスするためのJDBCドライバが必須であり、このような基本的なライブラリを含有するイメージを作成して、開発チームで共有することで、アプリのイメージのビルドが高速化される。また、アプリ開発者が必要とする作業を簡素化して、すべての依存関係が満たされる。
構成に環境変数を利用
コンテナ・イメージは環境変数を利用して、コンテナ実行時の設定をおこなうことで、再利用性を高めることができる。例えば、ボリュームのマウント、パスワード、設定ファイルのパスなどを、環境変数を利用してコンテナへ与えることで、再ビルドすることなく、KubernetesのYAMLファイルの設定だけで、再利用できる。
OCP (Kubernetes)では、ポッドのコンテナに与えられたメモリの使用最大容量を越えると、コンテナのプロセスを強制終了して、再スタートさせる。このため、Javaを実行する場合には、OCP(Kubernetes)のマニフェストから、Java VM のヒープサイズを与えることで、トラブルを回避できる。
Dockerfileにメタデータを設定する
OCPのCICD機能はPaaSの様な機能を提供している。その機能の中では、Dockerfileを読んで、アプリケーションをビルドして実行までを、一つのコマンドで実行してくれるといった、開発者にとって便利な機能を提供している。この機能がより良く働くために、DockerfileのLABEL命令を利用して、メタ情報を記述することで、詳細な制御ができる。
参考資料
クラスタリング
OCP (Kubernetes)のポッドは起動時やノード間の移動時に、動的にIPアドレスが付与され一定しません。つまり、ポッドのクラスタは動的であることを理解していなければなりません。これは、多くのフレームワークでは、クラスタメンバーの中からリーダーを選出する場合や、フェイルオーバー時に、お互いのIPアドレスを知ることが必要となる。ポッドのIPアドレスを求めることは可能ですが、一定しないことを認識していることが重要である。
ロギング
コンテナ上のアプリケーションは、全てのログをSTDOUTへ書き出すべきである。OCP (Kubernetes)は、コンテナのSTDOUTを収集して、集中ログサービスに送信して表示できるようにする。ログの内容を分離する場合は、適切なキーワードを出力の前に付けて、メッセージをフィルターできるようにする。
もしも、アプリケーションが、コンテナのファイルにログを記録すると、ここのコンテナからログを収集する必要があるが、コンテナが稼働するポッドのIPアドレスは動的に変化するなど、非効率な作業を強いられるため、STDOUTヘ書き出すことが賢明である。
プローブ
コンテナには、liveness probes (活性プローブ)と readiness probes (準備状態プローブ) に対応する実装を考慮するべきある。活性プローブが続けて失敗を返すと、ポッドを再起動して回復を試みる。また、準備状態プローブが真を返すまで、サービスはリクエストを転送しない。 これらのプローブは、OCPが基礎として利用するKubernetesに実装された機能である。
テンプレート
Kubernetesには、コントローラのマニフェストに、ポッドテンプレート(podTemplate)などのテンプレートを記述しておき、コントローラがポッドを起動する際に、テンプレートのスペックに記述に従って実行する。OCPのテンプレートは、独自に拡張されたAPIで、BuildConfigやDeploymentConfigコントローラなど、起動するべきオブジェクトをテンプレート化するものである。この機能で登録されたテンプレートは、Webコンソールのカタログから、迅速にアプリケーションを起動することができる。とても便利な機能なので利用を検討すると良い。
参考資料
-
Templates, https://docs.openshift.com/container-platform/3.11/dev_guide/templates.html#dev-guide-templates
-
日本語Docs 第10章 TEMPLATES (テンプレート)、https://access.redhat.com/documentation/ja-jp/openshift_container_platform/3.11/html/developer_guide/dev-guide-templates
まとめ
コンテナをビルドすること、OCPやK8sで実行するための考慮点であるが、範囲が広く、箇条書きでまとめるのは難しいので、敢えてまとめることは見送りたい。 とても役に立つ内容と思うので、是非、本文を読んで欲しい。