« ウノウってこんな会社です | メイン | 最強のIDEを追い求める Eclipse + Aptana + TruStudio (+RadRails) »

ベンチャー流サーバ構築のススメ(同期ツール編)
このエントリーをブックマークに追加 このエントリーをlivedoorクリップに追加

ダイエット中で炭水化物の量が気になる尾藤正人です。

前回のエントリベンチャー流サーバ構築のススメ(ソフトウェア編)では、主にOS周りのことについて書きました。複数台のサーバを管理するのに重要なのは極力構成を同じにすることです。そうすることで管理コストが大幅に下がります。

以前Klabさんのサーバ管理者向け無精のすすめ ~ちょっと便利なツールの紹介~というエントリで同期ツールの紹介がありましたが、ウノウでも同じような感じの独自ツールを作って同期をとっています。今回はこの同期ツールの紹介をしたいと思います。

僕が shell scripter ということもあってスクリプトは全て sh で。zsh の特殊な記法が使いたかったので zsh で書いています。

凡例

全てのコマンドは最後に対象とするホスト名を指定します。all というは特殊な指定で全てのサーバに対してリクエストが送られます。

共通関数

全てのスクリプトから読み込まれる共通関数です。

function absdirname()
{
        local dir=`dirname $1`
        if [ "x$dir" = "x." ]; then
                dir=$PWD
        elif ! echo $dir|grep '^/' > /dev/null; then
                dir=$PWD/$dir
        fi
        cd $dir
        echo $PWD
}

function absfilename()
{
        local dir=`absdirname $1`
        if [ "x$dir" = "x/" ]; then
                dir=""
        fi
        echo $dir/`basename $1`
}

function get_servers()
{
        local servers i=1 max=12
        while [ $i -le $max ]; do
                servers=(${servers[@]} "s$i");
                i=`expr $i + 1`
        done
        echo ${servers[@]};
}

dir_diff.sh directory host

ディレクトリ以下にあるファイルの diff を表示します。zsh の "<()" という記法を使ってます。これを使うと標準出力をファイルのように扱えて直接 diff に渡せるので便利です。

#!/bin/zsh
source `dirname $0`/sh_functions
PROGRAM_NAME=`basename $0`

function usage()
{
        echo Usage: $PROGRAM_NAME directory host
        exit $1
}

function _dir_diff()
{
        if [ -z "$1" -o -z "$2" ]; then
                echo "Lack of arguments" 1>&2
                return 1
        fi

        local source=`absfilename $1`
        diff -u <(ssh root@$2 find $source|sort) <(sudo find $source|sort)|grep '^\(+\|-\)'
}

# check arguments
if [ -z "$1" -o -z "$2" ]; then
        usage 1
fi

if [ "x$2" != "xall" ]; then
        _dir_diff $1 $2
        exit $?
fi
for host in `get_servers`; do
        echo ++++++++++++++++++++++++++++++++++++++
        echo + diff from $host
        echo ++++++++++++++++++++++++++++++++++++++
        _dir_diff $1 $host
        echo
done

rpm_diff.sh host

rpm パッケージの diff を表示します。サーバ間でパッケージの過不足がないかどうか確認するのに使います。

#!/bin/zsh
source `dirname $0`/sh_functions
PROGRAM_NAME=`basename $0`

function usage()
{
        echo Usage: $PROGRAM_NAME host
        exit $1
}

function _rpm_diff()
{
        if [ -z "$1" ]; then
                echo "You must specify a host name" 1>&2
                return 1
        fi

        diff -u <(ssh root@$1 rpm -qa|sort) <(rpm -qa|sort)|grep '^\(+\|-\)'
}

# check arguments
if [ -z "$1" ]; then
        usage 1
fi

if [ "x$1" != "xall" ]; then
        _rpm_diff $1
        exit $?
fi
for host in `get_servers`; do
        echo ++++++++++++++++++++++++++++++++++++++
        echo + diff from $host
        echo ++++++++++++++++++++++++++++++++++++++
        _rpm_diff $host
        echo
done

xdiff.sh source host

ファイルの diff をとります。設定ファイルがちゃんと同期されてるかどうか確認したりするのに便利です。

#!/bin/zsh
source `dirname $0`/sh_functions
PROGRAM_NAME=`basename $0`

function usage()
{
        echo Usage: $PROGRAM_NAME source host
        exit $1
}

# check arguments
if [ -z "$1" -o -z "$2" ]; then
        usage 1
fi

if [ "x$2" != "xall" ]; then
        _xdiff $1 $2
        exit $?
fi
for host in `get_servers`; do
        echo ++++++++++++++++++++++++++++++++++++++
        echo + diff from $host
        echo ++++++++++++++++++++++++++++++++++++++
        _xdiff $1 $host
        echo
done

xexec.sh command [options ...] host

host で指定されたサーバでコマンドを実行します。ライブラリが更新されたときに一気に全サーバで ldconfig 実行したりとか、他にもいろいろ使えます。

#!/bin/zsh
source `dirname $0`/sh_functions
PROGRAM_NAME=`basename $0`

function usage()
{
        echo "Usage: $PROGRAM_NAME command [options ...] host"
        exit $1
}

function _xexec()
{
        if [ -z "$1" -o -z "$2" ]; then
                echo "Lack of arguments" 1>&2
                return 1
        fi

        ssh root@$2 $1
}

# check arguments
if [ -z "$1" -o -z "$2" ]; then
        usage 1
fi

host=
command=
while [ -n "$1" ]; do
        command="$command $host"
        host=$1
        shift
done

if [ "x$host" != "xall" ]; then
        _xexec $command $host
        exit $?
fi
for host in `get_servers`; do
        echo ++++++++++++++++++++++++++++++++++++++
        echo + execute $command on $host
        echo ++++++++++++++++++++++++++++++++++++++
        _xexec $command $host
        if [ $? -ne 0 ]; then
                echo "Failed to execute on $host" 1>&2
                exit $?
        fi
        echo
done

xrsync.sh source host

その名の通り rsync します。ファイルを一斉にシンクロするのに便利です。

#!/bin/zsh
source `dirname $0`/sh_functions
PROGRAM_NAME=`basename $0`

function usage()
{
        echo Usage: $PROGRAM_NAME source host
        exit $1
}

function _xrsync()
{
        if [ -z "$1" -o -z "$2" ]; then
                echo "Lack of arguments" 1>&2
                return 1
        fi

        local opts source host
        while [ -n "$1" ]; do
                opts="$opts $source"
                source=$host
                host=$1
                shift
        done

        local source=`absfilename $source`
        local destination=`dirname $source`

        local opts="-auvz -e ssh $opts"
        eval sudo rsync $opts $source root@$host:$destination
}

# check arguments
if [ -z "$1" -o -z "$2" ]; then
        usage 1
fi

host=
arguments=
while [ -n "$1" ]; do
        arguments=(${arguments[@]} $host)
        host=$1
        shift
done

if [ "x$host" != "xall" ]; then
        _xrsync ${arguments[@]} $host
        exit $?
fi
for host in `get_servers`; do
        echo ++++++++++++++++++++++++++++++++++++++
        echo + rsync to $host
        echo ++++++++++++++++++++++++++++++++++++++
        _xrsync ${arguments[@]} $host
        if [ $? -ne 0 ]; then
                echo "Failed to execute on $host" 1>&2
                exit $?
        fi
        echo
done

xscp.sh source [source ...] host

こっちは scp 版。ちょっとしたファイルを同期したときに便利。

#!/bin/zsh
source `dirname $0`/sh_functions
PROGRAM_NAME=`basename $0`

function usage()
{
        echo "Usage: $PROGRAM_NAME source [source ...] host"
        exit $1
}

function _xscp()
{
        if [ -z "$1" -o -z "$2" ]; then
                echo "Lack of arguments" 1>&2
                return 1
        fi

        local sources host
        while [ -n "$1" ]; do
                sources=(${sources[@]} $host)
                host=$1
                shift
        done

        local opts="-pr"
        local source destination
        for source in ${sources[@]}; do
                source=`absfilename $source`
                destination=`dirname $source`
                eval sudo scp $opts $source root@$host:$destination
        done
}

# check arguments
if [ -z "$1" -o -z "$2" ]; then
        usage 1
fi

host=
sources=
while [ -n "$1" ]; do
        sources=(${sources[@]} $host)
        host=$1
        shift
done

if [ "x$host" != "xall" ]; then
        _xscp ${sources[@]} $host
        exit $?
fi
for host in `get_servers`; do
        echo ++++++++++++++++++++++++++++++++++++++
        echo + scp to $host
        echo ++++++++++++++++++++++++++++++++++++++
        _xscp ${sources[@]} $host
        if [ $? -ne 0 ]; then
                echo "Failed to execute on $host" 1>&2
                exit $?
        fi
        echo
done

まとめ

サーバ間の同期を取るためにウノウで導入しているツール群を紹介しました。こういう細かいツールを最初に作っておくと、作業が効率よく進み非常に楽になります。ぜひ、みなさんの環境でも導入してみてはいかがでしょうか。

何回か続けてきたベンチャー流サーバ構築のススメですが、おかげさまでみなさんから多くのトラックバック、コメント、ブックマークをいただきました。今回で大体サーバ構築に関するトピックは網羅したかと思います。何かありましたら、トラックバックなりコメントなりをいただければ幸いです。

トラックバック

このエントリーのトラックバックURL:
http://www.unoh.net/mt32/mt-tb.cgi/255

コメント

xrsync.sh "-e 'touch /tmp/hoge --'" /etc/passwd remotehost

とかすると、root権限でtouchが実行できちゃいますよー

sudo で root権限は保護してますが、もっと安全な方法ってありますか?

これってどういう意味でしょうか?>「sudo で root権限は保護してます」

-e オプションで指定したプログラムはローカルで実行されるので、ローカルのroot権限が奪取されることを危惧されているということですよね。
root権限の取得は全てsudo経由で行っており、特定ユーザ以外はrootでコマンドは実行できないようになっています。
rsync には suid ビットはたっていないので、一般ユーザが rsync 経由で root権限を取得できることは基本的にないと思います。

なんかちょっと誤解されているような気がします。

おっしゃる通り、rsyncの-eで指定したコマンドは、ローカルで実行され、*それはrsyncの実効ユーザ権限で実行*されます。

xrsync.shのコードでは、

eval sudo rsync ...

しているので、rsyncはroot権限でされますよね?

ということは-eのコマンドはrootで実行されますよね?

最初のコメントのように実行すると、/tmp/hogeのownerはrootになるはずです。

もっと強烈な例を挙げてみます。

$ cat /tmp/r00tsh.c
int main(int argc, char ** argv) {
system("/bin/sh");
return 0;
}
$ gcc -o /tmp/r00tsh /tmp/r00tsh.c

$ cat /tmp/setu+s
#!/bin/sh
chown root:root $1
chmod u+s $1

$ xrsync.sh "-e '/tmp/setu+s /tmp/r00tsh'" /tekitou/na/file remotehost

$ ls l /tmp/r00tsh
-r-sr-xr-x 1 root root 11356 Aug 8 15:15 /tmp/r00tsh
$ /tmp/r00tsh
sh-3.00# id
uid=2050(hirose31) gid=2050(hirose31) euid=0(root) groups=0(root),2050(hirose31)
sh-3.00# echo bad > /how_are_you
sh-3.00# cat /how_are_you
bad

rootになれるシェルのできあがりです。;p)

sudo で root 権限が取得できるアカウントであれば、わざわざ rsync 経由で実行する必要は全然なくて、直接コマンドを実行すればいいだけの話ではないですか?
なぜ、rsync 経由での実行に言及する必要があるんでしょうか

root 権限がとれるかどうかは rsync は全然関係なくて、sudo の設定がどうなっているかが問題でしょう
そのための sudo ですよね

あ、もしかして、「特定ユーザ以外はrootでコマンドは実行できないように」というのは、「特定ユーザは(rsyncに限らず)全てのコマンドをroot権限で実行できる」ようにsudoの設定がされている、というこでしょうか?

だとしたら、てっきり、「特定ユーザは特定のコマンド(rsync)のみをroot権限で実行できる」ようにsudoの設定がされている、と思い込んでいたので、話が噛み合わないわけですね…
# (もちろん、特定のコマンドとはいえroot権限でのrsyncの実行を許してしまったら全然ダメですけど)

おお、そういうことでしたか。
上記ツールは root 権限を持つ管理者が使用するコマンドなので、特定ユーザに開放したりとかはしてません。

確かに特定ユーザに rsync だけの root 権限での実行を許可した場合、hirose31 さんのおっしゃるような問題がありますね。

これは
local opts="-auvz -e ssh $opts"

local opts="-auvz $opts -e ssh"
にすると -e オプションが上書きされなくなって大丈夫っぽい。

ですです。

おっしゃる通り、もし、sudoでrsyncだけを許可する場合(これはよくないです。任意のファイルが上書きでちゃうので)、もしくは、(xrsync.shのような)内部でrsyncする特定のコマンドだけを許可する場合は、rsyncの末尾で「-e ssh」と書かないとまずいことが起こる可能性があります。

rsyncの-eは、複数ある場合は最後の-eが有効で、DESTの後にも書けるので、

eval sudo rsync $opts $source root@$host:$destination

eval sudo rsync $opts $source root@$host:$destination -e ssh

と書くのが確実ですね。($sourceや$desticationに-eを注入される危険性がないとはいえないので)

蛇足ですが、コピー元とコピー先のパスがいつも同じならば、rsyncの-Rオプションを使って

eval sudo rsync $opts -R $source root@$host:/ -e ssh

とすれば、ディレクトリの異なる複数のsourceのコピー (xrsync.sh /dir1/foo /dir2/bar desthost とか) もできるようになりますね。
# ($sourceを組み立てるところにもちょっと修正が必要ですが)


ところで、特定ユーザにsudoによる全コマンドのroot権限での実行を許可していると、そのユーザの権限が奪取された場合、sudoのpasswd_timeoutが切れるまでは攻撃者はroot権限で好き放題できてしまうような気がするのですがどうなんでしょうか。

-R オプションは知りませんでした。これは便利ですね。参考になります。

passwd_timeout って sudo でパスワード打った後にしばらくパスワードを入力する必要なくなる時間のタイムアウトのことでしょうか?
経験則ですが、あれって端末毎に制御されてるみたいなので、パスワード無しでいきなり root 権限使われまくりっていうことはないかと思います。
まあ、そもそも一般ユーザであれクラックされたら再インストールしないといけませんが。

端末毎ではないようですよ。(Debian sargeのsudo 1.6.8p7)

僕の手元だとちゃんと端末毎にタイムアウトの時間が制御されています。(Fedora Core 3, sudo 1.6.7p5)

/var/run/sudo/username
に端末毎のファイルが作成されていて、ファイルのタイムスタンプで最後に sudo を実行した時間を記録しているようです。

ながながとおつきあいいただいてありがとうございましたー m(_ _)m

ブログ、今後も楽しみにしています!

こちらこそありがとうございました!!

それはともかく、参考になりました!

シェル系がものすごく弱いんで、
どこかで勉強するか何か考えないといけないです。

ありがとうございます。
シェルスクリプトは慣れるとかなり便利です。
ちょっとしたツールが簡単にかけるので、UNIX の管理をするならぜひ身につけておきたいスキルの一つですね。

function get_servers() ってちゃんと動きますか?
「s1 s2 s3 s4 s5 s6 s7 s8 s9 s10 s11 s12」が返るだけのような。

はい、その通りです。そういう風に意図して作ってます。
スクリプトを見ていただけると分かりますが、for と組み合わせて使っています。

ああ、実際のホスト名が s1 なんですね。納得です。

大変参考になります.
xdiff.shを実行してみたところ,_xdiffが無いと言われました.

s1# xdiff /etc/hosts s2
xdiff:17: command not found: _xdiff

どのようにすればいいのか教えていただけませんでしょうか.

コメントを投稿


画像の中に見える文字を入力してください。