とあるプロジェクトで昨年から約1年半、Ansibleでサーバ構築をしていて書きづらいなーと思いながらDocker/Kubenetesとか使いたいけど、それに合わせたシステムの改修をが間に合うわけもなく……
という鬱々とした気持ちを抱えておりました。が、不平不満を言うだけでははじまらないと思い立って、自分の好きなように書けるツールを作成してみました
Submarine - https://gitlab.com/mjusui/submarine
私がAnsibleを書きづらいと感じたのは、別にAnsibleが悪いわけではなく、単にプログラミングパラダイムの問題です
結論から言うと、タイトルどおりNode.jsで書きたかったんです
紹介の前にインフラエンジニア以外の人向けにAnsibleがどういうものなのか、簡単に説明しておきます
Ansibleとは
サーバ構築、構成管理や自動化のためのツール。特徴としては以下のようなものがあります
- コードは全部
YAML
で表現される -
オブジェクト指向
や関数
の概念はなく、条件分岐とループのみで処理が進んでいく(ただしコードをinclude/importできるので多少の再利用性はある) - 変数は基本すべてグローバルアクセス可能で、再代入も自由(一部スコープを制限する機能はある)
- サーバ構築でよく使う機能(ファイルコピーなど)は
module
という単位でAnsibleやサードパーティが用意してくれたものを使う -
module
を自前で作成することもできる - Ansible自体はPythonで開発されており、Pythonの機能や、パッケージを使って
module
も開発されている - サーバには
ssh
さえできればよく、専用のエージェントをインストールする必要はない
プログラミング経験があまりないインフラエンジニアにも分かりやすいよう、シンプルに設計されているという印象です
Submarineとは
Node.jsで開発するインフラ管理のFrameworkです。以下のような特徴があります
- Ansibleは構成管理がメインのツールですが、Submairneはサーバのテストもできます
- テストの結果に応じて、コマンドを実行する/しないが決定されます
- Classと継承を使って、コードの再利用性を高めています
- Node.jsの機能を使えば、変数の再代入を制限したり、スコープを限定することもできます
- サーバの状態を取得する関数、取得した状態をテストする関数、サーバに変更を加える関数を分離することで、安全で読みやすい(条件分岐の少ない)実装ができます
- HTTPサーバを起動して、上記で実装した関数をエンドポイントとして公開する機能があります
今回はSubmarineの基本的な機能をサンプルコードをまじえて、紹介していこうと思います
サーバの状態を取得する
まずはサーバから情報を取得します
const Submarine = require('Submarine');
const Tutrial = class extends Submarine {
query(){
return {
hostname: 'hostname -s',
ipv4_addrs: String.raw`
ip -o -f inet a \
|awk '{print $4}'
`
};
}
}
const tut=new Tutrial({
conn: 'bash'
});
tut.current().then((stats)=>{
console.log(stats);
});
Node.jsが読める人は感覚的にわかるかもしれませんが Submarine
というクラスを継承して Tutrial
というオリジナルのクラスを定義しています。そして、そのクラスを new
でインスタンス化して tut.current()
というところで Tutrial
クラスに定義した query
関数が実行されます
クラスをインスタンス化するときに { conn: 'bash' }
という引数を与えることで Tutrial
クラスに定義した query
関数の中のコマンド(hostname -s
など)が localhost
の bash
で実行される、ということを指定しています
クラスをインスタンス化するときに例えば { conn: 'ssh', host: <ip address> }
といった具合に指定すれば、指定したIPアドレスにsshして、コマンドが実行されます
サーバの状態をテストする
今度は取得した情報をテストします
const Submarine = require('Submarine');
const Tutrial = class extends Submarine {
query(){
return {
nonexecutable: String.raw`
which none \
2> /dev/null \
|| exit 0
`,
executable: String.raw`
which node \
2> /dev/null \
|| exit 0
`,
};
}
test(stats){
return {
none_is_not_executable: stats.nonexecutable === '',
node_is_executable: stats.executable,
};
}
}
const tut=new Tutrial({
conn: 'ssh',
host: '127.0.0.1'
});
tut.check().then((done)=>{
console.log(done);
});
query
の部分で none
というコマンドと node
というコマンドのパスをwhichコマンドで引くことができるか確認しています。none
というコマンドは、普通は存在しませんから test
の中でパスが空 ''
であることを検証しています。一方 node
というコマンドは、このNode.jsのコードが実行できている以上、どこかに存在しますからwhichコマンドの結果に何らかのパスが含まれていることでしょう。test
関数の戻り値に含まれている2つの値(none_is_not_executable
と node_is_executable
)は tut.check()
という部分で評価され論理積(AND)でTrue/Falseが決定されます。結果は done
変数に格納されています
サーバに変更を加える
サーバの状態を取得して、それをテストしました。テストの結果が問題なければ、何もする必要はありませんが、テストで異常が発見された場合は、それを修正しなければなりません。テストがFalseになった場合にだけ実行されるコマンドを以下のように定義します
const Submarine = require('Submarine');
const Tutrial = class extends Submarine {
query(){
return {
file_content: String.raw`
[ -r /tmp/submarine/hogehoge ] && {
cat /tmp/submarine/hogehoge
} || {
echo 'File not readable' >&2
}
`
};
}
test(stats){
return {
file_content_is_hogehoge: stats.file_content === 'hogehoge',
};
}
command(props){
return String.raw`
mkdir -p /tmp/submarine \
&& echo ${props.msg} > /tmp/submarine/hogehoge
`;
}
}
const tut=new Tutrial({
conn: 'ssh',
host: '127.0.0.1'
});
tut.correct({
msg: 'fugafuga'
}).then((done)=>{
console.log(done);
});
/tmp/submarine/hogehoge
というファイルの内容が 'hogehoge'
であるか確認し、そうでない場合はファイルが作成され、内容は 'hogehoge'
で書き換えられます
done
にはコマンドが実行された場合は、実行したコマンドのreturn codeやstdoutの情報が格納され、実行されなかった場合は test
関数のときの結果が格納されます
複数サーバーで実行する
上記の例は1台のサーバに対してコマンドを実行していましたが、複数台のサーバに実行することも可能です
const Submarine = require('Submarine');
const Tutrial = class extends Submarine {
query(files){
return {
availables: String.raw`
df -P \
|awk '{print $4}' \
|grep "^[0-9]*$"
`
};
}
format(stats){
return {
available_max: Array.isArray(stats.availables)
? stats.availables.map(
available => available * 1
).sort((a ,b)=>{
return a < b
? -1
: a == b
? 0
: 1;
}).reverse()[0]
: stats.availables * 1
};
}
}
const Tutrials = Submarine.hosts(
host => new Tutrial({
conn: 'ssh',
host: host
}),
server1,
server2,
server3,
server4,
server5
);
const tut = new Tutrials();
tut.current().then(
hosts => hosts.map(
host => host.available_max
).reduce((a, b)=>{
return a + b;
}) / 1024 / 1024
).then((available_sum)=>{
console.log(available_sum);
});
server1
から server5
がそれぞれ new Tutrial...
でインスタンス化され tut.current()
で全台に対して query
関数が評価されます。結果はホスト1台のときと同じフォーマットの結果が、配列で返されます(コードの hosts
に格納されている)
このサンプルコードでは、5台のサーバのディスク空き容量を足し合わせています
おわりに
他にも、Ansibleの基本的な機能は置き換えられるようか機能が実装されております
各機能のTutrialを鋭意作成中です