以前 Rake で Docker コンテナを管理するためのテンプレート権ツールを作成した。
その機能のひとつとして Docker のホスト側に Nginx 用のリバースプロキシを設定するものがあった。設定後はコンフィグを自動的に読み直す。
これらの処理には root 権限が必要だった。権限はスクリプト中の sudo
コマンドで得ていた。
sh "sudo echo -e \"$data\" > /path/to/nginx/config"
sh 'sudo service nginx reload'
実行するとエラーが起きた。
権限エラーだ。
Rake 経由だと sudo
が使えないのだろうか。だが、一応 1 行目を消して実行してみたら無事に Nginx コンフィグがリロードされた。
なぜ。
この問題はスクリプトの中で sudo
を使わないようにして Rake 自体を sudo
で実行すれば解決する。
しかし今回のツールでは、全ての処理に管理者権限が必要なわけではない。普段権限なしで実行していて特定の場面でだけエラーが起きるのは、たぶん数ヵ月後に出会うと小さなストレスだ。
必要になればパスワードを求める仕様にしたかった。
sudo
経由で書き込むことが今回の目的だ。
環境
- Ubuntu 15.04
- sudo 1.8.9p5
- Ruby 2.2.3p173
- Rake 10.5.0
状況と目的の再定義
序文が不必要に長くなった上に Rake 依存の問題のように読めなくもないので、改めて状況を説明する。
Bash で再現するとこういう状況。
$ touch rootfile
$ sudo chown root:root rootfile
$ sudo echo 1 > rootfile
bash: rootfile: Permission denied
sudo
を使って root 権限で書き込むのが本記事の目的。
解決方法
いくつか試して、目的を達成できた方法を書いていく。
1. mv
最初にやったのはこの方法。
echo 1 > rootfile.temp
sudo mv rootfile.temp rootfile
書き込みたい内容を一時ファイルに保存して、それを mv
によって目的の場所に移動、または上書きする。
でもこれだと所有者情報に違和感があるし (sudo
付きで echo
で実行しても、書き出したファイルの所有者は sudo
を実行した元ユーザーのものになる) 、既存のファイルに上書きする場合は inode も変わってしまう。
どちらも今回の処理では問題にならないが、別の処理では問題になる可能性がある。余計な変化はできるだけ避けたい。
2. tee
sudo
に頼った書き込みをする場合、必ず一度はシェル用のコマンドを使うことになる。
そこでまず思いついたのは echo
や cat
とリダイレクト記号 >
を組み合わせたものであり、そのつまずきが序文での出来事だ。
他にビルトインで書き込みができるものは何かないか、と探してみると tee
があった。
echo 1 | sudo tee rootfile
標準出力が気になる場合は /dev/null に捨てる。
3. sh -c
どうやら sudo
ではリダイレクトを使った書き込みが出来ないらしい。
先日の記事「sudo 時に root 権限で実行されてるかを確認する」を書いたときに知ったのだが、この仕様は man sudo
でも触れられている。
To make a usage listing of the directories in the /home partition. Note that this runs the commands in a sub-shell to make the cd and file redirection work.
$ sudo sh -c "cd /home ; du -s * | sort -rn > USAGE"
この例のように sudo sh -c
の中にリダイレクト処理を含めれば、問題なく書き込みができる。
sudo sh -c 'echo 1 > rootfile'
標準入力も扱える。
echo 1 | sudo sh -c 'cat - > rootfile'
4. ruby -e
おまけ。
今回自分がぶつかったケースの場合 Rake を使っているということで、当然そこには Ruby の実行環境がある。
以下のようにすれば sudo
経由で Ruby の機能を使って書き込みができる。
echo 1 | sudo ruby -e "File.write('rootfile', STDIN.read)"
Ruby で記述する場合はこう。
IO.popen("sudo ruby -e \"File.write('rootfile', STDIN.read)\"", 'w') do |io|
io.puts('1')
end
ただし sudo
は基本的に環境変数を引き継がない。引き継ぐようにもできるが、面倒くさい。
rbenv などでユーザーローカルに Ruby をインストールしている場合はその面倒事をやるか、 sudo
経由で呼び出している ruby
の部分をフルパスに書き換える必要がある。
開発環境や、開発環境をまたいで使う場合には向かない。
まとめ
使うとしたら 2 か 3 だろうか。