本番環境で新機能・旧機能を自由に切り替えたい

こんにちは、開発本部でバックエンドエンジニアをしています。江間です。

IPアドレスとCookieを使って、機能の切り替えが出来る仕組みを実装したので、それについてお話します。

目次

導入の背景

1度のリリースでの変更箇所を少なくしたい

これまで変更内容が大きいリリースを行う場合、数カ月間メインのブランチから独立して作業を行ってきました。しかし、このやり方では以下の様な問題がありました。

  • 変更箇所が多いので、コンフリクトが起こりやすくなる
  • 作業ブランチ間に依存関係が生じて、ブランチの関係性が複雑になる

これらの問題を解決するには、1度のリリースの変更箇所を少なくして、メインのブランチにmergeすることです。

例として、今年6月に行った企業ページのリニューアルではいくつかの修正が加わりました。
これに伴い、企業ユーザー向けの管理画面も多くの修正が加わりました。
企業ページの新機能は管理画面に依存していたので、両方を同時にリリースすることになりました。

企業ページのリニューアルの比較画像。企業説明文やカバー画像が新たに追加されている。

結果として、リリース直後にいくつかデグレが起こったり、他のメンバーの作業とコンフリクトが起こってしまいました。

1度のリリースの変更箇所を少なくして、新機能のソースコードをあらかじめメインのブランチにmergeしていれば、上記の問題はだいぶ解消されたと思います。

そのためには、新機能と旧機能が共存(切り替え)出来る仕組みが必要でした。

他部署の人でもリリース前に変更内容を確認できるようにしたい

実装した新機能に対して、営業やCR(カスタマーリレーション)のメンバーが変更内容をキャッチアップするタイミングが限られている問題がありました。

原因としては、新機能をSTG環境などに反映させる場合、一度エンジニアを介する必要があるためです。そのため、社内メンバーはエンジニアとスケジュールを決めた期間だけが、新機能を使えるタイミングでした。

そのため、新機能がリリースされた後に、リリース前にエンジニアが気づくことが出来なかった要望やバグを耳にすることがありました。
例えば「場所によってボイス&トーンが異なる」「検索結果の表示順序が異なる」など。

開発を担当したエンジニアがPR TIMESのサービスを全て理解している訳ではないので、このようなミスが起こる事は想定できます。しかし、もし、社内メンバーがいつでも新機能を確認できる状態であれば、エンジニアが気がつくことが出来なかったミスをリリース前に発見する確率が上がっていたかと思われます。

システムデザイン

では、実際にどうやってこの仕組みを実装するのかについて説明します。

新機能を使えるようにする対象は社内のメンバーだけなので、(VPNを含む)社内のIPアドレスと、Cookieの付与によって機能を切り替えます。

社内IPから特定のCookieが送信されると、新機能が有効にされたページが表示される。

Cookie付与ページ

利用者がCookieを直接書き換える必要を無くすために、IP制限された専用のページを作成し、機能切り替えのフラグとなるCookie付与を行います。

Cookieはバックエンドだけで操作する事を想定しているので、Secure属性とHttpOnly属性をつけます。

/**
 * 新機能(name)を使うためのCookieをsetする。
 * @param string $name
 */
private static function set($name)
{
    setcookie(self::getKey($name), 1, 0, '/', '', true, true);
}

/**
 * cookieをunsetする。
 * @param string $name
 */
private static function _unset($name)
{
    setcookie(self::getKey($name), 0, time() - 3600, '/', '', true, true);
}

新機能を実装予定のページ

新機能に切り替えるかどうかは、Cookieが付与されているかの確認と、現在のIPアドレスが社内IPであるかを確認して判定します。

/**
 * 新機能(name)が使える環境かどうかを判定する。
 * @param string $name
 * @return bool
 */
private static function check($name)
{
    return self::isAllowedIP() && self::existsKeyName($name);
}

/**
 * 現在のアクセスは、アクセスが許可されたIPアドレスかどうかを判定する。
 * @return bool
 */
private static function isAllowedIP()
{
    return in_array(Util::getClientIpAddress(), self::$allow_ip_addresses);
}

/**
 * Cookieに指定されたKeyが含まれているか判定する。
 * @param string $name
 * @return bool
 */
private static function existsKeyName($name)
{
    return isset($_COOKIE[self::getKey($name)]) && $_COOKIE[self::getKey($name)] == 1;
}

考慮事項

1つ目に、誤って開発中の機能を公開してしまうリスクがあります。2つ目に、新機能が予期せぬデータの更新などを行い、障害の原因になることがあります。

いずれも、新機能と旧機能が共存する事によるリスクです。

記事で紹介している実装は、機能の切り替えのするかどうかを判定しているに過ぎず、機能の切り替えは実装者に委ねられます。新機能が本番にも反映されることを常に意識して実装する必要があります。

実用例

企業ページのフロントエンドをReactにリプレイスするプロジェクトが行われており、この記事で紹介している機能が使われています。

現在は開発途中ですが、同じURLからリプレイス後のページを表示する事が可能となっています。

実際に https://prtimes.jp/main/html/searchrlp/company_id/112 へアクセスした時の表示の比較画像。

まとめ

新機能と旧機能を、IPアドレスとCookieを使って機能切り替えをする仕組みの実装方法について紹介しました。

簡単に実装することが出来て、多くの恩恵があるので、とてもオススメです。

この記事を書いた人

株式会社PR TIMES 開発本部 バックエンドエンジニア

目次
閉じる