HAProxyを使い始めてみる

HAProxy[^ha]とは何か？ から始まり、基本的な使い方までを調べました。
[^ha]: HAってやっぱりHigh Availabilityなのかなあ。たぶんそうなんでしょう。

## HAProxyとは
多機能なプロキシサーバです。ソフトウェアロードバランサの一種でもあります。古くから開発が続けられ、非常に高速かつ堅牢で信頼性が高いことを売りにしているようです。

HAProxyが何であって何でないかは、 [ドキュメントに箇条書きで記載](http://cbonte.github.io/haproxy-dconv/intro-1.6.html#3.1) されています。
ざっくりまとめると、こんな感じのことが書かれています。

- TCP接続のプロキシとして動作させ、アクセス経路を設定することができる
- HTTPリバースプロキシとしての機能を持ち、ルールに従ってリクエストを別のサーバに渡すことができる
  - このとき、URLやヘッダの書き換えも行える
  - SSLやHTTP圧縮を肩代わりできる
- TCP/HTTPの正規化器(normalizer)となる
  - 不正なトラフィックからの防護を提供する
- 負荷分散機能を提供する
- ロギング機能を持つので、ネットワーク上の観測点（an observation point）として機能させることができる
- トラフィックのコントロールができる
  - 例えば、同時接続数の制限
  - 例えば、IPベースのフィルタリング

一方で以下のような機能を持たない、とされています[^others]。

- HTTPプロキシではない。つまりインターネットへのアクセスするブラウザがアクセスするような性質のものではない
- キャッシュもしない
- bodyの書き換えもしない
- Webサーバではない
- IPやUDPといったパケットレベルのレイヤは見ない

[^others]: これらについては他に優れたソフトウェアがある、とのこと。例えば、SquidやVarnishが例示されています。

## インストール
CentOSの場合、yumで入れられます。ただし、CentOS7であっても、執筆時点での最新安定版である1.6は入らないので、最新版が欲しければソースからビルドする必要があるかもしれません。

```
$ sudo yum install -y haproxy
```

## 設定の方法
設定は`haproxy.cfg`に記述します。yumからインストールしたCentOSの場合は`/etc/haproxy`にあります。

例として設定ファイルは以下のような構造になっています。この例は[設定ドキュメント](http://cbonte.github.io/haproxy-dconv/configuration-1.6.html#2.4)から転載しました。

```text:haproxy.cfg
    # Simple configuration for an HTTP proxy listening on port 80 on all
    # interfaces and forwarding requests to a single backend "servers" with a
    # single server "server1" listening on 127.0.0.1:8000
    global
        daemon
        maxconn 256

    defaults
        mode http
        timeout connect 5000ms
        timeout client 50000ms
        timeout server 50000ms

    frontend http-in
        bind *:80
        default_backend servers

    backend servers
        server server1 127.0.0.1:8000 maxconn 32


    # The same configuration defined with a single listen block. Shorter but
    # less expressive, especially in HTTP mode.
    global
        daemon
        maxconn 256

    defaults
        mode http
        timeout connect 5000ms
        timeout client 50000ms
        timeout server 50000ms

    listen http-in
        bind *:80
        server server1 127.0.0.1:8000 maxconn 32
```

設定ファイルはいくつかのセクションに分かれています。大きく分けて、HAProxyのプロセスとしての動作や、各種チューニングパラメータについての設定をする`global`セクションと、プロキシとしての動作の設定をするプロキシセクションがあります。

プロキシセクションはさらに`defaults`、`frontend`、`backend`、`listen`に細分化されていることが分かると思います。次のような役割の違いがあるので、それに従って書き分けます。

- `frontend`には、プロキシとしてアクセスを受ける側についての設定をします
- `backend`には、プロキシとしてアクセスを振り分ける先のサーバについての設定をします
- `listen`には、フロントエンドとバックエンドを合わせて一つのプロキシ設定にしたものを、まとめ書きすることができます
- `defaults`に書いた設定は、それに続くセクション全てで有効になります。つまり共通する設定を`defaults`にまとめることができます
  - `defaults`によって、そこまで有効だったデフォルトパラメータはリセットされます

## とりあえずログを取れるようにする
yumでインストールした直後は、起動させることはできてもログが出ません。とりあえずログが見えるようにしました。

ロギングの方法は複数提供されていますが、一番シンプルなのはsyslogで渡す方法です。HAProxy側の設定項目として`log`パラメータを設定します。
http://cbonte.github.io/haproxy-dconv/configuration-1.6.html#3.1-log

```text:haproxy.cfg
global
    log  127.0.0.1 local2
```
これでローカルホストのUDPポート514を利用してログを送るようになります。2番目のパラメータであるファシリティは、ここではインストール時のテンプレートにならって、`local2`をそのまま利用します。

ログを受け付けられるように、rsyslogの設定も変更してUDPを有効化しておきます。

```text:/etc/rsyslog.conf
# Provides UDP syslog reception
$ModLoad imudp
$UDPServerRun 514
```

ファシリティ`local2`のログ出力を`/var/log/haproxy.log`に向けます[^logging]。

[^logging]: 単に`local2.* /var/log/haproxy.log`とだけ書いてしまうと、動作確認中、バックエンドが全台落ちているときに出るemergログが、rsyslogデフォルト設定の`*.emerg *`に引っかかってしまい、コンソールがややうるさくなるという問題が発生しました。この記事では動作確認のためと割り切り、info以上のログのみファイル出力し、他は捨てるようにしています。

```
$ sudo touch /etc/rsyslog.d/haproxy.conf
```

```text:/etc/rsyslog.d/haproxy.conf
local2.info                       /var/log/haproxy.log
local2.* ~
```

設定を反映するために、rsyslogとhaproxyをrestartします。

```
$ sudo service rsyslog restart
$ sudo service haproxy restart
```

`/var/log/haproxy.log`が作成されログが出始めたら成功です。

## 基本的なロードバランス機能の確認
ロードバランス機能を設定することで動作確認をします。ここでは、Sinatraを用いた簡易サーバを用意しました。ロードバランスの状況を分かりやすくするために、プロセスIDを出力するようにしてあります。

```rb:serve.rb
require 'sinatra'

get '/' do
  "Hello HAProxy!! pid=#{Process.pid}\n"
end
```

HAProxy側の設定としては、以下のようなものを用意しました。デフォルトのテンプレートを軽く修正したものです。

```text:haproxy.cfg

global
    log         127.0.0.1 local2
    chroot      /var/lib/haproxy
    pidfile     /var/run/haproxy.pid
    maxconn     4000
    user        haproxy
    group       haproxy
    daemon

defaults
    mode                    http
    log                     global
    option                  httplog
    timeout connect         10s
    timeout client          1m
    timeout server          1m

frontend  main *:5000
    default_backend             app

backend app
    balance     roundrobin
    server  app1 127.0.0.1:5001 check
    server  app2 127.0.0.1:5002 check
```

この設定では、フロントエンドがポート5000での待ち受けをし、ラウンドロビンに基づいてローカルホストのポート5001と5002にアクセスを割り振ります。ちなみに`timeout`の3つを消してしまうとwarningが出てしまいました。理由はよく分かっていません……。

```
$ ruby serve.rb -p 5001 2>/dev/null &
[1] 15176
$ ruby serve.rb -p 5002 2>/dev/null &
[2] 15220
$ curl localhost:5000/                              
Hello HAProxy!! pid=15176
$ curl localhost:5000/
Hello HAProxy!! pid=15220
$ curl localhost:5000/                              
Hello HAProxy!! pid=15176
$ curl localhost:5000/
Hello HAProxy!! pid=15220
```
ラウンドロビンしていることが確認できます。片側を落としてさらにcurlで叩いてみます。

```
$ kill 15176
$ curl localhost:5000/
Hello HAProxy!! pid=15176
[1]  - 15176 done       ruby serve.rb -p 5001 2> /dev/null
$ curl localhost:5000/
Hello HAProxy!! pid=15220
$ curl localhost:5000/
<html><body><h1>503 Service Unavailable</h1>
No server is available to handle this request.
</body></html>
$ curl localhost:5000/
Hello HAProxy!! pid=15220
$ curl localhost:5000/
Hello HAProxy!! pid=15220
```
何か見えてはいけないものが一瞬見えていますが、とはいえ、ちゃんと停止したサーバの切り離しができている様子です。この辺りの微妙な挙動は要検証でしょうか。パラメータもたくさんあるし……。

## まとめ
HAProxyの概要・設定の書き方・基本的な設定と動作確認をしてみる所までを調べてまとめました。

HAProxyはものすごく多機能です。この記事でまとめた機能はほんの一部にすぎません。例えばACL、HTTPヘッダのrewrite、stick-tablesなどには折を見て触れてみたいところではあります。

## 参考資料
ドキュメント類は最新の1.6系のものを参照しました。記事で動作させてみたのは1.5系なのですが、基本機能ではそこまで違いはないだろうし……？

- [HAProxy Starter Guide version 1.6.0](http://cbonte.github.io/haproxy-dconv/intro-1.6.html)
- [HAProxy Configuration Manual version 1.6.0](http://cbonte.github.io/haproxy-dconv/configuration-1.6.html)