端書き
最近Jenkinsとかでテスト書く上でDockerのお世話になっている。
プロジェクトごとの依存関係とかCI環境にいれなくていいのがとても素敵なのだけど、一つだけ不便な点がある。
そのままだとテストがrootで走ってしまう問題。
とくにテスト結果や生成物を保存したくてワークスペースを -v でコンテナにマウントしていると、root権限で書かれてしまってワークスペースを削除したりが失敗する。
というかテスト自体、root権限で動かせないケースもあったりなかったりなので、一般ユーザ権限で動かしたい。
Jenkinsの Docker Pipeline Plugin なんかだと -u オプションでユーザ権限で走らせていて、一見これで一件落着に見えるのだけど、地味に問題がある。
一つは存在しないユーザの可能性があること。そしてもう一つがroot権限を失うこと。
最初はこれでいいじゃないか、と思っていたけど結構地味にやっかい。
なんで問題なの?
ユーザがいなくてもuidがプロセスに設定されていれば問題ないじゃないか、root権限でファイル書きたくないって言ってるのになんてroot権限欲しいのさ。
ということなのだけど、理由はいくつか。
まずはホームディレクトリが欲しい。
rpmbuildとか素で叩くと ~/rpmbuild
を使おうとするのだけど、HOMEが設定されないので /rpmbuild
を使おうとしちゃう。
オプションで _topdir
設定すればいい話ではあるのだけど、別にBUILDとか保存したくないし、そも作業ユーザで書き込みできるディレクトリが他に欲しいという要望。(/tmpは書けるけど)
無ければ作ればいいじゃない、と思うのだけど、もう一つの問題、-uを付けた時点でコンテナ内ではroot権限を失っているので作ることも出来ない。
コンテナを作る時点であらかじめ作っておけば、と思っても作業ユーザのuidが何番になるのかの情報はその時点では無い。
Jenkins用と割り切って、あらかじめjenkinsユーザのuidを決め打ちしておくのも手なんだけど、なんか嫌。
というか開発中にも叩きたいことあるし。
あと、uidが非実在ユーザだったり、コンテナの中の別のユーザとかぶることがあって、それも結構問題になる。
sudoとか、あとmockなんかもそうだけど、起動時にgetentとかでユーザの情報取得してグループをチェックするようなコマンドが軒並み使えなくなる。
gidだけじゃなくてグループ名を付与したいのさ。という話。
結構ニッチな要望なのかもだけど地味にやっかいな問題で、ここ最近色々試行錯誤してなんとなく形が見えてきたので備忘録がてらメモ
私的ソリューション
CMDに複雑な指定はしたくない、汎用的に使いたい、ということでentrypointで頑張ってみることにした。
#!/usr/bin/env sh
set -e
# uid/gidは-uで指定せずに、-vでマウントされるであろうカレントディレクトリから取得する。
uid="`stat -c %u .`"
gid="`stat -c %g .`"
# 作業用ユーザ(user:staff)の作成。
# uid/gidがコンテナ内のユーザ/グループと衝突したときにuser:staffを優先にするためにファイルの先頭に追加。
# 普通にuseraddすると、uidがコンテナ内の既存ユーザと衝突したとき、getentが既存ユーザを優先してしまう。
# ちなみに 1i はsedのコマンドで、1行目の前の行(つまり先頭行)に行を追加するコマンド。
sed "1iuser:x:$uid:$gid:user:/home/user:/bin/sh" -i /etc/passwd
sed "1istaff:x:$gid:" -i /etc/group
# ホームディレクトリを作成
# ホームディレクトリの雛形 /etc/skel の中身を転送しておく
mkdir -p /home/user
find /etc/skel/ -mindepth 1 -maxdepth 1 -exec cp -r '{}' /home/user \;
chown user: -R /home/user
# 作ったユーザに何かしら特権権限で作業したいときのためのスクリプト類を /etc/provision.d/ に配置しておく
# 主な用途は作業用ユーザにグループをを追加したりなどか。 `usermod -a -G mock user`
# あらかじめコンテナにsudoerの設定しておいて、userにwheel渡しておくだけでも十分かも。というかそっちのほうが小回り効きそう。
for s in /etc/provision.d/*.sh; do
[ -e "$s" ] || continue;
${s}
done
# CMDの実行
# ログイン処理を走らせるために一回bash -lを挟む
command="$@"
su -c "cd '${PWD}'; bash -l -c '${command}'" user
こんなスクリプトをあらかじめ用意。
作業ディレクトリをコンテナにマウントしつつ、entrypointに上記のスクリプトを指定して実行する。
$ docker run -v ${PWD}:${PWD} -w ${PWD} --entrypoint=entrypoint.sh some_image ...
これで、CMDでは特に意識することなく、普通のユーザで普通にコマンドを叩く想定で書けばok。
あまり需要がない要望かもだし、私の使い方/想定がそもズレいているのかもだけど、現状これで満足。