はじめに
サービス利用者への影響を最小限にすることを目的に、新しいバージョンを段階的にサービスインさせるカナリアリリース(カナリアデプロイメントと呼ばれることもある)というデプロイ戦略をご存知の方は多いかと思います。サービス開発において、アプリケーションの冗長化や負荷分散のために上位にロードバランサーを配置することは一般的なので、ロードバランサーで重み付けによるトラフィック振り分けが可能であれば、それだけでカナリアリリースを実現できます。しかし、ロードバランサーの機能不足や、ロードバランサー運用者とサービス開発者が異なることによる連携負荷など、様々な事情によりそれが難しいケースも存在します。
このような背景を受けて、この記事ではロードバランサーとアプリケーションの間に TCP ロードバランサーとして機能する Nginx を配置してカナリアリリースを実現する方法を紹介します。
TCP/UDP Proxy Module
Nginx を TCP ロードバランサーとして稼働させるためには、TCP/UDP Proxy Module を使用する必要があります。しかし、公式から提供されている Nginx バイナリには TCP/UDP Proxy Module が組み込まれていないため、Building NGINX From Source を参考にソースコードから自前でビルドする必要があります。
ビルド手順
公式サイトからソースコードをダウンロードします。
cd /usr/local/src
sudo wget https://nginx.org/download/nginx-1.21.0.tar.gz
sudo tar -xf nginx-1.21.0.tar.gz
cd nginx-1.21.0
ビルドに必要なライブラリをインストールします。
sudo yum install gcc
sudo yum install pcre-devel
sudo yum install openssl-devel
Nginx 実行用のシステムグループとシステムユーザーを作成します。
sudo groupadd --system nginx
sudo useradd --system --shell /usr/sbin/nologin --gid nginx nginx
必要なオプションを付与して Nginx バイナリをビルドします。
sudo ./configure \
--sbin-path=/usr/sbin/nginx \
--conf-path=/etc/nginx/nginx.conf \
--pid-path=/var/run/nginx.pid \
--error-log-path=/var/log/nginx/error.log \
--http-log-path=/var/log/nginx/access.log \
--user=nginx \
--group=nginx \
--with-stream
sudo make
sudo make install
Nginx を TCP ロードバランサーとして使用する際に明示的に組み込む必要がある Module は以下のようになっていますが、今回は必要最低限として TCP/UDP Proxy Module を組み込むオプションのみを指定しています。
$ ./configure --help | grep with-stream
--with-stream enable TCP/UDP proxy module
--with-stream=dynamic enable dynamic TCP/UDP proxy module
--with-stream_ssl_module enable ngx_stream_ssl_module
--with-stream_realip_module enable ngx_stream_realip_module
--with-stream_geoip_module enable ngx_stream_geoip_module
--with-stream_geoip_module=dynamic enable dynamic ngx_stream_geoip_module
--with-stream_ssl_preread_module enable ngx_stream_ssl_preread_module
NGINX systemd service file を参考に systemd の Unit ファイルも自前で用意します。
cat << EOF | sudo tee /lib/systemd/system/nginx.service
[Unit]
Description=The NGINX HTTP and reverse proxy server
After=syslog.target network-online.target remote-fs.target nss-lookup.target
Wants=network-online.target
[Service]
Type=forking
PIDFile=/var/run/nginx.pid
ExecStartPre=/usr/sbin/nginx -t
ExecStart=/usr/sbin/nginx
ExecReload=/usr/sbin/nginx -s reload
ExecStop=/bin/kill -s QUIT \$MAINPID
PrivateTmp=true
[Install]
WantedBy=multi-user.target
EOF
Split Clients Module
Nginx でカナリアリリースをするためには Split Clients Module を使用する必要があります。こちらは TCP/UDP Proxy Module とは異なりデフォルトで Nginx バイナリに組み込まれているため、自前でのビルドは不要となります。
Split Clients Module は、MurmurHash2 を使用して第一引数で指定された変数(Core Module でサポートされている変数は こちら)のハッシュ値を求めて、その値から割合に応じた値を返却する仕様のため、クライアントの IP アドレスが格納される $remote_addr
を指定するのが良いかと思います。
split_clients "${remote_addr}" $variant {
10% one;
20% two;
* other;
}
蛇足ですが、以下の記事で Split Clients Module は map ディレクティブの応用であることが知れて勉強になりました。
設定ファイル例
以下は、version1
グループ(192.168.100.10, 192.168.100.11, 192.168.100.12)にトラフィックの30%を、version2
グループ(192.168.100.20, 192.168.100.21, 192.168.100.22)にトラフィックの70%を振り分ける設定となっています。
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
stream {
log_format basic '[$time_local] $remote_addr:$remote_port -> $upstream_addr '
'$protocol $status $bytes_sent $bytes_received '
'$session_time';
access_log /var/log/nginx/access.log basic;
split_clients $remote_addr:$remote_port $upstream {
30% version1;
* version2;
}
upstream version1 {
server 192.168.100.10:8080;
server 192.168.100.11:8080;
server 192.168.100.12:8080;
}
upstream version2 {
server 192.168.100.20:8080;
server 192.168.100.21:8080;
server 192.168.100.22:8080;
}
server {
listen 8080;
proxy_pass $upstream;
}
}
なお、ログの設定は ngx_stream_log_module Module の例を参考にしています。ログフォーマットとして使用できる変数は以下で定義されているので、要件によって設計するのが良いかと思います。
- https://nginx.org/en/docs/stream/ngx_stream_core_module.html#variables
- https://nginx.org/en/docs/stream/ngx_stream_upstream_module.html#variables
設定ファイルが用意できたら、以下のコマンドで Nginx を起動します。
sudo systemctl daemon-reload
sudo systemctl enable nginx
sudo systemctl start nginx
あとは、上位のロードバランサーから Nginx にトラフィックを流せば完成です。カナリアリリースを開始した後は、それぞれのバージョンのアプリケーションメトリクスや、サービス利用者への影響を確認しながら、割合を変更していくのが良いかと思います。
さいごに
今回は Nginx を TCP ロードバランサーとして稼働させてカナリアリリースを実現する方法を紹介しました。ニッチなナレッジかもしれませんが、どなたかの参考になれば幸いです。
参考資料
- https://www.nginx.com/blog/nginx-and-devops-methodologies-go-hand-in-hand/
- https://www.digitalocean.com/community/tutorials/how-to-target-your-users-with-nginx-analytics-and-a-b-testing
- https://docs.nginx.com/nginx/admin-guide/load-balancer/tcp-udp-load-balancer/
- https://engineering.mercari.com/blog/entry/2016-08-17-170114/