tkuchikiの日記

新ブログ https://blog.tkuchiki.net

application で認証した後に nginx で static file を配信する

application で認証している場合のみファイルをダウンロードさせたい場合があると思いますが、
そんなときの設定例です。
以下に簡単に試す方法を記載しています。

tkuchiki/nginx-direct-data-transfer-sample · GitHub

例では Basic 認証ですが、実際はちゃんとした認証がいいでしょう。
application 側で http headr に、
X-Accel-Redirect としてファイルのパスを追加すると、
nginx から static file を配信することができます。

X-Accel-Redirect を使用しなくても、
nginx の proxy cache がききますが、
大容量ファイルの配信を行う場合、
proxy cache の作成に Disk I/O、CPU リソースを大量に使ってしまう可能性があります。

sinatra の場合、以下のようになります。

content_type "image/png"
headers "X-Accel-Redirect" => "/img/black.png"
status 200

これで負荷を低減することができます。

Bats を使って Travis CI で bash(シェルスクリプト) のCI を回す

Bats で bash のテストを書く - tkuchikiの日記
の続きです。

bash のテストができるようになると CI を回したくなりますよね。
ということで、Travis CI でテストを回すまでの方法を説明します。

https://github.com/tkuchiki/bats-travis-ci
にすべてのコードを置いています。

.travis.yml

before_install で bats をインストールし、script で実行します。

language: bash
before_install:
  - sudo add-apt-repository ppa:duggan/bats --yes
  - sudo apt-get update -qq
  - sudo apt-get install -qq bats
script:
  - bats /path/to/test.bats

これで Travis CI で bats が使えるようになります。
あとは、テストを書いて push すれば bats を実行できます。
以下は、実行結果の一部です。

$ git clone --depth=50 --branch=master https://github.com/tkuchiki/bats-travis-ci.git tkuchiki/bats-travis-ci
$ cd tkuchiki/bats-travis-ci
$ git checkout -qf 56e66e566c89c7e891862bb2f3128cb8c9014dcf
$ sudo add-apt-repository ppa:duggan/bats --yes
$ sudo apt-get update -qq
$ sudo apt-get install -qq bats
$ sudo apt-get install -qq bc
$ bats ./test.bats
1..7
ok 1 addition using bc
ok 2 status code
ok 3 output
ok 4 lines
ok 5 run command
ok 6 load
ok 7 # skip (skipped) skip
The command "bats ./test.bats" exited with 0.
Done. Your build exited with 0.

実行結果の詳細は以下のページにアクセスすると確認できます。

https://travis-ci.org/tkuchiki/bats-travis-ci

まとめ

Travis CI で bash のテストを行う方法を紹介しました。
yaml を少し書くだけで bash の CI を回せるようになりますので、
是非お試しください。

Bats で bash(シェルスクリプト) のテストを書く

※記事を書いて公開するまで1年くらい経っているので情報が古い可能性があります。

Bats(Bash Automated Testing System)は、bash のテスティングフレームワークです。sstephenson/bats · GitHub

Test Kitchen(Bussser) の Bats plugin で使うことができるのでご存知の方も多いと思います。

簡単に使い方を説明すると、

@test "テストの説明" {
  command
  ...
}

という記法でテストを記述し、コマンドの返り値が 0 ならテストをパスします。
以下のようにスクリプトを実行してテストをすることができます。

$ bats test.bats

インストール

インストール方法は、以下のとおりです。

sudo add-apt-repository ppa:duggan/bats --yes
sudo apt-get update -qq
sudo apt-get install -qq bats
brew install bats
  • その他
git clone https://github.com/sstephenson/bats.git
cd bats
./install.sh /usr/local

テストの書き方

テストコードと実行結果は、以下のようになります。

  • テストコード


  • 実行結果

テストコードを元に上から順に説明していきます。

setup, teardown
  • setup
    • 各テストの実行前に実行される処理を関数として定義
  • teardown
    • 各テストの実行後に実行される処理を関数として定義

となります。
コード内で、BATS_ から始まる変数が使われていますが、
これらは Bats の特殊変数です。
詳細は後述します。

run, $status, $output, $lines

run FILEPATH で外部スクリプトを実行すると、
$status, $output, $lines に値がセットされます。
各変数は以下のとおりです。

以下のスクリプトだと、


status=1
output=foobar
lines[0]=foobar

になります。
lines については配列なので、出力が 2 行の場合は lines[1] に 2 行目の出力結果が格納されます。

skip

skip は、文字通りテストをスキップします。

load

load は、外部スクリプトを読み込みます。
load の引数となるファイルパスを書く場合は 2 つの注意点があります。

特殊変数

以下の実行結果から、それぞれの変数の値を見ていくと、


  • BATS_TEST_NAME
    • テストの説明部分を test_ prefix と、半角スペースを _ に変換したもの
  • BATS_TEST_FILENAME
    • 実行している Bats ファイルのフルパス
  • BATS_TEST_DIRNAME
    • Bats ファイルを配置しているディレクトリ
  • BATS_TEST_NAMES
    • BATS_TEST_NAME を 0 から始まる配列に格納
  • BATS_TEST_DESCRIPTION
    • テストの説明部分
  • BATS_TEST_NUMBER
    • テストの番号

となっていることがわかります。

また、実行結果を見ると、
setup -> テスト -> teardown と実行されていることが確認できます。

テスト外のコードについて

@test, setup, teardown 関数の外で標準出力をするコードを書いた場合、
>&2 で標準エラー出力にリダイレクトするようにしないと、
TAP の標準出力を汚染してテストが失敗する可能性があるようです。

まとめ

サンプルのテストコードを元に、Bats の使い方を説明しました。

スクリプトの実行ステータスや標準出力のチェックを行う用途であれば、
bash 以外のテストも行えるので色々な用途に利用できるのではないかと思います。

linux で TZ の offset を出力する

date +%z を使います。

$ TZ=UTC date +%z
+0000

$ TZ=Asia/Tokyo date +%z
+0900

$ TZ=EST date +%z
-0500

全ての TZ と offset を出力する場合は、以下のシェルスクリプトを実行します。

実行結果

$ ./tz.sh
EST5EDT -0500
MST7MDT -0700
Portugal +0000
Jamaica -0500
Poland +0100
Zulu +0000
GMT-0 +0000
ROK +0900
Chile/EasterIsland -0500
Chile/Continental -0300
...
  • posix 始まりの TZ を除く場合は ./tz.sh | grep -v ^posix
  • posix 始まりの TZ のみ出力する場合は ./tz.sh | grep ^posix

HAPrxoy 1.6.0 で導入された DNS の動的名前解決の検証結果

追記(2016-07-13 16:40)

Changelog に書いてあるとおり(BUG/MEDIUM: dns: unbreak DNS resolver after header fix)、
動的名前解決ができないバグが修正されました。

詳細は省きますが、本記事と同様の検証を行い、動的名前解決ができることを確認しました。

追記(2016-06-24 12:34)

HAProxy 1.6.5 は、DNS の動的名前解決が動作しないようです。
ご注意ください。

検証環境は以下のとおりです。

http://www.haproxy.org の Quick News に、

dynamic DNS-based server address resolution

と書いてあるとおり、
HAProxy 1.6.0 から DNS の動的名前解決に対応しました。
HAProxy 1.6.0 より前のバージョンではどうだったかというと、
起動時に DNS を参照して、それ以降 DNS を参照することはありません。
この挙動については、ソースコードを見るか、
port 53 の通信をキャプチャすると確認することができます。

追記(2015-10-30 14:57)

http://b.hatena.ne.jp/mapk0y/20151030#bookmark-270133853
ブックマークにコメントを頂いたとおり、
A レコードを動的に変えるものとして RDS を使いましたが、
例ですのでこの構成に意味はありません。
RDS の前段に HAProxy を置く場合は、
Slave(Read Replica) を複数台立てて参照を分散する、
といった使い方になると思います。
強引にメリットを上げるとすれば、
最初は1台でスタートして、
あとから複数台の DB を立てて参照分散したいとしたときに、
アプリの修正が必要ないことでしょうか。
コメントありがとうございました!

動的名前解決しないとどうなるか

動的名前解決機能を説明する前に、
HAProxy 1.5.2 を使い、
動的名前解決しない場合の挙動について説明します。

RDS の準備

サーバがダウンしたらいい感じに A レコードを更新してくれるものとして、
Amazon RDS があります。
Amazon RDS には reboot with failover という、
強制的に failover させる機能がありますので、これを検証に使います。
failover させるために、Multi-AZ を有効にしてインスタンスを作成してください。

MySQL の準備

HAProxy には mysql の health check を行う機能があります。
監視用のユーザが必要ですので作成します。
haproxy ユーザを作るために、以下の SQL を実行します。

grant usage on *.* to 'haproxy'@'%';

ngrep の準備

port 53 への通信をキャプチャするために、
ngrep をインストールします。
キャプチャできれば良いので、tcpdump でも良いです。

yum install -y epel-release
yum install -y ngrep

HAProxy の準備

amzn-main に HAProxy 1.5.2 があったので、

yum install -y haproxy

してインストールします。
設定ファイルは以下のとおりです。

# /etc/haproxy/haproxy.cfg
listen  mysql-slave
        bind            127.0.0.1:3307
        mode            tcp
        option          mysql-check user haproxy
        balance         roundrobin
        server          master  test-db.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com:3306 check

検証

準備が整ったので、HAProxy 1.5.2 では動的名前解決しないことを確認します。
RDS が failover する前の IP アドレスを調べます。

$ host test-db.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com
test-db01.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com has address 10.1.0.100

ngrep を実行して、port 53 への通信がないかを確認します。

$ ngrep -d any -W byline port 53 -q

上記のコマンドを実行した状態で、
service haproxy restart を実行して再起動すると、

$ ngrep -d any -W byline port 53 -q
interface: any
filter: ( port 53 ) and (ip or ip6)

U 10.1.0.2:53 -> 10.1.0.10:42761
.............test-db.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com.................
.."

U 10.1.0.10:49225 -> 10.1.0.2:53
.............test-db.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com.....

U 10.1.0.2:53 -> 10.1.0.10:49225
.............test-db.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com.................
.."

U 10.1.0.10:34814 -> 10.1.0.2:53
OU...........test-db.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com.....

U 10.1.0.2:53 -> 10.1.0.10:34814
OU...........test-db.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com.................
.."

のように、DNS を参照していることがわかります。
前述のとおり、HAProxy 1.5.2 は動的名前解決をしないので、
これ以降は ngrep の様子を気にする必要はありません。

それでは、RDS を failover させます。
AWS CLI では、以下のようにコマンドを実行し、

$ aws rds reboot-db-instance --db-instance-identifier test-db --force-failover

Management Console から操作する場合は、
チェックボックスにチェックを入れて再起動します。

f:id:tkuchiki:20151030115951p:plain

Multi-AZ instance failover completed というログが出たら failover しています。

$ host test-db.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com
test-db01.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com has address 10.2.0.200

IP アドレスが変わっていることを確認できます。
この状態で、HAProxy を通して RDS に接続します。

$ mysqladmin ping -u haproxy -h 127.0.0.1 -P 3307
mysqladmin: connect to server at '127.0.0.1' failed
error: 'Lost connection to MySQL server at 'reading initial communication packet', system error: 0'

接続できないことが確認できます。
netstat でも確認してみると、

$ netstat -anp | grep haproxy | grep :3306
tcp        0      1 10.1.0.10:56199            10.1.0.100:3306              SYN_SENT    1553/haproxy

failover 前の IP アドレスに接続しようとしています。

以上のことから、動的名前解決しないと、
A レコードなどを変更しても接続先を変えることができないことがわかります。

動的名前解決を試す

動的名前解決できないとどうなるかがわかったところで、
HAProxy 1.6.1 を使い、
どのように挙動が変わったか確認します。
amzn-main には HAProxy 1.6.1(1.6.0 も)ないので、
適宜ビルドしてください。

HAProxy のログを出力するために、
rsyslog を設定します。

# /etc/rsyslog.conf
+ $ModLoad imudp
+ $UDPServerRun 514
# /etc/rsyslog.d/haproxy.conf
+ $ModLoad imudp
+ $UDPServerRun 514
+ $template Haproxy,"%msg%\n"
+ local1.* -/var/log/haproxy.log;Haproxy

service rsyslog restart で再起動します。
動的名前解決用の設定をします。

# /etc/haproxy/haproxy.cfg
resolvers mydns
   nameserver dns1 10.2.0.2:53
   nameserver dns2 8.8.8.8:53
   resolve_retries 3
   timeout retry   1s
   hold valid      60s

listen  mysql-slave
        bind            127.0.0.1:3307
        mode            tcp
        log             127.0.0.1 local1 debug
        option          mysql-check user haproxy
        balance         roundrobin
        server          master  test-db.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com:3306 check resolvers mydns

resolve_retries 3, timeout retry 1s は見たままですが、
リトライする回数とタイムアウトの秒数です。
hold valid 60s は、名前解決が成功したら、指定秒数名前解決しない設定です。
このオプションは、health check が fail したときは使われません。

nameserverDNS サーバを複数設定できます。

http://cbonte.github.io/haproxy-dconv/configuration-1.6.html#5.3.2-nameserver を見ると、
port を省略しても良いという記述はないので、
省略することはできません。
私は、port を省略していたため動的名前解決ができなくて、
2時間くらい無駄にしました...。

設定ができたら、先述の ngrep を実行しておきます。
service haproxy restart して準備完了です。

この時点で、port 53 への通信が定期的にあるので、
動的名前解決ができそうな予感がします。

RDS の IP アドレスを確認して、RDS を failover させます。

$ host test-db.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com
test-db01.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com has address 10.2.0.200

ngrep の様子を見ているとhold の設定とは関係なく、
health check の間隔くらいで名前解決している様子を確認できます。

RDS の A レコードが書き換わり、
HAProxy の health check が成功すると、

mysql-slave/master changed its IP from 10.2.0.200 to 10.1.0.100 by mydns/dns1.
Server mysql-slave/master is UP, reason: Layer7 check passed, code: 0, info: "5.6.23", check duration: 1ms. 1 active and 0 backup servers online. 0 sessions requeued, 0 total in queue.

と、接続先が変わったことがログに出力されます。
IP アドレスを確認して、mysqladmin pingnetstat でも確認すると、

$ host test-db.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com
test-db01.xxxxxxxxxxxx.ap-northeast-1.rds.amazonaws.com has address 10.1.0.100

$ mysqladmin ping -u haproxy -h 127.0.0.1 -P 3307
mysqld is alive

$ netstat -anp | grep 3306
tcp        0      0 10.1.0.10:57427            10.1.0.100:3306              TIME_WAIT   -

failover 後のインスタンスに接続していることがわかります。

まとめ

HAProxy 1.6.0 から数行設定を追加するだけで動的名前解決できることを示しました。
メンテナンスで DNS 切り替えたら繋がらなくなった...、
みたいなことがなくなるので便利そうですね。
port 問題で消耗したのでソースコードはあまり読めていないので、
詳細な挙動が知りたい方は、ご一読いただくと良いと思います。

※ 検証するときは、以下のことに注意してください。

  • ドキュメントをよく読む
  • port を省略してはいけない、port を省略するのは甘え

zabbix の LLD で gearmand の job を動的に監視する

検証した環境の zabbix のバージョンは、2.0.15 です。

gearmand の監視を zabbix で行う際、
job の数がアプリケーションによって違うと思いますので、
テンプレートを作っても汎用的に使うことができません。

こういうケースで使えるのが LLD(ローレベルディスカバリ) です。
アイテムを動的に追加したりするあれです(vfs.fs.discovery など)。
LLD は自作もできるので、自作してみました。

Template

をインポートします。

ポートは、テンプレートのマクロで、
{$GEARMAND_PORTS} = 4730 を設定しています。

f:id:tkuchiki:20151027171421p:plain

ホストごとにポートを変更したい場合は、

ホストのマクロに {$GEARMAND_PORTS} = 14730 などと設定して上書きしてください。
また、複数ポート指定できるようにしてあるので、
必要であれば空白区切りでポートを列挙してください。

f:id:tkuchiki:20151027171626p:plain

UserParameter

LLD も UserParameter で集計するので設定ファイルとシェルスクリプトを置く必要があります。

gearmand.conf

UserParameter の設定を記述します。
zabbix_agentd.conf にベタ書きする場合は、
zabbix_agentd.conf で Include せず、直接記述してください。

zabbix_agentd.conf

UserParameter の定義を書いたファイルを読み込むために、
Include を追記します。

Include=/etc/zabbix/zabbix_agentd.d/gearmand.conf

user.discovery.gearmand.sh

アイテムを自動収集するためのスクリプトです。
LLD では、決まった形式の json を返すだけで、zabbix がアイテムを登録してくれます。

f:id:tkuchiki:20151027175902p:plain

となっているので、{$GEARMAND_PORTS} で設定したポートを引数にスクリプトを実行します。
user.discovery.gearmand.sh は第一引数に port を受けるようになっているので、
zabbix-agent が user.discovery.gearmand.sh 4730 のように叩き、
以下のような json を返します。

{
  "data": [
    {
      "{#GEARMAND_FUNC_NAME}": "Notification",
      "{#GEARMAND_PORT}": 4730
    },
    {
      "{#GEARMAND_FUNC_NAME}": "GroupNews",
      "{#GEARMAND_PORT}": 4730
    }
  ]
}

アイテムのプロトタイプに、gearmand[{#GEARMAND_PORT}, {#GEARMAND_FUNC_NAME}, capable_workers]
と設定しているので、

  • gearmand[4730, Notification, capable_workers]
  • gearmand[4730, GroupNews, capable_workers]

というアイテム(キー) が自動で登録されます。
json とアイテムのプロトタイプを見るとなんとなくお分かりいただけると思いますが、
json の key をアイテムのプロトタイプを作るときに利用でき、
key と対応する value が展開される、という動作をします。
capable_workers は、もう一つのシェルスクリプト(gearmand.sh)で使っています。

gearmand.sh

以下は、gearadmin の実行結果です。

$ gearadmin --status
Notification    1    0    1
GroupNews    2    1    1
.

左から順に、job名、キュー数、実行中のジョブ数、実行可能なworker数です。
job名以外は忘れやすそうでしたので、名前で参照できるようにしています。
名前で指定できるようにして、 STAT=$(eval echo '$'${3}) の部分で変数を展開して数値に戻しています。

設定完了

Template_gearmand をホストに設定して zabbix-agent を restart すれば、
LLD の設定完了です。
zbx_template_gearmand.xml では、port を listen しているか、capable_workers が 0 でないかを監視するトリガを設定しています。
必要であれば適宜トリガを追加してください。

まとめ

LLD で gearmand の job を監視する方法を紹介しました。
gearmand を使っていない方も、LLD の設定の参考にしていただければ幸いです。

参考資料

/etc/shadow の hash を生成するコマンドを作りました

perl, php, python の標準モジュールで /etc/shadow の hash を生成する方法 - tkuchikiの日記

で、/etc/shadow の hash を生成する方法を書きましたが、
どの環境でも同じ方法で出来たら楽だと思いましたので、

https://github.com/tkuchiki/encrypt-pw を作りました。

Installation

https://github.com/tkuchiki/encrypt-pw/releases からお使いの環境用のバイナリを取得して、
解凍してください(Windows でも動作します)。

Usage

実行例は、以下のとおりです。

$ ./encrypt-pw --help
usage: encrypt-pw [<flags>]

Encrypts password (starts from $1$, $5$, $6$ hash)

Flags:
      --help               Show context-sensitive help (also try --help-long and --help-man).
      -h, --hash="sha512"      Hash algorithm (sha512, sha256, md5)
      -r, --rounds=5000        Number of hashing rounds (min: 1000, max: 999999999)
      -c, --confirm            Confirm password
      -p, --password=PASSWORD  Password
      --version            Show application version.

$ ./encrypt-pw
Enter password:
$6$eVuwo0XKesWWkbv3$/L.Sw8RJuk69aamprVSN.id2tCWJ0OiSmRJ12JNyahAdPopx7aHOWDvZ/PYustFFQ6Eu7vp22FYLqvXTUIo9I0

$ ./encrypt-pw -c
Enter password:
Enter same password again:
$6$59LB.ugo8iWh4dJf$4YjghX24znjgRC5MPF/h/f.FP8K37EiWBjmaybGWIMFTEQRPaQUDibCUcg72qRx54qW0As3GsvkCEYShEifMA.

$ ./encrypt-pw -p your-password
$6$N2UrLLeq3i3560Vv$cOC/xvVRHo1vGAJcsp788UrkUIg1Bc66.pwYRhcQFfiI4lor8SDQQKgW8zT7qdc4bflbEnkTGEyulU7v9DCcT

$ ./encrypt-pw -h sha256 -p your-password
$5$m0Q22Redjm5D7.zH$rvRH/obRUtCu9osnxxOYcg0JOTLoDpPnSLagpU9gn6B

$ ./encrypt-pw -h md5 -p your-password
$1$CaybI5OM$p3F4OZCYEmOLOCtZmmbAw1

$ ./encrypt-pw -r 100000 -p your-password
$6$rounds=100000$g9EPi5gIxSX2cpMR$UVIstanJwu4uCtNpW1HIdQmu.Y4yPm9HzVL1mcwoz0E87Gn0FI7AunYy5wOQ8FBArwlIQc6N6YZITsDW6aZZh/

各オプションは、

  • -c|--confirm : 2回入力して一致したら hash を生成
  • -h|--hash (md5|sha256|sha512) : hash アルゴリズムを選択
  • -p|--password PASSWORD : 対話入力せずに hash を生成
  • -r|--rounds : hashing rounds を指定 (md5 では無効)

となっています。