はじめに
こんにちは、メルカリのネットワークチームでエンジニアをしているkakkyです。
普段は主にCDN領域を担当しており、新規サービスで使うCDN環境の構築や既存環境の動作内容のカスタマイズ、各種契約周りの業務などをやっています。
今回はUS版メルカリのバックエンド環境をFastlyのDirectorと呼ばれるLoad balance機能を使って、無停止で移行した方法について解説させていただきます。
なお、説明をシンプルにするために実際の設定内容とは少し異なる箇所がありますので予めご了承ください。
移行環境について
少し前に、US版メルカリではFastlyのバックエンドとなっているバックエンド環境を旧環境から新環境へと移行させるプロジェクトが開始されました。メルカリではUS側のCDNの運用もJPの私達のチームでやっていますので、ある日USのメンバーから達成方法についての相談がきました。
相談内容は以下のとおりです。
- 旧環境と新環境でバックエンドは同じドメインを使うようにしたい
- 可能な限りダウンタイムは無くしたい
- 旧環境と新環境を一度に切り替えるのではなく少しずつ新環境へ流量を増やしていきたい
図で表すと以下のイメージです。
この依頼をうけて、どうやってこの内容を実現するかを検討しました。
CDNでどう実現するか
旧環境と新環境両方で同じドメインを使いたいということでしたので、Fastlyのバックエンドサーバに関する設定はバックエンド環境のエンドポイントに割り当てられているIPアドレスを直書きする形で設定をしました。US版メルカリのバックエンドサーバで利用しているドメインはRoute53を使って管理をしているので、Route53のWeighted Routingを利用してはどうかという案も上がりました。しかしDNSレコードのキャッシュはFastlyのコンテンツキャッシュサーバがもってしまうため、意図した割合でトラフィックが振り分けられない可能性があるためこの方法を選択しました。
Terraformでこのbackend設定を既存のものを含めて書くと以下のようになります。ssl_cert_hostname
と ssl_sni_hostname
は適切なドメイン情報を設定します。
ちなみに、メルカリがFastlyの設定をTerraformを用いて管理をしている背景や今後の展望について、同じチームのkanemaruが別の記事を掲載していますので、よければそちらも御覧ください
# Current backend setting
backend {
name = "current-backend"
address = "<FQDN>"
port = 443
weight = 100
use_ssl = true
ssl_check_cert = true
ssl_cert_hostname = "<SSL certification hostname>"
ssl_sni_hostname = "<SSL SNI hostname>"
(中略)
auto_loadbalance = false
error_threshold = 0
}
# [Add] Temporary backend for US migration (Old endpoint)
backend {
name = "target-to-old-us-prod"
address = "<Old environment’s IP address>"
port = 443
weight = 100
use_ssl = true
ssl_check_cert = true
ssl_cert_hostname = "<SSL certification hostname>"
ssl_sni_hostname = "<SSL SNI hostname>"
(中略)
auto_loadbalance = false
error_threshold = 0
}
# [Add] Temporary backend for US migration (New endpoint)
backend {
name = "target-to-new-us-prod"
address = "<New environment’s IP Address>"
port = 443
weight = 100
use_ssl = true
ssl_check_cert = true
ssl_cert_hostname = "<SSL certification hostname>"
ssl_sni_hostname = "<SSL SNI hostname>"
(中略)
auto_loadbalance = false
error_threshold = 0
}
次に、可能な限りダウンタイムなく旧環境と新環境へのWeighted Routingをしたいとのことでしたので、こちらはFastlyのLoad balancing機能(Director)を使って実現させることにしました。
Directorとは、複数のバックエンドサーバ設定をグルーピングして設定した割合に応じてアクセスを分散させる機能です。
この設定をFastly VCL(以下VCLと記載)のinit部分に追加する設定をTerraform上で加えます。
今回はFastlyのSnippet機能を使ってDirectorの設定を追加しました。
Snippetというのは、指定したVCL Function内に指定した順番で任意のVCLコードを追加することのできる機能です。
下記コードではVCLのinit部分にPriorityを10とした旧環境99%、新環境1%の割合でトラフィックを振り分けるDirectorを設定するVCLコードが記載されています。
snippet {
name = "Create Director for US prod"
type = "init"
priority = 10
content = chomp(<<EOS
director lb_for_us_prod client {
{
.backend = F_target_to_old_us_prod;
.weight = 99;
}
{
.backend = F_target_to_new_us_prod;
.weight = 1;
}
}
EOS
)
}
ちなみに、Directorは割合を0%にした設定はすることが出来ない点に注意が必要です。かならず全てのバックエンドにトラフィックが流れる必要があります。
また、Directorにはいくつかモードがありますが、今回は client
モードというSticky sessionに対応したモードを利用しています。デフォルトではクライアントのIPアドレスを参照して振り分け先を選択していますが、もしIPアドレス以外(例えばCookieのfooというキー)を使って振り分け先を選択させたい場合、vcl_recv
部分に以下のような設定を追加する必要があります。
# Use foo as a identity
set client.identity = req.http.cookie:foo;
最後に、このDirectorを利用するための設定をvcl_recv内に設定すればDirectorの設定は完了です。
snippet {
name = "Set Director for US prod"
type = "recv"
priority = 1000
content = chomp(<<EOS
# Set Director as default backend
# set client.identity = req.http.cookie:user_id; # Default identity is "client.ip"
set req.backend = lb_for_us_prod;
EOS
)
}
Snippetを使った時に起きる問題について
バックエンド設定の意図しない上書きの発生
以上でDirectorの設定自体はひとまず完了しました。しかしながら、Fastlyのsnippet機能を利用して設定する場合に問題が発生します。
Fastlyでは独自のVCL設定を追加する場合、2つのやり方があります。ひとつは今まで書いてきたsnippet機能を使ってパーツ単位で設定を行い、Fastlyに最終的なVCLをビルドさせる方法。そしてもうひとつは利用側がカスタマイズ内容を1つのVCLファイルに全て記載し、Fastlyのビルド機能を利用しない方法です。
FastlyにVCLをビルドさせる場合、snippetが展開される箇所はVCL上の特別なディレクティブが書かれた箇所になります。
ここではvcl_recv部分を例に見てみましょう。元となるFastlyのカスタムVCL用テンプレート(Boilerplate)はここからダウンロードすることが出来ます。
sub vcl_recv {
#FASTLY recv
# Normally, you should consider requests other than GET and HEAD to be uncacheable
# (to this we add the special FASTLYPURGE method)
if (req.method != "HEAD" && req.method != "GET" && req.method != "FASTLYPURGE") {
return(pass);
}
# If you are using image optimization, insert the code to enable it here
# See https://developer.fastly.com/reference/io/ for more information.
return(lookup);
}
vcl_recvとはクライアントからFastlyにアクセスが来た際(Receiveした際)、最初に動作する関数部分です。このコードを見てみると関数内の一番最初に#FASTLY recv
と書かれた行があるのがわかります。これはFastlyの独自マクロであり、Snippetがこの箇所に展開されるようになっています。カスタムVCLを使えば、さらにそこから自由に(一定の制限の範囲内でですが)独自の処理を好きな箇所に追加していくことが出来ます。
しかし、Fastlyの独自マクロ内で展開されるSnippet等の設定は任意の位置に任意のコードを置くことができません。。snippet同士など同じ設定項目であればpriority値を使って設定される順番のコントロールが可能ですが、それ以上のことはできません。そこが今回問題となるところです。
先ほど紹介した、Set Director for US prod
というsnippetでバックエンドとしてDirectorを指定したVCLコードを追加しています。しかし、今の設定のままでは最終的にFastlyが自動的に挿入するコードによって既存のバックエンド設定が上書き利用されてしまいます。
Fastlyが展開したVCLの例を見てみましょう。
sub vcl_recv {
# Inserting Snippet
# Set Director as default backend
# set client.identity = req.http.cookie:user_id; # Default identity is "client.ip"
set req.backend = lb_for_us_prod;
# Default Conditions (Auto generate)
set req.backend = F_current_backend; # ここで上書きされてしまう
# Inserting Request Condition
...
}
このようにSnippet部分でDirectorをバックエンドとする設定が、Fastlyのシステムで自動生成されるDefault Conditions部分で上書きされてしまいます。そのため、Default Conditions部分でバックエンド設定が追加されないようにする必要があります。
解決方法
では、この問題を解決するにはどうすればいいのでしょうか。
答えは、"Fastlyの全backend設定にrequest condition設定を追加してあげれば良い"です。
Request Conditionの設定は本来であれば特定のURLパスやヘッダ、Cookieの値などの条件を指定することで利用するBackendを振り分けるために使うのですが、今回はバックエンドが常に使われないようにするために利用します。
今回の移行案件中、Directorがバックエンドサーバにリクエストを割り振るため、Director以外のbackendはリクエストを受け持つ必要がありません。なので、全てのバックエンドサーバに直接アクセスが来ない設定をしても特に問題はありません。
これを実現するには常にそれぞれのバックエンドが使われるかどうかの条件がfalseとなるようにRequest Conditionを設定します。具体的にTerraformのコードで示すと以下のようになります。
# Current backend setting
backend {
name = "current-backend"
address = "<FQDN>"
port = 443
weight = 100
use_ssl = true
ssl_check_cert = true
ssl_cert_hostname = "<SSL certification hostname>"
ssl_sni_hostname = "<SSL SNI hostname>"
(中略)
auto_loadbalance = false
error_threshold = 0
request_condition = "Proxy to current backend" # Request Condition設定を追加
}
# バックエンド(current-backend)のRequest Condition内容を設定
condition {
name = "Proxy to current backend"
priority = 10
statement = false
type = "REQUEST"
}
# [Add] Temporary backend for US migration (Old endpoint)
backend {
name = "target-to-old-us-prod"
address = "<IP address>"
port = 443
weight = 100
use_ssl = true
ssl_check_cert = true
ssl_cert_hostname = "<SSL certification hostname>"
ssl_sni_hostname = "<SSL SNI hostname>"
(中略)
auto_loadbalance = false
error_threshold = 0
request_condition = "Proxy to old US prod" # Request Condition設定を追加
}
# バックエンド(target-to-old-us-prod)のRequest Condition内容を設定
condition {
name = "Proxy to old US prod"
priority = 20
statement = false
type = "REQUEST"
}
# [Add] Temporary backend for US migration (New endpoint)
backend {
name = "target-to-new-us-prod"
address = "<IP Address>"
port = 443
weight = 100
use_ssl = true
ssl_check_cert = true
ssl_cert_hostname = "<SSL certification hostname>"
ssl_sni_hostname = "<SSL SNI hostname>"
(中略)
auto_loadbalance = false
error_threshold = 0
request_condition = "Proxy to new US prod" # Request Condition設定を追加
}
# バックエンド(target-to-new-us-prod)のRequest Condition内容を設定
condition {
name = "Proxy to new US prod"
priority = 30
statement = false
type = "REQUEST"
}
上記コードで示したように、Terraformのbackend blockの設定には request_condition
という設定を、さらにそこに書いた文字列と完全一致するnameを持った condition
というブロックを追加します。 condition
の中にある statement
という部分がそのバックエンドを使うかどうか判定する際の条件式です。このように全てのbackend設定にstatementが常にfalseとなる request_condition
設定が含まれる状態にすることで、先に述べたDefault Conditions部分で既定のバックエンドサーバが指定される事がなくなるので、自分たちでsnippetを使って指定したDirectorが既定のものとして使われるようになります。
この設定を追加後にFastlyが展開するコードは以下のようになります。
sub vcl_recv {
# Inserting Snippet
# Set Director as default backend
# set client.identity = req.http.cookie:user_id; # Default identity is "client.ip"
set req.backend = lb_for_us_prod;
# Default Conditions (Auto generate)
# 既定のバックエンド設定(set req.backend = F_current_backend;)が自動生成されなくなり
# 上のSnippet部分で設定したlb_for_us_prodが既定のものとして使われる
# Inserting Request Condition
...
}
またこの時気をつけないといけないのが、statementの条件が同じだからといって、1つのcondition設定を複数のバックエンド設定内のrequest_condition部分で利用することはできないという点です。かならず、別々のCondition設定を追加する必要があります。
ここまでの設定を適用することで、自分たちが意図した動作をしてくれるようになりました。
最終的な移行の流れとしては、以下のようにすることで無停止での切り替えが可能となります。
- 既存のバックエンド設定から、Directorを使ったLoad balance通信に切り替える
- Director設定内のweightの数値を何度か変更しながら少しずつ新しい環境へのトラフィックを切り替える
- ドメインは変えずにDirectorは指示したIPアドレスのバックエンドへアクセスを流す
- バックエンドサーバのドメイン設定(Aレコード)を古い環境から新しい環境のものに事前に切り替えておく
- IPアドレスによる通信状態なので、事前に修正しておいても問題はない
- Directorの設定の削除と同時に新しい環境向けのバックエンド設定のみの状態にする
- この時、既存のバックエンド設定に残っているrequest_conditionの設定行も削除する (そうしないと利用できるバックエンドが1つも無い状態になる)
FastlyのOrigin Inspectorについて
今回、段階的にトラフィックをシフトしていったのですが、この時実際にトラフィックがどのように移っていっているか、また新しい環境に移ったトラフィックが正常にStatus Code 200を返しているのかをスピーディーに確認する必要がありました。
この時に、Fastly側のリアルタイムな通信状態を確認するためにFastlyが提供しているOrigin Inspectorがとても役に立ちました。これはリアルタイムでバックエンドサーバへのトラフィック量やクライアントへ返しているStatus Codeを確認することができる機能です。
これを使うことで、Directorのweightを変えた時や、完全にアクセスを新しい環境へと切り替えた時に意図したとおりにトラフィックが切り替わっているかを即時に確認することが出来ました。以下の画像は実際に全てのトラフィックを新しい環境へと向ける設定を入れたときのものです。
前項までに説明した移行方法とこのOrigin Inspectorを使ったリアルタイムでの通信状況確認により、特に大きな問題が発生することなく私達は無事に新規環境への切り替えを完了させることが出来ました。
さいごに
今回はUS版メルカリのバックエンド環境を無停止で移行した方法を紹介しました。
Default conditionsの問題やrequest_condition設定を書く上で気をつけなければいけない部分などもありますが、一度把握してしまえば比較的容易にDirectorを利用することができるのではないでしょうか。
今回はsnippetやrequest_conditionを使ったやり方を紹介しましたが、本文中で少し触れたカスタムVCLを使った別のやり方も考えられるかと思います。カスタムVCLを使うと今回やろうとしていたこと以外にもかなり自由度の高いカスタマイズができるので、また別の機会に何か紹介できればと思っています。
チームメンバーを募集しています
株式会社メルカリでは、一緒に会社とサービスを盛り上げていってくれる人を探しています。
私の所属するネットワークチームも、サービスの根幹を担っているチームの1つですので非常にバラエティ豊富な相談が日々やってくるため刺激的な仕事ができると思います。
少しでも興味を持って頂けたら、下記のリンク先からぜひご連絡ください。
インターンシップも受け付けていますので、学生の皆さんもぜひ一度ご覧になってみてください。