BIG-IPで、Hostヘッダに基づいた振り分けを行うiRule３種

# はじめに
BIG-IPで、HTTPリクエストに含まれるHTTP Hostヘッダに基づいて振り分け先Poolを切り替える方法について。

簡単に言うと、下記のイメージです。

![20170103_bigip-01.png](https://qiita-image-store.s3.amazonaws.com/0/157891/7a5cacc6-575f-bddb-66d6-c97aca008404.png)

Apacheでは「名前ベースのバーチャルホスト」と言われるものでしょうか。BIG-IPでこれを実装するには、iRuleを使用する必要があります。今回、異なる３つのiRuleを考えてみました。（実際には、1→２→３の順にruleをブラッシュアップして行ったというほうが正しいです。）

1. 基本的なHostヘッダーベーススイッチング
2. Data Group Listを使ったHostヘッダーベーススイッチング
3. Pool名を使ったHostヘッダーベーススイッチング

それぞれについて、記載したいと思います。

# 1. 基本的なHostヘッダーベーススイッチング用iRule

## iRuleの紹介と解説
最も基本的なiRuleは、以下のようになります。

```tcl:iRule
when HTTP_REQUEST
{
  if { [HTTP::host] matches "www1.example.co.jp"　} {
    pool pool_www1
  }
  elseif { [HTTP::host] matches "www2.example.co.jp"　} {
    pool pool_www2
  }
  elseif { [HTTP::host] matches "www3.example.co.jp"　} {
    pool pool_www3
  }
  else {
    log local0. "No matching host. SRC-IP [IP::client_addr], Host [HTTP::host]"
  }
}
```

このiRuleは、クライアントからのHTTPリクエストを受信した時に実行されます。リクエスト内のHostヘッダが、
[**[HTTP::host]**](https://devcentral.f5.com/wiki/irules.http__host.ashx)というコマンドで読み込まれ、if文から逐次評価されます。マッチした場合は、そのifブロック内のpoolへ通信が振り分けられます。

HTTPリクエスト内にHostヘッダ自体がない、またはどの条件文ともマッチしないHostヘッダを含んでいる場合は、最後のelseブロックにマッチし、/var/log/ltmへ、送信元とHostヘッダ内容のログを出力します。
この後、もし、Virtual ServerにDefault Poolを設定している場合は、Default Poolへ振り分けられますが、Default Poolを設定していない場合は、コネクションがリセットされます。（BIG-IPがクライアントへTCP-RSTを応答します。）

### このiRuleのメリット
このiRuleは、単純（シンプルというより原始的）ですが、拡張性があります。例えば、ある特定のpoolへの振り分け時にのみ、何らかの処理を加えたい場合、該当箇所にコードを追加すれば対応できます。

### このiRuleのデメリット
このVirtual ServerでサポートするHostが増える都度iRule自体を修正する必要があります。頻繁にHostが増減するような環境では、それがかなり負担になるかもしれません。


# 2. Data Group Listを使ったHostヘッダーベーススイッチング用iRule

## iRuleの紹介と解説
次は、Data Group Listという機能を使ったiRuleです。

```tcl:irule
when HTTP_REQUEST
{
  if { [class match [HTTP::host] equals example.co.jp_host_list] } {
    set pool [class match -value [HTTP::host] equals example.co.jp_host_list]
    pool $pool
  }
  else {
    log local0. "No matching host. SRC-IP [IP::client_addr], Host [HTTP::host]"
  }
}
```

最初のiRuleと比べて、かなりシンプルになりました。

### Data Group Listとは
Data Group Listは、プログラムでいう連想配列のようなものです。
二つのデータ（KeyとValue)を１セットとして、BIG-IPに登録します。

例えば、以下の表ようなデータを、Data Group Listとして登録し、iRuleから扱うことができます。

| String(Key) | Value |
|:-:|:-:|
| www1.example.co.jp  | Pool_www1  |
| www2.example.co.jp  | Pool_www2  |
| www3.example.co.jp  | Pool_www3  |
| ・  | ・  |

このようにドメイン名と振り分け先Poolを１つのセットで入力しておけば、「HTTP::hostの文字列が、Data Group ListのKeyにマッチした場合は、そのElementのValueの名前のPoolに振り分ける」というiRuleを作成しておくことで、今後はHost名が追加されても、Data Group Listを編集するのみ（あとPoolの追加）で、iRule自体には手を加えないで済みます。

なお、一つのData Group List内で、Keyは一意である必要があります。

| String(Key) | Value |
|:-:|:-:|
| www1.example.co.jp  | Pool_www1  |
| www1.example.co.jp  | Pool_www2  |

↑こんなものは作れません。

| String(Key) | Value |
|:-:|:-:|
| www1.example.co.jp  | Pool_www1  |
| www２.example.co.jp  | Pool_www1  |

↑これは問題ありません。

Data Group Listは、GUI (Configuration Utility）の「Local Traffic -> iRules -> Data Group List」というところにあります。
![iRule_DataGroupList_SS.png](https://qiita-image-store.s3.amazonaws.com/0/157891/401177bd-b4b1-8c0f-74cc-ec866679162b.png)

新規に作成する場合は、Keyの型(Type)を選ぶところから始まります。型は「Address」「String」「Integer」から一つ、適当なものを選択します。今回はHost名が入るのでStringを使用します。そして、まずはデータが空のData Group Listを作成し、次に、そのData Group ListのRecords欄に実際のデータを入力していきます。

作成したData Group Listはこんな感じ↓になりました。

![iRule_DataGroupList_SS_2.png](https://qiita-image-store.s3.amazonaws.com/0/157891/253bda4b-4c41-8f79-46be-24b007ea1481.png)

String Records表内の、「:=」の左側がKey、右側がValueです。

### iRuleのclassコマンドについて
このiRuleでは[**class**](https://devcentral.f5.com/wiki/iRules.class.ashx)というコマンドを使用します。これはiRule内でData Group Listを扱うためのコマンドです。非常に多くの機能があるので、使う際は、Devcentralのリファレンスを参照したほうが良いと思います。

今回のiRuleで使用しているのは以下の二つのケースです。

`class match 文字列 equals DataGroupList名`
これは、文字列が対象DataGroupListのKeyと一致するかをチェックします。マッチした場合は1、マッチしなかったは場合は０が返ります。

`class match -value 文字列 equals DataGroupList名`
こちらは、文字列が対象DataGroupListのKeyに含まれる場合にそのValueが返ります。

### このiRuleのメリット
Data Group Listを使うので、Hostが追加、削除されてもiRule自体を触らずに済むのは、運用上大きなメリットと思います。DevCentralで検索してみると、実際にData Group Listを使ったL7スイッチ（Hostヘッダ以外にも、URI等も対象にできます）を利用している実績は少なくないようです。

### このiRuleのデメリット
通信の振り分けは行えますが、個々のPoolへの振り分け時に、個別にさらなる処理を加えることが難しくなります。iRuleに処理を加えることはできますが、全ての通信が一律して同じ処理を追加されることになります。（それもメリットと言えるかも。）


# 3. Pool名を使ったHostヘッダーベーススイッチング用iRule

## iRuleの紹介と解説

最後のiRuleは、Hostが追加や削除されても、iRuleを変更する必要がなく、Data Group Listも使用しません。
その代わり、Host名とPool名の間に簡単な命名規則を設けます。例えば、

| ドメイン名 |  | 振り分け先のPool名 |
|:-:|:-:|:-:|
| **www1**.example.co.jp  | ⇒  | Pool_**www1**  |
| **www2**.example.co.jp  | ⇒  | Pool_**www2**  |
| **www3**.example.co.jp  | ⇒  | Pool_**www3**  |

上記のように、「Host名の先頭」と「Pool名」を一致させるという規則があれば、次の手順で、HTTP　Hostヘッダの情報から、振り分け先Poolが判定できます。

1. HTTPリクエストのHostヘッダが"**(hoge).example.co.jp**" にマッチするかを確認する
2. マッチした場合、BIG-IPに"**Pool_(hoge)**"という　Poolがあるかを確認する
3. Poolが存在する場合、そのPoolへ通信を振り分ける

前置きは以上にして、iRuleを記載します。

```tcl:irule
when RULE_INIT
{
    set static::host_re {(\w+)\.example\.co\.jp}
}
when HTTP_REQUEST
{
    if { [regexp $static::host_re [HTTP::host] all host] } {
        if { [catch {pool "pool_$host"}] } {
            log local0. "No matching host. SRC-IP [IP::client_addr], Host [HTTP::host]"
        }
    }
    else {
        log local0. "Regexp not matched. SRC-IP [IP::client_addr], Host [HTTP::host]"
    }
}
```

このiRuleは、二つのイベント（RULE_INITとHTTP_REQUEST)に分かれているので、イベント毎に詳細を記載します。

### iRuleの「RULE_INIT」イベントについて

```tcl:RULE_INIT内
when RULE_INIT
{
    set static::host_re {(\w+)\.example\.co\.jp}
}
```

[**RULE_INIT**](https://devcentral.f5.com/wiki/irules.rule_init.ashx)イベントは、
 - そのiRuleがsaveされた時
 - 機器の起動時
 - ソフトウェアの再スタート時
のタイミングで一度実行されます。

RULE_INIT内で「**static::定数名**」で作成したグローバル定数は、他のイベント内で「**$static::定数名**」というフォーマットでグローバル定数として利用できます。

iRule内で常に同じ値となる定数は、RULE_INIT内に「**static::定数名**」で定義するのが効率的です。HTTP_REQUESTなどの一般的なイベント内で定義すると、そのiRuleが実行される都度に新しく定義し、そして通信の終了時に破棄されることになります。

### HTTP_REQUEST イベント内の処理について

```tcl:HTTP_REQUEST内
when HTTP_REQUEST
{
    if { [regexp $static::host_re [HTTP::host] all host] } {
        if { [catch {pool "pool_$host"}] } {
            log local0. "No matching host. SRC-IP [IP::client_addr], Host [HTTP::host]"
        }
    }
    else {
        log local0. "Regexp not matched. SRC-IP [IP::client_addr], Host [HTTP::host]"
    }
}
```

最初のif文
`if { [regexp $static::host_re [HTTP::host] all host] }`
で、正規表現を使って、Hostヘッダが、(\w+).example.co.jpにマッチしているかをチェックします。また、マッチした場合ホスト名の先頭部分がhostという変数に保存されます。

Hostヘッダがマッチした場合は、つづいて
`if {[ catch {pool "pool_$host"} ]}`
というif文とcatcheコマンドを使ってます。

catchコマンドは、続く引数を実行します。引数を実行してその応答が正であれば、cacheコマンドは０を応答します。つまりif文は偽になります。もし、実行中の引数がエラーを返した場合、catchコマンドは0以外を応答します。したがって、ここでは、そもそも存在するかわからないPoolに対して通信を振り分けようとしてPoolがあれば振り分けるし、なければエラーが返るという多少強引と思える方法を使ってます。これは[DevCentralにも載ってる](https://devcentral.f5.com/articles/irules-101-07-catch)ので、この使い方で問題ないかと思います。

### このiRuleのメリット
振り分ける新規ホストが追加された時の手間は３つのiRuleで最小です。
命名規則に従った　Poolを作成すれば良いだけです。

### このiRuleのデメリット
「命名規則にマッチするかどうか」で判断するので、Pool名が制限されます。
このiRuleの対象としないPoolについても、（上記のcache文でマッチしないように）名前が制限されることになります。

# 最後に
メリット、デメリットを書くならば、各iRuleごとに、大量のPoolがある場合の負荷はどうなるのか、ということも考えるべきと思ったのですが、通信速度に差異が出るほどの設定量を行うのが難しく、それは未確認です。おそらくは、DataGroupListを使うパターンが一番高速で負荷が軽くなるんじゃないでしょうか。

そもそもそれほど大規模な環境なら、グローバルIPもたくさんあって、Virtual Server一つでどうにかしよう、なんてしないのではと思います。

# 補足事項
ここではHostヘッダを取得する[HTTP::host]を使っていますが、iRuleを使えば、他のいろいろなHTTPヘッダの情報も簡単に取得して、扱うことができます。紹介記事を書いたので、よければ見てやってください。

[Qiita: iRuleでHTTPリクエストに含まれる値を取得する](https://qiita.com/Kaz_K/items/6317a1f2ff53b720ca0c)

# 参考情報
　 - [DevCentral: Getting Started with iRules: Variables](https://devcentral.f5.com/articles/getting-started-with-irules/getting-started-with-irules-variables-20403)
　 - [DevCentral: iRules 101 - #07 - Catch](https://devcentral.f5.com/articles/irules-101-07-catch)
　 - [DevCentral: iRules 101 - #08 - Classes](https://devcentral.f5.com/articles/irules-101-08-classes)
　 - [Tcl 8.4.1　Manual Command Reference: regexp](http://www.freesoftnet.co.jp/webfiles/tclkits/doc/TclCmdRef/TclCmd/regexp_jp.htm)

本ページは以上です。