⚙️

Nginx内でLaunchDarklyのフラグを使って、アプリケーションのメンテナンスモードを切り分ける

2024/11/01に公開

お疲れ様です!株式会社 CastingONEで働いているフロントエンドエンジニアの岡本です。

今回はNginx内でFeature FlagのLaunchDarklyを使って、アプリケーションのメンテナンスモードを切り分ける方法について書いていきます!

はじめに

弊社のフロントのアプリケーションはNetlifyにデプロイされており、今までのメンテナンスモードの切り分けは、NetlifyのSplit Testingの機能を活用し、メンテナンスブランチの比率を100%にすることで実現していました。

通常時 メンテナンス時
'通常時の構成図' 'メンテナンス時の構成図'

ただ、この方法のメンテナンスモードの切り分けは特殊かつ複雑で、リリース対応のメンバーが「どうやってメンテナンスモードにするんだっけ?」となってる姿をよく目にしており、もっとシンプルにメンテナンスモードを切り分ける方法はないかと考えていました。

フロントのアプリケーション層でfeature flagを使ってメンテナンスモードを切り分ける方法はどうかという話もありましたが、メンテナンスモードの責務をフロントエンドに持たせたくなかったため、インフラ層で切り分ける方法を模索していました。

色々調査していく中で、マネージャーから 「NginxからLaunchDarklyを見るのもアリですね」 という天啓にうたれ、その構成で実装を進めました⚡️

本記事では、その方法について解説していきます!

LaunchDarklyとは

弊社ではFeature Flagの管理にLaunchDarklyを使っています。LaunchDarklyは、Feature Flagを使ってアプリケーションの機能のオン・オフを制御できるサービスです。これにより、デプロイされたコードとは独立して機能のリリースやロールバックが可能になります!

https://launchdarkly.com/


LaunchDarklyではLua ServerSide SDKを提供しています。そしてNginxを拡張したWebサーバであるOpenRestyはngx_luaをベースにしているため、Lua ServerSide SDKと組み合わせることで、Nginx内でフラグの状態をチェックし、リクエストの振り分けを行うことが可能になりました!

https://docs.launchdarkly.com/guides/sdk/nginx

インフラ構成

弊社のインフラはGoogle Cloudを使っており、以下のような構成にしました。

'新しいインフラ構成図'

構成の流れは以下の通りです。

  1. ユーザーからのリクエスト
    • ユーザーがアプリケーションにアクセスすると、まずリクエストがロードバランサに送られます。
  2. Cloud Run(Nginx)での処理
    • リクエストはロードバランサからCloud RunにデプロイされているNginxに転送されます。
    • Nginxはリバースプロキシとして機能し、リクエストをどこに転送するかを決定します。
  3. LaunchDarklyによるメンテナンスフラグの制御
    • NginxはLaunchDarklyのメンテナンスフラグの状態をチェックし、フラグの状態に応じてリクエストの振り分けを行います。
    • フラグがOFFの場合は、通常のアプリケーション(Netlifyにホストされている)にリクエストを転送します。
    • フラグがONの場合は、メンテナンスページにリクエストを転送します。

この構成を元に、nginx.confとCloud RunにデプロイするNginxのDockerfileを作成しました!

LaunchDarklyの設定

まずは、LaunchDarklyでメンテナンス切り分け用のフラグを作成します。Flagsの設定画面の右上からCreate Flagボタンをクリックし、フラグを作成していきます。作成手順は以下の通りです。

  1. Detailsブロックの Namekey にフラグ名を入力
    'Detailsブロック'
  2. Configurationブロックの Flag TypeStringを選択し、Variationsに通常のアプリケーションのURLと、メンテナンスページのURLを入力し、Default VariationでフラグがONのとき、OFFのときに、どのvalueを返すかを設定
    • サンプルとして、アプリケーションのURLには弊社の採用ページ、メンテナンスページのURLにはexample.comを使用しています。
      'Configurationブロック'
  3. 右下のCreate flagボタンをクリックしてフラグを作成

Nginxの設定

次に、Nginxの設定のために必要なファイルを作成します。LaunchDarklyが提供しているlua-server-sdkのexamplesのhello-nginxを参考に作成していきます。今回使用するサンプルコードのレポジトリを置いておきます。

https://github.com/okm321/dynamic-proxying-with-nginx-and-launchdarkly

nginx.conf

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
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
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 --from=builder /usr/lib/lua-server-sdk .
COPY nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
COPY shared.lua /usr/local/openresty/nginx/scripts/
COPY --from=builder /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 --from=builder /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がどのような種類の情報を表しているかを示すものです。

https://docs.launchdarkly.com/home/observability/contexts

nginx.confの、以下の部分でContexts、Context kindを設定しています。

nginx.conf
-- 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と追加の属性attributestokenを設定しています。

上記の設定後、このフラグが実際に使用されると、LaunchDarklyの管理画面の Contexts ページに、以下のようにbypass_tokenというContext kindが表示されます。
'Contextページ'

フラグ突破のためのルールを追加

作られたContext kindを使って、メンテナンスモードを突破するためのルールをフラグに追加します。
ルールを追加するには、フラグの設定画面の Rules セクションのAdd ruleボタンをクリックし、Build a custom ruleを選択し、以下の画像のように設定します。

'メンテナンス突破のルール追加'

この設定を追加することにより、nginx.confで設定していた、bypass_tokenというContext kindのAttributesのtokensample_tokenの場合、フラグがONでもアプリケーションが表示されるようになります。tokenは、nginx.confの以下の部分でcookieから取得するようにしています。

nginx.conf
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