Help us understand the problem. What is going on with this article?

コンテナ開発のセキュリティとOpenShift特有のガイド

Red Hat General Container Image Guidelinesは、非常に重要で良い題材を扱っているにも関わらず、説明が不足したりして、勿体無い印象を受けた。そこで、読み解いてみることにした。この記事は、読者の理解であり誤った内容を含む可能性があります。もし発見されたら、是非コメント頂ければ幸いです。

コンテナ・イメージのビルド・ベストプラクティス

コンテナ・イメージのビルドについて、ベスト・プラクティスとなる記事を、以下にリストする。基礎的なことであったり、関連の深いことなので、見比べながら、理解する参考にした。

  1. Docker Docs Best practices for writing Dockerfiles
  2. Dockerfile を書くベスト・プラクティス 日本語訳
  3. Red Hat General Container Image Guidelines
  4. 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コマンドの資格情報となる。

参考資料

  1. 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
  2. 10 things to avoid in docker containers, https://developers.redhat.com/blog/2016/02/24/10-things-to-avoid-in-docker-containers/
  3. Run multiple services in a container, https://docs.docker.com/config/containers/multi-service_container/
  4. Docker - one process per container?, https://stackoverflow.com/questions/30003338/docker-one-process-per-container
  5. 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 すなわち、コンテナで、新たにプログラムを起動する際に、システムコールのレベルの二つのケースである。

  1. 自己プロセスを終了させて、新たなプロセスを起動するケース
  2. 自己プロセスの子プロセスとして、新たなプロセスを終了後、起動したプロセスが継続するケース

上記の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 以外の条件は同じである。

ダイレクトにコマンドを起動したケース

wrapper.sh
$ 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を先頭に付与してコマンドを起動したケース

wrapper.sh
$ 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/ を利用した。

参考資料

一時ファイルをマメに消す

次の二つのコンテナ cent-test:noccent-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を以下にリストする。

noc
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のリポジトリから、パッケージイントールのためにダウンロードしたキャッシュデータなどを削除する。

Dockerfile.clean
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 ソースコードに修正があっても、ビルド時間への影響が少ない。

Dockerfile.clean1
  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行目位以降のレイヤーが、無効として扱われ、以降のイメージ・レイヤーが再利用されなくなる。

Dockerfile.clean2
 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上でアプリを実行できる。以下に、その手順を箇条書きにする。

コンテナビルドの手順

  1. アプリに必要なビルダーイメージを特定する。Red Hat は、Python、Ruby、Perl、PHP および Node.js など各種の言語のビルダーイメージを複数提供、他のイメージはコミュニティースペース から取得する。
  2. S2I は、Git リポジトリーまたはローカルのファイルシステムのソースコードからイメージをビルドできる。ビルダーイメージおよびソースコードから新しいコンテナーイメージをビルドする。
$ s2i build <source-location> <builder-image-name> <output-image-name>

には GitのURL、またはローカルのソースコードのディレクトリのいずれかを指定できる。

  1. Dockerでビルドしたイメージをテスト
$ docker run -d --name <new-name> -p <port-number>:<port-number> <output-image-name>
$ curl localhost:<port-number>
  1. テストが完了したイメージを OpenShiftリポジトリへにプッシュ
$ docker tag  <local-repository-name> <OpenShift-repository-name>
$ docker push <OpenShift-repository-name>
  1. 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行を追加することで、必要なアクセス権限を、任意のユーザーへ与えることができる。

Dockerfile抜粋
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(数値)を設定する。

Dockerfile抜粋
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/run-app
#!/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コンソールのカタログから、迅速にアプリケーションを起動することができる。とても便利な機能なので利用を検討すると良い。

参考資料

まとめ

コンテナをビルドすること、OCPやK8sで実行するための考慮点であるが、範囲が広く、箇条書きでまとめるのは難しいので、敢えてまとめることは見送りたい。 とても役に立つ内容と思うので、是非、本文を読んで欲しい。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away