Goでプロビジョニングツールを作った
GitHub - kohkimakimoto/cofu: Minimum configuration management tool written in Go.github.com
CofuというサーバプロビジョニングツールをGoで実装しました。Itamaeを参考に作りました。実装言語の違い(ItamaeはRubyによる実装)はありますが、外部仕様、内部実装、共にかなり似せて作ってあるので、ItamaeまたはItamaeが参考にしているchefを使ったことがあると、理解は簡単かと思います。特徴をざっくり説明すると、
- ローカルでのプロビジョニングのみ対応。SSHなどでのリモートサーバのプロビジョニングはサポートしない
- Goなので実行ファイル一個で動く。導入が簡単
- レシピはLuaのDSLで記述する
- 今のところ動作プラットフォームはRedHat(CentOS)のみをサポート
あたりでしょうか。以下に軽く使い方を記載します。
インストール
ビルド済みバイナリがありますので
https://github.com/kohkimakimoto/cofu/releases
からファイルをダウンロードして解凍し、パスの通ったディレクトリに配置するだけでOKです。RPMパッケージも用意してありますが、これは/usr/bin/cofu
をインストールしているだけなので、利用はお好みでどうぞ。
使い方
レシピファイルをこんなふうに記述して、
-- recipe.lua software_package "httpd" { action = "install", } service "httpd" { action = {"enable", "start"}, }
cofu
コマンドの引数に指定して実行します
$ sudo cofu recipe.lua ==> Starting cofu... ==> Loaded 2 resources. ==> Evaluating software_package[httpd] software_package[httpd]: 'installed' will change from 'false' to 'true' ==> Evaluating service[httpd] service[httpd]: 'enabled' will change from 'false' to 'true' service[httpd]: 'running' will change from 'false' to 'true' ==> Complete!
レシピを指定して、実行する。これだけ。
リソース
Itamaeと同様にレシピファイルにはサーバの状態をリソースという単位で記述します。以下が実装済みのリソースです。だいたいItamaeで用意されているのと同じです。
- directory
- execute
- file
- group
- link
- lua_function
- remote_file
- service
- software_package
- template
- user
実体はLuaの関数なのですが、リソース記述のsyntaxは以下のようになります
resource_type "name" { attribute = "value", action = "type_of_action", }
Tips
では、もうちょっと実用的な使い方やサンプルなどを幾つか紹介
dry-run
-n
オプションをつければdry-runで実行します。実際には変更が行われないモードですね。変更内容の確認のために使います。
$ sudo cofu -n recipe.lua
executeリソースでコマンド実行
コマンドを実行します。not_if
とonly_if
は全てのリソースで使用できる共通のアトリビュートです。
execute "echo hello > /tmp/example" { not_if = "grep hello /tmp/example", -- このコマンドが失敗した時のみ実行される -- only_if = "grep -v hello /tmp/example", -- このコマンドが成功した時のみ実行される }
templateで設定ファイルを更新
template
リソースで設定ファイルを更新します。テンプレートはGoのtext/templateで記述します。
template "/etc/httpd/conf.d/foo.conf" { owner = "root", group = "root", mode = "0644", source = "foo.conf.tmpl", }
source
を指定しない場合、配置先のファイルパスからデフォルトでテンプレートファイルの位置を決定します。上記の例ですとレシピファイルからの相対パスで
templates/etc/httpd/conf.d/foo.conf
または
templates/etc/httpd/conf.d/foo.conf.tmpl
を使用します。
remote_fileでファイルを配置
テンプレート処理が必要なく、単純にファイルを配置したいだけのときはremote_file
リソースを使います。
remote_file "/path/to/hoge.zip" { owner = "root", group = "root", mode = "0644", source = "hoge.zip", }
この場合もsource
を指定しない場合はデフォルトのパスを使用します。上記の例ですと
files/path/to/hoge.zip
となります。
変数
chefやItamaeでいうところのノードアトリビュートにあたるものとして、実行時にコマンドラインから任意の変数を割り当てられます。-var
オプションをつかってJSONを直接指定します。
$ cofu recipe.lua -var='{"name": "kohkimakimoto"}'
これでテンプレートなどでは
{{var.name}}
で値を参照できます。レシピ内ではLuaのグローバル変数var
に割り当てられるので
var.name
で参照できます。
JSONを直接文字列からではなくファイルから読みたいときは-var-file
オプションを使用してください。
$ cofu recipe.lua -var-file=variables.json
パッケージのインストールとサービスの起動
software_package
でパッケージのインストール、アンインストール。service
でサービスの起動、停止、再起動ができます。software_package
はItamaeやchefでいうところのpackage
リソースと同等なのですが、Luaだとpackage
がビルトインのモジュールが利用している予約語なので、名前を変えています。
software_package "httpd" { action = "install", } service "httpd" { action = {"enable", "start"}, }
notifiesでリソース更新に合わせて別のリソースのアクションを実行する
設定ファイルが更新されたらサービスをリロードさせたりするのに使います。
template "/etc/httpd/conf/httpd.conf" { owner = "root", group = "root", mode = "0644", notifies = {"reload", "service[httpd]"}, } service "httpd" { action = "nothing", }
Itamaeだとこれの逆バージョンのsubscribes
(別のリソースの更新を検知してアクションを実行する)があるのですが、個人的にnotifies
しか使っていなかったのでCofuではnotifies
のみを実装しています。
別のレシピを読み込む
include_recipe
関数で別のレシピを読み込みます。
include_recipe "nginx.lua" include_recipe "php.lua"
ドキュメント
まだまだ欠けていますが、ここにあります。
https://github.com/kohkimakimoto/cofu/blob/master/docs/README.md
動作プラットフォームについて
前述のようにCentOSでしか動作確認、サポートしていません。
単純に私が他のOSは普段使っていないので今のところ対応しないというだけで、プラットフォームごとに処理を切り分ける部分はItamaeが利用しているSpecinfraを参考にして実装してあるので、マルチプラットフォーム対応ができる構造にはなっています。Specinfraを参考にプラットフォームによって異なる部分のコードを追加すれば、対応できるはずです。
ただテストを自動化してないので、本格的にマルチプラットフォーム対応させる際にはそのへんも考えていかなくてはいけないのかも。
開発の経緯と実際のユースケース
Itamaeに比べて機能は少ないし、SSHバックエンドをサポートしてないし、対応プラットフォームもCentOSのみというプロダクトなので、機能面でいうとこれは劣化版Itamaeという趣になっています。実際のところ私自身今はプロビジョニングツールにItamaeを使っていて、既存のItamaeのレシピをCofuで置き換えるつもりはなく、今後もItamaeを使い続ける予定であります。
実はこのCofuを作成した経緯は、フルスタックなプロビジョニングツールを作ろうと思っていたわけではなく、 そもそもの発端は「アプリケーションのデプロイ時に、サーバーのミドルウェアの設定ファイルを更新したい」という要件でした。
具体的なもののひとつがcronの設定ファイルで、これにアプリケーション固有のバッチ処理などが定期実行されるように記述されているわけです。アプリでバッチ処理を追加する場合、アプリケーションのリポジトリとサーバーコンフィグレーションを管理しているItamaeレシピのリポジトリの二つにコミットする必要がありました。
またアプリ固有の処理なので、プログラムロジック自体も特定の定期実行のスケジューリングを前提にした作りになっていることも多々あり、スケジューリングの設定と本質的に不可分で、この場合cronの設定自体アプリケーションのレイヤで管理すべきものです。それができないため、バッチプログラムのコメントに「日次バッチ。cronで実行される(/etc/cron.d/app参照)」とかメモを書いていたりしました。
こういう背景から、アプリケーション側に寄せるべき設定を管理するために、デプロイ時に特定の設定ファイルを書き換える仕組みがほしいと思っていて、最初はシェルスクリプトを書いていたのですが、いろいろ考えていくと冪等性が欲しかったり、設定ファイルが更新された場合にはコマンドを実行したりする必要が出てきたりして、Itamaeのようなプロビジョニングツールと機能要件が似てきたという次第です。
まあそれならデプロイ時にitamae
コマンドを叩けばいい、というのも当然あるのですが、設定ファイル一個をリポジトリルートにおいておいてデプロイ時にこれを実行すればOKというシンプルな仕組みが欲しかったので、結局最後まで作りました。以下がそのサンプルです
-- config_cron.lua template "/etc/cron.d/app" { owner = "root", group = "root", mode = "0644", content = [=[# This is generated by cofu. don't edit it manually! MAILTO="" */1 * * * * kohkimakimoto /path/to/app/batch1 */1 * * * * kohkimakimoto /path/to/app/batch2 */30 * * * * kohkimakimoto /path/to/app/batch3 0 * * * * kohkimakimoto /path/to/app/batch3 ]=], }
これをアプリのリポジトリルートにおいておき、デプロイ時のスクリプトにsudo -E cofu config_cron.lua
を実行するようにしてあります。
よって私の環境では実際に使っているリソースはtemplate
とservice
,execute
あたりのみになります。ただ、今後の個人の趣味プロジェクトなどではサーバプロビジョニングの用途でも使っていこうかな、とも思っています。せっかくその他のパッケージやユーザを管理するリソースも実装したので。。。
ちなみにレシピはシバンで使うこともできます。
https://ja.wikipedia.org/wiki/%E3%82%B7%E3%83%90%E3%83%B3_(Unix)
レシピを以下のように記述して、実行権限をつければ、レシピファイル自体を実行可能な設定スクリプトとして扱うことができます。
#!/usr/bin/env cofu -- config.lua template "/path/to/foo" { -- ... } template "/path/to/bar" { -- ... } -- 実行権限をつけて直接実行。 -- $ ./config.lua
実装のはなし
今回のCofuがそうなのですが、最近CLIツールを作るのにGoとGoPherLuaというGoのLua実装をセットで使うのが気に入っていて、これをベースにLuaをDSL風に使った設定ファイルを設計してアプリケーションを組み上げるのが最近、個人的によく使う手法になっています。
LuaのDSLはnginxの設定ファイルやHCLに見た目が似ていて、かつ変数や制御構文もつかえるので、なかなか便利だと思っています。
他にも幾つかこの技術スタックでツールを書いているので、後日それについても記事を書こうかと思っています。
CentOS6でサーバを構築する際におこなっている基本設定
2016年にもなって今さらという感じですが、CentOS6でサーバを構築する際の自分がやっている初期の基本設定、確認事項をまとめておこうと思います。
CentOS6のサポート期限が2020年までで、自分が今後メインに使っていく環境もCentOS7などに移行しようと思っています。遅まきながらローカルのVirtualBoxにCentOS7をいれていろいろ試していますが、CentOSは6と7で大幅な変更がなされ、今まで普通に出来ていたことを一から学び直す必要があったりしてツライ日々ではあります。
このように徐々にCentOS6から離れていくことになるので、そのあたりの記憶が急速に失われていく前に、今まで書いたメモなどを元に情報を整理しておこうかと思い立ったのがきっかけです。
※ここではサーバのロールにかかわらず行う基本的な作業をまとめています。通常この環境の上にWebサーバ(httpd, nginxなど)やDB(MySQLなど)をインストールすることになります。
※本文中のコマンドは基本的にrootユーザで行っているものとします。一般ユーザを使い必要に応じて都度root権限を使うようにしている方はsudo
をつけるなど、コマンドを読み替えてください。
- ランレベルの確認
- サービス起動設定の確認
- ディクスサイズの確認
- ロケールの設定
- タイムゾーンとNTPの設定
- ネットワーク関連の設定
- yumリポジトリ
- bash-completion
- ファイアウォール (iptables)
- sshd
- logrotate
- sysctl
- 環境変数
ランレベルの確認
OSのランレベルを確認します。ここではサーバ用途なので3が設定されていればいいでしょう
# cat /etc/inittab
# inittab is only used by upstart for the default runlevel. # # ADDING OTHER CONFIGURATION HERE WILL HAVE NO EFFECT ON YOUR SYSTEM. # # System initialization is started by /etc/init/rcS.conf # # Individual runlevels are started by /etc/init/rc.conf # # Ctrl-Alt-Delete is handled by /etc/init/control-alt-delete.conf # # Terminal gettys are handled by /etc/init/tty.conf and /etc/init/serial.conf, # with configuration in /etc/sysconfig/init. # # For information on how to write upstart event handlers, or how # upstart works, see init(5), init(8), and initctl(8). # # Default runlevel. The runlevels used are: # 0 - halt (Do NOT set initdefault to this) # 1 - Single user mode # 2 - Multiuser, without NFS (The same as 3, if you do not have networking) # 3 - Full multiuser mode # 4 - unused # 5 - X11 # 6 - reboot (Do NOT set initdefault to this) # id:3:initdefault:
id:3:initdefault:
となっていることを確認します。
サービス起動設定の確認
chkconfig
で起動設定になっているサービス(デーモン)を確認します。
# chkconfig --list auditd 0:off 1:off 2:on 3:on 4:on 5:on 6:off blk-availability 0:off 1:on 2:on 3:on 4:on 5:on 6:off cgconfig 0:off 1:off 2:off 3:off 4:off 5:off 6:off cgred 0:off 1:off 2:off 3:off 4:off 5:off 6:off crond 0:off 1:off 2:on 3:on 4:on 5:on 6:off ...
不要と思われるサービスはサーバリソースの無駄使いになるので、以下のコマンドで停止設定にしておきます。
# chkconfig <サービス名> off
これはOS起動時のサービスの自動起動設定なので、合わせて
# /etc/init.d/<サービス名> stop
として現在の実行状態も停止します。
さて、サーバ 用途でランレベルは3になっているので3:on
となっているサービスを上から順に調べて、offにしていくのですが、自分の知識範囲でよくわからないものもあったします。その場合はとりあえずonにしておいています。もちろんすべて把握できるのが良いのですが。。。
逆にOSの起動時に実行させたいサービスは
# chkconfig <サービス名> on
とします。
ディクスサイズの確認
# df -h
-h
をつけるとサイズにM(メガ)とかG(ギガ)とかがついてわかりやすく表示されます。
サーバスペックにあったディクスサイズになっていることや、パーティションを確認します。
ロケールの設定
システムが使うロケールを/etc/sysconfig/i18n
で設定します
日本語なら
LANG="ja_JP.UTF-8"
英語なら
LANG="en_US.UTF-8"
としておきます。また
# yum install man-pages-ja
をインストールして、ロケールを日本語にしておくとmanページが日本語表示されるようになります。
タイムゾーンとNTPの設定
タイムゾーンを日本にする
# cp -f /usr/share/zoneinfo/Japan /etc/localtime
/etc/sysconfig/clock
を以下のように編集する
ZONE="Asia/Tokyo"
サーバの時刻をあわせるntpdについての設定/etc/ntp.conf
を確認します。
server 0.centos.pool.ntp.org iburst server 1.centos.pool.ntp.org iburst server 2.centos.pool.ntp.org iburst server 3.centos.pool.ntp.org iburst
のようにntpサーバが指定されていること。
date
コマンドで表示される時刻が正しいことを確認する。
# date
ネットワーク関連の設定
幾つかあるので、順番に見ていきます。
ネットワークの設定
/etc/sysconfig/network
NETWORKING
がyes
になっているのとHOSTNAME
を確認する
NETWORKING=yes HOSTNAME=your-hostname
ネットワーク・インターフェースの確認
ネットワーク・インターフェースは/etc/sysconfig/network-scripts
配下にインターフェースごとにifcfg-xxx
というファイルに設定されている。
例えばifcfg-eth0
はイーサネットの0番目のインターフェース
DEVICE="eth0" BOOTPROTO="none" IPADDR=192.168.56.14 NETMASK=255.255.255.0 ONBOOT="yes" TYPE="Ethernet" UUID="c67412a6-19cf-427a-9f4d-96ca89217519"
静的はIPを割り当てている場合はIPADDR
やNETMASK
などの設定を確認、編集する。またネットワークの設定を変更したら
# /etc/init.d/network restart
で反映させる。
IPアドレスの確認
# ifconfig
IPアドレスの設定が、設定通りか確認します。
ネットワークの疎通確認
# ping yahoo.co.jp
pingが外に向けて通るかを確認します。0% packet loss
となればOK。
# traceroute yahoo.co.jp
外につなぎに行く際にどのようなIPを経由するかを確認。ネットワーク管理者ではないので、ざっと結果を眺めるだけです。
yumリポジトリ
yumはCentOSの標準パッケージ管理システムです。システムワイドにパッケージをインストールする場合、基本的にyumを使う方針にしています。ソースからのインストールに代表されるその他のインストール方法はファイルシステムのどこに何が配置されるか把握、管理するのが難しく、これはアンインストールなどの作業を困難にします。
ただCentOSでは標準のyumリポジトリでインストールできるパッケージは数が少なかったり、バージョンが古かったりする(特にPHPのバージョンが古いのがLAMP環境では致命的です)ので、以下のサードパーティのリポジトリを追加して使っています。俗に「野良リポジトリ」と呼ばれるものです。
- epel
- remi
追加は以下のコマンドを実行します。
# rpm -ivh http://ftp.riken.jp/Linux/fedora/epel/6/x86_64/epel-release-6-8.noarch.rpm # rpm -ivh http://rpms.famillecollet.com/enterprise/remi-release-6.rpm
追加したサードパーティリポジトリは標準では想定されていない環境をインストールするので、予期せぬ不具合を発生させる可能性があると思われます。デフォルトでは使わないように設定し、インストールやアップデート時、必要に応じて有効化して使うのが広く知られている手法のようです(※といいつつ、私はデフォルトで有効化してしまっています。後述します)。その場合は以下のように設定ファイルを修正、確認しましょう。
/etc/yum.repos.d/epel.repo
[epel] # 中略... enabled=0
/etc/yum.repos.d/epel.repo
[remi] # 中略... enabled=0
これでデフォルトは無効化されます。利用する場合は以下のようにします。
# yum install php --enablerepo=remi,epel
コマンドメモ
インストール(phpをインストールの例)
# yum install php
アップデート
# yum update php
アンインストール
# yum remove php
インストール済みパッケージの情報
# rpm -qi php
インストール済みパッケージのファイル一覧
# rpm -ql php
補足(個人的なデフォルトの設定)
前述したように、サードパーティリポジトリ利用のデフォルト設定を無効にはしていません。というのも実際にパッケージをインストールする作業はchefなどのプロビジョニングツールを使っているので、デフォルトで有効になっていないと、これらのツールから正しくインストールできないという理由があるからです。
bash-completion
素のbashのままだとzshになれた補完脳にはツライので、bash-completionを入れます。
# yum install bash-completion
インストールするだけで、ある程度補完が効くようになります。カスタマイズは特にしていません。
ファイアウォール (iptables)
iptablesはファイアウォールです。iptables
コマンドから設定できるのですが、よく覚えてないし全体の設定を眺めて設定できるので、設定ファイルを直接編集しています。(設定ファイルの先頭にManual customization of this file is not recommended.
って書かれているので本来は非推奨なのですが。。。)
iptablesの設定の詳細は覚えていないのですが(よくググります)、基本的に以下のようにINPUTチェインに外部接続を許可する設定を書いていきます。 最後にそれ以外はすべて拒否するように設定しておきます。
/etc/sysconfig/iptables
# Firewall configuration written by system-config-firewall # Manual customization of this file is not recommended. *filter :INPUT ACCEPT [0:0] :FORWARD ACCEPT [0:0] :OUTPUT ACCEPT [0:0] -A INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT -A INPUT -p icmp -j ACCEPT -A INPUT -i lo -j ACCEPT # httpd -A INPUT -m state --state NEW -m tcp -p tcp --dport 80 -j ACCEPT -A INPUT -m state --state NEW -m tcp -p tcp --dport 443 -j ACCEPT # ssh -A INPUT -m state --state NEW -m tcp -p tcp --dport 22 -j ACCEPT # ...中略 # 最後に拒否 -A INPUT -j REJECT --reject-with icmp-host-prohibited
これで必要のない外部アクセスを遮断でき、不正アクセス対策になります。最後にrestartして設定を反映します。
# /etc/init.d/iptables restart
なお、iptablesの設定をミスってsshでリモートサーバに繋げなくなって詰む、という状況を避けたいので今のターミナルのセッションが生きているうちに、restart
したらまず別のターミナルを立ち上げてssh接続を試します。
sshd
ssh接続はセキュリティを強くするため、ポート番号を変えて、鍵認証オンリーにします。以下、編集および確認箇所のみを記載します。
/etc/ssh/sshd_config
# 22以外に変えます Port 2222 # rootログインは禁止 PermitRootLogin no # パスワード認証は禁止 PasswordAuthentication no
再起動して設定を反映します。
/etc/init.d/sshd restart
logrotate
ログのローテート設定。ログが無尽蔵に大きくなってディクスサイズを圧迫しないように設定、確認します。 初期設定時の他にhttpdなどのデーモンやログを出力するアプリケーションを追加した時にも目を通します。
/etc/logrotate.d/
以下にログの対象ごとに設定ファイルがあるのでそれを確認します。例えばnginxは以下のようになっていたりします。
/etc/logrotate.d/nginx
/var/log/nginx/*.log { daily missingok rotate 52 compress delaycompress notifempty create 640 nginx adm sharedscripts postrotate [ -f /var/run/nginx.pid ] && kill -USR1 `cat /var/run/nginx.pid` endscript }
実際の設定はログのサイズや保存期間の要件などによって異なるので、それを考慮し設定します。
sysctl
カーネルパラメータの設定です。/etc/sysctl.conf
を編集します。以下のパラメータを追加、変更。
net.nf_conntrack_max = 1000000 net.netfilter.nf_conntrack_max = 1000000 net.core.somaxconn = 1024 vm.overcommit_memory = 1
net.nf_conntrack_max
とnet.netfilter.nf_conntrack_max
ネットワークアクセスが高負荷になったときパケットのトラッキングがデフォルトの上限値を上回ることがあるのでそれに対応するため、実際の数値はメモリの量と相談です。
参考:
https://www.e-agency.co.jp/column/20121225.html
net.core.somaxconnは
ソケットをListenするキューの最大数。memcacnedとかredisはこれがカーネルのデフォルト値(128)より大きく設定されているので、増やす
参考:
net.core.somaxconnについて調べてみた - tetsuyai’s blog
vm.overcommit_memory
はメモリが足りなくなったときのmalloc
の動作についての制御のよう。デフォルトだと高負荷時にredis起動時にバックグランドセーブが失敗する可能性がある、という警告がでる。
参考:
passingloop • Linux のオーバーコミットについて調べてみた
設定値を反映するために以下のコマンドを打つ。
sysctl -p
確認。
sysctl -a
環境変数
環境変数はサーバにデプロイするアプリケーションの振る舞いを制御するためなどに使います。
サーバのロールやdevelopment
やproduction
のような環境情報を設定しておきます。
アプリケーションは実行時にこの環境変数を読むことでデバッグモードに変えたり、本番用の設定に切り替えたりできるように実装します。
ただ環境変数はsudo時、sshでログイン時、sshで直接コマンド実行時、実行中のhttpdプロセス内、などプロセスの実行のコンテキストによって変わるので、 常に一貫した設定を読めるようにしておくのがなかなか難しく、悩みどころになっています。現状は同じ環境変数を反映させる設定を複数の箇所に行っています。
/etc/envrionment
SERVER_ENVIRONMENT=production SERVER_ROLE=web
/etc/profile.d/environment.sh
export SERVER_ENVIRONMENT=production export SERVER_ROLE=web
以上2つが基本。あとはhttpdをアプリケーションサーバに使うときは/etc/httpd/conf/httpd.conf
にも設定します
SetEnv SERVER_ENVIRONMENT "production" SetEnv SERVER_ROLE "web"
設定の記述が重複してしまうのでコピペにならないように、chefやitamaeのようなプロビジョニングツールで変数にしてカバーしています。。。
Bashスクリプトのチートシートと便利なスニペットまとめ
Bashでスクリプトを書く際によく自分が使っている小技や関数などです。
またBashでは他のプログラミング言語以上に$
や:
などの記号の使い方が独特でググラビリティが低いので、基本文法などもチートシートとしてまとめておきます。
- 基本文法
- 便利なスニペット
- Tips
- 標準出力を標準エラー出力にリダイレクト
- 標準エラー出力を標準出力にリダイレクト
- プロセスIDを取得する($$)
- コマンドの戻り値を取得する ($?)
- パイプでつないだコマンドの戻り値を取得する (${PIPESTATUS[0]})
- 未定義の変数を使用するとそこでスクリプトを終了する (set -u)
- コマンドがエラーだった場合そこでスクリプトを終了する (set -e)
- set -eの状態でエラー後も処理を続ける (&&:)
- パイプで繋いだコマンドがエラーのとき終了させる
- スクリプト終了時にコマンドを実行する
- 一時ファイルを作る
- 一時ディレクトリを作る
- rootユーザでのみ実行を許可する
- 標準入力からデータを受け取る
- ヒアドキュメント
- スクリプトのロック(多重起動防止)
基本文法
変数と配列
変数
# 変数 v="variable" echo $v
配列。bashは配列しかデータ構造がない。つらい。。。
# 配列 declare -a array=() # 初期値 declare -a array=("a" "b" "c") # 要素数 echo ${#array[@]} # 先頭に追加 array=("x" "${array[@]}") # 末尾に追加 array=("${array[@]}" "d") # indexを指定して取得 echo ${array[0]} echo ${array[1]} # 配列全体を取得 echo ${array[@]} # 配列をforループで参照する for v in "${array[@]}" do echo $v done
参考:
制御構文
while
v=0 while [ $v -lt 10 ] do echo $v v=$((v+1)) done
for-in
for i in {0..9}; do echo $i done
if - elif - else
v="hoge" if [ "$v" = "hoge" ]; then echo "v is hoge" elif [ "$v" = "foo" ]; then echo "v is foo" elif [ ! "$v" = "foo" ]; then echo "v is not foo" else echo "v is unknown" fi
条件判定のあとのブロックを空にすることはできない。
変数は"で囲ったほうがいい。[ $v = "~" ];
は変数が空のときsyntaxエラーになる。参考。
条件の否定は先頭に!
。
if文のone-liner
[ "$v" = "hoge" ] && echo "v is hoge"
文字列比較
文字列が等しい (=)
v="hoge" if [ "$v" = "hoge" ]; then echo "equal" fi
文字列が等しくない (!=)
v="hoge" if [ "$v" != "foo" ]; then echo "not equal" fi
空文字、文字列長が0 (-z)
v="" if [ -z "$v" ]; then echo "zero length" fi
空文字でない、文字列長が0でない (-n)
v="aaa" if [ -n "$v" ]; then echo "not zero length" fi
数値比較
数値が等しい (-eq)
v=55 if [ "$v" -eq 55 ]; then echo "equal" fi
数値が等しくない (-ne)
v=55 if [ "$v" -ne 20 ]; then echo "not equal" fi
数値がより小さい (-lt)
v=10 if [ "$v" -lt 20 ]; then echo "less than" fi
数値がより大きい (-gt)
v=30 if [ "$v" -gt 20 ]; then echo "greater than" fi
ファイルの判定
存在する (-e)
v="/tmp/aaa" if [ -e "$v" ]; then echo "exists" fi
ファイルである (-f)
v="/path/to/file" if [ -f "$v" ]; then echo "file" fi
ディレクトリである (-d)
v="/tmp" if [ -d "$v" ]; then echo "directory" fi
シンボリックリンクである (-L)
v="/path/to/link" if [ -L "$v" ]; then echo "symbolic link" fi
関数
function hoge() { local v="local variable" # ローカル変数定義 echo $1 # n番目引数 echo $2 echo $@ # 引数全体 echo $# # 引数の数 return 0 # 戻り値 } hoge "arg1" "arg2" # 実行結果 # arg1 # arg2 # arg1 arg2 # 2
ブロック内を空にすることはできない。
戻り値は数値のみ。コマンドと同様に0
が正常でそれ以外はエラー。
$0
は関数名ではなく、呼び出し元スクリプト名が入る。
文字列を戻したいときはechoなどで標準出力を使う。
function hoge() { echo "hogehoge" } $v=$(hoge) echo $v
便利なスニペット
実行スクリプトがあるディレクトリを絶対パスで取得する
SOURCE="${BASH_SOURCE[0]}" while [ -h "$SOURCE" ] ; do SOURCE="$(readlink "$SOURCE")"; done bin_dir="$( cd -P "$( dirname "$SOURCE" )/" && pwd )" # /usr/local/bin # などがbin_dirに取得できる
以下でも良い。
READLINK=$(type -p greadlink readlink | head -1) if [ -z "$READLINK" ]; then echo "cannot find readlink - are you missing GNU coreutils?" >&2 exit 1 fi resolve_link() { $READLINK "$1" } # get absolute path. abs_dirname() { local cwd="$(pwd)" local path="$1" while [ -n "$path" ]; do # cd "${path%/*}" does not work in "$ bash script.sh" # cd "${path%/*}" cd "$(dirname $path)" local name="${path##*/}" path="$(resolve_link "$name" || true)" done pwd -P cd "$cwd" } bin_dir="$(abs_dirname "$0")"
シンボリックに対応しないなら、以下の記述でもOK。
# スクリプトがシンボリックリンクから呼び出されたときは、リンクを辿らず、リンク先のパスを返す bin_dir=$(cd $(dirname $0); pwd)
参考:
https://github.com/hashicorp/consul/blob/master/scripts/build.sh
https://github.com/rbenv/rbenv/blob/master/libexec%2Frbenv
標準出力にプリフィクスをつける
prefix() { local p="${1:-prefix}" local c="s/^/$p/" case $(uname) in Darwin) sed -l "$c";; # mac/bsd sed: -l buffers on line boundaries *) sed -u "$c";; # unix/gnu sed: -u unbuffered (arbitrary) chunks of data esac } # 使い方 echo "message" | prefix "[hoge] " # [hoge] message # 標準エラー出力も対象とする場合はエラーを標準出力にリダイレクトしてから行う echo "message" 2>&1 | prefix "[hoge] "
参考:
https://github.com/heroku/heroku-buildpack-php
標準出力にインデントをつける
プリフィクスをつける、の応用。
indent() { local n="${1:-4}" local p="" for i in `seq 1 $n`; do p="$p " done; local c="s/^/$p/" case $(uname) in Darwin) sed -l "$c";; # mac/bsd sed: -l buffers on line boundaries *) sed -u "$c";; # unix/gnu sed: -u unbuffered (arbitrary) chunks of data esac } # 使い方 # デフォルトで4スペースインデント echo "message" | indent # message # インデントサイズを引数に渡せる echo "message" | indent 6 # message
コンソールに確認用のプロンプトを出す
confirm() { local response # call with a prompt string or use a default read -r -p "${1:-Are you sure? [y/N]:} " response case $response in [yY][eE][sS]|[yY]) return 0 ;; *) return 1 ;; esac } # 使い方 confirm if [ $? -ne 0 ]; then exit 1 fi # Are you sure? [y/N]: # と表示されて入力まちになるのでyやyesを入力で続行。それ以外はexitする。 # 引数で表示メッセージを変えられる。 confirm "Please input yes!:" # set -eしておけばconfirmの戻りが1のとき即終了するのであと条件分岐を書く必要がない set -e confirm
参考:
コンソールにユーザー入力用のプロンプトを出す
ask() { local response # call with a prompt string or use a default read -r -p "${1:->} " response echo $response } # 使い方 v=$(ask) echo $v # > # と表示されて入力待ち状態になる。入力値はvに入る。 # プロンプトの表示も買えられる v=$(ask "[input your name]> ") echo $v
テキスト装飾(色、太字、アンダーライン)
if [ "${TERM:-dumb}" != "dumb" ]; then txtunderline=$(tput sgr 0 1) # Underline txtbold=$(tput bold) # Bold txtred=$(tput setaf 1) # red txtgreen=$(tput setaf 2) # green txtyellow=$(tput setaf 3) # yellow txtblue=$(tput setaf 4) # blue txtreset=$(tput sgr0) # Reset else txtunderline="" txtbold="" txtred="" txtgreen="" txtyellow="" txtblue=$"" txtreset="" fi # 使い方 (装飾後は${txtreset}で戻すようにして使う) ${txtred}this text is red${txtreset}
参考:
https://linuxtidbits.wordpress.com/2008/08/11/output-color-on-bash-scripts/
http://stackoverflow.com/questions/2924697/how-does-one-output-bold-text-in-bash
エラーメッセージを出して終了する
上記のテキスト装飾を使って、赤文字のエラーメッセージを出力してプログラムを終了させる
abort() { { if [ "$#" -eq 0 ]; then cat - else echo "${txtred}${progname}: $*${txtreset}" fi } >&2 exit 1 } # 使い方 abort "error message"
コンソールにラインを引く
hr() { printf '%*s\n' "${2:-$(tput cols)}" '' | tr ' ' "${1:--}" } # 使い方 hr # 引数でラインのキャラクタを変えられる(デフォルトは-) hr "=" # 第2引数でラインの長さを変えられる(デフォルトはターミナルの幅いっぱい) hr "=" 10
参考:http://wiki.bash-hackers.org/snipplets/print_horizontal_line
文字列を小文字->大文字に変換する
upper() { echo -n "$1" | tr '[a-z]' '[A-Z]' } # 使い方 v=$(upper "abcdefg") echo $v
オプションとサブコマンドを扱うためのテンプレート
よくあるhoge.sh [<options...>] <command>
という形式のコマンドをbashで作るためのテンプレート。
#!/usr/bin/env bash set -eu progname=$(basename $0) progversion="0.1.0" # actions. usage() { echo "Usage: $progname [OPTIONS] COMMAND" echo echo "Options:" echo " -h, --help show help." echo " -v, --version print the version." echo " -d, --dir <DIR> change working directory." echo echo "Commands:" echo " help show help." echo } printversion() { echo "${progversion}" } # parse arguments and options. declare -a params=() for OPT in "$@" do case "$OPT" in '-h'|'--help' ) usage exit 0 ;; '-v'|'--version' ) # パラメータを取らないオプション printversion exit 0 ;; '-d'|'--dir' ) # パラメータを取るオプション。 "-d /tmp"のようにスペースで区切ってパラメータを渡す。 if [[ -z "${2:-}" ]] || [[ "${2:-}" =~ ^-+ ]]; then echo "$progname: option '$1' requires an argument." 1>&2 exit 1 fi optarg="$2" cd $optarg shift 2 ;; '--'|'-' ) shift 1 params+=( "$@" ) break ;; -*) echo "$progname: illegal option -- '$(echo $1 | sed 's/^-*//')'" 1>&2 exit 1 ;; *) if [[ ! -z "${1:-}" ]] && [[ ! "${1:-}" =~ ^-+ ]]; then params+=( "$1" ) shift 1 fi ;; esac done # サブコマンドに対応して処理を実行 command="" && [ ${#params[@]} -ne 0 ] && command=${params[0]} case $command in 'help' ) usage exit 0 ;; '' ) usage exit 0 ;; *) echo "$progname: illegal command '$command'" 1>&2 exit 1 ;; esac
参考:
Tips
標準出力を標準エラー出力にリダイレクト
echo "error" 1>&2
標準エラー出力を標準出力にリダイレクト
echo "error" 2>&1
プロセスIDを取得する($$)
echo $$ # 99807
コマンドの戻り値を取得する ($?)
[ "aaa" = "bbb" ] echo $? # 1
パイプでつないだコマンドの戻り値を取得する (${PIPESTATUS[0]})
$?
は最後に実行されたコマンドの戻り値なので、前述のindent
関数などをパイプでつなげた場合、もとの実行コマンドの戻り値は取れない。この場合はPIPESTATUS
を使う。
[ "aaa" = "bbb" ] | indent; status=${PIPESTATUS[0]} echo $status # 1
参考: パイプでつないだコマンドの戻り値を調べる@bash | Mazn.net
未定義の変数を使用するとそこでスクリプトを終了する (set -u)
set -u
基本的に次のset -e
も含めてset -eu
としておくのがよさそう。
コマンドがエラーだった場合そこでスクリプトを終了する (set -e)
set -e
しておくと、エラー(戻り値0以外を戻すコマンド実行)があると即時終了する
set -eの状態でエラー後も処理を続ける (&&:)
安全のため基本的にset -e
をしておいていいが、これはスクリプト内のすべてのコマンド実行の戻り値に対して効果をもつので、困ることも多い。
たとえばコマンド実行の結果をみてメッセージを出したい場合、以下のように書いたりする。
set -e /path/to/yourcommand if [ $? -eq 0 ]; then echo "yourcommand OK" else echo "yourcommand NG" fi
しかし/path/to/yourcommand
がエラーの時は即時終了してしまうので[ $? -eq 0 ];
は評価されず、結果yourcommand NG
は絶対に表示されない。
このような場合はコマンドに&&:
を続けておくとうまく動作する。(動作原理は下記参考のリンク先参照)
set -e /path/to/yourcommand &&: if [ $? -eq 0 ]; then echo "yourcommand OK" else echo "yourcommand NG" fi
参考:
`set -e` しているときにコマンドの戻り値を得る - Qiita
パイプで繋いだコマンドがエラーのとき終了させる
パイプでつなぐとset -e
をしててもエラー時に終了しないので以下のようにかく。
foo | indent; status=${PIPESTATUS[0]}; [[ ! $status -eq 0 ]] && exit $status
スクリプト終了時にコマンドを実行する
trapコマンドでシグナルをハンドリングできる
echo "start" trap "echo 'after end'" 0 echo "end"
参考: シグナルと trap コマンド | UNIX & Linux コマンド・シェルスクリプト リファレンス
一時ファイルを作る
# 一時ファイルを作成 tmpfile=$(mktemp -t prefix.XXXXXXXX) echo $tmpfile # 終了時に一時ファイルを削除 trap "rm $tmpfile" 0
osxとlinuxで挙動が違う。どちらでもひとまず動くバージョン linuxだとプリフィクスの末尾に"X"が必要でここがランダムな文字列に置き換わる。 osxだとXがリプレースされずそのまま使用され、その後にランダムな文字列が追加される
一時ディレクトリを作る
# 一時ディレクトリを作成 tmpdir=$(mktemp -d -t prefix.XXXXXXXX) echo $tmpdir # 終了時に一時ディレクトリを削除 trap "rm -rf $tmpdir" 0
rootユーザでのみ実行を許可する
user=`whoami` if [ $user != "root" ]; then echo "you need to run it on the 'root' user." 1>&2 exit 1 fi
標準入力からデータを受け取る
if [ -t 0 ]; then echo "stdin is not pipe" 1>&2 exit 1 else cat - fi | yourcommand
ヒアドキュメント
ヒアドキュメント内のシェル変数は展開される
cat << EOF This is a heredoc home is $HOME EOF # This is a heredoc # home is /Users/kohkimakimoto
変数は展開をさせたくないときはコロンで囲む
cat << 'EOF' This is a heredoc home is $HOME EOF # This is a heredoc # home is $HOME
ヒアドキュメントの内容を変数に入れる
heredoc=`cat << 'EOF' This is a heredoc home is $HOME EOF` echo $heredoc # This is a heredoc # home is $HOME
ヒアドキュメントの内容をファイルに出力する
cat << 'EOF' > heredocfile This is a heredoc home is $HOME EOF cat heredocfile # This is a heredoc # home is $HOME
スクリプトのロック(多重起動防止)
exec 9< $0 perl -mFcntl=:flock -e "open(LOCK,'<&=9');exit(!flock(LOCK,LOCK_EX|LOCK_NB))" || { echo "duplicate process." >&2 exit 1 }
参照:
yum updateでエラー(Error: Package: 2:irqbalance-1.0.7-5.el6.x86_64)
古いCentOS6のサーバにyum update
をかけたら依存の問題で以下のエラーがでて、更新できなかった。
# yum update ... Error: Package: 2:irqbalance-1.0.7-5.el6.x86_64 (base) Requires: kernel >= 2.6.32-358.2.1 Installed: kernel-2.6.32-71.el6.x86_64 (@anaconda-CentOS-201106060106.x86_64/6.0) kernel = 2.6.32-71.el6 kernel = 2.6.32-71.el6 Installed: kernel-2.6.32-71.29.1.el6.x86_64 (@updates) kernel = 2.6.32-71.29.1.el6 kernel = 2.6.32-71.29.1.el6
カーネルが古すぎて、あたらしいカーネルを必要とするパッケージが入れられないのはわかるので、yum update kernel
などを行ってみたが
No Packages marked for Update
と表示されてアップデートできず。調べたところ/etc/yum.conf
にカーネルアップデートを除外するよう、デフォルトで設定が書かれていたからと判明する。
[main] cachedir=/var/cache/yum/$basearch/$releasever keepcache=0 debuglevel=2 logfile=/var/log/yum.log exactarch=1 obsoletes=1 gpgcheck=1 plugins=1 installonly_limit=5 bugtracker_url=http://bugs.centos.org/set_project.php?project_id=16&ref=http://bugs.centos.org/bug_report_page.php?category=yum distroverpkg=centos-release # This is the default, if you make this bigger yum won't see if the metadata # is newer on the remote and so you'll "gain" the bandwidth of not having to # download the new metadata and "pay" for it by yum not having correct # information. # It is esp. important, to have correct metadata, for distributions like # Fedora which don't keep old packages around. If you don't like this checking # interupting your command line usage, it's much better to have something # manually check the metadata once an hour (yum-updatesd will do this). # metadata_expire=90m # PUT YOUR REPOS HERE OR IN separate files named file.repo # in /etc/yum.repos.d exclude=kernel*
最後のexclude=kernel*
をコメントアウトしたら、無事アップデートできた。
IntelliJからMacの辞書(Dictionary.app)を開くプラグインをつくった
Macの辞書アプリはかなり便利で、何かキーワードを選択している状態でCTRL+CMD+Dのショートカットを押すとそのキーワードで辞書を引いてくれる。 主に英語ドキュメントなどにある分からない英単語を調べるのに使うのだけど、メインに使っているエディタのAtomとIntelliJ IDEAだとなんでかこのキーバインドが効かなくて残念な思いをしてた。そこでしばらく前にAtomから辞書を引けるようにしたパッケージとして以下を作った。
そしてここ最近Go言語をIntelliJ IDEAで書くようになったので、こっちでも辞書を引けるようにしたくて、さきほどちょちょいと作ってみた。
実装にあたってAtom版もIntelliJ版も両方ともすでに存在していたDashのプラグインを参考にした。 これにはエディタの選択領域からテキストを抽出して、外部コマンドとしてDashを起動するというコードある。 私がやったことは、そのあたりを参考元からを切りだしてDictionary.appを叩くように変えただけのもの。よって細かいところなどにはまるで気を使っていない実装なのだけど、個人的に使う分には必要十分に動くのでこれで満足している(^o^)
なお、Atomの方はパッケージの登録までしているのでAtomの設定画面から直接インストールできるが、IntelliJの方はJarをGithubのリリースページにおいただけなので、インストールするにはJarをダウンロードしてローカルファイルからインストールする必要があります。
sshのラッパーコマンドを作った
GoでSSHコマンドに便利機能を追加したコマンドを作りました。
2017/04/11 追記: この記事の内容は古くなっています!
2017/04/11時点の最新の仕様は、次の記事を参照してください
SSHラッパーコマンドEsshのv1.0.0をリリースしました - オープンソースこねこね
追記ここまで。以下は2015/11時点の古い情報となります。
機能として
- Luaで
~/.ssh/config
に相当する設定を書ける。 - zshの補完機能を使って、接続先一覧を出す。
- サーバ接続時にフックを仕込める。自分はスクリーンの色を変えるの使っている。
- 複数のリモートサーバにまとめてコマンドを実行する。
といったところです。詳細はリポジトリのREADMEを見ていだければと思います。 ビルド済みバイナリをリリースページにおいてあるので、インストールはダウンロードして解凍してパスの通ったディレクトリに配置すればOKです。
~/.ssh/config
を上書きするので~/.ssh/config
のバックアップを取っておくのをお忘れなく。
https://github.com/kohkimakimoto/zssh/releases/latest
使い方
zssh
はssh
のラッパーコマンドになっているのでssh
コマンドと同様に使えます。zssh
を実行するとssh
コマンドで実際の処理を実行する前に、設定ファイル~/.ssh/zssh.lua
を読み込んで~/.ssh/config
を生成するしくみになっています。
~/.ssh/zssh.lua
に以下のような設定を書いておくと
Host "web01.localhost" { ForwardAgent = "yes", HostName = "192.168.0.11", Port = "22", User = "kohkimakimoto", -- 小文字で始まる設定は、~/.ssh/configに出力されない。descriptionはzsh補完の説明文に使用される(後述) description = "my web01 server", } Host "web02.localhost" { ForwardAgent = "yes", HostName = "192.168.0.12", Port = "22", User = "kohkimakimoto", description = "my web02 server", }
zssh
実行時に次のような~/.ssh/config
を自動生成して上書きします。
Host web01.localhost ForwardAgent yes HostName 192.168.0.11 Port 22 User kohkimakimoto Host web02.localhost ForwardAgent yes HostName 192.168.0.12 Port 22 User kohkimakimoto
これで、sshコマンドと同様に以下のようにしてサーバにSSH接続できます。
zssh web01.localhost
ZSH補完
zsh補完をサポートしているので、以下のコードを~/.zshrc
に書いておくと
eval "$(zssh --zsh-completion)"
上に貼り付けたアニメgifのようにサーバが説明文付きで候補にでるようになります。
フック
hooks
の設定でサーバ接続時と切断時にローカルでLuaのコードを実行できます。
os.execute
でコマンドを実行できるので、以下のようにするとMacのターミナルの色を変更できます。
Host "web01.localhost" { HostName = "192.168.0.11", Port = "22", User = "kohkimakimoto", ForwardAgent = "yes", description = "my web01 server", -- フックの設定 hooks = { before = function() -- This is an example to change screen color to red. os.execute("osascript -e 'tell application \"Terminal\" to set current settings of first window to settings set \"Red Sands\"'") end, after = function() -- This is an example to change screen color to black. os.execute("osascript -e 'tell application \"Terminal\" to set current settings of first window to settings set \"Pro\"'") end, } }
ちなみに、ターミナルの色を変える方法は以前にも書きましたので、よければそちらも参照ください。
http://kohkimakimoto.hatenablog.com/entry/2015/04/02/211232
マクロ
コマンドを複数サーバにまとめて実行できます。
Host "web01.localhost" { HostName = "192.168.0.11", Port = "22", User = "kohkimakimoto", ForwardAgent = "yes", description = "my web01 server", tags = { role = "web" }, } Host "web02.localhost" { HostName = "192.168.0.12", Port = "22", User = "kohkimakimoto", ForwardAgent = "yes", description = "my web02 server", tags = { role = "web" }, } Macro "example" { -- 並列実行するか? parallel = true, -- 実行前に確認プロンプトを出す。 confirm = "Are you OK?", -- zsh補完時の説明 description = "example macro", -- 実行先サーバを指定。Host設定のtagsで設定したタグを指定する。指定しないとローカルでの実行になる。 on = {role = "web"}, -- ttyを使うか? tail -f でログを監視するときなどはtrueに tty = false, -- コマンドの内容 command = [[ ls -la ]], }
マクロ名を指定して実行できます。
$ zssh example