share facebook facebook2 twitter menu hatena pocket slack

2015.01.14 WED

HAProxy の acl によるバックエンドへの振り分けを色々と試す(1) (「User-Agent と接続元 IP」という条件でバックエンドへの振り分けをコントロールする)

川原 洋平

WRITTEN BY川原 洋平

ども、cloudpackかっぱ (@inokara) です。

はじめに

HAProxy で User-Agent やリクエストパス等でバックエンドに対する振り分けを行う際に利用する use_backend で複数条件が利用出来るか試してみた。ドキュメントを改めてみると何でも出来そうな雰囲気が HAProxy の底力を魅せつけられたような気がする。

参考

解ったこと

とりあえず検証結果が長くなってしまったので結論から。

  • use_backend で複数の条件を定義することが可能
  • acl で定義した条件をそのまま並べると and 条件となる
  • and の代わりに or を付けることも可能(どちらかにマッチしたらという条件に代わる)
  • and 条件を or で並記することが可能
  • HAProxy とは関係ないけど Sinatra で複数のパス指定は sinatra/multi_routerequire すると良い

俺の条件

以下のような条件で試す。

パターン(1)(or)

  • User-Agent が Firefox か、リクエストパスが /firefox であれば Firefox 用サイトに飛ばす
  • User-Agent が Chrome か、リクエストパスが /chrome であれば Chrome 用サイトに飛ばす
  • 上記以外はデフォルトのバックエンドに飛ばす

パターン(2)(and)

  • User-Agent が Firefox で且つ、リクエストパスが /firefox であれば Firefox 用サイトに飛ばす
  • User-Agent が Chrome で且つ、リクエストパスが /chorme であれば Chrome 用サイトに飛ばす
  • 上記以外はデフォルトのバックエンドに飛ばす

パターン(3)(接続元 and User-Agent)

  • User-Agent が Firefox で且つ、接続元が xxx.xxx.xxx.xxx/32 であれば Firefox 用サイトに飛ばす
  • User-Agent が Chrome で且つ、接続元が yyy.yyy.yyy.yyy/32 であれば Chrome 用サイトに飛ばす

パターン(4)(接続元(X-Forwarded-for) and User-Agent)

  • User-Agent が Firefox で且つ、接続元が xxx.xxx.xxx.xxx/32 であれば Firefox 用サイトに飛ばす
  • User-Agent が Chrome で且つ、接続元が yyy.yyy.yyy.yyy/32 であれば Chrome 用サイトに飛ばす

Firefox 用サイト

require 'sinatra'
require 'sinatra/reloader'
require "sinatra/multi_route"

get '/', '/firefox' do
  "firefox<br/>""#{request.path_info}""n"
end

Chrome 用サイト

require 'sinatra'
require 'sinatra/reloader'
require "sinatra/multi_route"

get '/', '/firefox' do
  "chrome<br/>""#{request.path_info}""n"
end

HAProxy の設定

パターン(1)(or)

(snip)

frontend balancer-test
  option forwardfor
  bind 0.0.0.0:8000
  acl foo path_beg /firefox
  acl bar path_beg /chrome
  acl crm hdr_sub(User-Agent) -i Chrome
  acl fox hdr_sub(User-Agent) -i Firefox
  use_backend hoge_bk if foo or fox
  use_backend fuga_bk if bar or crm
  default_backend aa

backend hoge_bk
  option forwardfor
  server web01 127.0.0.1:8081 check

backend fuga_bk
  option forwardfor
  server web02 127.0.0.1:8082 check

backend aa
  server web03 127.0.0.1:80

パターン(2)(and)

use_backend にて acl で定義した条件を並べると and 条件となる。

(snip)

frontend balancer-test
  option forwardfor
  bind 0.0.0.0:8000
  acl foo path_beg /firefox
  acl bar path_beg /chrome
  acl crm hdr_sub(User-Agent) -i Chrome
  acl fox hdr_sub(User-Agent) -i Firefox
  # condition foo and fox
  use_backend hoge_bk if foo fox
  # condition bar and crm
  use_backend fuga_bk if bar crm
  default_backend aa

backend hoge_bk
  option forwardfor
  server web01 127.0.0.1:8081 check

backend fuga_bk
  option forwardfor
  server web02 127.0.0.1:8082 check

backend aa
  server web03 127.0.0.1:80

パターン(3)(接続元 and User-Agent)

use_backend にて acl で定義した条件を並べると and 条件となる。

(snip)

frontend balancer-test
  option forwardfor
  bind 0.0.0.0:8000
  #
  acl crm hdr_sub(User-Agent) -i Chrome
  acl fox hdr_sub(User-Agent) -i Firefox
  # IP アドレスは CIDR 表記が可能
  acl hoge src xxx.xxx.xxx.xxx/32
  acl fuga src yyy.yyy.yyy.yyy/32
  #
  use_backend firefox_bk if fox hoge
  use_backend chrome_bk if crm fuga
  default_backend aa

backend hoge_bk
  option forwardfor
  server web01 127.0.0.1:8081 check

backend fuga_bk
  option forwardfor
  server web02 127.0.0.1:8082 check

backend aa
  server web03 127.0.0.1:80

単純に接続元 IP を指定する場合には src を利用するが、X-Forwarded-for がヘッダにくっついてくる場合には以下のように acl を設定する必要がある。

acl hoge_x hdr_ip(X-Forwarded-For) xxx.xxx.xxx.xxx/32

パターン(4)(接続元(X-Forwarded-for) and User-Agent)

HAProxy の前面に ELB を配置したのでクライアントからは ELB のエンドポイントにアクセスすることになる。

frontend balancer-test
  option forwardfor
  bind 0.0.0.0:8000
  #
  acl foo path_beg /firefox
  acl bar path_beg /chrome
  acl crm hdr_sub(User-Agent) -i Chrome
  acl fox hdr_sub(User-Agent) -i Firefox
  acl hoge src xxx.xxx.xxx.xxx/32
  acl fuga src yyy.yyy.yyy.yyy/32
  acl hoge_x req.hdr_ip(X-Forwarded-For) xxx.xxx.xxx.xxx/32
  acl fuga_x req.hdr_ip(X-Forwarded-For) yyy.yyy.yyy.yyy/32
  #
  use_backend firefox_bk if fox hoge or fox hoge_x
  use_backend chrome_bk if crm fuga or crm fuga_x
  default_backend aa

backend firefox_bk
  option forwardfor
  server web01 127.0.0.1:8001 check

backend chrome_bk
  option forwardfor
  server web02 127.0.0.1:8002 check

backend aa
  server web03 127.0.0.1:80

以下のような条件でバックエンドへの振分を行う。

use_backend firefox_bk if fox hoge or fox hoge_x は以下の条件が含まれる。

  • User-Agent に Firefox が含まれており且つ接続元が xxx.xxx.xxx.xxx/32 の場合には firefox_bk に振り分ける
  • User-Agent に Firefox が含まれており且つ X-Forwarded-Forxxx.xxx.xxx.xxx/32 の場合には firefox_bk に振り分ける

use_backend chrome_bk if crm fuga or crm fuga_x は以下の条件が含まれる。

  • User-Agent に Chrome が含まれており且つ接続元が yyy.yyy.yyy.yyy/32 の場合には chrome_bk に振り分ける
  • User-Agent に Chrome が含まれており且つ X-Forwarded-Foryyy.yyy.yyy.yyy/32 の場合には chrome_bk に振り分ける

また、バックエンドで利用するスクリプトを以下のように用意した。

firefox_bk 用。

<?php
  $headers = getallheaders();
  print("Firefox" . "n");
  print("X-Forwarded-For: " . $headers["X-Forwarded-For"] . "n");
  print("User-Agent: " . $headers["User-Agent"] . "n");

chrome_bk 用。

<?php
  $headers = getallheaders();
  print("Chrome" . "n");
  print("X-Forwarded-For: " . $headers["X-Forwarded-For"] . "n");
  print("User-Agent: " . $headers["User-Agent"] . "n");

default_backend 用。

<?php
  $headers = getallheaders();
  print("Chrome" . "n");
  print("X-Forwarded-For: " . $headers["X-Forwarded-For"] . "n");
  print("User-Agent: " . $headers["User-Agent"] . "n");

試した

パターン(1)(or)

Chrome で / にアクセス。
HAProxy の acl によるバックエンドへの振り分けを試す(1): パターン(1)(or)(Chrome: /)

Chrome で /chrome にアクセス。
HAProxy の acl によるバックエンドへの振り分けを試す(1): パターン(1)(or)(Chrome: /chrome)

Firefox で/にアクセス。
HAProxy の acl によるバックエンドへの振り分けを試す(1): パターン(1)(or)(Firefox: /)

Firefox で /firefox にアクセス。
HAProxy の acl によるバックエンドへの振り分けを試す(1): パターン(1)(or)(Firefox: /firefox)

Chrome で /firefox にアクセス(上)、そして Firefox で /chrome にアクセス(下)。
HAProxy の acl によるバックエンドへの振り分けを試す(1): パターン(1)(or)(Chrome: /firefox)

以下、メモ。

  • Chrome で /firefox にアクセス出来るのは use_backend hoge_bk if foo or foxor 条件となっているから
  • Firefox で /chrome にアクセスした場合には use_backend hoge_bk if foo or fox の条件にマッチしているものの Firefox 用サイトに /chrome というパスが定義されていない為に Sinatra の 404 エラーとなっている

まとめると…

  • or で複数の条件を並べることが可能
  • 複数の条件がある場合は条件にマッチした時点で条件判断処理は終了

パターン(2)(and)

Chrome で / にアクセス。
HAProxy の acl によるバックエンドへの振り分けを試す(1): パターン(2)(and)(Chrome: /)
and 条件となるので User-Agent とリクエストパスが両方マッチする必要があるので default_backend が利用される。

Chrome で /chrome にアクセス。
HAProxy の acl によるバックエンドへの振り分けを試す(1): パターン(2)(and)(Chrome: /chrome)
User-Agent とリクエストパスの両方がマッチしたので表示される。

Firefox で / にアクセス。
HAProxy の acl によるバックエンドへの振り分けを試す(1): パターン(2)(and)(Firefox: /)
and 条件となるので User-Agent とリクエストパスが両方マッチする必要があるので default_backend が利用される。

Firefox で /firefox にアクセス。
HAProxy の acl によるバックエンドへの振り分けを試す(1): パターン(2)(and)(Firefox: /firefox)
User-Agent とリクエストパスの両方がマッチしたので表示される。

Chrome で /firefox にアクセス(上)、そして Firefox で /chrome にアクセス(下)。
HAProxy の acl によるバックエンドへの振り分けを試す(1): パターン(2)(and)(Chrome: /firefox)

以下、メモ。

どちらの条件にもマッチしないので default_backend が利用されるが、default_backend のパスに /chrome/firefox が存在しない為にバックエンドの Web サーバーの 404 エラーが表示される。

まとめると…

  • and は明示的に指定する必要はない

パターン(3)(接続元 and User-Agent)

すいません、以下、スクショが辛いのでコマンドラインから実行します。

接続元 xxx.xxx.xxx.xxx/32 から Chrome でアクセス。

% http http://zzz.zzz.zzz.zzz:8000/ User-Agent:"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36"
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 8
Content-Type: text/html; charset=UTF-8
Date: Fri, 09 Jan 2015 03:11:37 GMT
ETag: "40975-8-50c2bca4b0cf9"
Last-Modified: Thu, 08 Jan 2015 22:45:00 GMT
Server: Apache/2.2.29 (Amazon)

default

条件にマッチしないので default_backend にアクセスする。

接続元 xxx.xxx.xxx.xxx/32 から Firefox で / にアクセス。

% http http://zzz.zzz.zzz.zzz:8000/ User-Agent:"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:34.0) Gecko/20100101 Firefox/34.0"
HTTP/1.1 200 OK
Content-Length: 14
Content-Type: text/html;charset=utf-8
Date: Fri, 09 Jan 2015 03:06:41 GMT
Server: WEBrick/1.3.1 (Ruby/2.0.0/2014-11-13)
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
X-Xss-Protection: 1; mode=block

firefox<br/>/

接続元 xxx.xxx.xxx.xxx/32 から User-Agent を指定しない場合には以下のようになる。

% http http://54.65.162.168:8000/
HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 8
Content-Type: text/html; charset=UTF-8
Date: Fri, 09 Jan 2015 03:08:05 GMT
ETag: "40975-8-50c2bca4b0cf9"
Last-Modified: Thu, 08 Jan 2015 22:45:00 GMT
Server: Apache/2.2.29 (Amazon)

default

条件にマッチしないので default_backend にアクセスする。

接続元 yyy.yyy.yyy.yyy/32 から Chrome でアクセス。

$ http GET http://zzz.zzz.zzz.zzz:8000/ User-Agent:"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.95 Safari/537.36"
HTTP/1.1 200 OK
Content-Type: text/html;charset=utf-8
Content-Length: 13
X-Xss-Protection: 1; mode=block
X-Content-Type-Options: nosniff
X-Frame-Options: SAMEORIGIN
Server: WEBrick/1.3.1 (Ruby/2.0.0/2014-11-13)
Date: Fri, 09 Jan 2015 03:16:06 GMT

chrome<br/>/

接続元 yyy.yyy.yyy.yyy/32 から Firefox で / にアクセス。

$ http GET http://zzz.zzz.zzz.zzz:8000/ User-Agent:User-Agent:"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:34.0) Gecko/20100101 Firefox/34.0"
HTTP/1.1 200 OK
Date: Fri, 09 Jan 2015 03:17:25 GMT
Server: Apache/2.2.29 (Amazon)
Last-Modified: Thu, 08 Jan 2015 22:45:00 GMT
ETag: "40975-8-50c2bca4b0cf9"
Accept-Ranges: bytes
Content-Length: 8
Content-Type: text/html; charset=UTF-8

default

条件にマッチしないので default_backend にアクセスする。

接続元 yyy.yyy.yyy.yyy/32 から User-Agent を指定しない場合には以下のようになる。

$ http GET http://zzz.zzz.zzz.zzz:8000/
HTTP/1.1 200 OK
Date: Fri, 09 Jan 2015 03:18:17 GMT
Server: Apache/2.2.29 (Amazon)
Last-Modified: Thu, 08 Jan 2015 22:45:00 GMT
ETag: "40975-8-50c2bca4b0cf9"
Accept-Ranges: bytes
Content-Length: 8
Content-Type: text/html; charset=UTF-8

default

条件にマッチしないので default_backend にアクセスする。

パターン(4)(接続元(X-Forwarded-for) and User-Agent)

接続元 xxx.xxx.xxx.xxx/32 から Chrome で ELB にアクセス。

% http http://test-12345678.ap-northeast-1.elb.amazonaws.com/ User-Agent:Chrome
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 74
Content-Type: text/html; charset=UTF-8
Date: Fri, 09 Jan 2015 08:44:49 GMT
Server: Apache
X-Powered-By: PHP

X-Forwarded-For: xxx.xxx
Default
X-Forwarded-For: xxx.xxx.xxx.xxx, 172.xx.xx.xx
User-Agent: Chrome

条件にマッチしない為に default_backend が利用される。尚、X-Forwarded-For にはクライアントの IP である xxx.xxx.xxx.xxx と ELB の IP アドレス 172.xx.xx.xx が記録される。

また、以下の通り ELB 経由ではなく直接 HAProxy にアクセスしてみる。

% http http://zzz.zzz.zzz.zzz:8000/ User-Agent:Chrome
HTTP/1.1 200 OK
Content-Length: 60
Content-Type: text/html; charset=UTF-8
Date: Fri, 09 Jan 2015 08:48:34 GMT
Server: Apache
X-Powered-By: PHP

Default
X-Forwarded-For: xxx.xxx.xxx.xxx
User-Agent: Chrome

こちらも条件にマッチしない為に default_backend が利用される。

以下は接続元 xxx.xxx.xxx.xxx/32 から Firefox で ELB にアクセス。

% http http://test-12345678.ap-northeast-1.elb.amazonaws.com/ User-Agent:Firefox
HTTP/1.1 200 OK
Connection: keep-alive
Content-Length: 75
Content-Type: text/html; charset=UTF-8
Date: Fri, 09 Jan 2015 08:51:10 GMT
Server: Apache
X-Powered-By: PHP

Firefox
X-Forwarded-For: xxx.xxx.xxx.xxx, 172.xx.xx.xx
User-Agent: Firefox

use_backend の条件にマッチしている為に Firefox ページにアクセスされる。

% http http://zzz.zzz.zzz.zzz:8000/ User-Agent:Firefox
HTTP/1.1 200 OK
Content-Length: 60
Content-Type: text/html; charset=UTF-8
Date: Fri, 09 Jan 2015 08:54:50 GMT
Server: Apache
X-Powered-By: PHP

Default
X-Forwarded-For: xxx.xxx.xxx.xxx
User-Agent: Chrome

接続元 yyy.yyy.yyy.yyy/32 から Chrome で ELB にアクセス。

http GET http://test-12345678.ap-northeast-1.elb.amazonaws.com/ User-Agent:Chrome
HTTP/1.1 200 OK
Content-Type: text/html; charset=UTF-8
Date: Fri, 09 Jan 2015 08:56:39 GMT
Server: Apache
X-Powered-By: PHP
Content-Length: 72
Connection: keep-alive

Chrome
X-Forwarded-For: yyy.yyy.yyy.yyy, 172.xx.xx.xx
User-Agent: Chrome

use_backend の条件にマッチしている為に Chrome ページにアクセスされる。

引続き、接続元 yyy.yyy.yyy.yyy/32 から HAProxy に直接 User-Agent を Firefox にしてアクセスしてみる。

$ http GET http://zzz.zzz.zzz.zzz:8000/ User-Agent:Firefox
HTTP/1.1 200 OK
Date: Fri, 09 Jan 2015 09:00:06 GMT
Server: Apache
X-Powered-By: PHP
Content-Length: 60
Content-Type: text/html; charset=UTF-8

Default
X-Forwarded-For: yyy.yyy.yyy.yyy
User-Agent: Firefox

use_backend 何れの条件にもマッチしない為に default_backend に指定されたバックエンドにアクセスされる。

最後に

わかった事

冒頭にも書いたけど。

  • use_backend で複数の条件を定義することが可能
  • acl で定義した条件をそのまま並べると and 条件となる
  • and の代わりに or を付けることも可能(どちらかにマッチしたらという条件に代わる)
  • and 条件を or で並記することが可能
  • HAProxy とは関係ないけど Sinatra で複数のパス指定は sinatra/multi_routerequire すると良い

元記事はこちらです。
HAProxy の acl によるバックエンドへの振り分けを色々と試す(1) (「User-Agent と接続元 IP」という条件でバックエンドへの振り分けをコントロールする)