NissyBlog

Life goes on.

移行×並行稼働×事故なくリリース

この記事は
BrainPad Advent Calendar 2017 - Qiita5日目の記事になります。
こんにちは。@nissy0409240です。
BrainPadでエンジニアをしています。
好きな漫画は「HUNTER×HUNTER」です。
他の漫画も大好きです。

タイトルとの関連性を保つために強引に作った前置きはさておき、
本エントリーは私が担当している自社プロダクトにて行った改修内容と、
リリースに関する工夫点についてお話させて頂きます。

改修内容について

まず、そもそもどのような改修内容かについてお話させて頂きます。
今回の改修内容を一言で表せば、
「プロダクト中の設定をファイルからDBへ移行する」
という内容です。

私が担当しているプロダクトでは設定変更毎にエンジニアの運用作業が発生し、
作業毎に設定内容の変更とデプロイにエンジニアの工数が取られている状態でした。
この状況を緩和すべく設定内容をDBに持たせた後、
管理画面化してエンジニアに限らず運用作業が出来るようにする。
これが改修内容の全容です。

この改修内容全てを一回のリリースにまとめるのは
一回のリリースではスコープが大きいと判断致しました。
そこで、以下のようなフェーズを切って改修を行っております。

  • 設定をファイルからDBに移行する
  • DBへアクセス出来るAPIを作成する
  • 管理画面を作成する

24時間365日動いているプロダクトであり、
有難いことに沢山のクライアント様と関わりのあるプロダクトです。
クライアントに影響が無いように無停止でリリースすることはマストでした。

また、設定内容自体はプロダクトの多くの部分で参照されており、
設定が影響しないところを挙げる方が容易い程、
大規模な改修となることが目に見えていました。

この中で本エントリーでは
「設定をファイルからDBに移行する」
にフォーカスし、お送りさせて頂きます。

工夫点について

以上の状況を踏まえ、
この改修に際して行った工夫点を以下に記載させて頂きます。
工夫点を一言で表せば「抽象レイヤー内で条件分岐するよう改修した」
という内容です。
付け加えれば、

  • 抽象レイヤー内でファイル参照機構とDB参照機構の並行稼働を実現したこと
  • 抽象レイヤー内でホワイトリスト方式のリストにて適用範囲のハンドリングを行ったこと

と言えるでしょうか。

文章だけではどこまで伝えられ切れるか自信がないため、
以下、実装内容を元にお話させて頂きます。

実装内容

実装内容の説明に先立ちまして抽象レイヤーと呼び出し元からどの様に呼び出しているか
修正前・修正後のものを掲載させて頂きます。
なお、実際のソースコードから関数名、クラス名、変数名、実際の処理を書き換えた上で、
一部抜粋したものを記載させて頂くことを御了承下さい。

修正前

抽象レイヤー

def base_factory():
    return FactoryDatabase()


class FactoryBase(object):
    __metaclass__ = ABCMeta
    @abstractmethod
    def get_information(self, content):
        pass


class FactoryFile(FactoryBase):
    def get_information(self, content):
        return file.keys(content)

呼び出し元

def caller(self):
    base_factory().get_information(content)
修正後

抽象レイヤー

db_list = []

def base_factory():
   if content in db_list:
        return FactoryDatabase()
    else:
        return FactoryFile()


class FactoryBase(object):
    __metaclass__ = ABCMeta
    @abstractmethod
    def get_information(self, content):
        pass


class FactoryDatabase(FactoryBase):
    def get_information(self, content):
        return DataBaseConnection.get_infomation(content)


class FactoryFile(FactoryBase):
    def get_information(self, content):
        return file.keys(content)

呼び出し元

def caller(self):
    base_factory().get_information(content)

修正後の抽象レイヤーにてFactoryMethodパターンを用いて実装することで、
呼び出し元の実装は抽象レイヤーを参照してさえいれば、
何も変えることなく参照先をファイルからDBへと切り替えられるようになっています。

合わせて修正後のソースコード中にdb_listというリストを用意しています。
このリストの中に含まれる設定のみデータベースから取得し、
残りはファイルから取得するようにしたことで、
修正前後の内容の並行稼働を実現致しました。

リリース計画

先ほどの実装で並行稼働を実現させたとありますが、
ただ並行稼働させたのではありません。
目的と致しましては、
負荷状況のモニタリングと全体リリース前のリスクヘッジ
という二つの目的がありました。

まず、負荷状況のモニタリングについてお話させて頂きます。
繰り返しになりますが、
今回の修正は「プロダクト中の設定内容をファイルからDBへ移行する」という内容です。
設定内容の参照のされ方を元に事前にキャパシティプランニングは充分に行っていましたが、
万が一、DBへのアクセスが想定以上に多くなり、
システムに影響が出てしまう可能性はゼロではありませんでした。

ここで、先ほどの修正後の実装に戻ります。
先ほど紹介した修正後の実装ではリスト内は空でしたが

# 一部の要素を対象とする際の例
# 実際の設定内容は一文字のものがキーとなっている訳ではありません
db_list = ['a', 'b']

上記の様に全ての設定のうち一部の設定をDBから取得する対象と致します。
これにより、いきなり全ての設定をDBへ参照しに行くのではなく、
エンジニア側でハンドリングしながら少しづつDBを参照するようにすることが実現出来ました。

また、全ての設定リリース前に一部の設定を対象としたことで
DBへのアクセスのみならずDBインスタンス上のリソースの使われ方もモニタリング出来ましたため、
事前のキャパシティプランニングした上で構築した際のスペックで充分耐えれることが確認出来ました。
これにより、全体リリースにおける負荷の不安を払拭した上でリリースを行うことが実現出来ました。

続いて、全体リリース前のリスクヘッジについてお話させて頂きます。
先ほどのように一部の設定をDBから取得する対象とする際に、
いきなり本番環境にリリースするのではなく、
本番環境と同じスペックで動いているテスト環境にのみ適用するよう、
db_list内で指定してリリース後の状況をモニタリングすることが行えました。
これにより、修正後の挙動にて不具合が見られる可能性を
限りなく低くした上で本番環境にリリースを行うことが出来ました。

上記のような取り組みを踏まえて、
リカバリーや障害対応を一切行うことなく、
広範囲に渡る大規模改修を事故なくリリースすることが出来ました。

最後に

いかがでしたでしょうか。
実際は他にも工夫した点もありましたが、
本エントリーでは実装内容とリリース計画に絞ってお送りさせて頂きました。
全く同じシチュエーションに出会うことはないかもしれませんが、
皆様のお役に立てれば幸いでございます。
最後までお付き合い頂き、ありがとうございました。