Jenkins Pipeline を使ってみたメモ

次のようなことがやりたかった。

  • テストを実行するジョブと、テストが通った特定(master)のブランチをデプロイするジョブを作る
    • さらにデプロイが完了したブランチを Redmine に同期するジョブも作るけどそれは省略
    • 機能ブランチをテスト → master にマージしてテスト → デプロイ → Redmine に同期
  • デプロイ対象のサーバ自体を Jenkins スレーブにする
    • デプロイは Jenkins がソースをチェックアウトしてローカルコピーするだけ
    • テストとデプロイは別のスレーブで実行されることがある

Pipeline

  • 環境変数 BRANCH_NAME が設定されない
    • シェルで同じ情報を得られなくもないけど
  • properties ビルド世代を指定するとジョブの実行時に一部の設定が消える?
    • 後述の Multibranch Pipeline のように設定してると SCMをポーリング の設定が消える
    • 上書きされてしまうため?
  • checkout scm にコミットのIDが渡されないっぽい
    • 連続でばばばっとプッシュされると同じコミットが2回ビルドされることがある
    • ジョブが実行されたときの最新コミットでビルドされるため
    • 普通は Git のポーリングで検出されたコミットになるはずなのに
    • ので Pipeline の後段で checkout scm していると前段とは異なるコミットになることがある
    • ので前段と後段で同じコミットをチェックアウトするためにはIDを指定する必要がある
    • ので checkout scm は使えない(使いにくい)

次のように test のステージでチェックアウトされたコミットを取得して deploy のステージで使う。

def git_url = 'http://gitlab.example.com/goto/testing.git'
def git_branch
def git_commit

node('test'){
   stage 'test'
   git url: git_url, branch: "**"
   echo "this is test"

   git_branch = sh(script: 'git rev-parse --abbrev-ref HEAD', returnStdout: true).trim()
   git_commit = sh(script: 'git rev-parse HEAD', returnStdout: true).trim()

   if (git_branch == 'master') {
      node('dev'){
         stage 'deploy'
         checkout([$class: 'GitSCM', branches: [[name: git_commit]], userRemoteConfigs: [[url: git_url]]])
         echo "this is deploy"
      }
   }
}

あるいは test のステージのツリーを stash して deploy のステージで unstash でも良いかもしれないけどツリーが巨大だと辛そう。

node('test'){
   stage 'test'
   checkout scm
   echo "this is test"
   def git_branch = sh(script: 'git describe --all', returnStdout: true).trim().replaceFirst(/^\w+\/\w+\//, '')
   if (git_branch == 'master') {
      stash "ok"
      node('dev'){
        stage 'deploy'
        unstash "ok"
        echo "this is deploy"
      }
   }
}

Multibranch Pipeline

  • ブランチごとにジョブが自動的に作成される
    • Jenkinsfile が含まれるブランチだけ対象
  • Branch SourcesGitSingle repository ってのがあるけど何が違うの?
    • Single repository だとジョブは指定した名前で1つだけできる?
    • なぜか設定を保存してもう一度開くと設定がクリアされている?
    • よくわからない
  • Discard old items で保持数に 1 以上を指定するとブランチが消えてもジョブが消えないっぽい?
    • 0 を指定すれば大丈夫だけどそういうものなの?
  • ブランチが消えてジョブが削除されてもワークスペースが削除されない
    • master には下記のディレクトリができる
      • workspace/${multi}/${branch}@script/
    • node で指定した slave には下記のディレクトリができる
      • workspace/${multi}/${branch}/
      • workspace/${multi}/${branch}@tmp/
    • ブランチ作成→削除を繰り返すとものすごいゴミがたまる
  • ブランチジョブのビルド履歴は properties で指定しないとダメ
    • 親の Discard old items が継承されるわけではない

下記のような Jenkinsfile でそこそこ良い感じにできるけど、ブランチ作成→削除でゴミがたまる。

properties properties: [
  [$class: 'BuildDiscarderProperty', strategy: [$class: 'LogRotator', numToKeepStr: '10']]
]

node('test'){
   stage name: 'test', concurrency: 1
   checkout scm
   echo "this is test"
}

if (env.BRANCH_NAME == 'master') {
   node('dev'){
      stage name: 'deploy', concurrency: 1
      checkout scm
      echo "this is deploy"
   }
}

例えばジョブの実行時にカレントディレクトリの兄弟のディレクトリが古かったら削除するとか。

properties properties: [
  [$class: 'BuildDiscarderProperty', strategy: [$class: 'LogRotator', numToKeepStr: '10']]
]

node('test'){
   stage name: 'test', concurrency: 1
   checkout scm
   echo "this is test"
}

if (env.BRANCH_NAME == 'master') {
   node('dev'){
      stage name: 'deploy', concurrency: 1
      checkout scm
      echo "this is deploy"
   }
}

stage 'purge'
node('test'){
  sh "find ../ -mindepth 1 -maxdepth 1 -mmin +60 -print0 | xargs -0 rm -vfr"
}
node('master'){
  sh "find ../ -mindepth 1 -maxdepth 1 -mmin +60 -print0 | xargs -0 rm -vfr"
}

ブランチの有無、あるいはジョブの有無を確認して削除するかどうかを判断できると理想。

まとめ

そんなに多段にしているわけでもないし、Jenkins DSL でいまのところ満足しているので、当面はそのままでいいや。

Windows と Linux (*nix) のコマンドライン引数の違い

元は下記で回答したものですけど。。。


Linux でプログラムを実行するとき、最終的に次の関数が実行されます。

int execve(const char *filename, char *const argv[], char *const envp[]);

つまり、次のものが必要とされます。

  • 実行ファイル名
  • コマンド引数の配列
  • 環境変数の配列

一方、PHP のような言語から外部コマンドを実行したい場合、大抵は次のようなものを指定すると思います。

  • コマンドライン文字列(実行ファイル名+コマンド引数をスペース区切りで結合したもの)

とりあえず環境変数は無視しまして・・・前述の通り最終的に必要なのは「実行ファイル名」と「コマンド引数の配列」なので「コマンドライン文字列」をバラバラに分割する必要がありますが、それを行っているのがいわゆる「シェル」です。

なので「コマンドライン文字列」を指定してコマンドを実行する場合は、大抵の場合シェルを経由します。

例えば、Node.js の child_process モジュールを見てみると、シェルを経由する child_process.exec は「コマンドライン文字列」を指定するのにたいして、シェルを経由しない child_process.spawn では「実行ファイル名」と「コマンド引数の配列」を指定するようになっています。

PHP でもシェルを経由しない pcntl_exec は「実行ファイル名」と「コマンド引数の配列」を指定します。

つまり「コマンドライン文字列」を指定して外部プログラムを実行できるようにするためにはシェルを経由する必要があります。

もちろん「コマンドライン文字列」をプログラミング言語の方でバラバラに分割するようにすればシェルを経由せずに実行することも可能です。例えば Ruby では、一部のメタ文字を含んでいない場合は Ruby の側で「コマンドライン文字列」を「実行ファイル名とコマンド引数の配列」に分割することでシェルを経由せずに実行するようです。

メタ文字を含まない、ということはスペースで分割するだけで簡単にコマンドライン文字列をバラすことができるので、そのような実装になっているのだと思います(メタ文字を含む場合はシェルと同等のパーサが必要になりますしリダイレクトやパイプなどのシェル特有の事情も出てきてしまいます)。

なお、これらの事情は Linux などの *nix 系 OS の事情であって Windows では異なります。

Windows だと外部プログラムを実行するときの API で「実行ファイル名」と「コマンドライン引数(コマンド引数をスペースで繋げた文字列)」を指定します。

BOOL CreateProcess(
  LPCTSTR lpApplicationName,                 // 実行可能モジュールの名前
  LPTSTR lpCommandLine,                      // コマンドラインの文字列
  LPSECURITY_ATTRIBUTES lpProcessAttributes, // セキュリティ記述子
  LPSECURITY_ATTRIBUTES lpThreadAttributes,  // セキュリティ記述子
  BOOL bInheritHandles,                      // ハンドルの継承オプション
  DWORD dwCreationFlags,                     // 作成のフラグ
  LPVOID lpEnvironment,                      // 新しい環境ブロック
  LPCTSTR lpCurrentDirectory,                // カレントディレクトリの名前
  LPSTARTUPINFO lpStartupInfo,               // スタートアップ情報
  LPPROCESS_INFORMATION lpProcessInformation // プロセス情報
);

それどころか lpApplicationName は省略可能なので、「コマンドライン文字列(実行ファイル名+コマンド引数をスペース区切りで結合したもの」だけでも可能です。

つまり Windows の場合はシェルを経由させなくても「コマンドライン文字列」を指定して外部プログラムを実行することができます。

そのためなのかどうかわかりませんが PHP の proc_open には Windows にだけ bypass_shell というシェルを経由しないオプションがあります。


そういえば @do_aki さんに教えてもらったんですけど、過去に *nix でも bypass_shell をサポートするコミットがあったらしいです、Revert されていますけど。

bypass_shell を指定したときの第1引数が Windows だと文字列(コマンドライン)なのに対して *nix だと配列(コマンドとコマンド引数の配列)となってしまうためなのかなーと想像しています。


ちなみに libuv を見た感じ、配列で渡されたコマンドライン引数を CreateProcess に文字列で渡すために自前で結合とエスケープ処理しているみたいですね。

もちろん *nix なら配列をそのまま渡すだけなのでそういう面倒なことはしていません。


ちなみに *nix だと execveargv がそのまま mainargv に渡ってくる感じだと思うんですけど、Windows だとそもそもプロセス作成時には引数を配列ではなく単一の文字列で指定するので、それをバラして argv に入れるのは CRT のスタートアップコードの仕事です。

CRT なんてコンパイラによって違うため(VC とか gcc とか)、コマンドライン文字列がどのようにバラされて argv になるかは微妙にブレがあります。

また、噂によると Ruby の Windows 版は CRT には頼らずに GetCommandLine かなにかで得たコマンドライン文字列を自前でバラしているとか。

そういう事情を鑑みると Windows で汎用的な escapeshellarg を作ることなんて不可能で とてもじゃないけど使い物になるようなシロモノではない のもしかたがないことだと思いますね。

redmine_dmsf を試してみたメモ

Redmine のファイル管理プラグイン redmine_dmsf をちょっと試してみたメモ。


インストールします。

sudo su - redmine
cd /var/lib/redmine/plugins
git clone https://github.com/seventhsense/redmine_dmsf
cd /var/lib/redmine/
bundle install

なにやらエラーになりました。

:
checking for uuid/uuid.h... no
checking for uuid.h... no
configure: error: Neither uuid/uuid.h nor uuid.h found - required for brass, chert and flint (you may need to install the uuid-dev, libuuid-devel or e2fsprogs-devel package)
rake aborted!
:

libuuid-devel が必要なようです。

sudo yum install libuuid-devel

改めて bundle install します。

cd /var/lib/redmine/
bundle install

マイグレーションやアセットのコンパイルをします。

bundle exec rake redmine:plugins:assets RAILS_ENV=production
bundle exec rake redmine:plugins:migrate RAILS_ENV=production

アプリケーションサーバを起動します。

RAILS_ENV=production bundle exec rails server -b 0.0.0.0 -p 9876

使ってみた感想です。

  • Redmine をファイルサーバっぽく使える
  • Redmine の画面から D&D で複数アップロードできる
  • 同じファイルをアップロードするとバージョン管理される
  • WebDAV でもアクセスできる
  • ファイルに承認フローを設定できる
    • 具体的に何が起こるのかは試していない
    • なぜか承認者に一部のユーザーしか選択できない? 謎い?
  • ファイルの編集をロックできる
    • 自分以外は編集できないようにできる
  • Redmine の画面からメールの添付ファイルで送信することができる
    • From は redmine のグローバル設定の From のまま

メールで送信するのがためらわれるようなサイズのファイルを取引先に送りたいとき、今は Apache で作った WebDAV とかにアップロードして、IP アドレスとか Basic 認証とかで制限していたのですが、Redmine を WebDAV として使えるなら Redmine のアカウントの管理だけで良くなるので便利そうです。

と、思ったんだけど、チケットやフォーラムでファイルを添付するなり、「ファイル」の機能を使えばいいんじゃないか? って気がした。

そもそも、このプラグインは Redmine 上で文章管理するためのものであって、ファイルの受け渡しのために一時的なファイルの置き場所にするような用途には向かないと思う。

ので、自社で使用している Redmine への導入は見送り。

KVM で macvtap (direct) を使ってみるメモ

下記の記事で KVM の macvtap (direct) というものを知ったので使ってみたメモ。

ホスト・ゲストともに下記で試しています。

# cat /etc/centos-release
CentOS Linux release 7.2.1511 (Core)

sv01 と sv02 というゲストがセットアップ済です。

# virsh list --all
 Id    Name                           State
----------------------------------------------------
 -     sv01                           shut off
 -     sv02                           shut off

既存のインタフェースはすべて削除しています。

virsh detach-interface sv01 bridge --config
virsh detach-interface sv02 bridge --config
virsh detach-interface sv01 direct --config
virsh detach-interface sv02 direct --config
virsh detach-interface sv01 network --config
virsh detach-interface sv02 network --config

direct-bridge を libvirt のインタフェースで作る

インタフェースの定義ファイルを作成。

/tmp/attach-device.xml

<interface type='direct'>
  <source dev='eth0' mode='bridge'/>
</interface>

インタフェースをゲストに追加。

virsh attach-device sv01 /tmp/attach-device.xml --config
virsh attach-device sv02 /tmp/attach-device.xml --config

開始してコンソールに接続して ping とかで疎通を確認する。

virsh start sv01
virsh start sv02
virsh console sv01
virsh console sv02

この時点で、「ゲスト~ゲスト」とか「ゲスト~外部」は疎通があるが、「ゲスト~ホスト」は接続できない。

direct-bridge を libvirt のネットワークで作る

ネットワークの定義ファイルを作成。

/tmp/net-direct.xml

<network>
  <name>direct</name>
  <forward mode="bridge">
    <interface dev="eth0"/>
  </forward>
</network>

ネットワークを追加して開始。

virsh net-define --file /tmp/net-direct.xml
virsh net-start direct

インタフェースの定義ファイルを作成。

/tmp/attach-device.xml

<interface type='network'>
  <source network='direct'/>
</interface>

インタフェースをゲストに追加。

virsh attach-device sv01 /tmp/attach-device.xml --config
virsh attach-device sv02 /tmp/attach-device.xml --config

開始してコンソールに接続して ping とかで疎通を確認する。

virsh start sv01
virsh start sv02
virsh console sv01
virsh console sv02

「ゲスト~ホスト」が接続できないのは同じ。

「ゲスト~ホスト」を通す

下記の記事の通り。

macvlan デバイスを作成して eth0 のアドレスとルーティングを macvlan0 に付ける。

ip link add link eth0 name macvlan0 type macvlan mode bridge
ip link set macvlan0 up
ip addr del 192.0.2.123/24 dev eth0
ip addr add 192.0.2.123/24 dev macvlan0
ip route flush dev eth0
ip route add default via 192.0.2.1 dev macvlan0 proto static metric 100

「ゲスト~ホスト」も接続できるようになる。

macvlan0 の永続化

man にはないけど下記だと nmclitypemacvlan が指定出来そうだったので試してみたところ・・

# nmcli con add ifname macvlan0 type macvlan dev eth0 mode bridge
Error: invalid connection type; 'macvlan' not among [generic, 802-3-ethernet (ethernet), pppoe, 802-11-wireless (wifi), wimax, gsm, cdma, infiniband, adsl, bluetooth, vpn, 802-11-olpc-mesh (olpc-mesh), vlan, bond, team, bridge, bond-slave, team-slave, bridge-slave].

だめっぽい。

参考

HAProxy を使ってみたメモ

CentOS 7 の yum でさくっとインストールできるバージョンで試しています。

  • haproxy-1.5.14-3.el7.x86_64

その他に、ログとかスティッキーセッションとかについても書いてます。

あと、いまのところ使う予定がなかったので mode tcp は試していません。

WEB で統計レポートを表示

プロキシのセクション(defaults/listen/frontend/backend)で stats enable を指定すれば、http://localhost/haproxy?stats のような URL で HAProxy の統計レポートを表示できる。

frontend main *:80
    stats enable
    :

URL は stats uri で変更できる。下記の例だと http://localhost/admin?stats となる。

frontend main *:80
    stats enable
    stats uri /admin?stats
    :

stats http-requestacl を組み合わせればソース IP アドレスで制限できる。

frontend main *:80
    acl is_private src 192.168.33.0/24
    stats enable
    stats uri /admin?stats
    stats http-request allow if is_private
    :

stats admin で条件を指定すると、その条件に該当するときだけ管理レベルの操作が可能になる。通常は統計レポート画面はリードオンリーな操作しかできないが、この設定が有効だとバックエンドサーバのメンテナンスモードへの切り替えや、ヘルスチェックの有効/無効、などの操作が可能になる。

frontend main *:80
    acl is_private src 192.168.33.0/24
    stats enable
    stats uri /admin?stats
    stats http-request allow if is_private
    stats admin if LOCALHOST
    :

↑で指定した LOCALHOST は組み込みの acl で、自分で定義した acl も指定できる。

stats auth で統計レポートの画面に認証を設けることもできる。

frontend main *:80
    acl is_private src 192.168.33.0/24
    stats enable
    stats uri /admin?stats
    stats http-request allow if is_private
    stats admin if TRUE
    stats auth admin:AdMiN123
    :

CLI で統計レポートを表示

グローバルセクションで stats socket を指定すると Unix ドメインソケット経由で CLI で統計レポートを得ることができる。

global
    stats socket /var/lib/haproxy/stats level admin
    :

例えば、次のように nc とかで統計情報レポートを得ることができる。

echo "show stat -1 4 -1" | nc -U /var/lib/haproxy/stats | grep -v -E '^#|^$' | cut -d, -f2,18

level で指定している admin はソケット経由で操作可能なレベルを表していて、admin ならすべての操作が可能。

また、次のように指定すれば TCP でも接続できる。

global
    stats socket ipv4@127.0.0.1:9999 level admin
    :

monitor-uri / monitor fail

monitor failmonitor-uri を使うと、特定の URL へのリクエストを、条件によって 200 または 503 を返すようにすることができる。

frontend main *:80
    acl is_dead nbsrv(app) lt 2
    monitor fail if is_dead
    monitor-uri /haproxy?alive
    :

この例だと /haproxy?alive へリクエストしたときに app バックエンドの生きているサーバが 2 台以上あるなら 200 を、2 台未満なら 503 を返す。

URL にクエリストリングを含めているのには特に深い意味は無い、クエリストリング無しで /alive とかでも構わない。

monitor-net

monitor-net を使うと、特定のアドレス帯からのリクエストは即座に 200 を応答するようになる。

frontend main *:80
    monitor-net 192.168.33.0/24
    :

この例だと 192.168.33.0/24 からのリクエストは直ちに 200 を応答する。

バックエンドのサーバが全滅しても 200 を応答するけど・・どういう用途に使うものなの? これ。

fullconn / maxconn / minconn

backendfullconnservermaxconnminconn の関係。

http://permalink.gmane.org/gmane.comp.web.haproxy/5378

上記によると、例えば次のような設定だと、

backend dynamic
    fullconn 10000
    server srv1 dyn1:80 minconn 100 maxconn 1000
    server srv2 dyn2:80 minconn 100 maxconn 1000
    :

低負荷時には最大接続数の制限は minconn の 100 で、バックエンド全体に対する同時接続数が増えてくると最大接続数の制限も増加する。その増加は線形で、バックエンド全体に対する同時接続数が fullconn の 10000 になったときに maxconn の 1000 になるように増加する。ただし maxconn を超えることはない。

ニュースサイトとかでトラフィックパターンに大きな変動を伴うサイトでは静的な maxconn だとちょうど良い値を設定することができない(低負荷時は小さい値にしたいし、高負荷時は大きい値にしたい)。

fullconnminconn を使えばサイトの負荷に応じて動的に同時接続数の制限を変動することができる。

・・らしいんだけど maxconn だけじゃダメな理由が良くわからない。

maxconn の設定箇所ごとの意味

globalmaxconn はプロセスに適用されるもので、frontend の接続の合計。

default/frontend/listenmaxconnfrontend ごとに適用される。

frontend がもたらすものよりも大きい値を servermaxconn に設定した場合、キューが使われることがなくなるだけで、とくに問題ではない。

HTTP のモードの設定

下記を参考に・・・

https://blog.cloudpack.jp/2014/07/28/ha-proxy-translation-document-http-transaction/ https://blog.cloudpack.jp/2014/07/28/ha-proxy-translation-document-option-httpclose/ https://blog.cloudpack.jp/2014/08/08/tips-haproxy-option-httpclose-and-http-server-close-with-tcpdump/

http-tunnelhttpclose は非奨励のようなので、下記のいずれかを設定する。

  • option http-keep-alive
    • サーバ側とクライアント側の両方で keep-alive する
  • option http-server-close
    • 応答の終わりでサーバ側はクローズするがクライアント側は keep-alive する
  • option forceclose
    • 応答の終わりでクライアントとサーバの両方をクローズする

おおむね、次のようにしておくと良い。

  • option http-keep-alive
    • 静的コンテンツのサーバ
  • option http-server-close
    • アプリケーションサーバ

タイムアウト関係の設定

  • timeout connect
    • HAProxy からバックエンドサーバへの接続のタイムアウト時間
    • クライアントからの接続のプロキシ時とヘルスチェックでの両方で適用される
  • timeout client
    • クライアントから HAProxy への接続で無応答のタイムアウト時間
    • つまり、HAProxy がクライアントから ACK やデータ送信を期待したときに、どれだけ待つかの時間
    • なので、リクエスト〜レスポンスのトータル時間とは異なる
  • timeout server
    • HAProxy からバックエンドサーバへの接続で無応答のタイムアウト時間
    • timeout client のバックエンドサーバの版
    • なので、リクエスト〜レスポンスのトータル時間とは異なる
  • timeout http-request
    • 完全なリクエスト〜レスポンスのタムアウト時間
    • クライアントから最初のバイトを受信してからクライアントに最後のバイトを送信するまでの時間
  • timeout http-keep-alive
    • いわゆる keep-alive のタイムアウト時間
    • クライアントに最後のバイトを送信してから次のリクエストの最初のバイトを受信するまでの時間
  • timeout check
    • ヘルスチェックで接続が確立された後の追加のタイムアウト時間
    • ヘルスチェックでは接続のタイムアウト時間として timeout connectinter の小さい方が使用される
    • 接続が確立されると read のタイムアウト時間として timeout check が追加される
  • timeout queue
    • キューに入った接続を解放するまでのタイムアウト時間
    • HAProxy への接続が servermaxconn に達すると接続はキューに入って server の接続の空きを待つ
    • タイムアウト時間が経過するとクライアントには 503 が応答される

option redispatch

スティッキークッキーが有効な場合、振り分けられるバックエンドのサーバが死ぬと他のサーバが生きていても HAProxy が当該サーバをダウンと判断するまでクライアントはサービスにアクセスすることができなくなる。

下記のオプションを指定すると、そのようなケースでも別のサーバへの振り分けが行われる。

defaults
    option redispatch
    :

また、retries も指定されている場合、リトライの最後の施行だけ別のサーバへ振り分けられる。

defaults
    option redispatch
    retries 3
    :

option httplog

HTTP のアクセスログを記録する。

次のように指定すると CLF という Apache のアクセスログでお馴染みの標準的な形式のログになる。

defaults
    option httplog clf
    :

option dontlognull

いわゆる NULL スキャンのログを記録しないオプション、、、というわけではなくて、TCP で 80 番ポートを開いて直ぐ閉じるような、内容が HTTP ではないアクセスもログをログに記録しないようにするオプション。

nmap でいうところの -sT オプションのスキャンがログに残るかどうか、ということ(-sN はどっちにしろ残らない)。

例えば、死活監視でポートが開いているかを監視するために bash で < /dev/tcp/127.0.0.1/80 みたいなことをやっていると、通常はこのアクセスまでログに記録されてしまうが、option dontlognull を指定しておけば、このようなアクセスはログには残らなくなる。

インターネットのような制御されていない環境(uncontrolled environments)では指定しないことをおすすめする。

forwardfor

次のように指定すると X-Forwarded-ForX-Original-To ヘッダがバックエンドサーバへのリクエストに追加される。

defaults
    option forwardfor
    option originalto
    :

既に X-Forwarded-ForX-Original-To が付与されていた場合(HAProxy へのリクエストの時点でこれらのヘッダが付いていた場合)、カンマ区切りで追記される。

次のように except を追加すると、そのネットワークからのリクエストでは X-Forwarded-ForX-Original-To を追加しなくなる。

defaults
    option forwardfor except 127.0.0.0/8
    option originalto except 127.0.0.0/8
    :

例えば、HAProxy へのアクセスが、クライアントから直接と、既知のリバースプロキシ経由との 2 つが混在するような場合に、except でリバースプロキシを指定すれば、既知のアドレスが X-Forwarded-For に追加されないようにすることができる。

他にも、HAProxy のホスト上から http://localhost/ などとアクセスすると X-Forwarded-For127.0.0.1 になり、バックエンドサーバでこの値をアクセスログに記録している場合、まるで意味のないログになってしまう。そこで、except 127.0.0.0/8 を指定すれば X-Forwarded-For が記録されなくなるので、Web サーバで X-Forwarded-For がなければ通常のリモートアドレスにフォールバックするようになっていれば、アクセスログには Web サーバから見た HAProxy のアドレスが記録されるようになる・・・という使い方もできるかもしれない。

option logasap

デフォルトではログはレスポンスの完了時に記録されるが(じゃないと総転送量とかがわからない)、次のように設定するとレスポンスヘッダが送信された時点でアクセスログが記録されるようになる。

defaults
    option logasap
    :

ただ、その場合、ログに記録される転送量や転送時間は不完全なものになる。実際に見比べた感じ、このオプションがなければ 688 のような具体的な値が記録されるのに対して、このオプションを指定していると +445 のように記録される。

option log-health-checks

デフォルトではヘルスチェックのログは risefall で指定された回数のリクエストの結果を元にステータスが変化したときにだけ記録されるが、次の設定をすると、ステータスが変化する前のヘルスチェックの結果もログに記録される。

defaults
    option log-health-checks
    :

例えば、ヘルスチェックの設定が次のようになっているとき、

defaults
    default-server inter 2s rise 2 fall 3
    :

ステータスが OK の状態でヘルスチェックに失敗したとき、上記のオプションが指定されていなければ 3 回失敗して初めてログに記録されるが、上記のオプションが指定されている場合は最初の 1 回目からログに記録され、3 回ログに記録された後にステータスが変わった旨のログが記録される。

agent-check

バックエンドサーバの Up/Down や重みをサーバの特定のポートに TCP でアクセスして返ってきた値に応じて行なう。

次のように設定すると、サーバの 1234 ポートに定期的にアクセスする。

backend app
    balance roundrobin

    default-server inter 2s rise 2 fall 3 agent-port 1234 agent-inter 3s

    server ap01 192.168.33.21:80 check agent-check
    server ap02 192.168.33.22:80 check agent-check

    option httpchk GET /ok.html
    http-check expect status 200

そのポートが 80% のような値を返すと、振り分けの重みが 80% になる。

他にも、次のような値を返すことができる。

  • サーバの Up/Down の状態
    • up
    • down または failed または stopped
  • サーバの管理状態
    • ready
    • maint
    • drain
  • 重み
    • 80% など

ポートが返さなかった値は変更されない。つまり 80% とだけ返した場合は重みだけが変更されて、Up/Down やメンテナンスなどの状態は変更されない。

また、ポートが応答がなかった場合でも Down になるわけではないので(ステータスが更新されないだけ)、option httpchk と併用はしなければならない。

READY/MAINT/DRAIN

agent-check の説明ででてきた READY/MAINT/DRAIN はバックエンドサーバの管理状態を意味するもので、それぞれ次のような意味。

  • READY
    • MAINT や DRAIN が解除された通常の状態
  • DRAIN
    • 新しい接続を受け入れない
  • MAINT
    • 新しい接続を受け入れず、かつ、死活監視も停止する

動的なサーバの取り外し

稼働中の HAProxy から一時的に特定のバックエンドサーバへの振り分けを停止する方法。

WEB での統計レポートの管理モードを有効にして、特定のサーバを DRAIN や MAINT に切り替えることで実現できる。

もしくは、CLI での統計レポートの管理モードが有効になっていれば、次のようなコマンドで切り替えることができる。

# MAINT にする
echo "disable server app/ap01" | nc -U /var/lib/haproxy/stats

# MAINT から元に戻す
echo "enable server app/ap01" | nc -U /var/lib/haproxy/stats
# DRAIN
echo "set weight app/ap01 0%" | nc -U /var/lib/haproxy/stats

# DRAIN から元に戻す
echo "set weight app/ap01 100%" | nc -U /var/lib/haproxy/stats

サービスダウンのログ

バックエンドサーバが全滅すると次のようなログが発生する。

Server app/ap02 is DOWN, reason: Layer7 wrong status, code: 404, info: "HTTP status check returned code <3C>404<3E>", check duration: 0ms. 0 active and 0 backup servers left. 0 sessions active, 0 requeued, 0 remaining in queue.
backend app has no server available!

1個目のログは alert だけど2個目のログは emerg で発生する。

emerg のログは rsyslog の設定に依らず(というか rsyslog を停止しても)コンソールにブロードキャストされる。

Broadcast message from systemd-journald@ha01 (Tue 2016-04-19 14:04:43 JST):

haproxy[7758]: backend app has no server available!

見ての通り systemd-journald が出しているもので、/etc/systemd/journald.confMaxLevelWall のデフォルトが emerg だからなのだと思われる。

参考

rsyslog のメモ

rsyslogd - reliable and extended syslogd

信頼できる拡張された syslogd

Remote syslogd ではない(syslogd 時代からリモートからの受信は可能)

設定ファイル

設定ファイルは /etc/rsyslog.conf ですが、このファイルで下記の通りに記述されているので、

$IncludeConfig /etc/rsyslog.d/*.conf

/etc/rsyslog.d/ の拡張子 .conf のファイルもマージされます。

/dev/log

/dev/log という Unix ドメインソケットに書き込めばログを送ることができます。

echo "<133>$(LANG=C date '+%b %d %H:%M:%S') oreore: XXX" | nc -Uu /dev/log

CentOS 6 の nc だと -u と -U を同時に指定できなかったので、代わりに socat で送れます。

echo "<133>$(LANG=C date '+%b %d %H:%M:%S') oreore: XXX" | socat STDIN UNIX-SENDTO:/dev/log

<133> の部分はファシリティとプライオリティで、次の計算式で求めます。

facility * 8 + severity

例えば、local0.notice だと次のとおりです。

local0(16) * 8 + info(5) =  133

日時の部分を改ざんしても、実際にログを送信した日時が記録されました。

echo "<133>Apr 01 06:12:34 oreore: uso" | nc -Uu /dev/log
sudo tail -1 /var/log/messages
# Apr 14 09:15:48 ore-no-server oreore: uso
LANG=C date '+%b %d %H:%M:%S'
# Apr 14 09:15:48

ただまあ、普通はこんなことせずに logger コマンドを使います。

logger -p local0.notice -t oreore -i message
sudo tail -1 /var/log/messages
# Apr 14 09:15:48 ore-no-server oreore[1234]: message

なお、CentOS 7 だと実は /dev/log は journald がリッスンしていて、rsyslog は journald からログを取得しています。

UDP

デフォでは UDP でリッスンしていないので /etc/rsyslog.conf の下記をコメントインするか、

$ModLoad imudp
$UDPServerRun 514

/etc/rsyslog.d/udp.conf のような名前で下記のファイルを作成する必要があります。

$ModLoad imudp
$UDPServerRun 514

UDP で送る場合は次の形式です。

echo "<133>$(LANG=C date '+%b %d %H:%M:%S') $(hostname -s) oreore: XXX" | nc -u localhost 514

/dev/log とは異なり、日時を改ざんするとその通りにログが記録されます。

echo "<133>Apr 01 06:12:34 ore-no-server oreore: uso" | nc -u localhost 514
sudo tail -1 /var/log/messages
# Apr  1 06:12:34 ore-no-server oreore: uso
LANG=C date '+%b %d %H:%M:%S'
# Apr 14 09:16:19

journald

systemd から起動するサービスが標準入力や標準出力に書き込むと journald を経由して rsyslog にも出力されます。

プロパティベースのフィルター

rsyslog でログファイルを分けたい場合、設定ファイルで昔ながらの次のようなフィルターが使えますが、

# local1 のログを出さない
*.info;mail.none;authpriv.none;cron.none;local1.none    /var/log/messages

# local1 のログは oreore.log に出す
local1.*                                                /var/log/oreore.log

次のような、プロパティベースのフィルターも使用できます。

/etc/rsyslog.d/oreore.conf

:programname, isequal, "oreore" /var/log/oreore.log

この例だと programnameoreore であるログは /var/log/oreore.log に出力されます。

:programname というのは下記のログの oreore の部分です。

Apr 14 09:15:48 hostname oreore[1234]: message

& は、直前のパターンにマッチしたもの、という意味です。また、ログファイル名に ~ を指定するとログは破棄されます。

なので、次のように指定すると、oreore のログは /var/log/oreore.log に書き込まれて破棄されます(/var/log/messages)には書かれない。

:programname, isequal, "oreore" /var/log/oreore.log
& ~

なお、最近の rsyslog だと ~ を使うと警告が表示されるので代わりに stop を使うのが正しいようです。

:programname, isequal, "oreore" /var/log/oreore.log
& stop

なお :programname は CentOS 6 の rsyslog では使えませんでした、代わりに次のようにすると良いでしょう。

:syslogtag, startswith, "oreore" /var/log/oreore.log
& stop

:syslogtag というのは下記のようなログの oreore[1234] の部分です。

Apr 14 09:15:48 hostname oreore[1234]: message

式ベースのフィルター

式ベースのフィルターというものを使えばもっと複雑なこともできます。

if $programname == 'prog1' then {
   action(type="omfile" file="/var/log/prog1.log")
   if $msg contains 'test' then
     action(type="omfile" file="/var/log/prog1test.log")
   else
     action(type="omfile" file="/var/log/prog1notest.log")
}

が、そこまでやることってあるのだろうか・・・

テンプレート

テンプレート機能を使えばログファイルの形式をカスタマイズできます。

次のようにテンプレートを登録します。

$template oretemplate, "%timestamp% %hostname% %syslogtag% <%syslogfacility-text%.%syslogseverity-text%> %msg%\n"

ログファイル名を指定するときにテンプレート名も一緒に指定します。

:programname, isequal, "oreore" /var/log/oreore.log;oretemplate
& stop

すると、ログに次のようにファシリティとプライオリティも記録されるようになります。

Apr 14 09:15:48 hostname oreore[1234]: <local0.notice> message

参考

Logwatch で独自の通知を追加するメモ

Logwatch とは

  • Logwatch
  • 日次でシステム内のさまざまなログから見つかった問題を通知する
    • /etc/cron.daily/0logwatch で実行される
  • デフォだと root へのメールで通知される
  • デフォでいろいろなログの通知が組み込まれている
    • ls /usr/share/logwatch/scripts/services/

独自の通知の追加

例えば oreore というサービスのログを追加することにします。

ログファイルの設定

oreore のログファイルに関する設定を追加します。

/etc/logwatch/conf/logfiles/oreore.conf

LogFile = oreore.log
LogFile = oreore.log-*[0-9]
Archive = oreore.log-*.gz

LogFile にはログファイル名を glob 形式で指定します。パスは /var/log/ からの相対です。このディレクトリ以外のログを指定したい場合は絶対パスで指定します。

Archive には gzip でアーカイブされたログファイルを指定します。ただ、この設定は普段は使われません(logwatch の実行時のオプションでアーカイブされたログも走査するかのオプションがあるけどデフォで指定されていない)。

また、通常はこの設定ファイルでログファイルを日付でフィルタするための設定を記述します。例えば syslog 形式のログであれば次のように記述すれば日付でフィルタされます。

*ApplyStdDate

この記述によって /usr/share/logwatch/scripts/shared/applystddate にログファイルがパイプされるようになります。

他にも /usr/share/logwatch/scripts/shared/ ディレクトリにあるスクリプトを指定することができます。ログファイルに独自の処理を行いたい場合は下記のディレクトリにスクリプトを配置します。

  • /etc/logwatch/scripts/logfiles/oreore/

このディレクトリに配置したスクリプトは oreore のログの走査時に自動的に適用されます。

例えば次のような内容で作成します。

/etc/logwatch/scripts/logfiles/oreore/applydate

use Logwatch ':dates';

my $Debug = $ENV{'LOGWATCH_DEBUG'} || 0;

my $SearchDate = TimeFilter('\[%Y-%m-%dT%H:%M:%S\]');

if ( $Debug > 5 ) {
    print STDERR "DEBUG: Inside oreore..\n";
    print STDERR "DEBUG: Looking For: (" . $SearchDate . ")\n";
}

while (defined($ThisLine = <STDIN>)) {
    if ($ThisLine =~ s/^$SearchDate\s+//o) {
        print $ThisLine;
    }
}

既存のスクリプトからほとんどコピペです。Logwatch モジュールの TimeFilter を使えば日時でフィルタするための正規表現を簡単に作ることができます。例えば↑の例だと $SearchDate\[2016-04-14T..:..:..\] のようになります。

この例では perl でスクリプトを作成していますが、シェバングを書けばそのインタプリタで実行されるので、好きな言語で作成することができます。

サービスの設定

oreore のサービスに関する設定を追加します。

/etc/logwatch/conf/services/oreore.conf

Title = "oreore report"
LogFile = oreore

Title は通知のレポートで使われるサービスの名称です。

LogFile には↑で設定したログファイルの設定名を指定します。この例ではログファイルとサービスで同じ oreore という設定名ですが、例えばこのサービスが /var/log/messages にログを出すようになっているのなら LogFile = messages と指定したりすることもあります。

また、ログファイルの設定と同じように、フィルタを下記のように記述することができます。

*Remove = "^(DEBUG|INFO):"

指定できるものはログファイルの設定と同じです(/usr/share/logwatch/scripts/shared/ のスクリプト)。

最後に、レポートを出力するためのスクリプトを /etc/logwatch/scripts/services/oreore に作成します。ログファイルの設定やサービスの設定によってフィルタされたログが標準入力から入ってくるので、それっぽい出力にしてやれば OK です。

例えば、単にそのまま流すだけですが、次のような内容です。

/etc/logwatch/scripts/services/oreore

while (defined($ThisLine = <STDIN>)) {
    print $ThisLine;
}

もちろん perl 以外でも作成できるので、そのまま流すだけなら下記で十分です。

#!/bin/bash
cat

環境変数

ログファイルやサービスの設定ファイルで次のように変数を記述すると、

$oreore_value = 123

スクリプトで環境変数として取り出せます。

my $oreore_value = $ENV{'oreore_value'} || 0;

変数名に大文字を用いてもすべて小文字に置換されます。

ログファイルの設定ファイルで記述した変数は、ログファイルとサービスの両方のスクリプトで有効になります。ただ、見た感じ既存のログファイルの設定ファイルで変数が記述されているものはありませんでした。

サービスの設定ファイルで記述した変数は、サービスのスクリプトのみで有効になります。

動作確認

次のように環境変数を設定しつつログファイルからスクリプトをパイプしていくと最終的な出力が得られます。

export LANG=C PERL5LIB=/usr/share/logwatch/lib/ LOGWATCH_DEBUG=9 LOGWATCH_DATE_RANGE=today
sudo cat /var/log/oreore.log |
  perl /etc/logwatch/scripts/logfiles/oreore/applydate |
  perl /usr/share/logwatch/scripts/shared/remove "^(DEBUG|INFO):" |
  perl /etc/logwatch/scripts/services/oreore

切り貼りすれば途中までの結果を出力したりもできるのでデバッグが捗ります。

最終的な動作確認には 次のように logwatch を実行します(CentOS 7)。

sudo logwatch --service oreore --output stdout --range today

CentOS 6 だと --output オプションが無くて、代わりに --print を使います。

sudo logwatch --service oreore --print--range today

フィルタ

ログファイルやサービスの設定で指定可能なフィルタ(/usr/share/logwatch/scripts/shared/ のスクリプト)には以下のようなものがあります。

  • applybinddate
  • applyeurodate
  • applyhttpdate
  • applystddate
  • applytaidate
  • applyusdate
  • applyvsftpddate
    • 日付でフィルタ(ログファイルの形式により様々)
  • eventlogonlyservice
  • eventlogremoveservice
    • 謎・・MSWinEventLog とかいう記述が見えるけど
  • expandrepeats
    • last message repeated 10 times のように省略されたログを展開
  • hosthash
    • 謎・・syslog 形式のログからホスト名の一覧を出力するっぽいけど
  • hostlist
    • 謎・・syslog 形式のログからホスト名の一覧をテンポラリに書き出すっぽいけど
  • multiservice
    • syslog 形式のログで特定のサービスの以外の行を除去
    • *RemoveService = ntpd,ntpdate のように引数をカンマ区切りで指定する
  • onlycontains
    • 指定されたパターンにマッチする行のみを抽出
    • *OnlyContains = ^(ERROR|WARN): のように正規表現で指定する
  • onlyhost
    • syslog 形式のログで特定のホストの行のみを抽出
    • ホスト名は logwatch.confHostLimit または引数で指定する
    • logwatch.conf で指定するときはカンマ区切りまたは正規表現で指定する
      • カンマの有無で意味が変わる
    • 引数で指定するときは正規表現で指定する
  • onlyservice
    • syslog 形式のログで特定のサービスの行のみを抽出
    • *OnlyService = (spamd|sendmail) のように引数を正規表現で指定する
  • remove
    • 指定されたパターンにマッチする行を除去する
    • *remove = ^(DEBUG|INFO): のように正規表現で指定する
  • removeheaders
    • syslog 形式のログのメッセージ以外の部分を削除
    • Apr 13 23:20:01 localhost systemd: hogehoge -> hogehoge
  • removeservice
    • syslog 形式のログで特定のサービスの行を除去
    • *RemoveService = anacron,cron のように引数をカンマ区切りで指定する

詳しいことは less /usr/share/doc/logwatch-*/HOWTO-Customize-LogWatch で。

参考

全部自分の記事だけど・・