Nginx内でLaunchDarklyのフラグを使って、アプリケーションのメンテナンスモードを切り分ける
お疲れ様です!株式会社 CastingONEで働いているフロントエンドエンジニアの岡本です。
今回はNginx内でFeature FlagのLaunchDarklyを使って、アプリケーションのメンテナンスモードを切り分ける方法について書いていきます!
はじめに
弊社のフロントのアプリケーションはNetlifyにデプロイされており、今までのメンテナンスモードの切り分けは、NetlifyのSplit Testingの機能を活用し、メンテナンスブランチの比率を100%にすることで実現していました。
通常時 | メンテナンス時 |
---|---|
ただ、この方法のメンテナンスモードの切り分けは特殊かつ複雑で、リリース対応のメンバーが「どうやってメンテナンスモードにするんだっけ?」となってる姿をよく目にしており、もっとシンプルにメンテナンスモードを切り分ける方法はないかと考えていました。
フロントのアプリケーション層でfeature flagを使ってメンテナンスモードを切り分ける方法はどうかという話もありましたが、メンテナンスモードの責務をフロントエンドに持たせたくなかったため、インフラ層で切り分ける方法を模索していました。
色々調査していく中で、マネージャーから 「NginxからLaunchDarklyを見るのもアリですね」 という天啓にうたれ、その構成で実装を進めました⚡️
本記事では、その方法について解説していきます!
LaunchDarklyとは
弊社ではFeature Flagの管理にLaunchDarklyを使っています。LaunchDarklyは、Feature Flagを使ってアプリケーションの機能のオン・オフを制御できるサービスです。これにより、デプロイされたコードとは独立して機能のリリースやロールバックが可能になります!
LaunchDarklyではLua ServerSide SDKを提供しています。そしてNginxを拡張したWebサーバであるOpenRestyはngx_luaをベースにしているため、Lua ServerSide SDKと組み合わせることで、Nginx内でフラグの状態をチェックし、リクエストの振り分けを行うことが可能になりました!
インフラ構成
弊社のインフラはGoogle Cloudを使っており、以下のような構成にしました。
構成の流れは以下の通りです。
- ユーザーからのリクエスト
- ユーザーがアプリケーションにアクセスすると、まずリクエストがロードバランサに送られます。
- Cloud Run(Nginx)での処理
- リクエストはロードバランサからCloud RunにデプロイされているNginxに転送されます。
- Nginxはリバースプロキシとして機能し、リクエストをどこに転送するかを決定します。
- LaunchDarklyによるメンテナンスフラグの制御
- NginxはLaunchDarklyのメンテナンスフラグの状態をチェックし、フラグの状態に応じてリクエストの振り分けを行います。
- フラグがOFFの場合は、通常のアプリケーション(Netlifyにホストされている)にリクエストを転送します。
- フラグがONの場合は、メンテナンスページにリクエストを転送します。
この構成を元に、nginx.conf
とCloud RunにデプロイするNginxのDockerfile
を作成しました!
LaunchDarklyの設定
まずは、LaunchDarklyでメンテナンス切り分け用のフラグを作成します。Flagsの設定画面の右上からCreate Flag
ボタンをクリックし、フラグを作成していきます。作成手順は以下の通りです。
- Detailsブロックの Name と key にフラグ名を入力
- Configurationブロックの Flag Type で Stringを選択し、Variationsに通常のアプリケーションのURLと、メンテナンスページのURLを入力し、Default VariationでフラグがONのとき、OFFのときに、どのvalueを返すかを設定
- サンプルとして、アプリケーションのURLには弊社の採用ページ、メンテナンスページのURLにはexample.comを使用しています。
- サンプルとして、アプリケーションのURLには弊社の採用ページ、メンテナンスページのURLにはexample.comを使用しています。
- 右下の
Create flag
ボタンをクリックしてフラグを作成
Nginxの設定
次に、Nginxの設定のために必要なファイルを作成します。LaunchDarklyが提供しているlua-server-sdkのexamplesのhello-nginxを参考に作成していきます。今回使用するサンプルコードのレポジトリを置いておきます。
nginx.conf
nginx.conf
events {
worker_connections 1024;
}
env LAUNCHDARKLY_SDK_KEY;
env LAUNCHDARKLY_FLAG_KEY;
env FALLBACK_DOMAIN;
http {
resolver 8.8.8.8;
lua_package_path ";;/usr/local/openresty/nginx/scripts/?.lua;";
init_worker_by_lua_file scripts/shared.lua;
server {
location / {
default_type text/html;
set $upstream "";
rewrite_by_lua_block {
local os = require("os")
local ld = require("launchdarkly_server_sdk")
local client = require("shared")
local get_from_env_or_default = require("get_from_env_or_default")
-- cookieのbypass_tokenというkeyに保存されている値を取得
local token = ngx.var.cookie_bypass_token
-- LaunchDarklyのcontextを作成
local context = ld.makeContext({
-- メンテナンスモードの突破用のConxtext kindを設定
bypass_token = {
key = "dynamic-proxying",
attributes = {
token = token
}
}
})
ngx.var.upstream = client:stringVariation(context, get_from_env_or_default("LAUNCHDARKLY_FLAG_KEY", ""), get_from_env_or_default("FALLBACK_DOMAIN", ""))
}
proxy_http_version 1.1;
proxy_pass https://$upstream;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
環境変数でLaunchDarklyのSDKキーとフラグのキーを受け取り、それを元にLaunchDarklyのcontextを作成し、フラグの状態に応じてリクエストの振り分けを行うようにしています。
後述しますが、メンテナンスモード時に開発メンバーなどが特別にアクセスできるための設定として、bypass_token
という Context kind を設定しています。
shared.lua
shared.lua
local ld = require("launchdarkly_server_sdk")
local get_from_env_or_default = require("get_from_env_or_default")
-- Set MY_SDK_KEY to your LaunchDarkly SDK key. To specify the SDK key as an environment variable instead,
-- set LAUNCHDARKLY_SDK_KEY using '--env LAUNCHDARKLY_SDK_KEY=my-sdk-key' as a 'docker run' argument.
local MY_SDK_KEY = ""
local config = {}
return ld.clientInit(get_from_env_or_default("LAUNCHDARKLY_SDK_KEY", MY_SDK_KEY), 1000, config)
shared.lua
ファイルは、LaunchDarklyのSDKを初期化するためのファイルです。SDKキーを環境変数から取得し、SDKを初期化しています。
Dockerfile
Dockerfile
# libboostをインストールするためのビルダーイメージ
FROM ubuntu:22.04 AS builder
RUN apt-get update && apt-get install -y software-properties-common git \
&& add-apt-repository ppa:mhier/libboost-latest \
&& apt-get update && apt-get install -y libboost1.81-all-dev
RUN cd /usr/lib && git clone https://github.com/launchdarkly/lua-server-sdk.git
FROM openresty/openresty:1.25.3.2-0-jammy
# {{ x-release-please-start-version }}
ARG VERSION=2.1.1
# {{ x-release-please-end }}
ARG CPP_SDK_VERSION=3.5.2
RUN apt-get update && apt-get install -y \
git netbase curl libssl-dev apt-transport-https ca-certificates \
software-properties-common \
cmake ninja-build locales-all
RUN add-apt-repository ppa:mhier/libboost-latest && \
apt-get update && \
apt-get install -y boost1.81
RUN mkdir cpp-sdk-libs
RUN git clone --branch launchdarkly-cpp-server-v${CPP_SDK_VERSION} https://github.com/launchdarkly/cpp-sdks.git && \
cd cpp-sdks && \
mkdir build-dynamic && \
cd build-dynamic && \
cmake -GNinja \
-DLD_BUILD_EXAMPLES=OFF \
-DBUILD_TESTING=OFF \
-DLD_BUILD_SHARED_LIBS=ON \
-DLD_DYNAMIC_LINK_OPENSSL=ON .. && \
cmake --build . --target launchdarkly-cpp-server && \
cmake --install . --prefix=../../cpp-sdk-libs
RUN mkdir -p /usr/local/openresty/nginx/scripts
COPY /usr/lib/lua-server-sdk .
COPY nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
COPY shared.lua /usr/local/openresty/nginx/scripts/
COPY /usr/lib/lua-server-sdk/examples/env-helper/get_from_env_or_default.lua /usr/local/openresty/nginx/scripts/
RUN luarocks make launchdarkly-server-sdk-"${VERSION}"-0.rockspec LD_DIR=./cpp-sdk-libs && \
cp launchdarkly_server_sdk.so /usr/local/openresty/lualib/
# COPY --from=builder /usr/lib/x86_64-linux-gnu/libboost_*.so* /usr/lib/ # --plat-form=linux/amd64の場合
COPY /usr/lib/aarch64-linux-gnu/libboost_*.so* /usr/lib/
RUN ln -s /usr/lib/libboost_json.so.1.81.0 /usr/lib/libboost_json-mt-x64.so.1.81.0 && \
ln -s /usr/lib/libboost_url.so.1.81.0 /usr/lib/libboost_url-mt-x64.so.1.81.0
CMD ["nginx", "-g", "daemon off;"]
基本的に、hello-nginxのDockerfileを参考にしていますが、自分の環境だとビルドエラーになったため、少し変更しています。マルチビルドステージを使って、ビルダーステージでlibboostをインストール&lua-server-sdkをクローンし、メインステージでそれらを使いNginxの設定を行っています。
ビルドして、フラグを切り替えてみる
以下のコマンドでDockerイメージをビルドし起動します。LaunchDarklyのSDKキーは、LaunchDarklyの管理画面から取得してください。
docker build -t launchdarkly-nginx .
docker run --rm -p 8333:80 -e LAUNCHDARKLY_SDK_KEY="sdk-key" -e LAUNCHDARKLY_FLAG_KEY="maintenance-flag-test" launchdarkly-nginx
実際にフラグを切り替えて、ページが切り替わるか確認してみます。
上の画像のように、フラグを切り替えることで、通常のアプリケーションとメンテナンスページを切り替えることができました🎉 しかも切り替わる速度も早いですね!!
メンテナンスモードの突破機構を実装
上記の設定で、LaunchDarklyを使ってメンテナンスモードの切り替えができるようになりましたが、このままだと開発メンバーなどがメンテナンス時にアプリケーションにアクセスできません。これを突破するための機構として、LaunchDarklyの Contexts 機能を使用します。
Contextsとは
Contextsは、LaunchDarklyのフラグが実際に使用されたときに、フラグの評価に影響を与える追加の情報をもとにターゲティングや条件を詳細に設定できる機能です。Contextsを利用すると、ユーザーだけでなく、デバイス、アカウント、トークンなど、様々な属性を持ったエンティティを使って機能フラグの評価を行うことが可能になります。
Contextには必ず一つの Context kind が含まれます。Context kindとは、そのContextがどのような種類の情報を表しているかを示すものです。
nginx.conf
の、以下の部分でContexts、Context kindを設定しています。
-- cookieのbypass_tokenというkeyに保存されている値を取得
local token = ngx.var.cookie_bypass_token
-- LaunchDarklyのcontextを作成
local context = ld.makeContext({
-- メンテナンスモードの突破用のConxtext kindを設定
bypass_token = {
key = "dynamic-proxying", -- Context kindにkeyは必須
attributes = {
token = token
}
}
})
LaunchDarklyのSDKからmakeContext
を使って、その引数に設定したい情報を渡し、Contextを作成しています。ここでは、bypass_token
というContext kindを設定し、その識別情報としてkey
と追加の属性attributes
にtoken
を設定しています。
上記の設定後、このフラグが実際に使用されると、LaunchDarklyの管理画面の Contexts ページに、以下のようにbypass_token
というContext kindが表示されます。
フラグ突破のためのルールを追加
作られたContext kindを使って、メンテナンスモードを突破するためのルールをフラグに追加します。
ルールを追加するには、フラグの設定画面の Rules セクションのAdd rule
ボタンをクリックし、Build a custom rule
を選択し、以下の画像のように設定します。
この設定を追加することにより、nginx.conf
で設定していた、bypass_token
というContext kindのAttributesのtoken
がsample_token
の場合、フラグがONでもアプリケーションが表示されるようになります。token
は、nginx.conf
の以下の部分でcookieから取得するようにしています。
local token = ngx.var.cookie_bypass_token
ngx.var.cookie_bypass_token
は、Nginxのngx
モジュールを使って、HTTPリクエストに含まれるbypass_token
というcookieの値を取得します。ngx.var
は、Nginxの変数にアクセスするためのプロパティで、cookie_
というプレフィックスをつけることで、cookieの値にアクセスできます。
上記の設定後、再度Dockerイメージを起動して、cookieにbypass_token: sample_token
を設定してアクセスすると、メンテナンスフラグがONの状態でもアプリケーションが表示されるようになります!
おわりに
いかがでしたでしょうか? 以上がNginx内でLaunchDarklyのフラグを使って、アプリケーションのメンテナンスモードを切り分ける方法についての記事でした!
弊社ではまだテスト段階ですが、実際にビルドしたイメージをCloud Runにデプロイして、本番環境で運用していく予定です。
自分はフロントエンジニアで、インフラ周りの知識が全然と言っていいほどありませんでしたが、マネージャーやバックエンドエンジニアの方に助けてもらいながら実装を進めさせていただき、めちゃくちゃいい経験ができました!slackのtimesチャンネルで、うまくいかないことを嘆いていたら、色んなエンジニアが助けてくれて、いい開発チームだなぁと思いました😊
Discussion