読者です 読者をやめる 読者になる 読者になる

LIVESENSE made*

リブセンスのエンジニアやデザイナーの活動や注目していることをまとめたブログです。

MENU

良いチーム作りが成果創出につながった事例

f:id:livesense-made:20170104150246j:plain

皆さんはご自分のチームが、成果を生み出し続ける「最高に良いチーム」だと思いますか?

最近よくそんなことについて考えを巡らせている、リブセンスの風間です。 ジョブセンスリンクアプリ開発チームで、ディレクター兼プロダクト・オーナー(PO)をしています。 サービスを提供するユーザーさんのこともチームメンバーのことも幸せにしたいと欲張りながら奮闘し、失敗したり喜んだりする毎日を送っています。

昨年11月22日にリブセンス社内で「アプリ大会議」が開催されました。 アプリ大会議とは、ネイティブアプリに関わるリブセンス社員が互いの知見を持ち寄り、楽しく交流を深めて仲良くなることを目的としたLT大会です。 今回は、その中で私が発表した「良いチーム作りが成果創出につながった事例」をご紹介したいと思います。

POになんてなりたくなかった

今なら胸をはって「POの仕事が楽しいです!」と言えますが、実はPO就任した当時は嫌で嫌で…。正直に打ち明けると、とても怖かったんですね。 f:id:livesense-made:20170104154738j:plain   理由は以下2つ。

  • アプリの知見が社内になく、数字を伸ばせる自信がなかったから
  • リーダーの経験がなく、チームをリードできるか不安だったから

それをどうやって良い方向に持っていくことができたか? 今回はテーマを後者の「チームビルディング」に絞って、どんな取り組みをしてきたか、それがどう成果に繋がったかをお話したいと思います。

「自分よりも優秀なメンバーたち」

当時のチームにアサインされたメンバーは、全員が年上で経験/スキル豊富な人たちでした。一方で、POの自分はアプリ社内開発も未経験なら、リーダーになるのも初めてという頼りなさ。 こんな自分が信頼してもらえるだろうか…と強く不安を感じたことを覚えています。しかし、成果をあげて目標をクリアするためにはメンバーの協力が不可欠。気持ちを切り替えて、優秀なメンバーに協力してもらえるリーダーになるためにどうしたらいいか一つずつ考えていくことにしました。

「仲間のことをすきになる」

まずは、皆のことをすきになることに決めました。そうすることで本気で相手を理解し、協力し合いやすくなると考えたからです。 やってみたのはこのあたり。

  1. 接触頻度を増やす(1on1定期開催、理由をつけてランチ等)
  2. 相手の情報を知る(すきなこと、きらいなこと等)
  3. 相手の名前を沢山呼ぶ(リアルでもチャットでも)
  4. 共通体験を増やす(納涼船に乗る/UNOで遊ぶ等)

それぞれ効果を感じましたが、中でも意外とよかったのがUNOです。チームビルディングにUNO?と思われるかもしれませんが、年齢も職種も超えて盛り上がれるUNOは互いの距離を縮めるのに効果抜群。チームの雰囲気が一気によくなって、仕事も進めやすくなりました。その後、新メンバー加入後のUNOはチームの恒例行事になっています。

f:id:livesense-made:20170104163017j:plain

「ひとりひとりの考えを理解する」

次に、メンバーのことをより深く理解するために、1人ずつじっくり話をきかせてもらう時間を作りました。 何が好きで、どんな仕事をしてる時が楽しいか、ここで何を実現させたいか…等を1つずつ尋ねていき、これまでの失敗経験なども教えてもらいます。 そして、教えてもらった内容をもとに、皆の思いを実現させつつ同時にプロダクトも伸ばせる方法がないか考え、一つずつ実行してみることに。実際にどんなことを行ったかは、次のSTEP.1~3でご説明していきます。

STEP.1 戦いにそなえる

f:id:livesense-made:20170104153459j:plain シニアエンジニアさんが一番心配していたのが、リリースを急ぎすぎたことによる今後の運用に耐えられない実装状態でした。まずは私自身が課題を把握するために、専門的な話は「例え話」を使ってもらいながら理解して、修正工数を確保。一部機能を作り直す決断もしました。その結果、改善スピードを妨げる「負」が取り除かれ、機能追加がしやすくなりました。これまであまりここに工数を割くPOがいなかったとのことで、この件はとても喜ばれ、この時に信頼関係ができはじめたように思います。

STEP.2 作戦はみんなで

f:id:livesense-made:20170104150433j:plain 企画に参加したいという声をうけ、企画案はできるだけ早い段階で皆に相談することを徹底しました。「こんなことをやりたいけどどう思う?」と手描き案を共有し、皆の知恵をもらって案を改善することで、開発着手時には「これは僕/私が一緒に考えた企画だ」と思える状態を目指します。その結果、「アプリはPOがつくるもの」→「アプリをつくるのは私たち」という意識が根付き、チームメンバー全員が自分ごと感を持って改善に取り組むようになりました。 また、企画職だけでは考えつかない優れた案がうまれやすくなる、開発側が早めに要件を把握できて開発がスムーズになる等の効果もありました。

STEP.3 見積もりは信頼してまかせる

f:id:livesense-made:20170104155559j:plain 開発方針を考えるのが楽しい、設計が好きだという意見も大切にしました。 私はよく「締切に厳しい」と言われますが、自分から締切日を指定しません。私がやるのは、この施策は何のためにやるのか、次は何が控えているか、目的とロードマップをしっかり伝えて全員に理解してもらうことだけです。いつまでにできるか/どう作るべきかはメンバーを信頼して任せ、開発方針には細かく口出ししないようにしています。(このやり方がいいかどうかは、チームのメンバー構成にもよると思います。)

たまに、そのやり方だとゆるいスケジュールを引いてラクしようとされるんじゃないかと聞かれることもありますが、実際に皆が目的を充分に理解した上で自発的に考えるスケジュールは、毎回ビックリする程スピード感のあるもの。「POに言われたから急ぐ」ではなく、「自分でやるべきだと思った基準で全力でやる」という意識がチーム内に根付いた結果、事業部内の他のチームに驚かれる早さで改善サイクルが回るようになりました。

自分ごと感をもったチームは強い!

こうしてチーム全体が前のめりになって、リリースはどんどん高速になっていき、新しい機能が次々とアプリ内に追加されるようになりました。全員で議論を重ねながら改善を重ねた施策の効果は順調に伸び、季節が変わる頃には当初のチーム目標を大きく超える成果を出すことにも成功しました。

はじめは周囲に心配されていた小さなチームでしたが、少しずつ社内で事例共有させていただく機会も持てるようになり、2016年上半期にはリブセンスベストチーム賞にノミネートして頂くまでに成長しました。 f:id:livesense-made:20170104163055j:plain

さいごに

これらの取り組みの中で、個人的にとても嬉しかった「忘れられない出来事」があります。

弊社では定期的にエンジニア向けのサーベイを実施して、開発メンバーから意見や不満を拾い上げるということをしているのですが、なんとなくそのサーベイ結果を眺めていたある日、回答の中に自分に向けられたメッセージがあることにふと気が付いたのです。

【質問項目】仕事仲間は責任をもって精一杯クオリティの高い仕事をしているか【回答】 自分がアサインした時点でかなり不利な状況を押し付けられても前向きに日々改善を繰り返しているPOは素晴らしい。正直、自分の成績のみを考えたら違う案件や部署にいったほうが正解だと感じる。そうした案件にもかかわらず、また、その分野について詳しい知識を持っていないにも関わらず、ここまでサービスを成長させた手腕と責任感はすごい。

f:id:livesense-made:20170106210954j:plain

我武者羅に走りながらも常に不安を感じていた自分にとって、これはとても心に染み入る言葉でした。

チーム全員で力を合わせて戦うことに懸命になり続けた結果、それが成果につながり、お互いへの信頼や尊敬にもつながったことを実感したこの数ヶ月。今後は、さらに大きな成果を生み続けるチームであれるよう、POとしてもっともっと成長せねばと思います。

さて、そんな私たちジョブセンスリンクアプリ開発チームですが、次に掲げた新たな夢は大きく、目標達成への道のりは険しく、会社への貢献度だってまだまだです。今日も目黒の片隅のビルで、「転職経験の乏しいユーザーにこのUIは使いやすいだろうか?」「この機能は本当にユーザーに役立つだろうか?」と真剣に議論を重ねながら、最高に使いやすい転職アプリを目指して開発をしています。もしもあなたがスマートフォンで気軽に転職活動をしてみたいなとちょうどお考えでしたら、ぜひ一度このアプリを試していただけますと大変嬉しく思います。

play.google.com

リブセンスで働くのは面白そうかもしれないな、という考えが少し頭をよぎったあなたは、ぜひ以下よりお気軽にご連絡ください。社員一同、あなたにお会い出来ることをたのしみにお待ちしております。

recruit.livesense.co.jp

ということで… 最後まで読んでくださり、ありがとうございました。

【本発表スライドを全て見たい方はこちら】

speakerdeck.com

Livesense式 開発合宿マニュアル part1

開発合宿

f:id:taise:20161112151149j:plain

Analyticsグループの大政です。
普段はデータ分析基盤や、レコメンド・エンジンの開発をしています。

開発合宿、やっていますか?
Livesenseでは、仕事の一環で開発合宿をすることもあれば、有志で集まって週末に開発合宿をすることもあります。
これまで5回ほど有志で開発合宿を行ってきた経験を元に、開発合宿を成功させるためのノウハウをお話したいと思います。

今回は、(1)なぜ開発合宿をやるのか、(2)どんなスタイルがあるのか、(3)おすすめの施設はどこかについてご紹介したいと思います。

続きを読む

レガシーコードの最適化とPHPバージョンアップ

PHP 開発 運用

f:id:boscoworks:20161021132156p:plain

 少し前のことになりますが、正社員転職サービス「ジョブセンスリンク」を構成するPHPアプリケーション群のPHPバージョンアップ対応と、それに合わせてレガシーコードの大幅な整理を行いました。
 「PHPのバージョンあげて、リファクタリングしたんだ」と一言で言えば簡単ですが、日々のサービス改善を滞らせず、システムのリニューアルを同時に進めていくのは多大な労力を要しました。
 今回はその仕事を主に担当した、キャリア事業部技術基盤チーム*1の海野がお届けします。お手柔らかにどうぞ。

ミッション

 PHPのバージョン問題。レガシーコードの山積。システムが歳を重ねるにつれ、必ず直面する大きな問題です。
 システムは、初めてリリースされた数年前の数倍の規模になっているでしょう。
 売上を支えるシステムを維持し、事業を加速させる施策を阻むこと無く、システムのリニューアルを進める。これが今回のミッションとなりました。
 このミッションを進める上で解決したかったのは、以下の2点です。

  • PHPバージョンアップ
  • 全社共有ライブラリの最適化

 今回は、この課題に対する取り組みについてお話します。

*1:ジョブセンスリンクのインフラ管理やシステム開発を主に担当しています

続きを読む

ReduxにおけるGlobal stateとLocal stateの共存

フロントエンド Redux 開発 React.js

初めまして!エンジニアの米山と申します。 転職会議ではフロントエンド開発にReact.jsとReduxを利用しています。 今回はReact, Redux開発におけるGlobal stateLocal stateという考え方について、軽く紹介させていただきます。

Redux開発の難点

ReduxはSingle source of truthという原則を採用しており、アプリケーションの状態は1つのオブジェクトに格納されます。それゆえ、アプリケーションの状態が散らばることなく管理が楽になります。

ただし、その弊害としてstateが肥大化します。stateが肥大化すると、reducerが肥大化する可能性が高まります。

対応策としては、reducerを分割したりNormalizrのような便利なツールを使う方法が考えられます。

しかし、React自身が提供するState管理を併用することで、Reduxの責務範囲をより適切化できる可能性があります。そして結果的にstateの肥大化を多少は抑制できるかもしれません。

それがGlobal stateとLocal stateの共存です。

Global stateとLocal state

今回の記事は以下のディスカッションを参考にさせていただきました。

Redux and global state vs. local state

Global state, Local stateという考え方は上記のディスカッションで使われているものであり、この記事はその紹介、という形になります。その上で事例を作成してみて、所感を載せています。

気になる方はぜひ元記事をご覧ください。

Global stateとは

Global stateは単にReduxの管理下に置かれるstateです。

どこからでも参照できるという意味でのGlobalではなく、アプリケーションレベルで管理される程度の意味合いですね。

Local stateとは

一方でGlobal stateではない(Reduxの管理外であるような)stateをLocal stateと呼びます。

最もカンタンな例は、React.Componenrtを継承したクラスを使って管理される状態でしょう。

Local stateの例

Local stateの例としては、コンポーネントに閉じるような状態が挙げられるかなと思います。

たとえば、あるコンポーネントがクリックされた回数を考えます。もしこのクリック回数という状態が、コンポーネントの中でしか使われない(たとえばrender関数内で表示するのみ)としたら、Reduxで管理せずとも良さそうです。

逆に言うと、クリック回数がアプリケーション全体に影響するならReduxに任せるべきかもしれません。

実例で見るGlobal stateとLocal stateの共存

では、ここからはGlobal stateとLocal stateの共存方法について、実例を見ていきたいと思います。

前提環境

まずは、React + Reduxによるアプリケーション作成環境を整えます。

お試しに使う程度なので、こちらのボイラープレートを使うと良いでしょう。

TrySpace/simple-redux-boilerplate

$ git clone git@github.com:TrySpace/simple-redux-boilerplate.git

内容はシンプルなカウンタアプリケーションで、最低限のRedux stateフローが作成されています。

$ git cloneが終わったら、ディレクトリをリネームしておくと良いでしょう。

$ mv simple-redux-boilerplate local-state-trial

パッケージをインストールして、npm startコマンドを打てば、開発に入ることができます。

$ cd local-state-trial
$ npm i
$ npm start

http://localhost:3000/ にアクセスすると、以下のようなアプリケーションが立ち上がります。

f:id:livesense-made:20160926162230p:plain

マイナスあるいはプラスのボタンをクリックすると、カウンタがデクリメントあるいはインクリメントされます。

Global state

Global stateについては、今回は既に作成済です。

// ./src/reducers/counter.js
export default function counter(state = 0, action) {
  switch (action.type) {
    case INCREMENT_COUNTER:
      return state + 1;
    case DECREMENT_COUNTER:
      return state - 1;
    default:
      return state;
  }
}

カウンタの状態に関して、Reduxが管理を行っています。なのでpropsを通じて、他のコンポーネントから現在のカウント数を参照することができます(Local stateの場合は、その子コンポーネントからのみ参照可能)。

Local state

では、Local stateを作成していきましょう。 ここでは、カウンタの表示・非表示を切り替える機能を付けてみます。表示であるか非表示であるかがここでのstateですね。

こちらが現在のカウンタコンポーネントです。

import React, { Component, PropTypes } from 'react';

export default class Counter extends Component {
  constructor(props, context) {
    super(props, context)
  }

  handleIncrement() {
    this.props.actions.increment()
  }

  handleDecrement() {
    this.props.actions.decrement()
  }

  render() {
    return (
      <div className="counter-container">
        <div className="counter-num-label">{this.props.counter}</div>
        <div className="counter-even-label">{this.props.counter % 2 === 0 ? 'even' : 'odd'}</div>
        <br />
        <div className="counter-buttons">
          <button onClick={() => {this.handleDecrement();}}>-</button>
          <button onClick={() => {this.handleIncrement();}}>+</button>
        </div>
      </div>
    )
  }
}

Counter.propTypes = {
  counter: PropTypes.number.isRequired,
  actions: PropTypes.object.isRequired
}

まあ、普通のReactコンポーネントですね。

ちなみに、Koba04さんの「Reactの最新動向とベストプラクティス」によりますと、最近では状態を持たないコンポーネントはStatelessFunctionalComponent(ステートレス ファンクショナル コンポーネント)を使うと良いらしいです。

import React from 'react'

const Hoge = (props) => {
  return (
    <div>
      {props.hoge.body}
    </div>
  )
}

export default Hoge

そしてもし、コンポーネントが状態を持つようになったら、React.componentを継承したclassを使おうねとのこと。

import React from 'react'

class Hoge extends React.Component {
  constructor(props, context) {
    super(props, context)
    this.state = {
      hoge: {
        body: '本文'
      }
    }
  }

  render() {
    return (
      <div>
        {this.state.hoge.body}
      </div>
    )
  }
}

なので、Local stateを扱うには、StatelessFunctionalComponentではなく、React.Componentを継承したクラスを用います。

では、先程のカウンタコンポーネントに、表示・非表示のLocal stateを持たせてみます。

export default class Counter extends Component {
  constructor(props, context) {
    super(props, context)

    // Local stateを定義
    this.state = {
      isVisible: true
    }
  }

  // 表示・非表示の切り替えアクション
  toggleVisibility() {
    this.setState({
      // 表示・非表示を反転
      isVisible: !this.state.isVisible
    })
  }

  handleIncrement() {
    this.props.actions.increment()
  }

  handleDecrement() {
    this.props.actions.decrement()
  }

  render() {
    // 表示 or 非表示
    const isVisible = this.state.isVisible ? 'block' : 'none'
    return (
      <div className="counter-container">
        {/* ↓ 表示・非表示stateに応じて、CSSで表示を操作 */}
        <div className="counter-num-label"  style={{display: isVisible}}>{this.props.counter}</div>
        <div className="counter-even-label">{this.props.counter % 2 === 0 ? 'even' : 'odd'}</div>
        <br />
        <div className="counter-buttons">
          <button onClick={() => {this.handleDecrement();}}>-</button>
          <button onClick={() => {this.handleIncrement();}}>+</button>
        </div>
        
        {/* 表示・非表示の切り替えアクションを発行するボタン */}
        <button onClick={() => {this.toggleVisibility() }}>
          {this.state.isVisible ? '閉じる' : '開く'}
        </button>
      </div>
    )
  }
}

コード中にコメントを書きましたが、以下、個別に見ていきます。

Local state定義

contractor 関数内でLocal stateを定義します。

constructor(props, context) {
  super(props, context)

  // Local stateを定義
  this.state = {
    isVisible: true
  }
}

これはReact, Reduxというより、JavaScriptのclassの話ですね。class内から、this.stateの形で参照することができます。

参照: MDN - class

Local state変更アクションの定義

Local stateを変更するアクションを定義します。

// 表示・非表示の切り替えアクション
toggleVisibility() {
  this.setState({
    // 表示・非表示を反転
    isVisible: !this.state.isVisible
  })
}

toggleVisibilityが呼ばれると、setStateが行われてカウンタコンポーネントが再レンダリングされます。

参照: Component API - setState

Local stateをrender関数内で利用

class内で定義したLocal stateとactionをrender関数内で利用します。

render() {
  // 表示 or 非表示
  const isVisible = this.state.isVisible ? 'block' : 'none'
  return (
    <div className="counter-container">
      {/* ↓ 表示・非表示stateに応じて、CSSで表示を操作 */}
      <div className="counter-num-label"  style={{display: isVisible}}>{this.props.counter}</div>
      <div className="counter-even-label">{this.props.counter % 2 === 0 ? 'even' : 'odd'}</div>
      <br />
      <div className="counter-buttons">
        <button onClick={() => {this.handleDecrement();}}>-</button>
        <button onClick={() => {this.handleIncrement();}}>+</button>
      </div>
      
      {/* 表示・非表示の切り替えアクションを発行するボタン */}
      <button onClick={() => {this.toggleVisibility() }}>
        {this.state.isVisible ? '閉じる' : '開く'}
      </button>
    </div>
  )
}

コードについては以上です。 コンポーネント内に閉じたstate及び、actionを作成することができました。要は素のReactの世界ですね。これがReduxのGlobal stateと共存しています。ポイントは、お互いが干渉すること無く共存している点ですね。

挙動

きちんとGlobal state(カウンタのインクリメント・デクリメント)と、Local state(カウンタの表示・非表示)が共存しているのがわかります。

f:id:livesense-made:20160926162321g:plain

状態管理をRedux外に漏らすことについて

Reduxの公式FAQに、以下のような項目があります。

Do I have to put all my state into Redux? Should I ever use React's setState()?

アプリケーションの状態管理を全てのReduxに任せるべきか、あるいはReactのsetStateも使っていくべきなのか。というものです。

返答としては、以下の通り。

There is no “right” answer for this. (中略) Find a balance that works for you, and go with it.

正しい答えはないので、良い塩梅を自分で探ってみてね。とのこと。 Reduxサイドとしても「全ての状態管理をReduxで行うべき」とは考えていないようです。

2016/09/29 追記:

Reduxの作者のDan AbramovがReduxの用法についてMedium.comに記事を投稿していました。

You Might Not Need Redux - Medium.com

Reduxを使わずともReduxのアイデアを取り入れることはできるし、 Local state is fine. とのことです。

Local stateを実現するツール

先程のリンクの中で、Local stateを実現するためのツールがいくつか挙げられています。

ただしどれもdecoratorを使う必要があるので、慣れていない人は今回紹介したような手法でもいいのかなと思います。

Local stateを使うべき時

では、どんなときにLocal stateを使うべきなのでしょうか。 個人的には、UIに関わる状態はLocal stateに向いていそうだなと思います。例としては、

  • 上下キーによるセレクトボックスのカーソル移動
  • レビューの評点スターhoverイベント

などが、コンポーネントに閉じたstateとして適していそうです。以下は、転職会議のUIイベントをスクショしたものです。

f:id:livesense-made:20160926162444g:plain

https://jobtalk.jp/

これはUI上の描画に関わる状態であり、Local stateとしてコンポーネント内で利用するのが良いでしょう。

一方で、ドメインやビジネスロジックに関わる状態は、ReduxでGlobal stateとして管理するのが良さそうです。

また、Local stateとして定義したものが、実はGlobal stateとして定義するべきだった…ということもあるかもしれません。状態がGlobalで持つべきものなのかLocalで持つべきものかについては、チーム内で話し合うと良いかもしれません。

締め

今回はReduxでGlobal stateとLocal stateを共存させる方法について書かせて頂きました。

Global state, Local stateと偉そうな名前を使いましたが、要はReduxと素のReactを一緒に使っているだけですね。ただし、Redux初心者の自分としてはなるほどなあという気持ちでした。

適宜Local stateを用いることで、Reduxの難点の1つであるState, Reducerの肥大化を抑制できるかもしれませんね。 React・Reduxアプリケーションを作成する上で、参考になりましたら幸いです。

Terraform & Packer での運用におけるサーバの構成変更

AWS Insfrastructure as Code Terraform インフラ 開発 運用 Packer

こんにちは、エンジニアの野本です。先日、door 賃貸をオンプレから AWS に移行した際、Terraform & Packer を中心に行ったという話を紹介しました。サーバの構成変更が必要な場合は Packer & Ansible で AMI を再作成、Terraform にて入れ替えるといういわゆる Immutable Infrastructure な運用をしているのですが、基本的には無停止でオンラインで入れ替える必要があり、それを行うのに工夫している部分をご紹介します。

Packer / Terraform による構成管理

Packer による AMI の作成

Packer の build にて、Ansible を利用して以下のようなものをプロビジョニングするようにします。

  • 各種ミドルウェアとその基本的な設定
  • 監視 / ログ収集 / デプロイのエージェント

この際、ステージング環境や本番環境に依存するような設定は、実際の起動時に cloud-init で設定を注入出来るように設定ファイルのテンプレートを配置しておくなどの対応をして、その環境の差分をなくすように構築するようにしておきます。

Terraform でのインスタンス起動時の user-data の利用

Packer にて作成した AMI を元にインスタンスを起動しますが、先の Packer のところで説明したように、環境が決まって初めて決定される設定項目は cloud-init を利用して設定を行います。 cloud-init で実行する基本的な user-data を template_file (0.7 以降であれば data として) で作成、Terraform の実行時に環境毎の vars で設定してある内容で user-data を生成するようにします。

このように Packer と Terraform の設定を行い、ステージングでも本番環境に対して行うのとほぼ同等のオペレーションを実施 / 検証出来るようになっています。

Terraform でのサーバの入れ替えの為の設定 / 作業

Auto Scaling グループに対する ELB 付け外しの利用

現在の Auto Scaling グループは ELB の付け外しが出来るようになっているので、これを利用します。Auto Scaling は blue / green の2系統を用意、ELB も本番用 / 確認用の2つを用意しています。

基本的な Terraform の設定はこちら。各設定は説明のために簡略化されてます。

autoscale.tf
resource "aws_launch_configuration" "blue" {
  image_id = "${var.blue_ami_id}"
  instance_type = "${var.blue_instance_type}"
  user_data = "${template_file.user_data_blue.rendered}"
  ...
}

resource "aws_autoscaling_group" "blue" {
  launch_configuration = "${aws_launch_configuration.blue.id}"
  min_size = "${var.blue_min_size}"
  max_size = "${var.blue_max_size}"
  load_balancers = ["${var.blue_elb_name}"]
  ...
}

resource "aws_launch_configuration" "green" {
  image_id = "${var.green_ami_id}"
  instance_type = "${var.green_instance_type}"
  user_data = "${template_file.user_data_green.rendered}"
  ...
}

resource "aws_autoscaling_group" "green" {
  launch_configuration = "${aws_launch_configuration.green.id}"
  min_size = "${var.green_min_size}"
  max_size = "${var.green_max_size}"
  load_balancers = ["${var.green_elb_name}"]
  ...
}
elb.tf
resource "aws_elb" "prd" {
    name = "elb_prd"
    ...
}

resource "aws_elb" "test" {
    name = "elb_test"
    ...
}
codedeploy.tf
resource "aws_codedeploy_app" "app" {
  name = "app"
}

resource "aws_codedeploy_deployment_group" "app" {
  app_name = "${aws_codedeploy_app.app.name}"
  deployment_group_name = "app"
  autoscaling_groups = [
    "${aws_autoscaling_group.blue.id}",
    "${aws_autoscaling_group.green.id}"
  ]
  ...
}
variables.tfvars
blue_ami_id = "ami-12345xyz"
blue_min_size = "2"
blue_max_size = "4"
blue_elb_name = "elb_prd"

green_ami_id = "ami-12345xyz"
green_min_size = "0"
green_max_size = "0"
green_elb_name = "elb_test"

参考にしたものはこちら

また、今回は blue green deploy の話ではないのですが、CodeDeploy を利用して blue / green のAuto Scaling ともに同じ deployment group を設定しておくと、新しく構築した系統のサーバにアプリケーションが自動的にデプロイされて便利です。

実際のオペレーションの手順

前述のように、同一の AMI でステージングでその AMI の検証と Terraform でのオペレーションの確認を行えるようになっています。なので、以降の手順は基本的にはステージングで全く同じ手順で確認作業を行い、問題がなければまた本番で同じ手順で作業を行うという運用を行っています。 ここでは、blue が稼動中の以下のような状態からどのような動きになるのか説明していきます。

f:id:livesense-made:20160905130036p:plain

各手順においての主な作業は、各手順の下部に記載している variables.tfvars の変数を変更し terraform apply で適応するというものになります。

1. green の設定変更 / 起動

green の台数を指定して、green サーバを起動します。この際、Auto Scaling と Code Deploy の連携により前回成功時のアプリケーションが自動的に green にデプロイされます。

この時はまだ本番の ELB ではなく確認用の ELB にぶら下がっています。起動とデプロイノ完了を確認したら確認用の ELB に対して動作確認を行います。

f:id:livesense-made:20160905130048p:plain

# variables.tfvars

blue_ami_id = "ami-12345xyz"
blue_min_size = "2"
blue_max_size = "4"
blue_elb_name = "elb_prd"

green_ami_id = "ami-98765abc"       # 新しい AMI を設定
green_min_size = "2"                # AutoScale の台数を設定し EC2 を起動
green_max_size = "4"                # 同上
green_elb_name = "elb_test"         # 確認用の ELB を設定
2. green サーバ群を本番 ELB に設定

正常に動作することが確認出来たら、green サーバ群を本番 ELB に設定します。ここでも Terraform の変数化した ELB の設定箇所を変更し apply します。完了したら green サーバ群でエラーが起っていないか確認を行います。

f:id:livesense-made:20160905130100p:plain

# variables.tfvars

blue_ami_id = "ami-12345xyz"
blue_min_size = "2"
blue_max_size = "4"
blue_elb_name = "elb_prd"

green_ami_id = "ami-98765abc"
green_min_size = "2"
green_max_size = "4"
green_elb_name = "elb_prd"       # ELB を本番に
3. blue サーバを本番 ELB から切り離す

エラーもなく問題がなければ blue サーバ群を本番 ELB から切り離します。green サーバのみになった後もエラーが起きていないかを確認。問題があれば blue をまた ELB に戻すよう設定を行います。

f:id:livesense-made:20160905130112p:plain

# variables.tfvars

blue_ami_id = "ami-12345xyz"
blue_min_size = "2"
blue_max_size = "4"
blue_elb_name = ""              # blue の ELB を無効に

green_ami_id = "ami-98765abc"
green_min_size = "2"
green_max_size = "4"
green_elb_name = "elb_prd"
4. blue の台数を 0 に

green サーバ群のみで問題がなければ移行は無事完了ということで blue サーバ群を退役させます。

以降は green サーバ群が稼働中となり、次回入れ替え時は blue を green に置き替えて 1. からまた作業を実施します。

f:id:livesense-made:20160905130122p:plain

# variables.tfvars
blue_ami_id = "ami-12345xyz"
blue_min_size = "0"             # EC2 を起動しないように
blue_max_size = "0"             # 同上
blue_elb_name = ""

green_ami_id = "ami-98765abc"
green_min_size = "2"
green_max_size = "4"
green_elb_name = "elb_prd"

実際に運用してみて

  • Terraform の変数と AWS の各種機能の連携で導入はさほど難しくない
  • ステージング / 本番で全く同様の手順で操作出来る環境を作っておくことで、作成した AMI のテストや Terraform の挙動などの確認も行える
  • 段階を踏んで入れ替えることでより安全に作業を行える

課題や今後

  • terraform apply する回数が多い / 自動化しづらい

    • 各手順において確認を行いたいので今のところ無理に自動化はしていないがやはり若干の手間は感じている
  • blue green 'deployment'

    • 本サイトのデプロイ頻度などの要因を顧み、平常時のデプロイにおいて blue / green のデプロイは現在行っていない
    • blue / green のデプロイを行える基盤は出来ているので、大幅な改修のデプロイ時には活用出来る
  • Packer build の時間がかかる

まとめ

この仕組みで移行後しばらく運用し、実際に入れ替え作業も何度も行っていますが、問題なく行いたい作業は実現出来ています。カジュアルにサーバを捨てたり入れ替えたり出来る仕組みがあるのは、何か問題があった際は準備してあるサーバを入れ替えてしまえば良いという運用が出来るので、心理的に大分楽になったと思います。

また、クラスメソッドさんのこちらの記事でも同様のオペレーションを実現しており、大変参考になると思います。

以上 Terraform & Packer を使った運用の一部を紹介させていただきました。何かのご参考になればと思います。