エムスリーテックブログ

エムスリー(m3)のエンジニア・開発メンバーによる技術ブログです

デプロイの度に障害が起きるシステムを安全にした話

f:id:doloopwhile:20180808130745j:plain
鉄道では個人の注意力だけでなくシステムにより安全を確保している。 写真は「タブレット閉塞式」のタブレットを交換する様子。1つの区間にはタブレットを持った列車しか進入できないため、衝突事故を防ぐことができる。(作者 Spbear [CC BY-SA 3.0 ], ウィキメディア・コモンズより)

こんにちは、エムスリーでソフトウェアエンジニアとして働いている小本です。

私は基盤開発チームという、エムスリーの複数のサービスにまたがって使われるシステムを開発・運用するチームに所属しています。 基盤開発チームが担当するシステムの1つに、会員向けメルマガの配信システム「メールコンシェルジュ」があります1。

エムスリーはメールコンシェルジュで1日数十万通のメルマガを配信しており、機械学習でメルマガを最適化する施策2などもメールコンシェルジュの存在が前提になっています。

このようにエムスリーにとって重要なシステムなのですが、メールコンシェルジュは数年前まで運用、特にデプロイに問題を抱えていました。デプロイ時のミスによる配信障害が頻繁に発生し新人がデプロイすると必ず障害を起きるという状況だったのです。また、1回デプロイするのに30分かかり、デプロイ中はメルマガの配信ができませんでした。

「怖すぎるデプロイ」の辛さ

言うまでもなく、障害が発生している間はメルマガを配信できません。特に「今配信すること」が重要なニュース系メルマガは「障害が解消してから送り直す」ことができず、大きな機会損失に繋がります。また、障害対応に時間を取られれば、そのぶん開発に割ける時間が少なくなります。

それだけではなく、エンジニアにデプロイに対する恐怖心が刷り込まれていました。デプロイしたくないので管理画面の表示崩れのような簡単な不具合修正すらためらったり、複数の変更を一つにまとめてリリースしたりしていました。

しかし「Githubは1日に175回デプロイした、Amazonは1時間に1079回デプロイした って話なのに3、エムスリーでは1回30分もかかるなんておかしいよ!!」と思いました。デプロイはそれ自体は価値を産まない作業ですから、「障害の恐れがある危険な作業」ではなく「誰でもできる簡単なルーチンワーク」であるべきです。

基盤開発チームでは、メールコンシェルジュ本体の開発のかたわら、デプロイの改善も行なっていきました。

デプロイ手順書の作成

まず、ともあれ新人がデプロイすると必ず障害が起きるという状況を何とかすることにしました。そもそも、なぜデプロイ作業中にミスをするかというと、手順書がこんな簡単なメモがあるだけだったからです:

サーバーにSSHする
cd /app/mail_concierge/mail_concierge/
git pull origin master
unicornを再起動
テスト配信する

例えば単に「テスト配信する」と書かれても手順書としては役立ちません。配信には複数のオプションがあり、ベテランはともかく新人には、どんな設定でテスト配信すればいいか分からないからです。

新人でも間違えることが無いよう、全ての設定項目を指定し、コピペするだけでデプロイできるような手順書に書き換えました。「テスト配信」なら以下のような感じです:

1.  mail-contents.xlsをダウンロードする
2. 「事前生成コンテンツ」画面から mail-contents.xls をアップロードする
  - この時「日付」は今日を指定する
3. 「配信登録」画面から以下の内容を入力し、「配信登録」する
  - サービス: 「配信テスト」
  - メルマガ: 「配信テスト/HTML」
  - メールテンプレート:「デプロイ時試験配信1」「デプロイ時試験配信2」
  - 配信リスト: 「配信リストの種類を選択 - 配信テスト用リスト」
  - 配信名: 「デプロイ時試験配信/YYYYMMDD」
  - プレ配信率: 「50%」
  - 勝者決定ロジック: 「開封分のクリック率」
  - プレ配信開始日時: 現在時刻から5分後
  - 本配信開始日時: 現在時刻から10分後
  - 配信対象ランク: 「配信対象ランクを指定する - NO」

これで、本番環境へのデプロイは(一応)誰でも安全にできるようになりました。

ステージング環境の追加

次に、デプロイ作業の自動化をしたかったのですが、デプロイ用のスクリプトをテストできないという問題がありました。

本番環境の他に開発環境があったのですが、あくまで開発用であるため、本番との差異がありました。 例えば、本番はサーバー4台構成なのに開発環境は1台でした(かつてはサーバーのコストが高かったため、止むを得ない部分もありますが)。

エイヤッ!と、本番環境で実行することもできます。しかし、それで障害が起きてしまっては本末転倒です。

結局、複数台構成のステージング環境を用意しました。

実際に本番同様のステージング環境を作ってみると、開発の際にも本番同等の環境の方が便利だと分かりました。 今では、ステージング環境を3つに増やし、かつての開発環境は廃止しています。

デプロイの自動化

手順書の内容を実行するシェルスクリプトを作りました4。

コマンドを1回実行するだけで、各サーバーにSSH、最新コードのチェックアウト、インストール、サーバー再起動、簡単な動作検証、までが完了します。

これによる心理的効果は莫大でした。今までは「よしやるぞ!」と気合いを入れ手順書を見ながら慎重にしていた作業が、「あ〜あ、面倒くせえな ポチポチ 」で出来るようになったんですから。

デプロイの改良

手順書やスクリプトを作り目に見える形にしたことで、デプロイ手順について議論できるようになりました(今までは、議論しようにもできなかった)。

時間短縮

メルマガを配信できない時間を減らすため、スクリプトを改良していきました。デプロイの大部分は、Gitレポジトリの clone やRubyライブラリのインストールだったため、以下のような改善をしました。

  • git のミラーリングレポジトリを利用する
  • Ruby のライブラリをキャッシュする
  • ライブラリのインストールはバックグランドで行い、インストールが終わってから、本体の切り替えをする

これにより、デプロイ時間(= メルマガが配信できない時間)を短くすることができました。

DBへのデータ投入で psql をやめる

自動化によりRubyのコードのデプロイは安全・簡単になりましたが、たまに行う作業の中には危険なものが残っていました。 一つは、メルマガを追加したりする時などにDBの内容を変更する作業です。かつてはそれを、

「サーバーにSSH → psql を開く→ スニペットからSQLをコピペして実行」

という原始的な方法で行なっていました。そのせいで事故も多く、当時「DB変更は本質的に危険な作業なんだ」と言われていました。 しかし、DB変更も「よくある作業」なのですから、できるようにするべきでしょう。

まず、seed_fu を導入しました。seed_fu はRails標準の rails db:seed の強化版です。冪等性があり、繰り返し実行してもデータが壊れません。いわゆるマスターデータの変更は、seed_fu で管理します。

また、seed_fuを使えない場合もpsql ではなく、SQLを実行するrake タスクをデプロイごとに作るようにしました。

namespace :manual_rollout do
  desc 'マージリクエスト 793'
  task mr793: :environment do
    raise '本番環境でのみ実行可能です' if !Rails.env.production?

    helper = RolloutHelper.new('mr793')

    # メルマガの追加
    sql = Rails.root.join('db/sql/20180523_add_mail_magazine_patient_support_by_mail_address.sql').read
    helper.execute_sql(sql)
  end
end

seed-fu や rake を使うようになったことで、デプロイ時にはコマンドを1つ実行するだけで済み、ミスは起きなくなりました。

また、今までデプロイ時に実行するSQLは、Gitで管理していませんでした(どうせ1回しか使わないので)。 しかし、seed-fu の設定ファイルやrakeタスクは、Rubyのコードと一緒にGitで管理するようにしたので、

  • コードレビューしやすい
  • 過去の履歴をさかのぼれる
  • git checkout するだけで、サーバー上にコピーできる

という、Rubyと同じメリットを得られるようになりました。

Rubyのコードと同じくらいの安全・便利にDB変更が出来るようになりました。

crontabを編集するのをやめる

もう一つ「たまに行う危険な作業」がありました。crontab の変更です。

メールコンシェルジュのバックグラウンドジョブは cron で管理しています5。 ジョブの実行頻度を変えたりするときは、crontab を変更しなければなりません。しかし、これが辛かった。

# crontab を変更するとき
1. crontab -e でエディタを開く
2. crontab の所定の部分を削除する
3. config/mail-concierge-web1.crontab の内容を貼り付ける

のような、素朴な手作業で「コピペミスで文法エラーになり、ジョブが実行されなかった」「間違えてジョブを消してしまった」といった事故が起きました。

そこで、crontab をwheneverで管理するようにしました。 whenever は crontab をRubyのDSLで書け、コマンドで crontab の必要な部分だけ書き換えることができます。

whenever により crontab の更新は簡単になりました。それどころか、デプロイスクリプト中で whenever を実行するようにして、 「crontab の更新」という作業自体が無くなりました。

デプロイが怖くなくなった!!

現在では作業ミスによる障害はほぼ無くなりました。また、デプロイは5分程度で完了し、その間もメルマガを止める必要がありません(GithubやAmazonほどの頻度ではデプロイしていませんが)。また、デプロイが安全になったことで、心置き無く開発に時間を割けるようになり、UI改善や配信性能の向上などの機能追加をできるようになりました。

まとめ

メールコンシェルジュでは、

  • デプロイ手順書を作った
  • ステージング環境を作って、デプロイ自動化をテストできるようにした
  • シェルスクリプトでデプロイを自動化した
  • 各種ツールでDBã‚„crontabの変更も簡単・安全にできるようにした
  • その結果、開発に時間を割けるようになった!!

エンジニアを募集しています!

基盤開発チームでは、メールコンシェルジュ以外にも、 会員認証基盤などのエムスリー全体を支える様々なシステムを開発・運用しています。

また、事業から一歩引いた立ち位置にいるので、新技術導入、開発フローの改善などをリードしてきたいと思っています。 (最近はDockerを導入や、JenkinsからGitlab-CIへの移行などを行いました。)

一緒に働く仲間を絶賛募集中です。お気軽にお問い合わせください。

jobs.m3.com


  1. Ruby on Railsで作られており、DBや社内システムとの連携、開封率やクリック率の計測、A/Bテスト、など商用サービスと遜色無いほど高機能なアプリケーションなのですが、それについては別の機会に紹介します。

  2. このあたりは、別の記事で紹介されると思います。

  3. 2012年時点の数字の話です。アマゾンは現在0.5秒に1回だそうですね。

  4. Capistrano や Fabric を使ってもよかったのですが、当時はチーム内で使った経験がありませんでした。そこで悩むよりは、とっとと自動化して楽をしたい・デプロイを速く安全にしたかったため、シェルスクリプトで書いてしまいました。

  5. はい、cronはイカしてないと思います。今から設計するなら cron にはしませんし、将来は別のスケジューラに変えるかもしれませんが、とにかく今は cron なのです。