先日、社内で勉強会をした時に発表した資料(ちょっと改変)です。
最初はVMODを作るときに知っておくと楽じゃないかなぁ的なポイントを解説しようと思ったんですが
そもそもVMODのワンポイントなんか社内的に誰得極まりないので
公式でリスト化されてるVMODを紹介してみました。
サービス側がこれ使ってみたいってのがあればいいなぁと考えています。
結構口頭で説明してるとこがあるので、そこは申し訳ないです。
いろいろ他の人で面白い発表も聞けたのでまた第二回とかやりたいですね
先日、社内で勉強会をした時に発表した資料(ちょっと改変)です。
最初はVMODを作るときに知っておくと楽じゃないかなぁ的なポイントを解説しようと思ったんですが
そもそもVMODのワンポイントなんか社内的に誰得極まりないので
公式でリスト化されてるVMODを紹介してみました。
サービス側がこれ使ってみたいってのがあればいいなぁと考えています。
結構口頭で説明してるとこがあるので、そこは申し訳ないです。
いろいろ他の人で面白い発表も聞けたのでまた第二回とかやりたいですね
hiro_yさんからこんな質問を受けたので
@xcir VarnishからS3の認証突破する方法とかご存知ないですか…と思いまして
— 山岡広幸さん (@hiro_y) 7月 2, 2012
AWSの勉強がてら作ってみました。
S3のREST-APIのAuthorizationヘッダは、日付やリソースの場所などを改行で結合して
HMAC-SHA1でハッシュ化して、BASE64エンコードする必要があります。
HMAC-SHA1については、Varnish公式が公開しているvmod-digestを使うことでできるのですが
出力をBASE64にすることができないので、コードを拝借して今回のVMODを作ってみました。
ちなみに改行を扱うことについても、インラインCかVMODを使う必要があります。
import awsrest; backend default { .host = "s3.amazonaws.com"; .port = "80"; } sub vcl_recv{ awsrest.s3_generic( "accessKey", //AWSAccessKeyId "secretKey", //SecretAccessKeyID req.request, //HTTP-Verb req.http.content-md5, //Content-MD5 req.http.content-type, //Content-Type "", //canonicalizedAmzHeaders req.url, //canonicalizedResource now //Date ); }
上記のように呼び出すとreq.http.Authorizationとreq.http.Dateに生成したヘッダがセットされます。
15 TxHeader b Date: Tue, 03 Jul 2012 16:21:47 +0000 15 TxHeader b Authorization: AWS accessKey:XUfSbQDuOWL24PTR1qavWSr6vjM=
セットされるのがbereqではないのでvcl_recvで呼び出すことを推奨します。
またAuthorizationを生成するのでもしキャッシュを行いたい場合は適切にVCLを記述してください。
デフォルトのVCLではreq.http.Authorizationを含む場合はpassするためです。
僕の方でも簡単な動作チェックを行なっていますが、もし変な動きをしたら教えて下さい。
—
参考サイト
PHP で Amazon S3 の REST API を使用 #1
Authenticating REST Requests
Difficult to access the POST/GET/Cookie value in Varnish.
I want more easily to access it.
Therefore, I tried to make a VMOD to parse.
For example, how to set the response header from the POST key of hoge.
It’s a simple.
import parsereq; vcl_recv{ //please write "parsereq.init();" to 1st line in vcl_recv. parsereq.init(); } vcl_deliver{ set resp.http.hoge = parsereq.post_header("hoge"); }
I hope that this code is of help to you.
download here.
vmod-parsereq
この前のエントリでPOSTを扱うVMODを作ってみたというのを上げたのですが
あの後、使ってくれた人とかからインタフェース変えるとイイヨーイイヨーと言われたので
req.http.*に格納するのではなくvmod_curlのように良い感じに使いやすくしてみました。
それだけじゃつまらないということで、おまけでGETとCOOKIEに対応してみました。
今回の特徴は以下です。
・application/x-www-form-urlencodedとmultipart/form-data両方に対応
・GET・POST・COOKIEに対応
・格納されているキーの一覧を取得することができます。
インラインCと併用することですべてのGET・POST・COOKIEのキーに対して特定の値
(攻撃っぽいコードなど)が含まれているかなどのチェックが容易です
使い方はこんな感じです。
たとえばPOSTでhogeというキーを取得してレスポンスする場合はこんな感じです。
import parsereq; vcl_recv{ parsereq.init();//必ずvcl_recvの先頭で宣言する } vcl_deliver{ set resp.http.hoge = parsereq.post_header("hoge"); }
前に比べてわかりやすくなったんではないかと思います。
また今回は値はurlencodeされていません。生の値です。
そのため改行を含む可能性がある場合はresp.http.*などに含めると
意図しない結果が起きるので注意が必要です。
ココらへんはencode/decodeメソッドを用意して解決する予定ですので
少し待ってもらえるとありがたいです。
関数はこんな感じです。
VOID init()
初期化を行う。
必ずvcl_recvの先頭で呼び出す必要があります。
INT errcode()
初期化の結果を受け取る
(成功) 2 content-typeが無いかサポート外 (成功) 1 パースに成功 (失敗) -1 sess_workspaceの残りサイズがなくなってメモリ確保に失敗 (失敗) -2 content-lengthが無い (失敗) -3 読み取れるサイズがcontent-length以下
STRING post_header(STRING)
STRING get_header(STRING)
STRING cookie_header(STRING)
指定したキーの値を取得する
STRING post_body()
STRING get_body()
STRING cookie_body()
POST,GET,COOKIEの生データを取得
STRING post_read_keylist()
STRING get_read_keylist()
STRING cookie_read_keylist()
呼び出すごとに格納されているキーを取得する
//req /?name1=a&name2=b //vcl vcl_deliver{ set resp.http.n1 = parsereq.get_read_keylist(); set resp.http.n2 = parsereq.get_read_keylist(); //nothing set resp.http.n3 = parsereq.get_read_keylist(); } //return n1: name2 n2: name1
VOID post_seek_reset()
VOID get_seek_reset()
VOID cookie_seek_reset()
前述のXXX_read_keylistで、何処まで取得したかをリセットする。
リセットしてXXX_read_keylistを呼び出すとまた最初から読み出す。
今後リリースされる3.0.3についても
rc1において正常動作を確認していますので多分動くと思います。
基本的に3.0.1以降であれば動きます。
今後の予定は
・エンコード・デコードロジック追加
・値の変更した後にリビルドしてそれを使う
・コードが若干カオスってるので綺麗にする
・etc…
です
まだChangelogが出ていないので気づいている範囲での変更点です。
リリースになったらまた書きます。
今まで記述ミスなどがあったのですがかなり書きなおされています。
-jでJSON形式で出力します。
通信に関わるパラメータ
idle_send_timeout (sec default:60)
送信時にアイドル状態(データの送信ができなくなった)になった場合のタイムアウト時間です
内部での正規表現の呼び出しについての制限
pcre_match_limit (1~UINT_MAX default:10000)
呼び出し回数の制限
pcre_match_limit_recursion (1~UINT_MAX default:10000)
再起回数の制限
http_range_support (BOOL default:ON)
rangeリクエストのサポートですがEXPERIMENTALから正式になりました。
ただ3.0.0からデフォルトでONになっていたので、特に気にすることはないと思います。
send_timeout (sec default:600)
送信時のタイムアウト時間が60秒から600秒に変更されました。
idle_send_timeoutでアイドルの設定ができるようになった影響で増えてると思います。
HTC_Read関数の呼び出し方が変わりました
基本的に普通の使い方をしていれば全く影響はありませんが、
インラインCやVMODでゴリゴリpipeline周りをいじろうとしている場合、
バージョンアップによって動かなくなる可能性があります。
たとえばいままで以下のように呼び出していた場合変更する必要があります。
~3.0.2 rsize = HTC_Read(sp->htc, buf, buf_size); 3.0.3~ rsize = HTC_Read(sp->wrk, sp->htc, buf, buf_size);
僕が知っている限りVFW(Varnish Firewall)が影響を受けると思います。
また昨日公開しているvmod_parsepostも影響を受けるのですが
既に修正しています。
Varnishは基本的にPOSTデータを解釈してなにかしらの処理をすることができません。
そこまで不便はないとは思うのですが、たとえば特定のキーワードがPOSTに入ってたら
Varnishではじきたいといったことは普通は出来ません。(インジェクションっぽいクエリとか)
そのためVarnishでPOSTを手軽に扱うためのVMODを作ってみました。
以下のような特徴・機能があります。
・application/x-www-form-urlencodedとmultipart/form-data両方に対応
・パースした値をreq.http.*に格納する
・指定されたヘッダにパースしていない生データを格納可能
・multipartのデータはurlencodeして格納
・multipartの生データは(扱いづらいので)application/x-www-form-urlencodedと同じ形式に直して格納
・multipart時のファイルについても格納
こんな感じで利用します。
//VCL if(parsepost.parse("x-raw",true,"p_",true,true) == 1){ std.log("raw: " + req.http.x-raw); std.log("submitter: " + req.http.p_submitter); std.log("submitter2: " + req.http.p_submitter2); } //response 12 VCL_Log c raw: submitter=abcdef&submitter2=b 12 VCL_Log c submitter: abcdef 12 VCL_Log c submitter2: b
また特性上セッションワークスペースとスタックを多く使います。
そのためエラーが出る場合は
Varnishのsess_workspaceの値の引き上げとulimit -sの値の引き上げを行なってください。
またhttp_req_sizeを大きめにしておくと結果としてメモリの節約になります
あと生データは大きくなりがちなので使い終わったらunsetをおすすめします
そうしないとバックエンドにフェッチする際のリクエストに含まれてヘッダ大きすぎエラー(413 Request Entity Too Large/400 Bad Request)が出る可能性もあります。
若干小ネタですが、こんな書き方もできるという例です。
先日のエントリ(Varnishを使う際に覚えておきたいデフォルトの罠)において、
Varnishはユーザの入力したVCLコードの後に、デフォルトのVCLコードを挿入すると紹介しました。
これはユーザーのVCLとデフォルトのVCLの組み合わせですが、
実はユーザのVCLを複数定義して連結することが可能です。
やり方は非常に簡単で、同じアクションを複数定義するだけです。
■VCL sub vcl_deliver{ set resp.http.a = "a"; } sub vcl_deliver{ set resp.http.a = resp.http.a + "b"; } sub vcl_deliver{ set resp.http.a = resp.http.a + "c"; } ■出力ヘッダ a: abc
上記のVCLは以下のように結合されVarnishで使用されます
sub vcl_deliver { {//ユーザの定義1 set resp.http.a = "a"; } {//ユーザの定義2 set resp.http.a = resp.http.a + "b"; } {//ユーザの定義3 set resp.http.a = resp.http.a + "c"; } {//デフォルト return (deliver); } }
因みに結合する際の順番は先に出てきたものからどんどん突っ込んでいく感じです。
実際のところどのように使えばいいかというと、includeと組み合わせて使うのが便利だと思います。
include"/etc/varnish/ext.vcl";
先頭や末尾にincludeで別処理を入れることで、本来の処理を邪魔せず
デバッグすることや、複数のサーバにまたがる共通処理などを記述することが可能です。
ちょっと小ネタですが役に立てばいいなぁとおもいます。
最近とある人が嵌っていて聞かれたので
Varnishを使う上で覚えておきたいデフォルト設定の罠を説明したいと思います。
そのかたはbackendの設定をLISTENポートと勘違いしていました。
Varnishの設定は主に2つにわかれています
起動パラメータはキャッシュを保存するストレージのサイズを指定したり、保持するスレッド数や、ワークスペースのサイズ、LISTENポートを指定したりなどと
主に変更する場合はVarnish自体の再起動が必要などと即時に反映できないものが多いです。
(デフォルトのTTLなどもあり、必ずしも全てではない)
そしてVCL設定はリクエストに対してVarnishがどのように振る舞うかを定義しています。
redhat系の場合起動パラメータは
/etc/sysconfig/varnish
に記述されており、LISTENポートは以下のように定義されています
# # Default address and port to bind to # # Blank address means all IPv4 and IPv6 interfaces, otherwise specify # # a host name, an IPv4 dotted quad, or an IPv6 address in brackets. # VARNISH_LISTEN_ADDRESS= VARNISH_LISTEN_PORT=6081
保存されている画像などが変更されずに、URLに対してユニークであることが保証できる場合
if-modified-sinceを受け取った段階で、即304を返す場合があります。
たとえば以下のようなコードです
...バックエンドの設定... sub vcl_recv{ if(req.http.If-Modified-Since) { error 304 "Not Modified"; } }
特に問題もなく動くブラウザと動かないブラウザが出てくると思います。
Varnishの304時のレスポンスをよく観察してみると、ボディがくっついているのが確認できると思います。
ステータス304はレスポンスボディを含んではならないため
ブラウザによっては予期せぬ動作になったりします。
これはVarnishのデフォルト設定が原因です。
まずVarnishがユーザーの記述したVCLコードと、デフォルトのVCLコードをどのように扱っているかを説明します。
Varnishはユーザの入力したVCLコードの後に、デフォルトのVCLコードを挿入します。
そのため、たとえば以下のようにvcl_recvを指定した場合でも
sub vcl_recv{ if(req.http.If-Modified-Since) { error 304 "Not Modified"; } }
実際にVarnishは以下のように解釈します
sub vcl_recv{ //ユーザの設定したvcl_recv { if(req.http.If-Modified-Since) { error 304 "Not Modified"; } } //Varnishのデフォルトのvcl_recv { if (req.restarts == 0) { if (req.http.x-forwarded-for) { set req.http.X-Forwarded-For = req.http.X-Forwarded-For + ", " + client.ip; } else { set req.http.X-Forwarded-For = client.ip; } } if (req.request != "GET" && req.request != "HEAD" && req.request != "PUT" && req.request != "POST" && req.request != "TRACE" && req.request != "OPTIONS" && req.request != "DELETE") { /* Non-RFC2616 or CONNECT which is weird. */ return (pipe); } if (req.request != "GET" && req.request != "HEAD") { /* We only deal with GET and HEAD by default */ return (pass); } if (req.http.Authorization || req.http.Cookie) { /* Not cacheable by default */ return (pass); } return (lookup); } }
つまり、ユーザが何も指定しないアクションは全てデフォルトの動作となります。
vcl_errorのデフォルト設定は以下のとおりです
sub vcl_error { set obj.http.Content-Type = "text/html; charset=utf-8"; set obj.http.Retry-After = "5"; synthetic {" <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html> <head> <title>"} + obj.status + " " + obj.response + {"</title> </head> <body> <h1>Error "} + obj.status + " " + obj.response + {"</h1> <p>"} + obj.response + {"</p> <h3>Guru Meditation:</h3> <p>XID: "} + req.xid + {"</p> <hr> <p>Varnish cache server</p> </body> </html> "}; return (deliver); }
つまり、以下のようにデフォルトの設定が動いてしまいレスポンスボディが生成されたため
ブラウザによって不具合が起きてしまいます。
これを防ぐためには、vcl_errorで以下のような記述をする必要があります。
sub vcl_error { if(obj.status == 304){ return (deliver); } }
この設定によりerrorで304を発行する場合、
デフォルトの設定を行わずそのまま次の処理に移動します。
また204 No Contentや1XXについてもレスポンスボディを含んではいけないので
sub vcl_error { if((obj.status >= 100 && obj.status < 200) || obj.status == 204 || obj.status == 304){ return (deliver); } }
とするのも良いでしょう。
全て静的コンテンツで、バックエンドは200を返しているのにキャッシュされない場合があります。
これは先程のデフォルトの設定と絡んでいます。
vcl_recvのデフォルト設定を見てみましょう。
sub vcl_recv { if (req.restarts == 0) { if (req.http.x-forwarded-for) { set req.http.X-Forwarded-For = req.http.X-Forwarded-For + ", " + client.ip; } else { set req.http.X-Forwarded-For = client.ip; } } if (req.request != "GET" && req.request != "HEAD" && req.request != "PUT" && req.request != "POST" && req.request != "TRACE" && req.request != "OPTIONS" && req.request != "DELETE") { /* Non-RFC2616 or CONNECT which is weird. */ return (pipe); } if (req.request != "GET" && req.request != "HEAD") { /* We only deal with GET and HEAD by default */ return (pass); } if (req.http.Authorization || req.http.Cookie) { /* Not cacheable by default */ return (pass); } return (lookup); }
見ての通り、認証時やクッキーが付いている場合はデフォルトではpass動作となりキャッシュされません。
VCLの記述をする際は、必ず自分の書いたコードの後にデフォルトのコードが付与されることを意識して書く必要があります。
全て自分の記述したコードで完結し、デフォルトの設定を動かしたくない場合は
必ずreturn(XXXX)で終わるようにしましょう。
以上
意外と嵌りやすいんじゃないかなーという点を解説しました。
(僕も触り始めの頃デフォルト設定が動いて嵌りました)
何かの参考になればと思います。
VMOD processing want to before/after VCL function processing.
But, I do not want to write extra line.
How to do it?
Can be realized by the replace the pointer to sp->vcl->XXXX_func (VCL_XXXX)
Can be added to the processing by replacing pointer
static vcl_func_f *vmod_redirect_Hook_vcl_error = NULL; static pthread_mutex_t vmod_redirect_mutex = PTHREAD_MUTEX_INITIALIZER; static unsigned hook_done = 0; static int vmod_Hook_vcl_error(struct sess *sp){ .... //write the processing //call original vcl_error return(vmod_redirect_Hook_vcl_error(sp)); } int vmod_location(struct sess *sp, int status, const char*p,...) { //vcl reload detection. if(hook_done == 1 && sp->vcl->error_func != vmod_redirect_Hook_vcl_error ) hook_done = 0; if(hook_done == 0){ //lock to prevent another thread write AZ(pthread_mutex_lock(&vmod_redirect_mutex)); if(hook_done == 0) { //store the original vcl_error pointer vmod_redirect_Hook_vcl_error = sp->vcl->error_func; //hook sp->vcl->error_func = vmod_Hook_vcl_error; hook_done = 1; } //unlock AZ(pthread_mutex_unlock(&vmod_redirect_mutex)); } .... return (status); }
(via:vmod_redirect)
Hook to vcl_error at vmod processing in first time.
because there is no way to write-access the pointer at vmod_Init.
also, has locked using the mutex, because to prevent the loop by thread concurrent access.
I hope that this code is of help to you.
ATTENTION
don’t use varnishadm’s command “vcl.use” and “vcl.discard” . because to the segfault or call to other vcl function.
—
modify(2012-08-01)
when you vcl reloaded, hook method be off.
VMODを作る上で何かしらの後処理や前処理を、ユーザの処理前にやりたいケースが存在します。
たとえば僕が以前作ったvmod_redirectの場合です。
Varnishのリダイレクトはかなりめんどくさくて以下のように記述する必要があります。
sub vcl_recv { if (req.http.user-agent ~ "iP(hone|od)") { error 750 "Moved Temporarily"; } } sub vcl_error { if (obj.status == 750) { set obj.http.Location = "http://www.example.com/iphoneversion/"; set obj.status = 302; return(deliver); } }
このように一回エラー関数でvcl_errorに飛ばして判断する必要があります。
しかしvmod_redirectではvcl_errorに記述することなくredirectを実現しています。
import redirect; sub vcl_recv { if (req.http.user-agent ~ "iP(hone|od)") { error(redirect.location(302,"http://www.example.com/iphoneversion/") , "Moved Temporarily"); } }
これはvcl_errorの関数ポインタをフックすることにより
オリジナルのvcl_errorが呼び出される前に独自の関数を呼び出すためです。
ではフックするためのポインタは何処に定義されているでしょうか?
vmodで使うsturct sess構造体(sp)に含まれています。
vcl_errorの場合 sp->vcl->error_func vcl_deliverの場合 sp->vcl->deliver_func ...
ここのポインタを書き換えることでVarnishが呼び出すアクションを変更することが可能です。
そのことでオリジナルのアクションを呼び出す前後で任意の処理を行うことができ非常に便利です。
実際に書き換える場合はこのようなコードで行います
static vcl_func_f *vmod_redirect_Hook_vcl_error = NULL; static pthread_mutex_t vmod_redirect_mutex = PTHREAD_MUTEX_INITIALIZER; static unsigned hook_done = 0; static int vmod_Hook_vcl_error(struct sess *sp){ ....//任意の処理を書く //オリジナルのvcl_errorを呼び出してリターン return(vmod_redirect_Hook_vcl_error(sp)); } int vmod_location(struct sess *sp, int status, const char*p,...) { //VCLのreloadでフックが外れた場合の検知 if(hook_done == 1 && sp->vcl->error_func != vmod_redirect_Hook_vcl_error ) hook_done = 0; if(hook_done == 0){ //別のスレッドが同時に書き込みをしないようにロックする AZ(pthread_mutex_lock(&vmod_redirect_mutex)); if(hook_done == 0) { //退避先にオリジナルのポインタを保存 vmod_redirect_Hook_vcl_error = sp->vcl->error_func; //フックする sp->vcl->error_func = vmod_Hook_vcl_error; hook_done = 1; } //ロック開放 AZ(pthread_mutex_unlock(&vmod_redirect_mutex)); } .... return (status); }
ポイントとして初回のvmod関数呼び出し時にフックします。
これはvmod_Initではフックをするために必要な構造体へのアクセス方法が無いためです。
またmutexを利用してロックしていますが、これは複数のスレッドが同時にフックを行おうとしてフック関数でループするのを防ぐためです
ロックを行わないと、上図の(6)でオリジナルのポインタとおもって、フック後のポインタを取得してしまいます。
二回0チェックしているのは、ロックをかける間に他のスレッドが書き込んでいないことを確認するためです。
このようにフックを使うことでよりVMODの表現が増えるんじゃないかなと思います。
参考になれば幸いです
注意
varnishadmのvcl.useとvcl.discardを使わないでください。
vcl.useの場合は直前のVCLのメソッドが呼ばれ、vcl.discardはセグフォを起こす危険性があります
—-
2012-08-02追記
VCLのリロードを行った時にフックが外れるので検知用コードを追加(hook_done)